SDK 编程:用代码驱动 pi
本教程共 30 篇 · 第 21 篇 · 更新于 2026-08-10 · 约 8 分钟阅读
本节目标:学会用 pi 的 TypeScript SDK 在代码里创建 Agent、发送消息、接收流式输出。学完你能写出一个”最小可用的 pi Agent 应用”,并知道什么时候选 SDK、什么时候选 RPC 模式。
为什么需要 SDK
上一章讲了 RPC 模式——通过子进程和 JSON 协议远程控制 pi。它通用性好,什么语言都能用,但有个明显的代价:你需要自己管理子进程生命周期、自己解析 JSON 事件流、自己处理错误和重连。
如果你恰好用 Node.js / TypeScript 写应用,pi 还提供了更优雅的方式:直接用 SDK。
SDK 就是 @earendil-works/pi-coding-agent 这个 npm 包里导出的编程接口。你 import 进来,几个函数调用就能创建一个 Agent 会话,发消息,收回复。所有事件都有 TypeScript 类型,IDE 自动补全,不需要手工拼 JSON。
TipSDK 和 RPC 不互斥。复杂场景可以混用——比如主进程用 SDK 直接创建 Agent,同时对外暴露一个 RPC 接口给其他语言的后端服务调用。
装 SDK
SDK 包含在 pi 的主包里,不需要额外安装:
npm install @earendil-works/pi-coding-agent
如果你的项目已经有 type: "module" 或者用 tsx/ts-node 跑 TypeScript,直接用 ESM import。如果还是 CommonJS 项目,需要用 require() 或者改成 ESM。
三行代码跑起第一个 Agent
最精简的例子:
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
modelRuntime,
});
await session.prompt("当前目录下有哪些文件?列出前 5 个。");
这三步在做什么:
ModelRuntime.create()初始化模型运行时,读取你的 API Key 配置和环境变量createAgentSession()创建一个 Agent 会话,SessionManager.inMemory()表示不持久化session.prompt()发送消息并等待 Agent 处理完成
如果你之前已经在 pi 里配好了 API Key(通过 /login 或环境变量),ModelRuntime.create() 会自动读到。
接收流式输出
上面的例子调用 session.prompt() 后会阻塞等待,直到 Agent 完成全部工作才返回。这在简单的脚本里够用,但如果你想让用户实时看到 Agent 在说什么、在想什么,就得订阅事件流:
session.subscribe((event) => {
if (event.type === "message_update") {
const e = event.assistantMessageEvent;
if (e.type === "text_delta") {
process.stdout.write(e.delta);
}
if (e.type === "thinking_delta") {
// 思考内容,如果你想展示的话
process.stderr.write(`[思考] ${e.delta}`);
}
}
});
subscribe() 的回调会收到各种事件——跟 RPC 模式的事件流完全对应,只是你现在拿到的是类型化的 TypeScript 对象,不是裸 JSON。
常用事件一览:
| 事件类型 | 含义 | 典型用途 |
|---|---|---|
message_update | 流式输出增量 | 实时显示 AI 回复文字 |
tool_execution_start | 工具开始执行 | 显示”正在执行 bash…” |
tool_execution_end | 工具执行完成 | 显示执行结果 |
agent_start / agent_end | Agent 生命周期 | 进度指示 |
compaction_start / compaction_end | 上下文压缩 | 提示用户”处理中…” |
每个 message_update 的 assistantMessageEvent 可能是 text_delta(回复文字)、thinking_delta(思考过程)、toolcall_delta(工具调用参数)等。组装起来就是完整的 Agent 输出。
指定模型和工具
创建会话时可以指定模型和工具集合:
import { getModel } from "@earendil-works/pi-ai";
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
// 按 provider + model id 查找模型
const model = getModel("anthropic", "claude-sonnet-4-20250514");
if (!model) throw new Error("模型没找到,检查 API Key 和网络");
const { session } = await createAgentSession({
model,
thinkingLevel: "medium",
tools: ["read", "bash", "edit", "write", "grep"],
sessionManager: SessionManager.inMemory(),
modelRuntime,
});
tools 数组列出启用的内置工具。默认只有 read、bash、edit、write 四个。想加 grep、find、ls 需要显式列出。设为 ["read", "grep", "find", "ls"] 就是只读模式——能看不能改。
处理 Agent 运行中的交互
Agent 没跑完的时候你可能想插话——比如纠正方向、追加新任务。SDK 提供了 steer() 和 followUp():
// 等 Agent 开始处理后,插一条纠正消息
await session.prompt("帮我重构这个项目");
// ... 看了几秒觉得方向不对 ...
await session.steer("停,不要动 node_modules,只重构 src 目录下的文件");
// Agent 跑完后追加后续任务
await session.followUp("重构完成后跑一遍测试,有失败就修");
steer() 在当前轮工具执行完后插入——Agent 处理完当前的工具调用结果,在下一次调用 LLM 之前看到你的纠正。followUp() 等 Agent 完全空闲后才发。
用 session.prompt() 时也可以传 streamingBehavior 达到同样效果:
await session.prompt("换个方案", { streamingBehavior: "steer" });
完整的自定义 Agent 应用
下面是一个可以直接跑的完整示例。启动后读 stdin 上的每一行作为用户消息,把 Agent 的回复流式输出到 stdout:
// my-pi-app.ts
import { createInterface } from "node:readline";
import { getModel } from "@earendil-works/pi-ai";
import {
createAgentSession,
ModelRuntime,
SessionManager,
} from "@earendil-works/pi-coding-agent";
async function main() {
// 1. 初始化模型运行时
const modelRuntime = await ModelRuntime.create();
const model = getModel("anthropic", "claude-sonnet-4-20250514");
if (!model) {
console.error("模型未找到。请确认 API Key 已配置。");
process.exit(1);
}
// 2. 创建 Agent 会话
const { session } = await createAgentSession({
model,
thinkingLevel: "medium",
tools: ["read", "bash", "edit", "write", "grep", "find", "ls"],
sessionManager: SessionManager.inMemory(),
modelRuntime,
});
// 3. 订阅事件 - 实时输出 Agent 的回复
let agentStarted = false;
session.subscribe((event) => {
switch (event.type) {
case "agent_start":
agentStarted = true;
break;
case "message_update": {
const e = event.assistantMessageEvent;
if (e.type === "text_delta") {
process.stdout.write(e.delta);
}
break;
}
case "tool_execution_start":
console.log(`\n🔧 正在执行: ${event.toolName}`);
break;
case "tool_execution_end":
if (event.isError) {
console.log(`❌ 工具 ${event.toolName} 执行出错`);
}
break;
case "agent_settled":
agentStarted = false;
console.log("\n--- Agent 空闲,可以输入下一条消息 ---");
promptUser();
break;
case "compaction_start":
console.log("\n🔄 正在压缩上下文...");
break;
case "compaction_end":
console.log("✅ 压缩完成");
break;
}
});
// 4. 交互循环 - 读用户输入
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
function promptUser() {
if (agentStarted) return; // Agent 还在忙,不催用户
rl.question("\n> ", async (input) => {
const trimmed = input.trim();
if (trimmed === "exit" || trimmed === "quit") {
console.log("再见!");
rl.close();
process.exit(0);
}
if (!trimmed) {
promptUser();
return;
}
try {
await session.prompt(trimmed);
// prompt() 在 agent_settled 后才 resolve
// 但我们在事件里已经处理了 promptUser()
} catch (err) {
console.error("发送消息出错:", err);
promptUser();
}
});
}
console.log("pi Agent 已就绪。输入消息开始对话,输入 exit 退出。");
promptUser();
}
main().catch((err) => {
console.error("启动失败:", err);
process.exit(1);
});
把这个文件保存为 my-pi-app.ts,用 tsx 跑:
npx tsx my-pi-app.ts
你会看到一个终端对话界面——你在 > 后面输入消息,Agent 流式输出回复,工具执行时显示进度。输入 exit 退出。
控制更多细节
SDK 提供了丰富的配置项。下面几个是日常开发中最常用的。
自定义系统提示词
import { createAgentSession, DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
const loader = new DefaultResourceLoader({
systemPromptOverride: () => "你是一个专注于代码安全的审查助手。",
});
await loader.reload();
const { session } = await createAgentSession({
resourceLoader: loader,
sessionManager: SessionManager.inMemory(),
});
注册自定义工具
import { Type } from "typebox";
import { createAgentSession, defineTool } from "@earendil-works/pi-coding-agent";
const timeTool = defineTool({
name: "current_time",
label: "当前时间",
description: "获取当前系统时间",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: new Date().toISOString() }],
details: {},
}),
});
const { session } = await createAgentSession({
customTools: [timeTool],
tools: ["read", "bash", "current_time"],
sessionManager: SessionManager.inMemory(),
});
current_time 这个名字同时出现在 customTools 数组和 tools 数组里——customTools 注册工具定义,tools 决定它是否启用。
持久化会话
import { SessionManager } from "@earendil-works/pi-coding-agent";
// 新建持久化会话
const { session } = await createAgentSession({
sessionManager: SessionManager.create(process.cwd()),
modelRuntime,
});
console.log(`会话已保存到: ${session.sessionFile}`);
// 下次启动时继续
const { session: resumed } = await createAgentSession({
sessionManager: SessionManager.continueRecent(process.cwd()),
modelRuntime,
});
SessionManager.create() 在项目目录下创建会话文件。continueRecent() 自动找到最近的会话继续对话。
SDK vs RPC:什么时候用哪个
两章讲完,你可能会纠结选哪个。一个简单的判断标准:
| 场景 | 选 SDK | 选 RPC |
|---|---|---|
| 主程序是 Node.js/TypeScript | ✅ 类型安全,代码简洁 | 也可以用,但没必要 |
| 主程序是 Python/Go/Rust 等 | ❌ 没法直接 import | ✅ 唯一选择 |
| 需要进程隔离 | 理论上可以但没意义 | ✅ pi 在独立进程 |
| 需要直接访问 Agent 内部状态 | ✅ session.agent.state 随便读 | 需要通过 get_state 命令 |
| 写 CI 脚本 | 太重了,要装 npm 依赖 | ✅ 一条 pi --mode rpc 搞定 |
| 构建自定义前端 UI | ✅ 直接嵌入 Node 后端 | ✅ Electron 里启动子进程也行 |
| 需要类型安全和 IDE 补全 | ✅ 全 TypeScript 类型 | ❌ 只有 JSON |
Tip如果你想从 Python 或 Go 调 pi,但又想要好一点的开发体验,可以自己写一个 Node.js RPC 中间层——Node 进程用 SDK 创建 Agent,暴露一个 HTTP/WebSocket 接口给其他语言调。pi 官方没有内置 HTTP 模式,但社区有人这么干过。
一步到位:SDK 还能干什么
SDK 能做的事情远不止发消息。它暴露了 pi 的完整能力:
session.agent.state直接读写 Agent 内部状态(消息列表、模型、工具集)session.compact()手动触发上下文压缩session.setModel()/session.setThinkingLevel()运行时切换模型和思考级别session.navigateTree()在会话树中跳转到历史节点session.abort()中止当前 Agent 操作
还用到了 SettingsManager 管理配置、createAgentSessionRuntime() 做会话替换(新建/切换/fork)——这些是构建完整 pi 替代客户端的基础构件。如果你对更深入的用法感兴趣,官方仓库的 examples/sdk/ 目录下有从最小到完整控制的渐进式示例。