import { workerStatus, type RunState } from '../runner/state.ts'; import type { ObservableEvent } from './types.ts'; import type { PublicationView } from './publication.ts'; import type { CommandFacts } from './run-reader.ts'; import type { RunManifest, RunResult } from './run-resolver.ts'; export interface RunSummary { runId: string; executionId?: number; repo?: string; prNumber?: number; task?: string; permission?: string; commandExecutionType?: string; phase?: string; status: string; worker: 'idle' | 'running' | 'interrupted' | 'unknown'; currentActivity: string; startedAt?: string; lastEventAt?: string; provider?: string; model?: string; requests: number; attempts: number; retries: number; tools: number; toolErrors: number; errors: number; validations: number; ciPolls: number; commits: number; pushes: number; remoteConfirmations: number; publications: number; workspace?: string; head?: string; lastCommit?: string; lastPush?: string; result?: RunResult; publication?: PublicationView; } export interface RunSummaryInput { runId: string; manifest?: RunManifest; result?: RunResult | null; state?: RunState | null; events: ObservableEvent[]; publication?: PublicationView; command?: CommandFacts; } function detailText(event: ObservableEvent, key: string) { const value = event.detail?.[key]; return typeof value === 'string' ? value : undefined; } function detailIdentifier(event: ObservableEvent, key: string) { const value = event.detail?.[key]; return typeof value === 'string' || typeof value === 'number' ? String(value) : undefined; } function detailNumber(event: ObservableEvent, key: string) { const value = event.detail?.[key]; return typeof value === 'number' && Number.isFinite(value) ? value : undefined; } function detailIndex(event: ObservableEvent) { const value = detailNumber(event, 'index'); return value === undefined ? undefined : String(value); } function manifestTask(manifest: RunManifest | undefined) { const chain = manifest?.task_chain; return Array.isArray(chain) && typeof chain.at(-1) === 'string' ? chain.at(-1) as string : undefined; } export function summarizeRun(input: RunSummaryInput): RunSummary { const result = input.result ?? undefined; const state = input.state ?? null; const summary: RunSummary = { runId: input.runId, executionId: typeof input.manifest?.execution_id === 'number' ? input.manifest.execution_id : state?.execution_id, repo: typeof input.manifest?.repo === 'string' ? input.manifest.repo : state?.repo, prNumber: typeof input.manifest?.pr_number === 'number' ? input.manifest.pr_number : state?.pr_number, task: manifestTask(input.manifest), permission: input.command?.permission, commandExecutionType: input.command?.executionType, phase: state?.phase, status: typeof result?.status === 'string' ? result.status : state?.active ? 'active' : state?.phase ?? 'unknown', worker: state ? workerStatus(state) : result ? 'idle' : 'unknown', currentActivity: 'idle', startedAt: typeof input.manifest?.started_at === 'string' ? input.manifest.started_at : undefined, requests: 0, attempts: 0, retries: 0, tools: 0, toolErrors: 0, errors: 0, validations: 0, ciPolls: 0, commits: 0, pushes: 0, remoteConfirmations: 0, publications: 0, head: typeof result?.final_head_sha === 'string' && result.final_head_sha ? result.final_head_sha : state?.current_head_sha || undefined, result, ...(input.publication ? { publication: input.publication } : {}), }; const activeTools = new Map(); const pendingProviders = new Set(); let validationPending = false; let waitingForCi = false; let lastTitle = 'idle'; for (const event of input.events) { summary.lastEventAt = event.time; summary.executionId = event.executionId ?? summary.executionId; lastTitle = event.title; const source = event.sourceEvent ?? ''; const request = detailIdentifier(event, 'request'); const index = detailIndex(event); if (source === 'phase' && detailText(event, 'phase')) summary.phase = detailText(event, 'phase'); if (source === 'task_turn_start' || source === 'task_turn_end' || source === 'repair_started') summary.task = detailText(event, 'task') ?? summary.task; if (source === 'model_request') { summary.requests++; summary.provider = detailText(event, 'provider') ?? summary.provider; summary.model = detailText(event, 'model') ?? summary.model; pendingProviders.add(request ?? `request-${summary.requests}`); } if (source === 'provider_attempt') { summary.attempts++; pendingProviders.add(request ?? `attempt-${summary.attempts}`); } if (source === 'provider_retry') { summary.retries++; if (request !== undefined) pendingProviders.add(request); } if (source === 'model_response' || source === 'provider_error') if (request !== undefined) pendingProviders.delete(request); if (source === 'tool_start') { summary.tools++; activeTools.set(index ?? `tool-${summary.tools}`, detailText(event, 'tool') ?? 'unknown'); } if (source === 'tool_end') { const tool = detailText(event, 'tool'); const activeKey = index ?? [...activeTools.entries()].reverse().find(([, name]) => name === tool)?.[0]; if (activeKey) activeTools.delete(activeKey); const exitCode = detailNumber(event, 'exit_code'); if (event.detail?.error || (exitCode !== undefined && exitCode !== 0)) summary.toolErrors++; } if (event.kind === 'error') summary.errors++; if (source === 'validation' || source === 'repair_verification') summary.validations++; if (source === 'ci_poll') summary.ciPolls++; if (source === 'repair_verification_requested') validationPending = true; if (source === 'repair_verification') validationPending = false; if (source === 'ci_observation_wait') waitingForCi = true; if (source === 'ci_observation_complete') waitingForCi = false; if (source === 'repair_commit') { summary.commits++; summary.lastCommit = detailText(event, 'sha') ?? summary.lastCommit; summary.head = summary.lastCommit ?? summary.head; } if (source === 'repair_push') { summary.pushes++; summary.lastPush = detailText(event, 'sha') ?? summary.lastPush; summary.head = summary.lastPush ?? summary.head; } if (source.endsWith('push_confirmed')) summary.remoteConfirmations++; if (event.kind === 'publication') summary.publications++; summary.workspace = detailText(event, 'workspace') ?? summary.workspace; } const activeTool = activeTools.values().next().value as string | undefined; if (activeTool) summary.currentActivity = `running tool ${activeTool}`; else if (pendingProviders.size) summary.currentActivity = 'waiting for provider'; else if (state?.waiting_for_ci || waitingForCi) summary.currentActivity = 'waiting for CI'; else if (validationPending) summary.currentActivity = 'waiting for validation'; else if (summary.worker === 'interrupted') summary.currentActivity = 'worker interrupted'; else summary.currentActivity = lastTitle; if (summary.worker === 'idle' && result) summary.currentActivity = 'terminal'; return summary; }