Think 支持在浏览器中执行的工具。客户端在聊天请求 body 中发送可序列化的工具 schema,Think 将其与服务端工具合并;当 LLM 调用客户端工具时,调用会路由到客户端执行。
对于动态客户端工具,将 tools 传给 useAgentChat。带 execute 函数的工具会作为客户端执行工具注册到服务端:
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();
},
},
},
});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();
},
},
},
});客户端工具是服务端上没有 execute 的工具——它们只有 schema。当 LLM 为其产生工具调用时,Think 将其路由到客户端。
大多数应用应在服务端定义工具,并用 onToolCall 处理仅浏览器可执行的逻辑。tools 选项最适合浏览器在运行时决定可用工具表面的 SDK 或平台。
当父 agent 通过 RPC 用 chat() 委托给 Think 子 agent(而非浏览器 WebSocket)时,没有 WebSocket 承载 clientTools 或回传工具结果。改为通过 ChatOptions 传递:
await child.chat(message, callback, {
signal,
clientTools: [
{
name: "get_user_timezone",
description: "Get the caller's timezone",
parameters: { type: "object" },
},
],
onClientToolCall: async ({ toolName, input }) => {
// Run the client tool wherever the parent can — return its output.
return runClientTool(toolName, input);
},
});await child.chat(message, callback, {
signal,
clientTools: [
{
name: "get_user_timezone",
description: "Get the caller's timezone",
parameters: { type: "object" },
},
],
onClientToolCall: async ({ toolName, input }) => {
// Run the client tool wherever the parent can — return its output.
return runClientTool(toolName, input);
},
});clientTools为该轮注册工具 schema,与 WebSocketclientTools字段相同。onClientToolCall执行客户端工具调用并返回输出。模型可调用客户端工具、接收结果并继续——全部在单次chat()调用内完成。
若省略 onClientToolCall,工具会注册但没有结果:模型的调用通过流回调浮现,轮次以悬空工具调用结束(RPC 流回调本身没有入站结果通道)。若希望往返完成,请提供 onClientToolCall。
- 恢复: schema 与
onClientToolCall执行器仅每轮次有效,永不持久化(执行器是随 isolate 消亡的实时 RPC 引用;与 WebSocket 路径不同,驱逐后没有客户端重放tool-result)。若驱逐在客户端工具调用进行中打断轮次,聊天恢复会将孤立调用标记为错误(与服务端工具相同),模型继续。要干净重跑,父级需再次用clientTools与onClientToolCall调用chat()。 - 错误: 若
onClientToolCall抛出,失败会以工具错误(output-error)浮现给模型,轮次继续——不会崩溃。 - 序列化:
onClientToolCall的返回值成为工具输出,必须是 JSON 可序列化(经 RPC 传回并进入模型上下文)。 - 无审批门控: RPC 客户端工具通过
onClientToolCall立即执行。WebSocket 审批流程(needsApproval)不适用于此路径——如需门控,在执行器内实现。 - 名称优先级: 客户端工具在服务端工具之后合并,因此与服务端工具同名的客户端工具(例如工作区工具)会覆盖该轮——与 WebSocket 路径相同。
- 中止: 通过
signal中止轮次会停止循环,但进行中的onClientToolCall本身不会被取消;当前调用兑现后轮次结束。
在客户端上用 onToolCall 处理浏览器端工具执行:
useAgentChat({
agent,
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "read") {
const result = await readFromBrowser(toolCall.input);
addToolOutput({
toolCallId: toolCall.toolCallId,
output: result,
});
}
},
});useAgentChat({
agent,
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "read") {
const result = await readFromBrowser(toolCall.input);
addToolOutput({
toolCallId: toolCall.toolCallId,
output: result,
});
}
},
});收到客户端工具结果后,Think 无需新用户消息即自动继续对话。续传轮次的 TurnContext 中 continuation: true,可在 beforeTurn 中调整模型或工具选择。
当一轮产生多个客户端工具调用时,Think 会等待所有结果再开始单次续传,而不是每个结果各开一次。若续传已挂起时收到立即恢复请求,会挂接到挂起的续传而非重复启动;服务端 needsApproval 续传在审批记录后可靠恢复。
Durable Object 随时可能被驱逐,包括在轮次暂停于审批提示或客户端工具调用时。由于 Think 默认启用 chatRecovery,SDK 将此类轮次视为等待人类而非卡住。它会停放轮次而非失败,用户最终审批或工具结果会恢复对话。
哪些交互免除恢复预算,请参阅 等待人类的轮次不会被封存。
messageConcurrency 属性控制聊天轮次已活跃时重叠用户提交的行为。
| 策略 | 行为 |
|---|---|
"queue" |
排队每个提交并按顺序处理。默认。 |
"latest" |
仅保留最新重叠提交;被取代的提交仍持久化用户消息但不启动模型轮次 |
"merge" |
排队重叠提交,然后在最新排队轮次运行前将其尾部用户消息合并为一次合并轮次 |
"drop" |
完全忽略重叠提交。消息不持久化。 |
{ strategy: "debounce", debounceMs?: number } |
尾沿取最新,带静默窗口(默认 750ms)。 |
import { Think } from "@cloudflare/think";
export class SearchAgent extends Think {
messageConcurrency = "latest";
getModel() {
/* ... */
}
}import { Think } from "@cloudflare/think";
import type { MessageConcurrency } from "@cloudflare/think";
export class SearchAgent extends Think<Env> {
override messageConcurrency: MessageConcurrency = "latest";
getModel() {
/* ... */
}
}Think 向所有已连接 WebSocket 客户端广播流式响应。多个浏览器标签连接到同一 agent 时,所有标签实时看到流式响应。工具调用状态(待处理、结果、审批)广播到所有标签。
编程式 chat() 轮次与 clearMessages() 也会向已连接 useAgentChat 客户端广播消息更新,浏览器客户端无需重连即可保持同步。