Build extensible agent harness with interactive chat loop

This commit is contained in:
Adolfo Reyna
2026-05-15 14:17:07 -04:00
commit d8d79efe83
16 changed files with 1398 additions and 0 deletions

89
src/agent.ts Normal file
View 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;
}
}