插件与扩展

Pi 的扩展系统是它的核心产品哲学:核心保持相对小,把很多工作流能力留给 extension 和 package。这里的“插件”主要对应源码里的 extensionspi packages

本章涉及的 loader、runner、hook、resource loader 和 package manager 行号见源码索引

关键源码:

  • packages/coding-agent/src/core/extensions/types.ts
  • packages/coding-agent/src/core/extensions/loader.ts
  • packages/coding-agent/src/core/extensions/runner.ts
  • packages/coding-agent/src/core/resource-loader.ts
  • packages/coding-agent/examples/extensions/*

扩展能做什么

扩展是 TypeScript 模块,默认导出一个 factory:

export default function (pi: ExtensionAPI) {
  pi.registerTool({ name: "deploy", ... });
  pi.registerCommand("stats", { ... });
  pi.on("tool_call", async (event, ctx) => { ... });
}

它可以:

  • 注册工具。
  • 注册 slash command。
  • 注册快捷键和 CLI flag。
  • 监听 agent/session/tool/provider 事件。
  • 改写输入、上下文、工具结果、message。
  • 注入 custom message。
  • 注册 provider。
  • 请求 UI:select、confirm、input、notify、自定义组件、footer/header/editor。
  • 发现额外 skills、prompts、themes。
  • 实现权限门、路径保护、git checkpoint、sub-agent、plan mode、MCP adapter 等上层工作流。

加载路径

DefaultResourceLoader 负责发现扩展。典型来源:

  • 用户全局目录:~/.pi/agent/extensions/
  • 项目目录:<cwd>/.pi/extensions/
  • settings 中声明的 extension source
  • pi package 中的 pi.extensions
  • SDK 传入的 additionalExtensionPathsextensionFactories

项目扩展受 project trust 控制。未信任项目时,Pi 只加载较安全的上下文文件、用户/全局扩展和 CLI 显式扩展,让它们有机会处理 project_trust 事件;项目本地扩展要等信任后加载。

Loader 设计

loader.ts 做两件事:

  1. jiti 加载 TypeScript/JavaScript extension module。
  2. 创建 ExtensionAPI,把注册方法写入当前 extension 对象,把动作方法委托给共享 runtime。

注册方法包括:

  • on(event, handler)
  • registerTool(tool)
  • registerCommand(name, options)
  • registerShortcut(shortcut, options)
  • registerFlag(name, options)
  • registerMessageRenderer(customType, renderer)
  • registerProvider(name, config)

动作方法包括:

  • sendMessage
  • sendUserMessage
  • appendEntry
  • setSessionName
  • setLabel
  • setActiveTools
  • setModel
  • setThinkingLevel
  • exec

这个分离很重要:扩展加载期只能登记能力;绑定到具体 session/runtime 后,动作方法才有真实实现。

Runner 设计

ExtensionRunner 是扩展执行器。它知道当前 extensions、runtime、UI context、session manager、model registry,并负责事件分发。

事件不是一律广播。不同事件有不同合并规则:

事件 合并/短路规则
project_trust 第一个返回 yes/no 的 handler 胜出,undecided 继续
session_before_switch/fork/compact/tree handler 可 cancel,cancel 后短路
message_end handler 可返回同 role replacement,串行传递给下一个 handler
tool_call handler 可 block,block 后短路
tool_result handler 可改 content/details/isError,串行累积
context handler 可替换 messages,串行传递
before_agent_start handler 可追加 custom message 或修改 system prompt
普通 lifecycle event 通知式分发,错误被记录而不是打崩主流程

这比简单 event emitter 更适合 agent,因为不同插入点的安全性和语义完全不同。

扩展上下文失效

AgentSessionRuntime 在 new session、switch session、fork、reload 时会 dispose 旧 session,并让旧 runner/context 失效。源码中对 stale context 有明确错误信息,要求扩展在 withSession 中使用新 context。

这是处理插件系统时很容易忽略的点:扩展闭包可能捕获旧 session,如果不显式失效,就会写错会话或操作已释放资源。

UI Context

扩展不直接依赖交互式 TUI。它通过 ExtensionUIContext 请求能力:

  • 交互模式提供真实 UI。
  • RPC 模式把 UI 请求编码成 JSONL extension_ui_request
  • print/headless 模式使用 no-op UI。

这让同一个扩展可以在不同运行模式下退化运行,而不是只能绑定终端 UI。

Pi Package

Pi package 是对扩展、skills、prompts、themes 的分发格式。package.json 可声明:

{
  "pi": {
    "extensions": ["./extensions"],
    "skills": ["./skills"],
    "prompts": ["./prompts"],
    "themes": ["./themes"]
  }
}

没有 manifest 时,Pi 会按约定目录自动发现。包可从 npm、git、https、ssh 安装。项目本地安装和全局安装分开,便于团队共享工作流。

设计启发

  • 扩展系统不只是 hook,要同时有注册表、资源发现、UI 抽象和 session 生命周期。
  • 每类事件都应定义自己的合并/短路语义。
  • 插件上下文需要可失效,尤其是支持 session replacement 的应用。
  • 扩展 UI 用能力接口抽象,可以同时支持 TUI、RPC 和 headless。