feat: Agent 全栈升级——模块化重构、Hooks/Skills/Memory/SubAgent/Team 子系统、审计与任务持久化

架构重构:
  - Agent Runtime 由单文件拆为 runtime/ 目录 12 模块(熔断/流式执行/Token预算/文件缓存/权限等)
  - Agent Tools 由单文件拆为 tools/ 目录 20+ 模块(filesystem/astro/memory/skill/subagent/team 等)
  - 解析器体系重构(common.rs 836行变更),各解析器同步升级
  - Download 服务重构(562行),反爬策略强化
  - LLM 客户端重构(266行),流式调用优化

  新子系统:
  - Hooks 生命周期系统(9种事件类型,PreToolUse/PostToolUse 支持输入输出拦截)
  - Skills 双层加载系统(system-reminder 轻量注入 + LoadSkillTool 按需加载,notify 文件监听热更新)
  - Memory 项目记忆管理(类型/提取/去重/衰减/保活/选择策略/护栏 7 模块)
  - SubAgent 上下文隔离子代理运行器(独立 ReAct 循环 + Hook 管道)
  - Team 多智能体团队协作(文件 inbox 通信、lead/teammate 协调)
  - TaskBoard DAG 任务依赖管理
  - Trajectory 会话轨迹、Terminal 终止信号、Autonomous 自主模式、Background 异步通知

  数据库:
  - agent_tasks 表(DAG 依赖模式,blocked_by JSON 数组)
  - agent_audit_log 表(工具调用审计:名称/状态/耗时/输出预览)
  - agent_identity 迁移(消息/审计/任务的 agent_name 归属,agent_team_members 团队注册表)

  API:
  - GET /chat/metrics 聚合指标端点
  - GET /chat/sessions/:id/audit 会话审计查询
  - GET /chat/questions + POST /chat/answer 人机交互问答

  工程:
  - 新增依赖:serde_yaml、notify、glob、walkdir、lru
  - Skills 目录含 methodology/plotting/presentation 三个初始 SKILL.md
  - CLAUDE.md 完整项目架构文档
This commit is contained in:
fmq
2026-06-17 00:14:02 +08:00
parent b1fb884f21
commit 49784739fa
113 changed files with 20253 additions and 2869 deletions
+185
View File
@@ -0,0 +1,185 @@
// src/agent/tools/memory.rs
//
// save_memory 工具 — 让 Agent 可以将重要信息持久化到项目记忆系统。
// 参考 Claude Code memdir 设计。
use async_trait::async_trait;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::info;
use super::{AgentTool, InterruptBehavior, ToolContext, ToolOutput};
use crate::agent::memory::dedup;
use crate::agent::memory::types::MemoryType;
use crate::agent::memory::MemoryManager;
pub struct SaveMemoryTool {
memory_manager: Arc<Mutex<MemoryManager>>,
}
impl SaveMemoryTool {
pub fn new(memory_manager: Arc<Mutex<MemoryManager>>) -> Self {
SaveMemoryTool { memory_manager }
}
}
#[async_trait]
impl AgentTool for SaveMemoryTool {
fn name(&self) -> &str {
"save_memory"
}
fn description(&self) -> &str {
"将重要信息保存到项目记忆系统。记忆会跨会话持久化,在后续会话中自动加载。\
用于保存:用户偏好、研究方法论、项目进展、重要发现、外部参考。\n\n\
不应保存:代码模式、架构详情、Git 历史、调试方案、已记录在 CLAUDE.md 中的内容、\
临时任务状态。即使用户要求保存以上内容,请先询问哪些部分是非预期的。\n\n\
系统会自动检测内容重复和低质量输入。保存前先检查是否有可更新的现有记忆 — 不要写重复项。"
}
fn parameters(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "记忆标识符(短横线命名,如 'user-prefs'"
},
"name": {
"type": "string",
"description": "记忆标题"
},
"description": {
"type": "string",
"description": "简短描述(用于决定何时加载此记忆)"
},
"memory_type": {
"type": "string",
"enum": ["user", "feedback", "project", "reference"],
"description": "记忆类型:user=用户偏好, feedback=用户反馈, project=项目进展, reference=外部参考"
},
"content": {
"type": "string",
"description": "记忆内容(Markdown 格式)"
}
},
"required": ["slug", "name", "description", "memory_type", "content"]
})
}
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
false // 写入操作,不并发安全
}
fn interrupt_behavior(&self) -> InterruptBehavior {
InterruptBehavior::Block // 写入操作不可中断
}
async fn execute(&self, args: serde_json::Value, _ctx: &ToolContext) -> ToolOutput {
let slug = args["slug"].as_str().unwrap_or("");
let name = args["name"].as_str().unwrap_or("");
let description = args["description"].as_str().unwrap_or("");
let memory_type_str = args["memory_type"].as_str().unwrap_or("user");
let content = args["content"].as_str().unwrap_or("");
if slug.is_empty() || name.is_empty() || content.is_empty() {
return ToolOutput::error("slug, name, content 均为必填项");
}
// Validate slug format (kebab-case)
if slug.contains(' ') || slug.contains('/') || slug.contains('\\') {
return ToolOutput::error("slug 不能包含空格、斜杠或反斜杠");
}
let memory_type = match MemoryType::from_str(memory_type_str) {
Some(t) => t,
None => {
return ToolOutput::error(format!(
"无效的 memory_type: {}。有效值: user, feedback, project, reference",
memory_type_str
));
}
};
let mut mgr = self.memory_manager.lock().await;
// ── 写入时门控 ──
// 1. 内容质量检查(仅警告,不拒绝)
let quality = dedup::check_content_quality(content);
let quality_warning = match &quality {
dedup::QualityCheck::TooShort(n) => {
Some(format!("⚠️ 内容偏短 ({} 字符),建议展开说明", n))
}
dedup::QualityCheck::TransientState => {
Some("⚠️ 检测到瞬时状态描述,建议仅保存长期有价值的信息".to_string())
}
dedup::QualityCheck::VagueLanguage(w) => {
Some(format!("⚠️ 检测到模糊语言 '{}',建议使用明确表述", w))
}
dedup::QualityCheck::CodePattern => {
Some("⚠️ 检测到代码片段 — 代码模式不应保存为记忆".to_string())
}
dedup::QualityCheck::Accept => None,
};
// 2. Jaccard 内容重复检测
let duplicate_slug = dedup::find_duplicate_by_content(content, mgr.entries(), 0.70);
// 检查 slug 是否已存在
let is_update = dedup::slug_exists(mgr.memory_dir(), slug);
match mgr.save_memory(slug, name, description, memory_type, content) {
Ok(_) => {
info!("[SaveMemory] 已保存记忆: {} ({})", name, slug);
// 标记主代理已写入,抑制本会话的自动提取
mgr.mark_main_agent_wrote();
// 构建现有记忆清单供 LLM 参考
let manifest = dedup::build_manifest_preview(mgr.entries());
let action = if is_update {
"🔄 已更新"
} else {
"✅ 已保存"
};
let mut message = format!(
"{} 记忆: {} ({}) — 类型: {}",
action, name, slug, memory_type_str
);
// 附加质量警告
let has_quality_warning = quality_warning.is_some();
if let Some(w) = &quality_warning {
message.push_str(&format!("\n\n{}", w));
}
// 附加重复检测信息
if let Some(ref dup_slug) = duplicate_slug {
message.push_str(&format!(
"\n\n💡 检测到与现有记忆 `{}` 内容接近(≥70% 重叠),请考虑更新该文件而非创建新的。",
dup_slug
));
}
message.push_str(&format!("\n\n{}", manifest));
if !is_update && duplicate_slug.is_none() {
message
.push_str("\n\n💡 提示:如有其他重要信息需保存,请继续使用 save_memory。");
}
ToolOutput::success(
message,
json!({
"slug": slug,
"name": name,
"memory_type": memory_type_str,
"is_update": is_update,
"quality_check": has_quality_warning,
"duplicate_detected": duplicate_slug.is_some()
}),
)
}
Err(e) => ToolOutput::error(format!("保存记忆失败: {}", e)),
}
}
}