Earendil 发布 Pi Durable 后,一个很自然的疑问是:Pi 不是早就能持久化对话、恢复会话了吗?为什么还需要一个带着「Durable」名字的新库?

这个疑问没有错。Pi 已经具备相当完整的会话持久化能力。Pi Durable 的新增点不是「保存聊天记录」,而是保存正在执行的工作,并让它在进程重启后按规则继续。

可以把两者的区别概括为:

普通 Pi:恢复对话上下文,让 agent 能继续聊、继续干活。
Pi Durable:恢复执行状态,让系统知道哪些工作完成了、哪些还在等待、哪些可以重跑。

发布文章介绍了会话、工具、扩展、压缩等许多能力,其中不少在普通 Pi 中已经存在。要看清真正的区别,关键不是看正常运行时能做什么,而是看:进程突然死掉时,系统保证什么?

一、Pi 已有的持久化,保存了什么?

普通 Pi 的会话持久化并不简陋。它已经会保存:

  • 用户消息、模型回复、工具调用和工具结果;
  • 模型与思考级别的变化;
  • 对话树、分支和压缩摘要;
  • 扩展通过自定义条目写入的状态。

退出后使用 pi --continue,就能恢复历史上下文,接着工作。因此,「Pi 已经会保存对话」完全正确。

但恢复一段对话,不等于恢复一个尚未完成的执行流程。

以 Pi 1.1.0 的普通 coding agent 实现为例,模型回复和工具结果主要在 message_end 时写入会话。流式输出过程、正在运行的工具,以及尚未进入对话的 steering/follow-up 消息队列,主要仍是进程内状态。

重开 session 的核心动作是从记录重建上下文,而不是恢复所有未完成的执行任务。

例如,会话里已经记录了「模型要调用这个工具」,但进程在工具执行中被杀掉,还没写入结果。仅凭这条历史记录,系统不能确定:

  • 工具根本没开始;
  • 工具做了一半;
  • 工具已经完成,只是结果没来得及落盘;
  • 是否可以安全地再执行一次。

这就是「对话持久化」与「执行持久化」之间的缺口。

二、Pi Durable 补上的是什么?

Pi Durable 将模型请求、工具调用、压缩,以及应用自定义工作,纳入同一套持久化任务机制。

这里的 task 不只是一段普通的异步函数,而是一个保存了执行阶段和检查点的状态机。进程重启后,运行时可以找到未完成的任务,从最后保存的检查点继续调度。

场景 普通 Pi 的内置机制 Pi Durable
恢复历史对话 从 session 重建上下文 同样支持
进程死后继续未完成工作 恢复会话本身不会自动恢复执行流程 从持久化的任务和检查点恢复调度
工具执行中崩溃 保留已写入的调用和结果 调用意图先落盘,按工具的重放策略恢复
排队但尚未处理的消息 普通队列在内存中 收件箱持久化,重启后消息仍在队列里
客户端重试提交同一请求 需要应用自己去重 通过 requestId 找回原提交
应用状态与执行状态 可由扩展保存,但恢复逻辑需要自行设计 文档、对话条目和任务可以在同一原子提交中更新

一个旅行规划的例子

发布文章中的旅行规划 agent 很适合说明这个区别。

一个子 agent 同时查询天气、博物馆和火车。天气与博物馆查询已经完成,火车还在查,此时进程崩溃。

普通 Pi 的恢复思路是:

恢复已有对话记录,再让模型根据记录继续处理。

Pi Durable 的恢复思路是:

任务记录表明天气和博物馆已完成,火车任务尚未完成。火车查询允许安全重放,因此恢复这个任务,然后继续等待并汇总。

决定恢复什么的,是持久化的任务状态和恢复规则,而不是让 LLM 再推断一次「我上次做到哪儿了」。

两种方式最终都可能完成任务,但运行时承担的责任不同。前者主要保存已有上下文,后者还负责记录、恢复和协调未完成的工作。

三、Durable 不是「断点续传一切」的魔法

「持久执行」很容易让人误以为:无论中断发生在哪里,都能原地接上,且绝不会重复执行。Pi Durable 的承诺没有这么宽。

它明确区分不同类型的中断:

  • 模型请求中断:重新发送请求,而不是接回原来的 token 流;已保存的部分回答标记为 aborted。
  • 允许安全重放的工具中断:只有声明了 replay: "safe",才会在恢复时重新执行。
  • 未声明安全重放的工具中断:向模型报告调用被中断,而不是盲目再执行。

例如,一个部署工具可能已经成功部署,只是本地还没写入结果。Pi Durable 不会因此自动再部署一次,但它也不能凭空知道远端最终发生了什么。

因此,**requestId 提供的是提交去重,不是所有外部副作用都「恰好执行一次」**。

扣款、发邮件、部署等操作,仍然需要外部系统的幂等键或状态核对机制。文章里的支付示例,也显式使用了银行接口的幂等键,避免任务重跑时重复扣款。

同样,恢复的前提是存储仍然可用,并且应用重新启动运行时。Pi Durable 不是进程自动重启器,也不是磁盘损坏后的数据恢复系统;具体能承受什么故障,还取决于存储后端及其配置。

四、为什么不直接增强现有 Pi?

普通 Pi 已有 SDK、RPC 和扩展,当然也能被嵌入服务,或用来构建机器人。区别不是「能不能做」,而是:执行恢复保证由运行时内置提供,还是由应用开发者自己补齐?

Earendil 将 Pi Durable 单独做成一个包,主要有两个原因。

1. 从个人编码工具,走向长期运行的 agent 应用

普通 Pi coding agent 的典型使用方式,是一个人在终端里驱动它。出了问题,人可以检查现场,再告诉它继续。

Earendil 想构建的应用则包括 Slack bot、GitHub triage bot,以及多人从不同界面共同驱动的 agent。它们需要:

  • 无人值守时,也能从执行中断中恢复;
  • 一个运行时并发管理多个 conversation;
  • 客户端晚加入或断线重连后,能拿到当前完整状态;
  • 子任务、后台任务、等待关系、取消与清理,都有明确语义;
  • agent 运行时与工具执行环境可以分开部署。

这不只是给 session 多存几个字段,而是要让执行器、工具 API、状态提交和客户端观察机制,都围绕持久化重新设计。

比如,普通扩展可以把状态写入会话,但「保存了状态」之后,还需要回答:谁来恢复任务?如何知道父任务在等哪个子任务?取消如何传递?重试会不会重复创建子 agent?客户端断线后如何看见正在运行的工具?

Pi Durable 试图把这些问题收进一套统一的运行时,而不是让每个应用各做一遍。

2. 保留稳定的 coding agent,同时试验新架构

发布文章也明确提出:单独构建 Pi Durable,可以探索这些设计,而不扰动现有 Pi coding agent;验证有价值的经验,再反馈回去。

严格说,它并不是新开了一个独立 Git 仓库,而是在同一个 earendil-works/pi 仓库中新增了 packages/durable,并发布为独立 npm 包。

它与普通 Pi 共享 pi-ai 等基础设施,但提供另一套运行时。按照项目当前的说明,Pi Durable 仍是实验性项目,API 可能变化。

五、什么情况下需要 Pi Durable?

如果需求只是:

关掉 Pi,下次还能接着这个对话写代码。

普通 Pi 已经满足,Pi Durable 没有带来根本变化。

如果需求是:

把任务交给 agent 后,即使服务重启,它仍然知道哪些任务未完成、哪些消息没处理、哪些操作能重跑,并继续处理。

Pi Durable 才有实质性的价值。

它更接近一个围绕 agent 设计的轻量持久化工作流引擎,而不是一个更高级的聊天记录保存器。

这也解释了为什么两者看上去有很多重复功能:正常运行时,它们都在接收消息、调用模型和执行工具;真正的差别,体现在故障发生以后,以及应用开发者需要自己承担多少恢复逻辑。

会话持久化回答的是「我们之前说了什么、做了什么」;执行持久化还要回答「现在有哪些工作没做完,接下来应该怎样继续」。


参考资料


评语

现如今,看自媒体写的文章,甚至是官方文档,也许都不如让AI直接给你解释来的好。看完第五章,你认为你需要Durable这个库么?我相信绝大多数人不需要,但这并不影响它是一个好东西。