feat(harness): rollout collection + openenv collect CLI#560
Conversation
Preserves the public API (`from openenv.core.harness import X` keeps working) while making room for a ``collect`` submodule that layers synthetic-dataset generation on top of the runtime primitives from RFC 005 / PR huggingface#471. Relative imports in the module are adjusted from ``.client_types`` → ``..client_types``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces ``openenv.core.harness.collect``, a thin layer on top of the harness runtime from RFC 005 / PR huggingface#471 for generating synthetic datasets from deployed environments: - ``EpisodeRecord`` — serializable view of one rollout + its verification. Uses ``_resolve_env_reward`` so any mismatch between a tool-result reward and ``verify.env_reward`` raises, preserving the "rewards in env" invariant end-to-end. - ``RolloutSerializer`` — append-only JSONL writer with a ``metadata.json`` sidecar. ``collected_episode_ids()`` enables resume. - ``CollectRunner.run()`` — orchestrates N episodes: session.create → harness.run_white_box → verify → optional rubric filter → serialize. Returns a ``CollectResult`` with aggregate stats. - ``build_model_step(llm_client)`` — adapts any ``LLMClient`` (OpenAI, Anthropic, and any OpenAI-compatible endpoint such as vLLM, TGI, Ollama, HF Inference, Together, Groq, Fireworks) into a ``ModelStep`` for the white-box harness. - ``push_to_hf_hub(output_dir, repo_id)`` — uploads ``results.jsonl``, ``metadata.json``, and an auto-generated dataset card to the Hub. The card's YAML front-matter tells the HF Dataset Viewer to treat the JSONL as ``split=train`` instead of trying to merge it with the metadata sidecar. 37 new tests (EpisodeRecord, RolloutSerializer, CollectRunner, build_model_step, push_to_hf_hub, build_dataset_readme). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``envs/openspiel_env/harness.py`` exposes an ``OpenSpielSessionFactory`` that wraps ``OpenSpielEnv`` (via ``StepEnvSessionAdapter``) and lets a harness drive any OpenSpiel game — tic_tac_toe initially — through a single ``play_move(action_id)`` MCP-style tool. - Initial prompt renders legal actions plus a human-readable board for TTT so the LLM can reason about positions without needing to decode the 27-float info_state tensor. - ``render_tic_tac_toe_board`` decodes the OpenSpiel TTT info_state (empty/X/O planes) into a 3x3 grid. Empty cells show their action_id so the prompt doubles as an action legend. - Follows the pattern established by ``envs/browsergym_env/harness.py`` in PR huggingface#471 — no changes to the underlying env, client, or protocol. Tests cover the board rendering, tool dispatch, reset-kwargs forwarding, and an end-to-end collect run against a scripted client exercising the full ``MCPHarnessAdapter`` → ``CollectRunner`` → JSONL pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps ``CollectRunner`` + ``OpenSpielSessionFactory`` into a single
command so a deployed OpenEnv environment can produce a dataset with
one call:
openenv collect openspiel:tic_tac_toe \\
--base-url https://user-space.hf.space \\
--output-dir /tmp/ttt-sft-v1 \\
-n 200 --provider openai --model gpt-5-mini \\
--push-to-hub user/ttt-sft-v1
Flags in short:
- ``--provider scripted | openai | anthropic`` — teacher selection.
Scripted picks the first legal action and requires no API key,
making ``openenv collect`` smoke-testable out of the box.
- ``--llm-endpoint / --llm-port`` — point at any OpenAI-compatible
endpoint (vLLM, TGI, Ollama, HF Inference, Together, Groq, ...).
- ``--push-to-hub REPO`` — upload the directory as a dataset after
collect; ``--private``/``--commit-message`` available.
- ``--resume / --no-resume`` — ``CollectRunner`` skips ``episode_ids``
already serialized on disk.
- ``--keep-losses`` — by default filters rollouts with reward < 0 so
the output is SFT-ready.
Env dispatch is via ``"family:variant"`` strings (e.g.
``openspiel:tic_tac_toe``). Unknown families raise a typer
``BadParameter`` with the supported set.
7 new CLI tests mocking ``CollectRunner`` + ``OpenSpielEnv`` so the
dispatch logic (scripted vs hosted provider, push-to-hub, filter
defaults, bad ``--env``) is exercised without network.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ``src/openenv/core/harness/README.md`` documents the collect module: quick-start (CLI + programmatic), output schema, and design notes covering the "thin envs" / "rewards in env" / "provider-agnostic teacher" choices. - ``examples/ttt_collect_demo.py`` — scripted teacher against either a built-in fake OpenSpiel client or a real deployed server (``--base-url``). Runs with zero setup for pipeline smoke-testing. - ``examples/ttt_collect_with_llm.py`` — provider-agnostic example that picks between hosted OpenAI/Anthropic (via ``--provider``) and any OpenAI-compatible self-hosted endpoint (via ``--llm-endpoint``), using the same collect pipeline unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
PR title or description contains an excluded keyword. |
Darktex
left a comment
There was a problem hiding this comment.
Note: This is an automated review by Claude Code, not a human review.
Alignment Review Report
Automated Checks
- Lint: PASS on PR files (lint failures are pre-existing in
envs/carla_env/, not introduced here) - Debug code: CLEAN in PR scope
Tier 1: Fixes Required
-
src/openenv/core/harness/collect.py(build_model_step):asyncio.run()is called inside a synchronous wrapper. If the caller is already inside a running event loop (async test runner, Jupyter, or an async orchestrator), this raisesRuntimeError: This event loop is already running. Useasyncio.get_event_loop().run_until_complete()with a loop-detection guard, or require callers to be sync-context-only and document it explicitly. This is the path that all LLM teacher traffic goes through — it will silently fail in common notebook and async-server contexts. -
src/openenv/core/harness/collect.py(CollectResult.episode_ids): The field is populated withplanned_ids— the full list of all planned episode IDs including skipped and dropped ones, not just collected ones. The nameepisode_idsimplies "what was collected". Either rename toplanned_episode_idsor filter to only collected IDs. Currentlyresult.episode_idsreturns stale IDs from prior runs whenresume=True, which is surprising.
Tier 2: Alignment Discussion
ALIGNMENT FLAG: HarnessAdapter name collision with RFC 005 WIP
- Principle at stake: RFC 005 defines
HarnessAdapteras an async ABC for wrapping external harness processes (OpenClaw, Claude Code). This PR introduces a differentHarnessAdapterinopenenv.core.harness— a synchronous protocol for white-box/black-box rollouts. Both will live underopenenv.coreonce #471 and this PR merge. The names are identical, the semantics are entirely different. - The concern: Imports like
from openenv.core.harness import HarnessAdapterwill silently shadow whichever one loads second. If RFC 005's implementation lands before this is resolved, the public API surface breaks for one of them. The PR does not acknowledge this conflict. - Suggested reviewer: @Darktex
ALIGNMENT FLAG: CLI env registry is hardcoded, bypasses the self-registration pattern implied by RFC 003
- Principle at stake: RFC 003 (MCP as universal standard) and the "be hands-on" principle assume environments are self-describing. The
_build_session_factorydispatch incollect.pyis a hand-writtenif env_name != "openspiel"block. Adding TextArena or Connect4 requires editing a core CLI file. - The concern: This is a load-bearing architectural pattern that will get copied for every new env. The right fix (Python entry points or a registry decorator) is not expensive to add now but becomes painful to retrofit. The PR authors flag this as a follow-up with no issue number.
- Suggested reviewer: @Darktex
Automated review by Claude Code | Learn more
|
Addressed the three review findings on the PR branch.
Added regression tests for all three cases. Verified locally with: All 57 targeted tests passed. |
|
Merging into the harness branch. |
5414298
into
huggingface:feature/harness-interface
* Add harness session runtime for training and evaluation * Handle MCP tool errors and lazy-load BrowserGym exports * Refine experimental harness APIs and reward forwarding * docs: mention environment_factory in harness tutorial * test: cover harness review regressions * feat(harness): rollout collection + openenv collect CLI (#560) * refactor(harness): move harness.py into harness/ package Preserves the public API (`from openenv.core.harness import X` keeps working) while making room for a ``collect`` submodule that layers synthetic-dataset generation on top of the runtime primitives from RFC 005 / PR #471. Relative imports in the module are adjusted from ``.client_types`` → ``..client_types``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(harness): add rollout collection module Introduces ``openenv.core.harness.collect``, a thin layer on top of the harness runtime from RFC 005 / PR #471 for generating synthetic datasets from deployed environments: - ``EpisodeRecord`` — serializable view of one rollout + its verification. Uses ``_resolve_env_reward`` so any mismatch between a tool-result reward and ``verify.env_reward`` raises, preserving the "rewards in env" invariant end-to-end. - ``RolloutSerializer`` — append-only JSONL writer with a ``metadata.json`` sidecar. ``collected_episode_ids()`` enables resume. - ``CollectRunner.run()`` — orchestrates N episodes: session.create → harness.run_white_box → verify → optional rubric filter → serialize. Returns a ``CollectResult`` with aggregate stats. - ``build_model_step(llm_client)`` — adapts any ``LLMClient`` (OpenAI, Anthropic, and any OpenAI-compatible endpoint such as vLLM, TGI, Ollama, HF Inference, Together, Groq, Fireworks) into a ``ModelStep`` for the white-box harness. - ``push_to_hf_hub(output_dir, repo_id)`` — uploads ``results.jsonl``, ``metadata.json``, and an auto-generated dataset card to the Hub. The card's YAML front-matter tells the HF Dataset Viewer to treat the JSONL as ``split=train`` instead of trying to merge it with the metadata sidecar. 37 new tests (EpisodeRecord, RolloutSerializer, CollectRunner, build_model_step, push_to_hf_hub, build_dataset_readme). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(envs/openspiel): add TTT harness session factory ``envs/openspiel_env/harness.py`` exposes an ``OpenSpielSessionFactory`` that wraps ``OpenSpielEnv`` (via ``StepEnvSessionAdapter``) and lets a harness drive any OpenSpiel game — tic_tac_toe initially — through a single ``play_move(action_id)`` MCP-style tool. - Initial prompt renders legal actions plus a human-readable board for TTT so the LLM can reason about positions without needing to decode the 27-float info_state tensor. - ``render_tic_tac_toe_board`` decodes the OpenSpiel TTT info_state (empty/X/O planes) into a 3x3 grid. Empty cells show their action_id so the prompt doubles as an action legend. - Follows the pattern established by ``envs/browsergym_env/harness.py`` in PR #471 — no changes to the underlying env, client, or protocol. Tests cover the board rendering, tool dispatch, reset-kwargs forwarding, and an end-to-end collect run against a scripted client exercising the full ``MCPHarnessAdapter`` → ``CollectRunner`` → JSONL pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add openenv collect command Wraps ``CollectRunner`` + ``OpenSpielSessionFactory`` into a single command so a deployed OpenEnv environment can produce a dataset with one call: openenv collect openspiel:tic_tac_toe \\ --base-url https://user-space.hf.space \\ --output-dir /tmp/ttt-sft-v1 \\ -n 200 --provider openai --model gpt-5-mini \\ --push-to-hub user/ttt-sft-v1 Flags in short: - ``--provider scripted | openai | anthropic`` — teacher selection. Scripted picks the first legal action and requires no API key, making ``openenv collect`` smoke-testable out of the box. - ``--llm-endpoint / --llm-port`` — point at any OpenAI-compatible endpoint (vLLM, TGI, Ollama, HF Inference, Together, Groq, ...). - ``--push-to-hub REPO`` — upload the directory as a dataset after collect; ``--private``/``--commit-message`` available. - ``--resume / --no-resume`` — ``CollectRunner`` skips ``episode_ids`` already serialized on disk. - ``--keep-losses`` — by default filters rollouts with reward < 0 so the output is SFT-ready. Env dispatch is via ``"family:variant"`` strings (e.g. ``openspiel:tic_tac_toe``). Unknown families raise a typer ``BadParameter`` with the supported set. 7 new CLI tests mocking ``CollectRunner`` + ``OpenSpielEnv`` so the dispatch logic (scripted vs hosted provider, push-to-hub, filter defaults, bad ``--env``) is exercised without network. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(harness): add README and runnable examples - ``src/openenv/core/harness/README.md`` documents the collect module: quick-start (CLI + programmatic), output schema, and design notes covering the "thin envs" / "rewards in env" / "provider-agnostic teacher" choices. - ``examples/ttt_collect_demo.py`` — scripted teacher against either a built-in fake OpenSpiel client or a real deployed server (``--base-url``). Runs with zero setup for pipeline smoke-testing. - ``examples/ttt_collect_with_llm.py`` — provider-agnostic example that picks between hosted OpenAI/Anthropic (via ``--provider``) and any OpenAI-compatible self-hosted endpoint (via ``--llm-endpoint``), using the same collect pipeline unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address collect feedback --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: burtenshaw <ben.burtenshaw@gmail.com> * fix: address browsergym harness review comments * fix: format collect files * envs/opencode_env: OpenCode coding-agent harness primitive (#603) * feat(opencode_env): OpenCode harness primitive (M1.1-M1.5) Harness primitive for running the OpenCode CLI agent in a sandbox against any OpenAI-compatible endpoint. Stacked on the PR #471 harness-runtime branch; implements the ResourceSession / ResourceSessionFactory contracts. Modes: - black_box: opencode talks directly to the configured endpoint. Verified end-to-end against real OpenAI (gpt-4o-mini) inside an E2B sandbox; the agent produces a correct fizzbuzz and the verifier scores reward=1.0. - transparent_proxy: a per-session FastAPI proxy runs inside the sandbox, forwards /v1/chat/completions to the configured upstream with logprobs=true injected, captures per-turn (messages, completion tokens, logprobs, finish reason) to a JSON-lines trace, and strips logprobs from what opencode sees. Handles both unary and streaming (SSE). Caps max_tokens and auto-translates to max_completion_tokens for gpt-5.x/o* models. Components: - config.py: OpenCodeConfig (generic provider/base_url/api_key/model fields supporting OpenAI, Anthropic, and OpenAI-compatible endpoints; proxy tuning knobs; sandbox_home override for non-E2B backends). - opencode_runtime.py: pure builders for opencode.json, install/run shell commands, and env vars. - task.py: OpenCodeTask pydantic model (instruction + optional setup_shell + file uploads + opaque metadata); coerces from str/dict. - sandbox/base.py: SandboxBackend / SandboxHandle / BgJob Protocols. - sandbox/e2b.py: E2BSandboxBackend with a threaded BgJob wrapper that provides timeout support over E2B's CommandHandle. - harness.py: OpenCodeSession + OpenCodeSessionFactory; in Mode B, installs proxy deps, uploads the proxy module, starts it as a bg job on localhost:7000, rewrites opencode.json to point at the proxy, and forces @ai-sdk/openai-compatible so routing goes through /v1/chat/completions. - interception.py: InterceptionProxy (FastAPI, unary + streaming), per-turn trace capture, CLI entry point for sandbox-side execution. Tests: 37 unit tests plus 3 live integration tests gated on E2B_API_KEY / OPENAI_API_KEY. Known limitation: OpenAI's gpt-5.x chat family refuses logprob requests, so Mode B live validation against OpenAI requires gpt-4o-mini or older. vLLM (the intended training-time upstream) returns logprobs natively. * feat(opencode_env): live vLLM validation + post-rollout summary End-to-end Mode B now verified against a live vLLM tunnel: - Qwen/Qwen3.5-4B on 2x A100 (tp=2, 16K ctx) via `vllm serve` - Cloudflared tunnel exposes the endpoint publicly - E2B sandbox runs opencode against the tunnel through the in-sandbox proxy - Proxy captures 4 turns, 36 tokens with real per-token logprobs - Agent produces correct fizzbuzz.py, verifier scores reward=1.0 - 55.86s total (sandbox + install + 4 LLM turns + verify) Changes: - config: add proxy_disable_thinking flag; plumbed through harness -> proxy via --disable-thinking CLI arg. - interception: inject chat_template_kwargs.enable_thinking=false on forwarded requests when enabled (Qwen3/Qwen3.5 tokenizer hook); also split the request handler into unary + streaming paths (SSE) and auto-translate max_tokens -> max_completion_tokens for gpt-5.x/o* models. - sandbox/e2b: E2B's commands.run(background=True) has a default server-side timeout=60 that kills long-running opencode bg jobs; pass timeout=0 to disable it. - live_watch: RolloutSummary / collect_rollout_summary / print_rollout_summary — post-rollout structured report reading proxy trace, opencode event log, and the workdir listing + file contents. - tests/test_harness_live_vllm: end-to-end Mode B test against a live vLLM tunnel (gated on VLLM_TUNNEL_URL + E2B_API_KEY), asserts logprobs are captured with shape matching completion tokens. * docs(opencode_env): README with Mode A + Mode B quickstarts and full config * feat(opencode_env): retries, error surfacing, model override Fixes the server-mode rollout path. Under load E2B can return sandboxes that aren't yet accepting commands, curl-install can transiently fail, and opencode's internal title-generation call can emit a stripped model id (e.g. ``Qwen3.5-4B`` instead of ``Qwen/Qwen3.5-4B``) that vLLM rejects with 404. Previously the proxy silently swallowed upstream error bodies and returned an empty event-stream, which opencode interpreted as an empty assistant turn. Changes: - harness: ``_wait_for_sandbox_ready`` probes ``echo ok`` up to 15x before issuing commands. - harness: ``_exec_with_retry`` wraps install / pip-deps / extra-setup with exponential backoff, up to 3 attempts, bailing on deterministic errors (non-empty stderr). - interception: ``ProxyConfig.model_override`` rewrites the ``model`` field on every forwarded request to the exact upstream id, bypassing opencode's provider-prefix quirks. Plumbed through as ``--model-override`` on the CLI. - interception: ``_proxy_streaming`` now inspects upstream status before committing to an SSE response — non-2xx returns a JSON error response to opencode AND logs the full upstream body to proxy.log, so the caller sees the real failure reason. - harness: ``_start_proxy`` passes ``--model-override`` built from ``config.model`` so the upstream always sees the right id. - harness: proxy deps install now uses ``_exec_with_retry`` too. Verified via local uvicorn: fizzbuzz + fibonacci tasks both succeed end-to-end through the server path in 19-20s with reward=1.0, 3-4 productive turns, and real per-token logprobs captured on every turn. * feat(opencode_env): add serve driver + opencode_client (Phase 2b primitive) Adds the infrastructure for driving opencode via its HTTP server instead of the CLI. This is the Phase 2b foundation — fine-grained MCP tools in the consumer server wrap these primitives. Changes: - opencode_client.OpenCodeServerClient: typed httpx wrapper over the OpenAPI spec at /doc. Sync and async methods for create_session, send_message / send_prompt_async, list_messages, get_session, get_all_status, abort, plus stream_events / astream_events (SSE) and a wait_for_ready helper. Base64 basic-auth when OPENCODE_SERVER_PASSWORD is set. - harness.OpenCodeSession: new ``driver: Literal["cli", "serve"]`` field. driver="cli" is today's `opencode run` path. driver="serve" stores serve_public_url + serve_client + serve_session_id on the session. start_agent() dispatches on driver; wait_for_completion() polls /session/:id for idle when driver="serve". New abort() method hits /session/:id/abort for cancellation. - harness.OpenCodeSessionFactory: new ``driver`` constructor arg plumbs through to create(). _start_serve() runs opencode serve bound to 0.0.0.0:4096 as a bg job, probes it internally via curl, then uses the sandbox backend's get_host(4096) to build a public URL (E2B returns https://4096-<sandbox_id>.e2b.app). Fails fast if the backend doesn't support get_host. - tests/test_opencode_client.py: 7 unit tests covering URL/method/body shape, auth header, prompt text extraction, abort bool, limit param, wait_for_ready polling, SSE event helpers. Uses httpx MockTransport patched via monkeypatch — no live opencode serve needed. - tests/test_harness.py: _FakeSandbox now responds to the health-probe "echo ok" command so the existing factory tests work after the Phase-1 reliability layer landed. Verified: 34 unit tests pass (7 new + 27 existing), driver=cli path unchanged. End-to-end E2B spike confirms: sandbox_id assigned in 0.4s opencode install 2.5s opencode serve --port 4096 --hostname 0.0.0.0 listening in 1s sandbox.get_host(4096) returns https://4096-<id>.e2b.app external /doc returns HTTP 200 with OpenAPI spec external POST /session returns real session metadata Next: wire 4 new MCP tools (start_rollout / get_state / abort_rollout / finalize_rollout) in the consumer env + SSE endpoint for live rollout events. Ship to HF Space. * fix(opencode_env): bump proxy-start wait from 10s to 60s Uvicorn+fastapi cold boot inside E2B can take >10s under load; the tight probe loop was producing false 'proxy did not start within 10s' errors. 60s cap at 0.5s intervals keeps the retry fast while tolerating the slow path. * fix(opencode_env): cwd into workdir, fix idle detection, preserve reasoning Three audit fixes surfaced by end-to-end testing through the deployed env server: 1. `_start_serve` now `cd`s into workdir_path(config) before launching opencode serve. Without this, the agent writes files to $HOME and RolloutResult.workdir_files (reading /home/user/workdir) comes back empty — the "rollout succeeded but nothing appeared" symptom. 2. `wait_for_completion` idle check was `status.get("idle")` but opencode's /session/status returns `{"type":"idle"}`, not `{"idle":true}`. Every serve-driver rollout silently timed out at agent_timeout_s. Now checks `status.get("type") == "idle"` and adds structured logging on every tick. 3. Interception proxy now preserves `delta.reasoning` on streaming chunks and surfaces it as `message.reasoning` on the assembled response. HF Router's Qwen3.5 thinking mode returns reasoning as a separate field from content; previously it was dropped. 4. `upstream_model` no longer strips the Qwen/ org prefix — full `config.model` is forwarded as the model-override so both vLLM (served as `Qwen/Qwen3.5-4B`) and HF Router (requires `Qwen/<repo>:<provider>`) work. 5. Structured logging at every factory.create phase so operators can see exactly which step is stuck (sandbox, bootstrap, proxy, serve, wait_for_completion). * test(opencode_env): drop live integration tests requiring external secrets The four live tests (OpenAI / vLLM / mode-B / E2B) required OPENAI_API_KEY, VLLM_URL, or E2B_API_KEY to execute and were development-time fixtures rather than CI checks. The core functionality is already covered offline by test_harness.py (end-to-end factory lifecycle against a mock sandbox + mock OpenAI endpoint), test_interception.py (proxy forward + per-turn record assembly), test_opencode_client.py (serve client over httpx mocks), and test_sandbox_base.py (E2BSandboxBackend key-required unit). * feat(opencode_env): deployable env with HF Space, Gradio UI, and 3-endpoint catalog Wraps the existing OpenCode harness primitive in a deployable OpenEnv environment that can run as an HF Space, exposing a single MCP ``run_rollout`` tool plus a Gradio web UI at /web. Highlights ---------- - Single MCP tool ``run_rollout`` accepting a uniform Task shape (instruction + setup[] + verify[] bash commands), reward = passed_verify / total or override via /home/user/logs/verifier/reward.txt. - Endpoint shorthand catalog (``vllm`` / ``openai`` / ``hf_router``) that resolves base_url / api_key / model from env vars + sane defaults. - In-sandbox FastAPI proxy (``transparent_proxy`` mode) injects logprobs=true and captures per-token logprobs for GRPO training. - Optional ``black_box`` mode skips the proxy for SFT / eval rollouts. - Pre-baked E2B template (``opencode-rl``) drops sandbox cold start from ~2min to ~6s by shipping opencode + proxy deps in the image. - Streaming Gradio UI: /run handler is a generator that yields a live phase log (sandbox boot → setup → agent → verify → collect) so the user sees progress instead of a spinner. - HF Space deployed at AdithyaSK/opencode-env, end-to-end verified against vLLM, OpenAI, and HF Router (all 3 reward=1.0 on the binary_search smoke task). Layout ------ envs/opencode_env/ {client.py, models.py, __init__.py} # HTTP client + pydantic {config.py, harness.py, opencode_runtime.py, task.py} # primitive (CLI-only) server/{app.py, opencode_environment.py, gradio_ui.py, catalog.py, Dockerfile} # FastAPI + Gradio + MCP sandbox/{base.py, e2b.py, interception.py, build_template.py} # E2B + proxy + template {pyproject.toml, openenv.yaml, uv.lock, README.md, .dockerignore, .gitignore} Removed (CLI-only refactor) --------------------------- - harness.py: dropped the ``opencode serve`` driver path (~270 LOC). - Deleted opencode_client.py, live_watch.py, env-local tests/. CI / tests ---------- - New tests/envs/test_opencode_env.py: 14 unit tests (no E2B, no LLM, no network) covering catalog resolution, model serialization, and task coercion. Plus one @pytest.mark.integration test that runs opencode end-to-end against the deployed Space (skipped by default). - sandbox/__init__.py: e2b import wrapped in try/except so the package loads cleanly without e2b installed (CI-friendly). - Added opencode-env to .github/workflows/docker-build.yml matrix so the image is built and pushed to GHCR alongside other envs. openenv-core dependency ----------------------- Currently pinned to the ``opencode-harness`` branch via git because PyPI's ``openenv-core`` (0.2.x) does not yet ship the ``openenv.core.harness`` module that this env imports. Switch to ``openenv-core[core]>=0.2.2`` once RFC 5 / PR #471 ships in a published release. The intended end-state is documented inline in pyproject.toml. * docs(opencode_env): add examples/opencode_env_simple.py Minimal end-to-end example: hits the deployed HF Space, runs a binary_search rollout via the MCP run_rollout tool, prints the reward + per-turn logprobs + the file the agent produced. Mirrors the per-env convention in ``examples/`` (echo_mcp_demo.py / coding_env_inference.py / atari_simple.py etc.). Defaults point at ``https://adithyask-opencode-env.hf.space``; override with ``OPENCODE_ENV_SPACE`` to target a different Space or local container. Requires ``OPENAI_API_KEY`` in the environment (passed in the request body, no Space secret required). Swap ``endpoint="openai"`` for ``"vllm"`` or ``"hf_router"`` to exercise the other backends. * fix: secure opencode proxy command --------- Co-authored-by: burtenshaw <ben.burtenshaw@gmail.com> * fix: address ci docs and formatting * feat(tutorials): add SFT warm-up tutorial with reasoning_gym collect support (#636) * feat(collect): add reasoning_gym support + --dataset-config + --system-prompt Extends `openenv collect` to support reasoning_gym environments: - Register ReasoningGymSessionFactory in _build_session_factory - Add --dataset-config option (JSON string) for env-specific config - Add --system-prompt option to override the default system prompt - Add ReasoningGymSessionFactory in envs/reasoning_gym_env/harness.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(harness): generate random seed per episode when none provided reasoning_gym server requires seed when dataset_name is specified. CollectRunner never passes a seed, so generate one per episode to ensure variety across collected rollouts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(llm_client): use max_completion_tokens for OpenAI client Newer OpenAI models (gpt-5-mini, o1, o3) reject max_tokens and require max_completion_tokens instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(llm_client): use max_completion_tokens only for OpenAI API, not self-hosted Newer OpenAI models require max_completion_tokens; self-hosted OpenAI-compatible endpoints (vLLM, Ollama, TGI) only support max_tokens. Add use_max_completion_tokens flag to OpenAIClient, enabled automatically by create_llm_client for the openai provider and left off for self-hosted endpoints via --llm-endpoint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(llm_client): omit temperature for OpenAI API newer models gpt-5-mini and other newer OpenAI models only accept the default temperature (1) and reject any explicit value. Omit temperature entirely when use_max_completion_tokens is set; self-hosted endpoints are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(collect): add rich progress bar to CollectRunner Shows episode count, collected count, running avg reward, and elapsed time during collection — previously the loop ran silently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(tutorials): add SFT warm-up tutorial with end-to-end validated notebook Adds docs/source/tutorials/sft-warmup.md and examples/sft_warmup.ipynb. Tutorial collects rollouts via CollectRunner Python API, pushes to Hub, filters by reward, and fine-tunes Qwen3-1.7B with SFTTrainer. Validated end-to-end: 0% → 64% format compliance, 4% → 60% accuracy on chain_sum. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tutorials,collect): address PR review findings - Scope use_max_completion_tokens to models that require it (gpt-5-mini, o1, o3, o4-mini) — was incorrectly applied to all OpenAI models, breaking gpt-4o and other standard models - Remove duplicate YOUR_HF_USERNAME re-declaration in tutorial section 10 - Add missing asyncio import in tutorial section 10 code block - Fix prose: max_seq_length → max_length (correct SFTConfig field name) - Fix usort import order in collect.py and harness/collect.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(collect,tutorials): address Greptile review findings - Move _MAX_COMPLETION_TOKENS_PREFIXES to module level as frozenset - Use prefix matching for versioned model names (o1-2024-12-17, etc.) - Replace asyncio.run() with await in tutorial section 10 (Jupyter compat) - Use json.dumps for tool_call text in to_qwen3_messages (safe escaping) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: burtenshaw <ben.burtenshaw@gmail.com> * fix: address harness review feedback * fix: format harness lint * fix: address harness review comments --------- Co-authored-by: Sergio Paniego Blanco <sergiopaniegoblanco@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Adithya S K <adithyaskolavi@gmail.com>
Summary
Adds a
collectsubmodule underopenenv.core.harnessand anopenenv collectCLI that turn any deployed OpenEnv environment + anyLLMClientinto a TRL-ready JSONL dataset pushed to the Hub, using the session-based runtime from #471 as foundation. First env driver:OpenSpielSessionFactoryfor tic_tac_toe.Stacked on #471 — base branch is
feature/harness-interface. This PR should merge after #471.Type of Change
(Refactoring is a pure rename
harness.py→harness/__init__.pyto make room for the submodule; no behaviour change.)Alignment Checklist
Before submitting, verify:
.claude/docs/PRINCIPLES.mdand this PR aligns with our principles.claude/docs/INVARIANTS.mdand no invariants are violated/pre-submit-pr(orbash .claude/hooks/lint.shand tests) and addressed all issuesRFC Status
Test Plan
66 new tests; the existing 1009 pass unchanged.
Reproduce locally:
End-to-end smoke test (no Docker, no API key) — verifies the pipeline produces real JSONL:
PYTHONPATH=src:envs uv run openenv collect openspiel:tic_tac_toe \ --base-url <deployed-openspiel-space-with-OPENSPIEL_GAME=tic_tac_toe> \ --output-dir /tmp/ttt-smoke \ -n 5 --provider scripted cat /tmp/ttt-smoke/results.jsonl | head -1 | python -m json.toolAgainst a real LLM teacher + push to Hub:
OPENAI_API_KEY=... PYTHONPATH=src:envs uv run openenv collect openspiel:tic_tac_toe \ --base-url <deployed-openspiel-space> \ --output-dir /tmp/ttt-sft-v1 \ -n 20 --provider openai --model gpt-5-mini \ --push-to-hub <user>/ttt-sft-v1Validated manually against a live HF Space running
openspiel_envwithOPENSPIEL_GAME=tic_tac_toe; dataset visible in the HF Dataset Viewer thanks to the auto-generated README front-matter.Claude Code Review
check-debug.shis clean in oursrc/scope (pre-existing hits in unrelated files).should_keep) bypasses RFC 004 Rubric abstraction. v1 uses a bareCallable[[EpisodeRecord], bool]; a futurerubric=arg would subsume it.cli/commands/collect.py. Follow-up candidate for an entry-points mechanism.Related
feature/harness-interface.