Drive a Remote Agent
Drive a Remote Agent
The remote_agent server lets an agent that runs as its own HTTP service — in your repo, on
your infrastructure — be driven by standard rollout collection. Your service implements an
endpoint compliant with the OpenAI /v1/responses contract, and the two servers compose as
Responses-speaking agents: each call your service receives the conversation so far and returns
what it wants to do next. Gym runs the loop, executes environment tools, holds the session, and
verifies.
Your results land in the standard artifacts (gym eval profile, aggregation, and training
pipelines all work unchanged).
Who does what
Gym is responsible for:
- Seeding a fresh environment session per rollout and holding its cookies — session state
and the task’s answer key (
verifier_metadata) never reach your service. - Driving the loop: calling your service, executing the tool calls it asks for against the environment, appending the results, and calling again.
- All failure handling: bounded connect retries, per-call and whole-rollout timeouts, and converting every failure into a reward-0 row in the failures sidecar (a bad rollout never crashes the collection run).
- Verifying the finished trajectory and reporting the reward.
Your service is responsible for:
- Implementing
POST {agent_base_url}/v1/responsesand answering every call with a valid Responses API object (required fields and types are enforced, unknown extra fields are tolerated; an invalid object is a terminal, non-retried failure). - Deciding what to do next on each call. Within a call your service can do anything — run its own model for any number of turns, execute its own tools, spawn sub-agents. There are exactly two reasons to return: you need a Gym-hosted tool executed (return the ask), or the rollout is finished (return the answer).
- Being callable N times per rollout. Each call carries the full conversation so far, so a stateless service needs nothing extra; if you want per-rollout state, set a cookie — Gym echoes your cookies back on every subsequent call of the same rollout.
- Reporting
usage(or omitting it entirely — allowed, but your token metrics are empty).
The contract, exactly
Every request you receive is the task’s responses_create_params with the conversation
accumulated in input:
input— the task messages, plus (from turn 2 onward) everything so far: your previous output items and onefunction_call_outputper tool call Gym executed for you.tools— the tool schemas this environment serves, verbatim from the dataset row. These are the only tools you may ask Gym to execute.- Your own cookies from earlier calls of this rollout, echoed back.
Every response you return is one Responses API object whose output decides the next step.
Returning does not mean your agent is done thinking — it means one of two things: “I need a Gym
tool” or “I’m finished.” Everything your agent did internally since the last call (its own model
turns, its own tool executions) either stays private or rides along as paired call+output
records:
Expectations and edge semantics:
- Ask only for tools the request’s
toolsdeclared. An unknown tool name is not an error — Gym sends the resources server’s error text (e.g. a 404) back to you as that call’sfunction_call_output, and the rollout continues. - Malformed
arguments(not valid JSON) likewise come back to you as an error output, never a crash. - Never return an unpaired
function_callfor a tool you already executed yourself — unpaired means “Gym, run this.” - One rollout = one conversation. Requests of the same rollout share your cookies; different rollouts (including retries of the same task) start clean.
Quickstart
A complete, realistic service against the in-repo stateful counter environment
(example_session_state_mgmt; tasks read “add 1 then add 2 then get the count”). The agent
brain is the Claude Code CLI: each call, the service renders the conversation into a prompt,
lets Claude decide, and translates the decision into tool asks or a final answer. Requirements
on the service’s machine: claude on PATH and authenticated (a logged-in CLI or
ANTHROPIC_API_KEY in the environment); pick the model with CLAUDE_MODEL (default haiku).
One config file wires the environment and the agent together (this exact shape drove the live end-to-end run — the only thing that changes for your own benchmark is the resources-server block and the ref name):
Run the three pieces in separate terminals — the first two are long-running servers, the third is the driver:
Expected: 5 rollouts, mean/reward: 1.0, and each trajectory reads
function_call → function_call_output → ... → message. A live end-to-end run of exactly this
setup — the service in a container on its own network, Claude deciding every call — scored 5/5.
Nothing here is specific to FastAPI or the Claude CLI: any HTTP stack works (the live run used
stdlib http.server), and any brain works — render the conversation into your agent’s context,
let it decide, translate the decision into unpaired function_call items or a final message.
Errors on your side are safe: a 5xx or timeout becomes a reward-0 row in Gym’s failures sidecar,
never a crashed run.
Configuration knobs
Failure handling and resume
Failures never crash a collection run. A down service (3 connection attempts, then fail), a
timed-out call, a malformed reply, or a verifier error becomes a reward-0 row with
_ng_failure_class: "remote_agent_error" in the failures sidecar (for
results/rollouts.jsonl the sidecar is results/rollouts_failures.jsonl) —
the main rollouts file stays clean, and +resume_from_cache=true retries failed tasks up to the
attempt cap (NEMO_GYM_MAX_ROLLOUT_ATTEMPTS, default 3; terminal failures — an invalid response
shape — are not retried). Connection failures and timeouts name the failing URL or the timeout
knob involved.
Gotchas
- Your service is called multiple times per rollout. Design for it: everything you need is
in each request’s
input, or in your own cookies. - Redirects are rejected, not followed. Any
3xxfrom your service fails the rollout with theLocationshown — pointagent_base_urlat the final address. (Followed, a301/302/303would silently re-issue the POST as a body-less GET; a307/308would silently re-send the task to an address you never configured.) - Partial
usagefails validation. Report the full object —input_tokens,output_tokens,total_tokens,input_tokens_details: {cached_tokens},output_tokens_details: {reasoning_tokens}— or omitusageentirely (allowed; token metrics are then empty, with a warning). - A previous run’s output can be reused as the input dataset (for example, re-running the
tasks in a failures sidecar). Output rows carry the old run’s results (
reward,response,error, internal_ng_*flags); those fields are stripped from each row on the way in, so a stale result can’t collide with or leak into the new run’s output. - Exposing your service on the public internet needs network-level trust. Gym sends no
credential on the
/v1/responsescall (agent_base_urlrejects embedded credentials), so an internet-exposed service should sit behind a VPN, private network, IP allowlist, or authenticating tunnel. Note that proxies configured viaHTTP_PROXY/HTTPS_PROXYenvironment variables are ignored (Gym’s HTTP client does not read them), so a host that can only reach the internet through such a proxy cannot reach your service.