大家好,我是小木头。10月1日,Pi发布了1.0版本、Code Mode、MCP延迟加载工具。这些能力现在呢,都成了正式的功能。同一天,Pi团队还放出了一个实验性的框架——Pi Durable。今天我们就来聊聊Pi Durable是什么、有什么用,以及怎样上手。

我读了它的README、设计文档和相关的示例,同时也把Pi 1.0的源代码拉到本地跑了几个实验。包括用DeepSeek驱动官方的度假规划Agent,这些实验会穿插在后面的分享中。那现在我们就开始吧,先来了解一下Pi Durable是什么。

它是Pi团队和Pi 1.0同一天发布的一个实验性的npm包,官方对它的定位是一个“durable agent harness”,也是一个用来构建各种Agent应用的框架(coding agent,当然了也包含其中harness)。

“这个词”官方给了定义:一份存储加上运行一段或多段大模型会话所需的那套机制。它还提供模型要调用的工具,以及工具运行的执行环境。

它提供的接口大致可以分成8组:第一组是负责打开和恢复,你来指定一个存储(比如一个SQLite文件),Agent它的会话记录和任务进度都写进这份存储。进程重启后,用同一份存储重新打开就能接着跑还没有完成的任务。官方demo会自动找回这份存储;在你的自己的应用里,存储放在哪里呢?这完全由你的代码决定。

第二组接口是管理会话,包括了像创建会话、或者从任何一条记录分叉出一段新的会话。那第三组接口呢,是关于提交和等待:呃比如提交一条输入等它回答,或者中止当前的工作。Agent正忙的时候,新消息可以排队、可以插话转向,也可以直接拒绝。

第四组是给每段会话单独配置模型思考强度、工具指令和工作目录。第五组接口是用来扩展Agent的能力:你可以定义新的工具;可以定义system prompt它的段落,每次请求模型之前这些段落会拼接成完整的系统提示词;还可以定义钩子在工具执行前后、请求模型的前后插入自己的逻辑,比如执行危险命令之前先把它拦下来。

这些东西呢打包成了一个扩展装进代码里的注册表(也就是registry),它记录当前程序装了哪些扩展。第六组是定义应用自己的状态,让它和会话一起呢持久化;也可以定义带检查点(也就是checkpoint)的自定义长任务。

第七组接口是订阅会话的实时状态,会推给终端、网页或其他客户端。第八组接口是选择存储以及工具执行的环境:存储呢可以是SQLite文件、JSONL目录,甚至是内存。

另外它还自带了读文件、写文件、编辑文件以及执行命令这四个工具,装上以后就能做一个coding agent。那么用这些接口能做些什么呢?首先可以做长时间运行的Agent:任务跑几十分钟甚至几个小时,中途进程崩溃了、机器重启了、容器重新部署都能够从断点继续。

还可以做聊天平台的Agent,比如Slack机器人:频道里的主对话是一段会话;每个话题串(或者Thread)是从主对话分叉出来的另一段会话,它们可以同时运行。可以做多人共用协作的Agent:网页、桌面、Slack同时连着同一段会话,后加入的人直接可以看当前的状态;任何人都可以在Agent工作的时候呢插话。

还可以做带subagent应用:主Agent把调研、审阅这类工作交给subagent,可以用更便宜的或更有针对性的模型只读的工具在后台运行。还可以做带业务状态的Agent:待办、计划、工单这类状态和对话记录一起保存,一起做分叉不会出现两边对不上的情况。

还可以做后台自动化:比如GitHub issue分拣机器人、定时提醒,会话空闲了后台任务仍然呢在继续。官方仓库里面的两个demo:一个终端coding agent,一个度假规划Agent就是用这些接口搭建出来的。

那么Pi Durable和Pi是什么关系呢?Pi的定位是:一个人在终端里用的coding agent,进程挂了你看一眼发生了什么告诉它继续就行了。官方说的很明确:这是Pi 1.0专注的事情,这个原则呢并不会变。

Pi团队想用各种适合的形式把这套技术带给更多的人。为此他们需要一个新的harness,能在任何地方运行、能从不同的界面接入;会话可以无限长能够应对内部和外部的严重故障,允许多人同时操控同一个Agent。

而Pi Durable就是这个harness:它不替代Pi,而是一个用来构建各种Agent应用的框架(coding agent也是其中一种)。官方还特别提到两点:一是它和Pi共享代码,比如模型接入库pi-ai也共享两条原则——极简和可定制。

另一点是单独做一个包,可以在不打扰Pi的前提下试验新的设计;在Pi Durable上验证有价值的经验再将其整合回Pi。最后是一个数字:Pi Durable不算测试大约15000行代码,用GPT的分词算大约15万token;用Claude算大约25万。

官方说和Pi的其他部分一样,它被设计成让你的Agent能够很轻易的读懂它。这个代码量应该是在现代化的这些模型的上下文窗口中,能够比较轻松地支持的。

接下来看它的核心机制:Pi Durable的核心对象叫Harness(官方的定义是一份存储加上在这份存储上运行Agent那套机制)。它有一条贯穿全局的规则:所有变化都通过同一条原子提交写进存储,提交完成之前不展示任何东西。

存储里有两类东西:一类呢是会话记录里的条目(会话记录在文档或代码的原文呢叫transcript):用户消息、模型回答、工具执行的结果都是这种条目,写进去以后就不能再改。另一类是任务(pi.generation负责请求一次模型;pi.tool负责执行一次工具调用)。

任务是一个持久化的状态机,每往前走一步都先把进度存为一个检查点(也就是checkpoint)。我们看一次带两个工具调用的问答,在存储里是怎样一步一步的产生的:你提交一句话会话记录里会先写入一条用户消息,然后启动一个generation任务请求模型。

模型决定调用两个工具这个回答作为一条模型消息写入了会话记录,接着两个工具调用各是一个tool任务它们都归刚才那个generation所有。工具执行完结果分别写成两条工具调用的结果,generation等两个工具都结束再把这一轮交给下一个generation。

下个generation继续请求模型拿到最终的回答写进最后一条模型的消息。在界面上看到的一切(包括流式输出的每句话、工具正在打印的日志、排队的消息)都是从已经提交的状态里面读出来的。流式内容最多每100毫秒提交一次,所以进程突然崩溃流式输出最多丢失最后的那100毫秒。

那么崩溃后怎么继续呢?每个任务都按同一个顺序执行,设计文档把它叫做“effect sandwich”(可以理解成一个三层的夹心):先把打算做什么写进存储,再真正去做最后把结果写进存储推进到下一个检查点。新进程打开同一份存储会找到所有还没有完成或没有结束的任务,从各自最后的一个检查点继续。

那么难点就在中间那一层:如果进程恰好死在已经做了但结果还没写的位置呢?重启以后没人知道这件事到底做完没有。Pi Durable把这个判断交给写工具的人:定义工具的时候可以声明replay属性,比如将它的值设为safe(意思是这个工具可以安全的重新执行)。

读文件、搜索、查询这类只读操作挺适合这样声明的。崩溃以后它会直接的重新执行;没有声明的工具不会重新执行,此时模型会收到一条“工具被中断可能已经执行了一部分”这样类似的错误信息或错误的结果,连同崩溃前已经提交的输出一起由模型来决定下一步。

自定义任务还可以用一个叫memo的机制,把这件事已经做过记录下来;重启以后先查memo就能避免同一个操作执行两遍(比如同一封邮件发两次)。我用这个规则做了第一个实验:为了结果可以复现,这个实验用的是Pi自带的模拟模型它按脚本返回固定的工具调用。

因此呢并不需要API key,我定义了两个工具:每0.4秒输出一行进度各12步。一个叫fetch_report模拟读取报告,声明了它是可以重放的(也就是replay设为了safe);另一个叫send_email发邮件,这个呢就没有去声明可以重放。模型在同一轮里并行调用它们跑到第7步的时候,我对进程发出kill -9杀掉进程然后用同一个SQLite文件重新打开。

我们来关注三点:第一,重新提交时我用了和第一次相同的requestId(Harness没有创建新的提交而是返回了原来编号为7的那一个)。客户端在崩溃后重试Agent不会把同一件事呢做两遍。第二,fetch_report声明了可以重放(也就是replay),所以它从第一步重新跑到第12步。

第三,send_email它没有重新执行它的结果是一条中断错误后面附着崩溃前已经提交的7行输出。最后看到那句回答“报告已拿到邮件被中断没有重发”是我写在模拟的模型脚本里的。如果换成真实模型它读到的就是这条错误和那7条输出再由它决定要不要先确认一下邮件到底有没有发出去。

这个实验说明了什么呢?能从断点继续不等于每个操作都只会执行一次:Pi Durable保证每一步都有记录崩溃后都能从记录继续。那么一个操作能不能重做呢?仍然要由写工具的人来自行判断,一切都先写进存储。

这个前提一旦成立很多原本需要单独设计的功能看起来就变得顺理成章,我们一个一个看。第一个是分叉:它解决场景是同一段对话要往两个方向继续。会话可以从任意一条条目分出一段新的会话,新会话能看到分叉点之前的历史之后各自就继续而且并不需要复制数据。

官方的例子是Slack Agent:在频道里回答了一个问题,有人在这条回答下面开了一个新的Thread。在这个Thread我们认为它就是一个分支和频道里的主对话可以同时运行。应用自己的状态也可以跟着分叉:Pi Durable把待办、计划、工单这类状态叫文档(它是带类型的JSON和会话记录在同一次原子提交里修改)。

文档可以声明分叉是怎样继承:从初始值开始沿用父会话当前的值,或者取分叉那一刻的值。我跑了官方的分叉示例:在分支里改一份笔记父会话里的那一份并不受影响。

第二个是收件箱:它解决的场景是Agent正在干活你又发了一条消息。默认呢是追加任务排在当前这轮回答之后等这轮结束再开始;第二种是插话转向(也就是大家在Codex当中或许时常会用到功能steer),它会被放在当前这轮工具调用之后直接加入正在进行的工作。

比如Agent正在排查一次部署失败,你补了一句“先看staging的日志”,它不用等整轮结束就能调整任务执行的方向。第三种就是忙:那就拒绝提交会直接报错。另外还有一种写入只往会话记录里写一条条目并不触发模型,适合记录用户打开了某个文件这类事件。

第三是多人协作:它解决的场景是几个人几个界面同时看着同一个Agent(因为界面只渲染已经提交的状态)。多个客户端可以连接同一个会话,后加入的客户端先拿到当前的完整视图再接收后续的变化。并不需要重放历史:推给远程客户端的是每次提交对应的一小组操作,数据量都很小适合直接走WebSocket。

也可以拿到兼容Pi原来格式的事件流那代价是传输量呢更大一些。第四个是subagent:Pi Durable的核心里面其实没有subagent这个概念,subagent就是一个由工具创建并归这个工具调用所有的会话。

工具创建一段子会话给它换模型、换工具、加指令提交任务然后呢等它回答。子会话也是普通会话所以天然具备了崩溃恢复、费用统计也能在界面里单独查看。归属关系还决定了中止的顺序:中止父级的工具调用系统会从下往上中止它拥有的所有的工作,每一层先撤销自己已经做过的操作。

任务还可以放到后台前台按Esc并不会中止后台任务。第五个是运行中替换代码:它解决的场景是Agent还在跑你想改它的工具(对话里只保存扩展的名字并不保存代码)。所以代码里的注册表可以在Agent运行时直接替换:已经开始的调用用旧代码跑那下一次调用呢就直接用新代码。

我跑了官方的示例:替换发生时正在执行那次调用输出的是v1那下一次调用呢输出的就是v2。系统提示词也一样:每次请求前根据当前启用的扩展重新拼装变化会作为一条条目写进会话记录只把变化的部分发给模型尽量保住提示词的缓存。

前面的实验用的是模拟模型,接下来我们换成真实的模型:Pi仓库里面有一个基于Pi Durable的度假规划Agent大约1300行TypeScript大部分是终端界面。它有一个research工具会在后台启动一个subagent并行做三项搜索:天气、博物馆、火车。

为了方便演示呢搜索结果写死了,三项分别需要大约6秒、10秒和30秒。搜索工具声明了可以重放,我在这里用DeepSeek Flash让它规划两个人在维也纳的周末并把调研交给subagent。请留意界面下方的任务面板它会实时显示每个任务以及它们之间的归属关系。

在这一刻天气和博物馆已经搜完火车还在搜索中进程呢直接被杀掉了。另外留意一个细节:subagent在后台调研的时候主Agent并没有停下来等,它把调研交出去以后马上回头问我:哪个周末出发?想要什么节奏?酒店预算是多少?

现在呢用--continue重新打开同一个会话。重启以后任务面板里只剩火车搜索这一项,天气和博物馆并没有重新执行。火车搜索完成以后subagent写了报告送回主会话主Agent把它整理成了完整的一个行程单。

我又直接打开了这次会话的SQLite文件核对:子会话里面一共三条搜索结果每条只有一次(天气和博物馆在崩溃前写入火车信息呢是在重启后写入的)。最终的行程也同时用上了三项结果:周六晴24度左右安排户外;周日下午有阵雨安排博物馆;离开时坐什么什么火车去萨尔茨堡每30分钟一班。

最后这条呢正是重启后才搜完的火车信息。这一次完整的运行费用大约是0.002美元。还有一点:这个demo的界面代码里没有任何处理崩溃的逻辑,它只负责渲染会话的当前状态恢复是Harness自己完成的。

现在来看看怎样上手吧:先安装3个包(Pi Durable本身、模型接入库pi-ai还有负责状态订阅和取消信号的chord——一个可以跨重启保留的最小程序)。只需要3步:第一步准备模型和注册表(创建模型集合设置模型提供方也就是我们常说的model provider;创建注册表加载必要的工具)。

第二步打开Harness:传入一个比如SQLite存储以及模型注册表和执行环境。第三步拿到根会话提交输入等待结果;进程重启后再跑一遍拿到的还是同一个根会话(也就是咱们常说的主会话),没完成的任务会接着跑相同的requestId也不会重复提交。

我在本地用模拟模型验证过:同一个SQLite文件跑两次,两次拿到的都是同一个会话、同一个提交。如果你想先体验一下效果可以直接跑仓库里的两个demo:一个是终端的coding agent,一个呢就是刚才咱们演示的度假规划Agent。

它们复用了Pi的登录信息和设置,会话存储在本地的SQLite文件里加上--continue参数就能继续最近的一次会话。另外还有32个示例:从最简单的对话到崩溃的恢复、分叉(也就是咱们平时会看到的fork概念)、还有收件箱、subagent、运行中替换代码。

每个呢都能单独运行,其中只有一个必须接真实模型其余的呢都不需要API key。

Pi Durable 自带三种存储:内存、SQLite 和 JSONL。SQLite 和 JSONL 的核心实现不依赖于 Node API,所以呢稍作适配就能跑在比如 Bun 或者 Cloudflare Durable Objects 里面。官方还提供了存储的一致性测试和基准测试,你也可以自己实现基于键值存储或者 PostgreSQL 的后端。

需要读写文件、执行命令的工具,通过一个很小的执行环境接口工作 Harness 可以跑在一台服务器上,工具实际在另一台机器、容器或沙箱里执行。每段会话还可以有自己的工作目录,这里顺便说一下 Pi Durable 和 Cloudflare 的 Durable Objects 名字很像但并不是一回事。前者呢是一个 npm 包,后者呢是 Cloudflare 的运行时,两者现在其实已经可以组合使用。

Cloudflare 的 Agents SDK 在 10 月 2 日发布了 PiHarness 测试版,把 Pi Durable 跑进 Durable Objects。对象被回收以后由定时唤醒机制重新拉起,从检查点继续回到开头的问题吧 Pi Durable 做的事情可以用一句话概括:Agent 状态全部放在存储里,进程只负责从存储里读出进度接着往下执行。

这个进程没了换一个进程打开同一份存储就能继续跑,崩溃恢复、分叉、多人协作、subagent 运行中替换代码。看上去呢是不同的功能,其实都来自于同一个前提:每一步先写进存储再执行再展示。

如果你正在做需要长时间运行或者需要多人一起操控的 Agent,Pi Durable 值得读一读。它的代码量并不大示例呢也非常的完整,本期用到的实验的脚本和运行记录我都会整理在视频描述中。如果你已经用 Pi Durable 做了一些东西也欢迎在评论区来分享吧,那我是小木头我们下期见。