Skip to content

Architecture

Which object Robot(...) returns in each mode, which module owns what, and the seven-layer rule a change must obey.

strands_robots is one factory over two lanes and seven import layers: this page says which object Robot(...) hands you in each mode, which module owns what, and the layer rule a change must obey.

Two lanes behind one factory

Left column, top to bottom: a Strands Agent with the robot in its tools makes a tool call; it passes the operator gate, the one green element, where run_policy and send_action through the tool wait for a yes (an interrupt, fail closed without an operator, an audit row); after a yes it reaches Robot("so101"), the execution target and the tool, with run_policy, send_action, get_observation, get_state, render and status; a dashed result wire returns to the agent. Under the robot a dashed policy runtime layer holds create_policy, the embodiment map and the action chunk, fed by the observation and returning actions. Right column, the backends the same calls reach: simulation with MuJoCo, Newton or Isaac Sim; a hardware driver, lerobot or native; and a PolicyServer on a GPU host that a RemotePolicy talks to over a WebSocket. Footnote: one interface, get_observation, send_action, run_policy; the backend changes, the call does not.Left column, top to bottom: a Strands Agent with the robot in its tools makes a tool call; it passes the operator gate, the one green element, where run_policy and send_action through the tool wait for a yes (an interrupt, fail closed without an operator, an audit row); after a yes it reaches Robot("so101"), the execution target and the tool, with run_policy, send_action, get_observation, get_state, render and status; a dashed result wire returns to the agent. Under the robot a dashed policy runtime layer holds create_policy, the embodiment map and the action chunk, fed by the observation and returning actions. Right column, the backends the same calls reach: simulation with MuJoCo, Newton or Isaac Sim; a hardware driver, lerobot or native; and a PolicyServer on a GPU host that a RemotePolicy talks to over a WebSocket. Footnote: one interface, get_observation, send_action, run_policy; the backend changes, the call does not.
from strands_robots import Robot

sim = Robot("so101")                                    # mode="sim": a MuJoCoSimEngine
arm = Robot("so101", mode="real", port="/dev/ttyACM0")  # lerobot RobotConfig behind hardware_robot.Robot
arm = Robot("so101", mode="real", driver="strands", port="/dev/ttyACM0")  # a native driver from strands_robots.drivers

Robot in strands_robots/robot.py is a function: it resolves the alias through the registry, then dispatches on mode:

mode returns built by
sim (default) a SimEngine from the backend registry (mujoco default, newton, plugin isaac) simulation.factory.create_simulation
real, driver="lerobot" (what auto resolves to without a registry hardware.driver) hardware_robot.Robot, a lerobot RobotConfig under a Strands AgentTool hardware_robot.py
real, driver="strands" a class satisfying the drivers.base.HardwareDriver protocol, no lerobot import drivers.registry
auto STRANDS_ROBOT_MODE, else probes USB for a servo controller, else sim _auto_detect_mode

Both lanes are AgentTools, so Agent(tools=[robot]) works the same on each, and both take a Policy from policies.create_policy for run_policy. Sim is the default so a script never moves hardware by accident.

The sim lane is the MuJoCoSimEngine class: SimEngine plus mixins for physics, rendering, recording, randomization, manipulation, motion primitives and teleop, exposed as one tool with an action vocabulary. The hardware lane is hardware_robot.Robot plus the driver layer; a task runs in a background thread with TaskStatus and a stop flag, and a policy dispatch passes the operator gate in _command_gate.py.

Layers

Left, a stack of seven layers read top to bottom: dashboard, tools, app (the Robot factory, the one green element), sim beside policies, drivers beside mesh, registry, core. A wire beside the stack points downward, labelled imports. Right, three cards: check_import_layers reads every import with ast; deferred upward edges must be in KNOWN_DEFERRED_UPWARD_EDGES, a roster that only shrinks; the factory in app joins sim and policies in run_policy, and the two never import each other. Footnote: core has no strands_robots import at all.Left, a stack of seven layers read top to bottom: dashboard, tools, app (the Robot factory, the one green element), sim beside policies, drivers beside mesh, registry, core. A wire beside the stack points downward, labelled imports. Right, three cards: check_import_layers reads every import with ast; deferred upward edges must be in KNOWN_DEFERRED_UPWARD_EDGES, a roster that only shrinks; the factory in app joins sim and policies in run_policy, and the two never import each other. Footnote: core has no strands_robots import at all.

Seven layers, top to bottom; a module imports only from layers below its own:

core -> registry -> drivers|mesh -> sim|policies -> app -> tools -> dashboard

scripts/check_import_layers.py grades this from the source with ast: no runtime import cycle, and no upward edge unless it is written in the script's KNOWN_DEFERRED_UPWARD_EDGES roster. The roster is a ratchet: removing an inversion deletes its line, adding one fails the check until it is listed. It is empty at this commit.

The table is generated from the grader's LAYERS declaration.

layer what it is for members
0 core leaves everything imports: gates, audit, dataset formats, refusal codes, rendering helpers audit, bus_access, dataset_metadata, dataset_recorder, dataset_source, dataset_transfer, episode_labels, locomotion_envelope, recorder, recording_errors, refusal_codes, rendering, streaming_dataset, utils and 11 private helpers (_async_utils, _command_gate, _description_cache, _dyld, _hitl_audit, _mesh_switch, _motion_grants, _mujoco_gl, _pacing, _path_validation, _serial_discovery)
1 registry the robot and policy rows, and the asset paths they declare assets, registry
2 drivers|mesh talk to hardware (native drivers, ROS, teleop) and to other peers (Zenoh mesh) device_connect, drivers, foxglove, mesh, ros, ros_telemetry, rosbridge, rtps, teleop, teleop_mixin, teleoperator
3 sim|policies the simulation backends, the policy providers, training and remote inference inference, policies, simulation, training
4 app what Robot(...) returns: the factory, the hardware lane, doctor and dataset verification doctor, hardware_observe, hardware_robot, hardware_ros_bridge, hardware_rtps_bridge, robot, verify_dataset
5 tools the @tool surface an agent calls tools
6 dashboard the operator UI, a mesh gateway over everything below __main__, dashboard

Placements that are a judgement: assets sits with registry because it resolves the paths the registry declares; the dataset modules (dataset_recorder, dataset_metadata, dataset_source, streaming_dataset, dataset_transfer) sit in core because a dataset is a contract two layers agree on, not a host.

Rules every module obeys

  • Cheap import. import strands_robots leaves numpy, torch, mujoco and lerobot out of sys.modules; every heavy name in __all__ is behind the package __getattr__, and the import-time shims (_mujoco_gl, _dyld) are stdlib-only leaves.
  • Registry is the source of truth. registry/robots.json holds the robots (156 at this commit, in 8 families) and policies.json the providers. Code reads the row; it never hard-codes a robot.
  • Refuse, do not guess. A value the code cannot honour (a non-finite pose, an unknown joint name, a hardware kwarg on a sim robot) is refused with a message naming the valid set; continuable refusals carry a code.
  • A policy does not reach hardware without an answer. Mutative verbs on use_unitree, serial_tool, pose_tool, the Robot tool's execute and start, and every ROS 2 transport raise the SDK interrupt; STRANDS_*_COMMAND_ALLOW pre-approves one command for an unattended run. Stop verbs are never gated; the native drivers' move_to is not gated yet.
  • Providers are plugins. Simulation backends register through register_backend or the strands_robots.backends entry-point group; policies through register_policy or policies.json; native drivers through register_native_driver.

Extras

The package installs with no heavy dependency; each lane pulls its own extra: [sim-mujoco] for the sim lane, [lerobot] for the lerobot hardware lane, [mesh] for Zenoh, [dashboard] for the operator UI, and one extra per native driver or policy provider (19 drivers, 16 providers). pyproject.toml is the list; a door that needs an extra you lack refuses with the install line.

What changes in 1.0

1.0 keeps this layer DAG and changes what each layer holds (roadmap).

Edit page