← 返回目录

第 5 卷

第 5 / 6 卷日期:

5user | <tool-result> 6user | 那算了,换个问题</tool-result></tool-call>

事件 12 条 = 继承的 8 条 + 一条 session/end-seed + 新写的 3 条。

消息历史是完整的:继承来的提问、助手的工具调用、那条“结果未知”的交代,再加上新问的这一轮。对模型而言这是一段连续的对话,而且它清清楚楚知道上一次调用没拿到结果,不会

傻乎乎地以为天气已经查到了。

其中第三条 <tool-result></tool-result> 正是上一节修复补出来的,修复的成果直接流进了新会话的消息历史,这就是两节之间真正的接口。

seedLength = 8 是关键。有了它,任何读这份日志的人都能分清:前 8 条是从 sesstool 继承的,后面才是这个新会话自己的工作:

复制代码 1磁盘上那一份(header.seedLength = 8)

2 3seq0 ─────── 7 ┆ 8 ─────── 11

4└── seed ───┘└── live ───┘ 5┆

6└─ end-seed 7

8seed = 从 sess-tool 继承来的 8 条;live = 这个会话自己写下的 9seedLength 存在 header 里;firstLiveSeq 只活在内存,落盘靠上面那条 end-seed

上一讲说过,投影是从日志算出来的各种视图;seedLength 就是给这些视图用的一条界线,比如“这一轮跑了多少步”这种统计,就不该把继承来的步骤算进去。

header 里还有个字段值得留意:agentPreset,记录这个会话的 agent 是按哪套预设组装的。它必须持久化,因为预设决定了这个会话有哪些工具、提示词长什么样。要是恢复时装上

最后交代一下:上面走的是底层原语。我们拿 load 的结果当 seed、再 create 一个新会话,这两步很直白,适合看清发生了什么。官方给 resume 准备的正规入口,就是接口里那个

“起”——prepare:

复制代码 1const preparation = await ctx.sessionPersistence.prepare(toolId) *// 同一个 id,场景

2preparation[Symbol.dispose]()

它直接给你一个 Session 对象,而不是一串事件。有意思的是这个对象的事件数比 load 多一条:因为它把存储日志当作 seed 来构造会话,于是照例多了一条 session/end-seed

(前面 fork 那节见过它)。ctx.agents.resume({ resumeSessionId }) 内部走的就是这一步。

到这里,五个问题都有了着落:

还在吗?在。日志写在磁盘上,append 返回成功就代表真的写完了。

元数据放哪儿? session.header。跟事件流分开存,永远不进 deriveMessages()。

能岔出去吗?能。fork 从任意稳定的轮次间位置分叉,父子此后互不相干。

死过一次还算数吗?算。后端把没闭合的轮次配平,不截断、不丢弃做过的工作。

能接着干吗?能。拿修好的日志当 seed 造新会话,消息历史 = 继承 + 新增。

本讲小结

上一讲我们看清了日志的“形”——只追加、改不动、各种视图都从它算出来;这一讲看清了它怎么活下去:

写盘是批处理的,session/flush 是领下一轮之前的检查点;写失败绝不记成事件。

Fork 从历史的稳定点分叉:种子事件深克隆,session/end-seed 标出继承边界, seedLength 和 parentSession 记进 header;落在开放轮次内的 boundary 一律拒

绝。

崩溃修复只配平、不截断:补 step/end、turn/end { reason: 'interrupted' };没结果的工具调用还会补一条 tool/result(TOOL_OUTCOME_UNKNOWN /

TOOL_NOT_STARTED),把"能不能重试"交代给模型。而且只对冷会话生效。

Resume 是读回来当 seed:消息历史 = 继承 + 新增,seedLength 划清界线。

整套机制里最值得带走的一条是它的取向:遇到不完整的状态,先想能不能配平,实在不行就拒绝——绝不静默截断,也绝不假装没发生过。上一讲的 fail closed 在这里换了三种样子:

拒绝非法的 fork 边界、拒绝读不认识的格式、拒绝修复还活着的会话。

注:本讲 GitHub 链接

思考题

动手实操:在 fork-demo.ts 里,往 ② 分出来的子会话再追加一轮(照开头那两轮的写法,turn 编号用 3),然后分别打印父子会话的事件数与消息数。想一下:为什么子会话变

了,父会话一条都没多?提示:seed 是深克隆的。

小改动:把 crash-resume-demo.ts 场景一里那行 await ctx.serial('session/flush', session) 注释掉,重新跑。观察 ② 还能 load 出几

条——连会话本身还在不在列表里都想一想。那 3 条事件去哪儿了?

提示:写盘是批处理的,窗口默认 200ms;而 create 是延迟实体化的。

有余力可以再走一步:把场景一那处 load 换成 inspect,输出不会有任何变化,两者返回

全站分卷索引 · 6 卷
  1. 1第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷
  5. 5第 5 卷
  6. 6第 6 卷