Repository navigation
[feat] Limit each benchmark trial's time, tokens and turns #1400
Description
Parent: #1137
Goal
A benchmark trial has no limit of its own. #1180's guide says so: "A trial has no timeout of its own. A tool that never returns runs until the suite's timeout, and every trial after it settles not_run." One hung tool turns a whole run into not_run, and a tool that loops spends tokens no table shows until the end.
Workflows already have the limits a trial needs: timeout, budget: { tokens } and max_turns (docs/guide/workflow-advanced.md, "Requirements and limits"). A trial is a prompt step, so let the suite set them for each trial.
Example
# benchmarks/fix-auth/suite.yaml
schemaVersion: v1alpha1
name: fix-auth
repo: https://github.com/acme/auth-service.git
defaults: { tool: claude }
arms:
- name: claude
- name: codex
tool: codex
repetitions: 3
timeout: 45m
trial:
timeout: 8m
budget: { tokens: 300000 }
max_turns: 60| Arm | Tool | … | Passed | Failed | Error | … | Stopped by a limit | Deviations | … |
| claude | claude | … | 4 | 2 | 0 | … | 1 | 0 | … |
| codex | codex | … | 3 | 3 | 0 | … | 2 | 3 | … |
Scope
-
trialinsuite.yaml, with any oftimeout,budgetandmax_turns, for every trial's prompt step.planwrites them onto that step withon_limit: end_turn, a key any workflow prompt step can use, on whichbudgetis allowed:- id: prompt timeout: 8m budget: { tokens: 300000 } max_turns: 60 on_limit: end_turn # a limit ends the agent's turn; the step still succeeds, with limit: <kind>
Under
end_turnthe limits count from the tool's launch, so preparing the clone and setup never count against the turn. No key means no limit, as today. -
The check still decides. When a limit stops the prompt, teamai runs the trial's check on what the agent left, after restoring
protectas usual. The trial ispassedorfailedby that check, and its record entry gainslimit: timeout | budget | max_turns. A limit is part of the task, like the time a person has, so a slower tool that does not finish fails the task; it is not an error of the harness. A task judged bysuccessCriteriais judged on what the agent left, in the same way. -
The table gains "Stopped by a limit", counted per arm, so a pass rate shows how many trials ran out.
-
An unenforced limit is a deviation. The local driver passes
max_turnsto headless Claude only, and other tools report it inunenforced. A driver that does not declarecan.end_turngets the releasedon_limit: faillease instead, and its pass listson_limitinunenforced. A trial whose report lists a suite limit, oron_limit, inunenforcedcounts as a deviation, as a trial on another tool or model does today, so the table never compares a capped arm with an uncapped one silently. -
The suite's
timeoutstays the cap of the whole run. Atrial.timeoutthat is not shorter than it makes the suite invalid (trial.timeout must be shorter than timeout, which bounds the whole run):teamai review --branchfails it,teamai pullwarns, andteamai benchmark runrefuses it before a run is created, as for any other invalid suite. -
budgetfollows the step rule: a tool that reports no tokens cannot be held to a budget, and its trial ends as that rule says (budget_unmeasured). -
The plan frozen at start includes
trial, so a pull during the run changes no limit.
Out of scope
Limits per task, a soft limit that stops the agent and counts the trial as a success, and limits on setup or on the check, which stay under the suite's timeout.
- Arms that run a workflow ([feat] Compare team asset sets in benchmarks #1181).
timeoutandbudgetare one allowance for the whole workflow, spent by the measured duration and usage of its prompt turns. Each inner prompt is limited to what is left. Once it is spent, the workflow's remaining steps are skipped and the check runs.max_turnsis listed inunenforcedfor such an arm, so each of its trials counts as a deviation.
Acceptance criteria
- With
trial.timeout: 1mand a fake tool that sleeps 5 minutes, that trial's tool is stopped after 1 minute, the check runs, the trial isfailedwithlimit: timeout, and the next trial runs. - A fake tool that writes the fix and then hangs is stopped at the limit, and its trial is
passedwithlimit: timeout. - A trial whose tool passes
budget.tokensis stopped, its check runs, and its entry haslimit: budget. - With
max_turnsset, under the local driver, the codex arm's trials count as deviations and listmax_turnsinunenforced; the claude arm's do not. trial.timeoutnot shorter than the suite'stimeoutfailsteamai review --branchwith the file and line, andteamai benchmark runrefuses the suite before a run is created.- The table counts "Stopped by a limit" per arm, and each such record entry carries
limit. - A suite without
trialruns and reports as before.
Test plan
- Unit tests for parsing
trial, the checks against the suite'stimeout, and the deviation rule. - E2E with a fixture project and fake tools for criteria 1 to 4, in the sandbox [feat] Run benchmark suites as workflows #1180 requires.
- One real-CLI run with Claude for the PR body.
Definition of done
docs/guide/benchmarks.mdanddocs/guide/zh-CN/benchmarks.mddocumenttrial, "Stopped by a limit",limiton the record entry, and how the limits apply to a workflow arm.- The workflow guide pages, English and Chinese, document
on_limit: end_turnon a prompt step,budgetunder it, andcan.end_turnfor external drivers. - The tests in Test plan pass, including the e2e for criteria 1 to 4.
- The PR's real-CLI record includes one Claude trial stopped by
trial.timeoutwhose check still ran.
Watch out for
- The trial's prompt step must stop its tool's process group, not only the step, so the check never runs while the tool still writes.
- Document
trialindocs/guide/benchmarks.mdand its zh-CN page.
Dependencies
- Blocked by: [feat] Run benchmark suites as workflows #1180
- Unblocks: none
Activity
SaulMoro commentedon Oct 26, 2026
Decisions for the implementation in #1379, which closes this issue.
A limit ends the prompt. It does not fail it. With the released
on_limit: fail, a step-leveltimeout,budgetormax_turnsfails the step, and a failed prompt never reaches its check. So the trial's prompt step setson_limit: end_turn.planfreezes them, and the benchmark record copies them aslimits.can.end_turnenforces the lease'slimits: { timeoutMs?, maxTurns?, budget?, onLimit }, and reportsdurationMs.outcome: successwithlimit: timeout | budget | max_turns. teamai refuses alimitthe lease did not set.How the local driver enforces each limit:
timeoutmax_turns--max-turns. Itserror_max_turnsresult becomeslimit: max_turns.unenforcedunenforcedbudgetstep_finishusageunenforced. Codex reports usage only when its turn completes, so it cannot be stopped mid-run. A total past the budget still recordslimit: budget.budget_unmeasured.Acceptance criterion 3 now reads as follows. A trial whose streamed usage passes
budget.tokensis stopped, its check runs, and its entry haslimit: budget. A Codex trial whose final usage passes the budget listsbudgetinunenforced, counts as a deviation, and haslimit: budget.Everything else stands as written: the table column, the
trial.timeoutnot shorter than the suite'stimeoutbeing invalid, and the frozen plan.