Skip to content
My Blog
Go back

压缩自己也进日志:dsh 的 Compaction 设计

长会话总会撞上下文上限,做 agent 的都逃不掉压缩这个课题。多数做法是「改历史」——把旧消息替换成摘要。dsh 的做法不太一样:压缩产生的摘要不是历史的一部分,压缩这个动作本身才进日志。

三个 log-only 事件

packages/compaction/ 定义了三个只写日志的事件:compaction/startcompaction/summarycompaction/end。子系统文档的原话:它们「record the lock, summary, selected range, shadowed event seqs, token count, and model call without joining the surface」——记录了锁、摘要、选中区间、被遮蔽的事件序号、token 数和模型调用,但不进入模型表面。

「不进入表面」是关键。摘要不会直接出现在模型看到的消息里,它搭一条便车:一条带 surfaceOp: { op: 'replace', start, end }user/message,这条消息是压缩带来的唯一表面变更。换句话说,模型历史的重建规则没变,变的只是某段区间被一条摘要消息替换了。

日志锁:崩溃变成可检测,而不是假完成

compaction/startcompaction/end 这对事件实际上是一把日志级的锁。释放锁放在最后一步,文档解释了为什么:

Releasing the lock last turns a crash mid-operation into a detectable orphaned lock (a compaction/start with no matching compaction/end).

压缩做到一半进程挂了,日志里会留下一把没有配对 end 的孤儿锁——下次能检测出来,而不是误以为压缩完成了。这是「崩溃要留下可检测的痕迹」这条原则的又一次出现,和第二篇里「崩溃的 turn 关闭而不截断」是同一族设计。

两个触发时机

触发类型定义为 CompactionTrigger = 'pressure' | 'context-overflow',对应两条路径:

服务暴露三个动词:compactIfNeeded(agent, trigger)(自动)、compactNow()(空闲期主动缩减,此时 turn: null 单独成档)、compactRegion()(显式指定区间)。人类侧的入口是 /compact 命令。

摘要之前的二级接缝

压缩之前还有一道工序:ctx.toolResultPruner。超大工具输出先被裁剪,再谈整体摘要,每次替换都报告 charsBefore / charsAfter。选区边界也有讲究——保持工具调用和结果成对(压缩前后都校验配对平衡),所以一个超大 turn 里,只有早期已经完成的 step 会被选中。

小结

回头看,这个设计之所以干净,是因为它没给压缩开特权:压缩不修改历史,只追加自己的痕迹,表面变更走标准的消息事件。第二篇说的「模型可见 = 已记录」在这里升级成了「连压缩这种对可见内容的改写,自己也被记录」。不变量守住了自己。

本系列


Share this post:

Previous Post
第 03 课 · 五种事件分发模式:协作的语法
Next Post
状态就是日志:plan、todo、goal 在 dsh 里怎么存