Skip to content

API · Orchestration & Runs

The run surface: kicking off agent work, inspecting run history, the scheduler, model benchmarking, and the test/compare endpoints. All endpoints are unauthenticated (see API conventions).

OrchestratorController

api/orchestrator — the project-run API. The agent-run kickoff is POST /api/orchestrator/run with a RunRequest body.

Verb Route Request Response Async
POST /api/orchestrator/run body RunRequest 200 { Response, KnowledgeUsed } yes
POST /api/orchestrator/clear-session/{projectId} route projectId:int 200 no
POST /api/orchestrator/end-session/{projectId} route projectId:int; query stopContainer:bool=false 200 { Message, ContainerStopped } yes
GET /api/orchestrator/container-status/{projectId} route projectId:int 200 status object no
POST /api/orchestrator/test-drive body TestDriveRequest TestDriveResponse yes
GET /api/orchestrator/runs/latest — List<ProjectRunStateDto> (ProjectId, RunGuid, Status, StartedAt, CompletedAt): each project's newest run, one row per project that has run. Status is the run's status, or Interrupted for a run still recorded Running or Queued that started before the server process did (a restart killed it). The Projects page badge reads it. yes
GET /api/orchestrator/runs/{projectId} route projectId:int; query limit:int=20 (1–100) List<BuildRun> yes
GET /api/orchestrator/runs/{projectId}/{runGuid:guid} route projectId:int, runGuid:guid BuildRunDetailResponse yes
GET /api/orchestrator/runs/{projectId}/tool-invocations route projectId:int; query runGuid:string?, step:int? ToolInvocationsResponse yes
GET /api/orchestrator/runs/{projectId}/summaries route projectId:int; query limit:int List<RunSummaryDto> yes
GET /api/orchestrator/runs/{projectId}/trajectory route projectId:int; query runGuid:string? TrajectoryDto / 404 — plus events (firstSeq, lastSeq, count): the run's slice of the durable session log, null for runs that predate it yes
POST /api/orchestrator/run-from-template body RunFromTemplateRequest (Wait, default true) RunFromTemplateResponse; 202 with Status: "started" when Wait is false; 400 when the message is missing or an overrides field names an unknown slot yes
GET /api/orchestrator/runs/{projectId}/logs route projectId:int; query runGuid:string? 200 (run meta + steps + messages) yes
POST /api/orchestrator/runs/{projectId}/cancel route projectId:int 200 / 404 yes
POST /api/orchestrator/run-research body RunResearchRequest RunResearchResponse (fire-and-forget) yes
POST /api/orchestrator/continue-research body ContinueResearchRequest ContinueResearchResponse; 409 if a build is active yes
GET /api/orchestrator/latest-research — 200 / 404 yes

RunRequest

The load-bearing request body. Fields:

Field Type Notes
ProjectId int Target project
Message string User message that drives the run
ToolDefaults Dictionary<string, Dictionary<string,object>>? Per-tool argument defaults (toolName → {argName: value}); agent-supplied values always win. Flows to McpSessionContext.ToolDefaults → router.
SamplingOverrides Dictionary<string, Dictionary<string,object>>? Per-role sampling params; only seed is honored today
HandoffChecks Dictionary<string,object>? Gate conditions (e.g. min_kb_documents)
SourceAgent string? Who is sending the message, recorded on the build run and in its build.completed / build.failed webhooks; absent means the Studio (ui). run-from-template forwards its own SourceAgent the same way.

Sync vs. fire-and-forget

run, run-from-template, and test-drive await the full build — except run-from-template with Wait: false, which creates the project, starts the run in the background and answers 202 with the project id and Status: "started" (for a caller with a short request timeout). run-research and continue-research return 200 immediately and run in the background — poll GET /api/orchestrator/runs/{projectId} for progress.

RunRequest, TestDriveRequest, RunResearchRequest/Response, ContinueResearchRequest/Response, and ToolInvocationsResponse are declared inline in the controller. RunFromTemplateRequest/Response and BuildRunDetailResponse are in Aetherium.Shared.DTOs.

TrajectoryEvalController

api/trajectoryeval — LLM-as-judge scoring of a completed run's trajectory, and the template-promotion gate built on it.

Verb Route Request Response Async
POST /api/trajectoryeval/run/{buildRunId} route buildRunId:int TrajectoryEvalResult yes
POST /api/trajectoryeval/promote body { BuildRunId, TemplateId, Threshold? } TrajectoryPromotionResult yes
GET /api/trajectoryeval/run/{buildRunId} route buildRunId:int List<TrajectoryEvalResult> (persisted verdicts) no

The judge model resolves from AI_TrajectoryJudgeModelId (→ AI_SupervisorModelId → AI_ModelId); promotion requires the trajectory score to meet AI_PromotionScoreThreshold (default 7). Verdicts persist in interaction_logs (type trajectory_eval). With AI_TrajectoryEvalOnRunComplete set to true, the Engine scores every finalized run (Completed or Failed) in the background after the run is broadcast; it is off by default and never gates a run. The run viewer shows the latest verdict and can request one on demand.

FinishingController

api/finishing — the two-phase finishing pass and the human answer between its halves. A gate is one project's pass: Phase 1 researches and writes a brief, the operator answers, Phase 2 applies the answer.

Verb Route Request Response Async
GET /api/finishing/gates query limit (default 100) 200 { Gates, Waiting, LongestWaitSeconds } no
GET /api/finishing/gates/{projectId} route projectId:int FinishingGate, 404 when the project has never had one no
POST /api/finishing/gates body { ProjectId, SourceRunGuid? } FinishingGate, 409 with the reason when no gate opened no
POST /api/finishing/gates/{projectId}/answer body { Answer } FinishingGate, 409 when the gate is not awaiting an answer no

The listing puts the gates waiting on a human first and the longest-waiting at the top, which is what makes it usable as a queue. A gate's Phase1RunGuid and Phase2RunGuid name each phase's run from the moment it starts; before that the column holds the claim token claimed. A pass the Engine was running when it restarted cannot finish: on its next poll the finishing runner closes that gate as Failed, with a note naming the stranded run, and fails the run's row if it still reads Running. The pass is not restarted on its own; open a new gate to run it again.

The React client's Finishing page (/app/finishing, under Operations) is the screen for this controller: it lists each project's current gate, opens a pass, shows the brief and the three candidate pages, takes the answer, and shows Phase 2's repair list and delivery gate, with links to both phase runs. The brief ends its three directions with a Recommended: <A, B or C> — <why> line, which the page marks in its answer form; an answer that names no direction gets the recommended one in Phase 2.

Opening a gate by hand skips the eligibility checks — the operator is the decision — but not the recursion guard or the one-live-gate rule, which are about consistency rather than about who asked. Phase 1 is told the status of the build it finishes, a failed one included, and the brief opens on it. A re-opened gate starts clean: before Phase 1, the earlier pass's finishing/ files, except the style record, move to finishing-history/ beside the workspace. When the brief cites a reference that research never captured, the gate's Note says which. Phase 2 repairs before it restyles: at its start the Engine writes finishing/repair-list.md, the build team's own required outputs that fail on disk and every page reference that points at nothing, and the director plans the repair as step 1 from a one-line summary of each item (it plans with up to 10,240 output tokens; the full text stays in the file for the repair step). The director is told to give any step that edits a page the build owed that page's owed checks, and the final step owes finishing/delivery-gate.md verdict PASS. A PASS the Engine's own check_finishing rows refuse is held, and a FAIL goes back to the steps that wrote the failing files, each told what those rows found in its files. When Phase 2 ends, the Note also records what it took away from the pages (pictures, headings, most of a page's words, a removed page), recorded rather than enforced. Automatic opening is the Aetherium:Finishing:AutoTrigger key (Configuration), off by default.

Answering writes the text to finishing/answer.md in the project's workspace before stamping the gate, and the order is deliberate: a gate stamped Phase 2 with no answer file is a pass that cannot start, whereas a file with no stamp is just a file. It is also how the Phase-2 runner gets the prose — the gate table carries timestamps, not text. The shape is in Schema · FinishingGate.

Where to go next