mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-22 21:23:44 +08:00
refactor: organize agent harness courses
This commit is contained in:
200
learn-pi-agent/s09_extension_runtime/README.md
Normal file
200
learn-pi-agent/s09_extension_runtime/README.md
Normal 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 要让**外部代码**接入 core,core 不用动就能长出新能力。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||
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 对外只给一个受控的 API,extension 加的工具和内置工具同源同链,事件按类型分发。后面 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>
|
||||
334
learn-pi-agent/s09_extension_runtime/code.ts
Normal file
334
learn-pi-agent/s09_extension_runtime/code.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
// s09: Extension Runtime — mini Pi 的第 9 版
|
||||
//
|
||||
// 外部代码通过公开 API 接入 core:订阅事件、注册工具、注册命令。core 不用动就能长出新能力。
|
||||
// 词汇边界:本章新增 Extension / ExtensionAPI / ExtensionRuntime / Command / RuntimeEvent / on / registerTool / registerCommand / emit / use。
|
||||
// 关键:registerTool 复用既有 Tool 类型,注入现有 ToolRegistry——extension 的工具和内置工具走同一条执行链。
|
||||
|
||||
declare const process: {
|
||||
exitCode?: number;
|
||||
};
|
||||
|
||||
// —— 停止原因(s04 起)——
|
||||
export type StopReason = "stop" | "toolUse" | "error";
|
||||
|
||||
// —— 消息 ——
|
||||
export type UserMessage = { role: "user"; content: string };
|
||||
export type AssistantMessage = { role: "assistant"; content: string; stopReason: StopReason };
|
||||
export type ToolResultMessage = { role: "toolResult"; toolCallId: string; content: string };
|
||||
export type AgentMessage = UserMessage | AssistantMessage | ToolResultMessage;
|
||||
|
||||
// —— 会话历史(s07 起)——
|
||||
export type SessionEntry = { id: string; parentId: string | null; message: AgentMessage };
|
||||
export class SessionTree {
|
||||
private entries = new Map<string, SessionEntry>();
|
||||
private activeLeafId: string | null = null;
|
||||
private counter = 0;
|
||||
append(message: AgentMessage): SessionEntry {
|
||||
const entry = { id: `e${++this.counter}`, parentId: this.activeLeafId, message };
|
||||
this.entries.set(entry.id, entry);
|
||||
this.activeLeafId = entry.id;
|
||||
return entry;
|
||||
}
|
||||
moveTo(entryId: string): void {
|
||||
if (!this.entries.has(entryId)) throw new Error(`unknown entry: ${entryId}`);
|
||||
this.activeLeafId = entryId;
|
||||
}
|
||||
currentPath(): AgentMessage[] {
|
||||
const path: AgentMessage[] = [];
|
||||
let cursor = this.activeLeafId;
|
||||
while (cursor) {
|
||||
const entry = this.entries.get(cursor);
|
||||
if (!entry) break;
|
||||
path.push(entry.message);
|
||||
cursor = entry.parentId;
|
||||
}
|
||||
return path.reverse();
|
||||
}
|
||||
allEntries(): SessionEntry[] { return [...this.entries.values()]; }
|
||||
}
|
||||
export type AgentState = { session: SessionTree; model: string };
|
||||
|
||||
// —— 工具契约(s02 起)——
|
||||
export type ToolSpec = { name: string; description: string; input: Record<string, string> };
|
||||
export type ToolHandler = (input: Record<string, string>) => string;
|
||||
export type ToolCall = { id: string; name: string; input: Record<string, string> };
|
||||
export type Tool = { spec: ToolSpec; handler: ToolHandler };
|
||||
|
||||
export class ToolRegistry {
|
||||
private tools = new Map<string, Tool>();
|
||||
register(tool: Tool): void { this.tools.set(tool.spec.name, tool); }
|
||||
getSpecs(): ToolSpec[] { return [...this.tools.values()].map((tool) => tool.spec); }
|
||||
count(): number { return this.tools.size; }
|
||||
run(call: ToolCall): string {
|
||||
const tool = this.tools.get(call.name);
|
||||
if (!tool) return `unknown tool: ${call.name}`;
|
||||
return tool.handler(call.input);
|
||||
}
|
||||
}
|
||||
|
||||
// —— 上下文资源(s08 起)——
|
||||
export type ContextResource = { kind: "agents" | "skill" | "prompt"; name: string; content: string };
|
||||
export class ResourceLoader {
|
||||
constructor(private resources: ContextResource[]) {}
|
||||
load(): ContextResource[] { return this.resources.map((r) => ({ ...r })); }
|
||||
}
|
||||
// s08:资源组装进 systemPrompt(对齐 Pi buildSystemPrompt)
|
||||
export function buildSystemPrompt(resources: ContextResource[]): string {
|
||||
return resources.map((r) => `[${r.kind}:${r.name}]\n${r.content}`).join("\n\n");
|
||||
}
|
||||
|
||||
// —— provider 对外 ——
|
||||
export type ProviderMessage =
|
||||
| { role: "user" | "assistant"; content: string }
|
||||
| { role: "toolResult"; toolCallId: string; content: string };
|
||||
export type ProviderInput = { systemPrompt: string; messages: ProviderMessage[]; tools: ToolSpec[] };
|
||||
export type ProviderEvent =
|
||||
| { type: "message_start" }
|
||||
| { type: "text_delta"; text: string }
|
||||
| { type: "tool_call"; call: ToolCall }
|
||||
| { type: "message_end"; stopReason: StopReason };
|
||||
export interface Provider { stream(input: ProviderInput): AsyncGenerator<ProviderEvent>; }
|
||||
|
||||
export type Output = { log(line: string): void };
|
||||
export function createConsoleOutput(): Output { return { log: (line) => console.log(line) }; }
|
||||
|
||||
// —— s05 起:执行插口 ——
|
||||
export type BeforeToolCallResult = { type: "allow" } | { type: "block"; reason: string };
|
||||
export type ToolHooks = {
|
||||
beforeToolCall?: (call: ToolCall) => BeforeToolCallResult;
|
||||
afterToolCall?: (call: ToolCall, result: string) => string;
|
||||
};
|
||||
export function executeToolCall(registry: ToolRegistry, hooks: ToolHooks, call: ToolCall): ToolResultMessage {
|
||||
const before = hooks.beforeToolCall?.(call) ?? { type: "allow" };
|
||||
if (before.type === "block") {
|
||||
return { role: "toolResult", toolCallId: call.id, content: `blocked: ${before.reason}` };
|
||||
}
|
||||
let result: string;
|
||||
try { result = registry.run(call); }
|
||||
catch (error) { result = `error: ${error instanceof Error ? error.message : String(error)}`; }
|
||||
const finalResult = hooks.afterToolCall?.(call, result) ?? result;
|
||||
return { role: "toolResult", toolCallId: call.id, content: finalResult };
|
||||
}
|
||||
|
||||
// —— s06 起:一轮快照 ——
|
||||
export type TurnSnapshot = { systemPrompt: string; messages: ProviderMessage[]; tools: ToolSpec[] };
|
||||
function toProviderMessages(messages: AgentMessage[]): ProviderMessage[] {
|
||||
return messages.map((message) => {
|
||||
if (message.role === "toolResult") {
|
||||
return { role: "toolResult", toolCallId: message.toolCallId, content: message.content };
|
||||
}
|
||||
return { role: message.role, content: message.content };
|
||||
});
|
||||
}
|
||||
export function createTurnSnapshot(state: AgentState, registry: ToolRegistry, loader: ResourceLoader): TurnSnapshot {
|
||||
return {
|
||||
systemPrompt: buildSystemPrompt(loader.load()),
|
||||
messages: toProviderMessages(state.session.currentPath()),
|
||||
tools: registry.getSpecs(),
|
||||
};
|
||||
}
|
||||
export function buildProviderInputFromSnapshot(snapshot: TurnSnapshot, state: AgentState): ProviderInput {
|
||||
return {
|
||||
systemPrompt: snapshot.systemPrompt,
|
||||
messages: toProviderMessages(state.session.currentPath()),
|
||||
tools: snapshot.tools,
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 构造函数 ============
|
||||
export function createInitialState(model = "demo-small"): AgentState { return { session: new SessionTree(), model }; }
|
||||
export function createUserMessage(content: string): UserMessage { return { role: "user", content }; }
|
||||
|
||||
// ============ 工具循环(不变)============
|
||||
const MAX_TURNS = 8;
|
||||
export async function runEventedToolLoop(
|
||||
state: AgentState, provider: Provider, registry: ToolRegistry,
|
||||
hooks: ToolHooks, snapshot: TurnSnapshot, output: Output,
|
||||
): Promise<AssistantMessage> {
|
||||
let turns = 0;
|
||||
while (true) {
|
||||
turns += 1;
|
||||
if (turns > MAX_TURNS) {
|
||||
const stopped: AssistantMessage = { role: "assistant", content: "(达到最大轮次,停止)", stopReason: "stop" };
|
||||
state.session.append(stopped);
|
||||
return stopped;
|
||||
}
|
||||
const providerInput = buildProviderInputFromSnapshot(snapshot, state);
|
||||
let content = "";
|
||||
let stopReason: StopReason = "stop";
|
||||
let sawToolCall = false;
|
||||
for await (const event of provider.stream(providerInput)) {
|
||||
if (event.type === "message_start") output.log("message_start");
|
||||
else if (event.type === "text_delta") { output.log(`text_delta: ${event.text}`); content += event.text; }
|
||||
else if (event.type === "tool_call") {
|
||||
sawToolCall = true;
|
||||
output.log(`tool_call: ${event.call.name}`);
|
||||
const resultMessage = executeToolCall(registry, hooks, event.call);
|
||||
state.session.append(resultMessage);
|
||||
output.log(`tool_result: ${resultMessage.content}`);
|
||||
} else if (event.type === "message_end") { stopReason = event.stopReason; output.log(`message_end: ${stopReason}`); }
|
||||
}
|
||||
if (!sawToolCall || stopReason !== "toolUse") {
|
||||
const assistant: AssistantMessage = { role: "assistant", content, stopReason };
|
||||
state.session.append(assistant);
|
||||
return assistant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ s09 新增:扩展运行时 ============
|
||||
|
||||
// U2 全局唯一:s09 定义,s10 复用。
|
||||
export type RuntimeEvent =
|
||||
| { type: "message"; content: string }
|
||||
| { type: "done" };
|
||||
|
||||
// 命令:一个不带参数、返回字符串的动作。
|
||||
export type Command = { name: string; run: () => string };
|
||||
|
||||
// 订阅某类事件时,handler 收到的事件结构自动对应(订阅 "message" 就只收 message 事件)。
|
||||
type EventHandler<T extends RuntimeEvent["type"]> = (
|
||||
event: Extract<RuntimeEvent, { type: T }>,
|
||||
) => void;
|
||||
|
||||
// extension 能接触的全部表面。
|
||||
export type ExtensionAPI = {
|
||||
on<T extends RuntimeEvent["type"]>(type: T, handler: EventHandler<T>): void;
|
||||
registerTool(tool: Tool): void; // 复用 s02 的 Tool
|
||||
registerCommand(command: Command): void;
|
||||
};
|
||||
|
||||
// 一个 extension 就是一个接收 API 的函数。
|
||||
export type Extension = (api: ExtensionAPI) => void;
|
||||
|
||||
export class ExtensionRuntime {
|
||||
private registry: ToolRegistry; // 复用既有 registry:extension 注册的工具和内置工具同源
|
||||
private commands = new Map<string, Command>();
|
||||
private handlers: { type: RuntimeEvent["type"]; handler: (event: RuntimeEvent) => void }[] = [];
|
||||
|
||||
constructor(registry: ToolRegistry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
// 外部只能拿到这个 API,拿不到 runtime 内部字段。
|
||||
createApi(): ExtensionAPI {
|
||||
return {
|
||||
on: (type, handler) => {
|
||||
this.handlers.push({
|
||||
type,
|
||||
handler: handler as (event: RuntimeEvent) => void,
|
||||
});
|
||||
},
|
||||
registerTool: (tool) => {
|
||||
this.registry.register(tool); // 注入既有 registry,走同一执行链
|
||||
},
|
||||
registerCommand: (command) => {
|
||||
this.commands.set(command.name, command);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
use(extension: Extension): void {
|
||||
extension(this.createApi());
|
||||
}
|
||||
|
||||
// 按事件类型分发(不是全部 handler 都调)。
|
||||
emit(event: RuntimeEvent): void {
|
||||
for (const { type, handler } of this.handlers) {
|
||||
if (type === event.type) {
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runCommand(name: string): string {
|
||||
const command = this.commands.get(name);
|
||||
if (!command) return `unknown command: ${name}`;
|
||||
return command.run();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Demo Provider(保留,累积)============
|
||||
export class DemoProvider implements Provider {
|
||||
public lastInput: ProviderInput | undefined;
|
||||
async *stream(input: ProviderInput): AsyncGenerator<ProviderEvent> {
|
||||
this.lastInput = input;
|
||||
const last = input.messages[input.messages.length - 1];
|
||||
yield { type: "message_start" };
|
||||
if (last?.role === "toolResult") {
|
||||
yield { type: "text_delta", text: `工具结果是:${last.content}` };
|
||||
yield { type: "message_end", stopReason: "stop" };
|
||||
return;
|
||||
}
|
||||
yield { type: "tool_call", call: { id: "call_1", name: "current_time", input: {} } };
|
||||
yield { type: "message_end", stopReason: "toolUse" };
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 演示脚手架 ============
|
||||
|
||||
function createRegistry(): ToolRegistry {
|
||||
const registry = new ToolRegistry();
|
||||
registry.register({
|
||||
spec: { name: "current_time", description: "返回一个固定的演示时间", input: {} },
|
||||
handler: () => "2026-01-01T00:00:00Z",
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
// 一个 demo extension:订阅事件、注册命令、注册工具。全部通过 API,不碰 core 内部。
|
||||
function createDemoExtension(output: Output): Extension {
|
||||
return (api) => {
|
||||
api.on("message", (event) => {
|
||||
output.log(`[event] message: ${event.content}`);
|
||||
});
|
||||
api.registerCommand({ name: "status", run: () => "extension is active" });
|
||||
api.registerTool({
|
||||
spec: { name: "note", description: "保存一条笔记", input: { text: "内容" } },
|
||||
handler: (input) => `note saved: ${input.text ?? ""}`,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const output = createConsoleOutput();
|
||||
const registry = createRegistry();
|
||||
const runtime = new ExtensionRuntime(registry);
|
||||
|
||||
output.log("s09: Extension Runtime");
|
||||
output.log("");
|
||||
|
||||
// extension 接入:通过 API 注册能力。
|
||||
runtime.use(createDemoExtension(output));
|
||||
|
||||
// 注册后,registry 里既有内置工具,也有 extension 注册的工具。
|
||||
output.log("[registry]");
|
||||
for (const spec of registry.getSpecs()) {
|
||||
output.log(`tool: ${spec.name}`);
|
||||
}
|
||||
output.log("");
|
||||
|
||||
// 事件:core emit,extension 的 handler 被触发(按类型匹配)。
|
||||
runtime.emit({ type: "message", content: "hello from core" });
|
||||
output.log("");
|
||||
|
||||
// 命令。
|
||||
output.log("[command]");
|
||||
output.log(`/status -> ${runtime.runCommand("status")}`);
|
||||
output.log("");
|
||||
|
||||
// extension 注册的工具,走既有执行链(executeToolCall)。
|
||||
output.log("[tool via extension]");
|
||||
const result = executeToolCall(
|
||||
registry,
|
||||
{},
|
||||
{ id: "c1", name: "note", input: { text: "hi" } },
|
||||
);
|
||||
output.log(`note -> ${result.content}`);
|
||||
output.log("");
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user