diff --git a/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/context-event/route.ts b/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/context-event/route.ts index 5770b995e..76fc75ca7 100644 --- a/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/context-event/route.ts +++ b/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/context-event/route.ts @@ -5,10 +5,10 @@ import { ContextEventState } from "@/lib/types"; export async function POST(request: Request) { const payload = (await request.json()) as { - runId: string; + sampleId: string; taskId: string; event: ContextEventState; }; - emitHarnessContextEvent(payload.runId, payload.taskId, payload.event); + emitHarnessContextEvent(payload.sampleId, payload.taskId, payload.event); return NextResponse.json({ ok: true }); } diff --git a/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/run-complete/route.ts b/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/run-complete/route.ts index 5efe85e30..fb108d60a 100644 --- a/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/run-complete/route.ts +++ b/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/run-complete/route.ts @@ -4,7 +4,7 @@ import { emitHarnessRunCompleted } from "@/lib/testing/dashboardHarness"; export async function POST(request: Request) { const payload = (await request.json()) as { - runId: string; + sampleId: string; status: "completed" | "failed"; durationSeconds: number; finalScore: number | null; diff --git a/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/task-evaluation/route.ts b/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/task-evaluation/route.ts index d03482843..02cb7d759 100644 --- a/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/task-evaluation/route.ts +++ b/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/task-evaluation/route.ts @@ -5,10 +5,10 @@ import { TaskEvaluationState } from "@/lib/types"; export async function POST(request: Request) { const payload = (await request.json()) as { - runId: string; + sampleId: string; taskId: string | null; evaluation: TaskEvaluationState; }; - emitHarnessTaskEvaluation(payload.runId, payload.taskId, payload.evaluation); + emitHarnessTaskEvaluation(payload.sampleId, payload.taskId, payload.evaluation); return NextResponse.json({ ok: true }); } diff --git a/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/thread-message/route.ts b/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/thread-message/route.ts index daaa1ca44..6f3ab28e8 100644 --- a/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/thread-message/route.ts +++ b/ergon-dashboard/src/app/api/danger/test-harness/dashboard/events/thread-message/route.ts @@ -5,9 +5,9 @@ import { CommunicationThreadState } from "@/lib/types"; export async function POST(request: Request) { const payload = (await request.json()) as { - runId: string; + sampleId: string; thread: CommunicationThreadState; }; - emitHarnessThreadMessage(payload.runId, payload.thread); + emitHarnessThreadMessage(payload.sampleId, payload.thread); return NextResponse.json({ ok: true }); } diff --git a/ergon-dashboard/src/app/api/runs/[runId]/mutations/route.ts b/ergon-dashboard/src/app/api/samples/[sampleId]/mutations/route.ts similarity index 79% rename from ergon-dashboard/src/app/api/runs/[runId]/mutations/route.ts rename to ergon-dashboard/src/app/api/samples/[sampleId]/mutations/route.ts index 4b15d2e53..238a8638b 100644 --- a/ergon-dashboard/src/app/api/runs/[runId]/mutations/route.ts +++ b/ergon-dashboard/src/app/api/samples/[sampleId]/mutations/route.ts @@ -6,21 +6,21 @@ import { getHarnessRunMutations } from "@/lib/testing/dashboardHarness"; interface RouteContext { params: Promise<{ - runId: string; + sampleId: string; }>; } export async function GET(_request: Request, context: RouteContext) { - const { runId } = await context.params; + const { sampleId } = await context.params; try { if (config.enableTestHarness) { - const harnessMutations = getHarnessRunMutations(runId); + const harnessMutations = getHarnessRunMutations(sampleId); if (harnessMutations) { return NextResponse.json(harnessMutations); } } - const response = await fetchErgonApi(`/runs/${runId}/mutations`); + const response = await fetchErgonApi(`/samples/${sampleId}/mutations`); const body = await response.json(); if (response.ok) { return NextResponse.json(body, { status: response.status }); @@ -29,7 +29,7 @@ export async function GET(_request: Request, context: RouteContext) { } catch (error) { return NextResponse.json( { - detail: `Ergon API is unavailable while loading mutations for run ${runId}.`, + detail: `Ergon API is unavailable while loading mutations for run ${sampleId}.`, error: error instanceof Error ? error.message : "Unknown backend fetch failure", }, { status: 503 }, diff --git a/ergon-dashboard/src/app/api/runs/[runId]/resources/[resourceId]/content/route.ts b/ergon-dashboard/src/app/api/samples/[sampleId]/resources/[resourceId]/content/route.ts similarity index 85% rename from ergon-dashboard/src/app/api/runs/[runId]/resources/[resourceId]/content/route.ts rename to ergon-dashboard/src/app/api/samples/[sampleId]/resources/[resourceId]/content/route.ts index 9846469f6..f75fde43f 100644 --- a/ergon-dashboard/src/app/api/runs/[runId]/resources/[resourceId]/content/route.ts +++ b/ergon-dashboard/src/app/api/samples/[sampleId]/resources/[resourceId]/content/route.ts @@ -4,22 +4,22 @@ import { fetchErgonApi } from "@/lib/serverApi"; interface RouteContext { params: Promise<{ - runId: string; + sampleId: string; resourceId: string; }>; } /** - * Proxy the binary-or-text body of a RunResource blob from the Ergon API back + * Proxy the binary-or-text body of a SampleResource blob from the Ergon API back * to the dashboard. No parsing — we preserve Content-Type and * Content-Disposition so viewers (markdown, PDF via iframe, image, CSV) can * interpret the stream themselves. */ export async function GET(_request: Request, context: RouteContext) { - const { runId, resourceId } = await context.params; + const { sampleId, resourceId } = await context.params; try { - const upstream = await fetchErgonApi(`/runs/${runId}/resources/${resourceId}/content`, { + const upstream = await fetchErgonApi(`/samples/${sampleId}/resources/${resourceId}/content`, { timeoutMs: 30_000, headers: { Accept: "*/*", diff --git a/ergon-dashboard/src/app/api/runs/[runId]/route.ts b/ergon-dashboard/src/app/api/samples/[sampleId]/route.ts similarity index 65% rename from ergon-dashboard/src/app/api/runs/[runId]/route.ts rename to ergon-dashboard/src/app/api/samples/[sampleId]/route.ts index c8a561d19..725ba4538 100644 --- a/ergon-dashboard/src/app/api/runs/[runId]/route.ts +++ b/ergon-dashboard/src/app/api/samples/[sampleId]/route.ts @@ -1,16 +1,16 @@ import { NextResponse } from "next/server"; -import { loadRunSnapshot } from "@/lib/server-data/runs"; +import { loadRunSnapshot } from "@/lib/server-data/samples"; interface RouteContext { params: Promise<{ - runId: string; + sampleId: string; }>; } export async function GET(_request: Request, context: RouteContext) { - const { runId } = await context.params; - const result = await loadRunSnapshot(runId); + const { sampleId } = await context.params; + const result = await loadRunSnapshot(sampleId); if (result.ok) { return NextResponse.json(result.data, { status: result.status }); diff --git a/ergon-dashboard/src/app/experiments/[definitionId]/page.tsx b/ergon-dashboard/src/app/experiments/[definitionId]/page.tsx index 762ade9b6..2138ed6a9 100644 --- a/ergon-dashboard/src/app/experiments/[definitionId]/page.tsx +++ b/ergon-dashboard/src/app/experiments/[definitionId]/page.tsx @@ -2,8 +2,8 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { StatusBadge } from "@/components/common/StatusBadge"; -import { RunMetricExplorer } from "@/components/experiments/RunMetricExplorer"; -import { formatRunMetricValue, metricDescriptor } from "@/components/experiments/runMetricExplorerModel"; +import { SampleRunMetricExplorer } from "@/components/experiments/SampleRunMetricExplorer"; +import { formatRunMetricValue, metricDescriptor } from "@/components/experiments/sampleRunMetricExplorerModel"; import { formatDurationMs } from "@/lib/formatDuration"; import { loadExperimentDetail, type ExperimentDetailWithRunMetrics } from "@/lib/server-data/experiments"; @@ -27,8 +27,8 @@ function workerTeamLabel(workerTeam: Record) { return entries.map(([key, value]) => `${key}: ${String(value)}`).join(", "); } -function runHref(runId: string) { - return `/run/${runId}`; +function runHref(sampleId: string) { + return `/samples/${sampleId}`; } export default async function ExperimentPage({ params }: ExperimentPageProps) { @@ -143,7 +143,7 @@ export default async function ExperimentPage({ params }: ExperimentPageProps) {
- +
@@ -163,51 +163,51 @@ export default async function ExperimentPage({ params }: ExperimentPageProps) { {detail.runMetricPoints.map((point) => ( {point.runName} - + {point.sampleLabel} - + {point.errorSummary ?
{point.errorSummary}
: null} - + {formatRunMetricValue(scoreDescriptor, point.metrics.score)} - + {formatRunMetricValue(durationDescriptor, point.metrics.duration_ms)} - + {formatRunMetricValue(tasksDescriptor, point.metrics.total_tasks)} - + {formatRunMetricValue(costDescriptor, point.metrics.total_cost_usd)} - + {point.modelTarget ?? "—"} diff --git a/ergon-dashboard/src/app/run/[runId]/page.tsx b/ergon-dashboard/src/app/run/[runId]/page.tsx deleted file mode 100644 index 4de873afa..000000000 --- a/ergon-dashboard/src/app/run/[runId]/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { RunWorkspacePage } from "@/components/run/RunWorkspacePage"; -import { loadRunSnapshot } from "@/lib/server-data/runs"; -import type { SerializedWorkflowRunState } from "@/lib/types"; - -interface LegacyRunPageProps { - params: Promise<{ - runId: string; - }>; -} - -export default async function RunPage({ params }: LegacyRunPageProps) { - const { runId } = await params; - let initialRunState: SerializedWorkflowRunState | null = null; - let ssrError: string | null = null; - - const result = await loadRunSnapshot(runId); - if (result.ok) { - initialRunState = result.data; - } else { - const detail = (result.body as { detail?: string })?.detail; - ssrError = detail ?? `Run API returned ${result.status}`; - } - - return ; -} diff --git a/ergon-dashboard/src/app/samples/[sampleId]/page.tsx b/ergon-dashboard/src/app/samples/[sampleId]/page.tsx new file mode 100644 index 000000000..a62ad0a0f --- /dev/null +++ b/ergon-dashboard/src/app/samples/[sampleId]/page.tsx @@ -0,0 +1,25 @@ +import { SampleWorkspacePage } from "@/components/sample/SampleWorkspacePage"; +import { loadRunSnapshot } from "@/lib/server-data/samples"; +import type { SerializedSampleWorkspaceState } from "@/lib/types"; + +interface LegacyRunPageProps { + params: Promise<{ + sampleId: string; + }>; +} + +export default async function RunPage({ params }: LegacyRunPageProps) { + const { sampleId } = await params; + let initialRunState: SerializedSampleWorkspaceState | null = null; + let ssrError: string | null = null; + + const result = await loadRunSnapshot(sampleId); + if (result.ok) { + initialRunState = result.data; + } else { + const detail = (result.body as { detail?: string })?.detail; + ssrError = detail ?? `Run API returned ${result.status}`; + } + + return ; +} diff --git a/ergon-dashboard/src/app/runs/page.tsx b/ergon-dashboard/src/app/samples/page.tsx similarity index 94% rename from ergon-dashboard/src/app/runs/page.tsx rename to ergon-dashboard/src/app/samples/page.tsx index b59aff93f..97393b9c2 100644 --- a/ergon-dashboard/src/app/runs/page.tsx +++ b/ergon-dashboard/src/app/samples/page.tsx @@ -1,5 +1,5 @@ -import { RunIndexTable } from "@/components/indexes/RunIndexTable"; -import { loadRunList, type RunSummary } from "@/lib/server-data/runs"; +import { SampleIndexTable } from "@/components/indexes/SampleIndexTable"; +import { loadRunList, type RunSummary } from "@/lib/server-data/samples"; export default async function RunsPage() { let runs: RunSummary[] = []; @@ -47,7 +47,7 @@ export default async function RunsPage() {
- + ); } diff --git a/ergon-dashboard/src/components/dag/DAGCanvas.tsx b/ergon-dashboard/src/components/dag/DAGCanvas.tsx index e3ca411a0..3934d1608 100644 --- a/ergon-dashboard/src/components/dag/DAGCanvas.tsx +++ b/ergon-dashboard/src/components/dag/DAGCanvas.tsx @@ -7,7 +7,7 @@ * - Hierarchical dagre layout with nested container rendering * - Depth-based expansion control via floating controls * - Search/filter tasks by name - * - Live updates via useRunState hook + * - Live updates via useSampleWorkspaceState hook * - Zoom/pan controls */ @@ -25,7 +25,7 @@ import { } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; -import { TaskStatus, type WorkflowRunState } from "@/lib/types"; +import { TaskStatus, type SampleWorkspaceState } from "@/lib/types"; import { nodeTypes, type TaskNodeType } from "./TaskNode"; import { GraphDependencyEdge } from "./edges/GraphDependencyEdge"; import { buildContainerEvaluationRollup } from "@/features/evaluation/selectors"; @@ -36,8 +36,8 @@ import type { ContainerDimensions } from "@/features/graph/layout/layoutTypes"; import { SearchInput } from "@/components/common/SearchInput"; interface DAGCanvasProps { - runId: string; - runState: WorkflowRunState | null; + sampleId: string; + runState: SampleWorkspaceState | null; isLoading?: boolean; error?: string | null; isSubscribed?: boolean; @@ -235,7 +235,7 @@ function LegendCard() { /* ─── Main canvas ───────────────────────────────────────────────── */ function DAGCanvasInner({ - runId, + sampleId, runState, isLoading = false, error = null, @@ -444,7 +444,7 @@ function DAGCanvasInner({

{error}

- Run ID: {runId} + Sample ID: {sampleId}

@@ -474,7 +474,7 @@ function DAGCanvasInner({ : "Connecting to server..."}

- Run ID: {runId} + Sample ID: {sampleId}

diff --git a/ergon-dashboard/src/components/experiments/RunMetricExplorer.tsx b/ergon-dashboard/src/components/experiments/SampleRunMetricExplorer.tsx similarity index 93% rename from ergon-dashboard/src/components/experiments/RunMetricExplorer.tsx rename to ergon-dashboard/src/components/experiments/SampleRunMetricExplorer.tsx index 40b91f475..5a42f37ff 100644 --- a/ergon-dashboard/src/components/experiments/RunMetricExplorer.tsx +++ b/ergon-dashboard/src/components/experiments/SampleRunMetricExplorer.tsx @@ -9,16 +9,16 @@ import { metricDescriptor, percentile, RUN_METRIC_DESCRIPTORS, - selectRunMetricExplorerView, - type RunMetricExplorerMode, + selectSampleRunMetricExplorerView, + type SampleRunMetricExplorerMode, type RunMetricKey, type RunMetricPoint, -} from "./runMetricExplorerModel"; +} from "./sampleRunMetricExplorerModel"; -interface RunMetricExplorerProps { +interface SampleRunMetricExplorerProps { points: RunMetricPoint[]; runHrefBase?: string; - initialMode?: RunMetricExplorerMode; + initialMode?: SampleRunMetricExplorerMode; initialPrimaryMetric?: RunMetricKey; initialSecondaryMetric?: RunMetricKey; } @@ -38,8 +38,8 @@ function scale(value: number, min: number, max: number, outputMin: number, outpu return outputMin + ((value - min) / (max - min)) * (outputMax - outputMin); } -function runHref(runHrefBase: string, runId: string) { - return `${runHrefBase.replace(/\/$/, "")}/${runId}`; +function runHref(runHrefBase: string, sampleId: string) { + return `${runHrefBase.replace(/\/$/, "")}/${sampleId}`; } function HoverCard({ @@ -97,8 +97,8 @@ function RankedList({
{sorted.map((point, index) => ( onHover(point)} onMouseLeave={() => onHover(null)} className="flex items-center justify-between gap-3 rounded-[var(--radius-sm)] border border-[var(--line)] bg-[var(--card)] px-3 py-2 text-xs hover:border-[var(--line-strong)] hover:bg-[var(--paper)]" @@ -138,8 +138,8 @@ function StripPlot({ const left = scale(value, min, max, CHART_PAD, CHART_WIDTH - CHART_PAD); return ( onHover(point)} onMouseLeave={() => onHover(null)} className="absolute top-1/2 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border border-[var(--card)] bg-[var(--accent)] shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)]" @@ -206,8 +206,8 @@ function Histogram({
{points.slice(0, 12).map((point) => ( onHover(point)} onMouseLeave={() => onHover(null)} className="rounded-full border border-[var(--line)] bg-[var(--card)] px-2 py-0.5 font-mono text-[10px] text-[var(--muted)] hover:border-[var(--line-strong)] hover:text-[var(--ink)]" @@ -255,8 +255,8 @@ function ScatterPlot({ const y = scale(point.metrics[secondaryMetricKey].value ?? 0, yMin, yMax, CHART_HEIGHT - CHART_PAD, CHART_PAD); return ( onHover(point)} onMouseLeave={() => onHover(null)} > @@ -273,14 +273,14 @@ function ScatterPlot({ ); } -export function RunMetricExplorer({ +export function SampleRunMetricExplorer({ points, - runHrefBase = "/run", + runHrefBase = "/samples", initialMode = "1d", initialPrimaryMetric = "score", initialSecondaryMetric = "duration_ms", -}: RunMetricExplorerProps) { - const [mode, setMode] = useState(initialMode); +}: SampleRunMetricExplorerProps) { + const [mode, setMode] = useState(initialMode); const [primaryMetricKey, setPrimaryMetricKey] = useState(initialPrimaryMetric); const [secondaryMetricKey, setSecondaryMetricKey] = useState(initialSecondaryMetric); const [hoveredPoint, setHoveredPoint] = useState(null); @@ -288,11 +288,11 @@ export function RunMetricExplorer({ () => availableMetricPoints(points, primaryMetricKey, mode === "2d" ? secondaryMetricKey : undefined), [mode, points, primaryMetricKey, secondaryMetricKey], ); - const view = selectRunMetricExplorerView(mode, points, primaryMetricKey, secondaryMetricKey); + const view = selectSampleRunMetricExplorerView(mode, points, primaryMetricKey, secondaryMetricKey); const primaryDescriptor = metricDescriptor(primaryMetricKey); return ( -
+

Run metric explorer

diff --git a/ergon-dashboard/src/components/experiments/runMetricExplorerModel.test.ts b/ergon-dashboard/src/components/experiments/sampleRunMetricExplorerModel.test.ts similarity index 81% rename from ergon-dashboard/src/components/experiments/runMetricExplorerModel.test.ts rename to ergon-dashboard/src/components/experiments/sampleRunMetricExplorerModel.test.ts index bbfd52e48..85534f84f 100644 --- a/ergon-dashboard/src/components/experiments/runMetricExplorerModel.test.ts +++ b/ergon-dashboard/src/components/experiments/sampleRunMetricExplorerModel.test.ts @@ -7,16 +7,16 @@ import { formatRunMetricValue, metricDescriptor, normalizeRunMetricPoint, - selectRunMetricExplorerView, + selectSampleRunMetricExplorerView, type RunMetricPoint, -} from "./runMetricExplorerModel"; +} from "./sampleRunMetricExplorerModel"; const definitionId = "11111111-1111-4111-8111-111111111111"; const defaultRunId = "22222222-2222-4222-8222-222222222222"; function metrics(overrides: Partial = {}): ExperimentRunRow["metrics"] { return { - run_id: defaultRunId, + sample_id: defaultRunId, status: "completed", instance_key: "sample-a", tool_call_count: 0, @@ -27,7 +27,7 @@ function metrics(overrides: Partial = {}): Experime function runRow(overrides: Partial = {}): ExperimentRunRow { return { - run_id: defaultRunId, + sample_id: defaultRunId, definition_id: definitionId, benchmark_type: "minif2f", instance_key: "sample-a", @@ -52,7 +52,7 @@ function runRow(overrides: Partial = {}): ExperimentRunRow { function point(index: number): RunMetricPoint { return normalizeRunMetricPoint( runRow({ - run_id: `${String(index).padStart(8, "0")}-2222-4222-8222-222222222222`, + sample_id: `${String(index).padStart(8, "0")}-2222-4222-8222-222222222222`, instance_key: `sample-${index}`, final_score: index / 10, running_time_ms: index * 1000, @@ -110,11 +110,11 @@ test("uses explicit observed token and cost instrumentation when present", () => }); test("selects small-n explorer views by mode and numeric threshold", () => { - assert.equal(selectRunMetricExplorerView("1d", [point(1), point(2), point(3), point(4)], "score"), "ranked-list"); - assert.equal(selectRunMetricExplorerView("1d", Array.from({ length: 5 }, (_, index) => point(index + 1)), "score"), "strip"); - assert.equal(selectRunMetricExplorerView("1d", Array.from({ length: 20 }, (_, index) => point(index + 1)), "score"), "histogram"); - assert.equal(selectRunMetricExplorerView("2d", [point(1), point(2)], "score", "duration_ms"), "too-few-points"); - assert.equal(selectRunMetricExplorerView("2d", [point(1), point(2), point(3)], "score", "duration_ms"), "scatter"); + assert.equal(selectSampleRunMetricExplorerView("1d", [point(1), point(2), point(3), point(4)], "score"), "ranked-list"); + assert.equal(selectSampleRunMetricExplorerView("1d", Array.from({ length: 5 }, (_, index) => point(index + 1)), "score"), "strip"); + assert.equal(selectSampleRunMetricExplorerView("1d", Array.from({ length: 20 }, (_, index) => point(index + 1)), "score"), "histogram"); + assert.equal(selectSampleRunMetricExplorerView("2d", [point(1), point(2)], "score", "duration_ms"), "too-few-points"); + assert.equal(selectSampleRunMetricExplorerView("2d", [point(1), point(2), point(3)], "score", "duration_ms"), "scatter"); }); test("formats available and unavailable metric states", () => { diff --git a/ergon-dashboard/src/components/experiments/runMetricExplorerModel.ts b/ergon-dashboard/src/components/experiments/sampleRunMetricExplorerModel.ts similarity index 95% rename from ergon-dashboard/src/components/experiments/runMetricExplorerModel.ts rename to ergon-dashboard/src/components/experiments/sampleRunMetricExplorerModel.ts index 856c40104..fb00704cd 100644 --- a/ergon-dashboard/src/components/experiments/runMetricExplorerModel.ts +++ b/ergon-dashboard/src/components/experiments/sampleRunMetricExplorerModel.ts @@ -25,7 +25,7 @@ export interface RunMetricValue { } export interface RunMetricPoint { - runId: string; + sampleId: string; runName: string; status: RunLifecycleStatus | string; sampleLabel: string; @@ -39,8 +39,8 @@ export interface RunMetricPoint { tokenBreakdown: Record | null; } -export type RunMetricExplorerMode = "1d" | "2d"; -export type RunMetricExplorerView = +export type SampleRunMetricExplorerMode = "1d" | "2d"; +export type SampleRunMetricExplorerView = | "empty" | "ranked-list" | "strip" @@ -107,12 +107,12 @@ export function availableMetricPoints( }); } -export function selectRunMetricExplorerView( - mode: RunMetricExplorerMode, +export function selectSampleRunMetricExplorerView( + mode: SampleRunMetricExplorerMode, points: RunMetricPoint[], primaryMetricKey: RunMetricKey, secondaryMetricKey?: RunMetricKey, -): RunMetricExplorerView { +): SampleRunMetricExplorerView { const comparableCount = availableMetricPoints(points, primaryMetricKey, mode === "2d" ? secondaryMetricKey : undefined).length; if (comparableCount === 0) return "empty"; if (mode === "2d") return comparableCount >= 3 ? "scatter" : "too-few-points"; @@ -151,8 +151,8 @@ export function normalizeRunMetricPoint(run: ExperimentRunRow): RunMetricPoint { const tokenBreakdown = metrics.token_breakdown ?? null; return { - runId: run.run_id, - runName: metrics.run_name ?? run.run_id.slice(0, 8), + sampleId: run.sample_id, + runName: metrics.run_name ?? run.sample_id.slice(0, 8), status: run.status, sampleLabel: metrics.sample_label ?? run.instance_key, instanceKey: run.instance_key, diff --git a/ergon-dashboard/src/components/indexes/RunIndexTable.tsx b/ergon-dashboard/src/components/indexes/SampleIndexTable.tsx similarity index 97% rename from ergon-dashboard/src/components/indexes/RunIndexTable.tsx rename to ergon-dashboard/src/components/indexes/SampleIndexTable.tsx index b4640b013..84e79fe15 100644 --- a/ergon-dashboard/src/components/indexes/RunIndexTable.tsx +++ b/ergon-dashboard/src/components/indexes/SampleIndexTable.tsx @@ -10,10 +10,10 @@ import { formatNumber, formatPercent, } from "@/components/indexes/format"; -import type { RunSummary } from "@/lib/server-data/runs"; +import type { RunSummary } from "@/lib/server-data/samples"; import type { RunLifecycleStatus } from "@/lib/types"; -export function RunIndexTable({ runs }: { runs: RunSummary[] }) { +export function SampleIndexTable({ runs }: { runs: RunSummary[] }) { const [query, setQuery] = useState(""); const [status, setStatus] = useState("all"); @@ -87,7 +87,7 @@ export function RunIndexTable({ runs }: { runs: RunSummary[] }) { > {run.name} diff --git a/ergon-dashboard/src/components/panels/EvaluationPanel.test.ts b/ergon-dashboard/src/components/panels/EvaluationPanel.test.ts index a5560cbbc..16d355542 100644 --- a/ergon-dashboard/src/components/panels/EvaluationPanel.test.ts +++ b/ergon-dashboard/src/components/panels/EvaluationPanel.test.ts @@ -9,7 +9,7 @@ import { EvaluationPanel } from "./EvaluationPanel"; function evaluation(): TaskEvaluationState { return { id: "evaluation-1", - runId: "run-1", + sampleId: "run-1", taskId: "task-1", evaluatorName: "rubric", aggregationRule: "weighted_sum", diff --git a/ergon-dashboard/src/components/panels/ResourcePanel.tsx b/ergon-dashboard/src/components/panels/ResourcePanel.tsx index 70ff7d028..405004c0f 100644 --- a/ergon-dashboard/src/components/panels/ResourcePanel.tsx +++ b/ergon-dashboard/src/components/panels/ResourcePanel.tsx @@ -16,7 +16,7 @@ import { formatDate } from "@/lib/timeFormat"; interface ResourcePanelProps { resources: ResourceState[]; - runId?: string | null; + sampleId?: string | null; } /** @@ -186,7 +186,7 @@ function ResourceItem({ resource, onOpen }: ResourceItemProps) { return
{content}
; } -export function ResourcePanel({ resources, runId = null }: ResourcePanelProps) { +export function ResourcePanel({ resources, sampleId = null }: ResourcePanelProps) { const [selected, setSelected] = useState(null); if (resources.length === 0) { @@ -211,9 +211,9 @@ export function ResourcePanel({ resources, runId = null }: ResourcePanelProps) { ); } - // onOpen is only wired when we have a runId — otherwise clicking has no + // onOpen is only wired when we have a sampleId — otherwise clicking has no // way to fetch content, so keep the row as a non-interactive div. - const onOpen = runId !== null ? setSelected : undefined; + const onOpen = sampleId !== null ? setSelected : undefined; return (
@@ -226,7 +226,7 @@ export function ResourcePanel({ resources, runId = null }: ResourcePanelProps) { ))}
setSelected(null)} /> diff --git a/ergon-dashboard/src/components/run/RunHeaderMetrics.test.ts b/ergon-dashboard/src/components/sample/SampleRuntimeSummaryHeader.test.ts similarity index 87% rename from ergon-dashboard/src/components/run/RunHeaderMetrics.test.ts rename to ergon-dashboard/src/components/sample/SampleRuntimeSummaryHeader.test.ts index 657dad7e1..47b3b700c 100644 --- a/ergon-dashboard/src/components/run/RunHeaderMetrics.test.ts +++ b/ergon-dashboard/src/components/sample/SampleRuntimeSummaryHeader.test.ts @@ -3,11 +3,11 @@ import test from "node:test"; import React from "react"; import { renderToStaticMarkup } from "react-dom/server"; -import { RunHeaderMetrics } from "./RunHeaderMetrics"; +import { SampleRuntimeSummaryHeader } from "./SampleRuntimeSummaryHeader"; test("run header metrics render strong values and unavailable states", () => { const html = renderToStaticMarkup( - React.createElement(RunHeaderMetrics, { + React.createElement(SampleRuntimeSummaryHeader, { metrics: { tasks: { completed: 3, running: 1, failed: 1, total: 6 }, tokens: null, @@ -29,7 +29,7 @@ test("run header metrics render strong values and unavailable states", () => { test("run header metrics render observed token cost and score values", () => { const html = renderToStaticMarkup( - React.createElement(RunHeaderMetrics, { + React.createElement(SampleRuntimeSummaryHeader, { metrics: { tasks: { completed: 8, running: 0, failed: 0, total: 8 }, tokens: 9820, diff --git a/ergon-dashboard/src/components/run/RunHeaderMetrics.tsx b/ergon-dashboard/src/components/sample/SampleRuntimeSummaryHeader.tsx similarity index 93% rename from ergon-dashboard/src/components/run/RunHeaderMetrics.tsx rename to ergon-dashboard/src/components/sample/SampleRuntimeSummaryHeader.tsx index ebd0e7941..0fbea60ac 100644 --- a/ergon-dashboard/src/components/run/RunHeaderMetrics.tsx +++ b/ergon-dashboard/src/components/sample/SampleRuntimeSummaryHeader.tsx @@ -6,7 +6,7 @@ import { formatTaskCount, formatTokens, type MetricDisplay, -} from "@/lib/run-state/formatters"; +} from "@/lib/sample-state/formatters"; export interface RunHeaderMetricValues { tasks: { @@ -59,7 +59,7 @@ function MetricTile({ ); } -export function RunHeaderMetrics({ metrics }: { metrics: RunHeaderMetricValues }) { +export function SampleRuntimeSummaryHeader({ metrics }: { metrics: RunHeaderMetricValues }) { return (
; total: number; onFilter?: (status: TaskStatus | null) => void; activeFilter?: TaskStatus | null; } -export function RunStatusBar({ +export function SampleStatusBar({ counts, total, onFilter, activeFilter = null, -}: RunStatusBarProps) { +}: SampleStatusBarProps) { const safeTotal = total > 0 ? total : 1; return ( -
+
{TASK_STATUS_ORDER.map((status) => { const tokens = STATUS_TOKENS[status]; @@ -52,7 +52,7 @@ export function RunStatusBar({
{TASK_STATUS_ORDER.map((status) => { const tokens = STATUS_TOKENS[status]; diff --git a/ergon-dashboard/src/components/run/RunWorkspacePage.tsx b/ergon-dashboard/src/components/sample/SampleWorkspacePage.tsx similarity index 92% rename from ergon-dashboard/src/components/run/RunWorkspacePage.tsx rename to ergon-dashboard/src/components/sample/SampleWorkspacePage.tsx index a9554cf6f..0f07dafcd 100644 --- a/ergon-dashboard/src/components/run/RunWorkspacePage.tsx +++ b/ergon-dashboard/src/components/sample/SampleWorkspacePage.tsx @@ -7,8 +7,8 @@ import { Group, Panel, Separator } from "react-resizable-panels"; import { DAGCanvas } from "@/components/dag/DAGCanvas"; import { StatusBadge } from "@/components/common/StatusBadge"; -import { RunHeaderMetrics, type RunHeaderMetricValues } from "@/components/run/RunHeaderMetrics"; -import { UnifiedEventStream } from "@/components/run/UnifiedEventStream"; +import { SampleRuntimeSummaryHeader, type RunHeaderMetricValues } from "@/components/sample/SampleRuntimeSummaryHeader"; +import { UnifiedEventStream } from "@/components/sample/UnifiedEventStream"; import { TaskWorkspace } from "@/components/workspace/TaskWorkspace"; import { ActivityStackTimeline } from "@/features/activity/components/ActivityStackTimeline"; import { buildRunActivities } from "@/features/activity/buildRunActivities"; @@ -18,17 +18,17 @@ import { parseGraphMutationDtoArray, type GraphMutationDto, } from "@/features/graph/contracts/graphMutations"; -import { useRunState } from "@/hooks/useRunState"; -import { buildRunEvents } from "@/lib/runEvents"; -import { RunLifecycleStatus, SerializedWorkflowRunState, TaskStatus, type WorkflowRunState } from "@/lib/types"; +import { useSampleWorkspaceState } from "@/hooks/useSampleWorkspaceState"; +import { buildRunEvents } from "@/lib/sampleEvents"; +import { RunLifecycleStatus, SerializedSampleWorkspaceState, TaskStatus, type SampleWorkspaceState } from "@/lib/types"; import { nearestMutationAtOrBefore, - useRunDisplayState, -} from "@/components/run/useRunDisplayState"; -import { useRunKeyboardShortcuts } from "@/components/run/useRunKeyboardShortcuts"; -import { panelPercent, useRunPanelLayout } from "@/components/run/useRunPanelLayout"; -import { resolveReplayStep } from "@/components/run/replayNavigation"; -import { formatDuration } from "@/lib/run-state/formatters"; + useSampleDisplayState, +} from "@/components/sample/useSampleDisplayState"; +import { useSampleKeyboardShortcuts } from "@/components/sample/useSampleKeyboardShortcuts"; +import { panelPercent, useSamplePanelLayout } from "@/components/sample/useSamplePanelLayout"; +import { resolveReplayStep } from "@/components/sample/replayNavigation"; +import { formatDuration } from "@/lib/sample-state/formatters"; type OptionalRunMetrics = { metrics?: { @@ -38,7 +38,7 @@ type OptionalRunMetrics = { } | null; }; -function countObservedTokens(runState: WorkflowRunState | null): number | null { +function countObservedTokens(runState: SampleWorkspaceState | null): number | null { if (!runState) return null; let tokenCount = 0; for (const events of runState.contextEventsByTask.values()) { @@ -52,13 +52,13 @@ function countObservedTokens(runState: WorkflowRunState | null): number | null { return tokenCount > 0 ? tokenCount : null; } -export function RunWorkspacePage({ - runId, +export function SampleWorkspacePage({ + sampleId, initialRunState = null, ssrError = null, }: { - runId: string; - initialRunState?: SerializedWorkflowRunState | null; + sampleId: string; + initialRunState?: SerializedSampleWorkspaceState | null; ssrError?: string | null; }) { const [selectedTaskId, setSelectedTaskId] = useState(null); @@ -72,8 +72,8 @@ export function RunWorkspacePage({ horizontalLayout, setHorizontalLayout, hasLoadedPanelLayouts, - } = useRunPanelLayout(); - const { runState, isLoading, error, isSubscribed } = useRunState(runId, initialRunState); + } = useSamplePanelLayout(); + const { runState, isLoading, error, isSubscribed } = useSampleWorkspaceState(sampleId, initialRunState); const [mutations, setMutations] = useState([]); const requestedSequenceRef = useRef(null); @@ -89,7 +89,7 @@ export function RunWorkspacePage({ setSnapshotSequence, currentSequence, selectedTimelineTime, - } = useRunDisplayState(runState, mutations); + } = useSampleDisplayState(runState, mutations); useEffect(() => { selectedActivityIdRef.current = selectedActivityId; @@ -100,7 +100,7 @@ export function RunWorkspacePage({ let cancelled = false; mutationsLoadedRef.current = false; pendingActivityResolutionRef.current = null; - fetch(`/api/runs/${runId}/mutations`) + fetch(`/api/samples/${sampleId}/mutations`) .then((res) => res.json()) .then((data) => { if (cancelled) return; @@ -134,7 +134,7 @@ export function RunWorkspacePage({ return () => { cancelled = true; }; - }, [runId, setSnapshotSequence]); + }, [sampleId, setSnapshotSequence]); // Status counts shown in the run header. Only leaf tasks so the totals // match the "units of work" the user is tracking (parents double-count). @@ -158,7 +158,7 @@ export function RunWorkspacePage({ }, [displayState]); const runHeaderMetrics: RunHeaderMetricValues = useMemo(() => { - const optionalMetrics = runState as (WorkflowRunState & OptionalRunMetrics) | null; + const optionalMetrics = runState as (SampleWorkspaceState & OptionalRunMetrics) | null; return { tasks: { completed: leafStatusCounts[TaskStatus.COMPLETED], @@ -210,7 +210,7 @@ export function RunWorkspacePage({ return ids; }, [selectedActivity, selectedTaskId]); - useRunKeyboardShortcuts({ + useSampleKeyboardShortcuts({ selectedTaskId, clearSelectedTask: () => setSelectedTaskId(null), snapshotSequence, @@ -275,7 +275,7 @@ export function RunWorkspacePage({ {/* Run header strip */}
@@ -283,11 +283,11 @@ export function RunWorkspacePage({ Experiment - {runId.slice(0, 8)}… + {sampleId.slice(0, 8)}…

- {runState?.name ?? runId} + {runState?.name ?? sampleId}

@@ -297,7 +297,7 @@ export function RunWorkspacePage({
- + {mutations.length > 0 && (
{error}
@@ -474,7 +474,7 @@ export function RunWorkspacePage({ data-testid="graph-region" > void; snapshotSequence: number | null; diff --git a/ergon-dashboard/src/components/run/useRunPanelLayout.ts b/ergon-dashboard/src/components/sample/useSamplePanelLayout.ts similarity index 98% rename from ergon-dashboard/src/components/run/useRunPanelLayout.ts rename to ergon-dashboard/src/components/sample/useSamplePanelLayout.ts index 5ecc3083d..e562e7335 100644 --- a/ergon-dashboard/src/components/run/useRunPanelLayout.ts +++ b/ergon-dashboard/src/components/sample/useSamplePanelLayout.ts @@ -39,7 +39,7 @@ export function panelPercent(layout: Layout, id: string, fallback: number): stri return `${Number.isFinite(size) ? size : fallback}%`; } -export function useRunPanelLayout() { +export function useSamplePanelLayout() { const [verticalLayout, setVerticalLayoutState] = useState(() => loadPanelLayout(VERTICAL_LAYOUT_STORAGE_KEY, DEFAULT_VERTICAL_LAYOUT), ); diff --git a/ergon-dashboard/src/components/shell/Topbar.tsx b/ergon-dashboard/src/components/shell/Topbar.tsx index 94f4c1f0f..bc3bf0dff 100644 --- a/ergon-dashboard/src/components/shell/Topbar.tsx +++ b/ergon-dashboard/src/components/shell/Topbar.tsx @@ -5,15 +5,15 @@ import { usePathname } from "next/navigation"; const NAV_ITEMS = [ { label: "Experiments", href: "/experiments" }, - { label: "Runs", href: "/runs" }, + { label: "Runs", href: "/samples" }, ] as const; function isActive(href: string, pathname: string): boolean { if (href === "/experiments") { return pathname === "/" || pathname.startsWith("/experiments"); } - if (href === "/runs") { - return pathname.startsWith("/run/") || pathname.startsWith("/runs"); + if (href === "/samples") { + return pathname.startsWith("/samples/") || pathname.startsWith("/samples"); } return pathname.startsWith(href); } diff --git a/ergon-dashboard/src/components/viewers/ResourceViewerDialog.tsx b/ergon-dashboard/src/components/viewers/ResourceViewerDialog.tsx index edc435563..779e62429 100644 --- a/ergon-dashboard/src/components/viewers/ResourceViewerDialog.tsx +++ b/ergon-dashboard/src/components/viewers/ResourceViewerDialog.tsx @@ -16,27 +16,27 @@ import { resourceContentUrl, useResourceContent } from "@/hooks/useResourceConte import type { ResourceState } from "@/lib/types"; interface ResourceViewerDialogProps { - runId: string | null; + sampleId: string | null; resource: ResourceState | null; onClose: () => void; } -export function ResourceViewerDialog({ runId, resource, onClose }: ResourceViewerDialogProps) { - const open = resource !== null && runId !== null; +export function ResourceViewerDialog({ sampleId, resource, onClose }: ResourceViewerDialogProps) { + const open = resource !== null && sampleId !== null; const kind = resource ? resolveViewerKind(resource.mimeType) : "text"; const wantsText = viewerWantsText(kind); const { text, error, isLoading } = useResourceContent( - runId, + sampleId, resource?.id ?? null, open && wantsText, ); return ( - {resource === null || runId === null ? null : ( + {resource === null || sampleId === null ? null : ( void; @@ -453,7 +453,7 @@ export function TaskWorkspace({ {activeTab === "outputs" && ( - + )} diff --git a/ergon-dashboard/src/components/workspace/filterTaskEvidenceForTime.test.ts b/ergon-dashboard/src/components/workspace/filterTaskEvidenceForTime.test.ts index 13d9985e9..74ed1c7a1 100644 --- a/ergon-dashboard/src/components/workspace/filterTaskEvidenceForTime.test.ts +++ b/ergon-dashboard/src/components/workspace/filterTaskEvidenceForTime.test.ts @@ -1,8 +1,8 @@ import assert from "node:assert/strict"; import test from "node:test"; -import fixture from "../../../tests/fixtures/mas-runs/concurrent-mas-run.json"; -import { deserializeRunState } from "@/lib/runState"; +import fixture from "../../../tests/fixtures/mas-samples/concurrent-mas-run.json"; +import { deserializeRunState } from "@/lib/sampleState"; import { filterTaskEvidenceForTime } from "./filterTaskEvidenceForTime"; const searchTaskId = "10000000-0000-4000-8000-000000000002"; diff --git a/ergon-dashboard/src/features/activity/buildRunActivities.test.ts b/ergon-dashboard/src/features/activity/buildRunActivities.test.ts index 016de9b80..4ff28454f 100644 --- a/ergon-dashboard/src/features/activity/buildRunActivities.test.ts +++ b/ergon-dashboard/src/features/activity/buildRunActivities.test.ts @@ -1,11 +1,11 @@ import assert from "node:assert/strict"; import test from "node:test"; -import fixture from "../../../tests/fixtures/mas-runs/concurrent-mas-run.json"; +import fixture from "../../../tests/fixtures/mas-samples/concurrent-mas-run.json"; import { parseGraphMutationDtoArray } from "@/features/graph/contracts/graphMutations"; -import type { RunEvent } from "@/lib/runEvents"; -import { buildRunEvents } from "@/lib/runEvents"; -import { deserializeRunState } from "@/lib/runState"; +import type { RunEvent } from "@/lib/sampleEvents"; +import { buildRunEvents } from "@/lib/sampleEvents"; +import { deserializeRunState } from "@/lib/sampleState"; import { TaskStatus, TaskTrigger } from "@/lib/types"; import { buildRunActivities } from "./buildRunActivities"; import { resolveActivitySnapshotSequence } from "./snapshotSequence"; @@ -54,7 +54,7 @@ test("buildRunActivities surfaces semantic activity kinds without creating actor runState.contextEventsByTask.set(noisyTaskId, [ { id: "context-noisy", - runId: runState.id, + sampleId: runState.id, taskExecutionId: "execution-noisy", taskId: noisyTaskId, workerBindingKey: "worker-1", @@ -78,7 +78,7 @@ test("buildRunActivities surfaces semantic activity kinds without creating actor ...runState.threads, { id: "thread-noisy", - runId: runState.id, + sampleId: runState.id, taskId: noisyTaskId, topic: "coordination", agentAId: "agent-a", @@ -90,7 +90,7 @@ test("buildRunActivities surfaces semantic activity kinds without creating actor id: "message-noisy", threadId: "thread-noisy", threadTopic: "coordination", - runId: runState.id, + sampleId: runState.id, taskId: noisyTaskId, taskExecutionId: null, fromAgentId: "agent-a", diff --git a/ergon-dashboard/src/features/activity/buildRunActivities.ts b/ergon-dashboard/src/features/activity/buildRunActivities.ts index 1c65ee3e5..5e8b43d02 100644 --- a/ergon-dashboard/src/features/activity/buildRunActivities.ts +++ b/ergon-dashboard/src/features/activity/buildRunActivities.ts @@ -3,13 +3,13 @@ import type { ContextEventState, ExecutionAttemptState, SandboxCommandState, - WorkflowRunState, + SampleWorkspaceState, } from "@/lib/types"; -import type { RunEvent } from "@/lib/runEvents"; +import type { RunEvent } from "@/lib/sampleEvents"; import type { RunActivity } from "./types"; export interface BuildRunActivitiesInput { - runState: WorkflowRunState | null; + runState: SampleWorkspaceState | null; events: RunEvent[]; mutations: GraphMutationDto[]; currentSequence: number | null; @@ -27,7 +27,7 @@ function compareActivity(a: RunActivity, b: RunActivity): number { return a.id.localeCompare(b.id); } -function executionLabel(execution: ExecutionAttemptState, run: WorkflowRunState): string { +function executionLabel(execution: ExecutionAttemptState, run: SampleWorkspaceState): string { const task = run.tasks.get(execution.taskId); return task?.name ?? `Attempt ${execution.attemptNumber}`; } @@ -44,7 +44,7 @@ function addMs(timestamp: string, durationMs: number | null): string | null { } function executionActivities( - run: WorkflowRunState, + run: SampleWorkspaceState, ): RunActivity[] { const activities: RunActivity[] = []; for (const executions of run.executionsByTask.values()) { @@ -89,7 +89,7 @@ function sandboxCommandLabel(command: SandboxCommandState): string { } function sandboxActivities( - run: WorkflowRunState, + run: SampleWorkspaceState, ): RunActivity[] { const activities: RunActivity[] = []; for (const sandbox of run.sandboxesByTask.values()) { @@ -172,7 +172,7 @@ function contextLabel(event: ContextEventState): string { return payloadType ?? event.eventType; } -function contextActivities(run: WorkflowRunState): RunActivity[] { +function contextActivities(run: SampleWorkspaceState): RunActivity[] { const activities: RunActivity[] = []; for (const [taskId, events] of run.contextEventsByTask.entries()) { for (const event of events) { diff --git a/ergon-dashboard/src/features/activity/goldenFixture.test.ts b/ergon-dashboard/src/features/activity/goldenFixture.test.ts index c8eecbe6f..431cd8583 100644 --- a/ergon-dashboard/src/features/activity/goldenFixture.test.ts +++ b/ergon-dashboard/src/features/activity/goldenFixture.test.ts @@ -1,16 +1,16 @@ import assert from "node:assert/strict"; import test from "node:test"; -import fixture from "../../../tests/fixtures/mas-runs/concurrent-mas-run.json"; +import fixture from "../../../tests/fixtures/mas-samples/concurrent-mas-run.json"; import { parseGraphMutationDtoArray } from "@/features/graph/contracts/graphMutations"; import { replayToSequence } from "@/features/graph/state/graphMutationReducer"; -import { buildRunEvents } from "@/lib/runEvents"; -import { deserializeRunState } from "@/lib/runState"; -import type { WorkflowRunState } from "@/lib/types"; +import { buildRunEvents } from "@/lib/sampleEvents"; +import { deserializeRunState } from "@/lib/sampleState"; +import type { SampleWorkspaceState } from "@/lib/types"; import { buildRunActivities } from "./buildRunActivities"; import { stackActivities } from "./stackLayout"; -function emptyRunStateFrom(runState: WorkflowRunState): WorkflowRunState { +function emptyRunStateFrom(runState: SampleWorkspaceState): SampleWorkspaceState { return { ...runState, tasks: new Map(), diff --git a/ergon-dashboard/src/features/activity/snapshotSequence.test.ts b/ergon-dashboard/src/features/activity/snapshotSequence.test.ts index 0e61e03c0..dacf820ac 100644 --- a/ergon-dashboard/src/features/activity/snapshotSequence.test.ts +++ b/ergon-dashboard/src/features/activity/snapshotSequence.test.ts @@ -28,7 +28,7 @@ function activity(overrides: Partial = {}): RunActivity { function mutation(sequence: number, createdAt: string): GraphMutationDto { return { id: "00000000-0000-4000-8000-000000000001", - run_id: "00000000-0000-4000-8000-000000000002", + sample_id: "00000000-0000-4000-8000-000000000002", sequence, mutation_type: "node.added", target_type: "node", diff --git a/ergon-dashboard/src/features/activity/types.ts b/ergon-dashboard/src/features/activity/types.ts index 9388bda4a..16d4b45bc 100644 --- a/ergon-dashboard/src/features/activity/types.ts +++ b/ergon-dashboard/src/features/activity/types.ts @@ -1,4 +1,4 @@ -import type { RunEventKind } from "@/lib/runEvents"; +import type { RunEventKind } from "@/lib/sampleEvents"; export type ActivityKind = | "execution" diff --git a/ergon-dashboard/src/features/evaluation/selectors.test.ts b/ergon-dashboard/src/features/evaluation/selectors.test.ts index bb56d737e..0aee8dbed 100644 --- a/ergon-dashboard/src/features/evaluation/selectors.test.ts +++ b/ergon-dashboard/src/features/evaluation/selectors.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import type { TaskEvaluationState, TaskState, WorkflowRunState } from "@/lib/types"; +import type { TaskEvaluationState, TaskState, SampleWorkspaceState } from "@/lib/types"; import { TaskStatus } from "@/lib/types"; import { buildContainerEvaluationRollup, @@ -34,7 +34,7 @@ function task(id: string, childIds: string[] = []): TaskState { function evaluation(taskId: string, statuses: Array<"passed" | "failed" | "errored" | "skipped">): TaskEvaluationState { return { id: `evaluation-${taskId}`, - runId: "run-1", + sampleId: "run-1", taskId, evaluatorName: "rubric", aggregationRule: "weighted_sum", @@ -71,7 +71,7 @@ function evaluation(taskId: string, statuses: Array<"passed" | "failed" | "error }; } -function state(evaluationsByTask: Map): WorkflowRunState { +function state(evaluationsByTask: Map): SampleWorkspaceState { return { id: "run-1", definitionId: "experiment-1", diff --git a/ergon-dashboard/src/features/evaluation/selectors.ts b/ergon-dashboard/src/features/evaluation/selectors.ts index ffcfe530d..691be2c15 100644 --- a/ergon-dashboard/src/features/evaluation/selectors.ts +++ b/ergon-dashboard/src/features/evaluation/selectors.ts @@ -1,5 +1,5 @@ -import type { TaskEvaluationState, WorkflowRunState } from "@/lib/types"; -import { formatScore } from "@/lib/run-state/formatters"; +import type { TaskEvaluationState, SampleWorkspaceState } from "@/lib/types"; +import { formatScore } from "@/lib/sample-state/formatters"; import type { EvalCriterionStatus, EvalRollupStatus, EvaluationRollup } from "./contracts"; function criterionStatusToRollupStatus(status: EvalCriterionStatus): EvalRollupStatus { @@ -160,7 +160,7 @@ export function evaluationToViewModel( } export function buildContainerEvaluationRollup( - state: WorkflowRunState, + state: SampleWorkspaceState, taskId: string, ): EvaluationRollup | null { const task = state.tasks.get(taskId); @@ -196,6 +196,6 @@ export function buildContainerEvaluationRollup( }; } -export function isEvaluationBearingTask(state: WorkflowRunState, taskId: string): boolean { +export function isEvaluationBearingTask(state: SampleWorkspaceState, taskId: string): boolean { return buildContainerEvaluationRollup(state, taskId) !== null; } diff --git a/ergon-dashboard/src/features/graph/contracts/graphMutations.test.ts b/ergon-dashboard/src/features/graph/contracts/graphMutations.test.ts index 7a8e33d8a..4cea5429a 100644 --- a/ergon-dashboard/src/features/graph/contracts/graphMutations.test.ts +++ b/ergon-dashboard/src/features/graph/contracts/graphMutations.test.ts @@ -10,12 +10,12 @@ import assert from "node:assert/strict"; import test from "node:test"; import { EdgeAddedValueSchema, MutationTypeSchema } from "./graphMutations"; import { applyGraphMutation, createReplayInitialState, replayToSequence } from "../state/graphMutationReducer"; -import type { WorkflowRunState } from "@/lib/types"; +import type { SampleWorkspaceState } from "@/lib/types"; import { TaskStatus } from "@/lib/types"; import type { DashboardGraphMutationData } from "@/lib/contracts/events"; import type { GraphMutationDto } from "./graphMutations"; -function emptyState(): WorkflowRunState { +function emptyState(): SampleWorkspaceState { return { id: "run-test", definitionId: "00000000-0000-0000-0000-000000000000", @@ -98,7 +98,7 @@ function syntheticMutation( return { id: "77777777-7777-4777-8777-777777777777", - run_id: "00000000-0000-0000-0000-000000000000", + sample_id: "00000000-0000-0000-0000-000000000000", sequence: 1, mutation_type: mutationType as DashboardGraphMutationData["mutation_type"], target_type: "node", @@ -151,7 +151,7 @@ test("edge.added accepts backend source_task_id and target_task_id payloads", () const next = applyGraphMutation(state, { id: "77777777-7777-4777-8777-777777777778", - run_id: "00000000-0000-0000-0000-000000000000", + sample_id: "00000000-0000-0000-0000-000000000000", sequence: 3, mutation_type: "edge.added", target_type: "edge", @@ -253,7 +253,7 @@ test("replay base preserves snapshot hierarchy while dependency edges remain dep graphNodeAdded(2, "33333333-3333-4333-8333-333333333333", "dependent"), { id: "44444444-4444-4444-8444-444444444444", - run_id: "00000000-0000-0000-0000-000000000000", + sample_id: "00000000-0000-0000-0000-000000000000", sequence: 3, mutation_type: "edge.added", target_type: "edge", @@ -350,7 +350,7 @@ test("replay base does not leak future dependency edges or node field changes", graphNodeAdded(2, "33333333-3333-4333-8333-333333333333", "target"), { id: "66666666-6666-4666-8666-666666666666", - run_id: "00000000-0000-0000-0000-000000000000", + sample_id: "00000000-0000-0000-0000-000000000000", sequence: 3, mutation_type: "node.field_changed", target_type: "node", @@ -363,7 +363,7 @@ test("replay base does not leak future dependency edges or node field changes", }, { id: "77777777-7777-4777-8777-777777777777", - run_id: "00000000-0000-0000-0000-000000000000", + sample_id: "00000000-0000-0000-0000-000000000000", sequence: 4, mutation_type: "edge.added", target_type: "edge", @@ -439,7 +439,7 @@ test("dependency edges between root-level tasks do not become containment", () = graphNodeAdded(1, "33333333-3333-4333-8333-333333333333", "target"), { id: "88888888-8888-4888-8888-888888888888", - run_id: "00000000-0000-0000-0000-000000000000", + sample_id: "00000000-0000-0000-0000-000000000000", sequence: 2, mutation_type: "edge.added", target_type: "edge", @@ -474,7 +474,7 @@ function graphNodeAdded( ): GraphMutationDto { return { id: `55555555-5555-4555-8555-55555555555${sequence}`, - run_id: "00000000-0000-0000-0000-000000000000", + sample_id: "00000000-0000-0000-0000-000000000000", sequence, mutation_type: "node.added", target_type: "node", diff --git a/ergon-dashboard/src/features/graph/contracts/graphMutations.ts b/ergon-dashboard/src/features/graph/contracts/graphMutations.ts index e4d3b7de3..6f6494d68 100644 --- a/ergon-dashboard/src/features/graph/contracts/graphMutations.ts +++ b/ergon-dashboard/src/features/graph/contracts/graphMutations.ts @@ -71,7 +71,7 @@ export type AnnotationValue = z.infer; export const GraphMutationDtoSchema = z.object({ id: z.string().uuid(), - run_id: z.string().uuid(), + sample_id: z.string().uuid(), sequence: z.number().int().nonnegative(), mutation_type: MutationTypeSchema, target_type: GraphTargetTypeSchema, diff --git a/ergon-dashboard/src/features/graph/hooks/useGraphMutations.ts b/ergon-dashboard/src/features/graph/hooks/useGraphMutations.ts index 316b2cba3..de4b0b43f 100644 --- a/ergon-dashboard/src/features/graph/hooks/useGraphMutations.ts +++ b/ergon-dashboard/src/features/graph/hooks/useGraphMutations.ts @@ -1,12 +1,12 @@ import { useRef, useCallback } from "react"; import type { DashboardGraphMutationData } from "@/lib/contracts/events"; -import type { WorkflowRunState } from "@/lib/types"; +import type { SampleWorkspaceState } from "@/lib/types"; import { applyGraphMutation } from "@/features/graph/state/graphMutationReducer"; const DEBOUNCE_MS = 200; export function useGraphMutations( - setRunState: React.Dispatch>, + setRunState: React.Dispatch>, ) { const buffer = useRef([]); const timer = useRef | null>(null); diff --git a/ergon-dashboard/src/features/graph/layout/goldenLayout.test.ts b/ergon-dashboard/src/features/graph/layout/goldenLayout.test.ts index ad3c11537..5b0a5a280 100644 --- a/ergon-dashboard/src/features/graph/layout/goldenLayout.test.ts +++ b/ergon-dashboard/src/features/graph/layout/goldenLayout.test.ts @@ -2,10 +2,10 @@ import assert from "node:assert/strict"; import test from "node:test"; import type { Node } from "@xyflow/react"; -import fixture from "../../../../tests/fixtures/mas-runs/concurrent-mas-run.json"; +import fixture from "../../../../tests/fixtures/mas-samples/concurrent-mas-run.json"; import { parseGraphMutationDtoArray } from "@/features/graph/contracts/graphMutations"; import { createReplayInitialState, replayToSequence } from "@/features/graph/state/graphMutationReducer"; -import { deserializeRunState } from "@/lib/runState"; +import { deserializeRunState } from "@/lib/sampleState"; import { calculateExpandedContainers, computeHierarchicalLayout } from "./hierarchicalLayout"; import { NODE_VARIANTS, getNodeVariant } from "./layoutTypes"; diff --git a/ergon-dashboard/src/features/graph/state/graphMutationReducer.ts b/ergon-dashboard/src/features/graph/state/graphMutationReducer.ts index f08e92804..f471ecb85 100644 --- a/ergon-dashboard/src/features/graph/state/graphMutationReducer.ts +++ b/ergon-dashboard/src/features/graph/state/graphMutationReducer.ts @@ -4,7 +4,7 @@ import type { TaskState, TaskTransitionRecord, UnhandledMutationRecord, - WorkflowRunState, + SampleWorkspaceState, } from "@/lib/types"; import { TaskStatus } from "@/lib/types"; import type { DashboardGraphMutationData } from "@/lib/contracts/events"; @@ -25,7 +25,7 @@ import { NodeFieldChangedValueSchema, NodeStatusChangedValueSchema, } from "@/features/graph/contracts/graphMutations"; -import { inferTrigger } from "@/lib/runEvents"; +import { inferTrigger } from "@/lib/sampleEvents"; const TERMINAL_STATUSES: Set = new Set([ "completed", @@ -60,23 +60,23 @@ function ctxFromMutation(mutation: DashboardGraphMutationData): MutationContext }; } -function ensureAnnotations(state: WorkflowRunState): Map { +function ensureAnnotations(state: SampleWorkspaceState): Map { if (!state.annotationsByTarget) state.annotationsByTarget = new Map(); return state.annotationsByTarget; } -function ensureEdges(state: WorkflowRunState): Map { +function ensureEdges(state: SampleWorkspaceState): Map { if (!state.edges) state.edges = new Map(); return state.edges; } -function ensureUnhandled(state: WorkflowRunState): UnhandledMutationRecord[] { +function ensureUnhandled(state: SampleWorkspaceState): UnhandledMutationRecord[] { if (!state.unhandledMutations) state.unhandledMutations = []; return state.unhandledMutations; } function recordUnhandled( - state: WorkflowRunState, + state: SampleWorkspaceState, mutation: DashboardGraphMutationData, note: string, ): void { @@ -97,7 +97,7 @@ function edgeId(sourceId: string, targetId: string): string { } /** - * Apply a single graph mutation to a WorkflowRunState. + * Apply a single graph mutation to a SampleWorkspaceState. * * Returns a new object reference (shallow copy) for React state updates. * Exhaustive on mutation_type — adding a new variant without handling @@ -107,10 +107,10 @@ function edgeId(sourceId: string, targetId: string): string { * (b) is recorded in `unhandledMutations`. No mutation is ever silently dropped. */ export function applyGraphMutation( - state: WorkflowRunState, + state: SampleWorkspaceState, mutation: DashboardGraphMutationData, -): WorkflowRunState { - const next: WorkflowRunState = { ...state, tasks: new Map(state.tasks) }; +): SampleWorkspaceState { + const next: SampleWorkspaceState = { ...state, tasks: new Map(state.tasks) }; if (state.edges) next.edges = new Map(state.edges); if (state.annotationsByTarget) next.annotationsByTarget = new Map(state.annotationsByTarget); @@ -193,10 +193,10 @@ export function applyGraphMutation( } function applyNodeAdded( - state: WorkflowRunState, + state: SampleWorkspaceState, nodeId: string, value: NodeAddedValue, -): WorkflowRunState { +): SampleWorkspaceState { if (state.tasks.has(nodeId)) return state; const task: TaskState = { @@ -224,11 +224,11 @@ function applyNodeAdded( } function applyNodeStatusChange( - state: WorkflowRunState, + state: SampleWorkspaceState, nodeId: string, value: NodeStatusChangedValue, ctx: MutationContext, -): WorkflowRunState { +): SampleWorkspaceState { const task = state.tasks.get(nodeId); if (!task) return state; @@ -264,10 +264,10 @@ function applyNodeStatusChange( } function applyNodeFieldChange( - state: WorkflowRunState, + state: SampleWorkspaceState, nodeId: string, value: NodeFieldChangedValue, -): WorkflowRunState { +): SampleWorkspaceState { const task = state.tasks.get(nodeId); if (!task) return state; @@ -286,10 +286,10 @@ function applyNodeFieldChange( } function applyEdgeAdded( - state: WorkflowRunState, + state: SampleWorkspaceState, value: EdgeAddedValue, ctx: MutationContext, -): WorkflowRunState { +): SampleWorkspaceState { const source = state.tasks.get(value.source_task_id); const target = state.tasks.get(value.target_task_id); @@ -336,10 +336,10 @@ function applyEdgeAdded( } function applyEdgeRemoved( - state: WorkflowRunState, + state: SampleWorkspaceState, targetEdgeId: string, _ctx: MutationContext, -): WorkflowRunState { +): SampleWorkspaceState { const edges = ensureEdges(state); // `target_id` for edge mutations is documented as the edge id — we look it up // either directly or by the source::target convention. @@ -381,11 +381,11 @@ function applyEdgeRemoved( } function applyEdgeStatusChanged( - state: WorkflowRunState, + state: SampleWorkspaceState, targetEdgeId: string, value: EdgeStatusChangedValue, ctx: MutationContext, -): WorkflowRunState { +): SampleWorkspaceState { const edges = ensureEdges(state); const existing = edges.get(targetEdgeId); edges.set(targetEdgeId, { @@ -399,11 +399,11 @@ function applyEdgeStatusChanged( } function applyAnnotationSet( - state: WorkflowRunState, + state: SampleWorkspaceState, targetId: string, value: AnnotationValue, ctx: MutationContext, -): WorkflowRunState { +): SampleWorkspaceState { const annotations = ensureAnnotations(state); const existing = annotations.get(targetId) ?? []; const next = existing.filter((a) => a.namespace !== value.namespace); @@ -418,11 +418,11 @@ function applyAnnotationSet( } function applyAnnotationDeleted( - state: WorkflowRunState, + state: SampleWorkspaceState, targetId: string, value: AnnotationValue, ctx: MutationContext, -): WorkflowRunState { +): SampleWorkspaceState { const annotations = ensureAnnotations(state); const existing = annotations.get(targetId); if (!existing) return state; @@ -435,7 +435,7 @@ function applyAnnotationDeleted( return state; } -function recalculateMetrics(state: WorkflowRunState): void { +function recalculateMetrics(state: SampleWorkspaceState): void { let completed = 0, running = 0, failed = 0; @@ -501,10 +501,10 @@ function countStatus( * existed at `upToSequence`, then lets status/annotation mutations replay on top. */ export function createReplayInitialState( - runState: WorkflowRunState, + runState: SampleWorkspaceState, mutations: GraphMutationDto[], upToSequence: number, -): WorkflowRunState { +): SampleWorkspaceState { const includedNodeIds = nodeIdsAddedAtOrBefore(mutations, upToSequence); const initialNodeValues = initialNodeValueById(mutations, upToSequence); const tasks = new Map(); @@ -563,11 +563,11 @@ export function createReplayInitialState( export function replayToSequence( mutations: GraphMutationDto[], upToSequence: number, - initialState: WorkflowRunState, - snapshotCache?: Map, -): WorkflowRunState { + initialState: SampleWorkspaceState, + snapshotCache?: Map, +): SampleWorkspaceState { let startSeq = -1; - let state: WorkflowRunState = { + let state: SampleWorkspaceState = { ...initialState, tasks: new Map(initialState.tasks), }; diff --git a/ergon-dashboard/src/generated/events/DashboardContextEventEvent.ts b/ergon-dashboard/src/generated/events/DashboardContextEventEvent.ts index 7d9dddd22..81ed9c347 100644 --- a/ergon-dashboard/src/generated/events/DashboardContextEventEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardContextEventEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardContextEventEventSchema = z.object({ "id": z.string().uuid().describe("RunContextEvent.id used by the frontend as a stable deduplication key."), "run_id": z.string().uuid(), "task_execution_id": z.string().uuid(), "task_id": z.string().uuid().describe("Graph task id resolved from the task execution by the dashboard emitter at event emission time."), "worker_binding_key": z.string(), "sequence": z.number().int(), "event_type": z.enum(["system_prompt","user_message","assistant_text","tool_call","tool_result","thinking"]), "payload": z.any().describe("Typed context event payload serialized with model_dump(mode='json') before being sent through Inngest."), "created_at": z.string().datetime({ offset: true }), "started_at": z.union([z.string().datetime({ offset: true }), z.null()]), "completed_at": z.union([z.string().datetime({ offset: true }), z.null()]) }).catchall(z.any()) +export const DashboardContextEventEventSchema = z.object({ "id": z.string().uuid().describe("SampleContextEvent.id used by the frontend as a stable deduplication key."), "sample_id": z.string().uuid(), "task_execution_id": z.string().uuid(), "task_id": z.string().uuid().describe("Graph task id resolved from the task execution by the dashboard emitter at event emission time."), "worker_binding_key": z.string(), "sequence": z.number().int(), "event_type": z.enum(["system_prompt","user_message","assistant_text","tool_call","tool_result","thinking"]), "payload": z.any().describe("Typed context event payload serialized with model_dump(mode='json') before being sent through Inngest."), "created_at": z.string().datetime({ offset: true }), "started_at": z.union([z.string().datetime({ offset: true }), z.null()]), "completed_at": z.union([z.string().datetime({ offset: true }), z.null()]) }).catchall(z.any()) diff --git a/ergon-dashboard/src/generated/events/DashboardResourcePublishedEvent.ts b/ergon-dashboard/src/generated/events/DashboardResourcePublishedEvent.ts index d5f0d2213..c21c8ce5d 100644 --- a/ergon-dashboard/src/generated/events/DashboardResourcePublishedEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardResourcePublishedEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardResourcePublishedEventSchema = z.object({ "run_id": z.string().uuid(), "task_id": z.string().uuid(), "task_execution_id": z.string().uuid(), "resource_id": z.string().uuid(), "resource_name": z.string(), "mime_type": z.string(), "size_bytes": z.number().int(), "file_path": z.string(), "timestamp": z.string().datetime({ offset: true }) }).catchall(z.any()) +export const DashboardResourcePublishedEventSchema = z.object({ "sample_id": z.string().uuid(), "task_id": z.string().uuid(), "task_execution_id": z.string().uuid(), "resource_id": z.string().uuid(), "resource_name": z.string(), "mime_type": z.string(), "size_bytes": z.number().int(), "file_path": z.string(), "timestamp": z.string().datetime({ offset: true }) }).catchall(z.any()) diff --git a/ergon-dashboard/src/generated/events/DashboardSandboxCommandEvent.ts b/ergon-dashboard/src/generated/events/DashboardSandboxCommandEvent.ts index 72c8edc5a..6bcc91338 100644 --- a/ergon-dashboard/src/generated/events/DashboardSandboxCommandEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardSandboxCommandEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardSandboxCommandEventSchema = z.object({ "run_id": z.string().uuid(), "task_id": z.string().uuid(), "sandbox_id": z.string(), "command": z.string(), "stdout": z.union([z.string(), z.null()]).default(null), "stderr": z.union([z.string(), z.null()]).default(null), "exit_code": z.union([z.number().int(), z.null()]).default(null), "duration_ms": z.union([z.number().int(), z.null()]).default(null), "timestamp": z.string().datetime({ offset: true }) }).catchall(z.any()) +export const DashboardSandboxCommandEventSchema = z.object({ "sample_id": z.string().uuid(), "task_id": z.string().uuid(), "sandbox_id": z.string(), "command": z.string(), "stdout": z.union([z.string(), z.null()]).default(null), "stderr": z.union([z.string(), z.null()]).default(null), "exit_code": z.union([z.number().int(), z.null()]).default(null), "duration_ms": z.union([z.number().int(), z.null()]).default(null), "timestamp": z.string().datetime({ offset: true }) }).catchall(z.any()) diff --git a/ergon-dashboard/src/generated/events/DashboardSandboxCreatedEvent.ts b/ergon-dashboard/src/generated/events/DashboardSandboxCreatedEvent.ts index a475f800e..2388389cc 100644 --- a/ergon-dashboard/src/generated/events/DashboardSandboxCreatedEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardSandboxCreatedEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardSandboxCreatedEventSchema = z.object({ "run_id": z.string().uuid(), "task_id": z.string().uuid(), "sandbox_id": z.string(), "template": z.union([z.string(), z.null()]).default(null), "timeout_minutes": z.number().int(), "timestamp": z.string().datetime({ offset: true }) }).catchall(z.any()) +export const DashboardSandboxCreatedEventSchema = z.object({ "sample_id": z.string().uuid(), "task_id": z.string().uuid(), "sandbox_id": z.string(), "template": z.union([z.string(), z.null()]).default(null), "timeout_minutes": z.number().int(), "timestamp": z.string().datetime({ offset: true }) }).catchall(z.any()) diff --git a/ergon-dashboard/src/generated/events/DashboardTaskEvaluationUpdatedEvent.ts b/ergon-dashboard/src/generated/events/DashboardTaskEvaluationUpdatedEvent.ts index ccc5956ef..71196392f 100644 --- a/ergon-dashboard/src/generated/events/DashboardTaskEvaluationUpdatedEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardTaskEvaluationUpdatedEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardTaskEvaluationUpdatedEventSchema = z.object({ "run_id": z.string().uuid(), "task_id": z.string().uuid(), "evaluation": z.any() }).catchall(z.any()).describe("Embeds the full RunTaskEvaluationDto as ``evaluation``.") +export const DashboardTaskEvaluationUpdatedEventSchema = z.object({ "sample_id": z.string().uuid(), "task_id": z.string().uuid(), "evaluation": z.any() }).catchall(z.any()).describe("Embeds the full SampleTaskEvaluationDto as ``evaluation``.") diff --git a/ergon-dashboard/src/generated/events/DashboardTaskStatusChangedEvent.ts b/ergon-dashboard/src/generated/events/DashboardTaskStatusChangedEvent.ts index 2fa0feb0d..650e38667 100644 --- a/ergon-dashboard/src/generated/events/DashboardTaskStatusChangedEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardTaskStatusChangedEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardTaskStatusChangedEventSchema = z.object({ "run_id": z.string().uuid(), "task_id": z.string().uuid(), "task_name": z.string(), "parent_task_id": z.union([z.string().uuid(), z.null()]).default(null), "old_status": z.union([z.enum(["pending","ready","running","completed","failed","cancelled","blocked"]), z.null()]).default(null), "new_status": z.enum(["pending","ready","running","completed","failed","cancelled","blocked"]), "triggered_by": z.union([z.string(), z.null()]).default(null), "timestamp": z.string().datetime({ offset: true }), "assigned_worker_id": z.union([z.string().uuid(), z.null()]).default(null), "assigned_worker_slug": z.union([z.string(), z.null()]).default(null) }).catchall(z.any()) +export const DashboardTaskStatusChangedEventSchema = z.object({ "sample_id": z.string().uuid(), "task_id": z.string().uuid(), "task_name": z.string(), "parent_task_id": z.union([z.string().uuid(), z.null()]).default(null), "old_status": z.union([z.enum(["pending","ready","running","completed","failed","cancelled","blocked"]), z.null()]).default(null), "new_status": z.enum(["pending","ready","running","completed","failed","cancelled","blocked"]), "triggered_by": z.union([z.string(), z.null()]).default(null), "timestamp": z.string().datetime({ offset: true }), "assigned_worker_id": z.union([z.string().uuid(), z.null()]).default(null), "assigned_worker_slug": z.union([z.string(), z.null()]).default(null) }).catchall(z.any()) diff --git a/ergon-dashboard/src/generated/events/DashboardThreadMessageCreatedEvent.ts b/ergon-dashboard/src/generated/events/DashboardThreadMessageCreatedEvent.ts index 150acb14b..fdca22b29 100644 --- a/ergon-dashboard/src/generated/events/DashboardThreadMessageCreatedEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardThreadMessageCreatedEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardThreadMessageCreatedEventSchema = z.object({ "run_id": z.string().uuid(), "thread": z.any(), "message": z.any() }).catchall(z.any()).describe("Embeds full RunCommunicationThreadDto + RunCommunicationMessageDto.") +export const DashboardThreadMessageCreatedEventSchema = z.object({ "sample_id": z.string().uuid(), "thread": z.any(), "message": z.any() }).catchall(z.any()).describe("Embeds full RunCommunicationThreadDto + RunCommunicationMessageDto.") diff --git a/ergon-dashboard/src/generated/events/DashboardWorkflowCompletedEvent.ts b/ergon-dashboard/src/generated/events/DashboardWorkflowCompletedEvent.ts index 1cb42fb8c..5314bb122 100644 --- a/ergon-dashboard/src/generated/events/DashboardWorkflowCompletedEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardWorkflowCompletedEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardWorkflowCompletedEventSchema = z.object({ "run_id": z.string().uuid(), "status": z.string(), "completed_at": z.string().datetime({ offset: true }), "duration_seconds": z.number(), "final_score": z.union([z.number(), z.null()]).default(null), "error": z.union([z.string(), z.null()]).default(null) }).catchall(z.any()) +export const DashboardWorkflowCompletedEventSchema = z.object({ "sample_id": z.string().uuid(), "status": z.string(), "completed_at": z.string().datetime({ offset: true }), "duration_seconds": z.number(), "final_score": z.union([z.number(), z.null()]).default(null), "error": z.union([z.string(), z.null()]).default(null) }).catchall(z.any()) diff --git a/ergon-dashboard/src/generated/events/DashboardWorkflowStartedEvent.ts b/ergon-dashboard/src/generated/events/DashboardWorkflowStartedEvent.ts index 63f03ceae..307e0d069 100644 --- a/ergon-dashboard/src/generated/events/DashboardWorkflowStartedEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardWorkflowStartedEvent.ts @@ -1,3 +1,3 @@ import { z } from "zod" -export const DashboardWorkflowStartedEventSchema = z.object({ "run_id": z.string().uuid(), "definition_id": z.string().uuid(), "workflow_name": z.string(), "snapshot": z.any(), "started_at": z.string().datetime({ offset: true }), "total_tasks": z.number().int(), "total_leaf_tasks": z.number().int() }).catchall(z.any()) +export const DashboardWorkflowStartedEventSchema = z.object({ "sample_id": z.string().uuid(), "definition_id": z.string().uuid(), "workflow_name": z.string(), "snapshot": z.any(), "started_at": z.string().datetime({ offset: true }), "total_tasks": z.number().int(), "total_leaf_tasks": z.number().int() }).catchall(z.any()) diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardContextEventEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardContextEventEvent.schema.json index c1d10d1bc..28086a9da 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardContextEventEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardContextEventEvent.schema.json @@ -500,14 +500,14 @@ "additionalProperties": true, "properties": { "id": { - "description": "RunContextEvent.id used by the frontend as a stable deduplication key.", + "description": "SampleContextEvent.id used by the frontend as a stable deduplication key.", "format": "uuid", "title": "Id", "type": "string" }, - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "task_execution_id": { @@ -577,7 +577,7 @@ }, "required": [ "id", - "run_id", + "sample_id", "task_execution_id", "task_id", "worker_binding_key", diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardGraphMutationEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardGraphMutationEvent.schema.json index a593075d9..000ff5efc 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardGraphMutationEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardGraphMutationEvent.schema.json @@ -141,9 +141,9 @@ "title": "Id", "type": "string" }, - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "sequence": { @@ -301,7 +301,7 @@ }, "required": [ "id", - "run_id", + "sample_id", "sequence", "mutation_type", "target_type", diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardResourcePublishedEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardResourcePublishedEvent.schema.json index c0832ea92..731059d6a 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardResourcePublishedEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardResourcePublishedEvent.schema.json @@ -1,9 +1,9 @@ { "additionalProperties": true, "properties": { - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "task_id": { @@ -44,7 +44,7 @@ } }, "required": [ - "run_id", + "sample_id", "task_id", "task_execution_id", "resource_id", diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardSandboxCommandEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardSandboxCommandEvent.schema.json index 3a8b55e1d..3f0d192fe 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardSandboxCommandEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardSandboxCommandEvent.schema.json @@ -1,9 +1,9 @@ { "additionalProperties": true, "properties": { - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "task_id": { @@ -74,7 +74,7 @@ } }, "required": [ - "run_id", + "sample_id", "task_id", "sandbox_id", "command", diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardSandboxCreatedEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardSandboxCreatedEvent.schema.json index 32f685774..3d77ecd9c 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardSandboxCreatedEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardSandboxCreatedEvent.schema.json @@ -1,9 +1,9 @@ { "additionalProperties": true, "properties": { - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "task_id": { @@ -38,7 +38,7 @@ } }, "required": [ - "run_id", + "sample_id", "task_id", "sandbox_id", "timeout_minutes", diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardTaskEvaluationUpdatedEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardTaskEvaluationUpdatedEvent.schema.json index d78ada70e..020bb9c4b 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardTaskEvaluationUpdatedEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardTaskEvaluationUpdatedEvent.schema.json @@ -173,15 +173,15 @@ "title": "RunEvaluationCriterionDto", "type": "object" }, - "RunTaskEvaluationDto": { + "SampleTaskEvaluationDto": { "additionalProperties": false, "properties": { "id": { "title": "Id", "type": "string" }, - "runId": { - "title": "Runid", + "sampleId": { + "title": "Sampleid", "type": "string" }, "taskId": { @@ -251,7 +251,7 @@ }, "required": [ "id", - "runId", + "sampleId", "evaluatorName", "aggregationRule", "totalScore", @@ -261,16 +261,16 @@ "stagesPassed", "createdAt" ], - "title": "RunTaskEvaluationDto", + "title": "SampleTaskEvaluationDto", "type": "object" } }, "additionalProperties": true, - "description": "Embeds the full RunTaskEvaluationDto as ``evaluation``.", + "description": "Embeds the full SampleTaskEvaluationDto as ``evaluation``.", "properties": { - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "task_id": { @@ -279,11 +279,11 @@ "type": "string" }, "evaluation": { - "$ref": "#/$defs/RunTaskEvaluationDto" + "$ref": "#/$defs/SampleTaskEvaluationDto" } }, "required": [ - "run_id", + "sample_id", "task_id", "evaluation" ], diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardTaskStatusChangedEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardTaskStatusChangedEvent.schema.json index 1e22360e0..703bf6b78 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardTaskStatusChangedEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardTaskStatusChangedEvent.schema.json @@ -1,9 +1,9 @@ { "additionalProperties": true, "properties": { - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "task_id": { @@ -106,7 +106,7 @@ } }, "required": [ - "run_id", + "sample_id", "task_id", "task_name", "new_status", diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardThreadMessageCreatedEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardThreadMessageCreatedEvent.schema.json index 0717b7c11..be0069507 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardThreadMessageCreatedEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardThreadMessageCreatedEvent.schema.json @@ -15,8 +15,8 @@ "title": "Threadtopic", "type": "string" }, - "runId": { - "title": "Runid", + "sampleId": { + "title": "Sampleid", "type": "string" }, "taskId": { @@ -69,7 +69,7 @@ "id", "threadId", "threadTopic", - "runId", + "sampleId", "fromAgentId", "toAgentId", "content", @@ -86,8 +86,8 @@ "title": "Id", "type": "string" }, - "runId": { - "title": "Runid", + "sampleId": { + "title": "Sampleid", "type": "string" }, "taskId": { @@ -146,7 +146,7 @@ }, "required": [ "id", - "runId", + "sampleId", "topic", "agentAId", "agentBId", @@ -160,9 +160,9 @@ "additionalProperties": true, "description": "Embeds full RunCommunicationThreadDto + RunCommunicationMessageDto.", "properties": { - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "thread": { @@ -173,7 +173,7 @@ } }, "required": [ - "run_id", + "sample_id", "thread", "message" ], diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowCompletedEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowCompletedEvent.schema.json index 7cf3c6454..d5aac7d37 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowCompletedEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowCompletedEvent.schema.json @@ -1,9 +1,9 @@ { "additionalProperties": true, "properties": { - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "status": { @@ -45,7 +45,7 @@ } }, "required": [ - "run_id", + "sample_id", "status", "completed_at", "duration_seconds" diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowStartedEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowStartedEvent.schema.json index 27ee288f3..9fbc011e3 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowStartedEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowStartedEvent.schema.json @@ -346,8 +346,8 @@ "title": "Threadtopic", "type": "string" }, - "runId": { - "title": "Runid", + "sampleId": { + "title": "Sampleid", "type": "string" }, "taskId": { @@ -400,7 +400,7 @@ "id", "threadId", "threadTopic", - "runId", + "sampleId", "fromAgentId", "toAgentId", "content", @@ -417,8 +417,8 @@ "title": "Id", "type": "string" }, - "runId": { - "title": "Runid", + "sampleId": { + "title": "Sampleid", "type": "string" }, "taskId": { @@ -477,7 +477,7 @@ }, "required": [ "id", - "runId", + "sampleId", "topic", "agentAId", "agentBId", @@ -487,98 +487,6 @@ "title": "RunCommunicationThreadDto", "type": "object" }, - "RunContextEventDto": { - "additionalProperties": false, - "properties": { - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "runId": { - "format": "uuid", - "title": "Runid", - "type": "string" - }, - "taskExecutionId": { - "format": "uuid", - "title": "Taskexecutionid", - "type": "string" - }, - "taskId": { - "format": "uuid", - "title": "Taskid", - "type": "string" - }, - "workerBindingKey": { - "title": "Workerbindingkey", - "type": "string" - }, - "sequence": { - "title": "Sequence", - "type": "integer" - }, - "eventType": { - "enum": [ - "system_prompt", - "user_message", - "assistant_text", - "tool_call", - "tool_result", - "thinking" - ], - "title": "Eventtype", - "type": "string" - }, - "payload": { - "$ref": "#/$defs/ContextPartChunkLog" - }, - "createdAt": { - "format": "date-time", - "title": "Createdat", - "type": "string" - }, - "startedAt": { - "anyOf": [ - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Startedat" - }, - "completedAt": { - "anyOf": [ - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Completedat" - } - }, - "required": [ - "id", - "runId", - "taskExecutionId", - "taskId", - "workerBindingKey", - "sequence", - "eventType", - "payload", - "createdAt" - ], - "title": "RunContextEventDto", - "type": "object" - }, "RunEvaluationCriterionDto": { "additionalProperties": false, "properties": { @@ -887,56 +795,6 @@ "title": "RunExecutionAttemptDto", "type": "object" }, - "RunResourceDto": { - "additionalProperties": false, - "properties": { - "id": { - "title": "Id", - "type": "string" - }, - "taskId": { - "title": "Taskid", - "type": "string" - }, - "taskExecutionId": { - "title": "Taskexecutionid", - "type": "string" - }, - "name": { - "title": "Name", - "type": "string" - }, - "mimeType": { - "title": "Mimetype", - "type": "string" - }, - "filePath": { - "title": "Filepath", - "type": "string" - }, - "sizeBytes": { - "title": "Sizebytes", - "type": "integer" - }, - "createdAt": { - "format": "date-time", - "title": "Createdat", - "type": "string" - } - }, - "required": [ - "id", - "taskId", - "taskExecutionId", - "name", - "mimeType", - "filePath", - "sizeBytes", - "createdAt" - ], - "title": "RunResourceDto", - "type": "object" - }, "RunSandboxCommandDto": { "additionalProperties": false, "properties": { @@ -1084,7 +942,149 @@ "title": "RunSandboxDto", "type": "object" }, - "RunSnapshotDto": { + "SampleContextEventDto": { + "additionalProperties": false, + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "taskExecutionId": { + "format": "uuid", + "title": "Taskexecutionid", + "type": "string" + }, + "taskId": { + "format": "uuid", + "title": "Taskid", + "type": "string" + }, + "workerBindingKey": { + "title": "Workerbindingkey", + "type": "string" + }, + "sequence": { + "title": "Sequence", + "type": "integer" + }, + "eventType": { + "enum": [ + "system_prompt", + "user_message", + "assistant_text", + "tool_call", + "tool_result", + "thinking" + ], + "title": "Eventtype", + "type": "string" + }, + "payload": { + "$ref": "#/$defs/ContextPartChunkLog" + }, + "createdAt": { + "format": "date-time", + "title": "Createdat", + "type": "string" + }, + "startedAt": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Startedat" + }, + "completedAt": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completedat" + } + }, + "required": [ + "id", + "sampleId", + "taskExecutionId", + "taskId", + "workerBindingKey", + "sequence", + "eventType", + "payload", + "createdAt" + ], + "title": "SampleContextEventDto", + "type": "object" + }, + "SampleResourceDto": { + "additionalProperties": false, + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "taskId": { + "title": "Taskid", + "type": "string" + }, + "taskExecutionId": { + "title": "Taskexecutionid", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "mimeType": { + "title": "Mimetype", + "type": "string" + }, + "filePath": { + "title": "Filepath", + "type": "string" + }, + "sizeBytes": { + "title": "Sizebytes", + "type": "integer" + }, + "createdAt": { + "format": "date-time", + "title": "Createdat", + "type": "string" + } + }, + "required": [ + "id", + "taskId", + "taskExecutionId", + "name", + "mimeType", + "filePath", + "sizeBytes", + "createdAt" + ], + "title": "SampleResourceDto", + "type": "object" + }, + "SampleSnapshotDto": { "additionalProperties": false, "properties": { "id": { @@ -1105,7 +1105,7 @@ }, "tasks": { "additionalProperties": { - "$ref": "#/$defs/RunTaskDto" + "$ref": "#/$defs/SampleTaskDto" }, "title": "Tasks", "type": "object" @@ -1118,7 +1118,7 @@ "resourcesByTask": { "additionalProperties": { "items": { - "$ref": "#/$defs/RunResourceDto" + "$ref": "#/$defs/SampleResourceDto" }, "type": "array" }, @@ -1137,7 +1137,7 @@ }, "evaluationsByTask": { "additionalProperties": { - "$ref": "#/$defs/RunTaskEvaluationDto" + "$ref": "#/$defs/SampleTaskEvaluationDto" }, "title": "Evaluationsbytask", "type": "object" @@ -1152,7 +1152,7 @@ "contextEventsByTask": { "additionalProperties": { "items": { - "$ref": "#/$defs/RunContextEventDto" + "$ref": "#/$defs/SampleContextEventDto" }, "type": "array" }, @@ -1249,7 +1249,7 @@ "metrics": { "anyOf": [ { - "$ref": "#/$defs/RunSnapshotMetricsDto" + "$ref": "#/$defs/SampleSnapshotMetricsDto" }, { "type": "null" @@ -1276,14 +1276,14 @@ "name", "status" ], - "title": "RunSnapshotDto", + "title": "SampleSnapshotDto", "type": "object" }, - "RunSnapshotMetricsDto": { + "SampleSnapshotMetricsDto": { "additionalProperties": false, "properties": { - "runId": { - "title": "Runid", + "sampleId": { + "title": "Sampleid", "type": "string" }, "status": { @@ -1350,15 +1350,15 @@ } }, "required": [ - "runId", + "sampleId", "status" ], - "title": "RunSnapshotMetricsDto", + "title": "SampleSnapshotMetricsDto", "type": "object" }, - "RunTaskDto": { + "SampleTaskDto": { "additionalProperties": false, - "description": "REST projection of RunGraphNode for run detail pages.\n\nThis is not the canonical graph schema; graph semantics live in\napplication/graph/models.py and application/runtime/status.py.", + "description": "REST projection of SampleGraphNode for run detail pages.\n\nThis is not the canonical graph schema; graph semantics live in\napplication/graph/models.py and application/runtime/status.py.", "properties": { "id": { "title": "Id", @@ -1469,18 +1469,18 @@ "isLeaf", "level" ], - "title": "RunTaskDto", + "title": "SampleTaskDto", "type": "object" }, - "RunTaskEvaluationDto": { + "SampleTaskEvaluationDto": { "additionalProperties": false, "properties": { "id": { "title": "Id", "type": "string" }, - "runId": { - "title": "Runid", + "sampleId": { + "title": "Sampleid", "type": "string" }, "taskId": { @@ -1550,7 +1550,7 @@ }, "required": [ "id", - "runId", + "sampleId", "evaluatorName", "aggregationRule", "totalScore", @@ -1560,7 +1560,7 @@ "stagesPassed", "createdAt" ], - "title": "RunTaskEvaluationDto", + "title": "SampleTaskEvaluationDto", "type": "object" }, "SystemPromptPart": { @@ -1731,9 +1731,9 @@ }, "additionalProperties": true, "properties": { - "run_id": { + "sample_id": { "format": "uuid", - "title": "Run Id", + "title": "Sample Id", "type": "string" }, "definition_id": { @@ -1746,7 +1746,7 @@ "type": "string" }, "snapshot": { - "$ref": "#/$defs/RunSnapshotDto" + "$ref": "#/$defs/SampleSnapshotDto" }, "started_at": { "format": "date-time", @@ -1763,7 +1763,7 @@ } }, "required": [ - "run_id", + "sample_id", "definition_id", "workflow_name", "snapshot", diff --git a/ergon-dashboard/src/generated/rest/contracts.ts b/ergon-dashboard/src/generated/rest/contracts.ts index ab8982af5..81662b05f 100644 --- a/ergon-dashboard/src/generated/rest/contracts.ts +++ b/ergon-dashboard/src/generated/rest/contracts.ts @@ -9,7 +9,7 @@ type JsonScalar = | Array; const status = z.union([z.string(), z.null()]).optional(); -const RunSummaryDto = z +const SampleSummaryDto = z .object({ id: z.string().uuid(), name: z.string(), @@ -53,7 +53,7 @@ const HTTPValidationError = z .object({ detail: z.array(ValidationError) }) .partial() .passthrough(); -const RunTaskDto = z.object({ +const SampleTaskDto = z.object({ id: z.string(), name: z.string(), description: z.string(), @@ -68,7 +68,7 @@ const RunTaskDto = z.object({ startedAt: z.union([z.string(), z.null()]).optional(), completedAt: z.union([z.string(), z.null()]).optional(), }); -const RunResourceDto = z.object({ +const SampleResourceDto = z.object({ id: z.string(), taskId: z.string(), taskExecutionId: z.string(), @@ -121,9 +121,9 @@ const RunEvaluationCriterionDto = z.object({ .optional(), error: z.union([z.object({}).partial().passthrough(), z.null()]).optional(), }); -const RunTaskEvaluationDto = z.object({ +const SampleTaskEvaluationDto = z.object({ id: z.string(), - runId: z.string(), + sampleId: z.string(), taskId: z.union([z.string(), z.null()]).optional(), evaluatorName: z.string(), aggregationRule: z.string(), @@ -248,9 +248,9 @@ const ContextPartChunkLog = z policy_version: z.union([z.string(), z.null()]).optional(), }) .passthrough(); -const RunContextEventDto = z.object({ +const SampleContextEventDto = z.object({ id: z.string().uuid(), - runId: z.string().uuid(), + sampleId: z.string().uuid(), taskExecutionId: z.string().uuid(), taskId: z.string().uuid(), workerBindingKey: z.string(), @@ -272,7 +272,7 @@ const RunCommunicationMessageDto = z.object({ id: z.string(), threadId: z.string(), threadTopic: z.string(), - runId: z.string(), + sampleId: z.string(), taskId: z.union([z.string(), z.null()]).optional(), taskExecutionId: z.union([z.string(), z.null()]).optional(), fromAgentId: z.string(), @@ -283,7 +283,7 @@ const RunCommunicationMessageDto = z.object({ }); const RunCommunicationThreadDto = z.object({ id: z.string(), - runId: z.string(), + sampleId: z.string(), taskId: z.union([z.string(), z.null()]).optional(), topic: z.string(), summary: z.union([z.string(), z.null()]).optional(), @@ -293,8 +293,8 @@ const RunCommunicationThreadDto = z.object({ updatedAt: z.string().datetime({ offset: true }), messages: z.array(RunCommunicationMessageDto).optional(), }); -const RunSnapshotMetricsDto = z.object({ - runId: z.string(), +const SampleSnapshotMetricsDto = z.object({ + sampleId: z.string(), status: z.string(), durationMs: z.union([z.number(), z.null()]).optional(), totalTasks: z.number().int().optional().default(0), @@ -304,18 +304,18 @@ const RunSnapshotMetricsDto = z.object({ totalCostUsd: z.union([z.number(), z.null()]).optional(), costObserved: z.boolean().optional().default(false), }); -const RunSnapshotDto = z.object({ +const SampleSnapshotDto = z.object({ id: z.string(), definitionId: z.string(), name: z.string(), status: z.string(), - tasks: z.record(z.string(), RunTaskDto).optional(), + tasks: z.record(z.string(), SampleTaskDto).optional(), rootTaskId: z.string().optional().default(""), - resourcesByTask: z.record(z.string(), z.array(RunResourceDto)).optional(), + resourcesByTask: z.record(z.string(), z.array(SampleResourceDto)).optional(), executionsByTask: z.record(z.string(), z.array(RunExecutionAttemptDto)).optional(), - evaluationsByTask: z.record(z.string(), RunTaskEvaluationDto).optional(), + evaluationsByTask: z.record(z.string(), SampleTaskEvaluationDto).optional(), sandboxesByTask: z.record(z.string(), RunSandboxDto).optional(), - contextEventsByTask: z.record(z.string(), z.array(RunContextEventDto)).optional(), + contextEventsByTask: z.record(z.string(), z.array(SampleContextEventDto)).optional(), threads: z.array(RunCommunicationThreadDto).optional(), startedAt: z.union([z.string(), z.null()]).optional(), completedAt: z.union([z.string(), z.null()]).optional(), @@ -327,7 +327,7 @@ const RunSnapshotDto = z.object({ runningTasks: z.number().int().optional().default(0), cancelledTasks: z.number().int().optional().default(0), finalScore: z.union([z.number(), z.null()]).optional(), - metrics: z.union([RunSnapshotMetricsDto, z.null()]).optional(), + metrics: z.union([SampleSnapshotMetricsDto, z.null()]).optional(), error: z.union([z.string(), z.null()]).optional(), }); const NodeAddedMutation = z @@ -402,7 +402,7 @@ const AnnotationDeletedMutation = z const GraphMutationRecordDto = z .object({ id: z.string().uuid(), - run_id: z.string().uuid(), + sample_id: z.string().uuid(), sequence: z.number().int(), mutation_type: z.enum([ "node.added", @@ -485,7 +485,7 @@ const ExperimentSummaryDto = z .passthrough(); const ExperimentRunMetricsDto = z .object({ - run_id: z.string().uuid(), + sample_id: z.string().uuid(), run_name: z.union([z.string(), z.null()]).optional(), status: z.string(), sample_label: z.union([z.string(), z.null()]).optional(), @@ -506,7 +506,7 @@ const ExperimentRunMetricsDto = z .passthrough(); const ExperimentRunRowDto = z .object({ - run_id: z.string().uuid(), + sample_id: z.string().uuid(), definition_id: z.string().uuid(), benchmark_type: z.string(), instance_key: z.string(), @@ -567,7 +567,7 @@ const run_experiment_experiments__definition_id__run_post_Body = z.union([ const ExperimentRunResult = z .object({ definition_id: z.string().uuid(), - run_ids: z.array(z.string().uuid()), + sample_ids: z.array(z.string().uuid()), definition_ids: z.array(z.string().uuid()).optional(), }) .passthrough(); @@ -589,13 +589,13 @@ const RolloutStatus = z.enum([ const SubmitResponse = z .object({ batch_id: z.string().uuid(), - run_ids: z.array(z.string().uuid()), + sample_ids: z.array(z.string().uuid()), status: RolloutStatus.optional(), }) .passthrough(); const Trajectory = z .object({ - run_id: z.string().uuid(), + sample_id: z.string().uuid(), agent_id: z.string(), prompt_ids: z.array(z.number().int()), completion_ids: z.array(z.number().int()), @@ -606,7 +606,7 @@ const Trajectory = z }) .passthrough(); const EpisodeFailure = z - .object({ run_id: z.string().uuid(), error: z.string() }) + .object({ sample_id: z.string().uuid(), error: z.string() }) .passthrough(); const PollResponse = z .object({ @@ -658,7 +658,7 @@ const TestExecutionDto = z .passthrough(); const TestRunStateDto = z .object({ - run_id: z.string().uuid(), + sample_id: z.string().uuid(), status: z.string(), graph_nodes: z.array(TestGraphNodeDto), mutations: z.array(TestGraphMutationDto), @@ -672,7 +672,7 @@ const TestRunStateDto = z }) .passthrough(); const TestExperimentRunDto = z - .object({ run_id: z.string().uuid(), status: z.string() }) + .object({ sample_id: z.string().uuid(), status: z.string() }) .passthrough(); const SeedRunRequest = z .object({ @@ -701,19 +701,19 @@ const SubmitExperimentRunsRequest = z }) .passthrough(); const SubmitExperimentRunsResponse = z - .object({ run_ids: z.array(z.string().uuid()) }) + .object({ sample_ids: z.array(z.string().uuid()) }) .passthrough(); export const schemas = { status, - RunSummaryDto, + SampleSummaryDto, ValidationError, HTTPValidationError, - RunTaskDto, - RunResourceDto, + SampleTaskDto, + SampleResourceDto, RunExecutionAttemptDto, RunEvaluationCriterionDto, - RunTaskEvaluationDto, + SampleTaskEvaluationDto, RunSandboxCommandDto, RunSandboxDto, SystemPromptPart, @@ -728,11 +728,11 @@ export const schemas = { TokenLogprob, ProviderTokenUsage, ContextPartChunkLog, - RunContextEventDto, + SampleContextEventDto, RunCommunicationMessageDto, RunCommunicationThreadDto, - RunSnapshotMetricsDto, - RunSnapshotDto, + SampleSnapshotMetricsDto, + SampleSnapshotDto, NodeAddedMutation, NodeRemovedMutation, NodeStatusChangedMutation, diff --git a/ergon-dashboard/src/generated/rest/openapi.json b/ergon-dashboard/src/generated/rest/openapi.json index edf37ce1d..2ff6d8109 100644 --- a/ergon-dashboard/src/generated/rest/openapi.json +++ b/ergon-dashboard/src/generated/rest/openapi.json @@ -6,7 +6,7 @@ "version": "0.1.0" }, "paths": { - "/runs": { + "/samples": { "get": { "tags": [ "runs" @@ -93,7 +93,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/RunSummaryDto" + "$ref": "#/components/schemas/SampleSummaryDto" }, "title": "Response List Runs Runs Get" } @@ -113,17 +113,17 @@ } } }, - "/runs/{run_id}": { + "/samples/{sample_id}": { "get": { "tags": [ "runs" ], "summary": "Get Run", "description": "Get a persisted run-detail snapshot suitable for frontend hydration.", - "operationId": "get_run_runs__run_id__get", + "operationId": "get_run_runs__sample_id__get", "parameters": [ { - "name": "run_id", + "name": "sample_id", "in": "path", "required": true, "schema": { @@ -139,7 +139,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RunSnapshotDto" + "$ref": "#/components/schemas/SampleSnapshotDto" } } } @@ -157,17 +157,17 @@ } } }, - "/runs/{run_id}/mutations": { + "/samples/{sample_id}/mutations": { "get": { "tags": [ "runs" ], "summary": "Get Mutations", "description": "Return the append-only mutation log for a run, ordered by sequence.", - "operationId": "get_mutations_runs__run_id__mutations_get", + "operationId": "get_mutations_runs__sample_id__mutations_get", "parameters": [ { - "name": "run_id", + "name": "sample_id", "in": "path", "required": true, "schema": { @@ -205,17 +205,17 @@ } } }, - "/runs/{run_id}/resources/{resource_id}/content": { + "/samples/{sample_id}/resources/{resource_id}/content": { "get": { "tags": [ "runs" ], "summary": "Get Resource Content", - "description": "Stream the blob bytes for a RunResource.", - "operationId": "get_resource_content_runs__run_id__resources__resource_id__content_get", + "description": "Stream the blob bytes for a SampleResource.", + "operationId": "get_resource_content_runs__sample_id__resources__resource_id__content_get", "parameters": [ { - "name": "run_id", + "name": "sample_id", "in": "path", "required": true, "schema": { @@ -592,16 +592,16 @@ } } }, - "/api/__danger__/test-harness/read/run/{run_id}/state": { + "/api/__danger__/test-harness/read/samples/{sample_id}/state": { "get": { "tags": [ "danger-test-harness" ], "summary": "Read Run State", - "operationId": "read_run_state_api___danger___test_harness_read_run__run_id__state_get", + "operationId": "read_run_state_api___danger___test_harness_read_run__sample_id__state_get", "parameters": [ { - "name": "run_id", + "name": "sample_id", "in": "path", "required": true, "schema": { @@ -635,7 +635,7 @@ } } }, - "/api/__danger__/test-harness/read/experiment/{experiment}/runs": { + "/api/__danger__/test-harness/read/experiment/{experiment}/samples": { "get": { "tags": [ "danger-test-harness" @@ -682,7 +682,7 @@ } } }, - "/api/__danger__/test-harness/write/run/seed": { + "/api/__danger__/test-harness/write/samples/seed": { "post": { "tags": [ "danger-test-harness" @@ -765,7 +765,7 @@ "danger-test-harness" ], "summary": "Submit Experiment Runs", - "description": "Build + persist + dispatch N runs under one experiment tag.\n\nPer-slot flow persists the object-bound smoke ``Benchmark`` into the\nimmutable ``ExperimentDefinition`` tables. The v2 experiment grouping tag\nis written into definition metadata and copied to ``RunRecord.experiment``\nby launch. Slots submit sequentially \u2014 typical N \u2264 3, so the\nparallel-gather savings are negligible.", + "description": "Build + persist + dispatch N runs under one experiment tag.\n\nPer-slot flow persists the object-bound smoke ``Benchmark`` into the\nimmutable ``ExperimentDefinition`` tables. The v2 experiment grouping tag\nis written into definition metadata and copied to ``SampleRecord.experiment``\nby launch. Slots submit sequentially \u2014 typical N \u2264 3, so the\nparallel-gather savings are negligible.", "operationId": "submit_experiment_runs_api___danger___test_harness_write_experiment_runs_post", "requestBody": { "content": { @@ -1151,7 +1151,7 @@ }, "EpisodeFailure": { "properties": { - "run_id": { + "sample_id": { "type": "string", "format": "uuid", "title": "Run Id" @@ -1163,7 +1163,7 @@ }, "type": "object", "required": [ - "run_id", + "sample_id", "error" ], "title": "EpisodeFailure", @@ -1328,7 +1328,7 @@ }, "ExperimentRunMetricsDto": { "properties": { - "run_id": { + "sample_id": { "type": "string", "format": "uuid", "title": "Run Id" @@ -1482,7 +1482,7 @@ }, "type": "object", "required": [ - "run_id", + "sample_id", "status", "instance_key" ], @@ -1525,7 +1525,7 @@ "format": "uuid", "title": "Definition Id" }, - "run_ids": { + "sample_ids": { "items": { "type": "string", "format": "uuid" @@ -1545,13 +1545,13 @@ "type": "object", "required": [ "definition_id", - "run_ids" + "sample_ids" ], "title": "ExperimentRunResult" }, "ExperimentRunRowDto": { "properties": { - "run_id": { + "sample_id": { "type": "string", "format": "uuid", "title": "Run Id" @@ -1701,7 +1701,7 @@ }, "type": "object", "required": [ - "run_id", + "sample_id", "definition_id", "benchmark_type", "instance_key", @@ -1955,7 +1955,7 @@ "title": "Id", "description": "Identifier of the mutation row itself, not a graph target id." }, - "run_id": { + "sample_id": { "type": "string", "format": "uuid", "title": "Run Id" @@ -2116,7 +2116,7 @@ "type": "object", "required": [ "id", - "run_id", + "sample_id", "sequence", "mutation_type", "target_type", @@ -2521,7 +2521,7 @@ "type": "string", "title": "Threadtopic" }, - "runId": { + "sampleId": { "type": "string", "title": "Runid" }, @@ -2575,7 +2575,7 @@ "id", "threadId", "threadTopic", - "runId", + "sampleId", "fromAgentId", "toAgentId", "content", @@ -2590,7 +2590,7 @@ "type": "string", "title": "Id" }, - "runId": { + "sampleId": { "type": "string", "title": "Runid" }, @@ -2650,7 +2650,7 @@ "type": "object", "required": [ "id", - "runId", + "sampleId", "topic", "agentAId", "agentBId", @@ -2659,14 +2659,14 @@ ], "title": "RunCommunicationThreadDto" }, - "RunContextEventDto": { + "SampleContextEventDto": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, - "runId": { + "sampleId": { "type": "string", "format": "uuid", "title": "Runid" @@ -2738,7 +2738,7 @@ "type": "object", "required": [ "id", - "runId", + "sampleId", "taskExecutionId", "taskId", "workerBindingKey", @@ -2747,7 +2747,7 @@ "payload", "createdAt" ], - "title": "RunContextEventDto" + "title": "SampleContextEventDto" }, "RunEvaluationCriterionDto": { "properties": { @@ -3043,7 +3043,7 @@ ], "title": "RunExecutionAttemptDto" }, - "RunResourceDto": { + "SampleResourceDto": { "properties": { "id": { "type": "string", @@ -3091,7 +3091,7 @@ "sizeBytes", "createdAt" ], - "title": "RunResourceDto" + "title": "SampleResourceDto" }, "RunSandboxCommandDto": { "properties": { @@ -3233,7 +3233,7 @@ ], "title": "RunSandboxDto" }, - "RunSnapshotDto": { + "SampleSnapshotDto": { "properties": { "id": { "type": "string", @@ -3253,7 +3253,7 @@ }, "tasks": { "additionalProperties": { - "$ref": "#/components/schemas/RunTaskDto" + "$ref": "#/components/schemas/SampleTaskDto" }, "type": "object", "title": "Tasks" @@ -3266,7 +3266,7 @@ "resourcesByTask": { "additionalProperties": { "items": { - "$ref": "#/components/schemas/RunResourceDto" + "$ref": "#/components/schemas/SampleResourceDto" }, "type": "array" }, @@ -3285,7 +3285,7 @@ }, "evaluationsByTask": { "additionalProperties": { - "$ref": "#/components/schemas/RunTaskEvaluationDto" + "$ref": "#/components/schemas/SampleTaskEvaluationDto" }, "type": "object", "title": "Evaluationsbytask" @@ -3300,7 +3300,7 @@ "contextEventsByTask": { "additionalProperties": { "items": { - "$ref": "#/components/schemas/RunContextEventDto" + "$ref": "#/components/schemas/SampleContextEventDto" }, "type": "array" }, @@ -3393,7 +3393,7 @@ "metrics": { "anyOf": [ { - "$ref": "#/components/schemas/RunSnapshotMetricsDto" + "$ref": "#/components/schemas/SampleSnapshotMetricsDto" }, { "type": "null" @@ -3420,11 +3420,11 @@ "name", "status" ], - "title": "RunSnapshotDto" + "title": "SampleSnapshotDto" }, - "RunSnapshotMetricsDto": { + "SampleSnapshotMetricsDto": { "properties": { - "runId": { + "sampleId": { "type": "string", "title": "Runid" }, @@ -3491,12 +3491,12 @@ "additionalProperties": false, "type": "object", "required": [ - "runId", + "sampleId", "status" ], - "title": "RunSnapshotMetricsDto" + "title": "SampleSnapshotMetricsDto" }, - "RunSummaryDto": { + "SampleSummaryDto": { "properties": { "id": { "type": "string", @@ -3727,9 +3727,9 @@ "instance_key", "sample_label" ], - "title": "RunSummaryDto" + "title": "SampleSummaryDto" }, - "RunTaskDto": { + "SampleTaskDto": { "properties": { "id": { "type": "string", @@ -3837,16 +3837,16 @@ "isLeaf", "level" ], - "title": "RunTaskDto", - "description": "REST projection of RunGraphNode for run detail pages.\n\nThis is not the canonical graph schema; graph semantics live in\napplication/graph/models.py and application/runtime/status.py." + "title": "SampleTaskDto", + "description": "REST projection of SampleGraphNode for run detail pages.\n\nThis is not the canonical graph schema; graph semantics live in\napplication/graph/models.py and application/runtime/status.py." }, - "RunTaskEvaluationDto": { + "SampleTaskEvaluationDto": { "properties": { "id": { "type": "string", "title": "Id" }, - "runId": { + "sampleId": { "type": "string", "title": "Runid" }, @@ -3917,7 +3917,7 @@ "type": "object", "required": [ "id", - "runId", + "sampleId", "evaluatorName", "aggregationRule", "totalScore", @@ -3927,7 +3927,7 @@ "stagesPassed", "createdAt" ], - "title": "RunTaskEvaluationDto" + "title": "SampleTaskEvaluationDto" }, "SeedRunRequest": { "properties": { @@ -4035,7 +4035,7 @@ }, "SubmitExperimentRunsResponse": { "properties": { - "run_ids": { + "sample_ids": { "items": { "type": "string", "format": "uuid" @@ -4046,7 +4046,7 @@ }, "type": "object", "required": [ - "run_ids" + "sample_ids" ], "title": "SubmitExperimentRunsResponse" }, @@ -4100,7 +4100,7 @@ "format": "uuid", "title": "Batch Id" }, - "run_ids": { + "sample_ids": { "items": { "type": "string", "format": "uuid" @@ -4116,7 +4116,7 @@ "type": "object", "required": [ "batch_id", - "run_ids" + "sample_ids" ], "title": "SubmitResponse", "description": "Ergon \u2192 Trainer: batch accepted." @@ -4217,7 +4217,7 @@ }, "TestExperimentRunDto": { "properties": { - "run_id": { + "sample_id": { "type": "string", "format": "uuid", "title": "Run Id" @@ -4229,7 +4229,7 @@ }, "type": "object", "required": [ - "run_id", + "sample_id", "status" ], "title": "TestExperimentRunDto" @@ -4320,7 +4320,7 @@ }, "TestRunStateDto": { "properties": { - "run_id": { + "sample_id": { "type": "string", "format": "uuid", "title": "Run Id" @@ -4380,7 +4380,7 @@ }, "type": "object", "required": [ - "run_id", + "sample_id", "status", "graph_nodes", "mutations", @@ -4519,7 +4519,7 @@ }, "Trajectory": { "properties": { - "run_id": { + "sample_id": { "type": "string", "format": "uuid", "title": "Run Id" @@ -4567,7 +4567,7 @@ }, "type": "object", "required": [ - "run_id", + "sample_id", "agent_id", "prompt_ids", "completion_ids", diff --git a/ergon-dashboard/src/hooks/useResourceContent.ts b/ergon-dashboard/src/hooks/useResourceContent.ts index dd85dcfe1..38de8d8bc 100644 --- a/ergon-dashboard/src/hooks/useResourceContent.ts +++ b/ergon-dashboard/src/hooks/useResourceContent.ts @@ -2,7 +2,7 @@ /** * Fetches text content for a resource through the Next.js proxy (which in turn - * calls GET /runs/{runId}/resources/{resourceId}/content on the Ergon API). + * calls GET /samples/{sampleId}/resources/{resourceId}/content on the Ergon API). * Responses are cached in a module-level Map so re-opening a viewer is free. */ @@ -10,8 +10,8 @@ import { useEffect, useState } from "react"; const textCache = new Map(); -export function resourceContentUrl(runId: string, resourceId: string): string { - return `/api/runs/${runId}/resources/${resourceId}/content`; +export function resourceContentUrl(sampleId: string, resourceId: string): string { + return `/api/samples/${sampleId}/resources/${resourceId}/content`; } interface UseResourceContentResult { @@ -21,7 +21,7 @@ interface UseResourceContentResult { } export function useResourceContent( - runId: string | null, + sampleId: string | null, resourceId: string | null, enabled: boolean, ): UseResourceContentResult { @@ -30,14 +30,14 @@ export function useResourceContent( const [isLoading, setIsLoading] = useState(false); useEffect(() => { - if (!enabled || runId === null || resourceId === null) { + if (!enabled || sampleId === null || resourceId === null) { setText(null); setError(null); setIsLoading(false); return; } - const cacheKey = `${runId}:${resourceId}`; + const cacheKey = `${sampleId}:${resourceId}`; const cached = textCache.get(cacheKey); if (cached !== undefined) { setText(cached); @@ -51,7 +51,7 @@ export function useResourceContent( setError(null); setText(null); - fetch(resourceContentUrl(runId, resourceId), { + fetch(resourceContentUrl(sampleId, resourceId), { cache: "no-store", signal: controller.signal, }) @@ -73,7 +73,7 @@ export function useResourceContent( }); return () => controller.abort(); - }, [runId, resourceId, enabled]); + }, [sampleId, resourceId, enabled]); return { text, error, isLoading }; } diff --git a/ergon-dashboard/src/hooks/useRunState.socketHydration.test.ts b/ergon-dashboard/src/hooks/useSampleWorkspaceState.socketHydration.test.ts similarity index 93% rename from ergon-dashboard/src/hooks/useRunState.socketHydration.test.ts rename to ergon-dashboard/src/hooks/useSampleWorkspaceState.socketHydration.test.ts index 0a715934a..5237a6dbe 100644 --- a/ergon-dashboard/src/hooks/useRunState.socketHydration.test.ts +++ b/ergon-dashboard/src/hooks/useSampleWorkspaceState.socketHydration.test.ts @@ -1,11 +1,11 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { hydrateRunSnapshot } from "@/lib/run-state"; -import { applySandboxCommand, applySandboxCreated, applyTaskStatusChanged } from "@/lib/run-state/reducers"; +import { hydrateRunSnapshot } from "@/lib/sample-state"; +import { applySandboxCommand, applySandboxCreated, applyTaskStatusChanged } from "@/lib/sample-state/reducers"; import { TaskStatus } from "@/lib/types"; import { createDashboardSeed, FIXTURE_IDS } from "../../tests/helpers/dashboardFixtures"; -import { shouldRequestSocketSnapshot } from "./useRunState"; +import { shouldRequestSocketSnapshot } from "./useSampleWorkspaceState"; test("does not request socket full-state snapshot when REST or SSR state is already hydrated", () => { assert.equal(shouldRequestSocketSnapshot(true), false); @@ -21,7 +21,7 @@ test("task status reducer records transition history", () => { const state = hydrateRunSnapshot(run); const next = applyTaskStatusChanged(state, { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, taskId: FIXTURE_IDS.solveTaskId, status: TaskStatus.COMPLETED, timestamp: "2026-03-18T12:01:00.000Z", @@ -109,7 +109,7 @@ test("cancelled task status records terminal task and execution timestamps", () assert.ok(run); const running = applyTaskStatusChanged(hydrateRunSnapshot(run), { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, taskId: FIXTURE_IDS.solveTaskId, status: TaskStatus.RUNNING, timestamp: "2026-03-18T12:00:10.000Z", @@ -117,7 +117,7 @@ test("cancelled task status records terminal task and execution timestamps", () assignedWorkerSlug: null, }); const cancelled = applyTaskStatusChanged(running, { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, taskId: FIXTURE_IDS.solveTaskId, status: TaskStatus.CANCELLED, timestamp: "2026-03-18T12:00:20.000Z", diff --git a/ergon-dashboard/src/hooks/useRunState.ts b/ergon-dashboard/src/hooks/useSampleWorkspaceState.ts similarity index 84% rename from ergon-dashboard/src/hooks/useRunState.ts rename to ergon-dashboard/src/hooks/useSampleWorkspaceState.ts index 8e8f28e6e..6097772a1 100644 --- a/ergon-dashboard/src/hooks/useRunState.ts +++ b/ergon-dashboard/src/hooks/useSampleWorkspaceState.ts @@ -1,10 +1,10 @@ "use client"; /** - * useRunState - Hook for managing a single workflow run's state. + * useSampleWorkspaceState - Hook for managing a single workflow run's state. * * Subscribes to a specific run's updates and maintains the full - * WorkflowRunState for that run, including tasks, actions, resources, etc. + * SampleWorkspaceState for that run, including tasks, actions, resources, etc. * * On subscription, requests the full run state from the server to hydrate * existing data (important for completed runs or page refreshes). @@ -29,20 +29,20 @@ import { TaskStatus, SandboxState, SandboxCommandState, - WorkflowRunState, - SerializedWorkflowRunState, + SampleWorkspaceState, + SerializedSampleWorkspaceState, } from "@/lib/types"; -import { compareContextEvents, deserializeRunState } from "@/lib/runState"; +import { compareContextEvents, deserializeRunState } from "@/lib/sampleState"; import { applySandboxClosed, applySandboxCommand, applySandboxCreated, applyTaskStatusChanged, -} from "@/lib/run-state/reducers"; +} from "@/lib/sample-state/reducers"; import { useGraphMutations } from "@/features/graph/hooks/useGraphMutations"; interface UseRunStateResult { - runState: WorkflowRunState | null; + runState: SampleWorkspaceState | null; isLoading: boolean; error: string | null; isSubscribed: boolean; @@ -81,12 +81,12 @@ export function shouldRequestSocketSnapshot(hasHydratedRunState: boolean): boole return !hasHydratedRunState; } -export function useRunState( - runId: string, - initialRunState: SerializedWorkflowRunState | null = null, +export function useSampleWorkspaceState( + sampleId: string, + initialRunState: SerializedSampleWorkspaceState | null = null, ): UseRunStateResult { const { socket, isConnected, subscribe, unsubscribe } = useSocket(); - const [runState, setRunState] = useState( + const [runState, setRunState] = useState( initialRunState ? deserializeRunState(initialRunState) : null, ); const [isLoading, setIsLoading] = useState(initialRunState === null); @@ -98,7 +98,7 @@ export function useRunState( const loadSnapshot = useCallback(async () => { try { - const response = await fetch(`/api/runs/${runId}`, { cache: "no-store" }); + const response = await fetch(`/api/samples/${sampleId}`, { cache: "no-store" }); if (!response.ok) { throw new Error(`Failed to load run (${response.status})`); } @@ -110,7 +110,7 @@ export function useRunState( } finally { setIsLoading(false); } - }, [runId]); + }, [sampleId]); useEffect(() => { hasRunStateRef.current = runState !== null; @@ -133,12 +133,12 @@ export function useRunState( (payload: unknown) => { const data = parseTaskStatusSocketData(payload); const status = data.status as TaskStatus; - if (data.runId !== runId) return; + if (data.sampleId !== sampleId) return; setRunState((prev) => { if (!prev) return prev; return applyTaskStatusChanged(prev, { - runId: data.runId, + sampleId: data.sampleId, taskId: data.taskId, status, timestamp: data.timestamp, @@ -147,14 +147,14 @@ export function useRunState( }); }); }, - [runId] + [sampleId] ); // Handle new resource const handleResourceNew = useCallback( (payload: unknown) => { const data = parseResourceSocketData(payload); - if (data.runId !== runId) return; + if (data.sampleId !== sampleId) return; setRunState((prev) => { if (!prev) return prev; @@ -170,14 +170,14 @@ export function useRunState( return { ...prev, resourcesByTask: newResourcesByTask }; }); }, - [runId] + [sampleId] ); // Handle sandbox created const handleSandboxCreated = useCallback( (payload: unknown) => { const data = parseSandboxCreatedSocketData(payload); - if (data.runId !== runId) return; + if (data.sampleId !== sampleId) return; setRunState((prev) => { if (!prev) return prev; @@ -194,14 +194,14 @@ export function useRunState( ); }); }, - [runId] + [sampleId] ); // Handle sandbox command const handleSandboxCommand = useCallback( (payload: unknown) => { const data = parseSandboxCommandSocketData(payload); - if (data.runId !== runId) return; + if (data.sampleId !== sampleId) return; setRunState((prev) => { if (!prev) return prev; @@ -216,14 +216,14 @@ export function useRunState( return applySandboxCommand(prev, data.taskId, command); }); }, - [runId] + [sampleId] ); // Handle sandbox closed const handleSandboxClosed = useCallback( (payload: unknown) => { const data = parseSandboxClosedSocketData(payload); - if (data.runId !== runId) return; + if (data.sampleId !== sampleId) return; setRunState((prev) => { if (!prev) return prev; @@ -231,14 +231,14 @@ export function useRunState( return applySandboxClosed(prev, data.taskId, data.reason, data.timestamp); }); }, - [runId] + [sampleId] ); // Handle run completed const handleRunCompleted = useCallback( (payload: unknown) => { const data = parseRunCompletedSocketData(payload); - if (data.runId !== runId) return; + if (data.sampleId !== sampleId) return; setRunState((prev) => { if (!prev) return prev; @@ -253,13 +253,13 @@ export function useRunState( }; }); }, - [runId] + [sampleId] ); const handleThreadMessage = useCallback( (payload: unknown) => { const data = parseDashboardThreadMessageCreatedData(payload); - if (data.run_id !== runId) return; + if (data.sample_id !== sampleId) return; setRunState((prev) => { if (!prev) return prev; @@ -279,12 +279,12 @@ export function useRunState( }; }); }, - [runId] + [sampleId] ); const handleContextEvent = useCallback( - (payload: { runId: string; taskId: string; event: ContextEventState }) => { - if (payload.runId !== runId) return; + (payload: { sampleId: string; taskId: string; event: ContextEventState }) => { + if (payload.sampleId !== sampleId) return; setRunState((prev) => { if (!prev) return prev; @@ -303,13 +303,13 @@ export function useRunState( }; }); }, - [runId] + [sampleId] ); const handleTaskEvaluation = useCallback( (payload: unknown) => { const data = parseDashboardTaskEvaluationUpdatedData(payload); - if (data.run_id !== runId) return; + if (data.sample_id !== sampleId) return; setRunState((prev) => { if (!prev) return prev; @@ -322,24 +322,24 @@ export function useRunState( }; }); }, - [runId] + [sampleId] ); const { handleGraphMutation } = useGraphMutations(setRunState); const handleGraphMutationSocket = useCallback( (data: GraphMutationSocketData) => { - if (data.runId !== runId) return; + if (data.sampleId !== sampleId) return; handleGraphMutation(data.mutation); }, - [runId, handleGraphMutation], + [sampleId, handleGraphMutation], ); // Handle full run state sync (for initial load / completed runs) const handleSyncRun = useCallback( - (data: SerializedWorkflowRunState | null) => { + (data: SerializedSampleWorkspaceState | null) => { console.log( - "[useRunState] Received sync:run", + "[useSampleWorkspaceState] Received sync:run", data ? `(${Object.keys(data.tasks ?? {}).length} tasks)` : "(null)", ); @@ -363,40 +363,40 @@ export function useRunState( // Subscribe to run updates useEffect(() => { if (!socket || !isConnected) { - console.log("[useRunState] Socket not ready - socket:", !!socket, "isConnected:", isConnected); + console.log("[useSampleWorkspaceState] Socket not ready - socket:", !!socket, "isConnected:", isConnected); return; } let retryTimeout: ReturnType | null = null; - // Only subscribe if we haven't already for this runId - if (subscriptionRef.current !== runId) { + // Only subscribe if we haven't already for this sampleId + if (subscriptionRef.current !== sampleId) { // Unsubscribe from previous run if any if (subscriptionRef.current) { unsubscribe(subscriptionRef.current); } // Subscribe to new run - console.log("[useRunState] Subscribing to run", runId); - subscribe(runId); - subscriptionRef.current = runId; + console.log("[useSampleWorkspaceState] Subscribing to run", sampleId); + subscribe(sampleId); + subscriptionRef.current = sampleId; setIsSubscribed(true); setIsLoading((prev) => (hasRunStateRef.current ? false : prev)); if (shouldRequestSocketSnapshot(hasRunStateRef.current)) { // Request full run state only when REST/SSR did not hydrate us. - console.log("[useRunState] Requesting full state for run", runId, "socket.connected:", socket.connected); - socket.emit("request:run", runId); + console.log("[useSampleWorkspaceState] Requesting full state for run", sampleId, "socket.connected:", socket.connected); + socket.emit("request:run", sampleId); // Set up a retry in case the first request is lost retryTimeout = setTimeout(() => { if (socket.connected && shouldRequestSocketSnapshot(hasRunStateRef.current)) { - console.log("[useRunState] Retrying request:run for", runId); - socket.emit("request:run", runId); + console.log("[useSampleWorkspaceState] Retrying request:run for", sampleId); + socket.emit("request:run", sampleId); } }, 1000); } else { - console.log("[useRunState] Skipping full socket state request; REST/SSR snapshot is already loaded", runId); + console.log("[useSampleWorkspaceState] Skipping full socket state request; REST/SSR snapshot is already loaded", sampleId); } } @@ -430,7 +430,7 @@ export function useRunState( }, [ socket, isConnected, - runId, + sampleId, subscribe, unsubscribe, handleSyncRun, diff --git a/ergon-dashboard/src/hooks/useTaskDetails.ts b/ergon-dashboard/src/hooks/useTaskDetails.ts index 71cad2576..3c611a2af 100644 --- a/ergon-dashboard/src/hooks/useTaskDetails.ts +++ b/ergon-dashboard/src/hooks/useTaskDetails.ts @@ -15,7 +15,7 @@ import { ResourceState, SandboxState, TaskEvaluationState, - WorkflowRunState, + SampleWorkspaceState, } from "@/lib/types"; export interface TaskDependencies { @@ -49,11 +49,11 @@ export interface UseTaskDetailsResult { /** * Hook to get detailed information about a specific task within a run. * - * @param runId - The workflow run ID + * @param sampleId - The workflow run ID * @param taskId - The task ID (null means no task selected) */ export function useTaskDetails( - runState: WorkflowRunState | null, + runState: SampleWorkspaceState | null, taskId: string | null ): UseTaskDetailsResult { // Extract task diff --git a/ergon-dashboard/src/inngest/functions/index.ts b/ergon-dashboard/src/inngest/functions/index.ts index c81afb8df..d30be3a05 100644 --- a/ergon-dashboard/src/inngest/functions/index.ts +++ b/ergon-dashboard/src/inngest/functions/index.ts @@ -50,7 +50,7 @@ const onWorkflowStarted = inngest.createFunction( async ({ event }) => { const payload = parseDashboardWorkflowStartedData(event.data); const { - run_id, + sample_id, definition_id, workflow_name, snapshot, @@ -60,14 +60,14 @@ const onWorkflowStarted = inngest.createFunction( } = payload; console.log("[Dashboard] Workflow started - INNGEST FUNCTION TRIGGERED:", { - run_id, + sample_id, workflow_name, total_tasks, }); // Update store store.initializeRun( - run_id, + sample_id, definition_id, workflow_name, snapshot, @@ -82,11 +82,11 @@ const onWorkflowStarted = inngest.createFunction( // Broadcast to all clients (new run appeared) console.log("[Dashboard] About to call broadcastRunStarted..."); - broadcastRunStarted(run_id, workflow_name); + broadcastRunStarted(sample_id, workflow_name); console.log("[Dashboard] broadcastRunStarted completed"); // Prune old runs to prevent memory growth - store.pruneOldRuns(); + store.pruneOldSamples(); return { success: true }; } @@ -98,7 +98,7 @@ const onWorkflowCompleted = inngest.createFunction( async ({ event }) => { const payload = DashboardWorkflowCompletedEventSchema.parse(event.data); const { - run_id, + sample_id, status, completed_at, duration_seconds, @@ -110,14 +110,14 @@ const onWorkflowCompleted = inngest.createFunction( const narrowedStatus = status as "completed" | "failed"; console.log("[Dashboard] Workflow completed:", { - run_id, + sample_id, status, duration_seconds, }); // Update store store.completeRun( - run_id, + sample_id, narrowedStatus, completed_at, duration_seconds, @@ -127,7 +127,7 @@ const onWorkflowCompleted = inngest.createFunction( // Broadcast to run subscribers broadcastRunCompleted( - run_id, + sample_id, narrowedStatus, completed_at, duration_seconds, @@ -144,7 +144,7 @@ const onThreadMessageCreated = inngest.createFunction( { event: "dashboard/thread.message_created" }, async ({ event }) => { const payload = parseDashboardThreadMessageCreatedData(event.data); - store.upsertThread(payload.run_id, payload.thread); + store.upsertThread(payload.sample_id, payload.thread); broadcastThreadMessage(payload); return { success: true }; }, @@ -155,7 +155,7 @@ const onTaskEvaluationUpdated = inngest.createFunction( { event: "dashboard/task.evaluation_updated" }, async ({ event }) => { const payload = parseDashboardTaskEvaluationUpdatedData(event.data); - store.upsertEvaluation(payload.run_id, payload.task_id, payload.evaluation); + store.upsertEvaluation(payload.sample_id, payload.task_id, payload.evaluation); broadcastTaskEvaluation(payload); return { success: true }; }, @@ -171,7 +171,7 @@ const onTaskStatusChanged = inngest.createFunction( async ({ event }) => { const payload = DashboardTaskStatusChangedEventSchema.parse(event.data); const { - run_id, + sample_id, task_id, task_name, new_status, @@ -181,7 +181,7 @@ const onTaskStatusChanged = inngest.createFunction( } = payload; console.log("[Dashboard] Task status changed:", { - run_id, + sample_id, task_id, task_name, new_status, @@ -189,7 +189,7 @@ const onTaskStatusChanged = inngest.createFunction( // Update store store.updateTaskStatus( - run_id, + sample_id, task_id, new_status as TaskStatus, timestamp, @@ -199,7 +199,7 @@ const onTaskStatusChanged = inngest.createFunction( // Broadcast to run subscribers broadcastTaskStatus( - run_id, + sample_id, task_id, new_status as TaskStatus, timestamp, @@ -221,7 +221,7 @@ const onResourcePublished = inngest.createFunction( async ({ event }) => { const payload = DashboardResourcePublishedEventSchema.parse(event.data); const { - run_id, + sample_id, task_id, task_execution_id, resource_id, @@ -233,7 +233,7 @@ const onResourcePublished = inngest.createFunction( } = payload; console.log("[Dashboard] Resource published:", { - run_id, + sample_id, task_id, resource_name, mime_type, @@ -253,10 +253,10 @@ const onResourcePublished = inngest.createFunction( }; // Update store - store.addResource(run_id, resource); + store.addResource(sample_id, resource); // Broadcast to run subscribers - broadcastResourceNew(run_id, resource); + broadcastResourceNew(sample_id, resource); return { success: true }; } @@ -271,21 +271,21 @@ const onSandboxCreated = inngest.createFunction( { event: "dashboard/sandbox.created" }, async ({ event }) => { const payload = DashboardSandboxCreatedEventSchema.parse(event.data); - const { run_id, task_id, sandbox_id, template, timeout_minutes, timestamp } = + const { sample_id, task_id, sandbox_id, template, timeout_minutes, timestamp } = payload; console.log("[Dashboard] Sandbox created:", { - run_id, + sample_id, task_id, sandbox_id, template, }); - const runId = run_id; + const sampleId = sample_id; // Update store store.createSandbox( - runId, + sampleId, task_id, sandbox_id, template ?? null, @@ -294,9 +294,9 @@ const onSandboxCreated = inngest.createFunction( ); // Broadcast to run subscribers - const sandbox = store.getSandboxForTask(runId, task_id); + const sandbox = store.getSandboxForTask(sampleId, task_id); if (sandbox) { - broadcastSandboxCreated(runId, sandbox); + broadcastSandboxCreated(sampleId, sandbox); } return { success: true }; @@ -326,18 +326,18 @@ const onSandboxCommand = inngest.createFunction( exit_code, }); - // Find the run_id for this task + // Find the sample_id for this task const runs = store.getAllRuns(); - let runId: string | null = null; + let sampleId: string | null = null; for (const run of runs) { if (run.tasks.has(task_id)) { - runId = run.id; + sampleId = run.id; break; } } - if (!runId) { + if (!sampleId) { console.warn( `[Dashboard] Could not find run for task ${task_id} in sandbox.command` ); @@ -355,10 +355,10 @@ const onSandboxCommand = inngest.createFunction( }; // Update store - store.addSandboxCommand(runId, task_id, commandState); + store.addSandboxCommand(sampleId, task_id, commandState); // Broadcast to run subscribers - broadcastSandboxCommand(runId, task_id, commandState); + broadcastSandboxCommand(sampleId, task_id, commandState); return { success: true }; } @@ -377,18 +377,18 @@ const onSandboxClosed = inngest.createFunction( reason, }); - // Find the run_id for this task + // Find the sample_id for this task const runs = store.getAllRuns(); - let runId: string | null = null; + let sampleId: string | null = null; for (const run of runs) { if (run.tasks.has(task_id)) { - runId = run.id; + sampleId = run.id; break; } } - if (!runId) { + if (!sampleId) { console.warn( `[Dashboard] Could not find run for task ${task_id} in sandbox.closed` ); @@ -396,10 +396,10 @@ const onSandboxClosed = inngest.createFunction( } // Update store - store.closeSandbox(runId, task_id, reason, timestamp); + store.closeSandbox(sampleId, task_id, reason, timestamp); // Broadcast to run subscribers - broadcastSandboxClosed(runId, task_id, reason, timestamp); + broadcastSandboxClosed(sampleId, task_id, reason, timestamp); return { success: true }; } @@ -414,8 +414,8 @@ const onGraphMutation = inngest.createFunction( { event: "dashboard/graph.mutation" }, async ({ event }) => { const mutation = parseDashboardGraphMutationData(event.data); - store.applyGraphMutation(mutation.run_id, mutation); - broadcastGraphMutation(mutation.run_id, mutation); + store.applyGraphMutation(mutation.sample_id, mutation); + broadcastGraphMutation(mutation.sample_id, mutation); return { success: true }; }, ); diff --git a/ergon-dashboard/src/inngest/functions/onContextEvent.ts b/ergon-dashboard/src/inngest/functions/onContextEvent.ts index b54d6863a..c657cd8cb 100644 --- a/ergon-dashboard/src/inngest/functions/onContextEvent.ts +++ b/ergon-dashboard/src/inngest/functions/onContextEvent.ts @@ -12,7 +12,7 @@ export const onContextEvent = inngest.createFunction( const contextEvent: ContextEventState = { id: payload.id, - runId: payload.run_id, + sampleId: payload.sample_id, taskExecutionId: payload.task_execution_id, taskId: payload.task_id, workerBindingKey: payload.worker_binding_key, @@ -24,8 +24,8 @@ export const onContextEvent = inngest.createFunction( completedAt: payload.completed_at ?? null, }; - store.addContextEvent(payload.run_id, payload.task_id, contextEvent); - broadcastContextEvent(payload.run_id, payload.task_id, contextEvent); + store.addContextEvent(payload.sample_id, payload.task_id, contextEvent); + broadcastContextEvent(payload.sample_id, payload.task_id, contextEvent); return { success: true }; }, diff --git a/ergon-dashboard/src/lib/config.ts b/ergon-dashboard/src/lib/config.ts index c52ec8aeb..beee7e848 100644 --- a/ergon-dashboard/src/lib/config.ts +++ b/ergon-dashboard/src/lib/config.ts @@ -26,7 +26,7 @@ export const config = { enableTestHarness: true, // Store - maxRunsToKeep: parseInt(process.env.MAX_RUNS_TO_KEEP || "50", 10), + maxSamplesToKeep: parseInt(process.env.MAX_SAMPLES_TO_KEEP || "50", 10), } as const; export type Config = typeof config; diff --git a/ergon-dashboard/src/lib/contracts/contextEvents.ts b/ergon-dashboard/src/lib/contracts/contextEvents.ts index 07e24169d..cf64569b5 100644 --- a/ergon-dashboard/src/lib/contracts/contextEvents.ts +++ b/ergon-dashboard/src/lib/contracts/contextEvents.ts @@ -1,6 +1,6 @@ // ergon-dashboard/src/lib/contracts/contextEvents.ts /** - * TypeScript types for run_context_events — mirrors Python ContextEventPayload. + * TypeScript types for sample_context_events — mirrors Python ContextEventPayload. * Must stay in sync with ergon_core/ergon_core/core/persistence/context/event_payloads.py */ @@ -63,7 +63,7 @@ export type ContextEventPayload = export interface ContextEventState { id: string; - runId: string; + sampleId: string; taskExecutionId: string; taskId: string; workerBindingKey: string; diff --git a/ergon-dashboard/src/lib/contracts/events.ts b/ergon-dashboard/src/lib/contracts/events.ts index 17ae24c06..a2114956b 100644 --- a/ergon-dashboard/src/lib/contracts/events.ts +++ b/ergon-dashboard/src/lib/contracts/events.ts @@ -19,21 +19,21 @@ import { parseRunSandbox, parseRunSandboxCommand, parseRunSnapshot, - parseRunTaskEvaluation, + parseSampleTaskEvaluation, RunCommunicationMessageSchema, RunCommunicationThreadSchema, - RunResourceSchema, - RunResource, + SampleResourceSchema, + SampleResource, RunSandbox, RunSandboxCommand, RunSandboxCommandSchema, RunSandboxSchema, RunSnapshot, - RunTaskEvaluation, - RunTaskEvaluationSchema, + SampleTaskEvaluation, + SampleTaskEvaluationSchema, TaskStatusSchema, } from "@/lib/contracts/rest"; -import { normalizeContextEventPayload } from "@/lib/run-state/contextEvents"; +import { normalizeContextEventPayload } from "@/lib/sample-state/contextEvents"; export { dashboardEventSchemas }; @@ -60,7 +60,7 @@ export type ResourceRef = z.infer; export type EvaluatorRef = z.infer; export const DashboardWorkflowStartedDataSchema = z.object({ - run_id: z.string().uuid(), + sample_id: z.string().uuid(), definition_id: z.string().uuid(), workflow_name: z.string(), snapshot: z.unknown(), @@ -70,19 +70,19 @@ export const DashboardWorkflowStartedDataSchema = z.object({ }); export const DashboardThreadMessageCreatedDataSchema = z.object({ - run_id: z.string().uuid(), + sample_id: z.string().uuid(), thread: RunCommunicationThreadSchema, message: RunCommunicationMessageSchema, }); export const DashboardTaskEvaluationUpdatedDataSchema = z.object({ - run_id: z.string().uuid(), + sample_id: z.string().uuid(), task_id: z.string().nullable().optional(), - evaluation: RunTaskEvaluationSchema, + evaluation: SampleTaskEvaluationSchema, }); export const RunListEntrySchema = z.object({ - runId: z.string(), + sampleId: z.string(), name: z.string(), status: z.enum(["pending", "executing", "evaluating", "completed", "failed", "cancelled"]), startedAt: z.string(), @@ -94,7 +94,7 @@ export const RunListEntrySchema = z.object({ export const SyncRunsSchema = z.array(RunListEntrySchema); export const RunCompletedSocketDataSchema = z.object({ - runId: z.string(), + sampleId: z.string(), status: z.enum(["completed", "failed"]), completedAt: z.string(), durationSeconds: z.number(), @@ -102,7 +102,7 @@ export const RunCompletedSocketDataSchema = z.object({ error: z.string().nullable(), }); export const TaskStatusSocketDataSchema = z.object({ - runId: z.string(), + sampleId: z.string(), taskId: z.string(), status: TaskStatusSchema, timestamp: z.string(), @@ -110,20 +110,20 @@ export const TaskStatusSocketDataSchema = z.object({ assignedWorkerSlug: z.string().nullable(), }); export const ResourceSocketDataSchema = z.object({ - runId: z.string(), - resource: RunResourceSchema, + sampleId: z.string(), + resource: SampleResourceSchema, }); export const SandboxCreatedSocketDataSchema = z.object({ - runId: z.string(), + sampleId: z.string(), sandbox: RunSandboxSchema, }); export const SandboxCommandSocketDataSchema = z.object({ - runId: z.string(), + sampleId: z.string(), taskId: z.string(), command: RunSandboxCommandSchema, }); export const SandboxClosedSocketDataSchema = z.object({ - runId: z.string(), + sampleId: z.string(), taskId: z.string(), reason: z.string(), timestamp: z.string(), @@ -131,7 +131,7 @@ export const SandboxClosedSocketDataSchema = z.object({ export type TaskTrigger = z.infer; export interface DashboardWorkflowStartedData { - run_id: string; + sample_id: string; definition_id: string; workflow_name: string; snapshot: RunSnapshot; @@ -147,28 +147,28 @@ export type DashboardSandboxCreatedData = GeneratedDashboardSandboxCreatedEvent; export type DashboardSandboxCommandData = GeneratedDashboardSandboxCommandEvent; export type DashboardSandboxClosedData = GeneratedDashboardSandboxClosedEvent; export interface DashboardThreadMessageCreatedData { - run_id: string; + sample_id: string; thread: ReturnType; message: ReturnType; } export interface DashboardTaskEvaluationUpdatedData { - run_id: string; + sample_id: string; task_id: string | null; - evaluation: RunTaskEvaluation; + evaluation: SampleTaskEvaluation; } export type RunListEntry = z.infer; export type RunCompletedSocketData = z.infer; export type TaskStatusSocketData = z.infer; export interface ResourceSocketData { - runId: string; - resource: RunResource; + sampleId: string; + resource: SampleResource; } export interface SandboxCreatedSocketData { - runId: string; + sampleId: string; sandbox: RunSandbox; } export interface SandboxCommandSocketData { - runId: string; + sampleId: string; taskId: string; command: RunSandboxCommand; } @@ -218,7 +218,7 @@ export function parseDashboardThreadMessageCreatedData( message: camelizeObjectKeys(raw.message), }); return { - run_id: parsed.run_id, + sample_id: parsed.sample_id, thread: parseRunCommunicationThread(parsed.thread), message: parseRunCommunicationMessage(parsed.message), }; @@ -233,9 +233,9 @@ export function parseDashboardTaskEvaluationUpdatedData( evaluation: camelizeObjectKeys(raw.evaluation), }); return { - run_id: parsed.run_id, + sample_id: parsed.sample_id, task_id: parsed.task_id ?? null, - evaluation: parseRunTaskEvaluation(parsed.evaluation), + evaluation: parseSampleTaskEvaluation(parsed.evaluation), }; } @@ -246,7 +246,7 @@ export function parseDashboardWorkflowStartedData(input: unknown): DashboardWork snapshot: camelizeSnapshotKeys(raw.snapshot), }); return { - run_id: parsed.run_id, + sample_id: parsed.sample_id, definition_id: parsed.definition_id, workflow_name: parsed.workflow_name, snapshot: parseRunSnapshot(parsed.snapshot), @@ -271,7 +271,7 @@ export function parseTaskStatusSocketData(input: unknown): TaskStatusSocketData export function parseResourceSocketData(input: unknown): ResourceSocketData { const parsed = ResourceSocketDataSchema.parse(input); return { - runId: parsed.runId, + sampleId: parsed.sampleId, resource: parsed.resource, }; } @@ -279,7 +279,7 @@ export function parseResourceSocketData(input: unknown): ResourceSocketData { export function parseSandboxCreatedSocketData(input: unknown): SandboxCreatedSocketData { const parsed = SandboxCreatedSocketDataSchema.parse(input); return { - runId: parsed.runId, + sampleId: parsed.sampleId, sandbox: parseRunSandbox(parsed.sandbox), }; } @@ -287,7 +287,7 @@ export function parseSandboxCreatedSocketData(input: unknown): SandboxCreatedSoc export function parseSandboxCommandSocketData(input: unknown): SandboxCommandSocketData { const parsed = SandboxCommandSocketDataSchema.parse(input); return { - runId: parsed.runId, + sampleId: parsed.sampleId, taskId: parsed.taskId, command: parseRunSandboxCommand(parsed.command), }; @@ -317,7 +317,7 @@ export function parseDashboardGraphMutationData(input: unknown): DashboardGraphM } export const GraphMutationSocketDataSchema = z.object({ - runId: z.string().uuid(), + sampleId: z.string().uuid(), mutation: DashboardGraphMutationDataSchema, }); export type GraphMutationSocketData = z.infer; diff --git a/ergon-dashboard/src/lib/contracts/rest.ts b/ergon-dashboard/src/lib/contracts/rest.ts index a6c7194ec..0ab0f766d 100644 --- a/ergon-dashboard/src/lib/contracts/rest.ts +++ b/ergon-dashboard/src/lib/contracts/rest.ts @@ -3,20 +3,20 @@ import { z } from "zod"; import { schemas } from "@/generated/rest/contracts"; export const BenchmarkNameSchema = z.string(); -export const RunStatusSchema = z.enum(["pending", "executing", "evaluating", "completed", "failed", "cancelled"]); +export const SampleStatusSchema = z.enum(["pending", "executing", "evaluating", "completed", "failed", "cancelled"]); export const TaskStatusSchema = z.string(); export const ExperimentDetailSchema = schemas.ExperimentDetailDto; export const RunExecutionAttemptSchema = schemas.RunExecutionAttemptDto; -export const RunResourceSchema = schemas.RunResourceDto; +export const SampleResourceSchema = schemas.SampleResourceDto; export const RunSandboxCommandSchema = schemas.RunSandboxCommandDto; export const RunSandboxSchema = schemas.RunSandboxDto; -export const RunTaskSchema = schemas.RunTaskDto; +export const SampleTaskSchema = schemas.SampleTaskDto; export const RunCommunicationMessageSchema = schemas.RunCommunicationMessageDto; export const RunCommunicationThreadSchema = schemas.RunCommunicationThreadDto; -export const RunTaskEvaluationSchema = schemas.RunTaskEvaluationDto; -export const RunSnapshotSchema = schemas.RunSnapshotDto; +export const SampleTaskEvaluationSchema = schemas.SampleTaskEvaluationDto; +export const RunSnapshotSchema = schemas.SampleSnapshotDto; type KnownKeys = { [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K @@ -25,21 +25,21 @@ type KnownKeys = { }; export type BenchmarkName = z.infer; -export type RunLifecycleStatus = z.infer; +export type RunLifecycleStatus = z.infer; export type TaskStatusValue = z.infer; type RawExperimentDetail = KnownKeys>; type RawExperimentRunRow = KnownKeys[number]>; type RawExperimentSummary = KnownKeys; type RawRunExecutionAttempt = KnownKeys>; -type RawRunResource = KnownKeys>; +type RawSampleResource = KnownKeys>; type RawRunSandboxCommand = KnownKeys>; type RawRunSandbox = KnownKeys>; -type RawRunTask = KnownKeys>; +type RawSampleTask = KnownKeys>; type RawRunCommunicationMessage = KnownKeys>; type RawRunCommunicationThread = KnownKeys>; -type RawRunTaskEvaluation = KnownKeys>; -type RawRunEvaluationCriterion = KnownKeys[number]>; +type RawSampleTaskEvaluation = KnownKeys>; +type RawRunEvaluationCriterion = KnownKeys[number]>; type RawRunSnapshot = KnownKeys>; type RawRunSnapshotMetrics = KnownKeys>; @@ -58,7 +58,7 @@ export interface ExperimentStatusCounts { } export interface ExperimentRunMetrics { - run_id: string; + sample_id: string; run_name?: string | null; status: string; sample_label?: string | null; @@ -164,7 +164,7 @@ export interface RunExecutionAttempt startedAt: string | null; } -export type RunResource = RawRunResource; +export type SampleResource = RawSampleResource; export interface RunSandboxCommand extends Omit { @@ -185,13 +185,13 @@ export interface RunSandbox /** * Per-task row in {@link RunSnapshot.tasks} (camelCase on the wire). * - * Semantics mirror the backend `RunTaskDto` field descriptions: + * Semantics mirror the backend `SampleTaskDto` field descriptions: * - `startedAt`: null only while the task has not actually started yet (e.g. pending / ready). * - `completedAt`: null until a terminal outcome; may be null together with `startedAt` if not started. */ -export interface RunTask +export interface SampleTask extends Omit< - RawRunTask, + RawSampleTask, "assignedWorkerId" | "assignedWorkerSlug" | "childIds" | "completedAt" | "dependsOnIds" | "parentId" | "startedAt" > { assignedWorkerId: string | null; @@ -222,8 +222,8 @@ export interface RunEvaluationCriterion evaluatedResourceIds: string[]; } -export interface RunTaskEvaluation - extends Omit { +export interface SampleTaskEvaluation + extends Omit { criterionResults: RunEvaluationCriterion[]; failedGate: string | null; taskId: string | null; @@ -247,13 +247,13 @@ export interface RunSnapshot completedAt: string | null; durationSeconds: number | null; error: string | null; - evaluationsByTask: Record; + evaluationsByTask: Record; executionsByTask: Record; finalScore: number | null; - resourcesByTask: Record; + resourcesByTask: Record; sandboxesByTask: Record; startedAt: string; - tasks: Record; + tasks: Record; threads: RunCommunicationThread[]; } @@ -291,7 +291,7 @@ function normalizeRunSandbox(sandbox: RawRunSandbox): RunSandbox { }; } -function normalizeRunTask(task: RawRunTask): RunTask { +function normalizeSampleTask(task: RawSampleTask): SampleTask { return { ...task, assignedWorkerId: task.assignedWorkerId ?? null, @@ -320,7 +320,7 @@ function normalizeRunCommunicationThread(thread: RawRunCommunicationThread): Run }; } -function normalizeRunTaskEvaluation(evaluation: RawRunTaskEvaluation): RunTaskEvaluation { +function normalizeSampleTaskEvaluation(evaluation: RawSampleTaskEvaluation): SampleTaskEvaluation { return { ...evaluation, criterionResults: (evaluation.criterionResults ?? []).map((criterion) => ({ @@ -389,8 +389,8 @@ export function parseRunCommunicationThread(input: unknown): RunCommunicationThr return normalizeRunCommunicationThread(RunCommunicationThreadSchema.parse(input)); } -export function parseRunTaskEvaluation(input: unknown): RunTaskEvaluation { - return normalizeRunTaskEvaluation(RunTaskEvaluationSchema.parse(input)); +export function parseSampleTaskEvaluation(input: unknown): SampleTaskEvaluation { + return normalizeSampleTaskEvaluation(SampleTaskEvaluationSchema.parse(input)); } export function parseRunSnapshot(input: unknown): RunSnapshot { @@ -403,7 +403,7 @@ export function parseRunSnapshot(input: unknown): RunSnapshot { evaluationsByTask: Object.fromEntries( Object.entries(snapshot.evaluationsByTask ?? {}).map(([taskId, evaluation]) => [ taskId, - normalizeRunTaskEvaluation(evaluation), + normalizeSampleTaskEvaluation(evaluation), ]), ), executionsByTask: Object.fromEntries( @@ -422,7 +422,7 @@ export function parseRunSnapshot(input: unknown): RunSnapshot { ), startedAt: snapshot.startedAt ?? new Date(0).toISOString(), tasks: Object.fromEntries( - Object.entries(snapshot.tasks ?? {}).map(([taskId, task]) => [taskId, normalizeRunTask(task)]), + Object.entries(snapshot.tasks ?? {}).map(([taskId, task]) => [taskId, normalizeSampleTask(task)]), ), threads: (snapshot.threads ?? []).map(normalizeRunCommunicationThread), }; diff --git a/ergon-dashboard/src/lib/run-state/domain.ts b/ergon-dashboard/src/lib/run-state/domain.ts deleted file mode 100644 index 2ddca9169..000000000 --- a/ergon-dashboard/src/lib/run-state/domain.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { RunSnapshot } from "@/lib/contracts/rest"; -import type { WorkflowRunState } from "@/lib/types"; - -export type WireRunSnapshot = RunSnapshot; -export type DashboardRunState = WorkflowRunState; diff --git a/ergon-dashboard/src/lib/run-state/contextEvents.ts b/ergon-dashboard/src/lib/sample-state/contextEvents.ts similarity index 100% rename from ergon-dashboard/src/lib/run-state/contextEvents.ts rename to ergon-dashboard/src/lib/sample-state/contextEvents.ts diff --git a/ergon-dashboard/src/lib/sample-state/domain.ts b/ergon-dashboard/src/lib/sample-state/domain.ts new file mode 100644 index 000000000..8a846b471 --- /dev/null +++ b/ergon-dashboard/src/lib/sample-state/domain.ts @@ -0,0 +1,5 @@ +import type { RunSnapshot } from "@/lib/contracts/rest"; +import type { SampleWorkspaceState } from "@/lib/types"; + +export type WireRunSnapshot = RunSnapshot; +export type DashboardRunState = SampleWorkspaceState; diff --git a/ergon-dashboard/src/lib/run-state/formatters.test.ts b/ergon-dashboard/src/lib/sample-state/formatters.test.ts similarity index 100% rename from ergon-dashboard/src/lib/run-state/formatters.test.ts rename to ergon-dashboard/src/lib/sample-state/formatters.test.ts diff --git a/ergon-dashboard/src/lib/run-state/formatters.ts b/ergon-dashboard/src/lib/sample-state/formatters.ts similarity index 100% rename from ergon-dashboard/src/lib/run-state/formatters.ts rename to ergon-dashboard/src/lib/sample-state/formatters.ts diff --git a/ergon-dashboard/src/lib/run-state/hydrate.ts b/ergon-dashboard/src/lib/sample-state/hydrate.ts similarity index 95% rename from ergon-dashboard/src/lib/run-state/hydrate.ts rename to ergon-dashboard/src/lib/sample-state/hydrate.ts index 9279a078d..eb5dcde8c 100644 --- a/ergon-dashboard/src/lib/run-state/hydrate.ts +++ b/ergon-dashboard/src/lib/sample-state/hydrate.ts @@ -8,7 +8,7 @@ import type { TaskEvaluationState, TaskState, TaskStatus, - WorkflowRunState, + SampleWorkspaceState, } from "@/lib/types"; import { compareContextEvents, contextPartToUiPayload } from "./contextEvents"; @@ -80,7 +80,7 @@ function deserializeContextEvents(data: RunSnapshot): Map ({ id: String(event.id ?? ""), - runId: String(event.runId ?? data.id), + sampleId: String(event.sampleId ?? data.id), taskExecutionId: String(event.taskExecutionId ?? ""), taskId: String(event.taskId ?? taskId), workerBindingKey: String(event.workerBindingKey ?? ""), @@ -96,14 +96,14 @@ function deserializeContextEvents(data: RunSnapshot): Map [taskId, deserializeTask(task)]), ), diff --git a/ergon-dashboard/src/lib/run-state/index.ts b/ergon-dashboard/src/lib/sample-state/index.ts similarity index 100% rename from ergon-dashboard/src/lib/run-state/index.ts rename to ergon-dashboard/src/lib/sample-state/index.ts diff --git a/ergon-dashboard/src/lib/run-state/metrics.ts b/ergon-dashboard/src/lib/sample-state/metrics.ts similarity index 85% rename from ergon-dashboard/src/lib/run-state/metrics.ts rename to ergon-dashboard/src/lib/sample-state/metrics.ts index 2c599cceb..67f979652 100644 --- a/ergon-dashboard/src/lib/run-state/metrics.ts +++ b/ergon-dashboard/src/lib/sample-state/metrics.ts @@ -1,7 +1,7 @@ -import { TaskStatus, type TaskState, type WorkflowRunState } from "@/lib/types"; +import { TaskStatus, type TaskState, type SampleWorkspaceState } from "@/lib/types"; export function recalculateTaskMetrics(tasks: Map): Pick< - WorkflowRunState, + SampleWorkspaceState, "completedTasks" | "runningTasks" | "failedTasks" | "cancelledTasks" > { let completedTasks = 0; diff --git a/ergon-dashboard/src/lib/run-state/reducers.ts b/ergon-dashboard/src/lib/sample-state/reducers.ts similarity index 93% rename from ergon-dashboard/src/lib/run-state/reducers.ts rename to ergon-dashboard/src/lib/sample-state/reducers.ts index 366ee7a70..b1085ab4c 100644 --- a/ergon-dashboard/src/lib/run-state/reducers.ts +++ b/ergon-dashboard/src/lib/sample-state/reducers.ts @@ -1,4 +1,4 @@ -import { inferTrigger } from "@/lib/runEvents"; +import { inferTrigger } from "@/lib/sampleEvents"; import { type SandboxCommandState, type SandboxState, @@ -6,13 +6,13 @@ import { type ExecutionAttemptState, type TaskState, type TaskTransitionRecord, - type WorkflowRunState, + type SampleWorkspaceState, } from "@/lib/types"; import { recalculateTaskMetrics } from "./metrics"; export interface TaskStatusChanged { - runId: string; + sampleId: string; taskId: string; status: TaskStatus; timestamp: string; @@ -33,7 +33,7 @@ function isTerminalStatus(status: TaskStatus): boolean { } function updateExecutionsForStatus( - run: WorkflowRunState, + run: SampleWorkspaceState, task: TaskState, event: TaskStatusChanged, ): ExecutionAttemptState[] { @@ -135,10 +135,10 @@ function updateTaskForStatus(task: TaskState, event: TaskStatusChanged): TaskSta } export function applyTaskStatusChanged( - run: WorkflowRunState, + run: SampleWorkspaceState, event: TaskStatusChanged, -): WorkflowRunState { - if (event.runId !== run.id) return run; +): SampleWorkspaceState { + if (event.sampleId !== run.id) return run; const task = run.tasks.get(event.taskId); if (!task) return run; @@ -158,10 +158,10 @@ export function applyTaskStatusChanged( } export function applySandboxCreated( - run: WorkflowRunState, + run: SampleWorkspaceState, sandbox: SandboxState, pendingCommands: SandboxCommandState[] = [], -): WorkflowRunState { +): SampleWorkspaceState { const sandboxesByTask = new Map(run.sandboxesByTask); const seenCommands = new Set(); const commands = [...pendingCommands, ...sandbox.commands].filter((command) => { @@ -178,10 +178,10 @@ export function applySandboxCreated( } export function applySandboxCommand( - run: WorkflowRunState, + run: SampleWorkspaceState, taskId: string, command: SandboxCommandState, -): WorkflowRunState { +): SampleWorkspaceState { const sandbox = run.sandboxesByTask.get(taskId); if (!sandbox) return run; @@ -194,11 +194,11 @@ export function applySandboxCommand( } export function applySandboxClosed( - run: WorkflowRunState, + run: SampleWorkspaceState, taskId: string, reason: string, timestamp: string, -): WorkflowRunState { +): SampleWorkspaceState { const sandbox = run.sandboxesByTask.get(taskId); if (!sandbox) return run; diff --git a/ergon-dashboard/src/lib/run-state/serialize.ts b/ergon-dashboard/src/lib/sample-state/serialize.ts similarity index 74% rename from ergon-dashboard/src/lib/run-state/serialize.ts rename to ergon-dashboard/src/lib/sample-state/serialize.ts index 59c5bc800..079f181fd 100644 --- a/ergon-dashboard/src/lib/run-state/serialize.ts +++ b/ergon-dashboard/src/lib/sample-state/serialize.ts @@ -1,8 +1,8 @@ -import type { SerializedWorkflowRunState, WorkflowRunState } from "@/lib/types"; +import type { SerializedSampleWorkspaceState, SampleWorkspaceState } from "@/lib/types"; import { serializeContextEvent } from "./contextEvents"; -export function serializeRunSnapshot(run: WorkflowRunState): SerializedWorkflowRunState { +export function serializeRunSnapshot(run: SampleWorkspaceState): SerializedSampleWorkspaceState { return { ...run, tasks: Object.fromEntries(run.tasks.entries()), @@ -16,7 +16,7 @@ export function serializeRunSnapshot(run: WorkflowRunState): SerializedWorkflowR events.map(serializeContextEvent), ]), ), - } as unknown as SerializedWorkflowRunState; + } as unknown as SerializedSampleWorkspaceState; } export const serializeRunState = serializeRunSnapshot; diff --git a/ergon-dashboard/src/lib/runEvents.ts b/ergon-dashboard/src/lib/sampleEvents.ts similarity index 97% rename from ergon-dashboard/src/lib/runEvents.ts rename to ergon-dashboard/src/lib/sampleEvents.ts index e0f9504e6..432962f14 100644 --- a/ergon-dashboard/src/lib/runEvents.ts +++ b/ergon-dashboard/src/lib/sampleEvents.ts @@ -8,7 +8,7 @@ * different panel. The `RunEvent` union gives us one chronologically sortable * shape that the UnifiedEventStream and the RunTimeline can both consume. * - * Construction is **derived** on the client from the `WorkflowRunState` we already + * Construction is **derived** on the client from the `SampleWorkspaceState` we already * assemble — no new backend contract is introduced. */ @@ -21,7 +21,7 @@ import type { TaskTransitionRecord, TaskTrigger, UnhandledMutationRecord, - WorkflowRunState, + SampleWorkspaceState, ContextEventState, TaskEvaluationState, CommunicationThreadState, @@ -220,13 +220,13 @@ function messagePreview(msg: CommunicationMessageState): string { } /** - * Build the flat, chronologically-sorted RunEvent log from a WorkflowRunState. + * Build the flat, chronologically-sorted RunEvent log from a SampleWorkspaceState. * * Consumers should treat this as a *pure derivation* of state — it is cheap - * enough to recompute on every render of the stream, but the RunWorkspacePage + * enough to recompute on every render of the stream, but the SampleWorkspacePage * memoizes it on `runState` identity. */ -export function buildRunEvents(run: WorkflowRunState | null): RunEvent[] { +export function buildRunEvents(run: SampleWorkspaceState | null): RunEvent[] { if (!run) return []; const events: RunEvent[] = []; diff --git a/ergon-dashboard/src/lib/runState.ts b/ergon-dashboard/src/lib/sampleState.ts similarity index 68% rename from ergon-dashboard/src/lib/runState.ts rename to ergon-dashboard/src/lib/sampleState.ts index 28584cc93..582774778 100644 --- a/ergon-dashboard/src/lib/runState.ts +++ b/ergon-dashboard/src/lib/sampleState.ts @@ -8,5 +8,5 @@ export { serializeRunSnapshot, serializeRunState, uiPayloadToContextPart, -} from "@/lib/run-state"; -export type { DashboardRunState, WireRunSnapshot } from "@/lib/run-state"; +} from "@/lib/sample-state"; +export type { DashboardRunState, WireRunSnapshot } from "@/lib/sample-state"; diff --git a/ergon-dashboard/src/lib/server-data/experiments.ts b/ergon-dashboard/src/lib/server-data/experiments.ts index 362da7462..7425333b6 100644 --- a/ergon-dashboard/src/lib/server-data/experiments.ts +++ b/ergon-dashboard/src/lib/server-data/experiments.ts @@ -5,7 +5,7 @@ import { getHarnessExperiment } from "@/lib/testing/dashboardHarness"; import { normalizeRunMetricPoints, type RunMetricPoint, -} from "@/components/experiments/runMetricExplorerModel"; +} from "@/components/experiments/sampleRunMetricExplorerModel"; import { backendUnavailable, type ServerDataResult } from "./responses"; diff --git a/ergon-dashboard/src/lib/server-data/runs.ts b/ergon-dashboard/src/lib/server-data/samples.ts similarity index 93% rename from ergon-dashboard/src/lib/server-data/runs.ts rename to ergon-dashboard/src/lib/server-data/samples.ts index 2837a5d77..492ba962c 100644 --- a/ergon-dashboard/src/lib/server-data/runs.ts +++ b/ergon-dashboard/src/lib/server-data/samples.ts @@ -54,7 +54,7 @@ export async function loadRunList( if (filters.experiment) searchParams.set("experiment", filters.experiment); try { - const response = await fetchErgonApi(`/runs?${searchParams.toString()}`); + const response = await fetchErgonApi(`/samples?${searchParams.toString()}`); const body = await response.json(); if (response.ok) { return { @@ -70,16 +70,16 @@ export async function loadRunList( } } -export async function loadRunSnapshot(runId: string): Promise> { +export async function loadRunSnapshot(sampleId: string): Promise> { if (config.enableTestHarness) { - const run = getHarnessRun(runId); + const run = getHarnessRun(sampleId); if (run !== null) { return { ok: true, data: parseRunSnapshot(run), status: 200, source: "harness" }; } } try { - const response = await fetchErgonApi(`/runs/${runId}`); + const response = await fetchErgonApi(`/samples/${sampleId}`); const body = await response.json(); if (response.ok) { return { @@ -91,7 +91,7 @@ export async function loadRunSnapshot(runId: string): Promise ({ - runId: r.id, + sampleId: r.id, name: r.name, status: r.status, startedAt: r.startedAt, @@ -91,33 +91,33 @@ export function initSocketServer(httpServer: HttpServer): TypedServer { }); // Request full state for a specific run (for run detail page) - socket.on("request:run", (runId: string) => { - console.log(`[Socket.io] Client ${socket.id} requested full state for run ${runId}`); + socket.on("request:run", (sampleId: string) => { + console.log(`[Socket.io] Client ${socket.id} requested full state for run ${sampleId}`); // Debug: log all runs in store const allRuns = store.getAllRuns(); console.log(`[Socket.io] Store contains ${allRuns.length} runs:`, allRuns.map(r => r.id)); - const run = store.getRun(runId); + const run = store.getRun(sampleId); if (run) { console.log(`[Socket.io] Found run! Sending full state (${run.tasks.size} tasks, status: ${run.status})`); socket.emit("sync:run", serializeRunState(run)); } else { - console.log(`[Socket.io] Run ${runId} NOT FOUND in store. Available runs: ${allRuns.map(r => r.id.substring(0, 8)).join(', ') || 'none'}`); + console.log(`[Socket.io] Run ${sampleId} NOT FOUND in store. Available runs: ${allRuns.map(r => r.id.substring(0, 8)).join(', ') || 'none'}`); socket.emit("sync:run", null); } }); // Client subscribes to a run's updates - socket.on("subscribe", (runId: string) => { - const room = `run:${runId}`; + socket.on("subscribe", (sampleId: string) => { + const room = `run:${sampleId}`; socket.join(room); console.log(`[Socket.io] ${socket.id} subscribed to ${room}`); }); // Client unsubscribes from a run's updates - socket.on("unsubscribe", (runId: string) => { - const room = `run:${runId}`; + socket.on("unsubscribe", (sampleId: string) => { + const room = `run:${sampleId}`; socket.leave(room); console.log(`[Socket.io] ${socket.id} unsubscribed from ${room}`); }); @@ -146,13 +146,13 @@ export function getSocketServer(): TypedServer | null { /** * Broadcast a new run started to all connected clients. */ -export function broadcastRunStarted(runId: string, name: string): void { +export function broadcastRunStarted(sampleId: string, name: string): void { const io = getIO(); - console.log(`[Socket.io] broadcastRunStarted called - io is ${io ? 'initialized' : 'NULL'}, runId: ${runId}, name: ${name}`); + console.log(`[Socket.io] broadcastRunStarted called - io is ${io ? 'initialized' : 'NULL'}, sampleId: ${sampleId}, name: ${name}`); if (io) { const socketsCount = io.sockets.sockets.size; console.log(`[Socket.io] Broadcasting run:started to ${socketsCount} connected clients`); - io.emit("run:started", { runId, name }); + io.emit("run:started", { sampleId, name }); } else { console.warn("[Socket.io] WARNING: Cannot broadcast run:started - io is null! Check that initSocketServer was called."); } @@ -162,7 +162,7 @@ export function broadcastRunStarted(runId: string, name: string): void { * Broadcast run completed to subscribers of that run. */ export function broadcastRunCompleted( - runId: string, + sampleId: string, status: "completed" | "failed", completedAt: string, durationSeconds: number, @@ -172,7 +172,7 @@ export function broadcastRunCompleted( const io = getIO(); // Broadcast to all clients (not just room subscribers) so the run list updates io?.emit("run:completed", { - runId, + sampleId, status, completedAt, durationSeconds, @@ -185,7 +185,7 @@ export function broadcastRunCompleted( * Broadcast task status change to subscribers of that run. */ export function broadcastTaskStatus( - runId: string, + sampleId: string, taskId: string, status: TaskStatus, timestamp: string, @@ -193,8 +193,8 @@ export function broadcastTaskStatus( assignedWorkerSlug: string | null ): void { const io = getIO(); - io?.to(`run:${runId}`).emit("task:status", { - runId, + io?.to(`run:${sampleId}`).emit("task:status", { + sampleId, taskId, status, timestamp, @@ -207,76 +207,76 @@ export function broadcastTaskStatus( * Broadcast new resource to subscribers of that run. */ export function broadcastResourceNew( - runId: string, + sampleId: string, resource: ResourceState ): void { const io = getIO(); - io?.to(`run:${runId}`).emit("resource:new", { runId, resource }); + io?.to(`run:${sampleId}`).emit("resource:new", { sampleId, resource }); } /** * Broadcast sandbox created to subscribers of that run. */ export function broadcastSandboxCreated( - runId: string, + sampleId: string, sandbox: SandboxState ): void { const io = getIO(); - io?.to(`run:${runId}`).emit("sandbox:created", { runId, sandbox }); + io?.to(`run:${sampleId}`).emit("sandbox:created", { sampleId, sandbox }); } /** * Broadcast sandbox command to subscribers of that run. */ export function broadcastSandboxCommand( - runId: string, + sampleId: string, taskId: string, command: SandboxCommandState ): void { const io = getIO(); - io?.to(`run:${runId}`).emit("sandbox:command", { runId, taskId, command }); + io?.to(`run:${sampleId}`).emit("sandbox:command", { sampleId, taskId, command }); } /** * Broadcast sandbox closed to subscribers of that run. */ export function broadcastSandboxClosed( - runId: string, + sampleId: string, taskId: string, reason: string, timestamp: string, ): void { const io = getIO(); - io?.to(`run:${runId}`).emit("sandbox:closed", { runId, taskId, reason, timestamp }); + io?.to(`run:${sampleId}`).emit("sandbox:closed", { sampleId, taskId, reason, timestamp }); } export function broadcastThreadMessage( data: DashboardThreadMessageCreatedData ): void { const io = getIO(); - io?.to(`run:${data.run_id}`).emit("thread:message", data); + io?.to(`run:${data.sample_id}`).emit("thread:message", data); } export function broadcastTaskEvaluation( data: DashboardTaskEvaluationUpdatedData ): void { const io = getIO(); - io?.to(`run:${data.run_id}`).emit("task:evaluation", data); + io?.to(`run:${data.sample_id}`).emit("task:evaluation", data); } export function broadcastGraphMutation( - runId: string, + sampleId: string, mutation: DashboardGraphMutationData, ): void { const io = getIO(); - io?.to(`run:${runId}`).emit("graph:mutation", { runId, mutation }); + io?.to(`run:${sampleId}`).emit("graph:mutation", { sampleId, mutation }); } export function broadcastContextEvent( - runId: string, + sampleId: string, taskId: string, event: ContextEventState, ): void { const io = getIO(); - io?.to(`run:${runId}`).emit("context:event", { runId, taskId, event }); + io?.to(`run:${sampleId}`).emit("context:event", { sampleId, taskId, event }); } diff --git a/ergon-dashboard/src/lib/state/store.ts b/ergon-dashboard/src/lib/state/store.ts index 639a5fa3d..b5b0f3cc6 100644 --- a/ergon-dashboard/src/lib/state/store.ts +++ b/ergon-dashboard/src/lib/state/store.ts @@ -21,18 +21,18 @@ import { SandboxState, SandboxCommandState, TaskEvaluationState, - WorkflowRunState, + SampleWorkspaceState, } from "../types"; import { applyGraphMutation as reduceGraphMutation } from "@/features/graph/state/graphMutationReducer"; import type { DashboardGraphMutationData } from "@/lib/contracts/events"; import type { RunSnapshot } from "@/lib/contracts/rest"; -import { hydrateRunSnapshot } from "@/lib/run-state/hydrate"; +import { hydrateRunSnapshot } from "@/lib/sample-state/hydrate"; import { applySandboxClosed, applySandboxCommand, applySandboxCreated, applyTaskStatusChanged, -} from "@/lib/run-state/reducers"; +} from "@/lib/sample-state/reducers"; // Extend global to store DashboardStore instance across module loads declare global { @@ -41,7 +41,7 @@ declare global { } class DashboardStore { - private runs: Map = new Map(); + private runs: Map = new Map(); private pendingSandboxCommands: Map> = new Map(); @@ -49,42 +49,42 @@ class DashboardStore { // Queries // ========================================================================== - getRun(runId: string): WorkflowRunState | undefined { - return this.runs.get(runId); + getRun(sampleId: string): SampleWorkspaceState | undefined { + return this.runs.get(sampleId); } - getAllRuns(): WorkflowRunState[] { + getAllRuns(): SampleWorkspaceState[] { return Array.from(this.runs.values()); } - getActiveRuns(): WorkflowRunState[] { + getActiveRuns(): SampleWorkspaceState[] { return this.getAllRuns().filter( (r) => r.status === "pending" || r.status === "executing" || r.status === "evaluating", ); } - getRecentRuns(limit: number = 10): WorkflowRunState[] { + getRecentRuns(limit: number = 10): SampleWorkspaceState[] { return this.getAllRuns() .sort((a, b) => b.startedAt.localeCompare(a.startedAt)) .slice(0, limit); } - getTask(runId: string, taskId: string): TaskState | undefined { - return this.runs.get(runId)?.tasks.get(taskId); + getTask(sampleId: string, taskId: string): TaskState | undefined { + return this.runs.get(sampleId)?.tasks.get(taskId); } - getTasksAtLevel(runId: string, level: number): TaskState[] { - const run = this.runs.get(runId); + getTasksAtLevel(sampleId: string, level: number): TaskState[] { + const run = this.runs.get(sampleId); if (!run) return []; return Array.from(run.tasks.values()).filter((t) => t.level === level); } - getResourcesForTask(runId: string, taskId: string): ResourceState[] { - return this.runs.get(runId)?.resourcesByTask.get(taskId) ?? []; + getResourcesForTask(sampleId: string, taskId: string): ResourceState[] { + return this.runs.get(sampleId)?.resourcesByTask.get(taskId) ?? []; } - getSandboxForTask(runId: string, taskId: string): SandboxState | undefined { - return this.runs.get(runId)?.sandboxesByTask.get(taskId); + getSandboxForTask(sampleId: string, taskId: string): SandboxState | undefined { + return this.runs.get(sampleId)?.sandboxesByTask.get(taskId); } reset(): void { @@ -92,7 +92,7 @@ class DashboardStore { this.pendingSandboxCommands.clear(); } - seedRun(run: WorkflowRunState): void { + seedRun(run: SampleWorkspaceState): void { this.runs.set(run.id, run); } @@ -104,17 +104,17 @@ class DashboardStore { * Initialize a new workflow run from a workflow.started event. */ initializeRun( - runId: string, + sampleId: string, definitionId: string, name: string, snapshot: RunSnapshot, startedAt: string, totalTasks: number, totalLeafTasks: number - ): WorkflowRunState { + ): SampleWorkspaceState { const hydrated = hydrateRunSnapshot({ ...snapshot, - id: runId, + id: sampleId, definitionId, name, status: "executing", @@ -122,7 +122,7 @@ class DashboardStore { totalTasks, totalLeafTasks, }); - const run: WorkflowRunState = { + const run: SampleWorkspaceState = { ...hydrated, status: "executing", completedAt: null, @@ -131,7 +131,7 @@ class DashboardStore { error: null, }; - this.runs.set(runId, run); + this.runs.set(sampleId, run); return run; } @@ -139,14 +139,14 @@ class DashboardStore { * Mark a workflow run as completed or failed. */ completeRun( - runId: string, + sampleId: string, status: "completed" | "failed", completedAt: string, durationSeconds: number, finalScore: number | null, error: string | null ): void { - const run = this.runs.get(runId); + const run = this.runs.get(sampleId); if (!run) return; run.status = status; @@ -160,20 +160,20 @@ class DashboardStore { * Update a task's status from a task.status_changed event. */ updateTaskStatus( - runId: string, + sampleId: string, taskId: string, newStatus: TaskStatus, timestamp: string, assignedWorkerId?: string | null, assignedWorkerSlug?: string | null ): void { - const run = this.runs.get(runId); + const run = this.runs.get(sampleId); if (!run) return; this.runs.set( - runId, + sampleId, applyTaskStatusChanged(run, { - runId, + sampleId, taskId, status: newStatus, timestamp, @@ -186,8 +186,8 @@ class DashboardStore { /** * Add a resource from a resource.published event. */ - addResource(runId: string, resource: ResourceState): void { - const run = this.runs.get(runId); + addResource(sampleId: string, resource: ResourceState): void { + const run = this.runs.get(sampleId); if (!run) return; const taskResources = run.resourcesByTask.get(resource.taskId) ?? []; @@ -195,8 +195,8 @@ class DashboardStore { run.resourcesByTask.set(resource.taskId, taskResources); } - upsertThread(runId: string, thread: CommunicationThreadState): void { - const run = this.runs.get(runId); + upsertThread(sampleId: string, thread: CommunicationThreadState): void { + const run = this.runs.get(sampleId); if (!run) return; const existingIndex = run.threads.findIndex((candidate) => candidate.id === thread.id); @@ -207,8 +207,8 @@ class DashboardStore { } } - addContextEvent(runId: string, taskId: string, event: ContextEventState): void { - const run = this.runs.get(runId); + addContextEvent(sampleId: string, taskId: string, event: ContextEventState): void { + const run = this.runs.get(sampleId); if (!run) return; const existing = run.contextEventsByTask.get(taskId) ?? []; if (existing.some((e) => e.id === event.id)) return; // deduplicate @@ -218,8 +218,8 @@ class DashboardStore { ); } - upsertEvaluation(runId: string, taskId: string | null, evaluation: TaskEvaluationState): void { - const run = this.runs.get(runId); + upsertEvaluation(sampleId: string, taskId: string | null, evaluation: TaskEvaluationState): void { + const run = this.runs.get(sampleId); if (!run) return; run.evaluationsByTask.set(taskId ?? "__run__", evaluation); @@ -229,18 +229,18 @@ class DashboardStore { * Create or update a sandbox from sandbox.created event. */ createSandbox( - runId: string, + sampleId: string, taskId: string, sandboxId: string, template: string | null, timeoutMinutes: number, timestamp: string ): void { - const run = this.runs.get(runId); + const run = this.runs.get(sampleId); if (!run) return; const pendingCommands = - this.pendingSandboxCommands.get(runId)?.get(taskId) ?? []; + this.pendingSandboxCommands.get(sampleId)?.get(taskId) ?? []; const sandbox: SandboxState = { sandboxId, @@ -254,13 +254,13 @@ class DashboardStore { commands: pendingCommands, }; - this.runs.set(runId, applySandboxCreated(run, sandbox)); + this.runs.set(sampleId, applySandboxCreated(run, sandbox)); - const pendingByTask = this.pendingSandboxCommands.get(runId); + const pendingByTask = this.pendingSandboxCommands.get(sampleId); if (pendingByTask) { pendingByTask.delete(taskId); if (pendingByTask.size === 0) { - this.pendingSandboxCommands.delete(runId); + this.pendingSandboxCommands.delete(sampleId); } } } @@ -269,54 +269,54 @@ class DashboardStore { * Add a command to a sandbox from sandbox.command event. */ addSandboxCommand( - runId: string, + sampleId: string, taskId: string, command: SandboxCommandState ): void { - const run = this.runs.get(runId); + const run = this.runs.get(sampleId); const sandbox = run?.sandboxesByTask.get(taskId); if (!run) return; if (!sandbox) { const pendingByTask = - this.pendingSandboxCommands.get(runId) ?? new Map(); + this.pendingSandboxCommands.get(sampleId) ?? new Map(); const pendingCommands = pendingByTask.get(taskId) ?? []; pendingCommands.push(command); pendingByTask.set(taskId, pendingCommands); - this.pendingSandboxCommands.set(runId, pendingByTask); + this.pendingSandboxCommands.set(sampleId, pendingByTask); return; } - this.runs.set(runId, applySandboxCommand(run, taskId, command)); + this.runs.set(sampleId, applySandboxCommand(run, taskId, command)); } /** * Close a sandbox from sandbox.closed event. */ closeSandbox( - runId: string, + sampleId: string, taskId: string, reason: string, timestamp: string ): void { - const run = this.runs.get(runId); + const run = this.runs.get(sampleId); if (!run) return; - this.runs.set(runId, applySandboxClosed(run, taskId, reason, timestamp)); + this.runs.set(sampleId, applySandboxClosed(run, taskId, reason, timestamp)); } - applyGraphMutation(runId: string, mutation: DashboardGraphMutationData): void { - const run = this.runs.get(runId); + applyGraphMutation(sampleId: string, mutation: DashboardGraphMutationData): void { + const run = this.runs.get(sampleId); if (!run) return; const updated = reduceGraphMutation(run, mutation); - this.runs.set(runId, updated); + this.runs.set(sampleId, updated); } /** * Remove old completed runs to prevent memory growth. * Keeps the most recent N runs. */ - pruneOldRuns(keepCount: number = config.maxRunsToKeep): void { + pruneOldSamples(keepCount: number = config.maxSamplesToKeep): void { const runs = this.getAllRuns(); if (runs.length <= keepCount) return; diff --git a/ergon-dashboard/src/lib/testing/dashboardHarness.ts b/ergon-dashboard/src/lib/testing/dashboardHarness.ts index 393e530b4..a8dc8de0a 100644 --- a/ergon-dashboard/src/lib/testing/dashboardHarness.ts +++ b/ergon-dashboard/src/lib/testing/dashboardHarness.ts @@ -11,11 +11,11 @@ import { CommunicationThreadState, ContextEventState, ExperimentDetail, - SerializedWorkflowRunState, + SerializedSampleWorkspaceState, TaskEvaluationState, TaskStatus, } from "@/lib/types"; -import { deserializeRunState, serializeRunState } from "@/lib/runState"; +import { deserializeRunState, serializeRunState } from "@/lib/sampleState"; declare global { // eslint-disable-next-line no-var @@ -30,7 +30,7 @@ declare global { export interface DashboardHarnessSeedPayload { experimentDetails?: Record; - runs?: SerializedWorkflowRunState[]; + runs?: SerializedSampleWorkspaceState[]; mutations?: Record; } @@ -79,22 +79,22 @@ export function getHarnessExperiment(definitionId: string): ExperimentDetail | n return getHarnessState().experimentDetails[definitionId] ?? null; } -export function getHarnessRun(runId: string): SerializedWorkflowRunState | null { +export function getHarnessRun(sampleId: string): SerializedSampleWorkspaceState | null { requireHarnessEnabled(); - if (!getHarnessState().seededRunIds.has(runId)) { + if (!getHarnessState().seededRunIds.has(sampleId)) { return null; } - const run = store.getRun(runId); + const run = store.getRun(sampleId); return run ? serializeRunState(run) : null; } -export function getHarnessRunMutations(runId: string): unknown[] | null { +export function getHarnessRunMutations(sampleId: string): unknown[] | null { requireHarnessEnabled(); - return getHarnessState().mutationsByRun[runId] ?? null; + return getHarnessState().mutationsByRun[sampleId] ?? null; } export function emitHarnessRunCompleted(data: { - runId: string; + sampleId: string; status: "completed" | "failed"; durationSeconds: number; finalScore: number | null; @@ -102,7 +102,7 @@ export function emitHarnessRunCompleted(data: { }): void { requireHarnessEnabled(); store.completeRun( - data.runId, + data.sampleId, data.status, new Date().toISOString(), data.durationSeconds, @@ -110,7 +110,7 @@ export function emitHarnessRunCompleted(data: { data.error, ); broadcastRunCompleted( - data.runId, + data.sampleId, data.status, new Date().toISOString(), data.durationSeconds, @@ -121,7 +121,7 @@ export function emitHarnessRunCompleted(data: { } export function emitHarnessTaskStatus(data: { - runId: string; + sampleId: string; taskId: string; status: TaskStatus; assignedWorkerId?: string | null; @@ -129,7 +129,7 @@ export function emitHarnessTaskStatus(data: { }): void { requireHarnessEnabled(); store.updateTaskStatus( - data.runId, + data.sampleId, data.taskId, data.status, new Date().toISOString(), @@ -137,7 +137,7 @@ export function emitHarnessTaskStatus(data: { data.assignedWorkerName, ); broadcastTaskStatus( - data.runId, + data.sampleId, data.taskId, data.status, new Date().toISOString(), @@ -146,14 +146,14 @@ export function emitHarnessTaskStatus(data: { ); } -export function emitHarnessThreadMessage(runId: string, thread: CommunicationThreadState): void { +export function emitHarnessThreadMessage(sampleId: string, thread: CommunicationThreadState): void { requireHarnessEnabled(); - store.upsertThread(runId, thread); + store.upsertThread(sampleId, thread); const messages = thread.messages ?? []; const message = messages[messages.length - 1]; if (message) { broadcastThreadMessage({ - run_id: runId, + sample_id: sampleId, thread, message, }); @@ -161,24 +161,24 @@ export function emitHarnessThreadMessage(runId: string, thread: CommunicationThr } export function emitHarnessContextEvent( - runId: string, + sampleId: string, taskId: string, event: ContextEventState, ): void { requireHarnessEnabled(); - store.addContextEvent(runId, taskId, event); - broadcastContextEvent(runId, taskId, event); + store.addContextEvent(sampleId, taskId, event); + broadcastContextEvent(sampleId, taskId, event); } export function emitHarnessTaskEvaluation( - runId: string, + sampleId: string, taskId: string | null, evaluation: TaskEvaluationState, ): void { requireHarnessEnabled(); - store.upsertEvaluation(runId, taskId, evaluation); + store.upsertEvaluation(sampleId, taskId, evaluation); broadcastTaskEvaluation({ - run_id: runId, + sample_id: sampleId, task_id: taskId, evaluation, }); diff --git a/ergon-dashboard/src/lib/types.ts b/ergon-dashboard/src/lib/types.ts index 35a0095db..783fe5c25 100644 --- a/ergon-dashboard/src/lib/types.ts +++ b/ergon-dashboard/src/lib/types.ts @@ -9,7 +9,7 @@ import type { RunLifecycleStatus as RestRunLifecycleStatus, RunSnapshot, RunSnapshotMetrics, - RunTaskEvaluation as RestRunTaskEvaluation, + SampleTaskEvaluation as RestSampleTaskEvaluation, } from "@/lib/contracts/rest"; import type { DashboardGraphMutationData as GeneratedDashboardGraphMutationData, @@ -92,8 +92,8 @@ export type DashboardSandboxCommandData = GeneratedDashboardSandboxCommandData; export type DashboardSandboxClosedData = GeneratedDashboardSandboxClosedData; export type CommunicationMessageState = RestRunCommunicationMessage; export type CommunicationThreadState = RestRunCommunicationThread; -export type EvaluationCriterionState = NonNullable[number]; -export type TaskEvaluationState = RestRunTaskEvaluation; +export type EvaluationCriterionState = NonNullable[number]; +export type TaskEvaluationState = RestSampleTaskEvaluation; export type DashboardThreadMessageCreatedData = GeneratedDashboardThreadMessageCreatedData; export type DashboardTaskEvaluationUpdatedData = GeneratedDashboardTaskEvaluationUpdatedData; @@ -290,7 +290,7 @@ export interface UnhandledMutationRecord { * Complete workflow run state. * This is the top-level state object held in the DashboardStore. */ -export interface WorkflowRunState { +export interface SampleWorkspaceState { id: string; definitionId: string; name: string; @@ -354,7 +354,7 @@ export interface WorkflowRunState { * Events sent from server to client via Socket.io. */ export interface ServerToClientEvents { - "run:started": (data: { runId: string; name: string }) => void; + "run:started": (data: { sampleId: string; name: string }) => void; "run:completed": (data: RunCompletedSocketData) => void; "task:status": (data: TaskStatusSocketData) => void; "resource:new": (data: ResourceSocketData) => void; @@ -364,24 +364,24 @@ export interface ServerToClientEvents { "thread:message": (data: DashboardThreadMessageCreatedData) => void; "task:evaluation": (data: DashboardTaskEvaluationUpdatedData) => void; "graph:mutation": (data: GraphMutationSocketData) => void; - "context:event": (data: { runId: string; taskId: string; event: ContextEventState }) => void; + "context:event": (data: { sampleId: string; taskId: string; event: ContextEventState }) => void; // Sync event - sends all current runs to a client on request "sync:runs": (runs: RunListEntry[]) => void; // Sync event - sends full state for a specific run - "sync:run": (run: SerializedWorkflowRunState | null) => void; + "sync:run": (run: SerializedSampleWorkspaceState | null) => void; } /** * Validated run snapshot payload used over REST and Socket.io sync. */ -export type SerializedWorkflowRunState = RunSnapshot; +export type SerializedSampleWorkspaceState = RunSnapshot; /** * Events sent from client to server via Socket.io. */ export interface ClientToServerEvents { - subscribe: (runId: string) => void; - unsubscribe: (runId: string) => void; + subscribe: (sampleId: string) => void; + unsubscribe: (sampleId: string) => void; "request:runs": () => void; - "request:run": (runId: string) => void; + "request:run": (sampleId: string) => void; } diff --git a/ergon-dashboard/src/providers/SocketProvider.tsx b/ergon-dashboard/src/providers/SocketProvider.tsx index cc29aa892..33a7860a1 100644 --- a/ergon-dashboard/src/providers/SocketProvider.tsx +++ b/ergon-dashboard/src/providers/SocketProvider.tsx @@ -24,8 +24,8 @@ interface SocketContextValue { connectionStatus: ConnectionStatus; connectionError: string | null; reconnect: () => void; - subscribe: (runId: string) => void; - unsubscribe: (runId: string) => void; + subscribe: (sampleId: string) => void; + unsubscribe: (sampleId: string) => void; } const SocketContext = createContext(null); @@ -114,15 +114,15 @@ export function SocketProvider({ children }: SocketProviderProps) { } }, [socket]); - const subscribe = useCallback((runId: string) => { + const subscribe = useCallback((sampleId: string) => { if (socket?.connected) { - socket.emit("subscribe", runId); + socket.emit("subscribe", sampleId); } }, [socket]); - const unsubscribe = useCallback((runId: string) => { + const unsubscribe = useCallback((sampleId: string) => { if (socket?.connected) { - socket.emit("unsubscribe", runId); + socket.emit("unsubscribe", sampleId); } }, [socket]); diff --git a/ergon-dashboard/tests/contracts/context-events.contract.test.ts b/ergon-dashboard/tests/contracts/context-events.contract.test.ts index 155436a36..6cbb3d05b 100644 --- a/ergon-dashboard/tests/contracts/context-events.contract.test.ts +++ b/ergon-dashboard/tests/contracts/context-events.contract.test.ts @@ -4,7 +4,7 @@ import test from "node:test"; import { contextPartToUiPayload, uiPayloadToContextPart, -} from "../../src/lib/run-state/contextEvents"; +} from "../../src/lib/sample-state/contextEvents"; test("tool_call context part converts to UI payload", () => { const payload = contextPartToUiPayload({ diff --git a/ergon-dashboard/tests/contracts/contracts.test.ts b/ergon-dashboard/tests/contracts/contracts.test.ts index 4e2be3f40..f4f3b357c 100644 --- a/ergon-dashboard/tests/contracts/contracts.test.ts +++ b/ergon-dashboard/tests/contracts/contracts.test.ts @@ -13,7 +13,7 @@ import { parseTaskStatusSocketData, } from "../../src/lib/contracts/events"; import { parseRunSnapshot } from "../../src/lib/contracts/rest"; -import { deserializeRunState } from "../../src/lib/runState"; +import { deserializeRunState } from "../../src/lib/sampleState"; import { store } from "../../src/lib/state/store"; import { getHarnessRun, @@ -29,7 +29,7 @@ test("run snapshot parser accepts object-map transport", () => { assert.ok(run); const parsed = parseRunSnapshot(run); - assert.equal(parsed.id, FIXTURE_IDS.runId); + assert.equal(parsed.id, FIXTURE_IDS.sampleId); assert.deepEqual(Object.keys(parsed.tasks ?? {}).sort(), [ FIXTURE_IDS.exploreTaskId, FIXTURE_IDS.rootTaskId, @@ -118,9 +118,9 @@ test("dashboard harness only serves explicitly seeded runs", () => { seedDashboardHarness({ runs: [run] }); - const seededRun = getHarnessRun(FIXTURE_IDS.runId); - assert.equal(seededRun?.id, FIXTURE_IDS.runId); - assert.equal(deserializeRunState(seededRun).id, FIXTURE_IDS.runId); + const seededRun = getHarnessRun(FIXTURE_IDS.sampleId); + assert.equal(seededRun?.id, FIXTURE_IDS.sampleId); + assert.equal(deserializeRunState(seededRun).id, FIXTURE_IDS.sampleId); }); test("workflow started event parser validates run snapshots", () => { @@ -129,7 +129,7 @@ test("workflow started event parser validates run snapshots", () => { assert.ok(run); const payload = { - run_id: FIXTURE_IDS.runId, + sample_id: FIXTURE_IDS.sampleId, definition_id: FIXTURE_IDS.definitionId, workflow_name: "parallel", started_at: "2026-03-18T12:00:00.000Z", @@ -168,10 +168,10 @@ test("dashboard nested DTO event parser accepts backend snake-case payloads", () assert.ok(evaluation); const parsedThread = parseDashboardThreadMessageCreatedData({ - run_id: FIXTURE_IDS.runId, + sample_id: FIXTURE_IDS.sampleId, thread: { id: thread.id, - run_id: thread.runId, + sample_id: thread.sampleId, task_id: thread.taskId, topic: thread.topic, summary: "Leaf workers report completion artifacts and probe exit status.", @@ -185,7 +185,7 @@ test("dashboard nested DTO event parser accepts backend snake-case payloads", () id: message.id, thread_id: message.threadId, thread_topic: message.threadTopic, - run_id: message.runId, + sample_id: message.sampleId, task_id: message.taskId, from_agent_id: message.fromAgentId, to_agent_id: message.toAgentId, @@ -201,11 +201,11 @@ test("dashboard nested DTO event parser accepts backend snake-case payloads", () ); const parsedEvaluation = parseDashboardTaskEvaluationUpdatedData({ - run_id: FIXTURE_IDS.runId, + sample_id: FIXTURE_IDS.sampleId, task_id: FIXTURE_IDS.solveTaskNodeUuid, evaluation: { id: evaluation.id, - run_id: evaluation.runId, + sample_id: evaluation.sampleId, task_id: evaluation.taskId, evaluator_name: evaluation.evaluatorName, aggregation_rule: evaluation.aggregationRule, @@ -228,7 +228,7 @@ test("dashboard graph mutation parser accepts backend wrapped mutation event", ( const parsed = parseDashboardGraphMutationData({ mutation: { id: "77777777-7777-4777-8777-777777777777", - run_id: FIXTURE_IDS.runId, + sample_id: FIXTURE_IDS.sampleId, sequence: 4, mutation_type: "node.added", target_type: "node", @@ -244,7 +244,7 @@ test("dashboard graph mutation parser accepts backend wrapped mutation event", ( }, }); - assert.equal(parsed.run_id, FIXTURE_IDS.runId); + assert.equal(parsed.sample_id, FIXTURE_IDS.sampleId); assert.equal(parsed.sequence, 4); assert.equal(parsed.target_id, FIXTURE_IDS.solveTaskNodeUuid); assert.equal(parsed.created_at, "2026-03-18T12:00:14.000000Z"); @@ -254,7 +254,7 @@ test("dashboard graph mutation parser preserves canonical edge task ids", () => const parsed = parseDashboardGraphMutationData({ mutation: { id: "77777777-7777-4777-8777-777777777777", - run_id: FIXTURE_IDS.runId, + sample_id: FIXTURE_IDS.sampleId, sequence: 5, mutation_type: "edge.added", target_type: "edge", @@ -281,7 +281,7 @@ test("dashboard graph mutation parser preserves canonical edge task ids", () => test("dashboard context event parser accepts backend context part payloads", () => { const parsed = parseDashboardContextEventData({ id: "88888888-8888-4888-8888-888888888888", - run_id: FIXTURE_IDS.runId, + sample_id: FIXTURE_IDS.sampleId, task_execution_id: "99999999-9999-4999-8999-999999999999", task_id: FIXTURE_IDS.solveTaskNodeUuid, worker_binding_key: "swebench-smoke-worker", @@ -318,7 +318,7 @@ test("dashboard context event parser accepts backend context part payloads", () test("socket task status parser rejects malformed payloads", () => { assert.throws(() => parseTaskStatusSocketData({ - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, taskId: FIXTURE_IDS.solveTaskId, timestamp: "2026-03-18T12:00:14.000Z", assignedWorkerId: FIXTURE_IDS.workerId, diff --git a/ergon-dashboard/tests/contracts/frontend-regressions.contract.test.ts b/ergon-dashboard/tests/contracts/frontend-regressions.contract.test.ts index a6e95962e..9a53654b9 100644 --- a/ergon-dashboard/tests/contracts/frontend-regressions.contract.test.ts +++ b/ergon-dashboard/tests/contracts/frontend-regressions.contract.test.ts @@ -9,7 +9,7 @@ test("experiment detail does not pass server functions into client components", }); test("run display state keeps live mode separate from graph sequence zero", () => { - const source = readFileSync("src/components/run/useRunDisplayState.ts", "utf8"); + const source = readFileSync("src/components/sample/useSampleDisplayState.ts", "utf8"); assert.doesNotMatch(source, /snapshotSequence\s*\?\?\s*0/); }); @@ -17,12 +17,12 @@ test("run display state keeps live mode separate from graph sequence zero", () = test("experiment detail bottom run table exposes each row as run navigation", () => { const source = readFileSync("src/app/experiments/[definitionId]/page.tsx", "utf8"); - assert.match(source, /data-testid=\{`experiment-run-row-\$\{point\.runId\}`\}/); - assert.match(source, /href=\{runHref\(point\.runId\)\}/); + assert.match(source, /data-testid=\{`experiment-run-row-\$\{point\.sampleId\}`\}/); + assert.match(source, /href=\{runHref\(point\.sampleId\)\}/); }); test("run workspace can collapse the bottom activity timeline", () => { - const source = readFileSync("src/components/run/RunWorkspacePage.tsx", "utf8"); + const source = readFileSync("src/components/sample/SampleWorkspacePage.tsx", "utf8"); assert.match(source, /const \[isTimelineOpen, setIsTimelineOpen\] = useState\(true\)/); assert.match(source, /data-testid="activity-timeline-toggle"/); @@ -30,7 +30,7 @@ test("run workspace can collapse the bottom activity timeline", () => { }); test("run workspace keeps replay arrows available outside the timeline panel", () => { - const source = readFileSync("src/components/run/RunWorkspacePage.tsx", "utf8"); + const source = readFileSync("src/components/sample/SampleWorkspacePage.tsx", "utf8"); assert.match(source, /data-testid="replay-step-previous"/); assert.match(source, /data-testid="replay-step-next"/); @@ -39,7 +39,7 @@ test("run workspace keeps replay arrows available outside the timeline panel", ( }); test("run workspace does not render the task inspection placeholder", () => { - const source = readFileSync("src/components/run/RunWorkspacePage.tsx", "utf8"); + const source = readFileSync("src/components/sample/SampleWorkspacePage.tsx", "utf8"); assert.doesNotMatch(source, /Task inspection/); assert.doesNotMatch(source, /Click node/); @@ -47,7 +47,7 @@ test("run workspace does not render the task inspection placeholder", () => { }); test("run workspace links back to the owning experiment detail", () => { - const source = readFileSync("src/components/run/RunWorkspacePage.tsx", "utf8"); + const source = readFileSync("src/components/sample/SampleWorkspacePage.tsx", "utf8"); assert.match(source, /const experimentHref = runState\?\.definitionId \? `\/experiments\/\$\{runState\.definitionId\}` : "\/experiments"/); assert.match(source, /href=\{experimentHref\}/); diff --git a/ergon-dashboard/tests/contracts/run-state-roundtrip.contract.test.ts b/ergon-dashboard/tests/contracts/sample-state-roundtrip.contract.test.ts similarity index 92% rename from ergon-dashboard/tests/contracts/run-state-roundtrip.contract.test.ts rename to ergon-dashboard/tests/contracts/sample-state-roundtrip.contract.test.ts index 6e33df88c..099c6b1dd 100644 --- a/ergon-dashboard/tests/contracts/run-state-roundtrip.contract.test.ts +++ b/ergon-dashboard/tests/contracts/sample-state-roundtrip.contract.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { parseRunSnapshot } from "../../src/lib/contracts/rest"; -import { hydrateRunSnapshot, serializeRunSnapshot } from "../../src/lib/run-state"; +import { hydrateRunSnapshot, serializeRunSnapshot } from "../../src/lib/sample-state"; import { createDashboardSeed, FIXTURE_IDS } from "../helpers/dashboardFixtures"; test("run state serializes back into a valid wire run snapshot", () => { @@ -13,7 +13,7 @@ test("run state serializes back into a valid wire run snapshot", () => { const wire = serializeRunSnapshot(state); const reparsed = parseRunSnapshot(wire); - assert.equal(reparsed.id, FIXTURE_IDS.runId); + assert.equal(reparsed.id, FIXTURE_IDS.sampleId); assert.equal(reparsed.tasks[FIXTURE_IDS.solveTaskId]?.id, FIXTURE_IDS.solveTaskId); }); @@ -24,7 +24,7 @@ test("run snapshot hydration preserves nested run metrics for header display", ( const state = hydrateRunSnapshot({ ...run, metrics: { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, status: "failed", durationMs: 9000, totalTasks: 3, diff --git a/ergon-dashboard/tests/contracts/server-data.contract.test.ts b/ergon-dashboard/tests/contracts/server-data.contract.test.ts index 57fd5082b..786f87b03 100644 --- a/ergon-dashboard/tests/contracts/server-data.contract.test.ts +++ b/ergon-dashboard/tests/contracts/server-data.contract.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { loadExperimentList } from "../../src/lib/server-data/experiments"; -import { loadRunList } from "../../src/lib/server-data/runs"; +import { loadRunList } from "../../src/lib/server-data/samples"; import { getHarnessExperiment, resetDashboardHarness } from "../../src/lib/testing/dashboardHarness"; test("harness miss for experiment is represented as null, not notFound policy", () => { @@ -93,7 +93,7 @@ test("run list server data applies list filters and parses index summary fields" experiment: "alpha", }); - assert.match(requestedUrl, /\/runs\?/); + assert.match(requestedUrl, /\/samples\?/); assert.match(requestedUrl, /limit=25/); assert.match(requestedUrl, /offset=50/); assert.match(requestedUrl, /status=completed/); diff --git a/ergon-dashboard/tests/e2e/_shared/smoke.ts b/ergon-dashboard/tests/e2e/_shared/smoke.ts index a8708123b..07aa122ba 100644 --- a/ergon-dashboard/tests/e2e/_shared/smoke.ts +++ b/ergon-dashboard/tests/e2e/_shared/smoke.ts @@ -4,10 +4,10 @@ * Each per-env spec (researchrubrics / minif2f / swebench-verified) * invokes ``defineSmokeSpec`` and the factory handles: * - * - Parsing ``SMOKE_EXPERIMENT_JSON`` (array of ``{run_id, kind}``) so a + * - Parsing ``SMOKE_EXPERIMENT_JSON`` (array of ``{sample_id, kind}``) so a * heterogeneous experiment group (happy + sad slots) dispatches per-kind. * - Per-run assertions against the backend harness DTO + dashboard UI. - * - Screenshot capture points keyed on run_id + kind. + * - Screenshot capture points keyed on sample_id + kind. * * Contract with the dashboard: all assertions use ``data-testid`` * attributes. See ``docs/superpowers/plans/test-refactor/03-dashboard-and-playwright.md §6``. @@ -24,11 +24,11 @@ import { EXPECTED_NESTED_SUBTASK_SLUGS, EXPECTED_SUBTASK_SLUGS } from "./expecte export interface SmokeSpecConfig { env: string; /** Optional per-run additional assertions (e.g. env-specific UI check) */ - extraRunAssertions?: (page: Page, runId: string) => Promise; + extraRunAssertions?: (page: Page, sampleId: string) => Promise; } interface ExperimentGroupMember { - run_id: string; + sample_id: string; kind: "happy" | "sad"; } @@ -75,7 +75,7 @@ function graphElementForTask(page: Page, taskId: string): Locator { async function selectRenderedGraphTask( page: Page, state: BackendRunState, - runId: string, + sampleId: string, evaluatedTaskIds: Set, ): Promise { const candidates = [ @@ -99,7 +99,7 @@ async function selectRenderedGraphTask( } } - throw new Error(`no rendered graph task found for run ${runId}`); + throw new Error(`no rendered graph task found for run ${sampleId}`); } async function openWorkspaceForGraphTask(page: Page, taskId: string): Promise { @@ -126,15 +126,15 @@ async function expectNoTimelinePlaybackControls(page: Page): Promise { async function assertRunWorkspace( page: Page, state: BackendRunState, - runId: string, + sampleId: string, ): Promise { - await expect(page.getByTestId("run-header")).toBeVisible(); + await expect(page.getByTestId("sample-header")).toBeVisible(); await expect(page.getByTestId("graph-canvas")).toBeVisible(); await expect(page.getByTestId("activity-stack-region")).toBeVisible(); await expect(page.locator('[data-testid^="activity-bar-"]').first()).toBeVisible(); const evaluatedTaskIds = new Set(state.evaluations.map((evaluation) => evaluation.task_id)); - const selected = await selectRenderedGraphTask(page, state, runId, evaluatedTaskIds); + const selected = await selectRenderedGraphTask(page, state, sampleId, evaluatedTaskIds); await openWorkspaceForGraphTask(page, selected.id); await expect(page.getByTestId("workspace-region")).toBeVisible(); @@ -209,9 +209,9 @@ export function defineSmokeSpec(cfg: SmokeSpecConfig): void { const experimentRuns = readExperimentGroupFromEnv(); test.describe(`${cfg.env} canonical smoke`, () => { - for (const { run_id, kind } of experimentRuns) { - test(`run ${run_id} (${kind})`, async ({ page }) => { - const state = await client.getRunState(run_id); + for (const { sample_id, kind } of experimentRuns) { + test(`run ${sample_id} (${kind})`, async ({ page }) => { + const state = await client.getRunState(sample_id); // Backend-DTO assertions are the load-bearing contract. The UI // assertions below use graph-canvas + page-load + screenshot @@ -250,24 +250,24 @@ export function defineSmokeSpec(cfg: SmokeSpecConfig): void { const successfulEval = state.evaluations.some((e) => e.score === 1.0); expect(successfulEval).toBe(true); - await page.goto(`/run/${run_id}`); - await assertRunWorkspace(page, state, run_id); + await page.goto(`/samples/${sample_id}`); + await assertRunWorkspace(page, state, sample_id); await screenshot( page, - path.join(screenshotDir, cfg.env, `${run_id}-happy.png`), + path.join(screenshotDir, cfg.env, `${sample_id}-happy.png`), ); await screenshot( page, - path.join(screenshotDir, cfg.env, `${run_id}-visual-debugger-full.png`), + path.join(screenshotDir, cfg.env, `${sample_id}-visual-debugger-full.png`), ); await locatorScreenshot( page.getByTestId("activity-stack-region"), - path.join(screenshotDir, cfg.env, `${run_id}-activity-stack.png`), + path.join(screenshotDir, cfg.env, `${sample_id}-activity-stack.png`), ); if (cfg.extraRunAssertions) { - await cfg.extraRunAssertions(page, run_id); + await cfg.extraRunAssertions(page, sample_id); } return; } @@ -288,19 +288,19 @@ export function defineSmokeSpec(cfg: SmokeSpecConfig): void { expect(statusBySlug.get("l_2")).toBe("failed"); expect(statusBySlug.get("l_3")).toBe("blocked"); - await page.goto(`/run/${run_id}`); - await assertRunWorkspace(page, state, run_id); + await page.goto(`/samples/${sample_id}`); + await assertRunWorkspace(page, state, sample_id); await screenshot( page, - path.join(screenshotDir, cfg.env, `${run_id}-sad.png`), + path.join(screenshotDir, cfg.env, `${sample_id}-sad.png`), ); await screenshot( page, - path.join(screenshotDir, cfg.env, `${run_id}-visual-debugger-full.png`), + path.join(screenshotDir, cfg.env, `${sample_id}-visual-debugger-full.png`), ); await locatorScreenshot( page.getByTestId("activity-stack-region"), - path.join(screenshotDir, cfg.env, `${run_id}-activity-stack.png`), + path.join(screenshotDir, cfg.env, `${sample_id}-activity-stack.png`), ); }); } diff --git a/ergon-dashboard/tests/e2e/activity-stack.spec.ts b/ergon-dashboard/tests/e2e/activity-stack.spec.ts index 1402cae2f..aae20e2c0 100644 --- a/ergon-dashboard/tests/e2e/activity-stack.spec.ts +++ b/ergon-dashboard/tests/e2e/activity-stack.spec.ts @@ -121,10 +121,10 @@ async function expectNoTimelinePlaybackControls(page: Page) { test("visual debugger renders graph, activity stack, and time-aware workspace", async ({ page }) => { await page.goto( - `/run/${CONCURRENT_MAS_FIXTURE_IDS.runId}`, + `/samples/${CONCURRENT_MAS_FIXTURE_IDS.sampleId}`, ); - await expect(page.getByTestId("run-header")).toBeVisible(); + await expect(page.getByTestId("sample-header")).toBeVisible(); await expect(page.getByTestId("graph-canvas")).toBeVisible(); await expect(page.getByTestId("activity-stack-region")).toBeVisible(); await expect(page.getByTestId("activity-kind-legend")).toContainText("Span"); diff --git a/ergon-dashboard/tests/e2e/run.delta.spec.ts b/ergon-dashboard/tests/e2e/sample.delta.spec.ts similarity index 85% rename from ergon-dashboard/tests/e2e/run.delta.spec.ts rename to ergon-dashboard/tests/e2e/sample.delta.spec.ts index 7d523676a..368092749 100644 --- a/ergon-dashboard/tests/e2e/run.delta.spec.ts +++ b/ergon-dashboard/tests/e2e/sample.delta.spec.ts @@ -32,13 +32,13 @@ test.afterEach(async () => { }); test("run header reacts to controlled completion delta", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); - await expect(page.getByTestId("run-header")).toContainText("Executing"); + await expect(page.getByTestId("sample-header")).toContainText("Executing"); const response = await page.request.post("/api/danger/test-harness/dashboard/events/run-complete", { data: { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, status: "completed", durationSeconds: 42, finalScore: 0.75, @@ -47,17 +47,17 @@ test("run header reacts to controlled completion delta", async ({ page }) => { }); expect(response.ok()).toBeTruthy(); - await expect(page.getByTestId("run-header")).toContainText("Completed"); - await expect(page.getByTestId("run-header")).toContainText("75.0%"); + await expect(page.getByTestId("sample-header")).toContainText("Completed"); + await expect(page.getByTestId("sample-header")).toContainText("75.0%"); }); test("communication and evaluation react to controlled deltas", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); await page.getByTestId(`graph-node-${FIXTURE_IDS.solveTaskId}`).click(); const messageResponse = await page.request.post("/api/danger/test-harness/dashboard/events/thread-message", { data: { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, thread: createDeltaThread(), }, }); @@ -65,7 +65,7 @@ test("communication and evaluation react to controlled deltas", async ({ page }) const evaluationResponse = await page.request.post("/api/danger/test-harness/dashboard/events/task-evaluation", { data: { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, taskId: FIXTURE_IDS.solveTaskId, evaluation: createUpdatedEvaluation(), }, @@ -87,14 +87,14 @@ test("communication and evaluation react to controlled deltas", async ({ page }) }); test("workspace actions react to controlled context event deltas", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); await page.getByTestId(`graph-node-${FIXTURE_IDS.solveTaskId}`).click(); await page.getByTestId("workspace-tab-actions").click(); await expect(page.getByTestId("workspace-actions")).toContainText("lean_check"); const response = await page.request.post("/api/danger/test-harness/dashboard/events/context-event", { data: { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, taskId: FIXTURE_IDS.solveTaskId, event: createDeltaContextEvent(), }, @@ -105,12 +105,12 @@ test("workspace actions react to controlled context event deltas", async ({ page }); test("evaluation tab shows a clear empty criteria state", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); await page.getByTestId(`graph-node-${FIXTURE_IDS.solveTaskId}`).click(); const response = await page.request.post("/api/danger/test-harness/dashboard/events/task-evaluation", { data: { - runId: FIXTURE_IDS.runId, + sampleId: FIXTURE_IDS.sampleId, taskId: FIXTURE_IDS.solveTaskId, evaluation: createEmptyCriteriaEvaluation(), }, diff --git a/ergon-dashboard/tests/e2e/run.snapshot.spec.ts b/ergon-dashboard/tests/e2e/sample.snapshot.spec.ts similarity index 94% rename from ergon-dashboard/tests/e2e/run.snapshot.spec.ts rename to ergon-dashboard/tests/e2e/sample.snapshot.spec.ts index eb33a8303..7961dae41 100644 --- a/ergon-dashboard/tests/e2e/run.snapshot.spec.ts +++ b/ergon-dashboard/tests/e2e/sample.snapshot.spec.ts @@ -38,21 +38,21 @@ async function expectNoTimelinePlaybackControls(page: import("@playwright/test") } test("run page links back to experiments", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); - await expect(page.getByTestId("run-header")).toContainText("Experiments"); - await expect(page.getByTestId("run-header")).toContainText("parallel"); + await expect(page.getByTestId("sample-header")).toContainText("Experiments"); + await expect(page.getByTestId("sample-header")).toContainText("parallel"); }); test("run workspace does not expose manual live or timeline mode controls", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); await expect(page.getByTestId("graph-canvas")).toBeVisible(); await expectNoTimelinePlaybackControls(page); }); test("run workspace shows rerun as unavailable until backend support exists", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); const rerunButton = page.getByTestId("rerun-button"); await expect(rerunButton).toBeVisible(); @@ -62,7 +62,7 @@ test("run workspace shows rerun as unavailable until backend support exists", as test("snapshot selection does not expose playback or speed controls", async ({ page }) => { await page.goto( - `/run/${CONCURRENT_MAS_FIXTURE_IDS.runId}`, + `/samples/${CONCURRENT_MAS_FIXTURE_IDS.sampleId}`, ); await expect(page.getByTestId("activity-stack-region")).toBeVisible(); @@ -78,7 +78,7 @@ test("activity marker locks graph and header to snapshot until Escape returns to page, }) => { await page.goto( - `/run/${CONCURRENT_MAS_FIXTURE_IDS.runId}`, + `/samples/${CONCURRENT_MAS_FIXTURE_IDS.sampleId}`, ); await expect(page.getByTestId("graph-canvas")).toBeVisible(); @@ -95,17 +95,17 @@ test("activity marker locks graph and header to snapshot until Escape returns to await expect(page.getByTestId("snapshot-lock-label")).toBeVisible(); await expect(page.getByTestId("snapshot-pin").first()).toBeVisible(); - await expect(page.getByTestId("run-header")).toContainText("snapshot · seq 14"); + await expect(page.getByTestId("sample-header")).toContainText("snapshot · seq 14"); await expect(validateCitationsNode).toHaveAttribute("data-task-status", "pending"); await page.keyboard.press("Escape"); - await expect(page.getByTestId("run-header")).toContainText(/live/i); + await expect(page.getByTestId("sample-header")).toContainText(/live/i); await expect(page.getByTestId("snapshot-lock-label")).toHaveCount(0); await expect(validateCitationsNode).toHaveAttribute("data-task-status", "completed"); }); test("graph selection opens workspace evidence sections", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); await expect(page.getByTestId("graph-canvas")).toBeVisible(); await page.getByTestId(`graph-node-${FIXTURE_IDS.solveTaskId}`).click(); @@ -189,7 +189,7 @@ test("graph selection opens workspace evidence sections", async ({ page }) => { }); test("persisted run snapshot remains inspectable after refresh", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); await page.reload(); await expect(page.getByTestId("graph-canvas")).toBeVisible(); @@ -203,7 +203,7 @@ test("persisted run snapshot remains inspectable after refresh", async ({ page } }); test("run debugger panels can be resized and persist across reloads", async ({ page }) => { - await page.goto(`/run/${FIXTURE_IDS.runId}`); + await page.goto(`/samples/${FIXTURE_IDS.sampleId}`); await expect(page.getByTestId("graph-canvas")).toBeVisible(); await expect(page.getByTestId("timeline-region")).toBeVisible(); diff --git a/ergon-dashboard/tests/fixtures/mas-runs/README.md b/ergon-dashboard/tests/fixtures/mas-samples/README.md similarity index 100% rename from ergon-dashboard/tests/fixtures/mas-runs/README.md rename to ergon-dashboard/tests/fixtures/mas-samples/README.md diff --git a/ergon-dashboard/tests/fixtures/mas-runs/concurrent-mas-run.json b/ergon-dashboard/tests/fixtures/mas-samples/concurrent-mas-run.json similarity index 94% rename from ergon-dashboard/tests/fixtures/mas-runs/concurrent-mas-run.json rename to ergon-dashboard/tests/fixtures/mas-samples/concurrent-mas-run.json index 9b670ec14..a17f16fd6 100644 --- a/ergon-dashboard/tests/fixtures/mas-runs/concurrent-mas-run.json +++ b/ergon-dashboard/tests/fixtures/mas-samples/concurrent-mas-run.json @@ -213,7 +213,7 @@ "threads": [ { "id": "40000000-0000-4000-8000-000000000001", - "runId": "99999999-9999-4999-8999-999999999999", + "sampleId": "99999999-9999-4999-8999-999999999999", "taskId": "10000000-0000-4000-8000-000000000003", "topic": "task_clarification", "agentAId": "agent-b", @@ -225,7 +225,7 @@ "id": "40000000-0000-4000-8000-000000000002", "threadId": "40000000-0000-4000-8000-000000000001", "threadTopic": "task_clarification", - "runId": "99999999-9999-4999-8999-999999999999", + "sampleId": "99999999-9999-4999-8999-999999999999", "taskId": "10000000-0000-4000-8000-000000000003", "taskExecutionId": "30000000-0000-4000-8000-000000000002", "fromAgentId": "agent-b", @@ -240,7 +240,7 @@ "evaluationsByTask": { "10000000-0000-4000-8000-000000000006": { "id": "50000000-0000-4000-8000-000000000001", - "runId": "99999999-9999-4999-8999-999999999999", + "sampleId": "99999999-9999-4999-8999-999999999999", "taskId": "10000000-0000-4000-8000-000000000006", "evaluatorName": "rubric", "aggregationRule": "weighted_sum", @@ -282,7 +282,7 @@ "10000000-0000-4000-8000-000000000002": [ { "id": "60000000-0000-4000-8000-000000000001", - "runId": "99999999-9999-4999-8999-999999999999", + "sampleId": "99999999-9999-4999-8999-999999999999", "taskExecutionId": "30000000-0000-4000-8000-000000000001", "taskId": "10000000-0000-4000-8000-000000000002", "workerBindingKey": "researcher-a", @@ -316,7 +316,7 @@ "mutations": [ { "id": "70000000-0000-4000-8000-000000000001", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 1, "mutation_type": "node.added", "target_type": "node", @@ -335,7 +335,7 @@ }, { "id": "70000000-0000-4000-8000-000000000002", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 2, "mutation_type": "node.status_changed", "target_type": "node", @@ -352,7 +352,7 @@ }, { "id": "70000000-0000-4000-8000-000000000003", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 3, "mutation_type": "node.added", "target_type": "node", @@ -371,7 +371,7 @@ }, { "id": "70000000-0000-4000-8000-000000000004", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 4, "mutation_type": "edge.added", "target_type": "edge", @@ -388,7 +388,7 @@ }, { "id": "70000000-0000-4000-8000-000000000005", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 5, "mutation_type": "node.added", "target_type": "node", @@ -407,7 +407,7 @@ }, { "id": "70000000-0000-4000-8000-000000000006", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 6, "mutation_type": "edge.added", "target_type": "edge", @@ -424,7 +424,7 @@ }, { "id": "70000000-0000-4000-8000-000000000007", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 7, "mutation_type": "node.added", "target_type": "node", @@ -443,7 +443,7 @@ }, { "id": "70000000-0000-4000-8000-000000000008", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 8, "mutation_type": "edge.added", "target_type": "edge", @@ -460,7 +460,7 @@ }, { "id": "70000000-0000-4000-8000-000000000009", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 9, "mutation_type": "node.status_changed", "target_type": "node", @@ -477,7 +477,7 @@ }, { "id": "70000000-0000-4000-8000-000000000010", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 10, "mutation_type": "node.status_changed", "target_type": "node", @@ -494,7 +494,7 @@ }, { "id": "70000000-0000-4000-8000-000000000011", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 11, "mutation_type": "node.added", "target_type": "node", @@ -513,7 +513,7 @@ }, { "id": "70000000-0000-4000-8000-000000000012", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 12, "mutation_type": "edge.added", "target_type": "edge", @@ -530,7 +530,7 @@ }, { "id": "70000000-0000-4000-8000-000000000013", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 13, "mutation_type": "node.added", "target_type": "node", @@ -549,7 +549,7 @@ }, { "id": "70000000-0000-4000-8000-000000000014", - "run_id": "99999999-9999-4999-8999-999999999999", + "sample_id": "99999999-9999-4999-8999-999999999999", "sequence": 14, "mutation_type": "edge.added", "target_type": "edge", diff --git a/ergon-dashboard/tests/helpers/backendHarnessClient.ts b/ergon-dashboard/tests/helpers/backendHarnessClient.ts index 306ee5e37..ba9ac9596 100644 --- a/ergon-dashboard/tests/helpers/backendHarnessClient.ts +++ b/ergon-dashboard/tests/helpers/backendHarnessClient.ts @@ -10,7 +10,7 @@ */ export interface BackendRunState { - run_id: string; + sample_id: string; status: "completed" | "failed" | "cancelled" | "in_progress" | string; graph_nodes: { id: string; @@ -44,16 +44,16 @@ export interface BackendRunState { } export interface BackendExperimentRun { - run_id: string; + sample_id: string; status: string; } export class BackendHarnessClient { constructor(private readonly baseUrl: string) {} - async getRunState(runId: string): Promise { + async getRunState(sampleId: string): Promise { const r = await fetch( - `${this.baseUrl}/api/__danger__/test-harness/read/run/${runId}/state`, + `${this.baseUrl}/api/__danger__/test-harness/read/samples/${sampleId}/state`, ); if (!r.ok) { throw new Error(`harness ${r.status}: ${await r.text()}`); @@ -63,7 +63,7 @@ export class BackendHarnessClient { async getExperimentRuns(experiment: string): Promise { const r = await fetch( - `${this.baseUrl}/api/__danger__/test-harness/read/experiment/${encodeURIComponent(experiment)}/runs`, + `${this.baseUrl}/api/__danger__/test-harness/read/experiment/${encodeURIComponent(experiment)}/samples`, ); if (!r.ok) { throw new Error(`harness ${r.status}: ${await r.text()}`); diff --git a/ergon-dashboard/tests/helpers/dashboardFixtures.ts b/ergon-dashboard/tests/helpers/dashboardFixtures.ts index 96e1dbffb..aed9f6684 100644 --- a/ergon-dashboard/tests/helpers/dashboardFixtures.ts +++ b/ergon-dashboard/tests/helpers/dashboardFixtures.ts @@ -1,16 +1,16 @@ import type { DashboardHarnessSeedPayload } from "../../src/lib/testing/dashboardHarness"; -import concurrentMasFixture from "../fixtures/mas-runs/concurrent-mas-run.json"; +import concurrentMasFixture from "../fixtures/mas-samples/concurrent-mas-run.json"; import type { CommunicationThreadState, ContextEventState, - SerializedWorkflowRunState, + SerializedSampleWorkspaceState, TaskEvaluationState, TaskState, } from "../../src/lib/types"; import { TaskStatus } from "../../src/lib/types"; export const FIXTURE_IDS = { - runId: "22222222-2222-4222-8222-222222222222", + sampleId: "22222222-2222-4222-8222-222222222222", definitionId: "33333333-3333-4333-8333-333333333333", rootTaskId: "task-root", exploreTaskId: "task-explore", @@ -31,7 +31,7 @@ export const FIXTURE_IDS = { export const CONCURRENT_MAS_FIXTURE_IDS = { definitionId: "33333333-3333-4333-8333-333333333333", - runId: "99999999-9999-4999-8999-999999999999", + sampleId: "99999999-9999-4999-8999-999999999999", searchTaskId: "10000000-0000-4000-8000-000000000002", checkTaskId: "10000000-0000-4000-8000-000000000003", } as const; @@ -49,7 +49,7 @@ function taskState(task: Partial & Pick { + async getRunState(sampleId: string): Promise { const response = await this.request.get( - `${this.baseUrl}/api/__danger__/test-harness/read/run/${runId}/state`, + `${this.baseUrl}/api/__danger__/test-harness/read/samples/${sampleId}/state`, ); if (!response.ok()) { throw new Error( diff --git a/ergon_builtins/AGENTS.md b/ergon_builtins/AGENTS.md index f32c3fb95..ec20e7845 100644 --- a/ergon_builtins/AGENTS.md +++ b/ergon_builtins/AGENTS.md @@ -31,10 +31,10 @@ Which worker emits what. `—` = not applicable, `✗` = nothing emitted. | Worker | GENERATIONS | OUTPUTS | SANDBOX | COMMUNICATION | |---|---|---|---|---| | `training-stub` | ✓ (multi-turn synthetic w/ logprobs) | ✗ | ✗ | ✗ | -| `canonical-smoke` | ✓ | ✓ (per-env leaf RunResource) | ✓ (via per-env leaf) | ✗ | +| `canonical-smoke` | ✓ | ✓ (per-env leaf SampleResource) | ✓ (via per-env leaf) | ✗ | | `react-v1` | ✓ | ✗ | ✗ | ✓ (system/user/assistant/thinking/tool calls) | | `minif2f-react` | ✓ | ✓ (proof artifact) | ✓ (Lean files) | ✓ | -| `researchrubrics-researcher` | ✓ | ✓ (RunResource kind=REPORT) | ✓ (writes `final_output/report.md`) | ✗ | +| `researchrubrics-researcher` | ✓ | ✓ (SampleResource kind=REPORT) | ✓ (writes `final_output/report.md`) | ✗ | EVALUATION is populated by whichever **evaluator** you pass with `--evaluator`; see table below. @@ -49,7 +49,7 @@ EVALUATION is populated by whichever **evaluator** you pass with | `canonical-smoke` | `workers/stubs/canonical_smoke_worker.py` | per-env leaf + its sandbox | Dispatches to a per-benchmark smoke leaf (`SweBenchSmokeRubric`, `ResearchRubricsSmokeRubric`, `MiniF2FSmokeRubric`) — the RFC 2026-04-21 canonical smoke path. | | `react-v1` | `workers/baselines/react_worker.py` | LLM | Generic ReAct-style worker built on pydantic-ai. Used by most real benchmarks. | | `minif2f-react` | `workers/baselines/minif2f_react_worker.py` | LLM + Lean 4 sandbox | ReAct pre-wired with `write_lean_file`, `check_lean_file`, `verify_lean_proof`. Produces a proof artifact in `WorkerOutput`. | -| `researchrubrics-researcher` | `workers/research_rubrics/researcher_worker.py` | LLM + E2B sandbox (`ResearchRubricsSandboxManager`) | Writes a research report to `/workspace/final_output/report.md` and publishes it as a RunResource. | +| `researchrubrics-researcher` | `workers/research_rubrics/researcher_worker.py` | LLM + E2B sandbox (`ResearchRubricsSandboxManager`) | Writes a research report to `/workspace/final_output/report.md` and publishes it as a SampleResource. | --- @@ -73,7 +73,7 @@ EVALUATION is populated by whichever **evaluator** you pass with | `minif2f-rubric` | `benchmarks/minif2f/rubric.py` | Lean 4 sandbox | Compiles the final `.lean` in the sandbox; awards partial credit for syntactically-valid-but-unproved proofs. | | `minif2f-smoke-rubric` | `benchmarks/minif2f/smoke_rubric.py` | Lean sandbox | Canonical-smoke leaf for the MiniF2F env. | | `staged-rubric` | `benchmarks/gdpeval/rubric.py` | LLM (embedded `llm-judge` criteria) | Sequential-gate multi-stage evaluator used by GDPEval. | -| `researchrubrics-smoke-rubric` | `benchmarks/researchrubrics/smoke_rubric.py` | none (reads local RunResource) | Asserts a `REPORT` RunResource exists with required headers (`# Findings`, `## Sources`). | +| `researchrubrics-smoke-rubric` | `benchmarks/researchrubrics/smoke_rubric.py` | none (reads local SampleResource) | Asserts a `REPORT` SampleResource exists with required headers (`# Findings`, `## Sources`). | | `swebench-smoke-rubric` | `benchmarks/swebench_verified/smoke_rubric.py` | SWE-Bench sandbox | Canonical-smoke leaf for the SWE-Bench env. | | `swebench-rubric` | `evaluators/rubrics/swebench_rubric.py` | SWE-Bench sandbox | Real SWE-Bench patch evaluation. | @@ -86,7 +86,7 @@ EVALUATION is populated by whichever **evaluator** you pass with | `stub-criterion` | `evaluators/criteria/stub_criterion.py` | none | | `varied-stub-criterion` | `evaluators/criteria/varied_stub_criterion.py` | none | | `sandbox-file-check` | `evaluators/criteria/sandbox_file_check.py` | E2B sandbox | -| `stub-report-exists` | `evaluators/criteria/stub_report_exists.py` | reads RunResource blobs on disk | +| `stub-report-exists` | `evaluators/criteria/stub_report_exists.py` | reads SampleResource blobs on disk | | `llm-judge` | `evaluators/criteria/llm_judge.py` | LLM | | `file-check` | `evaluators/criteria/file_check.py` | none | | `code-check` | `evaluators/criteria/code_check.py` | none (lightweight path) | diff --git a/ergon_builtins/ergon_builtins/agents/training/synthetic_worker.py b/ergon_builtins/ergon_builtins/agents/training/synthetic_worker.py index e9e661c4f..0242d8cc6 100644 --- a/ergon_builtins/ergon_builtins/agents/training/synthetic_worker.py +++ b/ergon_builtins/ergon_builtins/agents/training/synthetic_worker.py @@ -2,7 +2,7 @@ Unlike stub-worker (which returns a plain string with no turns), this worker generates fake token-level data that exercises the full trajectory -extraction pipeline: RunContextEvent persistence, logprob storage, +extraction pipeline: SampleContextEvent persistence, logprob storage, and rollout_func return formatting. Use with ``--worker training-stub`` for CPU-only integration tests of diff --git a/ergon_builtins/ergon_builtins/benchmarks/researchrubrics/criteria/evidence.py b/ergon_builtins/ergon_builtins/benchmarks/researchrubrics/criteria/evidence.py index f4e865653..c188f931e 100644 --- a/ergon_builtins/ergon_builtins/benchmarks/researchrubrics/criteria/evidence.py +++ b/ergon_builtins/ergon_builtins/benchmarks/researchrubrics/criteria/evidence.py @@ -3,14 +3,14 @@ from pathlib import Path from ergon_core.api.criterion import CriterionContext -from ergon_core.core.application.resources import RunResourceView +from ergon_core.core.application.resources import SampleResourceView from pydantic import BaseModel class ResourceEvidence(BaseModel): model_config = {"frozen": True, "arbitrary_types_allowed": True} - resource: RunResourceView + resource: SampleResourceView text: str @@ -20,7 +20,7 @@ async def load_researchrubrics_evidence( resources = context.metadata.get("resources", ()) evidence: list[ResourceEvidence] = [] for resource in resources: - if not isinstance(resource, RunResourceView): + if not isinstance(resource, SampleResourceView): continue evidence.append(ResourceEvidence(resource=resource, text=_read_resource_text(resource))) @@ -29,7 +29,7 @@ async def load_researchrubrics_evidence( return final_outputs, scratch_outputs -def _read_resource_text(resource: RunResourceView) -> str: +def _read_resource_text(resource: SampleResourceView) -> str: try: raw_content = Path(resource.file_path).read_bytes() except OSError as exc: @@ -37,6 +37,6 @@ def _read_resource_text(resource: RunResourceView) -> str: return raw_content.decode("utf-8", errors="replace") -def _is_final_output_resource(resource: RunResourceView) -> bool: +def _is_final_output_resource(resource: SampleResourceView) -> bool: sandbox_origin = str(resource.metadata.get("sandbox_origin") or "") return resource.kind.value == "report" or sandbox_origin.startswith("/workspace/final_output/") diff --git a/ergon_builtins/ergon_builtins/common/llm_context/adapters/pydantic_ai.py b/ergon_builtins/ergon_builtins/common/llm_context/adapters/pydantic_ai.py index 893ca6ab4..846345886 100644 --- a/ergon_builtins/ergon_builtins/common/llm_context/adapters/pydantic_ai.py +++ b/ergon_builtins/ergon_builtins/common/llm_context/adapters/pydantic_ai.py @@ -14,7 +14,7 @@ ToolResultPart, UserMessagePart, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from pydantic import BaseModel from pydantic_ai.messages import ModelMessage, ModelRequest, ModelResponse from pydantic_ai.messages import ModelRequestPart as PydanticModelRequestPart diff --git a/ergon_builtins/ergon_builtins/toolkits/resources/models.py b/ergon_builtins/ergon_builtins/toolkits/resources/models.py index 3a3028bcf..0d4872cea 100644 --- a/ergon_builtins/ergon_builtins/toolkits/resources/models.py +++ b/ergon_builtins/ergon_builtins/toolkits/resources/models.py @@ -7,14 +7,14 @@ from datetime import datetime from uuid import UUID -from ergon_core.core.application.resources import RunResourceView -from ergon_core.core.persistence.shared.enums import RunResourceKind -from ergon_core.core.persistence.telemetry.models import RunResource, RunTaskExecution +from ergon_core.core.application.resources import SampleResourceView +from ergon_core.core.persistence.shared.enums import SampleResourceKind +from ergon_core.core.persistence.telemetry.models import SampleResource, SampleTaskAttempt from pydantic import BaseModel, ConfigDict, Field class ResourceRef(BaseModel): - """Subset of ``RunResourceView`` surfaced to the LLM.""" + """Subset of ``SampleResourceView`` surfaced to the LLM.""" model_config = ConfigDict(frozen=True) @@ -26,7 +26,7 @@ class ResourceRef(BaseModel): "tool surface." ), ) - kind: RunResourceKind = Field( + kind: SampleResourceKind = Field( description="Canonical kind (report, output, note, ...).", ) mime_type: str = Field( @@ -49,11 +49,11 @@ class ResourceRef(BaseModel): ) @classmethod - def from_view(cls, view: RunResourceView) -> "ResourceRef": - """Lift a ``RunResourceView`` to a ``ResourceRef``.""" + def from_view(cls, view: SampleResourceView) -> "ResourceRef": + """Lift a ``SampleResourceView`` to a ``ResourceRef``.""" return cls( logical_path=view.file_path, - kind=RunResourceKind(view.kind), + kind=SampleResourceKind(view.kind), mime_type=view.mime_type, file_path=view.file_path, content_hash=view.content_hash, @@ -62,11 +62,11 @@ def from_view(cls, view: RunResourceView) -> "ResourceRef": ) @classmethod - def from_row(cls, row: RunResource) -> "ResourceRef": - """Lift an ORM ``RunResource`` row to a ``ResourceRef``.""" + def from_row(cls, row: SampleResource) -> "ResourceRef": + """Lift an ORM ``SampleResource`` row to a ``ResourceRef``.""" return cls( logical_path=row.file_path, - kind=RunResourceKind(row.kind), + kind=SampleResourceKind(row.kind), mime_type=row.mime_type, file_path=row.file_path, content_hash=row.content_hash, @@ -76,7 +76,7 @@ def from_row(cls, row: RunResource) -> "ResourceRef": class TaskExecutionRef(BaseModel): - """Subset of ``RunTaskExecution`` surfaced to the LLM.""" + """Subset of ``SampleTaskAttempt`` surfaced to the LLM.""" model_config = ConfigDict(frozen=True) @@ -98,8 +98,8 @@ class TaskExecutionRef(BaseModel): ) @classmethod - def from_row(cls, row: RunTaskExecution) -> "TaskExecutionRef": - """Lift an ORM ``RunTaskExecution`` row to a ``TaskExecutionRef``.""" + def from_row(cls, row: SampleTaskAttempt) -> "TaskExecutionRef": + """Lift an ORM ``SampleTaskAttempt`` row to a ``TaskExecutionRef``.""" return cls( task_execution_id=row.id, status=str(row.status), diff --git a/ergon_builtins/ergon_builtins/toolkits/resources/toolkit.py b/ergon_builtins/ergon_builtins/toolkits/resources/toolkit.py index d24e51a42..760f4c71f 100644 --- a/ergon_builtins/ergon_builtins/toolkits/resources/toolkit.py +++ b/ergon_builtins/ergon_builtins/toolkits/resources/toolkit.py @@ -9,8 +9,8 @@ from uuid import UUID from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunResource -from ergon_core.core.application.resources.repository import RunResourceRepository +from ergon_core.core.persistence.telemetry.models import SampleResource +from ergon_core.core.application.resources.repository import SampleResourceRepository from ergon_core.core.application.runtime.task_execution_repository import ( TaskExecutionRepository, ) @@ -31,10 +31,10 @@ class ResearchGraphToolkit: subclass unpacks context and passes them in (§7.3 pattern). """ - def __init__(self, *, run_id: UUID, task_execution_id: UUID) -> None: - self._run_id = run_id + def __init__(self, *, sample_id: UUID, task_execution_id: UUID) -> None: + self._run_id = sample_id self._task_execution_id = task_execution_id - self._resource_repo = RunResourceRepository() + self._resource_repo = SampleResourceRepository() self._task_repo = TaskExecutionRepository() def build_tools(self) -> list[Tool[AgentToolBudgetDeps]]: @@ -55,7 +55,7 @@ def build_tools(self) -> list[Tool[AgentToolBudgetDeps]]: # ------------------------------------------------------------------ def _list_my_resources(self) -> Tool[AgentToolBudgetDeps]: - run_id = self._run_id + sample_id = self._run_id task_execution_id = self._task_execution_id async def list_my_resources( @@ -75,7 +75,7 @@ async def list_my_resources( with get_session() as session: rows = self._resource_repo.list_by_execution(session, task_execution_id) return _to_refs_sorted( - [r for r in rows if r.run_id == run_id], + [r for r in rows if r.sample_id == sample_id], ) return Tool(function=list_my_resources, takes_ctx=True) @@ -85,7 +85,7 @@ async def list_my_resources( # ------------------------------------------------------------------ def _list_child_resources(self) -> Tool[AgentToolBudgetDeps]: - run_id = self._run_id + sample_id = self._run_id task_execution_id = self._task_execution_id async def list_child_resources( @@ -107,11 +107,11 @@ async def list_child_resources( session, task_execution_id, ) - result: list[RunResource] = [] + result: list[SampleResource] = [] for child in children: with get_session() as session: rows = self._resource_repo.list_by_execution(session, child.id) - result.extend(r for r in rows if r.run_id == run_id) + result.extend(r for r in rows if r.sample_id == sample_id) return _to_refs_sorted(result) return Tool(function=list_child_resources, takes_ctx=True) @@ -121,7 +121,7 @@ async def list_child_resources( # ------------------------------------------------------------------ def _list_descendant_resources(self) -> Tool[AgentToolBudgetDeps]: - run_id = self._run_id + sample_id = self._run_id task_execution_id = self._task_execution_id async def list_descendant_resources( @@ -145,7 +145,7 @@ async def list_descendant_resources( return tool_budget.exhausted_result("non-workflow tool budget reached") visited: set[UUID] = {task_execution_id} frontier: list[UUID] = [task_execution_id] - result: list[RunResource] = [] + result: list[SampleResource] = [] for _depth in range(max_depth): next_frontier: list[UUID] = [] @@ -162,7 +162,7 @@ async def list_descendant_resources( next_frontier.append(child.id) with get_session() as session: rows = self._resource_repo.list_by_execution(session, child.id) - result.extend(r for r in rows if r.run_id == run_id) + result.extend(r for r in rows if r.sample_id == sample_id) frontier = next_frontier if not frontier: break @@ -176,7 +176,7 @@ async def list_descendant_resources( # ------------------------------------------------------------------ def _list_run_resources(self) -> Tool[AgentToolBudgetDeps]: - run_id = self._run_id + sample_id = self._run_id async def list_run_resources( ctx: "RunContext[AgentToolBudgetDeps]", @@ -193,7 +193,7 @@ async def list_run_resources( ): return tool_budget.exhausted_result("non-workflow tool budget reached") with get_session() as session: - rows = self._resource_repo.list_by_run(session, run_id) + rows = self._resource_repo.list_by_run(session, sample_id) return _to_refs_sorted(rows) return Tool(function=list_run_resources, takes_ctx=True) @@ -203,7 +203,7 @@ async def list_run_resources( # ------------------------------------------------------------------ def _get_resource_by_logical_path(self) -> Tool[AgentToolBudgetDeps]: - run_id = self._run_id + sample_id = self._run_id async def get_resource_by_logical_path( ctx: "RunContext[AgentToolBudgetDeps]", @@ -224,7 +224,7 @@ async def get_resource_by_logical_path( ): return tool_budget.exhausted_result("non-workflow tool budget reached") with get_session() as session: - rows = self._resource_repo.list_by_run(session, run_id) + rows = self._resource_repo.list_by_run(session, sample_id) matching = [r for r in rows if r.file_path == logical_path] if not matching: return None @@ -238,7 +238,7 @@ async def get_resource_by_logical_path( # ------------------------------------------------------------------ def _get_resource_by_content_hash(self) -> Tool[AgentToolBudgetDeps]: - run_id = self._run_id + sample_id = self._run_id async def get_resource_by_content_hash( ctx: "RunContext[AgentToolBudgetDeps]", @@ -259,7 +259,7 @@ async def get_resource_by_content_hash( ): return tool_budget.exhausted_result("non-workflow tool budget reached") with get_session() as session: - rows = self._resource_repo.list_by_run(session, run_id) + rows = self._resource_repo.list_by_run(session, sample_id) matching = [r for r in rows if r.content_hash == content_hash] if not matching: return None @@ -274,7 +274,7 @@ async def get_resource_by_content_hash( # --------------------------------------------------------------------------- -def _to_refs_sorted(rows: Sequence[RunResource]) -> list[ResourceRef]: +def _to_refs_sorted(rows: Sequence[SampleResource]) -> list[ResourceRef]: """Convert ORM rows to ResourceRef DTOs, sorted created_at DESC.""" sorted_rows = sorted(rows, key=lambda r: (r.created_at, r.id), reverse=True) return [ResourceRef.from_row(r) for r in sorted_rows] diff --git a/ergon_builtins/ergon_builtins/toolkits/subagents/toolkit.py b/ergon_builtins/ergon_builtins/toolkits/subagents/toolkit.py index 9e79e260c..94f954d22 100644 --- a/ergon_builtins/ergon_builtins/toolkits/subagents/toolkit.py +++ b/ergon_builtins/ergon_builtins/toolkits/subagents/toolkit.py @@ -40,7 +40,7 @@ class SubtaskLifecycleToolkit: """Produces the eight manager-facing tool callables for ``Agent(tools=[...])``. The toolkit is a closure factory, not a service: it captures - ``run_id`` and ``parent_task_id`` from ``WorkerContext`` so that + ``sample_id`` and ``parent_task_id`` from ``WorkerContext`` so that creation tools (add_subtask, plan_subtasks, list_subtasks) are scoped to the manager's subtree by construction. @@ -50,7 +50,7 @@ class SubtaskLifecycleToolkit: only response-shaping and UUID parsing logic here. ``definition_id`` is NOT captured here --- the service resolves it - from ``run_id`` at dispatch time, keeping the tool surface + from ``sample_id`` at dispatch time, keeping the tool surface thin and eliminating a class of stale-id bugs when definitions are reloaded mid-run. """ diff --git a/ergon_builtins/ergon_builtins/toolkits/workflow_cli/tool.py b/ergon_builtins/ergon_builtins/toolkits/workflow_cli/tool.py index 1288736fd..b833ec3dc 100644 --- a/ergon_builtins/ergon_builtins/toolkits/workflow_cli/tool.py +++ b/ergon_builtins/ergon_builtins/toolkits/workflow_cli/tool.py @@ -9,7 +9,7 @@ execute_workflow_command, ) from ergon_core.api import WorkerContext -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from ergon_core.core.persistence.shared.db import get_session from pydantic_ai import RunContext from sqlmodel import Session @@ -56,7 +56,7 @@ async def run_command(command: str) -> str: maybe_output = execute_command( command, context=WorkflowCommandContext( - run_id=worker_context.run_id, + sample_id=worker_context.sample_id, task_id=worker_context.task_id, execution_id=worker_context.execution_id, sandbox_task_key=sandbox_task_key, diff --git a/ergon_builtins/ergon_builtins/tools/workflow_command_adapter.py b/ergon_builtins/ergon_builtins/tools/workflow_command_adapter.py index 924ddff74..2ddb7b03d 100644 --- a/ergon_builtins/ergon_builtins/tools/workflow_command_adapter.py +++ b/ergon_builtins/ergon_builtins/tools/workflow_command_adapter.py @@ -18,14 +18,14 @@ ) from ergon_core.api import Task, WorkerContext from ergon_core.core.application.runtime.errors import GraphError -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunResourceKind +from ergon_core.core.persistence.shared.enums import SampleResourceKind from ergon_core.core.shared.json_types import JsonObject _RESOURCE_SCOPES = ("visible", "own", "input", "upstream", "children", "descendants") -_RESOURCE_KINDS = tuple(kind.value for kind in RunResourceKind) +_RESOURCE_KINDS = tuple(kind.value for kind in SampleResourceKind) _OUTPUT_FORMATS = ("text", "json") _DEPENDENCY_DIRECTIONS = ("upstream", "downstream", "both") _FORBIDDEN_CONTEXT_FLAGS = { @@ -42,7 +42,7 @@ class WorkflowCommandContext(BaseModel): model_config = ConfigDict(frozen=True) - run_id: UUID + sample_id: UUID task_id: UUID execution_id: UUID sandbox_task_key: UUID @@ -180,7 +180,7 @@ def _handle_inspect( if args.action == "resource-list": resources = service.list_resources( session, - run_id=context.run_id, + sample_id=context.sample_id, task_id=context.task_id, scope=args.scope, kind=args.kind, @@ -200,7 +200,7 @@ def _handle_inspect( resource_id = UUID(args.resource_id) content = service.read_resource_bytes( session, - run_id=context.run_id, + sample_id=context.sample_id, resource_id=resource_id, max_bytes=args.max_bytes, ) @@ -210,7 +210,7 @@ def _handle_inspect( if args.action == "task-tree": parent = UUID(args.parent_task_id) if args.parent_task_id else None deadline = time.monotonic() + max(args.wait_seconds, 0) - tasks = service.list_tasks(session, run_id=context.run_id, parent_task_id=parent) + tasks = service.list_tasks(session, sample_id=context.sample_id, parent_task_id=parent) while args.wait_seconds > 0 and time.monotonic() < deadline: children = [task for task in tasks if task.parent_task_id == context.task_id] if children and all( @@ -218,7 +218,7 @@ def _handle_inspect( ): break time.sleep(2) - tasks = service.list_tasks(session, run_id=context.run_id, parent_task_id=parent) + tasks = service.list_tasks(session, sample_id=context.sample_id, parent_task_id=parent) return _format_output( {"tasks": [_dump(task) for task in tasks]}, text_lines=[ @@ -230,7 +230,7 @@ def _handle_inspect( if args.action == "task-dependencies": deps = service.list_dependencies( session, - run_id=context.run_id, + sample_id=context.sample_id, task_id=context.task_id, direction=args.direction, ) @@ -245,7 +245,7 @@ def _handle_inspect( if args.action == "next-actions": actions = service.get_next_actions( session, - run_id=context.run_id, + sample_id=context.sample_id, task_id=context.task_id, manager_capable=args.manager_capable, ) @@ -296,9 +296,9 @@ async def _load_parent_task( sandbox_id: str, ) -> Task: row = session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == context.run_id, - RunGraphNode.task_id == context.task_id, + select(SampleGraphNode).where( + SampleGraphNode.sample_id == context.sample_id, + SampleGraphNode.task_id == context.task_id, ) ).one() if not row.task_json: diff --git a/ergon_builtins/tests/fixtures/toy_workflow.py b/ergon_builtins/tests/fixtures/toy_workflow.py index 165bd0ce5..f59aebe5d 100644 --- a/ergon_builtins/tests/fixtures/toy_workflow.py +++ b/ergon_builtins/tests/fixtures/toy_workflow.py @@ -12,9 +12,9 @@ from ergon_core.core.application.runtime.task_inspection import TaskInspectionService from ergon_core.core.application.runtime.task_management import TaskManagementService from ergon_core.core.persistence.definitions.models import ExperimentDefinitionTask -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord class ToySandbox(Sandbox): @@ -61,14 +61,14 @@ async def graph_mutation(self, row: object) -> None: return None -async def _dispatch_task_ready(run_id: UUID, definition_id: UUID, task_id: UUID) -> None: +async def _dispatch_task_ready(sample_id: UUID, definition_id: UUID, task_id: UUID) -> None: return None class ToyWorkflowHarness(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) - run_id: UUID + sample_id: UUID parent_task_id: UUID parent_task: Task context: WorkerContext @@ -85,18 +85,20 @@ def child_task(self, *, task_slug: str, description: str) -> Task: deep=True, ) - def nodes(self) -> list[RunGraphNode]: + def nodes(self) -> list[SampleGraphNode]: return list( self.session.exec( - select(RunGraphNode) - .where(RunGraphNode.run_id == self.run_id) - .order_by(RunGraphNode.task_slug) + select(SampleGraphNode) + .where(SampleGraphNode.sample_id == self.sample_id) + .order_by(SampleGraphNode.task_slug) ).all() ) - def edges(self) -> list[RunGraphEdge]: + def edges(self) -> list[SampleGraphEdge]: return list( - self.session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == self.run_id)).all() + self.session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == self.sample_id) + ).all() ) def definition_tasks(self) -> list[ExperimentDefinitionTask]: @@ -117,7 +119,7 @@ def make_toy_workflow_harness(monkeypatch: pytest.MonkeyPatch) -> ToyWorkflowHar lambda: _SessionContext(session), ) - run_id = uuid4() + sample_id = uuid4() definition_id = uuid4() parent_task = Task( task_slug="parent", @@ -128,17 +130,17 @@ def make_toy_workflow_harness(monkeypatch: pytest.MonkeyPatch) -> ToyWorkflowHar evaluators=(), ) session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="toy", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) - parent = RunGraphNode( - run_id=run_id, + parent = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug=parent_task.task_slug, description=parent_task.description, @@ -149,8 +151,8 @@ def make_toy_workflow_harness(monkeypatch: pytest.MonkeyPatch) -> ToyWorkflowHar parent_task_id=None, level=0, ) - dependency = RunGraphNode( - run_id=run_id, + dependency = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug="dependency", description="Dependency task", @@ -169,7 +171,7 @@ def make_toy_workflow_harness(monkeypatch: pytest.MonkeyPatch) -> ToyWorkflowHar session.commit() context = WorkerContext( - run_id=run_id, + sample_id=sample_id, task_id=parent.task_id, execution_id=uuid4(), definition_id=definition_id, @@ -183,7 +185,7 @@ def make_toy_workflow_harness(monkeypatch: pytest.MonkeyPatch) -> ToyWorkflowHar session_factory=lambda: _SessionContext(session), ) return ToyWorkflowHarness( - run_id=run_id, + sample_id=sample_id, parent_task_id=parent.task_id, parent_task=parent_task, context=context, diff --git a/ergon_builtins/tests/integration/tools/test_workflow_command_adapter.py b/ergon_builtins/tests/integration/tools/test_workflow_command_adapter.py index 951c22733..e73d8a11b 100644 --- a/ergon_builtins/tests/integration/tools/test_workflow_command_adapter.py +++ b/ergon_builtins/tests/integration/tools/test_workflow_command_adapter.py @@ -13,7 +13,7 @@ def _command_context(harness) -> WorkflowCommandContext: return WorkflowCommandContext( - run_id=harness.context.run_id, + sample_id=harness.context.sample_id, task_id=harness.context.task_id, execution_id=harness.context.execution_id, sandbox_task_key=harness.context.task_id, diff --git a/ergon_builtins/tests/unit/benchmarks/test_minif2f_proof_verification.py b/ergon_builtins/tests/unit/benchmarks/test_minif2f_proof_verification.py index 086e0dcdc..7b57dcdab 100644 --- a/ergon_builtins/tests/unit/benchmarks/test_minif2f_proof_verification.py +++ b/ergon_builtins/tests/unit/benchmarks/test_minif2f_proof_verification.py @@ -44,7 +44,7 @@ async def test_reads_proof_via_task_sandbox() -> None: sandbox.is_live = True context = CriterionContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), task=_make_task(sandbox), @@ -68,7 +68,7 @@ async def test_scores_zero_when_proof_missing() -> None: sandbox.read_file = AsyncMock(side_effect=OSError("missing")) context = CriterionContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), task=_make_task(sandbox), diff --git a/ergon_builtins/tests/unit/benchmarks/test_swebench_criterion_patch_source.py b/ergon_builtins/tests/unit/benchmarks/test_swebench_criterion_patch_source.py index fe6312f95..934bba639 100644 --- a/ergon_builtins/tests/unit/benchmarks/test_swebench_criterion_patch_source.py +++ b/ergon_builtins/tests/unit/benchmarks/test_swebench_criterion_patch_source.py @@ -69,9 +69,9 @@ async def test_criterion_computes_patch_via_run_command( # Worker produces empty output; criterion must still derive the patch # from the sandbox. - run_id = uuid4() + sample_id = uuid4() context = CriterionContext( - run_id=run_id, + sample_id=sample_id, task_id=uuid4(), execution_id=uuid4(), task=task, @@ -139,7 +139,7 @@ async def _empty_diff(cmd: str, timeout: int = 30) -> CommandResult: task.sandbox = sandbox context = CriterionContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), task=task, diff --git a/ergon_builtins/tests/unit/builtins/common/test_transcript_adapters.py b/ergon_builtins/tests/unit/builtins/common/test_transcript_adapters.py index d02af3f85..b052e57a1 100644 --- a/ergon_builtins/tests/unit/builtins/common/test_transcript_adapters.py +++ b/ergon_builtins/tests/unit/builtins/common/test_transcript_adapters.py @@ -13,7 +13,7 @@ UserMessagePart as ErgonUserMessagePart, ) from ergon_core.core.shared.context_parts import ContextEventType -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from pydantic_ai.messages import ( ModelRequest, ModelResponse, @@ -25,15 +25,15 @@ ) -def _make_event(part, sequence: int, turn_id: str | None = None) -> RunContextEvent: +def _make_event(part, sequence: int, turn_id: str | None = None) -> SampleContextEvent: payload = ContextPartChunkLog( part=part, sequence=sequence, worker_binding_key="test-worker", turn_id=turn_id, ) - return RunContextEvent( - run_id=uuid4(), + return SampleContextEvent( + sample_id=uuid4(), task_execution_id=uuid4(), worker_binding_key="test-worker", sequence=sequence, diff --git a/ergon_builtins/tests/unit/state/test_llm_judge_runtime_injection.py b/ergon_builtins/tests/unit/state/test_llm_judge_runtime_injection.py index c97f8a0fe..b73852e86 100644 --- a/ergon_builtins/tests/unit/state/test_llm_judge_runtime_injection.py +++ b/ergon_builtins/tests/unit/state/test_llm_judge_runtime_injection.py @@ -18,7 +18,7 @@ def _make_eval_context() -> CriterionContext: return CriterionContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), task=task_with_id( @@ -42,7 +42,7 @@ def test_runtime_field_is_removed(self): def test_context_is_frozen(self): ctx = _make_eval_context() with pytest.raises(Exception): # slopcop: ignore[no-broad-except] - ctx.run_id = uuid4() # type: ignore[misc] + ctx.sample_id = uuid4() # type: ignore[misc] class TestLLMJudgeCriterionWithRuntime: diff --git a/ergon_builtins/tests/unit/state/test_research_rubrics_benchmark.py b/ergon_builtins/tests/unit/state/test_research_rubrics_benchmark.py index d24657c4d..b9cf1a859 100644 --- a/ergon_builtins/tests/unit/state/test_research_rubrics_benchmark.py +++ b/ergon_builtins/tests/unit/state/test_research_rubrics_benchmark.py @@ -20,24 +20,24 @@ from ergon_core.api.criterion import CriterionContext from ergon_core.api.criterion import CriterionOutcome from ergon_core.api.worker import WorkerOutput -from ergon_core.core.application.resources import RunResourceView -from ergon_core.core.persistence.shared.enums import RunResourceKind +from ergon_core.core.application.resources import SampleResourceView +from ergon_core.core.persistence.shared.enums import SampleResourceKind from ergon_core.api.benchmark import Task from ergon_core.test_support.task_factory import task_with_id def _resource_view( *, - kind: RunResourceKind, + kind: SampleResourceKind, name: str, sandbox_origin: str, text: str, -) -> tuple[RunResourceView, bytes]: +) -> tuple[SampleResourceView, bytes]: resource_id = uuid4() return ( - RunResourceView( + SampleResourceView( id=resource_id, - run_id=uuid4(), + sample_id=uuid4(), task_execution_id=uuid4(), kind=kind, name=name, @@ -223,13 +223,13 @@ async def test_judge_prioritizes_final_resources_over_final_message( tmp_path: Path, ) -> None: final_resource, final_blob = _resource_view( - kind=RunResourceKind.REPORT, + kind=SampleResourceKind.REPORT, name="report.md", sandbox_origin="/workspace/final_output/report.md", text="# Final report\nThis is the primary answer artifact.", ) scratch_resource, scratch_blob = _resource_view( - kind=RunResourceKind.NOTE, + kind=SampleResourceKind.NOTE, name="notes.md", sandbox_origin="/workspace/notes.md", text="scratch notes", @@ -243,7 +243,7 @@ async def test_judge_prioritizes_final_resources_over_final_message( captured_user_prompts: list[str] = [] context = CriterionContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), task=task_with_id( diff --git a/ergon_builtins/tests/unit/tools/test_workflow_cli_tool.py b/ergon_builtins/tests/unit/tools/test_workflow_cli_tool.py index f2e66805e..c9357a138 100644 --- a/ergon_builtins/tests/unit/tools/test_workflow_cli_tool.py +++ b/ergon_builtins/tests/unit/tools/test_workflow_cli_tool.py @@ -33,7 +33,7 @@ class _ResourceService: def _worker_context() -> WorkerContext: return WorkerContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), sandbox_id="sandbox", @@ -96,7 +96,7 @@ def execute(command, *, context, worker_context, session_factory, service): assert await workflow("inspect task-tree") == "ok" assert seen["command"] == "inspect task-tree" - assert seen["context"].run_id == context.run_id + assert seen["context"].sample_id == context.sample_id assert seen["context"].task_id == context.task_id assert seen["context"].execution_id == context.execution_id assert seen["context"].sandbox_task_key == context.task_id @@ -145,15 +145,15 @@ async def test_workflow_adapter_add_subtask_spawns_object_bound_child() -> None: spawned = {} class TaskManagement: - async def spawn_dynamic_task(self, *, run_id, parent_task_id, task, depends_on=()): - spawned["run_id"] = run_id + async def spawn_dynamic_task(self, *, sample_id, parent_task_id, task, depends_on=()): + spawned["sample_id"] = sample_id spawned["parent_task_id"] = parent_task_id spawned["task"] = task spawned["depends_on"] = depends_on return SpawnedTaskHandle(task_id=uuid4()) worker_context = WorkerContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), sandbox_id="sandbox", @@ -180,7 +180,7 @@ def close(self): output = await execute_workflow_command( "manage add-subtask --task-slug child --description 'Child task' --format json", context=WorkflowCommandContext( - run_id=worker_context.run_id, + sample_id=worker_context.sample_id, task_id=worker_context.task_id, execution_id=worker_context.execution_id, sandbox_task_key=worker_context.task_id, @@ -192,7 +192,7 @@ def close(self): assert output.exit_code == 0 assert "child" in output.stdout - assert spawned["run_id"] == worker_context.run_id + assert spawned["sample_id"] == worker_context.sample_id assert spawned["parent_task_id"] == worker_context.task_id assert spawned["task"].task_slug == "child" assert spawned["task"].description == "Child task" @@ -210,7 +210,7 @@ class Service: def __init__(self) -> None: self.wait_observed = False - def list_tasks(self, session, *, run_id, parent_task_id=None): + def list_tasks(self, session, *, sample_id, parent_task_id=None): self.wait_observed = True return [ GraphTaskRef( @@ -228,7 +228,7 @@ def list_tasks(self, session, *, run_id, parent_task_id=None): output = await execute_workflow_command( "inspect task-tree --wait-seconds 0.1 --format json", context=WorkflowCommandContext( - run_id=worker_context.run_id, + sample_id=worker_context.sample_id, task_id=worker_context.task_id, execution_id=worker_context.execution_id, sandbox_task_key=worker_context.task_id, diff --git a/ergon_builtins/tests/unit/workers/test_react_worker_contract.py b/ergon_builtins/tests/unit/workers/test_react_worker_contract.py index 32a959632..3afb7fc4f 100644 --- a/ergon_builtins/tests/unit/workers/test_react_worker_contract.py +++ b/ergon_builtins/tests/unit/workers/test_react_worker_contract.py @@ -197,7 +197,7 @@ def _minimal_task() -> Task: def _minimal_context() -> WorkerContext: return WorkerContext( - run_id=UUID(int=3), + sample_id=UUID(int=3), definition_id=UUID(int=4), execution_id=UUID(int=5), sandbox_id="test-sandbox", diff --git a/ergon_cli/ergon_cli/app.py b/ergon_cli/ergon_cli/app.py index e586254ff..020d5da30 100644 --- a/ergon_cli/ergon_cli/app.py +++ b/ergon_cli/ergon_cli/app.py @@ -10,7 +10,7 @@ from ergon_cli.domains.experiments.parser import register_experiment_parser from ergon_cli.domains.ingestion.parser import register_ingest_parser from ergon_cli.domains.onboarding.parser import register_onboard_parser -from ergon_cli.domains.runs.parser import register_run_parser +from ergon_cli.domains.samples.parser import register_sample_parser from ergon_cli.domains.stack.parser import register_stack_parser from ergon_cli.domains.tests.parser import register_test_parser from ergon_cli.domains.workers.parser import register_worker_parser @@ -22,7 +22,7 @@ def build_parser() -> argparse.ArgumentParser: register_benchmark_parser(subparsers) register_experiment_parser(subparsers) - register_run_parser(subparsers) + register_sample_parser(subparsers) register_ingest_parser(subparsers) register_worker_parser(subparsers) register_evaluator_parser(subparsers) diff --git a/ergon_cli/ergon_cli/domains/experiments/commands.py b/ergon_cli/ergon_cli/domains/experiments/commands.py index 4c7bdf503..f9f742b6e 100644 --- a/ergon_cli/ergon_cli/domains/experiments/commands.py +++ b/ergon_cli/ergon_cli/domains/experiments/commands.py @@ -57,11 +57,11 @@ def handle_experiment_show(args: Namespace) -> int: if detail.sample_selection: lines.append(f"SAMPLE_SELECTION={detail.sample_selection}") if detail.runs: - lines.append("RUNS") + lines.append("SAMPLES") lines.extend( "\t".join( [ - str(run.run_id), + str(run.sample_id), run.instance_key, run.status, "" if run.model_target is None else run.model_target, @@ -78,7 +78,7 @@ def handle_experiment_list(args: Namespace) -> int: if not experiments: print("No experiments found.") return exit_codes.OK - lines = ["DEFINITION_ID\tNAME\tBENCHMARK\tSTATUS\tSAMPLES\tRUNS\tMODEL"] + lines = ["DEFINITION_ID\tNAME\tBENCHMARK\tSTATUS\tSAMPLES\tATTEMPTS\tMODEL"] lines.extend( "\t".join( [ diff --git a/ergon_cli/ergon_cli/domains/experiments/models.py b/ergon_cli/ergon_cli/domains/experiments/models.py index 3b656f10b..e7dc7aff2 100644 --- a/ergon_cli/ergon_cli/domains/experiments/models.py +++ b/ergon_cli/ergon_cli/domains/experiments/models.py @@ -38,7 +38,7 @@ class ExperimentSummaryView(BaseModel): class ExperimentRunView(BaseModel): model_config = ConfigDict(frozen=True) - run_id: UUID + sample_id: UUID instance_key: str status: str model_target: str | None = None diff --git a/ergon_cli/ergon_cli/domains/experiments/service.py b/ergon_cli/ergon_cli/domains/experiments/service.py index d67bbb22b..e747383ed 100644 --- a/ergon_cli/ergon_cli/domains/experiments/service.py +++ b/ergon_cli/ergon_cli/domains/experiments/service.py @@ -80,7 +80,7 @@ def _detail_view(detail: ExperimentDetailDto) -> ExperimentDetailView: sample_selection=detail.sample_selection, runs=tuple( ExperimentRunView( - run_id=run.run_id, + sample_id=run.sample_id, instance_key=run.instance_key, status=run.status, model_target=run.model_target, diff --git a/ergon_cli/ergon_cli/domains/runs/commands.py b/ergon_cli/ergon_cli/domains/runs/commands.py deleted file mode 100644 index a2d8f1368..000000000 --- a/ergon_cli/ergon_cli/domains/runs/commands.py +++ /dev/null @@ -1,108 +0,0 @@ -from argparse import Namespace - -from ergon_cli.domains.runs.models import CancelRunCommand, ListRunsCommand, RunStatusCommand -from ergon_cli.domains.runs.service import cancel_existing_run, get_run_status, list_runs -from ergon_cli.shared import exit_codes -from ergon_cli.shared.errors import CliError -from ergon_cli.shared.output import render_table, render_text -from ergon_cli.shared.parsing import parse_uuid - - -def handle_run(args: Namespace) -> int: - if args.run_action == "list": - return list_runs_command(args) - if args.run_action == "cancel": - return cancel_run_command(args) - if args.run_action == "status": - return status_run_command(args) - print("Usage: ergon run {list|status|cancel}") - return exit_codes.RUNTIME_ERROR - - -def list_runs_command(args: Namespace) -> int: - try: - definition_id = ( - parse_uuid(args.definition_id, field_name="definition_id") - if args.definition_id - else None - ) - result = list_runs( - ListRunsCommand( - limit=args.limit, - status=args.status, - definition_id=definition_id, - experiment=args.experiment, - ) - ) - except CliError as exc: - print(exc.message) - return exc.exit_code - - if not result.runs: - parts = ["No runs found"] - if result.status: - parts.append(f"with status={result.status!r}") - if result.definition_id: - parts.append(f"for definition_id={str(result.definition_id)!r}") - if result.experiment: - parts.append(f"for experiment={result.experiment!r}") - print(" ".join(parts)) - return exit_codes.OK - - print( - render_table( - ["ID (short)", "Status", "Created", "Duration", "Full ID"], - [ - [str(run.id)[:8], run.status, run.created, run.duration, str(run.id)] - for run in result.runs - ], - ) - ) - return exit_codes.OK - - -def cancel_run_command(args: Namespace) -> int: - try: - result = cancel_existing_run(CancelRunCommand(run_id=parse_uuid(args.run_id))) - except CliError as exc: - print(exc.message) - return exc.exit_code - print( - render_text( - [ - f"Run {result.run.id} cancelled.", - f" Status: {result.run.status}", - " Inngest: run/cancelled event sent (in-flight functions will be killed)", - " Cleanup: run/cleanup event sent (sandbox teardown scheduled)", - ] - ) - ) - return exit_codes.OK - - -def status_run_command(args: Namespace) -> int: - try: - run = get_run_status(RunStatusCommand(run_id=parse_uuid(args.run_id))) - except CliError as exc: - print(exc.message) - return exc.exit_code - lines = [ - f"run_id: {run.id}", - f"status: {run.status}", - f"benchmark_type: {run.benchmark_type}", - f"definition_id: {run.definition_id}", - f"instance_key: {run.instance_key}", - ] - if run.evaluator_slug is not None: - lines.append(f"evaluator: {run.evaluator_slug}") - if run.model_target is not None: - lines.append(f"model: {run.model_target}") - lines.append(f"created_at: {run.created}") - if run.started: - lines.append(f"started_at: {run.started}") - if run.completed: - lines.append(f"completed_at: {run.completed}") - if run.error_message: - lines.append(f"error: {run.error_message}") - print(render_text(lines)) - return exit_codes.OK diff --git a/ergon_cli/ergon_cli/domains/runs/parser.py b/ergon_cli/ergon_cli/domains/runs/parser.py deleted file mode 100644 index 11b9e71b8..000000000 --- a/ergon_cli/ergon_cli/domains/runs/parser.py +++ /dev/null @@ -1,30 +0,0 @@ -import argparse - -from ergon_cli.domains.runs.commands import handle_run - - -def register_run_parser(subparsers: argparse._SubParsersAction) -> None: - run = subparsers.add_parser("run", help="Run management") - run.set_defaults(handler=handle_run) - run_sub = run.add_subparsers(dest="run_action") - run_list_parser = run_sub.add_parser("list", help="List recent runs") - run_list_parser.add_argument("--limit", type=int, default=20, help="Number of runs to show") - run_list_parser.add_argument( - "--status", - default=None, - help="Filter by status (pending, executing, completed, failed, cancelled)", - ) - run_list_parser.add_argument( - "--definition-id", - default=None, - help="Filter by definition UUID", - ) - run_list_parser.add_argument( - "--experiment", - default=None, - help="Filter by v2 experiment tag", - ) - run_cancel_parser = run_sub.add_parser("cancel", help="Cancel a running experiment") - run_cancel_parser.add_argument("run_id", help="Run ID (UUID) to cancel") - run_status_parser = run_sub.add_parser("status", help="Show status of one run") - run_status_parser.add_argument("run_id", help="Run ID (UUID)") diff --git a/ergon_cli/ergon_cli/domains/runs/service.py b/ergon_cli/ergon_cli/domains/runs/service.py deleted file mode 100644 index b1e7f657f..000000000 --- a/ergon_cli/ergon_cli/domains/runs/service.py +++ /dev/null @@ -1,74 +0,0 @@ -from ergon_cli.domains.runs.models import ( - CancelRunCommand, - CancelRunResult, - ListRunsCommand, - RunListResult, - RunStatusCommand, - RunSummaryView, -) -from ergon_cli.shared.errors import CliNotFoundError -from ergon_core.core.application.runtime.run_records import cancel_run -from ergon_core.core.views.runs.models import RunSummaryDto -from ergon_core.core.views.runs.service import RunReadService - - -def list_runs( - command: ListRunsCommand, *, read_service: RunReadService | None = None -) -> RunListResult: - service = read_service or RunReadService() - rows = service.list_runs( - limit=command.limit, - status=command.status, - definition_id=command.definition_id, - experiment=command.experiment, - ) - return RunListResult( - runs=tuple(_view(row) for row in rows), - status=command.status, - definition_id=command.definition_id, - experiment=command.experiment, - ) - - -def get_run_status( - command: RunStatusCommand, - *, - read_service: RunReadService | None = None, -) -> RunSummaryView: - service = read_service or RunReadService() - row = service.get_run_summary(command.run_id) - if row is None: - raise CliNotFoundError(f"No run found with id {command.run_id}") - return _view(row) - - -def cancel_existing_run(command: CancelRunCommand) -> CancelRunResult: - try: - run = cancel_run(command.run_id) - except ValueError as exc: - raise CliNotFoundError(f"Error: {exc}") from exc - return CancelRunResult(run=_view(RunSummaryDto.model_validate(run, from_attributes=True))) - - -def _view(row: RunSummaryDto) -> RunSummaryView: - created = row.created_at.strftime("%Y-%m-%d %H:%M") if row.created_at else "-" - started = row.started_at.strftime("%Y-%m-%d %H:%M:%S") if row.started_at else None - completed = row.completed_at.strftime("%Y-%m-%d %H:%M:%S") if row.completed_at else None - duration = "" - if row.started_at and row.completed_at: - delta = row.completed_at - row.started_at - duration = f"{int(delta.total_seconds())}s" - return RunSummaryView( - id=row.id, - status=row.status, - created=created, - started=started, - completed=completed, - duration=duration, - definition_id=row.definition_id, - benchmark_type=row.benchmark_type, - instance_key=row.instance_key, - evaluator_slug=row.evaluator_slug, - model_target=row.model_target, - error_message=row.error_message, - ) diff --git a/ergon_cli/ergon_cli/domains/runs/__init__.py b/ergon_cli/ergon_cli/domains/samples/__init__.py similarity index 100% rename from ergon_cli/ergon_cli/domains/runs/__init__.py rename to ergon_cli/ergon_cli/domains/samples/__init__.py diff --git a/ergon_cli/ergon_cli/domains/samples/commands.py b/ergon_cli/ergon_cli/domains/samples/commands.py new file mode 100644 index 000000000..cebd4b847 --- /dev/null +++ b/ergon_cli/ergon_cli/domains/samples/commands.py @@ -0,0 +1,116 @@ +from argparse import Namespace + +from ergon_cli.domains.samples.models import ( + CancelSampleCommand, + ListSamplesCommand, + SampleStatusCommand, +) +from ergon_cli.domains.samples.service import ( + cancel_existing_sample, + get_sample_status, + list_samples, +) +from ergon_cli.shared import exit_codes +from ergon_cli.shared.errors import CliError +from ergon_cli.shared.output import render_table, render_text +from ergon_cli.shared.parsing import parse_uuid + + +def handle_sample(args: Namespace) -> int: + if args.sample_action == "list": + return list_samples_command(args) + if args.sample_action == "cancel": + return cancel_sample_command(args) + if args.sample_action == "status": + return status_sample_command(args) + print("Usage: ergon sample {list|status|cancel}") + return exit_codes.RUNTIME_ERROR + + +def list_samples_command(args: Namespace) -> int: + try: + definition_id = ( + parse_uuid(args.definition_id, field_name="definition_id") + if args.definition_id + else None + ) + result = list_samples( + ListSamplesCommand( + limit=args.limit, + status=args.status, + definition_id=definition_id, + experiment=args.experiment, + ) + ) + except CliError as exc: + print(exc.message) + return exc.exit_code + + if not result.samples: + parts = ["No samples found"] + if result.status: + parts.append(f"with status={result.status!r}") + if result.definition_id: + parts.append(f"for definition_id={str(result.definition_id)!r}") + if result.experiment: + parts.append(f"for experiment={result.experiment!r}") + print(" ".join(parts)) + return exit_codes.OK + + print( + render_table( + ["ID (short)", "Status", "Created", "Duration", "Full ID"], + [ + [str(sample.id)[:8], sample.status, sample.created, sample.duration, str(sample.id)] + for sample in result.samples + ], + ) + ) + return exit_codes.OK + + +def cancel_sample_command(args: Namespace) -> int: + try: + result = cancel_existing_sample(CancelSampleCommand(sample_id=parse_uuid(args.sample_id))) + except CliError as exc: + print(exc.message) + return exc.exit_code + print( + render_text( + [ + f"Sample {result.sample.id} cancelled.", + f" Status: {result.sample.status}", + " Inngest: sample/cancelled event sent (in-flight functions will be killed)", + " Cleanup: sample/cleanup event sent (sandbox teardown scheduled)", + ] + ) + ) + return exit_codes.OK + + +def status_sample_command(args: Namespace) -> int: + try: + sample = get_sample_status(SampleStatusCommand(sample_id=parse_uuid(args.sample_id))) + except CliError as exc: + print(exc.message) + return exc.exit_code + lines = [ + f"sample_id: {sample.id}", + f"status: {sample.status}", + f"benchmark_type: {sample.benchmark_type}", + f"definition_id: {sample.definition_id}", + f"instance_key: {sample.instance_key}", + ] + if sample.evaluator_slug is not None: + lines.append(f"evaluator: {sample.evaluator_slug}") + if sample.model_target is not None: + lines.append(f"model: {sample.model_target}") + lines.append(f"created_at: {sample.created}") + if sample.started: + lines.append(f"started_at: {sample.started}") + if sample.completed: + lines.append(f"completed_at: {sample.completed}") + if sample.error_message: + lines.append(f"error: {sample.error_message}") + print(render_text(lines)) + return exit_codes.OK diff --git a/ergon_cli/ergon_cli/domains/runs/models.py b/ergon_cli/ergon_cli/domains/samples/models.py similarity index 72% rename from ergon_cli/ergon_cli/domains/runs/models.py rename to ergon_cli/ergon_cli/domains/samples/models.py index f662e393f..235551ca5 100644 --- a/ergon_cli/ergon_cli/domains/runs/models.py +++ b/ergon_cli/ergon_cli/domains/samples/models.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, ConfigDict -class ListRunsCommand(BaseModel): +class ListSamplesCommand(BaseModel): model_config = ConfigDict(frozen=True) limit: int = 20 @@ -12,19 +12,19 @@ class ListRunsCommand(BaseModel): experiment: str | None = None -class RunStatusCommand(BaseModel): +class SampleStatusCommand(BaseModel): model_config = ConfigDict(frozen=True) - run_id: UUID + sample_id: UUID -class CancelRunCommand(BaseModel): +class CancelSampleCommand(BaseModel): model_config = ConfigDict(frozen=True) - run_id: UUID + sample_id: UUID -class RunSummaryView(BaseModel): +class SampleSummaryView(BaseModel): model_config = ConfigDict(frozen=True) id: UUID @@ -41,16 +41,16 @@ class RunSummaryView(BaseModel): error_message: str | None = None -class RunListResult(BaseModel): +class SampleListResult(BaseModel): model_config = ConfigDict(frozen=True) - runs: tuple[RunSummaryView, ...] + samples: tuple[SampleSummaryView, ...] status: str | None = None definition_id: UUID | None = None experiment: str | None = None -class CancelRunResult(BaseModel): +class CancelSampleResult(BaseModel): model_config = ConfigDict(frozen=True) - run: RunSummaryView + sample: SampleSummaryView diff --git a/ergon_cli/ergon_cli/domains/samples/parser.py b/ergon_cli/ergon_cli/domains/samples/parser.py new file mode 100644 index 000000000..d190ef054 --- /dev/null +++ b/ergon_cli/ergon_cli/domains/samples/parser.py @@ -0,0 +1,32 @@ +import argparse + +from ergon_cli.domains.samples.commands import handle_sample + + +def register_sample_parser(subparsers: argparse._SubParsersAction) -> None: + sample = subparsers.add_parser("sample", help="Sample management") + sample.set_defaults(handler=handle_sample) + sample_sub = sample.add_subparsers(dest="sample_action") + sample_list_parser = sample_sub.add_parser("list", help="List recent samples") + sample_list_parser.add_argument( + "--limit", type=int, default=20, help="Number of samples to show" + ) + sample_list_parser.add_argument( + "--status", + default=None, + help="Filter by status (pending, executing, completed, failed, cancelled)", + ) + sample_list_parser.add_argument( + "--definition-id", + default=None, + help="Filter by definition UUID", + ) + sample_list_parser.add_argument( + "--experiment", + default=None, + help="Filter by v2 experiment tag", + ) + sample_cancel_parser = sample_sub.add_parser("cancel", help="Cancel a running sample") + sample_cancel_parser.add_argument("sample_id", help="Sample ID (UUID) to cancel") + sample_status_parser = sample_sub.add_parser("status", help="Show status of one sample") + sample_status_parser.add_argument("sample_id", help="Sample ID (UUID)") diff --git a/ergon_cli/ergon_cli/domains/samples/service.py b/ergon_cli/ergon_cli/domains/samples/service.py new file mode 100644 index 000000000..0f22d4c05 --- /dev/null +++ b/ergon_cli/ergon_cli/domains/samples/service.py @@ -0,0 +1,76 @@ +from ergon_cli.domains.samples.models import ( + CancelSampleCommand, + CancelSampleResult, + ListSamplesCommand, + SampleListResult, + SampleStatusCommand, + SampleSummaryView, +) +from ergon_cli.shared.errors import CliNotFoundError +from ergon_core.core.application.runtime.sample_records import cancel_sample +from ergon_core.core.views.samples.models import SampleSummaryDto +from ergon_core.core.views.samples.service import SampleSnapshotReadService + + +def list_samples( + command: ListSamplesCommand, *, read_service: SampleSnapshotReadService | None = None +) -> SampleListResult: + service = read_service or SampleSnapshotReadService() + rows = service.list_samples( + limit=command.limit, + status=command.status, + definition_id=command.definition_id, + experiment=command.experiment, + ) + return SampleListResult( + samples=tuple(_view(row) for row in rows), + status=command.status, + definition_id=command.definition_id, + experiment=command.experiment, + ) + + +def get_sample_status( + command: SampleStatusCommand, + *, + read_service: SampleSnapshotReadService | None = None, +) -> SampleSummaryView: + service = read_service or SampleSnapshotReadService() + row = service.get_sample_summary(command.sample_id) + if row is None: + raise CliNotFoundError(f"No sample found with id {command.sample_id}") + return _view(row) + + +def cancel_existing_sample(command: CancelSampleCommand) -> CancelSampleResult: + try: + sample = cancel_sample(command.sample_id) + except ValueError as exc: + raise CliNotFoundError(f"Error: {exc}") from exc + return CancelSampleResult( + sample=_view(SampleSummaryDto.model_validate(sample, from_attributes=True)) + ) + + +def _view(row: SampleSummaryDto) -> SampleSummaryView: + created = row.created_at.strftime("%Y-%m-%d %H:%M") if row.created_at else "-" + started = row.started_at.strftime("%Y-%m-%d %H:%M:%S") if row.started_at else None + completed = row.completed_at.strftime("%Y-%m-%d %H:%M:%S") if row.completed_at else None + duration = "" + if row.started_at and row.completed_at: + delta = row.completed_at - row.started_at + duration = f"{int(delta.total_seconds())}s" + return SampleSummaryView( + id=row.id, + status=row.status, + created=created, + started=started, + completed=completed, + duration=duration, + definition_id=row.definition_id, + benchmark_type=row.benchmark_type, + instance_key=row.instance_key, + evaluator_slug=row.evaluator_slug, + model_target=row.model_target, + error_message=row.error_message, + ) diff --git a/ergon_cli/ergon_cli/domains/workflow/commands.py b/ergon_cli/ergon_cli/domains/workflow/commands.py index 9aa32f56a..203a5c4b2 100644 --- a/ergon_cli/ergon_cli/domains/workflow/commands.py +++ b/ergon_cli/ergon_cli/domains/workflow/commands.py @@ -2,7 +2,7 @@ from ergon_cli.domains.workflow.context import build_workflow_context from ergon_cli.domains.workflow.executor import build_workflow_parser, execute_workflow_command -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from ergon_core.core.persistence.shared.db import get_session @@ -15,7 +15,7 @@ async def handle_workflow(args: argparse.Namespace) -> int: missing = [ name for name, value in { - "--run-id": args.run_id, + "--sample-id": args.sample_id, "--task-id": args.task_id, "--execution-id": args.execution_id, "--sandbox-task-key": args.sandbox_task_key, @@ -27,7 +27,7 @@ async def handle_workflow(args: argparse.Namespace) -> int: output = execute_workflow_command( command, context=build_workflow_context( - run_id=args.run_id, + sample_id=args.sample_id, task_id=args.task_id, execution_id=args.execution_id, sandbox_task_key=args.sandbox_task_key, diff --git a/ergon_cli/ergon_cli/domains/workflow/context.py b/ergon_cli/ergon_cli/domains/workflow/context.py index d312cc1f1..07ae11e0a 100644 --- a/ergon_cli/ergon_cli/domains/workflow/context.py +++ b/ergon_cli/ergon_cli/domains/workflow/context.py @@ -5,14 +5,14 @@ def build_workflow_context( *, - run_id: str, + sample_id: str, task_id: str, execution_id: str, sandbox_task_key: str, benchmark_type: str, ) -> WorkflowCommandContext: return WorkflowCommandContext( - run_id=UUID(run_id), + sample_id=UUID(sample_id), task_id=UUID(task_id), execution_id=UUID(execution_id), sandbox_task_key=UUID(sandbox_task_key), diff --git a/ergon_cli/ergon_cli/domains/workflow/executor.py b/ergon_cli/ergon_cli/domains/workflow/executor.py index d8c9ddbcb..599b0c65d 100644 --- a/ergon_cli/ergon_cli/domains/workflow/executor.py +++ b/ergon_cli/ergon_cli/domains/workflow/executor.py @@ -9,7 +9,7 @@ from uuid import UUID from ergon_cli.domains.workflow.models import WorkflowCommandContext, WorkflowCommandOutput -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from ergon_core.core.shared.json_types import JsonObject from pydantic import BaseModel from sqlmodel import Session @@ -20,7 +20,7 @@ _DEPENDENCY_DIRECTIONS = ("upstream", "downstream", "both") _FORBIDDEN_CONTEXT_FLAGS = { - "--run-id", + "--sample-id", "--task-id", "--node-id", "--execution-id", @@ -126,7 +126,7 @@ def _handle_inspect( if args.action == "resource-list": resources = service.list_resources( session, - run_id=context.run_id, + sample_id=context.sample_id, task_id=context.task_id, scope=args.scope, kind=args.kind, @@ -146,7 +146,7 @@ def _handle_inspect( resource_id = UUID(args.resource_id) content = service.read_resource_bytes( session, - run_id=context.run_id, + sample_id=context.sample_id, resource_id=resource_id, max_bytes=args.max_bytes, ) @@ -156,7 +156,7 @@ def _handle_inspect( if args.action == "task-tree": parent = UUID(args.parent_task_id) if args.parent_task_id else None deadline = time.monotonic() + max(args.wait_seconds, 0) - tasks = service.list_tasks(session, run_id=context.run_id, parent_task_id=parent) + tasks = service.list_tasks(session, sample_id=context.sample_id, parent_task_id=parent) while args.wait_seconds > 0 and time.monotonic() < deadline: children = [task for task in tasks if task.parent_task_id == context.task_id] if children and all( @@ -164,7 +164,7 @@ def _handle_inspect( ): break time.sleep(2) - tasks = service.list_tasks(session, run_id=context.run_id, parent_task_id=parent) + tasks = service.list_tasks(session, sample_id=context.sample_id, parent_task_id=parent) return _format_output( {"tasks": [_dump(task) for task in tasks]}, text_lines=[ @@ -176,7 +176,7 @@ def _handle_inspect( if args.action == "task-dependencies": deps = service.list_dependencies( session, - run_id=context.run_id, + sample_id=context.sample_id, task_id=context.task_id, direction=args.direction, ) @@ -191,7 +191,7 @@ def _handle_inspect( if args.action == "next-actions": actions = service.get_next_actions( session, - run_id=context.run_id, + sample_id=context.sample_id, task_id=context.task_id, manager_capable=args.manager_capable, ) diff --git a/ergon_cli/ergon_cli/domains/workflow/models.py b/ergon_cli/ergon_cli/domains/workflow/models.py index 8fb7ad5db..948b45e3b 100644 --- a/ergon_cli/ergon_cli/domains/workflow/models.py +++ b/ergon_cli/ergon_cli/domains/workflow/models.py @@ -6,7 +6,7 @@ class WorkflowCommandContext(BaseModel): model_config = ConfigDict(frozen=True) - run_id: UUID + sample_id: UUID task_id: UUID execution_id: UUID sandbox_task_key: UUID diff --git a/ergon_cli/ergon_cli/domains/workflow/parser.py b/ergon_cli/ergon_cli/domains/workflow/parser.py index 51be0e7bd..c67f44258 100644 --- a/ergon_cli/ergon_cli/domains/workflow/parser.py +++ b/ergon_cli/ergon_cli/domains/workflow/parser.py @@ -6,7 +6,7 @@ def register_workflow_parser(subparsers: argparse._SubParsersAction) -> None: workflow = subparsers.add_parser("workflow", help="Workflow topology and resource operations") workflow.set_defaults(handler=handle_workflow) - workflow.add_argument("--run-id", default=None, help="Current run UUID") + workflow.add_argument("--sample-id", default=None, help="Current run UUID") workflow.add_argument("--task-id", default=None, help="Current task UUID") workflow.add_argument("--execution-id", default=None, help="Current task execution UUID") workflow.add_argument("--sandbox-task-key", default=None, help="Sandbox task key UUID") diff --git a/ergon_cli/tests/unit/cli/test_experiment_cli.py b/ergon_cli/tests/unit/cli/test_experiment_cli.py index e0bc25554..b67a4d3ac 100644 --- a/ergon_cli/tests/unit/cli/test_experiment_cli.py +++ b/ergon_cli/tests/unit/cli/test_experiment_cli.py @@ -91,7 +91,7 @@ def list_experiments(self, *, limit: int): def test_experiment_show_prints_detail(monkeypatch, capsys): - run_id = uuid4() + sample_id = uuid4() class FakeReadService: def get_experiment(self, definition_id): @@ -99,14 +99,14 @@ def get_experiment(self, definition_id): experiment=_summary(definition_id=definition_id), runs=[ ExperimentRunRowDto( - run_id=run_id, + sample_id=sample_id, definition_id=uuid4(), benchmark_type="ci-benchmark", instance_key="sample-a", status="completed", created_at="2026-04-27T12:00:00Z", metrics=ExperimentRunMetricsDto( - run_id=run_id, + sample_id=sample_id, status="completed", instance_key="sample-a", ), @@ -123,7 +123,7 @@ def get_experiment(self, definition_id): assert rc == 0 out = capsys.readouterr().out assert str(definition_id) in out - assert str(run_id) in out + assert str(sample_id) in out assert "sample-a" in out diff --git a/ergon_cli/tests/unit/cli/test_parser_registration.py b/ergon_cli/tests/unit/cli/test_parser_registration.py index f7ab5cdf4..346777313 100644 --- a/ergon_cli/tests/unit/cli/test_parser_registration.py +++ b/ergon_cli/tests/unit/cli/test_parser_registration.py @@ -13,7 +13,7 @@ ["experiment", "show", "00000000-0000-0000-0000-000000000000"], {"command": "experiment", "experiment_action": "show"}, ), - (["run", "list", "--limit", "3"], {"command": "run", "run_action": "list"}), + (["sample", "list", "--limit", "3"], {"command": "sample", "sample_action": "list"}), (["ingest", "list"], {"command": "ingest", "ingest_action": "list"}), (["worker", "list"], {"command": "worker", "worker_action": "list"}), (["evaluator", "list"], {"command": "evaluator", "evaluator_action": "list"}), diff --git a/ergon_cli/tests/unit/cli/test_run_cli.py b/ergon_cli/tests/unit/cli/test_sample_cli.py similarity index 57% rename from ergon_cli/tests/unit/cli/test_run_cli.py rename to ergon_cli/tests/unit/cli/test_sample_cli.py index 286d67c53..8a74d7fbc 100644 --- a/ergon_cli/tests/unit/cli/test_run_cli.py +++ b/ergon_cli/tests/unit/cli/test_sample_cli.py @@ -1,17 +1,17 @@ -"""Tests for the ``run`` CLI subcommands.""" +"""Tests for the ``sample`` CLI subcommands.""" from argparse import Namespace from datetime import UTC, datetime from uuid import uuid4 import pytest -import ergon_cli.domains.runs.commands as run_cmd +import ergon_cli.domains.samples.commands as sample_cmd from ergon_cli.main import build_parser -import ergon_core.core.views.runs.service as core_run_views +import ergon_core.core.views.samples.service as core_sample_views from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine @@ -21,24 +21,24 @@ # --------------------------------------------------------------------------- -def test_run_subcommands_are_registered_in_main_parser() -> None: +def test_sample_subcommands_are_registered_in_main_parser() -> None: parser = build_parser() - status_args = parser.parse_args(["run", "status", str(uuid4())]) + status_args = parser.parse_args(["sample", "status", str(uuid4())]) definition_id = uuid4() - list_args = parser.parse_args(["run", "list", "--definition-id", str(definition_id)]) + list_args = parser.parse_args(["sample", "list", "--definition-id", str(definition_id)]) - assert status_args.run_action == "status" - assert list_args.run_action == "list" + assert status_args.sample_action == "status" + assert list_args.sample_action == "list" assert list_args.definition_id == str(definition_id) -def test_run_list_accepts_experiment_tag_filter() -> None: +def test_sample_list_accepts_experiment_tag_filter() -> None: parser = build_parser() - list_args = parser.parse_args(["run", "list", "--experiment", "alpha"]) + list_args = parser.parse_args(["sample", "list", "--experiment", "alpha"]) - assert list_args.run_action == "list" + assert list_args.sample_action == "list" assert list_args.experiment == "alpha" @@ -59,8 +59,8 @@ def session_factory(): engine, tables=[ ExperimentDefinition.__table__, - RunRecord.__table__, - RunGraphNode.__table__, + SampleRecord.__table__, + SampleGraphNode.__table__, ], ) @@ -84,34 +84,34 @@ def _definition(*, name: str) -> ExperimentDefinition: ) -def _run_record(*, definition_id: object, experiment: str | None = None) -> RunRecord: - return RunRecord( +def _sample_record(*, definition_id: object, experiment: str | None = None) -> SampleRecord: + return SampleRecord( id=uuid4(), definition_id=definition_id, benchmark_type="ci-benchmark", instance_key="k", worker_team_json={}, experiment=experiment, - status=RunStatus.PENDING, + status=SampleStatus.PENDING, created_at=datetime(2026, 1, 1, tzinfo=UTC), ) # --------------------------------------------------------------------------- -# test_run_status_prints_status_fields +# test_sample_status_prints_status_fields # --------------------------------------------------------------------------- -def test_run_status_prints_status_fields(monkeypatch, capsys): - run_id = uuid4() +def test_sample_status_prints_status_fields(monkeypatch, capsys): + sample_id = uuid4() definition_id = uuid4() - fake_run = RunRecord( - id=run_id, + fake_sample = SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="ci-benchmark", instance_key="sample-1", worker_team_json={}, - status=RunStatus.COMPLETED, + status=SampleStatus.COMPLETED, created_at=datetime(2026, 3, 1, 12, 0, tzinfo=UTC), started_at=datetime(2026, 3, 1, 12, 1, tzinfo=UTC), completed_at=datetime(2026, 3, 1, 12, 3, tzinfo=UTC), @@ -125,16 +125,16 @@ def __exit__(self, *args): pass def get(self, model, pk): - assert pk == run_id - return fake_run + assert pk == sample_id + return fake_sample - monkeypatch.setattr(core_run_views, "get_session", lambda: FakeSession()) + monkeypatch.setattr(core_sample_views, "get_session", lambda: FakeSession()) - rc = run_cmd.status_run_command(Namespace(run_id=str(run_id))) + rc = sample_cmd.status_sample_command(Namespace(sample_id=str(sample_id))) assert rc == 0 out = capsys.readouterr().out - assert str(run_id) in out + assert str(sample_id) in out assert "completed" in out assert "ci-benchmark" in out assert "sample-1" in out @@ -143,12 +143,12 @@ def get(self, model, pk): # --------------------------------------------------------------------------- -# test_run_status_reports_invalid_uuid +# test_sample_status_reports_invalid_uuid # --------------------------------------------------------------------------- -def test_run_status_reports_invalid_uuid(monkeypatch, capsys): - rc = run_cmd.status_run_command(Namespace(run_id="not-a-valid-uuid")) +def test_sample_status_reports_invalid_uuid(monkeypatch, capsys): + rc = sample_cmd.status_sample_command(Namespace(sample_id="not-a-valid-uuid")) assert rc == 2 out = capsys.readouterr().out @@ -156,12 +156,12 @@ def test_run_status_reports_invalid_uuid(monkeypatch, capsys): # --------------------------------------------------------------------------- -# test_run_status_reports_missing_run +# test_sample_status_reports_missing_run # --------------------------------------------------------------------------- -def test_run_status_reports_missing_run(monkeypatch, capsys): - run_id = uuid4() +def test_sample_status_reports_missing_run(monkeypatch, capsys): + sample_id = uuid4() class FakeSession: def __enter__(self): @@ -173,43 +173,43 @@ def __exit__(self, *args): def get(self, model, pk): return None - monkeypatch.setattr(core_run_views, "get_session", lambda: FakeSession()) + monkeypatch.setattr(core_sample_views, "get_session", lambda: FakeSession()) - rc = run_cmd.status_run_command(Namespace(run_id=str(run_id))) + rc = sample_cmd.status_sample_command(Namespace(sample_id=str(sample_id))) assert rc == 3 out = capsys.readouterr().out - assert str(run_id) in out or "not found" in out.lower() or "No run" in out + assert str(sample_id) in out or "not found" in out.lower() or "No sample" in out # --------------------------------------------------------------------------- -# test_run_list_filters_by_definition +# test_sample_list_filters_by_definition # --------------------------------------------------------------------------- -def test_run_list_filters_by_definition(monkeypatch, session_factory, capsys): - """Only runs for the requested definition appear.""" +def test_sample_list_filters_by_definition(monkeypatch, session_factory, capsys): + """Only samples for the requested definition appear.""" definition_matching = _definition(name="matching") definition_other = _definition(name="other") - run_matching = _run_record(definition_id=definition_matching.id) - run_other = _run_record(definition_id=definition_other.id) + sample_matching = _sample_record(definition_id=definition_matching.id) + sample_other = _sample_record(definition_id=definition_other.id) # Capture IDs before the session closes to avoid DetachedInstanceError matching_definition_id = str(definition_matching.id) - matching_id = str(run_matching.id) - other_id = str(run_other.id) + matching_id = str(sample_matching.id) + other_id = str(sample_other.id) with session_factory() as session: session.add(definition_matching) session.add(definition_other) - session.add(run_matching) - session.add(run_other) + session.add(sample_matching) + session.add(sample_other) session.commit() - monkeypatch.setattr(core_run_views, "get_session", session_factory) + monkeypatch.setattr(core_sample_views, "get_session", session_factory) - rc = run_cmd.list_runs_command( + rc = sample_cmd.list_samples_command( Namespace(definition_id=matching_definition_id, experiment=None, status=None, limit=20) ) @@ -219,25 +219,25 @@ def test_run_list_filters_by_definition(monkeypatch, session_factory, capsys): assert other_id not in out -def test_run_list_filters_by_experiment_tag(monkeypatch, session_factory, capsys): - """The experiment filter reads the v2 ``RunRecord.experiment`` tag.""" +def test_sample_list_filters_by_experiment_tag(monkeypatch, session_factory, capsys): + """The experiment filter reads the v2 ``SampleRecord.experiment`` tag.""" definition = _definition(name="matching") - run_matching = _run_record(definition_id=definition.id, experiment="alpha") - run_other = _run_record(definition_id=definition.id, experiment="beta") + sample_matching = _sample_record(definition_id=definition.id, experiment="alpha") + sample_other = _sample_record(definition_id=definition.id, experiment="beta") - matching_id = str(run_matching.id) - other_id = str(run_other.id) + matching_id = str(sample_matching.id) + other_id = str(sample_other.id) with session_factory() as session: session.add(definition) - session.add(run_matching) - session.add(run_other) + session.add(sample_matching) + session.add(sample_other) session.commit() - monkeypatch.setattr(core_run_views, "get_session", session_factory) + monkeypatch.setattr(core_sample_views, "get_session", session_factory) - rc = run_cmd.list_runs_command( + rc = sample_cmd.list_samples_command( Namespace(definition_id=None, experiment="alpha", status=None, limit=20) ) diff --git a/ergon_cli/tests/unit/cli/test_shared_errors.py b/ergon_cli/tests/unit/cli/test_shared_errors.py index 0bf028621..c1b3c12b6 100644 --- a/ergon_cli/tests/unit/cli/test_shared_errors.py +++ b/ergon_cli/tests/unit/cli/test_shared_errors.py @@ -16,4 +16,4 @@ def test_parse_uuid_returns_uuid_or_usage_error() -> None: "00000000-0000-0000-0000-000000000000" ) with pytest.raises(CliUsageError): - parse_uuid("not-a-uuid", field_name="run_id") + parse_uuid("not-a-uuid", field_name="sample_id") diff --git a/ergon_cli/tests/unit/cli/test_workflow_cli.py b/ergon_cli/tests/unit/cli/test_workflow_cli.py index b6d5b74e8..8c4c48847 100644 --- a/ergon_cli/tests/unit/cli/test_workflow_cli.py +++ b/ergon_cli/tests/unit/cli/test_workflow_cli.py @@ -19,10 +19,12 @@ class _Service(BaseModel): resource: WorkflowResourceRef | None - def list_resources(self, session, *, run_id, task_id, scope, kind=None, max_depth=3, limit=50): + def list_resources( + self, session, *, sample_id, task_id, scope, kind=None, max_depth=3, limit=50 + ): assert isinstance(session, _Session) assert self.resource is not None - assert run_id == self.resource.run_id + assert sample_id == self.resource.sample_id assert task_id == self.resource.task_id assert scope == "visible" assert kind is None @@ -34,7 +36,7 @@ def list_resources(self, session, *, run_id, task_id, scope, kind=None, max_dept class _TaskTreeService(BaseModel): requested_parent_task_id: object | None = None - def list_tasks(self, session, *, run_id, parent_task_id=None): + def list_tasks(self, session, *, sample_id, parent_task_id=None): assert isinstance(session, _Session) self.requested_parent_task_id = parent_task_id return [ @@ -57,7 +59,7 @@ def list_resources(self, *args, **kwargs): def _context() -> WorkflowCommandContext: return WorkflowCommandContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), sandbox_task_key=uuid4(), @@ -66,11 +68,11 @@ def _context() -> WorkflowCommandContext: def test_resource_list_json_uses_injected_context() -> None: - run_id = uuid4() + sample_id = uuid4() task_id = uuid4() resource = WorkflowResourceRef( resource_id=uuid4(), - run_id=run_id, + sample_id=sample_id, task_execution_id=uuid4(), task_id=task_id, task_slug="research", @@ -86,7 +88,7 @@ def test_resource_list_json_uses_injected_context() -> None: output = execute_workflow_command( "inspect resource-list --scope visible --limit 5 --format json", context=WorkflowCommandContext( - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=uuid4(), sandbox_task_key=uuid4(), @@ -105,7 +107,7 @@ def test_resource_list_json_uses_injected_context() -> None: def test_agent_command_rejects_user_supplied_context_flags() -> None: output = execute_workflow_command( - f"inspect resource-list --scope visible --run-id {uuid4()}", + f"inspect resource-list --scope visible --sample-id {uuid4()}", context=_context(), session_factory=_Session, service=_Service(resource=None), # type: ignore[arg-type] diff --git a/ergon_core/ergon_core/api/criterion/context.py b/ergon_core/ergon_core/api/criterion/context.py index b8d37bf4f..ae4338b0d 100644 --- a/ergon_core/ergon_core/api/criterion/context.py +++ b/ergon_core/ergon_core/api/criterion/context.py @@ -13,7 +13,7 @@ class CriterionContext(BaseModel): model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - run_id: UUID + sample_id: UUID task_id: UUID execution_id: UUID task: Task diff --git a/ergon_core/ergon_core/api/worker/context.py b/ergon_core/ergon_core/api/worker/context.py index 3c63a5cbb..1744cd80b 100644 --- a/ergon_core/ergon_core/api/worker/context.py +++ b/ergon_core/ergon_core/api/worker/context.py @@ -9,25 +9,25 @@ from ergon_core.api.benchmark.task import Task from ergon_core.api.errors import ContainmentViolation from ergon_core.api.worker.results import SpawnedTaskHandle -from ergon_core.core.application.resources.models import RunResourceView +from ergon_core.core.application.resources.models import SampleResourceView from ergon_core.core.application.runtime.task_models import SubtaskInfo from ergon_core.core.persistence.shared.types import NodeId, RunId if TYPE_CHECKING: from sqlmodel import Session - from ergon_core.core.application.resources.service import RunResourceReadService + from ergon_core.core.application.resources.service import SampleResourceReadService from ergon_core.core.application.runtime.task_inspection import TaskInspectionService from ergon_core.core.application.runtime.task_management import TaskManagementService TaskManagementServiceAlias: TypeAlias = TaskManagementService TaskInspectionServiceAlias: TypeAlias = TaskInspectionService - RunResourceReadServiceAlias: TypeAlias = RunResourceReadService + SampleResourceReadServiceAlias: TypeAlias = SampleResourceReadService SessionFactory: TypeAlias = Callable[[], ContextManager[Session]] else: TaskManagementServiceAlias: TypeAlias = Any TaskInspectionServiceAlias: TypeAlias = Any - RunResourceReadServiceAlias: TypeAlias = Any + SampleResourceReadServiceAlias: TypeAlias = Any SessionFactory: TypeAlias = Callable[[], ContextManager[Any]] @@ -45,8 +45,8 @@ def _require_injected_dependency(value: object | None) -> object: TaskInspectionServiceAlias, AfterValidator(_require_injected_dependency), ] -RunResourceReadServiceDependency: TypeAlias = Annotated[ - RunResourceReadServiceAlias, +SampleResourceReadServiceDependency: TypeAlias = Annotated[ + SampleResourceReadServiceAlias, AfterValidator(_require_injected_dependency), ] SessionFactoryDependency: TypeAlias = Annotated[ @@ -67,9 +67,9 @@ class WorkerContext(BaseModel): model_config = {"arbitrary_types_allowed": True} - run_id: UUID + sample_id: UUID task_id: UUID = Field( - description="RunGraphNode.task_id — canonical runtime task identity.", + description="SampleGraphNode.task_id — canonical runtime task identity.", ) definition_id: UUID | None = Field( default=None, @@ -96,7 +96,7 @@ class WorkerContext(BaseModel): exclude=True, repr=False, ) - resource_service: RunResourceReadServiceDependency = Field( + resource_service: SampleResourceReadServiceDependency = Field( exclude=True, repr=False, ) @@ -106,14 +106,14 @@ class WorkerContext(BaseModel): def _for_job( cls, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, execution_id: UUID, definition_id: UUID | None, sandbox_id: str, task_mgmt: TaskManagementServiceAlias, task_inspect: TaskInspectionServiceAlias, - resource_service: RunResourceReadServiceAlias, + resource_service: SampleResourceReadServiceAlias, session_factory: SessionFactory, ) -> "WorkerContext": """Construct the job runtime ``WorkerContext``. @@ -124,7 +124,7 @@ def _for_job( """ return cls( - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, definition_id=definition_id, @@ -146,7 +146,7 @@ async def spawn_task( """Spawn a child task under this context's task_id.""" return await self.task_mgmt.spawn_dynamic_task( - run_id=self.run_id, + sample_id=self.sample_id, parent_task_id=self.task_id, task=task, depends_on=depends_on, @@ -168,7 +168,7 @@ async def cancel_task(self, task_id: UUID, *, reason: str | None = None) -> None with self.session_factory() as session: await self.task_mgmt.cancel_task( session, - CancelTaskCommand(run_id=RunId(self.run_id), task_id=NodeId(task_id)), + CancelTaskCommand(sample_id=RunId(self.sample_id), task_id=NodeId(task_id)), ) async def refine_task(self, task_id: UUID, *, description: str) -> None: @@ -182,7 +182,7 @@ async def refine_task(self, task_id: UUID, *, description: str) -> None: await self.task_mgmt.refine_task( session, RefineTaskCommand( - run_id=RunId(self.run_id), + sample_id=RunId(self.sample_id), task_id=NodeId(task_id), new_description=description, ), @@ -198,7 +198,7 @@ async def restart_task(self, task_id: UUID) -> SpawnedTaskHandle: with self.session_factory() as session: result = await self.task_mgmt.restart_task( session, - RestartTaskCommand(run_id=RunId(self.run_id), task_id=NodeId(task_id)), + RestartTaskCommand(sample_id=RunId(self.sample_id), task_id=NodeId(task_id)), ) return SpawnedTaskHandle(task_id=result.task_id) @@ -208,7 +208,7 @@ async def subtasks(self) -> tuple[SubtaskInfo, ...]: with self.session_factory() as session: rows = self.task_inspect.list_subtasks( session, - run_id=self.run_id, + sample_id=self.sample_id, parent_task_id=self.task_id, ) return tuple(rows) @@ -217,14 +217,14 @@ async def descendants(self) -> tuple[SubtaskInfo, ...]: """Return the transitive descendants of this context's task_id.""" descendant_ids = await self.task_inspect.descendant_ids( - run_id=self.run_id, + sample_id=self.sample_id, root_task_id=self.task_id, ) with self.session_factory() as session: return tuple( self.task_inspect.get_subtask( session, - run_id=self.run_id, + sample_id=self.sample_id, task_id=task_id, ) for task_id in descendant_ids @@ -237,7 +237,7 @@ async def get_task(self, task_id: UUID) -> SubtaskInfo: with self.session_factory() as session: return self.task_inspect.get_subtask( session, - run_id=self.run_id, + sample_id=self.sample_id, task_id=task_id, ) @@ -248,7 +248,7 @@ async def resources( execution_id: UUID | None = None, kind: str | None = None, name: str | None = None, - ) -> tuple[RunResourceView, ...]: + ) -> tuple[SampleResourceView, ...]: """List resources visible to this worker within the current run. Resource access is run-scoped by design: workers may inspect and @@ -257,7 +257,7 @@ async def resources( """ return self.resource_service.list_for_run( - run_id=self.run_id, + sample_id=self.sample_id, task_id=task_id, task_execution_id=execution_id, kind=kind, @@ -268,7 +268,7 @@ async def read_resource(self, resource_id: UUID) -> bytes: """Read a visible resource blob from this run.""" return self.resource_service.read_bytes( - run_id=self.run_id, + sample_id=self.sample_id, current_task_id=self.task_id, resource_id=resource_id, ) @@ -279,7 +279,7 @@ async def _assert_descendant(self, task_id: UUID) -> None: if task_id == self.task_id: return descendant_ids = await self.task_inspect.descendant_ids( - run_id=self.run_id, + sample_id=self.sample_id, root_task_id=self.task_id, ) if task_id not in descendant_ids: diff --git a/ergon_core/ergon_core/core/application/communication/models.py b/ergon_core/ergon_core/core/application/communication/models.py index a4e82aaf9..c89032458 100644 --- a/ergon_core/ergon_core/core/application/communication/models.py +++ b/ergon_core/ergon_core/core/application/communication/models.py @@ -7,12 +7,12 @@ class CreateMessageRequest(BaseModel): - run_id: UUID + sample_id: UUID from_agent_id: str = Field( - description="ID of the sending agent, e.g. '{run_id}:worker'", + description="ID of the sending agent, e.g. '{sample_id}:worker'", ) to_agent_id: str = Field( - description="ID of the receiving agent, e.g. '{run_id}:stakeholder'", + description="ID of the receiving agent, e.g. '{sample_id}:stakeholder'", ) thread_topic: str thread_summary: str | None = Field( @@ -26,7 +26,7 @@ class CreateMessageRequest(BaseModel): class MessageResponse(BaseModel): message_id: UUID thread_id: UUID - run_id: UUID + sample_id: UUID thread_topic: str from_agent_id: str to_agent_id: str @@ -38,7 +38,7 @@ class MessageResponse(BaseModel): class ThreadSummary(BaseModel): thread_id: UUID - run_id: UUID + sample_id: UUID topic: str summary: str | None = None agent_a_id: str @@ -50,7 +50,7 @@ class ThreadSummary(BaseModel): class ThreadWithMessages(BaseModel): thread_id: UUID - run_id: UUID + sample_id: UUID topic: str summary: str | None = None agent_a_id: str diff --git a/ergon_core/ergon_core/core/application/communication/service.py b/ergon_core/ergon_core/core/application/communication/service.py index 7a86d5092..96e1ea45d 100644 --- a/ergon_core/ergon_core/core/application/communication/service.py +++ b/ergon_core/ergon_core/core/application/communication/service.py @@ -14,7 +14,7 @@ ) from ergon_core.core.shared.utils import utcnow from ergon_core.core.views.dashboard_events.contracts import DashboardThreadMessageCreatedEvent -from ergon_core.core.views.runs.models import ( +from ergon_core.core.views.samples.models import ( RunCommunicationMessageDto, RunCommunicationThreadDto, ) @@ -32,7 +32,7 @@ async def save_message(self, request: CreateMessageRequest) -> MessageResponse: with get_session() as session: thread = self._get_or_create_thread( session, - run_id=request.run_id, + sample_id=request.sample_id, agent_a_id=request.from_agent_id, agent_b_id=request.to_agent_id, topic=request.thread_topic, @@ -50,7 +50,7 @@ async def save_message(self, request: CreateMessageRequest) -> MessageResponse: message = ThreadMessage( thread_id=thread.id, - run_id=request.run_id, + sample_id=request.sample_id, task_execution_id=request.task_execution_id, from_agent_id=request.from_agent_id, to_agent_id=request.to_agent_id, @@ -68,7 +68,7 @@ async def save_message(self, request: CreateMessageRequest) -> MessageResponse: response = MessageResponse( message_id=message.id, thread_id=thread.id, - run_id=message.run_id, + sample_id=message.sample_id, thread_topic=thread.topic, from_agent_id=message.from_agent_id, to_agent_id=message.to_agent_id, @@ -80,7 +80,7 @@ async def save_message(self, request: CreateMessageRequest) -> MessageResponse: thread_dto = RunCommunicationThreadDto( id=str(thread.id), - run_id=str(thread.run_id), + sample_id=str(thread.sample_id), topic=thread.topic, summary=thread.summary, agent_a_id=thread.agent_a_id, @@ -93,7 +93,7 @@ async def save_message(self, request: CreateMessageRequest) -> MessageResponse: id=str(message.id), thread_id=str(message.thread_id), thread_topic=thread.topic, - run_id=str(message.run_id), + sample_id=str(message.sample_id), from_agent_id=message.from_agent_id, to_agent_id=message.to_agent_id, content=message.content, @@ -104,7 +104,7 @@ async def save_message(self, request: CreateMessageRequest) -> MessageResponse: try: await get_dashboard_event_publisher().publish( DashboardThreadMessageCreatedEvent( - run_id=request.run_id, + sample_id=request.sample_id, thread=thread_dto, message=message_dto, ) @@ -132,7 +132,7 @@ def get_thread_messages(self, thread_id: UUID) -> list[MessageResponse]: MessageResponse( message_id=m.id, thread_id=m.thread_id, - run_id=m.run_id, + sample_id=m.sample_id, thread_topic=thread.topic, from_agent_id=m.from_agent_id, to_agent_id=m.to_agent_id, @@ -144,10 +144,10 @@ def get_thread_messages(self, thread_id: UUID) -> list[MessageResponse]: for m in messages ] - def get_all_threads_for_run(self, run_id: UUID) -> list[ThreadSummary]: + def get_all_threads_for_run(self, sample_id: UUID) -> list[ThreadSummary]: """Return summaries for every thread belonging to a run.""" with get_session() as session: - threads = list(session.exec(select(Thread).where(Thread.run_id == run_id)).all()) + threads = list(session.exec(select(Thread).where(Thread.sample_id == sample_id)).all()) summaries: list[ThreadSummary] = [] for thread in threads: @@ -157,7 +157,7 @@ def get_all_threads_for_run(self, run_id: UUID) -> list[ThreadSummary]: summaries.append( ThreadSummary( thread_id=thread.id, - run_id=thread.run_id, + sample_id=thread.sample_id, topic=thread.topic, summary=thread.summary, agent_a_id=thread.agent_a_id, @@ -179,7 +179,7 @@ def get_thread_with_messages(self, thread_id: UUID) -> ThreadWithMessages | None messages = self.get_thread_messages(thread_id) return ThreadWithMessages( thread_id=thread.id, - run_id=thread.run_id, + sample_id=thread.sample_id, topic=thread.topic, summary=thread.summary, agent_a_id=thread.agent_a_id, @@ -197,17 +197,17 @@ def get_thread_with_messages(self, thread_id: UUID) -> ThreadWithMessages | None def _get_or_create_thread( session: Session, *, - run_id: UUID, + sample_id: UUID, agent_a_id: str, agent_b_id: str, topic: str, thread_summary: str | None = None, ) -> Thread: - # Threads are keyed by (run_id, topic) only — all senders on the same + # Threads are keyed by (sample_id, topic) only — all senders on the same # topic share one thread per run (broadcast/group semantics). # The unique constraint uq_threads_run_topic enforces this at the DB level # and lets us safely retry on concurrent INSERT races. - stmt = select(Thread).where(Thread.run_id == run_id).where(Thread.topic == topic) + stmt = select(Thread).where(Thread.sample_id == sample_id).where(Thread.topic == topic) existing = session.exec(stmt).first() if existing is not None: if existing.summary is None and thread_summary: @@ -217,7 +217,7 @@ def _get_or_create_thread( a, b = sorted([agent_a_id, agent_b_id]) thread = Thread( - run_id=run_id, + sample_id=sample_id, topic=topic, summary=thread_summary, agent_a_id=a, diff --git a/ergon_core/ergon_core/core/application/context/service.py b/ergon_core/ergon_core/core/application/context/service.py index db5ca8b53..ad5995ffb 100644 --- a/ergon_core/ergon_core/core/application/context/service.py +++ b/ergon_core/ergon_core/core/application/context/service.py @@ -19,21 +19,21 @@ ToolResultPart, UserMessagePart, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from sqlmodel import Session, select logger = logging.getLogger(__name__) class ContextEventService: - """Append-only write and read path for ``run_context_events``.""" + """Append-only write and read path for ``sample_context_events``.""" def __init__(self) -> None: - self._listeners: list[Callable[[RunContextEvent], Awaitable[None]]] = [] + self._listeners: list[Callable[[SampleContextEvent], Awaitable[None]]] = [] self._sequence_counters: dict[UUID, int] = {} self._active_turn_ids: dict[UUID, str] = {} - def add_listener(self, listener: Callable[[RunContextEvent], Awaitable[None]]) -> None: + def add_listener(self, listener: Callable[[SampleContextEvent], Awaitable[None]]) -> None: self._listeners.append(listener) def _next_sequence(self, execution_id: UUID) -> int: @@ -41,7 +41,7 @@ def _next_sequence(self, execution_id: UUID) -> int: def _make_event( self, - run_id: UUID, + sample_id: UUID, execution_id: UUID, worker_binding_key: str, sequence: int, @@ -50,9 +50,9 @@ def _make_event( started_at: datetime | None = None, completed_at: datetime | None = None, policy_version: str | None = None, - ) -> RunContextEvent: - return RunContextEvent( - run_id=run_id, + ) -> SampleContextEvent: + return SampleContextEvent( + sample_id=sample_id, task_execution_id=execution_id, worker_binding_key=worker_binding_key, sequence=sequence, @@ -80,14 +80,14 @@ async def persist_chunk( self, session: Session, *, - run_id: UUID, + sample_id: UUID, execution_id: UUID, worker_binding_key: str, chunk: ContextPartChunk, started_at: datetime | None = None, completed_at: datetime | None = None, policy_version: str | None = None, - ) -> RunContextEvent: + ) -> SampleContextEvent: """Enrich and persist one worker-emitted context stream chunk.""" seq = self._next_sequence(execution_id) now = datetime.now(UTC) @@ -106,7 +106,7 @@ async def persist_chunk( policy_version=policy_version, ) event = self._make_event( - run_id, + sample_id, execution_id, worker_binding_key, seq, @@ -129,18 +129,18 @@ async def persist_chunk( return event - def get_for_execution(self, session: Session, execution_id: UUID) -> list[RunContextEvent]: + def get_for_execution(self, session: Session, execution_id: UUID) -> list[SampleContextEvent]: stmt = ( - select(RunContextEvent) - .where(RunContextEvent.task_execution_id == execution_id) - .order_by(RunContextEvent.sequence) + select(SampleContextEvent) + .where(SampleContextEvent.task_execution_id == execution_id) + .order_by(SampleContextEvent.sequence) ) return list(session.exec(stmt).all()) - def get_for_run(self, session: Session, run_id: UUID) -> list[RunContextEvent]: + def get_for_run(self, session: Session, sample_id: UUID) -> list[SampleContextEvent]: stmt = ( - select(RunContextEvent) - .where(RunContextEvent.run_id == run_id) - .order_by(RunContextEvent.task_execution_id, RunContextEvent.sequence) + select(SampleContextEvent) + .where(SampleContextEvent.sample_id == sample_id) + .order_by(SampleContextEvent.task_execution_id, SampleContextEvent.sequence) ) return list(session.exec(stmt).all()) diff --git a/ergon_core/ergon_core/core/application/evaluation/service.py b/ergon_core/ergon_core/core/application/evaluation/service.py index f16a13ad8..51f2312a3 100644 --- a/ergon_core/ergon_core/core/application/evaluation/service.py +++ b/ergon_core/ergon_core/core/application/evaluation/service.py @@ -19,8 +19,8 @@ from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.shared.ids import new_id from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunTaskEvaluation, + SampleRecord, + SampleTaskEvaluation, ) from pydantic import BaseModel from sqlmodel import Session, select @@ -42,7 +42,7 @@ class PersistedEvaluation(BaseModel): summary: EvaluationSummary evaluation_id: UUID - run_id: UUID + sample_id: UUID task_id: UUID total_score: float created_at: datetime @@ -103,7 +103,7 @@ async def evaluate( async def persist_success( self, *, - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, task_id: UUID, binding_key: str, @@ -114,10 +114,10 @@ async def persist_success( result = service_result.result session = get_session() try: - evaluator_id = self.lookup_evaluator_id(session, run_id, binding_key) + evaluator_id = self.lookup_evaluator_id(session, sample_id, binding_key) evaluation = await _create_task_evaluation( session, - run_id=run_id, + sample_id=sample_id, task_execution_id=task_execution_id, task_id=task_id, definition_evaluator_id=evaluator_id, @@ -126,13 +126,13 @@ async def persist_success( feedback=result.feedback, summary_json=summary.model_dump(mode="json"), ) - self._refresh_run_evaluation_summary(session, run_id) + self._refresh_run_evaluation_summary(session, sample_id) session.commit() session.refresh(evaluation) return PersistedEvaluation( summary=summary, evaluation_id=evaluation.id, - run_id=evaluation.run_id, + sample_id=evaluation.sample_id, task_id=evaluation.task_id, total_score=0.0 if evaluation.score is None else evaluation.score, created_at=evaluation.created_at, @@ -143,7 +143,7 @@ async def persist_success( async def persist_failure( self, *, - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, task_id: UUID, binding_key: str, @@ -160,10 +160,10 @@ async def persist_failure( ) session = get_session() try: - evaluator_id = self.lookup_evaluator_id(session, run_id, binding_key) + evaluator_id = self.lookup_evaluator_id(session, sample_id, binding_key) await _create_task_evaluation( session, - run_id=run_id, + sample_id=sample_id, task_execution_id=task_execution_id, task_id=task_id, definition_evaluator_id=evaluator_id, @@ -172,7 +172,7 @@ async def persist_failure( feedback=f"{error_type}: {exc}", summary_json=summary.model_dump(mode="json"), ) - self._refresh_run_evaluation_summary(session, run_id) + self._refresh_run_evaluation_summary(session, sample_id) session.commit() finally: session.close() @@ -180,7 +180,7 @@ async def persist_failure( def lookup_evaluator_id( self, session: Session, - run_id: UUID, + sample_id: UUID, binding_key: str, *, evaluator_type: str | None = None, @@ -192,17 +192,17 @@ def lookup_evaluator_id( inline ``task.evaluators[i]`` object and passes only its ``evaluator.name``. The persistence layer needs the normalized evaluator id for the FK on - ``run_task_evaluations.definition_evaluator_id``. + ``sample_task_evaluations.definition_evaluator_id``. The normalized evaluator row remains the persistence/read-model target for evaluation summaries, even though runtime dispatch executes the inline evaluator from ``task.evaluators``. """ - run = session.get(RunRecord, run_id) + run = session.get(SampleRecord, sample_id) if run is None: raise ContractViolationError( - f"RunRecord {run_id} not found while resolving evaluator id" + f"SampleRecord {sample_id} not found while resolving evaluator id" ) evaluator_def = session.exec( select(ExperimentDefinitionEvaluator).where( @@ -222,11 +222,11 @@ def lookup_evaluator_id( session.flush() return evaluator_def.id - def _refresh_run_evaluation_summary(self, session: Session, run_id: UUID) -> None: - run = session.get(RunRecord, run_id) + def _refresh_run_evaluation_summary(self, session: Session, sample_id: UUID) -> None: + run = session.get(SampleRecord, sample_id) if run is None: return - evaluations = _list_task_evaluations(session, run_id) + evaluations = _list_task_evaluations(session, sample_id) score_summary = self.summarize_scores(evaluations) existing_summary = dict({} if run.summary_json is None else run.summary_json) existing_summary.update( @@ -241,15 +241,15 @@ def _refresh_run_evaluation_summary(self, session: Session, run_id: UUID) -> Non session.flush() -def _list_task_evaluations(session: Session, run_id: UUID) -> list[RunTaskEvaluation]: - stmt = select(RunTaskEvaluation).where(RunTaskEvaluation.run_id == run_id) +def _list_task_evaluations(session: Session, sample_id: UUID) -> list[SampleTaskEvaluation]: + stmt = select(SampleTaskEvaluation).where(SampleTaskEvaluation.sample_id == sample_id) return list(session.exec(stmt).all()) async def _create_task_evaluation( session: Session, *, - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, task_id: UUID, definition_evaluator_id: UUID, @@ -257,10 +257,10 @@ async def _create_task_evaluation( passed: bool | None = None, feedback: str | None = None, summary_json: dict | None = None, -) -> RunTaskEvaluation: - evaluation = RunTaskEvaluation( +) -> SampleTaskEvaluation: + evaluation = SampleTaskEvaluation( id=new_id(), - run_id=run_id, + sample_id=sample_id, task_execution_id=task_execution_id, task_id=task_id, definition_evaluator_id=definition_evaluator_id, diff --git a/ergon_core/ergon_core/core/application/evaluation/summary.py b/ergon_core/ergon_core/core/application/evaluation/summary.py index 30b2fb5a1..c66f86e52 100644 --- a/ergon_core/ergon_core/core/application/evaluation/summary.py +++ b/ergon_core/ergon_core/core/application/evaluation/summary.py @@ -1,4 +1,4 @@ -"""Strongly-typed model for RunTaskEvaluation.summary_json. +"""Strongly-typed model for SampleTaskEvaluation.summary_json. This is the canonical schema for evaluation summary persistence. Both the write side (evaluate_task_run.py) and read side (runs.py) @@ -52,7 +52,7 @@ def _populate_criterion_slug(cls, data: Any) -> Any: # slopcop: ignore[no-typin class EvaluationSummary(BaseModel): - """Typed schema for RunTaskEvaluation.summary_json.""" + """Typed schema for SampleTaskEvaluation.summary_json.""" evaluator_name: str max_score: float = 1.0 diff --git a/ergon_core/ergon_core/core/application/events/__init__.py b/ergon_core/ergon_core/core/application/events/__init__.py index abe5ac3cb..ced792089 100644 --- a/ergon_core/ergon_core/core/application/events/__init__.py +++ b/ergon_core/ergon_core/core/application/events/__init__.py @@ -4,8 +4,8 @@ from ergon_core.core.application.events.runtime import ( CancelCause, PropagationCancelCause, - RunCancelledEvent, - RunCleanupEvent, + SampleCancelledEvent, + SampleCleanupEvent, TaskCancelledEvent, TaskCompletedEvent, TaskFailedEvent, @@ -20,8 +20,8 @@ "CancelCause", "InngestEventContract", "PropagationCancelCause", - "RunCancelledEvent", - "RunCleanupEvent", + "SampleCancelledEvent", + "SampleCleanupEvent", "TaskCancelledEvent", "TaskCompletedEvent", "TaskFailedEvent", diff --git a/ergon_core/ergon_core/core/application/events/runtime.py b/ergon_core/ergon_core/core/application/events/runtime.py index 0adffc840..f39d4185b 100644 --- a/ergon_core/ergon_core/core/application/events/runtime.py +++ b/ergon_core/ergon_core/core/application/events/runtime.py @@ -13,16 +13,16 @@ from ergon_core.core.application.events.base import InngestEventContract -class RunCancelledEvent(InngestEventContract): - name: ClassVar[str] = "run/cancelled" +class SampleCancelledEvent(InngestEventContract): + name: ClassVar[str] = "sample/cancelled" - run_id: UUID + sample_id: UUID -class RunCleanupEvent(InngestEventContract): - name: ClassVar[str] = "run/cleanup" +class SampleCleanupEvent(InngestEventContract): + name: ClassVar[str] = "sample/cleanup" - run_id: UUID + sample_id: UUID status: str error_message: str | None = None @@ -30,7 +30,7 @@ class RunCleanupEvent(InngestEventContract): class TaskReadyEvent(InngestEventContract): name: ClassVar[str] = "task/ready" - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID @@ -38,7 +38,7 @@ class TaskReadyEvent(InngestEventContract): class TaskStartedEvent(InngestEventContract): name: ClassVar[str] = "task/started" - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID execution_id: UUID @@ -57,7 +57,7 @@ class TaskStartedEvent(InngestEventContract): class TaskCancelledEvent(InngestEventContract): name: ClassVar[str] = "task/cancelled" - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID execution_id: UUID | None @@ -69,7 +69,7 @@ class TaskCancelledEvent(InngestEventContract): class TaskCompletedEvent(InngestEventContract): name: ClassVar[str] = "task/completed" - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID execution_id: UUID @@ -79,7 +79,7 @@ class TaskCompletedEvent(InngestEventContract): class TaskFailedEvent(InngestEventContract): name: ClassVar[str] = "task/failed" - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID execution_id: UUID @@ -90,20 +90,20 @@ class TaskFailedEvent(InngestEventContract): class WorkflowStartedEvent(InngestEventContract): name: ClassVar[str] = "workflow/started" - run_id: UUID + sample_id: UUID definition_id: UUID class WorkflowCompletedEvent(InngestEventContract): name: ClassVar[str] = "workflow/completed" - run_id: UUID + sample_id: UUID definition_id: UUID class WorkflowFailedEvent(InngestEventContract): name: ClassVar[str] = "workflow/failed" - run_id: UUID + sample_id: UUID definition_id: UUID error: str diff --git a/ergon_core/ergon_core/core/application/experiments/launch.py b/ergon_core/ergon_core/core/application/experiments/launch.py index 871e11b42..f8fba6c60 100644 --- a/ergon_core/ergon_core/core/application/experiments/launch.py +++ b/ergon_core/ergon_core/core/application/experiments/launch.py @@ -7,7 +7,9 @@ from ergon_core.core.application.events import WorkflowStartedEvent from ergon_core.core.application.experiments.errors import DefinitionNotFoundError from ergon_core.core.application.experiments.models import DefinitionHandle, ExperimentRunResult -from ergon_core.core.application.runtime.run_lifecycle import create_run +from ergon_core.core.application.runtime.sample_lifecycle import ( + create_run as create_definition_backed_sample, +) from ergon_core.core.infrastructure.inngest.client import inngest_client from ergon_core.core.persistence.definitions.models import ExperimentDefinition from ergon_core.core.persistence.shared.db import get_session @@ -16,13 +18,13 @@ WorkflowStartedEmitter = Callable[[UUID, UUID], Awaitable[None]] -async def launch_run( +async def launch_sample( definition_id: UUID, *, assignment_metadata: Mapping[str, JsonValue] | None = None, emit_workflow_started: WorkflowStartedEmitter | None = None, ) -> ExperimentRunResult: - """Materialize a run directly from an ExperimentDefinition row.""" + """Materialize a sample directly from an ExperimentDefinition row.""" emitter = emit_workflow_started or _emit_workflow_started with get_session() as session: @@ -31,7 +33,7 @@ async def launch_run( raise DefinitionNotFoundError(definition_id) metadata = definition.parsed_metadata() experiment = metadata.get("experiment") - run = create_run( + sample = create_definition_backed_sample( DefinitionHandle( definition_id=definition.id, benchmark_type=definition.benchmark_type, @@ -47,16 +49,19 @@ async def launch_run( seed=None, experiment=experiment if isinstance(experiment, str) else None, ) - await emitter(run.id, definition_id) + await emitter(sample.id, definition_id) return ExperimentRunResult( definition_id=definition_id, - run_ids=[run.id], + sample_ids=[sample.id], definition_ids=[definition_id], ) -async def _emit_workflow_started(run_id: UUID, definition_id: UUID) -> None: - event = WorkflowStartedEvent(run_id=run_id, definition_id=definition_id) +launch_run = launch_sample + + +async def _emit_workflow_started(sample_id: UUID, definition_id: UUID) -> None: + event = WorkflowStartedEvent(sample_id=sample_id, definition_id=definition_id) await inngest_client.send( inngest.Event( name=WorkflowStartedEvent.name, diff --git a/ergon_core/ergon_core/core/application/experiments/models.py b/ergon_core/ergon_core/core/application/experiments/models.py index 2dae77a66..7f75781d4 100644 --- a/ergon_core/ergon_core/core/application/experiments/models.py +++ b/ergon_core/ergon_core/core/application/experiments/models.py @@ -33,7 +33,7 @@ class ExperimentRunRequest(BaseModel): class ExperimentRunResult(BaseModel): definition_id: UUID - run_ids: list[UUID] + sample_ids: list[UUID] definition_ids: list[UUID] = Field(default_factory=list) diff --git a/ergon_core/ergon_core/core/application/experiments/service.py b/ergon_core/ergon_core/core/application/experiments/service.py index 90094206b..fbef760c5 100644 --- a/ergon_core/ergon_core/core/application/experiments/service.py +++ b/ergon_core/ergon_core/core/application/experiments/service.py @@ -26,7 +26,7 @@ def persist_benchmark(benchmark: "Benchmark") -> DefinitionHandle: return _persist_benchmark(benchmark) -async def launch_run( +async def launch_sample( definition_id: UUID, *, emit_workflow_started: WorkflowStartedEmitter | None = None, @@ -34,9 +34,9 @@ async def launch_run( """Launch a persisted definition while keeping the heavy runtime import lazy.""" # reason: keep HTTP app imports from cycling through runtime models and public API exports. - from ergon_core.core.application.experiments.launch import launch_run as _launch_run + from ergon_core.core.application.experiments.launch import launch_sample as _launch_sample - return await _launch_run( + return await _launch_sample( definition_id, emit_workflow_started=emit_workflow_started, ) @@ -47,9 +47,12 @@ async def run_experiment( *, emit_workflow_started: WorkflowStartedEmitter | None = None, ) -> ExperimentRunResult: - """Materialize one run directly from an ExperimentDefinition row.""" + """Materialize one sample directly from an ExperimentDefinition row.""" - return await launch_run( + return await launch_sample( request.definition_id, emit_workflow_started=emit_workflow_started, ) + + +launch_run = launch_sample diff --git a/ergon_core/ergon_core/core/application/resources/__init__.py b/ergon_core/ergon_core/core/application/resources/__init__.py index b67ff24fb..06784aa21 100644 --- a/ergon_core/ergon_core/core/application/resources/__init__.py +++ b/ergon_core/ergon_core/core/application/resources/__init__.py @@ -1,5 +1,5 @@ -from ergon_core.core.application.resources.errors import RunResourceNotFoundError -from ergon_core.core.application.resources.models import RunResourceView -from ergon_core.core.application.resources.service import RunResourceReadService +from ergon_core.core.application.resources.errors import SampleResourceNotFoundError +from ergon_core.core.application.resources.models import SampleResourceView +from ergon_core.core.application.resources.service import SampleResourceReadService -__all__ = ["RunResourceNotFoundError", "RunResourceReadService", "RunResourceView"] +__all__ = ["SampleResourceNotFoundError", "SampleResourceReadService", "SampleResourceView"] diff --git a/ergon_core/ergon_core/core/application/resources/errors.py b/ergon_core/ergon_core/core/application/resources/errors.py index da40ee675..59a0f5247 100644 --- a/ergon_core/ergon_core/core/application/resources/errors.py +++ b/ergon_core/ergon_core/core/application/resources/errors.py @@ -3,9 +3,9 @@ from uuid import UUID -class RunResourceNotFoundError(LookupError): +class SampleResourceNotFoundError(LookupError): """Raised when a run resource row cannot be found.""" def __init__(self, resource_id: UUID) -> None: - super().__init__(f"RunResource not found: {resource_id}") + super().__init__(f"SampleResource not found: {resource_id}") self.resource_id = resource_id diff --git a/ergon_core/ergon_core/core/application/resources/models.py b/ergon_core/ergon_core/core/application/resources/models.py index 51af3239c..9d744a32b 100644 --- a/ergon_core/ergon_core/core/application/resources/models.py +++ b/ergon_core/ergon_core/core/application/resources/models.py @@ -4,31 +4,31 @@ from typing import TYPE_CHECKING from uuid import UUID -from ergon_core.core.persistence.shared.enums import RunResourceKind +from ergon_core.core.persistence.shared.enums import SampleResourceKind from ergon_core.core.shared.json_types import JsonObject from pydantic import BaseModel, ConfigDict, Field if TYPE_CHECKING: - from ergon_core.core.persistence.telemetry.models import RunResource as _RunResourceRow + from ergon_core.core.persistence.telemetry.models import SampleResource as _SampleResourceRow -class RunResourceView(BaseModel): - """Read-only DTO for a ``run_resources`` row. +class SampleResourceView(BaseModel): + """Read-only DTO for a ``sample_resources`` row. - Construct via ``RunResourceView.from_row(orm_row)``. + Construct via ``SampleResourceView.from_row(orm_row)``. """ model_config = ConfigDict(frozen=True) - id: UUID = Field(description="Primary key of the run_resources row.") - run_id: UUID = Field(description="The run this resource was produced in.") + id: UUID = Field(description="Primary key of the sample_resources row.") + sample_id: UUID = Field(description="The run this resource was produced in.") task_execution_id: UUID | None = Field( description=( "The task execution that produced the resource, or ``None`` for " "run-scoped resources (e.g. aggregate reports)." ), ) - kind: RunResourceKind = Field( + kind: SampleResourceKind = Field( description="Canonical category (report, worker_output, trace, etc.).", ) name: str = Field( @@ -62,13 +62,13 @@ class RunResourceView(BaseModel): ) @classmethod - def from_row(cls, row: "_RunResourceRow") -> "RunResourceView": - """Map an ORM ``RunResource`` row to a frozen DTO.""" + def from_row(cls, row: "_SampleResourceRow") -> "SampleResourceView": + """Map an ORM ``SampleResource`` row to a frozen DTO.""" return cls( id=row.id, - run_id=row.run_id, + sample_id=row.sample_id, task_execution_id=row.task_execution_id, - kind=RunResourceKind(row.kind), + kind=SampleResourceKind(row.kind), name=row.name, mime_type=row.mime_type, file_path=row.file_path, diff --git a/ergon_core/ergon_core/core/application/resources/publishing.py b/ergon_core/ergon_core/core/application/resources/publishing.py index a7f2cce76..43008a15d 100644 --- a/ergon_core/ergon_core/core/application/resources/publishing.py +++ b/ergon_core/ergon_core/core/application/resources/publishing.py @@ -10,24 +10,24 @@ from sqlmodel import Session from ergon_core.core.application.ports import ResourceBlobWriter, SandboxFileReader -from ergon_core.core.application.resources.models import RunResourceView -from ergon_core.core.application.resources.repository import RunResourceRepository +from ergon_core.core.application.resources.models import SampleResourceView +from ergon_core.core.application.resources.repository import SampleResourceRepository from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunResourceKind +from ergon_core.core.persistence.shared.enums import SampleResourceKind SessionFactory: TypeAlias = Callable[[], AbstractContextManager[Session]] -class RunResourcePublishService: +class SampleResourcePublishService: """Owns resource append/dedup semantics for sandbox outputs.""" def __init__( self, *, - repository: RunResourceRepository | None = None, + repository: SampleResourceRepository | None = None, session_factory: SessionFactory = get_session, ) -> None: - self._resource_repo = repository or RunResourceRepository() + self._resource_repo = repository or SampleResourceRepository() self._session_factory = session_factory async def publish_sandbox_files( @@ -35,12 +35,12 @@ async def publish_sandbox_files( *, reader: SandboxFileReader, blob_store: ResourceBlobWriter, - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, - publish_dirs: tuple[tuple[str, RunResourceKind], ...], - ) -> list[RunResourceView]: + publish_dirs: tuple[tuple[str, SampleResourceKind], ...], + ) -> list[SampleResourceView]: """Publish changed files from configured sandbox dirs as run resources.""" - created: list[RunResourceView] = [] + created: list[SampleResourceView] = [] for sandbox_dir, resource_kind in publish_dirs: entries = await reader.list_sandbox_dir(sandbox_dir) for entry in entries: @@ -66,7 +66,7 @@ async def publish_sandbox_files( with self._session_factory() as session: row = self._resource_repo.append( session, - run_id=run_id, + sample_id=sample_id, task_execution_id=task_execution_id, kind=resource_kind.value, name=entry_name, @@ -79,7 +79,7 @@ async def publish_sandbox_files( ) session.commit() session.refresh(row) - created.append(RunResourceView.from_row(row)) + created.append(SampleResourceView.from_row(row)) return created @@ -87,13 +87,13 @@ def publish_value( self, *, blob_store: ResourceBlobWriter, - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, - kind: RunResourceKind, + kind: SampleResourceKind, name: str, content: str, mime_type: str = "text/plain", - ) -> RunResourceView | None: + ) -> SampleResourceView | None: """Publish an explicit value as a run resource, deduping by content hash.""" content_bytes = content.encode("utf-8") content_hash = self._content_hash(content_bytes) @@ -112,7 +112,7 @@ def publish_value( with self._session_factory() as session: row = self._resource_repo.append( session, - run_id=run_id, + sample_id=sample_id, task_execution_id=task_execution_id, kind=kind.value, name=name, @@ -124,7 +124,7 @@ def publish_value( ) session.commit() session.refresh(row) - return RunResourceView.from_row(row) + return SampleResourceView.from_row(row) @staticmethod def _coerce_bytes(content: bytes | str) -> bytes: diff --git a/ergon_core/ergon_core/core/application/resources/repository.py b/ergon_core/ergon_core/core/application/resources/repository.py index fbfa0d2c2..fdd118dca 100644 --- a/ergon_core/ergon_core/core/application/resources/repository.py +++ b/ergon_core/ergon_core/core/application/resources/repository.py @@ -3,69 +3,69 @@ from uuid import UUID from ergon_core.api.errors import ContainmentViolation -from ergon_core.core.application.resources.errors import RunResourceNotFoundError -from ergon_core.core.application.resources.models import RunResourceView -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.telemetry.models import RunResource -from ergon_core.core.persistence.telemetry.models import RunTaskExecution +from ergon_core.core.application.resources.errors import SampleResourceNotFoundError +from ergon_core.core.application.resources.models import SampleResourceView +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.telemetry.models import SampleResource +from ergon_core.core.persistence.telemetry.models import SampleTaskAttempt from ergon_core.core.shared.json_types import JsonObject from sqlmodel import Session, select -class RunResourceRepository: +class SampleResourceRepository: """Domain repository for append-only run resource rows.""" - def list_by_run(self, session: Session, run_id: UUID) -> list[RunResource]: - stmt = select(RunResource).where(RunResource.run_id == run_id) + def list_by_run(self, session: Session, sample_id: UUID) -> list[SampleResource]: + stmt = select(SampleResource).where(SampleResource.sample_id == sample_id) return list(session.exec(stmt).all()) - def list_by_execution(self, session: Session, task_execution_id: UUID) -> list[RunResource]: - stmt = select(RunResource).where(RunResource.task_execution_id == task_execution_id) + def list_by_execution(self, session: Session, task_execution_id: UUID) -> list[SampleResource]: + stmt = select(SampleResource).where(SampleResource.task_execution_id == task_execution_id) return list(session.exec(stmt).all()) def list_for_run( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID | None = None, task_execution_id: UUID | None = None, kind: str | None = None, name: str | None = None, - ) -> list[RunResourceView]: - stmt = select(RunResource).where(RunResource.run_id == run_id) + ) -> list[SampleResourceView]: + stmt = select(SampleResource).where(SampleResource.sample_id == sample_id) if task_execution_id is not None: - execution = session.get(RunTaskExecution, task_execution_id) - if execution is None or execution.run_id != run_id: + execution = session.get(SampleTaskAttempt, task_execution_id) + if execution is None or execution.sample_id != sample_id: raise ContainmentViolation( parent_task_id=task_id, target_task_id=task_execution_id, ) - stmt = stmt.where(RunResource.task_execution_id == task_execution_id) + stmt = stmt.where(SampleResource.task_execution_id == task_execution_id) if task_id is not None: - node = session.get(RunGraphNode, (run_id, task_id)) - if node is None or node.run_id != run_id: + node = session.get(SampleGraphNode, (sample_id, task_id)) + if node is None or node.sample_id != sample_id: return [] execution_ids = session.exec( - select(RunTaskExecution.id).where( - RunTaskExecution.run_id == run_id, - RunTaskExecution.task_id == task_id, + select(SampleTaskAttempt.id).where( + SampleTaskAttempt.sample_id == sample_id, + SampleTaskAttempt.task_id == task_id, ) ).all() - stmt = stmt.where(RunResource.task_execution_id.in_(execution_ids)) + stmt = stmt.where(SampleResource.task_execution_id.in_(execution_ids)) if kind is not None: - stmt = stmt.where(RunResource.kind == kind) + stmt = stmt.where(SampleResource.kind == kind) if name is not None: - stmt = stmt.where(RunResource.name == name) + stmt = stmt.where(SampleResource.name == name) rows = session.exec( - stmt.order_by(RunResource.created_at.desc(), RunResource.id.desc()) + stmt.order_by(SampleResource.created_at.desc(), SampleResource.id.desc()) ).all() - return [RunResourceView.from_row(row) for row in rows] + return [SampleResourceView.from_row(row) for row in rows] - def get(self, session: Session, resource_id: UUID) -> RunResource: - resource = session.get(RunResource, resource_id) + def get(self, session: Session, resource_id: UUID) -> SampleResource: + resource = session.get(SampleResource, resource_id) if resource is None: - raise RunResourceNotFoundError(resource_id) + raise SampleResourceNotFoundError(resource_id) return resource def latest_by_path( @@ -74,14 +74,14 @@ def latest_by_path( *, task_execution_id: UUID, file_path: str, - ) -> RunResource | None: + ) -> SampleResource | None: stmt = ( - select(RunResource) + select(SampleResource) .where( - RunResource.task_execution_id == task_execution_id, - RunResource.file_path == file_path, + SampleResource.task_execution_id == task_execution_id, + SampleResource.file_path == file_path, ) - .order_by(RunResource.created_at.desc(), RunResource.id.desc()) + .order_by(SampleResource.created_at.desc(), SampleResource.id.desc()) .limit(1) ) return session.exec(stmt).first() @@ -92,12 +92,12 @@ def find_by_hash( *, task_execution_id: UUID, content_hash: str, - ) -> RunResource | None: + ) -> SampleResource | None: stmt = ( - select(RunResource) + select(SampleResource) .where( - RunResource.task_execution_id == task_execution_id, - RunResource.content_hash == content_hash, + SampleResource.task_execution_id == task_execution_id, + SampleResource.content_hash == content_hash, ) .limit(1) ) @@ -107,7 +107,7 @@ def append( # slopcop: ignore[max-function-params] self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, kind: str, name: str, @@ -118,9 +118,9 @@ def append( # slopcop: ignore[max-function-params] content_hash: str | None, metadata: JsonObject | None = None, copied_from_resource_id: UUID | None = None, - ) -> RunResource: - row = RunResource( - run_id=run_id, + ) -> SampleResource: + row = SampleResource( + sample_id=sample_id, task_execution_id=task_execution_id, kind=kind, name=name, diff --git a/ergon_core/ergon_core/core/application/resources/service.py b/ergon_core/ergon_core/core/application/resources/service.py index 8d1c99c1d..b375674c8 100644 --- a/ergon_core/ergon_core/core/application/resources/service.py +++ b/ergon_core/ergon_core/core/application/resources/service.py @@ -9,40 +9,40 @@ from sqlmodel import Session from ergon_core.api.errors import ContainmentViolation -from ergon_core.core.application.resources.models import RunResourceView -from ergon_core.core.application.resources.repository import RunResourceRepository +from ergon_core.core.application.resources.models import SampleResourceView +from ergon_core.core.application.resources.repository import SampleResourceRepository from ergon_core.core.persistence.shared.db import get_session SessionFactory: TypeAlias = Callable[[], AbstractContextManager[Session]] -class RunResourceReadService: +class SampleResourceReadService: """Owns worker-facing read policy for run resources.""" def __init__( self, *, - repository: RunResourceRepository | None = None, + repository: SampleResourceRepository | None = None, session_factory: SessionFactory = get_session, ) -> None: - self._resource_repo = repository or RunResourceRepository() + self._resource_repo = repository or SampleResourceRepository() self._session_factory = session_factory def list_for_run( self, *, - run_id: UUID, + sample_id: UUID, task_id: UUID | None = None, task_execution_id: UUID | None = None, kind: str | None = None, name: str | None = None, - ) -> tuple[RunResourceView, ...]: + ) -> tuple[SampleResourceView, ...]: """List resources visible inside a run.""" with self._session_factory() as session: rows = self._resource_repo.list_for_run( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, task_execution_id=task_execution_id, kind=kind, @@ -53,7 +53,7 @@ def list_for_run( def read_bytes( self, *, - run_id: UUID, + sample_id: UUID, current_task_id: UUID, resource_id: UUID, ) -> bytes: @@ -61,7 +61,7 @@ def read_bytes( with self._session_factory() as session: resource = self._resource_repo.get(session, resource_id) - if resource.run_id != run_id: + if resource.sample_id != sample_id: raise ContainmentViolation( parent_task_id=current_task_id, target_task_id=resource_id, diff --git a/ergon_core/ergon_core/core/application/runtime/__init__.py b/ergon_core/ergon_core/core/application/runtime/__init__.py index 4772ba08f..a80dbf432 100644 --- a/ergon_core/ergon_core/core/application/runtime/__init__.py +++ b/ergon_core/ergon_core/core/application/runtime/__init__.py @@ -1,4 +1,4 @@ -"""Runtime application owner for run graph and task lifecycle behavior.""" +"""Runtime application owner for sample graph and task lifecycle behavior.""" from importlib import import_module from types import ModuleType @@ -8,8 +8,8 @@ "inspection": "task_inspection", "management": "task_management", "propagation": "lifecycle", - "runs": "run_records", - "service": "run_lifecycle", + "samples": "sample_records", + "service": "sample_lifecycle", } __all__ = [ @@ -17,7 +17,7 @@ "inspection", "management", "propagation", - "runs", + "samples", "service", ] diff --git a/ergon_core/ergon_core/core/application/runtime/events.py b/ergon_core/ergon_core/core/application/runtime/events.py index 040819f8f..5be7f7850 100644 --- a/ergon_core/ergon_core/core/application/runtime/events.py +++ b/ergon_core/ergon_core/core/application/runtime/events.py @@ -30,18 +30,18 @@ def __init__(self, task_ready_dispatcher: TaskReadyDispatcher | None = None) -> async def dispatch_task_ready( self, *, - run_id: UUID, + sample_id: UUID, definition_id: UUID, task_id: UUID, ) -> None: """Emit the canonical ``task/ready`` event for a committed task state.""" if self._task_ready_dispatcher is not None: - await self._task_ready_dispatcher(run_id, definition_id, task_id) + await self._task_ready_dispatcher(sample_id, definition_id, task_id) logger.info("dispatch_task_ready: fired custom dispatcher for task %s", task_id) return event = TaskReadyEvent( - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=task_id, ) diff --git a/ergon_core/ergon_core/core/application/runtime/graph_lookup.py b/ergon_core/ergon_core/core/application/runtime/graph_lookup.py index 7086064ed..17ed25fdf 100644 --- a/ergon_core/ergon_core/core/application/runtime/graph_lookup.py +++ b/ergon_core/ergon_core/core/application/runtime/graph_lookup.py @@ -2,23 +2,23 @@ from uuid import UUID -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode from sqlmodel import Session, select class GraphNodeLookup: """Caches task and edge ids for one run.""" - def __init__(self, session: Session, run_id: UUID) -> None: + def __init__(self, session: Session, sample_id: UUID) -> None: task_ids = session.exec( - select(RunGraphNode.task_id).where(RunGraphNode.run_id == run_id) + select(SampleGraphNode.task_id).where(SampleGraphNode.sample_id == sample_id) ).all() self._tasks: frozenset[UUID] = frozenset(task_ids) edge_rows = session.exec( - select(RunGraphEdge.id, RunGraphEdge.source_task_id, RunGraphEdge.target_task_id).where( - RunGraphEdge.run_id == run_id - ) + select( + SampleGraphEdge.id, SampleGraphEdge.source_task_id, SampleGraphEdge.target_task_id + ).where(SampleGraphEdge.sample_id == sample_id) ).all() self._edges: dict[tuple[UUID, UUID], UUID] = { (src, tgt): eid for eid, src, tgt in edge_rows diff --git a/ergon_core/ergon_core/core/application/runtime/graph_repository.py b/ergon_core/ergon_core/core/application/runtime/graph_repository.py index c44b498b5..437869f72 100644 --- a/ergon_core/ergon_core/core/application/runtime/graph_repository.py +++ b/ergon_core/ergon_core/core/application/runtime/graph_repository.py @@ -2,8 +2,8 @@ Every mutation method: 1. Validates structural invariants (acyclicity, referential integrity). -2. Writes to run_graph_* tables. -3. Appends to run_graph_mutations in the same transaction. +2. Writes to sample_graph_* tables. +3. Appends to sample_graph_mutations in the same transaction. The repository does NOT validate status transitions or authorization. Those are the experiment layer's responsibility. @@ -23,10 +23,10 @@ ExperimentDefinitionWorker, ) from ergon_core.core.persistence.graph.models import ( - RunGraphAnnotation, - RunGraphEdge, - RunGraphMutation, - RunGraphNode, + SampleGraphAnnotation, + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, ) from ergon_core.core.application.runtime.status import TERMINAL_STATUSES from ergon_core.core.application.runtime.errors import ( @@ -47,7 +47,7 @@ NodeAddedMutation, NodeFieldChangedMutation, NodeStatusChangedMutation, - RunGraphNodeView, + SampleGraphNodeView, WorkflowGraphDto, ) from ergon_core.core.shared.utils import utcnow @@ -78,10 +78,10 @@ class RuntimeGraphRepository: """ def __init__(self) -> None: - self._mutation_listeners: list[Callable[[RunGraphMutation], Awaitable[None]]] = [] + self._mutation_listeners: list[Callable[[SampleGraphMutation], Awaitable[None]]] = [] def add_mutation_listener( - self, listener: Callable[[RunGraphMutation], Awaitable[None]] + self, listener: Callable[[SampleGraphMutation], Awaitable[None]] ) -> None: self._mutation_listeners.append(listener) @@ -90,7 +90,7 @@ def add_mutation_listener( def initialize_from_definition( self, session: Session, - run_id: UUID, + sample_id: UUID, definition_id: UUID, *, initial_node_status: str, @@ -149,14 +149,14 @@ def initialize_from_definition( ) def_to_node: dict[UUID, UUID] = {} - node_rows: list[RunGraphNode] = [] + node_rows: list[SampleGraphNode] = [] for task in tasks: def_to_node[task.id] = task.id node_rows.append( - RunGraphNode( + SampleGraphNode( task_id=task.id, - run_id=run_id, + sample_id=sample_id, instance_key=instance_key_by_id[task.instance_id], task_slug=task.task_slug, description=task.description, @@ -169,12 +169,12 @@ def initialize_from_definition( ) ) - edge_rows: list[RunGraphEdge] = [] + edge_rows: list[SampleGraphEdge] = [] for dep in deps: edge_rows.append( - RunGraphEdge( + SampleGraphEdge( id=uuid4(), - run_id=run_id, + sample_id=sample_id, definition_dependency_id=dep.id, source_task_id=def_to_node[dep.depends_on_task_id], target_task_id=def_to_node[dep.task_id], @@ -188,15 +188,15 @@ def initialize_from_definition( session.add_all(edge_rows) session.flush() - seq = self._next_sequence(session, run_id) + seq = self._next_sequence(session, sample_id) - annotation_rows: list[RunGraphAnnotation] = [] - mutation_rows: list[RunGraphMutation] = [] + annotation_rows: list[SampleGraphAnnotation] = [] + mutation_rows: list[SampleGraphMutation] = [] for task, node in zip(tasks, node_rows): mutation_rows.append( - RunGraphMutation( - run_id=run_id, + SampleGraphMutation( + sample_id=sample_id, sequence=seq, mutation_type="node.added", target_type="node", @@ -213,8 +213,8 @@ def initialize_from_definition( payload = task.task_json.get("task_payload") or {} if payload: annotation_rows.append( - RunGraphAnnotation( - run_id=run_id, + SampleGraphAnnotation( + sample_id=sample_id, target_type="node", target_id=node.task_id, namespace="payload", @@ -224,8 +224,8 @@ def initialize_from_definition( ) ) mutation_rows.append( - RunGraphMutation( - run_id=run_id, + SampleGraphMutation( + sample_id=sample_id, sequence=seq, mutation_type="annotation.set", target_type="node", @@ -244,8 +244,8 @@ def initialize_from_definition( for edge in edge_rows: mutation_rows.append( - RunGraphMutation( - run_id=run_id, + SampleGraphMutation( + sample_id=sample_id, sequence=seq, mutation_type="edge.added", target_type="edge", @@ -264,7 +264,7 @@ def initialize_from_definition( session.flush() return WorkflowGraphDto( - run_id=run_id, + sample_id=sample_id, nodes=[_to_node_dto(n) for n in node_rows], edges=[_to_edge_dto(e) for e in edge_rows], ) @@ -275,39 +275,39 @@ async def node( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str | None = None, - ) -> RunGraphNodeView: - """Inflate a typed RunGraphNodeView from `run_graph_nodes.task_json`. + ) -> SampleGraphNodeView: + """Inflate a typed SampleGraphNodeView from `sample_graph_nodes.task_json`. The repository reads only the run-tier ``task_json`` column and reconstructs a typed Task via ``Task.from_definition``. The job - body downstream receives a ``RunGraphNodeView`` with the Task + body downstream receives a ``SampleGraphNodeView`` with the Task already inflated: no raw JSON and no definition-tier reads. Async because ``Task.from_definition`` is async. - ``RunGraphNode.task_id`` is the task identity for both + ``SampleGraphNode.task_id`` is the task identity for both definition-seeded and dynamic nodes. """ row = session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.task_id == task_id, + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.task_id == task_id, ) ).first() if row is None: - raise NodeNotFoundError(task_id, run_id=run_id) + raise NodeNotFoundError(task_id, sample_id=sample_id) task = await Task.from_definition( row.task_json, task_id=row.task_id, sandbox_id=sandbox_id, ) - return RunGraphNodeView( - run_id=row.run_id, + return SampleGraphNodeView( + sample_id=row.sample_id, task_id=row.task_id, parent_task_id=row.parent_task_id, status=row.status, @@ -318,7 +318,7 @@ async def node( async def add_node( # slopcop: ignore[max-function-params] self, session: Session, - run_id: UUID, + sample_id: UUID, *, task_slug: str, instance_key: str, @@ -342,8 +342,8 @@ async def add_node( # slopcop: ignore[max-function-params] if task_json is None: raise ValueError("RuntimeGraphRepository.add_task requires task_json") now = utcnow() - node = RunGraphNode( - run_id=run_id, + node = SampleGraphNode( + sample_id=sample_id, instance_key=instance_key, task_slug=task_slug, description=description, @@ -361,7 +361,7 @@ async def add_node( # slopcop: ignore[max-function-params] await self._log_mutation( session, - run_id, + sample_id, mutation_type="node.added", target_type="node", target_id=node.task_id, @@ -375,7 +375,7 @@ async def update_node_status( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, new_status: str, meta: MutationMeta, @@ -390,7 +390,7 @@ async def update_node_status( write a terminal status resolve to "first writer wins" without requiring distributed locks. """ - node = self._get_node_row(session, run_id, task_id) + node = self._get_node_row(session, sample_id, task_id) if only_if_not_terminal and node.status in TERMINAL_STATUSES: return False @@ -403,7 +403,7 @@ async def update_node_status( await self._log_mutation( session, - run_id, + sample_id, mutation_type="node.status_changed", target_type="node", target_id=task_id, @@ -417,7 +417,7 @@ async def update_node_field( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, field: Literal["description", "assigned_worker_slug"], value: str | None, @@ -427,7 +427,7 @@ async def update_node_field( raise ValueError( f"Field {field!r} is not updatable. Allowed: {sorted(_UPDATABLE_NODE_FIELDS)}" ) - node = self._get_node_row(session, run_id, task_id) + node = self._get_node_row(session, sample_id, task_id) if field == "description": if value is None: raise ValueError("description cannot be cleared") @@ -442,7 +442,7 @@ async def update_node_field( await self._log_mutation( session, - run_id, + sample_id, mutation_type="node.field_changed", target_type="node", target_id=task_id, @@ -457,20 +457,20 @@ async def update_node_field( async def add_edge( self, session: Session, - run_id: UUID, + sample_id: UUID, *, source_task_id: UUID, target_task_id: UUID, status: str, meta: MutationMeta, ) -> GraphEdgeDto: - self._require_node_exists(session, run_id, source_task_id) - self._require_node_exists(session, run_id, target_task_id) - self._check_no_cycle(session, run_id, source_task_id, target_task_id) + self._require_node_exists(session, sample_id, source_task_id) + self._require_node_exists(session, sample_id, target_task_id) + self._check_no_cycle(session, sample_id, source_task_id, target_task_id) now = utcnow() - edge = RunGraphEdge( - run_id=run_id, + edge = SampleGraphEdge( + sample_id=sample_id, source_task_id=source_task_id, target_task_id=target_task_id, status=status, @@ -482,7 +482,7 @@ async def add_edge( await self._log_mutation( session, - run_id, + sample_id, mutation_type="edge.added", target_type="edge", target_id=edge.id, @@ -496,12 +496,12 @@ async def update_edge_status( self, session: Session, *, - run_id: UUID, + sample_id: UUID, edge_id: UUID, new_status: str, meta: MutationMeta, ) -> GraphEdgeDto: - edge = self._get_edge_row(session, run_id, edge_id) + edge = self._get_edge_row(session, sample_id, edge_id) old_status = edge.status edge.status = new_status @@ -511,7 +511,7 @@ async def update_edge_status( await self._log_mutation( session, - run_id, + sample_id, mutation_type="edge.status_changed", target_type="edge", target_id=edge_id, @@ -527,23 +527,23 @@ def get_node( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> GraphNodeDto: - return _to_node_dto(self._get_node_row(session, run_id, task_id)) + return _to_node_dto(self._get_node_row(session, sample_id, task_id)) def get_incoming_edges( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> list[GraphEdgeDto]: rows = list( session.exec( - select(RunGraphEdge).where( - RunGraphEdge.run_id == run_id, - RunGraphEdge.target_task_id == task_id, + select(SampleGraphEdge).where( + SampleGraphEdge.sample_id == sample_id, + SampleGraphEdge.target_task_id == task_id, ) ).all() ) @@ -553,14 +553,14 @@ def get_outgoing_edges( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> list[GraphEdgeDto]: rows = list( session.exec( - select(RunGraphEdge).where( - RunGraphEdge.run_id == run_id, - RunGraphEdge.source_task_id == task_id, + select(SampleGraphEdge).where( + SampleGraphEdge.sample_id == sample_id, + SampleGraphEdge.source_task_id == task_id, ) ).all() ) @@ -571,59 +571,59 @@ def get_outgoing_edges( def _get_node_row( self, session: Session, - run_id: UUID, + sample_id: UUID, task_id: UUID, - ) -> RunGraphNode: + ) -> SampleGraphNode: row = session.exec( - select(RunGraphNode).where( - RunGraphNode.task_id == task_id, - RunGraphNode.run_id == run_id, + select(SampleGraphNode).where( + SampleGraphNode.task_id == task_id, + SampleGraphNode.sample_id == sample_id, ) ).first() if row is None: - raise NodeNotFoundError(task_id, run_id=run_id) + raise NodeNotFoundError(task_id, sample_id=sample_id) return row def _get_edge_row( self, session: Session, - run_id: UUID, + sample_id: UUID, edge_id: UUID, - ) -> RunGraphEdge: + ) -> SampleGraphEdge: row = session.exec( - select(RunGraphEdge).where( - RunGraphEdge.id == edge_id, - RunGraphEdge.run_id == run_id, + select(SampleGraphEdge).where( + SampleGraphEdge.id == edge_id, + SampleGraphEdge.sample_id == sample_id, ) ).first() if row is None: - raise EdgeNotFoundError(edge_id, run_id=run_id) + raise EdgeNotFoundError(edge_id, sample_id=sample_id) return row def _require_node_exists( self, session: Session, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> None: exists = session.exec( - select(RunGraphNode.task_id).where( - RunGraphNode.task_id == task_id, - RunGraphNode.run_id == run_id, + select(SampleGraphNode.task_id).where( + SampleGraphNode.task_id == task_id, + SampleGraphNode.sample_id == sample_id, ) ).first() if exists is None: raise DanglingEdgeError( edge_id=uuid4(), missing_task_id=task_id, - run_id=run_id, + sample_id=sample_id, ) - def _next_sequence(self, session: Session, run_id: UUID) -> int: + def _next_sequence(self, session: Session, sample_id: UUID) -> int: stmt = ( - select(RunGraphMutation.sequence) - .where(RunGraphMutation.run_id == run_id) - .order_by(col(RunGraphMutation.sequence).desc()) + select(SampleGraphMutation.sequence) + .where(SampleGraphMutation.sample_id == sample_id) + .order_by(col(SampleGraphMutation.sequence).desc()) .limit(1) ) last = session.exec(stmt).first() @@ -632,7 +632,7 @@ def _next_sequence(self, session: Session, run_id: UUID) -> int: async def _log_mutation( self, session: Session, - run_id: UUID, + sample_id: UUID, *, mutation_type: str, target_type: str, @@ -641,9 +641,9 @@ async def _log_mutation( old_value: GraphMutationValue | None, new_value: GraphMutationValue, ) -> None: - seq = self._next_sequence(session, run_id) - row = RunGraphMutation( - run_id=run_id, + seq = self._next_sequence(session, sample_id) + row = SampleGraphMutation( + sample_id=sample_id, sequence=seq, mutation_type=mutation_type, target_type=target_type, @@ -666,13 +666,17 @@ async def _log_mutation( def _check_no_cycle( self, session: Session, - run_id: UUID, + sample_id: UUID, source_id: UUID, target_id: UUID, ) -> None: """DFS from target_id following outgoing edges. If we reach source_id, adding source→target would create a cycle.""" - edges = list(session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all()) + edges = list( + session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all() + ) adj: dict[UUID, list[UUID]] = defaultdict(list) for e in edges: adj[e.source_task_id].append(e.target_task_id) @@ -682,7 +686,7 @@ def _check_no_cycle( while stack: current = stack.pop() if current == source_id: - raise CycleError(source_id, target_id, run_id=run_id) + raise CycleError(source_id, target_id, sample_id=sample_id) if current in visited: continue visited.add(current) @@ -694,10 +698,10 @@ def _check_no_cycle( # --------------------------------------------------------------------------- -def _to_node_dto(row: RunGraphNode) -> GraphNodeDto: +def _to_node_dto(row: SampleGraphNode) -> GraphNodeDto: return GraphNodeDto( task_id=row.task_id, - run_id=row.run_id, + sample_id=row.sample_id, instance_key=row.instance_key, task_slug=row.task_slug, description=row.description, @@ -708,10 +712,10 @@ def _to_node_dto(row: RunGraphNode) -> GraphNodeDto: ) -def _to_edge_dto(row: RunGraphEdge) -> GraphEdgeDto: +def _to_edge_dto(row: SampleGraphEdge) -> GraphEdgeDto: return GraphEdgeDto( id=row.id, - run_id=row.run_id, + sample_id=row.sample_id, definition_dependency_id=row.definition_dependency_id, source_task_id=row.source_task_id, target_task_id=row.target_task_id, @@ -719,7 +723,7 @@ def _to_edge_dto(row: RunGraphEdge) -> GraphEdgeDto: ) -def _node_snapshot(node: RunGraphNode) -> NodeAddedMutation: +def _node_snapshot(node: SampleGraphNode) -> NodeAddedMutation: return NodeAddedMutation( task_slug=node.task_slug, instance_key=node.instance_key, @@ -729,7 +733,7 @@ def _node_snapshot(node: RunGraphNode) -> NodeAddedMutation: ) -def _edge_snapshot(edge: RunGraphEdge) -> EdgeAddedMutation: +def _edge_snapshot(edge: SampleGraphEdge) -> EdgeAddedMutation: return EdgeAddedMutation( source_task_id=edge.source_task_id, target_task_id=edge.target_task_id, diff --git a/ergon_core/ergon_core/core/application/runtime/graph_traversal.py b/ergon_core/ergon_core/core/application/runtime/graph_traversal.py index b22ee07c2..bbbb4a40e 100644 --- a/ergon_core/ergon_core/core/application/runtime/graph_traversal.py +++ b/ergon_core/ergon_core/core/application/runtime/graph_traversal.py @@ -3,19 +3,19 @@ from collections import deque from uuid import UUID -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from sqlmodel import Session, select def descendants( session: Session, *, - run_id: UUID, + sample_id: UUID, root_task_id: UUID, max_depth: int | None = None, -) -> list[RunGraphNode]: +) -> list[SampleGraphNode]: """Return containment descendants under root_task_id in breadth-first order.""" - result: list[RunGraphNode] = [] + result: list[SampleGraphNode] = [] queue: deque[tuple[UUID, int]] = deque([(root_task_id, 0)]) while queue: @@ -25,9 +25,9 @@ def descendants( children = list( session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.parent_task_id == parent_id, + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.parent_task_id == parent_id, ) ).all() ) @@ -41,7 +41,7 @@ def descendants( def descendant_ids( session: Session, *, - run_id: UUID, + sample_id: UUID, root_task_id: UUID, max_depth: int | None = None, ) -> set[UUID]: @@ -50,7 +50,7 @@ def descendant_ids( node.task_id for node in descendants( session, - run_id=run_id, + sample_id=sample_id, root_task_id=root_task_id, max_depth=max_depth, ) diff --git a/ergon_core/ergon_core/core/application/runtime/lifecycle.py b/ergon_core/ergon_core/core/application/runtime/lifecycle.py index 2a284362a..2ef9d6e69 100644 --- a/ergon_core/ergon_core/core/application/runtime/lifecycle.py +++ b/ergon_core/ergon_core/core/application/runtime/lifecycle.py @@ -1,7 +1,7 @@ """Workflow propagation service helpers. -All state is stored in the graph layer (RunGraphNode, RunGraphEdge, -RunGraphMutation). The graph mutation WAL is the single source of truth +All state is stored in the graph layer (SampleGraphNode, SampleGraphEdge, +SampleGraphMutation). The graph mutation WAL is the single source of truth for DAG execution state. """ @@ -13,7 +13,7 @@ ExperimentDefinitionTaskDependency, ) from ergon_core.core.application.runtime import status as graph_status -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode from ergon_core.core.application.runtime.models import MutationMeta from ergon_core.core.application.runtime.graph_lookup import GraphNodeLookup from ergon_core.core.application.runtime.graph_repository import RuntimeGraphRepository @@ -24,7 +24,7 @@ async def _update_task_status( session: Session, - run_id: UUID, + sample_id: UUID, task_id: UUID, new_status: str, *, @@ -39,7 +39,7 @@ async def _update_task_status( reason = str(event_metadata["error"]) await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, new_status=new_status, meta=MutationMeta(actor="system:propagation", reason=reason), @@ -48,7 +48,7 @@ async def _update_task_status( async def mark_task_ready( session: Session, - run_id: UUID, + sample_id: UUID, task_id: UUID, *, graph_repo: RuntimeGraphRepository, @@ -56,7 +56,7 @@ async def mark_task_ready( ) -> None: await _update_task_status( session, - run_id, + sample_id, task_id, graph_status.PENDING, graph_repo=graph_repo, @@ -66,7 +66,7 @@ async def mark_task_ready( async def mark_task_running( session: Session, - run_id: UUID, + sample_id: UUID, task_id: UUID, execution_id: UUID, *, @@ -75,7 +75,7 @@ async def mark_task_running( ) -> None: await _update_task_status( session, - run_id, + sample_id, task_id, graph_status.RUNNING, graph_repo=graph_repo, @@ -85,7 +85,7 @@ async def mark_task_running( async def mark_task_failed( session: Session, - run_id: UUID, + sample_id: UUID, task_id: UUID, error: str, *, @@ -95,7 +95,7 @@ async def mark_task_failed( ) -> None: await _update_task_status( session, - run_id, + sample_id, task_id, graph_status.FAILED, graph_repo=graph_repo, @@ -106,7 +106,7 @@ async def mark_task_failed( async def get_initial_ready_tasks( session: Session, - run_id: UUID, + sample_id: UUID, definition_id: UUID, *, graph_repo: RuntimeGraphRepository, @@ -128,7 +128,7 @@ async def get_initial_ready_tasks( for task_id in ready_ids: await mark_task_ready( session, - run_id, + sample_id, task_id, graph_repo=graph_repo, graph_lookup=graph_lookup, @@ -140,7 +140,7 @@ async def get_initial_ready_tasks( async def mark_task_failed_by_node( session: Session, - run_id: UUID, + sample_id: UUID, task_id: UUID, error: str, *, @@ -150,7 +150,7 @@ async def mark_task_failed_by_node( del execution_id await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, new_status=graph_status.FAILED, meta=MutationMeta( @@ -163,7 +163,7 @@ async def mark_task_failed_by_node( # TODO: as per the experiments design comment, feels like alot of this would benefit from being a service or repository method? async def _block_successors_bfs( session: Session, - run_id: UUID, + sample_id: UUID, seed_task_ids: set[UUID], *, failed_task_id: UUID, @@ -174,7 +174,7 @@ async def _block_successors_bfs( queue = list(seed_task_ids) while queue: target_id = queue.pop() - target_node = session.get(RunGraphNode, (run_id, target_id)) + target_node = session.get(SampleGraphNode, (sample_id, target_id)) if target_node is None: continue if target_node.status == graph_status.RUNNING: @@ -184,7 +184,7 @@ async def _block_successors_bfs( applied = await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=target_id, new_status=graph_status.BLOCKED, meta=MutationMeta( @@ -197,16 +197,16 @@ async def _block_successors_bfs( if applied: target_outgoing = list( session.exec( - select(RunGraphEdge).where( - RunGraphEdge.run_id == run_id, - RunGraphEdge.source_task_id == target_id, + select(SampleGraphEdge).where( + SampleGraphEdge.sample_id == sample_id, + SampleGraphEdge.source_task_id == target_id, ) ).all() ) for edge in target_outgoing: await graph_repo.update_edge_status( session, - run_id=run_id, + sample_id=sample_id, edge_id=edge.id, new_status=graph_status.EDGE_INVALIDATED, meta=_PROPAGATION_META, @@ -214,21 +214,21 @@ async def _block_successors_bfs( queue.append(edge.target_task_id) -def _dependency_free_children(session: Session, run_id: UUID, task_id: UUID) -> set[UUID]: +def _dependency_free_children(session: Session, sample_id: UUID, task_id: UUID) -> set[UUID]: child_ids: set[UUID] = set() containment_children = list( session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.parent_task_id == task_id, + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.parent_task_id == task_id, ) ).all() ) for child in containment_children: has_incoming_edges = session.exec( - select(RunGraphEdge.id) - .where(RunGraphEdge.run_id == run_id) - .where(RunGraphEdge.target_task_id == child.task_id) + select(SampleGraphEdge.id) + .where(SampleGraphEdge.sample_id == sample_id) + .where(SampleGraphEdge.target_task_id == child.task_id) ).first() if has_incoming_edges is None: child_ids.add(child.task_id) @@ -237,7 +237,7 @@ def _dependency_free_children(session: Session, run_id: UUID, task_id: UUID) -> async def on_task_completed_or_failed( session: Session, - run_id: UUID, + sample_id: UUID, task_id: UUID, terminal_status: str, *, @@ -248,9 +248,9 @@ async def on_task_completed_or_failed( outgoing = list( session.exec( - select(RunGraphEdge).where( - RunGraphEdge.run_id == run_id, - RunGraphEdge.source_task_id == task_id, + select(SampleGraphEdge).where( + SampleGraphEdge.sample_id == sample_id, + SampleGraphEdge.source_task_id == task_id, ) ).all() ) @@ -259,7 +259,7 @@ async def on_task_completed_or_failed( for edge in outgoing: await graph_repo.update_edge_status( session, - run_id=run_id, + sample_id=sample_id, edge_id=edge.id, new_status=edge_status, meta=_PROPAGATION_META, @@ -267,13 +267,13 @@ async def on_task_completed_or_failed( candidate_task_ids = {edge.target_task_id for edge in outgoing} if is_success: - candidate_task_ids.update(_dependency_free_children(session, run_id, task_id)) + candidate_task_ids.update(_dependency_free_children(session, sample_id, task_id)) newly_ready: list[UUID] = [] if not is_success: await _block_successors_bfs( session, - run_id=run_id, + sample_id=sample_id, seed_task_ids=candidate_task_ids, failed_task_id=task_id, terminal_status=terminal_status, @@ -283,7 +283,7 @@ async def on_task_completed_or_failed( return newly_ready for candidate_id in candidate_task_ids: - candidate_node = session.get(RunGraphNode, (run_id, candidate_id)) + candidate_node = session.get(SampleGraphNode, (sample_id, candidate_id)) if candidate_node is None: continue if ( @@ -302,15 +302,15 @@ async def on_task_completed_or_failed( incoming = list( session.exec( - select(RunGraphEdge).where( - RunGraphEdge.run_id == run_id, - RunGraphEdge.target_task_id == candidate_id, + select(SampleGraphEdge).where( + SampleGraphEdge.sample_id == sample_id, + SampleGraphEdge.target_task_id == candidate_id, ) ).all() ) source_nodes = [ - session.get(RunGraphNode, (run_id, edge.source_task_id)) for edge in incoming + session.get(SampleGraphNode, (sample_id, edge.source_task_id)) for edge in incoming ] if all(node is not None and node.status == graph_status.COMPLETED for node in source_nodes): reason = ( @@ -320,7 +320,7 @@ async def on_task_completed_or_failed( ) await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=candidate_id, new_status=graph_status.PENDING, meta=MutationMeta( @@ -335,10 +335,12 @@ async def on_task_completed_or_failed( return newly_ready -def is_workflow_complete_v2(session: Session, run_id: UUID) -> bool: +def is_workflow_complete_v2(session: Session, sample_id: UUID) -> bool: """Every node terminal; zero FAILED. CANCELLED is neutral.""" statuses = list( - session.exec(select(RunGraphNode.status).where(RunGraphNode.run_id == run_id)).all() + session.exec( + select(SampleGraphNode.status).where(SampleGraphNode.sample_id == sample_id) + ).all() ) if not statuses: return True @@ -350,10 +352,12 @@ def is_workflow_complete_v2(session: Session, run_id: UUID) -> bool: _SETTLED_STATUSES = graph_status.TERMINAL_STATUSES | frozenset({graph_status.BLOCKED}) -def is_workflow_failed_v2(session: Session, run_id: UUID) -> bool: +def is_workflow_failed_v2(session: Session, sample_id: UUID) -> bool: """All nodes settled and at least one FAILED.""" statuses = list( - session.exec(select(RunGraphNode.status).where(RunGraphNode.run_id == run_id)).all() + session.exec( + select(SampleGraphNode.status).where(SampleGraphNode.sample_id == sample_id) + ).all() ) if not statuses: return False diff --git a/ergon_core/ergon_core/core/application/runtime/models.py b/ergon_core/ergon_core/core/application/runtime/models.py index 5e905398e..ada90ed30 100644 --- a/ergon_core/ergon_core/core/application/runtime/models.py +++ b/ergon_core/ergon_core/core/application/runtime/models.py @@ -4,7 +4,7 @@ UUID fields use NewType aliases (RunId, NodeId, etc.) so that type checkers catch cross-field swaps — e.g. passing a task id where a -run_id is expected. The aliases are erased at runtime (zero +sample_id is expected. The aliases are erased at runtime (zero serialization cost). """ @@ -45,7 +45,7 @@ class GraphNodeDto(BaseModel): model_config = {"frozen": True} task_id: NodeId - run_id: RunId + sample_id: RunId instance_key: str task_slug: str description: str @@ -78,7 +78,7 @@ class GraphEdgeDto(BaseModel): model_config = {"frozen": True} id: EdgeId - run_id: RunId + sample_id: RunId definition_dependency_id: DefinitionId | None source_task_id: NodeId target_task_id: NodeId @@ -94,7 +94,7 @@ class GraphAnnotationDto(BaseModel): model_config = {"frozen": True} id: UUID = Field(description="Identifier of the annotation row itself.") - run_id: RunId + sample_id: RunId target_type: GraphTargetType target_id: UUID = Field( description=( @@ -113,7 +113,7 @@ class GraphMutationRecordDto(BaseModel): model_config = {"frozen": True} id: UUID = Field(description="Identifier of the mutation row itself, not a graph target id.") - run_id: RunId + sample_id: RunId sequence: int mutation_type: MutationType target_type: GraphTargetType @@ -135,7 +135,7 @@ class WorkflowGraphDto(BaseModel): model_config = {"frozen": True} - run_id: RunId + sample_id: RunId nodes: list[GraphNodeDto] = Field(default_factory=list) edges: list[GraphEdgeDto] = Field(default_factory=list) @@ -255,8 +255,8 @@ class AnnotationDeletedMutation(BaseModel): ] -class RunGraphNodeView(BaseModel): - """Typed view of one ``run_graph_nodes`` row + its inflated Task. +class SampleGraphNodeView(BaseModel): + """Typed view of one ``sample_graph_nodes`` row + its inflated Task. The job body receives this view from ``RuntimeGraphRepository.node`` instead of raw JSON. The Task is already inflated via @@ -269,7 +269,7 @@ class RunGraphNodeView(BaseModel): model_config = {"frozen": True, "arbitrary_types_allowed": True} - run_id: RunId + sample_id: RunId task_id: UUID parent_task_id: NodeId | None status: str diff --git a/ergon_core/ergon_core/core/application/runtime/orchestration.py b/ergon_core/ergon_core/core/application/runtime/orchestration.py index 9a296193d..2b2d6359b 100644 --- a/ergon_core/ergon_core/core/application/runtime/orchestration.py +++ b/ergon_core/ergon_core/core/application/runtime/orchestration.py @@ -35,14 +35,14 @@ class TaskDescriptor(BaseModel): class InitializeWorkflowCommand(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID definition_id: UUID class InitializedWorkflow(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID definition_id: UUID benchmark_type: str total_tasks: int @@ -54,7 +54,7 @@ class InitializedWorkflow(BaseModel): class PrepareTaskExecutionCommand(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID @@ -68,7 +68,7 @@ class PreparedTaskExecution(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID task_slug: str @@ -94,7 +94,7 @@ class FailTaskExecutionCommand(BaseModel): model_config = {"frozen": True} execution_id: UUID - run_id: UUID + sample_id: UUID task_id: UUID | None error_message: str error_json: JsonObject | None = None @@ -109,7 +109,7 @@ class WorkflowTerminalState(StrEnum): class PropagateTaskCompletionCommand(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID execution_id: UUID @@ -118,7 +118,7 @@ class PropagateTaskCompletionCommand(BaseModel): class PropagationResult(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID definition_id: UUID completed_task_id: UUID ready_tasks: list[TaskDescriptor] = Field(default_factory=list) @@ -128,14 +128,14 @@ class PropagationResult(BaseModel): class FinalizeWorkflowCommand(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID definition_id: UUID class FinalizedWorkflowResult(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID final_score: float | None = None normalized_score: float | None = None evaluators_count: int = 0 diff --git a/ergon_core/ergon_core/core/application/runtime/resources.py b/ergon_core/ergon_core/core/application/runtime/resources.py index c9076dfb4..ce3f4b614 100644 --- a/ergon_core/ergon_core/core/application/runtime/resources.py +++ b/ergon_core/ergon_core/core/application/runtime/resources.py @@ -5,7 +5,7 @@ module when new runtime resource callers are added. """ -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService class RuntimeResourceService(WorkflowService): diff --git a/ergon_core/ergon_core/core/application/runtime/run_identity.py b/ergon_core/ergon_core/core/application/runtime/run_identity.py deleted file mode 100644 index 18d18f0ea..000000000 --- a/ergon_core/ergon_core/core/application/runtime/run_identity.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Run identity helpers owned by the runtime layer.""" - -from __future__ import annotations - -from uuid import UUID - -from ergon_core.core.application.runtime.task_errors import RunRecordMissingError -from ergon_core.core.persistence.telemetry.models import RunRecord -from sqlmodel import Session, select - - -def definition_id_for_run(session: Session, run_id: UUID) -> UUID: - """Return the definition id for a run or fail the runtime invariant loudly.""" - run = session.exec(select(RunRecord).where(RunRecord.id == run_id)).first() - if run is None: - raise RunRecordMissingError(run_id) - return run.definition_id diff --git a/ergon_core/ergon_core/core/application/runtime/sample_identity.py b/ergon_core/ergon_core/core/application/runtime/sample_identity.py new file mode 100644 index 000000000..fa0df8a04 --- /dev/null +++ b/ergon_core/ergon_core/core/application/runtime/sample_identity.py @@ -0,0 +1,17 @@ +"""Run identity helpers owned by the runtime layer.""" + +from __future__ import annotations + +from uuid import UUID + +from ergon_core.core.application.runtime.task_errors import SampleRecordMissingError +from ergon_core.core.persistence.telemetry.models import SampleRecord +from sqlmodel import Session, select + + +def definition_id_for_run(session: Session, sample_id: UUID) -> UUID: + """Return the definition id for a run or fail the runtime invariant loudly.""" + run = session.exec(select(SampleRecord).where(SampleRecord.id == sample_id)).first() + if run is None: + raise SampleRecordMissingError(sample_id) + return run.definition_id diff --git a/ergon_core/ergon_core/core/application/runtime/run_lifecycle.py b/ergon_core/ergon_core/core/application/runtime/sample_lifecycle.py similarity index 80% rename from ergon_core/ergon_core/core/application/runtime/run_lifecycle.py rename to ergon_core/ergon_core/core/application/runtime/sample_lifecycle.py index b384ac87a..8122c9bb8 100644 --- a/ergon_core/ergon_core/core/application/runtime/run_lifecycle.py +++ b/ergon_core/ergon_core/core/application/runtime/sample_lifecycle.py @@ -8,25 +8,29 @@ ExperimentDefinitionTask, ) from ergon_core.core.application.runtime import status as graph_status -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunResourceKind, RunStatus, TaskExecutionStatus +from ergon_core.core.persistence.shared.enums import ( + SampleResourceKind, + SampleStatus, + TaskExecutionStatus, +) from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskEvaluation, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskEvaluation, + SampleTaskAttempt, ) from ergon_core.core.application.runtime.events import ( RuntimeEventDispatcher, TaskReadyDispatcher, ) -from ergon_core.core.application.runtime.run_records import ( +from ergon_core.core.application.runtime.sample_records import ( cancel_run, create_run, latest_run_for_definition, ) -from ergon_core.core.application.runtime.run_identity import definition_id_for_run +from ergon_core.core.application.runtime.sample_identity import definition_id_for_run from ergon_core.core.application.runtime.graph_lookup import GraphNodeLookup from ergon_core.core.application.runtime.lifecycle import ( get_initial_ready_tasks, @@ -116,7 +120,7 @@ async def initialize(self, command: InitializeWorkflowCommand) -> InitializedWor self._graph_repo.initialize_from_definition( session, - command.run_id, + command.sample_id, command.definition_id, initial_node_status=graph_status.PENDING, initial_edge_status=graph_status.EDGE_PENDING, @@ -132,20 +136,20 @@ async def initialize(self, command: InitializeWorkflowCommand) -> InitializedWor ) for t in all_tasks ] - graph_lookup = GraphNodeLookup(session, command.run_id) + graph_lookup = GraphNodeLookup(session, command.sample_id) run_record = require_not_none( - session.get(RunRecord, command.run_id), - f"RunRecord {command.run_id} not found", + session.get(SampleRecord, command.sample_id), + f"SampleRecord {command.sample_id} not found", ) - run_record.status = RunStatus.EXECUTING + run_record.status = SampleStatus.EXECUTING run_record.started_at = utcnow() session.add(run_record) session.commit() ready_ids = await get_initial_ready_tasks( session, - command.run_id, + command.sample_id, command.definition_id, graph_repo=self._graph_repo, graph_lookup=graph_lookup, @@ -154,7 +158,7 @@ async def initialize(self, command: InitializeWorkflowCommand) -> InitializedWor root_count = sum(1 for t in all_tasks if t.parent_task_id is None) return InitializedWorkflow( - run_id=command.run_id, + sample_id=command.sample_id, definition_id=command.definition_id, benchmark_type=definition.benchmark_type, total_tasks=len(all_tasks), @@ -166,13 +170,15 @@ async def initialize(self, command: InitializeWorkflowCommand) -> InitializedWor def finalize(self, command: FinalizeWorkflowCommand) -> FinalizedWorkflowResult: """Aggregate evaluations and close the run.""" # reason: importing EvaluationService at module load creates a cycle via - # ergon_core.api -> experiments.service -> experiments.launch -> run_lifecycle. + # ergon_core.api -> experiments.service -> experiments.launch -> sample_lifecycle. from ergon_core.core.application.evaluation.service import EvaluationService with get_session() as session: evaluations = list( session.exec( - select(RunTaskEvaluation).where(RunTaskEvaluation.run_id == command.run_id) + select(SampleTaskEvaluation).where( + SampleTaskEvaluation.sample_id == command.sample_id + ) ).all() ) score_summary = EvaluationService.summarize_scores(evaluations) @@ -182,10 +188,10 @@ def finalize(self, command: FinalizeWorkflowCommand) -> FinalizedWorkflowResult: normalized_score=score_summary.normalized_score, ) run_record = require_not_none( - session.get(RunRecord, command.run_id), - f"RunRecord {command.run_id} not found", + session.get(SampleRecord, command.sample_id), + f"SampleRecord {command.sample_id} not found", ) - run_record.status = RunStatus.COMPLETED + run_record.status = SampleStatus.COMPLETED run_record.completed_at = completion.completed_at run_record.summary_json = { "final_score": completion.final_score, @@ -198,7 +204,7 @@ def finalize(self, command: FinalizeWorkflowCommand) -> FinalizedWorkflowResult: session.commit() return FinalizedWorkflowResult( - run_id=command.run_id, + sample_id=command.sample_id, final_score=score_summary.final_score, normalized_score=score_summary.normalized_score, evaluators_count=score_summary.evaluators_count, @@ -211,7 +217,7 @@ async def propagate(self, command: PropagateTaskCompletionCommand) -> Propagatio await self._graph_repo.update_node_status( session, - run_id=command.run_id, + sample_id=command.sample_id, task_id=task_id, new_status=graph_status.COMPLETED, meta=MutationMeta( @@ -222,20 +228,20 @@ async def propagate(self, command: PropagateTaskCompletionCommand) -> Propagatio ) newly_ready_task_ids = await on_task_completed_or_failed( session, - command.run_id, + command.sample_id, task_id, graph_status.COMPLETED, graph_repo=self._graph_repo, ) ready_descriptors = self._task_descriptors_for_nodes(session, newly_ready_task_ids) terminal = WorkflowTerminalState.NONE - if is_workflow_complete_v2(session, command.run_id): + if is_workflow_complete_v2(session, command.sample_id): terminal = WorkflowTerminalState.COMPLETED - elif is_workflow_failed_v2(session, command.run_id): + elif is_workflow_failed_v2(session, command.sample_id): terminal = WorkflowTerminalState.FAILED return PropagationResult( - run_id=command.run_id, + sample_id=command.sample_id, definition_id=command.definition_id, completed_task_id=command.task_id, ready_tasks=ready_descriptors, @@ -248,7 +254,7 @@ async def propagate_failure(self, command: PropagateTaskCompletionCommand) -> Pr task_id = command.task_id await self._graph_repo.update_node_status( session, - run_id=command.run_id, + sample_id=command.sample_id, task_id=task_id, new_status=graph_status.FAILED, meta=MutationMeta( @@ -259,39 +265,39 @@ async def propagate_failure(self, command: PropagateTaskCompletionCommand) -> Pr ) await on_task_completed_or_failed( session, - command.run_id, + command.sample_id, task_id, graph_status.FAILED, graph_repo=self._graph_repo, ) terminal = WorkflowTerminalState.NONE - if is_workflow_failed_v2(session, command.run_id): + if is_workflow_failed_v2(session, command.sample_id): terminal = WorkflowTerminalState.FAILED return PropagationResult( - run_id=command.run_id, + sample_id=command.sample_id, definition_id=command.definition_id, completed_task_id=command.task_id, workflow_terminal_state=terminal, ) - async def operator_unblock(self, *, run_id: UUID, task_id: UUID, reason: str) -> None: + async def operator_unblock(self, *, sample_id: UUID, task_id: UUID, reason: str) -> None: with get_session() as session: await self._graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, new_status=graph_status.PENDING, meta=MutationMeta(actor="operator:unblock", reason=reason), ) session.commit() - async def restart_node(self, *, run_id: UUID, task_id: UUID, reason: str) -> None: + async def restart_node(self, *, sample_id: UUID, task_id: UUID, reason: str) -> None: with get_session() as session: await self._graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, new_status=graph_status.PENDING, meta=MutationMeta(actor="operator:restart", reason=reason), @@ -305,7 +311,9 @@ def _task_descriptors_for_nodes( ) -> list[TaskDescriptor]: descriptors: list[TaskDescriptor] = [] for task_id in task_ids: - node = session.exec(select(RunGraphNode).where(RunGraphNode.task_id == task_id)).first() + node = session.exec( + select(SampleGraphNode).where(SampleGraphNode.task_id == task_id) + ).first() if node is not None: descriptors.append( TaskDescriptor( @@ -319,12 +327,12 @@ def list_tasks( self, session: Session, *, - run_id: UUID, + sample_id: UUID, parent_task_id: UUID | None = None, ) -> list[WorkflowTaskRef]: - stmt = select(RunGraphNode).where(RunGraphNode.run_id == run_id) + stmt = select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) if parent_task_id is not None: - stmt = stmt.where(RunGraphNode.parent_task_id == parent_task_id) + stmt = stmt.where(SampleGraphNode.parent_task_id == parent_task_id) nodes = list(session.exec(stmt).all()) nodes.sort(key=lambda node: (node.level, node.task_slug, str(node.task_id))) return [self._task_ref(node) for node in nodes] @@ -333,11 +341,13 @@ def get_task( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID | None = None, task_slug: str | None = None, ) -> WorkflowTaskRef: - node = self._resolve_node(session, run_id=run_id, task_id=task_id, task_slug=task_slug) + node = self._resolve_node( + session, sample_id=sample_id, task_id=task_id, task_slug=task_slug + ) return self._task_ref(node) def get_latest_execution( @@ -345,32 +355,32 @@ def get_latest_execution( session: Session, *, task_id: UUID, - ) -> RunTaskExecution | None: + ) -> SampleTaskAttempt | None: return self._task_execution_repo.latest_for_node(session, task_id) def list_dependencies( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, direction: Literal["upstream", "downstream", "both"], ) -> list[WorkflowDependencyRef]: clauses = [] if direction in {"upstream", "both"}: - clauses.append(RunGraphEdge.target_task_id == task_id) + clauses.append(SampleGraphEdge.target_task_id == task_id) if direction in {"downstream", "both"}: - clauses.append(RunGraphEdge.source_task_id == task_id) + clauses.append(SampleGraphEdge.source_task_id == task_id) if not clauses: raise ValueError(f"unsupported dependency direction: {direction}") - stmt = select(RunGraphEdge).where(RunGraphEdge.run_id == run_id) + stmt = select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) if len(clauses) == 1: stmt = stmt.where(clauses[0]) else: stmt = stmt.where(clauses[0] | clauses[1]) edges = list(session.exec(stmt).all()) - nodes = self._nodes_by_id(session, run_id) + nodes = self._nodes_by_id(session, sample_id) return [ WorkflowDependencyRef( edge_id=edge.id, @@ -385,7 +395,7 @@ def list_resources( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, scope: ResourceScope, kind: str | None = None, @@ -394,16 +404,16 @@ def list_resources( ) -> list[WorkflowResourceRef]: execution_ids = self._execution_ids_for_scope( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, scope=scope, max_depth=max_depth, ) - stmt = select(RunResource).where(RunResource.run_id == run_id) + stmt = select(SampleResource).where(SampleResource.sample_id == sample_id) if execution_ids is not None: - stmt = stmt.where(col(RunResource.task_execution_id).in_(execution_ids)) + stmt = stmt.where(col(SampleResource.task_execution_id).in_(execution_ids)) if kind is not None: - stmt = stmt.where(RunResource.kind == kind) + stmt = stmt.where(SampleResource.kind == kind) resources = list(session.exec(stmt).all()) resources.sort(key=lambda resource: (resource.created_at, resource.id), reverse=True) if limit >= 0: @@ -414,11 +424,11 @@ def read_resource_bytes( self, session: Session, *, - run_id: UUID, + sample_id: UUID, resource_id: UUID, max_bytes: int, ) -> bytes: - resource = self._resource_in_run(session, run_id=run_id, resource_id=resource_id) + resource = self._resource_in_run(session, sample_id=sample_id, resource_id=resource_id) with open(resource.file_path, "rb") as handle: return handle.read(max_bytes) @@ -426,10 +436,10 @@ def get_resource_location( self, session: Session, *, - run_id: UUID, + sample_id: UUID, resource_id: UUID, ) -> WorkflowResourceLocationRef: - resource = self._resource_in_run(session, run_id=run_id, resource_id=resource_id) + resource = self._resource_in_run(session, sample_id=sample_id, resource_id=resource_id) producer = self._producer_node_for_resource(session, resource) copied_name = self._copy_name(resource.name) default_path = self._sandbox_destination( @@ -448,18 +458,18 @@ def get_task_workspace( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> WorkflowTaskWorkspaceRef: - node = self._resolve_node(session, run_id=run_id, task_id=task_id, task_slug=None) + node = self._resolve_node(session, sample_id=sample_id, task_id=task_id, task_slug=None) latest = self.get_latest_execution(session, task_id=task_id) own_resources: list[WorkflowResourceRef] = [] if latest is not None: own_rows = list( session.exec( - select(RunResource) - .where(RunResource.run_id == run_id) - .where(RunResource.task_execution_id == latest.id), + select(SampleResource) + .where(SampleResource.sample_id == sample_id) + .where(SampleResource.task_execution_id == latest.id), ).all(), ) own_rows.sort(key=lambda resource: (resource.created_at, resource.id), reverse=True) @@ -470,7 +480,7 @@ def get_task_workspace( own_resources=own_resources, input_resources=self.list_resources( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, scope="input", ), @@ -480,11 +490,13 @@ def get_task_blockers( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> list[WorkflowBlockerRef]: - task = self.get_task(session, run_id=run_id, task_id=task_id) - deps = self.list_dependencies(session, run_id=run_id, task_id=task_id, direction="upstream") + task = self.get_task(session, sample_id=sample_id, task_id=task_id) + deps = self.list_dependencies( + session, sample_id=sample_id, task_id=task_id, direction="upstream" + ) blockers: list[WorkflowBlockerRef] = [] pending = [dep.source.task_slug for dep in deps if dep.edge_status != "satisfied"] if pending: @@ -502,11 +514,11 @@ def get_next_actions( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, manager_capable: bool, ) -> list[WorkflowNextActionRef]: - task = self.get_task(session, run_id=run_id, task_id=task_id) + task = self.get_task(session, sample_id=sample_id, task_id=task_id) commands = [ "inspect task-workspace", "inspect resource-list --scope input", @@ -527,20 +539,20 @@ async def add_edge( self, session: Session, *, - run_id: UUID, + sample_id: UUID, source_task_slug: str, target_task_slug: str, dry_run: bool, ) -> WorkflowMutationRef: source = self._resolve_node( session, - run_id=run_id, + sample_id=sample_id, task_id=None, task_slug=source_task_slug, ) target = self._resolve_node( session, - run_id=run_id, + sample_id=sample_id, task_id=None, task_slug=target_task_slug, ) @@ -560,7 +572,7 @@ async def add_edge( created = await self._graph_repo.add_edge( session, - run_id, + sample_id, source_task_id=source.task_id, target_task_id=target.task_id, status="pending", @@ -570,7 +582,7 @@ async def add_edge( return WorkflowMutationRef( action="add-edge", dry_run=False, - edge=self._dependency_ref_from_graph(session, run_id, created), + edge=self._dependency_ref_from_graph(session, sample_id, created), message=f"Added dependency {source_task_slug} -> {target_task_slug}", ) @@ -578,12 +590,12 @@ async def update_task_description( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_slug: str, description: str, dry_run: bool, ) -> WorkflowMutationRef: - node = self._resolve_node(session, run_id=run_id, task_id=None, task_slug=task_slug) + node = self._resolve_node(session, sample_id=sample_id, task_id=None, task_slug=task_slug) if dry_run: return WorkflowMutationRef( action="update-task-description", @@ -594,7 +606,7 @@ async def update_task_description( updated = await self._graph_repo.update_node_field( session, - run_id=run_id, + sample_id=sample_id, task_id=node.task_id, field="description", value=description, @@ -612,7 +624,7 @@ async def materialize_resource( # slopcop: ignore[max-function-params] -- mirro self, session: Session, *, - run_id: UUID, + sample_id: UUID, current_task_id: UUID, current_execution_id: UUID, sandbox_task_key: UUID, @@ -621,7 +633,7 @@ async def materialize_resource( # slopcop: ignore[max-function-params] -- mirro destination: str | None, dry_run: bool, ) -> WorkflowMaterializedResourceRef: - source = self._resource_in_run(session, run_id=run_id, resource_id=resource_id) + source = self._resource_in_run(session, sample_id=sample_id, resource_id=resource_id) producer = self._producer_node_for_resource(session, source) copied_name = self._copy_name(source.name) sandbox_path = self._sandbox_destination( @@ -647,10 +659,10 @@ async def materialize_resource( # slopcop: ignore[max-function-params] -- mirro manager = self._sandbox_manager_factory(benchmark_type) await manager.upload_file(sandbox_task_key, source.file_path, sandbox_path) - copy = RunResource( - run_id=run_id, + copy = SampleResource( + sample_id=sample_id, task_execution_id=current_execution_id, - kind=RunResourceKind.IMPORT.value, + kind=SampleResourceKind.IMPORT.value, name=result.copied_name, mime_type=source.mime_type, file_path=source.file_path, @@ -679,7 +691,7 @@ def _sandbox_manager_for(benchmark_type: str) -> SandboxMaterializer: return DefaultSandboxManager() @staticmethod - def _task_ref(node: RunGraphNode) -> WorkflowTaskRef: + def _task_ref(node: SampleGraphNode) -> WorkflowTaskRef: return WorkflowTaskRef( task_id=node.task_id, task_slug=node.task_slug, @@ -705,10 +717,10 @@ def _task_ref_from_graph(node: GraphNodeDto) -> WorkflowTaskRef: def _dependency_ref_from_graph( self, session: Session, - run_id: UUID, + sample_id: UUID, edge: GraphEdgeDto, ) -> WorkflowDependencyRef: - nodes = self._nodes_by_id(session, run_id) + nodes = self._nodes_by_id(session, sample_id) return WorkflowDependencyRef( edge_id=edge.id, edge_status=edge.status, @@ -720,11 +732,11 @@ def _dependency_ref_from_graph( def _meta(action: str) -> MutationMeta: return MutationMeta(actor="workflow-cli", reason=action) - def _resource_ref(self, session: Session, resource: RunResource) -> WorkflowResourceRef: + def _resource_ref(self, session: Session, resource: SampleResource) -> WorkflowResourceRef: producer = self._producer_node_for_resource(session, resource) return WorkflowResourceRef( resource_id=resource.id, - run_id=resource.run_id, + sample_id=resource.sample_id, task_execution_id=resource.task_execution_id, task_id=producer.task_id if producer is not None else None, task_slug=producer.task_slug if producer is not None else None, @@ -739,7 +751,7 @@ def _resource_ref(self, session: Session, resource: RunResource) -> WorkflowReso ) @staticmethod - def _execution_ref(execution: RunTaskExecution) -> WorkflowExecutionRef: + def _execution_ref(execution: SampleTaskAttempt) -> WorkflowExecutionRef: return WorkflowExecutionRef( execution_id=execution.id, status=execution.status, @@ -748,9 +760,9 @@ def _execution_ref(execution: RunTaskExecution) -> WorkflowExecutionRef: ) @staticmethod - def _resource_in_run(session: Session, *, run_id: UUID, resource_id: UUID) -> RunResource: - resource = session.get(RunResource, resource_id) - if resource is None or resource.run_id != run_id: + def _resource_in_run(session: Session, *, sample_id: UUID, resource_id: UUID) -> SampleResource: + resource = session.get(SampleResource, resource_id) + if resource is None or resource.sample_id != sample_id: raise ValueError(f"resource {resource_id} is not visible in current run") return resource @@ -758,32 +770,36 @@ def _resource_in_run(session: Session, *, run_id: UUID, resource_id: UUID) -> Ru def _resolve_node( session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID | None, task_slug: str | None, - ) -> RunGraphNode: + ) -> SampleGraphNode: if task_id is None and task_slug is None: raise ValueError("task_id or task_slug is required") - stmt = select(RunGraphNode).where(RunGraphNode.run_id == run_id) + stmt = select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) if task_id is not None: - stmt = stmt.where(RunGraphNode.task_id == task_id) + stmt = stmt.where(SampleGraphNode.task_id == task_id) if task_slug is not None: - stmt = stmt.where(RunGraphNode.task_slug == task_slug) + stmt = stmt.where(SampleGraphNode.task_slug == task_slug) rows = list(session.exec(stmt).all()) if len(rows) != 1: raise ValueError(f"expected exactly one task, got {len(rows)}") return rows[0] @staticmethod - def _nodes_by_id(session: Session, run_id: UUID) -> dict[UUID, RunGraphNode]: - nodes = list(session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all()) + def _nodes_by_id(session: Session, sample_id: UUID) -> dict[UUID, SampleGraphNode]: + nodes = list( + session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all() + ) return {node.task_id: node for node in nodes} def _execution_ids_for_scope( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, scope: ResourceScope, max_depth: int, @@ -792,7 +808,7 @@ def _execution_ids_for_scope( return None task_ids = self._task_ids_for_scope( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, scope=scope, max_depth=max_depth, @@ -808,7 +824,7 @@ def _task_ids_for_scope( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, scope: ResourceScope, max_depth: int, @@ -817,23 +833,23 @@ def _task_ids_for_scope( return {task_id} if scope in {"input", "upstream"}: edges = session.exec( - select(RunGraphEdge).where( - RunGraphEdge.run_id == run_id, - RunGraphEdge.target_task_id == task_id, + select(SampleGraphEdge).where( + SampleGraphEdge.sample_id == sample_id, + SampleGraphEdge.target_task_id == task_id, ) ).all() return {edge.source_task_id for edge in edges} if scope == "children": children = session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.parent_task_id == task_id, + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.parent_task_id == task_id, ) ).all() return {child.task_id for child in children} if scope == "descendants": return self._descendant_ids( - session, run_id=run_id, task_id=task_id, max_depth=max_depth + session, sample_id=sample_id, task_id=task_id, max_depth=max_depth ) raise ValueError(f"unsupported resource scope: {scope}") @@ -841,23 +857,25 @@ def _descendant_ids( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, max_depth: int, ) -> set[UUID]: - return descendant_ids(session, run_id=run_id, root_task_id=task_id, max_depth=max_depth) + return descendant_ids( + session, sample_id=sample_id, root_task_id=task_id, max_depth=max_depth + ) @staticmethod def _producer_node_for_resource( session: Session, - resource: RunResource, - ) -> RunGraphNode | None: + resource: SampleResource, + ) -> SampleGraphNode | None: if resource.task_execution_id is None: return None - execution = session.get(RunTaskExecution, resource.task_execution_id) + execution = session.get(SampleTaskAttempt, resource.task_execution_id) if execution is None: return None - return session.get(RunGraphNode, (execution.run_id, execution.task_id)) + return session.get(SampleGraphNode, (execution.sample_id, execution.task_id)) @staticmethod def _copy_name(name: str) -> str: diff --git a/ergon_core/ergon_core/core/application/runtime/run_records.py b/ergon_core/ergon_core/core/application/runtime/sample_records.py similarity index 52% rename from ergon_core/ergon_core/core/application/runtime/run_records.py rename to ergon_core/ergon_core/core/application/runtime/sample_records.py index 9d6419bfa..8da422b18 100644 --- a/ergon_core/ergon_core/core/application/runtime/run_records.py +++ b/ergon_core/ergon_core/core/application/runtime/sample_records.py @@ -1,15 +1,15 @@ -"""Run creation, dispatch, and cancellation via Inngest.""" +"""Sample creation, dispatch, and cancellation via Inngest.""" import logging from uuid import UUID import inngest from ergon_core.core.application.experiments.models import DefinitionHandle -from ergon_core.core.application.events import RunCancelledEvent, RunCleanupEvent +from ergon_core.core.application.events import SampleCancelledEvent, SampleCleanupEvent from ergon_core.core.shared.json_types import JsonObject from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import TERMINAL_RUN_STATUSES, RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import TERMINAL_SAMPLE_STATUSES, SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from sqlmodel import select from ergon_core.core.infrastructure.inngest.client import inngest_client from ergon_core.core.shared.settings import settings @@ -19,7 +19,7 @@ def _checkpoint_metadata() -> JsonObject: - """Checkpoint context for ``RunRecord.summary_json`` (eval watcher / checkpoint subprocess). + """Checkpoint context for ``SampleRecord.summary_json`` (eval watcher / checkpoint subprocess). Values come from ``Settings`` (``.env`` + process env), including ``ERGON_CHECKPOINT_*`` set by the eval runner when spawning evaluation. @@ -32,7 +32,7 @@ def _checkpoint_metadata() -> JsonObject: } -def create_run( # slopcop: ignore[max-function-params] -- service boundary mirrors RunRecord provenance fields +def create_definition_backed_sample( # slopcop: ignore[max-function-params] -- service boundary mirrors SampleRecord provenance fields definition: DefinitionHandle, *, definition_id: UUID, @@ -45,9 +45,9 @@ def create_run( # slopcop: ignore[max-function-params] -- service boundary mirr assignment_json: JsonObject | None = None, seed: int | None = None, experiment: str | None = None, -) -> RunRecord: +) -> SampleRecord: with get_session() as session: - run = RunRecord( + sample = SampleRecord( definition_id=definition_id, benchmark_type=definition.benchmark_type, instance_key=instance_key, @@ -59,59 +59,64 @@ def create_run( # slopcop: ignore[max-function-params] -- service boundary mirr assignment_json=assignment_json or {}, seed=seed, experiment=experiment, - status=RunStatus.PENDING, + status=SampleStatus.PENDING, created_at=utcnow(), summary_json=_checkpoint_metadata(), ) - session.add(run) + session.add(sample) session.commit() - session.refresh(run) - return run + session.refresh(sample) + return sample -def cancel_run(run_id: UUID) -> RunRecord: - """Cancel a run: mark CANCELLED in PG, kill Inngest functions, trigger cleanup.""" +def cancel_sample(sample_id: UUID) -> SampleRecord: + """Cancel a sample: mark CANCELLED in PG, kill Inngest functions, trigger cleanup.""" with get_session() as session: - run = session.get(RunRecord, run_id) - if run is None: - raise ValueError(f"Run {run_id} not found") - if run.status in TERMINAL_RUN_STATUSES: - raise ValueError(f"Run {run_id} is already in terminal state: {run.status}") - - run.status = RunStatus.CANCELLED - run.completed_at = utcnow() - session.add(run) + sample = session.get(SampleRecord, sample_id) + if sample is None: + raise ValueError(f"Sample {sample_id} not found") + if sample.status in TERMINAL_SAMPLE_STATUSES: + raise ValueError(f"Sample {sample_id} is already in terminal state: {sample.status}") + + sample.status = SampleStatus.CANCELLED + sample.completed_at = utcnow() + session.add(sample) session.commit() - session.refresh(run) + session.refresh(sample) inngest_client.send_sync( inngest.Event( - name=RunCancelledEvent.name, - data=RunCancelledEvent(run_id=run_id).model_dump(mode="json"), + name=SampleCancelledEvent.name, + data=SampleCancelledEvent(sample_id=sample_id).model_dump(mode="json"), ) ) inngest_client.send_sync( inngest.Event( - name=RunCleanupEvent.name, - data=RunCleanupEvent( - run_id=run_id, + name=SampleCleanupEvent.name, + data=SampleCleanupEvent( + sample_id=sample_id, status="cancelled", ).model_dump(mode="json"), ) ) - logger.info("Cancelled run %s and dispatched cleanup", run_id) - return run + logger.info("Cancelled sample %s and dispatched cleanup", sample_id) + return sample -def latest_run_for_definition(definition_id: UUID) -> RunRecord | None: - """Most-recent ``RunRecord`` for a given definition, or None.""" +def latest_sample_for_definition(definition_id: UUID) -> SampleRecord | None: + """Most-recent ``SampleRecord`` for a given definition, or None.""" with get_session() as session: stmt = ( - select(RunRecord) - .where(RunRecord.definition_id == definition_id) - .order_by(RunRecord.created_at.desc()) + select(SampleRecord) + .where(SampleRecord.definition_id == definition_id) + .order_by(SampleRecord.created_at.desc()) .limit(1) ) return session.exec(stmt).first() + + +create_run = create_definition_backed_sample +cancel_run = cancel_sample +latest_run_for_definition = latest_sample_for_definition diff --git a/ergon_core/ergon_core/core/application/runtime/status.py b/ergon_core/ergon_core/core/application/runtime/status.py index c1088931f..ecf5166ea 100644 --- a/ergon_core/ergon_core/core/application/runtime/status.py +++ b/ergon_core/ergon_core/core/application/runtime/status.py @@ -1,4 +1,4 @@ -"""Conventional status values for RunGraphNode and RunGraphEdge. +"""Conventional status values for SampleGraphNode and SampleGraphEdge. The graph layer accepts any string at the DB level -- these are not enforced by the schema. They are the values used by the core runtime, diff --git a/ergon_core/ergon_core/core/application/runtime/task_cleanup.py b/ergon_core/ergon_core/core/application/runtime/task_cleanup.py index 9a2e9e08a..0ff7e8071 100644 --- a/ergon_core/ergon_core/core/application/runtime/task_cleanup.py +++ b/ergon_core/ergon_core/core/application/runtime/task_cleanup.py @@ -9,7 +9,7 @@ from uuid import UUID from ergon_core.core.persistence.shared.enums import TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunTaskExecution +from ergon_core.core.persistence.telemetry.models import SampleTaskAttempt from ergon_core.core.application.runtime.task_models import CleanupResult from sqlmodel import Session, select @@ -28,14 +28,14 @@ def cleanup( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, execution_id: UUID | None, ) -> CleanupResult: """Mark execution CANCELLED and release resources. Idempotent.""" if execution_id is None: return CleanupResult( - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=None, sandbox_id=None, @@ -55,7 +55,7 @@ def cleanup( sandbox_id, ) return CleanupResult( - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, sandbox_id=sandbox_id, @@ -63,12 +63,12 @@ def cleanup( execution_row_updated=execution_updated, ) - def _execution(self, session: Session, execution_id: UUID) -> RunTaskExecution | None: + def _execution(self, session: Session, execution_id: UUID) -> SampleTaskAttempt | None: return session.exec( - select(RunTaskExecution).where(RunTaskExecution.id == execution_id) + select(SampleTaskAttempt).where(SampleTaskAttempt.id == execution_id) ).first() - def _mark_execution_cancelled(self, session: Session, exe: RunTaskExecution | None) -> bool: + def _mark_execution_cancelled(self, session: Session, exe: SampleTaskAttempt | None) -> bool: """Idempotent: skip if already terminal.""" if exe is None or exe.status in { TaskExecutionStatus.COMPLETED, diff --git a/ergon_core/ergon_core/core/application/runtime/task_errors.py b/ergon_core/ergon_core/core/application/runtime/task_errors.py index 74abc1851..b852ca16d 100644 --- a/ergon_core/ergon_core/core/application/runtime/task_errors.py +++ b/ergon_core/ergon_core/core/application/runtime/task_errors.py @@ -54,17 +54,17 @@ def __init__(self, task_id: UUID, current_status: str) -> None: self.current_status = current_status -class RunRecordMissingError(DelegationError): - """Raised when a service is asked to mutate a run that has no RunRecord. +class SampleRecordMissingError(DelegationError): + """Raised when a service is asked to mutate a run that has no SampleRecord. - Every run must have a RunRecord (with ``experiment_definition_id``) + Every run must have a SampleRecord (with ``experiment_definition_id``) before any task/graph service is invoked on it. This is enforced as a hard invariant so missing fixtures in tests surface as a loud failure. """ - def __init__(self, run_id: UUID) -> None: + def __init__(self, sample_id: UUID) -> None: super().__init__( - f"RunRecord missing for run_id={run_id}; seed a RunRecord before " + f"SampleRecord missing for sample_id={sample_id}; seed a SampleRecord before " "invoking TaskManagementService.", ) - self.run_id = run_id + self.sample_id = sample_id diff --git a/ergon_core/ergon_core/core/application/runtime/task_execution.py b/ergon_core/ergon_core/core/application/runtime/task_execution.py index cc267ead1..56034dccb 100644 --- a/ergon_core/ergon_core/core/application/runtime/task_execution.py +++ b/ergon_core/ergon_core/core/application/runtime/task_execution.py @@ -9,13 +9,13 @@ ExperimentDefinitionWorker, ) from ergon_core.core.application.runtime import status as graph_status -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.shared.enums import TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord, RunTaskExecution +from ergon_core.core.persistence.telemetry.models import SampleRecord, SampleTaskAttempt from ergon_core.core.infrastructure.inngest.errors import ConfigurationError from ergon_core.api.worker.results import WorkerOutput -from ergon_core.core.application.runtime.models import MutationMeta, RunGraphNodeView +from ergon_core.core.application.runtime.models import MutationMeta, SampleGraphNodeView from ergon_core.core.application.runtime.graph_repository import RuntimeGraphRepository from ergon_core.core.application.runtime.orchestration import ( FailTaskExecutionCommand, @@ -38,7 +38,7 @@ async def _emit_task_status( - run_id: UUID, + sample_id: UUID, task_id: UUID | None, task_slug: str, new_status: str, @@ -52,7 +52,7 @@ async def _emit_task_status( try: await get_dashboard_event_publisher().publish( DashboardTaskStatusChangedEvent( - run_id=run_id, + sample_id=sample_id, task_id=task_id, task_name=task_slug, new_status=new_status, @@ -88,14 +88,14 @@ async def load_task_view( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str | None = None, - ) -> RunGraphNodeView: + ) -> SampleGraphNodeView: """Load the object-bound runtime task view by public task_id.""" return await self._graph_repo.node( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=sandbox_id, ) @@ -145,12 +145,14 @@ async def _prepare_run_node( ) -> PreparedTaskExecution: lookup_id = command.task_id with get_session() as session: - view = await self._graph_repo.node(session, run_id=command.run_id, task_id=lookup_id) - node = session.get(RunGraphNode, (command.run_id, view.task_id)) + view = await self._graph_repo.node( + session, sample_id=command.sample_id, task_id=lookup_id + ) + node = session.get(SampleGraphNode, (command.sample_id, view.task_id)) if node is None: raise ConfigurationError( - f"RunGraphNode {view.task_id} not found", - run_id=command.run_id, + f"SampleGraphNode {view.task_id} not found", + sample_id=command.sample_id, task_id=lookup_id, ) definition = require_not_none( @@ -162,16 +164,16 @@ async def _prepare_run_node( worker_type, model_target, definition_worker_id = self._resolve_worker_config( session, definition_id=command.definition_id, - run_id=command.run_id, + sample_id=command.sample_id, assigned_worker_slug=assigned_worker_slug, ) - execution = RunTaskExecution( - run_id=command.run_id, + execution = SampleTaskAttempt( + sample_id=command.sample_id, task_id=view.task_id, definition_worker_id=definition_worker_id, attempt_number=self._task_execution_repo.next_attempt_for_node( - session, command.run_id, view.task_id + session, command.sample_id, view.task_id ), status=TaskExecutionStatus.RUNNING, started_at=utcnow(), @@ -188,7 +190,7 @@ async def _prepare_run_node( execution_id = execution.id await self._graph_repo.update_node_status( session, - run_id=command.run_id, + sample_id=command.sample_id, task_id=view.task_id, new_status=graph_status.RUNNING, meta=MutationMeta( @@ -199,7 +201,7 @@ async def _prepare_run_node( session.commit() await _emit_task_status( - run_id=command.run_id, + sample_id=command.sample_id, task_id=view.task_id, task_slug=view.task.task_slug, new_status=graph_status.RUNNING, @@ -208,7 +210,7 @@ async def _prepare_run_node( worker_slug=assigned_worker_slug, ) return PreparedTaskExecution( - run_id=command.run_id, + sample_id=command.sample_id, definition_id=command.definition_id, task_id=view.task_id, task_slug=view.task.task_slug, @@ -225,7 +227,7 @@ def _resolve_worker_config( session: Session, *, definition_id: UUID, - run_id: UUID, + sample_id: UUID, assigned_worker_slug: str | None, ) -> tuple[str | None, str | None, UUID | None]: """Resolve (worker_type, model_target, definition_worker_id) for a @@ -248,15 +250,15 @@ def _resolve_worker_config( if worker_row is not None: return worker_row.worker_type, worker_row.model_target, worker_row.id # No matching binding — use the run-level default model_target. - run = session.get(RunRecord, run_id) + run = session.get(SampleRecord, sample_id) model_target = run.model_target if run is not None else None return assigned_worker_slug, model_target, None async def finalize_success(self, command: FinalizeTaskExecutionCommand) -> None: with get_session() as session: execution = require_not_none( - session.get(RunTaskExecution, command.execution_id), - f"RunTaskExecution {command.execution_id} not found", + session.get(SampleTaskAttempt, command.execution_id), + f"SampleTaskAttempt {command.execution_id} not found", ) execution.status = TaskExecutionStatus.COMPLETED execution.completed_at = utcnow() @@ -269,7 +271,7 @@ async def finalize_success(self, command: FinalizeTaskExecutionCommand) -> None: session.commit() await _emit_task_status( - run_id=execution.run_id, + sample_id=execution.sample_id, task_id=execution.task_id, task_slug=str(execution.task_id or ""), new_status=graph_status.COMPLETED, @@ -279,8 +281,8 @@ async def finalize_success(self, command: FinalizeTaskExecutionCommand) -> None: async def finalize_failure(self, command: FailTaskExecutionCommand) -> None: with get_session() as session: execution = require_not_none( - session.get(RunTaskExecution, command.execution_id), - f"RunTaskExecution {command.execution_id} not found", + session.get(SampleTaskAttempt, command.execution_id), + f"SampleTaskAttempt {command.execution_id} not found", ) execution.status = TaskExecutionStatus.FAILED execution.completed_at = utcnow() @@ -291,7 +293,7 @@ async def finalize_failure(self, command: FailTaskExecutionCommand) -> None: if execution.task_id is not None: await mark_task_failed_by_node( session, - command.run_id, + command.sample_id, execution.task_id, command.error_message, execution_id=command.execution_id, @@ -300,7 +302,7 @@ async def finalize_failure(self, command: FailTaskExecutionCommand) -> None: session.commit() await _emit_task_status( - run_id=command.run_id, + sample_id=command.sample_id, task_id=execution.task_id, task_slug=str(execution.task_id or ""), new_status=graph_status.FAILED, diff --git a/ergon_core/ergon_core/core/application/runtime/task_execution_repository.py b/ergon_core/ergon_core/core/application/runtime/task_execution_repository.py index f6dceccb0..5288eb373 100644 --- a/ergon_core/ergon_core/core/application/runtime/task_execution_repository.py +++ b/ergon_core/ergon_core/core/application/runtime/task_execution_repository.py @@ -3,8 +3,8 @@ from uuid import UUID from ergon_core.api.worker.results import WorkerOutput -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.telemetry.models import RunTaskExecution +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.telemetry.models import SampleTaskAttempt from sqlalchemy import func, update from sqlmodel import Session, col, select @@ -20,13 +20,13 @@ def __init__(self, *, execution_id: UUID) -> None: class TaskExecutionRepository: """Domain queries over task execution rows.""" - def latest_for_node(self, session: Session, task_id: UUID) -> RunTaskExecution | None: + def latest_for_node(self, session: Session, task_id: UUID) -> SampleTaskAttempt | None: stmt = ( - select(RunTaskExecution) - .where(RunTaskExecution.task_id == task_id) + select(SampleTaskAttempt) + .where(SampleTaskAttempt.task_id == task_id) .order_by( - col(RunTaskExecution.attempt_number).desc(), - col(RunTaskExecution.started_at).desc(), + col(SampleTaskAttempt.attempt_number).desc(), + col(SampleTaskAttempt.started_at).desc(), ) .limit(1) ) @@ -36,23 +36,23 @@ def list_children_of_execution( self, session: Session, parent_execution_id: UUID, - ) -> list[RunTaskExecution]: - parent = session.get(RunTaskExecution, parent_execution_id) + ) -> list[SampleTaskAttempt]: + parent = session.get(SampleTaskAttempt, parent_execution_id) if parent is None: return [] - child_task_ids_stmt = select(RunGraphNode.task_id).where( - RunGraphNode.parent_task_id == parent.task_id + child_task_ids_stmt = select(SampleGraphNode.task_id).where( + SampleGraphNode.parent_task_id == parent.task_id ) - stmt = select(RunTaskExecution).where( - col(RunTaskExecution.task_id).in_(child_task_ids_stmt) + stmt = select(SampleTaskAttempt).where( + col(SampleTaskAttempt.task_id).in_(child_task_ids_stmt) ) return list(session.exec(stmt).all()) - def next_attempt_for_node(self, session: Session, run_id: UUID, task_id: UUID) -> int: + def next_attempt_for_node(self, session: Session, sample_id: UUID, task_id: UUID) -> int: count = session.exec( - select(func.count(RunTaskExecution.id)).where( - RunTaskExecution.run_id == run_id, - RunTaskExecution.task_id == task_id, + select(func.count(SampleTaskAttempt.id)).where( + SampleTaskAttempt.sample_id == sample_id, + SampleTaskAttempt.task_id == task_id, ) ).one() return count + 1 @@ -72,8 +72,8 @@ async def set_sandbox_id( """ session.exec( - update(RunTaskExecution) - .where(RunTaskExecution.id == execution_id) + update(SampleTaskAttempt) + .where(SampleTaskAttempt.id == execution_id) .values(sandbox_id=sandbox_id) ) @@ -83,7 +83,7 @@ class WorkerOutputRepository: The orchestrator (``worker_execute``) persists the terminal worker output before fanning out per-evaluator invocations; each eval worker reloads it - from a thin id-only payload. Storage lives on ``RunTaskExecution`` — + from a thin id-only payload. Storage lives on ``SampleTaskAttempt`` — keeping the execution row authoritative for everything the eval workers need keeps the run-tier read boundary one query wide. """ @@ -98,8 +98,8 @@ async def persist( """Write ``output`` onto the execution row. Caller commits.""" session.exec( - update(RunTaskExecution) - .where(RunTaskExecution.id == execution_id) + update(SampleTaskAttempt) + .where(SampleTaskAttempt.id == execution_id) .values(worker_output_json=output.model_dump(mode="json")) ) @@ -116,7 +116,7 @@ async def load( worker fails loudly rather than silently evaluating a missing output. """ - row = session.get(RunTaskExecution, execution_id) + row = session.get(SampleTaskAttempt, execution_id) if row is None or row.worker_output_json is None: raise WorkerOutputNotFound(execution_id=execution_id) return WorkerOutput.model_validate(row.worker_output_json) diff --git a/ergon_core/ergon_core/core/application/runtime/task_inspection.py b/ergon_core/ergon_core/core/application/runtime/task_inspection.py index aef868924..237ffe2f7 100644 --- a/ergon_core/ergon_core/core/application/runtime/task_inspection.py +++ b/ergon_core/ergon_core/core/application/runtime/task_inspection.py @@ -7,7 +7,7 @@ import logging from uuid import UUID -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode from ergon_core.core.application.runtime.graph_traversal import descendants from ergon_core.core.application.runtime.status import COMPLETED, FAILED from ergon_core.core.application.runtime.graph_repository import RuntimeGraphRepository @@ -36,7 +36,7 @@ def list_subtasks( self, session: Session, *, - run_id: UUID, + sample_id: UUID, parent_task_id: UUID, ) -> list[SubtaskInfo]: """Direct children of parent_task_id, ordered by task_slug. @@ -45,12 +45,12 @@ def list_subtasks( position across turns without task_id confusion. """ nodes = session.exec( - select(RunGraphNode) + select(SampleGraphNode) .where( - RunGraphNode.run_id == run_id, - RunGraphNode.parent_task_id == parent_task_id, + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.parent_task_id == parent_task_id, ) - .order_by(RunGraphNode.task_slug, RunGraphNode.task_id) + .order_by(SampleGraphNode.task_slug, SampleGraphNode.task_id) ).all() return [self._hydrate(session, n) for n in nodes] @@ -58,14 +58,14 @@ def get_subtask( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> SubtaskInfo: """Single subtask snapshot by task_id.""" node = session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.task_id == task_id, + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.task_id == task_id, ) ).one() return self._hydrate(session, node) @@ -73,22 +73,22 @@ def get_subtask( async def descendant_ids( self, *, - run_id: UUID, + sample_id: UUID, root_task_id: UUID, ) -> frozenset[UUID]: """Return all task_ids reachable as children/grandchildren of root_task_id.""" with get_session() as session: - rows = descendants(session, run_id=run_id, root_task_id=root_task_id) + rows = descendants(session, sample_id=sample_id, root_task_id=root_task_id) # Collect IDs inside the session scope to avoid DetachedInstanceError. return frozenset(row.task_id for row in rows) - def _hydrate(self, session: Session, node: RunGraphNode) -> SubtaskInfo: - """Build a SubtaskInfo from a RunGraphNode, attaching deps and output/error.""" + def _hydrate(self, session: Session, node: SampleGraphNode) -> SubtaskInfo: + """Build a SubtaskInfo from a SampleGraphNode, attaching deps and output/error.""" deps = session.exec( - select(RunGraphEdge.source_task_id).where( - RunGraphEdge.target_task_id == node.task_id, - RunGraphEdge.run_id == node.run_id, + select(SampleGraphEdge.source_task_id).where( + SampleGraphEdge.target_task_id == node.task_id, + SampleGraphEdge.sample_id == node.sample_id, ) ).all() diff --git a/ergon_core/ergon_core/core/application/runtime/task_management.py b/ergon_core/ergon_core/core/application/runtime/task_management.py index 405f9c9f9..334884db4 100644 --- a/ergon_core/ergon_core/core/application/runtime/task_management.py +++ b/ergon_core/ergon_core/core/application/runtime/task_management.py @@ -20,7 +20,7 @@ from ergon_core.api.worker.results import SpawnedTaskHandle from ergon_core.core.application.events.service import get_dashboard_event_publisher from ergon_core.core.application.ports import DashboardEventPublisher -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.application.runtime.status import ( BLOCKED, CANCELLED, @@ -35,7 +35,7 @@ RuntimeEventDispatcher, TaskReadyDispatcher, ) -from ergon_core.core.application.runtime.run_identity import definition_id_for_run +from ergon_core.core.application.runtime.sample_identity import definition_id_for_run from ergon_core.core.application.runtime.task_errors import ( TaskAlreadyTerminalError, TaskNotTerminalError, @@ -60,7 +60,7 @@ RestartTaskResult, ) from ergon_core.core.application.runtime.task_execution_repository import TaskExecutionRepository -from ergon_core.core.persistence.graph.models import RunGraphMutation +from ergon_core.core.persistence.graph.models import SampleGraphMutation from ergon_core.core.views.dashboard_events.graph_mutations import ( dashboard_graph_mutation_event_from_row, ) @@ -72,10 +72,10 @@ class _LegacyDashboardGraphMutationEmitter(Protocol): - def graph_mutation(self, row: RunGraphMutation) -> Awaitable[None] | None: ... + def graph_mutation(self, row: SampleGraphMutation) -> Awaitable[None] | None: ... -def _count_non_terminal_descendants(session: Session, run_id: UUID, task_id: UUID) -> int: +def _count_non_terminal_descendants(session: Session, sample_id: UUID, task_id: UUID) -> int: """Count non-terminal descendants via iterative BFS on parent_task_id. Uses Python-level BFS rather than a recursive CTE so the logic is @@ -83,7 +83,7 @@ def _count_non_terminal_descendants(session: Session, run_id: UUID, task_id: UUI """ return sum( 1 - for descendant in descendants(session, run_id=run_id, root_task_id=task_id) + for descendant in descendants(session, sample_id=sample_id, root_task_id=task_id) if descendant.status not in TERMINAL_STATUSES ) @@ -109,12 +109,12 @@ def __init__( self._dashboard_publisher = dashboard_publisher or get_dashboard_event_publisher() self._graph_repo.add_mutation_listener(self._publish_graph_mutation) - async def _publish_graph_mutation(self, row: RunGraphMutation) -> None: + async def _publish_graph_mutation(self, row: SampleGraphMutation) -> None: if self._dashboard_publisher is None: return await self._dashboard_publisher.publish(dashboard_graph_mutation_event_from_row(row)) - async def _legacy_publish_graph_mutation(self, row: RunGraphMutation) -> None: + async def _legacy_publish_graph_mutation(self, row: SampleGraphMutation) -> None: result = self._legacy_graph_mutation_listener(row) if inspect.isawaitable(result): await result @@ -124,7 +124,7 @@ async def _legacy_publish_graph_mutation(self, row: RunGraphMutation) -> None: async def spawn_dynamic_task( self, *, - run_id: UUID, + sample_id: UUID, parent_task_id: UUID, task: Task, depends_on: tuple[UUID, ...] = (), @@ -133,15 +133,15 @@ async def spawn_dynamic_task( Used by WorkerContext.spawn_task to make dynamic subtasks graph-native. No experiment_definition_tasks row is written — - the full Task snapshot lives in run_graph_nodes.task_json with + the full Task snapshot lives in sample_graph_nodes.task_json with is_dynamic=True. """ dispatch: tuple[UUID, UUID, UUID] | None = None with get_session() as session: - parent = self._graph_repo.get_node(session, run_id=run_id, task_id=parent_task_id) + parent = self._graph_repo.get_node(session, sample_id=sample_id, task_id=parent_task_id) node = await self._graph_repo.add_node( session, - run_id, + sample_id, task_slug=task.task_slug, instance_key=task.instance_key, description=task.description, @@ -156,7 +156,7 @@ async def spawn_dynamic_task( for dep in depends_on: await self._graph_repo.add_edge( session, - run_id, + sample_id, source_task_id=dep, target_task_id=node.task_id, status=EDGE_PENDING, @@ -164,13 +164,13 @@ async def spawn_dynamic_task( ) task_id = node.task_id if not depends_on: - definition_id = definition_id_for_run(session, run_id) - dispatch = (run_id, definition_id, task_id) + definition_id = definition_id_for_run(session, sample_id) + dispatch = (sample_id, definition_id, task_id) session.commit() if dispatch is not None: await self._runtime_events.dispatch_task_ready( - run_id=dispatch[0], + sample_id=dispatch[0], definition_id=dispatch[1], task_id=dispatch[2], ) @@ -189,7 +189,9 @@ async def cancel_task( Uses only_if_not_terminal to avoid races. Counts non-terminal descendants so the caller knows the cascade scope. """ - node = self._graph_repo.get_node(session, run_id=command.run_id, task_id=command.task_id) + node = self._graph_repo.get_node( + session, sample_id=command.sample_id, task_id=command.task_id + ) old_status = node.status if old_status in TERMINAL_STATUSES: @@ -202,7 +204,7 @@ async def cancel_task( # rather than a double-write. applied = await self._graph_repo.update_node_status( session, - run_id=command.run_id, + sample_id=command.sample_id, task_id=command.task_id, new_status=CANCELLED, meta=_MANAGER_META, @@ -211,15 +213,15 @@ async def cancel_task( cascaded = 0 if applied: - cascaded = _count_non_terminal_descendants(session, command.run_id, command.task_id) + cascaded = _count_non_terminal_descendants(session, command.sample_id, command.task_id) session.commit() if applied: - definition_id = definition_id_for_run(session, command.run_id) + definition_id = definition_id_for_run(session, command.sample_id) event = self._task_cancelled_event( session, - run_id=command.run_id, + sample_id=command.sample_id, definition_id=definition_id, task_id=command.task_id, cause="manager_decision", @@ -248,7 +250,7 @@ async def cancel_orphans( self, session: Session, *, - run_id: UUID, + sample_id: UUID, definition_id: UUID, parent_task_id: UUID, cause: PropagationCancelCause, @@ -257,12 +259,12 @@ async def cancel_orphans( meta = MutationMeta(actor="system:cascade", reason=cause) transitioned: list[UUID] = [] - for child in descendants(session, run_id=run_id, root_task_id=parent_task_id): + for child in descendants(session, sample_id=sample_id, root_task_id=parent_task_id): if child.status in TERMINAL_STATUSES: continue applied = await self._graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=child.task_id, new_status=CANCELLED, meta=meta, @@ -274,7 +276,7 @@ async def cancel_orphans( events = [ self._task_cancelled_event( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=nid, cause=cause, @@ -291,7 +293,7 @@ async def block_pending_descendants( self, session: Session, *, - run_id: UUID, + sample_id: UUID, parent_task_id: UUID, cause: str, ) -> list[UUID]: @@ -299,12 +301,12 @@ async def block_pending_descendants( meta = MutationMeta(actor="system:cascade", reason=cause) blocked: list[UUID] = [] - for child in descendants(session, run_id=run_id, root_task_id=parent_task_id): + for child in descendants(session, sample_id=sample_id, root_task_id=parent_task_id): if child.status == RUNNING or child.status in TERMINAL_STATUSES: continue applied = await self._graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=child.task_id, new_status=BLOCKED, meta=meta, @@ -333,7 +335,9 @@ async def refine_task( The graph node's description is the single source of truth -- no definition row to keep in sync. """ - node = self._graph_repo.get_node(session, run_id=command.run_id, task_id=command.task_id) + node = self._graph_repo.get_node( + session, sample_id=command.sample_id, task_id=command.task_id + ) old_description = node.description if node.status == RUNNING: @@ -341,7 +345,7 @@ async def refine_task( await self._graph_repo.update_node_field( session, - run_id=command.run_id, + sample_id=command.sample_id, task_id=command.task_id, field="description", value=command.new_description, @@ -378,7 +382,9 @@ async def restart_task( cancels non-terminal downstream targets (stale input) and recurses into COMPLETED downstream targets (stale output). """ - node = self._graph_repo.get_node(session, run_id=command.run_id, task_id=command.task_id) + node = self._graph_repo.get_node( + session, sample_id=command.sample_id, task_id=command.task_id + ) old_status = node.status if old_status not in TERMINAL_STATUSES: @@ -386,19 +392,19 @@ async def restart_task( invalidated_task_ids = await self._invalidate_downstream( session, - run_id=command.run_id, + sample_id=command.sample_id, task_id=command.task_id, ) # Reset this node's outgoing edges so they re-satisfy on re-run. outgoing = self._graph_repo.get_outgoing_edges( - session, run_id=command.run_id, task_id=command.task_id + session, sample_id=command.sample_id, task_id=command.task_id ) for edge in outgoing: if edge.status != EDGE_PENDING: await self._graph_repo.update_edge_status( session, - run_id=command.run_id, + sample_id=command.sample_id, edge_id=edge.id, new_status=EDGE_PENDING, meta=_MANAGER_META, @@ -409,7 +415,7 @@ async def restart_task( # check above already rejected non-terminal inputs. await self._graph_repo.update_node_status( session, - run_id=command.run_id, + sample_id=command.sample_id, task_id=command.task_id, new_status=PENDING, meta=_MANAGER_META, @@ -418,9 +424,9 @@ async def restart_task( session.commit() - definition_id = definition_id_for_run(session, command.run_id) + definition_id = definition_id_for_run(session, command.sample_id) await self._runtime_events.dispatch_task_ready( - run_id=command.run_id, + sample_id=command.sample_id, definition_id=definition_id, task_id=command.task_id, ) @@ -444,7 +450,7 @@ async def _invalidate_downstream( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> list[UUID]: """Cascade invalidate downstream targets whose input is becoming stale. @@ -482,23 +488,31 @@ async def _invalidate_downstream( while stack: current = stack.pop() - outgoing = self._graph_repo.get_outgoing_edges(session, run_id=run_id, task_id=current) + outgoing = self._graph_repo.get_outgoing_edges( + session, sample_id=sample_id, task_id=current + ) for edge in outgoing: target_id = edge.target_task_id if target_id in seen: continue seen.add(target_id) - target = self._graph_repo.get_node(session, run_id=run_id, task_id=target_id) + target = self._graph_repo.get_node(session, sample_id=sample_id, task_id=target_id) if target.status == COMPLETED: # Stale output — cancel, reset incoming edges (so # other fan-in parents re-satisfy them on their next # completion), reset outgoing edges, then recurse. - await self._cancel_for_invalidation(session, run_id=run_id, task_id=target_id) + await self._cancel_for_invalidation( + session, sample_id=sample_id, task_id=target_id + ) invalidated.append(target_id) - await self._reset_incoming_edges(session, run_id=run_id, task_id=target_id) - await self._reset_outgoing_edges(session, run_id=run_id, task_id=target_id) + await self._reset_incoming_edges( + session, sample_id=sample_id, task_id=target_id + ) + await self._reset_outgoing_edges( + session, sample_id=sample_id, task_id=target_id + ) stack.append(target_id) elif target.status in TERMINAL_STATUSES: # FAILED or CANCELLED — no stale output, no recursion. @@ -512,9 +526,13 @@ async def _invalidate_downstream( # siblings must re-satisfy them before the target # re-activates. Do NOT recurse into outgoing: the # target never completed, so no stale downstream. - await self._cancel_for_invalidation(session, run_id=run_id, task_id=target_id) + await self._cancel_for_invalidation( + session, sample_id=sample_id, task_id=target_id + ) invalidated.append(target_id) - await self._reset_incoming_edges(session, run_id=run_id, task_id=target_id) + await self._reset_incoming_edges( + session, sample_id=sample_id, task_id=target_id + ) return invalidated @@ -522,7 +540,7 @@ async def _cancel_for_invalidation( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> None: """Cancel a node as part of downstream invalidation and emit task/cancelled. @@ -535,7 +553,7 @@ async def _cancel_for_invalidation( """ await self._graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, new_status=CANCELLED, meta=MutationMeta( @@ -545,10 +563,10 @@ async def _cancel_for_invalidation( only_if_not_terminal=False, ) - definition_id = definition_id_for_run(session, run_id) + definition_id = definition_id_for_run(session, sample_id) event = self._task_cancelled_event( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=task_id, cause="downstream_invalidation", @@ -564,7 +582,7 @@ async def _reset_outgoing_edges( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> None: """Reset a node's outgoing edges to EDGE_PENDING. @@ -573,12 +591,14 @@ async def _reset_outgoing_edges( downstream edges are ready to re-satisfy when this node is eventually re-run (via its own restart or via re-activation). """ - outgoing = self._graph_repo.get_outgoing_edges(session, run_id=run_id, task_id=task_id) + outgoing = self._graph_repo.get_outgoing_edges( + session, sample_id=sample_id, task_id=task_id + ) for edge in outgoing: if edge.status != EDGE_PENDING: await self._graph_repo.update_edge_status( session, - run_id=run_id, + sample_id=sample_id, edge_id=edge.id, new_status=EDGE_PENDING, meta=MutationMeta( @@ -591,7 +611,7 @@ async def _reset_incoming_edges( self, session: Session, *, - run_id: UUID, + sample_id: UUID, task_id: UUID, ) -> None: """Reset a node's incoming edges to EDGE_PENDING. @@ -606,12 +626,14 @@ async def _reset_incoming_edges( edge status), so this does not affect whether the target re-activates — it only keeps the edge WAL honest. """ - incoming = self._graph_repo.get_incoming_edges(session, run_id=run_id, task_id=task_id) + incoming = self._graph_repo.get_incoming_edges( + session, sample_id=sample_id, task_id=task_id + ) for edge in incoming: if edge.status != EDGE_PENDING: await self._graph_repo.update_edge_status( session, - run_id=run_id, + sample_id=sample_id, edge_id=edge.id, new_status=EDGE_PENDING, meta=MutationMeta( @@ -624,14 +646,14 @@ def _task_cancelled_event( self, session: Session, *, - run_id: UUID, + sample_id: UUID, definition_id: UUID, task_id: UUID, cause: CancelCause, ) -> TaskCancelledEvent: execution = self._task_execution_repo.latest_for_node(session, task_id) return TaskCancelledEvent( - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=task_id, execution_id=None if execution is None else execution.id, diff --git a/ergon_core/ergon_core/core/application/runtime/task_models.py b/ergon_core/ergon_core/core/application/runtime/task_models.py index 1ad50705d..9c8a99389 100644 --- a/ergon_core/ergon_core/core/application/runtime/task_models.py +++ b/ergon_core/ergon_core/core/application/runtime/task_models.py @@ -14,7 +14,7 @@ class CancelTaskCommand(BaseModel): """Command to cancel a subtask.""" - run_id: RunId + sample_id: RunId task_id: NodeId model_config = {"frozen": True} @@ -36,7 +36,7 @@ class CancelTaskResult(BaseModel): class RefineTaskCommand(BaseModel): """Command to update description on a pending sub-task.""" - run_id: RunId + sample_id: RunId task_id: NodeId new_description: str = Field(min_length=1) @@ -64,7 +64,7 @@ class RestartTaskCommand(BaseModel): ``restart_task`` to put the node back in the scheduling queue. """ - run_id: RunId + sample_id: RunId task_id: NodeId model_config = {"frozen": True} @@ -112,7 +112,7 @@ class SubtaskInfo(BaseModel): class CleanupResult(BaseModel): """Result of cleaning up a cancelled task execution.""" - run_id: RunId + sample_id: RunId task_id: NodeId execution_id: UUID | None sandbox_id: str | None = None diff --git a/ergon_core/ergon_core/core/application/runtime/workflow_models.py b/ergon_core/ergon_core/core/application/runtime/workflow_models.py index 640763658..9fa0272c2 100644 --- a/ergon_core/ergon_core/core/application/runtime/workflow_models.py +++ b/ergon_core/ergon_core/core/application/runtime/workflow_models.py @@ -18,7 +18,7 @@ class WorkflowResourceRef(BaseModel): model_config = {"frozen": True} resource_id: UUID - run_id: UUID + sample_id: UUID task_execution_id: UUID | None task_id: UUID | None task_slug: str | None diff --git a/ergon_core/ergon_core/core/application/testing/test_harness_service.py b/ergon_core/ergon_core/core/application/testing/test_harness_service.py index 5d8f08ce3..8a7975a3c 100644 --- a/ergon_core/ergon_core/core/application/testing/test_harness_service.py +++ b/ergon_core/ergon_core/core/application/testing/test_harness_service.py @@ -4,22 +4,22 @@ from dataclasses import dataclass from uuid import UUID -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphMutation, SampleGraphNode from ergon_core.core.persistence.shared.db import get_engine -from ergon_core.core.persistence.shared.enums import RunStatus +from ergon_core.core.persistence.shared.enums import SampleStatus from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskEvaluation, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskEvaluation, + SampleTaskAttempt, Thread, ) from sqlmodel import Session, asc, select -class UnknownRunStatusError(ValueError): +class UnknownSampleStatusError(ValueError): """Raised when a test seed request names an unknown run status.""" @@ -61,7 +61,7 @@ class HarnessExecution: @dataclass(frozen=True) class HarnessRunState: - run_id: UUID + sample_id: UUID status: str graph_nodes: list[HarnessGraphNode] mutations: list[HarnessGraphMutation] @@ -76,7 +76,7 @@ class HarnessRunState: @dataclass(frozen=True) class HarnessExperimentRun: - run_id: UUID + sample_id: UUID status: str @@ -86,12 +86,14 @@ def get_session_dep() -> Iterator[Session]: yield session -def read_run_state(run_id: UUID, session: Session) -> HarnessRunState | None: - run = session.exec(select(RunRecord).where(RunRecord.id == run_id)).first() +def read_run_state(sample_id: UUID, session: Session) -> HarnessRunState | None: + run = session.exec(select(SampleRecord).where(SampleRecord.id == sample_id)).first() if run is None: return None - nodes = list(session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all()) + nodes = list( + session.exec(select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id)).all() + ) slug_by_task_id: dict[UUID, str] = {n.task_id: n.task_slug for n in nodes} graph_nodes = [ @@ -108,9 +110,9 @@ def read_run_state(run_id: UUID, session: Session) -> HarnessRunState | None: mutation_rows = list( session.exec( - select(RunGraphMutation) - .where(RunGraphMutation.run_id == run_id) - .order_by(asc(RunGraphMutation.sequence)) + select(SampleGraphMutation) + .where(SampleGraphMutation.sample_id == sample_id) + .order_by(asc(SampleGraphMutation.sequence)) ).all() ) mutations = [ @@ -123,7 +125,9 @@ def read_run_state(run_id: UUID, session: Session) -> HarnessRunState | None: ] eval_rows = list( - session.exec(select(RunTaskEvaluation).where(RunTaskEvaluation.run_id == run_id)).all() + session.exec( + select(SampleTaskEvaluation).where(SampleTaskEvaluation.sample_id == sample_id) + ).all() ) evaluations = [ HarnessEvaluation( @@ -136,7 +140,9 @@ def read_run_state(run_id: UUID, session: Session) -> HarnessRunState | None: ] execution_rows = list( - session.exec(select(RunTaskExecution).where(RunTaskExecution.run_id == run_id)).all() + session.exec( + select(SampleTaskAttempt).where(SampleTaskAttempt.sample_id == sample_id) + ).all() ) executions = [ HarnessExecution( @@ -148,15 +154,23 @@ def read_run_state(run_id: UUID, session: Session) -> HarnessRunState | None: ] resource_count = len( - list(session.exec(select(RunResource).where(RunResource.run_id == run_id)).all()) + list( + session.exec(select(SampleResource).where(SampleResource.sample_id == sample_id)).all() + ) + ) + thread_count = len( + list(session.exec(select(Thread).where(Thread.sample_id == sample_id)).all()) ) - thread_count = len(list(session.exec(select(Thread).where(Thread.run_id == run_id)).all())) context_event_count = len( - list(session.exec(select(RunContextEvent).where(RunContextEvent.run_id == run_id)).all()) + list( + session.exec( + select(SampleContextEvent).where(SampleContextEvent.sample_id == sample_id) + ).all() + ) ) return HarnessRunState( - run_id=run_id, + sample_id=sample_id, status=run.status, graph_nodes=graph_nodes, mutations=mutations, @@ -172,9 +186,9 @@ def read_run_state(run_id: UUID, session: Session) -> HarnessRunState | None: def read_experiment_runs(experiment: str, session: Session) -> list[HarnessExperimentRun]: runs = list( - session.exec(select(RunRecord).where(RunRecord.experiment == experiment)).all(), + session.exec(select(SampleRecord).where(SampleRecord.experiment == experiment)).all(), ) - return [HarnessExperimentRun(run_id=r.id, status=r.status) for r in runs] + return [HarnessExperimentRun(sample_id=r.id, status=r.status) for r in runs] def seed_run( @@ -188,16 +202,16 @@ def seed_run( task_slugs: list[str], ) -> UUID: try: - run_status = RunStatus(status) + run_status = SampleStatus(status) except ValueError as exc: - raise UnknownRunStatusError(status) from exc + raise UnknownSampleStatusError(status) from exc with Session(get_engine()) as session: definition = session.get(ExperimentDefinition, definition_id) if definition is None: raise DefinitionNotFoundError(str(definition_id)) - run = RunRecord( + run = SampleRecord( definition_id=definition_id, benchmark_type=benchmark_type, instance_key=instance_key, @@ -220,7 +234,7 @@ def reset_test_rows(*, experiment_prefix: str) -> None: with Session(get_engine()) as session: # Cannot SQL-filter on JSON prefix portably; load seeded rows and # filter in Python. Bounded by the seed endpoint being test-only. - candidates = list(session.exec(select(RunRecord)).all()) + candidates = list(session.exec(select(SampleRecord)).all()) for run in candidates: metadata = {} if run.summary_json is None else run.summary_json if not metadata.get("_test_seeded"): @@ -231,7 +245,7 @@ def reset_test_rows(*, experiment_prefix: str) -> None: session.commit() -def _execution_error_message(execution: RunTaskExecution) -> str | None: +def _execution_error_message(execution: SampleTaskAttempt) -> str | None: error = execution.parsed_error() if error is None: return None diff --git a/ergon_core/ergon_core/core/infrastructure/http/app.py b/ergon_core/ergon_core/core/infrastructure/http/app.py index 17223ce32..21fe3a229 100644 --- a/ergon_core/ergon_core/core/infrastructure/http/app.py +++ b/ergon_core/ergon_core/core/infrastructure/http/app.py @@ -28,7 +28,7 @@ import inngest.fast_api from ergon_core.core.infrastructure.http.routes.experiments import router as experiments_router from ergon_core.core.infrastructure.http.routes.rollouts import router as rollouts_router -from ergon_core.core.infrastructure.http.routes.runs import router as runs_router +from ergon_core.core.infrastructure.http.routes.samples import router as samples_router from ergon_core.core.infrastructure.http.routes.test_harness import router as _test_harness_router from ergon_core.core.infrastructure.dashboard.provider import ( init_dashboard_emitter, @@ -89,7 +89,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: lifespan=lifespan, ) -app.include_router(runs_router) +app.include_router(samples_router) app.include_router(experiments_router) app.include_router(rollouts_router) diff --git a/ergon_core/ergon_core/core/infrastructure/http/routes/runs.py b/ergon_core/ergon_core/core/infrastructure/http/routes/runs.py deleted file mode 100644 index 99b795cb3..000000000 --- a/ergon_core/ergon_core/core/infrastructure/http/routes/runs.py +++ /dev/null @@ -1,74 +0,0 @@ -"""FastAPI router for persisted run-detail snapshots.""" - -from uuid import UUID - -from ergon_core.core.views.runs.models import ( - RunSummaryDto, - RunSnapshotDto, -) -from ergon_core.core.application.runtime.models import GraphMutationRecordDto -from ergon_core.core.views.errors import ResourceTooLargeError -from ergon_core.core.views.runs.service import RunReadService -from fastapi import APIRouter, HTTPException -from fastapi.responses import FileResponse - -router = APIRouter(prefix="/runs", tags=["runs"]) - - -@router.get("", response_model=list[RunSummaryDto]) -def list_runs( - limit: int = 20, - status: str | None = None, - definition_id: UUID | None = None, - experiment: str | None = None, - offset: int = 0, -) -> list[RunSummaryDto]: - """List persisted run summaries for dashboard indexes.""" - return RunReadService().list_runs( - limit=limit, - status=status, - definition_id=definition_id, - experiment=experiment, - offset=offset, - ) - - -@router.get("/{run_id}", response_model=RunSnapshotDto) -def get_run(run_id: UUID) -> RunSnapshotDto: - """Get a persisted run-detail snapshot suitable for frontend hydration.""" - snapshot = RunReadService().build_run_snapshot(run_id) - if snapshot is None: - raise HTTPException(status_code=404, detail=f"Run {run_id} not found") - return snapshot - - -@router.get("/{run_id}/mutations", response_model=list[GraphMutationRecordDto]) -def get_mutations(run_id: UUID) -> list[GraphMutationRecordDto]: - """Return the append-only mutation log for a run, ordered by sequence.""" - mutations = RunReadService().list_mutations(run_id) - if mutations is None: - raise HTTPException(status_code=404, detail=f"Run {run_id} not found") - return mutations - - -@router.get("/{run_id}/resources/{resource_id}/content") -def get_resource_content(run_id: UUID, resource_id: UUID) -> FileResponse: - """Stream the blob bytes for a RunResource.""" - try: - blob = RunReadService().get_resource_blob(run_id, resource_id) - except (FileNotFoundError, OSError) as e: - raise HTTPException(status_code=404, detail="Resource blob missing on disk") from e - except ResourceTooLargeError as e: - raise HTTPException(status_code=413, detail=str(e)) from e - except ValueError as e: - raise HTTPException(status_code=404, detail="Resource blob outside blob root") from e - - if blob is None: - raise HTTPException(status_code=404, detail=f"Resource {resource_id} not found") - - return FileResponse( - path=blob.path, - media_type=blob.media_type, - filename=blob.filename, - content_disposition_type="inline", - ) diff --git a/ergon_core/ergon_core/core/infrastructure/http/routes/samples.py b/ergon_core/ergon_core/core/infrastructure/http/routes/samples.py new file mode 100644 index 000000000..3859fcb99 --- /dev/null +++ b/ergon_core/ergon_core/core/infrastructure/http/routes/samples.py @@ -0,0 +1,74 @@ +"""FastAPI router for persisted sample-detail snapshots.""" + +from uuid import UUID + +from ergon_core.core.views.samples.models import ( + SampleSummaryDto, + SampleSnapshotDto, +) +from ergon_core.core.application.runtime.models import GraphMutationRecordDto +from ergon_core.core.views.errors import ResourceTooLargeError +from ergon_core.core.views.samples.service import SampleSnapshotReadService +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse + +router = APIRouter(prefix="/samples", tags=["samples"]) + + +@router.get("", response_model=list[SampleSummaryDto]) +def list_samples( + limit: int = 20, + status: str | None = None, + definition_id: UUID | None = None, + experiment: str | None = None, + offset: int = 0, +) -> list[SampleSummaryDto]: + """List persisted sample summaries for dashboard indexes.""" + return SampleSnapshotReadService().list_samples( + limit=limit, + status=status, + definition_id=definition_id, + experiment=experiment, + offset=offset, + ) + + +@router.get("/{sample_id}", response_model=SampleSnapshotDto) +def get_sample_snapshot(sample_id: UUID) -> SampleSnapshotDto: + """Get a persisted sample-detail snapshot suitable for frontend hydration.""" + snapshot = SampleSnapshotReadService().build_snapshot(sample_id) + if snapshot is None: + raise HTTPException(status_code=404, detail=f"Sample {sample_id} not found") + return snapshot + + +@router.get("/{sample_id}/mutations", response_model=list[GraphMutationRecordDto]) +def get_mutations(sample_id: UUID) -> list[GraphMutationRecordDto]: + """Return the append-only mutation log for a sample, ordered by sequence.""" + mutations = SampleSnapshotReadService().list_mutations(sample_id) + if mutations is None: + raise HTTPException(status_code=404, detail=f"Sample {sample_id} not found") + return mutations + + +@router.get("/{sample_id}/resources/{resource_id}/content") +def get_resource_content(sample_id: UUID, resource_id: UUID) -> FileResponse: + """Stream the blob bytes for a SampleResource.""" + try: + blob = SampleSnapshotReadService().get_resource_blob(sample_id, resource_id) + except (FileNotFoundError, OSError) as e: + raise HTTPException(status_code=404, detail="Resource blob missing on disk") from e + except ResourceTooLargeError as e: + raise HTTPException(status_code=413, detail=str(e)) from e + except ValueError as e: + raise HTTPException(status_code=404, detail="Resource blob outside blob root") from e + + if blob is None: + raise HTTPException(status_code=404, detail=f"Resource {resource_id} not found") + + return FileResponse( + path=blob.path, + media_type=blob.media_type, + filename=blob.filename, + content_disposition_type="inline", + ) diff --git a/ergon_core/ergon_core/core/infrastructure/http/routes/test_harness.py b/ergon_core/ergon_core/core/infrastructure/http/routes/test_harness.py index 0fe2e18fb..d3495d34a 100644 --- a/ergon_core/ergon_core/core/infrastructure/http/routes/test_harness.py +++ b/ergon_core/ergon_core/core/infrastructure/http/routes/test_harness.py @@ -17,7 +17,7 @@ ) from ergon_core.core.application.testing.test_harness_service import ( DefinitionNotFoundError, - UnknownRunStatusError, + UnknownSampleStatusError, get_session_dep, read_experiment_runs as _read_experiment_runs, read_run_state as _read_run_state, @@ -80,7 +80,7 @@ class TestExecutionDto(BaseModel): class TestRunStateDto(BaseModel): - run_id: UUID + sample_id: UUID status: str graph_nodes: list[TestGraphNodeDto] mutations: list[TestGraphMutationDto] @@ -94,7 +94,7 @@ class TestRunStateDto(BaseModel): class TestExperimentRunDto(BaseModel): - run_id: UUID + sample_id: UUID status: str @@ -103,19 +103,19 @@ class TestExperimentRunDto(BaseModel): # --------------------------------------------------------------------------- -@router.get("/read/run/{run_id}/state", response_model=TestRunStateDto) +@router.get("/read/samples/{sample_id}/state", response_model=TestRunStateDto) def read_run_state( - run_id: UUID, + sample_id: UUID, session: Annotated[object, Depends(get_session_dep)], ) -> TestRunStateDto: - state = _read_run_state(run_id, session) # type: ignore[arg-type] + state = _read_run_state(sample_id, session) # type: ignore[arg-type] if state is None: - raise HTTPException(status_code=404, detail=f"run {run_id} not found") + raise HTTPException(status_code=404, detail=f"run {sample_id} not found") return TestRunStateDto(**asdict(state)) @router.get( - "/read/experiment/{experiment}/runs", + "/read/experiment/{experiment}/samples", response_model=list[TestExperimentRunDto], ) def read_experiment_runs( @@ -124,7 +124,7 @@ def read_experiment_runs( ) -> list[TestExperimentRunDto]: """List all runs attached to a v2 experiment grouping tag.""" return [ - TestExperimentRunDto(run_id=run.run_id, status=run.status) + TestExperimentRunDto(sample_id=run.sample_id, status=run.status) for run in _read_experiment_runs(experiment, session) # type: ignore[arg-type] ] @@ -134,7 +134,7 @@ def read_experiment_runs( # --------------------------------------------------------------------------- # # Seeded rows record the test experiment tag in ``summary_json`` and -# ``RunRecord.experiment`` so dashboard/read tests use the canonical v2 grouping +# ``SampleRecord.experiment`` so dashboard/read tests use the canonical v2 grouping # column. # ``ResetRequest.experiment_prefix`` has no default: reset is destructive, so # callers must always specify what to nuke. @@ -154,12 +154,12 @@ class ResetRequest(BaseModel): experiment_prefix: str -@router.post("/write/run/seed", status_code=201) +@router.post("/write/samples/seed", status_code=201) def seed_run( body: SeedRunRequest, ) -> dict: try: - run_id = _seed_run( + sample_id = _seed_run( definition_id=body.definition_id, benchmark_type=body.benchmark_type, instance_key=body.instance_key, @@ -168,7 +168,7 @@ def seed_run( status=body.status, task_slugs=body.task_slugs, ) - except UnknownRunStatusError as exc: + except UnknownSampleStatusError as exc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"unknown run status: {body.status!r}", @@ -178,7 +178,7 @@ def seed_run( status_code=status.HTTP_404_NOT_FOUND, detail=f"definition {body.definition_id} not found", ) from exc - return {"run_id": str(run_id)} + return {"sample_id": str(sample_id)} @router.post("/write/reset", status_code=204) @@ -192,7 +192,7 @@ def reset_test_rows( # --------------------------------------------------------------------------- # Experiment-run submission endpoint — the single entry point for smoke drivers. # -# Moved here (rather than a separate /runs POST) because it's the test +# Moved here (rather than a separate /samples POST) because it's the test # harness that cares about grouped multi-run submission. Host-side # pytest never imports ergon internals; it just POSTs slugs. That keeps # the smoke fixtures single-sourced in the api container's process (one @@ -219,7 +219,7 @@ class SubmitExperimentRunsRequest(BaseModel): class SubmitExperimentRunsResponse(BaseModel): - run_ids: list[UUID] + sample_ids: list[UUID] @router.post("/write/experiment-runs", response_model=SubmitExperimentRunsResponse) @@ -230,13 +230,13 @@ async def submit_experiment_runs( Per-slot flow persists the object-bound smoke ``Benchmark`` into the immutable ``ExperimentDefinition`` tables. The v2 experiment grouping tag - is written into definition metadata and copied to ``RunRecord.experiment`` + is written into definition metadata and copied to ``SampleRecord.experiment`` by launch. Slots submit sequentially — typical N ≤ 3, so the parallel-gather savings are negligible. """ from ergon_core.core.application.experiments.service import persist_benchmark - run_ids: list[UUID] = [] + sample_ids: list[UUID] = [] for slot in body.slots: try: benchmark_cls = _SMOKE_BENCHMARKS[body.benchmark_slug] @@ -264,6 +264,6 @@ async def submit_experiment_runs( handle = persist_benchmark(benchmark_source) launched = await _run_experiment(ExperimentRunRequest(definition_id=handle.definition_id)) - run_ids.extend(launched.run_ids) + sample_ids.extend(launched.sample_ids) - return SubmitExperimentRunsResponse(run_ids=run_ids) + return SubmitExperimentRunsResponse(sample_ids=sample_ids) diff --git a/ergon_core/ergon_core/core/infrastructure/inngest/client.py b/ergon_core/ergon_core/core/infrastructure/inngest/client.py index f4e6c65e1..7bd73a1db 100644 --- a/ergon_core/ergon_core/core/infrastructure/inngest/client.py +++ b/ergon_core/ergon_core/core/infrastructure/inngest/client.py @@ -26,12 +26,12 @@ logger=logging.getLogger("ergon_core.inngest"), ) -# All orchestration functions carry run_id in their trigger event data. -# Sending a run/cancelled event with a matching run_id kills them in-flight. +# All orchestration functions carry sample_id in their trigger event data. +# Sending a sample/cancelled event with a matching sample_id kills them in-flight. RUN_CANCEL = [ inngest.Cancel( - event="run/cancelled", - if_exp="event.data.run_id == async.data.run_id", + event="sample/cancelled", + if_exp="event.data.sample_id == async.data.sample_id", ) ] diff --git a/ergon_core/ergon_core/core/infrastructure/inngest/registry.py b/ergon_core/ergon_core/core/infrastructure/inngest/registry.py index 7eae543e4..9afdda993 100644 --- a/ergon_core/ergon_core/core/infrastructure/inngest/registry.py +++ b/ergon_core/ergon_core/core/infrastructure/inngest/registry.py @@ -14,7 +14,7 @@ cleanup_cancelled_task_fn, ) from ergon_core.core.jobs.resources.persist_outputs.inngest import persist_outputs_fn -from ergon_core.core.jobs.run.cleanup.inngest import run_cleanup_fn +from ergon_core.core.jobs.run.cleanup.inngest import sample_cleanup_fn from ergon_core.core.jobs.sandbox.cleanup.inngest import ( sandbox_cleanup_on_completed_fn, sandbox_cleanup_on_failed_fn, @@ -45,7 +45,7 @@ block_descendants_on_failed_fn, cancel_orphans_on_cancelled_fn, cleanup_cancelled_task_fn, - run_cleanup_fn, + sample_cleanup_fn, sandbox_cleanup_on_completed_fn, sandbox_cleanup_on_failed_fn, ] diff --git a/ergon_core/ergon_core/core/infrastructure/sandbox/event_sink.py b/ergon_core/ergon_core/core/infrastructure/sandbox/event_sink.py index 94f5eed1f..029d2b4f8 100644 --- a/ergon_core/ergon_core/core/infrastructure/sandbox/event_sink.py +++ b/ergon_core/ergon_core/core/infrastructure/sandbox/event_sink.py @@ -19,7 +19,7 @@ class SandboxEventSink(Protocol): async def sandbox_created( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, timeout_minutes: int, @@ -28,7 +28,7 @@ async def sandbox_created( async def sandbox_command( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, command: str, @@ -43,7 +43,7 @@ async def sandbox_closed( task_id: UUID, sandbox_id: str, reason: str, - run_id: UUID | None = None, + sample_id: UUID | None = None, ) -> None: ... @@ -52,7 +52,7 @@ class NoopSandboxEventSink: async def sandbox_created( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, timeout_minutes: int, @@ -62,7 +62,7 @@ async def sandbox_created( async def sandbox_command( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, command: str, @@ -78,7 +78,7 @@ async def sandbox_closed( task_id: UUID, sandbox_id: str, reason: str, - run_id: UUID | None = None, + sample_id: UUID | None = None, ) -> None: return @@ -91,7 +91,7 @@ def __init__(self, emitter: DashboardEventPublisher) -> None: async def sandbox_created( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, timeout_minutes: int, @@ -99,7 +99,7 @@ async def sandbox_created( ) -> None: await self._emitter.publish( DashboardSandboxCreatedEvent( - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=sandbox_id, timeout_minutes=timeout_minutes, @@ -110,7 +110,7 @@ async def sandbox_created( async def sandbox_command( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, command: str, @@ -121,7 +121,7 @@ async def sandbox_command( ) -> None: await self._emitter.publish( DashboardSandboxCommandEvent( - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=sandbox_id, command=command, @@ -138,7 +138,7 @@ async def sandbox_closed( task_id: UUID, sandbox_id: str, reason: str, - run_id: UUID | None = None, + sample_id: UUID | None = None, ) -> None: await self._emitter.publish( DashboardSandboxClosedEvent( @@ -160,7 +160,7 @@ class PostgresSandboxEventSink: async def sandbox_created( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, timeout_minutes: int, @@ -174,7 +174,7 @@ async def sandbox_created( with get_session() as s: s.add( SandboxEvent( - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=sandbox_id, kind="sandbox_created", @@ -186,7 +186,7 @@ async def sandbox_created( async def sandbox_command( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, command: str, @@ -203,7 +203,7 @@ async def sandbox_command( with get_session() as s: s.add( SandboxCommandWalEntry( - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=sandbox_id, command=command, @@ -220,9 +220,9 @@ async def sandbox_closed( task_id: UUID, sandbox_id: str, reason: str, - run_id: UUID | None = None, + sample_id: UUID | None = None, ) -> None: - if run_id is None: + if sample_id is None: return # reason: avoid import cycle with ergon_core.api package exports. from ergon_core.core.persistence.telemetry.models import ( @@ -232,7 +232,7 @@ async def sandbox_closed( with get_session() as s: s.add( SandboxEvent( - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=sandbox_id, kind="sandbox_closed", @@ -255,7 +255,7 @@ def __init__(self, *sinks: SandboxEventSink) -> None: async def sandbox_created( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, timeout_minutes: int, @@ -263,7 +263,7 @@ async def sandbox_created( ) -> None: for sink in self._sinks: await sink.sandbox_created( - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=sandbox_id, timeout_minutes=timeout_minutes, @@ -272,7 +272,7 @@ async def sandbox_created( async def sandbox_command( self, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, command: str, @@ -283,7 +283,7 @@ async def sandbox_command( ) -> None: for sink in self._sinks: await sink.sandbox_command( - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=sandbox_id, command=command, @@ -298,12 +298,12 @@ async def sandbox_closed( task_id: UUID, sandbox_id: str, reason: str, - run_id: UUID | None = None, + sample_id: UUID | None = None, ) -> None: for sink in self._sinks: await sink.sandbox_closed( task_id=task_id, sandbox_id=sandbox_id, reason=reason, - run_id=run_id, + sample_id=sample_id, ) diff --git a/ergon_core/ergon_core/core/infrastructure/sandbox/instrumentation.py b/ergon_core/ergon_core/core/infrastructure/sandbox/instrumentation.py index e3de8f65f..9d03e394f 100644 --- a/ergon_core/ergon_core/core/infrastructure/sandbox/instrumentation.py +++ b/ergon_core/ergon_core/core/infrastructure/sandbox/instrumentation.py @@ -34,14 +34,14 @@ class InstrumentedSandboxCommands: def __init__( self, sink: SandboxEventSink, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, commands: Commands, max_output_len: int = 4000, ) -> None: self._sink = sink - self._run_id = run_id + self._sample_id = sample_id self._task_id = task_id self._sandbox_id = sandbox_id self._commands = commands @@ -57,7 +57,7 @@ async def _emit( ) -> None: duration_ms = int((time.time() - started_at) * 1000) await self._sink.sandbox_command( - run_id=self._run_id, + sample_id=self._sample_id, task_id=self._task_id, sandbox_id=self._sandbox_id, command=_truncate(command, 512) or command, @@ -106,14 +106,14 @@ class InstrumentedSandboxFiles: def __init__( self, sink: SandboxEventSink, - run_id: UUID, + sample_id: UUID, task_id: UUID, sandbox_id: str, files: Filesystem, max_output_len: int = 4000, ) -> None: self._sink = sink - self._run_id = run_id + self._sample_id = sample_id self._task_id = task_id self._sandbox_id = sandbox_id self._files = files @@ -129,7 +129,7 @@ async def _emit( ) -> None: duration_ms = int((time.time() - started_at) * 1000) await self._sink.sandbox_command( - run_id=self._run_id, + sample_id=self._sample_id, task_id=self._task_id, sandbox_id=self._sandbox_id, command=command, @@ -205,22 +205,22 @@ def __init__( self, sandbox: AsyncSandbox, sink: SandboxEventSink, - run_id: UUID, + sample_id: UUID, task_id: UUID, max_output_len: int = 4000, ) -> None: self._sandbox = sandbox self._sink = sink - self._run_id = run_id + self._sample_id = sample_id self._task_id = task_id self._max_output_len = max_output_len sid = sandbox.sandbox_id self.commands = InstrumentedSandboxCommands( - sink, run_id, task_id, sid, sandbox.commands, max_output_len + sink, sample_id, task_id, sid, sandbox.commands, max_output_len ) self.files = InstrumentedSandboxFiles( - sink, run_id, task_id, sid, sandbox.files, max_output_len + sink, sample_id, task_id, sid, sandbox.files, max_output_len ) async def _emit( @@ -233,7 +233,7 @@ async def _emit( ) -> None: duration_ms = int((time.time() - started_at) * 1000) await self._sink.sandbox_command( - run_id=self._run_id, + sample_id=self._sample_id, task_id=self._task_id, sandbox_id=self._sandbox.sandbox_id, command=_truncate(command, 512) or command, diff --git a/ergon_core/ergon_core/core/infrastructure/sandbox/manager.py b/ergon_core/ergon_core/core/infrastructure/sandbox/manager.py index 9cc514c35..045f66758 100644 --- a/ergon_core/ergon_core/core/infrastructure/sandbox/manager.py +++ b/ergon_core/ergon_core/core/infrastructure/sandbox/manager.py @@ -68,7 +68,7 @@ class BaseSandboxManager(ABC): _sandboxes: dict[UUID, "AsyncSandbox"] = {} _file_registries: dict[UUID, dict[str, str]] = {} _created_files_registry: dict[UUID, set[str]] = {} - _run_ids: dict[UUID, UUID] = {} + _sample_ids: dict[UUID, UUID] = {} _display_task_ids: dict[UUID, UUID] = {} _sandbox_manager_classes: dict[UUID, type["BaseSandboxManager"]] = {} _creation_locks: dict[UUID, asyncio.Lock] = {} @@ -142,9 +142,9 @@ async def _emit_wal_entry( # slopcop: ignore[max-function-params] resolved_duration_ms = int((time.time() - started_at) * 1000) max_len = settings.otel_stdout_stderr_max_length - resolved_run_id = self._run_ids.get(sandbox_key, sandbox_key) + resolved_sample_id = self._sample_ids.get(sandbox_key, sandbox_key) await self._event_sink.sandbox_command( - run_id=resolved_run_id, + sample_id=resolved_sample_id, task_id=task_id or self._get_display_task_id(sandbox_key), sandbox_id=resolved_sandbox_id, command=_truncate(command, 512) or command, @@ -275,7 +275,7 @@ async def _verify_setup(self, sandbox: AsyncSandbox, task_id: UUID) -> None: async def create( self, sandbox_key: UUID, - run_id: UUID, + sample_id: UUID, timeout_minutes: int = 30, envs: dict[str, str] | None = None, display_task_id: UUID | None = None, @@ -326,12 +326,12 @@ async def create( self._sandboxes[sandbox_key] = sandbox self._ensure_registries(sandbox_key) - self._run_ids[sandbox_key] = run_id + self._sample_ids[sandbox_key] = sample_id self._display_task_ids[sandbox_key] = display_task_id self._sandbox_manager_classes[sandbox_key] = type(self) await self._event_sink.sandbox_created( - run_id=run_id, + sample_id=sample_id, task_id=display_task_id, sandbox_id=sandbox.sandbox_id, timeout_minutes=timeout_minutes, @@ -479,14 +479,14 @@ async def terminate(self, task_id: UUID, reason: str = "completed") -> None: ) self._file_registries.pop(task_id, None) self._created_files_registry.pop(task_id, None) - self._run_ids.pop(task_id, None) + self._sample_ids.pop(task_id, None) self._display_task_ids.pop(task_id, None) self._sandbox_manager_classes.pop(task_id, None) return sandbox_id = sandbox.sandbox_id display_task_id = self._get_display_task_id(task_id) - run_id = self._run_ids.get(task_id) + sample_id = self._sample_ids.get(task_id) try: await sandbox.kill() except Exception as e: # slopcop: ignore[no-broad-except] @@ -495,7 +495,7 @@ async def terminate(self, task_id: UUID, reason: str = "completed") -> None: finally: self._file_registries.pop(task_id, None) self._created_files_registry.pop(task_id, None) - self._run_ids.pop(task_id, None) + self._sample_ids.pop(task_id, None) self._display_task_ids.pop(task_id, None) self._sandbox_manager_classes.pop(task_id, None) @@ -503,7 +503,7 @@ async def terminate(self, task_id: UUID, reason: str = "completed") -> None: task_id=display_task_id, sandbox_id=sandbox_id, reason=reason, - run_id=run_id, + sample_id=sample_id, ) await self._emit_wal_entry( task_id, @@ -531,7 +531,7 @@ async def terminate_by_sandbox_id(sandbox_id: str) -> bool: return True display_task_id = BaseSandboxManager._display_task_ids.get(task_id, task_id) - run_id = BaseSandboxManager._run_ids.get(task_id) + sample_id = BaseSandboxManager._sample_ids.get(task_id) try: await sandbox.kill() except Exception as e: # slopcop: ignore[no-broad-except] @@ -540,7 +540,7 @@ async def terminate_by_sandbox_id(sandbox_id: str) -> bool: BaseSandboxManager._sandboxes.pop(task_id, None) BaseSandboxManager._file_registries.pop(task_id, None) BaseSandboxManager._created_files_registry.pop(task_id, None) - BaseSandboxManager._run_ids.pop(task_id, None) + BaseSandboxManager._sample_ids.pop(task_id, None) BaseSandboxManager._display_task_ids.pop(task_id, None) BaseSandboxManager._sandbox_manager_classes.pop(task_id, None) @@ -548,10 +548,10 @@ async def terminate_by_sandbox_id(sandbox_id: str) -> bool: task_id=display_task_id, sandbox_id=sandbox_id, reason="completed", - run_id=run_id, + sample_id=sample_id, ) await manager_cls._event_sink.sandbox_command( - run_id=run_id or task_id, + sample_id=sample_id or task_id, task_id=display_task_id, sandbox_id=sandbox_id, command="sandbox.closed: completed", diff --git a/ergon_core/ergon_core/core/infrastructure/sandbox/resource_publisher.py b/ergon_core/ergon_core/core/infrastructure/sandbox/resource_publisher.py index a034c6c68..7b2781417 100644 --- a/ergon_core/ergon_core/core/infrastructure/sandbox/resource_publisher.py +++ b/ergon_core/ergon_core/core/infrastructure/sandbox/resource_publisher.py @@ -2,7 +2,7 @@ Copies bytes out of an E2B sandbox into a content-addressed blob store on the local filesystem. Application resource row semantics live in -``RunResourcePublishService``. +``SampleResourcePublishService``. """ import logging @@ -12,9 +12,9 @@ from uuid import UUID from e2b_code_interpreter import AsyncSandbox # type: ignore[import-untyped] -from ergon_core.core.application.resources.models import RunResourceView -from ergon_core.core.application.resources.publishing import RunResourcePublishService -from ergon_core.core.persistence.shared.enums import RunResourceKind +from ergon_core.core.application.resources.models import SampleResourceView +from ergon_core.core.application.resources.publishing import SampleResourcePublishService +from ergon_core.core.persistence.shared.enums import SampleResourceKind logger = logging.getLogger(__name__) @@ -25,28 +25,28 @@ class SandboxResourcePublisher: """Adapter for reading sandbox files and writing content-addressed blobs. ``sync()`` and ``publish_value()`` remain as compatibility helpers, but - they delegate resource append/dedup decisions to ``RunResourcePublishService``. + they delegate resource append/dedup decisions to ``SampleResourcePublishService``. """ # Default ``(sandbox_path, resource_kind)`` pairs scanned by ``sync()``. # Managers that need to publish from additional directories (e.g. a # researcher's scratchpad as well as ``final_output/``) pass a custom # ``publish_dirs`` to ``__init__``. - DEFAULT_PUBLISH_DIRS: ClassVar[tuple[tuple[str, RunResourceKind], ...]] = ( - ("/workspace/final_output/", RunResourceKind.REPORT), + DEFAULT_PUBLISH_DIRS: ClassVar[tuple[tuple[str, SampleResourceKind], ...]] = ( + ("/workspace/final_output/", SampleResourceKind.REPORT), ) def __init__( self, *, sandbox: AsyncSandbox | Any, # slopcop: ignore[no-typing-any] - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, blob_root: Path = _DEFAULT_BLOB_ROOT, - publish_dirs: tuple[tuple[str, RunResourceKind], ...] | None = None, + publish_dirs: tuple[tuple[str, SampleResourceKind], ...] | None = None, ) -> None: self._sandbox = sandbox - self._run_id = run_id + self._sample_id = sample_id self._task_execution_id = task_execution_id self._blob_root = blob_root self._publish_dirs = publish_dirs if publish_dirs is not None else self.DEFAULT_PUBLISH_DIRS @@ -56,14 +56,14 @@ def from_public_sandbox( cls, *, sandbox: Any, # slopcop: ignore[no-typing-any] - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, blob_root: Path = _DEFAULT_BLOB_ROOT, - publish_dirs: tuple[tuple[str, RunResourceKind], ...] | None = None, + publish_dirs: tuple[tuple[str, SampleResourceKind], ...] | None = None, ) -> "SandboxResourcePublisher": return cls( sandbox=sandbox, - run_id=run_id, + sample_id=sample_id, task_execution_id=task_execution_id, blob_root=blob_root, publish_dirs=publish_dirs, @@ -74,12 +74,12 @@ def from_public_sandbox( # persist_outputs_fn at task end. # ------------------------------------------------------------------ - async def sync(self) -> list[RunResourceView]: + async def sync(self) -> list[SampleResourceView]: """Publish configured sandbox dirs through the application service.""" - return await RunResourcePublishService().publish_sandbox_files( + return await SampleResourcePublishService().publish_sandbox_files( reader=self, blob_store=self, - run_id=self._run_id, + sample_id=self._sample_id, task_execution_id=self._task_execution_id, publish_dirs=self._publish_dirs, ) @@ -92,15 +92,15 @@ async def sync(self) -> list[RunResourceView]: def publish_value( self, *, - kind: RunResourceKind, + kind: SampleResourceKind, name: str, content: str, mime_type: str = "text/plain", - ) -> RunResourceView | None: + ) -> SampleResourceView | None: """Publish explicit value content through the application service.""" - return RunResourcePublishService().publish_value( + return SampleResourcePublishService().publish_value( blob_store=self, - run_id=self._run_id, + sample_id=self._sample_id, task_execution_id=self._task_execution_id, kind=kind, name=name, diff --git a/ergon_core/ergon_core/core/infrastructure/tracing/__init__.py b/ergon_core/ergon_core/core/infrastructure/tracing/__init__.py index 401a310d6..fb1bc7130 100644 --- a/ergon_core/ergon_core/core/infrastructure/tracing/__init__.py +++ b/ergon_core/ergon_core/core/infrastructure/tracing/__init__.py @@ -5,7 +5,7 @@ from ergon_core.core.infrastructure.tracing import get_trace_sink -Target span hierarchy (one trace per run, keyed by run_id):: +Target span hierarchy (one trace per run, keyed by sample_id):: workflow.execute (synthetic root) | experiment, instance_count @@ -22,7 +22,7 @@ +-- communication.message (per ThreadMessage, optional) +-- workflow.complete OR workflow.failed -Every span stores relational IDs (run_id, task_id, execution_id, evaluator_id) +Every span stores relational IDs (sample_id, task_id, execution_id, evaluator_id) for PG lookup, not payload copies. See otel_tracing_v2.md for full attribute schemas per span. """ @@ -49,7 +49,7 @@ from ergon_core.core.infrastructure.tracing.ids import ( DeterministicIdGenerator, span_id_from_key, - trace_id_from_run_id, + trace_id_from_sample_id, ) from ergon_core.core.infrastructure.tracing.noop import NoopTraceSink from ergon_core.core.infrastructure.tracing.otel import OtelTraceSink @@ -80,7 +80,7 @@ "span_id_from_key", "task_execute_context", "task_propagate_context", - "trace_id_from_run_id", + "trace_id_from_sample_id", "truncate_text", "workflow_complete_context", "workflow_failed_context", diff --git a/ergon_core/ergon_core/core/infrastructure/tracing/contexts.py b/ergon_core/ergon_core/core/infrastructure/tracing/contexts.py index fed57ef21..4e8e651b8 100644 --- a/ergon_core/ergon_core/core/infrastructure/tracing/contexts.py +++ b/ergon_core/ergon_core/core/infrastructure/tracing/contexts.py @@ -7,142 +7,142 @@ from uuid import UUID -from ergon_core.core.infrastructure.tracing.ids import span_id_from_key, trace_id_from_run_id +from ergon_core.core.infrastructure.tracing.ids import span_id_from_key, trace_id_from_sample_id from ergon_core.core.infrastructure.tracing.types import TraceContext -def workflow_root_context(run_id: UUID) -> TraceContext: - tid = trace_id_from_run_id(run_id) +def workflow_root_context(sample_id: UUID) -> TraceContext: + tid = trace_id_from_sample_id(sample_id) return TraceContext( trace_id=tid, - span_id=span_id_from_key("workflow", str(run_id)), - run_id=run_id, + span_id=span_id_from_key("workflow", str(sample_id)), + sample_id=sample_id, ) -def workflow_start_context(run_id: UUID) -> TraceContext: - root = workflow_root_context(run_id) +def workflow_start_context(sample_id: UUID) -> TraceContext: + root = workflow_root_context(sample_id) return TraceContext( trace_id=root.trace_id, - span_id=span_id_from_key("workflow_start", str(run_id)), + span_id=span_id_from_key("workflow_start", str(sample_id)), parent_span_id=root.span_id, - run_id=run_id, + sample_id=sample_id, ) -def task_execute_context(run_id: UUID, task_id: UUID) -> TraceContext: - root = workflow_root_context(run_id) +def task_execute_context(sample_id: UUID, task_id: UUID) -> TraceContext: + root = workflow_root_context(sample_id) return TraceContext( trace_id=root.trace_id, - span_id=span_id_from_key("task_execute", str(run_id), str(task_id)), + span_id=span_id_from_key("task_execute", str(sample_id), str(task_id)), parent_span_id=root.span_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, ) -def sandbox_setup_context(run_id: UUID, task_id: UUID) -> TraceContext: - parent = task_execute_context(run_id, task_id) +def sandbox_setup_context(sample_id: UUID, task_id: UUID) -> TraceContext: + parent = task_execute_context(sample_id, task_id) return TraceContext( trace_id=parent.trace_id, - span_id=span_id_from_key("sandbox_setup", str(run_id), str(task_id)), + span_id=span_id_from_key("sandbox_setup", str(sample_id), str(task_id)), parent_span_id=parent.span_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, ) def worker_execute_context( - run_id: UUID, + sample_id: UUID, task_id: UUID, execution_id: UUID, ) -> TraceContext: - parent = task_execute_context(run_id, task_id) + parent = task_execute_context(sample_id, task_id) return TraceContext( trace_id=parent.trace_id, span_id=span_id_from_key( "worker_execute", - str(run_id), + str(sample_id), str(task_id), str(execution_id), ), parent_span_id=parent.span_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, ) def persist_outputs_context( - run_id: UUID, + sample_id: UUID, task_id: UUID, execution_id: UUID, ) -> TraceContext: - parent = task_execute_context(run_id, task_id) + parent = task_execute_context(sample_id, task_id) return TraceContext( trace_id=parent.trace_id, span_id=span_id_from_key( "persist_outputs", - str(run_id), + str(sample_id), str(task_id), str(execution_id), ), parent_span_id=parent.span_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, ) -def task_propagate_context(run_id: UUID, task_id: UUID) -> TraceContext: - root = workflow_root_context(run_id) +def task_propagate_context(sample_id: UUID, task_id: UUID) -> TraceContext: + root = workflow_root_context(sample_id) return TraceContext( trace_id=root.trace_id, - span_id=span_id_from_key("task_propagate", str(run_id), str(task_id)), + span_id=span_id_from_key("task_propagate", str(sample_id), str(task_id)), parent_span_id=root.span_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, ) -def workflow_complete_context(run_id: UUID) -> TraceContext: - root = workflow_root_context(run_id) +def workflow_complete_context(sample_id: UUID) -> TraceContext: + root = workflow_root_context(sample_id) return TraceContext( trace_id=root.trace_id, - span_id=span_id_from_key("workflow_complete", str(run_id)), + span_id=span_id_from_key("workflow_complete", str(sample_id)), parent_span_id=root.span_id, - run_id=run_id, + sample_id=sample_id, ) -def workflow_failed_context(run_id: UUID) -> TraceContext: - root = workflow_root_context(run_id) +def workflow_failed_context(sample_id: UUID) -> TraceContext: + root = workflow_root_context(sample_id) return TraceContext( trace_id=root.trace_id, - span_id=span_id_from_key("workflow_failed", str(run_id)), + span_id=span_id_from_key("workflow_failed", str(sample_id)), parent_span_id=root.span_id, - run_id=run_id, + sample_id=sample_id, ) def evaluation_task_context( - run_id: UUID, + sample_id: UUID, task_id: UUID, execution_id: UUID, evaluator_id: UUID, ) -> TraceContext: - parent = task_execute_context(run_id, task_id) + parent = task_execute_context(sample_id, task_id) return TraceContext( trace_id=parent.trace_id, span_id=span_id_from_key( "evaluation_task", - str(run_id), + str(sample_id), str(task_id), str(execution_id), str(evaluator_id), ), parent_span_id=parent.span_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, evaluator_id=evaluator_id, @@ -150,19 +150,19 @@ def evaluation_task_context( def evaluation_criterion_context( - run_id: UUID, + sample_id: UUID, task_id: UUID, execution_id: UUID, evaluator_id: UUID, stage_idx: int, criterion_idx: int, ) -> TraceContext: - parent = evaluation_task_context(run_id, task_id, execution_id, evaluator_id) + parent = evaluation_task_context(sample_id, task_id, execution_id, evaluator_id) return TraceContext( trace_id=parent.trace_id, span_id=span_id_from_key( "evaluation_criterion", - str(run_id), + str(sample_id), str(task_id), str(execution_id), str(evaluator_id), @@ -170,7 +170,7 @@ def evaluation_criterion_context( str(criterion_idx), ), parent_span_id=parent.span_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, evaluator_id=evaluator_id, diff --git a/ergon_core/ergon_core/core/infrastructure/tracing/ids.py b/ergon_core/ergon_core/core/infrastructure/tracing/ids.py index d01d9c0ff..324de1024 100644 --- a/ergon_core/ergon_core/core/infrastructure/tracing/ids.py +++ b/ergon_core/ergon_core/core/infrastructure/tracing/ids.py @@ -16,9 +16,9 @@ _desired_span_id: ContextVar[int | None] = ContextVar("desired_span_id", default=None) -def trace_id_from_run_id(run_id: UUID) -> int: +def trace_id_from_sample_id(sample_id: UUID) -> int: """Derive a deterministic 128-bit trace ID from a run UUID.""" - return int(run_id.hex, 16) & MAX_TRACE_ID + return int(sample_id.hex, 16) & MAX_TRACE_ID def span_id_from_key(*parts: str) -> int: diff --git a/ergon_core/ergon_core/core/infrastructure/tracing/noop.py b/ergon_core/ergon_core/core/infrastructure/tracing/noop.py index 768bb2f81..f0ad9b3a2 100644 --- a/ergon_core/ergon_core/core/infrastructure/tracing/noop.py +++ b/ergon_core/ergon_core/core/infrastructure/tracing/noop.py @@ -28,7 +28,7 @@ def child_context( parent: TraceContext, *, span_key: str, - run_id: UUID | None = None, + sample_id: UUID | None = None, task_id: UUID | None = None, execution_id: UUID | None = None, evaluator_id: UUID | None = None, @@ -39,7 +39,7 @@ def child_context( trace_id=parent.trace_id, span_id=child_span, parent_span_id=parent.span_id, - run_id=parent.run_id if run_id is None else run_id, + sample_id=parent.sample_id if sample_id is None else sample_id, task_id=parent.task_id if task_id is None else task_id, execution_id=parent.execution_id if execution_id is None else execution_id, evaluator_id=parent.evaluator_id if evaluator_id is None else evaluator_id, diff --git a/ergon_core/ergon_core/core/infrastructure/tracing/otel.py b/ergon_core/ergon_core/core/infrastructure/tracing/otel.py index 926e2e9a8..e74b5f180 100644 --- a/ergon_core/ergon_core/core/infrastructure/tracing/otel.py +++ b/ergon_core/ergon_core/core/infrastructure/tracing/otel.py @@ -66,7 +66,7 @@ def child_context( parent: TraceContext, *, span_key: str, - run_id: UUID | None = None, + sample_id: UUID | None = None, task_id: UUID | None = None, execution_id: UUID | None = None, evaluator_id: UUID | None = None, @@ -76,7 +76,7 @@ def child_context( trace_id=parent.trace_id, span_id=span_id_from_key(str(parent.trace_id), str(parent.span_id), span_key), parent_span_id=parent.span_id, - run_id=run_id if run_id is not None else parent.run_id, + sample_id=sample_id if sample_id is not None else parent.sample_id, task_id=task_id if task_id is not None else parent.task_id, execution_id=execution_id if execution_id is not None else parent.execution_id, evaluator_id=evaluator_id if evaluator_id is not None else parent.evaluator_id, diff --git a/ergon_core/ergon_core/core/infrastructure/tracing/types.py b/ergon_core/ergon_core/core/infrastructure/tracing/types.py index 05e3e775c..c73319cfb 100644 --- a/ergon_core/ergon_core/core/infrastructure/tracing/types.py +++ b/ergon_core/ergon_core/core/infrastructure/tracing/types.py @@ -15,7 +15,7 @@ class TraceContext(BaseModel): trace_id: int span_id: int parent_span_id: int | None = None - run_id: UUID | None = None + sample_id: UUID | None = None task_id: UUID | None = None execution_id: UUID | None = None evaluator_id: UUID | None = None @@ -59,7 +59,7 @@ def child_context( parent: TraceContext, *, span_key: str, - run_id: UUID | None = None, + sample_id: UUID | None = None, task_id: UUID | None = None, execution_id: UUID | None = None, evaluator_id: UUID | None = None, diff --git a/ergon_core/ergon_core/core/jobs/resources/persist_outputs/composition.py b/ergon_core/ergon_core/core/jobs/resources/persist_outputs/composition.py index ee787112a..47e6bb031 100644 --- a/ergon_core/ergon_core/core/jobs/resources/persist_outputs/composition.py +++ b/ergon_core/ergon_core/core/jobs/resources/persist_outputs/composition.py @@ -4,9 +4,9 @@ from typing import Protocol -from ergon_core.core.application.resources.publishing import RunResourcePublishService +from ergon_core.core.application.resources.publishing import SampleResourcePublishService from ergon_core.core.infrastructure.sandbox.resource_publisher import SandboxResourcePublisher -from ergon_core.core.persistence.shared.enums import RunResourceKind +from ergon_core.core.persistence.shared.enums import SampleResourceKind from .contract import PersistOutputsRequest @@ -20,17 +20,17 @@ async def publish_public_sandbox_resources( payload: PersistOutputsRequest, ) -> int: publish_dir = payload.output_dir or sandbox.output_path - publish_dirs = ((publish_dir, RunResourceKind.REPORT),) + publish_dirs = ((publish_dir, SampleResourceKind.REPORT),) publisher = SandboxResourcePublisher.from_public_sandbox( sandbox=sandbox, - run_id=payload.run_id, + sample_id=payload.sample_id, task_execution_id=payload.execution_id, publish_dirs=publish_dirs, ) - synced = await RunResourcePublishService().publish_sandbox_files( + synced = await SampleResourcePublishService().publish_sandbox_files( reader=publisher, blob_store=publisher, - run_id=payload.run_id, + sample_id=payload.sample_id, task_execution_id=payload.execution_id, publish_dirs=publish_dirs, ) diff --git a/ergon_core/ergon_core/core/jobs/resources/persist_outputs/contract.py b/ergon_core/ergon_core/core/jobs/resources/persist_outputs/contract.py index de0d8e9ed..af0a37e7f 100644 --- a/ergon_core/ergon_core/core/jobs/resources/persist_outputs/contract.py +++ b/ergon_core/ergon_core/core/jobs/resources/persist_outputs/contract.py @@ -9,7 +9,7 @@ class PersistOutputsRequest(InngestEventContract): model_config = {"extra": "allow"} name: ClassVar[str] = "task/persist-outputs" - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID execution_id: UUID diff --git a/ergon_core/ergon_core/core/jobs/resources/persist_outputs/job.py b/ergon_core/ergon_core/core/jobs/resources/persist_outputs/job.py index c3de7834e..24c628b50 100644 --- a/ergon_core/ergon_core/core/jobs/resources/persist_outputs/job.py +++ b/ergon_core/ergon_core/core/jobs/resources/persist_outputs/job.py @@ -1,6 +1,6 @@ """Inngest child function: persist outputs from sandbox via resource service. -All resource row semantics go through :class:`RunResourcePublishService`. +All resource row semantics go through :class:`SampleResourcePublishService`. Consumers read via (``kind='report'``/``kind='artifact'`` rows whose ``file_path`` points at the content-addressed blob store). @@ -25,15 +25,15 @@ async def run_persist_outputs_job(payload: PersistOutputsRequest) -> PersistOutputsResult: """Sync sandbox publish dirs to the blob store and register resources.""" - run_id = payload.run_id + sample_id = payload.sample_id task_id = payload.task_id execution_id = payload.execution_id span_start = datetime.now(UTC) sandbox_id = payload.sandbox_id logger.info( - "persist-outputs run_id=%s task_id=%s sandbox_id=%s", - run_id, + "persist-outputs sample_id=%s task_id=%s sandbox_id=%s", + sample_id, task_id, sandbox_id, ) @@ -41,7 +41,7 @@ async def run_persist_outputs_job(payload: PersistOutputsRequest) -> PersistOutp if not sandbox_id: raise ContractViolationError( "persist-outputs invoked without sandbox_id", - run_id=run_id, + sample_id=sample_id, task_id=task_id, ) @@ -49,7 +49,7 @@ async def run_persist_outputs_job(payload: PersistOutputsRequest) -> PersistOutp with get_session() as session: view = await task_execution.load_task_view( session, - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, sandbox_id=sandbox_id, ) @@ -59,11 +59,11 @@ async def run_persist_outputs_job(payload: PersistOutputsRequest) -> PersistOutp get_trace_sink().emit_span( CompletedSpan( name="persist.outputs", - context=persist_outputs_context(run_id, task_id, execution_id), + context=persist_outputs_context(sample_id, task_id, execution_id), start_time=span_start, end_time=datetime.now(UTC), attributes={ - "run_id": str(run_id), + "sample_id": str(sample_id), "task_id": str(task_id), "execution_id": str(execution_id), "outputs_count": outputs_count, @@ -73,8 +73,8 @@ async def run_persist_outputs_job(payload: PersistOutputsRequest) -> PersistOutp if outputs_count: logger.info( - "persist-outputs: public sandbox publisher created %d resource(s) for run_id=%s", + "persist-outputs: public sandbox publisher created %d resource(s) for sample_id=%s", outputs_count, - payload.run_id, + payload.sample_id, ) return PersistOutputsResult(outputs_count=outputs_count) diff --git a/ergon_core/ergon_core/core/jobs/run/cleanup/contract.py b/ergon_core/ergon_core/core/jobs/run/cleanup/contract.py index 0766788cd..1e2de0ba9 100644 --- a/ergon_core/ergon_core/core/jobs/run/cleanup/contract.py +++ b/ergon_core/ergon_core/core/jobs/run/cleanup/contract.py @@ -1,13 +1,13 @@ from uuid import UUID -from ergon_core.core.application.events.runtime import RunCancelledEvent, RunCleanupEvent +from ergon_core.core.application.events.runtime import SampleCancelledEvent, SampleCleanupEvent from pydantic import BaseModel -class RunCleanupResult(BaseModel): +class SampleCleanupResult(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID status: str | None = None sandbox_terminated: bool = False sandbox_id: str | None = None diff --git a/ergon_core/ergon_core/core/jobs/run/cleanup/inngest.py b/ergon_core/ergon_core/core/jobs/run/cleanup/inngest.py index 642890aba..1151f1c37 100644 --- a/ergon_core/ergon_core/core/jobs/run/cleanup/inngest.py +++ b/ergon_core/ergon_core/core/jobs/run/cleanup/inngest.py @@ -2,19 +2,19 @@ import inngest -from .job import run_run_cleanup_job +from .job import run_sample_cleanup_job from ergon_core.core.infrastructure.inngest.client import inngest_client -from .contract import RunCleanupEvent, RunCleanupResult +from .contract import SampleCleanupEvent, SampleCleanupResult @inngest_client.create_function( fn_id="run-cleanup", - trigger=inngest.TriggerEvent(event="run/cleanup"), + trigger=inngest.TriggerEvent(event="sample/cleanup"), retries=0, - output_type=RunCleanupResult, + output_type=SampleCleanupResult, ) -async def run_cleanup_fn(ctx: inngest.Context) -> RunCleanupResult: - return await run_run_cleanup_job(ctx, RunCleanupEvent.model_validate(ctx.event.data)) +async def sample_cleanup_fn(ctx: inngest.Context) -> SampleCleanupResult: + return await run_sample_cleanup_job(ctx, SampleCleanupEvent.model_validate(ctx.event.data)) -__all__ = ["run_cleanup_fn"] +__all__ = ["sample_cleanup_fn"] diff --git a/ergon_core/ergon_core/core/jobs/run/cleanup/job.py b/ergon_core/ergon_core/core/jobs/run/cleanup/job.py index 770176be9..131c8ba01 100644 --- a/ergon_core/ergon_core/core/jobs/run/cleanup/job.py +++ b/ergon_core/ergon_core/core/jobs/run/cleanup/job.py @@ -8,51 +8,53 @@ from uuid import UUID from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.infrastructure.inngest.errors import ConfigurationError, DataIntegrityError from ergon_core.core.jobs.sandbox._lifecycle import terminate_external_sandbox -from .contract import RunCleanupEvent, RunCleanupResult +from .contract import SampleCleanupEvent, SampleCleanupResult from typing import Any logger = logging.getLogger(__name__) -_STATUS_MAP: dict[str, RunStatus] = { - "completed": RunStatus.COMPLETED, - "failed": RunStatus.FAILED, - "cancelled": RunStatus.CANCELLED, +_STATUS_MAP: dict[str, SampleStatus] = { + "completed": SampleStatus.COMPLETED, + "failed": SampleStatus.FAILED, + "cancelled": SampleStatus.CANCELLED, } -async def run_run_cleanup_job(ctx: Any, payload: RunCleanupEvent) -> RunCleanupResult: +async def run_sample_cleanup_job(ctx: Any, payload: SampleCleanupEvent) -> SampleCleanupResult: """Cleanup: terminate sandbox, ensure run status is correct.""" - run_id = payload.run_id + sample_id = payload.sample_id status = payload.status error_message = payload.error_message - logger.info("run-cleanup run_id=%s status=%s", run_id, status) + logger.info("run-cleanup sample_id=%s status=%s", sample_id, status) return await ctx.step.run( "cleanup-run", - partial(_cleanup_run, run_id, status, error_message), - output_type=RunCleanupResult, + partial(_cleanup_run, sample_id, status, error_message), + output_type=SampleCleanupResult, ) -async def _cleanup_run(run_id: UUID, status: str, error_message: str | None) -> RunCleanupResult: +async def _cleanup_run( + sample_id: UUID, status: str, error_message: str | None +) -> SampleCleanupResult: """Terminate sandbox and update run status.""" expected = _STATUS_MAP.get(status) if expected is None: raise ConfigurationError( f"Unknown cleanup status: {status!r}", - run_id=run_id, + sample_id=sample_id, ) session = get_session() try: - run = session.get(RunRecord, run_id) + run = session.get(SampleRecord, sample_id) if run is None: - raise DataIntegrityError("RunRecord", run_id) + raise DataIntegrityError("SampleRecord", sample_id) sandbox_id = run.parsed_summary().get("sandbox_id") sandbox_result = await terminate_external_sandbox( @@ -62,8 +64,8 @@ async def _cleanup_run(run_id: UUID, status: str, error_message: str | None) -> if sandbox_id is not None and not isinstance(sandbox_id, str): logger.warning( - "run-cleanup run_id=%s: sandbox_id has unexpected type %s, skipping termination", - run_id, + "run-cleanup sample_id=%s: sandbox_id has unexpected type %s, skipping termination", + sample_id, type(sandbox_id).__name__, ) @@ -77,8 +79,8 @@ async def _cleanup_run(run_id: UUID, status: str, error_message: str | None) -> finally: session.close() - return RunCleanupResult( - run_id=run_id, + return SampleCleanupResult( + sample_id=sample_id, status=status, sandbox_terminated=sandbox_terminated, sandbox_id=sandbox_id if isinstance(sandbox_id, str) else None, diff --git a/ergon_core/ergon_core/core/jobs/sandbox/setup/contract.py b/ergon_core/ergon_core/core/jobs/sandbox/setup/contract.py index 95442b4a0..29775f336 100644 --- a/ergon_core/ergon_core/core/jobs/sandbox/setup/contract.py +++ b/ergon_core/ergon_core/core/jobs/sandbox/setup/contract.py @@ -9,7 +9,7 @@ class SandboxSetupRequest(InngestEventContract): model_config = {"extra": "allow"} name: ClassVar[str] = "task/sandbox-setup" - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID benchmark_type: str diff --git a/ergon_core/ergon_core/core/jobs/sandbox/setup/job.py b/ergon_core/ergon_core/core/jobs/sandbox/setup/job.py index 3e2928b15..3ab9e6500 100644 --- a/ergon_core/ergon_core/core/jobs/sandbox/setup/job.py +++ b/ergon_core/ergon_core/core/jobs/sandbox/setup/job.py @@ -19,14 +19,14 @@ async def run_sandbox_setup_job(ctx: Any, payload: SandboxSetupRequest) -> SandboxReadyResult: """Create and configure a sandbox for task execution.""" - run_id = payload.run_id + sample_id = payload.sample_id task_id = payload.task_id benchmark_type = payload.benchmark_type span_start = datetime.now(UTC) logger.info( - "sandbox-setup run_id=%s task_id=%s benchmark=%s", - run_id, + "sandbox-setup sample_id=%s task_id=%s benchmark=%s", + sample_id, task_id, benchmark_type, ) @@ -35,7 +35,7 @@ async def run_sandbox_setup_job(ctx: Any, payload: SandboxSetupRequest) -> Sandb with get_session() as session: view = await task_execution.load_task_view( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, ) @@ -48,11 +48,11 @@ async def run_sandbox_setup_job(ctx: Any, payload: SandboxSetupRequest) -> Sandb get_trace_sink().emit_span( CompletedSpan( name="sandbox.setup", - context=sandbox_setup_context(run_id, task_id), + context=sandbox_setup_context(sample_id, task_id), start_time=span_start, end_time=datetime.now(UTC), attributes={ - "run_id": str(run_id), + "sample_id": str(sample_id), "task_id": str(task_id), "benchmark_type": benchmark_type, "sandbox_id": result.sandbox_id, diff --git a/ergon_core/ergon_core/core/jobs/task/cancel_orphans/job.py b/ergon_core/ergon_core/core/jobs/task/cancel_orphans/job.py index 7c54180ca..15ab6b3d5 100644 --- a/ergon_core/ergon_core/core/jobs/task/cancel_orphans/job.py +++ b/ergon_core/ergon_core/core/jobs/task/cancel_orphans/job.py @@ -25,7 +25,7 @@ async def _cancel_orphans_for( ctx: Any, *, - run_id: UUID, + sample_id: UUID, definition_id: UUID, parent_task_id: UUID, cause: PropagationCancelCause, @@ -37,7 +37,7 @@ async def _scan_and_cancel() -> dict: with get_session() as session: result = await svc.cancel_orphans( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, parent_task_id=parent_task_id, cause=cause, @@ -73,7 +73,7 @@ async def _block_descendants() -> list[str]: with get_session() as session: blocked_ids = await svc.block_pending_descendants( session, - run_id=payload.run_id, + sample_id=payload.sample_id, parent_task_id=payload.task_id, cause="parent_failed", ) @@ -88,7 +88,7 @@ async def run_cancel_orphans_on_cancelled_job(ctx: Any, payload: TaskCancelledEv logger.info("cancel-orphans parent=%s cause=parent_terminal", payload.task_id) return await _cancel_orphans_for( ctx, - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, parent_task_id=payload.task_id, cause="parent_terminal", diff --git a/ergon_core/ergon_core/core/jobs/task/cleanup_cancelled/inngest.py b/ergon_core/ergon_core/core/jobs/task/cleanup_cancelled/inngest.py index 4668ace89..acc28427e 100644 --- a/ergon_core/ergon_core/core/jobs/task/cleanup_cancelled/inngest.py +++ b/ergon_core/ergon_core/core/jobs/task/cleanup_cancelled/inngest.py @@ -2,7 +2,7 @@ import inngest -from .job import run_cleanup_cancelled_task_job +from .job import sample_cleanup_cancelled_task_job from ergon_core.core.infrastructure.inngest.client import RUN_CANCEL, inngest_client from .contract import TaskCancelledEvent from ergon_core.core.shared.json_types import JsonObject @@ -15,7 +15,7 @@ retries=3, ) async def cleanup_cancelled_task_fn(ctx: inngest.Context) -> JsonObject: - return await run_cleanup_cancelled_task_job( + return await sample_cleanup_cancelled_task_job( ctx, TaskCancelledEvent.model_validate(ctx.event.data) ) diff --git a/ergon_core/ergon_core/core/jobs/task/cleanup_cancelled/job.py b/ergon_core/ergon_core/core/jobs/task/cleanup_cancelled/job.py index fa1e23ede..62cafae80 100644 --- a/ergon_core/ergon_core/core/jobs/task/cleanup_cancelled/job.py +++ b/ergon_core/ergon_core/core/jobs/task/cleanup_cancelled/job.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) -async def run_cleanup_cancelled_task_job(ctx: Any, payload: TaskCancelledEvent) -> JsonObject: +async def sample_cleanup_cancelled_task_job(ctx: Any, payload: TaskCancelledEvent) -> JsonObject: """Clean up a single cancelled task's resources.""" logger.info( "cleanup-cancelled task_id=%s execution_id=%s cause=%s", @@ -34,7 +34,7 @@ async def run_cleanup_cancelled_task_job(ctx: Any, payload: TaskCancelledEvent) if payload.execution_id is None: return CleanupResult( - run_id=RunId(payload.run_id), + sample_id=RunId(payload.sample_id), task_id=NodeId(payload.task_id), execution_id=None, sandbox_id=None, @@ -48,7 +48,7 @@ def _update_db_rows() -> JsonObject: with get_session() as session: result = svc.cleanup( session, - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, execution_id=payload.execution_id, ) @@ -67,7 +67,7 @@ async def _release_sandbox() -> bool: await get_dashboard_event_publisher().publish( DashboardTaskStatusChangedEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, task_name="", parent_task_id=None, diff --git a/ergon_core/ergon_core/core/jobs/task/evaluate/contract.py b/ergon_core/ergon_core/core/jobs/task/evaluate/contract.py index e0389cda1..72d02176b 100644 --- a/ergon_core/ergon_core/core/jobs/task/evaluate/contract.py +++ b/ergon_core/ergon_core/core/jobs/task/evaluate/contract.py @@ -9,7 +9,7 @@ class TaskEvaluateRequest(InngestEventContract): model_config = {"frozen": True} name: ClassVar[str] = "task/evaluate" - run_id: UUID + sample_id: UUID task_id: UUID execution_id: UUID evaluator_index: int diff --git a/ergon_core/ergon_core/core/jobs/task/evaluate/job.py b/ergon_core/ergon_core/core/jobs/task/evaluate/job.py index 32ab7b9ab..f56c3f10f 100644 --- a/ergon_core/ergon_core/core/jobs/task/evaluate/job.py +++ b/ergon_core/ergon_core/core/jobs/task/evaluate/job.py @@ -2,11 +2,11 @@ Receives one ``TaskEvaluateRequest`` per evaluator from `execute_task._fan_out_evaluators`. -The payload only carries identity (``run_id`` + ``task_id`` + +The payload only carries identity (``sample_id`` + ``task_id`` + ``execution_id`` + ``evaluator_index``); everything else is reconstructed locally from the run-tier read boundary: -- execution row + stamped ``sandbox_id`` ← ``session.get(RunTaskExecution)`` +- execution row + stamped ``sandbox_id`` ← ``session.get(SampleTaskAttempt)`` - typed Task view ← ``TaskExecutionService.load_task_view(..., sandbox_id=...)`` - persisted ``WorkerOutput`` ← ``TaskExecutionService.load_worker_output`` - evaluator instance ← ``task.evaluators[payload.evaluator_index]`` @@ -40,13 +40,13 @@ get_trace_sink, ) from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunTaskExecution +from ergon_core.core.persistence.telemetry.models import SampleTaskAttempt from ergon_core.core.views.dashboard_events.contracts import DashboardTaskEvaluationUpdatedEvent -from ergon_core.core.views.runs.evaluation_mapping import build_dashboard_evaluation_dto +from ergon_core.core.views.samples.evaluation_mapping import build_dashboard_evaluation_dto if TYPE_CHECKING: from ergon_core.api.rubric import Evaluator - from ergon_core.core.application.runtime.models import RunGraphNodeView + from ergon_core.core.application.runtime.models import SampleGraphNodeView logger = logging.getLogger(__name__) _evaluation_persistence = EvaluationService() @@ -66,23 +66,23 @@ async def run_evaluate_task_run_job( del ctx # The orchestrator provides the evaluator-level retry boundary. span_start = datetime.now(UTC) - run_id = payload.run_id + sample_id = payload.sample_id task_id = payload.task_id execution_id = payload.execution_id evaluator_index = payload.evaluator_index with get_session() as session: - execution = session.get(RunTaskExecution, execution_id) + execution = session.get(SampleTaskAttempt, execution_id) if execution is None: raise ContractViolationError( - f"RunTaskExecution {execution_id} not found", - run_id=run_id, + f"SampleTaskAttempt {execution_id} not found", + sample_id=sample_id, task_id=task_id, execution_id=execution_id, ) view = await _task_execution.load_task_view( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id=execution.sandbox_id, ) @@ -95,7 +95,7 @@ async def run_evaluate_task_run_job( raise ContractViolationError( f"evaluator_index {evaluator_index} out of range for task " f"{task.task_slug!r} (has {len(task.evaluators)} evaluators)", - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, ) @@ -103,7 +103,7 @@ async def run_evaluate_task_run_job( binding_key = _evaluator_binding_key(evaluator, evaluator_index) context = CriterionContext( - run_id=run_id, + sample_id=sample_id, task_id=task.task_id, execution_id=execution_id, task=task, @@ -117,7 +117,7 @@ async def run_evaluate_task_run_job( binding_key=binding_key, evaluator_index=evaluator_index, view=view, - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, span_start=span_start, @@ -136,8 +136,8 @@ async def _run_evaluation( context: CriterionContext, binding_key: str, evaluator_index: int, - view: "RunGraphNodeView", - run_id: UUID, + view: "SampleGraphNodeView", + sample_id: UUID, task_id: UUID, execution_id: UUID, span_start: datetime, @@ -155,13 +155,13 @@ async def _run_evaluation( ) except Exception as exc: # slopcop: ignore[no-broad-except] logger.exception( - "evaluate_task_run failed run_id=%s task_id=%s index=%s", - run_id, + "evaluate_task_run failed sample_id=%s task_id=%s index=%s", + sample_id, task_id, evaluator_index, ) await _evaluation_persistence.persist_failure( - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, task_id=view.task_id, binding_key=binding_key, @@ -175,7 +175,7 @@ async def _run_evaluation( result = service_result.result persisted = await _evaluation_persistence.persist_success( - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, task_id=view.task_id, binding_key=binding_key, @@ -183,11 +183,11 @@ async def _run_evaluation( ) await get_dashboard_event_publisher().publish( DashboardTaskEvaluationUpdatedEvent( - run_id=run_id, + sample_id=sample_id, task_id=view.task_id, evaluation=build_dashboard_evaluation_dto( evaluation_id=persisted.evaluation_id, - run_id=persisted.run_id, + sample_id=persisted.sample_id, task_id=persisted.task_id, total_score=persisted.total_score, created_at=persisted.created_at, @@ -198,12 +198,12 @@ async def _run_evaluation( # Trace span needs the evaluator_id for stable key derivation; # reuse the persistence lookup so the span key matches the - # `run_task_evaluations.definition_evaluator_id` FK on the + # `sample_task_evaluations.definition_evaluator_id` FK on the # row persist_success just wrote. with get_session() as session: evaluator_id = _evaluation_persistence.lookup_evaluator_id( session, - run_id, + sample_id, binding_key, evaluator_type=evaluator.type_slug, snapshot_json=evaluator.model_dump(mode="json"), @@ -211,11 +211,11 @@ async def _run_evaluation( get_trace_sink().emit_span( CompletedSpan( name="evaluation.task", - context=evaluation_task_context(run_id, view.task_id, execution_id, evaluator_id), + context=evaluation_task_context(sample_id, view.task_id, execution_id, evaluator_id), start_time=span_start, end_time=datetime.now(UTC), attributes={ - "run_id": str(run_id), + "sample_id": str(sample_id), "task_id": str(view.task_id), "execution_id": str(execution_id), "evaluator_id": str(evaluator_id), diff --git a/ergon_core/ergon_core/core/jobs/task/execute/contract.py b/ergon_core/ergon_core/core/jobs/task/execute/contract.py index 277573187..27f001719 100644 --- a/ergon_core/ergon_core/core/jobs/task/execute/contract.py +++ b/ergon_core/ergon_core/core/jobs/task/execute/contract.py @@ -7,7 +7,7 @@ class TaskExecuteResult(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID task_id: UUID execution_id: UUID success: bool = False diff --git a/ergon_core/ergon_core/core/jobs/task/execute/job.py b/ergon_core/ergon_core/core/jobs/task/execute/job.py index bee5ec326..926caa245 100644 --- a/ergon_core/ergon_core/core/jobs/task/execute/job.py +++ b/ergon_core/ergon_core/core/jobs/task/execute/job.py @@ -75,7 +75,7 @@ from ergon_core.core.infrastructure.inngest.errors import ContractViolationError, NonRetriableError from ergon_core.core.jobs._events import send_job_event from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.infrastructure.tracing import ( CompletedSpan, get_trace_sink, @@ -95,7 +95,7 @@ async def _prepare_execution( async def _prepare() -> PreparedTaskExecution: return await svc.prepare( PrepareTaskExecutionCommand( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=payload.task_id, ) @@ -114,19 +114,19 @@ async def _invoke_sandbox_setup( "sandbox-setup", function=sandbox_setup_function, data=SandboxSetupRequest( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=payload.task_id, benchmark_type=prepared.benchmark_type, - sandbox_slug=_load_sandbox_slug(payload.run_id), + sandbox_slug=_load_sandbox_slug(payload.sample_id), ).model_dump(), ) -def _load_sandbox_slug(run_id: UUID) -> str | None: +def _load_sandbox_slug(sample_id: UUID) -> str | None: session = get_session() try: - run = session.get(RunRecord, run_id) + run = session.get(SampleRecord, sample_id) return None if run is None else run.sandbox_slug finally: session.close() @@ -142,14 +142,14 @@ async def _invoke_worker_execute( if prepared.assigned_worker_slug is None or prepared.worker_type is None: raise ContractViolationError( "prepared task execution is missing worker identity", - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, ) return await ctx.step.invoke( "worker-execute", function=worker_execute_function, data=WorkerExecuteJobRequest( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=payload.task_id, execution_id=prepared.execution_id, @@ -195,7 +195,7 @@ async def _fan_out_evaluators( with get_session() as session: view = await svc.load_task_view( session, - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, ) if not view.task.evaluators: @@ -208,7 +208,7 @@ async def _fan_out_evaluators( f"eval-{i}", function=evaluate_task_run_function, data=TaskEvaluateRequest( - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, execution_id=prepared.execution_id, evaluator_index=i, @@ -230,14 +230,14 @@ async def _invoke_persist_outputs( "persist-outputs", function=persist_outputs_function, data=PersistOutputsRequest( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=payload.task_id, execution_id=prepared.execution_id, sandbox_id=sandbox_result.sandbox_id, output_dir=sandbox_result.output_dir, benchmark_type=prepared.benchmark_type, - sandbox_slug=_load_sandbox_slug(payload.run_id), + sandbox_slug=_load_sandbox_slug(payload.sample_id), ).model_dump(), ) @@ -250,7 +250,7 @@ async def _emit_task_completed( await send_job_event( TaskCompletedEvent.name, TaskCompletedEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=payload.task_id, execution_id=prepared.execution_id, @@ -268,7 +268,7 @@ async def _emit_task_failed( await send_job_event( TaskFailedEvent.name, TaskFailedEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=payload.task_id, execution_id=prepared.execution_id, @@ -290,7 +290,7 @@ async def run_execute_task_job( persist_outputs_function: Any, evaluate_task_run_function: Any, ) -> TaskExecuteResult: - logger.info("task-execute run_id=%s task_id=%s", payload.run_id, payload.task_id) + logger.info("task-execute sample_id=%s task_id=%s", payload.sample_id, payload.task_id) span_start = datetime.now(UTC) svc = TaskExecutionService() @@ -311,7 +311,7 @@ async def run_execute_task_job( raise ContractViolationError( "Skipped task execution cannot emit task/completed without a real sandbox_id. " "Introduce a first-class task/skipped event before supporting skipped tasks.", - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, ) @@ -319,7 +319,7 @@ async def run_execute_task_job( if not sandbox_result.sandbox_id: raise ContractViolationError( "sandbox-setup returned empty sandbox_id", - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, ) task_sandbox_id = sandbox_result.sandbox_id @@ -336,7 +336,7 @@ async def run_execute_task_job( await svc.finalize_failure( FailTaskExecutionCommand( execution_id=prepared.execution_id, - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, error_message=error_msg, error_json=worker_result.error_json, @@ -344,7 +344,7 @@ async def run_execute_task_job( ) await _emit_task_failed(payload, prepared, error_msg, task_sandbox_id) return TaskExecuteResult( - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, execution_id=prepared.execution_id, success=False, @@ -376,7 +376,7 @@ async def run_execute_task_job( if task_sandbox_id is None: raise ContractViolationError( "task_sandbox_id is None after sandbox-setup completed", - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, ) await _emit_task_completed(payload, prepared, task_sandbox_id) @@ -384,11 +384,11 @@ async def run_execute_task_job( get_trace_sink().emit_span( CompletedSpan( name="task.execute", - context=task_execute_context(payload.run_id, prepared.task_id), + context=task_execute_context(payload.sample_id, prepared.task_id), start_time=span_start, end_time=datetime.now(UTC), attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "definition_id": str(payload.definition_id), "task_id": str(prepared.task_id), "execution_id": str(prepared.execution_id), @@ -404,7 +404,7 @@ async def run_execute_task_job( ) return TaskExecuteResult( - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, execution_id=prepared.execution_id, success=True, @@ -421,7 +421,7 @@ async def run_execute_task_job( await svc.finalize_failure( FailTaskExecutionCommand( execution_id=prepared.execution_id, - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, error_message=error_msg, error_json={ @@ -448,13 +448,13 @@ async def run_execute_task_job( get_trace_sink().emit_span( CompletedSpan( name="task.execute", - context=task_execute_context(payload.run_id, prepared.task_id), + context=task_execute_context(payload.sample_id, prepared.task_id), start_time=span_start, end_time=datetime.now(UTC), status_code="error", status_message=truncate_text(error_msg), attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "definition_id": str(payload.definition_id), "task_id": str(prepared.task_id), "execution_id": str(prepared.execution_id), diff --git a/ergon_core/ergon_core/core/jobs/task/propagate/contract.py b/ergon_core/ergon_core/core/jobs/task/propagate/contract.py index 98512185d..e5c5bf832 100644 --- a/ergon_core/ergon_core/core/jobs/task/propagate/contract.py +++ b/ergon_core/ergon_core/core/jobs/task/propagate/contract.py @@ -7,7 +7,7 @@ class TaskPropagateResult(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID task_id: UUID newly_ready_tasks: int = 0 workflow_complete: bool = False diff --git a/ergon_core/ergon_core/core/jobs/task/propagate/job.py b/ergon_core/ergon_core/core/jobs/task/propagate/job.py index eaae2bded..329392d33 100644 --- a/ergon_core/ergon_core/core/jobs/task/propagate/job.py +++ b/ergon_core/ergon_core/core/jobs/task/propagate/job.py @@ -11,7 +11,7 @@ PropagateTaskCompletionCommand, WorkflowTerminalState, ) -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from ergon_core.core.jobs._events import JobEvent, send_job_events from ergon_core.core.jobs.task.execute.contract import TaskReadyEvent from ergon_core.core.jobs.workflow.complete.contract import WorkflowCompletedEvent @@ -26,13 +26,13 @@ async def run_propagate_task_job(payload: TaskCompletedEvent) -> TaskPropagateResult: - logger.info("task-propagate run_id=%s task_id=%s", payload.run_id, payload.task_id) + logger.info("task-propagate sample_id=%s task_id=%s", payload.sample_id, payload.task_id) span_start = datetime.now(UTC) svc = WorkflowService() propagation = await svc.propagate( PropagateTaskCompletionCommand( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=payload.task_id, execution_id=payload.execution_id, @@ -43,7 +43,7 @@ async def run_propagate_task_job(payload: TaskCompletedEvent) -> TaskPropagateRe ( TaskReadyEvent.name, TaskReadyEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=td.task_id, ).model_dump(mode="json"), @@ -56,7 +56,7 @@ async def run_propagate_task_job(payload: TaskCompletedEvent) -> TaskPropagateRe ( WorkflowCompletedEvent.name, WorkflowCompletedEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, ).model_dump(mode="json"), ) @@ -66,7 +66,7 @@ async def run_propagate_task_job(payload: TaskCompletedEvent) -> TaskPropagateRe ( WorkflowFailedEvent.name, WorkflowFailedEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, error="Workflow failed during task propagation", ).model_dump(mode="json"), @@ -76,7 +76,7 @@ async def run_propagate_task_job(payload: TaskCompletedEvent) -> TaskPropagateRe await send_job_events(events) result = TaskPropagateResult( - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, newly_ready_tasks=len(propagation.ready_tasks), workflow_complete=(propagation.workflow_terminal_state == WorkflowTerminalState.COMPLETED), @@ -86,11 +86,11 @@ async def run_propagate_task_job(payload: TaskCompletedEvent) -> TaskPropagateRe get_trace_sink().emit_span( CompletedSpan( name="task.propagate", - context=task_propagate_context(payload.run_id, payload.task_id), + context=task_propagate_context(payload.sample_id, payload.task_id), start_time=span_start, end_time=datetime.now(UTC), attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "task_id": str(payload.task_id), "newly_ready_tasks": len(propagation.ready_tasks), "workflow_terminal": str(propagation.workflow_terminal_state), @@ -103,8 +103,8 @@ async def run_propagate_task_job(payload: TaskCompletedEvent) -> TaskPropagateRe async def run_propagate_task_failure_job(payload: TaskFailedEvent) -> TaskPropagateResult: logger.info( - "task-failure-propagate run_id=%s task_id=%s error=%s", - payload.run_id, + "task-failure-propagate sample_id=%s task_id=%s error=%s", + payload.sample_id, payload.task_id, payload.error, ) @@ -112,7 +112,7 @@ async def run_propagate_task_failure_job(payload: TaskFailedEvent) -> TaskPropag svc = WorkflowService() propagation = await svc.propagate_failure( PropagateTaskCompletionCommand( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=payload.task_id, execution_id=payload.execution_id, @@ -127,7 +127,7 @@ async def run_propagate_task_failure_job(payload: TaskFailedEvent) -> TaskPropag ( WorkflowFailedEvent.name, WorkflowFailedEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, error=payload.error, ).model_dump(mode="json"), @@ -137,7 +137,7 @@ async def run_propagate_task_failure_job(payload: TaskFailedEvent) -> TaskPropag await send_job_events(failure_events) result = TaskPropagateResult( - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, newly_ready_tasks=0, workflow_complete=False, diff --git a/ergon_core/ergon_core/core/jobs/task/worker_execute/contract.py b/ergon_core/ergon_core/core/jobs/task/worker_execute/contract.py index cd1384e28..5b6d80b60 100644 --- a/ergon_core/ergon_core/core/jobs/task/worker_execute/contract.py +++ b/ergon_core/ergon_core/core/jobs/task/worker_execute/contract.py @@ -10,7 +10,7 @@ class WorkerExecuteRequest(InngestEventContract): model_config = {"extra": "allow"} name: ClassVar[str] = "task/worker-execute" - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID execution_id: UUID diff --git a/ergon_core/ergon_core/core/jobs/task/worker_execute/job.py b/ergon_core/ergon_core/core/jobs/task/worker_execute/job.py index 2da5aa313..3080bb88c 100644 --- a/ergon_core/ergon_core/core/jobs/task/worker_execute/job.py +++ b/ergon_core/ergon_core/core/jobs/task/worker_execute/job.py @@ -19,7 +19,7 @@ from ergon_core.core.jobs._events import send_job_step_event from ergon_core.core.jobs.task.execute.contract import TaskReadyEvent from ergon_core.core.application.events.service import get_dashboard_event_publisher -from ergon_core.core.application.resources.service import RunResourceReadService +from ergon_core.core.application.resources.service import SampleResourceReadService from ergon_core.core.application.runtime.task_execution import TaskExecutionService from ergon_core.core.application.runtime.task_inspection import TaskInspectionService from ergon_core.core.application.runtime.task_management import TaskManagementService @@ -27,7 +27,7 @@ from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.application.context.service import ContextEventService from ergon_core.core.infrastructure.inngest.errors import ContractViolationError -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from .contract import WorkerExecuteJobRequest from .contract import WorkerExecuteJobResult from ergon_core.core.infrastructure.tracing import ( @@ -35,7 +35,7 @@ get_trace_sink, worker_execute_context, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.views.dashboard_events.context_events import context_event_to_dashboard_event from pydantic import BaseModel from sqlmodel import Session @@ -49,8 +49,8 @@ async def run_worker_execute_job( ctx: object | None = None, ) -> WorkerExecuteJobResult: logger.info( - "worker-execute run_id=%s task_id=%s worker_type=%s", - payload.run_id, + "worker-execute sample_id=%s task_id=%s worker_type=%s", + payload.sample_id, payload.task_id, payload.worker_type, ) @@ -64,7 +64,7 @@ async def run_worker_execute_job( with get_session() as session: view = await task_execution.load_task_view( session, - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, sandbox_id=payload.sandbox_id, ) @@ -74,7 +74,7 @@ async def run_worker_execute_job( if not task.sandbox.is_live: raise ContractViolationError( "worker-execute object-bound task requires a live sandbox attached via sandbox_id", - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, execution_id=payload.execution_id, sandbox_id=payload.sandbox_id, @@ -82,14 +82,14 @@ async def run_worker_execute_job( worker.validate_runtime_deps() worker_context = WorkerContext._for_job( - run_id=payload.run_id, + sample_id=payload.sample_id, task_id=payload.task_id, execution_id=payload.execution_id, definition_id=payload.definition_id, sandbox_id=payload.sandbox_id, task_mgmt=_task_management_service_for_context(ctx), task_inspect=TaskInspectionService(), - resource_service=RunResourceReadService(), + resource_service=SampleResourceReadService(), session_factory=get_session, ) @@ -97,7 +97,7 @@ async def run_worker_execute_job( dashboard_publisher = get_dashboard_event_publisher() execution_task_map = {payload.execution_id: payload.task_id} - async def _publish_context_event(event: RunContextEvent) -> None: + async def _publish_context_event(event: SampleContextEvent) -> None: dashboard_event = context_event_to_dashboard_event(event, execution_task_map) if dashboard_event is None: logger.warning( @@ -148,7 +148,7 @@ async def _publish_context_event(event: RunContextEvent) -> None: # else from the run-tier read boundary: # # WorkerOutput ← TaskExecutionService.load_worker_output(execution_id) - # live sandbox_id ← session.get(RunTaskExecution, ...).sandbox_id + # live sandbox_id ← session.get(SampleTaskAttempt, ...).sandbox_id # (then fed to load_task_view(..., sandbox_id=)) # # Both reads happen *after* the orchestrator's gather starts, so @@ -171,14 +171,14 @@ async def _publish_context_event(event: RunContextEvent) -> None: CompletedSpan( name="worker.execute", context=worker_execute_context( - payload.run_id, + payload.sample_id, payload.task_id, payload.execution_id, ), start_time=span_start, end_time=datetime.now(UTC), attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "task_id": str(payload.task_id), "execution_id": str(payload.execution_id), "sandbox_id": payload.sandbox_id, @@ -207,7 +207,7 @@ def _task_management_service_for_context(ctx: Any | None) -> TaskManagementServi class _ReadyDispatch(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID definition_id: UUID task_id: UUID @@ -236,7 +236,7 @@ def __init__(self, ctx: Any) -> None: async def spawn_dynamic_task( self, *, - run_id: UUID, + sample_id: UUID, parent_task_id: UUID, task: Task, depends_on: tuple[UUID, ...] = (), @@ -252,7 +252,7 @@ async def _run_spawn() -> _SpawnTaskStepResult: try: handle = await TaskManagementService.spawn_dynamic_task( self, - run_id=run_id, + sample_id=sample_id, parent_task_id=parent_task_id, task=task, depends_on=depends_on, @@ -271,18 +271,18 @@ async def _run_spawn() -> _SpawnTaskStepResult: async def _collect_ready_dispatch( self, - run_id: UUID, + sample_id: UUID, definition_id: UUID, task_id: UUID, ) -> None: if self._active_ready_dispatches is None: raise ContractViolationError( "Worker task-ready dispatch attempted outside a memoized graph mutation", - run_id=run_id, + sample_id=sample_id, task_id=task_id, ) self._active_ready_dispatches.append( - _ReadyDispatch(run_id=run_id, definition_id=definition_id, task_id=task_id) + _ReadyDispatch(sample_id=sample_id, definition_id=definition_id, task_id=task_id) ) async def _dispatch_collected_ready_events( @@ -292,7 +292,7 @@ async def _dispatch_collected_ready_events( ) -> None: for dispatch in ready: event = TaskReadyEvent( - run_id=dispatch.run_id, + sample_id=dispatch.sample_id, definition_id=dispatch.definition_id, task_id=dispatch.task_id, ) @@ -347,7 +347,7 @@ async def _persist_context_events( with get_session() as session: await context_event_repo.persist_chunk( session, - run_id=payload.run_id, + sample_id=payload.sample_id, execution_id=payload.execution_id, worker_binding_key=payload.assigned_worker_slug, chunk=chunk, diff --git a/ergon_core/ergon_core/core/jobs/workflow/complete/contract.py b/ergon_core/ergon_core/core/jobs/workflow/complete/contract.py index 047da92ec..d0d93ddef 100644 --- a/ergon_core/ergon_core/core/jobs/workflow/complete/contract.py +++ b/ergon_core/ergon_core/core/jobs/workflow/complete/contract.py @@ -8,7 +8,7 @@ class WorkflowCompleteResult(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID status: Literal["completed"] = "completed" final_score: float | None = None normalized_score: float | None = None diff --git a/ergon_core/ergon_core/core/jobs/workflow/complete/job.py b/ergon_core/ergon_core/core/jobs/workflow/complete/job.py index ab822fef2..60b9a710a 100644 --- a/ergon_core/ergon_core/core/jobs/workflow/complete/job.py +++ b/ergon_core/ergon_core/core/jobs/workflow/complete/job.py @@ -4,12 +4,12 @@ from datetime import UTC, datetime from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunRecord -from ergon_core.core.jobs.run.cleanup.contract import RunCleanupEvent +from ergon_core.core.persistence.telemetry.models import SampleRecord +from ergon_core.core.jobs.run.cleanup.contract import SampleCleanupEvent from .contract import WorkflowCompletedEvent, WorkflowCompleteResult from ergon_core.core.application.events.service import get_dashboard_event_publisher from ergon_core.core.application.runtime.orchestration import FinalizeWorkflowCommand -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from ergon_core.core.jobs._events import send_job_event from ergon_core.core.infrastructure.tracing import ( CompletedSpan, @@ -24,19 +24,19 @@ async def run_complete_workflow_job(payload: WorkflowCompletedEvent) -> WorkflowCompleteResult: - logger.info("workflow-complete run_id=%s", payload.run_id) + logger.info("workflow-complete sample_id=%s", payload.sample_id) span_start = datetime.now(UTC) svc = WorkflowService() finalized = svc.finalize( FinalizeWorkflowCommand( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, ) ) with get_session() as _session: - _run = _session.get(RunRecord, payload.run_id) + _run = _session.get(SampleRecord, payload.sample_id) _duration = ( (_run.completed_at - _run.started_at).total_seconds() if _run and _run.started_at and _run.completed_at @@ -44,7 +44,7 @@ async def run_complete_workflow_job(payload: WorkflowCompletedEvent) -> Workflow ) await get_dashboard_event_publisher().publish( DashboardWorkflowCompletedEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, status="completed", completed_at=utcnow(), duration_seconds=_duration, @@ -53,15 +53,15 @@ async def run_complete_workflow_job(payload: WorkflowCompletedEvent) -> Workflow ) await send_job_event( - RunCleanupEvent.name, - RunCleanupEvent( - run_id=payload.run_id, + SampleCleanupEvent.name, + SampleCleanupEvent( + sample_id=payload.sample_id, status="completed", ).model_dump(mode="json"), ) result = WorkflowCompleteResult( - run_id=payload.run_id, + sample_id=payload.sample_id, status="completed", final_score=finalized.final_score, normalized_score=finalized.normalized_score, @@ -72,11 +72,11 @@ async def run_complete_workflow_job(payload: WorkflowCompletedEvent) -> Workflow sink.emit_span( CompletedSpan( name="workflow.complete", - context=workflow_complete_context(payload.run_id), + context=workflow_complete_context(payload.sample_id), start_time=span_start, end_time=datetime.now(UTC), attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "definition_id": str(payload.definition_id), "final_score": finalized.final_score, "normalized_score": finalized.normalized_score, @@ -86,16 +86,16 @@ async def run_complete_workflow_job(payload: WorkflowCompletedEvent) -> Workflow ) with get_session() as session: - run = session.get(RunRecord, payload.run_id) + run = session.get(SampleRecord, payload.sample_id) if run and run.started_at and run.completed_at: sink.emit_span( CompletedSpan( name="workflow.execute", - context=workflow_root_context(payload.run_id), + context=workflow_root_context(payload.sample_id), start_time=run.started_at, end_time=run.completed_at, attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "definition_id": str(payload.definition_id), "status": run.status, "final_score": finalized.final_score, diff --git a/ergon_core/ergon_core/core/jobs/workflow/fail/contract.py b/ergon_core/ergon_core/core/jobs/workflow/fail/contract.py index 73804bcc5..3fa845969 100644 --- a/ergon_core/ergon_core/core/jobs/workflow/fail/contract.py +++ b/ergon_core/ergon_core/core/jobs/workflow/fail/contract.py @@ -8,6 +8,6 @@ class WorkflowFailedResult(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID status: Literal["failed"] = "failed" error: str | None = None diff --git a/ergon_core/ergon_core/core/jobs/workflow/fail/job.py b/ergon_core/ergon_core/core/jobs/workflow/fail/job.py index 31534c8d4..bdfa330f4 100644 --- a/ergon_core/ergon_core/core/jobs/workflow/fail/job.py +++ b/ergon_core/ergon_core/core/jobs/workflow/fail/job.py @@ -4,10 +4,10 @@ from datetime import UTC, datetime from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.infrastructure.inngest.errors import DataIntegrityError -from ergon_core.core.jobs.run.cleanup.contract import RunCleanupEvent +from ergon_core.core.jobs.run.cleanup.contract import SampleCleanupEvent from .contract import WorkflowFailedEvent, WorkflowFailedResult from ergon_core.core.jobs._events import send_job_event from ergon_core.core.infrastructure.tracing import ( @@ -23,30 +23,30 @@ async def run_fail_workflow_job(payload: WorkflowFailedEvent) -> WorkflowFailedResult: - logger.info("workflow-failed run_id=%s error=%s", payload.run_id, payload.error) + logger.info("workflow-failed sample_id=%s error=%s", payload.sample_id, payload.error) span_start = datetime.now(UTC) with get_session() as session: - run_record = session.get(RunRecord, payload.run_id) + run_record = session.get(SampleRecord, payload.sample_id) if run_record is None: - raise DataIntegrityError("RunRecord", payload.run_id) - run_record.status = RunStatus.FAILED + raise DataIntegrityError("SampleRecord", payload.sample_id) + run_record.status = SampleStatus.FAILED run_record.error_message = payload.error run_record.completed_at = utcnow() session.add(run_record) session.commit() await send_job_event( - RunCleanupEvent.name, - RunCleanupEvent( - run_id=payload.run_id, + SampleCleanupEvent.name, + SampleCleanupEvent( + sample_id=payload.sample_id, status="failed", error_message=payload.error, ).model_dump(mode="json"), ) result = WorkflowFailedResult( - run_id=payload.run_id, + sample_id=payload.sample_id, status="failed", error=payload.error, ) @@ -55,13 +55,13 @@ async def run_fail_workflow_job(payload: WorkflowFailedEvent) -> WorkflowFailedR sink.emit_span( CompletedSpan( name="workflow.failed", - context=workflow_failed_context(payload.run_id), + context=workflow_failed_context(payload.sample_id), start_time=span_start, end_time=datetime.now(UTC), status_code="error", status_message=truncate_text(payload.error), attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "definition_id": str(payload.definition_id), "error": truncate_text(payload.error), }, @@ -69,18 +69,18 @@ async def run_fail_workflow_job(payload: WorkflowFailedEvent) -> WorkflowFailedR ) with get_session() as session: - run = session.get(RunRecord, payload.run_id) + run = session.get(SampleRecord, payload.sample_id) if run and run.started_at and run.completed_at: sink.emit_span( CompletedSpan( name="workflow.execute", - context=workflow_root_context(payload.run_id), + context=workflow_root_context(payload.sample_id), start_time=run.started_at, end_time=run.completed_at, status_code="error", status_message=truncate_text(payload.error), attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "definition_id": str(payload.definition_id), "status": run.status, "error": truncate_text(payload.error), diff --git a/ergon_core/ergon_core/core/jobs/workflow/start/contract.py b/ergon_core/ergon_core/core/jobs/workflow/start/contract.py index 6360fc1d2..06a667d47 100644 --- a/ergon_core/ergon_core/core/jobs/workflow/start/contract.py +++ b/ergon_core/ergon_core/core/jobs/workflow/start/contract.py @@ -7,6 +7,6 @@ class WorkflowStartResult(BaseModel): model_config = {"frozen": True} - run_id: UUID + sample_id: UUID initial_ready_tasks: int = 0 total_tasks: int = 0 diff --git a/ergon_core/ergon_core/core/jobs/workflow/start/job.py b/ergon_core/ergon_core/core/jobs/workflow/start/job.py index 55ada8f74..265bc571c 100644 --- a/ergon_core/ergon_core/core/jobs/workflow/start/job.py +++ b/ergon_core/ergon_core/core/jobs/workflow/start/job.py @@ -7,7 +7,7 @@ from ergon_core.core.jobs.task.execute.contract import TaskReadyEvent from ergon_core.core.application.events.service import get_dashboard_event_publisher from ergon_core.core.application.runtime.orchestration import InitializeWorkflowCommand -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from ergon_core.core.jobs._events import send_job_events from ergon_core.core.infrastructure.tracing import ( CompletedSpan, @@ -16,19 +16,21 @@ ) from ergon_core.core.shared.utils import utcnow from ergon_core.core.views.dashboard_events.contracts import DashboardWorkflowStartedEvent -from ergon_core.core.views.runs.service import RunReadService +from ergon_core.core.views.samples.service import SampleSnapshotReadService logger = logging.getLogger(__name__) async def run_start_workflow_job(payload: WorkflowStartedEvent) -> WorkflowStartResult: - logger.info("workflow-start run_id=%s definition_id=%s", payload.run_id, payload.definition_id) + logger.info( + "workflow-start sample_id=%s definition_id=%s", payload.sample_id, payload.definition_id + ) span_start = datetime.now(UTC) svc = WorkflowService() initialized = await svc.initialize( InitializeWorkflowCommand( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, ) ) @@ -37,7 +39,7 @@ async def run_start_workflow_job(payload: WorkflowStartedEvent) -> WorkflowStart ( TaskReadyEvent.name, TaskReadyEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, task_id=td.task_id, ).model_dump(mode="json"), @@ -47,13 +49,13 @@ async def run_start_workflow_job(payload: WorkflowStartedEvent) -> WorkflowStart await send_job_events(events) - snapshot = RunReadService().build_run_snapshot(payload.run_id) + snapshot = SampleSnapshotReadService().build_snapshot(payload.sample_id) if snapshot is None: - raise RuntimeError(f"Run snapshot {payload.run_id} not found after workflow start") + raise RuntimeError(f"Run snapshot {payload.sample_id} not found after workflow start") await get_dashboard_event_publisher().publish( DashboardWorkflowStartedEvent( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, workflow_name=initialized.benchmark_type, snapshot=snapshot, @@ -64,7 +66,7 @@ async def run_start_workflow_job(payload: WorkflowStartedEvent) -> WorkflowStart ) result = WorkflowStartResult( - run_id=payload.run_id, + sample_id=payload.sample_id, initial_ready_tasks=len(initialized.initial_ready_tasks), total_tasks=initialized.total_tasks, ) @@ -72,11 +74,11 @@ async def run_start_workflow_job(payload: WorkflowStartedEvent) -> WorkflowStart get_trace_sink().emit_span( CompletedSpan( name="workflow.start", - context=workflow_start_context(payload.run_id), + context=workflow_start_context(payload.sample_id), start_time=span_start, end_time=datetime.now(UTC), attributes={ - "run_id": str(payload.run_id), + "sample_id": str(payload.sample_id), "definition_id": str(payload.definition_id), "total_tasks": initialized.total_tasks, "initial_ready_tasks": len(initialized.initial_ready_tasks), diff --git a/ergon_core/ergon_core/core/persistence/context/models.py b/ergon_core/ergon_core/core/persistence/context/models.py index 41b0b1069..01025bf74 100644 --- a/ergon_core/ergon_core/core/persistence/context/models.py +++ b/ergon_core/ergon_core/core/persistence/context/models.py @@ -1,5 +1,5 @@ # ergon_core/ergon_core/core/persistence/context/models.py -"""ORM model for run_context_events.""" +"""ORM model for sample_context_events.""" from datetime import UTC, datetime from typing import Any @@ -22,17 +22,17 @@ def _utcnow() -> datetime: _PAYLOAD_ADAPTER: TypeAdapter[ContextPartChunkLog] = TypeAdapter(ContextPartChunkLog) -class RunContextEvent(SQLModel, table=True): - __tablename__ = "run_context_events" +class SampleContextEvent(SQLModel, table=True): + __tablename__ = "sample_context_events" __table_args__ = ( sa.UniqueConstraint( - "task_execution_id", "sequence", name="uq_run_context_events_execution_sequence" + "task_execution_id", "sequence", name="uq_sample_context_events_execution_sequence" ), ) id: UUID = Field(default_factory=new_id, primary_key=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) - task_execution_id: UUID = Field(foreign_key="run_task_executions.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) + task_execution_id: UUID = Field(foreign_key="sample_task_attempts.id", index=True) worker_binding_key: str = Field(index=True) sequence: int event_type: str = Field( diff --git a/ergon_core/ergon_core/core/persistence/graph/models.py b/ergon_core/ergon_core/core/persistence/graph/models.py index 49e31b4c5..1cf7d7a0c 100644 --- a/ergon_core/ergon_core/core/persistence/graph/models.py +++ b/ergon_core/ergon_core/core/persistence/graph/models.py @@ -4,10 +4,10 @@ constrain values. Domain semantics live in the experiment layer. Tables: - run_graph_nodes — mutable task nodes, one per run - run_graph_edges — mutable dependency edges, one per run - run_graph_annotations — append-only namespaced metadata (WAL) - run_graph_mutations — append-only audit log of every change + sample_graph_nodes — mutable task nodes, one per run + sample_graph_edges — mutable dependency edges, one per run + sample_graph_annotations — append-only namespaced metadata (WAL) + sample_graph_mutations — append-only audit log of every change """ from datetime import datetime @@ -38,14 +38,14 @@ # --------------------------------------------------------------------------- -# RunGraphNode +# SampleGraphNode # --------------------------------------------------------------------------- -class RunGraphNode(SQLModel, table=True): - __tablename__ = "run_graph_nodes" +class SampleGraphNode(SQLModel, table=True): + __tablename__ = "sample_graph_nodes" - run_id: UUID = Field(foreign_key="runs.id", primary_key=True, index=True) + sample_id: UUID = Field(foreign_key="samples.id", primary_key=True, index=True) task_id: UUID = Field( default_factory=uuid4, primary_key=True, @@ -125,15 +125,15 @@ class RunGraphNode(SQLModel, table=True): # --------------------------------------------------------------------------- -# RunGraphEdge +# SampleGraphEdge # --------------------------------------------------------------------------- -class RunGraphEdge(SQLModel, table=True): - __tablename__ = "run_graph_edges" +class SampleGraphEdge(SQLModel, table=True): + __tablename__ = "sample_graph_edges" id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) definition_dependency_id: UUID | None = Field( default=None, foreign_key="experiment_definition_task_dependencies.id", @@ -150,11 +150,11 @@ class RunGraphEdge(SQLModel, table=True): # --------------------------------------------------------------------------- -# RunGraphAnnotation +# SampleGraphAnnotation # --------------------------------------------------------------------------- -class RunGraphAnnotation(SQLModel, table=True): +class SampleGraphAnnotation(SQLModel, table=True): """Append-only annotation WAL. Each set_annotation() inserts a new row. Current value = latest sequence. Point-in-time = sequence <= N. @@ -162,11 +162,11 @@ class RunGraphAnnotation(SQLModel, table=True): reconstructed at any mutation sequence — needed for counterfactual replay and credit assignment in the training pipeline.""" - __tablename__ = "run_graph_annotations" + __tablename__ = "sample_graph_annotations" __table_args__ = ( Index( "ix_annotation_lookup", - "run_id", + "sample_id", "target_type", "target_id", "namespace", @@ -175,7 +175,7 @@ class RunGraphAnnotation(SQLModel, table=True): ) id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) target_type: str = Field( description=( "GraphTargetType literal ('node' or 'edge') stored as a string for SQLModel " @@ -198,21 +198,21 @@ def _parse_payload(cls, data: dict) -> JsonObject: return data @model_validator(mode="after") - def _validate_payload(self) -> "RunGraphAnnotation": + def _validate_payload(self) -> "SampleGraphAnnotation": self.__class__._parse_payload(self.payload) return self # --------------------------------------------------------------------------- -# RunGraphMutation +# SampleGraphMutation # --------------------------------------------------------------------------- -class RunGraphMutation(SQLModel, table=True): - __tablename__ = "run_graph_mutations" +class SampleGraphMutation(SQLModel, table=True): + __tablename__ = "sample_graph_mutations" id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) sequence: int = Field(index=True) mutation_type: str = Field( index=True, @@ -231,7 +231,7 @@ class RunGraphMutation(SQLModel, table=True): reason: str | None = None triggered_by_mutation_id: UUID | None = Field( default=None, - foreign_key="run_graph_mutations.id", + foreign_key="sample_graph_mutations.id", ondelete="SET NULL", ) batch_operation_id: UUID | None = Field(default=None, index=False) diff --git a/ergon_core/ergon_core/core/persistence/shared/enums.py b/ergon_core/ergon_core/core/persistence/shared/enums.py index a7b0ffc30..591b80ff4 100644 --- a/ergon_core/ergon_core/core/persistence/shared/enums.py +++ b/ergon_core/ergon_core/core/persistence/shared/enums.py @@ -7,7 +7,7 @@ from enum import StrEnum -class RunStatus(StrEnum): +class SampleStatus(StrEnum): PENDING = "pending" EXECUTING = "executing" EVALUATING = "evaluating" @@ -16,7 +16,7 @@ class RunStatus(StrEnum): CANCELLED = "cancelled" -TERMINAL_RUN_STATUSES = {RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED} +TERMINAL_SAMPLE_STATUSES = {SampleStatus.COMPLETED, SampleStatus.FAILED, SampleStatus.CANCELLED} class TaskExecutionStatus(StrEnum): @@ -29,8 +29,8 @@ class TaskExecutionStatus(StrEnum): BLOCKED = "blocked" -class RunResourceKind(StrEnum): - """Canonical kinds for ``run_resources.kind``. +class SampleResourceKind(StrEnum): + """Canonical kinds for ``sample_resources.kind``. Stored as VARCHAR; enforced at the model/API boundary, not in the DB schema. Each kind documents the publisher that produces it so a new @@ -41,7 +41,7 @@ class RunResourceKind(StrEnum): """Explicit text artifact published by a worker/toolkit. Worker final assistant messages belong on - ``RunTaskExecution.final_assistant_message`` instead of this resource log. + ``SampleTaskAttempt.final_assistant_message`` instead of this resource log. """ REPORT = "report" @@ -57,4 +57,4 @@ class RunResourceKind(StrEnum): """Free-form scratch note written by an agent.""" IMPORT = "import" - """Copied snapshot materialized from another ``RunResource``.""" + """Copied snapshot materialized from another ``SampleResource``.""" diff --git a/ergon_core/ergon_core/core/persistence/telemetry/models.py b/ergon_core/ergon_core/core/persistence/telemetry/models.py index ef7985ec8..6bb82108b 100644 --- a/ergon_core/ergon_core/core/persistence/telemetry/models.py +++ b/ergon_core/ergon_core/core/persistence/telemetry/models.py @@ -10,7 +10,7 @@ import sqlalchemy as sa from ergon_core.core.shared.json_types import JsonObject from ergon_core.core.persistence.shared.enums import ( - RunStatus, + SampleStatus, TaskExecutionStatus, ) from ergon_core.core.shared.rollout_status import RolloutStatus @@ -26,12 +26,12 @@ # for table=True). Validators here protect the API/deserialization boundary. # --------------------------------------------------------------------------- -# RunRecord +# SampleRecord # --------------------------------------------------------------------------- -class RunRecord(SQLModel, table=True): - __tablename__ = "runs" +class SampleRecord(SQLModel, table=True): + __tablename__ = "samples" id: UUID = Field(default_factory=uuid4, primary_key=True) definition_id: UUID = Field( @@ -84,7 +84,7 @@ class RunRecord(SQLModel, table=True): "for grouping related runs, not a foreign key to a retired table." ), ) - status: RunStatus = Field(index=True) + status: SampleStatus = Field(index=True) error_message: str | None = None created_at: datetime = Field(default_factory=_utcnow, sa_type=TZDateTime) started_at: datetime | None = Field(default=None, sa_type=TZDateTime) @@ -112,31 +112,31 @@ def _parse_json_object(cls, data: dict, field_name: str) -> JsonObject: return data @model_validator(mode="after") - def _validate_fields(self) -> "RunRecord": + def _validate_fields(self) -> "SampleRecord": self.__class__._parse_json_object(self.worker_team_json, "worker_team_json") self.__class__._parse_json_object(self.dependency_extras_json, "dependency_extras_json") self.__class__._parse_json_object(self.assignment_json, "assignment_json") self.__class__._parse_json_object(self.summary_json, "summary_json") try: - RunStatus(self.status) + SampleStatus(self.status) except ValueError: raise ValueError( - f"{self.status!r} is not a valid RunStatus; " - f"valid values: {[e.value for e in RunStatus]}" + f"{self.status!r} is not a valid SampleStatus; " + f"valid values: {[e.value for e in SampleStatus]}" ) return self # --------------------------------------------------------------------------- -# RunTaskExecution +# SampleTaskAttempt # --------------------------------------------------------------------------- -class RunTaskExecution(SQLModel, table=True): - __tablename__ = "run_task_executions" +class SampleTaskAttempt(SQLModel, table=True): + __tablename__ = "sample_task_attempts" id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) task_id: UUID = Field(index=True) definition_worker_id: UUID | None = Field( default=None, @@ -157,7 +157,7 @@ class RunTaskExecution(SQLModel, table=True): # stuffs it onto `worker_result`, but every other context capability # (`run_command`, `read_resource`, ...) is fetched lazily by the # criterion. When that redesign lands, this column may live in a - # dedicated output store rather than on `RunTaskExecution`. + # dedicated output store rather than on `SampleTaskAttempt`. worker_output_json: dict | None = Field(default=None, sa_column=Column(JSON)) # -- JSON accessor: output_json -- @@ -187,10 +187,10 @@ def _parse_error(cls, data: dict | None) -> JsonObject | None: def validate_identity(self) -> None: """Require enough identity to map execution rows to a static or dynamic task.""" if self.task_id is None: - raise ValueError("RunTaskExecution requires task_id") + raise ValueError("SampleTaskAttempt requires task_id") @model_validator(mode="after") - def _validate_fields(self) -> "RunTaskExecution": + def _validate_fields(self) -> "SampleTaskAttempt": self.__class__._parse_output(self.output_json) self.__class__._parse_error(self.error_json) self.validate_identity() @@ -205,22 +205,22 @@ def _validate_fields(self) -> "RunTaskExecution": # --------------------------------------------------------------------------- -# RunResource +# SampleResource # --------------------------------------------------------------------------- -class RunResource(SQLModel, table=True): - __tablename__ = "run_resources" +class SampleResource(SQLModel, table=True): + __tablename__ = "sample_resources" id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) task_execution_id: UUID | None = Field( default=None, - foreign_key="run_task_executions.id", + foreign_key="sample_task_attempts.id", ) kind: str = Field( default="output", - description="Canonical artifact kind from shared RunResourceKind.", + description="Canonical artifact kind from shared SampleResourceKind.", ) name: str mime_type: str @@ -234,7 +234,7 @@ class RunResource(SQLModel, table=True): content_hash: str | None = Field(default=None, index=True) copied_from_resource_id: UUID | None = Field( default=None, - foreign_key="run_resources.id", + foreign_key="sample_resources.id", index=True, ) @@ -250,32 +250,32 @@ def _parse_metadata(cls, data: dict) -> JsonObject: return data @model_validator(mode="after") - def _validate_fields(self) -> "RunResource": - from ergon_core.core.persistence.shared.enums import RunResourceKind + def _validate_fields(self) -> "SampleResource": + from ergon_core.core.persistence.shared.enums import SampleResourceKind self.__class__._parse_metadata(self.metadata_json) try: - RunResourceKind(self.kind) + SampleResourceKind(self.kind) except ValueError: raise ValueError( - f"{self.kind!r} is not a valid RunResourceKind; " - f"valid values: {[e.value for e in RunResourceKind]}" + f"{self.kind!r} is not a valid SampleResourceKind; " + f"valid values: {[e.value for e in SampleResourceKind]}" ) return self # --------------------------------------------------------------------------- -# RunTaskEvaluation +# SampleTaskEvaluation # --------------------------------------------------------------------------- -class RunTaskEvaluation(SQLModel, table=True): - __tablename__ = "run_task_evaluations" +class SampleTaskEvaluation(SQLModel, table=True): + __tablename__ = "sample_task_evaluations" id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) task_execution_id: UUID = Field( - foreign_key="run_task_executions.id", + foreign_key="sample_task_attempts.id", index=True, ) task_id: UUID = Field(index=True) @@ -290,7 +290,7 @@ class RunTaskEvaluation(SQLModel, table=True): created_at: datetime = Field(default_factory=_utcnow, sa_type=TZDateTime) @model_validator(mode="after") - def _validate_summary_json(self) -> "RunTaskEvaluation": + def _validate_summary_json(self) -> "SampleTaskEvaluation": if not isinstance(self.summary_json, dict): raise ValueError(f"summary_json must be a dict, got {type(self.summary_json).__name__}") return self @@ -303,10 +303,10 @@ def _validate_summary_json(self) -> "RunTaskEvaluation": class Thread(SQLModel, table=True): __tablename__ = "threads" - __table_args__ = (sa.UniqueConstraint("run_id", "topic", name="uq_threads_run_topic"),) + __table_args__ = (sa.UniqueConstraint("sample_id", "topic", name="uq_threads_run_topic"),) id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) topic: str summary: str | None = None agent_a_id: str = Field(index=True) @@ -325,10 +325,10 @@ class ThreadMessage(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) thread_id: UUID = Field(foreign_key="threads.id", index=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) task_execution_id: UUID | None = Field( default=None, - foreign_key="run_task_executions.id", + foreign_key="sample_task_attempts.id", index=True, ) from_agent_id: str @@ -376,7 +376,7 @@ class RolloutBatchRun(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) batch_id: UUID = Field(foreign_key="rollout_batches.id", index=True) - run_id: UUID = Field(foreign_key="runs.id", index=True) + sample_id: UUID = Field(foreign_key="samples.id", index=True) # --------------------------------------------------------------------------- @@ -387,16 +387,16 @@ class RolloutBatchRun(SQLModel, table=True): class SandboxCommandWalEntry(SQLModel, table=True): """One row per bash command emitted by ``SandboxEventSink.sandbox_command``. - ``run_id`` is indexed but carries no FK constraint — the sandbox.closed - synthetic WAL entry may arrive with run_id=task_id due to a pre-existing + ``sample_id`` is indexed but carries no FK constraint — the sandbox.closed + synthetic WAL entry may arrive with sample_id=task_id due to a pre-existing quirk in the manager's teardown sequence. Queries should filter by - run_id; rows with an unexpected run_id will simply not appear. + sample_id; rows with an unexpected sample_id will simply not appear. """ __tablename__ = "sandbox_command_wal_entries" id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(index=True) + sample_id: UUID = Field(index=True) task_id: UUID = Field(index=True) sandbox_id: str = Field(index=True) command: str @@ -416,14 +416,14 @@ class SandboxEvent(SQLModel, table=True): """One row per sandbox lifecycle event emitted by ``SandboxEventSink``. ``kind`` is one of ``"sandbox_created"`` or ``"sandbox_closed"``. - ``run_id`` carries no FK — same teardown-sequence caveat as + ``sample_id`` carries no FK — same teardown-sequence caveat as ``SandboxCommandWalEntry``. """ __tablename__ = "sandbox_events" id: UUID = Field(default_factory=uuid4, primary_key=True) - run_id: UUID = Field(index=True) + sample_id: UUID = Field(index=True) task_id: UUID = Field(index=True) sandbox_id: str = Field(index=True) kind: str diff --git a/ergon_core/ergon_core/core/rl/__init__.py b/ergon_core/ergon_core/core/rl/__init__.py index f8a44ac4c..9b11f93ed 100644 --- a/ergon_core/ergon_core/core/rl/__init__.py +++ b/ergon_core/ergon_core/core/rl/__init__.py @@ -5,7 +5,7 @@ Core components: -- ``extraction``: per-agent trajectory extraction from RunContextEvent rows +- ``extraction``: per-agent trajectory extraction from SampleContextEvent rows - ``rewards``: reward strategies for per-agent credit assignment - ``rollout_service``: service client for managed rollout execution """ diff --git a/ergon_core/ergon_core/core/rl/extraction.py b/ergon_core/ergon_core/core/rl/extraction.py index 23eb165b9..18cd55064 100644 --- a/ergon_core/ergon_core/core/rl/extraction.py +++ b/ergon_core/ergon_core/core/rl/extraction.py @@ -1,5 +1,5 @@ # ergon_core/ergon_core/core/rl/extraction.py -"""Per-agent trajectory extraction from RunContextEvent rows. +"""Per-agent trajectory extraction from SampleContextEvent rows. Reads the lossless per-event records and builds the flat (prompt_ids, completion_ids, logprobs, env_mask, reward) tuples that @@ -23,7 +23,7 @@ ToolResultPart, UserMessagePart, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.rl.rewards import IndependentTaskReward, RewardStrategy from pydantic import BaseModel, Field @@ -48,7 +48,7 @@ class AgentTrajectory(BaseModel): def extract_agent_trajectories( - context_events: list[RunContextEvent], + context_events: list[SampleContextEvent], eval_scores: dict[str, float], tokenizer: Tokenizer, *, @@ -62,7 +62,7 @@ def extract_agent_trajectories( if reward_strategy is None: reward_strategy = IndependentTaskReward() - by_worker: dict[str, list[RunContextEvent]] = defaultdict(list) + by_worker: dict[str, list[SampleContextEvent]] = defaultdict(list) for event in context_events: by_worker[event.worker_binding_key].append(event) @@ -115,7 +115,7 @@ def extract_agent_trajectories( return trajectories -def _build_prompt_text(events: list[RunContextEvent]) -> str: +def _build_prompt_text(events: list[SampleContextEvent]) -> str: parts: list[str] = [] for event in events: payload = event.parsed_payload() @@ -157,7 +157,7 @@ def _get_logprobs(parsed: ContextPartChunkLog, n_tokens: int) -> list[float]: return scalars[:n_tokens] -def _count_turns(events: list[RunContextEvent]) -> int: +def _count_turns(events: list[SampleContextEvent]) -> int: seen: set[str] = set() for event in events: parsed = event.parsed_payload() diff --git a/ergon_core/ergon_core/core/rl/rollout_service.py b/ergon_core/ergon_core/core/rl/rollout_service.py index 813f32430..ab2d34238 100644 --- a/ergon_core/ergon_core/core/rl/rollout_service.py +++ b/ergon_core/ergon_core/core/rl/rollout_service.py @@ -12,19 +12,19 @@ from uuid import UUID, uuid4 import inngest -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.persistence.definitions.models import ExperimentDefinition from ergon_core.core.persistence.shared.enums import ( - TERMINAL_RUN_STATUSES, - RunStatus, + TERMINAL_SAMPLE_STATUSES, + SampleStatus, ) from ergon_core.core.persistence.shared.ids import new_id from ergon_core.core.persistence.telemetry.models import ( RolloutBatch, RolloutBatchRun, - RunRecord, - RunTaskEvaluation, - RunTaskExecution, + SampleRecord, + SampleTaskEvaluation, + SampleTaskAttempt, ) from ergon_core.core.rl.extraction import ( Tokenizer, @@ -49,7 +49,7 @@ class RolloutService: """Orchestrate rollout batches: create runs, fire events, poll, extract. Lifecycle: - 1. Trainer calls ``submit()`` → RunRecords + RolloutBatch created, Inngest events fired + 1. Trainer calls ``submit()`` → SampleRecords + RolloutBatch created, Inngest events fired 2. Trainer polls ``poll()`` → returns RUNNING until all episodes finish 3. When all terminal → ``poll()`` extracts trajectories and returns COMPLETE @@ -79,9 +79,9 @@ def _get_tokenizer(self) -> Tokenizer: return self._tokenizer def submit(self, request: SubmitRequest) -> SubmitResponse: - """Create RunRecords, RolloutBatch, and fire Inngest workflow/started events.""" + """Create SampleRecords, RolloutBatch, and fire Inngest workflow/started events.""" batch_id = uuid4() - run_ids: list[UUID] = [] + sample_ids: list[UUID] = [] with self._session_factory() as session: definition = session.get(ExperimentDefinition, request.definition_id) @@ -95,35 +95,35 @@ def submit(self, request: SubmitRequest) -> SubmitResponse: ) for index in range(request.num_episodes): - run_id = new_id() + sample_id = new_id() session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=request.definition_id, benchmark_type=benchmark_type, instance_key=f"episode-{index}", worker_team_json={"primary": "rl-rollout"}, model_target=request.model_target_override, - status=RunStatus.PENDING, + status=SampleStatus.PENDING, ) ) session.add( RolloutBatchRun( id=new_id(), batch_id=batch_id, - run_id=run_id, + sample_id=sample_id, ) ) - run_ids.append(run_id) + sample_ids.append(sample_id) session.commit() - for run_id in run_ids: + for sample_id in sample_ids: self._inngest_send( inngest.Event( name=WorkflowStartedEvent.name, data=WorkflowStartedEvent( - run_id=run_id, + sample_id=sample_id, definition_id=request.definition_id, ).model_dump(mode="json"), ) @@ -137,7 +137,7 @@ def submit(self, request: SubmitRequest) -> SubmitResponse: ) return SubmitResponse( batch_id=batch_id, - run_ids=run_ids, + sample_ids=sample_ids, status=BatchStatus.PENDING, ) @@ -153,9 +153,9 @@ def poll(self, batch_id: UUID) -> PollResponse | None: select(RolloutBatchRun).where(RolloutBatchRun.batch_id == batch_id) ).all() ) - run_ids = [br.run_id for br in batch_runs] + sample_ids = [br.sample_id for br in batch_runs] - if not run_ids: + if not sample_ids: return PollResponse( batch_id=batch_id, status=BatchStatus.COMPLETE, @@ -163,36 +163,36 @@ def poll(self, batch_id: UUID) -> PollResponse | None: runs = list( session.exec( - select(RunRecord).where( - RunRecord.id.in_(run_ids) # type: ignore[union-attr] + select(SampleRecord).where( + SampleRecord.id.in_(sample_ids) # type: ignore[union-attr] ) ).all() ) - terminal = set(TERMINAL_RUN_STATUSES) + terminal = set(TERMINAL_SAMPLE_STATUSES) completed_ids: list[UUID] = [] failed_ids: list[UUID] = [] for run in runs: if run.status not in terminal: continue - if run.status == RunStatus.COMPLETED: + if run.status == SampleStatus.COMPLETED: completed_ids.append(run.id) else: failed_ids.append(run.id) total_terminal = len(completed_ids) + len(failed_ids) - if total_terminal < len(run_ids): + if total_terminal < len(sample_ids): return PollResponse( batch_id=batch_id, status=BatchStatus.RUNNING, completed=len(completed_ids), - total=len(run_ids), + total=len(sample_ids), ) trajectories = self._extract_trajectories(completed_ids) failures = [ - EpisodeFailure(run_id=rid, error="episode failed or timed out") for rid in failed_ids + EpisodeFailure(sample_id=rid, error="episode failed or timed out") for rid in failed_ids ] with self._session_factory() as session: @@ -212,7 +212,7 @@ def poll(self, batch_id: UUID) -> PollResponse | None: batch_id=batch_id, status=BatchStatus.COMPLETE, completed=len(completed_ids), - total=len(run_ids), + total=len(sample_ids), trajectories=trajectories, failures=failures, ) @@ -229,84 +229,86 @@ def cancel(self, batch_id: UUID) -> None: select(RolloutBatchRun).where(RolloutBatchRun.batch_id == batch_id) ).all() ) - run_ids = [br.run_id for br in batch_runs] + sample_ids = [br.sample_id for br in batch_runs] - if run_ids: + if sample_ids: runs = list( session.exec( - select(RunRecord).where( - RunRecord.id.in_(run_ids) # type: ignore[union-attr] + select(SampleRecord).where( + SampleRecord.id.in_(sample_ids) # type: ignore[union-attr] ) ).all() ) for run in runs: - if run.status not in set(TERMINAL_RUN_STATUSES): - run.status = RunStatus.CANCELLED + if run.status not in set(TERMINAL_SAMPLE_STATUSES): + run.status = SampleStatus.CANCELLED session.add(run) batch.status = BatchStatus.CANCELLED session.add(batch) session.commit() - def _extract_trajectories(self, run_ids: list[UUID]) -> list[Trajectory]: + def _extract_trajectories(self, sample_ids: list[UUID]) -> list[Trajectory]: """Load context events + evals from DB, run extraction, build Trajectory list.""" with self._session_factory() as session: all_events = list( session.exec( - select(RunContextEvent) - .where(RunContextEvent.run_id.in_(run_ids)) # type: ignore[union-attr] + select(SampleContextEvent) + .where(SampleContextEvent.sample_id.in_(sample_ids)) # type: ignore[union-attr] .order_by( - RunContextEvent.run_id, - RunContextEvent.task_execution_id, - RunContextEvent.sequence, + SampleContextEvent.sample_id, + SampleContextEvent.task_execution_id, + SampleContextEvent.sequence, ) ).all() ) all_evals = list( session.exec( - select(RunTaskEvaluation).where(RunTaskEvaluation.run_id.in_(run_ids)) # type: ignore[union-attr] + select(SampleTaskEvaluation).where( + SampleTaskEvaluation.sample_id.in_(sample_ids) + ) # type: ignore[union-attr] ).all() ) all_execs = list( session.exec( - select(RunTaskExecution).where(RunTaskExecution.run_id.in_(run_ids)) # type: ignore[union-attr] + select(SampleTaskAttempt).where(SampleTaskAttempt.sample_id.in_(sample_ids)) # type: ignore[union-attr] ).all() ) - events_by_run: dict[UUID, list[RunContextEvent]] = defaultdict(list) + events_by_run: dict[UUID, list[SampleContextEvent]] = defaultdict(list) for event in all_events: - events_by_run[event.run_id].append(event) + events_by_run[event.sample_id].append(event) evals_by_run: dict[UUID, dict[str, float]] = defaultdict(dict) for ev in all_evals: if ev.score is not None: - evals_by_run[ev.run_id][str(ev.task_id)] = ev.score + evals_by_run[ev.sample_id][str(ev.task_id)] = ev.score exec_to_def_task: dict[str, str] = {} for ex in all_execs: exec_to_def_task[str(ex.id)] = str(ex.task_id) evals_remapped: dict[UUID, dict[str, float]] = defaultdict(dict) - for run_id, scores in evals_by_run.items(): + for sample_id, scores in evals_by_run.items(): for def_task_id, score in scores.items(): for exec_id, mapped_def_id in exec_to_def_task.items(): if mapped_def_id == def_task_id: - evals_remapped[run_id][exec_id] = score + evals_remapped[sample_id][exec_id] = score result: list[Trajectory] = [] tokenizer = self._get_tokenizer() - for run_id in run_ids: - run_events = events_by_run.get(run_id, []) + for sample_id in sample_ids: + run_events = events_by_run.get(sample_id, []) agent_trajs = extract_agent_trajectories( run_events, - evals_remapped.get(run_id, {}), + evals_remapped.get(sample_id, {}), tokenizer, reward_strategy=self._reward_strategy, ) for traj in agent_trajs: result.append( Trajectory( - run_id=run_id, + sample_id=sample_id, agent_id=traj.agent_id, prompt_ids=traj.prompt_ids, completion_ids=traj.completion_ids, diff --git a/ergon_core/ergon_core/core/rl/rollout_types.py b/ergon_core/ergon_core/core/rl/rollout_types.py index 53c442e5a..481413acb 100644 --- a/ergon_core/ergon_core/core/rl/rollout_types.py +++ b/ergon_core/ergon_core/core/rl/rollout_types.py @@ -23,7 +23,7 @@ class SubmitResponse(BaseModel): """Ergon → Trainer: batch accepted.""" batch_id: UUID - run_ids: list[UUID] + sample_ids: list[UUID] status: BatchStatus = BatchStatus.PENDING @@ -33,7 +33,7 @@ class Trajectory(BaseModel): Maps 1:1 to AgentTrajectory from extraction.py, plus metadata. """ - run_id: UUID + sample_id: UUID agent_id: str prompt_ids: list[int] completion_ids: list[int] @@ -46,7 +46,7 @@ class Trajectory(BaseModel): class EpisodeFailure(BaseModel): """An episode that didn't complete successfully.""" - run_id: UUID + sample_id: UUID error: str diff --git a/ergon_core/ergon_core/core/views/dashboard_events/context_events.py b/ergon_core/ergon_core/core/views/dashboard_events/context_events.py index 59b8007d0..72e9cf4f2 100644 --- a/ergon_core/ergon_core/core/views/dashboard_events/context_events.py +++ b/ergon_core/ergon_core/core/views/dashboard_events/context_events.py @@ -3,13 +3,13 @@ from typing import cast from uuid import UUID -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.shared.context_parts import ContextEventType from ergon_core.core.views.dashboard_events.contracts import DashboardContextEventEvent def context_event_to_dashboard_event( - event: RunContextEvent, + event: SampleContextEvent, execution_task_map: dict[UUID, UUID], ) -> DashboardContextEventEvent | None: task_id = execution_task_map.get(event.task_execution_id) @@ -17,7 +17,7 @@ def context_event_to_dashboard_event( return None return DashboardContextEventEvent( id=event.id, - run_id=event.run_id, + sample_id=event.sample_id, task_execution_id=event.task_execution_id, task_id=task_id, worker_binding_key=event.worker_binding_key, diff --git a/ergon_core/ergon_core/core/views/dashboard_events/contracts.py b/ergon_core/ergon_core/core/views/dashboard_events/contracts.py index dd1aaaf9d..09e68e0f0 100644 --- a/ergon_core/ergon_core/core/views/dashboard_events/contracts.py +++ b/ergon_core/ergon_core/core/views/dashboard_events/contracts.py @@ -11,11 +11,11 @@ from typing import ClassVar from uuid import UUID -from ergon_core.core.views.runs.models import ( +from ergon_core.core.views.samples.models import ( RunCommunicationMessageDto, RunCommunicationThreadDto, - RunSnapshotDto, - RunTaskEvaluationDto, + SampleSnapshotDto, + SampleTaskEvaluationDto, ) from ergon_core.core.shared.context_parts import ContextEventType, ContextPartChunkLog from ergon_core.core.application.events.base import InngestEventContract @@ -31,10 +31,10 @@ class DashboardWorkflowStartedEvent(InngestEventContract): name: ClassVar[str] = "dashboard/workflow.started" - run_id: UUID + sample_id: UUID definition_id: UUID workflow_name: str - snapshot: RunSnapshotDto + snapshot: SampleSnapshotDto started_at: datetime total_tasks: int total_leaf_tasks: int @@ -43,7 +43,7 @@ class DashboardWorkflowStartedEvent(InngestEventContract): class DashboardWorkflowCompletedEvent(InngestEventContract): name: ClassVar[str] = "dashboard/workflow.completed" - run_id: UUID + sample_id: UUID status: str completed_at: datetime duration_seconds: float @@ -59,7 +59,7 @@ class DashboardWorkflowCompletedEvent(InngestEventContract): class DashboardTaskStatusChangedEvent(InngestEventContract): name: ClassVar[str] = "dashboard/task.status_changed" - run_id: UUID + sample_id: UUID task_id: UUID task_name: str parent_task_id: UUID | None = None @@ -72,13 +72,13 @@ class DashboardTaskStatusChangedEvent(InngestEventContract): class DashboardTaskEvaluationUpdatedEvent(InngestEventContract): - """Embeds the full RunTaskEvaluationDto as ``evaluation``.""" + """Embeds the full SampleTaskEvaluationDto as ``evaluation``.""" name: ClassVar[str] = "dashboard/task.evaluation_updated" - run_id: UUID + sample_id: UUID task_id: UUID - evaluation: RunTaskEvaluationDto + evaluation: SampleTaskEvaluationDto # --------------------------------------------------------------------------- @@ -89,7 +89,7 @@ class DashboardTaskEvaluationUpdatedEvent(InngestEventContract): class DashboardResourcePublishedEvent(InngestEventContract): name: ClassVar[str] = "dashboard/resource.published" - run_id: UUID + sample_id: UUID task_id: UUID task_execution_id: UUID resource_id: UUID @@ -108,7 +108,7 @@ class DashboardResourcePublishedEvent(InngestEventContract): class DashboardSandboxCreatedEvent(InngestEventContract): name: ClassVar[str] = "dashboard/sandbox.created" - run_id: UUID + sample_id: UUID task_id: UUID sandbox_id: str template: str | None = None @@ -119,7 +119,7 @@ class DashboardSandboxCreatedEvent(InngestEventContract): class DashboardSandboxCommandEvent(InngestEventContract): name: ClassVar[str] = "dashboard/sandbox.command" - run_id: UUID + sample_id: UUID task_id: UUID sandbox_id: str command: str @@ -149,7 +149,7 @@ class DashboardThreadMessageCreatedEvent(InngestEventContract): name: ClassVar[str] = "dashboard/thread.message_created" - run_id: UUID + sample_id: UUID thread: RunCommunicationThreadDto message: RunCommunicationMessageDto @@ -169,9 +169,9 @@ class DashboardContextEventEvent(InngestEventContract): name: ClassVar[str] = "dashboard/context.event" id: UUID = Field( - description="RunContextEvent.id used by the frontend as a stable deduplication key." + description="SampleContextEvent.id used by the frontend as a stable deduplication key." ) - run_id: UUID + sample_id: UUID task_execution_id: UUID task_id: UUID = Field( description=( diff --git a/ergon_core/ergon_core/core/views/dashboard_events/graph_mutations.py b/ergon_core/ergon_core/core/views/dashboard_events/graph_mutations.py index 18739ffdc..59f3c5101 100644 --- a/ergon_core/ergon_core/core/views/dashboard_events/graph_mutations.py +++ b/ergon_core/ergon_core/core/views/dashboard_events/graph_mutations.py @@ -9,16 +9,16 @@ from ergon_core.core.persistence.graph.models import ( GraphTargetType, MutationType, - RunGraphMutation, + SampleGraphMutation, ) from ergon_core.core.persistence.shared.types import RunId from ergon_core.core.views.dashboard_events.contracts import DashboardGraphMutationEvent -def graph_mutation_record_from_row(row: RunGraphMutation) -> GraphMutationRecordDto: +def graph_mutation_record_from_row(row: SampleGraphMutation) -> GraphMutationRecordDto: return GraphMutationRecordDto( id=row.id, - run_id=cast(RunId, row.run_id), + sample_id=cast(RunId, row.sample_id), sequence=row.sequence, mutation_type=cast(MutationType, row.mutation_type), target_type=cast(GraphTargetType, row.target_type), @@ -32,6 +32,6 @@ def graph_mutation_record_from_row(row: RunGraphMutation) -> GraphMutationRecord def dashboard_graph_mutation_event_from_row( - row: RunGraphMutation, + row: SampleGraphMutation, ) -> DashboardGraphMutationEvent: return DashboardGraphMutationEvent(mutation=graph_mutation_record_from_row(row)) diff --git a/ergon_core/ergon_core/core/views/experiments/models.py b/ergon_core/ergon_core/core/views/experiments/models.py index e40d1975b..cf66927e3 100644 --- a/ergon_core/ergon_core/core/views/experiments/models.py +++ b/ergon_core/ergon_core/core/views/experiments/models.py @@ -40,7 +40,7 @@ class ExperimentSummaryDto(BaseModel): class ExperimentRunMetricsDto(BaseModel): - run_id: UUID + sample_id: UUID run_name: str | None = None status: str sample_label: str | None = None @@ -60,7 +60,7 @@ class ExperimentRunMetricsDto(BaseModel): class ExperimentRunRowDto(BaseModel): - run_id: UUID + sample_id: UUID definition_id: UUID benchmark_type: str instance_key: str diff --git a/ergon_core/ergon_core/core/views/experiments/service.py b/ergon_core/ergon_core/core/views/experiments/service.py index 8d6e14f8c..e292d2daf 100644 --- a/ergon_core/ergon_core/core/views/experiments/service.py +++ b/ergon_core/ergon_core/core/views/experiments/service.py @@ -12,15 +12,15 @@ ExperimentSummaryDto, ExperimentTagDefinitionDto, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.persistence.definitions.models import ( ExperimentDefinition, ExperimentDefinitionInstance, ) -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunRecord -from ergon_core.core.views.runs.metrics import aggregate_run_metrics +from ergon_core.core.persistence.telemetry.models import SampleRecord +from ergon_core.core.views.samples.metrics import aggregate_run_metrics from sqlmodel import Session, col, select @@ -60,7 +60,7 @@ def distinct_tags(self) -> list[str]: with get_session() as session: tags = { tag - for tag in session.exec(select(RunRecord.experiment)).all() + for tag in session.exec(select(SampleRecord.experiment)).all() if isinstance(tag, str) and tag } return sorted(tags) @@ -69,12 +69,12 @@ def definitions_by_tag(self, tag: str) -> list[ExperimentTagDefinitionDto]: with get_session() as session: runs = list( session.exec( - select(RunRecord) - .where(RunRecord.experiment == tag) - .order_by(col(RunRecord.created_at).desc()) + select(SampleRecord) + .where(SampleRecord.experiment == tag) + .order_by(col(SampleRecord.created_at).desc()) ).all() ) - latest_by_definition: dict[UUID, RunRecord] = {} + latest_by_definition: dict[UUID, SampleRecord] = {} for run in runs: latest_by_definition.setdefault(run.definition_id, run) @@ -98,13 +98,13 @@ def _definition_summary( session: Session, definition: ExperimentDefinition, *, - runs: list[RunRecord] | None = None, + runs: list[SampleRecord] | None = None, ) -> ExperimentSummaryDto: """Build a summary DTO from an ``ExperimentDefinition`` row. Identity fields (``name``/``description``/``benchmark_type``/``created_by``) come directly from the columns Task 1 added. Run / sample bookkeeping is - derived: ``RunRecord.experiment`` indexes grouped runs, and + derived: ``SampleRecord.experiment`` indexes grouped runs, and ``ExperimentDefinitionInstance`` rows index instances. """ runs = runs if runs is not None else _runs_for_definition_view(session, definition) @@ -147,7 +147,7 @@ def _definition_detail( ) -> ExperimentDetailDto: """Build a detail DTO from an ``ExperimentDefinition`` row.""" runs = _runs_for_definition_view(session, definition) - task_counts = _task_counts_by_run(session, [run.id for run in runs]) + task_counts = _task_counts_by_sample(session, [run.id for run in runs]) context_events = _context_events_by_run(session, [run.id for run in runs]) run_rows = [ _run_row( @@ -174,12 +174,14 @@ def _definition_detail( def _runs_for_definition_view( session: Session, definition: ExperimentDefinition, -) -> list[RunRecord]: +) -> list[SampleRecord]: experiment = optional_str_metadata(definition.parsed_metadata(), "experiment") if experiment: - return list(session.exec(select(RunRecord).where(RunRecord.experiment == experiment)).all()) + return list( + session.exec(select(SampleRecord).where(SampleRecord.experiment == experiment)).all() + ) return list( - session.exec(select(RunRecord).where(RunRecord.definition_id == definition.id)).all() + session.exec(select(SampleRecord).where(SampleRecord.definition_id == definition.id)).all() ) @@ -196,10 +198,10 @@ def _instance_count(session: Session, definition_id: UUID) -> int: def _run_row( - run: RunRecord, + run: SampleRecord, *, total_tasks: int | None = None, - context_events: list[RunContextEvent], + context_events: list[SampleContextEvent], ) -> ExperimentRunRowDto: summary = run.parsed_summary() duration_ms = _duration_ms(run) @@ -207,7 +209,7 @@ def _run_row( error_summary = _error_summary(run, summary) aggregated = aggregate_run_metrics(context_events, summary=summary) return ExperimentRunRowDto( - run_id=run.id, + sample_id=run.id, definition_id=run.definition_id, benchmark_type=run.benchmark_type, instance_key=run.instance_key, @@ -225,7 +227,7 @@ def _run_row( total_cost_usd=aggregated.total_cost_usd, error_message=error_summary, metrics=ExperimentRunMetricsDto( - run_id=run.id, + sample_id=run.id, run_name=run.sample_id or run.instance_key, status=str(run.status), sample_label=run.sample_id, @@ -246,28 +248,34 @@ def _run_row( ) -def _task_counts_by_run(session: Session, run_ids: list[UUID]) -> dict[UUID, int]: +def _task_counts_by_sample(session: Session, sample_ids: list[UUID]) -> dict[UUID, int]: return { - run_id: len( - list(session.exec(select(RunGraphNode.task_id).where(RunGraphNode.run_id == run_id))) + sample_id: len( + list( + session.exec( + select(SampleGraphNode.task_id).where(SampleGraphNode.sample_id == sample_id) + ) + ) ) - for run_id in run_ids + for sample_id in sample_ids } def _context_events_by_run( session: Session, - run_ids: list[UUID], -) -> dict[UUID, list[RunContextEvent]]: - if not run_ids: + sample_ids: list[UUID], +) -> dict[UUID, list[SampleContextEvent]]: + if not sample_ids: return {} rows = list( - session.exec(select(RunContextEvent).where(col(RunContextEvent.run_id).in_(run_ids))) + session.exec( + select(SampleContextEvent).where(col(SampleContextEvent.sample_id).in_(sample_ids)) + ) ) - result: dict[UUID, list[RunContextEvent]] = {run_id: [] for run_id in run_ids} + result: dict[UUID, list[SampleContextEvent]] = {sample_id: [] for sample_id in sample_ids} for row in rows: - result.setdefault(row.run_id, []).append(row) + result.setdefault(row.sample_id, []).append(row) return result @@ -360,7 +368,7 @@ def _rounded_average(values: list[int]) -> int | None: return None if average is None else round(average) -def _duration_ms(run: RunRecord) -> int | None: +def _duration_ms(run: SampleRecord) -> int | None: if run.started_at is None or run.completed_at is None: return None return round((run.completed_at - run.started_at).total_seconds() * 1000) @@ -380,7 +388,7 @@ def _summary_text(summary: dict, key: str) -> str | None: return None -def _error_summary(run: RunRecord, summary: dict) -> str | None: +def _error_summary(run: SampleRecord, summary: dict) -> str | None: if run.error_message: return run.error_message if str(run.status) not in {"failed", "cancelled"}: diff --git a/ergon_core/ergon_core/core/views/runs/__init__.py b/ergon_core/ergon_core/core/views/samples/__init__.py similarity index 100% rename from ergon_core/ergon_core/core/views/runs/__init__.py rename to ergon_core/ergon_core/core/views/samples/__init__.py diff --git a/ergon_core/ergon_core/core/views/runs/evaluation_mapping.py b/ergon_core/ergon_core/core/views/samples/evaluation_mapping.py similarity index 85% rename from ergon_core/ergon_core/core/views/runs/evaluation_mapping.py rename to ergon_core/ergon_core/core/views/samples/evaluation_mapping.py index 2af8dd91d..90138d7ca 100644 --- a/ergon_core/ergon_core/core/views/runs/evaluation_mapping.py +++ b/ergon_core/ergon_core/core/views/samples/evaluation_mapping.py @@ -4,22 +4,22 @@ from uuid import UUID from ergon_core.core.application.evaluation.summary import EvaluationSummary -from ergon_core.core.persistence.telemetry.models import RunTaskEvaluation -from ergon_core.core.views.runs.models import ( +from ergon_core.core.persistence.telemetry.models import SampleTaskEvaluation +from ergon_core.core.views.samples.models import ( RunEvaluationCriterionDto, - RunTaskEvaluationDto, + SampleTaskEvaluationDto, ) def build_dashboard_evaluation_dto( *, evaluation_id: UUID, - run_id: UUID, + sample_id: UUID, task_id: UUID, total_score: float, created_at: datetime, summary: EvaluationSummary, -) -> RunTaskEvaluationDto: +) -> SampleTaskEvaluationDto: criterion_results = [ RunEvaluationCriterionDto( id=f"{evaluation_id}-{i}", @@ -47,9 +47,9 @@ def build_dashboard_evaluation_dto( ) for i, cr in enumerate(summary.criterion_results) ] - return RunTaskEvaluationDto( + return SampleTaskEvaluationDto( id=str(evaluation_id), - run_id=str(run_id), + sample_id=str(sample_id), task_id=str(task_id), evaluator_name=summary.evaluator_name, aggregation_rule="weighted_sum", @@ -64,11 +64,11 @@ def build_dashboard_evaluation_dto( ) -def evaluation_row_to_dto(evaluation: RunTaskEvaluation) -> RunTaskEvaluationDto: +def evaluation_row_to_dto(evaluation: SampleTaskEvaluation) -> SampleTaskEvaluationDto: summary = EvaluationSummary.model_validate(evaluation.summary_json) return build_dashboard_evaluation_dto( evaluation_id=evaluation.id, - run_id=evaluation.run_id, + sample_id=evaluation.sample_id, task_id=evaluation.task_id, total_score=0.0 if evaluation.score is None else evaluation.score, created_at=evaluation.created_at, diff --git a/ergon_core/ergon_core/core/views/runs/metrics.py b/ergon_core/ergon_core/core/views/samples/metrics.py similarity index 97% rename from ergon_core/ergon_core/core/views/runs/metrics.py rename to ergon_core/ergon_core/core/views/samples/metrics.py index e1e528140..bf130f6e6 100644 --- a/ergon_core/ergon_core/core/views/runs/metrics.py +++ b/ergon_core/ergon_core/core/views/samples/metrics.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from typing import Any -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.shared.context_parts import ContextPartChunkLog, ProviderTokenUsage TOKEN_BREAKDOWN_KEYS = ( @@ -29,7 +29,7 @@ class AggregatedRunMetrics: def aggregate_run_metrics( - events: list[RunContextEvent], + events: list[SampleContextEvent], *, summary: dict[str, Any], ) -> AggregatedRunMetrics: diff --git a/ergon_core/ergon_core/core/views/runs/models.py b/ergon_core/ergon_core/core/views/samples/models.py similarity index 86% rename from ergon_core/ergon_core/core/views/runs/models.py rename to ergon_core/ergon_core/core/views/samples/models.py index b48a1ed5f..1710f3569 100644 --- a/ergon_core/ergon_core/core/views/runs/models.py +++ b/ergon_core/ergon_core/core/views/samples/models.py @@ -1,7 +1,7 @@ """Pydantic DTOs for the run detail API surface. -Task structure comes from RunGraphNode + RunGraphEdge rows (the live graph), -not from ExperimentDefinitionTask. All task keys are RunGraphNode.task_id. +Task structure comes from SampleGraphNode + SampleGraphEdge rows (the live graph), +not from ExperimentDefinitionTask. All task keys are SampleGraphNode.task_id. """ @@ -33,7 +33,7 @@ class RunCommunicationMessageDto(CamelModel): id: str thread_id: str thread_topic: str - run_id: str + sample_id: str task_id: str | None = None task_execution_id: str | None = None from_agent_id: str @@ -45,7 +45,7 @@ class RunCommunicationMessageDto(CamelModel): class RunCommunicationThreadDto(CamelModel): id: str - run_id: str + sample_id: str task_id: str | None = None topic: str summary: str | None = None @@ -56,8 +56,8 @@ class RunCommunicationThreadDto(CamelModel): messages: list[RunCommunicationMessageDto] = Field(default_factory=list) -class RunTaskDto(CamelModel): - """REST projection of RunGraphNode for run detail pages. +class SampleTaskDto(CamelModel): + """REST projection of SampleGraphNode for run detail pages. This is not the canonical graph schema; graph semantics live in application/graph/models.py and application/runtime/status.py. @@ -78,7 +78,7 @@ class RunTaskDto(CamelModel): completed_at: datetime | None = None -class RunResourceDto(CamelModel): +class SampleResourceDto(CamelModel): id: str task_id: str task_execution_id: str @@ -130,9 +130,9 @@ class RunEvaluationCriterionDto(CamelModel): error: dict[str, Any] | None = None # slopcop: ignore[no-typing-any] -class RunTaskEvaluationDto(CamelModel): +class SampleTaskEvaluationDto(CamelModel): id: str - run_id: str + sample_id: str task_id: str | None = None evaluator_name: str aggregation_rule: str @@ -167,9 +167,9 @@ class RunSandboxDto(CamelModel): commands: list[RunSandboxCommandDto] = Field(default_factory=list) -class RunContextEventDto(CamelModel): +class SampleContextEventDto(CamelModel): id: UUID - run_id: UUID + sample_id: UUID task_execution_id: UUID task_id: UUID worker_binding_key: str @@ -181,8 +181,8 @@ class RunContextEventDto(CamelModel): completed_at: datetime | None = None -class RunSnapshotMetricsDto(CamelModel): - run_id: str +class SampleSnapshotMetricsDto(CamelModel): + sample_id: str status: str duration_ms: int | None = None total_tasks: int = 0 @@ -193,18 +193,18 @@ class RunSnapshotMetricsDto(CamelModel): cost_observed: bool = False -class RunSnapshotDto(CamelModel): +class SampleSnapshotDto(CamelModel): id: str definition_id: str name: str status: str - tasks: dict[str, RunTaskDto] = Field(default_factory=dict) + tasks: dict[str, SampleTaskDto] = Field(default_factory=dict) root_task_id: str = "" # slopcop: ignore[no-str-empty-default] - resources_by_task: dict[str, list[RunResourceDto]] = Field(default_factory=dict) + resources_by_task: dict[str, list[SampleResourceDto]] = Field(default_factory=dict) executions_by_task: dict[str, list[RunExecutionAttemptDto]] = Field(default_factory=dict) - evaluations_by_task: dict[str, RunTaskEvaluationDto] = Field(default_factory=dict) + evaluations_by_task: dict[str, SampleTaskEvaluationDto] = Field(default_factory=dict) sandboxes_by_task: dict[str, RunSandboxDto] = Field(default_factory=dict) - context_events_by_task: dict[str, list[RunContextEventDto]] = Field(default_factory=dict) + context_events_by_task: dict[str, list[SampleContextEventDto]] = Field(default_factory=dict) threads: list[RunCommunicationThreadDto] = Field(default_factory=list) started_at: datetime | None = None completed_at: datetime | None = None @@ -216,11 +216,11 @@ class RunSnapshotDto(CamelModel): running_tasks: int = 0 cancelled_tasks: int = 0 final_score: float | None = None - metrics: RunSnapshotMetricsDto | None = None + metrics: SampleSnapshotMetricsDto | None = None error: str | None = None -class RunSummaryDto(BaseModel): +class SampleSummaryDto(BaseModel): model_config = ConfigDict(populate_by_name=True) id: UUID diff --git a/ergon_core/ergon_core/core/views/runs/service.py b/ergon_core/ergon_core/core/views/samples/service.py similarity index 74% rename from ergon_core/ergon_core/core/views/runs/service.py rename to ergon_core/ergon_core/core/views/samples/service.py index 7ff4e60f9..12b57d3f7 100644 --- a/ergon_core/ergon_core/core/views/runs/service.py +++ b/ergon_core/ergon_core/core/views/samples/service.py @@ -5,28 +5,28 @@ from datetime import datetime from uuid import UUID -from ergon_core.core.views.runs.models import ( - RunSnapshotMetricsDto, - RunSummaryDto, - RunSnapshotDto, +from ergon_core.core.views.samples.models import ( + SampleSnapshotMetricsDto, + SampleSummaryDto, + SampleSnapshotDto, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.persistence.definitions.models import ( ExperimentDefinition, ExperimentDefinitionWorker, ) from ergon_core.core.persistence.graph.models import ( - RunGraphEdge, - RunGraphMutation, - RunGraphNode, + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, ) from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunStatus +from ergon_core.core.persistence.shared.enums import SampleStatus from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskEvaluation, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskEvaluation, + SampleTaskAttempt, Thread, ThreadMessage, ) @@ -35,7 +35,7 @@ EvaluationService, ) from ergon_core.core.application.runtime.models import GraphMutationRecordDto -from ergon_core.core.views.runs.snapshot import ( +from ergon_core.core.views.samples.snapshot import ( _build_communication_threads, _build_task_map, _context_events_by_task, @@ -47,12 +47,12 @@ ) from ergon_core.core.views.resources import require_viewable_resource_size from ergon_core.core.views.dashboard_events.graph_mutations import graph_mutation_record_from_row -from ergon_core.core.views.runs.metrics import aggregate_run_metrics, observed_cost_from_summary +from ergon_core.core.views.samples.metrics import aggregate_run_metrics, observed_cost_from_summary from pydantic import BaseModel from sqlmodel import Session, col, select -class RunResourceBlob(BaseModel): +class SampleResourceBlob(BaseModel): model_config = {"frozen": True} path: Path @@ -60,10 +60,10 @@ class RunResourceBlob(BaseModel): filename: str -class RunReadService: +class SampleSnapshotReadService: """Owns database reads and DTO shaping for run API endpoints.""" - def list_runs( + def list_samples( self, *, limit: int = 20, @@ -71,15 +71,15 @@ def list_runs( definition_id: UUID | None = None, experiment: str | None = None, offset: int = 0, - ) -> list[RunSummaryDto]: + ) -> list[SampleSummaryDto]: with get_session() as session: - stmt = select(RunRecord).order_by(col(RunRecord.created_at).desc()) + stmt = select(SampleRecord).order_by(col(SampleRecord.created_at).desc()) if status: - stmt = stmt.where(RunRecord.status == status) + stmt = stmt.where(SampleRecord.status == status) if definition_id: - stmt = stmt.where(RunRecord.definition_id == definition_id) + stmt = stmt.where(SampleRecord.definition_id == definition_id) if experiment: - stmt = stmt.where(RunRecord.experiment == experiment) + stmt = stmt.where(SampleRecord.experiment == experiment) stmt = stmt.offset(offset).limit(limit) rows = list(session.exec(stmt).all()) definition_names = { @@ -90,7 +90,7 @@ def list_runs( ) ).all() } - task_counts = _task_counts_by_run(session, [row.id for row in rows]) + task_counts = _task_counts_by_sample(session, [row.id for row in rows]) return [ _run_summary( row, @@ -100,14 +100,14 @@ def list_runs( for row in rows ] - def get_run_summary(self, run_id: UUID) -> RunSummaryDto | None: + def get_sample_summary(self, sample_id: UUID) -> SampleSummaryDto | None: with get_session() as session: - run = session.get(RunRecord, run_id) + run = session.get(SampleRecord, sample_id) return _run_summary(run) if run is not None else None - def build_run_snapshot(self, run_id: UUID) -> RunSnapshotDto | None: + def build_snapshot(self, sample_id: UUID) -> SampleSnapshotDto | None: with get_session() as session: - run = session.get(RunRecord, run_id) + run = session.get(SampleRecord, sample_id) if run is None: return None @@ -117,10 +117,14 @@ def build_run_snapshot(self, run_id: UUID) -> RunSnapshotDto | None: def_id = run.definition_id nodes = list( - session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all() + session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all() ) edges = list( - session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all() + session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all() ) def_workers = list( session.exec( @@ -131,28 +135,32 @@ def build_run_snapshot(self, run_id: UUID) -> RunSnapshotDto | None: ) executions = list( session.exec( - select(RunTaskExecution).where(RunTaskExecution.run_id == run_id) + select(SampleTaskAttempt).where(SampleTaskAttempt.sample_id == sample_id) ).all() ) resources = list( - session.exec(select(RunResource).where(RunResource.run_id == run_id)).all() + session.exec( + select(SampleResource).where(SampleResource.sample_id == sample_id) + ).all() ) evaluations = list( session.exec( - select(RunTaskEvaluation).where(RunTaskEvaluation.run_id == run_id) + select(SampleTaskEvaluation).where(SampleTaskEvaluation.sample_id == sample_id) ).all() ) - threads = list(session.exec(select(Thread).where(Thread.run_id == run_id)).all()) + threads = list(session.exec(select(Thread).where(Thread.sample_id == sample_id)).all()) thread_messages = list( - session.exec(select(ThreadMessage).where(ThreadMessage.run_id == run_id)).all() + session.exec( + select(ThreadMessage).where(ThreadMessage.sample_id == sample_id) + ).all() ) context_events = list( session.exec( - select(RunContextEvent) - .where(RunContextEvent.run_id == run_id) + select(SampleContextEvent) + .where(SampleContextEvent.sample_id == sample_id) .order_by( - col(RunContextEvent.task_execution_id), - col(RunContextEvent.sequence), + col(SampleContextEvent.task_execution_id), + col(SampleContextEvent.sequence), ) ).all() ) @@ -186,14 +194,14 @@ def build_run_snapshot(self, run_id: UUID) -> RunSnapshotDto | None: if run.started_at and run.completed_at: duration_seconds = (run.completed_at - run.started_at).total_seconds() - run_id_str = str(run.id) + sample_id_str = str(run.id) run_summary = run.parsed_summary() aggregated_metrics = aggregate_run_metrics(context_events, summary=run_summary) meta = definition.parsed_metadata() run_name = str(meta.get("name", definition.benchmark_type)) - return RunSnapshotDto( - id=run_id_str, + return SampleSnapshotDto( + id=sample_id_str, definition_id=str(run.definition_id), name=run_name, status=run.status, @@ -209,7 +217,7 @@ def build_run_snapshot(self, run_id: UUID) -> RunSnapshotDto | None: ), evaluations_by_task=_task_keyed_evaluations( evaluations, - run_id_str, + sample_id_str, ), context_events_by_task=dict(context_events_by_task), sandboxes_by_task=_task_keyed_sandboxes(run_summary), @@ -228,8 +236,8 @@ def build_run_snapshot(self, run_id: UUID) -> RunSnapshotDto | None: running_tasks=running_tasks, cancelled_tasks=cancelled_tasks, final_score=_display_run_score(score_summary, run.status, run_summary), - metrics=RunSnapshotMetricsDto( - run_id=run_id_str, + metrics=SampleSnapshotMetricsDto( + sample_id=sample_id_str, status=str(run.status), duration_ms=( round(duration_seconds * 1000) if duration_seconds is not None else None @@ -244,27 +252,27 @@ def build_run_snapshot(self, run_id: UUID) -> RunSnapshotDto | None: error=run.error_message, ) - def list_mutations(self, run_id: UUID) -> list[GraphMutationRecordDto] | None: + def list_mutations(self, sample_id: UUID) -> list[GraphMutationRecordDto] | None: with get_session() as session: - run = session.get(RunRecord, run_id) + run = session.get(SampleRecord, sample_id) if run is None: return None mutations = list( session.exec( - select(RunGraphMutation) - .where(RunGraphMutation.run_id == run_id) - .order_by(col(RunGraphMutation.sequence)) + select(SampleGraphMutation) + .where(SampleGraphMutation.sample_id == sample_id) + .order_by(col(SampleGraphMutation.sequence)) ).all() ) return [graph_mutation_record_from_row(m) for m in mutations] - def get_resource_blob(self, run_id: UUID, resource_id: UUID) -> RunResourceBlob | None: + def get_resource_blob(self, sample_id: UUID, resource_id: UUID) -> SampleResourceBlob | None: with get_session() as session: resource = session.exec( - select(RunResource).where( - RunResource.id == resource_id, - RunResource.run_id == run_id, + select(SampleResource).where( + SampleResource.id == resource_id, + SampleResource.sample_id == sample_id, ) ).first() @@ -275,7 +283,7 @@ def get_resource_blob(self, run_id: UUID, resource_id: UUID) -> RunResourceBlob blob_path.relative_to(_blob_root()) size = blob_path.stat().st_size require_viewable_resource_size(size) - return RunResourceBlob( + return SampleResourceBlob( path=blob_path, media_type=resource.mime_type or "application/octet-stream", filename=resource.name, @@ -292,7 +300,7 @@ def _display_run_score( persisted_score = _summary_number(summary, "final_score") if persisted_score is None: persisted_score = _summary_number(summary, "score") - if run_status != RunStatus.COMPLETED: + if run_status != SampleStatus.COMPLETED: return persisted_score # TODO: this is a hack, we need to fix the calculation / rename variables to make clear that the output score should be normalised by here. return ( @@ -303,15 +311,15 @@ def _display_run_score( def _run_summary( - run: RunRecord, + run: SampleRecord, *, definition_name: str | None = None, task_counts: dict[str, object] | None = None, -) -> RunSummaryDto: +) -> SampleSummaryDto: summary = run.parsed_summary() metrics = summary.get("metrics") total_cost_usd, _ = observed_cost_from_summary(summary) - return RunSummaryDto( + return SampleSummaryDto( id=run.id, name=_summary_text(summary, "name") or _summary_text(summary, "run_name") @@ -346,27 +354,31 @@ def _run_summary( ) -def _task_counts_by_run(session: Session, run_ids: list[UUID]) -> dict[UUID, dict[str, object]]: +def _task_counts_by_sample( + session: Session, sample_ids: list[UUID] +) -> dict[UUID, dict[str, object]]: counts: dict[UUID, dict[str, int]] = { - run_id: { + sample_id: { "total": 0, "completed": 0, "failed": 0, "running": 0, "cancelled": 0, } - for run_id in run_ids + for sample_id in sample_ids } - if not run_ids: + if not sample_ids: return {} nodes = list( - session.exec(select(RunGraphNode).where(col(RunGraphNode.run_id).in_(run_ids))).all() + session.exec( + select(SampleGraphNode).where(col(SampleGraphNode.sample_id).in_(sample_ids)) + ).all() ) latest_updates: dict[UUID, datetime] = {} for node in nodes: run_counts = counts.setdefault( - node.run_id, + node.sample_id, {"total": 0, "completed": 0, "failed": 0, "running": 0, "cancelled": 0}, ) run_counts["total"] += 1 @@ -376,16 +388,16 @@ def _task_counts_by_run(session: Session, run_ids: list[UUID]) -> dict[UUID, dic elif status in {"executing", "evaluating"}: run_counts["running"] += 1 if node.updated_at is not None: - latest_updates[node.run_id] = max( - latest_updates.get(node.run_id, node.updated_at), + latest_updates[node.sample_id] = max( + latest_updates.get(node.sample_id, node.updated_at), node.updated_at, ) result: dict[UUID, dict[str, object]] = { - run_id: dict(run_counts) for run_id, run_counts in counts.items() + sample_id: dict(run_counts) for sample_id, run_counts in counts.items() } - for run_id, updated_at in latest_updates.items(): - result[run_id]["latest_update"] = updated_at + for sample_id, updated_at in latest_updates.items(): + result[sample_id]["latest_update"] = updated_at return result @@ -394,7 +406,7 @@ def _count_value(task_counts: dict[str, object] | None, key: str) -> int: return value if isinstance(value, int) else 0 -def _latest_activity(run: RunRecord, task_counts: dict | None = None) -> datetime | None: +def _latest_activity(run: SampleRecord, task_counts: dict | None = None) -> datetime | None: candidates = [run.created_at, run.started_at, run.completed_at] latest_update = (task_counts or {}).get("latest_update") if isinstance(latest_update, datetime): @@ -402,7 +414,7 @@ def _latest_activity(run: RunRecord, task_counts: dict | None = None) -> datetim return max(value for value in candidates if value is not None) -def _duration_seconds(run: RunRecord) -> float | None: +def _duration_seconds(run: SampleRecord) -> float | None: if run.started_at is None or run.completed_at is None: return None return (run.completed_at - run.started_at).total_seconds() diff --git a/ergon_core/ergon_core/core/views/runs/snapshot.py b/ergon_core/ergon_core/core/views/samples/snapshot.py similarity index 87% rename from ergon_core/ergon_core/core/views/runs/snapshot.py rename to ergon_core/ergon_core/core/views/samples/snapshot.py index 44379b88b..b7199fda1 100644 --- a/ergon_core/ergon_core/core/views/runs/snapshot.py +++ b/ergon_core/ergon_core/core/views/samples/snapshot.py @@ -5,25 +5,25 @@ from typing import cast from uuid import UUID -from ergon_core.core.views.runs.evaluation_mapping import evaluation_row_to_dto -from ergon_core.core.views.runs.models import ( +from ergon_core.core.views.samples.evaluation_mapping import evaluation_row_to_dto +from ergon_core.core.views.samples.models import ( RunCommunicationMessageDto, RunCommunicationThreadDto, - RunContextEventDto, + SampleContextEventDto, RunExecutionAttemptDto, - RunResourceDto, + SampleResourceDto, RunSandboxCommandDto, RunSandboxDto, - RunTaskDto, - RunTaskEvaluationDto, + SampleTaskDto, + SampleTaskEvaluationDto, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.persistence.definitions.models import ExperimentDefinitionWorker -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode from ergon_core.core.persistence.telemetry.models import ( - RunResource, - RunTaskEvaluation, - RunTaskExecution, + SampleResource, + SampleTaskEvaluation, + SampleTaskAttempt, Thread, ThreadMessage, ) @@ -32,11 +32,11 @@ # TODO: this file / logic almost certainly duplicates the run benchmarks logic? if not it needs to be moved, renamed and laid out cleaner. def _build_task_map( - nodes: list[RunGraphNode], - edges: list[RunGraphEdge], + nodes: list[SampleGraphNode], + edges: list[SampleGraphEdge], worker_by_binding: dict[str, ExperimentDefinitionWorker], task_timestamps: dict[UUID, tuple[datetime | None, datetime | None]], -) -> tuple[dict[str, RunTaskDto], str, int, int, int, int, int, int]: +) -> tuple[dict[str, SampleTaskDto], str, int, int, int, int, int, int]: """Three clean passes using stored containment columns. Pass 1: node columns (parent_task_id, level) - no edge traversal. @@ -46,7 +46,7 @@ def _build_task_map( if not nodes: return {}, "", 0, 0, 0, 0, 0, 0 - task_map: dict[str, RunTaskDto] = {} + task_map: dict[str, SampleTaskDto] = {} for node in nodes: nid = str(node.task_id) @@ -56,7 +56,7 @@ def _build_task_map( else None ) started_at, completed_at = task_timestamps.get(node.task_id, (None, None)) - task_map[nid] = RunTaskDto( + task_map[nid] = SampleTaskDto( id=nid, name=node.task_slug, description=node.description, @@ -101,7 +101,7 @@ def _build_task_map( def _task_keyed_executions( - executions: list[RunTaskExecution], + executions: list[SampleTaskAttempt], worker_map: dict[UUID, ExperimentDefinitionWorker], ) -> dict[str, list[RunExecutionAttemptDto]]: by_task: dict[str, list[RunExecutionAttemptDto]] = defaultdict(list) @@ -145,10 +145,10 @@ def _task_keyed_executions( def _task_keyed_resources( - resources: list[RunResource], + resources: list[SampleResource], execution_task_map: dict[UUID, UUID], -) -> dict[str, list[RunResourceDto]]: - by_task: dict[str, list[RunResourceDto]] = defaultdict(list) +) -> dict[str, list[SampleResourceDto]]: + by_task: dict[str, list[SampleResourceDto]] = defaultdict(list) for resource in resources: task_id_uuid = ( execution_task_map.get(resource.task_execution_id) @@ -159,7 +159,7 @@ def _task_keyed_resources( continue tid = str(task_id_uuid) by_task[tid].append( - RunResourceDto( + SampleResourceDto( id=str(resource.id), task_id=tid, task_execution_id=( @@ -176,10 +176,10 @@ def _task_keyed_resources( def _task_keyed_evaluations( - evaluations: list[RunTaskEvaluation], - run_id: str, -) -> dict[str, RunTaskEvaluationDto]: - result: dict[str, RunTaskEvaluationDto] = {} + evaluations: list[SampleTaskEvaluation], + sample_id: str, +) -> dict[str, SampleTaskEvaluationDto]: + result: dict[str, SampleTaskEvaluationDto] = {} for ev in evaluations: tid = str(ev.task_id) result[tid] = evaluation_row_to_dto(ev) @@ -241,7 +241,7 @@ def _build_communication_threads( result.append( RunCommunicationThreadDto( id=str(thread.id), - run_id=str(thread.run_id), + sample_id=str(thread.sample_id), task_id=str(thread_task_id) if thread_task_id else None, topic=thread.topic, summary=thread.summary, @@ -253,7 +253,7 @@ def _build_communication_threads( RunCommunicationMessageDto( id=str(message.id), thread_id=str(message.thread_id), - run_id=str(message.run_id), + sample_id=str(message.sample_id), thread_topic=thread.topic, task_id=( str(execution_task_map[message.task_execution_id]) @@ -280,11 +280,11 @@ def _build_communication_threads( def _task_timestamps( - executions: list[RunTaskExecution], + executions: list[SampleTaskAttempt], ) -> dict[UUID, tuple[datetime | None, datetime | None]]: """Derive per-task started_at/completed_at from execution records.""" result: dict[UUID, tuple[datetime | None, datetime | None]] = {} - by_task: dict[UUID, list[RunTaskExecution]] = defaultdict(list) + by_task: dict[UUID, list[SampleTaskAttempt]] = defaultdict(list) for execution in executions: by_task[execution.task_id].append(execution) @@ -302,18 +302,18 @@ def _task_timestamps( def _context_events_by_task( - context_events_rows: list[RunContextEvent], + context_events_rows: list[SampleContextEvent], execution_task_map: dict[UUID, UUID], -) -> dict[str, list[RunContextEventDto]]: - context_events_by_task: dict[str, list[RunContextEventDto]] = defaultdict(list) +) -> dict[str, list[SampleContextEventDto]]: + context_events_by_task: dict[str, list[SampleContextEventDto]] = defaultdict(list) for event in context_events_rows: task_id = execution_task_map.get(event.task_execution_id) if task_id is None: continue context_events_by_task[str(task_id)].append( - RunContextEventDto( + SampleContextEventDto( id=event.id, - run_id=event.run_id, + sample_id=event.sample_id, task_execution_id=event.task_execution_id, task_id=task_id, worker_binding_key=event.worker_binding_key, diff --git a/ergon_core/ergon_core/test_support/e2e_read_helpers.py b/ergon_core/ergon_core/test_support/e2e_read_helpers.py index 00ef001e7..b52c5fb87 100644 --- a/ergon_core/ergon_core/test_support/e2e_read_helpers.py +++ b/ergon_core/ergon_core/test_support/e2e_read_helpers.py @@ -7,12 +7,12 @@ from pathlib import Path from uuid import UUID -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.telemetry.models import ( - RunResource, - RunTaskEvaluation, - RunTaskExecution, + SampleResource, + SampleTaskEvaluation, + SampleTaskAttempt, SandboxCommandWalEntry, SandboxEvent, ) @@ -52,7 +52,7 @@ class SandboxEventSnapshot: kind: str -def _resource_snapshot(row: RunResource) -> ResourceSnapshot: +def _resource_snapshot(row: SampleResource) -> ResourceSnapshot: return ResourceSnapshot( name=row.name, file_path=row.file_path, @@ -62,7 +62,7 @@ def _resource_snapshot(row: RunResource) -> ResourceSnapshot: ) -def _execution_snapshot(row: RunTaskExecution) -> TaskExecutionSnapshot: +def _execution_snapshot(row: SampleTaskAttempt) -> TaskExecutionSnapshot: return TaskExecutionSnapshot( task_id=row.task_id, started_at=row.started_at, @@ -70,7 +70,7 @@ def _execution_snapshot(row: RunTaskExecution) -> TaskExecutionSnapshot: ) -def _evaluation_snapshot(row: RunTaskEvaluation) -> TaskEvaluationSnapshot: +def _evaluation_snapshot(row: SampleTaskEvaluation) -> TaskEvaluationSnapshot: return TaskEvaluationSnapshot(score=row.score, created_at=row.created_at) @@ -78,17 +78,17 @@ def read_resource_bytes(resource: ResourceSnapshot) -> bytes: return Path(resource.file_path).read_bytes() -def first_probe_resource(run_id: UUID) -> ResourceSnapshot | None: +def first_probe_resource(sample_id: UUID) -> ResourceSnapshot | None: with get_session() as session: row = session.exec( - select(RunResource) - .where(RunResource.run_id == run_id) + select(SampleResource) + .where(SampleResource.sample_id == sample_id) .where( - RunResource.name.like("probe_%.json"), # ty: ignore[unresolved-attribute] + SampleResource.name.like("probe_%.json"), # ty: ignore[unresolved-attribute] ) - .where(RunResource.kind == "report") + .where(SampleResource.kind == "report") .order_by( - RunResource.created_at, # ty: ignore[unresolved-attribute] + SampleResource.created_at, # ty: ignore[unresolved-attribute] ) .limit(1), ).first() @@ -96,7 +96,7 @@ def first_probe_resource(run_id: UUID) -> ResourceSnapshot | None: def list_named_resources( - run_id: UUID, + sample_id: UUID, *, prefix: str, suffix: str, @@ -104,10 +104,10 @@ def list_named_resources( with get_session() as session: rows = list( session.exec( - select(RunResource) - .where(RunResource.run_id == run_id) + select(SampleResource) + .where(SampleResource.sample_id == sample_id) .where( - RunResource.name.like(f"{prefix}%{suffix}"), # ty: ignore[unresolved-attribute] + SampleResource.name.like(f"{prefix}%{suffix}"), # ty: ignore[unresolved-attribute] ), ).all(), ) @@ -115,59 +115,61 @@ def list_named_resources( def list_root_execution_and_evaluations( - run_id: UUID, + sample_id: UUID, ) -> tuple[TaskExecutionSnapshot | None, list[TaskEvaluationSnapshot]]: with get_session() as session: root = session.exec( - select(RunGraphNode) - .where(RunGraphNode.run_id == run_id) - .where(RunGraphNode.level == 0), + select(SampleGraphNode) + .where(SampleGraphNode.sample_id == sample_id) + .where(SampleGraphNode.level == 0), ).one() execution = session.exec( - select(RunTaskExecution).where(RunTaskExecution.task_id == root.task_id), + select(SampleTaskAttempt).where(SampleTaskAttempt.task_id == root.task_id), ).first() evaluations = list( session.exec( - select(RunTaskEvaluation) - .where(RunTaskEvaluation.run_id == run_id) - .where(RunTaskEvaluation.task_id == root.task_id), + select(SampleTaskEvaluation) + .where(SampleTaskEvaluation.sample_id == sample_id) + .where(SampleTaskEvaluation.task_id == root.task_id), ).all(), ) execution_snapshot = None if execution is None else _execution_snapshot(execution) return execution_snapshot, [_evaluation_snapshot(row) for row in evaluations] -def list_sandbox_command_wal(run_id: UUID) -> list[SandboxCommandWalSnapshot]: +def list_sandbox_command_wal(sample_id: UUID) -> list[SandboxCommandWalSnapshot]: with get_session() as session: rows = list( session.exec( - select(SandboxCommandWalEntry).where(SandboxCommandWalEntry.run_id == run_id), + select(SandboxCommandWalEntry).where(SandboxCommandWalEntry.sample_id == sample_id), ).all(), ) return [SandboxCommandWalSnapshot(command=row.command) for row in rows] -def list_sandbox_events(run_id: UUID) -> list[SandboxEventSnapshot]: +def list_sandbox_events(sample_id: UUID) -> list[SandboxEventSnapshot]: with get_session() as session: - rows = list(session.exec(select(SandboxEvent).where(SandboxEvent.run_id == run_id)).all()) + rows = list( + session.exec(select(SandboxEvent).where(SandboxEvent.sample_id == sample_id)).all() + ) return [SandboxEventSnapshot(sandbox_id=row.sandbox_id, kind=row.kind) for row in rows] -def leaf_execution_timings_by_slug(run_id: UUID) -> dict[str, TaskExecutionSnapshot | None]: +def leaf_execution_timings_by_slug(sample_id: UUID) -> dict[str, TaskExecutionSnapshot | None]: with get_session() as session: leaves = list( session.exec( - select(RunGraphNode) - .where(RunGraphNode.run_id == run_id) - .where(RunGraphNode.level > 0), + select(SampleGraphNode) + .where(SampleGraphNode.sample_id == sample_id) + .where(SampleGraphNode.level > 0), ).all(), ) executions = list( session.exec( - select(RunTaskExecution) - .where(RunTaskExecution.run_id == run_id) + select(SampleTaskAttempt) + .where(SampleTaskAttempt.sample_id == sample_id) .where( - RunTaskExecution.task_id.in_([leaf.task_id for leaf in leaves]), # ty: ignore[unresolved-attribute] + SampleTaskAttempt.task_id.in_([leaf.task_id for leaf in leaves]), # ty: ignore[unresolved-attribute] ), ).all(), ) diff --git a/ergon_core/ergon_core/test_support/sandbox/stub_manager.py b/ergon_core/ergon_core/test_support/sandbox/stub_manager.py index a73f56280..eb30e1675 100644 --- a/ergon_core/ergon_core/test_support/sandbox/stub_manager.py +++ b/ergon_core/ergon_core/test_support/sandbox/stub_manager.py @@ -24,7 +24,7 @@ class StubSandboxManager(BaseSandboxManager): async def create( self, sandbox_key: UUID, - run_id: UUID, + sample_id: UUID, timeout_minutes: int = 30, envs: dict[str, str] | None = None, display_task_id: UUID | None = None, @@ -33,7 +33,7 @@ async def create( logger.info("Returning test stub sandbox id %s for task %s", stub_id, sandbox_key) self._ensure_registries(sandbox_key) self._sandboxes[sandbox_key] = cast("AsyncSandbox", _StubSandbox(stub_id)) - self._run_ids[sandbox_key] = run_id + self._sample_ids[sandbox_key] = sample_id self._display_task_ids[sandbox_key] = display_task_id or sandbox_key self._sandbox_manager_classes[sandbox_key] = type(self) return stub_id @@ -45,7 +45,7 @@ async def terminate(self, task_id: UUID, reason: str = "completed") -> None: self._sandboxes.pop(task_id, None) self._file_registries.pop(task_id, None) self._created_files_registry.pop(task_id, None) - self._run_ids.pop(task_id, None) + self._sample_ids.pop(task_id, None) self._display_task_ids.pop(task_id, None) self._sandbox_manager_classes.pop(task_id, None) diff --git a/ergon_core/tests/unit/api/test_public_api_imports.py b/ergon_core/tests/unit/api/test_public_api_imports.py index 2bb2165b6..f09da2378 100644 --- a/ergon_core/tests/unit/api/test_public_api_imports.py +++ b/ergon_core/tests/unit/api/test_public_api_imports.py @@ -13,16 +13,16 @@ def test_telemetry_models_can_import_before_public_api() -> None: shared_enums = importlib.import_module("ergon_core.core.persistence.shared.enums") public_api = importlib.import_module("ergon_core.api") - assert shared_enums.RunResourceKind.REPORT.value == "report" - assert not hasattr(telemetry, "RunResourceKind") - assert not hasattr(public_api, "RunResourceKind") + assert shared_enums.SampleResourceKind.REPORT.value == "report" + assert not hasattr(telemetry, "SampleResourceKind") + assert not hasattr(public_api, "SampleResourceKind") def test_public_api_root_stays_authoring_scoped() -> None: public_api = importlib.import_module("ergon_core.api") assert "__getattr__" not in public_api.__dict__ - assert not hasattr(public_api, "RunResourceView") + assert not hasattr(public_api, "SampleResourceView") assert not hasattr(public_api, "CriterionRuntime") assert not hasattr(public_api, "CommandResult") assert not hasattr(public_api, "SandboxResult") diff --git a/ergon_core/tests/unit/api/worker/test_worker_context_facade.py b/ergon_core/tests/unit/api/worker/test_worker_context_facade.py index 9b2940604..f41cc8b01 100644 --- a/ergon_core/tests/unit/api/worker/test_worker_context_facade.py +++ b/ergon_core/tests/unit/api/worker/test_worker_context_facade.py @@ -12,22 +12,22 @@ from ergon_core.api.errors import ContainmentViolation from ergon_core.api.worker.context import WorkerContext from ergon_core.api.worker.results import AwaitCompletionNotSupportedError, SpawnedTaskHandle -from ergon_core.core.application.resources.models import RunResourceView -from ergon_core.core.application.resources.service import RunResourceReadService +from ergon_core.core.application.resources.models import SampleResourceView +from ergon_core.core.application.resources.service import SampleResourceReadService from ergon_core.core.application.runtime.task_models import ( CancelTaskCommand, RefineTaskCommand, RestartTaskCommand, SubtaskInfo, ) -from ergon_core.core.persistence.shared.enums import RunResourceKind -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus +from ergon_core.core.persistence.shared.enums import SampleResourceKind +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskAttempt, ) from ergon_core.core.shared.utils import utcnow from ergon_core.test_support import task_factory @@ -60,8 +60,8 @@ class _FakeTaskManagement: def __init__(self) -> None: self.calls: list[tuple[str, object, object]] = [] - async def spawn_dynamic_task(self, *, run_id, parent_task_id, task, depends_on): - self.calls.append(("spawn", run_id, parent_task_id, task, depends_on)) + async def spawn_dynamic_task(self, *, sample_id, parent_task_id, task, depends_on): + self.calls.append(("spawn", sample_id, parent_task_id, task, depends_on)) return SpawnedTaskHandle(task_id=uuid4()) async def cancel_task(self, session, command: CancelTaskCommand): @@ -80,8 +80,8 @@ def __init__(self, descendants: frozenset | None = None) -> None: self.descendant_set = descendants or frozenset() self.calls: list[tuple[str, object]] = [] - def list_subtasks(self, session, *, run_id, parent_task_id): - self.calls.append(("list_subtasks", session, run_id, parent_task_id)) + def list_subtasks(self, session, *, sample_id, parent_task_id): + self.calls.append(("list_subtasks", session, sample_id, parent_task_id)) return [ SubtaskInfo( task_id=uuid4(), @@ -94,8 +94,8 @@ def list_subtasks(self, session, *, run_id, parent_task_id): ) ] - def get_subtask(self, session, *, run_id, task_id): - self.calls.append(("get_subtask", session, run_id, task_id)) + def get_subtask(self, session, *, sample_id, task_id): + self.calls.append(("get_subtask", session, sample_id, task_id)) return SubtaskInfo( task_id=task_id, task_slug="target", @@ -106,14 +106,14 @@ def get_subtask(self, session, *, run_id, task_id): error=None, ) - async def descendant_ids(self, *, run_id, root_task_id): - self.calls.append(("descendant_ids", run_id, root_task_id)) + async def descendant_ids(self, *, sample_id, root_task_id): + self.calls.append(("descendant_ids", sample_id, root_task_id)) return self.descendant_set class _FakeResources: - def __init__(self, *, run_id, other_run_id, blob_path: Path) -> None: - self.run_id = run_id + def __init__(self, *, sample_id, other_run_id, blob_path: Path) -> None: + self.sample_id = sample_id self.other_run_id = other_run_id self.blob_path = blob_path self.calls: list[tuple[str, object]] = [] @@ -121,11 +121,11 @@ def __init__(self, *, run_id, other_run_id, blob_path: Path) -> None: def list_for_run(self, **kwargs): self.calls.append(("list_for_run", kwargs)) return [ - RunResourceView( + SampleResourceView( id=uuid4(), - run_id=self.run_id, + sample_id=self.sample_id, task_execution_id=kwargs.get("task_execution_id"), - kind=RunResourceKind.REPORT, + kind=SampleResourceKind.REPORT, name="report.txt", mime_type="text/plain", file_path=str(self.blob_path), @@ -137,10 +137,10 @@ def list_for_run(self, **kwargs): ) ] - def read_bytes(self, *, run_id, current_task_id, resource_id): - self.calls.append(("read_bytes", run_id, current_task_id, resource_id)) - run_id = self.other_run_id if str(resource_id).endswith("ffff") else self.run_id - if run_id != self.run_id: + def read_bytes(self, *, sample_id, current_task_id, resource_id): + self.calls.append(("read_bytes", sample_id, current_task_id, resource_id)) + sample_id = self.other_run_id if str(resource_id).endswith("ffff") else self.sample_id + if sample_id != self.sample_id: raise ContainmentViolation( parent_task_id=current_task_id, target_task_id=resource_id, @@ -148,9 +148,9 @@ def read_bytes(self, *, run_id, current_task_id, resource_id): return self.blob_path.read_bytes() -def _context(*, run_id, task_id, inspect=None, resource_service=None) -> WorkerContext: +def _context(*, sample_id, task_id, inspect=None, resource_service=None) -> WorkerContext: return WorkerContext._for_job( - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=uuid4(), definition_id=uuid4(), @@ -164,13 +164,13 @@ def _context(*, run_id, task_id, inspect=None, resource_service=None) -> WorkerC @pytest.mark.asyncio async def test_facade_mutations_call_current_service_command_signatures() -> None: - run_id = uuid4() + sample_id = uuid4() root_id = uuid4() child_id = uuid4() inspect = _FakeInspection(frozenset({child_id})) mgmt = _FakeTaskManagement() context = WorkerContext._for_job( - run_id=run_id, + sample_id=sample_id, task_id=root_id, execution_id=uuid4(), definition_id=uuid4(), @@ -195,11 +195,11 @@ async def test_facade_mutations_call_current_service_command_signatures() -> Non @pytest.mark.asyncio async def test_spawn_task_uses_empty_tuple_dependency_default() -> None: - run_id = uuid4() + sample_id = uuid4() root_id = uuid4() mgmt = _FakeTaskManagement() context = WorkerContext._for_job( - run_id=run_id, + sample_id=sample_id, task_id=root_id, execution_id=uuid4(), definition_id=uuid4(), @@ -225,11 +225,11 @@ async def test_spawn_task_uses_empty_tuple_dependency_default() -> None: @pytest.mark.asyncio async def test_facade_inspection_uses_current_service_names() -> None: - run_id = uuid4() + sample_id = uuid4() root_id = uuid4() child_id = uuid4() inspect = _FakeInspection(frozenset({child_id})) - context = _context(run_id=run_id, task_id=root_id, inspect=inspect) + context = _context(sample_id=sample_id, task_id=root_id, inspect=inspect) subtasks = await context.subtasks() descendants = await context.descendants() @@ -250,10 +250,10 @@ async def test_facade_inspection_uses_current_service_names() -> None: @pytest.mark.asyncio @pytest.mark.parametrize("method", ["cancel_task", "refine_task", "restart_task", "get_task"]) async def test_lifecycle_methods_enforce_descendant_containment(method: str) -> None: - run_id = uuid4() + sample_id = uuid4() root_id = uuid4() outside_id = uuid4() - context = _context(run_id=run_id, task_id=root_id, inspect=_FakeInspection(frozenset())) + context = _context(sample_id=sample_id, task_id=root_id, inspect=_FakeInspection(frozenset())) with pytest.raises(ContainmentViolation): if method == "refine_task": @@ -264,15 +264,15 @@ async def test_lifecycle_methods_enforce_descendant_containment(method: str) -> @pytest.mark.asyncio async def test_resources_are_run_scoped_not_descendant_scoped(tmp_path: Path) -> None: - run_id = uuid4() + sample_id = uuid4() other_run_id = uuid4() root_id = uuid4() sibling_task_id = uuid4() execution_id = uuid4() blob = tmp_path / "blob.txt" blob.write_bytes(b"ok") - repo = _FakeResources(run_id=run_id, other_run_id=other_run_id, blob_path=blob) - context = _context(run_id=run_id, task_id=root_id, resource_service=repo) + repo = _FakeResources(sample_id=sample_id, other_run_id=other_run_id, blob_path=blob) + context = _context(sample_id=sample_id, task_id=root_id, resource_service=repo) resources = await context.resources(task_id=sibling_task_id, execution_id=execution_id) data = await context.read_resource(resources[0].id) @@ -284,12 +284,12 @@ async def test_resources_are_run_scoped_not_descendant_scoped(tmp_path: Path) -> @pytest.mark.asyncio async def test_read_resource_rejects_cross_run_rows(tmp_path: Path) -> None: - run_id = uuid4() + sample_id = uuid4() other_run_id = uuid4() blob = tmp_path / "blob.txt" blob.write_bytes(b"ok") - repo = _FakeResources(run_id=run_id, other_run_id=other_run_id, blob_path=blob) - context = _context(run_id=run_id, task_id=uuid4(), resource_service=repo) + repo = _FakeResources(sample_id=sample_id, other_run_id=other_run_id, blob_path=blob) + context = _context(sample_id=sample_id, task_id=uuid4(), resource_service=repo) cross_run_resource_id = uuid4() cross_run_resource_id = type(cross_run_resource_id)(f"{str(cross_run_resource_id)[:-4]}ffff") @@ -300,7 +300,7 @@ async def test_read_resource_rejects_cross_run_rows(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_resources_use_repository_run_scope_with_real_rows(tmp_path: Path) -> None: session = _sql_session() - run_id = uuid4() + sample_id = uuid4() other_run_id = uuid4() definition_id = uuid4() root_id = uuid4() @@ -317,54 +317,54 @@ async def test_resources_use_repository_run_scope_with_real_rows(tmp_path: Path) name="bench", metadata_json={}, ), - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="bench", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ), - RunRecord( + SampleRecord( id=other_run_id, definition_id=definition_id, benchmark_type="bench", instance_key="sample-2", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ), - RunGraphNode( + SampleGraphNode( task_id=root_id, - run_id=run_id, + sample_id=sample_id, instance_key="sample-1", task_slug="root", description="root", status="running", ), - RunGraphNode( + SampleGraphNode( task_id=sibling_id, - run_id=run_id, + sample_id=sample_id, instance_key="sample-1", task_slug="sibling", description="sibling", status="completed", ), - RunTaskExecution( + SampleTaskAttempt( id=sibling_execution_id, - run_id=run_id, + sample_id=sample_id, task_id=sibling_id, status=TaskExecutionStatus.COMPLETED, ), - RunTaskExecution( + SampleTaskAttempt( id=other_execution_id, - run_id=other_run_id, + sample_id=other_run_id, task_id=uuid4(), status=TaskExecutionStatus.COMPLETED, ), - RunResource( - run_id=run_id, + SampleResource( + sample_id=sample_id, task_execution_id=sibling_execution_id, - kind=RunResourceKind.REPORT.value, + kind=SampleResourceKind.REPORT.value, name="report.txt", mime_type="text/plain", file_path=str(blob), @@ -379,14 +379,14 @@ def session_factory(): yield session context = WorkerContext._for_job( - run_id=run_id, + sample_id=sample_id, task_id=root_id, execution_id=uuid4(), definition_id=definition_id, sandbox_id="sbx", task_mgmt=_FakeTaskManagement(), task_inspect=_FakeInspection(), - resource_service=RunResourceReadService(session_factory=session_factory), + resource_service=SampleResourceReadService(session_factory=session_factory), session_factory=session_factory, ) @@ -403,11 +403,11 @@ def session_factory(): def test_context_requires_facade_services_at_construction() -> None: with pytest.raises(ValidationError, match="task_mgmt"): - WorkerContext(run_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), sandbox_id="sbx") + WorkerContext(sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), sandbox_id="sbx") with pytest.raises(ValidationError, match="injected dependencies"): WorkerContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), sandbox_id="sbx", diff --git a/ergon_core/tests/unit/architecture/test_api_runs_boundary.py b/ergon_core/tests/unit/architecture/test_api_runs_boundary.py index 2b2db7b09..9ccca28fe 100644 --- a/ergon_core/tests/unit/architecture/test_api_runs_boundary.py +++ b/ergon_core/tests/unit/architecture/test_api_runs_boundary.py @@ -10,7 +10,7 @@ / "infrastructure" / "http" / "routes" - / "runs.py" + / "samples.py" ) DOMAIN_HELPERS = { diff --git a/ergon_core/tests/unit/architecture/test_application_domain_boundaries.py b/ergon_core/tests/unit/architecture/test_application_domain_boundaries.py index fa66c7ff5..3b5b0e1b9 100644 --- a/ergon_core/tests/unit/architecture/test_application_domain_boundaries.py +++ b/ergon_core/tests/unit/architecture/test_application_domain_boundaries.py @@ -22,7 +22,7 @@ "runtime": { "orchestration", "resources", - "run_lifecycle", + "sample_lifecycle", "task_execution", "task_inspection", "task_management", @@ -56,9 +56,9 @@ "lifecycle.py", "orchestration.py", "resources.py", - "run_identity.py", - "run_lifecycle.py", - "run_records.py", + "sample_identity.py", + "sample_lifecycle.py", + "sample_records.py", "status.py", "task_cleanup.py", "task_errors.py", diff --git a/ergon_core/tests/unit/architecture/test_core_schema_sources.py b/ergon_core/tests/unit/architecture/test_core_schema_sources.py index 9b9d3d3f5..c78757a0d 100644 --- a/ergon_core/tests/unit/architecture/test_core_schema_sources.py +++ b/ergon_core/tests/unit/architecture/test_core_schema_sources.py @@ -51,7 +51,7 @@ def test_eval_criterion_status_literal_is_defined_only_in_evaluation_summary() - def test_run_task_dto_does_not_label_worker_slug_as_name() -> None: - path = ROOT / "ergon_core/ergon_core/core/views/runs/models.py" + path = ROOT / "ergon_core/ergon_core/core/views/samples/models.py" text = path.read_text() assert "assigned_worker_name" not in text assert "assigned_worker_slug" in text @@ -86,7 +86,7 @@ def test_cancel_cause_literals_live_in_application_event_contracts() -> None: def test_core_schema_source_imports_are_directional() -> None: forbidden_pairs = { - "ergon_core.core.views.runs.models": ( + "ergon_core.core.views.samples.models": ( "EvalCriterionStatus = Literal", "GraphMutationValue =", ), @@ -418,7 +418,7 @@ def test_runtime_services_do_not_import_api_schema_modules() -> None: text = path.read_text() if ( "ergon_core.core.infrastructure.http.routes.schemas" in text - or "ergon_core.core.infrastructure.http.routes.runs" in text + or "ergon_core.core.infrastructure.http.routes.samples" in text ): offenders.append(str(path.relative_to(ROOT))) @@ -521,7 +521,7 @@ def test_runtime_and_builtins_do_not_use_task_execution_query_bag_for_domain_rea def test_resource_viewer_limits_live_with_read_model_resources() -> None: - api_path = ROOT / "ergon_core/ergon_core/core/infrastructure/http/routes/runs.py" + api_path = ROOT / "ergon_core/ergon_core/core/infrastructure/http/routes/samples.py" resource_path = ROOT / "ergon_core/ergon_core/core/views/resources.py" assert "_RESOURCE_CONTENT_MAX_BYTES" not in api_path.read_text() @@ -573,7 +573,7 @@ def test_workflow_lifecycle_has_one_front_door_service() -> None: spec = None assert spec is None - workflow_service = ROOT / "ergon_core/ergon_core/core/application/runtime/run_lifecycle.py" + workflow_service = ROOT / "ergon_core/ergon_core/core/application/runtime/sample_lifecycle.py" text = workflow_service.read_text() for method_name in ("initialize", "propagate", "propagate_failure", "finalize"): assert f"def {method_name}(" in text diff --git a/ergon_core/tests/unit/architecture/test_final_core_folder_state.py b/ergon_core/tests/unit/architecture/test_final_core_folder_state.py index 589b23861..b139bd8e4 100644 --- a/ergon_core/tests/unit/architecture/test_final_core_folder_state.py +++ b/ergon_core/tests/unit/architecture/test_final_core_folder_state.py @@ -164,9 +164,9 @@ def test_job_inngest_wrappers_do_not_query_sqlmodel_directly() -> None: def test_infrastructure_does_not_own_views_or_resource_append_policy() -> None: forbidden_snippets = ( - "RunResourceRepository(", - "RunResource(", - "session.add(RunResource", + "SampleResourceRepository(", + "SampleResource(", + "session.add(SampleResource", ) forbidden_view_builders = {"build_run_snapshot", "_build_task_tree_for_run"} offenders: list[str] = [] diff --git a/ergon_core/tests/unit/architecture/test_model_field_descriptions.py b/ergon_core/tests/unit/architecture/test_model_field_descriptions.py index 30e41b96d..dbf3e13fe 100644 --- a/ergon_core/tests/unit/architecture/test_model_field_descriptions.py +++ b/ergon_core/tests/unit/architecture/test_model_field_descriptions.py @@ -9,13 +9,13 @@ ToolResultPart, UserMessagePart, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.persistence.graph.models import ( - RunGraphAnnotation, - RunGraphMutation, - RunGraphNode, + SampleGraphAnnotation, + SampleGraphMutation, + SampleGraphNode, ) -from ergon_core.core.persistence.telemetry.models import RunRecord, RunResource +from ergon_core.core.persistence.telemetry.models import SampleRecord, SampleResource from ergon_core.core.application.runtime.models import ( GraphAnnotationDto, GraphEdgeDto, @@ -63,24 +63,26 @@ def test_graph_dto_field_docs_are_schema_metadata() -> None: def test_sqlmodel_field_docs_are_schema_metadata() -> None: - assert _description(RunGraphNode, "instance_key") - assert _description(RunGraphNode, "task_slug") - assert _description(RunGraphNode, "status") - assert _description(RunGraphNode, "assigned_worker_slug") - assert _description(RunGraphNode, "parent_task_id") - assert _description(RunGraphNode, "level") - assert _description(RunContextEvent, "event_type") - assert _description(RunContextEvent, "payload") - assert _description(RunGraphAnnotation, "target_type") - assert _description(RunGraphMutation, "mutation_type") - assert _description(RunGraphMutation, "target_type") - assert "Canonical runtime" in (_description(RunRecord, "definition_id") or "") - assert "Optional v2 experiment grouping tag" in (_description(RunRecord, "experiment") or "") - assert "Compatibility/display-only" in (_description(RunRecord, "worker_team_json") or "") - assert "Compatibility/display-only" in (_description(RunRecord, "evaluator_slug") or "") - assert "Compatibility/display-only" in (_description(RunRecord, "sandbox_slug") or "") - assert "Compatibility/display-only" in (_description(RunRecord, "dependency_extras_json") or "") - assert _description(RunResource, "kind") + assert _description(SampleGraphNode, "instance_key") + assert _description(SampleGraphNode, "task_slug") + assert _description(SampleGraphNode, "status") + assert _description(SampleGraphNode, "assigned_worker_slug") + assert _description(SampleGraphNode, "parent_task_id") + assert _description(SampleGraphNode, "level") + assert _description(SampleContextEvent, "event_type") + assert _description(SampleContextEvent, "payload") + assert _description(SampleGraphAnnotation, "target_type") + assert _description(SampleGraphMutation, "mutation_type") + assert _description(SampleGraphMutation, "target_type") + assert "Canonical runtime" in (_description(SampleRecord, "definition_id") or "") + assert "Optional v2 experiment grouping tag" in (_description(SampleRecord, "experiment") or "") + assert "Compatibility/display-only" in (_description(SampleRecord, "worker_team_json") or "") + assert "Compatibility/display-only" in (_description(SampleRecord, "evaluator_slug") or "") + assert "Compatibility/display-only" in (_description(SampleRecord, "sandbox_slug") or "") + assert "Compatibility/display-only" in ( + _description(SampleRecord, "dependency_extras_json") or "" + ) + assert _description(SampleResource, "kind") def test_builtin_task_schema_field_docs_are_schema_metadata() -> None: diff --git a/ergon_core/tests/unit/architecture/test_persistence_boundaries.py b/ergon_core/tests/unit/architecture/test_persistence_boundaries.py index 6556a9240..b842abf45 100644 --- a/ergon_core/tests/unit/architecture/test_persistence_boundaries.py +++ b/ergon_core/tests/unit/architecture/test_persistence_boundaries.py @@ -96,13 +96,13 @@ def test_persistence_import_reducer_models_are_absent() -> None: def test_run_record_uses_definition_id_as_single_runtime_definition_identity() -> None: - from ergon_core.core.persistence.telemetry.models import RunRecord + from ergon_core.core.persistence.telemetry.models import SampleRecord - assert "definition_id" in RunRecord.model_fields - assert ("workflow" + "_definition_id") not in RunRecord.model_fields + assert "definition_id" in SampleRecord.model_fields + assert ("workflow" + "_definition_id") not in SampleRecord.model_fields def test_run_record_does_not_expose_legacy_definition_group_identity() -> None: - from ergon_core.core.persistence.telemetry.models import RunRecord + from ergon_core.core.persistence.telemetry.models import SampleRecord - assert ("experiment" + "_id") not in RunRecord.model_fields + assert ("experiment" + "_id") not in SampleRecord.model_fields diff --git a/ergon_core/tests/unit/architecture/test_public_api_boundaries.py b/ergon_core/tests/unit/architecture/test_public_api_boundaries.py index e60e834e7..7cad1a732 100644 --- a/ergon_core/tests/unit/architecture/test_public_api_boundaries.py +++ b/ergon_core/tests/unit/architecture/test_public_api_boundaries.py @@ -272,9 +272,9 @@ def test_application_clusters_stay_out_of_runtime_layout() -> None: for new_path in ( core_root / "application" / "runtime" / "__init__.py", - core_root / "application" / "runtime" / "run_lifecycle.py", + core_root / "application" / "runtime" / "sample_lifecycle.py", core_root / "application" / "runtime" / "orchestration.py", - core_root / "application" / "runtime" / "run_records.py", + core_root / "application" / "runtime" / "sample_records.py", core_root / "application" / "runtime" / "workflow_models.py", core_root / "application" / "runtime" / "graph_repository.py", core_root / "application" / "runtime" / "lifecycle.py", @@ -325,7 +325,7 @@ def test_read_context_and_resource_modules_stay_in_application_and_views_layout( core_root / "runtime" / "resources.py", core_root / "application" / "read_models", core_root / "application" / "read_models" / "models.py", - core_root / "application" / "read_models" / "runs.py", + core_root / "application" / "read_models" / "samples.py", core_root / "application" / "read_models" / "run_snapshot.py", core_root / "application" / "read_models" / "experiments.py", core_root / "application" / "read_models" / "resources.py", @@ -345,10 +345,10 @@ def test_read_context_and_resource_modules_stay_in_application_and_views_layout( core_root / "application" / "resources" / "models.py", core_root / "application" / "resources" / "repository.py", core_root / "views" / "__init__.py", - core_root / "views" / "runs" / "__init__.py", - core_root / "views" / "runs" / "models.py", - core_root / "views" / "runs" / "service.py", - core_root / "views" / "runs" / "snapshot.py", + core_root / "views" / "samples" / "__init__.py", + core_root / "views" / "samples" / "models.py", + core_root / "views" / "samples" / "service.py", + core_root / "views" / "samples" / "snapshot.py", core_root / "views" / "experiments" / "__init__.py", core_root / "views" / "experiments" / "models.py", core_root / "views" / "experiments" / "service.py", @@ -382,7 +382,7 @@ def test_views_package_replaces_non_compat_read_models() -> None: for removed_path in ( read_models_root / "models.py", - read_models_root / "runs.py", + read_models_root / "samples.py", read_models_root / "run_snapshot.py", read_models_root / "experiments.py", read_models_root / "resources.py", @@ -393,10 +393,10 @@ def test_views_package_replaces_non_compat_read_models() -> None: for new_path in ( views_root / "__init__.py", - views_root / "runs" / "__init__.py", - views_root / "runs" / "models.py", - views_root / "runs" / "service.py", - views_root / "runs" / "snapshot.py", + views_root / "samples" / "__init__.py", + views_root / "samples" / "models.py", + views_root / "samples" / "service.py", + views_root / "samples" / "snapshot.py", views_root / "experiments" / "__init__.py", views_root / "experiments" / "models.py", views_root / "experiments" / "service.py", @@ -432,7 +432,7 @@ def test_views_services_may_read_persistence_rows() -> None: views_root = ROOT / "ergon_core" / "ergon_core" / "core" / "views" for path in ( - views_root / "runs" / "service.py", + views_root / "samples" / "service.py", views_root / "experiments" / "service.py", ): text = path.read_text() diff --git a/ergon_core/tests/unit/architecture/test_runtime_application_ownership.py b/ergon_core/tests/unit/architecture/test_runtime_application_ownership.py index d8e0a95e7..9a7c4f605 100644 --- a/ergon_core/tests/unit/architecture/test_runtime_application_ownership.py +++ b/ergon_core/tests/unit/architecture/test_runtime_application_ownership.py @@ -35,8 +35,8 @@ def test_runtime_is_single_application_owner_for_graph_task_and_workflow_lifecyc "lifecycle.py", "models.py", "resources.py", - "run_identity.py", - "run_lifecycle.py", + "sample_identity.py", + "sample_lifecycle.py", "status.py", "task_cleanup.py", "task_execution.py", @@ -67,7 +67,7 @@ def test_core_imports_do_not_reference_retired_runtime_packages() -> None: def test_runtime_services_do_not_reintroduce_duplicated_dispatch_or_identity_helpers() -> None: offenders: list[str] = [] for path in RUNTIME_ROOT.glob("*.py"): - if path.name in {"events.py", "run_identity.py"}: + if path.name in {"events.py", "sample_identity.py"}: continue tree = ast.parse(path.read_text(), filename=str(path)) for node in ast.walk(tree): @@ -87,7 +87,7 @@ def test_runtime_public_service_parameters_use_task_id_vocabulary() -> None: "ergon_core.core.application.runtime.task_inspection", "ergon_core.core.application.runtime.task_management", "ergon_core.core.application.runtime.resources", - "ergon_core.core.application.runtime.run_lifecycle", + "ergon_core.core.application.runtime.sample_lifecycle", ): module = __import__(module_name, fromlist=["*"]) for _, member in inspect.getmembers(module, inspect.isfunction): diff --git a/ergon_core/tests/unit/architecture/test_sample_vocabulary_boundaries.py b/ergon_core/tests/unit/architecture/test_sample_vocabulary_boundaries.py new file mode 100644 index 000000000..3862752ca --- /dev/null +++ b/ergon_core/tests/unit/architecture/test_sample_vocabulary_boundaries.py @@ -0,0 +1,64 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] + +CHECKED_ROOTS = [ + ROOT / "ergon_core" / "ergon_core", + ROOT / "ergon_cli" / "ergon_cli", + ROOT / "ergon-dashboard" / "src", + ROOT / "ergon-dashboard" / "tests", + ROOT / "examples", +] + +DISALLOWED_RUNTIME_NOUNS = [ + "run_id", + "runId", + "RunRecord", + "RunTask", + "RunGraph", + "RunResource", + "WorkflowRunState", + "/run/", + "/runs/", + 'data-testid="run-', + ".run-", + "--run-", + "RUNS", +] + +ALLOWED_VERB_SNIPPETS = [ + "uv run", + "pnpm run", + "subprocess.run(", + '"run"', + "'run'", + "examples run", +] + + +def test_runtime_surfaces_do_not_keep_run_as_identity_noun() -> None: + offenders: list[str] = [] + for root in CHECKED_ROOTS: + for path in root.rglob("*"): + if path.suffix not in {".py", ".ts", ".tsx", ".json", ".md"}: + continue + if "generated" in path.parts: + continue + if path.parts[-2:] == ("rl", "rollout_types.py") or path.parts[-2:] == ( + "rl", + "rollout_service.py", + ): + continue + for line_number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ): + if any(allowed in line for allowed in ALLOWED_VERB_SNIPPETS): + continue + for needle in DISALLOWED_RUNTIME_NOUNS: + if needle in line: + offenders.append( + f"{path.relative_to(ROOT)}:{line_number} contains {needle!r}" + ) + + assert offenders == [] diff --git a/ergon_core/tests/unit/architecture/test_sandbox_resource_boundary.py b/ergon_core/tests/unit/architecture/test_sandbox_resource_boundary.py index 594daeb07..a537cbebc 100644 --- a/ergon_core/tests/unit/architecture/test_sandbox_resource_boundary.py +++ b/ergon_core/tests/unit/architecture/test_sandbox_resource_boundary.py @@ -31,15 +31,15 @@ def _import_offenders(path: Path, tree: ast.AST) -> list[str]: def _import_from_offenders(path: Path, node: ast.ImportFrom) -> list[str]: if node.module == "ergon_core.core.application.resources.repository": return [ - f"{path.relative_to(ROOT)} imports RunResourceRepository" + f"{path.relative_to(ROOT)} imports SampleResourceRepository" for alias in node.names - if alias.name == "RunResourceRepository" + if alias.name == "SampleResourceRepository" ] if node.module == "ergon_core.core.persistence.telemetry.models": return [ - f"{path.relative_to(ROOT)} imports RunResource" + f"{path.relative_to(ROOT)} imports SampleResource" for alias in node.names - if alias.name == "RunResource" + if alias.name == "SampleResource" ] return [] @@ -60,12 +60,12 @@ def _call_offenders(path: Path, tree: ast.AST) -> list[str]: if not isinstance(node, ast.Call): continue called = _attr_name(node.func) - if called == "RunResource": - offenders.append(f"{path.relative_to(ROOT)} constructs RunResource") + if called == "SampleResource": + offenders.append(f"{path.relative_to(ROOT)} constructs SampleResource") if _is_repository_append(node, called): - offenders.append(f"{path.relative_to(ROOT)} appends through RunResourceRepository") - if called == "add" and node.args and _calls_name(node.args[0], "RunResource"): - offenders.append(f"{path.relative_to(ROOT)} adds RunResource through session") + offenders.append(f"{path.relative_to(ROOT)} appends through SampleResourceRepository") + if called == "add" and node.args and _calls_name(node.args[0], "SampleResource"): + offenders.append(f"{path.relative_to(ROOT)} adds SampleResource through session") return offenders diff --git a/ergon_core/tests/unit/architecture/test_v2_final_state_ledger.py b/ergon_core/tests/unit/architecture/test_v2_final_state_ledger.py index 84537a3e9..782415f6d 100644 --- a/ergon_core/tests/unit/architecture/test_v2_final_state_ledger.py +++ b/ergon_core/tests/unit/architecture/test_v2_final_state_ledger.py @@ -78,11 +78,11 @@ def _assert_evaluate_task_run_takes_thin_payload() -> None: ) -def _assert_run_graph_node_uses_task_id_primary_key() -> None: - from ergon_core.core.persistence.graph.models import RunGraphNode +def _assert_sample_graph_node_uses_task_id_primary_key() -> None: + from ergon_core.core.persistence.graph.models import SampleGraphNode - assert "task_id" in RunGraphNode.model_fields - assert "id" not in RunGraphNode.model_fields + assert "task_id" in SampleGraphNode.model_fields + assert "id" not in SampleGraphNode.model_fields def _assert_task_has_no_model_post_init() -> None: @@ -166,8 +166,8 @@ def _assert_no_check_evaluators_registration() -> None: reason="Δ.7: write-only package, no readers", ), FinalStateAssertion( - name="run_graph_node_uses_task_id_primary_key", - check=_assert_run_graph_node_uses_task_id_primary_key, + name="sample_graph_node_uses_task_id_primary_key", + check=_assert_sample_graph_node_uses_task_id_primary_key, reason="Δ.7 + identity model: task_id is the single canonical id", ), FinalStateAssertion( diff --git a/ergon_core/tests/unit/core/application/evaluation/test_inline_evaluator_persistence.py b/ergon_core/tests/unit/core/application/evaluation/test_inline_evaluator_persistence.py index 217cf342a..f97fea394 100644 --- a/ergon_core/tests/unit/core/application/evaluation/test_inline_evaluator_persistence.py +++ b/ergon_core/tests/unit/core/application/evaluation/test_inline_evaluator_persistence.py @@ -15,12 +15,12 @@ ExperimentDefinitionInstance, ExperimentDefinitionTask, ) -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunTaskEvaluation, - RunTaskExecution, + SampleRecord, + SampleTaskEvaluation, + SampleTaskAttempt, ) @@ -39,7 +39,7 @@ def _seed_inline_evaluator_run(session: Session) -> tuple: instance_id = uuid4() task_id = uuid4() evaluator_id = uuid4() - run_id = uuid4() + sample_id = uuid4() execution_id = uuid4() session.add_all( [ @@ -70,32 +70,32 @@ def _seed_inline_evaluator_run(session: Session) -> tuple: evaluator_type="rubric", snapshot_json={"name": "judge"}, ), - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="bench", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ), - RunGraphNode( - run_id=run_id, + SampleGraphNode( + sample_id=sample_id, task_id=task_id, instance_key="sample-1", task_slug="root", description="root task", status="running", ), - RunTaskExecution( + SampleTaskAttempt( id=execution_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, status=TaskExecutionStatus.RUNNING, ), ] ) session.commit() - return run_id, task_id, evaluator_id, execution_id + return sample_id, task_id, evaluator_id, execution_id @pytest.mark.asyncio @@ -105,11 +105,11 @@ async def test_persist_success_links_inline_evaluator_definition_row(monkeypatch session = _session() monkeypatch.setattr(module, "get_session", lambda: session) monkeypatch.setattr(session, "close", lambda: None) - run_id, task_id, evaluator_id, execution_id = _seed_inline_evaluator_run(session) + sample_id, task_id, evaluator_id, execution_id = _seed_inline_evaluator_run(session) service = EvaluationService() await service.persist_success( - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, task_id=task_id, binding_key="judge", @@ -125,7 +125,7 @@ async def test_persist_success_links_inline_evaluator_definition_row(monkeypatch ), ) - rows = session.exec(select(RunTaskEvaluation)).all() + rows = session.exec(select(SampleTaskEvaluation)).all() assert len(rows) == 1 assert rows[0].definition_evaluator_id == evaluator_id @@ -137,18 +137,18 @@ async def test_persist_failure_links_inline_evaluator_definition_row(monkeypatch session = _session() monkeypatch.setattr(module, "get_session", lambda: session) monkeypatch.setattr(session, "close", lambda: None) - run_id, task_id, evaluator_id, execution_id = _seed_inline_evaluator_run(session) + sample_id, task_id, evaluator_id, execution_id = _seed_inline_evaluator_run(session) service = EvaluationService() await service.persist_failure( - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, task_id=task_id, binding_key="judge", exc=RuntimeError("boom"), ) - rows = session.exec(select(RunTaskEvaluation)).all() + rows = session.exec(select(SampleTaskEvaluation)).all() assert len(rows) == 1 assert rows[0].definition_evaluator_id == evaluator_id @@ -162,11 +162,11 @@ async def test_persist_success_creates_dynamic_inline_evaluator_definition_row( session = _session() monkeypatch.setattr(module, "get_session", lambda: session) monkeypatch.setattr(session, "close", lambda: None) - run_id, task_id, _evaluator_id, execution_id = _seed_inline_evaluator_run(session) + sample_id, task_id, _evaluator_id, execution_id = _seed_inline_evaluator_run(session) service = EvaluationService() await service.persist_success( - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, task_id=task_id, binding_key="dynamic-judge", @@ -187,5 +187,5 @@ async def test_persist_success_creates_dynamic_inline_evaluator_definition_row( ExperimentDefinitionEvaluator.binding_key == "dynamic-judge" ) ).one() - rows = session.exec(select(RunTaskEvaluation)).all() + rows = session.exec(select(SampleTaskEvaluation)).all() assert rows[-1].definition_evaluator_id == evaluator_row.id diff --git a/ergon_core/tests/unit/core/application/jobs/test_execute_task_object_bound_fanout.py b/ergon_core/tests/unit/core/application/jobs/test_execute_task_object_bound_fanout.py index dc868679b..ea75390c5 100644 --- a/ergon_core/tests/unit/core/application/jobs/test_execute_task_object_bound_fanout.py +++ b/ergon_core/tests/unit/core/application/jobs/test_execute_task_object_bound_fanout.py @@ -35,14 +35,14 @@ class _FakeTaskExecutionService: def __init__(self, task: SimpleNamespace) -> None: self._task = task - async def load_task_view(self, _session: object, *, run_id, task_id, sandbox_id=None): - del run_id, task_id, sandbox_id + async def load_task_view(self, _session: object, *, sample_id, task_id, sandbox_id=None): + del sample_id, task_id, sandbox_id return SimpleNamespace(task=self._task) -def _prepared(run_id, definition_id, task_id, execution_id) -> PreparedTaskExecution: +def _prepared(sample_id, definition_id, task_id, execution_id) -> PreparedTaskExecution: return PreparedTaskExecution( - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=task_id, task_slug="root", @@ -54,7 +54,7 @@ def _prepared(run_id, definition_id, task_id, execution_id) -> PreparedTaskExecu @pytest.mark.asyncio async def test_fanout_uses_object_bound_evaluator_count(monkeypatch) -> None: - run_id = uuid4() + sample_id = uuid4() definition_id = uuid4() task_id = uuid4() execution_id = uuid4() @@ -71,11 +71,11 @@ async def test_fanout_uses_object_bound_evaluator_count(monkeypatch) -> None: ctx, _FakeTaskExecutionService(task), TaskReadyEvent( - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=task_id, ), - _prepared(run_id, definition_id, task_id, execution_id), + _prepared(sample_id, definition_id, task_id, execution_id), evaluate_task_run_function=object(), ) @@ -84,7 +84,7 @@ async def test_fanout_uses_object_bound_evaluator_count(monkeypatch) -> None: @pytest.mark.asyncio async def test_fanout_emits_no_jobs_without_inline_evaluators(monkeypatch) -> None: - run_id = uuid4() + sample_id = uuid4() definition_id = uuid4() task_id = uuid4() execution_id = uuid4() @@ -101,11 +101,11 @@ async def test_fanout_emits_no_jobs_without_inline_evaluators(monkeypatch) -> No ctx, _FakeTaskExecutionService(task), TaskReadyEvent( - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=task_id, ), - _prepared(run_id, definition_id, task_id, execution_id), + _prepared(sample_id, definition_id, task_id, execution_id), evaluate_task_run_function=object(), ) diff --git a/ergon_core/tests/unit/core/application/jobs/test_persist_outputs_public_sandbox.py b/ergon_core/tests/unit/core/application/jobs/test_persist_outputs_public_sandbox.py index d12b18ae4..36e27e775 100644 --- a/ergon_core/tests/unit/core/application/jobs/test_persist_outputs_public_sandbox.py +++ b/ergon_core/tests/unit/core/application/jobs/test_persist_outputs_public_sandbox.py @@ -12,8 +12,8 @@ class _FakeTaskExecutionService: def __init__(self, seen: list[str | None]) -> None: self._seen = seen - async def load_task_view(self, _session, *, run_id, task_id, sandbox_id=None): - del run_id, task_id + async def load_task_view(self, _session, *, sample_id, task_id, sandbox_id=None): + del sample_id, task_id self._seen.append(sandbox_id) sandbox = SimpleNamespace( output_path="/workspace/public-output/", @@ -54,11 +54,11 @@ async def test_persist_outputs_publishes_from_public_sandbox_output_path(monkeyp lambda: _FakeTaskExecutionService(seen_sandbox_ids), ) monkeypatch.setattr(composition, "SandboxResourcePublisher", _FakePublisher) - monkeypatch.setattr(composition, "RunResourcePublishService", _FakePublishService) + monkeypatch.setattr(composition, "SampleResourcePublishService", _FakePublishService) result = await run_persist_outputs_job( PersistOutputsRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), diff --git a/ergon_core/tests/unit/core/application/jobs/test_sandbox_setup_public_sandbox.py b/ergon_core/tests/unit/core/application/jobs/test_sandbox_setup_public_sandbox.py index edc4b88b5..4b4b3df4d 100644 --- a/ergon_core/tests/unit/core/application/jobs/test_sandbox_setup_public_sandbox.py +++ b/ergon_core/tests/unit/core/application/jobs/test_sandbox_setup_public_sandbox.py @@ -38,8 +38,8 @@ class _FakeTaskExecutionService: def __init__(self, sandbox: _PublicSandbox) -> None: self._sandbox = sandbox - async def load_task_view(self, _session, *, run_id, task_id, sandbox_id=None): - del run_id, task_id, sandbox_id + async def load_task_view(self, _session, *, sample_id, task_id, sandbox_id=None): + del sample_id, task_id, sandbox_id return SimpleNamespace(task=SimpleNamespace(sandbox=self._sandbox)) @@ -54,7 +54,7 @@ async def test_sandbox_setup_provisions_public_sandbox(monkeypatch) -> None: result = await run_sandbox_setup_job( _FakeCtx(), SandboxSetupRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), benchmark_type="benchmark", diff --git a/ergon_core/tests/unit/core/application/jobs/test_worker_execute_live_sandbox_attach.py b/ergon_core/tests/unit/core/application/jobs/test_worker_execute_live_sandbox_attach.py index 0c81c6d84..d1b50e0f0 100644 --- a/ergon_core/tests/unit/core/application/jobs/test_worker_execute_live_sandbox_attach.py +++ b/ergon_core/tests/unit/core/application/jobs/test_worker_execute_live_sandbox_attach.py @@ -26,8 +26,8 @@ def __init__(self, seen: list[str | None]) -> None: self.persisted_outputs = [] self.attached_sandboxes = [] - async def load_task_view(self, _session, *, run_id, task_id, sandbox_id=None): - del run_id, task_id + async def load_task_view(self, _session, *, sample_id, task_id, sandbox_id=None): + del sample_id, task_id self._seen.append(sandbox_id) sandbox = SimpleNamespace(is_live=sandbox_id == "sbx-live") return SimpleNamespace(task=SimpleNamespace(worker=_FakeWorker(), sandbox=sandbox)) @@ -63,7 +63,7 @@ async def _publish(_event): monkeypatch.setattr(module.ContextEventService, "persist_chunk", _persist) monkeypatch.setattr(module, "TaskManagementService", lambda **kwargs: object()) monkeypatch.setattr(module, "TaskInspectionService", lambda: object()) - monkeypatch.setattr(module, "RunResourceReadService", lambda: object()) + monkeypatch.setattr(module, "SampleResourceReadService", lambda: object()) monkeypatch.setattr( module, "get_dashboard_event_publisher", @@ -72,7 +72,7 @@ async def _publish(_event): result = await run_worker_execute_job( WorkerExecuteJobRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), @@ -97,8 +97,8 @@ async def test_worker_execute_rejects_object_bound_worker_without_live_sandbox( from ergon_core.core.jobs.task.worker_execute import job as module class _NonLiveTaskExecutionService: - async def load_task_view(self, _session, *, run_id, task_id, sandbox_id=None): - del run_id, task_id, sandbox_id + async def load_task_view(self, _session, *, sample_id, task_id, sandbox_id=None): + del sample_id, task_id, sandbox_id return SimpleNamespace( task=SimpleNamespace( worker=_FakeWorker(), @@ -110,12 +110,12 @@ async def load_task_view(self, _session, *, run_id, task_id, sandbox_id=None): monkeypatch.setattr(module, "TaskExecutionService", lambda: _NonLiveTaskExecutionService()) monkeypatch.setattr(module, "TaskManagementService", lambda: object()) monkeypatch.setattr(module, "TaskInspectionService", lambda: object()) - monkeypatch.setattr(module, "RunResourceReadService", lambda: object()) + monkeypatch.setattr(module, "SampleResourceReadService", lambda: object()) with pytest.raises(Exception, match="live sandbox"): await run_worker_execute_job( WorkerExecuteJobRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), @@ -142,7 +142,7 @@ async def send_event(self, step_id: str, event: object) -> None: sent.append((step_id, event)) task_id = uuid4() - run_id = uuid4() + sample_id = uuid4() definition_id = uuid4() async def _publish(_event): @@ -156,7 +156,7 @@ async def _publish(_event): service = module._StepAwareTaskManagementService(SimpleNamespace(step=_Step())) await service._dispatch_collected_ready_events( "plan-subtasks-test", - [module._ReadyDispatch(run_id=run_id, definition_id=definition_id, task_id=task_id)], + [module._ReadyDispatch(sample_id=sample_id, definition_id=definition_id, task_id=task_id)], ) assert len(sent) == 1 @@ -164,6 +164,6 @@ async def _publish(_event): assert step_id == f"plan-subtasks-test-dispatch-task-ready-{task_id}" payload = TaskReadyEvent.model_validate(event.data) assert event.name == TaskReadyEvent.name - assert payload.run_id == run_id + assert payload.sample_id == sample_id assert payload.definition_id == definition_id assert payload.task_id == task_id diff --git a/ergon_core/tests/unit/core/application/tasks/test_spawn_dynamic_task_dispatch.py b/ergon_core/tests/unit/core/application/tasks/test_spawn_dynamic_task_dispatch.py index f0a868469..efd49ebdf 100644 --- a/ergon_core/tests/unit/core/application/tasks/test_spawn_dynamic_task_dispatch.py +++ b/ergon_core/tests/unit/core/application/tasks/test_spawn_dynamic_task_dispatch.py @@ -32,18 +32,18 @@ def __init__(self) -> None: def add_mutation_listener(self, listener) -> None: del listener - def get_node(self, session, *, run_id, task_id): - del session, run_id, task_id + def get_node(self, session, *, sample_id, task_id): + del session, sample_id, task_id return self.parent - async def add_node(self, session, run_id, **kwargs): - del session, run_id + async def add_node(self, session, sample_id, **kwargs): + del session, sample_id node = SimpleNamespace(task_id=uuid4(), **kwargs) self.added_nodes.append(kwargs) return node - async def add_edge(self, session, run_id, **kwargs): - del session, run_id + async def add_edge(self, session, sample_id, **kwargs): + del session, sample_id self.added_edges.append(kwargs) @@ -55,8 +55,10 @@ async def test_spawn_dynamic_task_dispatches_ready_event_when_dependency_free(mo graph_repo = _FakeGraphRepo() dispatched: list[dict] = [] - async def dispatch_task_ready(run_id, definition_id, task_id): - dispatched.append({"run_id": run_id, "definition_id": definition_id, "task_id": task_id}) + async def dispatch_task_ready(sample_id, definition_id, task_id): + dispatched.append( + {"sample_id": sample_id, "definition_id": definition_id, "task_id": task_id} + ) service = TaskManagementService( graph_repo=graph_repo, @@ -68,7 +70,7 @@ async def dispatch_task_ready(run_id, definition_id, task_id): monkeypatch.setattr(module, "definition_id_for_run", lambda _session, _run_id: uuid4()) handle = await service.spawn_dynamic_task( - run_id=uuid4(), + sample_id=uuid4(), parent_task_id=uuid4(), task=_DynamicTask( task_slug="child", @@ -81,7 +83,7 @@ async def dispatch_task_ready(run_id, definition_id, task_id): assert dispatched == [ { - "run_id": dispatched[0]["run_id"], + "sample_id": dispatched[0]["sample_id"], "definition_id": dispatched[0]["definition_id"], "task_id": handle.task_id, } @@ -100,8 +102,10 @@ async def test_spawn_dynamic_task_with_dependencies_waits_for_propagation(monkey graph_repo = _FakeGraphRepo() dispatched: list[dict] = [] - async def dispatch_task_ready(run_id, definition_id, task_id): - dispatched.append({"run_id": run_id, "definition_id": definition_id, "task_id": task_id}) + async def dispatch_task_ready(sample_id, definition_id, task_id): + dispatched.append( + {"sample_id": sample_id, "definition_id": definition_id, "task_id": task_id} + ) service = TaskManagementService( graph_repo=graph_repo, @@ -113,7 +117,7 @@ async def dispatch_task_ready(run_id, definition_id, task_id): dependency_id = uuid4() await service.spawn_dynamic_task( - run_id=uuid4(), + sample_id=uuid4(), parent_task_id=uuid4(), task=_DynamicTask( task_slug="child", diff --git a/ergon_core/tests/unit/dashboard/test_communication_threads.py b/ergon_core/tests/unit/dashboard/test_communication_threads.py index 1ce86fc71..38f404e82 100644 --- a/ergon_core/tests/unit/dashboard/test_communication_threads.py +++ b/ergon_core/tests/unit/dashboard/test_communication_threads.py @@ -1,17 +1,17 @@ from uuid import uuid4 from ergon_core.core.persistence.telemetry.models import Thread, ThreadMessage -from ergon_core.core.views.runs.snapshot import _build_communication_threads +from ergon_core.core.views.samples.snapshot import _build_communication_threads def test_build_communication_threads_populates_summary_and_task_anchors() -> None: - run_id = uuid4() + sample_id = uuid4() thread_id = uuid4() execution_id = uuid4() task_id = uuid4() thread = Thread( id=thread_id, - run_id=run_id, + sample_id=sample_id, topic="smoke-completion", summary="Leaf workers report completion artifacts and probe exit status.", agent_a_id="leaf-l_1", @@ -19,7 +19,7 @@ def test_build_communication_threads_populates_summary_and_task_anchors() -> Non ) message = ThreadMessage( thread_id=thread_id, - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, from_agent_id="leaf-l_1", to_agent_id="parent", @@ -42,13 +42,13 @@ def test_build_communication_threads_populates_summary_and_task_anchors() -> Non def test_build_communication_threads_keeps_run_level_thread_when_messages_span_tasks() -> None: - run_id = uuid4() + sample_id = uuid4() thread_id = uuid4() execution_a = uuid4() execution_b = uuid4() thread = Thread( id=thread_id, - run_id=run_id, + sample_id=sample_id, topic="smoke-completion", agent_a_id="leaf-l_1", agent_b_id="parent", @@ -56,7 +56,7 @@ def test_build_communication_threads_keeps_run_level_thread_when_messages_span_t messages = [ ThreadMessage( thread_id=thread_id, - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_a, from_agent_id="leaf-l_1", to_agent_id="parent", @@ -65,7 +65,7 @@ def test_build_communication_threads_keeps_run_level_thread_when_messages_span_t ), ThreadMessage( thread_id=thread_id, - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_b, from_agent_id="leaf-l_2", to_agent_id="parent", diff --git a/ergon_core/tests/unit/dashboard/test_event_contract_types.py b/ergon_core/tests/unit/dashboard/test_event_contract_types.py index 9f5b23587..bd7138661 100644 --- a/ergon_core/tests/unit/dashboard/test_event_contract_types.py +++ b/ergon_core/tests/unit/dashboard/test_event_contract_types.py @@ -15,10 +15,10 @@ DashboardThreadMessageCreatedEvent, DashboardWorkflowStartedEvent, ) -from ergon_core.core.views.runs.models import ( +from ergon_core.core.views.samples.models import ( RunCommunicationMessageDto, RunCommunicationThreadDto, - RunSnapshotDto, + SampleSnapshotDto, ) @@ -61,7 +61,7 @@ def test_thread_dto_exposes_summary_and_task_identity() -> None: def test_workflow_started_event_embeds_run_snapshot_contract() -> None: assert "task_tree" not in DashboardWorkflowStartedEvent.model_fields - assert DashboardWorkflowStartedEvent.model_fields["snapshot"].annotation is RunSnapshotDto + assert DashboardWorkflowStartedEvent.model_fields["snapshot"].annotation is SampleSnapshotDto @pytest.mark.asyncio @@ -77,7 +77,7 @@ async def send(event) -> None: emitter = DashboardEmitter(enabled=True) event = DashboardTaskStatusChangedEvent( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), task_name="task", new_status="running", diff --git a/ergon_core/tests/unit/evaluation/test_evaluation_dto_mapping.py b/ergon_core/tests/unit/evaluation/test_evaluation_dto_mapping.py index b8072f4b7..10cf45072 100644 --- a/ergon_core/tests/unit/evaluation/test_evaluation_dto_mapping.py +++ b/ergon_core/tests/unit/evaluation/test_evaluation_dto_mapping.py @@ -1,13 +1,13 @@ from uuid import uuid4 from ergon_core.core.application.evaluation.summary import EvaluationSummary -from ergon_core.core.persistence.telemetry.models import RunTaskEvaluation -from ergon_core.core.views.runs.evaluation_mapping import evaluation_row_to_dto +from ergon_core.core.persistence.telemetry.models import SampleTaskEvaluation +from ergon_core.core.views.samples.evaluation_mapping import evaluation_row_to_dto def test_evaluation_row_to_dto_maps_multiple_criterion_outcomes() -> None: evaluation_id = uuid4() - run_id = uuid4() + sample_id = uuid4() task_id = uuid4() summary = EvaluationSummary( evaluator_name="judge", @@ -52,9 +52,9 @@ def test_evaluation_row_to_dto_maps_multiple_criterion_outcomes() -> None: }, ], ) - row = RunTaskEvaluation( + row = SampleTaskEvaluation( id=evaluation_id, - run_id=run_id, + sample_id=sample_id, task_execution_id=uuid4(), task_id=task_id, definition_evaluator_id=uuid4(), @@ -67,7 +67,7 @@ def test_evaluation_row_to_dto_maps_multiple_criterion_outcomes() -> None: dto = evaluation_row_to_dto(row) assert dto.id == str(evaluation_id) - assert dto.run_id == str(run_id) + assert dto.sample_id == str(sample_id) assert dto.task_id == str(task_id) assert dto.evaluator_name == "judge" assert dto.total_score == 1.5 diff --git a/ergon_core/tests/unit/evaluation/test_evaluation_persistence.py b/ergon_core/tests/unit/evaluation/test_evaluation_persistence.py index 8c7d71526..3e68d3bdb 100644 --- a/ergon_core/tests/unit/evaluation/test_evaluation_persistence.py +++ b/ergon_core/tests/unit/evaluation/test_evaluation_persistence.py @@ -15,12 +15,12 @@ ExperimentDefinitionInstance, ExperimentDefinitionTask, ) -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunTaskEvaluation, - RunTaskExecution, + SampleRecord, + SampleTaskEvaluation, + SampleTaskAttempt, ) @@ -39,7 +39,7 @@ def _seed_run(session: Session) -> tuple: instance_id = uuid4() task_id = uuid4() evaluator_id = uuid4() - run_id = uuid4() + sample_id = uuid4() execution_id = uuid4() session.add_all( [ @@ -70,32 +70,32 @@ def _seed_run(session: Session) -> tuple: evaluator_type="rubric", snapshot_json={"name": "judge"}, ), - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="bench", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ), - RunGraphNode( - run_id=run_id, + SampleGraphNode( + sample_id=sample_id, task_id=task_id, instance_key="sample-1", task_slug="root", description="root task", status="running", ), - RunTaskExecution( + SampleTaskAttempt( id=execution_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, status=TaskExecutionStatus.RUNNING, ), ] ) session.commit() - return run_id, task_id, evaluator_id, execution_id + return sample_id, task_id, evaluator_id, execution_id @pytest.mark.asyncio @@ -105,10 +105,10 @@ async def test_persist_success_writes_evaluation_row_with_service_summary(monkey session = _session() monkeypatch.setattr(service_module, "get_session", lambda: session) monkeypatch.setattr(session, "close", lambda: None) - run_id, task_id, evaluator_id, execution_id = _seed_run(session) + sample_id, task_id, evaluator_id, execution_id = _seed_run(session) persisted = await EvaluationService().persist_success( - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, task_id=task_id, binding_key="judge", @@ -124,7 +124,7 @@ async def test_persist_success_writes_evaluation_row_with_service_summary(monkey ), ) - row = session.exec(select(RunTaskEvaluation)).one() + row = session.exec(select(SampleTaskEvaluation)).one() assert row.summary_json == persisted.summary.model_dump(mode="json") assert row.score == 0.75 assert row.passed is True diff --git a/ergon_core/tests/unit/persistence/test_context_event_repository.py b/ergon_core/tests/unit/persistence/test_context_event_repository.py index 8fd90b9b0..53f11cab9 100644 --- a/ergon_core/tests/unit/persistence/test_context_event_repository.py +++ b/ergon_core/tests/unit/persistence/test_context_event_repository.py @@ -11,14 +11,14 @@ ToolResultPart, UserMessagePart, ) -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.application.context.service import ContextEventService from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunTaskExecution, + SampleRecord, + SampleTaskAttempt, ) from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine @@ -36,11 +36,11 @@ def _session() -> Session: def _execution_fixture(session: Session) -> tuple: - run_id = uuid4() + sample_id = uuid4() definition_id = uuid4() task_id = uuid4() - node = RunGraphNode( - run_id=run_id, + node = SampleGraphNode( + sample_id=sample_id, task_id=task_id, instance_key="instance", task_slug="task", @@ -57,25 +57,25 @@ def _execution_fixture(session: Session) -> tuple: ) ) session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="unit", instance_key="instance", - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) session.add(node) session.flush() - execution = RunTaskExecution( - run_id=run_id, + execution = SampleTaskAttempt( + sample_id=sample_id, task_id=node.task_id, node_id=node.task_id, status=TaskExecutionStatus.RUNNING, ) session.add(execution) session.commit() - return run_id, execution.id + return sample_id, execution.id def test_run_context_event_parsed_payload_is_context_part_chunk_log() -> None: @@ -85,8 +85,8 @@ def test_run_context_event_parsed_payload_is_context_part_chunk_log() -> None: worker_binding_key="worker-a", turn_id="turn-1", ) - event = RunContextEvent( - run_id=uuid4(), + event = SampleContextEvent( + sample_id=uuid4(), task_execution_id=uuid4(), worker_binding_key="worker-a", sequence=3, @@ -103,26 +103,26 @@ def test_run_context_event_parsed_payload_is_context_part_chunk_log() -> None: @pytest.mark.asyncio async def test_persist_chunk_records_prompt_and_model_output_in_order() -> None: session = _session() - run_id, execution_id = _execution_fixture(session) + sample_id, execution_id = _execution_fixture(session) repo = ContextEventService() await repo.persist_chunk( session, - run_id=run_id, + sample_id=sample_id, execution_id=execution_id, worker_binding_key="worker", chunk=ContextPartChunk(part=UserMessagePart(content="question")), ) await repo.persist_chunk( session, - run_id=run_id, + sample_id=sample_id, execution_id=execution_id, worker_binding_key="worker", chunk=ContextPartChunk(part=ThinkingPart(content="think")), ) await repo.persist_chunk( session, - run_id=run_id, + sample_id=sample_id, execution_id=execution_id, worker_binding_key="worker", chunk=ContextPartChunk(part=AssistantTextPart(content="answer")), @@ -142,12 +142,12 @@ async def test_persist_chunk_records_prompt_and_model_output_in_order() -> None: @pytest.mark.asyncio async def test_persist_chunk_records_provider_usage() -> None: session = _session() - run_id, execution_id = _execution_fixture(session) + sample_id, execution_id = _execution_fixture(session) repo = ContextEventService() await repo.persist_chunk( session, - run_id=run_id, + sample_id=sample_id, execution_id=execution_id, worker_binding_key="worker", chunk=ContextPartChunk( @@ -184,12 +184,12 @@ async def test_persist_chunk_records_provider_usage() -> None: @pytest.mark.asyncio async def test_persist_chunk_tool_result_closes_current_turn() -> None: session = _session() - run_id, execution_id = _execution_fixture(session) + sample_id, execution_id = _execution_fixture(session) repo = ContextEventService() await repo.persist_chunk( session, - run_id=run_id, + sample_id=sample_id, execution_id=execution_id, worker_binding_key="worker", chunk=ContextPartChunk( @@ -198,7 +198,7 @@ async def test_persist_chunk_tool_result_closes_current_turn() -> None: ) await repo.persist_chunk( session, - run_id=run_id, + sample_id=sample_id, execution_id=execution_id, worker_binding_key="worker", chunk=ContextPartChunk( diff --git a/ergon_core/tests/unit/read_models/test_experiment_read_service.py b/ergon_core/tests/unit/read_models/test_experiment_read_service.py index 40a553f17..dd1171e12 100644 --- a/ergon_core/tests/unit/read_models/test_experiment_read_service.py +++ b/ergon_core/tests/unit/read_models/test_experiment_read_service.py @@ -7,10 +7,10 @@ ExperimentDefinitionInstance, ExperimentDefinitionTask, ) -from ergon_core.core.persistence.context.models import RunContextEvent -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.context.models import SampleContextEvent +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.shared.context_parts import ( AssistantTextPart, ContextPartChunkLog, @@ -28,8 +28,8 @@ def session_factory(): _ = ExperimentDefinition _ = ExperimentDefinitionInstance _ = ExperimentDefinitionTask - _ = RunContextEvent - _ = RunGraphNode + _ = SampleContextEvent + _ = SampleGraphNode engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, @@ -72,14 +72,14 @@ def test_experiment_detail_aggregates_run_analytics(monkeypatch, session_factory instance_key=instance_key, ) ) - for run_id, instance_key, status, started, completed, score, cost in [ - (run_a_id, "a", RunStatus.COMPLETED, now, now + timedelta(seconds=10), 1.0, 0.2), - (run_b_id, "b", RunStatus.FAILED, now, now + timedelta(seconds=20), 0.0, 0.3), - (run_c_id, "c", RunStatus.EXECUTING, now, None, None, None), + for sample_id, instance_key, status, started, completed, score, cost in [ + (run_a_id, "a", SampleStatus.COMPLETED, now, now + timedelta(seconds=10), 1.0, 0.2), + (run_b_id, "b", SampleStatus.FAILED, now, now + timedelta(seconds=20), 0.0, 0.3), + (run_c_id, "c", SampleStatus.EXECUTING, now, None, None, None), ]: session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="ci-benchmark", instance_key=instance_key, @@ -102,8 +102,8 @@ def test_experiment_detail_aggregates_run_analytics(monkeypatch, session_factory ) for index in range(2): session.add( - RunGraphNode( - run_id=run_id, + SampleGraphNode( + sample_id=sample_id, instance_key=instance_key, task_slug=f"{instance_key}-{index}", description="Task", @@ -134,7 +134,7 @@ def test_experiment_detail_aggregates_run_analytics(monkeypatch, session_factory def test_experiment_run_rows_project_nested_metrics(monkeypatch, session_factory) -> None: now = datetime(2026, 4, 27, 12, 0, tzinfo=UTC) definition_id = uuid4() - run_id = uuid4() + sample_id = uuid4() execution_id = uuid4() with session_factory() as session: @@ -148,8 +148,8 @@ def test_experiment_run_rows_project_nested_metrics(monkeypatch, session_factory ) ) session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="metric-benchmark", instance_key="sample-1", @@ -157,7 +157,7 @@ def test_experiment_run_rows_project_nested_metrics(monkeypatch, session_factory worker_team_json={"primary": "ci-worker"}, evaluator_slug="metric-evaluator", model_target="openai:gpt-4o", - status=RunStatus.COMPLETED, + status=SampleStatus.COMPLETED, started_at=now, completed_at=now + timedelta(seconds=3), summary_json={ @@ -168,8 +168,8 @@ def test_experiment_run_rows_project_nested_metrics(monkeypatch, session_factory ) ) session.add( - RunGraphNode( - run_id=run_id, + SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug="root", description="Task", @@ -179,8 +179,8 @@ def test_experiment_run_rows_project_nested_metrics(monkeypatch, session_factory ) ) session.add( - RunContextEvent( - run_id=run_id, + SampleContextEvent( + sample_id=sample_id, task_execution_id=execution_id, worker_binding_key="ci-worker", sequence=0, @@ -194,8 +194,8 @@ def test_experiment_run_rows_project_nested_metrics(monkeypatch, session_factory ) ) session.add( - RunContextEvent( - run_id=run_id, + SampleContextEvent( + sample_id=sample_id, task_execution_id=execution_id, worker_binding_key="ci-worker", sequence=1, @@ -222,7 +222,7 @@ def test_experiment_run_rows_project_nested_metrics(monkeypatch, session_factory row = detail.runs[0] assert row.running_time_ms == 3_000 assert row.total_cost_usd is None - assert row.metrics.run_id == run_id + assert row.metrics.sample_id == sample_id assert row.metrics.run_name == "sample label" assert row.metrics.status == "completed" assert row.metrics.sample_label == "sample label" @@ -266,20 +266,20 @@ def test_experiment_detail_groups_runs_by_experiment_tag(monkeypatch, session_fa metadata_json={"experiment": "group-alpha"}, ) ) - for run_id, definition_id, experiment in ( + for sample_id, definition_id, experiment in ( (grouped_run_a, definition_a, "group-alpha"), (grouped_run_b, definition_b, "group-alpha"), (ungrouped_run, definition_a, "other-group"), ): session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="ci-benchmark", - instance_key=str(run_id), + instance_key=str(sample_id), worker_team_json={}, experiment=experiment, - status=RunStatus.COMPLETED, + status=SampleStatus.COMPLETED, ) ) session.commit() @@ -289,7 +289,7 @@ def test_experiment_detail_groups_runs_by_experiment_tag(monkeypatch, session_fa detail = ExperimentReadService().get_experiment(definition_a) assert detail is not None - assert {run.run_id for run in detail.runs} == {grouped_run_a, grouped_run_b} + assert {run.sample_id for run in detail.runs} == {grouped_run_a, grouped_run_b} assert detail.experiment.run_count == 2 assert detail.analytics.total_runs == 2 @@ -365,10 +365,10 @@ def test_list_experiments_projects_aggregate_lifecycle_status(monkeypatch, sessi ) ) for index, status in enumerate( - (RunStatus.COMPLETED, RunStatus.COMPLETED, RunStatus.FAILED) + (SampleStatus.COMPLETED, SampleStatus.COMPLETED, SampleStatus.FAILED) ): session.add( - RunRecord( + SampleRecord( definition_id=definition_id, benchmark_type="ci-benchmark", instance_key=f"sample-{index}", diff --git a/ergon_core/tests/unit/read_models/test_run_metrics.py b/ergon_core/tests/unit/read_models/test_sample_metrics.py similarity index 93% rename from ergon_core/tests/unit/read_models/test_run_metrics.py rename to ergon_core/tests/unit/read_models/test_sample_metrics.py index b9c80d8cb..db8740982 100644 --- a/ergon_core/tests/unit/read_models/test_run_metrics.py +++ b/ergon_core/tests/unit/read_models/test_sample_metrics.py @@ -1,6 +1,6 @@ from uuid import uuid4 -from ergon_core.core.persistence.context.models import RunContextEvent +from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.shared.context_parts import ( AssistantTextPart, ContextPartChunkLog, @@ -10,15 +10,15 @@ ToolResultPart, UserMessagePart, ) -from ergon_core.core.views.runs.metrics import ( +from ergon_core.core.views.samples.metrics import ( aggregate_run_metrics, observed_cost_from_summary, ) -def _event(event_type: str, payload: ContextPartChunkLog) -> RunContextEvent: - return RunContextEvent( - run_id=uuid4(), +def _event(event_type: str, payload: ContextPartChunkLog) -> SampleContextEvent: + return SampleContextEvent( + sample_id=uuid4(), task_execution_id=uuid4(), worker_binding_key="worker", sequence=payload.sequence, diff --git a/ergon_core/tests/unit/read_models/test_run_read_service.py b/ergon_core/tests/unit/read_models/test_sample_read_service.py similarity index 85% rename from ergon_core/tests/unit/read_models/test_run_read_service.py rename to ergon_core/tests/unit/read_models/test_sample_read_service.py index bfdd580de..c891559c3 100644 --- a/ergon_core/tests/unit/read_models/test_run_read_service.py +++ b/ergon_core/tests/unit/read_models/test_sample_read_service.py @@ -3,11 +3,11 @@ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord -from ergon_core.core.views.runs import service as module -from ergon_core.core.views.runs.service import RunReadService +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord +from ergon_core.core.views.samples import service as module +from ergon_core.core.views.samples.service import SampleSnapshotReadService from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine @@ -15,7 +15,7 @@ @pytest.fixture() def session_factory(): _ = ExperimentDefinition - _ = RunGraphNode + _ = SampleGraphNode engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, @@ -55,19 +55,19 @@ def test_list_runs_filters_offsets_and_projects_index_summary(monkeypatch, sessi ) ) session.add( - RunRecord( + SampleRecord( id=older_run_id, definition_id=definition_id, benchmark_type="miniwob", instance_key="task-old", sample_id="sample-old", experiment="alpha", - status=RunStatus.COMPLETED, + status=SampleStatus.COMPLETED, created_at=now - timedelta(hours=2), ) ) session.add( - RunRecord( + SampleRecord( id=matching_run_id, definition_id=definition_id, benchmark_type="miniwob", @@ -76,7 +76,7 @@ def test_list_runs_filters_offsets_and_projects_index_summary(monkeypatch, sessi experiment="alpha", evaluator_slug="judge-v1", model_target="openai:gpt-4.1", - status=RunStatus.COMPLETED, + status=SampleStatus.COMPLETED, created_at=now - timedelta(hours=1), started_at=now - timedelta(minutes=55), completed_at=now - timedelta(minutes=5), @@ -90,20 +90,20 @@ def test_list_runs_filters_offsets_and_projects_index_summary(monkeypatch, sessi ) ) session.add( - RunRecord( + SampleRecord( id=skipped_run_id, definition_id=skipped_definition_id, benchmark_type="math", instance_key="math-1", experiment="beta", - status=RunStatus.FAILED, + status=SampleStatus.FAILED, created_at=now, ) ) for status in ("completed", "failed", "running"): session.add( - RunGraphNode( - run_id=matching_run_id, + SampleGraphNode( + sample_id=matching_run_id, instance_key="task-1", task_slug=status, description="Task", @@ -116,7 +116,7 @@ def test_list_runs_filters_offsets_and_projects_index_summary(monkeypatch, sessi monkeypatch.setattr(module, "get_session", session_factory) - summaries = RunReadService().list_runs( + summaries = SampleSnapshotReadService().list_samples( limit=1, offset=0, status="completed", @@ -151,7 +151,7 @@ def test_list_runs_filters_offsets_and_projects_index_summary(monkeypatch, sessi def test_failed_run_snapshot_preserves_persisted_final_score(monkeypatch, session_factory) -> None: now = datetime(2026, 5, 20, 12, 0, tzinfo=UTC) definition_id = uuid4() - run_id = uuid4() + sample_id = uuid4() with session_factory() as session: session.add( @@ -163,20 +163,20 @@ def test_failed_run_snapshot_preserves_persisted_final_score(monkeypatch, sessio ) ) session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="smoke", instance_key="sad-path", - status=RunStatus.FAILED, + status=SampleStatus.FAILED, started_at=now, completed_at=now + timedelta(seconds=9), summary_json={"final_score": 0.5}, ) ) session.add( - RunGraphNode( - run_id=run_id, + SampleGraphNode( + sample_id=sample_id, instance_key="sad-path", task_slug="root", description="Root task", @@ -188,7 +188,7 @@ def test_failed_run_snapshot_preserves_persisted_final_score(monkeypatch, sessio monkeypatch.setattr(module, "get_session", session_factory) - snapshot = RunReadService().build_run_snapshot(run_id) + snapshot = SampleSnapshotReadService().build_snapshot(sample_id) assert snapshot is not None assert snapshot.status == "failed" diff --git a/ergon_core/tests/unit/registry/test_inngest_job_registry.py b/ergon_core/tests/unit/registry/test_inngest_job_registry.py index a0444335f..061d012ff 100644 --- a/ergon_core/tests/unit/registry/test_inngest_job_registry.py +++ b/ergon_core/tests/unit/registry/test_inngest_job_registry.py @@ -23,7 +23,7 @@ "block-descendants-on-failed": "task/failed", "cancel-orphans-on-cancelled": "task/cancelled", "cleanup-cancelled-task": "task/cancelled", - "run-cleanup": "run/cleanup", + "run-cleanup": "sample/cleanup", "sandbox-cleanup-on-completed": "task/completed", "sandbox-cleanup-on-failed": "task/failed", } @@ -47,7 +47,7 @@ "sandbox-cleanup-on-failed", ] -RUN_CANCEL = (("run/cancelled", "event.data.run_id == async.data.run_id"),) +RUN_CANCEL = (("sample/cancelled", "event.data.sample_id == async.data.sample_id"),) TASK_CANCEL = (("task/cancelled", "event.data.task_id == async.data.task_id"),) EXPECTED_FUNCTION_METADATA = { @@ -133,7 +133,7 @@ "retries": 0, "cancel": (), "concurrency": (), - "output": "RunCleanupResult", + "output": "SampleCleanupResult", }, "sandbox-cleanup-on-completed": { "retries": 1, diff --git a/ergon_core/tests/unit/resources/test_publishing.py b/ergon_core/tests/unit/resources/test_publishing.py index 56b027d49..caf698564 100644 --- a/ergon_core/tests/unit/resources/test_publishing.py +++ b/ergon_core/tests/unit/resources/test_publishing.py @@ -5,7 +5,7 @@ import pytest -from ergon_core.core.persistence.shared.enums import RunResourceKind +from ergon_core.core.persistence.shared.enums import SampleResourceKind class _Entry: @@ -83,14 +83,14 @@ def _session_factory(session: _Session): @pytest.mark.asyncio async def test_publish_sandbox_files_writes_blob_and_appends_resource_row() -> None: - from ergon_core.core.application.resources.publishing import RunResourcePublishService + from ergon_core.core.application.resources.publishing import SampleResourcePublishService - run_id = uuid4() + sample_id = uuid4() execution_id = uuid4() session = _Session() repository = _Repository() blob_store = _BlobStore() - service = RunResourcePublishService( + service = SampleResourcePublishService( repository=repository, session_factory=lambda: _session_factory(session), ) @@ -98,16 +98,16 @@ async def test_publish_sandbox_files_writes_blob_and_appends_resource_row() -> N created = await service.publish_sandbox_files( reader=_Reader(), blob_store=blob_store, - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, - publish_dirs=(("/workspace/final_output/", RunResourceKind.REPORT),), + publish_dirs=(("/workspace/final_output/", SampleResourceKind.REPORT),), ) assert len(created) == 1 appended = repository.appended[0] - assert appended["run_id"] == run_id + assert appended["sample_id"] == sample_id assert appended["task_execution_id"] == execution_id - assert appended["kind"] == RunResourceKind.REPORT.value + assert appended["kind"] == SampleResourceKind.REPORT.value assert appended["name"] == "report.md" assert appended["mime_type"] == "text/markdown" assert appended["size_bytes"] == len(b"# report") @@ -119,12 +119,12 @@ async def test_publish_sandbox_files_writes_blob_and_appends_resource_row() -> N @pytest.mark.asyncio async def test_publish_sandbox_files_skips_existing_blob_path_without_writing() -> None: - from ergon_core.core.application.resources.publishing import RunResourcePublishService + from ergon_core.core.application.resources.publishing import SampleResourcePublishService session = _Session() repository = _Repository(prior_by_path=object()) blob_store = _BlobStore() - service = RunResourcePublishService( + service = SampleResourcePublishService( repository=repository, session_factory=lambda: _session_factory(session), ) @@ -132,9 +132,9 @@ async def test_publish_sandbox_files_skips_existing_blob_path_without_writing() created = await service.publish_sandbox_files( reader=_Reader(), blob_store=blob_store, - run_id=uuid4(), + sample_id=uuid4(), task_execution_id=uuid4(), - publish_dirs=(("/workspace/final_output/", RunResourceKind.REPORT),), + publish_dirs=(("/workspace/final_output/", SampleResourceKind.REPORT),), ) assert created == [] @@ -144,20 +144,20 @@ async def test_publish_sandbox_files_skips_existing_blob_path_without_writing() def test_publish_value_dedups_by_hash_before_blob_write() -> None: - from ergon_core.core.application.resources.publishing import RunResourcePublishService + from ergon_core.core.application.resources.publishing import SampleResourcePublishService repository = _Repository(prior_by_hash=object()) blob_store = _BlobStore() - service = RunResourcePublishService( + service = SampleResourcePublishService( repository=repository, session_factory=lambda: _session_factory(_Session()), ) created = service.publish_value( blob_store=blob_store, - run_id=uuid4(), + sample_id=uuid4(), task_execution_id=uuid4(), - kind=RunResourceKind.REPORT, + kind=SampleResourceKind.REPORT, name="summary.txt", content="already present", ) diff --git a/ergon_core/tests/unit/rest_api/test_app_mounts_danger_test_harness.py b/ergon_core/tests/unit/rest_api/test_app_mounts_danger_test_harness.py index 155150c53..a6da0d4a0 100644 --- a/ergon_core/tests/unit/rest_api/test_app_mounts_danger_test_harness.py +++ b/ergon_core/tests/unit/rest_api/test_app_mounts_danger_test_harness.py @@ -3,4 +3,4 @@ def test_app_mounts_danger_test_harness_routes() -> None: routes = {route.path for route in app.routes} - assert "/api/__danger__/test-harness/read/run/{run_id}/state" in routes + assert "/api/__danger__/test-harness/read/samples/{sample_id}/state" in routes diff --git a/ergon_core/tests/unit/rest_api/test_rollouts_di.py b/ergon_core/tests/unit/rest_api/test_rollouts_di.py index 20a42f223..5af3888ba 100644 --- a/ergon_core/tests/unit/rest_api/test_rollouts_di.py +++ b/ergon_core/tests/unit/rest_api/test_rollouts_di.py @@ -8,12 +8,12 @@ class _FakeRolloutService: def __init__(self) -> None: self.batch_id = uuid4() - self.run_id = uuid4() + self.sample_id = uuid4() def submit(self, _request: object) -> dict[str, object]: return { "batch_id": self.batch_id, - "run_ids": [self.run_id], + "sample_ids": [self.sample_id], "status": "pending", } diff --git a/ergon_core/tests/unit/rest_api/test_runs_routes.py b/ergon_core/tests/unit/rest_api/test_samples_routes.py similarity index 68% rename from ergon_core/tests/unit/rest_api/test_runs_routes.py rename to ergon_core/tests/unit/rest_api/test_samples_routes.py index 41de12f3c..3dbd112f4 100644 --- a/ergon_core/tests/unit/rest_api/test_runs_routes.py +++ b/ergon_core/tests/unit/rest_api/test_samples_routes.py @@ -1,17 +1,17 @@ from datetime import UTC, datetime from uuid import UUID, uuid4 -from ergon_core.core.infrastructure.http.routes import runs as module -from ergon_core.core.infrastructure.http.routes.runs import router -from ergon_core.core.views.runs.models import RunSummaryDto +from ergon_core.core.infrastructure.http.routes import samples as module +from ergon_core.core.infrastructure.http.routes.samples import router +from ergon_core.core.views.samples.models import SampleSummaryDto from fastapi import FastAPI from fastapi.testclient import TestClient -class _FakeRunReadService: +class _FakeSampleSnapshotReadService: calls: list[dict] = [] - def list_runs( + def list_samples( self, *, limit: int = 20, @@ -19,7 +19,7 @@ def list_runs( definition_id: UUID | None = None, experiment: str | None = None, offset: int = 0, - ) -> list[RunSummaryDto]: + ) -> list[SampleSummaryDto]: self.calls.append( { "limit": limit, @@ -30,9 +30,9 @@ def list_runs( } ) return [ - RunSummaryDto( + SampleSummaryDto( id=uuid4(), - name="route run", + name="route sample", status="completed", created_at=datetime(2026, 5, 20, 12, 0, tzinfo=UTC), definition_id=definition_id or uuid4(), @@ -46,17 +46,17 @@ def list_runs( ] -def test_list_runs_route_passes_filters_to_read_service(monkeypatch) -> None: +def test_list_samples_route_passes_filters_to_read_service(monkeypatch) -> None: app = FastAPI() app.include_router(router) client = TestClient(app) - fake_service = _FakeRunReadService() + fake_service = _FakeSampleSnapshotReadService() definition_id = uuid4() - monkeypatch.setattr(module, "RunReadService", lambda: fake_service) + monkeypatch.setattr(module, "SampleSnapshotReadService", lambda: fake_service) response = client.get( - f"/runs?limit=25&offset=50&status=completed&definition_id={definition_id}&experiment=alpha" + f"/samples?limit=25&offset=50&status=completed&definition_id={definition_id}&experiment=alpha" ) assert response.status_code == 200 @@ -70,6 +70,6 @@ def test_list_runs_route_passes_filters_to_read_service(monkeypatch) -> None: } ] body = response.json() - assert body[0]["name"] == "route run" + assert body[0]["name"] == "route sample" assert body[0]["definition_name"] == "route experiment" assert body[0]["total_tasks"] == 1 diff --git a/ergon_core/tests/unit/rest_api/test_samples_routes_existing.py b/ergon_core/tests/unit/rest_api/test_samples_routes_existing.py new file mode 100644 index 000000000..3dbd112f4 --- /dev/null +++ b/ergon_core/tests/unit/rest_api/test_samples_routes_existing.py @@ -0,0 +1,75 @@ +from datetime import UTC, datetime +from uuid import UUID, uuid4 + +from ergon_core.core.infrastructure.http.routes import samples as module +from ergon_core.core.infrastructure.http.routes.samples import router +from ergon_core.core.views.samples.models import SampleSummaryDto +from fastapi import FastAPI +from fastapi.testclient import TestClient + + +class _FakeSampleSnapshotReadService: + calls: list[dict] = [] + + def list_samples( + self, + *, + limit: int = 20, + status: str | None = None, + definition_id: UUID | None = None, + experiment: str | None = None, + offset: int = 0, + ) -> list[SampleSummaryDto]: + self.calls.append( + { + "limit": limit, + "status": status, + "definition_id": definition_id, + "experiment": experiment, + "offset": offset, + } + ) + return [ + SampleSummaryDto( + id=uuid4(), + name="route sample", + status="completed", + created_at=datetime(2026, 5, 20, 12, 0, tzinfo=UTC), + definition_id=definition_id or uuid4(), + definition_name="route experiment", + benchmark_type="route-bench", + instance_key="sample-a", + sample_label="sample-a", + total_tasks=1, + completed_tasks=1, + ) + ] + + +def test_list_samples_route_passes_filters_to_read_service(monkeypatch) -> None: + app = FastAPI() + app.include_router(router) + client = TestClient(app) + fake_service = _FakeSampleSnapshotReadService() + definition_id = uuid4() + + monkeypatch.setattr(module, "SampleSnapshotReadService", lambda: fake_service) + + response = client.get( + f"/samples?limit=25&offset=50&status=completed&definition_id={definition_id}&experiment=alpha" + ) + + assert response.status_code == 200 + assert fake_service.calls == [ + { + "limit": 25, + "offset": 50, + "status": "completed", + "definition_id": definition_id, + "experiment": "alpha", + } + ] + body = response.json() + assert body[0]["name"] == "route sample" + assert body[0]["definition_name"] == "route experiment" + assert body[0]["total_tasks"] == 1 diff --git a/ergon_core/tests/unit/rest_api/test_test_harness.py b/ergon_core/tests/unit/rest_api/test_test_harness.py index 3da76a394..b91cf20be 100644 --- a/ergon_core/tests/unit/rest_api/test_test_harness.py +++ b/ergon_core/tests/unit/rest_api/test_test_harness.py @@ -16,7 +16,7 @@ class _NullSession: """Minimal session stub that returns no rows for any exec/get call. - The read endpoint queries for a RunRecord first and 404s when absent; in + The read endpoint queries for a SampleRecord first and 404s when absent; in that branch no further DB access occurs. This stub exists so unit tests don't require a live Postgres. """ @@ -51,7 +51,7 @@ def _build_app_with_harness() -> FastAPI: def test_read_endpoint_returns_404_for_unknown_run_id() -> None: app = _build_app_with_harness() client = TestClient(app) - resp = client.get(f"/api/__danger__/test-harness/read/run/{uuid4()}/state") + resp = client.get(f"/api/__danger__/test-harness/read/samples/{uuid4()}/state") assert resp.status_code == 404 @@ -69,22 +69,22 @@ def test_read_state_dto_exposes_live_playwright_contract_fields() -> None: def test_read_experiment_runs_route_uses_v2_experiment_grouping(monkeypatch) -> None: - run_id = uuid4() + sample_id = uuid4() monkeypatch.setattr( test_harness, "_read_experiment_runs", lambda experiment, session: [ - HarnessExperimentRun(run_id=run_id, status=f"{experiment}:completed") + HarnessExperimentRun(sample_id=sample_id, status=f"{experiment}:completed") ], ) app = _build_app_with_harness() client = TestClient(app) - resp = client.get("/api/__danger__/test-harness/read/experiment/group-alpha/runs") + resp = client.get("/api/__danger__/test-harness/read/experiment/group-alpha/samples") assert resp.status_code == 200 - assert resp.json() == [{"run_id": str(run_id), "status": "group-alpha:completed"}] + assert resp.json() == [{"sample_id": str(sample_id), "status": "group-alpha:completed"}] def test_reset_route_is_available_without_secret_header() -> None: diff --git a/ergon_core/tests/unit/rl/test_rollout_service.py b/ergon_core/tests/unit/rl/test_rollout_service.py index b0b6970fd..c05fc9254 100644 --- a/ergon_core/tests/unit/rl/test_rollout_service.py +++ b/ergon_core/tests/unit/rl/test_rollout_service.py @@ -6,7 +6,7 @@ from ergon_core.core.persistence.telemetry.models import ( RolloutBatch, RolloutBatchRun, - RunRecord, + SampleRecord, ) from ergon_core.core.rl.rollout_service import RolloutService from ergon_core.core.rl.rollout_types import SubmitRequest @@ -25,7 +25,7 @@ def session_factory(): engine, tables=[ ExperimentDefinition.__table__, - RunRecord.__table__, + SampleRecord.__table__, RolloutBatch.__table__, RolloutBatchRun.__table__, ], @@ -69,7 +69,7 @@ def test_rollout_submit_uses_rollout_batch_and_run_definition_without_legacy_rec with session_factory() as session: batch = session.get(RolloutBatch, response.batch_id) - runs = list(session.exec(select(RunRecord)).all()) + runs = list(session.exec(select(SampleRecord)).all()) assert batch is not None assert batch.definition_id == definition_id diff --git a/ergon_core/tests/unit/runtime/test_child_function_payloads.py b/ergon_core/tests/unit/runtime/test_child_function_payloads.py index 645cb1e5b..58d022e3e 100644 --- a/ergon_core/tests/unit/runtime/test_child_function_payloads.py +++ b/ergon_core/tests/unit/runtime/test_child_function_payloads.py @@ -12,7 +12,7 @@ def test_task_evaluate_request_is_id_only() -> None: request = TaskEvaluateRequest( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), evaluator_index=0, @@ -24,7 +24,7 @@ def test_task_evaluate_request_is_id_only() -> None: def test_task_evaluate_request_rejects_legacy_definition_fields() -> None: with pytest.raises(ValidationError): TaskEvaluateRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), @@ -37,7 +37,7 @@ def test_task_evaluate_request_rejects_legacy_definition_fields() -> None: def test_worker_execute_request_requires_static_or_dynamic_identity() -> None: with pytest.raises(ValidationError): WorkerExecuteRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=None, execution_id=uuid4(), @@ -54,7 +54,7 @@ def test_worker_execute_request_requires_static_or_dynamic_identity() -> None: def test_worker_execute_request_allows_dynamic_worker_without_model_target() -> None: task_id = uuid4() request = WorkerExecuteRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=task_id, execution_id=uuid4(), @@ -74,14 +74,14 @@ def test_worker_execute_request_allows_dynamic_worker_without_model_target() -> def test_sandbox_and_persist_outputs_require_task_id() -> None: with pytest.raises(ValidationError): SandboxSetupRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), benchmark_type="researchrubrics", ) with pytest.raises(ValidationError): PersistOutputsRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), execution_id=uuid4(), benchmark_type="researchrubrics", diff --git a/ergon_core/tests/unit/runtime/test_cleanup_cancelled_task.py b/ergon_core/tests/unit/runtime/test_cleanup_cancelled_task.py index badb3f878..bd44ec12a 100644 --- a/ergon_core/tests/unit/runtime/test_cleanup_cancelled_task.py +++ b/ergon_core/tests/unit/runtime/test_cleanup_cancelled_task.py @@ -23,18 +23,18 @@ def __init__(self) -> None: async def test_cleanup_cancelled_task_marks_execution_without_releasing_sandbox( monkeypatch: pytest.MonkeyPatch, ) -> None: - run_id = uuid4() + sample_id = uuid4() task_id = uuid4() execution_id = uuid4() payload = TaskCancelledEvent( - run_id=run_id, + sample_id=sample_id, definition_id=uuid4(), task_id=task_id, execution_id=execution_id, cause="manager_decision", ) cleanup = CleanupResult( - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=execution_id, sandbox_id="sbx-cancelled", @@ -61,7 +61,7 @@ def __exit__(self, *_exc): monkeypatch.setattr(cleanup_module, "get_session", lambda: SessionContext()) monkeypatch.setattr(cleanup_module, "get_dashboard_event_publisher", lambda: Emitter()) - result = await cleanup_module.run_cleanup_cancelled_task_job(_FakeStepCtx(), payload) + result = await cleanup_module.sample_cleanup_cancelled_task_job(_FakeStepCtx(), payload) assert result["sandbox_id"] == "sbx-cancelled" assert result["sandbox_released"] is False diff --git a/ergon_core/tests/unit/runtime/test_communication_service.py b/ergon_core/tests/unit/runtime/test_communication_service.py index 3991cd7db..25b767ab2 100644 --- a/ergon_core/tests/unit/runtime/test_communication_service.py +++ b/ergon_core/tests/unit/runtime/test_communication_service.py @@ -52,13 +52,13 @@ async def _record_thread_event(event: object) -> None: monkeypatch.setattr(emitter, "publish", _record_thread_event) set_dashboard_emitter(emitter) - run_id = uuid4() + sample_id = uuid4() execution_id = uuid4() summary = "Leaf workers report completion artifacts and probe exit status." response = await module.CommunicationService().save_message( CreateMessageRequest( - run_id=run_id, + sample_id=sample_id, from_agent_id="leaf-l_1", to_agent_id="parent", thread_topic="smoke-completion", @@ -93,10 +93,10 @@ async def _ignore_thread_event(event: object) -> None: set_dashboard_emitter(emitter) service = module.CommunicationService() - run_id = uuid4() + sample_id = uuid4() await service.save_message( CreateMessageRequest( - run_id=run_id, + sample_id=sample_id, from_agent_id="leaf-l_1", to_agent_id="parent", thread_topic="smoke-completion", @@ -105,7 +105,7 @@ async def _ignore_thread_event(event: object) -> None: ) await service.save_message( CreateMessageRequest( - run_id=run_id, + sample_id=sample_id, from_agent_id="leaf-l_2", to_agent_id="parent", thread_topic="smoke-completion", @@ -115,7 +115,7 @@ async def _ignore_thread_event(event: object) -> None: ) await service.save_message( CreateMessageRequest( - run_id=run_id, + sample_id=sample_id, from_agent_id="leaf-l_3", to_agent_id="parent", thread_topic="smoke-completion", @@ -125,6 +125,6 @@ async def _ignore_thread_event(event: object) -> None: ) with session_factory() as session: - thread = session.exec(select(Thread).where(Thread.run_id == run_id)).one() + thread = session.exec(select(Thread).where(Thread.sample_id == sample_id)).one() assert thread.summary == "Completion reports from leaf workers." diff --git a/ergon_core/tests/unit/runtime/test_context_event_contracts.py b/ergon_core/tests/unit/runtime/test_context_event_contracts.py index 8e34804ac..5e7167f7f 100644 --- a/ergon_core/tests/unit/runtime/test_context_event_contracts.py +++ b/ergon_core/tests/unit/runtime/test_context_event_contracts.py @@ -1,7 +1,7 @@ from uuid import uuid4 -from ergon_core.core.persistence.context.models import RunContextEvent -from ergon_core.core.views.runs.models import RunContextEventDto +from ergon_core.core.persistence.context.models import SampleContextEvent +from ergon_core.core.views.samples.models import SampleContextEventDto from ergon_core.core.views.dashboard_events.context_events import ( context_event_to_dashboard_event, ) @@ -18,7 +18,7 @@ def test_rest_and_dashboard_context_events_share_typed_payload_shape() -> None: ) common = { "id": uuid4(), - "run_id": uuid4(), + "sample_id": uuid4(), "task_execution_id": uuid4(), "task_id": uuid4(), "worker_binding_key": "worker", @@ -30,7 +30,7 @@ def test_rest_and_dashboard_context_events_share_typed_payload_shape() -> None: "completed_at": None, } - rest = RunContextEventDto.model_validate(common) + rest = SampleContextEventDto.model_validate(common) dashboard = DashboardContextEventEvent.model_validate(common) assert rest.payload == dashboard.payload @@ -40,7 +40,7 @@ def test_rest_and_dashboard_context_events_share_typed_payload_shape() -> None: def test_dashboard_context_event_serializes_canonical_task_id_field() -> None: event = DashboardContextEventEvent( id=uuid4(), - run_id=uuid4(), + sample_id=uuid4(), task_execution_id=uuid4(), task_id=uuid4(), worker_binding_key="worker", @@ -64,7 +64,7 @@ def test_dashboard_context_event_serializes_canonical_task_id_field() -> None: def test_context_event_row_mapper_uses_execution_task_map() -> None: - run_id = uuid4() + sample_id = uuid4() execution_id = uuid4() task_id = uuid4() payload = ContextPartChunkLog( @@ -73,9 +73,9 @@ def test_context_event_row_mapper_uses_execution_task_map() -> None: worker_binding_key="worker", turn_id="turn-1", ) - row = RunContextEvent( + row = SampleContextEvent( id=uuid4(), - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, worker_binding_key="worker", sequence=1, @@ -90,7 +90,7 @@ def test_context_event_row_mapper_uses_execution_task_map() -> None: assert event == DashboardContextEventEvent( id=row.id, - run_id=run_id, + sample_id=sample_id, task_execution_id=execution_id, task_id=task_id, worker_binding_key="worker", @@ -104,9 +104,9 @@ def test_context_event_row_mapper_uses_execution_task_map() -> None: def test_context_event_row_mapper_returns_none_for_unknown_execution() -> None: - row = RunContextEvent( + row = SampleContextEvent( id=uuid4(), - run_id=uuid4(), + sample_id=uuid4(), task_execution_id=uuid4(), worker_binding_key="worker", sequence=1, diff --git a/ergon_core/tests/unit/runtime/test_definition_repository_and_latest_run.py b/ergon_core/tests/unit/runtime/test_definition_repository_and_latest_run.py index 683421157..788df44e9 100644 --- a/ergon_core/tests/unit/runtime/test_definition_repository_and_latest_run.py +++ b/ergon_core/tests/unit/runtime/test_definition_repository_and_latest_run.py @@ -1,14 +1,14 @@ -"""Unit tests for latest_run_for_definition.""" +"""Unit tests for latest_sample_for_definition.""" from datetime import UTC, datetime, timedelta from uuid import uuid4 import pytest -from ergon_core.core.application.runtime import runs as runs_module -from ergon_core.core.application.runtime.run_records import latest_run_for_definition +from ergon_core.core.application.runtime import samples as runs_module +from ergon_core.core.application.runtime.sample_records import latest_sample_for_definition from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine @@ -33,19 +33,19 @@ def _run( *, definition_id: object, created_at: datetime | None = None, -) -> RunRecord: - return RunRecord( +) -> SampleRecord: + return SampleRecord( id=uuid4(), definition_id=definition_id, benchmark_type="ci-benchmark", instance_key="k", worker_team_json={}, - status=RunStatus.PENDING, + status=SampleStatus.PENDING, created_at=created_at or datetime(2026, 1, 1, tzinfo=UTC), ) -def test_latest_run_for_definition_returns_most_recent(monkeypatch, session_factory) -> None: +def test_latest_sample_for_definition_returns_most_recent(monkeypatch, session_factory) -> None: definition_id = uuid4() now = datetime(2026, 3, 1, 12, 0, tzinfo=UTC) @@ -74,13 +74,15 @@ def test_latest_run_for_definition_returns_most_recent(monkeypatch, session_fact monkeypatch.setattr(runs_module, "get_session", session_factory) - result = latest_run_for_definition(definition_id) + result = latest_sample_for_definition(definition_id) assert result is not None assert result.id == newest_id -def test_latest_run_for_definition_ignores_other_definitions(monkeypatch, session_factory) -> None: +def test_latest_sample_for_definition_ignores_other_definitions( + monkeypatch, session_factory +) -> None: def_a = uuid4() def_b = uuid4() now = datetime(2026, 3, 1, 12, 0, tzinfo=UTC) @@ -114,17 +116,19 @@ def test_latest_run_for_definition_ignores_other_definitions(monkeypatch, sessio monkeypatch.setattr(runs_module, "get_session", session_factory) - result = latest_run_for_definition(def_a) + result = latest_sample_for_definition(def_a) assert result is not None assert result.id == run_a_id -def test_latest_run_for_definition_returns_none_when_no_runs(monkeypatch, session_factory) -> None: +def test_latest_sample_for_definition_returns_none_when_no_runs( + monkeypatch, session_factory +) -> None: definition_id = uuid4() monkeypatch.setattr(runs_module, "get_session", session_factory) - result = latest_run_for_definition(definition_id) + result = latest_sample_for_definition(definition_id) assert result is None diff --git a/ergon_core/tests/unit/runtime/test_descendant_lookups.py b/ergon_core/tests/unit/runtime/test_descendant_lookups.py index 99aabe706..c2ae00852 100644 --- a/ergon_core/tests/unit/runtime/test_descendant_lookups.py +++ b/ergon_core/tests/unit/runtime/test_descendant_lookups.py @@ -3,7 +3,7 @@ from uuid import UUID, uuid4 import pytest -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.application.runtime import inspection as inspection_module from ergon_core.core.application.runtime.graph_traversal import descendants from ergon_core.core.application.runtime.task_inspection import TaskInspectionService @@ -29,13 +29,13 @@ def _make_session() -> Session: def _node( session: Session, *, - run_id: UUID, + sample_id: UUID, slug: str, parent_task_id: UUID | None = None, status: str = "PENDING", -) -> RunGraphNode: - node = RunGraphNode( - run_id=run_id, +) -> SampleGraphNode: + node = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug=slug, description=f"Task {slug}", @@ -57,15 +57,17 @@ class TestContainmentDescendants: def test_returns_direct_and_transitive_children(self) -> None: """root → child1, root → child2, child1 → grandchild: 3 rows returned.""" session = _make_session() - run_id = uuid4() - - root = _node(session, run_id=run_id, slug="root") - child1 = _node(session, run_id=run_id, slug="child1", parent_task_id=root.task_id) - child2 = _node(session, run_id=run_id, slug="child2", parent_task_id=root.task_id) - grandchild = _node(session, run_id=run_id, slug="grandchild", parent_task_id=child1.task_id) + sample_id = uuid4() + + root = _node(session, sample_id=sample_id, slug="root") + child1 = _node(session, sample_id=sample_id, slug="child1", parent_task_id=root.task_id) + child2 = _node(session, sample_id=sample_id, slug="child2", parent_task_id=root.task_id) + grandchild = _node( + session, sample_id=sample_id, slug="grandchild", parent_task_id=child1.task_id + ) session.commit() - rows = descendants(session, run_id=run_id, root_task_id=root.task_id) + rows = descendants(session, sample_id=sample_id, root_task_id=root.task_id) result_ids = {row.task_id for row in rows} assert result_ids == {child1.task_id, child2.task_id, grandchild.task_id} @@ -73,14 +75,14 @@ def test_returns_direct_and_transitive_children(self) -> None: def test_does_not_include_root_or_sibling(self) -> None: """root and an unrelated sibling at run root level are excluded.""" session = _make_session() - run_id = uuid4() + sample_id = uuid4() - root = _node(session, run_id=run_id, slug="root") - child1 = _node(session, run_id=run_id, slug="child1", parent_task_id=root.task_id) - sibling = _node(session, run_id=run_id, slug="sibling") # no parent_task_id + root = _node(session, sample_id=sample_id, slug="root") + child1 = _node(session, sample_id=sample_id, slug="child1", parent_task_id=root.task_id) + sibling = _node(session, sample_id=sample_id, slug="sibling") # no parent_task_id session.commit() - rows = descendants(session, run_id=run_id, root_task_id=root.task_id) + rows = descendants(session, sample_id=sample_id, root_task_id=root.task_id) result_ids = {row.task_id for row in rows} assert root.task_id not in result_ids @@ -90,44 +92,44 @@ def test_does_not_include_root_or_sibling(self) -> None: def test_returns_empty_when_root_has_no_children(self) -> None: """A leaf node returns ().""" session = _make_session() - run_id = uuid4() + sample_id = uuid4() - leaf = _node(session, run_id=run_id, slug="leaf") + leaf = _node(session, sample_id=sample_id, slug="leaf") session.commit() - rows = descendants(session, run_id=run_id, root_task_id=leaf.task_id) + rows = descendants(session, sample_id=sample_id, root_task_id=leaf.task_id) assert rows == [] def test_depth_greater_than_two(self) -> None: """Recursion works past depth 2: root → A → B → C → D.""" session = _make_session() - run_id = uuid4() + sample_id = uuid4() - root = _node(session, run_id=run_id, slug="root") - a = _node(session, run_id=run_id, slug="a", parent_task_id=root.task_id) - b = _node(session, run_id=run_id, slug="b", parent_task_id=a.task_id) - c = _node(session, run_id=run_id, slug="c", parent_task_id=b.task_id) - d = _node(session, run_id=run_id, slug="d", parent_task_id=c.task_id) + root = _node(session, sample_id=sample_id, slug="root") + a = _node(session, sample_id=sample_id, slug="a", parent_task_id=root.task_id) + b = _node(session, sample_id=sample_id, slug="b", parent_task_id=a.task_id) + c = _node(session, sample_id=sample_id, slug="c", parent_task_id=b.task_id) + d = _node(session, sample_id=sample_id, slug="d", parent_task_id=c.task_id) session.commit() - rows = descendants(session, run_id=run_id, root_task_id=root.task_id) + rows = descendants(session, sample_id=sample_id, root_task_id=root.task_id) result_ids = {row.task_id for row in rows} assert result_ids == {a.task_id, b.task_id, c.task_id, d.task_id} def test_cross_run_isolation(self) -> None: - """Nodes from a different run_id are not included.""" + """Nodes from a different sample_id are not included.""" session = _make_session() - run_id = uuid4() + sample_id = uuid4() other_run_id = uuid4() - root = _node(session, run_id=run_id, slug="root") - child = _node(session, run_id=run_id, slug="child", parent_task_id=root.task_id) + root = _node(session, sample_id=sample_id, slug="root") + child = _node(session, sample_id=sample_id, slug="child", parent_task_id=root.task_id) # A node in another run that happens to point at root.task_id as parent - other = _node(session, run_id=other_run_id, slug="other", parent_task_id=root.task_id) + other = _node(session, sample_id=other_run_id, slug="other", parent_task_id=root.task_id) session.commit() - rows = descendants(session, run_id=run_id, root_task_id=root.task_id) + rows = descendants(session, sample_id=sample_id, root_task_id=root.task_id) result_ids = {row.task_id for row in rows} assert result_ids == {child.task_id} @@ -143,12 +145,14 @@ class TestTaskInspectionServiceDescendantIds: async def test_returns_same_set_as_repository(self, monkeypatch: pytest.MonkeyPatch) -> None: """descendant_ids returns a frozenset matching runtime traversal.""" session = _make_session() - run_id = uuid4() - - root = _node(session, run_id=run_id, slug="root") - child1 = _node(session, run_id=run_id, slug="child1", parent_task_id=root.task_id) - child2 = _node(session, run_id=run_id, slug="child2", parent_task_id=root.task_id) - grandchild = _node(session, run_id=run_id, slug="grandchild", parent_task_id=child1.task_id) + sample_id = uuid4() + + root = _node(session, sample_id=sample_id, slug="root") + child1 = _node(session, sample_id=sample_id, slug="child1", parent_task_id=root.task_id) + child2 = _node(session, sample_id=sample_id, slug="child2", parent_task_id=root.task_id) + grandchild = _node( + session, sample_id=sample_id, slug="grandchild", parent_task_id=child1.task_id + ) session.commit() # Patch get_session in the inspection module to return the test session @@ -156,7 +160,7 @@ async def test_returns_same_set_as_repository(self, monkeypatch: pytest.MonkeyPa svc = TaskInspectionService() - result = await svc.descendant_ids(run_id=run_id, root_task_id=root.task_id) + result = await svc.descendant_ids(sample_id=sample_id, root_task_id=root.task_id) assert isinstance(result, frozenset) assert result == frozenset({child1.task_id, child2.task_id, grandchild.task_id}) @@ -166,34 +170,34 @@ async def test_returns_empty_frozenset_when_no_children( ) -> None: """Returns frozenset() when root has no children.""" session = _make_session() - run_id = uuid4() + sample_id = uuid4() - leaf = _node(session, run_id=run_id, slug="leaf") + leaf = _node(session, sample_id=sample_id, slug="leaf") session.commit() monkeypatch.setattr(inspection_module, "get_session", lambda: session) svc = TaskInspectionService() - result = await svc.descendant_ids(run_id=run_id, root_task_id=leaf.task_id) + result = await svc.descendant_ids(sample_id=sample_id, root_task_id=leaf.task_id) assert result == frozenset() async def test_depth_greater_than_two(self, monkeypatch: pytest.MonkeyPatch) -> None: """descendant_ids resolves all nodes in a chain of depth > 2.""" session = _make_session() - run_id = uuid4() + sample_id = uuid4() - root = _node(session, run_id=run_id, slug="root") - a = _node(session, run_id=run_id, slug="a", parent_task_id=root.task_id) - b = _node(session, run_id=run_id, slug="b", parent_task_id=a.task_id) - c = _node(session, run_id=run_id, slug="c", parent_task_id=b.task_id) + root = _node(session, sample_id=sample_id, slug="root") + a = _node(session, sample_id=sample_id, slug="a", parent_task_id=root.task_id) + b = _node(session, sample_id=sample_id, slug="b", parent_task_id=a.task_id) + c = _node(session, sample_id=sample_id, slug="c", parent_task_id=b.task_id) session.commit() monkeypatch.setattr(inspection_module, "get_session", lambda: session) svc = TaskInspectionService() - result = await svc.descendant_ids(run_id=run_id, root_task_id=root.task_id) + result = await svc.descendant_ids(sample_id=sample_id, root_task_id=root.task_id) assert result == frozenset({a.task_id, b.task_id, c.task_id}) diff --git a/ergon_core/tests/unit/runtime/test_dynamic_task_evaluation_mapping.py b/ergon_core/tests/unit/runtime/test_dynamic_task_evaluation_mapping.py index 35ae2979d..9fdcdbd7e 100644 --- a/ergon_core/tests/unit/runtime/test_dynamic_task_evaluation_mapping.py +++ b/ergon_core/tests/unit/runtime/test_dynamic_task_evaluation_mapping.py @@ -1,8 +1,8 @@ from uuid import uuid4 from types import SimpleNamespace -from ergon_core.core.persistence.telemetry.models import RunTaskEvaluation -from ergon_core.core.views.runs.snapshot import _task_keyed_evaluations +from ergon_core.core.persistence.telemetry.models import SampleTaskEvaluation +from ergon_core.core.views.samples.snapshot import _task_keyed_evaluations from ergon_core.core.jobs.task.evaluate.job import _evaluator_binding_key @@ -30,11 +30,11 @@ def _summary_json() -> dict: def test_task_keyed_evaluations_use_runtime_task_id_for_dynamic_tasks() -> None: - run_id = uuid4() + sample_id = uuid4() dynamic_task_id = uuid4() - evaluation = RunTaskEvaluation( - run_id=run_id, + evaluation = SampleTaskEvaluation( + sample_id=sample_id, task_execution_id=uuid4(), task_id=dynamic_task_id, definition_evaluator_id=uuid4(), @@ -46,7 +46,7 @@ def test_task_keyed_evaluations_use_runtime_task_id_for_dynamic_tasks() -> None: result = _task_keyed_evaluations( [evaluation], - str(run_id), + str(sample_id), ) assert set(result) == {str(dynamic_task_id)} diff --git a/ergon_core/tests/unit/runtime/test_evaluation_context_schemas.py b/ergon_core/tests/unit/runtime/test_evaluation_context_schemas.py index 770119053..d238bd7c8 100644 --- a/ergon_core/tests/unit/runtime/test_evaluation_context_schemas.py +++ b/ergon_core/tests/unit/runtime/test_evaluation_context_schemas.py @@ -11,14 +11,14 @@ def test_task_evaluate_request_is_id_only_with_evaluator_index() -> None: request = TaskEvaluateRequest( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), evaluator_index=1, ) assert set(request.model_dump(mode="json")) == { - "run_id", + "sample_id", "task_id", "execution_id", "evaluator_index", @@ -28,7 +28,7 @@ def test_task_evaluate_request_is_id_only_with_evaluator_index() -> None: def test_task_evaluate_request_requires_evaluator_index() -> None: with pytest.raises(ValidationError, match="evaluator_index"): TaskEvaluateRequest( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), ) diff --git a/ergon_core/tests/unit/runtime/test_evaluation_summary_contracts.py b/ergon_core/tests/unit/runtime/test_evaluation_summary_contracts.py index 963b2d10b..bf91b1584 100644 --- a/ergon_core/tests/unit/runtime/test_evaluation_summary_contracts.py +++ b/ergon_core/tests/unit/runtime/test_evaluation_summary_contracts.py @@ -17,7 +17,7 @@ from ergon_core.core.application.evaluation.models import CriterionSpec from ergon_core.core.application.evaluation.mappers import build_evaluation_summary from ergon_core.core.application.evaluation.service import EvaluationServiceResult -from ergon_core.core.views.runs.evaluation_mapping import build_dashboard_evaluation_dto +from ergon_core.core.views.samples.evaluation_mapping import build_dashboard_evaluation_dto from pydantic import ValidationError @@ -211,7 +211,7 @@ def test_dashboard_evaluation_dto_allows_nullable_feedback_and_input() -> None: dto = build_dashboard_evaluation_dto( evaluation_id=uuid4(), - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), total_score=1.0, created_at="2026-04-25T20:00:00Z", @@ -234,7 +234,7 @@ def test_dashboard_evaluation_dto_exposes_required_rubric_metadata() -> None: dto = build_dashboard_evaluation_dto( evaluation_id=uuid4(), - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), total_score=1.0, created_at="2026-04-25T20:00:00Z", diff --git a/ergon_core/tests/unit/runtime/test_execute_task_evaluator_fanout.py b/ergon_core/tests/unit/runtime/test_execute_task_evaluator_fanout.py index e0de8bf83..f93b3a086 100644 --- a/ergon_core/tests/unit/runtime/test_execute_task_evaluator_fanout.py +++ b/ergon_core/tests/unit/runtime/test_execute_task_evaluator_fanout.py @@ -75,7 +75,7 @@ async def _run( def _prepared(execution_id, task_id) -> PreparedTaskExecution: return PreparedTaskExecution( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=task_id, execution_id=execution_id, @@ -89,9 +89,9 @@ def _prepared(execution_id, task_id) -> PreparedTaskExecution: ) -def _ready_event(run_id, definition_id, task_id) -> TaskReadyEvent: +def _ready_event(sample_id, definition_id, task_id) -> TaskReadyEvent: return TaskReadyEvent( - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=task_id, ) @@ -118,13 +118,13 @@ async def test_execute_task_emits_completed_strictly_after_eval_gather( ordering: list[str] = [] ctx = _OrderedFakeCtx(ordering) - run_id = uuid4() + sample_id = uuid4() definition_id = uuid4() task_id = uuid4() execution_id = uuid4() prepared = _prepared(execution_id, task_id) - payload = _ready_event(run_id, definition_id, task_id) + payload = _ready_event(sample_id, definition_id, task_id) async def fake_prepare( _ctx: inngest.Context, @@ -244,9 +244,9 @@ async def test_execute_task_emits_failed_when_worker_fails( ordering: list[str] = [] ctx = _OrderedFakeCtx(ordering) - run_id = uuid4() + sample_id = uuid4() prepared = _prepared(uuid4(), uuid4()) - payload = _ready_event(run_id, uuid4(), prepared.task_id) + payload = _ready_event(sample_id, uuid4(), prepared.task_id) async def fake_prepare( _ctx: inngest.Context, diff --git a/ergon_core/tests/unit/runtime/test_experiment_definition_service.py b/ergon_core/tests/unit/runtime/test_experiment_definition_service.py index 086bee848..ed99bbe57 100644 --- a/ergon_core/tests/unit/runtime/test_experiment_definition_service.py +++ b/ergon_core/tests/unit/runtime/test_experiment_definition_service.py @@ -37,21 +37,21 @@ def fake_persist(candidate: object) -> DefinitionHandle: @pytest.mark.asyncio -async def test_run_experiment_uses_experiments_service_public_facade(monkeypatch) -> None: +async def test_sample_experiment_uses_experiments_service_public_facade(monkeypatch) -> None: definition_id = uuid4() - run_id = uuid4() + sample_id = uuid4() result = ExperimentRunResult( definition_id=definition_id, - run_ids=[run_id], + sample_ids=[sample_id], definition_ids=[definition_id], ) seen: list[tuple[object, object]] = [] - async def fake_launch_run(candidate_definition_id, *, emit_workflow_started=None): + async def fake_launch_sample(candidate_definition_id, *, emit_workflow_started=None): seen.append((candidate_definition_id, emit_workflow_started)) return result - monkeypatch.setattr(service, "launch_run", fake_launch_run) + monkeypatch.setattr(service, "launch_sample", fake_launch_sample) assert await service.run_experiment(ExperimentRunRequest(definition_id=definition_id)) == result assert seen == [(definition_id, None)] diff --git a/ergon_core/tests/unit/runtime/test_experiment_launch_service.py b/ergon_core/tests/unit/runtime/test_experiment_launch_service.py index 10d0495d2..d72167b32 100644 --- a/ergon_core/tests/unit/runtime/test_experiment_launch_service.py +++ b/ergon_core/tests/unit/runtime/test_experiment_launch_service.py @@ -4,13 +4,13 @@ import pytest from ergon_core.core.application.experiments import launch as launch_module from ergon_core.core.application.experiments.errors import DefinitionNotFoundError -from ergon_core.core.application.experiments.launch import launch_run +from ergon_core.core.application.experiments.launch import launch_sample from ergon_core.core.application.experiments.models import ExperimentRunRequest from ergon_core.core.application.experiments.handles import DefinitionHandle from ergon_core.core.application.experiments.service import run_experiment from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord class _FakeSession: @@ -46,31 +46,31 @@ def refresh(self, row) -> None: @pytest.mark.asyncio -async def test_run_experiment_creates_one_run_per_selected_sample(monkeypatch): +async def test_sample_experiment_creates_one_run_per_selected_sample(monkeypatch): definition = ExperimentDefinition( id=uuid4(), name="ci experiment", benchmark_type="ci-benchmark", metadata_json={}, ) - created_runs: list[RunRecord] = [] + created_samples: list[SampleRecord] = [] emitted: list[tuple] = [] - def fake_create_run(definition, **kwargs): - run = RunRecord( + def fake_create_sample(definition, **kwargs): + run = SampleRecord( id=uuid4(), - status=RunStatus.PENDING, + status=SampleStatus.PENDING, benchmark_type=definition.benchmark_type, **kwargs, ) - created_runs.append(run) + created_samples.append(run) return run - async def fake_emit(run_id, definition_id): - emitted.append((run_id, definition_id)) + async def fake_emit(sample_id, definition_id): + emitted.append((sample_id, definition_id)) monkeypatch.setattr(launch_module, "get_session", lambda: _FakeSession(definition=definition)) - monkeypatch.setattr(launch_module, "create_run", fake_create_run) + monkeypatch.setattr(launch_module, "create_definition_backed_sample", fake_create_sample) result = await run_experiment( ExperimentRunRequest(definition_id=definition.id), @@ -78,19 +78,19 @@ async def fake_emit(run_id, definition_id): ) assert result.definition_id == definition.id - assert result.run_ids == [created_runs[0].id] + assert result.sample_ids == [created_samples[0].id] assert result.definition_ids == [definition.id] - assert [run.instance_key for run in created_runs] == ["default"] - assert {run.definition_id for run in created_runs} == {definition.id} - assert emitted == [(created_runs[0].id, definition.id)] + assert [run.instance_key for run in created_samples] == ["default"] + assert {run.definition_id for run in created_samples} == {definition.id} + assert emitted == [(created_samples[0].id, definition.id)] @pytest.mark.asyncio -async def test_launch_run_accepts_definition_id(monkeypatch): - """``launch_run`` materializes a run straight from ``ExperimentDefinition``. +async def test_launch_sample_accepts_definition_id(monkeypatch): + """``launch_sample`` materializes a sample straight from ``ExperimentDefinition``. - The real DB write inside ``create_run`` is blocked by - ``create_run`` is mocked here so the orchestration around the + The real DB write inside ``create_definition_backed_sample`` is blocked by + ``create_definition_backed_sample`` is mocked here so the orchestration around the definition-first path can be exercised without a database write. The orchestration around it (session lookup, emitter, result shape) is still exercised end-to-end against the new definition-first path. @@ -104,12 +104,12 @@ async def test_launch_run_accepts_definition_id(monkeypatch): ) captured: dict = {} - def fake_create_run(handle, **kwargs): + def fake_create_sample(handle, **kwargs): captured["handle"] = handle captured["kwargs"] = kwargs - return RunRecord( + return SampleRecord( id=uuid4(), - status=RunStatus.PENDING, + status=SampleStatus.PENDING, benchmark_type=handle.benchmark_type, definition_id=kwargs.get("definition_id"), worker_team_json=kwargs.get("worker_team_json") or {}, @@ -117,12 +117,12 @@ def fake_create_run(handle, **kwargs): ) monkeypatch.setattr(launch_module, "get_session", lambda: _FakeSession(definition=definition)) - monkeypatch.setattr(launch_module, "create_run", fake_create_run) + monkeypatch.setattr(launch_module, "create_definition_backed_sample", fake_create_sample) emitter = AsyncMock() - result = await launch_run(definition.id, emit_workflow_started=emitter) + result = await launch_sample(definition.id, emit_workflow_started=emitter) - # Orchestration: create_run was reached with the definition handle. + # Orchestration: create_definition_backed_sample was reached with the definition handle. assert captured["handle"].definition_id == definition.id assert captured["handle"].benchmark_type == "mini" assert captured["kwargs"]["definition_id"] == definition.id @@ -131,16 +131,16 @@ def fake_create_run(handle, **kwargs): # Result shape mirrors the spec. assert result.definition_id == definition.id assert result.definition_ids == [definition.id] - assert result.run_ids - assert len(result.run_ids) == 1 + assert result.sample_ids + assert len(result.sample_ids) == 1 - # Emitter was awaited with the new run id and the definition id. - emitter.assert_awaited_once_with(result.run_ids[0], definition.id) + # Emitter was awaited with the new sample id and the definition id. + emitter.assert_awaited_once_with(result.sample_ids[0], definition.id) @pytest.mark.asyncio -async def test_launch_run_raises_typed_error_when_definition_missing(monkeypatch): - """``launch_run`` raises ``DefinitionNotFoundError`` (not a generic +async def test_launch_sample_raises_typed_error_when_definition_missing(monkeypatch): + """``launch_sample`` raises ``DefinitionNotFoundError`` (not a generic ``ValueError``) when the requested ``ExperimentDefinition`` row is absent. Callers depend on the typed exception to differentiate "missing definition" from other lookup failures without string- @@ -149,4 +149,4 @@ async def test_launch_run_raises_typed_error_when_definition_missing(monkeypatch monkeypatch.setattr(launch_module, "get_session", lambda: _FakeSession()) with pytest.raises(DefinitionNotFoundError): - await launch_run(uuid4(), emit_workflow_started=AsyncMock()) + await launch_sample(uuid4(), emit_workflow_started=AsyncMock()) diff --git a/ergon_core/tests/unit/runtime/test_failed_task_sandbox_cleanup.py b/ergon_core/tests/unit/runtime/test_failed_task_sandbox_cleanup.py index b725e98b9..2a0b5b3c9 100644 --- a/ergon_core/tests/unit/runtime/test_failed_task_sandbox_cleanup.py +++ b/ergon_core/tests/unit/runtime/test_failed_task_sandbox_cleanup.py @@ -15,7 +15,7 @@ @pytest.mark.asyncio async def test_failed_task_propagation_does_not_terminate_sandbox_directly() -> None: payload = TaskFailedEvent( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), @@ -23,7 +23,7 @@ async def test_failed_task_propagation_does_not_terminate_sandbox_directly() -> sandbox_id="sandbox-real", ) propagation = PropagationResult( - run_id=payload.run_id, + sample_id=payload.sample_id, definition_id=payload.definition_id, completed_task_id=payload.task_id, ready_tasks=[], diff --git a/ergon_core/tests/unit/runtime/test_failure_error_json.py b/ergon_core/tests/unit/runtime/test_failure_error_json.py index c1cfc703b..60a74a14f 100644 --- a/ergon_core/tests/unit/runtime/test_failure_error_json.py +++ b/ergon_core/tests/unit/runtime/test_failure_error_json.py @@ -12,11 +12,11 @@ async def test_finalize_failure_preserves_structured_error_json(monkeypatch) -> from ergon_core.core.application.runtime.task_execution import TaskExecutionService execution_id = uuid4() - run_id = uuid4() + sample_id = uuid4() task_id = uuid4() execution = SimpleNamespace( id=execution_id, - run_id=run_id, + sample_id=sample_id, task_id=task_id, ) @@ -56,7 +56,7 @@ async def fake_emit_task_status(*args, **kwargs): await TaskExecutionService().finalize_failure( FailTaskExecutionCommand( execution_id=execution_id, - run_id=run_id, + sample_id=sample_id, task_id=None, error_message="provider returned malformed response", error_json=structured_error, diff --git a/ergon_core/tests/unit/runtime/test_graph_mutation_contracts.py b/ergon_core/tests/unit/runtime/test_graph_mutation_contracts.py index 83a548a7e..e1982de3f 100644 --- a/ergon_core/tests/unit/runtime/test_graph_mutation_contracts.py +++ b/ergon_core/tests/unit/runtime/test_graph_mutation_contracts.py @@ -5,7 +5,7 @@ GraphMutationRecordDto, GraphMutationValue, ) -from ergon_core.core.persistence.graph.models import RunGraphMutation +from ergon_core.core.persistence.graph.models import SampleGraphMutation from ergon_core.core.views.dashboard_events.contracts import DashboardGraphMutationEvent from ergon_core.core.views.dashboard_events.graph_mutations import ( dashboard_graph_mutation_event_from_row, @@ -15,7 +15,7 @@ def test_rest_and_dashboard_mutations_share_graph_mutation_record_payloads() -> None: - run_id = uuid4() + sample_id = uuid4() mutation_id = uuid4() edge_id = uuid4() source_id = uuid4() @@ -31,7 +31,7 @@ def test_rest_and_dashboard_mutations_share_graph_mutation_record_payloads() -> record = GraphMutationRecordDto( id=mutation_id, - run_id=run_id, + sample_id=sample_id, sequence=1, mutation_type="edge.added", target_type="edge", @@ -58,14 +58,14 @@ def test_rest_and_dashboard_mutations_share_graph_mutation_record_payloads() -> def test_graph_mutation_row_mapper_is_shared_by_rest_and_dashboard_events() -> None: - run_id = uuid4() + sample_id = uuid4() mutation_id = uuid4() edge_id = uuid4() source_id = uuid4() target_id = uuid4() - row = RunGraphMutation( + row = SampleGraphMutation( id=mutation_id, - run_id=run_id, + sample_id=sample_id, sequence=7, mutation_type="edge.added", target_type="edge", @@ -87,7 +87,7 @@ def test_graph_mutation_row_mapper_is_shared_by_rest_and_dashboard_events() -> N assert event == DashboardGraphMutationEvent(mutation=record) assert record.id == mutation_id - assert record.run_id == run_id + assert record.sample_id == sample_id assert record.new_value == EdgeAddedMutation( source_task_id=source_id, target_task_id=target_id, diff --git a/ergon_core/tests/unit/runtime/test_graph_traversal.py b/ergon_core/tests/unit/runtime/test_graph_traversal.py index ab7ac139b..9b3855955 100644 --- a/ergon_core/tests/unit/runtime/test_graph_traversal.py +++ b/ergon_core/tests/unit/runtime/test_graph_traversal.py @@ -1,6 +1,6 @@ from uuid import UUID, uuid4 -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.application.runtime.graph_traversal import descendant_ids, descendants from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine @@ -19,13 +19,13 @@ def _session() -> Session: def _node( session: Session, *, - run_id: UUID, + sample_id: UUID, slug: str, parent_task_id: UUID | None = None, status: str = "pending", -) -> RunGraphNode: - node = RunGraphNode( - run_id=run_id, +) -> SampleGraphNode: + node = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug=slug, description=f"Task {slug}", @@ -39,17 +39,19 @@ def _node( def test_descendants_walks_full_containment_subtree_past_terminal_nodes() -> None: session = _session() - run_id = uuid4() - root = _node(session, run_id=run_id, slug="root") + sample_id = uuid4() + root = _node(session, sample_id=sample_id, slug="root") child = _node( - session, run_id=run_id, slug="child", parent_task_id=root.task_id, status="completed" + session, sample_id=sample_id, slug="child", parent_task_id=root.task_id, status="completed" ) - grandchild = _node(session, run_id=run_id, slug="grandchild", parent_task_id=child.task_id) - sibling = _node(session, run_id=run_id, slug="sibling", parent_task_id=root.task_id) - other_run_child = _node(session, run_id=uuid4(), slug="other", parent_task_id=root.task_id) + grandchild = _node( + session, sample_id=sample_id, slug="grandchild", parent_task_id=child.task_id + ) + sibling = _node(session, sample_id=sample_id, slug="sibling", parent_task_id=root.task_id) + other_run_child = _node(session, sample_id=uuid4(), slug="other", parent_task_id=root.task_id) session.commit() - walked = descendants(session, run_id=run_id, root_task_id=root.task_id) + walked = descendants(session, sample_id=sample_id, root_task_id=root.task_id) assert [node.task_id for node in walked] == [child.task_id, sibling.task_id, grandchild.task_id] assert other_run_child.task_id not in {node.task_id for node in walked} @@ -57,16 +59,18 @@ def test_descendants_walks_full_containment_subtree_past_terminal_nodes() -> Non def test_descendant_ids_respects_max_depth() -> None: session = _session() - run_id = uuid4() - root = _node(session, run_id=run_id, slug="root") - child = _node(session, run_id=run_id, slug="child", parent_task_id=root.task_id) - grandchild = _node(session, run_id=run_id, slug="grandchild", parent_task_id=child.task_id) + sample_id = uuid4() + root = _node(session, sample_id=sample_id, slug="root") + child = _node(session, sample_id=sample_id, slug="child", parent_task_id=root.task_id) + grandchild = _node( + session, sample_id=sample_id, slug="grandchild", parent_task_id=child.task_id + ) session.commit() - assert descendant_ids(session, run_id=run_id, root_task_id=root.task_id, max_depth=1) == { + assert descendant_ids(session, sample_id=sample_id, root_task_id=root.task_id, max_depth=1) == { child.task_id } - assert descendant_ids(session, run_id=run_id, root_task_id=root.task_id, max_depth=2) == { + assert descendant_ids(session, sample_id=sample_id, root_task_id=root.task_id, max_depth=2) == { child.task_id, grandchild.task_id, } diff --git a/ergon_core/tests/unit/runtime/test_graph_worker_identity.py b/ergon_core/tests/unit/runtime/test_graph_worker_identity.py index abea584a1..bab6c45ac 100644 --- a/ergon_core/tests/unit/runtime/test_graph_worker_identity.py +++ b/ergon_core/tests/unit/runtime/test_graph_worker_identity.py @@ -8,11 +8,11 @@ ExperimentDefinitionTaskAssignment, ExperimentDefinitionWorker, ) -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunTaskExecution, + SampleRecord, + SampleTaskAttempt, ) from ergon_core.core.application.runtime import execution as task_execution_module from ergon_core.core.application.runtime.models import MutationMeta @@ -22,7 +22,7 @@ PrepareTaskExecutionCommand, ) from ergon_core.core.application.runtime.task_execution import TaskExecutionService -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from ergon_core.test_support.task_factory import task_with_id from pydantic import BaseModel from sqlalchemy.pool import StaticPool @@ -95,19 +95,19 @@ def _run( session: Session, *, definition_id: UUID, - run_id: UUID | None = None, + sample_id: UUID | None = None, model_target: str = "stub:constant", ) -> UUID: - resolved_run_id = run_id or uuid4() + resolved_run_id = sample_id or uuid4() session.add( - RunRecord( + SampleRecord( id=resolved_run_id, definition_id=definition_id, benchmark_type="minif2f", instance_key="sample-1", worker_team_json={"primary": "minif2f-react"}, model_target=model_target, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) session.commit() @@ -117,18 +117,18 @@ def _run( def test_graph_initialization_writes_concrete_worker_slug_from_definition_binding() -> None: session = _session() definition_id = _definition_with_worker(session, worker_type="minif2f-react") - run_id = _run(session, definition_id=definition_id) + sample_id = _run(session, definition_id=definition_id) RuntimeGraphRepository().initialize_from_definition( session, - run_id, + sample_id, definition_id, initial_node_status=TaskExecutionStatus.PENDING, initial_edge_status="pending", meta=MutationMeta(actor="test"), ) - node = session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).one() + node = session.exec(select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id)).one() assert node.assigned_worker_slug == "minif2f-react" @@ -143,21 +143,21 @@ async def test_workflow_initialization_returns_task_ids_for_initial_ready_static worker_type="minif2f-react", benchmark_type=benchmark_type, ) - run_id = _run(session, definition_id=definition_id) + sample_id = _run(session, definition_id=definition_id) monkeypatch.setattr( - "ergon_core.core.application.runtime.run_lifecycle.get_session", + "ergon_core.core.application.runtime.sample_lifecycle.get_session", lambda: _session_context(session), ) initialized = await WorkflowService().initialize( - InitializeWorkflowCommand(run_id=run_id, definition_id=definition_id) + InitializeWorkflowCommand(sample_id=sample_id, definition_id=definition_id) ) assert len(initialized.initial_ready_tasks) == 1 ready_task = initialized.initial_ready_tasks[0] node = session.exec( - select(RunGraphNode).where(RunGraphNode.task_id == ready_task.task_id) + select(SampleGraphNode).where(SampleGraphNode.task_id == ready_task.task_id) ).one() assert ready_task.task_id == node.task_id assert node.assigned_worker_slug == "minif2f-react" @@ -169,7 +169,7 @@ async def test_dynamic_prepare_uses_node_worker_slug_and_run_model_without_defin ) -> None: session = _session() definition_id = _definition_with_worker(session, worker_type="minif2f-react") - run_id = _run(session, definition_id=definition_id, model_target="stub:constant") + sample_id = _run(session, definition_id=definition_id, model_target="stub:constant") task_id = uuid4() task = task_with_id( task_id, @@ -177,9 +177,9 @@ async def test_dynamic_prepare_uses_node_worker_slug_and_run_model_without_defin instance_key="sample-1", description="Dynamic specialist task", ) - node = RunGraphNode( + node = SampleGraphNode( task_id=task_id, - run_id=run_id, + sample_id=sample_id, instance_key="sample-1", task_slug="dynamic-leaf", description="Dynamic specialist task", @@ -197,14 +197,14 @@ async def test_dynamic_prepare_uses_node_worker_slug_and_run_model_without_defin prepared = await TaskExecutionService().prepare( PrepareTaskExecutionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, task_id=node.task_id, ) ) execution = session.exec( - select(RunTaskExecution).where(RunTaskExecution.id == prepared.execution_id) + select(SampleTaskAttempt).where(SampleTaskAttempt.id == prepared.execution_id) ).one() dynamic_worker = session.exec( select(ExperimentDefinitionWorker).where( diff --git a/ergon_core/tests/unit/runtime/test_identity_invariants.py b/ergon_core/tests/unit/runtime/test_identity_invariants.py index 2175b725c..0dfd1ae7b 100644 --- a/ergon_core/tests/unit/runtime/test_identity_invariants.py +++ b/ergon_core/tests/unit/runtime/test_identity_invariants.py @@ -1,6 +1,6 @@ """Identity-flow invariants from 02-persistence-layer.md §2. -task_id is born once and flows unchanged. (run_id, task_id) is the +task_id is born once and flows unchanged. (sample_id, task_id) is the canonical row key. execution_id is the per-attempt id. Sandbox identity is preserved across the worker → evaluate Inngest boundary. @@ -32,9 +32,9 @@ ExperimentDefinitionInstance, ExperimentDefinitionTask, ) -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.tests.unit.runtime._test_workers import EchoSandbox, EchoWorker from pydantic import BaseModel, ConfigDict from sqlalchemy.pool import StaticPool @@ -60,13 +60,13 @@ def _session() -> Session: def _seed_definition(session: Session) -> tuple[UUID, UUID, set[UUID]]: - """Insert a definition with two tasks; return (definition_id, run_id, + """Insert a definition with two tasks; return (definition_id, sample_id, set_of_task_ids).""" definition_id = uuid4() instance_id = uuid4() task_ids = {uuid4(), uuid4()} - run_id = uuid4() + sample_id = uuid4() session.add_all( [ ExperimentDefinition( @@ -100,17 +100,17 @@ def _seed_definition(session: Session) -> tuple[UUID, UUID, set[UUID]]: ) ) session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="test", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) session.commit() - return definition_id, run_id, task_ids + return definition_id, sample_id, task_ids # ── PR 1 invariant — GREEN today ───────────────────────────────────── @@ -118,29 +118,29 @@ def _seed_definition(session: Session) -> tuple[UUID, UUID, set[UUID]]: def test_task_id_is_preserved_from_definition_to_run_tier() -> None: """PR 1 invariant: the same UUID flows from - experiment_definition_tasks → run_graph_nodes. + experiment_definition_tasks → sample_graph_nodes. The runtime identity lives in ``task_id``. """ session = _session() - definition_id, run_id, defn_task_ids = _seed_definition(session) + definition_id, sample_id, defn_task_ids = _seed_definition(session) repo = RuntimeGraphRepository() repo.initialize_from_definition( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, initial_node_status="pending", initial_edge_status="pending", meta=MutationMeta(actor="test", reason="identity"), ) - rows = session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all() + rows = session.exec(select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id)).all() # Every task_id should appear on exactly one run-graph # row. - assert "task_id" in RunGraphNode.model_fields - assert "id" not in RunGraphNode.model_fields + assert "task_id" in SampleGraphNode.model_fields + assert "id" not in SampleGraphNode.model_fields seen = {row.task_id for row in rows} assert seen == defn_task_ids, ( f"task_id did not survive prepare: definition={defn_task_ids}, run-tier={seen}" @@ -157,29 +157,31 @@ async def test_task_id_propagates_into_runtime_task_instance() -> None: definition row had.""" session = _session() - definition_id, run_id, defn_task_ids = _seed_definition(session) + definition_id, sample_id, defn_task_ids = _seed_definition(session) repo = RuntimeGraphRepository() repo.initialize_from_definition( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, initial_node_status="pending", initial_edge_status="pending", meta=MutationMeta(actor="test", reason="identity"), ) - nodes = session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all() + nodes = session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all() for row in nodes: canonical_id = row.task_id - view = await repo.node(session, run_id=run_id, task_id=canonical_id) + view = await repo.node(session, sample_id=sample_id, task_id=canonical_id) assert view.task_id == canonical_id assert view.task.task_id == canonical_id # And the inflated task ids match the original definition task ids seen = set() for row in nodes: canonical_id = row.task_id - view = await repo.node(session, run_id=run_id, task_id=canonical_id) + view = await repo.node(session, sample_id=sample_id, task_id=canonical_id) seen.add(view.task.task_id) assert seen == defn_task_ids @@ -192,7 +194,7 @@ def test_sandbox_identity_is_preserved_across_worker_to_evaluate_boundary() -> N ``worker_execute`` stamps the live ``sandbox_id`` onto the execution row, and ``evaluate_task_run`` reads ``execution.sandbox_id`` and passes it to ``load_task_view(..., sandbox_id=...)`` to reattach. The - ``sandbox_id`` field on ``RunTaskExecution`` is the carrier; the + ``sandbox_id`` field on ``SampleTaskAttempt`` is the carrier; the structural guard checks both halves of the contract. """ @@ -201,10 +203,10 @@ def test_sandbox_identity_is_preserved_across_worker_to_evaluate_boundary() -> N from ergon_core.core.application.runtime.task_execution_repository import ( TaskExecutionRepository, ) - from ergon_core.core.persistence.telemetry.models import RunTaskExecution + from ergon_core.core.persistence.telemetry.models import SampleTaskAttempt # Carrier: the execution row owns the sandbox_id. - assert "sandbox_id" in RunTaskExecution.model_fields + assert "sandbox_id" in SampleTaskAttempt.model_fields # Internal writer still owns the column mutation; jobs reach it through # TaskExecutionService.attach_sandbox_to_execution. assert hasattr(TaskExecutionRepository, "set_sandbox_id") @@ -229,7 +231,7 @@ def test_execution_id_is_unique_per_attempt_and_shared_across_evaluators() -> No execution_id; a retry mints a new one. PR 4 makes execution_id the join key linking - ``RunTaskExecution`` ⇄ ``TaskExecutionService`` ⇄ each + ``SampleTaskAttempt`` ⇄ ``TaskExecutionService`` ⇄ each ``TaskEvaluateRequest`` invocation. The structural guard checks that (a) ``TaskEvaluateRequest`` carries ``execution_id``, (b) the orchestrator's fanout reuses ``prepared.execution_id`` for every @@ -297,19 +299,19 @@ def ctx_factory() -> _SessionContext: monkeypatch.setattr(inspection_module, "get_session", ctx_factory) -def _seed_identity_parent(session: Session, *, run_id: UUID) -> RunGraphNode: +def _seed_identity_parent(session: Session, *, sample_id: UUID) -> SampleGraphNode: session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=uuid4(), benchmark_type="test", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) - node = RunGraphNode( - run_id=run_id, + node = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug="parent", description="parent task", @@ -327,7 +329,7 @@ def _seed_identity_parent(session: Session, *, run_id: UUID) -> RunGraphNode: async def test_dynamic_task_id_has_no_definition_row( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Δ.3: dynamic spawn writes only to run_graph_nodes.""" + """Δ.3: dynamic spawn writes only to sample_graph_nodes.""" # 1. In-memory SQLite with all tables. engine = create_engine( @@ -338,8 +340,8 @@ async def test_dynamic_task_id_has_no_definition_row( SQLModel.metadata.create_all(engine) session = Session(engine) - run_id = uuid4() - parent = _seed_identity_parent(session, run_id=run_id) + sample_id = uuid4() + parent = _seed_identity_parent(session, sample_id=sample_id) _patch_get_session_identity(monkeypatch, session) monkeypatch.setattr( @@ -352,7 +354,7 @@ async def test_dynamic_task_id_has_no_definition_row( ) task_inspect = TaskInspectionService() context = WorkerContext._for_job( - run_id=run_id, + sample_id=sample_id, task_id=parent.task_id, execution_id=uuid4(), definition_id=None, @@ -385,14 +387,14 @@ async def test_dynamic_task_id_has_no_definition_row( ) assert def_count == 0 - # 3. It DOES appear as the task_id of exactly one run_graph_nodes row. + # 3. It DOES appear as the task_id of exactly one sample_graph_nodes row. node_count = len( - session.exec(select(RunGraphNode).where(RunGraphNode.task_id == handle.task_id)).all() + session.exec(select(SampleGraphNode).where(SampleGraphNode.task_id == handle.task_id)).all() ) assert node_count == 1 child_context = WorkerContext._for_job( - run_id=run_id, + sample_id=sample_id, task_id=handle.task_id, execution_id=uuid4(), definition_id=None, diff --git a/ergon_core/tests/unit/runtime/test_import_boundaries.py b/ergon_core/tests/unit/runtime/test_import_boundaries.py index 7bd029eb5..ea6ccf154 100644 --- a/ergon_core/tests/unit/runtime/test_import_boundaries.py +++ b/ergon_core/tests/unit/runtime/test_import_boundaries.py @@ -42,18 +42,18 @@ def test_persistent_component_catalog_model_is_deleted() -> None: def test_telemetry_models_import_before_run_resource_api() -> None: - from ergon_core.core.persistence.telemetry.models import RunResource + from ergon_core.core.persistence.telemetry.models import SampleResource - from ergon_core.core.application.resources import RunResourceView + from ergon_core.core.application.resources import SampleResourceView - assert RunResource.__tablename__ == "run_resources" - assert RunResourceView.__name__ == "RunResourceView" + assert SampleResource.__tablename__ == "sample_resources" + assert SampleResourceView.__name__ == "SampleResourceView" def test_context_models_import_without_worker_cycle() -> None: - from ergon_core.core.persistence.context.models import RunContextEvent + from ergon_core.core.persistence.context.models import SampleContextEvent - assert RunContextEvent.__tablename__ == "run_context_events" + assert SampleContextEvent.__tablename__ == "sample_context_events" def test_context_part_logs_use_shared_logprob_type_without_api_cycle() -> None: @@ -70,7 +70,7 @@ def test_worker_execute_does_not_expose_result_adapter_helpers() -> None: def test_runs_api_does_not_own_run_snapshot_read_model_helpers() -> None: - import ergon_core.core.infrastructure.http.routes.runs as runs_api + import ergon_core.core.infrastructure.http.routes.samples as runs_api assert not hasattr(runs_api, "_build_task_map") assert not hasattr(runs_api, "_task_keyed_executions") diff --git a/ergon_core/tests/unit/runtime/test_persist_outputs.py b/ergon_core/tests/unit/runtime/test_persist_outputs.py index 8e6310c78..1e5fd41f2 100644 --- a/ergon_core/tests/unit/runtime/test_persist_outputs.py +++ b/ergon_core/tests/unit/runtime/test_persist_outputs.py @@ -12,8 +12,8 @@ class _FakeTaskExecutionService: def __init__(self, seen: list[str | None]) -> None: self._seen = seen - async def load_task_view(self, _session, *, run_id, task_id, sandbox_id=None): - del run_id, task_id + async def load_task_view(self, _session, *, sample_id, task_id, sandbox_id=None): + del sample_id, task_id self._seen.append(sandbox_id) sandbox = SimpleNamespace(output_path="/workspace/public-output/") return SimpleNamespace(task=SimpleNamespace(sandbox=sandbox)) @@ -56,11 +56,11 @@ async def test_persist_outputs_publishes_public_sandbox_through_resource_service lambda: _FakeTaskExecutionService(seen_sandbox_ids), ) monkeypatch.setattr(composition, "SandboxResourcePublisher", _FakePublisher) - monkeypatch.setattr(composition, "RunResourcePublishService", _FakePublishService) + monkeypatch.setattr(composition, "SampleResourcePublishService", _FakePublishService) result = await run_persist_outputs_job( PersistOutputsRequest( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), diff --git a/ergon_core/tests/unit/runtime/test_persist_outputs_resources.py b/ergon_core/tests/unit/runtime/test_persist_outputs_resources.py index 11b6c358f..3a143c15a 100644 --- a/ergon_core/tests/unit/runtime/test_persist_outputs_resources.py +++ b/ergon_core/tests/unit/runtime/test_persist_outputs_resources.py @@ -31,13 +31,13 @@ async def publish_sandbox_files(self, **kwargs): @pytest.mark.asyncio async def test_worker_final_message_is_not_published_as_run_resource(monkeypatch) -> None: monkeypatch.setattr(persist_outputs, "SandboxResourcePublisher", _Publisher) - monkeypatch.setattr(persist_outputs, "RunResourcePublishService", _PublishService) + monkeypatch.setattr(persist_outputs, "SampleResourcePublishService", _PublishService) count = await persist_outputs.publish_public_sandbox_resources( _Manager().get_sandbox(uuid4()), PersistOutputsRequest.model_validate( { - "run_id": uuid4(), + "sample_id": uuid4(), "definition_id": uuid4(), "task_id": uuid4(), "execution_id": uuid4(), diff --git a/ergon_core/tests/unit/runtime/test_propagation_contracts.py b/ergon_core/tests/unit/runtime/test_propagation_contracts.py index f8ad655d7..4954be202 100644 --- a/ergon_core/tests/unit/runtime/test_propagation_contracts.py +++ b/ergon_core/tests/unit/runtime/test_propagation_contracts.py @@ -1,5 +1,5 @@ from ergon_core.core.application.runtime import status as graph_status -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode from ergon_core.core.persistence.definitions.models import ExperimentDefinition from ergon_core.core.application.runtime import execution as task_execution_service from ergon_core.core.application.runtime.lifecycle import on_task_completed_or_failed @@ -56,17 +56,17 @@ async def test_parent_completion_readies_dependency_free_dynamic_children() -> N ) SQLModel.metadata.create_all(engine) session = Session(engine) - run_id = uuid4() - parent = RunGraphNode( - run_id=run_id, + sample_id = uuid4() + parent = SampleGraphNode( + sample_id=sample_id, instance_key="sample", task_slug="parent", description="parent", status=graph_status.COMPLETED, level=0, ) - root_child = RunGraphNode( - run_id=run_id, + root_child = SampleGraphNode( + sample_id=sample_id, instance_key="sample", task_slug="root-child", description="root child", @@ -74,8 +74,8 @@ async def test_parent_completion_readies_dependency_free_dynamic_children() -> N parent_task_id=parent.task_id, level=1, ) - blocked_child = RunGraphNode( - run_id=run_id, + blocked_child = SampleGraphNode( + sample_id=sample_id, instance_key="sample", task_slug="blocked-child", description="blocked child", @@ -86,8 +86,8 @@ async def test_parent_completion_readies_dependency_free_dynamic_children() -> N session.add_all([parent, root_child, blocked_child]) session.flush() session.add( - RunGraphEdge( - run_id=run_id, + SampleGraphEdge( + sample_id=sample_id, source_task_id=root_child.task_id, target_task_id=blocked_child.task_id, status=graph_status.EDGE_PENDING, @@ -97,7 +97,7 @@ async def test_parent_completion_readies_dependency_free_dynamic_children() -> N ready = await on_task_completed_or_failed( session, - run_id, + sample_id, parent.task_id, graph_status.COMPLETED, graph_repo=RuntimeGraphRepository(), diff --git a/ergon_core/tests/unit/runtime/test_real_llm_rollout_artifact_health.py b/ergon_core/tests/unit/runtime/test_real_llm_rollout_artifact_health.py index f0570a8c2..286207bd3 100644 --- a/ergon_core/tests/unit/runtime/test_real_llm_rollout_artifact_health.py +++ b/ergon_core/tests/unit/runtime/test_real_llm_rollout_artifact_health.py @@ -37,7 +37,7 @@ def _write_minimal_rollout( (root / "manifest.json").write_text( json.dumps( { - "run_id": str(uuid4()), + "sample_id": str(uuid4()), "benchmark": "researchrubrics", "worker": "researchrubrics-researcher", "evaluator": "research-rubric", @@ -47,16 +47,16 @@ def _write_minimal_rollout( "wall_clock": {"duration_seconds": 1.0}, "screenshots": {}, "db_row_counts": { - "run_task_executions": task_count, - "run_task_evaluations": len(evaluation_rows or []), - "run_resources": len(resources), - "run_graph_nodes": task_count, + "sample_task_attempts": task_count, + "sample_task_evaluations": len(evaluation_rows or []), + "sample_resources": len(resources), + "sample_graph_nodes": task_count, }, } ) ) _write_jsonl( - db / "run_task_executions.jsonl", + db / "sample_task_attempts.jsonl", [ { "id": execution_ids[idx], @@ -67,7 +67,7 @@ def _write_minimal_rollout( ], ) _write_jsonl( - db / "run_graph_nodes.jsonl", + db / "sample_graph_nodes.jsonl", [ { "id": str(uuid4()), @@ -79,8 +79,8 @@ def _write_minimal_rollout( for idx in range(task_count) ], ) - _write_jsonl(db / "run_resources.jsonl", resources) - _write_jsonl(db / "run_task_evaluations.jsonl", evaluation_rows or []) + _write_jsonl(db / "sample_resources.jsonl", resources) + _write_jsonl(db / "sample_task_evaluations.jsonl", evaluation_rows or []) def test_artifact_health_fails_when_completed_tasks_lack_evaluations(tmp_path: Path) -> None: diff --git a/ergon_core/tests/unit/runtime/test_rubric_evaluation_service.py b/ergon_core/tests/unit/runtime/test_rubric_evaluation_service.py index 400a9fddb..3949f28e1 100644 --- a/ergon_core/tests/unit/runtime/test_rubric_evaluation_service.py +++ b/ergon_core/tests/unit/runtime/test_rubric_evaluation_service.py @@ -68,7 +68,7 @@ async def test_rubric_service_uses_criterion_max_score_not_signed_weight() -> No ) result = await service.evaluate( context=CriterionContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=task.task_id, execution_id=uuid4(), task=task, @@ -125,7 +125,7 @@ async def test_evaluator_dependencies_are_validated_before_criteria_resolution() with pytest.raises(DependencyError, match="definitely_missing_ergon_eval_dep_17"): await service.evaluate( context=CriterionContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=task.task_id, execution_id=uuid4(), task=task, diff --git a/ergon_core/tests/unit/runtime/test_run_record_missing_error.py b/ergon_core/tests/unit/runtime/test_run_record_missing_error.py deleted file mode 100644 index 179078ab2..000000000 --- a/ergon_core/tests/unit/runtime/test_run_record_missing_error.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Tests for RunRecordMissingError and the runtime identity helper.""" - -from uuid import uuid4 - -import pytest -from ergon_core.core.application.runtime.run_identity import definition_id_for_run -from ergon_core.core.application.runtime.task_errors import RunRecordMissingError - - -def test_error_message_contains_run_id(): - """RunRecordMissingError message includes the run_id for debugging.""" - run_id = uuid4() - err = RunRecordMissingError(run_id) - assert str(run_id) in str(err) - - -def test_error_is_exception_subclass(): - """RunRecordMissingError must be an Exception subclass so it propagates as an error.""" - run_id = uuid4() - err = RunRecordMissingError(run_id) - assert isinstance(err, Exception) - - -def test_service_raises_when_run_record_missing(): - """definition_id_for_run raises RunRecordMissingError when session returns None.""" - from unittest.mock import MagicMock - - session = MagicMock() - # exec().first() returns None → no RunRecord found - session.exec.return_value.first.return_value = None - - run_id = uuid4() - - with pytest.raises(RunRecordMissingError, match=str(run_id)): - definition_id_for_run(session, run_id) diff --git a/ergon_core/tests/unit/runtime/test_run_graph_task_snapshot.py b/ergon_core/tests/unit/runtime/test_sample_graph_task_snapshot.py similarity index 82% rename from ergon_core/tests/unit/runtime/test_run_graph_task_snapshot.py rename to ergon_core/tests/unit/runtime/test_sample_graph_task_snapshot.py index af71ae918..c612a4f0f 100644 --- a/ergon_core/tests/unit/runtime/test_run_graph_task_snapshot.py +++ b/ergon_core/tests/unit/runtime/test_sample_graph_task_snapshot.py @@ -17,9 +17,9 @@ ExperimentDefinitionInstance, ExperimentDefinitionTask, ) -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from pydantic import BaseModel, ConfigDict from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine, select @@ -88,16 +88,16 @@ def _seed_run( session: Session, *, definition_id: UUID, - run_id: UUID, + sample_id: UUID, ) -> None: session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="test", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) session.commit() @@ -105,25 +105,25 @@ def _seed_run( def test_initialize_from_definition_copies_task_json() -> None: session = _session() - run_id = uuid4() + sample_id = uuid4() definition_id, _task_id = _seed_definition(session, task_slug="solve", payload={"problem": "p"}) _seed_run( session, definition_id=definition_id, - run_id=run_id, + sample_id=sample_id, ) repo = RuntimeGraphRepository() repo.initialize_from_definition( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, initial_node_status="pending", initial_edge_status="pending", meta=MutationMeta(actor="test", reason="snapshot"), ) - rows = session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all() + rows = session.exec(select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id)).all() assert rows, "initialize_from_definition produced no nodes" row = rows[0] assert row.task_json, "task_json must be populated for static nodes" @@ -137,18 +137,18 @@ def test_initialize_from_definition_copies_task_json() -> None: @pytest.mark.asyncio async def test_graph_repo_node_inflates_task_from_run_tier() -> None: - """PR 2 invariant: graph_repo.node reads run_graph_nodes.task_json - and returns a typed RunGraphNodeView with the Task already + """PR 2 invariant: graph_repo.node reads sample_graph_nodes.task_json + and returns a typed SampleGraphNodeView with the Task already inflated. No definition-tier read; no raw dict in the caller's hands.""" session = _session() - run_id = uuid4() + sample_id = uuid4() definition_id, _task_id = _seed_definition(session, task_slug="solve", payload={"problem": "p"}) _seed_run( session, definition_id=definition_id, - run_id=run_id, + sample_id=sample_id, ) repo = RuntimeGraphRepository() @@ -156,17 +156,19 @@ async def test_graph_repo_node_inflates_task_from_run_tier() -> None: # inflate. repo.initialize_from_definition( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, initial_node_status="pending", initial_edge_status="pending", meta=MutationMeta(actor="test", reason="setup"), ) - row = session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).first() + row = session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).first() assert row is not None canonical_task_id = row.task_id - view = await repo.node(session, run_id=run_id, task_id=canonical_task_id) + view = await repo.node(session, sample_id=sample_id, task_id=canonical_task_id) assert view.task.task_slug == "solve" assert view.task_id == canonical_task_id @@ -177,7 +179,7 @@ async def test_graph_repo_node_inflates_task_from_run_tier() -> None: def test_graph_repo_node_does_not_reference_definition_tier_models() -> None: """PR 2 textual boundary guard: `graph_repo.node`'s source must not mention definition-tier symbols. The runtime read path goes through - run_graph_nodes.task_json only — any subtle import or helper + sample_graph_nodes.task_json only — any subtle import or helper delegation to DefinitionRepository would re-open the read path PR 11 is closing.""" @@ -200,12 +202,12 @@ def test_graph_repo_node_does_not_reference_definition_tier_models() -> None: @pytest.mark.asyncio async def test_add_node_can_write_dynamic_task_json() -> None: session = _session() - run_id = uuid4() + sample_id = uuid4() definition_id, _ = _seed_definition(session, task_slug="parent", payload={}) _seed_run( session, definition_id=definition_id, - run_id=run_id, + sample_id=sample_id, ) repo = RuntimeGraphRepository() @@ -216,7 +218,7 @@ async def test_add_node_can_write_dynamic_task_json() -> None: } node_dto = await repo.add_node( session, - run_id, + sample_id, task_slug="child", instance_key="sample-1", description="child task", @@ -226,7 +228,7 @@ async def test_add_node_can_write_dynamic_task_json() -> None: meta=MutationMeta(actor="test", reason="dynamic"), ) - row = session.get(RunGraphNode, (run_id, node_dto.task_id)) + row = session.get(SampleGraphNode, (sample_id, node_dto.task_id)) assert row is not None assert row.task_json == payload assert row.is_dynamic is True @@ -239,19 +241,19 @@ def test_initialize_from_definition_can_seed_same_task_ids_for_multiple_runs() - definition_id, definition_task_id = _seed_definition( session, task_slug="solve", payload={"problem": "p"} ) - _seed_run(session, definition_id=definition_id, run_id=run_a) - _seed_run(session, definition_id=definition_id, run_id=run_b) + _seed_run(session, definition_id=definition_id, sample_id=run_a) + _seed_run(session, definition_id=definition_id, sample_id=run_b) repo = RuntimeGraphRepository() - for run_id in (run_a, run_b): + for sample_id in (run_a, run_b): repo.initialize_from_definition( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, initial_node_status="pending", initial_edge_status="pending", meta=MutationMeta(actor="test", reason="multi-run"), ) - assert session.get(RunGraphNode, (run_a, definition_task_id)) is not None - assert session.get(RunGraphNode, (run_b, definition_task_id)) is not None + assert session.get(SampleGraphNode, (run_a, definition_task_id)) is not None + assert session.get(SampleGraphNode, (run_b, definition_task_id)) is not None diff --git a/ergon_core/tests/unit/runtime/test_run_read_service.py b/ergon_core/tests/unit/runtime/test_sample_read_service.py similarity index 66% rename from ergon_core/tests/unit/runtime/test_run_read_service.py rename to ergon_core/tests/unit/runtime/test_sample_read_service.py index bba42d9c7..af1f1bd99 100644 --- a/ergon_core/tests/unit/runtime/test_run_read_service.py +++ b/ergon_core/tests/unit/runtime/test_sample_read_service.py @@ -1,19 +1,19 @@ from types import SimpleNamespace -from ergon_core.core.views.runs.service import _display_run_score -from ergon_core.core.persistence.shared.enums import RunStatus +from ergon_core.core.views.samples.service import _display_run_score +from ergon_core.core.persistence.shared.enums import SampleStatus def test_display_run_score_uses_normalized_score_for_multi_evaluator_runs() -> None: score_summary = SimpleNamespace(final_score=2.0, normalized_score=1.0) - assert _display_run_score(score_summary, RunStatus.COMPLETED, {}) == 1.0 + assert _display_run_score(score_summary, SampleStatus.COMPLETED, {}) == 1.0 def test_display_run_score_does_not_fallback_to_raw_total_score() -> None: score_summary = SimpleNamespace(final_score=1.0, normalized_score=None) - assert _display_run_score(score_summary, RunStatus.COMPLETED, {}) is None + assert _display_run_score(score_summary, SampleStatus.COMPLETED, {}) is None def test_display_run_score_uses_persisted_score_for_failed_runs() -> None: @@ -22,7 +22,7 @@ def test_display_run_score_uses_persisted_score_for_failed_runs() -> None: assert ( _display_run_score( score_summary, - RunStatus.FAILED, + SampleStatus.FAILED, {"normalized_score": 0.75}, ) == 0.75 diff --git a/ergon_core/tests/unit/runtime/test_sample_record_missing_error.py b/ergon_core/tests/unit/runtime/test_sample_record_missing_error.py new file mode 100644 index 000000000..c476c428f --- /dev/null +++ b/ergon_core/tests/unit/runtime/test_sample_record_missing_error.py @@ -0,0 +1,35 @@ +"""Tests for SampleRecordMissingError and the runtime identity helper.""" + +from uuid import uuid4 + +import pytest +from ergon_core.core.application.runtime.sample_identity import definition_id_for_run +from ergon_core.core.application.runtime.task_errors import SampleRecordMissingError + + +def test_error_message_contains_run_id(): + """SampleRecordMissingError message includes the sample_id for debugging.""" + sample_id = uuid4() + err = SampleRecordMissingError(sample_id) + assert str(sample_id) in str(err) + + +def test_error_is_exception_subclass(): + """SampleRecordMissingError must be an Exception subclass so it propagates as an error.""" + sample_id = uuid4() + err = SampleRecordMissingError(sample_id) + assert isinstance(err, Exception) + + +def test_service_raises_when_run_record_missing(): + """definition_id_for_run raises SampleRecordMissingError when session returns None.""" + from unittest.mock import MagicMock + + session = MagicMock() + # exec().first() returns None → no SampleRecord found + session.exec.return_value.first.return_value = None + + sample_id = uuid4() + + with pytest.raises(SampleRecordMissingError, match=str(sample_id)): + definition_id_for_run(session, sample_id) diff --git a/ergon_core/tests/unit/runtime/test_sample_record_service.py b/ergon_core/tests/unit/runtime/test_sample_record_service.py new file mode 100644 index 000000000..0e4ca2f32 --- /dev/null +++ b/ergon_core/tests/unit/runtime/test_sample_record_service.py @@ -0,0 +1,60 @@ +from uuid import uuid4 + +from ergon_core.core.application.experiments.handles import DefinitionHandle +from ergon_core.core.application.runtime import samples as sample_service +from ergon_core.core.persistence.shared.enums import SampleStatus + + +class _FakeSession: + def __init__(self) -> None: + self.added = [] + + def __enter__(self) -> "_FakeSession": + return self + + def __exit__(self, *args) -> None: + return None + + def add(self, row) -> None: + self.added.append(row) + + def commit(self) -> None: + return None + + def refresh(self, row) -> None: + return None + + +def test_create_definition_backed_sample_records_assignment(monkeypatch): + session = _FakeSession() + definition_id = uuid4() + definition = DefinitionHandle( + definition_id=definition_id, + benchmark_type="ci-benchmark", + worker_bindings={"primary": "test-worker"}, + evaluator_bindings={"primary": "test-evaluator"}, + ) + + monkeypatch.setattr(sample_service, "get_session", lambda: session) + + sample = sample_service.create_definition_backed_sample( + definition, + definition_id=definition_id, + instance_key="sample-1", + worker_team_json={"primary": "test-worker"}, + evaluator_slug="test-evaluator", + model_target="openai:gpt-4o", + assignment_json={"arm_key": "default"}, + seed=123, + ) + + assert session.added == [sample] + assert sample.definition_id == definition_id + assert sample.benchmark_type == "ci-benchmark" + assert sample.instance_key == "sample-1" + assert sample.worker_team_json == {"primary": "test-worker"} + assert sample.evaluator_slug == "test-evaluator" + assert sample.model_target == "openai:gpt-4o" + assert sample.assignment_json == {"arm_key": "default"} + assert sample.seed == 123 + assert sample.status == SampleStatus.PENDING diff --git a/ergon_core/tests/unit/runtime/test_run_service.py b/ergon_core/tests/unit/runtime/test_sample_service.py similarity index 84% rename from ergon_core/tests/unit/runtime/test_run_service.py rename to ergon_core/tests/unit/runtime/test_sample_service.py index 0d178432a..e40bf72b0 100644 --- a/ergon_core/tests/unit/runtime/test_run_service.py +++ b/ergon_core/tests/unit/runtime/test_sample_service.py @@ -1,8 +1,8 @@ from uuid import uuid4 from ergon_core.core.application.experiments.handles import DefinitionHandle -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.application.runtime import runs as run_service +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.application.runtime import samples as sample_service class _FakeSession: @@ -35,9 +35,9 @@ def test_create_run_requires_definition_identity_and_records_assignment(monkeypa evaluator_bindings={"primary": "test-evaluator"}, ) - monkeypatch.setattr(run_service, "get_session", lambda: session) + monkeypatch.setattr(sample_service, "get_session", lambda: session) - run = run_service.create_run( + run = sample_service.create_run( definition, definition_id=definition_id, instance_key="sample-1", @@ -57,4 +57,4 @@ def test_create_run_requires_definition_identity_and_records_assignment(monkeypa assert run.model_target == "openai:gpt-4o" assert run.assignment_json == {"arm_key": "default"} assert run.seed == 123 - assert run.status == RunStatus.PENDING + assert run.status == SampleStatus.PENDING diff --git a/ergon_core/tests/unit/runtime/test_sandbox_cleanup.py b/ergon_core/tests/unit/runtime/test_sandbox_cleanup.py index 4007dff2c..8fb962d81 100644 --- a/ergon_core/tests/unit/runtime/test_sandbox_cleanup.py +++ b/ergon_core/tests/unit/runtime/test_sandbox_cleanup.py @@ -36,7 +36,7 @@ def __init__(self) -> None: async def test_cleanup_on_completed_terminates_sandbox(monkeypatch: pytest.MonkeyPatch) -> None: """A ``task/completed`` event with a sandbox_id terminates that sandbox.""" payload = TaskCompletedEvent( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), @@ -65,7 +65,7 @@ async def fake_terminate(sandbox_id: str) -> SandboxTerminationResult: async def test_cleanup_on_failed_terminates_sandbox(monkeypatch: pytest.MonkeyPatch) -> None: """A ``task/failed`` event with a sandbox_id terminates that sandbox.""" payload = TaskFailedEvent( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), @@ -97,7 +97,7 @@ async def test_cleanup_on_failed_skips_when_sandbox_id_missing( ) -> None: """A failure before sandbox-setup carries ``sandbox_id=None``; cleanup is a no-op.""" payload = TaskFailedEvent( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), diff --git a/ergon_core/tests/unit/runtime/test_smoke_worker_metrics.py b/ergon_core/tests/unit/runtime/test_smoke_worker_metrics.py index eafdfc756..b60ffcf5f 100644 --- a/ergon_core/tests/unit/runtime/test_smoke_worker_metrics.py +++ b/ergon_core/tests/unit/runtime/test_smoke_worker_metrics.py @@ -9,7 +9,7 @@ class _SpawnOnlyContext: def __init__(self) -> None: - self.run_id = uuid4() + self.sample_id = uuid4() self.task_id = uuid4() self.execution_id = uuid4() self.sandbox_id = "smoke-sandbox-test" diff --git a/ergon_core/tests/unit/runtime/test_spawn_dynamic_task.py b/ergon_core/tests/unit/runtime/test_spawn_dynamic_task.py index b506259c0..b92b6c2ec 100644 --- a/ergon_core/tests/unit/runtime/test_spawn_dynamic_task.py +++ b/ergon_core/tests/unit/runtime/test_spawn_dynamic_task.py @@ -2,10 +2,10 @@ Verifies the dynamic-spawn write path: -- Exactly one row inserted into run_graph_nodes with is_dynamic=True and +- Exactly one row inserted into sample_graph_nodes with is_dynamic=True and the full Task snapshot in task_json. - Zero rows inserted into experiment_definition_tasks. -- Optional depends_on creates the dependency edge in run_graph_edges. +- Optional depends_on creates the dependency edge in sample_graph_edges. - Returned SpawnedTaskHandle.task_id matches the inserted node's task_id. """ @@ -20,9 +20,9 @@ from ergon_core.core.application.runtime import management as management_module from ergon_core.core.application.runtime.task_management import TaskManagementService from ergon_core.core.persistence.definitions.models import ExperimentDefinitionTask -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.tests.unit.runtime._test_workers import EchoSandbox, EchoWorker from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine, select @@ -61,19 +61,19 @@ def _make_session() -> Session: return Session(engine) -def _seed_parent(session: Session, *, run_id: UUID) -> RunGraphNode: +def _seed_parent(session: Session, *, sample_id: UUID) -> SampleGraphNode: session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=uuid4(), benchmark_type="test", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) - parent = RunGraphNode( - run_id=run_id, + parent = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug="parent", description="Parent task", @@ -87,9 +87,9 @@ def _seed_parent(session: Session, *, run_id: UUID) -> RunGraphNode: return parent -def _seed_other(session: Session, *, run_id: UUID, slug: str) -> RunGraphNode: - node = RunGraphNode( - run_id=run_id, +def _seed_other(session: Session, *, sample_id: UUID, slug: str) -> SampleGraphNode: + node = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug=slug, description=f"Task {slug}", @@ -162,24 +162,24 @@ async def test_spawn_dynamic_task_inserts_dynamic_node_with_task_json( ) -> None: """A single is_dynamic=True row is written with the full Task snapshot.""" session = _make_session() - run_id = uuid4() - parent = _seed_parent(session, run_id=run_id) + sample_id = uuid4() + parent = _seed_parent(session, sample_id=sample_id) svc = _service(session, monkeypatch) - nodes_before = session.exec(select(RunGraphNode)).all() + nodes_before = session.exec(select(SampleGraphNode)).all() assert len(nodes_before) == 1 # only the parent task = _make_task() handle = await svc.spawn_dynamic_task( - run_id=run_id, + sample_id=sample_id, parent_task_id=parent.task_id, task=task, ) new_node = session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.task_slug == "child", + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.task_slug == "child", ) ).one() @@ -208,14 +208,14 @@ async def test_spawn_dynamic_task_does_not_write_definition_row( ) -> None: """experiment_definition_tasks count is unchanged before/after spawn.""" session = _make_session() - run_id = uuid4() - parent = _seed_parent(session, run_id=run_id) + sample_id = uuid4() + parent = _seed_parent(session, sample_id=sample_id) svc = _service(session, monkeypatch) defs_before = session.exec(select(ExperimentDefinitionTask)).all() await svc.spawn_dynamic_task( - run_id=run_id, + sample_id=sample_id, parent_task_id=parent.task_id, task=_make_task(), ) @@ -228,21 +228,23 @@ async def test_spawn_dynamic_task_does_not_write_definition_row( async def test_spawn_dynamic_task_creates_dependency_edge( monkeypatch: pytest.MonkeyPatch, ) -> None: - """depends_on=(other,) writes a run_graph_edges row source=other, target=new.""" + """depends_on=(other,) writes a sample_graph_edges row source=other, target=new.""" session = _make_session() - run_id = uuid4() - parent = _seed_parent(session, run_id=run_id) - other = _seed_other(session, run_id=run_id, slug="other") + sample_id = uuid4() + parent = _seed_parent(session, sample_id=sample_id) + other = _seed_other(session, sample_id=sample_id, slug="other") svc = _service(session, monkeypatch) handle = await svc.spawn_dynamic_task( - run_id=run_id, + sample_id=sample_id, parent_task_id=parent.task_id, task=_make_task(), depends_on=(other.task_id,), ) - edges = session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all() + edges = session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all() assert len(edges) == 1 assert edges[0].source_task_id == other.task_id assert edges[0].target_task_id == handle.task_id @@ -254,12 +256,12 @@ async def test_spawn_dynamic_task_handle_matches_inserted_row_id( ) -> None: """SpawnedTaskHandle.task_id is the new task's task_id (UUID).""" session = _make_session() - run_id = uuid4() - parent = _seed_parent(session, run_id=run_id) + sample_id = uuid4() + parent = _seed_parent(session, sample_id=sample_id) svc = _service(session, monkeypatch) handle = await svc.spawn_dynamic_task( - run_id=run_id, + sample_id=sample_id, parent_task_id=parent.task_id, task=_make_task(), ) @@ -267,9 +269,9 @@ async def test_spawn_dynamic_task_handle_matches_inserted_row_id( inserted_ids = frozenset( row.task_id for row in session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.task_slug == "child", + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.task_slug == "child", ) ).all() ) @@ -285,8 +287,8 @@ async def test_step_aware_spawn_dynamic_task_is_replay_safe( ) -> None: """Repeated Inngest replay returns the memoized handle without duplicating DB rows/events.""" session = _make_session() - run_id = uuid4() - parent = _seed_parent(session, run_id=run_id) + sample_id = uuid4() + parent = _seed_parent(session, sample_id=sample_id) _patch = monkeypatch.setattr _patch(management_module, "get_session", lambda: _SessionContext(session)) _patch( @@ -299,21 +301,21 @@ async def test_step_aware_spawn_dynamic_task_is_replay_safe( task = _make_task() first = await _StepAwareTaskManagementService(_FakeCtx(step)).spawn_dynamic_task( - run_id=run_id, + sample_id=sample_id, parent_task_id=parent.task_id, task=task, ) second = await _StepAwareTaskManagementService(_FakeCtx(step)).spawn_dynamic_task( - run_id=run_id, + sample_id=sample_id, parent_task_id=parent.task_id, task=task, ) child_rows = session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.parent_task_id == parent.task_id, - RunGraphNode.task_slug == "child", + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.parent_task_id == parent.task_id, + SampleGraphNode.task_slug == "child", ) ).all() assert first == second diff --git a/ergon_core/tests/unit/runtime/test_task_execution_public_facade.py b/ergon_core/tests/unit/runtime/test_task_execution_public_facade.py index d1394a7df..488aa58cb 100644 --- a/ergon_core/tests/unit/runtime/test_task_execution_public_facade.py +++ b/ergon_core/tests/unit/runtime/test_task_execution_public_facade.py @@ -10,11 +10,11 @@ class _GraphRepo: def __init__(self) -> None: self.calls = [] - async def node(self, session, *, run_id, task_id, sandbox_id=None): + async def node(self, session, *, sample_id, task_id, sandbox_id=None): self.calls.append( { "session": session, - "run_id": run_id, + "sample_id": sample_id, "task_id": task_id, "sandbox_id": sandbox_id, } @@ -59,12 +59,12 @@ async def test_task_execution_facade_loads_task_view_by_task_id() -> None: graph_repo = _GraphRepo() service = TaskExecutionService(graph_repo=graph_repo) session = object() - run_id = uuid4() + sample_id = uuid4() task_id = uuid4() view = await service.load_task_view( session, - run_id=run_id, + sample_id=sample_id, task_id=task_id, sandbox_id="sandbox-1", ) @@ -73,7 +73,7 @@ async def test_task_execution_facade_loads_task_view_by_task_id() -> None: assert graph_repo.calls == [ { "session": session, - "run_id": run_id, + "sample_id": sample_id, "task_id": task_id, "sandbox_id": "sandbox-1", } diff --git a/ergon_core/tests/unit/runtime/test_task_execution_repository.py b/ergon_core/tests/unit/runtime/test_task_execution_repository.py index 5d71ccc72..76afe8da9 100644 --- a/ergon_core/tests/unit/runtime/test_task_execution_repository.py +++ b/ergon_core/tests/unit/runtime/test_task_execution_repository.py @@ -1,9 +1,9 @@ from datetime import UTC, datetime, timedelta from uuid import UUID, uuid4 -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord, RunTaskExecution +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord, SampleTaskAttempt from ergon_core.core.application.runtime.task_execution_repository import TaskExecutionRepository from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine @@ -20,23 +20,23 @@ def _session() -> Session: def _run(session: Session) -> UUID: - run_id = uuid4() + sample_id = uuid4() session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=uuid4(), benchmark_type="ci-task-execution-repository", instance_key="sample-1", worker_team_json={"primary": "test-worker"}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) - return run_id + return sample_id -def _node(session: Session, run_id: UUID) -> UUID: - node = RunGraphNode( - run_id=run_id, +def _node(session: Session, sample_id: UUID) -> UUID: + node = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug="task", description="Task", @@ -49,14 +49,14 @@ def _node(session: Session, run_id: UUID) -> UUID: def _execution( *, - run_id: UUID, + sample_id: UUID, task_id: UUID, attempt_number: int, started_at: datetime, message: str = "output", -) -> RunTaskExecution: - return RunTaskExecution( - run_id=run_id, +) -> SampleTaskAttempt: + return SampleTaskAttempt( + sample_id=sample_id, task_id=task_id, attempt_number=attempt_number, status=TaskExecutionStatus.COMPLETED, @@ -67,25 +67,25 @@ def _execution( def test_latest_for_node_orders_by_attempt_then_started_at() -> None: session = _session() - run_id = _run(session) - node_id = _node(session, run_id) + sample_id = _run(session) + node_id = _node(session, sample_id) now = datetime(2026, 4, 28, 12, 0, tzinfo=UTC) older_attempt_two = _execution( - run_id=run_id, + sample_id=sample_id, task_id=node_id, attempt_number=2, started_at=now, message="attempt-two-old", ) newer_attempt_one = _execution( - run_id=run_id, + sample_id=sample_id, task_id=node_id, attempt_number=1, started_at=now + timedelta(minutes=10), message="attempt-one-newer", ) newer_attempt_two = _execution( - run_id=run_id, + sample_id=sample_id, task_id=node_id, attempt_number=2, started_at=now + timedelta(minutes=5), @@ -103,15 +103,15 @@ def test_latest_for_node_orders_by_attempt_then_started_at() -> None: def test_next_attempt_counts_existing_node_executions() -> None: session = _session() - run_id = _run(session) - node_id = _node(session, run_id) + sample_id = _run(session) + node_id = _node(session, sample_id) now = datetime(2026, 4, 28, 12, 0, tzinfo=UTC) session.add_all( [ - _execution(run_id=run_id, task_id=node_id, attempt_number=1, started_at=now), - _execution(run_id=run_id, task_id=node_id, attempt_number=2, started_at=now), + _execution(sample_id=sample_id, task_id=node_id, attempt_number=1, started_at=now), + _execution(sample_id=sample_id, task_id=node_id, attempt_number=2, started_at=now), ] ) session.commit() - assert TaskExecutionRepository().next_attempt_for_node(session, run_id, node_id) == 3 + assert TaskExecutionRepository().next_attempt_for_node(session, sample_id, node_id) == 3 diff --git a/ergon_core/tests/unit/runtime/test_walkthrough_smoketest.py b/ergon_core/tests/unit/runtime/test_walkthrough_smoketest.py index b7ddeb763..4c7c16ff8 100644 --- a/ergon_core/tests/unit/runtime/test_walkthrough_smoketest.py +++ b/ergon_core/tests/unit/runtime/test_walkthrough_smoketest.py @@ -30,9 +30,9 @@ ExperimentDefinitionTaskAssignment, ExperimentDefinitionWorker, ) -from ergon_core.core.persistence.graph.models import RunGraphNode -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.tests.unit.runtime._test_workers import EchoSandbox, EchoWorker from pydantic import BaseModel, ConfigDict from sqlalchemy.pool import StaticPool @@ -59,12 +59,12 @@ def _session() -> Session: def _seed_run(session: Session) -> tuple[UUID, UUID]: """Insert a minimal experiment/definition/run with one task; return - (run_id, definition_id).""" + (sample_id, definition_id).""" definition_id = uuid4() instance_id = uuid4() task_id = uuid4() - run_id = uuid4() + sample_id = uuid4() task_json = _SmokeTask( task_slug="root", instance_key="sample-1", @@ -92,42 +92,42 @@ def _seed_run(session: Session) -> tuple[UUID, UUID]: task_payload_json={"problem": "p"}, task_json=task_json, ), - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="test", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ), ] ) session.commit() - return run_id, definition_id + return sample_id, definition_id # ── PR 1 invariant — GREEN today ───────────────────────────────────── def test_prepare_run_populates_task_json_for_every_node() -> None: - """PR 1 invariant: every run_graph_nodes row produced by + """PR 1 invariant: every sample_graph_nodes row produced by initialize_from_definition carries a non-empty task_json snapshot.""" session = _session() - run_id, definition_id = _seed_run(session) + sample_id, definition_id = _seed_run(session) repo = RuntimeGraphRepository() repo.initialize_from_definition( session, - run_id=run_id, + sample_id=sample_id, definition_id=definition_id, initial_node_status="pending", initial_edge_status="pending", meta=MutationMeta(actor="test", reason="smoke"), ) - rows = session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all() + rows = session.exec(select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id)).all() assert rows, "prepare_run produced no nodes" assert all(row.task_json for row in rows), ( @@ -263,12 +263,12 @@ def test_worker_execute_emits_one_evaluate_invocation_per_evaluator() -> None: def test_evaluate_task_run_payload_is_id_only() -> None: """PR 4 invariant: TaskEvaluateRequest has exactly four fields: - run_id, task_id, execution_id, evaluator_index.""" + sample_id, task_id, execution_id, evaluator_index.""" from ergon_core.core.jobs.task.evaluate.contract import TaskEvaluateRequest assert set(TaskEvaluateRequest.model_fields) == { - "run_id", + "sample_id", "task_id", "execution_id", "evaluator_index", @@ -362,19 +362,19 @@ def ctx_factory() -> _SessionContext: monkeypatch.setattr(inspection_module, "get_session", ctx_factory) -def _seed_parent_node(session: Session, *, run_id: UUID) -> RunGraphNode: +def _seed_parent_node(session: Session, *, sample_id: UUID) -> SampleGraphNode: session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=uuid4(), benchmark_type="test", instance_key="sample-1", worker_team_json={}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) - node = RunGraphNode( - run_id=run_id, + node = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug="parent", description="parent task", @@ -389,7 +389,7 @@ def _seed_parent_node(session: Session, *, run_id: UUID) -> RunGraphNode: @pytest.mark.asyncio -async def test_dynamic_spawn_writes_only_to_run_graph_nodes( +async def test_dynamic_spawn_writes_only_to_sample_graph_nodes( monkeypatch: pytest.MonkeyPatch, ) -> None: """Δ.3 / PR 9 invariant: dynamic subtasks are graph-native.""" @@ -404,8 +404,8 @@ async def test_dynamic_spawn_writes_only_to_run_graph_nodes( session = Session(engine) # 2. Seed one parent graph node. - run_id = uuid4() - parent = _seed_parent_node(session, run_id=run_id) + sample_id = uuid4() + parent = _seed_parent_node(session, sample_id=sample_id) # 3. Patch get_session so service writes stay in the test session. _patch_get_session_smoke(monkeypatch, session) @@ -419,7 +419,7 @@ async def test_dynamic_spawn_writes_only_to_run_graph_nodes( ) task_inspect = TaskInspectionService() context = WorkerContext._for_job( - run_id=run_id, + sample_id=sample_id, task_id=parent.task_id, execution_id=uuid4(), definition_id=None, @@ -430,7 +430,7 @@ async def test_dynamic_spawn_writes_only_to_run_graph_nodes( session_factory=management_module.get_session, ) - nodes_before = session.exec(select(RunGraphNode)).all() + nodes_before = session.exec(select(SampleGraphNode)).all() defs_before = session.exec(select(ExperimentDefinitionTask)).all() assert len(nodes_before) == 1 # only the parent @@ -446,18 +446,18 @@ async def test_dynamic_spawn_writes_only_to_run_graph_nodes( ) ) - # 5. Exactly one new run_graph_nodes row (is_dynamic=True); zero new + # 5. Exactly one new sample_graph_nodes row (is_dynamic=True); zero new # experiment_definition_tasks rows. - nodes_after = session.exec(select(RunGraphNode)).all() + nodes_after = session.exec(select(SampleGraphNode)).all() defs_after = session.exec(select(ExperimentDefinitionTask)).all() assert len(nodes_after) == len(nodes_before) + 1 assert len(defs_after) == len(defs_before) == 0 new_node = session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.task_slug == "child", + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.task_slug == "child", ) ).one() assert new_node.is_dynamic is True diff --git a/ergon_core/tests/unit/runtime/test_worker_context_containment.py b/ergon_core/tests/unit/runtime/test_worker_context_containment.py index b8780f031..d05e04105 100644 --- a/ergon_core/tests/unit/runtime/test_worker_context_containment.py +++ b/ergon_core/tests/unit/runtime/test_worker_context_containment.py @@ -4,7 +4,7 @@ PR 9 Tasks 2-3: - ``spawn_task`` round-trips through ``TaskManagementService.spawn_dynamic_task`` - and writes only to ``run_graph_nodes`` (never ``experiment_definition_tasks``). + and writes only to ``sample_graph_nodes`` (never ``experiment_definition_tasks``). - The spawned dynamic node inflates correctly through ``RuntimeGraphRepository.node`` (is_dynamic=True + correct task_slug). - ``cancel_task`` enforces containment via ``_assert_descendant``, @@ -26,14 +26,14 @@ from ergon_core.api.errors import ContainmentViolation from ergon_core.api.worker.context import WorkerContext from ergon_core.api.worker.results import SpawnedTaskHandle -from ergon_core.core.application.runtime.models import RunGraphNodeView +from ergon_core.core.application.runtime.models import SampleGraphNodeView from ergon_core.core.application.runtime.graph_repository import RuntimeGraphRepository from ergon_core.core.application.runtime import inspection as inspection_module from ergon_core.core.application.runtime import management as management_module from ergon_core.core.application.runtime.task_inspection import TaskInspectionService from ergon_core.core.application.runtime.task_management import TaskManagementService from ergon_core.core.persistence.definitions.models import ExperimentDefinitionTask -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.tests.unit.runtime._test_workers import EchoSandbox, EchoWorker from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine, select @@ -75,14 +75,14 @@ def _make_session() -> Session: def _seed_node( session: Session, *, - run_id: UUID, + sample_id: UUID, slug: str, parent_task_id: UUID | None = None, level: int = 0, status: str = "RUNNING", -) -> RunGraphNode: - node = RunGraphNode( - run_id=run_id, +) -> SampleGraphNode: + node = SampleGraphNode( + sample_id=sample_id, instance_key="sample-1", task_slug=slug, description=f"Task {slug}", @@ -126,13 +126,13 @@ def ctx_factory() -> _SessionContext: def _build_context( *, - run_id: UUID, + sample_id: UUID, task_id: UUID, task_mgmt: object, task_inspect: object, ) -> WorkerContext: return WorkerContext._for_job( - run_id=run_id, + sample_id=sample_id, task_id=task_id, execution_id=uuid4(), definition_id=None, @@ -156,8 +156,8 @@ async def test_spawn_task_via_worker_context_does_not_write_definition_row( """spawn_task through the facade writes one run-graph row, zero definition rows.""" session = _make_session() - run_id = uuid4() - parent = _seed_node(session, run_id=run_id, slug="parent") + sample_id = uuid4() + parent = _seed_node(session, sample_id=sample_id, slug="parent") _patch_get_session(monkeypatch, session) monkeypatch.setattr( @@ -169,19 +169,19 @@ async def test_spawn_task_via_worker_context_does_not_write_definition_row( ) task_inspect = TaskInspectionService() context = _build_context( - run_id=run_id, + sample_id=sample_id, task_id=parent.task_id, task_mgmt=task_mgmt, task_inspect=task_inspect, ) - nodes_before = session.exec(select(RunGraphNode)).all() + nodes_before = session.exec(select(SampleGraphNode)).all() defs_before = session.exec(select(ExperimentDefinitionTask)).all() assert len(nodes_before) == 1 # only the parent handle = await context.spawn_task(_make_task()) - nodes_after = session.exec(select(RunGraphNode)).all() + nodes_after = session.exec(select(SampleGraphNode)).all() defs_after = session.exec(select(ExperimentDefinitionTask)).all() # Exactly one new run-graph row, no new definition rows. @@ -189,9 +189,9 @@ async def test_spawn_task_via_worker_context_does_not_write_definition_row( assert len(defs_after) == len(defs_before) == 0 new_node = session.exec( - select(RunGraphNode).where( - RunGraphNode.run_id == run_id, - RunGraphNode.task_slug == "child", + select(SampleGraphNode).where( + SampleGraphNode.sample_id == sample_id, + SampleGraphNode.task_slug == "child", ) ).one() @@ -209,8 +209,8 @@ async def test_spawned_task_inflates_through_graph_repo_node( """The spawned dynamic node round-trips through graph_repo.node as is_dynamic=True.""" session = _make_session() - run_id = uuid4() - parent = _seed_node(session, run_id=run_id, slug="parent") + sample_id = uuid4() + parent = _seed_node(session, sample_id=sample_id, slug="parent") _patch_get_session(monkeypatch, session) monkeypatch.setattr( @@ -222,7 +222,7 @@ async def test_spawned_task_inflates_through_graph_repo_node( ) task_inspect = TaskInspectionService() context = _build_context( - run_id=run_id, + sample_id=sample_id, task_id=parent.task_id, task_mgmt=task_mgmt, task_inspect=task_inspect, @@ -231,9 +231,9 @@ async def test_spawned_task_inflates_through_graph_repo_node( handle = await context.spawn_task(_make_task()) graph_repo = RuntimeGraphRepository() - view = await graph_repo.node(session, run_id=run_id, task_id=handle.task_id) + view = await graph_repo.node(session, sample_id=sample_id, task_id=handle.task_id) - assert isinstance(view, RunGraphNodeView) + assert isinstance(view, SampleGraphNodeView) assert view.is_dynamic is True assert view.task_id == handle.task_id assert view.task.task_slug == "child" @@ -246,16 +246,16 @@ async def test_worker_context_cancel_raises_on_non_descendant( """cancel_task enforces containment: non-descendant → ContainmentViolation.""" session = _make_session() - run_id = uuid4() - root = _seed_node(session, run_id=run_id, slug="root") + sample_id = uuid4() + root = _seed_node(session, sample_id=sample_id, slug="root") child = _seed_node( session, - run_id=run_id, + sample_id=sample_id, slug="child", parent_task_id=root.task_id, level=1, ) - sibling = _seed_node(session, run_id=run_id, slug="sibling") # peer of root, no parent + sibling = _seed_node(session, sample_id=sample_id, slug="sibling") # peer of root, no parent _patch_get_session(monkeypatch, session) # Real inspection service so _assert_descendant queries the actual graph; @@ -266,7 +266,7 @@ async def test_worker_context_cancel_raises_on_non_descendant( task_inspect = TaskInspectionService() context = _build_context( - run_id=run_id, + sample_id=sample_id, task_id=root.task_id, task_mgmt=task_mgmt, task_inspect=task_inspect, @@ -285,5 +285,5 @@ async def test_worker_context_cancel_raises_on_non_descendant( args = task_mgmt.cancel_task.await_args.args assert args[0] is session - assert args[1].run_id == run_id + assert args[1].sample_id == sample_id assert args[1].task_id == child.task_id diff --git a/ergon_core/tests/unit/runtime/test_workflow_service.py b/ergon_core/tests/unit/runtime/test_workflow_service.py index c8f1e06a2..7f44fc3e2 100644 --- a/ergon_core/tests/unit/runtime/test_workflow_service.py +++ b/ergon_core/tests/unit/runtime/test_workflow_service.py @@ -3,18 +3,18 @@ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode from ergon_core.core.persistence.shared.enums import ( - RunResourceKind, - RunStatus, + SampleResourceKind, + SampleStatus, TaskExecutionStatus, ) from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskAttempt, ) -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine, select @@ -32,15 +32,15 @@ def _session() -> Session: def _node( *, - run_id: UUID, + sample_id: UUID, slug: str, description: str | None = None, status: str = "completed", parent_task_id: UUID | None = None, level: int = 0, -) -> RunGraphNode: - return RunGraphNode( - run_id=run_id, +) -> SampleGraphNode: + return SampleGraphNode( + sample_id=sample_id, instance_key="instance", task_slug=slug, description=description or f"Task {slug}", @@ -51,9 +51,9 @@ def _node( ) -def _edge(*, run_id: UUID, source_task_id: UUID, target_task_id: UUID) -> RunGraphEdge: - return RunGraphEdge( - run_id=run_id, +def _edge(*, sample_id: UUID, source_task_id: UUID, target_task_id: UUID) -> SampleGraphEdge: + return SampleGraphEdge( + sample_id=sample_id, source_task_id=source_task_id, target_task_id=target_task_id, status="satisfied", @@ -62,12 +62,12 @@ def _edge(*, run_id: UUID, source_task_id: UUID, target_task_id: UUID) -> RunGra def _execution( *, - run_id: UUID, + sample_id: UUID, task_id: UUID, status: TaskExecutionStatus = TaskExecutionStatus.COMPLETED, -) -> RunTaskExecution: - return RunTaskExecution( - run_id=run_id, +) -> SampleTaskAttempt: + return SampleTaskAttempt( + sample_id=sample_id, task_id=task_id, status=status, final_assistant_message=f"output for {task_id}", @@ -76,16 +76,16 @@ def _execution( def _resource( *, - run_id: UUID, + sample_id: UUID, execution_id: UUID, name: str, path: Path, content: bytes, - kind: RunResourceKind = RunResourceKind.REPORT, -) -> RunResource: + kind: SampleResourceKind = SampleResourceKind.REPORT, +) -> SampleResource: path.write_bytes(content) - return RunResource( - run_id=run_id, + return SampleResource( + sample_id=sample_id, task_execution_id=execution_id, kind=kind.value, name=name, @@ -97,60 +97,60 @@ def _resource( def _run(session: Session) -> UUID: - run_id = uuid4() + sample_id = uuid4() definition_id = uuid4() session.add( - RunRecord( - id=run_id, + SampleRecord( + id=sample_id, definition_id=definition_id, benchmark_type="ci-workflow-service", instance_key="sample-1", worker_team_json={"primary": "test-worker"}, - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) ) - return run_id + return sample_id def test_input_scope_uses_immediate_upstream_resources_only(tmp_path: Path) -> None: session = _session() - run_id = _run(session) - a = _node(run_id=run_id, slug="a") - b = _node(run_id=run_id, slug="b") - c = _node(run_id=run_id, slug="c") + sample_id = _run(session) + a = _node(sample_id=sample_id, slug="a") + b = _node(sample_id=sample_id, slug="b") + c = _node(sample_id=sample_id, slug="c") session.add_all([a, b, c]) session.flush() session.add_all( [ - RunGraphEdge( - run_id=run_id, + SampleGraphEdge( + sample_id=sample_id, source_task_id=a.task_id, target_task_id=b.task_id, status="satisfied", ), - RunGraphEdge( - run_id=run_id, + SampleGraphEdge( + sample_id=sample_id, source_task_id=b.task_id, target_task_id=c.task_id, status="satisfied", ), ] ) - exec_a = _execution(run_id=run_id, task_id=a.task_id) - exec_b = _execution(run_id=run_id, task_id=b.task_id) + exec_a = _execution(sample_id=sample_id, task_id=a.task_id) + exec_b = _execution(sample_id=sample_id, task_id=b.task_id) session.add_all([exec_a, exec_b]) session.flush() session.add_all( [ _resource( - run_id=run_id, + sample_id=sample_id, execution_id=exec_a.id, name="a.txt", path=tmp_path / "a.txt", content=b"a", ), _resource( - run_id=run_id, + sample_id=sample_id, execution_id=exec_b.id, name="b.txt", path=tmp_path / "b.txt", @@ -162,7 +162,7 @@ def test_input_scope_uses_immediate_upstream_resources_only(tmp_path: Path) -> N resources = WorkflowService().list_resources( session, - run_id=run_id, + sample_id=sample_id, task_id=c.task_id, scope="input", ) @@ -172,28 +172,28 @@ def test_input_scope_uses_immediate_upstream_resources_only(tmp_path: Path) -> N def test_visible_scope_stays_inside_current_run(tmp_path: Path) -> None: session = _session() - run_id = _run(session) + sample_id = _run(session) other_run_id = _run(session) - current = _node(run_id=run_id, slug="current") - peer = _node(run_id=run_id, slug="peer") - other = _node(run_id=other_run_id, slug="other") + current = _node(sample_id=sample_id, slug="current") + peer = _node(sample_id=sample_id, slug="peer") + other = _node(sample_id=other_run_id, slug="other") session.add_all([current, peer, other]) session.flush() - peer_exec = _execution(run_id=run_id, task_id=peer.task_id) - other_exec = _execution(run_id=other_run_id, task_id=other.task_id) + peer_exec = _execution(sample_id=sample_id, task_id=peer.task_id) + other_exec = _execution(sample_id=other_run_id, task_id=other.task_id) session.add_all([peer_exec, other_exec]) session.flush() session.add_all( [ _resource( - run_id=run_id, + sample_id=sample_id, execution_id=peer_exec.id, name="peer.txt", path=tmp_path / "peer.txt", content=b"peer", ), _resource( - run_id=other_run_id, + sample_id=other_run_id, execution_id=other_exec.id, name="other.txt", path=tmp_path / "other.txt", @@ -205,7 +205,7 @@ def test_visible_scope_stays_inside_current_run(tmp_path: Path) -> None: resources = WorkflowService().list_resources( session, - run_id=run_id, + sample_id=sample_id, task_id=current.task_id, scope="visible", ) @@ -216,24 +216,24 @@ def test_visible_scope_stays_inside_current_run(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_materialize_resource_creates_current_task_owned_copy(tmp_path: Path) -> None: session = _session() - run_id = _run(session) - producer = _node(run_id=run_id, slug="producer") - consumer = _node(run_id=run_id, slug="consumer") + sample_id = _run(session) + producer = _node(sample_id=sample_id, slug="producer") + consumer = _node(sample_id=sample_id, slug="consumer") session.add_all([producer, consumer]) session.flush() - producer_exec = _execution(run_id=run_id, task_id=producer.task_id) + producer_exec = _execution(sample_id=sample_id, task_id=producer.task_id) consumer_exec = _execution( - run_id=run_id, task_id=consumer.task_id, status=TaskExecutionStatus.RUNNING + sample_id=sample_id, task_id=consumer.task_id, status=TaskExecutionStatus.RUNNING ) session.add_all([producer_exec, consumer_exec]) session.flush() source = _resource( - run_id=run_id, + sample_id=sample_id, execution_id=producer_exec.id, name="paper.pdf", path=tmp_path / "paper.pdf", content=b"paper", - kind=RunResourceKind.REPORT, + kind=SampleResourceKind.REPORT, ) session.add(source) session.commit() @@ -249,7 +249,7 @@ async def upload_file(self, task_id: UUID, local_path: str, sandbox_path: str) - sandbox_manager_factory=lambda _benchmark_type: manager ).materialize_resource( session, - run_id=run_id, + sample_id=sample_id, current_task_id=consumer.task_id, current_execution_id=consumer_exec.id, sandbox_task_key=consumer.task_id, @@ -260,13 +260,13 @@ async def upload_file(self, task_id: UUID, local_path: str, sandbox_path: str) - ) copy = session.exec( - select(RunResource).where(RunResource.id == result.copied_resource_id) + select(SampleResource).where(SampleResource.id == result.copied_resource_id) ).one() - original = session.get(RunResource, source.id) + original = session.get(SampleResource, source.id) assert copy.id != source.id assert copy.task_execution_id == consumer_exec.id - assert copy.kind == RunResourceKind.IMPORT.value + assert copy.kind == SampleResourceKind.IMPORT.value assert copy.name == "paper (copy).pdf" assert copy.file_path == source.file_path assert copy.content_hash == source.content_hash @@ -283,19 +283,19 @@ async def test_materialize_resource_dry_run_keeps_copy_name_for_explicit_destina tmp_path: Path, ) -> None: session = _session() - run_id = _run(session) - producer = _node(run_id=run_id, slug="producer") - consumer = _node(run_id=run_id, slug="consumer") + sample_id = _run(session) + producer = _node(sample_id=sample_id, slug="producer") + consumer = _node(sample_id=sample_id, slug="consumer") session.add_all([producer, consumer]) session.flush() - producer_exec = _execution(run_id=run_id, task_id=producer.task_id) + producer_exec = _execution(sample_id=sample_id, task_id=producer.task_id) consumer_exec = _execution( - run_id=run_id, task_id=consumer.task_id, status=TaskExecutionStatus.RUNNING + sample_id=sample_id, task_id=consumer.task_id, status=TaskExecutionStatus.RUNNING ) session.add_all([producer_exec, consumer_exec]) session.flush() source = _resource( - run_id=run_id, + sample_id=sample_id, execution_id=producer_exec.id, name="paper.pdf", path=tmp_path / "paper.pdf", @@ -306,7 +306,7 @@ async def test_materialize_resource_dry_run_keeps_copy_name_for_explicit_destina result = await WorkflowService().materialize_resource( session, - run_id=run_id, + sample_id=sample_id, current_task_id=consumer.task_id, current_execution_id=consumer_exec.id, sandbox_task_key=consumer.task_id, @@ -322,15 +322,15 @@ async def test_materialize_resource_dry_run_keeps_copy_name_for_explicit_destina def test_resource_location_describes_producer_and_workspace_destination(tmp_path: Path) -> None: session = _session() - run_id = _run(session) - producer = _node(run_id=run_id, slug="producer") + sample_id = _run(session) + producer = _node(sample_id=sample_id, slug="producer") session.add(producer) session.flush() - producer_exec = _execution(run_id=run_id, task_id=producer.task_id) + producer_exec = _execution(sample_id=sample_id, task_id=producer.task_id) session.add(producer_exec) session.flush() source = _resource( - run_id=run_id, + sample_id=sample_id, execution_id=producer_exec.id, name="paper.pdf", path=tmp_path / "paper.pdf", @@ -341,7 +341,7 @@ def test_resource_location_describes_producer_and_workspace_destination(tmp_path location = WorkflowService().get_resource_location( session, - run_id=run_id, + sample_id=sample_id, resource_id=source.id, ) @@ -353,33 +353,33 @@ def test_resource_location_describes_producer_and_workspace_destination(tmp_path def test_task_workspace_reports_latest_execution_and_resources(tmp_path: Path) -> None: session = _session() - run_id = _run(session) - current = _node(run_id=run_id, slug="current", status="running") - upstream = _node(run_id=run_id, slug="upstream") + sample_id = _run(session) + current = _node(sample_id=sample_id, slug="current", status="running") + upstream = _node(sample_id=sample_id, slug="upstream") session.add_all([current, upstream]) session.flush() current_exec = _execution( - run_id=run_id, + sample_id=sample_id, task_id=current.task_id, status=TaskExecutionStatus.RUNNING, ) - upstream_exec = _execution(run_id=run_id, task_id=upstream.task_id) + upstream_exec = _execution(sample_id=sample_id, task_id=upstream.task_id) session.add_all([current_exec, upstream_exec]) session.flush() session.add( - _edge(run_id=run_id, source_task_id=upstream.task_id, target_task_id=current.task_id) + _edge(sample_id=sample_id, source_task_id=upstream.task_id, target_task_id=current.task_id) ) session.add_all( [ _resource( - run_id=run_id, + sample_id=sample_id, execution_id=current_exec.id, name="own.txt", path=tmp_path / "own.txt", content=b"own", ), _resource( - run_id=run_id, + sample_id=sample_id, execution_id=upstream_exec.id, name="input.txt", path=tmp_path / "input.txt", @@ -391,7 +391,7 @@ def test_task_workspace_reports_latest_execution_and_resources(tmp_path: Path) - workspace = WorkflowService().get_task_workspace( session, - run_id=run_id, + sample_id=sample_id, task_id=current.task_id, ) @@ -407,21 +407,21 @@ async def test_materialize_resource_rejects_parent_directory_destination( tmp_path: Path, ) -> None: session = _session() - run_id = _run(session) - producer = _node(run_id=run_id, slug="producer") - consumer = _node(run_id=run_id, slug="consumer") + sample_id = _run(session) + producer = _node(sample_id=sample_id, slug="producer") + consumer = _node(sample_id=sample_id, slug="consumer") session.add_all([producer, consumer]) session.flush() - producer_exec = _execution(run_id=run_id, task_id=producer.task_id) + producer_exec = _execution(sample_id=sample_id, task_id=producer.task_id) consumer_exec = _execution( - run_id=run_id, + sample_id=sample_id, task_id=consumer.task_id, status=TaskExecutionStatus.RUNNING, ) session.add_all([producer_exec, consumer_exec]) session.flush() source = _resource( - run_id=run_id, + sample_id=sample_id, execution_id=producer_exec.id, name="paper.pdf", path=tmp_path / "paper.pdf", @@ -433,7 +433,7 @@ async def test_materialize_resource_rejects_parent_directory_destination( with pytest.raises(ValueError, match="destination must stay inside /workspace"): await WorkflowService().materialize_resource( session, - run_id=run_id, + sample_id=sample_id, current_task_id=consumer.task_id, current_execution_id=consumer_exec.id, sandbox_task_key=consumer.task_id, @@ -447,15 +447,15 @@ async def test_materialize_resource_rejects_parent_directory_destination( @pytest.mark.asyncio async def test_add_edge_writes_dependency_between_slugs() -> None: session = _session() - run_id = _run(session) - source = _node(run_id=run_id, slug="source") - target = _node(run_id=run_id, slug="target") + sample_id = _run(session) + source = _node(sample_id=sample_id, slug="source") + target = _node(sample_id=sample_id, slug="target") session.add_all([source, target]) session.commit() result = await WorkflowService().add_edge( session, - run_id=run_id, + sample_id=sample_id, source_task_slug="source", target_task_slug="target", dry_run=False, @@ -463,7 +463,7 @@ async def test_add_edge_writes_dependency_between_slugs() -> None: assert result.action == "add-edge" assert result.edge is not None - edge = session.get(RunGraphEdge, result.edge.edge_id) + edge = session.get(SampleGraphEdge, result.edge.edge_id) assert edge is not None assert edge.source_task_id == source.task_id assert edge.target_task_id == target.task_id @@ -473,20 +473,20 @@ async def test_add_edge_writes_dependency_between_slugs() -> None: @pytest.mark.asyncio async def test_update_task_description_changes_only_description() -> None: session = _session() - run_id = _run(session) - node = _node(run_id=run_id, slug="target", description="Old") + sample_id = _run(session) + node = _node(sample_id=sample_id, slug="target", description="Old") session.add(node) session.commit() result = await WorkflowService().update_task_description( session, - run_id=run_id, + sample_id=sample_id, task_slug="target", description="New description", dry_run=False, ) - refreshed = session.get(RunGraphNode, (run_id, node.task_id)) + refreshed = session.get(SampleGraphNode, (sample_id, node.task_id)) assert refreshed is not None assert refreshed.description == "New description" assert refreshed.task_slug == "target" diff --git a/ergon_core/tests/unit/sandbox/test_ensure_sandbox_idempotence.py b/ergon_core/tests/unit/sandbox/test_ensure_sandbox_idempotence.py index bef51c7ee..9d7458eb1 100644 --- a/ergon_core/tests/unit/sandbox/test_ensure_sandbox_idempotence.py +++ b/ergon_core/tests/unit/sandbox/test_ensure_sandbox_idempotence.py @@ -43,7 +43,7 @@ def _reset_sandbox_singleton() -> None: BaseSandboxManager._instance = None BaseSandboxManager._sandboxes = {} BaseSandboxManager._creation_locks = {} - BaseSandboxManager._run_ids = {} + BaseSandboxManager._sample_ids = {} BaseSandboxManager._display_task_ids = {} BaseSandboxManager._file_registries = {} BaseSandboxManager._created_files_registry = {} @@ -92,9 +92,9 @@ async def test_install_dependencies_runs_exactly_once_on_repeated_create( ) mgr = _ProbeManager() - await mgr.create(sandbox_key=task_id, run_id=task_id, timeout_minutes=30) - await mgr.create(sandbox_key=task_id, run_id=task_id, timeout_minutes=30) - await mgr.create(sandbox_key=task_id, run_id=task_id, timeout_minutes=30) + await mgr.create(sandbox_key=task_id, sample_id=task_id, timeout_minutes=30) + await mgr.create(sandbox_key=task_id, sample_id=task_id, timeout_minutes=30) + await mgr.create(sandbox_key=task_id, sample_id=task_id, timeout_minutes=30) assert _ProbeManager.install_calls == 1, ( "BaseSandboxManager.create must early-return on a cached sandbox " diff --git a/ergon_core/tests/unit/sandbox/test_sandbox_reconnect.py b/ergon_core/tests/unit/sandbox/test_sandbox_reconnect.py index 3cbe59ac1..4e87166e2 100644 --- a/ergon_core/tests/unit/sandbox/test_sandbox_reconnect.py +++ b/ergon_core/tests/unit/sandbox/test_sandbox_reconnect.py @@ -34,7 +34,7 @@ def _reset_singleton() -> None: BaseSandboxManager._instance = None BaseSandboxManager._sandboxes = {} BaseSandboxManager._creation_locks = {} - BaseSandboxManager._run_ids = {} + BaseSandboxManager._sample_ids = {} BaseSandboxManager._display_task_ids = {} BaseSandboxManager._file_registries = {} BaseSandboxManager._created_files_registry = {} diff --git a/ergon_core/tests/unit/state/test_event_schema_phase0.py b/ergon_core/tests/unit/state/test_event_schema_phase0.py index eb57c1812..ab2747fab 100644 --- a/ergon_core/tests/unit/state/test_event_schema_phase0.py +++ b/ergon_core/tests/unit/state/test_event_schema_phase0.py @@ -16,12 +16,12 @@ _TASK_ID_CASES = [ ( "TaskReadyEvent", - lambda: TaskReadyEvent(run_id=uuid4(), definition_id=uuid4(), task_id=uuid4()), + lambda: TaskReadyEvent(sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4()), ), ( "TaskFailedEvent", lambda: TaskFailedEvent( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), @@ -31,7 +31,7 @@ ( "PrepareTaskExecutionCommand", lambda: PrepareTaskExecutionCommand( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), ), @@ -46,7 +46,7 @@ ( "PropagateTaskCompletionCommand", lambda: PropagateTaskCompletionCommand( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), @@ -57,7 +57,7 @@ def test_task_ready_event_requires_task_id() -> None: with pytest.raises(ValueError): - TaskReadyEvent(run_id=uuid4(), definition_id=uuid4()) # type: ignore[call-arg] + TaskReadyEvent(sample_id=uuid4(), definition_id=uuid4()) # type: ignore[call-arg] @pytest.mark.parametrize("label,factory", _TASK_ID_CASES, ids=[c[0] for c in _TASK_ID_CASES]) @@ -73,7 +73,7 @@ def test_task_id_round_trips(label, factory): def test_task_completed_event_uses_task_id() -> None: task_id = uuid4() event = TaskCompletedEvent( - run_id=uuid4(), + sample_id=uuid4(), definition_id=uuid4(), task_id=task_id, execution_id=uuid4(), diff --git a/ergon_core/tests/unit/state/test_sample_runtime_vocabulary.py b/ergon_core/tests/unit/state/test_sample_runtime_vocabulary.py new file mode 100644 index 000000000..d94e3a6bb --- /dev/null +++ b/ergon_core/tests/unit/state/test_sample_runtime_vocabulary.py @@ -0,0 +1,16 @@ +from ergon_core.core.persistence.graph.models import SampleGraphEdge, SampleGraphNode +from ergon_core.core.persistence.telemetry.models import ( + SampleRecord, + SampleResource, + SampleTaskAttempt, + SampleTaskEvaluation, +) + + +def test_runtime_tables_use_sample_vocabulary() -> None: + assert SampleRecord.__tablename__ == "samples" + assert SampleTaskAttempt.__tablename__ == "sample_task_attempts" + assert SampleTaskEvaluation.__tablename__ == "sample_task_evaluations" + assert SampleResource.__tablename__ == "sample_resources" + assert SampleGraphNode.__tablename__ == "sample_graph_nodes" + assert SampleGraphEdge.__tablename__ == "sample_graph_edges" diff --git a/ergon_core/tests/unit/state/test_task_execution_status_enum_members.py b/ergon_core/tests/unit/state/test_task_execution_status_enum_members.py index e9010df8f..dffb3448c 100644 --- a/ergon_core/tests/unit/state/test_task_execution_status_enum_members.py +++ b/ergon_core/tests/unit/state/test_task_execution_status_enum_members.py @@ -25,7 +25,7 @@ Follow-up --------- A real-database equivalent — round-tripping each enum value through a -throwaway ``run_task_executions`` row against a real Postgres fixture — +throwaway ``sample_task_attempts`` row against a real Postgres fixture — is a follow-up. The repo's existing state/unit tests run on SQLite, and SQLite renders ``sa.Enum`` as VARCHAR so it cannot catch this class of drift. diff --git a/ergon_core/tests/unit/state/test_type_invariants.py b/ergon_core/tests/unit/state/test_type_invariants.py index a1e513646..e8ea8c6fc 100644 --- a/ergon_core/tests/unit/state/test_type_invariants.py +++ b/ergon_core/tests/unit/state/test_type_invariants.py @@ -11,18 +11,18 @@ import pytest from ergon_core.core.persistence.graph.models import ( - RunGraphAnnotation, - RunGraphMutation, + SampleGraphAnnotation, + SampleGraphMutation, ) from ergon_core.core.persistence.shared.enums import ( - RunStatus, + SampleStatus, TaskExecutionStatus, ) from ergon_core.core.persistence.telemetry.models import ( RolloutBatch, - RunRecord, - RunResource, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskAttempt, ) from pydantic import ValidationError @@ -35,19 +35,19 @@ "build_fn,field,expected", [ ( - lambda: RunRecord( + lambda: SampleRecord( definition_id=uuid4(), benchmark_type="ci-test", instance_key="sample-1", worker_team_json={"primary": "test-worker"}, - status=RunStatus.PENDING, + status=SampleStatus.PENDING, ), "status", - RunStatus.PENDING, + SampleStatus.PENDING, ), ( - lambda: RunTaskExecution( - run_id=uuid4(), + lambda: SampleTaskAttempt( + sample_id=uuid4(), task_id=uuid4(), node_id=uuid4(), status=TaskExecutionStatus.RUNNING, @@ -56,8 +56,8 @@ TaskExecutionStatus.RUNNING, ), ( - lambda: RunResource( - run_id=uuid4(), + lambda: SampleResource( + sample_id=uuid4(), kind="output", name="test.txt", mime_type="text/plain", @@ -68,8 +68,8 @@ "output", ), ( - lambda: RunGraphMutation( - run_id=uuid4(), + lambda: SampleGraphMutation( + sample_id=uuid4(), sequence=0, mutation_type="node.added", target_type="node", @@ -81,8 +81,8 @@ "node.added", ), ( - lambda: RunGraphMutation( - run_id=uuid4(), + lambda: SampleGraphMutation( + sample_id=uuid4(), sequence=0, mutation_type="edge.added", target_type="edge", @@ -94,8 +94,8 @@ "edge", ), ( - lambda: RunGraphAnnotation( - run_id=uuid4(), + lambda: SampleGraphAnnotation( + sample_id=uuid4(), target_type="node", target_id=uuid4(), namespace="payload", @@ -116,9 +116,9 @@ def test_field_accepts_valid_value(build_fn, field, expected): def test_task_execution_rejects_missing_static_or_dynamic_identity(): with pytest.raises(ValidationError): - RunTaskExecution.model_validate( + SampleTaskAttempt.model_validate( { - "run_id": str(uuid4()), + "sample_id": str(uuid4()), "status": TaskExecutionStatus.RUNNING, } ) @@ -127,7 +127,7 @@ def test_task_execution_rejects_missing_static_or_dynamic_identity(): def test_run_record_uses_definition_identity(): definition_id = uuid4() - run = RunRecord.model_validate( + run = SampleRecord.model_validate( { "definition_id": str(definition_id), "benchmark_type": "ci-benchmark", @@ -147,8 +147,8 @@ def test_run_record_uses_definition_identity(): def test_enum_value_matches_string(): - assert RunStatus.PENDING == "pending" - assert RunStatus.COMPLETED == "completed" + assert SampleStatus.PENDING == "pending" + assert SampleStatus.COMPLETED == "completed" # --------------------------------------------------------------------------- @@ -164,7 +164,7 @@ def test_enum_value_matches_string(): "cls,base_data,invalid_field,invalid_value", [ ( - RunRecord, + SampleRecord, { "definition_id": str(uuid4()), "benchmark_type": "ci-test", @@ -176,15 +176,15 @@ def test_enum_value_matches_string(): "not-a-status", ), ( - RunTaskExecution, - {"run_id": str(uuid4()), "status": "pending"}, + SampleTaskAttempt, + {"sample_id": str(uuid4()), "status": "pending"}, "status", "garbage", ), ( - RunResource, + SampleResource, { - "run_id": str(uuid4()), + "sample_id": str(uuid4()), "name": "test", "mime_type": "text/plain", "file_path": "/x", diff --git a/ergon_core/tests/unit/test_swebench_criterion_no_sandbox.py b/ergon_core/tests/unit/test_swebench_criterion_no_sandbox.py index cc5645030..8d95b177f 100644 --- a/ergon_core/tests/unit/test_swebench_criterion_no_sandbox.py +++ b/ergon_core/tests/unit/test_swebench_criterion_no_sandbox.py @@ -68,7 +68,7 @@ async def test_evaluate_calls_ensure_sandbox_not_spawn_eval_sandbox() -> None: task.sandbox = sandbox ctx = CriterionContext( - run_id=uuid4(), + sample_id=uuid4(), task_id=uuid4(), execution_id=uuid4(), task=task, diff --git a/ergon_ingestion/ergon_ingestion/exports/sharded.py b/ergon_ingestion/ergon_ingestion/exports/sharded.py index 2f0acf7ba..4b76d63e6 100644 --- a/ergon_ingestion/ergon_ingestion/exports/sharded.py +++ b/ergon_ingestion/ergon_ingestion/exports/sharded.py @@ -12,8 +12,8 @@ from sqlmodel import Session, select from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, + SampleRecord, + SampleResource, ) from ergon_ingestion.exports.models import ( DatasetExportManifest, @@ -116,20 +116,22 @@ def export_dataset_from_config(config: ShardedExportConfig) -> DatasetExportMani return export_dataset(session=session, config=config) -def _load_runs(*, session: Session, dataset: str, batch: str) -> list[RunRecord]: +def _load_runs(*, session: Session, dataset: str, batch: str) -> list[SampleRecord]: statement = ( - select(RunRecord) - .where(RunRecord.benchmark_type == f"imported:{dataset}") - .order_by(RunRecord.created_at, RunRecord.id) + select(SampleRecord) + .where(SampleRecord.benchmark_type == f"imported:{dataset}") + .order_by(SampleRecord.created_at, SampleRecord.id) ) runs = list(session.exec(statement)) return [run for run in runs if run.summary_json.get("import_batch_id") == batch] def _export_resources( - session: Session, output_dir: Path, run_id: UUID, config: ShardedExportConfig + session: Session, output_dir: Path, sample_id: UUID, config: ShardedExportConfig ) -> tuple[list[dict[str, object]], int]: - resources = list(session.exec(select(RunResource).where(RunResource.run_id == run_id))) + resources = list( + session.exec(select(SampleResource).where(SampleResource.sample_id == sample_id)) + ) descriptors: list[dict[str, object]] = [] copied_bytes = 0 for resource in resources: @@ -169,12 +171,12 @@ def _export_resources( def _run_row( - config: ShardedExportConfig, run: RunRecord, resources: list[dict[str, object]] + config: ShardedExportConfig, run: SampleRecord, resources: list[dict[str, object]] ) -> dict[str, object]: return { "dataset": config.dataset, "batch": config.batch, - "run_id": str(run.id), + "sample_id": str(run.id), "definition_id": str(run.definition_id), "benchmark_type": run.benchmark_type, "instance_key": run.instance_key, @@ -191,13 +193,13 @@ def _run_row( def _load_reducer_rows( - *, session: Session, config: ShardedExportConfig, runs: list[RunRecord] + *, session: Session, config: ShardedExportConfig, runs: list[SampleRecord] ) -> list[dict[str, object]]: return [] def _load_drop_rows( - *, session: Session, config: ShardedExportConfig, runs: list[RunRecord] + *, session: Session, config: ShardedExportConfig, runs: list[SampleRecord] ) -> list[dict[str, object]]: return [] @@ -250,7 +252,7 @@ def _shard_batches( yield batch -def _malformed_source_records(runs: list[RunRecord]) -> int: +def _malformed_source_records(runs: list[SampleRecord]) -> int: return sum( 1 for run in runs if "source_parse_error" in run.summary_json.get("observed_fields", {}) ) diff --git a/ergon_ingestion/ergon_ingestion/reducers/maestro.py b/ergon_ingestion/ergon_ingestion/reducers/maestro.py index 8acf015b5..4dfd86544 100644 --- a/ergon_ingestion/ergon_ingestion/reducers/maestro.py +++ b/ergon_ingestion/ergon_ingestion/reducers/maestro.py @@ -62,7 +62,7 @@ def coordination_overhead_reducer(spans: Sequence[Mapping[str, Any]]) -> ParsedR implementation_ref="ergon_ingestion.reducers.maestro.coordination_overhead_reducer", fields_read=COORDINATION_FIELDS, aggregation={ - "group_by": ["run_id"], + "group_by": ["sample_id"], "span_count": "count(span_id)", "token_count": "sum(token_count, input_tokens, output_tokens)", "duration_ms": "sum(duration_ms or duration)", diff --git a/ergon_ingestion/ergon_ingestion/sources/agent_reward_bench.py b/ergon_ingestion/ergon_ingestion/sources/agent_reward_bench.py index 9e3ee8a50..bb2da0cbd 100644 --- a/ergon_ingestion/ergon_ingestion/sources/agent_reward_bench.py +++ b/ergon_ingestion/ergon_ingestion/sources/agent_reward_bench.py @@ -249,7 +249,7 @@ def _resources( def _source_run_id(record: Record, *, fallback_id: str) -> str: - value = record.get("trajectory_id") or record.get("source_run_id") or record.get("run_id") + value = record.get("trajectory_id") or record.get("source_run_id") or record.get("sample_id") return fallback_id if value is None else str(value) diff --git a/ergon_ingestion/ergon_ingestion/sources/agentharm.py b/ergon_ingestion/ergon_ingestion/sources/agentharm.py index 399c481e7..07a6b422d 100644 --- a/ergon_ingestion/ergon_ingestion/sources/agentharm.py +++ b/ergon_ingestion/ergon_ingestion/sources/agentharm.py @@ -238,7 +238,7 @@ def _resources( def _source_run_id(record: Record, *, fallback_id: str) -> str: - value = record.get("trajectory_id") or record.get("source_run_id") or record.get("run_id") + value = record.get("trajectory_id") or record.get("source_run_id") or record.get("sample_id") return fallback_id if value is None else str(value) diff --git a/ergon_ingestion/ergon_ingestion/sources/atbench.py b/ergon_ingestion/ergon_ingestion/sources/atbench.py index c16ddd0f4..105b6baa3 100644 --- a/ergon_ingestion/ergon_ingestion/sources/atbench.py +++ b/ergon_ingestion/ergon_ingestion/sources/atbench.py @@ -244,7 +244,7 @@ def _source_run_id(record: Record, *, fallback_id: str) -> str: explicit = ( record.get("trajectory_id") or record.get("source_run_id") - or record.get("run_id") + or record.get("sample_id") or record.get("id") ) if explicit is not None: diff --git a/ergon_ingestion/ergon_ingestion/sources/bfcl.py b/ergon_ingestion/ergon_ingestion/sources/bfcl.py index 7da367364..b9d057094 100644 --- a/ergon_ingestion/ergon_ingestion/sources/bfcl.py +++ b/ergon_ingestion/ergon_ingestion/sources/bfcl.py @@ -197,7 +197,7 @@ def _tool_calls(record: Record) -> object | None: def _source_run_id(record: Record, *, fallback_id: str) -> str: explicit = ( record.get("source_run_id") - or record.get("run_id") + or record.get("sample_id") or record.get("id") or record.get("question_id") ) diff --git a/ergon_ingestion/ergon_ingestion/sources/browsecomp.py b/ergon_ingestion/ergon_ingestion/sources/browsecomp.py index 2b99866eb..afa18933d 100644 --- a/ergon_ingestion/ergon_ingestion/sources/browsecomp.py +++ b/ergon_ingestion/ergon_ingestion/sources/browsecomp.py @@ -150,7 +150,7 @@ def _source_run_id(record: Record, *, fallback_id: str) -> str: explicit = ( record.get("question_id") or record.get("source_run_id") - or record.get("run_id") + or record.get("sample_id") or record.get("id") ) return str(explicit) if explicit is not None else fallback_id diff --git a/ergon_ingestion/ergon_ingestion/sources/copra.py b/ergon_ingestion/ergon_ingestion/sources/copra.py index 22546e358..12e40f32e 100644 --- a/ergon_ingestion/ergon_ingestion/sources/copra.py +++ b/ergon_ingestion/ergon_ingestion/sources/copra.py @@ -79,10 +79,10 @@ def parsed_run_from_copra_record( """Convert one parsed theorem attempt into a database-independent run.""" observed = _normalise_record(record) theorem = str(observed.get("Theorem") or observed.get("theorem") or source_run_id or "unknown") - run_id = source_run_id or theorem + sample_id = source_run_id or theorem resource_payload = _resource_payload(observed) return ParsedRun( - source_run_id=run_id, + source_run_id=sample_id, instance_key=theorem, description=f"COPRA theorem attempt {theorem}", schema_fit_class="artifact-only", diff --git a/ergon_ingestion/ergon_ingestion/sources/debate_mallm.py b/ergon_ingestion/ergon_ingestion/sources/debate_mallm.py index 792e226e2..ab2cdc9d0 100644 --- a/ergon_ingestion/ergon_ingestion/sources/debate_mallm.py +++ b/ergon_ingestion/ergon_ingestion/sources/debate_mallm.py @@ -235,7 +235,7 @@ def _source_run_id(record: Record, *, fallback_id: str) -> str: record.get("debate_id") or record.get("conversation_id") or record.get("source_run_id") - or record.get("run_id") + or record.get("sample_id") ) return fallback_id if value is None else str(value) diff --git a/ergon_ingestion/ergon_ingestion/sources/gap.py b/ergon_ingestion/ergon_ingestion/sources/gap.py index aadb14041..8dfcb2b7f 100644 --- a/ergon_ingestion/ergon_ingestion/sources/gap.py +++ b/ergon_ingestion/ergon_ingestion/sources/gap.py @@ -52,7 +52,7 @@ def iter_runs(self, source: ImportSource) -> Iterator[ParsedRun]: def parse_row(self, row: dict[str, object], *, fallback_id: str) -> ParsedRun: source_id = str( row.get("source_run_id") - or row.get("run_id") + or row.get("sample_id") or row.get("id") or row.get("task_id") or fallback_id diff --git a/ergon_ingestion/ergon_ingestion/sources/generic.py b/ergon_ingestion/ergon_ingestion/sources/generic.py index b7d2621ce..a8142af03 100644 --- a/ergon_ingestion/ergon_ingestion/sources/generic.py +++ b/ergon_ingestion/ergon_ingestion/sources/generic.py @@ -112,7 +112,7 @@ def _run_from_mapping(self, record: dict[str, Any], *, fallback_id: str) -> Pars record.get("source_run_id") or record.get("instance_key") or record.get("id") - or record.get("run_id") + or record.get("sample_id") or fallback_id ) instance_key = str(record.get("instance_key") or source_id) diff --git a/ergon_ingestion/ergon_ingestion/sources/gpqa.py b/ergon_ingestion/ergon_ingestion/sources/gpqa.py index d64e0e9b0..8467f449f 100644 --- a/ergon_ingestion/ergon_ingestion/sources/gpqa.py +++ b/ergon_ingestion/ergon_ingestion/sources/gpqa.py @@ -149,7 +149,7 @@ def _resources(record: Record, generation: object) -> list[ParsedResource]: def _source_run_id(record: Record, *, fallback_id: str) -> str: - explicit = record.get("source_run_id") or record.get("run_id") or record.get("id") + explicit = record.get("source_run_id") or record.get("sample_id") or record.get("id") if explicit is not None: return str(explicit) item_id = record.get("item_id") diff --git a/ergon_ingestion/ergon_ingestion/sources/gsm8k.py b/ergon_ingestion/ergon_ingestion/sources/gsm8k.py index 0d8b29421..e4be17906 100644 --- a/ergon_ingestion/ergon_ingestion/sources/gsm8k.py +++ b/ergon_ingestion/ergon_ingestion/sources/gsm8k.py @@ -164,7 +164,7 @@ def _missing_fields(record: Record) -> list[str]: def _source_run_id(record: Record, *, fallback_id: str) -> str: explicit = ( record.get("source_run_id") - or record.get("run_id") + or record.get("sample_id") or record.get("id") or record.get("item_id") or record.get("question_id") diff --git a/ergon_ingestion/ergon_ingestion/sources/humaneval.py b/ergon_ingestion/ergon_ingestion/sources/humaneval.py index fe133cb37..46eb21a79 100644 --- a/ergon_ingestion/ergon_ingestion/sources/humaneval.py +++ b/ergon_ingestion/ergon_ingestion/sources/humaneval.py @@ -125,7 +125,7 @@ def _planned_runs(path: Path) -> int: def _source_run_id(record: Record, *, fallback_id: str) -> str: - explicit = record.get("source_run_id") or record.get("run_id") or record.get("id") + explicit = record.get("source_run_id") or record.get("sample_id") or record.get("id") if explicit is not None: return str(explicit) return _string_field(record, "task_id") or fallback_id diff --git a/ergon_ingestion/ergon_ingestion/sources/maestro.py b/ergon_ingestion/ergon_ingestion/sources/maestro.py index b514e5443..e2eeafa33 100644 --- a/ergon_ingestion/ergon_ingestion/sources/maestro.py +++ b/ergon_ingestion/ergon_ingestion/sources/maestro.py @@ -22,7 +22,7 @@ class MaestroImporter: - """Parse MAESTRO public span rows into one run per ``run_id``.""" + """Parse MAESTRO public span rows into one run per ``sample_id``.""" info = ImporterInfo( slug="maestro", @@ -46,7 +46,7 @@ def validate(self, source: ImportSource) -> ValidationReport: dataset=self.info.slug, input_path=source.input_path, ok=True, - planned_runs=len({str(row["run_id"]) for row in _read_rows(source.input_path)}), + planned_runs=len({str(row["sample_id"]) for row in _read_rows(source.input_path)}), warnings=[f"{self.info.slug} has conditional export-claim status"], ) @@ -58,24 +58,24 @@ def iter_runs(self, source: ImportSource) -> Iterator[ParsedRun]: def parse_maestro_runs(rows: Iterable[Mapping[str, Any]]) -> list[ParsedRun]: - """Group MAESTRO span rows by ``run_id`` and emit parsed run contracts.""" + """Group MAESTRO span rows by ``sample_id`` and emit parsed run contracts.""" grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) for row in rows: record = dict(row) - grouped[str(record["run_id"])].append(record) + grouped[str(record["sample_id"])].append(record) - return [_run_from_spans(run_id, spans) for run_id, spans in sorted(grouped.items())] + return [_run_from_spans(sample_id, spans) for sample_id, spans in sorted(grouped.items())] -def _run_from_spans(run_id: str, spans: list[dict[str, Any]]) -> ParsedRun: +def _run_from_spans(sample_id: str, spans: list[dict[str, Any]]) -> ParsedRun: outcome = _first_present(spans, "attributes.run.outcome", "run.outcome") judgement = _first_present(spans, "attributes.run.judgement", "run.judgement") trace_ids = sorted({str(span["trace_id"]) for span in spans if span.get("trace_id")}) agent_names = sorted({str(span["agent_name"]) for span in spans if span.get("agent_name")}) coordination = coordination_overhead_reducer(spans) observed_fields = { - "run_id": run_id, + "sample_id": sample_id, "trace_ids": trace_ids, "span_count": len(spans), "agent_names": agent_names, @@ -88,9 +88,9 @@ def _run_from_spans(run_id: str, spans: list[dict[str, Any]]) -> ParsedRun: } return ParsedRun( - source_run_id=run_id, - instance_key=run_id, - description=f"Imported MAESTRO span trace {run_id}", + source_run_id=sample_id, + instance_key=sample_id, + description=f"Imported MAESTRO span trace {sample_id}", schema_fit_class="span-trace", observed_fields=observed_fields, annotations=[ @@ -115,7 +115,7 @@ def _run_from_spans(run_id: str, spans: list[dict[str, Any]]) -> ParsedRun: ], resources=[ ParsedResource( - name=f"{run_id}-spans.json", + name=f"{sample_id}-spans.json", kind="artifact", mime_type="application/json", payload={"spans": spans}, diff --git a/ergon_ingestion/ergon_ingestion/sources/math.py b/ergon_ingestion/ergon_ingestion/sources/math.py index 9bfb8a54d..dfc7f2306 100644 --- a/ergon_ingestion/ergon_ingestion/sources/math.py +++ b/ergon_ingestion/ergon_ingestion/sources/math.py @@ -148,7 +148,7 @@ def _resources(record: Record, completion: object) -> list[ParsedResource]: def _source_run_id(record: Record, *, fallback_id: str) -> str: - explicit = _first_present(record, ["source_run_id", "run_id", "problem_id", "id"]) + explicit = _first_present(record, ["source_run_id", "sample_id", "problem_id", "id"]) if explicit is not None: return str(explicit) return fallback_id diff --git a/ergon_ingestion/ergon_ingestion/sources/miniwob.py b/ergon_ingestion/ergon_ingestion/sources/miniwob.py index e6338103a..e474bede7 100644 --- a/ergon_ingestion/ergon_ingestion/sources/miniwob.py +++ b/ergon_ingestion/ergon_ingestion/sources/miniwob.py @@ -209,7 +209,7 @@ def _source_run_id(record: Record, *, fallback_id: str) -> str: value = ( record.get("episode_id") or record.get("source_run_id") - or record.get("run_id") + or record.get("sample_id") or record.get("id") ) return fallback_id if value is None else str(value) diff --git a/ergon_ingestion/ergon_ingestion/sources/mle_bench.py b/ergon_ingestion/ergon_ingestion/sources/mle_bench.py index b909dfee3..509668fa8 100644 --- a/ergon_ingestion/ergon_ingestion/sources/mle_bench.py +++ b/ergon_ingestion/ergon_ingestion/sources/mle_bench.py @@ -164,7 +164,7 @@ def _resource_kind(value: object) -> str: def _source_run_id(record: Record, competition_id: str, submission_id: str) -> str: - explicit = record.get("source_run_id") or record.get("run_id") or record.get("id") + explicit = record.get("source_run_id") or record.get("sample_id") or record.get("id") if explicit is not None: return str(explicit) return f"mle:{competition_id}:{submission_id}" diff --git a/ergon_ingestion/ergon_ingestion/sources/mmlu.py b/ergon_ingestion/ergon_ingestion/sources/mmlu.py index 2a7e055f4..56e618ff9 100644 --- a/ergon_ingestion/ergon_ingestion/sources/mmlu.py +++ b/ergon_ingestion/ergon_ingestion/sources/mmlu.py @@ -119,7 +119,7 @@ def _planned_runs(path: Path) -> int: def _source_run_id(row: Record, *, model: str, subject: str, item_id: str) -> str: - explicit = _first_present(row, ["source_run_id", "run_id"]) + explicit = _first_present(row, ["source_run_id", "sample_id"]) if explicit is not None: return str(explicit) return f"{model}:{subject}:{item_id}" diff --git a/ergon_ingestion/ergon_ingestion/sources/stabletoolbench.py b/ergon_ingestion/ergon_ingestion/sources/stabletoolbench.py index 72c785403..0a7f4b4e5 100644 --- a/ergon_ingestion/ergon_ingestion/sources/stabletoolbench.py +++ b/ergon_ingestion/ergon_ingestion/sources/stabletoolbench.py @@ -168,7 +168,7 @@ def _source_run_id(record: Record, *, fallback_id: str) -> str: explicit = ( record.get("trajectory_id") or record.get("source_run_id") - or record.get("run_id") + or record.get("sample_id") or record.get("id") ) if explicit is not None: diff --git a/ergon_ingestion/ergon_ingestion/sources/swe_lancer.py b/ergon_ingestion/ergon_ingestion/sources/swe_lancer.py index c030d3989..68fec3f8a 100644 --- a/ergon_ingestion/ergon_ingestion/sources/swe_lancer.py +++ b/ergon_ingestion/ergon_ingestion/sources/swe_lancer.py @@ -174,7 +174,7 @@ def _resources_from_record(record: Record, *, task_prompt: str) -> list[ParsedRe def _source_run_id(record: Record, *, instance_id: str) -> str: - explicit = record.get("source_run_id") or record.get("run_id") or record.get("id") + explicit = record.get("source_run_id") or record.get("sample_id") or record.get("id") if explicit is not None and explicit != "": return str(explicit) return f"swe-lancer:{instance_id}" diff --git a/ergon_ingestion/ergon_ingestion/sources/swe_smith.py b/ergon_ingestion/ergon_ingestion/sources/swe_smith.py index 4ffee8e51..8368d0942 100644 --- a/ergon_ingestion/ergon_ingestion/sources/swe_smith.py +++ b/ergon_ingestion/ergon_ingestion/sources/swe_smith.py @@ -167,7 +167,7 @@ def _resources_from_record(record: Record, *, problem_statement: str) -> list[Pa def _source_run_id(record: Record, *, instance_id: str) -> str: - explicit = record.get("source_run_id") or record.get("run_id") or record.get("id") + explicit = record.get("source_run_id") or record.get("sample_id") or record.get("id") if explicit is not None: return str(explicit) return f"swe-smith:{instance_id}" diff --git a/ergon_ingestion/ergon_ingestion/sources/swebench_cross_harness.py b/ergon_ingestion/ergon_ingestion/sources/swebench_cross_harness.py index 71d91a9cf..c802edc31 100644 --- a/ergon_ingestion/ergon_ingestion/sources/swebench_cross_harness.py +++ b/ergon_ingestion/ergon_ingestion/sources/swebench_cross_harness.py @@ -225,7 +225,7 @@ def _patch_payload_or_path(record: Record) -> tuple[str, Path | None]: def _source_run_id(record: Record, *, instance_id: str, harness: str) -> str: - explicit = record.get("source_run_id") or record.get("run_id") or record.get("id") + explicit = record.get("source_run_id") or record.get("sample_id") or record.get("id") if explicit is not None: return str(explicit) return f"swebench-cross-harness:{instance_id}:{harness}" diff --git a/ergon_ingestion/ergon_ingestion/sources/tau_bench.py b/ergon_ingestion/ergon_ingestion/sources/tau_bench.py index c01af01aa..decc35e2c 100644 --- a/ergon_ingestion/ergon_ingestion/sources/tau_bench.py +++ b/ergon_ingestion/ergon_ingestion/sources/tau_bench.py @@ -182,7 +182,7 @@ def _events_from_messages(record: Record) -> list[ParsedEvent]: def _source_run_id(record: Record, *, fallback_id: str) -> str: - explicit = record.get("source_run_id") or record.get("run_id") or record.get("id") + explicit = record.get("source_run_id") or record.get("sample_id") or record.get("id") if explicit is not None: return str(explicit) domain = _string_field(record, "domain") diff --git a/ergon_ingestion/ergon_ingestion/sources/tot_game24.py b/ergon_ingestion/ergon_ingestion/sources/tot_game24.py index fbc020cdb..a7b030b43 100644 --- a/ergon_ingestion/ergon_ingestion/sources/tot_game24.py +++ b/ergon_ingestion/ergon_ingestion/sources/tot_game24.py @@ -149,7 +149,7 @@ def _event( def _source_run_id(record: dict[str, Any], puzzle_id: str) -> str: - segment_id = record.get("segment_id") or record.get("run_id") or record.get("log_id") + segment_id = record.get("segment_id") or record.get("sample_id") or record.get("log_id") if segment_id is None: return puzzle_id return f"{puzzle_id}:{segment_id}" diff --git a/ergon_ingestion/ergon_ingestion/sources/weblinx.py b/ergon_ingestion/ergon_ingestion/sources/weblinx.py index 2c8d16bd1..9bbcb6e92 100644 --- a/ergon_ingestion/ergon_ingestion/sources/weblinx.py +++ b/ergon_ingestion/ergon_ingestion/sources/weblinx.py @@ -206,7 +206,7 @@ def _external_refs(record: Record, actions: list[Record]) -> list[Record]: def _source_run_id(record: Record, *, fallback_id: str) -> str: - value = record.get("demo_id") or record.get("source_run_id") or record.get("run_id") + value = record.get("demo_id") or record.get("source_run_id") or record.get("sample_id") return fallback_id if value is None else str(value) diff --git a/ergon_ingestion/ergon_ingestion/writers/external_run_writer.py b/ergon_ingestion/ergon_ingestion/writers/external_run_writer.py index c272eb947..4ffc1aa92 100644 --- a/ergon_ingestion/ergon_ingestion/writers/external_run_writer.py +++ b/ergon_ingestion/ergon_ingestion/writers/external_run_writer.py @@ -14,12 +14,16 @@ ExperimentDefinitionInstance, ExperimentDefinitionTask, ) -from ergon_core.core.persistence.graph.models import RunGraphAnnotation, RunGraphNode -from ergon_core.core.persistence.shared.enums import RunResourceKind, RunStatus, TaskExecutionStatus +from ergon_core.core.persistence.graph.models import SampleGraphAnnotation, SampleGraphNode +from ergon_core.core.persistence.shared.enums import ( + SampleResourceKind, + SampleStatus, + TaskExecutionStatus, +) from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskAttempt, ) from ergon_ingestion.models import ImportSource, ParsedResource, ParsedRun @@ -31,7 +35,7 @@ class WriteRunResult(BaseModel): model_config = ConfigDict(frozen=True) - run_id: UUID + sample_id: UUID task_id: UUID task_execution_id: UUID @@ -77,12 +81,12 @@ def write_run(self, parsed: ParsedRun) -> WriteRunResult: self._session.add(task) self._session.flush() - run = RunRecord( + run = SampleRecord( definition_id=definition.id, benchmark_type=f"imported:{self._source.dataset}", instance_key=parsed.instance_key, sample_id=parsed.source_run_id, - status=RunStatus.COMPLETED, + status=SampleStatus.COMPLETED, summary_json={ "imported": True, "source_slug": self._source.dataset, @@ -96,8 +100,8 @@ def write_run(self, parsed: ParsedRun) -> WriteRunResult: self._session.add(run) self._session.flush() - node = RunGraphNode( - run_id=run.id, + node = SampleGraphNode( + sample_id=run.id, task_id=task.id, instance_key=parsed.instance_key, task_slug="imported-root", @@ -107,8 +111,8 @@ def write_run(self, parsed: ParsedRun) -> WriteRunResult: self._session.add(node) self._session.flush() - execution = RunTaskExecution( - run_id=run.id, + execution = SampleTaskAttempt( + sample_id=run.id, task_id=node.task_id, status=TaskExecutionStatus.COMPLETED, output_json={ @@ -122,8 +126,8 @@ def write_run(self, parsed: ParsedRun) -> WriteRunResult: for sequence, annotation in enumerate(parsed.annotations, start=1): self._session.add( - RunGraphAnnotation( - run_id=run.id, + SampleGraphAnnotation( + sample_id=run.id, target_type="node", target_id=node.task_id, namespace=annotation.namespace, @@ -135,7 +139,9 @@ def write_run(self, parsed: ParsedRun) -> WriteRunResult: for resource in parsed.resources: self._session.add(self._resource_row(run.id, execution.id, resource)) - return WriteRunResult(run_id=run.id, task_id=node.task_id, task_execution_id=execution.id) + return WriteRunResult( + sample_id=run.id, task_id=node.task_id, task_execution_id=execution.id + ) def _definition_row(self) -> ExperimentDefinition: if self._definition is None: @@ -158,15 +164,15 @@ def _definition_row(self) -> ExperimentDefinition: def _resource_row( self, - run_id: UUID, + sample_id: UUID, task_execution_id: UUID, resource: ParsedResource, - ) -> RunResource: - path, content_hash, size = self._materialize_resource(run_id, resource) - return RunResource( - run_id=run_id, + ) -> SampleResource: + path, content_hash, size = self._materialize_resource(sample_id, resource) + return SampleResource( + sample_id=sample_id, task_execution_id=task_execution_id, - kind=RunResourceKind(resource.kind).value, + kind=SampleResourceKind(resource.kind).value, name=resource.name, mime_type=resource.mime_type, file_path=str(path), @@ -177,7 +183,7 @@ def _resource_row( def _materialize_resource( self, - run_id: UUID, + sample_id: UUID, resource: ParsedResource, ) -> tuple[Path, str, int]: if resource.path is not None: @@ -194,7 +200,7 @@ def _materialize_resource( ).encode() suffix = Path(resource.name).suffix or ".json" content_hash = hashlib.sha256(data).hexdigest() - directory = self._blob_root / str(run_id) + directory = self._blob_root / str(sample_id) directory.mkdir(parents=True, exist_ok=True) path = directory / f"{content_hash}{suffix}" path.write_bytes(data) diff --git a/ergon_ingestion/tests/unit/sources/test_gap.py b/ergon_ingestion/tests/unit/sources/test_gap.py index 33fe7ef9b..c06ccafe7 100644 --- a/ergon_ingestion/tests/unit/sources/test_gap.py +++ b/ergon_ingestion/tests/unit/sources/test_gap.py @@ -11,7 +11,7 @@ def test_gap_importer_parses_row_record_with_safety_reducers(tmp_path: Path) -> gap_module = _load_gap_module() source_path = tmp_path / "gap.jsonl" row = { - "run_id": "gap-row-1", + "sample_id": "gap-row-1", "task_id": "harmful-tool-use-001", "prompt": "Open the calculator, but do not access the shell.", "answer": "I cannot perform that unsafe tool call.", diff --git a/ergon_ingestion/tests/unit/sources/test_maestro.py b/ergon_ingestion/tests/unit/sources/test_maestro.py index 45db58256..2374d357d 100644 --- a/ergon_ingestion/tests/unit/sources/test_maestro.py +++ b/ergon_ingestion/tests/unit/sources/test_maestro.py @@ -71,7 +71,7 @@ def test_maestro_importer_reads_jsonl_fixture_grouped_by_run(tmp_path: Path) -> def _maestro_rows() -> list[dict]: return [ { - "run_id": "run-alpha", + "sample_id": "run-alpha", "trace_id": "trace-alpha", "span_id": "alpha-root", "parent_span_id": None, @@ -87,7 +87,7 @@ def _maestro_rows() -> list[dict]: }, }, { - "run_id": "run-alpha", + "sample_id": "run-alpha", "trace_id": "trace-alpha", "span_id": "alpha-worker", "parent_span_id": "alpha-root", @@ -99,7 +99,7 @@ def _maestro_rows() -> list[dict]: "attributes": {"coordination.round": 1}, }, { - "run_id": "run-alpha", + "sample_id": "run-alpha", "trace_id": "trace-alpha", "span_id": "alpha-reviewer", "parent_span_id": "alpha-root", @@ -111,7 +111,7 @@ def _maestro_rows() -> list[dict]: "attributes": {"coordination.round": 1}, }, { - "run_id": "run-beta", + "sample_id": "run-beta", "trace_id": "trace-beta", "span_id": "beta-root", "parent_span_id": None, @@ -125,7 +125,7 @@ def _maestro_rows() -> list[dict]: }, }, { - "run_id": "run-beta", + "sample_id": "run-beta", "trace_id": "trace-beta", "span_id": "beta-worker", "parent_span_id": "beta-root", diff --git a/ergon_ingestion/tests/unit/test_external_run_writer.py b/ergon_ingestion/tests/unit/test_external_run_writer.py index 51fd61e97..7244aeb00 100644 --- a/ergon_ingestion/tests/unit/test_external_run_writer.py +++ b/ergon_ingestion/tests/unit/test_external_run_writer.py @@ -5,11 +5,11 @@ from sqlmodel import SQLModel, Session, create_engine, select from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphAnnotation, RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphAnnotation, SampleGraphNode from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskAttempt, ) from ergon_ingestion.models import ( ImportSource, @@ -74,17 +74,18 @@ def test_external_run_writer_persists_import_spine_without_reducer_tables(tmp_pa result = writer.write_run(parsed) session.commit() - assert result.run_id is not None + assert result.sample_id is not None definition = session.exec(select(ExperimentDefinition)).one() assert definition.benchmark_type == "imported:gap" assert definition.metadata_json["import_batch_id"] == "paper-rq1-v1" - assert session.exec(select(RunRecord)).one().instance_key == "gap-row-1" - assert session.exec(select(RunGraphNode)).one().task_slug == "imported-root" + assert session.exec(select(SampleRecord)).one().instance_key == "gap-row-1" + assert session.exec(select(SampleGraphNode)).one().task_slug == "imported-root" assert ( - session.exec(select(RunTaskExecution)).one().output_json["source_run_id"] == "gap-row-1" + session.exec(select(SampleTaskAttempt)).one().output_json["source_run_id"] + == "gap-row-1" ) - assert session.exec(select(RunGraphAnnotation)).one().namespace == "gap.labels" - assert session.exec(select(RunResource)).one().name == "source-row.json" + assert session.exec(select(SampleGraphAnnotation)).one().namespace == "gap.labels" + assert session.exec(select(SampleResource)).one().name == "source-row.json" def test_external_run_writer_sanitizes_non_finite_json_values(tmp_path: Path) -> None: @@ -116,7 +117,7 @@ def test_external_run_writer_sanitizes_non_finite_json_values(tmp_path: Path) -> writer.write_run(parsed) session.commit() - run = session.exec(select(RunRecord)).one() + run = session.exec(select(SampleRecord)).one() assert run.summary_json["observed_fields"]["thinking_budget"] is None assert run.summary_json["observed_fields"]["nested"]["temperature"] is None @@ -150,8 +151,8 @@ def test_external_run_writer_materializes_numpy_array_payloads(tmp_path: Path) - writer.write_run(parsed) session.commit() - run = session.exec(select(RunRecord)).one() - resource = session.exec(select(RunResource)).one() + run = session.exec(select(SampleRecord)).one() + resource = session.exec(select(SampleResource)).one() assert run.summary_json["observed_fields"]["scores"] == [1.0, None] assert resource.file_path.endswith(".json") @@ -197,8 +198,8 @@ def test_external_run_writer_compacts_oversized_db_metadata(tmp_path: Path) -> N writer.write_run(parsed) session.commit() - run = session.exec(select(RunRecord)).one() - resource = session.exec(select(RunResource)).one() + run = session.exec(select(SampleRecord)).one() + resource = session.exec(select(SampleResource)).one() assert run.summary_json["observed_fields"]["trajectory_id"] == "agent-reward-large" assert run.summary_json["observed_fields"]["process_trace"]["_ergon_compacted"] is True assert Path(resource.file_path).read_text().count("x") == 4_500_000 diff --git a/ergon_ingestion/tests/unit/test_sharded_export.py b/ergon_ingestion/tests/unit/test_sharded_export.py index 6de268fcd..e97f92560 100644 --- a/ergon_ingestion/tests/unit/test_sharded_export.py +++ b/ergon_ingestion/tests/unit/test_sharded_export.py @@ -51,7 +51,7 @@ def test_sharded_export_writes_parquet_manifest_state_and_resources(tmp_path: Pa assert set(run_rows[0]) >= { "dataset", "batch", - "run_id", + "sample_id", "sample_id", "instance_key", "observed_fields_json", diff --git a/examples/README.md b/examples/README.md index 945df0ae4..c6dca9195 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,4 +7,4 @@ import shared example helpers and Ergon packages without mutating `sys.path`. Start with [`getting_started/`](getting_started/). Those examples are intentionally small, but they exercise the same object-bound APIs used by larger benchmarks: configure a benchmark object, bind workers and sandboxes, persist the definition, -launch a run, and inspect the resulting artifacts. +launch a sample, and inspect the resulting artifacts. diff --git a/examples/getting_started/01_minif2f_local_llamacpp/README.md b/examples/getting_started/01_minif2f_local_llamacpp/README.md index d2e0d0359..2a4bd4085 100644 --- a/examples/getting_started/01_minif2f_local_llamacpp/README.md +++ b/examples/getting_started/01_minif2f_local_llamacpp/README.md @@ -25,8 +25,8 @@ If `llama-server` is installed somewhere else, pass its path with From the repository root, pass a Hugging Face GGUF reference in `:` form. The example downloads the file into the model -cache, starts `llama-server`, waits for `/v1/models`, launches the MiniF2F run, -and cleans up the model server when the run command exits. +cache, starts `llama-server`, waits for `/v1/models`, launches the MiniF2F sample, +and cleans up the model server when the example command exits. ```bash uv run ergon examples run minif2f-local-llamacpp \ @@ -115,12 +115,12 @@ The script: 4. Builds `MiniF2FBenchmark(limit=3, worker_factory=make_worker)`. 5. Binds `make_minif2f_worker(model="llamacpp:", max_iterations=12)`. 6. Persists the benchmark definition with `persist_benchmark`. -7. Launches a run with `launch_run`. -8. Prints the definition id, run id, model target, and observation commands. +7. Launches a sample with `launch_sample`. +8. Prints the definition id, sample id, model target, and observation commands. MiniF2F is a real theorem-proving benchmark. Local model quality, quantization, -and context length strongly affect proof success. A terminal run with failed -proofs is still an honest first-run outcome: inspect the attempts, proof files, +and context length strongly affect proof success. A terminal sample with failed +proofs is still an honest first-sample outcome: inspect the attempts, proof files, tool calls, and evaluator feedback before changing models or iteration limits. ## Manual Readiness Notes @@ -140,9 +140,9 @@ failures rather than successful benchmark launches: - Missing MiniF2F Lean template or E2B provisioning failure: build/pin the template with `uv run ergon benchmark setup minif2f`, then retry. - Model tool-call incompatibility: try a model and prompt configuration that can - use OpenAI-compatible tool calls, or inspect the run artifacts for the failed + use OpenAI-compatible tool calls, or inspect the sample artifacts for the failed attempts. -Do not treat theorem failures as setup failures. A run that launches all three +Do not treat theorem failures as setup failures. A sample that launches all three tasks and records failed proof attempts is still useful evidence about the local model, context length, and iteration budget. diff --git a/examples/getting_started/01_minif2f_local_llamacpp/run.py b/examples/getting_started/01_minif2f_local_llamacpp/run.py index 52f6c6a61..4f9a86f20 100644 --- a/examples/getting_started/01_minif2f_local_llamacpp/run.py +++ b/examples/getting_started/01_minif2f_local_llamacpp/run.py @@ -11,7 +11,7 @@ from ergon_builtins.benchmarks.minif2f.benchmark import MiniF2FBenchmark from ergon_builtins.benchmarks.minif2f.worker_factory import make_minif2f_worker from ergon_core.api.worker import Worker -from ergon_core.core.application.experiments.service import launch_run, persist_benchmark +from ergon_core.core.application.experiments.service import launch_run as launch_sample, persist_benchmark from getting_started._shared.env import ( DEFAULT_LLAMA_CPP_BASE_URL, DEFAULT_MINIF2F_LIMIT, @@ -25,9 +25,9 @@ preflight_llamacpp_and_e2b, ) from getting_started._shared.llamacpp import ManagedLlamaServer, start_llama_server -from getting_started._shared.launch import first_run_id +from getting_started._shared.launch import first_sample_id from getting_started._shared.model_cache import resolve_base_model -from getting_started._shared.observe import cli_status_command, dashboard_run_url +from getting_started._shared.observe import cli_status_command, dashboard_sample_url def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: @@ -70,7 +70,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: default=None, help=( "Local GGUF path or Hugging Face ':' ref. " - "Starts a managed llama.cpp server for this run." + "Starts a managed llama.cpp server for this sample." ), ) parser.add_argument( @@ -173,11 +173,11 @@ def make_worker() -> Worker: try: benchmark = MiniF2FBenchmark(limit=args.limit, worker_factory=make_worker) handle = persist_benchmark(benchmark) - run_result = await launch_run(handle.definition_id) - run_id = first_run_id(run_result) + sample_result = await launch_sample(handle.definition_id) + sample_id = first_sample_id(sample_result) _print_launch_summary( definition_id=handle.definition_id, - run_id=run_id, + sample_id=sample_id, model_target=model_target, limit=args.limit, max_iterations=args.max_iterations, @@ -218,19 +218,19 @@ def _validate_model_routing(args: argparse.Namespace, parser: argparse.ArgumentP def _print_launch_summary( *, definition_id: object, - run_id: UUID, + sample_id: UUID, model_target: str, limit: int, max_iterations: int, ) -> None: - print("MiniF2F llama.cpp run launched") + print("MiniF2F llama.cpp sample launched") print(f"Definition id: {definition_id}") - print(f"Run id: {run_id}") + print(f"Sample id: {sample_id}") print(f"Model target: {model_target}") print(f"Limit: {limit}") print(f"Max iterations: {max_iterations}") - print(f"CLI status: {cli_status_command(run_id)}") - url = dashboard_run_url(run_id) + print(f"CLI status: {cli_status_command(sample_id)}") + url = dashboard_sample_url(sample_id) if url: print(f"Dashboard: {url}") diff --git a/examples/getting_started/_shared/launch.py b/examples/getting_started/_shared/launch.py index 7bc4b50b2..17ddbd42c 100644 --- a/examples/getting_started/_shared/launch.py +++ b/examples/getting_started/_shared/launch.py @@ -3,7 +3,7 @@ from uuid import UUID -def first_run_id(run_result: object) -> UUID: - """Return the first launched run id from an Ergon launch result.""" - run_ids = getattr(run_result, "run_ids") - return run_ids[0] +def first_sample_id(sample_result: object) -> UUID: + """Return the first launched sample id from an Ergon launch result.""" + sample_ids = getattr(sample_result, "sample_ids") + return sample_ids[0] diff --git a/examples/getting_started/_shared/observe.py b/examples/getting_started/_shared/observe.py index 419118bd3..84f653c87 100644 --- a/examples/getting_started/_shared/observe.py +++ b/examples/getting_started/_shared/observe.py @@ -5,15 +5,15 @@ from uuid import UUID -def dashboard_run_url(run_id: UUID, environ: Mapping[str, str] | None = None) -> str | None: - """Build a dashboard run URL when the dashboard base URL is configured.""" +def dashboard_sample_url(sample_id: UUID, environ: Mapping[str, str] | None = None) -> str | None: + """Build a dashboard sample URL when the dashboard base URL is configured.""" source = environ if environ is not None else os.environ base_url = source.get("ERGON_DASHBOARD_URL") if not base_url: return None - return f"{base_url.rstrip('/')}/run/{run_id}" + return f"{base_url.rstrip('/')}/samples/{sample_id}" -def cli_status_command(run_id: UUID) -> str: - """Build the CLI command for checking a run's status.""" - return f"uv run ergon run status {run_id}" +def cli_status_command(sample_id: UUID) -> str: + """Build the CLI command for checking a sample's status.""" + return f"uv run ergon sample status {sample_id}" diff --git a/tests/e2e/_asserts.py b/tests/e2e/_asserts.py index 0f9a60a1c..b270bda1a 100644 --- a/tests/e2e/_asserts.py +++ b/tests/e2e/_asserts.py @@ -1,9 +1,9 @@ """Shared assertion helpers for canonical smoke drivers. -Per-run helpers take a single ``run_id`` and are called in a loop for +Per-run helpers take a single ``sample_id`` and are called in a loop for each experiment-group member. No "at-least-one-passed" fallbacks; each run must pass every check independently. Experiment-group helpers take -the experiment key + run_id list. +the experiment key + sample_id list. See docs/superpowers/plans/test-refactor/02-drivers-and-asserts.md §2 and §10 for the full catalogue. @@ -22,7 +22,7 @@ from uuid import UUID import httpx -from ergon_core.core.views.runs.models import RunTaskDto +from ergon_core.core.views.samples.models import SampleTaskDto from ergon_core.test_support.e2e_read_helpers import ( ResourceSnapshot, first_probe_resource, @@ -54,9 +54,9 @@ # ============================================================================= -def _assert_run_graph(run_id: UUID) -> None: +def _assert_sample_graph(sample_id: UUID) -> None: """Happy path: root + 9 direct children + 2 nested children; all COMPLETED.""" - snapshot = require_run_snapshot(run_id) + snapshot = require_run_snapshot(sample_id) tasks = list(snapshot.tasks.values()) by_slug = {task.name: task for task in tasks} root_tasks = [task for task in tasks if task.level == 0] @@ -80,7 +80,7 @@ def _assert_run_graph(run_id: UUID) -> None: _assert_dag_edges(tasks) -def _assert_dag_edges(leaves: list[RunTaskDto]) -> None: +def _assert_dag_edges(leaves: list[SampleTaskDto]) -> None: """Verify each dependency edge is exposed by the read-service task DTO.""" by_id = {task.id: task for task in leaves} actual_pairs = { @@ -102,9 +102,9 @@ def _assert_dag_edges(leaves: list[RunTaskDto]) -> None: assert not missing, f"missing DAG edges: {missing}" -def _assert_run_resources(run_id: UUID) -> None: +def _assert_sample_resources(sample_id: UUID) -> None: """Exactly 20 task resources: 10 benchmark artifacts + 10 probe_*.json.""" - snapshot = require_run_snapshot(run_id) + snapshot = require_run_snapshot(sample_id) resources = [ resource for task_resources in snapshot.resources_by_task.values() @@ -125,11 +125,11 @@ def _assert_run_resources(run_id: UUID) -> None: ) -def _assert_run_turn_counts(run_id: UUID) -> None: +def _assert_run_turn_counts(sample_id: UUID) -> None: """Parent + recursive ``l_2`` + artifact leaves emit fixed chunk counts. Each smoke context chunk contains one assistant text part, so persistence - emits exactly one ``RunContextEvent`` per chunk. + emits exactly one ``SampleContextEvent`` per chunk. """ leaf_count = len(EXPECTED_SUBTASK_SLUGS) - 1 + len(NESTED_LINE_SLUGS) expected = ( @@ -138,7 +138,7 @@ def _assert_run_turn_counts(run_id: UUID) -> None: + leaf_count * BaseSmokeLeafWorker.LEAF_TURN_COUNT ) # currently 3 + 3 + 10×2 = 26 - snapshot = require_run_snapshot(run_id) + snapshot = require_run_snapshot(sample_id) event_count = sum(len(events) for events in snapshot.context_events_by_task.values()) assert event_count == expected, ( @@ -149,8 +149,8 @@ def _assert_run_turn_counts(run_id: UUID) -> None: ) -def _assert_run_evaluation(run_id: UUID) -> None: - """Exactly 2 root RunTaskEvaluation rows with score 1.0. +def _assert_run_evaluation(sample_id: UUID) -> None: + """Exactly 2 root SampleTaskEvaluation rows with score 1.0. Retries for up to 30 s because the evaluator invocations land asynchronously even though PR 4's ``execute_task`` fanout is @@ -159,7 +159,7 @@ def _assert_run_evaluation(run_id: UUID) -> None: Note on ordering: pre-PR-4 the evaluator was a sibling Inngest function triggered by ``task/completed``, so evaluations were - written strictly after ``RunTaskExecution.completed_at``. PR 4 + written strictly after ``SampleTaskAttempt.completed_at``. PR 4 moved fanout inside ``execute_task`` via ``ctx.group.parallel`` after ``persist_outputs`` returns, so evaluation rows are written *before* ``finalize_success`` stamps ``completed_at``. The @@ -173,7 +173,7 @@ def _assert_run_evaluation(run_id: UUID) -> None: evaluations = [] root_execution = None while time.monotonic() < deadline: - root_execution, evaluations = list_root_execution_and_evaluations(run_id) + root_execution, evaluations = list_root_execution_and_evaluations(sample_id) if len(evaluations) == 2: break time.sleep(2) @@ -182,7 +182,7 @@ def _assert_run_evaluation(run_id: UUID) -> None: assert len(evaluations) == 2, f"expected 2 root task evaluations, got {len(evaluations)}" scores = [evaluation.score for evaluation in evaluations] assert scores == [1.0, 1.0], f"expected two score 1.0 evaluations, got {scores}" - snapshot = require_run_snapshot(run_id) + snapshot = require_run_snapshot(sample_id) assert snapshot.final_score == 1.0 snapshot_evaluations = list(snapshot.evaluations_by_task.values()) assert snapshot_evaluations, "expected run snapshot evaluation DTOs" @@ -201,21 +201,21 @@ def _assert_run_evaluation(run_id: UUID) -> None: # ============================================================================= -def _assert_sandbox_command_wal(run_id: UUID) -> None: +def _assert_sandbox_command_wal(sample_id: UUID) -> None: """Bash commands land as WAL rows via ``PostgresSandboxEventSink``.""" - entries = list_sandbox_command_wal(run_id) + entries = list_sandbox_command_wal(sample_id) probes = [e for e in entries if "wc" in e.command or "probe" in e.command] # Canonical sad-path smokes block l_3 before it starts, so the eight # executed leaves should emit probe commands while l_3 emits none. assert len(probes) >= 8, f"expected ≥8 probe WAL entries, got {len(probes)}" -def _assert_sandbox_lifecycle_events(run_id: UUID) -> None: +def _assert_sandbox_lifecycle_events(sample_id: UUID) -> None: """``sandbox_created`` + ``sandbox_closed`` symmetric per sandbox.""" deadline = time.monotonic() + 30 events = [] while time.monotonic() < deadline: - events = list_sandbox_events(run_id) + events = list_sandbox_events(sample_id) created = {e.sandbox_id for e in events if e.kind == "sandbox_created"} closed = {e.sandbox_id for e in events if e.kind == "sandbox_closed"} if created == closed: @@ -230,9 +230,9 @@ def _assert_sandbox_lifecycle_events(run_id: UUID) -> None: ) -def _assert_thread_messages_ordered(run_id: UUID) -> None: +def _assert_thread_messages_ordered(sample_id: UUID) -> None: """11 completion messages on the ``smoke-completion`` thread.""" - snapshot = require_run_snapshot(run_id) + snapshot = require_run_snapshot(sample_id) threads = [thread for thread in snapshot.threads if thread.topic == "smoke-completion"] assert len(threads) == 1, f"expected 1 smoke-completion thread, got {len(threads)}" msgs = sorted(threads[0].messages, key=lambda msg: msg.sequence_num) @@ -246,7 +246,7 @@ def _assert_thread_messages_ordered(run_id: UUID) -> None: assert all(m.task_execution_id is not None for m in msgs) -def _assert_blob_roundtrip(run_id: UUID) -> None: +def _assert_blob_roundtrip(sample_id: UUID) -> None: """Read one probe JSON artifact from disk; confirm it parses and is byte-stable across two reads. @@ -256,7 +256,7 @@ def _assert_blob_roundtrip(run_id: UUID) -> None: direct ``kind='output'`` rows store container-internal download paths that are not directly accessible from the host-side test process. """ - row = first_probe_resource(run_id) + row = first_probe_resource(sample_id) assert row is not None, "no probe_*.json (kind=report) to round-trip" assert row.content_hash bytes_a = read_resource_bytes(row) @@ -266,18 +266,22 @@ def _assert_blob_roundtrip(run_id: UUID) -> None: assert "exit_code" in parsed, f"probe JSON missing exit_code: {parsed!r}" -def _assert_minif2f_artifacts(run_id: UUID) -> None: +def _assert_minif2f_artifacts(sample_id: UUID) -> None: """Every MiniF2F leaf persists a Lean proof artifact with the smoke theorem.""" - resources = _require_named_resources(run_id, prefix="proof_", suffix=".lean", expected_count=10) + resources = _require_named_resources( + sample_id, prefix="proof_", suffix=".lean", expected_count=10 + ) for resource in resources: text = read_resource_bytes(resource).decode("utf-8") assert "theorem smoke_trivial" in text, f"{resource.name} missing theorem marker" assert ":=" in text, f"{resource.name} missing Lean proof term" -def _assert_swebench_artifacts(run_id: UUID) -> None: +def _assert_swebench_artifacts(sample_id: UUID) -> None: """Every SWE-Bench leaf persists a parseable Python patch with add().""" - resources = _require_named_resources(run_id, prefix="patch_", suffix=".py", expected_count=10) + resources = _require_named_resources( + sample_id, prefix="patch_", suffix=".py", expected_count=10 + ) for resource in resources: source = read_resource_bytes(resource).decode("utf-8") module = ast.parse(source, filename=resource.name) @@ -288,13 +292,13 @@ def _assert_swebench_artifacts(run_id: UUID) -> None: def _require_named_resources( - run_id: UUID, + sample_id: UUID, *, prefix: str, suffix: str, expected_count: int, ) -> list[ResourceSnapshot]: - resources = list_named_resources(run_id, prefix=prefix, suffix=suffix) + resources = list_named_resources(sample_id, prefix=prefix, suffix=suffix) assert len(resources) == expected_count, ( f"expected {expected_count} {prefix}*{suffix} resources, got {len(resources)}" ) @@ -303,15 +307,15 @@ def _require_named_resources( return resources -def _assert_temporal_ordering(run_id: UUID) -> None: +def _assert_temporal_ordering(sample_id: UUID) -> None: """Schedule honours DAG deps: children start no earlier than parents finish. - Uses ``RunTaskExecution.started_at`` / ``completed_at`` via + Uses ``SampleTaskAttempt.started_at`` / ``completed_at`` via ``task_id`` join. Only checks edges whose both endpoints reached at least ``started`` state. Blocked descendants are skipped because they should never have execution timestamps. """ - slug_exec = leaf_execution_timings_by_slug(run_id) + slug_exec = leaf_execution_timings_by_slug(sample_id) def _after(child: str, parents: list[str]) -> None: c_exec = slug_exec.get(child) @@ -342,12 +346,12 @@ def _assert_experiment_membership(experiment: str, run_ids: list[UUID]) -> None: """Runs are visible via the experiment-group test-harness endpoint.""" api_base = os.environ["ERGON_API_BASE_URL"] r = httpx.get( - f"{api_base}/api/__danger__/test-harness/read/experiment/{experiment}/runs", + f"{api_base}/api/__danger__/test-harness/read/experiment/{experiment}/samples", timeout=10.0, ) r.raise_for_status() rows = r.json() - returned = {UUID(row["run_id"]) for row in rows} + returned = {UUID(row["sample_id"]) for row in rows} expected = set(run_ids) assert expected <= returned, f"experiment group missing expected run ids: {expected - returned}" @@ -357,9 +361,9 @@ def _assert_experiment_membership(experiment: str, run_ids: list[UUID]) -> None: # ============================================================================= -def _assert_sadpath_graph_cascade(run_id: UUID) -> None: +def _assert_sadpath_graph_cascade(sample_id: UUID) -> None: """Canonical sad path: parent plans, l_2 fails, l_3 blocks, independent leaves complete.""" - snapshot = require_run_snapshot(run_id) + snapshot = require_run_snapshot(sample_id) tasks = list(snapshot.tasks.values()) leaves = [task for task in tasks if task.level > 0] root_tasks = [task for task in tasks if task.level == 0] @@ -381,13 +385,13 @@ def _assert_sadpath_graph_cascade(run_id: UUID) -> None: ) -def _assert_sadpath_partial_artifact(run_id: UUID) -> None: +def _assert_sadpath_partial_artifact(sample_id: UUID) -> None: """``AlwaysFailSubworker`` writes ``partial_.md`` before raising. - The runtime's persist step must still serialize it as a RunResource.""" + The runtime's persist step must still serialize it as a SampleResource.""" deadline = time.monotonic() + 30 partials: list[ResourceSnapshot] = [] while time.monotonic() < deadline: - partials = list_named_resources(run_id, prefix="partial_", suffix=".md") + partials = list_named_resources(sample_id, prefix="partial_", suffix=".md") if partials: break time.sleep(2) @@ -401,12 +405,12 @@ def _assert_sadpath_partial_artifact(run_id: UUID) -> None: assert body.startswith("# Partial work"), f"partial artifact body unexpected: {body[:80]!r}" -def _assert_sadpath_partial_wal(run_id: UUID) -> None: +def _assert_sadpath_partial_wal(sample_id: UUID) -> None: """Pre-failure ``wc -l partial_*`` command persists as WAL row.""" deadline = time.monotonic() + 30 wc = [] while time.monotonic() < deadline: - entries = list_sandbox_command_wal(run_id) + entries = list_sandbox_command_wal(sample_id) wc = [e for e in entries if "wc -l" in e.command and "partial_" in e.command] if wc: break @@ -417,9 +421,9 @@ def _assert_sadpath_partial_wal(run_id: UUID) -> None: ) -def _assert_sadpath_thread_messages(run_id: UUID) -> None: +def _assert_sadpath_thread_messages(sample_id: UUID) -> None: """Sad path sends messages for the 7 completed leaves only.""" - snapshot = require_run_snapshot(run_id) + snapshot = require_run_snapshot(sample_id) thread = next( (thread for thread in snapshot.threads if thread.topic == "smoke-completion"), None ) @@ -438,9 +442,9 @@ def _assert_sadpath_thread_messages(run_id: UUID) -> None: assert from_slugs == set(EXPECTED_SUBTASK_SLUGS) - {"l_2", "l_3"} -def _assert_sadpath_evaluation(run_id: UUID) -> None: +def _assert_sadpath_evaluation(sample_id: UUID) -> None: """Sad-path run should not be mistaken for a successful run.""" - snapshot = require_run_snapshot(run_id) + snapshot = require_run_snapshot(sample_id) assert snapshot.status == "failed" @@ -449,17 +453,17 @@ def _assert_sadpath_evaluation(run_id: UUID) -> None: # ============================================================================= -async def wait_for_terminal(run_id: UUID, timeout_seconds: int = 270) -> str: +async def wait_for_terminal(sample_id: UUID, timeout_seconds: int = 270) -> str: """Poll the harness read endpoint until the run reaches a terminal state.""" return await wait_for_terminal_status( - run_id, + sample_id, expected_statuses=frozenset({"completed"}), timeout_seconds=timeout_seconds, ) async def wait_for_terminal_status( - run_id: UUID, + sample_id: UUID, *, expected_statuses: frozenset[str], timeout_seconds: int = 270, @@ -470,7 +474,9 @@ async def wait_for_terminal_status( last_state: dict[str, object] | None = None async with httpx.AsyncClient(timeout=10.0) as client: while time.monotonic() < deadline: - r = await client.get(f"{api_base}/api/__danger__/test-harness/read/run/{run_id}/state") + r = await client.get( + f"{api_base}/api/__danger__/test-harness/read/samples/{sample_id}/state" + ) if r.status_code == 200: state = r.json() last_state = state @@ -479,11 +485,11 @@ async def wait_for_terminal_status( return status if status in TERMINAL_STATUSES: raise AssertionError( - f"run {run_id} reached terminal failure status {status!r}:\n" + f"run {sample_id} reached terminal failure status {status!r}:\n" f"{json.dumps(state, indent=2, sort_keys=True)}" ) await asyncio.sleep(2) raise TimeoutError( - f"run {run_id} did not reach terminal status within {timeout_seconds}s:\n" + f"run {sample_id} did not reach terminal status within {timeout_seconds}s:\n" f"{json.dumps(last_state, indent=2, sort_keys=True)}", ) diff --git a/tests/e2e/_read_contracts.py b/tests/e2e/_read_contracts.py index 9b6543ab9..44f1fcfe8 100644 --- a/tests/e2e/_read_contracts.py +++ b/tests/e2e/_read_contracts.py @@ -4,11 +4,13 @@ from uuid import UUID -from ergon_core.core.views.runs.models import RunSnapshotDto -from ergon_core.core.views.runs.service import RunReadService +from ergon_core.core.views.samples.models import SampleSnapshotDto +from ergon_core.core.views.samples.service import SampleSnapshotReadService -def require_run_snapshot(run_id: UUID) -> RunSnapshotDto: - snapshot = RunReadService().build_run_snapshot(run_id) - assert snapshot is not None, f"RunReadService returned no snapshot for run {run_id}" +def require_run_snapshot(sample_id: UUID) -> SampleSnapshotDto: + snapshot = SampleSnapshotReadService().build_snapshot(sample_id) + assert snapshot is not None, ( + f"SampleSnapshotReadService returned no snapshot for sample {sample_id}" + ) return snapshot diff --git a/tests/e2e/_submit.py b/tests/e2e/_submit.py index 6c8676631..82d3f3dd2 100644 --- a/tests/e2e/_submit.py +++ b/tests/e2e/_submit.py @@ -1,7 +1,7 @@ """Experiment-group submission helper for canonical smoke drivers. POSTs ``/api/__danger__/test-harness/write/experiment-runs`` on the api container; returns the -run_ids in the same order as the slots passed in. +sample_ids in the same order as the slots passed in. Tests are a pure black-box client of the stack: they do not import any ergon internals, do not call ``build_experiment`` / ``create_run`` / @@ -70,7 +70,7 @@ async def submit_experiment_runs( model: str = "openai:gpt-4o", timeout: int = 300, # reserved — server-side per-run timeout ) -> list[UUID]: - """Submit one run per slot under ``experiment``; return run_ids in order. + """Submit one sample per slot under ``experiment``; return sample_ids in order. Args: benchmark_slug: e.g. ``"researchrubrics"`` @@ -103,7 +103,7 @@ async def submit_experiment_runs( f"response body:\n{response.text[:4000]}", ) body = response.json() - return [UUID(rid) for rid in body["run_ids"]] + return [UUID(sample_id) for sample_id in body["sample_ids"]] __all__ = ["build_experiment_payload", "smoke_experiment_key", "submit_experiment_runs"] diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index bc5c96df9..1bbc9f5e2 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -143,7 +143,7 @@ def _parse_uuid_line(prefix: str, output: str) -> str: def benchmarked(): """Memoize `run_benchmark` calls by explicit runtime configuration. - The stubbed E2E tests each assert against the *latest* RunRecord; re-running + The stubbed E2E tests each assert against the *latest* SampleRecord; re-running the same benchmark per-test burned ~4× subprocess launches with identical outcomes. This fixture runs each unique config exactly once per session and returns the cached `CompletedProcess`. diff --git a/tests/e2e/reassert.py b/tests/e2e/reassert.py index 7cce5199e..78fc986e8 100755 --- a/tests/e2e/reassert.py +++ b/tests/e2e/reassert.py @@ -1,4 +1,4 @@ -"""Re-run smoke assertions against an already-completed run_id. +"""Re-run smoke assertions against an already-completed sample_id. Lets you iterate on assertion logic (or debug a failing assertion) without re-submitting the whole experiment through E2B — a 60s sandbox run becomes a @@ -43,8 +43,8 @@ from tests.e2e._asserts import ( _assert_blob_roundtrip, _assert_run_evaluation, - _assert_run_graph, - _assert_run_resources, + _assert_sample_graph, + _assert_sample_resources, _assert_run_turn_counts, _assert_sadpath_evaluation, _assert_sadpath_graph_cascade, @@ -58,8 +58,8 @@ ) HAPPY_ASSERTS = [ - ("graph", _assert_run_graph), - ("resources", _assert_run_resources), + ("graph", _assert_sample_graph), + ("resources", _assert_sample_resources), ("turn_counts", _assert_run_turn_counts), ("sandbox_command_wal", _assert_sandbox_command_wal), ("sandbox_lifecycle_events", _assert_sandbox_lifecycle_events), @@ -108,12 +108,12 @@ def main() -> int: failed: list[tuple[str, BaseException]] = [] print( - f"[smoke_reassert] run_id={args.run_id} env={args.env} kind={args.kind} " + f"[smoke_reassert] sample_id={args.sample_id} env={args.env} kind={args.kind} " f"→ {len(asserts)} checks", ) for name, fn in asserts: try: - fn(args.run_id) + fn(args.sample_id) except BaseException as exc: failed.append((name, exc)) print(f" ✗ {name}: {type(exc).__name__}: {exc}") diff --git a/tests/e2e/test_minif2f_smoke.py b/tests/e2e/test_minif2f_smoke.py index 0b1523f28..4e33ee87b 100644 --- a/tests/e2e/test_minif2f_smoke.py +++ b/tests/e2e/test_minif2f_smoke.py @@ -16,8 +16,8 @@ _assert_experiment_membership, _assert_minif2f_artifacts, _assert_run_evaluation, - _assert_run_graph, - _assert_run_resources, + _assert_sample_graph, + _assert_sample_resources, _assert_run_turn_counts, _assert_sadpath_evaluation, _assert_sadpath_graph_cascade, @@ -98,7 +98,7 @@ async def test_smoke_experiment_group(tmp_path: pathlib.Path) -> None: _invoke_playwright( experiment=experiment, experiment_runs=[ - {"run_id": str(rid), "kind": kind} + {"sample_id": str(rid), "kind": kind} for (kind, _, _), rid in zip(smoke_slots, run_ids, strict=True) ], screenshot_dir=screenshot_dir, @@ -106,8 +106,8 @@ async def test_smoke_experiment_group(tmp_path: pathlib.Path) -> None: def _assert_happy_run(rid) -> None: - _assert_run_graph(rid) - _assert_run_resources(rid) + _assert_sample_graph(rid) + _assert_sample_resources(rid) _assert_run_turn_counts(rid) _assert_thread_messages_ordered(rid) _assert_blob_roundtrip(rid) diff --git a/tests/e2e/test_researchrubrics_smoke.py b/tests/e2e/test_researchrubrics_smoke.py index 2d977631e..03d054fec 100644 --- a/tests/e2e/test_researchrubrics_smoke.py +++ b/tests/e2e/test_researchrubrics_smoke.py @@ -27,8 +27,8 @@ _assert_blob_roundtrip, _assert_experiment_membership, _assert_run_evaluation, - _assert_run_graph, - _assert_run_resources, + _assert_sample_graph, + _assert_sample_resources, _assert_run_turn_counts, _assert_sadpath_evaluation, _assert_sadpath_graph_cascade, @@ -110,7 +110,7 @@ async def test_smoke_experiment_group(tmp_path: pathlib.Path) -> None: _invoke_playwright( experiment=experiment, experiment_runs=[ - {"run_id": str(rid), "kind": kind} + {"sample_id": str(rid), "kind": kind} for (kind, _, _), rid in zip(smoke_slots, run_ids, strict=True) ], screenshot_dir=screenshot_dir, @@ -118,8 +118,8 @@ async def test_smoke_experiment_group(tmp_path: pathlib.Path) -> None: def _assert_happy_run(rid) -> None: - _assert_run_graph(rid) - _assert_run_resources(rid) + _assert_sample_graph(rid) + _assert_sample_resources(rid) _assert_run_turn_counts(rid) _assert_thread_messages_ordered(rid) _assert_blob_roundtrip(rid) diff --git a/tests/e2e/test_swebench_smoke.py b/tests/e2e/test_swebench_smoke.py index a323ed589..82c34cec9 100644 --- a/tests/e2e/test_swebench_smoke.py +++ b/tests/e2e/test_swebench_smoke.py @@ -15,8 +15,8 @@ _assert_blob_roundtrip, _assert_experiment_membership, _assert_run_evaluation, - _assert_run_graph, - _assert_run_resources, + _assert_sample_graph, + _assert_sample_resources, _assert_run_turn_counts, _assert_sadpath_evaluation, _assert_sadpath_graph_cascade, @@ -103,7 +103,7 @@ async def test_smoke_experiment_group(tmp_path: pathlib.Path) -> None: _invoke_playwright( experiment=experiment, experiment_runs=[ - {"run_id": str(rid), "kind": kind} + {"sample_id": str(rid), "kind": kind} for (kind, _, _), rid in zip(smoke_slots, run_ids, strict=True) ], screenshot_dir=screenshot_dir, @@ -111,8 +111,8 @@ async def test_smoke_experiment_group(tmp_path: pathlib.Path) -> None: def _assert_happy_run(rid) -> None: - _assert_run_graph(rid) - _assert_run_resources(rid) + _assert_sample_graph(rid) + _assert_sample_resources(rid) _assert_run_turn_counts(rid) _assert_thread_messages_ordered(rid) _assert_blob_roundtrip(rid) diff --git a/tests/examples/test_minif2f_local_llamacpp_example.py b/tests/examples/test_minif2f_local_llamacpp_example.py index bc6fdf4a7..37a6414c3 100644 --- a/tests/examples/test_minif2f_local_llamacpp_example.py +++ b/tests/examples/test_minif2f_local_llamacpp_example.py @@ -325,7 +325,7 @@ async def test_main_persists_and_launches_minif2f_with_local_worker(monkeypatch, module = _load_example_module() observed: dict[str, object] = {} definition_id = UUID("11111111-1111-1111-1111-111111111111") - run_id = UUID("22222222-2222-2222-2222-222222222222") + sample_id = UUID("22222222-2222-2222-2222-222222222222") def fake_preflight(*, base_url: str) -> object: observed["preflight_base_url"] = base_url @@ -360,7 +360,7 @@ def fake_persist_benchmark(benchmark: FakeBenchmark) -> FakeHandle: async def fake_launch_run(persisted_definition_id: UUID) -> FakeRunResult: observed["launched_definition_id"] = persisted_definition_id - return FakeRunResult(run_id) + return FakeRunResult(sample_id) monkeypatch.setattr(module, "preflight_llamacpp_and_e2b", fake_preflight) monkeypatch.setattr(module, "MiniF2FBenchmark", FakeBenchmark) @@ -393,7 +393,7 @@ async def fake_launch_run(persisted_definition_id: UUID) -> FakeRunResult: } output = capsys.readouterr().out assert str(definition_id) in output - assert str(run_id) in output + assert str(sample_id) in output assert "uv run ergon run status 22222222-2222-2222-2222-222222222222" in output assert "http://localhost:3000/run/22222222-2222-2222-2222-222222222222" in output @@ -406,7 +406,7 @@ async def test_main_resolves_base_model_starts_llamacpp_and_cleans_up( module = _load_example_module() observed: dict[str, object] = {} definition_id = UUID("33333333-3333-3333-3333-333333333333") - run_id = UUID("44444444-4444-4444-4444-444444444444") + sample_id = UUID("44444444-4444-4444-4444-444444444444") resolved_model = tmp_path / "model.gguf" resolved_model.write_text("fake model") @@ -442,7 +442,7 @@ def fake_persist_benchmark(benchmark: _ObservedBenchmark) -> FakeHandle: async def fake_launch_run(persisted_definition_id: UUID) -> FakeRunResult: observed["launched_definition_id"] = persisted_definition_id - return FakeRunResult(run_id) + return FakeRunResult(sample_id) monkeypatch.setattr(module, "resolve_base_model", fake_resolve) monkeypatch.setattr(module, "start_llama_server", fake_start) diff --git a/tests/fixtures/smoke_components/criteria/minif2f_smoke.py b/tests/fixtures/smoke_components/criteria/minif2f_smoke.py index 65343b151..86e7f2d13 100644 --- a/tests/fixtures/smoke_components/criteria/minif2f_smoke.py +++ b/tests/fixtures/smoke_components/criteria/minif2f_smoke.py @@ -14,7 +14,7 @@ from ergon_core.api.criterion import CriterionContext from ergon_core.api.errors import CriterionCheckError from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunResource, RunTaskExecution +from ergon_core.core.persistence.telemetry.models import SampleResource, SampleTaskAttempt from tests.fixtures.smoke_components.smoke_base.criterion_base import SmokeCriterionBase from sqlmodel import col, desc, select @@ -32,29 +32,29 @@ async def _verify_env_content(self, context, children, probes) -> None: exec_ids = [ row.id for row in session.exec( - select(RunTaskExecution).where(RunTaskExecution.task_id == child.task_id), + select(SampleTaskAttempt).where(SampleTaskAttempt.task_id == child.task_id), ).all() ] if not exec_ids: raise CriterionCheckError( - f"{child.task_slug}: no RunTaskExecution rows", + f"{child.task_slug}: no SampleTaskAttempt rows", ) resource = session.exec( - select(RunResource) + select(SampleResource) .where( - col(RunResource.task_execution_id).in_(exec_ids), + col(SampleResource.task_execution_id).in_(exec_ids), ) .where( - col(RunResource.name).like("proof_%.lean"), + col(SampleResource.name).like("proof_%.lean"), ) .order_by( - desc(RunResource.created_at), + desc(SampleResource.created_at), ) .limit(1), ).first() if resource is None: raise CriterionCheckError( - f"{child.task_slug}: no proof_*.lean RunResource", + f"{child.task_slug}: no proof_*.lean SampleResource", ) text = Path(resource.file_path).read_bytes().decode("utf-8") if "theorem smoke_trivial" not in text: diff --git a/tests/fixtures/smoke_components/criteria/researchrubrics_smoke.py b/tests/fixtures/smoke_components/criteria/researchrubrics_smoke.py index e593fff98..9bc308526 100644 --- a/tests/fixtures/smoke_components/criteria/researchrubrics_smoke.py +++ b/tests/fixtures/smoke_components/criteria/researchrubrics_smoke.py @@ -22,7 +22,7 @@ from ergon_core.api.criterion import CriterionContext from ergon_core.api.errors import CriterionCheckError from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunResource, RunTaskExecution +from ergon_core.core.persistence.telemetry.models import SampleResource, SampleTaskAttempt from tests.fixtures.smoke_components.smoke_base.criterion_base import SmokeCriterionBase from sqlmodel import col, desc, select @@ -39,29 +39,29 @@ async def _verify_env_content(self, context, children, probes) -> None: exec_ids = [ row.id for row in session.exec( - select(RunTaskExecution).where(RunTaskExecution.task_id == child.task_id), + select(SampleTaskAttempt).where(SampleTaskAttempt.task_id == child.task_id), ).all() ] if not exec_ids: raise CriterionCheckError( - f"{child.task_slug}: no RunTaskExecution rows", + f"{child.task_slug}: no SampleTaskAttempt rows", ) resource = session.exec( - select(RunResource) + select(SampleResource) .where( - col(RunResource.task_execution_id).in_(exec_ids), + col(SampleResource.task_execution_id).in_(exec_ids), ) .where( - col(RunResource.name).like("report_%.md"), + col(SampleResource.name).like("report_%.md"), ) .order_by( - desc(RunResource.created_at), + desc(SampleResource.created_at), ) .limit(1), ).first() if resource is None: raise CriterionCheckError( - f"{child.task_slug}: no report_*.md RunResource", + f"{child.task_slug}: no report_*.md SampleResource", ) body = Path(resource.file_path).read_bytes() if not body.startswith(b"# Research report"): diff --git a/tests/fixtures/smoke_components/criteria/swebench_smoke.py b/tests/fixtures/smoke_components/criteria/swebench_smoke.py index 3739ab1cf..64b9a7a35 100644 --- a/tests/fixtures/smoke_components/criteria/swebench_smoke.py +++ b/tests/fixtures/smoke_components/criteria/swebench_smoke.py @@ -17,7 +17,7 @@ from ergon_core.api.criterion import CriterionContext from ergon_core.api.errors import CriterionCheckError from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunResource, RunTaskExecution +from ergon_core.core.persistence.telemetry.models import SampleResource, SampleTaskAttempt from tests.fixtures.smoke_components.smoke_base.criterion_base import SmokeCriterionBase from sqlmodel import col, desc, select @@ -37,29 +37,29 @@ async def _verify_env_content(self, context, children, probes) -> None: exec_ids = [ row.id for row in session.exec( - select(RunTaskExecution).where(RunTaskExecution.task_id == child.task_id), + select(SampleTaskAttempt).where(SampleTaskAttempt.task_id == child.task_id), ).all() ] if not exec_ids: raise CriterionCheckError( - f"{child.task_slug}: no RunTaskExecution rows", + f"{child.task_slug}: no SampleTaskAttempt rows", ) resource = session.exec( - select(RunResource) + select(SampleResource) .where( - col(RunResource.task_execution_id).in_(exec_ids), + col(SampleResource.task_execution_id).in_(exec_ids), ) .where( - col(RunResource.name).like("patch_%.py"), + col(SampleResource.name).like("patch_%.py"), ) .order_by( - desc(RunResource.created_at), + desc(SampleResource.created_at), ) .limit(1), ).first() if resource is None: raise CriterionCheckError( - f"{child.task_slug}: no patch_*.py RunResource", + f"{child.task_slug}: no patch_*.py SampleResource", ) source = Path(resource.file_path).read_bytes().decode("utf-8") try: diff --git a/tests/fixtures/smoke_components/sandbox.py b/tests/fixtures/smoke_components/sandbox.py index e5d4694bf..dbb576449 100644 --- a/tests/fixtures/smoke_components/sandbox.py +++ b/tests/fixtures/smoke_components/sandbox.py @@ -202,10 +202,10 @@ async def provision(self) -> None: SmokeSandboxManager.set_event_sink(DefaultSandboxManager._event_sink) manager = SmokeSandboxManager() sandbox_key = uuid4() - run_id = uuid4() + sample_id = uuid4() await manager.create( sandbox_key=sandbox_key, - run_id=run_id, + sample_id=sample_id, timeout_minutes=(self.timeout_seconds or 1800) // 60, envs=self.env if self.env else None, display_task_id=sandbox_key, @@ -237,7 +237,7 @@ class SmokeSandboxManager(BaseSandboxManager): async def create( self, sandbox_key: UUID, - run_id: UUID, + sample_id: UUID, timeout_minutes: int = 30, envs: dict[str, str] | None = None, display_task_id: UUID | None = None, @@ -257,12 +257,12 @@ async def create( self._sandbox_ids[sandbox_id] = sandbox_key self._tempdirs[sandbox_key] = tempdir self._ensure_registries(sandbox_key) - self._run_ids[sandbox_key] = run_id + self._sample_ids[sandbox_key] = sample_id self._display_task_ids[sandbox_key] = display_task_id self._sandbox_manager_classes[sandbox_key] = type(self) await self._event_sink.sandbox_created( - run_id=run_id, + sample_id=sample_id, task_id=display_task_id, sandbox_id=sandbox_id, timeout_minutes=timeout_minutes, @@ -294,22 +294,22 @@ async def terminate(self, task_id: UUID, reason: str = "completed") -> None: sandbox.sandbox_id if sandbox is not None else f"{_SMOKE_SANDBOX_PREFIX}{task_id}" ) display_task_id = self._get_display_task_id(task_id) - run_id = self._run_ids.get(task_id) + sample_id = self._sample_ids.get(task_id) self._sandbox_ids.pop(sandbox_id, None) self._file_registries.pop(task_id, None) self._created_files_registry.pop(task_id, None) - self._run_ids.pop(task_id, None) + self._sample_ids.pop(task_id, None) self._display_task_ids.pop(task_id, None) self._sandbox_manager_classes.pop(task_id, None) tempdir = self._tempdirs.pop(task_id, None) if tempdir is not None: tempdir.cleanup() - if run_id is not None: + if sample_id is not None: await self._event_sink.sandbox_closed( task_id=display_task_id, sandbox_id=sandbox_id, reason=reason, - run_id=run_id, + sample_id=sample_id, ) diff --git a/tests/fixtures/smoke_components/smoke_base/criterion_base.py b/tests/fixtures/smoke_components/smoke_base/criterion_base.py index a0a90f61c..8d9cc2a86 100644 --- a/tests/fixtures/smoke_components/smoke_base/criterion_base.py +++ b/tests/fixtures/smoke_components/smoke_base/criterion_base.py @@ -31,10 +31,10 @@ from ergon_core.api.criterion import CriterionContext, CriterionOutcome from ergon_core.api.errors import CriterionCheckError from ergon_core.api.sandbox.runtime import CommandResult -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.application.runtime.status import COMPLETED, NON_AUTONOMOUS_STATUSES from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunResource, RunTaskExecution +from ergon_core.core.persistence.telemetry.models import SampleResource, SampleTaskAttempt from tests.fixtures.smoke_components.smoke_base.constants import EXPECTED_SUBTASK_SLUGS from pydantic import BaseModel from sqlmodel import col, desc, select @@ -115,25 +115,25 @@ async def evaluate(self, context: CriterionContext) -> CriterionOutcome: async def _pull_children( self, context: CriterionContext, - ) -> list[RunGraphNode]: - """Return direct-child ``RunGraphNode`` rows of the parent task. + ) -> list[SampleGraphNode]: + """Return direct-child ``SampleGraphNode`` rows of the parent task. ``context.execution_id`` points at the parent's - ``RunTaskExecution``; ``RunTaskExecution.task_id`` is the parent's + ``SampleTaskAttempt``; ``SampleTaskAttempt.task_id`` is the parent's graph-node id. Direct children are the rows whose ``parent_task_id`` equals that id. """ with get_session() as session: - parent_exec = session.get(RunTaskExecution, context.execution_id) + parent_exec = session.get(SampleTaskAttempt, context.execution_id) if parent_exec is None or parent_exec.task_id is None: raise CriterionCheckError( - f"no RunTaskExecution / task_id for execution_id={context.execution_id}", + f"no SampleTaskAttempt / task_id for execution_id={context.execution_id}", ) children = list( session.exec( - select(RunGraphNode) - .where(RunGraphNode.parent_task_id == parent_exec.task_id) - .order_by(RunGraphNode.task_slug), + select(SampleGraphNode) + .where(SampleGraphNode.parent_task_id == parent_exec.task_id) + .order_by(SampleGraphNode.task_slug), ).all(), ) return children @@ -144,7 +144,7 @@ async def _wait_for_artifact_state( *, timeout_s: float = 180.0, interval_s: float = 2.0, - ) -> tuple[list[RunGraphNode], list[RunGraphNode], dict[UUID, ProbeResult]]: + ) -> tuple[list[SampleGraphNode], list[SampleGraphNode], dict[UUID, ProbeResult]]: deadline = time.monotonic() + timeout_s last_error: CriterionCheckError | None = None @@ -170,8 +170,8 @@ async def _wait_for_artifact_state( async def _artifact_children( self, - children: list[RunGraphNode], - ) -> list[RunGraphNode]: + children: list[SampleGraphNode], + ) -> list[SampleGraphNode]: """Return leaf descendants that should publish probe/artifact resources. The happy smoke path routes direct child ``l_2`` to a recursive worker. @@ -181,9 +181,11 @@ async def _artifact_children( with get_session() as session: nested = list( session.exec( - select(RunGraphNode) - .where(RunGraphNode.parent_task_id.in_([child.task_id for child in children])) # ty: ignore[unresolved-attribute] - .order_by(RunGraphNode.task_slug), + select(SampleGraphNode) + .where( + SampleGraphNode.parent_task_id.in_([child.task_id for child in children]) + ) # ty: ignore[unresolved-attribute] + .order_by(SampleGraphNode.task_slug), ).all(), ) nested_parent_ids = {node.parent_task_id for node in nested} @@ -195,12 +197,12 @@ async def _artifact_children( async def _pull_probe_results( self, context: CriterionContext, - children: list[RunGraphNode], + children: list[SampleGraphNode], ) -> dict[UUID, ProbeResult]: """Return ``{child_task_id: {"exit_code": int, "stdout": str}}``. - For each child, finds its ``RunTaskExecution`` rows, picks the - latest ``RunResource`` whose name begins with ``probe_`` and + For each child, finds its ``SampleTaskAttempt`` rows, picks the + latest ``SampleResource`` whose name begins with ``probe_`` and ends with ``.json``, and parses its blob-stored bytes. """ results: dict[UUID, ProbeResult] = {} @@ -209,31 +211,31 @@ async def _pull_probe_results( exec_ids = [ row.id for row in session.exec( - select(RunTaskExecution).where( - RunTaskExecution.task_id == child.task_id, + select(SampleTaskAttempt).where( + SampleTaskAttempt.task_id == child.task_id, ), ).all() ] if not exec_ids: raise CriterionCheckError( - f"{child.task_slug}: no RunTaskExecution rows for task", + f"{child.task_slug}: no SampleTaskAttempt rows for task", ) resource = session.exec( - select(RunResource) + select(SampleResource) .where( - col(RunResource.task_execution_id).in_(exec_ids), + col(SampleResource.task_execution_id).in_(exec_ids), ) .where( - col(RunResource.name).like("probe_%.json"), + col(SampleResource.name).like("probe_%.json"), ) .order_by( - desc(RunResource.created_at), + desc(SampleResource.created_at), ) .limit(1), ).first() if resource is None: raise CriterionCheckError( - f"{child.task_slug}: no probe_*.json RunResource row", + f"{child.task_slug}: no probe_*.json SampleResource row", ) blob_bytes = Path(resource.file_path).read_bytes() try: @@ -300,7 +302,7 @@ def _check_probes_succeeded( async def _verify_env_content( self, context: CriterionContext, - children: list[RunGraphNode], + children: list[SampleGraphNode], probes: dict[UUID, ProbeResult], ) -> None: """Subclass hook: read artifacts and check env-specific file shape. diff --git a/tests/fixtures/smoke_components/smoke_base/leaf_base.py b/tests/fixtures/smoke_components/smoke_base/leaf_base.py index 9f89377cf..78cc41499 100644 --- a/tests/fixtures/smoke_components/smoke_base/leaf_base.py +++ b/tests/fixtures/smoke_components/smoke_base/leaf_base.py @@ -26,7 +26,7 @@ from ergon_core.api import Task, Worker, WorkerContext, WorkerStreamItem from ergon_core.api.worker import WorkerOutput -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.infrastructure.sandbox.instrumentation import InstrumentedSandbox from ergon_core.core.application.communication.models import CreateMessageRequest @@ -91,7 +91,7 @@ async def execute( sandbox = InstrumentedSandbox( raw_sandbox, SmokeSandboxManager._event_sink, - context.run_id, + context.sample_id, context.task_id or context.execution_id, settings.otel_stdout_stderr_max_length, ) @@ -133,7 +133,7 @@ async def _send_completion_message( - Thread topic: ``"smoke-completion"`` - ``from_agent_id``: ``f"leaf-{task_slug}"`` — looked up from - ``RunGraphNode.task_slug`` by ``context.task_id`` + ``SampleGraphNode.task_slug`` by ``context.task_id`` - ``to_agent_id``: ``"parent"`` - 9 messages per happy run, sequence_num 1..9 per-thread-monotonic - 8 messages per sad run (l_2 suppresses this call; l_3 still runs) @@ -141,7 +141,7 @@ async def _send_completion_message( task_slug = self._lookup_task_slug(context.task_id) await communication_service.save_message( CreateMessageRequest( - run_id=context.run_id, + sample_id=context.sample_id, task_execution_id=context.execution_id, from_agent_id=f"leaf-{task_slug}", to_agent_id="parent", @@ -154,7 +154,7 @@ async def _send_completion_message( @staticmethod def _lookup_task_slug(task_id: UUID | None) -> str: - """Resolve the leaf's ``task_slug`` from its ``RunGraphNode``. + """Resolve the leaf's ``task_slug`` from its ``SampleGraphNode``. ``WorkerContext`` exposes ``task_id`` but not ``task_slug``; the leaf's message needs the slug so observers can identify which @@ -165,5 +165,7 @@ def _lookup_task_slug(task_id: UUID | None) -> str: if task_id is None: return "unknown" with get_session() as session: - node = session.exec(select(RunGraphNode).where(RunGraphNode.task_id == task_id)).first() + node = session.exec( + select(SampleGraphNode).where(SampleGraphNode.task_id == task_id) + ).first() return node.task_slug if node is not None else f"node-{task_id.hex[:8]}" diff --git a/tests/fixtures/smoke_components/smoke_base/recursive.py b/tests/fixtures/smoke_components/smoke_base/recursive.py index e592d3511..735e9841f 100644 --- a/tests/fixtures/smoke_components/smoke_base/recursive.py +++ b/tests/fixtures/smoke_components/smoke_base/recursive.py @@ -12,7 +12,7 @@ from ergon_core.api import Task, Worker, WorkerContext, WorkerStreamItem from ergon_core.api.worker import WorkerOutput -from ergon_core.core.persistence.graph.models import RunGraphNode +from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.shared.types import AssignedWorkerSlug, TaskSlug from ergon_core.core.application.communication.models import CreateMessageRequest @@ -104,7 +104,7 @@ async def _send_recursive_completion_message( task_slug = self._lookup_task_slug(context.task_id) await communication_service.save_message( CreateMessageRequest( - run_id=context.run_id, + sample_id=context.sample_id, task_execution_id=context.execution_id, from_agent_id=f"leaf-{task_slug}", to_agent_id="parent", @@ -118,7 +118,9 @@ def _lookup_task_slug(task_id: UUID | None) -> str: if task_id is None: return "unknown" with get_session() as session: - node = session.exec(select(RunGraphNode).where(RunGraphNode.task_id == task_id)).first() + node = session.exec( + select(SampleGraphNode).where(SampleGraphNode.task_id == task_id) + ).first() return node.task_slug if node is not None else f"node-{task_id.hex[:8]}" diff --git a/tests/fixtures/smoke_components/smoke_base/sadpath.py b/tests/fixtures/smoke_components/smoke_base/sadpath.py index 8bc3375c5..abae4689d 100644 --- a/tests/fixtures/smoke_components/smoke_base/sadpath.py +++ b/tests/fixtures/smoke_components/smoke_base/sadpath.py @@ -24,7 +24,7 @@ async def work(self, task_id: str, sandbox: AsyncSandbox) -> SubworkerResult: ( f"# Partial work {task_id}\n\n" "This content was written before a deliberate failure. If smoke " - "sees this as a RunResource row, partial serialization works.\n" + "sees this as a SampleResource row, partial serialization works.\n" ), ) diff --git a/tests/fixtures/smoke_components/smoke_base/subworker.py b/tests/fixtures/smoke_components/smoke_base/subworker.py index fabfc745a..bb36e9393 100644 --- a/tests/fixtures/smoke_components/smoke_base/subworker.py +++ b/tests/fixtures/smoke_components/smoke_base/subworker.py @@ -9,7 +9,7 @@ 1. Write a deterministic, well-known file into the sandbox under ``/workspace/final_output/`` so the runtime's persist step can - hash it and produce a ``RunResource`` row. + hash it and produce a ``SampleResource`` row. 2. Run a bash probe against it (compile / parse / count lines / etc.) and persist the probe result as a second file (``probe_.json``) that the criterion later reads. diff --git a/tests/fixtures/smoke_components/workers/researchrubrics_smoke.py b/tests/fixtures/smoke_components/workers/researchrubrics_smoke.py index 4c641f3f7..ec8c3453c 100644 --- a/tests/fixtures/smoke_components/workers/researchrubrics_smoke.py +++ b/tests/fixtures/smoke_components/workers/researchrubrics_smoke.py @@ -44,7 +44,7 @@ class ResearchRubricsSubworker: """Writes a deterministic markdown report + runs ``wc -l`` as the probe. Artifacts written to ``/workspace/final_output/`` so the runtime's - persist step produces RunResource rows: + persist step produces SampleResource rows: - ``report_.md`` — markdown content (content check target) - ``probe_.json`` — ``{exit_code, stdout}`` from ``wc -l`` diff --git a/tests/integration/propagation/_helpers.py b/tests/integration/propagation/_helpers.py index a0fc2c97e..ea5b0ba5b 100644 --- a/tests/integration/propagation/_helpers.py +++ b/tests/integration/propagation/_helpers.py @@ -4,11 +4,15 @@ from uuid import UUID from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.application.runtime.status import TERMINAL_STATUSES from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import SampleStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from sqlmodel import Session, select @@ -21,8 +25,8 @@ def poll_until(condition, *, timeout: float = 30, interval: float = 0.5) -> None raise TimeoutError("poll_until timed out") -def get_node(session: Session, task_id: UUID) -> RunGraphNode: - node = session.exec(select(RunGraphNode).where(RunGraphNode.task_id == task_id)).one() +def get_node(session: Session, task_id: UUID) -> SampleGraphNode: + node = session.exec(select(SampleGraphNode).where(SampleGraphNode.task_id == task_id)).one() session.refresh(node) return node @@ -33,9 +37,11 @@ def get_node_status(session: Session, task_id: UUID) -> str: return node.status -def get_wal_entries(session: Session, task_id: UUID) -> list[RunGraphMutation]: +def get_wal_entries(session: Session, task_id: UUID) -> list[SampleGraphMutation]: return list( - session.exec(select(RunGraphMutation).where(RunGraphMutation.target_id == task_id)).all() + session.exec( + select(SampleGraphMutation).where(SampleGraphMutation.target_id == task_id) + ).all() ) @@ -58,9 +64,11 @@ def assert_wal_has_status( ) -def assert_cross_cutting_invariants(session: Session, run_id: UUID) -> None: +def assert_cross_cutting_invariants(session: Session, sample_id: UUID) -> None: """Basic invariants that should hold after any settled state.""" - nodes = list(session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all()) + nodes = list( + session.exec(select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id)).all() + ) for node in nodes: session.refresh(node) entries = get_wal_entries(session, node.task_id) @@ -81,14 +89,14 @@ def make_experiment_definition(session: Session) -> ExperimentDefinition: return defn -def make_run(session: Session, definition_id: UUID) -> RunRecord: - """Create a minimal RunRecord row for test scaffolding.""" - run = RunRecord( +def make_run(session: Session, definition_id: UUID) -> SampleRecord: + """Create a minimal SampleRecord row for test scaffolding.""" + run = SampleRecord( definition_id=definition_id, workflow_definition_id=definition_id, benchmark_type="ci-propagation-test", instance_key="test", - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) session.add(run) session.flush() @@ -98,16 +106,16 @@ def make_run(session: Session, definition_id: UUID) -> RunRecord: def make_node( session: Session, - run_id: UUID, + sample_id: UUID, *, task_slug: str, status: str = "pending", parent_task_id: UUID | None = None, level: int = 0, -) -> RunGraphNode: - """Create a RunGraphNode row for test scaffolding.""" - node = RunGraphNode( - run_id=run_id, +) -> SampleGraphNode: + """Create a SampleGraphNode row for test scaffolding.""" + node = SampleGraphNode( + sample_id=sample_id, instance_key="test", task_slug=task_slug, description=f"Test node: {task_slug}", @@ -123,15 +131,15 @@ def make_node( def make_edge( session: Session, - run_id: UUID, + sample_id: UUID, *, source_task_id: UUID, target_task_id: UUID, status: str = "pending", -) -> RunGraphEdge: - """Create a RunGraphEdge row for test scaffolding.""" - edge = RunGraphEdge( - run_id=run_id, +) -> SampleGraphEdge: + """Create a SampleGraphEdge row for test scaffolding.""" + edge = SampleGraphEdge( + sample_id=sample_id, source_task_id=source_task_id, target_task_id=target_task_id, status=status, @@ -144,27 +152,27 @@ def make_edge( def seed_linear_chain( session: Session, - run_id: UUID, + sample_id: UUID, slugs: list[str], *, first_status: str = "running", rest_status: str = "pending", -) -> list[RunGraphNode]: +) -> list[SampleGraphNode]: """Create a linear chain of nodes A→B→C… with edges between them. The first node defaults to 'running'; all others default to 'pending'. Returns nodes in order [A, B, C, ...]. """ - nodes: list[RunGraphNode] = [] + nodes: list[SampleGraphNode] = [] for i, slug in enumerate(slugs): status = first_status if i == 0 else rest_status - node = make_node(session, run_id, task_slug=slug, status=status) + node = make_node(session, sample_id, task_slug=slug, status=status) nodes.append(node) for i in range(len(nodes) - 1): make_edge( session, - run_id, + sample_id, source_task_id=nodes[i].task_id, target_task_id=nodes[i + 1].task_id, ) diff --git a/tests/integration/propagation/test_propagation_blocked.py b/tests/integration/propagation/test_propagation_blocked.py index 24b4d3d13..257eba58a 100644 --- a/tests/integration/propagation/test_propagation_blocked.py +++ b/tests/integration/propagation/test_propagation_blocked.py @@ -2,15 +2,19 @@ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.application.runtime.status import BLOCKED, CANCELLED from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.application.runtime.models import MutationMeta from ergon_core.core.application.runtime.graph_repository import RuntimeGraphRepository from ergon_core.core.application.runtime.orchestration import PropagateTaskCompletionCommand -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from sqlmodel import select from tests.integration.propagation._helpers import ( @@ -32,18 +36,22 @@ # --------------------------------------------------------------------------- -def _cleanup_run(run_id, defn_id) -> None: # type: ignore[no-untyped-def] +def _cleanup_run(sample_id, defn_id) -> None: # type: ignore[no-untyped-def] """Remove all rows created by a test, in FK-safe order.""" with get_session() as session: for mut in session.exec( - select(RunGraphMutation).where(RunGraphMutation.run_id == run_id) + select(SampleGraphMutation).where(SampleGraphMutation.sample_id == sample_id) ).all(): session.delete(mut) - for edge in session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all(): + for edge in session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all(): session.delete(edge) - for nd in session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all(): + for nd in session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all(): session.delete(nd) - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) if run_row is not None: session.delete(run_row) defn_row = session.get(ExperimentDefinition, defn_id) @@ -62,7 +70,7 @@ async def test_3_failure_cascade_successor_blocked() -> None: """Linear chain A→B→C. B fails. C must become BLOCKED, not CANCELLED. Also asserts: - - RunRecord does not transition to FAILED (the run stays EXECUTING). + - SampleRecord does not transition to FAILED (the run stays EXECUTING). - WAL entry for C records BLOCKED status. """ with get_session() as session: @@ -76,7 +84,7 @@ async def test_3_failure_cascade_successor_blocked() -> None: first_status="completed", rest_status="pending", ) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -89,14 +97,14 @@ async def test_3_failure_cascade_successor_blocked() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=TaskExecutionStatus.COMPLETED, meta=MutationMeta(actor="test:setup", reason="test: A completed"), ) await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_b_id, new_status=TaskExecutionStatus.FAILED, meta=MutationMeta(actor="test:setup", reason="test: B failed"), @@ -107,7 +115,7 @@ async def test_3_failure_cascade_successor_blocked() -> None: svc = WorkflowService() await svc.propagate_failure( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_b_id, execution_id=node_b_id, @@ -121,21 +129,21 @@ async def test_3_failure_cascade_successor_blocked() -> None: # WAL must have a BLOCKED entry for C assert_wal_has_status(session, node_c_id, BLOCKED) - # RunRecord must remain EXECUTING — propagation of a single failure must not flip the run + # SampleRecord must remain EXECUTING — propagation of a single failure must not flip the run # to FAILED while successor nodes are in the BLOCKED (operator-awaiting) state. with get_session() as session: - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) assert run_row is not None - assert run_row.status == RunStatus.EXECUTING, ( - f"RunRecord must remain EXECUTING while blocked successors await operator; " + assert run_row.status == SampleStatus.EXECUTING, ( + f"SampleRecord must remain EXECUTING while blocked successors await operator; " f"got {run_row.status!r}" ) with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) # --------------------------------------------------------------------------- @@ -174,7 +182,7 @@ async def test_7_parent_failure_children_blocked() -> None: make_edge( session, run.id, source_task_id=parent_node.task_id, target_task_id=child_d.task_id ) - run_id = run.id + sample_id = run.id defn_id = defn.id parent_task_id = parent_node.task_id child_a_id = child_a.task_id @@ -188,21 +196,21 @@ async def test_7_parent_failure_children_blocked() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=parent_task_id, new_status=TaskExecutionStatus.FAILED, meta=MutationMeta(actor="test:setup", reason="test: parent failed"), ) await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=child_c_id, new_status=TaskExecutionStatus.RUNNING, meta=MutationMeta(actor="test:setup", reason="test: child-c already running"), ) await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=child_d_id, new_status=TaskExecutionStatus.COMPLETED, meta=MutationMeta(actor="test:setup", reason="test: child-d already completed"), @@ -212,7 +220,7 @@ async def test_7_parent_failure_children_blocked() -> None: svc = WorkflowService() await svc.propagate_failure( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=parent_task_id, execution_id=parent_task_id, @@ -241,20 +249,20 @@ async def test_7_parent_failure_children_blocked() -> None: f"got {child_d_status!r}" ) - # RunRecord must remain EXECUTING — not auto-failed by propagation + # SampleRecord must remain EXECUTING — not auto-failed by propagation with get_session() as session: - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) assert run_row is not None - assert run_row.status == RunStatus.EXECUTING, ( - f"RunRecord must remain EXECUTING while blocked children await operator; " + assert run_row.status == SampleStatus.EXECUTING, ( + f"SampleRecord must remain EXECUTING while blocked children await operator; " f"got {run_row.status!r}" ) with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) # --------------------------------------------------------------------------- @@ -279,7 +287,7 @@ async def test_10_blocked_propagates_transitively() -> None: first_status="running", rest_status="pending", ) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -291,7 +299,7 @@ async def test_10_blocked_propagates_transitively() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=TaskExecutionStatus.FAILED, meta=MutationMeta(actor="test:setup", reason="test: A failed"), @@ -301,7 +309,7 @@ async def test_10_blocked_propagates_transitively() -> None: svc = WorkflowService() await svc.propagate_failure( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_a_id, execution_id=node_a_id, @@ -321,20 +329,20 @@ async def test_10_blocked_propagates_transitively() -> None: ) assert_wal_has_status(session, node_c_id, BLOCKED) - # RunRecord must remain EXECUTING — not auto-failed by propagation + # SampleRecord must remain EXECUTING — not auto-failed by propagation with get_session() as session: - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) assert run_row is not None - assert run_row.status == RunStatus.EXECUTING, ( - f"RunRecord must remain EXECUTING while blocked successors await operator; " + assert run_row.status == SampleStatus.EXECUTING, ( + f"SampleRecord must remain EXECUTING while blocked successors await operator; " f"got {run_row.status!r}" ) with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) # --------------------------------------------------------------------------- @@ -355,7 +363,7 @@ async def test_12_running_successor_not_interrupted() -> None: node_a = make_node(session, run.id, task_slug="task-a", status="failed") node_b = make_node(session, run.id, task_slug="task-b", status="running") make_edge(session, run.id, source_task_id=node_a.task_id, target_task_id=node_b.task_id) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -366,14 +374,14 @@ async def test_12_running_successor_not_interrupted() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=TaskExecutionStatus.FAILED, meta=MutationMeta(actor="test:setup", reason="test: A failed"), ) await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_b_id, new_status=TaskExecutionStatus.RUNNING, meta=MutationMeta(actor="test:setup", reason="test: B already running"), @@ -383,7 +391,7 @@ async def test_12_running_successor_not_interrupted() -> None: svc = WorkflowService() await svc.propagate_failure( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_a_id, execution_id=node_a_id, @@ -398,16 +406,16 @@ async def test_12_running_successor_not_interrupted() -> None: f"propagation must not interrupt a running task. Got {b_status!r}" ) - # RunRecord must remain EXECUTING — B is still running, the run is not over + # SampleRecord must remain EXECUTING — B is still running, the run is not over with get_session() as session: - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) assert run_row is not None - assert run_row.status == RunStatus.EXECUTING, ( - f"RunRecord must remain EXECUTING while B is still running; got {run_row.status!r}" + assert run_row.status == SampleStatus.EXECUTING, ( + f"SampleRecord must remain EXECUTING while B is still running; got {run_row.status!r}" ) with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) diff --git a/tests/integration/propagation/test_propagation_cancel.py b/tests/integration/propagation/test_propagation_cancel.py index bbdb018a1..184022c67 100644 --- a/tests/integration/propagation/test_propagation_cancel.py +++ b/tests/integration/propagation/test_propagation_cancel.py @@ -4,17 +4,21 @@ entry. This is expected to pass with current code — cancellation is already implemented. No xfail needed. -If this were asserting RunRecord status assertions that aren't yet wired, +If this were asserting SampleRecord status assertions that aren't yet wired, it would need xfail; but the simple node-level cancel works today. """ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.application.runtime.status import CANCELLED from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.shared.enums import TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.application.runtime.models import MutationMeta from ergon_core.core.application.runtime.graph_repository import RuntimeGraphRepository from sqlmodel import select @@ -35,17 +39,21 @@ # --------------------------------------------------------------------------- -def _cleanup_run(run_id, defn_id) -> None: # type: ignore[no-untyped-def] +def _cleanup_run(sample_id, defn_id) -> None: # type: ignore[no-untyped-def] with get_session() as session: for mut in session.exec( - select(RunGraphMutation).where(RunGraphMutation.run_id == run_id) + select(SampleGraphMutation).where(SampleGraphMutation.sample_id == sample_id) ).all(): session.delete(mut) - for edge in session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all(): + for edge in session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all(): session.delete(edge) - for nd in session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all(): + for nd in session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all(): session.delete(nd) - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) if run_row is not None: session.delete(run_row) defn_row = session.get(ExperimentDefinition, defn_id) @@ -71,7 +79,7 @@ async def test_6_manager_decision_cancel_pending_node() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="cancel-target", status="pending") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -83,7 +91,7 @@ async def test_6_manager_decision_cancel_pending_node() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=TaskExecutionStatus.PENDING, meta=MutationMeta(actor="test:setup", reason="test setup: node pending"), @@ -94,7 +102,7 @@ async def test_6_manager_decision_cancel_pending_node() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=CANCELLED, meta=MutationMeta( @@ -117,10 +125,10 @@ async def test_6_manager_decision_cancel_pending_node() -> None: ) with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -135,7 +143,7 @@ async def test_6b_cancel_does_not_affect_already_terminal_node() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="already-completed", status="completed") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -147,7 +155,7 @@ async def test_6b_cancel_does_not_affect_already_terminal_node() -> None: # Stamp COMPLETED into WAL await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=TaskExecutionStatus.COMPLETED, meta=MutationMeta(actor="test:setup", reason="test setup: node completed"), @@ -158,7 +166,7 @@ async def test_6b_cancel_does_not_affect_already_terminal_node() -> None: with get_session() as session: applied = await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=CANCELLED, meta=MutationMeta(actor="test:operator", reason="late cancel attempt"), @@ -175,7 +183,7 @@ async def test_6b_cancel_does_not_affect_already_terminal_node() -> None: ) with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) diff --git a/tests/integration/propagation/test_propagation_edge_cases.py b/tests/integration/propagation/test_propagation_edge_cases.py index daf8bd780..df0e707d3 100644 --- a/tests/integration/propagation/test_propagation_edge_cases.py +++ b/tests/integration/propagation/test_propagation_edge_cases.py @@ -6,15 +6,19 @@ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.application.runtime.status import BLOCKED, CANCELLED from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.application.runtime.models import MutationMeta from ergon_core.core.application.runtime.graph_repository import RuntimeGraphRepository from ergon_core.core.application.runtime.orchestration import PropagateTaskCompletionCommand -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from sqlmodel import select from tests.integration.propagation._helpers import ( @@ -34,15 +38,21 @@ # --------------------------------------------------------------------------- -def _cleanup_run(run_id, defn_id) -> None: # type: ignore[no-untyped-def] +def _cleanup_run(sample_id, defn_id) -> None: # type: ignore[no-untyped-def] with get_session() as session: for mut in session.exec( - select(RunGraphMutation).where(RunGraphMutation.run_id == run_id) + select(SampleGraphMutation).where(SampleGraphMutation.sample_id == sample_id) ).all(): session.delete(mut) - for edge in session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all(): + for edge in session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all(): session.delete(edge) - nodes = list(session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all()) + nodes = list( + session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all() + ) remaining = {node.task_id: node for node in nodes} while remaining: parent_ids = { @@ -55,7 +65,7 @@ def _cleanup_run(run_id, defn_id) -> None: # type: ignore[no-untyped-def] session.delete(node) remaining.pop(node.task_id) session.flush() - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) if run_row is not None: session.delete(run_row) defn_row = session.get(ExperimentDefinition, defn_id) @@ -94,7 +104,7 @@ async def test_ec1_fan_in_one_dep_fails_target_blocked() -> None: node_c = make_node(session, run.id, task_slug="fan-c", status="pending") make_edge(session, run.id, source_task_id=node_a.task_id, target_task_id=node_c.task_id) make_edge(session, run.id, source_task_id=node_b.task_id, target_task_id=node_c.task_id) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -106,14 +116,14 @@ async def test_ec1_fan_in_one_dep_fails_target_blocked() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=TaskExecutionStatus.FAILED, meta=MutationMeta(actor="test:setup", reason="test: A failed"), ) await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_b_id, new_status=TaskExecutionStatus.COMPLETED, meta=MutationMeta(actor="test:setup", reason="test: B completed"), @@ -125,7 +135,7 @@ async def test_ec1_fan_in_one_dep_fails_target_blocked() -> None: # Propagate A's failure first await svc.propagate_failure( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_a_id, execution_id=node_a_id, @@ -135,7 +145,7 @@ async def test_ec1_fan_in_one_dep_fails_target_blocked() -> None: # Then propagate B's completion — B is done but A already failed C await svc.propagate( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_b_id, execution_id=node_b_id, @@ -150,19 +160,19 @@ async def test_ec1_fan_in_one_dep_fails_target_blocked() -> None: ) assert_wal_has_status(session, node_c_id, BLOCKED) - # RunRecord must remain EXECUTING — the run is stuck, not over + # SampleRecord must remain EXECUTING — the run is stuck, not over with get_session() as session: - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) assert run_row is not None - assert run_row.status == RunStatus.EXECUTING, ( - f"RunRecord must remain EXECUTING while C is blocked; got {run_row.status!r}" + assert run_row.status == SampleStatus.EXECUTING, ( + f"SampleRecord must remain EXECUTING while C is blocked; got {run_row.status!r}" ) with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) # --------------------------------------------------------------------------- @@ -185,7 +195,7 @@ async def test_ec2_duplicate_propagate_is_idempotent() -> None: node_a = make_node(session, run.id, task_slug="idem-a", status="running") node_b = make_node(session, run.id, task_slug="idem-b", status="pending") make_edge(session, run.id, source_task_id=node_a.task_id, target_task_id=node_b.task_id) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -196,7 +206,7 @@ async def test_ec2_duplicate_propagate_is_idempotent() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=TaskExecutionStatus.RUNNING, meta=MutationMeta(actor="test:setup", reason="test setup"), @@ -206,7 +216,7 @@ async def test_ec2_duplicate_propagate_is_idempotent() -> None: svc = WorkflowService() command = PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_a_id, execution_id=node_a_id, @@ -239,7 +249,7 @@ async def test_ec2_duplicate_propagate_is_idempotent() -> None: }, f"B status corrupted by duplicate propagate; got {b_status!r}" with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) diff --git a/tests/integration/propagation/test_propagation_happy.py b/tests/integration/propagation/test_propagation_happy.py index 6be02b8bb..ea5d65581 100644 --- a/tests/integration/propagation/test_propagation_happy.py +++ b/tests/integration/propagation/test_propagation_happy.py @@ -1,21 +1,25 @@ """Test 1 — single-task happy path. A single-node run completes successfully. The graph node must reach -COMPLETED status and the RunRecord must stay non-failed. +COMPLETED status and the SampleRecord must stay non-failed. Expected to PASS with current production code (no xfail). """ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.persistence.shared.db import get_engine, get_session -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.application.runtime.models import MutationMeta from ergon_core.core.application.runtime.graph_repository import RuntimeGraphRepository from ergon_core.core.application.runtime.orchestration import PropagateTaskCompletionCommand -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from sqlalchemy import text from sqlalchemy.exc import OperationalError from sqlmodel import select @@ -58,18 +62,22 @@ def _skip_if_db_unreachable() -> None: # --------------------------------------------------------------------------- -def _cleanup_run(run_id, defn_id) -> None: # type: ignore[no-untyped-def] +def _cleanup_run(sample_id, defn_id) -> None: # type: ignore[no-untyped-def] """Remove all rows created by a test, in FK-safe order.""" with get_session() as session: for mut in session.exec( - select(RunGraphMutation).where(RunGraphMutation.run_id == run_id) + select(SampleGraphMutation).where(SampleGraphMutation.sample_id == sample_id) ).all(): session.delete(mut) - for edge in session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all(): + for edge in session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all(): session.delete(edge) - for nd in session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all(): + for nd in session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all(): session.delete(nd) - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) if run_row is not None: session.delete(run_row) defn_row = session.get(ExperimentDefinition, defn_id) @@ -94,7 +102,7 @@ async def test_1_single_task_happy_path() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="task-a", status="running") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -105,7 +113,7 @@ async def test_1_single_task_happy_path() -> None: with get_session() as session: await graph_repo.update_node_status( session, - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_status=TaskExecutionStatus.RUNNING, meta=MutationMeta(actor="test:setup", reason="test setup: running"), @@ -116,7 +124,7 @@ async def test_1_single_task_happy_path() -> None: svc = WorkflowService() await svc.propagate( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_a_id, execution_id=node_a_id, @@ -131,15 +139,15 @@ async def test_1_single_task_happy_path() -> None: ) assert_wal_has_status(session, node_a_id, "completed") - # RunRecord must not be FAILED after single-task happy-path completion. + # SampleRecord must not be FAILED after single-task happy-path completion. with get_session() as session: - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) assert run_row is not None - assert run_row.status != RunStatus.FAILED, ( - f"RunRecord should not be FAILED; got {run_row.status!r}" + assert run_row.status != SampleStatus.FAILED, ( + f"SampleRecord should not be FAILED; got {run_row.status!r}" ) with get_session() as session: - assert_cross_cutting_invariants(session, run_id) + assert_cross_cutting_invariants(session, sample_id) finally: - _cleanup_run(run_id, defn_id) + _cleanup_run(sample_id, defn_id) diff --git a/tests/integration/propagation/test_propagation_restart.py b/tests/integration/propagation/test_propagation_restart.py index 92d4b018b..1b3102053 100644 --- a/tests/integration/propagation/test_propagation_restart.py +++ b/tests/integration/propagation/test_propagation_restart.py @@ -49,7 +49,7 @@ async def test_8_blocked_node_cannot_be_restarted() -> None: node_a = make_node(session, run.id, task_slug="task-a-failed", status="failed") node_b = make_node(session, run.id, task_slug="task-b-blocked", status=BLOCKED) make_edge(session, run.id, source_task_id=node_a.task_id, target_task_id=node_b.task_id) - run_id = run.id + sample_id = run.id defn_id = defn.id node_b_id = node_b.task_id session.commit() @@ -63,10 +63,10 @@ async def test_8_blocked_node_cannot_be_restarted() -> None: with pytest.raises(TaskNotTerminalError): await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_b_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_b_id), ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -80,7 +80,7 @@ async def test_8b_restart_failed_node_re_enters_pending() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="task-a-to-restart", status="failed") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -93,7 +93,7 @@ async def test_8b_restart_failed_node_re_enters_pending() -> None: with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) assert result.old_status == TaskExecutionStatus.FAILED @@ -103,4 +103,4 @@ async def test_8b_restart_failed_node_re_enters_pending() -> None: ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) diff --git a/tests/integration/restart/_helpers.py b/tests/integration/restart/_helpers.py index dacc56fea..40b7ac2d4 100644 --- a/tests/integration/restart/_helpers.py +++ b/tests/integration/restart/_helpers.py @@ -3,21 +3,31 @@ from uuid import UUID from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.telemetry.models import SampleRecord from sqlmodel import select -def cleanup_run(run_id: UUID, defn_id: UUID) -> None: +def cleanup_run(sample_id: UUID, defn_id: UUID) -> None: with get_session() as session: for mut in session.exec( - select(RunGraphMutation).where(RunGraphMutation.run_id == run_id) + select(SampleGraphMutation).where(SampleGraphMutation.sample_id == sample_id) ).all(): session.delete(mut) - for edge in session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all(): + for edge in session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all(): session.delete(edge) - nodes = list(session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all()) + nodes = list( + session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all() + ) remaining = {node.task_id: node for node in nodes} while remaining: parent_ids = { @@ -30,7 +40,7 @@ def cleanup_run(run_id: UUID, defn_id: UUID) -> None: session.delete(node) remaining.pop(node.task_id) session.flush() - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) if run_row is not None: session.delete(run_row) defn_row = session.get(ExperimentDefinition, defn_id) @@ -39,12 +49,12 @@ def cleanup_run(run_id: UUID, defn_id: UUID) -> None: session.commit() -def get_edge_status(session, run_id: UUID, source_id: UUID, target_id: UUID) -> str: # type: ignore[no-untyped-def] +def get_edge_status(session, sample_id: UUID, source_id: UUID, target_id: UUID) -> str: # type: ignore[no-untyped-def] edge = session.exec( - select(RunGraphEdge).where( - RunGraphEdge.run_id == run_id, - RunGraphEdge.source_task_id == source_id, - RunGraphEdge.target_task_id == target_id, + select(SampleGraphEdge).where( + SampleGraphEdge.sample_id == sample_id, + SampleGraphEdge.source_task_id == source_id, + SampleGraphEdge.target_task_id == target_id, ) ).first() assert edge is not None, f"No edge from {source_id} to {target_id}" diff --git a/tests/integration/restart/test_downstream_invalidation.py b/tests/integration/restart/test_downstream_invalidation.py index 6041b329e..fe5a484d9 100644 --- a/tests/integration/restart/test_downstream_invalidation.py +++ b/tests/integration/restart/test_downstream_invalidation.py @@ -11,7 +11,11 @@ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.application.runtime.status import ( CANCELLED, EDGE_PENDING, @@ -19,7 +23,7 @@ ) from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.shared.enums import TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.application.runtime.task_models import RestartTaskCommand from ergon_core.core.application.runtime.task_management import TaskManagementService from sqlmodel import select @@ -59,7 +63,7 @@ async def test_completed_successor_is_cancelled_by_invalidation() -> None: target_task_id=node_b.task_id, status=EDGE_SATISFIED, ) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -73,7 +77,7 @@ async def test_completed_successor_is_cancelled_by_invalidation() -> None: with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) assert node_b_id in result.invalidated_task_ids, ( @@ -85,11 +89,11 @@ async def test_completed_successor_is_cancelled_by_invalidation() -> None: assert get_node_status(session, node_b_id) == CANCELLED, ( "B must be CANCELLED — its output is stale after A was restarted" ) - assert get_edge_status(session, run_id, node_a_id, node_b_id) == EDGE_PENDING, ( + assert get_edge_status(session, sample_id, node_a_id, node_b_id) == EDGE_PENDING, ( "Edge A→B must be reset to EDGE_PENDING so re-run of A can re-satisfy it" ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -119,7 +123,7 @@ async def test_pending_successor_is_cancelled_but_cascade_stops_there() -> None: target_task_id=node_c.task_id, status=EDGE_SATISFIED, ) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -134,7 +138,7 @@ async def test_pending_successor_is_cancelled_but_cascade_stops_there() -> None: with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) assert node_b_id in result.invalidated_task_ids, "B must be invalidated" @@ -150,7 +154,7 @@ async def test_pending_successor_is_cancelled_but_cascade_stops_there() -> None: "C must remain COMPLETED — cascade does not recurse through non-terminal B" ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -181,7 +185,7 @@ async def test_failed_and_cancelled_successors_are_skipped() -> None: target_task_id=node_c.task_id, status=EDGE_SATISFIED, ) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -196,7 +200,7 @@ async def test_failed_and_cancelled_successors_are_skipped() -> None: with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) assert result.invalidated_task_ids == [], ( @@ -211,14 +215,14 @@ async def test_failed_and_cancelled_successors_are_skipped() -> None: assert get_node_status(session, node_c_id) == CANCELLED, ( "CANCELLED successor must not be overwritten" ) - assert get_edge_status(session, run_id, node_a_id, node_b_id) == EDGE_PENDING, ( + assert get_edge_status(session, sample_id, node_a_id, node_b_id) == EDGE_PENDING, ( "A→B edge must be reset to EDGE_PENDING so B can be retried later" ) - assert get_edge_status(session, run_id, node_a_id, node_c_id) == EDGE_PENDING, ( + assert get_edge_status(session, sample_id, node_a_id, node_c_id) == EDGE_PENDING, ( "A→C edge must be reset to EDGE_PENDING so C can be retried later" ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -248,7 +252,7 @@ async def test_cascade_invalidation_recurses_through_completed_chain() -> None: target_task_id=node_c.task_id, status=EDGE_SATISFIED, ) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -263,7 +267,7 @@ async def test_cascade_invalidation_recurses_through_completed_chain() -> None: with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) assert set(result.invalidated_task_ids) == {node_b_id, node_c_id}, ( @@ -279,9 +283,9 @@ async def test_cascade_invalidation_recurses_through_completed_chain() -> None: assert get_node_status(session, node_c_id) == CANCELLED, ( "C must be CANCELLED (COMPLETED via recursion through B)" ) - assert get_edge_status(session, run_id, node_a_id, node_b_id) == EDGE_PENDING - assert get_edge_status(session, run_id, node_b_id, node_c_id) == EDGE_PENDING, ( + assert get_edge_status(session, sample_id, node_a_id, node_b_id) == EDGE_PENDING + assert get_edge_status(session, sample_id, node_b_id, node_c_id) == EDGE_PENDING, ( "B→C edge must also be reset to EDGE_PENDING by the cascade" ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) diff --git a/tests/integration/restart/test_manager_dag_scenario.py b/tests/integration/restart/test_manager_dag_scenario.py index 8b0c23f7c..4d7d5f5e7 100644 --- a/tests/integration/restart/test_manager_dag_scenario.py +++ b/tests/integration/restart/test_manager_dag_scenario.py @@ -27,7 +27,7 @@ from ergon_core.core.application.runtime.orchestration import PropagateTaskCompletionCommand from ergon_core.core.application.runtime.task_models import RestartTaskCommand from ergon_core.core.application.runtime.task_management import TaskManagementService -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from tests.integration.propagation._helpers import ( get_node_status, @@ -101,7 +101,7 @@ async def test_diamond_restart_invalidates_fanin_and_reactivates_on_recompletion target_task_id=task_c.task_id, status="satisfied", ) - run_id = run.id + sample_id = run.id defn_id = defn.id task_a_id = task_a.task_id task_b_id = task_b.task_id @@ -123,7 +123,7 @@ async def test_diamond_restart_invalidates_fanin_and_reactivates_on_recompletion with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=task_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=task_a_id), ) assert result.old_status == TaskExecutionStatus.COMPLETED @@ -144,10 +144,10 @@ async def test_diamond_restart_invalidates_fanin_and_reactivates_on_recompletion assert get_node_status(session, task_b_id) == TaskExecutionStatus.COMPLETED, ( "task_b must remain COMPLETED — it is not downstream of task_a" ) - assert get_edge_status(session, run_id, task_a_id, task_c_id) == EDGE_PENDING, ( + assert get_edge_status(session, sample_id, task_a_id, task_c_id) == EDGE_PENDING, ( "task_a→task_c edge must be reset to EDGE_PENDING" ) - assert get_edge_status(session, run_id, task_b_id, task_c_id) == EDGE_PENDING, ( + assert get_edge_status(session, sample_id, task_b_id, task_c_id) == EDGE_PENDING, ( "task_b→task_c edge must be reset to EDGE_PENDING (task_c's incoming reset)" ) @@ -158,7 +158,7 @@ async def test_diamond_restart_invalidates_fanin_and_reactivates_on_recompletion prop_svc = WorkflowService() await prop_svc.propagate( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=task_a_id, execution_id=task_a_id, @@ -175,4 +175,4 @@ async def test_diamond_restart_invalidates_fanin_and_reactivates_on_recompletion ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) diff --git a/tests/integration/restart/test_reactivation.py b/tests/integration/restart/test_reactivation.py index 56e662537..5aa857585 100644 --- a/tests/integration/restart/test_reactivation.py +++ b/tests/integration/restart/test_reactivation.py @@ -13,13 +13,17 @@ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.application.runtime.status import CANCELLED, EDGE_PENDING from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.shared.enums import TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.application.runtime.orchestration import PropagateTaskCompletionCommand -from ergon_core.core.application.runtime.run_lifecycle import WorkflowService +from ergon_core.core.application.runtime.sample_lifecycle import WorkflowService from sqlmodel import select from tests.integration.propagation._helpers import ( @@ -57,7 +61,7 @@ async def test_cancelled_managed_subtask_reactivates_when_dep_completes() -> Non ) # Edge is EDGE_PENDING: reset by restart_task / _invalidate_downstream make_edge(session, run.id, source_task_id=node_a.task_id, target_task_id=node_b.task_id) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -67,7 +71,7 @@ async def test_cancelled_managed_subtask_reactivates_when_dep_completes() -> Non svc = WorkflowService() await svc.propagate( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_a_id, execution_id=node_a_id, @@ -81,7 +85,7 @@ async def test_cancelled_managed_subtask_reactivates_when_dep_completes() -> Non f"got {b_status!r}" ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -98,7 +102,7 @@ async def test_cancelled_static_node_does_not_reactivate() -> None: # node_b is a static node — parent_task_id=None (default) node_b = make_node(session, run.id, task_slug="task-b-static", status=CANCELLED) make_edge(session, run.id, source_task_id=node_a.task_id, target_task_id=node_b.task_id) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -108,7 +112,7 @@ async def test_cancelled_static_node_does_not_reactivate() -> None: svc = WorkflowService() await svc.propagate( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_a_id, execution_id=node_a_id, @@ -122,7 +126,7 @@ async def test_cancelled_static_node_does_not_reactivate() -> None: f"only managed subtasks re-activate. Got {b_status!r}" ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -150,7 +154,7 @@ async def test_fan_in_managed_subtask_reactivates_only_when_all_deps_complete() ) make_edge(session, run.id, source_task_id=node_a.task_id, target_task_id=node_c.task_id) make_edge(session, run.id, source_task_id=node_b.task_id, target_task_id=node_c.task_id) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -163,7 +167,7 @@ async def test_fan_in_managed_subtask_reactivates_only_when_all_deps_complete() # Propagate A completing — B is still PENDING, so C must NOT re-activate await svc.propagate( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_a_id, execution_id=node_a_id, @@ -180,7 +184,7 @@ async def test_fan_in_managed_subtask_reactivates_only_when_all_deps_complete() # Now propagate B completing — both A and B are COMPLETED, so C re-activates await svc.propagate( PropagateTaskCompletionCommand( - run_id=run_id, + sample_id=sample_id, definition_id=defn_id, task_id=node_b_id, execution_id=node_b_id, @@ -193,4 +197,4 @@ async def test_fan_in_managed_subtask_reactivates_only_when_all_deps_complete() f"C must re-activate to PENDING once both A and B are COMPLETED; got {c_status!r}" ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) diff --git a/tests/integration/restart/test_restart_task.py b/tests/integration/restart/test_restart_task.py index 7bf8a71f3..b47e07600 100644 --- a/tests/integration/restart/test_restart_task.py +++ b/tests/integration/restart/test_restart_task.py @@ -11,7 +11,11 @@ import pytest from ergon_core.core.persistence.definitions.models import ExperimentDefinition -from ergon_core.core.persistence.graph.models import RunGraphEdge, RunGraphMutation, RunGraphNode +from ergon_core.core.persistence.graph.models import ( + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, +) from ergon_core.core.application.runtime.status import ( CANCELLED, EDGE_PENDING, @@ -19,7 +23,7 @@ ) from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.shared.enums import TaskExecutionStatus -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.telemetry.models import SampleRecord from ergon_core.core.application.runtime.task_errors import TaskNotTerminalError, TaskRunningError from ergon_core.core.application.runtime.task_models import ( RefineTaskCommand, @@ -63,7 +67,7 @@ async def test_restart_completed_node_becomes_pending_and_resets_edge() -> None: target_task_id=node_b.task_id, status=EDGE_SATISFIED, ) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id node_b_id = node_b.task_id @@ -77,7 +81,7 @@ async def test_restart_completed_node_becomes_pending_and_resets_edge() -> None: with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) assert result.old_status == TaskExecutionStatus.COMPLETED @@ -88,12 +92,12 @@ async def test_restart_completed_node_becomes_pending_and_resets_edge() -> None: "node_a must be PENDING after restart" ) assert_wal_has_status(session, node_a_id, "pending") - edge_st = get_edge_status(session, run_id, node_a_id, node_b_id) + edge_st = get_edge_status(session, sample_id, node_a_id, node_b_id) assert edge_st == EDGE_PENDING, ( f"Edge A→B must be reset to EDGE_PENDING after restart; got {edge_st!r}" ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -103,7 +107,7 @@ async def test_restart_failed_node_becomes_pending() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="failed-task", status="failed") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -116,14 +120,14 @@ async def test_restart_failed_node_becomes_pending() -> None: with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) assert result.old_status == TaskExecutionStatus.FAILED with get_session() as session: assert get_node_status(session, node_a_id) == TaskExecutionStatus.PENDING finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -133,7 +137,7 @@ async def test_restart_cancelled_node_becomes_pending() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="cancelled-task", status=CANCELLED) - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -146,14 +150,14 @@ async def test_restart_cancelled_node_becomes_pending() -> None: with get_session() as session: result = await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) assert result.old_status == CANCELLED with get_session() as session: assert get_node_status(session, node_a_id) == TaskExecutionStatus.PENDING finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -163,7 +167,7 @@ async def test_restart_pending_node_raises() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="pending-task", status="pending") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -177,10 +181,10 @@ async def test_restart_pending_node_raises() -> None: with pytest.raises(TaskNotTerminalError): await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -190,7 +194,7 @@ async def test_restart_running_node_raises() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="running-task", status="running") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -204,10 +208,10 @@ async def test_restart_running_node_raises() -> None: with pytest.raises(TaskNotTerminalError): await svc.restart_task( session, - RestartTaskCommand(run_id=run_id, task_id=node_a_id), + RestartTaskCommand(sample_id=sample_id, task_id=node_a_id), ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -217,7 +221,7 @@ async def test_refine_task_non_pending_node_succeeds() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="refine-completed", status="completed") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -231,7 +235,7 @@ async def test_refine_task_non_pending_node_succeeds() -> None: result = await svc.refine_task( session, RefineTaskCommand( - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_description="Updated after restart", ), @@ -239,7 +243,7 @@ async def test_refine_task_non_pending_node_succeeds() -> None: assert result.new_description == "Updated after restart" finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) @pytest.mark.asyncio @@ -249,7 +253,7 @@ async def test_refine_task_running_node_raises() -> None: defn = make_experiment_definition(session) run = make_run(session, defn.id) node_a = make_node(session, run.id, task_slug="refine-running", status="running") - run_id = run.id + sample_id = run.id defn_id = defn.id node_a_id = node_a.task_id session.commit() @@ -264,10 +268,10 @@ async def test_refine_task_running_node_raises() -> None: await svc.refine_task( session, RefineTaskCommand( - run_id=run_id, + sample_id=sample_id, task_id=node_a_id, new_description="Should not update", ), ) finally: - cleanup_run(run_id, defn_id) + cleanup_run(sample_id, defn_id) diff --git a/tests/integration/smokes/test_smoke_harness.py b/tests/integration/smokes/test_smoke_harness.py index ea49221b4..03f876941 100644 --- a/tests/integration/smokes/test_smoke_harness.py +++ b/tests/integration/smokes/test_smoke_harness.py @@ -107,10 +107,10 @@ def test_seed_then_read_then_reset_roundtrip() -> None: defn_id = defn.id try: - # ── Step 2: seed a run via POST /api/__danger__/test-harness/write/run/seed ───────────────── + # ── Step 2: seed a sample via POST /api/__danger__/test-harness/write/samples/seed ────────── with httpx.Client(timeout=10.0) as client: seed_resp = client.post( - f"{API}/api/__danger__/test-harness/write/run/seed", + f"{API}/api/__danger__/test-harness/write/samples/seed", json={ "definition_id": str(defn_id), "experiment": _EXPERIMENT, @@ -121,16 +121,18 @@ def test_seed_then_read_then_reset_roundtrip() -> None: if seed_resp.status_code == 401: pytest.skip("Test harness secret mismatch - skipping harness integration test") assert seed_resp.status_code == 201, seed_resp.text - run_id = seed_resp.json()["run_id"] - assert run_id # non-empty UUID string + sample_id = seed_resp.json()["sample_id"] + assert sample_id # non-empty UUID string - # ── Step 3: read state via GET /api/__danger__/test-harness/read/run/{run_id}/state ───────── + # ── Step 3: read state via GET /api/__danger__/test-harness/read/samples/{sample_id}/state ─── with httpx.Client(timeout=10.0) as client: - state_resp = client.get(f"{API}/api/__danger__/test-harness/read/run/{run_id}/state") + state_resp = client.get( + f"{API}/api/__danger__/test-harness/read/samples/{sample_id}/state" + ) assert state_resp.status_code == 200, state_resp.text body = state_resp.json() - assert body["run_id"] == run_id + assert body["sample_id"] == sample_id assert body["status"] == "completed" # ── Step 4: reset via POST /api/__danger__/test-harness/write/reset ───────────────────────── @@ -142,14 +144,21 @@ def test_seed_then_read_then_reset_roundtrip() -> None: assert reset_resp.status_code == 204, reset_resp.text - # ── Step 5: confirm the run is gone ────────────────────────────────────── + # ── Step 5: confirm the sample is gone ─────────────────────────────────── with httpx.Client(timeout=10.0) as client: - gone_resp = client.get(f"{API}/api/__danger__/test-harness/read/run/{run_id}/state") + gone_resp = client.get( + f"{API}/api/__danger__/test-harness/read/samples/{sample_id}/state" + ) assert gone_resp.status_code == 404, gone_resp.text finally: - # ── Cleanup: delete the ExperimentDefinition row to avoid leaks ────────── + # ── Cleanup: delete seeded samples before their definition row ─────────── + with httpx.Client(timeout=10.0) as client: + client.post( + f"{API}/api/__danger__/test-harness/write/reset", + json={"experiment_prefix": _EXPERIMENT_PREFIX}, + ) with get_session() as session: row = session.get(ExperimentDefinition, defn_id) if row is not None: @@ -185,4 +194,4 @@ def test_write_experiment_runs_accepts_explicit_runtime_choices() -> None: pytest.skip("Test harness secret mismatch - skipping harness integration test") assert response.status_code == 200, response.text body = response.json() - assert body["run_ids"], body + assert body["sample_ids"], body diff --git a/tests/integration/swebench_verified/conftest.py b/tests/integration/swebench_verified/conftest.py index 9499a8b1e..08101f5d8 100644 --- a/tests/integration/swebench_verified/conftest.py +++ b/tests/integration/swebench_verified/conftest.py @@ -11,10 +11,10 @@ ExperimentDefinitionTask, ) from ergon_core.core.persistence.shared.db import get_session -from ergon_core.core.persistence.shared.enums import RunStatus, TaskExecutionStatus +from ergon_core.core.persistence.shared.enums import SampleStatus, TaskExecutionStatus from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunTaskExecution, + SampleRecord, + SampleTaskAttempt, ) from sqlmodel import select @@ -37,10 +37,10 @@ def swebench_execution() -> tuple[UUID, UUID]: """Seed the minimal FK chain needed by SWEBenchSandboxManager._install_dependencies. Seeds: ExperimentDefinition → ExperimentDefinitionInstance → - ExperimentDefinitionTask + RunRecord → RunTaskExecution(id=execution_id). + ExperimentDefinitionTask + SampleRecord → SampleTaskAttempt(id=execution_id). - Yields (execution_id, run_id) so tests can pass execution_id as - sandbox_key and run_id as run_id to mgr.create(). + Yields (execution_id, sample_id) so tests can pass execution_id as + sandbox_key and sample_id as sample_id to mgr.create(). Cleans up all seeded rows after the test. """ @@ -71,36 +71,36 @@ def swebench_execution() -> tuple[UUID, UUID]: session.flush() session.refresh(task) - run = RunRecord( + run = SampleRecord( definition_id=defn.id, workflow_definition_id=defn.id, benchmark_type="swebench-verified", instance_key="django__django-1", - status=RunStatus.EXECUTING, + status=SampleStatus.EXECUTING, ) session.add(run) session.flush() session.refresh(run) - execution = RunTaskExecution( + execution = SampleTaskAttempt( id=execution_id, - run_id=run.id, + sample_id=run.id, task_id=task.id, status=TaskExecutionStatus.RUNNING, ) session.add(execution) session.commit() - run_id: UUID = run.id + sample_id: UUID = run.id defn_id: UUID = defn.id - yield execution_id, run_id + yield execution_id, sample_id with get_session() as session: - exec_row = session.get(RunTaskExecution, execution_id) + exec_row = session.get(SampleTaskAttempt, execution_id) if exec_row is not None: session.delete(exec_row) - run_row = session.get(RunRecord, run_id) + run_row = session.get(SampleRecord, sample_id) if run_row is not None: session.delete(run_row) for t in session.exec( diff --git a/tests/real_llm/artifact_health.py b/tests/real_llm/artifact_health.py index 623e7779c..447947ea4 100644 --- a/tests/real_llm/artifact_health.py +++ b/tests/real_llm/artifact_health.py @@ -133,11 +133,11 @@ def analyze_rollout_artifacts( # noqa: C901 """Analyze a rollout directory without importing DB/runtime models.""" manifest = _read_json(out_dir / "manifest.json") db_dir = out_dir / "db" - executions = _read_jsonl(db_dir / "run_task_executions.jsonl") - evaluations = _read_jsonl(db_dir / "run_task_evaluations.jsonl") - resources = _read_jsonl(db_dir / "run_resources.jsonl") - graph_nodes = _read_jsonl(db_dir / "run_graph_nodes.jsonl") - context_events = _read_jsonl(db_dir / "run_context_events.jsonl") + executions = _read_jsonl(db_dir / "sample_task_attempts.jsonl") + evaluations = _read_jsonl(db_dir / "sample_task_evaluations.jsonl") + resources = _read_jsonl(db_dir / "sample_resources.jsonl") + graph_nodes = _read_jsonl(db_dir / "sample_graph_nodes.jsonl") + context_events = _read_jsonl(db_dir / "sample_context_events.jsonl") task_count = len(executions) evaluation_count = len(evaluations) diff --git a/tests/real_llm/benchmarks/test_researchrubrics.py b/tests/real_llm/benchmarks/test_researchrubrics.py index 5d52803e3..f39dee291 100644 --- a/tests/real_llm/benchmarks/test_researchrubrics.py +++ b/tests/real_llm/benchmarks/test_researchrubrics.py @@ -5,7 +5,7 @@ (Sonnet 4.6 via OpenRouter by default) and dumps an exhaustive rollout artifact — every persistence table, dashboard screenshots, and a stitched ``report.md`` — to -``tests/real_llm/.rollouts/-/``. +``tests/real_llm/.rollouts/-/``. A reviewing agent (or human) then opens ``report.md`` and reasons about whether the agent succeeded, and what to iterate on in the @@ -32,9 +32,9 @@ import pytest from ergon_core.core.persistence.shared.db import ensure_db, get_session from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskEvaluation, + SampleRecord, + SampleResource, + SampleTaskEvaluation, ) from ergon_core.core.shared.settings import settings from sqlmodel import select @@ -75,36 +75,42 @@ def _require_keys() -> None: def _latest_run_id_since(since: datetime) -> UUID: - """Return the most recent RunRecord.id created at or after ``since``.""" + """Return the most recent SampleRecord.id created at or after ``since``.""" ensure_db() with get_session() as session: stmt = ( - select(RunRecord) - .where(RunRecord.created_at >= since) - .order_by(RunRecord.created_at.desc()) + select(SampleRecord) + .where(SampleRecord.created_at >= since) + .order_by(SampleRecord.created_at.desc()) .limit(1) ) row = session.exec(stmt).first() if row is None: raise RuntimeError( - "no RunRecord created since the harness started — " + "no SampleRecord created since the harness started — " "did the CLI subprocess actually dispatch a run?" ) return row.id -def _wait_for_post_terminal_artifacts(run_id: UUID) -> None: +def _wait_for_post_terminal_artifacts(sample_id: UUID) -> None: """Let async resource/evaluation rows land before dumping artifacts.""" deadline = time.monotonic() + _POST_TERMINAL_ARTIFACT_TIMEOUT_SECONDS while time.monotonic() < deadline: with get_session() as session: resources = len( - list(session.exec(select(RunResource).where(RunResource.run_id == run_id)).all()) + list( + session.exec( + select(SampleResource).where(SampleResource.sample_id == sample_id) + ).all() + ) ) evaluations = len( list( session.exec( - select(RunTaskEvaluation).where(RunTaskEvaluation.run_id == run_id) + select(SampleTaskEvaluation).where( + SampleTaskEvaluation.sample_id == sample_id + ) ).all() ) ) @@ -115,7 +121,7 @@ def _wait_for_post_terminal_artifacts(run_id: UUID) -> None: async def test_researchrubrics_rollout( real_llm_stack: None, # session fixture: stack up - harness_client, # poll /api/__danger__/test-harness/read/run/{id}/state + harness_client, # poll /api/__danger__/test-harness/read/samples/{id}/state playwright_context, # dashboard screenshots openrouter_budget: OpenRouterBudget | None, ) -> None: @@ -160,22 +166,22 @@ async def test_researchrubrics_rollout( check=False, ) - run_id = _latest_run_id_since(started_at) + sample_id = _latest_run_id_since(started_at) terminal_state = harness_client.wait_for_terminal( - run_id, + sample_id, timeout_s=_HARNESS_POLL_TIMEOUT_SECONDS, ) - _wait_for_post_terminal_artifacts(run_id) + _wait_for_post_terminal_artifacts(sample_id) - out_dir = rollout_dir(run_id) + out_dir = rollout_dir(sample_id) # Persist CLI stdout/stderr up front so a crashed DB dump still # leaves breadcrumbs for the reviewing agent. (out_dir / "cli_stdout.txt").write_text(cli_proc.stdout or "") (out_dir / "cli_stderr.txt").write_text(cli_proc.stderr or "") - table_counts = dump_rollout(run_id, out_dir) - screenshots = await capture_dashboard(run_id, playwright_context, out_dir) + table_counts = dump_rollout(sample_id, out_dir) + screenshots = await capture_dashboard(sample_id, playwright_context, out_dir) finished_at = datetime.now(timezone.utc) budget_after = ( @@ -184,7 +190,7 @@ async def test_researchrubrics_rollout( manifest_path = write_manifest( out_dir, - run_id=run_id, + sample_id=sample_id, benchmark=benchmark, worker=worker, evaluator=evaluator, @@ -215,6 +221,6 @@ async def test_researchrubrics_rollout( # The single assertion. ``failed`` and ``cancelled`` are still # successful rollouts — the artifact is the product. assert terminal_state["status"] in {"completed", "failed", "cancelled"}, ( - f"run {run_id} did not reach a terminal status within " + f"run {sample_id} did not reach a terminal status within " f"{_HARNESS_POLL_TIMEOUT_SECONDS}s — see {out_dir}" ) diff --git a/tests/real_llm/benchmarks/test_smoke_stub.py b/tests/real_llm/benchmarks/test_smoke_stub.py index 5fd06b8e4..6e4bed565 100644 --- a/tests/real_llm/benchmarks/test_smoke_stub.py +++ b/tests/real_llm/benchmarks/test_smoke_stub.py @@ -4,7 +4,7 @@ Validates: - docker stack up (or --assume-stack-up), stack fixture did not skip - `ergon benchmark run` CLI path works - - /api/__danger__/test-harness/read/run/{id}/state returns a terminal state + - /api/__danger__/test-harness/read/samples/{id}/state returns a terminal state - Postgres row exists with the right relationships - Playwright can find the run grouping in the dashboard """ @@ -15,25 +15,25 @@ import pytest from ergon_core.core.persistence.shared.db import ensure_db, get_session -from ergon_core.core.persistence.telemetry.models import RunRecord +from ergon_core.core.persistence.telemetry.models import SampleRecord from sqlmodel import select pytestmark = [pytest.mark.real_llm, pytest.mark.asyncio] def _latest_run_id_since(since: datetime) -> str: - """Query the most recent RunRecord created at or after `since`.""" + """Query the most recent SampleRecord created at or after `since`.""" ensure_db() with get_session() as session: stmt = ( - select(RunRecord) - .where(RunRecord.created_at >= since) - .order_by(RunRecord.created_at.desc()) + select(SampleRecord) + .where(SampleRecord.created_at >= since) + .order_by(SampleRecord.created_at.desc()) .limit(1) ) row = session.exec(stmt).first() if row is None: - raise RuntimeError("no RunRecord found after canary CLI invocation") + raise RuntimeError("no SampleRecord found after canary CLI invocation") return str(row.id) @@ -78,10 +78,10 @@ async def test_harness_canary_smoke_stub( f"CLI failed (rc={result.returncode}):\nstdout: {result.stdout}\nstderr: {result.stderr}" ) - run_id = _latest_run_id_since(before) + sample_id = _latest_run_id_since(before) # Poll the harness until terminal. - state = harness_client.wait_for_terminal(run_id, timeout_s=120) + state = harness_client.wait_for_terminal(sample_id, timeout_s=120) assert state["status"] == "completed", f"run did not complete: {state}" assert len(state.get("graph_nodes", [])) >= 1 diff --git a/tests/real_llm/fixtures/harness_client.py b/tests/real_llm/fixtures/harness_client.py index 8e20b4470..84b4c24b4 100644 --- a/tests/real_llm/fixtures/harness_client.py +++ b/tests/real_llm/fixtures/harness_client.py @@ -14,26 +14,28 @@ class BackendHarnessClient: def __init__(self, base_url: str) -> None: self._base = base_url - def get_run_state(self, run_id: str) -> dict[str, Any]: # slopcop: ignore[no-typing-any] + def get_run_state(self, sample_id: str) -> dict[str, Any]: # slopcop: ignore[no-typing-any] with httpx.Client(timeout=10.0) as client: - r = client.get(f"{self._base}/api/__danger__/test-harness/read/run/{run_id}/state") + r = client.get( + f"{self._base}/api/__danger__/test-harness/read/samples/{sample_id}/state" + ) r.raise_for_status() return r.json() def wait_for_terminal( self, - run_id: str, + sample_id: str, *, timeout_s: float = 600.0, poll_s: float = 3.0, ) -> dict[str, Any]: # slopcop: ignore[no-typing-any] deadline = time.monotonic() + timeout_s while time.monotonic() < deadline: - state = self.get_run_state(run_id) + state = self.get_run_state(sample_id) if state["status"] in {"completed", "failed", "cancelled"}: return state time.sleep(poll_s) - raise TimeoutError(f"run {run_id} did not reach terminal status in {timeout_s}s") + raise TimeoutError(f"run {sample_id} did not reach terminal status in {timeout_s}s") @pytest.fixture diff --git a/tests/real_llm/rollout.py b/tests/real_llm/rollout.py index 9ebd4f1e6..ded42df22 100644 --- a/tests/real_llm/rollout.py +++ b/tests/real_llm/rollout.py @@ -11,18 +11,18 @@ Artifact layout: - tests/real_llm/.rollouts/-/ + tests/real_llm/.rollouts/-/ ├── manifest.json # run metadata + key fingerprints ├── db/ # one jsonl/json per persistence table │ ├── run_record.json - │ ├── run_task_executions.jsonl - │ ├── run_resources.jsonl - │ ├── run_task_evaluations.jsonl + │ ├── sample_task_attempts.jsonl + │ ├── sample_resources.jsonl + │ ├── sample_task_evaluations.jsonl │ ├── sandbox_events.jsonl - │ ├── run_graph_nodes.jsonl - │ ├── run_graph_edges.jsonl - │ ├── run_graph_mutations.jsonl - │ └── run_context_events.jsonl + │ ├── sample_graph_nodes.jsonl + │ ├── sample_graph_edges.jsonl + │ ├── sample_graph_mutations.jsonl + │ └── sample_context_events.jsonl ├── screenshots/ │ ├── experiment_index.png │ └── run_detail.png @@ -45,10 +45,10 @@ _ROLLOUTS_ROOT = Path(__file__).parent / ".rollouts" -def rollout_dir(run_id: UUID) -> Path: - """Return (and ensure) ``tests/real_llm/.rollouts/-/``.""" +def rollout_dir(sample_id: UUID) -> Path: + """Return (and ensure) ``tests/real_llm/.rollouts/-/``.""" ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") - out = _ROLLOUTS_ROOT / f"{ts}-{run_id}" + out = _ROLLOUTS_ROOT / f"{ts}-{sample_id}" (out / "db").mkdir(parents=True, exist_ok=True) (out / "screenshots").mkdir(parents=True, exist_ok=True) return out @@ -79,15 +79,15 @@ def _write_mapping_jsonl( return len(rows) -def dump_rollout(run_id: UUID, out_dir: Path) -> dict[str, int]: +def dump_rollout(sample_id: UUID, out_dir: Path) -> dict[str, int]: """Dump every persistence table for a run into ``out_dir/db/``. Returns a ``{table_name: row_count}`` map for the manifest. - Each table's rows are filtered by ``run_id`` (all relevant tables + Each table's rows are filtered by ``sample_id`` (all relevant tables carry it as either an FK or an indexed column). Rows are serialised via SQLModel's ``.model_dump_json()`` so the dump preserves the - exact Pydantic schema — downstream readers can ``RunRecord.model_validate_json`` + exact Pydantic schema — downstream readers can ``SampleRecord.model_validate_json`` to round-trip. """ # reason: importing persistence models at module import time triggers a @@ -95,16 +95,16 @@ def dump_rollout(run_id: UUID, out_dir: Path) -> dict[str, int]: # pure report helpers from this module. The DB models are only needed for # live rollout dumping, so keep this import scoped to that operation. from ergon_core.core.persistence.graph.models import ( - RunGraphEdge, - RunGraphMutation, - RunGraphNode, + SampleGraphEdge, + SampleGraphMutation, + SampleGraphNode, ) from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.telemetry.models import ( - RunRecord, - RunResource, - RunTaskEvaluation, - RunTaskExecution, + SampleRecord, + SampleResource, + SampleTaskEvaluation, + SampleTaskAttempt, SandboxEvent, ) from sqlalchemy import text @@ -114,69 +114,83 @@ def dump_rollout(run_id: UUID, out_dir: Path) -> dict[str, int]: counts: dict[str, int] = {} with get_session() as session: - run = session.exec(select(RunRecord).where(RunRecord.id == run_id)).first() + run = session.exec(select(SampleRecord).where(SampleRecord.id == sample_id)).first() if run is None: - raise RuntimeError(f"run {run_id} not found in DB — cannot dump rollout") + raise RuntimeError(f"run {sample_id} not found in DB — cannot dump rollout") _write_json_model(db_dir / "run_record.json", run) counts["run_record"] = 1 - counts["run_task_executions"] = _write_jsonl( - db_dir / "run_task_executions.jsonl", + counts["sample_task_attempts"] = _write_jsonl( + db_dir / "sample_task_attempts.jsonl", list( session.exec( - select(RunTaskExecution).where(RunTaskExecution.run_id == run_id) + select(SampleTaskAttempt).where(SampleTaskAttempt.sample_id == sample_id) ).all() ), ) - counts["run_resources"] = _write_jsonl( - db_dir / "run_resources.jsonl", - list(session.exec(select(RunResource).where(RunResource.run_id == run_id)).all()), + counts["sample_resources"] = _write_jsonl( + db_dir / "sample_resources.jsonl", + list( + session.exec( + select(SampleResource).where(SampleResource.sample_id == sample_id) + ).all() + ), ) - counts["run_task_evaluations"] = _write_jsonl( - db_dir / "run_task_evaluations.jsonl", + counts["sample_task_evaluations"] = _write_jsonl( + db_dir / "sample_task_evaluations.jsonl", list( session.exec( - select(RunTaskEvaluation).where(RunTaskEvaluation.run_id == run_id) + select(SampleTaskEvaluation).where(SampleTaskEvaluation.sample_id == sample_id) ).all() ), ) counts["sandbox_events"] = _write_jsonl( db_dir / "sandbox_events.jsonl", - list(session.exec(select(SandboxEvent).where(SandboxEvent.run_id == run_id)).all()), + list( + session.exec(select(SandboxEvent).where(SandboxEvent.sample_id == sample_id)).all() + ), ) - counts["run_graph_nodes"] = _write_jsonl( - db_dir / "run_graph_nodes.jsonl", - list(session.exec(select(RunGraphNode).where(RunGraphNode.run_id == run_id)).all()), + counts["sample_graph_nodes"] = _write_jsonl( + db_dir / "sample_graph_nodes.jsonl", + list( + session.exec( + select(SampleGraphNode).where(SampleGraphNode.sample_id == sample_id) + ).all() + ), ) - counts["run_graph_edges"] = _write_jsonl( - db_dir / "run_graph_edges.jsonl", - list(session.exec(select(RunGraphEdge).where(RunGraphEdge.run_id == run_id)).all()), + counts["sample_graph_edges"] = _write_jsonl( + db_dir / "sample_graph_edges.jsonl", + list( + session.exec( + select(SampleGraphEdge).where(SampleGraphEdge.sample_id == sample_id) + ).all() + ), ) - counts["run_graph_mutations"] = _write_jsonl( - db_dir / "run_graph_mutations.jsonl", + counts["sample_graph_mutations"] = _write_jsonl( + db_dir / "sample_graph_mutations.jsonl", list( session.exec( - select(RunGraphMutation).where(RunGraphMutation.run_id == run_id) + select(SampleGraphMutation).where(SampleGraphMutation.sample_id == sample_id) ).all() ), ) - # Avoid importing RunContextEvent here: that model depends on context + # Avoid importing SampleContextEvent here: that model depends on context # payloads, which currently have a circular import through api.Worker. rows = [ dict(row) for row in session.connection() .execute( text( - "select * from run_context_events " - "where run_id = :run_id order by sequence asc, created_at asc" + "select * from sample_context_events " + "where sample_id = :sample_id order by sequence asc, created_at asc" ), - {"run_id": str(run_id)}, + {"sample_id": str(sample_id)}, ) .mappings() .all() ] - counts["run_context_events"] = _write_mapping_jsonl( - db_dir / "run_context_events.jsonl", + counts["sample_context_events"] = _write_mapping_jsonl( + db_dir / "sample_context_events.jsonl", rows, ) @@ -184,14 +198,14 @@ def dump_rollout(run_id: UUID, out_dir: Path) -> dict[str, int]: async def capture_dashboard( - run_id: UUID, + sample_id: UUID, playwright_context: Any, # slopcop: ignore[no-typing-any] out_dir: Path, ) -> dict[str, str]: """Screenshot the two dashboard pages that matter for a rollout. ``/`` — experiment index (confirms the run exists in the aggregate list). - ``/run/`` — run detail (agent graph, turn timeline, outputs). + ``/run/`` — run detail (agent graph, turn timeline, outputs). Returns a ``{page_name: screenshot_path}`` map. Failures on either page are logged and the entry is omitted — a missing screenshot is @@ -219,13 +233,15 @@ async def capture_dashboard( page = await playwright_context.new_page() try: - await page.goto(f"/run/{run_id}") + await page.goto(f"/run/{sample_id}") await page.wait_for_load_state("networkidle") shot = shots_dir / "run_detail.png" await page.screenshot(path=str(shot), full_page=True) captured["run_detail"] = str(shot.relative_to(out_dir)) except Exception: # slopcop: ignore[no-broad-except] - logger.exception("capture_dashboard: run_detail screenshot failed for run_id=%s", run_id) + logger.exception( + "capture_dashboard: run_detail screenshot failed for sample_id=%s", sample_id + ) finally: await page.close() @@ -242,7 +258,7 @@ def _fingerprint(value: str | None) -> str | None: def write_manifest( # slopcop: ignore[max-function-params] out_dir: Path, *, - run_id: UUID, + sample_id: UUID, benchmark: str, worker: str, evaluator: str, @@ -258,7 +274,7 @@ def write_manifest( # slopcop: ignore[max-function-params] ) -> Path: """Write ``manifest.json`` — the top-level index into the rollout.""" manifest: dict[str, Any] = { # slopcop: ignore[no-typing-any] - "run_id": str(run_id), + "sample_id": str(sample_id), "benchmark": benchmark, "worker": worker, "evaluator": evaluator, @@ -293,7 +309,7 @@ def write_report(out_dir: Path, manifest_path: Path) -> Path: duration = manifest.get("wall_clock", {}).get("duration_seconds") lines: list[str] = [ - f"# Rollout {manifest['run_id']}", + f"# Rollout {manifest['sample_id']}", "", f"- benchmark: `{manifest['benchmark']}`", f"- worker: `{manifest['worker']}`", @@ -355,11 +371,11 @@ def write_report(out_dir: Path, manifest_path: Path) -> Path: "", "- `db/run_record.json` — one row. `summary_json` carries the run-wide", " outcome fields; `status`, `started_at`, `completed_at` anchor the timeline.", - "- `db/run_context_events.jsonl` — every recorded context event in order.", + "- `db/sample_context_events.jsonl` — every recorded context event in order.", " Tool calls + returns + thinking + text reconstruct what the agent did.", - "- `db/run_graph_nodes.jsonl` + `run_graph_mutations.jsonl` — agent's", + "- `db/sample_graph_nodes.jsonl` + `sample_graph_mutations.jsonl` — agent's", " subtask structure over time.", - "- `db/run_task_evaluations.jsonl` — rubric scores, if the evaluator ran.", + "- `db/sample_task_evaluations.jsonl` — rubric scores, if the evaluator ran.", "- `db/sandbox_events.jsonl` — commands executed in the E2B sandbox.", "- `screenshots/` — what the dashboard renders for this run.", ] diff --git a/tests/real_llm/test_artifact_health.py b/tests/real_llm/test_artifact_health.py index 6a9a977cd..ed2a635b1 100644 --- a/tests/real_llm/test_artifact_health.py +++ b/tests/real_llm/test_artifact_health.py @@ -20,14 +20,14 @@ def test_artifact_health_reports_tool_budget_signals(tmp_path: Path) -> None: db_dir = tmp_path / "db" db_dir.mkdir() for name in [ - "run_task_executions.jsonl", - "run_task_evaluations.jsonl", - "run_resources.jsonl", - "run_graph_nodes.jsonl", + "sample_task_attempts.jsonl", + "sample_task_evaluations.jsonl", + "sample_resources.jsonl", + "sample_graph_nodes.jsonl", ]: (db_dir / name).write_text("") _write_jsonl( - db_dir / "run_context_events.jsonl", + db_dir / "sample_context_events.jsonl", [ { "event_type": "tool_call",