From the repository, docs/contract/conductor-v1/README.md
Conductor API contract v1
These files are the truth for /v1/conductor/* (server/conductor/routes.py). The server's tests validate real responses against them (tests/test_conductor_e2e.py), and the dashboard's mock engine validates its fixtures and every mock response (dashboard/tests/conductor-contract.test.ts). They live beside dashboard-v1 rather than inside it so the two contracts can change on their own.
| file | endpoint |
|---|---|
config.schema.json |
GET /v1/conductor/config: loaded versions, load errors, the journal |
meta.schema.json |
GET /v1/conductor/meta: enums, the task catalogue with units, the schemas |
profiles.schema.json |
GET /v1/conductor/profiles: the tiles |
document.schema.json |
GET /v1/conductor/{settings,router,roles,packages}, GET /v1/conductor/profiles/{name} |
write.schema.json |
PUT of a document, POST .../restore, POST /v1/conductor/profiles/{name}/mode |
versions.schema.json |
GET .../versions |
diff.schema.json |
GET .../diff?from=&to= |
roles.schema.json |
GET /v1/conductor/roles: the document and each role's health |
role-test.schema.json |
POST /v1/conductor/roles/{role}/test |
classify.schema.json |
POST /v1/conductor/classify: the test box |
decisions.schema.json, decision.schema.json |
GET /v1/conductor/decisions, GET /v1/conductor/decisions/{id}; $defs holds one router decision (with events, the monitor's decisions in the same request) and one monitor event |
events.schema.json, event.schema.json |
GET /v1/conductor/events?task=&mode=&acted=&session=, GET /v1/conductor/events/{id}: the session monitor's decisions |
stats.schema.json |
GET /v1/conductor/stats?hours= |
sessions.schema.json, session.schema.json, export.schema.json |
the session list, one timeline, the JSON export |
evals.schema.json |
GET /v1/conductor/evals: reports in the config directory's evals/ (a suite report's own schema is dashboard-v1/tandem-eval.schema.json; a speed_guard report is written by tools/speed_guard.py: build, base and cand (label, runs, code), runs_each, noise_estimated, stats (per statistic base, cand, delta_pct, noise_pct or null), verdict pass, worse or no_noise_estimate, why) |
eval-write.schema.json |
POST /v1/conductor/evals {report}: a suite report from tools/tandem_eval.py kept in evals/ (the admin rule; 422 when it lacks kind suite, night or rows with suite and arm) |
watch.schema.json |
GET /v1/conductor/watch: the session tasks' state per session (S1's gates, S2's compaction watch, S4's progress and the idle client) |
residency-plan.schema.json |
GET /v1/conductor/residency/plan (OPS-41): the residency document, what may load beside the engine now, the engine's memory split, the planner's journal; the document itself is GET/PUT /v1/conductor/residency like the others |
deciders-status.schema.json |
GET /v1/conductor/deciders/status (SRV-131): the decision models this engine serves, each with its state and numbers, the residency planner's answer for a load now, the last refused engine load, the model behind each question id and what auto picked; enabled: false with why when QWEN38_DECIDER_MODELS is empty. The deciders document itself is GET/PUT /v1/conductor/deciders like the others |
decider-action.schema.json |
POST /v1/conductor/deciders/models/{name}/load (202, or 409 with the planner's decision), .../unload (200; 409 when not loaded), POST /v1/conductor/deciders/autoload (202, queued: the on_start models; what ops/start.sh asks for) |
systemone.schema.json |
POST /v1/conductor/deciders/test (the test box, the dashboard's rule) and the engine's own POST /v1/systemone and /v1/predict (the decider auth rule): one Jev answer object per question |
laya-ask.schema.json, laya-ask-texts.schema.json |
POST /v1/laya/ask (L1, anyone as the chat API): Laya's typed answer for the model's own scripts, one text (with model and ms) or texts (answers, one a text). Errors have the OpenAI shape. The deciders document's api section configures it and GET /v1/conductor/deciders/status reports it as api |
library.schema.json, library-search.schema.json, library-skill.schema.json |
GET /v1/library, GET /v1/library/search?q=&k=&class=, GET /v1/library/{name} (R1, anyone as the chat API): the package library's enabled skills, a search's best skills first ([] when no word matches), one skill's injected text and its reference. Errors have the OpenAI shape. A profile's library switch and the router's library section configure the router's own retrieval |
memory-sessions.schema.json, memory-session.schema.json, memory-keep.schema.json |
VIS-48, SRV-137: GET /v1/conductor/memory/sessions (every session transcript on the box with its meta.json, and the writer's counters and settings), GET /v1/conductor/memory/sessions/{key}?page=N (its blocks, 200 a page, the newest page by default, read to the last complete block) and .../export?format=json (every block; format=md is the file itself), PATCH .../keep {keep}. Every memory route needs the admin token, a dashboard session or the box itself, reads included |
memory-disk.schema.json, memory-disk-clear.schema.json |
C1: GET /v1/conductor/memory/disk (the disk tier: whether it runs, its settings and their version, its bytes against the budget and the free space against the 50 GB floor, the hit rate, the 20 most-hit entries -- a preview only for opencode heads and packages -- and its last events) and POST /v1/conductor/memory/disk/clear (what was on disk before, and the tier after; 409 when it does not run). Admin token, like every memory route |
scan.schema.json |
GET /v1/conductor/packages/scan |
packages-catalog.schema.json, packages-item.schema.json, packages-tokens.schema.json, packages-usage.schema.json |
G2, the Packages and Skills screen's reads: GET /v1/conductor/packages/catalog (every package: category, source and path, the tokens of the text that goes in on the engine's tokenizer or the 3.5-character estimate, its limit and whether it is over, the classes of the router and of each profile that list it, INDEX.md's fit, each profile's library switch), GET /v1/conductor/packages/item/{name} (frontmatter, body, reference, the text that goes in, their tokens, a library skill's BM25 neighbours; 404 for an unknown name), POST /v1/conductor/packages/tokens {texts} (a read: the tokens of 1 to 20 texts; 400 otherwise), GET /v1/conductor/packages/usage?name=&limit= (the journal's 7 days: per package its injections, shadow notes and library picks in 24 h and 7 d, one package's newest rows, the library's newest retrieval rows; 400 for a limit outside 1 to 200) |
error.schema.json |
every 4xx body; a 422 adds errors with one line per violation and its JSON path |
<kind>.config.schema.json |
the documents themselves; written from server/conductor/schema.py by tools/conductor_contract.py --write |
seeds.json |
the built-in version 0 of every document and the meta; the mock engine serves it |
Rules the schemas cannot express, which both sides test:
- A write needs a one-line note (422 without one).
base_versionis optional; when it is sent and the document moved on since, the answer is 409.rawandbenchanswer 409 to every write. - A
PUTthat fails validation answers 422 and leaves the current version as it was.POST .../checkruns the same validation without writing and returns the diff against the live version. - A profile whose Tier 1 task is set to
actwhile that task's role is offline is refused with the reason. stats.precisioncounts, per profile and decision type that ran in shadow, the decisions a person labelled and how many were right; the dashboard allows "Promote to on" at precision 0.9 or more over 30 or more labelled decisions.
Changes
- v1.0, first version.
- v1.0, SRV-113/115, additive: one journal for the router and the session monitor. The
monitordocument (the monitor's policy; its schema is the policy's own, a branch per rule, and version 0 is the server'sops/supervisor.yaml).eventsandevent. A router row gains the journal's common fields (kind,task,tier,decision,action,trigger,score,tier1_model,question_id,probabilities,tier1_ms,hidden_text_id,hidden_tokens,position, ...) andevents;sessiongainsevents;sessionsgainsacts,gave_ups,shadowandinterventions;stats.monitoris the monitor's block (null when no monitor runs); the export lists every intervention of either half (sourcerouter or monitor) with its exact text and sayssummary, "N interventions in M turns". A label (POST .../decisions/{id}/label) takes any row's id.config.journalnames the ledger's file. - v1.0, SRV-121 and OPS-43, additive:
GET /v1/conductor/watch(the session tasks' state per session) andPOST /v1/conductor/evals(a suite report kept for the Compare screen). Themonitordocument gains the session tasks S2 (context_health), S1 (gate_stagnation) and S4 (stall_watchdog, with itsnotifyblock); a monitor event from the watch hasspwatch(S4'sclient_gone,decisionPASS, or NOTIFY when its push is enabled). A profile'scontext(s1_runs,s4_minutes,notify) sets S1'sfails, S4'sminutesandnotify.enabled. - v1.0, OPS-41, additive: the
residencydocument (the planner's floor, margin, settle time, load order and each role's memory claim with its basis),GET /v1/conductor/residency/plan, andresidencyinconfig.store.documents. - v1.0, additive: the
monitordocument's C2 (project_rules) takesallow_paths, the paths no C2 rule applies to (default/tmp/opencode; left out, the default applies). Aspeed_guardreport inevals/(tools/speed_guard.py) carries the row pair's numbers and a verdict; one run a side isno_noise_estimate. - v1.0, SRV-131, additive: the
decidersdocument (each decision model's load policy,on_start,on_demandoroff, and the model behind each question id,classifyandpackagestoday),GET /v1/conductor/deciders/status,POST /v1/conductor/deciders/models/{name}/load|unload,POST /v1/conductor/deciders/autoload,POST /v1/conductor/deciders/test, anddecidersinconfig.store.documents. Therolesseed'sdeciderisendpoint: self(the engine's own decision models, asked in process) instead of the :8010 sidecar. - v1.0, OPS-47, additive: the
residencydocument gainsengine, the engine's memory limits the dashboard overrides by their serve.env names (MEM_CEILING_GIB,MEM_PEAK_GIB,RESERVE_SLACK_GIB,RESIDENT_GB,RESIDENT_STASH_GB,RESIDENT_GUEST_MAX,RESIDENT_PIN_GB,RESIDENT_PARK_GB,CACHE_GB,STORE_ENTRY_GB,VISION_EMBED_MB,SUFFIX_STORE_MB,PREFIX_CHUNK,NARROW_ARM; neverMAX_LEN), and the planner counts the engine's growth to its declared peak before a claim: a check, a plan row and the plan gainengine_peak_gib,engine_now_gibandengine_growth_gib, andafter_gibis MemAvailable less that growth less the claim.GET /v1/conductor/residency/plangainslimits(every limit, what it holds now, the shed log, the last session switch), and the journal gains the engine's memory lines (memory_shed,memory_refused,memory_estimate_raised, roleengine). - v1.0, SRV-133, a rename and additions: a role in the
rolesdocument carries the role contract --kind(reasoner, decider, probe, clerk, librarian, tool),memory_claim_gib(wasmemory_gb),window(idle, in_decode, any),preemptible,latency_budget_ms,fallback(PASS, stale, skip, next-role),tokenizer_sha256(64 hex characters ornone),writes(client_visible, hidden_notes, memory_files, nothing) andsharing(text, tokens, states) -- every one required on a write. Rules:writes: client_visibleonly for kind reasoner;window: in_decodeonly for a decider or a probe;sharing: statesonly for a probe;sharing: tokensonly with the engine's own tokenizer hash. A version stored before the contract loads with defaults (memory_gbbecomesmemory_claim_gib,latency_budget_msthe role'stimeout_ms,kindfrom the role's name or contract, then window idle, preemptible true, fallback PASS, writes nothing, tokenizer none, sharing text), listed in the document'smeta.defaultsand journaled (kindconfig); a restore of such a version writes it whole.GET /v1/conductor/rolesgainscontract(the engine's tokenizer hash; per role the forms of sharing it may use and whether its hash is the engine's); the meta gainsroles(the contract's values); a decision model indeciders/statusgainsrole, the contract it signs, which the registry checks before it serves the model. - v1.0, SRV-137 and VIS-48, additive: the
settingsdocument gainsmemory.transcripts(scopetools, all or none;retention_days;clip_chars;loop_chars; left out, the defaults apply), the memory routes above, and the JSON export of a session gainstranscript(the block count and the memory export's path, or null). The journal gains rows ofkindmemory (a block's redaction counts per kind, never a value; a failed write; the prune; a keep) andkindconfig. - v1.0, C1, additive: the
settingsdocument gainsmemory.disk_cache(modeon or off,budget_gb,ttl_days,admit_after,min_free_gbof at least 50; left out, the defaults 50 GB, 7 days, 2, 50 GB apply), the two disk routes above, and the journal gains rows ofkindmemory,taskdisk_cache (decisionhit, write, evict, drop, restore_failed, clear, error; counts, token lengths and reasons, never a text). - v1.0, S1 (2026-10-02), additive: the
routerdocument'sstructuralgainsside_requests(on,off; left out, on) andside_shapes(a list of{id, client, kind: title|summary, match, max_tokens}that replaces the built-in shapes of a client's side requests); a profile'suigainsstates_channel(reasoning,content; left out, reasoning) andprogress({mode, prefill_every_s, thinking_every_s, thinking_every_tokens}; left out, on with the defaults), andui.kindsnamesqueue,prefillandthinking. A router row'skindmay beside(a client's side request served by its shape:decisionthe shape,actionSIDE_REQUEST,capits output cap,extra.side);GET /v1/conductor/decisionslists the side rows with the router's turns;stats,sessionsand a session's turns read the turns alone. The meta gainsstate_channelsandside_kinds. A version saved with any of these keys is refused by a server before S1, which serves the newest older version. - v1.0, G2 (2026-10-03), additive:
GET /v1/conductor/packages/catalog,GET /v1/conductor/packages/item/{name},POST /v1/conductor/packages/tokensandGET /v1/conductor/packages/usage, all reads under the dashboard's rule; no document and no journal row changes. - v1.0, X1 (2026-10-03), additive: a profile's
ui.states_channelmay beinline(each state line the title of a reasoning part of its own: an emptytool_callsdelta, then a reasoning delta\n**› text**\n\n; on areasoning_contentstream only,contentotherwise); the meta'sstate_channelslists it. Thedecidersdocument'sapigainsdrive_rate_per_s,drive_sessions,drive_idle_sanddrive_max_texts(left out, 10, 4, 30, 36,000);POST /v1/laya/asktakes an optionalsession(a driver session: its own rate, no journal row a call; the answer is unchanged),POST /v1/laya/drive/end {session, summary?}answers the session's numbers and writes one journal row (kindlaya_drive,tasksession,decisionend or idle),GET /v1/laya/kit/<file>serves the playtest kit (laya_drive.py,laya-drive.js) as text;deciders-status'sapigains the four limits andstats.drive_sessions. A version saved with these keys is refused by a server before X1.