跳至正文
Agents — Pi Agent 框架架构:包边界、Agent Loop 与接入选择

Pi Agent 框架架构:包边界、Agent Loop 与接入选择

AI 参与说明(Agent:Codex):本文依据 Pi 官方文档、v1.1.0 源码和 npm 公开元数据整理框架边界,并在 Node.js 24.16.0 中运行固定版本的最小案例。资料核验于 2026-10-09;选型建议是本文的工程判断。运行记录:模型 gpt-6.1-sol,reasoning effort ultra,执行入口 Codex Desktop,提供方 openai;运行记录中的 CLI 版本 0.162.0-alpha.17.2(不代表桌面 App 版本)。

中文润色(2026-10-10,Agent:Codex):使用 humanizer-zh 调整正文表达,保留原有事实、术语、代码和链接;本次未重新核验技术事实。运行记录:模型 gpt-6.1-sol,reasoning effort ultra,执行入口 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-aiProvider、模型、认证、消息格式和流式请求已有自己的循环,只需要统一模型接口
@earendil-works/pi-agent-coreAgent Loop、内存状态、Tool 执行、事件与输入队列自己构建产品,保留对工具和对话的控制
@earendil-works/pi-coding-agentCLI,以及 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:创建后注册 Provider,再从中取得模型并发起请求。不同应用可以维护自己的 Provider 组合,不必共享一个全局注册表。Models 源码

ts
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:

bash
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
ts
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();
}

编译并执行:

bash
npx tsc --target ES2023 --module NodeNext --strict --skipLibCheck --outDir out core-smoke.ts
node out/core-smoke.js

本次实际输出:

text
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 Agentpi-coding-agent SDK显式工具范围、资源发现与 Session 生命周期
非 TypeScript 宿主或需要独立进程RPC modeJSONL 协议、进程生命周期与异常退出处理
后台长任务必须跨重启继续pi-durable崩溃恢复、去重、存储所有权与版本升级

如果尚未选定场景,可以先读 pi-ai 的请求边界和 pi-agent-core 的循环,再了解 AgentSession 增加了哪些能力,最后研究 pi-durable 的持久化语义。相比先安装一组社区扩展,这个顺序更容易看清使用框架时宿主需要承担哪些职责。

权限、文件系统、进程和网络的隔离仍属于宿主环境。Pi 默认继承启动进程的权限,内存会话、Tool 参数校验和执行钩子都不能替代系统级隔离。Containerization

关联阅读

本文共 3390 字,创建于 Oct 9, 2026
博客助手

正在打开博客助手…