agi.diy

Documentation / Concepts

How it works.

What a policy is, what one call sends and returns, what a session costs, and what has to match on your robot. Diagrams are drawn from the live contract and one recorded session.

The model behind every agi.diy call, in the words the product uses. Each diagram is generated at build time from the deployment files, the Worker constants and one recorded session (web/scripts/concepts.mjs); the numbers are labelled with where they came from.

Three ways in, depending on why you are here:

Policies#

A policy is a trained model that turns what a robot sees and feels into what it should do next. agi.diy serves policies over an API: you send an observation, it returns actions. Your robot, your control loop and your safety checks stay on your side.

Two policies are served today, both for the SO-101 arm:

modelkindcheckpointinputsreturns per call
act/so101ACT — action chunking transformervineetr196/act_so101_pickplace_v1, a pick-and-place checkpoint6 joint values, top + wrist cameras100 actions
smolvla/so101VLA — vision-language-actionedge-inference/smolvla-so101-pick-orange, trained in simulation to put oranges on a plate6 joint values, front + wrist cameras, task text50 actions
The two policies served todayact/so101: six joint values and top+wrist cameras in, 100 actions out, 198 MB checkpoint, no text input. smolvla/so101: six joint values, front+wrist cameras and task text in, 50 actions out, 1.2 GB checkpoint trained in simulation. Both SO-101, 30 fps. act/so101ACT · action chunking transformerin6 joint valuestop + wrist cameras, 640×480no text inputout100 actions × 6 joints3.33 s of motion at 30 fpscheckpoint198 MB · pick-and-place18–20 ms policy time on the runnersmolvla/so101SmolVLA · vision-language-actionin6 joint valuesfront + wrist cameras, 640×480+ task textout50 actions × 6 joints1.67 s of motion at 30 fpscheckpoint1.2 GB · trained in simulation365 ms policy time on the runner
The two policies served todayact/so101: six joint values and top+wrist cameras in, 100 actions out, 198 MB checkpoint, no text input. smolvla/so101: six joint values, front+wrist cameras and task text in, 50 actions out, 1.2 GB checkpoint trained in simulation. Both SO-101, 30 fps. act/so101ACT · action chunking transformerin6 joint valuestop + wrist cameras, 640×480no text inputout100 actions × 6 joints3.33 s of motion at 30 fpscheckpoint198 MB · pick-and-place18–20 ms policy time on the runnersmolvla/so101SmolVLA · vision-language-actionin6 joint valuesfront + wrist cameras, 640×480+ task textout50 actions × 6 joints1.67 s of motion at 30 fpscheckpoint1.2 GB · trained in simulation365 ms policy time on the runner
The two policies served today. Drawn from runtime/deployments/*.yaml (cameras, fps, training note) · runtime/CAPACITY.md (horizon, checkpoint size, policy time)

Both checkpoints are task-specific. ACT takes no text at all. SmolVLA reads the task sentence, but it was fine-tuned for one task in one kind of scene, so a different sentence is not general instruction following.

Each model has one page with everything you need to judge it — inputs, price, measured times, licences, a one-call test and live availability: /models/act-so101 and /models/smolvla-so101.

Licences, as each Hub card declares them at the revision the runner serves (checked 2026-10-25):

modelcheckpointtraining data
act/so101vineetr196/act_so101_pickplace_v1 · apache-2.0vineetr196/so101_pickplace_v1 · no licence declared
smolvla/so101edge-inference/smolvla-so101-pick-orange · apache-2.0 (base lerobot/smolvla_base · apache-2.0)LightwheelAI/leisaac-pick-orange · apache-2.0

These are the upstream authors' declarations, not a legal review; check them against your own use. The per-model facts are also available as data: /concepts/models.json.

Your own checkpoint, before it is a sessionA model id starting hf: is validated against the Hub before anything is charged: the repo must exist and be readable (with hf_token for a private or gated one), the policy must be act, smolvla, diffusion, the weights must be safetensors and at most 3 GB, and the checkpoint must declare observation.state and at least one camera. Each gate has its own refusal. Only then is a dynamic deployment leased: the session starts warming (about 10 s for an ACT, 45 s for a SmolVLA), becomes leased when the runner has it loaded, and ends load_failed if it cannot load or stays warming for 300 s. model: "hf:<org>/<repo>[@<revision>]"validated on the Hub, before any chargethe repo exists404 unknown_modelyou may read it403 hf_forbiddenpolicy is act, smolvla, diffusion400 unsupported_policyweights are safetensors400 unsupported_weightsweights ≤ 3 GB400 too_largestate + ≥1 camera400 missing_configokdynamic deploymentcommit pinned to a shafeatures + horizon from the checkpointwarming ~10 s act · ~45 s smolvlaleased — connect within 60 scannot load / 300 s → load_failedlimits: 2 live custom sessions and 10 cold starts an hour per account (429)hf_token for a private or gated repo: handed to the runner once, erased when the deployment is upprice per action is the same as our own policies
Your own checkpoint, before it is a sessionA model id starting hf: is validated against the Hub before anything is charged: the repo must exist and be readable (with hf_token for a private or gated one), the policy must be act, smolvla, diffusion, the weights must be safetensors and at most 3 GB, and the checkpoint must declare observation.state and at least one camera. Each gate has its own refusal. Only then is a dynamic deployment leased: the session starts warming (about 10 s for an ACT, 45 s for a SmolVLA), becomes leased when the runner has it loaded, and ends load_failed if it cannot load or stays warming for 300 s. hf:<org>/<repo>checked on the Hub firstthe repo exists404 unknown_modelyou may read it403 hf_forbiddenpolicy is act, smolvla, diffusion400 unsupported_policyweights are safetensors400 unsupported_weightsweights ≤ 3 GB400 too_largestate + ≥1 camera400 missing_configokdynamic deploymentcommit pinned to a shawarming ~10/45 sleased → connect in 60 s300 s warming → load_failed2 live · 10 cold starts/hourhf_token erased once upsame price per action
Your own checkpoint, before it is a session. Drawn from worker/src/byo.ts — the validation gates and their status codes, the weight cap, the cold-start estimates and the warming timeout, read from the constants that enforce them

Your own checkpoint. You are not limited to the two: model: "hf:<org>/<repo>[@<revision>]" leases a dynamic deployment of any LeRobot checkpoint on the Hugging Face Hub. The hf: prefix is what tells us the id is yours and not one of ours — leave it off and vineetr196/act_so101_pickplace_v1 is read as a catalog id and refused 404 unknown_model, which is the first mistake most people make. The checkpoint has to be something the runner can load: an act, smolvla or diffusion policy, weights as safetensors — a pickled file is refused rather than executed — at most 3 GB, declaring observation.state and at least one camera. We validate the repo before charging anything, pin the commit you get (model comes back as hf:<org>/<repo>@<sha>), and read the features and the action horizon out of the checkpoint itself, so your own training decides the shape of the loop. A private or gated repo needs an hf_token; it is handed to the runner once and erased when the deployment is up.

Two things come from the checkpoint and not from us, and both will catch you out if you assume otherwise. The camera names are whatever its config.json declares — lerobot/smolvla_base asks for camera1, camera2, camera3, not our top and wrist — so read features on the lease (or hello.cameras on the wire) and send exactly those keys. And the action horizon is the checkpoint's, so a chunk may be a different length than ours.

Loading is the one place a custom checkpoint differs from ours: the session starts warming instead of leased — ready_in_s estimates roughly 10 s for an ACT and 45 s for a SmolVLA — and you poll until leased, then connect within 60 s. Measured on production, the same SmolVLA repo took 33.7 s, then 25.1 s, then 20.8 s across three leases: the weight cache is what shrinks it, so the slow one is the first lease of a repo nobody has run yet. A checkpoint the runner cannot load, or 300 s of warming, ends load_failed. Two live custom sessions and ten cold starts an hour per account; the price per action is the same as ours.

Direction, not inventory. Larger VLAs and world foundation models (WFMs) — models that predict how a scene will evolve — are where the platform is going. No WFM endpoint and no other robot is served today; Limits says what is. One checkpoint of ours is loaded on the runner for evaluation and is deliberately not listed in the catalog: it does not appear in GET /v1/models, and a session request for it from an ordinary account is refused like any unknown model (operators can lease it to run the evaluations, which is logged like their other reads) — availability is what the catalog means, not existence.

The loop#

The robot ↔ cloud loopYour robot sends an observation (six joint values, two top/wrist or front/wrist camera images, a task for SmolVLA). The runner returns a chunk of 100 (ACT) or 50 (SmolVLA) actions × 6 joints, played at 30 per second while the next observation is sent. Recorded ACT round trips from a Mac over the public relay: 59–157 ms. your robot · SO-1016 joint values2 cameras · 640×480plays actions at 30 fpsyou own this loopagi.diy runnerone warm slot per sessionact/so101 · smolvla/so101GPU, never touchesyour hardwareobservationstate[6] + 2 JPEG+ task text (SmolVLA)chunkn × 6 actions(100 ACT · 50 SmolVLA)round trip, act/so101 from a Mac over the public relay (recorded probe): 59–157 msa chunk plays 3.3 s (ACT) or 1.7 s (SmolVLA): ask for the next before it ends1 call = 1 observation in, 1 chunk out
The robot ↔ cloud loopYour robot sends an observation (six joint values, two top/wrist or front/wrist camera images, a task for SmolVLA). The runner returns a chunk of 100 (ACT) or 50 (SmolVLA) actions × 6 joints, played at 30 per second while the next observation is sent. Recorded ACT round trips from a Mac over the public relay: 59–157 ms. 1 call = 1 observation in, 1 chunk outyour robot · SO-1016 joint values2 cameras · 640×480plays actions at 30 fpsyou own this loopobservationstate[6]2 JPEG(+ task)chunk100 or 50actions× 6 jointsagi.diy runnerone warm slot per sessionact/so101 · smolvla/so101GPU, never touchesyour hardwareact round trip 59–157 ms (recorded)chunk = 3.3 s (ACT) · 1.7 s (SmolVLA)
The robot ↔ cloud loop. Drawn from runtime/deployments/*.yaml (cameras, horizon, fps) · act round trip: recorded probe ses_fxA3qHVEJekI5JH3TleZUf

One call is one observation in and one chunk out. The robot starts playing the chunk while it already sends the next observation, so the network round trip hides inside the motion the previous chunk bought. Two transports carry the same call:

  • Python / LeRobot — the lease names a Zenoh endpoint and a private deployment name; lerobot connects with its RemoteClient, images JPEG-encoded. agi run, agi rollout and agi.Policy do this for you.
  • Browser — GET /v1/sessions/:id/wire returns a short-lived signed WebSocket URL speaking agi-wire/1; /play and the dashboard test call use it. No Python, no Zenoh in the page. Its own limits: open the socket within 120 s of minting, one message at most 8 MB (two JPEGs are about 60 KB), and send an observation at least every 120 s or the gateway closes the socket with idle. When it refuses something it says so in one line: on production today that is {type: "error", code, message} — e.g. bad_frame, "Text frames must be JSON." (probed 2026-10-25). The merged gateway adds request_id and a request_id_origin that says whether the id is the one you sent or one the gateway minted; it reaches you when the runner's gateway next restarts, because that process is deployed separately from the API.

Either way the bill and the recording come from the same place on the runner, so there is one meter, not two.

Sessions#

Session lifecyclePOST /v1/sessions creates a session in state leased. It becomes active when a client attaches (within about a second of connecting). It ends as client_done, expired (ttl 60–3600 s, default 600), credits, runner_lost, or never_connected if no client attaches within 60 s. POST /v1/sessionswarmingloadingleasedreservedactiveattachedendedback to poolloadedconnectsendsnever_connected — no client within 60 s of the leaseclient_done — you closed itexpired — ttl_s (default 600 s)credits — usage credit hit 0runner_lost — slot disappearedreplay_failed — a replay feed stoppedload_failed — the runner could not load the checkpointbilling: calls only, never lease time — tokens are charged per returned action
Session lifecyclePOST /v1/sessions creates a session in state leased. It becomes active when a client attaches (within about a second of connecting). It ends as client_done, expired (ttl 60–3600 s, default 600), credits, runner_lost, or never_connected if no client attaches within 60 s. POST /v1/sessionsleasedslot reservedclient connectsactiveclient attachedany end reasonendedslot back to poolnever_connected — no client in 60 sclient_done — you closed itexpired — ttl_s (default 600 s)credits — usage credit hit 0runner_lost — slot disappearedreplay_failed — a replay feed stoppedload_failed — the runner could not load the checkpointbilled per action, not lease time
Session lifecycle. Drawn from worker/src/runners.ts (states, end reasons) · worker/src/util.ts (ttl, connect_by)

POST /v1/sessions leases one warm slot — a policy process already loaded on the GPU — and returns a session: its id, the deployment name, the endpoint, the features the model expects, the horizon and the fps. Nobody else can use that slot until the session ends.

  • warming — only for your own checkpoint (hf:<org>/<repo>): the runner is loading it, so there is no slot to attach to yet. Poll the session until it reads leased, then connect. A checkpoint the runner cannot load ends the session load_failed.
  • leased — reserved, waiting for your client. If nothing attaches within 60 s the session ends never_connected and the slot goes back to the pool.
  • active — a client attached (reported within about a second).
  • ended — client_done (you closed it, or the SDK did on exit), expired (ttl_s, default 600, allowed 60–3600), credits (usage credit reached zero), runner_lost (the runner stopped reporting the slot), replay_failed, or load_failed (a checkpoint of yours the runner could not load).

A session costs nothing to hold; only returned actions are billed. Closing is still good manners: the slot is free for the next person only after the runner respawns it (10–30 s measured). Revoking an API key refuses future calls made with it; sessions already running keep going until they end.

Availability#

GET /v1/models (public, no key) reports each model's state and its free slots. Today one GPU runner keeps 2 ACT slots and 3 SmolVLA slots warm. The words the product uses:

statemeanswhat to do
Readyat least one slot is free (state: "ready", slots_free > 0)lease now
Busy — try again shortlythe model is up but every slot is leased (slots_free: 0)a lease answers 409 no_slots with retry_after_s: 15; a freed slot respawns in 10–30 s
Warmingslot processes are loading (state: "warming")wait; ACT loads in about 12 s, SmolVLA about 31 s
Offlineno runner is online for it, or its public endpoint is unreachablea lease answers 503 relay_down (retry_after_s: 60) or 404 unknown_model
Availability unavailableyour request for /v1/models itself failednot the same as offline or zero; retry the request

A ready model is a promise about capacity, not about inference: the first call is what proves the policy answers.

More than one arm#

A slot serves one client at a time, so two arms means two sessions and the pool is the ceiling: 2 ACT slots and 3 SmolVLA slots on one runner today (Availability). The lease after the last free slot is 409 no_slots with retry_after_s: 15 — a refusal you retry, not a queue you wait in.

Running them together costs time as well as slots. Measured on the runner (runtime/CAPACITY.md, 2026-10-24, local client): SmolVLA 365 ms alone, about 495 ms each with two streams — +35 %, not double, because one 500M-parameter forward pass does not saturate the GPU. Extrapolated from that same increment: 3 streams about 620 ms and 5 about 880 ms per call, still inside the 1.67 s a 50-action chunk buys, so each arm stays ahead of its queue. Five concurrent streams have not been run for real yet. Your round trip is on top of these numbers (Latency).

Credits are per account, not per session: two SmolVLA arms at one call per second spend about twice 2,900 tokens a minute. Each session bills itself, and the dashboard total is the sum.

Observations and cameras#

An observation is one snapshot from the robot:

  • observation.state — six float32 joint values, in this order: shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper.
  • two RGB camera images, 640×480 each, under the model's camera names: ACT observation.images.top + observation.images.wrist; SmolVLA observation.images.front + observation.images.wrist.
  • for SmolVLA, the task text — over the browser wire only the first 512 characters reach the policy and the rest is cut silently (the echo and the trace show the short version); a rollout over the public relay passes your task through unchanged.

The names matter because each policy learned from particular viewpoints: top/front is a fixed camera looking at the table, wrist rides on the gripper.

Where the two cameras sitSide view. The table camera is 0.5 m out along the arm's reach and 0.6 m up, tilted back toward the arm (vertical field of view 52.9°); ACT receives it as observation.images.top, SmolVLA as observation.images.front. The wrist camera rides on the gripper (41.4°). Pose from the /play world manifest, matched to SmolVLA's training camera. tableSO-101 basetable camera · 0.5 m out, 0.6 m upACT: top · SmolVLA: front52.9° vertical view, tilted to the armwrist camera · on the gripper41.4° · both models: wristpose from the /play world manifest, matched to SmolVLA's training camera;ACT's training camera pose is not published · arm drawn schematically
Where the two cameras sitSide view. The table camera is 0.5 m out along the arm's reach and 0.6 m up, tilted back toward the arm (vertical field of view 52.9°); ACT receives it as observation.images.top, SmolVLA as observation.images.front. The wrist camera rides on the gripper (41.4°). Pose from the /play world manifest, matched to SmolVLA's training camera. tableSO-101 basetable camera · 0.5 m out, 0.6 m upACT: top · SmolVLA: frontwrist camera · on the gripper41.4° · both models: wrist52.9° vertical view, tilted to the armpose: /play world, SmolVLA rig
Where the two cameras sit. Drawn from runtime/worlds/worlds/desk/world.v1.json (camera pose, field of view, wire names) · arm drawn schematically
front · what smolvla/so101 received (simulation)front · what smolvla/so101 received (simulation)
wrist · what smolvla/so101 received (simulation)wrist · what smolvla/so101 received (simulation)
Call 1 of recorded session ses_6VvrpGKfCWlUDyAgNYgGfD: the two 640×480 images the policy actually received, rendered by the browser simulation with the camera matched to SmolVLA's training rig. A reference for placement and framing — your real camera should frame the table and gripper the same way.

That is where /play places them: the table camera's pose and field of view were taken from SmolVLA's training rig. ACT's training camera pose is not published; in /play its top image comes from this same camera, which is a stand-in rather than ACT's rig. For ACT, compare your top image with the checkpoint's training data on the Hub rather than trusting a number.

agi rollout maps your two cameras onto the model's names by name, refuses a wrong count, and refuses names it cannot place — all before anything is leased; the exact rule, with examples run against the SDK, is under Compatibility. A missing image is not an error on the browser wire — the gateway fills it with black so a connection test can run — but a policy fed black frames is not seeing anything.

Chunks, horizon and fps#

One returned chunk: 50 actions × 6 jointsFirst call of recorded session ses_6VvrpGKfCWlUDyAgNYgGfD (smolvla/so101, 2026-10-25T00:05:10Z): 50 rows of six joint targets in normalized units (−100…100, gripper 0…100), covering 1.67 s at 30 fps. Camera inputs were the browser simulation. call 1 of ses_6VvrpG… · smolvla/so101camera inputs: browser simulation · chunk: live policy output100500-50-10001020304049action row · each row is 1/30 s · the whole chunk is 1.67 s of motionwrist_flexelbow_flexgripperwrist_rollshoulder_panshoulder_lift
One returned chunk: 50 actions × 6 jointsFirst call of recorded session ses_6VvrpGKfCWlUDyAgNYgGfD (smolvla/so101, 2026-10-25T00:05:10Z): 50 rows of six joint targets in normalized units (−100…100, gripper 0…100), covering 1.67 s at 30 fps. Camera inputs were the browser simulation. call 1 of ses_6VvrpG… · smolvla/so101simulated cameras · live policy100500-50-10002449row (1/30 s each) → 1.67 swristelbowgripperrollsh_pansh_lift
One returned chunk. Drawn from call 1 of recorded session ses_6VvrpGKfCWlUDyAgNYgGfD (smolvla/so101, 2026-10-25T00:05:10Z) — simulated cameras, live policy output

A chunk is the sequence of actions one call returns: n rows × 6 joints, float32. Each row is one action — six target joint values for one tick of the control loop. The horizon is n: 100 for ACT, 50 for SmolVLA, fixed when the session is leased.

fps is the rate the chunk is meant to be played at: 30 actions per second for both models. So one ACT chunk is about 3.3 s of planned motion and one SmolVLA chunk about 1.7 s. That is an execution rate, not a promise about how many requests per second the service answers.

The chart above is the first chunk of a recorded session: the camera images it answered were rendered by the browser simulation; the chunk itself is the live policy's output.

Tokens and billing#

One returned action is one token, and one token costs $0.0001. A call is billed for every row in its chunk, whether or not your robot plays all of them:

modelactions per callper call
act/so101100$0.01
smolvla/so10150$0.005

A minute of motion costs more than 30 tokens a second. Billing counts every returned row, but the robot does not play a whole chunk before the next one arrives: LeRobot asks for the next chunk while 0.5 s of motion (or one round trip, if that is longer) is still queued, and the new chunk replaces the unplayed rest. Recorded with agi rollout --fake (agi-diy 0.4.9, over the public relay, 2026-10-25): smolvla/so101 settled at one call every 1.03 s — about 48 actions a second — about 2,900 tokens, $0.29, per minute of motion; act/so101 played 3.12 s of its first 3.33 s chunk before the next call — about 32 actions a second — about 1,900 tokens, $0.19, per minute (one interval). A slower network makes the requests come earlier and the minute cost more; the session total on the dashboard is the number to plan with.

Why a minute of motion costs more than 30 actions a secondRecorded smolvla/so101 rollout (ses_HqREpKrHDikvj6brxmQOrj): 7 calls, 350 actions billed. Each call waits 607, 2194, 527, 575, 470, 498, 573 ms for its chunk; a chunk is 1.67 s of motion but the next one arrives every 1.03 s in steady state and replaces the unplayed rest, so the robot plays about 48 actions a second and a minute costs about 2,900 tokens, $0.29. smolvla/so101 · agi rollout --fake over the public relay · recorded 2026-10-25one chunk = 50 actions = 1.67 s of motion · in steady state the next call lands every 1.03 sso the robot plays about 48 actions a second — about 2,900 tokens, $0.29, per minute of motion0s1s2s3s4s5s6s7s8s9s10s11scall 1 · waited 607 ms · played 1.67 s · then 1.49 s with an empty queue (the arm holds)call 2 · waited 2194 ms · played 0.61 s · 1.06 s replaced unplayed (32 of 50 actions, still billed)call 3 · waited 527 ms · played 1.64 s · 0.03 s replaced unplayed (1 of 50 actions, still billed)call 4 · waited 575 ms · played 0.93 s · 0.73 s replaced unplayed (22 of 50 actions, still billed)call 5 · waited 470 ms · played 1.06 s · 0.61 s replaced unplayed (18 of 50 actions, still billed)call 6 · waited 498 ms · played 1.11 s · 0.56 s replaced unplayed (17 of 50 actions, still billed)call 7 · waited 573 ms · played 1.67 s · the last chunk, played to the endwaiting for the chunkchunk playingreplaced unplayed — billed anyway7 calls · 350 actions billed · session ses_HqREpKrHDikvj6brxmQOrj · ended client_done
Why a minute of motion costs more than 30 actions a secondRecorded smolvla/so101 rollout (ses_HqREpKrHDikvj6brxmQOrj): 7 calls, 350 actions billed. Each call waits 470, 498, 573 ms for its chunk; a chunk is 1.67 s of motion but the next one arrives every 1.03 s in steady state and replaces the unplayed rest, so the robot plays about 48 actions a second and a minute costs about 2,900 tokens, $0.29. smolvla/so101 · steady statechunk = 1.67 s · call every 1.03 s≈ 2,900 tokens $0.29 / min6s7s8s9s10s11scall 5 · 470 ms wait0.61 s replaced = 18 actions billedcall 6 · 498 ms wait0.56 s replaced = 17 actions billedcall 7 · 573 ms waitplayed to the endwaiting for the chunkplayedreplaced unplayed — billedcalls 5–7 of 7 · ses_HqREpK…
Why a minute of motion costs more than 30 actions a second. Drawn from recorded rollout ses_HqREpKrHDikvj6brxmQOrj (agi rollout --fake, 2026-10-25T07:16Z) — observation/chunk event times from GET /v1/sessions/:id/trace · horizon / fps from the deployment files

The picture is that recorded SmolVLA run, event by event: the first calls were slow enough that the queue ran dry and the arm held its last target, then the loop settled into a call every 1.03 s — so roughly a third of every chunk was replaced before it could be played, and billed all the same.

A token is a robot action, not an LLM text token. New accounts receive 200,000 tokens once — shown as $20 in usage credit. Usage credit is a balance for calls, not cash. A session can start only while the balance is above zero; when it reaches zero, live sessions end with credits (the balance can dip below zero by at most one metering batch). The first CLI command in the docs, agi run act/so101 --probe -n 1, is exactly one call: 100 tokens, $0.01.

Normalized units#

Normalized unitsBoth checkpoints use LeRobot normalized positions: five arm joints from −100 to 100 over each joint's calibrated range, the gripper 0 to 100. The session label so101-degrees-gripper-percent-v1 says degrees; do not execute these values as degrees. Marks are the first recorded observation of ses_6VvrpGKfCWlUDyAgNYgGfD. what the six numbers meanlabel says "so101-degrees-gripper-percent-v1" — the data is normalizedarm joint (5)-1000100shoulder_lift -20gripper050100gripper 9.1±100 = the ends of YOUR arm's calibrated range,not a fixed angle — calibration decides the mapping
Normalized unitsBoth checkpoints use LeRobot normalized positions: five arm joints from −100 to 100 over each joint's calibrated range, the gripper 0 to 100. The session label so101-degrees-gripper-percent-v1 says degrees; do not execute these values as degrees. Marks are the first recorded observation of ses_6VvrpGKfCWlUDyAgNYgGfD. what the six numbers meanlabel says "degrees"; data is normalizedarm joint (5)-1000100shoulder_lift -20gripper050100gripper 9.1±100 = ends of your calibrated range,not a fixed angle: calibration maps it
Normalized units. Drawn from web/lib/sim/so101-units.ts · marks: first observation of ses_6VvrpGKfCWlUDyAgNYgGfD

Both live checkpoints were trained on LeRobot's normalized motor positions (use_degrees=false, the default when their datasets were recorded): the five arm joints run from −100 to 100 across each joint's calibrated range, the gripper from 0 to 100. The values are not degrees, even though the session's semantics label still reads so101-degrees-gripper-percent-v1. That label is a known mismatch kept for compatibility until the contract changes; trust the data, not the name.

Set use_degrees=false on a real arm. Current LeRobot defaults the SO-101 follower to use_degrees=True: the arm would then report its state in degrees and execute these normalized actions as degrees. Pass --robot.use_degrees=false to lerobot-rollout. agi rollout from agi-diy 0.4.6 adds it for you and refuses true; on 0.4.5 or earlier, pass it yourself after -- (pip install -U agi-diy to upgrade). The session label cannot catch this — it says degrees either way.

What ±100 means physically depends on your arm's LeRobot calibration file. Confirm joint order, sign and limits against the checkpoint before you let a chunk move hardware. The browser simulation converts its own radians to these units; that conversion does not calibrate a real arm.

Latency#

Three numbers, never one:

  • policy time — the model's forward pass (server_ms.policy in a trace).
  • time on the runner — from the observation reaching the runner to the chunk leaving it: policy time plus decoding and post-processing. A trace's rtt_ms is this number; it does not include your network.
  • round trip — what your client measures from sending the observation to holding the chunk: time on the runner plus the network both ways. Only your client can measure it.
Where the time goes in one act/so101 callThree recorded act/so101 calls from ses_fxA3qHVEJekI5JH3TleZUf (2026-10-25T05:36:27Z, synthetic inputs, the agi CLI on a Mac, Zenoh over the public relay (tcp/bore.pub)). Policy time 67.2, 18.7, 20.3 ms; time on the runner 98.4, 37.1, 35.3 ms; round trip at the client 157, 59, 95 ms. The rest is network. act/so101 · recorded probe ses_fxA3qHVEJekI5JH3TleZUf · 2026-10-25T05:36:27Zsynthetic inputs · the agi CLI on a Mac, Zenoh over the public relay050100150200call 167.2 · 98.4 · 157 mscall 218.7 · 37.1 · 59 mscall 320.3 · 35.3 · 95 mspolicyrest of the runnernetworkms
Where the time goes in one act/so101 callThree recorded act/so101 calls from ses_fxA3qHVEJekI5JH3TleZUf (2026-10-25T05:36:27Z, synthetic inputs, the agi CLI on a Mac, Zenoh over the public relay (tcp/bore.pub)). Policy time 67.2, 18.7, 20.3 ms; time on the runner 98.4, 37.1, 35.3 ms; round trip at the client 157, 59, 95 ms. The rest is network. act/so101 · recorded probeMac → public relay · synthetic inputs050100150200call 167.2 · 98.4 · 157 mscall 218.7 · 37.1 · 59 mscall 320.3 · 35.3 · 95 mspolicyrest of the runnernetworkms
Where the time goes in one call. Drawn from recorded probe ses_fxA3qHVEJekI5JH3TleZUf (2026-10-25T05:36:27Z) — trace server_ms.policy + rtt_ms, client ms from the agi CLI

A recorded example: three act/so101 test calls from a Mac over the public relay (session ses_fxA3qHVEJekI5JH3TleZUf, 2026-10-25, synthetic inputs). Policy time 18.7–67.2 ms, time on the runner 35.3–98.4 ms, round trip at the client 59–157 ms. Here the first call was the slowest, and for ACT the network was most of the wait.

Where the time goes in one smolvla/so101 callThree recorded smolvla/so101 calls from ses_L2hzN236hB45ArRQkeSf1d (2026-10-25T06:00:01Z, synthetic inputs, the agi CLI on a Mac, Zenoh over the public relay (tcp/bore.pub)). Policy time 416.6, 366.6, 369 ms; time on the runner 446.1, 382, 381.9 ms; round trip at the client 552, 757, 1141 ms. The rest is network. smolvla/so101 · recorded probe ses_L2hzN236hB45ArRQkeSf1d · 2026-10-25T06:00:01Zsynthetic inputs · the agi CLI on a Mac, Zenoh over the public relay025050075010001250call 1416.6 · 446.1 · 552 mscall 2366.6 · 382 · 757 mscall 3369 · 381.9 · 1141 mspolicyrest of the runnernetworkms
Where the time goes in one smolvla/so101 callThree recorded smolvla/so101 calls from ses_L2hzN236hB45ArRQkeSf1d (2026-10-25T06:00:01Z, synthetic inputs, the agi CLI on a Mac, Zenoh over the public relay (tcp/bore.pub)). Policy time 416.6, 366.6, 369 ms; time on the runner 446.1, 382, 381.9 ms; round trip at the client 552, 757, 1141 ms. The rest is network. smolvla/so101 · recorded probeMac → public relay · synthetic inputs025050075010001250call 1416.6 · 446.1 · 552 mscall 2366.6 · 382 · 757 mscall 3369 · 381.9 · 1141 mspolicyrest of the runnernetworkms
Where the time goes in one smolvla/so101 call. Drawn from recorded probe ses_L2hzN236hB45ArRQkeSf1d (2026-10-25T06:00:01Z) — trace server_ms.policy + rtt_ms, client ms from the agi CLI

The same test for smolvla/so101 (session ses_L2hzN236hB45ArRQkeSf1d, 2026-10-25): policy time 366.6–416.6 ms, time on the runner 381.9–446.1 ms, round trip at the client 552–1141 ms. The runner's share stayed flat; the public relay's did not — the third call spent about 760 ms on the network. Plan your loop around the round trip you measure, not the runner's.

Compute time versus the motion it buysMeasured on the runner (runtime/CAPACITY.md): ACT 18–20 ms (rtt 34 ms) per call for 100 actions = 3.33 s of motion; SmolVLA 365 ms (rtt 375 ms) for 50 actions = 1.67 s; 495 ms each (rtt ~495) with two concurrent sessions. Public network adds to every round trip. server time per call vs. motion per chunkmeasured on the runner, local client (runtime/CAPACITY.md)act/so10118–20 ms (rtt 34 ms) · 3.33 s of motionsmolvla/so101365 ms (rtt 375 ms) · 1.67 s of motionsmolvla/so101 ×2495 ms each, 2 concurrent · 1.67 s of motionserver timemotion in one chunk
Compute time versus the motion it buysMeasured on the runner (runtime/CAPACITY.md): ACT 18–20 ms (rtt 34 ms) per call for 100 actions = 3.33 s of motion; SmolVLA 365 ms (rtt 375 ms) for 50 actions = 1.67 s; 495 ms each (rtt ~495) with two concurrent sessions. Public network adds to every round trip. server time vs. motion per chunkmeasured on the runner, local clientact/so10118–20 ms (rtt 34 ms) · 3.33 s of motionsmolvla/so101365 ms (rtt 375 ms) · 1.67 s of motionsmolvla/so101 ×2495 ms each, 2 concurrent · 1.67 s of motionserver timemotion in one chunk
Compute time versus the motion it buys. Drawn from runtime/CAPACITY.md (on the GPU runner, local client, 2026-10-24) · horizon / fps from the deployment files

Measured on the runner with a local client (runtime/CAPACITY.md, 2026-10-24): ACT 18–20 ms per call; SmolVLA 365 ms alone, about 495 ms each with two sessions at once. The public relay has added roughly 60–150 ms to a round trip. What matters for a smooth loop is the ratio: a round trip must finish before the chunk you are playing runs out.

Time in a recorded sessionFour calls from ses_6VvrpGKfCWlUDyAgNYgGfD (smolvla/so101). Time on the runner per call (444, 405, 403, 389 ms, of which policy time 413, 388, 388, 373 ms) is short next to the 1.67 s of motion each chunk buys. smolvla/so101 · recorded session ses_6VvrpGKfCWlUDyAgNYgGfD · 2026-10-25T00:05:10Zcamera inputs: browser simulation · time on the runner and policy time: live service0s1s2s3s4s5s6s7s8s9scall 1: 444 ms on the runner (413 ms policy) → 50 actionscall 2: 405 ms on the runner (388 ms policy) → 50 actionscall 3: 403 ms on the runner (388 ms policy) → 50 actionscall 4: 389 ms on the runner (373 ms policy) → 50 actionson the runnerchunk playing (1.67 s)
Time in a recorded sessionFour calls from ses_6VvrpGKfCWlUDyAgNYgGfD (smolvla/so101). Time on the runner per call (444, 405, 403, 389 ms, of which policy time 413, 388, 388, 373 ms) is short next to the 1.67 s of motion each chunk buys. smolvla/so101 · recorded 2026-10-25simulated cameras · live policy0s1s2s3s4s5s6s7s8s9s444 ms405 ms403 ms389 mson the runnerchunk playing (1.67 s)policy time 373–413 ms of each
Time in a recorded session. Drawn from 4 calls of ses_6VvrpGKfCWlUDyAgNYgGfD — trace rtt_ms (time on the runner) and server_ms.policy

In the recorded SmolVLA session above, time on the runner per call (389–444 ms, 373–413 ms of it policy time) was about a quarter of the 1.67 s its chunk bought; that trace does not hold the browser's own round trip. Your numbers will differ with network, load and model; these are dated measurements, not a service level.

The robot plays chunks from a local queue, so a network drop does not stop the arm at once — and it does not let it run on forever either. The figure shows the worst case; the four steps under it are the same paragraph /docs carries, generated from one source (web/lib/linkdrop.mjs).

When the link drops mid-runWorst case, the link drops just after a chunk arrived: the arm plays what is queued without new observations (up to 3.3 s for ACT, 1.67 s for SmolVLA), then holds its last position target for the 3 s grace agi rollout sets, then the run stops with "action starvation grace expired". Chunks that arrive during the hold resume the run from a fresh camera frame. link dropsact/so101≤ 3.3 s3 s3 storque offsmolvla/so101≤ 1.67 s3 s3 storque off0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 splays the queued chunk · no new imagesholds the last position target| run stopsmoves back to the pose it started in| torque off: the arm can fall under gravityworst case: the drop lands right after a chunk arrived. LeRobot asks for the nextchunk when 0.5 s of motion is left, so usually far less is queued. Chunks thatarrive during the hold resume the run from a fresh camera frame.
When the link drops mid-runWorst case, the link drops just after a chunk arrived: the arm plays what is queued without new observations (up to 3.3 s for ACT, 1.67 s for SmolVLA), then holds its last position target for the 3 s grace agi rollout sets, then the run stops with "action starvation grace expired". Chunks that arrive during the hold resume the run from a fresh camera frame. link dropsact/so1013.333smolvla/so1011.67330 s2 s4 s6 s8 s10 splays up to one chunk · no new imagesholds the last position target| run stops: starvation grace expiredmoves back to its start pose| torque off: the arm can fallworst case: the drop lands justafter a chunk arrived. LeRobot asksagain at 0.5 s left, so usuallyless is queued. Chunks during thehold resume from a fresh frame.
When the link drops mid-run. Drawn from sdk/python/agi/rollout.py (starvation grace, timeouts) · LeRobot @d3793b7e remote engine (refill at 0.5 s left, hold, starvation fault) + rollout teardown (move to start pose, disconnect torque) · horizon / fps from the deployment files

If the link drops mid-run — what LeRobot's remote rollout does with the values agi rollout sets (LeRobot @d3793b7e, the commit the runners serve):

  1. It finishes the queued chunk, open loop. The arm keeps playing the actions it already has, with no new camera image or joint reading reaching the policy: at most one chunk: 100 rows, about 3.3 s for act/so101; 50 rows, 1.67 s for smolvla/so101. Usually less — LeRobot asks for the next chunk when 0.5 s of motion is left.
  2. The moment the queue is empty, it holds. LeRobot re-sends the last position targets, so the motors stay energized at the last commanded pose. This is a position hold, not a brake: the arm can still settle toward that target. A chunk that arrives now resumes the run from a fresh camera frame.
  3. After the grace, the run ends. If no chunk arrives within the 3 s grace agi rollout sets (starvation grace, --inference.action_starvation_grace_s; LeRobot's own default is 1 s), the run faults with "action starvation grace expired".
  4. On the way out it moves once more, then lets go. When the run ends LeRobot first drives the arm back to its initial position, the pose it had when the run started (a smooth 3 s move; --return_to_initial_position=false skips it), then disconnects with the follower's default disable_torque_on_disconnect=true: torque is released and the arm can fall under gravity. Support the arm, or pass --robot.disable_torque_on_disconnect=false to keep the motors powered after exit.

Plan the workspace for the open-loop part and for the move home: whatever the last chunk was doing, the arm completes it, holds, travels back to where it started (unless you turned that off), and then lets go. A hosted policy cannot protect a physical arm through a network outage; your local stop can.

Traces, playback and running inference again#

Every session is recorded by default as a trace: each observation the policy saw (joint state, task, camera frames) and each chunk it returned, with timing.

What a trace holdsThe recording of ses_fxA3qHVEJekI5JH3TleZUf: each observation event (joint state, task, both camera frames) and each action event (the chunk, its time on the runner, policy time, tokens), at their real times — obs 0 ms → act 99 ms; obs 122 ms → act 159 ms; obs 221 ms → act 256 ms. trace of ses_fxA3qHVEJekI5JH3TleZUf · act/so101 · synthetic inputs○ observation event (what the policy saw) ● action event (what it returned)050100150200250300mscall 1○ obs · t 0 msstate[6] · top + wrist11.4 KB sent · frames kept● act · t 99 ms100 × 6 · 100 tokensrtt_ms 98.4 · policy 67.2call 2○ obs · t 122 msstate[6] · top + wrist11.4 KB sent · frames kept● act · t 159 ms100 × 6 · 100 tokensrtt_ms 37.1 · policy 18.7call 3○ obs · t 221 msstate[6] · top + wrist11.4 KB sent · frames kept● act · t 256 ms100 × 6 · 100 tokensrtt_ms 35.3 · policy 20.3readable 24 h · playback is free · run inference again = a new, billed session
What a trace holdsThe recording of ses_fxA3qHVEJekI5JH3TleZUf: each observation event (joint state, task, both camera frames) and each action event (the chunk, its time on the runner, policy time, tokens), at their real times — obs 0 ms → act 99 ms; obs 122 ms → act 159 ms; obs 221 ms → act 256 ms. trace of a recorded act probe○ observation in ● chunk out050100150200250300mscall 1○ obs · t 0 msstate[6] · top + wrist11.4 KB sent · frames kept● act · t 99 ms100 × 6 · 100 tokensrtt_ms 98.4 · policy 67.2call 2○ obs · t 122 msstate[6] · top + wrist11.4 KB sent · frames kept● act · t 159 ms100 × 6 · 100 tokensrtt_ms 37.1 · policy 18.7call 3○ obs · t 221 msstate[6] · top + wrist11.4 KB sent · frames kept● act · t 256 ms100 × 6 · 100 tokensrtt_ms 35.3 · policy 20.3playback free · run again is billed
What a trace holds. Drawn from recorded probe ses_fxA3qHVEJekI5JH3TleZUf — GET /v1/sessions/:id/trace event times, sizes and timings (docs/TRACE.md)

Camera images and actions are recorded for session inspection — frames may show your room, so treat them like any camera recording.

Who can read a recording. The account that owns the session, through its browser cookie, or the API key that leased it. One more case, said plainly because the alternative is a surprise: an operator account — the logins in the Worker's ADMIN_LOGINS, today one — can read any session and read its trace and frames. It stops there: an operator cannot open a session's browser-wire URL, because a wire socket sends observations and returns chunks and every action is billed to the session's owner — that capability was removed, and an attempt is refused and logged {admin_denied, login, session}. Every operator read is written to the Worker's log as {admin_read, login, session, owner}. Nothing else reaches a recording: traces are not public, a frame is not served without the owner's cookie or the leasing key, and record: false at session create means there is nothing to read at all — the bill and the session row stay either way.

  • Playback — viewing a trace on the dashboard. Free; nothing runs.
  • Run inference again (POST /v1/sessions/:id/replay) — feeds the recorded observations to a policy in a new session, at the recorded cadence. It is billed like any session, moves no robot, and may return different actions. The target has to be a model served right now and it has to expect the same cameras (act/so101 top/wrist cannot run on a smolvla/so101 front/wrist recording). With no model in the body the target defaults to the recording's own — which for a custom checkpoint is the pinned hf:<org>/<repo>@<sha> that nobody serves once the session ended, so a custom session must be replayed with an explicit model.
  • Retention — traces are readable through the API for 24 hours from session creation; the runner deletes older recordings periodically, and skips frames (keeps events) if its trace disk passes 20 GB. record: false on POST /v1/sessions turns recording off; billing and session history are kept either way.

Did it work?#

Nothing in the API answers that. A session tells you the policy answered — a chunk came back, in this many milliseconds, for this many tokens — and how the run ended (end_reason, see Sessions). There is no success, reward or score field in a session, a trace event or any other response: whether the orange ended up on the plate is your measurement, in your workspace, against your task.

What a run answers, and what you have to measureA recorded run (ses_fxA3qHVEJekI5JH3TleZUf, 3 calls of 100 actions, 300 tokens, ended client_done) answers with chunks and timings; its trace keeps the observations for 24 hours; POST /v1/sessions/:id/replay asks the same observations again in a new billed session, so two policies can be compared on identical inputs. Task success appears nowhere in the API: it is measured by watching the robot. recorded run ses_fxA3qHVEJekI5JH3TleZUf · act/so101 · synthetic probe inputsthe service answers3 chunks · 100 × 6 each35–98 ms on the runner300 tokens · $0.03ended client_donenever answereddid the task succeedhow close it camewas that action rightthe trace3 observationsstate[6] + 2 frames11.4 KB eachkept 24 hoursre-ask act/so101same inputs → $0.01 a callre-ask smolvla/so101409 — it wants front + wristrun inference again:a new, billed sessionno robot movesThe world does not replay: identical inputs compare judgement, not success.Counting completions is still yours — watch the run.
What a run answers, and what you have to measureA recorded run (ses_fxA3qHVEJekI5JH3TleZUf, 3 calls of 100 actions, 300 tokens, ended client_done) answers with chunks and timings; its trace keeps the observations for 24 hours; POST /v1/sessions/:id/replay asks the same observations again in a new billed session, so two policies can be compared on identical inputs. Task success appears nowhere in the API: it is measured by watching the robot. recorded run · act/so101the service answers3 chunks · 100 × 6 each35–98 ms on the runner300 tokens · $0.03ended client_donenever answereddid the task succeedhow close it camewas that action rightthe trace3 observations, 24 hstate[6] + 2 frames eachre-ask → new billed sessionidentical inputs comparejudgement, not success —watch the run yourself
What a run answers, and what you have to measure. Drawn from recorded probe ses_fxA3qHVEJekI5JH3TleZUf (2026-10-25T05:36:27Z) — calls, time on the runner, tokens and end reason from its trace · replay rules from docs/CONTRACT.md (409 incompatible_features) and the deployment cameras
aboutthe service reportsit never reports
the answerthe chunk's actions and its timing — rtt_ms, server_ms.policywhether those were the right actions
the endingend_reason: client_done, expired, credits, runner_lost, never_connected, replay_failed, load_failed (a custom checkpoint the runner could not load)whether the task was completed
the billtokens_used, and the session total on the dashboardhow close it came, or why it failed
the recordthe frames and states it answered, for 24 hourswhat the arm did after the chunk left

So judging a policy is three things you do yourself:

  1. Watch the run. On a real arm that is your own cameras and your own eyes; in the browser it is /play, where the same policy drives a simulated SO-101 you can see from any angle. Count attempts and completions by hand — a dozen honest trials say more than any number we could return.
  2. Read the trace. The recording holds exactly what the policy was asked and what it answered, frame by frame. A failed grasp usually shows up there first: a camera pointing at the wrong thing, a state the checkpoint never saw in training, a chunk that barely moves.
  3. Ask the same questions again — run inference again on a recorded session (POST /v1/sessions/:id/replay) and the recorded observations go to a policy a second time, at the recorded cadence, as a new billed session. That is the honest way to compare two policies: identical inputs, different model, no robot in the room. The refusals come in the order the slot is resolved, not the order you would guess: a model nobody serves is 404 unknown_model, a model whose slots are all busy is 409 no_slots with retry_after_s (retry it), and only a target that is actually available gets as far as the camera and state comparison — 409 incompatible_features, which names both camera lists.

What re-running does not give you is a different world. The recording answers the questions the first run asked; the moment a policy acts differently the real scene would have diverged, so a replay compares judgement on fixed inputs, not task success. For that, go back to step 1.

Simulation and real#

Simulated inputs, live policy outputIn /play the arm, its physics and the camera images the policy receives are simulated in the browser; the room backdrop is shown to people only and never sent to the policy. The policy, its chunks, the timing, the bill and the trace are the live service. simulated in your browserSO-101 arm + physics (MuJoCo)policy cameras: top · wrist · front(rendered from geometry)room backdrop: human view onlyactions as position targetskp 998.22 · ±2.94 N·m · 200 Hzlive on the servicethe policy and its weightsevery returned chunktiming on the runnerthe bill and the traceobschunkno physical robot moves; a call here is billed like any othernot modelled: your calibration and wear · the grip you really havecamera exposure, blur and lighting · the room your arm is in
Simulated inputs, live policy outputIn /play the arm, its physics and the camera images the policy receives are simulated in the browser; the room backdrop is shown to people only and never sent to the policy. The policy, its chunks, the timing, the bill and the trace are the live service. simulated in your browserSO-101 arm + physics (MuJoCo)cameras: top · wrist · front(rendered from geometry)backdrop: human view onlyactions as position targetskp 998.22 · 200 Hzobschunklive on the servicethe policy and its weightsevery returned chunktiming on the runnerthe bill and the traceno robot moves · calls are billednot modelled: calibration, grip, optics
Simulated inputs, live policy output. Drawn from web/lib/sim (MuJoCo, rendered policy cameras, splat backdrop) · the live service for everything else

/play is an SO-101 simulated in your browser — MuJoCo physics and rendered cameras — driven by real calls to the live policies. Simulated inputs, live policy output. Two views exist there: the camera images sent to the policy are rendered from the simulation's geometry, and the photographic room behind the arm is a human view only — a policy never sees it. Running a policy in /play spends usage credit like any other call.

The recorded session on the landing page and in the charts above is from that simulation, labelled as such. No result here shows a physical arm completing a task: a returned chunk is not proof the task succeeded.

What it models, and what it cannot. Each action row is written as a position target for a modelled STS3215 servo (kp 998.22, kv 2.731, torque limited to ±2.94 N·m), stepped at a 0.005 s timestep — 200 steps a second — with joint damping 1, friction loss 0.1 and armature 0.005. So a chunk in /play is followed the way a servo follows it, with its lag and its torque ceiling, not teleported joint by joint. Objects get the mass and friction the world manifest declares (0.15 kg when it declares none), and the contact model is MuJoCo's.

What no number in that list can stand in for: your arm's own calibration, backlash and wear; the grip your gripper actually has on the thing in front of it; and your cameras — exposure, white balance, motion blur, lighting. The policy's images in /play are rendered from the scene's geometry, which is a cleaner picture than a camera takes. Treat a run there as a test of the loop, the units and the bill; the only test of a task is your own arm in your own room.

Compatibility#

Your setup matches a served model when all of these hold:

  1. Robot — an SO-101 follower arm driven through LeRobot (--robot.type=so101_follower).
  2. State — six joint values in the order above, in normalized units from your own calibration (--robot.use_degrees=false).
  3. Cameras — exactly two, 640×480 RGB, placed like the model's: a fixed table view (top for ACT, front for SmolVLA) and a wrist camera.
  4. Rate — your loop plays actions at the model's 30 fps.
  5. Scene — close to what the checkpoint was trained on. Task-specific checkpoints degrade quickly with a different table, lighting or objects.
  6. Python — 3.12 for agi rollout and wire inference. LeRobot at the pinned commit requires Python ≥ 3.12, and on Python 3.14 lerobot-rollout crashes while parsing its own flags (TypeError: str | None is not callable, raised in draccus; the session closes with 0 tokens). The REST calls alone (agi models, --dry-run) work on any supported Python.

agi rollout checks the first four before it leases anything (the Python version it does not check yet), so a mismatch costs nothing. What it does with each (sentences quoted from sdk/python/agi/rollout.py):

checkwhat agi rollout does
robotno --robot.type → refused: "--robot.type is required after --"
unitsadds --robot.use_degrees=false; true → refused: "reads and commands degrees, but this checkpoint uses normalized units"
camera countanything but two → refused: "expects 2 cameras"
camera sizenot 640×480 → refused: "expects 640×480 (width×height)"
camera namesmaps your names onto the model's (rename map, --rename_map) and prints the mapping — the rule is below
rateadds --fps=30; any other value → refused: "differs from the model's 30"
policy flags--policy.path → refused: "the runner's business under remote inference"
scenecannot check it: the policy still answers, the chunk is just less likely to do the task

Name each camera after the view it is. agi-diy 0.4.9 and later match cameras by name, never by order: a camera whose name contains exactly one view word as a whole word takes that slot (wrist_cam → wrist, Front → front; laptop never counts as top). One leftover camera takes the remaining slot only if its name does not say it is another view: {"wrist": …, "table": …} → table → top for act/so101, but {"top": …, "front": …} is refused (front is not wrist), and so is a name with two views (top_wrist). Two cameras it cannot place are refused too. Every refusal prints the exact --rename_map to pass — check that each one really is that view, and read the cameras: line the plan prints. agi-diy 0.4.8 keeps only exact names and maps the rest by position, and 0.4.7 and earlier map purely by position: both can swap the two views silently. agi --version says which one you have.

agi rollout --dry-run composes the full lerobot-rollout command from the public model list without a key and without moving anything; --fake runs the loop with a hardware-free robot and black frames (calls are still billed). See Your SO-101.

Glossary#

The product's vocabulary, generated from web/lib/vocab.ts. The same list as data: /docs/concepts/glossary.json.

termmeansin the API
policyA trained model that maps what the robot sees and feels to what it should do next. agi.diy serves two today: ACT and SmolVLA, both for SO-101.
modelA policy family bound to one robot, named family/robot, e.g. act/so101. The id you pass to POST /v1/sessions.
checkpointThe exact trained weights behind a model, pinned by Hub repo and revision. Task-specific: each was trained for one kind of task in one kind of scene.
VLAVision-language-action policy: images, joint state and a task sentence in, actions out. SmolVLA is one. It was fine-tuned for one task, so a task sentence is not general instruction following.
world foundation modelA model that predicts how a scene will evolve. Platform direction only: no WFM endpoint is served today.WFM
observationOne snapshot sent by the client: six joint values, two camera images and, for SmolVLA, the task text.
featureOne named field of an observation, with a fixed shape and dtype, advertised in the lease.observation.state, observation.images.<camera>
camera nameThe short name of an image feature. ACT expects top + wrist; SmolVLA expects front + wrist; each 640×480 RGB.top · front · wrist
actionOne returned time step: six target joint values for one tick of the control loop. The billable unit.
chunkThe sequence of actions one call returns: n rows × 6 joints, float32. The robot plays it back at the model's fps.action chunk
open loopPlaying actions without new observations reaching the policy: what the arm does with the rest of its queue when the link drops (at most one chunk).
starvation graceHow long LeRobot holds the arm on an empty queue before it ends the run: 3 s with agi rollout, 1 s by LeRobot's default.--inference.action_starvation_grace_s
rename mapLeRobot's mapping from your camera names to the model's (observation.images.<yours> → observation.images.<model's>). agi rollout builds it; check the cameras: line it prints.--rename_map
horizonRows per chunk: ACT 100, SmolVLA 50. Fixed at lease time.action_horizon
fpsThe rate the chunk is meant to be executed at: 30 actions per second, so 100 rows ≈ 3.3 s of motion and 50 ≈ 1.7 s. Not a request rate.
callOne observation sent, one chunk returned.
sessionOne lease on one warm policy process: groups calls, carries the bill and the trace.ses_…
leaseWhat POST /v1/sessions does: reserves a ready slot for you until the session ends.
slotOne warm policy process on a runner. A model is Ready when it has a free slot, Busy when all are leased.
runnerThe GPU machine that hosts slots and reports them every second (today: one GPU runner).
deployment nameThe private address of your slot, handed out in the lease and retired when the session ends.d_…
custom checkpointA checkpoint of your own on the Hugging Face Hub, leased by passing it as the model. The hf: prefix marks it as yours; act, smolvla or diffusion, safetensors only, at most 3 GB. Validated before anything is charged, and the commit is pinned.hf:<org>/<repo>[@<revision>]
dynamic deploymentThe slot a custom checkpoint gets: brought up for your session rather than kept warm in the pool, which is why it starts warming and why there is a per-account limit on live ones.
tierWhich capacity a model is served from: default (our act and smolvla), flagship (larger models, evaluated before they are listed) or custom (your own checkpoint). It describes where the slot comes from, not how good the policy is.tier
tokenBilling unit. 1 token = 1 returned action = $0.0001. Not an LLM text token.
usage creditYour token balance shown in dollars: 200,000 tokens = $20, granted once to new accounts. Not cash, not withdrawable. A price may be written "$0.01 credit equivalent" (accepted, used in docs and dashboard).
normalized unitsLeRobot's default joint scale: arm joints −100…100 over the calibrated range, gripper 0…100. Both live checkpoints use it, although the session label says degrees. On a real arm this means LeRobot use_degrees=false (its current default is true).
semanticsThe units label carried in the lease. Known mismatch: today it says degrees; the data is normalized units.so101-degrees-gripper-percent-v1
policy timeHow long the model's forward pass took for one call.trace server_ms.policy
time on the runnerFrom the observation reaching the runner to the chunk leaving it: policy time plus decoding and post-processing. Excludes your network. The trace's rtt_ms is this number.trace rtt_ms
round tripTime from sending an observation to holding the chunk, as your client measures it: time on the runner plus the network both ways. Not recorded in the trace.
traceThe recording of a session: every observation (state, task, camera frames) and every returned chunk, with timing.
playbackViewing a recorded trace. Free; nothing runs.
run inference againFeeds a trace's recorded observations to a policy in a new session. Billed like any session; no robot moves; output may differ.POST /v1/sessions/:id/replay
public relayThe TCP tunnel that puts the runner's Zenoh endpoint on the public internet; the lease names it (tcp/:) and it may change mid-session, so re-read the session instead of hard-coding it.
browser wireThe WebSocket data plane a page uses instead of Zenoh: signed URL from GET /v1/sessions/:id/wire, JPEG observations in, chunks out, billed identically. Open within 120 s, 8 MB a message, an observation at least every 120 s.agi-wire/1
simulationThe browser SO-101 at /play: MuJoCo physics, rendered camera images, real policy calls. Simulated inputs, live policy output.
test callOne billed call with synthetic inputs (black images, synthetic state). Tests the connection and inference, not task success.agi run <model> --probe -n 1

Say this, not that#

The words to avoid, from the same avoid[] lists in web/lib/vocab.ts that the drift guard reads.

Today the build refuses 12 phrasings outright; the other 33 are review rules — a row with a context names the one place the word is wrong, because elsewhere it is fine ("no robot moves" is not a billing unit).

not thissay thiswhen it matters
AI agentpolicyalways — kept by review, not yet by the build
brainpolicyalways — kept by review, not yet by the build
input packetobservationalways — kept by review, not yet by the build
one jointactionalways — kept by review, not yet by the build
our serverspublic relayalways — kept by review, not yet by the build
re-run for freerun inference againalways — kept by review, not yet by the build
streaming APIbrowser wirealways — kept by review, not yet by the build
trajectorychunkalways — kept by review, not yet by the build
try the robottest callalways — kept by review, not yet by the build
websocket APIbrowser wirealways — kept by review, not yet by the build
autonomousopen loopfor this
balanceusage creditwhen it means alone, without tokens
batchchunkfor a chunk
containerdynamic deploymentfor a dynamic deployment
credittokenfor one token
degreesnormalized unitsfor these checkpoints
demosimulationfor /play
demotest callfor a billed test call
frameobservationfor the whole snapshot
inference runcallfor one call
instancedynamic deploymentfor a dynamic deployment
latencyround tripwhen it stands unqualified
moveactionfor one action
plantierfor a tier
real camerasimulationfor /play's rendered images
replayrun inference againin UI copy, unqualified
requestcallin billing copy
round triptime on the runnerfor trace rtt_ms
stepactionwhen it stands alone
subscriptiontierwhen it means there is none — you pay per action
uploadcustom checkpointwhen it means nothing is uploaded — we read the Hub
your modelcustom checkpointfor one of ours
zenoh.api.agi.diypublic relayas the endpoint to connect to — the lease names a tcp/ relay

12 further phrasings are refused outright by the build and are not quoted here — quoting one would fail the guard that bans it. They live in DEPRECATED in web/lib/vocab.ts (2 of the rows above would have repeated them).

Where you meet these words#

Every term above is printed somewhere in the product. This is where to look, so a definition can be checked against the thing itself rather than taken on faith.

The wordWhere it is on screenWhat to compare
token, credit/dashboard — the spend figuresthe account total against GET /v1/usage
call, chunk/dashboard → a session → its traceone act event per call, its rows the chunk
actionthe same trace, inside a chunk50 or 100 rows of 6 numbers, one row per action
normalized unitsthe trace's state and action rowsarm values inside -100…100, gripper 0…100
observation, camerathe trace's obs eventsyour two frames and state[6], as the policy got them
session, end reason/dashboard/sessionsthe state and end_reason of every run
latency, rtta session's own readoutround trip against server_ms.policy
model, checkpoint/modelsthe pinned Hub revision and licence
slot, pool/statusfree slots now, per model
simulation/playthe arm and cameras rendered in your browser

Where to go next#

  • /docs — the reference: every route, field, error code and limit, with runnable examples.
  • /play — a simulated SO-101 driven by the live policies; costs usage credit like any call.
  • /models — one page per model: inputs, price, measured times, licences, availability.
  • /dashboard — your keys, sessions, spend and traces.
  • /docs/concepts/glossary.json — this vocabulary as data, for agents and writers.