<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://waltstephen.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://waltstephen.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-10-30T01:59:23+00:00</updated><id>https://waltstephen.github.io/feed.xml</id><title type="html">blank</title><subtitle>Yijia Fan&apos;s personal website. Undergraduate student in Computer Science at Sun Yat-sen University. Research interests: Visual Language Models, Multi-modal Intelligence, Reinforcement Learning. </subtitle><entry><title type="html">CaPtain: betting on code as the action space</title><link href="https://waltstephen.github.io/blog/2026/captain/" rel="alternate" type="text/html" title="CaPtain: betting on code as the action space"/><published>2026-10-28T04:00:00+00:00</published><updated>2026-10-28T04:00:00+00:00</updated><id>https://waltstephen.github.io/blog/2026/captain</id><content type="html" xml:base="https://waltstephen.github.io/blog/2026/captain/"><![CDATA[<p><em>Yijia Fan, Xilin He, Yilei Jiang, Dongming Wu, Jiaxu Wang, Xiangyu Yue · MMLab, CUHK · <a href="/captain/">project page</a> · <a href="/captain/assets/paper/CaPtain.pdf">paper</a> · <a href="https://github.com/waltstephen/CaPtain">code</a> · <a href="https://huggingface.co/collections/YijiaFan/captain">models &amp; data</a></em></p> <style>.post figure{margin:1.5rem 0}.post figure img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:4px}.post figcaption{font-size:.9rem;opacity:.8;margin-top:.4rem}.post pre{white-space:pre;overflow-x:auto}.post .tldr{border-left:3px solid #e8562a;padding:.2rem 0 .2rem 1rem;margin:1.2rem 0}</style> <div class="tldr"> <p><b>TL;DR.</b> We think the best way to turn a vision-language model into a robot policy is to let it write code against low-level perception and motion primitives, and to train it like any other agent: SFT, then RL. CaPtain is the pipeline that does this for a 4B open model. It takes Qwen3.5-4B from 0% to 73.8% on held-out LIBERO, reaches 78.5% on LIBERO-PRO, and carries over without further training to RoboTwin 2.0, SimplerEnv, RoboDojo, CaP-Bench and a real arm. Everything is open.</p> </div> <p>This post is less about the numbers and more about how we got there: the bet we made, the things that broke, and what we believe now that we did not believe a year ago.</p> <h2 id="bet">The bet</h2> <p>The project started with an observation that is hard to unsee. Used directly as a robot policy, GPT-6 Astra ranks above every public entry on the RoboDojo leaderboard. Given a coding harness, it solves 94.0% of LIBERO-PRO episodes. None of these models were trained to output motor commands. What they do is write programs: look at the scene, call a segmenter, measure a distance, move the gripper under a force limit, check the result, try again.</p> <p>The way we read this: a large vision-language model already has a surprisingly good model of space. It knows that the bowl is left of the plate, that a drawer opens along its handle's normal, that a cube in a closed gripper moves with the gripper. What it lacks is a channel through which that knowledge can reach the motors. Code is such a channel. It turns spatial understanding into explicit, composable, checkable operations, and it does so in the representation the model is best at: language.</p> <p>Compare the two dominant alternatives.</p> <ul> <li><b>Vision-language-action models</b> ask the network to predict continuous, high-dimensional action trajectories directly. This is a hard regression problem, it is tied to one embodiment's action space, and the data it needs (teleoperated demonstrations) does not scale the way text and images do. Every new robot is, to a first approximation, a new dataset.</li> <li><b>World-action models</b> predict future frames and actions jointly. Pixels are an expensive and indirect proxy for physics: two frames can look nearly identical while the contact state, the one thing that matters for a grasp, is completely different. Learning physical law through video prediction is possible, but it is learning it the long way round.</li> </ul> <p>Language is different. It is a dense, compositional representation that already encodes most of the relations a manipulation task needs, and a program built from it is grounded at every step by the robot's own feedback. Our hot take: for manipulation that requires reasoning, code is the right action space, and the hard part is not the representation but training a small model to use it.</p> <p>That last part is the problem. Every strong code-as-policy (CaP) agent today runs on a closed frontier model. You cannot fine-tune it, you cannot put it on the robot, and at \$8–22 per episode for the best open-harness systems you cannot run it at scale. An off-the-shelf 4B model placed in the same harness solves <i>nothing</i>: 0.0% on LIBERO. So the question became: can we post-train a small open model into a CaP agent, the same way coding and tool-use agents are post-trained, and what does it take?</p> <h2 id="harness">Decision 1: no skills, no VLA</h2> <p>The first decision was what the agent is allowed to call. It is tempting to give it <code>grasp()</code>, <code>place()</code>, <code>open_drawer()</code>, or a trained VLA as a skill. Benchmarks get much easier. But each of these moves the hard part of the problem out of the policy and into hand-written code or a separately trained network that has to be rebuilt for every robot. A VLA as a skill also brings back exactly the scaling problem we were trying to avoid.</p> <p>So CaPtain-Harness exposes 22 low-level capabilities and nothing else: perceive (<code>observe</code>, <code>segment</code>, <code>point</code>, <code>mark_point</code>, <code>track_target</code>), measure (object and contact geometry, observation comparisons), act (<code>move_to</code>, <code>hold</code>, <code>recover</code>, <code>release</code>), and check (motion previews, robot state). Down to IK-level motion control, every tool is a primitive. A grasp is something the policy has to compose: measure the object, choose an approach, descend under a force guard, close, verify.</p> <p>The second design choice was the <b>receipt</b>. Robot calls never raise. Each returns a structured result, e.g. <i>"stopped: contact · moved 0.12 m · peak 6.2 N · jaw 8 → 1 cm"</i>. A small model cannot recover from a failure it cannot see, and, as it turned out, much of what RL improves is how the policy reads these receipts.</p> <p>The payoff of this decision came later: the same API runs on LIBERO, RoboTwin 2.0, RoboDojo, SimplerEnv, CaP-Bench and a real robot. Only the backend underneath changes.</p> <figure> <img src="/assets/img/blog/captain/fig1_harness.jpg" alt="Overview of CaPtain"/> <figcaption>Figure 1. Top: inference in CaPtain-Harness. Bottom: SFT, then reinforcement training with a simulator critic.</figcaption> </figure> <p>Here is a piece of an episode the trained 4B model wrote (verbatim code, receipts shortened):</p> <pre><code># request 6 (after the grasp): lift, re-observe, re-identify bowl and plate, measure
lift = go(np.array(obs2["value"]["robot_state"]["end_effector_position_m"]) + [0, 0, .12], q,
          speed=0.035, mode="manipulation")
obs3 = robot.observe()
b2 = robot.mark_point(obs3["value"]["observation_handle"], "agentview", .75, .421, label="held black bowl")
p2 = robot.mark_point(obs3["value"]["observation_handle"], "agentview", .78, .725, label="plate")
g2 = robot.measure_object_geometry(b2["value"]["detection_id"], p2["value"]["detection_id"])
&gt;&gt;&gt; go: True none bounded Cartesian sweep reached its endpoint

# request 7: carry the bowl by the measured object-to-destination offset
transfer = np.array(obs3["value"]["robot_state"]["end_effector_position_m"])
transfer[:2] -= np.array(g2["value"]["object_to_destination_xy_offset_m"])
trans = go(transfer, q, mode="manipulation")
&gt;&gt;&gt; move_to: ok; stopped_reason=target_reached executed_distance_m=0.1681 max_observed_force_n=5.198

# request 13: fresh final measurement, then finish
&gt;&gt;&gt; {'object_to_destination_xy_offset_m': [0.0012, 0.0170], 'xy_distance_m': 0.0171, ...}
"The bowl is resting on the plate. Fresh measurements show a center offset of about 1.7 cm,
 within the required 2 cm tolerance."  -&gt; robot.finish(...)</code></pre> <p>Nothing here is a skill. The model clicks pixels to ground objects, measures, moves by the measured offset, and checks the final relation before it declares success. This is what we mean by letting spatial understanding reach the motors.</p> <h2 id="blind">Lesson 1: the model stopped looking</h2> <p>SFT on synthetic trajectories, generated by GPT-6 Astra in the same harness, gets a small model off the ground. Our first SFT models looked fine on held-out states. They were quietly broken.</p> <p>We noticed when we replaced the input image with a different scene and the likelihood of the predicted pixel coordinates did not change. The model was not reading the image. It had learned to output coordinates from the task identity alone. The cause was the data: most episodes started from LIBERO's official initial states, where each object sits in nearly the same place every time (per-object position standard deviation about 1.4 cm). Under that distribution, memorising the layout is a perfectly good solution, and gradient descent found it.</p> <p>The fix was in the data, not the model. We regenerated data from wide-layout start states (standard deviation about 15 cm), mixed all task suites, randomised lighting, camera pose and materials during RL, and masked earlier <code>mark_point</code> coordinates in the history with probability 0.2, so every new call has to be grounded in the current image. That single change is also why the policy later works on a real robot.</p> <p>The general point is worth stating plainly: visual grounding in a fine-tuned VLM is fragile. It is inherited from pretraining, and it is erased by any data distribution in which the image is not needed to predict the label. For embodied SFT, layout diversity matters more than data volume.</p> <h2 id="noise">Lesson 2: RL that goes nowhere</h2> <p>With a working SFT model, we started RL. It did not learn. Success rates drifted up and down within noise, and nothing in the pipeline was wrong.</p> <p>The diagnosis came from looking at the updates rather than the rewards. Consecutive updates were nearly orthogonal, and the distance of the weights from the start grew like the square root of the number of steps. The policy was doing a random walk.</p> <p>The reason is batch size. We measured the gradient-noise scale \(B_{\text{noise}}\) of the 4B policy: about 9,500 episodes (95% lower bound 3,300). An update from 256 episodes is mostly noise. Accumulating over \(K = 4\) rounds, 1,024 episodes per optimizer step, fixed it. In hindsight this is obvious; multi-turn episodes with one binary outcome are about the noisiest training signal there is. If we could give one piece of advice to anyone doing RL on multi-turn agents: measure \(B_{\text{noise}}\) before you tune anything else.</p> <h2 id="critic">Lesson 3: credit lives where physics changes</h2> <p>Bigger batches make the gradient point somewhere; they do not make it point at the right requests. A LIBERO episode has 20–60 requests and one outcome. Most requests look at the scene or measure something. A handful, the grasp, the carry, the release, decide the episode. An outcome reward spreads the same signal over all of them.</p> <p>The insight that unlocked this was simple: <b>the simulator already knows what the policy is trying to infer.</b> It knows every object pose, whether something is in the gripper, and how many sub-goals are satisfied. A policy should not see this, because it will not have it at test time. But a critic can.</p> <p>So we train a small critic, an ensemble of five MLPs over 36 physical features, to predict the probability of success from the simulator's state, and use the change in that prediction as each request's credit:</p> \[\delta_t \;=\; c_t \;+\; V_\phi(s_{t+1},\,t) \;-\; V_\phi(s_t,\,t), \qquad \delta_T \;=\; c_T + R - V_\phi(s_T,\,T),\] <p>accumulated with GAE (\(\lambda = 0.9\)) and combined with a group-relative outcome term. The critic is co-trained online, as in SAO, and disappears at test time.</p> <p>Physical state only changes through physical action. A request that only perceives, however many tokens it produces, gets almost no one-step credit, and receives its share through GAE from the action it enables. In an offline study on 15k episodes, the critic predicts final success with AUC 0.970. Successful grasps get \(\Delta V = +0.058\), failed grasps \(-0.0075\), perception-only requests \(-0.0002\). 65% of the advantage mass goes to moves, 14% to grasps, 14% to releases, and 1.3% to perception. This is what credit assignment should look like.</p> <p>Two things we got wrong first, both instructive:</p> <ul> <li><b>Time leaks into credit.</b> The critic needs the request index \(t\) to be calibrated: half a task done at request 55 is worse than at request 5. But if \(t\) advances inside the difference, the passage of time alone produces credit. We hold \(t\) fixed within each difference.</li> <li><b>Anything about the policy is a clock.</b> Adding counters of the policy's own calls made the critic slightly more accurate and pulled credit onto perception requests. The policy's LM hidden states were worse: they decode the request index with \(R^2 = 0.80\), and used as critic inputs they moved 35% of the advantage mass onto perception. The critic should see the world, not the policy.</li> </ul> <h2 id="stop">Lesson 4: knowing when to stop</h2> <p>We report two numbers throughout. <i>Host success</i> is the benchmark's goal predicate on the final state. <i>Strict success</i> additionally requires the agent to call <code>finish()</code> itself. On a robot, only the second counts: an agent that never says it is done cannot be deployed.</p> <p>Late in training the two come apart. After about round 340, host success holds while strict success falls from about 60% to 44%. The policy still solves the task; it keeps re-measuring a scene that is already solved until it runs out of requests. The reason is structural: the critic is trained on host success, so every post-success state has \(V \approx 1\), and the decision to stop gets no credit from it at all. We use the round-300 checkpoint as CaPtain-4B. The real fix is to give termination its own target, and it is the first thing we will change.</p> <figure> <img src="/assets/img/blog/captain/collapse.jpg" alt="Host and strict success over RL rounds" style="max-width:520px;display:block;margin:0 auto"/> <figcaption>Figure 2. Host and strict success during RL. After about round 340, strict success falls while host success stays flat.</figcaption> </figure> <h2 id="results">Where it landed</h2> <ul> <li><b>LIBERO</b> (held-out initial states): base 0.0% → SFT 55.4% → RL <b>73.8%</b>.</li> <li><b>Perturbations</b>: RL adds <b>+19.0</b> on LIBERO-PRO Pos+Task (to 78.5), <b>+17.6</b> on LIBERO-Plus, <b>+21.2</b> and <b>+18.8</b> on LIBERO-MAX Dynamic and Base. The gains are not memorisation of training layouts.</li> <li><b>New tasks and embodiments, no further training</b>: RoboTwin 2.0 (official Hard protocol) 22.6%, above X-VLA (20.9%) and Xiaomi-Robotics-0 (18.2%); SimplerEnv WidowX 83.3%; CaP-Bench 75.0, above CaP-X's multi-turn agents on Gemini-3-Pro (73.5) and GPT-5.2 (55.5); RoboDojo Score 1.7.</li> <li><b>A real arm</b>: after adaptation on episodes from a coarse simulation of an XR1 robot, with no real-world data, the same API and receipts drive the real arm.</li> </ul> <figure> <img src="/assets/img/blog/captain/sft_vs_rl.jpg" alt="SFT vs RL host and strict success on LIBERO and four perturbation suites"/> <figcaption>Figure 3. SFT (grey) vs. RL (orange) on held-out LIBERO and four perturbation suites. Left: host success. Right: strict success.</figcaption> </figure> <figure> <img src="/assets/img/blog/captain/real_robot.jpg" alt="Two real-robot episodes"/> <figcaption>Figure 4. Two real-robot episodes. In (b), "put the cube that has the color of the sky into the container that is not round", the agent has to resolve both references from the image before it acts.</figcaption> </figure> <p>The transfer results are the part we find most telling. The policy was trained with RL on 40 LIBERO tasks with one arm. It runs on new robots because nothing it learned is tied to an action space: it learned to look, measure, move and check. RoboDojo marks the next frontier: tasks that need two arms at once, handovers or memory.</p> <h2 id="beliefs">What we believe now</h2> <ol> <li><b>Code is an underrated action space.</b> A VLM's spatial understanding is real but locked behind its output format. Code unlocks it, and the resulting policy is portable across embodiments for free, because the embodiment lives below the API.</li> <li><b>Robot agents should be post-trained like every other agent.</b> Fixed harness, SFT for a cold start, RL for the rest. There is nothing special about manipulation that requires a different recipe; there are just different failure modes.</li> <li><b>The interface is the ceiling.</b> What the API cannot express, the policy cannot learn. Sustained pressing (CaP-Bench Wipe) is impossible with guarded motion primitives, no matter how good the model. Designing the primitives is designing the hypothesis space.</li> <li><b>Diversity beats volume, and grounding is fragile.</b> A fine-tuned VLM will stop looking at the image the moment the image stops being necessary.</li> <li><b>Multi-turn RL is a batch-size problem before it is an algorithm problem.</b> Measure the noise scale first.</li> <li><b>Privileged information belongs in the critic.</b> The simulator is not only an environment; it is a teacher of credit. The policy stays deployable because it never sees what the critic sees.</li> </ol> <h2 id="open">Open questions</h2> <ul> <li><b>Termination.</b> Stopping needs its own credit target; host success is the wrong label for the decision to finish.</li> <li><b>Two arms.</b> A critic over joint two-arm state is the obvious next step.</li> <li><b>Off the simulator.</b> The critic needs object poses a real robot does not have. With 2 cm pose noise, an estimated-state critic agrees with the simulator critic at only 0.49 correlation, so RL on hardware needs better state estimation.</li> <li><b>More training worlds.</b> RL saw only LIBERO. How far does transfer go when RL itself sees many embodiments?</li> </ul> <h2 id="release">Release</h2> <p>Everything is open under MIT: <a href="https://github.com/waltstephen/CaPtain">the training code</a>, <a href="https://github.com/waltstephen/CaPtain-Harness">CaPtain-Harness</a> with adapters for every benchmark above, <a href="https://huggingface.co/datasets/YijiaFan/CaPtain-SFT-Data">the SFT data</a>, and the <a href="https://huggingface.co/YijiaFan/CaPtain-4B">CaPtain-4B</a> and <a href="https://huggingface.co/YijiaFan/CaPtain-4B-SFT">CaPtain-4B-SFT</a> weights. Train your own model on our pipeline, or test yours in our harness. The <a href="https://youtu.be/KMy-Bx5HXsg">video</a> walks through the whole pipeline.</p> <hr/> <h2 id="refs">References</h2> <ol class="refs"> <li>Liang et al. Code as Policies: Language Model Programs for Embodied Control. ICRA 2023.</li> <li>Liu et al. LIBERO: Benchmarking Knowledge Transfer for Lifelong Robot Learning. NeurIPS 2023.</li> <li>Zhou et al. LIBERO-PRO: Towards Robust and Fair Evaluation of Vision-Language-Action Models Beyond Memorization. 2025.</li> <li>Fei et al. LIBERO-Plus: In-depth Robustness Analysis of Vision-Language-Action Models. 2025.</li> <li>Zhang et al. LIBERO-MAX: Do Robot Policies Adapt When the World Changes? 2026.</li> <li>Chen et al. RoboTwin 2.0: A Scalable Data Generator and Benchmark with Strong Domain Randomization for Robust Bimanual Robotic Manipulation. 2025.</li> <li>Chen et al. RoboDojo: A Unified Sim-and-Real Benchmark for Comprehensive Evaluation of Generalist Robot Manipulation Policies. 2026.</li> <li>Fu et al. CaP-X: A Framework for Benchmarking and Improving Coding Agents for Robot Manipulation. 2026.</li> <li>Hou et al. Single-Rollout Asynchronous Optimization for Agentic Reinforcement Learning (SAO). 2026.</li> <li>Pinto et al. Asymmetric Actor Critic for Image-Based Robot Learning. RSS 2018.</li> <li>Schulman et al. High-Dimensional Continuous Control Using Generalized Advantage Estimation. ICLR 2016.</li> <li>McCandlish et al. An Empirical Model of Large-Batch Training. 2018.</li> </ol> <h2 id="citation">Citation</h2> <pre><code>@misc{fan2026captain,
  title  = {CaPtain: A Pipeline for Post-Training Code-as-Policy Robot Agents},
  author = {Fan, Yijia and He, Xilin and Jiang, Yilei and Wu, Dongming and Wang, Jiaxu and Yue, Xiangyu},
  year   = {2026},
  note   = {Blog post}
}</code></pre>]]></content><author><name></name></author><category term="research"/><category term="robotics"/><category term="code-as-policy"/><category term="reinforcement-learning"/><summary type="html"><![CDATA[Why we think code is the right interface between a vision-language model and a robot, and what it took to post-train a 4B model to use it.]]></summary></entry></feed>