Files
analysis_claude_code/learn-pi-agent/s04_evented_tool_loop/code.ts
2026-06-16 00:10:35 +08:00

307 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// s04: Evented Tool Loop — mini Pi 的第 4 版
//
// [U1 升级] runOneTurn → runEventedToolLoopprovider 请求工具core 执行后把结果送回,循环到 provider 不再请求为止。
// 词汇边界:本章新增 ToolCall / ToolResultMessage / tool_call / toolUse / runEventedToolLoop / run。
// 关键tools 取 registry.getSpecs()单一数据源不硬编码循环有上限R5工具出错不崩R4
declare const process: {
exitCode?: number;
};
// —— 停止原因R1s04 加 toolUse——
export type StopReason = "stop" | "toolUse" | "error";
// —— s01 起:消息 ——
export type UserMessage = {
role: "user";
content: string;
};
export type AssistantMessage = {
role: "assistant";
content: string;
stopReason: StopReason;
};
// s04 新增:工具执行结果也是一种消息,和 user / assistant 平级。
export type ToolResultMessage = {
role: "toolResult";
toolCallId: string;
content: string;
};
// s04 起AgentMessage 并入 ToolResultMessageR1 只增)
export type AgentMessage = UserMessage | AssistantMessage | ToolResultMessage;
// —— s01 起core 内部状态 ——
export type AgentState = {
messages: AgentMessage[];
};
// —— s02 起:工具契约 ——
export type ToolSpec = {
name: string;
description: string;
input: Record<string, string>;
};
export type ToolHandler = (input: Record<string, string>) => string;
// s04 新增provider 对一个工具的调用请求。
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);
}
// s04 新增:执行工具。未知工具不崩,返回一句说明。
run(call: ToolCall): string {
const tool = this.tools.get(call.name);
if (!tool) {
return `unknown tool: ${call.name}`;
}
return tool.handler(call.input);
}
}
// —— provider 对外消息s04加 toolResult 形态)——
export type ProviderMessage =
| { role: "user" | "assistant"; content: string }
| { role: "toolResult"; toolCallId: string; content: string };
// provider 输入R1 只增)
export type ProviderInput = {
messages: ProviderMessage[];
tools: ToolSpec[];
};
// —— s03 起事件流s04 加 tool_call保留 message_startR1——
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>;
}
// —— s01 起输出抽象R7——
export type Output = {
log(line: string): void;
};
export function createConsoleOutput(): Output {
return { log: (line) => console.log(line) };
}
// ============ 构造函数 ============
export function createInitialState(): AgentState {
return { messages: [] };
}
export function createUserMessage(content: string): UserMessage {
return { role: "user", content };
}
// s04buildProviderInput 要把 toolResult 消息也正确转给 provider。
export function buildProviderInput(
state: AgentState,
registry: ToolRegistry,
): ProviderInput {
return {
messages: state.messages.map((message) => {
if (message.role === "toolResult") {
return {
role: "toolResult",
toolCallId: message.toolCallId,
content: message.content,
};
}
return { role: message.role, content: message.content };
}),
tools: registry.getSpecs(),
};
}
// ============ s04 [U1]:工具循环(取代 runOneTurn============
// s03 的 collectAssistantMessage只攒文本事件被这里的循环内联收集取代——
// 循环要处理 tool_call所以收集逻辑直接长在循环里。
// R5循环必须有上限。否则一个一直请求工具的 provider 会让 core 死循环。
const MAX_TURNS = 8;
export async function runEventedToolLoop(
state: AgentState,
provider: Provider,
registry: ToolRegistry,
userInput: string,
output: Output,
): Promise<AssistantMessage> {
state.messages.push(createUserMessage(userInput));
let turns = 0;
while (true) {
turns += 1;
if (turns > MAX_TURNS) {
output.log(`(达到最大轮次 ${MAX_TURNS},停止)`);
const stopped: AssistantMessage = {
role: "assistant",
content: "(达到最大轮次,停止)",
stopReason: "stop",
};
state.messages.push(stopped);
return stopped;
}
const providerInput = buildProviderInput(state, registry);
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}`);
// R4工具执行抛错也不崩错误变成一条结果消息送回去。
let result: string;
try {
result = registry.run(event.call);
} catch (error) {
result = `error: ${error instanceof Error ? error.message : String(error)}`;
}
const resultMessage: ToolResultMessage = {
role: "toolResult",
toolCallId: event.call.id,
content: result,
};
state.messages.push(resultMessage);
output.log(`tool_result: ${result}`);
} else if (event.type === "message_end") {
stopReason = event.stopReason;
output.log(`message_end: ${stopReason}`);
}
}
// 没有 tool_call或 provider 明确不再用工具stopReason 不是 toolUse就结束。
if (!sawToolCall || stopReason !== "toolUse") {
const assistant: AssistantMessage = { role: "assistant", content, stopReason };
state.messages.push(assistant);
return assistant;
}
}
}
// ============ Demo Providerfake============
// 演示一个完整的工具循环:第一轮请求工具,第二轮收到结果后输出文本并结束。
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;
}
function printAssistantMessage(output: Output, message: AssistantMessage): void {
output.log("[assistant]");
output.log(`content: ${message.content}`);
output.log(`stopReason: ${message.stopReason}`);
output.log("");
}
function printState(output: Output, state: AgentState): void {
output.log("[state]");
output.log(state.messages.map((message) => message.role).join(" -> "));
output.log("");
}
async function main(): Promise<void> {
const output = createConsoleOutput();
const state = createInitialState();
const registry = createRegistry();
const provider = new DemoProvider();
output.log("s04: Evented Tool Loop");
output.log("");
output.log("[user]");
output.log("现在几点?");
output.log("");
const assistant = await runEventedToolLoop(
state,
provider,
registry,
"现在几点?",
output,
);
output.log("");
printAssistantMessage(output, assistant);
printState(output, state);
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});