Skip to content

fix(chat): stop resending server-executed tool turns - #7173

Open
Haroenv wants to merge 1 commit into
masterfrom
fix/chat-server-executed-tool-resend
Open

fix(chat): stop resending server-executed tool turns#7173
Haroenv wants to merge 1 commit into
masterfrom
fix/chat-server-executed-tool-resend

Conversation

@Haroenv

@Haroenv Haroenv commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Every tool-using turn on a stock <Chat agentId={...} /> fires a second completions request that the backend rejects, so the widget renders an error under an otherwise correct answer. Reported by Chuck Meyer; tracked as FX-3900, and FX-3931 ("duplicate conclusion text after a tool call") is the same bug seen before the backend started rejecting the shape.

Why it happens. Agent Studio runs its tools server-side and streams the tool call, its output and the closing text in one turn — without flagging the call providerExecuted. So:

  1. ai-lite books the call as client-owned, because the flag is the only signal it has (abstract-chat.ts, tool-input-available) and connectChat always passes an onToolCall (the widget always registers display-only tools for the built-in algolia_* names).
  2. The server's own tool-output-available marks it resolved.
  3. continueResponse then sees a response whose required tool calls are all resolved, and the default sendAutomaticallyWhenlastAssistantMessageIsCompleteWithToolCalls, which only looks at tool-part states and never at whether the assistant already emitted its closing text — says "continue".
  4. We POST a conversation that is already complete, with messages[messages.length - 1].role === "assistant". The backend 422s it and the widget renders the rejection via messagesErrorComponent, which is why it reads as a UI bug rather than an extra round-trip.

toolParts.length === 0 short-circuits the predicate to false, which is why plain "hi" turns are clean.

The fix

A tool call the server answers itself is server-owned, regardless of the providerExecuted flag: nothing is left for the client to submit, so the response stops requiring a result for it, and the turn no longer auto-continues on the server's behalf.

That is one line in acceptServerToolResult — drop the call from requiredToolCallIds instead of only adding it to resolvedToolCallIds. A turn made only of server-executed calls then hits the existing requiredToolCallIds.size === 0 guard and never reaches the predicate.

Client-registered tools are unaffected: both the onToolCall flow and the manual addToolResult-from-the-layout flow still continue the turn, including in a turn that mixes server- and client-executed calls (covered by a new test).

I preferred this over the two alternatives discussed:

  • Allowlisting the algolia_* tool constants by prefix — misses customer server-side tools (MCP tools, future built-ins) and needs a release for every new name.
  • Returning false when the turn already ends in a text part with state: "done" — a reasonable belt-and-braces check, but it infers completeness from part ordering rather than from who owns the call. Happy to add it on top if reviewers want the extra guard.

The real root cause is upstream of us: if Agent Studio streamed providerExecuted: true for its server-executed tools, this would fix every already-released version of the widget with no release at all. This PR makes the widget robust when it doesn't. Thread with the Agent Studio folks is in #proj-agent-studio.

How this relates to the AI SDK

The AI SDK fixed the neighbouring half of this in vercel/ai#9944 (97b1d77, 2025-10-31) — "Don't resend messages for providerExecuted tools" — by filtering provider-executed parts out of lastAssistantMessageIsCompleteWithToolCalls:

   const lastStepToolInvocations = message.parts
     .slice(lastStepStartIndex + 1)
-    .filter(isToolOrDynamicToolUIPart);
+    .filter(isToolOrDynamicToolUIPart)
+    .filter(part => !part.providerExecuted);

Their regression test is exactly the shape from this report: a providerExecuted: true tool part with state: 'output-available' followed by a text part with state: 'done'.

We never picked that up. It shipped in ai@6.0.0-beta.92, then ai@6.0.0, and was not backported to 5.x — and #6880 replaced ai@^5.0.18 with ai-lite, reimplementing the v5-era predicate. So our copy is missing both the providerExecuted filter and upstream's last-step (step-start) scoping. That drift is real but separate, and it is being closed in its own PR.

Note that adopting upstream's filter would not fix this bug: it trusts a flag Agent Studio never sets. Upstream also gates its finish-time auto-send on the predicate alone (chat.ts, if (!isError && await this.shouldSendAutomatically())) and has no equivalent of the per-response requiredToolCallIds bookkeeping our fork grew — which is why there is a more precise place to fix it here than in the predicate.

Behavior change to review

When the server streams an output (or an error) for a call the client also owns, the turn no longer auto-continues — previously it did. One existing test asserted that continuation (settles an awaited client result after a server result for the same call); its expectations are updated. This is deliberate: whoever answered the call, the server had the tool output in hand when it finished the turn, so continuation is its decision to make, not ours. It does mean a client tool whose input the server errors no longer gets a client-driven recovery round.

Testing

  • New: does not auto-continue when the server answers its own tool call — the Agent Studio stream shape (call → output → closing text → finish, no providerExecuted). One request instead of two. Fails on master.
  • New: still auto-continues for the client tool call in a mixed turn.
  • Updated: settles an awaited client result after a server result for the same call.
  • yarn jest packages/instantsearch.js/src/lib/ai-lite packages/instantsearch.js/src/connectors/chat — 227 passing.
  • yarn jest common-widgets -t "Chat widget common tests", chat widget suites (JS + React), yarn lint:changed, oxfmt — clean. yarn type-check reports 95 pre-existing errors from a stale dist on master too (identical count with and without this change).

No new common-suite tests: the fix is in the single shared ai-lite core that all three flavors consume, and the common chat suites drive messages directly rather than through a transport stream.

Follow-ups (not in this PR)

  • While providerExecuted is absent, connectChat's onToolCall also runs for server-executed calls. For a tool name that isn't registered it throws in __DEV__, and in production it submits "No tool implemented for <name>" as the tool output — which then wins over the server's real output for that call. Any customer server-side tool the widget doesn't know about hits this.
  • Nothing caps consecutive auto-continues. While building the repro, a stream that kept answering with tool-shaped turns looped until Node ran out of heap; in production only the backend rejection stops it.

🤖 Generated with Claude Code

Agent Studio runs its tools server-side and streams the tool call, its
output and the closing text in a single turn, without flagging the call
`providerExecuted`. `ai-lite` therefore booked those calls as
client-owned, the server's own `tool-output-available` marked them
resolved, and the default `sendAutomaticallyWhen`
(`lastAssistantMessageIsCompleteWithToolCalls`) then resent a
conversation that was already complete — a POST whose last message is
`role: "assistant"`. The backend rejects that shape, which the widget
renders through `messagesErrorComponent`; before it did, the resend
answered and the conclusion rendered twice.

A tool call the server answers itself is server-owned regardless of the
`providerExecuted` flag: there is nothing for the client to submit, so
the response no longer requires a result for it and no longer
auto-continues on the server's behalf. Client-registered tools are
unaffected — both the `onToolCall` and the manual `addToolResult` flows
still continue the turn, including in a turn that mixes server- and
client-executed calls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

View in Codacy

TIP This summary will be updated as you push new changes.

Copilot AI 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.

Pull request overview

Prevents Chat from resending completed server-executed tool turns by removing server-resolved calls from client-required tool tracking.

Changes:

  • Marks streamed server tool results as no longer client-required.
  • Adds coverage for server-only, mixed-ownership, and competing-result turns.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
packages/instantsearch.js/src/lib/ai-lite/abstract-chat.ts Prevents automatic continuation for server-resolved calls.
packages/instantsearch.js/src/lib/ai-lite/__tests__/abstract-chat.test.ts Tests server and mixed tool-call ownership behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@pkg-pr-new

pkg-pr-new Bot commented Aug 18, 2026

Copy link
Copy Markdown
More templates

algoliasearch-helper

npm i https://pkg.pr.new/algolia/instantsearch/algoliasearch-helper@7173

instantsearch-ui-components

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch-ui-components@7173

instantsearch.css

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.css@7173

instantsearch.js

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.js@7173

react-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch@7173

react-instantsearch-core

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-core@7173

react-instantsearch-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-nextjs@7173

react-instantsearch-router-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-router-nextjs@7173

vue-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/vue-instantsearch@7173

commit: ae6ae82

@github-actions

Copy link
Copy Markdown

Size Change: +202 B (+0.02%)

Total Size: 1.23 MB

📦 View Changed
Filename Size Change
packages/instantsearch.js/dist/instantsearch.development.js 301 kB +202 B (+0.07%)
ℹ️ View Unchanged
Filename Size Change
packages/algolia-experiences/dist/algolia-experiences.development.js 179 kB 0 B
packages/algolia-experiences/dist/algolia-experiences.production.min.js 81.4 kB -1 B (0%)
packages/algoliasearch-helper/dist/algoliasearch.helper.js 44.3 kB 0 B
packages/algoliasearch-helper/dist/algoliasearch.helper.min.js 13.9 kB 0 B
packages/instantsearch.css/components/ai-mode-button.css 1.75 kB 0 B
packages/instantsearch.css/components/autocomplete-min.css 4.31 kB 0 B
packages/instantsearch.css/components/autocomplete.css 4.66 kB 0 B
packages/instantsearch.css/components/button.css 1.96 kB 0 B
packages/instantsearch.css/components/chat-min.css 6.33 kB 0 B
packages/instantsearch.css/components/chat.css 6.77 kB 0 B
packages/instantsearch.css/components/filter-suggestions.css 1.88 kB 0 B
packages/instantsearch.css/themes/algolia-min.css 10.8 kB 0 B
packages/instantsearch.css/themes/algolia.css 11.4 kB 0 B
packages/instantsearch.css/themes/nova-min.css 10.9 kB 0 B
packages/instantsearch.css/themes/nova.css 11.6 kB 0 B
packages/instantsearch.css/themes/reset-min.css 1.3 kB 0 B
packages/instantsearch.css/themes/reset.css 1.38 kB 0 B
packages/instantsearch.css/themes/satellite-min.css 11.6 kB 0 B
packages/instantsearch.css/themes/satellite.css 12.5 kB 0 B
packages/instantsearch.js/dist/instantsearch.production.min.js 142 kB +3 B (0%)
packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js 68 kB +2 B (0%)
packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js 113 kB +1 B (0%)
packages/vue-instantsearch/vue2/cjs/index.js 19.9 kB 0 B
packages/vue-instantsearch/vue2/umd/index.js 73.3 kB -6 B (-0.01%)
packages/vue-instantsearch/vue3/cjs/index.js 20.6 kB 0 B
packages/vue-instantsearch/vue3/umd/index.js 73.7 kB -6 B (-0.01%)

compressed-size-action

@Haroenv
Haroenv marked this pull request as ready for review August 18, 2026 19:29
@Haroenv
Haroenv requested review from a team, FabienMotte and afrencalg and removed request for a team August 18, 2026 19:30
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.

2 participants