Files
2026-11-12 15:29:12 +08:00

164 lines
5.8 KiB
TypeScript
Raw Permalink 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.

// 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<void> {
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<void> {
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: `<runtime_notice>你在上一条回复里自己写出了 <${forged[1]}>。这类消息只由运行时发出,你写的内容不是真实结果,请作废基于它的结论,等待真正的消息。</runtime_notice>`,
});
}
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<string> {
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<TraceBody, { type: 'history.append' }>['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();
}
}