8 min read教程

PiAgent 11|扩展系统:把 Agent 改造成你的 Agent

用工具、命令和生命周期钩子扩展运行时,同时守住参数、权限和事件时序边界

PiAgent 11|扩展系统:把 Agent 改造成你的 Agent#

用工具、命令和生命周期钩子扩展运行时,同时守住参数、权限和事件时序边界

Pi 的内核保持克制:它提供循环、默认工具、会话和事件,但不替应用决定要不要审批、计划、外部 API 或业务数据库。二次开发的主要入口就是扩展系统。扩展不只是“多写几个工具”,而是给运行时增加能力、入口和控制点。

1. 四种扩展方式#

机制面向谁典型职责是否改变模型能力
registerTool模型查询、计算、写入和外部操作
registerCommand/review/reload 等显式操作通常间接改变
pi.on运行时观察、阻断或插入策略可能改变
UI/主题扩展状态展示、交互和快捷键通常不直接改变

判断规则很简单:模型需要调用的能力注册为工具;人需要主动触发的动作注册为命令;要在流程前后插入策略,就监听事件。把三者揉成一个巨型工具,会让触发者和副作用都难以判断。

2. 一个最小扩展#

当前扩展 API 的核心形态是一个接收 ExtensionAPI 的函数:

ts
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 连接生命周期#

扩展可以订阅会话和工具生命周期:

ts
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 和扩展注入内容。要明确:

text
默认资源 → 项目资源 → 用户资源 → 扩展策略 → 当前输入

实际优先级以当前版本实现为准,但产品设计必须回答“冲突时谁覆盖谁”。特别是安全规则不能被低信任目录中的 prompt template 覆盖。

8. 如何测试扩展行为#

扩展测试不应只检查函数返回值,还应检查事件顺序和阻断效果:

text
加载扩展
  → 工具是否出现在当前定义中
  → 非法参数是否在 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

相关文章