Skip to content

fix: pass client tool schemas during tool continuation#882

Merged
whoiskatrin merged 4 commits into
cloudflare:mainfrom
alexanderjacobsen:fix/multi-step-tool-resolution
Feb 11, 2026
Merged

fix: pass client tool schemas during tool continuation#882
whoiskatrin merged 4 commits into
cloudflare:mainfrom
alexanderjacobsen:fix/multi-step-tool-resolution

Conversation

@alexanderjacobsen

@alexanderjacobsen alexanderjacobsen commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix multi-step client tool calling: when autoContinueAfterToolResult triggers a server continuation, the onChatMessage callback was called without clientTools, causing the LLM to fail with tool-output-error when trying to call another client tool
  • Store client tool schemas from the initial request (_lastClientTools) and pass them during continuation
  • Add toolResolutionTrigger on the client side to handle race conditions where new tool calls arrive while the resolution effect is busy

Test plan

  • Configure a chat agent with autoContinueAfterToolResult: true and multiple client-side tools
  • Send a prompt that triggers multiple sequential client tool calls (e.g. "change background to green then black")
  • Verify all tool calls execute successfully without tool-output-error
  • Verify single tool calls still work correctly
  • Verify server-side tools are unaffected

When a client tool result triggers server auto-continuation via
`autoContinueAfterToolResult`, the `onChatMessage` callback was called
without `clientTools`. This caused the LLM continuation to fail when
trying to call another client tool, since the tool schemas were missing
from the `streamText` call, resulting in `tool-output-error`.

Fix by storing client tool schemas from the initial chat request on the
agent instance (`_lastClientTools`) and passing them to `onChatMessage`
during tool continuations.

Also add a `toolResolutionTrigger` state counter on the client side to
force the tool resolution effect to re-run after completing a batch.
This handles the race condition where new tool calls arrive via server
auto-continuation while `isResolvingToolsRef` is true, preventing the
effect from picking them up.
@changeset-bot

changeset-bot Bot commented Feb 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d39ab84

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@cloudflare/ai-chat Patch
agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@whoiskatrin

Copy link
Copy Markdown
Contributor

/bonk review this PR

@pkg-pr-new

pkg-pr-new Bot commented Feb 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@882

commit: 4ed8f0b

@ask-bonk ask-bonk 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.

Good fix overall — the root cause analysis is correct and both changes address real bugs. The server-side _lastClientTools fix is the core of the PR and directly resolves the tool-output-error on continuation. The client-side toolResolutionTrigger is a clean solution to the ref-not-triggering-rerender problem.

A few things to consider:

1. _lastClientTools is never cleared

When chat is cleared via CF_AGENT_CHAT_CLEAR (line 424), many internal state fields are reset (_activeStreamId, _activeRequestId, _streamChunkIndex, messages, etc.), but _lastClientTools is not. This means after a chat clear, a subsequent request that omits clientTools will still have the stale schemas available for continuations. Additionally, if a client sends a request without client tools (e.g. tools were removed from the config), the conditional if (clientTools && clientTools.length > 0) means the old schemas persist. Consider resetting _lastClientTools on chat clear and also handling the case where a new request arrives with no client tools:

// On chat clear (line 424 block):
this._lastClientTools = undefined;

// On chat request:
this._lastClientTools = clientTools?.length ? clientTools : undefined;

2. Multi-connection staleness

Since AIChatAgent is a Durable Object, multiple WebSocket connections share the same _lastClientTools. If connection A sends tools [foo, bar] and connection B sends tools [baz], a continuation triggered by connection A would use connection B's schemas. This is a pre-existing architectural concern (not introduced by this PR), but worth noting since _lastClientTools makes it more visible. A per-connection map would be more correct, though may be overkill for now.

3. Changeset needed

The changeset bot flagged this — since this is a bug fix in @cloudflare/ai-chat, a patch changeset should be added.

The toolResolutionTrigger approach on the client side is clean. Using a state counter to force re-render after the ref-based guard is reset is a well-established React pattern. No issues there.

Comment thread packages/ai-chat/src/index.ts Outdated
// Store client tools for use during tool continuations
if (clientTools && clientTools.length > 0) {
this._lastClientTools = clientTools;
}

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.

_lastClientTools is only ever set, never cleared. This means:

  1. If a subsequent chat request arrives with no clientTools (e.g. the client removed tools from its config), the stale schemas from a previous request will persist and be used during continuations.
  2. When chat is cleared (CF_AGENT_CHAT_CLEAR handler at line 424), _lastClientTools is not reset alongside _activeStreamId, messages, etc.

Consider:

Suggested change
}
// Store client tools for use during tool continuations
this._lastClientTools = clientTools?.length ? clientTools : undefined;

And in the CF_AGENT_CHAT_CLEAR handler, add this._lastClientTools = undefined; alongside the other resets.

@cloudflare cloudflare deleted a comment from ask-bonk Bot Feb 10, 2026
@whoiskatrin

Copy link
Copy Markdown
Contributor

@alexanderjacobsen can you please add the changeset and address valid bonk review comments?

- Clear _lastClientTools when client sends no tools (instead of keeping stale schemas)
- Reset _lastClientTools on chat clear alongside other state
- Add patch changeset for @cloudflare/ai-chat
@alexanderjacobsen

Copy link
Copy Markdown
Contributor Author

@whoiskatrin Should be done now

abortSignal
abortSignal,
clientTools: this._lastClientTools
}

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.

This is the core fix and it looks correct. One thing I notice: the initial request at line 403-410 also passes body (the custom body fields) to onChatMessage, but the continuation here only passes abortSignal and clientTools. If a user's onChatMessage implementation relies on options.body for model selection, temperature, or other custom metadata, those would be lost during continuation.

This is arguably a pre-existing issue (the body was never passed during continuation even before this PR), but since you're already storing _lastClientTools for this purpose, it might be worth also storing the custom body and forwarding it here for consistency. Not a blocker — just flagging it as a follow-up consideration.

Suggested change
}
abortSignal,
clientTools: this._lastClientTools,
// TODO: consider also forwarding stored custom body for continuation parity

Comment thread packages/ai-chat/src/react.tsx
Comment thread packages/ai-chat/src/index.ts
@ask-bonk

ask-bonk Bot commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's a summary of what I did:

PR #882 Review — fix: pass client tool schemas during tool continuation

I posted a top-level review summary and 3 inline comments:

  1. index.ts:538 (continuation clientTools fix) — Confirmed the core fix is correct. Suggested a follow-up: the initial request also passes options.body (custom body fields) to onChatMessage, but the continuation omits it. Worth storing and forwarding for parity.

  2. react.tsx:818 (toolResolutionTrigger in finally) — Noted this is a clean pattern. Minor observation that the unconditional trigger causes one extra no-op effect run per cycle, which is negligible but worth noting.

  3. index.ts:353 (_lastClientTools storage) — Confirmed the previous review feedback was addressed: the clientTools?.length ? ... : undefined guard correctly prevents stale schemas from persisting.

github run

@cloudflare cloudflare deleted a comment from ask-bonk Bot Feb 11, 2026
ask-bonk[bot]

This comment was marked as outdated.

@ask-bonk

This comment was marked as duplicate.

@whoiskatrin

Copy link
Copy Markdown
Contributor

@alexanderjacobsen can we add some tests for this? otherwise happy to ship in the next release

- Test that client tools are passed to onChatMessage during auto-continuation
- Test that stored client tools are cleared on chat clear
- Test that stored client tools are cleared when a new request has no tools
@alexanderjacobsen

Copy link
Copy Markdown
Contributor Author

@whoiskatrin Added tests now

@whoiskatrin
whoiskatrin merged commit 584cebe into cloudflare:main Feb 11, 2026
5 checks passed
@github-actions github-actions Bot mentioned this pull request Feb 11, 2026
alexanderjacobsen added a commit to alexanderjacobsen/agents that referenced this pull request Feb 12, 2026
…fter reconnect

Client tools are sent with each prompt, but _lastClientTools is
in-memory only and lost on DO restart. This breaks tool confirmation
flows: if the user refreshes before approving a tool, the continuation
calls onChatMessage without tool schemas and the AI says it doesn't
have the tools.

Fix: include clientTools in the CF_AGENT_TOOL_RESULT message. The
client is the source of truth for its tools and always has current
schemas, so re-sending them is both simpler and more correct than
persisting server-side.

Related to cloudflare#882.
alexanderjacobsen added a commit to alexanderjacobsen/agents that referenced this pull request Feb 12, 2026
…fter reconnect

Client tools are sent with each prompt, but _lastClientTools is
in-memory only and lost on DO restart. This breaks tool confirmation
flows: if the user refreshes before approving a tool, the continuation
calls onChatMessage without tool schemas and the AI says it doesn't
have the tools.

Fix: include clientTools in the CF_AGENT_TOOL_RESULT message. The
client is the source of truth for its tools and always has current
schemas, so re-sending them is both simpler and more correct than
persisting server-side.

Related to cloudflare#882.
whoiskatrin pushed a commit that referenced this pull request Feb 13, 2026
…fter reconnect (#897)

Client tools are sent with each prompt, but _lastClientTools is
in-memory only and lost on DO restart. This breaks tool confirmation
flows: if the user refreshes before approving a tool, the continuation
calls onChatMessage without tool schemas and the AI says it doesn't
have the tools.

Fix: include clientTools in the CF_AGENT_TOOL_RESULT message. The
client is the source of truth for its tools and always has current
schemas, so re-sending them is both simpler and more correct than
persisting server-side.

Related to #882.
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.

4 participants