Why: The review record still said the proposal was unmerged, but Documents PR #34 is on main and this branch is cut from that commit.
Evidence: origin/main is 76044b5, the merge of feature/agent-stack-blueprint.
Impact: One sentence in the review record. The proposal text is unchanged.
Refs: Dev feature/pi-durable-borrow
Co-authored-by: Cursor <cursoragent@cursor.com>
11 KiB
调研:pi packages/durable(Pico5)有什么可以借鉴
- 状态:记录(2026-10-03);owner:weichen;任务:Dev 仓
feature/pi-durable-borrow - 对象:本机参考克隆
/home/weichen/projects/Agent-System/.refs/pi,origin为https://github.com/earendil-works/pi.git,HEADc7cdb460aa8a0cebef3446c4166729b8a0d97ead(2026-09-21,fix(coding-agent): reject bug reports offline)。包版本@earendil-works/pi-durable0.86.1,MIT(仓库根 LICENSE 版权 2025 Mario Zechner;packages/durable/package.json的license为 MIT)。 - 对照的本仓事实以 Dev
a5c9fd3385为准。库层对照的是提案 2026-10-02-agent-stack-completion.md(Documents PR #34,本记录合入时已经在 documents main)及其审计记录。
结论先说:不把这个包或 Chord 当成依赖引进来。 它现在只有记录契约和一份内存存储。值得拿走的是几条恢复规则,而且用我们已有的表和事件词汇就能落地。本任务在 Backend 修四个已经核实的缺陷;库层规则写在这里。提案已经合入,在提案里加链接留到后续小任务,本记录不改提案正文。
1. 这个包实际有什么
packages/durable 的 README 写明公开 API 是持久记录契约和一份脱离调用方的内存存储。交接文档 docs/pico-v5-handoff.md 的状态栏写着:旧的 pico、pico4 原型已删,pico3 还在,No Pico5 implementation exists。24 个实现包里,代码只覆盖第 1 包:
| 文件 | 内容 |
|---|---|
src/types.ts |
ConversationRecord、EntryRecord(model / data / head / edits)、Input 四态、TaskRecord / TaskState / TaskOutcome、DocumentRecord、Storage |
src/memory-storage.ts |
MemoryStorage:原子提交、只读副本、分叉历史扫描、任务整行替换 |
test/memory-storage.test.ts |
12 个存储行为测试 |
docs/pico-v5.md |
规范。Session、文档、任务、收件箱、存储后端都在这里,没有对应实现 |
同仓里真正跑起来的同类代码在别的包,不在 durable:
packages/agent/src/harness/pico3/:实验内核,约 8000 行,导出路径是@earendil-works/pi-agent-core/experimental/pico3。交接文档要求只把它当参考,不要搬它的外观接口。packages/agent/src/harness/session/types.ts:工具调用状态含effect_pending,并带replay: "never" | "safe"。这是 Pico5「先记意图再执行」在现行 harness 里的落地,比规范更值得对照。packages/coding-agent的会话仍是 JSONL 树,不依赖@earendil-works/pi-durable。- 依赖的
@earendil-works/chord0.86.1 也是 MIT,但packages/chord/PLANNING.md写明 not a stable public API contract yet。
因此评级是 Reject:不引入 @earendil-works/pi-durable,也不引入 Chord。 语言栈是 TypeScript,我们的执行与存储在 Go;包本身没有 Session、调度器或 SQLite 后端可以链接。
2. 规范里值得单独拿走的规则
Pico5 的核心句(docs/pico-v5.md 开头):一次 Session 提交原子地写下不可变条目、完整任务记录和文档;只有提交成功的状态可被观察到。下面每条都标了我们这边的落点。
2.1 采纳,本任务在 Backend 实施
这四条不改变骨架设计 §4.2「不做透明恢复」。它们补的是那条决策已经承诺、但硬杀路径没有做到的事。
| 规则 | Pico5 出处 | 我们的缺口(修复前) | 落点 |
|---|---|---|---|
| 重开时把活着的任务收成终态 | 规范 §5.1:打开时把残留的 running 改回可调度;未知任务种类收成 orphaned |
InterruptRunning 只在收到 SIGINT/SIGTERM 时遍历内存里的 run(app.Run 停机路径)。进程被硬杀后,库里的 pending / running / waiting_hitl 没有启动扫描。前端 activeRun 把一切非终态都当成进行中,outbox 因此不再发送 |
启动对账。SQLite 扫描全部;PostgreSQL 只扫描本实例(外加升级前 instance_id 为空的行)。收成 interrupted,未决 HITL 以 cancelled 关闭,发出已有的 run.failed(码 run.interrupted)。不新增 Core 状态 |
| 输入先准入,再落库;用请求键去重 | 规范 §6:requestId 在写入前按会话去重;忙时 reject 则什么都不写 |
postMessage 先 AppendMessage 再 StartConversationRun。槽位被占时返回 409,用户消息已经在库里。前端把同一条放回 outbox 队首,回合结束后再发一次,于是出现两条用户消息,第一条没有 run |
回合和工作流先抢槽位,忙则 409 且不写。messages.request_id 部分唯一索引。重复请求返回已有消息和 run。命令路由仍不占槽位 |
| 终态一次提交 | 规范 §1 不变量 1、§5.3:终态任务、结果条目、输入结算在同一次提交 | finishRun 先 UpdateRun,再 AppendMessage;消息失败被跳过。可能留下 completed 且没有助手消息 |
SettleRun 在一个事务里更新 run 并追加助手消息。失败则把 run 收成 failed(码 run.settle_failed)并发 run.failed,不再静默 |
| 只发布已提交的状态;慢消费者靠持久游标收敛 | 规范 §1 不变量 2–3;§9.2 积压过多时用一份完整快照替换未送达的增量 | 事件是先落盘再 Publish,这一侧已经符合。缺口在总线:订阅缓冲满时直接丢掉,连接不断。浏览器不会带 Last-Event-ID 重连,注释里的「重放补齐」不会发生。另外 EventsAfter 只取一页 1000 条,更早的积压被跳过 |
缓冲满则关闭该订阅。SSE 看到通道关闭就结束响应,由 EventSource 重连。重放按页读到不足一页为止 |
多实例约束来自 双档存储提案 的 D24:run 在所属实例的内存里执行,跨实例不迁移。所以 PostgreSQL 上的对账不能扫描别的实例正在跑的 run。实例标识来自启动配置 instance(环境变量 AGENT_SYSTEM_INSTANCE),缺省是「主机名:监听地址」。
2.2 试点,写给库层,本任务不改代码
这些规则对得上 agent-stack 提案的 N3(可恢复 RunState)、Q1(图执行检查点)和审计 Q4(恢复会重跑 running 步骤)。提案合入后再开任务,在提案 §2.1 / §2.2 加链接。不要把 Pico5 的状态词原样加进我们的结果枚举:提案写了跨库结果状态对齐 A2A,不自造第四套词。下面的 faulted 用原因码表达。
| 规则 | Pico5 出处 | 对应我们的缺口 | 试点时怎么用 |
|---|---|---|---|
| 重放取交集:存储时的策略和当前声明都是 safe 才重跑;否则写一条 interrupted 结果。当前的 safe 不能把当初的 unsafe 升级成可重跑 | 规范 §7.2;现行代码在 harness/session/types.ts 的 effect_pending |
审计 Q4:Resume 会把 running 步骤再执行一次。contract 有 SideEffects,但没有工具级的 replay 位,而且 contract 还没接到 runtime |
Native 的工具声明加 replay 位,缺省 unsafe。Squad 的 attempt id 恢复时读这个位:unsafe 写中断结果,不调用执行器 |
| 意图和结果分两次提交。一轮没有持久进展就失败 | 规范 §5.1 第 6 步、§5.2 effect sandwich | 审计 N1:预算用尽时 assistant 已经落盘,工具结果没有。重试会再打出副作用 | 工具调用先落「即将执行」再执行,执行后落结果。恢复时按上一行的交集决定重跑还是写中断结果。无进展用现有失败状态加原因码,不新增 faulted 枚举 |
| 先写者胜出的 memo,和检查点分开,终态时丢掉 | 规范 §5.2 | 审计 Q1/Q2:批准和预算不是原子落盘,恢复后丢失或再次挂起 | 放在步骤记录上,候选插入和读出赢家同一次写入。批准、修复预算用它,不要再靠散落字段 |
| 内存实现是语义参照,SQLite / JSONL 跑同一套一致性测试 | 规范 §10 末、§11.1;MemoryStorage 的测试就是这套测试的雏形 |
Squad 的文件 run 和 platform SQLite 各记一份步骤状态(提案 Q1 验收已经要求合成一份) | 先有内存参照和共用测试,再写文件与 SQLite 后端。不要每个后端自己发明恢复语义 |
| 多文件写入用「先写侧车、再写主标记、标记成功才对外可见」 | 规范 §11.3 | 审计 Q3:writeJSON 直接 os.WriteFile,崩溃留下半截 JSON |
文件档的检查点用这个协议。没有 fsync 时只承诺扛住进程崩溃,不承诺扛住断电;这一点写进文档,不要暗示成完整耐久 |
压缩不改历史:追加一条带 head 的摘要,旧条目还在 |
规范 §2.1、§8 | 骨架设计:窗口外原文不删,滚动摘要是远期。Native 的 compaction 会按条数裁尾,审计 N7 指出可能切断工具配对 | 压缩产物是新条目加下界指针,回放从指针开始。不要重写 transcript 文件 |
2.3 暂缓,以及什么情况下再看
| 规则 | 为什么现在不拿 | 重新评估的条件 |
|---|---|---|
| 透明续跑(从检查点接着做,而不是标记中断后让用户重发) | 骨架 §4.2 明确不做,避免分布式续跑。N3 还没落地 | N3 的 RunState 可恢复已经合入,并且有「杀进程后工具配对仍成立」的测试 |
| 服务端 steer / follow-up 队列 | 直连路径是 409 + 浏览器 outbox。agent-native 的 AgentSession 已有队列,但没有接到 HTTP |
harness 提案 P1:直连和小队共用同一个回合循环 |
| 崩溃后把未写完的助手输出收成一条 interrupted 消息 | 增量在 150ms 缓冲里,终态消息只在 run 结束时写。恢复它要定义「事件残片算不算消息」 | 终态原子提交稳定之后,如果用户确实需要看到半截回复 |
把系统提示增量写进对话记录(SystemMessage.sections / toolsAdded) |
我们的系统段由 ctxasm 每次组装,不进消息表。pi-ai 已有这个类型,但是另一套回放 | 只有当我们要把「当时实际给模型的系统和工具」做成可回放事实时 |
| 带作用域、历史和分叉策略的类型化文档,以及 Chord 增量订阅 | 我们已有持久 seq + Query 快照 + SSE 重放,订阅模型够用。Chord API 不稳定 | 出现真正的多文档、可分叉、要按历史时点读取的产品需求 |
| 会话分叉(fork)和所有权树(owner 管中止,parent 管历史) | 领域模型没有会话分叉。子 run 用 parent_run_id,取消靠 context 级联 |
产品要做「从某一条消息开分支」或跨进程的子树取消 |
3. 本任务实施了什么
对应 §2.1,不改 Core 契约:
- Backend 迁移 v14:
runs.instance_id,messages.request_id及(conversation_id, request_id)部分唯一索引。 - 启动对账
ReconcileInterrupted,与停机时的InterruptRunning共用收尾。 - 发消息先查 requestId、回合与工作流先抢槽位再落库。
SettleRun单事务写 run 终态和助手消息。- 事件总线在订阅缓冲满时关闭连接;SSE 按页重放直到不足一页。
- Frontend 数据层在发送和 outbox 重发时携带同一个 requestId。界面不变。
不在本任务:透明续跑、服务端 steer、把半截输出收成消息、修改 Documents PR #34。