pi-coding-agent既是交互式 CLI,也是 TypeScript SDK。它在 pi-agent-core 的基础上加入了内置工具、会话持久化、上下文压缩、资源发现,以及扩展运行时2。- 用 SDK 时,一个
createAgentSession()调用就能得到一个和 CLI 使用同一套机制的 agent 会话23。 - 会话是一棵树:每条记录是 JSONL 中的一行,通过
id和parentId连接,在同一个文件里就能分支4。 - 扩展是 pi 的灵魂:扩展是一个 TypeScript 模块,可以注册工具、命令、提供方、MCP server、事件处理器、UI。拦截危险命令、实现 plan mode、实现 subagent,都靠扩展15。
1. 定位#
- 分层位置:L4 Harness,在你的 Node.js 或 Bun 进程内部运行。这一点不同于 Claude Agent SDK,后者会启动一个 CLI 子进程。
- 与 CLI 的关系:交互模式、print 模式、JSON 模式、RPC 模式和 SDK,用的都是同一套 agent 和会话机制3。
- 如果需要跨语言集成或进程隔离,可以改用 RPC 模式:启动一个子进程,通过 stdin 和 stdout 交换 JSONL6。
createAgentSession() 在不传参数时,会自动创建 ModelRuntime、基于文件的 SettingsManager、持久化的 SessionManager、DefaultResourceLoader,以及配置好的默认工具。每一项都可以由你显式替换2。
2. SDK 快速上手#
import { createAgentSession } from "@earendil-works/pi-coding-agent";
// 默认行为:使用当前工作目录,自动发现资源、读取已保存的设置和凭据const { session } = await createAgentSession();
try { await session.prompt("当前目录下有哪些文件?"); // prompt() 会在这次运行结束后返回 console.log(session.getLastAssistantText());} finally { session.dispose(); // 中止进行中的工作,并清理扩展上下文和事件监听器}这是官方 SDK 文档开头的例子2。
2.1 更常见的嵌入方式:内存会话、只读工具、流式输出#
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
const { session } = await createAgentSession({ cwd: process.cwd(), tools: ["read", "grep", "find", "ls"], // 只读模式:不启用 edit、write、bash sessionManager: SessionManager.inMemory(), // 不写会话文件});
const unsubscribe = session.subscribe((event) => { if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { process.stdout.write(event.assistantMessageEvent.delta); // 流式输出文本 }});
try { await session.prompt("阅读 README,然后用三句话介绍这个项目。"); // 运行进行中可以这样插话: // session.steer("..."):在当前这轮工具执行完之后插入 // session.followUp("..."):在这次运行结束后再追加一个任务} finally { unsubscribe(); session.dispose();}改编自官方的 SDK 文档和 examples/sdk/05-tools.ts27。
| 要点 | 说明 |
|---|---|
运行中再次 prompt() | 必须说明是 steer 还是 followUp,否则会被拒绝,不会替你猜2 |
agent_end 和 agent_settled 的区别 | agent_end 只表示一次底层运行结束,之后可能还有自动重试或排队的工作;agent_settled 才表示 pi 不会再自动继续2 |
| 会话的权威数据 | 最终发给模型的上下文以 SessionManager 为准。直接给 session.agent.state.messages 赋值,并不会替换持久化的上下文2 |
| 内置扩展 | CLI 会把 codemode、tool_search、MCP 作为内置扩展加载,SDK 会话不会,需要手动加入 extensionFactories2 |
2.2 在 SDK 中启用 MCP 和 codemode#
import { createAgentSession, createCodemodeExtension, createMcpExtension, createToolSearchExtension, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager,} from "@earendil-works/pi-coding-agent";
const cwd = process.cwd();const resourceLoader = new DefaultResourceLoader({ cwd, agentDir: getAgentDir(), extensionFactories: [ createCodemodeExtension({ mode: "on" }), createToolSearchExtension(), createMcpExtension(), // 和 CLI 一样,从 agent 目录和受信任的项目中读取 mcp.json ],});await resourceLoader.reload();
const settingsManager = SettingsManager.create(cwd);settingsManager.applyOverrides({ defaultTools: ["+codemode", "+tool_search"] }); // +name 表示在默认工具的基础上追加
const { session } = await createAgentSession({ resourceLoader, settingsManager, sessionManager: SessionManager.inMemory() });try { await session.bindExtensions({}); // 触发 session_start 事件,MCP server 会在后台连接 console.log("当前可用的工具:", session.getActiveToolNames().join(", "));} finally { session.dispose();}改编自 examples/sdk/14-codemode-mcp.ts7。
codemode 工具让模型编写一段 JavaScript 脚本来调用 pi 的其他工具。只有脚本的输出会进入模型的上下文,因此脚本可以并行调用工具,或者先过滤掉大量结果再返回。脚本运行在 QuickJS 沙箱中,不能访问 Node API、文件系统、网络和定时器,只能通过注入的工具与外界交互8。
它和 OpenAI 的 Programmatic Tool Calling、DSH 的 PTC 模式是同一种思路。
3. 会话:一棵 JSONL 树#
这棵树的结构来自官方 Session File Format 文档4:
- 存储位置:
~/.pi/agent/sessions/--<路径>--/<时间戳>_<会话ID>.jsonl。第一行是会话头,之后每行是一条记录,记录之间通过id和parentId构成一棵树。 - 叶子是当前所在的位置。从某条较早的记录继续,就会在同一个文件里长出一条新分支;fork 或 clone 则会把选中的历史复制到一个新文件中。
- 构建上下文时,从当前叶子一路走到根节点,再结合压缩记录,得出模型可见的消息列表。被压缩的原始记录仍然保留在树中。
- 记录类型:message、model_change、thinking_level_change、compaction、context_edit、branch_summary、custom、custom_message、label、session_info 等。
- 版本:v1 是线性结构;v2 改为树结构;v3 把
hookMessage角色改名为custom。加载旧会话时会自动迁移。
它本质上是一个事件溯源的数据结构:只追加,从不修改;当前状态等于从根节点重放到叶子。分支、回滚、审计都变得很自然。DSH 的事件日志也是同样的思路,见 5.3 DSH 核心机制与插件开发。
4. 扩展系统#
4.1 扩展是什么#
- 扩展是导出一个默认工厂函数的 TypeScript 模块,工厂函数接收
ExtensionAPI(通常命名为pi),并通过它注册各种能力1。 - 扩展的存放位置有三种:
~/.pi/agent/extensions/(用户级)、<项目>/.pi/extensions/(项目级),以及settings.json中声明的路径。pi 借助 jiti 加载扩展,所以不需要单独编译 TypeScript1。 - 扩展在 pi 进程内运行,拥有相同的权限,所以只加载可信来源的扩展1。
| 能力 | API |
|---|---|
| 观察或修改生命周期 | pi.on(event, handler) |
| 添加模型可以调用的工具 | pi.registerTool() |
添加 / 斜杠命令 | pi.registerCommand() |
| 添加快捷键或 CLI 参数 | pi.registerShortcut() / pi.registerFlag() |
| 发送用户消息或自定义消息 | pi.sendUserMessage() / pi.sendMessage() |
| 持久化不进入上下文的数据 | pi.appendEntry() |
| 添加模型提供方或 MCP server | pi.registerProvider() / pi.registerMcpServer() |
| 按请求路由到不同模型 | pi.registerVirtualModel() |
| 和其他扩展通信 | pi.events |
以上取自官方文档1。
4.2 示例:一个最小的安全扩展#
下面是官方示例 permission-gate.ts 的完整代码,它在执行危险的 bash 命令前要求用户确认:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) { const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
pi.on("tool_call", async (event, ctx) => { if (event.toolName !== "bash") return undefined;
const command = event.input.command as string; const isDangerous = dangerousPatterns.some((p) => p.test(command));
if (isDangerous) { if (!ctx.hasUI) { // 非交互模式下默认拦截 return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" }; }
const choice = await ctx.ui.select(`⚠️ Dangerous command:\n\n ${command}\n\nAllow?`, ["Yes", "No"]);
if (choice !== "Yes") { return { block: true, reason: "Blocked by user" }; } }
return undefined; });}源码见 examples/extensions/permission-gate.ts5。把它保存到 ~/.pi/agent/extensions/ 目录,或者用 pi --extension ./permission-gate.ts 加载。
4.3 示例:在 SDK 中用内联扩展注册工具#
import { Type } from "@earendil-works/pi-ai";import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager,} from "@earendil-works/pi-coding-agent";
const resourceLoader = new DefaultResourceLoader({ cwd: process.cwd(), agentDir: getAgentDir(), extensionFactories: [ (pi) => { // 1) 注册一个工具,模型可以直接调用 pi.registerTool({ name: "get_weather", label: "查询天气", description: "查询城市今天的天气", parameters: Type.Object({ city: Type.String({ description: "城市名" }) }), execute: async (_toolCallId, params) => ({ content: [{ type: "text", text: params.city === "北京" ? "晴,25°C" : "暂无数据" }], details: { city: params.city }, // details 用于界面渲染或状态重建,不会发给模型 }), }); // 2) 审计日志:观察每一次工具调用。返回 { block: true, reason } 可以拦截 pi.on("tool_call", async (event) => { console.log(`[audit] ${event.toolName}`); return undefined; }); // 3) 注册一个斜杠命令 pi.registerCommand("weather-help", { description: "显示天气工具的用法", handler: async (_args, ctx) => ctx.ui.notify("直接问:北京今天天气怎么样?", "info"), }); }, ],});await resourceLoader.reload();
const { session } = await createAgentSession({ resourceLoader, sessionManager: SessionManager.inMemory() });try { await session.prompt("北京今天适合跑步吗?"); console.log(session.getLastAssistantText());} finally { session.dispose();}改编自 examples/sdk/06-extensions.ts7。
4.4 关键事件与约定#
| 事件 | 能做什么 |
|---|---|
before_agent_start | 查看或修改提示词的各个 section 和选中的工具,也可以强制替换整个提示词 |
tool_call | 修改输入,或者拦截这次执行 |
tool_result | 依次改写工具结果,每个处理器都能看到前一个处理器的修改 |
context / context_with_system | 在发出请求前改写本次请求的对话消息 |
turn_end / agent_before_settle | 可以追加记录,并请求再进行一次模型请求。要做好条件判断,避免无限循环 |
provider_stream_event | 只读地观察提供方的原始流事件 |
以上取自官方文档1。
工具的**暴露模式(exposure)**决定了模型如何接触到一个工具1:
| 模式 | 含义 |
|---|---|
direct(默认) | 声明给模型,可以直接调用 |
model-only | 只声明给模型,其他工具不能通过 ctx.executeTool() 调用它 |
codemode | 只能被 codemode 脚本调用 |
deferred | 由 tool_search 按需激活 |
hidden | 已注册但不可达 |
5. Skills、Prompt Templates 与 Packages#
| 资源 | 是什么 | 怎么加载 |
|---|---|---|
| Skills | 一个包含 SKILL.md 的目录,可以附带脚本、参考资料和素材文件。pi 实现了 Agent Skills specification | 启动时只向模型列出每个 skill 的名称和描述,真正需要时才加载完整的说明9 |
| Prompt templates | 可复用的提示词文本 | 在用户输入变成消息之前展开3 |
| Themes | 终端的配色方案 | — |
| Pi packages | 把扩展、skills、模板、主题打包在一起,通过 npm 或 git 分发 | pi install npm:@example/pi-tools@1.0.0,或者 pi install git:github.com/example/pi-tools@v110 |
项目级的包和设置只有在用户授予项目信任之后才会加载。包可以执行扩展代码,也可以通过 skill 指示模型运行程序,因此安装第三方包之前要先审查它的源码10。
6. RPC 模式:跨语言集成#
pi --mode rpc --no-session- 协议有四类记录:stdin 上的命令;stdout 上的
response;stdout 上的会话事件;双向传递的扩展 UI 记录6。 - 每条命令可以带一个可选的
id,对应的响应会原样带回这个id,用来关联请求和响应6。 - TypeScript 中可以直接使用导出的
RpcClient,它负责启动 pi 进程、关联响应,并提供类型化的命令方法6。
| 接口 | 进程边界 | 适合什么 |
|---|---|---|
| SDK | 同一进程 | Node.js 或 Bun 应用,需要完整的 API |
| RPC | 子进程 | 其他语言、需要进程隔离、IDE、自定义客户端 |
以上取自官方文档6。
7. 与 Claude Agent SDK 的对比#
| 维度 | pi-coding-agent SDK | 3.3 Claude Agent SDK |
|---|---|---|
| 进程模型 | 同一进程(Node 或 Bun);RPC 是可选项 | 启动 CLI 子进程,通过 stdio 通信 |
| 默认工具 | 4 个:read、bash、edit、write | 一整套:Read、Edit、Write、Bash、Glob、Grep、Web 等 |
| 权限 | 没有内置;用扩展或容器实现 | 多层权限判定、权限模式、can_use_tool |
| 会话 | 树状 JSONL,同一个文件里就能分支 | JSONL,支持 resume、fork |
| 扩展方式 | 扩展(TypeScript 模块)、skills、packages | hooks、subagents、plugins、skills、MCP |
| 模型 | 数十家,经由 pi-ai | Claude |
| 设计取向 | 最小核心,由你构建 | 能力完整,开箱即用 |
这张表是笔者根据两篇笔记归纳的。
小结#
- pi-coding-agent 等于 pi-agent-core 加上工具、树状会话、压缩、资源发现、扩展运行时。SDK、CLI 和 RPC 共享同一套机制。
- 它真正的力量在于扩展系统:pi 不打算预设所有需求,而是给你一套足够强的钩子,让你把 pi 改造成自己的 agent。
- 对后端工程师来说,它的树状 JSONL 会话、进程边界的选择(SDK 还是 RPC),以及供应链加固实践都很值得借鉴。
相关笔记#
- 4.1 pi 全景与设计哲学 · 4.2 pi-ai 统一多模型 API · 4.3 pi-agent-core 最小 Agent 运行时
- 对照:3.3 Claude Agent SDK · 5.3 DSH 核心机制与插件开发
参考资料#
注释与出处#
-
earendil-works/pi,
packages/coding-agent/docs/extensions.md,https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/docs/extensions.md ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
earendil-works/pi,
packages/coding-agent/docs/sdk.md(commit6fb2e78),https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/docs/sdk.md ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
earendil-works/pi,
packages/coding-agent/docs/how-pi-works.md,https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/docs/how-pi-works.md ↩ ↩2 ↩3 -
earendil-works/pi,
packages/coding-agent/docs/session-format.md,https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/docs/session-format.md ↩ ↩2 -
earendil-works/pi,
packages/coding-agent/examples/extensions/(含permission-gate.ts),https://github.com/earendil-works/pi/tree/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/examples/extensions ↩ ↩2 ↩3 -
earendil-works/pi,
packages/coding-agent/docs/rpc.md,https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/docs/rpc.md ↩ ↩2 ↩3 ↩4 ↩5 -
earendil-works/pi,
packages/coding-agent/examples/sdk/(01-minimal、05-tools、06-extensions、14-codemode-mcp 等),https://github.com/earendil-works/pi/tree/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/examples/sdk ↩ ↩2 ↩3 -
earendil-works/pi,
packages/coding-agent/docs/codemode.md,https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/docs/codemode.md ↩ -
earendil-works/pi,
packages/coding-agent/docs/skills.md,https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/docs/skills.md ↩ -
earendil-works/pi,
packages/coding-agent/docs/packages.md,https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/coding-agent/docs/packages.md ↩ ↩2