Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
48cfc5c
feat(tbench2_env): report execution budgets in the reset observation …
KNambiarDJsc Sep 18, 2026
6ffbdf6
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 21, 2026
eee0504
fix: enforce verifier timeout budgets
burtenshaw Sep 21, 2026
a8c93ac
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 21, 2026
07e56d2
fix: freeze verifier budget per episode
burtenshaw Sep 21, 2026
e9b6302
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 21, 2026
a9c98da
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 22, 2026
6b2ef1b
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 22, 2026
325eaa7
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 23, 2026
e385d58
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 23, 2026
d335703
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 23, 2026
374634c
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 23, 2026
5766958
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 23, 2026
df4a913
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 24, 2026
47418fd
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 25, 2026
7977831
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 28, 2026
97890f1
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 28, 2026
a4f3ea1
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 29, 2026
fb17178
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 29, 2026
f39432a
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 29, 2026
68efc29
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 30, 2026
ab77e87
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 30, 2026
4780ea9
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 30, 2026
22974b4
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 30, 2026
ba55b31
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 30, 2026
1f9fb55
Merge branch 'main' into feat/tbench2-reset-budgets
burtenshaw Sep 30, 2026
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
7 changes: 7 additions & 0 deletions docs/source/environments/tbench2.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ docker build -t tbench2-env:latest -f envs/tbench2_env/server/Dockerfile .
| `action_type` | str | The action type that produced this observation |
| `info` | dict | Additional metadata |

**Execution budgets in the `reset` observation.** `reset` reports the time budgets that bound a single env op in `info`, so clients can derive per-message deadlines (budget plus margin) instead of guessing:

| Key | Modes | Description |
|-----|-------|-------------|
| `verifier_timeout_sec` | local, Docker | Budget for one `evaluate` call: the task's `task.toml` `[verifier].timeout_sec`, else 900. |
| `command_timeout_s` | local only | Per-command budget for `exec` (`TB2_COMMAND_TIMEOUT_S`). Docker mode has no server-side per-command timeout, so it does not report one. |

### State
**Tbench2State**: Server-side state for the task session

Expand Down
7 changes: 7 additions & 0 deletions envs/tbench2_env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ docker build -t tbench2-env:latest -f envs/tbench2_env/server/Dockerfile .
| `action_type` | str | The action type that produced this observation |
| `info` | dict | Additional metadata |

**Execution budgets in the `reset` observation.** `reset` reports the time budgets that bound a single env op in `info`, so clients can derive per-message deadlines (budget plus margin) instead of guessing:

| Key | Modes | Description |
|-----|-------|-------------|
| `verifier_timeout_sec` | local, Docker | Budget for one `evaluate` call: the task's `task.toml` `[verifier].timeout_sec`, else 900. |
| `command_timeout_s` | local only | Per-command budget for `exec` (`TB2_COMMAND_TIMEOUT_S`). Docker mode has no server-side per-command timeout, so it does not report one. |

### State
**Tbench2State**: Server-side state for the task session

Expand Down
64 changes: 46 additions & 18 deletions envs/tbench2_env/server/tbench2_env_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import io
import logging
import math
import os
import re
import shlex
Expand Down Expand Up @@ -153,6 +154,11 @@ def _workdir_is_server_tree(workdir: str) -> bool:
return True # unresolvable path: fail toward the task-dir fallback


# Verifier budget when task.toml declares no [verifier].timeout_sec. Shared by
# reset() (which reports it) and evaluation (which enforces it).
_DEFAULT_VERIFIER_TIMEOUT_S = 900.0


def _read_timeout(task_dir: Path, fallback: float) -> float:
task_toml = task_dir / "task.toml"
if not task_toml.exists():
Expand All @@ -162,7 +168,13 @@ def _read_timeout(task_dir: Path, fallback: float) -> float:
except Exception:
return fallback
verifier = data.get("verifier", {})
return float(verifier.get("timeout_sec", fallback))
if not isinstance(verifier, dict):
return fallback
try:
timeout_s = float(verifier.get("timeout_sec", fallback))
except (TypeError, ValueError, OverflowError):
return fallback
return timeout_s if math.isfinite(timeout_s) and timeout_s > 0 else fallback


# The scoring exec echoes its verdict on a marker line so the caller can parse
Expand All @@ -185,14 +197,14 @@ def _canonical_eval_cmd(workdir: str, timeout_s: float | None = None) -> str:
then, and callers need the pytest diagnostics on failure; the reward
marker line stays last for parsing.

``timeout_s`` bounds test.sh with coreutils ``timeout`` when present in
the image. The local mode passes None: its terminal toolkit enforces the
``timeout_s`` bounds test.sh with coreutils ``timeout`` (required in the
image). The local mode passes None: its terminal toolkit enforces the
budget itself. Docker exec has no server-side timeout, so the task's own
verifier budget is enforced in-shell.
"""
run = f"bash {_VERIFY_TESTS_DIR}/test.sh"
if timeout_s is not None:
run = f"if command -v timeout >/dev/null 2>&1; then timeout {int(timeout_s)} {run}; else {run}; fi"
run = f"timeout {timeout_s:g} {run}"
return (
f"cd {shlex.quote(workdir)} && "
f"{run} > {_VERIFIER_LOG_DIR}/testsh.log 2>&1; "
Expand Down Expand Up @@ -233,20 +245,21 @@ def _require_canonical_verdict(reward: float | None, output: str) -> float:
return reward


def _fallback_eval_cmd(workdir: str) -> str:
def _fallback_eval_cmd(workdir: str, timeout_s: float | None = None) -> str:
"""pytest against the staged tests copy, for task dirs without the
canonical harness (none of the 89 official TB2 tasks — they all ship
test.sh — but custom task dirs may only have bare pytest tests). Prefer
uvx so pytest comes with its own toolchain like the canonical harness
does. Verify from the same directory the agent worked in.
"""
return (
f"cd {shlex.quote(workdir)} && "
run = (
"if command -v uvx >/dev/null 2>&1; "
f"then uvx --with pytest==8.4.1 pytest -q {_VERIFY_TESTS_DIR} -rA; "
f"else python -m pytest -q {_VERIFY_TESTS_DIR} -rA; fi; "
f"echo {_EXIT_CODE_MARKER}$?"
f"else python -m pytest -q {_VERIFY_TESTS_DIR} -rA; fi"
)
if timeout_s is not None:
run = f"timeout {timeout_s:g} bash -c {shlex.quote(run)}"
return f"cd {shlex.quote(workdir)} && {run}; echo {_EXIT_CODE_MARKER}$?"


def _parse_exit_code_marker(output: str) -> int:
Expand Down Expand Up @@ -298,6 +311,7 @@ def __init__(

self._state = Tbench2State()
self._task_dir: Path | None = None
self._verifier_timeout_s = _DEFAULT_VERIFIER_TIMEOUT_S
self._terminal_toolkit = None
self._instruction = ""
self._workdir = ""
Expand Down Expand Up @@ -356,6 +370,9 @@ def reset(
session_logs_dir=session_logs_dir,
safe_mode=self.safe_mode,
)
self._verifier_timeout_s = _read_timeout(
task_dir, fallback=_DEFAULT_VERIFIER_TIMEOUT_S
)

self._state = Tbench2State(
episode_id=episode_id or str(uuid4()),
Expand All @@ -374,7 +391,10 @@ def reset(
task_path=str(task_dir),
session_id=None,
action_type="reset",
info={},
info={
"verifier_timeout_sec": self._verifier_timeout_s,
"command_timeout_s": self.command_timeout_s,
},
reward=0.0,
done=False,
)
Expand Down Expand Up @@ -479,6 +499,7 @@ def state(self) -> Tbench2State:
def close(self) -> None:
self._terminal_toolkit = None
self._task_dir = None
self._verifier_timeout_s = _DEFAULT_VERIFIER_TIMEOUT_S
self._instruction = ""

def _resolve_task_path(self, task_id: str | None, task_path: str | None) -> Path:
Expand Down Expand Up @@ -583,9 +604,8 @@ def _evaluate_task(self) -> tuple[str, float, dict[str, Any]]:
if self._terminal_toolkit is None:
raise RuntimeError("Terminal toolkit not initialized.")

# The task's own verifier budget (task.toml [verifier].timeout_sec) —
# heavy tests legitimately run minutes (circuit-fibsqrt declares 3600s).
verifier_timeout_s = _read_timeout(self._task_dir, fallback=900.0)
# Honor the reset-time budget even if the agent edits task.toml.
verifier_timeout_s = self._verifier_timeout_s

with self._CANONICAL_EVAL_LOCK:
try:
Expand Down Expand Up @@ -708,6 +728,7 @@ def __init__(

self._state = Tbench2State()
self._task_dir: Path | None = None
self._verifier_timeout_s = _DEFAULT_VERIFIER_TIMEOUT_S
self._docker_client = None
self._container = None
self._instruction = ""
Expand Down Expand Up @@ -789,6 +810,9 @@ def reset(
except Exception:
self.close()
raise
self._verifier_timeout_s = _read_timeout(
task_dir, fallback=_DEFAULT_VERIFIER_TIMEOUT_S
)

return Tbench2Observation(
instruction=self._instruction,
Expand All @@ -799,7 +823,10 @@ def reset(
task_path=str(task_dir),
session_id=None,
action_type="reset",
info={"docker_image": self._task_image},
info={
"docker_image": self._task_image,
"verifier_timeout_sec": self._verifier_timeout_s,
},
reward=0.0,
done=False,
)
Expand Down Expand Up @@ -1027,9 +1054,7 @@ def _evaluate_docker(self) -> tuple[str, float, dict[str, Any]]:
{"tests_passed": False, "error": "missing tests"},
)

# The task's own verifier budget (task.toml [verifier].timeout_sec) —
# heavy tests legitimately run minutes (circuit-fibsqrt declares 3600s).
verifier_timeout_s = _read_timeout(self._task_dir, fallback=900.0)
verifier_timeout_s = self._verifier_timeout_s
workdir = self._workdir or "/task"

wipe_ec, wipe_out = self._exec_in_container(
Expand All @@ -1054,7 +1079,9 @@ def _evaluate_docker(self) -> tuple[str, float, dict[str, Any]]:
)
info = {"tests_passed": reward == 1.0, "harness": "tests/test.sh"}
else:
_, output = self._exec_in_container(_fallback_eval_cmd(workdir))
_, output = self._exec_in_container(
_fallback_eval_cmd(workdir, timeout_s=verifier_timeout_s)
)
exit_code = _parse_exit_code_marker(output)
reward = 1.0 if exit_code == 0 else 0.0
info = {"tests_passed": exit_code == 0, "exit_code": exit_code}
Expand Down Expand Up @@ -1097,6 +1124,7 @@ def close(self) -> None:
pass
self._container = None
self._task_dir = None
self._verifier_timeout_s = _DEFAULT_VERIFIER_TIMEOUT_S
self._instruction = ""
self._workdir = ""

Expand Down
Loading