
字节笔记本
2026年10月6日 · 约 7 分钟读完
DeepSeek Harness 会话事件溯源设计解析
DeepSeek Harness(dsh)是 DeepSeek 开源的 agent 框架,主打「一切皆插件」,整体构建在 Cordis 插件框架之上,目前在 GitHub 上处于开发者预览阶段。框架里最核心的一块基础设施是 Session 会话子系统:它把 agent 与用户、模型、工具之间的全部交互,收敛成一份只进不改的事件日志,再从这份日志按需派生出模型真正看到的对话历史。本文基于其官方文档的 session 子系统篇,梳理这套事件溯源设计的关键决策。

一份只进不改的事件日志
Session 的本质是一个由类型化事件组成的仅追加日志,也是 agent 完整交互历史的唯一真源。模型看到的消息历史并不单独存储,而是从日志派生出来的投影,任何时候都可以从同一组事件重新推导,回放即派生。每条事件携带单调递增的序号 seq,seq 恒等于日志长度,序号必须连续、不能出现空洞,同时附带毫秒时间戳。
日志对数据有硬约束:所有事件数据必须能无损序列化为 JSON。append 在写入现场做递归校验,BigInt、函数、Symbol、循环引用、Map 和 Set 这类不可序列化的值会被直接拒绝,坏事件根本进不了日志。已写入的事件在入库时被深度冻结,事后既改不动历史,也无法让校验读到与实际入库不同的值。
开放的事件词汇
核心事件类型由 SessionEventMap 定义,大致分几类。轮次与步骤边界:turn/start、turn/end、step/start、step/end;产生消息的三类事件:user/message、assistant/message、tool/result;流式分片:assistant/chunk,逐 token 记录,保证回放保真;仅记日志的状态:todo/write 全量待办快照、request/header 请求头快照、request/context 路由容量,以及标记种子边界的 session/end-seed。
这套词汇是开放集合:插件可以通过声明合并追加新类型。例如压缩子系统引入 compaction/start、compaction/summary、compaction/end 三连事件,钩子桥接层引入仅记日志的 hook/invoked 与 hook/result 记录。也正因为集合可扩展,代码里对事件做 switch 时禁止穷尽断言:插件添加的变体是合法的未知值,处理完已知分支后必须在 default 里放行。
消息历史是投影,不是存储
三类产生消息的事件合称 surface 类型,每条都必须声明自己如何进入有序的 surface:默认是 append 尾部追加;另一种是 replace 位置替换,把一段旧节点整体遮蔽,再在原位插入新事件。长对话压缩就靠 replace 实现:旧消息被摘要概括后从派生历史里消失,但日志原文一字不动。
deriveMessages() 把日志投影成模型看到的消息数组。投影规则很克制:user/message 原样映射为 user 消息;assistant/message 映射为 assistant 消息;tool/result 映射为携带工具结果块的 user 消息;文件变更通知、技能内容这类注入上下文,同样以 user 消息的身份按时间顺序进入。其余事件,包括流式分片和所有边界标记,都是结构信息,不进消息历史。两个细节值得注意:内容为空的 assistant 消息(比如被输出上限截断且没有任何文本)不会进入对话记录,但事件本身保留下来,用于保存用量、提供方与模型信息;token 记账优先读用量分片,缺失时才回退到消息上的 usage 字段。
投影是缓存的:每个节点只在首次出现时计算一次,之后每步成本只与新事件成正比,surface 发生替换时整体重建。返回的数组每次都是新快照,数组里的消息对象共享且深冻结,消费方不可能通过投影改写已记录的历史。
请求头快照:请求是日志的纯函数
request/header 事件把完整请求信封写进日志:调用配置、适配器补齐的默认值、渲染后的系统提示词、组装好的工具 schema。每个循环实例的第一个快照以 initial 或 resume 为原因落账,之后请求参数一变,就再写一份完整快照,原因标记为 change,重建时取最新快照即可。这样每个对话请求都是日志的纯函数,出了问题可以直接对账。紧随其后的 request/context 记录路由容量元数据,包括提供方、模型和上下文窗口,只在路由或容量变化时追加;它与请求头分开记录,因为容量描述的是路由而不是请求输入,混在一起会把一次容量变化误登记为请求信封变更。

轮次边界与结束原因
一个轮次包围一次模型循环执行,而不是整个会话日志。轮次里可以有一个或多个步骤,一个步骤是一次模型调用加上它触发的工具执行。轮次如何结束由类型化原因说明:completed 表示正常完成;aborted 表示被取消请求打断;blocked 表示阻塞;error 携带结构化失败信息;max-tokens 表示有步骤触及输出上限,且只要轮次内出现过截断,整个轮次就按 max-tokens 计,截断事实优先于正常完成;interrupted 最特殊,它不由任何运行中的循环发出,只由持久化层的崩溃恢复合成,用来关掉崩溃时悬空的轮次。
fork 是这套日志的另一个受益者。会话存储提供 fork 接口,从活跃会话切出一个事件前缀生成子会话,边界可以指定到任意事件序号,但接口拒绝结束在开放轮次内的前缀,宁可报错也不静默截断。带种子创建的会话(恢复、fork 或回放)会在种子之后写入一条载荷为空的 session/end-seed 事件,把继承的历史与本生命周期的新写入划清界限;种子已以此事件结尾时不会重复标记,重新打开一个未被改动的会话不会让日志增长。
持久化约定与协作边界
日志怎么落盘由独立的持久化子系统负责,会话模块只立规矩:后端必须无损保存每个事件,包括流式分片,且不能破坏 seq 连续性,加载回来的事件要与追加时完全一致。需要立即落盘的生产方,显式等待 ctx.sessions.flush() 这个持久化屏障。会话存储同时提供 create、fork、flush 等服务与一组生命周期事件:session/created、session/event、session/flush、session/disposed,持久化插件订阅这些事件异步缓冲写入,热路径上的追加永远不阻塞在 I/O 上。
小结
这套设计的核心取舍,是把发生过什么与模型看见什么彻底分开:前者是不可变的事实账本,后者是可重建的视图。换来的是四件事:完整回放,连 token 分片都在;安全 fork,边界有类型化保证;可靠崩溃恢复,悬空轮次被合成原因关闭;无损压缩,摘要替换视图而原文永不丢失。项目整体还在开发者预览阶段、接口可能变动,但对任何在做 agent 运行时的人来说,这份会话子系统文档都值得当作事件溯源的实战教材读一读。



