AI 参与说明(Agent:Codex):本文依据 Pi 官方文档、
v1.1.0源码和 npm 公开元数据整理框架边界,并在 Node.js24.16.0中运行固定版本的最小案例。资料核验于 2026-10-09;选型建议是本文的工程判断。运行记录:模型gpt-6.1-sol,reasoning effortultra,执行入口 Codex Desktop,提供方openai;运行记录中的 CLI 版本0.162.0-alpha.17.2(不代表桌面 App 版本)。
Pi 可以作为构建 Agent 的工具箱使用。研究架构时,最有用的起点是分清模型调用、Agent Loop、完整 Coding Agent Session 和持久化任务运行分别属于哪一层,再决定需要复用多少。
日常终端使用见 Pi Coding Agent 入门与社区玩法。本文围绕框架本身展开。
先认识几个术语
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Agent Loop | 智能体循环 | 请求模型、执行模型选择的工具,再将结果交回模型 |
| Agent Runtime | 智能体运行时 | 管理上述循环、状态、事件与取消的软件层 |
| AgentSession | 保留 API 名称 | Pi Coding Agent 中管理对话、资源和工具的对象 |
| Provider | 提供方 | 接入某个模型服务所需的协议、认证和模型定义 |
| Tool | 工具 | 由程序执行、供模型请求的具体操作 |
| Transcript | 对话记录 | 包含指令、消息、工具调用和结果的有序记录 |
| Durable Execution | 持久执行 | 保存运行状态,使进程重启后可以恢复工作 |
版本基线与包边界
截至整理日,pi-ai、pi-agent-core、pi-coding-agent 和 pi-durable 的 npm latest 均为 1.1.0,这些包要求 Node.js >=22.19.0。本文以 npm 精确版本和 Git tag v1.1.0 为基线;官网的 latest 和仓库 main 会继续变化。Release,coding-agent package.json
| 包 | 主要职责 | 适合复用的部分 |
|---|---|---|
@earendil-works/pi-ai | Provider、模型、认证、消息格式和流式请求 | 已有自己的循环,只需要统一模型接口 |
@earendil-works/pi-agent-core | Agent Loop、内存状态、Tool 执行、事件与输入队列 | 自己构建产品,保留对工具和对话的控制 |
@earendil-works/pi-coding-agent | CLI,以及 AgentSession、资源加载、会话管理和开发工具 | 复用完整 Coding Agent 行为,或开发 Pi Extensions |
@earendil-works/pi-durable | 持久化 conversation、task、document 与恢复运行 | 研究需要跨重启继续的后台任务 |
@earendil-works/chord | 服务、状态、RPC 与插件的应用组合 | 理解 pi-durable 等上层应用的运行支撑 |
这里的依赖关系很关键:pi-coding-agent 使用 pi-agent-core 和 pi-ai;pi-durable 直接使用 pi-ai 与 Chord,拥有自己的 harness。它不是在 Agent 构造参数里增加一个数据库开关。仓库包清单,durable package.json
flowchart TB
Session[pi-coding-agent] --> Core[pi-agent-core]
Session --> AI[pi-ai]
Core --> AI
Durable[pi-durable] --> AI
Durable --> Chord[chord]
图中只画与 Agent Loop 和持久化相关的直接依赖。一个完整应用也可以复用 pi-ai 的 Provider,同时使用自己的持久化系统。
pi-ai:统一模型请求,执行工具仍由上层负责
A model request can produce Tool calls; the host must execute them and supply the results. pi-ai 提供模型与流式消息接口,Tool 的描述告诉模型有哪些能力,真正的操作仍需要上层实现。Tool 调用处理
当前接口使用显式的 Models collection:创建 collection,注册 Provider,再从 collection 取得模型并发起请求。不同应用可以维护自己的 Provider 组合,不必共享一个全局注册表。Models 源码
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
const models = createModels();
models.setProvider(anthropicProvider());
const model = models.getModel("anthropic", "claude-sonnet-4-6");
if (!model) throw new Error("Model not found");
这是配置片段,尚未发起模型请求。真实调用还需要该 Provider 的有效认证。旧的全局 getModel()、stream()、complete() 接口位于 /compat,属于兼容入口;新程序应从 collection API 开始。迁移说明
pi-agent-core:把模型请求组织成 Agent Loop
Agent 保存模型、可执行 Tool 和消息,借助 streamFn 请求模型。模型返回 Tool call 后,循环验证参数、调用相应实现、追加 Tool result,然后继续请求模型。模型不再要求 Tool,且没有待处理的输入时,本次运行结束。Agent Loop 源码
flowchart TB
Input[用户输入] --> Context[组织 Transcript]
Context --> Request[pi-ai<br/>请求 Provider]
Request --> Response{返回 Tool call?}
Response -->|是| Validate[验证参数<br/>与执行前检查]
Validate --> Execute[执行 Tool<br/>或生成错误结果]
Execute --> Result[追加 Tool result]
Result --> Steering[完成本轮<br/>处理 steering]
Steering --> Context
Response -->|否| Queue{还有 steering<br/>或 follow-up?}
Queue -->|是| Context
Queue -->|否| End[结束本次运行]
这张图展示正常路径;模型错误和取消也会让循环收束。工具错误可以作为 Tool result 反馈给模型,供它调整下一步,不能只把失败记录在服务器日志中。
几个接口决定了宿主能在哪些位置介入:Core types
| 接口 | 用途 |
|---|---|
transformContext | 在发送前裁剪消息或补充外部上下文 |
convertToLlm | 将应用自定义消息转换为模型能接收的消息 |
beforeToolCall / afterToolCall | 在参数验证后检查执行条件,或处理执行结果 |
prepareRequest | 紧邻每次 Provider 请求前调整请求上下文 |
finishTurn | 完成本轮模型与工具处理后决定结束或继续 |
subscribe | 接收文本更新、Tool 进展与运行结束等事件 |
不能把 finishTurn 无条件设成继续,否则会造成无终点的请求循环。应用自己的通知、UI 状态等消息也不必全部交给模型;通过 convertToLlm 明确决定哪些内容进入请求。
并发、输入队列与取消
Tool concurrency and concurrent prompts are different controls. 同一轮 Tool 默认可并发执行;同一个 Agent 在忙时再次 prompt() 会抛错。工具结束事件可以按完成先后到达,但 Transcript 中的 Tool result 保持模型原始调用顺序。Agent 控制源码,并发执行说明
如果工具修改同一份资源,可以设 toolExecution: "sequential";某个 Tool 的 executionMode: "sequential" 也会使整批串行。忙时补充输入用 steer() 或 followUp():前者在当前工具轮结束后介入,后者在循环自然准备结束时追加工作。两者都不能撤销已经完成的外部操作。
abort() 发出取消信号,Tool 实现需要响应 AbortSignal。关闭宿主时,再 await agent.waitForIdle() 等待收束。异步 subscribe 回调会被等待,因此慢回调会影响运行时间;日志写入和事件转发应有明确的耗时上限。
pi-coding-agent SDK:复用完整 Session 的代价与收益
The SDK embeds Pi in a Node.js or Bun process. createAgentSession() 返回可直接提交 Prompt、订阅事件、管理模型和资源的 AgentSession。它默认使用工作目录、发现的资源、已有设置和凭据。SDK
这意味着 createAgentSession() 很适合复用已经配置好的 Pi 环境。嵌入服务时则应逐项决定:
cwd和agentDir属于哪个工作空间。- 哪些 Extensions、Skills、Prompt Templates 和项目指令允许加载。
- 哪些 Tool 激活,以及其操作实际作用在哪个文件系统或服务。
- 使用什么模型、认证、设置与
SessionManager。 - 运行完成或连接关闭后,何时取消订阅并
session.dispose()。
SessionManager.inMemory() 只改变会话存储,不会一并关闭资源发现或把工具变成只读。需要完全显式的环境时,应提供 ResourceLoader、工具范围与内存设置,而不是只换一个 SessionManager。Full control 示例,资源加载选项
CLI 中默认加载的 MCP、Codemode 和 Tool Search 也不能直接推定为 SDK 的默认行为;SDK 按官方示例显式装配相应 Extensions。Codemode / MCP 示例
Session persistence 与 Durable Execution 的区别
Core 的 sessionId 用于 Provider 缓存关联,不是会话数据库。Agent 的内存状态需要宿主保存;Coding Agent 的 SessionManager 管理会话记录,但保存对话也不等于保证任意外部操作能在崩溃后安全重跑。Core sessionId,SessionManager
pi-durable 则把 conversation、task、inbox、document 与恢复调度纳入自己的 harness。它使用不同的 Tool 接口,不应把 Core 的 AgentTool.execute(toolCallId, params, signal, onUpdate) 原样拿来替换 durable 的 execute(args, api, context)。Durable Tool types
Pi Durable is experimental in v1.1.0. 官方仍提示 API 可能随版本变化。其存储目前由单个进程拥有,没有跨进程锁;恢复时,崩溃中断的工具默认不重跑,只有明确标注 replay: "safe" 才参与重放。Pi Durable
因此,研究长任务时至少要分开核验三件事:状态能否恢复、工具能否安全重试、同一外部操作能否去重。例如一个发送请求的工具,即使已有持久化记录,也仍要考虑远端已经接受请求、而本地尚未来得及记录成功的情况。replay: "safe" 是工具作者承担的条件,不是框架自动保证外部操作只发生一次。
一个可运行案例:观察真实 Loop,替换模型服务
下面使用 Pi 自带的 fauxProvider,预先安排模型输出,运行真实 Agent 与 Tool。它验证两条路径:合法调用得到 5 并交回下一轮;非法参数在执行前被拒绝。无网络请求,无需 API Key。Faux Provider
在一个空工作目录中安装精确版本,将下方 TypeScript 保存为 core-smoke.ts:
npm init -y
npm pkg set type=module
npm install --save-exact --ignore-scripts @earendil-works/pi-agent-core@1.1.0 @earendil-works/pi-ai@1.1.0 typebox@1.3.27
npm install --save-dev --save-exact --ignore-scripts typescript@6.0.3 @types/node@26.4.1
import assert from "node:assert/strict";
import { Agent, type AgentTool } from "@earendil-works/pi-agent-core";
import {
createModels, fauxProvider, fauxAssistantMessage, fauxToolCall,
} from "@earendil-works/pi-ai";
import { Type } from "typebox";
const parameters = Type.Object({ a: Type.Number(), b: Type.Number() });
let executions = 0;
const add: AgentTool<typeof parameters, { sum: number }> = {
name: "add", label: "Add", description: "Add two numbers.",
parameters,
async execute(_id, { a, b }, signal) {
signal?.throwIfAborted();
executions++;
const sum = a + b;
return { content: [{ type: "text", text: String(sum) }], details: { sum } };
},
};
const faux = fauxProvider();
const models = createModels();
models.setProvider(faux.provider);
const agent = new Agent({
initialState: { model: faux.getModel(), systemPrompt: "Use add.", tools: [add] },
streamFn: models.streamSimple.bind(models),
});
const events: string[] = [];
const unsubscribe = agent.subscribe((event) => { events.push(event.type); });
try {
faux.setResponses([
fauxAssistantMessage(fauxToolCall("add", { a: 2, b: 3 }), { stopReason: "toolUse" }),
(context) => {
const result = context.messages.findLast((m) => m.role === "toolResult");
assert.equal(result?.isError, false);
assert.deepEqual(result?.content, [{ type: "text", text: "5" }]);
return fauxAssistantMessage("5");
},
]);
await agent.prompt("Add 2 and 3.");
assert.equal(executions, 1);
assert.equal(faux.state.callCount, 2);
assert.ok(events.includes("tool_execution_end"));
assert.equal(events.at(-1), "agent_end");
faux.setResponses([
fauxAssistantMessage(fauxToolCall("add", { a: "bad", b: 3 }), { stopReason: "toolUse" }),
(context) => {
const result = context.messages.findLast((m) => m.role === "toolResult");
assert.equal(result?.isError, true);
return fauxAssistantMessage("Invalid arguments rejected.");
},
]);
await agent.prompt("Try an invalid argument.");
assert.equal(executions, 1);
assert.equal(faux.state.callCount, 4);
console.log("PASS: tool loop, result feedback, schema rejection");
} finally {
agent.abort();
await agent.waitForIdle();
unsubscribe();
}
编译并执行:
npx tsc --target ES2023 --module NodeNext --strict --skipLibCheck --outDir out core-smoke.ts
node out/core-smoke.js
本次实际输出:
PASS: tool loop, result feedback, schema rejection
这验证了库的循环与参数检查行为;模型选择 Tool 的能力还需用真实 Provider 另测。TypeBox 参数校验只验证数据形状,工具内部仍须实现业务规则与访问条件。示例中的 details 是 Core Tool result 的必填字段;失败可以抛出异常,或者明确返回 isError: true,不能把普通成功内容中的一句“失败”当作错误标记。Tool result 类型
怎样决定后续研究顺序
以下是工程建议,可以等目标明确后按条件选择:
| 目标或约束 | 优先研究 | 应先验证的结果 |
|---|---|---|
| 已有 Agent Loop,只缺多模型接口 | pi-ai | 目标 Provider 的 Tool call、错误与流式事件 |
| 自定义工具、界面与已有会话存储 | pi-agent-core | 工具结果回传、输入队列、取消与状态保存 |
| 尽快复用完整 Coding Agent | pi-coding-agent SDK | 显式工具范围、资源发现与 Session 生命周期 |
| 非 TypeScript 宿主或需要独立进程 | RPC mode | JSONL 协议、进程生命周期与异常退出处理 |
| 后台长任务必须跨重启继续 | pi-durable | 崩溃恢复、去重、存储所有权与版本升级 |
尚未选定场景时,先读 pi-ai 的请求边界和 pi-agent-core 的循环,再观察 AgentSession 增加了什么,最后研究 pi-durable 的持久化语义。这比先安装一组社区扩展更能看清框架本身需要宿主承担的职责。
权限、文件系统、进程和网络的隔离仍属于宿主环境。Pi 默认继承启动进程的权限,内存会话、Tool 参数校验和执行钩子都不能替代系统级隔离。Containerization
关联阅读
- Pi Coding Agent 入门与社区玩法:终端使用、Extensions、Skills 与 Packages。
- pi-hashline-edit-pro:锚点编辑为什么能拒绝过期修改,以及怎么用:一个真实编辑 Tool 的条件检查与受控验证案例。
- CLI Integration:Print、JSON 与 RPC 的宿主集成边界。