Problem
maxIterations reads like a step/tool-call budget but counts model turns, not tool calls. A single turn can emit unbounded parallel tool calls, so the budget never trips.
Repro
agentLoopStrategy: combineStrategies([
maxIterations(20),
untilFinishReason(['stop']),
])
Expected ~20 tool calls. Got ~160, blowing a 300s serverless timeout. The model emitted ~160 calls across a few turns; iterationCount only saw a handful. ~99% of the wall clock was the model generating the fat multi-call turns — tool execution was negligible.
Root cause (3 layers)
- iterations ≠ tool calls. One assistant turn can carry an array of parallel tool calls; the loop counts turns.
- No tool-call count in strategy state.
AgentLoopState exposes only { iterationCount, messages, finishReason } (packages/ai/src/types.ts:833) — so maxToolCalls is unbuildable in userland.
- Budget checked only between turns.
shouldContinue() runs the strategy at turn boundaries (packages/ai/src/activities/chat/index.ts:1936); a single runaway turn is never inspected mid-flight.
Proposed
- Add cumulative
toolCallCount (and per-turn lastTurnToolCallCount) to AgentLoopState.
- Ship a
maxToolCalls(n) strategy.
- Cap parallel fan-out per turn (bound calls executed in one turn — the direct fix for the 160-in-one-turn case).
- Optional: a
deadline(ms) / wall-clock-aware strategy, since call-counting alone can't catch a single slow turn.
- Docs note: iterations ≠ tool calls when parallel tool use is enabled.
Minimum to close the report: AgentLoopState.toolCallCount + per-turn fan-out cap. Needs E2E coverage (fixture emitting a fat parallel-tool-call turn) + docs update per repo convention.
Seen on @tanstack/ai@0.28.0.
Problem
maxIterationsreads like a step/tool-call budget but counts model turns, not tool calls. A single turn can emit unbounded parallel tool calls, so the budget never trips.Repro
Expected ~20 tool calls. Got ~160, blowing a 300s serverless timeout. The model emitted ~160 calls across a few turns;
iterationCountonly saw a handful. ~99% of the wall clock was the model generating the fat multi-call turns — tool execution was negligible.Root cause (3 layers)
AgentLoopStateexposes only{ iterationCount, messages, finishReason }(packages/ai/src/types.ts:833) — somaxToolCallsis unbuildable in userland.shouldContinue()runs the strategy at turn boundaries (packages/ai/src/activities/chat/index.ts:1936); a single runaway turn is never inspected mid-flight.Proposed
toolCallCount(and per-turnlastTurnToolCallCount) toAgentLoopState.maxToolCalls(n)strategy.deadline(ms)/ wall-clock-aware strategy, since call-counting alone can't catch a single slow turn.Minimum to close the report:
AgentLoopState.toolCallCount+ per-turn fan-out cap. Needs E2E coverage (fixture emitting a fat parallel-tool-call turn) + docs update per repo convention.Seen on
@tanstack/ai@0.28.0.