← Создание агентов на Cloudflare / agents / harnesses / think
Хуки жизненного цикла
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 |
нет |
Порядок выполнения
Для хода с двумя вызовами инструментов:
flowchart TD
cfg["configureSession() — once at startup, not per-turn"] --> bt["beforeTurn() — inspect context, override model/tools/prompt"]
bt --> bs
subgraph loop ["streamText (repeats per step)"]
bs["beforeStep()"] --> chunk["onChunk() — per streaming chunk"]
chunk --> btc["beforeToolCall()"]
btc --> exec["tool executes"]
exec --> atc["afterToolCall()"]
atc --> sf["onStepFinish()"]
sf -->|"more steps"| bs
end
sf -->|"turn complete"| ocr["onChatResponse() — message persisted, turn lock released"]
beforeTurn
Вызывается перед streamText. Получает полностью собранный контекст: системный промпт, преобразованные сообщения, объединённые инструменты и модель. Верните TurnConfig чтобы переопределить любую часть, либо void, чтобы принять значения по умолчанию.
beforeTurn(ctx: TurnContext): TurnConfig | void | Promise<TurnConfig | void>TurnContext
| Поле | Тип | Описание |
|---|---|---|
system |
string |
Собранный системный промпт (из блоков контекста или getSystemPrompt()) |
messages |
ModelMessage[] |
Собранные сообщения модели (усечённые, прорежённые) |
tools |
ToolSet |
Объединённый набор инструментов (workspace + getTools + session + extensions + MCP + client) |
model |
LanguageModel |
Модель из getModel() |
continuation |
boolean |
Является ли этот ход продолжением (автопродолжение после результата инструмента) |
body |
Record<string, unknown> |
Пользовательские поля тела из клиентского запроса |
TurnConfig
Все поля необязательны. Возвращайте только то, что хотите изменить.
| Поле | Тип | Описание |
|---|---|---|
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"] };
}Добавьте контекст для каждого хода из тела клиентского запроса:
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 не может переключать структурированный вывод в рамках одного шага.
beforeStep
Вызывается перед каждым шагом AI SDK в агентном цикле. Think перенаправляет этот хук в streamText как prepareStep, чтобы он получал полный контекст prepare-step из AI SDK и мог возвращать переопределения для каждого шага. Используйте beforeTurn для сборки в рамках всего хода и beforeStep когда решение зависит от номера шага или результатов предыдущих шагов.
beforeStep(ctx: PrepareStepContext): StepConfig | void {
if (ctx.stepNumber > 0) {
return { activeTools: [] };
}
}beforeToolCall
Вызывается перед выполнением серверного инструмента 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 выполняется. Если вам нужен настоящий предварительный streaming от этого инструмента, избегайте его перехвата с помощью beforeToolCall.
afterToolCall
Вызывается после того как становится известен результат работы инструмента. Сюда входят реальные выполнения, заблокированные вызовы, подменённые вызовы и выброшенные ошибки инструмента.
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.
onStepFinish
Вызывается после завершения каждого шага в агентном цикле. StepContext это событие step-finish из AI SDK, поэтому оно включает полную запись шага: сгенерированный текст, рассуждения, файлы, источники, типизированные вызовы инструментов и их результаты, использование, предупреждения, метаданные запроса и ответа, а также метаданные провайдера.
onStepFinish(ctx: StepContext) {
console.log(
`Step ${ctx.stepNumber} (${ctx.finishReason}): ` +
`${ctx.usage.inputTokens}in/${ctx.usage.outputTokens}out`,
);
}| Поле | Описание |
|---|---|
stepNumber |
Индекс шага, отсчитываемый от нуля |
text |
Текст, сгенерированный на этом шаге |
reasoning |
Части рассуждений, генерируемые моделью |
files |
Файлы, созданные на этом шаге |
sources |
Цитаты или источники, использованные моделью |
toolCalls |
Типизированные вызовы инструментов, выполненные на этом шаге |
toolResults |
Типизированные результаты инструментов, полученные на этом шаге |
finishReason |
Почему шаг завершился |
usage |
Использование токенов, включая токены кэша и рассуждений |
providerMetadata |
Метаданные для конкретных провайдеров |
onChunk
Вызывается для каждого потокового чанка. Высокая частота: срабатывает на каждый токен. Используйте для потоковой аналитики, индикаторов прогресса или подсчёта токенов. Только для наблюдения.
onChatResponse
Вызывается после того как ход чата создаёт и сохраняет сообщение ассистента. Блокировка хода снимается до запуска этого хука, поэтому в нём безопасно вызывать 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
Вызывается при возникновении ошибки во время хода чата. Верните ошибку, чтобы передать её дальше, либо верните другую ошибку. Необязательный контекст описывает, где произошёл сбой и были ли уже сохранены сообщения пользователя. Частичное сообщение ассистента (если оно есть) сохраняется до срабатывания этого хука.
onChatError(error: unknown, ctx?: ChatErrorContext): unknownChatErrorContext включает:
| Поле | Тип | Описание |
|---|---|---|
requestId |
string | undefined |
ID запроса чата, если доступен |
stage |
"parse" | "persist" | "turn" | "stream" | "recovery" | "transcript" |
Этап сбоя |
messagesPersisted |
boolean |
Были ли уже сохранены входящие сообщения пользователя |
classification |
ChatErrorClassification | undefined |
Установите значение "context_overflow" в терминальном onChatError когда переполнение контекста не удалось восстановить (см. classifyChatError); undefined иначе |
Think также генерирует chat:request:failed на agents:chat канал наблюдаемости с той же информацией об этапе и сохранении.
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.");
}classifyChatError
Вызывается при возникновении ошибки во время хода, до onChatError. Преобразует необработанную ошибку провайдера в категорию, не зависящую от провайдера, чтобы Think мог реагировать, не встраивая специфичные для провайдера строки во фреймворк: то же разделение, что и у tokenCounter вы передаете в compactAfter(). Приложение владеет этим сопоставлением, поскольку знает, с каким провайдером и моделью оно взаимодействует.
classifyChatError(error: unknown, ctx?: ChatErrorContext): ChatErrorClassification | voidChatErrorClassification это "context_overflow" | "rate_limit" | "transient" | "fatal" | "unknown". Сегодня этот хук отвечает только за восстановление при переполнении контекста. Think вызывает его, когда в ходе возникает ошибка и contextOverflow.reactive включён. Если reactive отключён, он не вызывается.
Возврат "context_overflow" запускает защитный механизм compact-and-retry (см. Восстановление после переполнения контекстного окна). Если восстановление не может спасти ход, эта классификация проявляется в финальном onChatError вызов через ChatErrorContext.classification.
Остальные категории зарезервированы на будущее. Если сегодня вернуть одну из них, это не даст эффекта и не будет передано в onChatError. Возврат void (значение по умолчанию) сохраняет существующее поведение при завершении.
Аргументом может быть Error, AI SDK APICallError (с statusCode/responseBody), либо, для ошибок поставщика внутри потока, которые проявляются как часть потока с ошибкой, а не как исключение, строку с сообщением об ошибке. Сужайте тип соответствующим образом. Ошибки поставщика из-за переполнения контекста приходят как части потока с ошибкой, поэтому этот хук получает их в виде строки, а не как выброшенное исключение.
Вторым аргументом является 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);
}
}