Download src/core/harness/adapter.py from openenv/snake_env: direct link, hf CLI and curl.
- Browser
- Download file 5.49 kB
-
https://huggingface.co/spaces/openenv/snake_env/resolve/main/src/core/harness/adapter.py
- Command line
-
hf download hf://spaces/openenv/snake_env/src/core/harness/adapter.py
-
curl -L -o adapter.py https://huggingface.co/spaces/openenv/snake_env/resolve/main/src/core/harness/adapter.py
5.49 kB
| # SPDX-License-Identifier: BSD-3-Clause | |
| """Turn-based adapter interface for external agentic harnesses (RFC 005).""" | |
| from __future__ import annotations | |
| from abc import ABC, abstractmethod | |
| from typing import AsyncIterator, Optional | |
| from ..env_server.mcp_types import Tool | |
| from .config import HarnessConfig | |
| from .events import HarnessEvent, HarnessEventType, HarnessResponse | |
| class HarnessError(Exception): | |
| """Base error for agentic harness failures.""" | |
| class HarnessStartupError(HarnessError): | |
| """The harness process failed to start or become ready in time.""" | |
| class HarnessNotRunningError(HarnessError): | |
| """An operation required a running harness but none was alive.""" | |
| class HarnessTurnTimeoutError(HarnessError): | |
| """A single conversational turn exceeded its wall-clock budget.""" | |
| class AgenticHarnessAdapter(ABC): | |
| """ | |
| Abstract adapter for a turn-based external agentic harness. | |
| Subclass this to integrate a harness such as OpenClaw or Claude Code. The | |
| adapter owns the harness process lifecycle and communication; the harness | |
| itself is a long-lived process that maintains conversation context across | |
| turns. Each `send_message()` call is one conversational turn: the harness | |
| runs its internal ReAct loop and returns when it has a response. | |
| Distinct from the trainer-side rollout | |
| [`~openenv.core.harness.rollout.HarnessAdapter`], which drives an entire | |
| episode in a single call. | |
| Attributes: | |
| BUILTIN_TOOL_NAMES (`frozenset[str]`): | |
| Names of the harness's built-in tools, used to detect conflicts | |
| with injected environment tools. Concrete adapters override this. | |
| """ | |
| BUILTIN_TOOL_NAMES: frozenset[str] = frozenset() | |
| def __init__(self, config: HarnessConfig): | |
| self.config = config | |
| async def start(self, working_directory: str) -> None: | |
| """ | |
| Start the harness process. | |
| Args: | |
| working_directory (`str`): | |
| Path where the harness should operate. | |
| Raises: | |
| [`~openenv.core.harness.adapter.HarnessStartupError`]: | |
| If the process fails to start or become ready in time. | |
| """ | |
| ... | |
| async def stop(self) -> None: | |
| """ | |
| Stop the harness process and clean up resources. | |
| Implementations must be idempotent: calling `stop()` on an already | |
| stopped (or never started) adapter must succeed silently. | |
| """ | |
| ... | |
| async def inject_tools( | |
| self, tools: list[Tool], bridge_url: Optional[str] = None | |
| ) -> None: | |
| """ | |
| Inject environment MCP tool definitions into the harness configuration. | |
| Called before `start()` so the harness discovers the tools at startup. | |
| The mechanism is harness-specific (config file, CLI flags, environment | |
| variables). | |
| Args: | |
| tools (`list[Tool]`): | |
| Conflict-resolved environment tool definitions to inject. | |
| bridge_url (`str`, *optional*): | |
| URL of the MCP bridge serving these tools, when the | |
| environment exposes one. | |
| """ | |
| ... | |
| def send_message_streaming(self, message: str) -> AsyncIterator[HarnessEvent]: | |
| """ | |
| Send a message and stream intermediate events for one turn. | |
| Yields events as the harness processes the turn (tool calls, LLM | |
| chunks, text output). The final event must be a | |
| [`~openenv.core.harness.events.HarnessEvent`] of type `TURN_COMPLETE` | |
| whose data carries the turn's `response` and optionally `done`. | |
| Args: | |
| message (`str`): | |
| The user message for this conversational turn. | |
| Yields: | |
| [`~openenv.core.harness.events.HarnessEvent`] instances. | |
| """ | |
| ... | |
| async def is_alive(self) -> bool: | |
| """Check whether the harness process is still running.""" | |
| ... | |
| async def send_message(self, message: str) -> HarnessResponse: | |
| """ | |
| Send a message and collect the complete response for one turn. | |
| Drains `send_message_streaming()` and assembles a | |
| [`~openenv.core.harness.events.HarnessResponse`] from the terminal | |
| `TURN_COMPLETE` event. | |
| Args: | |
| message (`str`): | |
| The user message for this conversational turn. | |
| Returns: | |
| [`~openenv.core.harness.events.HarnessResponse`] with the text | |
| response, all turn events, and the harness's done signal. | |
| Raises: | |
| [`~openenv.core.harness.adapter.HarnessError`]: | |
| If the event stream ends without a `TURN_COMPLETE` event. | |
| """ | |
| events: list[HarnessEvent] = [] | |
| async for event in self.send_message_streaming(message): | |
| events.append(event) | |
| if not events or events[-1].type is not HarnessEventType.TURN_COMPLETE: | |
| raise HarnessError( | |
| "harness event stream ended without a TURN_COMPLETE event" | |
| ) | |
| terminal = events[-1] | |
| return HarnessResponse( | |
| response=str(terminal.data.get("response", "")), | |
| events=events, | |
| done=bool(terminal.data.get("done", False)), | |
| ) | |
| __all__ = [ | |
| "AgenticHarnessAdapter", | |
| "HarnessError", | |
| "HarnessNotRunningError", | |
| "HarnessStartupError", | |
| "HarnessTurnTimeoutError", | |
| ] | |