第 3 卷
复制代码 1--- ② 塞一个无法无损序列化的值 ---
2被拒:session event "turn/start" carries non-JSON-serializable data 3
4--- ③ 试着改一下已经写下的历史 --- 5Object.isFrozen(events[0])= true
6Object.isFrozen(events[0].data) = true 7events[0].data.turn = 99→TypeError: Cannot assign to read only property 'tu
8events[0].data.turn 仍然是 1
三条硬约束,前两条恰好被这两段输出演出来了:
- 追加时校验,坏数据在入口就被拒。
事件数据必须是无损 JSON:BigInt、函数、symbol、undefined、 NaN/Infinity、-0、循环引用、稀疏数组、Map/Set/Date / 类实例……统统不行。为什
么偏偏卡在 append 这一行?三个理由,一个比一个硬:
append 承诺的是已经落盘。官方对后端的要求是 append resolves only after durability——它返回成功,就意味着东⻄已经写下去了。若放一个存不下的值进日志,
就会出现一条内存里算数、磁盘上没有的事件,这个承诺当场就破了。
内存里的日志,恒等于磁盘上能存下的东⻄。官方的说法是:错误事件绝不会进入日志,因此 session.events 始终与后端可持久化的内容一致。于是你眼前那串事件,
就是能原样存下来的那一串,不存在看着在、其实存不下这种事。
还原不靠猜。有人会说:Date 变成字符串再变回 Date,也不是做不到。确实做得到,但那要求读的人知道这个字段原本是 Date。事件类型由插件扩展、还会跨版本演进,
这份哪个字段原本是什么类型的表既没处放、又会漂移。dsh 的选择是不猜:宁可在入口拒掉,也不在重建时猜(这个取向本节还会再遇到一次)。
所以宁可在 append 这一行就炸掉,绝不让存不下的值溜进日志。
事件和它嵌套的数据在通过校验时就被深冻结了,输出里连查两层 isFrozen,连 data 里嵌的对象也没放过。所以那句赋值直接抛 TypeError:turn 还是 1。历史一旦写下就改不
动,这正是唯一真源该有的样子。
- 读不懂的事件,默认不许跳过。
这条不在 data 里,而是写在信封上,所以两段输出里也看不见它。
先说清一个动作:日志要落盘,落盘之后还得读回来,把磁盘上那串事件一条条装回内存、重新冻结、重新派生出消息历史,这个动作叫重建(官方叫 load,崩溃之后就是靠它接着
跑)。麻烦出在重建的时候:插件可以往日志里加自己的事件类型,于是读的那一方,常常是个更旧的版本,可能撞上一个它根本没见过的类型。怎么办?
dsh 的答案是默认拒绝。事件可以带一个 ignorable 标记;不带这个标记的事件,读的那一方遇到不认识的类型时必须拒绝重建整个会话,宁可一个字都读不出来,也不许悄悄把那一条
丢掉,因为一条你读不懂的事件,很可能改变后面所有事件的解释方式:少一条,后面的账全跟着错。
这个标记长什么样?它的类型写死成 ignorable?: true,要么不写,要么写 true,没有 false 这一档。这不是偷懒:类型上就不给你写 false 的机会,免得冒出写了 false
却自以为是默认值的糊涂账;想说这条可以丢,只能明明白白写一个 true。官方还补了一道限制:只有纯信息性的记录才配标它,丢了也不影响重建的那种。
于是默认值就是不写 = 必需。为什么定在这一头?因为写事件的人迟早会忘,而官方把这笔账算得很清楚:
| 忘了标记时 | 后果 | 性质 |
|---|---|---|
| 默认必需(dsh选的这个) | 过度拒绝:日志读不出来 | 麻烦,但不危险 |
是不是很眼熟?守卫不许放行、审批无人应答降级为拒绝、沙箱不可用就抛异常,第 06、07 讲你已经见过这个选择的三种样子。它有个正式名字:失败关闭(fail closed),拿不准时,
宁可停下来,也不带着不确定性继续。这一节的第三条硬约束,读不懂就不重建,是它的第四种样子,形状一模一样:认不出这条事件,就停下来、拒绝重建,而不是带着一个被掏空的会
话继续。
另两条严格说不属于它,各有各的名字:
坏数据进不来是尽早失败(fail fast):在 append 这一行就炸,绝不让坏数据拖到落盘或重放时才暴露。它跟失败关闭不冲突,只是角度不同,失败关闭讲往哪边倒(安全侧),尽
早失败讲什么时候炸(越早越好)。
写下就锁死根本不是失败处理,而是让失败无从发生:深冻结之后,改这个动作在类型和运行时两处都不存在,也就无所谓失败不失败。
三条合起来是同一个取向:不猜、不拖、不给悄悄出错的机会。
第一个投影:消息历史
日志这头稳住了,从它算出来的东⻄才可信。上面记下了 8 条事件,可模型并不需要看到全部。为什么?
那 8 条里只有 3 条是对话内容——用户问了什么、模型答了什么、工具返回了什么。剩下 5 条都是记账:一轮从哪开始到哪结束(turn/start、turn/end)、一次模型调用何时起止
(step/start、step/end)、模型要求了什么调用(tool/call)。
它们不是没用,恰恰相反,少了这些边界信息,事后就没法把当时发生的事原样还原。只是还原给谁都行,唯独不必念给模型听。