Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

クライアントツール

最終更新 Markdown で表示Agent セットアップ

Think は、ブラウザーで実行するツールをサポートします。クライアントはシリアライズ可能なツールスキーマをチャットリクエストボディで送り、Think がサーバーツールとマージします。LLM がクライアントツールを呼ぶと、その呼び出しはクライアントへ送られ、そこで実行されます。

クライアントツールの定義

動的なクライアント側ツールでは、useAgentChattools を渡します。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 やプラットフォームで、利用可能なツールの集合をランタイムにブラウザーが決める場合です。

サブエージェント RPC の chat() 経路でのクライアントツール

親エージェントが、ブラウザーの 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 が退避されると、チャット復旧は孤立した呼び出しをサーバーツールと同様にエラー扱いし、モデルは先へ進みます。きれいに再実行するには、親が clientToolsonClientToolCall を付けて 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 クライアントへメッセージ更新を配信します。再接続しなくても、ブラウザークライアントは同期されたままです。

役に立ちましたか?