把示例 block-destructive-shell.ts.example 复制成 Extension .ts 文件并放进项目目录后,
它为什么能在 Tool 执行前工作?
从文件到运行行为,至少跨越四层:
Package/Settings 解析资源
→ Loader 导入 Module 并执行 Factory
→ ExtensionRunner 收集注册结果
→ AgentSession 绑定 Core Actions 与 Tool Registry
1. 总览:文件不是 Runtime
flowchart LR
P["Extension Path"] --> M["loadExtensionModule"]
M --> F["Extension Factory"]
F --> A["ExtensionAPI"]
A --> E["Extension<br/>handlers/tools/..."]
E --> R["ExtensionRunner"]
R --> B["AgentSession Bindings"]
每个阶段的产物不同:
| 阶段 | 输入 | 输出 |
|---|---|---|
| 资源解析 | Settings、Package、CLI Path | 有序 Extension Paths + Metadata |
| Module 加载 | 文件绝对路径 | Factory Function |
| Factory 执行 | Factory + API | Extension 注册容器 |
| Runtime 组装 | Extensions + Shared Runtime | ExtensionRunner |
| Core 绑定 | Runner + AgentSession | 可执行 Actions、Events、Tools |
2. 两条“发现”路径不要混为一谈
Loader 导出 discoverAndLoadExtensions()。它直接扫描:
- 项目
.pi/extensions; - 用户
agentDir/extensions; - 显式配置路径。
但当前 coding-agent 的完整 ResourceLoader.reload() 路径先调用 PackageManager.resolve(),
把 Settings、Package、Trust、Enable/Disable 和 CLI Source 解析成资源,再调用
loadExtensionsCached()。
flowchart TD
A["直接 Loader Helper"] --> D["discoverAndLoadExtensions"]
D --> L["loadExtensions"]
S["实际 ResourceLoader"] --> P["PackageManager.resolve"]
P --> X["有序 Enabled Paths"]
X --> C["loadExtensionsCached"]
前者适合独立使用 Loader;后者才保留完整 Package SourceInfo 和项目配置语义。
3. Loader Helper 怎样扫描
discoverExtensionsInDir() 只看一层:
extensions/
├── one.ts ← 加载
├── two.js ← 加载
├── three/
│ └── index.ts ← 加载
└── package/
└── package.json ← 读取 pi.extensions
规则:
- 直接
.ts/.js文件; - 子目录中的
index.ts优先于index.js; - 子目录
package.json的pi.extensions优先; - 不递归无限扫描;
- 复杂目录必须通过 Manifest 声明入口。
不存在或无法读取的自动目录返回空列表,而不是抛出终止整个加载的异常。
4. PackageManager 的生产扫描
完整 Resource Loader 使用 collectAutoExtensionEntries()。它比 Loader Helper 多处理:
- 先检查 Extension Root 自身的
package.json#pi.extensions或index.ts/index.js; - 若 Root 没有入口,再扫描一层直接文件和子目录入口;
- 读取
.gitignore、.ignore、.fdignore; - 跳过 Dot Directory 与
node_modules; - 对 Symbolic Link 先
stat,再判断 File/Directory; - 目录中直接发现的文件只接收
.ts、.js。Manifest 中的pi.extensions入口只检查是否 存在,不按扩展名过滤;只有至少解析出一个存在入口时才短路,否则继续回退到index.ts/index.js。
flowchart TD
D["extension root"] --> R{"root manifest/index?"}
R -->|有| E["只返回声明入口"]
R -->|无| I["建立 ignore matcher"]
I --> S["扫描一层"]
S --> F["直接 ts/js"]
S --> C["子目录 manifest/index"]
所以直接 Helper 的规则用于理解 Loader 自包含能力,PackageManager 的规则才是当前完整产品 路径。
5. Resource Loader 先建立加载顺序
ResourceLoader.reload():
- 必要时清 Extension Module Cache;
- 处理 Project Trust Bootstrap;
- Reload Settings;
- 让 Package Manager 解析资源;
- 过滤 Disabled Resource;
- 解析 CLI Additional Extension Sources;
- 合并成有序 Path;
- 调用
loadFinalExtensionSet()。
noExtensions 关闭常规 Package/Settings/Auto Resource,但临时 CLI/Additional Extension
Path 与宿主传入的 Inline Factory 仍可进入。这些是显式调用者选择的例外,不要把它理解成
绝对“什么 Extension 都不加载”。
6. 谁排在前面
PackageManager 用 Rank 排序,Resource Loader 再把 CLI/Additional Path 放到最前。最终 优先级从高到低是:
- CLI / Additional Extension Source;
- Project Settings;
- Project Auto-discovered;
- User Settings;
- User Auto-discovered;
- Package Resource;
- Inline Factory 最后追加。
Tool/Flag 跨 Extension First Wins,因此这个顺序会决定 Winner。相同 Path 还会按 Canonical Path 去重。
7. Project Trust 为什么需要两阶段
在需要询问 Trust 时,Resource Loader 先把项目标记为不可信,加载:
- 用户/全局 Extension;
- 临时 CLI Extension;
- Inline Factory。
它们可以参与 project_trust 决策。用户确认后,再加载项目 Extension。
第二阶段复用已经加载的路径和同一个 Runtime,只导入剩余 Path,最后恢复目标顺序。这避免 全局 Extension Factory 为一次 Trust 决策重复执行。
8. Module 怎样被导入
loadExtensionModule() 为每个 Path 创建 Jiti:
const module = await jiti.import(extensionPath, {
default: true,
});
Jiti 允许直接加载 TypeScript。随后检查默认导出是否为 Function;不是则返回加载错误:
Extension does not export a valid factory function
Node/开发环境通过 Alias 解析 Pi Package;编译后的 Bun Binary 使用 Virtual Modules。 课程不引入 TUI 使用方式,但 Loader 的兼容模块表仍包含该包,这是源码事实。
9. Cache 到底缓存什么
extensionCache 缓存的是:
resolved extension path → ExtensionFactory
不是已经执行完成的 Extension Object。
Cache Token 同时包含:
- Resolved CWD;
- Cache Generation。
CWD 变化会清 Cache;clearExtensionCache() 也会增加 Generation。Jiti 自身配置
moduleCache: false,外层 Cache 决定是否复用 Factory。
10. SourceInfo 何时出现
createExtension() 先产生 Synthetic SourceInfo:
- 普通文件默认 Source
local; <inline:...>一类虚拟路径从标签推断 Source;- 普通文件 BaseDir 是 Resolved Path 的 Directory。
完整 Resource Loader 随后调用 applyExtensionSourceInfo(),用 Package Manager 收集的
Metadata 覆盖/补全来源。这就是为什么仅看 Loader 阶段的 Synthetic Source,可能还看不到
最终 npm、Git、Scope 和 Origin。
11. 创建空 Extension 容器
每个文件对应一个 Extension Object:
path / resolvedPath / sourceInfo
handlers: Map
tools: Map
commands: Map
flags: Map
shortcuts: Map
messageRenderers: Map
entryRenderers: Map
Factory 调用的 pi.on()、registerTool() 等,本质是在这些 Map 中写入注册结果。
同一个 Extension 内重复注册同名 Tool、Command 或 Flag,会由各自 Map 后写覆盖前写; Handler 则 Push 到 Array,因此同一 Event 可以有多个 Handler。Flag 有一项细节:Definition 会后写覆盖,但 Default Value 只在 Runtime 还没有这个 Name 时写入,所以重复 Flag 可能呈现 “后一个 Definition + 第一个 Default”。
12. 创建受控 ExtensionAPI
createExtensionAPI() 把方法分成两类:
Registration
直接写当前 Extension:
on / registerTool / registerCommand / registerFlag / ...
Action
委托给 Shared ExtensionRuntime:
sendMessage / setActiveTools / setModel / appendEntry / ...
在 Core 尚未绑定时,大多数 Action 是 Throwing Stub。Factory 阶段可以注册能力,但不能 假设已经拥有完整 AgentSession Runtime。
例外包括:
registerTool()在加载阶段合法,Refresh 此时是 No-op;- Provider Registration 会先进入 Pending Queue;
exec()直接调用本地 Command Runner,不依赖 Session Binding。
13. 执行 Factory
Loader 调用:
await factory(api);
所以 Async Factory 会阻塞该 Extension 的加载。Paths 又在 for...of 中逐个 Await,Factory
执行顺序与 Extension Path 顺序一致。
Factory Import 或执行抛错时:
{
extension: null,
error: "Failed to load extension: ..."
}
失败项不进入 Extensions Array,后续 Path 继续加载。
14. LoadExtensionsResult
最终 Loader 返回:
{
extensions: Extension[],
errors: Array<{ path, error }>,
runtime: ExtensionRuntime
}
错误是收集式,不是第一个失败就终止全部加载。桌面 Agent 应在诊断页展示 Path 与 Error, 否则 Extension 失效会表现成“注册的 Tool 不见了”。
15. Inline Factory
SDK 宿主还可以传入 Inline Extension Factory。它没有磁盘 Module Import:
Factory
→ loadExtensionFromFactory()
→ <inline:...> Extension
Inline Factory 与文件 Extension 最终进入同一个 Extensions Array 和 Shared Runtime。
它适合应用内置行为,不需要伪造临时 .ts 文件。
16. ExtensionRunner 怎样形成
AgentSession 拿到 extensionsResult 后构造:
new ExtensionRunner(
extensions,
runtime,
cwd,
sessionManager,
modelRegistry,
);
Runner 保留有序 Extension Array,并负责:
- 遍历 Handler;
- 汇总 Tool、Flag 和 Command;
- 创建运行时 Context;
- 记录 Handler Error;
- 管理 Shared Runtime Binding。
此时“注册结果”有了调度者,但 Core Action 还需要下一步绑定。
17. AgentSession Bindings
_bindExtensionCore() 把真实实现写入 Runtime:
sendMessage → AgentSession Message API
appendEntry → SessionManager
get/setActiveTools → Tool Registry
setModel → Model Runtime
compact → Session Compaction
abort → 当前 Run
...
_applyExtensionBindings() 再连接宿主提供的 UI Context、Command Context Actions、Shutdown
Handler 和 Error Listener。
这解释了为什么 Factory 不直接得到 AgentSession:Loader 先收集声明,宿主随后把合适的
运行能力注入 Runtime。
18. Tool 怎样进入 Agent
Runner 的 getAllRegisteredTools() 按 Extension 顺序合并,同名时第一次注册保留。
wrapRegisteredTools() 再把 ToolDefinition 变成 Agent Core 能执行的 AgentTool。最后
AgentSession 刷新 Tool Registry 和 Active Tool Names。
flowchart LR
D["ToolDefinition"] --> G["RegisteredTool + SourceInfo"]
G --> W["wrapRegisteredTool"]
W --> A["AgentTool"]
A --> R["Tool Registry"]
Tool Event 拦截不是 Wrapper 完成,而是 AgentSession 连接 Agent Core Hook 后交给 Runner。
最终 Registry 还有一层覆盖:AgentSession 先放 Built-in Tool,再写入 Extension/SDK Custom
Tool,因此 Extension Tool 与 Built-in 同名时会覆盖 Built-in;SDK customTools 又排在
Extension Tool 之后。Runner 的 First Wins 只决定多个 Extension 之间的 Winner。
19. 顺序、重名与冲突
以下表格描述跨 Extension 聚合;同一 Extension 内已在第 11 节说明为 Map 后写覆盖:
| 注册项 | 冲突行为 |
|---|---|
| Tool | Runner 第一个 Name 胜出;Resource Loader 加 Conflict Error |
| Flag | 第一个 Name 胜出;Resource Loader 加 Conflict Error |
| Command | 跨 Extension 全部保留;同名改为 name:1、name:2 |
| Event Handler | 全部保留,按 Extension/注册顺序运行 |
| Shortcut | 有单独的 Built-in/Extension 冲突规则 |
当前 detectExtensionConflicts() 只主动为跨 Extension 的 Tool 与 Flag 生成这类 Load Error,
不要笼统说“所有重名都会报同一种错误”。
20. Reload 发生什么
AgentSession.reload():
- 保存 Flag Values;
- 发
session_shutdown,Reason 为reload; - Reload Settings;
- Reset Provider;
- Resource Loader 清 Cache 并重新解析、导入、执行 Factory;
- 创建新的 Runner 并绑定;
- 恢复 Flag Values;
- 刷新全部 Extension Tool;
- 若宿主绑定存在,发新的
session_start; - 再执行
resources_discover扩展资源。
Extension 自己启动的 Watcher/Process 必须在 Shutdown 中清理,否则 Reload 会叠加资源。
21. 一次完整加载时序
sequenceDiagram
participant PM as PackageManager
participant RL as ResourceLoader
participant L as Extension Loader
participant F as Factory
participant S as AgentSession
participant R as ExtensionRunner
RL->>PM: resolve()
PM-->>RL: ordered paths + metadata
RL->>L: loadExtensionsCached(paths)
L->>F: import + await factory(api)
F-->>L: registrations in Extension
L-->>RL: extensions + errors + runtime
RL->>RL: apply SourceInfo/conflicts
RL-->>S: LoadExtensionsResult
S->>R: new ExtensionRunner(...)
S->>R: bindCore/bindUI
S->>S: wrap tools + refresh registry
22. 常见误区
“文件被发现就开始拦截”
错误。还要 Import、执行 Factory、创建 Runner、绑定 Core。
“生产路径只调用 discoverAndLoadExtensions”
错误。当前完整 coding-agent 路径由 Package Manager 与 Resource Loader 组织。
“Factory 能在加载时调用所有 Runtime Action”
错误。大多数 Action 在 Bind 前是 Throwing Stub。
“一个 Extension 加载失败会终止全部 Extension”
错误。Loader 收集错误并继续后续 Path。
“所有重名都按后者覆盖”
错误。Tool/Flag First Wins,Command 会获得带序号的 Invocation Name。
23. 本章小结
- Extension 从 Path、Factory、Registration Container 到 Runtime 逐层形成;
- 完整 coding-agent 路径先由 Package Manager 解析资源,再由 Loader 导入;
- Loader Helper 与 PackageManager 生产扫描都只看一层;后者还处理 Root Entry、Ignore、 Hidden Directory、node_modules 与 Symlink;
- CLI、项目、用户、Package 与 Inline Factory 有明确优先顺序;
- Jiti 加载 TS/JS,默认导出必须是 Factory Function;
- Extension Cache 保存 Factory,不保存已执行 Extension Object;
- Factory 顺序执行,失败被收集,不阻断后续 Path;
- SourceInfo 在 Loader 先合成,Resource Loader 后补真实来源;
- Registration 写 Extension,Action 委托 Shared Runtime;
- Runner 负责调度,AgentSession 负责绑定真实 Core Actions;
- Tool/Flag/Command 的冲突语义不同;
- Reload 会 Shutdown 旧资源、重新加载 Factory、重建 Runner 和 Tool Registry。
24. 自测
discoverAndLoadExtensions()与当前完整 Resource Loader 路径有什么区别?- Extension Directory 会无限递归扫描吗?
- Loader Cache 保存 Factory 还是 Extension Object?
- 为什么多数 Action 不能在 Factory 阶段调用?
- 一个 Factory 抛错后,其他 Extension 是否继续加载?
- SourceInfo 为什么分两个阶段形成?
- Tool、Flag、Command 同名时各怎么处理?
- Reload 时为什么必须处理
session_shutdown? - Tool/Flag First Wins 时,CLI、项目、用户、Package 与 Inline Factory 谁优先?