跳至正文
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 版本)。

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:创建 collection,注册 Provider,再从 collection 取得模型并发起请求。不同应用可以维护自己的 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

关联阅读

本文共 3265 字,创建于 Oct 9, 2026

相关标签:AI, Agent, TypeScript, Tools, ByAI

博客助手

正在打开博客助手…