CHAPTER 07 · STAGE 02 · 追踪完整对话
Session:为什么对话是一份事件日志
一次 Agent 运行除了用户与助手消息,还包含 Turn/Step 边界、流式 chunk、工具调用、工具结果、请求头、取消原因、压缩记录和 UI 状态。如果只保存最后的 messages 数组,许多行为无法重放或诊断。不要把 Session 理解成一个 messages 数组
一次 Agent 运行除了用户与助手消息,还包含 Turn/Step 边界、流式 chunk、工具调用、工具结果、请求头、取消原因、压缩记录和 UI 状态。如果只保存最后的 messages 数组,许多行为无法重放或诊断。
DeepSeek Harness 把 Session 设计成只追加的 typed event log。每条事件有类型、连续 seq、时间和对应 data。模型历史、UI、持久化、遥测、Fork 和恢复都从这份日志派生。
Append-only 的含义
Append-only 表示已经写入的事件不在原位置修改。新事实通过追加新事件表达。
这不意味着界面永远显示所有旧内容。Compaction 可以追加 replacement 事件,让当前模型 surface 用摘要节点遮蔽旧节点;Todo 可以使用 whole-list snapshot 表达最新状态。但底层事实仍保留,便于重放和追踪来源。
最重要的事件
初学阶段先记住八类:
| 事件 | 表示什么 | 是否直接成为模型消息 |
|---|---|---|
turn/start |
一项工作开始 | 否 |
turn/end |
Turn 以某原因结束 | 否 |
step/start |
一次模型请求开始 | 否 |
step/end |
该模型请求及工具执行结束 | 否 |
user/message |
进入模型历史的用户角色消息 | 是 |
assistant/message |
一次成功模型调用组装出的回复 | 是 |
tool/call |
模型请求调用工具 | 作为助手消息的一部分重建 |
tool/result |
工具的模型可见结果 | 是 |
assistant/chunk 保存流式细节,方便实时 UI 和精确回放,但最终派生历史通常使用 assistant/message 作为完成锚点。
seq 与 time 各自解决什么问题
seq 是 Session 内的单调位置,通常等于追加时的日志长度。它建立可靠顺序,并允许事件引用早先事件。
time 是 Unix 时间戳,用于展示和时间分析。不能只靠 time 排序,因为多个事件可能拥有相同毫秒时间,而且系统需要严格的因果顺序。
为什么事件是 discriminated union
事件的 type 与 data 必须绑定。tool/result 的 data 与 turn/start 完全不同。类型定义让 switch (event.type) 后,TypeScript 自动知道 data 的具体字段。
SessionEventMap 允许插件通过 declaration merging 增加事件类型。因此处理事件时通常要为未知扩展类型保留 default,而不是把当前已知事件误认为永远完整的封闭集合。
Surface:日志中的哪些节点会进入模型历史
并非所有事件都会发送给模型。Turn/Step 边界、raw chunk、请求 context 等是日志事实,却不应该变成聊天消息。
Session 会从日志计算当前 Surface,再把 Surface 投影成 LLM Message。核心的 surface 事件包括 user/message、assistant/message 和 tool/result;Tool Call 与 Assistant 内容按照协议关系一起重建。
这解释了“日志”和“模型历史”的区别:日志追求完整事实,模型历史只选择模型协议需要的内容。
deriveMessages() 为什么是关键函数
deriveMessages() 不读取另一个独立保存的 messages 数组,而是从当前 Session surface 派生消息。这样持久化恢复、Fork 和 replay 都使用同一套逻辑。
如果产品把一段内容直接塞进模型请求,却不在日志中留下足以重建它的事实,恢复后请求就会变化。因此仓库用运行时不变式验证 AgentLoop 请求可以从 Session 重建。
源码入口:packages/core/session/src/index.ts。先搜索 deriveMessages,再反向寻找它使用的 surface projection。
request/header:不是消息,但仍影响模型请求
模型请求还包含 provider、model、系统提示词和 Tool Schema。这些内容不适合作为聊天消息,却属于请求输入。
AgentLoop 在 Step 内记录完整 request/header snapshot。后续只有请求 envelope 变化时再追加新 snapshot。恢复时选择最新头部,就能重建当前请求的非消息部分。
request/context 记录当前模型路由及 context window 等容量元数据。它描述路由事实,不直接参与消息历史。
来源引用与替换
Surface 事件可以通过 sourceEventSeqs 引用产生自己的早期事件。例如 assistant/message 引用组成它的 assistant/chunk;Compaction replacement 引用被替换节点。
这让系统不仅知道“当前显示什么”,还知道“它由哪些历史事实产生”。UI 重放、审计和压缩都能利用这种关系。
JSON 可序列化要求
Session 要持久化,因此事件 data 必须是 lossless JSON。Tool Result 的 meta 也必须 JSON 可序列化,才能在重放时还原相同 UI 表现。
函数、AbortSignal、打开的文件句柄或任意类实例不能直接作为事件 data。运行时对象属于 Agent/Capability 生命周期,持久事实需要转换成稳定 JSON 数据。
事件日志怎样支持 Fork 与 Replay
Replay 用现有事件 seed 创建新的内存 Session,再从相同事件派生历史。
Fork 可以复制源 Session 在某个边界前的事件作为 child seed,并记录 parentSession、seedLength 等 metadata。子 Session 后续追加自己的事件,不修改父日志。
Resume 则加载同一持久 Session id,继续原日志的 Turn 和历史编号。三者都依赖事件是连续、可验证和不可变的。
模型可见与日志可见并不相同
一个事件可能只给 UI 或恢复使用;一个模型可见内容却必须在日志中有来源。可以用集合关系记忆:模型输入是日志投影的一部分,加上可由日志重建的请求头,而不是运行时任意对象的快照。
本篇练习
- 阅读
packages/core/session/src/types.ts,找到 SessionEventMap。 - 把一轮“用户请求 → 模型调 Bash → 模型回答”写成事件序列。
- 解释为什么 assistant/chunk 和 assistant/message 都需要存在。
- 判断 AbortSignal 能不能直接放入 SessionEvent data,并说明原因。
- 找到
packages/core/session/tests中与 deriveMessages 或 surface 有关的测试。
验收标准:能区分“日志事实”“当前模型 surface”“最终 LLM Message”三层,并解释它们为什么不能合并成一个数组。