Files
AstroResearch/src/agent/tools/memory.rs
T
fmq 49784739fa 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 完整项目架构文档
2026-06-17 00:14:02 +08:00

186 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)),
}
}
}