乐于分享
好东西不私藏

Agent插件化架构的代表-Pi插件化机制解析

Agent插件化架构的代表-Pi插件化机制解析
作为龙虾(OpenClaw)早期的实现基座, 开源AI Agent项目“pi”,正被越来越多人所提及. 其优雅简洁的设计以及便捷的扩展能力让你可以轻松的基于它进行二次创作而快速地得到自己独有的Agent. 尤其在各路主流Coding Agent内置了各种臃肿上下文的情况下(比如在claude code输入一个hello则动辄携带上万token的提示词), 清爽简洁的pi则看起来别具一格.而从agent设计入门和借鉴的角度, pi也是再好不过的一个参考项目. 本系列主要解析pi核心模块的工作原理.
今天我们终于来分析下Pi Agent最为人所称道的插件机制的实现.

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,但每个团队、每个项目、每个人的需求都不同:

需求方
需要
如果不靠扩展机制
公司内部团队
访问自家 Jira / Confluence
改 Pi 源码、PR、等待发版
开源贡献者
分享一个"读 PDF 工具"给别人用
每个人都要手动 fork Pi
个人用户
每天写 /standup 自动生成工作日志
反复手敲同样的话
前端开发者
把页脚换成自己品牌的 logo
去翻 Pi 源码改 CSS

扩展机制把"Pi 本身做什么"和"用户想让 Pi 做什么"彻底解耦:Pi 核心只负责"通用骨架"(会话、循环、状态),所有"个性化能力"都通过扩展来加。

1.2 同类项目通常怎么做?

同类项目有三种主流做法:

  1. 硬编码
    :所有功能都写死在核心代码里(VS Code 早期)
  2. 配置文件
    :通过 YAML/JSON 配置启用/关闭功能(vim)
  3. 运行时扩展
    :动态加载代码模块,调用一组注册 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
从磁盘找到扩展 → 用 jiti 动态加载 → 调用 factory 函数 → 收集所有注册项
loader.ts
types.ts
定义 ExtensionAPI 接口(26 种事件 + 5 类注册点 + 10+ 个动作方法)
types.ts
runner.ts
事件分发器 + UI/命令上下文注入;把宿主事件"广播"给所有订阅者
runner.ts
wrapper.ts
把扩展注册的 ToolDefinition 包装成 AgentTool,加错误隔离和 source info
wrapper.ts

3. 一个扩展的一生:从磁盘到生效

假设你写了一个 my-tool.ts 放在 ~/.pi/agent/extensions/

4. 核心数据流:注册时 vs 运行时

Pi 的扩展机制分两个完全独立的阶段:

4.1 注册阶段(启动时,一次性)

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

5. 五种注册点:你到底能扩展什么?

Pi 把"扩展能做的事"严格归为 5 类,每类都对应一个 register* 方法。理解这 5 类,就理解了扩展的整个能力边界。

注册方法
目的
生效位置
典型用途
registerTool
给 LLM 加一个可调用的"工具"
LLM 推理循环
"读 PDF"、"查数据库"
registerCommand
加一个 /xxx 斜杠命令
用户输入栏
/standup
 自动生成日志
registerShortcut
绑定键盘快捷键
编辑器 / 全局
Ctrl+L 清屏
registerFlag
加 CLI 参数
命令行启动
--my-verbose
registerProvider
注册自定义 LLM provider
模型选择器
接公司内部代理

此外还有两类"渲染"扩展:registerMessageRenderer(自定义 CustomMessage 的渲染)和 26 个 on() 事件订阅(覆盖会话/Agent/工具/输入/模型/UI 全生命周期)。

6. 26 个事件订阅点:你能"挂钩"到哪些时刻?

这是 Pi 扩展机制最强大的地方:几乎 Agent 运行的每个关键时刻,你都能"插一脚"

6.1 三种"力量等级"的事件

这 26 个事件按"能改变什么"分成三个等级:

等级
能做什么
典型事件
举例
观察型
只读,看看发生了什么
agent_start / turn_start / message_update
在 UI 角落显示"AI 正在思考"
修改型
改一改内容或参数
context / tool_call / message_end
给 LLM 发的消息前加一段"系统提醒"
拦截型
取消/替换/阻止后续流程
session_before_compact / tool_call(返回 block:true) / input(返回 handled)
禁止在某个目录执行 bash

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 优点

维度
优点
体现
零编译
扩展就是普通 .ts 文件,不用 build 不用 watch
jiti 运行时转译(loader.ts:381-389)
API 极简
一个 ExtensionFactory 函数 + 一个 ExtensionAPI 参数
types.ts:1424
事件齐全
26 个事件覆盖 Agent 全生命周期
types.ts:993-1016
类型安全
每个事件 / 注册点都有强类型 handler 签名
types.ts:1133-1171
沙箱友好
扩展只能通过 ExtensionAPI 触达宿主,没有直接 require 系统的口子
loader.ts:217 闭包构造 API
作用域隔离
全局/项目/CLI 三层作用域 + 信任检查
resource-loader.ts:340-353
跨扩展通信
内置 EventBus,扩展之间可以发自定义事件
types.ts:1355
可热重载/reload
 命令重新走加载流程
loader.ts:139

10.2 缺点

维度
缺点
改进方向
无权限模型
扩展一旦加载就能调所有 register* API,没有"只读 / 只加工具 / 不能改 UI"的细分
未来可加 cap-based 权限
无版本约束
扩展用 import 的 ExtensionAPI 类型可能与 Pi 不同版本不兼容,没有 manifest 声明最低 Pi 版本
可加 pi.peer 字段
jiti 性能
第一次加载每个 .ts 都要转译,启动慢 100-300ms
已有缓存(loader.ts:130-143),但 reload 会清空
事件无优先级
所有 handler 串行调用且按注册顺序,无法声明"我这个必须先跑"
可加 priority 字段
UI 扩展无声明式 schemasetFooter
 / setWidget 接收"返回 Component 的工厂函数",没有结构化描述
可加 declarative schema
错误传播粗
一个 handler 抛错整个事件链中断;没有 try/catch 包裹
runner.ts 可加 per-handler try/catch + error listener
无扩展商店
用户得自己写 .ts 或从 GitHub 复制,没有 npm 包的官方市场
可加 pi install <name>

11. 与同类插件系统对比

对比维度
Pi 扩展
VS Code 扩展
Sublime 插件
JetBrains Plugin
LangChain Tool
扩展形式
单个 .ts 文件
Node 项目 + package.json
Python 文件
Java/Kotlin 项目
Python 函数 + schema
加载方式
jiti 运行时转译
宿主 Node 启动 extension host 进程
嵌入式 Python 解释器
JVM 加载 .jar
import 即生效
沙箱性
API 闭包(半沙箱)
独立进程 + RPC(强沙箱)
同进程(弱沙箱)
JVM 隔离(中)
同进程(无)
事件模型
26 个内置事件 + EventBus
setContext / onDidChangeTextDocument 等数百个
EventListener 几十个
MessageBus + Listener 几十个
无统一事件,靠 wrapper
类型安全
完全 TS 强类型
部分(基于 JSON RPC)
弱(动态)
强(Java 强类型)
中(Pydantic)
权限模型
无(按作用域划分)
有(capability 声明)
有(action 声明)
UI 扩展
有(widget / footer / header)
有(Webview / TreeView)
弱(Annotation)
有(ToolWindow)
工具扩展
有(registerTool,TypeBox schema)
有(Language Server)
有(核心场景)
启动开销
低(jiti 单文件转译)
高(启动额外进程)
高(classloader)
极低

总结:Pi 扩展机制在"轻量 + 类型安全 + 覆盖完整生命周期"这三件事上做到了较好的平衡,比 VS Code 轻、比 Sublime 安全、比 LangChain 完整;但在"权限模型 + 插件市场"上不如成熟的 IDE 插件生态。

12. 一图总结