Skip to content

feat: add single-agent-turn environment server - #3717

Merged
ananthsub merged 4 commits into
mainfrom
ananthsub/episode-orchestration-06-single-agent-processor
Sep 28, 2026
Merged

ananthsub merged 4 commits into
mainfrom
ananthsub/episode-orchestration-06-single-agent-processor

Conversation

@ananthsub

@ananthsub ananthsub commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Implements the built-in resources-backed single-agent-turn Environment Server protocol. The Environment Server assigns participant session IDs, registers cleanup before each seed, runs one Agent activation, verifies the response through Resources, and closes both participants.

The episode result is the Resources verify response itself, plus the Agent's session observations as ng_agent_observations. SingleAgentTurnResult subclasses BaseVerifyResponse and allows extra fields, so each Resources Server's benchmark-specific fields are preserved. A stored rollout record therefore has the same shape as an Agent's /run result, and rollout collection can store it without knowing its fields. This makes ResourcesVerifyResponse redundant, so it is removed.

Tool-grant session requests are serialized as JSON before dispatch so Pydantic URLs are strings and the kind/transport discriminators remain present. The Resources session closes only in final cleanup, which the episode deadline does not bound, so a slow or failed close is logged and cannot replace a verified result. Every downstream call in an episode, including cleanup, carries the attempt's rollout ID for observability. Connection failures, including a response body cut off mid-read, are retryable. Verification needs the Agent close response, which carries the Agent's observations and final Resources cookies, so a transient close failure makes the episode fail as retryable instead of retrying the close.

Single-agent-turn interaction flow

This diagram describes the built-in single-agent-turn use case. Other Environment Servers may coordinate different participants and use different request, result, and lifecycle flows.

sequenceDiagram
    participant C as RolloutCollector
    participant E as EnvironmentServer
    participant R as ResourcesServer
    participant A as AgentServer
    participant M as ModelServer

    C->>E: POST /run
    E->>R: Seed resources session
    R-->>E: Tool access and optional SandboxAccess
    E->>A: Seed agent session
    E->>A: One /v1/responses activation
    A->>M: Model requests
    A-->>E: Agent response
    E->>A: Close agent session
    A-->>E: Agent observations
    E->>R: Verify response
    Note over E,R: Final cleanup, outside the episode deadline
    E->>R: Close resources session
    E-->>C: Verify response plus agent observations
Loading

Stack position

Part 1 of 6. Next: #3724.

No issue is linked because this is one layer of the Environment Server stack.

Validation

  • uv run pytest environment_servers/single_agent_turn/tests/test_app.py -q
  • uv run pytest tests/unit_tests/test_resources_sessions.py tests/unit_tests/test_agent_sessions.py -q
  • Production-shaped fake requests are encoded with orjson and validated with AgentSeedSessionRequest.model_validate_json.
  • The tests check that the episode result has no nested verification and keeps benchmark-specific fields.
  • The tests check that a slow Resources close keeps the verified result, that every downstream call carries the rollout ID, and that an interrupted response body is retryable.

Rollouts

See #3722 for real-model rollouts through this Environment Server.

Compatibility and benchmark impact

No rollout is redirected by default. Resources tool transports remain explicit opt-ins. Cleanup failures after a verified reward no longer turn that rollout into a terminal failure.

ResourcesVerifyResponse, added to nemo_gym.base_resources_server in #3544, is removed. Nothing outside this stack used it. A caller that parses a verify response without knowing its concrete type can declare extra="allow" on its own model, as SingleAgentTurnResult does.

@ananthsub
ananthsub requested a review from a team as a code owner September 28, 2026 15:37
@nemo-automation-bot
nemo-automation-bot Bot requested review from a team September 28, 2026 15:37
@ananthsub
ananthsub added this pull request to stack #3723 September 28, 2026 15:38
@yaoyu-33 yaoyu-33 added area:env-infra Shared environment framework, lifecycle, registry, scaffolding, and validation complexity:high Cross-component or shared-contract change with a large review surface feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer labels Sep 28, 2026
@ananthsub
ananthsub removed this pull request from stack #3723 September 28, 2026 15:41
@ananthsub
ananthsub added this pull request to stack #3725 September 28, 2026 15:43
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…e result

SingleAgentTurnResult nested the Resources verify response under
`verification`. Stored rollout records therefore differed from an
agent's /run result, and the collector had to reach into the nested
object to find the reward.

Make SingleAgentTurnResult the verify response itself, with agent
observations as `ng_agent_observations`. It allows extra fields so each
Resources Server's benchmark-specific fields are preserved, which makes
ResourcesVerifyResponse redundant; remove it. Rollout collection can
then store any Environment Server's result as returned.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Comment on lines +1 to +3
single_agent_turn:
environment_servers:
single_agent_turn:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mentioning single_agent_turn feels a bit clunky because it's not clear whether it should be the top level item or under environment_servers. any way we can fix this confusion?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The top-level key is the instance name, which routes, _inherit_from, and server references use. The key under environment_servers is the implementation directory. The two usually match, which is where the confusion comes from. This follows the repo's config convention, so I added a comment to the template explaining both keys in 895d0af instead of renaming.

) from error

try:
await resources_cleanup.close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we leave this teardown to the registered final cleanup? It still runs under the episode deadline: if that expires here, an already verified reward becomes an Episode timed out failure, even when final cleanup succeeds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I removed the explicit close after verify, so the Resources session now closes only in final cleanup, outside the episode deadline. A new test checks that a slow close keeps the verified result. Fixed in 895d0af.

try:
verify_http_response = await self.server_client.post(
server_name=self.config.resources_server.name,
url_path="/verify",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we bind rollout_context(request.episode_id.capture_key) for downstream calls? This route has no rollout prefix, and ServerClient's fallback does not recognize native episode_id, so verifier/judge model calls lose rollout correlation when observability is enabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. run_request now binds rollout_context(request.episode_id.capture_key) around the whole episode, including final cleanup, so the base class covers every Environment Server. A new test checks that each downstream call, including /verify and /close_session, carries the attempt's rollout ID. Fixed in 895d0af.

def _is_retryable_dependency_error(error: Exception) -> bool:
if isinstance(error, ClientResponseError):
return error.status in {408, 425, 429} or error.status >= 500
return isinstance(error, (ClientConnectionError, TimeoutError))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also classify interrupted response-body reads as retryable? A mid-body disconnect raises ClientPayloadError, not ClientConnectionError, so this currently produces terminal=True and prevents another rollout attempt.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, ClientPayloadError is now retryable, and the retry test covers it. Fixed in 895d0af.

@ffrujeri
ffrujeri self-requested a review September 28, 2026 22:40
@ananthsub
ananthsub merged commit 2b892c9 into main Sep 28, 2026
41 checks passed
@ananthsub
ananthsub deleted the ananthsub/episode-orchestration-06-single-agent-processor branch September 28, 2026 23:58
pull Bot pushed a commit to Stars1233/Gym that referenced this pull request Sep 29, 2026
## What does this PR do?

Adds compatibility Environment Servers before environment-specific
Resources and Agent migrations:

- `legacy_agent` relays existing Agent `/run` and `/aggregate_metrics`
contracts without interpreting them.
- `single_agent_turn_legacy` converts legacy flat rows to the
single-agent-turn protocol and returns its result in the legacy result
shape, adding only `agent_ref`.
- Config composition, Agent swapping, scaffolding, migration tooling,
and repository configs route existing Agents through compatibility
deployments.

Rollout collection routes each flat row to the Environment Server that
fronts its Agent. The Agent-to-Environment-Server map is built once, and
every Agent in a batch is resolved before any rollout is dispatched. An
Agent fronted by several Environment Servers raises
`AmbiguousEnvironmentServerError` before dispatch, instead of silently
sending every row to the first one in config order. Several Environment
Servers may still front one Agent when no row routes by that Agent.

## Compatibility flow

This is the compatibility flow for existing single-agent environments.
Other Environment Servers may coordinate different participants.

```mermaid
sequenceDiagram
    participant C as RolloutCollector
    participant E as LegacyEnvironmentServer
    participant A as ExistingAgentServer

    C->>E: POST /run with legacy row
    E->>A: Relay existing /run
    A-->>E: Existing result body
    E-->>C: Unchanged result body
```

## Stack position

Part 2 of 6. Previous:
[NVIDIA-NeMo#3717](NVIDIA-NeMo#3717). Next:
[NVIDIA-NeMo#3718](NVIDIA-NeMo#3718).

No issue is linked because this is one layer of the Environment Server
stack.

## Validation

- `uv run pytest tests/unit_tests/test_global_config.py
tests/unit_tests/test_rollout_collection.py -q`
- `uv run pytest
environment_servers/single_agent_turn_legacy/tests/test_app.py -q`
- `uv run pytest environment_servers/legacy_agent/tests/test_app.py -q`
- The rollout collection tests check that an ambiguous Agent fails
before any request is sent, and that twin Environment Servers for an
Agent no row uses do not interfere.

## Rollouts

`example_single_tool_call` with Simple Agent was run through
`legacy_agent` against a real model. The relay returned the same status,
result fields, and `Set-Cookie` as calling the Agent's `/run` directly,
and passed a malformed request's 422 through unchanged. End-to-end
evidence for the whole stack is on
[NVIDIA-NeMo#3722](NVIDIA-NeMo#3722).

## Compatibility and migration impact

Every runnable Agent instance now needs an Environment Server in front
of it. Out-of-tree configurations, including NeMo RL configurations,
must add a `legacy_agent` deployment or run
`scripts/add_legacy_agent_environment_servers.py`. Legacy request and
result bodies do not change.

A run whose rows route by an Agent that several Environment Servers
front now fails before dispatch with an error naming the candidates.

---------

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Signed-off-by: Marta Stepniewska-Dziubinska <martas@nvidia.com>
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Co-authored-by: Marta Stepniewska-Dziubinska <martas@nvidia.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: yaoyu-33 <yaoyu.094@gmail.com>
ananthsub added a commit to ananthsub/Gym that referenced this pull request Sep 29, 2026
## What does this PR do?

Adds typed session support to SWE-bench Pro while preserving its legacy
request shapes, and makes the single-agent-turn Environment Server
verify through each Resources Server's own verify body.

SWE-bench Pro sessions:

- The Resources Server owns task sandbox setup, verification, and
cleanup.
- The typed sessions build on the SWE-bench Pro lifecycle from NVIDIA-NeMo#3542.
`/close_session` closes a typed session when given a
`ResourcesCloseSessionRequest`, and closes the cookie session when given
an empty body, as `hermes_sandboxed_agent` sends.
- A repeated verify of a typed session fails once its task sandbox has
been consumed, instead of grading an empty patch and returning a
normal-looking reward.

Verification contract:

- `single_agent_turn` now sends `/verify` the flat body an Agent's
`/run` sends: the task's `task_data` fields, `responses_create_params`,
and the agent's `response`. The request carries the Resources session
cookie from seeding, which identifies the episode.
- `SingleAgentTurnVerificationInput` and
`SingleAgentTurnResourcesVerifyRequest` are removed, so no Resources
Server depends on an Environment Server's protocol. This changes
`single_agent_turn` code merged in NVIDIA-NeMo#3717.
- SWE-bench Pro accepts one verify model, `SWEBenchProVerifyRequest`,
and finds its sandbox through the session.
- `ResourcesVerifyRequest[T]` stays for Resources Servers paired with
their own Environment Server, whose payload type they define.
- Records from `single_agent_turn` now carry the same echoed task fields
as records from an Agent's `/run`, and reverification sends the body
live grading sends.

## Single-agent-turn interaction flow

This diagram describes SWE-bench Pro under the built-in
single-agent-turn Environment Server. Other Environment Servers may use
different lifecycle and result contracts.

```mermaid
sequenceDiagram
    participant E as EnvironmentServer
    participant R as SWEProResources
    participant S as TaskSandbox

    E->>R: Seed with task data and caller ID
    R->>S: Start and prepare sandbox
    R-->>E: SandboxAccess and session cookie
    E->>R: Verify with task fields and the Agent response, under the session cookie
    R->>S: Extract patch and run evaluator
    R-->>E: Verification result
    E->>R: Close session
```

## Stack position

Part 1 of 4, based on `main`. Next:
[NVIDIA-NeMo#3720](NVIDIA-NeMo#3720).

No issue is linked because this is one layer of the Environment Server
stack.

## Validation

- `uv run pytest resources_servers/swebench_pro/tests -q` β€” 84 passed
- `uv run pytest environment_servers/single_agent_turn/tests/test_app.py
-q` β€” 12 passed, including a check that `/verify` receives the flat body
under the Resources session cookie
- `uv run pytest tests/unit_tests -q` β€” passes except two tests that
also fail on `main` in the local environment (missing `sandbox` extras
and a pip notice)
- `pre-commit run` on the changed files

## Rollouts

A real Hermes + SWE-bench Pro rollout through `single_agent_turn_legacy`
resolved its task (reward `1.0`, 300 of 300 tests) with the flat verify
body under a typed session, with an empty failure sidecar and no
leftover sandboxes. Its record carries the task fields, as a `/run`
record does.

## Compatibility and benchmark impact

Legacy SWE-bench Pro request bodies remain supported. Existing Resources
Servers verify single-agent-turn episodes without changes.
`SingleAgentTurnVerificationInput` and
`SingleAgentTurnResourcesVerifyRequest` are removed from
`nemo_gym.single_agent_turn_types`; they landed in NVIDIA-NeMo#3717 and had no
users outside this stack.

---------

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
lafrancef added a commit that referenced this pull request Sep 30, 2026
…r stack

Rebase the TB4 full-swapping integration onto main 17add3c, including
mini-SWE refactor #3680 and the merged EnvironmentServer replacement stack
#3717, #3718, #3720, #3721, and #3722.

Adapt native OpenCode session support from #3639 (dce7f83) and Hermes
session hardening from #3554 (6dc9c53). Keep existing legacy entrypoints.

Add optional resource-resolved task context, caller-assigned native TB4
sessions, a generic swappable profile, and lifecycle-audited CPU/GPU/Compose
smokes. Preserve the mini-SWE CLI/Responses flow while supervising all
agent descendants before verification. Honor task users and limits, select
sandbox-compatible bootstrap binaries, and avoid an unnecessary root UID
switch when preparing the already-executable TB4 verifier.

Bind native TB4 close to the merged HTTP hook and confirm destruction,
allowing retries after provider deletion failures. Route Hermes iteration-limit
summaries through its regular request formatter, preserving reasoning, history,
and request settings without leaking internal Responses IDs into chat requests.

Signed-off-by: Frederic Lafrance <flafrance@nvidia.com>
yuhezhang-ai pushed a commit to ManoharPaturi/Gym that referenced this pull request Oct 1, 2026
NVIDIA-NeMo#3812)

## What

`interactive_browser` now serves the resources-session protocol an
Environment Server uses (NVIDIA-NeMo#3717): a typed `/seed_session` under the
caller's `resources_session_id`, and a `/close_session` that actually
releases the browser.

This is the follow-up I mentioned when closing NVIDIA-NeMo#2612.

## Why

Two gaps under Environment Server episodes today:

1. **Typed seed is swallowed.** `BrowserSeedSessionRequest` allows extra
fields, so a `ResourcesSeedSessionRequest` body validates as a legacy
seed and gets an empty `{}` back. `single_agent_turn` then fails
validating the seed response.
2. **Close is a no-op.** `/close_session` falls through to the base
default, so an episode that ends before `verify` (timeout, cancelled
agent) keeps its browser. With a remote provider that is a paid session
held until the provider reclaims it.

## How

Same shape as `swebench_pro`:

- `seed_session` takes `Union[ResourcesSeedSessionRequest,
BrowserSeedSessionRequest]` with `union_mode="left_to_right"`, so the
typed body wins. It opens the browser from `task_data` under the
caller's id and binds the cookie to that id, so direct-HTTP tool calls
and the MCP token both land on that browser. A retried seed for the same
`(episode_id, task_id)` returns the existing session; a different
episode on the same id is rejected.
- `close_resources_session` releases the browser, is idempotent, checks
`episode_id`, and remembers closed ids. A seed that arrives after its
close is rejected, so it can't open a browser nobody will close. An
empty body still closes the cookie session for `/run` callers.
- A per-id lock serializes seed and close. A close that arrives while
the browser is still opening waits for it, then releases it.
- `num_workers` other than 1 is refused, since browsers live in process.

Legacy `/run` seeding is unchanged and still returns `{}`.

## Tests

`tests/test_resources_sessions.py` (15 new): typed seed over HTTP, tool
calls reaching the seeded browser over direct HTTP and over MCP, retried
seed, id reuse across episodes, close before verify, double close,
mismatched episode, malformed close (422), legacy seed and empty close,
close during a slow seed, seed after close, `num_workers` guard, verify
after close.

```
cd resources_servers/interactive_browser && pytest tests -q   # 49 passed
pytest tests/unit_tests/test_mcp_auto_exposure.py tests/unit_tests/test_base_resources_server.py -q   # 60 passed
```

## Not in this PR

Idle expiry for sessions whose Environment Server never closes them
(process killed mid-episode). I'll ask on NVIDIA-NeMo#3037 whether that belongs in
the base class or per server before opening anything.

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
Co-authored-by: waple0820 <232305951+waple0820@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aroshanghias-nvd pushed a commit to aroshanghias-nvd/Gym that referenced this pull request Oct 16, 2026
Stage the portable NOOA runtime for borrowed task sandboxes. This layer
now builds on the standard single-agent-turn session integration.

This is part of the existing NOOA integration stack, based on NVIDIA-NeMo#3848; no
separate issue is needed for this requested architecture revision.
Shared/base environment servers, shared agent contracts, generic sandbox
code, and other harnesses remain fixed.

Patterns adopted from merged NVIDIA-NeMo#3717 (single-agent-turn lifecycle),
NVIDIA-NeMo#3554/NVIDIA-NeMo#3961 (Hermes sessions and sandbox cleanup), NVIDIA-NeMo#3640 (Codex), NVIDIA-NeMo#3626
(Pi), and open NVIDIA-NeMo#4560 (borrowed DeepSWE/OpenCode sandbox): seed
Resources, seed agent, activate, close agent and collect
evidence/cookies, verify, close Resources.

Compatibility: in sandboxed NOOA, agent-owned background services stop
before verification; the Resources-owned sandbox and persisted artifacts
remain available. Tasks requiring a NOOA-started service during grading
are unsupported by this lifecycle. The standard contract has an
episode-wide deadline and no separate NOOA agent deadline;
Terminal-Bench recipes retain verifier timeouts but do not reproduce
canonical per-agent deadlines.

Current revision: `0efed19193e1bb3e906318a6c6486f64078188ee`.

Validation evidence:

Implementation pushed before writing or running new behavior tests or
local checks, as requested. Existing tests received only mechanical
naming/import updates before this push. Checks, exact-layer tests,
live-model smoke rollouts, and current-head CI are pending for this
revision. Earlier test and CI evidence applies only to earlier revisions
and does not validate this change. No new benchmark scores are claimed.

## Tests and validation

Current revision: `a55f1eddbf13448c30728dafd17ef0af8ac26dbc`. Rebased
the existing ten-PR stack onto main
`9ef5ed0f80a9f81c5d82af6588cd4cc956c4f5d1`; assembled tip is
`196bd5e333abd6e8ee7a93f4c8805ae33f67a66a` (NVIDIA-NeMo#4357).

Main compatibility adds only two test updates: NVIDIA-NeMo#3846 gives the fake HTTP
response its headers mapping for checkpoint-aware Environment seeding;
NVIDIA-NeMo#4088 checks serialized tool-output snapshots while retaining history,
round-trip, and interpreter-object coverage. Both changes were
independently reviewed. The rebase preserves upstream NOOA serialization
and runtime configuration changes.

Validation on the exact rebased revisions:
- NVIDIA-NeMo#3843: **170 passed** (one expected skip-verification
aggregate-metrics warning).
- NVIDIA-NeMo#3846: **195 passed**.
- NVIDIA-NeMo#3844: **218 passed, 8 Linux-only installer skips** on macOS.
- Assembled NVIDIA-NeMo#4357 tip: **373 passed, 16 platform skips** (10 installer
and 6 Linux supervisor tests).
- `pre-commit run --all-files` passed. DCO sign-offs, sequential layer
ancestry, and the focused final diff were checked.

Command from each revision's `responses_api_agents/nooa_agent`: `python
-m pytest tests --import-mode=importlib -q -o addopts= -o log_cli=false
--tb=short -rs`. Exact early-layer tests use installed NOOA
`051472343211914222e24ce36d8752f4e86bbe43`; the assembled tip uses
coherent core/CLI/BenchAgent `19caab169b018476ac433d040f6ae3f06aeff101`.
Commands and logs:
`/tmp/nooa-merge-main-candidate-20261016/final-tests-{3843,3846,3844,4357}/`.

Prior real-model evidence for the unchanged reviewer-fix behavior
remains at `/tmp/nooa-review-fixes-20261016/live-final/`, run on
pre-rebase tip `8185eb1f405ba476aab42b00a222f92dbec4364f`: real Qwen
output-token and iteration exhaustion each verified once; an actual
iteration limit followed by injected cleanup failure remained fatal,
skipped verification, and closed both sessions. This main sync adds test
compatibility rather than new agent behavior, so that smoke was not
rerun and is not presented as a smoke of the rebased tree. It used the
integration-test Resources fixture and was not a new
SWE-bench/OpenSandbox canary.

GitHub CI is requested for the new heads. Optional cross-harness
conformance failures also reproduce on main and are separate from the
required `Nemo_CICD_Test` gate. Existing branch names, bases, draft
states, native groups, and description text outside this validation
section are preserved.

Required [branch
CI](https://github.com/NVIDIA-NeMo/Gym/actions/runs/38964312169) passed
on this revision: 8,793 core tests, 921 sandbox tests, 95.07% coverage,
and 226 NOOA tests (including Linux installer tests for NVIDIA-NeMo#3844).

Signed-off-by: Nikhil Deshmudre <nikhil.deshmudre@gmail.com>

This branch was previously deployed

1 inactive deployment
public β€” 81a49c93 Deployed Sep 28, 2026 by copy-pr-bot[bot] via release / finalize / notify #3784
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:env-infra Shared environment framework, lifecycle, registry, scaffolding, and validation complexity:high Cross-component or shared-contract change with a large review surface feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants