Think はチャットターンを、常に復旧可能な fiber で包みます。Durable Object がストリーム途中で退避されると、Think はバッファ済みチャンクを再構築します。部分出力を永続化し、継続または再試行をスケジュールします。
WebSocket ターン、サブエージェントの chat() ターン、耐久的な submitMessages() 実行、自動継続、saveMessages()、continueLastTurn() は runFiber で包まれます。
ストール監視による中止(chatStreamStallTimeoutMs)も、同じ上限付き復旧経路を使います。SDK は確定した部分出力を保持し、継続をスケジュールします。一時的なハングは自動で復旧します。プロバイダーがハングし続ける場合は、デプロイや退避と同じ経路で予算を使い切ります。SDK は onExhausted を呼び、chat:recovery:exhausted を発行し、設定した terminalMessage を表示します。
上限付き復旧は、chatRecovery にオブジェクトを設定して構成します。
export class MyAgent extends Think {
chatRecovery = {
maxAttempts: 6,
stableTimeoutMs: 10_000,
terminalMessage: "The assistant was interrupted and could not recover.",
async onExhausted(ctx) {
console.warn("Chat recovery exhausted", ctx.incidentId);
},
};
getModel() {
/* ... */
}
}export class MyAgent extends Think<Env> {
override chatRecovery = {
maxAttempts: 6,
stableTimeoutMs: 10_000,
terminalMessage: "The assistant was interrupted and could not recover.",
async onExhausted(ctx) {
console.warn("Chat recovery exhausted", ctx.incidentId);
},
};
getModel() {
/* ... */
}
}同じ復旧イベントは、agents/observability の chat チャネルでも利用できます。トランスクリプトの修復は transcript チャネルに発行されます。Diagnostics channels を参照してください。
保存済みの OpenAI Responses 結果を取得するなど、新しいモデル呼び出しの代わりにプロバイダー固有の復旧が必要なときは、onChatRecovery をオーバーライドします。
export class MyAgent extends Think {
chatRecovery = {
maxAttempts: 10,
terminalMessage: "The assistant was interrupted. Please try again.",
};
async onChatRecovery(ctx) {
console.log("Recovering chat turn", ctx.incidentId, ctx.attempt);
return {}; // persist partial output and continue/retry when possible
}
}import type {
ChatRecoveryContext,
ChatRecoveryOptions,
} from "@cloudflare/think";
export class MyAgent extends Think<Env> {
override chatRecovery = {
maxAttempts: 10,
terminalMessage: "The assistant was interrupted. Please try again.",
};
override async onChatRecovery(
ctx: ChatRecoveryContext,
): Promise<ChatRecoveryOptions> {
console.log("Recovering chat turn", ctx.incidentId, ctx.attempt);
return {}; // persist partial output and continue/retry when possible
}
}| フィールド | 型 | 説明 |
|---|---|---|
incidentId |
string |
この復旧インシデントの安定した ID |
attempt |
number |
このインシデントの現在の試行回数。1 から始まります |
maxAttempts |
number |
終端(予算切れ)までの設定済み試行上限 |
recoveryKind |
"retry" | "continue" |
未応答のユーザーターンを再試行するか、部分的なアシスタントターンを継続するか |
streamId |
string |
中断されたターンのストリーム ID |
requestId |
string |
中断されたターンのリクエスト ID |
partialText |
string |
中断前に生成されたテキスト |
partialParts |
MessagePart[] |
中断前に蓄積されたパーツ |
recoveryData |
unknown | null |
ターン中の this.stash() からのデータ |
messages |
UIMessage[] |
現在の会話履歴 |
lastBody |
Record<string, unknown>? |
中断されたターンのボディ |
lastClientTools |
ClientToolSchema[]? |
中断されたターンのクライアントツール |
createdAt |
number |
ターン開始時のエポックミリ秒 |
| フィールド | 型 | 説明 |
|---|---|---|
persist |
boolean? |
部分的なアシスタントメッセージを永続化するかどうか |
continue |
boolean? |
continueLastTurn() で新しいターンを自動継続するかどうか |
persist: true の場合、部分メッセージを保存します。continue: true の場合、エージェントが安定状態に達したあと、Think は continueLastTurn() を呼びます。
ストリーム開始前の中断(ctx.streamId === "" かつ ctx.partialText === "" で、最新の永続化メッセージがまだ未応答のユーザーメッセージ)では、continue が false でない限り、Think はそのターンを自動で再試行します。
onChatRecovery(ctx: ChatRecoveryContext): ChatRecoveryOptions {
if (!ctx.streamId && !ctx.partialText) {
console.log("Recovering a pre-stream interruption");
}
return {};
}古い復旧をスキップするには ctx.createdAt を使います。たとえば中断ターンが数分以上前なら { continue: false } を返し、部分応答は残しつつ古い継続は始めないようにします。
自動継続が適切でない場合でも、耐久的な帳簿処理は続きます。別のモデル呼び出しを防ぐには { continue: false } を返します。キャンセル、副作用、コスト制御については 自動継続を制御する を参照してください。
復旧上限と終端時の動作を調整するには、chatRecovery オブジェクトを割り当てます。前進しているターンは、maxRecoveryWork の範囲内であれば、繰り返しの中断を生き延びます。復旧を止めるタイミングは、次のオプションで制御します。
export class MyAgent extends Think {
chatRecovery = {
maxAttempts: 10,
noProgressTimeoutMs: 5 * 60 * 1000,
maxRecoveryWork: 1_000,
terminalMessage: "The assistant was interrupted and could not recover.",
// Consulted from the second recovery attempt onward. Return false to stop.
// Called as `config.shouldKeepRecovering(ctx)`, so it is NOT bound to the
// agent instance — track real token/cost spend in your own store keyed by
// `ctx.recoveryRootRequestId`.
async shouldKeepRecovering(ctx) {
return (await getSpendForTurn(ctx.recoveryRootRequestId)) < MAX_SPEND;
},
async onExhausted(ctx) {
console.warn("Recovery exhausted", ctx.incidentId, ctx.reason);
},
};
}export class MyAgent extends Think<Env> {
override chatRecovery = {
maxAttempts: 10,
noProgressTimeoutMs: 5 * 60 * 1000,
maxRecoveryWork: 1_000,
terminalMessage: "The assistant was interrupted and could not recover.",
// Consulted from the second recovery attempt onward. Return false to stop.
// Called as `config.shouldKeepRecovering(ctx)`, so it is NOT bound to the
// agent instance — track real token/cost spend in your own store keyed by
// `ctx.recoveryRootRequestId`.
async shouldKeepRecovering(ctx) {
return (await getSpendForTurn(ctx.recoveryRootRequestId)) < MAX_SPEND;
},
async onExhausted(ctx) {
console.warn("Recovery exhausted", ctx.incidentId, ctx.reason);
},
};
}| フィールド | デフォルト | 説明 |
|---|---|---|
maxAttempts |
10 |
試行上限。前進があるとリセットされます。健全な長いターンではなく、進展なしのアラームループを捉えます。 |
stableTimeoutMs |
10_000 |
isolate が安定状態に達するまで、1 回の試行が待つ時間。超えると再スケジュールします。 |
noProgressTimeoutMs |
300_000(5 分) |
停滞ターンの主上限。前進なしの最長時間を超えると確定します。前進を伴う試行のたびにリセットされます。 |
maxRecoveryWork |
1,000 |
暴走ループのガード。まだ前進しているターンでも、生成したコンテンツ / ツール単位がこの上限に達すると確定します。長いエージェントターンでは、より大きい値か Infinity を設定します。 |
maxOomRetries |
3 |
Durable Object のメモリ上限リセットに対する再試行予算。最初のメモリ上限リセット後に止めるには 0 を設定します。 |
shouldKeepRecovering |
— | 2 回目以降の試行で参照する呼び出し側ポリシー。復旧を止めるには false を返します。トークン / コスト予算のフックです(ctx.work は粗いセグメント数であり、トークン数ではありません)。 |
terminalMessage |
汎用メッセージ | 復旧を諦めたときにユーザーへ表示するメッセージ。 |
onExhausted |
— | 復旧を諦めたときに一度だけ呼ばれます。ctx.reason を確認してください。 |
exhausted フックの ctx.reason は次のいずれかです。no_progress_timeout(停滞)、max_attempts_exceeded(進展なしのアラームループ)、work_budget_exceeded(暴走)、recovery_aborted(shouldKeepRecovering が false を返した)、out_of_memory(メモリ上限の再試行予算)、stable_timeout(極端なチャーン)。共有リファレンスの全体は ストリーム復旧 を参照してください。Think と @cloudflare/ai-chat は同じ復旧設定を使います。
ターンが途中で中断されると、トランスクリプトに、結果が確定していないツール呼び出しが残ることがあります。次のプロバイダー呼び出しの前に、Think はそうした呼び出しを修復します。モデルが黙って再実行せず、プロバイダーが AI_MissingToolResultsError でトランスクリプトを拒否しないようにするためです。既定では、中断された呼び出しをエラーのツール結果に切り替えます。記録は残り、変換時にもツール結果があります。
修復後の形をカスタマイズするには、repairInterruptedToolPart をオーバーライドします。よくあるのはクライアント解決のツールです。たとえばサーバー側 execute がなく、通常はユーザーの次のメッセージで答える ask_user の質問です。プレーンなテキストパーツに変換すると、モデルはツールエラーではなく通常の会話として扱い、コンパクション後も質問文をそのまま残せます。
export class MyAgent extends Think {
repairInterruptedToolPart(part) {
const record = part;
if (record.type === "tool-ask_user") {
const input = record.input;
if (input?.prompt) {
return { type: "text", text: input.prompt };
}
}
return super.repairInterruptedToolPart(part);
}
}import type { UIMessage } from "ai";
export class MyAgent extends Think<Env> {
protected override repairInterruptedToolPart(
part: UIMessage["parts"][number],
): UIMessage["parts"][number] {
const record = part as Record<string, unknown>;
if (record.type === "tool-ask_user") {
const input = record.input as { prompt?: string } | undefined;
if (input?.prompt) {
return { type: "text", text: input.prompt };
}
}
return super.repairInterruptedToolPart(part);
}
}これはトランスクリプト修復中に走ります。修復済みトランスクリプトが永続化され、モデルへ送られる前です。そのため変換は次のターンだけでなく、現在のターンも形作ります。input はすでに有効なオブジェクトに正規化されています。返すツールパーツには確定結果(output-available、output-error、output-denied)が必要です。テキストなど、ツール以外のパーツを返すことも問題ありません。
コンパクション の確認は ターンとターンの間 です。compactAfter() は各 appendMessage() のあとで走ります。ただしツールが多い長い 1 ターンは、1 つの streamText ループ内でプロンプトが段階的に増え、次のターン前チェックより前に ターン途中 でモデルのコンテキストウィンドウを超えることがあります。プロバイダーはそのリクエストを拒否し("prompt is too long"、context_length_exceeded)、ターンは終端で死んでしまいます。
Think は、プロバイダー非依存のオプトイン層を 2 つ使い、この状況から復旧します。どちらも contextOverflow プロパティで設定します。どちらも既定ではオフなので、既存の動作は変わりません。どちらもセッションのコンパクション関数を再利用するため、onCompaction() を設定した configureSession() が必要です。どちらも、どのエラーが超過かを Think に伝える classifyChatError が必要です。コアにプロバイダー固有のマッチングはありません。
1. 事後の安全網 — contextOverflow.reactive。 ターンが、あなたが "context_overflow" と分類したエラーで失敗すると、Think は途中で切れた部分出力を捨て、session.compact() を実行し、コンパクション済み履歴からターンを再実行します。部分出力は永続化しません。ターンは最初からやり直すため、切れたアシスタントメッセージを残すと、復旧後の回答の横に孤立します。上限は contextOverflow.maxRetries(既定 1)です。コンパクションで履歴を短縮できない、または予算を使い切った場合、超過は classification: "context_overflow" 付きで onChatError に終端として出ます。ループせず、黙って終わりません。
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think {
contextOverflow = { reactive: true };
// The bundled classifier covers the common providers (Anthropic, OpenAI,
// Google, Bedrock, …). Assign it directly, or write your own.
classifyChatError = defaultContextOverflowClassifier;
}import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think<Env> {
override contextOverflow = { reactive: true };
// The bundled classifier covers the common providers (Anthropic, OpenAI,
// Google, Bedrock, …). Assign it directly, or write your own.
override classifyChatError = defaultContextOverflowClassifier;
}2. 事前のガード — contextOverflow.proactive。 プロバイダーエラーが起きる前に防ぎます。各ステップの前に、Think は直前ステップのモデル報告 usage.inputTokens(プロバイダー非依存)を読み、maxInputTokens * (headroom ?? 0.9) を超えていればその場でコンパクションし、再コンパクションした履歴を次のステップに渡します。プロバイダーが inputTokens を省略した場合は usage.totalTokens にフォールバックします(安全側の過大評価です。閾値を逃すより、少し早めにコンパクションします)。1 ターンあたりのコンパクションは最大 proactive.maxCompactions 回(既定 1)です。事後の maxRetries 予算とは独立なので、短縮できない履歴でも、毎ステップコンパクションしません。
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think {
contextOverflow = {
reactive: true,
// Compact mid-turn once a step approaches 90% of a 200K window.
proactive: { maxInputTokens: 200_000 },
};
classifyChatError = defaultContextOverflowClassifier;
}import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think<Env> {
override contextOverflow = {
reactive: true,
// Compact mid-turn once a step approaches 90% of a 200K window.
proactive: { maxInputTokens: 200_000 },
};
override classifyChatError = defaultContextOverflowClassifier;
}どちらか一方だけでも、両方でも使えます。事前ガードがほとんどの超過を避け、事後の安全網がすり抜けたもの(すでに予算超過で始まるターン、コンパクションでは対処できない巨大な 1 つのツール結果など)を捉えます。後者はきれいに終端します。どちらもすべてのターン入口(WebSocket、サブエージェントの chat()、プログラムからの saveMessages() / submitMessages())に適用され、どちらも chat:context:compacted の diagnostics channel イベント を発行します。
実際の Workers AI モデルに対する実行可能なデモは、context-overflow-recovery の例 ↗ を参照してください。
Think は、エージェントが安定状態かどうかを確認するメソッドを提供します。未完了のツール結果、未完了の承認、実行中のターンがない状態です。
いずれかのアシスタントメッセージに未完了のツール呼び出し(結果のないツール、または未完了の承認)がある場合、true を返します。
protected hasPendingInteraction(): booleanエージェントが安定状態に達すると true に解決する Promise を返します。タイムアウトを超えた場合は false です。
const stable = await this.waitUntilStable({ timeout: 30_000 });
if (stable) {
await this.saveMessages([
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Now that you are done, summarize." }],
},
]);
}const stable = await this.waitUntilStable({ timeout: 30_000 });
if (stable) {
await this.saveMessages([
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Now that you are done, summarize." }],
},
]);
}