Skip to content

openenv_swebench: SWE-bench-style agentic RL adapter (mirrors TB2)#1705

Open
Shi-Dong wants to merge 6 commits into
mainfrom
shi/openenv-swebench-adapter
Open

openenv_swebench: SWE-bench-style agentic RL adapter (mirrors TB2)#1705
Shi-Dong wants to merge 6 commits into
mainfrom
shi/openenv-swebench-adapter

Conversation

@Shi-Dong

Copy link
Copy Markdown
Contributor

Summary

Adds a self-contained SWE-bench-style agentic-RL adapter under
examples/experimental/openenv_swebench/, mirroring the existing
Terminal-Bench-2 adapter in examples/experimental/openenv/ (added in #1487).
It trains GLM-4.7-Flash with GRPO on SWE-Rebench-V2 "donor" tasks through the
unmodified OpenEnv env server — no changes to examples/experimental/openenv
and no OpenEnv patch/vendor.

New files (parallel to the TB2 set):

  • swebench_agent_function.py — the multi-turn adapter (the only file with real
    swebench-specific logic).
  • swebench_generate.py — reward_func (reads back metadata["reward"]).
  • swebench_launch_common.py — shared launch helpers.
  • run-openenv-swebench.py — GLM-4.7-Flash launcher.
  • make_swebench_data.py — prompt-data builder over a task pool.
  • README.md — setup, incl. the offline image data-prep step.

Why a new directory instead of generalizing the TB2 adapter

Per review preference, the TB2 adapter stays untouched. SWE-bench tasks differ
from TB2 on two axes, both handled entirely inside the new adapter:

  1. Working directory. TB2 tasks live at a fixed /app; SWE-Rebench-V2 donor
    images put the repo at varying paths (/app, /testbed, /<repo>, …). The
    adapter auto-detects the repo root once (the same probe tests/test.sh uses),
    caches it, and cds the agent there. OPENENV_TASK_WORKDIR forces a fixed dir.
  2. Scoring. The donor verifier does not write /logs/verifier/reward.txt; it
    prints RESULT: PASSED/RESULT: FAILED and exits 0/1. The adapter runs it via
    a plain exec step and derives the reward from the verdict line. No verdict
    (verifier crashed before scoring) → sample dropped, same as the TB2 adapter.

The env-client plumbing, capacity/retry handling, session-server prefix-echo,
wall-clock cap, and sandbox rm-hack are shared verbatim with the TB2 adapter.

Out of scope (not code)

The OpenEnv env server only pulls task.toml [environment].docker_image.
Donor tasks ship an environment/Dockerfile and no docker_image, so before a
run each task needs its image built/pushed and docker_image recorded — offline
data prep documented in the README, no code change here.

Test plan

  • python -m py_compile on all five modules (done locally, clean).
  • bash -n on the generated eval + workdir shell snippets (done locally, clean).
  • Smoke run: build a handful of donor images, make_swebench_data.py --n 8,
    serve the env, run-openenv-swebench.py --mode debug_rollout_only, confirm
    non-zero reward variance across a task where the base policy sometimes passes.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces an experimental SWE-bench-style GRPO training setup using HuggingFace's OpenEnv, adding documentation, data preparation scripts, a learning launcher, and an agentic adapter. Feedback on the changes suggests appending the user's UID to the shared temporary cache file path in swebench_agent_function.py to prevent permission conflicts on multi-user clusters, and refactoring exception checks in _is_retryable_env_error to use robust isinstance checks instead of string-based class name comparisons.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

# .git scan) and cache the answer, so every subsequent turn cds there cheaply.
# Setting OPENENV_TASK_WORKDIR forces a fixed dir and skips detection.
_TASK_WORKDIR = os.getenv("OPENENV_TASK_WORKDIR", "")
_REPO_ROOT_CACHE = "/tmp/.openenv_swebench_repo_root"

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.

medium

To prevent permission conflicts and directory sharing issues on multi-user clusters, shared temporary files should include the user's UID in their path. This ensures that different users running the adapter on the same host do not attempt to read or write to the same cache file.

Suggested change
_REPO_ROOT_CACHE = "/tmp/.openenv_swebench_repo_root"
_REPO_ROOT_CACHE = f"/tmp/.openenv_swebench_repo_root_{os.getuid()}" if hasattr(os, "getuid") else "/tmp/.openenv_swebench_repo_root"
References
  1. When using shared temporary directories (such as /tmp) for caching or process-specific storage, append the user's UID (e.g., using os.getuid()) to the path to avoid permission conflicts and directory sharing issues on multi-user clusters.

Comment on lines +182 to +192
def _is_retryable_env_error(e: BaseException) -> bool:
"""True when an env op failed only because no env slot was free (transient).

The docker-mode server caps concurrent envs; over that cap it either returns
CAPACITY_REACHED or closes the WebSocket cleanly (ConnectionClosedOK). Both
mean "retry once a slot frees up", not a genuine episode failure. Match the
close exceptions by class name so the adapter need not import websockets.
"""
if "CAPACITY_REACHED" in str(e):
return True
return type(e).__name__ in {"ConnectionClosedOK", "ConnectionClosedError", "ConnectionClosed"}

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.

medium

When identifying or filtering specific exception types, prefer using isinstance checks rather than parsing the exception's string representation or checking for class names within the exception string. Since websockets is a dependency of tbench2_env (which is required to run this code), we can safely import ConnectionClosed and perform an isinstance check.

Suggested change
def _is_retryable_env_error(e: BaseException) -> bool:
"""True when an env op failed only because no env slot was free (transient).
The docker-mode server caps concurrent envs; over that cap it either returns
CAPACITY_REACHED or closes the WebSocket cleanly (ConnectionClosedOK). Both
mean "retry once a slot frees up", not a genuine episode failure. Match the
close exceptions by class name so the adapter need not import websockets.
"""
if "CAPACITY_REACHED" in str(e):
return True
return type(e).__name__ in {"ConnectionClosedOK", "ConnectionClosedError", "ConnectionClosed"}
def _is_retryable_env_error(e: BaseException) -> bool:
"""True when an env op failed only because no env slot was free (transient).
The docker-mode server caps concurrent envs; over that cap it either returns
CAPACITY_REACHED or closes the WebSocket cleanly (ConnectionClosedOK). Both
mean "retry once a slot frees up", not a genuine episode failure.
"""
if "CAPACITY_REACHED" in str(e):
return True
try:
from websockets.exceptions import ConnectionClosed
return isinstance(e, ConnectionClosed)
except ImportError:
return type(e).__name__ in {"ConnectionClosedOK", "ConnectionClosedError", "ConnectionClosed"}
References
  1. When identifying or filtering specific exception types, prefer using isinstance checks rather than parsing the exception's string representation or checking for class names within the exception string.

Shi Dong added 5 commits July 17, 2026 19:41
SWE-bench images install the repo + pytest + runtime deps into a conda env
(default "testbed"), not the base env the container boots into. tests/test.sh
activates it for grading, but the agent's exec commands ran in the base env,
where pytest is missing and importing the target package raises
ModuleNotFoundError. The agent got no test feedback and its edits were never run
in the graded environment, yielding uniform zero reward across all rollouts.

Activate the env (mirroring test.sh) for every agent command via _apply_workdir,
gated on the env existing so it stays a no-op on donor images without one.
Configurable via OPENENV_CONDA_ENV (default "testbed"; "" disables).
The policy narrates with illustrative ```python planning snippets before
emitting its one shell command, and GLM-4.7 sometimes uses its native
<tool_call>...<arg_value>CMD</arg_value> format instead of a fenced block.
The old parser grabbed the first fence of any language (executing planning
python as a shell command) and fed raw <tool_call> XML to bash when no fence
was present, producing a syntax-error storm: ~24% of all agent turns errored,
so the model could never land a fix and every trajectory scored reward 0.

Extract the command by preferring the last bash/sh/shell (or language-less)
fence, skipping python blocks, and falling back to the <tool_call> arg. When
no runnable command is present, nudge for the right format instead of
executing the raw reply. Validated on 1501 real turns: garbage-to-bash drops
24% -> ~1%, 80% now yield a clean command.
The tbench2 env wraps every exec as bash -c '<cmd>' and docker-py
shlex-splits that string. The eval command's single-quoted grep pattern
collided with the wrapper quotes, leaving bash an unterminated $( -> parse
error, empty output, no reward marker, and every sample dropped as "no
verdict". Make the command single-quote-free (double-quote the grep
pattern) so it survives the wrapper.
…tamper snapshot, black

- Timeout no longer scored 0.0; dropped like any no-verdict episode (the cap
  spans infra phases, so a timeout can precede any verdict -> false negative).
- cleanup() now scopes the process kill to the current euid and honors
  OPENENV_SKIP_CLEANUP=1; docstring states the disposable-pod assumption.
- --num-rollout is now a real CLI flag (ScriptArgs field + Protocol), no longer
  a hardcoded 40 that the README documented but the CLI ignored.
- Snapshot pristine grader tests at reset and grade from the snapshot, so
  mid-episode tampering with the staged tests can't reach the grader (not a hard
  boundary vs a root agent; noted as such).
- Clarify the rm-hack comment: effective/necessary in Daytona mode, no-op in
  docker mode (dir is host-side, not bind-mounted).
- Reformat swebench_agent_function.py with black to fix the red CI check.
base_env_vars only derived four env vars from launcher flags, but the adapter
reads six more straight from os.getenv (OPENENV_TASK_WORKDIR, OPENENV_CONDA_ENV,
OPENENV_MESSAGE_TIMEOUT_S, OPENENV_SWEBENCH_TESTS_SRC,
OPENENV_SWEBENCH_TESTS_SNAPSHOT, OPENENV_EVAL_CMD). Under
MILES_SCRIPT_EXTERNAL_RAY=1 the rollout runs on a remote cluster that does not
inherit the submission shell, so those overrides silently reverted to defaults.
Forward each one that is set (membership test so an intentional empty value
still propagates).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant