2026-07-14 Pi Extension

Pi Extension 机制及工作原理

Pi 的核心理念是极致可扩展,最小化核心。 Plan Mode、Sub-agent、权限弹窗、待办列表,内核都不做; 用户通过 Extension API 按需构建。本文拆开 loader / runner / wrapper 三层。

一句话

Extension 机制是插件化的事件驱动架构: 内核只保留四种基础工具 + TUI,行为决策权全部在 Extension 层。 piex 的每一个 package 都只走这套标准 API。

设计哲学:为什么用 Extension?

Pi is aggressively extensible so it doesn't have to dictate your workflow.

其他 AI 编码助手内置的特性(Plan Mode、Sub-agent、MCP、权限弹窗、待办列表、后台 Shell), pi 全部不做。取而代之的是一套 Extension 机制,让用户按自己需要构建这些能力。

内核只做最小的事:四种基础工具(read、write、edit、bash)加上基本 TUI。 所有高级特性通过 Extension 实现,不侵入核心代码。

三层架构

ExtensionFactory

用户写的 export default function(pi)。注册工具、命令、事件 handler。

loader.ts — 加载 & 发现 & API 工厂

  • 发现扩展路径(全局 / 项目 / 配置)
  • jiti 运行时加载 TypeScript
  • 执行工厂函数 → 填充 Extension 对象
  • 创建共享 ExtensionRuntime

runner.ts — 编排 & 事件分发

  • bindCore() 注入真实实现
  • createContext() 构造 ExtensionContext
  • 按加载顺序串行分发事件
  • 错误隔离,单扩展不拖垮 pi

wrapper.ts — 工具包装

注册的 ToolDefinition → AgentTool,供 agent-core 调用。

一、加载层(loader.ts)

运行时依赖解析

Extension 是 TypeScript 文件,通过 jiti 在运行时直接加载,无需编译。模块别名保证扩展 import 的是 pi 内部打包实例:

模式解析方式说明
Bun 二进制 virtualModules 将 pi 系列包直接注入 jiti 缓存
Node.js / 开发 alias 映射到本地 workspace 路径

扩展发现

discoverAndLoadExtensions() 按优先级扫描三处(不递归,只一层):

Priority 1

项目级 · {cwd}/.pi/extensions/

需信任项目后才加载。

Priority 2

全局 · ~/.pi/agent/extensions/

对所有项目生效。

Priority 3

配置 · settings.json → extensions

显式路径;npm 包通过 package.jsonpi.extensions 字段声明入口。

extensions/ layout
extensions/
├── my-ext.ts          → 单文件扩展
├── my-package/        → 子目录
│   ├── package.json   → 优先读 pi.extensions
│   └── index.ts
└── with-deps/         → 可带 node_modules
    ├── package.json
    └── src/index.ts

Extension 对象模型

每个扩展加载后生成独立 Extension 对象,数据不共享:

Extension
interface Extension {
  sourceInfo: SourceInfo;
  handlers:         Map<string, Function[]>;
  tools:            Map<string, RegisteredTool>;
  commands:         Map<string, RegisteredCommand>;
  flags:            Map<string, ExtensionFlag>;
  shortcuts:        Map<string, ExtensionShortcut>;
  messageRenderers: Map<string, Function>;
  entryRenderers:   Map<string, Function>;
}

ExtensionAPI 的两层委托

注册类 → Extension 自身

  • pi.registerTool()
  • pi.on("event", fn)
  • pi.registerCommand()

数据归属扩展,互不污染。

行动类 → 共享 Runtime

  • pi.sendMessage()
  • pi.setActiveTools()
  • pi.appendEntry()
  • pi.exec()

需要与会话系统交互,走全局后端。

ExtensionRuntime:延迟绑定

共享 Runtime 初始化时,所有 action 方法都是 throwing stub。 扩展加载期是纯声明阶段,不能发消息、改工具集。 bindCore() 后 stub 被替换为真实 AgentSession 实现。

Provider 注册队列

pi.registerProvider() 在加载期不会立即执行,而是压入 pendingProviderRegistrations,在 bindCore() 时批量冲刷, 解决 ModelRegistry 尚未就绪的时序问题。

二、运行层(runner.ts)

bindCore:连接真实世界

bindCore
runner.bindCore(
  actions,         // sendMessage, appendEntry, ...
  contextActions,  // getModel, isIdle, abort, compact, ...
  providerActions, // registerProvider(可选)
);
  1. 注入 actions → 替换 throwing stub
  2. 保存 context 闭包(getModel、isIdle…)
  3. 冲刷 provider 队列
  4. 之后 registerProvider 立即生效

createContext:懒求值

ExtensionContextgetter,每次访问实时取值, 避免 session 切换后闭包快照过时:

createContext
return {
  get ui() { return runner.uiContext; },
  get cwd() { return runner.cwd; },
  get model() { return getModel(); },
  get signal() { return runner.getSignalFn(); },
  isIdle: () => runner.isIdleFn(),
  compact: (opts) => runner.compactFn(opts),
};

事件分发的三种模式

按扩展加载顺序串行;每个 handler 包 try-catch,异常绝不向上传播。

Mode A

Fire-and-forget

session_startturn_endagent_start 等 30+ 事件。返回值忽略。

Mode B

First-cancel-wins

session_before_* 系列。任一 handler 返回 { cancel: true } 即停止后续分发。

Mode C

Chain 链式转换

tool_resultcontextbefore_agent_start。修改累积传递,下一 handler 看到上一步结果。

emitToolResult (Chain)
async emitToolResult(event) {
  const currentEvent = { ...event };
  for (const ext of this.extensions) {
    for (const handler of handlers) {
      const result = await handler(currentEvent, ctx);
      if (result?.content) currentEvent.content = result.content;
      if (result?.details) currentEvent.details = result.details;
      if (result?.isError) currentEvent.isError = result.isError;
    }
  }
  return { content, details, isError };
}

hashline 的 read hook 正是挂在 tool_result 链上, 把 [PATH#TAG] 快照头注入模型可见结果。

Stale Context 保护

Session 切换 / fork / reload 后,invalidate() 标记 stale, 旧 pi / ctx 上任何操作都抛异常, 防止扩展误用旧引用(最容易出 bug 的地方)。

三、包装层(wrapper.ts)

扩展工具经 wrapRegisteredTool() 变成 agent-core 的 AgentTool。 每次 execute 创建新 context;若执行期间动态 registerTool, 返回 addedToolNames 通知系统刷新工具集。

工具覆盖

注册与内置同名工具(如 edit)即可覆盖。省略的 render 槽位自动继承内置实现。

+

动态注册

扩展可在 execute 期间注册新工具,下一轮 LLM 调用自动包含。

四、完整生命周期

1 · Boot

discoverAndLoadExtensions()

扫描路径 → jiti 加载 → factory(pi) → 返回 extensions[] + runtime

2 · Session

AgentSession 初始化

new ExtensionRunner → bindCore → bindCommandContext → setUIContext

3–5 · Start

project_trust → session_start → resources_discover

信任决策、会话启动、扩展可提供 skill/prompt/theme 路径

6–9 · Loop

用户输入 → Agent 循环

  • 扩展命令匹配 / input transform
  • before_agent_start 注入上下文
  • 每 turn:context → provider hooks → LLM → tool_call / tool_result 链
  • turn_end → agent_end → agent_settled

10 · Shutdown

session_shutdown → invalidate

标记旧 ctx 失效;reload / 切换 session 时重建 Runner

五、设计要点总结

设计实现目的
数据归属清晰 注册 → Extension;行动 → Runtime 扩展独立,互不污染
延迟绑定 Runtime 初建 throwing stub 加载阶段纯声明
Context 懒求值 getter 实时取值 避免闭包过时
串行事件分发 按加载顺序 行为可预测
错误不传播 try-catch + emitError 单扩展不拖垮 pi
三种分发模式 Fire / Cancel / Chain 按语义选策略
Stale 保护 invalidate() 防 session 切换误用
Provider 队列 加载期入队,bind 时冲刷 解决注册时序

对 piex 的意义

正因为这套 API 边界清晰,piex 才能 100% 通过 Extension 实现 hashline / dap / lsp / plan / review, 不 fork pi、不改内核,并随 pi 升级而升级。