Skip to content

[feature] Import AgentEnv bundles with openenv import - #1493

Open
earakely-scale wants to merge 11 commits into
openenv:mainfrom
earakely-scale:agentenv-importer
Open

earakely-scale wants to merge 11 commits into
openenv:mainfrom
earakely-scale:agentenv-importer

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 26, 2026 •

Copy link
Copy Markdown

Summary

Adds an agentenv importer to openenv import, next to ORS/OpenReward and Verifiers. It turns an AgentEnv bundle into an ordinary OpenEnv environment package whose server/app.py serves the vendored bundle with build_app from agentenv-framework's openenv extra (0.9.1366 and later). This replaces the TRL-side integration proposed in huggingface/trl#7759.

openenv import path/to/bundle --name my_env --output-dir ./envs

Detection reads JSON only and imports nothing. A bundle is a folder whose tasks/ holds task files, each a JSON list of steps with an id and a type. The importer finds every bundle under the source and skips the importers' usual excluded directories. A plugin repository can hold several bundles; pick one with --env-class <folder name>.

Generation uses the shared package template:

  • Only the picked bundle folder is vendored, not the rest of the source, and it keeps its own folder name, because a bundle takes its name from its folder and that name is the served env's name. To allow this, prepare_import_package takes an optional vendor_dir.
  • server/app.py comes from agentenv_app.py.tpl, and the README from agentenv_README.md.tpl. The README links the delegated adapter's source (agent_env/openenv/environment.py and app.py) at the minimum supported agentenv-framework release. The template's example environment module and echo models.py are dropped, since build_app serves the env.
  • server/app.py passes MAX_CONCURRENT_ENVS to build_app, as OpenEnv's own env apps do (default 8).
  • client.py is an MCPToolClient subclass, and __init__.py exports it with CallToolAction and ListToolsAction, as envs/echo_env does. It comes from agentenv_client.py.tpl and agentenv_init.py.tpl.
  • The dependencies are the ones the bundle declares in its own requirements.txt or pyproject.toml, with agentenv-framework[openenv]>=0.9.1366 added. If the bundle already lists agentenv-framework, the extra and the floor are merged into that entry instead. A direct URL reference to it is refused, since its version can't be checked without fetching it. requires-python is >=3.11.4 (agentenv-framework's floor) so uv lock resolves. To allow this, append_dependency_files takes an optional requires_python.

Both new parameters are optional, so the ORS and Verifiers importers are unchanged.

What the served env does:

  • Each session plays one task. reset(split=..., index=...) deploys the task's environment.
  • The tools are the environment's MCP tools plus end_turn, and end_turn returns the task's score as the reward.
  • A step before the session's first reset raises, as RFC 015's wrapper contract requires. An episode that can't go on ends in an error observation.
  • The split all holds every task, and each of the bundle's evals is its own split.

In-tree or a hook: this follows the ORS and Verifiers precedent and lives in-tree. If you'd rather keep third-party formats out of tree, an entry-point group for importers would let agentenv-framework ship this importer itself. Happy to switch.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • New environment
  • Refactoring

Alignment Checklist

  • I have read .claude/docs/PRINCIPLES.md and this PR aligns with our principles
  • I have checked .claude/docs/INVARIANTS.md and no invariants are violated
  • I have run /pre-submit-pr (or bash .claude/hooks/lint.sh and tests) and addressed all issues

Two things for reviewers:

  • The server needs local Docker. It deploys each episode's environment itself, the way agent-env's sandbox configuration says (local Docker by default), and builds any environment image the bundle defines with a Dockerfile when it starts. The environments it deploys stay isolated, one per episode. But the generated server/Dockerfile has no Docker inside, so openenv build / openenv push don't give a working Space for a bundle yet.
  • Bundles must be self-contained. Everything a bundle's tasks use has to be in the bundle folder or built into agent-env. A bundle whose environments or verifiers a plugin registers by other means won't serve from the generated package alone.

Docs: the CLI reference says what the importer detects and vendors, and links to AgentEnv's OpenEnv page for what the served env does and needs. Details that change with agentenv-framework are documented there rather than here.

RFC Status

  • Not required (bug fix, docs, minor refactoring)
  • RFC exists: #___
  • RFC needed (will create before merge)

Test Plan

Unit tests and lint

  • PYTHONPATH=src:envs uv run pytest tests/test_cli: 210 passed. The importer's command tests fail if the importer isn't registered, and fail on the first revision, which vendored the whole source and let a bundle's own agentenv-framework entry drop the floor (the Bugbot finding).
  • Full suite on macOS (Python 3.14, without tests/envs/test_coding_codeact_env.py, which needs smolagents): 3701 passed, 187 skipped, 12 failed. The 12 also fail on unmodified main on the same machine: the agentic_harness_process timing tests, the live HF Space tests in test_auto_env.py, test_harbor_capture_server's port test and two websocket client tests.
  • usort check, ruff format --check and ruff check pass on the changed files. On this base, lint.sh also reports tests/envs/test_julia_env.py, tests/envs/test_grid_world.py and src/openenv/core/harness/README.md, which were already on main.
  • The built wheel contains importers/agentenv.py and its templates.

End to end (macOS, local Docker, from PyPI: agentenv-framework 0.9.1360 and openenv 0.10.0 at first, then 0.9.1364, where OpenEnv became an optional extra)

  1. The bundle: echo-desk, the echo-mcp example's MCP server (envs/echo/) plus one task that asks for the echo tool's output and scores whether the answer contains it (agent_prompt_response_verifier).
  2. Import: openenv import echo-desk --name echo_desk --output-dir out generated the package, and uv lock resolved.
  3. Install and serve: pip install ./out/echo_desk into a fresh venv installed the vendored bundle as package data. Then server --port 8765, the package's own entry point.
  4. Play: with GenericEnvClient:
    • /list_environments returned ["echo-desk"], and the splits were all.
    • Two concurrent sessions offered the tools echo and end_turn.
    • Answering with the echoed ping scored reward 1.0 with done; answering otherwise scored 0.0. Both took 29 s together, deploys included.
    • No episode containers were left afterwards, and SIGTERM stopped the server cleanly.
  5. Validate: openenv validate out/echo_desk passes static.manifest.
  6. On 0.9.1364: the same bundle, imported again, locks to agentenv-framework[openenv] 0.9.1364 and openenv 0.10.0, installs into a fresh venv and serves echo-desk. It scored 1.0 and 0.0.
  7. One bundle out of a repository: echo-desk and a second bundle under bundles/, a 50 MB file under results/, a local .agentenv/config.toml and a root requirements.txt listing torch. openenv import repo --env-class echo-desk vendored 20 KB, the echo-desk folder alone, without torch. Installed into a fresh venv and served, it scored 1.0 and 0.0 as above.
  8. On 0.9.1366, everything from PyPI: the same bundle, imported again, locks to agentenv-framework[openenv] 0.9.1366 (protocol 0.1.381) and openenv 0.10.0. It's served with MAX_CONCURRENT_ENVS=1.
    • The generated EchoDeskEnv raised on a tool call before reset. It then reset to split="all", index=0, listed echo and end_turn, got ping back from echo, and got done=True and reward 1.0 from end_turn.
    • A second session at the same time was refused with CAPACITY_REACHED.
    • Over GenericEnvClient, a step before reset came back as an EXECUTION_ERROR; the same session then reset and played, scoring 0.0 for a wrong answer.
    • No episode containers were left, and SIGTERM stopped the server cleanly.

Claude Code Review

Written with Claude Code. /alignment-review wasn't run; the note for reviewers above is the one alignment point I found against PRINCIPLES.md and INVARIANTS.md.

🤖 Generated with Claude Code


Note

Medium Risk
New import path and dependency/version enforcement are well tested, but served bundles depend on host Docker and delegated agentenv-framework runtime, which limits deployment assumptions for users of build/push.

Overview
Adds AgentEnv bundle support to openenv import, alongside ORS/OpenReward and Verifiers.

Detection finds folders whose tasks/ contains valid step JSON (no source import). Multi-bundle repos require --env-class with the bundle folder name.

Generation vendors only the selected bundle, drops the usual models.py / *_environment.py scaffold, and wires server/app.py to agentenv-framework's build_app (with agentenv-framework[openenv]>=0.9.1366, merged into existing framework deps; direct URL refs are rejected). The client is a generated MCPToolClient with fresh tool discovery by default. Shared importer helpers gain optional vendor_dir, requires_python, and iter_directories.

Docs (README, first-environment guide, CLI reference) describe AgentEnv import, local Docker requirements, and that openenv build / push do not yield a working deployment without a Docker-capable host. Tests cover detection, CLI generation, dependency merging, selective vendoring, and HTTP client tool discovery.

Reviewed by Cursor Bugbot for commit 598514f. Bugbot is set up for automated code reviews on this repo. Configure here.

Add an agentenv importer beside ORS/OpenReward and Verifiers. It detects
AgentEnv bundles (folders whose tasks/ holds task files) by reading JSON
only, vendors the bundle under its own folder name, and generates a
server/app.py that serves it with agentenv-framework's build_app.
prepare_import_package takes an optional vendor_dir and
append_dependency_files an optional requires_python, so the generated
package keeps the bundle's name and locks against agentenv-framework.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread src/openenv/cli/importers/agentenv.py Outdated
earakely-scale and others added 3 commits October 26, 2026 17:17
The agentenv importer copied the whole source tree even when --env-class
picked one bundle out of a repository, carrying unrelated files and local
config into the package; it now vendors the bundle folder alone and takes
dependencies from the bundle's own requirements. An agentenv-framework entry
the bundle already lists now gets the >=0.9.1360 floor merged into it rather
than replacing it. The CLI reference says the server needs local Docker to
build a bundle's environment images, and that the generated image can't
serve a bundle yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the CLI reference to what the importer detects and vendors, and
link to AgentEnv's OpenEnv page for what the served env does and needs,
so details that change with agentenv-framework are documented there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agentenv-framework 0.9.1364 makes OpenEnv an optional extra, so the
generated package asks for agentenv-framework[openenv]>=0.9.1364, and an
agentenv-framework entry the bundle already lists gets the extra added
along with the floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread src/openenv/cli/importers/agentenv.py Outdated

@burtenshaw burtenshaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the careful PR and test plan. I checked out the branch: tests/test_cli passes (163 passed), and ruff check, ruff format and usort pass on every changed file. The remaining lint hits (src/openenv/core/harness/README.md, tests/envs/test_julia_env.py, tests/envs/test_grid_world.py) are already on main. I found no Tier 1 correctness bugs, so I pushed nothing.

Bugbot's latest "unpublished version" finding is stale. agentenv-framework 0.9.1364 is on PyPI with the openenv extra and requires-python >=3.11.4.

Design questions for maintainers (blocking until someone decides)

  1. Delegating importer vs RFC 015 (#794). RFC 015 Decision 1 says openenv import generates an inspectable wrapper that subclasses Environment in the generated package. This importer generates a 26-line shim (templates/agentenv_app.py.tpl), and the wrapper (agent_env.openenv.AgentEnvEnvironment, about 480 lines) ships in agentenv-framework. I read it. It uses create_app, CallToolAction/CallToolObservation and the TaskProvider methods, so the boundaries hold today. But reviewers cannot audit the served env from the generated package, and the adapter is versioned against openenv.core internals outside this repo. I don't think this needs its own RFC: no core/ changes, and RFC 015 Goal 6 expects more importers in the registry. RFC 015 should say whether a "delegating" importer is acceptable, though, and add an AgentEnv mapping section if so.

  2. In-tree vs entry point. The entry-point option in the PR description would be a new plugin mechanism. Per CLAUDE.md, that needs an RFC, so it is probably out of scope here. In-tree follows the ORS and Verifiers precedent.

  3. The generated package can't be deployed with OpenEnv tooling. The server needs Docker on the host, so openenv build / openenv push produce an image that builds but cannot serve. RFC 015 Goal 3 says imported packages deploy like hand-written ones. At minimum, generate() (src/openenv/cli/importers/agentenv.py:97-117) or import_env should print a warning, and the generated README should state the requirement. Today the README is still the echo template.

Non-blocking

  • agentenv.py:101 deletes server/<env>_environment.py, but the generated README still documents it (python3 server/<env>_environment.py, file tree). The template client.py/models.py still describe echo actions, while the served env takes MCP actions.
  • agentenv.py:49: detection now runs on every openenv import. An ORS or Verifiers repo with a tasks/*.json list of {"type": ...} objects would now hit "Multiple source formats" and need --type. That is probably rare, but it is a behavior change for existing users.
  • docs/source/guides/first-environment.md:36: the tip says the importer "vendors the source" and "emits an OpenEnv wrapper". For AgentEnv, only the bundle folder is vendored, and the wrapper comes from agentenv-framework.
  • docs/source/reference/cli.md:34: "deploys each episode on the local Docker" reads better as "in a local Docker container".

Verdict: needs maintainer review on items 1 to 3. The code itself is small and clean.

burtenshaw and others added 3 commits October 27, 2026 11:30
Resolve the README overlap from openenv#668 by retaining host-local serve guidance and the AgentEnv importer entry. Preserve the generated AgentEnv documentation, runtime warning, dependency floor, and client limitations from openenv#1493. Existing RFC015 delegation, deployment, and client integration holds remain open.
The generated README now links agent_env.openenv's environment.py and
app.py at the minimum supported agentenv-framework release, so the served
environment can be read from the generated package. Detection also
requires each step to carry a string id, as AgentEnv task steps do, which
makes it less likely that another project's tasks/*.json is mistaken for
a bundle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Author

@burtenshaw thanks for the thorough review, and for the README, warning and docs commits.

1. Delegating importer vs RFC 015. Below is proposed text for RFC 015, written to drop into it as it stands on #794: a short "Delegating Importers" subsection that says when an importer may serve the library's own OpenEnv adapter instead of generating a wrapper, and an "AgentEnv Mapping" section beside the ORS/OpenReward and Verifiers ones. Happy to open a PR against your RFC branch instead, if that's easier.

Proposed: Delegating Importers (after "Generated Wrapper Contract")

Delegating Importers

An importer may delegate instead of generating a wrapper when the source library already ships an OpenEnv adapter. The generated package then holds the vendored source, a server entry point that hands it to the library's adapter, and a README. The wrapper code lives in the library.

A delegating importer is accepted when all of these hold:

  1. The adapter is an Environment served through create_app, and it keeps the Generated Wrapper Contract and the Boundary Rules. Agents act through MCP-style actions only, a tool call before reset() never starts an episode, and rewards are computed in the server.
  2. The adapter uses only public OpenEnv runtime APIs: Environment, create_app, the RFC 003 MCP action and observation models, and TaskProvider.
  3. The library tests the adapter in its own CI against the OpenEnv version it locks, and the generated package pins a minimum library version that has the adapter.
  4. The generated package documents the delegation. Its README names the adapter and links its source at that minimum version, and states any runtime it needs beyond the package, such as a container runtime.
  5. Detection stays static, as in Decision 2. The importer reads files and never imports the library or the source.

The trade-off against Decision 1: the wrapper is read at the linked source, not in the generated package. In return, the mapping isn't copied into a template that drifts from the library, and a fix to the adapter reaches imported packages through a version bump rather than a re-import.

Proposed: AgentEnv Mapping (after "Verifiers Mapping")

AgentEnv Mapping

An AgentEnv importer adapts one detected bundle. A bundle is a folder whose tasks/ holds task files, each a JSON list of steps with an id and a type. Detection reads those files and imports nothing. When a source holds several bundles, --env-class picks one by folder name, and only that folder is vendored.

The importer delegates (see Delegating Importers). Its generated server/app.py passes the vendored bundle to agent_env.openenv.build_app from agentenv-framework[openenv]>=0.9.1364. The adapter is AgentEnvEnvironment (source at v0.9.1364; build_app's source). Dependencies are the bundle's own requirements.txt or pyproject.toml, with that floor merged in.

Each task is one episode. build_app prepares the bundle when the server starts. A task it can't serve as an episode, such as one with no agent for the client to play, is left out and logged.

Task discovery maps the bundle's tasks into the task API:

list_splits() -> ["all", <one per eval in the bundle>]
list_tasks(split) -> [{"id", "name", "index", "prompt", "system_prompt", "agent"}, ...]
num_tasks(split), get_task(split, index), get_task_range(split, start, stop)

reset() accepts split and index, or neither for the first task served. It does not take task_spec, because a task is a bundle entry chosen by position. Reset deploys the task's environment and runs the task's steps up to the client's first turn. It returns an Observation whose metadata holds the turn's prompt and system_prompt, and the task's id, name, split and index.

Agent actions map to the open turn:

  • ListToolsAction returns the environment's MCP tools for the turn, plus end_turn. For a task whose agent works in a sandbox, the tools include bash, which runs where that agent works.
  • CallToolAction calls the tool and returns its content in a CallToolObservation. A failed call returns a ToolError (not found, timeout, transport or execution), and the episode goes on.
  • end_turn(response) ends the turn with that reply. The observation carries the next turn's prompt with done=False. After the last turn it carries done=True, the reward, and the task's scores in metadata["scores"].

Rewards come from the task's own verifier steps, which run in the server after the last turn. A task that records one score has that score as its reward. A bundle whose tasks record several needs reward= passed to build_app, by editing server/app.py.

An episode that can't go on ends in an observation with done=True, reward=None and metadata["status"] = "error", never in an exception. That covers a failed deploy, a crashed environment, and a step before reset(); none of these starts an episode.

SUPPORTS_CONCURRENT_SESSIONS is True, because each session plays its own episode on its own deployment and sessions share no state. max_concurrent_envs, 8 by default, caps them. close() abandons the session's episode and tears its deployment down, and server shutdown does the same for every open episode.

Episodes deploy where agentenv-framework's sandbox configuration says, local Docker by default. The generated server/Dockerfile has no Docker inside, so the package runs on a host with Docker, and openenv push doesn't yet give a working Space.

Proposed: smaller edits elsewhere in RFC 015

One line or paragraph each, so the new sections don't contradict the rest of the RFC.

Where Proposed text
Summary, 2nd paragraph "The initial implementation targets ORS/OpenReward and Prime Intellect Verifiers; AgentEnv bundles follow in #1493, through a delegating importer."
Non-Goals, new bullet "Providing a container runtime to an imported package whose adapter needs one. Such an importer states the requirement in the generated README and at import time."
Static Detection, new paragraph "The AgentEnv importer reads tasks/*.json files and detects folders whose task files are JSON lists of steps, each with an id and a type. It imports nothing."
Key Design Decisions, Decision 1, new last paragraph "Exception: an importer may delegate to the source library's own OpenEnv adapter under the conditions in Delegating Importers. The package is still written to disk and reviewed before publication, but its wrapper is read at the adapter's pinned source."
Boundary Rules, rule 3 "Rewards are computed inside the wrapper, or for a delegating importer inside the library's adapter, using the source library's native reward, rubric, harness, or scoring methods."
Examples, new subsection after Verifiers "Import an AgentEnv Bundle", below.
Implementation Notes, new paragraph "#1493 adds src/openenv/cli/importers/agentenv.py, the agentenv_app.py.tpl and agentenv_README.md.tpl templates, optional vendor_dir and requires_python parameters in importers/base.py, tests in tests/test_cli/test_import_env.py, and the CLI reference."

The new example, in the style of the existing ones:

Import an AgentEnv Bundle

openenv import ./my-bundle --name my_bundle --output-dir ./envs
cd envs/my_bundle && uv sync && uv run server --host 127.0.0.1 --port 8000

The served environment's tools are the bundle environment's MCP tools plus end_turn:

from openenv.core.generic_client import GenericEnvClient

async with GenericEnvClient(base_url="http://127.0.0.1:8000") as env:
    obs = await env.reset(split="all", index=0)
    tools = await env.step({"type": "list_tools"})
    done = await env.step({"type": "call_tool", "tool_name": "end_turn", "arguments": {"response": "Done."}})
    print(done.reward)

2. In-tree vs entry point. Agreed: in-tree, like ORS and Verifiers. We don't need the entry point.

3. Deployment. Thanks for the generated README and the import-time warning. For this PR we'd keep the limitation documented; the text above proposes it as an explicit Non-Goal. A working Space needs a remote sandbox and published environment images, which is on our side and separate from this PR.

Non-blocking. c834349 (on top of your commits) covers two of them:

  • The generated README now links the adapter's source, environment.py and app.py, at v0.9.1364, so the served environment can be read from the generated package (condition 4 above).
  • Detection requires a string id on every step as well as a type, as AgentEnv task steps always have. A tasks/*.json of {"type": ...} objects in an ORS or Verifiers repo no longer matches.

client.py and models.py stay template scaffolding for now, as your README says. tests/test_cli passes (194).

@burtenshaw

Copy link
Copy Markdown
Collaborator

Independent review at c834349 confirms 194 CLI tests pass, the linked v0.9.1364 source tag exists, the GenericEnvClient example is valid, and upstream adapter CI provides useful isolation/shutdown evidence. The proposal makes the delegation exception explicit. Targeted corrections remain: (1) named direct-URL requirements currently bypass the claimed minimum framework version; a narrow importer repair is being prepared; (2) RFC015 currently requires pre-reset calls to raise, while this adapter returns a terminal error observation without deployment, so reconcile that contract explicitly; (3) the API list also needs Observation, State and RESERVED_TOOL_NAMES; (4) the universal no-exception error guarantee is broader than tested implementation: turn.end and cleanup have paths outside the exception handler, so narrow that text or cover those paths. Delegation, the Docker-runtime non-goal and incompatible generated scaffold client/models still need explicit design acceptance. Documentation of those limitations makes the tradeoff reviewable but does not establish acceptance.

…penenv#1493)

The framework floor was skipped for named direct references. Validate collected dependencies before scaffolding and reject sources whose adapter version cannot be established statically. Preserve normal range constraints, extras and markers.
…NVS, and require agentenv-framework 0.9.1366

- client.py is now an MCPToolClient subclass, and __init__.py exports it with
  CallToolAction and ListToolsAction, as envs/echo_env does. The echo
  scaffolding's models.py is dropped. The README shows the client.
- server/app.py passes MAX_CONCURRENT_ENVS to build_app, as OpenEnv's own env
  apps do (default 8).
- The floor is agentenv-framework 0.9.1366, whose adapter raises on a step
  before the session's first reset, as RFC 015's wrapper contract requires,
  and ends an episode whose end_turn the proxy refuses in an error observation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a72456a. Configure here.

Comment thread src/openenv/cli/importers/agentenv.py
…nv#1493)

The generated subclass inherited MCPToolClient's persistent discovery cache, so reset and turn changes kept old tools. Default to fresh discovery while preserving explicit use_cache arguments. Cover reset, end_turn via HTTP and WebSocket, and dynamic tool changes with generated-client transport regressions.
@earakely-scale

Copy link
Copy Markdown
Author

@burtenshaw thanks, all four corrections were right. This is fixed in agentenv-framework 0.9.1366 (scaleapi/agentenv-framework#350), and a72456a on this branch raises the floor to it.

1. Direct references. Thanks for bc98140. A direct reference can't show the adapter's version without fetching it, so refusing it at import is the right call.

2. A step before reset(). The adapter now raises, as the Generated Wrapper Contract requires.

  • A ListToolsAction or CallToolAction before the session's first reset raises RuntimeError("no episode has started; call reset before stepping"), and nothing is deployed.
  • Over the WebSocket the client gets an EXECUTION_ERROR, and the same session can then reset and play.
  • Once a reset has happened, an ended episode still answers a step with done=True and the error metadata.

3. The API list. Corrected. Condition 2 of the proposed Delegating Importers text now reads:

  1. The adapter uses only public OpenEnv runtime APIs: Environment, Observation, State, create_app, the RFC 003 MCP models (ListToolsAction, CallToolAction, their observations, Tool, ToolError, ToolErrorType and RESERVED_TOOL_NAMES), and the task-discovery methods of TaskProvider.

4. The no-exception guarantee. You were right about turn.end: when the agent's proxy no longer held the turn, its EpisodeError escaped end_turn. It's now inside the handler, with a test.

Cleanup was already safe, and a new test now covers it:

  • closing a turn's tools does nothing that can fail;
  • abandoning an episode waits for its run without re-raising the run's error;
  • the test makes the teardown fail and checks that the step still answers with an observation and the session is freed.

The narrowed paragraph for the AgentEnv Mapping:

An episode that can't go on ends in an observation with done=True, reward=None and metadata["status"] = "error", not in an exception. That covers a failed deploy, a crashed environment, a turn the agent's proxy no longer holds, a verifier that fails, and a teardown that fails. Two cases raise instead and start nothing: a step before the session's first reset() raises RuntimeError, as the Generated Wrapper Contract requires, and an action other than ListToolsAction or CallToolAction raises TypeError.

The generated client. It now matches the served API, so it should drop off the list of things needing acceptance. It follows envs/echo_env:

  • client.py defines <Name>Env(MCPToolClient);
  • __init__.py exports it with CallToolAction and ListToolsAction;
  • models.py is gone.

The generated README shows the client in use. End to end from a generated package, everything from PyPI (0.9.1366, openenv 0.10.0), the client:

  • raised on a tool call before reset;
  • reset to split="all", index=0;
  • listed echo and end_turn;
  • got ping back from echo;
  • got done=True and reward 1.0 from end_turn.

Also in a72456a: the generated server/app.py reads MAX_CONCURRENT_ENVS the way OpenEnv's own env apps do (int(os.getenv("MAX_CONCURRENT_ENVS", "8"))). With it set to 1, a second session was refused with CAPACITY_REACHED. The Mapping's concurrency sentence becomes: "max_concurrent_envs caps them; the generated server/app.py reads it from MAX_CONCURRENT_ENVS, 8 by default."

tests/test_cli passes (210 tests). That leaves the two design points for maintainers: delegation itself, and the Docker-runtime Non-Goal.

@burtenshaw

Copy link
Copy Markdown
Collaborator

Correction after reconciling the exact v0.9.1366 lifecycle: I withdraw the teardown exception-escape blocker. Our earlier probe replaced Episode.abandon itself with a raising coroutine, which bypassed its real asyncio.wait behavior. The upstream teardown_run injection is the relevant boundary. Independent offline probes with unchanged release lifecycle functions and real OpenEnv WebSocket transport confirm a raised teardown_run exception becomes done=True/reward=None/status=error, and the session can reset afterward. One wording qualification remains: say teardown exceptions that fail the episode run, rather than every teardown failure. Provider termination failures caught into the best-effort teardown report can be logged while normal scores are returned; freeing session tracking does not prove every sandbox stopped. This is a documentation qualification, not the withdrawn exception-escape blocker. Delegation and the Docker-runtime Non-Goal remain the two maintainer design decisions.

@earakely-scale

Copy link
Copy Markdown
Author

@burtenshaw thanks for 598514f. That's a real bug I missed: MCPToolClient's discovery cache kept the previous task's or turn's tools after a reset or end_turn, and fresh discovery by default is the right fix.

Agreed on the teardown wording. Teardown is best-effort: a sandbox it can't stop is logged and the episode keeps its score. Only a teardown that raises ends the episode in an error observation. The corrected paragraph for the AgentEnv Mapping:

An episode that can't go on ends in an observation with done=True, reward=None and metadata["status"] = "error", not in an exception. That covers a failed deploy, a crashed environment, a turn the agent's proxy no longer holds, a verifier that fails, and a teardown that raises. Teardown is otherwise best-effort: a sandbox it can't stop is logged as not terminated, and the episode keeps its score. Two cases raise instead and start nothing: a step before the session's first reset() raises RuntimeError, as the Generated Wrapper Contract requires, and an action other than ListToolsAction or CallToolAction raises TypeError.

This branch has not been deployed

No deployments
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.

2 participants