06. Session 与 Event Sourcing:日志、Surface、Replay、Fork 与恢复
系列:DeepSeek Harness 从入门到源码与二次开发
基于仓库:
deepseek-ai/deepseek-harness的master分支(核对日期:2026-08-26)项目状态:官方仍标记为 Developer Preview ,内部 API、配置结构和包边界可能继续发生破坏性变化。
阅读约定:本文优先解释"设计与执行链",示例代码若标注"示意",应以仓库当前类型定义与生成文档为最终准绳。

DSH 的 Session 文档非常长,因为它承载的是整个系统最关键的事实边界之一。最重要的一句话是:Session 是 append-only typed SessionEvent log;LLM history 从日志派生,不单独维护。
如果你只记住这一句话,仍然不够。下面需要理解:什么应该记录、什么进入模型 surface、为什么请求头也要记录、如何做 replay/fork,以及自定义业务状态应该怎么加入。
1. 为什么不直接存 messages[]
最简单的聊天系统会保存:
json
[
{"role":"user","content":"..."},
{"role":"assistant","content":"..."}
]
但 Agent 系统还存在:
- Turn/Step 边界;
- raw stream chunk;
- Tool Call/Result;
- 中途取消;
- system prompt/tool schema 变化;
- compaction;
- plan mode / goal / todo 等业务状态;
- resume/fork;
- 持久化 checkpoint。
如果这些都只靠 messages 推断,就会产生大量模糊状态。
2. Session Event Log 是 single source of truth
概念上:
text
seq=1 turn/start
seq=2 step/start
seq=3 user/message
seq=4 assistant/chunk
seq=5 assistant/chunk
seq=6 assistant/message
seq=7 tool/call
seq=8 tool/result
seq=9 step/end
seq=10 turn/end
所有派生视图都从这里来:
text
log → deriveMessages()
log → UI transcript
log → projections
log → goal/plan/todo state
log → resume
log → fork
3. Event vocabulary 是可扩展的
官方 Session 文档说明 SessionEventMap 可以通过 TypeScript declaration merging 扩展。
这意味着插件可以加入自己的 durable facts,例如官方已有的:
compaction/*;plan/mode;goal/change;todo/write;- 某些 hook protocol 的日志事件。
关键是:加入日志的应该是可恢复事实,不是随便什么内部变量。
4. Surface Event:哪些事件会影响模型看到的上下文
不是所有 Session Event 都进入 LLM history。
可以把事件分成:
text
log-only events
记录状态/审计,但不直接变成 message
surface events
通过 surfaceOp 影响模型可见历史
例如 request/header 是重要的请求状态快照,但它不直接成为一条模型 message;plan/mode 记录 plan state,也不是消息。另一方面 user/message、assistant/message、tool/result 等会形成模型上下文表面。
5. "Model-visible means logged"
一个很有用的设计原则是:如果某段内容真正进入模型可见上下文,就应该能从 Session 中追溯到它来自哪里。
比如通过 agent.inject() 注入的文件变化通知、skill catalog、goal continuation,不应该偷偷只存在于临时数组。它们通常以带 source 的 user/message 形式进入 surface。
这样 resume/replay 时不会"忘记模型曾经看见过什么"。
6. source 为什么重要
两条都是 user-role message,但来源可能完全不同:
text
真人输入
插件注入
goal continuation
skill invocation
如果只看 role,UI 和审计逻辑会误以为它们都来自用户键盘。DSH 的 message/source 设计让 provenance 与 role 分开。
这对科研系统尤其有价值:你可以区分"研究者给出的假设"和"系统自动注入的数据审计结论"。
7. Raw Chunk 为什么也写日志
有些系统只保存最终 assistant message。DSH 还记录 assistant/chunk,原因是 replay fidelity。
它能支持:
- UI 重放真实 streaming;
- 关联每个完整 message 的 source chunk seq;
- 诊断 provider 在中途失败时已经输出了什么;
- 区分"完全没有输出"和"输出了一半后取消"。
而 assistant/message 则是派生 history 使用的组装结果。
8. 中途取消如何表达
Session 文档当前说明:如果 Turn 在 streaming 中途被取消,已交付的 text/reasoning 前缀可被最终化为带 interrupted: true 的 assistant/message;没有 dispatch 的 tool calls 不应出现。
这很重要,因为不能简单根据 turn/end reason 推断每条 assistant 内容是否完整。事件自己携带更精确事实。
9. request/header:为什么模型请求配置也应该可重建
当前 Session 文档把 request/header 作为 logged request state,其中包含类似:
- call configuration;
- adapter-resolved defaults markers;
- rendered system prompt;
- assembled tool schemas。
意义是让"一次会话请求"更接近日志的纯函数:不是只知道说了什么,也知道当时 模型被怎样配置、系统提示是什么、有哪些工具可用。
这对严格实验复现非常关键。
10. Replay:不是读取缓存的 messages
Replay 更接近:
text
读取事件序列
↓
按规则 fold / derive
↓
重建模型 surface / projections
所以当新业务状态加入时,通常应该同时考虑:
- event schema;
- replay/fold 规则;
- invalid event 如何 fail;
- projection 如何从 seq 增量更新。
11. Fork:为什么事件日志天然适合分叉
一个 Session 可以在某个历史点分叉出新的工作线。事件溯源的优势是:
text
共同前缀 events
├── branch A 后续 events
└── branch B 后续 events
而不是复制一堆难以解释的 mutable object 状态。
对科研流水线,这可以对应:同一数据审计结论后,分叉 3 个模型想法分别实验,最后再汇总。
12. Compaction 不等于删除历史
DSH 的 compaction 不是"把旧 events 真删掉"。它通过 compaction events 与 surface replacement 等机制,让模型可见面变短,同时保留可审计历史。
这比直接从数据库删除旧 message 强很多,因为你仍然知道:
- 哪些事件被摘要覆盖;
- 摘要由哪次模型调用产生;
- token 压力状态;
- 何时发生 replacement generation。
13. Projection:不要每次都从头扫描整个日志
随着 Session 变长,某些 UI/业务状态不应该每次都全量重放。DSH 还有 session projection / projection cache 等能力,用 fold unit 和 watermark 维护增量视图。
理解层次:
text
Event Log = 事实源
Projection = 从事实源得到的可查询视图
Cache = Projection 的性能优化
不要把 cache 反过来当事实源。
14. 自定义科研事件怎么设计
例如你需要保存 Idea 生命周期,可以设计:
text
research/idea-created
research/idea-evaluated
research/idea-selected
research/idea-rejected
事件数据应尽量包含稳定、可重放事实,而不是临时 UI 文案。例如:
json
{
"ideaId": "I-003",
"hypothesis": "...",
"evidenceRefs": ["artifact:..."],
"decision": "rejected",
"reasonCode": "data-leakage"
}
然后通过 fold 得到当前 Idea Registry view。
15. 什么时候不该写 Session Event
以下通常不适合:
- 当前按钮 hover 状态;
- 纯 UI 展开/收起;
- 一个可从 durable data 重新计算的临时缓存;
- 单进程内短暂锁;
- 不需要跨 resume/fork 保留的对象引用。
判断问题:进程崩掉并恢复后,我是否必须知道这件事发生过? 如果答案是否定的,通常不需要 durable event。
16. 本篇实践
设计一个"实验运行"事件链:
text
experiment/submitted
experiment/started
experiment/metric
experiment/artifact
experiment/completed
experiment/rejected
然后分别标记:哪些进入模型 surface,哪些 log-only,哪些应该通过 projection 汇总成当前状态。
小结
Session Event Sourcing 把 DSH 从"聊天记录程序"提升为可恢复运行时:事件日志保存事实,surface 决定模型可见历史,projection 负责高效视图,persistence 负责落盘,resume/fork 都从同一事实源重建。 对需要科研审计、长流程自动化和多分支实验的系统,这一层尤其重要。
官方参考资料
- https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/session.md
- https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/persistence.md
- https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/compaction.md
- https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/persistence-catalog.md