Skip to content

openenv: TB2 agentic RL adapter + GLM-4.7-Flash launcher#1487

Merged
Shi-Dong merged 12 commits into
mainfrom
shi/openenv-miles-adapter
Jul 17, 2026
Merged

openenv: TB2 agentic RL adapter + GLM-4.7-Flash launcher#1487
Shi-Dong merged 12 commits into
mainfrom
shi/openenv-miles-adapter

Conversation

@Shi-Dong

@Shi-Dong Shi-Dong commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a self-contained example for driving the HuggingFace OpenEnv Terminal-Bench-2 (tbench2) environment from miles GRPO training. A generic miles-side adapter runs the multi-turn agentic loop against an unmodified upstream OpenEnv env server (used as a dumb docker-exec transport); all TB2 fidelity lives in the adapter. Tokens / logprobs / loss-masks are captured natively through the session server, and the env-computed binary pytest reward is fed back into the policy.

Everything lives under examples/experimental/openenv/; this PR carries no core-miles changes. The one generic hook it originally bundled (surfacing agent-reported tool time on Sample.non_generation_time) has been peeled out into its own standalone PR (#1663), and this PR is stacked on top of it.

Adapter-driven fidelity

miles is env-agnostic, so something must own the per-env translation (action schema, multi-turn loop, where the reward lives). Rather than forking/vendoring OpenEnv, that logic lives entirely in the miles-side adapter:

  • Agent commands and the evaluation run in the task image WORKDIR (/app, via OPENENV_TASK_WORKDIR), matching the real TB2 image contract (upstream OpenEnv otherwise defaults to /task).
  • The task's tests are staged and the canonical tests/test.sh is run to produce the binary pytest reward.
  • The same adapter works over a real-docker host (an OpenEnv agent server) or an optional Daytona sandbox pool (OPENENV_DAYTONA_*), chosen purely by env vars.

Multi-turn loop

reset(task_id) → { policy emits one shell command in a single ```bash block → step(exec) → feed output back } until TASK_COMPLETE or OPENENV_MAX_TURNSevaluate → binary reward (1.0 = all tests pass, else 0.0). Each episode is wall-clock bounded by OPENENV_MAX_ROLLOUT_TIME_SECONDS; a straggler that exceeds it is terminated and scored 0 so it can't stall the whole rollout batch.

Files

  • openenv_agent_function.py — the generic TB2 adapter: the multi-turn agentic loop, the WebSocket env transport (handles capacity/close semantics), and the optional Daytona sandbox pool. Wired via --custom-agent-function-path openenv_agent_function.run.
  • openenv_generate.py — the --custom-rm-path reward fn; task-agnostic, reads the env reward the adapter stored in metadata["reward"].
  • openenv_launch_common.py — shared, model-agnostic launch fragments (process cleanup, GRPO / optimizer / rollout / agent flags, W&B + Prometheus wiring, OpenEnv/Daytona env-var plumbing) so per-model launchers can't silently drift.
  • run-openenv-tbench2.py — the GLM-4.7-Flash GRPO launcher; sets the OPENENV_* env vars and the GLM serving/training profile, delegating the shared flags to openenv_launch_common.
  • make_tbench2_data.py — builds the prompt set from a terminal-bench-2 checkout: one row per discovered task_id (system prompt + metadata["task_id"]); the env serves the per-task instruction at reset().

The generic miles/rollout/generate_hub/agentic_tool_call.py throughput-accounting hook this example relies on lives in its own base PR #1663 (this PR stacks on it). The DeepSeek-V4-Flash launcher variant is split into a stacked follow-up: #1662.

How to run

# 1. Install the env client where the rollout runs (isolate if deps clash with the miles image)
pip install -e <OpenEnv>/envs/tbench2_env
# 2. Get the TB2 suite + build prompt-data (task_ids)
git clone --depth 1 https://github.com/laude-institute/terminal-bench-2.git /workspace/terminal-bench-2
python make_tbench2_data.py --tasks_dir /workspace/terminal-bench-2 --output /root/tbench2_train.jsonl
# 3. Serve the env on a real-docker host, then launch
python run-openenv-tbench2.py --openenv-env-url http://<env-host>:8080

Validation notes

  • tbench2 env contract validated standalone: reset(task_id)execevaluate returns the real binary reward (confirmed both 1.0 and 0.0).
  • GLM-4.7-Flash GRPO launched end-to-end via run-openenv-tbench2.py against an OpenEnv agent server: rollouts drive the full multi-turn loop and produce env-computed rollout/raw_reward; GRPO steps advance.
  • Caveat for reviewers: the binary sparse reward needs a task subset where the base policy sometimes succeeds (advantage variance). On the full TB2 suite GLM-4.7-Flash's low base solve-rate yields a near-flat signal, so a variance-band subset (or a stronger base) is needed to show a learning climb — a dataset choice, not a seam change.

Operational caveat: sandbox container leakage

The real-docker path relies on unmodified upstream OpenEnv for the sandbox lifecycle, and upstream currently leaks containers on unclean disconnects:

  • The env server creates each task container with remove=False and only tears it down (container.stop() + remove(force=True)) inside its close() handler, i.e. on a clean session end.
  • The idle-session reaper is off by default (session_timeout is None, no env var wires it on), so nothing sweeps sessions whose WebSocket dropped without a clean close.
  • Net effect: whenever a rollout dies uncleanly — trainer crash, OOM-kill, dropped WS — its sandbox container is orphaned and lingers indefinitely. On this run we found 14 orphans aged 11–19h accumulated across earlier crashed sessions.

The adapter side bounds live episodes (OPENENV_MAX_ROLLOUT_TIME_SECONDS, per-message timeout, max turns) and closes the env via async with, so a healthy run self-cleans; the leak is strictly a crashed-session residue. Until upstream enables the reaper (or we set a session_timeout), sweep orphaned containers between runs on the env host, e.g. docker rm -f of any TB2 task-image container older than the episode wall-cap (2h is a safe threshold since the cap is 3600s).

Test plan

  • tbench2 env contract: standalone reset(task_id)execevaluate returns 1.0 / 0.0.
  • run-openenv-tbench2.py reaches GRPO steps with rollout/raw_reward populated from the env.
  • Task-subset / infra decision to demonstrate a reward climb (variance-band subset).

@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 experimental integration with OpenEnv, adding dataset generation scripts, concurrent environment servers, and agent adapters for both Phase-1 (Echo smoke test) and Phase-2 (Coding learning run) setups. The feedback highlights critical improvements to prevent potential crashes and resource leaks: specifically, using AsyncOpenAI as an asynchronous context manager to avoid unclosed connections, guarding against empty completion choices to prevent IndexError, and applying safe fallbacks for sample metadata to avoid AttributeError crashes.

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.

Comment thread examples/experimental/openenv/openenv_coding_agent_function.py Outdated
Comment thread examples/experimental/openenv/openenv_echo_agent_function.py Outdated
Comment thread examples/experimental/openenv/openenv_generate.py
@Shi-Dong Shi-Dong changed the title Add OpenEnv example: miles <-> HuggingFace OpenEnv integration openenv: TB2 agentic RL adapter + GLM-4.7-Flash launcher Jul 14, 2026
@Shi-Dong
Shi-Dong force-pushed the shi/openenv-miles-adapter branch from 6f576df to 777fe2a Compare July 14, 2026 03:31
@Shi-Dong
Shi-Dong changed the base branch from main to shi/rollout-non-generation-time July 14, 2026 03:32
Shi Dong added 4 commits July 14, 2026 01:08
The per-episode client opened in run() was never closed on any path
(success, timeout, or exception), leaking httpx connections/sessions
under concurrent rollout. Close it in a finally so every path releases it.
The single-node TB2 example only ever drives the env server over
--openenv-env-url; the optional Daytona sandbox pool was never exercised
here. Remove the pool (adapter class + launch plumbing + launcher fields)
and its docstrings so the example carries only the code path it uses.
@Shi-Dong
Shi-Dong marked this pull request as ready for review July 14, 2026 15:11
Shi Dong added 3 commits July 14, 2026 08:22
…tling

The WebSocket env server holds an FD per live session + Docker connection
and leaks sockets on unclean disconnects, so a long run under the default
1024 soft nofile exhausts it and the accept loop fails every connection
with EMFILE, silently throttling rollouts. Document `ulimit -n` in the
start step and the leakage note.
@Shi-Dong
Shi-Dong force-pushed the shi/openenv-miles-adapter branch from 4889fe3 to 3e97c8b Compare July 14, 2026 15:37
Base automatically changed from shi/rollout-non-generation-time to main July 15, 2026 12:30

@nblintao nblintao left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Some nits regarding the reward calculation but we can refine that in the future when we have bigger runs.

Comment on lines +108 to +111
return float(raw) if raw else 0.0
except ValueError:
return 0.0
return 0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we can't find reward, it's likely an infra issues. I think we should return None to throw away the sample instead of introducing false zero.

# fails to run (e.g. in a reused sandbox where /logs survives across episodes).
"mkdir -p /tests /logs/verifier && rm -f /logs/verifier/reward.txt && "
f"cp -a {_TB2_TESTS_SRC}/. /tests/ 2>/dev/null || true; "
f"{_EVAL_CD_CMD}bash /tests/test.sh > /tmp/tb2_testsh.log 2>&1; "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should store the exist code of test.sh too to help differientiate infra error.

Per nblintao's review: when the canonical eval produces no reward marker,
an empty value (reward.txt absent), or a non-numeric value, the harness
never produced a verdict -- an infra/harness failure, not a task the agent
legitimately failed. Scoring it 0.0 injects a false negative into the
training signal.

_parse_reward_marker now returns None in those cases; run() propagates it
by returning None so the sample is dropped (the same drop signal already
used on episode exceptions). A genuine failure writes reward.txt = 0 and is
still returned as 0.0.
Per nblintao's second review comment: capture test.sh's exit code and
surface it when a sample is dropped for having no recoverable reward, so
the drop can be triaged -- a nonzero rc points at a test.sh crash
(infra/harness failure) vs. a clean run that wrote no verdict.

The eval exec now echoes $? on a __TB2_TESTSH_RC__ marker right after
test.sh; _parse_testsh_rc reads it back and the value is threaded through
_multi_turn into agent_metrics and into the drop-path warning.

Diagnostic only: the exit code does NOT drive the drop decision. A
nonzero rc from merely-failing tests is a legitimate reward 0, not an
infra error, so keying the drop on rc would be wrong; the existing
"no recoverable reward" condition remains the sole drop trigger.
@Shi-Dong
Shi-Dong merged commit 913633d into main Jul 17, 2026
37 checks passed
@Shi-Dong
Shi-Dong deleted the shi/openenv-miles-adapter branch July 17, 2026 07:02
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.

3 participants