大数跨境

Pi‑Durable:面向 LLM 智能体的可容错持久化运行框架

Pi‑Durable:面向 LLM 智能体的可容错持久化运行框架 苏哲管理咨询
2026-10-08
2
导读:Pi‑Durable 是 Earendil 推出的 TypeScript 智能体持久化框架,核心目标:进程中途崩溃视为暂停,而非任务丢失。它将智能体每一步可见操作原子提交到 SQLite/JSONL

编者摘要:Pi‑Durable 是 Earendil 推出的 TypeScript 智能体持久化框架,核心目标:进程中途崩溃视为暂停,而非任务丢失。它将智能体每一步可见操作原子提交到 SQLite/JSONL 存储,重启后恢复断点,不用全部重新运行任务。

整套体系由四层堆叠而成:宿主应用、Harness(执行框架)、Session 会话内核、持久化存储。Session 拥有唯一变更队列(mutation line),所有写入都经过该队列,保证提交原子性:一次提交内的对话记录、任务、应用文档要么全部落盘,要么全部回滚。外部网络、模型调用、工具执行绝对不能放在提交回调内部,避免阻塞整个会话,这就是效果三明治模式:先提交执行意图,再执行外部操作,最后提交执行结果。

框架定义 5 种核心存储记录:对话、条目、任务、提交记录、文档。Entry 条目是不可变对话转录记录,不会修改旧内容;依靠head字段实现上下文裁剪(压缩、重置会话),不会删除历史数据。Task 任务是可恢复状态机,每一个阶段会写入检查点;进程崩溃重启后,所有运行中任务回置为 pending,从最近检查点继续执行。工具存在两种重放策略:safe安全(可重复执行,崩溃后自动重跑)、unsafe不安全(不可重复,崩溃后返回中断报错,告知模型工具可能部分执行)。

Document 文档用来存放业务状态,和对话转录原子一同提交,杜绝内存状态和会话记录不一致。文档支持基础快照 + 增量 Delta,支持版本迁移;fork 分支可以按策略继承文档状态。

内置运行机制包含:提交队列 Inbox、Generation 模型调用任务、Tool 工具任务、Compaction 上下文压缩。扩展代码全部驻留内存注册表,磁盘仅保存扩展名字,重启重新加载代码即可恢复历史任务,无需持久化业务代码。

框架有明确非目标:不支持多进程同时读写一份存储,不提供 CRDT 多用户离线合并,不会自动回滚外部真实副作用。存储失败分三类:提交前失败直接回滚;存储明确拒绝,会话继续;存储结果不确定,会话被 “污染”,必须关闭并重新打开存储。

整个系统 8 条不变量约束保障持久化正确性:提交原子、存储落盘后才对外发布、无易变发布通道、外部效果脱离事务、记录 ID 不可复用、草稿提交后失效、变更队列贯穿全流程、不确定存储故障直接污染会话。开发者编写任务、工具时,所有重启恢复依赖存储内检查点,内存变量进程重启全部丢失。

20 个关键问题问与答

  1. Pi‑Durable 主要解决什么问题?
    答:解决 LLM Agent 进程崩溃、休眠、重新部署时任务丢失;重启后从断点恢复,区分工具是否可以安全重跑。
  2. 核心设计原则是什么?
    答:所有对外可见状态必须完成原子提交才对外暴露;崩溃只会丢失最后一次提交之后的进度。
  3. Mutation line 变更队列作用?
    答:所有提交串行执行,保证原子性;防止并发写冲突;外部网络 IO 不能阻塞队列。
  4. 什么是效果三明治?
    答:先提交执行意图,再执行外部工具 / 网络操作,最后提交结果;崩溃时存储留下明确记录用于恢复。
  5. safe 与 unsafe 工具 replay 区别?
    答:safe:操作幂等,崩溃重启自动重跑;unsafe:不可重复执行,重启返回中断错误,告诉模型工具可能部分运行。
  6. Task 任务 5 种状态分别含义?
    答:pending待调度;running正在执行;waiting等待子任务;completing自身完成,等待子任务结束;terminal任务彻底结束。
  7. 进程崩溃重启,正在运行的 task 会变成什么?
    答:全部退回pending,读取存储中的 checkpoint 检查点继续执行。
  8. Entry 条目为什么是不可变?如何实现会话重置?
    答:历史记录绝不修改;通过新增条目设置head,修改模型上下文起始点,原始记录保留在存储。
  9. Document 文档作用?
    答:存放 Agent 业务状态,与会话条目在同一个 commit 原子落盘,避免业务状态与对话记录不一致。
  10. base 基础快照和 delta 增量是什么?
    答:base 保存完整文档对象;delta 只保存修改补丁;系统可配置阈值自动生成 base,避免增量无限膨胀。
  11. fork 会话分支和 owned 拥有会话的差异?
    答:fork:共享父会话历史,用于 “换个方案重试”;owned:任务拥有全新空会话,用于子代理,父任务中止则子任务一并中止。
  12. Inbox 收件箱队列用途?
    答:Agent 繁忙的时候,新消息排队;区分 steer 中途介入、followUp 等待本轮结束、write 写入笔记。
  13. Compaction 压缩会删除旧对话吗?
    答:不会删除存储数据;新增摘要条目 + head 指针,模型不再读取旧内容,但原始记录仍保留,可以 fork 回看历史。
  14. Registry 注册表为什么代码不保存在磁盘?
    答:扩展工具、钩子代码保存在内存;磁盘只保存扩展名字;升级代码重启加载注册表即可恢复旧任务。
  15. 什么是 Session 会话 “被污染 poisoned”?
    答:存储调用后结果不确定(不确定是否写入成功);会话不再允许操作,必须 close 后重新打开存储。
  16. abort 中止任务流程?
    答:写入 abortRequested 标记,信号运行实例;等待全部子任务完成;执行本任务 abort 清理处理函数;输出 terminal 中止结果。
  17. 什么情况任务会 orphaned(孤立)?
    答:任务被中止,但是对应的 task 代码扩展没有安装,无法运行 abort 清理逻辑,标记为 orphaned,外部资源需要开发者自己校验。
  18. Pi‑Durable 不支持哪些能力?
    答:不允许多进程同时操作同一存储;不会自动回滚外部真实副作用;没有 CRDT 离线多人编辑合并。
  19. commit 提交回调中绝对不能做什么?
    答:禁止模型调用、网络请求、工具执行等外部 IO;会阻塞全局变更队列,同时造成状态不一致风险。
  20. 开发 Durable 任务最重要注意点?
    答:进程重启所有内存变量全部丢失,所有恢复需要的数据,必须写入 checkpoint 检查点或者 Document 文档。

附录:Pi‑Durable:面向 LLM 智能体的可容错持久化运行框架,解决进程崩溃、重启后智能体任务丢失、工具调用状态不明的问题;版本 1.0.3

Pi‑Durable 技术手册・详细总结附录

一、手册定位与阅读方法

《Pi Durable Technical Manual》是 npm 包 @earendil-works/pi-durable(版本 1.0.3,工程版,2026 年 10 月)的权威技术文档,来自 earendil-works/pi 仓库的 commit 5b6c792。它的特殊性在于:这不是根据记忆编写的说明书,而是对 “某一个包在某一次提交下的状态映射”—— 书中出现的每一条记录、每一个文件、每一个事件,都来自对该包的一次真实运行。这意味着书中的所有示例都可以复现,所有存储行都是真实落盘的结果,而非示意。

手册明确给出资料权威性排序:源码(src/**)>规范(docs/spec.md)>README 与更新日志>设计笔记>测试>示例与运行记录。当源码与规范冲突时,以源码为准,正文会明确指出差异。规范文档用内部名 “Pico5” 称呼该系统,且先于代码存在(§2.2 甚至仍写着 “Pico5 尚未实现”);而手册其余部分统一使用包的正式名称 Pi Durable。

捕获工具包(capture kit)是本书的底气所在:书中所有记录由脚本驱动真实包、并转储其存储内容得到。这些脚本运行在 @earendil-works/pi-ai的假提供者(faux provider)之上,因此无需网络、无需 API 密钥即可复现全部内容。捕获记录中 ID 与提交号跨运行稳定,但时间戳、进程 ID、UUID 不稳定;一个可变的点是:被节流的进度提交可能提前或延后一个提交,导致后续序号偏移一位。

手册的读者假设是 “已经构建或使用过 LLM 智能体的 TypeScript 工程师”:理解 “把转录发给模型、运行它返回的工具调用、再把结果发回去” 这一循环。读过本书后,读者应能设计带正确阶段(phase)的任务、预测运行任意时刻崩溃后哪些内容能幸存、能逐行阅读存储转储、能编写可安全重跑的工具、能定位运行卡住的原因。

二、核心规则与八大不变量

整本书用一个句子定义了系统:“会话(Session)原子地提交不可变条目、完整任务记录和由 Chord 跟踪的文档;只有已提交的状态可被观察。”前半句说明提交的内容 —— 转录记录、任务记录、文档变更作为一个整体写入;后半句说明观察的边界 —— 屏幕、等待者(waiter)、观察者(watch)、调度器本身,全部读取已提交状态,不存在旁路通道(side channel)。

这个设计被概括为 “以提交为唯一入口的门”:所有可见变更都必须穿过提交这道门,所有观察者都坐在门后。系统围绕它定义了八个不变量,每个都堵住一类具体故障:

  1. 一次提交跨所有记录与文档写入原子
    —— 要么全部落盘,要么全部没有。若拆成多次写入,崩溃就会留下 “永远没有任务去执行的工具调用”。
  2. 文档更新只在存储提交成功后才发布
    —— 避免屏幕上出现崩溃即被抹掉的状态。
  3. 所有可见进度都是持久的,无易变发布路径
    —— 不存在 “不提交也能显示” 的旁路,半截回答与工具输出也被提交(由 settings.progress 节流)。
  4. 外部效果不进入变更事务
    —— 模型、进程、工具、网络、人工效果都在变更队列之外运行,防止一次慢调用拖垮所有会话。
  5. 条目与 ID 不可变、永不重用
    —— 所有记录共享一个 ID 空间,记录之间用 ID 互相引用,ID 重用会让引用悄悄改变含义;即使某次提交因故障失败,其占用的 ID 也保持占用。
  6. 草稿随回调结束而失效
    —— 提交回调内的 tx.doc()返回可变草稿,回调落定时草稿被撤销,越界引用无法在任意提交之外修改文档。
  7. 变更队列贯穿到存储落定与采纳
    —— 下一提交永远从上一提交已存储的状态开始;提交观察者在队列上运行但只捕获不可变值,用户的 watch 与文档状态回调在队列外稍后运行,慢 UI 不会卡死框架。
  8. 不确定的存储故障使会话失效(poisoned)
    —— 存储返回 “可能写了也可能没写” 时,内存无法确知磁盘内容,会话拒绝后续所有调用,必须关闭重开;只有 “存储明确拒绝(StorageRejected)” 能保证什么都没写入,才允许会话继续。

对开发者最核心的三条纪律是:任何展示或作用于其上的状态都必须是已提交的;相关联的状态变更要放进同一次提交;绝不在提交回调内调用模型、工具或网络。

三、四层架构与内存 / 存储分工

手册把包看作四层堆叠:宿主进程(你的程序)在最上,调用 Harness;Harness(运行框架)把调用转成一次提交;Session 会话内核拥有唯一的变更队列、对话、条目、任务、提交与文档;存储(Storage)在最底层保存每一次提交。宿主在打开 Harness 时把框架要运行但不拥有的代码 —— 扩展、模型、执行环境 —— 注入进来。

Harness 内部由四个组件构成,各司其职:Conversation 句柄(只是 ID 加方法,不含状态,按 ID 比较);Submissions 提交组件(输入的准入、每个会话的收件箱、wait () 背后的等待者);任务调度器(每个存活任务的内存镜像、保留 / 运行 / 中止);视图组件(viewState、watch、任务图、智能体事件)。框架用三个内置任务回答输入:pi.generation(准备提示并调用模型)、pi.tool(运行一次工具调用)、pi.compaction(总结旧上下文)。它们是内置的,不可移除也不可替换,因此必须用 createRegistry()构建注册表,Harness.open()会拒绝缺少它们的注册表。

内存与存储的分工是整个包的关键:存储保存真相,内存只保存 “总能从存储重建的工作集”。内存中的东西包括:存活任务(打开时全部加载,并把死进程遗留的运行中任务在单次提交内退回 pending)、已加载文档缓存、等待者与观察者订阅。进程死亡时,这些都不重要 —— 重新打开存储就能全部读回,“重新打开存储就把工作从停止处接起来”。

包的入口面:@earendil-works/pi-durable暴露 Harness、createRegistry、defineExtension/defineTool/defineTask/defineDoc、内置文档与任务、MemoryStorage、createSession 等;storage/sqlite与 storage/jsonl分别提供可移植核心(后者基于 FileSystem,也运行在 Bun 或 Cloudflare Durable Objects),/node后缀提供 Node 专属实现;env提供执行环境接口;tools提供读写编辑与 bash 工具;testing提供存储与环境一致性测试。包要求 Node 21.9.0 或更高。

四、会话与提交(Session and Commits)

会话(Session)是系统的内核,拥有唯一一条变更队列、对话、条目、任务、提交与文档。提交(Commit)是原子保存:一次调用 Storage.commit(writes),要么全部存储,要么全部不存。所有记录共享一个 ID 空间,记录之间靠 ID 互相引用。

变更队列(mutation line)一次只运行一个提交,从回调一直持有到存储落定、新修订被采纳。这样每个提交都从上一提交已存储的状态出发。提交观察者在队列上同步运行(仅捕获不可变值),而用户的 watch 与文档状态回调在队列外稍后运行。在 “一次回答一个工具调用” 的捕获中,一次小交互产生约 17 次提交,其中每次提交都对应存储中的一个序号(seq)。

提交失败的处理按失败阶段区分:回调准备失败(如草稿中出现非 JSON 值)发生在存储看到批次之前,正常回滚;存储明确拒绝(StorageRejected)保证什么都没写入,会话可继续;存储结果不确定则使会话 “中毒”,此后必须关闭并重新打开存储。此外,HEAD等特殊字段用于控制上下文起点,使会话支持压缩、重置而不删除历史。

五、对话与上下文(Conversations and Context)

对话(Conversation)是一个转录作用域,可以分叉(fork)出另一个对话。条目(Entry)是不可变转录记录。轮次(Turn)指一次助手回答及其工具调用;运行(Run)是从被接收的输入到最终回答的轮次序列,运行活跃期间对话处于 “忙碌(busy)” 状态 —— 而 “忙碌” 被定义为 pi.live.run存在。

内置条目类型覆盖整个生命周期:用户输入(pi.user)、系统提示(pi.system)、助手消息(pi.assistant)、工具调用(pi.tool)、工具结果(pi.tool-result)、进度(pi.live)、任务报告(pi.task-report)等。转录要变成模型上下文,会经过一条生成管线:渲染系统提示与工具列表、设定思维级别与截断边界。

分叉(fork)让新对话读取父对话到某一点的条目,然后写自己的;owned 会话则是任务拥有的全新空会话,用于子代理 —— 父任务中止,其子对话一并中止。重置(reset)通过新增条目设置 head,而不是删除历史;交接(handoff)与 heads机制管理多分支下的上下文起点。所有旧条目仍保存在存储中,可随时 fork 回去查看。

六、文档(Documents)

文档(Document)是应用的类型化 JSON 状态,与会话条目在同一次原子提交中变更 —— 这是手册反复强调的要点:待办列表永远不可能领先于修改它的那条消息。文档由 定义(Definition)声明,定义是 TypeScript 声明,命名文档、设定初始值与存储规则。

文档有三种作用域 / 生命周期形式:随会话保存或随任务保存。底层实现采用 base 基础快照 + delta 增量:base 保存完整文档对象,delta 只保存修改补丁;系统按配置阈值自动生成 base,防止增量无限膨胀,并支持版本迁移(migration)。

五个内置文档分别是:pi.live(运行状态)、pi.inbox(收件箱)、pi.usage(用量)、pi.provider(提供者)、pi.agent(智能体配置,存模型选择)。它们随根对话的创建在同一次提交中建立(ID 2–6)。这些文档用 Chord 库跟踪,每个已加载文档在会话内核里有一个跟踪器。

七、任务(Tasks)

任务(Task)是可恢复的状态机,附着在一个对话上,运行于任何提交之外。每次模型调用和每次工具调用都是一个任务。

任务有多个阶段(phase):prepare(准备)、request(请求)、execute(执行)、report(报告)等;每个阶段都会写入检查点(checkpoint)。进程崩溃后,任务从最近检查点恢复。

任务状态机包含:pending(待调度)、running(运行中)、waiting(等待子任务 / 工具)、completing(自身完成、等子任务收尾)、terminal(彻底结束)。调度器保留每个存活任务,只通过已发布的提交得知新任务。

效果三明治(effect sandwich)是工具与外部效果的核心模式:先提交执行意图(把最终参数与重放策略写入任务),再在变更队列之外运行 execute()(工具、进程、网络),最后把结果作为新提交提交回去。这样崩溃时,存储里留下了明确意图记录。

工具重放(replay)策略是关键设计:声明为 safe的工具是幂等的,崩溃后重开存储会把它重新运行一遍;声明为 unsafe的工具不可重复执行,崩溃后框架不会重跑,而是写一条带 “工具可能部分执行(interrupted)” 错误的工具结果,让模型基于已提交的输出行与错误信息作答。开发者必须逐工具决定 “运行两次是否安全”,这一个决定决定了被中断调用的命运。

拥有关系与结构化并发(ownership & structured concurrency):任务拥有子任务,中止(abort)自上而下传播 —— 写入 abortRequested 标记、发出信号、等待全部子任务完成、执行本任务的 abort 清理钩子、输出 terminal 中止结果。若任务被中止但对应代码未安装(无法运行清理),任务被标记为 orphaned(孤立),外部资源需开发者自行核验。pi.generation任务把 pi.live.generation写成 {attempt: 1}等进度记录。

八、运行(Runs)

运行是从输入到答案的完整周期。提交(Submission)是准入单位:一次提交把用户输入追加为 pi.user 条目、创建 submission 记录、创建首个 pi.generation 任务、并在 pi.live中把对话标记为忙碌。收件箱(Inbox)在对话忙碌时给新消息排队,区分 steer(中途介入)、followUp(等本轮结束)、write(写入笔记)等意图。

运行控制由 pi.live文档承载,定义忙碌 / 空闲。生成(generation):request 阶段先提交 pi.live.generation = {attempt:1},再在队列外调用 models.streamSimple();流式输出以默认最小 100ms 间隔提交进度(throttle),慢流会产生被节流的中间提交。工具调用与重放见上文。压缩(compaction):通过新增摘要条目并推进 head,让模型不再读取旧内容,但不删除存储数据;旧记录仍保留,可 fork 回看。

用量与成本(usage & cost)由 pi.usage文档累计,提供按请求、按会话的用量核算。

九、扩展与智能体(Extensions and the Agent)

注册表(Registry)保存已安装扩展:工具、提示片段、钩子、包装、自定义任务。关键设计是代码不持久化—— 磁盘只保存扩展名字,代码只在内存注册表中;升级扩展后重新打开存储、重载注册表,即可恢复旧任务继续运行。因此 README 提醒:恢复未完成工作前,要重新安装相同的扩展与任务代码。

每个对话一个智能体(per-conversation agent):智能体配置(模型选择等)存在 pi.agent文档里,随对话创建。

钩子(Hooks)提供在提交、观察等时机注入行为的机制。定义工具(defining tools):用 defineTool 声明工具名、参数、执行逻辑与重放策略。系统提示(system prompt)由 prompt sections 组成,prepare 阶段渲染为 pi.system 条目。重载(reload while running)支持运行中更新扩展。子代理(subagents)通过 owned 会话实现:父任务调用子任务,子任务拥有独立空对话,父中止则子一并中止,结果作为任务结果提交回父对话。

十、观察(Watching)

系统提供多级可观察性:Chord 文档状态,通过文档跟踪器可订阅每次提交的文档变更;对话视图与 watch (),把每次提交的操作(ops)发布给订阅者,一个视图一个帧(frame per commit);智能体事件(agent events),捕获任务状态流转;任务图与 inspect (),展示存活任务的依赖与状态。

观察者可以读取、steer 一个对话,但所有写入仍只经过一个 Harness—— 这是 “多客户端可看可引导、但单写入者” 的体现。时序图中的每个箭头是一种交换样式,实线 / 虚线 / 点线区分调用、提交、任务提交、模型请求、工具执行、发布视图、中止、回复等。

十一、存储与环境(Storage and Environment)

存储契约:存储后端保存记录与文档,并原子地提交一批写入。包提供三种:MemoryStorage(内存,不持久)、SqliteStorage(单数据库文件)、JsonStorage(目录中的文件)。storage/sqlite核心是可移植的同步数据库门面上的核心,/node提供 Node 专属入口。

SQLite:对话、条目、任务、提交、文档、记录 ID 都落在 session.sqlite的单文件里,所有记录共享一个 ID 计数器。JSONL:每个会话一个文件,每行一个 JSON 对象,type标记行类型,第一行是会话头。一致性测试:registerStorageConformance()与 registerEnvConformance()验证后端是否符合契约。执行环境(ExecutionEnv):每次工具调用与提示渲染都会构建一个执行环境,定义工具可访问的 I/O 能力;env提供接口,env/node提供 NodeExecutionEnv。

十二、实践与边界(In Practice)

构建编码智能体:手册用 read/write/edit/bash 四个工具(CodingTool 扩展)演示如何组合文档、任务与运行机制构造真实编码代理。“会咬人的契约(contracts that bite)”:几个最容易踩的坑 —— 把外部 IO 放进提交回调、依赖内存状态做恢复、允许多进程写同一存储、认为崩溃会自动回滚外部效果。

限制与非目标(limits and non-goals)明确列出:一次存储同时只允许一个进程拥有,无跨进程锁,框架不是集群调度器,监督与重启进程是宿主的工作;不提供 CRDT / 离线多写合并;提交只在存储内原子,外部效果不在会话变更事务内运行,当框架无法确知支付是否到账时,它会明说,并把答案交给幂等键或外部记录。

十三、参考(Reference)要点

参考部分提供完整 API(Harness 与 Conversation API)、内置条目类型清单、设置与默认值、错误码与原因(如 Interrupted、StorageRejected、Session poisoned)、术语表、来源清单与图索引。它是开发者在实现时逐项对照的权威速查。


【声明】内容源于网络
0
0
苏哲管理咨询
为企业及组织提供AI+战略、数智化转型咨询及观点、建议等
内容 2289
粉丝 0
苏哲管理咨询 为企业及组织提供AI+战略、数智化转型咨询及观点、建议等
总阅读56.3k
粉丝0
内容2.3k