Skip to content

fix(ai): bound agent tool-call fan-out (#964)#965

Merged
AlemTuzlak merged 3 commits into
mainfrom
964-chat-maxiterations-doesnt-bound-tool-calls-one-turn-can-fan-out-unbounded-parallel-calls
Jul 20, 2026
Merged

fix(ai): bound agent tool-call fan-out (#964)#965
AlemTuzlak merged 3 commits into
mainfrom
964-chat-maxiterations-doesnt-bound-tool-calls-one-turn-can-fan-out-unbounded-parallel-calls

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • AgentLoopState now includes toolCallCount and lastTurnToolCallCount so strategies can budget tools, not just model turns
  • New maxToolCalls(n) strategy stops the agent loop once cumulative tool calls hit the limit
  • New chat({ maxToolCallsPerTurn }) option caps how many parallel tool calls execute in a single turn (excess get error results so history stays consistent)
  • Docs clarify that iterations ≠ tool calls when parallel tool use is enabled

Fixes the report where maxIterations(20) still allowed ~160 tool calls in a few fat turns and blew serverless timeouts.

Usage

import { chat, combineStrategies, maxIterations, maxToolCalls } from '@tanstack/ai'

const stream = chat({
  adapter,
  messages,
  tools,
  maxToolCallsPerTurn: 10,
  agentLoopStrategy: combineStrategies([
    maxIterations(20),
    maxToolCalls(20),
  ]),
})

Test plan

  • Unit: maxToolCalls strategy behavior
  • Unit: strategy state exposes toolCallCount / lastTurnToolCallCount
  • Unit: maxToolCallsPerTurn executes only the first N calls and emits skip errors for the rest
  • E2E: testing/e2e/tests/max-tool-calls.spec.ts (fat 8-call turn → 3 executed, 5 skipped)
  • Types pass for @tanstack/ai

Closes #964

Summary by CodeRabbit

  • New Features

    • Added tool-call budgeting controls across an agent run and within a single model turn.
    • Introduced a cumulative tool-call strategy and a new per-turn maxToolCallsPerTurn limit.
    • When limits are exceeded, extra tool calls are not executed, but error tool results are emitted to keep chat history consistent.
  • Documentation

    • Clarified that maxIterations bounds model turns (not tool calls).
    • Expanded docs and examples for using maxToolCalls and combining strategies with per-turn tool-call caps.

…allsPerTurn

maxIterations only counts model turns, so one turn can emit unbounded parallel
tool calls. Expose toolCallCount on AgentLoopState, add a maxToolCalls strategy,
and cap per-turn execution with maxToolCallsPerTurn.

Closes #964
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The agent loop now tracks cumulative and per-turn tool calls, exports a maxToolCalls strategy, caps per-turn tool execution, emits error results for skipped calls, and adds tests, E2E coverage, documentation, and release metadata.

Changes

Tool Call Budgeting

Layer / File(s) Summary
Tool-call contracts and strategy
packages/ai/src/types.ts, packages/ai/src/activities/chat/agent-loop-strategies.ts, packages/ai/src/index.ts, packages/ai/tests/agent-loop-strategies.test.ts, packages/ai/tests/chat.test.ts
AgentLoopState exposes cumulative and per-turn counters, while maxToolCalls stops continuation at a cumulative tool-call limit.
Per-turn budget enforcement
packages/ai/src/activities/chat/index.ts
TextEngine validates and applies maxToolCallsPerTurn, executes allowed calls, emits error results for skipped calls, and preserves complete tool-result ordering.
Fat parallel-call regression coverage
testing/e2e/src/routes/api.max-tool-calls-wire.ts, testing/e2e/src/routeTree.gen.ts, testing/e2e/tests/max-tool-calls.spec.ts
A custom adapter and route exercise parallel tool calls, capped execution, cumulative counting, and emitted skipped results.
Documentation and release metadata
docs/api/ai.md, docs/chat/agentic-cycle.md, packages/ai/skills/ai-core/tool-calling/SKILL.md, .changeset/max-tool-calls-budget.md, docs/config.json
Documentation and release entries describe model-turn limits, cumulative tool-call budgets, per-turn caps, and the new state counters.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Chat
  participant TextEngine
  participant Adapter
  participant Tool
  Client->>Chat: start chat with tool-call limits
  Chat->>TextEngine: configure maxToolCallsPerTurn
  TextEngine->>Adapter: request model turn
  Adapter-->>TextEngine: return parallel tool calls
  TextEngine->>Tool: execute allowed calls
  TextEngine-->>Chat: emit executed and skipped tool results
  Chat-->>Client: stream completed response
Loading

Possibly related PRs

  • TanStack/ai#711: Updates documentation metadata timestamps in docs/config.json, matching the metadata changes here.

Suggested reviewers: alemtuzlak

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the core change and testing, but it omits the template's checklist and release-impact sections. Add the Changes, Checklist, and Release Impact sections from the template, including the required checkbox items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: bounding agent tool-call fan-out.
Linked Issues check ✅ Passed The implementation adds tool-call counters, a maxToolCalls strategy, per-turn caps, docs, and E2E coverage matching #964.
Out of Scope Changes check ✅ Passed The changes stay focused on tool-call budgeting, docs, and tests; no unrelated feature work is apparent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 964-chat-maxiterations-doesnt-bound-tool-calls-one-turn-can-fan-out-unbounded-parallel-calls

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

testing/e2e/tests/max-tool-calls.spec.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): testing/e2e/tests/max-tool-calls.spec.ts


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

1 package(s) bumped directly, 44 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-acp 0.2.2 → 1.0.0 Dependent
@tanstack/ai-angular 0.3.0 → 1.0.0 Dependent
@tanstack/ai-anthropic 0.16.2 → 1.0.0 Dependent
@tanstack/ai-bedrock 0.1.3 → 1.0.0 Dependent
@tanstack/ai-claude-code 0.2.2 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.3.7 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.3.10 → 1.0.0 Dependent
@tanstack/ai-codex 0.2.2 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.33 → 1.0.0 Dependent
@tanstack/ai-fal 0.9.11 → 1.0.0 Dependent
@tanstack/ai-gemini 0.20.0 → 1.0.0 Dependent
@tanstack/ai-grok 0.14.8 → 1.0.0 Dependent
@tanstack/ai-grok-build 0.2.2 → 1.0.0 Dependent
@tanstack/ai-groq 0.5.2 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.46 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.46 → 1.0.0 Dependent
@tanstack/ai-mistral 0.2.2 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.15 → 1.0.0 Dependent
@tanstack/ai-openai 0.17.0 → 1.0.0 Dependent
@tanstack/ai-opencode 0.2.2 → 1.0.0 Dependent
@tanstack/ai-openrouter 0.15.9 → 1.0.0 Dependent
@tanstack/ai-preact 0.11.0 → 1.0.0 Dependent
@tanstack/ai-react 0.18.0 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.15 → 1.0.0 Dependent
@tanstack/ai-sandbox 0.2.3 → 1.0.0 Dependent
@tanstack/ai-sandbox-cloudflare 0.2.3 → 1.0.0 Dependent
@tanstack/ai-sandbox-daytona 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-docker 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-local-process 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-sprites 0.2.1 → 1.0.0 Dependent
@tanstack/ai-sandbox-vercel 0.2.0 → 1.0.0 Dependent
@tanstack/ai-solid 0.15.0 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.14 → 1.0.0 Dependent
@tanstack/ai-svelte 0.15.0 → 1.0.0 Dependent
@tanstack/ai-vue 0.15.0 → 1.0.0 Dependent
@tanstack/openai-base 0.9.8 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.41.0 → 0.42.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-client 0.22.0 → 0.22.1 Dependent
@tanstack/ai-devtools-core 0.4.23 → 0.4.24 Dependent
@tanstack/ai-isolate-cloudflare 0.2.37 → 0.2.38 Dependent
@tanstack/ai-mcp 0.2.4 → 0.2.5 Dependent
@tanstack/ai-vue-ui 0.2.33 → 0.2.34 Dependent
@tanstack/preact-ai-devtools 0.1.66 → 0.1.67 Dependent
@tanstack/react-ai-devtools 0.2.66 → 0.2.67 Dependent
@tanstack/solid-ai-devtools 0.2.66 → 0.2.67 Dependent

@nx-cloud

nx-cloud Bot commented Jul 20, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit f9858a6

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2m 5s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-20 06:19:54 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@965

@tanstack/ai-acp

npm i https://pkg.pr.new/@tanstack/ai-acp@965

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@965

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@965

@tanstack/ai-bedrock

npm i https://pkg.pr.new/@tanstack/ai-bedrock@965

@tanstack/ai-claude-code

npm i https://pkg.pr.new/@tanstack/ai-claude-code@965

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@965

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@965

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@965

@tanstack/ai-codex

npm i https://pkg.pr.new/@tanstack/ai-codex@965

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@965

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@965

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@965

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@965

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@965

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@965

@tanstack/ai-grok-build

npm i https://pkg.pr.new/@tanstack/ai-grok-build@965

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@965

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@965

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@965

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@965

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@965

@tanstack/ai-mistral

npm i https://pkg.pr.new/@tanstack/ai-mistral@965

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@965

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@965

@tanstack/ai-opencode

npm i https://pkg.pr.new/@tanstack/ai-opencode@965

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@965

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@965

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@965

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@965

@tanstack/ai-sandbox

npm i https://pkg.pr.new/@tanstack/ai-sandbox@965

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-sandbox-cloudflare@965

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/@tanstack/ai-sandbox-daytona@965

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/@tanstack/ai-sandbox-docker@965

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/@tanstack/ai-sandbox-local-process@965

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/@tanstack/ai-sandbox-sprites@965

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/@tanstack/ai-sandbox-vercel@965

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@965

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@965

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@965

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@965

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@965

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@965

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@965

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@965

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@965

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@965

commit: f9858a6

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai/src/activities/chat/index.ts`:
- Around line 1979-2013: Clamp maxToolCallsPerTurn to a non-negative value in
applyToolCallBudget before using it for comparisons, slicing, and skipped-result
generation. Ensure negative configuration values execute zero tool calls and
mark the entire batch as skipped, while preserving the existing behavior for
null, zero, and positive caps.

In `@testing/e2e/tests/max-tool-calls.spec.ts`:
- Around line 1-7: Update the header comment in the
maxToolCalls/maxToolCallsPerTurn spec to explicitly state that it uses the fully
synthetic in-process createFatParallelAdapter, never reaches a real LLM
provider, and is therefore exempt from aimock wiring under the policy for
runtime/infrastructure-only tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15f0b90b-96c1-457e-95c4-7dc9557d3d34

📥 Commits

Reviewing files that changed from the base of the PR and between bf870fa and 68d8565.

📒 Files selected for processing (14)
  • .changeset/max-tool-calls-budget.md
  • docs/api/ai.md
  • docs/chat/agentic-cycle.md
  • docs/config.json
  • packages/ai/skills/ai-core/tool-calling/SKILL.md
  • packages/ai/src/activities/chat/agent-loop-strategies.ts
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/src/index.ts
  • packages/ai/src/types.ts
  • packages/ai/tests/agent-loop-strategies.test.ts
  • packages/ai/tests/chat.test.ts
  • testing/e2e/src/routeTree.gen.ts
  • testing/e2e/src/routes/api.max-tool-calls-wire.ts
  • testing/e2e/tests/max-tool-calls.spec.ts

Comment on lines +1979 to +2013
private applyToolCallBudget(toolCalls: Array<ToolCall>): {
toExecute: Array<ToolCall>
skippedResults: Array<ToolResult>
} {
this.lastTurnToolCallCount = toolCalls.length
this.toolCallCount += toolCalls.length

const cap = this.maxToolCallsPerTurn
if (cap == null || toolCalls.length <= cap) {
return { toExecute: toolCalls, skippedResults: [] }
}

this.logger.agentLoop(
`maxToolCallsPerTurn=${cap} skipped=${toolCalls.length - cap}`,
{
maxToolCallsPerTurn: cap,
emitted: toolCalls.length,
skipped: toolCalls.length - cap,
},
)

const toExecute = toolCalls.slice(0, cap)
const skippedResults: Array<ToolResult> = toolCalls
.slice(cap)
.map((tc) => ({
toolCallId: tc.id,
toolName: tc.function.name,
result: {
error: `Skipped: exceeded maxToolCallsPerTurn (${cap})`,
},
state: 'output-error' as const,
}))

return { toExecute, skippedResults }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node -e "console.log([0,1,2,3,4,5,6,7].slice(0,-1))"

Repository: TanStack/ai

Length of output: 177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the option wiring and any validation/defaulting for maxToolCallsPerTurn.
rg -n "maxToolCallsPerTurn" packages/ai/src -S

# Inspect the relevant implementation area around applyToolCallBudget.
sed -n '1920,2035p' packages/ai/src/activities/chat/index.ts

# Inspect the option/type definition if present.
fd -a "types.ts" packages/ai/src

Repository: TanStack/ai

Length of output: 5672


Clamp maxToolCallsPerTurn to 0 or greater before slicing. Negative values make slice(0, cap) execute most of the batch and only skip the tail, so a bad config defeats the intended per-turn cap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/activities/chat/index.ts` around lines 1979 - 2013, Clamp
maxToolCallsPerTurn to a non-negative value in applyToolCallBudget before using
it for comparisons, slicing, and skipped-result generation. Ensure negative
configuration values execute zero tool calls and mark the entire batch as
skipped, while preserving the existing behavior for null, zero, and positive
caps.

Comment on lines +1 to +7
import { expect, test } from '@playwright/test'

/**
* Regression for issue #964: maxIterations counts model turns, not tool calls.
* maxToolCalls + maxToolCallsPerTurn must bound fan-out from a single fat turn.
*/
test.describe('chat() maxToolCalls / maxToolCallsPerTurn (#964)', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the aimock exemption in the header comment.

This spec exercises a fully synthetic in-process adapter (createFatParallelAdapter in api.max-tool-calls-wire.ts) that never reaches a real LLM provider, so it's exempt from the aimock-wiring policy — but that rationale isn't stated here.

Based on learnings, "if the E2E spec only tests runtime/infrastructure behavior ... and the code path under test never reaches the provider HTTP layer, do not add aimock wiring ... document the policy exception in the spec's header comment".

📝 Suggested header addition
 /**
  * Regression for issue `#964`: maxIterations counts model turns, not tool calls.
  * maxToolCalls + maxToolCallsPerTurn must bound fan-out from a single fat turn.
+ *
+ * Uses a synthetic in-process adapter (no real provider HTTP call), so aimock
+ * wiring is not applicable here.
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { expect, test } from '@playwright/test'
/**
* Regression for issue #964: maxIterations counts model turns, not tool calls.
* maxToolCalls + maxToolCallsPerTurn must bound fan-out from a single fat turn.
*/
test.describe('chat() maxToolCalls / maxToolCallsPerTurn (#964)', () => {
import { expect, test } from '`@playwright/test`'
/**
* Regression for issue `#964`: maxIterations counts model turns, not tool calls.
* maxToolCalls + maxToolCallsPerTurn must bound fan-out from a single fat turn.
*
* Uses a synthetic in-process adapter (no real provider HTTP call), so aimock
* wiring is not applicable here.
*/
test.describe('chat() maxToolCalls / maxToolCallsPerTurn (`#964`)', () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/e2e/tests/max-tool-calls.spec.ts` around lines 1 - 7, Update the
header comment in the maxToolCalls/maxToolCallsPerTurn spec to explicitly state
that it uses the fully synthetic in-process createFatParallelAdapter, never
reaches a real LLM provider, and is therefore exempt from aimock wiring under
the policy for runtime/infrastructure-only tests.

Source: Learnings

tombeckenham and others added 2 commits July 20, 2026 16:14
Apply maxToolCallsPerTurn to pending/resume batches, reject negative caps,
dedupe toolCallCount on wait→resume, and lock skipped-emission counting with
unit tests. Clarify post-turn / emitted-budget docs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/ai/tests/chat.test.ts (1)

1302-1341: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate skip-filter assertion logic across tests.

The chunks.filter(...) block checking for TOOL_CALL_RESULT events whose content includes 'exceeded maxToolCallsPerTurn' is repeated near-verbatim at Lines 1330-1337 and 1422-1429 (and likely elsewhere per the earlier maxToolCallsPerTurn tests). Extracting a small helper (e.g. filterSkippedBudgetResults(chunks)) would reduce the risk of these assertions drifting out of sync if the skip-error wording ever changes.

♻️ Proposed helper extraction
+function filterSkippedBudgetResults(chunks: Array<StreamChunk>) {
+  return chunks.filter((c) => {
+    if (c.type !== 'TOOL_CALL_RESULT') return false
+    const content = (c as { content?: unknown }).content
+    return (
+      typeof content === 'string' &&
+      content.includes('exceeded maxToolCallsPerTurn')
+    )
+  })
+}

Then in each test:

-      const skipped = chunks.filter((c) => {
-        if (c.type !== 'TOOL_CALL_RESULT') return false
-        const content = (c as { content?: unknown }).content
-        return (
-          typeof content === 'string' &&
-          content.includes('exceeded maxToolCallsPerTurn')
-        )
-      })
+      const skipped = filterSkippedBudgetResults(chunks)

Also applies to: 1422-1429

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/tests/chat.test.ts` around lines 1302 - 1341, Extract the
repeated TOOL_CALL_RESULT filtering logic into a shared helper such as
filterSkippedBudgetResults near the related test utilities, matching content
containing “exceeded maxToolCallsPerTurn”. Replace the inline chunks.filter
assertions in the maxToolCallsPerTurn tests, including the cases around both
referenced sections, with this helper while preserving their existing length
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/ai/tests/chat.test.ts`:
- Around line 1302-1341: Extract the repeated TOOL_CALL_RESULT filtering logic
into a shared helper such as filterSkippedBudgetResults near the related test
utilities, matching content containing “exceeded maxToolCallsPerTurn”. Replace
the inline chunks.filter assertions in the maxToolCallsPerTurn tests, including
the cases around both referenced sections, with this helper while preserving
their existing length assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46b84fcb-140a-4c14-a409-32a1a0c76420

📥 Commits

Reviewing files that changed from the base of the PR and between 68d8565 and f9858a6.

📒 Files selected for processing (7)
  • docs/api/ai.md
  • docs/chat/agentic-cycle.md
  • packages/ai/src/activities/chat/agent-loop-strategies.ts
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/src/types.ts
  • packages/ai/tests/chat.test.ts
  • testing/e2e/tests/max-tool-calls.spec.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • testing/e2e/tests/max-tool-calls.spec.ts
  • packages/ai/src/types.ts
  • docs/api/ai.md
  • packages/ai/src/activities/chat/agent-loop-strategies.ts
  • docs/chat/agentic-cycle.md
  • packages/ai/src/activities/chat/index.ts

@tombeckenham
tombeckenham requested a review from a team July 20, 2026 06:44
@AlemTuzlak
AlemTuzlak merged commit 3e1b510 into main Jul 20, 2026
10 checks passed
@AlemTuzlak
AlemTuzlak deleted the 964-chat-maxiterations-doesnt-bound-tool-calls-one-turn-can-fan-out-unbounded-parallel-calls branch July 20, 2026 08:58
@github-actions github-actions Bot mentioned this pull request Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chat(): maxIterations doesn't bound tool calls — one turn can fan out unbounded parallel calls

2 participants