Kernel¶
The one episode loop and what it records. EpisodeRunner runs an environment and an agent through the
SafetyShield, in sync mode (the world waits for the policy) or realtime mode (the world keeps moving while the
policy computes the next chunk); evaluation, the playground's live sandbox and the workcell all use it
(RFC-0005 part 3). Every event goes to an
EventBus and, through a TraceStore, to the run's trace. See also the
execution lifecycle.
Episode loop¶
kernel ¶
Execution kernel: the one episode loop, used for evaluation, learning rollouts and deployment.
env.reset -> agent.reset -> loop[ request a chunk (when the queue runs low) -> merge it when it arrives
-> SafetyShield.review -> env.step -> agent.on_step ]
-> episode.end -> agent.on_episode_end
Timing decides how policy time relates to robot time:
- "sync" (default): the world waits for every policy call; a chunk is requested when the queue is empty and runs
execution_steps actions. Deterministic; what benchmarks have always measured.
- "realtime": the robot keeps moving while the policy thinks (LeRobot async inference, openpi's runtime,
vla-eval's live runner): a chunk is requested when at most refill_steps actions remain, a chunk computed from
the observation of step s that arrives at step t has its first t - s actions dropped (aligned merge), an empty
queue executes a hold action, and a watchdog aborts after max_hold_steps consecutive holds. On the "virtual"
clock the policy latency is measured (or fixed by latency_s) and the steps it would have taken run before the
chunk is merged: deterministic, so benchmarks can measure deployment behaviour. On the "wall" clock the policy
runs in a worker thread and the loop ticks at the control rate: the workcell next to a robot.
Both modes share the SafetyShield, the trace events, error classification and the operator channel (stop, latched e-stop, reset confirmation, outcome verdicts), so what the gate approved in simulation is what the robot runs.
This is "driven mode". In "served mode" (an external harness owns the loop) the same Agent + SafetyShield + trace are wrapped by a protocol adapter; see docs/execution-lifecycle.md.
Timing ¶
OperatorChannel ¶
Thread-safe operator input: stop, estop (latched), clear_fault, reset_done, verdict.
EpisodeRecord
dataclass
¶
EpisodeRecord(outcome: EpisodeOutcome, safety_interventions: int, events: int, compat: CompatReport, holds: int = 0, chunks: int = 0, verdict: dict[str, Any] | None = None, status: str = '')
EpisodeRunner ¶
EpisodeRunner(env: Environment, agent: Agent, *, trace: TraceStore | None = None, bus: EventBus | None = None, safety: Mapping[str, Any] | None = None, act_timeout_s: float | None = None, record_privileged: bool = True, image_every: int = 1, timing: Timing | None = None, operator: OperatorChannel | None = None)
shield
instance-attribute
¶
shield = SafetyShield(self.env_spec, build_guards(safety), clip_tolerance=float(safety.get('clip_tolerance', 0.05)))
record_privileged
instance-attribute
¶
record_privileged = record_privileged and isinstance(env, PrivilegedStateSource) and self.env_spec.capabilities.privileged_state
hold_action ¶
What to execute when no action is queued: the env's own hold_action(last), the last target for absolute
control, or zero motion (gripper kept) for delta / velocity control.
Events¶
events ¶
Typed events and a synchronous event bus.
Every observable thing in an episode is an Event. The Trace is the persisted event stream, so tracing, live dashboards and online critics all subscribe to the same bus.
Traces¶
trace ¶
Event-sourced, replayable episode trace.
Layout of a run directory
Scorers and shadow replay read only this; they never call the Agent or the Environment.