CaPtain: A Pipeline for Post-Training Code-as-Policy Robot Agents
Abstract
Across coding, web, and tool-use tasks, language-model agents are now routinely trained with the same recipe: supervised fine-tuning (SFT) to learn the interface, then multi-turn reinforcement learning (RL) to master it. Code-as-policy (CaP) casts robot manipulation in the same form, as the policy writes code against robot APIs and reads back the results, so the same recipe should apply. Yet existing CaP agents sidestep training: they either prompt closed frontier models or delegate manipulation to pre-trained vision-language-action models (VLAs) and high-level skills, while small open vision-language models (VLMs) fail outright. We present CaPtain, an open post-training pipeline that turns a small open VLM into a capable CaP agent, with no VLA, frontier model, or privileged state at test time. CaPtain trains and deploys the policy inside CaPtain-Harness, which exposes low-level perception, geometry, and guarded-motion primitives across simulation and real hardware. We first align the base model via SFT on synthetic trajectories generated with GPT-6 Astra, then apply multi-turn RL. Multi-turn RL must credit tens of agent requests, whose physical effects often surface turns later, from a single end-of-episode outcome. We therefore introduce a physical critic that reads privileged simulator state, never seen by the policy, and credits each request by how much it changes the predicted chance of success. Starting from Qwen3.5-4B (0% zero-shot), CaPtain-4B reaches 55.4% held-out LIBERO success after SFT and 73.8% after RL. On LIBERO-PRO, it reaches 78.5%, surpassing several frontier-model agents, and its RL gains persist across three perturbation suites (+17.6 to +21.2 points). Without target-domain training, it attains 83.3% on SimplerEnv WidowX, exceeds frontier models on CaP-Bench, and runs on a real robot after simulation-only adaptation. We release all code, data, and model weights.
Figures & tables
| Host Success (%) | Strict Success (%) | |||||||
|---|---|---|---|---|---|---|---|---|
| Benchmark | Base | SFT | CaPtain-4B | SFT | CaPtain-4B | |||
| LIBERO | 240 | 0.0 | 55.4 | 73.8 | +18.3 | 52.1 | 67.5 | +15.4 |
| LIBERO-PRO (3 suites) | 600 | 0.3 | 59.5 | 78.5 | +19.0 | 56.7 | 71.5 | +14.8 |
| LIBERO-PRO (All 4 suites) | 800 | 0.2 | 49.2 | 68.6 | +19.4 | 45.1 | 58.9 | +13.8 |
| LIBERO-Plus | 700 | 0.0 | 48.6 | 66.1 | +17.6 | 44.6 | 59.0 | +14.4 |
| LIBERO-PRO (4 categories) | 160 | 0.0 | 48.8 | 68.8 | +20.0 | 48.1 | 60.6 | +12.5 |
| Frontier | VLA | In-Domain | LIBERO-PRO (%) | |||
| System | Policy Model | API | Skill | Data | 3 Suites | All 4 Suites |
| End-to-end VLAs & VLA-assisted agents | ||||||
| Harness VLA [ 94 ] | Claude Opus 4.7 + | ✓ | ✓ | ✓ | 87.7 | 82.4 |
| AgenticRoboRL [ 48 ] | Qwen3.5-9B + RL-tuned | ✗ | ✓ | ✓ | 81.3 | 71.0 |
| RLinf [ 94 ] | (RL fine-tuned) | ✗ | ✓ | ✓ | 56.2 | 50.0 |
| [ 97 ] | (SFT) | ✗ | ✓ | ✓ | 12.8 | 10.8 |
| Method | Type | All | 1-Arm |
|---|---|---|---|
| PyRUA-Lean Coder-4B [ 67 ] | CaP+VLA | 52.4 | – |
| OpenWAM- [ 76 ] | WAM | 48.7 | – |
| [ 55 ] | VLA | 46.0 | 45.8 |
| X-VLA [ 96 ] | VLA | 20.9 | – |
| Xiaomi-Robotics-0 [ 5 ] | VLA | 18.2 | – |
| [ 2 ] | VLA | 16.3 | 18.3 |
Appendix figures & tables15 assets
Supplementary material from the paper’s appendix.
Appendix
| Capability | Semantics |
|---|---|
| observe | head + wrist RGB-D ( ), calibration, robot state |
| segment | SAM 3 text segmentation (Molmo point fallback) depth-backed detections |
| select_detection | bind a chosen detection to identity evidence (read-only) |
| point | Molmo pointing deprojected, point-segmented evidence |
| mark_point | validate and back-project a model-chosen pixel detection id |
| track_target | optical-flow tracking of a detection into a newer observation |
| Source | Rows | Tokens | Share |
|---|---|---|---|
| Broad-coverage trajectories (code-normalized, filtered) | 7,903 | 348.9M | 40.5% |
| Hard-task targeted trajectories (Round 2) | 5,090 | 238.4M | 27.7% |
| Hard-task targeted trajectories (Round 1) | 2,138 | 99.3M | 11.5% |
| Hard-task trajectories (60-request cap) | 925 | 43.4M | 5.0% |
| Self-generated RL rollouts (successful episodes) | 1,386 (267 episodes) | 52.3M | 6.1% |
| Visual QA (loss weight 0.5) | 8,519 | 78.8M | 9.2% |
| Optimization | Effect |
|---|---|
| FlashInfer attention and a chunked-prefill budget of 8,192 tokens in vLLM | +17.6% policy throughput (30.4 35.7 requests per minute per GPU); log-probabilities within the kernel noise floor |
| Sensor artifacts in shared memory | observe latency 2.2 s 0.16 s |
| Harness fast loop and lazy rendering | episode wall time, bit-identical physics and responses |
| Critic-based early stopping (5% control slice never stopped) | 24–26% of episodes cut early; no successful episode in the control slice would have been cut |
| KL reference evaluated every 8 steps | unbiased KL estimate at of the reference cost |
| Pack-level rank assignment | balanced trainer ranks; identical log-probabilities, gradient cosine 0.9999 |
| Estimator | |
|---|---|
| GRPO (0/1 outcome) | 9.0k |
| simulator critic, | 12.6k |
| simulator critic, | 6.7k |
| simulator critic, | 6.5k |
| Failure Category | RoboTwin ( ) | RoboDojo ( ) |
| Bimanual coordination required | 167 | 1 |
| Contact / force guard triggered | 149 | 4 |
| Reach / inverse kinematics (IK) | 22 | 21 |
| Perception / viewpoint occlusion | 34 | 8 |
| Request cap / looping | 19 | – |
| Grasp failure (empty / slip) | 7 | 6 |
| Model | Host | Strict | Req. Cap | False Finish | Missed Finish | Spatial | Object | Goal | Long |
|---|---|---|---|---|---|---|---|---|---|
| CaPtain-4B -SFT | 133 | 125 | 96 | 17 | 8 | 47/46 | 46/45 | 25/21 | 15/13 |
| RL, round 128 | 174 | 159 | 68 | 10 | 15 | 54/54 | 59/57 | 35/31 | 26/17 |
| RL, round 292 | 177 | 156 | 80 | 4 | 21 | 56/56 | 57/51 | 38/35 | 26/14 |
| CaPtain-4B (RL, round 300) | 177 | 162 | 73 | 5 | 15 | 58/58 | 55/52 | 41/36 | 23/16 |
| Benchmark | Host -value | Strict -value | |
|---|---|---|---|
| LIBERO | 240 | ||
| LIBERO-PRO (3 suites) | 600 | ||
| LIBERO-PRO (All 4 suites) | 800 | ||
| LIBERO-Plus | 700 | ||
| LIBERO-PRO (4 categories) | 160 | ||
| LIBERO-MAX Dynamic | 80 |
| Benchmark | Change | Scope |
|---|---|---|
| RoboTwin 2.0 | Optional arm argument; a call without it goes to the arm nearer the target | Generic |
| Contact detection recalibrated for the Franka gripper | Generic | |
| Tilted re-approach when IK fails; gripper closes on contact; grasp check reported in the receipt | Generic | |
| Automatic hand-over for tasks that pass an object between arms | Specific | |
| RoboDojo | Success check that does not end the episode (the official check is final) | Generic |
| Simulated depth and camera calibration as perception inputs | Specific |
| Request | Time (s) | Action | Outcome |
|---|---|---|---|
| 1 | 7.7 | observe() , robot_geometry() | Head and wrist images; cube lies just outside the tray’s right edge |
| 2 | 11.0–15.5 | mark_point on cube and tray in head image; measure_object_geometry | Cube mask leaks into background (height 0.7 mm); not used |
| 3–5 | 19.0–27.2 | segment("yellow cube") , segment("wooden tray") ; select_detection bound to clicked points; re-measure | First binding rejected (point outside mask); after a new click, cube 3.0 cm tall and tray interior clear |
| 6 | 31.7 | inspect_contact_geometry , preview_motion | Top-down grasp with cube between the jaws |
| 7 | 34.9–43.2 | move_to 11 cm above the cube; observe | target_reached ; wrist view shows cube below gripper |
| 8 | 45.9–52.2 | move_to down at 2.5 cm/s; observe | target_reached , no contact stop |
| Request | Time (s) | Action | Outcome |
|---|---|---|---|
| 1 | 7.8 | observe() , robot_geometry() | Head and wrist images |
| 2 | 10.4–14.0 | Reasoning: the pale-blue cube matches the sky; the brown tray is rectangular and the white bowl is round, so it is excluded. mark_point on both; measure_object_geometry | Cube 2.7 cm tall; tray center 11 cm away |
| 3 | 18.1–18.2 | Re-measure with shape="box" ; inspect_contact_geometry | Cube footprint 3.4 3.5 cm; centered top-down grasp |
| 4 | 22.4–35.7 | move_to 12 cm above cube; guarded descent at 2.5 cm/s; observe | Approach target_reached ; descent stops ( motion_stalled ) about 4 mm above target with no force; cube between open jaws |
| 5–7 | 39.8–56.6 | Re-select cube (stale detection rejected; wrist camera has no depth, so head camera); inspect_contact_geometry ; close; lift 7 cm | Open fraction 1.00 0.33; cube follows gripper |
| 8–13 | 60.6–86.3 | Measure held cube and tray; raise 10 cm more | Occluded masks of the held cube rejected; cube still held |
| Method | Family | Frontier | LIBERO | LIBERO | LIBERO-PRO | LIBERO- | LIBERO-MAX | Plus / | ||
| at test | demos | 3 Suites | 4 Suites | Plus | Base | Dyn. | LIBERO | |||
| CaPtain-4B (ours) | CaP | ✗ | ✗ | 73.8 | 78.5 | 68.6 | 66.1 | 70.0 | 61.2 | 90 |
| CaPtain-4B -SFT (ours) | CaP | ✗ | ✗ | 55.4 | 59.5 | 49.2 | 48.6 | 51.2 | 40.0 | 88 |
| Qwen3.5-4B (base) | CaP | ✗ | ✗ | 0.0 | 0.3 | 0.2 | 0.0 | 0.0 | 0.0 | – |
| Vision-language-action models | ||||||||||
| OpenVLA [ 30 ] | VLA | ✗ | ✓ | 76.5 | 0.0 | 0.0 | 15.6 | – | – | 20 |
RL for Robotics
Reinforcement learning for robotics trains robots to act through interaction, from locomotion and navigation to dexterous manipulation.
See all 260 papers