// Agent Loop。四个级别共用这一个文件,差别只在工具。 // // 空闲 ──收件箱有东西──▶ 一轮(turn) // ├─ 一步(step):请求模型 → 依次执行它要的工具 → 结果加进历史 // ├─ 每步结束:把收件箱里的东西加进历史(用户插话、后台完成、问题答复) // └─ 模型不再调用工具 → 一轮结束,回到空闲 // // 收件箱只在步与步之间被取走。工具没返回,模型就看不到新东西。 import type { Message, TraceBody } from '../shared/types.ts'; import { Inbox } from './inbox.ts'; import type { LLM } from './llm.ts'; import type { LevelConfig } from './levels.ts'; import { ToolBox } from './tools.ts'; import { workspaceDiff } from './workspace.ts'; export type AgentOptions = { level: LevelConfig; workspace: string; llm: LLM; emit: (body: TraceBody) => void; }; const MAX_STEPS_PER_TURN = 40; export class Agent { readonly history: Message[] = []; readonly inbox = new Inbox(); readonly tools: ToolBox; private opts: AgentOptions; private abort = new AbortController(); private stopped = false; private requestCount = 0; constructor(opts: AgentOptions) { this.opts = opts; this.inbox.onPush = (item) => opts.emit({ type: 'inbox.push', item }); this.tools = new ToolBox({ workspace: opts.workspace, level: opts.level, emit: opts.emit, pushInbox: (item) => this.inbox.push(item), onFileChanged: () => this.emitDiff(), }); this.append({ role: 'system', content: opts.level.systemPrompt }); } // 外部入口:三件事都只是往收件箱里放东西。 sendUserMessage(text: string): void { this.inbox.push({ kind: 'user', content: text }); } answerQuestion(questionId: string, answer: string): void { this.tools.answer(questionId, answer); } async run(): Promise { try { while (!this.stopped) { await this.inbox.waitForItem(); if (this.stopped) break; await this.runTurn(); } } catch (error) { if (this.stopped) return; const message = error instanceof Error ? error.message : String(error); this.opts.emit({ type: 'run.stop', reason: 'error', error: message }); this.stop(); } } private async runTurn(): Promise { this.deliverInbox(); for (let step = 0; step < MAX_STEPS_PER_TURN && !this.stopped; step += 1) { const assistant = await this.callModel(); if (!assistant.tool_calls?.length) break; // 模型没再要工具,这一轮结束 for (const call of assistant.tool_calls) { const result = await this.runTool(call.id, call.function.name, call.function.arguments); if (this.stopped) return; this.append({ role: 'tool', tool_call_id: call.id, content: result }); } this.deliverInbox(); } this.emitDiff(); } private async callModel() { const n = ++this.requestCount; const { emit, llm } = this.opts; emit({ type: 'model.start', n, messageCount: this.history.length }); try { const result = await llm.chat({ messages: this.history, tools: this.tools.specs(), signal: this.abort.signal, onDelta: (delta) => emit({ type: 'model.delta', n, ...delta }), }); emit({ type: 'model.end', n, ...result }); this.append(result.message); // 防线:完成通知和问题答复只能由运行时放进收件箱。模型自己写出这些标签,就是在冒充结果。 const forged = result.message.content.match(/<(background_job_done|question_reply)\b/); if (forged) { emit({ type: 'guard.forged', n, tag: forged[1] }); this.inbox.push({ kind: 'user', content: `你在上一条回复里自己写出了 <${forged[1]}>。这类消息只由运行时发出,你写的内容不是真实结果,请作废基于它的结论,等待真正的消息。`, }); } return result.message; } catch (error) { if (!this.stopped) { emit({ type: 'model.error', n, error: error instanceof Error ? error.message : String(error) }); } throw error; } } private async runTool(callId: string, name: string, rawArgs: string): Promise { let args: unknown; try { args = rawArgs ? JSON.parse(rawArgs) : {}; } catch { const result = `错误:工具参数不是合法 JSON:${rawArgs}`; this.opts.emit({ type: 'tool.start', callId, name, args: rawArgs, n: this.requestCount }); this.opts.emit({ type: 'tool.end', callId, result }); return result; } this.opts.emit({ type: 'tool.start', callId, name, args, n: this.requestCount }); const result = await this.tools.run(callId, name, args); this.opts.emit({ type: 'tool.end', callId, result }); return result; } // 把收件箱里攒下的东西按到达顺序加进历史,下一次模型请求就能看到。 private deliverInbox(): void { const items = this.inbox.drain(); if (!items.length) return; this.opts.emit({ type: 'inbox.deliver', ids: items.map((item) => item.id) }); for (const item of items) { this.append( { role: 'user', content: item.content }, { inboxId: item.id, kind: item.kind, ref: item.ref }, ); } } private append(message: Message, origin?: Extract['origin']) { this.history.push(message); this.opts.emit({ type: 'history.append', index: this.history.length - 1, message, origin }); } private emitDiff(): void { this.opts.emit({ type: 'workspace.diff', diff: workspaceDiff(this.opts.workspace) }); } stop(): void { if (this.stopped) return; this.stopped = true; this.abort.abort(); this.tools.stop(); this.inbox.interrupt(); } }