Skip to content

feat(workflow): add OpenEnvWorkflow adapter for HuggingFace OpenEnv - #1576

Draft
NancyFyong wants to merge 4 commits into
areal-project:mainfrom
NancyFyong:feat/openenv-adapter
Draft

feat(workflow): add OpenEnvWorkflow adapter for HuggingFace OpenEnv#1576
NancyFyong wants to merge 4 commits into
areal-project:mainfrom
NancyFyong:feat/openenv-adapter

Conversation

@NancyFyong

Copy link
Copy Markdown

Description

Add a generic OpenEnvWorkflow that speaks the
HuggingFace OpenEnv protocol so any
OpenEnv-compatible environment (BrowserGym, OpenSpiel, coding sandboxes,
chess, echo, ...) can be trained against with GRPO through YAML alone -- no
new Python per environment.

Key pieces:

  • areal/api/openenv_api.pyOpenEnvConfig dataclass + ActionParser / ObservationFormatter Protocols, with post-init validation (base_url xor provider, provider extras).
  • areal/workflow/openenv.pyOpenEnvWorkflow.arun_episode drives the reset()[chat.completion → parse action → env.step()] * N loop. Per-step rewards go on each LLM completion via ArealOpenAI.set_reward, so GRPO grouping sees per-turn advantages. Supports terminal_reward_only, backward step_discount propagation, and an optional reward_shaping_fn.
  • areal/workflow/openenv_utils.py — default JSONActionParser / TagActionParser / PassthroughActionParser; AutoObservationFormatter that JSON-encodes dataclasses / dicts / objects. Users can drop in custom parsers via dotted import path.
  • Non-Docker path: the workflow lazily imports UVProvider when provider="uv", so the whole thing works on Slurm / K8s clusters without a Docker daemon.
  • examples/openenv/ — shared train.py + echo_smoke.yaml (public HF Space, zero local install) + blackjack_grpo.yaml (UVProvider path).
  • docs/en/customization/openenv.md — 5-minute guide covering install, parsers, formatters, reward propagation, and wiring in a new env.
  • tests/test_openenv_workflow.py — 22 unit tests with mocked EnvClient / ArealOpenAI.

Related Issue

Addresses the "Support OpenEnv in Agentic RL" H2 2026 roadmap item in #1381.

Type of Change

  • ✨ New feature

Checklist

  • Pre-commit hooks pass (ruff check / ruff format --check on all new files)
  • Relevant tests pass; new tests added for new functionality
    • pytest tests/test_openenv_workflow.py → 22/22 pass in ~35s
  • Documentation updated (docs/en/customization/openenv.md + toc entry + example README)
  • Branch is up to date with main
  • Self-reviewed via /review-pr command
  • This PR was created by a coding agent via /create-pr
  • This PR is a breaking change

Additional Context

Verification performed:

Layer Test Result
1 pytest tests/test_openenv_workflow.py (22 tests: config validation, parsers, formatter, arun_episode reset/step loop, done early-exit, max_turns cap, terminal_reward_only, step_discount hook) ✅ 22/22 in 35s
1a ruff check + ruff format --check on all new files ✅ clean
2 End-to-end against the public echo HF Space (https://openenv-echo-env.hf.space) — real WebSocket, real observation payload ✅ episode completes, rewards recorded
3 5 episodes × random policy against the public OpenSpiel HF Space ✅ each episode 9 turns → done, rewards ∈ {−1, +1}

Not yet verified locally:

  • Full multi-step GRPO training on a real GPU with SGLang inference (skipped due to shared-machine constraints). Trainer plumbing goes through the identical RolloutWorkflow + ArealOpenAI path that MultiTurnV2Workflow uses.

Design notes:

  • The workflow deliberately depends on openenv>=0.4 (async EnvClient API landed in 0.4; 0.2.x is sync-only). Pinned as an optional extra.
  • Actions round-trip as either a dict, a dataclass instance (via action_class), or a raw string; the parser layer decides. GenericEnvClient accepts plain dicts and covers ~all built-in envs by URL alone.
  • No cluster / launcher changes. New workflow is loaded lazily via areal.workflow.__init__, so users who never touch it don't pay any import cost.

How to try it locally:

uv sync --extra cuda --extra openenv --group dev
# Smoke: zero-GPU workflow test against the real echo Space
uv run pytest tests/test_openenv_workflow.py -v
# Full training entry point
uv run python examples/openenv/train.py --config examples/openenv/echo_smoke.yaml

NancyFyong and others added 2 commits August 4, 2026 14:26
Adds a generic RolloutWorkflow that drives any OpenEnv-compatible
environment (BrowserGym, OpenSpiel, Coding, Chess, Echo, ...) via its
uniform reset/step/state protocol. New environments plug in through YAML
alone: users point at an EnvClient subclass, optionally an Action
dataclass, and pick an action_parser / obs_formatter -- no new Python.

Highlights:
* areal/api/openenv_api.py: OpenEnvConfig + ActionParser/ObservationFormatter
  Protocols, with validation for base_url/provider mutex and provider extras.
* areal/workflow/openenv.py: OpenEnvWorkflow.arun_episode drives one full
  episode, capturing per-step reward on each LLM turn via ArealOpenAI so
  GRPO grouping sees per-turn advantages. Supports terminal-reward-only,
  step-discount backward propagation, and optional reward_shaping_fn.
* areal/workflow/openenv_utils.py: default JSON / tag / passthrough action
  parsers, Auto observation formatter that JSON-encodes dataclasses/dicts.
* Non-Docker path: UVProvider (openenv >= 0.1) launches env projects via
  'uv run', so this works on GPU clusters without a Docker daemon.
* examples/openenv/: shared train.py + two configs -- echo_smoke (HF Space,
  zero local install) and blackjack_grpo (real GRPO curve via UVProvider).
* docs/en/customization/openenv.md and README with 5-minute quickstart.
* tests/test_openenv_workflow.py: mock-based unit tests covering config
  validation, parser variants, obs formatting, and the reset/step loop
  (done early-exit, max_turns cap, terminal_reward_only, discount hookup).

Closes the "Support OpenEnv in Agentic RL" H2 2026 roadmap item (areal-project#1381).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ericEnvClient

openenv 0.4 is the first release with the async EnvClient API (aenter/aexit,
awaitable reset/step). 0.2 exposes only the sync surface, so our workflow's
'async with env_client as env' unwinds unusably against it.

Also switch echo_smoke.yaml from echo_env.EchoEnv (requires installing the
env-specific package) to openenv.core.generic_client.GenericEnvClient, which
speaks the protocol by URL alone. This lets the smoke config run against
https://openenv-echo-env.hf.space with zero extra installs.

Verified end-to-end:
* Layer 1: 22/22 unit tests pass (~35s)
* Layer 2: OpenEnvWorkflow drives one episode against the public echo Space
* Layer 3: 5 episodes against the public openspiel Space produce +/-1 rewards
  and honor done termination.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@NancyFyong
NancyFyong force-pushed the feat/openenv-adapter branch from d6f8775 to 981f990 Compare August 4, 2026 07:59
NancyFyong and others added 2 commits August 4, 2026 16:25
Round-1 review surfaced 4 HIGH and 4 MED items. Fixes bundled here.

HIGH areal-project#1 -- parse-failure no longer poisons terminal_reward_only
  When a mid-trajectory action failed to parse, the workflow appended a 0.0
  sentinel to step_rewards and completion_ids, then terminal_reward_only
  read step_rewards[-1] == 0.0 and zeroed every prior real turn. Now
  parse-failure sets the completion's reward directly but stays out of
  step_rewards/completion_ids, so the terminal bookkeeping still sees only
  real env interactions. Regression test added.

HIGH areal-project#2/areal-project#3 -- uv.lock + pyproject.vllm.toml parity
  Regenerated uv.lock so `provides-extras` includes `openenv`; added the
  same `openenv = ["openenv>=0.4"]` block to pyproject.vllm.toml and
  regenerated uv.vllm.lock via the standard swap-lock-restore dance.

HIGH areal-project#4 -- blackjack_grpo.yaml example was broken
  `_instantiate_env_client` imports `env_client_class` eagerly, so the
  yaml's `openspiel_env.OpenSpielEnv` reference required the package to be
  installed in the workflow process before UVProvider even ran -- and
  `openspiel_env` is not in the openenv extra. Switched the example to
  `openenv.core.generic_client.GenericEnvClient` (URL-only, zero extra
  install); kept `provider: uv` + `project_path` so UVProvider still
  launches the env server locally.

MED areal-project#5 -- terminal_reward_only + step_discount interaction
  Back-propagating discount after zeroing intermediates smeared non-zero
  credit onto the 'discarded' turns, contradicting the docstring. Gated:
  `if terminal_reward_only ... elif step_discount < 1.0: apply_discount`.

MED areal-project#6 -- per-episode seed now overrides workflow default
  Old logic (`if seed in data and seed not in reset_kwargs`) silently
  ignored per-row seeds when the config set a workflow-level default,
  giving identical env init across the whole batch. Now `data['seed']`
  always wins.

MED areal-project#7 -- `provider: Literal["uv", "docker"] | None` for type-safe config.

MED areal-project#8 -- docs/en/customization/openenv.md aligned with echo_smoke.yaml
  Docs example switched from `echo_env.EchoEnv` (needs separate install)
  to `openenv.core.generic_client.GenericEnvClient` (zero extra install),
  matching the reference YAML. README's 'add a new env' snippet updated
  the same way.

Also drops `@runtime_checkable` from the Protocols (no isinstance() sites),
emits `parse_failed` in the workflow stats, and adds 6 regression tests
(28 total, up from 22). All ruff/format/tests clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…h import

Adds a fourth deployment path to OpenEnvWorkflow that keeps everything local
-- no HF Space upload, no Docker daemon, no HTTP server. The env class ships
alongside its config and is loaded directly from disk.

Changes:
* `_import_from_string` now accepts `/abs/path/file.py:ClassName` in addition
  to dotted module paths. Uses `importlib.util.spec_from_file_location` and
  registers into `sys.modules` before `exec_module` so `@dataclass` /
  typing lookups via `cls.__module__` work correctly. Missing files raise
  ImportError (not silent success). Two regression tests cover both paths.
* `examples/openenv/local_envs/blackjack_env.py` -- reference offline env
  (in-process BlackJack) implementing the OpenEnv async surface:
  __aenter__/__aexit__, reset(seed), step(action).
* `examples/openenv/local_blackjack_grpo.yaml` -- ready-to-run GRPO config
  pointing at a local Qwen model + the in-process env. Zero external traffic
  at runtime.
* `docs/en/customization/openenv.md` -- documents the 4 deployment modes
  (HF Space / UV / Docker / in-process) and flags the HF-Space data-leak
  concern for anyone doing production/sensitive training.

Smoke verified: 30 random-policy episodes against LocalBlackjackEnv → 7/23
win/loss, matching a random-strategy baseline for BlackJack. 30/30 unit
tests pass (up from 28); ruff check / format clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had recent activity within the last 14 days.

Please add a comment or push new commits to keep it active.

Thank you for your contribution!

@github-actions github-actions Bot added the stale label Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant