diff --git a/examples/swe-agent/README.md b/examples/swe-agent/README.md index 19f377e7cdc..9e42e08dd8c 100644 --- a/examples/swe-agent/README.md +++ b/examples/swe-agent/README.md @@ -48,6 +48,19 @@ generous — agentic trials routinely run past an hour, and a short timeout kill them mid-episode. Verify `http://:30000/health` before launching Miles. +The two per-trial timeouts must be ordered. `--agent-timeout` is the authoritative +one: when it fires, the agent server ends the trial and frees its sandbox. The +rollout client applies a second ceiling, `AGENT_TRIAL_TIMEOUT` (default 7200 +seconds), which has to stay above `--agent-timeout`. If the client gives up first, +the trial is recorded as aborted while the agent server keeps running it, so the +sandbox and its `--max-concurrent` slot stay busy for the remaining difference, and +the aborted sample takes its whole GRPO group down with it. Raise it through the +launcher's generic env-var hook: + +```bash +python examples/swe-agent/run.py ... --extra-env-vars 'AGENT_TRIAL_TIMEOUT=10800' +``` + If the trainer reaches the agent server through a proxy or an in-cluster service rather than directly, point `--agent-server-url` at that stable name rather than an ephemeral pod address. The rollout client enables TCP keepalive probes so diff --git a/examples/swe-agent/swe_agent_function.py b/examples/swe-agent/swe_agent_function.py index 6ec2252367f..62f1f7ea3ca 100644 --- a/examples/swe-agent/swe_agent_function.py +++ b/examples/swe-agent/swe_agent_function.py @@ -23,9 +23,17 @@ logger = logging.getLogger(__name__) +# Backstop for an unreachable agent server; its own --agent-timeout should fire first. +_DEFAULT_AGENT_TRIAL_TIMEOUT_S = 7200 + _agent_server_client: httpx.AsyncClient | None = None +def _agent_trial_timeout_s() -> int: + """Per-trial ceiling for the agent-server call, overridable via AGENT_TRIAL_TIMEOUT.""" + return int(os.environ.get("AGENT_TRIAL_TIMEOUT", _DEFAULT_AGENT_TRIAL_TIMEOUT_S)) + + def _get_agent_server_client() -> httpx.AsyncClient: """Return a client whose long-running requests survive idle network paths.""" global _agent_server_client @@ -102,13 +110,14 @@ async def run( if session_server_instance_id is not None: request["session_server_instance_id"] = session_server_instance_id + trial_timeout_s = _agent_trial_timeout_s() try: response = await asyncio.wait_for( _post_agent_server(f"{agent_server_url}/run", request), - timeout=3600, # 1 hour max per trial + timeout=trial_timeout_s, ) except asyncio.TimeoutError: - logger.error("Agent server call timed out after 3600s") + logger.error(f"Agent server call timed out after {trial_timeout_s}s") return None except asyncio.CancelledError: logger.warning("Agent server call cancelled (sibling task failure?)")