101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import type { AgentMessage, ModelClient } from "./types.js";
|
|
import { ToolRegistry } from "./tools/registry.js";
|
|
import type { EventBus } from "./events.js";
|
|
|
|
export type AgentOptions = {
|
|
cwd: string;
|
|
maxTurns?: number;
|
|
events?: EventBus;
|
|
};
|
|
|
|
export type RunTurnOptions = {
|
|
onTextDelta?: (delta: string) => void;
|
|
};
|
|
|
|
export class Agent {
|
|
private readonly history: AgentMessage[] = [];
|
|
|
|
constructor(
|
|
private readonly model: ModelClient,
|
|
private readonly tools: ToolRegistry,
|
|
private readonly options: AgentOptions
|
|
) {}
|
|
|
|
async runTurn(prompt: string, runOptions: RunTurnOptions = {}): Promise<string> {
|
|
const events = this.options.events;
|
|
const maxTurns = this.options.maxTurns ?? 20;
|
|
|
|
this.history.push({ role: "user", content: prompt });
|
|
await events?.emit("agent.started", { maxTurns });
|
|
|
|
for (let turn = 0; turn < maxTurns; turn += 1) {
|
|
await events?.emit("agent.turn.started", { turn: turn + 1 });
|
|
const result =
|
|
runOptions.onTextDelta && this.model.respondStream
|
|
? await this.model.respondStream(
|
|
this.history,
|
|
this.tools.list(),
|
|
runOptions.onTextDelta
|
|
)
|
|
: await this.model.respond(this.history, this.tools.list());
|
|
this.history.push(...(result.output as AgentMessage[]));
|
|
|
|
if (result.toolCalls.length === 0) {
|
|
await events?.emit("agent.completed", { turn: turn + 1 });
|
|
return result.outputText;
|
|
}
|
|
|
|
for (const call of result.toolCalls) {
|
|
await events?.emit("tool.call.requested", { tool: call.name });
|
|
const tool = this.tools.get(call.name);
|
|
|
|
if (!tool) {
|
|
await events?.emit("tool.call.unknown", { tool: call.name });
|
|
this.history.push({
|
|
type: "function_call_output",
|
|
call_id: call.id,
|
|
output: JSON.stringify({ error: `Unknown tool: ${call.name}` })
|
|
});
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(call.arguments);
|
|
await events?.emit("tool.call.started", { tool: call.name, input: parsed });
|
|
const output = await tool.execute(parsed, { cwd: this.options.cwd });
|
|
await events?.emit("tool.call.completed", { tool: call.name });
|
|
|
|
this.history.push({
|
|
type: "function_call_output",
|
|
call_id: call.id,
|
|
output: JSON.stringify(output)
|
|
});
|
|
} catch (error) {
|
|
await events?.emit("tool.call.failed", {
|
|
tool: call.name,
|
|
error: error instanceof Error ? error.message : "Tool execution failed"
|
|
});
|
|
this.history.push({
|
|
type: "function_call_output",
|
|
call_id: call.id,
|
|
output: JSON.stringify({
|
|
error: error instanceof Error ? error.message : "Tool execution failed"
|
|
})
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
await events?.emit("agent.failed", { reason: "max_turns_exceeded", maxTurns });
|
|
throw new Error(`Agent exceeded max turns (${maxTurns}).`);
|
|
}
|
|
|
|
getHistory(): AgentMessage[] {
|
|
return [...this.history];
|
|
}
|
|
|
|
reset(): void {
|
|
this.history.length = 0;
|
|
}
|
|
}
|