Skip to content

tools: subagent has no visibility into its own iter budget #488

@esengine

Description

@esengine

Problem

spawnSubagent (src/tools/subagent.ts:120) caps the child loop with maxToolIters — 16 by default, 20 for the explore persona, 8 for verify, hard ceiling 32. The cap is enforced from the outside: the parent loop refuses to dispatch tool call N+1.

The child never sees this number. The system prompts in src/tools/subagent-types.ts say things like "Cap at 6-8 tool calls" as English prose, but:

  1. The actual budget is whatever the caller passed — could be the type default, could be a user override. The prose can be wrong.
  2. The child doesn't know which iteration it's currently on. So it can't make the call "I have 3 iters left, time to wrap up rather than open another rabbit hole."

The visible failure mode: explore opens an investigation, burns 17 iters mapping the territory, then hits the cap mid-thought and emits a partial summary. The parent has to re-dispatch or read the file itself.

Proposal

Two changes, both small.

1. Bake the actual budget into the child's system prompt.

Replace the static "Cap at 6-8 tool calls" sentence with a slot the spawner fills:

Tool budget: you have ${maxToolIters} tool calls total. Pace yourself
— if you can't fully resolve the task, stop early and return what you
have plus the gap, rather than burning the budget on one branch.

Computed once per spawn from the resolved maxToolIters. Goes into the prompt before NEGATIVE_CLAIM_RULE / TUI_FORMATTING_RULES.

2. Surface a remaining-iter hint as a tool-result suffix when budget is tight.

In spawnSubagent's for-await loop (around subagent.ts:225-240), when toolIter >= maxToolIters - 3, append a one-line note to the tool result the child sees:

[budget: 3 of 20 tool calls left — wrap up soon]

The child observes this through the normal tool-result channel; no schema change. Two iters from cap → "1 left — finalize now". Beyond that the cap fires as today.

Why prompt + telemetry, not just prompt

Static prompt text decays the moment the model thinks it's "fine, I have plenty of room." A countdown injected at run time is harder to ignore. We've tried "static rule only" elsewhere (the storm-breaker, the abort-prompt) and the dynamic surface always outperforms.

Out of scope

  • Changing the budget itself. Defaults are fine.
  • Cooperative early-exit signal (child voluntarily ends). Maybe later; for now, prompt + countdown is enough.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions