← blog 技术原理与实践 · 2026-06-04

Pi Coding Agent 扩展开发完全指南

从零开始掌握 Pi Coding Agent 的扩展系统:事件钩子、自定义工具、UI 组件、状态管理、Skill 技能包,以及 60+ 实战示例索引

20 min read

Pi Coding Agent 扩展开发完全指南

Pi Coding Agent 的扩展系统是其”自修改性”理念的核心载体。通过 TypeScript 扩展,你可以注册自定义工具、拦截事件流、定制 UI、修改系统提示——几乎可以对 Pi 的每一个环节进行编程控制。本文将全面介绍扩展系统的方方面面。


扩展是什么

扩展是 TypeScript 模块,通过导出一个默认工厂函数来接收 ExtensionAPI 对象。Pi 使用 jiti 进行运行时加载,因此 TypeScript 无需预编译。

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

export default function (pi: ExtensionAPI) {
  // 你的扩展逻辑
}

工厂函数也可以是异步的——Pi 会等待 async 初始化完成后再继续启动:

export default async function (pi: ExtensionAPI) {
  const response = await fetch("http://localhost:1234/v1/models");
  const payload = await response.json();
  pi.registerProvider("local", { /* ... */ });
}

核心能力一览

能力API说明
自定义工具pi.registerTool()LLM 可调用的工具
事件拦截pi.on()拦截工具调用、修改消息、控制流程
自定义命令pi.registerCommand()注册 /mycommand 命令
快捷键pi.registerShortcut()注册键盘快捷键
CLI 标志pi.registerFlag()注册自定义命令行参数
提供商注册pi.registerProvider()注册或覆盖模型提供商
UI 组件ctx.ui.*状态栏、Widget、对话框、自定义编辑器等
消息注入pi.sendMessage() / pi.sendUserMessage()向会话注入消息

快速开始

创建 ~/.pi/agent/extensions/my-extension.ts:

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

export default function (pi: ExtensionAPI) {
  // 1. 监听会话启动事件
  pi.on("session_start", async (_event, ctx) => {
    ctx.ui.notify("扩展已加载!", "info");
  });

  // 2. 拦截危险命令
  pi.on("tool_call", async (event, ctx) => {
    if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
      const ok = await ctx.ui.confirm("危险操作", "允许执行 rm -rf?");
      if (!ok) return { block: true, reason: "用户拒绝" };
    }
  });

  // 3. 注册自定义工具
  pi.registerTool({
    name: "greet",
    label: "打招呼",
    description: "按名字向某人问好",
    parameters: Type.Object({
      name: Type.String({ description: "名字" }),
    }),
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      return {
        content: [{ type: "text", text: `你好, ${params.name}!` }],
        details: {},
      };
    },
  });

  // 4. 注册命令
  pi.registerCommand("hello", {
    description: "打个招呼",
    handler: async (args, ctx) => {
      ctx.ui.notify(`你好 ${args || "世界"}!`, "info");
    },
  });
}

测试运行:

# 方式一:命令行直接加载
pi -e ./my-extension.ts

# 方式二:放到自动发现目录(推荐,支持热重载)
cp my-extension.ts ~/.pi/agent/extensions/

扩展文件组织

方式目录结构适用场景
单文件extensions/my-ext.ts简单扩展
目录extensions/my-ext/index.ts多文件扩展
带依赖extensions/my-ext/package.json + src/index.ts需要 npm 包

扩展存放位置

位置作用域
~/.pi/agent/extensions/*.ts全局
~/.pi/agent/extensions/*/index.ts全局(子目录)
.pi/extensions/*.ts项目级
.pi/extensions/*/index.ts项目级(子目录)

也可以在 settings.json 中指定额外路径:

{
  "extensions": ["/path/to/local/extension.ts"]
}

可用导入

包用途
@earendil-works/pi-coding-agent扩展类型、事件、工具函数
typebox工具参数的 Schema 定义
@earendil-works/pi-aiStringEnum(Google API 兼容枚举)
@earendil-works/pi-tuiTUI 组件
node:fs, node:path 等Node.js 内置模块

事件系统

事件系统是扩展的核心机制。通过 pi.on(event, handler) 订阅事件,可以对 Pi 的每一步操作进行拦截、修改或响应。

生命周期概览

graph TD
    Start["pi 启动"]
    Start --> SS["session_start { reason: startup }"]
    Start --> RD["resources_discover { reason: startup }"]

    Prompt["用户发送 prompt"]
    Prompt --> Input["input (可拦截/转换/完全处理)"]
    Input --> BAS["before_agent_start (可注入消息/修改系统提示)"]
    BAS --> AS["agent_start"]

    subgraph Turn["turn 循环 (直到 LLM 不再调用工具)"]
        TS["turn_start"] --> Ctx["context (可修改发送给 LLM 的消息)"]
        Ctx --> BPR["before_provider_request"]
        BPR --> LLM["LLM 响应,可能调用工具"]
        LLM --> TC["tool_call (可阻止/修改参数)"]
        TC --> TR["tool_result (可修改结果)"]
        TR --> TE["turn_end"]
    end

    AS --> TS
    TE -->|"继续下一轮"| TS
    TE -->|"LLM 不再调用工具"| AE["agent_end"]
    AE -. "用户再次发送 prompt" .-> Prompt

会话事件

session_start

会话启动、加载或重载时触发:

pi.on("session_start", async (event, ctx) => {
  // event.reason: "startup" | "reload" | "new" | "resume" | "fork"
  ctx.ui.notify(`会话已加载 (${event.reason})`, "info");
});

session_shutdown

会话关闭前触发,用于清理工作:

pi.on("session_shutdown", async (event, ctx) => {
  // event.reason: "quit" | "reload" | "new" | "resume" | "fork"
  connection?.close();
});

session_before_switch / session_before_fork

会话切换或分叉前触发,可取消:

pi.on("session_before_switch", async (event, ctx) => {
  if (event.reason === "new") {
    const ok = await ctx.ui.confirm("确认?", "清除所有消息?");
    if (!ok) return { cancel: true };
  }
});

Agent 事件

before_agent_start

每次用户提交 prompt 后、agent 循环开始前触发。可注入消息和修改系统提示:

pi.on("before_agent_start", async (event, ctx) => {
  return {
    // 注入持久化消息(存储在会话中,发送给 LLM)
    message: {
      customType: "my-extension",
      content: "额外的上下文信息",
      display: true,
    },
    // 修改系统提示(跨扩展链式修改)
    systemPrompt: event.systemPrompt + "\n\n额外的指令...",
  };
});

agent_start / agent_end

每次用户 prompt 对应一个 agent_start / agent_end 对:

pi.on("agent_end", async (event, ctx) => {
  // event.messages - 本次 prompt 产生的所有消息
});

工具事件

tool_call

工具执行前触发。可阻止执行、可修改参数:

import { isToolCallEventType } from "@earendil-works/pi-coding-agent";

pi.on("tool_call", async (event, ctx) => {
  // 类型安全的参数访问
  if (isToolCallEventType("bash", event)) {
    // event.input.command 是类型安全的
    if (event.input.command.includes("rm -rf")) {
      return { block: true, reason: "危险命令被阻止" };
    }
    // 修改参数(原地修改)
    event.input.command = `source ~/.profile\n${event.input.command}`;
  }
});

关键行为保证:

  • 对 event.input 的修改会影响实际工具执行
  • 后续 tool_call 处理器能看到前面处理器的修改
  • 不会在修改后重新校验

tool_result

工具执行完成后触发。可修改结果,支持链式中间件模式:

pi.on("tool_result", async (event, ctx) => {
  // 可以对结果做后处理
  return { content: [...], details: {...}, isError: false };
});

上下文事件

context

每次 LLM 调用前触发,可以非破坏性地修改发送给 LLM 的消息:

pi.on("context", async (event, ctx) => {
  const filtered = event.messages.filter(m => !shouldPrune(m));
  return { messages: filtered };
});

输入事件

input

用户输入到达时触发(在扩展命令检查之后、技能/模板展开之前):

pi.on("input", async (event, ctx) => {
  // 转换输入
  if (event.text.startsWith("?quick "))
    return { action: "transform", text: `简短回答: ${event.text.slice(7)}` };

  // 完全处理(不经过 LLM)
  if (event.text === "ping") {
    ctx.ui.notify("pong", "info");
    return { action: "handled" };
  }

  return { action: "continue" }; // 默认:传递给后续处理
});

处理结果:

  • continue — 原样传递(默认)
  • transform — 修改文本/图片后继续
  • handled — 跳过 agent 处理(第一个返回此值的处理器获胜)

模型事件

model_select

模型切换时触发(/model、Ctrl+P、会话恢复):

pi.on("model_select", async (event, ctx) => {
  // event.model, event.previousModel, event.source ("set" | "cycle" | "restore")
});

thinking_level_select

思考级别变化时触发(仅通知,返回值被忽略):

pi.on("thinking_level_select", async (event, ctx) => {
  ctx.ui.setStatus("thinking", `思考级别: ${event.level}`);
});

自定义工具

自定义工具是扩展最强大的能力之一。通过 pi.registerTool() 注册的工具会出现在系统提示中,LLM 可以像调用内置工具一样调用它们。

完整工具定义

import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";

pi.registerTool({
  name: "my_tool",
  label: "我的工具",
  description: "工具描述(LLM 可见)",
  promptSnippet: "一句话描述工具功能",              // 出现在系统提示的 Available tools 中
  promptGuidelines: [                               // 工具级指引
    "当用户需要 X 时使用 my_tool 而不是直接编辑文件"
  ],
  parameters: Type.Object({
    action: StringEnum(["list", "add"] as const),   // 必须用 StringEnum!
    text: Type.Optional(Type.String()),
  }),
  prepareArguments(args) {
    // 可选:在 schema 校验前转换参数(用于向后兼容)
    return args;
  },
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // 检查取消
    if (signal?.aborted) {
      return { content: [{ type: "text", text: "已取消" }] };
    }

    // 流式进度更新
    onUpdate?.({
      content: [{ type: "text", text: "处理中..." }],
      details: { progress: 50 },
    });

    // 执行操作
    const result = await pi.exec("some-cmd", [], { signal });

    return {
      content: [{ type: "text", text: "完成" }],  // 发送给 LLM
      details: { data: result },                    // 用于 UI 渲染和状态持久化
      terminate: true,                              // 可选:提示跳过后续 LLM 调用
    };
  },

  // 可选:自定义渲染
  renderCall(args, theme, context) { /* ... */ },
  renderResult(result, options, theme, context) { /* ... */ },
});

重要注意事项

1. 使用 StringEnum 而非 Type.Union

// ✅ 正确 — 兼容所有 provider(包括 Google)
action: StringEnum(["list", "add"] as const)

// ❌ 错误 — Google API 不支持
action: Type.Union([Type.Literal("list"), Type.Literal("add")])

2. 错误处理:抛出异常而非返回值

// ✅ 正确:抛出异常标记为错误
async execute(toolCallId, params) {
  if (!isValid(params.input)) {
    throw new Error(`无效输入: ${params.input}`);
  }
  return { content: [{ type: "text", text: "OK" }], details: {} };
}

3. 输出截断(必须!)

工具输出超过 50KB / 2000 行会导致上下文溢出:

import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";

const output = await runCommand();
const truncation = truncateHead(output, {
  maxLines: DEFAULT_MAX_LINES,  // 2000
  maxBytes: DEFAULT_MAX_BYTES,  // 50KB
});

let result = truncation.content;
if (truncation.truncated) {
  result += `\n\n[输出已截断,完整内容保存在: ${tempFile}]`;
}

4. 文件变更安全:withFileMutationQueue

当工具修改文件时,使用 withFileMutationQueue() 避免并行工具的竞态条件:

import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";

async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
  const absolutePath = resolve(ctx.cwd, params.path);

  return withFileMutationQueue(absolutePath, async () => {
    const current = await readFile(absolutePath, "utf8");
    const next = current.replace(params.oldText, params.newText);
    await writeFile(absolutePath, next, "utf8");
    return {
      content: [{ type: "text", text: `已更新 ${params.path}` }],
      details: {},
    };
  });
}

覆盖内置工具

注册同名工具即可覆盖内置的 read、bash、edit、write、grep、find、ls:

pi.registerTool({
  name: "read",  // 同名覆盖
  label: "Read",
  description: "带日志记录的文件读取",
  parameters: Type.Object({ path: Type.String() }),
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    console.log(`[读取] ${params.path}`);
    // 你的自定义实现...
    return { content: [{ type: "text", text: "..." }], details: {} };
  },
});

渲染是按槽位继承的——如果覆盖时省略了 renderCall,内置的渲染器仍然生效。

远程执行

内置工具支持可插拔的操作接口,可以委托给远程系统:

import { createReadTool, createBashTool } from "@earendil-works/pi-coding-agent";

const remoteRead = createReadTool(cwd, {
  operations: {
    readFile: (path) => sshExec(remote, `cat ${path}`),
    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
  }
});

Bash 工具还支持 spawn hook,可以在执行前调整命令、工作目录和环境变量:

const bashTool = createBashTool(cwd, {
  spawnHook: ({ command, cwd, env }) => ({
    command: `source ~/.profile\n${command}`,
    cwd: `/mnt/sandbox${cwd}`,
    env: { ...env, CI: "1" },
  }),
});

自定义渲染

工具可以提供 renderCall 和 renderResult 来自定义在终端中的显示效果:

import { Text } from "@earendil-works/pi-tui";

pi.registerTool({
  name: "my_tool",
  // ...

  renderCall(args, theme, context) {
    const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
    let content = theme.fg("toolTitle", theme.bold("my_tool "));
    content += theme.fg("muted", args.action);
    text.setText(content);
    return text;
  },

  renderResult(result, { expanded, isPartial }, theme, context) {
    if (isPartial) {
      return new Text(theme.fg("warning", "处理中..."), 0, 0);
    }
    let text = theme.fg("success", "✓ 完成");
    if (expanded && result.details?.items) {
      for (const item of result.details.items) {
        text += "\n  " + theme.fg("dim", item);
      }
    }
    return new Text(text, 0, 0);
  },
});

自定义 UI

扩展可以通过 ctx.ui 提供的方法与用户交互,并自定义消息/工具的渲染方式。

对话框

// 选择
const choice = await ctx.ui.select("选择一项:", ["A", "B", "C"]);

// 确认
const ok = await ctx.ui.confirm("删除?", "此操作不可撤销");

// 文本输入
const name = await ctx.ui.input("名字:", "默认值");

// 多行编辑
const text = await ctx.ui.editor("编辑内容:", "预填文本");

// 通知(非阻塞)
ctx.ui.notify("操作完成!", "info");  // "info" | "warning" | "error"

带倒计时的对话框

// 5 秒后自动取消
const confirmed = await ctx.ui.confirm(
  "限时确认",
  "此对话框将在 5 秒后自动取消。确认吗?",
  { timeout: 5000 }
);

if (confirmed) {
  // 用户确认
} else {
  // 用户取消或超时
}

状态栏与 Widget

// 底部状态栏(持续显示直到清除)
ctx.ui.setStatus("my-ext", "处理中...");
ctx.ui.setStatus("my-ext", undefined);  // 清除

// 工作指示器(流式输出时显示)
ctx.ui.setWorkingIndicator({
  frames: [
    ctx.ui.theme.fg("dim", "·"),
    ctx.ui.theme.fg("muted", "•"),
    ctx.ui.theme.fg("accent", "●"),
    ctx.ui.theme.fg("muted", "•"),
  ],
  intervalMs: 120,
});

// 编辑器上方 Widget
ctx.ui.setWidget("my-widget", ["状态行 1", "状态行 2"]);

// 编辑器下方 Widget
ctx.ui.setWidget("my-widget", ["行 1", "行 2"], { placement: "belowEditor" });

// 清除 Widget
ctx.ui.setWidget("my-widget", undefined);

完全替换内置的底部状态栏:

ctx.ui.setFooter((tui, theme) => ({
  render(width) {
    return [theme.fg("dim", `自定义 Footer | 宽度: ${width}`)];
  },
  invalidate() {},
}));

// 恢复内置 Footer
ctx.ui.setFooter(undefined);

自定义编辑器

可以用自定义实现替换主输入编辑器(例如实现 Vim 模式):

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

class VimEditor extends CustomEditor {
  private mode: "normal" | "insert" = "insert";

  handleInput(data: string): void {
    if (this.mode === "insert" && data === "escape") {
      this.mode = "normal";
      return;
    }
    if (this.mode === "normal" && data === "i") {
      this.mode = "insert";
      return;
    }
    super.handleInput(data);  // 保留 app 快捷键
  }
}

export default function (pi: ExtensionAPI) {
  pi.on("session_start", (_event, ctx) => {
    ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
      new VimEditor(theme, keybindings)
    );
  });
}

也可以包装已有的自定义编辑器:

const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
  new MyEditor(tui, theme, keybindings, {
    base: previous?.(tui, theme, keybindings)
  })
);

自定义组件

对于复杂的 UI 交互,使用 ctx.ui.custom() 临时替换编辑器:

import { Text } from "@earendil-works/pi-tui";

const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
  const text = new Text("按 Enter 确认,Escape 取消", 1, 1);

  text.onKey = (key) => {
    if (key === "return") done(true);
    if (key === "escape") done(false);
    return true;
  };

  return text;
});

还支持 Overlay 覆盖层模式(浮在现有内容之上,不清屏):

const result = await ctx.ui.custom<string | null>(
  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
  { overlay: true }
);

自定义消息渲染

注册自定义渲染器来控制特定 customType 消息的显示:

pi.registerMessageRenderer("my-extension", (message, options, theme) => {
  const { expanded } = options;
  let text = theme.fg("accent", `[${message.customType}] `);
  text += message.content;

  if (expanded && message.details) {
    text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
  }

  return new Text(text, 0, 0);
});

自动补全

可以在内置的斜杠命令和路径补全之上叠加自定义补全逻辑:

ctx.ui.addAutocompleteProvider((current) => ({
  async getSuggestions(lines, cursorLine, cursorCol, options) {
    const line = lines[cursorLine] ?? "";
    const beforeCursor = line.slice(0, cursorCol);

    // 匹配 #1234 格式的 GitHub issue
    const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
    if (!match) {
      return current.getSuggestions(lines, cursorLine, cursorCol, options);
    }

    return {
      prefix: `#${match[1] ?? ""}`,
      items: [
        { value: "#2983", label: "#2983", description: "扩展 API 示例" },
        { value: "#2753", label: "#2753", description: "重载资源配置" },
      ],
    };
  },
  applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
    return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
  },
  shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
    return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
  },
}));

主题颜色

所有渲染函数都会收到 theme 对象:

// 前景色
theme.fg("toolTitle", text)   // 工具名称
theme.fg("accent", text)      // 高亮
theme.fg("success", text)     // 成功(绿色)
theme.fg("error", text)       // 错误(红色)
theme.fg("warning", text)     // 警告(黄色)
theme.fg("muted", text)       // 次要文本
theme.fg("dim", text)         // 三级文本

// 文本样式
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)

代码语法高亮:

import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";

const lang = getLanguageFromPath("/path/to/file.rs");  // "rust"
const highlighted = highlightCode(code, lang, theme);

ExtensionAPI 速查

核心方法

方法用途
pi.on(event, handler)订阅事件
pi.registerTool(def)注册自定义工具
pi.registerCommand(name, opts)注册 /命令
pi.registerShortcut(key, opts)注册快捷键
pi.registerFlag(name, opts)注册 CLI 标志
pi.registerProvider(name, config)注册/覆盖模型提供商
pi.unregisterProvider(name)移除提供商

消息与状态

方法用途
pi.sendMessage(msg, opts)注入自定义消息到会话
pi.sendUserMessage(content, opts)发送用户消息
pi.appendEntry(type, data)持久化扩展状态(不发给 LLM)
pi.setSessionName(name)设置会话名称

运行时控制

方法用途
pi.exec(cmd, args, opts)执行 shell 命令
pi.getActiveTools()获取当前活跃工具列表
pi.getAllTools()获取所有可用工具
pi.setActiveTools(names)设置活跃工具
pi.setModel(model)切换模型
pi.getThinkingLevel()获取思考级别
pi.setThinkingLevel(level)设置思考级别
pi.events扩展间事件总线

消息投递模式

sendMessage 和 sendUserMessage 支持 deliverAs 选项:

模式行为
"steer"当前 turn 结束后立即投递(默认)
"followUp"等 agent 完全结束后投递
"nextTurn"排队等待下一次用户 prompt

ExtensionContext

所有事件处理器都会收到 ctx: ExtensionContext,提供运行时上下文:

属性/方法说明
ctx.uiUI 交互方法
ctx.hasUI是否有 UI(打印/JSON 模式下为 false)
ctx.cwd当前工作目录
ctx.sessionManager只读的会话状态访问
ctx.modelRegistry / ctx.model模型信息
ctx.signal当前 agent 的 AbortSignal
ctx.isIdle()agent 是否空闲
ctx.abort()中断当前操作
ctx.getContextUsage()获取上下文使用情况
ctx.compact()触发上下文压缩
ctx.getSystemPrompt()获取当前系统提示
ctx.shutdown()请求优雅退出

命令专有方法

命令处理器额外拥有 ExtensionCommandContext:

// 等待 agent 空闲
await ctx.waitForIdle();

// 创建新会话
await ctx.newSession({
  parentSession,
  setup: async (sm) => { /* 初始化新会话 */ },
  withSession: async (ctx) => { /* 在新会话中工作 */ },
});

// 从特定节点分叉
await ctx.fork("entry-id-123", { position: "before" });

// 导航到树中其他节点
await ctx.navigateTree("entry-id-456", { summarize: true });

// 切换到其他会话
await ctx.switchSession("/path/to/session.jsonl");

// 重载运行时
await ctx.reload();

状态管理

扩展的状态应存储在工具结果的 details 中,以支持会话分支:

export default function (pi: ExtensionAPI) {
  let items: string[] = [];

  // 从会话恢复状态
  pi.on("session_start", async (_event, ctx) => {
    items = [];
    for (const entry of ctx.sessionManager.getBranch()) {
      if (entry.type === "message" && entry.message.role === "toolResult") {
        if (entry.message.toolName === "my_tool") {
          items = entry.message.details?.items ?? [];
        }
      }
    }
  });

  pi.registerTool({
    name: "my_tool",
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      items.push("新项目");
      return {
        content: [{ type: "text", text: "已添加" }],
        details: { items: [...items] },  // 持久化到会话
      };
    },
  });
}

也可以使用 pi.appendEntry() 存储不参与 LLM 上下文的状态:

// 存储
pi.appendEntry("my-state", { count: 42 });

// 恢复
pi.on("session_start", async (_event, ctx) => {
  for (const entry of ctx.sessionManager.getEntries()) {
    if (entry.type === "custom" && entry.customType === "my-state") {
      // 从 entry.data 重建状态
    }
  }
});

自定义提供商

扩展可以动态注册或覆盖模型提供商:

pi.registerProvider("my-proxy", {
  name: "My Proxy",
  baseUrl: "https://proxy.example.com",
  apiKey: "$PROXY_API_KEY",  // 环境变量引用
  api: "anthropic-messages",
  models: [
    {
      id: "claude-sonnet-4-20250514",
      name: "Claude 4 Sonnet (proxy)",
      reasoning: false,
      input: ["text", "image"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 200000,
      maxTokens: 16384,
    }
  ]
});

还可以覆盖已有提供商的 baseUrl,或注册带 OAuth 支持的提供商。


Skill 技能系统

Skill 是 Markdown 格式的按需能力包,遵循 Agent Skills 标准。与扩展不同,Skill 不需要编写代码——它是给 LLM 的指令文档。

Skill 存放位置

位置作用域
~/.pi/agent/skills/全局
~/.agents/skills/全局
.pi/skills/项目级
.agents/skills/(cwd 及父目录)项目级

Skill 结构

my-skill/
├── SKILL.md              # 必须:前置元数据 + 指令
├── scripts/              # 辅助脚本(可选)
└── references/           # 详细文档(可选)

SKILL.md 格式

---
name: my-skill
description: 这个技能做什么以及何时使用。要具体描述。
---

# 我的技能

## 环境准备

首次使用前运行:
\`\`\`bash
cd /path/to/skill && npm install
\`\`\`

## 使用方法

\`\`\`bash
./scripts/process.sh <input>
\`\`\`

前置元数据字段

字段必填说明
name✅1-64字符,小写字母+数字+连字符
description✅最多1024字符,描述用途和触发条件
license❌许可证
compatibility❌环境要求
disable-model-invocation❌true 时需手动 /skill:name 加载

渐进披露

Skill 的关键设计是渐进披露:

  • 只有 description 常驻系统提示上下文
  • 完整的 SKILL.md 内容按需加载(Agent 用 read 工具读取)
  • 这减少了 token 开销

使用 Skill

# 命令行触发
/skill:my-skill

# 带参数
/skill:my-skill 参数1 参数2

# Agent 也会根据描述自动判断何时加载

通过 settings.json 配置 Skill 路径

{
  "skills": ["~/.claude/skills", "/path/to/custom-skill"]
}

思考级别

Pi 支持 6 个思考级别,控制模型在回答前的”深度思考”程度:

级别说明
off关闭思考
minimal最少思考
low低度思考
medium中度思考
high高度思考
xhigh超高度思考

配置方式

交互模式:按 Shift+Tab 循环切换

命令行:

pi --thinking high "解决这个复杂问题"
pi --model sonnet:high "复杂任务"  # 模型+思考级别简写

配置文件:

{
  "defaultThinkingLevel": "high",
  "thinkingBudgets": {
    "minimal": 1024,
    "low": 4096,
    "medium": 10240,
    "high": 32768
  }
}

通过扩展:

pi.setThinkingLevel("high");

非推理模型(如 GPT-4o)不支持思考,始终为 off。


设置系统

Pi 使用 JSON 配置文件,项目级覆盖全局:

位置作用域
~/.pi/agent/settings.json全局
.pi/settings.json项目级(覆盖全局,嵌套对象合并)

关键配置项

{
  "defaultProvider": "anthropic",
  "defaultModel": "claude-sonnet-4-20250514",
  "defaultThinkingLevel": "medium",
  "theme": "dark",
  "packages": ["pi-skills"],
  "extensions": ["/path/to/extension.ts"],
  "skills": ["~/.claude/skills"],
  "compaction": {
    "enabled": true,
    "reserveTokens": 16384,
    "keepRecentTokens": 20000
  },
  "enabledModels": ["claude-*", "gpt-4o"]
}

60+ 实战示例索引

Pi 内置了丰富的示例扩展,覆盖几乎所有使用场景。

工具类

示例说明关键 API
hello.ts最简工具注册registerTool
question.ts带用户交互的工具registerTool, ui.select
questionnaire.ts多步向导工具registerTool, ui.custom
todo.ts有状态工具 + 持久化registerTool, appendEntry, renderResult
dynamic-tools.ts运行时动态注册工具registerTool, session_start
structured-output.ts终止型工具registerTool, terminate: true
truncated-tool.ts输出截断registerTool, truncateHead
tool-override.ts覆盖内置工具registerTool(同名)
ssh.tsSSH 远程执行registerFlag, 工具操作
subagent/子代理registerTool, exec

命令与 UI 类

示例说明关键 API
pirate.ts修改系统提示registerCommand, before_agent_start
summarize.ts对话摘要registerCommand, ui.custom
handoff.ts跨 provider 交接registerCommand, ctx.newSession
qna.tsQ&A 交互registerCommand, ui.custom, setEditorText
custom-footer.ts自定义 FooterregisterCommand, setFooter
custom-header.ts自定义头部session_start, setHeader
modal-editor.tsVim 模态编辑器setEditorComponent, CustomEditor
widget-placement.tsWidget 放置setWidget
overlay-test.tsOverlay 组件ui.custom, overlay options
github-issue-autocomplete.tsGitHub Issue 补全addAutocompleteProvider

事件与安全类

示例说明关键 API
permission-gate.ts阻止危险命令on("tool_call"), ui.confirm
protected-paths.ts保护文件路径on("tool_call")
confirm-destructive.ts确认破坏性操作on("session_before_*")
dirty-repo-guard.tsGit 脏仓库警告on("session_before_*"), exec
input-transform.ts输入转换on("input")
model-status.ts模型切换状态on("model_select"), setStatus

Git 集成类

示例说明关键 API
git-checkpoint.ts每轮 git stashon("turn_start"), exec
auto-commit-on-exit.ts退出时自动提交on("session_shutdown"), exec
git-merge-and-resolve.ts合并并解决冲突on("agent_end"), sendUserMessage

复杂扩展

示例说明关键 API
plan-mode/完整 Plan Mode 实现全部事件类型、命令、快捷键、标志
preset.ts可保存的预设setModel, setActiveTools, setThinkingLevel
snake.ts贪吃蛇游戏ui.custom, 键盘处理
space-invaders.ts太空侵略者ui.custom
doom-overlay/Doom 覆盖层ui.custom, overlay
sandbox/沙箱执行工具操作
custom-provider-anthropic/自定义 Anthropic 代理registerProvider
custom-provider-gitlab-duo/GitLab Duo 集成registerProvider, OAuth

常见模式

模式一:权限控制

在工具执行前弹出确认对话框:

pi.on("tool_call", async (event, ctx) => {
  if (isToolCallEventType("bash", event)) {
    const cmd = event.input.command;
    if (cmd.includes("rm -rf") || cmd.includes("sudo")) {
      const ok = await ctx.ui.confirm("危险操作", `允许执行: ${cmd}?`);
      if (!ok) return { block: true, reason: "用户拒绝" };
    }
  }
});

模式二:系统提示增强

每轮动态修改系统提示:

pi.on("before_agent_start", async (event, ctx) => {
  const branch = ctx.sessionManager.getBranch();
  const turnCount = branch.filter(e => e.type === "message" && e.message?.role === "user").length;

  return {
    systemPrompt: event.systemPrompt + `\n\n当前对话轮次: ${turnCount}`,
  };
});

模式三:工具动态切换

根据条件启用或禁用工具:

pi.on("agent_start", async (_event, ctx) => {
  const usage = ctx.getContextUsage();
  if (usage && usage.tokens > 80_000) {
    // 上下文快满了,只保留必要工具
    pi.setActiveTools(["read", "bash", "edit"]);
  }
});

模式四:自动提交

Agent 结束时自动 git commit:

pi.on("agent_end", async (event, ctx) => {
  const status = await pi.exec("git", ["status", "--porcelain"]);
  if (status.stdout.trim()) {
    await pi.exec("git", ["add", "-A"]);
    const lastMsg = event.messages.findLast(m => m.role === "assistant");
    const msg = lastMsg?.content?.[0]?.text?.slice(0, 72) || "auto: agent changes";
    await pi.exec("git", ["commit", "-m", msg]);
    ctx.ui.notify("已自动提交更改", "info");
  }
});

错误处理

场景处理方式
扩展错误记录日志,agent 继续
tool_call 错误阻止工具执行(fail-safe)
工具 execute 错误必须用 throw 信号化;错误被捕获后以 isError: true 报告给 LLM

非交互模式行为

模式UI 方法说明
交互模式完整 TUI正常操作
RPC (--mode rpc)JSON 协议客户端处理 UI
JSON (--mode json)无操作事件流输出到 stdout
打印 (-p)无操作扩展运行但无法弹窗

在非交互模式下,使用 ctx.hasUI 检查是否有 UI 可用。


总结

Pi 的扩展系统遵循其”自修改性”核心理念——Pi 不内置的功能(Plan Mode、权限控制、子代理、MCP、Todo……),都可以通过扩展来实现。20+ 个生命周期钩子、完整的 UI 组件系统、按需加载的 Skill 机制,加上 TypeScript 无编译热重载的开发体验,让 Pi 成为一个真正”可编程”的编码 Agent。

正如 Pi 作者 Mario Zechner 所说:

“如果我不需要它,它就不会被构建。”

但这并不意味着你不能拥有它——只需要一个 TypeScript 文件,就能让 Pi 变成你想要的任何样子。

Sources

No external sources for this entry.

Related