拆解 pi(DeepSeek 开源终端 Coding Agent)v0.83.0 的 setter 与 pending writes:调用 setModel() 返回后,「新模型生效」有四个不同的观察面,其中两个还是旧的。源码依据:pi-internals 21 章。

setter 返回了,然后呢
Agent 运行中,你调用了 await harness.setModel(second),它返回了。直觉上「模型已经是 second 了」。
但如果你同时问四个地方,会得到四个答案:
harness.getModel():second------setter 已更新 Harness 的最新配置- 已经发出的 provider request:旧模型------请求用的是 active turn state,不重读字段
- 下一次
createTurnState():second------下一份快照会读到新值 session.buildContext()(保存点 flush 前):还是旧的 branch 配置------busy 时 setter 只把变更放进 pending queue
一次修改,四个观察面,其中两个还停在旧值。这不是 bug,是设计:当前请求必须用稳定的输入(turn snapshot),修改只对下一轮生效。
setter 的两个时钟
以 setModel() 为例。它先保存 previousModel。phase 为 idle 时,等 appendModelChange() 成功;phase 非 idle 时,只向 pending queue 放入一条 model_change。接着无论哪条路径,都把 this.model 指向新模型,并发出 model_update 事件。
setThinkingLevel() 和 setActiveTools() 结构相同。setTools() 还会先校验工具名唯一、active name 都存在,再决定追加还是排队。setResources() 与 setStreamOptions() 不写 Session entry------它们只改变当前实例,进程重建时需要宿主重新提供。
pending write 为什么不能立即插队
这是这一章最值得细看的地方。assistant message 可能带多个 tool call。低层 loop 要先把 assistant message 和对应的 tool results 形成相邻、完整的对话片段,再进入下一轮。
如果一个 subscriber 在 message_end(assistant) 里追加 custom message,并且立刻写入 Session------这条消息就会落在 assistant 与 tool result 之间。下一次从树构建上下文时,模型 API 所需的工具配对可能被切断:模型看到 tool call,却看不到对应的 result。
所以 Harness 让运行中的 appendMessage() 只排队,idle 时才直接追加。flushPendingSessionWrites() 始终读数组首项,按 entry type 调 Session 对应方法,成功后才 shift()。某项失败不会越过它写后面的内容------简单的 FIFO,没有重试计数、没有 durable queue id、没有跨进程恢复。
Hook 是顺序的,不是并发的
subscribe() 注册全事件观察者,存放在专门的 handler set 里。emit 时按注册顺序等待每个 listener;任意一个抛错,错误被规范成 Hook error,后续 listener 不再执行。它不是 fire-and-forget 的埋点。
on(type, handler) 注册带类型的 Hook,同样顺序等待,保存最后一个非 undefined 结果。所以 context、tool_call、tool_result 这些 Hook 的默认 reducer 是 last-result-wins,不是自动合并所有对象。provider options 和 payload 是例外:链式处理,前一个 handler 改出的结果交给下一个。
失败不会回滚
message_end 到来时,Harness 先 session.appendMessage(event.message),成功后才通知 subscriber。subscriber 抛错,已经追加的消息不会从树上删除。setter 也是先提交自己负责的状态,再发 update 事件;update listener 失败会让 public Promise reject,却没有补偿性回滚。
拆完这章,我的感受是:Agent 框架的「修改」语义,比普通系统复杂得多------因为同一份状态同时活在内存配置、turn 快照、pending queue 和 Session storage 四个地方。pi 的处理是给每个观察面明确的语义:修改立即可见于内存,只对下一轮生效于请求,只在保存点落盘于存储。做 Agent 平台的人,最怕的就是把四个观察面混成一个「当前状态」------那才是真正找不到 bug 的根源。

想看完整拆解(setter 四观察面状态表与 Hook 失败语义源码),这里有 pi-internals 小册子。
源码依据:packages/agent/src/harness/agent-harness.ts L225-L317、L442-L484、L512-L536、L538-L554、L884-L955、L961-L1023,pi v0.83.0,commit 845d6ff1。