diff --git a/ergon-dashboard/scripts/generate-event-contracts.mjs b/ergon-dashboard/scripts/generate-event-contracts.mjs index 06c106a9d..607f4fa4c 100644 --- a/ergon-dashboard/scripts/generate-event-contracts.mjs +++ b/ergon-dashboard/scripts/generate-event-contracts.mjs @@ -48,6 +48,15 @@ import { GraphMutationDtoSchema } from "@/features/graph/contracts/graphMutation export const DashboardGraphMutationEventSchema = z.object({ mutation: GraphMutationDtoSchema, }).catchall(z.any()); +`; + } + if (entry.modelName === "DashboardSampleRuntimeEvent") { + return `import { z } from "zod"; +import { SampleRuntimeEventViewSchema } from "@/lib/contracts/rest"; + +export const DashboardSampleRuntimeEventSchema = z.object({ + event: SampleRuntimeEventViewSchema, +}).catchall(z.any()); `; } return null; diff --git a/ergon-dashboard/scripts/generate-rest-contracts.mjs b/ergon-dashboard/scripts/generate-rest-contracts.mjs index 04ffa7e6a..3e27f52ea 100644 --- a/ergon-dashboard/scripts/generate-rest-contracts.mjs +++ b/ergon-dashboard/scripts/generate-rest-contracts.mjs @@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const contractsPath = path.resolve(__dirname, "../src/generated/rest/contracts.ts"); -const source = readFileSync(contractsPath, "utf8") +let source = readFileSync(contractsPath, "utf8") .replace('import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core";\n', "") // openapi-zod-client generates z.record(V) but Zod requires z.record(K, V). .replace(/z\.record\((?!z\.string\(\))/g, "z.record(z.string(), ") @@ -26,6 +26,54 @@ const source = readFileSync(contractsPath, "utf8") /const JsonValue_(Input|Output): z\.ZodType = z\.union\(\[\n([\s\S]*?)\n\]\);/g, "const JsonValue_$1: z.ZodType = z.lazy(() => z.union([\n$2\n]));", ); + +const sampleRuntimeEventDiscriminators = { + SampleStatusChangedEventView: "sample.status_changed", + SampleTaskAddedEventView: "task.added", + SampleTaskRemovedEventView: "task.removed", + SampleTaskStatusChangedEventView: "task.status_changed", + SampleEdgeAddedEventView: "edge.added", + SampleEdgeRemovedEventView: "edge.removed", + SampleEdgeStatusChangedEventView: "edge.status_changed", + SampleWorkerAddedEventView: "worker.added", + SampleWorkerRemovedEventView: "worker.removed", + SampleEvaluatorAddedEventView: "evaluator.added", + SampleEvaluatorRemovedEventView: "evaluator.removed", + SampleSandboxAddedEventView: "sandbox.added", + SampleSandboxRemovedEventView: "sandbox.removed", + SampleAnnotationSetEventView: "annotation.set", + SampleAnnotationUpdatedEventView: "annotation.updated", + SampleAnnotationDeletedEventView: "annotation.deleted", +}; + +for (const [schemaName, eventType] of Object.entries(sampleRuntimeEventDiscriminators)) { + const pattern = new RegExp(`(const ${schemaName} = z[\\s\\S]*?eventType: )z\\.string\\(\\)(,)`); + source = source.replace(pattern, `$1z.literal("${eventType}")$2`); +} + +const sampleRuntimeEventUnion = `const SampleRuntimeEventView = z.discriminatedUnion("eventType", [ + SampleStatusChangedEventView, + SampleTaskAddedEventView, + SampleTaskRemovedEventView, + SampleTaskStatusChangedEventView, + SampleEdgeAddedEventView, + SampleEdgeRemovedEventView, + SampleEdgeStatusChangedEventView, + SampleWorkerAddedEventView, + SampleWorkerRemovedEventView, + SampleEvaluatorAddedEventView, + SampleEvaluatorRemovedEventView, + SampleSandboxAddedEventView, + SampleSandboxRemovedEventView, + SampleAnnotationSetEventView, + SampleAnnotationUpdatedEventView, + SampleAnnotationDeletedEventView, +]); +`; + +source = source.replace("\nconst SampleEventsView =", `\n${sampleRuntimeEventUnion}const SampleEventsView =`); +source = source.replace(" SampleEventsView,", " SampleRuntimeEventView,\n SampleEventsView,"); + const endpointMarker = "\nconst endpoints = makeApi(["; const markerIndex = source.indexOf(endpointMarker); diff --git a/ergon-dashboard/src/components/sample/SampleWorkspacePage.tsx b/ergon-dashboard/src/components/sample/SampleWorkspacePage.tsx index 13ff99479..a74f379d0 100644 --- a/ergon-dashboard/src/components/sample/SampleWorkspacePage.tsx +++ b/ergon-dashboard/src/components/sample/SampleWorkspacePage.tsx @@ -77,15 +77,14 @@ function payloadRecord(value: unknown): Record { function graphMutationValue(event: SampleRuntimeEventView, mutationType: MutationType): Record { const payload = payloadRecord(event.payload); - const task = payloadRecord(payload.task); - const edge = payloadRecord(payload.edge); - if (mutationType === "node.added") { + if (event.eventType === "task.added" && mutationType === "node.added") { + const task = payloadRecord(event.task); return { - task_slug: String(payload.task_slug ?? task.task_slug ?? task.name ?? event.target_id ?? "task"), - instance_key: String(task.instance_key ?? payload.instance_key ?? event.target_id ?? "task"), + task_slug: String(event.taskSlug ?? task.task_slug ?? task.name ?? event.targetId ?? "task"), + instance_key: String(task.instance_key ?? payload.instance_key ?? event.targetId ?? "task"), description: String(task.description ?? payload.description ?? ""), - status: String(payload.status ?? task.status ?? "pending"), + status: String(event.status ?? task.status ?? "pending"), assigned_worker_slug: typeof task.assigned_worker_slug === "string" ? task.assigned_worker_slug @@ -95,22 +94,32 @@ function graphMutationValue(event: SampleRuntimeEventView, mutationType: Mutatio }; } - if (mutationType === "node.status_changed" || mutationType === "edge.status_changed") { - return { status: String(payload.status ?? "pending") }; + if ( + (event.eventType === "task.status_changed" && mutationType === "node.status_changed") || + (event.eventType === "edge.status_changed" && mutationType === "edge.status_changed") + ) { + return { status: event.status }; } - if (mutationType === "edge.added" || mutationType === "edge.removed") { + if ( + (event.eventType === "edge.added" || event.eventType === "edge.removed") && + (mutationType === "edge.added" || mutationType === "edge.removed") + ) { + const edge = event.eventType === "edge.added" ? payloadRecord(event.edge) : {}; return { - source_task_id: String(payload.source_task_id ?? edge.source_task_id ?? event.target_id), - target_task_id: String(payload.target_task_id ?? edge.target_task_id ?? event.target_id), - status: String(payload.status ?? edge.status ?? "pending"), + source_task_id: event.sourceTaskId, + target_task_id: event.targetTaskId, + status: String(event.eventType === "edge.added" ? (event.status ?? edge.status ?? "pending") : "removed"), }; } - if (mutationType === "annotation.set" || mutationType === "annotation.deleted") { + if ( + (event.eventType === "annotation.set" || event.eventType === "annotation.deleted") && + (mutationType === "annotation.set" || mutationType === "annotation.deleted") + ) { return { - namespace: String(payload.namespace ?? payload.key ?? "runtime"), - payload: payloadRecord(payload.value ?? payload.payload), + namespace: event.key, + payload: event.eventType === "annotation.set" ? payloadRecord(event.value) : payload, }; } @@ -119,23 +128,24 @@ function graphMutationValue(event: SampleRuntimeEventView, mutationType: Mutatio function sampleRuntimeEventsToGraphMutations(events: SampleRuntimeEventView[]): GraphMutationDto[] { return events.flatMap((event, index) => { - const mutationType = graphMutationType(event.event_type); - if (mutationType === null || event.target_id === null) return []; - const targetType = event.target_type === "edge" ? "edge" : "node"; + const mutationType = graphMutationType(event.eventType); + const targetId = event.targetId; + if (mutationType === null || !targetId) return []; + const targetType = event.targetType === "edge" ? "edge" : "node"; const newValue = graphMutationValue(event, mutationType); return [ { - id: event.id, - sample_id: event.sample_id, + id: event.eventId, + sample_id: event.sampleId, sequence: index + 1, mutation_type: mutationType, target_type: targetType, - target_id: event.target_id, + target_id: targetId, actor: "runtime", old_value: null, new_value: newValue, reason: null, - created_at: event.event_timestamp, + created_at: event.timestamp, }, ]; }); diff --git a/ergon-dashboard/src/generated/events/DashboardSampleRuntimeEvent.ts b/ergon-dashboard/src/generated/events/DashboardSampleRuntimeEvent.ts index 0a00a12d6..c887d5769 100644 --- a/ergon-dashboard/src/generated/events/DashboardSampleRuntimeEvent.ts +++ b/ergon-dashboard/src/generated/events/DashboardSampleRuntimeEvent.ts @@ -1,3 +1,6 @@ -import { z } from "zod" +import { z } from "zod"; +import { SampleRuntimeEventViewSchema } from "@/lib/contracts/rest"; -export const DashboardSampleRuntimeEventSchema = z.object({ "event": z.any() }).catchall(z.any()) +export const DashboardSampleRuntimeEventSchema = z.object({ + event: SampleRuntimeEventViewSchema, +}).catchall(z.any()); diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardSampleRuntimeEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardSampleRuntimeEvent.schema.json index 7175915b3..f7f935189 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardSampleRuntimeEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardSampleRuntimeEvent.schema.json @@ -44,45 +44,1150 @@ } ] }, - "SampleRuntimeEventView": { + "SampleAnnotationDeletedEventView": { "properties": { - "id": { + "eventId": { "format": "uuid", - "title": "Id", + "title": "Eventid", "type": "string" }, - "sample_id": { + "sampleId": { "format": "uuid", - "title": "Sample Id", + "title": "Sampleid", "type": "string" }, - "event_timestamp": { + "timestamp": { "format": "date-time", - "title": "Event Timestamp", + "title": "Timestamp", "type": "string" }, - "table": { - "enum": [ - "sample_status_events", - "sample_task_events", - "sample_edge_events", - "sample_worker_events", - "sample_evaluator_events", - "sample_sandbox_events", - "sample_annotation_events" + "eventType": { + "const": "annotation.deleted", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "key": { + "title": "Key", + "type": "string" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "key" + ], + "title": "SampleAnnotationDeletedEventView", + "type": "object" + }, + "SampleAnnotationSetEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "annotation.set", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "key": { + "title": "Key", + "type": "string" + }, + "value": { + "$ref": "#/$defs/JsonObject" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "key" + ], + "title": "SampleAnnotationSetEventView", + "type": "object" + }, + "SampleAnnotationUpdatedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "annotation.updated", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "key": { + "title": "Key", + "type": "string" + }, + "value": { + "$ref": "#/$defs/JsonObject" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "key" + ], + "title": "SampleAnnotationUpdatedEventView", + "type": "object" + }, + "SampleEdgeAddedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "edge.added", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "edge", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "sourceTaskId": { + "format": "uuid", + "title": "Sourcetaskid", + "type": "string" + }, + "targetTaskId": { + "format": "uuid", + "title": "Targettaskid", + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "edge": { + "$ref": "#/$defs/JsonObject" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "sourceTaskId", + "targetTaskId" + ], + "title": "SampleEdgeAddedEventView", + "type": "object" + }, + "SampleEdgeRemovedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "edge.removed", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "edge", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "sourceTaskId": { + "format": "uuid", + "title": "Sourcetaskid", + "type": "string" + }, + "targetTaskId": { + "format": "uuid", + "title": "Targettaskid", + "type": "string" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "sourceTaskId", + "targetTaskId" + ], + "title": "SampleEdgeRemovedEventView", + "type": "object" + }, + "SampleEdgeStatusChangedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "edge.status_changed", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "edge", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "sourceTaskId": { + "format": "uuid", + "title": "Sourcetaskid", + "type": "string" + }, + "targetTaskId": { + "format": "uuid", + "title": "Targettaskid", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "sourceTaskId", + "targetTaskId", + "status" + ], + "title": "SampleEdgeStatusChangedEventView", + "type": "object" + }, + "SampleEvaluatorAddedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "evaluator.added", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "task", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Targetid" + }, + "evaluatorSlug": { + "title": "Evaluatorslug", + "type": "string" + }, + "evaluatorType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Evaluatortype" + }, + "evaluator": { + "$ref": "#/$defs/JsonObject" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "evaluatorSlug" + ], + "title": "SampleEvaluatorAddedEventView", + "type": "object" + }, + "SampleEvaluatorRemovedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "evaluator.removed", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "task", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Targetid" + }, + "evaluatorSlug": { + "title": "Evaluatorslug", + "type": "string" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "evaluatorSlug" + ], + "title": "SampleEvaluatorRemovedEventView", + "type": "object" + }, + "SampleSandboxAddedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "sandbox.added", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "task", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Targetid" + }, + "sandboxSlug": { + "title": "Sandboxslug", + "type": "string" + }, + "sandboxType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sandboxtype" + }, + "sandbox": { + "$ref": "#/$defs/JsonObject" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "sandboxSlug" + ], + "title": "SampleSandboxAddedEventView", + "type": "object" + }, + "SampleSandboxRemovedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "sandbox.removed", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "task", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Targetid" + }, + "sandboxSlug": { + "title": "Sandboxslug", + "type": "string" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "sandboxSlug" + ], + "title": "SampleSandboxRemovedEventView", + "type": "object" + }, + "SampleStatusChangedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "sample.status_changed", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "sample", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "status" + ], + "title": "SampleStatusChangedEventView", + "type": "object" + }, + "SampleTaskAddedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "task.added", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "task", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "taskSlug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Taskslug" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "task": { + "$ref": "#/$defs/JsonObject" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId" + ], + "title": "SampleTaskAddedEventView", + "type": "object" + }, + "SampleTaskRemovedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "task.removed", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "task", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "taskSlug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Taskslug" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId" + ], + "title": "SampleTaskRemovedEventView", + "type": "object" + }, + "SampleTaskStatusChangedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "task.status_changed", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "task", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "format": "uuid", + "title": "Targetid", + "type": "string" + }, + "taskSlug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Taskslug" + }, + "status": { + "title": "Status", + "type": "string" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "status" + ], + "title": "SampleTaskStatusChangedEventView", + "type": "object" + }, + "SampleWorkerAddedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "eventType": { + "const": "worker.added", + "title": "Eventtype", + "type": "string" + }, + "targetType": { + "const": "task", + "title": "Targettype", + "type": "string" + }, + "targetId": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Targetid" + }, + "workerSlug": { + "title": "Workerslug", + "type": "string" + }, + "workerType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Workertype" + }, + "modelTarget": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modeltarget" + }, + "worker": { + "$ref": "#/$defs/JsonObject" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ], - "title": "Table", + "default": null, + "title": "Actor" + }, + "payload": { + "$ref": "#/$defs/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "workerSlug" + ], + "title": "SampleWorkerAddedEventView", + "type": "object" + }, + "SampleWorkerRemovedEventView": { + "properties": { + "eventId": { + "format": "uuid", + "title": "Eventid", + "type": "string" + }, + "sampleId": { + "format": "uuid", + "title": "Sampleid", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", "type": "string" }, - "event_type": { - "title": "Event Type", + "eventType": { + "const": "worker.removed", + "title": "Eventtype", "type": "string" }, - "target_type": { - "title": "Target Type", + "targetType": { + "const": "task", + "title": "Targettype", "type": "string" }, - "target_id": { + "targetId": { "anyOf": [ { "format": "uuid", @@ -92,30 +1197,117 @@ "type": "null" } ], - "title": "Target Id" + "default": null, + "title": "Targetid" + }, + "workerSlug": { + "title": "Workerslug", + "type": "string" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" }, "payload": { "$ref": "#/$defs/JsonObject", - "description": "Typed event payload copied from the source sample runtime WAL row." + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." } }, "required": [ - "id", - "sample_id", - "event_timestamp", - "table", - "event_type", - "target_type", - "target_id" + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "workerSlug" ], - "title": "SampleRuntimeEventView", + "title": "SampleWorkerRemovedEventView", "type": "object" } }, "additionalProperties": true, "properties": { "event": { - "$ref": "#/$defs/SampleRuntimeEventView" + "discriminator": { + "mapping": { + "annotation.deleted": "#/$defs/SampleAnnotationDeletedEventView", + "annotation.set": "#/$defs/SampleAnnotationSetEventView", + "annotation.updated": "#/$defs/SampleAnnotationUpdatedEventView", + "edge.added": "#/$defs/SampleEdgeAddedEventView", + "edge.removed": "#/$defs/SampleEdgeRemovedEventView", + "edge.status_changed": "#/$defs/SampleEdgeStatusChangedEventView", + "evaluator.added": "#/$defs/SampleEvaluatorAddedEventView", + "evaluator.removed": "#/$defs/SampleEvaluatorRemovedEventView", + "sample.status_changed": "#/$defs/SampleStatusChangedEventView", + "sandbox.added": "#/$defs/SampleSandboxAddedEventView", + "sandbox.removed": "#/$defs/SampleSandboxRemovedEventView", + "task.added": "#/$defs/SampleTaskAddedEventView", + "task.removed": "#/$defs/SampleTaskRemovedEventView", + "task.status_changed": "#/$defs/SampleTaskStatusChangedEventView", + "worker.added": "#/$defs/SampleWorkerAddedEventView", + "worker.removed": "#/$defs/SampleWorkerRemovedEventView" + }, + "propertyName": "eventType" + }, + "oneOf": [ + { + "$ref": "#/$defs/SampleStatusChangedEventView" + }, + { + "$ref": "#/$defs/SampleTaskAddedEventView" + }, + { + "$ref": "#/$defs/SampleTaskRemovedEventView" + }, + { + "$ref": "#/$defs/SampleTaskStatusChangedEventView" + }, + { + "$ref": "#/$defs/SampleEdgeAddedEventView" + }, + { + "$ref": "#/$defs/SampleEdgeRemovedEventView" + }, + { + "$ref": "#/$defs/SampleEdgeStatusChangedEventView" + }, + { + "$ref": "#/$defs/SampleWorkerAddedEventView" + }, + { + "$ref": "#/$defs/SampleWorkerRemovedEventView" + }, + { + "$ref": "#/$defs/SampleEvaluatorAddedEventView" + }, + { + "$ref": "#/$defs/SampleEvaluatorRemovedEventView" + }, + { + "$ref": "#/$defs/SampleSandboxAddedEventView" + }, + { + "$ref": "#/$defs/SampleSandboxRemovedEventView" + }, + { + "$ref": "#/$defs/SampleAnnotationSetEventView" + }, + { + "$ref": "#/$defs/SampleAnnotationUpdatedEventView" + }, + { + "$ref": "#/$defs/SampleAnnotationDeletedEventView" + } + ], + "title": "Event" } }, "required": [ diff --git a/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowStartedEvent.schema.json b/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowStartedEvent.schema.json index f2f0bd429..496fe15ce 100644 --- a/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowStartedEvent.schema.json +++ b/ergon-dashboard/src/generated/events/schemas/DashboardWorkflowStartedEvent.schema.json @@ -487,6 +487,98 @@ "title": "SampleCommunicationThreadDto", "type": "object" }, + "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" + }, "SampleEvaluationCriterionDto": { "additionalProperties": false, "properties": { @@ -795,6 +887,56 @@ "title": "SampleExecutionAttemptDto", "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" + }, "SampleSandboxCommandDto": { "additionalProperties": false, "properties": { @@ -942,148 +1084,6 @@ "title": "SampleSandboxDto", "type": "object" }, - "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": { diff --git a/ergon-dashboard/src/generated/rest/contracts.ts b/ergon-dashboard/src/generated/rest/contracts.ts index 2f90bdb0e..dc5a3b0b4 100644 --- a/ergon-dashboard/src/generated/rest/contracts.ts +++ b/ergon-dashboard/src/generated/rest/contracts.ts @@ -19,7 +19,7 @@ const SampleSummaryDto = z completed_at: z.union([z.string(), z.null()]).optional(), latest_activity_at: z.union([z.string(), z.null()]).optional(), duration_seconds: z.union([z.number(), z.null()]).optional(), - definition_id: z.string().uuid(), + definition_id: z.union([z.string(), z.null()]).optional(), definition_name: z.union([z.string(), z.null()]).optional(), experiment: z.union([z.string(), z.null()]).optional(), benchmark_type: z.string(), @@ -306,7 +306,7 @@ const SampleSnapshotMetricsDto = z.object({ }); const SampleSnapshotDto = z.object({ id: z.string(), - definitionId: z.string(), + definitionId: z.union([z.string(), z.null()]).optional(), name: z.string(), status: z.string(), tasks: z.record(z.string(), SampleTaskDto).optional(), @@ -330,132 +330,368 @@ const SampleSnapshotDto = z.object({ metrics: z.union([SampleSnapshotMetricsDto, z.null()]).optional(), error: z.union([z.string(), z.null()]).optional(), }); -const SampleRuntimeEventView = z +const SampleDetailView = z.object({ + sampleId: z.string().uuid(), + experimentId: z.string().uuid(), + environmentId: z.string().uuid(), + environmentName: z.string(), + sampleKey: z.string(), + sampleRef: z.object({}).partial().passthrough().optional(), + sourceMetadata: z.object({}).partial().passthrough().optional(), + status: z.string(), + createdAt: z.string().datetime({ offset: true }), + startedAt: z.union([z.string(), z.null()]).optional(), + completedAt: z.union([z.string(), z.null()]).optional(), +}); +const SampleStatusChangedEventView = z .object({ - id: z.string().uuid(), - sample_id: z.string().uuid(), - event_timestamp: z.string().datetime({ offset: true }), - table: z.enum([ - "sample_status_events", - "sample_task_events", - "sample_edge_events", - "sample_worker_events", - "sample_evaluator_events", - "sample_sandbox_events", - "sample_annotation_events", - ]), - event_type: z.string(), - target_type: z.string(), - target_id: z.union([z.string(), z.null()]), + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("sample.status_changed"), + targetType: z.string(), + targetId: z.string().uuid(), + status: z.string(), + actor: z.union([z.string(), z.null()]).optional(), payload: JsonObject.optional(), }) .passthrough(); -const ExperimentStatusCountsDto = z +const SampleTaskAddedEventView = z .object({ - pending: z.number().int().default(0), - executing: z.number().int().default(0), - evaluating: z.number().int().default(0), - completed: z.number().int().default(0), - failed: z.number().int().default(0), - cancelled: z.number().int().default(0), + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("task.added"), + targetType: z.string(), + targetId: z.string().uuid(), + taskSlug: z.union([z.string(), z.null()]).optional(), + status: z.union([z.string(), z.null()]).optional(), + task: JsonObject.optional(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), }) - .partial() .passthrough(); -const ExperimentSummaryDto = z +const SampleTaskRemovedEventView = z .object({ - definition_id: z.string().uuid(), - name: z.string(), - description: z.union([z.string(), z.null()]).optional(), - benchmark_type: z.string(), - sample_count: z.number().int(), + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("task.removed"), + targetType: z.string(), + targetId: z.string().uuid(), + taskSlug: z.union([z.string(), z.null()]).optional(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleTaskStatusChangedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("task.status_changed"), + targetType: z.string(), + targetId: z.string().uuid(), + taskSlug: z.union([z.string(), z.null()]).optional(), status: z.string(), - default_worker_team: z.object({}).partial().passthrough().optional(), - default_evaluator_slug: z.union([z.string(), z.null()]).optional(), - default_model_target: z.union([z.string(), z.null()]).optional(), - created_by: z.union([z.string(), z.null()]).optional(), - created_at: z.string().datetime({ offset: true }), - started_at: z.union([z.string(), z.null()]).optional(), - completed_at: z.union([z.string(), z.null()]).optional(), - run_count: z.number().int().optional().default(0), - status_counts: ExperimentStatusCountsDto.optional(), - failure_count: z.number().int().optional().default(0), - latest_activity_at: z.union([z.string(), z.null()]).optional(), - average_score: z.union([z.number(), z.null()]).optional(), - average_duration_ms: z.union([z.number(), z.null()]).optional(), - average_tasks: z.union([z.number(), z.null()]).optional(), - total_cost_usd: z.union([z.number(), z.null()]).optional(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), }) .passthrough(); -const ExperimentRunMetricsDto = z +const SampleEdgeAddedEventView = z .object({ - sample_id: z.string().uuid(), - run_name: z.union([z.string(), z.null()]).optional(), + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("edge.added"), + targetType: z.string(), + targetId: z.string().uuid(), + sourceTaskId: z.string().uuid(), + targetTaskId: z.string().uuid(), + status: z.union([z.string(), z.null()]).optional(), + edge: JsonObject.optional(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleEdgeRemovedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("edge.removed"), + targetType: z.string(), + targetId: z.string().uuid(), + sourceTaskId: z.string().uuid(), + targetTaskId: z.string().uuid(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleEdgeStatusChangedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("edge.status_changed"), + targetType: z.string(), + targetId: z.string().uuid(), + sourceTaskId: z.string().uuid(), + targetTaskId: z.string().uuid(), status: z.string(), - sample_label: z.union([z.string(), z.null()]).optional(), - instance_key: z.string(), - score: z.union([z.number(), z.null()]).optional(), - return_value: z.union([z.number(), z.null()]).optional(), - duration_ms: z.union([z.number(), z.null()]).optional(), - total_tasks: z.union([z.number(), z.null()]).optional(), - tool_call_count: z.number().int().optional().default(0), - total_tokens: z.union([z.number(), z.null()]).optional(), - token_breakdown: z.record(z.string(), z.number().int()).optional(), - total_cost_usd: z.union([z.number(), z.null()]).optional(), - cost_observed: z.boolean().optional().default(false), - model_target: z.union([z.string(), z.null()]).optional(), - evaluator_slug: z.union([z.string(), z.null()]).optional(), - error_summary: z.union([z.string(), z.null()]).optional(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), }) .passthrough(); -const ExperimentRunRowDto = z +const SampleWorkerAddedEventView = z .object({ - sample_id: z.string().uuid(), - definition_id: z.string().uuid(), - benchmark_type: z.string(), - instance_key: z.string(), + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("worker.added"), + targetType: z.string(), + targetId: z.union([z.string(), z.null()]).optional(), + workerSlug: z.string(), + workerType: z.union([z.string(), z.null()]).optional(), + modelTarget: z.union([z.string(), z.null()]).optional(), + worker: JsonObject.optional(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleWorkerRemovedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("worker.removed"), + targetType: z.string(), + targetId: z.union([z.string(), z.null()]).optional(), + workerSlug: z.string(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleEvaluatorAddedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("evaluator.added"), + targetType: z.string(), + targetId: z.union([z.string(), z.null()]).optional(), + evaluatorSlug: z.string(), + evaluatorType: z.union([z.string(), z.null()]).optional(), + evaluator: JsonObject.optional(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleEvaluatorRemovedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("evaluator.removed"), + targetType: z.string(), + targetId: z.union([z.string(), z.null()]).optional(), + evaluatorSlug: z.string(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleSandboxAddedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("sandbox.added"), + targetType: z.string(), + targetId: z.union([z.string(), z.null()]).optional(), + sandboxSlug: z.string(), + sandboxType: z.union([z.string(), z.null()]).optional(), + sandbox: JsonObject.optional(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleSandboxRemovedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("sandbox.removed"), + targetType: z.string(), + targetId: z.union([z.string(), z.null()]).optional(), + sandboxSlug: z.string(), + actor: z.union([z.string(), z.null()]).optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleAnnotationSetEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("annotation.set"), + targetType: z.string(), + targetId: z.string().uuid(), + key: z.string(), + value: JsonObject.optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleAnnotationUpdatedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("annotation.updated"), + targetType: z.string(), + targetId: z.string().uuid(), + key: z.string(), + value: JsonObject.optional(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleAnnotationDeletedEventView = z + .object({ + eventId: z.string().uuid(), + sampleId: z.string().uuid(), + timestamp: z.string().datetime({ offset: true }), + eventType: z.literal("annotation.deleted"), + targetType: z.string(), + targetId: z.string().uuid(), + key: z.string(), + payload: JsonObject.optional(), + }) + .passthrough(); +const SampleRuntimeEventView = z.discriminatedUnion("eventType", [ + SampleStatusChangedEventView, + SampleTaskAddedEventView, + SampleTaskRemovedEventView, + SampleTaskStatusChangedEventView, + SampleEdgeAddedEventView, + SampleEdgeRemovedEventView, + SampleEdgeStatusChangedEventView, + SampleWorkerAddedEventView, + SampleWorkerRemovedEventView, + SampleEvaluatorAddedEventView, + SampleEvaluatorRemovedEventView, + SampleSandboxAddedEventView, + SampleSandboxRemovedEventView, + SampleAnnotationSetEventView, + SampleAnnotationUpdatedEventView, + SampleAnnotationDeletedEventView, +]); +const SampleEventsView = z + .object({ + items: z.array( + z.discriminatedUnion("eventType", [ + SampleStatusChangedEventView, + SampleTaskAddedEventView, + SampleTaskRemovedEventView, + SampleTaskStatusChangedEventView, + SampleEdgeAddedEventView, + SampleEdgeRemovedEventView, + SampleEdgeStatusChangedEventView, + SampleWorkerAddedEventView, + SampleWorkerRemovedEventView, + SampleEvaluatorAddedEventView, + SampleEvaluatorRemovedEventView, + SampleSandboxAddedEventView, + SampleSandboxRemovedEventView, + SampleAnnotationSetEventView, + SampleAnnotationUpdatedEventView, + SampleAnnotationDeletedEventView, + ]) + ), + }) + .partial(); +const SampleGraphNodeView = z.object({ + taskId: z.string().uuid(), + taskSlug: z.string(), + description: z.string(), + status: z.string(), + parentTaskId: z.union([z.string(), z.null()]).optional(), + level: z.number().int().optional().default(0), + assignedWorkerSlug: z.union([z.string(), z.null()]).optional(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), +}); +const SampleGraphEdgeView = z.object({ + edgeId: z.string().uuid(), + sourceTaskId: z.string().uuid(), + targetTaskId: z.string().uuid(), + status: z.string(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }), +}); +const SampleGraphView = z + .object({ + nodes: z.array(SampleGraphNodeView), + edges: z.array(SampleGraphEdgeView), + }) + .partial(); +const EnvironmentContributionView = z + .object({ + environmentId: z.string().uuid(), + environmentName: z.string(), + sourceMode: z.string(), + sampleCount: z.number().int(), + selectedCount: z.number().int(), + sourceMetadata: z.object({}).partial().passthrough().optional(), + }) + .passthrough(); +const ExperimentSampleSummaryView = z + .object({ + sampleId: z.string().uuid(), + experimentId: z.string().uuid(), + environmentId: z.string().uuid(), + environmentName: z.string(), + sampleKey: z.string(), + sampleRef: z.object({}).partial().passthrough().optional(), + sourceMetadata: z.object({}).partial().passthrough().optional(), status: z.string(), - created_at: z.string().datetime({ offset: true }), - started_at: z.union([z.string(), z.null()]).optional(), - completed_at: z.union([z.string(), z.null()]).optional(), - evaluator_slug: z.union([z.string(), z.null()]).optional(), - model_target: z.union([z.string(), z.null()]).optional(), - worker_team: z.object({}).partial().passthrough().optional(), - seed: z.union([z.number(), z.null()]).optional(), - running_time_ms: z.union([z.number(), z.null()]).optional(), - final_score: z.union([z.number(), z.null()]).optional(), - total_tasks: z.union([z.number(), z.null()]).optional(), - total_cost_usd: z.union([z.number(), z.null()]).optional(), - error_message: z.union([z.string(), z.null()]).optional(), - metrics: ExperimentRunMetricsDto, + createdAt: z.string().datetime({ offset: true }), }) .passthrough(); -const ExperimentAnalyticsDto = z +const SamplerInvocationView = z .object({ - total_runs: z.number().int().default(0), - status_counts: ExperimentStatusCountsDto, - average_score: z.union([z.number(), z.null()]), - average_duration_ms: z.union([z.number(), z.null()]), - average_tasks: z.union([z.number(), z.null()]), - total_cost_usd: z.union([z.number(), z.null()]), - latest_activity_at: z.union([z.string(), z.null()]), - error_count: z.number().int().default(0), + samplerInvocationId: z.string().uuid(), + samplerName: z.string(), + requestedK: z.number().int(), + candidatePoolSize: z.number().int(), + selectedCount: z.number().int(), + samplerConfig: z.object({}).partial().passthrough().optional(), + createdAt: z.string().datetime({ offset: true }), }) - .partial() .passthrough(); -const ExperimentDetailDto = z +const ExperimentDetailView = z .object({ - definition_id: z.union([z.string(), z.null()]).optional(), - name: z.union([z.string(), z.null()]).optional(), + experimentId: z.string().uuid(), + name: z.string(), description: z.union([z.string(), z.null()]).optional(), - benchmark_type: z.union([z.string(), z.null()]).optional(), - experiment: ExperimentSummaryDto, - runs: z.array(ExperimentRunRowDto).optional(), - analytics: ExperimentAnalyticsDto.optional(), - sample_selection: z.object({}).partial().passthrough().optional(), - design: z.object({}).partial().passthrough().optional(), + environments: z.array(EnvironmentContributionView).optional(), + sampleCount: z.number().int(), + samples: z.array(ExperimentSampleSummaryView).optional(), + samplerInvocations: z.array(SamplerInvocationView).optional(), metadata: z.object({}).partial().passthrough().optional(), + createdAt: z.string().datetime({ offset: true }), }) .passthrough(); +const ExperimentListView = z + .object({ items: z.array(ExperimentDetailView) }) + .partial() + .passthrough(); +const ExperimentSamplesView = z + .object({ items: z.array(ExperimentSampleSummaryView) }) + .partial() + .passthrough(); +const SamplerInvocationsView = z + .object({ items: z.array(SamplerInvocationView) }) + .partial() + .passthrough(); const ExperimentRunRequest = z .object({ definition_id: z.string().uuid(), @@ -521,6 +757,15 @@ const PollResponse = z failures: z.array(EpisodeFailure).optional(), }) .passthrough(); +const RolloutBatchSummary = z + .object({ + batch_id: z.string().uuid(), + sample_ids: z.array(z.string().uuid()), + status: RolloutStatus, + experiment_id: z.union([z.string(), z.null()]).optional(), + sampler_invocation_id: z.union([z.string(), z.null()]).optional(), + }) + .passthrough(); const WeightSyncRequest = z .object({ checkpoint_path: z.string(), model_name: z.string() }) .passthrough(); @@ -637,13 +882,35 @@ export const schemas = { SampleCommunicationThreadDto, SampleSnapshotMetricsDto, SampleSnapshotDto, + SampleDetailView, + SampleStatusChangedEventView, + SampleTaskAddedEventView, + SampleTaskRemovedEventView, + SampleTaskStatusChangedEventView, + SampleEdgeAddedEventView, + SampleEdgeRemovedEventView, + SampleEdgeStatusChangedEventView, + SampleWorkerAddedEventView, + SampleWorkerRemovedEventView, + SampleEvaluatorAddedEventView, + SampleEvaluatorRemovedEventView, + SampleSandboxAddedEventView, + SampleSandboxRemovedEventView, + SampleAnnotationSetEventView, + SampleAnnotationUpdatedEventView, + SampleAnnotationDeletedEventView, SampleRuntimeEventView, - ExperimentStatusCountsDto, - ExperimentSummaryDto, - ExperimentRunMetricsDto, - ExperimentRunRowDto, - ExperimentAnalyticsDto, - ExperimentDetailDto, + SampleEventsView, + SampleGraphNodeView, + SampleGraphEdgeView, + SampleGraphView, + EnvironmentContributionView, + ExperimentSampleSummaryView, + SamplerInvocationView, + ExperimentDetailView, + ExperimentListView, + ExperimentSamplesView, + SamplerInvocationsView, ExperimentRunRequest, run_experiment_experiments__definition_id__run_post_Body, ExperimentRunResult, @@ -653,6 +920,7 @@ export const schemas = { Trajectory, EpisodeFailure, PollResponse, + RolloutBatchSummary, WeightSyncRequest, WeightSyncResponse, TestGraphNodeDto, diff --git a/ergon-dashboard/src/generated/rest/openapi.json b/ergon-dashboard/src/generated/rest/openapi.json index 2831ee8ef..1df32454b 100644 --- a/ergon-dashboard/src/generated/rest/openapi.json +++ b/ergon-dashboard/src/generated/rest/openapi.json @@ -113,14 +113,14 @@ } } }, - "/samples/{sample_id}": { + "/samples/{sample_id}/workspace": { "get": { "tags": [ "samples" ], - "summary": "Get Sample Snapshot", - "description": "Get a persisted sample-detail snapshot suitable for frontend hydration.", - "operationId": "get_sample_snapshot_samples__sample_id__get", + "summary": "Get Sample Workspace", + "description": "Get the existing dashboard-compatible sample snapshot contract.", + "operationId": "get_sample_workspace_samples__sample_id__workspace_get", "parameters": [ { "name": "sample_id", @@ -157,6 +157,50 @@ } } }, + "/samples/{sample_id}": { + "get": { + "tags": [ + "samples" + ], + "summary": "Get Sample Detail", + "description": "Get persisted sample provenance and status details.", + "operationId": "get_sample_detail_samples__sample_id__get", + "parameters": [ + { + "name": "sample_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Sample Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleDetailView" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/samples/{sample_id}/events": { "get": { "tags": [ @@ -183,11 +227,51 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SampleRuntimeEventView" - }, - "title": "Response Get Sample Runtime Events Samples Sample Id Events Get" + "$ref": "#/components/schemas/SampleEventsView" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/samples/{sample_id}/graph": { + "get": { + "tags": [ + "samples" + ], + "summary": "Get Sample Graph", + "description": "Return the sample graph projection.", + "operationId": "get_sample_graph_samples__sample_id__graph_get", + "parameters": [ + { + "name": "sample_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Sample Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleGraphView" } } } @@ -282,11 +366,7 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ExperimentSummaryDto" - }, - "title": "Response List Experiments Experiments Get" + "$ref": "#/components/schemas/ExperimentListView" } } } @@ -304,22 +384,108 @@ } } }, - "/experiments/{definition_id}": { + "/experiments/{experiment_id}": { "get": { "tags": [ "experiments" ], "summary": "Get Experiment", - "operationId": "get_experiment_experiments__definition_id__get", + "operationId": "get_experiment_experiments__experiment_id__get", "parameters": [ { - "name": "definition_id", + "name": "experiment_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid", - "title": "Definition Id" + "title": "Experiment Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExperimentDetailView" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/experiments/{experiment_id}/samples": { + "get": { + "tags": [ + "experiments" + ], + "summary": "Get Experiment Samples", + "operationId": "get_experiment_samples_experiments__experiment_id__samples_get", + "parameters": [ + { + "name": "experiment_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Experiment Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExperimentSamplesView" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/experiments/{experiment_id}/sampler-invocations": { + "get": { + "tags": [ + "experiments" + ], + "summary": "Get Experiment Sampler Invocations", + "operationId": "get_experiment_sampler_invocations_experiments__experiment_id__sampler_invocations_get", + "parameters": [ + { + "name": "experiment_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Experiment Id" } } ], @@ -329,7 +495,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExperimentDetailDto" + "$ref": "#/components/schemas/SamplerInvocationsView" } } } @@ -528,6 +694,50 @@ } } }, + "/rollouts/batches/{batch_id}": { + "get": { + "tags": [ + "rollouts" + ], + "summary": "Get Rollout Batch", + "description": "Load durable trainer batch membership by sample id.", + "operationId": "get_rollout_batch_rollouts_batches__batch_id__get", + "parameters": [ + { + "name": "batch_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Batch Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RolloutBatchSummary" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/rollouts/sync-weights": { "post": { "tags": [ @@ -1017,6 +1227,45 @@ "title": "ContextPartChunkLog", "description": "Core-enriched context stream item suitable for API/dashboard projection." }, + "EnvironmentContributionView": { + "properties": { + "environmentId": { + "type": "string", + "format": "uuid", + "title": "Environmentid" + }, + "environmentName": { + "type": "string", + "title": "Environmentname" + }, + "sourceMode": { + "type": "string", + "title": "Sourcemode" + }, + "sampleCount": { + "type": "integer", + "title": "Samplecount" + }, + "selectedCount": { + "type": "integer", + "title": "Selectedcount" + }, + "sourceMetadata": { + "additionalProperties": true, + "type": "object", + "title": "Sourcemetadata" + } + }, + "type": "object", + "required": [ + "environmentId", + "environmentName", + "sourceMode", + "sampleCount", + "selectedCount" + ], + "title": "EnvironmentContributionView" + }, "EpisodeFailure": { "properties": { "sample_id": { @@ -1037,324 +1286,85 @@ "title": "EpisodeFailure", "description": "An episode that didn't complete successfully." }, - "ExperimentAnalyticsDto": { + "ExperimentDetailView": { "properties": { - "total_runs": { - "type": "integer", - "title": "Total Runs", - "default": 0 + "experimentId": { + "type": "string", + "format": "uuid", + "title": "Experimentid" }, - "status_counts": { - "$ref": "#/components/schemas/ExperimentStatusCountsDto" + "name": { + "type": "string", + "title": "Name" }, - "average_score": { + "description": { "anyOf": [ { - "type": "number" + "type": "string" }, { "type": "null" } ], - "title": "Average Score" + "title": "Description" }, - "average_duration_ms": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Average Duration Ms" + "environments": { + "items": { + "$ref": "#/components/schemas/EnvironmentContributionView" + }, + "type": "array", + "title": "Environments" }, - "average_tasks": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Average Tasks" + "sampleCount": { + "type": "integer", + "title": "Samplecount" }, - "total_cost_usd": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Total Cost Usd" - }, - "latest_activity_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Latest Activity At" - }, - "error_count": { - "type": "integer", - "title": "Error Count", - "default": 0 - } - }, - "type": "object", - "title": "ExperimentAnalyticsDto" - }, - "ExperimentDetailDto": { - "properties": { - "definition_id": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "Definition Id" - }, - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "benchmark_type": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Benchmark Type" - }, - "experiment": { - "$ref": "#/components/schemas/ExperimentSummaryDto" - }, - "runs": { + "samples": { "items": { - "$ref": "#/components/schemas/ExperimentRunRowDto" + "$ref": "#/components/schemas/ExperimentSampleSummaryView" }, "type": "array", - "title": "Runs" - }, - "analytics": { - "$ref": "#/components/schemas/ExperimentAnalyticsDto" - }, - "sample_selection": { - "additionalProperties": true, - "type": "object", - "title": "Sample Selection" + "title": "Samples" }, - "design": { - "additionalProperties": true, - "type": "object", - "title": "Design" + "samplerInvocations": { + "items": { + "$ref": "#/components/schemas/SamplerInvocationView" + }, + "type": "array", + "title": "Samplerinvocations" }, "metadata": { "additionalProperties": true, "type": "object", "title": "Metadata" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" } }, "type": "object", "required": [ - "experiment" + "experimentId", + "name", + "sampleCount", + "createdAt" ], - "title": "ExperimentDetailDto" + "title": "ExperimentDetailView" }, - "ExperimentRunMetricsDto": { + "ExperimentListView": { "properties": { - "sample_id": { - "type": "string", - "format": "uuid", - "title": "Sample Id" - }, - "run_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Run Name" - }, - "status": { - "type": "string", - "title": "Status" - }, - "sample_label": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Sample Label" - }, - "instance_key": { - "type": "string", - "title": "Instance Key" - }, - "score": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Score" - }, - "return_value": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Return Value" - }, - "duration_ms": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Duration Ms" - }, - "total_tasks": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Total Tasks" - }, - "tool_call_count": { - "type": "integer", - "title": "Tool Call Count", - "default": 0 - }, - "total_tokens": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Total Tokens" - }, - "token_breakdown": { - "additionalProperties": { - "type": "integer" + "items": { + "items": { + "$ref": "#/components/schemas/ExperimentDetailView" }, - "type": "object", - "title": "Token Breakdown" - }, - "total_cost_usd": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Total Cost Usd" - }, - "cost_observed": { - "type": "boolean", - "title": "Cost Observed", - "default": false - }, - "model_target": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Model Target" - }, - "evaluator_slug": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Evaluator Slug" - }, - "error_summary": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Error Summary" + "type": "array", + "title": "Items" } }, "type": "object", - "required": [ - "sample_id", - "status", - "instance_key" - ], - "title": "ExperimentRunMetricsDto" + "title": "ExperimentListView" }, "ExperimentRunRequest": { "properties": { @@ -1417,87 +1427,245 @@ ], "title": "ExperimentRunResult" }, - "ExperimentRunRowDto": { + "ExperimentRunSlotRequest": { "properties": { - "sample_id": { + "worker_slug": { "type": "string", - "format": "uuid", - "title": "Sample Id" + "title": "Worker Slug" }, - "definition_id": { + "evaluator_slug": { + "type": "string", + "title": "Evaluator Slug" + } + }, + "type": "object", + "required": [ + "worker_slug", + "evaluator_slug" + ], + "title": "ExperimentRunSlotRequest" + }, + "ExperimentSampleSummaryView": { + "properties": { + "sampleId": { "type": "string", "format": "uuid", - "title": "Definition Id" + "title": "Sampleid" }, - "benchmark_type": { + "experimentId": { "type": "string", - "title": "Benchmark Type" + "format": "uuid", + "title": "Experimentid" }, - "instance_key": { + "environmentId": { "type": "string", - "title": "Instance Key" + "format": "uuid", + "title": "Environmentid" + }, + "environmentName": { + "type": "string", + "title": "Environmentname" + }, + "sampleKey": { + "type": "string", + "title": "Samplekey" + }, + "sampleRef": { + "additionalProperties": true, + "type": "object", + "title": "Sampleref" + }, + "sourceMetadata": { + "additionalProperties": true, + "type": "object", + "title": "Sourcemetadata" }, "status": { "type": "string", "title": "Status" }, - "created_at": { + "createdAt": { "type": "string", "format": "date-time", - "title": "Created At" + "title": "Createdat" + } + }, + "type": "object", + "required": [ + "sampleId", + "experimentId", + "environmentId", + "environmentName", + "sampleKey", + "status", + "createdAt" + ], + "title": "ExperimentSampleSummaryView" + }, + "ExperimentSamplesView": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/ExperimentSampleSummaryView" + }, + "type": "array", + "title": "Items" + } + }, + "type": "object", + "title": "ExperimentSamplesView" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "JsonObject": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + "JsonScalar": { + "anyOf": [ + { + "type": "string" }, - "started_at": { + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "JsonValue": { + "anyOf": [ + { + "$ref": "#/components/schemas/JsonScalar" + }, + { + "items": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "array" + }, + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + } + ] + }, + "PollResponse": { + "properties": { + "batch_id": { + "type": "string", + "format": "uuid", + "title": "Batch Id" + }, + "status": { + "$ref": "#/components/schemas/RolloutStatus" + }, + "completed": { + "type": "integer", + "title": "Completed", + "default": 0 + }, + "total": { + "type": "integer", + "title": "Total", + "default": 0 + }, + "trajectories": { + "items": { + "$ref": "#/components/schemas/Trajectory" + }, + "type": "array", + "title": "Trajectories" + }, + "failures": { + "items": { + "$ref": "#/components/schemas/EpisodeFailure" + }, + "type": "array", + "title": "Failures" + } + }, + "type": "object", + "required": [ + "batch_id", + "status" + ], + "title": "PollResponse", + "description": "Ergon \u2192 Trainer: current batch status + trajectories if complete." + }, + "ProviderTokenUsage": { + "properties": { + "prompt_tokens": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "integer" }, { "type": "null" } ], - "title": "Started At" + "title": "Prompt Tokens", + "description": "Input tokens charged or reported by the model provider." }, - "completed_at": { + "completion_tokens": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "integer" }, { "type": "null" } ], - "title": "Completed At" + "title": "Completion Tokens", + "description": "Output tokens charged or reported by the model provider." }, - "evaluator_slug": { + "reasoning_tokens": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Evaluator Slug" + "title": "Reasoning Tokens", + "description": "Reasoning tokens when the provider reports them separately." }, - "model_target": { + "tool_call_tokens": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Model Target" - }, - "worker_team": { - "additionalProperties": true, - "type": "object", - "title": "Worker Team" + "title": "Tool Call Tokens", + "description": "Tool-call argument tokens when reported separately." }, - "seed": { + "tool_result_tokens": { "anyOf": [ { "type": "integer" @@ -1506,9 +1674,10 @@ "type": "null" } ], - "title": "Seed" + "title": "Tool Result Tokens", + "description": "Tool-result tokens when reported separately." }, - "running_time_ms": { + "cached_tokens": { "anyOf": [ { "type": "integer" @@ -1517,173 +1686,295 @@ "type": "null" } ], - "title": "Running Time Ms" + "title": "Cached Tokens", + "description": "Cached/read tokens when reported separately." }, - "final_score": { + "total_tokens": { "anyOf": [ { - "type": "number" + "type": "integer" }, { "type": "null" } ], - "title": "Final Score" + "title": "Total Tokens", + "description": "Provider total when semantic buckets are unavailable." }, - "total_tasks": { + "total_cost_usd": { "anyOf": [ { - "type": "integer" + "type": "number" }, { "type": "null" } ], - "title": "Total Tasks" + "title": "Total Cost Usd", + "description": "Observed provider cost in USD for this generation boundary." + } + }, + "type": "object", + "title": "ProviderTokenUsage", + "description": "Provider-reported token and cost usage for one generation boundary." + }, + "ResetRequest": { + "properties": { + "experiment_prefix": { + "type": "string", + "title": "Experiment Prefix" + } + }, + "type": "object", + "required": [ + "experiment_prefix" + ], + "title": "ResetRequest" + }, + "RolloutBatchSummary": { + "properties": { + "batch_id": { + "type": "string", + "format": "uuid", + "title": "Batch Id" }, - "total_cost_usd": { + "sample_ids": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "Sample Ids" + }, + "status": { + "$ref": "#/components/schemas/RolloutStatus" + }, + "experiment_id": { "anyOf": [ { - "type": "number" + "type": "string", + "format": "uuid" }, { "type": "null" } ], - "title": "Total Cost Usd" + "title": "Experiment Id" }, - "error_message": { + "sampler_invocation_id": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "uuid" }, { "type": "null" } ], - "title": "Error Message" - }, - "metrics": { - "$ref": "#/components/schemas/ExperimentRunMetricsDto" + "title": "Sampler Invocation Id" } }, "type": "object", "required": [ - "sample_id", - "definition_id", - "benchmark_type", - "instance_key", - "status", - "created_at", - "metrics" + "batch_id", + "sample_ids", + "status" ], - "title": "ExperimentRunRowDto" + "title": "RolloutBatchSummary", + "description": "Durable trainer batch membership exposed by sample id." }, - "ExperimentRunSlotRequest": { + "RolloutStatus": { + "type": "string", + "enum": [ + "pending", + "running", + "complete", + "failed", + "cancelled" + ], + "title": "RolloutStatus" + }, + "SampleAnnotationDeletedEventView": { "properties": { - "worker_slug": { + "eventId": { "type": "string", - "title": "Worker Slug" + "format": "uuid", + "title": "Eventid" }, - "evaluator_slug": { + "sampleId": { "type": "string", - "title": "Evaluator Slug" + "format": "uuid", + "title": "Sampleid" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "annotation.deleted", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "title": "Targettype" + }, + "targetId": { + "type": "string", + "format": "uuid", + "title": "Targetid" + }, + "key": { + "type": "string", + "title": "Key" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." } }, "type": "object", "required": [ - "worker_slug", - "evaluator_slug" + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "key" ], - "title": "ExperimentRunSlotRequest" + "title": "SampleAnnotationDeletedEventView" }, - "ExperimentStatusCountsDto": { + "SampleAnnotationSetEventView": { "properties": { - "pending": { - "type": "integer", - "title": "Pending", - "default": 0 + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" }, - "executing": { - "type": "integer", - "title": "Executing", - "default": 0 + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" }, - "evaluating": { - "type": "integer", - "title": "Evaluating", - "default": 0 + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "completed": { - "type": "integer", - "title": "Completed", - "default": 0 + "eventType": { + "type": "string", + "const": "annotation.set", + "title": "Eventtype" }, - "failed": { - "type": "integer", - "title": "Failed", - "default": 0 + "targetType": { + "type": "string", + "title": "Targettype" }, - "cancelled": { - "type": "integer", - "title": "Cancelled", - "default": 0 + "targetId": { + "type": "string", + "format": "uuid", + "title": "Targetid" + }, + "key": { + "type": "string", + "title": "Key" + }, + "value": { + "$ref": "#/components/schemas/JsonObject" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." } }, "type": "object", - "title": "ExperimentStatusCountsDto" + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "key" + ], + "title": "SampleAnnotationSetEventView" }, - "ExperimentSummaryDto": { + "SampleAnnotationUpdatedEventView": { "properties": { - "definition_id": { + "eventId": { "type": "string", "format": "uuid", - "title": "Definition Id" + "title": "Eventid" }, - "name": { + "sampleId": { "type": "string", - "title": "Name" + "format": "uuid", + "title": "Sampleid" }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "benchmark_type": { + "eventType": { "type": "string", - "title": "Benchmark Type" + "const": "annotation.updated", + "title": "Eventtype" }, - "sample_count": { - "type": "integer", - "title": "Sample Count" + "targetType": { + "type": "string", + "title": "Targettype" }, - "status": { + "targetId": { "type": "string", - "title": "Status" + "format": "uuid", + "title": "Targetid" }, - "default_worker_team": { - "additionalProperties": true, - "type": "object", - "title": "Default Worker Team" + "key": { + "type": "string", + "title": "Key" }, - "default_evaluator_slug": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Default Evaluator Slug" + "value": { + "$ref": "#/components/schemas/JsonObject" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "key" + ], + "title": "SampleAnnotationUpdatedEventView" + }, + "SampleCommunicationMessageDto": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "threadId": { + "type": "string", + "title": "Threadid" + }, + "threadTopic": { + "type": "string", + "title": "Threadtopic" + }, + "sampleId": { + "type": "string", + "title": "Sampleid" }, - "default_model_target": { + "taskId": { "anyOf": [ { "type": "string" @@ -1692,9 +1983,9 @@ "type": "null" } ], - "title": "Default Model Target" + "title": "Taskid" }, - "created_by": { + "taskExecutionId": { "anyOf": [ { "type": "string" @@ -1703,293 +1994,1354 @@ "type": "null" } ], - "title": "Created By" + "title": "Taskexecutionid" }, - "created_at": { + "fromAgentId": { "type": "string", - "format": "date-time", - "title": "Created At" + "title": "Fromagentid" }, - "started_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Started At" + "toAgentId": { + "type": "string", + "title": "Toagentid" }, - "completed_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Completed At" + "content": { + "type": "string", + "title": "Content" }, - "run_count": { + "sequenceNum": { "type": "integer", - "title": "Run Count", - "default": 0 + "title": "Sequencenum" }, - "status_counts": { - "$ref": "#/components/schemas/ExperimentStatusCountsDto" + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "threadId", + "threadTopic", + "sampleId", + "fromAgentId", + "toAgentId", + "content", + "sequenceNum", + "createdAt" + ], + "title": "SampleCommunicationMessageDto" + }, + "SampleCommunicationThreadDto": { + "properties": { + "id": { + "type": "string", + "title": "Id" }, - "failure_count": { - "type": "integer", - "title": "Failure Count", - "default": 0 + "sampleId": { + "type": "string", + "title": "Sampleid" }, - "latest_activity_at": { + "taskId": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Latest Activity At" + "title": "Taskid" }, - "average_score": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Average Score" + "topic": { + "type": "string", + "title": "Topic" }, - "average_duration_ms": { + "summary": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Average Duration Ms" + "title": "Summary" }, - "average_tasks": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Average Tasks" + "agentAId": { + "type": "string", + "title": "Agentaid" }, - "total_cost_usd": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Total Cost Usd" - } - }, - "type": "object", + "agentBId": { + "type": "string", + "title": "Agentbid" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + }, + "messages": { + "items": { + "$ref": "#/components/schemas/SampleCommunicationMessageDto" + }, + "type": "array", + "title": "Messages" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "sampleId", + "topic", + "agentAId", + "agentBId", + "createdAt", + "updatedAt" + ], + "title": "SampleCommunicationThreadDto" + }, + "SampleContextEventDto": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" + }, + "taskExecutionId": { + "type": "string", + "format": "uuid", + "title": "Taskexecutionid" + }, + "taskId": { + "type": "string", + "format": "uuid", + "title": "Taskid" + }, + "workerBindingKey": { + "type": "string", + "title": "Workerbindingkey" + }, + "sequence": { + "type": "integer", + "title": "Sequence" + }, + "eventType": { + "type": "string", + "enum": [ + "system_prompt", + "user_message", + "assistant_text", + "tool_call", + "tool_result", + "thinking" + ], + "title": "Eventtype" + }, + "payload": { + "$ref": "#/components/schemas/ContextPartChunkLog" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "startedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Startedat" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completedat" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "sampleId", + "taskExecutionId", + "taskId", + "workerBindingKey", + "sequence", + "eventType", + "payload", + "createdAt" + ], + "title": "SampleContextEventDto" + }, + "SampleDetailView": { + "properties": { + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" + }, + "experimentId": { + "type": "string", + "format": "uuid", + "title": "Experimentid" + }, + "environmentId": { + "type": "string", + "format": "uuid", + "title": "Environmentid" + }, + "environmentName": { + "type": "string", + "title": "Environmentname" + }, + "sampleKey": { + "type": "string", + "title": "Samplekey" + }, + "sampleRef": { + "additionalProperties": true, + "type": "object", + "title": "Sampleref" + }, + "sourceMetadata": { + "additionalProperties": true, + "type": "object", + "title": "Sourcemetadata" + }, + "status": { + "type": "string", + "title": "Status" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "startedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Startedat" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completedat" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "sampleId", + "experimentId", + "environmentId", + "environmentName", + "sampleKey", + "status", + "createdAt" + ], + "title": "SampleDetailView" + }, + "SampleEdgeAddedEventView": { + "properties": { + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" + }, + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "edge.added", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "const": "edge", + "title": "Targettype" + }, + "targetId": { + "type": "string", + "format": "uuid", + "title": "Targetid" + }, + "sourceTaskId": { + "type": "string", + "format": "uuid", + "title": "Sourcetaskid" + }, + "targetTaskId": { + "type": "string", + "format": "uuid", + "title": "Targettaskid" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + }, + "edge": { + "$ref": "#/components/schemas/JsonObject" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "sourceTaskId", + "targetTaskId" + ], + "title": "SampleEdgeAddedEventView" + }, + "SampleEdgeRemovedEventView": { + "properties": { + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" + }, + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "edge.removed", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "const": "edge", + "title": "Targettype" + }, + "targetId": { + "type": "string", + "format": "uuid", + "title": "Targetid" + }, + "sourceTaskId": { + "type": "string", + "format": "uuid", + "title": "Sourcetaskid" + }, + "targetTaskId": { + "type": "string", + "format": "uuid", + "title": "Targettaskid" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "sourceTaskId", + "targetTaskId" + ], + "title": "SampleEdgeRemovedEventView" + }, + "SampleEdgeStatusChangedEventView": { + "properties": { + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" + }, + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "edge.status_changed", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "const": "edge", + "title": "Targettype" + }, + "targetId": { + "type": "string", + "format": "uuid", + "title": "Targetid" + }, + "sourceTaskId": { + "type": "string", + "format": "uuid", + "title": "Sourcetaskid" + }, + "targetTaskId": { + "type": "string", + "format": "uuid", + "title": "Targettaskid" + }, + "status": { + "type": "string", + "title": "Status" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "sourceTaskId", + "targetTaskId", + "status" + ], + "title": "SampleEdgeStatusChangedEventView" + }, + "SampleEvaluationCriterionDto": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "stageNum": { + "type": "integer", + "title": "Stagenum" + }, + "stageName": { + "type": "string", + "title": "Stagename" + }, + "criterionNum": { + "type": "integer", + "title": "Criterionnum" + }, + "criterionSlug": { + "type": "string", + "title": "Criterionslug" + }, + "criterionType": { + "type": "string", + "title": "Criteriontype" + }, + "criterionDescription": { + "type": "string", + "title": "Criteriondescription" + }, + "criterionName": { + "type": "string", + "title": "Criterionname" + }, + "status": { + "type": "string", + "enum": [ + "passed", + "failed", + "errored", + "skipped" + ], + "title": "Status" + }, + "passed": { + "type": "boolean", + "title": "Passed" + }, + "weight": { + "type": "number", + "title": "Weight" + }, + "contribution": { + "type": "number", + "title": "Contribution" + }, + "evaluationInput": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Evaluationinput" + }, + "score": { + "type": "number", + "title": "Score" + }, + "maxScore": { + "type": "number", + "title": "Maxscore" + }, + "feedback": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Feedback" + }, + "modelReasoning": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Modelreasoning" + }, + "skippedReason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Skippedreason" + }, + "evaluatedActionIds": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Evaluatedactionids" + }, + "evaluatedResourceIds": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Evaluatedresourceids" + }, + "observation": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Observation" + }, + "error": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "additionalProperties": false, + "type": "object", "required": [ - "definition_id", - "name", - "benchmark_type", - "sample_count", + "id", + "stageNum", + "stageName", + "criterionNum", + "criterionSlug", + "criterionType", + "criterionDescription", + "criterionName", "status", - "created_at" + "passed", + "weight", + "contribution", + "score", + "maxScore" ], - "title": "ExperimentSummaryDto" + "title": "SampleEvaluationCriterionDto" }, - "HTTPValidationError": { + "SampleEvaluatorAddedEventView": { "properties": { - "detail": { + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" + }, + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "evaluator.added", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "const": "task", + "title": "Targettype" + }, + "targetId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Targetid" + }, + "evaluatorSlug": { + "type": "string", + "title": "Evaluatorslug" + }, + "evaluatorType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Evaluatortype" + }, + "evaluator": { + "$ref": "#/components/schemas/JsonObject" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "evaluatorSlug" + ], + "title": "SampleEvaluatorAddedEventView" + }, + "SampleEvaluatorRemovedEventView": { + "properties": { + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" + }, + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "evaluator.removed", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "const": "task", + "title": "Targettype" + }, + "targetId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Targetid" + }, + "evaluatorSlug": { + "type": "string", + "title": "Evaluatorslug" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "evaluatorSlug" + ], + "title": "SampleEvaluatorRemovedEventView" + }, + "SampleEventsView": { + "properties": { + "items": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/SampleStatusChangedEventView" + }, + { + "$ref": "#/components/schemas/SampleTaskAddedEventView" + }, + { + "$ref": "#/components/schemas/SampleTaskRemovedEventView" + }, + { + "$ref": "#/components/schemas/SampleTaskStatusChangedEventView" + }, + { + "$ref": "#/components/schemas/SampleEdgeAddedEventView" + }, + { + "$ref": "#/components/schemas/SampleEdgeRemovedEventView" + }, + { + "$ref": "#/components/schemas/SampleEdgeStatusChangedEventView" + }, + { + "$ref": "#/components/schemas/SampleWorkerAddedEventView" + }, + { + "$ref": "#/components/schemas/SampleWorkerRemovedEventView" + }, + { + "$ref": "#/components/schemas/SampleEvaluatorAddedEventView" + }, + { + "$ref": "#/components/schemas/SampleEvaluatorRemovedEventView" + }, + { + "$ref": "#/components/schemas/SampleSandboxAddedEventView" + }, + { + "$ref": "#/components/schemas/SampleSandboxRemovedEventView" + }, + { + "$ref": "#/components/schemas/SampleAnnotationSetEventView" + }, + { + "$ref": "#/components/schemas/SampleAnnotationUpdatedEventView" + }, + { + "$ref": "#/components/schemas/SampleAnnotationDeletedEventView" + } + ], + "discriminator": { + "propertyName": "eventType", + "mapping": { + "annotation.deleted": "#/components/schemas/SampleAnnotationDeletedEventView", + "annotation.set": "#/components/schemas/SampleAnnotationSetEventView", + "annotation.updated": "#/components/schemas/SampleAnnotationUpdatedEventView", + "edge.added": "#/components/schemas/SampleEdgeAddedEventView", + "edge.removed": "#/components/schemas/SampleEdgeRemovedEventView", + "edge.status_changed": "#/components/schemas/SampleEdgeStatusChangedEventView", + "evaluator.added": "#/components/schemas/SampleEvaluatorAddedEventView", + "evaluator.removed": "#/components/schemas/SampleEvaluatorRemovedEventView", + "sample.status_changed": "#/components/schemas/SampleStatusChangedEventView", + "sandbox.added": "#/components/schemas/SampleSandboxAddedEventView", + "sandbox.removed": "#/components/schemas/SampleSandboxRemovedEventView", + "task.added": "#/components/schemas/SampleTaskAddedEventView", + "task.removed": "#/components/schemas/SampleTaskRemovedEventView", + "task.status_changed": "#/components/schemas/SampleTaskStatusChangedEventView", + "worker.added": "#/components/schemas/SampleWorkerAddedEventView", + "worker.removed": "#/components/schemas/SampleWorkerRemovedEventView" + } + } + }, + "type": "array", + "title": "Items" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SampleEventsView" + }, + "SampleExecutionAttemptDto": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "taskId": { + "type": "string", + "title": "Taskid" + }, + "attemptNumber": { + "type": "integer", + "title": "Attemptnumber" + }, + "status": { + "type": "string", + "title": "Status" + }, + "startedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Startedat" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completedat" + }, + "finalAssistantMessage": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Finalassistantmessage" + }, + "errorMessage": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Errormessage" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Score" + }, + "agentId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agentid" + }, + "agentName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agentname" + }, + "evaluationDetails": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Evaluationdetails" + }, + "outputResourceIds": { "items": { - "$ref": "#/components/schemas/ValidationError" + "type": "string" }, "type": "array", - "title": "Detail" + "title": "Outputresourceids" } }, + "additionalProperties": false, "type": "object", - "title": "HTTPValidationError" - }, - "JsonObject": { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - }, - "type": "object" + "required": [ + "id", + "taskId", + "attemptNumber", + "status" + ], + "title": "SampleExecutionAttemptDto" }, - "JsonScalar": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" + "SampleGraphEdgeView": { + "properties": { + "edgeId": { + "type": "string", + "format": "uuid", + "title": "Edgeid" }, - { - "type": "number" + "sourceTaskId": { + "type": "string", + "format": "uuid", + "title": "Sourcetaskid" }, - { - "type": "boolean" + "targetTaskId": { + "type": "string", + "format": "uuid", + "title": "Targettaskid" }, - { - "type": "null" - } - ] - }, - "JsonValue": { - "anyOf": [ - { - "$ref": "#/components/schemas/JsonScalar" + "status": { + "type": "string", + "title": "Status" }, - { - "items": { - "$ref": "#/components/schemas/JsonValue" - }, - "type": "array" + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" }, - { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - }, - "type": "object" + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" } - ] + }, + "additionalProperties": false, + "type": "object", + "required": [ + "edgeId", + "sourceTaskId", + "targetTaskId", + "status", + "createdAt", + "updatedAt" + ], + "title": "SampleGraphEdgeView" }, - "PollResponse": { + "SampleGraphNodeView": { "properties": { - "batch_id": { + "taskId": { "type": "string", "format": "uuid", - "title": "Batch Id" + "title": "Taskid" + }, + "taskSlug": { + "type": "string", + "title": "Taskslug" + }, + "description": { + "type": "string", + "title": "Description" }, "status": { - "$ref": "#/components/schemas/RolloutStatus" + "type": "string", + "title": "Status" }, - "completed": { - "type": "integer", - "title": "Completed", - "default": 0 + "parentTaskId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Parenttaskid" }, - "total": { + "level": { "type": "integer", - "title": "Total", + "title": "Level", "default": 0 }, - "trajectories": { + "assignedWorkerSlug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Assignedworkerslug" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "title": "Updatedat" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "taskId", + "taskSlug", + "description", + "status", + "createdAt", + "updatedAt" + ], + "title": "SampleGraphNodeView" + }, + "SampleGraphView": { + "properties": { + "nodes": { "items": { - "$ref": "#/components/schemas/Trajectory" + "$ref": "#/components/schemas/SampleGraphNodeView" }, "type": "array", - "title": "Trajectories" + "title": "Nodes" }, - "failures": { + "edges": { "items": { - "$ref": "#/components/schemas/EpisodeFailure" + "$ref": "#/components/schemas/SampleGraphEdgeView" }, "type": "array", - "title": "Failures" + "title": "Edges" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SampleGraphView" + }, + "SampleResourceDto": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "taskId": { + "type": "string", + "title": "Taskid" + }, + "taskExecutionId": { + "type": "string", + "title": "Taskexecutionid" + }, + "name": { + "type": "string", + "title": "Name" + }, + "mimeType": { + "type": "string", + "title": "Mimetype" + }, + "filePath": { + "type": "string", + "title": "Filepath" + }, + "sizeBytes": { + "type": "integer", + "title": "Sizebytes" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" } }, + "additionalProperties": false, "type": "object", "required": [ - "batch_id", - "status" + "id", + "taskId", + "taskExecutionId", + "name", + "mimeType", + "filePath", + "sizeBytes", + "createdAt" ], - "title": "PollResponse", - "description": "Ergon \u2192 Trainer: current batch status + trajectories if complete." + "title": "SampleResourceDto" }, - "ProviderTokenUsage": { + "SampleSandboxAddedEventView": { "properties": { - "prompt_tokens": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Prompt Tokens", - "description": "Input tokens charged or reported by the model provider." + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" + }, + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "sandbox.added", + "title": "Eventtype" }, - "completion_tokens": { + "targetType": { + "type": "string", + "const": "task", + "title": "Targettype" + }, + "targetId": { "anyOf": [ { - "type": "integer" + "type": "string", + "format": "uuid" }, { "type": "null" } ], - "title": "Completion Tokens", - "description": "Output tokens charged or reported by the model provider." + "title": "Targetid" }, - "reasoning_tokens": { + "sandboxSlug": { + "type": "string", + "title": "Sandboxslug" + }, + "sandboxType": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Reasoning Tokens", - "description": "Reasoning tokens when the provider reports them separately." + "title": "Sandboxtype" }, - "tool_call_tokens": { + "sandbox": { + "$ref": "#/components/schemas/JsonObject" + }, + "actor": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Tool Call Tokens", - "description": "Tool-call argument tokens when reported separately." + "title": "Actor" }, - "tool_result_tokens": { + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "sandboxSlug" + ], + "title": "SampleSandboxAddedEventView" + }, + "SampleSandboxCommandDto": { + "properties": { + "command": { + "type": "string", + "title": "Command" + }, + "stdout": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Tool Result Tokens", - "description": "Tool-result tokens when reported separately." + "title": "Stdout" }, - "cached_tokens": { + "stderr": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Cached Tokens", - "description": "Cached/read tokens when reported separately." + "title": "Stderr" }, - "total_tokens": { + "exitCode": { "anyOf": [ { "type": "integer" @@ -1998,80 +3350,44 @@ "type": "null" } ], - "title": "Total Tokens", - "description": "Provider total when semantic buckets are unavailable." + "title": "Exitcode" }, - "total_cost_usd": { + "durationMs": { "anyOf": [ { - "type": "number" + "type": "integer" }, { "type": "null" } ], - "title": "Total Cost Usd", - "description": "Observed provider cost in USD for this generation boundary." - } - }, - "type": "object", - "title": "ProviderTokenUsage", - "description": "Provider-reported token and cost usage for one generation boundary." - }, - "ResetRequest": { - "properties": { - "experiment_prefix": { + "title": "Durationms" + }, + "timestamp": { "type": "string", - "title": "Experiment Prefix" + "format": "date-time", + "title": "Timestamp" } }, + "additionalProperties": false, "type": "object", "required": [ - "experiment_prefix" - ], - "title": "ResetRequest" - }, - "RolloutStatus": { - "type": "string", - "enum": [ - "pending", - "running", - "complete", - "failed", - "cancelled" + "command", + "timestamp" ], - "title": "RolloutStatus" + "title": "SampleSandboxCommandDto" }, - "SampleCommunicationMessageDto": { + "SampleSandboxDto": { "properties": { - "id": { - "type": "string", - "title": "Id" - }, - "threadId": { - "type": "string", - "title": "Threadid" - }, - "threadTopic": { - "type": "string", - "title": "Threadtopic" - }, - "sampleId": { + "sandboxId": { "type": "string", - "title": "Sampleid" + "title": "Sandboxid" }, "taskId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], + "type": "string", "title": "Taskid" }, - "taskExecutionId": { + "template": { "anyOf": [ { "type": "string" @@ -2080,71 +3396,34 @@ "type": "null" } ], - "title": "Taskexecutionid" - }, - "fromAgentId": { - "type": "string", - "title": "Fromagentid" + "title": "Template" }, - "toAgentId": { - "type": "string", - "title": "Toagentid" + "timeoutMinutes": { + "type": "integer", + "title": "Timeoutminutes" }, - "content": { + "status": { "type": "string", - "title": "Content" - }, - "sequenceNum": { - "type": "integer", - "title": "Sequencenum" + "title": "Status" }, "createdAt": { "type": "string", "format": "date-time", "title": "Createdat" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "id", - "threadId", - "threadTopic", - "sampleId", - "fromAgentId", - "toAgentId", - "content", - "sequenceNum", - "createdAt" - ], - "title": "SampleCommunicationMessageDto" - }, - "SampleCommunicationThreadDto": { - "properties": { - "id": { - "type": "string", - "title": "Id" - }, - "sampleId": { - "type": "string", - "title": "Sampleid" }, - "taskId": { + "closedAt": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Taskid" - }, - "topic": { - "type": "string", - "title": "Topic" + "title": "Closedat" }, - "summary": { + "closeReason": { "anyOf": [ { "type": "string" @@ -2153,347 +3432,272 @@ "type": "null" } ], - "title": "Summary" - }, - "agentAId": { - "type": "string", - "title": "Agentaid" - }, - "agentBId": { - "type": "string", - "title": "Agentbid" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "title": "Createdat" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "title": "Updatedat" + "title": "Closereason" }, - "messages": { + "commands": { "items": { - "$ref": "#/components/schemas/SampleCommunicationMessageDto" + "$ref": "#/components/schemas/SampleSandboxCommandDto" }, "type": "array", - "title": "Messages" + "title": "Commands" } }, "additionalProperties": false, "type": "object", "required": [ - "id", - "sampleId", - "topic", - "agentAId", - "agentBId", - "createdAt", - "updatedAt" + "sandboxId", + "taskId", + "timeoutMinutes", + "status", + "createdAt" ], - "title": "SampleCommunicationThreadDto" + "title": "SampleSandboxDto" }, - "SampleContextEventDto": { + "SampleSandboxRemovedEventView": { "properties": { - "id": { + "eventId": { "type": "string", "format": "uuid", - "title": "Id" + "title": "Eventid" }, "sampleId": { "type": "string", "format": "uuid", "title": "Sampleid" }, - "taskExecutionId": { - "type": "string", - "format": "uuid", - "title": "Taskexecutionid" - }, - "taskId": { - "type": "string", - "format": "uuid", - "title": "Taskid" - }, - "workerBindingKey": { + "timestamp": { "type": "string", - "title": "Workerbindingkey" - }, - "sequence": { - "type": "integer", - "title": "Sequence" + "format": "date-time", + "title": "Timestamp" }, "eventType": { "type": "string", - "enum": [ - "system_prompt", - "user_message", - "assistant_text", - "tool_call", - "tool_result", - "thinking" - ], - "title": "Eventtype" - }, - "payload": { - "$ref": "#/components/schemas/ContextPartChunkLog" + "const": "sandbox.removed", + "title": "Eventtype" }, - "createdAt": { + "targetType": { "type": "string", - "format": "date-time", - "title": "Createdat" + "const": "task", + "title": "Targettype" }, - "startedAt": { + "targetId": { "anyOf": [ { "type": "string", - "format": "date-time" + "format": "uuid" }, { "type": "null" } ], - "title": "Startedat" + "title": "Targetid" }, - "completedAt": { + "sandboxSlug": { + "type": "string", + "title": "Sandboxslug" + }, + "actor": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Completedat" + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." } }, - "additionalProperties": false, "type": "object", "required": [ - "id", + "eventId", "sampleId", - "taskExecutionId", - "taskId", - "workerBindingKey", - "sequence", + "timestamp", "eventType", - "payload", - "createdAt" + "targetType", + "sandboxSlug" ], - "title": "SampleContextEventDto" + "title": "SampleSandboxRemovedEventView" }, - "SampleEvaluationCriterionDto": { + "SampleSnapshotDto": { "properties": { "id": { "type": "string", "title": "Id" }, - "stageNum": { - "type": "integer", - "title": "Stagenum" - }, - "stageName": { - "type": "string", - "title": "Stagename" - }, - "criterionNum": { - "type": "integer", - "title": "Criterionnum" + "definitionId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Definitionid" }, - "criterionSlug": { + "name": { "type": "string", - "title": "Criterionslug" + "title": "Name" }, - "criterionType": { + "status": { "type": "string", - "title": "Criteriontype" + "title": "Status" }, - "criterionDescription": { - "type": "string", - "title": "Criteriondescription" + "tasks": { + "additionalProperties": { + "$ref": "#/components/schemas/SampleTaskDto" + }, + "type": "object", + "title": "Tasks" }, - "criterionName": { + "rootTaskId": { "type": "string", - "title": "Criterionname" + "title": "Roottaskid", + "default": "" }, - "status": { - "type": "string", - "enum": [ - "passed", - "failed", - "errored", - "skipped" - ], - "title": "Status" + "resourcesByTask": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/SampleResourceDto" + }, + "type": "array" + }, + "type": "object", + "title": "Resourcesbytask" }, - "passed": { - "type": "boolean", - "title": "Passed" + "executionsByTask": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/SampleExecutionAttemptDto" + }, + "type": "array" + }, + "type": "object", + "title": "Executionsbytask" }, - "weight": { - "type": "number", - "title": "Weight" + "evaluationsByTask": { + "additionalProperties": { + "$ref": "#/components/schemas/SampleTaskEvaluationDto" + }, + "type": "object", + "title": "Evaluationsbytask" }, - "contribution": { - "type": "number", - "title": "Contribution" + "sandboxesByTask": { + "additionalProperties": { + "$ref": "#/components/schemas/SampleSandboxDto" + }, + "type": "object", + "title": "Sandboxesbytask" }, - "evaluationInput": { - "anyOf": [ - { - "type": "string" + "contextEventsByTask": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/SampleContextEventDto" }, - { - "type": "null" - } - ], - "title": "Evaluationinput" - }, - "score": { - "type": "number", - "title": "Score" + "type": "array" + }, + "type": "object", + "title": "Contexteventsbytask" }, - "maxScore": { - "type": "number", - "title": "Maxscore" + "threads": { + "items": { + "$ref": "#/components/schemas/SampleCommunicationThreadDto" + }, + "type": "array", + "title": "Threads" }, - "feedback": { + "startedAt": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Feedback" + "title": "Startedat" }, - "modelReasoning": { + "completedAt": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Modelreasoning" + "title": "Completedat" }, - "skippedReason": { + "durationSeconds": { "anyOf": [ { - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Skippedreason" - }, - "evaluatedActionIds": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Evaluatedactionids" + "title": "Durationseconds" }, - "evaluatedResourceIds": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Evaluatedresourceids" + "totalTasks": { + "type": "integer", + "title": "Totaltasks", + "default": 0 }, - "observation": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Observation" + "totalLeafTasks": { + "type": "integer", + "title": "Totalleaftasks", + "default": 0 }, - "error": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Error" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "id", - "stageNum", - "stageName", - "criterionNum", - "criterionSlug", - "criterionType", - "criterionDescription", - "criterionName", - "status", - "passed", - "weight", - "contribution", - "score", - "maxScore" - ], - "title": "SampleEvaluationCriterionDto" - }, - "SampleExecutionAttemptDto": { - "properties": { - "id": { - "type": "string", - "title": "Id" + "completedTasks": { + "type": "integer", + "title": "Completedtasks", + "default": 0 }, - "taskId": { - "type": "string", - "title": "Taskid" + "failedTasks": { + "type": "integer", + "title": "Failedtasks", + "default": 0 }, - "attemptNumber": { + "runningTasks": { "type": "integer", - "title": "Attemptnumber" + "title": "Runningtasks", + "default": 0 }, - "status": { - "type": "string", - "title": "Status" + "cancelledTasks": { + "type": "integer", + "title": "Cancelledtasks", + "default": 0 }, - "startedAt": { + "finalScore": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "number" }, { "type": "null" } ], - "title": "Startedat" + "title": "Finalscore" }, - "completedAt": { + "metrics": { "anyOf": [ { - "type": "string", - "format": "date-time" + "$ref": "#/components/schemas/SampleSnapshotMetricsDto" }, { "type": "null" } - ], - "title": "Completedat" + ] }, - "finalAssistantMessage": { + "error": { "anyOf": [ { "type": "string" @@ -2502,274 +3706,254 @@ "type": "null" } ], - "title": "Finalassistantmessage" + "title": "Error" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "name", + "status" + ], + "title": "SampleSnapshotDto" + }, + "SampleSnapshotMetricsDto": { + "properties": { + "sampleId": { + "type": "string", + "title": "Sampleid" }, - "errorMessage": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Errormessage" + "status": { + "type": "string", + "title": "Status" }, - "score": { + "durationMs": { "anyOf": [ { - "type": "number" + "type": "integer" }, { "type": "null" } ], - "title": "Score" + "title": "Durationms" }, - "agentId": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Agentid" + "totalTasks": { + "type": "integer", + "title": "Totaltasks", + "default": 0 }, - "agentName": { + "toolCallCount": { + "type": "integer", + "title": "Toolcallcount", + "default": 0 + }, + "totalTokens": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Agentname" + "title": "Totaltokens" }, - "evaluationDetails": { + "tokenBreakdown": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "Tokenbreakdown" + }, + "totalCostUsd": { "anyOf": [ { - "additionalProperties": true, - "type": "object" + "type": "number" }, { "type": "null" } ], - "title": "Evaluationdetails" + "title": "Totalcostusd" }, - "outputResourceIds": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Outputresourceids" + "costObserved": { + "type": "boolean", + "title": "Costobserved", + "default": false } }, "additionalProperties": false, "type": "object", "required": [ - "id", - "taskId", - "attemptNumber", + "sampleId", "status" ], - "title": "SampleExecutionAttemptDto" + "title": "SampleSnapshotMetricsDto" }, - "SampleResourceDto": { + "SampleStatusChangedEventView": { "properties": { - "id": { + "eventId": { "type": "string", - "title": "Id" + "format": "uuid", + "title": "Eventid" }, - "taskId": { + "sampleId": { "type": "string", - "title": "Taskid" + "format": "uuid", + "title": "Sampleid" }, - "taskExecutionId": { + "timestamp": { "type": "string", - "title": "Taskexecutionid" + "format": "date-time", + "title": "Timestamp" }, - "name": { + "eventType": { "type": "string", - "title": "Name" + "const": "sample.status_changed", + "title": "Eventtype" }, - "mimeType": { + "targetType": { "type": "string", - "title": "Mimetype" + "const": "sample", + "title": "Targettype" }, - "filePath": { + "targetId": { "type": "string", - "title": "Filepath" - }, - "sizeBytes": { - "type": "integer", - "title": "Sizebytes" + "format": "uuid", + "title": "Targetid" }, - "createdAt": { + "status": { "type": "string", - "format": "date-time", - "title": "Createdat" + "title": "Status" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." } }, - "additionalProperties": false, "type": "object", "required": [ - "id", - "taskId", - "taskExecutionId", - "name", - "mimeType", - "filePath", - "sizeBytes", - "createdAt" + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "status" ], - "title": "SampleResourceDto" + "title": "SampleStatusChangedEventView" }, - "SampleRuntimeEventView": { + "SampleSummaryDto": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, - "sample_id": { + "name": { "type": "string", - "format": "uuid", - "title": "Sample Id" + "title": "Name" }, - "event_timestamp": { + "status": { "type": "string", - "format": "date-time", - "title": "Event Timestamp" + "title": "Status" }, - "table": { - "type": "string", - "enum": [ - "sample_status_events", - "sample_task_events", - "sample_edge_events", - "sample_worker_events", - "sample_evaluator_events", - "sample_sandbox_events", - "sample_annotation_events" + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } ], - "title": "Table" - }, - "event_type": { - "type": "string", - "title": "Event Type" - }, - "target_type": { - "type": "string", - "title": "Target Type" + "title": "Created At" }, - "target_id": { + "started_at": { "anyOf": [ { "type": "string", - "format": "uuid" + "format": "date-time" }, { "type": "null" } ], - "title": "Target Id" + "title": "Started At" }, - "payload": { - "$ref": "#/components/schemas/JsonObject", - "description": "Typed event payload copied from the source sample runtime WAL row." - } - }, - "type": "object", - "required": [ - "id", - "sample_id", - "event_timestamp", - "table", - "event_type", - "target_type", - "target_id" - ], - "title": "SampleRuntimeEventView" - }, - "SampleSandboxCommandDto": { - "properties": { - "command": { - "type": "string", - "title": "Command" + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completed At" }, - "stdout": { + "latest_activity_at": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Stdout" + "title": "Latest Activity At" }, - "stderr": { + "duration_seconds": { "anyOf": [ { - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Stderr" + "title": "Duration Seconds" }, - "exitCode": { + "definition_id": { "anyOf": [ { - "type": "integer" + "type": "string", + "format": "uuid" }, { "type": "null" } ], - "title": "Exitcode" + "title": "Definition Id" }, - "durationMs": { + "definition_name": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Durationms" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "command", - "timestamp" - ], - "title": "SampleSandboxCommandDto" - }, - "SampleSandboxDto": { - "properties": { - "sandboxId": { - "type": "string", - "title": "Sandboxid" - }, - "taskId": { - "type": "string", - "title": "Taskid" + "title": "Definition Name" }, - "template": { + "experiment": { "anyOf": [ { "type": "string" @@ -2778,34 +3962,32 @@ "type": "null" } ], - "title": "Template" - }, - "timeoutMinutes": { - "type": "integer", - "title": "Timeoutminutes" + "title": "Experiment" }, - "status": { + "benchmark_type": { "type": "string", - "title": "Status" + "title": "Benchmark Type" }, - "createdAt": { + "instance_key": { "type": "string", - "format": "date-time", - "title": "Createdat" + "title": "Instance Key" }, - "closedAt": { + "sample_id": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Closedat" + "title": "Sample Id" }, - "closeReason": { + "sample_label": { + "type": "string", + "title": "Sample Label" + }, + "evaluator_slug": { "anyOf": [ { "type": "string" @@ -2814,133 +3996,31 @@ "type": "null" } ], - "title": "Closereason" - }, - "commands": { - "items": { - "$ref": "#/components/schemas/SampleSandboxCommandDto" - }, - "type": "array", - "title": "Commands" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "sandboxId", - "taskId", - "timeoutMinutes", - "status", - "createdAt" - ], - "title": "SampleSandboxDto" - }, - "SampleSnapshotDto": { - "properties": { - "id": { - "type": "string", - "title": "Id" - }, - "definitionId": { - "type": "string", - "title": "Definitionid" - }, - "name": { - "type": "string", - "title": "Name" - }, - "status": { - "type": "string", - "title": "Status" - }, - "tasks": { - "additionalProperties": { - "$ref": "#/components/schemas/SampleTaskDto" - }, - "type": "object", - "title": "Tasks" - }, - "rootTaskId": { - "type": "string", - "title": "Roottaskid", - "default": "" - }, - "resourcesByTask": { - "additionalProperties": { - "items": { - "$ref": "#/components/schemas/SampleResourceDto" - }, - "type": "array" - }, - "type": "object", - "title": "Resourcesbytask" - }, - "executionsByTask": { - "additionalProperties": { - "items": { - "$ref": "#/components/schemas/SampleExecutionAttemptDto" - }, - "type": "array" - }, - "type": "object", - "title": "Executionsbytask" - }, - "evaluationsByTask": { - "additionalProperties": { - "$ref": "#/components/schemas/SampleTaskEvaluationDto" - }, - "type": "object", - "title": "Evaluationsbytask" - }, - "sandboxesByTask": { - "additionalProperties": { - "$ref": "#/components/schemas/SampleSandboxDto" - }, - "type": "object", - "title": "Sandboxesbytask" - }, - "contextEventsByTask": { - "additionalProperties": { - "items": { - "$ref": "#/components/schemas/SampleContextEventDto" - }, - "type": "array" - }, - "type": "object", - "title": "Contexteventsbytask" - }, - "threads": { - "items": { - "$ref": "#/components/schemas/SampleCommunicationThreadDto" - }, - "type": "array", - "title": "Threads" + "title": "Evaluator Slug" }, - "startedAt": { + "model_target": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Startedat" + "title": "Model Target" }, - "completedAt": { + "final_score": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "number" }, { "type": "null" } ], - "title": "Completedat" + "title": "Final Score" }, - "durationSeconds": { + "return": { "anyOf": [ { "type": "number" @@ -2949,39 +4029,34 @@ "type": "null" } ], - "title": "Durationseconds" - }, - "totalTasks": { - "type": "integer", - "title": "Totaltasks", - "default": 0 + "title": "Return" }, - "totalLeafTasks": { + "total_tasks": { "type": "integer", - "title": "Totalleaftasks", + "title": "Total Tasks", "default": 0 }, - "completedTasks": { + "completed_tasks": { "type": "integer", - "title": "Completedtasks", + "title": "Completed Tasks", "default": 0 }, - "failedTasks": { + "failed_tasks": { "type": "integer", - "title": "Failedtasks", + "title": "Failed Tasks", "default": 0 }, - "runningTasks": { + "running_tasks": { "type": "integer", - "title": "Runningtasks", + "title": "Running Tasks", "default": 0 }, - "cancelledTasks": { + "cancelled_tasks": { "type": "integer", - "title": "Cancelledtasks", + "title": "Cancelled Tasks", "default": 0 }, - "finalScore": { + "total_cost_usd": { "anyOf": [ { "type": "number" @@ -2990,19 +4065,9 @@ "type": "null" } ], - "title": "Finalscore" - }, - "metrics": { - "anyOf": [ - { - "$ref": "#/components/schemas/SampleSnapshotMetricsDto" - }, - { - "type": "null" - } - ] + "title": "Total Cost Usd" }, - "error": { + "error_message": { "anyOf": [ { "type": "string" @@ -3011,145 +4076,183 @@ "type": "null" } ], - "title": "Error" + "title": "Error Message" + }, + "metrics": { + "additionalProperties": true, + "type": "object", + "title": "Metrics" } }, - "additionalProperties": false, "type": "object", "required": [ "id", - "definitionId", "name", - "status" + "status", + "benchmark_type", + "instance_key", + "sample_label" ], - "title": "SampleSnapshotDto" + "title": "SampleSummaryDto" }, - "SampleSnapshotMetricsDto": { + "SampleTaskAddedEventView": { "properties": { + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" + }, "sampleId": { "type": "string", + "format": "uuid", "title": "Sampleid" }, - "status": { + "timestamp": { "type": "string", - "title": "Status" + "format": "date-time", + "title": "Timestamp" }, - "durationMs": { + "eventType": { + "type": "string", + "const": "task.added", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "const": "task", + "title": "Targettype" + }, + "targetId": { + "type": "string", + "format": "uuid", + "title": "Targetid" + }, + "taskSlug": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Durationms" - }, - "totalTasks": { - "type": "integer", - "title": "Totaltasks", - "default": 0 + "title": "Taskslug" }, - "toolCallCount": { - "type": "integer", - "title": "Toolcallcount", - "default": 0 - }, - "totalTokens": { + "status": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Totaltokens" + "title": "Status" }, - "tokenBreakdown": { - "additionalProperties": { - "type": "integer" - }, - "type": "object", - "title": "Tokenbreakdown" + "task": { + "$ref": "#/components/schemas/JsonObject" }, - "totalCostUsd": { + "actor": { "anyOf": [ { - "type": "number" + "type": "string" }, { "type": "null" } ], - "title": "Totalcostusd" + "title": "Actor" }, - "costObserved": { - "type": "boolean", - "title": "Costobserved", - "default": false + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." } }, - "additionalProperties": false, "type": "object", "required": [ + "eventId", "sampleId", - "status" + "timestamp", + "eventType", + "targetType", + "targetId" ], - "title": "SampleSnapshotMetricsDto" + "title": "SampleTaskAddedEventView" }, - "SampleSummaryDto": { + "SampleTaskDto": { "properties": { "id": { "type": "string", - "format": "uuid", "title": "Id" }, "name": { "type": "string", "title": "Name" }, + "description": { + "type": "string", + "title": "Description" + }, "status": { "type": "string", "title": "Status" }, - "created_at": { + "parentId": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Created At" + "title": "Parentid" }, - "started_at": { + "childIds": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Childids" + }, + "dependsOnIds": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dependsonids" + }, + "isLeaf": { + "type": "boolean", + "title": "Isleaf" + }, + "level": { + "type": "integer", + "title": "Level" + }, + "assignedWorkerId": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Started At" + "title": "Assignedworkerid" }, - "completed_at": { + "assignedWorkerSlug": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Completed At" + "title": "Assignedworkerslug" }, - "latest_activity_at": { + "startedAt": { "anyOf": [ { "type": "string", @@ -3159,36 +4262,45 @@ "type": "null" } ], - "title": "Latest Activity At" + "title": "Startedat" }, - "duration_seconds": { + "completedAt": { "anyOf": [ { - "type": "number" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Duration Seconds" - }, - "definition_id": { + "title": "Completedat" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "name", + "description", + "status", + "isLeaf", + "level" + ], + "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." + }, + "SampleTaskEvaluationDto": { + "properties": { + "id": { "type": "string", - "format": "uuid", - "title": "Definition Id" + "title": "Id" }, - "definition_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Definition Name" + "sampleId": { + "type": "string", + "title": "Sampleid" }, - "experiment": { + "taskId": { "anyOf": [ { "type": "string" @@ -3197,43 +4309,37 @@ "type": "null" } ], - "title": "Experiment" + "title": "Taskid" }, - "benchmark_type": { + "evaluatorName": { "type": "string", - "title": "Benchmark Type" + "title": "Evaluatorname" }, - "instance_key": { + "aggregationRule": { "type": "string", - "title": "Instance Key" + "title": "Aggregationrule" }, - "sample_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Sample Id" + "totalScore": { + "type": "number", + "title": "Totalscore" }, - "sample_label": { - "type": "string", - "title": "Sample Label" + "maxScore": { + "type": "number", + "title": "Maxscore" }, - "evaluator_slug": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Evaluator Slug" + "normalizedScore": { + "type": "number", + "title": "Normalizedscore" }, - "model_target": { + "stagesEvaluated": { + "type": "integer", + "title": "Stagesevaluated" + }, + "stagesPassed": { + "type": "integer", + "title": "Stagespassed" + }, + "failedGate": { "anyOf": [ { "type": "string" @@ -3242,67 +4348,81 @@ "type": "null" } ], - "title": "Model Target" + "title": "Failedgate" }, - "final_score": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Final Score" + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Createdat" }, - "return": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Return" + "criterionResults": { + "items": { + "$ref": "#/components/schemas/SampleEvaluationCriterionDto" + }, + "type": "array", + "title": "Criterionresults" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "sampleId", + "evaluatorName", + "aggregationRule", + "totalScore", + "maxScore", + "normalizedScore", + "stagesEvaluated", + "stagesPassed", + "createdAt" + ], + "title": "SampleTaskEvaluationDto" + }, + "SampleTaskRemovedEventView": { + "properties": { + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" }, - "total_tasks": { - "type": "integer", - "title": "Total Tasks", - "default": 0 + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" }, - "completed_tasks": { - "type": "integer", - "title": "Completed Tasks", - "default": 0 + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "failed_tasks": { - "type": "integer", - "title": "Failed Tasks", - "default": 0 + "eventType": { + "type": "string", + "const": "task.removed", + "title": "Eventtype" }, - "running_tasks": { - "type": "integer", - "title": "Running Tasks", - "default": 0 + "targetType": { + "type": "string", + "const": "task", + "title": "Targettype" }, - "cancelled_tasks": { - "type": "integer", - "title": "Cancelled Tasks", - "default": 0 + "targetId": { + "type": "string", + "format": "uuid", + "title": "Targetid" }, - "total_cost_usd": { + "taskSlug": { "anyOf": [ { - "type": "number" + "type": "string" }, { "type": "null" } ], - "title": "Total Cost Usd" + "title": "Taskslug" }, - "error_message": { + "actor": { "anyOf": [ { "type": "string" @@ -3311,45 +4431,72 @@ "type": "null" } ], - "title": "Error Message" + "title": "Actor" }, - "metrics": { - "additionalProperties": true, - "type": "object", - "title": "Metrics" + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." } }, "type": "object", "required": [ - "id", - "name", - "status", - "definition_id", - "benchmark_type", - "instance_key", - "sample_label" + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId" ], - "title": "SampleSummaryDto" + "title": "SampleTaskRemovedEventView" }, - "SampleTaskDto": { + "SampleTaskStatusChangedEventView": { "properties": { - "id": { + "eventId": { "type": "string", - "title": "Id" + "format": "uuid", + "title": "Eventid" }, - "name": { + "sampleId": { "type": "string", - "title": "Name" + "format": "uuid", + "title": "Sampleid" }, - "description": { + "timestamp": { "type": "string", - "title": "Description" + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "task.status_changed", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "const": "task", + "title": "Targettype" + }, + "targetId": { + "type": "string", + "format": "uuid", + "title": "Targetid" + }, + "taskSlug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Taskslug" }, "status": { "type": "string", "title": "Status" }, - "parentId": { + "actor": { "anyOf": [ { "type": "string" @@ -3358,42 +4505,69 @@ "type": "null" } ], - "title": "Parentid" + "title": "Actor" }, - "childIds": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Childids" + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "targetId", + "status" + ], + "title": "SampleTaskStatusChangedEventView" + }, + "SampleWorkerAddedEventView": { + "properties": { + "eventId": { + "type": "string", + "format": "uuid", + "title": "Eventid" }, - "dependsOnIds": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Dependsonids" + "sampleId": { + "type": "string", + "format": "uuid", + "title": "Sampleid" }, - "isLeaf": { - "type": "boolean", - "title": "Isleaf" + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "level": { - "type": "integer", - "title": "Level" + "eventType": { + "type": "string", + "const": "worker.added", + "title": "Eventtype" }, - "assignedWorkerId": { + "targetType": { + "type": "string", + "const": "task", + "title": "Targettype" + }, + "targetId": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "uuid" }, { "type": "null" } ], - "title": "Assignedworkerid" + "title": "Targetid" }, - "assignedWorkerSlug": { + "workerSlug": { + "type": "string", + "title": "Workerslug" + }, + "workerType": { "anyOf": [ { "type": "string" @@ -3402,96 +4576,93 @@ "type": "null" } ], - "title": "Assignedworkerslug" + "title": "Workertype" }, - "startedAt": { + "modelTarget": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Startedat" + "title": "Modeltarget" }, - "completedAt": { + "worker": { + "$ref": "#/components/schemas/JsonObject" + }, + "actor": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Completedat" + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." } }, - "additionalProperties": false, "type": "object", "required": [ - "id", - "name", - "description", - "status", - "isLeaf", - "level" + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "workerSlug" ], - "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." + "title": "SampleWorkerAddedEventView" }, - "SampleTaskEvaluationDto": { + "SampleWorkerRemovedEventView": { "properties": { - "id": { + "eventId": { "type": "string", - "title": "Id" + "format": "uuid", + "title": "Eventid" }, "sampleId": { "type": "string", + "format": "uuid", "title": "Sampleid" }, - "taskId": { + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "eventType": { + "type": "string", + "const": "worker.removed", + "title": "Eventtype" + }, + "targetType": { + "type": "string", + "const": "task", + "title": "Targettype" + }, + "targetId": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "uuid" }, { "type": "null" } ], - "title": "Taskid" - }, - "evaluatorName": { - "type": "string", - "title": "Evaluatorname" + "title": "Targetid" }, - "aggregationRule": { + "workerSlug": { "type": "string", - "title": "Aggregationrule" - }, - "totalScore": { - "type": "number", - "title": "Totalscore" - }, - "maxScore": { - "type": "number", - "title": "Maxscore" - }, - "normalizedScore": { - "type": "number", - "title": "Normalizedscore" - }, - "stagesEvaluated": { - "type": "integer", - "title": "Stagesevaluated" - }, - "stagesPassed": { - "type": "integer", - "title": "Stagespassed" + "title": "Workerslug" }, - "failedGate": { + "actor": { "anyOf": [ { "type": "string" @@ -3500,36 +4671,81 @@ "type": "null" } ], - "title": "Failedgate" + "title": "Actor" + }, + "payload": { + "$ref": "#/components/schemas/JsonObject", + "description": "Raw typed WAL payload retained for diagnostics and forward compatibility." + } + }, + "type": "object", + "required": [ + "eventId", + "sampleId", + "timestamp", + "eventType", + "targetType", + "workerSlug" + ], + "title": "SampleWorkerRemovedEventView" + }, + "SamplerInvocationView": { + "properties": { + "samplerInvocationId": { + "type": "string", + "format": "uuid", + "title": "Samplerinvocationid" + }, + "samplerName": { + "type": "string", + "title": "Samplername" + }, + "requestedK": { + "type": "integer", + "title": "Requestedk" + }, + "candidatePoolSize": { + "type": "integer", + "title": "Candidatepoolsize" + }, + "selectedCount": { + "type": "integer", + "title": "Selectedcount" + }, + "samplerConfig": { + "additionalProperties": true, + "type": "object", + "title": "Samplerconfig" }, "createdAt": { "type": "string", "format": "date-time", "title": "Createdat" - }, - "criterionResults": { - "items": { - "$ref": "#/components/schemas/SampleEvaluationCriterionDto" - }, - "type": "array", - "title": "Criterionresults" } }, - "additionalProperties": false, "type": "object", "required": [ - "id", - "sampleId", - "evaluatorName", - "aggregationRule", - "totalScore", - "maxScore", - "normalizedScore", - "stagesEvaluated", - "stagesPassed", + "samplerInvocationId", + "samplerName", + "requestedK", + "candidatePoolSize", + "selectedCount", "createdAt" ], - "title": "SampleTaskEvaluationDto" + "title": "SamplerInvocationView" + }, + "SamplerInvocationsView": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/SamplerInvocationView" + }, + "type": "array", + "title": "Items" + } + }, + "type": "object", + "title": "SamplerInvocationsView" }, "SeedRunRequest": { "properties": { diff --git a/ergon-dashboard/src/lib/contracts/events.ts b/ergon-dashboard/src/lib/contracts/events.ts index 0c0840751..3831cfeee 100644 --- a/ergon-dashboard/src/lib/contracts/events.ts +++ b/ergon-dashboard/src/lib/contracts/events.ts @@ -20,6 +20,7 @@ import { parseSampleSandboxCommand, parseSampleSnapshot, parseSampleTaskEvaluation, + SampleRuntimeEventView, SampleCommunicationMessageSchema, SampleCommunicationThreadSchema, SampleResourceSchema, @@ -310,23 +311,23 @@ export const DashboardGraphMutationDataSchema = z.preprocess((input) => { if (outer.mutation !== undefined) { return outer.mutation; } - const event = outer.event === undefined ? outer : GeneratedDashboardSampleRuntimeEventSchema.parse(input).event; - const targetType = event.target_type === "task" ? "node" : event.target_type; - const mutationType = event.event_type + const event = (outer.event === undefined ? outer : GeneratedDashboardSampleRuntimeEventSchema.parse(input).event) as SampleRuntimeEventView; + const targetType = event.targetType === "task" ? "node" : event.targetType; + const mutationType = event.eventType .replace("task.", "node.") .replace("sample.", "node."); return { - id: event.id, - sample_id: event.sample_id, + id: event.eventId, + sample_id: event.sampleId, sequence: 0, mutation_type: mutationType, target_type: targetType, - target_id: event.target_id ?? event.sample_id, + target_id: event.targetId ?? event.sampleId, actor: "typed-sample-wal", old_value: null, new_value: event.payload, reason: null, - created_at: event.event_timestamp, + created_at: event.timestamp, }; }, GraphMutationDtoSchema); diff --git a/ergon-dashboard/src/lib/contracts/rest.ts b/ergon-dashboard/src/lib/contracts/rest.ts index 710cbeb4d..c6127f590 100644 --- a/ergon-dashboard/src/lib/contracts/rest.ts +++ b/ergon-dashboard/src/lib/contracts/rest.ts @@ -6,7 +6,11 @@ export const BenchmarkNameSchema = z.string(); export const SampleStatusSchema = z.enum(["pending", "executing", "evaluating", "completed", "failed", "cancelled"]); export const TaskStatusSchema = z.string(); -export const ExperimentDetailSchema = schemas.ExperimentDetailDto; +const restSchemas = schemas as typeof schemas & { + ExperimentDetailDto?: z.ZodTypeAny; +}; + +export const ExperimentDetailSchema = restSchemas.ExperimentDetailDto ?? schemas.ExperimentDetailView; export const SampleExecutionAttemptSchema = schemas.SampleExecutionAttemptDto; export const SampleResourceSchema = schemas.SampleResourceDto; @@ -29,9 +33,9 @@ export type BenchmarkName = z.infer; export type SampleLifecycleStatus = z.infer; export type TaskStatusValue = z.infer; -type RawExperimentDetail = KnownKeys>; -type RawExperimentRunRow = KnownKeys[number]>; -type RawExperimentSummary = KnownKeys; +type RawExperimentDetail = Record; +type RawExperimentRunRow = Record; +type RawExperimentSummary = Record; type RawSampleExecutionAttempt = KnownKeys>; type RawSampleResource = KnownKeys>; type RawSampleSandboxCommand = KnownKeys>; @@ -337,9 +341,10 @@ function normalizeSampleTaskEvaluation(evaluation: RawSampleTaskEvaluation): Sam } export function parseExperimentDetail(input: unknown): ExperimentDetail { - const detail = ExperimentDetailSchema.parse(input); + const detail = ExperimentDetailSchema.parse(input) as RawExperimentDetail; return { ...detail, + experiment: detail.experiment ?? {}, analytics: { total_runs: detail.analytics?.total_runs ?? 0, average_duration_ms: detail.analytics?.average_duration_ms ?? null, @@ -357,7 +362,7 @@ export function parseExperimentDetail(input: unknown): ExperimentDetail { }, total_cost_usd: detail.analytics?.total_cost_usd ?? null, }, - runs: (detail.runs ?? []).map((run) => ({ + runs: ((detail.runs ?? []) as RawExperimentRunRow[]).map((run) => ({ ...run, completed_at: run.completed_at ?? null, error_message: run.error_message ?? null, diff --git a/ergon-dashboard/src/lib/sample-state/hydrate.ts b/ergon-dashboard/src/lib/sample-state/hydrate.ts index aac48a5ed..e3d862718 100644 --- a/ergon-dashboard/src/lib/sample-state/hydrate.ts +++ b/ergon-dashboard/src/lib/sample-state/hydrate.ts @@ -101,13 +101,13 @@ export function hydrateSampleSnapshot(input: unknown): SampleWorkspaceState { return { id: data.id, - definitionId: data.definitionId, + definitionId: data.definitionId ?? "", name: data.name, status: data.status as SampleWorkspaceState["status"], tasks: new Map( Object.entries(data.tasks ?? {}).map(([taskId, task]) => [taskId, deserializeTask(task)]), ), - rootTaskId: data.rootTaskId, + rootTaskId: data.rootTaskId ?? "", resourcesByTask: new Map( Object.entries(data.resourcesByTask ?? {}).map(([taskId, resources]) => [ taskId, diff --git a/ergon-dashboard/tests/contracts/sample-rest-contract.test.ts b/ergon-dashboard/tests/contracts/sample-rest-contract.test.ts index 263d57709..5bd1072f3 100644 --- a/ergon-dashboard/tests/contracts/sample-rest-contract.test.ts +++ b/ergon-dashboard/tests/contracts/sample-rest-contract.test.ts @@ -2,6 +2,12 @@ import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import test from "node:test"; +import { schemas } from "../../src/generated/rest/contracts"; + +const sampleId = "00000000-0000-4000-8000-000000000001"; +const taskId = "00000000-0000-4000-8000-000000000002"; +const edgeId = "00000000-0000-4000-8000-000000000003"; + test("sample detail no longer exposes generic mutation proxy route", () => { assert.equal(existsSync("src/app/api/samples/[sampleId]/mutations/route.ts"), false); }); @@ -22,3 +28,37 @@ test("generated REST contract does not expose sample mutations", () => { assert.doesNotMatch(openapi, /GraphMutationRecordDto/); assert.doesNotMatch(contracts, /GraphMutationRecordDto/); }); + +test("generated REST contract exposes sample runtime events as a discriminated union", () => { + const contracts = readFileSync("src/generated/rest/contracts.ts", "utf8"); + + assert.match(contracts, /const SampleRuntimeEventView = z\.discriminatedUnion\("eventType"/); + assert.match(contracts, /eventType: z\.literal\("task\.added"\)/); + assert.match(contracts, /eventType: z\.literal\("edge\.added"\)/); + + const parsed = schemas.SampleRuntimeEventView.parse({ + eventId: edgeId, + sampleId, + timestamp: "2026-05-27T12:00:00Z", + eventType: "edge.added", + targetType: "edge", + targetId: edgeId, + sourceTaskId: taskId, + targetTaskId: "00000000-0000-4000-8000-000000000004", + status: "pending", + payload: {}, + }); + + assert.equal(parsed.eventType, "edge.added"); + assert.throws(() => + schemas.SampleRuntimeEventView.parse({ + eventId: edgeId, + sampleId, + timestamp: "2026-05-27T12:00:00Z", + eventType: "not.real", + targetType: "edge", + targetId: edgeId, + payload: {}, + }), + ); +}); 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 de5a5a489..94f0c9f13 100644 --- a/ergon_core/ergon_core/core/application/runtime/task_management.py +++ b/ergon_core/ergon_core/core/application/runtime/task_management.py @@ -19,7 +19,7 @@ from ergon_core.core.application.ports import DashboardEventPublisher from ergon_core.core.application.samples.events import ( SampleRuntimeEventRow, - sample_runtime_event_from_row, + sample_runtime_event_view_from_row, ) from ergon_core.core.persistence.graph.models import SampleGraphNode from ergon_core.core.application.runtime.status import ( @@ -105,7 +105,7 @@ async def _publish_runtime_event(self, row: SampleRuntimeEventRow) -> None: if self._dashboard_publisher is None: return await self._dashboard_publisher.publish( - DashboardSampleRuntimeEvent(event=sample_runtime_event_from_row(row)) + DashboardSampleRuntimeEvent(event=sample_runtime_event_view_from_row(row)) ) # ── spawn_dynamic_task ─────────────────────────────────── diff --git a/ergon_core/ergon_core/core/application/samples/event_views.py b/ergon_core/ergon_core/core/application/samples/event_views.py new file mode 100644 index 000000000..6214bfd16 --- /dev/null +++ b/ergon_core/ergon_core/core/application/samples/event_views.py @@ -0,0 +1,437 @@ +"""Canonical public views for typed sample runtime WAL events.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Annotated, Literal, get_args +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + +from ergon_core.core.application.samples.events import ( + SampleAnnotationEventKind, + SampleEdgeEventKind, + SampleEvaluatorEventKind, + SampleSandboxEventKind, + SampleStatusEventKind, + SampleTaskEventKind, + SampleWorkerEventKind, +) +from ergon_core.core.persistence.samples.models import ( + SampleAnnotationEventRow, + SampleEdgeEventRow, + SampleEvaluatorEventRow, + SampleSandboxEventRow, + SampleStatusEventRow, + SampleTaskEventRow, + SampleWorkerEventRow, +) +from ergon_core.core.shared.json_types import JsonObject + + +ALL_SAMPLE_RUNTIME_EVENT_TYPES = { + "sample.status_changed", + "task.added", + "task.removed", + "task.status_changed", + "edge.added", + "edge.removed", + "edge.status_changed", + "worker.added", + "worker.removed", + "evaluator.added", + "evaluator.removed", + "sandbox.added", + "sandbox.removed", + "annotation.set", + "annotation.updated", + "annotation.deleted", +} +RAW_PAYLOAD_DESCRIPTION = ( + "Raw typed WAL payload retained for diagnostics and forward compatibility." +) + +ROW_MODEL_EVENT_TYPES: set[str] = set().union( + get_args(SampleStatusEventKind), + get_args(SampleTaskEventKind), + get_args(SampleEdgeEventKind), + get_args(SampleWorkerEventKind), + get_args(SampleEvaluatorEventKind), + get_args(SampleSandboxEventKind), + get_args(SampleAnnotationEventKind), +) + + +def _to_camel(value: str) -> str: + head, *tail = value.split("_") + return head + "".join(part.capitalize() for part in tail) + + +class SampleEventBase(BaseModel): + model_config = ConfigDict(alias_generator=_to_camel, populate_by_name=True) + + event_id: UUID + sample_id: UUID + timestamp: datetime + + +class SampleStatusChangedEventView(SampleEventBase): + event_type: Literal["sample.status_changed"] + target_type: Literal["sample"] + target_id: UUID + status: str + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleTaskAddedEventView(SampleEventBase): + event_type: Literal["task.added"] + target_type: Literal["task"] + target_id: UUID + task_slug: str | None = None + status: str | None = None + task: JsonObject = Field(default_factory=dict) + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleTaskRemovedEventView(SampleEventBase): + event_type: Literal["task.removed"] + target_type: Literal["task"] + target_id: UUID + task_slug: str | None = None + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleTaskStatusChangedEventView(SampleEventBase): + event_type: Literal["task.status_changed"] + target_type: Literal["task"] + target_id: UUID + task_slug: str | None = None + status: str + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleEdgeAddedEventView(SampleEventBase): + event_type: Literal["edge.added"] + target_type: Literal["edge"] + target_id: UUID + source_task_id: UUID + target_task_id: UUID + status: str | None = None + edge: JsonObject = Field(default_factory=dict) + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleEdgeRemovedEventView(SampleEventBase): + event_type: Literal["edge.removed"] + target_type: Literal["edge"] + target_id: UUID + source_task_id: UUID + target_task_id: UUID + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleEdgeStatusChangedEventView(SampleEventBase): + event_type: Literal["edge.status_changed"] + target_type: Literal["edge"] + target_id: UUID + source_task_id: UUID + target_task_id: UUID + status: str + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleWorkerAddedEventView(SampleEventBase): + event_type: Literal["worker.added"] + target_type: Literal["task"] + target_id: UUID | None = None + worker_slug: str + worker_type: str | None = None + model_target: str | None = None + worker: JsonObject = Field(default_factory=dict) + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleWorkerRemovedEventView(SampleEventBase): + event_type: Literal["worker.removed"] + target_type: Literal["task"] + target_id: UUID | None = None + worker_slug: str + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleEvaluatorAddedEventView(SampleEventBase): + event_type: Literal["evaluator.added"] + target_type: Literal["task"] + target_id: UUID | None = None + evaluator_slug: str + evaluator_type: str | None = None + evaluator: JsonObject = Field(default_factory=dict) + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleEvaluatorRemovedEventView(SampleEventBase): + event_type: Literal["evaluator.removed"] + target_type: Literal["task"] + target_id: UUID | None = None + evaluator_slug: str + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleSandboxAddedEventView(SampleEventBase): + event_type: Literal["sandbox.added"] + target_type: Literal["task"] + target_id: UUID | None = None + sandbox_slug: str + sandbox_type: str | None = None + sandbox: JsonObject = Field(default_factory=dict) + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleSandboxRemovedEventView(SampleEventBase): + event_type: Literal["sandbox.removed"] + target_type: Literal["task"] + target_id: UUID | None = None + sandbox_slug: str + actor: str | None = None + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleAnnotationSetEventView(SampleEventBase): + event_type: Literal["annotation.set"] + target_type: str + target_id: UUID + key: str + value: JsonObject = Field(default_factory=dict) + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleAnnotationUpdatedEventView(SampleEventBase): + event_type: Literal["annotation.updated"] + target_type: str + target_id: UUID + key: str + value: JsonObject = Field(default_factory=dict) + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +class SampleAnnotationDeletedEventView(SampleEventBase): + event_type: Literal["annotation.deleted"] + target_type: str + target_id: UUID + key: str + payload: JsonObject = Field(default_factory=dict, description=RAW_PAYLOAD_DESCRIPTION) + + +SampleRuntimeEventView = Annotated[ + SampleStatusChangedEventView + | SampleTaskAddedEventView + | SampleTaskRemovedEventView + | SampleTaskStatusChangedEventView + | SampleEdgeAddedEventView + | SampleEdgeRemovedEventView + | SampleEdgeStatusChangedEventView + | SampleWorkerAddedEventView + | SampleWorkerRemovedEventView + | SampleEvaluatorAddedEventView + | SampleEvaluatorRemovedEventView + | SampleSandboxAddedEventView + | SampleSandboxRemovedEventView + | SampleAnnotationSetEventView + | SampleAnnotationUpdatedEventView + | SampleAnnotationDeletedEventView, + Field(discriminator="event_type"), +] +VIEW_EVENT_TYPES = ALL_SAMPLE_RUNTIME_EVENT_TYPES + + +SampleRuntimeEventRow = ( + SampleStatusEventRow + | SampleTaskEventRow + | SampleEdgeEventRow + | SampleWorkerEventRow + | SampleEvaluatorEventRow + | SampleSandboxEventRow + | SampleAnnotationEventRow +) + + +EventViewFields = dict[str, object] + + +def _event_common(row: SampleRuntimeEventRow) -> EventViewFields: + return { + "event_id": row.id, + "sample_id": row.sample_id, + "event_type": row.event_type, + "timestamp": row.event_timestamp, + "payload": dict(row.payload_json), + } + + +def _task_event_from_row( + row: SampleTaskEventRow, + common: EventViewFields, +) -> SampleTaskAddedEventView | SampleTaskRemovedEventView | SampleTaskStatusChangedEventView: + fields = dict( + common, + target_type="task", + target_id=row.task_id, + task_slug=row.task_slug, + actor=row.actor, + ) + if row.event_type == "task.added": + return SampleTaskAddedEventView( + **fields, + status=row.status, + task=row.task_snapshot_json, + ) + if row.event_type == "task.removed": + return SampleTaskRemovedEventView(**fields) + return SampleTaskStatusChangedEventView(**fields, status=str(row.status)) + + +def _edge_event_from_row( + row: SampleEdgeEventRow, + common: EventViewFields, +) -> SampleEdgeAddedEventView | SampleEdgeRemovedEventView | SampleEdgeStatusChangedEventView: + fields = dict( + common, + target_type="edge", + target_id=row.edge_id, + source_task_id=row.source_task_id, + target_task_id=row.target_task_id, + actor=row.actor, + ) + if row.event_type == "edge.added": + return SampleEdgeAddedEventView( + **fields, + status=row.status, + edge=row.edge_snapshot_json, + ) + if row.event_type == "edge.removed": + return SampleEdgeRemovedEventView(**fields) + return SampleEdgeStatusChangedEventView(**fields, status=str(row.status)) + + +def _worker_event_from_row( + row: SampleWorkerEventRow, + common: EventViewFields, +) -> SampleWorkerAddedEventView | SampleWorkerRemovedEventView: + fields = dict( + common, + target_type="task", + target_id=row.task_id, + worker_slug=row.worker_slug, + actor=row.actor, + ) + if row.event_type == "worker.added": + return SampleWorkerAddedEventView( + **fields, + worker_type=row.worker_type, + model_target=row.model_target, + worker=row.worker_snapshot_json, + ) + return SampleWorkerRemovedEventView(**fields) + + +def _evaluator_event_from_row( + row: SampleEvaluatorEventRow, + common: EventViewFields, +) -> SampleEvaluatorAddedEventView | SampleEvaluatorRemovedEventView: + fields = dict( + common, + target_type="task", + target_id=row.task_id, + evaluator_slug=row.evaluator_slug, + actor=row.actor, + ) + if row.event_type == "evaluator.added": + return SampleEvaluatorAddedEventView( + **fields, + evaluator_type=row.evaluator_type, + evaluator=row.evaluator_snapshot_json, + ) + return SampleEvaluatorRemovedEventView(**fields) + + +def _sandbox_event_from_row( + row: SampleSandboxEventRow, + common: EventViewFields, +) -> SampleSandboxAddedEventView | SampleSandboxRemovedEventView: + fields = dict( + common, + target_type="task", + target_id=row.task_id, + sandbox_slug=row.sandbox_slug, + actor=row.actor, + ) + if row.event_type == "sandbox.added": + return SampleSandboxAddedEventView( + **fields, + sandbox_type=row.sandbox_type, + sandbox=row.sandbox_snapshot_json, + ) + return SampleSandboxRemovedEventView(**fields) + + +def _annotation_event_from_row( + row: SampleAnnotationEventRow, + common: EventViewFields, +) -> ( + SampleAnnotationSetEventView + | SampleAnnotationUpdatedEventView + | SampleAnnotationDeletedEventView +): + value = row.payload_json.get("value") + fields = dict( + common, + target_type=row.target_type, + target_id=row.target_id, + key=row.key, + ) + if row.event_type == "annotation.set": + return SampleAnnotationSetEventView( + **fields, + value=value if isinstance(value, dict) else {}, + ) + if row.event_type == "annotation.updated": + return SampleAnnotationUpdatedEventView( + **fields, + value=value if isinstance(value, dict) else {}, + ) + return SampleAnnotationDeletedEventView(**fields) + + +def sample_runtime_event_from_row(row: SampleRuntimeEventRow) -> SampleRuntimeEventView: + common = _event_common(row) + if isinstance(row, SampleStatusEventRow): + return SampleStatusChangedEventView( + **common, + target_type="sample", + target_id=row.sample_id, + status=row.status, + actor=row.actor, + ) + if isinstance(row, SampleTaskEventRow): + return _task_event_from_row(row, common) + if isinstance(row, SampleEdgeEventRow): + return _edge_event_from_row(row, common) + if isinstance(row, SampleWorkerEventRow): + return _worker_event_from_row(row, common) + if isinstance(row, SampleEvaluatorEventRow): + return _evaluator_event_from_row(row, common) + if isinstance(row, SampleSandboxEventRow): + return _sandbox_event_from_row(row, common) + return _annotation_event_from_row(row, common) diff --git a/ergon_core/ergon_core/core/application/samples/events.py b/ergon_core/ergon_core/core/application/samples/events.py index 8f8c6ff9c..6f407a340 100644 --- a/ergon_core/ergon_core/core/application/samples/events.py +++ b/ergon_core/ergon_core/core/application/samples/events.py @@ -1,7 +1,9 @@ -"""Thin append helper and view DTOs for typed sample runtime WAL rows.""" +"""Thin append helper for typed sample runtime WAL rows.""" + +from __future__ import annotations from datetime import datetime -from typing import Literal, Protocol +from typing import TYPE_CHECKING, Literal, Protocol from uuid import UUID from ergon_core.core.persistence.samples.models import ( @@ -15,9 +17,11 @@ ) from ergon_core.core.shared.json_types import JsonObject from ergon_core.core.shared.utils import utcnow -from pydantic import BaseModel, Field from sqlmodel import Session, select +if TYPE_CHECKING: + from ergon_core.core.application.samples.event_views import SampleRuntimeEventView + SampleStatusEventKind = Literal["sample.status_changed"] SampleTaskEventKind = Literal["task.added", "task.removed", "task.status_changed"] SampleEdgeEventKind = Literal["edge.added", "edge.removed", "edge.status_changed"] @@ -41,30 +45,6 @@ ) -class SampleRuntimeEventView(BaseModel): - model_config = {"frozen": True} - - id: UUID - sample_id: UUID - event_timestamp: datetime - table: Literal[ - "sample_status_events", - "sample_task_events", - "sample_edge_events", - "sample_worker_events", - "sample_evaluator_events", - "sample_sandbox_events", - "sample_annotation_events", - ] - event_type: str - target_type: str - target_id: UUID | None - payload: JsonObject = Field( - default_factory=dict, - description="Typed event payload copied from the source sample runtime WAL row.", - ) - - class SampleRuntimeEventSubscriber(Protocol): async def __call__(self, event: SampleRuntimeEventRow) -> None: ... @@ -138,63 +118,16 @@ def list_events(self, session: Session, sample_id: UUID) -> list[SampleRuntimeEv for model in _EVENT_MODELS: rows.extend(session.exec(select(model).where(model.sample_id == sample_id)).all()) rows.sort(key=lambda row: (row.event_timestamp, row.id)) - return [sample_runtime_event_from_row(row) for row in rows] - - -def sample_runtime_event_from_row(row: SampleRuntimeEventRow) -> SampleRuntimeEventView: - table = row.__tablename__ - target_type, target_id = _target_for_row(row) - payload = _payload_for_row(row) - return SampleRuntimeEventView( - id=row.id, - sample_id=row.sample_id, - event_timestamp=row.event_timestamp, - table=table, - event_type=row.event_type, - target_type=target_type, - target_id=target_id, - payload=payload, - ) + return [sample_runtime_event_view_from_row(row) for row in rows] -def _target_for_row(row: SampleRuntimeEventRow) -> tuple[str, UUID | None]: - if isinstance(row, SampleStatusEventRow): - return "sample", row.sample_id - if isinstance(row, SampleTaskEventRow): - return "task", row.task_id - if isinstance(row, SampleEdgeEventRow): - return "edge", row.edge_id - if isinstance(row, (SampleWorkerEventRow, SampleEvaluatorEventRow, SampleSandboxEventRow)): - return "task", row.task_id - return row.target_type, row.target_id - - -def _payload_for_row(row: SampleRuntimeEventRow) -> JsonObject: - payload = dict(row.payload_json) - if isinstance(row, SampleStatusEventRow): - payload.setdefault("status", row.status) - elif isinstance(row, SampleTaskEventRow): - if row.task_slug is not None: - payload.setdefault("task_slug", row.task_slug) - if row.status is not None: - payload.setdefault("status", row.status) - payload.setdefault("task", row.task_snapshot_json) - elif isinstance(row, SampleEdgeEventRow): - if row.status is not None: - payload.setdefault("status", row.status) - payload.setdefault("source_task_id", str(row.source_task_id)) - payload.setdefault("target_task_id", str(row.target_task_id)) - payload.setdefault("edge", row.edge_snapshot_json) - elif isinstance(row, SampleWorkerEventRow): - payload.setdefault("worker", row.worker_snapshot_json) - payload.setdefault("worker_slug", row.worker_slug) - elif isinstance(row, SampleEvaluatorEventRow): - payload.setdefault("evaluator", row.evaluator_snapshot_json) - payload.setdefault("evaluator_slug", row.evaluator_slug) - elif isinstance(row, SampleSandboxEventRow): - payload.setdefault("sandbox", row.sandbox_snapshot_json) - payload.setdefault("sandbox_slug", row.sandbox_slug) - return payload +def sample_runtime_event_view_from_row(row: SampleRuntimeEventRow) -> SampleRuntimeEventView: + """Convert a typed WAL row into the public sample runtime event view.""" + from ergon_core.core.application.samples.event_views import ( + sample_runtime_event_from_row, + ) + + return sample_runtime_event_from_row(row) _EVENT_MODELS = ( diff --git a/ergon_core/ergon_core/core/infrastructure/http/routes/experiments.py b/ergon_core/ergon_core/core/infrastructure/http/routes/experiments.py index 1ce978ad6..fd8af84c6 100644 --- a/ergon_core/ergon_core/core/infrastructure/http/routes/experiments.py +++ b/ergon_core/ergon_core/core/infrastructure/http/routes/experiments.py @@ -6,8 +6,10 @@ run_experiment as _run_experiment, ) from ergon_core.core.views.experiments.models import ( - ExperimentDetailDto, - ExperimentSummaryDto, + ExperimentDetailView, + ExperimentListView, + ExperimentSamplesView, + SamplerInvocationsView, ) from ergon_core.core.views.experiments.service import ExperimentReadService from ergon_core.core.application.experiments.models import ( @@ -19,19 +21,35 @@ router = APIRouter(prefix="/experiments", tags=["experiments"]) -@router.get("", response_model=list[ExperimentSummaryDto]) -def list_experiments(limit: int = 50) -> list[ExperimentSummaryDto]: - return ExperimentReadService().list_experiments(limit=limit) +@router.get("", response_model=ExperimentListView) +def list_experiments(limit: int = 50) -> ExperimentListView: + return ExperimentReadService().list_experiment_states(limit=limit) -@router.get("/{definition_id}", response_model=ExperimentDetailDto) -def get_experiment(definition_id: UUID) -> ExperimentDetailDto: - detail = ExperimentReadService().get_experiment(definition_id) +@router.get("/{experiment_id}", response_model=ExperimentDetailView) +def get_experiment(experiment_id: UUID) -> ExperimentDetailView: + detail = ExperimentReadService().get_experiment_state(experiment_id) if detail is None: - raise HTTPException(status_code=404, detail=f"Experiment {definition_id} not found") + raise HTTPException(status_code=404, detail=f"Experiment {experiment_id} not found") return detail +@router.get("/{experiment_id}/samples", response_model=ExperimentSamplesView) +def get_experiment_samples(experiment_id: UUID) -> ExperimentSamplesView: + samples = ExperimentReadService().list_experiment_samples(experiment_id) + if samples is None: + raise HTTPException(status_code=404, detail=f"Experiment {experiment_id} not found") + return samples + + +@router.get("/{experiment_id}/sampler-invocations", response_model=SamplerInvocationsView) +def get_experiment_sampler_invocations(experiment_id: UUID) -> SamplerInvocationsView: + invocations = ExperimentReadService().list_sampler_invocations(experiment_id) + if invocations is None: + raise HTTPException(status_code=404, detail=f"Experiment {experiment_id} not found") + return invocations + + @router.post("/{definition_id}/run", response_model=ExperimentRunResult, status_code=202) async def run_experiment( definition_id: UUID, request: ExperimentRunRequest | None = None diff --git a/ergon_core/ergon_core/core/infrastructure/http/routes/samples.py b/ergon_core/ergon_core/core/infrastructure/http/routes/samples.py index b574dc01b..d0161be7f 100644 --- a/ergon_core/ergon_core/core/infrastructure/http/routes/samples.py +++ b/ergon_core/ergon_core/core/infrastructure/http/routes/samples.py @@ -3,12 +3,14 @@ from uuid import UUID from ergon_core.core.views.samples.models import ( - SampleSummaryDto, + SampleDetailView, + SampleEventsView, + SampleGraphView, SampleSnapshotDto, + SampleSummaryDto, ) -from ergon_core.core.application.samples.events import SampleRuntimeEventView from ergon_core.core.views.errors import ResourceTooLargeError -from ergon_core.core.views.samples.service import SampleSnapshotReadService +from ergon_core.core.views.samples.service import SampleReadService, SampleSnapshotReadService from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse @@ -33,24 +35,42 @@ def list_samples( ) -@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.""" +@router.get("/{sample_id}/workspace", response_model=SampleSnapshotDto) +def get_sample_workspace(sample_id: UUID) -> SampleSnapshotDto: + """Get the existing dashboard-compatible sample snapshot contract.""" 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}/events", response_model=list[SampleRuntimeEventView]) -def get_sample_runtime_events(sample_id: UUID) -> list[SampleRuntimeEventView]: +@router.get("/{sample_id}", response_model=SampleDetailView) +def get_sample_detail(sample_id: UUID) -> SampleDetailView: + """Get persisted sample provenance and status details.""" + detail = SampleReadService().get_sample_detail(sample_id) + if detail is None: + raise HTTPException(status_code=404, detail=f"Sample {sample_id} not found") + return detail + + +@router.get("/{sample_id}/events", response_model=SampleEventsView) +def get_sample_runtime_events(sample_id: UUID) -> SampleEventsView: """Return the typed append-only runtime event stream for a sample.""" - events = SampleSnapshotReadService().list_events(sample_id) + events = SampleReadService().list_sample_events(sample_id) if events is None: raise HTTPException(status_code=404, detail=f"Sample {sample_id} not found") return events +@router.get("/{sample_id}/graph", response_model=SampleGraphView) +def get_sample_graph(sample_id: UUID) -> SampleGraphView: + """Return the sample graph projection.""" + graph = SampleReadService().get_sample_graph(sample_id) + if graph is None: + raise HTTPException(status_code=404, detail=f"Sample {sample_id} not found") + return graph + + @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.""" 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 278b8cc16..3ecedd25e 100644 --- a/ergon_core/ergon_core/core/views/dashboard_events/contracts.py +++ b/ergon_core/ergon_core/core/views/dashboard_events/contracts.py @@ -20,7 +20,7 @@ from ergon_core.core.shared.context_parts import ContextEventType, ContextPartChunkLog from ergon_core.core.application.events.base import InngestEventContract from ergon_core.core.application.runtime.status import NodeStatus -from ergon_core.core.application.samples.events import SampleRuntimeEventView +from ergon_core.core.application.samples.event_views import SampleRuntimeEventView from pydantic import Field # --------------------------------------------------------------------------- diff --git a/ergon_core/ergon_core/core/views/experiments/models.py b/ergon_core/ergon_core/core/views/experiments/models.py index 3ec272149..6a986fc19 100644 --- a/ergon_core/ergon_core/core/views/experiments/models.py +++ b/ergon_core/ergon_core/core/views/experiments/models.py @@ -3,7 +3,16 @@ from datetime import datetime from uuid import UUID -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +def _to_camel(value: str) -> str: + head, *tail = value.split("_") + return head + "".join(part.capitalize() for part in tail) + + +class CamelModel(BaseModel): + model_config = ConfigDict(alias_generator=_to_camel, populate_by_name=True) class ExperimentStatusCountsDto(BaseModel): @@ -123,3 +132,58 @@ class ExperimentTagDefinitionDto(BaseModel): name: str benchmark_type: str latest_run_status: str | None = None + + +class SamplerInvocationView(CamelModel): + sampler_invocation_id: UUID + sampler_name: str + requested_k: int + candidate_pool_size: int + selected_count: int + sampler_config: dict = Field(default_factory=dict) + created_at: datetime + + +class EnvironmentContributionView(CamelModel): + environment_id: UUID + environment_name: str + source_mode: str + sample_count: int + selected_count: int + source_metadata: dict = Field(default_factory=dict) + + +class ExperimentSampleSummaryView(CamelModel): + sample_id: UUID + experiment_id: UUID + environment_id: UUID + environment_name: str + sample_key: str + sample_ref: dict = Field(default_factory=dict) + source_metadata: dict = Field(default_factory=dict) + status: str + created_at: datetime + + +class ExperimentDetailView(CamelModel): + experiment_id: UUID + name: str + description: str | None = None + environments: list[EnvironmentContributionView] = Field(default_factory=list) + sample_count: int + samples: list[ExperimentSampleSummaryView] = Field(default_factory=list) + sampler_invocations: list[SamplerInvocationView] = Field(default_factory=list) + metadata: dict = Field(default_factory=dict) + created_at: datetime + + +class ExperimentListView(CamelModel): + items: list[ExperimentDetailView] = Field(default_factory=list) + + +class ExperimentSamplesView(CamelModel): + items: list[ExperimentSampleSummaryView] = Field(default_factory=list) + + +class SamplerInvocationsView(CamelModel): + items: list[SamplerInvocationView] = Field(default_factory=list) diff --git a/ergon_core/ergon_core/core/views/experiments/service.py b/ergon_core/ergon_core/core/views/experiments/service.py index 3c6101d06..ec2e9646e 100644 --- a/ergon_core/ergon_core/core/views/experiments/service.py +++ b/ergon_core/ergon_core/core/views/experiments/service.py @@ -1,22 +1,35 @@ """Read service for experiment API views.""" from datetime import datetime +from contextlib import AbstractContextManager from uuid import UUID from ergon_core.core.views.experiments.models import ( + EnvironmentContributionView, ExperimentAnalyticsDto, ExperimentDetailDto, + ExperimentDetailView, + ExperimentListView, ExperimentRunMetricsDto, ExperimentRunRowDto, + ExperimentSampleSummaryView, + ExperimentSamplesView, ExperimentStatusCountsDto, ExperimentSummaryDto, ExperimentTagDefinitionDto, + SamplerInvocationView, + SamplerInvocationsView, ) from ergon_core.core.persistence.context.models import SampleContextEvent from ergon_core.core.persistence.definitions.models import ( ExperimentDefinition, ExperimentDefinitionInstance, ) +from ergon_core.core.persistence.experiments.models import ( + ExperimentEnvironmentRow, + ExperimentRow, + ExperimentSamplerInvocationRow, +) 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 SampleRecord @@ -27,6 +40,66 @@ class ExperimentReadService: """List/show queries for persisted benchmark definitions.""" + def __init__(self, session: Session | None = None) -> None: + self._session = session + + def list_experiment_states(self, *, limit: int = 50) -> ExperimentListView: + with self._session_scope() as session: + rows = list( + session.exec( + select(ExperimentRow) + .order_by(col(ExperimentRow.created_at).desc()) + .limit(limit) + ).all() + ) + return ExperimentListView( + items=[_experiment_state(session, row, include_samples=False) for row in rows] + ) + + def get_experiment_state(self, experiment_id: UUID) -> ExperimentDetailView | None: + with self._session_scope() as session: + row = session.get(ExperimentRow, experiment_id) + if row is None: + return None + return _experiment_state(session, row, include_samples=True) + + def list_experiment_samples(self, experiment_id: UUID) -> ExperimentSamplesView | None: + with self._session_scope() as session: + if session.get(ExperimentRow, experiment_id) is None: + return None + environment_names = _environment_names(session, experiment_id) + samples = list( + session.exec( + select(SampleRecord) + .where(SampleRecord.experiment_id == experiment_id) + .order_by(col(SampleRecord.created_at).desc()) + ).all() + ) + return ExperimentSamplesView( + items=[ + _sample_summary_view(sample, environment_names=environment_names) + for sample in samples + ] + ) + + def list_sampler_invocations(self, experiment_id: UUID) -> SamplerInvocationsView | None: + with self._session_scope() as session: + if session.get(ExperimentRow, experiment_id) is None: + return None + rows = list( + session.exec( + select(ExperimentSamplerInvocationRow) + .where(ExperimentSamplerInvocationRow.experiment_id == experiment_id) + .order_by(col(ExperimentSamplerInvocationRow.created_at).desc()) + ).all() + ) + return SamplerInvocationsView(items=[_sampler_invocation_view(row) for row in rows]) + + def _session_scope(self) -> AbstractContextManager[Session]: + if self._session is not None: + return _ExistingSessionScope(self._session) + return get_session() + def list_experiments(self, *, limit: int = 50) -> list[ExperimentSummaryDto]: with get_session() as session: definitions = list( @@ -359,6 +432,120 @@ def _experiment_lifecycle_status( return fallback +class _ExistingSessionScope: + def __init__(self, session: Session) -> None: + self._session = session + + def __enter__(self) -> Session: + return self._session + + def __exit__(self, *args: object) -> None: + return None + + +def _experiment_state( + session: Session, + row: ExperimentRow, + *, + include_samples: bool, +) -> ExperimentDetailView: + environments = list( + session.exec( + select(ExperimentEnvironmentRow).where(ExperimentEnvironmentRow.experiment_id == row.id) + ).all() + ) + environment_names = {environment.id: environment.name for environment in environments} + sample_rows = list( + session.exec(select(SampleRecord).where(SampleRecord.experiment_id == row.id)).all() + ) + selected_by_environment: dict[UUID, int] = {} + for sample in sample_rows: + if sample.environment_id is not None: + selected_by_environment[sample.environment_id] = ( + selected_by_environment.get(sample.environment_id, 0) + 1 + ) + sampler_rows = list( + session.exec( + select(ExperimentSamplerInvocationRow).where( + ExperimentSamplerInvocationRow.experiment_id == row.id + ) + ).all() + ) + return ExperimentDetailView( + experiment_id=row.id, + name=row.name, + description=row.description, + environments=[ + EnvironmentContributionView( + environment_id=environment.id, + environment_name=environment.name, + source_mode=environment.source_mode, + sample_count=selected_by_environment.get(environment.id, 0), + selected_count=selected_by_environment.get(environment.id, 0), + source_metadata=environment.source_metadata_json, + ) + for environment in environments + ], + sample_count=len(sample_rows), + samples=[ + _sample_summary_view(sample, environment_names=environment_names) + for sample in sample_rows + if include_samples + ], + sampler_invocations=[_sampler_invocation_view(invocation) for invocation in sampler_rows], + metadata=row.metadata_json, + created_at=row.created_at, + ) + + +def _environment_names(session: Session, experiment_id: UUID) -> dict[UUID, str]: + rows = session.exec( + select(ExperimentEnvironmentRow).where( + ExperimentEnvironmentRow.experiment_id == experiment_id + ) + ).all() + return {row.id: row.name for row in rows} + + +def _sample_summary_view( + sample: SampleRecord, + *, + environment_names: dict[UUID, str], +) -> ExperimentSampleSummaryView: + if sample.experiment_id is None or sample.environment_id is None: + raise ValueError("Sample is missing experiment/environment provenance") + environment_name = environment_names.get(sample.environment_id) + if environment_name is None: + raise ValueError( + f"Sample {sample.id} points at missing environment {sample.environment_id}" + ) + assignment = sample.parsed_assignment() + source_metadata = assignment.get("source_metadata", {}) + return ExperimentSampleSummaryView( + sample_id=sample.id, + experiment_id=sample.experiment_id, + environment_id=sample.environment_id, + environment_name=environment_name, + sample_key=sample.sample_key or sample.instance_key, + sample_ref=sample.sample_ref_json, + source_metadata=source_metadata if isinstance(source_metadata, dict) else {}, + status=str(sample.status), + created_at=sample.created_at, + ) + + +def _sampler_invocation_view(row: ExperimentSamplerInvocationRow) -> SamplerInvocationView: + return SamplerInvocationView( + sampler_invocation_id=row.id, + sampler_name=row.sampler_name, + requested_k=row.requested_k, + candidate_pool_size=row.candidate_pool_size, + selected_count=row.selected_count, + sampler_config=row.sampler_config_json, + created_at=row.created_at, + ) + + def _average(values: list[float] | list[int]) -> float | None: if not values: return None diff --git a/ergon_core/ergon_core/core/views/samples/models.py b/ergon_core/ergon_core/core/views/samples/models.py index 8813d7746..f186ad02b 100644 --- a/ergon_core/ergon_core/core/views/samples/models.py +++ b/ergon_core/ergon_core/core/views/samples/models.py @@ -10,6 +10,9 @@ from uuid import UUID from ergon_core.core.application.evaluation.summary import EvalCriterionStatus +from ergon_core.core.application.samples.event_views import ( + SampleRuntimeEventView as SampleEventView, +) from ergon_core.core.shared.context_parts import ContextEventType, ContextPartChunkLog from pydantic import BaseModel, ConfigDict, Field @@ -250,3 +253,57 @@ class SampleSummaryDto(BaseModel): total_cost_usd: float | None = None error_message: str | None = None metrics: dict[str, Any] = Field(default_factory=dict) + + +class SampleDetailView(CamelModel): + sample_id: UUID + experiment_id: UUID + environment_id: UUID + environment_name: str + sample_key: str + sample_ref: dict[str, Any] = Field(default_factory=dict) + source_metadata: dict[str, Any] = Field(default_factory=dict) + status: str + created_at: datetime + started_at: datetime | None = None + completed_at: datetime | None = None + + +class SampleGraphNodeView(CamelModel): + task_id: UUID + task_slug: str + description: str + status: str + parent_task_id: UUID | None = None + level: int = 0 + assigned_worker_slug: str | None = None + created_at: datetime + updated_at: datetime + + +class SampleGraphEdgeView(CamelModel): + edge_id: UUID + source_task_id: UUID + target_task_id: UUID + status: str + created_at: datetime + updated_at: datetime + + +class SampleGraphView(CamelModel): + nodes: list[SampleGraphNodeView] = Field(default_factory=list) + edges: list[SampleGraphEdgeView] = Field(default_factory=list) + + +class SampleEventsView(CamelModel): + items: list[SampleEventView] = Field(default_factory=list) + + +class SampleStateView(CamelModel): + sample_id: UUID + experiment_id: UUID + environment_id: UUID + environment_name: str + detail: SampleDetailView + events: list[SampleEventView] = Field(default_factory=list) + graph: SampleGraphView = Field(default_factory=SampleGraphView) diff --git a/ergon_core/ergon_core/core/views/samples/service.py b/ergon_core/ergon_core/core/views/samples/service.py index 4d011e389..04e3efd22 100644 --- a/ergon_core/ergon_core/core/views/samples/service.py +++ b/ergon_core/ergon_core/core/views/samples/service.py @@ -1,11 +1,18 @@ """Read service for dashboard/API run snapshots and related views.""" import os +from contextlib import AbstractContextManager from pathlib import Path from datetime import datetime from uuid import UUID from ergon_core.core.views.samples.models import ( + SampleDetailView, + SampleEventsView, + SampleGraphEdgeView, + SampleGraphNodeView, + SampleGraphView, + SampleStateView, SampleSnapshotMetricsDto, SampleSummaryDto, SampleSnapshotDto, @@ -15,14 +22,15 @@ ExperimentDefinition, ExperimentDefinitionWorker, ) +from ergon_core.core.persistence.experiments.models import ExperimentEnvironmentRow from ergon_core.core.persistence.graph.models import ( SampleGraphEdge, SampleGraphNode, ) -from ergon_core.core.application.samples.events import ( - SampleRuntimeEventReadService, +from ergon_core.core.application.samples.event_views import ( SampleRuntimeEventView, ) +from ergon_core.core.application.samples.events import SampleRuntimeEventReadService from ergon_core.core.persistence.shared.db import get_session from ergon_core.core.persistence.shared.enums import SampleStatus from ergon_core.core.persistence.telemetry.models import ( @@ -298,6 +306,148 @@ def get_resource_blob(self, sample_id: UUID, resource_id: UUID) -> SampleResourc ) +class SampleReadService: + """Sample-centered read service backed by typed WAL and graph projections.""" + + def __init__(self, session: Session | None = None) -> None: + self._session = session + + def get_sample_detail(self, sample_id: UUID) -> SampleDetailView | None: + with self._session_scope() as session: + sample = session.get(SampleRecord, sample_id) + if sample is None: + return None + environment = _sample_environment(session, sample) + return _sample_detail_view(sample, environment_name=environment.name) + + def list_sample_events(self, sample_id: UUID) -> SampleEventsView | None: + with self._session_scope() as session: + if session.get(SampleRecord, sample_id) is None: + return None + events = SampleRuntimeEventReadService().list_events(session, sample_id) + return SampleEventsView(items=events) + + def get_sample_graph(self, sample_id: UUID) -> SampleGraphView | None: + with self._session_scope() as session: + if session.get(SampleRecord, sample_id) is None: + return None + return _sample_graph_view(session, sample_id) + + def get_sample_state(self, sample_id: UUID) -> SampleStateView | None: + with self._session_scope() as session: + sample = session.get(SampleRecord, sample_id) + if sample is None: + return None + environment = _sample_environment(session, sample) + detail = _sample_detail_view(sample, environment_name=environment.name) + events = SampleRuntimeEventReadService().list_events(session, sample_id) + graph = _sample_graph_view(session, sample_id) + return SampleStateView( + sample_id=sample.id, + experiment_id=detail.experiment_id, + environment_id=detail.environment_id, + environment_name=detail.environment_name, + detail=detail, + events=events, + graph=graph, + ) + + def _session_scope(self) -> AbstractContextManager[Session]: + if self._session is not None: + return _ExistingSessionScope(self._session) + return get_session() + + +class _ExistingSessionScope: + def __init__(self, session: Session) -> None: + self._session = session + + def __enter__(self) -> Session: + return self._session + + def __exit__(self, *args: object) -> None: + return None + + +def _sample_environment(session: Session, sample: SampleRecord) -> ExperimentEnvironmentRow: + if sample.environment_id is None: + raise ValueError(f"Sample {sample.id} is missing environment provenance") + environment = session.get(ExperimentEnvironmentRow, sample.environment_id) + if environment is None: + raise ValueError( + f"Sample {sample.id} points at missing environment {sample.environment_id}" + ) + return environment + + +def _sample_detail_view( + sample: SampleRecord, + *, + environment_name: str, +) -> SampleDetailView: + if sample.experiment_id is None or sample.environment_id is None: + raise ValueError(f"Sample {sample.id} is missing experiment/environment provenance") + assignment = sample.parsed_assignment() + source_metadata = assignment.get("source_metadata", {}) + return SampleDetailView( + sample_id=sample.id, + experiment_id=sample.experiment_id, + environment_id=sample.environment_id, + environment_name=environment_name, + sample_key=sample.sample_key or sample.instance_key, + sample_ref=sample.sample_ref_json, + source_metadata=source_metadata if isinstance(source_metadata, dict) else {}, + status=str(sample.status), + created_at=sample.created_at, + started_at=sample.started_at, + completed_at=sample.completed_at, + ) + + +def _sample_graph_view(session: Session, sample_id: UUID) -> SampleGraphView: + nodes = list( + session.exec( + select(SampleGraphNode) + .where(SampleGraphNode.sample_id == sample_id) + .order_by(col(SampleGraphNode.created_at), col(SampleGraphNode.task_id)) + ).all() + ) + edges = list( + session.exec( + select(SampleGraphEdge) + .where(SampleGraphEdge.sample_id == sample_id) + .order_by(col(SampleGraphEdge.created_at), col(SampleGraphEdge.id)) + ).all() + ) + return SampleGraphView( + nodes=[ + SampleGraphNodeView( + task_id=node.task_id, + task_slug=node.task_slug, + description=node.description, + status=str(node.status), + parent_task_id=node.parent_task_id, + level=node.level, + assigned_worker_slug=node.assigned_worker_slug, + created_at=node.created_at, + updated_at=node.updated_at, + ) + for node in nodes + ], + edges=[ + SampleGraphEdgeView( + edge_id=edge.id, + source_task_id=edge.source_task_id, + target_task_id=edge.target_task_id, + status=str(edge.status), + created_at=edge.created_at, + updated_at=edge.updated_at, + ) + for edge in edges + ], + ) + + def _display_run_score( score_summary: EvaluationScoreSummary, run_status: str, diff --git a/ergon_core/migrations/versions/00000000_initial_v2.py b/ergon_core/migrations/versions/00000000_initial_v2.py index 4271f2929..9fbd2b672 100644 --- a/ergon_core/migrations/versions/00000000_initial_v2.py +++ b/ergon_core/migrations/versions/00000000_initial_v2.py @@ -45,8 +45,8 @@ "sample_evaluator_events", "sample_sandbox_events", "sample_annotation_events", - "sample_context_events", "sample_task_attempts", + "sample_context_events", "sample_resources", "sample_task_evaluations", "threads", 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 c6f9b8da7..91459bb95 100644 --- a/ergon_core/tests/unit/architecture/test_application_domain_boundaries.py +++ b/ergon_core/tests/unit/architecture/test_application_domain_boundaries.py @@ -82,7 +82,7 @@ "workflow_errors.py", "workflow_models.py", }, - "samples": {"events.py", "materialization.py", "state.py"}, + "samples": {"events.py", "event_views.py", "materialization.py", "state.py"}, "testing": {"suppression_budget.py", "test_harness_service.py"}, } LAYOUT_DIR_EXCEPTIONS: dict[str, set[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 5c1225101..fdee0e9fc 100644 --- a/ergon_core/tests/unit/architecture/test_model_field_descriptions.py +++ b/ergon_core/tests/unit/architecture/test_model_field_descriptions.py @@ -22,7 +22,7 @@ GraphEdgeDto, GraphNodeDto, ) -from ergon_core.core.application.samples.events import SampleRuntimeEventView +from ergon_core.core.application.samples.event_views import SampleTaskAddedEventView from ergon_builtins.benchmarks.swebench_verified.task_schemas import ( SWEBenchInstance, SWEBenchTaskPayload, @@ -57,7 +57,7 @@ def test_dashboard_context_event_field_docs_are_schema_metadata() -> None: def test_graph_dto_field_docs_are_schema_metadata() -> None: assert _description(GraphNodeDto, "status") assert _description(GraphEdgeDto, "status") - assert _description(SampleRuntimeEventView, "payload") + assert _description(SampleTaskAddedEventView, "payload") def test_sqlmodel_field_docs_are_schema_metadata() -> None: diff --git a/ergon_core/tests/unit/architecture/test_single_alembic_head.py b/ergon_core/tests/unit/architecture/test_single_alembic_head.py index 5ecfec712..4317fdbec 100644 --- a/ergon_core/tests/unit/architecture/test_single_alembic_head.py +++ b/ergon_core/tests/unit/architecture/test_single_alembic_head.py @@ -20,3 +20,9 @@ def test_initial_migration_has_no_parent() -> None: text = (VERSIONS / "00000000_initial_v2.py").read_text() assert 'revision = "00000000"' in text assert "down_revision = None" in text + + +def test_initial_migration_orders_fk_targets_before_dependents() -> None: + text = (VERSIONS / "00000000_initial_v2.py").read_text() + + assert text.index('"sample_task_attempts"') < text.index('"sample_context_events"') 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 bd7138661..8e1767bc8 100644 --- a/ergon_core/tests/unit/dashboard/test_event_contract_types.py +++ b/ergon_core/tests/unit/dashboard/test_event_contract_types.py @@ -16,8 +16,8 @@ DashboardWorkflowStartedEvent, ) from ergon_core.core.views.samples.models import ( - RunCommunicationMessageDto, - RunCommunicationThreadDto, + SampleCommunicationMessageDto, + SampleCommunicationThreadDto, SampleSnapshotDto, ) @@ -42,21 +42,21 @@ def test_every_dashboard_event_contract_is_in_generated_schema_manifest() -> Non def test_thread_message_event_uses_dashboard_dtos() -> None: assert DashboardThreadMessageCreatedEvent.model_fields["thread"].annotation is ( - RunCommunicationThreadDto + SampleCommunicationThreadDto ) assert DashboardThreadMessageCreatedEvent.model_fields["message"].annotation is ( - RunCommunicationMessageDto + SampleCommunicationMessageDto ) def test_thread_message_dto_exposes_execution_identity() -> None: - assert "task_execution_id" in RunCommunicationMessageDto.model_fields + assert "task_execution_id" in SampleCommunicationMessageDto.model_fields def test_thread_dto_exposes_summary_and_task_identity() -> None: - assert "summary" in RunCommunicationThreadDto.model_fields - assert "task_id" in RunCommunicationThreadDto.model_fields - assert "task_id" in RunCommunicationMessageDto.model_fields + assert "summary" in SampleCommunicationThreadDto.model_fields + assert "task_id" in SampleCommunicationThreadDto.model_fields + assert "task_id" in SampleCommunicationMessageDto.model_fields def test_workflow_started_event_embeds_run_snapshot_contract() -> None: 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 dd1171e12..7954bb39f 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 @@ -1,4 +1,5 @@ from datetime import UTC, datetime, timedelta +import json from uuid import uuid4 import pytest @@ -7,6 +8,11 @@ ExperimentDefinitionInstance, ExperimentDefinitionTask, ) +from ergon_core.core.persistence.experiments.models import ( + ExperimentEnvironmentRow, + ExperimentRow, + ExperimentSamplerInvocationRow, +) 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 @@ -30,6 +36,9 @@ def session_factory(): _ = ExperimentDefinitionTask _ = SampleContextEvent _ = SampleGraphNode + _ = ExperimentRow + _ = ExperimentEnvironmentRow + _ = ExperimentSamplerInvocationRow engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, @@ -242,6 +251,68 @@ def test_experiment_run_rows_project_nested_metrics(monkeypatch, session_factory assert row.metrics.error_summary is None +def test_experiment_state_contains_environments_samples_and_invocations(session_factory) -> None: + now = datetime(2026, 5, 26, 12, 0, tzinfo=UTC) + experiment_id = uuid4() + environment_id = uuid4() + sample_id = uuid4() + + with session_factory() as session: + session.add( + ExperimentRow( + id=experiment_id, + name="mixed-training", + metadata_json={"purpose": "test"}, + created_at=now, + ) + ) + session.add( + ExperimentEnvironmentRow( + id=environment_id, + experiment_id=experiment_id, + name="mini-validation", + source_mode="materialized", + source_metadata_json={"provider": "records"}, + ) + ) + session.add( + ExperimentSamplerInvocationRow( + experiment_id=experiment_id, + sampler_name="random", + requested_k=1, + candidate_pool_size=4, + selected_count=1, + ) + ) + session.add( + SampleRecord( + id=sample_id, + experiment_id=experiment_id, + environment_id=environment_id, + sample_key="problem-1", + sample_ref_json={"id": "problem-1"}, + benchmark_type="experiment", + instance_key="problem-1", + status=SampleStatus.COMPLETED, + assignment_json={"source_metadata": {"split": "validation"}}, + ) + ) + session.commit() + + state = ExperimentReadService(session).get_experiment_state(experiment_id) + + assert state is not None + assert state.experiment_id == experiment_id + assert state.environments[0].environment_name == "mini-validation" + assert state.environments[0].sample_count == 1 + assert state.sample_count == 1 + assert state.samples[0].sample_id == sample_id + assert state.sampler_invocations[0].sampler_name == "random" + dumped = state.model_dump(mode="json", by_alias=True) + assert "definitionId" not in json.dumps(dumped) + assert "runId" not in json.dumps(dumped) + + def test_experiment_detail_groups_runs_by_experiment_tag(monkeypatch, session_factory) -> None: definition_a = uuid4() definition_b = uuid4() diff --git a/ergon_core/tests/unit/read_models/test_sample_read_service.py b/ergon_core/tests/unit/read_models/test_sample_read_service.py index c891559c3..b2b7af873 100644 --- a/ergon_core/tests/unit/read_models/test_sample_read_service.py +++ b/ergon_core/tests/unit/read_models/test_sample_read_service.py @@ -1,21 +1,47 @@ from datetime import UTC, datetime, timedelta +import json +from typing import Literal, get_args, get_origin from uuid import uuid4 import pytest +from pydantic import TypeAdapter +from ergon_core.core.application.samples.event_views import ( + ALL_SAMPLE_RUNTIME_EVENT_TYPES, + ROW_MODEL_EVENT_TYPES, + VIEW_EVENT_TYPES, + SampleRuntimeEventView, +) from ergon_core.core.persistence.definitions.models import ExperimentDefinition +from ergon_core.core.persistence.experiments.models import ExperimentEnvironmentRow, ExperimentRow from ergon_core.core.persistence.graph.models import SampleGraphNode +from ergon_core.core.persistence.samples.models import SampleStatusEventRow, SampleTaskEventRow 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 ergon_core.core.views.samples.service import SampleReadService, SampleSnapshotReadService from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine +def _sample_runtime_event_view_types() -> tuple[type, ...]: + union_type = get_args(SampleRuntimeEventView)[0] + return get_args(union_type) + + +def _literal_values(annotation: object) -> set[str]: + if get_origin(annotation) is Literal: + return set(get_args(annotation)) + return set() + + @pytest.fixture() def session_factory(): _ = ExperimentDefinition + _ = ExperimentRow + _ = ExperimentEnvironmentRow _ = SampleGraphNode + _ = SampleStatusEventRow + _ = SampleTaskEventRow engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, @@ -193,3 +219,103 @@ def test_failed_run_snapshot_preserves_persisted_final_score(monkeypatch, sessio assert snapshot is not None assert snapshot.status == "failed" assert snapshot.final_score == 0.5 + + +def test_sample_state_uses_typed_wal_and_graph_projection(session_factory) -> None: + now = datetime(2026, 5, 26, 12, 0, tzinfo=UTC) + experiment_id = uuid4() + environment_id = uuid4() + sample_id = uuid4() + task_id = uuid4() + + with session_factory() as session: + session.add(ExperimentRow(id=experiment_id, name="mixed-training", created_at=now)) + session.add( + ExperimentEnvironmentRow( + id=environment_id, + experiment_id=experiment_id, + name="mini-validation", + source_mode="materialized", + ) + ) + session.add( + SampleRecord( + id=sample_id, + experiment_id=experiment_id, + environment_id=environment_id, + sample_key="problem-1", + sample_ref_json={"id": "problem-1"}, + benchmark_type="experiment", + instance_key="problem-1", + status=SampleStatus.PENDING, + assignment_json={"source_metadata": {"provider": "records"}}, + created_at=now, + ) + ) + session.add( + SampleGraphNode( + sample_id=sample_id, + task_id=task_id, + instance_key="problem-1", + task_slug="solve", + description="Solve problem 1", + status="pending", + created_at=now, + updated_at=now, + ) + ) + session.add( + SampleStatusEventRow( + sample_id=sample_id, + event_timestamp=now, + event_type="sample.status_changed", + status="pending", + actor="test", + ) + ) + session.add( + SampleTaskEventRow( + sample_id=sample_id, + task_id=task_id, + task_slug="solve", + event_timestamp=now + timedelta(seconds=1), + event_type="task.added", + status="pending", + ) + ) + session.commit() + + state = SampleReadService(session).get_sample_state(sample_id) + + assert state is not None + assert state.sample_id == sample_id + assert state.experiment_id == experiment_id + assert state.environment_id == environment_id + assert state.environment_name == "mini-validation" + assert state.graph.nodes[0].task_slug == "solve" + assert [event.event_type for event in state.events] == [ + "sample.status_changed", + "task.added", + ] + dumped = state.model_dump(mode="json", by_alias=True) + assert "GraphMutation" not in json.dumps(dumped) + assert "runId" not in json.dumps(dumped) + + +def test_sample_runtime_event_view_union_covers_every_typed_wal_event() -> None: + union_event_types = { + event_type + for view_type in _sample_runtime_event_view_types() + for event_type in _literal_values(view_type.model_fields["event_type"].annotation) + } + + assert ROW_MODEL_EVENT_TYPES == ALL_SAMPLE_RUNTIME_EVENT_TYPES + assert VIEW_EVENT_TYPES == ALL_SAMPLE_RUNTIME_EVENT_TYPES + assert union_event_types == ALL_SAMPLE_RUNTIME_EVENT_TYPES + + +def test_sample_runtime_event_view_is_discriminated_by_event_type() -> None: + schema = TypeAdapter(SampleRuntimeEventView).json_schema(by_alias=True) + + assert schema["discriminator"]["propertyName"] == "eventType" + assert set(schema["discriminator"]["mapping"]) == ALL_SAMPLE_RUNTIME_EVENT_TYPES diff --git a/ergon_core/tests/unit/rest_api/test_experiment_samples_routes.py b/ergon_core/tests/unit/rest_api/test_experiment_samples_routes.py new file mode 100644 index 000000000..c4def6ba8 --- /dev/null +++ b/ergon_core/tests/unit/rest_api/test_experiment_samples_routes.py @@ -0,0 +1,114 @@ +from datetime import UTC, datetime +from uuid import uuid4 + +from ergon_core.core.infrastructure.http.routes import experiments as module +from ergon_core.core.infrastructure.http.routes.experiments import router +from ergon_core.core.views.experiments.models import ( + EnvironmentContributionView, + ExperimentDetailView, + ExperimentSamplesView, + ExperimentSampleSummaryView, + SamplerInvocationsView, + SamplerInvocationView, +) +from fastapi import FastAPI +from fastapi.testclient import TestClient + + +class _FakeExperimentReadService: + def __init__(self) -> None: + self.experiment_id = uuid4() + self.environment_id = uuid4() + self.sample_id = uuid4() + + def get_experiment_state(self, experiment_id): + return ExperimentDetailView( + experiment_id=experiment_id, + name="mixed-training", + environments=[ + EnvironmentContributionView( + environment_id=self.environment_id, + environment_name="mini-validation", + source_mode="materialized", + sample_count=1, + selected_count=1, + ) + ], + sample_count=1, + samples=[ + ExperimentSampleSummaryView( + sample_id=self.sample_id, + experiment_id=experiment_id, + environment_id=self.environment_id, + environment_name="mini-validation", + sample_key="problem-1", + status="completed", + created_at=datetime(2026, 5, 26, tzinfo=UTC), + ) + ], + sampler_invocations=[], + created_at=datetime(2026, 5, 26, tzinfo=UTC), + ) + + def list_experiment_samples(self, experiment_id): + return ExperimentSamplesView( + items=[ + ExperimentSampleSummaryView( + sample_id=self.sample_id, + experiment_id=experiment_id, + environment_id=self.environment_id, + environment_name="mini-validation", + sample_key="problem-1", + status="completed", + created_at=datetime(2026, 5, 26, tzinfo=UTC), + ) + ] + ) + + def list_sampler_invocations(self, experiment_id): + return SamplerInvocationsView( + items=[ + SamplerInvocationView( + sampler_invocation_id=uuid4(), + sampler_name="random", + requested_k=1, + candidate_pool_size=4, + selected_count=1, + created_at=datetime(2026, 5, 26, tzinfo=UTC), + ) + ] + ) + + +def test_experiment_detail_lists_environment_contributions(monkeypatch) -> None: + service = _FakeExperimentReadService() + monkeypatch.setattr(module, "ExperimentReadService", lambda: service) + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + response = client.get(f"/experiments/{service.experiment_id}") + + assert response.status_code == 200 + body = response.json() + assert body["environments"][0]["environmentName"] == "mini-validation" + assert body["environments"][0]["sampleCount"] == 1 + assert "definitionId" not in response.text + assert "runId" not in response.text + + +def test_experiment_child_routes(monkeypatch) -> None: + service = _FakeExperimentReadService() + monkeypatch.setattr(module, "ExperimentReadService", lambda: service) + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + samples = client.get(f"/experiments/{service.experiment_id}/samples") + invocations = client.get(f"/experiments/{service.experiment_id}/sampler-invocations") + + assert samples.status_code == 200 + assert samples.json()["items"][0]["sampleId"] == str(service.sample_id) + assert "runId" not in samples.text + assert invocations.status_code == 200 + assert invocations.json()["items"][0]["samplerName"] == "random" diff --git a/ergon_core/tests/unit/rest_api/test_samples_routes.py b/ergon_core/tests/unit/rest_api/test_samples_routes.py index 025f74f62..1bbd17805 100644 --- a/ergon_core/tests/unit/rest_api/test_samples_routes.py +++ b/ergon_core/tests/unit/rest_api/test_samples_routes.py @@ -3,8 +3,15 @@ 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.application.samples.events import SampleRuntimeEventView from ergon_core.core.views.samples.models import SampleSummaryDto +from ergon_core.core.application.samples.event_views import SampleTaskAddedEventView +from ergon_core.core.views.samples.models import ( + SampleDetailView, + SampleEventsView, + SampleGraphNodeView, + SampleGraphView, + SampleSnapshotDto, +) from fastapi import FastAPI from fastapi.testclient import TestClient @@ -46,20 +53,13 @@ def list_samples( ) ] - def list_events(self, sample_id: UUID) -> list[SampleRuntimeEventView] | None: - self.calls.append({"sample_id": sample_id, "method": "list_events"}) - return [ - SampleRuntimeEventView( - id=uuid4(), - sample_id=sample_id, - event_timestamp=datetime(2026, 5, 20, 12, 1, tzinfo=UTC), - table="sample_task_events", - event_type="task.status_changed", - target_type="task", - target_id=uuid4(), - payload={"status": "completed"}, - ) - ] + def build_snapshot(self, sample_id: UUID) -> SampleSnapshotDto: + return SampleSnapshotDto( + id=str(sample_id), + name="route snapshot", + status="completed", + definition_id=str(uuid4()), + ) def test_list_samples_route_passes_filters_to_read_service(monkeypatch) -> None: @@ -91,19 +91,99 @@ def test_list_samples_route_passes_filters_to_read_service(monkeypatch) -> None: assert body[0]["total_tasks"] == 1 -def test_sample_runtime_events_route_replaces_mutations_route(monkeypatch) -> None: +class _FakeSampleReadService: + def __init__(self) -> None: + self.sample_id = uuid4() + self.experiment_id = uuid4() + self.environment_id = uuid4() + self.task_id = uuid4() + + def get_sample_detail(self, sample_id: UUID): + return SampleDetailView( + sample_id=sample_id, + experiment_id=self.experiment_id, + environment_id=self.environment_id, + environment_name="mini-validation", + sample_key="problem-1", + sample_ref={"id": "problem-1"}, + status="completed", + created_at=datetime(2026, 5, 26, tzinfo=UTC), + ) + + def list_sample_events(self, sample_id: UUID): + return SampleEventsView( + items=[ + SampleTaskAddedEventView( + event_id=uuid4(), + sample_id=sample_id, + event_type="task.added", + target_type="task", + target_id=self.task_id, + timestamp=datetime(2026, 5, 26, tzinfo=UTC), + task_slug="solve", + ) + ] + ) + + def get_sample_graph(self, sample_id: UUID): + return SampleGraphView( + nodes=[ + SampleGraphNodeView( + task_id=self.task_id, + task_slug="solve", + description="Solve problem 1", + status="completed", + created_at=datetime(2026, 5, 26, tzinfo=UTC), + updated_at=datetime(2026, 5, 26, tzinfo=UTC), + ) + ] + ) + + +def test_sample_detail_events_and_graph_routes(monkeypatch) -> None: + service = _FakeSampleReadService() + monkeypatch.setattr(module, "SampleReadService", lambda: service) app = FastAPI() app.include_router(router) client = TestClient(app) - fake_service = _FakeSampleSnapshotReadService() - sample_id = uuid4() - monkeypatch.setattr(module, "SampleSnapshotReadService", lambda: fake_service) + detail = client.get(f"/samples/{service.sample_id}") + events = client.get(f"/samples/{service.sample_id}/events") + graph = client.get(f"/samples/{service.sample_id}/graph") - events_response = client.get(f"/samples/{sample_id}/events") - mutations_response = client.get(f"/samples/{sample_id}/mutations") + assert detail.status_code == 200 + assert detail.json()["sampleId"] == str(service.sample_id) + assert detail.json()["experimentId"] == str(service.experiment_id) + assert "runId" not in detail.text + assert events.status_code == 200 + assert events.json()["items"][0]["eventType"] == "task.added" + assert "GraphMutation" not in events.text + assert graph.status_code == 200 + assert graph.json()["nodes"][0]["taskSlug"] == "solve" - assert events_response.status_code == 200 - assert events_response.json()[0]["sample_id"] == str(sample_id) - assert events_response.json()[0]["event_type"] == "task.status_changed" - assert mutations_response.status_code in {404, 405} + +def test_sample_mutations_route_is_removed(monkeypatch) -> None: + service = _FakeSampleReadService() + monkeypatch.setattr(module, "SampleReadService", lambda: service) + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + response = client.get(f"/samples/{service.sample_id}/mutations") + + assert response.status_code in {404, 405} + + +def test_sample_workspace_route_preserves_dashboard_contract(monkeypatch) -> None: + service = _FakeSampleSnapshotReadService() + sample_id = uuid4() + monkeypatch.setattr(module, "SampleSnapshotReadService", lambda: service) + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + response = client.get(f"/samples/{sample_id}/workspace") + + assert response.status_code == 200 + assert response.json()["id"] == str(sample_id) + assert response.json()["name"] == "route snapshot" 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 946d60414..b06c05e61 100644 --- a/ergon_core/tests/unit/runtime/test_graph_mutation_contracts.py +++ b/ergon_core/tests/unit/runtime/test_graph_mutation_contracts.py @@ -1,6 +1,6 @@ from uuid import uuid4 -from ergon_core.core.application.samples.events import sample_runtime_event_from_row +from ergon_core.core.application.samples.event_views import sample_runtime_event_from_row from ergon_core.core.persistence.samples.models import SampleEdgeEventRow from ergon_core.core.views.dashboard_events.contracts import DashboardSampleRuntimeEvent @@ -23,9 +23,10 @@ def test_rest_and_dashboard_events_share_typed_sample_wal_payloads() -> None: dto = sample_runtime_event_from_row(row) event = DashboardSampleRuntimeEvent(event=dto) - assert event.event.table == "sample_edge_events" assert event.event.event_type == "edge.added" assert event.event.target_id == edge_id + assert event.event.source_task_id == source_id + assert event.event.target_task_id == target_id data = event.model_dump(mode="json") - assert data["event"]["payload"]["source_task_id"] == str(source_id) - assert data["event"]["payload"]["target_task_id"] == str(target_id) + assert data["event"]["source_task_id"] == str(source_id) + assert data["event"]["target_task_id"] == str(target_id)