Skip to content

Client tools

Think supports tools that execute in the browser. The client sends serializable tool schemas in the chat request body, Think merges them with server tools, and when the LLM calls a client tool, the call is routed to the client for execution.

Defining client tools

For dynamic client-side tools, pass tools to useAgentChat. Tools with an execute function are registered with the server as client-executed tools:

JavaScript
const { messages, sendMessage } = useAgentChat({
agent,
tools: {
getUserTimezone: {
description: "Get the user's timezone from their browser",
parameters: {},
execute: async () => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
},
},
getClipboard: {
description: "Read text from the user's clipboard",
parameters: {},
execute: async () => {
return navigator.clipboard.readText();
},
},
},
});

Client tools are tools without an execute function on the server — they only have a schema. When the LLM produces a tool call for one, Think routes it to the client.

For most apps, prefer defining tools on the server and using onToolCall for browser-only execution. The tools option is most useful for SDKs or platforms where the browser decides the available tool surface at runtime.

Approval flow

Handle browser-side tool execution on the client with onToolCall:

JavaScript
useAgentChat({
agent,
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "read") {
const result = await readFromBrowser(toolCall.input);
addToolOutput({
toolCallId: toolCall.toolCallId,
output: result,
});
}
},
});

Auto-continuation

After a client tool result is received, Think automatically continues the conversation without a new user message. The continuation turn has continuation: true in the TurnContext, which you can use in beforeTurn to adjust model or tool selection.

When a turn produces several client tool calls at once, Think waits for all of their results before starting a single continuation, instead of starting one continuation per result. An immediate resume request that arrives while a continuation is already pending attaches to that pending continuation rather than starting a duplicate, and server-side needsApproval continuations resume reliably once the approval is recorded.

Message concurrency

The messageConcurrency property controls how overlapping user submits behave when a chat turn is already active.

StrategyBehavior
"queue"Queue every submit and process them in order. Default.
"latest"Keep only the latest overlapping submission; superseded submissions still persist their user messages but do not start a model turn
"merge"Queue overlapping submissions, then collapse their trailing user messages into one combined turn before the latest queued turn runs
"drop"Ignore overlapping submits entirely. Messages are not persisted.
{ strategy: "debounce", debounceMs?: number }Trailing-edge latest with a quiet window (default 750ms).
JavaScript
import { Think } from "@cloudflare/think";
export class SearchAgent extends Think {
messageConcurrency = "latest";
getModel() {
/* ... */
}
}

Multi-tab broadcast

Think broadcasts streaming responses to all connected WebSocket clients. When multiple browser tabs are connected to the same agent, all tabs see the streamed response in real time. Tool call states (pending, result, approval) are broadcast to all tabs.

Programmatic chat() turns and clearMessages() also broadcast message updates to connected useAgentChat clients, so browser clients stay in sync without reconnecting.