agi.diy

Reference · look things up, do not read through

Flywheel.

Every run becomes a LeRobot v3.0 episode in the format of the teleop data: POST …/export, /play's upload of each turn, agi rollout --record, the schema contract. The four-minute read is /docs.

agi.diy serves robot policies over an API. The flywheel turns the runs back into a dataset: one call after a session ends, and its recording is one LeRobot v3.0 episode in a Hugging Face dataset — the same format as the teleop data the policy trained on, with a success label. Train on it, and the next checkpoint has seen its own mistakes.

curl -s -X POST https://api.agi.diy/v1/sessions/ses_…/export -H "authorization: Bearer $AGI_API_KEY" \
  -H 'content-type: application/json' -d '{"label":"success","task":"pick up the x piece and place it on the center cell"}'
# 201 {"session":"ses_…","dataset":"cagataydev/so101-ttt-flywheel-sim","episode_index":17,"episodes":18,"frames":283,"seconds":9.43,
#      "fps":30,"label":"success","source":"sim","commit":"5e4698aa…","url":"https://huggingface.co/datasets/cagataydev/so101-ttt-flywheel-sim"}

/play does this after every judged attempt: the move log reads episode N saved with the dataset link.

The format — one contract, tested

The reference is cagataydev/so101-tic-tac-toe, the 210-episode teleop dataset recorded on the real SO-101 (30 fps, lerobot 0.6.2). Every flywheel episode is written by lerobot's own LeRobotDataset at the same commit, with the same features:

featuredtypeshapeunits
observation.state, actionfloat32[6] — shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripperdegrees; gripper 0–100 %
observation.images.top, observation.images.wristvideo480×640×3AV1 (libsvtav1, crf 30, g 2, yuv420p), 30 fps
timestamp, frame_index, episode_index, index, task_indexfloat32 / int64[1]—

The test that says so is agi flywheel check <reference root> <flywheel root> (agi-diy ≥ 0.5.11, or python -m agi.flywheel check): it compares meta/info.json feature by feature, the parquet column schema and the codec parameters of one video per camera, and exits non-zero on any difference. Frame content differs by construction — see the two sources below.

The wire speaks so101-normalized-v1 (arm −100…100, gripper 0…100); the dataset is degrees, converted with the per-joint scale of the calibration the reference was recorded with (blue_follower: pan 1.0927, lift 0.9685, elbow 1.0336, wrist 0.9913, roll 0.5556 normalized units per degree; gripper unchanged). A dataset in one unit, always.

Two sources, said on every episode

meta/flywheel.jsonl carries one row per episode: session, model, checkpoint, label, judge, source, and three fields that say how the data was obtained — read them before you train.

source: sim — POST …/export of a traced sessionsource: real — agi rollout … --record
actionsthe executed trajectory: the real-time-chunking splice replayed from the recording (chunk k covers rows [cursor_k, cursor_k + n), the latest chunk that had arrived rules each row; holds are not rows), then the policy's 15 Hz upsampled to 30 fps by linear interpolation. cursor_source: recorded when the client sent its cursor, estimated (matched to the measured state) on older traceswhat the arm was commanded, read by LeRobot at 30 Hz
observation.statestate: reconstructed — the target one tick earlier, corrected to the measured state at every observation (exact there)state: measured — the arm's joints at 30 Hz
framesframes: held — the frames the policy saw, one per call (~1 a second), each repeated until the next callframes: measured — both cameras at 30 fps
labelthe caller's: /play passes its physics judge's verdict (judge: ttt-sim-judge-v1/browser)yours: --success, --failure, or the y/n prompt at the end

A sim episode is a rendered simulation, not a measurement of the real arm; its in-between frames are the last frame the policy saw. Say so wherever a number from it appears.

POST /v1/sessions/:id/export

Owner cookie or the key that leased the session; the session must have ended (409 session_live otherwise — the episode is the whole recording). Body, every field optional:

fielddefaultmeaning
labelnull"success", "failure" or null (unlabeled: kept, never trains)
taskthe trace'sthe task string of the episode (one per episode; /play passes the cell it asked for)
source"sim""sim" or "real" — picks the default dataset and is stamped on the episode
datasetcagataydev/so101-ttt-flywheel-sim / -realany <org>/<repo>; your own dataset needs your hf_token (write access; forwarded to the runner once, never stored)
judge—who labelled it, ≤ 64 chars
privatetruethe dataset's visibility when it is created

Answer 201 {session, model, dataset, episode_index, episodes, frames, seconds, fps, task, label, source, commit, url}. Errors: 404 no_trace (nothing recorded, record:false, or older than 24 h — export within a day), 409 export_running, 502 export_failed (the runner's one-line reason, e.g. the trace carries no task string), 504 export_timeout. Exports into one dataset run in series on the runner; a dataset written from elsewhere since the last export is re-read from the Hub first, never overwritten. One export is 10–20 s (AV1 encoding of two 30 fps videos, then the push).

Your own arm, your own dataset

agi rollout agi-wfm/so101-ttt --record my-org/so101-ttt-rollouts --dry-run -- \
  --robot.type=so101_follower --robot.port=/dev/tty.usbmodem… --robot.cameras='{"front":{…},"wrist":{…}}' --task="pick up the x piece and place it on the center cell"

--dry-run prints the composed command and leases nothing; remove it to run. --record <org>/<repo> runs LeRobot's episodic rollout strategy under the lease: one episode per run, both cameras and the joint reads at the true 30 Hz, the task string, then the push to your dataset (private). Label it with agi flywheel label <org>/<repo> --episode N --success|--failure, which writes the same provenance row (source: real, frames: measured, state: measured). The writer is the same lerobot commit the runner uses, so agi flywheel check passes against the reference by construction.

Train on it

A flywheel dataset loads like any LeRobot dataset — LeRobotDataset("cagataydev/so101-ttt-flywheel-sim") — and lerobot-train reads it unchanged. Train on label == success rows of meta/flywheel.jsonl only; failures are kept for a critic, never for imitation. Our own retraining loop (what is mixed, how a round is scored and promoted) is agi-wfm's docs/FLYWHEEL.md.