import type { AgentRunStore } from "./store.js"; import type { CommandRecord, JsonRecord, JsonValue, RunEvent, RunRecord, RunnerJobRecord, TerminalStatus } from "../common/types.js"; import { outputBytesFromPayload, outputTruncatedFromPayload } from "../common/output.js"; export async function buildRunResult(store: AgentRunStore, runId: string, commandId?: string): Promise { const run = await store.getRun(runId); const command = await selectCommand(store, runId, commandId); const events = await store.listEvents(runId, 0, 500); const scopedEvents = command ? eventsForCommand(events, command.id) : events; const jobs = await store.listRunnerJobs(runId, command?.id); const latestJob = jobs.at(-1) ?? null; const commandTerminal = command ? terminalFromCommand(command) : null; const terminalEventStatus = terminalFromEvents(scopedEvents); const terminal = commandTerminal ?? terminalEventStatus ?? run.terminalStatus; const terminalSource = commandTerminal ? "command-record" : terminalEventStatus ? "terminal_status-event" : run.terminalStatus ? "run-record" : "none"; const failureKind = command ? failureKindFromEvents(scopedEvents) : run.failureKind ?? failureKindFromEvents(scopedEvents); const failureMessage = command ? messageFromEvents(scopedEvents) : run.failureMessage ?? messageFromEvents(scopedEvents); const reply = assistantReply(scopedEvents); const blocker = terminal === "blocked" || terminal === "failed" ? { failureKind, message: failureMessage } : null; return { runId: run.id, commandId: command?.id ?? commandId ?? null, attemptId: latestJob?.attemptId ?? attemptFromEvents(events), runnerId: latestJob?.runnerId ?? null, jobName: latestJob?.jobName ?? null, namespace: latestJob?.namespace ?? null, status: command?.state ?? run.status, runStatus: run.status, commandState: command?.state ?? null, terminalStatus: terminal, terminalSource, completed: terminal === "completed", reply, failureKind, failureMessage, blocker, lastSeq: events.at(-1)?.seq ?? 0, eventCount: events.length, artifactSummary: artifactSummary(scopedEvents), sessionRef: sessionSummary(run), resourceBundleRef: resourceBundleSummary(run, events), runnerJobCount: jobs.length, }; } async function selectCommand(store: AgentRunStore, runId: string, commandId?: string): Promise { if (commandId) { const command = await store.getCommand(commandId); return command.runId === runId ? command : null; } const commands = await store.listCommands(runId, 0, 100); return commands.at(-1) ?? null; } function terminalFromEvents(events: RunEvent[]): TerminalStatus | null { for (const event of [...events].reverse()) { if (event.type !== "terminal_status") continue; const value = event.payload.terminalStatus; if (value === "completed" || value === "failed" || value === "blocked" || value === "cancelled") return value; } return null; } function terminalFromCommand(command: CommandRecord): TerminalStatus | null { if (command.state === "completed") return "completed"; if (command.state === "failed") return "failed"; if (command.state === "cancelled") return "cancelled"; return null; } function eventsForCommand(events: RunEvent[], commandId: string): RunEvent[] { const scoped = events.filter((event) => event.payload.commandId === commandId || typeof event.payload.commandId !== "string"); return scoped.length > 0 ? scoped : events; } function failureKindFromEvents(events: RunEvent[]): string | null { for (const event of [...events].reverse()) { const value = event.payload.failureKind; if (typeof value === "string") return value; } return null; } function messageFromEvents(events: RunEvent[]): string | null { for (const event of [...events].reverse()) { const value = event.payload.message; if (typeof value === "string" && value.length > 0) return value; } return null; } function assistantReply(events: RunEvent[]): string { const assistantEvents = events.filter((event) => event.type === "assistant_message"); const authoritative = assistantEvents .filter((event) => event.payload.replyAuthority === true || event.payload.final === true) .map((event) => textPayload(event.payload)) .filter((text) => text.length > 0); const latestAuthoritative = authoritative.at(-1); if (latestAuthoritative) return latestAuthoritative; const completedAgentMessages = assistantEvents .filter((event) => event.payload.source === "completed-agent-message") .map((event) => textPayload(event.payload)) .filter((text) => text.length > 0); const latestCompletedAgentMessage = completedAgentMessages.at(-1); if (latestCompletedAgentMessage) return latestCompletedAgentMessage; return assistantEvents.map((event) => textPayload(event.payload)).filter((text) => text.length > 0).join(""); } function textPayload(payload: JsonRecord): string { for (const key of ["text", "content", "delta"]) { const value = payload[key]; if (typeof value === "string") return value; } return ""; } function artifactSummary(events: RunEvent[]): JsonRecord { let commandOutputEvents = 0; let diffEvents = 0; let toolCallEvents = 0; let outputChars = 0; let outputBytes = 0; let outputTruncatedEvents = 0; const streamSummary: Record = { stdout: { events: 0, outputBytes: 0, outputTruncated: false }, stderr: { events: 0, outputBytes: 0, outputTruncated: false }, }; for (const event of events) { if (event.type === "command_output") { commandOutputEvents += 1; const text = textPayload(event.payload); outputChars += text.length; const bytes = outputBytesFromPayload(event.payload); outputBytes += bytes; const truncated = outputTruncatedFromPayload(event.payload); if (truncated) outputTruncatedEvents += 1; const stream = event.payload.stream === "stderr" ? "stderr" : "stdout"; streamSummary[stream].events += 1; streamSummary[stream].outputBytes += bytes; streamSummary[stream].outputTruncated ||= truncated; } if (event.type === "diff") diffEvents += 1; if (event.type === "tool_call") toolCallEvents += 1; } return { commandOutputEvents, diffEvents, toolCallEvents, outputChars, outputBytes, truncatedEvents: outputTruncatedEvents, outputTruncatedEvents, stdoutSummary: streamSummary.stdout, stderrSummary: streamSummary.stderr }; } function attemptFromEvents(events: RunEvent[]): string | null { for (const event of [...events].reverse()) { const value = event.payload.attemptId; if (typeof value === "string") return value; } return null; } function sessionSummary(run: RunRecord): JsonRecord | null { if (!run.sessionRef) return null; return { sessionId: run.sessionRef.sessionId, conversationId: run.sessionRef.conversationId ?? null, threadId: run.sessionRef.threadId ?? null, expiresAt: run.sessionRef.expiresAt ?? null, metadataKeys: Object.keys(run.sessionRef.metadata ?? {}).sort(), valuesPrinted: false, }; } function resourceBundleSummary(run: RunRecord, events: RunEvent[]): JsonRecord | null { if (!run.resourceBundleRef) return null; const materialized = events.find((event) => event.type === "backend_status" && event.payload.phase === "resource-bundle-materialized")?.payload ?? null; return { kind: run.resourceBundleRef.kind, repoUrl: run.resourceBundleRef.repoUrl, commitId: run.resourceBundleRef.commitId, bundles: { count: run.resourceBundleRef.bundles.length, items: run.resourceBundleRef.bundles.map((item) => ({ name: item.name ?? null, repoUrl: item.repoUrl ?? run.resourceBundleRef?.repoUrl ?? null, commitId: item.commitId ?? run.resourceBundleRef?.commitId ?? null, subpath: item.subpath, targetPath: item.targetPath, valuesPrinted: false })), valuesPrinted: false, }, promptRefs: run.resourceBundleRef.promptRefs ? { count: run.resourceBundleRef.promptRefs.length, names: run.resourceBundleRef.promptRefs.map((item) => item.name), required: run.resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false }, materialized: materialized as JsonValue, }; }