INTEGRITY Документация

Устойчивое восстановление

Think всегда оборачивает ходы чата в восстанавливаемые fiber. Если Durable Object вытесняется в середине потоковой передачи, Think восстанавливает все буферизованные чанки. Он сохраняет частичный результат и планирует продолжение или повторную попытку.

Ходы WebSocket, дочерний агент chat() ходов, устойчивые submitMessages() выполнений, автоматических продолжений, saveMessages(), а также continueLastTurn() оборачиваются в runFiber.

Ограниченное восстановление

Прерывание watchdog из-за зависания потока (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 канал. См. Диагностические каналы.

onChatRecovery

Переопределить onChatRecovery когда нужно восстановление, специфичное для провайдера, например получение сохранённого результата OpenAI Responses вместо нового вызова модели:

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
	}
}

ChatRecoveryContext

Поле Тип Описание
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 Миллисекунды эпохи, когда начался ход

ChatRecoveryOptions

Поле Тип Описание
persist boolean? Нужно ли сохранять частичное сообщение ассистента
continue boolean? Нужно ли автоматически продолжать новым ходом через continueLastTurn()

С persist: true, частичное сообщение сохраняется. При continue: true, Think вызывает continueLastTurn() после того как агент достигает стабильного состояния.

Для прерываний до начала потоковой передачи, когда ctx.streamId === "" и ctx.partialText === "" но последнее сохранённое сообщение по-прежнему остаётся сообщением пользователя без ответа, Think автоматически повторяет этот ход, если только continue это false.

onChatRecovery(ctx: ChatRecoveryContext): ChatRecoveryOptions {
	if (!ctx.streamId && !ctx.partialText) {
		console.log("Recovering a pre-stream interruption");
	}
	return {};
}

Используйте ctx.createdAt чтобы пропускать устаревшие восстановления. Например, если прерванный ход старше нескольких минут, верните { continue: false } чтобы частичный ответ сохранялся без запуска старого continuation.

Устойчивое ведение учёта остаётся активным, когда автоматическое продолжение неуместно. Верните { 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 Предел числа попыток. Сбрасывается при продвижении вперёд, поэтому отслеживает зацикливание alarm без прогресса, а не полноценный длинный ход.
stableTimeoutMs 10_000 Сколько времени попытка ожидает, пока isolate не достигнет стабильного состояния, прежде чем будет запланирована повторно.
noProgressTimeoutMs 300_000 (5 мин) Основной предел зависшего хода: максимальное время без продвижения вперёд до запечатывания. Сбрасывается при каждой попытке с прогрессом.
maxRecoveryWork 1,000 Защита от неконтролируемого цикла: максимальное количество произведённых единиц контента и вызовов инструментов до того, как ещё выполняющийся ход будет опечатан. Задайте более высокое значение или Infinity для длительного агентного хода.
maxOomRetries 3 Бюджет повторных попыток при сбросах Durable Object из-за ограничения памяти. Задайте 0 чтобы остановиться после первого сброса лимита памяти.
shouldKeepRecovering , Политика вызывающей стороны учитывается начиная со второй попытки. Верните false чтобы остановить восстановление. Точка привязки hook для бюджета токенов или расходов (ctx.work это приблизительное количество сегментов, а не токенов).
terminalMessage общее сообщение Сообщение, которое показывается пользователю, если восстановление прекращается.
onExhausted , Вызывается один раз, когда восстановление окончательно прекращается. Изучите ctx.reason.

ctx.reason для исчерпанного хука принимает одно из следующих значений: no_progress_timeout (застряло), max_attempts_exceeded (цикл alarm без прогресса), work_budget_exceeded (неконтролируемый), recovery_aborted (ваш shouldKeepRecovering возвращено false), out_of_memory (бюджет повторов при ограничении памяти), или stable_timeout (экстремальная флуктуация). См. Восстановление потока для полного общего справочника. Think и @cloudflare/ai-chat используют одинаковую конфигурацию восстановления.

Восстановление прерванных вызовов инструментов

Если ход прерывается на середине выполнения, в транскрипте может остаться вызов инструмента без итогового результата. Перед следующим обращением к провайдеру Think исправляет такие вызовы, чтобы модель не перезапустила их незаметно, а провайдер не отклонил транскрипт с AI_MissingToolResultsError. По умолчанию прерванный вызов переводится в результат инструмента с ошибкой, поэтому запись сохраняется, а преобразование по-прежнему получает результат инструмента для него.

Переопределить repairInterruptedToolPart чтобы настроить восстановленную форму. Типичный случай: инструмент, который разрешается на стороне клиента, например ask_user вопрос, у которого нет серверного execute и обычно получает ответ в следующем сообщении пользователя. Преобразование его в обычную текстовую часть позволяет модели воспринимать это как обычный разговор, а не как ошибку инструмента, и сохраняет вопрос дословно при компакции:

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 уже нормализован до корректного объекта. Возвращённый tool part должен нести согласованный результат (output-available, output-error, или output-denied); также допустимо вернуть часть, не являющуюся инструментом, например текст.

Восстановление после переполнения контекстного окна

Компактизация проверяется между ходами, compactAfter() выполняется после каждого appendMessage(). Но один длинный ход с большим количеством вызовов инструментов постепенно увеличивает промпт внутри одного streamText цикл и может превысить контекстное окно модели посреди хода, ещё до следующей проверки перед ходом. После этого поставщик отклоняет запрос ("prompt is too long", context_length_exceeded), иначе ход завершился бы окончательной ошибкой.

Think восстанавливается после этого с помощью двух опциональных слоёв, не зависящих от провайдера; оба настраиваются через contextOverflow свойство. Оба по умолчанию отключены, поэтому существующее поведение не меняется. Оба используют функцию компакции вашей сессии, поэтому для них требуется configureSession() с onCompaction() настроено. Оба требуют classifyChatError чтобы сообщить Think, какие ошибки считаются переполнением: в своём ядре Think не содержит логики сопоставления, зависящей от провайдера.

1. Реактивная подстраховка: contextOverflow.reactive. Когда ход завершается ошибкой, которую вы классифицируете как "context_overflow", Think отбрасывает усечённый частичный результат, запускает session.compact(), и перезапускает ход с уже компактизированной историей. Частичный результат не сохраняется: ход запускается заново с нуля, поэтому сохранение оборванного сообщения ассистента сделало бы его осиротевшим рядом с восстановленным ответом. Это ограничено contextOverflow.maxRetries (по умолчанию 1); если компактизация не может сократить историю или бюджет исчерпан, переполнение проявляется как финальная ошибка через onChatError с classification: "context_overflow" : никогда не зацикливается и не завершается молча.

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 (безопасное, слегка завышенное приближение: компактизация происходит немного раньше, чем пропуск порога). Компактизация выполняется не более proactive.maxCompactions раз за ход (по умолчанию 1), независимо от reactive 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;
}

Используйте один из уровней по отдельности или оба вместе: проактивная защита предотвращает большинство переполнений, а реактивный резервный механизм перехватывает те, что всё же проскальзывают (например, ход, который начинается уже с превышением бюджета, или единичный результат инструмента настолько большой, что компактизация не помогает, и тогда он корректно переводится в терминальное состояние). Оба уровня применяются на каждом пути входа хода (WebSocket, дочерний агент chat(), и программный saveMessages() / submitMessages()), и оба выдают chat:context:compacted событие диагностического канала.

Готовую к запуску демонстрацию с реальной моделью Workers AI см. в context-overflow-recovery пример ↗.

Обнаружение стабильности

Think предоставляет методы для проверки того, находится ли агент в стабильном состоянии: нет незавершённых результатов инструментов, нет незавершённых подтверждений, нет активных ходов.

hasPendingInteraction

Возвращает true если какое-либо сообщение ассистента содержит невыполненные вызовы инструментов (инструменты без результатов или с ожидающими подтверждениями).

protected hasPendingInteraction(): boolean

waitUntilStable

Возвращает promise, который разрешается в true когда агент достигает стабильного состояния, или 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." }],
		},
	]);
}