// 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>, } impl SaveMemoryTool { pub fn new(memory_manager: Arc>) -> 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)), } } }