TL;DR:Earendil 在 2026 年 10 月 1 日随 Pi 1.0 发布实验包 Pi Durable,面向会长跑、会崩溃、能从多个界面接入的 Agent 应用,不替换终端里的 Pi coding agent。模型请求和工具调用都是带检查点的任务:先记意图,再执行副作用,最后写入结果。进程死后打开同一份存储就能接着跑;工具默认不重放,只有 replay: "safe" 才会重跑。Mario 和 Armin 在 10 月 7 日的播客里把这套结构称为 effect sandwich。以下依据官方公告、README 与源码约定整理,不是独立实测。
它不是下一个 coding agent
Pi 1.0 仍然是给一个人、在一台(远程)机器的终端里使用的 coding agent。进程挂了,人看一眼发生了什么,再让它继续。Earendil 在同一天的官方公告 [1] 里写得很明确:这一点不会改变。
但他们还需要另一种运行时形态。Agent 要从 Slack、邮件、手机和网页都能接入;对话要能无限长;笔记本休眠、容器被拉起、内存耗尽,都不能丢掉进行中的工作;几个人还要能同时盯同一条会话、中途插话。把这些需求塞进 Pi coding agent,只会把那条刻意收窄的产品线撑变形。于是他们另开了一个实验包:@earendil-works/pi-durable。
公告里的定义很短:harness 是存储,加上并行跑一条或多条与大模型对话所需的机制;它提供模型调用的工具,以及工具真正执行时所在的环境。一次对话是你和 agent 的交互,记成 transcript。agent 是模型加上设置和可用工具。工具通过 execution environment 做事,可以是笔记本、远端虚拟机或内存沙箱。harness 跑的每一件事——调模型、跑工具——都是一个 task。

10 月 7 日,Mario Zechner 和 Armin Ronacher 上了 0xSero 的节目 Pi Durable: Agents That Survive Crashes [2]。Mario 把“耐久性”归结为几句话:agent 在一个进程里启动,去调工具,进程死了;你要的是重启进程之后,它从停下的地方接着跑,人不用再打一遍“继续”,工具结果和助手输出也不该丢。播客简介写明他们不用 event sourcing,而是采用一层叫作 effect sandwich 的结构:先记录意图,再跑副作用,再持久化结果。
这也解释了包的位置。Pi Durable 和 Pi coding agent 共用 pi-ai,同样秉持极简、可改的原则,但互不替换。公告说,只有在 Durable 上做出的 agent 应用里被证明有用的东西,才会回流到 coding agent。Earendil 想要的是“把这套技术带给每个人、用最合适的形态”;Durable 是这条线的运行时,不是又一个终端里的编码助手。
包本身刻意保持短小。公告称不含测试大约 15,000 行,按 GPT 计约 15 万 token,按 Claude 计约 25 万;存储后端单独就有 3,000 行,多数时候 agent 读代码可以直接跳过。截至 2026 年 10 月 12 日,npm 上的版本是 1.2.0,README 第一句仍然是 Experimental,API 会在版本之间无通知地变化。
一次任务怎么穿过系统
官方演示样本不是又一个 coding agent,而是大约 1,300 行 TypeScript 的度假规划 TUI,其中大部分是界面,交互组件借自 Pi coding agent。公告按八步写下它的设计路径,不是一份运行日志:主会话接到规划需求;子会话并行发起天气、博物馆、火车三次搜索,每次都是独立的 durable task;主会话同时还能聊天;进程在火车搜索未完成时死掉;重启后,只有标记为可重放的 search 会再跑未完成的那一次;人可以切到子会话去 steer;回到主会话可以边跑边问、压缩、插话;子会话的报告作为消息回来,主会话把它写成计划。

应用先打开一份 storage,再调用 Harness.open()。内置后端包括内存、SQLite 和 JSONL,并带 conformance suite。SQLite 与 JSONL 的实现不依赖 Node API,加一层小适配就能跑在 Bun 或 Cloudflare Durable Object 上。同一时刻只有一个进程拥有这份存储,其他客户端挂到这个进程。SQLite 上内存里只留工作集:活动中的 transcript、活着的任务、尚未安置的 submission;其余都在磁盘上,用到再读。活动 transcript 会被模型的上下文窗口自然卡住,因为 compaction 会在溢出前把旧消息收成摘要。
工具不直接碰文件系统。env 按会话的工作目录给每一次工具调用、prompt 渲染构建执行环境;内置的是 NodeExecutionEnv。harness 可以在一台机器上跑,工具在另一台。官方内置工具是 read / write / edit / bash,装在 CodingTools 扩展里。
一次 submit({ type: "input", content, requestId }) 会先被持久化接纳。内置的 generation 任务去调模型,把回答追加进 transcript。模型若要跑工具,每个调用又是自己的 tool 任务,归这一轮 generation 所有。会话空闲,只发生在当前工作下的前台任务都结束之后。Esc 之类的 abort 会自下而上中止所有权树上的东西,每个任务先清理自己的副作用。
播客里 Mario 把“任务能再派一个任务并等待它结束”称为全部魔法。官方代码把这条写成所有权树,而不是内置 subagent 产品。triage 示例只有几行:工具在同一次 commit 里查找或创建自己拥有的会话,配更小的模型和更少的工具,用稳定的 requestId 提交,然后等待。子会话和别的会话一样能挨过崩溃;UI 可以通过这次调用的 details 把它画在工具下面。需要活过当前回合的后台工作,则标 { background: true }:普通 abort 碰不到它,会话也可以在它还跑着的时候进入空闲。
扩展是 named bundle:system prompt 的 section、工具、hook、以及自定义 task。会话只存名字,不存代码。prompt 每次请求前从当前选中的 section 重建,变更作为位置记录写进 transcript;支持中途改 system prompt 的模型只再发变更,prompt cache 还能用。hook 可以改请求、拦住工具、换掉结果,或决定这一轮还要不要继续。hook 自己也会在崩溃后重跑,所以做决定的 hook 必须把答案写进 memo:跟任务存在一起的小值,第一次写入生效。
崩溃之后走哪条路
Durable 的承诺不是“副作用在世界上只发生一次”。它承诺的是:进程死了以后,新进程打开同一份存储,能找到没做完的任务,从最后一个检查点继续。

工具调用在 execute() 之前先把意图提交。恢复时,只有存储里记下的 replay 和当前工具声明都是 "safe",才会用已保存的参数再跑一次;否则模型收到 interrupted,带着已经提交的部分输出,由它决定下一步。generation 被切断时,部分答案留在 transcript 里标成 aborted,请求会再发一遍。Provider 的流本身不可续传,这是更早的 durable-harness 设计笔记 [6] 里就已经写明的边界。
度假规划 demo 走的是左路:search 声明可重放,天气和博物馆已经跑完,只有火车再跑。子会话工具若也标 safe,重跑时按 owner task 找到原来的子会话和同一条 submission,接着等,而不是再开一份。嵌套的 executeTool() 用 key 记住自己发起过的调用;replay-safe 的调用方按同样顺序或同样的 key 把它们接回来。
右路是默认。types.ts 写明 replay 缺省为 unsafe。未声明可重放的工具,崩溃后不会再进 execute();它拥有的、还没结束的嵌套调用和子任务带 abandonOnRestart,恢复时先 abort。公告里的 deploy 示例故意不写 replay:一次部署若在崩溃时已经发出去,重放会变成第二次。
有一处容易看走眼。v1.0.0 的 CodingTools——read / write / edit / bash——源码和 npm dist 里都没有 replay 字段。issue #10320 [7] 对照过这些文件,结论是崩溃恢复会把 read 也当成打断,而不是安全重放。该 issue 被 Pi 的新贡献者自动关闭策略关掉,维护者未重开。对照 2026 年 10 月 main 上的 read / write / bash,以及 issue 对 1.0.0 四个工具的核对,它们仍然没有声明 replay。公告把只读搜索标成 safe,并不等于内置 coding 工具已经按只读处理。
外部副作用的“恰好一次”不在 harness 里完成。播客简介把 idempotency key 写进这套方法:先拿到一个 ID,存下来,再带着这个 ID 去调外部系统;对方若说已经在跑,就等,而不是再触发一次。官方 checkout 示例用 payment-${task.id} 做银行卡扣款键,失败的支付走 abort 退款。这是源码规定的流程,不是一次真实结账记录。
客户端重试用同一条 requestId。崩溃后再次 submit 同一份 job,拿到的是原来那次 submission,不会把问题问两遍。流式的部分答案和工具输出默认最多每 100 ms 提交一次,一次崩溃最多丢掉这个窗口。
Codemode 走同一套嵌套调用,但多了一本 journal。脚本在 QuickJS 里跑,沙箱是确定性的(虚拟时钟、固定随机种子、UTC);脚本每拿到一个结果,先写入 journal 再交给脚本。重启后脚本从头再跑,按顺序把已经 journal 过的结果喂回去,嵌套调用按 key 接上,然后从崩溃点继续。脚本若走出和上次不同的路径,或沙箱指纹、codemode 版本变了,这次调用以错误结束。
会话、文档、多人同时改
一条 harness 同时跑多条会话。新会话可以从头建,也可以在 transcript 的任意一条 entry 上 fork:子会话能看到分叉点之前的父历史,但不复制那份历史。公告用 Slack 频道作例子:频道是一条会话,帖子下的 thread 是对某条回答的 fork,两条同时跑、互不堵塞。每条会话存自己的 agent:模型、thinking level、选中的扩展和工具、附加指令、执行环境里的工作目录。旁边的 reviewer 可以用更便宜的模型、只读工具、另一份 checkout。
应用状态——todo、计划、工单、沙箱句柄——放在 document 里:带类型的 JSON,和 transcript 做同一次原子提交,所以状态不会和产生它的对话对不上。fork 时每份文档自己规定起点:分叉点当时的值、现在的值,或一份全新的。播客把这层说成带时间旅行的文档存储。画布一类的对象可以跟着 transcript 往回看;回到对话中途,拿到的是那个时刻的笔画,而不是最新画布。序列化没用 JSON Patch。Armin 在播客里提到,可变数据上的结构化更新在 Web 里始终没有被广泛采用的标准答案;Pi Durable 用 JavaScript Proxy 把对普通对象的改动收成可重放的操作。Mario 自己把这层叫做慢的 hack,但对他们的用量够用。文档实现落在 @earendil-works/chord 上。
UI 需要的东西都是已提交状态。任意数量的客户端可以挂上任意会话:先拿到当前视图——transcript、正在流的回答、跑着的工具和输出、排队的消息、agent、用量——之后只收变更。迟到或重连的客户端从当前视图开始,不重放历史。whenBusy: "steer" 把插话放到当前工具轮次之后,加入正在进行的工作;默认 follow-up 则等这轮回答完再开下一轮。远程客户端可以用 watch() 收每次提交的操作;watchEvents() 把提交翻译成 coding agent 那套事件,字节会更多。
长对话靠 compaction 撑住,而且 compaction 本身也是任务。上下文接近窗口时,后台摘要先做,摘要放到下一个 turn 边界;只有下一次请求否则塞不进去,会话才等待摘要。Provider 若仍因太长拒请求,harness 再压缩并重试一次。reset() 和工具返回的 control: { handoff } 会新开一段上下文,旧消息不删,所以另一个工具仍能搜到手递之前的内容。
运行中的代码也可以换。registry 在会话还在跑时安装同名扩展,就是一次替换。已经开始的工具调用用它启动时的代码跑完;下一轮请求或调用用新代码。重启之后会话按名字去拿新进程装上的实现。这是 malleable 在 Durable 里的具体意思:存名字,不存代码。
实验包停在哪里
Earendil 自己把 Pi Durable 标成实验。公告结尾预告后面几周会谈他们用它做的小工具,比如 Slack bot 和 GitHub triage bot,但没有把那些应用一起发布。Armin 在 Pi 1.0 的配套公告 [8] 里写,Durable 是为了把 Pi 的极简带到 coding agent 和终端之外,而不是把 Pi 1.0 改成另一种东西。
它也不是 Temporal 那种通用持久工作流产品。Lee Robinson 在 Stanford 讲座里讲常驻 Agent 时,用 Temporal 的 event history 做对照;Pi Durable 明确不用 event sourcing,把检查点、所有权树和会话模型绑在 agent 循环上。工具对外部世界做过什么,崩溃也无法撤销。幂等是和外部系统的契约,不是 harness 送出的保证。
名字接近的社区包不要和它混在一起。pi-durable-subagents [9] 是给 Pi coding agent 用的第三方 subagent 编排,有自己的 workflow 和 chaos 测试,作者仓库在 purboo,不是 @earendil-works/pi-durable。pi-agents [10] 倒是直接坐在 Durable 上:每个 Pi 会话里开一份独立的 pi-durable harness,把委托出去的 named agent 做成崩溃后可恢复的会话。截至 2026 年 10 月 13 日它发到 0.31.1,作者是 mavam,文档写明这些 agent 用不了 MCP 和其他扩展的工具。这是下游用法,不是 Earendil 的官方产品线。
语言选择写在公告 FAQ 里:TypeScript 是目前最容易 bootstrap 的方式,不排除以后移植,眼下就停在 TypeScript。能跑的地方被说成“有 JavaScript 运行时的地方”;播客简介补了 Node、Bun、Cloudflare Durable Objects、E2B 和 Mario 自己的手机。这些是目标运行时,不是本文逐一部署过的环境。
0xSero 那期后半段,两人把 Earendil 拆成产品和基础设施两条线,并说不想明年变成一家 coding agent 公司。Durable 被放在第二条线上:你可以自己做一套,但他们反复重做过这层,真正费时间的是设计而不是行数。这句话是访谈立场,不是路线图。
如果你也在折腾 Agent 崩溃恢复与工具幂等,欢迎来 云栈社区 聊聊。
References