AstroResearch/docs/architecture/agent/memory.md
Asfmq cec4b8cf7b feat: Docker 容器化、Cookie 鉴权、Coordinator 编排、FTS5 搜索与 P1-P3 全面收尾
Docker 容器化部署
  - 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
    两种镜像,Docker Compose 一键启动
  - build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
  - 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制

  安全:Cookie-Based 鉴权系统
  - HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
  - 登录/登出/验证接口 + 中间件注入
  - 前端登录页面 + 退出按钮
  - 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
  - 书签脚本 fetch 添加 credentials:'include'

  Coordinator 模式 (P2)
  - 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
  - WorkerPool + Semaphore 并发控制 + 超时保护
  - 前端协调者模式开关

  Hook 系统:UserPromptSubmit 事件 (P2)
  - 第 13 个生命周期事件,fire-and-forget 审计

  FTS5 全文搜索 (P3)
  - agent_sessions_fts + agent_messages_fts 虚拟表
  - search_history Agent 工具 + /api/search/history HTTP 接口
  - 前端防抖搜索框 + 仅当前会话筛选

  工具加载优化 (P3)
  - defer_loading 延迟加载 (7 个重型工具)
  - is_readonly 只读标记 (9 个查询工具)
  - classifier_summary 工具目录供 LLM 按需判断

  模型回退策略 (P3)
  - LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
  - LlmClient model 改为 Arc<RwLock> 支持运行时切换
  - 连续 3 次过载后自动切换

  压缩记忆桥接 (P3)
  - 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)

  git2 依赖修复
  - 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
2026-06-23 20:22:06 +08:00

491 lines
23 KiB
Markdown
Raw Permalink 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.

# 记忆系统 (`memory/`)
参考 Claude Code `memdir/` 设计,提供**完全基于文件系统**(非数据库)的项目级持久化记忆管理。核心代码位于 `src/agent/memory/`8 个文件)和 `src/agent/tools/memory.rs``save_memory` 工具)。
## 整体架构
```mermaid
graph TD
subgraph Storage["文件存储 ({library_dir}/memory/)"]
MEMORY_MD["MEMORY.md<br/>索引文件 (≤200行, ≤25KB)"]
Files["{slug}.md × N<br/>每个记忆一个 Markdown 文件"]
Archive["{slug}_v1.md<br/>旧版本归档(永不删除)"]
end
subgraph Manager["MemoryManager (mod.rs)"]
Load["reload()<br/>扫描目录 → 解析 frontmatter → 按 mtime 排序"]
Save["save_memory()<br/>写文件 → 归档旧版 → 更新索引 → reload"]
Reminder["build_system_reminder(N)<br/>构建注入 system prompt 的 XML 块"]
SelectRel["select_relevant_memories()<br/>LLM 语义选择 + 指数衰减排序"]
end
subgraph Tool["save_memory 工具 (tools/memory.rs)"]
Validate["参数校验<br/>slug 格式 + memory_type 枚举"]
QualityGate["写入时门控<br/>质量检查 + Jaccard 去重"]
Execute["执行写入<br/>MemoryManager.save_memory()"]
end
subgraph Pipeline["记忆生命周期"]
Extract["extraction.rs<br/>会话结束时自动提取(默认关闭)"]
Dedup["dedup.rs<br/>Jaccard 相似度去重 (≥70%) + 内容质量门控"]
Decay["decay.rs<br/>指数时间衰减 + Hebbian 激活层级"]
Age["age.rs<br/>时效标签 + freshness 警告"]
Guardrails["guardrails.rs<br/>WHAT_NOT_TO_SAVE + VERIFY_BEFORE_RECOMMENDING"]
end
subgraph Types["types.rs — 数据模型"]
MemoryEntry["MemoryEntry<br/>{ slug, name, description, memory_type, mtime, content, path, status }"]
MemoryType["MemoryType: User | Feedback | Project | Reference"]
MemoryStatus["MemoryStatus: Active | Historical { superseded_by }"]
end
Tool --> Manager
Manager --> Storage
Reminder -->|"try_lock() 非阻塞"| Runtime["AgentRuntime System Prompt"]
Extract --> Dedup --> Tool
Decay --> SelectRel
Guardrails --> Reminder
Guardrails --> Tool
```
## 存储结构
记忆**不使用 SQLite**,全部存储在文件系统 `{library_dir}/memory/` 目录下:
```
{library_dir}/memory/
├── MEMORY.md # 索引文件(最多 200 行 / 25KB
├── user-prefs.md # 活跃记忆YAML frontmatter + Markdown 内容)
├── project-goals.md
├── user-prefs_v1.md # 旧版本归档(原文件重命名,永不删除)
└── ...
```
**每个记忆文件**格式:
```markdown
---
name: user-role
description: 用户的科研角色和偏好
type: user
status: active
---
用户是天体物理学博士后,主要研究恒星演化与双星系统。
**Why:** 在对话开始时用户明确说明了研究领域。
**How to apply:** 默认使用天体物理学术语,优先推荐恒星演化相关文献。
```
**版本管理机制**:当更新已有 slug 时,旧文件重命名为 `{slug}_v1.md`,其 frontmatter 中 `status` 改为 `historical` 并记录 `superseded_by` 指向新版本。旧版本**永不删除**——保留完整的事实演进历史。`MemoryStatus` 枚举控制此生命周期:
```
Active ──(更新)──→ Historical { superseded_by: Some("new-slug") }
```
## 核心数据结构 (`types.rs`)
| 结构体/枚举 | 字段/变体 | 说明 |
|:---|:---|:---|
| `MemoryType` | `User` | 用户角色、偏好、知识背景 |
| | `Feedback` | 用户提供的修正或确认的方法论(含 Why 和 How to apply |
| | `Project` | 项目上下文、目标、约束(不可从代码推导的部分) |
| | `Reference` | 外部资源指针URL、仪表盘、工单系统 |
| `MemoryStatus` | `Active` | 当前有效(默认值) |
| | `Historical { superseded_by }` | 已被更新版本取代,`superseded_by` 指向新 slug |
| `MemoryEntry` | `slug: String` | 文件名标识(不含 `.md` 扩展名) |
| | `name: String` | 记忆标题 |
| | `description: String` | 简短描述,用于相关性匹配 |
| | `memory_type: MemoryType` | 记忆类型 |
| | `mtime: u64` | 文件修改时间Unix 时间戳),用于排序 |
| | `content: String` | 不含 frontmatter 的纯正文Markdown |
| | `path: PathBuf` | 文件完整路径 |
| | `status: MemoryStatus` | 生命周期状态(默认 Active |
`parse_frontmatter(raw)` 函数解析 YAML-like frontmatter支持 `name`、`description`、`type`/`memory_type`、`status`、`superseded_by` 字段。缺失 `status` 时默认为 `Active`
## MemoryManager 核心方法 (`mod.rs`)
`MemoryManager` 在应用启动时创建(`main.rs`),存入 `AppState.memory_manager: Arc<tokio::sync::Mutex<MemoryManager>>`
| 方法 | 调用时机 | 行为 |
|:---|:---|:---|
| `new(library_dir)` | 应用启动 | `create_dir_all(memory/)``reload()` 扫描所有 `.md` 文件 → 解析 frontmatter → 按 mtime 降序排序 |
| `reload()` | 每次 `save_memory` 后 | 全量重新扫描目录,清空并重建 `entries: Vec<MemoryEntry>` |
| `entries()` | 查询 | 返回 `&[MemoryEntry]` 不可变引用 |
| `save_memory(slug, name, desc, type, content)` | `save_memory` 工具调用 | ① 检查是否已有活跃版本 → 归档为 `{slug}_v1.md` ② 写入新 YAML frontmatter + content ③ `update_index()` 更新 MEMORY.md ④ `reload()` 刷新内存状态 |
| `build_system_reminder(max)` | System prompt 组装 | 取最近 `max` 条记忆,构建 `<project-memory-context>` XML 块,包含类型标签、时效警告、验证提醒 |
| `build_system_reminder_from(selected)` | LLM 选择后 | 同上,但从指定的条目子集构建 |
| `select_relevant_memories(llm, ctx, max)` | 按需 | LLM 语义匹配 → 指数时间衰减重排序 → 回退到 recency 排序 |
| `mark_main_agent_wrote()` | `save_memory` 工具执行后 | 设置标志抑制本会话的自动提取 |
**关键设计:非阻塞加载**。System prompt 组装时使用 `try_lock()`(非 `lock()`)——如果 mutex 已被持有则直接跳过,不阻塞会话启动。
## `update_index()` 索引维护
`MEMORY.md` 索引文件受双重容量保护:
| 参数 | 值 | 触发行为 |
|:---|:---|:---|
| `MAX_ENTRYPOINT_LINES` | 200 | 行数满时移除最旧条目行 |
| `MAX_ENTRYPOINT_BYTES` | 25,000 (~25KB) | 字节数超限时在约 25KB 处截断,附加截断提示 |
索引格式(每行一条):
```markdown
- [记忆标题](slug.md) — 简短描述 (type: user)
```
写入时执行**原地更新**:若 `MEMORY.md` 中已存在同 slug 的行(通过 `](slug.md)` 标记匹配),则替换该行而非追加。
## `save_memory` 工具 (`tools/memory.rs`)
这是**唯一**暴露给 LLM 的记忆写入工具。
**参数 Schema**
| 参数 | 类型 | 必填 | 校验规则 |
|:---|:---|:---|:---|
| `slug` | string | 是 | kebab-case禁止空格、`/`、`\` |
| `name` | string | 是 | 记忆标题 |
| `description` | string | 是 | 用于决定何时加载此记忆 |
| `memory_type` | enum | 是 | `user` / `feedback` / `project` / `reference` |
| `content` | string | 是 | Markdown 格式,最少有效信息量推荐 >10 字符 |
**工具配置**
| 配置项 | 值 | 原因 |
|:---|:---|:---|
| `is_concurrency_safe` | `false` | 写入操作不可与其他工具并发执行 |
| `interrupt_behavior` | `Block` | 写入磁盘不可中途中断 |
**完整执行流程**
```mermaid
sequenceDiagram
participant LLM as LLM
participant Tool as SaveMemoryTool
participant QG as 质量门控 (dedup.rs)
participant Mgr as MemoryManager
participant FS as 文件系统
LLM->>Tool: save_memory(slug, name, desc, type, content)
Tool->>Tool: ① 参数校验 (slug 非空 + 格式合法 + type 枚举有效)
Tool->>Mgr: ② lock().await 获取互斥锁
Tool->>QG: ③ check_content_quality(content)
QG-->>Tool: QualityCheck (Accept / TooShort / TransientState / VagueLanguage / CodePattern)
Tool->>QG: ④ find_duplicate_by_content(content, entries, 0.70)
QG-->>Tool: 重复的 slug 或 None
Tool->>QG: ⑤ slug_exists(memory_dir, slug) → 判断是新建还是更新
Tool->>Mgr: ⑥ save_memory(slug, name, desc, type, content)
Mgr->>FS: 若已有活跃版本 → 归档 {slug}_v1.md
Mgr->>FS: 写入新 {slug}.md (frontmatter + content)
Mgr->>FS: 更新 MEMORY.md 索引
Mgr->>Mgr: reload() 刷新内存状态
Tool->>Mgr: ⑦ mark_main_agent_wrote() 抑制自动提取
Tool->>QG: ⑧ build_manifest_preview(entries) 构建清单
Tool-->>LLM: success(message + manifest + quality warning + duplicate hint)
```
**返回消息结构**
1. 操作状态(✅ 已保存 / 🔄 已更新)
2. 质量警告(如有:⚠️ 过短 / 瞬时状态 / 模糊语言 / 代码模式)
3. 重复提示(如有:💡 检测到与 `{slug}` 内容 ≥70% 重叠)
4. 当前记忆清单(所有现有条目供 LLM 参考)
## 记忆注入机制
在 Agent 会话启动时system prompt 按顺序组装各 section。记忆注入发生在 **Section 4**(位于 Tools、Skills 之后Core Principles 之前):
```
① 静态身份声明
② 工具定义
③ Skills 提醒
④ <project-memory-context> ← 记忆(通过 try_lock 非阻塞加载,默认 5 条)
⑤ 核心原则
```
注入格式示例:
```xml
<project-memory-context>
[PROJECT MEMORY]
[偏好] 用户角色: 天体物理学博士后,研究恒星演化与双星系统
用户是天体物理学博士后,主要研究恒星演化与双星系统。
**Why:** 在对话开始...
<system-reminder>此记忆已过 3 天。记忆是某个时间点的快照,不是实时状态...</system-reminder>
[反馈] 测试策略: 优先使用单元测试而非集成测试
用户偏好单元测试,认为集成测试太慢...
使用 save_memory 工具保存重要信息。记忆内容可能过时,请在使用前验证。
## 从记忆中推荐前先核实
"记忆说 X 存在" 不等于 "X 现在存在"。
</project-memory-context>
```
记忆列表中每个条目的格式:
```
[类型标签] 标题 [状态标签]: 描述
内容预览(前 3 行)
<system-reminder>时效警告(超过 1 天的记忆)</system-reminder>
```
类型标签:`[偏好]` / `[反馈]` / `[项目]` / `[参考]`。Historical 记忆额外显示 `[已更新→新slug]`
## 写入时质量护栏 (`dedup.rs` + `guardrails.rs`)
**四层内容质量门控**(全部仅警告,不强制拒绝——最终决定权在 LLM
| 检测类型 | 触发条件 | 示例 |
|:---|:---|:---|
| **过短** | 有效字符 < 10 | `QualityCheck::TooShort(n)` |
| **瞬时状态** | "正在做" / "currently" / "at the moment" 11 个关键词 | `QualityCheck::TransientState` |
| **模糊语言** | "可能" / "maybe" / "大概" / "perhaps" 8 个关键词 | `QualityCheck::VagueLanguage(word)` |
| **代码模式** | `fn ` / `impl ` / `struct ` / `import {` / `from "` 12 个模式 | `QualityCheck::CodePattern` |
**Jaccard 相似度去重**使用字符级 bigram支持中文不依赖分词器阈值 70% 即判定为高度重复
- `jaccard_similarity(a, b) = |bigrams(a) ∩ bigrams(b)| / |bigrams(a) bigrams(b)|`
- 单字符内容使用单字符本身作为 bigram
- 仅比对 `status == Active` 的记忆跳过 Historical 条目
- 检测到重复时不阻止写入仅在返回消息中附加 💡 提示
**`guardrails.rs` 护栏两层防护**
1. **`WHAT_NOT_TO_SAVE`** 注入到工具 `description()` 明确告知 LLM 不应保存的内容
- 代码模式架构详情 —— 可从项目状态推导
- Git 历史最近修改 —— `git log` 是权威来源
- 调试方案或临时 workaround —— 修复在代码中commit message 有上下文
- 已在 CLAUDE.md 中的内容
- **规则前置**"即使用户明确要求保存以上内容请先询问其中哪些是非预期的部分"
2. **`VERIFY_BEFORE_RECOMMENDING`** 注入到 system prompt 记忆段落后、`</project-memory-context>` 之前:
- 如果记忆提到了文件路径 → 先确认文件存在
- 如果记忆提到了函数或标志 → 先用 grep 搜索
- 核心原则:**"记忆说 X 存在" ≠ "X 现在存在"**
- 设计依据Claude Code 评估表明此提醒放在独立标题下3/3 通过vs 埋在通用指南中0/3 通过)
## 自动记忆提取 (`extraction.rs`)
在会话结束时(`finalize.rs`)触发,**fire-and-forget** 模式(`tokio::spawn`),不阻塞会话关闭。
```mermaid
sequenceDiagram
participant RT as AgentRuntime
participant Fin as finalize.rs
participant Ext as extraction.rs
participant Sub as SubAgentRunner
participant Mgr as MemoryManager
RT->>Fin: 会话结束 → finalize_turn()
Fin->>Ext: tokio::spawn(run_extraction())
Ext->>Ext: 检查 EXTRACT_MEMORY_ENABLED → false 则 return
Ext->>Mgr: lock().await → 递增 turns_since_last_extraction
alt turns < throttle_turns
Ext->>Ext: return (未达到节流轮次)
else main_agent_saved_this_session == true
Ext->>Ext: 重置 tracker → return (主代理已手动保存)
end
Ext->>Mgr: build_manifest_preview() 获取现有清单
Ext->>Ext: 构建受限 ToolRegistry (ReadFile + Grep + Glob + SaveMemory)
Ext->>Sub: SubAgentRunner.run(system_prompt, prompt, max_steps=3)
Sub-->>Ext: SubagentResult { content, is_error }
Ext->>Ext: 记录日志 (成功摘要 / 错误截断)
```
**环境变量配置**
| 变量 | 默认值 | 说明 |
|:---|:---|:---|
| `EXTRACT_MEMORY_ENABLED` | `false` | 是否启用自动提取(默认关闭,避免意外 LLM 费用) |
| `EXTRACT_MEMORY_THROTTLE_TURNS` | `3` | 最小提取间隔(轮次),避免每轮都触发 |
| `EXTRACT_MEMORY_MAX_STEPS` | `3` | 子代理最大 ReAct 步数,限制提取成本 |
**跳过条件**(任一满足即跳过):
- `EXTRACT_MEMORY_ENABLED != true`
- `turns_since_last_extraction < throttle_turns`(节流未到)
- `main_agent_saved_this_session == true`(主代理已通过 `save_memory` 工具写入)
- 子代理工具集为受限集(仅 4 个只读工具 + `save_memory`),不能执行 bash、不能搜索论文
## 压缩记忆桥接 (`compact.rs`, P3)
上下文压缩compaction丢弃旧消息时其中可能包含有长期价值的科研结论和用户偏好。`extract_memories_from_compaction()` 在压缩后自动从丢弃的消息中提取持久记忆,将"即将消失的上下文"转化为"跨会话可用的记忆"。
```mermaid
sequenceDiagram
participant RT as AgentRuntime
participant Cmp as compact.rs
participant Ext as extract_memories_from_compaction
participant Sub as SubAgentRunner
participant Mgr as MemoryManager
RT->>Cmp: compress_context_with_hooks_and_log(messages, ...)
Cmp-->>RT: 压缩完成,旧消息将从上下文中移除
RT->>Ext: extract_memories_from_compaction(before_messages, sid, mgr, app_state)
Ext->>Ext: 检查 EXTRACT_MEMORY_ENABLED → false 则 return
Ext->>Ext: 过滤非系统消息 → 每条截断 400 字符 → 拼接
alt 拼接文本 < 300 字符
Ext->>Ext: return (太少不值得提取)
end
Ext->>Mgr: lock() → build_manifest_preview() 获取现有记忆清单
Ext->>Ext: 构建受限 ToolRegistry (仅 SaveMemoryTool)
Ext->>Sub: SubAgentRunner.run(EXTRACTION_SYSTEM_PROMPT, prompt, max_steps=3)
Note over Ext,Sub: tokio::spawn (fire-and-forget, 不阻塞压缩/ReAct 循环)
Sub-->>Mgr: save_memory() 写入提取结果
```
**与 `run_extraction()` 的区别**
| 维度 | `run_extraction()` (会话结束) | `extract_memories_from_compaction()` (压缩桥接) |
|:---|:---|:---|
| 触发时机 | `finalize_turn()` 会话收尾 | `compress_and_restore()` 压缩完成后 |
| 数据来源 | 完整对话历史(已持久化到 DB | 压缩前即将被丢弃的消息快照 |
| 消息范围 | 系统 + 用户 + 助手消息 | 仅用户 + 助手 + 工具(去除 system |
| 节流机制 | `ExtractionTracker` 轮次计数 | 仅靠 `EXTRACT_MEMORY_ENABLED` 开关 |
| 阻塞行为 | fire-and-forget | fire-and-forget |
| 子代理工具集 | 4 只读工具 + `save_memory` | 仅 `save_memory`(最小化成本) |
| 最小内容阈值 | 无显式阈值 | 拼接文本 < 300 字符跳过 |
**设计原理**
- 压缩是最佳记忆提取时机——此时旧消息尚未被物理删除但即将从上下文中移除记忆系统可以在"内容消失前最后一刻"将其抢救出来
- 截断每条消息到 400 字符平衡了信息完整性和子代理 prompt 长度
- fire-and-forget 确保压缩不因 LLM 调用延迟而阻塞主 ReAct 循环
## 时效性与衰减系统
**指数时间衰减** (`decay.rs`)
```
score = e^(-λ × days_old)
λ = ln(2) / half_life_days
```
| 参数 | 默认值 | 说明 |
|:---|:---|:---|
| `DEFAULT_HALF_LIFE_DAYS` | 30 | 半衰期 30 0 score=1.0,第 30 score=0.5,第 60 score=0.25 |
| Historical 记忆固定分数 | 0.01 | 已更新记忆始终排在最后 |
**Hebbian 启发式激活层级**
| 层级 | 天数范围 | 权重乘数 | 说明 |
|:---|:---|:---|:---|
| **Hot** | 7 | 1.0 | 最近活跃全额权重 |
| **Warm** | 8-30 | 0.7 | 中等时效7 折权重 |
| **Cool** | > 30 天 | 0.3 | 较久远3 折权重 |
**时效警告** (`age.rs`)
| 函数 | 输出 | 用途 |
|:---|:---|:---|
| `memory_age_label(mtime)` | "今天" / "昨天" / "N 天前" | 人类可读的年龄标签 |
| `memory_freshness_note(mtime)` | 超过 1 天时返回 `<system-reminder>` 警告 | 注入 system prompt 的 XML 标签,利用 system-reminder 对模型的强注意力引导 |
| `memory_freshness_text(mtime)` | 同上但纯文本 | 备选方案 |
设计洞察:**"LLM 对绝对日期 ('2026-01-15') 的时效感知弱,但对相对时间 ('47 天前') 的感知强"**——因此使用天数差而非 ISO 日期。
## 记忆选择器 (`selection.rs`)
当记忆数量超过 `max_entries`(默认 5`select_relevant_memories()` 进行 LLM 语义选择:
1. 过滤掉 `SelectionContext.already_surfaced` 中已展示的条目
2. 若候选数 ≤ max_entries → 直接返回全部
3. 构建候选目录slug + type + name + description
4. LLM 结构化 JSON 选择 → 支持两种响应格式:`["slug1", "slug2"]` 和 `[0, 1, 3]`
5. LLM 失败或 JSON 解析失败 → **回退到 recency 排序**(保证优雅降级)
6. 通过 `apply_decay_scoring()` 对选中结果进行指数衰减重排序
`SelectionContext` 结构:
| 字段 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| `already_surfaced` | `Vec<usize>` | `[]` | 已展示过的条目索引,本轮不再选择 |
| `recent_tools` | `Vec<String>` | `[]` | 最近使用的工具,相关记忆降权 |
| `max_entries` | `usize` | `5` | 每次最多选择条数 |
## 与数据库的关系
**记忆完全使用文件系统,不使用 SQLite**。数据库表仅存储 Agent 会话状态:
| 数据库表 | 存储内容 | 与记忆的关联 |
|:---|:---|:---|
| `agent_sessions` | 会话元数据title, model, turn_count | 无直接关联 |
| `agent_messages` | 消息历史role, content, thought, token_count | 自动提取时子代理读取对话历史 |
| `agent_tasks` | DAG 任务看板 | 无关联 |
| `agent_audit_log` | 工具调用审计tool_name, status, elapsed_ms | **每个 `save_memory` 调用被 `AuditLogHook` 记录到此表** |
## 完整数据流
```
应用启动
main.rs: MemoryManager::new(library_dir)
→ create_dir_all(memory/)
→ reload() 扫描 *.md → 解析 frontmatter → 按 mtime 排序
→ 存入 AppState.memory_manager
会话开始
runtime/mod.rs: build_system_prompt()
→ try_lock() memory_manager (非阻塞)
→ build_system_reminder(5)
→ 构建 <project-memory-context> XML 块注入 system prompt
会话中LLM 主动保存)
LLM 调用 save_memory(slug, name, desc, type, content)
→ SaveMemoryTool.execute()
→ 参数校验slug 格式 + type 枚举)
→ lock() memory_manager
→ check_content_quality(content) ← 四层质量门控(仅警告)
→ find_duplicate_by_content(0.70) ← Jaccard bigram 去重
→ mgr.save_memory()
→ 归档旧版 {slug}_v1.md如存在
→ 写入新 {slug}.mdfrontmatter + content
→ update_index() → 更新 MEMORY.md
→ reload()
→ mark_main_agent_wrote()
→ 返回: 操作状态 + 质量警告 + 重复提示 + manifest 清单
会话中(压缩时) ← P3 桥接
compact.rs: compress_context_with_hooks_and_log(messages, ...)
→ 压缩快照前捕获消息副本 (pre_compact_snapshot)
→ 执行压缩snip/auto/aggro_micro
→ extract_memories_from_compaction(snapshot, sid, mgr, app_state)
→ 检查 EXTRACT_MEMORY_ENABLED → false 则 return
→ 过滤非系统消息 + 截断 400 字符 + 拼接
→ 拼接文本 < 300 字符 → return (太少不值得提取)
→ tokio::spawn(子代理)
→ build_manifest_preview() 获取现有清单
→ SubAgentRunner(仅 SaveMemoryTool, max_steps=3)
→ save_memory() 写入提取的记忆
→ 恢复文件缓存(不等待子代理完成)
会话结束
finalize.rs: finalize_turn()
→ 更新 agent_sessions (turn_count, metrics)
→ 运行 OnSessionStop hooks审计日志、指标
→ 导出轨迹 JSONL
→ 如果 EXTRACT_MEMORY_ENABLED:
tokio::spawn(run_extraction())
→ 检查节流 + 主代理写入标志
→ 子代理分析对话 → 提取记忆 → save_memory
```
## 子模块文件索引
| 文件 | 职责 |
|:---|:---|
| `src/agent/memory/mod.rs` | `MemoryManager` 核心:构造、`reload`、`save_memory`、`build_system_reminder`、索引维护 |
| `src/agent/memory/types.rs` | `MemoryType`(4 变体)、`MemoryStatus`(Active/Historical)、`MemoryEntry`、frontmatter 解析器 |
| `src/agent/memory/selection.rs` | LLM 语义记忆选择器 + 指数衰减排序 + recency 回退 |
| `src/agent/memory/decay.rs` | 指数时间衰减 (`e^(-λt)`) + Hebbian Hot/Warm/Cool 激活层级 |
| `src/agent/memory/age.rs` | 时效标签生成 + `<system-reminder>` freshness 警告 |
| `src/agent/memory/guardrails.rs` | `WHAT_NOT_TO_SAVE` 排除规则 + `VERIFY_BEFORE_RECOMMENDING` 验证提醒 |
| `src/agent/memory/dedup.rs` | Jaccard 相似度去重 (bigram) + `check_content_quality` 四层门控 + manifest 构建 |
| `src/agent/memory/extraction.rs` | 自动提取:`ExtractionConfig`(env) + `ExtractionTracker` + `run_extraction` 子代理 |
| `src/agent/tools/memory.rs` | `save_memory` 工具:`AgentTool` trait 实现,完整执行流程 |
---