Own the run-loop
The kit's deploy client
Calling the model returned a stream of action chunks. Turning that stream into an arm that moves is a loop: read the robot's state, ask the model what to do next, drive the arm on the answer, read again. The kit ships that loop — and you're free to rewrite it.
The loop the kit ships
tools/apps/deploy_policy.py is the kit's deploy client. Stripped to its spine, every tick does four things:
while running:
state = read_calibrated(follower) # 6 joint angles
frames = observe_cameras(cameras) # one JPEG each: top, side
chunk = query_actions(server, task, state, frames) # an (N, 6) action chunk
for action in chunk[:execute_steps]: # execute part of the plan
target = clip(action, current - cap, current + cap) # never lunge past the cap
drive_to(follower, target)
# re-observe, re-infer, repeat
Read state, infer, move, repeat. The two camera names are top and side — the exact two your episodes were recorded with — and the state is the six joint angles, so the observation lines up with what your model trained on.
Run it
Always dry-run a new checkpoint first: it streams the model's predictions to the viewer while the arm stays still.
pixi run deploy-policy -- --task "put the red cube in the bowl" --dry-run
Drop --dry-run to close the loop for real. --task is the same kind of instruction you typed while recording, and --server points at the inference server holding your policy.
pixi run deploy-policy -- --task "put the red cube in the bowl"
Stopping the arm is your code's job
Nothing stops the arm but your code
This is the first step with a robot actually in motion, and nothing in the API stops the arm for you. The kit's client owns it: Ctrl-C releases torque, and every step is clamped to --max-step-deg per joint per tick, so one bad prediction can't slam the arm. Whatever loop you write inherits that responsibility.
How much of a chunk you trust
The model doesn't return one move — it returns a plan. An SO-101 fine-tune answers with thirty steps, but the kit's client executes only the first execute_steps of them (eight, by default) before re-observing and asking again. A chunk gets staler than it's worth the longer you play it against an observation that has already gone by. That eight is the kit's harness decision, sitting in deploy_policy.py — not a number the model hides from you. Your own loop can pick differently.
Write your own
The kit's client is one harness. The newt SDK gives you the same loop as two callbacks and a run() that drives them — the shape to rewrite when you want your own logic between "chunk arrived" and "chunk applied":
import newt
def read_state():
return {"state": current_joints(), "images": {"top": top_frame(), "side": side_frame()}}
def execute(chunk):
for action in chunk: # gate, clip, or truncate however you like
drive(action)
robot = newt.Robot(read_state=read_state, execute=execute, model="so101")
robot.run("put the red cube in the bowl", max_duration=10.0)
run() calls read_state, infers, and hands each chunk to execute in a loop until the task ends or the clock runs out. Everything between the two callbacks is yours: read a camera in read_state and abort when what you see is wrong, refuse a chunk that would push a joint past a bound, or play only the first few steps and re-infer. Chaining tasks is the same move twice — run() returns when one instruction finishes, so call it again with the next.
Run it yourself
→ The newt SDK — the full read_state/execute callback and embodiment contract — with two clocks for why the model returns a plan instead of one move, and WebSocket vs HTTP if you're driving the model from your own client instead of the SDK.
You've made the loop yours. The last chapter is where you find out if it's any good.