Repository navigation
feat(training): train a bundle's own agents with TRL's harness rollouts - #434
Merged
Merged
Conversation
AgentEnvSessionFactory is an OpenEnv ResourceSessionFactory for TRL's HarnessRolloutWorker with harness_adapter=None: each rollout runs a bundle task as agent-env run does, its agents' model calls going through OpenEnv's capture proxy to the trainer's vLLM, and the task's verifiers give the reward. prompt_rows builds the dataset, a row per task, which names its task so the factory finds it from the prompt TRL forwards. The factory refuses an engine that returns no token ids, pins each session to the policy version TRL published when it was created, and keeps the proxy's session routes behind an admin key. Rollouts waiting on a restarting server now say so once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndpoint The run-wide litellm_base_url and litellm_api_key overrides also reach the rubric judge and envs that call a model, so a judge would have asked the policy through the proxy and its tokens been trained on. The factory now sets them in step_params for the task's solver deploys (solver_steps), which deploy_agent reads over the run-wide ones, and agent= swaps the solver as agent-env run --agent does. prepare_bundle's solver_endpoint tells preflight the caller gives those deploys their endpoint, so training needs no endpoint configured while a judge still does. A task whose solver no deploy_agent step deploys is left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e, and a solver that judges itself A task whose rubric judge is its solver would have its grading trained on, so it's left out. Tasks are now chosen from the plan (planned_tasks) before prepare_bundle checks and writes, so what a left-out task alone needs, such as a configured endpoint for an unprompted agent, can't stop the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e proxy, on Docker opencode and claude-code run their smoke tasks against a stand-in vLLM that has them run the shell command and then answer, with no endpoint configured: the rollout scores 1.0 and every captured turn trains. The stand-in moves to tst/util/fake_vllm.py, which the unit test shares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…un only on local Docker TRL closes the sessions in flight when it stops, but close only dropped the captures: the run went on in the session loop's thread and its sandboxes outlived the trainer. close now cancels a run still in its task and returns once the run is torn down; a run already tearing down is waited for, not cut short. A rollout's timeout bounds its task alone, so its teardown also completes before TimeoutError reaches TRL. The factory and prompt_rows no longer take a sandbox: a remote sandbox can't reach the capture proxy on the trainer's loopback, so every deploy runs on local Docker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nly once close read the running task under the lock but cancelled it later, so a run that left its task in between could have its teardown cancelled. The check and the cancel now happen together on the session loop, where the run clears its task before tearing down, and a second close sends no cancel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er arguments; CI on the latest OpenEnv - A session runs Task.run as an asyncio task of its own, which a stop or the timeout cancels, and tears down in its own coroutine, which nothing cancels, so a stop can't cut a teardown short by construction. - reward_of moves beside recorded_scores in agent_env.task.task, so the harness no longer imports the OpenEnv server module. - The factory drops api_key and max_model_calls, which nothing used. - AgentEnvSession takes keyword arguments; imports and long lines tidied. - harness.yml runs the harness tier on the locked and the latest OpenEnv, whose capture internals the harness reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reward_of and its helpers move from agent_env.task.task to a module of their own beside build_app, whose rules they are; the harness imports them there, and task.py is as on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Train a bundle's own agents (claude-code, codex, opencode, any harness a task deploys) with TRL's harness rollouts:
trl.experimental.async_grpo.HarnessRolloutWorkerwithharness_adapter=None, where the agent owns its loop. Each GRPO rollout runs one bundle task the wayagent-env rundoes: its envs, its deployed agent, its prompts, its verifiers. The agent's model calls go through OpenEnv's capture proxy to the trainer's vLLM. The proxy records the exact token ids and log-probs TRL trains on, and the task's verifiers give the reward.Pass
functools.partial(AgentEnvSessionFactory, bundle, llm_url=..., model=...)asHarnessRolloutWorker'sharness_session_factory(fromtrl.experimental.async_grpo.openenv_harness). Give the trainerDataset.from_list(prompt_rows(bundle))as its dataset andreward_funcs=[]. The docs page will carry the full recipe once the GPU run below pins it.agent_env.training.harness(extraharness: OpenEnv 0.10 or later; TRL and vLLM are the trainer's to pin):prompt_rows(bundle, ...): the dataset, a row per task. TRL hands the session factory only a row's prompt, so each row names its task in a system line. The model never sees that prompt, since the task's agent builds its own.AgentEnvSessionFactory(bundle, llm_url=, model=, ...): OpenEnv'sResourceSessionFactory, built in TRL's rollout process. It does four things:agent-env rundoes;require_llm), rather than producing untrainable rollouts;trl-policy-vNwith LoRA), even after TRL moves on.reward=works as inbuild_app. A run that fails isNone, which TRL leaves out of the update rather than scoring 0.Task.runas an asyncio task of its own, whichclose()or the rollout's timeout cancels, and tears down in its own coroutine, which nothing cancels. Soclose()returns only once the run is torn down, no sandbox outlives the trainer, and a stop can't cut a teardown short. A secondclose()sends no cancel.sandboxargument.Only the solver calls the policy
A task's rubric judge, its user simulator and envs that call a model must keep the configured endpoint. Otherwise a judge would ask the policy through the proxy, and its tokens would be trained on. So the factory doesn't use the run-wide
litellm_base_url/litellm_api_keyoverrides:deploy_agentreadslitellm_base_urlandlitellm_api_keyfrom its ownstep_paramsover the run-wide overrides.solver_steps, the agent a step prompts).agent=swaps the solver's agent exactly asagent-env run --agentdoes:prepare_bundletakesagent, andsolver_params(was_solver_params) gives the step params.prepare_bundle(solver_endpoint=True)tells preflight that the caller gives those deploys their endpoint. Training needs no endpoint configured, while a task with an agent judge still does.planned_tasks, new). A task whose solver nodeploy_agentstep deploys is left out with a warning, and so is one whose rubric judge is its solver. Nothing a left-out task alone needs can stop the rest.Also
reward_of, the reward rulesbuild_appapplies, moves out ofapp.pyintoagent_env.openenv.reward, which the harness imports.harness.ymlruns the harness tier on the locked and on the latest OpenEnv, whose capture internals (session registry, export, training trace) the harness reads. Like the TRL and verl jobs, it isn't a required check.Rollouts waiting for a restarting env server now say so, once per server (
_openenv.reopened); before, they waited silently.Tests
harness_test.pydrives a bundle with realdeploy_agent/prompt_agentconfigs against a stand-in vLLM (token ids, processed logprobs), with no model endpoint configured:agent=swaps the solver;tst/integration/training/harness_test.py(slow tier): the shipped opencode and claude-code run their smoke tasks in Docker through the proxy, against the stand-in (tst/util/fake_vllm.py), which has them run the shell command and then answer. With no endpoint configured, the rollout scores 1.0 and every captured turn trains.preflight_test.py: withsolver_endpoint, the judge still needs the configured endpoint.test_step_overrides_agent_and_model.py:deploy_agent's endpoint is its step params', over the run-wide overrides, then its own.harness_test.pyalso covers stopping:close()on a rollout mid-task returns once it's torn down, with the run unscored; a rollout past its timeout is torn down beforeTimeoutError; and a session closed before it runs never runs.sessions_test.py: the restart warning appears once.No-GPU check
On a Linux EC2 box with Docker, using TRL 1.17.1 and OpenEnv 0.10.0 (CPU torch). The real claude-code and opencode harnesses run their shipped smoke tasks (
agents/tasks/*-smoke.json: runecho $((6 * 7)), answer 42) in local Docker, two rollouts at once per task, as a GRPO group runs in TRL's session pool. Their calls go through the capture proxy to a scripted stand-in for vLLM that returns token ids and processed logprobs. It answers the first call with a shell tool call and the call after the tool result with "42". Each capture then goes through TRL's own no-adapter path:_turns_from_training_trace,to_trace_entries,_messages_from_traceand_chain_to_sequenceswithfork_threshold_tokens=0.Bashcall, then "42"bashcall, then "42"temperature1,top_p1,top_k-1,min_p0,repetition_penalty1) reached the engine on every agent call./v1/messagescalls were translated to chat completions.forkin TRL's drift tally comes from the stand-in's token rendering, not from the capture.The first version of this PR used the run-wide model overrides, and the check found that preflight refused the smoke tasks with no endpoint configured. Fixing that turned up the judge problem above.
Not yet
🤖 Generated with Claude Code


Confidence Score: 5/5
The changes since the previous review appear safe to merge.Summary
Adds bundle-agent training through OpenEnv’s capture proxy for TRL harness rollouts.
Since the previous review, the reward helpers moved into
agent_env.openenv.rewardwithout changing their behavior. No new actionable issues were found.Diagram
sequenceDiagram participant Trainer as TRL participant Session as AgentEnvSession participant Agent as Task solver participant Proxy as Capture proxy participant Model as vLLM Trainer->>Session: Create rollout Session->>Agent: Run task with session endpoint Agent->>Proxy: Model calls with session key Proxy->>Model: Request pinned policy Model-->>Proxy: Tokens and log probabilities Proxy-->>Agent: Model response Session->>Session: Record scores and tear down Session-->>Trainer: Training trace and rewardReviews (7) · Last reviewed commit: "refactor(openenv): keep the reward rules..." · Reviewed by Greptile