第三章:扩展系统 -- 从"硬编码"到"可插拔"
对应源文件:
packages/coding-agent/src/core/extensions/types.ts
为什么需要扩展系统
假设你想给 pi 添加一个"代码审查"功能。没有扩展系统时,你需要修改 pi 的源代码:改 agent-session、改 system prompt、改 TUI 渲染...
有了扩展系统,你只需要写一个 TypeScript 文件:
// .pi/extensions/code-review.ts
export default function(ctx) {
// 注册工具
ctx.registerTool({
name: "review",
description: "Review code changes",
parameters: Type.Object({ path: Type.String() }),
execute: async (id, { path }) => { ... },
});
// 注册斜杠命令
ctx.registerCommand("/review", {
description: "Start code review",
handler: async (cmdCtx) => { ... },
});
// 监听事件
ctx.on("agent_end", async (event, handlerCtx) => {
// 每次 AI 完成后做点什么
});
}
扩展能做什么
扩展 API(ExtensionContext + ExtensionUIContext)提供了几乎覆盖所有层面的能力:
1. 注册工具
ctx.registerTool({
name: "deploy",
label: "deploy",
description: "Deploy to production",
parameters: deploySchema,
execute: async (id, args, signal) => { ... },
renderCall: (args, theme) => { ... }, // 自定义 TUI 渲染
renderResult: (result, opts, theme) => { ... },
});
2. 注册命令
斜杠命令出现在输入框的 / 菜单中:
ctx.registerCommand("/deploy", {
description: "Deploy the current project",
aliases: ["/d"], // 快捷别名
args: "<env>", // 参数提示
handler: async (cmdCtx, args) => {
const env = args || await cmdCtx.ui.select("Select environment", ["staging", "prod"]);
await doDeploy(env);
},
});
3. 注册快捷键
ctx.registerKeybinding({
key: "ctrl+shift+d",
description: "Quick deploy",
handler: async (cmdCtx) => {
// 同命令 handler
},
});
4. 注册 CLI flags
扩展可以声明自己的 CLI 参数:
ctx.registerFlag({
name: "deploy-env",
description: "Target deployment environment",
type: "string",
});
// 用户可以 pi --deploy-env=staging
// 扩展通过 ctx.flagValues.get("deploy-env") 获取值
5. 事件监听
扩展可以订阅几乎所有事件:
session_start / session_shutdown -- 会话生命周期
before_agent_start / agent_start / agent_end -- agent 循环生命周期
turn_start / turn_end -- 每一轮
message_start / message_update / message_end -- 消息流式传输
tool_execution_start / tool_execution_end -- 工具执行
context -- LLM 调用前的消息列表
model_select -- 模型切换
input -- 用户输入(可拦截/转换)
session_before_compact / session_compact -- 压缩前后
tool_call (read/write/edit/bash/grep/find/ls) -- 内置工具调用
user_bash -- 用户 ! 命令
6. UI 操作
扩展可以通过 ctx.ui 与用户交互:
// 选择对话框
const choice = await ctx.ui.select("选择", ["A", "B", "C"]);
// 确认对话框
const ok = await ctx.ui.confirm("确认?", "真的要执行吗?");
// 文本输入
const name = await ctx.ui.input("输入名称", "默认值");
// 通知
ctx.ui.notify("部署成功!", "info");
// 状态栏
ctx.ui.setStatus("deploy", "Deploying...");
// 自定义组件
const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
return new MyCustomComponent(tui, theme, done);
});
// 自定义 widget
ctx.ui.setWidget("deploy-status", ["Status: deploying..."], { placement: "aboveEditor" });
// 自定义页脚
ctx.ui.setFooter((tui, theme, footerData) => new MyFooter(tui, theme));
// 自定义页眉
ctx.ui.setHeader((tui, theme) => new MyHeader(tui, theme));
// 自定义编辑器
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
7. 会话控制
扩展命令(ExtensionCommandContext)还可以控制会话:
// 创建新会话
await cmdCtx.newSession();
// Fork 会话
await cmdCtx.fork(entryId);
// 切换会话
await cmdCtx.switchSession(sessionPath);
// 导航会话树
await cmdCtx.navigateTree(targetId, { summarize: true });
// 重新加载扩展
await cmdCtx.reload();
扩展生命周期
1. 发现阶段
ResourceLoader 扫描以下位置:
├── .pi/extensions/ (项目级)
├── ~/.pi/agent/extensions/ (全局级)
└── --extension CLI 参数 (显式指定)
2. 加载阶段
import() 加载 TypeScript 模块
执行 default export 函数
注入 ExtensionContext
3. session_start 事件
扩展初始化完成
可以读取历史 entries 恢复状态
4. 运行阶段
事件驱动
工具调用、命令处理
5. session_shutdown 事件
清理资源
保存状态
resources_discover 事件
扩展可以在 resources_discover 事件中注册额外的资源路径:
ctx.on("resources_discover", async (event) => {
return {
skillPaths: ["/path/to/my/skills/"],
promptPaths: ["/path/to/my/prompts/"],
themePaths: ["/path/to/my/themes/"],
};
});
事件钩子机制
before 事件的取消能力
以 session_before_compact 为例,扩展可以阻止压缩:
ctx.on("session_before_compact", async (event) => {
// 自定义压缩逻辑
const result = await myCustomCompaction(event.preparation);
return { cancel: true, result }; // 取消默认压缩,使用自定义结果
});
input 事件的拦截能力
ctx.on("input", async (event) => {
if (event.text.startsWith("@translate")) {
const translated = await translate(event.text.slice(10));
return { action: "transform", text: translated };
}
return { action: "continue" }; // 不干预
});
context 事件的消息修改
ctx.on("context", async (event) => {
// 在发给 LLM 前修改消息列表
event.messages.push({
role: "user",
content: `当前时间:${new Date().toISOString()}`,
timestamp: Date.now(),
});
});
扩展 vs 内置功能的边界
Pi 的设计哲学是最小核心 + 最大扩展。判断一个功能应该内置还是做成扩展的标准:
| 内置 | 扩展 |
|---|---|
| 所有用户都需要(read, write, edit, bash) | 特定场景需要(代码审查、部署) |
| 性能敏感(session 持久化) | 不影响核心性能 |
| 需要深度集成(TUI 渲染循环) | 可以通过 API 完成 |
实际上,很多"核心"功能也是通过扩展机制实现的:
- Sub-agent(子代理调用其他 AI)
- Plan mode(让 AI 先规划再执行)
- 权限弹窗(bash 命令的用户确认)
小结
扩展系统
├── 注册能力
│ ├── registerTool() → LLM 可调用的工具
│ ├── registerCommand() → 斜杠命令
│ ├── registerKeybinding()→ 快捷键
│ └── registerFlag() → CLI 参数
│
├── 事件监听
│ ├── 会话事件 → start / shutdown / compact / tree
│ ├── Agent 事件 → start / end / turn / message / tool
│ └── 输入/模型事件 → input / model_select / context
│
├── UI 操作
│ ├── 对话框 → select / confirm / input / editor
│ ├── 状态栏 → setStatus / setWidget
│ └── 自定义组件 → setHeader / setFooter / setEditorComponent / custom
│
└── 会话控制
├── newSession / fork / switchSession
├── navigateTree
└── compact / reload