export type FrontendBenchmarkProgressSurface = 'flow' | 'app' | 'script' | 'global' export type FrontendBenchmarkProgressEvent = | { type: 'run-start' surface: FrontendBenchmarkProgressSurface totalCases: number runs: number concurrency: number } | { type: 'attempt-start' surface: FrontendBenchmarkProgressSurface caseId: string caseNumber: number totalCases: number attempt: number runs: number } | { type: 'attempt-finish' surface: FrontendBenchmarkProgressSurface caseId: string caseNumber: number totalCases: number attempt: number runs: number passed: boolean durationMs: number judgeScore: number | null error: string | null } | { type: 'assistant-message-start' surface: FrontendBenchmarkProgressSurface caseId: string caseNumber: number totalCases: number attempt: number runs: number } | { type: 'assistant-chunk' surface: FrontendBenchmarkProgressSurface caseId: string caseNumber: number totalCases: number attempt: number runs: number chunk: string } | { type: 'assistant-message-end' surface: FrontendBenchmarkProgressSurface caseId: string caseNumber: number totalCases: number attempt: number runs: number } | { type: 'tool-call' surface: FrontendBenchmarkProgressSurface caseId: string caseNumber: number totalCases: number attempt: number runs: number toolName: string argumentsText: string } export const FRONTEND_BENCHMARK_PROGRESS_PREFIX = 'WMILL_FRONTEND_AI_EVAL_PROGRESS ' export function emitFrontendBenchmarkProgress(event: FrontendBenchmarkProgressEvent): void { process.stderr.write( `${FRONTEND_BENCHMARK_PROGRESS_PREFIX}${JSON.stringify(event)}\n` ) } export function parseFrontendBenchmarkProgressLine( line: string ): FrontendBenchmarkProgressEvent | null { if (!line.startsWith(FRONTEND_BENCHMARK_PROGRESS_PREFIX)) { return null } try { const parsed = JSON.parse( line.slice(FRONTEND_BENCHMARK_PROGRESS_PREFIX.length) ) as FrontendBenchmarkProgressEvent return parsed?.type ? parsed : null } catch { return null } } export function formatFrontendBenchmarkProgressEvent( event: FrontendBenchmarkProgressEvent ): string { switch (event.type) { case 'run-start': return `Running ${event.surface}: ${event.totalCases} cases x ${event.runs} run${event.runs === 1 ? '' : 's'}, concurrency ${event.concurrency}` case 'attempt-start': return `${formatCasePrefix(event.caseNumber, event.totalCases)} ${event.caseId} attempt ${event.attempt}/${event.runs}...` case 'attempt-finish': { const parts = [ `${formatCasePrefix(event.caseNumber, event.totalCases)} ${event.caseId} attempt ${event.attempt}/${event.runs} ${event.passed ? 'pass' : 'fail'}`, formatDuration(event.durationMs) ] if (event.judgeScore !== null) { parts.push(`judge ${formatNumber(event.judgeScore)}`) } if (event.error) { parts.push(truncateSingleLine(event.error, 120)) } return parts.join(' | ') } case 'assistant-message-start': case 'assistant-chunk': case 'assistant-message-end': return '' case 'tool-call': return `${formatCasePrefix(event.caseNumber, event.totalCases)} ${event.caseId} attempt ${event.attempt}/${event.runs} tool ${event.toolName} ${truncateSingleLine(event.argumentsText, 200)}` } } function formatCasePrefix(caseNumber: number, totalCases: number): string { return `[${caseNumber}/${totalCases}]` } function formatDuration(durationMs: number): string { return `${formatNumber(durationMs / 1000)}s` } function formatNumber(value: number): string { return Number.isInteger(value) ? String(value) : value.toFixed(1) } function truncateSingleLine(value: string, maxLength: number): string { const normalized = value.replace(/\s+/g, ' ').trim() if (normalized.length <= maxLength) { return normalized } return `${normalized.slice(0, Math.max(0, maxLength - 3))}...` }