ARTICLE · 980461
DeepSeek Harness 源码导读 04|Agent 怎样记住过去
一个任务跑了 40 分钟,读过 30 个文件,改到一半时进程退出。 |
重新打开页面后,如果系统只保存最后一段聊天文本,它不知道哪些工具真的执行过,也不知道哪次审批已经结束。继续运行就像拿到一本缺页的施工日志,最后一句还写着“马上完成”。
DeepSeek Harness 把会话设计成一条只追加的事件流,正是为了避开这个坑。
01真相不是聊天气泡
页面上的对话是一个视图,不是全部事实。
一次完整任务里,系统还需要保存轮次边界、步骤边界、模型流式片段、最终消息、工具调用、工具结果、审批问答、权限变化、压缩记录和子 Agent 关系。
这些事实统一写成SessionEvent。每条事件至少有类型、递增序号、时间和对应数据。
一条真实事件在概念上接近这样:
seq是会话内连续递增的位置,不是全局数据库 ID。time负责时间信息,data则由事件类型决定。所有事件数据都必须能无损表示为 JSON,无法序列化的对象会在追加入口被拒绝。
工具调用保留模型产生的原始参数字符串,工具结果再用同一个callId配对。这样系统既能还原“模型当时要求什么”,也能确认“宿主最后交回了什么”。
典型的一段日志是:
事件一旦追加,就不原地修改。当前状态通过事件计算出来。
这和银行流水有点像。余额可以随时重算,但不能因为今天心情不错,就把上周那笔支出擦掉。会计会先不高兴,恢复逻辑随后也会加入。
02“模型可见即已记录”
DeepSeek Harness 有一条很硬的原则:任何进入模型请求的内容,都必须能从会话日志重建。
用户消息当然要记录。插件注入的上下文、模型选择变化、压缩摘要等只要影响模型,也要找到自己的持久事件。
这样做带来一个直接结果:请求不是靠运行时内存临时拼出来的黑盒。给定日志和当前插件规则,系统能解释模型为什么看到了这些内容。
deriveMessages()会从日志投影模型历史。这里的“投影”不是复制,而是按规则把底层事件变成某个用途的视图。
同一份日志可以产生不同投影:
模型需要有角色和内容的消息历史 Web UI 需要流式文字、工具卡片和状态 SDK 需要事件通知与最终回复 统计模块需要 token 和耗时 transcript 导出需要适合人读的记录
大家读取同一组事实,就不必各自维护一份容易漂移的“当前状态”。
日志、Surface 和 Projection 不是同一个东西
这三个词在源码里经常一起出现,可以这样区分:
SessionEvent,包括边界、chunk、审批和工具事件 | ||
deriveMessages() | ||
核心 Surface 只由user/message、assistant/message和tool/result三类事件产生。turn/start很重要,但它不是模型消息;assistant/chunk用于保真,也不直接进入下一次模型历史。
普通 Surface 节点追加到尾部。压缩时,新的摘要节点可以通过surfaceOp: replace替换一段旧 Surface;被替换的原始日志事件仍然存在,只是下一次派生模型历史时不再逐条展开。
Projection 则面向“当前值”。例如权限插件可以把一串sandbox/mode事件折叠成当前沙箱模式,UI 不必自己重演领域规则。每个投影单元都应是同步纯函数:旧状态加一条已提交事件,得到新状态。
03为什么同时保存 chunk 和 message
模型流式返回时,每个片段都写成assistant/chunk。UI 可以立刻显示,回放也能复现当时的输出过程。
提供方调用完成后,系统再写assistant/message。它保存最终内容、停止原因、用量和对应的 chunk 序号。
看起来有重复,其实职责不同。chunk 保真,message 定案。
assistant/message还会通过sourceEventSeqs指向组成它的 chunk 序号。即使提供方完整返回了空流,也可以显式记录空列表,表示“已知这次没有任何 chunk”,而不是“旧格式没有保存来源”。
如果运行在流式输出中途被取消,系统可以把已经交付的文本或推理前缀写成带interrupted: true的assistant/message。没有真正发出的工具调用不会凭空补进日志。用户因此能看见已经出现过的内容,恢复逻辑也不会把半个调用当成完整动作。
如果最终内容为空,message 仍可能存在,用来保存这次请求确实发生过以及消耗了多少 token。系统不会为了页面好看,把账目一起扔掉。

04进程重启后怎样恢复
默认组合会把会话持久化到存储后端,其中包括 SQLite 实现。加载会话时,系统读取事件,检查格式和序列,再恢复可继续使用的 Session。
恢复不是“把最后一条消息再发一次”。系统要先判断日志停在什么边界。
如果上一个轮次已经完整结束,新输入就打开新轮次。
如果日志来自中断现场,恢复逻辑会按明确规则收口或继续,避免重复提交已经完成的工具结果。
会话还可以 fork。子会话从某个事件边界继承历史,随后拥有自己的新增事件。它不是复制一段页面文字,而是从有序事实上分叉。
这为 Subagent、实验性协作和“从某个节点另开思路”提供了基础。
用一个中断现场看恢复为什么困难
假设任务日志停在下面的位置:
这和“两个工具都没运行”完全不同,也和“两个工具都完成,只是页面没刷新”不同。若系统只保存最后一段助手文字,就无法判断工具 B 有没有产生外部效果,贸然重跑可能造成重复写入。
因此,可靠恢复需要边界事件、工具调用与结果配对、持久化检查点和具体能力自己的恢复纪律共同工作。Session 日志提供事实,但不会神奇地让所有外部副作用变成可重试事务。
对读者来说,最重要的安全结论是:
“日志能恢复”不等于“任意工具都能自动重放”。 只追加记录能帮助系统识别中断位置,但外部 API、进程和文件操作仍要设计幂等性或补偿策略。 SDK 等待 idle后再读取持久层时,还应显式 flush;轮次边界本身不代表所有后端已经同步落盘。
05历史太长怎么办
日志可以一直追加,但模型上下文有上限。两者不能混成一件事。
DeepSeek Harness 的 compaction 不会删除原始会话日志。它改变的是“下一次模型请求如何投影历史”。
自动压缩会在agent/pre-step检查上下文压力。遇到规范的上下文溢出时,agent/request-error也能触发恢复流程。
压缩通常先尝试修剪可省略的工具结果,再生成摘要。只有当替代后的模型表面真正前进,系统才开启新的重试轮次;如果压缩没有产生有效变化,原始错误仍然成立。
可以把它理解成:仓库保留完整账本,模型拿到的是整理后的工作摘要。账本没有被撕,桌面只是终于能放下咖啡了。
压缩具体替换了什么
压缩处理的是 Surface,而不是简单按事件序号删除“最老的 100 条”。它必须选择一个工具调用与工具结果仍然配对的安全范围,否则摘要可能保留一半问题、丢掉一半答案。
一次成功压缩通常会留下自己的事务事件,例如开始、摘要和结束标记,再插入一条代表压缩检查点的模型可见消息。原始assistant/chunk、工具事件和审批事件仍在底层日志里,审计与 UI 回放仍可访问。
自动触发有两条入口:正常请求前的上下文压力检查,以及模型提供方明确返回上下文溢出后的恢复路径。两者都会先尝试更便宜的工具结果剪枝,再决定是否生成摘要。只有新的 Surface 确实比原来更小、更能继续请求,系统才会开启重试轮次。

06把三个层次分开
Session 是运行时里的会话对象,负责追加事件并提供读取入口。
Persistence 是存储层,负责把事件可靠写到 SQLite 等后端,并在下次启动时读回来。
Projection 是解释层,负责从同一组事件计算模型历史、界面节点或统计视图。
这三者常被一句“保存聊天记录”糊在一起。分开以后,替换数据库不必改模型消息规则,调整 UI 展示也不必重写原始事件。存、算、看各有负责人,出了问题不用全体开会。
07持久化不等于永远兼容
当前项目仍是 Developer Preview。
仓库明确说明,首个正式标签发布前,磁盘格式可以发生破坏性变化,后端会拒绝旧格式,而不是偷偷猜测如何兼容。SQLite 使用单调递增的 schema 版本,会话格式目前也没有长期兼容承诺。
这是一种“尽早暴露不匹配”的选择。对开发者来说,意味着升级版本前要备份数据,并阅读迁移说明。不要拿预览版会话库当传家宝,至少现在还不合适。
08日志还解决了什么
第一是审计。系统可以回答某个工具何时被请求、是否获批、最后返回什么。
第二是 UI 一致性。刷新页面后,工具 diff 和终端结果能从持久元数据重建,而不是再次执行工具。
第三是 SDK。客户端监听同一条事件流,等待 Agent 回到idle,再从最后一条根会话assistant/message派生最终回复。
第四是测试。仓库里大量快照测试会重放录制的会话,验证界面和模型可见输出是否变化。测试不必每次都花 API 费用,也不会被模型当天的心情影响。
一条事件提交后,谁会用到它
以tool/result为例:
Session.append()校验 JSON、分配 seq并提交事件。session/event把已提交事实广播给实时观察者。 持久化插件把它写入 SQLite 或 JSONL 等后端。 下一步骤的 deriveMessages()把模型可见内容投影进历史。Web 或其他客户端根据事件与持久 meta渲染完成后的工具卡片。统计、遥测和领域 Projection 各自折叠自己关心的字段。
这些消费方读取同一事件,却不共享一份可随意修改的“当前聊天对象”。这正是事件溯源在 Agent 场景里的价值:模型、界面、SDK 和恢复逻辑可以各自拥有视图,但不能各自发明事实。
09读源码的顺序
先挑一段真实session.jsonl,按seq看事件,再回头读类型。直接从持久化 SQL 开始,很容易研究了半小时页大小,却还没看见一条用户消息。
练习时,可以在 Web UI 完成一个含工具调用的短任务,刷新页面,再检查文字、工具卡片和最终状态是否都能恢复。
10三句话收住
会话日志保存发生过的事实,聊天界面只是其中一种投影。
模型看到的内容必须能从日志重建,恢复、fork、SDK 和审计因此共享同一基础。
压缩改变模型拿到的历史,不删除原始事件。
下一篇,我们讨论最容易被一锅端的三个概念:权限、审批和沙箱。它们层层配合,但谁也不能替另外两个值班。
谢谢你读我的文章。 如果觉得不错,随手点个赞、在看、转发三连吧🙂 如果想第一时间收到推送,也可以给我个星标⭐~ |