import type { ApiResponse, ErrorCode, DemoRunResult, DemoSummary, EvalLaneSummary, EvalRunRequest, EvalRunResult, ArtifactRef, ArtifactDetail, TurnReplayComparison, ProposalDetail, ProposalState, ProposalSummary, AuditEvent, RunSummary, RunDetail, PackSummary, PackDetail, VaultSummary, VaultEntry, CalibrationClass, ServingMetrics, TurnJournalEntry, TurnJournalSummary, MathProposalSummary, MathProposalDetail, MathRatifyResult, } from "../types/api"; export class WorkbenchApiError extends Error { constructor( public readonly code: ErrorCode, message: string, ) { super(message); this.name = "WorkbenchApiError"; } } export const API_URL: string = import.meta.env.VITE_WORKBENCH_API_URL ?? "http://127.0.0.1:8765"; export async function apiFetch( path: string, init?: RequestInit & { timeoutMs?: number }, ): Promise { const { timeoutMs = 5000, ...requestInit } = init ?? {}; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(`${API_URL}${path}`, { ...requestInit, signal: controller.signal }); const json: ApiResponse = await res.json(); if (!json.ok) { throw new WorkbenchApiError(json.error.code, json.error.message); } return json.data; } finally { clearTimeout(timeout); } } export async function fetchEvalLanes(): Promise { const envelope = await apiFetch<{ lanes: EvalLaneSummary[] }>("/evals"); return envelope.lanes; } export async function runEvalLane(req: EvalRunRequest): Promise { if (req.split === "holdout") { const hasConfig = typeof window !== "undefined" && (window as any).sealedEvalConfig === true; if (!hasConfig) { throw new WorkbenchApiError( "client_refused_sealed_holdout", "Holdout runs require sealed-eval config — use CLI" ); } } const lanes = await fetchEvalLanes(); const lane = lanes.find((l) => l.lane === req.lane); if (!lane) { throw new WorkbenchApiError("not_found", `Eval lane not found: ${req.lane}`); } if (!lane.read_only) { throw new WorkbenchApiError("client_refused_unsafe_lane", "API run disabled — use CLI"); } return apiFetch("/evals/run", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), timeoutMs: 120000, }); } export async function fetchArtifacts(): Promise { const data = await apiFetch<{ items: ArtifactRef[] }>("/artifacts"); return data.items; } export async function fetchArtifactDetail(artifactId: string): Promise { return apiFetch(`/artifacts/${artifactId}`); } export async function fetchTurnReplay(turnId: number): Promise { return apiFetch(`/replay/${encodeURIComponent(String(turnId))}`); } export async function fetchDemos(): Promise { const envelope = await apiFetch>("/demos"); return envelope.items; } export async function runDemo(demoId: string): Promise { return apiFetch(`/demos/${encodeURIComponent(demoId)}/run`, { method: "POST", }); } export type ProposalStateFilter = ProposalState | "all"; interface ItemsEnvelope { items: T[]; } export async function fetchProposals( filter: ProposalStateFilter = "all", ): Promise { const envelope = await apiFetch>("/proposals"); if (filter === "all") { return envelope.items; } return envelope.items.filter((proposal) => proposal.state === filter); } export async function fetchProposalDetail(proposalId: string): Promise { return apiFetch(`/proposals/${encodeURIComponent(proposalId)}`); } export async function fetchTraceTurns( limit?: number, offset?: number, ): Promise { const params = new URLSearchParams(); if (limit !== undefined) params.set("limit", String(limit)); if (offset !== undefined) params.set("offset", String(offset)); const query = params.toString(); const envelope = await apiFetch>( query ? `/trace/turns?${query}` : "/trace/turns", ); return envelope.items; } export async function fetchTraceTurn(turnId: number): Promise { return apiFetch(`/trace/${encodeURIComponent(String(turnId))}`); } export async function fetchAuditEvents( limit?: number, offset?: number, ): Promise> { const params = new URLSearchParams(); if (limit !== undefined) params.set("limit", String(limit)); if (offset !== undefined) params.set("offset", String(offset)); const query = params.toString(); return apiFetch>(query ? `/audit/events?${query}` : "/audit/events"); } export async function fetchRuns(limit?: number, offset?: number): Promise { const params = new URLSearchParams(); if (limit !== undefined) params.set("limit", String(limit)); if (offset !== undefined) params.set("offset", String(offset)); const query = params.toString(); const envelope = await apiFetch>(query ? `/runs?${query}` : "/runs"); return envelope.items; } export async function fetchRun( sessionId: string, turnLimit?: number, turnOffset?: number, ): Promise { const params = new URLSearchParams(); if (turnLimit !== undefined) params.set("limit", String(turnLimit)); if (turnOffset !== undefined) params.set("offset", String(turnOffset)); const query = params.toString(); const path = `/runs/${encodeURIComponent(sessionId)}`; return apiFetch(query ? `${path}?${query}` : path); } export async function fetchMathProposals(): Promise { const envelope = await apiFetch>("/math-proposals"); return envelope.items; } export async function fetchMathProposalDetail(proposalId: string): Promise { return apiFetch(`/math-proposals/${encodeURIComponent(proposalId)}`); } export async function ratifyMathProposal( proposalId: string, category?: string, polarity?: string, dryRun?: boolean, ): Promise { return apiFetch(`/math-proposals/${encodeURIComponent(proposalId)}/ratify`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ category, polarity, dry_run: dryRun }), }); } export async function rejectMathProposal( proposalId: string, note?: string, ): Promise<{ proposal_id: string; rejected: boolean }> { return apiFetch<{ proposal_id: string; rejected: boolean }>( `/math-proposals/${encodeURIComponent(proposalId)}/reject`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ note }), }, ); } export async function deferMathProposal( proposalId: string, ): Promise<{ proposal_id: string; deferred: boolean }> { return apiFetch<{ proposal_id: string; deferred: boolean }>( `/math-proposals/${encodeURIComponent(proposalId)}/defer`, { method: "POST", }, ); } export async function fetchPacks(limit?: number, offset?: number): Promise { const params = new URLSearchParams(); if (limit !== undefined) params.set("limit", String(limit)); if (offset !== undefined) params.set("offset", String(offset)); const query = params.toString(); const envelope = await apiFetch>(query ? `/packs?${query}` : "/packs"); return envelope.items; } export async function fetchPack(packId: string): Promise { return apiFetch(`/packs/${encodeURIComponent(packId)}`); } export async function fetchVaultSummary(): Promise { return apiFetch("/vault/summary"); } export async function fetchVaultEntries(limit?: number, offset?: number): Promise { const params = new URLSearchParams(); if (limit !== undefined) params.set("limit", String(limit)); if (offset !== undefined) params.set("offset", String(offset)); const query = params.toString(); const envelope = await apiFetch>( query ? `/vault/entries?${query}` : "/vault/entries", ); return envelope.items; } export async function fetchCalibrationClasses(): Promise { const envelope = await apiFetch>("/calibration/classes"); return envelope.items; } export async function fetchServingMetrics(): Promise { const envelope = await apiFetch>("/serving/metrics"); return envelope.items; }