잠시만요, 달이 떠오르면 글도 환해질 거예요.
终端 / 网页 / 聊天工具
↕
Pi Durable Harness
│
├── 模型访问:请求模型、处理回答
├── 会话管理:历史、分支、消息队列
├── 任务管理:执行、等待、恢复、取消
├── 工具与扩展:工具调用、审批、提示词
├── 存储:会话、任务进度、业务状态
└── 执行环境:文件、Shell、本地或远程计算环境replay: "safe"{
type: "input",
content: "Fix the flaky login test",
requestId: "job-42"
}云服务器上的 Harness
├── 使用数据库保存状态
├── 调用模型
└── 通过远程执行环境,在另一台机器上执行工具主会话
模型:能力较强的模型
工具:读取、编辑、执行命令
审查会话
模型:较便宜的模型
工具:只读工具
运维讨论会话
工具:搜索日志
禁止:生产部署主任务
└── 调查工具任务
└── 子会话
├── 查询天气
├── 查询博物馆
└── 查询列车{
"items": ["检查发布日志"]
}
一、为什么已经有 Pi,还要做 Pi Durable?
普通 Pi 的典型使用方式是:
这种使用方式有一个重要前提:有人在操作它。
如果进程中断,你可以查看文件、检查会话,再让它继续。普通 Pi 本身也有会话保存和扩展能力;文章并没有说这些能力不存在。
但是,当你想做下面这些应用时,要求就不同了:
这时,仅有聊天记录还不够。系统还必须知道:
Pi Durable 主要解决的,就是这些运行过程中的状态管理问题。
它不会直接提高模型的推理能力,但是会改变模型能力被组织、执行和恢复的方式。
二、先理解文章里的几个基本概念
1. Harness 到底是什么?
在本文中,Harness 可以理解为 Agent 运行框架。
模型负责生成回答、决定调用什么工具;Harness 负责让这些决定得到执行,并维护整个过程。
因此,调用一次模型 API,只完成了其中一部分。
2. 这些名词分别代表什么?
名词
在这里的具体含义
Agent
某个模型,加上思考级别、工具、指令等配置
Conversation,会话
一段独立的交互过程,有自己的历史和 Agent 配置
Transcript,会话记录
用户消息、模型回答、工具结果、提示词变化等记录
Task,任务
一个有持久化执行进度的工作单元
Document,状态文档
待办、计划、配置等结构化 JSON 数据
Execution Environment,执行环境
工具实际读取文件、执行命令的环境
Extension,扩展
一组提示词片段、工具、钩子和任务定义
这里最重要的是 Task。
一次模型请求是任务,一次工具调用是任务,生成历史摘要也是任务。开发者自己定义的支付、提醒、调查流程,也可以成为任务。
这种统一设计,使不同类型的工作能够使用同一套恢复和取消机制。
三、Durable 的核心:进程中断之后,怎样继续?
这是整篇文章最值得仔细理解的部分。
1. 保存执行状态,才能恢复执行
假设 Agent 正在处理:
执行过程可能是:
如果只有聊天记录,进程崩溃之后,系统可能不知道:
Pi Durable 将工作组织成持久化状态机。
“状态机”在这里的意思很简单:
例如:
任务推进到一个新的检查点,就保存新的状态。新进程打开同一份存储后,可以根据这些状态恢复工作。
2. 检查点恢复,不等于恢复到任意一行 JavaScript
这一点很容易误解。
Pi Durable 不会自动保存整个 JavaScript 内存、调用栈和每个局部变量。
它依赖明确保存的:
因此,恢复单位是任务阶段和持久化记录。
对于中断的模型请求,原文说得很明确:
所以,它也不保证模型从中断的那个字继续生成,重试后的回答可能不同。
3. 最难的问题:外部操作已经成功,但是本地还没记录
看这个顺序:
如果进程恰好在②之后、③之前崩溃:
重启之后,框架无法仅凭本地记录确定银行究竟做了什么。
部署、发送邮件、创建工单,也有同样的问题。
因此,Pi Durable 不会无条件重复所有中断的工具调用。
它要求工具声明是否可以安全重放:
工具类型
中断之后的处理思路
搜索问题、读取数据
通常可以重新执行
有可靠幂等机制的写操作
可以在满足条件时重新执行
部署、扣款等不确定操作
默认不自动重放,向模型报告中断
safe 是开发者对工具行为作出的声明,框架不会自动证明这个工具安全。
另外,“安全重放”不意味着返回值一定相同。例如搜索重新执行后,结果可能因为外部数据变化而不同;关键是重复执行不会造成不可接受的副作用。
对于不自动重放的部署调用,模型仍可能在看到中断结果后决定再次发起部署。因此,生产系统仍然需要授权检查、状态查询和幂等控制。
4. requestId 保证的是哪一种“不重复”?
原文有:
假设客户端提交成功,但是收到确认前网络断开了。它不知道服务器是否已经接受任务,于是再次提交。
如果仍然使用同一个 requestId,同一会话会返回原来的提交记录,不再创建第二份提交。
可以这样区分:
两者解决的是不同位置的问题。
“一次提交”不等于“所有相关模型请求、工具执行和外部副作用都只发生一次”。
原文支付示例给银行传入 payment-${task.id},正是为了让银行依据稳定的操作标识去重。这个保证需要银行 API 自身支持。
四、“可以在任何地方运行”具体是什么意思?
文章将两个位置分开了:
例如:
因此,一个会话可以使用本地目录,另一个会话可以使用远程容器。
env 函数负责为工具调用提供对应的执行环境。
这意味着你可以分别决定:
但是,“任何地方”有明确条件:文章当前指的是具备 JavaScript 运行时,并且有相应存储、执行环境适配的地方。
存储后端也有区别
后端
主要特点
Memory
数据只在内存中,进程结束后不能依靠它恢复
SQLite
持久保存;适合长历史,只按需要读取数据
JSONL
文件形式保存;当前实现还会在内存中保留完整历史
Cloudflare Durable Object 的 SQLite
使用该平台提供的持久存储,需要对应适配
原文关于“历史很长,内存占用仍然可控”的描述,重点针对 SQLite 的工作方式:
这不意味着内存永远不增长。并发会话更多、活跃任务更多,所需内存仍然会增加。
还有一个非常重要的限制:
同一份存储同一时间由一个进程管理,其他客户端连接这个进程。
所以,它支持很多客户端,并不自动意味着你可以启动多个服务进程,同时管理同一份存储。
五、会话、分支、子 Agent,为什么能用同一套机制?
1. 每个会话都有自己的 Agent 配置
同一个 Harness 可以同时管理:
会话之间可以并发工作,不需要相互等待。
这里的“一个 Harness”并不等于“一个模型”或“一个共享上下文”。
2. Fork:从历史中的某个位置创建分支
假设频道里已经讨论到消息 C:
讨论串从 C 分叉,它可以看到 A、B、C,然后独立继续。
它不需要复制所有父会话消息,而是引用截至分叉点的历史。
但是要区分:
会话 fork 不会自动为你创建 Git worktree、复制目录或启动独立容器。如果两个会话使用同一个目录,它们仍然可能影响同一批文件。
3. 子 Agent 就是有特定任务归属的会话
原文没有提供一个固定的“多 Agent 产品模式”。
它提供的基本机制足以让开发者这样组织子 Agent:
子会话本身已经具备:
因此,不必再为子 Agent 单独设计一套恢复机制。
原文示例还有两项关键处理:
所以父工具恢复时,可以继续等待原来的子 Agent,而不会无意间创建第二个。
4. Ownership:谁取消,哪些工作跟着取消?
历史分支关系与任务归属关系是两回事。
例如:
取消主任务时,其所属的前台工作也需要取消。
框架按照从下到上的顺序执行清理,让子任务先处理自己的副作用,然后父任务结束。
前台任务和后台任务
类型
当前会话是否等待它结束
普通会话取消是否影响它
前台任务
是
是
后台任务
否
否,需要显式取消或包含后台任务的取消
“明天提醒我”适合作为后台任务;“完成这次修复前必须通过的测试”适合作为前台任务。
但是,取消任务不天然等于撤销已经发生的业务操作。原文支付示例中的退款,是开发者编写的清理逻辑。已经完成的支付,也不会仅因为另一个任务失败就自动变成未发生。
六、“无限长对话”是怎样实现的?
这里的“无限长”指的是会话可以持续积累历史并继续交互。
模型每次处理的上下文,仍然有长度限制。
假设完整历史已经很长:
模型下一次实际看到的内容可能是:
Pi Durable 的改进是后台压缩
传统的处理方式常常是在接近上下文上限时停下来生成摘要。
Pi Durable 会提前创建一个摘要任务:
只有当下一次请求已经无法容纳当前上下文时,主会话才必须等待摘要。
为什么要等到“轮次边界”?
因为已经开始的模型请求有固定的输入,不能在它执行途中突然替换其中一部分历史。摘要需要在后续请求开始前生效。
如果模型服务仍然报告上下文过长,框架会进行压缩,并重试一次。
这套机制保留了什么,又失去了什么?
因此:
完整历史可检索,与模型当前知道完整历史,是两个不同的能力。
原文还提供了 reset(handoff):
你可以据此构建长期助手,但是历史检索、重要事实的结构化保存,以及摘要质量,仍然需要应用设计。
七、扩展为什么也必须参与“持久化”?
如果只有框架自身能恢复,而扩展一重启就忘记执行到了哪里,那么整个应用仍然不可靠。
因此,Pi Durable 将扩展能力纳入同一套运行机制。
一个扩展主要可以提供四类内容
内容
用途
提示词片段
项目说明、技能列表、当前业务状态
工具
搜索、文件操作、部署等
Hooks,钩子
在请求或工具执行前后检查、修改或阻止行为
任务定义
自定义长期流程、等待、计时和清理逻辑
1. 提示词可以变化,而且变化会被记录
每次请求之前,框架重新生成当前会话需要的提示词片段。
例如:
框架会把这些变化记录在会话中的对应位置。
这有两个好处:
第二点依赖模型服务的能力,不能理解为所有服务都具有相同的缓存效果。
2. 审批不能只存在于某次函数调用里
原文有一个部署审批例子:
问题是,如果批准之后进程重启了,是否还要重新询问?
Pi Durable 提供 memo,把这个任务的决定保存下来:
恢复后,如果已经保存了批准结果,就直接复用。
这里的保证仍有边界:如果外部审批完成,但本地还没保存结果就崩溃,仍然需要审批系统自己的关联 ID 或去重设计。
还有一个值得注意的安全选择:
beforeTool 钩子抛出异常时,框架会阻止工具调用。
因为审批代码异常时继续部署,会绕过原本应有的检查。
3. Malleable:运行期间更新代码
这里的“可塑性”主要体现为扩展可以动态替换:
会话保存的是扩展名和工具名,实际代码由当前进程的 Registry 提供。
这种分离允许:
但是,状态兼容问题仍然存在。如果新代码改变了任务检查点的结构或含义,就需要相应的版本迁移。
热更新不会自动解决旧数据与新代码不兼容的问题。
八、为什么还需要 Document?聊天记录不够吗?
假设 Agent 管理一份待办列表。
仅仅在回答中写:
并不能替代一份可靠的业务数据。
界面需要展示列表,后续工具需要修改列表,进程重启后也需要恢复列表。因此,应用通常还需要:
Pi Durable 把这类数据称为 Document:带有类型定义的 JSON 状态。
它可以保存:
关键能力:状态和记录可以在同一个事务中修改
例如一次提交同时完成:
事务保证这些修改全部成功,或者全部不生效。
因此,应用有办法避免“会话记录已经宣布成功,但是对应业务状态没保存”的问题。
不过,这需要开发者将相关修改放进同一个事务。调用外部工单 API、写另一个数据库等操作,不会自动获得同样的原子性。
分叉时,业务状态应该是什么?
假设:
现在从消息 A 创建分支,新分支应该有什么待办?
框架允许选择:
配置
分支初始状态
`asOf`
分叉位置当时的值:`[检查日志]`
`current`
创建分支时父会话的最新值:`[检查日志, 修改配置]`
`initial`
使用初始状态,例如空列表
这是业务语义,不只是性能选项。
如果你希望“从过去某个位置探索另一种方案”,通常需要当时的状态;如果某项配置应当始终继承最新值,就可能选择 current。
九、多人协作为什么与持久化有关?
原文有一个贯穿设计的原则:
顺序是:
因此,界面中的内容有统一的数据来源:
一个用户中途加入时:
客户端断线后,也可以重新获取当前状态,不必依靠此前收到过哪些临时事件来猜测进度。
这就是持久化如何同时支持:
多个人发消息时,如何处理?
文章展示了两种主要方式:
方式
什么时候生效
**Steer**
当前一轮工具调用结束后,加入正在处理的工作
**Follow-up**
当前工作完成后,作为下一项输入处理
例如:
可以作为 steer,调整当前任务。
可以作为 follow-up。
但是,多客户端共享状态并不自动解决:
这些仍然属于应用的权限和协作规则。
十、用文章最后的旅游助手,把这些机制连起来理解
文章演示的过程大致是:
与此同时,主 Agent 仍然可以和用户交谈。
然后进程中断:
重新启动后:
期间,用户还可以查看子会话、调整调查方向,或者要求压缩上下文。
这个演示想说明的是:并发、子 Agent、恢复、交互和上下文管理,都建立在同一套会话与任务机制上。
它们之间不必各自维护一份互不一致的执行状态。
十一、哪些表述需要附带条件理解?
这是读完文章后最需要保留的判断。
文章中的能力
实际边界
崩溃后继续
需要存储仍然可用,并有新进程打开它、恢复调度
长期运行
不会自动保证机器、容器或服务永远在线
无限长对话
历史可持续增长;模型仍然只处理有限上下文
不重复提交
`requestId` 在同一会话内去重,不覆盖所有外部副作用
安全重放工具
依赖工具作者正确声明,并实现必要的幂等机制
多人协作
共享状态和提交能力,不包含完整的认证授权方案
热更新
已有持久化状态仍然需要版本兼容或迁移
多执行环境
执行环境抽象不自动构成安全沙箱
文档还进一步说明了存储层面的限制:
因此,文章中的“durable”应该结合具体故障类型、存储后端和工具行为理解,不能视为任何故障下都不会丢失任何数据。
另外,它仍然是实验性项目,API 会变化。 如果准备实际开发,应按使用版本的文档核对,文章示例主要用于解释设计。