Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ def step(self, action: ActT) -> StepResult[ObsT]:
StepResult[ObsT]: The resulting observation, reward, done flag, and info.
"""
raise NotImplementedError

def close(self) -> None:
"""Release resources (containers, sessions, etc.)."""
pass
82 changes: 82 additions & 0 deletions src/core/base_env_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
core/runner_env.py
Minimal HTTP-based environment client.
- Talks to a single env worker exposing: POST /reset, POST /step

Future hooks (commented below) for:
- episode_id, seed on reset
- request_id on step
- custom headers (auth/trace)
"""

from __future__ import annotations

from abc import abstractmethod
from typing import Any, Dict, Generic, Optional, TypeVar

import requests

from .base import BaseEnv
from .types import StepResult

ActT = TypeVar("ActT")
ObsT = TypeVar("ObsT")


class HTTPEnvClient(BaseEnv[ActT, ObsT], Generic[ActT, ObsT]):
def __init__(
self,
base_url: str,
request_timeout_s: float = 15.0,
default_headers: Optional[Dict[str, str]] = None,
):
self._base = base_url.rstrip("/")
self._timeout = float(request_timeout_s)
self._http = requests.Session()
self._headers = default_headers or {}

@abstractmethod
def _step_payload(self, action: ActT) -> dict:
"""Convert an Action object to the JSON body expected by the env server."""
raise NotImplementedError

@abstractmethod
def _parse_result(self, payload: dict) -> StepResult[ObsT]:
"""Convert a JSON response from the env server to StepResult[ObsT]."""
raise NotImplementedError

# ---------- BaseEnv ----------
def reset(self) -> ObsT:
body: Dict[str, Any] = {}
# TODO: later:
# body["seed"] = seed
# body["episode_id"] = episode_id
r = self._http.post(
f"{self._base}/reset",
json=body,
headers=self._headers,
timeout=self._timeout,
)
r.raise_for_status()
return self._parse_result(r.json()).observation

def step(self, action: ActT) -> StepResult[ObsT]:
body: Dict[str, Any] = {
"action": self._step_payload(action),
"timeout_s": int(self._timeout),
}
# TODO: later:
# body["request_id"] = str(uuid.uuid4())
# body["episode_id"] = current_episode_id
r = self._http.post(
f"{self._base}/step",
json=body,
headers=self._headers,
timeout=self._timeout,
)
r.raise_for_status()
return self._parse_result(r.json())

def close(self) -> None:
# nothing to close; higher-level libraries own lifecycles of the endpoints
pass
78 changes: 28 additions & 50 deletions src/envs/coding_env/env.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,50 @@
"""
envs/coding_env/env.py
--------------------------------
Concrete environment implementation using the core BaseEnv.
POC implementation runs code locally via subprocess that can be changed later.
CodingEnv
---------
Client-side wrapper for the Coding environment server.
Talks HTTP to a single base_url exposing: /reset and /step.

- users instantiate CodingEnv with a base_url provided by the higher-level
vector/orchestration layer.
- Environment authors ship the Docker image that serves the HTTP API.

(Seeds, episode IDs, request IDs, capabilities can be added later in the payloads.)
"""

from __future__ import annotations

import subprocess
from typing import Optional

from core.base import BaseEnv
from core.base_env_client import HTTPEnvClient
from core.types import StepResult

from .models import CodeAction, CodeObservation


class CodingEnv(BaseEnv[CodeAction, CodeObservation]):
"""
Minimal Coding Environment.

POC behavior:
- reset(): returns a fresh, empty observation (no persistent state).
- step(action): runs Python code with `python -c` and returns stdout/stderr/exit_code.

Future swap:
Replace _run_code_locally() with a call to your Docker/gateway backend without
changing the public API.
"""

class CodingEnv(HTTPEnvClient[CodeAction, CodeObservation]):
def __init__(
self,
default_timeout_s: float = 10.0,
python_executable: str = "python",
base_url: str,
request_timeout_s: float = 15.0,
):
"""
Args:
default_timeout_s: Max seconds to allow code execution before timing out.
python_executable: Interpreter to run (e.g., "python3", a venv path, etc.).
"""
self._default_timeout_s = float(default_timeout_s)
self._python = python_executable

# --- BaseEnv interface ---

def reset(self) -> CodeObservation:
# No state to clear in this POC; return an initial observation.
return CodeObservation(stdout="", stderr="", exit_code=0)

def step(self, action: CodeAction) -> StepResult[CodeObservation]:
if not isinstance(action, CodeAction):
raise TypeError(f"Expected CodeAction, got {type(action)!r}")

# TODO: replace dummy response with the call to the code executor inside the container
obs, timed_out = CodeObservation(stderr="", stdout="", exit_code=0), False

# Simple reward heuristic: success and no stderr -> 1.0 else 0.0
reward: Optional[float] = (
1.0 if (obs.exit_code == 0 and not obs.stderr) else 0.0
super().__init__(
base_url=base_url,
request_timeout_s=request_timeout_s,
)

info = {
"timed_out": timed_out,
"interpreter": self._python,
# --- HTTPEnvClient abstract hooks ---

def _step_payload(self, action: CodeAction) -> dict:
# Shape expected by the server's /step endpoint under "action"
return {
"code": action.code,
}

def _parse_result(self, payload: dict) -> StepResult[CodeObservation]:
# Expecting: { "observation": {...}, "reward": <float|null>, "done": <bool>, "info": {...} }
obs = CodeObservation(**payload["observation"])
return StepResult(
observation=obs,
reward=reward,
done=False, # Coding env is not episodic by default
reward=payload.get("reward"),
done=bool(payload.get("done", False)),
)
7 changes: 7 additions & 0 deletions src/envs/coding_env/example_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from envs.coding_env.env import CodingEnv
from envs.coding_env.models import CodeAction

env = CodingEnv(base_url="http://localhost:8080")
obs0 = env.reset()
result = env.step(CodeAction(code="print('hi')"))
print(result.observation.stdout.strip(), result.reward)