
0. 一个生活化的比喻:Pi 的扩展就像"App Store"
如果你把 Pi 本身看作一部"出厂的 iPhone",那 Pi 的扩展系统就是它的 App Store:你不用换手机、不用重写系统,下载/编写一个 .ts 文件扔进 ~/.pi/agent/extensions/ 目录,就能给 AI 增加新能力——比如:
给它一个能"在浏览器里搜东西"的工具(registerTool) 给它一个 /git快捷命令(registerCommand)给编辑器绑定 Ctrl+L触发"清屏"(registerShortcut)在每次回答后自动加个"代码质量评分"(on('message_end')) 把整个页脚换成自己的 logo(setFooter)
这一切,不用改 Pi 的源码,不用重新编译 Pi。它靠的是运行时动态加载 TS 模块 + 事件订阅 + 注册点这三件套。
1. 业务定位:为什么需要扩展机制?
1.1 业务场景
Pi 是一个通用型 coding agent,但每个团队、每个项目、每个人的需求都不同:
/standup 自动生成工作日志 | ||
扩展机制把"Pi 本身做什么"和"用户想让 Pi 做什么"彻底解耦:Pi 核心只负责"通用骨架"(会话、循环、状态),所有"个性化能力"都通过扩展来加。
1.2 同类项目通常怎么做?
同类项目有三种主流做法:
- 硬编码
:所有功能都写死在核心代码里(VS Code 早期) - 配置文件
:通过 YAML/JSON 配置启用/关闭功能(vim) - 运行时扩展
:动态加载代码模块,调用一组注册 API(VS Code 后期、VSCode 扩展、Sublime、JetBrains 插件、Pi)
Pi 选择了第 3 种中最轻量的形式:你只写一个 TS 函数,导出它,Pi 在启动时调一下,这个函数里的所有"我要注册 XXX"就成了 Pi 的一部分。
2. 整体架构:四大组件如何协作
Pi 的扩展机制由 4 个核心模块协作完成。Pi 的整个扩展子系统的位置在 packages/coding-agent/src/core/extensions/。

2.1 四大组件的职责
loader.ts | ||
types.ts | ExtensionAPI 接口(26 种事件 + 5 类注册点 + 10+ 个动作方法) | |
runner.ts | ||
wrapper.ts | ToolDefinition 包装成 AgentTool,加错误隔离和 source info |
3. 一个扩展的一生:从磁盘到生效
假设你写了一个 my-tool.ts 放在 ~/.pi/agent/extensions/:

4. 核心数据流:注册时 vs 运行时
Pi 的扩展机制分两个完全独立的阶段:
4.1 注册阶段(启动时,一次性)

4.2 运行时阶段(每个事件触发一次)

5. 五种注册点:你到底能扩展什么?
Pi 把"扩展能做的事"严格归为 5 类,每类都对应一个 register* 方法。理解这 5 类,就理解了扩展的整个能力边界。
registerTool | |||
registerCommand | /xxx 斜杠命令 | /standup | |
registerShortcut | |||
registerFlag | --my-verbose | ||
registerProvider |
此外还有两类"渲染"扩展:registerMessageRenderer(自定义 CustomMessage 的渲染)和 26 个 on() 事件订阅(覆盖会话/Agent/工具/输入/模型/UI 全生命周期)。
6. 26 个事件订阅点:你能"挂钩"到哪些时刻?
这是 Pi 扩展机制最强大的地方:几乎 Agent 运行的每个关键时刻,你都能"插一脚"。

6.1 三种"力量等级"的事件
这 26 个事件按"能改变什么"分成三个等级:
| 观察型 | |||
| 修改型 | |||
| 拦截型 |
7. 数据结构:一个扩展到底长什么样?
从代码视角,一个 Extension 对象其实就是一堆 Map:

关键类型定义全部在 types.ts:1585-1595:
Extension:已加载的扩展实例,本质是 6 个 Map 组成的注册表 ExtensionRuntime:跨扩展共享的 runtime,保存 flag 值和待应用的 provider 注册队列 ExtensionContext:事件触发时传给 handler 的"上下文对象",含 UI/会话/模型访问 ExtensionAPI:传给 factory 的"注册 API",是扩展与 Pi 交互的唯一通道
8. 加载机制细节:jiti、信任、缓存、冲突
8.1 jiti:为什么能"运行时加载 TS"
Pi 用 jiti 在运行时即时转译并加载 TS 文件。这意味着:
用户扩展不用编译、直接 .ts就行支持热重载( /reload重新走一遍 jiti)支持两套模块解析:在 Node 模式下用 alias,在 Bun 二进制模式下用virtualModules(见 loader.ts:44-66)
8.2 项目信任(Project Trust)
出于安全,Pi 区分两类扩展:
~/.pi/agent/extensions/ | ||
./.pi/extensions/ | 是 |
信任检查在 resource-loader.ts:340-353,通过 project_trust 事件让所有扩展投票决定是否信任。
8.3 缓存与热重载
loader 内部维护了 per-cwd 的 factory 缓存(loader.ts:130-143),/reload 时调用 clearExtensionCache()。注意:runtime 本身跨 reload 复用,但旧 extension 的 invalidate() 会被调用,使得"在旧 ctx 之外的代码访问旧 ctx"会抛错(防 stale ctx 误用)。
8.4 冲突检测
同名 tool/flag 来自不同扩展时,resource-loader.ts:1000-1036 会把冲突记为 diagnostic,但不阻止加载。后注册的会覆盖前注册的,冲突信息在 UI 中显示给用户。
9. 一次完整事件分发:用户在编辑器里按下回车后发生什么

10. 优缺点总结
10.1 优点
| 零编译 | ||
| API 极简 | ExtensionFactory 函数 + 一个 ExtensionAPI 参数 | |
| 事件齐全 | ||
| 类型安全 | ||
| 沙箱友好 | ||
| 作用域隔离 | ||
| 跨扩展通信 | ||
| 可热重载 | /reload |
10.2 缺点
| 无权限模型 | ||
| 无版本约束 | ExtensionAPI 类型可能与 Pi 不同版本不兼容,没有 manifest 声明最低 Pi 版本 | pi.peer 字段 |
| jiti 性能 | ||
| 事件无优先级 | ||
| UI 扩展无声明式 schema | setFootersetWidget 接收"返回 Component 的工厂函数",没有结构化描述 | |
| 错误传播粗 | ||
| 无扩展商店 | pi install <name> |
11. 与同类插件系统对比
总结:Pi 扩展机制在"轻量 + 类型安全 + 覆盖完整生命周期"这三件事上做到了较好的平衡,比 VS Code 轻、比 Sublime 安全、比 LangChain 完整;但在"权限模型 + 插件市场"上不如成熟的 IDE 插件生态。
12. 一图总结

夜雨聆风