graph TD
A[pi 启动] --> B[project_trust]
B --> C[session_start]
C --> D[resources_discover]
D --> E[用户发送消息]
E --> F{是扩展命令?}
F -->|是| G[执行扩展命令]
F -->|否| H[input 事件]
H --> I[before_agent_start]
I --> J[agent_start]
J --> K[turn 循环]
K --> L[turn_start]
L --> M[context 事件]
M --> N[provider 请求/响应]
N --> O{LLM 调用工具?}
O -->|是| P[tool_execution_start]
P --> Q[tool_call 事件]
Q --> R[tool_result 事件]
R --> S[tool_execution_end]
S --> K
O -->|否| T[turn_end]
T --> U[agent_end]
U --> V[agent_settled]
13 扩展开发
13.1 扩展系统概述
扩展(Extension)是 Pi 最强大的定制机制。扩展是 TypeScript 模块,可以订阅生命周期事件、注册供 LLM 调用的自定义工具、添加斜杠命令、渲染自定义 UI 组件,几乎能覆盖 Pi 的每一个行为层面。
Pi 的核心设计理念是”保持内核极小,通过扩展实现无限可能”。这意味着你在日常使用中遇到的大部分功能,都可以通过扩展来定制或增强。
13.1.1 核心能力一览
| 能力 | 说明 |
|---|---|
| 自定义工具 | 通过 pi.registerTool() 注册 LLM 可调用的工具 |
| 事件拦截 | 拦截或修改工具调用、注入上下文、自定义压缩 |
| 用户交互 | 通过 ctx.ui(select、confirm、input、notify)与用户交互 |
| 自定义 UI | 通过 ctx.ui.custom() 实现完整的 TUI 组件 |
| 自定义命令 | 通过 pi.registerCommand() 注册 /mycommand 等命令 |
| 会话持久化 | 通过 pi.appendEntry() 存储跨重启的状态 |
| 自定义渲染 | 控制工具调用结果和消息在 TUI 中的显示方式 |
13.2 快速开始
创建文件 ~/.pi/agent/extensions/my-extension.ts:
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("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("Dangerous!", "Allow 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, signal, onUpdate, ctx) {
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");
},
});
}使用 -e 标志测试:
pi -e ./my-extension.ts让 Pi 帮你创建扩展!直接在对话中说”帮我创建一个扩展,实现 XXX 功能”,Pi 会自动生成代码。
13.3 扩展位置
扩展按以下优先级自动发现:
| 位置 | 作用域 | 说明 |
|---|---|---|
~/.pi/agent/extensions/ |
全局 | 所有项目共享,自动发现 |
.pi/extensions/ |
项目级 | 仅当前项目,需项目受信任后加载 |
-e ./path.ts |
CLI | 仅本次运行,适合快速测试 |
安全提示:扩展拥有完整的系统权限,可以执行任意代码。只安装来自可信来源的扩展。
放在自动发现位置的扩展可以通过 /reload 命令热重载,无需重启 Pi。
13.4 可用导入
扩展模块可以通过相对路径或 bare import 引入依赖:
// 从 Pi 核心包导入类型
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
// 从 typebox 导入(用于参数 schema 定义)
import { Type } from "typebox";
// 从 Pi TUI 包导入组件
import { Text, Box, Container } from "@earendil-works/pi-tui";
// 从 Pi AI 包导入
import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";如果扩展需要第三方依赖,在扩展目录创建 package.json:
{
"name": "my-extension",
"dependencies": {
"zod": "^3.0.0",
"chalk": "^5.0.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}运行 npm install 后即可在扩展中导入 node_modules/ 中的包。
13.5 编写扩展
13.5.1 默认导出函数
最基本的形式——导出一个接收 ExtensionAPI 的函数:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// 注册事件监听器、工具、命令等
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Hello from my extension!", "info");
});
}13.5.2 异步工厂函数
当需要在启动时执行异步操作(如从远程 API 获取配置)时,导出 async 函数:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default async function (pi: ExtensionAPI) {
// 启动时拉取配置
const config = await fetch("https://api.example.com/config").then(r => r.json());
// 根据配置注册不同的工具
if (config.enableSearch) {
pi.registerTool({
name: "search",
// ...
});
}
}Pi 会等待异步工厂函数完成后再继续启动流程,因此在此注册的提供者和工具在交互式启动时就可使用。
13.5.3 长生命周期资源与关闭
对于需要清理的扩展(如文件监听、数据库连接、WebSocket),在 session_shutdown 事件中释放资源:
export default function (pi: ExtensionAPI) {
let watcher = startFileWatcher();
pi.on("session_shutdown", async () => {
watcher.close();
watcher = null;
});
pi.on("session_start", async () => {
if (!watcher) watcher = startFileWatcher();
});
}13.6 扩展风格
根据复杂度,扩展可以采用不同的组织方式:
- 单文件扩展(简单场景):所有逻辑在一个
.ts文件中 - 多文件扩展(中等复杂度):主文件导入辅助模块
- 包形式扩展(复杂场景):完整的 npm 包,包含
package.json和依赖管理
13.7 事件系统
13.7.1 生命周期概览
13.7.2 启动事件
13.7.2.1 project_trust
在决定是否信任项目之前触发。仅全局扩展和 CLI 扩展参与:
pi.on("project_trust", async (event, ctx) => {
// event.cwd - 当前工作目录
if (await ctx.ui.confirm("Trust project?", event.cwd)) {
return { trusted: "yes", remember: true };
}
return { trusted: "undecided" };
});13.7.2.2 session_start
会话启动、加载或重载时触发:
pi.on("session_start", async (event, ctx) => {
// event.reason: "startup" | "reload" | "new" | "resume" | "fork"
// event.previousSessionFile: 切换前的会话文件路径
ctx.ui.notify(`Session started: ${event.reason}`, "info");
});13.7.2.3 resources_discover
会话启动后触发,允许扩展贡献额外的 skill、prompt 和 theme 路径:
pi.on("resources_discover", async (event) => {
return {
skillPaths: ["/path/to/skills"],
promptPaths: ["/path/to/prompts"],
themePaths: ["/path/to/themes"],
};
});13.7.3 会话事件
| 事件 | 触发时机 |
|---|---|
session_start |
会话启动/加载/重载 |
session_info_changed |
会话名称变更 |
session_before_switch |
新建/切换会话之前 |
session_before_fork |
分支/克隆之前 |
session_before_compact |
压缩之前 |
session_compact |
压缩执行 |
session_before_tree |
树状导航之前 |
session_tree |
树状导航执行 |
session_shutdown |
会话关闭/清理 |
13.7.4 Agent 事件
| 事件 | 触发时机 |
|---|---|
before_agent_start |
Agent 开始处理之前(可注入消息或修改系统提示) |
agent_start |
Agent 开始处理 |
agent_end |
Agent 处理完成 |
agent_settled |
无更多重试/压缩/跟进时 |
13.7.5 模型事件
| 事件 | 触发时机 |
|---|---|
context |
每轮对话前(可修改消息列表) |
before_provider_headers |
发送请求前(可修改 headers) |
before_provider_request |
发送请求前(可检查或替换 payload) |
after_provider_response |
收到响应后(状态码和 headers) |
model_select |
模型切换时 |
thinking_level_select |
思考级别变更时 |
13.7.6 工具事件
| 事件 | 触发时机 |
|---|---|
tool_execution_start |
工具开始执行 |
tool_call |
工具被调用(可阻止) |
tool_execution_update |
工具执行中的流式更新 |
tool_result |
工具返回结果(可修改) |
tool_execution_end |
工具执行结束 |
13.7.7 用户 Bash 事件
| 事件 | 触发时机 |
|---|---|
bash_input |
用户在 bash 模式下输入命令 |
bash_execution_update |
bash 命令执行中的流式输出 |
bash_end |
bash 命令执行结束 |
13.7.8 输入事件
13.7.8.1 input
在用户输入处理之前触发,可以拦截、转换或完全处理输入:
pi.on("input", async (event, ctx) => {
// event.text - 用户输入的原始文本
// 返回 { handled: true } 可阻止默认处理
if (event.text === "/secret") {
ctx.ui.notify("Secret command!", "info");
return { handled: true };
}
// 返回 { text: "modified" } 可转换输入
});13.8 ExtensionContext API
每个事件处理函数接收一个 ExtensionContext(简称 ctx),提供与 Pi 交互的能力:
13.8.1 UI 操作
// 通知
ctx.ui.notify("Message", "info"); // "info" | "warning" | "error"
// 确认对话框
const ok = await ctx.ui.confirm("Title", "Message");
// 文本输入
const text = await ctx.ui.input("Enter name:");
// 选择列表
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
// 自定义 TUI 组件
const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
return new MyComponent({ onClose: done });
});13.8.2 运行时信息
| 属性 | 说明 |
|---|---|
ctx.mode |
当前运行模式 |
ctx.hasUI |
是否有 TUI 界面 |
ctx.cwd |
当前工作目录 |
ctx.isProjectTrusted() |
项目是否已受信任 |
ctx.sessionManager |
会话管理器实例 |
ctx.model |
当前使用的模型 |
ctx.thinkingLevel |
当前思考级别 |
ctx.signal |
AbortSignal,用于取消操作 |
13.8.3 会话控制
// 检查是否空闲
ctx.isIdle();
// 中止当前操作
ctx.abort();
// 获取上下文使用量
ctx.getContextUsage();
// 手动触发压缩
ctx.compact();
// 获取系统提示
ctx.getSystemPrompt();13.9 ExtensionAPI 方法
13.9.1 事件注册
pi.on("event_name", async (event, ctx) => {
// 处理事件
});13.9.2 工具注册
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "Does something useful",
parameters: Type.Object({
input: Type.String({ description: "Input parameter" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: "text", text: "Result" }],
details: {},
};
},
});13.9.3 消息发送
// 发送消息(触发 LLM 响应)
pi.sendMessage("Let's continue working");
// 以用户身份发送消息
pi.sendUserMessage("Please fix the bug in main.ts");13.9.4 命令注册
pi.registerCommand("deploy", {
description: "Deploy the current project",
handler: async (args, ctx) => {
ctx.ui.notify(`Deploying ${args}...`, "info");
// 执行部署逻辑
},
});13.9.5 模型控制
// 切换模型
pi.setModel({ provider: "anthropic", id: "claude-sonnet-4-5" });
// 设置思考级别
pi.setThinkingLevel("high");
// 获取活跃工具列表
const tools = pi.getActiveTools();
// 设置活跃工具
pi.setActiveTools(["read", "write", "bash", "my_tool"]);13.9.6 提供者注册
// 注册自定义提供者
pi.registerProvider("my-provider", {
baseUrl: "https://api.example.com/v1",
apiKey: "$MY_API_KEY",
api: "openai-completions",
models: [
{ id: "my-model", name: "My Model" },
],
});
// 注销提供者
pi.unregisterProvider("my-provider");13.10 状态管理
扩展可以在会话中存储持久化状态。通过 pi.appendEntry() 创建自定义条目,状态会随会话文件保存:
pi.on("tool_execution_end", async (event, ctx) => {
if (event.toolName === "bash") {
// 记录每次 bash 执行
pi.appendEntry("bash_log", {
timestamp: Date.now(),
command: event.args?.command,
success: !event.isError,
});
}
});13.11 自定义工具
13.11.1 工具定义
完整的工具定义包含以下字段:
pi.registerTool({
name: "fetch_data",
label: "Fetch Data",
description: "Fetch data from a URL",
parameters: Type.Object({
url: Type.String({ description: "URL to fetch" }),
method: Type.Optional(Type.Union([
Type.Literal("GET"),
Type.Literal("POST"),
])),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
// 流式更新
onUpdate({ type: "text", text: "Fetching..." });
const response = await fetch(params.url, { signal });
const text = await response.text();
return {
content: [{ type: "text", text }],
details: { status: response.status },
};
},
});13.11.2 覆盖内置工具
通过注册同名工具,可以覆盖内置工具的行为:
pi.registerTool({
name: "bash", // 覆盖内置 bash 工具
// ...自定义实现
});13.11.3 远程执行
工具可以在远程服务器上执行命令:
async execute(toolCallId, params, signal, onUpdate, ctx) {
// 通过 SSH 在远程服务器执行
const result = await sshExec(params.host, params.command);
return {
content: [{ type: "text", text: result.stdout }],
details: { exitCode: result.exitCode },
};
}13.11.4 输出截断
对于大量输出,使用 onUpdate 进行流式传输:
async execute(toolCallId, params, signal, onUpdate, ctx) {
const stream = await fetchLargeData(params.url);
for await (const chunk of stream) {
if (signal.aborted) break;
onUpdate({ type: "text", text: chunk });
}
return { content: [{ type: "text", text: "Done" }], details: {} };
}13.11.5 动态工具加载
根据上下文动态注册/注销工具:
pi.on("session_start", async () => {
const projectType = await detectProjectType();
if (projectType === "react") {
pi.registerTool({ name: "component_gen", /* ... */ });
} else if (projectType === "python") {
pi.registerTool({ name: "test_runner", /* ... */ });
}
});13.12 自定义 UI
13.12.1 对话框
const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
return new MyDialog({
theme,
keybindings,
onSelect: (value) => done(value),
onCancel: () => done(null),
});
});13.12.2 组件与 Widget
通过 ctx.ui.custom() 在编辑器上方或下方渲染组件:
pi.on("session_start", async (_event, ctx) => {
// 显示一个状态 widget
ctx.ui.custom((tui) => {
const widget = new StatusBar();
const interval = setInterval(() => {
widget.update();
tui.requestRender();
}, 1000);
return widget;
});
});13.12.3 自动完成
注册自动完成提供者,为用户输入提供建议:
pi.registerAutocompleteProvider(async (input, ctx) => {
if (input.startsWith("/deploy")) {
return ["production", "staging", "development"];
}
return [];
});13.12.4 消息渲染
自定义消息和条目的渲染方式:
pi.registerMessageRenderer("custom_type", (message, theme) => {
return [`✨ ${message.data.title} ✨`];
});
pi.registerEntryRenderer("bash_log", (entry, theme) => {
return [`📝 ${entry.data.command} → ${entry.data.success ? "✓" : "✗"}`];
});13.12.5 Markdown 变换
在 Markdown 渲染前对其进行变换:
pi.registerMarkdownTransformer((text) => {
// 将 TODO: 替换为 emoji
return text.replace(/TODO:/g, "📋");
});13.12.6 主题颜色
在扩展中访问当前主题的颜色:
pi.on("session_start", async (_event, ctx) => {
// 通过 theme 对象获取颜色
const accentColor = ctx.theme?.colors.accent;
});13.13 错误处理
扩展中的错误会被 Pi 捕获并显示给用户,不会导致崩溃:
pi.registerTool({
name: "risky_tool",
async execute(toolCallId, params, signal, onUpdate, ctx) {
try {
const result = await riskyOperation();
return { content: [{ type: "text", text: result }], details: {} };
} catch (error) {
// 返回错误信息给 LLM
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
details: { error: true },
isError: true,
};
}
},
});13.14 模式行为
扩展在不同运行模式下行为不同:
| 模式 | ctx.hasUI |
UI 方法 | 说明 |
|---|---|---|---|
| Interactive | true |
全部可用 | 完整 TUI 支持 |
false |
notify 可用 |
无交互式 UI | |
| RPC | false |
通过 RPC 协议 | 取决于客户端 |
| JSON | false |
不可用 | 纯事件流 |
在编写需要 UI 交互的扩展时,始终检查 ctx.hasUI,确保在非交互模式下也能正常工作。
13.15 完整示例
以下是一个完整的扩展示例,结合了事件监听、自定义工具和命令:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// 状态
let todoList: string[] = [];
// 会话启动时加载状态
pi.on("session_start", async () => {
pi.sendMessage(`Loaded with ${todoList.length} pending items`);
});
// 注册 TODO 工具
pi.registerTool({
name: "todo_add",
label: "Add Todo",
description: "Add an item to the todo list",
parameters: Type.Object({
item: Type.String({ description: "Todo item text" }),
}),
async execute(_id, params) {
todoList.push(params.item);
return {
content: [{ type: "text", text: `Added: ${params.item}. Total: ${todoList.length}` }],
details: {},
};
},
});
pi.registerTool({
name: "todo_list",
label: "List Todos",
description: "List all todo items",
parameters: Type.Object({}),
async execute() {
const text = todoList.length === 0
? "No items"
: todoList.map((t, i) => `${i + 1}. ${t}`).join("\n");
return { content: [{ type: "text", text }], details: {} };
},
});
// 注册命令
pi.registerCommand("todo", {
description: "Show todo list",
handler: async (_args, ctx) => {
ctx.ui.notify(`${todoList.length} items in todo list`, "info");
},
});
// 会话关闭时保存
pi.on("session_shutdown", async () => {
pi.appendEntry("todo_state", { items: todoList });
});
}更多扩展示例请参考 GitHub 官方示例库。