架构重构: - 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 完整项目架构文档
186 lines
7.3 KiB
Rust
186 lines
7.3 KiB
Rust
// 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)),
|
||
}
|
||
}
|
||
}
|