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:
@@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user