Files
deepseek-harness/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.zh.md

20 KiB
Raw Blame History

Agent Note: 压缩作为能力 seam抽象约定 + 基础后端)

Status: implemented

English | 中文

问题

长时间运行的 agent智能体对话会无限增长。随着事件日志不断累积轮次派生出的消息历史最终逼近模型的上下文窗口模型随即在响应中途停止生成max-tokens),或表现退化。上下文压缩context compaction 是对此的缓解手段:用一段简洁的摘要替换一批较早的历史,保持近期上下文完整。

会话接口面正是为此而构建的基础设施:一份建立在事件日志之上的有序投影,带有专门设计的 surfaceOp: { op: 'replace', start, end } 操作,用于遮蔽一段条目并插入替换内容,sourceEventSeqs 列出每个来源事件,使回放可以验证替换是否引用了它移除的每个事件。剩下的是那个决定压缩什么、并产出摘要的插件。

两股力量塑造了设计。第一,压缩策略与可复用的 token 测量独立变化:测量归 LLM大语言模型系列的 ctx.tokenMeter 服务所有,摘要生成则可以使用模型调用、模板或远程服务。第二,SurfaceEventType 封闭为产生消息的事件类型(user/messageassistant/messagetool/result);只有这些类型可以携带 surfaceOp。因此一个专用的 compaction/* 事件不能出现在 surface 上,编译器与 Session 始终启用的 append/seed 边界都会拒绝在其上附加 surfaceOp

决策

压缩是一个能力 seamService Definition 与 Service Provider 角色分离

遵循能力 seam Agent Note,压缩以独立包发布,使约定、算法和(后续的)消费方 API 各自独立演进:

  1. 接口@deepseek-ai/dsh-compaction:抽象 CompactionEngine,拥有 ctx.compaction 键、CompactionResult 词汇、compaction/* 会话事件、手动失败分类体系以及规范的检查点消息来源。它将 compactIfNeeded()compactNow()compactRegion() 声明为抽象方法——约定说明压缩做什么,而非怎么做
  2. 实现@deepseek-ai/dsh-compaction-basic:具体的 BasicCompactionEngine,消费 ctx.tokenMeter,并拥有尾→头保留遍历、通过 ctx.llm.stream() 生成摘要、surface 替换、锁、步骤前压力处理和规范的上下文溢出恢复。summarize() 是其唯一的子类钩子;计价与回放仍归 meter 所有。
  3. 无模型配套服务@deepseek-ai/dsh-compaction-tool-result-pruner:一个具体的可选服务,在后端选择摘要范围之前,重写当前过大的 tool/result 节点。它不是第二种压缩实现,也不实现 CompactionEngine
  4. 面向用户的消费方@deepseek-ai/dsh-command-compact 通过 ctx.commands 注册无参数 /compact,并调用后端无关的 compactNow() 操作。它是供用户直接控制的命令,不是面向模型的工具。

约定依赖 dsh-sessiondsh-llm——有意为之的偏离

能力 seam Agent Note 规定 Service Definition 包「仅依赖 cordis」dsh-shell 成立,因为其词汇是自包含的)。压缩无法遵守这一点:它的动词作用于 agent 拥有的 SessioncompactRegion(start, end, agent)),其输出使用内容词汇(CompactionResult.summary: ContentBlock[])。不引用 Session/SessionEvent(来自 dsh-session)和 ContentBlock(来自 dsh-llm),约定就无法表达。

这不是耦合异味,而是约定的领域所在。「仅 cordis」的指导原则一直是「接口仅依赖约定真正需要命名的东西绝不依赖实现」的简写。dsh-sessiondsh-llm 本身是接口/词汇包,不是实现;dsh-compaction 仍然不导入任何后端。seam 的真正不变式——消费方和实现在抽象服务背后独立演进——完好无损。

三个抽象操作,算法在后端

将完整算法保留遍历、token 求和、文本提取)作为接口上的具体方法,会将约定重新耦合到一种策略:想要不同保留策略或事件排序的后端必须与继承来的具体代码对抗。将三个操作都设为抽象,把所有怎么做的决策放在后端,并让接口保持为做什么的声明。token 测量根本不是压缩钩子;单例服务使多个消费方能够共享逐会话的回放折叠。

compactIfNeeded(agent, trigger, signal) 接受显式的 'pressure' | 'context-overflow' 触发原因与取消信号。它只读取最新的持久化已路由请求;没有 header 就不执行工作,任何已路由的提供方/模型目标都使用单例估算器。compactNow(agent, signal) 要求 agent 处于 idle即使未达到压力也进行一次有效的平衡缩减不存在这种范围时返回 null,且不写入任何内容。compactRegion(start, end, agent, signal?)agent.session 作为唯一会话身份,并为显式调用方保留可选 signal。默认摘要器依次从显式配置、最新记录的已路由目标和 agent 选项解析目标,并在任何 llm/stream 路由后记录提供方/模型对。它回放已路由请求的前缀,并将压缩指令追加为尾部 user 消息,从而复用提供方的热 KV Cache摘要前缀缓存 Agent Note。该结果携带 llmStreamCall: true,因为生成它时恰好通过此上下文的 LLM 服务发起了一次调用;只有满足相同条件时,子类才设置该标记,因为单有保留的 rawOutput 并不能判定调用路径。该调用将提供方无关的 GenerateOptions.purpose 设为 compaction适配器可以将此用途映射为对模型隐藏的传输元数据DeepSeek 适配器会发送 x-deepseek-harness-compact: 1

成功的持久步骤工作完成后运行自动压力检查

成功调用的压力检查在下一个 agent/pre-step 运行;此时前一响应、工具结果、缓冲上下文与 steering中途引导已经持久化而下一个请求尚未派生。dsh-compaction-basic 通过 ctx.tokenMeter 测量规范的已记录请求,因此下一个请求无需推测性覆盖信封即可看到任何替换。压力达到条件后,可选的 ctx.toolResultPruner 重写在摘要范围选择前运行compaction-basic 重新测量持久 surface如果修剪恢复到安全压力便跳过摘要生成。

规范的提供方上下文溢出走另一条路径。失败步骤先关闭,agent/request-error 接收原始请求错误。compaction-basic 自行持有按 agent 计的溢出次数,在强制执行一次有效且平衡的缩减前先修剪,且仅当 session.surface.replaceGeneration 增加时才返回 { kind: 'retry' };这包括没有摘要范围时仅修剪取得的进展。随后循环关闭失败轮次,开启新的编号重试轮次,并从持久日志重建请求。没有替换、任何替换前的恢复失败、取消、耗尽的上限或无关错误都会保留原始提供方失败。如果修剪已经推进 generation而后续摘要工作失败恢复会从该持久的已修剪 surface 重试,除非取消或 dispose资源释放先发生。完整生命周期决策见调用后恢复 Agent Note

assistant/message → tool/result/context/steering → step/end
claim the next batch → await waterfall agent/pre-step  ⟵ pressure compaction before the next request
enter → next step/start

provider overflow → step/end
await waterfall agent/request-error  ⟵ forced compaction between attempts
retry → next numbered step/start      ⟵ derives from the replacement surface

保留是轮次无关的;工具配对平衡是唯一的结构守卫

自动压缩在每个成功的步骤之后检查,而非每轮一次。这对失控轮次存活至关重要:工具密集型的 ReAct 轮次每步追加一个 assistant/message + 一个 tool/result,因此 surface 会在一轮之内增长。下一个 pre-step 检查可以在后续执行开启另一个步骤之前压缩早期已关闭的工具对;如果请求率先越过限制,由提供方确认的溢出仍是兜底机制。

compactIfNeeded 保留估算大小达到解析后保留 token 预算的最小完整 surface 单元尾部,压缩更早的节点。一个单元是一个完整的已关闭步骤或一条无步骤消息。如果 token 截断点落在步骤内部,保留范围会扩展直到切割点满足工具配对平衡。平衡按 surface 顺序检查,而非日志序号,因为替换摘要在旧的 surface 位置拥有新的序号。dsh-compaction 导出前后边缘辅助函数;只要 replaceGeneration 不变,其逐会话缓存就只折叠新增的 surface 尾部节点,面对仅日志增长时不读取事件,并在替换后重建当前成员关系与平衡。compactRegion 拒绝将工具调用与其结果拆分的边界。进行中的轮次不享受特殊保留。

因此失控轮次的压缩方式与其他历史完全相同:其早期已关闭步骤被摘要,近期步骤保持原样。当唯一可压缩的内容只剩一个不可拆分的开放尾部步骤(其工具调用尚无结果)时,压缩拒绝执行(返回 null)并在该步骤关闭后重试。

部分单一单元溢出仍不在范围内。 摘要范围选择无法拆分不可分割的单元。当可移除的文本型工具结果内容占据大部分空间,且修剪后的剩余内容不再超限时,可选修剪器可以修复一个已关闭的工具对。仅信封压力、粘贴的 user/message 等不可分割的超大非工具节点,以及不可修剪余量仍然过大的工具单元,依旧不属于压缩范围;限制这些单元是另一个关注点。

头部锚定:一个自动检查点,始终在头部

自动压缩始终从 surface 头部开始,将先前的检查点与新压缩的历史合并,因此只保留一个自动检查点。shadowedRange 因此是位置性的而非数值序号区间:一个较新的摘要序号可能占据较旧的 surface 位置。shadowedSeqs 记录权威的 surface 顺序。手动的中间范围压缩可能留下多个检查点。

近似收敛不变式

resolveConfig 提供可用默认值:阈值比例 0.8、保留尾部比例 0.16、空的摘要提供方/模型覆盖、maxTokens: 8192compactionRetries: 1maxOverflowRetries: 1 以及 auto: true。可选的精确提供方/模型策略会部分覆盖顶层默认值;压力根据拥有该路由的 LLM 适配器所报告容量缩放比例,而 retainTokens 可以替代按比例保留。保留量必须低于最终阈值。收敛仍然是动态的因为提供方输出上限可能被隐藏或显式的推理reasoningtoken 消耗,摘要大小也不可预测。如果压力仍高于阈值,compactIfNeeded() 会按配置的重试次数再次压缩头部检查点,但每次提交的摘要必须小于其遮蔽的内容。溢出不需要容量元数据,并会绕过阈值和保留尾部策略,执行一次最大且平衡的头部缩减,留下最新的不可分割单元。所有权划分由已路由模型上下文与压缩策略 Agent Note规定。

Surface 替换:compaction/* 事件仅存在于日志;一条 user/message 承载摘要

由于 SurfaceEventType 是封闭的,摘要不能搭载在 compaction/* 事件上。后端改为追加单条 user/message,带有 source: COMPACT_CHECKPOINT_SOURCEsurfaceOp: { op: 'replace', start, end };其 content 是(带框架的)摘要,sourceEventSeqs 覆盖被遮蔽的条目簿记事件。接口导出该来源和 isCompactCheckpointSource(),使消费方无需依赖后端包身份,即可识别持久化或克隆得到的检查点。compaction/* 事件记录锁、摘要、选中区间、被遮蔽的 seq、token 数和模型调用,但不加入 surface。surface 变更位于锁内部compaction/end 是最后追加的事件:

compaction/start    → log-only. Acquires the lock.
[summarize older range via the backend]
compaction/summary  → log-only. Records the raw summary, local-call marker, range, shadowed seqs, and token count.
user/message     → canonical checkpoint source + surfaceOp { op:'replace', start, end }.
                   THE surface mutation (framed summary).
                   deriveMessages() renders it as a user-role message.
compaction/end      → log-only. Releases the lock (carries `error` on a recoverable failure).

deriveMessages() 随后产出 [summary_as_user_message, ...retained_entries]。复用 user/message 是诚实的而非变通:摘要确实 user 角色的上下文。

检查点框架 + 增量合并(后端私有)

基础后端将摘要包装为既定的检查点上下文,并标记以便下一轮增量合并。原始摘要保留在 compaction/summary 上。框架是后端策略seam 承诺由一条替换 user 消息承载可能带框架的摘要,并使用规范的检查点来源。

通过日志记录的锁实现阻塞,加上崩溃/可恢复失败的分类

compaction/start … compaction/end 事件对承担两项职责:

  1. 可检测的崩溃孤儿 + 已记录的摘要输入(首要)。摘要生成是一次慢速模型调用,持久化在 compaction/start 之后。摘要生成中途崩溃会留下一个没有匹配 compaction/endcompaction/start——一个可检测的孤儿。最后释放锁(而非最先)将崩溃窗口从静默损坏转变为可检测的孤儿。
  2. 防止并发压缩。 每个自动、手动和显式范围入口点都会拒绝活动的未匹配 compaction/start。该标记对就是唯一的锁;没有进程本地 mutex 重复承担同一职责。

该锁只排除另一项压缩,不排除无关事实。其标记是时间点,而不是排他的容器,因此持久 inbox splice 可以出现在独立手动 start 与 end 之间。自动工作要求其轮次内的整个 surface 保持稳定。手动工作只重新验证所选位置 span使其外部的仅追加上下文在替换后保持可见。

生命周期边界使崩溃状态含义明确:

  • 当前生命周期: 最新 session/end-seed 之后悬空的 compaction/start 是活动的持久锁,并报告 busy。
  • 后续生命周期: 构造函数写入的较新 session/end-seed 证明更早的未匹配 start 已陈旧因此恢复、fork 和接手不会被已死的写入方持续卡住。
  • 可恢复失败: start 落地后,后端会恰好尝试一次 compaction/end { error }。摘要或稳定性失败会保持会话 surface 不变,同时在日志中保留失败尝试。如果追加闭合事件失败,未匹配 start 会继续有意阻塞。

compaction/end 保留其 error? 字段(与 tool/result 的自包含错误一致——一个事件即可区分成功与失败,无需关联兄弟事件)。没有单独的 compaction/error 事件。

核心会话修复保持对压缩无感知——这是有意为之。 interruptedTurnClosers 从不被教导 compaction/*。通用 session/end-seed 生命周期边界提供压缩所有者所需的证据;压缩不变量与后端负责解释它,无需向核心添加插件专属修复。

曾考虑的替代方案

  • 完整算法作为接口的具体方法——否决,因为它将约定重新耦合到一种保留策略。三个操作都是抽象的;可复用测量属于单独的 LLM 系列服务,summarize() 是 basic 唯一的钩子。
  • agent/request 或压缩专属的 loop 回调上执行压缩——否决,因为前者观察的是临时请求,后者会将通用生命周期耦合到压缩策略。对先前持久请求进行 pre-step 回放,再加上规范溢出恢复,即可覆盖成功和被拒绝的调用。
  • compact 布尔值或无类型的请求元数据 map——否决,因为多个辅助调用种类会变成互斥标志,而开放 map 会丢弃由编译器检查的词汇。一个类型化的 purpose 判别字段可以扩展其他调用种类,而无需再为 GenerateOptions 添加字段。
  • 单独的 compaction/error 事件——否决:compaction/end 保留 error? 字段,与 tool/result 的自包含错误一致——一个事件即可区分成功与失败,无需关联兄弟事件。
  • 教导核心轮次修复识别 compaction/*——否决:通用 end-seed 边界已经能够区分先前生命周期的历史;为每个未来的 xxx/start … xxx/end 事件对修补核心模块,恰好是能力 seam 架构存在的意义所要避免的耦合。

后果

  • packages/compaction/compaction 提供接口,compaction-basic 提供后端,compaction-tool-result-pruner 提供可选的确定性重写,command-compact 提供面向用户的 /compactpackages/llm/token-meter 独立拥有回放感知的测量。
  • 自动扩展点agent/pre-step@mode waterfall)在请求派生前处理压力,agent/request-error@mode waterfall处理失败步骤关闭后的最终请求失败。pre-step 的 payload 携带已领取批次、轮次、步骤与 signal参见 payload-object 事件决策),不携带压缩专属的提示词/前缀 payload。
  • SessionEventMap 通过可合并扩展的声明合并获得 compaction/start / compaction/summary / compaction/endSurfaceEventType 未被触及。这些是会话事件,不是 cordis Events,因此事件分类门禁无需新增条目。
  • dsh-compaction 拥有 COMPACT_CHECKPOINT_SOURCEisCompactCheckpointSource(source)toolPairingBalancedBefore(session, seq)toolPairingBalancedAfter(session, seq)。该标记用于跨后端实现识别替换摘要。带缓存的 surface 边缘检查会防止 compactRegioncompactIfNeeded 拆分工具调用/结果对,按 seq 校验当前成员关系,从每个切割点的一条平衡序列回答两侧边缘,并拒绝陈旧或缺失的 seq 与孤立结果。
  • dsh-session 通过唯一的 surface 管理器校验位置替换、引用的来源事件是否覆盖完整,以及仅内容的单节点 tool/result 重写。其不变式配套插件将新追加的工具结果视为执行,要求存在已打开的步骤与待处理调用,而压缩配套组件负责维护数字轮次 owner 与独立 null owner 事件对之间的关系。
  • 接线examples/tui-agent/cordis.yml 依次加载零配置的 dsh-token-meterdsh-compaction-tool-result-prunerdsh-compaction-basic,然后加载 dsh-command-compact;服务级默认值使组合无需重复数值策略即可使用。

测试

  • 单元测试: 使用真实 Loader 和 invariant 插件覆盖完整单元保留、修剪配置与回放、富块顺序、元数据保留、收敛、compaction/end 的两种结果、开放尾部拒绝、仅修剪与带摘要的溢出恢复、generation 证明、上限和原始错误保留。
  • 循环测试: 测试固定 pre-step 发生在前一个 step/end 之后、下一个 step/start 之前,使用实际 agent/request 路由,关闭失败步骤,分配新的重试编号,并覆盖完整的抛出/带内溢出 → 压缩 → 重建重试组合。
  • 手动测试: 无需模型密钥即可固定 maintenance 串行化、标记顺序、注入保留、活动陈旧未匹配标记分类、取消、闭合flush 失败、命令映射以及排队 TUI 流程。
  • 带密钥 e2e 真实模型和 bash 会话在降低的限制下触发压缩,记录完整的 compaction/start…end 对,缩小 surface并完成任务。
  • 快照: 组装后的上下文溢出场景仅在 llmStreamCall: true 证明本地 LLM 服务执行了辅助调用时,才从 compaction/summary 派生该调用;规范重建的块在不固定提供方增量切分的情况下固定完整恢复过程。