Download scripts/build-data.ts from seonglae/AgentStateGraph-Browser: direct link, hf CLI and curl.
- Browser
- Download file 38.5 kB
-
https://huggingface.co/spaces/seonglae/AgentStateGraph-Browser/resolve/main/scripts/build-data.ts
- Command line
-
hf download hf://spaces/seonglae/AgentStateGraph-Browser/scripts/build-data.ts
-
curl -L -o build-data.ts https://huggingface.co/spaces/seonglae/AgentStateGraph-Browser/resolve/main/scripts/build-data.ts
38.5 kB
| /** | |
| * 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<string, number> | |
| } | |
| 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<string, unknown> | null | |
| mistakeLocalization: Record<string, unknown> | 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<string, { auroc: number; curve?: Array<{ point: number; f1: number }> }> | |
| } | 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<string, DatasetBundle['meta']> = { | |
| 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<string>() | |
| 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<string, number> } { | |
| let current = 'init' | |
| const steps: TraceStep[] = [] | |
| const stateSequence: string[] = ['init'] | |
| const stateVisits: Record<string, number> = { 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<string, unknown> | 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<string, string[]> = { | |
| 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<string, DatasetBundle['downstream']['neuralProbe']> = | |
| 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<string, { n: number; awm: number; fsm: number }> = { | |
| 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<string, DatasetBundle['downstream']['monitor']> = | |
| tryLoadJSON('experiments/poc-online-monitor/online-monitor-results.json') || {} | |
| const cfRoot: Record<string, DatasetBundle['downstream']['counterfactual']> = | |
| 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<string, { | |
| finalStates: number; finalEdges: number; trainTraces: number | |
| fitnessTrajectory: Array<{ pctData: number; fitness: number }> | |
| }> = 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<string, unknown> | |
| 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<string, unknown> = 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<string, unknown>][]) { | |
| 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<string, unknown>).success) { | |
| const edsm = baseline.edsm as Record<string, unknown> | |
| 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<string, number>) => ({ | |
| 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<string, { success: boolean; fullFitness: number; successFitness: number }> = {} | |
| 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<string, any> | |
| 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<string, string>) => ({ id: s.id })), | |
| transitions: (graph.transitions || []).map((t: Record<string, string>) => ({ | |
| 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<string, number> = { | |
| 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() | |