| # Scenario Config Contract |
|
|
| A scenario is declarative data: a goal, a seed, and a **cast of agent names**. |
| Defined by `ScenarioConfig` (`src/core/config.py`), loaded from |
| `config/scenarios/<name>.yaml`, validated by `validate_scenario()`. |
|
|
| ## Schema |
|
|
| ```yaml |
| name: mystery-roots # unique slug (required) |
| title: "π Mystery Roots" # display name for the UI (optional) |
| goal: > # shared objective, injected into every prompt |
| Converge on the most interesting, evidence-supported explanation. |
| default_seed: "All the clocks stopped at 3:07." # required |
| example_seeds: # gallery seeds for the UI |
| - "All the clocks stopped at 3:07." |
| - "The bridge appeared overnight." |
| cast: # agent names, resolved via the agent registry |
| - clue-gatherer |
| - hypothesis-former |
| - devils-advocate |
| - mystery-judge |
| genesis_text: "A mystery settles over the wood: {seed}" # '{seed}' substituted |
| competition: # optional contest contract (ADR-0029); absent == none |
| kind: judged # versus | judged | none |
| governor: # optional per-scenario budget (else defaults) |
| max_turns: 2000 |
| max_calls_per_turn: 16 |
| max_total_calls: 20000 |
| ``` |
|
|
| ## Fields |
|
|
| | Field | Meaning | |
| |---|---| |
| | `name` | Unique slug; the registry key. | |
| | `title` | UI display label; falls back to `name`. | |
| | `goal` | The shared objective. Rendered as a `SHARED GOAL` block in every agent prompt and carried on the genesis `run.started` event (`projection.goal`). This is how a scenario "sets up the goal." | |
| | `default_seed` | Seed used when none is supplied. | |
| | `example_seeds` | Seed gallery for the UI dropdown. | |
| | `cast` | Agent names that participate. **Selecting who participates is editing this list.** Each must exist in `config/agents/`. | |
| | `genesis_text` | Template for the opening `world.observed`; `{seed}` is replaced. | |
| | `competition` | Optional `CompetitionConfig` β does this scenario produce a winner, and how? See below. | |
| | `governor` | Optional `GovernorConfig`; omit for engine defaults. | |
|
|
| ## Competition: who can win, and who decides |
|
|
| A scenario declares whether it produces a winner with the optional `competition:` |
| block (`CompetitionConfig`, ADR-0029). Absent block == `kind: none` β full sessions |
| and history, but nobody wins. |
|
|
| ```yaml |
| competition: |
| kind: versus | judged | none # default none |
| teams: # versus only |
| spy: [spy-nil] |
| herd: [spy-cara, spy-bex, spy-ovo] |
| ``` |
|
|
| The three kinds split *who derives the winner*: |
|
|
| | kind | ground truth? | winner derived by | shipped example | |
| |---|---|---|---| |
| | `versus` | yes β the team map | **code** β the scenario's handler scores the judge's accusation against `teams` | `the-steeped` | |
| | `judged` | no β judgment *is* the result | **the model** β the judge's validated `winner` field | `mystery-roots` | |
| | `none` | n/a | nobody β judges don't declare `winner` at all | everything else | |
|
|
| Validation rules (enforced in `CompetitionConfig` and `WorldConfig`, |
| `src/core/config.py` β a bad block fails loudly at load, per ADR-0011): |
|
|
| - `teams` is permitted only when `kind: versus`, and is required (non-empty) there. |
| - Member lists must be non-empty and **mutually disjoint** β no double agents. |
| - Every team member must appear in the scenario's `cast`. |
| - **No team label may equal an agent name.** The `winner` payload key carries either |
| an agent name or a team label; this rule keeps that union unambiguous. |
|
|
| The registry injects the competition context into the cast's agents at build time |
| (the same seam as `agent.manifest`), which arms verdict validation in the base agent. |
| The machine-readable verdict and run-summary keys this produces are documented in |
| [events.md](events.md#verdict-and-run-payloads-adr-0029). |
|
|
| ## Scheduling lives on the agents |
|
|
| A scenario does **not** declare a scheduling policy. Cadence is per-agent β |
| each cast member's manifest carries `subscribes_to` (reactive) and |
| `schedule.tick_every` (periodic). The conductor routes accordingly. (The legacy |
| `Scenario.schedule()` method remains only as the Phase-0/1 fallback for agents |
| without a manifest.) |
|
|
| ## Building one |
|
|
| ```python |
| from src.core.registry import default_registry |
| scenario = default_registry().build_scenario("mystery-roots") # cast -> live agents |
| ``` |
|
|
| See also: [agent-manifest.md](agent-manifest.md), [world-config.md](world-config.md). |
|
|