概述

Pi Coding Agent 的扩展系统位于 F:\Pi\packages\coding-agent\src\core\extensions\,包含 5 个 TypeScript 文件,构成了一个完整的 生命周期事件驱动 + 自定义工具注册 的扩展框架。

扩展系统允许第三方模块:

  • 订阅 agent 生命周期事件(会话开始、agent 循环、消息流、工具执行等)
  • 注册 LLM 可调用的自定义工具
  • 注册命令、键盘快捷键、CLI 标志
  • 通过 UI 基元与用户交互(选择、确认、输入、通知)
  • 注册/覆盖模型 provider
  • 提供自定义消息渲染器

架构总览 — 依赖与抽象关系

文件的职责定位

文件行数角色核心职责
types.ts~1615 行类型定义层 (Foundation)定义所有类型接口、事件结构、API 契约
loader.ts~500 行加载层 (Loader)从文件系统发现并加载扩展模块,创建运行时骨架
runner.ts~750 行运行时层 (Runner)管理扩展生命周期、事件分发、上下文创建
wrapper.ts~30 行适配层 (Adapter)将扩展工具定义包装为 AgentTool
index.ts~80 行入口层 (Barrel)统一重导出公共 API,隐藏内部实现

依赖关系图 (引用链):

types.ts  ←───────────┬─── loader.ts
                      ├─── runner.ts
                      ├─── wrapper.ts
                      └─── index.ts

runner.ts  ←────────── wrapper.ts
types.ts   ←────────── index.ts
loader.ts  ←────────── index.ts
runner.ts  ←────────── index.ts
wrapper.ts ←────────── index.ts

关键设计决策:

  • loader.ts 和 runner.ts 互不依赖 — 加载器和运行时是解耦的,通过 ExtensionRuntime 接口桥接
  • wrapper.ts 依赖 runner.ts — 包装器需要 runner 的 createContext() 来传递一致的上下文
  • index.ts 不导出 loader.ts 的内部函数(如 loadExtensionModule) — 只导出公共 API

外部包类型详解

扩展系统引用了 4 个外部包和多个内部模块的类型。本节逐一说明每个外部包的核心类型及其在扩展系统中的用途。

@earendil-works/pi-agent-core — Agent 核心类型

这是 agent 循环的底层核心包,定义了消息、工具执行、thinking 等基础类型。

类型定义用途说明
AgentMessageLLM 与系统之间的消息单元(包含 role、content、tool_calls 等字段)ContextEvent 中传递用户对话消息给扩展修改;在 AgentEndEvent 中暴露最终消息列表;作为 ExtensionAPI.sendUserMessage() 的参数基础
AgentToolResult<TDetails>工具执行结果类型,泛型参数 TDetails 携带工具特定的详细信息ToolDefinition.execute() 的返回值类型,扩展必须返回此类型的结果
AgentToolUpdateCallback<TDetails>工具执行过程中的流式更新的回调函数类型传递给 ToolDefinition.execute()onUpdate 参数,扩展可调用它推送增量结果
ThinkingLevel模型思考级别(通常为 none、low、medium、high 等枚举)用于 ThinkingLevelSelectEvent 事件,以及 ExtensionAPI.getThinkingLevel() / setThinkingLevel() 的返回值/参数类型
ToolExecutionMode工具执行模式:"sequential""parallel"ToolDefinition.executionMode 字段中指定单工具的并行策略覆盖
AgentToolagent-core 可执行工具接口(包含 execute() 方法)wrapper.ts 的输出目标类型 — 扩展的 ToolDefinition 最终被包装为此类型供 agent-core 调用

在扩展系统中的流转路径

ToolDefinition.execute() → AgentToolResult
    ↓ (wrapper.ts 包装)
AgentTool ←── ToolDefinition 适配
    ↓ (agent-core 调度)
AgentMessage ←── LLM 调用结果

@earendil-works/pi-ai — AI Provider 类型

定义 AI 模型的接口、消息内容的格式、OAuth 认证等。

类型定义用途说明
ApiAPI 类型标识字符串(如 "anthropic-messages""openai-chat""openai-responses" 等)ProviderConfig.apiProviderModelConfig.api 中指定模型的 API 协议类型
Model<Api>模型的完整配置(包括 id、name、baseUrl、contextWindow、maxTokens、cost、input capabilities 等),泛型 Api 标记协议类型ExtensionContext.model 获取当前模型;ExtensionAPI.setModel() 的参数;ModelSelectEvent 的事件负载
AssistantMessageEvent流式助手消息中的一个事件(token delta、content block start/end、thinking delta 等)MessageUpdateEvent.assistantMessageEvent 中以流式增量形式传递,扩展可以逐帧观察 AI 输出
AssistantMessageEventStreamAsyncIterable<AssistantMessageEvent> 异步可迭代流ProviderConfig.streamSimple() 的返回值类型,用于自定义 API 的流处理器
ContextAI provider 上下文(包含消息历史、系统提示、配置等)ProviderConfig.streamSimple() 的参数之一,提供执行上下文
TextContent文本内容块({ type: "text", text: string }InputEvent.images 的替代内容中、ToolResultEvent.content 中、ExtensionAPI.sendUserMessage() 的参数中出现
ImageContent图片内容块({ type: "image", source: { type, data } }用户输入中的图片附件;BeforeAgentStartEvent.images 中传递;InputEvent.images 中传递
ToolResultMessageLLM 使用的工具结果消息格式TurnEndEvent.toolResults 的类型,包含该轮所有工具执行的结果数组
OAuthCredentialsOAuth 凭据存储类型(包含 access token、refresh token、到期时间等)ProviderConfig.oauth.login() 的返回值类型,扩展的 OAuth 流程返回此类型
OAuthLoginCallbacksOAuth 登录流程中提供给扩展的回调集(如 openUrl(url)ProviderConfig.oauth.login(callbacks) 的参数,扩展用它触发浏览器登录
SimpleStreamOptions简单流处理器的配置选项(如 signal 中止信号、onEvent 等)ProviderConfig.streamSimple() 的可选参数

在扩展系统中的流转路径

ProviderConfig.streamSimple → AssistantMessageEventStream
    ↓ (扩展注册 provider)
ModelRegistry ──持有──→ Model<Api> 列表
    ↓ (用户选择模型)
ExtensionContext.model ──→ Model<Api> | undefined

@earendil-works/pi-tui — 终端 UI 组件类型

定义了 TUI(终端用户界面)的全部组件系统,扩展用它构建自定义 UI。

类型定义用途说明
Component基础 UI 组件接口(包含 render()onMount()onUnmount() 等方法)ExtensionUIContext.setWidget()setFooter()setHeader()custom() 的组件工厂返回值;ToolDefinition.renderCall() / renderResult() 的返回值
TUITUI 核心接口(管理事件循环、屏幕渲染、图层等)传递给组件工厂(如 setFooter((tui, theme, data) => Component)),扩展可用它操作终端
KeyId键盘按键标识符字符串类型(如 "ctrl-p""escape""enter" 等)ExtensionAPI.registerShortcut(shortcut, options) 的第一个参数;ExtensionShortcut.shortcut 字段的类型
AutocompleteItem自动补全建议项(包含 label、description、insertText 等)RegisteredCommand.getArgumentCompletions() 的返回值元素类型
AutocompleteProvider自动补全提供者接口(getSuggestions(prefix) 等)AutocompleteProviderFactory 的输入/输出类型 — 扩展可以包装当前 provider 叠加自定义补全
EditorComponent编辑器组件接口(继承 Component,添加 getText()setText()handleInput() 等)EditorFactory 的工厂返回值类型
EditorTheme编辑器主题配置(包含颜色、边框样式、选中高亮等)传递给 EditorFactory(tui, theme, keybindings),扩展用它自定义编辑器外观
OverlayHandleOverlay 控制器(包含 close()update() 等方法)ExtensionUIContext.custom()onHandle 回调参数,扩展可用它控制 overlay 的显示/关闭
OverlayOptionsOverlay 配置选项(包含位置、尺寸、是否模态等)ExtensionUIContext.custom()overlayOptions 字段类型

组件工厂的数据流

扩展调用 setWidget(key, factory)
    ↓
TUI 运行时调用 factory(tui, theme)
    ↓
Component 对象 (render, onMount, onUnmount)
    ↓
TUI 渲染到终端屏幕

typebox — 运行时类型校验

TypeBox 是一个 TypeScript 优先的运行时类型校验库,用于工具参数的 schema 声明与校验。

类型定义用途说明
TSchema所有 TypeBox schema 的基类/接口ToolDefinition.parameters: TParams 的泛型约束 TParams extends TSchema,确保参数是合法的 TypeBox schema
Static<T>从 TypeBox schema T 中推断出其对应的静态 TypeScript 类型ToolDefinition.prepareArguments() 的返回值类型 Static<TParams>,确保参数预处理函数返回符合 schema 的数据

使用示例(在扩展代码中):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { Type, type Static } from "typebox";

const MyParams = Type.Object({
  name: Type.String(),
  count: Type.Optional(Type.Integer({ minimum: 1 })),
});

pi.registerTool({
  name: "my-tool",
  parameters: MyParams,    // TSchema 约束
  execute: (id, params: Static<typeof MyParams>, signal, onUpdate, ctx) => {
    // params.name 类型为 string
    // params.count 类型为 number | undefined
  },
});

内部模块引用

除了外部包,扩展系统还引用了大量内部模块的类型。这些是 Pi Coding Agent 的内部子系统:

源文件导入类型在扩展系统中的角色
../../config.tsCONFIG_DIR_NAME, getAgentDir, isBunBinary加载器获取配置目录名称(.pi)、agent 目录路径,判断是否为 Bun 二进制运行模式
../event-bus.tsEventBus扩展间通信的事件总线,ExtensionAPI.events 的类型
../exec.tsExecOptions, ExecResult, execCommandExtensionAPI.exec() 的执行引擎
../session-manager.tsSessionManager, ReadonlySessionManager, BranchSummaryEntry, CompactionEntry, SessionEntry会话存储与导航,ExtensionContext.sessionManager 的类型
../model-registry.tsModelRegistry模型注册中心,管理所有 provider 的模型列表
../keybindings.tsKeybindingsManager, AppKeybinding快捷键管理系统
../system-prompt.tsBuildSystemPromptOptions系统提示的构建选项
../source-info.tsSourceInfo, createSyntheticSourceInfo记录扩展/工具的来源元数据(文件路径、是否是内置等)
../messages.tsCustomMessage自定义消息类型,用于 ExtensionAPI.sendMessage()
../tools/index.tsBashToolDetails, ReadToolDetails, EditToolDetails内置工具的详情类型,用于事件中的类型细化
../tools/bash.tsBashOperationsUserBashEventResult.operations 的类型
../compaction/index.tsCompactionPreparation, CompactionResult会话压缩的事件参数类型
../footer-data-provider.tsReadonlyFooterDataProvider底部栏数据提供者
../slash-commands.tsSlashCommandInfo斜杠命令信息
../../modes/interactive/theme/theme.tsTheme交互模式的主题类型
../diagnostics.tsResourceDiagnostic资源诊断信息(快捷键冲突、命令问题等)

文件一:types.ts — 类型定义 (Foundation Layer)

角色与位置

这是整个扩展系统的类型基础。所有其他文件都从它导入类型。它定义了扩展系统的全部契约:事件结构、API 接口、工具定义、配置类型、上下文接口等。

导入分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode } from "@earendil-works/pi-agent-core";
import type { Api, AssistantMessageEvent, AssistantMessageEventStream, Context, ImageContent, Model, OAuthCredentials, OAuthLoginCallbacks, SimpleStreamOptions, TextContent, ToolResultMessage } from "@earendil-works/pi-ai";
import type { AutocompleteItem, AutocompleteProvider, Component, EditorComponent, EditorTheme, KeyId, OverlayHandle, OverlayOptions, TUI } from "@earendil-works/pi-tui";
import type { Static, TSchema } from "typebox";
import type { Theme } from "../../modes/interactive/theme/theme.ts";
import type { BashResult } from "../bash-executor.ts";
import type { CompactionPreparation, CompactionResult } from "../compaction/index.ts";
import type { EventBus } from "../event-bus.ts";
import type { ExecOptions, ExecResult } from "../exec.ts";
import type { ReadonlyFooterDataProvider } from "../footer-data-provider.ts";
import type { KeybindingsManager } from "../keybindings.ts";
import type { CustomMessage } from "../messages.ts";
import type { ModelRegistry } from "../model-registry.ts";
import type { SessionManager, ReadonlySessionManager, SessionEntry, BranchSummaryEntry, CompactionEntry } from "../session-manager.ts";
import type { SlashCommandInfo } from "../slash-commands.ts";
import type { SourceInfo } from "../source-info.ts";
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
import type { BashOperations } from "../tools/bash.ts";
import type { EditToolDetails } from "../tools/edit.ts";
import type { BashToolDetails, BashToolInput, EditToolInput, FindToolDetails, FindToolInput, GrepToolDetails, GrepToolInput, LsToolDetails, LsToolInput, ReadToolDetails, ReadToolInput, WriteToolInput } from "../tools/index.ts";

抽象依赖:types.ts 引用了 4 个外部包(pi-agent-corepi-aipi-tuitypebox)和约 15 个内部模块。这体现了扩展系统位于应用架构的上层 — 它需要理解底层所有子系统的类型才能提供统一的扩展 API。

外部包类型逐项详解

下面的表格展示了每个外部类型的具体使用位置功能含义,以及它们在扩展生命周期中的流转路径。

@earendil-works/pi-agent-core — Agent 核心包导入项:

类型在扩展系统中的具体使用位置作用描述
AgentMessageContextEvent.messages, AgentEndEvent.messages, MessageEndEvent.message, ExtensionAPI.sendUserMessage()Agent 循环中的核心消息单元。包含 role(user/assistant/tool)、content、tool_calls 等字段。扩展可以通过 context 事件修改消息列表
AgentToolResult<TDetails>ToolDefinition.execute() 返回值工具执行的结果包装类型,包含 content(TextContent/ImageContent 数组)、isError(boolean)、details(TDetails 工具特定详情)三个字段
AgentToolUpdateCallback<TDetails>ToolDefinition.execute()onUpdate 参数工具执行过程中推送增量结果的回调。扩展在长时间运行的工具(如文件搜索、API 调用)中可以多次调用它
ThinkingLevelThinkingLevelSelectEvent.level, ExtensionAPI.getThinkingLevel() / setThinkingLevel()模型思考级别的枚举。控制模型在生成回答前进行内部推理的深度。典型值:none、low、medium、high
ToolExecutionModeToolDefinition.executionMode字面量联合类型 `“sequential”
AgentToolwrapper.ts 的输出目标agent-core 的可执行工具接口。ToolDefinition 经过 wrapper.ts 包装后最终成为 AgentTool,被 agent-core 调度执行

在扩展系统中的流转路径

ToolDefinition.execute() → AgentToolResult
    ↓ (wrapper.ts 包装)
AgentTool ←── ToolDefinition 适配
    ↓ (agent-core 调度执行)
AgentMessage ←── LLM 调用结果中嵌入

@earendil-works/pi-ai — AI Provider 包导入项:

类型在扩展系统中的具体使用位置作用描述
ApiProviderConfig.api, ProviderModelConfig.api标识 API 协议类型的字符串。常见值:"anthropic-messages""openai-chat""openai-responses"
Model<Api>ExtensionContext.model, ModelSelectEvent.model/previousModel, ExtensionAPI.setModel()模型的完整配置对象。包含 id、name、baseUrl、contextWindow、maxTokens、cost(input/output/cacheRead/cacheWrite)、input(支持的输入类型 text/image)等
AssistantMessageEventMessageUpdateEvent.assistantMessageEventAI 流式响应中的增量事件联合体。包含 content_block_delta(文本 delta)、thinking_delta(思维链 delta)、content_block_start/stop 等多种变体
AssistantMessageEventStreamProviderConfig.streamSimple() 返回值AsyncIterable<AssistantMessageEvent> 异步可迭代流。扩展实现自定义 API 时将任意协议的流式响应包装为此类型
ContextProviderConfig.streamSimple() 的 context 参数AI 会话上下文,包含消息历史、系统提示、模型配置、中止信号等。由系统传入,扩展不需要构造
ImageContentInputEvent.images, BeforeAgentStartEvent.images, ExtensionAPI.sendUserMessage() content多模态消息中的图片内容块。结构为 `{ type: “image”, source: { type: “base64”
TextContentToolResultEvent.content, InputEvent 文本内容消息中的纯文本内容块,结构为 { type: "text", text: string }
ToolResultMessageTurnEndEvent.toolResultsLLM 接收的工具结果消息格式。包含 role: “tool”、tool_call_id、content(执行结果)等字段
OAuthCredentialsProviderConfig.oauth.login() 返回值OAuth 认证凭据存储对象。包含 access(访问令牌)、refresh(刷新令牌)、expiresAt(过期时间)等,由系统加密持久化
OAuthLoginCallbacksProviderConfig.oauth.login(callbacks) 参数OAuth 登录流程回调集。openUrl(url) 打开浏览器;pollForCallback() 轮询等待回调
SimpleStreamOptionsProviderConfig.streamSimple() 可选参数简单流处理器的配置选项,包含 signal(AbortSignal 中止信号)、onEvent 等

在扩展系统中的流转路径

ProviderConfig.streamSimple → AssistantMessageEventStream
    ↓ (扩展注册 provider)
ModelRegistry ──持有──→ Model<Api>[] 列表
    ↓ (用户选择模型)
ExtensionContext.model ──→ Model<Api> | undefined
    ↓ (LLM 调用)
AssistantMessageEventStream ──stream──→ AssistantMessageEvent

@earendil-works/pi-tui — 终端 UI 组件包导入项:

类型在扩展系统中的具体使用位置作用描述
ComponentToolDefinition.renderCall/renderResult 返回值;setWidget/setFooter/setHeader/custom 的工厂返回值终端 UI 组件的基接口,包含 render() 渲染方法、onMount/onUnmount 生命周期钩子。所有 UI 构建的最终产物
TUI组件工厂的参数,如 setFooter((tui, theme, data) => Component)TUI 核心接口,扩展可通过它操作终端屏幕、注册图层、控制渲染循环、获取终端尺寸
KeyIdExtensionAPI.registerShortcut(shortcut)ExtensionShortcut.shortcut标准化键盘按键标识符。格式如 "ctrl-p""alt-enter""escape" 等,全部小写
AutocompleteItemRegisteredCommand.getArgumentCompletions() 返回值元素自动补全建议项,包含 label(显示文本)、description(右侧描述)、insertText(选中后插入文本)等字段
AutocompleteProviderAutocompleteProviderFactory 输入/输出类型自动补全提供者接口(getSuggestions(prefix) 方法)。扩展可通过 addAutocompleteProvider() 包装它叠加自定义补全
EditorComponentEditorFactory 返回值编辑器组件接口,继承 Component。额外提供 getText()、setText()、handleInput(data) 等文本编辑方法
EditorThemeEditorFactory(tui, theme, keybindings) 参数编辑器的主题配置,包含边框样式(border)、选中颜色(selectionColor)、自动补全颜色(completionColor)等
OverlayHandleExtensionUIContext.custom()onHandle 回调参数浮层控制器,提供 close() 关闭方法、update(options) 更新方法,用于动态控制浮层
OverlayOptionsExtensionUIContext.custom()overlayOptions浮层的位置和大小配置。支持静态对象或动态函数 () => OverlayOptions(用于跟随光标等动态场景)

组件工厂的数据流

扩展调用 setWidget(key, factory)
    ↓
TUI 系统在适当时机调用 factory(tui, theme)
    ↓
Component 对象 { render(), onMount(), onUnmount(), dispose()? }
    ↓
TUI 渲染引擎调用 render() 输出到终端

typebox — 运行时类型校验库导入项:

类型在扩展系统中的具体使用位置作用描述
Static<T>ToolDefinition.prepareArguments() 的返回值类型标注 Static<TParams>元编程类型工具:从 TypeBox schema T 推断对应的静态 TypeScript 类型。如 Static<typeof Type.Object({ name: Type.String() })> 结果是 { name: string }
TSchemaToolDefinition 泛型约束 TParams extends TSchema所有 TypeBox schema 的基接口,确保 ToolDefinition.parameters 字段是合法的 schema

使用示例(在扩展代码中):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { Type, type Static } from "typebox";

const MyParams = Type.Object({
  name: Type.String(),
  count: Type.Optional(Type.Integer({ minimum: 1 })),
});

pi.registerTool({
  name: "my-tool",
  label: "My Tool",
  description: "A custom tool with validated parameters",
  parameters: MyParams,      // TSchema 约束确保合法性
  execute: (id, params: Static<typeof MyParams>, signal, onUpdate, ctx) => {
    // TypeScript 自动推断:params.name 是 string
    // TypeScript 自动推断:params.count 是 number | undefined
    // 运行时 TypeBox 校验确保传入参数符合 schema
  },
});

内部模块引用分析

types.ts 中的内部模块导入提供了对其他 Pi Coding Agent 子系统的类型引用:

源文件导入类型在扩展系统中的具体使用该类型的含义
../../modes/interactive/theme/theme.tsThemeExtensionUIContext.themesetTheme()/getTheme()完整主题配置,包含颜色、边框、组件样式等
../event-bus.tsEventBusExtensionAPI.events 的类型扩展间通信的事件总线,提供 on/emit/off 方法
../exec.tsExecOptions, ExecResultExtensionAPI.exec() 的参数和返回值系统命令执行的选项(cwd、timeout、env)和结果(exitCode、stdout、stderr)
../session-manager.tsSessionManager, ReadonlySessionManager, SessionEntry, BranchSummaryEntry, CompactionEntryExtensionContext.sessionManagerSessionCompactEvent 事件负载会话存储与导航系统,管理对话历史的读写、分支、压缩、持久化
../model-registry.tsModelRegistryExtensionContext.modelRegistry模型注册中心,管理所有 provider 注册的模型列表
../keybindings.tsKeybindingsManager, AppKeybindingsetEditorComponent 的 keybindings 参数;重导出类型快捷键管理系统,管理绑定、冲突解决、事件分发
../system-prompt.tsBuildSystemPromptOptionsExtensionCommandContext.getSystemPromptOptions()BeforeAgentStartEvent 字段系统提示构建选项,包含 cwd、active tools、session info 等
../source-info.tsSourceInfoExtension.sourceInfoRegisteredTool.sourceInfo来源元数据:标记扩展/工具来自哪个文件,是内置还是用户安装
../messages.tsCustomMessageExtensionAPI.sendMessage() 的消息体自定义消息结构:customType(类型标识)、content(内容)、display(显示选项)、details(附加数据)
../tools/index.ts各内置工具详情和输入类型ToolCallEvent/ToolResultEvent 的细化类型字段内置工具的类型声明,用于事件处理器中对特定工具的类型细化
../tools/bash.tsBashOperationsUserBashEventResult.operationsbash 操作抽象接口,提供 exec/execScript/spawn 等方法
../compaction/index.tsCompactionPreparation, CompactionResultSessionBeforeCompactEvent 准备数据;SessionCompactEvent 结果会话压缩的准备数据和结果,包含条目列表、摘要文本等
../footer-data-provider.tsReadonlyFooterDataProvidersetFooter() 中传递给组件工厂的参数底部栏数据提供者,提供 git 分支、扩展状态等只读数据
../bash-executor.tsBashResultUserBashEventResult.resultbash 命令执行的完整结果:exitCode、stdout、stderr、cwd

多层抽象:内部模块引用体现了扩展系统与内部实现解耦的设计。扩展系统不直接依赖这些模块的实现,只依赖它们的类型接口。这使得两者可以独立演进。

逐段解读

段 1:UI 上下文 (ExtensionUIContext)

1
2
3
4
5
6
7
export interface ExtensionUIContext {
  select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
  confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise<boolean>;
  input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
  notify(message: string, type?: "info" | "warning" | "error"): void;
  // ... ~30 个方法
}

功能:定义了扩展与用户交互的全部 UI 基元。每个运行模式(TUI、RPC、print)提供自己的实现。

关键方法解析:

方法签名功能说明
select(title, options, opts) => Promise<string | undefined>显示选择器,返回用户选中的选项
confirm(title, message, opts) => Promise<boolean>确认对话框
input(title, placeholder, opts) => Promise<string | undefined>文本输入框
notify(message, type) => void推送通知(info/warning/error)
onTerminalInput(handler) => () => void监听原始终端输入,返回取消函数
setWidget两个重载:字符串数组版 / 组件工厂版在编辑器上方或下方显示组件
setFooter(factory | undefined) => void设置自定义底部组件
setHeader(factory | undefined) => void设置自定义顶部组件
custom(factory, options) => Promise<T>显示自定义组件,支持 overlay 模式
addAutocompleteProvider(factory) => void叠加自动补全行为
setEditorComponent(factory | undefined) => void替换编辑器组件(如 Vim 编辑器)

设计模式ExtensionUIContext 使用了策略模式 — 统一接口,不同模式提供不同实现。noOpUIContext(在 runner.ts 中)是空操作的缺省实现。

段 2:扩展上下文 (ExtensionContext)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export interface ExtensionContext {
  ui: ExtensionUIContext;
  mode: ExtensionMode;         // "tui" | "rpc" | "json" | "print"
  hasUI: boolean;
  cwd: string;
  sessionManager: ReadonlySessionManager;
  modelRegistry: ModelRegistry;
  model: Model<any> | undefined;
  isIdle(): boolean;
  isProjectTrusted(): boolean;
  signal: AbortSignal | undefined;
  abort(): void;
  hasPendingMessages(): boolean;
  shutdown(): void;
  getContextUsage(): ContextUsage | undefined;
  compact(options?: CompactOptions): void;
  getSystemPrompt(): string;
}

抽象与实现ExtensionContext 定义了扩展在事件处理中能访问的只读环境。它的所有 getter 和方法最终都委托到 ExtensionRunner 的内部函数(如 getModelisIdleFn),这些函数通过 bindCore() 注入。

ExtensionCommandContext 继承了 ExtensionContext 并额外提供:

  • getSystemPromptOptions() — 系统提示构建选项
  • waitForIdle() — 等待 agent 空闲
  • newSession()fork()navigateTree()switchSession()reload() — 会话控制方法

安全设计:命令上下文只能在用户发起的命令中安全使用,因为它包含会话控制能力。

段 3:工具定义 (ToolDefinition)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any> {
  name: string;
  label: string;
  description: string;
  promptSnippet?: string;         // 可选:系统提示中的简短描述
  promptGuidelines?: string[];     // 可选:系统提示指南中的要点
  parameters: TParams;             // TypeBox 模式(运行时校验)
  renderShell?: "default" | "self";
  prepareArguments?: (args: unknown) => Static<TParams>;
  executionMode?: ToolExecutionMode;  // "sequential" | "parallel"
  execute(toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<TDetails>>;
  renderCall?: (args, theme, context) => Component;
  renderResult?: (result, options, theme, context) => Component;
}

语法要点 — 泛型约束:

  • TParams extends TSchema — 参数必须符合 TypeBox schema 类型
  • TDetails = unknown — 工具执行的详细结果类型(默认为 unknown)
  • TState = any — 渲染状态类型

defineTool() 是一个类型辅助函数,用于在变量赋值时保留参数类型推断:

1
2
3
export function defineTool<TParams extends TSchema, TDetails = unknown, TState = any>(
  tool: ToolDefinition<TParams, TDetails, TState>,
): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition { ... }

功能:不使用 defineTool() 时,TypeScript 会将放到数组中的 ToolDefinitionparams 收窄为 unknowndefineTool() 返回交叉类型 & AnyToolDefinition,既保留具体类型信息,又兼容 any 参数位置。

段 4:事件体系

事件系统定义了三层结构:

4.1 事件类型 — 按领域分组:

事件组事件列表触发时机
Startup/Resourceproject_trust, resources_discover启动时、重载时
Sessionsession_start, session_before_switch, session_before_fork, session_before_compact, session_compact, session_shutdown, session_before_tree, session_tree会话生命周期
Agentcontext, before_provider_request, after_provider_response, before_agent_start, agent_start, agent_endAgent 循环
Turn/Messageturn_start, turn_end, message_start, message_update, message_end每次交互回合
Tooltool_execution_start, tool_execution_update, tool_execution_end, tool_call, tool_result工具执行
Modelmodel_select, thinking_level_select模型切换
Inputinput用户输入
User Bashuser_bash用户执行 bash 命令

4.2 事件结果类型 — 每个事件有对应的结果类型:

1
2
3
4
5
6
export interface ToolCallEventResult { block?: boolean; reason?: string; }
export interface ContextEventResult { messages?: AgentMessage[]; }
export interface InputEventResult =
  | { action: "continue" }
  | { action: "transform"; text: string; images?: ImageContent[] }
  | { action: "handled" };

设计模式:事件结果的 ? 可选字段允许扩展选择性地干涉,不影响正常流程。Result 类型体现的是观察者模式的增强变体 — 观察者不仅可以观察,还可以修改或阻止事件。

4.3 类型守卫函数

1
2
export function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent { ... }
export function isToolCallEventType(toolName: string, event: ToolCallEvent): boolean { ... }

语法要点 — 函数重载 + 类型守卫:

  • 内置工具的 isToolCallEventType 不需要类型参数,自动窄化
  • 自定义工具需要显式泛型参数:isToolCallEventType<"my_tool", MyInput>("my_tool", event)
  • 重载签名确保了类型安全;实现签名使用 boolean 返回

段 5:ExtensionAPI(扩展的入口对象)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export interface ExtensionAPI {
  on(event, handler): void;                                  // 订阅事件
  registerTool(tool): void;                                  // 注册工具
  registerCommand(name, options): void;                      // 注册命令
  registerShortcut(shortcut, options): void;                  // 注册快捷键
  registerFlag(name, options): void;                          // 注册 CLI 标志
  registerMessageRenderer(customType, renderer): void;        // 注册消息渲染器
  sendMessage(message, options?): void;                        // 发送自定义消息
  sendUserMessage(content, options?): void;                    // 发送用户消息
  appendEntry(customType, data?): void;                        // 追加会话条目
  registerProvider(name, config): void;                        // 注册/覆盖 Provider
  unregisterProvider(name): void;                              // 取消注册 Provider
  events: EventBus;                                            // 扩展间事件总线
  // ... 更多方法
}

抽象与实现ExtensionAPI 是扩展作者看到的 pi 对象。它的 on()registerTool() 等注册方法将数据写入 Extension 对象的 collections(handlers Map、tools Map 等)。动作方法(sendMessage()setModel() 等)委托给共享的 ExtensionRuntime

段 6:Provider 注册类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export interface ProviderConfig {
  name?: string;
  baseUrl?: string;
  apiKey?: string;     // "$PROXY_API_KEY" — 环境变量插值
  api?: Api;
  streamSimple?: (model, context, options?) => AssistantMessageEventStream;  // 自定义 API 流处理器
  headers?: Record<string, string>;
  authHeader?: boolean;
  models?: ProviderModelConfig[];
  oauth?: { login, refreshToken, getApiKey, modifyModels? };
}

功能:允许扩展注册自定义模型 provider,支持:

  • 纯 URL 覆盖(代理转发)
  • 完整模型列表替换
  • OAuth 认证流程
  • streamSimple 自定义 API 兼容层

段 7:运行时和加载状态类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions {}
// 继承关系:runtime = 状态 + 动作

export interface Extension {
  path: string;                    // 原始路径
  resolvedPath: string;            // 解析后路径
  sourceInfo: SourceInfo;          // 来源元数据
  handlers: Map<string, HandlerFn[]>;  // 事件 -> 处理器列表
  tools: Map<string, RegisteredTool>;
  messageRenderers: Map<string, MessageRenderer>;
  commands: Map<string, RegisteredCommand>;
  flags: Map<string, ExtensionFlag>;
  shortcuts: Map<KeyId, ExtensionShortcut>;
}

抽象关系ExtensionRuntime共享单例(所有扩展共用),而 Extension按扩展实例化(每个加载的扩展有一个对象存储其注册内容)。这是关键的设计区分。


文件二:loader.ts — 加载器 (Loader Layer)

角色与位置

将 TypeScript/JavaScript 文件加载为扩展模块,创建 Extension 对象和共享的 ExtensionRuntime。不涉及事件分发或生命周期管理。

导入分析

1
2
3
4
5
6
import { createJiti } from "jiti/static";
import * as _bundledTypebox from "typebox";
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.ts";
import * as _bundledPiCodingAgent from "../../index.ts";
import { createEventBus, type EventBus } from "../event-bus.ts";
import { createSyntheticSourceInfo } from "../source-info.ts";

语法要点 — Bundled Imports 与 virtualModules:

  • import * as _bundledXxx 使用 import * as 命名空间导入,将所有导出聚合为一个对象
  • 这些前缀 _bundled 的导入必须是静态的,以便 Bun 打包时包含到二进制文件中
  • VIRTUAL_MODULES 对象映射包名到运行时对象,jiti 的 virtualModules 选项使它们在扩展代码中进行 import 时可用

逐段解读

段 1:VIRTUAL_MODULES 与 getAliases()

1
2
3
4
5
6
7
8
9
const VIRTUAL_MODULES: Record<string, unknown> = {
  typebox: _bundledTypebox,
  "typebox/compile": _bundledTypeboxCompile,
  "@earendil-works/pi-agent-core": _bundledPiAgentCore,
  "@earendil-works/pi-tui": _bundledPiTui,
  "@earendil-works/pi-ai": _bundledPiAiCompat,    // compat 是核心的超集
  "@earendil-works/pi-coding-agent": _bundledPiCodingAgent,
  "@mariozechner/pi-*": ...,                       // 旧命名空间别名
};

功能:提供两种模块解析方式:

  • Bun 二进制模式 (生产):virtualModules — 预打包的模块直接注入,无文件系统解析
  • Node.js 开发模式aliases — 通过 jiti 的 alias 选项映射到 node_modules 路径
  • 两种模式都包含旧命名空间 @mariozechner/* 的兼容映射

getAliases() 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function getAliases(): Record<string, string> {
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
  const packageIndex = path.resolve(__dirname, "../..", "index.js");
  const packagesRoot = path.resolve(__dirname, "../../../../");
  
  const resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {
    const workspacePath = path.join(packagesRoot, workspaceRelativePath);
    if (fs.existsSync(workspacePath)) { return workspacePath; }  // monorepo workspace
    return fileURLToPath(import.meta.resolve(specifier));        // fallback to node_modules
  };
  // ...
}

语法要点import.meta.resolve()

  • 返回模块说明符在运行时的绝对路径(Node.js 21+ / Bun)
  • require.resolve() 功能相似,但用于 ESM 环境

抽象与实现resolveWorkspaceOrImport() 实现了工作区优先策略 — 在 monorepo 开发环境中优先使用本地构建产物,否则回退到 node_modules

段 2:createExtensionRuntime() — 运行时骨架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
export function createExtensionRuntime(): ExtensionRuntime {
  const notInitialized = () => {
    throw new Error("Extension runtime not initialized");
  };
  const state: { staleMessage?: string } = {};
  const assertActive = () => { if (state.staleMessage) throw new Error(state.staleMessage); };

  const runtime: ExtensionRuntime = {
    sendMessage: notInitialized,    // 抛出错误的桩函数
    sendUserMessage: notInitialized,
    // ... 所有动作都是桩函数
    registerProvider: (name, config, extensionPath = "<unknown>") => {
      runtime.pendingProviderRegistrations.push({ name, config, extensionPath });
    },
    invalidate: (message) => { state.staleMessage = message; },
    // ...
  };
  return runtime;
}

设计模式Two-Phase Initialization(两阶段初始化):

  1. 加载阶段createExtensionRuntime() 创建运行时,所有动作方法都是抛出错误的桩函数
  2. 绑定阶段runner.bindCore() 用真实实现替换这些桩函数

这种方式允许扩展在加载期间安全地调用注册方法(如 registerTool()),但动作方法(如 sendMessage())只能在绑定后调用。

pendingProviderRegistrations 队列的作用:

  • 加载期间扩展调用 registerProvider() → 入队
  • bindCore() 时统一刷新 → 调用 modelRegistry.registerProvider()
  • 绑定后 registerProvider 被替换为直接调用 → 立即生效

段 3:createExtensionAPI() — API 包装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function createExtensionAPI(extension: Extension, runtime: ExtensionRuntime, cwd: string, eventBus: EventBus): ExtensionAPI {
  const api = {
    on(event: string, handler: HandlerFn): void {
      runtime.assertActive();
      const list = extension.handlers.get(event) ?? [];
      list.push(handler);
      extension.handlers.set(event, list);
    },
    registerTool(tool: ToolDefinition): void {
      runtime.assertActive();
      extension.tools.set(tool.name, { definition: tool, sourceInfo: extension.sourceInfo });
      runtime.refreshTools();
    },
    getFlag(name: string): boolean | string | undefined {
      runtime.assertActive();
      if (!extension.flags.has(name)) return undefined;  // 安全检查:只能读自己注册的
      return runtime.flagValues.get(name);
    },
    // ...
  };
}

关键安全设计:每个 api 方法调用前都会 runtime.assertActive(),检测当前扩展是否已失效(因会话替换或重载导致)。

功能边界registerTool() 写入 extension.tools Map 并调用 runtime.refreshTools() 通知系统更新;sendMessage() 直接委托给运行时;getFlag() 限制在扩展自己注册的标志范围内。

段 4:loadExtensionModule() — 模块加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
async function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken) {
  if (isCurrentCacheToken(cacheToken)) {
    const cachedFactory = extensionCache.get(extensionPath);
    if (cachedFactory) return cachedFactory;  // 缓存命中
  }

  const jiti = createJiti(import.meta.url, {
    moduleCache: false,
    ...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),
  });

  const module = await jiti.import(extensionPath, { default: true });
  const factory = module as ExtensionFactory;
  if (typeof factory !== "function") return undefined;  // 不是有效的扩展
  // ...
}

语法要点 — jiti 的使用:

  • createJiti(import.meta.url) — 创建一个能直接运行 TypeScript 的运行时
  • jiti.import(path, { default: true }) — 加载模块并提取 default 导出
  • moduleCache: false — 禁用 jiti 的模块缓存,因为 loader 有自己的缓存层
  • tryNative: false — Bun 模式下禁用原生解析,完全使用 virtualModules

段 5:扩展发现系统

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function readPiManifest(packageJsonPath: string): PiManifest | null {
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
  return pkg.pi && typeof pkg.pi === "object" ? pkg.pi as PiManifest : null;
}

function discoverExtensionsInDir(dir: string): string[] {
  // 发现规则:
  // 1. *.ts / *.js 文件 → 直接加载
  // 2. 子目录有 index.ts/index.js → 加载
  // 3. 子目录有 package.json 含 "pi.extensions" 字段 → 按清单加载
  // 不递归超过一层,复杂包必须使用 package.json 清单
}

export async function discoverAndLoadExtensions(
  configuredPaths: string[], cwd: string, agentDir: string
): Promise<LoadExtensionsResult> {
  // 优先级:
  // 1. 项目本地: cwd/.pi/extensions/
  // 2. 全局: agentDir/extensions/
  // 3. 配置路径(逐条解析)
  // 去重:Set<resolvedPath>
}

抽象与实现:扩展发现实现了三层路径合并策略:

  • 项目本地 → 全局 → 显式配置
  • 去重保护(seen Set)防止路径冲突
  • discoverExtensionsInDir() 是核心发现函数,支持文件、子目录、package.json 清单三种形式

段 6:缓存机制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let extensionCacheCwd: string | undefined;
let extensionCacheGeneration = 0;
const extensionCache = new Map<string, ExtensionFactory>();

export function clearExtensionCache(): void { extensionCacheGeneration++; }

function useExtensionCacheCwd(cwd: string): ExtensionCacheToken {
  if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) clearExtensionCache();
  // 切换工作目录 → 清空缓存
}

设计:缓存与工作目录绑定。切换工作目录自动清空缓存。extensionCacheGeneration 用作版本戳,配合 isCurrentCacheToken() 检查缓存是否有效。


文件三:runner.ts — 运行器 (Runtime Layer)

角色与位置

这是扩展系统的心脏ExtensionRunner 类管理扩展生命周期、事件分发、上下文创建、快捷键解析和工具注册汇总。

导入分析

1
2
3
4
5
6
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import type { KeyId } from "@earendil-works/pi-tui";
import { type Theme, theme } from "../../modes/interactive/theme/theme.ts";
import type { ModelRegistry } from "../model-registry.ts";
import type { SessionManager } from "../session-manager.ts";
// ... 从同一目录的 types.ts 导入 ~30 个类型

引用关系:runner.ts 大量引用 types.ts 的类型定义,但不引用 loader.ts。这意味着 runner 只关心"已经加载好的扩展",不关心它们是如何加载的。

逐段解读

段 1:快捷键冲突检测

1
2
3
4
5
const RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS = [
  "app.interrupt", "app.clear", "app.exit", "app.suspend", "tui.input.submit", ...
] as const;

type BuiltInKeyBindings = Partial<Record<KeyId, { keybinding: string; restrictOverride: boolean }>>;

功能:定义一组保留快捷键,扩展不能覆盖。这防止了扩展劫持核心功能(如 Ctrl+C 中断、提交输入等)。

buildBuiltinKeybindings()KeybindingsConfig 解析为按键查找表:

1
2
3
4
5
for (const key of keyList) {
  const existing = builtinKeybindings[normalizedKey];
  if (existing?.restrictOverride && !restrictOverride) continue;  // 保留的获胜
  builtinKeybindings[normalizedKey] = { keybinding, restrictOverride };
}

抽象与实现:当多个 action 绑定同一个键时,保留 action 优先。这通过 restrictOverride 标志实现。

段 2:ExtensionRunner 类核心结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export class ExtensionRunner {
  private extensions: Extension[];
  private runtime: ExtensionRuntime;
  private uiContext: ExtensionUIContext;        // 默认为 noOpUIContext
  private mode: ExtensionMode = "print";
  private cwd: string;
  private sessionManager: SessionManager;
  private modelRegistry: ModelRegistry;
  
  // 所有内部函数都可被外部替换(通过 bindCore/bindCommandContext)
  private getModel: () => Model<any> | undefined = () => undefined;
  private isIdleFn: () => boolean = () => true;
  // ... 约 15 个可注入函数
}

设计模式Strategy Pattern + Dependency Injection — Runner 的所有行为依赖都是可替换的私有函数字段。外部通过 bindCore()bindCommandContext()setUIContext() 注入具体实现。

段 3:bindCore() — 核心绑定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
bindCore(actions: ExtensionActions, contextActions: ExtensionContextActions, providerActions?): void {
  // 1. 将 actions 复制到共享 runtime(所有 ExtensionAPI 引用同一个 runtime 对象)
  this.runtime.sendMessage = actions.sendMessage;
  this.runtime.sendUserMessage = actions.sendUserMessage;
  // ...

  // 2. 设置 context actions(getter 函数)
  this.getModel = contextActions.getModel;
  this.isIdleFn = contextActions.isIdle;
  // ...

  // 3. 刷新 provider 注册队列
  for (const { name, config } of this.runtime.pendingProviderRegistrations) {
    this.modelRegistry.registerProvider(name, config);
  }
  this.runtime.pendingProviderRegistrations = [];

  // 4. 替换 registerProvider 为立即生效的版本
  this.runtime.registerProvider = (name, config) => {
    this.modelRegistry.registerProvider(name, config);
  };
}

关键时序

  1. 加载阶段:registerProvider() 入队 pendingProviderRegistrations
  2. 绑定阶段:bindCore() 统一刷新所有排队注册
  3. 绑定后:registerProvider() 被替换为直接调用 modelRegistry.registerProvider(),立即生效

段 4:上下文创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
createContext(): ExtensionContext {
  const runner = this;
  return {
    get ui() {
      runner.assertActive();
      return runner.uiContext;
    },
    get mode() {
      runner.assertActive();
      return runner.mode;
    },
    isIdle: () => {
      runner.assertActive();
      return runner.isIdleFn();
    },
    // ...
  };
}

语法要点 — 惰性 getter (Lazy Property Descriptors):

  • get ui()Object.defineProperty 的简写语法
  • 每次访问都重新求值,反映运行时的最新状态
  • 所有访问都经过 assertActive() 检查

createCommandContext() 的区别

  • createContext() 返回 ExtensionContext — 用于事件处理和工具执行
  • createCommandContext() 使用 Object.defineProperties() + Object.getOwnPropertyDescriptors() 继承 createContext() 的所有惰性 getter,再添加命令专用方法(waitForIdle()newSession() 等)
1
2
3
4
5
6
7
8
9
createCommandContext(): ExtensionCommandContext {
  const context = Object.defineProperties(
    {},
    Object.getOwnPropertyDescriptors(this.createContext()),
  ) as ExtensionCommandContext;
  context.waitForIdle = () => { ... };
  context.newSession = (options) => { ... };
  // ...
}

语法要点Object.getOwnPropertyDescriptors()

  • 复制 createContext() 返回对象的所有属性描述符(包括 getter)
  • 这是正确复制惰性 getter 的唯一方式;普通展开 {...obj} 会求值 getter

段 5:事件分发系统

通用事件分发(emit()):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
  const ctx = this.createContext();
  for (const ext of this.extensions) {
    const handlers = ext.handlers.get(event.type);
    if (!handlers) continue;
    for (const handler of handlers) {
      const handlerResult = await handler(event, ctx);
      if (this.isSessionBeforeEvent(event) && handlerResult?.cancel) return;
    }
  }
}

设计模式责任链模式 — 每个扩展依次处理事件,session_before_* 事件在处理返回 cancel 时提前终止。

专用事件分发(类型安全版本):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
async emitMessageEnd(event: MessageEndEvent): Promise<AgentMessage | undefined> {
  const ctx = this.createContext();
  let currentMessage = event.message;
  let modified = false;

  for (const ext of this.extensions) {
    for (const handler of ext.handlers.get("message_end") ?? []) {
      const currentEvent: MessageEndEvent = { ...event, message: currentMessage };
      const handlerResult = await handler(currentEvent, ctx) as MessageEndEventResult;
      if (handlerResult?.message) {
        if (handlerResult.message.role !== currentMessage.role) {
          this.emitError({...});  // 角色不变检查
          continue;
        }
        currentMessage = handlerResult.message;
        modified = true;
      }
    }
  }
  return modified ? currentMessage : undefined;
}

功能message_end 处理器可以修改最终消息内容。要求返回的消息必须保持相同角色(role),防止注入攻击。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
async emitToolResult(event: ToolResultEvent): Promise<ToolResultEventResult | undefined> {
  // 对原事件对象展开(不会污染原始对象)
  const currentEvent: ToolResultEvent = { ...event };
  for (const handler of handlers) {
    const handlerResult = await handler(currentEvent, ctx) as ToolResultEventResult;
    if (handlerResult.content !== undefined) currentEvent.content = handlerResult.content;
    if (handlerResult.details !== undefined) currentEvent.details = handlerResult.details;
    if (handlerResult.isError !== undefined) currentEvent.isError = handlerResult.isError;
  }
  // 返回修改后的完整结果
}

链式修改tool_result 处理器可以逐步修改结果,每个处理器在之前处理器修改的基础上继续。多个扩展可以合作修改同一个工具结果。

1
2
3
4
5
6
7
8
9
async emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {
  for (const handler of handlers) {
    const handlerResult = await handler(event, ctx);
    if (handlerResult) {
      result = handlerResult as ToolCallEventResult;
      if (result.block) return result;  // block 立即终止
    }
  }
}

阻断机制tool_call 处理器可以返回 { block: true } 阻止工具执行。event.input 是可变引用,处理器可以原地修改参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
async emitInput(text, images, source, streamingBehavior?): Promise<InputEventResult> {
  let currentText = text;
  let currentImages = images;
  for (const handler of ext.handlers.get("input") ?? []) {
    const event: InputEvent = { type: "input", text: currentText, images: currentImages, source, streamingBehavior };
    const result = await handler(event, ctx) as InputEventResult;
    if (result?.action === "handled") return result;      // 短路:扩展完全处理了输入
    if (result?.action === "transform") {                  // 转换:修改输入文本
      currentText = result.text;
      currentImages = result.images ?? currentImages;
    }
  }
  return currentText !== text
    ? { action: "transform", text: currentText, images: currentImages }
    : { action: "continue" };
}

三态结果:输入事件支持三种结果:

  • "continue" — 扩展未修改输入,按原样处理
  • "transform" — 扩展修改了输入文本或图片
  • "handled" — 扩展完全处理了输入,后续停止处理 “handled” 会短路径终止,“transform” 会链式累积修改。

段 6:工具注册管理

getAllRegisteredTools() — 将各扩展工具按名称去重合并:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
getAllRegisteredTools(): RegisteredTool[] {
  const toolsByName = new Map<string, RegisteredTool>();
  for (const ext of this.extensions) {
    for (const tool of ext.tools.values()) {
      if (!toolsByName.has(tool.definition.name)) {
        toolsByName.set(tool.definition.name, tool);  // 先注册者获胜
      }
    }
  }
  return Array.from(toolsByName.values());
}

设计决策:同名工具先注册者获胜。这意味着内置工具优先于扩展工具,第一个加载的扩展优先于后续加载的。

段 7:命令去重

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private resolveRegisteredCommands(): ResolvedCommand[] {
  const counts = new Map<string, number>();
  for (const ext of this.extensions) {
    for (const command of ext.commands.values()) {
      counts.set(command.name, (counts.get(command.name) ?? 0) + 1);
    }
  }
  // 重名命令自动添加数字后缀:command:1, command:2
  return commands.map((command) => {
    const invocationName = counts.get(command.name) > 1
      ? `${command.name}:${occurrence}`
      : command.name;
    // ...
  });
}

抽象与实现:当多个扩展注册同名命令时,invocationName 自动生成带序号的后缀(如 mycommand:1mycommand:2),避免冲突。

段 8:失效机制 (Stale Invalidation)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
invalidate(message?: string): void {
  if (!this.staleMessage) {
    this.staleMessage = message;
    this.runtime.invalidate(message);  // 传播到共享 runtime
  }
}

private assertActive(): void {
  if (this.staleMessage) {
    throw new Error(this.staleMessage);
  }
}

功能:会话切换、fork、重载后,旧的 ExtensionRunner 实例被标记为 stale。所有后续操作抛出清晰的错误,防止使用已过期的上下文。错误消息指导用户将工作移到 withSession 回调中。


文件四:wrapper.ts — 包装器 (Adapter Layer)

角色与位置

将扩展定义的 ToolDefinition 转换为 agent-core 可执行的 AgentTool。这是适配器层的唯一文件,只有 30 行。

完整代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { wrapToolDefinition, wrapToolDefinitions } from "../tools/tool-definition-wrapper.ts";
import type { ExtensionRunner } from "./runner.ts";
import type { RegisteredTool } from "./types.ts";

export function wrapRegisteredTool(registeredTool: RegisteredTool, runner: ExtensionRunner): AgentTool {
  return wrapToolDefinition(registeredTool.definition, () => runner.createContext());
}

export function wrapRegisteredTools(registeredTools: RegisteredTool[], runner: ExtensionRunner): AgentTool[] {
  return wrapToolDefinitions(
    registeredTools.map((registeredTool) => registeredTool.definition),
    () => runner.createContext(),
  );
}

逐行解读

代码含义
1-2导入 AgentToolagent-core 的类型,表示 LLM 可调用的工具
3导入 wrapToolDefinitiontool-definition-wrapper.ts 中的通用包装函数
4-5导入 ExtensionRunnerRegisteredTool本模块的类型依赖
7-9wrapRegisteredTool()将单个 RegisteredTool 包装为 AgentTool。第二个参数是一个惰性上下文工厂 () => runner.createContext()
11-16wrapRegisteredTools()批量包装。调用 wrapToolDefinitions()(带 s 的复数版本),传入定义数组和共享工厂

关键设计 — 惰性上下文工厂:

  • () => runner.createContext() 不是直接传递上下文对象,而是传递一个在工具执行时才调用的工厂
  • 这确保了 ExtensionContext 在工具实际运行时反映最新状态(如当前工作目录、model、信号等)
  • 也避开了扩展生命周期问题:即使 runner 状态在注册后发生了变化,执行时仍能访问最新状态

引用与被引用

  • 引用 runner.tsExtensionRunner — 需要 runner 的 createContext() 方法
  • 引用 types.tsRegisteredTool — 包装器的输入类型
  • 引用 ../tools/tool-definition-wrapper.ts — 实际的包装逻辑
  • 被引用 index.ts 重导出两个函数

抽象层次:wrapper.ts 是非常薄的一层适配,核心逻辑在 tool-definition-wrapper.ts 中。这里只做 “如何获取上下文” 的决策。


文件五:index.ts — 入口 (Barrel Layer)

角色与位置

作为包的公共入口点,统一重导出所有需要暴露的类型和函数。这是唯一的公共 API 表面。

代码结构分析

重导出 loader.ts 的函数:

1
2
3
4
5
6
export {
  createExtensionRuntime,
  discoverAndLoadExtensions,
  loadExtensionFromFactory,
  loadExtensions,
} from "./loader.ts";

注意:loader.ts 的 loadExtensionModulecreateExtensionAPIloadExtensionsInternal 等内部函数未导出,对包使用者不可见。

重导出 runner.ts 的类和类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export {
  ExtensionRunner,
} from "./runner.ts";
export type {
  ExtensionErrorListener,
  ForkHandler,
  NavigateTreeHandler,
  NewSessionHandler,
  SwitchSessionHandler,
} from "./runner.ts";

重导出 wrapper.ts 的函数:

1
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.ts";

重导出 types.ts:

这是最庞大的部分,约 70 个类型重导出,涵盖:

  • 事件类型(SessionStartEvent, AgentStartEvent, ToolCallEvent 等)
  • 结果类型(ContextEventResult, ToolCallEventResult 等)
  • 上下文类型(ExtensionContext, ExtensionCommandContext, ExtensionUIContext 等)
  • API 类型(ExtensionAPI, ExtensionFactory 等)
  • 配置类型(ProviderConfig, ProviderModelConfig 等)
  • 类型守卫(isBashToolResult, isToolCallEventType 等)

额外导出:

1
2
export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.ts";
export type { SourceInfo } from "../source-info.ts";

设计:这些类型本不属于扩展系统,但由于扩展 API 中引用了它们(如 ExtensionAPI.getCommands() 返回 SlashCommandInfo[]),所以在这里重导出,方便扩展作者一次性导入所有需要的类型。

导出的分层结构

index.ts
├── 类型定义 (types.ts)         → 约 70 个 types
├── 加载器函数 (loader.ts)      → 4 个导出函数
├── 运行器类 (runner.ts)        → ExtensionRunner (class) + 5 个类型
├── 包装器函数 (wrapper.ts)     → 2 个导出函数
├── 额外类型 (../slash-commands.ts, ../source-info.ts) → 2 个 type
└── 类型守卫 (types.ts)         → 8 个类型守卫函数

关系总结

引用链全景

                           ┌─────────────────────────────────────────┐
                           │            types.ts                     │
                           │  (所有类型定义:事件、API、上下文、工具)     │
                           └────────────┬────────────┬───────────────┘
                                        │            │
                    ┌───────────────────┘    ┌────────┘
                    ▼                        ▼
            ┌───────────────┐       ┌─────────────────┐
            │  loader.ts    │       │  runner.ts      │
            │  (加载扩展)    │       │  (生命周期管理)   │
            └───────────────┘       └────────┬────────┘
                                             │
                                             ▼
                                     ┌─────────────────┐
                                     │  wrapper.ts     │
                                     │  (工具适配)      │
                                     └────────┬────────┘
                                              │
                    ┌─────────────────────────┘
                    ▼
            ┌───────────────────┐
            │   index.ts        │
            │   (公共 API 入口)  │
            └───────────────────┘

核心数据流

[扩展 .ts 文件]
      │
      ▼  jiti.import()
loader.ts ────→ ExtensionFactory (default export 的函数)
      │          │
      │          ▼  factory(api)
      │     Extension 对象 (handlers, tools, commands, etc.)
      │          │
      ▼          ▼
ExtensionRuntime (共享单例, pendingProviderRegistrations 队列)
      │
      ▼  bindCore()
runner.ts ────→ 替换 runtime 桩函数 → 刷新 provider 队列
      │
      ├── createContext() → ExtensionContext (事件处理用)
      ├── createCommandContext() → ExtensionCommandContext (命令用)
      ├── emit(event) → 遍历扩展的 handlers 分派事件
      ├── getAllRegisteredTools() → 汇总所有工具(去重)
      └── getShortcuts() → 汇总快捷键(冲突检测)
      │
      ▼  wrapRegisteredTool()
wrapper.ts ────→ AgentTool (可被 agent-core 调用)

关键设计模式总结

模式位置说明
策略模式ExtensionUIContext不同运行模式(TUI/RPC/print)提供不同 UI 实现
两阶段初始化createExtensionRuntime() + bindCore()加载期用桩函数,绑定后替换为真实实现
责任链模式emit() 系列方法所有扩展依次处理事件,可提前终止
观察者模式ExtensionAPI.on()扩展订阅事件,runner 分发
适配器模式wrapper.tsToolDefinition → AgentTool 的适配转换
惰性求值createContext() 的 getter上下文属性在访问时求值,反映最新状态
工厂模式createExtensionAPI()为每个扩展创建独立的 API 包装

抽象层次

高抽象层 ─── index.ts (公共 API 表面)
                │
            runner.ts (事件分发、上下文创建、生命周期)
                │
            loader.ts (模块解析、扩展发现、缓存)
                │
            wrapper.ts (工具适配桥接)
                │
低抽象层 ─── types.ts (全部类型契约)

安全机制

安全机制位置说明
Stale Invalidationrunner.ts + loader.ts会话切换后自动失效旧实例
assertActive()整个 API 表面每次操作前检查有效性
快捷键冲突检测runner.ts保留快捷键阻止覆盖(app.interrupt 等)
命令重名自动编号runner.ts同名命令生成 name:n 后缀避免冲突
角色检查emitMessageEnd()防止扩展修改消息角色
Flag 自检loader.ts createExtensionAPI()只能读自己注册的标志

附录:文件一览

文件路径行数角色
types.tsextensions/types.ts1615全部类型定义
loader.tsextensions/loader.ts~500模块加载与发现
runner.tsextensions/runner.ts~750生命周期与事件分发
wrapper.tsextensions/wrapper.ts~30工具适配
index.tsextensions/index.ts~80公共入口

文档生成时间:2026-06-27 分析对象:F:\Pi\packages\coding-agent\src\core\extensions/