Add EnterpriseOps-Gym benchmark: resources server, benchmark registration, and per-turn telemetry agent - #2142
Conversation
…er-turn telemetry
Adapts the ServiceNow EnterpriseOps-Gym benchmark (Apache 2.0; 8 enterprise
domains, external MCP gym servers, SQL verifiers over final DB state) to
NeMo Gym:
- resources_servers/enterpriseops_gym: per-rollout DB seeding (SQL content
cache + per-gym seed semaphores), catch-all /{tool_name} MCP proxy with
EOG-parity observations and per-tool latency capture, idempotent /verify
with guaranteed DB deletion, TTL janitor for killed rollouts, replica
pools (gym_url_pools) for horizontal MCP scale-out, and per-domain
aggregate metrics (leaderboard-style macro average).
- verifier_engine.py is a line-for-line port of the upstream engine,
preserving its quirks for score parity (verifier name-collapse where
duplicate-named verifiers overwrite; loose comparison semantics; skipped
unknown-gym verifiers), pinned by golden fixtures generated from the
original implementation. strict_verifiers=true switches the reward to
every-verifier-counts for RL shaping.
- convert_tasks.py / snapshot_tools.py convert EOG tasks (local or the
ServiceNow-AI/EnterpriseOps-Gym HF dataset) into NeMo Gym JSONL, baking
tool schemas from live tools/list snapshots with per-task gym-order
merge semantics (hybrid parity).
- benchmarks/enterpriseops: oracle public split (649 tasks) with HF
download and offline local fallbacks.
- responses_api_agents/turn_logging_agent: behaviorally identical
simple_agent variant that records per-turn telemetry (timestamps,
input/output/cached tokens, tool names) and attaches it to verify
responses; export_eval_telemetry.py emits the eval team's 21-field
per-turn JSONL schema.
Validated at full scale against the native harness on the same 649 tasks,
model, and containers: macro success 16.4-17.8% across three runs, 90-94%
per-task agreement, McNemar p>=0.21 (no detectable harness bias), and
100% identical collapsed-verifier scoring structure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Consolidates the equivalence case vs the native harness: golden-fixture unit parity, 12/12 live task parity, full-public-split single-run comparisons (McNemar p>=0.21, 100% scoring-structure agreement), and the k=5 interleaved variance experiment (6,480 rollouts): mean@5 macro 16.54+/-0.73 vs 16.76+/-0.99 (delta -0.23pp), per-task preference 67:67, and a direction-free gateway serving-path effect (+3.49pp outcome-flip excess, permutation p<0.002) attributed to Responses-vs-ChatCompletions serving rather than either harness. Includes calibration guidance (report mean@k; ~6.3% of outcomes flip on any rerun) and upstream-relevant operational findings (container fd leak, silent task drops, resume mode). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Upstream #1682 made the MCP Python SDK a core dependency, but for Gym-as-MCP-server (the inverse of this module's Gym-as-MCP-client role). Records why the SDK client is not a fit here: httpx transport (banned for high-concurrency async), session-level vs required per-call isolation headers, non-MCP REST endpoints comprising half the surface, and frozen upstream protocol version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
…ed GPU
Full oracle-split sweep (649 tasks x 2 harnesses x c in {8,16,32,64,128})
on 4xH100 TP=4 serving Nemotron 3 Nano FP8 locally, both harnesses on the
identical chat-completions endpoint. The port is 1.14-1.71x faster at
matched concurrency (largest at realistic low-c settings), needs ~4x less
client concurrency for equal throughput, and saturates the hardware at
c=64 while native never reaches the throughput floor in the tested range.
Success rates identical within noise at every level. Mechanism: GPU batch
starvation from native's per-request connections, per-task handshakes,
and sequential verifiers (GPU 98% busy both sides, client CPU idle).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Treats the 4xH100 performance sweep as an independent k=5 replication on dedicated vLLM serving (Nemotron 3 Nano FP8, both harnesses on one endpoint). Confirms the serving-path attribution via its designed falsification test (cross-harness trajectory excess 3.49 -> 1.57pp, 55% -> 11% of the noise floor), establishes formal TOST equivalence within +/-2pp at alpha=0.05 with direction-free residuals (112:106 task preference, sign p=0.735), and adds a determinism calibration for reasoning models (41% flaky tasks vs 20% non-reasoning; report mean@5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Config-only GRPO readiness validation on the 4xH100 stack: token-ID capture via return_token_id_information, curriculum selection from repeat-run sweep data, and group-mixing results (15/20 binary-mixed at k=8, mean group std 0.341). Documents the unshuffled-benchmark +limit pitfall and operational notes for long collection runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Measured train-sequence lengths (exact from v3 token IDs, band-wide via calibrated estimate): curriculum yield is 63% at a 32k cap vs 94% at 64k, which drives the shape ranking (B300/B200 single node > H200 > 2x8 H100 disaggregated > single 8x H100 > LoRA fallback). Includes trainer memory budget, step-time model, node layout, and the pre-registered proof-point run sketch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Generated by scripts/update_env_list.py; the turnlog overlay config gains a metadata block (a no-op merge over the inherited server) so the table generator can render its row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Per the environment contribution guide: data/example_rollouts.jsonl (5 pre-generated rollouts from example.jsonl against live containers) and a licensing/data-provenance section in the server README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Generated via gym dataset collate +mode=example_validation; CI's should_validate_data gate requires it alongside example.jsonl and example_rollouts.jsonl. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
…h-integration-enterpriseops Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
The five flagged strings in enterpriseops_gym/data/tools/drive.json are example Google Drive document IDs from the upstream EOG container's tool schemas (one is the sample spreadsheet ID from Google's own API docs), not credentials. Baseline updated with detect-secrets 1.5.0 to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
gym env test discovery only treats modules with a README.md as testable; with fail_on_total_and_test_mismatch=true the missing README failed CI's shared server-tests job (found 137 modules, tested 136). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
| @@ -0,0 +1,3436 @@ | |||
| { | |||
There was a problem hiding this comment.
can these long files be pulled from somewhere like huggingface instead of committed?
|
/claude review |
|
SHIP WITH CARE — no BLOCKERs. The verifier port and reward aggregation are the highest-risk surface here and they're the most carefully done part of the PR: EOG parity quirks (name-collapse, unknown-gym skips, loose comparisons) are documented and pinned by golden fixtures, Two non-blocking findings, both inline:
Neither blocks merge; #2 is the one I'd want resolved before this feeds a training run. |
| model_response = await self.server_client.post( | ||
| server_name=self.config.model_server.name, | ||
| url_path="/v1/responses", | ||
| json=new_body, | ||
| cookies=model_server_cookies, | ||
| ) |
There was a problem hiding this comment.
RISK — drops the /ng-rollout/<id> capture-correlation prefix that simple_agent applies.
The docstring says this loop "mirrors simple_agent step for step... Keep it in sync," but it has already diverged here: simple_agent.responses() uses url_path_for_request("/v1/responses", request) (line 89) and simple_agent.run() uses url_path_for_run("/v1/responses", body) (line 190). This agent hardcodes bare /v1/responses at both sites (here and line 273).
WHAT BREAKS: when model-call capture / observability is enabled, url_path_for_* prefixes the outgoing call with /ng-rollout/<rollout_id> so the model server can key captured calls to the rollout. Without it, this agent's captured model calls carry no correlation id — the exact per-rollout observability that a telemetry agent exists to provide is silently lost.
BLAST RADIUS: only when observability is on (no-op prefix otherwise), so eval/reward and return_token_id_information (inline in the response body) are unaffected — RLPILOT numbers stand. But this agent targets RL rollout collection, which is where capture is most likely enabled.
FIX: url_path=self.url_path_for_request("/v1/responses", request) here, and self.url_path_for_run("/v1/responses", body) in run(). Nearly every contributed agent (browsecomp, critpt, cvdp, non_executing_simple, gymnasium…) uses these helpers.
There was a problem hiding this comment.
Both call sites now use url_path_for_request / url_path_for_run, matching simple_agent, with a regression test that drives the /ng-rollout//v1/responses route and asserts the prefix survives onto the model call.
| overall_success = all(v["passed"] for v in verification_results.values()) | ||
| verifier_pass_rate = passed_verifiers / total_verifiers if total_verifiers > 0 else 0.0 | ||
|
|
||
| # Strict scoring over every defined verifier (skipped verifiers count as failed). | ||
| strict_passed = [entry["result"].get("passed", False) for entry in all_verifier_results] | ||
| strict_success = all(strict_passed) | ||
| strict_pass_rate = sum(strict_passed) / len(strict_passed) if strict_passed else 0.0 |
There was a problem hiding this comment.
RISK — an empty or all-skipped verifier set scores reward 1.0.
all([]) is True, so:
- If every verifier is skipped (all reference an unknown
gym_name),verification_resultsis empty →overall_success = all([]) = True→ parity reward 1.0. - If a task defines zero verifiers,
strict_passed = []→strict_success = all([]) = True→ strict reward 1.0.
WHAT BREAKS: a misconfigured gym_url_pools/dataset gym_name mismatch (plausible when pointing at replicas for an RL run) silently turns every task into a false pass instead of a hard failure. That corrupts eval macro-rates upward and injects spurious positive reward into GRPO training — the worst-case silent-corruption path.
Note the skipped-verifier→fail guarantee already holds for strict mode when some verifiers survive (test_tool_execution_verifier_and_skipped_gym); the gap is only the fully-empty set.
FIX (at minimum for the strict/RL path, where empty=pass is never intended):
strict_success = bool(strict_passed) and all(strict_passed).
For the parity path, confirm upstream EOG also awards 1.0 on an all-skipped task; if it does, keep it but consider a log.warning when num_verifiers_scored == 0 so silent all-skips are visible in large runs.
There was a problem hiding this comment.
Fixed for the strict/RL path in b0494d2 (bool(strict_passed) and all(strict_passed)), with a regression test.
The collapsed metric keeps upstream EOG's all([]) behavior on purpose: that path is a bug-for-bug port of the upstream scorer, which is what keeps our numbers comparable to the published EOG leaderboard. The rationale is documented at the code site, and num_verifiers_scored exposes the empty-set case to callers.
Mirror simple_agent's url_path_for_request/url_path_for_run so /ng-rollout/<id>-prefixed self-calls keep per-rollout observability correlation on downstream model calls. Adds a regression test driving the prefixed route. Addresses PR #2142 review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
all([]) is True, so a task with zero scorable verifiers awarded strict reward 1.0. Guard the strict path (which feeds RL rewards); the collapsed parity path intentionally keeps upstream's all([]) semantics for leaderboard comparability, now documented in place. Addresses PR #2142 review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
|
Good question. Quick context on what these are: the data/tools/*.json files are snapshots of the tool schemas advertised by EnterpriseOps-Gym's MCP
containers (512 tools across 7 domains). They can't come from HuggingFace as-is because the upstream HF dataset only contains tasks; the schemas exist
only inside the running Docker containers, and they're needed at prepare.py time to build task rows. Pinned copies are also what guarantees byte-level
prompt parity with the upstream harness (see PARITY.md).
I agree they're bulky. Two options:
1.
Host them on HF (a small companion dataset, or the existing dataset repo if ServiceNow will take them, which would need upstreaming) and have prepare.py download them, keeping only a checksum in-repo. Downside: prepare gains a network dependency for data that changes only when upstream ships new containers.
2.
Keep them committed as the reproducibility anchor. They're versioned exactly like golden test fixtures.
My mild preference is (2) for reproducibility, but (1) works if repo size is the priority.
From: Christian Munley ***@***.***>
Date: Monday, July 27, 2026 at 10:13 AM
To: NVIDIA-NeMo/Gym ***@***.***>
Cc: Marc Cuevas ***@***.***>; Author ***@***.***>
Subject: Re: [NVIDIA-NeMo/Gym] Add EnterpriseOps-Gym benchmark: resources server, benchmark registration, and per-turn telemetry agent (PR #2142)
@cmunley1 commented on this pull request.
________________________________
In resources_servers/enterpriseops_gym/data/tools/calendar.json<#2142 (comment)>:
@@ -0,0 +1,3436 @@
+{
can these long files be pulled from somewhere like huggingface instead of committed?
—
Reply to this email directly, view it on GitHub<#2142?email_source=notifications&email_token=B55WDQREOH2VD5KOXZ7ALYD5G6ERPA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINZYHE3DENBRGMYKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#pullrequestreview-4789624130>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/B55WDQVTNXPZ77HYPHRFJJD5G6ERPAVCNFSNUABGKJSXA33TNF2G64TZHMYTANBUGU2TIOBUGA5US43TOVSTWNBZG4ZTENRWHEYTTILWAI>.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS<https://github.com/notifications/mobile/ios/B55WDQVOGQBHPOBD2ZQZR735G6ERPA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINZYHE3DENBRGMYKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG> and Android<https://github.com/notifications/mobile/android/B55WDQR4JTS3JH3KU26HLPL5G6ERPA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINZYHE3DENBRGMYKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>. Download it today!
You are receiving this because you authored the thread.
|
…h-integration-enterpriseops Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Summary
This PR integrates ServiceNow's EnterpriseOps-Gym (EOG) into NeMo Gym: a 649-task benchmark of stateful, multi-step enterprise tool use across 8 domains (calendar, CSM, drive, email, HR, ITSM, teams, and cross-domain hybrid), where an agent operates 512 tools against live MCP servers backed by SQL databases and is scored on final database state. The integration is eval-complete and RL-ready (token-ID capture, fractional reward mode, and a validated GRPO rollout-collection recipe), with no changes to NeMo Gym core.
What's included (46 files)
resources_servers/enterpriseops_gym//{tool_name}route, pooled aiohttp, per-sessionx-database-id), runs verifiers concurrently, cleans up idempotently (TTL janitor + delete-on-verify). Supports replica pools (gym_url_pools), per-domain metrics, per-tool latency capture, and astrict_verifiersfractional-reward mode for RL..../verifier_engine.py.../convert_tasks.py+benchmarks/enterpriseops/ServiceNow-AI/EnterpriseOps-Gym) to Responses-API task rows atprepare.pytime; benchmark registered withprompt_config: null(pre-baked rows). Dataset files are not committed.responses_api_agents/turn_logging_agent/SimpleAgentsubclass with an identical loop that records per-turn timestamps, durations, input/output/cached/reasoning tokens, and tool names, attachingturnsto the verify response. Generic — not EOG-specific.tests/stub_gym.py) plus golden parity fixtures. No containers or network needed to run CI.PARITY.md,PERF.md,RLPILOT.mdVerification logic
Each task carries
verifier_metadatawith a list of verifiers.database_stateverifiersrun a SQL query against the session's final database state (via the gym containers'
/api/sql-runner), extract a value, and compare it to the expected value using EOG'scomparison semantics;
response_checkverifiers score the agent's final message with anLLM judge (defaults to the policy model, temperature pinned to 0.0, matching EOG). The
public oracle split is 100%
database_state(3,496/3,496 verifiers), so scoring there isfully deterministic given a final DB state. Reward = 1.0 iff all (name-collapsed) verifiers
pass, matching the upstream leaderboard;
strict_verifiers: trueswitches toevery-verifier-counts and a fractional
strict_pass_ratefor RL shaping.Scoring fidelity (PARITY.md)
The port is validated bug-for-bug against the upstream harness, preserving its quirks (verifier name-collapse, unknown-gym skips, loose comparison semantics) for leaderboard comparability:
Performance (PERF.md)
Scale-tested end-to-end at five client concurrencies (c = 8, 16, 32, 64, 128) with a full
649-task pass per level per harness (10 passes, 4×H100 vLLM, identical endpoint): the port
completed 649/649 tasks at every level with zero retries, with success rates flat
across levels. At matched concurrency the port is 1.14–1.71× faster (largest at low
concurrency, nearest the native harness's documented defaults). The gap narrows by design:
both harnesses converge toward the same GPU throughput floor — and reaching it is the key
result. The port saturates the hardware at c=64; the native harness never reaches the
floor in the tested range and needs ~4× the client concurrency for equal throughput.
Net cost: a full-split eval is 1h27m of 4×H100 time (port) vs 4h40m at native's documented
settings — 3.2× GPU-hours. Mechanism: pooled connections, persistent MCP sessions, and
concurrent verifiers keep vLLM's continuous batch fed. (Since the gap is client dead time
relative to GPU service time, it is expected to widen on faster serving hardware, where
saturation demands even more effective concurrency.)
RL readiness (RLPILOT.md)
A config-only pilot (zero code changes) validated GRPO rollout collection end-to-end: 100% token-ID/logprob coverage via
return_token_id_information, 15/20 task groups with mixed binary reward at k=8 (mean within-group std 0.341) on a curriculum selected from repeat-run data, plus a measured deployment-sizing guide (sequence-length distribution, memory budget, recommended node shapes).How to run
Validation on this exact branch state
mainruff check+ruff format --checkclean; README environment table regenerated viascripts/update_env_list.pyng_rundry runContribution-guide compliance
Mapped to the environment /
benchmark guides:
app.py,configs/*.yaml(validdomain: agent),tests/test_app.py(42 tests),data/example.jsonl(5 tasks),data/example_rollouts.jsonl(5 pre-generated rollouts against live containers; notethese samples are CSM-domain, the benchmark's hardest — near-zero rewards on them are
expected and consistent with the full-split CSM rate of ~4%),
requirements.txt,README.mdwith licensing information.thinking model (Nemotron 3 Nano, reasoning on: 22–25%), i.e. the instruct+thinking
mixture the guide asks for. Scores sit inside the official leaderboard's published range
(Qwen3-4B 13.6% … GPT-5-Mini 22.0%) with a coherent domain pattern (email easiest, CSM
hardest, matching the leaderboard). This benchmark is legitimately hard — no public model
reaches 30%.
Calibration guidance for users is included in PARITY.md.
(documented in the three reports).
number (leaderboard models weren't available on our serving), we ran the original EOG
harness side-by-side on identical models, containers, and endpoints: per-task agreement
with symmetric disagreements (McNemar p = 0.90) and formal TOST equivalence within ±2 pp.
This isolates harness fidelity even more directly; happy to additionally run a listed
leaderboard model if reviewers want the published-number check.
(resources server + tests → benchmark registration → turn-logging agent → reports) if
reviewers prefer; presented as one PR first since the layers are tightly coupled by the
parity evidence.
Design decisions & notes for reviewers
mcp_client.pydocstring): the SDK's transport is httpx-based (banned for async here), EOG needs session-level and per-callx-database-idheaders, and half the surface is non-MCP REST (/api/seed-databaseetc.).strict_verifiers: trueopts into every-verifier-counts scoring for RL.turn_logging_agentis separable — it's a general-purpose agent; happy to split it into its own PR if preferred.verified: falseper convention for new resources servers.Data provenance & licensing
EnterpriseOps-Gym is Apache 2.0 (code) with a public HF dataset (
ServiceNow-AI/EnterpriseOps-Gym). This PR commits only: tool-schema snapshots captured from the public EOG containers (7 JSON files, 512 tools), 13 sample tasks derived from the EOG repo's task files, 5 example rollouts generated against live containers, one synthetic hybrid task hand-authored for tests (written with LLM assistance against live container schemas — disclosed per the synthetic-data guideline), and golden verifier fixtures generated by running the EOG engine. The full benchmark dataset is downloaded atprepare.pytime and gitignored.