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 的每一步操作进行拦截、修改或响应。
生命周期概览
pi 启动 ├─► session_start { reason: "startup" } └─► resources_discover { reason: "startup" }
用户发送 prompt ───────────────────────────────┐ │ │ ├─► input (可拦截/转换/完全处理) │ ├─► before_agent_start (可注入消息/修改系统提示) │ ├─► agent_start │ │ │ │ ┌── turn (循环直到 LLM 不再调用工具) ──┐ │ │ │ │ │ │ │ turn_start │ │ │ │ context (可修改发送给 LLM 的消息) │ │ │ │ before_provider_request │ │ │ │ │ │ │ │ LLM 响应,可能调用工具: │ │ │ │ tool_call (可阻止/修改参数) │ │ │ │ tool_result (可修改结果) │ │ │ │ │ │ │ │ turn_end │ │ │ └──────────────────────────────────────┘ │ │ │ └─► agent_end │ │用户再次发送 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,});
// 编辑器上方 Widgetctx.ui.setWidget("my-widget", ["状态行 1", "状态行 2"]);
// 编辑器下方 Widgetctx.ui.setWidget("my-widget", ["行 1", "行 2"], { placement: "belowEditor" });
// 清除 Widgetctx.ui.setWidget("my-widget", undefined);自定义 Footer
完全替换内置的底部状态栏:
ctx.ui.setFooter((tui, theme) => ({ render(width) { return [theme.fg("dim", `自定义 Footer | 宽度: ${width}`)]; }, invalidate() {},}));
// 恢复内置 Footerctx.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-skilldescription: 这个技能做什么以及何时使用。要具体描述。---
# 我的技能
## 环境准备
首次使用前运行:\`\`\`bashcd /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 变成你想要的任何样子。