背景 链接到标题

Pi Coding Agent 的扩展机制允许向 AgentSession 注册自定义 Tool,LLM 在对话中可以直接调用这些工具。扩展是 ESM 模块,通过 pi.registerTool() 注册,自动加载到当前 Session。

注册 Tool 链接到标题

Extension 文件导出一个默认函数,接收 pi: ExtensionAPI 作为参数:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  pi.registerTool({ /* ... */ });
}

pi.registerTool() 接收一个对象,关键字段如下:

字段 类型 说明
name string 工具唯一标识,LLM 据此调用
label string 显示名称,UI 中展示
description string 工具描述,LLM 据此判断何时调用
promptSnippet string 简短提示,注入系统提示词
promptGuidelines string[] 调用 guideline,LLM 参考
parameters TypeBox Schema 参数 Schema,LLM 据此生成参数
execute function 工具执行函数

TypeBox Schema 链接到标题

parameters 用 TypeBox 定义:

parameters: Type.Object({
  query: Type.String({ description: "搜索关键词" }),
  topK: Type.Optional(Type.Number({ default: 5 })),
})

支持 Type.StringType.NumberType.BooleanType.ArrayType.Optional 等。

execute 函数 链接到标题

async execute(_toolCallId, params, signal, onUpdate, ctx) {
  // toolCallId: 本次调用 ID
  // params: LLM 生成的参数,按 Schema 解析
  // signal: AbortSignal,支持取消
  // onUpdate: 流式更新回调
  // ctx: 扩展上下文
}

返回格式:

{
  content: [{ type: "text", text: "结果文本" }],
  details: {},
}

content 给 LLM 阅读,details 给 UI 展示(如结构化 JSON)。

完整示例 链接到标题

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "my_tool",
    label: "我的工具",
    description: "接收一个搜索关键词,返回相关结果",
    promptSnippet: "搜索相关信息",
    promptGuidelines: [
      "优先使用此工具获取实时数据",
      "参数不足时向用户确认",
    ],
    parameters: Type.Object({
      query: Type.String({ description: "搜索关键词" }),
      topK: Type.Optional(Type.Number({ default: 5, description: "最多返回条数" })),
    }),
    async execute(_toolCallId, params, signal) {
      const resp = await fetch("https://api.example.com/search", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ q: params.query, limit: params.topK }),
        signal,
      });
      const data = await resp.json();
      return {
        content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
        details: {},
      };
    },
  });
}

关键点:

  • description 写清楚工具用途,LLM 据此判断是否调用
  • signal 透传给 fetch,支持中断
  • 返回 contenttext 类型,LLM 直接阅读

Extension 加载方式 链接到标题

项目级扩展 链接到标题

将扩展目录放在项目根目录的 .pi/extensions/ 下,DefaultResourceLoader 根据 cwd 自动发现:

项目根目录/
└── .pi/
    └── extensions/
        └── my-tool/
            ├── index.ts
            └── package.json  # 可选,额外 npm 依赖

内联加载 链接到标题

main.js 中直接 import,传入 extensionFactories

const { default: myExt } = await import('./extensions/my-tool.js');
const resourceLoader = new DefaultResourceLoader({
  cwd: expandHome(config.cwd),
  agentDir: getAgentDir(),
  extensionFactories: [myExt],
});

热重载 链接到标题

扩展 handler 中调用 ctx.reload(),不中断当前 Session 即可重载所有扩展。

参考 链接到标题