refactor: organize agent harness courses

This commit is contained in:
Haoran
2026-06-16 00:10:35 +08:00
parent 20e7cbb72c
commit 8af5c24e46
491 changed files with 7961 additions and 564 deletions

View File

@@ -0,0 +1,200 @@
# s09: Extension Runtime — 外部代码通过 API 接入
> *core 不改,能力从外面接进来。*
> **Pi 边界**:扩展 API 边界 —— core 暴露的是 API不是内部对象。
[上一节s08](../s08_context_resources/) → `s09` → [下一节s10](../s10_runtime_modes/)
---
## 问题
到 s08 为止core 的能力(工具、资源)全都写在 core 代码里。每想加一种新玩法——一个新工具、一条新命令、对某类事件做个处理——都得改 core 自己。core 只会越来越重。
s09 要让**外部代码**接入 corecore 不用动就能长出新能力。
---
## 解决方案
core 暴露一个 `ExtensionAPI`,外部代码(叫一个 extension只能通过它做三件事
```text
on(type, handler) 订阅事件
registerTool(tool) 注册工具
registerCommand(command) 注册命令
```
一个 extension 就是一个接收 API 的函数。它拿不到 core 的内部对象,只能用这三个方法。
关键设计:`registerTool` 复用的是 s02 就有的 `Tool` 类型,注册进去的工具直接进**既有 ToolRegistry**。也就是说extension 注册的工具和 core 内置的工具,走的是**同一条执行链**(经过 s05 的 hook——不分彼此。
---
## 工作原理
**先定义事件和命令。** 事件是 core 往外发的信号;命令是外部注册的无参动作。
```ts
export type RuntimeEvent =
| { type: "message"; content: string }
| { type: "done" };
export type Command = { name: string; run: () => string };
```
**定义 API 表面。** 这就是 extension 能碰的全部。
```ts
export type ExtensionAPI = {
on(type: RuntimeEvent["type"], handler: (event: RuntimeEvent) => void): void;
registerTool(tool: Tool): void;
registerCommand(command: Command): void;
};
export type Extension = (api: ExtensionAPI) => void;
```
**ExtensionRuntime 接住注册。** 它构造时接收既有 registry`registerTool` 直接往这个 registry 里加。
```ts
export class ExtensionRuntime {
constructor(private registry: ToolRegistry) {}
createApi(): ExtensionAPI {
return {
on: (type, handler) => { this.handlers.push({ type, handler }); },
registerTool: (tool) => { this.registry.register(tool); }, // 注入既有 registry
registerCommand: (command) => { this.commands.set(command.name, command); },
};
}
emit(event) { /* 按类型匹配 handler不是全调 */ }
runCommand(name) { /* 找不到返回 unknown command */ }
}
```
两个细节:`emit` 按**事件类型**匹配 handler不是把所有 handler 都调一遍);命令找不到时返回一句说明,不抛错。
> 这一节真正建立的是**扩展 API 边界**core 对外只给一个受控的 APIextension 加的工具和内置工具同源同链,事件按类型分发。后面 s11 的权限检查会同样作用在 extension 注册的工具上,因为它们本就在同一个 registry 里。
---
## 试一下
运行:
```sh
npm run s09
```
输出类似:
```text
s09: Extension Runtime
[registry]
tool: current_time
tool: note
[event] message: hello from core
[command]
/status -> extension is active
[tool via extension]
note -> note saved: hi
```
观察重点:`[registry]``current_time` 是内置的、`note` 是 extension 注册的,两者同处一个 registry`[tool via extension]` 里 extension 的工具走的还是 `executeToolCall` 那条既有执行链。
---
## 接入主线
s09 在 s08 上累积。相对 s08 的变更:
| 组件 | s08 | s09 |
| --- | --- | --- |
| 新增类型 | — | `RuntimeEvent`U2 全局唯一)/ `Command` / `Extension` / `ExtensionAPI` |
| 新增类 | — | `ExtensionRuntime`(构造接收既有 `ToolRegistry` |
| 工具来源 | 只有 core 内置 | core 内置 + extension 注册(同一 registry |
| `ProviderInput` / 主循环 | — | **不变**(纯新增,无 U1 升级) |
**焊接点**`ExtensionRuntime` 构造接收既有 `ToolRegistry``registerTool` 往里加。extension 工具和内置工具同源,执行时都走 `executeToolCall`
---
## 接下来
现在 core 能产生结果但结果怎么展示打印JSON写死在代码里。
下一节会把"产生结果"和"展示结果"分开:同一个 core接不同的输出方式。
进入下一节:[s10](../s10_runtime_modes/)。
---
<details>
<summary>Pi 源码溯源Extension API 和它的 20 多个事件</summary>
教学版的 ExtensionAPI 暴露 on/registerTool/registerCommand 三个方法。Pi 的 `packages/coding-agent` 有一套庞大得多的 extension 系统。
### 源码在哪
- `packages/coding-agent/src/core/extensions/types.ts``ExtensionAPI` 类型
- `packages/coding-agent/src/core/extensions/loader.ts` — 发现 + 加载
- `packages/coding-agent/src/core/extensions/runner.ts` — 运行时
- `.pi/extensions/` — 项目级扩展目录
### API 比教学版大得多
教学版三个方法。Pi 的 `ExtensionAPI``types.ts`)有一长串:
```ts
interface ExtensionAPI {
// 注册能力
registerTool(tool): void;
registerCommand(name, options): void;
registerFlag(name, { description, type, default }): void;
// 订阅事件20+ 种)
on(event: "session_start" | "tool_execution_start" | "before_agent_start" | ..., handler): void;
// 运行时动作
sendMessage(msg): void;
setModel(model): void;
getActiveTools(): AgentTool[];
registerProvider(...) / unregisterProvider(...): void;
exec(command): Promise<...>;
}
```
教学版的 on/registerTool/registerCommand 是它的一个子集。Pi 的 extension 不仅能加工具/命令,还能改模型、注册 provider、执行命令、订阅 20 多种生命周期事件。
### 20 多种事件
教学版只有 `message` / `done` 两种 RuntimeEvent。Pi 的 extension 能订阅 `session_start``tool_execution_start``before_agent_start``project_trust`s11 用它决定信任)……覆盖整个 agent 生命周期。每个事件的 handler 还能返回结果反向影响 core比如 `before_agent_start` 的返回值能改本轮配置)。
### 四种发现来源
`discoverAndLoadExtensions``loader.ts:557`)从四个地方找扩展:
```text
1. cwd/.pi/extensions/ 项目级
2. agentDir/.pi/extensions/ 全局级
3. package.json 的 pi.extensions 字段 包声明
4. 命令行传入的路径 CLI 级
```
教学版的 extension 是手动 `runtime.use(...)`。Pi 是自动发现——放对目录就加载。
### 冲突检测 + 沙箱
两个扩展注册同名工具怎么办?`detectExtensionConflicts``loader.ts:988`)检查工具/命令/标志名冲突,通过 `ResourceDiagnostic` 报告,保留先加载的。扩展代码跑在 jiti 沙箱里,每个扩展有 `sourceInfo` 标记来源和权限级别——这是教学版完全没有的隔离层。
### notInitialized 守卫
`createExtensionRuntime``runner.ts`)有个巧思:扩展加载阶段(执行 factory 函数时runtime 的动作方法sendMessage 等)都指向 `notInitialized`——一调用就抛错。因为加载时 core 还没就绪,扩展只能"注册",不能"动作"。加载完成后才换上真实实现。
### 一句话
教学版的 ExtensionAPI 立的是"外部代码通过受控 API 接入 core"。Pi 把它坐实成 20 多个事件 + 注册 tool/command/flag/provider + 四种自动发现 + 冲突检测 + 沙箱隔离。教学版只保留最小接入on/registerTool/registerCommand + 手动 use但"core 暴露 API 而非内部"这条边界一致。
</details>