Skip to content

feat(agents): RemoteAgent, thin proxy server for user-hosted remote agent services - #2163

Merged
adil-a merged 26 commits into
mainfrom
remote-agent
Jul 30, 2026
Merged

feat(agents): RemoteAgent, thin proxy server for user-hosted remote agent services#2163
adil-a merged 26 commits into
mainfrom
remote-agent

Conversation

@adil-a

@adil-a adil-a commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Part of the external-agent-integration epic #1396. Supersedes the collector-side agent_url approach proposed in #2006 (closed unmerged in favor of this PR).

What

An agent server, responses_api_agents/remote_agent/, that drives an agent service running outside Gym's process tree (your own repo, your own infrastructure) through Gym's tool loop. The remote service implements one endpoint compliant with the OpenAI /v1/responses contract, and the two servers compose as Responses-speaking agents:

POST {agent_base_url}/v1/responses — receives the conversation so far (the row's responses_create_params with accumulated output and tool results appended to input) and returns a Responses API object: unpaired function_call items to ask Gym to execute environment tools, paired call+output items as records of its own internal tools (passed through untouched), or a final assistant message to finish.

Gym owns everything else: it seeds the session and holds its cookies, executes the asked-for tools against the resources server, appends results and calls the service again, validates each reply against the Responses API schema (failing the rollout on mismatch), verifies, and returns the verify response from /run. The resources server is never exposed to the service — tool execution, session cookies, and verifier_metadata all stay inside Gym. The service's own cookies are round-tripped per call so it can keep per-rollout state; the only network direction is Gym → service. To the collector it is a normal named agent — resume, aggregation, and profiling work unchanged.

The loop is simple_agent's responses() with two marked in-loop divergences — the model hop is a hardened POST to agent_base_url, and only unpaired function_calls are executed (a call the service already answered itself is its own record, not an ask) — plus the never-raise /run contract that turns transport/validation errors into failure rows, and one deliberate cookie difference: the service's own cookies are round-tripped to it but stay out of the outgoing Set-Cookie (they are its private session, not Gym's). run() mirrors simple_agent's seed → self-post /v1/responses → verify.

Why a server instead of collector-side agent_url (#2006)

  • The token-capture/training stack (feat(token-id-capture): capture training tokens from external harnesses #2124feat(token-id-capture): Claude Code external-harness example #2128, unmerged) gates participation by the agent's name in Gym's config; a url-dispatched agent has no config entry and is excluded by construction. A named RemoteAgent is compatible by design — note this PR contains no capture wiring; a wiring example is a follow-up once that stack lands.
  • Failure handling, validation, timeouts, and session plumbing live in the agent-server layer — this server's /run implementation plus the shared SimpleServer/ServerClient plumbing — instead of being reimplemented inside the shared collection loop.
  • /run responsibilities stay in the agent-server layer; the collector stays agent-agnostic.
  • Same pattern as every other harness integration (claude_code_agent, codex, …).

Failure contract

Failures never raise out of /run: remote endpoint down (3 total connection attempts, ClientOSError/ServerDisconnectedError only; timeouts and all other errors fail after a single attempt), per-call and whole-rollout timeouts, malformed/interrupted replies, seed/verify errors, and internal bugs all become reward-0 sentinel verify-responses (_ng_failure_class="remote_agent_error") that rollout collection routes to the failures sidecar and retries on resume — non-terminal failures only, up to NEMO_GYM_MAX_ROLLOUT_ATTEMPTS (default 3). An invalid Responses object from the service is terminal (a schema bug will not fix itself on retry); the terminal flag crosses the HTTP self-post boundary by exception name. Reused rollout/failures JSONL rows carrying stale result keys are sanitized rather than crashing or leaking routing flags. Tool-level errors are NOT rollout failures: an unknown tool name or malformed arguments come back to the service as that call's function_call_output, matching simple_agent's semantics.

Testing

  • 51 offline tests (mocked ServerClient + mocked aiohttp seam; the /v1/responses self-post is routed into the real responses() with the exception middleware emulated): config validation, loop mechanics (multi-turn tool execution, paired-call pass-through, unknown-tool feedback, malformed-arguments feedback, max_steps, service-cookie round-trip, usage accumulation), every transport failure mode → sentinel, terminal classification across the route boundary, semaphore bounds incl. release-on-failure, run-wallclock-after-semaphore semantics, aggregate proxy + bound, route-level serialization (HTTP 200, never 500).
  • Stateful E2E in-suite: the real example_session_state_mgmt counter server in-process; the service returns unpaired tool asks, GYM executes them on the seeded session, reward 1.0 through the real verifier — and the test asserts the service was fed the counter value Gym read back.
  • Collector round-trip in-suite: real RolloutCollectionHelper.run_from_config driving this agent — successes to the main JSONL, sentinel rows to the failures sidecar.
  • Live E2E, off-host, real agent: a containerized service (own network namespace) wrapping the Claude CLI (opus via an internal gateway) as the decision-maker, agent_base_url pointing at the container's bridge IP; the counter resources server stayed on loopback with a random port — unreachable from the container by construction. 5/5 rollouts reward 1.0, trajectories reading function_call → function_call_output → … → message, real token usage accumulated across loop turns (~8.3k mean/rollout). The only network path was Gym → container.

Follow-ups

🤖 Generated with Claude Code

…agent services

Adds responses_api_agents/remote_agent/: a thin agent server that brokers
rollouts to an agent service running outside Gym's process tree. The remote
service implements one endpoint (POST {agent_base_url}/v1/responses),
receives only responses_create_params (verifier_metadata never leaves Gym),
runs its own loop with its own model and tools, and returns one finished
Responses API trajectory. The server owns seed_session, session cookies,
strict trajectory validation, and /verify, and returns the verify response
from /run.

- failures never raise: every failure (remote endpoint down, timeout,
  malformed or interrupted reply, seed/verify errors, internal bugs)
  becomes a reward-0 sentinel verify-response routed to the failures
  sidecar and retried on resume; reused rollout/failures JSONL rows with
  stale result keys are sanitized rather than crashing or leaking routing
- bounded outbound retries (3x, connect/disconnect only), per-request
  ClientTimeout, whole-run wallclock applied after semaphore acquire,
  redirects rejected, per-worker concurrency semaphore
- optional forward_session: resources-server URL + session cookie sent as
  X-NeMo-Gym-* headers so the remote service can call Gym-hosted tools
  (stateful environments); declared-tools guard refuses silent-zero
  configurations; advertised_resources_url for off-host services
- aggregate_metrics proxied to the resources server with a wallclock bound
- 48 offline tests incl. an in-process stateful counter E2E (forwarded
  session cookie, real verify) and a collector round-trip pinning sidecar
  routing

Part of the external-agent-integration epic #1396; supersedes the
collector-side agent_url approach in #2006 (see PR description).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@adil-a
adil-a marked this pull request as ready for review July 28, 2026 15:15
@adil-a adil-a changed the title feat(agents): RemoteAgent — thin proxy server for user-hosted remote agent services feat(agents): RemoteAgent, thin proxy server for user-hosted remote agent services Jul 28, 2026
@adil-a adil-a assigned adil-a and ffrujeri and unassigned adil-a and ffrujeri Jul 28, 2026
@adil-a
adil-a requested a review from ffrujeri July 28, 2026 17:40
@adil-a adil-a self-assigned this Jul 28, 2026
- replace forward_session/assume_remote_tools with a single
  tools_mode: refuse|forward|remote enum, each choice documented
- remove the speculative non-terminal-trajectory warning (and its test)
- document the body/record duality on _run_once, the per-request URL
  forwarding rationale (random ports), and advertised_resources_url's
  bind-vs-advertise role directly in the code
- README: off-host checklist (bind, route, verify from the remote
  machine, advertise) and the remote-side reachability self-check
  recommendation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
Usage guide for the remote_agent server: the one-endpoint contract with
request/response expectations, a minimal FastAPI quickstart, every config
knob with defaults, the tools_mode choices with the off-host checklist for
forwarded sessions, failure/resume semantics, and the gotchas list
(redirects, missing usage, timeout retry semantics, skills, reused rollout
files, stacked concurrency bounds).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

Comment thread responses_api_agents/remote_agent/app.py
Docs: quickstart usage example and contract table now show the full usage
shape strict validation requires; placeholder/ref-name clarified; retry-cap
and terminal-failure semantics stated; MCP forward-mode limitation and
internet-exposure (no outbound auth, no HTTP_PROXY) notes added; dead README
cross-reference removed.

Code: the unroutable-advertise warning now also covers 0.0.0.0 (a bind
address resolves to the remote machine's own loopback — confirmed live in an
off-host container E2E where it produced silent zero rewards with no
warning); remote-hop failures honor global_aiohttp_client_request_debug with
full tracebacks/bodies, matching core request()/raise_for_status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
…model

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
…-reasons-to-return contract

The quickstart service now uses the Claude Code CLI as the agent brain —
render conversation, let Claude decide, translate to Responses format (the
CLI envelope note added where the translation happens). Verified by
execution: the doc's literal block driven by the real RemoteAgent against
the real counter server with real haiku scored 5/5 reward 1.0, including
batched-vs-incremental ask patterns and fenced-JSON model output. The
contract docs now state explicitly that the service runs its own tools
freely and returns only to ask for a Gym tool or to finish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
…ckstart

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
adil-a and others added 4 commits July 30, 2026 01:10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
…the intro

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
@ffrujeri

Copy link
Copy Markdown
Contributor

The remote service implements one endpoint and is called like a model:

I think here we don't necessarily need to say that it is called like a model, because it is more like a composition of agents all compliant with the OpenAI Responses contract. And with the possibility of calling Gym's resources servers tools

Comment thread fern/versions/latest/pages/agent-server/remote-agent.mdx Outdated
Comment thread fern/versions/latest/pages/agent-server/remote-agent.mdx
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: adil-a <adil.asif2000@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants