Scenario framework: marathon / DUMBO pedestrian / evacuation on one kernel - #1
Draft
yorkerhodes3 wants to merge 12 commits into
Draft
Scenario framework: marathon / DUMBO pedestrian / evacuation on one kernel#1yorkerhodes3 wants to merge 12 commits into
yorkerhodes3 wants to merge 12 commits into
Conversation
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>
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>
Author
|
📋 Documentation review requested: #3 (assigned to @carolina-moron) The README rewrite in this PR is under first-time-reader review — checking that Note for reviewers: the updated README lives on this PR's branch, not |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_RATEin the 4th decimal fails withtick drift for golden-runner-000 at tick 4.What it adds
agents/runner/kernel.pyagents/scenarios/spec.pyagents/scenarios/goals.pyagents/scenarios/hazard.pyagents/scenarios/narrative.pymarathon.py,dumbo.py,mariupol.pyThe 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.pyruns one invariant suite across all three scenarios: adding a fourth means adding one entry toALL_SCENARIOS.Three bugs were caught by these tests before merge:
PromotionBudgetexcluded the lowest-salience agent under capacity; persona attributes were mixed into persona constraints (making every destination ineligible, silently returningNone); and 33 pyright errors from unnarrowedOptionalreturns.Ethics boundary, enforced by tests
For the evacuation scenario these are assertions, not conventions:
collapse_threshold=0.0makes 'collapsed' unreachable; a test drives an agent at zero resource for 40 ticks to prove it.reviewed=Falseand 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.