Contents TandemLLM Docs
v0.9.1
Star58

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_version is optional; when it is sent and the document moved on since, the answer is 409. raw and bench answer 409 to every write.
  • A PUT that fails validation answers 422 and leaves the current version as it was. POST .../check runs the same validation without writing and returns the diff against the live version.
  • A profile whose Tier 1 task is set to act while that task's role is offline is refused with the reason.
  • stats.precision counts, 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 monitor document (the monitor's policy; its schema is the policy's own, a branch per rule, and version 0 is the server's ops/supervisor.yaml). events and event. 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, ...) and events; session gains events; sessions gains acts, gave_ups, shadow and interventions; stats.monitor is the monitor's block (null when no monitor runs); the export lists every intervention of either half (source router or monitor) with its exact text and says summary, "N interventions in M turns". A label (POST .../decisions/{id}/label) takes any row's id. config.journal names the ledger's file.
  • v1.0, SRV-121 and OPS-43, additive: GET /v1/conductor/watch (the session tasks' state per session) and POST /v1/conductor/evals (a suite report kept for the Compare screen). The monitor document gains the session tasks S2 (context_health), S1 (gate_stagnation) and S4 (stall_watchdog, with its notify block); a monitor event from the watch has sp watch (S4's client_gone, decision PASS, or NOTIFY when its push is enabled). A profile's context (s1_runs, s4_minutes, notify) sets S1's fails, S4's minutes and notify.enabled.
  • v1.0, OPS-41, additive: the residency document (the planner's floor, margin, settle time, load order and each role's memory claim with its basis), GET /v1/conductor/residency/plan, and residency in config.store.documents.
  • v1.0, additive: the monitor document's C2 (project_rules) takes allow_paths, the paths no C2 rule applies to (default /tmp/opencode; left out, the default applies). A speed_guard report in evals/ (tools/speed_guard.py) carries the row pair's numbers and a verdict; one run a side is no_noise_estimate.
  • v1.0, SRV-131, additive: the deciders document (each decision model's load policy, on_start, on_demand or off, and the model behind each question id, classify and packages today), GET /v1/conductor/deciders/status, POST /v1/conductor/deciders/models/{name}/load|unload, POST /v1/conductor/deciders/autoload, POST /v1/conductor/deciders/test, and deciders in config.store.documents. The roles seed's decider is endpoint: self (the engine's own decision models, asked in process) instead of the :8010 sidecar.
  • v1.0, OPS-47, additive: the residency document gains engine, 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; never MAX_LEN), and the planner counts the engine's growth to its declared peak before a claim: a check, a plan row and the plan gain engine_peak_gib, engine_now_gib and engine_growth_gib, and after_gib is MemAvailable less that growth less the claim. GET /v1/conductor/residency/plan gains limits (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, role engine).
  • v1.0, SRV-133, a rename and additions: a role in the roles document carries the role contract -- kind (reasoner, decider, probe, clerk, librarian, tool), memory_claim_gib (was memory_gb), window (idle, in_decode, any), preemptible, latency_budget_ms, fallback (PASS, stale, skip, next-role), tokenizer_sha256 (64 hex characters or none), writes (client_visible, hidden_notes, memory_files, nothing) and sharing (text, tokens, states) -- every one required on a write. Rules: writes: client_visible only for kind reasoner; window: in_decode only for a decider or a probe; sharing: states only for a probe; sharing: tokens only with the engine's own tokenizer hash. A version stored before the contract loads with defaults (memory_gb becomes memory_claim_gib, latency_budget_ms the role's timeout_ms, kind from the role's name or contract, then window idle, preemptible true, fallback PASS, writes nothing, tokenizer none, sharing text), listed in the document's meta.defaults and journaled (kind config); a restore of such a version writes it whole. GET /v1/conductor/roles gains contract (the engine's tokenizer hash; per role the forms of sharing it may use and whether its hash is the engine's); the meta gains roles (the contract's values); a decision model in deciders/status gains role, the contract it signs, which the registry checks before it serves the model.
  • v1.0, SRV-137 and VIS-48, additive: the settings document gains memory.transcripts (scope tools, 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 gains transcript (the block count and the memory export's path, or null). The journal gains rows of kind memory (a block's redaction counts per kind, never a value; a failed write; the prune; a keep) and kind config.
  • v1.0, C1, additive: the settings document gains memory.disk_cache (mode on or off, budget_gb, ttl_days, admit_after, min_free_gb of 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 of kind memory, task disk_cache (decision hit, write, evict, drop, restore_failed, clear, error; counts, token lengths and reasons, never a text).
  • v1.0, S1 (2026-10-02), additive: the router document's structural gains side_requests (on, off; left out, on) and side_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's ui gains states_channel (reasoning, content; left out, reasoning) and progress ({mode, prefill_every_s, thinking_every_s, thinking_every_tokens}; left out, on with the defaults), and ui.kinds names queue, prefill and thinking. A router row's kind may be side (a client's side request served by its shape: decision the shape, action SIDE_REQUEST, cap its output cap, extra.side); GET /v1/conductor/decisions lists the side rows with the router's turns; stats, sessions and a session's turns read the turns alone. The meta gains state_channels and side_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/tokens and GET /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_channel may be inline (each state line the title of a reasoning part of its own: an empty tool_calls delta, then a reasoning delta \n**› text**\n\n; on a reasoning_content stream only, content otherwise); the meta's state_channels lists it. The deciders document's api gains drive_rate_per_s, drive_sessions, drive_idle_s and drive_max_texts (left out, 10, 4, 30, 36,000); POST /v1/laya/ask takes an optional session (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 (kind laya_drive, task session, decision end or idle), GET /v1/laya/kit/<file> serves the playtest kit (laya_drive.py, laya-drive.js) as text; deciders-status's api gains the four limits and stats.drive_sessions. A version saved with these keys is refused by a server before X1.