/** * Preprocesses experiment results into a single JSON for the browser. * Computes per-trace FSM traversals for the interpretability view. * Run: npx tsx scripts/build-data.ts */ import { readFileSync, writeFileSync, mkdirSync } from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const ROOT = path.resolve(__dirname, '../..') // Active 12 datasets (paper set). tau-bench v1 (taubench-airline/retail) was dropped // and replaced by tau2-bench; excluded here so the dashboard reflects 12, not 14. const DATASETS = [ 'swesmith', 'whoandwhen', 'mind2web', 'sweagent', 'gui-odyssey', 'webarena', 'agentnet', 'tau2bench-airline', 'tau2bench-retail', 'tau2bench-telecom', 'atbench', 'osworld', ] as const // ---------- Types ---------- interface TraceStep { idx: number role: string activity: string fromState: string toState: string | null // null = FSM failure consumed: boolean } interface TraceDetail { id: string split: 'train' | 'test' success: boolean | null fullFitness: number | null successFitness: number | null steps: TraceStep[] stateSequence: string[] fitness: number consumed: number total: number firstFailIdx: number // -1 if no failure stateVisits: Record } interface DatasetBundle { id: string meta: { name: string shortName: string venue: string traces: number trainSize: number testSize: number activities: number description: string } baselines: Array<{ method: string label: string trainFitness: number testFitness: number states: number | null transitions: number | null randomAcceptRate: number | null permutedAcceptRate: number | null pmPrecision: number | null category: string note: string }> convergence: { stateConvergenceAt: number transitionConvergenceAt: number fitnessConvergenceAt: number curve: Array<{ n: number; states: number; transitions: number; fitness: number }> } traces: TraceDetail[] downstream: { fitnessProfile: { mean: number; std: number; min: number; max: number anomalyCount: number; anomalies: string[] histogram: Array<{ min: number; max: number; count: number }> } | null behavioralProfiling: { stateVisitDistribution: Array<{ state: string; count: number; fraction: number }> transitionFrequency: Array<{ transition: string; count: number; fraction: number }> deadTransitions: string[] transitionCoverage: number avgUniqueStatesPerTrace: number } | null structuralAnalysis: { density: number hubs: Array<{ id: string; outDegree: number; inDegree: number }> sinks: string[] sources: string[] selfLoops: string[] nonTrivialSCCs: string[][] } | null differentialFSM: { successOnlyTransitions: string[] failureOnlyTransitions: string[] sharedTransitions: string[] } | null failurePrediction: Record | null mistakeLocalization: Record | null neuralProbe: { seq_mlp: { cv: number; std: number; holdout: number } fsm_mlp: { cv: number; std: number; holdout: number } seq_gru: { cv: number; std: number; holdout: number } fsm_gru: { cv: number; std: number; holdout: number } seq_transf: { cv: number; std: number; holdout: number } fsm_transf: { cv: number; std: number; holdout: number } } | null workflowMemory: { n: number noMemory: { accuracy: number; correct: number } awm: { accuracy: number; correct: number } fsm: { accuracy: number; correct: number } fsmGain: number } | null crossModel: { models: Array<{ name: string; shortName: string; traces: number; successRate: number }> matrices: { fitness: number[][] auroc: number[][] stateCount: number[][] } summary: { diagonalAUROC: { mean: number; std: number } offDiagonalAUROC: { mean: number; std: number } aurocTransferGap: number } } | null monitor: { trainSize: number; testSize: number; succSize: number; failSize: number configs: Array<{ config: string; gamma: number; window: number monitoringCurve: Array<{ point: number; threshold: number; f1: number; precision: number; recall: number }> }> baselines: Record }> } | null counterfactual: { nSuccess: number; nFailure: number decisionPoints: Array<{ state: string; branching: number; targets: string[] }> pathDiversity: { uniqueSuccPaths: number; uniqueFailPaths: number; overlap: number } dpDivergence: Array<{ state: string; jsd: number }> editDistance: { meanInternal?: number; meanCross?: number } | null } | null } multiseed: { numSeeds: number ourFSM: { testFitness: { mean: number; std: number }; states: { mean: number; std: number } } rpni: { testFitness: { mean: number; std: number }; states: { mean: number; std: number } } awm: { testFitness: { mean: number; std: number } } } | null graph: { states: Array<{ id: string }> transitions: Array<{ source: string; target: string; subject: string }> } } const META: Record = { swesmith: { name: 'SWE-smith', shortName: 'SWE', venue: 'NeurIPS 2025 D&B', traces: 500, trainSize: 400, testSize: 100, activities: 9, description: 'Coding agent trajectories with tool calls (bash, str_replace_editor, submit)', }, whoandwhen: { name: 'Who & When to Delegate', shortName: 'W&W', venue: 'arXiv 2505.00212', traces: 184, trainSize: 147, testSize: 37, activities: 8, description: 'Multi-agent delegation failures across 8 specialized actors', }, mind2web: { name: 'Mind2Web', shortName: 'M2W', venue: 'NeurIPS 2023', traces: 500, trainSize: 400, testSize: 100, activities: 7, description: 'Web navigation agent traces with click, type, select, hover, enter actions', }, 'taubench-airline': { name: 'tau-bench (airline)', shortName: 'tau-air', venue: 'Sierra 2024', traces: 200, trainSize: 160, testSize: 40, activities: 5, description: 'Customer service agent traces for airline booking/cancellation with 12 APIs', }, 'taubench-retail': { name: 'tau-bench (retail)', shortName: 'tau-ret', venue: 'Sierra 2024', traces: 460, trainSize: 368, testSize: 92, activities: 5, description: 'Customer service agent traces for retail order management with 14 APIs', }, sweagent: { name: 'SWE-agent', shortName: 'SWE-a', venue: 'NeurIPS 2024', traces: 2000, trainSize: 1600, testSize: 400, activities: 68, description: 'SWE-agent coding trajectories with 68 unique bash/editor commands', }, atbench: { name: 'ATBench', shortName: 'ATBench', venue: 'arXiv 2604.02022', traces: 1000, trainSize: 800, testSize: 200, activities: 14, description: 'Agent trajectory safety benchmark with balanced safe/unsafe outcomes (503 safe / 497 unsafe)', }, osworld: { name: 'OSWorld', shortName: 'OSWorld', venue: 'NeurIPS 2024', traces: 2166, trainSize: 1732, testSize: 434, activities: 27, description: 'Desktop GUI agent traces (Gelato-30B, GTA1-32B) across real OS applications', }, 'gui-odyssey': { name: 'GUI-Odyssey', shortName: 'GUI', venue: 'CVPR 2025', traces: 7735, trainSize: 6188, testSize: 1547, activities: 6, description: 'Mobile GUI agent traces with click, type, scroll, swipe, enter, home actions', }, webarena: { name: 'WebArena', shortName: 'WA', venue: 'ICLR 2024 Oral', traces: 8337, trainSize: 6670, testSize: 1667, activities: 24, description: 'Web agent traces on realistic websites with navigation, form-filling, and content extraction', }, agentnet: { name: 'AgentNet', shortName: 'ANet', venue: 'NeurIPS 2024', traces: 5000, trainSize: 4000, testSize: 1000, activities: 24, description: 'Desktop GUI agent traces across diverse applications', }, 'tau2bench-airline': { name: 'tau2-bench (airline)', shortName: 'tau2-air', venue: 'arXiv 2506.07982', traces: 800, trainSize: 640, testSize: 160, activities: 17, description: 'Customer service agent traces (4 LLMs) for airline booking with 12 APIs', }, 'tau2bench-retail': { name: 'tau2-bench (retail)', shortName: 'tau2-ret', venue: 'arXiv 2506.07982', traces: 1824, trainSize: 1459, testSize: 365, activities: 18, description: 'Customer service agent traces (4 LLMs) for retail order management with 14 APIs', }, 'tau2bench-telecom': { name: 'tau2-bench (telecom)', shortName: 'tau2-tel', venue: 'arXiv 2506.07982', traces: 1824, trainSize: 1459, testSize: 365, activities: 4, description: 'Customer service agent traces (4 LLMs) for telecom workflows', }, } // ---------- Activity extraction (must match shared.ts exactly) ---------- function extractActivity(role: string, content: string, agentName?: string): string { const actor = agentName || role const c = content.trim() const bracketMatch = c.match(/^\[(\w+)\]/) if (bracketMatch) return `${actor}:${bracketMatch[1].toLowerCase()}` if (c.includes('function_call') || c.includes('tool_call')) { // Match "tool_call:toolname" anywhere in content (loader format) const prefixMatch = c.match(/tool_call:(\w+)/) if (prefixMatch) return `${actor}:tool:${prefixMatch[1]}` // Match "name='toolname'" or "name: toolname" (OpenAI/generic format) const nameMatch = c.match(/name['"=:\s]+(\w+)/) return nameMatch ? `${actor}:tool:${nameMatch[1]}` : `${actor}:tool_call` } return `${actor}:text` } // ---------- FSM replay ---------- type TransitionFn = (state: string, symbol: string) => string | null function graphToTransitionFn(graph: { transitions: Array<{ source: string; target: string }> }): TransitionFn { const valid = new Set() for (const t of graph.transitions) { valid.add(`${t.source}|${t.target}`) } return (current: string, symbol: string) => valid.has(`${current}|${symbol}`) ? symbol : null } function replayTrace( transitionFn: TransitionFn, messages: Array<{ role: string; content: string; name?: string }>, ): { steps: TraceStep[]; stateSequence: string[]; fitness: number; consumed: number; total: number; firstFailIdx: number; stateVisits: Record } { let current = 'init' const steps: TraceStep[] = [] const stateSequence: string[] = ['init'] const stateVisits: Record = { init: 1 } let consumed = 0 let firstFailIdx = -1 for (let i = 0; i < messages.length; i++) { const msg = messages[i] const activity = extractActivity(msg.role, msg.content, msg.name) const next = transitionFn(current, activity) if (next !== null) { steps.push({ idx: i, role: msg.role, activity, fromState: current, toState: next, consumed: true }) stateSequence.push(next) stateVisits[next] = (stateVisits[next] || 0) + 1 current = next consumed++ } else { steps.push({ idx: i, role: msg.role, activity, fromState: current, toState: null, consumed: false }) stateSequence.push(`FAIL:${activity}`) if (firstFailIdx === -1) firstFailIdx = i } } return { steps, stateSequence, fitness: messages.length > 0 ? consumed / messages.length : 1, consumed, total: messages.length, firstFailIdx, stateVisits, } } // ---------- Main ---------- function loadJSON(p: string) { return JSON.parse(readFileSync(path.join(ROOT, p), 'utf-8')) } function tryLoadJSON(p: string): Record | null { try { return loadJSON(p) } catch { return null } } function loadTauBenchRaw(domain: string): Array<{ id: string; messages: Array<{ role: string; content: string; name?: string }>; annotations?: { success?: boolean } }> { const raw: Array<{ task_id: number; reward: number; traj: Array<{ role: string; content: string; tool_calls?: Array<{ function: { name: string } }> }> }> = loadJSON(`data/taubench/${domain}.json`) return raw.map(entry => ({ id: `taubench-${domain}-${entry.task_id}`, messages: entry.traj.map(step => { if (step.role === 'assistant' && step.tool_calls && step.tool_calls.length > 0) { return { role: 'assistant', content: `tool_call:${step.tool_calls[0].function.name}` } } if (step.role === 'tool') { const c = step.content || '' return { role: 'tool', content: c.length > 500 ? c.slice(0, 500) : c } } return { role: step.role || 'user', content: step.content || '' } }), annotations: { success: entry.reward === 1.0 }, })) } const TAU2_FILES: Record = { airline: [ 'data/tau2bench/gpt-4.1-2025-04-14_airline_default_gpt-4.1-2025-04-14_4trials.json', 'data/tau2bench/claude-3-7-sonnet-20250219_airline_default_gpt-4.1-2025-04-14_4trials.json', 'data/tau2bench/gpt-4.1-mini-2025-04-14_airline_base_gpt-4.1-2025-04-14_4trials.json', 'data/tau2bench/o4-mini-2025-04-16_airline_default_gpt-4.1-2025-04-14_4trials.json', ], retail: [ 'data/tau2bench/gpt-4.1-2025-04-14_retail_default_gpt-4.1-2025-04-14_4trials.json', 'data/tau2bench/claude-3-7-sonnet-20250219_retail_default_gpt-4.1-2025-04-14_4trials.json', 'data/tau2bench/gpt-4.1-mini-2025-04-14_retail_base_gpt-4.1-2025-04-14_4trials.json', 'data/tau2bench/o4-mini-2025-04-16_retail_default_gpt-4.1-2025-04-14_4trials.json', ], telecom: [ 'data/tau2bench/telecom_gpt41.json', 'data/tau2bench/claude-3-7-sonnet-20250219_telecom_default_gpt-4.1-2025-04-14_4trials.json', 'data/tau2bench/gpt-4.1-mini-2025-04-14_telecom_base_gpt-4.1-2025-04-14_4trials.json', 'data/tau2bench/o4-mini-2025-04-16_telecom-workflow_default_gpt-4.1-2025-04-14_4trials.json', ], } function loadTau2BenchRaw(domain: string): Array<{ id: string; messages: Array<{ role: string; content: string; name?: string }>; annotations?: { success?: boolean } }> { const files = TAU2_FILES[domain] || [] const allTraces: Array<{ id: string; messages: Array<{ role: string; content: string; name?: string }>; annotations?: { success?: boolean } }> = [] for (const f of files) { try { const data: { simulations: Array<{ task_id: string; messages: Array<{ role: string; content: string; tool_calls?: Array<{ name?: string; function?: { name: string } }> }>; reward_info: { reward: number } }> } = loadJSON(f) const modelTag = f.replace(/.*\//, '').replace(/\.json$/, '').slice(0, 30) for (let idx = 0; idx < data.simulations.length; idx++) { const sim = data.simulations[idx] const messages = sim.messages .filter((m: { role: string }) => m.role !== 'system') .map((step: { role: string; content: string; tool_calls?: Array<{ name?: string; function?: { name: string } }> }) => { if (step.tool_calls && step.tool_calls.length > 0) { const tc = step.tool_calls[0] const toolName = tc.name || tc.function?.name || 'unknown' return { role: 'assistant', content: `tool_call:${toolName}` } } if (step.role === 'tool') { const c = step.content || '' return { role: 'tool', content: c.length > 500 ? c.slice(0, 500) : c } } return { role: step.role || 'user', content: step.content || '' } }) allTraces.push({ id: `tau2bench-${domain}-${modelTag}-${sim.task_id}-${idx}`, messages, annotations: { success: sim.reward_info.reward === 1.0 }, }) } } catch { console.log(` Warning: could not load ${f}`) } } return allTraces } const neuralProbe: Record = tryLoadJSON('experiments/fsm-neural-probe/results.json') || {} // Paper Table 4 (LLM-judged top-1, gpt-4.1-mini, fsmWorkflow format, FULL validation split). // Authoritative for the 8 datasets in the paper; older poc-fsm-memory JSONs used gpt-4o-mini // with a different prompt format and are not directly comparable. const PAPER_TABLE_4: Record = { webarena: { n: 4800, awm: 0.655, fsm: 0.812 }, swesmith: { n: 300, awm: 0.747, fsm: 1.000 }, sweagent: { n: 1200, awm: 0.677, fsm: 0.705 }, 'tau2bench-telecom': { n: 1095, awm: 0.285, fsm: 0.456 }, 'tau2bench-retail': { n: 1095, awm: 0.529, fsm: 0.651 }, 'tau2bench-airline': { n: 480, awm: 0.565, fsm: 0.573 }, atbench: { n: 600, awm: 0.478, fsm: 0.625 }, osworld: { n: 1286, awm: 0.550, fsm: 0.707 }, } const monitorRoot: Record = tryLoadJSON('experiments/poc-online-monitor/online-monitor-results.json') || {} const cfRoot: Record = tryLoadJSON('experiments/counterfactual/counterfactual-results.json') || {} // Incremental-convergence runs (6 datasets). The per-dataset evolution-vN folders are // LFS stubs for everything except atbench/osworld, so the dashboard convergence tab only // saw 2 of 6 — the same 'taubench-*' vs 'tau2bench-*' keying issue. Build the curve from // this file (fitnessTrajectory + final state/edge counts) when evolution snapshots are absent. const convRateRoot: Record }> = tryLoadJSON('experiments/convergence-rate/convergence-results.json') || {} function convRateCurve(id: string): { curve: DatasetBundle['convergence']['curve']; fitnessAt: number } | null { const c = convRateRoot[id] || convRateRoot[monitorKey(id)] if (!c || !c.fitnessTrajectory?.length) return null const curve = c.fitnessTrajectory.map(p => ({ n: Math.round(p.pctData * c.trainTraces), states: c.finalStates, // per-point counts unavailable; state count converges near-instantly transitions: c.finalEdges, fitness: p.fitness, })) const hit = c.fitnessTrajectory.find(p => p.fitness >= 0.95) return { curve, fitnessAt: hit ? Math.round(hit.pctData * c.trainTraces) : 0 } } // monitor/counterfactual JSONs key tau2-bench suites as 'taubench-*' (legacy // single-tau); the dataset registry uses 'tau2bench-*'. Map before lookup so all // four evaluated datasets (swesmith, sweagent, retail, airline) resolve, not just two. function monitorKey(id: string): string { return id.replace(/^tau2bench-/, 'taubench-') } function loadMonitor(id: string): DatasetBundle['downstream']['monitor'] { return monitorRoot[id] || monitorRoot[monitorKey(id)] || null } function loadCounterfactual(id: string): DatasetBundle['downstream']['counterfactual'] { return cfRoot[id] || cfRoot[monitorKey(id)] || null } function loadCrossModel(id: string): DatasetBundle['downstream']['crossModel'] { const d = tryLoadJSON(`experiments/${id}-cross-model/transfer-matrix.json`) if (!d || typeof d !== 'object') return null // Matrices in the experiment JSON are stored as {labels, values}; extract the 2-D values // (older files may already be a raw number[][]). type Mat = number[][] | { values?: number[][] } const matVals = (m: Mat | undefined): number[][] => Array.isArray(m) ? m : (m?.values || []) const obj = d as { models?: Array<{ name: string; shortName: string; totalTraces: number; successRate: number }> matrices?: { fitness?: Mat; auroc?: Mat; stateCount?: Mat } summary?: { diagonalAUROC?: { mean: number; std: number } offDiagonalAUROC?: { mean: number; std: number } aurocTransferGap?: number } } if (!obj.models || !obj.matrices || !obj.summary) return null return { models: obj.models.map(m => ({ name: m.name, shortName: m.shortName, traces: m.totalTraces, successRate: m.successRate, })), matrices: { fitness: matVals(obj.matrices.fitness), auroc: matVals(obj.matrices.auroc), stateCount: matVals(obj.matrices.stateCount), }, summary: { diagonalAUROC: obj.summary.diagonalAUROC || { mean: 0, std: 0 }, offDiagonalAUROC: obj.summary.offDiagonalAUROC || { mean: 0, std: 0 }, aurocTransferGap: obj.summary.aurocTransferGap || 0, }, } } function loadWorkflowMemory(id: string): DatasetBundle['downstream']['workflowMemory'] { const paper = PAPER_TABLE_4[id] if (paper) { // Use paper-authoritative numbers; pull noMemory from the older JSON if available. const json = tryLoadJSON(`experiments/poc-fsm-memory/${id}-results.json`) const noMem = (json && typeof json === 'object' ? ((json as { llm?: { noMemory?: { accuracy: number } } }).llm?.noMemory?.accuracy ?? null) : null) return { n: paper.n, noMemory: { accuracy: noMem ?? 0, correct: noMem != null ? Math.round(noMem * paper.n) : 0 }, awm: { accuracy: paper.awm, correct: Math.round(paper.awm * paper.n) }, fsm: { accuracy: paper.fsm, correct: Math.round(paper.fsm * paper.n) }, fsmGain: paper.fsm - paper.awm, } } return null } function buildDataset(id: string): DatasetBundle { // tau2-bench: the combined 4-model run lives in the UNVERSIONED folder; the versioned // (-v11) folders are stale single-model runs (telecom 5 states vs combined 43). Prefer // the combined run for tau2 so baselines/graph match the paper. const combinedFirst = id.startsWith('tau2bench-') // Load baselines: (tau2: unversioned combined first) → v12 → v11 → v8 → v7 → v6 → unversioned const baseline = (combinedFirst ? tryLoadJSON(`experiments/${id}-baselines/baseline-comparison.json`) : null) || tryLoadJSON(`experiments/${id}-baselines-v12/baseline-comparison.json`) || tryLoadJSON(`experiments/${id}-baselines-v11/baseline-comparison.json`) || tryLoadJSON(`experiments/${id}-baselines-v8/baseline-comparison.json`) || tryLoadJSON(`experiments/${id}-baselines-v7/baseline-comparison.json`) || tryLoadJSON(`experiments/${id}-baselines-v6/baseline-comparison.json`) || loadJSON(`experiments/${id}-baselines/baseline-comparison.json`) // Load evolution: v9 → v8 → v7 → v6 → v5 let evolution: Record const ev = tryLoadJSON(`experiments/${id}-evolution-v9/evolution-results.json`) || tryLoadJSON(`experiments/${id}-evolution-v8/evolution-results.json`) || tryLoadJSON(`experiments/${id}-evolution-v7/evolution-results.json`) || tryLoadJSON(`experiments/${id}-evolution-v6/evolution-results.json`) || tryLoadJSON(`experiments/${id}-evolution-v5/evolution-results.json`) evolution = ev || {} if (ev) console.log(` Loaded evolution for ${id}`) else console.log(` No evolution data for ${id}`) // Load downstream: v15 → v14 → v13 → v12 → v11 → v10-fixed → v9 → v8 → v7 → v2 → v6 → v5 const downstream: Record = tryLoadJSON(`experiments/${id}-downstream-v15/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v14/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v13/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v12/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v11/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v10-fixed/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v9/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v8/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v7/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v2/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v6/downstream-eval.json`) || tryLoadJSON(`experiments/${id}-downstream-v5/downstream-eval.json`) || {} if (Object.keys(downstream).length > 0) console.log(` Loaded downstream for ${id}`) else console.log(` No downstream data for ${id}`) // Load graph: (tau2: unversioned combined first) → v12 → v11 → v8 → v7 → v6 → unversioned const graph = (combinedFirst ? tryLoadJSON(`experiments/${id}-baselines/train-graph.json`) : null) || tryLoadJSON(`experiments/${id}-baselines-v12/train-graph.json`) || tryLoadJSON(`experiments/${id}-baselines-v11/train-graph.json`) || tryLoadJSON(`experiments/${id}-baselines-v8/train-graph.json`) || tryLoadJSON(`experiments/${id}-baselines-v7/train-graph.json`) || tryLoadJSON(`experiments/${id}-baselines-v6/train-graph.json`) || loadJSON(`experiments/${id}-baselines/train-graph.json`) // Load raw traces for FSM replay type RawTrace = { id: string; messages: Array<{ role: string; content: string; name?: string }>; annotations?: { success?: boolean } } let rawTraces: RawTrace[] if (id === 'taubench-airline') { rawTraces = loadTauBenchRaw('airline') } else if (id === 'taubench-retail') { rawTraces = loadTauBenchRaw('retail') } else if (id.startsWith('tau2bench-')) { const domain = id.replace('tau2bench-', '') rawTraces = loadTau2BenchRaw(domain) } else { rawTraces = loadJSON(`data/${id}/traces.json`) } const traceMap = new Map(rawTraces.map(t => [t.id, t])) // For tau2bench: also register traces under the old ID format (without modelTag) // so baselines generated with the old loader still match. // Old format: tau2bench-{domain}-{task_id}-{idx} // New format: tau2bench-{domain}-{modelTag}-{task_id}-{idx} if (id.startsWith('tau2bench-')) { const domain = id.replace('tau2bench-', '') const files = TAU2_FILES[domain] || [] for (const f of files) { try { const data: { simulations: Array<{ task_id: string }> } = loadJSON(f) for (let idx = 0; idx < data.simulations.length; idx++) { const sim = data.simulations[idx] const modelTag = f.replace(/.*\//, '').replace(/\.json$/, '').slice(0, 30) const newId = `tau2bench-${domain}-${modelTag}-${sim.task_id}-${idx}` const oldId = `tau2bench-${domain}-${sim.task_id}-${idx}` const trace = traceMap.get(newId) if (trace && !traceMap.has(oldId)) { traceMap.set(oldId, trace) } } } catch { /* skip */ } } } console.log(` Loaded ${rawTraces.length} raw traces for replay (traceMap: ${traceMap.size} keys)`) // Build transition function from graph const transitionFn = graphToTransitionFn(graph) // Extract baselines const baselines: DatasetBundle['baselines'] = [] // Our FSM const sg = baseline.stateGraph baselines.push({ method: 'stateGraph', label: 'Our FSM', trainFitness: sg.trainFitness, testFitness: sg.testFitness, states: sg.stateCount, transitions: sg.transitionCount, randomAcceptRate: sg.precision?.randomAcceptanceRate ?? null, permutedAcceptRate: sg.precision?.permutedAcceptanceRate ?? null, pmPrecision: null, category: 'ours', note: sg.note, }) // RPNI const rpni = baseline.rpni baselines.push({ method: 'rpni', label: 'RPNI', trainFitness: rpni.trainFitness, testFitness: rpni.testFitness, states: rpni.stateCount, transitions: rpni.transitionCount, randomAcceptRate: rpni.precision?.randomAcceptanceRate ?? null, permutedAcceptRate: rpni.precision?.permutedAcceptanceRate ?? null, pmPrecision: null, category: 'automata', note: rpni.note, }) // PM4Py miners const pm4py = baseline.pm4py?.miners || {} for (const [name, m] of Object.entries(pm4py) as [string, Record][]) { if (!m.success) continue const label = name.charAt(0).toUpperCase() + name.slice(1) + ' Miner' baselines.push({ method: name, label, trainFitness: m.train_fitness as number, testFitness: m.test_fitness as number, states: null, transitions: (m.transitions as number) || null, randomAcceptRate: null, permutedAcceptRate: null, pmPrecision: (m.test_precision as number) ?? null, category: 'process-mining', note: `${m.places || '?'}p, ${m.transitions || '?'}t`, }) } // AWM if (baseline.awm) { const awm = baseline.awm baselines.push({ method: 'awm', label: 'AWM', trainFitness: awm.trainCoverage || 0, testFitness: awm.testAvgMatchScore || 0, states: null, transitions: null, randomAcceptRate: null, permutedAcceptRate: null, pmPrecision: null, category: 'workflow', note: `${awm.count} workflows (success-only)`, }) } if (baseline.awm_all) { const awm = baseline.awm_all baselines.push({ method: 'awm_all', label: 'AWM-all', trainFitness: awm.trainCoverage || 0, testFitness: awm.testAvgMatchScore || 0, states: null, transitions: null, randomAcceptRate: null, permutedAcceptRate: null, pmPrecision: null, category: 'workflow', note: `${awm.count} workflows (all traces)`, }) } // EDSM if (baseline.edsm && (baseline.edsm as Record).success) { const edsm = baseline.edsm as Record baselines.push({ method: 'edsm', label: 'EDSM', trainFitness: (edsm.train_fitness as number) || 0, testFitness: (edsm.test_fitness as number) || 0, states: (edsm.state_count as number) || null, transitions: (edsm.transition_count as number) || null, randomAcceptRate: null, permutedAcceptRate: null, pmPrecision: null, category: 'automata', note: `EDSM (AALpy): ${edsm.state_count}S, ${edsm.alphabet_size || '?'} alphabet`, }) } // Evolution curve const snapshots = evolution.snapshots || evolution.curve || [] let curve = snapshots.map((p: Record) => ({ n: p.traceCount, states: p.states || p.stateCount, transitions: p.transitions || p.transitionCount, fitness: p.testFitness || p.fitness, })) const conv = evolution.convergence || {} // Fall back to the incremental-convergence-rate run when the evolution file is a stub. if (curve.length === 0) { const cr = convRateCurve(id) if (cr) { curve = cr.curve conv.fitnessConvergenceAt = conv.fitnessConvergenceAt || cr.fitnessAt console.log(` Convergence curve from convergence-rate run for ${id}`) } } // Per-trace data with FSM traversal const trainIds: string[] = baseline.config?.trainIds || [] const testIds: string[] = baseline.config?.testIds || [] const perTrace: Record = {} for (const t of ((downstream as any).failurePrediction?.perTrace || [])) { perTrace[t.id] = { success: t.success, fullFitness: t.fullFitness, successFitness: t.successFitness } } const traces: TraceDetail[] = [] let replayCount = 0 function buildTraceDetail(tid: string, split: 'train' | 'test'): TraceDetail { const pt = perTrace[tid] const raw = traceMap.get(tid) if (raw) { const replay = replayTrace(transitionFn, raw.messages) replayCount++ return { id: tid, split, success: pt?.success ?? raw.annotations?.success ?? null, fullFitness: pt?.fullFitness ?? null, successFitness: pt?.successFitness ?? null, steps: replay.steps, stateSequence: replay.stateSequence, fitness: replay.fitness, consumed: replay.consumed, total: replay.total, firstFailIdx: replay.firstFailIdx, stateVisits: replay.stateVisits, } } // Fallback: no raw trace data return { id: tid, split, success: pt?.success ?? null, fullFitness: pt?.fullFitness ?? null, successFitness: pt?.successFitness ?? null, steps: [], stateSequence: [], fitness: 0, consumed: 0, total: 0, firstFailIdx: -1, stateVisits: {}, } } for (const tid of trainIds) traces.push(buildTraceDetail(tid, 'train')) for (const tid of testIds) traces.push(buildTraceDetail(tid, 'test')) console.log(` Replayed ${replayCount}/${traces.length} traces through FSM`) // Load multi-seed data let multiseed: DatasetBundle['multiseed'] = null try { const ms = loadJSON(`experiments/${id}-multiseed/multiseed-results.json`) multiseed = { numSeeds: ms.config?.numSeeds || 5, ourFSM: ms.aggregated?.ourFSM || { testFitness: { mean: 0, std: 0 }, states: { mean: 0, std: 0 } }, rpni: ms.aggregated?.rpni || { testFitness: { mean: 0, std: 0 }, states: { mean: 0, std: 0 } }, awm: ms.aggregated?.awm || { testFitness: { mean: 0, std: 0 } }, } console.log(` Loaded multi-seed (${multiseed.numSeeds} seeds)`) } catch { console.log(` No multi-seed data for ${id}`) } // Build full downstream const ds = downstream as Record const fp = ds.fitnessProfile || null const bp = ds.behavioralProfiling || null const sa = ds.structuralAnalysis || null const df = ds.differentialFSM?.skipped ? null : (ds.differentialFSM || null) // Failure prediction is a labeled task; the 3 unlabeled datasets have no trustworthy // success labels (gui-odyssey yields a degenerate AUROC 1.000), so null them out to // match the paper's 9-labeled-dataset failure-prediction set. const UNLABELED = new Set(['gui-odyssey', 'mind2web', 'whoandwhen']) const pred = (UNLABELED.has(id) || ds.failurePrediction?.skipped) ? null : (ds.failurePrediction || null) const ml = ds.mistakeLocalization || null return { id, meta: META[id], baselines, traces, convergence: { stateConvergenceAt: conv.stateConvergenceAt || 0, transitionConvergenceAt: conv.transitionConvergenceAt || 0, fitnessConvergenceAt: conv.fitnessConvergenceAt || 0, curve, }, downstream: { fitnessProfile: fp ? { mean: fp.mean, std: fp.std, min: fp.min, max: fp.max, anomalyCount: fp.anomalyCount, anomalies: fp.anomalies?.map((a: any) => a.id || a) || [], histogram: fp.histogram || [], } : null, behavioralProfiling: bp ? { stateVisitDistribution: bp.stateVisitDistribution || [], transitionFrequency: bp.transitionFrequency || [], deadTransitions: bp.deadTransitions || [], transitionCoverage: bp.transitionCoverage ?? 1, avgUniqueStatesPerTrace: bp.avgUniqueStatesPerTrace ?? 0, } : null, structuralAnalysis: sa ? { density: sa.density, hubs: sa.hubs || [], sinks: sa.sinks || [], sources: sa.sources || [], selfLoops: sa.selfLoops || [], nonTrivialSCCs: sa.nonTrivialSCCs || [], } : null, differentialFSM: df ? { successOnlyTransitions: df.successOnlyTransitions || [], failureOnlyTransitions: df.failureOnlyTransitions || [], sharedTransitions: typeof df.sharedTransitions === 'number' ? [] : (df.sharedTransitions || []), } : null, failurePrediction: pred, mistakeLocalization: ml, neuralProbe: neuralProbe[id] || null, workflowMemory: loadWorkflowMemory(id), crossModel: loadCrossModel(id), monitor: loadMonitor(id), counterfactual: loadCounterfactual(id), }, multiseed, graph: { states: (graph.states || []).map((s: Record) => ({ id: s.id })), transitions: (graph.transitions || []).map((t: Record) => ({ source: t.source, target: t.target, subject: t.subject || '', })), }, } } // Cap heaviest datasets to keep each shard under 22 MB // (Cloudflare Pages / Netlify hard limit is 25 MB per static file). // Traces are sampled deterministically from the full set, preserving train/test split balance. const SHARD_TRACE_CAP: Record = { agentnet: 3500, } function capTraces(d: DatasetBundle): DatasetBundle { const cap = SHARD_TRACE_CAP[d.id] if (!cap || d.traces.length <= cap) return d const train = d.traces.filter(t => t.split === 'train') const test = d.traces.filter(t => t.split === 'test') const ratio = train.length / (train.length + test.length) const trainKeep = Math.round(cap * ratio) const testKeep = cap - trainKeep // Deterministic: keep the first N of each split (already shuffled by trainTestSplit seed). const truncated = [...train.slice(0, trainKeep), ...test.slice(0, testKeep)] return { ...d, traces: truncated } } function main() { const data = DATASETS.map(buildDataset).map(capTraces) const pub = path.join(__dirname, '../public') const shardDir = path.join(pub, 'datasets') mkdirSync(pub, { recursive: true }) mkdirSync(shardDir, { recursive: true }) // Per-dataset shards (lets deploy hosts with ≤25MB-per-file limits accept the build). for (const d of data) { writeFileSync(path.join(shardDir, `${d.id}.json`), JSON.stringify(d)) } // Slim index for the landing/list views (everything except per-trace replay details). const index = data.map(d => ({ id: d.id, meta: d.meta, baselines: d.baselines, convergence: d.convergence, multiseed: d.multiseed, graph: d.graph, downstream: { ...d.downstream, // Drop the largest passthrough blobs from the index to keep it < ~2MB. failurePrediction: d.downstream.failurePrediction ? { ...d.downstream.failurePrediction, perTrace: undefined } : null, mistakeLocalization: null, }, traceCount: d.traces.length, })) writeFileSync(path.join(pub, 'index.json'), JSON.stringify(index)) // Legacy monolithic file (kept for back-compat with older App.tsx). writeFileSync(path.join(pub, 'data.json'), JSON.stringify(data)) console.log(`\nBuilt data.json + index.json + ${data.length} per-dataset shards`) for (const d of data) { const withSteps = d.traces.filter(t => t.steps.length > 0).length console.log(` ${d.meta.name}: ${d.baselines.length} baselines, ${withSteps}/${d.traces.length} traces with FSM replay`) } } main()