Skip to content

refactor(evaluator)!: resolve the Gym CLI from PATH instead of a configured checkout - #1196

Merged
SandyChapman merged 3 commits into
mainfrom
gym-bin-resolution/schapman
Aug 10, 2026
Merged

refactor(evaluator)!: resolve the Gym CLI from PATH instead of a configured checkout#1196
SandyChapman merged 3 commits into
mainfrom
gym-bin-resolution/schapman

Conversation

@SandyChapman

@SandyChapman SandyChapman commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

GymRuntimeConfig no longer carries gym_root or a gym_bin path — the runner resolves gym from PATH, so NeMo Gym must be installed in the same environment as the SDK. Before, a caller had to name a Gym checkout and the runner derived the executable from <gym_root>/.venv/bin/gym; now there is no path configuration at all, and a missing CLI fails with an actionable message before the run starts instead of an ENOENT partway through.

Two reasons the old shape does not survive. These runner configs become serialized job specs when Gym runs as a governed platform job, and a path into somebody's checkout or venv means nothing on the other side of that boundary. And a checkout is no longer required: NeMo-Gym now ships its component trees in the wheel, so pip install nemo-gym resolves environments and their example data with nothing on disk.

Related Issue

Tracked in Linear as AALGO-485 (validating the Gym runner across a representative sample of built-in environments); this is the configuration cleanup that came out of it. No GitHub issue.

Changes

  • gym_runtime.py — remove gym_root and gym_bin from GymRuntimeConfig. Add a module-level _gym_executable() that resolves _GYM_CLI via shutil.which, mirroring how CodexCliAgentRuntime already resolves its own CLI, and raises with install guidance when absent.
  • Subprocess cwd — no longer set from config. The gym env start and gym eval run subprocesses inherit this process's working directory, which is where Gym looks for the gitignored env.yaml holding the collector's credentials before falling back to its install root. Running from a Gym checkout still makes its components take precedence, so that capability survives without a field.
  • examples/gym/run_gym_eval.py — drop --gym-root; default the dataset to the packaged resources_servers/<name>/data/example.jsonl via importlib.resources, with an actionable error when Gym is not importable.
  • examples/gym/README.md — lead with pip install nemo-gym. The previous text asserted "a working NeMo Gym checkout is required — Gym resolves its environments from the repo, not from a package install", which is no longer true.
  • examples/gym/inspect_results.py — update the usage example.
  • tests/agent_eval/test_run_metadata.py — drop gym_root from the two GymRuntimeConfig constructions. runner_info() output is unchanged, so persisted metadata.json provenance keeps its shape.
  • Vendored SDK copy — regenerated with make vendor; not hand-edited.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with documentation updates
  • Documentation only
  • Contributor tooling or automation
  • CI, build, or test infrastructure

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification: test_run_metadata.py covers the runner_info() surface and was updated for the removed field; the 542-test tests/agent_eval/ suite covers the runner. The new failure path needs a missing gym binary, which the existing suite cannot assert against without mocking PATH — it was verified by hand instead (below), and live coverage across real environments is the subject of AALGO-485.
  • Tests not applicable — justification:
  • Documentation updated for user-visible behavior
  • Documentation not applicable — justification:

Verification

  • Pull request title follows the repository's Conventional Commit format
  • Every commit includes an appropriate Signed-off-by: trailer
  • uv run pre-commit run -a passes, or any blocked checks are identified below
  • Targeted tests pass, or tests are marked not applicable above
  • No secrets, API keys, or credentials are included

Targeted validation:

uv run --frozen pytest packages/nemo_evaluator_sdk/tests/agent_eval/ -q
  → 542 passed, 4 warnings in 221s

uv run --frozen ruff check .
  → All checks passed!

uv run --frozen ruff format --check packages/nemo_evaluator_sdk/
  → 229 files already formatted

uv run --frozen ty check .../agent_eval/runtimes/gym_runtime.py .../examples/gym/run_gym_eval.py
  → All checks passed!

make vendor
  → vendored SDK copy regenerated and in sync

Failure path exercised by hand, since it requires gym to be absent from PATH:

$ uv run --frozen python -m packages.nemo_evaluator_sdk.examples.gym.run_gym_eval --dataset <packaged mcqa example.jsonl>
discovered 5 tasks from .../site-packages/resources_servers/mcqa/data/example.jsonl
RuntimeError: The 'gym' CLI was not found on PATH. NeMo Gym must be installed in the same
environment as this SDK: `pip install nemo-gym`, plus the target environment's own
dependencies (each resources-server ships a requirements.txt).

$ uv run --frozen python -m packages.nemo_evaluator_sdk.examples.gym.run_gym_eval
resources_servers.mcqa is not importable here, so its bundled dataset cannot be located.
Install Gym in this environment (`pip install nemo-gym`), or pass --dataset with the path
to the jsonl you want to run.

That first run also confirms the packaged-dataset claim end to end: discover_gym_tasks read example.jsonl straight out of site-packages with no checkout present.

Two pre-commit hooks fail in my environment, neither exercising this change:

  • Run uv lock with platform uv — requires uv 0.9.14; I have 0.9.30. This diff touches no pyproject.toml or uv.lock, and the separate Check for uv.lock drift hook passes, so the lock is correct. Worth noting the tension this PR walks into: nemo-platform pins that hook at 0.9.14 while NeMo-Gym's own pyproject.toml sets required-version = ">=0.9.30", so one uv install cannot satisfy both — and this change asks contributors to install Gym alongside the SDK.
  • Run UI lint-stagedmise ERROR No version is set for shim: pnpm. A local Node toolchain gap; this diff touches no web/ files.

Every other hook passes: ruff, ruff format, ty, config reference docs, Helm docs, uv.lock drift, copyright headers, plugin import boundary, merge-conflict check.

Summary by CodeRabbit

  • New Features

    • Gym evaluations now use the installed gym command available on your PATH.
    • Evaluations can run without a separate Gym source checkout.
    • The bundled example dataset is selected automatically, with optional dataset overrides.
    • Credentials are read from a local env.yaml file.
  • Bug Fixes

    • Missing Gym installations or datasets now produce clear errors before evaluation starts.
    • Result-inspection examples now use simplified command options.
  • Documentation

    • Updated Gym setup, configuration, dataset, and execution instructions.

…igured checkout

`GymRuntimeConfig` no longer carries `gym_root` or a `gym_bin` path. The runner
resolves `gym` from PATH, so NeMo Gym must be installed in the same environment
as the SDK.

Two reasons the old shape does not survive:

* These runner configs become serialized job specs when Gym runs as a governed
  platform job. A path into somebody's checkout or venv means nothing on the
  other side of that boundary, so the fields would only ever be stripped or
  ignored server-side.
* A checkout is no longer required. NeMo-Gym now ships its component trees in
  the wheel — `resources_servers` and friends install beside `nemo_gym`, configs
  and `data/example.jsonl` included — so a plain `pip install nemo-gym` resolves
  environments and their example data with nothing on disk.

`gym_root` had exactly two jobs: defaulting `gym_bin` to
`<gym_root>/.venv/bin/gym`, and setting the subprocess cwd. The first is
replaced by `shutil.which`, matching how `CodexCliAgentRuntime` already resolves
its own CLI. The second is dropped: the subprocesses inherit this process's
working directory, which is where Gym looks for the gitignored `env.yaml`
holding the collector's credentials before falling back to its install root.
Running from a Gym checkout still makes its components take precedence, so the
capability survives without a field to configure it.

A missing CLI now fails with an actionable message before the run starts,
instead of an ENOENT out of `create_subprocess_exec` partway through.

The mcqa example drops `--gym-root`, defaults its dataset to the packaged
`resources_servers/<name>/data/example.jsonl`, and its README leads with
`pip install nemo-gym` rather than a checkout.

BREAKING CHANGE: `GymRuntimeConfig.gym_root` and `GymRuntimeConfig.gym_bin` are
removed. Install NeMo Gym in the same environment as the SDK; run from the
directory holding `env.yaml`, or from a Gym checkout when you need components
the wheel does not carry.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@github-actions github-actions Bot added breaking breaking change (!-marked title) refactor labels Aug 10, 2026
@SandyChapman
SandyChapman marked this pull request as ready for review August 10, 2026 12:48
@SandyChapman
SandyChapman requested review from a team as code owners August 10, 2026 12:48
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d08ca309-f7d9-45ac-bbda-be2137e65d1f

📥 Commits

Reviewing files that changed from the base of the PR and between 4b37302 and d01b12e.

⛔ Files ignored due to path filters (1)
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/runtimes/gym_runtime.py is excluded by !sdk/**
📒 Files selected for processing (1)
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym_runtime.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym_runtime.py

📝 Walkthrough

Walkthrough

The Gym integration now uses an installed gym CLI from PATH, removes gym_root and gym_bin, inherits the caller’s working directory, and resolves a bundled example dataset when no dataset is supplied.

Changes

Gym CLI integration

Layer / File(s) Summary
Runtime CLI contract and execution
packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym_runtime.py, packages/nemo_evaluator_sdk/tests/agent_eval/test_run_metadata.py
GymRuntimeConfig no longer accepts checkout or executable paths. The runtime discovers gym from PATH and runs commands from the caller’s working directory. Tests use the updated configuration.
Packaged dataset and example wiring
packages/nemo_evaluator_sdk/examples/gym/*
The example resolves the installed package’s bundled example.jsonl unless --dataset is provided. Documentation and commands no longer require --gym-root.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant run_gym_eval
  participant GymRuntime
  participant GymCLI
  User->>run_gym_eval: start evaluation
  run_gym_eval->>GymRuntime: configure dataset and runtime
  GymRuntime->>GymCLI: resolve gym from PATH
  GymRuntime->>GymCLI: run evaluation from caller directory
  GymCLI-->>User: write evaluation results
Loading

Suggested reviewers: arpitsardhana, ngoncharenko

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the breaking refactor to resolve the Gym CLI from PATH instead of a configured checkout.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gym-bin-resolution/schapman

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nemo_evaluator_sdk/examples/gym/README.md`:
- Around line 9-16: Update the setup section in the NeMo Gym README to use a
tab-set containing both the CLI workflow and a tested Python SDK workflow. Keep
installation commands, env.yaml configuration, dataset selection, and output
behavior equivalent across both examples, and ensure the Python example reflects
the current SDK API.
- Around line 18-40: Update the README’s later “How it runs Gym” setup
description to match the runtime contract: state that the executable is resolved
from PATH and env.yaml is loaded from the caller’s working directory. Remove
wording that implies both come from a Gym checkout, while preserving the
existing explanation of checkout precedence where applicable.

In `@packages/nemo_evaluator_sdk/examples/gym/run_gym_eval.py`:
- Around line 81-100: Update _packaged_dataset to assign the bundled
example.jsonl resource to a variable, verify it with is_file(), and raise the
existing SystemExit guidance with the --dataset fallback when it is missing;
return the resource path only after validation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bcc8952b-8f74-4c78-8b68-53f36a86f0fe

📥 Commits

Reviewing files that changed from the base of the PR and between 1c2e9d6 and b0262e7.

⛔ Files ignored due to path filters (1)
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/runtimes/gym_runtime.py is excluded by !sdk/**
📒 Files selected for processing (5)
  • packages/nemo_evaluator_sdk/examples/gym/README.md
  • packages/nemo_evaluator_sdk/examples/gym/inspect_results.py
  • packages/nemo_evaluator_sdk/examples/gym/run_gym_eval.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym_runtime.py
  • packages/nemo_evaluator_sdk/tests/agent_eval/test_run_metadata.py

Comment thread packages/nemo_evaluator_sdk/examples/gym/README.md
Comment thread packages/nemo_evaluator_sdk/examples/gym/README.md
Comment thread packages/nemo_evaluator_sdk/examples/gym/run_gym_eval.py
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 31886/40500 78.7% 63.4%
Integration Tests 18517/38426 48.2% 20.8%

@arpitsardhana

Copy link
Copy Markdown
Contributor

I also think we should bubble up the requirement of thin python client to gym so that SDK can use without relying on commandline execution

SandyChapman added a commit that referenced this pull request Aug 10, 2026
Three fixes from review of #1196.

**README contradicted itself.** The "How it runs Gym" section still said the
executable comes from "your checkout" and credentials from "that checkout's
env.yaml", which the rewritten prerequisites had already replaced. It now says
`gym` is resolved from PATH and `env.yaml` is read from the directory you run
from.

**`_packaged_dataset` returned paths it never checked.** It caught
`ModuleNotFoundError` but not a missing file, so an importable environment
without bundled data returned a path that does not exist and left
`discover_gym_tasks` to raise a bare `FileNotFoundError` about a path the caller
never chose. Only git-tracked files ship in the wheel, so an environment whose
splits are downloaded at runtime has no `example.jsonl` — this is reachable, not
theoretical. It now fails with the same `--dataset` guidance as the import case.

**Clarified that `_GYM_CLI` is resolved, never executed.** Review asked whether
the resolving and executing processes could disagree about PATH. They cannot:
`shutil.which` returns an absolute path and that is what the subprocesses run.
The constant reads like a bare name, so the reasoning is now written down next
to it.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
Three fixes from review of #1196.

**README contradicted itself.** The "How it runs Gym" section still said the
executable comes from "your checkout" and credentials from "that checkout's
env.yaml", which the rewritten prerequisites had already replaced. It now says
`gym` is resolved from PATH and `env.yaml` is read from the directory you run
from.

**`_packaged_dataset` returned paths it never checked.** It caught
`ModuleNotFoundError` but not a missing file, so an importable environment
without bundled data returned a path that does not exist and left
`discover_gym_tasks` to raise a bare `FileNotFoundError` about a path the caller
never chose. Only git-tracked files ship in the wheel, so an environment whose
splits are downloaded at runtime has no `example.jsonl` — this is reachable, not
theoretical. It now fails with the same `--dataset` guidance as the import case.

**Clarified that `_GYM_CLI` is resolved, never executed.** Review asked whether
the resolving and executing processes could disagree about PATH. They cannot:
`shutil.which` returns an absolute path and that is what the subprocesses run.
The constant reads like a bare name, so the reasoning is now written down next
to it.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@SandyChapman
SandyChapman force-pushed the gym-bin-resolution/schapman branch from 1fc6cf5 to e636cf6 Compare August 10, 2026 16:26
Co-authored-by: Nick Goncharenko <8766167+ngoncharenko@users.noreply.github.com>
Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@SandyChapman
SandyChapman force-pushed the gym-bin-resolution/schapman branch from 4b37302 to d01b12e Compare August 10, 2026 17:07
@SandyChapman
SandyChapman added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 4a4d375 Aug 10, 2026
56 checks passed
@SandyChapman
SandyChapman deleted the gym-bin-resolution/schapman branch August 10, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking breaking change (!-marked title) refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants