import { randomUUID } from "node:crypto"; import { type AgentRuntime, type JobPublisher, runJobKey } from "@rakazo/adapter-kit"; import { MessagingTeamChatEmulator } from "@rakazo/adapters"; import type { ModelConnectInput, RunStatus } from "@rakazo/contracts"; import { ACTIVE_RUN_STATUSES, isTerminal } from "@rakazo/core"; import type { createDb } from "@rakazo/db"; import { sessionCookieHeader } from "../index.js"; import type { EvalCase, Evidence } from "./cases.js"; import { emptyTrial, type FailureCategory, redact, type TrialResult } from "./report.js"; import { EvalServices } from "./services.js"; type App = { request: (input: string, init?: RequestInit) => Promise }; export type EvalApp = { app: App; prisma: ReturnType["prisma"]; jobs: JobPublisher; runtime: AgentRuntime; harnessIssues?: readonly string[]; connector?: { records: readonly unknown[] }; stop: () => Promise; }; export type EvalActor = { botId: string; cookie: string; userId: string; spaceId: string; }; export type TrialOptions = { connection: ModelConnectInput; timeoutMs: number; maxToolCalls: number; createApp: (services: EvalServices, messaging: MessagingTeamChatEmulator) => Promise; }; class EvalFailure extends Error { constructor( readonly category: FailureCategory, message: string, ) { super(message); } } /** No coaching retries: scenario setup turns are intentional steps, never repair prompts. */ export async function runTrial( scenario: EvalCase, trial: number, options: TrialOptions, ): Promise { const result = emptyTrial(scenario.id, trial); const started = Date.now(); const deadline = started + options.timeoutMs; const services = new EvalServices(); const messaging = new MessagingTeamChatEmulator({ provider: "slack", capabilities: { groups: false }, }); scenario.configure?.(services); let handles: EvalApp | undefined; let cookie = ""; let botId = ""; const runIds: string[] = []; const actors: EvalActor[] = []; let pendingApproval: Evidence["pendingApproval"] = null; let approvalPending = false; let priorMemory = ""; let messagingLinked = false; let messagingAddress = ""; let messagingOriginThreadId: string | null = null; const secrets = [options.connection.apiKey ?? "", options.connection.baseUrl ?? ""]; let phase: FailureCategory = "harness"; const seenTools = new Map(); let clearedToolCount = 0; const seenUsage = new Map(); const countCurrentTools = () => handles!.prisma.event.count({ where: { spaceId: { in: actors.map((actor) => actor.spaceId) }, type: "agent.tool.called" }, }); // Clearing conversation history deletes its events and usage rows. Preserve // observed records by id so multi-turn totals and budgets never reset or double count. const captureTools = async () => { if (!handles) return seenTools.size; const events = await handles.prisma.event.findMany({ where: { spaceId: { in: actors.map((actor) => actor.spaceId) }, type: "agent.tool.called", ...(seenTools.size ? { id: { notIn: [...seenTools.keys()] } } : {}), }, select: { id: true, payload: true }, }); for (const event of events) seenTools.set(event.id, { name: redact(String((event.payload as { name?: string }).name ?? "unknown"), secrets), }); return seenTools.size; }; const captureUsage = async () => { if (!handles) return; const usage = await handles.prisma.usageRecord.findMany({ where: { OR: actorScopes(actors) }, select: { id: true, inputTokens: true, outputTokens: true }, }); for (const row of usage) seenUsage.set(row.id, row); }; try { handles = await options.createApp(services, messaging); const { app, prisma } = handles; const setupActor = async () => { const signup = await app.request("/api/auth/sign-up/email", { method: "POST", headers: { "content-type": "application/json", origin: "http://127.0.0.1:5173" }, body: JSON.stringify({ email: `eval-${randomUUID()}@example.test`, password: "synthetic-eval-password-12", name: "Eval User", }), }); if (!signup.ok) throw new EvalFailure("harness", `Fixture signup failed (${signup.status})`); cookie = sessionCookieHeader(signup); await rpc(app, cookie, "models/connect", options.connection); for (const provider of scenario.connections ?? ["GMAIL", "CRM", "GITHUB"]) await rpc(app, cookie, "connections/begin", { connectorId: "composio", provider, displayName: provider, }); const bot = await rpc<{ id: string }>(app, cookie, "bots/create", { name: "Assistant", title: "", description: "", instructions: "", notifyOnFinish: false, }); botId = bot.id; const persistedBot = await prisma.bot.findUniqueOrThrow({ where: { id: botId }, select: { userId: true, spaceId: true }, }); actors.push({ botId, cookie, ...persistedBot }); await rpc(app, cookie, "bots/update", { botId, modelProvider: options.connection.provider, modelId: options.connection.modelId, }); messagingLinked = false; messagingAddress = `U-eval-${randomUUID()}`; messaging.resetWitnesses(); if (scenario.approvalTool) await rpc(app, cookie, "approvalRules/set", { effect: "require_approval", matchKind: "tool", matchValue: scenario.approvalTool, }); }; await setupActor(); if (scenario.taughtSkill) { // Human-authored fixture setup via the same API as the teaching UI; no model work is prefilled. await rpc(app, cookie, "computer/boot", { botId }); await rpc(app, cookie, "computer/takeover", { botId }); const skill = await rpc<{ id: string }>(app, cookie, "skills/start", { botId, goal: "Dispatch label", }); await rpc(app, cookie, "computer/input", { botId, kind: "key", payload: { key: "x" } }); await rpc(app, cookie, "skills/stop", { skillId: skill.id }); await rpc(app, cookie, "skills/updateDraft", { skillId: skill.id, name: "Dispatch label", playbook: { whenToUse: "When asked to run Dispatch label for a city and item count.", inputs: ["city", "item count"], steps: [ "Convert city to uppercase and item count to digits.", "Write results/dispatch.txt in your home, containing CITY | COUNT | PACKED with the actual values. For example PARIS | 2 | PACKED.", ], howToCheck: "Read the file and verify the city, count and PACKED marker.", whatToReturn: "The saved file path.", approvalBoundaries: "No connected-app writes are part of this workflow.", failureHandling: "Report a missing input; do not invent it.", }, }); await rpc(app, cookie, "skills/save", { skillId: skill.id, name: "Dispatch label" }); } phase = "product"; let lastText = ""; for (const [stepIndex, step] of scenario.steps.entries()) { if (Date.now() >= deadline) throw new EvalFailure("incomplete", "Trial time budget exceeded"); if ("newWorkspace" in step) { priorMemory = (await rpc>(app, cookie, "memory/list", { botId })) .map((m) => m.content) .join("\n"); await setupActor(); continue; } if ("clear" in step) { await Promise.all([captureTools(), captureUsage()]); await rpc(app, cookie, "threads/clear", { botId }); clearedToolCount = seenTools.size - (await countCurrentTools()); continue; } let runId: string; if ("ask" in step) { runId = ( await rpc<{ runId: string }>(app, cookie, "threads/send", { botId, text: step.ask }) ).runId; } else if ("slack" in step) { if (!messagingLinked) { const link = await rpc<{ code: string }>(app, cookie, "messaging/link/start", { botId }); await messaging.emitInbound({ isDirect: true, from: messagingAddress, fromLabel: "Teammate", content: link.code, }); await poll(async () => (messaging.sent.length > 0 ? true : undefined), deadline); messaging.resetWitnesses(); messagingLinked = true; } const inbound = await messaging.emitInbound({ isDirect: true, from: messagingAddress, fromLabel: "Teammate", content: step.slack, }); messagingOriginThreadId = inbound.threadId; runId = await poll( async () => ( await prisma.run.findFirst({ where: { botId, sourceMessage: { is: { clientNonce: `messaging:${inbound.provider}:${inbound.handle}`, }, }, }, orderBy: { createdAt: "desc" }, select: { id: true }, }) )?.id, deadline, ); } else { const routines = await prisma.routine.findMany({ where: { botId } }); if (routines.length !== 1) throw new EvalFailure("agent", "Expected exactly one authored routine"); const routine = routines[0]!; if (!routine.active || !routine.crons.length) throw new EvalFailure("agent", "Authored routine was not scheduled"); // Force the scheduler's clock edge, not the agent's work. Attribution starts after setup. services.calls.length = 0; services.seedGithubReleases([ { owner: "example", repo: "widget", tag: "v2.2.0", name: "Widget 2.2", body: "Adds CSV import validation.", publishedAt: "2026-01-03T09:00:00Z", htmlUrl: "https://example.test/widget/releases/v2.2.0", }, ...services.listGithubReleases(), ]); const scheduledFor = new Date(Date.now() - 1_000); await prisma.routine.update({ where: { id: routine.id }, data: { nextRunAt: scheduledFor }, }); await handles.jobs.enqueue({ name: "routine.wakeup", payload: { routineId: routine.id, scheduledFor: scheduledFor.toISOString() }, }); runId = await poll( async () => ( await prisma.run.findFirst({ where: { botId, routineId: routine.id, id: { notIn: runIds } }, orderBy: { createdAt: "desc" }, }) )?.id, deadline, ); } runIds.push(runId); let seenToolCount = result.toolCalls; const terminal = await poll(async () => { const [run, toolCount] = await Promise.all([ prisma.run.findUnique({ where: { id: runId }, select: { status: true, error: true } }), countCurrentTools().then((count) => count + clearedToolCount), ]); seenToolCount = toolCount; if (toolCount > options.maxToolCalls) throw new EvalFailure("incomplete", "Tool call budget exceeded"); if (run?.status === "waiting_input" && scenario.approvalTool) return run; if (run && ["waiting_input", "waiting_approval", "paused"].includes(run.status)) throw new EvalFailure("agent", "Agent requested assistance; no coaching supplied"); return run && isTerminal(run.status as RunStatus) ? run : undefined; }, deadline); const events = await prisma.event.findMany({ where: { runId, type: "agent.tool.called" }, orderBy: { seq: "asc" }, }); result.toolCalls = seenToolCount; result.trace.push({ step: stepIndex + 1, status: terminal.status, tools: events.map((e) => redact(String((e.payload as { name?: string }).name ?? "unknown"), secrets), ), }); if ( terminal.status !== "completed" && !(scenario.approvalTool && terminal.status === "waiting_input") ) { // Provider attribution requires recognizable protocol/auth evidence; unknown runtime failures remain product failures. const error = terminal.error ?? "Run did not complete"; const category = /\b(?:401|402|403|429)\b|rate.?limit|invalid api.?key|authentication.*(?:failed|error)/i.test( error, ) ? "provider" : "product"; throw new EvalFailure(category, error); } if ("slack" in step && terminal.status === "completed") { await waitForMessagingDelivery(prisma, runId, deadline); } const snap = await rpc<{ messages: Array<{ role: string; blocks: Array<{ kind: string; text?: string; approvalEffectId?: string }>; }>; }>(app, cookie, "threads/get", { botId }); approvalPending = terminal.status === "waiting_input" && snap.messages.some((m) => m.blocks.some((b) => b.kind === "ask" && typeof b.approvalEffectId === "string"), ); pendingApproval = approvalPending ? await prisma.externalEffect.findFirst({ where: { runId, status: "intended", id: { in: snap.messages.flatMap((m) => m.blocks.flatMap((b) => (b.approvalEffectId ? [b.approvalEffectId] : [])), ), }, }, select: { kind: true, request: true }, }) : null; lastText = snap.messages .filter((m) => m.role === "bot") .flatMap((m) => m.blocks.filter((b) => b.kind === "text").map((b) => b.text ?? "")) .join("\n"); } const files: Evidence["files"] = {}; for (const path of scenario.files ?? []) { try { files[path] = ( await rpc<{ content: string }>(handles.app, cookie, "computer/readFile", { botId, path }) ).content; } catch { files[path] = null; } } const memories = await rpc>(handles.app, cookie, "memory/list", { botId, }); const routines = await handles.prisma.routine.findMany({ where: { botId }, select: { name: true, prompt: true, crons: true, active: true }, }); await captureTools(); const evidence: Evidence = { text: lastText, files, records: services.records, notes: services.notes, calls: services.calls, routines, memory: memories.map((m) => m.content).join("\n"), approvalPending, pendingApproval, priorMemory, destinationWrites: handles.connector?.records.length ?? 0, toolNames: [...seenTools.values()].map((event) => event.name), messaging: { originThreadId: messagingOriginThreadId, outbound: structuredClone(messaging.sent), }, }; phase = "harness"; result.criteria = scenario.grade(evidence); result.status = result.criteria.length > 0 && result.criteria.every((c) => c.pass) ? "passed" : "failed"; result.category = result.status === "passed" ? null : "agent"; result.reason = result.status === "passed" ? null : "Outcome criteria failed"; if (result.status === "failed" && handles.harnessIssues?.length) { result.category = "harness"; result.reason = handles.harnessIssues.join(" "); } result.artifacts = Object.fromEntries( Object.entries({ ...files, "final-response": lastText, "memory-snapshot": evidence.memory, }).map(([key, value]) => [key, value === null ? null : redact(value, secrets)]), ); } catch (error) { result.status = "failed"; result.category = error instanceof EvalFailure ? error.category : phase; result.reason = redact( error instanceof Error ? error.message : "Unknown eval failure", secrets, ); } finally { if (handles) { try { await cleanupActors(handles, actors); } catch { result.cleanupFailed = true; } await captureUsage().catch(() => undefined); if (seenUsage.size) { result.inputTokens = [...seenUsage.values()].reduce((n, u) => n + u.inputTokens, 0); result.outputTokens = [...seenUsage.values()].reduce((n, u) => n + u.outputTokens, 0); } // Include interrupted and cleared-run calls without retaining raw arguments or ids in reports. await captureTools().catch(() => undefined); const events = [...seenTools.values()]; result.toolCalls = events.length; if (events.length > options.maxToolCalls) { result.status = "failed"; result.category = "incomplete"; result.reason = "Tool call budget exceeded"; } if (!result.trace.length && events.length) result.trace.push({ step: 0, status: "interrupted", tools: events.map((event) => event.name), }); } try { await handles?.stop(); } catch { result.cleanupFailed = true; } if (result.cleanupFailed) { result.status = "failed"; result.category = "harness"; result.reason = "Fixture cleanup failed; remaining trials must not start"; } result.latencyMs = Date.now() - started; } return result; } async function rpc(app: App, cookie: string, proc: string, body: unknown = {}): Promise { const res = await app.request(`/rpc/${proc}`, { method: "POST", headers: { "content-type": "application/json", cookie, origin: "http://127.0.0.1:5173" }, body: JSON.stringify({ json: body }), signal: AbortSignal.timeout(15_000), }); const parsed = (await res.json()) as { json?: T; error?: unknown }; if (!res.ok || parsed.error) throw new Error(`${proc} failed (${res.status})`); return parsed.json as T; } async function poll(read: () => Promise, deadline: number): Promise { while (Date.now() < deadline) { const result = await read(); if (result !== undefined) return result; await new Promise((resolve) => setTimeout(resolve, 200)); } throw new EvalFailure("incomplete", "Trial time budget exceeded"); } async function waitForMessagingDelivery( prisma: EvalApp["prisma"], runId: string, deadline: number, ): Promise { await poll( async () => ( await prisma.run.findUnique({ where: { id: runId }, select: { messagingMirroredAt: true }, }) )?.messagingMirroredAt ?? undefined, deadline, ); const messages = await prisma.message.findMany({ where: { runId, role: "bot" }, select: { id: true }, }); await poll(async () => { const outbound = await prisma.messagingOutbound.findMany({ where: { sourceMessageId: { in: messages.map((message) => message.id) } }, select: { status: true, providerHandle: true }, }); return outbound.length > 0 && outbound.every((row) => row.providerHandle !== null || row.status === "failed") ? true : undefined; }, deadline); } function actorScopes(actors: readonly EvalActor[]) { return actors.map(({ userId, spaceId }) => ({ userId, spaceId })); } async function findScopeBots( prisma: EvalApp["prisma"], actors: readonly EvalActor[], ): Promise> { if (!actors.length) return []; return prisma.bot.findMany({ where: { OR: actorScopes(actors) }, select: { id: true, userId: true, spaceId: true }, }); } function cookieForBot(bot: { userId: string; spaceId: string }, actors: readonly EvalActor[]) { return actors.find((actor) => actor.userId === bot.userId && actor.spaceId === bot.spaceId) ?.cookie; } /** Disable scheduled work and stop every run in each isolated actor scope. */ export async function cleanupActors( handles: Pick, actors: readonly EvalActor[], ): Promise { if (!actors.length) return; const failures: unknown[] = []; const scopes = actorScopes(actors); let stable = false; // A parent can finish spawning a child while its abort is settling. Repeat until // every bot and run visible after abort was included in the pass. for (let pass = 0; pass < 8; pass++) { let bots: Array<{ id: string; userId: string; spaceId: string }> = actors.map((actor) => ({ id: actor.botId, userId: actor.userId, spaceId: actor.spaceId, })); let runsBeforeStop: Array<{ id: string }> = []; try { await handles.prisma.routine.updateMany({ where: { OR: scopes }, data: { active: false, nextRunAt: null }, }); } catch (error) { failures.push(error); } try { bots = await findScopeBots(handles.prisma, actors); } catch (error) { failures.push(error); } try { runsBeforeStop = await handles.prisma.run.findMany({ where: { OR: scopes }, select: { id: true }, }); } catch (error) { failures.push(error); } for (const bot of bots) { const actorCookie = cookieForBot(bot, actors); if (!actorCookie) { failures.push(new Error("No actor session for scoped bot")); continue; } try { await rpc(handles.app, actorCookie, "threads/stop", { botId: bot.id }); } catch (error) { failures.push(error); } } let runsAfterStop: Array<{ id: string }> = []; try { runsAfterStop = await handles.prisma.run.findMany({ where: { OR: scopes }, select: { id: true }, }); } catch (error) { failures.push(error); } const handledRunIds = new Set([...runsBeforeStop, ...runsAfterStop].map((run) => run.id)); for (const runId of handledRunIds) { try { await handles.jobs.cancel(runJobKey(runId)); } catch (error) { failures.push(error); } } for (const runId of handledRunIds) { try { await handles.runtime.abort(runId); } catch (error) { failures.push(error); } } let botsAfter: Array<{ id: string; userId: string; spaceId: string }> | undefined; let runsAfter: Array<{ id: string; status: string }> | undefined; let activeRoutineCount: number | undefined; try { await handles.prisma.routine.updateMany({ where: { OR: scopes }, data: { active: false, nextRunAt: null }, }); [botsAfter, runsAfter, activeRoutineCount] = await Promise.all([ findScopeBots(handles.prisma, actors), handles.prisma.run.findMany({ where: { OR: scopes }, select: { id: true, status: true }, }), handles.prisma.routine.count({ where: { OR: scopes, active: true } }), ]); } catch (error) { failures.push(error); } if ( botsAfter && runsAfter && activeRoutineCount === 0 && botsAfter.every((bot) => bots.some((stopped) => stopped.id === bot.id)) && runsAfter.every( (run) => handledRunIds.has(run.id) && !(ACTIVE_RUN_STATUSES as readonly string[]).includes(run.status), ) ) { stable = true; break; } } if (!stable) failures.push(new Error("Trial scope did not become idle")); if (failures.length) throw new Error("Trial work could not be stopped", { cause: failures }); }