DOCS v0.1.0 BUILD: STABLE GitHub ↗
EXTRAS · EXTENSIONS

扩展系统能力面

s10 讲了扩展怎么跑(lifecycle hooks + 三类钩子 + 三层错误隔离)。本篇讲扩展还能做什么——工具、命令、provider、UI、快捷键、CLI flag、自定义压缩、模式扩展。

边界:s10 讲了什么,本篇讲什么

能力面s10 已讲本篇覆盖
事件订阅三类钩子 + 6 个事件剩余 20 个事件清单
工具注册ToolDefinition 完整契约 + 与 dispatch table 桥接
命令注册registerCommand + 会话控制权
Provider 注册ProviderConfig + OAuth + 延迟 flush
UI 扩展ExtensionUIContext 十余项能力
快捷键 / CLI flagregisterShortcut + registerFlag + 冲突仲裁
自定义压缩session_before_compact 事件模式
模式扩展subagent vs plan-mode 两种思路
加载生命周期throwing stubs + await factory完整 7 阶段

Extension 自身加载生命周期 7 阶段

s10 讲的是扩展挂载 handler 后监听 agent 运行时事件。本节问的是 extension 实例本身的加载/注册/销毁——loader 和 runner 管理 extension 的视角。

  1. Discover:扫描 cwd/.pi/extensions/(项目级)+ agentDir/extensions/(全局级)。三种入口:直接 .ts 文件 / 子目录 index.ts / 子目录 package.json 含 pi.extensions 字段。
  2. Load module:jiti 运行时 import TS(moduleCache: false),取 module.default 作为 factory。
  3. Init factoryawait factory(api) 执行所有 api.on / api.registerTool 注册。此阶段 action 方法是 throwing stubs——防止扩展在加载期误调 sendMessage。registerProvider 入队 pendingProviderRegistrations
  4. Bind core:throwing stubs 替换成真实 actions(sendMessage / setModel / setActiveTools…)。flush pendingProviderRegistrations 到 ModelRegistry。从此刻起 registerProvider 立即生效,无需 /reload。
  5. Bind command context:注入 waitForIdle / newSession / fork / navigateTree / switchSession / reload。只有 slash 命令 handler 能拿到这些会话控制权,事件 handler 拿不到——权限分层。
  6. Bind UI:noOpUIContext 替换为真实 TUI 实现。print/RPC 模式保留 noOp。
  7. Shutdowninvalidate() 标记 stale + emit session_shutdown。reason 含 quit/reload/new/resume/fork,扩展可据 reason 决定清理策略。

两层生命周期的交汇点session_shutdown 既是 agent 运行时事件(s10 视角),也是 extension 实例销毁信号(本节视角)。扩展监听它做 cleanup,是两层唯一直接交互的地方。

Custom Tool 完整契约

s02 的 dispatch table 是 AgentTool[]。扩展注册的工具经 wrapRegisteredTool 转成 AgentTool 喂给 dispatch table——与内置工具地位平等,因此可以同名注册覆盖内置工具。

pi.registerTool(defineTool({
  name: "hello",
  label: "Hello",
  description: "Say hello to someone",
  promptSnippet: "Greet a person by name.",
  promptGuidelines: ["Always be polite"],
  parameters: Type.Object({ name: Type.String() }),
  executionMode: "sequential",
  renderShell: "self",
  execute: async (id, params, signal, onUpdate, ctx) => {
    return { content: [{ type: "text", text: "Hello, " + params.name + "!" }] };
  },
  renderCall: (args, theme) => CustomCallComponent,
  renderResult: (result, opts, theme) => CustomResultComponent,
}));

defineTool 的作用:直接传对象字面量给 registerTool 会丢失泛型推断,defineTool 包装后 TParams 完整保留。execute 返回 AgentToolResult,与内置工具同形。

执行模式sequential(串行,默认)/ parallel(可与其他并行工具并发)。renderShell: "self" 时工具自渲染外壳,"default" 走默认彩色框。

源码锚点types.ts:426-488 ToolDefinition · wrapper.ts:17-19 wrapRegisteredTool 桥接 · hello.ts 最小示例 · subagent/index.ts 完整示例(renderCall/renderResult/并发控制/子进程)

Custom Command + 会话控制权

pi.registerCommand("tools", {
  description: "Toggle tools on/off",
  getArgumentCompletions: (prefix) => filteredItems,
  handler: async (args, ctx) => {
    const choice = await ctx.ui.select("Select tool", items);
    if (choice) { pi.setActiveTools([choice]); }
  },
});

handler 收到原始 args: string,扩展自行解析——pi 不做参数 schema 校验,KISS。

重名冲突仲裁:两个扩展都注册 /foo,invocationName 自动变成 /foo:1/foo:2。避免静默覆盖。

ExtensionCommandContext 的会话控制权:只有 slash 命令 handler 拿到 waitForIdle / newSession / fork / navigateTree / switchSession / reload。事件 handler 拿到的是普通 ExtensionContext,不能切会话。这是权限分层——用户主动触发的命令才允许做高风险操作。

源码锚点types.ts:333-364 ExtensionCommandContext · runner.ts:512-546 resolveRegisteredCommands · commands.ts + tools.ts

Custom Provider + 延迟 flush

pi.registerProvider("custom-anthropic", {
  baseUrl: "https://api.anthropic.com",
  apiKey: process.env.ANTHROPIC_API_KEY,
  api: anthropicMessagesApi(),
  models: [{ id: "claude-sonnet-4-5", ... }],
  oauth: { login, refreshToken, getApiKey },
  streamSimple: (model, ctx, opts) => customSSEStream,
  headers: { "x-custom": "..." },
  authHeader: true,
});

与 s08 的关系:s08 讲 pi-ai 包的 LlmProvider 抽象。扩展的 registerProvider 最终调用 ModelRegistry.registerProvider。两条路径:提供 streamSimple 完全自定义 SSE 流;不提供则用 pi-ai 内置流处理器,只覆盖 baseUrl/headers。

延迟 flush 机制:加载阶段 ModelRegistry 还没绑定,registerProvider 入队 pendingProviderRegistrations。bindCore 后 flush——单个失败走 emitError 不中断。此后 registerProvider 立即生效,无需 /reload。

源码锚点types.ts:1240-1348 ProviderConfig · runner.ts:301-335 bindCore flush · custom-provider-anthropic 完整示例(OAuth + streamSimple + 工具名映射)

UI 扩展能力面

UI 能力不在 ExtensionAPI 上,而在 ExtensionContext.uiExtensionUIContext)。设计原因:UI 依赖 TUI 实例,print/RPC 模式没有 TUI(noOpUIContext 全空实现),所以 UI 能力必须在 context 上。

类别API示例
对话框select / confirm / input / editorpermission-gate.ts
通知notify(message, type?)pirate.ts
状态栏setStatus(key, text)plan-mode.ts
WidgetsetWidget(key, content, placement)widget-placement.ts · snake.ts
Footer/HeadersetFooter(factory) / setHeader(factory)custom-footer.ts
Overlay 组件custom(factory, overlayOptions)doom-overlay · snake.ts
编辑器替换setEditorComponent(factory)modal-editor.ts · rainbow-editor.ts
自动补全addAutocompleteProvider(factory)github-issue-autocomplete.ts
主题theme / getTheme / setThememac-system-theme.ts
终端标题setTitle(title)titlebar-spinner.ts
原始输入onTerminalInput(handler)自定义按键监听
消息渲染registerMessageRenderer(customType, renderer)message-renderer.ts

CustomEditor 模式:扩展继承 CustomEditor 基类重写 handleInput,对未处理的键调 super.handleInput(data) 让 app keybinding 继续生效——Vim 风格 modal editor 的标准模式。

doom-overlay 的意义:35 FPS 实时渲染 DOOM,证明 overlay 能承载真实游戏,不是玩具。任何"能不能在 pi 里跑 X"的疑问都有答案了。

Keybinding + CLI Flag

pi.registerShortcut(Key.ctrlAlt("p"), {
  description: "Toggle plan mode",
  handler: (ctx) => togglePlanMode(),
});
pi.registerFlag("plan", {
  type: "boolean",
  default: false,
});
const enabled = pi.getFlag("plan");

快捷键冲突仲裁(三类):

  1. 扩展键撞保留键(17 个,如 app.interrupt / app.exit)→ warning + skip(扩展无法覆盖)
  2. 扩展键撞非保留内置键 → warning 但扩展胜出
  3. 多个扩展注册同一键 → warning,后注册的胜出

Flag 生命周期:registerFlag 时若有 default 且 flagValues 没有该 key,写入默认值。CLI 解析后 setFlagValue 覆盖。扩展通过 pi.getFlag(name) 读取——先校验扩展自己注册了该 flag,再从 runtime 读取。

源码锚点runner.ts:62-80 保留键清单 · runner.ts:417-460 冲突检测 · plan-mode + preset

Custom Compaction:事件驱动可替换行为

pi 没有单独的 registerCompaction API。自定义压缩通过 session_before_compact 事件 + result 字段实现——这是"事件驱动可替换行为"的典型模式。所有"可替换核心行为"都用 before_xxx 事件,而不是新增 registerXxx API。

pi.on("session_before_compact", (event, ctx) => {
  const model = ctx.modelRegistry.find("google", "gemini-2.5-flash");
  const text = serializeConversation(event.preparation.messagesToSummarize);
  const summary = await complete(model, { messages: [{ role: "user", content: text }] }, { signal: event.signal });
  return {
    compaction: {
      summary,
      firstKeptEntryId: event.preparation.firstKeptEntryId,
      tokensBefore: event.preparation.tokensBefore,
    },
  };
});

返回 { compaction } 替换默认压缩结果;返回 { cancel: true } 取消压缩;返回 undefined 回退默认。失败时 return 即可,不用抛错。

源码锚点types.ts:536-549 事件签名 · custom-compaction.ts 完整示例 · trigger-compact.ts 主动触发

模式扩展两种思路

extension 没有直接的 registerMode API。两种实现思路:

维度subagent(spawn 子进程)plan-mode(白名单+注入)
context 隔离物理隔离(子进程)逻辑隔离(同进程,setActiveTools 限制)
实现复杂度高(子进程管理、JSON 协议、并发控制)中(事件组合)
可观测性子进程输出需自定义渲染主循环输出,原生可观测
适合场景长任务委派、多 agent 协作模式切换、安全约束
示例subagent/index.tsplan-mode/index.ts

plan-mode 的能力组合(几乎用到了一半的扩展能力):registerFlag + registerCommand + registerShortcut + setActiveTools + before_agent_start 注入 system prompt + tool_call 拦截 + context 过滤 + turn_end 提取标记 + session_start 恢复状态 + setStatus + setWidget + appendEntry 持久化。

官方示例索引

示例演示能力教学价值
hello.tsregisterTool + defineTool最小可运行扩展
tools.tsregisterCommand + setActiveTools + 持久化有状态扩展范式
commands.tsregisterCommand + autocomplete + getCommands命令系统元命令
custom-provider-anthropicregisterProvider + OAuth + streamSimpleprovider 最完整示例
doom-overlaycustom + overlayUI 极限能力(35 FPS)
snake.tscustom + setInterval + 持久化UI 游戏教学起点
plan-modeflag + command + shortcut + 事件组合模式扩展综合应用
subagentregisterTool + spawn + 并发控制最完整工具示例
custom-compaction.tssession_before_compact事件驱动可替换行为
modal-editor.tssetEditorComponent + CustomEditorVim 风格编辑器
tool-override.ts同名 registerTool 覆盖内置工具覆盖模式
event-bus.tspi.events EventBus扩展间通信

其余事件清单

s10 讲了 6 个事件。剩余 20 个按类别分组:

类别事件用途
会话树session_tree / session_before_tree / session_before_switch / session_before_fork与 s14 呼应,分支切换事件
Providerbefore_provider_request / after_provider_response请求/响应拦截
输入input / user_bash用户输入变换 / ! 命令拦截
工具细粒度BashToolCall / ReadToolCall / EditToolCall / WriteToolCall / GrepToolCall / FindToolCall / LsToolCall / CustomToolCall按工具类型的事件 + isToolCallEventType 类型守卫
工具结果tool_result工具结果变换
工具执行tool_execution_start / tool_execution_update / tool_execution_end执行进度事件
模型model_select / thinking_level_select模型/thinking 切换
消息流message_start / message_update消息流式事件(s10 只讲 message_end)
Turn/Agentturn_start / turn_end / agent_start / agent_endturn/agent 级事件
资源resources_discover动态注入 skills/prompts/themes 路径

Action 方法集

扩展不止被动监听,还能主动操作 agent。bindCore 后这些方法从 throwing stubs 变成真实实现:

  • 消息:sendMessage / sendUserMessage / appendEntry
  • 工具:setActiveTools(白名单)/ getActiveTools / getAllTools
  • 模型:setModel / setThinkingLevel / getThinkingLevel
  • 会话:setSessionName / setLabel(给 entry 打标签)
  • 执行:exec(执行 shell 命令)
  • 压缩:compact(主动触发压缩)
  • 查询:getContextUsage(上下文 token 用量)

能力全景

ExtensionAPI
├─ on(event, handler)          26 个事件(s10 讲了 6 个,本篇补 20 个)
├─ registerTool(tool)          注入 dispatch table(s02)
├─ registerCommand(name, opt) slash 命令 + 会话控制权
├─ registerProvider(name, cfg) 注入 ModelRegistry(s08)+ OAuth + streamSimple
├─ registerShortcut(key, opt) 快捷键 + 冲突仲裁
├─ registerFlag(name, opt)     CLI flag + getFlag
├─ registerMessageRenderer    自定义消息渲染
├─ pi.events                   EventBus 扩展间通信
└─ Action 方法                 sendMessage / setModel / setActiveTools / exec...

ExtensionContext.ui(TUI 模式独有)
├─ select / confirm / input    对话框
├─ notify / setStatus          通知 + 状态栏
├─ setWidget / setFooter       Widget / Footer / Header
├─ custom(factory, { overlay }) Overlay 自定义组件(doom-overlay)
├─ setEditorComponent          替换编辑器(modal-editor)
├─ addAutocompleteProvider     自动补全
├─ theme / setTheme            主题
└─ setTitle                    终端标题

返回番外索引 · 回到 s10 正文