Skip to content

feat(config): support environment servers that front several agents - #4053

Merged
ananthsub merged 7 commits into
NVIDIA-NeMo:mainfrom
ananthsub:fix-migration-renamed-agent-server
Oct 6, 2026
Merged

ananthsub merged 7 commits into
NVIDIA-NeMo:mainfrom
ananthsub:fix-migration-renamed-agent-server

Conversation

@ananthsub

@ananthsub ananthsub commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Makes Gym and its migration tooling agree on which agents an environment server fronts, including servers that front several agents. It combines the runtime change from #4059 with fixes to scripts/add_legacy_agent_environment_servers.py, the ServerRefNotFoundError rename hint, and the troubleshooting docs, so the environment server and agent server changes land together. The same commits are in the r0.7.0 cherry-pick of #3929 (#4050).

Where this fits

Datasets reach an environment server in one of two ways today: run request rows route by their agent, and datasets that declare taskset: are materialized during collation and route by environment_server_routes. The goal is one path, where every dataset declares a taskset and routes to an environment server:

flowchart LR
    P["Preparation script<br/>task rows with a stable task_id"] --> C["Collation<br/>validates, computes metrics,<br/>materializes"]
    Y["Dataset YAML<br/>file + taskset"] --> C
    S["task_data.py schemas<br/>environment server + resources server"] -.-> C
    C --> R["Rollout collection<br/>routes by taskset"]
    T["environment_server_routes"] --> R
    R --> E["Environment server<br/>single-agent, multi-agent,<br/>or legacy_agent relay"]
Loading

This PR is one step on the way there. Each step is additive, and datasets without taskset: keep working until the last one:

flowchart TB
    S3818["PR 3818 (merged)<br/>collation materializes<br/>declared tasksets"]
    S4053["PR 4053<br/>tasksets route to servers<br/>with several agents"]
    S4096["PR 4096<br/>stable, unique<br/>task IDs"]
    S4097["PR 4097<br/>metrics for<br/>every row"]
    S4098["PR 4098<br/>schema per<br/>environment server"]
    S4099["PR 4099<br/>legacy_agent relays<br/>serve tasksets"]
    SNEXT["Later<br/>configs declare taskset:,<br/>then agent routing retires"]
    S3818 --> S4053 --> S4096 --> S4097 --> S4098 --> S4099 --> SNEXT
    classDef done fill:#F1EFE8,stroke:#5F5E5A,color:#444441
    classDef this fill:#EEEDFE,stroke:#534AB7,color:#26215C,stroke-width:2px
    classDef next fill:#E1F5EE,stroke:#0F6E56,color:#085041
    class S3818 done
    class S4053 this
    class S4096,S4097,S4098,S4099,SNEXT next
Loading

Gray: merged. Purple: this PR. Green: other steps.

Environment servers that front several agents

An environment server can front several agents, such as the user and assistant agents of the user-simulation server in #3800. It references them through fields such as user_agent and assistant_agent. Gym only recognized the agent_server field. For such a server:

The first commit from #4059 (authored by Felipe Vieira Frujeri) adds environment_server_agent_refs to nemo_gym/global_config.py. Any top-level field whose value has type: responses_api_agents references an agent, and so does agent_server, whose type may be left out. The deprecation check, agent routing, and _retarget_environment_servers (which follows an agent swapped during composition) use it.

Two places in nemo_gym/rollout_collection.py still read only agent_server:

  • _agent_name_for_row names the agent a row's results are attributed to. It decides whether token IDs are captured and labels aggregate metrics. Results are now attributed to the server's agent_server, or to the only agent it references. A server that fronts several agents without an agent_server has no single agent to attribute, as before.
  • _validate_environment_servers rejects a row whose agent_ref names an agent the server does not front. It skipped the check for any server without agent_server. A row may now name any agent its server references.

The migration script

The script assumed an environment server references one agent, through agent_server. It now finds agent references with the same rule as environment_server_agent_refs. The script depends only on PyYAML, so it keeps its own copy of the rule, and a test checks that both agree. Each referenced agent counts as fronted, so the script no longer adds relays in front of agents a multi-agent server already fronts, such as those in resources_servers/usersim/configs/usersim.yaml from #3800.

For an agent renamed with _inherit_from, the renamed agent's server used to inherit from the name the script would generate for the source, always as a legacy_agent relay. That failed in three ways:

  • The source's server has a custom name: the inherited name does not exist, raising InheritPathNotFoundError.
  • The source's server is single_agent_turn_legacy: the inherited server gains a second legacy_agent block without an entrypoint, raising AlmostServerError.
  • Inheriting retires the source server's name, which environment_server_routes may still use. For example, usersim routes its tasksets to usersim_environment.

The script now handles a rename this way:

  • Where a declared server references the source, the script points that reference at the new name, on the existing server. The server keeps its name, so routes and the server's other agents are unaffected. Renaming several agents of one server writes one block for that server.
  • Where the source has only the server the script generates, the renamed agent's server inherits it, as before.
  • A rename in the same file as a server that references its source cannot be migrated by appending, because the server's key would repeat. The script reports the field to update by hand and exits with 2.

The renamed-agent hint

When an environment server references an agent that no longer exists, ServerRefNotFoundError suggests the agent may have been renamed with _inherit_from. The hint said the environment server "must be renamed with it", which no longer matches the script.

  • The hint now says the environment server must reference the agent's new name, and names the field to update instead of always agent_server.name.
  • It is shown for any agent reference on an environment server, not only agent_server.
  • fern/versions/latest/pages/troubleshooting/configuration.mdx describes the same rename behavior, and the script's exit code 2 for a rename that needs a manual update.

Declared tasksets routed to a multi-agent server

#3818 added resolve_dataset_agent's taskset path, used by benchmark discovery, gym eval prepare, manifest validation, and collation. It read only the routed environment server's agent_server, so a taskset routed to a server that fronts several agents failed with "must bind a valid agent_server".

A taskset routes to an environment server, not to an agent: rollout collection sends materialized rows to the server that environment_server_routes names and never uses an agent. So resolution now checks the route instead of choosing an agent:

  • Every agent the server references, through environment_server_agent_refs, must exist.
  • A dataset declared on an agent must be one of those agents.
  • A dataset declared on a resources server must be the one the server binds.
  • A dataset with taskset: cannot set agent:. Only benchmark datasets accept agent:, so requiring a pin would have left example and train datasets for a server with several agents unable to collate, and would have made an environment server that owns its dataset name one of its own agents.

It returns the declaring agent, or the server's attributed agent (its agent_server, or its only agent), or none when the server fronts several agents without an agent_server. environment_server_agent_names and environment_server_attributed_agent move to global_config.py so resolution and rollout attribution share that rule. Benchmarks record the environment server, and gym eval list shows it when there is no single agent. Environment manifest validation reports that a manifest, which describes one agent, cannot describe such a taskset. resources-server-apis.mdx describes the route check.

This supersedes #4059. No issue is linked. It follows up #3929, from review on #4050, and provides the runtime support #3800 needs.

Validation

  • tests/unit_tests/test_global_config.py: a multi-agent server fronts all of its agents and needs no relays, removing one agent's reference makes that agent unfronted, and a composition swap retargets only the swapped agent's reference. The rename hint test runs for an agent_server and a user_agent reference.
  • tests/unit_tests/test_rollout_collection.py: attribution to agent_server, to a server's only agent, and to no agent for a server with several, and row validation accepting any agent of the server and rejecting others.
  • tests/unit_tests/test_add_legacy_agent_environment_servers.py: migrate-then-parse tests, which run the script and parse the result with GlobalConfigDictParser. They cover a rename whose source server is generated, custom-named, or single_agent_turn_legacy, a two-agent server the script must leave alone, renaming both agents of that server (parsed with error_on_agent_without_environment_server: true), and a rename in the same file as the server it would update. A test checks that the script and environment_server_agent_refs find the same references.
  • tests/unit_tests/test_benchmarks.py: a multi-agent taskset resolves without a pin, to none or to the server's agent_server; any pin on a taskset dataset is rejected; a reference to a non-agent is rejected; a declaring participant is accepted; benchmark, example, and train datasets collate and route end to end; the benchmark listing names the environment server; and manifest validation reports a multi-agent taskset. These tests fail without the change.
  • pytest tests/unit_tests/test_global_config.py tests/unit_tests/test_add_legacy_agent_environment_servers.py tests/unit_tests/test_rollout_collection.py tests/unit_tests/test_environment_scaffold.py tests/unit_tests/test_benchmarks.py tests/unit_tests/test_task_materialization.py: all pass.
  • pytest tests/unit_tests: the tests that fail in this environment fail identically on main. They need the sandbox and telemetry extras, or Slurm.
  • With feat(nemo-user-sim): add materialized-row runtimeΒ #3800 merged locally: the script leaves resources_servers/usersim/configs/usersim.yaml unchanged. After renaming usersim_assistant, the usersim config parses with error_on_agent_without_environment_server: true, usersim_environment fronts both agents, and both usersim routes still name an existing server.
  • python scripts/add_legacy_agent_environment_servers.py --check on this repository reports the same files and blocks before and after this change.
  • pre-commit run on the changed files: all pass.

Rollouts

N/A. Single-agent servers resolve, route, and attribute exactly as before, and no configuration in this repository changes. The new behavior applies only to servers that reference agents through fields other than agent_server. None exist on main until #3800, which carries its own rollout evidence. The migration script runs offline.

Compatibility and benchmark impact

  • Environment servers that front several agents no longer get generated relays, and their agents route to them.
  • The error for a row that names the wrong agent for its environment server now lists the server's agents.
  • A dataset that declares taskset: can no longer set agent:. feat(data): collate declared tasksetsΒ #3818 accepted a pin that matched the routed agent; no configuration in this repository declares a taskset.
  • gym eval list --json adds an environment_server field, and agent_name is null for a taskset routed to a server with several agents.
  • The migration script updates a declared server in place when an agent is renamed, so server names and routes no longer change.
  • No benchmark impact.

@ananthsub ananthsub added bug Something isn't working area:config Hydra configuration, schemas, composition, and compatibility complexity:low Localized change in one scope with a small, straightforward review surface labels Oct 5, 2026
@nemo-automation-bot
nemo-automation-bot Bot requested review from a team October 5, 2026 17:32
@ananthsub
ananthsub requested a review from yaoyu-33 October 5, 2026 17:34
@ananthsub ananthsub changed the title fix(config): inherit the declared environment server when migrating a renamed agent fix(config): migrate renamed agents and multi-agent environment servers Oct 5, 2026
@nemo-automation-bot
nemo-automation-bot Bot requested review from a team October 5, 2026 18:13
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

ffrujeri
ffrujeri previously approved these changes Oct 5, 2026
@nemo-automation-bot
nemo-automation-bot Bot requested a review from a team October 5, 2026 19:06
@ananthsub ananthsub changed the title fix(config): migrate renamed agents and multi-agent environment servers feat(config): support environment servers that front several agents Oct 5, 2026
@ananthsub ananthsub added the r0.7.0 Auto-cherrypick to release branch. Apply before merge; cherrypick happens after merge. label Oct 6, 2026
@ananthsub
ananthsub added this pull request to the merge queue Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

πŸ”„ Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/37490986888

Merged via the queue into NVIDIA-NeMo:main with commit 89c065b Oct 6, 2026
45 checks passed
@ananthsub
ananthsub deleted the fix-migration-renamed-agent-server branch October 6, 2026 16:06
ananthsub added a commit to ananthsub/Gym that referenced this pull request Oct 6, 2026
…VIDIA-NeMo#4053)

## What does this PR do?

Makes Gym and its migration tooling agree on which agents an environment
server fronts, including servers that front several agents. It combines
the runtime change from NVIDIA-NeMo#4059 with fixes to
`scripts/add_legacy_agent_environment_servers.py`, the
`ServerRefNotFoundError` rename hint, and the troubleshooting docs, so
the environment server and agent server changes land together. The same
commits are in the `r0.7.0` cherry-pick of NVIDIA-NeMo#3929 (NVIDIA-NeMo#4050).

## Where this fits

Datasets reach an environment server in one of two ways today: run
request rows route by their agent, and datasets that declare `taskset:`
are materialized during collation and route by
`environment_server_routes`. The goal is one path, where every dataset
declares a taskset and routes to an environment server:

```mermaid
flowchart LR
    P["Preparation script<br/>task rows with a stable task_id"] --> C["Collation<br/>validates, computes metrics,<br/>materializes"]
    Y["Dataset YAML<br/>file + taskset"] --> C
    S["task_data.py schemas<br/>environment server + resources server"] -.-> C
    C --> R["Rollout collection<br/>routes by taskset"]
    T["environment_server_routes"] --> R
    R --> E["Environment server<br/>single-agent, multi-agent,<br/>or legacy_agent relay"]
```

This PR is one step on the way there. Each step is additive, and
datasets without `taskset:` keep working until the last one:

```mermaid
flowchart TB
    S3818["PR 3818 (merged)<br/>collation materializes<br/>declared tasksets"]
    S4053["PR 4053<br/>tasksets route to servers<br/>with several agents"]
    S4096["PR 4096<br/>stable, unique<br/>task IDs"]
    S4097["PR 4097<br/>metrics for<br/>every row"]
    S4098["PR 4098<br/>schema per<br/>environment server"]
    S4099["PR 4099<br/>legacy_agent relays<br/>serve tasksets"]
    SNEXT["Later<br/>configs declare taskset:,<br/>then agent routing retires"]
    S3818 --> S4053 --> S4096 --> S4097 --> S4098 --> S4099 --> SNEXT
    classDef done fill:#F1EFE8,stroke:#5F5E5A,color:#444441
    classDef this fill:#EEEDFE,stroke:#534AB7,color:#26215C,stroke-width:2px
    classDef next fill:#E1F5EE,stroke:#0F6E56,color:#085041
    class S3818 done
    class S4053 this
    class S4096,S4097,S4098,S4099,SNEXT next
```

Gray: merged. Purple: this PR. Green: other steps.

### Environment servers that front several agents

An environment server can front several agents, such as the user and
assistant agents of the user-simulation server in NVIDIA-NeMo#3800. It references
them through fields such as `user_agent` and `assistant_agent`. Gym only
recognized the `agent_server` field. For such a server:

- The deprecation check from NVIDIA-NeMo#3929 treated its agents as unfronted and
generated a `legacy_agent` relay in front of each.
- Agent-routed rows could not find the server, because
`_environment_servers_by_agent` only read `agent_server`.

The first commit from NVIDIA-NeMo#4059 (authored by Felipe Vieira Frujeri) adds
`environment_server_agent_refs` to `nemo_gym/global_config.py`. Any
top-level field whose value has `type: responses_api_agents` references
an agent, and so does `agent_server`, whose type may be left out. The
deprecation check, agent routing, and `_retarget_environment_servers`
(which follows an agent swapped during composition) use it.

Two places in `nemo_gym/rollout_collection.py` still read only
`agent_server`:

- `_agent_name_for_row` names the agent a row's results are attributed
to. It decides whether token IDs are captured and labels aggregate
metrics. Results are now attributed to the server's `agent_server`, or
to the only agent it references. A server that fronts several agents
without an `agent_server` has no single agent to attribute, as before.
- `_validate_environment_servers` rejects a row whose `agent_ref` names
an agent the server does not front. It skipped the check for any server
without `agent_server`. A row may now name any agent its server
references.

### The migration script

The script assumed an environment server references one agent, through
`agent_server`. It now finds agent references with the same rule as
`environment_server_agent_refs`. The script depends only on PyYAML, so
it keeps its own copy of the rule, and a test checks that both agree.
Each referenced agent counts as fronted, so the script no longer adds
relays in front of agents a multi-agent server already fronts, such as
those in `resources_servers/usersim/configs/usersim.yaml` from NVIDIA-NeMo#3800.

For an agent renamed with `_inherit_from`, the renamed agent's server
used to inherit from the name the script would generate for the source,
always as a `legacy_agent` relay. That failed in three ways:

- The source's server has a custom name: the inherited name does not
exist, raising `InheritPathNotFoundError`.
- The source's server is `single_agent_turn_legacy`: the inherited
server gains a second `legacy_agent` block without an entrypoint,
raising `AlmostServerError`.
- Inheriting retires the source server's name, which
`environment_server_routes` may still use. For example, usersim routes
its tasksets to `usersim_environment`.

The script now handles a rename this way:

- Where a declared server references the source, the script points that
reference at the new name, on the existing server. The server keeps its
name, so routes and the server's other agents are unaffected. Renaming
several agents of one server writes one block for that server.
- Where the source has only the server the script generates, the renamed
agent's server inherits it, as before.
- A rename in the same file as a server that references its source
cannot be migrated by appending, because the server's key would repeat.
The script reports the field to update by hand and exits with 2.

### The renamed-agent hint

When an environment server references an agent that no longer exists,
`ServerRefNotFoundError` suggests the agent may have been renamed with
`_inherit_from`. The hint said the environment server "must be renamed
with it", which no longer matches the script.

- The hint now says the environment server must reference the agent's
new name, and names the field to update instead of always
`agent_server.name`.
- It is shown for any agent reference on an environment server, not only
`agent_server`.
- `fern/versions/latest/pages/troubleshooting/configuration.mdx`
describes the same rename behavior, and the script's exit code 2 for a
rename that needs a manual update.

### Declared tasksets routed to a multi-agent server

NVIDIA-NeMo#3818 added `resolve_dataset_agent`'s taskset path, used by benchmark
discovery, `gym eval prepare`, manifest validation, and collation. It
read only the routed environment server's `agent_server`, so a taskset
routed to a server that fronts several agents failed with "must bind a
valid agent_server".

A taskset routes to an environment server, not to an agent: rollout
collection sends materialized rows to the server that
`environment_server_routes` names and never uses an agent. So resolution
now checks the route instead of choosing an agent:

- Every agent the server references, through
`environment_server_agent_refs`, must exist.
- A dataset declared on an agent must be one of those agents.
- A dataset declared on a resources server must be the one the server
binds.
- A dataset with `taskset:` cannot set `agent:`. Only benchmark datasets
accept `agent:`, so requiring a pin would have left example and train
datasets for a server with several agents unable to collate, and would
have made an environment server that owns its dataset name one of its
own agents.

It returns the declaring agent, or the server's attributed agent (its
`agent_server`, or its only agent), or none when the server fronts
several agents without an `agent_server`.
`environment_server_agent_names` and
`environment_server_attributed_agent` move to `global_config.py` so
resolution and rollout attribution share that rule. Benchmarks record
the environment server, and `gym eval list` shows it when there is no
single agent. Environment manifest validation reports that a manifest,
which describes one agent, cannot describe such a taskset.
`resources-server-apis.mdx` describes the route check.

This supersedes NVIDIA-NeMo#4059. No issue is linked. It follows up NVIDIA-NeMo#3929, from
review on NVIDIA-NeMo#4050, and provides the runtime support NVIDIA-NeMo#3800 needs.

## Validation

- `tests/unit_tests/test_global_config.py`: a multi-agent server fronts
all of its agents and needs no relays, removing one agent's reference
makes that agent unfronted, and a composition swap retargets only the
swapped agent's reference. The rename hint test runs for an
`agent_server` and a `user_agent` reference.
- `tests/unit_tests/test_rollout_collection.py`: attribution to
`agent_server`, to a server's only agent, and to no agent for a server
with several, and row validation accepting any agent of the server and
rejecting others.
- `tests/unit_tests/test_add_legacy_agent_environment_servers.py`:
migrate-then-parse tests, which run the script and parse the result with
`GlobalConfigDictParser`. They cover a rename whose source server is
generated, custom-named, or `single_agent_turn_legacy`, a two-agent
server the script must leave alone, renaming both agents of that server
(parsed with `error_on_agent_without_environment_server: true`), and a
rename in the same file as the server it would update. A test checks
that the script and `environment_server_agent_refs` find the same
references.
- `tests/unit_tests/test_benchmarks.py`: a multi-agent taskset resolves
without a pin, to none or to the server's `agent_server`; any pin on a
taskset dataset is rejected; a reference to a non-agent is rejected; a
declaring participant is accepted; benchmark, example, and train
datasets collate and route end to end; the benchmark listing names the
environment server; and manifest validation reports a multi-agent
taskset. These tests fail without the change.
- `pytest tests/unit_tests/test_global_config.py
tests/unit_tests/test_add_legacy_agent_environment_servers.py
tests/unit_tests/test_rollout_collection.py
tests/unit_tests/test_environment_scaffold.py
tests/unit_tests/test_benchmarks.py
tests/unit_tests/test_task_materialization.py`: all pass.
- `pytest tests/unit_tests`: the tests that fail in this environment
fail identically on `main`. They need the `sandbox` and telemetry
extras, or Slurm.
- With NVIDIA-NeMo#3800 merged locally: the script leaves
`resources_servers/usersim/configs/usersim.yaml` unchanged. After
renaming `usersim_assistant`, the usersim config parses with
`error_on_agent_without_environment_server: true`, `usersim_environment`
fronts both agents, and both usersim routes still name an existing
server.
- `python scripts/add_legacy_agent_environment_servers.py --check` on
this repository reports the same files and blocks before and after this
change.
- `pre-commit run` on the changed files: all pass.

## Rollouts

N/A. Single-agent servers resolve, route, and attribute exactly as
before, and no configuration in this repository changes. The new
behavior applies only to servers that reference agents through fields
other than `agent_server`. None exist on `main` until NVIDIA-NeMo#3800, which
carries its own rollout evidence. The migration script runs offline.

## Compatibility and benchmark impact

- Environment servers that front several agents no longer get generated
relays, and their agents route to them.
- The error for a row that names the wrong agent for its environment
server now lists the server's agents.
- A dataset that declares `taskset:` can no longer set `agent:`. NVIDIA-NeMo#3818
accepted a pin that matched the routed agent; no configuration in this
repository declares a taskset.
- `gym eval list --json` adds an `environment_server` field, and
`agent_name` is null for a taskset routed to a server with several
agents.
- The migration script updates a declared server in place when an agent
is renamed, so server names and routes no longer change.
- No benchmark impact.

---------

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
Co-authored-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
(cherry picked from commit 89c065b)
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
ananthsub added a commit that referenced this pull request Oct 6, 2026
…vers (#3818, #4070, #3929, #4053) into `r0.7.0` (#4107)

## What

This PR only cherry-picks four already-merged `main` PRs into `r0.7.0`.
It adds no new code. The commits are applied in their `main` merge order
with `git cherry-pick -x`, so each commit message records its source
commit.

| Original PR | Commit on `main` | Change |
|---|---|---|
| #3818 | `325fb49d6` |
feat(data): collate declared tasksets |
| #4070 | `412efc730` | refactor:
name episode requests and sessions without "native" |
| #3929 | `1f2446c17` |
feat(config): deprecate agents without an environment server and fix the
migration script |
| #4053 | `89c065b0e` |
feat(config): support environment servers that front several agents |

## Why

#4070 and #4053 carry the `r0.7.0` label, but the automatic cherry-pick
workflow could not apply them: both hit merge conflicts on `r0.7.0`. The
workflow still reports success, so the failure was silent. #3818 was
intended for `r0.7.0` but never had the label.

#3929 is included because #4053 builds on it. Every #4053 conflict came
from #3929 changes to `nemo_gym/global_config.py`,
`scripts/add_legacy_agent_environment_servers.py`, and their tests. With
#3929 applied first, #4053 applies cleanly.

## Conflict resolution

Picking #3818 first removed one #4070 conflict. The remaining conflicts
were in tests that other PRs added on `main` after the branch cut:

- **#4070**: renames identifiers, comments, and test names; no behavior
change. Where the conflict was a test that exists only on `main`
(`single_agent_turn`, SWE-bench Pro, and Hermes
`test_agent_sessions.py`), the `r0.7.0` version is kept and the
`main`-only test is not brought in. In `single_agent_turn_legacy`, the
existing test is renamed to
`test_flat_rows_and_episode_requests_project_the_same_result` to match
`main`, and the `task_source` tests from a separate PR are left out.
- **#3929**: one import line in
`tests/unit_tests/test_global_config.py`. `main` also imports
`hide_secrets_in_overrides`, added by
#3679 and unused in this file, so
the `r0.7.0` import is kept.
- **#3818** and **#4053**: applied cleanly.

## Validation

- `pytest tests/unit_tests/test_global_config.py
tests/unit_tests/test_add_legacy_agent_environment_servers.py
tests/unit_tests/test_rollout_collection.py
tests/unit_tests/test_environment_server.py
tests/unit_tests/test_task_materialization.py
tests/unit_tests/test_benchmarks.py`: pass.
- `pytest environment_servers/single_agent_turn/tests` and `pytest
environment_servers/single_agent_turn_legacy/tests` (run separately):
pass.
- `pytest tests/unit_tests -n 8`: same failures as unmodified `r0.7.0`
and no new ones. The existing failures are local-environment tests in
`test_opensandbox_compose.py`, `test_sandbox_stop_retry.py`, and
`test_slurm_script.py`.
- `pre-commit run --files` on all changed files: pass.
- Checked for old names #4070 renames (`_native_request`,
`_native_episode_request_body`, `_native_session_locks`,
`_closed_native_sessions`): none remain. The few remaining "native row"
comments are also on `main`.
- SWE-bench Pro and Hermes server tests were not run locally (they need
per-server venvs); release-branch CI covers them.

## Rollout evidence

N/A. Each change was reviewed and validated on `main`. This PR only
carries them to the release branch: #4070 is a rename with no behavior
change, and the config changes are covered by the unit tests above.

---------

Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
Co-authored-by: Yu Yao <54727607+yaoyu-33@users.noreply.github.com>
Co-authored-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
ananthsub added a commit that referenced this pull request Oct 6, 2026
main with #4053 and #4060 merged, plus the simplified #3800 (UserSim's
resources server, preparation, and simple_agent changes; no shared data code).
#3801, #4080, and #3802 are left out until they are restacked onto the
simplified #3800, because they still carry the previous #3800.

This branch only previews that base for review of the task-schema stack.
It is replaced by main once the UserSim stack merges.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
ananthsub added a commit that referenced this pull request Oct 6, 2026
…ions (#3554, #3784) into `r0.7.0` (#4113)

## What

This PR only cherry-picks two already-merged `main` PRs into `r0.7.0`.
It adds no new code. Each commit is applied with `git cherry-pick -x`,
so its message records the source commit.

| Original PR | Commit on `main` | Change |
|---|---|---|
| #3554 | `b846dfaa5` |
fix(hermes): harden sandbox sessions |
| #3784 | `e0e63e526` |
feat(simple-agent): run simple agent through environment server sessions
|

This branch is based on the current `r0.7.0`, which already includes
#4107 (#3818, #4070, #3929,
#4053) and #4078.

## Why

#3784 has the `r0.7.0` label, but the automatic cherry-pick workflow
could not apply it: it hit a merge conflict in
`nemo_gym/base_responses_api_agent.py` and reported success anyway, so
nothing was opened.

#3554 is included because #3784 builds on it. #3554 added the agent
session handling (seed and close retries, session locks, close receipts)
in `nemo_gym/base_responses_api_agent.py` that #3784 extends. With #3554
applied first, the conflict in that file goes away. #3554 has no
`r0.7.0` label; it is included on purpose.

### Behavior changes from #3554

#3554's own description lists these, and they now apply on `r0.7.0` too:

- Hermes host execution now combines the config system prompt, request
instructions, and input system message instead of letting the config
prompt replace the dataset prompt. It honors the request temperature,
including zero, and rejects `developer` messages with 422. These changes
can change benchmark scores for Hermes on the host path.
- Hermes rejects unsupported non-default controls with a permanent 422,
including `top_p=1.0`, `store`, `service_tier`, metadata overrides,
`max_output_tokens`, and a `model` that does not match the configured
model.

## Conflict resolution

- **#3554**: applied cleanly.
- **#3784**: one conflict in
`environment_servers/single_agent_turn_legacy/tests/test_app.py`. #3784
adds two `task_source` tests next to a test that #4070 (already on
`r0.7.0` through #4107) renamed. The resolution keeps both, and the new
tests call the adapter method by its post-#4070 name,
`_episode_request_from_row`. The resulting file is identical to the file
on `main`.

## Validation

- `pytest tests/unit_tests -n 8`: same failures as unmodified `r0.7.0`
and no new ones. The existing failures are local-environment tests in
`test_opensandbox_compose.py`, `test_sandbox_stop_retry.py`, and
`test_slurm_script.py`.
- #3554's focused suite, each path run separately with
`--import-mode=importlib`:
`environment_servers/single_agent_turn/tests`,
`environment_servers/single_agent_turn_legacy/tests`,
`environment_servers/legacy_agent/tests`,
`responses_api_agents/hermes_sandboxed_agent/tests`,
`resources_servers/swebench_pro/tests`, and
`tests/unit_tests/test_{episode_types,environment_server,rollout_outcomes,failure_kinds,agent_session_retries,agent_sessions,base_responses_api_agent,process_supervisor,local_provider}.py`:
all pass.
- `responses_api_agents/hermes_agent/tests` in its own venv from
`requirements.txt`: all pass.
- `responses_api_agents/simple_agent/tests` and
`resources_servers/example_single_tool_call/tests`: all pass.
- `pre-commit run --files` on all files changed by the two commits:
pass.

## Rollout evidence

N/A for the cherry-pick itself. Both changes are applied unchanged from
`main`, except for the one test-file conflict described above. #3554
includes a real-model Hermes with SWE-bench Pro lifecycle smoke rollout.

---------

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Signed-off-by: Marta Stepniewska-Dziubinska <martas@nvidia.com>
Signed-off-by: Adil Asif <adasif@nvidia.com>
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Co-authored-by: Yu Yao <54727607+yaoyu-33@users.noreply.github.com>
Co-authored-by: Marta Stepniewska-Dziubinska <martas@nvidia.com>
Co-authored-by: Adil Asif <adasif@nvidia.com>
yaoyu-33 added a commit to yaoyu-33/Gym that referenced this pull request Oct 6, 2026
…VIDIA-NeMo#3961)

Hermes can be composed with a Resources Server through Environment
Server. This PR decouples its default configuration and extracts a
shared sandbox execution lifecycle for Pi (NVIDIA-NeMo#3626), Codex (NVIDIA-NeMo#3640), and
OpenCode to adopt.

Implements [GYMINT-5](https://linear.app/nvidia/issue/GYMINT-5), follows
NVIDIA-NeMo#3554, and includes merged NVIDIA-NeMo#4053 from main. Only Hermes adopts the
shared session in this PR; the other adapters retain their own output
parsing when they migrate.

## Changes

- Keep shared execution code in `nemo_gym/agent_utils/`, the agreed
package name. This name preserves the agreed organization of the
lifecycle and protocol helpers. Core connection, exec, transfer and
provider lifecycle APIs remain in `nemo_gym/sandbox/`.
- `SandboxSession` uploads its supervisor into the session directory,
executes the harness, confirms cleanup and collects artifacts.
`SandboxCommand` carries only argv and an explicitly installed Python
interpreter. Preflight validates that interpreter before claiming
launch; Python on the task image is not assumed.
- Stage input and upload the supervisor in one cancellable task.
Concurrent/retried closes share work; closing/closed derive from the
close task. Execution retains the sandbox until explicit close. Borrowed
cleanup failures retain state for retry; owned sandboxes can fall back
to provider teardown.
- Use one timeout-budget helper, one set of protocol filenames, and one
cleanup receipt shape. Optional malformed diagnostics remain nonfatal;
literal positive cleanup confirmation is still required. Session log
access preserves diagnostics before files are released.
- Hermes separates seed-time `install_runtime()` from
`stage_activation()` and `collect_artifacts()`. Its process-identity
schema stays in the adapter. The runner receives the stop-marker path
from the session, preserving cancellation versus deadline reporting.
- Keep `upload_text()` / `read_text()` in `sandbox/utils.py`, with
Hemil's import/ordering cleanup. Remove unused byte helpers and their
tests as requested.
- Preserve native Hermes templates with explicit `resources_server:
null` during legacy Environment Server migration. Bound Hermes,
inherited Resources bindings and self-contained agents such as OSWorld
retain their existing migration behavior. This fixes the merge-queue
audit introduced by NVIDIA-NeMo#4084 without adding a conflicting legacy route.
- Default an owned sandbox's missing `ttl_s` to install timeout + runner
timeout + 600 seconds (23,100 seconds with default settings), so
providers that enforce TTL can expire it after an agent-server crash.
Preserve explicit values, including null, and borrowed connections; do
not mutate shared configuration.

## Architecture

From [Ananth's component
diagram](NVIDIA-NeMo#3961 (comment)):

```mermaid
flowchart LR
    subgraph AS["Agent server"]
        A["Harness adapter (Hermes)<br/>installs runtime, stages input, parses output"]
        S["Sandbox session<br/>uploads supervisor, runs, stops, collects, releases"]
        C["Supervisor client<br/>shell protocol over the session files"]
    end
    subgraph TS["Task sandbox"]
        D["Session directory<br/>claim, supervisor pid, stop request, receipt, log"]
        P["Process supervisor<br/>deadline, kills the process tree, writes receipt"]
        H["Harness process<br/>Hermes runner, Codex CLI"]
    end
    A -->|"argv and interpreter"| S
    S -->|"launch, stop, read receipt"| C
    C -->|"runs"| P
    C -->|"reads and writes"| D
    P -->|"starts and reaps"| H
    P -->|"writes receipt"| D
```

The session and supervisor client run on the agent server. Only
`process_supervisor.py` from the shared package is copied into the
sandbox; it uses only the standard library. A common runtime installer
and provider default mounts from [the earlier
diagram](NVIDIA-NeMo#3961 (comment))
remain follow-ups. The [owned-sandbox TTL
request](NVIDIA-NeMo#3961 (comment))
is now implemented in this PR.

## Folder layout

```text
nemo_gym/
β”œβ”€β”€ agent_utils/
β”‚   β”œβ”€β”€ __init__.py           # Component responsibilities and execution locations
β”‚   β”œβ”€β”€ sandbox_session.py    # Server: upload supervisor; execute, stop, collect, close
β”‚   β”œβ”€β”€ supervisor_client.py  # Server: protocol files, timeout budget, launch/stop/receipt
β”‚   └── process_supervisor.py # Sandbox: deadlines, kill/reap descendants, write receipt
└── sandbox/
    β”œβ”€β”€ api.py                # Core connection, exec, file transfer and provider lifecycle
    β”œβ”€β”€ providers/            # Backend implementations
    └── utils.py              # Text transfer, CPU caps and image rewriting

responses_api_agents/hermes_agent/
β”œβ”€β”€ app.py                    # Config, request/session hooks and Gym response conversion
β”œβ”€β”€ sandbox.py                # Install Hermes, stage input, parse artifacts/runtime metadata
└── sandbox_runner.py         # Sandbox: Hermes SDK entry point and partial-result capture
```

The control files are `process_supervisor.py`, `launch.claim`,
`supervisor.pid`, `stop.request`, `cleanup.json`, and `output.log`.
`supervisor_client.py` documents each writer and timing. `session_dir`
holds those files; `workdir` is the task repository.

## Compatibility

- Downstream branches must use
`nemo_gym.agent_utils.{sandbox_session,supervisor_client,process_supervisor}`.
Both in-branch Hermes adapters and tests use the new package. Session
adopters use `session_dir`, `stage_activation`, and `stop_harness`; they
no longer upload or choose a supervisor path.
- Default `resources_server` is null; unbound compatibility `/run`
returns 422. Explicitly bound recipes remain supported; legacy agent
swapping must restore that binding. The default model uses
`${policy_model_name}`.
- Owned TTL defaults only when the `ttl_s` key is absent. Explicit null
retains provider defaults; long-lived or delayed sessions can set their
own lifetime. Expiry still depends on provider support; this does not
add TTL support to providers that ignore it.
- No seed protocol or Hermes version change. Token accounting remains
separate in NVIDIA-NeMo#3981. Invalid runtime metadata leaves valid responses
intact and records an observation gap.
- Transport errors remain execution errors after partial capture;
capture failures are independent of cleanup confirmation. Model-writable
output still does not qualify these trajectories for RL training.

## Validation

**Latest reviewed head: `d5e6ad20b733faeeb3a35cabc4288c13609d4b11`.**
[Full CI
37531286874](https://github.com/NVIDIA-NeMo/Gym/actions/runs/37531286874)
and [build
37531287625](https://github.com/NVIDIA-NeMo/Gym/actions/runs/37531287625)
**passed** on this head: core **6,594 passed / 2 skipped**, sandbox
**913 passed / 5 skipped**, **95.25%** coverage, and all eight server
shards. All applicable head checks are complete. The PR currently has no
merge-queue entry; a maintainer needs to re-add it for fresh
combined-tree CI. The prior failed queue run does not validate this fix.

- Reproduced the [merge-queue
failure](https://github.com/NVIDIA-NeMo/Gym/actions/runs/37526772843/job/112486565515)
at combined SHA `0f1001ef33be7f4ab99d0e236a2ce15053f0f64e`. With this
patch applied to that exact tree, migration and configuration tests
passed: **219 passed**, including the unchanged repository-wide audit.
- Final PR checkout: migration, configuration and session-contract tests
**220 passed**. Six new cases cover explicit null versus bound Hermes,
OSWorld, inherited bindings and exactly one native Environment Server
after composition. Two independent reviews found no remaining issues;
the six focused cases also passed independently.
- Full `pre-commit run --all-files`, `git diff --check` and the default
Hermes migration `--check` passed. Model rollout: **N/A for this
follow-up**, which changes offline configuration migration only.
- Prior head `a309dfb9a5cb8c65b6bd7366260c189c3dcd3284`: both Hermes
suites plus shared session/client tests **375 passed, 9 skipped**;
mocked Docker TTL contract tests **2 passed**. No live provider
expiry/model run was performed for the TTL change; earlier model
evidence remains scoped to its recorded revisions.
- That prior head passed [CICD
37525046614](https://github.com/NVIDIA-NeMo/Gym/actions/runs/37525046614):
core **6,588 passed, 2 skipped**, sandbox **913 passed, 5 skipped**,
combined coverage **95.25%**, and all eight server shards. [Wheel/build
37525047397](https://github.com/NVIDIA-NeMo/Gym/actions/runs/37525047397)
and lightweight checks also passed. These results do not validate this
new migration commit or the failed merge-group tree.
- All 20 implementation review requests remain addressed, including
owned TTL; reviewer threads remain for reviewers to resolve. Shared
Python provisioning remains future installer work.

```sh
python -m pytest tests/unit_tests/test_add_legacy_agent_environment_servers.py \
  tests/unit_tests/test_global_config.py tests/unit_tests/test_session_contract_repo.py \
  --import-mode=importlib -q -o addopts='' -o log_cli=false --tb=short
# Exact failed merge-group source plus the two-file patch:
python -m pytest tests/unit_tests/test_add_legacy_agent_environment_servers.py \
  tests/unit_tests/test_global_config.py \
  --import-mode=importlib -q -o addopts='' -o log_cli=false --tb=short
python scripts/add_legacy_agent_environment_servers.py --check \
  responses_api_agents/hermes_agent/configs/hermes_agent.yaml
pre-commit run --all-files
git diff --check
```

---------

Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
khazic pushed a commit to khazic/Gym-lao that referenced this pull request Oct 7, 2026
## What changed and why

Adds the paired NeMo User Sim runtime on the materialized `resolved_row`
contract, tracked by NVIDIA-NeMo#985.

- Adds `resources_servers/nemo_user_sim` for immutable-row validation,
bounded verification, scorer classification, and conformant session
cleanup.
- Adds `environment_servers/nemo_user_sim`, which owns
`ProbeEpisodeRuntime`, participant dispatch, tool simulation,
embeddings, evidence capture, and cleanup.
- Pins both servers to the same public UserSim revision:
`a5f676bf6dc5a73914c8a0860f97c10dd2c214ee`.
- Routes `nemo_user_sim:example` and `nemo_user_sim:validation` through
`nemo_user_sim_environment`.
- Uses the multi-agent environment bindings and materialized-row metrics
already merged through NVIDIA-NeMo#4053 and NVIDIA-NeMo#4060.
- Adds five synthetic example rows, matching metrics, and compact live
rollout evidence so this PR passes changed-server data validation at its
own head.
- Adds Resources and Environment Server READMEs so both servers
participate in the scheduled full suite.

## Review fixes

- Assistant activation failures retain the prepared `trajectory_id` and
are verified by the real Resources Server as unmasked reward zero.
- UserSim contract rejection after an assistant activation is scored as
a policy failure; Agent 404 and 409 responses remain retryable
infrastructure failures.
- Stable source `task_id` is accepted, and per-role overrides use
`role_request_params` rather than the reserved `responses_create_params`
row key.
- Guarded health rows are rejected clearly at seed time for this pinned
UserSim revision, while evaluator `reasoning_effort` is forwarded.
- Resources session seed and close behavior passes Gym's
session-conformance test, including idempotent seed/close and
closed-session tombstones.
- UserSim no longer forces global taskset routing mode.
- Verifier evidence remains single-copy: invocations and the UserSim
result are stored only at the top-level episode result.

## Validation

- `uv run gym env test --resources-server nemo_user_sim` β€” 22 passed.
- `resources_servers/nemo_user_sim/.venv/bin/pytest -q
environment_servers/nemo_user_sim/tests` β€” 26 passed.
- `uv run gym env resolve --config
resources_servers/nemo_user_sim/configs/nemo_user_sim.yaml ...` β€”
passed.
- Scoped pre-commit, secret baseline validation, and `git diff --check`
β€” passed.

## Rollout evidence

Five live examples were generated with
`meta/llama-3.2-11b-vision-instruct` as the user/policy model and
`openai/gpt-oss-20b` as the judge/support model. All 5 completed with
`mask_sample=false`; rewards were 0.8, 0.8667, 0.7333, 0.8, and 0.0. The
committed rollout artifact is approximately 316 KiB and does not
duplicate verifier evidence.

## Compatibility and benchmark impact

This is a new pre-release runtime. Integration paths, component names,
and tasksets use `nemo_user_sim`; upstream package imports and
UserSim-owned data fields retain `usersim`. Infrastructure failures
remain retryable and masked, while assistant-caused invalid responses
are measured as unmasked reward zero.

---------

Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>

This branch was previously deployed

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

Labels

area:config Hydra configuration, schemas, composition, and compatibility complexity:medium Single-domain change with interacting parts or a moderate review surface feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer r0.7.0 Auto-cherrypick to release branch. Apply before merge; cherrypick happens after merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants