Pi Durable

Hacker News Top 工具

摘要

Earendil 与 Pi 社区在发布 Pi 1.0 的同时,也推出了一款实验性的新软件包 Pi Durable——一个极简框架,用于构建可长时间运行、可塑性强的智能体,只要具备 JavaScript 运行时即可在任何地方运行,并通过可插拔的存储后端实现故障容错。

相关内容:<i>Pi 1.0</i> - <a href="https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=49926069">https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=49926069</a> - 2026 年 10 月(184 条评论)
查看原文
查看缓存全文

缓存时间: 2026/10/01 23:13

# Pi Durable | Earendil 来源:https://earendil.com/posts/pi-durable/ 今天,Earendil 与 Pi 社区一起发布了 Pi 1.0(https://earendil.com/posts/pi-1-0/)。这体现了我们的信念:在经过无数小时的加固、维护和积极开发之后,Pi 现已是一个坚实的基础,可以在此之上构建。同时,Pi 也在持续演进。与 Pi 1.0 一同发布的,还有一个实验性的新包——Pi Durable。 Pi Durable 专为长期运行、可持久化且可塑性强的智能体而构建,可在任何地方运行。我们希望你能加入其中,帮助我们把它打造成最好的持久化运行时(durable harness)。 ## 为什么需要 Pi Durable? 作为编码智能体的 Pi,其设计目标是在你的(远程)机器上运行,位于终端之中,由一个人驱动。如果进程挂掉了,你查看发生了什么,然后让它继续。这正是 Pi 1.0 专注并擅长的事情,这一点不会改变。 在 Earendil,我们希望把这项技术带给所有人,以最契合其需求的形式。为此,我们需要一个能在任何地方运行、可从不同入口访问、支持无限长对话、能在灾难性的内部与外部故障中存活、并允许多人同时引导同一个智能体的运行时。Pi Durable 正是这样的运行时。 它并不取代 Pi 编码智能体。它是一个用于构建任意智能体应用的框架,编码智能体也包括在内。它不仅与 Pi 编码智能体共享代码(比如 pi-ai),也共享其原则:极简主义与可塑性。它还让我们能够在这一领域探索各种设计,而不干扰 Pi 编码智能体。在 Pi Durable 上构建智能体应用的过程中,我们学到的经验教训,只要被证明有价值,就会回流到 Pi 编码智能体中。 ## 什么是运行时(harness)? 每个人对运行时都有自己的定义。我们之前就此写过文章(https://earendil.com/posts/what-is-a-harness/),但让我们为 Pi Durable 重新介绍运行时的概念。 运行时(harness) = 存储 + 运行一个或多个与大语言模型并行对话所需的机制。它提供模型所调用的工具,以及工具运行的执行环境。 一次「对话」是指你与一个智能体之间的交互,被记录为一份对话记录(transcript)。智能体就是大语言模型本身,连同它的设置(比如思考等级)以及它能调用的工具。工具通过执行环境来完成工作,执行环境可以是你的笔记本电脑、一台远程虚拟机,或者一个内存沙箱。每个对话决定智能体获得哪些工具以及使用哪个执行环境。 运行时运行的一切,从调用模型到执行工具,都叫做一个任务(task)。和 Pi 中的一切一样,Pi Durable 的构建方式让智能体能够理解它。不含测试的全部源代码约 15,000 行,用 GPT 大约折合 150,000 个 token,用 Claude 大约 250,000 个。这是最坏情况。要在 Pi Durable 之上构建,你的智能体几乎很少需要全部代码;仅存储后端就有 3,000 行,它通常可以跳过。 现在让我们带你简单参观一下 Pi Durable,以说明我们构建了什么以及为什么这样构建。 ## 长时间、随处运行 我们希望智能体能长时间运行,并且能在任何地方运行——目前的「任何地方」指的是任何有 JavaScript 运行时的地方。在 Pi Durable 中,运行时通过打开一个存储后端来启动。Pi Durable 内置了内存、SQLite 和 JSONL 三种存储方式,还附带一套一致性测试套件,以及针对你自己的后端的基准测试。SQLite 和 JSONL 的存储代码不使用任何 Node API,因此通过一个小的适配器,它就能在 Bun 或 Cloudflare Durable Object 中运行。存储接口很小,在你现有的东西(比如键值存储或 Postgres)之上很容易实现。 同一时刻只有一个进程拥有某个存储,其他客户端附着到该进程上。在 SQLite 上,运行时只把工作集保留在内存中:活跃的对话记录、进行中的任务和待处理的提交。其他一切都在磁盘上,直到需要时才读取。活跃的对话记录天然被模型的上下文窗口所限,因为压缩机制会在旧消息溢出之前对它们进行摘要。因此,即使一个对话包含数万条消息,也能恰好放进内存。 需要文件或 shell 的工具从执行环境获得它们。Pi Durable 内置了一个 Node 执行环境,让工具能够访问你本地的文件。与存储一样,执行环境的接口也很小、很容易实现,因此你也可以为你的工具暴露远程执行环境。这就允许运行时运行在一台机器上,而它的工具运行在另一台机器上。你的 `env` 函数会为每次工具调用构建环境,从对话的工作目录出发,这样每个对话都可以运行在不同的位置。 ```js import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context"; import { createModels } from "@earendil-works/pi-ai/models"; import { openaiProvider } from "@earendil-works/pi-ai/providers/openai"; import { createRegistry, Harness } from "@earendil-works/pi-durable"; import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node"; import { openNodeSqliteStorage, } from "@earendil-works/pi-durable/storage/sqlite/node"; import { CodingTools } from "@earendil-works/pi-durable/tools"; const context = BACKGROUND_CONTEXT; // 每次调用都传入一个用于取消的上下文 const models = createModels(); models.setProvider(openaiProvider()); const registry = createRegistry(); registry.install(CodingTools); // 读、写、编辑、bash const env = ({ cwd }: { cwd?: string }) => new NodeExecutionEnv({ cwd: cwd ?? process.cwd() }); const harness = await Harness.open( await openNodeSqliteStorage("./agent.sqlite"), { models, registry, env }, context, ); // 根对话:首次使用时创建,之后每次重启后还是同一个 const root = await harness.root(context, { agent: { model: { provider: "openai", modelId: "gpt-6.1-sol" }, cwd: "/work/repo", }, }); ``` ## 在崩溃中存活 我们希望智能体即使进程挂掉也能存活下来——无论是笔记本睡眠、容器被重新部署,还是机器内存耗尽——并从中断处继续。在 Pi Durable 中,一次运行的每一步都是一个任务,它在继续之前会先存储一个检查点(checkpoint)。如果进程挂掉,一个新的进程会打开同一个存储,找到未完成的任务,并从各自的最后一个检查点继续。 被中断的模型请求会被重新发送;已有的部分答案会留在对话记录中,标记为已中止。被中断的工具调用,如果可以安全重跑就会重跑;否则会告知模型该调用被打断了。 Pi Durable 没有内置的子智能体,但只需几行代码就能构建,下面的 triage(分流)工具就演示了这一点。子智能体运行在自己的对话中,所以它也能从中断处继续;一个可以安全重跑的子智能体工具会重新找到它的子智能体,并等待它的回答。排队中的消息仍然保持排队。一个 `requestId` 让一次提交是「恰好一次」的,因此崩溃后重试的客户端会拿回原始提交结果,而不是提交两次。 ```js const job = { type: "input", content: "Fix the flaky login test", requestId: "job-42", } as const; await root.submit(job, context); // 进程在这里挂掉,卡在一次工具调用中间。 // 一个新的进程打开同一个存储。 const harness = await Harness.open( await openNodeSqliteStorage("./agent.sqlite"), { models, registry, env }, context, ); harness.resume(); // 继续被中断的运行 const root = await harness.root(context); // 同一个提交,已经得到回答 const settled = await (await root.submit(job, context)).wait(context); ``` ## 同时运行多个对话 我们希望一个运行时能同时运行多个对话,而不会相互阻塞。在 Pi Durable 中,一个运行时可以并发地运行你需要的任意数量的对话,且都享有相同的保证。 一个对话可以从零开始,也可以在任意时刻分叉出另一个对话,并且能看到父对话到那一刻为止的历史,而无需复制它。想象一个 Slack 频道,你的智能体回答来自任何人的 @ 提及。然后有人开启了一个帖子(thread)。频道可以是一个对话,帖子则是它在所回复那条消息处的分叉。两者同时运行,互不阻塞。 ```js const channel = await harness.root(context); const question = await channel.submit( { type: "input", content: "@agent why did the deploy fail?" }, context, ); const answered = await question.wait(context); // 有人在帖子中回复了智能体的答复。每个对话都会标明它的拥有者, // 这决定了「中止」会影响到谁(关于「任务」见下文)。 // 这个帖子没有拥有者。 const thread = await channel.fork( answered.answer!, { ownership: { kind: "ownerless" } }, context, ); // 两个对话同时工作。 const inThread = await thread.submit( { type: "input", content: "@agent can we roll it back?" }, context, ); const inChannel = await channel.submit( { type: "input", content: "@agent who is on call today?" }, context, ); await Promise.all([inThread.wait(context), inChannel.wait(context)]); ``` 每个对话还会存储它自己的智能体配置:模型、思考等级、所选用的扩展以及它们各自的哪些工具被激活、额外的指令,以及它在执行环境中的工作目录。主智能体旁边的审查者(reviewer)可以使用更便宜的模型、只读工具,以及它自己的代码检出。 ## 扩展 我们希望智能体能做的一切都是可插拔的,而每一个被插入的部件都要参与持久化。在 Pi Durable 中,一个扩展就是一组带名字的系统提示词片段、工具、钩子(hooks)和任务。应用程序把扩展安装到一个注册表中。每个对话选择它使用哪些扩展和工具,并且只存储它们的名字。 ### 系统提示词片段 系统提示词会在每次请求之前,由该对话所用扩展的各个片段重新构建而成,因此被修改的片段会在下一次请求时生效。Pi Durable 会在对话记录中记录哪里发生了变化,就记在变化发生的位置,因此重启或分叉后,看到的正是模型当时所看到的内容。在那些支持在对话中途修改系统提示词和工具的模型上,只会发送变更内容,因此提示词缓存仍然有效。 ```js import { defineExtension, section } from "@earendil-works/pi-durable"; const ProjectContext = defineExtension({ name: "project-context", sections: [ // 从对话的执行环境读取。这些文件可以在后台加载和监听; // 每次请求渲染的都是最新状态。 section("agents_md", (input) => agentsMd.latest(input.env)), section("skills", (input) => skills.latest(input.env)), ], }); ``` ### 工具 每次工具调用都作为一个独立的持久化任务运行,其意图(intent)会在执行之前存储。崩溃之后,只有工具自己声明可以重跑时才会重跑。否则,模型会被告知该调用被打断了,以及目前保存的输出,由模型决定下一步怎么做。每个对话还可以拥有自己的一套工具,比如前面提到的 Slack 帖子,它可能只能搜索而不能部署。 ```js import { Type } from "@earendil-works/pi-ai"; import { defineTool } from "@earendil-works/pi-durable"; const searchIssues = defineTool({ name: "search_issues", description: "Search the issue tracker", parameters: Type.Object({ query: Type.String() }), replay: "safe", // 只做读操作,崩溃后重跑没问题 execute: async (args, api) => { // 流式输出给每一个正在观看的客户端 api.output(`searching for ${args.query}\n`); return { content: [{ type: "text", text: await tracker.search(args.query) }], }; }, }); const deploy = defineTool({ name: "deploy", description: "Deploy a version to production", parameters: Type.Object({ version: Type.String() }), // 没有 replay:被崩溃打断的部署只会告知模型,绝不会重复执行。 execute: async (args) => ({ content: [{ type: "text", text: await ci.deploy(args.version) }], }), }); registry.install(defineExtension({ name: "ops", tools: [searchIssues, deploy] })); // 这个帖子可以搜索,但不能部署。 await thread.configure({ tools: { remove: [deploy] } }, context); ``` 工具在它的调用中会获得运行时 API:它可以提交条目和文档、启动任务和对话,以及与其他对话通信。这就让构建一个子智能体只需要几行代码。工具创建一个属于它的对话,给它一个更小的模型和自己的指令,然后等待它的回答。子智能体也是一个普通的对话,所以它能在崩溃中存活、自己计算成本,并且一个 UI 可以把它展示在那次调用之下。 ```js import type { AssistantMessage } from "@earendil-works/pi-ai"; import { AssistantEntry, configure } from "@earendil-works/pi-durable"; const triage = defineTool({ name: "triage", description: "Label an incoming issue as bug, feature, or question", parameters: Type.Object({ issue: Type.String() }), // 崩溃后的重跑会找到同一个子智能体和同一个提交 replay: "safe", execute: async (args, api, context) => { const child = await api.commit(async (tx) => { const existing = ( await tx.scanConversations({ ownerTaskId: api.taskId }, 1) ).items[0]; if (existing !== undefined) return existing.id; // 由这次调用拥有,所以中止调用就会中止子智能体。 const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId }, }); // 它一开始是这个对话智能体的副本。把它改成一个没有工具的小模型。 await configure(tx, created.id, { model: { provider: "openai", modelId: "gpt-6-luna" }, tools: [], instructions: "Answer with one word: bug, feature, or question.", }); return created.id; }, context); // 让 UI 能把子智能体展示在这次调用之下 await api.details({ conversationId: child }, context); const subagent = await api.conversation(child, context); const request = { type: "input", content: args.issue, requestId: `triage:${api.taskId}`, } as const; const settled = await ( await subagent!.submit(request, context) ).wait(context); // 回答是子智能体对话记录中的一个条目。读取它并取出文本。 const entry = await api.commit( (tx) => tx.entry(AssistantEntry, settled.answer!), context, ); const message = entry?.model?.[0] as AssistantMessage; const text = message.content .flatMap((content) => (content.type === "text" ? [content.text] : [])) .join(""); return { content: [{ type: "text", text }], }; }, }); ``` 扩展还可以修改其他扩展的工具。在更靠后的扩展中出现同名工具会替换掉先前的那个,例如一个在 Python 虚拟环境中运行的 bash。装饰(wrap)会作用于最终胜出的那个工具,无论选择装饰的扩展在哪里。 ```js import { wrapTool } from "@earendil-works/pi-durable"; import { createBashTool } from "@earendil-works/pi-durable/tools"; // 为每一次 bash 调用计时,无论对话最终用的是哪个 bash。 const Timing = defineExtension({ name: "timing", wraps: [ wrapTool(createBashTool(), (bash) => ({ ...bash, execute: async (args, api, context) => { const start = Date.now(); try { return await bash.execute(args, api, context); } finally { metrics.record("bash", Date.now() - start); } }, })), ], }); ``` ### 钩子 钩子(hooks)让扩展能够介入任务之中,包括那些内置任务,比如生成模型响应、调用工具或执行压缩。钩子可以在请求发往模型之前重写它,阻止或重写一次工具调用,替换一个结果,让一次运行继续下去,或者自己写一份摘要。钩子在崩溃后可能再次运行,所以一个需要做决策的钩子会把决策存进一个备忘录(memo):一个小值,随任务一起存储,以第一次写入为准。 ```js import { hook, ToolTask } from "@earendil-works/pi-durable"; const Approval = defineExtension({ name: "approval", hooks: [ hook(ToolTask, { beforeTool: async (call, api, context) => { if (call.name !== "deploy") return undefined; // 重启之后,钩子找到的是存储好的答案,而不是重新询问。 let approved = await api.memo( "approval:deploy", context, ); approved ??= await api.m

相似文章

Pi 1.0

Hacker News Top

Earendil 推出 Pi 1.0——一款加固、精简且可扩展的 AI 智能体框架,具备 Codemode(原生 MCP 支持)、虚拟模型的扩展支持以及延迟工具加载等特性,同时还带来实验性的 Pi Durable 包,用于长时间运行的智能体应用。两者均采用 MIT 许可证,现已可用。

@omarsar0:打造你自己的个人 agent,朋友们。我刚刚用 @pidotdev 的 Pi Durable 构建了一个我自己的小型个人 agent。我想看看……

X AI KOLs Timeline

# 一位开发者用 Pi Durable 打造个人 Agent 的实践分享 一位开发者展示了如何利用 **Pi Durable** 搭建一个属于自己的个人 Agent。项目重点聚焦于几个核心特性: ## 核心亮点 - **带检查点的持久化执行(Checkpointed Durable Execution)**:Agent 的任务执行状态可以随时保存并从检查点恢复,即使进程崩溃或被中断,也不需要从头重新开始。 - **跨会话的持久记忆(Persistent Memory)**:Agent 的上下文和知识不会随着会话结束而丢失,可以在多次对话之间保持连贯。 - **带有持久化审批的定时任务(Scheduled Jobs with Durable Approvals)**:Agent 可以自主安排定时任务,但关键操作仍需经过审批;审批状态本身也是持久化的,不会因为重启而丢失。 - **每个 Agent "Space" 独享完整的 Linux 桌面环境**:每个 Agent 都拥有一个可控的 Linux 桌面空间,可以像真人一样操作窗口、运行应用、执行命令。 ## 模型路由 在模型选择上,该设置通过 **OpenRouter** 进行路由,可以灵活地在多个模型提供商之间切换和组合,根据任务类型或成本动态选择最合适的模型。 ## 开源计划 开发者表示,计划将整套配置和搭建方案完全开源,方便社区复现并在此基础上继续扩展。 这个项目很好地展示了"个人 Agent"从概念走向可用形态的可能性——持久化执行保证了可靠性,跨会话记忆提供了连续性,Linux 桌面环境则让 Agent 具备了真正操作环境的能力。

Pi 的极简主义是其优势

Hacker News Top

Earendil 的 Pi 编码框架证明,极简设计在成本和性能方面优于复杂的替代方案,并以 Databricks 的基准测试和 Shopify 的案例研究作为证据。