← Создание агентов на Cloudflare / agents / harnesses / think
Устойчивое восстановление
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(): booleanwaitUntilStable
Возвращает 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." }],
},
]);
}