5575 words
28 minutes
Pi Coding Agent 扩展开发完全指南

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");
},
});
}

测试运行:

Terminal window
# 方式一:命令行直接加载
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-aiStringEnum(Google API 兼容枚举)
@earendil-works/pi-tuiTUI 组件
node:fs, node:pathNode.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: {},
};
});
}

覆盖内置工具#

注册同名工具即可覆盖内置的 readbasheditwritegrepfindls

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" },
}),
});

自定义渲染#

工具可以提供 renderCallrenderResult 来自定义在终端中的显示效果:

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);

完全替换内置的底部状态栏:

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扩展间事件总线

消息投递模式#

sendMessagesendUserMessage 支持 deliverAs 选项:

模式行为
"steer"当前 turn 结束后立即投递(默认)
"followUp"等 agent 完全结束后投递
"nextTurn"排队等待下一次用户 prompt

ExtensionContext#

所有事件处理器都会收到 ctx: ExtensionContext,提供运行时上下文:

属性/方法说明
ctx.uiUI 交互方法
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>
\`\`\`

前置元数据字段#

字段必填说明
name1-64字符,小写字母+数字+连字符
description最多1024字符,描述用途和触发条件
license许可证
compatibility环境要求
disable-model-invocationtrue 时需手动 /skill:name 加载

渐进披露#

Skill 的关键设计是渐进披露

  • 只有 description 常驻系统提示上下文
  • 完整的 SKILL.md 内容按需加载(Agent 用 read 工具读取)
  • 这减少了 token 开销

使用 Skill#

Terminal window
# 命令行触发
/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 循环切换

命令行

Terminal window
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.tsSSH 远程执行registerFlag, 工具操作
subagent/子代理registerTool, exec

命令与 UI 类#

示例说明关键 API
pirate.ts修改系统提示registerCommand, before_agent_start
summarize.ts对话摘要registerCommand, ui.custom
handoff.ts跨 provider 交接registerCommand, ctx.newSession
qna.tsQ&A 交互registerCommand, ui.custom, setEditorText
custom-footer.ts自定义 FooterregisterCommand, setFooter
custom-header.ts自定义头部session_start, setHeader
modal-editor.tsVim 模态编辑器setEditorComponent, CustomEditor
widget-placement.tsWidget 放置setWidget
overlay-test.tsOverlay 组件ui.custom, overlay options
github-issue-autocomplete.tsGitHub 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.tsGit 脏仓库警告on("session_before_*"), exec
input-transform.ts输入转换on("input")
model-status.ts模型切换状态on("model_select"), setStatus

Git 集成类#

示例说明关键 API
git-checkpoint.ts每轮 git stashon("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 变成你想要的任何样子。

Pi Coding Agent 扩展开发完全指南
https://sgjki547.top/posts/pi-coding-agent-extension-guide/
Author
SGJki
Published at
2026-06-04
License
CC BY-NC-SA 4.0