first commit

This commit is contained in:
魏天炜
2026-11-12 15:29:12 +08:00
commit 5dabea54ab
93 changed files with 12170 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
// 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();
}
}