Skip to content

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.

ABSOLUTE_MODES module-attribute

ABSOLUTE_MODES = ('joint_pos', 'eef_abs_pose')

Timing

Bases: Contract

How policy time relates to robot time. Part of what an experiment measures.

mode class-attribute instance-attribute
mode: Literal['sync', 'realtime'] = 'sync'
clock class-attribute instance-attribute
clock: Literal['virtual', 'wall'] = 'virtual'
control_hz class-attribute instance-attribute
control_hz: float | None = None
refill_steps class-attribute instance-attribute
refill_steps: int = Field(default=2, ge=0)
adaptive_refill class-attribute instance-attribute
adaptive_refill: bool = True
latency_s class-attribute instance-attribute
latency_s: float | None = None
max_hold_steps class-attribute instance-attribute
max_hold_steps: int = Field(default=100, ge=1)
require_reset_confirm class-attribute instance-attribute
require_reset_confirm: bool = False
verdict_timeout_s class-attribute instance-attribute
verdict_timeout_s: float = 0.0

OperatorChannel

OperatorChannel()

Thread-safe operator input: stop, estop (latched), clear_fault, reset_done, verdict.

fault instance-attribute
fault: str | None = None
stop instance-attribute
stop = threading.Event()
reset_done instance-attribute
reset_done = threading.Event()
verdicts instance-attribute
verdicts: 'queue.Queue[dict[str, Any]]' = queue.Queue()
log instance-attribute
log: list[dict[str, Any]] = []
post
post(event: Mapping[str, Any]) -> None
drain
drain() -> list[dict[str, Any]]

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 = '')
outcome instance-attribute
outcome: EpisodeOutcome
safety_interventions instance-attribute
safety_interventions: int
events instance-attribute
events: int
compat instance-attribute
compat: CompatReport
holds class-attribute instance-attribute
holds: int = 0
chunks class-attribute instance-attribute
chunks: int = 0
verdict class-attribute instance-attribute
verdict: dict[str, Any] | None = None
status class-attribute instance-attribute
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)
env_spec instance-attribute
env_spec = env.spec()
compat instance-attribute
compat = require_compatible(agent.spec(), self.env_spec)
shield instance-attribute
shield = SafetyShield(self.env_spec, build_guards(safety), clip_tolerance=float(safety.get('clip_tolerance', 0.05)))
image_every instance-attribute
image_every = image_every
bus instance-attribute
bus = bus or EventBus()
trace instance-attribute
trace = trace
act_timeout_s instance-attribute
act_timeout_s = act_timeout_s
record_privileged instance-attribute
record_privileged = record_privileged and isinstance(env, PrivilegedStateSource) and self.env_spec.capabilities.privileged_state
timing instance-attribute
timing = timing or Timing()
realtime instance-attribute
realtime = self.timing.mode == 'realtime'
hz instance-attribute
hz = float(self.timing.control_hz or self.env_spec.control_hz)
operator instance-attribute
operator = operator or OperatorChannel()
run
run(task: TaskInstance, *, episode_id: str | None = None) -> EpisodeRecord

hold_action

hold_action(env: Any, spec: EnvSpec, last: ndarray | None) -> np.ndarray

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.

obs_payload

obs_payload(obs: Observation, with_images: bool = True) -> dict[str, Any]

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.

TRACE_SCHEMA module-attribute

TRACE_SCHEMA = 'hylo.trace/v0.1'

EPISODE_START module-attribute

EPISODE_START = 'episode.start'

EPISODE_END module-attribute

EPISODE_END = 'episode.end'

OBSERVATION module-attribute

OBSERVATION = 'observation'

AGENT_CHUNK module-attribute

AGENT_CHUNK = 'agent.chunk'

AGENT_DECISION module-attribute

AGENT_DECISION = 'agent.decision'

MEMORY_RETRIEVAL module-attribute

MEMORY_RETRIEVAL = 'memory.retrieval'

MEMORY_WRITE module-attribute

MEMORY_WRITE = 'memory.write'

SAFETY_REVIEW module-attribute

SAFETY_REVIEW = 'safety.review'

ACTION module-attribute

ACTION = 'action'

STEP_RESULT module-attribute

STEP_RESULT = 'step.result'

PRIVILEGED_STATE module-attribute

PRIVILEGED_STATE = 'privileged.state'

ERROR module-attribute

ERROR = 'error'

SCORE module-attribute

SCORE = 'score'

Subscriber module-attribute

Subscriber = Callable[[Event], None]

Event

Bases: Contract

type instance-attribute
type: str
episode_id instance-attribute
episode_id: str
seq instance-attribute
seq: int
payload class-attribute instance-attribute
payload: dict[str, Any] = Field(default_factory=dict)
source class-attribute instance-attribute
source: str = 'kernel'
sim_time class-attribute instance-attribute
sim_time: float | None = None
wall_time class-attribute instance-attribute
wall_time: float = Field(default_factory=time.time)

EventBus

EventBus()

Fan-out with failure isolation: a failing non-critical subscriber is detached, not fatal. Critical subscribers (the trace writer) propagate their exceptions.

detached instance-attribute
detached: list[tuple[Subscriber, str]] = []
subscribe
subscribe(fn: Subscriber, *, critical: bool = False) -> None
publish
publish(event: Event) -> None

Traces

trace

Event-sourced, replayable episode trace.

Layout of a run directory

/manifest.json RunManifest (config hash, versions, seeds) /episodes/.jsonl one Event per line, arrays inline when small /blobs/.npy large arrays (images, point clouds), content-addressed /evallog.json aggregated scores; points back at episode files

Scorers and shadow replay read only this; they never call the Agent or the Environment.

BLOB_THRESHOLD_BYTES module-attribute

BLOB_THRESHOLD_BYTES = 64 * 1024

TraceStore

TraceStore(run_dir: str | PathLike)
root instance-attribute
root = Path(run_dir)
write
write(event: Event) -> None
close_episode
close_episode(episode_id: str) -> None
close
close() -> None
episode_ids
episode_ids() -> list[str]
read
read(episode_id: str) -> 'EpisodeTrace'

EpisodeTrace

EpisodeTrace(root: Path, episode_id: str)
events property
events: list[dict[str, Any]]
of_type
of_type(*types: str) -> Iterator[dict[str, Any]]
first
first(type_: str) -> dict[str, Any] | None
last
last(type_: str) -> dict[str, Any] | None