🔴 开发者章节 | 扩展是 Pi 留出来的"可编程接缝"——TypeScript 文件,可以监听事件、注册工具 / 命令 / Provider、修改 UI、拦截工具调用。比 Pi 内置的工具集更广,能做内置功能刻意没做的事。
一句话
Pi 的扩展是 TypeScript 文件,可以监听事件、注册自定义工具、注册命令、控制 UI、管理状态,是 Pi 最强的定制能力。
扩展能做什么
✅ Pi 原生支持:
扩展位置(自动发现)
1 2 3 4
| ~/.pi/agent/extensions/*.ts # 全局 ~/.pi/agent/extensions/*/index.ts # 全局(目录形式) .pi/extensions/*.ts # 项目本地(需信任) .pi/extensions/*/index.ts # 项目本地(目录形式)
|
项目本地扩展只有在项目被信任后才会加载。
通过 settings.json 加载额外路径
1 2 3 4 5 6 7 8 9 10
| { "packages": [ "npm:@foo/bar@1.0.0", "git:github.com/user/repo@v1" ], "extensions": [ "/path/to/local/extension.ts", "/path/to/local/extension/dir" ] }
|
临时测试用 -e
建议:正式扩展放到自动发现目录,才能 /reload 热重载。
写第一个扩展
创建 ~/.pi/agent/extensions/hello.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox";
export default function (pi: ExtensionAPI) { pi.on("session_start", async (_event, ctx) => { ctx.ui.notify("Hello Extension loaded!", "info"); });
pi.on("tool_call", async (event, ctx) => { if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) { const ok = await ctx.ui.confirm("危险操作", "允许执行 rm -rf 吗?"); if (!ok) return { block: true, reason: "Blocked by user" }; } });
pi.registerTool({ name: "greet", label: "Greet", description: "Greet someone by name", parameters: Type.Object({ name: Type.String({ description: "Name to greet" }), }), async execute(toolCallId, params) { return { content: [{ type: "text", text: `Hello, ${params.name}!` }], details: {}, }; }, });
pi.registerCommand("hello", { description: "Say hello", handler: async (args, ctx) => { ctx.ui.notify(`Hello ${args || "world"}!`, "info"); }, }); }
|
启动 Pi,输入 /hello,看效果。
事件系统(Events)
Pi 扩展的核心是事件。常用事件分组:
graph LR
subgraph 会话
session_start --> session_before_compact
session_before_compact --> session_compact
end
subgraph Agent
before_agent_start --> agent_start
agent_start --> agent_end
agent_end --> agent_settled
end
subgraph 工具
tool_execution_start --> tool_call
tool_call --> tool_execution_end
tool_execution_end --> tool_result
end
subgraph 输入
input --> model_select
model_select --> before_provider_request
before_provider_request --> after_provider_response
end常用事件分组:
启动与会话
Agent 生命周期
工具事件
输入与模型
想改写"发给模型的整段上下文"?Pi 没有单独叫 context 的事件。对应需求通常落到 before_agent_start(在 Agent 循环开始前修改初始消息)或 message_update/message_end(在流式生成过程中干预)。改写工具入参最直接的钩子是 tool_call,文档明确说 event.input 上的修改会被 Pi 直接应用到执行阶段,不会再校验一次。
用户 Bash
扩展示例
示例 1:拦截危险命令
1 2 3 4 5 6 7 8 9 10 11
| export default function (pi: ExtensionAPI) { pi.on("tool_call", async (event, ctx) => { if (event.toolName === "bash") { const cmd = event.input.command || ""; if (cmd.includes("rm -rf") || cmd.includes("sudo")) { const ok = await ctx.ui.confirm("危险命令", `执行:${cmd}?`); if (!ok) return { block: true, reason: "用户拒绝" }; } } }); }
|
示例 2:会话开始时显示项目信息
1 2 3 4 5 6
| export default function (pi: ExtensionAPI) { pi.on("session_start", async (_event, ctx) => { const file = ctx.sessionManager.getSessionFile(); ctx.ui.setStatus("my-ext", `会话:${file ?? "临时"}`); }); }
|
示例 3:注册一个"待办事项"工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { Type } from "typebox";
export default function (pi: ExtensionAPI) { let todos: string[] = [];
pi.registerTool({ name: "todo", label: "Todo", description: "Manage todo list", parameters: Type.Object({ action: Type.Union([Type.Literal("add"), Type.Literal("list")]), text: Type.Optional(Type.String()), }), async execute(toolCallId, params) { if (params.action === "add" && params.text) { todos.push(params.text); } return { content: [{ type: "text", text: JSON.stringify(todos) }], details: { todos: [...todos] }, }; }, }); }
|
常用 ExtensionAPI 方法
ExtensionContext(ctx)
所有事件处理器都收到 ctx:
错误处理
- 扩展抛错:会被记录,Agent 继续运行
tool_call 抛错:工具被拦截(fail-safe)- 工具执行报错:
throw new Error(...),会被标记 isError: true 并报告给 LLM
调试技巧
1 2 3 4 5 6 7 8
| pi --verbose
pi -e ./my-extension.ts
/reload
|
常见错误
你现在应该会什么
到这里你应该能:
- 解释 Pi 的事件生命周期(启动 / 会话 / Agent 循环 / 工具 / 输入),并指出每个阶段可以挂哪些钩子
- 写一个最小扩展:监听
session_start + 拦截 tool_call + 注册 pi.registerTool - 用
ctx.ui.* 在 TUI 里弹出通知 / 选择 / 确认 - 用
pi --verbose 与 /reload 调试扩展加载问题
下一步