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
+420
View File
@@ -0,0 +1,420 @@
// src/agent/memory/mod.rs
//
// 项目记忆管理器。
// 参考 Claude Code memdir 设计。
//
// 在 {library_dir}/memory/ 目录下维护:
// - MEMORY.md — 索引文件(最多 200 行,25KB)
// - {slug}.md — 每个记忆一个文件,YAML frontmatter + Markdown 内容
//
// 自动在 Agent 的 system prompt 中注入最近的记忆条目。
// 提供 save_memory 工具供 Agent 写入记忆。
pub mod age;
pub mod decay;
pub mod dedup;
pub mod extraction;
pub mod guardrails;
pub mod selection;
pub mod types;
use std::fs;
use std::path::PathBuf;
use tracing::{info, warn};
use self::types::{entry_from_frontmatter, parse_frontmatter, MemoryEntry, MemoryType};
/// 索引文件最大行数
const MAX_ENTRYPOINT_LINES: usize = 200;
/// 索引文件最大字节数
const MAX_ENTRYPOINT_BYTES: usize = 25_000;
/// 记忆管理器
pub struct MemoryManager {
/// 记忆目录
memory_dir: PathBuf,
/// 已加载的记忆条目
entries: Vec<MemoryEntry>,
/// 自动提取追踪状态
pub extraction_tracker: extraction::ExtractionTracker,
}
impl MemoryManager {
/// 创建并加载记忆。
/// `library_dir` 是项目配置中的 library 目录。
pub fn new(library_dir: PathBuf) -> Self {
let memory_dir = library_dir.join("memory");
let mut manager = MemoryManager {
memory_dir,
entries: Vec::new(),
extraction_tracker: extraction::ExtractionTracker::default(),
};
manager.reload();
manager
}
/// 重新从磁盘加载所有记忆。
pub fn reload(&mut self) {
// 确保目录存在
if let Err(e) = fs::create_dir_all(&self.memory_dir) {
warn!("[Memory] 无法创建记忆目录 {:?}: {}", self.memory_dir, e);
return;
}
self.entries.clear();
// 扫描 .md 文件(排除 MEMORY.md 和目录)
match fs::read_dir(&self.memory_dir) {
Ok(dir_entries) => {
for entry in dir_entries {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let path = entry.path();
if path.is_dir() {
continue;
}
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if file_name == "MEMORY.md" || !file_name.ends_with(".md") {
continue;
}
let slug = file_name.strip_suffix(".md").unwrap_or(file_name);
let mtime = entry
.metadata()
.ok()
.and_then(|m| m.modified().ok())
.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
match fs::read_to_string(&path) {
Ok(raw) => {
let (fields, content) = parse_frontmatter(&raw);
if let Some(mem_entry) =
entry_from_frontmatter(slug, &fields, &content, path.clone(), mtime)
{
self.entries.push(mem_entry);
}
}
Err(e) => {
warn!("[Memory] 无法读取 {:?}: {}", path, e);
}
}
}
}
Err(e) => {
warn!("[Memory] 无法扫描记忆目录: {}", e);
}
}
// 按修改时间排序(最新在前)
self.entries.sort_by(|a, b| b.mtime.cmp(&a.mtime));
info!(
"[Memory] 加载了 {} 条记忆从 {:?}",
self.entries.len(),
self.memory_dir
);
}
/// 获取所有记忆条目
pub fn entries(&self) -> &[MemoryEntry] {
&self.entries
}
/// 获取记忆目录路径
pub fn memory_dir(&self) -> &std::path::Path {
&self.memory_dir
}
/// 标记主代理在本会话中已写入记忆(抑制自动提取)。
pub fn mark_main_agent_wrote(&mut self) {
self.extraction_tracker.main_agent_saved_this_session = true;
}
/// 语义匹配:委托给 selection 模块使用 LLM 结构化选择。
///
/// 选择后应用指数时间衰减排序(更近的 active 记忆获得更高权重)。
/// 失败时回退到 recency-based 选择(跳过已展示的条目)。
pub async fn select_relevant_memories(
&self,
llm: &crate::clients::llm::LlmClient,
context: &str,
max_entries: usize,
) -> Vec<&MemoryEntry> {
if self.entries.len() <= max_entries {
return self.entries.iter().collect();
}
let sel_ctx = selection::SelectionContext {
max_entries,
..Default::default()
};
let indices = selection::select_structured(&self.entries, llm, context, &sel_ctx).await;
// 应用指数时间衰减排序(更近的 active 记忆在前,historical 在后)
let sorted =
selection::apply_decay_scoring(&indices, &self.entries, decay::DEFAULT_HALF_LIFE_DAYS);
sorted.iter().filter_map(|&i| self.entries.get(i)).collect()
}
/// 从指定条目列表构建 system reminder(而非全部条目)
pub fn build_system_reminder_from(&self, selected: &[&MemoryEntry]) -> Option<String> {
if selected.is_empty() {
return None;
}
let mut lines = vec![
"<project-memory-context>".to_string(),
String::new(),
"[PROJECT MEMORY]".to_string(),
String::new(),
];
for entry in selected {
let type_tag = match entry.memory_type {
MemoryType::User => "[偏好]",
MemoryType::Feedback => "[反馈]",
MemoryType::Project => "[项目]",
MemoryType::Reference => "[参考]",
};
// historical 记忆标记
let status_tag = if !entry.status.is_active() {
match entry.status.superseded_by() {
Some(new_slug) => format!(" [已更新→{}]", new_slug),
None => " [已更新]".to_string(),
}
} else {
String::new()
};
let preview: String = entry
.content
.lines()
.take(3)
.collect::<Vec<_>>()
.join("\n ");
lines.push(format!(
"{} {}{}: {}\n {}",
type_tag, entry.name, status_tag, entry.description, preview
));
// 注入时效警告(超过1天的记忆)
let freshness = age::memory_freshness_note(entry.mtime);
if !freshness.is_empty() {
lines.push(freshness);
}
}
lines.push(String::new());
lines.push(
"使用 save_memory 工具保存重要信息。记忆内容可能过时,请在使用前验证。".to_string(),
);
// 注入验证提醒(从记忆推荐前先核实)
lines.push(String::new());
lines.push(guardrails::build_verification_reminder());
lines.push("</project-memory-context>".to_string());
Some(lines.join("\n"))
}
/// 保存一条新的记忆。
///
/// 如果 slug 已存在且内容为 Active,旧文件归档为 `{slug}_v1.md`
/// 并将状态标记为 historical(永不删除旧记忆)。
pub fn save_memory(
&mut self,
slug: &str,
name: &str,
description: &str,
memory_type: MemoryType,
content: &str,
) -> std::io::Result<()> {
let file_path = self.memory_dir.join(format!("{}.md", slug));
// 如果已有活跃版本,归档旧版本
let old_path = self.memory_dir.join(format!("{}_v1.md", slug));
if file_path.exists() {
if let Ok(old_content) = fs::read_to_string(&file_path) {
fs::write(&old_path, &old_content)?;
info!("[Memory] 归档旧版本: {} → {}", slug, old_path.display());
}
}
let frontmatter = format!(
"---\nname: {}\ndescription: {}\ntype: {}\nstatus: active\n---\n",
name,
description,
memory_type.as_str()
);
let full_content = format!("{}{}", frontmatter, content);
fs::write(&file_path, &full_content)?;
// 更新 MEMORY.md 索引
self.update_index(slug, name, description, &memory_type)?;
// 重新加载
self.reload();
info!("[Memory] 已保存记忆: {} ({})", name, slug);
Ok(())
}
/// 更新 MEMORY.md 索引文件。
fn update_index(
&self,
slug: &str,
name: &str,
description: &str,
memory_type: &MemoryType,
) -> std::io::Result<()> {
let index_path = self.memory_dir.join("MEMORY.md");
let line = format!(
"- [{}]({}.md) — {} (type: {})",
name,
slug,
description,
memory_type.as_str()
);
let mut content = if index_path.exists() {
let existing = fs::read_to_string(&index_path).unwrap_or_default();
// 检查是否已有此 slug 的条目
let slug_marker = format!("]({}.md)", slug);
let lines: Vec<&str> = existing.lines().collect();
// 行数检查
if lines.len() >= MAX_ENTRYPOINT_LINES {
// 移除最旧的行(索引头部保持不变)
warn!(
"[Memory] MEMORY.md 行数已满 ({}), 移除最旧条目",
lines.len()
);
let keep = MAX_ENTRYPOINT_LINES - 1;
format!("{}\n{}", lines[..keep.min(lines.len())].join("\n"), line)
} else {
// 检查是否需要替换已存在的条目
let has_entry = lines.iter().any(|l| l.contains(&slug_marker));
if has_entry {
// 替换已存在的行
lines
.iter()
.map(|l| {
if l.contains(&slug_marker) {
line.as_str()
} else {
*l
}
})
.collect::<Vec<_>>()
.join("\n")
} else {
format!("{}\n{}", existing, line)
}
}
} else {
format!("# Project Memory\n\n{}", line)
};
// 字节数检查(在大约 25KB 处截断)
if content.len() > MAX_ENTRYPOINT_BYTES {
let truncated: String = content
.char_indices()
.take_while(|(i, _)| *i < MAX_ENTRYPOINT_BYTES - 100)
.map(|(_, c)| c)
.collect();
content = format!(
"{}\n\n[MEMORY.md 已达到 {}KB 上限,旧条目已截断]",
truncated,
MAX_ENTRYPOINT_BYTES / 1024
);
}
fs::write(&index_path, &content)?;
Ok(())
}
/// 生成 system prompt 中注入的记忆段落。
///
/// 包含最近的记忆条目(最多 10 条),并在前面注明可信度提醒。
pub fn build_system_reminder(&self, max_entries: usize) -> Option<String> {
if self.entries.is_empty() {
return None;
}
let mut lines = vec![
"<project-memory-context>".to_string(),
"".to_string(),
"[PROJECT MEMORY]".to_string(),
"".to_string(),
];
let count = max_entries.min(self.entries.len());
for entry in self.entries.iter().take(count) {
let type_tag = match entry.memory_type {
MemoryType::User => "[偏好]",
MemoryType::Feedback => "[反馈]",
MemoryType::Project => "[项目]",
MemoryType::Reference => "[参考]",
};
// historical 记忆标记
let status_tag = if !entry.status.is_active() {
match entry.status.superseded_by() {
Some(new_slug) => format!(" [已更新→{}]", new_slug),
None => " [已更新]".to_string(),
}
} else {
String::new()
};
let preview: String = entry
.content
.lines()
.take(3)
.collect::<Vec<_>>()
.join("\n ");
lines.push(format!(
"{} {}{}: {}\n {}",
type_tag, entry.name, status_tag, entry.description, preview
));
// 注入时效警告(超过1天的记忆)
let freshness = age::memory_freshness_note(entry.mtime);
if !freshness.is_empty() {
lines.push(freshness);
}
}
lines.push("".to_string());
lines.push(
"使用 save_memory 工具保存重要信息。记忆内容可能过时,请在使用前验证。".to_string(),
);
// 注入验证提醒(从记忆推荐前先核实)
lines.push(String::new());
lines.push(guardrails::build_verification_reminder());
lines.push("</project-memory-context>".to_string());
Some(lines.join("\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_empty_reminder() {
let manager = MemoryManager {
memory_dir: PathBuf::from("/tmp/nonexistent"),
entries: Vec::new(),
extraction_tracker: extraction::ExtractionTracker::default(),
};
assert!(manager.build_system_reminder(10).is_none());
}
}