agi.diy

Reference · look things up, do not read through

Rollout on hardware.

Running a hosted policy through LeRobot's control loop on a real SO-101: dry run, units, cameras, link loss. The four-minute read is /docs.

Run a hosted SO-101 policy through LeRobot's control loop. Start with a dry run, not motor execution.

Before connecting hardware

  • Set up and calibrate your SO-101 through LeRobot. Check serial port, joint order, camera positions, resolution and local limits.
  • Known metadata mismatch: the legacy so101-degrees-gripper-percent-v1 label (still answered by stock-* rows; agi-* leases answer so101-normalized-v1) does not describe these checkpoints' numeric values. Arm state/actions are normalized −100…100 and gripper 0…100, not degrees. Preserve the handshake identifier, but verify your calibrated input/output transforms before motion. Do not bypass admission checks.
  • ACT is a pick-and-place checkpoint, not language-conditioned. agi-smolvla/so101 is a fine-tune on one real-teleoperation orange-to-plate task (the third-party stock-smolvla/so101 is the simulation-trained one), not a demonstrated general instruction follower or guaranteed real-arm transfer.
  • Keep a clear workspace and physical stop. Interactive mode delays policy playback, but connecting hardware can energize motors. Ctrl-C is not an emergency-stop system.

Inspect the configuration

Install agi-diy in a dedicated Python 3.12 environment (LeRobot needs 3.12+; lerobot-rollout fails on 3.14 — agi rollout refuses both before leasing) and agi login using a dashboard key (quickstart). Replace the serial port and camera indices below. This command does not lease, install dependencies or move hardware.

agi rollout agi-act/so101 --dry-run -- \
  --robot.type=so101_follower --robot.use_degrees=false --robot.port=/dev/tty.usbmodemYOUR_ARM \
  --robot.cameras='{"top":{"type":"opencv","index_or_path":0,"width":640,"height":480,"fps":30},"wrist":{"type":"opencv","index_or_path":1,"width":640,"height":480,"fps":30}}' \
  --interactive=true --return_to_initial_position=false

After verifying the setup, remove --dry-run to start the hardware process. /start begins policy playback. Keep --return_to_initial_position=false to avoid a commanded return-to-start motion on exit.

The first real rollout installs the pinned LeRobot client and robot extras into the interpreter agi runs in. This can change dependency versions; use an isolated environment and allow time/disk space. AGI_NO_INSTALL=1 prints the install command instead.

What the lease fills in

agi rollout <model> leases one slot, then runs python -m agi._rollout_main (= lerobot-rollout, same interpreter as agi, never a PATH lookup) with these flags in front of yours:

flagfrom the leasewhy
--inference.type=remote—no local policy, no --policy.path, no --device
--inference.endpoint=zenoh.endpoint (tcp/…)the endpoint advertised by this lease; do not hard-code it
--inference.deployment=deployment (d_…)the slot's capability name; valid for this lease only
--inference.zenoh_mode=clientzenoh.modeconnect to the router; no multicast scouting
--inference.encoding=jpegzenoh.encodingtwo raw 480×640 frames do not cross the public wire
--inference.semantics=semanticsthe runner refuses a different profile
--inference.*_timeout_s=30—LeRobot's 5–10 s defaults trip on a cold first call
--inference.action_starvation_grace_s=3.0—LeRobot's 1.0 s grace is LAN-tuned; the public relay needs slack
--inference.refill_seconds=0.75—ask for the next chunk with 0.75 s still queued (real-time chunking, chunk_merge=aligned); LeRobot's 0.5 s is LAN-tuned — the public wire's measured tail (smolvla 527 ms) held the arm. Override allowed
--inference.max_observation_age_s=action_horizon, fpshow long an observation may still be driving the arm: its own chunk + the 3 s starvation grace + one 0.75 s refill window — 7.083 for agi-act/so101 (3.333 s chunk), 5.883 for agi-wfm/so101, never below LeRobot's own 5.0 s default. LeRobot revokes motion with a terminal fault the moment a dispatched action's source observation is older than this, so a control loop that slips (a loaded laptop, a slow camera) would otherwise end the run — and before agi-diy 0.5.9 the flag was refused, leaving no recourse. Override allowed
--fps=fpsthe runner checks 1/fps against the policy's interval
--robot.use_degrees=false / =trueunits of the lease (so101/so100 follower)catalog checkpoints are normalized → false; a custom hf: checkpoint whose own stats are degrees (units: so101-degrees-gripper-percent-v1) → true. agi-diy ≥ 0.5.10 reads units and refuses the opposite flag; 0.4.6–0.5.9 always injected false and refused true; ≤ 0.4.5 injected nothing (upgrade)

Everything after -- is yours and goes to lerobot-rollout unchanged. If you pass a flag the lease also sets, yours wins — except --fps, which must equal the lease's or admission fails, so agi rollout says so before leasing. The lease ends when the process exits, however it exits.

What you give:

flagwhat
--robot.type=so101_follower for the currently hosted checkpoints
--robot.port=the serial port (lerobot-find-port prints it)
--robot.cameras=JSON: name → {type, index_or_path, width, height, fps}
--task=the instruction; needed by language-conditioned models (smolvla/*)
--duration=seconds; 0 runs until /stop or Ctrl-C (default)
--interactive=truehardware warm, robot still until you type /start

Cameras

The deployment names its cameras. agi-act/so101 expects top and wrist; agi-smolvla/so101 expects front and wrist — agi models --json lists them under features, and the lease carries the same list.

Name yours the same and nothing else is needed. Otherwise agi rollout matches by name, never by order (agi-diy 0.4.9 and later):

  • a camera whose name contains a model camera name as a whole word keeps that slot — wrist_cam → wrist, top_cam → top, FrontCam → front (but laptop is not top);
  • one camera left over takes the one slot left over — unless its own name says it is a different view (it contains top, front, wrist, side, overhead or gripper, or a model camera name, but not the free slot's) or names two views (top_wrist): then it stops and prints the --rename_map for you to confirm;
  • two or more cameras it cannot place → it stops before leasing and prints the exact --rename_map to pass, so you decide which physical camera is which:
cameras: top_cam → top, wrist_cam → wrist
error: cannot tell which of your cameras (cam0, cam1) is agi-act/so101's top / wrist. Name them like the model's (top, wrist) or pass the mapping yourself, e.g. --rename_map='{"observation.images.cam0": "observation.images.top", "observation.images.cam1": "observation.images.wrist"}' (check that each one really is that view).

What that gives for agi-act/so101 (top, wrist) — every row is run against agi/rollout.py in CI:

Your --robot.cameras namesagi rollout
top, wristnothing to rename
top_cam, wrist_camtop_cam → top, wrist_cam → wrist
TOP, WristTOP → top, Wrist → wrist
top, cam2cam2 → wrist
wrist, tabletable → top
top, frontrefuses: front names another view, the free slot is wrist
wrist, wrist2refuses: wrist2 names the wrist, the free slot is top
top, gripperrefuses: gripper is not the model's word — pass the --rename_map it prints
top_wrist, wristrefuses: top_wrist names two views
front, wristrefuses: ACT has no front — rename it top if it is the scene camera
cam0, cam1refuses: two cameras it cannot place
gripper, laptoprefuses: two cameras it cannot place (laptop is not top)

agi-diy 0.4.8 kept exact names but mapped the rest by order, and 0.4.7 and earlier mapped everything by order — both can swap the wrist and scene views silently (wrist_cam → front). Upgrade before connecting hardware. A mapping only renames; it cannot tell which camera points where. Look at each view before motion.

If the count differs it stops before leasing:

error: agi-act/so101 expects 2 cameras (top, wrist); --robot.cameras names 1 (front).

Resolution must match too: the deployment's shapes are 480×640; declare "width": 640, "height": 480 on every camera.

SO-101

--robot.type=so101_follower needs LeRobot's Feetech driver. agi rollout installs lerobot[feetech] with the rest when the robot type asks for it:

--robot.typeextra installed
so100_follower so101_follower bi_so_follower koch_follower hope_jr_*feetech
lekiwilekiwi (feetech + zmq)
openarm_follower bi_openarm_followeropenarms (CAN)
anything elsenone beyond remote + dataset

Calibration is LeRobot's and unchanged: run lerobot-calibrate --robot.type=so101_follower --robot.port=… once per arm; the file is reused on every rollout. The model's joint order (shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper) is the SO-101's; still verify calibration, normalized units and joint limits.

What you will see

+ python -m agi._rollout_main --inference.type=remote --inference.endpoint=tcp/… --inference.deployment=d_… …
Remote server deployment=d_… instance=… protocol=1
Connecting robot (so101_follower)... Robot connected
Remote session ready: deployment=d_… mode=chunk merge=aligned action_rate=30.0 Hz horizon=3.333s; contract verified
Remote rollout admitted
Base strategy control loop started
Remote progress: accepted=2 rejected=0 … turnaround mean/max=0.197/0.329s … queued playback=1.900s

Without interactive mode, policy playback starts after admission. On exit LeRobot may command a return to the initial pose unless --return_to_initial_position=false was set. Ctrl-C requests shutdown; it is not a guaranteed physical stop. With --interactive=true the arm holds still until you type /start; /subtask <text> changes the instruction, /reset returns to the starting pose, /stop ends the run. Every chunk the policy returns is billed at one token per action, visible in agi usage after metering updates.

Historical transport measurements (2026-10-24, laptop → public endpoint, two JPEG cameras). These are not physical-task success rates or latency guarantees:

modelactions per callturnaround (mean / max)first call
agi-act/so101100197 / 329 ms≤ 300 ms
agi-smolvla/so10150425 / 494 ms≤ 900 ms

Read those turnarounds against 0.75 s, not against the chunk. agi rollout asks for the next chunk when 0.75 s of motion is left (--inference.refill_seconds, set because the public wire's measured tail — smolvla 527 ms, 2026-10-25 — exceeded LeRobot's 0.5 s default), so that is the whole budget for a round trip — a 3.3 s chunk buys no more time than a 1.7 s one, it only means you pay for a call less often. Against that threshold, on the measurements above: agi-act/so101 has roughly 170 ms of margin at its worst observed turnaround, and agi-smolvla/so101 has almost none (494 ms of 500). A turnaround past it does not break the run — the arm holds its last target until the next chunk lands — but it holds on every chunk, and nothing arriving within the 3 s starvation grace faults the run.

The other way a run ends is the arm playing a chunk for too long. Every action in a chunk comes from ONE observation, and LeRobot revokes motion — a terminal fault, "Dispatched action source observation is too old" — once that observation passes --inference.max_observation_age_s. Its own default is 5.0 s, which against agi-act/so101's 3.333 s chunk leaves about a second, so agi rollout derives the value from the lease instead (chunk + grace + refill: 7.083 s for act, 5.883 s for the flagship, never less than LeRobot's 5.0 s). This matters when your control loop runs slower than the model's rate: at half rate a 100-step chunk takes 6.7 s to play, and the observation that produced it is stale long before the end. If you see that error, your loop is not keeping --fps; check that first, then widen the flag.

agi-wfm/so101 is the exception, and it is the one that can starve the queue. Its chunk is 32 actions at 15 Hz — half the rate of the others, so agi rollout injects --fps=15 and the chunk is 2.13 s of motion rather than 3.3 s. The turnaround is the problem, and it is not close: measured on the serving box on 2026-10-25, the policy alone takes 643 ms at the 4 denoise steps the service runs — past the 500 ms budget before any network — against 418 ms at 1 step, which would leave about 80 ms. Round trips measured from a client have been 1.4–2.1 s. So at the served setting this policy holds the arm briefly on every chunk; it moves with where you call from, so read the current measurement on /models/agi-wfm-so101 before you plan a loop. What that means at the arm: LeRobot's look-ahead is 0.75 s of playback under agi rollout (LeRobot's default is 0.5 s), so if a call takes longer than the remaining queue the arm holds its last target and, if nothing arrives within the 3 s starvation grace, the run faults (the same path as a dropped link: what LeRobot does) — the hold and the release of torque are the same path as a network drop, not a different one. Keep a request in flight ahead of the one that is playing, or run the flagship in --fake / simulation and keep real hardware on the 30 Hz default tier until your own measurement says otherwise.

Without an arm

agi rollout <model> --fake -- --task="…" runs the real lerobot-rollout against the real policy with a robot that has no hardware: it holds position, reports a still state and black frames at the deployment's camera shapes, and accepts every action. Use it to check your install, your network and your bill before plugging in — the actions are real and billed.

agi rollout <model> --dry-run -- <your flags> prints the composed command and the camera mapping and leases nothing:

$ agi rollout agi-act/so101 --dry-run -- --robot.type=so101_follower --robot.use_degrees=false --robot.port=/dev/tty.usbmodem1 --robot.cameras='{"top_cam": …, "wrist_cam": …}'
cameras: top_cam → top, wrist_cam → wrist
installs: lerobot[remote,dataset,feetech] if missing
/…/bin/python -m agi._rollout_main --inference.type=remote '--inference.endpoint=<leased>' '--inference.deployment=<leased>' --inference.zenoh_mode=client --inference.encoding=jpeg --inference.semantics=so101-degrees-gripper-percent-v1 --inference.handshake_timeout_s=30 --inference.action_timeout_s=30 --inference.startup_timeout_s=30 --inference.action_starvation_grace_s=3.0 --inference.refill_seconds=0.75 --inference.max_observation_age_s=7.083 '--rename_map={"observation.images.top_cam": "observation.images.top", "observation.images.wrist_cam": "observation.images.wrist"}' --fps=30 --robot.type=so101_follower --robot.use_degrees=false --robot.port=/dev/tty.usbmodem1 '--robot.cameras={…}'

Errors

One sentence, naming the fix. From agi rollout before anything is leased:

you seedo
agi-act/so101 expects 2 cameras (top, wrist); --robot.cameras names 1declare both cameras
--fps=25 differs from the model's 30drop --fps or set it to 30
--robot.type is required after \--` …, or use --fake`add --robot.type=so101_follower after --
camera 'top' is 1280×720; agi-act/so101 expects 640×480set width 640, height 480 on that camera
no_slots: no free slot for agi-act/so101 (409)agi models; retry after retry_after_s; another model may require different cameras/task

From LeRobot once the hardware is up (its own text, unchanged):

you seemeans
Remote observation features differ: expected {…}camera names or shapes differ from the deployment's
Robot action component names differ from the deployment--robot.type is not the arm the model was trained on
Robot/action semantic profile differsa hand-set --inference.semantics; drop it
Policy action interval differs--fps differs from the model's
Remote inference request deadline exceededthe wire stalled; agi models shows whether the runner is up
Could not open port /dev/tty…wrong --robot.port; lerobot-find-port
No such file or directory: '/dev/tty…'wrong --robot.port; lerobot-find-port prints the right one
… is not calibratedrun lerobot-calibrate --robot.type=… --robot.port=… once, then retry
Specifying 'width' is requiredevery camera needs width, height and fps in --robot.cameras
OpenCVCamera …a camera did not open; check index_or_path in --robot.cameras
Connection refusedthe endpoint did not answer; agi models shows whether the runner is up
action starvation grace expiredthe client stopped asking before the buffer ran dry — a Ctrl-C mid-chunk or a quiet wire; the runner answered every query it received (see the trace)