← back to Govarbitrage
src/agents/framework.ts
92 lines
import { prisma } from "@/lib/db";
// Minimal, dependency-free agent runner. Every agent execution is recorded as
// an AgentRun; observations stream in as AgentFindings via ctx.finding().
// Errors are caught → status FAILED with the message in summary, so a crashed
// agent still leaves an auditable row instead of vanishing.
export type Severity = "INFO" | "WARN" | "CRITICAL";
export interface FindingInput {
kind: string;
severity: Severity;
title: string;
detail?: string;
listingId?: string;
listingTitle?: string;
}
export interface AgentContext {
runId: string;
agent: string;
/** Persist one finding against this run. */
finding(f: FindingInput): Promise<void>;
/** Increment the run's itemsProcessed counter (default +1). */
count(n?: number): void;
}
export interface AgentRunResult {
runId: string;
agent: string;
status: "OK" | "FAILED";
itemsProcessed: number;
summary: string | null;
findings: number;
}
/**
* Execute `fn` inside a tracked AgentRun. The function may return a summary
* string; on throw the run is marked FAILED with the error message.
*/
export async function runAgent(
name: string,
fn: (ctx: AgentContext) => Promise<string | void>,
): Promise<AgentRunResult> {
const run = await prisma.agentRun.create({
data: { agent: name, status: "RUNNING" },
});
let items = 0;
let findings = 0;
const ctx: AgentContext = {
runId: run.id,
agent: name,
async finding(f) {
findings++;
await prisma.agentFinding.create({
data: {
runId: run.id,
agent: name,
kind: f.kind,
severity: f.severity,
title: f.title,
detail: f.detail,
listingId: f.listingId,
listingTitle: f.listingTitle,
},
});
},
count(n = 1) {
items += n;
},
};
try {
const summary = (await fn(ctx)) ?? null;
await prisma.agentRun.update({
where: { id: run.id },
data: { status: "OK", finishedAt: new Date(), itemsProcessed: items, summary },
});
return { runId: run.id, agent: name, status: "OK", itemsProcessed: items, summary, findings };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
const summary = `FAILED: ${message}`.slice(0, 1000);
await prisma.agentRun.update({
where: { id: run.id },
data: { status: "FAILED", finishedAt: new Date(), itemsProcessed: items, summary },
});
return { runId: run.id, agent: name, status: "FAILED", itemsProcessed: items, summary, findings };
}
}