first commit
This commit is contained in:
163
async-agent-lab/agent/agent.ts
Normal file
163
async-agent-lab/agent/agent.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user