What it enables
- Decouple agent flow shape from worker code. New scenarios — code, writing, research, security, Q&A — ship as YAML + prompt builders + (optional) rubric markdown. No worker deploy required for a new flow once the relevant runners exist.
- Per-job scenario dispatch. Each scenario YAML declares the
intent_routevalues it serves (code_build→project,document_writing→document/writing,quick_qa→task/qa, …). The worker compiles every loaded scenario once at startup and picks one per job from (in priority):payload.metadata["scenario_id"]→payload.metadata["intent_route"]mapped via the loader →CEREBRUM_DEFAULT_SCENARIO. The command widget’s task type setsintent_route; the future universal-input classifier will set it (or pinscenario_iddirectly) by classifying the prompt. - Per-level model pinning. Each level resolves its own
ModelConstraintthroughworker/worker/llm/resolver.py, so an L4 executor can be pinned to Claude Opus while a Q&A scenario runs the whole thing on the free tier. - Pluggable verifiers (subprocess + LLM judge + schema + MCP). Verifiers can
run on prompt-kind levels (e.g., an
llm_judgerubric onl3_accept) or on a dedicated verifier-kind level that gates the next branch. - First-class DAG branching. Edges carry a typed boolean DSL
(
verdict == 'PASS' && !escalate_now); the compiler evaluates them at routing time.fan_outparents run children in parallel; aggregators (majority_pass,weighted_score,all_pass) merge results. - Agentic file-tool loop.
kind: tool_looplevels (l4_execute,l2_escalate) mutate the project workspace through tool calls the worker executes — surgicaledit_files on the scaffolded template, not a full-file Markdown blob. The workspace filesystem is the shared state; levels exchange paths + summaries. - Human approval gates. A
kind: human_approvallevel pauses the graph via LangGraph’sinterrupt(), packages a question for the approver, and resumes with their reply as the gate-readableverdict. - Budget kill switch + user Stop. Scenario-wide caps (cost / tokens /
duration) and a user-initiated Stop both set
aborted_reason, which short-circuits subsequent levels with synthetic skipped states and routes the gate straight toEND. The run endsscenario_finished status="aborted"(a cap) or"cancelled"(a Stop); a cancelled run discards partial work — no commit/push. Stop is a Redis flag (cerebrum:job:{id}:cancel) the worker polls between levels and per tool-turn viaEventBus.is_cancelled(). - Per-tenant overrides. A partial YAML stored in Postgres deep-merges over the base scenario at orchestrator construction time, so an org can tweak budgets / constraints / verifier weights without forking the base file.
Module layout
All paths underworker/worker/graph/scenarios/:
schemas.py— Pydantic models with strict (extra="forbid") validation.Scenario,Level,Edge,Gate,Budget,ModelConstraint,VerifierRef,FanInRef. Composite primary-key validation (unique level ids, edges target existing levels or the__end__sentinel, mixed-tier allows rejected).state.py—ScenarioStateTypedDict threaded through the LangGraph. Explicit top-level fields for every value the gate DSL or runners read:verdict,escalate_now,confidence,aborted_reason,budget_exceeded, per-level entries underlevels: dict[str, LevelState].scratchpad.py— Redis-backed scenario-scoped KV (cerebrum:job:{job_id}:scratchpad). Available to every level via thescratchpad.{get,put,list}intra-tools.expr.py— Recursive-descent parser for the edge DSL. Supports paths (verifier.build.passed), string/number/bool literals,==/!=/<,<=,>,>=,!,&&,||, parens. Missing paths resolve to a fail-closedUNDEFINEDsentinel. Loader compiles everyEdge.whenat load time.loader.py—ScenarioLoaderreads*.yamlfromknowledge-hub/scenarios/.refresh()swaps the registry atomically and preserves the old version on any validation error.with_overrides({tenant_id: partial_yaml})deep-merges tenant tweaks.Scenario.checksumis the sha256 of the canonical YAML, recorded on eachScenarioResultfor reproducibility.compiler.py—ScenarioCompilerturns aScenariointo a LangGraphStateGraph. One node per level; non-conditional edges viaadd_edge; multi-branch edges viaadd_conditional_edgeswith a router that evaluates the edge DSL and emitsgate_decisionevents.executor.py—ScenarioExecutordispatches each level to its kind-specific runner. Wraps every invocation withlevel_started/level_finishedevents alongside the legacynode_started/node_finishedpair (back-compat through one release).runners.py—PromptLevelRunner,ToolLoopLevelRunner,VerifierLevelRunner,FanOutLevelRunner,HumanApprovalLevelRunner. See Scenario execution for the per-runner semantics.ToolLoopLevelRunnerdrives thekind: tool_looplevels (l4_execute,l2_escalate): an agentic file-tool loop where the model mutates the workspace throughread_file/edit_file/write_file/list_dir/delete_file/finishtools — the filesystem is the artifact, downstream levels receive paths, not file bodies.prompts.py— Registered prompt builders for every scenario level (l1_team_lead_brief,writing_outline,security_triage,qa_answer, etc.). Builders return aBuiltPrompt { body, response_kind, handoff_* }and read prior-level outputs fromstate["levels"]. code_build’s L1 emits a Markdown brief and L3 emits Markdown prose + a trailing```controlblock carrying the verdict (worker/runtime/control_block.py) — not a JSONAgentMessageenvelope; the runner runs a bounded repair-retry when the verdict won’t parse.worker/runtime/file_tools.py— the six sandboxed file tools + the native (Anthropic) and textual transports the tool loop dispatches.aggregators.py—MajorityPassAggregator,WeightedScoreAggregator,AllPassAggregator. Aggregator name is referenced from the parentLevel.fan_in.aggregatorfield.tenant_overrides.py—TenantOverrideProviderProtocol with two impls:InMemoryTenantOverrideProvider(tests) andPostgresTenantOverrideProvider(production). DB schema is onworker/worker/storage/postgres_repo.pyundertenant_scenario_overrides.replay.py—CassetteProviders+ReplayHarness. Drives a scenario through recorded model responses (no live calls) and asserts no verifier or rubric-score regression beyond the scenario’sregression_thresholds. Runs in CI via.github/workflows/scenario-replay.yml.orchestrator.py—ScenarioOrchestratoris the public entry the worker calls. Samerun(payload, routing_decision, event_bus) -> ScenarioResultcontract the legacyHierarchyOrchestratorexposed.
worker/worker/runtime/verifiers/
and is consumed by both PromptLevelRunner (when a prompt level declares
verifiers) and VerifierLevelRunner. Built-ins: build (subprocess install +
build), typecheck, lint, unit_test, schema (JSON Schema + Pydantic),
llm_judge (rubric-scored), semgrep, gitleaks.
Lifecycle events
Worker emits both the legacy and the Phase-4 lifecycle events for one release. Frontend renders the new events viaapps/frontend/components/workflow/scenario-run-timeline.tsx.
| Event kind | Payload highlights |
|---|---|
scenario_started | scenario_id, scenario_version |
scenario_finished | status: completed | aborted, aborted_reason, totals |
level_started | level_id, role, kind, attempt, model_constraint_allow |
level_finished | ok, attempt, duration_ms, cost_usd, output_preview |
verifier_started | level_id, verifier_name, kind |
verifier_finished | passed, score, reasons, cost_usd, duration_ms |
gate_decision | source_level, target_level, matched_expression |
budget_consumed | cumulative total_cost_usd, total_tokens, total_duration_ms |
budget_exceeded | limit_kind, limit_value, observed_value |
worker/worker/runtime/agent_messages.py.
TypeScript mirror in
apps/frontend/lib/types/agent-event.ts.
Bundled scenarios
| Scenario | Domain | Special primitive |
|---|---|---|
code_build | code | Build verifier; L4 pinned paid |
code_build_strict | code | Parallel build/typecheck/lint + llm_judge architecture |
document_writing | writing | Editorial chain + fact-check judge |
research_brief | research | fan_out[web, kb, academic] + rigor judge |
quick_qa | qa | Single-level + schema intra-verifier |
security_review | security | human_approval gate + semgrep/gitleaks |
parallel_review_demo | demo | (test scaffold for fan_out) |
knowledge-hub/rubrics/:
architecture_review_v1, fact_check_v1, rigor_v1.
CI gate
.github/workflows/scenario-replay.yml
runs on every PR touching knowledge-hub/scenarios/**,
knowledge-hub/rubrics/**, or worker/worker/graph/scenarios/**. It loads each
scenario’s golden fixtures from
knowledge-hub/scenarios/<id>/golden/*.json and replays them through
ReplayHarness against CassetteProviders. The job fails on any verifier or
rubric regression beyond declared thresholds.
Impact scope
- Worker: hot path for every paid job goes through
ScenarioOrchestrator. LegacyHierarchyOrchestratoris deleted; the wrappingWorkerOrchestratoris unchanged. - Frontend:
JobDetailPanelmountsScenarioRunTimelinefor every job. Renders nothing on legacy backlog events for backwards compat. - Postgres: one new table
tenant_scenario_overrides(composite PK ontenant_id, scenario_id). - Defaults:
code_buildis the default scenario; override via theCEREBRUM_DEFAULT_SCENARIOenv var or per-jobscenario_idmetadata. - Backwards compat: legacy
node_started/node_finishedevents still fire alongside the newlevel_*events for one release.
Tests
All tests live at repo roottests/. The scenario runtime suite:
test_scenarios.py(28) — schemas, loader, scratchpad.test_scenario_expr.py(25) — gate DSL.test_verifiers.py+test_verifiers_phase2.py(55) — verifier framework + built-ins.test_scenario_compiler.py(10) — compiler topology + end-to-end with mock runners.test_scenario_prompts.py(10) — prompt-builder parity.test_scenario_orchestrator.py(9) — production orchestrator with fake providers.test_scenario_events.py(13) — Phase-4 lifecycle event payloads.test_scenario_phase5.py(12) — fan_out + aggregators + budget kill switch.test_scenario_phase6.py(16) — checksum, hot-reload, tenant overrides, replay harness.test_tenant_overrides.py(12) — DB-backed tenant overrides.test_model_resolver.py(15) — per-level model pinning.test_document_writing.py(15),test_quick_qa_and_research_brief.py(12),test_security_review.py(15) — bundled scenarios end-to-end.
test_provider_registry.py, test_scheduler_producer.py,
test_context_engine.py — all WorkerConfig.__init__ drift not introduced by
the scenario work).