fix(chat): stop resending server-executed tool turns - #7173
Open
Haroenv wants to merge 1 commit into
Open
Conversation
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>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
TIP This summary will be updated as you push new changes.
Contributor
There was a problem hiding this comment.
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.
More templates
algoliasearch-helper
instantsearch-ui-components
instantsearch.css
instantsearch.js
react-instantsearch
react-instantsearch-core
react-instantsearch-nextjs
react-instantsearch-router-nextjs
vue-instantsearch
commit: |
|
Size Change: +202 B (+0.02%) Total Size: 1.23 MB 📦 View Changed
ℹ️ View Unchanged
|
Haroenv
marked this pull request as ready for review
August 18, 2026 19:29
Haroenv
requested review from
a team,
FabienMotte and
afrencalg
and removed request for
a team
August 18, 2026 19:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:ai-litebooks the call as client-owned, because the flag is the only signal it has (abstract-chat.ts,tool-input-available) andconnectChatalways passes anonToolCall(the widget always registers display-only tools for the built-inalgolia_*names).tool-output-availablemarks it resolved.continueResponsethen sees a response whose required tool calls are all resolved, and the defaultsendAutomaticallyWhen—lastAssistantMessageIsCompleteWithToolCalls, which only looks at tool-part states and never at whether the assistant already emitted its closing text — says "continue".messages[messages.length - 1].role === "assistant". The backend 422s it and the widget renders the rejection viamessagesErrorComponent, which is why it reads as a UI bug rather than an extra round-trip.toolParts.length === 0short-circuits the predicate tofalse, which is why plain"hi"turns are clean.The fix
A tool call the server answers itself is server-owned, regardless of the
providerExecutedflag: 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 fromrequiredToolCallIdsinstead of only adding it toresolvedToolCallIds. A turn made only of server-executed calls then hits the existingrequiredToolCallIds.size === 0guard and never reaches the predicate.Client-registered tools are unaffected: both the
onToolCallflow and the manualaddToolResult-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:
algolia_*tool constants by prefix — misses customer server-side tools (MCP tools, future built-ins) and needs a release for every new name.falsewhen the turn already ends in atextpart withstate: "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: truefor 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 oflastAssistantMessageIsCompleteWithToolCalls: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: truetool part withstate: 'output-available'followed by atextpart withstate: 'done'.We never picked that up. It shipped in
ai@6.0.0-beta.92, thenai@6.0.0, and was not backported to 5.x — and #6880 replacedai@^5.0.18withai-lite, reimplementing the v5-era predicate. So our copy is missing both theproviderExecutedfilter 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-responserequiredToolCallIdsbookkeeping 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
does not auto-continue when the server answers its own tool call— the Agent Studio stream shape (call → output → closing text → finish, noproviderExecuted). One request instead of two. Fails onmaster.still auto-continues for the client tool call in a mixed turn.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-checkreports 95 pre-existing errors from a staledistonmastertoo (identical count with and without this change).No new common-suite tests: the fix is in the single shared
ai-litecore 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)
providerExecutedis absent,connectChat'sonToolCallalso 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.🤖 Generated with Claude Code