Runtime#
RobotRuntime runs the control loop on robot hardware. It owns hardware I/O, the callback lifecycle, and timing — while a required, pluggable action_source owns the actual decision of what action to send each tick. PolicySource wraps a trained policy (model + execution strategy + action queue); TeleopSource reads a leader arm; custom logic can implement the ActionSource protocol directly.
runtime = RobotRuntime(
fps=30,
robot=robot,
action_source=PolicySource(model=model, execution=SyncExecution()),
)
with runtime:
runtime.run(duration_s=60)
Responsibilities#
Component |
Owns |
Does not own |
|---|---|---|
|
model load, preprocess, inference, postprocess |
robot loop timing |
|
where and when inference runs |
robot IO |
|
action chunks and buffering |
model inference |
|
wiring model + execution + action queue into one action per tick |
robot IO, loop timing |
|
observe, call the action source, send action, callbacks, timing |
policy math, decision logic |
|
hardware connection, observations, actions |
policy inference |
Loop#
The runtime loop follows this general pattern:
while running:
if stop_requested() or duration_reached():
break
robot_state, camera_frames = read_observation()
action = action_source.update(robot_state, camera_frames, step)
action = on_action_ready(action) # callback hook, may transform
send_action_to_robot(action)
on_action_sent(action) # callback hook, notification only
sleep_until_next_tick()
The exact observation structure and merging strategy may change as the API stabilizes. Everything left of action_source.update() — deciding whether to run inference, pulling from the queue, holding the last action — is internal to the action source; RobotRuntime itself only ever sees one action per tick.
Stopping#
A stop takes effect between ticks, never inside one. The tick already underway finishes and sends its action, so the robot is never left halfway through a command. That means a stop is not instant: it lands once the current tick is done, and a tick that happens to be waiting on inference or a slow robot read takes as long as it takes.
Use runtime.stop() when the code asking for the stop lives in the same program. Use run(stop_event=...) when it does not: pass any object with an is_set() method, and one process can stop a session running in another. The runtime checks both, so whichever comes first ends the run.
Stopping is not the same as shutting down. It ends the control loop, but the robot, cameras, and callbacks stay active, so one runtime can stop and run again. Each run emits its own start and shutdown lifecycle events. last_run_reason and the shutdown event’s reason field say why a run ended.
disconnect() is final. It closes callbacks and disconnects hardware exactly once; create a new runtime to connect again. A failed connect() already rolls back partial hardware setup, so retry it by calling connect() again without calling disconnect(). This keeps callback resources such as files and background threads usable across runs while giving them one clear disposal point.
Execution Modes#
Preview:
RemoteExecutionis a planned API.
Mode |
Where inference runs |
Use |
|---|---|---|
|
runtime thread |
simple deployments and debugging |
|
worker thread |
avoid blocking the control loop |
|
remote server |
planned API |
Product Workflows#
HIL, recording, highlight, and DAgger should be composed through callbacks until they justify reusable runtime primitives.
class HILCallback:
def on_action_ready(self, *, action, step):
if teleop.enabled:
return teleop.read_action()
return action