PiAgent 11|扩展系统:把 Agent 改造成你的 Agent#
用工具、命令和生命周期钩子扩展运行时,同时守住参数、权限和事件时序边界
Pi 的内核保持克制:它提供循环、默认工具、会话和事件,但不替应用决定要不要审批、计划、外部 API 或业务数据库。二次开发的主要入口就是扩展系统。扩展不只是“多写几个工具”,而是给运行时增加能力、入口和控制点。
1. 四种扩展方式#
| 机制 | 面向谁 | 典型职责 | 是否改变模型能力 |
|---|---|---|---|
registerTool | 模型 | 查询、计算、写入和外部操作 | 是 |
registerCommand | 人 | /review、/reload 等显式操作 | 通常间接改变 |
pi.on | 运行时 | 观察、阻断或插入策略 | 可能改变 |
| UI/主题扩展 | 人 | 状态展示、交互和快捷键 | 通常不直接改变 |
判断规则很简单:模型需要调用的能力注册为工具;人需要主动触发的动作注册为命令;要在流程前后插入策略,就监听事件。把三者揉成一个巨型工具,会让触发者和副作用都难以判断。
2. 一个最小扩展#
当前扩展 API 的核心形态是一个接收 ExtensionAPI 的函数:
import { Type } from "typebox";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "lookup_note",
label: "查找笔记",
description: "按标题查找当前用户有权限访问的笔记。只读。",
parameters: Type.Object({
title: Type.String({ minLength: 1, maxLength: 120 }),
}),
async execute(_id, params, signal) {
const note = await findNote(params.title, { signal });
return {
content: [{ type: "text", text: note ? note.body : "没有找到" }],
};
},
});
}扩展模块的加载时机、异常处理和资源释放由扩展运行时管理;工具自身仍然要负责参数校验、权限判断和取消响应。
3. pi.registerTool 的真正边界#
工具注册完成后,模型能看到的是 name、description 和参数 schema;执行时得到的是已进入工具管道的参数。扩展不能假设“模型生成了合法 JSON”就代表业务操作安全。
一个生产工具通常还要检查:
- 当前 Session 是否拥有该工具。
- 当前用户是否能访问目标资源。
- 参数是否超出业务范围,而不仅是类型正确。
signal是否已经取消。- 外部请求是否有超时和重试预算。
- 结果是否需要脱敏或截断。
4. pi.registerCommand 适合人类入口#
命令由用户显式触发,不是模型在 Loop 中自由选择的工具。例如 /reload 可以重新加载资源,/review 可以让宿主启动一套固定审查流程。
命令和工具分开有两个好处:用户意图更明确,危险动作也更容易要求确认。若命令最终要向模型注入消息,应明确它是普通输入、steering 还是 follow-up,不要在命令内部偷偷改变队列语义。
5. pi.on 连接生命周期#
扩展可以订阅会话和工具生命周期:
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && !ctx.hasPermission("shell")) {
return { block: true, reason: "当前会话没有 shell 权限" };
}
});
pi.on("session_start", (_event, ctx) => {
ctx.ui?.notify("扩展已加载");
});
}能否修改或阻断取决于事件类型和官方 API 契约。观察型事件不要假设返回值会生效;可控制的钩子则要遵守其取消、替换或阻断协议。
6. 扩展加载是供应链边界#
扩展通常从资源目录发现并加载,因此要把它视作可执行代码供应链:
- 来源目录需要明确,不能任意扫描用户可写目录。
- 加载失败要有诊断信息,不能静默跳过。
- 扩展版本要能记录和回滚。
- 扩展拥有的工具和事件权限要可审计。
- 不可信扩展不应继承宿主全部环境变量和文件权限。
skills 是说明性资源,extensions 是可执行代码,二者的信任级别不同。不能因为都由 ResourceLoader 加载,就给它们相同权限。
7. 扩展与默认资源的合并顺序#
一个 Session 可能同时拥有默认系统提示词、项目上下文、用户 skills 和扩展注入内容。要明确:
默认资源 → 项目资源 → 用户资源 → 扩展策略 → 当前输入实际优先级以当前版本实现为准,但产品设计必须回答“冲突时谁覆盖谁”。特别是安全规则不能被低信任目录中的 prompt template 覆盖。
8. 如何测试扩展行为#
扩展测试不应只检查函数返回值,还应检查事件顺序和阻断效果:
加载扩展
→ 工具是否出现在当前定义中
→ 非法参数是否在 execute 前被拒绝
→ 无权限调用是否被阻断
→ 工具错误是否回到 Loop
→ dispose 后监听器是否注销对外部副作用工具,优先提供 dry-run 或假的后端适配器。扩展正确的标准不是“能让模型调用”,而是“在失败、取消、重试和重载时仍然可控”。
常见误区
- 把所有自定义逻辑都注册成工具,失去人类显式入口。
- 在
pi.on回调里执行不可取消的长任务。 - 把 skills 当成安全策略,把 extensions 当成普通配置。
- 扩展失败时静默吞掉诊断,导致 Agent 能力悄悄变化。
- 认为注册了工具就自动拥有数据库、文件或网络权限。
小结
扩展系统提供四类能力:给模型注册工具,给人注册命令,在生命周期上挂策略,把状态投影到 UI。它让 Pi 可以长成业务 Agent,但也把权限、版本、加载来源和错误处理责任交给了宿主。好的扩展会增加能力,同时让触发者、时序和副作用都更清楚。
源码定位
- 扩展 API:
packages/coding-agent/src/extensions/ - 工具注册与调用:
packages/agent/src/agent-loop.ts - 资源加载:
packages/coding-agent/src/core/resource-loader.ts - 扩展事件类型:官方文档
extensions