Agent Workflow¶
Guide version 1.1 — OpenCDA-MARL 2.1 (2026-10)
Open this before changing code: it is how changes are made to OpenCDA-MARL, written for coding agents (Claude Code, Codex, Cursor, Copilot, …) and applying equally to people. Read it with the Code Index, which maps where things live, and Conventions for naming, versioning and known issues. Nothing here is decorative — each rule exists because its absence cost a debugging session.
The map¶
The Code Index is the module-level map; this table is the edit policy per area.
| path | what it is | how to treat it |
|---|---|---|
opencda_marl/ |
the framework, in four zones: the sealed kernel (_kernel/), the public surface (api/), the plugin system (plugins/), the built-in plugin set (builtin/), plus facades (envs/) and services (telemetry/, evaluation/, utils/, tools/) |
keep the seams below; the architecture test enforces the zone boundaries |
opencda_marl/api/ |
everything third parties import: contracts (policy.py, contracts.py, events.py) and every register_* door |
public API — a change here updates Add an Algorithm in the same commit and may bump the plugin API version |
opencda_marl/_kernel/ |
the complete lifecycle — sealed | never imports a plugin; a new behaviour is a registration, never a new if algorithm == …. External code and plugins never import it |
opencda/ |
vendored OpenCDA core with local patches | every modification is a row in the patch ledger |
opencda_marl/_bundled/configs/<family>/ |
default.yaml → <scenario>/base.yaml → profile; the package's own data, shipped in the wheel |
profiles state only what they change |
artifacts/_runtime/ |
per-cell profiles written by the matrix launcher, resolved as an extra config root | generated and gitignored — never hand-edit |
scripts/experiments/ |
matrix launcher, evaluation launcher, analyzers | the experiment-matrix tooling in Run Experiments |
opencda_marl/_bundled/recordings/*.json |
deterministic traffic replays | evaluation ground truth: changing one invalidates every comparison made on it — do not modify |
artifacts/, checkpoints/, output/ |
run outputs (gitignored) | possibly someone's only copy — inventory before deleting |
test/ |
pytest suites (basic, marl, opencda, route_intent) |
every change ships with its test |
docs/ |
this portal (mkdocs.yml nav is the table of contents; the agent guides in docs/agents/ are built but kept out of the nav) |
put a feature on the page that owns it; a new topic gets a new page listed in the nav |
opencda_marl/builtin/algorithms/ |
the first-party learners; contrib/ inside it is the drop-in folder for cloned-repo algorithms |
loaded through the wheel's own entry point — engine code never imports a learner module |
.pixi/, dependencies/whl/ |
environment and vendored wheels | never edit; everything runs through pixi run |
The seams¶
These are the invariants that keep the framework pluggable. A change that needs to break one is a design change, and the PR says so.
- Algorithms do not edit the engine — and the engine imports none.
The engine guarantees observation delivery and action application; every
learner, the wheel's own
builtin/algorithms/included, arrives throughregister_algorithm(...)+ theopencda_marl.pluginsentry point and declaresPolicyCapabilities. Dispatch sites read capabilities, never algorithm names orisinstancechecks; if a feature needs a name list, the capability is missing — add it. - One episode loop for both backends.
MARLCoordinatordrives CARLA (MARLEnv) and SUMO (SumoMARLEnv) through the samerun()/step()/reset_episode()/close(); backend-specific behaviour lives in the environment, not in the coordinator, launcher or monitor. - World reloads carry state.
TrainingStateSnapshot(in_kernel/lifecycle/world_manager.py) captures and restores learner state across a CARLA world reload. Anything kept on the environment or manager that must survive a reload goes through it. - Per-episode state has a home. SUMO:
SumoEpisodeState(env.episode). New per-episode fields go there, never as loose attributes with hand-written clears. - Observations are str-keyed downstream of the extractor. Raw dicts are
keyed by CARLA actor ids (int) or SUMO ids (str); never
int(agent_id)— a source-scan test enforces it. - Metrics have one sink. Learners log through
TrainingLogger(log_scalar/log_episode_metrics), which feeds the metrics store (and TensorBoard when enabled). External policies are covered byMARLManager.record_update_metrics.
Commands¶
pixi run verify # the repeatable battery: suite + registry + components + a real SUMO training run (--full adds CARLA)
pixi run pytest test -q # the whole suite (~5 min, no CARLA needed)
pixi run pytest test/route_intent -q # the lifecycle / learner suite
pixi run python -m opencda_marl.plugins # what is registered, and from where
pixi run python opencda.py -t sumo-marl/intersection/route_speed_only_pcmappo --marl # SUMO smoke
pixi run marl-quick-test # CARLA smoke (server running)
pixi run -e docs mkdocs build --strict # docs must build clean
Tests do not touch CARLA; they skip rather than fail when a GPU lacks
headroom. test/conftest.py puts the repo root on sys.path, forces the
Agg backend and disables the metrics store — new test files add no path
hacks and do not switch the backend; test/opencda/ never gets an
__init__.py (it would shadow the real package).
Verification duties¶
A change is done when it is verified, not when it is written. In order of cost:
- Unit tests for the change — a new behaviour gets a test that fails without it. Known failures are never "fixed" by weakening assertions.
- The suites green on the platform you changed (
pixi run pytest test -q). Lifecycle, launcher, config and registry changes are run on both SUMO and CARLA code paths (the tests cover both without a server). - A live smoke run for anything that touches the episode loop, world
reloads, the launcher, checkpoints/resume or the monitor: one
short SUMO run always; one CARLA run when CARLA code changed. Read the
learner's counters (
training_step, updates, buffer size) and the monitor, not just the episode summary. git grepafter any move or rename for stale paths — in code, configs, pixi tasks, docs.- Docs in the same commit: a new flag, config key or command appears on the page that owns it (Run Experiments, Add an Algorithm, the Code Index, this page).
Pitfalls that have fooled agents before:
- Learners whose
update()catches exceptions return stale metrics; anisfiniteassertion passes on a stale0.0. Assert that a counter advanced and the loss is non-zero, or read the ERROR log. - Bit-identical summaries across two runs of a stochastic learner mean it never acted (a warmup that never ends). Determinism where noise is expected is a failure signal.
- Seeded outcome numbers are not comparable across commits that change numerics at the ulp level; pin semantics with equivalence tests, not with reward numbers.
- cProfile inflates tiny functions; device syncs (
.item(),.any(), boolean-mask gathers) andgc.collect()are real wall-clock. Microbenchmark before optimising. - A failed CUDA-graph capture poisons the process — the feature stays opt-in and fails fast.
Commits and pull requests¶
- Conventional commits, one verified change each:
feat(sac): …,fix(launcher): …,perf(envs): …,docs(marl): …,test(route_intent): …,chore(repo): …. The body says what was wrong, what changed, and how it was verified (the command and the observed result). - Work on a branch of your fork; open the PR against
radar-lab/OpenCDA-MARLmainusing the PR template. Maintainers review and squash-merge; nobody pushesmaindirectly. -
Agent-authored PRs are welcome and say so. The template's checklist is the review contract:
- the change is scoped to one concern; unrelated cleanups are separate PRs
- tests added or updated;
pixi run pytest test -qis green (paste the summary line) - live smoke run for lifecycle / launcher / monitor changes (paste the command and the counters you read)
-
opencda/edits are recorded in the patch ledger - docs updated on the owning page;
mkdocs build --strictpasses - no new algorithm-name dispatch; capabilities declared instead
- no generated files (
_runtime/,artifacts/, checkpoints, recordings), no secrets, hostnames, absolute paths or private URLs - the PR description states what an agent could not verify (no GPU, no CARLA) so a maintainer can
Reporting¶
Lead with the outcome. Failing tests are reported with their output, never summarised away; a skipped verification step is named as skipped. A number quoted anywhere (speed, reward, pass count) comes from a run you can point to.