45 min · agent-session-compaction.ts:490 + compaction.ts:196-199 + session-manager-entries.ts:515-530
🎒 预备知识:完成上一课「L17」(零基础入口:第 0 课)
| Case | 触发 | 行为 | 为什么这么设计 |
|---|---|---|---|
| 溢出恢复 | stopReason ∈ {error, length} 且 isContextOverflow | 压缩后 willRetry=true 续跑;从重试上下文剔除失败响应;有 MAX_OVERFLOW_COMPACTION_ATTEMPTS 上限 | 溢出是事故,压缩是急救——但不能无限急救,否则陷入"压→满→压"死循环 |
| 阈值维护 | tokens > contextWindow − 16384 | 维护性压缩;保留最近 20000 token 原文 | 在真的溢出之前主动维护,代价可控 |
两个常量值得背:reserveTokens: 16384(给压缩流程本身留的工作空间——
压缩也要调一次 LLM,也得占窗口)和 keepRecentTokens: 20000
(最近消息保原文——当前任务通常依赖近期细节,A6 讲过的原则)。
出处:compaction.ts:198-199,整个模块的灵魂就这三行 DEFAULT_COMPACTION_SETTINGS。
run 结束 → handlePostAgentRun → checkCompaction ① 时机:轮次之间,不打断当前流
→ compaction_start 事件 ② 外部可观测
→ LLM 生成结构化摘要(compaction-runtime) ③ 摘要本身是一次模型调用
→ CompactionEntry 追加落盘 ④ 见下方字段表
→ context replacement(会计系统收到 tokensAfter) ⑤ 计费上下文同步
→ agent.continue() ⑥ 无缝续跑,用户无感
所以 compaction 不是 stopReason——它发生在消息之后、run 层自动恢复。
推论(外部消费者必考):Easel 判断"这轮结束"必须看 agent_end 生命周期事件,
不能看单条 message_end——中间可能隔着一次无感的 compaction。
const entry: CompactionEntry = {
type: "compaction",
id: ..., parentId: ...,
summary, // LLM 生成的摘要
firstKeptEntryId, // ← 原文从哪条开始还"活着"——回放时的分界线
tokensBefore, ... // 计费与审计
};
this.appendEntry(entry, {...}); // 注意:append,不是 rewrite
firstKeptEntryId 是精髓:回放历史时,遇到 compaction entry 就跳到它标记的
存活线——旧原文还在文件里(审计可查),只是后续轮不再重发给模型。
"逻辑截断,物理完整",一句话记住整个设计。
① 可回放可审计。任何一次历史事故都能重放"当时模型到底看到什么"——
重写式历史做到这一点要靠备份,追加式天然免费。Easel 的 session_heal(L11)也依赖它:
修复 = 生成新的清洗记录,不是抹掉过去。
② prompt cache 稳定。重写历史 = 前缀变化 = 缓存全废(A5)。
追加式下,每轮的前缀严格是上一轮的超集,缓存逐轮累积命中。
③ 并发安全。追加是原子操作(单 writer claim:withOwnedSessionTranscriptWrites
保证同一时刻只有一个写入者),重写则需要全文锁。
持久化的物理形态:transcript 是 JSONL(按行 parse),写入经 SQLite worker; 追加 entry 无论普通消息还是 compaction 都走同一条 appendEntry 通道—— 统一写入路径是并发纪律的前提。
坑 1:外部消费者把 compaction 当失败。看到 compaction 事件以为会话出错而终止轮次—— 它是正常维护,压完 continue() 无缝续跑。
坑 2:自己实现时摘要存丢了边界。只存 summary 不存 firstKeptEntryId—— 回放时分不清哪些原文还算数,审计能力减半。
坑 3:把 reserveTokens 设成 0。压缩调用本身需要窗口工作空间—— 不留余量会陷入"想压但没有空间生成摘要"的死锁。
三分钟讲给我听:① 一次 compaction 的完整时序(六步); ② "只追加"的三重收益各举一个反例场景(如果重写了会怎么坏); ③ Easel 外部判断轮次结束的正确信号是什么、为什么。 通过即 M6 毕业,进入 L19 毕业蓝图。
一手源:compaction.ts 的 DEFAULT_COMPACTION_SETTINGS + session-manager-entries.ts:515-530。