AdithyaSK HF Staff commited on
Commit
9e98d70
·
verified ·
1 Parent(s): 297cffc

Upload folder using huggingface_hub

Browse files
__init__.py CHANGED
@@ -5,13 +5,14 @@ the OpenCode CLI agent against a caller-supplied LLM endpoint, runs the
5
  caller-supplied verifier script, and returns reward + proxy trace +
6
  workdir contents as a JSON-serialized :class:`RolloutResult`.
7
 
8
- Import either the :class:`OpenCodeEnv` HTTP client (for training scripts
9
- talking to a deployed server) or the models (for type-safe parsing of
10
- rollout results).
 
11
  """
12
 
13
- from .client import OpenCodeEnv
14
- from .models import OpenCodeState, RolloutResult, RolloutTurn
15
 
16
  __all__ = [
17
  "OpenCodeEnv",
@@ -19,3 +20,15 @@ __all__ = [
19
  "RolloutResult",
20
  "RolloutTurn",
21
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  caller-supplied verifier script, and returns reward + proxy trace +
6
  workdir contents as a JSON-serialized :class:`RolloutResult`.
7
 
8
+ The directory name (``openenv``) matches the installed ``openenv-core``
9
+ namespace package, so we use lazy ``__getattr__`` resolution to avoid
10
+ eager imports that would collide with the installed package when this
11
+ folder is on ``sys.path`` (pytest rootdir, etc.).
12
  """
13
 
14
+ from __future__ import annotations
15
+
16
 
17
  __all__ = [
18
  "OpenCodeEnv",
 
20
  "RolloutResult",
21
  "RolloutTurn",
22
  ]
23
+
24
+
25
+ def __getattr__(name):
26
+ if name == "OpenCodeEnv":
27
+ from .client import OpenCodeEnv
28
+
29
+ return OpenCodeEnv
30
+ if name in ("OpenCodeState", "RolloutResult", "RolloutTurn"):
31
+ from . import models
32
+
33
+ return getattr(models, name)
34
+ raise AttributeError(f"module 'opencode_env_server' has no attribute {name!r}")
models.py CHANGED
@@ -54,6 +54,12 @@ class RolloutResult(BaseModel):
54
  # Errors (if any) surfacing from sandbox/proxy/verifier path
55
  error: str | None = None
56
 
 
 
 
 
 
 
57
 
58
  class OpenCodeState(State):
59
  """Persistent env state across calls to the single environment instance.
 
54
  # Errors (if any) surfacing from sandbox/proxy/verifier path
55
  error: str | None = None
56
 
57
+ # Diagnostic tails — populated when the primitive or verifier misbehaves so
58
+ # the client can see WHAT happened inside the sandbox without a second
59
+ # round-trip. Each is truncated to a few KB.
60
+ proxy_log_tail: str = ""
61
+ install_log_tail: str = ""
62
+
63
 
64
  class OpenCodeState(State):
65
  """Persistent env state across calls to the single environment instance.
opencode_openenv.egg-info/PKG-INFO CHANGED
@@ -1,11 +1,11 @@
1
  Metadata-Version: 2.4
2
  Name: opencode-openenv
3
  Version: 0.1.0
4
- Summary: OpenEnv OpenCode environment spawns an E2B sandbox per rollout, runs OpenCode against a caller-supplied LLM endpoint, returns reward + proxy trace.
5
  Author-email: adithya-s-k <adithyaskolavi@gmail.com>
6
  Requires-Python: >=3.10
7
- Requires-Dist: openenv-core[core] @ git+https://github.com/adithya-s-k/OpenEnv.git@opencode-harness
8
- Requires-Dist: openenv-opencode_env @ git+https://github.com/adithya-s-k/OpenEnv.git@opencode-harness#subdirectory=envs/opencode_env
9
  Requires-Dist: fastmcp>=3.0.0
10
  Requires-Dist: fastapi>=0.115.0
11
  Requires-Dist: uvicorn>=0.24.0
 
1
  Metadata-Version: 2.4
2
  Name: opencode-openenv
3
  Version: 0.1.0
4
+ Summary: OpenEnv environment running the OpenCode coding agent inside an E2B sandbox, returning reward and per-turn trace.
5
  Author-email: adithya-s-k <adithyaskolavi@gmail.com>
6
  Requires-Python: >=3.10
7
+ Requires-Dist: openenv-core[core] @ file:///fsx/adithyaskolavi/projects/exp_rl/OpenEnv
8
+ Requires-Dist: openenv-opencode_env @ file:///fsx/adithyaskolavi/projects/exp_rl/OpenEnv/envs/opencode_env
9
  Requires-Dist: fastmcp>=3.0.0
10
  Requires-Dist: fastapi>=0.115.0
11
  Requires-Dist: uvicorn>=0.24.0
opencode_openenv.egg-info/SOURCES.txt CHANGED
@@ -18,4 +18,5 @@ server/__init__.py
18
  server/app.py
19
  server/gradio_ui.py
20
  server/opencode_environment.py
21
- server/requirements.txt
 
 
18
  server/app.py
19
  server/gradio_ui.py
20
  server/opencode_environment.py
21
+ server/requirements.txt
22
+ tests/test_client.py
opencode_openenv.egg-info/requires.txt CHANGED
@@ -1,5 +1,5 @@
1
- openenv-core[core] @ git+https://github.com/adithya-s-k/OpenEnv.git@opencode-harness
2
- openenv-opencode_env @ git+https://github.com/adithya-s-k/OpenEnv.git@opencode-harness#subdirectory=envs/opencode_env
3
  fastmcp>=3.0.0
4
  fastapi>=0.115.0
5
  uvicorn>=0.24.0
 
1
+ openenv-core[core] @ file:///fsx/adithyaskolavi/projects/exp_rl/OpenEnv
2
+ openenv-opencode_env @ file:///fsx/adithyaskolavi/projects/exp_rl/OpenEnv/envs/opencode_env
3
  fastmcp>=3.0.0
4
  fastapi>=0.115.0
5
  uvicorn>=0.24.0
pyproject.toml CHANGED
@@ -11,9 +11,8 @@ authors = [
11
  ]
12
  requires-python = ">=3.10"
13
  dependencies = [
14
- # NOTE: openenv-core must come from the same branch as the primitive
15
- # the ``openenv.core.harness`` module doesn't exist on PyPI yet (it lives
16
- # on PR #471 and our opencode-harness branch stacked on top of it).
17
  "openenv-core[core] @ git+https://github.com/adithya-s-k/OpenEnv.git@opencode-harness",
18
  "openenv-opencode_env @ git+https://github.com/adithya-s-k/OpenEnv.git@opencode-harness#subdirectory=envs/opencode_env",
19
  "fastmcp>=3.0.0",
 
11
  ]
12
  requires-python = ">=3.10"
13
  dependencies = [
14
+ # Pull both from the same branch so ``openenv.core.harness`` (only on
15
+ # PR #471's stack) is available alongside the primitive.
 
16
  "openenv-core[core] @ git+https://github.com/adithya-s-k/OpenEnv.git@opencode-harness",
17
  "openenv-opencode_env @ git+https://github.com/adithya-s-k/OpenEnv.git@opencode-harness#subdirectory=envs/opencode_env",
18
  "fastmcp>=3.0.0",
server/opencode_environment.py CHANGED
@@ -33,6 +33,14 @@ from openenv.core.env_server.types import Action, Observation
33
  load_dotenv()
34
 
35
 
 
 
 
 
 
 
 
 
36
  # Default test-script and reward paths inside the sandbox. The server writes
37
  # the caller-supplied ``test_script`` text to this path; the verifier reads
38
  # the reward file back out after it finishes.
@@ -180,6 +188,27 @@ class OpenCodeEnvironment(MCPEnvironment):
180
  },
181
  )
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  @property
184
  def state(self) -> Any:
185
  return self._state
@@ -270,8 +299,20 @@ class OpenCodeEnvironment(MCPEnvironment):
270
  }
271
  for raw in summary.proxy_turns:
272
  result.proxy_turns.append(self._turn_cls(**_clamp_turn(raw)))
 
 
 
 
 
 
273
  except Exception as exc:
274
  result.error = f"{type(exc).__name__}: {exc}"
 
 
 
 
 
 
275
  finally:
276
  if session is not None:
277
  try:
@@ -294,12 +335,17 @@ class OpenCodeEnvironment(MCPEnvironment):
294
 
295
 
296
  def _qualify_model(provider: str, model: str) -> str:
297
- """Return a ``<provider>/<model>`` string acceptable to the primitive.
298
 
299
- If the caller already prefixed the model, leave it alone; otherwise
300
- prepend the provider so OpenCode's config file is well-formed.
 
 
 
301
  """
302
- if "/" in model:
 
 
303
  return model
304
  return f"{provider}/{model}"
305
 
@@ -320,13 +366,19 @@ def _read_reward(sandbox: Any, reward_path: str) -> Optional[float]:
320
  def _clamp_turn(turn: dict[str, Any]) -> dict[str, Any]:
321
  """Clamp per-turn payload sizes to keep responses under a reasonable cap."""
322
  out = dict(turn)
323
- # Compact ``response`` we already captured tokens/logps explicitly.
324
- out["response"] = {
325
- "finish_reason": (out.get("response") or {}).get("choices", [{}])[0].get(
326
- "finish_reason"
327
- ),
328
- "usage": (out.get("response") or {}).get("usage"),
329
  }
 
 
 
 
 
 
330
  req = out.get("request") or {}
331
  messages = req.get("messages") or []
332
  # Keep request messages (trainer needs them) but drop very long tool schemas.
@@ -350,3 +402,16 @@ def _tail(events: list[dict[str, Any]], n: int) -> str:
350
  if not events:
351
  return ""
352
  return "\n".join(json.dumps(e) for e in events[-n:])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  load_dotenv()
34
 
35
 
36
+ # One rollout (sandbox create + opencode install + opencode run + verifier)
37
+ # typically takes 30-180s and can spike to 600s under load. Override
38
+ # OpenEnv's 30s MCP tool-call default so the server doesn't cut our tool
39
+ # off mid-flight. This default still defers to an explicit ``timeout_s``
40
+ # on the StepRequest when the client specifies one.
41
+ _RUN_ROLLOUT_TIMEOUT_S = 900.0
42
+
43
+
44
  # Default test-script and reward paths inside the sandbox. The server writes
45
  # the caller-supplied ``test_script`` text to this path; the verifier reads
46
  # the reward file back out after it finishes.
 
188
  },
189
  )
190
 
191
+ def step(
192
+ self,
193
+ action: Action,
194
+ timeout_s: Optional[float] = None,
195
+ **kwargs: Any,
196
+ ) -> Observation:
197
+ """Override the MCP 30s default with a rollout-friendly budget."""
198
+ if timeout_s is None:
199
+ timeout_s = _RUN_ROLLOUT_TIMEOUT_S
200
+ return super().step(action, timeout_s=timeout_s, **kwargs)
201
+
202
+ async def step_async(
203
+ self,
204
+ action: Action,
205
+ timeout_s: Optional[float] = None,
206
+ **kwargs: Any,
207
+ ) -> Observation:
208
+ if timeout_s is None:
209
+ timeout_s = _RUN_ROLLOUT_TIMEOUT_S
210
+ return await super().step_async(action, timeout_s=timeout_s, **kwargs)
211
+
212
  @property
213
  def state(self) -> Any:
214
  return self._state
 
299
  }
300
  for raw in summary.proxy_turns:
301
  result.proxy_turns.append(self._turn_cls(**_clamp_turn(raw)))
302
+
303
+ # Diagnostic capture — always try to grab the in-sandbox proxy log.
304
+ # Useful when productive_turns=0 or opencode exits abnormally.
305
+ result.proxy_log_tail = _read_safe(
306
+ session.sandbox, "/home/user/logs/agent/proxy.log", tail_chars=4000
307
+ )
308
  except Exception as exc:
309
  result.error = f"{type(exc).__name__}: {exc}"
310
+ # Even on failure, try to pull the proxy log if the sandbox still
311
+ # exists — it often contains the actual upstream HTTP error.
312
+ if session is not None:
313
+ result.proxy_log_tail = _read_safe(
314
+ session.sandbox, "/home/user/logs/agent/proxy.log", tail_chars=4000
315
+ )
316
  finally:
317
  if session is not None:
318
  try:
 
335
 
336
 
337
  def _qualify_model(provider: str, model: str) -> str:
338
+ """Return a ``<provider>/<model>`` string the primitive can split cleanly.
339
 
340
+ The primitive splits ``config.model`` on the first ``/`` to recover the
341
+ upstream model id. If the caller passes a model that already contains a
342
+ slash (e.g. ``Qwen/Qwen3.5-4B``), we still prepend the provider so the
343
+ split separates provider from model and the model part round-trips
344
+ intact (``openai_compatible/Qwen/Qwen3.5-4B`` → upstream ``Qwen/Qwen3.5-4B``).
345
  """
346
+ # Strip an existing <provider>/ prefix only if it matches the configured
347
+ # provider verbatim — otherwise treat the whole string as the model id.
348
+ if model.startswith(provider + "/"):
349
  return model
350
  return f"{provider}/{model}"
351
 
 
366
  def _clamp_turn(turn: dict[str, Any]) -> dict[str, Any]:
367
  """Clamp per-turn payload sizes to keep responses under a reasonable cap."""
368
  out = dict(turn)
369
+ raw_response = out.get("response") or {}
370
+ choices = raw_response.get("choices") or []
371
+ first_choice = choices[0] if choices else {}
372
+ compact: dict[str, Any] = {
373
+ "finish_reason": first_choice.get("finish_reason"),
374
+ "usage": raw_response.get("usage"),
375
  }
376
+ # Surface upstream errors captured by the proxy so they reach the client.
377
+ if raw_response.get("upstream_error") is not None:
378
+ compact["upstream_error"] = raw_response["upstream_error"]
379
+ if raw_response.get("upstream_status") is not None:
380
+ compact["upstream_status"] = raw_response["upstream_status"]
381
+ out["response"] = compact
382
  req = out.get("request") or {}
383
  messages = req.get("messages") or []
384
  # Keep request messages (trainer needs them) but drop very long tool schemas.
 
402
  if not events:
403
  return ""
404
  return "\n".join(json.dumps(e) for e in events[-n:])
405
+
406
+
407
+ def _read_safe(sandbox: Any, path: str, *, tail_chars: int = 4000) -> str:
408
+ """Read a file from the sandbox, returning its last ``tail_chars`` chars.
409
+
410
+ Returns empty string if the file is missing or unreadable. Used for
411
+ diagnostic tails so we never mask a real failure with a read error.
412
+ """
413
+ try:
414
+ content = sandbox.read_text(path) or ""
415
+ except Exception:
416
+ return ""
417
+ return content[-tail_chars:]
tests/conftest.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Conftest for opencode-openenv tests.
2
+
3
+ The env directory is named ``openenv`` which collides with the installed
4
+ ``openenv-core`` namespace when pytest adds the parent directory
5
+ (``…/opencode``) to ``sys.path``. Strip the shadowing entries and evict
6
+ any already-loaded stale modules so ``import openenv.core`` resolves to
7
+ the installed package.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import sys
14
+
15
+
16
+ _OPENENV_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ _OPENCODE_PARENT = os.path.dirname(_OPENENV_DIR)
18
+
19
+ for _entry in (_OPENCODE_PARENT, _OPENENV_DIR):
20
+ while _entry in sys.path:
21
+ sys.path.remove(_entry)
22
+
23
+ _stale_candidates = [
24
+ k
25
+ for k in list(sys.modules)
26
+ if (k == "openenv" or k.startswith("openenv."))
27
+ and not k.startswith("openenv.tests") # keep the conftest's own module
28
+ ]
29
+ for _stale in _stale_candidates:
30
+ module = sys.modules[_stale]
31
+ module_file = getattr(module, "__file__", None) or ""
32
+ # Only evict the modules that resolved to our shadow env directory.
33
+ if module_file.startswith(_OPENENV_DIR + os.sep) or (
34
+ _OPENENV_DIR in module_file and "/.venv/" not in module_file
35
+ ):
36
+ sys.modules.pop(_stale, None)
37
+
38
+ # Sanity check — fail loudly at collection time if the import still breaks.
39
+ try:
40
+ import openenv.core # noqa: F401
41
+ except ImportError as exc:
42
+ raise ImportError(
43
+ "openenv.core still not importable after sys.path scrub; check that "
44
+ "openenv-core is installed in the env's .venv."
45
+ ) from exc
uv.lock CHANGED
@@ -223,11 +223,11 @@ wheels = [
223
 
224
  [[package]]
225
  name = "cachetools"
226
- version = "7.0.5"
227
  source = { registry = "https://pypi.org/simple" }
228
- sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" }
229
  wheels = [
230
- { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" },
231
  ]
232
 
233
  [[package]]
@@ -1710,7 +1710,7 @@ dev = [
1710
  [[package]]
1711
  name = "openenv-core"
1712
  version = "0.2.3"
1713
- source = { git = "https://github.com/adithya-s-k/OpenEnv.git?rev=opencode-harness#048362e400ba87a95eb2aa7332abdce354078c17" }
1714
  dependencies = [
1715
  { name = "fastapi" },
1716
  { name = "fastmcp" },
@@ -1741,7 +1741,7 @@ core = [
1741
  [[package]]
1742
  name = "openenv-opencode-env"
1743
  version = "0.1.0"
1744
- source = { git = "https://github.com/adithya-s-k/OpenEnv.git?subdirectory=envs%2Fopencode_env&rev=opencode-harness#048362e400ba87a95eb2aa7332abdce354078c17" }
1745
  dependencies = [
1746
  { name = "e2b" },
1747
  { name = "fastapi" },
 
223
 
224
  [[package]]
225
  name = "cachetools"
226
+ version = "7.0.6"
227
  source = { registry = "https://pypi.org/simple" }
228
+ sdist = { url = "https://files.pythonhosted.org/packages/76/7b/1755ed2c6bfabd1d98b37ae73152f8dcf94aa40fee119d163c19ed484704/cachetools-7.0.6.tar.gz", hash = "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24", size = 37526, upload-time = "2026-04-20T19:02:23.289Z" }
229
  wheels = [
230
+ { url = "https://files.pythonhosted.org/packages/fe/c4/cf76242a5da1410917107ff14551764aa405a5fd10cd10cf9a5ca8fa77f4/cachetools-7.0.6-py3-none-any.whl", hash = "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", size = 13976, upload-time = "2026-04-20T19:02:21.187Z" },
231
  ]
232
 
233
  [[package]]
 
1710
  [[package]]
1711
  name = "openenv-core"
1712
  version = "0.2.3"
1713
+ source = { git = "https://github.com/adithya-s-k/OpenEnv.git?rev=opencode-harness#4f8bc23dc0e860ac990d8fd0ebdac7a5498e56a3" }
1714
  dependencies = [
1715
  { name = "fastapi" },
1716
  { name = "fastmcp" },
 
1741
  [[package]]
1742
  name = "openenv-opencode-env"
1743
  version = "0.1.0"
1744
+ source = { git = "https://github.com/adithya-s-k/OpenEnv.git?subdirectory=envs%2Fopencode_env&rev=opencode-harness#4f8bc23dc0e860ac990d8fd0ebdac7a5498e56a3" }
1745
  dependencies = [
1746
  { name = "e2b" },
1747
  { name = "fastapi" },