仔细讲讲最新的pi durable

回复 joye: 先说定位。Pi Durable 是 Earendil 10 月 1 日和 Pi 1.0 一起发的实验性新包 @earendil-works/pi-durable,MIT。它不是 Pi coding agent 的替代品,而是「用来构建任意 agentic 应用(包括 coding agent)的框架」。开篇那句话说得挺直白:Pi coding agent 是为一个人、在(远端)机器的终端里跑而设计的,进程死了你自己看看发生了啥、叫它继续;而他们要的是到处能跑、能从不同 surface 触达、对话无限长、扛得住内部和外部的灾难性失败、还能让多个人操舵同一个 agent 的 harness。共同代码(pi-ai 等)和理念(极简 + 可塑)都共享,Durable 上验证有效的设计会回流到 Pi coding agent。 它给的 harness 定义可以直接背:harness = 存储 + 并行跑一个或多个 LLM 对话所需的机械装置,同时提供工具和工具的执行环境。conversation 是你和 agent 的交互,记录成 transcript;agent = 模型加它的设置(比如 thinking level)和可调用的工具;工具通过 execution environment 干活,那可以是你的笔记本、远端 VM 或内存沙箱;而 harness 跑的每一样东西,从调模型到执行工具,都叫 task。整个源码不含测试约 15,000 行,GPT 约 150k tokens、Claude 约 250k tokens 是上限,实际用不到(光 storage backend 就 3,000 行,通常可跳过)——刻意做成「你的 agent 自己能读懂」。 第一块是长跑和随处跑。Durable 的 harness 是「开在一个 storage backend 上」的,自带 memory、SQLite、JSONL 三种存储,外加一套 conformance suite 和 benchmark 让你自己实现后端;SQLite 和 JSONL 的代码不用 Node API,配个小 adapter 就能跑在 Bun 或 Cloudflare Durable Object 里,接口很小,KV 或 Postgres 也能顶上。同一时刻只有一个进程拥有某个 storage,其他 client attach 到那个进程。在 SQLite 上只有 working set 进内存:活跃 transcript、live task、pending submission,其余留在磁盘按需读;又因为 compaction 会在溢出前把旧消息摘要掉,活跃 transcript 天然被 context window 限住,所以几万条消息的对话也能舒服放进内存。 第二块是崩溃恢复,这是它最核心的价值。每一步 task 前进之前都存 checkpoint。进程死了,新进程用同一个 storage 打开、找到没跑完的 task、各自从最后的 checkpoint 续上。被切断的模型请求会重发,已经生成的部分留在 transcript 里、标记为 aborted;被切断的工具调用只在声明 replay: safe 时才重跑,否则模型会被告知「这次调用被中断了」,并带上已经拿到的输出,由它决定下一步。requestId 让提交 exactly-once——崩溃后 client 重试会拿回原来那次提交,而不是把事情做两遍;排队中的消息也还在队列里。它没有内置 subagent,但几行代码就能搭:subagent 本身就是一条独立 conversation,所以照样续跑,一个 replay safe 的 subagent tool 崩溃后会找到同一个 subagent、接着等它的回答。 第三块是并发与分叉。一个 harness 同时跑任意多条 conversation,保证一致。conversation 可以新建,也可以在 transcript 任意一点 fork 另一条,看到父到那个位置为止的历史而不复制它。他们的例子是 Slack:频道是一条 conversation,有人开 thread,thread 就是频道在那条被回复的消息处的 fork,两条同时跑、互不阻塞。每条 conversation 还存自己的 agent 配置——模型、thinking level、启用哪些 extension 及其中哪些工具、额外 instructions、执行环境里的工作目录——所以主 agent 旁边那个 reviewer 可以用更便宜的模型、只读工具和自己的 checkout。 第四块是 Extension 模型,一个 extension 就是一组具名的 system prompt 片段、工具、hook 和 task,应用把它们装进 registry,conversation 只存名字。system prompt 每次请求前按该 conversation 选中的 extension 片段重建,改了片段下一次请求就生效;变更会记进 transcript、而且记在发生变更的位置,所以重启或 fork 看到的正是模型当时看到的东西。支持会话中途改 system prompt 和工具的模型只发变化那部分,prompt cache 依然有效。多个 extension 可以改别人的工具:同名工具后装者覆盖(比如换成跑在 venv 里的 bash),wrap 则能装饰最终胜出的那个工具(给每次 bash 调用计时就是这个写法)。 第五块是 hooks 和任务树。hook 能插进 task,包括生成模型响应、调用工具、做 compaction 这些内置 task:可以在请求送模型前重写它、block 或改写一次工具调用、替换结果、让这轮继续跑、或者自己写摘要。因为崩溃后 hook 可能再跑一次,需要做决定的 hook 必须把决定存进 memo(挂在 task 上的一个小值,first write wins)——他们的审批例子就是:重启后 hook 读到存好的答案,而不是再问你一遍。多个 extension 可以 hook 同一件事,按选择顺序成链:beforeTool 把重写后的参数往下传、第一个 block 就终止链,afterTool 传结果,onYield 停在第一个让 run 继续的 hook,afterResponse 这类 observer 总是全跑;hook 抛异常会被上报且链继续,只有 beforeTool 里抛异常会 block 掉这次调用。 任务与 conversation 构成一棵 ownership 树,abort 是自底向上的:每个 task 先清理自己的副作用,一个 task 只有在它拥有的活儿结束之后才算完成。文里的结账例子很能说明问题——多张卡同时扣款,一张被拒其他自动 abort 并退款;扣款用 task id 当幂等键,崩溃重跑也只扣一次。这里还有个实用区分:foreground task 属于当前工作,conversation 要等它们做完才算 idle,你按 Esc 中止对话就会连带中止它们和它们拥有的一切;background task 属于这条 conversation 但不属于当前这轮工作,它跑的时候对话照常 idle,普通 abort 不碰它——适合要比这一轮活得更久的 subagent,或者「明天提醒我」;当然单独 abort 那个 task、或者用 background 选项 abort 整条 conversation,还是能停掉它。 剩下几个细节也都值得一提。compaction 是在后台做的,总结旧消息但不打断 agent 正在干的活。应用状态(比如一个 todo list)是作为 document 跟 transcript 存在一起的,这样多个用户或 UI 能同时接入、观看并操舵同一个 agent。工具和 extension 的代码可以在 agent 运行时热替换,下一次工具调用自动用新代码。 放到大背景里看,Pi 1.0 同天到的还有 Codemode(原生 MCP 加上 Jev 这类决策模型和图像模型)、虚拟模型扩展、延迟工具加载、Anthropic 的 cache warming、会话中途的 system message、新 TUI 主题和默认全屏。安装就是 npm install @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord,它是库不是第二个 CLI。HN 上那天 932 点 307 评论,社区里比较真实的用法是 Slack/K8s 里常驻的会话、手机经 Tailscale 连回家里、CRM 或守护进程型 agent;反过来,改一个 PR、逛一遍 repo 这种一次性的事,还是留在 1.0 的 TUI 里更合适。最后提醒两点:这个包标着 experimental,MIT 是 license 不是稳定性承诺;Pi 本身没有内置权限系统,blast radius 得靠容器、沙箱、项目信任,以及频道那头有个活人看着。最现实的失败模式还是老两样——无界工具循环,和环境里的 secret。 一句话收:它做的事就是把 harness 从「跑在一个人终端里的 loop」升级成「跑在任何 JS 运行时上、能被多个 surface 和多个用户操舵、且崩了能自己续上的常驻底座」——跟你 Learn-Open-Harness 里拆的那层是同一根线,https://github.com/joyehuang/Learn-Open-Harness,只是往应用侧又推了一步。