乐于分享
好东西不私藏

DeepSeek Harness 源码研究(六):模型可见即已记录

DeepSeek Harness 源码研究(六):模型可见即已记录

模型可见即已记录

Agent 系统最隐蔽的一类 bug,不是模型答错,而是不同组件活在不同历史里。

模型请求由内存 messages 组装,聊天 UI 只保留最终文本,工具状态在另一个 store,持久化只写部分字段。正常运行时看不出问题;一旦刷新、重试、恢复、fork 或压缩,就会出现“界面看见过,但模型忘了”“模型使用过,但审计记录没有”“工具结果在回放时无法配对”等分叉。

dsh 用一条很强的不变量压住这类问题:Model-visible iff logged。凡是进入模型请求的内容,都必须能从 Session 日志重建。

第一层:Session 不是聊天记录,而是运行事实日志

core/session 创建和持有 append-only Session。事件包括 turn/startstep/startuser/messageassistant/chunkassistant/messagetool/calltool/result、request header/context 等。

这些事件的作用不同。Turn/Step 提供执行边界,message 提供模型表面,chunk 提供实时与回放细节,tool call/result 提供副作用请求与结果,request header 记录系统提示、工具 schema 和模型调用配置的 epoch 变化。

Agent Loop 每次构建请求时调用 session.deriveMessages()。它不会相信另一个可变 messages 数组;surface fold 对事件做增量投影,返回带稳定 identity、冻结的 Message。若 surface 被 compaction 或 rewrite 修改,投影会重建,而不是回退到原始日志的某种隐式拼接。

所以,agent.inject() 提供的模型上下文最终仍要以 user/message 进入已接纳的 Step。Prompt section 和工具 schema 则通过 request header 记录实际发送的快照与变化原因。运行时 invariant 可以据此比较“模型实际请求”与“日志可重建请求”。

第二层:durable fact 与 live control 必须分域

dsh 有三类事件。

Session events 是 durable facts。session.append() 是提交点,随后 session/event 广播给持久化、UI、遥测和查询。需要在进程重启后存在的事实,应进入这里。

Agent events 是 live coordination。agent/status、inbox inserted/claimed、pre-step、request、request-error、turn-stopping 都携带当前 Agent 或运行 signal,适合 UI 状态、steering 和策略拦截,但不能取代历史事实。

Capability events 位于工具、文件与遥测 seam,用于权限、policy、adapter 和 observer。

这一区分有实际后果:SDK 若要构建可重放 transcript,应消费 session/eventagent/* 适合显示当前队列和 running 状态。如果把 agent/status 当成 durable turn 事实,断线重连就会丢状态;如果把每个 live cancellation 细节都塞进模型消息,又会污染上下文。

持久化同样是插件。core/session 不写数据库;session-persistence 订阅 session/event,采用 write-behind 批处理,并在 session/flush 形成共享的 quiescence barrier。后台写失败会保留有序 batch、暂停自动重试;显式 flush 或 teardown 会立即重试并把重复失败暴露给调用方。

默认 Base bundle 使用 JSONL 保存 Session,同时用 SQLite 提供 Session Query。这里再次体现“事实合同”和“查询技术”分离。

第三层:append-only 增加可证明性,也制造信息增长压力

从信息系统角度,Session 做了三件事。

第一,它把多个瞬时输入压成一个总序事件流。不同组件不再各自猜测先后,而以 seq 和 commit point 对齐。

第二,它通过投影把高维运行事实变成不同视图。模型得到 Messages,聊天 UI 得到 Conversation Nodes,Trajectory UI 得到请求与工具阶段,Query 得到搜索索引。它们共享事实,不共享展示模型。

第三,它把不可逆的信息丢失变成显式操作。Compaction、tool-result pruning 和 spill 可以减少后续上下文,但必须在日志里留下 replacement、summary 或 locator,使新 surface 能解释自己从哪里来。

代价是历史持续增长。Append-only 不等于无限保留所有字节;流式 chunk、工具大结果和 Code Mode 子调用都可能造成空间与 Token 压力。dsh 因此引入 compaction、checkpoint policy、spill store、projection cache 和 token meter。但任何压缩都必须区分三类信息:

  • 模型下一步必须看见的工作信息;
  • UI/审计需要保留,但不应重复进入模型的信息;
  • 可以由稳定外部 locator 按需恢复的信息。

对老板而言,这种设计的商业价值是可审计、可恢复和多端一致;成本是存储、迁移、隐私与数据保留治理。项目当前 Session format 仍是 v0,只对少数旧记录做窄转换,并不承诺一般兼容。这意味着试点阶段必须锁版本、保留迁移演练,而不能把事件日志当成永久稳定的企业协议。

“模型可见即已记录”最终是一条组织原则:任何团队想让 Agent 可恢复,就必须给模型认知建立一个权威来源。没有这条来源,所有“长记忆”“会话恢复”和“操作审计”都可能只是多个缓存碰巧一致。

源码核验索引

  • Session 核心:packages/core/session/src/index.ts
  • Surface 投影:packages/core/session/src/surface.ts
  • Session README:packages/core/session/README.md
  • 持久化协调:packages/session/session-persistence/src/coordinator.ts
  • Write-behind:packages/session/session-persistence/src/write-behind.ts
  • Client Conversation Nodes:packages/client/runtime/README.md

事实边界:append-only 指 Session 存储合同,不意味着底层后端永不压缩或项目已冻结长期格式。v0 当前只有明确列出的旧记录转换。