import fs from "fs"; import path from "path"; import type { OpenHandsBenchReport, TraceBenchRow } from "./openhands-run.ts"; import { ROI_PATTERN_TYPES } from "./openhands-run.ts"; export type MetricsCsvRow = { instance_id: string; arm: string; resolved: string; total_tokens: string; prompt_tokens: string; completion_tokens: string; usd: string; tcg_savings_pct: string; repeat_read_rate: string; explore_fix_ratio: string; pattern_hit_count: string; pattern_types: string; optimize_latency_ms: string; log_count: string; }; export type ParetoPoint = { instance_id: string; arm: string; savings_rate: number; resolved: number; usd_c0: number | null; usd_arm: number | null; }; export type P1AggregateSummary = { generated_at: string; instance_count: number; arms: string[]; resolve_rate_by_arm: Record; resolve_drop_pp: number | null; avg_savings_pct_c1: number; avg_repeat_read_rate_c1: number; two_plus_pattern_rate_c1: number; pareto_points: ParetoPoint[]; }; const CSV_HEADER: (keyof MetricsCsvRow)[] = [ "instance_id", "arm", "resolved", "total_tokens", "prompt_tokens", "completion_tokens", "usd", "tcg_savings_pct", "repeat_read_rate", "explore_fix_ratio", "pattern_hit_count", "pattern_types", "optimize_latency_ms", "log_count", ]; function csvEscape(value: string): string { if (value.includes(",") || value.includes('"') || value.includes("\n")) { return `"${value.replace(/"/g, '""')}"`; } return value; } export function traceToCsvRow(row: TraceBenchRow): MetricsCsvRow { const swe = row.swe; const resolved = swe?.resolved != null ? String(swe.resolved) : ""; return { instance_id: row.instance_id, arm: row.arm, resolved, total_tokens: swe?.total_tokens != null ? String(swe.total_tokens) : String(row.tokens_from_logs), prompt_tokens: swe?.prompt_tokens != null ? String(swe.prompt_tokens) : "", completion_tokens: swe?.completion_tokens != null ? String(swe.completion_tokens) : "", usd: swe?.usd != null ? String(swe.usd) : String(row.usd_est_from_logs), tcg_savings_pct: row.savings_percentage != null ? String(row.savings_percentage) : "", repeat_read_rate: String(row.metrics.repeat_read_rate), explore_fix_ratio: String(row.metrics.explore_fix_ratio), pattern_hit_count: String(row.pattern_hit_count), pattern_types: row.pattern_types.filter((p) => ROI_PATTERN_TYPES.has(p)).join("|"), optimize_latency_ms: String(row.optimize_latency_ms), log_count: String(row.log_count), }; } export function resolveRate(rows: TraceBenchRow[]): number { const withOutcome = rows.filter((r) => r.swe?.resolved != null); if (withOutcome.length === 0) return 0; return withOutcome.filter((r) => r.swe?.resolved).length / withOutcome.length; } export function loadBenchReports(reportsDir: string): OpenHandsBenchReport[] { if (!fs.existsSync(reportsDir)) return []; return fs .readdirSync(reportsDir) .filter((f) => f.startsWith("bench-") && f.endsWith(".json")) .map((f) => JSON.parse(fs.readFileSync(path.join(reportsDir, f), "utf-8")) as OpenHandsBenchReport) .sort((a, b) => a.arm.localeCompare(b.arm)); } export function aggregateP1(reports: OpenHandsBenchReport[]): { csvRows: MetricsCsvRow[]; summary: P1AggregateSummary; } { const csvRows: MetricsCsvRow[] = []; const byInstanceArm = new Map(); for (const report of reports) { for (const trace of report.traces) { csvRows.push(traceToCsvRow(trace)); byInstanceArm.set(`${trace.instance_id}::${trace.arm}`, trace); } } const instanceIds = [...new Set(reports.flatMap((r) => r.traces.map((t) => t.instance_id)))].sort(); const arms = [...new Set(reports.map((r) => r.arm))].sort(); const resolve_rate_by_arm: Record = {}; for (const arm of arms) { const armRows = reports.filter((r) => r.arm === arm).flatMap((r) => r.traces); resolve_rate_by_arm[arm] = Number(resolveRate(armRows).toFixed(4)); } const c0Rate = resolve_rate_by_arm.C0 ?? null; const c1Rate = resolve_rate_by_arm.C1 ?? null; const resolve_drop_pp = c0Rate != null && c1Rate != null ? Number(((c0Rate - c1Rate) * 100).toFixed(2)) : null; const c1Traces = reports.filter((r) => r.arm === "C1").flatMap((r) => r.traces); const savingsVals = c1Traces.map((t) => t.savings_percentage).filter((v): v is number => v != null); const avg_savings_pct_c1 = savingsVals.length ? Number((savingsVals.reduce((a, b) => a + b, 0) / savingsVals.length).toFixed(4)) : 0; const avg_repeat_read_rate_c1 = c1Traces.length ? Number((c1Traces.reduce((s, t) => s + t.metrics.repeat_read_rate, 0) / c1Traces.length).toFixed(4)) : 0; const twoPlus = c1Traces.filter( (t) => new Set(t.pattern_types.filter((p) => ROI_PATTERN_TYPES.has(p))).size >= 2 ).length; const two_plus_pattern_rate_c1 = c1Traces.length ? Number((twoPlus / c1Traces.length).toFixed(4)) : 0; const pareto_points: ParetoPoint[] = []; for (const instance_id of instanceIds) { const c0 = byInstanceArm.get(`${instance_id}::C0`); const c1 = byInstanceArm.get(`${instance_id}::C1`); const target = c1 ?? byInstanceArm.get(`${instance_id}::C2`); if (!target) continue; const usdC0 = c0?.swe?.usd ?? c0?.usd_est_from_logs ?? null; const usdArm = target.swe?.usd ?? target.usd_est_from_logs ?? null; let savings_rate = target.savings_percentage ?? 0; if (usdC0 != null && usdArm != null && usdC0 > 0) { savings_rate = (usdC0 - usdArm) / usdC0; } pareto_points.push({ instance_id, arm: target.arm, savings_rate: Number(savings_rate.toFixed(4)), resolved: target.swe?.resolved ? 1 : 0, usd_c0: usdC0, usd_arm: usdArm, }); } return { csvRows, summary: { generated_at: new Date().toISOString(), instance_count: instanceIds.length, arms, resolve_rate_by_arm, resolve_drop_pp, avg_savings_pct_c1, avg_repeat_read_rate_c1, two_plus_pattern_rate_c1, pareto_points, }, }; } export function writeMetricsCsv(rows: MetricsCsvRow[], outPath: string): void { const lines = [ CSV_HEADER.join(","), ...rows.map((row) => CSV_HEADER.map((k) => csvEscape(row[k])).join(",")), ]; fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, `${lines.join("\n")}\n`, "utf-8"); } export function writeParetoSvg(points: ParetoPoint[], outPath: string): void { const width = 640; const height = 400; const pad = 48; const plotW = width - pad * 2; const plotH = height - pad * 2; const dots = points .map((p, i) => { const x = pad + Math.max(0, Math.min(1, p.savings_rate)) * plotW; const y = pad + (1 - p.resolved) * plotH * 0.85 + (i % 5) * 3; const color = p.resolved ? "#16a34a" : "#dc2626"; return `${p.instance_id} saved=${(p.savings_rate * 100).toFixed(1)}%`; }) .join("\n"); const svg = ` Pareto: savings rate vs resolve (C1/C2 vs C0 USD) Estimated / realized savings rate → Resolve (jittered) ${dots} `; fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, svg, "utf-8"); }