AgentCanvas / Pages / Developer Guide / Design Docs / Components / Env & Episode Lifecycle
2026-06-17

An evaluation run nests like rings: a job holds a suite, a suite holds episodes, an episode holds iterations. Each ring has a single owner β€” the env panel owns the heavy operations (build the env, choose the episode), graph nodes own the light ones (revive an episode, advance it), and the executor stitches the rings together with broadcast signals. This page is the ownership-and-timing map. The loop rings themselves are Loop Control; what the signals reset is State Containers; the panel contract is Env Panels.


1. The five scopes

Job β€” JobScheduler Suite / process β€” env panel + manager Episode placement β€” env panel Episode liveness β€” reset node In-episode advance β€” step_* + loop

Every lifecycle operation belongs to exactly one ring; the recurring failure mode is an operation drifting to a neighbouring ring's owner (Β§5).

ScopeOwnerKey operations
Job (cross-session)JobScheduleradmission (VRAM / exclusive-GPU), subprocess spawn, finalisation, orphan reaping (POST /api/eval/v2/start).
Suite / processEnv manager singleton, driven by the env panelinitialize() (build config, create env, reset to first episode), shutdown() β€” never a graph node.
Episode placementEnv panelfield cascade dataset β†’ split β†’ episode_index β†’ set_episode; emits the episode_reset request.
Episode livenessreset nodepre-loop band, fires once: idempotent ensure-live (habitat re-arms a finished episode; others read metadata β€” Β§6).
In-episode advancestep_* nodes + loop controlsim mutation, terminated/truncated emission; loop end seeds the after-loop band via final_*.

On top of the two innermost rings the executor lays its own banding: run_start β†’ pre-loop band (entry nodes, reset) β†’ loop (observe_* β†’ reason β†’ step_*) β†’ after-loop band (evaluate, fed once by the loop iterOut's final_* edges). The banding mechanics are Graph Executor; the loop's stop/final side is Loop Control Β§6.

2. The five signals

Signals are the only mechanism that crosses scope boundaries: the bus (broadcast_signal β†’ each container's on_signal) is the only lifecycle channel state containers respond to (State Containers Β§6). There are five canonical ones:

SignalEmitter β€” whenClears (lifetime)
run_startexecutor β€” before any node firesrun
run_endexecutor β€” at teardown (clean and error paths)β€”
step_startexecutor at the iterIn boundary β€” opens each iteration (payload carries scope_id)step (on the matching boundary)
step_endexecutor at the iterOut boundary β€” closes each iteration (payload carries scope_id)step
episode_resetenv panel, via the env-panel router β€” on a committed episode changeepisode

final_* is not a signal. The iterOut final side (final_<name> / final_stop) is a set of edge handles that route values to the after-loop band once at termination β€” it never goes through broadcast_signal. It seeds evaluate by ordinary dataflow, not by the signal bus. (Loop Control Β§6.)

3. Who owns each transition

TransitionOwnerMechanism
build / tear down the envenv panel + managerinitialize() / shutdown() (suite scope) β€” never a node
choose the episodeenv panelfield cascade β†’ set_episode, returns episode_reset intent
revive the episodereset nodepre-loop, idempotent ensure-live
advance the episodestep_* node + loopsim mutation; terminated β†’ iterOut.stop
run / step signalsexecutorrun_start/run_end/step_start/step_end broadcast at the band boundaries
the verdictevaluate nodeafter-loop band, fed once by final_*

4. Two driving modes

Interactive (canvas). The user opens the env panel; the field cascade initialises the manager and places an episode. Pressing Play returns a side_effect the frontend acts on (the panel only requests β€” Env Panels Β§6); the executor then runs run_start β†’ pre-loop reset (re-armed if the previous run finished the episode) β†’ loop β†’ after-loop evaluate. Re-running without touching the panel replays the same episode β€” placement is sticky, liveness is re-armed by reset.

Batch eval. The job is admitted by the JobScheduler and runs as a backend subprocess. Per episode the runner drives the same panel programmatically β€” on_field_change("episode_index", i) then on_action("play") β€” each env family keeping its own cascade shape. At worker_count > 1, env_panel_overrides map panel names to per-worker proxies so each worker's nodes and panel resolve to the same env instance.

One asymmetry between the modes. The control plane is symmetric on placement β€” batch chooses episodes through the panel exactly as a user would β€” but not on the episode_reset signal. The panel's signal side-effect is only forwarded to broadcast_signal on the interactive canvas API path; in batch the runner reads the panel's return for episode metadata and step_budget but does not re-broadcast episode_reset. Batch gets per-episode isolation a different way: a fresh LoopRunner (and thus fresh containers) is built per episode, so lifetime="episode" state is clean by construction.

5. Invariants (the contract)

  1. Suite init/teardown is panel/manager-owned β€” never a graph node. Per-episode close is forbidden (SIMPLER: SAPIEN GC-ordering segfault; mirror upstream's no-close pattern).
  2. Placement is panel-owned β€” nodes never choose which episode runs; the batch runner chooses through the panel, not around it.
  3. Liveness is reset-owned β€” reset is an idempotent ensure-live in the pre-loop band. It revives, never chooses.
  4. observe_* is a pure read β€” no lifecycle action, ever. A finished episode observed again returns the terminal frame.
  5. step_* is the only in-episode state advance, and emits control signals only (reward/terminated/truncated/info β€” never an observation).
  6. evaluate fires once, in the after-loop band, fed by the iterOut final side. Per-step telemetry rides step.info into viewers; the verdict never comes from an in-loop evaluate.
  7. Method-side state clears via signals only β€” run_start for lifetime="run", episode_reset for lifetime="episode". No node manually wipes containers at lifecycle boundaries.
  8. One control plane (for placement) β€” anything batch does to place an env, a user can do through the panel, and vice versa. New lifecycle abilities go into the panel or the engine's signals, not into ad-hoc node behaviour.

6. Known seams

Every lifecycle bug to date sat on a boundary between scopes, not inside one. The watch-list:

SeamWhat happened / can happenStatus
placement ↔ livenessan early auto-reset inside habitat's observe_* left episode rollover homeless, later a metric-pollution hazard (a post-loop observe re-fire could roll into a new episode under evaluate)resolved 2026-06-11 β€” rollover moved to reset
advance ↔ run accountinga node returning {"error": …} instead of its declared port dict makes the engine silently complete the episode with step_count=0 (routing checks src_handle in result) β€” cost 11/100 episodes in a smartway-ce runopen (engine)
loop ↔ after-loop banda post-loop node fed by a loop-body node resolves that input to None on the final re-fire (collapsed explore-eqa SR 0.43β†’0) β€” which is exactly why the after-loop band purity rule now bars bodyβ†’band edgesstructural rule β€” Loop Control Β§9
liveness, per-envonly habitat's reset implements the re-arm today; the other MDP envs (MP3D's is a pure metadata read) were never audited against the ensure-live wording β€” a dead-episode canvas re-run may hang on themopen (audit pending)
AgentCanvas docs