DeepSeek Harness 源码解读(七):会话日志为何是唯一真相源

DeepSeek Harness 源码解读(七):会话日志为何是唯一真相源

工具和 Provider 都可以替换,但模型下一次请求必须仍然能够解释"之前发生了什么"。DeepSeek Harness 把这项责任交给一个追加事件日志,并从日志推导模型消息、请求头、分叉和持久化。本文从 Session.append()surfaceOpderiveMessages() 和持久化协调器出发,说明"模型可见 ⟺ 已记录"如何落到代码。

项目地址:https://github.com/deepseek-ai/deepseek-harness

源码基线:仓库版本 0.1.0-rc.5,重点是 packages/core/sessionpackages/session/session-persistencepackages/session/session-persistence-jsonl 及对应测试。

一、本章要回答的问题

Agent 运行时同时拥有几种看起来都像"历史"的数据:模型消息数组、流式 chunk、工具调用状态、请求配置、UI 状态和磁盘 JSONL。如果每个消费者维护一份自己的真相,恢复时就会出现模型看见但日志没有、日志存在但模型历史不再包含、分叉复制了错误的尾部等问题。

会话日志要解决的不是"把聊天保存下来"这么简单,而是四个一致性问题:哪些事件产生模型消息;消息被替换或压缩后如何回放;一个新输入怎样和旧请求配置关联;进程崩溃或热重载时怎样保证不会重复写入或静默丢失。当前实现的答案是:Session 只追加经过验证的事件;surfaceOp 标记消息事件如何进入有序 surface;deriveMessages() 从 surface 投影消息;持久化插件异步复制冻结事件,但不改变 Session 的权威性。

二、核心结论

第一,Session.events 是事实,deriveMessages() 是投影。模型请求、UI 回放和部分持久化查询都应从同一事件序列派生,而不是各自维护可变数组。第二,只有 user/messageassistant/messagetool/result 进入模型可见 surface;assistant/chunk、turn/step 边界、usage、错误和审计事件仍然记录,但不会直接变成消息。第三,写入点负责校验 JSON、序号连续性、surface 来源和替换范围,坏事件在 append() 处失败。第四,持久化是追加事件的异步消费者,写入批处理有边界但不会改变事件顺序;冷加载会检查版本、未知事件和 crash tail。

因此"模型可见 ⟺ 已记录"可以写成一个工程不变量:任何进入下一次模型请求的内容,必须能由当前 Session 事件前缀重新构造;任何只存在于内存、没有事件证据的文本都不能成为模型历史的一部分。反向并不要求所有日志事件都进模型,审计和调试数据可以是 log-only。

三、相关包与源码入口

责任 源码入口 事实
事件类型与版本 packages/core/session/src/types.ts SessionEventMapSESSION_FORMAT_VERSION = 0SessionHeader
追加与快照 packages/core/session/src/index.ts Session.append()、连续 seq、深冻结、发布事件
模型历史投影 packages/core/session/src/surface.tsindex.ts surfaceOp 验证、deriveEventMessage()deriveMessages()
分叉 packages/core/session/src/index.tstests/fork.spec.ts SessionStore.fork()seedLengthparentSession
JSONL 持久化 packages/session/session-persistence-jsonl/src/index.ts JSONL/Zstd 后端、头部、追加批次、crash tail
写入协调 packages/session/session-persistence/src/coordinator.ts session/event、write-behind、flush、id 串行化
可重建性测试 packages/core/session/tests/session.spec.tsfork.spec.tsinvariant.spec.ts 投影、冻结、来源和分叉边界

四、Session:追加事件的所有权边界

4.1 Header 和 event 是两类数据

SessionHeader 保存 version、id、createdAt、cwd、parentSession、seedLength、delegationDepth 等存储元数据。它不进入事件日志,因为这些字段描述存储身份和分叉血缘,而不是按时间追加的对话事实。新建 Session 时版本从单一常量 SESSION_FORMAT_VERSION 读取;持久化后端加载时拒绝其他版本,当前预发布格式没有迁移承诺。

事件则拥有固定 envelope:type、连续 seq、时间、data,可选 ignorablesurfaceOpsourceEventSeqsSession.append() 先对 data 和 surface 元数据做一次 lossless JSON 快照,再校验请求头和 surface transition,最后给事件深冻结并加入私有 log。事件一旦进入 log,观察者抛错也不能撤销本次 append。

ts 复制代码
const event = deepFreeze({
  type,
  seq: this.log.length,
  time: Date.now(),
  data: dataSnapshot,
  ...surfaceMetadataSnapshot,
})
this.log.push(event)

公共 events getter 返回冻结数组的快照,不会随着后续 append 自动增长。这样,一个模型请求拿到的消息数组不会在请求进行中被另一条输入悄悄修改;下一次请求重新调用 deriveMessages() 才能看到新事件。

4.2 发布和持久化解耦

Append 的热路径同步通知 session/event,但不等待文件 I/O。持久化插件监听事件,将冻结事件复制进自己的 write-behind 缓冲。session/flush 是显式耐久屏障;Session 本身仍是进程内真相,后端失败不能让已经接受的事件从内存日志消失。这个分层让 Agent Loop 不会因为每个 token 或工具结果都 fsync 而被磁盘延迟锁住,同时保留退出时 drain 的机会。

五、Surface:从事件序列得到模型消息

5.1 只有三类事件生产消息

surface.tsSURFACE_EVENT_TYPES 只包含 user/messageassistant/messagetool/resultderiveEventMessage() 对这三种事件做逐节点投影:用户消息原样返回,assistant message 使用已冻结的 message,tool result 返回其 result message;空内容 assistant message 返回 null,因为它有时只承载 max-token usage。其他事件统一投影为 null

这条白名单是模型可见性的实质边界。assistant/chunk 仍然被记录,支持流式回放和故障定位,但不会被重复拼成另一个模型消息;turn/startturn/end 描述生命周期,不会污染模型 transcript;approval/askedapproval/decided 是审计事件,模型只有在另一个明确的 runtime-context 或 user message 中收到相关信息时才可见。

5.2 surfaceOp 记录"如何进入"

每个 message-producing append 必须提供 surfaceOpappend 把事件放到 surface 尾部;replace { start, end } 用新的 message 事件替换一段旧 surface,并且 sourceEventSeqs 必须逐个列出被遮蔽的旧节点。SurfaceManager.validateNext() 在 log 改变前验证起止节点、顺序、来源引用和完整覆盖,避免产生只能在回放时发现的歧义。

压缩场景因此不需要删除历史。原始事件仍在 append-only log 中,新的 assistant message 可以用 replace 操作成为当前模型 surface。人类审计若想看完整过程,应读取 append-origin 事件;模型请求则读取当前 surface,二者都从同一日志得到。

5.3 deriveMessages() 具有可缓存但不拥有事实

Session.deriveMessages() 按 surface nodes 顺序增量折叠。没有 replace 时只投影新节点;replaceGeneration 变化时重建缓存。返回的是新数组,但里面的 Message 对象复用已冻结的事件数据。缓存只是性能优化,不是第二份事实,因此重启或跨进程读取时可以用纯 projection 重新得到相同消息。

六、工具调用如何证明模型历史是可重建的

工具调用是最容易打破一致性的地方。Agent Loop 在收到模型工具 block 时先追加 tool/call,它不是 surface 事件;工具执行完成后,appendToolResult() 创建 createToolResultMessage() 并追加带 sourceEventSeqs: [callSeq]tool/result。因此:

  • 调用参数在日志中有原始字符串,取消、拒绝和未知工具也有 tool/result 结果;
  • result 明确引用 call 事件,surface 校验可以检查来源;
  • 模型下一步只读取 tool/result message,不读取工具私有 value;
  • 并行调用仍按模型顺序提交,重放不会因为完成时序不同而改变 transcript。

这也是为什么工具执行链不能只返回一段字符串。错误 code、presentation meta 和 additional context 都需要在结果或后续明确事件中留下可重建信息;一个只存在于 Promise 闭包里的"额外提示"不能成为下一次请求的隐形输入。

七、分叉、恢复与 rewrite 的边界

7.1 Fork 是 seed,不是复制可变对象

SessionStore.fork(source, boundary, id) 会选择一个已有的 turn/end 边界(未指定时取最近完成边界),把该边界之前的事件复制成 detached seed,并在 child header 中写入 parentSessionseedLengthSession 构造函数验证 seed 的每个 envelope、seq 连续性和 surface transition,然后追加 session/end-seed 标记,告诉持久化消费者从哪里开始区分 inherited prefix 与 child suffix。

子会话的前缀和父会话事件值相同但不共享可变引用;后续 child append 只改变 child log。测试 fork.spec.ts 还覆盖了空会话、较早边界、开放尾部和非法边界,防止"复制当前最后一条事件"这类含糊规则。

7.2 Rewrite 只作用于模型 surface

当前 surface 的 replace 是追加一个新的替代事件,并记录被遮蔽来源,原始事件仍留在 log。恢复时先重放所有事件再折叠 surface,不通过直接修改数组实现"删除"。这让日志保留审计证据,也让不同消费者可以选择当前模型视图或完整 append-origin transcript。

7.3 请求头让配置也成为可重建事实

request/header 事件保存一整个 EpochHeader:provider、model、reasoning effort、sampling、system prompt 和工具 schema。request/header 不直接进入消息 surface,但恢复时可以折叠出历史请求的配置;新的 turn 仍要根据当前装配重新组装 prompt 和 tools。配置变更不是悄悄改一个内存字段,而是追加一个新的 header epoch。request/context 则记录 exact provider/model 的 context metadata,供后续请求和 token 计算使用。

八、持久化:异步批处理和冷恢复

8.1 Write-behind 不是弱化事件语义

PersistenceCoordinator.installWritePath() 监听 session/createdsession/eventsession/flushsession/disposed。每个 Session 有独立 write-behind controller;第一条待写事件启动固定窗口,后续事件加入但不会不断重置截止时间。批次按 session id 串行化,flush 会取消等待并排空期间新加入的事件。后台失败会保留缓冲并记录警告,下一次显式 flush 或 teardown 重新尝试。

这个设计牺牲了一小段写入延迟,换取 token chunk 和连续工具事件不必各自触发文件写入。它不改变事件的 seq,也不允许批处理重新排序;JSONL 后端可以将一批事件写成一个 Zstd frame,但读取后仍得到逐事件、连续 seq 的逻辑日志。

8.2 JSONL 后端的身份和 crash tail

JSONL 后端以 header frame 开始,后续是事件记录或压缩的 chunk row。首次 append 才物化文件;每批写入后同步到磁盘,写入失败会回滚到之前字节长度。加载时会校验 session id、cwd、版本和完整事件序列。尾部若只有最后一帧/行不完整,后端截断无效尾并按持久化契约补出工具、step、turn 的恢复性 closers;已提交 turn/end 之前的损坏则拒绝,不能用"尽量读出来"掩盖 corruption。

未知事件也不是一律跳过:assertEventsSupported() 只接受当前已知事件或明确 ignorable: true 的扩展。因为未知事件可能改变 surface 或恢复状态,静默忽略会让旧 runtime 得到错误历史。新增普通 log-only 事件可借助 ignorable 机制增长词汇,结构性变化才需要 bump SESSION_FORMAT_VERSION

九、亮点、约束与代价

9.1 亮点:一个日志服务多个投影

模型请求、UI、遥测、回放、分叉和持久化都可以读取同一事件序列,再按需要投影。工具结果的 meta 可供 UI 重建卡片,消息 projection 只取 content;审计事件保留完整原因但不上 wire。新增消费者不必向 Agent Loop 再索取一份私有状态。

9.2 约束:追加事件必须可验证

每个事件都要是 lossless JSON,seq 必须连续,surface 事件必须声明操作和来源,消息 identity 需要保持稳定。这个约束让错误尽早暴露,却也使"随手 append 一个对象"变得困难;插件作者要在 SessionEventMap 做声明合并,并为模型可见事件补齐 surface metadata。

9.3 代价:日志会增长,投影和恢复有成本

追加日志不提供内建删除/保留策略;长会话需要 compaction 或外部存储治理。deriveMessages() 在 replace 后要重建缓存,冷恢复需要读取和校验完整前缀。JSONL 压缩降低字节量,但压缩文件不适合直接用普通文本工具逐行查看。另一个明确代价是:持久化崩溃恢复会添加模型可见的 synthetic error,模型必须根据提示谨慎重试可能产生副作用的工具。

十、扩展方式:添加一个事件或投影

新增事件前先决定它是否模型可见。如果只是审计、指标或生命周期记录,保持 log-only,不携带 surfaceOp;如果生成用户、助手或工具消息,加入 SessionEventMap,声明 SurfaceEventType 所需的 surface metadata,并在 deriveEventMessage() 中提供纯 projection。事件数据必须 lossless JSON,且跨重启、分叉和回放仍能解释。

测试至少覆盖:非法 envelope 和非 JSON 值在 append 处拒绝;sourceEventSeqs 缺失或重复时 replace 失败;deriveMessages 与 log replay 结果一致;fork 的 seedLength 和 parentSession 正确;持久化 load、flush、crash tail 和未知事件策略符合预期。若新事件改变模型输出,必须通过真实组装应用增加 keyless snapshot,而不是只测一个孤立 Session。

不要把派生消息写回一个新的"聊天数组"服务,也不要让 Consumer 读取某个 Provider 的私有缓存。事实应该进入 Session;派生结果应能从日志重算;缓存失效时只影响性能,不影响语义。

十一、小结

DeepSeek Harness 的会话日志不是数据库附属物,而是连接 Agent Loop、工具、Provider 和恢复流程的共同事实层。Session.append() 负责接受并冻结事件;surfaceOp 负责建立模型可见顺序;deriveMessages() 负责从 surface 生成请求历史;SessionStore.fork() 负责以 detached seed 延续血缘;Persistence Coordinator 负责异步、顺序、可恢复地复制到 JSONL/SQLite 等后端。

当底层能力可以替换时,只有可重建的日志事实能保持行为连续。理解这条规则后,后续分析安全边界、沙箱、审批和实际部署时,都应先问一句:这项新能力让模型看到了什么,是否已经拥有对应的 session event?

相关推荐
武子康1 小时前
机器人策略 90% 与 92%:为什么两个百分点通常不足以证明更
人工智能·llm·agent
ovO1 小时前
DeepSeek Harness 源码解读(十):六条设计纪律如何约束可替换运行时
开源·agent
阿里云大数据AI技术1 小时前
PAI支持一键部署Qwen3.8-Flash-Next、GLM-5.3等最新开源模型
人工智能·开源·llm
ovO1 小时前
DeepSeek Harness 源码解读(八):文件、命令、审批与沙箱如何协作
开源·agent
阿里云云原生1 小时前
经验自进化:自动挖掘经验资产,消融实验验证真实收益丨AgentLoop 数据飞轮实践(五)
agent
ovO2 小时前
DeepSeek Harness 源码解读(六):Provider、Consumer 与能力接缝
开源·agent
李燚2 小时前
把规则搬回家:三个 BC 的贫血→充血重构实录(第103篇)
golang·agent·ddd·领域驱动设计·eino·deepflux·eino adk
2601_962304913 小时前
2026年健康科普视频怎么制作:一条开源工具链从全手动到半自动的工程复盘
开源·音视频
ClouGence3 小时前
CloudDM 支持达梦、KingbaseES、GoldenDB,国产数据库也能统一管起来
数据库·sql·开源