Skip to content

Commit 5f4fa9a

Browse files
bai-uipathclaude
andcommitted
docs(run-limits): say max_turns restarts per iteration and expected_turns counts tool calls
max_turns caps model API calls per iteration, and each retry and dialog exchange starts a fresh count. expected_turns is a different unit and scope: tool calls plus the final reply, summed over the task. The Codex and Antigravity pages still described max_turns as a visible-turn cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d597f02 commit 5f4fa9a

6 files changed

Lines changed: 29 additions & 23 deletions

File tree

‎.claude/notes/agents.md‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,12 @@ intentionally brief and out of scope; trimming for DISPLAY belongs in the render
6161

6262
- **Harness run-limit parity**: a shared `BaseAgentConfig` field must mean the same
6363
thing on every backend, so a divergence is either fixed or documented — never silent.
64-
**`run_limits.max_turns` on Codex/Antigravity counts VISIBLE turns** (resolved tool
65-
calls, read live off the shared `EventCollector.visible_turn_count`, the same list
66-
`TurnRecord.commands` holds) because one `communicate()` is a single SDK turn on both,
67-
so a native counter would clamp at 1; claude-code keeps its native SDK cap, whose unit
68-
(an agent-loop turn) absorbs arbitrarily many parallel calls — the same number is NOT
69-
the same budget across harnesses. OpenCode and Pi each keep a native unit too, because
70-
their CLIs stream a real multi-step loop per `communicate()`
71-
(`step_start`/`step_finish`, `turn_start`/`turn_end`). The cap is enforced on the same
64+
**`run_limits.max_turns` counts main-thread model API calls on every harness**, per
65+
iteration (each retry and dialog exchange starts at zero). Codex and Antigravity run one
66+
SDK turn per `communicate()`, so each counts calls from its own stream (Codex
67+
`thread/tokenUsage/updated`, Antigravity MODEL steps). claude-code keeps the CLI's
68+
`--max-turns` plus a backstop that counts main-thread `message_id`s. OpenCode and Pi
69+
stream one `step_start` / `turn_start` per call. The cap is enforced on the same
7270
loop boundary as the cooperative early stop and finalizes cleanly as
7371
`max_turns_exhausted` (no crash, no retry); on Antigravity that boundary lives in
7472
`_drain()`, so the background-work poll loop honors it too.

‎docs/TASK_DEFINITION_GUIDE.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ run_limits:
255255

256256
| Field | Default | Constraint | Description |
257257
|-------|---------|------------|-------------|
258-
| `max_turns` | *unset* | `> 0` | Hard cap on main-thread model API calls per iteration, Claude Code's turn, counted the same on every harness. The tools the last allowed call asks for still run; the turn ends when the next call begins. Unset uses the SDK default. See [HARNESS_PARITY.md](agents/HARNESS_PARITY.md). |
259-
| `expected_turns` | *unset* | `>= 1` | **Soft** target for cumulative visible turns. Exceeding it warns and badges the report; it never aborts. See [`expected_turns`](#expected_turns-soft-efficiency-budget). |
258+
| `max_turns` | *unset* | `> 0` | Hard cap on main-thread model API calls per iteration, Claude Code's turn, counted the same on every harness. The tools the last allowed call asks for still run; the turn ends when the next call begins. Each retry and each dialog exchange starts a fresh count. Unset uses the SDK default. See [HARNESS_PARITY.md](agents/HARNESS_PARITY.md). |
259+
| `expected_turns` | *unset* | `>= 1` | **Soft** target for visible turns (tool calls plus the final reply) summed over the whole task, a different unit from `max_turns`. Exceeding it warns and badges the report; it never aborts. See [`expected_turns`](#expected_turns-soft-efficiency-budget). |
260260
| `task_timeout` | *unset* | `>= 30` | Max seconds for the full run envelope, including agent work, grading, and post-run work. |
261261
| `turn_timeout` | *unset* | `>= 10` | Max seconds for the agent's single `communicate()` iteration. |
262262
| `max_input_tokens` | *unset* | `>= 1` | Max cumulative input (prompt) tokens. |
@@ -327,8 +327,8 @@ that did: a budgeted task that failed counts as over budget, while tasks with no
327327
`expected_turns` budget are excluded entirely (success or fail).
328328

329329
The count compared against the budget is **visible turns** — one per tool call
330-
plus one for the agent's final reply — *not* the SDK's `total_turns` (which
331-
counts assistant messages and can bundle several tool calls into one).
330+
plus one for the agent's final reply. It is *not* `total_turns`, which counts
331+
model API calls (the `max_turns` unit), and one call can batch several tool calls.
332332

333333
Set it to the number of turns a competent agent should need for the task. Pick
334334
budgets consistently across a suite — the headline % is only comparable when
@@ -1721,7 +1721,7 @@ The simulator runs as a tools-disabled Claude Code agent on its own resolved `Ap
17211721
**Semantics:**
17221722

17231723
- The task's `initial_prompt` is the user's *opening* message; the simulator picks up from turn 2.
1724-
- `max_turns` is the intra-dialog cap (the worst-case agent call budget per trial). Use `n_trials` for variance sampling.
1724+
- `max_turns` caps exchanges. Each exchange also gets a fresh `run_limits.max_turns` of model API calls, so the worst case per trial is the product of the two. Use `n_trials` for variance sampling.
17251725
- The `reference` solution, if present, is hidden from the simulator (same security posture as for the coding agent).
17261726
- When `n_trials > 1`, each trial becomes its own `ResolvedTask` with its own zero-padded replicate directory (`runs/<ts>/<variant_id>/<task_id>/<NN>/`) and its own `task.json` — the same fan-out mechanism as experiment `repeats`, which `n_trials` takes precedence over when simulation is enabled. Trial-level metadata appears under `simulation.replicate_index` / `simulation.n_trials` on the `EvaluationResult`.
17271727

‎docs/agents/ANTIGRAVITY.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,9 @@ as every other agent.
190190
5. **`allowed_tools` / `disallowed_tools` are not read.** The harness runs with its
191191
full builtin tool set, so an Antigravity run has tools (web search, subagents,
192192
URL fetch) that the same task file denies on Claude Code and Codex.
193-
6. **`max_turns` counts visible turns.** One `communicate()` is a single SDK turn here,
194-
so the cap counts resolved tool calls instead, enforced on the step loop. See
193+
6. **`max_turns` is counted by the harness.** One `communicate()` is a single SDK turn
194+
here, so the harness counts model API calls itself (a MODEL step at a new
195+
`step_index` opens one) and enforces the cap on the step loop. See
195196
[Run-Limit Parity](HARNESS_PARITY.md).
196197
7. **Shell commands over ~10s are moved to the background.** The localharness has a
197198
10-second maximum synchronous wait; past it the command becomes a background task

‎docs/agents/CODEX.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ The Codex SDK is synchronous. The agent uses `_run_async()` helper to detect and
217217
| **Session Resume** | `--resume {session_id}` | Via thread ID |
218218
| **Permissions** | `permission_mode` + `allowed_tools` | `permission_mode` → sandbox/approval + `allowed_tools`/`disallowed_tools` → thread config |
219219
| **Tool Enforcement** | Not enforced by Coder Eval wrapper | `enabled_tools` honored; `disabled_tools` NOT enforced by the SDK |
220-
| **`max_turns`** | Native SDK turn cap (assistant messages) | Visible-turn cap (tool calls), enforced on the notification pump |
220+
| **`max_turns`** | Model API calls: the CLI's `--max-turns`, plus a harness backstop | Model API calls, counted per `thread/tokenUsage/updated` and enforced on the notification pump |
221221
| **Early stop** | Supported (cooperative `should_stop`, polled between messages) | Supported — polled after each streamed notification; the in-flight turn is interrupted best-effort |
222222
223223
Run-limit semantics per harness: [Run-Limit Parity](HARNESS_PARITY.md).

‎docs/agents/HARNESS_PARITY.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,11 @@ One call can carry several parallel tool calls, so `max_turns` bounds model call
578578
not tool calls. A model that batches does more work per turn, on every harness
579579
alike.
580580

581+
The count is per iteration: each retry and each dialog exchange starts at zero. A
582+
dialog whose agent hits the cap inside an exchange ends with `stop_reason:
583+
agent_max_turns`. That is distinct from `max_turns`, the simulator's cap on
584+
exchanges.
585+
581586
### What a capped run looks like
582587

583588
The signals a capped run leaves behind, on every backend:

‎src/coder_eval/models/limits.py‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,20 @@ class RunLimits(BaseModel):
3232
max_turns: int | None = Field(
3333
default=None,
3434
gt=0,
35-
description="Max main-thread model API calls per iteration, on every harness. None = SDK default.",
35+
description=(
36+
"Max main-thread model API calls per iteration, on every harness. Each retry and each "
37+
"dialog exchange starts a fresh count. None = SDK default."
38+
),
3639
)
3740
expected_turns: int | None = Field(
3841
default=None,
3942
ge=1,
4043
description=(
41-
"Soft target for cumulative visible turns across a task. A 'turn' is one "
42-
"entry in the Turn timeline: each tool call contributes 1, plus 1 for the "
43-
"final reply when present. "
44-
"When the running total exceeds this, the orchestrator logs a one-shot "
45-
"warning and the report renders a badge — the run is NOT aborted "
46-
"(use max_turns for a hard cap). None disables the check."
44+
"Soft target for visible turns summed over the whole task: each tool call counts 1, "
45+
"plus 1 for the final reply when present. This is a different unit and scope from "
46+
"max_turns, which caps model API calls per iteration. When the running total exceeds "
47+
"this, the orchestrator logs a one-shot warning and the report renders a badge; the "
48+
"run is NOT aborted. None disables the check."
4749
),
4850
)
4951
task_timeout: int | None = Field(

0 commit comments

Comments
 (0)