Skip to main content

Authoring an embodiment adapter

Every embodiment adapter (a package registering an inspect_robots.embodiments entry point) declares its spaces through EmbodimentInfo. Two consumers make those declarations load-bearing:

  • The CLI's default safety guardrails derive a bounds clamp and a per-step delta limit from the action space.
  • The LLM agent policy (inspect-robots-agent) builds its whole tool surface from the spaces at bind time: tool schemas from the bounds and labels, the motion strategy from the control mode, the proprioceptive reference from the StateSpec. It also injects EmbodimentInfo.docs (free-form operating notes: joint layout, positive directions, gripper polarity) into its system prompt, so an adapter that ships good notes measurably helps LLM policies.

An adapter with missing or dishonest declarations silently degrades both. This page is the contract; the conformance kit makes most of it mechanical.

Declare runtime requirements

Declare imports that cannot be expressed by package metadata on the registered component factory:

RUNTIME_REQUIREMENTS = {
"i2rt": 'uv pip install "i2rt @ git+https://github.com/i2rt-robotics/i2rt"',
"cv2": 'uv pip install "inspect-robots-yam[cameras]"',
}

The setup wizard checks the configured policy and embodiment when they are registered, while doctor checks its selected embodiment before construction. Both use importlib.util.find_spec without importing the declared top-level modules and print each remediation command verbatim when a module is missing.

Declare device slots

Declare device-shaped constructor arguments on the registered embodiment factory. Import DeviceSlot from the inspect_robots.conformance submodule:

from inspect_robots.conformance import DeviceSlot

DEVICE_SLOTS = (
DeviceSlot(
arg="left_channel",
kind="can",
label="left arm CAN channel",
group="arms",
),
DeviceSlot(
arg="right_channel",
kind="can",
label="right arm CAN channel",
group="arms",
),
DeviceSlot(
arg="top_cam_device",
kind="v4l2",
label="top camera",
group="cameras",
),
DeviceSlot(
arg="left_cam_device",
kind="v4l2",
label="left camera",
group="cameras",
),
DeviceSlot(
arg="right_cam_device",
kind="v4l2",
label="right camera",
group="cameras",
),
)

The recognized kinds are v4l2 for stable camera paths, can for SocketCAN interface names, and serial for absolute /dev/serial/by-id paths. The setup wizard probes and interviews slots in declaration order, then writes each selection to its arg key under [embodiment.args]. Slots with the same non-None group are all-or-none. Ungrouped slots remain independent.

The conformance kit

Add one test built on assert_embodiment_conformant to your adapter repo:

from inspect_robots.conformance import assert_embodiment_conformant

def test_embodiment_is_conformant() -> None:
assert_embodiment_conformant(MyEmbodiment().info)

Users can audit an installed adapter the same way:

inspect-robots doctor --embodiment my_arms

Both run the same checks, and neither touches hardware (keep your constructor hardware-free; connect in reset()).

What the kit checks (errors)

CodeRequirement
semanticsThe action Box carries ActionSemantics. Guardrails and the agent cannot tell absolute targets from displacements without it.
boundsFinite low/high on every dim. Without them the bounds clamp is skipped and no default delta limit can be derived.
dim_labelsEvery dim is named (("left_j0", ..., "right_gripper")), uniquely. The agent moves joints by these names.
state_alignmentAbsolute-target modes (joint_pos, eef_abs_pose) declare exactly one StateSpec field with shape == (action_dim,): the proprioceptive reference the agent interpolates from.
guardrailsDeltaLimitApprover(action_space) constructs. This catches absolute pose modes (eef_abs_pose) whose rotation representation cannot be clamped per dimension (quat_*, axis_angle, euler_xyz; use none or rot6d), and displacement pose modes (eef_delta_pose) whose rotation representation's identity is not the zero vector (quat_*, rot6d; use none, axis_angle, or euler_xyz).

Warnings

  • control_hz undeclared: agent motion durations fall back to 10 Hz step counting.
  • Zero-width dims (low == high): nothing can be commanded there.

What the kit cannot check

Conformance proves your adapter is guardrail-ready and agent-ready. It cannot prove the declarations are honest. Verify these by hand:

  1. Control mode matches step() behavior. If step() adds the action to the current position, declare joint_delta (or eef_delta_*), never joint_pos. Misdeclaring sends the delta limiter and the agent's motion layer down the absolute branch: "hold still" becomes "move by the current pose".
  2. Displacement bounds are per-step-sized. In delta modes the declared box is the per-step displacement limit, not the absolute joint limits. Reusing absolute limits derives uselessly large swing limits, and an asymmetric absolute box (like a [0, 1] gripper) clamps one direction of motion to zero. Keep an absolute-limit clamp on the summed command inside the embodiment as a backstop.
  3. Policy and embodiment declare in lockstep. If a config flag changes the control mode, both sides must build their semantics from the same config. A mismatch is a hard compatibility error (good: it fails before motion).
  4. Hold behavior between chunks. Slow policies (VLA servers, LLM agents) leave seconds between action chunks. Verify on the rig that motors hold position in your configured mode before any unattended run, and keep a hand on the e-stop the first time.

Conventions worth copying

The reference adapters (inspect-robots-yam, plugins/inspect-robots-isaacsim) share a shape worth reusing: hardware access behind injected seams (driver factory, camera reader, clock) so the full suite runs in CI; scalar-only constructor kwargs so -E key=value works from the CLI; a hard safety clamp inside step() independent of any approver; 100% coverage with the real drivers behind pragma: no cover seams; and a preflight or doctor run documented as the first step on new hardware.