diff --git a/envs/opencode_env/harness.py b/envs/opencode_env/harness.py index 3c5d31e53..8c5bc9318 100644 --- a/envs/opencode_env/harness.py +++ b/envs/opencode_env/harness.py @@ -225,12 +225,16 @@ def create( task: Any, seed: int | None = None, episode_id: str | None = None, + start_agent: bool = True, ) -> OpenCodeSession: """Create one session, retrying with exponential backoff. Session creation spins up a sandbox, installs opencode, and starts the proxy + agent, the flakiest step in a rollout. Each failed attempt tears its own sandbox down (see :meth:`_create_once`), so a retry never leaks. + + Pass ``start_agent=False`` to return before launching the agent (for + example to run setup first), then call ``session.start_agent()``. """ import logging import time @@ -239,7 +243,9 @@ def create( last_exc: Exception = RuntimeError("create_attempts must be >= 1") for i in range(self._create_attempts): try: - return self._create_once(task, seed=seed, episode_id=episode_id) + return self._create_once( + task, seed=seed, episode_id=episode_id, start_agent=start_agent + ) except Exception as exc: # noqa: BLE001 last_exc = exc if i + 1 < self._create_attempts: @@ -256,6 +262,7 @@ def _create_once( task: Any, seed: int | None = None, episode_id: str | None = None, + start_agent: bool = True, ) -> OpenCodeSession: import logging _log = logging.getLogger(__name__) @@ -318,7 +325,8 @@ def _create_once( proxy_trace_path=proxy_trace_path, proxy_bg_job=proxy_bg_job, ) - session.start_agent() + if start_agent: + session.start_agent() return session except Exception as exc: _log.error("factory.create: setup failed, killing sandbox: %r", exc) diff --git a/envs/opencode_env/opencode_runtime.py b/envs/opencode_env/opencode_runtime.py index 142176c8b..20fd99f15 100644 --- a/envs/opencode_env/opencode_runtime.py +++ b/envs/opencode_env/opencode_runtime.py @@ -124,6 +124,7 @@ def build_run_cmd(config: OpenCodeConfig) -> str: format_flag = "--format json" if config.run_format == "json" else "" return ( + "set -o pipefail && " 'export PATH="$HOME/.opencode/bin:$PATH" && ' f"cd {workdir_path(config)} && " f'opencode run {format_flag} "$(cat {instruction_path(config)})" ' diff --git a/envs/opencode_env/server/opencode_environment.py b/envs/opencode_env/server/opencode_environment.py index 2d7831a73..a10290711 100644 --- a/envs/opencode_env/server/opencode_environment.py +++ b/envs/opencode_env/server/opencode_environment.py @@ -333,21 +333,15 @@ def _emit(msg: str) -> None: f"creating E2B sandbox (template={template or 'default'}) — " "this is the slow phase (~5–60s cold, ~5s with template)" ) - session = factory.create(task=opencode_task) + session = factory.create(task=opencode_task, start_agent=False) result.sandbox_id = session.sandbox.sandbox_id _emit( - f"sandbox ready: {result.sandbox_id} — agent started " + f"sandbox ready: {result.sandbox_id} " f"({'proxy on :7000, logprobs capturing' if mode == 'transparent_proxy' else 'direct LLM, no logprobs'})" ) - # Run setup commands one at a time, *before* the agent starts. - # The factory has already started the agent in start_agent() - # during create(); to keep the order "setup → agent → verify" - # we'd need to restructure. As a pragmatic compromise we run - # setup IMMEDIATELY after create(), which races with the agent - # for ~1-2s but is fine for typical pip/git/download work - # because opencode itself takes >=20s to make its first model - # call. + # Run setup one command at a time, before the agent starts. create() + # was called with start_agent=False, so the agent has not launched yet. for i, cmd in enumerate(setup, 1): _emit(f"setup [{i}/{len(setup)}]: {cmd[:80]}") cr = self._exec_command(session.sandbox, cmd, timeout=SETUP_TIMEOUT_S) @@ -359,8 +353,9 @@ def _emit(msg: str) -> None: _emit(f"setup FAILED at [{i}]: exit={cr.exit_code}") break - # Block until the agent is done (or setup already failed). + # Setup is done and clean, so launch the agent now (deferred from create()). if result.error is None: + session.start_agent() _emit( f"agent running — opencode CLI in sandbox " f"(timeout {int(agent_timeout_s)}s)" @@ -370,6 +365,9 @@ def _emit(msg: str) -> None: timeout_s=agent_timeout_s ) _emit(f"agent finished: exit_code={result.agent_exit_code}") + if result.agent_exit_code != 0: + result.error = f"agent exited non-zero ({result.agent_exit_code})" + _emit(f"agent FAILED: exit_code={result.agent_exit_code}") except TimeoutError as exc: result.error = f"agent timeout: {exc}" _emit(f"agent TIMEOUT: {exc}") diff --git a/tests/envs/test_opencode_factory_lifecycle.py b/tests/envs/test_opencode_factory_lifecycle.py index 15377784d..455926f3e 100644 --- a/tests/envs/test_opencode_factory_lifecycle.py +++ b/tests/envs/test_opencode_factory_lifecycle.py @@ -80,7 +80,7 @@ def test_create_retries_then_succeeds(monkeypatch): calls = {"n": 0} session = object() - def _flaky(task, seed=None, episode_id=None): + def _flaky(task, seed=None, episode_id=None, start_agent=True): calls["n"] += 1 if calls["n"] < 3: raise RuntimeError("transient create failure") @@ -97,7 +97,7 @@ def test_create_raises_after_exhausting_attempts(monkeypatch): factory = _factory(sandbox, create_attempts=3, create_backoff_s=0) calls = {"n": 0} - def _always_fails(task, seed=None, episode_id=None): + def _always_fails(task, seed=None, episode_id=None, start_agent=True): calls["n"] += 1 raise RuntimeError("persistent create failure")