Skip to content

Scenario framework: marathon / DUMBO pedestrian / evacuation on one kernel - #1

Draft
yorkerhodes3 wants to merge 12 commits into
mainfrom
agents/marathon-agent-decision-analysis
Draft

Scenario framework: marathon / DUMBO pedestrian / evacuation on one kernel#1
yorkerhodes3 wants to merge 12 commits into
mainfrom
agents/marathon-agent-decision-analysis

Conversation

@yorkerhodes3

@yorkerhodes3 yorkerhodes3 commented Aug 15, 2026

Copy link
Copy Markdown

Generalises the marathon engine into a scenario-parameterised one, so the same physics kernel runs a marathon, a pedestrian district, and an urban evacuation. CI green.

Exactly one pre-existing file changed (agents/runner/running.py: 147 lines of physics out, 14 lines of plumbing in). Everything else is new.

Why this is safe to review

Marathon output is pinned by a committed golden fixture and is byte-identical across every refactor commit. The fixture is proven falsifiable, not merely asserted: perturbing BASE_DEPLETION_RATE in the 4th decimal fails with tick drift for golden-runner-000 at tick 4.

What it adds

Area Files
Pure physics kernel agents/runner/kernel.py
Frozen scenario schema agents/scenarios/spec.py
Goal selection + arrival agents/scenarios/goals.py
Hazard / perception / salience agents/scenarios/hazard.py
Pooled narrative agents/scenarios/narrative.py
Scenario packs marathon.py, dumbo.py, mariupol.py

The unifying abstraction is the destination: a marathon water stop, a subway entrance, a shelter and an aid post are one type, differing by whether they end the run, how they rank, their capacity, and when they are open. Closing a destination is how hazards and timetables remove options.

Marathon is deliberately the degenerate case — one terminal destination, no personas — which is what makes byte-identical reproduction a correctness test rather than a coincidence.

Tests

1757 passed, 0 failed (baseline was 1525). test_all_scenarios.py runs one invariant suite across all three scenarios: adding a fourth means adding one entry to ALL_SCENARIOS.

Three bugs were caught by these tests before merge: PromotionBudget excluded the lowest-salience agent under capacity; persona attributes were mixed into persona constraints (making every destination ineligible, silently returning None); and 33 pyright errors from unnarrowed Optional returns.

Ethics boundary, enforced by tests

For the evacuation scenario these are assertions, not conventions:

  • collapse_threshold=0.0 makes 'collapsed' unreachable; a test drives an agent at zero resource for 40 ticks to prove it.
  • A test asserts no hazard or casualty field ever reaches the simulation payload — hazards affect routing and attention only.
  • The evacuation narrative corpus ships reviewed=False and renders nothing until signed off; a tone guard rejects graphic vocabulary.

Not yet wired

The packs do not yet drive the live simulator — the gateway and tick loop still run the marathon. That plus the density field, capacity/queueing, household cohesion, and causal LLM decisions are documented in the README's Work in progress section and in the in-code FERRY_MODELLING_GAPS / MODELLING_LIMITS.

Copilot AI added 9 commits August 15, 2026 13:00
Phase 0 of the three-scenario generalization work: establish a
falsifiable regression contract before any refactor begins.

agents/tests/test_golden_marathon.py + golden_marathon.json
  Pins the exact numeric trajectory of the deterministic runner
  pipeline (8 runners x 10 ticks, fixed session IDs). Runner profiles
  derive from sha256(session_id) and process_tick is pure arithmetic,
  so the trajectory is reproducible bit-for-bit. inner_thought is
  excluded so the fixture is valid for both the autopilot and LLM
  paths. Verified to detect drift: perturbing BASE_DEPLETION_RATE in
  the 4th decimal fails with the exact runner and tick.

agents/runner/tests/test_llm_contract_guard.py
  The LLM runner path is not exercised by any existing test, so it can
  break at runtime while the suite stays green. Pins the three real
  break risks: every {placeholder} in RUNNER_DYNAMIC_INSTRUCTION is set
  by initialize_runner, inner_thought stays schema-required, and the
  static instruction matches the tool signature. Verified: renaming
  state['water'] is caught here while all other tests still pass.

No existing files modified. Suite: 1534 passed, 3 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Splits the per-tick dynamics out of the ADK tool wrapper so the
simulation physics can be exercised without ADK, Redis, or async.
Prerequisite for expressing marathon / evacuation / pedestrian as
swappable scenarios over one shared kernel.

agents/runner/kernel.py (new)
  step(state, env, inner_thought) -> dict, plus a frozen TickEnv
  carrying tick/timing/course/session_id. Owns velocity degradation
  (hydration, wall, fatigue), distance, hydration depletion, seeded
  auto-hydration, exhaustion/collapse, finish interpolation, and wave
  stagger. Physics moved verbatim -- no arithmetic was changed.

agents/runner/running.py
  process_tick is now a thin adapter: resolve sentinel tick params,
  build TickEnv, call step(), direct-write to the collector buffer.
  147 lines of physics -> 14 lines of plumbing. get_shared_redis_client
  stays imported here so existing patch targets keep working.

agents/runner/tests/test_kernel.py (new)
  17 tests using a plain dict and no ToolContext -- the property the
  extraction exists to provide. Covers the velocity closed form,
  dehydration scaling, wall gating, finish clamping, finished/collapsed
  no-ops, thresholds, wave stagger, and determinism (including a guard
  that station drinking never touches the global RNG).

Golden fixture byte-identical. Suite: 1551 passed, 3 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…se 2)

Generalizes the marathon simulation into a scenario-parameterised one so
the same engine can run a marathon, a pedestrian district, or an
evacuation. Freezes the contract that scenario packs are authored
against -- the point at which pack development can safely parallelise.

agents/scenarios/spec.py (new)
  Frozen dataclasses: DestinationKind (terminal vs opportunistic),
  Destination (rank/capacity/open window/constraints), PhysicsSpec,
  ProfileSpec, PersonaSpec, TerminationSpec, ScenarioSpec. The unifying
  idea is the destination: a marathon water stop, a Dumbo subway
  entrance, an evacuation shelter and an aid post are one type. Ranked
  + closable destinations are what let a scenario say 'A/High St first,
  else F/York St, else the ferry'.

agents/scenarios/marathon.py (new)
  Marathon as the reference scenario and regression anchor. Every value
  is imported from agents/runner/constants.py rather than re-typed, so
  the constants stay the single source of truth; a test asserts the
  wiring. Marathon is deliberately degenerate: one terminal
  destination, no personas, opportunistic hydration.

agents/runner/kernel.py
  TickEnv gains physics: PhysicsSpec defaulting to MARATHON_PHYSICS,
  and step() reads constants from it instead of module globals.
  Existing callers are unaffected. Spec field naming uses 'resource'
  rather than 'water' since the mechanic is generic.

agents/scenarios/tests/test_spec.py (new)
  19 tests pinning the schema: frozen-ness, terminal/opportunistic
  partition, closure fall-through to the next-ranked exit, marathon
  spec matches the constants module, and declared water stations line
  up with the kernel's int(distance/interval) marker maths. Verified
  both directions -- default reproduces the golden fixture exactly,
  swapping speed_scale provably changes output.

Golden fixture byte-identical. Suite: 1570 passed, 3 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the hard-coded 'distance >= race_distance_mi' termination with
a goal-driven arrival predicate, so agents can head for ranked
destinations that close underneath them. Completes the pure-refactor
phases: marathon output is still byte-identical.

agents/scenarios/goals.py (new)
  select_goal() picks the best open, non-excluded terminal destination,
  honouring persona constraints (e.g. step_free) and persona kind
  preferences over the scenario's ranking.
  next_goal_after_closure() keeps the current goal while it is valid and
  otherwise falls through to the next rank -- the mechanism hazards and
  schedules use to remove options.
  goal_distance_mi()/has_arrived() generalise the finish comparison;
  with no goal they reduce to the original marathon behaviour.
  Selection is deterministic (no RNG), so goal choice cannot perturb the
  seeded trajectories the golden fixture pins.

agents/runner/kernel.py
  TickEnv gains an optional goal. The arrival bound is the goal's
  position when set, else race_distance_mi -- identical for marathon.
  Terminal arrival keeps runner_status='finished' because the frontend
  and the simulator's aggregation both key off that exact string; the
  specific destination is reported separately via arrived_at. goal_id /
  goal_kind / arrived_at are emitted only when a goal is set, so
  marathon payloads keep their original key set exactly.

agents/scenarios/tests/test_goals.py (new)
  21 tests over a Dumbo-shaped fixture (A/High St, F/York St, a
  schedule-constrained ferry): preference ordering, closure
  fall-through, persona constraint filtering, 'no way out' returning
  None, and a kernel run that finishes at a 0.3 mi subway entrance.

Golden fixture byte-identical. Suite: 1591 passed, 3 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
First scenario that is not a race, and therefore the first real test of
the generalisation. Runs on the unmodified kernel -- no code branches,
only different constants and destinations.

agents/scenarios/dumbo.py (new)
  Exits are transit, ranked: A/C High St, then F York St, then the East
  River Ferry. Opportunistic stops cover the Washington St bridge view,
  Jane's Carousel, Pebble Beach, a water fountain and a first aid post.
  Four personas -- commuter, tourist, family, mobility_limited --
  replace the marathon's continuous ability scalar.

  Physics: velocity=1.0 is ~3.1 mph walking rather than 6.2 running,
  and the resource mechanic is deliberately inert
  (min_resource_factor=1.0, negligible depletion) because nobody
  dehydrates crossing a half-mile district.

  Accessibility is modelled, not decorative: High St is stairs-only, so
  family and mobility_limited personas route to York St via persona
  constraints. Commuters ignore the ferry; tourists prefer it while it
  is running.

  FERRY_MODELLING_GAPS records what is knowingly unmodelled -- capacity
  is declared but not enforced, boarding is instantaneous rather than a
  batch at departure, and only a single service window exists. A ferry
  is a vehicle, not a doorway; that needs the queueing work in later
  phases.

agents/scenarios/tests/test_dumbo.py (new)
  24 tests: exit ranking, ferry service window, accessibility routing,
  closure fall-through to York St and then to the ferry, realistic
  walking pace, no spurious collapse, and reproducibility from
  session_id independent of the global RNG.

Marathon golden fixture untouched. 156 scenario/runner tests green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…(Phases 6-7)

Completes the three-scenario eval set: marathon, DUMBO pedestrian, and
urban evacuation all run on the same unmodified kernel.

agents/scenarios/hazard.py (new)
  Hazard (onset, origin, severity, growth, warning) plus three
  deterministic layers: seeded four-channel perception (sight, sound,
  broadcast, word of mouth), blocked_destination_ids() feeding the
  existing goal fall-through, compute_salience() weighted so
  time-to-impact dominates, and PromotionBudget admission control.

  The budget exists because a hazard alerts every agent in radius on the
  SAME tick -- a thundering herd against a fixed tick window. The
  simulator derives a threshold from the previous tick's salience
  distribution; agents apply it locally with no coordination. Fails
  closed: nothing is promoted until a threshold is broadcast.

  Word of mouth degrades to zero without density data, which is why
  hazards do not depend on the density field landing first.

agents/scenarios/mariupol.py (new)
  Schematic urban evacuation: ranked corridor exits that close under
  threat, shelters as the alternative to leaving, household personas,
  and three hazard archetypes with distinct behaviour.

  Ethical boundary enforced by tests, not convention: collapse_threshold
  is 0.0 so 'collapsed' is unreachable, and tests drive an agent at zero
  resource for 40 ticks asserting it never collapses, and assert no
  hazard/casualty key ever reaches the kernel payload. Attrition is out
  of scope by construction.

agents/scenarios/spec.py
  PersonaSpec gains �ttributes, separating descriptive persona facts
  from constraints (hard destination requirements). Mixing them made
  every destination ineligible and goal selection silently returned
  None -- found by tests, now guarded permanently.

agents/scenarios/tests/test_all_scenarios.py (new)
  One invariant suite over all three scenarios: structure, persona
  validity, physics sanity, and runtime behaviour on the shared kernel
  (monotonic distance, resource in range, reachable goals,
  reproducibility). Adding a fourth scenario means adding one entry to
  ALL_SCENARIOS and inheriting the whole suite.

Marathon golden fixture byte-identical. Suite: 1730 passed, 4 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces per-agent LLM narration with a reviewed static corpus indexed
by the seeded RNG the simulation already uses. Needs no inference at
runtime, so it ships without waiting for a model endpoint.

Rationale: inner_thought is non-causal (a display string that cannot
affect dynamics) and is produced far faster than it can be consumed --
at 1000 agents the simulation generates ~100 thoughts/sec while the UI
shows one and rotates it every 5s. The repo already concedes this: the
frontend ships EXTERNAL_THOUGHTS, 65 hand-written strings used whenever
an agent-authored thought is absent. This makes that the primary
mechanism and makes it persona- and situation-aware.

agents/scenarios/narrative.py (new)
  NarrativePhase x ResourceBucket derived from agent state. Pools keyed
  (persona, phase) with a wildcard fallback chain, so specific lines are
  authored only where a persona's voice genuinely differs. Selection is
  seeded from (session_id, persona, phase, bucket, tick): same run, same
  narrative, and the global RNG cannot perturb it.

  Corpora for all three scenarios. The evacuation corpus ships
  reviewed=False deliberately -- thought_for() returns '' until someone
  with the relevant expertise signs it off. For a scenario shaped by
  real events a reviewed static corpus is the correct design, not a cost
  compromise: it can be vetted before anyone sees it, it is deterministic
  and therefore auditable, and it removes any possibility of a model
  improvising about a real siege.

agents/scenarios/tests/test_narrative.py (new)
  27 tests: fallback chain, determinism, per-agent and per-tick
  variation, the review gate, and a guard that every returned string
  comes from the corpus (no generation). Plus corpus quality checks --
  HUD length limits, no duplicates, full phase coverage, distinct
  persona voices, and a tone guard rejecting graphic vocabulary in the
  evacuation corpus.

Still pending an endpoint: cohort-cached causal decisions, which cannot
be pooled because the output depends on the agent's situation. The seam
is ready (RUNNER_MODEL=openai/... + OPENAI_API_BASE) and PromotionBudget
already caps how many agents may reason per tick.

Suite: 1757 passed, 4 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The README described the backend evacuation behaviour as 'largely still
an RFC', which is now out of date, and never explained how the
simulation actually decides anything.

Adds 'How the simulation works': the four-layer model (identity /
policy / physics / world) and the property that matters most for
repurposing the engine -- L2 physics is authoritative over L1 policy, so
a model can bias the kernel's inputs but never override its constraints.
Also records that everything is reproducible from sha256(session_id),
and that the LLM's causal footprint is limited to inner_thought.

Documents scenarios as data rather than code branches: the frozen spec,
the three packs, the destination abstraction that unifies water stops,
subway entrances, shelters and aid posts, and why marathon is
deliberately the degenerate case (it makes byte-identical reproduction a
correctness test). Adds sections on hazard perception/salience/promotion
budget and on pooled narrative.

Adds a testing section naming the three suites that guard scenario work,
plus the corporate-proxy and PYTHONIOENCODING workarounds needed to run
them.

Rewrites 'Work in progress' against the current state. Now-accurate
backlog, in priority order: wiring scenarios into the live tick loop,
the density field (no agent-to-agent interaction exists today),
destination capacity/queueing, household cohesion, causal LLM decisions,
narrative review sign-off, 2-D movement, and the frontend's hard-coded
marathon assumptions. Links the in-code FERRY_MODELLING_GAPS and
MODELLING_LIMITS so the gaps travel with the code.

Extends 'Fidelity & ethics' with the boundaries enforced by tests rather
than convention: attrition is unreachable in the evacuation scenario,
hazards affect routing and attention only, and the evacuation narrative
corpus renders nothing until reviewed.

All 30 relative links verified to resolve.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI runs 'make lint', which includes pyright; I had only run ruff
locally, so 33 type errors reached CI. Enabling Actions on the fork
surfaced them.

Root cause: select_goal() deliberately returns Destination | None --
'nowhere left to go' is a meaningful evacuation outcome, not an error --
so every call site is an Optional that tests must narrow before
dereferencing.

agents/scenarios/tests/helpers.py (new)
  require(value, what) asserts non-None and narrows the type. Better
  than a bare cast: a regression now fails with a clear message instead
  of an AttributeError on None.

Applied at every goal-dereferencing call site, plus:
  - Destination.coordinates narrowed before tuple unpacking
  - Destination.distance_mi narrowed before sorting and arithmetic
  - two 'assert <tuple constant>' checks changed to len(...) > 0, which
    pyright correctly flagged as always true
  - the golden fixture's _FakeToolContext given an explicit cast with a
    comment explaining why the test double is deliberate

pyright: 0 errors, 0 warnings. ruff: clean. 210 scenario/golden tests
pass; golden fixture unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yorkerhodes3 yorkerhodes3 changed the title test: marathon golden fixture + LLM contract guards (Phase 0) Scenario framework: marathon / DUMBO pedestrian / evacuation on one kernel Aug 15, 2026
Copilot AI added 2 commits August 15, 2026 20:47
The README still advertised the parent fork's build and badges. This
fork is now developed and published from Ethical-Tech-CoLab, so the
demo, badges and Actions links point there.

Enabled GitHub Pages on the org fork (build_type=workflow) and ran the
deploy, so the link is verified live rather than aspirational:
https://ethical-tech-colab.github.io/race-condition-mod/ returns 200 and
serves the app. Added a CI badge alongside the Pages badge.

Adds a Demos table that separates what works from what does not. The
marathon is the working reference simulation and the regression anchor;
everything else is marked WIP, because although the frontend renders the
city scenarios, none of them is yet driven by the live multi-agent
engine. Status columns added to both the scenario-pack table and the
backend ScenarioSpec table so the distinction is visible wherever a
reader lands.

Issue links deliberately still point at yorkerhodes3/race-condition-mod:
issues are disabled on this fork, so retargeting them would produce
404s. Called out explicitly so the split is not mistaken for an
oversight. Likewise the parent fork's build is named as such rather than
as 'upstream' -- upstream is GoogleCloudPlatform.

All 30 relative links resolve; every external URL verified 200; table
columns checked for consistency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Issues are now enabled on Ethical-Tech-CoLab/race-condition-mod and the
one open issue has been migrated, so tracker links point here instead of
at the parent fork.

VIS-1 moved from yorkerhodes3#20 to
#2. GitHub cannot transfer issues
between repositories with different owners (transferIssue rejects it), so
this was a recreate-and-close: the body, title and 'enhancement' label
were copied verbatim, a provenance header records the original author,
date and URL, and the original was commented and closed as 'not planned'
pointing at the new one. No split tracker.

Updates the README note (which previously explained that issues were
disabled here), the VIS-1 reference (yorkerhodes3#20 -> #2), the tracker link, and
the Tracking line in docs/BACKLOG.md, which keeps a link back to the
original for provenance.

All 30 relative links resolve; all 12 external URLs verified 200.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yorkerhodes3

Copy link
Copy Markdown
Author

📋 Documentation review requested: #3 (assigned to @carolina-moron)

The README rewrite in this PR is under first-time-reader review — checking that
status, WIP scope, and how multi-scenario support works are clear to
someone who did not build it.

Note for reviewers: the updated README lives on this PR's branch, not main,
so #3 links to the branch copy deliberately.

The analysis behind the scenario framework existed only as a session
artefact, so the reasoning for why the engine is shaped the way it is
was not in the repository. This commits it as an architecture document.

docs/architecture/runner_decision_model.md (new)
  Documents how an agent actually decides anything: the four layers
  (identity / policy / physics / world) and the property the whole
  design rests on -- L2 physics is authoritative over L1 policy, so a
  model can bias the kernel's inputs but never override its constraints.
  That is what makes the engine safe to repoint at an evacuation.

  Records the three schemas that do exist (seeded profile, plain-dict
  dynamic state, wire protocol), the full tick decision loop, and the
  finding most likely to surprise a reader: despite the agent README
  advertising LLM control over pacing and hydration, the shipped prompt
  restricts the model to one tool call per tick, leaving inner_thought
  as its only causal output. Everything else is deterministic given
  (session_id, tick).

  Also documents the A-H seams with current status, why marathon being
  the degenerate case makes it a byte-exact correctness test rather
  than a coincidence, and why per-agent inference does not scale -- the
  tick barrier, not cost, is the binding constraint.

  Written against the *current* code (kernel.py, agents/scenarios/),
  not the pre-refactor state the original analysis described. Every
  numeric claim re-verified against source: distributions and clamps in
  constants.py, the deliberate 100-vs-10 runner cap gap, the 10s tick
  interval default, and the 65-entry EXTERNAL_THOUGHTS fallback pool.

README.md, docs/architecture/README.md
  Link the analysis from 'How the simulation works' (now labelled as
  the short version), from the 'change agent behaviour' reading path,
  and from the architecture index.

All links verified: 17 in the new doc, 31 in README, 8 in the index.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants