@earendil-works/pi-ai
统一多提供方模型 API:模型目录、认证解析、流式与非流式调用、工具声明以及 token / 成本信息。它负责“怎样和模型说话”。
一条可以边读、边运行、边检查的路线:先认识 Pi 的包边界,再做 最小模型调用 → 最小 Agent 循环 → 带工具的 harness → 可恢复、可审计的实现。所有 API 示例以 Pi 当前官方文档和源码为准。
学习过程会从运行现成的 CLI 开始。完成这一步后,你可以直接检查项目行为,再往下拆解底层包。
node -v 确认当前版本。pi;在 Pi 中用 /login 配置一个受支持的模型提供方。pi --version 正常。node -v
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
pi --version
mkdir pi-lab && cd pi-lab
pi先划清安全边界:Pi 的内置工具和扩展以 Pi 进程本身的操作系统权限运行。项目 trust 决定是否加载部分项目资源,不会把工具调用变成沙箱。练习请用专用目录和非敏感文件;不可信代码或自动化任务应在容器等隔离边界中运行。
创建一个临时目录,放入一份可丢弃的 notes.txt。让 Pi 只总结这份文件;查看会话里实际发生的读文件动作。检查点:你能说明 Pi 进程在这个目录之外是否仍有权限吗?
把 harness 想成“让模型完成任务的运行系统”:它不仅发请求,还要管理消息、工具、运行状态和与使用者的交互。Pi 把这些职责拆成可复用的包。
统一多提供方模型 API:模型目录、认证解析、流式与非流式调用、工具声明以及 token / 成本信息。它负责“怎样和模型说话”。
基于 pi-ai 的有状态 runtime:维护消息与运行状态、发起请求、处理工具调用、继续循环并发出事件。它负责“怎样把请求变成一轮任务”。
交互式编码代理 CLI 与 TypeScript SDK:在核心 runtime 上提供会话、资源发现、设置、工具和多种运行入口。它负责“给人或应用一个完整的 Pi”。
| 包 / 能力 | 职责与适用时机 | 起步判断 |
|---|---|---|
| pi-tui | 差分渲染、终端组件、输入焦点和布局;用于构建自己的终端界面。 | 先不用 |
| pi-durable | 耐久化 conversation / task / document runtime;提供 memory、JSONL、SQLite 存储入口,是独立的持久化能力。 | 需要可恢复数据时研究 |
| pi-telemetry | 厂商中立的显式 telemetry contract 与 schema;带参考内存 adapter,不自带 exporter 或某个后端。 | 先定义观测点,再接 adapter |
| chord | 面向插件组合的独立 runtime:facets、typed services、replicated state、远程服务边界。它不依赖 monorepo 里的其他 Pi workspace package。 | 多环境插件系统才需要 |
依赖方向口诀:从底向上是“模型 API → Agent runtime → 面向人的 CLI / 面向应用的 SDK”。不要为了跑第一个模型请求把 TUI、Chord、telemetry 或持久化一起塞进来。
给下面三个目标各选一个主要入口:① 只想问一次模型;② 在自己的 Node 服务中加一个有状态工具循环;③ 直接把 Pi 的会话与配置能力嵌进 TypeScript 应用。
pi-ai 做一件最简单的事这一课先不做 Agent,也不执行工具。我们只注册一个 provider、找到模型、发一条 user message、读取完整回复。用这一层可以分辨:问题来自模型接入,还是之后的工具循环。
import { createModels } from "@earendil-works/pi-ai";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
const models = createModels();
models.setProvider(openaiProvider());
const model = models.getModel("openai", "gpt-4o-mini");
if (!model) throw new Error("Model not found in this provider catalog");
const response = await models.complete(model, {
messages: [{
role: "user",
content: "用一句话解释什么是 agent harness。",
timestamp: Date.now(),
}],
});
for (const block of response.content) {
if (block.type === "text") console.log(block.text);
}
console.log("tokens:", response.usage.input, response.usage.output);在项目目录执行:npm init -y,再 npm install @earendil-works/pi-ai;把 package.json 设为 ESM("type": "module"),然后用支持 TypeScript 的运行器或编译器运行示例。需要有效的提供方认证,依照官方 provider authentication 文档配置;不要把 API key 写进源码或网页。
Models 集合负责根据 provider + model 标识路由。这里使用完整 catalog 以便快速开始;产品只需一个 provider 时,可像示例一样导入 provider 子路径,减少不必要的模型目录和 SDK 包体。complete() 返回最终 AssistantMessage;若需要逐字增量和工具调用事件,再选择 stream() 并遍历事件。
先让这段 complete() 代码成功运行。然后把它改成官方文档中的 models.stream(model, context):监听 text_delta 输出增量,最后用 await stream.result() 拿到完整消息。观察二者最终文本是否一致。
模型 ID 和 provider catalog 会更新;若查不到模型,应从该 provider 当前目录查询,不要把示例 ID 当永久契约。Pi 的官方 README 强调可选择 provider 子路径以控制 bundle 大小。
Agent 接过消息和循环核心差别不是“模型变聪明了”,而是应用开始持有可变状态,并定义模型调用、事件观察和工具执行怎样接起来。Pi 的 Agent 是有状态 wrapper:拥有当前 transcript、发出 lifecycle 事件并执行工具。
import { Agent } from "@earendil-works/pi-agent-core";
import { createModels } from "@earendil-works/pi-ai";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
const models = createModels();
models.setProvider(openaiProvider());
const model = models.getModel("openai", "gpt-4o-mini");
if (!model) throw new Error("Model not found");
const agent = new Agent({
initialState: {
systemPrompt: "你是一个简洁、准确的学习助手。",
model,
},
streamFn: models.streamSimple.bind(models),
});
agent.subscribe((event) => {
if (event.type === "message_update" &&
event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
await agent.prompt("用三点解释:模型 API 和 harness 有什么不同?");
console.log("\\n消息条数:", agent.state.messages.length);这段与官方 agent-core Quick Start 使用同一结构:创建 provider / model,构造 Agent,把 models.streamSimple.bind(models) 传作 streamFn,订阅文本增量,然后 prompt()。绑定方法很重要:让调用时的 this 仍然是 Models 集合。
| 组成 | 最小做法 | 本阶段要问的问题 |
|---|---|---|
| 推理后端 |
| 从哪个 provider / 模型来?凭证如何注入? |
| 状态 |
| 消息如何保留、裁剪或恢复? |
| 事件通道 |
| UI 怎样展示文字、工具和结束状态? |
| 执行策略 | 本课暂不启用工具 | 模型请求之外,哪些操作有副作用? |
打印事件类型;核对 agent_start、message_update、message_end、turn_end 与 agent_end 是否出现。随后把订阅函数写成 async 并延迟一小段时间,观察 Agent 等待订阅者完成这一点。
到这里你有了最小 harness:它已管理一个运行态会话并公开事件。它还不是生产系统:目前无用户自定义工具、无应用持久层、无策略边界、无超时与评测。
Agentic 循环会把工具声明发给模型。模型返回结构化的 tool call 后,Pi runtime 验证参数、运行本地 execute、把 tool result 写回 transcript,再决定是否继续模型请求。记住,真正拥有能力的是执行工具的宿主进程。
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { Type } from "@earendil-works/pi-ai";
const addTool: AgentTool = {
name: "add_numbers",
label: "Add numbers",
description: "Add two finite numbers and return the sum.",
parameters: Type.Object({
a: Type.Number({ description: "First number" }),
b: Type.Number({ description: "Second number" }),
}),
execute: async (_toolCallId, params) => {
const sum = params.a + params.b;
return {
content: [{ type: "text", text: String(sum) }],
details: { sum },
};
},
};
agent.state.tools = [addTool];AgentTool 的 schema 描述要传给模型的参数;execute 是本地业务逻辑,返回 text content(也可有 details 给 host 使用)。Pi 官方示例还支持 onUpdate 发进度、AbortSignal 中断以及每工具的执行顺序配置。失败应 throw error,让 runtime 把工具错误交给模型,不要伪装成成功文本。
const agent = new Agent({
initialState: { systemPrompt, model, tools: [addTool] },
streamFn: models.streamSimple.bind(models),
beforeToolCall: async ({ toolCall, args, context }) => {
if (toolCall.name === "delete_file") {
return {
block: true,
reason: "delete_file is disabled in this lesson",
terminate: true,
};
}
},
});这只是运行时策略的一个挂点。实际应用还应由 host 检查经 schema 验证后的参数、用户身份与授权、路径归属、额度和副作用级别;对写文件、发邮件、交易等高影响动作,增加合适的确认或隔离流程。不能仅凭模型自述、提示词或者项目 trust 决定是否允许。
项目 trust ≠ 权限沙箱。Pi 官方安全文档明确说:工具和扩展按启动 Pi 的 OS 用户权限工作;trust 决定一部分项目资源是否加载,但不限制工具能访问什么。
先接上 add_numbers 并用明确提示要求它计算 17 + 25。检查工具参数、工具结果、随后模型答复。再写一个“读取白名单内文件”的工具:先做路径规范化,再确保目标仍位于允许目录内;不通过检查就 throw error。初次练习不要实现任意 shell 工具。
如果你想使用 Pi 的现成会话、工具、设置和资源发现,不一定要自己重新拼装 agent-core。pi-coding-agent 提供交互 CLI,以及 Node.js / Bun 中的 TypeScript SDK。先根据进程边界和控制需求选入口。
| 入口 | 数据形态 / 生命周期 | 适合的任务 |
|---|---|---|
| Interactive | 终端 UI;持续到用户退出 | 人直接与 Pi 协作 |
最终文本到 stdout;一次运行 | 脚本只要最终回答 | |
| JSON | JSONL 会话 / agent 事件;一次运行 | 程序要消费结构化进度 |
| RPC | JSONL 命令与响应;进程长驻,可双向控制 | 应用跨进程控制 Pi |
| TypeScript SDK | Pi 直接嵌入 Node.js / Bun 进程 | 需要进程内访问 AgentSession 与 Pi 能力 |
# 人交互
pi
# 一次调用,只取最终文本
pi --print "总结这个仓库的目录结构"
# 一次调用,订阅 JSONL 事件
pi --mode json "检查测试失败的原因" > events.jsonl
# 长驻 RPC 进程;命令通过 stdin 每行发送 JSON
pi --mode rpc --no-session注意:JSON mode 输出的是一串换行分隔的事件,不是单个 JSON 响应;RPC 的 prompt 命令被接受,不代表 run 已经结束,完成时应持续读事件直至 agent_settled。执行行为和权限仍沿用运行 Pi 的进程,模式不等于沙箱。
import { createAgentSession } from "@earendil-works/pi-coding-agent";
const { session } = await createAgentSession();
try {
await session.prompt("当前目录里有哪些文件?");
console.log(session.getLastAssistantText());
} finally {
session.dispose();
}此工厂默认使用当前工作目录、已发现资源、存储设置和配置凭证;会话默认持久化。若宿主明确不想写 session 文件,官方 SDK 提供 SessionManager.inMemory()。不再用时调用 dispose(),它会中止活动工作并清理 extension context 和事件监听器。
分别为三个需求选模式并写出理由:A. CI 里只要最终摘要;B. 桌面界面要实时显示 token / tool 活动,但运行完退出;C. 自己写的 Node 应用要在同一进程内订阅和控制 Pi。
--print;B 用 --mode json,消费 JSONL 事件;C 用 TypeScript SDK。如果 B 还要求后续任意时刻继续发送命令,则改为长期 RPC 进程。不同模式复用核心 agent / sessions / resources / tools,区别在 I/O 和进程生命周期。健壮性不是一层万能包装,而是明确管理失败、状态、安全与可观察性。下面按实施顺序增量加固,不必一次上齐所有 package。
agent.abort(),需要等待清理完成时再 waitForIdle()。pi-telemetry 是显式 contract,要由应用接到选用的后端。Pi 里可直接查证的控制点:Agent API 当前有 state、subscribe()、prompt()、continue()、abort()、waitForIdle(),并有 beforeToolCall / afterToolCall、prepareRequest、transformContext 等 hooks。先从官方类型声明和对应 README 验证版本,再为具体业务设计外围策略。
对你的示例逐条模拟:① provider 超时;② JSON/tool 参数无效;③ 工具抛异常;④ 用户取消;⑤ 存储失败;⑥ 重试后结果重复。每种情况写“对调用者的结果、transcript / 持久状态、是否重试、如何告警”。至少把 ③、④、⑥ 做成自动测试。
目标不是复制整个 coding-agent,而是做出一个权限有边界、能观测执行过程、出现故障时能说明白的最小产品原型。
达成标准:不是“模型答得很好”,而是你能说明任务在何处读取状态、工具如何被允许执行、失败怎样结束、下次怎样恢复,以及什么证据能证明行为符合预期。
核验时间:2026-09-29,基于上述仓库 main 分支资料。Pi 会持续发布,示例适用于该次核验时的 API 形状,不是固定版本承诺;升级依赖后先对照 Quickstart、README 与 TypeScript declarations 更新用例。