11 · Durable 与 Chord:理解长期 Agent 的设计¶
11.1 先明确成熟度¶
Pi 的 1.0 发布不意味着 monorepo 每个包都已有稳定 API。pi-durable、pi-protocol 和相关 server 路径仍标实验,API 可变化。本章研究它们的机制与设计,不把实验运行时当作你必须采用的基础。
普通 Coding Agent SDK 的 JSONL 会话和 Durable 的事务运行时是两条不同路径。
11.2 为什么保存聊天记录还不够¶
考虑发邮件工具:
随便重跑会重复发送,不重跑会丢任务。持久化问题的核心是「操作意图、结果和外部世界」如何协调,而不是把每个 token 落盘。
11.3 Durable 的基本对象¶
| 对象 | 含义 |
|---|---|
| Harness | 会话、模型、工具、调度和宿主扩展 |
| Session | 存储上的持久记录与事务对象 |
| Conversation | 一条对话和关联任务 |
| Entry | 持久历史记录 |
| Document | 自己的结构化状态 |
| Task | 可检查阶段和 checkpoint 的持续工作 |
| Registry | 当前版本定义和恢复所需类型 |
| Storage | memory / JSONL / SQLite 等后端 |
一次任务不只是内存 Promise;它的 phase、input、checkpoint 和结果由持久存储表达。宿主恢复后可以知道工作进行到什么阶段。
11.4 工具执行前先提交意图¶
harness/tool.ts 的 ToolTask 初始 phase 为 call。它查找工具、prepare、validate、beforeTool,再二次校验修改后的参数,然后 commit:
意图保存成功之后,才运行真实工具。
11.5 replay safe 的准确含义¶
恢复时只有保存的 replay 和当前工具 replay 都为 safe 才重跑;否则记录 interrupted,并保留已提交部分输出。
这并没有魔法解决外部恰好一次。safe 应基于操作性质或幂等业务实现,比如「通过固定提交 ID 查询结果并确保不会重复创建」,不能仅因为工具看起来很常用就标 safe。
Core AgentTool 有自己 replay 类型;Durable ToolRegistration 的字段和策略是另一个契约。不要把不同 package 的类型仅凭字段名混用。
11.6 Storage 后端的保证¶
| 后端 | 1.0 文档所述行为 |
|---|---|
| MemoryStorage | 无磁盘持久化,适合测试 |
| Node SQLite | WAL,synchronous NORMAL;进程崩溃可恢复,断电可能丢最新提交 |
| Node JSONL | append-only;fsync true 时每个 commit marker 前刷新 |
选择后端要按真正故障模型:进程 kill、机器断电、磁盘满、并发 writer 是不同情况。只有展示「重启后能恢复一条消息」不能证明所有场景可靠。
所有权前提:同一 storage 在同一时刻由一个进程拥有,官方未提供跨进程锁。不能让多个 worker 同时 open 同一 Durable storage;SQLite 自身的写锁不等于 Harness 多进程所有权协议。宿主应按 storage 分配唯一 owner。
自定义 storage 应运行官方 storage conformance 套件,保持扫描、提交、游标、所有权和事务契约。
11.7 Child tasks 与子 Agent¶
Durable 可以持久化 child task graph,也有 subagent 示例。父调用的 task outcome 决定 owned work 的取消,普通 error result 并不等于 failed task:
| 父调用情况 | task 结尾 | owned child 行为 |
|---|---|---|
execute 正常返回,含 {isError:true} |
completed | 不因这个 error 标志自动取消 |
| execute / environment 构建 throw | failed | 记录取消意图,取消其 owned work |
| abort | aborted | 取消 owned work |
| intent 后崩溃,恢复时不满足双 safe | failed + interrupted result | 取消失去监督的 owned work |
background / ownerless 需要显式选择,不能据此推断所有子任务自动与父任务共享生命周期。
这是一种结构化所有权:父任务不应失踪后留下无人管理的 child。对自己的多 Agent 产品,要明确谁拥有子任务、谁付费、谁取消、谁收结果,以及是否共享工具权限。
课程复审使用了独立审查代理;这里讲的是 Pi Durable 的产品运行机制,不表示实验示例自动启动 Pi child tasks。
11.8 Chord 的设计问题¶
一个插件的后台处理在 worker,UI 在终端/浏览器,状态又需要跨进程。Chord 把插件拆成 facets,在适合的环境分别加载;services 是稳定类型 token,声明提供/依赖关系。
启动先声明完整 graph,再验证依赖,provider 在 consumer 前激活;清理按逆依赖顺序。比随便在 import 时建立所有连接,更容易重载和确定生命周期。
Chord 本身不依赖 Pi,其他应用也能用;Pi packages 的 skills/prompts/extensions 分发机制与 Chord facets 不是一个概念。
11.9 Replicated state 与 Delta¶
权威端通过 change(context, draft => ...) 发布原子 revision;draft 只能在回调生命周期使用。未变化子树可以共享,wire 用操作增量降低流量。
每个远程 client / state stream 有自己的 path codec;断线和 replacement 后必须重新 hydrate,不能把另一个客户端的 delta 当通用日志复放。
public state subscriptions 对慢消费者有最多 100 待发值策略,溢出合并成最新值;service update 管线也有限流,但用 explicit reset snapshot 重置序列和字典。两个层的队列策略不等价,不能期待慢 UI 永远看到每个中间值。
11.10 不可变是所有权契约¶
Chord 接受 alias-free strict JSON,并进行所有权转移;发布值不一定 Object.freeze。消费者擅自修改共享对象会破坏权威状态,尤其同进程 loopback。
正确做法是通过 change 发布修改,在可变信任边界 clone / serialize。不要看见某字段能赋值,就认为它属于你的对象。
11.11 热重载的基本策略¶
先加载候选 facet generation、校验并激活,再切换 stable service facade;失败保留旧 provider;成功后释放旧 generation。内容地址与 SHA-256 验证解决 bundle 完整性,不等同认证插件作者。
keyed instance 会有 incarnation-specific generation,替换后不应把旧实例句柄当新实例。设计自有 Agent 插件时,也要分开代码版本、持久任务版本和 UI attachment 版本。
11.12 何时采用¶
- 个人一次性助手:Core 或 SDK 即可。
- 需要已保存会话继续:SDK + SessionManager。
- 需要业务任务队列:先做宿主任务数据库和幂等工具。
- 要深入研究事务 Agent 和跨进程插件:试验 Durable / Chord,在产品中固定版本并完整测试迁移。
这不是越低层越高级。你的需求越简单,组合越少越容易验证。