Build extensible agent harness with interactive chat loop
This commit is contained in:
89
src/agent.ts
Normal file
89
src/agent.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
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 class Agent {
|
||||
private readonly history: AgentMessage[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly model: ModelClient,
|
||||
private readonly tools: ToolRegistry,
|
||||
private readonly options: AgentOptions
|
||||
) {}
|
||||
|
||||
async runTurn(prompt: string): 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 = 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user