这一章不再只讲“memory 有哪些种类”,而是把实现链路拆开讲清楚:
- memory 到底存在哪里
- 什么时候写入
- 什么时候被召回
- Agent Memory 和 Auto / Session / Team Memory 的边界是什么
- agent 真正运行时,是怎么把 memory 接到 prompt、tools、snapshot 和 UI 上的
本章主要依据这些实现:
src/memdir/memdir.tssrc/memdir/paths.tssrc/memdir/findRelevantMemories.tssrc/services/SessionMemory/sessionMemory.tssrc/services/SessionMemory/sessionMemoryUtils.tssrc/tools/AgentTool/agentMemory.tssrc/tools/AgentTool/loadAgentsDir.tssrc/tools/AgentTool/agentMemorySnapshot.tssrc/components/memory/MemoryFileSelector.tsx
先给结论:
这个项目没有把 memory 做成单一数据库,也没有做成隐蔽的内部 KV 存储,而是做成了“多层文件化记忆系统”。其中 Agent Memory 只是其中一层,但它和 agent 定义、agent prompt、agent 工具权限、snapshot 初始化、UI 文件选择器都耦合在一起,所以它不是一个附属功能,而是 agent runtime 的组成部分。
总图如下:
会话 transcript / 当前 query
├─> Auto Memory 提取
│ ├─> MEMORY.md 索引
│ ├─> topic memories/*.md
│ └─> relevant recall 选出少量文件回灌本轮上下文
│
├─> Session Memory
│ └─> 当前会话摘要 markdown
│
├─> Agent Memory
│ ├─> user scope
│ ├─> project scope
│ └─> local scope
│ └─> 在 agent system prompt 中直接注入
│
└─> Team Memory
└─> 团队同步的共享记忆
另外还有:
└─> Agent Memory Snapshot
├─> 初始化本地 agent memory
└─> 提示本地记忆有新版 snapshot 可同步
相关实现:
这一套 memory 设计,核心不是“把内容记住”,而是“把不同生命周期、不同作用域、不同可见性的内容分开保存”。
从实现上看,至少分成四层:
Auto Memory面向整个用户和项目协作过程的长期记忆,存放在统一 memory 目录中。Session Memory面向当前会话的摘要文件,目标是辅助 compact 和长会话持续运行。Agent Memory面向某个 agent 类型的持久记忆,和 agent 定义直接绑定。Team Memory面向团队共享的 repo 级知识同步。
它们没有被硬塞进一套 schema,而是各自有独立目录、独立 prompt、独立更新策略。
这带来三个直接好处:
- 透明:用户可以直接打开目录看 markdown 文件
- 可治理:不同 memory 可以单独开关、单独同步、单独约束
- 可组合:Auto / Session / Agent / Team Memory 可以同时存在,但职责不同
相关实现:
memory 的最底层是目录,不是数据库。
在 src/memdir/memdir.ts 里可以看到几个关键常量:
ENTRYPOINT_NAME = 'MEMORY.md'MAX_ENTRYPOINT_LINES = 200MAX_ENTRYPOINT_BYTES = 25_000
这说明系统把 MEMORY.md 当成入口索引文件,而不是正文存储文件。
设计意图很明确:
- 每条 memory 应该单独写成一个 markdown 文件
MEMORY.md只维护索引链接和一行描述- agent / model 在 prompt 里默认只保证看到
MEMORY.md - 需要更细节时,再去读具体 memory 文件
这比“把所有记忆堆在一个大文件里”更稳,因为大文件很容易造成:
- prompt 爆炸
- 更新冲突
- 历史垃圾无法治理
- 单条错误记忆污染整份上下文
真实源码 (src/memdir/memdir.ts:199):
export function buildMemoryLines(
displayName: string,
memoryDir: string,
extraGuidelines?: string[],
skipIndex = false,
): string[] {
const lines: string[] = [
`# ${displayName}`,
'',
// DIR_EXISTS_GUIDANCE = "This directory already exists - write to it directly..."
// 避免模型浪费一轮对话去 ls/mkdir 确认目录
`You have a persistent, file-based memory system at ${"`"}${memoryDir}${"`"}. ${DIR_EXISTS_GUIDANCE}`,
'',
...TYPES_SECTION_INDIVIDUAL, // 记忆类型分类
...WHAT_NOT_TO_SAVE_SECTION, // 禁止保存的内容(可从代码推导的、重复的)
'',
...howToSave, // 双步法:先写 topic 文件,再在 MEMORY.md 添加索引行
'',
...(extraGuidelines ?? []), // agent 专属额外规则(scope 说明等)
]
lines.push(...buildSearchingPastContextSection(memoryDir))
return lines
}模型被要求像维护小型知识库一样操作 memory 目录:独立文件存储每条记忆、维护索引、更新过时记忆、避免重复。
真实源码 (src/memdir/memdir.ts:272):
export function buildMemoryPrompt(params: {
displayName: string
memoryDir: string
extraGuidelines?: string[]
}): string {
const entrypoint = params.memoryDir + ENTRYPOINT_NAME // <dir>/MEMORY.md
// 同步读取(某些调用来自 React render 路径,不能 await)
let entrypointContent = ''
try {
entrypointContent = fs.readFileSync(entrypoint, { encoding: 'utf-8' })
} catch { /* 文件不存在时静默忽略 */ }
const lines = buildMemoryLines(params.displayName, params.memoryDir, params.extraGuidelines)
if (entrypointContent.trim()) {
const t = truncateEntrypointContent(entrypointContent) // 硬截断保护
logMemoryDirCounts(params.memoryDir, {
content_length: t.byteCount, line_count: t.lineCount,
was_truncated: t.wasLineTruncated,
// 类型标注防止 PII 误上报
memory_type: memoryType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
lines.push(`## ${ENTRYPOINT_NAME}`, '', t.content)
} else {
lines.push(
`## ${ENTRYPOINT_NAME}`, '',
`Your ${ENTRYPOINT_NAME} is currently empty. When you save new memories, they will appear here.`,
)
}
return lines.join('\n')
}两个关键实现细节:
- 同步读取:
getSystemPrompt()存在于同步调用路径,不能使用await - 硬截断保护:
truncateEntrypointContent()将 MEMORY.md 限制在 200 行 / 25KB,防止 prompt 爆炸
相关实现:
Auto Memory 的目录不是写死的,而是通过 src/memdir/paths.ts 计算出来。
大致规则如下:
优先级从高到低:
1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE
2. settings.json 中可信来源的 autoMemoryDirectory
3. <memoryBase>/projects/<sanitized-git-root>/memory/
其中 memoryBase =
- CLAUDE_CODE_REMOTE_MEMORY_DIR
- 否则 ~/.claude
这说明 Auto Memory 从一开始就兼容三类部署形态:
- 本地默认安装
- 配置文件定制目录
- remote / cowork 之类带挂载盘的环境
isAutoMemoryEnabled() 的优先级也很工程化:
CLAUDE_CODE_DISABLE_AUTO_MEMORYCLAUDE_CODE_SIMPLE- remote 模式但没有持久目录时关闭
settings.autoMemoryEnabled- 默认开启
所以 Agent Memory 虽然是单独目录,但是否启用,实际上复用了 Auto Memory 的总开关逻辑。
Auto Memory 的更新不是靠当前主线程 agent 每次都手动完成,而是可以由后台提取流程补全。
它的目标是把长期有效的信息沉淀成 durable memories,例如:
- 用户长期偏好
- 项目外部上下文
- 非代码内生知识
- 需要跨会话延续的协作约束
这一层是“全局持久记忆基座”,而 Agent Memory 更像“某个 agent 的专属长期记忆”。
相关实现:
很多系统一旦有 memory,就会把所有历史记忆都塞进 prompt。这个项目不是这样做的。
findRelevantMemories() 的工作方式是:
memoryDir
-> scanMemoryFiles() 扫描文件头
-> 过滤掉 alreadySurfaced 的文件
-> formatMemoryManifest() 生成“文件名 + 描述”清单
-> sideQuery(...) 调一个轻量模型做选择
-> 最多选 5 个 memory 文件
-> 返回绝对路径和 mtimeMs
实现上有几个细节很值得注意:
- 被选的是文件名,不是整段正文
- 选择器只看 header / manifest,不先把所有正文塞进去
MEMORY.md本身不在这里选,因为它已经单独注入 system promptrecentTools会影响选择,避免当前正活跃使用的工具文档被重复召回alreadySurfaced会过滤之前已展示过的 memory,避免每轮都选同一批文件
所以这一层其实是“轻量检索器”,而不是“全文向量库”。
相关实现:
Session Memory 不是 Agent Memory,但它非常重要,因为它说明作者把“当前会话摘要”和“长期 agent 记忆”分开了。
默认阈值在 src/services/SessionMemory/sessionMemoryUtils.ts 中写得很明确:
minimumMessageTokensToInit = 10000minimumTokensBetweenUpdate = 5000toolCallsBetweenUpdates = 3
含义是:
- 会话没长到一定程度,不启用 Session Memory
- 即便已经启用,也不会每轮都更新
- 工具调用次数和 token 增长都要参与判断
真实源码 (src/services/SessionMemory/sessionMemory.ts:134):
export function shouldExtractMemory(messages: Message[]): boolean {
const currentTokenCount = tokenCountWithEstimation(messages)
if (!isSessionMemoryInitialized()) {
if (!hasMetInitializationThreshold(currentTokenCount)) return false
markSessionMemoryInitialized()
}
const hasMetTokenThreshold = hasMetUpdateThreshold(currentTokenCount)
const hasMetToolCallThreshold =
countToolCallsSince(messages, lastMemoryMessageUuid) >= getToolCallsBetweenUpdates()
const hasToolCallsInLastTurn = hasToolCallsInLastAssistantTurn(messages)
// token 阈值始终必要;在自然断点(无 tool_use)或双阈值都满足时才触发
const shouldExtract =
(hasMetTokenThreshold && hasMetToolCallThreshold) ||
(hasMetTokenThreshold && !hasToolCallsInLastTurn)
if (shouldExtract) {
lastMemoryMessageUuid = messages[messages.length - 1]?.uuid
return true
}
return false
}!hasToolCallsInLastTurn 寻找自然断点,防止在 tool_use 链中间截断生成孤立摘要。
Session Memory 文件以严格权限创建(src/services/SessionMemory/sessionMemory.ts:183):
async function setupSessionMemoryFile(ctx) {
const sessionMemoryDir = getSessionMemoryDir()
await fs.mkdir(sessionMemoryDir, { mode: 0o700 }) // 目录:只有属主可读写执行
const memoryPath = getSessionMemoryPath()
await writeFile(memoryPath, '', {
mode: 0o600, // 文件:只有属主可读写
flag: 'wx', // O_CREAT|O_EXCL:文件不存在时才创建,防止覆盖已有记忆
})
}0o700 + 0o600 说明 Session Memory 被视为敏感本地状态,而不是普通缓存文件。
调度链路:
主会话 shouldExtractMemory() == true
-> registerPostSamplingHook 触发
-> setupSessionMemoryFile() // 创建 0o600 文件,目录 0o700
-> buildSessionMemoryUpdatePrompt() // 构造摘要指令
-> runForkedAgent({ canUseTool: createMemoryFileCanUseTool(memoryPath) })
createMemoryFileCanUseTool() 真实源码 (src/services/SessionMemory/sessionMemory.ts:460):
export function createMemoryFileCanUseTool(memoryPath: string): CanUseToolFn {
return async (tool: Tool, input: unknown) => {
if (
tool.name === FILE_EDIT_TOOL_NAME &&
typeof input === 'object' && input !== null &&
'file_path' in input &&
typeof input.file_path === 'string' &&
input.file_path === memoryPath // 精确路径匹配,不允许路径穿越
) {
return { behavior: 'allow' as const, updatedInput: input }
}
return {
behavior: 'deny' as const,
message: `only ${FILE_EDIT_TOOL_NAME} on ${memoryPath} is allowed`,
decisionReason: { type: 'other', reason: `only ${FILE_EDIT_TOOL_NAME} on ${memoryPath} is allowed` },
}
}
}Session Memory 提取 subagent 是严密沙箱化的摘要代理:只有 FileEditTool,且只能操作精确路径,连 FileReadTool 和 FileWriteTool 都被 deny。
相关实现:
src/tools/AgentTool/agentMemory.tssrc/tools/AgentTool/loadAgentsDir.tssrc/tools/AgentTool/agentMemorySnapshot.tssrc/components/memory/MemoryFileSelector.tsx
这一节是本章重点。
Agent Memory 和 Auto Memory 的区别在于:
- Auto Memory 是“用户 / 项目维度”的长期记忆
- Agent Memory 是“某种 agent 类型”的长期记忆
也就是说,它不是“当前会话记住什么”,而是“这个 agent 以后应该长期知道什么”。
这使得 agent 不再只是一个静态 prompt 模板,而是:
agent =
静态角色 prompt
+ memory scope
+ memory 目录
+ 可写 memory tools
+ snapshot 初始化能力
在 src/tools/AgentTool/agentMemory.ts 中,AgentMemoryScope 明确只有三种:
userprojectlocal
含义分别是:
user跨项目复用的 agent 长期记忆project当前项目内共享的 agent 记忆local当前项目、当前机器或当前挂载环境的本地 agent 记忆
getAgentMemoryDir(agentType, scope) 的目录解析规则很清晰:
user:
<memoryBase>/agent-memory/<agentType>/
project:
<cwd>/.claude/agent-memory/<agentType>/
local:
默认:
<cwd>/.claude/agent-memory-local/<agentType>/
若设置 CLAUDE_CODE_REMOTE_MEMORY_DIR:
<remoteMemoryDir>/projects/<sanitized-git-root>/agent-memory-local/<agentType>/
其中还有两个实现细节:
agentType会先做路径清洗
:会被替换成-,因为插件命名空间可能长成my-plugin:my-agent,但这在某些平台文件名不安全。localscope 在 remote 环境下并不是真的“只在本地磁盘”
它会重定位到远端 memory mount 的 project namespace 中。
isAgentMemoryPath() 会把候选路径 normalize 后,再判断它是否属于三类 agent memory 目录之一。
这不是一个普通 util,它背后的意义是:
- 防止
..之类路径穿越绕过 memory 边界判断 - 让权限系统知道“这是 agent memory 文件”
- 让 UI 或工具层能做特殊处理
也就是说,Agent Memory 在系统里不是“普通文件夹”,而是被识别为一种特殊存储边界。
getAgentMemoryEntrypoint() 直接返回:
<agent-memory-dir>/MEMORY.md
这说明 Agent Memory 没有独立于 memdir 体系的新协议,它是直接复用整个 memory 文件系统设计的。
也就是说,Agent Memory 不是另起炉灶,而是:
- 目录结构沿用 memdir
- prompt 构造沿用
buildMemoryPrompt() - 记忆治理规则也沿用 typed memory instructions
agent 定义加载逻辑在 src/tools/AgentTool/loadAgentsDir.ts。
无论是 JSON agent 还是 Markdown agent,只要定义里带了 memory 字段,系统都会做两件事:
- 自动把 memory prompt 拼接到 agent system prompt 后面
- 自动把
Write / Edit / Read三个文件工具注入到 agent 的工具列表里
这两步缺一不可。
如果只注入 prompt,不注入文件工具,agent 看得到规则却没法落盘。
如果只注入文件工具,不注入 prompt,agent 有写权限却不知道 memory 的目录和治理方式。
在 parseAgentFromJson() 和 parseAgentFromMarkdown() 两条路径里,都有这样的逻辑:
if (isAutoMemoryEnabled() && parsed.memory) {
return systemPrompt + '\n\n' + loadAgentMemoryPrompt(agentType, parsed.memory)
}
所以 Agent Memory 的注入时机很早:
- 不是 agent 运行中动态附加
- 不是工具调用后再补
- 而是在构造 agent system prompt 时直接固定进去
这意味着 agent 从第一轮开始就知道:
- 自己的 memory 目录在哪里
- 该怎么写 memory
MEMORY.md当前有哪些索引
loadAgentMemoryPrompt(agentType, scope) 这一层又做了几件非常关键的事:
loadAgentMemoryPrompt()
-> 根据 scope 生成 scopeNote
-> 算出 memoryDir
-> fire-and-forget ensureMemoryDirExists(memoryDir)
-> buildMemoryPrompt({
displayName: 'Persistent Agent Memory',
memoryDir,
extraGuidelines: [scopeNote, ...可能的环境附加规则]
})
这几个点分别对应不同意图:
scopeNote告诉 agent:user scope 要写得更通用,project scope 要面向项目,local scope 要面向当前机器/工作区。ensureMemoryDirExists(memoryDir)prompt 构造时就提前 mkdir,但不阻塞主路径。buildMemoryPrompt(...)让 Agent Memory 复用整套 memdir 规则,而不是写一套单独 prompt。CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES允许在 cowork 场景额外附加记忆规则。
注释里明确解释了原因:
- 这段逻辑运行在同步
getSystemPrompt()回调中 - 某些调用场景来自 React render 路径
- 因此这里不能异步阻塞
于是作者采取了很务实的做法:
- 先异步创建目录
- 即便目录还没创建好,后面的
FileWriteTool自己也会补 mkdir
这就是典型的工程取舍:不为了“理论上更完美”而破坏 prompt 构建路径的同步性。
loadAgentsDir.ts 中有一条非常关键的自动注入逻辑:
如果 memory 开启,且 auto memory 开启:
强制把以下工具加入 agent tools:
- FileWriteTool
- FileEditTool
- FileReadTool
这意味着声明了 memory 的 agent,天然获得一套最小闭环:
读索引 -> FileReadTool
新建记忆 -> FileWriteTool
更新记忆 -> FileEditTool
从这里也能看出作者的设计立场:
- Agent Memory 不是给“系统后台”偷偷维护的
- 而是让 agent 自己显式读写 markdown 文件
把上面这些机制串起来,实际运行链路大概是:
加载 agent 定义
-> agent.memory = user / project / local
-> 自动补 FileRead / FileWrite / FileEdit
-> getSystemPrompt() 追加 Agent Memory prompt
spawn agent
-> prompt 中已经包含:
1. memory 使用规则
2. memory 目录位置
3. 当前 MEMORY.md 索引内容
agent 工作中发现需要记住的信息
-> 先读 MEMORY.md 或已有 memory 文件
-> 新增或更新某条 memory
-> 再维护 MEMORY.md 索引
后续再次调用同一 agent
-> 再次读取同一目录的 MEMORY.md
-> 获得上次沉淀下来的长期记忆
这说明 Agent Memory 的“应用”不是检索外挂,而是直接改变 agent 下一次的 system prompt 起点。
这一块是本章里最有意思的实现之一。
在 src/tools/AgentTool/agentMemorySnapshot.ts 中,snapshot 目录固定在:
<cwd>/.claude/agent-memory-snapshots/<agentType>/
其中关键文件有两个:
snapshot.json记录 snapshot 的updatedAt.snapshot-synced.json记录本地当前是从哪个 snapshot 时间点同步过来的
checkAgentMemorySnapshot() 只会返回三种动作:
noneinitializeprompt-update
判定逻辑可以概括成:
如果项目里没有 snapshot.json:
-> none
如果本地 agent memory 目录里没有任何 .md 文件:
-> initialize
如果本地已有 memory,但:
- 没有 .snapshot-synced.json
- 或 snapshot.updatedAt > syncedFrom
-> prompt-update
否则:
-> none
这说明 snapshot 不是每次无脑覆盖,而是区分:
- 首次初始化
- 有新版可提示更新
- 已同步无需处理
initializeFromSnapshot() 的逻辑很直接:
snapshot 目录
-> 复制除 snapshot.json 之外的文件
-> 写入本地 agent memory 目录
-> 保存 .snapshot-synced.json
而 replaceFromSnapshot() 会更激进:
本地 agent memory 目录
-> 先删除已有 .md 文件
-> 再复制 snapshot
-> 再写 synced metadata
还提供了 markSnapshotSynced():
- 不改正文
- 只更新同步元数据
这通常用于“我知道有新版,但我现在接受当前版本状态”的场景。
snapshot 检查逻辑在 src/tools/AgentTool/loadAgentsDir.ts。
要满足几个条件才会触发:
feature('AGENT_MEMORY_SNAPSHOT')isAutoMemoryEnabled()- agent 是 custom agent
- 并且当前实现里只对
memory === 'user'的 agent 执行初始化检查
最后这一点很关键:当前 snapshot 机制主要是给 user-scope 的 agent memory 做初始化和升级提示,不是对所有 scope 一视同仁。
因为它把 agent memory 从“运行时副产物”提升成了“可分发的角色资产”。
也就是说,一个项目可以不仅定义 agent 的 prompt,还能一起下发:
- 这个 agent 的初始记忆结构
- 这个 agent 已经积累好的协作经验
- 这个 agent 的更新版本
这比单纯 shipping 一个 prompt.md 更强,因为它允许项目把 agent 逐步养成熟。
src/components/memory/MemoryFileSelector.tsx 会把 agent memory 目录暴露给 UI。
这说明 Agent Memory 不是只给内部逻辑用的隐式存储,而是用户侧可浏览、可打开的文件系统对象。
这和很多“模型有记忆,但用户看不见”产品的思路完全不同。
相关实现:
src/services/teamMemorySync/index.tssrc/services/teamMemorySync/watcher.tssrc/memdir/teamMemPaths.ts
Team Memory 是另一个维度:它不围绕单个 agent,而是围绕团队共享知识。
从实现看,它不是简单共享目录,而是带有:
- pull / push
- watcher
- checksum
- optimistic locking
- path validation
- secret scanning
所以 Team Memory 的目标不是“帮一个 agent 记住事”,而是“把 repo 级知识做成一套受控同步的团队知识层”。
把几层 memory 放在一起看,职责边界就更清楚了:
Auto Memory
解决:用户 / 项目长期协作信息沉淀
Relevant Recall
解决:本轮只召回少量相关 memory,避免 prompt 污染
Session Memory
解决:长会话摘要与 compact 稳定性
Agent Memory
解决:某个 agent 类型的长期专属记忆
Agent Memory Snapshot
解决:agent 记忆的初始化、分发、升级
Team Memory
解决:团队共享知识同步
所以第四章如果只盯着“Agent Memory 目录在哪里”,其实是不够的。真正的工程设计在于:它被放进了一个分层 memory 体系里,每层负责不同问题。
相关实现:
Agent Memory 之所以重要,是因为它不在 runtime 之外,而在 runtime 之内。
主要耦合点有四个:
agent 定义阶段memory字段直接影响 agent 解析结果。system prompt 构造阶段memory prompt 直接拼到 agent 的 system prompt 后面。tool 能力阶段memory agent 自动获得读写编辑 memory 文件的能力。snapshot 生命周期阶段agent 定义加载时就会检查 snapshot 初始化或更新状态。
因此,所谓“agent 有记忆”,在这个项目里不是抽象概念,而是:
agent runtime
= agent definition
+ system prompt
+ tool set
+ permission context
+ memory directory
+ snapshot state
相关实现:
不管引入了多少层的记忆,由于 Agent 对话中伴随着大量读写结果(特别是文件代码输出),模型的 Context Token 极易到达上限。系统为此设计了底层的 compact(上下文压缩打薄)逻辑,分为手动 /compact 与自动阈值触发。为了不在打薄时丢失 Agent 功能与关键连贯性,它做了严密的技术处理:
在发起总结摘要请求给模型前,stripImagesFromMessages 和 stripReinjectedAttachments 会被调用来剔除所有的图像输入以及冗长的静态技能清单附件。这是为了防止因总结请求本身体积过大就撞到模型的 Token 墙导致请求死锁(PTL 异常)。万一触发 PTL,truncateHeadForPTLRetry 会回退削减最头部的几次 API Round 保证程序本身能够无阻运作。
这是本项目 Compaction 中最具亮点的工程设计。若系统已开启并处于一段长会话中,它不会再去调用额外的 API 浪费 Token 给模型总结大意,而是直接调用 trySessionMemoryCompaction,读取后台提取记忆的子 Agent 最新沉淀的 Session Memory 文件直接充当上下文断点(SummaryMessage)。
更重要的是,在寻找哪部分 Message 该不该丢弃时,由 calculateMessagesToKeepIndex 执行精确裁切:
- 必须优先从后向前留足最低限度配置的下限 Token 的原文(默认保留配置约 10K-40K Tokens),配置通过
getSessionMemoryCompactConfig读取。 - 它内置了高度防御的代码逻辑去解决面条并发问题:截断时,如果切断位置落在了
tool_use / tool_result执行链的中间,或者碰到了和 Assistant 共享相同message.id的 Thinking 流,adjustIndexToPreserveAPIInvariants会强制向头部平移 Index 合包这些记录,绝对避免切分出非法的孤立tool_result,从而规避 Anthropic 严格的 API 校验报错。
被丢弃打薄的消息转变成了带有 SystemCompactBoundaryMessage 且指向 Summary 的短消息,由 createCompactBoundaryMessage 生成。但这有一个致命的副作用:过去抛出给大模型的那些注册在前的可用能力描述(例如 Tool Schemas 和 MCP 远程工具列表等)会被连带着裁剪遗失。所以 Compact 完成后,createPostCompactFileAttachments 会自动重建 FileAttachments(工作区读取保留)及激活的 Plans 清单,并通过 getDeferredToolsDeltaAttachment 重新全量声明当前装载好的外部能力,追加回贴入新的队列中。在模型醒来后的第一回合看,虽然前世细节没有了,但是当前的技能蓝图依然齐装满员。
- 透明
所有 memory 都是文件,用户可以检查、编辑、删除。 - 作用域清晰
user / project / local 三层 scope 很实用。 - 检索成本可控
不是每轮都灌全部记忆,而是做 relevant recall。 - agent 能真正“成长”
因为记忆和 agent 类型绑定,而不是只绑定单次会话。 - 可分发
snapshot 机制让 agent memory 可以随项目一起发布和升级。
- 需要治理
错误记忆、过时记忆、重复记忆都会持续污染后续 agent 行为。 - 路径和权限逻辑变复杂
user / project / local / remote mount 都要分别处理。 - prompt 构造更重
MEMORY.md每次都要参与 prompt 生成,虽然有截断保护,但仍有成本。 - 需要模型遵守写入规范
系统已经做了不少规则约束,但最终仍依赖 agent 正确维护索引和文件。
如果只用一句话总结本项目的 Agent Memory:
它不是“给 agent 加一块隐藏缓存”,而是把 agent 的长期记忆做成了文件化、分 scope、可写入、可初始化、可升级、可在 UI 中查看的运行时基础设施。
如果再进一步总结整个 memory 体系:
Auto Memory 管长期协作记忆
Session Memory 管当前会话摘要
Agent Memory 管某类 agent 的专属长期记忆
Team Memory 管团队共享知识
Relevant Recall 负责按需召回
Snapshot 负责 agent 记忆资产化
Compaction 管底层 Token 会话流打薄拦截