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 theStateSpec. It also injectsEmbodimentInfo.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)
| Code | Requirement |
|---|---|
semantics | The action Box carries ActionSemantics. Guardrails and the agent cannot tell absolute targets from displacements without it. |
bounds | Finite low/high on every dim. Without them the bounds clamp is skipped and no default delta limit can be derived. |
dim_labels | Every dim is named (("left_j0", ..., "right_gripper")), uniquely. The agent moves joints by these names. |
state_alignment | Absolute-target modes (joint_pos, eef_abs_pose) declare exactly one StateSpec field with shape == (action_dim,): the proprioceptive reference the agent interpolates from. |
guardrails | DeltaLimitApprover(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_hzundeclared: 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:
- Control mode matches
step()behavior. Ifstep()adds the action to the current position, declarejoint_delta(oreef_delta_*), neverjoint_pos. Misdeclaring sends the delta limiter and the agent's motion layer down the absolute branch: "hold still" becomes "move by the current pose". - 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. - 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).
- 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.