Think は streamText の呼び出しを担当し、チャットターンの各段階にフックを提供します。フックは入口に関係なく、すべてのターンで発火します。WebSocket チャット、サブエージェントの chat()、saveMessages()、耐久性のある submitMessages() 実行、continueLastTurn()、ツール結果後の自動継続が含まれます。
| フック | 発火タイミング | 戻り値 | 非同期 |
|---|---|---|---|
configureSession(session) |
onStart 中に一度だけ |
Session |
はい |
beforeTurn(ctx) |
streamText の前 |
TurnConfig または void |
はい |
beforeStep(ctx) |
各モデルステップの前 | StepConfig または void |
はい |
beforeToolCall(ctx) |
サーバー側ツールの実行前 | ToolCallDecision または void |
はい |
afterToolCall(ctx) |
ツールの結果が分かったあと | void | はい |
onStepFinish(ctx) |
各ステップ完了後 | void | はい |
onChunk(ctx) |
ストリーミングチャンクごと | void | はい |
onChatResponse(result) |
ターン完了後、メッセージ永続化のあと | void | はい |
onChatError(error, ctx?) |
ターン中のエラー時 | 伝播するエラー | いいえ |
classifyChatError(error, ctx?) |
ターンエラー時で、contextOverflow.reactive が有効なとき |
ChatErrorClassification または void |
いいえ |
ツール呼び出しが 2 回あるターンの場合:
flowchart TD
cfg["configureSession() — 起動時に一度だけ。ターンごとではない"] --> bt["beforeTurn() — コンテキストを確認し、model / tools / prompt を上書き"]
bt --> bs
subgraph loop ["streamText(ステップごとに繰り返し)"]
bs["beforeStep()"] --> chunk["onChunk() — ストリーミングチャンクごと"]
chunk --> btc["beforeToolCall()"]
btc --> exec["ツールを実行"]
exec --> atc["afterToolCall()"]
atc --> sf["onStepFinish()"]
sf -->|"さらにステップがある"| bs
end
sf -->|"ターン完了"| ocr["onChatResponse() — メッセージを永続化し、ターンロックを解除"]
streamText の前に呼ばれます。組み立て済みのコンテキスト(システムプロンプト、変換済みメッセージ、マージ済みツール、モデル)を受け取ります。一部を上書きするには TurnConfig を返し、デフォルトを使う場合は void を返します。
beforeTurn(ctx: TurnContext): TurnConfig | void | Promise<TurnConfig | void>| フィールド | 型 | 説明 |
|---|---|---|
system |
string |
組み立て済みのシステムプロンプト(コンテキストブロックまたは getSystemPrompt() から) |
messages |
ModelMessage[] |
組み立て済みのモデルメッセージ(切り詰め、剪定済み) |
tools |
ToolSet |
マージ済みツールセット(workspace + getTools + session + extensions + MCP + client) |
model |
LanguageModel |
getModel() のモデル |
continuation |
boolean |
継続ターンかどうか(ツール結果後の自動継続) |
body |
Record<string, unknown> |
クライアントリクエストのカスタム body フィールド |
すべてのフィールドは任意です。変更したい項目だけ返します。
| フィールド | 型 | 説明 |
|---|---|---|
model |
LanguageModel |
このターンのモデルを上書きします |
system |
string |
システムプロンプトを上書きします |
messages |
ModelMessage[] |
組み立て済みメッセージを上書きします |
tools |
ToolSet |
追加でマージするツール(加算) |
activeTools |
string[] |
モデルが呼べるツールを制限します |
toolChoice |
ToolChoice |
特定のツール呼び出しを強制します |
maxSteps |
number |
このターンの maxSteps を上書きします |
sendReasoning |
boolean |
このターンの推論チャンクを送信します |
chatStreamStallTimeoutMs |
number |
このターンのストリーム停滞ウォッチドッグを上書きします(0 で無効)。ターン後に自動リセットします。遅いツールがあるターン向けです。耐久リカバリー を参照してください |
output |
Output |
このターンで構造化出力を要求します |
providerOptions |
Record<string, unknown> |
プロバイダー固有のオプション |
experimental_telemetry |
object |
このターンの AI SDK テレメトリ設定 |
experimental_transform |
StreamTextTransform | StreamTextTransform[] |
このターンの AI SDK ストリーム変換です。ストリーム部品の検査や書き換え(例: ツール結果から source 部品を出す)に使います。順に適用されます |
継続ターンでは安いモデルに切り替えます。
beforeTurn(ctx: TurnContext) {
if (ctx.continuation) {
return { model: this.cheapModel };
}
}モデルが呼べるツールを制限します。
beforeTurn(ctx: TurnContext) {
return { activeTools: ["read", "write", "getWeather"] };
}クライアントの body からターンごとのコンテキストを追加します。
beforeTurn(ctx: TurnContext) {
if (ctx.body?.selectedFile) {
return {
system: ctx.system + `\n\nUser is editing: ${ctx.body.selectedFile}`,
};
}
}内部の継続ターンでは推論を隠します。
beforeTurn(ctx: TurnContext) {
if (ctx.continuation) {
return { sendReasoning: false };
}
}ターンで構造化出力を強制します。
import { Output } from "ai";
import { z } from "zod";
const ResultSchema = z.object({ severity: z.enum(["low", "high"]) });
beforeTurn(ctx: TurnContext) {
if (ctx.body?.mode === "structured-answer") {
return {
output: Output.object({ schema: ResultSchema }),
activeTools: [],
};
}
}output はターン単位の設定だけです。AI SDK の prepareStep は output の上書きを受け付けないため、beforeStep で 1 ステップだけ構造化出力を切り替えることはできません。
エージェントループ内の各 AI SDK ステップの前に呼ばれます。Think はこのフックを streamText の prepareStep として転送するため、AI SDK の prepare-step コンテキスト全体を受け取り、ステップ単位の上書きを返せます。ターン全体の組み立ては beforeTurn、ステップ番号や直前のステップ結果に依存する判断は beforeStep を使います。
beforeStep(ctx: PrepareStepContext): StepConfig | void {
if (ctx.stepNumber > 0) {
return { activeTools: [] };
}
}サーバー側ツールの execute 関数が走る前に呼ばれます。Think は各サーバー側ツールをラップし、モデルがツール結果を受け取る前に、呼び出しの許可、変更、ブロック、差し替えができます。
beforeToolCall(ctx: ToolCallContext): ToolCallDecision | void {
if (ctx.toolName === "delete" && this.isReadOnlyMode) {
return { action: "block", reason: "delete is disabled in read-only mode" };
}
if (ctx.toolName === "weather") {
const cached = this.weatherCache.get(JSON.stringify(ctx.input));
if (cached) return { action: "substitute", output: cached };
}
}| フィールド | 型 | 説明 |
|---|---|---|
toolName |
string |
呼び出されるツール名 |
input |
unknown |
モデルが渡した入力 |
toolCallId |
string |
このツール呼び出しの ID |
messages |
ModelMessage[] |
ツール実行時点で見えるメッセージ |
abortSignal |
AbortSignal | undefined |
ターンがキャンセルされたときに中断するシグナル |
実行を制御するには ToolCallDecision を返します。
| 判定 | 動作 |
|---|---|
void または { action: "allow" } |
元の入力で元のツールを実行します |
{ action: "allow", input } |
変更した入力で元のツールを実行します |
{ action: "block", reason } |
元のツールをスキップし、reason をツール結果として返します |
{ action: "substitute", output } |
元のツールをスキップし、output をツール結果として返します |
ラップしたツールが予備結果として AsyncIterable を返す場合、Think は beforeToolCall のあと、イテラブルを最後に yield された値へ畳みます。そのツールから本当の予備ストリーミングが必要な場合は、beforeToolCall で横取りしないでください。
ツールの結果が分かったあとに呼ばれます。実際の実行、ブロックした呼び出し、差し替えた呼び出し、スローされたツールエラーを含みます。
afterToolCall(ctx: ToolCallResultContext) {
if (!ctx.success) return;
this.env.ANALYTICS.writeDataPoint({
blobs: [ctx.toolName],
doubles: [JSON.stringify(ctx.output).length],
});
}| フィールド | 型 | 説明 |
|---|---|---|
toolName |
string |
呼び出されたツール名 |
input |
unknown |
モデルが渡した入力 |
toolCallId |
string |
このツール呼び出しの ID |
messages |
ModelMessage[] |
ツール実行時点で見えるメッセージ |
durationMs |
number |
ツール実行時間(ミリ秒) |
success |
boolean |
モデルが成功したツール結果を受け取ったかどうか |
output |
unknown |
success が true のときに存在します |
error |
unknown |
success が false のときに存在します |
ブロックと差し替えのツール呼び出しでは、モデルが有効なツール結果を受け取るため success は true です。元のツール実行からスローされたエラーだけが success: false になります。
エージェントループ内の各ステップ完了後に呼ばれます。StepContext は AI SDK の step-finish イベントなので、ステップ全体の記録を含みます。生成テキスト、推論、ファイル、ソース、型付きツール呼び出しと結果、使用量、警告、リクエストとレスポンスのメタデータ、プロバイダーメタデータです。
onStepFinish(ctx: StepContext) {
console.log(
`Step ${ctx.stepNumber} (${ctx.finishReason}): ` +
`${ctx.usage.inputTokens}in/${ctx.usage.outputTokens}out`,
);
}| フィールド | 説明 |
|---|---|
stepNumber |
ステップの 0 始まりインデックス |
text |
このステップで生成されたテキスト |
reasoning |
モデルが出した推論部品 |
files |
ステップ中に生成されたファイル |
sources |
モデルが使った引用またはソース |
toolCalls |
このステップで行われた型付きツール呼び出し |
toolResults |
このステップで受け取った型付きツール結果 |
finishReason |
ステップが終了した理由 |
usage |
トークン使用量(キャッシュと推論トークンを含む) |
providerMetadata |
プロバイダー固有のメタデータ |
各ストリーミングチャンクで呼ばれます。高頻度で、トークンごとに発火します。ストリーミング分析、進捗表示、トークン計測に使います。観測専用です。
チャットターンがアシスタントメッセージを生成し、永続化したあとに呼ばれます。このフックの前にターンロックは解除されるため、内部から saveMessages や他のメソッドを呼んでも問題ありません。
アシスタントメッセージを永続化するすべてのターン経路で発火します。WebSocket、サブエージェント RPC、saveMessages、自動継続です。アシスタント部品を出す前にターンが失敗した場合は、代わりに onChatError がエラーを処理します。
onChatResponse(result: ChatResponseResult) {
if (result.status === "completed") {
console.log(`Turn ${result.requestId}: ${result.message.parts.length} parts`);
}
}| フィールド | 型 | 説明 |
|---|---|---|
message |
UIMessage |
永続化されたアシスタントメッセージ |
requestId |
string |
このターンの一意な ID |
continuation |
boolean |
継続ターンだったかどうか |
status |
"completed" | "error" | "aborted" |
ターンの終了方法 |
error |
string? |
エラーメッセージ(status が "error" のとき) |
チャットターン中にエラーが起きたときに呼ばれます。伝播するエラーを返すか、別のエラーを返します。任意のコンテキストは、失敗が起きた場所と、ユーザーメッセージがすでに永続化されていたかを示します。部分的なアシスタントメッセージがある場合は、このフックの前に永続化されます。
onChatError(error: unknown, ctx?: ChatErrorContext): unknownChatErrorContext には次が含まれます。
| フィールド | 型 | 説明 |
|---|---|---|
requestId |
string | undefined |
取得できる場合のチャットリクエスト ID |
stage |
"parse" | "persist" | "turn" | "stream" | "recovery" | "transcript" |
失敗した段階 |
messagesPersisted |
boolean |
受信したユーザーメッセージがすでに保存されていたか |
classification |
ChatErrorClassification | undefined |
コンテキスト超過を回復できなかったときの終端 onChatError で "context_overflow" になります(classifyChatError を参照)。それ以外は undefined です |
Think は同じ段階と永続化情報を、agents:chat 可観測性チャネルの chat:request:failed としても出します。
onChatError(error: unknown, ctx?: ChatErrorContext) {
console.error("Chat turn failed:", ctx?.stage, error);
if (ctx?.classification === "context_overflow") {
return new Error("This conversation is too long to continue. Please start a new one.");
}
return new Error("Something went wrong. Please try again.");
}ターン中にエラーが起きたとき、onChatError の前に呼ばれます。生のプロバイダーエラーを、プロバイダー非依存の分類へ写します。Think がフレームワーク内にプロバイダー固有の文字列を埋め込まずに反応できるようにするためです。compactAfter() に渡す tokenCounter と同じ役割分担です。どのプロバイダーとモデルを使うかはアプリが知っているため、写像はアプリが持ちます。
classifyChatError(error: unknown, ctx?: ChatErrorContext): ChatErrorClassification | voidChatErrorClassification は "context_overflow" | "rate_limit" | "transient" | "fatal" | "unknown" です。現在このフックが駆動するのはコンテキスト超過の回復だけです。Think はターンがエラーになり、contextOverflow.reactive が有効なときに呼びます。reactive がオフなら呼ばれません。
"context_overflow" を返すと、圧縮して再試行するバックストップが走ります(コンテキストウィンドウ超過の回復 を参照)。回復でターンを救えなかった場合、その分類は終端の onChatError 呼び出しで ChatErrorContext.classification として表面化します。
他の分類は将来用に予約されています。今返しても何も起きず、onChatError へ転送されません。void を返す(デフォルト)と、既存の終端動作のままです。
引数は Error、AI SDK の APICallError(statusCode / responseBody 付き)、またはスローではなくストリームエラー部品として表面化するストリーム内プロバイダーエラーの場合は、エラーメッセージ文字列です。型を絞り込んでください。プロバイダーのコンテキスト超過エラーはストリーム内エラー部品として届くため、このフックはスローされた例外ではなく文字列で受け取ります。
第 2 引数は ChatErrorContext です。超過回復中は { stage: "stream", requestId } なので、分類器は進行中のターンとエラーを対応付けできます。たとえば cancelChat(requestId) を呼んで回復を打ち切る、などです。
よくある場合は、同梱の defaultContextOverflowClassifier を割り当てます。Anthropic、OpenAI、Google、Bedrock などのコンテキスト超過エラーに一致します。
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think {
classifyChatError = defaultContextOverflowClassifier;
}import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think<Env> {
override classifyChatError = defaultContextOverflowClassifier;
}独自に書くこともできます。同梱の分類器へ委譲しても構いません。
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think {
classifyChatError(error) {
if (error instanceof Error && /rate.?limit/i.test(error.message)) {
return "rate_limit";
}
return defaultContextOverflowClassifier(error);
}
}import type { ChatErrorClassification } from "@cloudflare/think";
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think<Env> {
override classifyChatError(error: unknown): ChatErrorClassification | void {
if (error instanceof Error && /rate.?limit/i.test(error.message)) {
return "rate_limit";
}
return defaultContextOverflowClassifier(error);
}
}