Converge #1677 swe_env decoupling into anyswe + verify on SWE-bench Verified (#1249) - #1738
Conversation
…b + verifier + docker provider; drop openclaw) Self-contained anyswe-on-swe_env on top of cmunley1/anyswe (now merged with latest main): - add swe_env library (responses_api_agents/swe_env/) + inline verifier (resources_servers/swe_env/) - add the docker sandbox provider + register it (main has apptainer/opensandbox, not docker) - replace anyswe_agent with the swe_env-based version: docker + apptainer backends, local-.sif support, --writable-tmpfs, claude_code anthropic_base_url=null (real Anthropic by default) - drop openclaw + orphaned dataset setup scripts + unused example_rollouts (out of scope) Supersedes the NVIDIA-NeMo#1677 stack (NVIDIA-NeMo#1677/NVIDIA-NeMo#1678/NVIDIA-NeMo#1679) and the prior NVIDIA-NeMo#1738 base (stack/3). Unit tests: anyswe 16, swe_env lib 134 (+1 skip), verifier 14 (+2 skip) = 164 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90908fd to
2f0b75c
Compare
…y_task library The standalone /verify resources server (resources_servers/swe_env/) had no consumer in this PR: anyswe imports verify_task and grades in-process (app.py), and the swe_agents/mini WIP that would have POSTed to /verify is not part of the converged PR. Keep the grading logic as a library; remove the server packaging. - Move verify_task.py into the swe_env library (responses_api_agents/swe_env/) and repoint the 4 importers (anyswe app, self_drive lazy import, test_lifecycle, test_swe_env). All of verify_task's own imports are swe_env siblings + nemo_gym.sandbox, so the move needs no body change. - Move the library-level driver + real-instance e2e alongside it (run_swebench_verified.py, test_swebench_real_instance.py); their parents[3] sys.path depth is identical from the new location. - Delete the server: app.py, configs/swe_env.yaml, data/ (incl. the synthetic placeholder rollouts + metrics), README, requirements, and the HTTP-server tests (test_verify_http.py; test_verify.py — its verify_task assertions are subsumed by the library's test_swe_env.py/test_lifecycle.py, and its remaining tests only covered the deleted server-side request-parsing glue). - Fix stale "verifier server" / "POST the patch over HTTP" docstrings (harness.py, grading.py, self_drive.py, verify_task.py) now that grading is inline-only. swe_env library + anyswe suites green: 150 passed, 2 env-gated skips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
…d by this stack) responses_api_agents/openclaw_agent/ exists on main; it entered the NVIDIA-NeMo#1572 base via the main-merge (22f8ead), not from cmunley's original anyswe work (pr-1572 had zero openclaw files). The earlier "drop openclaw" trim therefore deleted a main artifact, so merging the stack down (NVIDIA-NeMo#1738 -> NVIDIA-NeMo#1572 -> main) would have removed openclaw_agent/ from main. Restore the openclaw files from the base so this PR neither adds nor removes openclaw: - Restore responses_api_agents/openclaw_agent/ (11 files). - Restore responses_api_agents/anyswe_agent/configs/anyswe_openclaw.yaml and setup_scripts/openclaw_agent_deps.sh. Still trimmed (these are NVIDIA-NeMo#1572-only, absent from main): the orphaned swebench.sh / r2e_gym.sh / swebench_multilingual.sh setup scripts and the unused example_rollouts_{claude,hermes}.jsonl fixtures. openclaw + anyswe suites green (45 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
It was an operational script (provision the official image -> apply the gold patch -> run eval_script -> grade with the swebench parser) with no consumer: nothing imports or invokes it, and it is not part of the e2e flow (prepare.py -> ng_run -> ng_collect_rollouts). Gold-patch fidelity belongs as an option in the e2e workflow, not a separate script. The env-gated test_swebench_real_instance.py still provides a real-instance gold-patch check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
…vior change)
Fold small, single-purpose spine files into their cohesive owners so the library
is easier to navigate. No logic changes; only code relocation + import repoints.
- harness.py <- absorbs registry.py + grading.py (the harness contract now also
holds the name->harness registry and the pure grading helpers).
- sandbox.py <- environment.py renamed, absorbing lifecycle.py (the async sandbox
wrapper plus its acquire/teardown context manager).
- self_drive.py <- absorbs model_endpoint.py (self-driving scaffolding plus the
in-sandbox model-server egress primitive).
- verify_task.py / __init__.py / harnesses/ / tests/: import paths repointed.
Removed: registry.py, grading.py, lifecycle.py, model_endpoint.py (environment.py
renamed to sandbox.py). anyswe_agent/app.py unchanged.
ruff clean; swe_env + anyswe suites green (150 passed, 2 skipped).
Signed-off-by: Codex <codex@openai.com>
…bench_ext + r2e host-side alignment) Fixes the confirmed grading drifts from the harness-vs-main audit so the extracted harnesses match what main's swe_agents produces: - swebench.py: port the SWE-bench per-repo FAIL_ONLY eval-type (chartjs/Chart.js, processing/p5.js, markedjs/marked) into compute_resolved; replace the --3way patch fallback with main's GIT_APPLY ladder (git apply --verbose || --reject || patch --batch --fuzz=5); an unbuildable/empty eval spec now grades unmasked resolved=False (reward 0, mask_sample=False) instead of error_kind=eval_error. - swe_bench_ext.py: a command-less row runs no test runner (drop the default `python -m pytest -rA -q`), matching SweBenchExtDatasetProcessor. - flat_eval.py / r2egym.py: restrict eval_error masking to genuine sandbox/timeout infra failures; feasible host-side r2e alignment only. Validation: harness-vs-main audit converged (0 residual behavioral drift); ruff clean; 158 unit tests pass; gold-patch e2e on real docker images = SWE-bench Verified 6/6, SWE-bench Multilingual 3/4 (the 1 miss is a flat-vs-nested approximation on druid, which uses the unchanged PASS_AND_FAIL rule -- not a regression). Deferred to PR3 (APPTAINER_PR3_TRACKER.md): R2E-Gym's native run_local_evaluation grader/parser (host-side flat remains an approximation) and the reverse-patch .orig restore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
ffrujeri
left a comment
There was a problem hiding this comment.
Where this PR sits
flowchart LR
DS[(Dataset JSONL<br/>SWE-bench Verified)] --> Anyswe[anyswe_agent/app.py<br/>thin runner]
Anyswe -->|_build_swetask| Harness[swe_env/harness.py<br/>SweTask + ABC]
Anyswe -->|drive inner agent| SelfDrive[swe_env/self_drive.py]
SelfDrive <-->|chat/responses| Model[Model Server]
Anyswe -->|acquire_sandbox| Sandbox[swe_env/sandbox.py]
Sandbox --> Docker[providers/docker NEW]
Sandbox --> Appt[providers/apptainer]
Anyswe -->|git diff HEAD| Verify[swe_env/verify_task.py<br/>inline grader]
Verify --> Grade[harness.grade + compute_resolved<br/>reward 1.0/0.0]
Grade -.->|reward| RL[(Nemo-RL / eval)]
This converges the anyswe runner onto the swe_env decoupling: grading moves from a separate /verify resources server to an inline verify_task library call that re-grades the agent's patch in a fresh, hermetic sandbox, plus a new docker sandbox provider (was apptainer-only) and a shared swe_env/ library (harnesses, parsing, lifecycle, grading). Stacked on the unmerged base cmunley1/anyswe.
Really nice work — this is a substantial, well-structured collapse (swe_env 9→5 files), and a lot held up under review: /run is async with exception isolation (reward is always a non-null float), sandbox teardown is guaranteed on every path (no container leak), and compute_resolved checks out as line-by-line equivalent to canonical swebench's check_pass_and_fail / check_fail_only / get_resolution_status (including the per-repo FAIL_ONLY_REPOS selection and the verbatim git apply → git apply --reject → patch --fuzz=5 ladder). The suite runs green and lint-clean locally (158 passing).
The inline comments are mostly questions — a couple of possible regressions to sanity-check (model_patch recording, a masking case vs main), a reproducibility question (pinning swebench), some docker-provider create()/concurrency hardening, and a few test-coverage questions. None are blockers.
| params.mask_sample = True | ||
|
|
||
| update_metrics(params.metrics_fpath, {"resolved": resolved}) | ||
| update_metrics( |
There was a problem hiding this comment.
Was dropping model_patch from the metrics dict here intentional? On the base branch metrics.model_patch = patch or None was set, so as written AnySweVerifyResponse.model_patch comes back None even when patch_exists=True. Reward is unaffected (it's derived from resolved), but would downstream consumers that read the patch from the structured result (rollout viewer / BLADE / training) still get what they need, given the patch now survives only as the on-disk patch.diff? If it should be recorded, would something like this work?
| update_metrics( | |
| update_metrics( | |
| params.metrics_fpath, | |
| { | |
| "resolved": resolved, | |
| "patch_exists": bool(patch.strip()), | |
| "model_patch": patch or None, | |
| "agent_timed_out": agent_timed_out, | |
| "error_kind": report.error_kind, | |
| "mask_sample": params.mask_sample, | |
| "openhands_run_time": agent_run_time, | |
| }, | |
| ) |
There was a problem hiding this comment.
Thanks for the catch, adding it in
| report = await verify_task(self._grading_provider(), dataclasses.replace(task, model_patch=patch)) | ||
| resolved = bool(report.resolved) | ||
| if report.error_kind is not None or agent_timed_out: | ||
| params.mask_sample = True |
There was a problem hiding this comment.
Question on the masking here: this masks only on error_kind / agent_timed_out, whereas main's swe_agents grading also masks the case where a task is resolved and the agent hit max_iteration / context_window (the “patch happened to pass but the agent never properly submitted / blew its context” accidental-reward mask). Is dropping that mask intentional for this convergence? It's negligible for the eval resolve-rate, but for training rollouts those accidental passes would leak into the gradient at reward 1.0 — curious whether that's acceptable here.
| cfg: AnySweInstanceConfig = self.config | ||
| (cfg.persistent_dir / "instruction.txt").write_text(cfg.problem_info.get("problem_statement", "")) | ||
|
|
||
| runner = _RUNNER_TEMPLATE.format( |
There was a problem hiding this comment.
A question about the patch the runner extracts: _RUNNER_TEMPLATE captures the diff with git diff HEAD, which excludes untracked / newly-created files. Since patch.diff is always written, the git add -A && git diff --cached fallback in self_drive looks like it never runs — so if an agent resolves an issue by adding a new file/module, would that file be omitted from the graded patch (a possible false-negative)? This seems carried over from the base branch rather than introduced here, but since grading fidelity on SWE-bench Verified is the goal of this PR, would it be worth a git add -A before the diff so capture matches SWE-bench's own model-patch extraction (gold patches do include new files)?
There was a problem hiding this comment.
Good catch! :D I can add it in
| -e nemo-gym[dev] @ ../../ | ||
| # Flat (host-side) grading on docker/opensandbox builds the official SWE-bench eval | ||
| # script via swebench.make_test_spec; the nested/apptainer grading path doesn't need it. | ||
| swebench |
There was a problem hiding this comment.
Would it be worth pinning swebench here? The resolve verdict now depends on swebench's per-repo log parsers, FAIL_ONLY_REPOS, and eval-script templates, all of which change across releases — so a bare swebench could let the grade drift across installs and over time. Could it be pinned to the version that produced the reported 95/100 + agent matrix (e.g. swebench==4.1.0, matching mini_swe_agent_2) to keep the numbers reproducible?
| swebench | |
| swebench==4.1.0 |
| from swebench.harness.constants import FAIL_ONLY_REPOS | ||
| from swebench.harness.grading import get_logs_eval | ||
| from swebench.harness.test_spec.test_spec import make_test_spec | ||
| except Exception: |
There was a problem hiding this comment.
When the swebench import (or make_test_spec / get_logs_eval) fails, grading falls back to the generic pytest-only parser, which — per this file's own docstring — mis-grades non-pytest repos like django as unresolved even for a correct patch. Combined with the unpinned swebench dep, could a bad or mismatched install quietly degrade SWE-bench Verified scoring rather than surfacing? Would it be safer to fail loud for swe-bench instances when swebench is unavailable, instead of degrading to the generic parser?
| self._run_args = list(run_args or []) | ||
| self._keep_alive = keep_alive_command | ||
|
|
||
| async def _run(self, *args: str, timeout_s: int | float | None = None) -> tuple[int, str, str]: |
There was a problem hiding this comment.
Would it be worth bounding _run with an asyncio.Semaphore here? ApptainerProvider bounds its docker/apptainer subprocesses with self._semaphore (CLAUDE.md: “bound concurrent subprocess/external calls”), but the docker provider spawns CLI processes unbounded. Concurrency is bounded at the call site today, so it isn't a blocker — but would a configurable per-provider semaphore be a useful match to the apptainer baseline on the SWE hot path (one sandbox per rollout, many exec each)?
| raise SandboxCreateError(f"docker run timed out for image {spec.image!r}") from exc | ||
| if rc != 0: | ||
| raise SandboxCreateError(f"docker run failed (rc={rc}) for {spec.image!r}: {err.strip() or out.strip()}") | ||
| container_id = out.strip().splitlines()[-1].strip() |
There was a problem hiding this comment.
Two small questions about create() robustness:
- If
docker run -dreturns rc 0 with empty stdout, would"".splitlines()[-1]raiseIndexErrorbefore the intendedif not container_idguard on the next line — i.e. is that guard currently reachable? Guarding the empty case would keep the friendlySandboxCreateError. - The
docker run -dhas no pre-assigned--name/label — on a timeout that kills the CLI client after the daemon already started the container, would that orphan be reachable by teardown (no handle captured yet)? Apptainer pre-generates a uuid name so a failed start stays cleanable; would a--name <uuid>close that gap here? Something like:
| container_id = out.strip().splitlines()[-1].strip() | |
| lines = out.strip().splitlines() | |
| container_id = lines[-1].strip() if lines else "" | |
| if not container_id: | |
| raise SandboxCreateError("docker run did not return a container id") |
| # ---- pure helpers ----------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_compute_resolved(): |
There was a problem hiding this comment.
Two questions on the grading-contract coverage:
- The
harness.pymodule docstring states “a test asserts agent adapters never referencereset_repo/run_eval/grade”, but I couldn't find that test — does it exist somewhere, or would it be worth adding an AST/source-inspection guard (or softening the docstring)? The boundary does hold by inspection today. test_compute_resolvedexercises the no-status_mappassed-set fallback — would it be worth also covering thestatus_mappass_and_failpath (the one that runs for SWE-bench Verified: neutral SKIPPED/XPASS exclusion, ERROR/absent → fail)? A populated-status_mapcase would lock in the swebench-equivalence this PR depends on.
| # ---- infra-gated golden-patch equivalence scaffold -------------------------- | ||
|
|
||
|
|
||
| @pytest.mark.skipif( |
There was a problem hiding this comment.
Since test_flat_vs_nested_equivalence_on_gold unconditionally raise AssertionError(...), it will fail (not skip) for anyone who sets SWE_ENV_RUN_REAL_CONTAINERS=1 — would pytest.skip("equivalence harness not yet implemented") in the body read better as a known-TODO? (Unrelated: test-agent verified 3 small tests that close the verify_task.py coverage gaps — the flat_eval metadata branch, ttl stamping, and the report_to_reward wrapper — happy to share them if they'd be useful.)
…pfs (fix ENOSPC) --writable-tmpfs caps the writable layer at apptainer's `sessiondir max size` (default 64 MiB), so repos that rebuild on apply/eval (e.g. astropy C extensions + pip wheel cache in /tmp) hit ENOSPC -> empty patch / masked rollout, silently depressing apptainer resolve rates vs docker (an our-side artifact, not the model). When a caller requests --writable-tmpfs, ApptainerProvider.create() now swaps it for a per-instance DISK-backed `--overlay <mkdtemp dir>` (bounded by host disk, ~TB) and rmtrees that dir on every teardown path. Verified: a 200 MB write to /testbed on the astropy-13453 .sif now succeeds (ENOSPC at 64 MiB before); 56 apptainer provider unit tests pass; ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
anyswe_agent declared `swebench_tests_timeout` but never used it: the grading `verify_task` call passed no eval_timeout_s, so the eval was always capped at verify_task's hard default of 1800s. On the apptainer backend (slower overlay I/O) a few slow test suites (e.g. django-10097) exceeded 1800s -> error_kind=timeout -> masked (an our-side artifact; the instance is model-unresolved on docker anyway). Pass eval_timeout_s=params.swebench_tests_timeout so the config field is honored. Set it higher (e.g. ++<agent>.responses_api_agents.anyswe_agent.swebench_tests_timeout=3600) for the apptainer backend to avoid masking slow-but-valid evals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
Remove the SWE_ENV_REAL_SWEBENCH and SWE_ENV_RUN_REAL_CONTAINERS flags and the tests they gated -- test_swebench_real_instance.py (whole file) and test_flat_vs_nested_equivalence_on_gold in test_flat_eval.py. Both require docker/apptainer + published per-instance SWE-bench images unavailable in CI, so they only ever skipped (and the equivalence test `raise`d instead of skipping). If it can't run in CI, it shouldn't live in the suite. 158 unit tests pass, 0 skips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
- anyswe: record model_patch in the metrics dict again; mask resolved+truncated samples via the Responses-API "incomplete" contract (+ an agent_truncated metric); stage untracked files before diffing the agent patch (git add -A && git diff --cached) so new files land in the graded patch. - swe_env: fail loud (GraderDependencyError) when swebench is unavailable for a real SWE-bench instance instead of silently degrading to the generic pytest-only parser (which mis-grades non-pytest repos); verify_task propagates it rather than swallowing it into an unmasked reward-0. - docker provider: bound the docker CLI with a configurable concurrency semaphore (mirrors apptainer); pre-assign --name and reap orphaned containers on every create() failure path; guard the empty-stdout IndexError in create(). - tests: AST trust-boundary guard (agent adapters never call grader-only methods) + a populated status_map pass_and_fail case; 3 verify_task coverage tests (flat_eval branch, ttl stamping, report_to_reward); a fail-loud swebench-unavailable test; and a new test_docker_provider.py (the provider had no unit coverage). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
Remove development-history narration left over from the spine collapse and parity work: '(merged from registry.py/grading.py/lifecycle.py/model_endpoint.py)' banner notes, an '(unchanged behavior)' note, and a comment restating collect_ignore_glob. No code, docstring, or logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
Align docstrings with the current code: all built-in families grade host-side (flat), so correct the lingering 'nested/apptainer-only supports_provider' narrative in harness.py, harnesses/__init__.py and flat_eval.py (the nested path was removed in PR NVIDIA-NeMo#1694; with_flat_eval() is a no-op returning self). Document the docker/ apptainer split in anyswe _provider; note is_eval is accepted but unused; fix test docstrings (FakeSandbox -> _FakeProvider, 'three layers' -> two, swe_rebench gate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Codex <codex@openai.com>
What this is
Converges the anyswe runner (#1572) onto the #1677 swe_env decoupling: a thin agent drops any Gym agent into the SWE task container, extracts
git diff HEAD, and grades in-process viaverify_task— no separate resources server. Based on the #1677 stack, so this diff is just the convergence + fixes (~55 files).Before / after
What changed
responses_api_agents/swe_env/harnesses/,parsing/, sandbox lifecycle,verify_task,self_driveresponses_api_agents/anyswe_agent/swe_env; gradesgit diffin-processnemo_gym/sandbox/providers/docker/…/swe_env/parsing/swe_agents/swe_bench_ext/(lighthouse parser)…/swe_env/verify_task.pyresources_servers/swe_env/; inline grader, importers repointedresources_servers/swe_env//verifyserver…/anyswe_agent/setup_scripts/{swebench,r2e_gym,swebench_multilingual}.shharnesses/+ pipswebenchresponses_api_agents/openclaw_agent/mainartifact — neither added nor removed)Other fixes: docker flat-eval uses swebench's official per-repo parser (gold resolve 4%→95%); per-repo FAIL_ONLY eval-type + main's patch-apply ladder ported into the harnesses; apptainer
--pidremoved (≥1.3.6 rejects it);get_harnessself-registers + HF-name tolerant.Suggested review order
nemo_gym/sandbox/providers/registry.py+providers/docker/— the new provider lazy-registration (small, foundational).responses_api_agents/swe_env/harness.py— the contract:SweTask, the harness ABC, the registry, and the resolution rule (compute_resolved). Read first — everything else builds on it.responses_api_agents/swe_env/verify_task.py— the inline grading orchestrator (the "no/verifyserver" core).responses_api_agents/swe_env/sandbox.py+self_drive.py— sandbox lifecycle, then provision / egress /git diffextraction.responses_api_agents/swe_env/harnesses/swebench.py— the primary harness + the parity fixes (FAIL_ONLY, patch-apply ladder, masking); thenswe_bench_ext.py/r2egym.py/flat_eval.pyas needed.responses_api_agents/anyswe_agent/app.py— how it wires together: routing →_build_swetask→ provider →self_drive+verify_task.responses_api_agents/swe_env/parsing/— skim only: byte-identical relocation from main, no new logic.…/swe_env/tests/+…/anyswe_agent/tests/— coverage for the above.Validation — SWE-bench Verified (N=100/cell), in-line grader
Each agent × model run on the same 100 instances on both sandbox backends; cells are resolve rate (
mean/reward), all clean (mask ≤ 0.01):docker and apptainer agree within ±4 pts (mean Δ ≈ +1, no systematic gap) — the two backends are equivalent. (claude_code × gpt-5.5 skipped: the Claude CLI speaks Anthropic
/v1/messagesonly.) Gold-patch grader sanity beforehand: docker 95/100,.sif10/10.--writable-tmpfs's 64 MiB cap → per-instance disk--overlay.)Reproduce — gpt-5.5 on hermes (docker or apptainer)
Shared setup + dataset (gpt-5.5 is a reasoning model, so temperature/top-p must be 1.0):
Docker (default provider; each instance image pulls on demand from DockerHub — no pre-build):
Apptainer (build a
.sifper instance once, then run with no registry pulls):Resolve rate =
mean/rewardinresults/<name>_aggregate_metrics.json; a clean run hasmean/mask_sample == 0. (For other cells:anyswe_claude_code.yaml+ an opus model id speaks Anthropic/v1/messagesvia++$A.agent_kwargs.anthropic_base_url=…;anyswe_openclaw.yamlruns OpenClaw. Samecontainer_formatter/sandbox_provideroverrides switch either between docker and apptainer.)Deferred to a follow-up
openclaw eval (runner restored, eval deferred); R2E-Gym / Multilingual native nested graders (host-side flat grading approximates those — Verified/English is exact); routing needles for
swe-bench-ext/swe-rebench/nv-internal.🤖 Generated with Claude Code