Paper · 8–12 min read · 2026-08-18

Lego-RL: Harness-Native Reinforcement Learning for Coding Agents

Online reinforcement learning for coding agents in their native harnesses and real repositories. arXiv:2608.17393.


Abstract

The ideal way to run reinforcement learning on a coding agent is simple to state: whatever agent you deploy in production, train with that same agent. Follow that road in practice, though, and you hit an engineering wall that has nothing to do with the algorithm:

We took that wall apart brick by brick and built Lego-RL. Without modifying a single line of the native control flow of OpenHands SDK, Claude Code, or OpenCode, we plugged all three directly into GSPO (Group Sequence Policy Optimization) training. The same Qwen3.5-35B-A3B weights (a sparse MoE) go from 64.0, 62.4, and 57.2 on SWE-bench Verified to 70.4, 68.2, and 66.6.

Lego-RL has three pillars, one for each problem:

+6.4 / +5.8 / +9.4

SWE-bench gains on three harnesses

≥ 0.998

median train/inference logprob correlation

2.5×

per-step speedup of async over sync

2,699

training tasks after difficulty screening

Why insist on training inside the native harness?

The mainstream recipe for agentic RL reshapes the agent into a form the training framework accepts: rewrite its initialization logic, swap in the framework's own tool set, then bolt on termination logic so the reward can be read back out. The recipe works, but it quietly changes the optimization target. The policy you train is optimal under that remodeled control flow, not necessarily under the harness users actually deploy.

How much does the harness matter? The starting model is the cleanest evidence we have: the same Qwen3.5-35B-A3B weights, dropped into three different harnesses, score 64.0, 62.4, and 57.2 on the same SWE-bench Verified suite. Swapping the harness alone opens a gap of nearly 7 points, larger than the headline gains of many post-training methods.

ModelOpenHands SDKClaude CodeOpenCode
Qwen3.5-35B-A3B (starting point of this work)64.062.457.2
Qwen3.6-35B-A3B (next-generation base)67.463.460.6
KAT-Coder-V2.5-Dev (post-trained from Qwen3.6)67.066.864.8
Lego-RL-Qwen3.5-35B-A3B (this work)70.468.266.6

SWE-bench Verified (%), all measured under one unified configuration: temperature 0.7, 200 turns, 200k context.

Lego-RL takes the top score in every column, beating the newer Qwen3.6-35B-A3B by more than the entire 3.5→3.6 generational jump. So we trained on unmodified harnesses: same checkpoint, 2,699 tasks, 200k context, 126 steps each. Reward climbs on all three curves and entropy never collapses, while mean response length nearly doubles under OpenHands SDK (43.5k → 90.9k) but only grows from 41k to 51k under Claude Code. The harness shapes the model's behavioral personality.

Training reward, held-out validation score, policy entropy, and mean response length on OpenHands SDK, Claude Code, and OpenCode.
Training reward, held-out validation score, policy entropy, and mean response length over three epochs (126 steps). One initial policy, three harnesses, three completely different trajectory distributions.

Overall architecture: one infrastructure, shared by all harnesses

Lego-RL builds its trainer on verl and its sandboxed execution on Harbor. A new harness needs one lightweight adapter that launches the agent, points it at the inference service, and passes the interaction data back; everything downstream is shared.

Lego-RL training infrastructure with sandbox, in-process proxy, inference service, buffer, and trainer.
The unmodified harness runs in a per-trial sandbox. Every model call passes through an in-process proxy, is forwarded with sticky routing to the inference service, and is written into the buffer the trainer consumes. The sandbox is the only harness-specific layer in the entire system.
Closed-loop Lego-RL workflow across data preparation, validation, training, live observability, and human review.
Five stages: data preparation, run validation, training, live observability, and human review. Stage 3 is the diagram above; the Agent Plugin is the control plane spanning stages 2 through 4.

The optimization objective: a formal definition

A task instance consists of a problem statement, an initialized repository environment, and a task-specific executable verifier. The harness belongs to the environment, not to the policy. At turn , the harness maps the current interaction and repository state to a context , the policy generates an action , and the harness executes whatever tool actions were requested, producing . A rollout is the sequence of prompt/response pairs actually exchanged at the model API, and the verifier ultimately outputs a single bit:

Only policy-generated tokens participate in training. Writing for those positions:

Every turn conditions on the harness-supplied context , not on the raw history. We maximize the expected verifier reward with group-relative advantages. Each task samples trajectories and takes , with . All three production runs use GSPO's sequence-level surrogate:

where is the policy version that generated the group, and zeroes out trajectories terminated by infrastructure failures. The asymmetric bounds give the sequence-level ratio more room to move up than down. Replacing with the per-token ratio recovers PPO or GRPO, which the trainer also supports.

Two properties decide the engineering that follows. When rewards within a group are identical, : the group stays in the batch but contributes no gradient, so task difficulty relative to the current policy is on the critical path. And comes from executing code, so the signal is only as credible as the sandbox that produced it.

Pillar 1: Faithful optimization

The core difficulty: the archived transcript ≠ the token sequence at sampling time

The most intuitive way to log training data for an agent is to save the conversation transcript and re-tokenize it at training time. That is enough for SFT. On-policy RL needs the log-probabilities of the exact token sequence generated at sampling time, and the quantity recomputed from a transcript is not that quantity.

Real harnesses do rewrite their own history:

Any one of these plants an error in the importance-sampling ratio that never raises an exception. It just drifts.

Lego-RL's answer: an in-process proxy at the serving boundary

The proxy speaks both the Anthropic and the OpenAI protocol, so the harness sees nothing but a changed base URL. At the moment of generation it records token IDs, response masks, and log-probabilities along with MoE expert routing. Context alignment then runs turn by turn:

MoE routing replay: a detail that is easy to miss

MoE (mixture-of-experts) models add one further requirement: identical token IDs are not sufficient. If vLLM routes token to experts {3, 17} at sampling time but the training-time forward pass selects {3, 41}, the two are not computing the same probability.

Replaying rollout-time routing (R3) lifts the correlation from 0.9946 to 0.9993. Two silent bugs showed why this has to be checked, not assumed: a one-position routing misalignment scored worse than no replay (0.750 vs 0.995), and an undersized capture buffer for hybrid-attention models wrote out-of-bounds entries as 0, so coverage decayed to 24% before the guard started raising. After the fix, coverage stays above 99.8%.

Formally, write for the log-probability recorded at generation time and for the value the trainer recomputes. Faithful optimization requires:

That identity is on the same weights . Under async training the trainer's runs ahead of — bounded off-policyness, which corrects. A violation of the identity is a capture defect, and nothing corrects for that. Across the three production runs, median train/inference correlation never falls below 0.998.

Pillar 2: Reliable execution

Reward integrity

Reward is the task's own tests: 1.0 resolved, 0.0 otherwise. No reward model, so no reward-model drift — and every shortcut to a score without a fix has to be sealed off, because a strong coding model will find them.

Cheat pathIncidenceNotes
Reading the fix from local git history4.6%–20.5%log -p, show, checkout; no command blacklist covers them all
Modifying test files2.4%–19.4%tests pass by construction
Downloading the reference patch from GitHub~1.9%one request away if the network is open
Grader applying the reference patch itself~2.5%score is 1.0 regardless of the agent

Defenses sit in the task environment, on by default, and flip with Harbor's phases: denied while the agent runs, restored for grading.

ResourceAgent phaseGrading phase
NetworkEgress firewall in a privileged sidecar; public traffic dropped. The main container has no NET_ADMIN.Relaxed so graders can still install PyPI deps.
Git historyRebased to a single commit; fix objects no longer exist..git.orig restored for git apply.
Test filesNot provided; edits to test paths are rolled back.Restored, then graded.

Difficulty is relative to the current policy

Each task samples 8 rollouts. All-success or all-fail groups contribute no gradient, so a fixed pool gets less informative as the policy improves: on OpenHands SDK, zero-variance groups climb from 44.7% to 51.4%; OpenCode holds around 43.3%.

In-batch reward distributions by number of successful rollouts out of eight, first epoch versus last epoch.
(a–c): tasks binned by how many of 8 attempts succeeded, first vs last epoch. (d): combined share of 0/8 and 8/8 groups.

Screening 36,884 OpenSWE candidates — validity, executability, then 1–3 solves out of 4 with Qwen3.6-27B on OpenHands SDK — leaves 2,699 tasks that also transfer to Claude Code and OpenCode. An ablation on four 951-task pools confirms the cut: the selected band and its upper half both improve; the lower half and an unscreened sample do not (72.7% of the unscreened pool was never solved).

Task-screening funnel from OpenSWE candidates down to the 2,699-task training pool.
12.4% of the executable pool enters training. Disjoint from SWE-bench Verified at repository and instance level.
Validation reward and training reward over training steps for pools of different difficulty.
Validation and training reward for pools of different difficulty.

Infrastructure failures are masked from the loss — 7.1% Claude Code, 2.4% OpenHands SDK, 6.4% OpenCode — and kept in the batch at zero weight. Trajectories that hit the turn or token ceiling still count.

Trajectory termination-reason composition per harness.
What gets excluded is harness-specific: timeouts dominate Claude Code; environment-setup failures dominate OpenCode.

91% of a trial is the agent executing

Mean OpenHands SDK trial: 920s, 91.3% in agent execution. Sync screening stalled 31 times at batch boundaries (median 38.7 min). Async cuts per-step time 2.5×, but the trainer still waits on generation 40.8–66.1% of the time.

Trainer schedule under synchronous versus asynchronous execution.
Same 7.5 hours: sync completes 3 steps, async 7 (2.5×). After correcting for optimizer throughput: 1.9 h vs 1.0 h/step at staleness = 1.

Partial rollout syncs weights without discarding a ten-minute trial: interrupt vLLM, load, continue on the same replica with the prefix KV cache. The harness sees one HTTP request.

OptimizationStageOnOffMedian speedup
Prebuilt task imagessandbox startup1.04s36.2s33.2×
Mounted agent runtimeagent startup0.51s7.82s15.4×
Lazy image pullsandbox startup1.57s2.66s1.7×
Packaged grading toolchaingrading3.81s2.72s0.71×

Lazy pull's gain is in the tail (23× worst-case, 21.6 GB → 1.59 GB network). Baking the grader into the image is overhead we accept for reward reproducibility.

Pillar 3: Observable training

Failure attribution: quickly telling policy problems from infrastructure problems

One run died at step 3 because the tool-call parser was set to hermes instead of qwen3_coder. In another, validation reward fell from 0.556 to 0.150 — it looked like policy collapse, but only 60 of 172 trajectories had reached grading; the rest never finished environment setup. A third killed all 1,024 trajectories on turn one, again from an incompatible parser. One run did collapse for real, and trajectory-level analysis produced an early-stop condition that would have fired 8 steps earlier.

Live UI failure diagnosis: per-step termination reasons and assisted analysis of a collapsed run.
Left: per-step termination-reason distribution of an environment-failure run. Right: assisted analysis of a collapsed run, and the corresponding early-stop condition.
Live UI diagnostic panels showing termination-reason breakdown and train/inference consistency.
Snapshots from step 103 of the Claude Code run. Top: 94.1% of 34,816 rollouts completed normally. Bottom: train/inference consistency — a log-ratio spike shows up before the reward curve moves.

What did training actually change? Evidence at the behavioral level

A rising mean reward cannot say what the model learned; behavior can. We drew 420 trajectories from each end of the OpenHands SDK production run and analyzed the agent's behavioral patterns.

The clearest change is in self-verification:

Behavioral metricBefore trainingAfter trainingChange
Re-reading a file to confirm after modifying it73.6%98.1%+24.5pp
Files examined before the first edit3.456.92
Proactively running the test suite85.0%93.6%+8.6pp

The model picked up a working habit: look around before editing, and check the result after every change. Error recovery barely moved — among trajectories that hit a failed command, the share that still solves the task rises only from 63.9% to 66.8%. A terminal binary reward only sees the final outcome, so verification gets reinforced while mid-course correction stays flat.

The gains lean toward reliability rather than coverage. pass@8 improves 4.7 points (83.2 → 87.9); pass8 improves 11.1 (28.3 → 39.4). Malformed tool calls fall from 1.07% to 0.15%. Response length grows because there are more turns, not longer ones (46.6 → 83.1 turns, +17% tokens per turn). Budget the turn limit as well as the context window, or late-training trajectories will be cut off at the ceiling.

Tool allocation and behavioral metrics over 420 trajectories at each end of two production runs.
Tool allocation and behavioral metrics over 420 trajectories at each end of the two production runs. The trend the two harnesses share: more test invocations, fewer malformed calls.

Quick start

git clone https://github.com/LegoX/Lego-RL
bash scripts/setup_env.sh
bash scripts/train/train.sh train/configs/my_run.env

To reproduce, start from a single-node 8-GPU sync_* template, and enable full async only once the single-node loop becomes the bottleneck. Typical signs: an MoE that cannot share GPUs with vLLM, a 200k context window, or a trainer whose idle ratio shows it waiting on rollouts.

Lego-RL is part of the LegoX series, alongside SWE-Lego (SFT recipes), Terminal-Lego (trajectory-quality screening), and SWE-Review (inference-time generate-review-revise).

BibTeX

@misc{du2026legorlharnessnativereinforcementlearning,
  title={LEGO-RL: Harness-Native Reinforcement Learning for Coding Agents},
  author={Yiming Du and Yuxin Jiang and Tao Yuan and Jianbo Dai and Shaowei Wang and Jierun Chen and Chaofan Tao and Xianzhi Yu and Lifeng Shang and Kam-Fai Wong and Xiaohui Li and Haoli Bai},
  year={2026},
  eprint={2608.17393},
  archivePrefix={arXiv},
  primaryClass={cs.AI},
  url={https://arxiv.org/abs/2608.17393},
}