Pi Coding Agent 扩展开发完全指南
从零开始掌握 Pi Coding Agent 的扩展系统:事件钩子、自定义工具、UI 组件、状态管理、Skill 技能包,以及 60+ 实战示例索引
Pi Coding Agent 扩展开发完全指南
Pi Coding Agent 的扩展系统是其”自修改性”理念的核心载体。通过 TypeScript 扩展,你可以注册自定义工具、拦截事件流、定制 UI、修改系统提示——几乎可以对 Pi 的每一个环节进行编程控制。本文将全面介绍扩展系统的方方面面。
扩展是什么
扩展是 TypeScript 模块,通过导出一个默认工厂函数来接收 ExtensionAPI 对象。Pi 使用 jiti 进行运行时加载,因此 TypeScript 无需预编译。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// 你的扩展逻辑
}
工厂函数也可以是异步的——Pi 会等待 async 初始化完成后再继续启动:
export default async function (pi: ExtensionAPI) {
const response = await fetch("http://localhost:1234/v1/models");
const payload = await response.json();
pi.registerProvider("local", { /* ... */ });
}
核心能力一览
| 能力 | API | 说明 |
|---|---|---|
| 自定义工具 | pi.registerTool() | LLM 可调用的工具 |
| 事件拦截 | pi.on() | 拦截工具调用、修改消息、控制流程 |
| 自定义命令 | pi.registerCommand() | 注册 /mycommand 命令 |
| 快捷键 | pi.registerShortcut() | 注册键盘快捷键 |
| CLI 标志 | pi.registerFlag() | 注册自定义命令行参数 |
| 提供商注册 | pi.registerProvider() | 注册或覆盖模型提供商 |
| UI 组件 | ctx.ui.* | 状态栏、Widget、对话框、自定义编辑器等 |
| 消息注入 | pi.sendMessage() / pi.sendUserMessage() | 向会话注入消息 |
快速开始
创建 ~/.pi/agent/extensions/my-extension.ts:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// 1. 监听会话启动事件
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("扩展已加载!", "info");
});
// 2. 拦截危险命令
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: "用户拒绝" };
}
});
// 3. 注册自定义工具
pi.registerTool({
name: "greet",
label: "打招呼",
description: "按名字向某人问好",
parameters: Type.Object({
name: Type.String({ description: "名字" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: "text", text: `你好, ${params.name}!` }],
details: {},
};
},
});
// 4. 注册命令
pi.registerCommand("hello", {
description: "打个招呼",
handler: async (args, ctx) => {
ctx.ui.notify(`你好 ${args || "世界"}!`, "info");
},
});
}
测试运行:
# 方式一:命令行直接加载
pi -e ./my-extension.ts
# 方式二:放到自动发现目录(推荐,支持热重载)
cp my-extension.ts ~/.pi/agent/extensions/
扩展文件组织
| 方式 | 目录结构 | 适用场景 |
|---|---|---|
| 单文件 | extensions/my-ext.ts | 简单扩展 |
| 目录 | extensions/my-ext/index.ts | 多文件扩展 |
| 带依赖 | extensions/my-ext/package.json + src/index.ts | 需要 npm 包 |
扩展存放位置
| 位置 | 作用域 |
|---|---|
~/.pi/agent/extensions/*.ts | 全局 |
~/.pi/agent/extensions/*/index.ts | 全局(子目录) |
.pi/extensions/*.ts | 项目级 |
.pi/extensions/*/index.ts | 项目级(子目录) |
也可以在 settings.json 中指定额外路径:
{
"extensions": ["/path/to/local/extension.ts"]
}
可用导入
| 包 | 用途 |
|---|---|
@earendil-works/pi-coding-agent | 扩展类型、事件、工具函数 |
typebox | 工具参数的 Schema 定义 |
@earendil-works/pi-ai | StringEnum(Google API 兼容枚举) |
@earendil-works/pi-tui | TUI 组件 |
node:fs, node:path 等 | Node.js 内置模块 |
事件系统
事件系统是扩展的核心机制。通过 pi.on(event, handler) 订阅事件,可以对 Pi 的每一步操作进行拦截、修改或响应。
生命周期概览
graph TD
Start["pi 启动"]
Start --> SS["session_start { reason: startup }"]
Start --> RD["resources_discover { reason: startup }"]
Prompt["用户发送 prompt"]
Prompt --> Input["input (可拦截/转换/完全处理)"]
Input --> BAS["before_agent_start (可注入消息/修改系统提示)"]
BAS --> AS["agent_start"]
subgraph Turn["turn 循环 (直到 LLM 不再调用工具)"]
TS["turn_start"] --> Ctx["context (可修改发送给 LLM 的消息)"]
Ctx --> BPR["before_provider_request"]
BPR --> LLM["LLM 响应,可能调用工具"]
LLM --> TC["tool_call (可阻止/修改参数)"]
TC --> TR["tool_result (可修改结果)"]
TR --> TE["turn_end"]
end
AS --> TS
TE -->|"继续下一轮"| TS
TE -->|"LLM 不再调用工具"| AE["agent_end"]
AE -. "用户再次发送 prompt" .-> Prompt
会话事件
session_start
会话启动、加载或重载时触发:
pi.on("session_start", async (event, ctx) => {
// event.reason: "startup" | "reload" | "new" | "resume" | "fork"
ctx.ui.notify(`会话已加载 (${event.reason})`, "info");
});
session_shutdown
会话关闭前触发,用于清理工作:
pi.on("session_shutdown", async (event, ctx) => {
// event.reason: "quit" | "reload" | "new" | "resume" | "fork"
connection?.close();
});
session_before_switch / session_before_fork
会话切换或分叉前触发,可取消:
pi.on("session_before_switch", async (event, ctx) => {
if (event.reason === "new") {
const ok = await ctx.ui.confirm("确认?", "清除所有消息?");
if (!ok) return { cancel: true };
}
});
Agent 事件
before_agent_start
每次用户提交 prompt 后、agent 循环开始前触发。可注入消息和修改系统提示:
pi.on("before_agent_start", async (event, ctx) => {
return {
// 注入持久化消息(存储在会话中,发送给 LLM)
message: {
customType: "my-extension",
content: "额外的上下文信息",
display: true,
},
// 修改系统提示(跨扩展链式修改)
systemPrompt: event.systemPrompt + "\n\n额外的指令...",
};
});
agent_start / agent_end
每次用户 prompt 对应一个 agent_start / agent_end 对:
pi.on("agent_end", async (event, ctx) => {
// event.messages - 本次 prompt 产生的所有消息
});
工具事件
tool_call
工具执行前触发。可阻止执行、可修改参数:
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
pi.on("tool_call", async (event, ctx) => {
// 类型安全的参数访问
if (isToolCallEventType("bash", event)) {
// event.input.command 是类型安全的
if (event.input.command.includes("rm -rf")) {
return { block: true, reason: "危险命令被阻止" };
}
// 修改参数(原地修改)
event.input.command = `source ~/.profile\n${event.input.command}`;
}
});
关键行为保证:
- 对
event.input的修改会影响实际工具执行 - 后续
tool_call处理器能看到前面处理器的修改 - 不会在修改后重新校验
tool_result
工具执行完成后触发。可修改结果,支持链式中间件模式:
pi.on("tool_result", async (event, ctx) => {
// 可以对结果做后处理
return { content: [...], details: {...}, isError: false };
});
上下文事件
context
每次 LLM 调用前触发,可以非破坏性地修改发送给 LLM 的消息:
pi.on("context", async (event, ctx) => {
const filtered = event.messages.filter(m => !shouldPrune(m));
return { messages: filtered };
});
输入事件
input
用户输入到达时触发(在扩展命令检查之后、技能/模板展开之前):
pi.on("input", async (event, ctx) => {
// 转换输入
if (event.text.startsWith("?quick "))
return { action: "transform", text: `简短回答: ${event.text.slice(7)}` };
// 完全处理(不经过 LLM)
if (event.text === "ping") {
ctx.ui.notify("pong", "info");
return { action: "handled" };
}
return { action: "continue" }; // 默认:传递给后续处理
});
处理结果:
continue— 原样传递(默认)transform— 修改文本/图片后继续handled— 跳过 agent 处理(第一个返回此值的处理器获胜)
模型事件
model_select
模型切换时触发(/model、Ctrl+P、会话恢复):
pi.on("model_select", async (event, ctx) => {
// event.model, event.previousModel, event.source ("set" | "cycle" | "restore")
});
thinking_level_select
思考级别变化时触发(仅通知,返回值被忽略):
pi.on("thinking_level_select", async (event, ctx) => {
ctx.ui.setStatus("thinking", `思考级别: ${event.level}`);
});
自定义工具
自定义工具是扩展最强大的能力之一。通过 pi.registerTool() 注册的工具会出现在系统提示中,LLM 可以像调用内置工具一样调用它们。
完整工具定义
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
pi.registerTool({
name: "my_tool",
label: "我的工具",
description: "工具描述(LLM 可见)",
promptSnippet: "一句话描述工具功能", // 出现在系统提示的 Available tools 中
promptGuidelines: [ // 工具级指引
"当用户需要 X 时使用 my_tool 而不是直接编辑文件"
],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const), // 必须用 StringEnum!
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
// 可选:在 schema 校验前转换参数(用于向后兼容)
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// 检查取消
if (signal?.aborted) {
return { content: [{ type: "text", text: "已取消" }] };
}
// 流式进度更新
onUpdate?.({
content: [{ type: "text", text: "处理中..." }],
details: { progress: 50 },
});
// 执行操作
const result = await pi.exec("some-cmd", [], { signal });
return {
content: [{ type: "text", text: "完成" }], // 发送给 LLM
details: { data: result }, // 用于 UI 渲染和状态持久化
terminate: true, // 可选:提示跳过后续 LLM 调用
};
},
// 可选:自定义渲染
renderCall(args, theme, context) { /* ... */ },
renderResult(result, options, theme, context) { /* ... */ },
});
重要注意事项
1. 使用 StringEnum 而非 Type.Union
// ✅ 正确 — 兼容所有 provider(包括 Google)
action: StringEnum(["list", "add"] as const)
// ❌ 错误 — Google API 不支持
action: Type.Union([Type.Literal("list"), Type.Literal("add")])
2. 错误处理:抛出异常而非返回值
// ✅ 正确:抛出异常标记为错误
async execute(toolCallId, params) {
if (!isValid(params.input)) {
throw new Error(`无效输入: ${params.input}`);
}
return { content: [{ type: "text", text: "OK" }], details: {} };
}
3. 输出截断(必须!)
工具输出超过 50KB / 2000 行会导致上下文溢出:
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
const output = await runCommand();
const truncation = truncateHead(output, {
maxLines: DEFAULT_MAX_LINES, // 2000
maxBytes: DEFAULT_MAX_BYTES, // 50KB
});
let result = truncation.content;
if (truncation.truncated) {
result += `\n\n[输出已截断,完整内容保存在: ${tempFile}]`;
}
4. 文件变更安全:withFileMutationQueue
当工具修改文件时,使用 withFileMutationQueue() 避免并行工具的竞态条件:
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const absolutePath = resolve(ctx.cwd, params.path);
return withFileMutationQueue(absolutePath, async () => {
const current = await readFile(absolutePath, "utf8");
const next = current.replace(params.oldText, params.newText);
await writeFile(absolutePath, next, "utf8");
return {
content: [{ type: "text", text: `已更新 ${params.path}` }],
details: {},
};
});
}
覆盖内置工具
注册同名工具即可覆盖内置的 read、bash、edit、write、grep、find、ls:
pi.registerTool({
name: "read", // 同名覆盖
label: "Read",
description: "带日志记录的文件读取",
parameters: Type.Object({ path: Type.String() }),
async execute(toolCallId, params, signal, onUpdate, ctx) {
console.log(`[读取] ${params.path}`);
// 你的自定义实现...
return { content: [{ type: "text", text: "..." }], details: {} };
},
});
渲染是按槽位继承的——如果覆盖时省略了 renderCall,内置的渲染器仍然生效。
远程执行
内置工具支持可插拔的操作接口,可以委托给远程系统:
import { createReadTool, createBashTool } from "@earendil-works/pi-coding-agent";
const remoteRead = createReadTool(cwd, {
operations: {
readFile: (path) => sshExec(remote, `cat ${path}`),
access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
}
});
Bash 工具还支持 spawn hook,可以在执行前调整命令、工作目录和环境变量:
const bashTool = createBashTool(cwd, {
spawnHook: ({ command, cwd, env }) => ({
command: `source ~/.profile\n${command}`,
cwd: `/mnt/sandbox${cwd}`,
env: { ...env, CI: "1" },
}),
});
自定义渲染
工具可以提供 renderCall 和 renderResult 来自定义在终端中的显示效果:
import { Text } from "@earendil-works/pi-tui";
pi.registerTool({
name: "my_tool",
// ...
renderCall(args, theme, context) {
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
let content = theme.fg("toolTitle", theme.bold("my_tool "));
content += theme.fg("muted", args.action);
text.setText(content);
return text;
},
renderResult(result, { expanded, isPartial }, theme, context) {
if (isPartial) {
return new Text(theme.fg("warning", "处理中..."), 0, 0);
}
let text = theme.fg("success", "✓ 完成");
if (expanded && result.details?.items) {
for (const item of result.details.items) {
text += "\n " + theme.fg("dim", item);
}
}
return new Text(text, 0, 0);
},
});
自定义 UI
扩展可以通过 ctx.ui 提供的方法与用户交互,并自定义消息/工具的渲染方式。
对话框
// 选择
const choice = await ctx.ui.select("选择一项:", ["A", "B", "C"]);
// 确认
const ok = await ctx.ui.confirm("删除?", "此操作不可撤销");
// 文本输入
const name = await ctx.ui.input("名字:", "默认值");
// 多行编辑
const text = await ctx.ui.editor("编辑内容:", "预填文本");
// 通知(非阻塞)
ctx.ui.notify("操作完成!", "info"); // "info" | "warning" | "error"
带倒计时的对话框
// 5 秒后自动取消
const confirmed = await ctx.ui.confirm(
"限时确认",
"此对话框将在 5 秒后自动取消。确认吗?",
{ timeout: 5000 }
);
if (confirmed) {
// 用户确认
} else {
// 用户取消或超时
}
状态栏与 Widget
// 底部状态栏(持续显示直到清除)
ctx.ui.setStatus("my-ext", "处理中...");
ctx.ui.setStatus("my-ext", undefined); // 清除
// 工作指示器(流式输出时显示)
ctx.ui.setWorkingIndicator({
frames: [
ctx.ui.theme.fg("dim", "·"),
ctx.ui.theme.fg("muted", "•"),
ctx.ui.theme.fg("accent", "●"),
ctx.ui.theme.fg("muted", "•"),
],
intervalMs: 120,
});
// 编辑器上方 Widget
ctx.ui.setWidget("my-widget", ["状态行 1", "状态行 2"]);
// 编辑器下方 Widget
ctx.ui.setWidget("my-widget", ["行 1", "行 2"], { placement: "belowEditor" });
// 清除 Widget
ctx.ui.setWidget("my-widget", undefined);
自定义 Footer
完全替换内置的底部状态栏:
ctx.ui.setFooter((tui, theme) => ({
render(width) {
return [theme.fg("dim", `自定义 Footer | 宽度: ${width}`)];
},
invalidate() {},
}));
// 恢复内置 Footer
ctx.ui.setFooter(undefined);
自定义编辑器
可以用自定义实现替换主输入编辑器(例如实现 Vim 模式):
import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
class VimEditor extends CustomEditor {
private mode: "normal" | "insert" = "insert";
handleInput(data: string): void {
if (this.mode === "insert" && data === "escape") {
this.mode = "normal";
return;
}
if (this.mode === "normal" && data === "i") {
this.mode = "insert";
return;
}
super.handleInput(data); // 保留 app 快捷键
}
}
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
new VimEditor(theme, keybindings)
);
});
}
也可以包装已有的自定义编辑器:
const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, {
base: previous?.(tui, theme, keybindings)
})
);
自定义组件
对于复杂的 UI 交互,使用 ctx.ui.custom() 临时替换编辑器:
import { Text } from "@earendil-works/pi-tui";
const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
const text = new Text("按 Enter 确认,Escape 取消", 1, 1);
text.onKey = (key) => {
if (key === "return") done(true);
if (key === "escape") done(false);
return true;
};
return text;
});
还支持 Overlay 覆盖层模式(浮在现有内容之上,不清屏):
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{ overlay: true }
);
自定义消息渲染
注册自定义渲染器来控制特定 customType 消息的显示:
pi.registerMessageRenderer("my-extension", (message, options, theme) => {
const { expanded } = options;
let text = theme.fg("accent", `[${message.customType}] `);
text += message.content;
if (expanded && message.details) {
text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
}
return new Text(text, 0, 0);
});
自动补全
可以在内置的斜杠命令和路径补全之上叠加自定义补全逻辑:
ctx.ui.addAutocompleteProvider((current) => ({
async getSuggestions(lines, cursorLine, cursorCol, options) {
const line = lines[cursorLine] ?? "";
const beforeCursor = line.slice(0, cursorCol);
// 匹配 #1234 格式的 GitHub issue
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, cursorLine, cursorCol, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [
{ value: "#2983", label: "#2983", description: "扩展 API 示例" },
{ value: "#2753", label: "#2753", description: "重载资源配置" },
],
};
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
},
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
},
}));
主题颜色
所有渲染函数都会收到 theme 对象:
// 前景色
theme.fg("toolTitle", text) // 工具名称
theme.fg("accent", text) // 高亮
theme.fg("success", text) // 成功(绿色)
theme.fg("error", text) // 错误(红色)
theme.fg("warning", text) // 警告(黄色)
theme.fg("muted", text) // 次要文本
theme.fg("dim", text) // 三级文本
// 文本样式
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)
代码语法高亮:
import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";
const lang = getLanguageFromPath("/path/to/file.rs"); // "rust"
const highlighted = highlightCode(code, lang, theme);
ExtensionAPI 速查
核心方法
| 方法 | 用途 |
|---|---|
pi.on(event, handler) | 订阅事件 |
pi.registerTool(def) | 注册自定义工具 |
pi.registerCommand(name, opts) | 注册 /命令 |
pi.registerShortcut(key, opts) | 注册快捷键 |
pi.registerFlag(name, opts) | 注册 CLI 标志 |
pi.registerProvider(name, config) | 注册/覆盖模型提供商 |
pi.unregisterProvider(name) | 移除提供商 |
消息与状态
| 方法 | 用途 |
|---|---|
pi.sendMessage(msg, opts) | 注入自定义消息到会话 |
pi.sendUserMessage(content, opts) | 发送用户消息 |
pi.appendEntry(type, data) | 持久化扩展状态(不发给 LLM) |
pi.setSessionName(name) | 设置会话名称 |
运行时控制
| 方法 | 用途 |
|---|---|
pi.exec(cmd, args, opts) | 执行 shell 命令 |
pi.getActiveTools() | 获取当前活跃工具列表 |
pi.getAllTools() | 获取所有可用工具 |
pi.setActiveTools(names) | 设置活跃工具 |
pi.setModel(model) | 切换模型 |
pi.getThinkingLevel() | 获取思考级别 |
pi.setThinkingLevel(level) | 设置思考级别 |
pi.events | 扩展间事件总线 |
消息投递模式
sendMessage 和 sendUserMessage 支持 deliverAs 选项:
| 模式 | 行为 |
|---|---|
"steer" | 当前 turn 结束后立即投递(默认) |
"followUp" | 等 agent 完全结束后投递 |
"nextTurn" | 排队等待下一次用户 prompt |
ExtensionContext
所有事件处理器都会收到 ctx: ExtensionContext,提供运行时上下文:
| 属性/方法 | 说明 |
|---|---|
ctx.ui | UI 交互方法 |
ctx.hasUI | 是否有 UI(打印/JSON 模式下为 false) |
ctx.cwd | 当前工作目录 |
ctx.sessionManager | 只读的会话状态访问 |
ctx.modelRegistry / ctx.model | 模型信息 |
ctx.signal | 当前 agent 的 AbortSignal |
ctx.isIdle() | agent 是否空闲 |
ctx.abort() | 中断当前操作 |
ctx.getContextUsage() | 获取上下文使用情况 |
ctx.compact() | 触发上下文压缩 |
ctx.getSystemPrompt() | 获取当前系统提示 |
ctx.shutdown() | 请求优雅退出 |
命令专有方法
命令处理器额外拥有 ExtensionCommandContext:
// 等待 agent 空闲
await ctx.waitForIdle();
// 创建新会话
await ctx.newSession({
parentSession,
setup: async (sm) => { /* 初始化新会话 */ },
withSession: async (ctx) => { /* 在新会话中工作 */ },
});
// 从特定节点分叉
await ctx.fork("entry-id-123", { position: "before" });
// 导航到树中其他节点
await ctx.navigateTree("entry-id-456", { summarize: true });
// 切换到其他会话
await ctx.switchSession("/path/to/session.jsonl");
// 重载运行时
await ctx.reload();
状态管理
扩展的状态应存储在工具结果的 details 中,以支持会话分支:
export default function (pi: ExtensionAPI) {
let items: string[] = [];
// 从会话恢复状态
pi.on("session_start", async (_event, ctx) => {
items = [];
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type === "message" && entry.message.role === "toolResult") {
if (entry.message.toolName === "my_tool") {
items = entry.message.details?.items ?? [];
}
}
}
});
pi.registerTool({
name: "my_tool",
async execute(toolCallId, params, signal, onUpdate, ctx) {
items.push("新项目");
return {
content: [{ type: "text", text: "已添加" }],
details: { items: [...items] }, // 持久化到会话
};
},
});
}
也可以使用 pi.appendEntry() 存储不参与 LLM 上下文的状态:
// 存储
pi.appendEntry("my-state", { count: 42 });
// 恢复
pi.on("session_start", async (_event, ctx) => {
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type === "custom" && entry.customType === "my-state") {
// 从 entry.data 重建状态
}
}
});
自定义提供商
扩展可以动态注册或覆盖模型提供商:
pi.registerProvider("my-proxy", {
name: "My Proxy",
baseUrl: "https://proxy.example.com",
apiKey: "$PROXY_API_KEY", // 环境变量引用
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet (proxy)",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384,
}
]
});
还可以覆盖已有提供商的 baseUrl,或注册带 OAuth 支持的提供商。
Skill 技能系统
Skill 是 Markdown 格式的按需能力包,遵循 Agent Skills 标准。与扩展不同,Skill 不需要编写代码——它是给 LLM 的指令文档。
Skill 存放位置
| 位置 | 作用域 |
|---|---|
~/.pi/agent/skills/ | 全局 |
~/.agents/skills/ | 全局 |
.pi/skills/ | 项目级 |
.agents/skills/(cwd 及父目录) | 项目级 |
Skill 结构
my-skill/
├── SKILL.md # 必须:前置元数据 + 指令
├── scripts/ # 辅助脚本(可选)
└── references/ # 详细文档(可选)
SKILL.md 格式
---
name: my-skill
description: 这个技能做什么以及何时使用。要具体描述。
---
# 我的技能
## 环境准备
首次使用前运行:
\`\`\`bash
cd /path/to/skill && npm install
\`\`\`
## 使用方法
\`\`\`bash
./scripts/process.sh <input>
\`\`\`
前置元数据字段
| 字段 | 必填 | 说明 |
|---|---|---|
name | ✅ | 1-64字符,小写字母+数字+连字符 |
description | ✅ | 最多1024字符,描述用途和触发条件 |
license | ❌ | 许可证 |
compatibility | ❌ | 环境要求 |
disable-model-invocation | ❌ | true 时需手动 /skill:name 加载 |
渐进披露
Skill 的关键设计是渐进披露:
- 只有
description常驻系统提示上下文 - 完整的 SKILL.md 内容按需加载(Agent 用
read工具读取) - 这减少了 token 开销
使用 Skill
# 命令行触发
/skill:my-skill
# 带参数
/skill:my-skill 参数1 参数2
# Agent 也会根据描述自动判断何时加载
通过 settings.json 配置 Skill 路径
{
"skills": ["~/.claude/skills", "/path/to/custom-skill"]
}
思考级别
Pi 支持 6 个思考级别,控制模型在回答前的”深度思考”程度:
| 级别 | 说明 |
|---|---|
off | 关闭思考 |
minimal | 最少思考 |
low | 低度思考 |
medium | 中度思考 |
high | 高度思考 |
xhigh | 超高度思考 |
配置方式
交互模式:按 Shift+Tab 循环切换
命令行:
pi --thinking high "解决这个复杂问题"
pi --model sonnet:high "复杂任务" # 模型+思考级别简写
配置文件:
{
"defaultThinkingLevel": "high",
"thinkingBudgets": {
"minimal": 1024,
"low": 4096,
"medium": 10240,
"high": 32768
}
}
通过扩展:
pi.setThinkingLevel("high");
非推理模型(如 GPT-4o)不支持思考,始终为
off。
设置系统
Pi 使用 JSON 配置文件,项目级覆盖全局:
| 位置 | 作用域 |
|---|---|
~/.pi/agent/settings.json | 全局 |
.pi/settings.json | 项目级(覆盖全局,嵌套对象合并) |
关键配置项
{
"defaultProvider": "anthropic",
"defaultModel": "claude-sonnet-4-20250514",
"defaultThinkingLevel": "medium",
"theme": "dark",
"packages": ["pi-skills"],
"extensions": ["/path/to/extension.ts"],
"skills": ["~/.claude/skills"],
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 20000
},
"enabledModels": ["claude-*", "gpt-4o"]
}
60+ 实战示例索引
Pi 内置了丰富的示例扩展,覆盖几乎所有使用场景。
工具类
| 示例 | 说明 | 关键 API |
|---|---|---|
hello.ts | 最简工具注册 | registerTool |
question.ts | 带用户交互的工具 | registerTool, ui.select |
questionnaire.ts | 多步向导工具 | registerTool, ui.custom |
todo.ts | 有状态工具 + 持久化 | registerTool, appendEntry, renderResult |
dynamic-tools.ts | 运行时动态注册工具 | registerTool, session_start |
structured-output.ts | 终止型工具 | registerTool, terminate: true |
truncated-tool.ts | 输出截断 | registerTool, truncateHead |
tool-override.ts | 覆盖内置工具 | registerTool(同名) |
ssh.ts | SSH 远程执行 | registerFlag, 工具操作 |
subagent/ | 子代理 | registerTool, exec |
命令与 UI 类
| 示例 | 说明 | 关键 API |
|---|---|---|
pirate.ts | 修改系统提示 | registerCommand, before_agent_start |
summarize.ts | 对话摘要 | registerCommand, ui.custom |
handoff.ts | 跨 provider 交接 | registerCommand, ctx.newSession |
qna.ts | Q&A 交互 | registerCommand, ui.custom, setEditorText |
custom-footer.ts | 自定义 Footer | registerCommand, setFooter |
custom-header.ts | 自定义头部 | session_start, setHeader |
modal-editor.ts | Vim 模态编辑器 | setEditorComponent, CustomEditor |
widget-placement.ts | Widget 放置 | setWidget |
overlay-test.ts | Overlay 组件 | ui.custom, overlay options |
github-issue-autocomplete.ts | GitHub Issue 补全 | addAutocompleteProvider |
事件与安全类
| 示例 | 说明 | 关键 API |
|---|---|---|
permission-gate.ts | 阻止危险命令 | on("tool_call"), ui.confirm |
protected-paths.ts | 保护文件路径 | on("tool_call") |
confirm-destructive.ts | 确认破坏性操作 | on("session_before_*") |
dirty-repo-guard.ts | Git 脏仓库警告 | on("session_before_*"), exec |
input-transform.ts | 输入转换 | on("input") |
model-status.ts | 模型切换状态 | on("model_select"), setStatus |
Git 集成类
| 示例 | 说明 | 关键 API |
|---|---|---|
git-checkpoint.ts | 每轮 git stash | on("turn_start"), exec |
auto-commit-on-exit.ts | 退出时自动提交 | on("session_shutdown"), exec |
git-merge-and-resolve.ts | 合并并解决冲突 | on("agent_end"), sendUserMessage |
复杂扩展
| 示例 | 说明 | 关键 API |
|---|---|---|
plan-mode/ | 完整 Plan Mode 实现 | 全部事件类型、命令、快捷键、标志 |
preset.ts | 可保存的预设 | setModel, setActiveTools, setThinkingLevel |
snake.ts | 贪吃蛇游戏 | ui.custom, 键盘处理 |
space-invaders.ts | 太空侵略者 | ui.custom |
doom-overlay/ | Doom 覆盖层 | ui.custom, overlay |
sandbox/ | 沙箱执行 | 工具操作 |
custom-provider-anthropic/ | 自定义 Anthropic 代理 | registerProvider |
custom-provider-gitlab-duo/ | GitLab Duo 集成 | registerProvider, OAuth |
常见模式
模式一:权限控制
在工具执行前弹出确认对话框:
pi.on("tool_call", async (event, ctx) => {
if (isToolCallEventType("bash", event)) {
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: "用户拒绝" };
}
}
});
模式二:系统提示增强
每轮动态修改系统提示:
pi.on("before_agent_start", async (event, ctx) => {
const branch = ctx.sessionManager.getBranch();
const turnCount = branch.filter(e => e.type === "message" && e.message?.role === "user").length;
return {
systemPrompt: event.systemPrompt + `\n\n当前对话轮次: ${turnCount}`,
};
});
模式三:工具动态切换
根据条件启用或禁用工具:
pi.on("agent_start", async (_event, ctx) => {
const usage = ctx.getContextUsage();
if (usage && usage.tokens > 80_000) {
// 上下文快满了,只保留必要工具
pi.setActiveTools(["read", "bash", "edit"]);
}
});
模式四:自动提交
Agent 结束时自动 git commit:
pi.on("agent_end", async (event, ctx) => {
const status = await pi.exec("git", ["status", "--porcelain"]);
if (status.stdout.trim()) {
await pi.exec("git", ["add", "-A"]);
const lastMsg = event.messages.findLast(m => m.role === "assistant");
const msg = lastMsg?.content?.[0]?.text?.slice(0, 72) || "auto: agent changes";
await pi.exec("git", ["commit", "-m", msg]);
ctx.ui.notify("已自动提交更改", "info");
}
});
错误处理
| 场景 | 处理方式 |
|---|---|
| 扩展错误 | 记录日志,agent 继续 |
tool_call 错误 | 阻止工具执行(fail-safe) |
工具 execute 错误 | 必须用 throw 信号化;错误被捕获后以 isError: true 报告给 LLM |
非交互模式行为
| 模式 | UI 方法 | 说明 |
|---|---|---|
| 交互模式 | 完整 TUI | 正常操作 |
RPC (--mode rpc) | JSON 协议 | 客户端处理 UI |
JSON (--mode json) | 无操作 | 事件流输出到 stdout |
打印 (-p) | 无操作 | 扩展运行但无法弹窗 |
在非交互模式下,使用 ctx.hasUI 检查是否有 UI 可用。
总结
Pi 的扩展系统遵循其”自修改性”核心理念——Pi 不内置的功能(Plan Mode、权限控制、子代理、MCP、Todo……),都可以通过扩展来实现。20+ 个生命周期钩子、完整的 UI 组件系统、按需加载的 Skill 机制,加上 TypeScript 无编译热重载的开发体验,让 Pi 成为一个真正”可编程”的编码 Agent。
正如 Pi 作者 Mario Zechner 所说:
“如果我不需要它,它就不会被构建。”
但这并不意味着你不能拥有它——只需要一个 TypeScript 文件,就能让 Pi 变成你想要的任何样子。