Skip to content

[RFC 005] 2/4: foundation types for agentic harnesses - #1098

Open
splusq wants to merge 4 commits into
huggingface:mainfrom
splusq:rfc-005/pr2-harness-foundation-types
Open

[RFC 005] 2/4: foundation types for agentic harnesses#1098
splusq wants to merge 4 commits into
huggingface:mainfrom
splusq:rfc-005/pr2-harness-foundation-types

Conversation

@splusq

@splusq splusq commented Aug 28, 2026

Copy link
Copy Markdown

Stack for RFC 005 — 2 of 4. Depends on #1097.

Cross-fork PRs cannot chain bases, so this targets main and its diff includes #1097. Review the top commit only: 01bb0ce.

  1. 1/4 — package split ([RFC 005] 1/4: split openenv.core.harness into a package #1097)
  2. 2/4 — this PR: foundation types
  3. 3/4 — HarnessEnvironment + subprocess + tool bridge
  4. 4/4 — production /harness route + mode wiring

What

The type layer for wrapping an external agentic harness (Claude Code, OpenClaw, Codex) as an OpenEnv environment. Types plus unit tests only — nothing runs a harness yet, that is 3/4.

module contents
config.py HarnessConfig, HarnessTransport
events.py HarnessEventType, HarnessEvent, HarnessResponse, events_to_metadata()
adapter.py AgenticHarnessAdapter ABC + error hierarchy
tools.py resolve_tool_conflicts()

Deviations from the RFC text, and why

The RFC has drifted from the code in a few places. Flagging each rather than silently following either one:

  1. ToolDefinition does not exist. The type is Tool (env_server/mcp_types.py, name/description/input_schema). Reused rather than duplicated, so inject_tools takes list[Tool]. Same for RESERVED_TOOL_NAMES, which resolve_tool_conflicts re-checks as defense in depth even though MCPEnvironment.__init__ already validates.
  2. send_message() is concrete, not abstract. Streaming is the single abstract turn primitive; send_message() drains it and pulls the response/done off the terminal event. This removes the same boilerplate from every concrete adapter, and turns "the stream ends with TURN_COMPLETE" from a convention into an enforced contract — a stream that ends without it raises rather than silently yielding an empty response.
  3. events_to_metadata() is new. Observation.metadata gets serialized over the wire, so raw pydantic events in metadata["turn_events"] would not survive. This is the one sanctioned way to put events there; 3/4 uses it and asserts the result is json.dumps-able.

Naming

The ABC is AgenticHarnessAdapter because HarnessAdapter is taken by the rollout layer (see the question at the end of #1097 — if we rename that layer, this becomes plain HarnessAdapter, matching the RFC).

Needs a decision: session_timeout_s

The RFC's field comment says "Max time for a single session/episode" but its temporal-semantics section says it bounds one turn. I implemented and documented the per-turn reading, since an episode-wide bound is not enforceable by an adapter that only sees one turn at a time. Flagging explicitly for sign-off.

Verification

82 passed

test_agentic_harness_types.py covers config defaults/validation, event JSON round-trip, events_to_metadata serializability, all six resolve_tool_conflicts branches (passthrough, env_ prefixing with schema preserved, reserved-name rejection, duplicate rejection, and both ambiguity cases where the prefixed name is also taken), and the default send_message including the missing-TURN_COMPLETE error. Back-compat and rollout suites still green. Lint clean.


Note

Low Risk
Types and re-exports only—no subprocess or harness runtime yet—and existing rollout/collect imports are preserved with explicit back-compat tests.

Overview
Adds the RFC 005 type layer for wrapping external agentic harnesses (e.g. OpenClaw, Claude Code) as OpenEnv environments, alongside the existing trainer-side rollout API now living in openenv.core.harness.rollout.

New modules expose HarnessConfig / HarnessTransport, turn HarnessEvent / HarnessResponse schemas (plus HarnessClientMessage for the future /harness WebSocket), AgenticHarnessAdapter with a concrete send_message() built on streaming TURN_COMPLETE, harness-specific errors, events_to_metadata() for wire-safe observation metadata, and resolve_tool_conflicts() to prefix env tools that clash with harness builtins. openenv.core.harness re-exports both layers; collect imports rollout symbols from .rollout; _resolve_env_reward stays on the package root for back-compat.

Unit tests cover config validation, event serialization, tool conflict branches, default send_message behavior, and rollout re-export guarantees.

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

splusq and others added 2 commits August 28, 2026 13:41
Moves the trainer-side rollout API out of the package __init__ and into
`openenv.core.harness.rollout`, leaving __init__ as a re-export shim. No
behavior change: every name previously importable from
`openenv.core.harness` still is, and is the same object.

The module was ~730 lines living directly in __init__ with a docstring
noting it sat outside the stable surface "while RFC 005 is still under
review". Splitting it now makes room for the RFC 005 turn-based agentic
harness layer to land in sibling modules instead of growing the __init__
further.

Also re-exports the private `_resolve_env_reward`, which
tests/scripts/test_browsergym_harness_eval_examples.py imports from the
package root, and points `collect.py` at `.rollout` directly rather than
importing from its own package.

Consumers left untouched and verified: `openenv collect`, pi_env,
opencode_env, browsergym_env, reasoning_gym_env, openspiel_env.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the type layer for wrapping an external agentic harness (Claude Code,
OpenClaw, Codex) as an OpenEnv environment. No runtime behavior yet — this
PR is types plus their unit tests.

- `config.py`: `HarnessConfig` / `HarnessTransport`. `session_timeout_s`
  is documented as bounding ONE conversational turn, per the RFC's
  temporal-semantics section (the field comment in the RFC is ambiguous;
  flagging for reviewer sign-off).
- `events.py`: `HarnessEventType` / `HarnessEvent` / `HarnessResponse`,
  plus `events_to_metadata()`, the sanctioned JSON-safe path for putting
  events into `Observation.metadata` so they survive wire serialization.
- `adapter.py`: `AgenticHarnessAdapter` ABC and its error hierarchy.
- `tools.py`: `resolve_tool_conflicts()` for the RFC's tool-name collision
  rules (`env_` prefixing, error on ambiguity).

Two deliberate deviations from the RFC text, both because the RFC is stale
against the code:

1. The RFC's `ToolDefinition` does not exist; the type is `Tool`
   (`env_server/mcp_types.py`), reused here rather than duplicated. Same
   for `RESERVED_TOOL_NAMES`, which `resolve_tool_conflicts` re-checks as
   defense in depth.
2. `send_message()` is concrete rather than abstract. Streaming is the
   single abstract turn primitive and `send_message()` drains it, which
   removes duplication from every concrete adapter and makes the terminal
   TURN_COMPLETE event an enforced contract instead of a convention.

The ABC is named `AgenticHarnessAdapter` to avoid colliding with the
rollout layer's existing `HarnessAdapter`. Worth discussing whether to
rename the rollout classes instead and reclaim the RFC's plain names --
see the PR description.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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