Think は、ブラウザーで実行するツールをサポートします。クライアントはシリアライズ可能なツールスキーマをチャットリクエストボディで送り、Think がサーバーツールとマージします。LLM がクライアントツールを呼ぶと、その呼び出しはクライアントへ送られ、そこで実行されます。
動的なクライアント側ツールでは、useAgentChat に tools を渡します。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 関数がなく、スキーマだけのツールです。LLM がそのツール呼び出しを出すと、Think はクライアントへ振り分けます。
ほとんどのアプリでは、ツールはサーバーで定義し、ブラウザー専用の実行には onToolCall を使う方がよいです。tools オプションが特に向くのは、SDK やプラットフォームで、利用可能なツールの集合をランタイムにブラウザーが決める場合です。
親エージェントが、ブラウザーの WebSocket ではなく RPC の chat() で Think サブエージェントへ委任する場合、clientTools を運んだりツール結果を返したりする WebSocket はありません。代わりに 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は、そのターンのツールスキーマを登録します。WebSocket のclientToolsフィールドと同じです。onClientToolCallはクライアントツール呼び出しを実行し、出力を返します。モデルはクライアントツールを呼び、結果を受け取り、同じchat()呼び出しの中で続けられます。
onClientToolCall を省略すると、ツールは登録されますが結果がありません。モデルの呼び出しはストリームコールバックに現れ、ターンは未完了のツール呼び出しで終わります(RPC のストリームコールバックには、結果を返す経路がありません)。往復を完了させたいときは、必ず onClientToolCall を渡します。
- 復旧: スキーマと
onClientToolCallの実行関数はターン単位のみで、永続化されません(実行関数は isolate とともに消えるライブな RPC 参照です。WebSocket 経路と違い、退避後にtool-resultを再送するクライアントはありません)。クライアントツール呼び出しの途中で isolate が退避されると、チャット復旧は孤立した呼び出しをサーバーツールと同様にエラー扱いし、モデルは先へ進みます。きれいに再実行するには、親が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 でモデルやツール選択を調整するときに使えます。
1 つのターンで複数のクライアントツール呼び出しが出た場合、Think は結果ごとに継続を始めず、すべて の結果を待ってから継続を 1 回だけ始めます。継続がすでに保留中のときに届いた即時再開リクエストは、重複を始めず、その保留中の継続に接続します。サーバー側の needsApproval 継続は、承認が記録されると確実に再開します。
Durable Object はいつでも退避されます。承認プロンプトやクライアント側ツール呼び出しでターンが一時停止しているときも同様です。Think の耐久復旧 は常に有効です。SDK は、こうしたターンをスタックではなく、人間待ちとして扱います。失敗させず、ターンを保留します。ユーザーの承認またはツール結果が届くと、会話が再開します。
復旧バジェットの対象外になる操作は、人間待ちのターンは確定されない を参照してください。
messageConcurrency プロパティは、チャットターン実行中にユーザー送信が重なったときの振る舞いを制御します。
| 戦略 | 振る舞い |
|---|---|
"queue" |
すべての送信をキューに入れ、順に処理します。デフォルトです。 |
"latest" |
重なった送信のうち最新だけを残します。置き換えられた送信もユーザーメッセージは永続化しますが、モデルターンは開始しません |
"merge" |
重なった送信をキューに入れ、末尾のユーザーメッセージを 1 つの結合ターンにまとめてから、キューの最新ターンを実行します |
"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 クライアントへストリーミング応答を配信します。同じエージェントに複数のブラウザータブが接続している場合、すべてのタブがリアルタイムでストリームを見ます。ツール呼び出しの状態(pending、result、approval)も全タブへ配信されます。
プログラムからの chat() ターンと clearMessages() も、接続中の useAgentChat クライアントへメッセージ更新を配信します。再接続しなくても、ブラウザークライアントは同期されたままです。