1. Worker dispatch
The legacyWorkerOrchestrator is unchanged: it still routes free vs. paid
through routing.py and either short-circuits to free_model_node or
delegates to a single HierarchyOrchestrator instance. The change is
that the field formerly named hierarchy is now a ScenarioOrchestrator
with the same public run(payload, routing_decision, event_bus) -> ScenarioResult signature.
2. Orchestrator construction (per-process)
When the worker boots:ScenarioOrchestrator.__post_init__runs once.register_with_existing_verifier(default_registry, build_verifier)wires the worker’s pre-constructedBuildVerifierunder the namebuild.register_llm_judge(default_registry, providers)injects the worker’sLlmProvidersinstance into a freshLLMJudgeVerifierfactory.ScenarioLoader(scenarios_root).load_all()reads everyknowledge-hub/scenarios/*.yaml, validates each against the Pydantic schemas, and compiles everyedge.whenclause throughexpr.py. Any error fails fast with the source filename in the message.- Per-tenant overrides (when the worker has been configured with a
tenant_id+TenantOverrideProvider) deep-merge over the base scenarios. PromptLevelRunner,VerifierLevelRunner,FanOutLevelRunner,HumanApprovalLevelRunnerare constructed and registered withScenarioExecutorunder their respectivekindkeys.ScenarioCompiler(scenario, dispatcher=executor).compile()runs once for every loaded scenario; the compiled graphs land in_compiled_by_idkeyed by scenario id.run()then picks one per job — see § 3a below. Topology rules (applied per scenario):- one node per level;
- edges with
when: Nonebecome unconditionaladd_edges; - edges with a
whenclause are grouped per source and emitted asadd_conditional_edgeswith a router function that evaluates the compiled DSL expressions in priority order and emits agate_decisionevent when a branch fires; - levels with no outgoing edges implicitly route to
END.
3a. Per-job scenario selection
The worker compiles every scenario at startup, then picks one per job.ScenarioOrchestrator._resolve_scenario_for_job(payload) walks three
sources, highest wins:
- Explicit pin —
payload.metadata["scenario_id"]. Used by replay fixtures, admin tools, and the future universal-input classifier that wants to override the route mapping. - Intent-route mapping —
payload.metadata["intent_route"]looked up in theintent_route → scenario_idindex the loader built from each scenario’sintent_routesdeclaration. Lookup is case-insensitive. Today’s bundled mapping:project → code_build,document/writing → document_writing,task/qa → quick_qa,research → research_brief,security_review → security_review. Two scenarios claiming the same route is a hard load error — the dispatch must be unambiguous. - Default —
CEREBRUM_DEFAULT_SCENARIOenv orcode_build. Also the fallback when an explicit id is unknown or a route is unmapped (the run continues with a warning log; a misconfigured payload must never break the job).
scenario_started event as
selection_reason: "explicit", "intent_route:<route>", or
"default" — useful for telemetry and “why did this scenario run?”
debugging.
3. Job invocation
run(payload, routing_decision, event_bus) builds the initial
ScenarioState:
scenario_started and invokes compiled.invoke(initial).
LangGraph drives every node from the entry point (first level by YAML
order) until the graph reaches END.
4. Level dispatch
ScenarioExecutor.run_level(state, level) is the per-node body. It:
- Skip-fast when
state.aborted_reasonis set (budget kill switch short-circuit). Returns a syntheticskippedlevel state and emitslevel_finishedwithok=true, duration_ms=0. - Emits legacy
node_startedand newlevel_started(withlevel_id,role,kind,attempt,model_constraint_allow). - Calls the kind-specific runner.
- Emits
node_finished+level_finishedwith cumulative duration, cost, and the output preview (first 200 chars from the level’s delta).
kind: prompt (PromptLevelRunner)
- Resolves the prompt builder via
get_prompt_builder(level.prompt_ref)and builds aBuiltPrompt. - Emits
agent_handoff+prompt_sentwhen the builder declared them (suppressed on retries by the builder itself). - Resolves the model via
resolve_model(level.model_constraint, routing_default)and emitsrouting_decisionwith the resolved tier / provider / model /constraint_source. - Wraps the call in
event_bus.heartbeat(...)for L4-class long generations. - Calls
_call_with_ask_user(...)— sameask_userclarification loop as the legacy hierarchy. - Records cost + tokens into the running aggregates, emits
budget_consumed. - Runs
state.budgetcheck; on exceeded, setsaborted_reasonand emitsbudget_exceeded. - Per-level post-processing:
l1_brief: emits a Markdown brief (goal + acceptance criteria) directly — no JSON envelope, no collapse step; its prose is the level output L2/L3 read.l3_accept/fact_check: extract the verdict via_verdict_from_l3_output— a trailing```controlfence first (worker/runtime/control_block.py), then a lenient JSON path (plain{verdict}orextra.verdict). When neither yields a clean PASS/FAIL, a single bounded repair-retry re-asks with the contract spelled out before falling through. The verdict is written tostate.verdict; FAIL registers a failure with the escalation tracker and setsescalate_nowwhen the conflict budget is hit.polish/synthesis/answer/final_report: terminal-output levels; theirresult.outputbecomesstate.final_output.
- If the level itself declared verifiers (e.g., an
llm_judgerubric onl3_acceptorfact_check), the runner invokes them through theVerifierRunnerand folds the per-verifier outcomes into the level state alongside the prompt result.
kind: tool_loop (ToolLoopLevelRunner)
l4_execute and l2_escalate are agentic file-tool loops — the model
mutates the project workspace through tool calls executed by the worker
(Claude Code style: the filesystem is the artifact, not the message
transcript). Downstream levels receive the touched paths, never file
bodies.
- Builds the task prompt via
prompt_ref(no Markdown file-fence contract — the runtime provides the tools) and emits theagent_handoff/routing_decisionevents likeprompt. - Loops up to
Level.metadata["max_iterations"](default 40):- paid transport —
LlmProviders.call_model_turn(messages, tools=anthropic_tools())returns text plus nativetool_useblocks; - free transport — only when the first paid turn fails and the
level allows
fallback_outside_allow: the textual```tool/```tool_resultprotocol overcall_free_model.
- paid transport —
- Each tool call (
read_file/write_file/edit_file/list_dir/delete_file/finish) is dispatched viaworker/runtime/file_tools.execute_toolagainst_project_cwd, sandboxed bycode_applier._safe_relative. Every call streams atool_call+tool_resultevent; writes also emitfile_created. - The loop ends on
finish, an empty turn,max_iterations, the scenario budget, or a user Stop (bus.is_cancelled(), checked per turn). Every turn’s usage folds into the running aggregates. - Writes the level state:
output= thefinishsummary,touched_paths= the runner’s authoritative write-tool set. Terminal tool-loop levels (l2_escalate) also setstate.final_output.
kind: verifier (VerifierLevelRunner)
- The
l4_executetool loop already wrote its changes to disk, so there is no file-apply step — the runner builds aVerifierContextwith the project workspace, payload, and scratchpad and runs the declared verifiers directly. - Emit
verifier_startedper declared verifier. - Invoke
VerifierRunner.run(level.verifiers, ctx). Mode isserial,parallel, orrace(see scenarios-runtime.md). - Emit
verifier_finishedper result. - Stash a one-line
build_result_summaryon the scratchpad (pass or fail) so thel3_acceptprompt can review plan-vs-done without re-reading the workspace. - On failure, format every failed verifier into a feedback string —
the build stderr tail plus any other gate’s reasons (e.g. the
route_reachabilityorphan/misplaced-route list) — and stash it underbuild_feedbackso the next L4 retry fixes all blockers. Register failure with the escalation tracker on conflict idbuild_failure. - Emit
build_verify_passedorbuild_verify_failedfor legacy compatibility.
code_build’s l3_build_verify runs two verifiers: build (install +
next build) and route_reachability — a deterministic, no-LLM gate
that every Next.js App Router route is navigable from /. It flags
orphan routes (a page.* nothing reachable from the home page links to)
and misplaced pages (a page.* written into a parallel app/ tree such
as apps/frontend/app/..., which Next never routes), following
Link/href/router.push/redirect targets transitively through
imported components. Dynamic ([id]) and api routes are exempt, and a
project with no app/ dir skip-passes (so non-Next builds and the replay
harness are untouched). The l3_build_verify → l3_accept edge ANDs both
verifiers, so a navigability failure routes back to L4 with the offending
routes — same retry/escalation budget as a build failure. code_build_strict
adds it alongside typecheck + lint.
kind: aggregator (FanOutLevelRunner)
- Dispatches each child in
level.fan_outthrough the sameScenarioExecutorvia aThreadPoolExecutor. Each child runs through its own kind runner (so a child can itself beprompt,verifier, orhuman_approval). - Merges child level states into the parent’s
levelsslot so the synthesis prompts can read them by id and the audit log retains every child output. - Hands the
ChildOutcome[]to the aggregator named inlevel.fan_in.aggregator(majority_pass,weighted_score,all_passby default). - Sets the parent level’s
outputto the aggregator’s summary and setsstate.verdictto the aggregator’sPASS/FAILso the gate router can branch off it. - Checks the budget against the sum of children’s cost (parallel children each see only their own slice before the merge, so the parent’s post-merge check is what stops a fan_out from slipping past the cap).
kind: human_approval (HumanApprovalLevelRunner)
- Builds the human-visible question via the level’s
prompt_refbuilder. - Emits
ask_userwithcall_id,interrupt_id,approver_role, and the level’soutput_schema(or a default{approved, comment}schema). - Calls
langgraph.types.interrupt(...)which raises aGraphInterruptthe worker’s checkpointer catches; the run pauses untilCommand(resume=...)lands. - On resume, packages
{approved, comment}into the level’soutput_parsedand writesstate.verdict = PASS|FAIL. - Emits
tool_resultwith the approval so the timeline shows the decision landed. - On reject, registers failure with the tracker under conflict
human_approval:<level_id>so a chain of rejections eventually tripsescalate_now.
graph.invoke()), the runner auto-approves with a
loud log so the runtime stays testable without a real checkpointer.
5. Gate routing
After each level returns, LangGraph dispatches to the source’s outgoing edges. For conditional edges:- The compiler’s router evaluates each compiled expression in
priorityorder against aMappingContextbuilt fromstate. The context surfacesverifier.<name>.<field>from the current level’s verifier results,verdict,escalate_now,confidence,level.<id>.<field>, andscratchpad.<key>. - The first matching edge wins. The router emits
gate_decisionwithsource_level,target_level,matched_expression, andreason. - When nothing matches, the router falls through to
ENDwith a loud log entry — scenarios should declare a defaultwhen: Noneedge to avoid this. - Kill-switch short-circuit: when
state.aborted_reasonis set (budget cap or user Stop), the router routes straight toEND— otherwise the stale state of skipped levels would route a retry edge back into a loop (e.g.l3_build_verify → l4_executeforever).
6. Termination
When the graph reachesEND, the orchestrator:
- Reads the terminal state and synthesizes a
ScenarioResult(output =state.final_outputor fallback tolevels[l4_execute].output). It also carriesworkspace_owned(true when the scenario has atool_looplevel), thel3_build_verifyoutcome, andaborted_reasonout to the thread runner. - Emits
scenario_finishedwithstatus: completed | aborted | cancelled,aborted_reason,total_cost_usd,total_tokens,total_duration_ms, andlevels_run.cancelledis a user Stop;abortedis a budget cap or a crash.
ScenarioRunTimeline consumes the event stream and
derives the per-level / per-verifier / gate / budget views via the
helpers in apps/frontend/lib/workflow/scenario-events.ts. The user
sees the full story of the run — which model ran which level, which
verifier passed or failed with what score, which branch the gate took —
without polling any additional endpoint.
7. User Stop
A user can halt a running job from the workflow composer’s Stop button at any step. The mechanism is a Redis flag, polled at safe points — no new thread, no signal handler:POST /v1/jobs/:id/cancel(mother-ai) setscerebrum:job:{id}:cancel(1 h TTL), marks the job terminalcancelledin the Redis status hash, and best-effort marksprompt_threads.status='cancelled'in Postgres — so the UI unsticks immediately even if no worker is holding the job.- The worker observes the flag via
EventBus.is_cancelled()(a memoizedEXISTScheck): the executor polls it at every level boundary,ToolLoopLevelRunneronce per tool turn, and the Anthropic streaming loop cuts a doomed generation short. - On detection the executor sets
aborted_reason="cancelled_by_user"— the same kill-switch path as a budget abort: all remaining levels skip, the gate router short-circuits toEND, andscenario_finishedfires withstatus: "cancelled". - A cancelled run discards partial work:
main.pyskipscommit_and_pushand marks the job + promptcancelled. Only a new prompt in the same workflow starts a fresh run from L1.
8. Replay path
For CI / regression detection, the same pipeline runs againstCassetteProviders instead of live LlmProviders. The cassette is a
list of recorded ModelCallResult dicts on a golden fixture; every
prompt-runner / call_model_turn / llm_judge call pops the next entry in
order — a tool_loop cassette entry carries the tool_calls the loop
dispatches. The harness provisions an ephemeral git workspace for any
scenario with a tool_loop level. Verifier results are byte-for-byte
deterministic (subprocess output or schema validation), so the harness
diffs the run’s verifier scores + verdict + cost against the fixture’s
expected block. The CI gate fails on any regression beyond declared
thresholds.