← Создание агентов на Cloudflare / agents / communication-channels / chat
Чат-агенты
Создавайте чат-интерфейсы на базе ИИ с помощью AIChatAgent и useAgentChat. Сообщения автоматически сохраняются в SQLite, потоки возобновляются после отключения, а вызовы инструментов работают одинаково на сервере и клиенте.
Обзор
@cloudflare/ai-chat пакет предоставляет два основных API:
| Экспорт | Импорт | Назначение |
|---|---|---|
AIChatAgent |
@cloudflare/ai-chat |
Класс агента на стороне сервера с сохранением сообщений и потоковой передачей |
useAgentChat |
@cloudflare/ai-chat/react |
React-хук для создания чат-интерфейсов |
Дополнительные вспомогательные функции также доступны в @cloudflare/ai-chat/react, @cloudflare/ai-chat/types, а также agents/chat; см. Экспорты для полного набора возможностей пакета.
Построено на AI SDK ↗ и Cloudflare Durable Objects вы получаете:
- Автоматическое сохранение сообщений : разговоры хранятся в SQLite, переживают перезапуски
- Возобновляемый стриминг : отключившиеся клиенты возобновляют работу с середины потока без потери данных
- Синхронизация в реальном времени : сообщения, рассылаемые всем подключённым клиентам через WebSocket
- Поддержка инструментов : паттерны инструментов на стороне сервера, на стороне клиента и с участием человека
- Части данных : прикрепление типизированного JSON (цитаты, ход выполнения, использование) к сообщениям вместе с текстом
- Защита размера строки : автоматическая компакция при приближении сообщений к лимитам SQLite
быстрый старт
Установить
npm i @cloudflare/ai-chat agents ai @ai-sdk/react workers-ai-providerСервер
import { AIChatAgent } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages } from "ai";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
// Use any provider such as workers-ai-provider, openai, anthropic, google, etc.
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}import { AIChatAgent } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages } from "ai";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
// Use any provider such as workers-ai-provider, openai, anthropic, google, etc.
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}Клиент
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const { messages, sendMessage, status } = useAgentChat({ agent });
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) =>
part.type === "text" ? <span key={i}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem("input");
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="input" placeholder="Type a message..." />
<button type="submit" disabled={status !== "ready"}>
Send
</button>
</form>
</div>
);
}import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const { messages, sendMessage, status } = useAgentChat({ agent });
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) =>
part.type === "text" ? <span key={i}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem(
"input",
) as HTMLInputElement;
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="input" placeholder="Type a message..." />
<button type="submit" disabled={status !== "ready"}>
Send
</button>
</form>
</div>
);
}Конфигурация Wrangler
// wrangler.jsonc
{
"ai": { "binding": "AI" },
"durable_objects": {
"bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }],
} new_sqlite_classes миграция обязательна: AIChatAgent использует SQLite для персистентности сообщений и буферизации чанков потока.
Как это работает
sequenceDiagram
participant Client as Client (useAgentChat)
participant Agent as AIChatAgent
participant DB as SQLite
Client->>Agent: CF_AGENT_USE_CHAT_REQUEST (WebSocket)
Agent->>DB: Persist messages
Agent->>Agent: onChatMessage()
loop Streaming response
Agent-->>Client: CF_AGENT_USE_CHAT_RESPONSE (chunks)
Agent->>DB: Buffer chunks
end
Agent->>DB: Persist final message
Agent-->>Client: CF_AGENT_CHAT_MESSAGES (broadcast to all clients)
- Клиент отправляет сообщение через WebSocket
AIChatAgentсохраняет сообщения в SQLite и вызывает вашonChatMessageметод- Ваш метод возвращает потоковый
Response(обычно отstreamText) - Чанки потоково передаются обратно через WebSocket в реальном времени
- Когда поток завершается, итоговое сообщение сохраняется и рассылается всем соединениям
Server API
AIChatAgent
Расширяет Agent из agents пакет. Управляет состоянием разговора, сохранением данных и потоковой передачей.
import { AIChatAgent } from "@cloudflare/ai-chat";
export class ChatAgent extends AIChatAgent {
// Access current messages
// this.messages: UIMessage[]
// Limit stored messages (optional)
maxPersistedMessages = 200;
async onChatMessage(onFinish, options) {
// onFinish: callback for streamText (cleanup is automatic)
// options.abortSignal: cancel signal
// options.body: custom data from client
// options.continuation: true for continuation turns
// Return a Response (streaming or plain text)
}
}import { AIChatAgent } from "@cloudflare/ai-chat";
export class ChatAgent extends AIChatAgent {
// Access current messages
// this.messages: UIMessage[]
// Limit stored messages (optional)
maxPersistedMessages = 200;
async onChatMessage(onFinish, options?) {
// onFinish: callback for streamText (cleanup is automatic)
// options.abortSignal: cancel signal
// options.body: custom data from client
// options.continuation: true for continuation turns
// Return a Response (streaming or plain text)
}
}onChatMessage
Это основной метод, который вы переопределяете. Он получает контекст беседы и должен возвращать Response.
Потоковый ответ (наиболее распространённый):
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
system: "You are a helpful assistant.",
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
system: "You are a helpful assistant.",
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}Ответ в виде обычного текста:
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
return new Response("Hello! I am a simple agent.", {
headers: { "Content-Type": "text/plain" },
});
}
}Доступ к пользовательским данным тела запроса и ID запроса:
export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const { timezone, userId } = options?.body ?? {};
// Use these values in your LLM call or business logic
// options.requestId — unique identifier for this chat request,
// useful for logging and correlating events
console.log("Request ID:", options?.requestId);
if (options?.continuation) {
// This turn continues a previous assistant message after a tool result,
// continueLastTurn(), or recovery.
}
}
}options.continuation это true для автоматического продолжения после результатов инструментов или подтверждений, вызовы continueLastTurn(), и восстановленных ходов. Используйте это, чтобы выбрать другую модель, скорректировать системный промпт или пропустить затратную сборку контекста для ходов-продолжений.
this.messages
Текущая история разговора, загружаемая из SQLite. Это массив UIMessage объекты из AI SDK. Сообщения автоматически сохраняются после каждого взаимодействия.
maxPersistedMessages
Ограничьте количество сообщений, хранимых в SQLite. При превышении лимита самые старые сообщения удаляются. Это влияет только на хранение: не на то, что отправляется в LLM.
export class ChatAgent extends AIChatAgent {
maxPersistedMessages = 200;
}export class ChatAgent extends AIChatAgent {
maxPersistedMessages = 200;
}Чтобы управлять тем, что отправляется модели, используйте pruneMessages():
import { streamText, convertToModelMessages, pruneMessages } from "ai";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: pruneMessages({
messages: await convertToModelMessages(this.messages),
reasoning: "before-last-message",
toolCalls: "before-last-2-messages",
}),
});
return result.toUIMessageStreamResponse();
}
}import { streamText, convertToModelMessages, pruneMessages } from "ai";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: pruneMessages({
messages: await convertToModelMessages(this.messages),
reasoning: "before-last-message",
toolCalls: "before-last-2-messages",
}),
});
return result.toUIMessageStreamResponse();
}
}waitForMcpConnections
Управляет тем, будет ли AIChatAgent дожидается стабилизации подключений MCP-сервера перед вызовом onChatMessage. Это гарантирует this.mcp.getAITools() возвращает полный набор инструментов, особенно после выхода Durable Object из hibernation, когда подключения восстанавливаются в фоновом режиме.
| Значение | Поведение |
|---|---|
{ timeout: 10_000 } |
Ждите до 10 секунд (по умолчанию) |
{ timeout: N } |
Ждите до N миллисекунд |
true |
Ждите бесконечно долго, пока все подключения не будут готовы |
false |
Не ждать (старое поведение до версии 0.2.0) |
export class ChatAgent extends AIChatAgent {
// Default — waits up to 10 seconds
// waitForMcpConnections = { timeout: 10_000 };
// Wait forever
waitForMcpConnections = true;
// Disable waiting
waitForMcpConnections = false;
}export class ChatAgent extends AIChatAgent {
// Default — waits up to 10 seconds
// waitForMcpConnections = { timeout: 10_000 };
// Wait forever
waitForMcpConnections = true;
// Disable waiting
waitForMcpConnections = false;
}Для контроля на более низком уровне вызовите this.mcp.waitForConnections() напрямую внутри вашего onChatMessage взамен.
messageConcurrency
Определяет поведение при поступлении новых запросов пользователя, когда ход чата уже активен или стоит в очереди.
export class ChatAgent extends AIChatAgent {
messageConcurrency = "queue";
}export class ChatAgent extends AIChatAgent {
messageConcurrency = "queue";
}| Стратегия | Поведение |
|---|---|
"queue" (по умолчанию) |
Ставить в очередь каждую отправку и обрабатывать по порядку |
"latest" |
Сохраняется только последняя из перекрывающихся отправок; вытесненные отправки по прежнему сохраняют свои сообщения пользователя, но не запускают ход модели |
"merge" |
Ставить в очередь параллельные отправки, а затем объединять их последние сообщения пользователя в один общий ход перед запуском последнего хода из очереди |
"drop" |
Полностью игнорируйте перекрывающиеся отправки. Сообщения не сохраняются. |
{ strategy: "debounce", debounceMs?: number } |
Trailing-edge latest с окном ожидания (по умолчанию 750 мс) |
Этот параметр применяется только к sendMessage() отправок. Регенерации, продолжения вызовов инструментов, подтверждения, очистки и программные saveMessages() вызовы сохраняют своё прежнее сериализованное поведение.
persistMessages и saveMessages
persistMessages хранит сообщения в SQLite и транслирует обновление всем подключённым клиентам, но не не запускает ход модели. Используйте это, если хотите добавлять в разговор сообщения, не запуская новый ответ.
saveMessages сохраняет сообщения и запускает onChatMessage() для нового ответа. Перед запуском он дожидается завершения текущего хода чата, поэтому запланированные или программные сообщения никогда не пересекаются с активным потоком.
// Store messages without triggering a response
await this.persistMessages(messages);
// Store messages AND trigger onChatMessage
const { requestId, status } = await this.saveMessages(messages);// Store messages without triggering a response
await this.persistMessages(messages);
// Store messages AND trigger onChatMessage
const { requestId, status } = await this.saveMessages(messages);saveMessages принимает либо массив сообщений, либо функцию, которая формирует следующий список сообщений на основе последнего сохранённого this.messages. Используйте функциональную форму, чтобы избежать устаревших базовых значений при постановке в очередь нескольких вызовов:
await this.saveMessages((messages) => [
...messages,
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Summarize the latest data" }],
createdAt: new Date(),
},
]);await this.saveMessages((messages) => [
...messages,
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Summarize the latest data" }],
createdAt: new Date(),
},
]);saveMessages возвращает { requestId, status, error? } где status это "completed" если ход выполнился, "error" если поток сообщил об ошибке, "skipped" если чат был очищен ещё до того, как он начался, или "aborted" если внешний AbortSignal отменил это до завершения. Когда status это "error", error содержит сообщение об ошибке потока, если оно доступно.
Передайте options.signal чтобы отменить программный ход извне чат-агента. Это полезно, когда родительский вызов инструмента должен отменить ход дочернего агента, не зная внутренне сгенерированный ID запроса:
const controller = new AbortController();
const result = await this.saveMessages(
(messages) => [...messages, syntheticUserMessage],
{ signal: controller.signal },
);
if (result.status === "aborted") {
// Partial chunks already streamed are persisted.
}const controller = new AbortController();
const result = await this.saveMessages(
(messages) => [...messages, syntheticUserMessage],
{ signal: controller.signal },
);
if (result.status === "aborted") {
// Partial chunks already streamed are persisted.
}continueLastTurn() принимает такой же options.signal аргумент. AbortSignal объекты не могут пересекать границы RPC Durable Object, поэтому создавайте контроллер внутри того Durable Object, который вызывает saveMessages() или continueLastTurn(). Сигнал хранится только в памяти. Если Durable Object переходит в гибернацию посреди хода, восстановленный ход выполняется уже без исходного сигнала. Сохраняйте намерение отмены отдельно, если отмена должна пережить перезапуск.
onChatResponse
Вызывается после того как ход чата создаёт и сохраняет сообщение ассистента. Блокировка хода снимается до запуска этого хука, поэтому в нём безопасно вызывать saveMessages изнутри. Срабатывает для путей хода, которые сохраняют сообщение ассистента: запросы чата через WebSocket, saveMessages, и автоматическое продолжение. Если ход завершается ошибкой до того, как создана хотя бы одна часть ответа ассистента, ошибка вместо этого проявляется через исходный запрос.
export class ChatAgent extends AIChatAgent {
async onChatResponse(result) {
if (result.status === "completed") {
console.log("Turn completed:", result.requestId);
}
if (result.status === "error") {
console.error("Turn failed:", result.error);
}
}
}import type { ChatResponseResult } from "@cloudflare/ai-chat";
export class ChatAgent extends AIChatAgent {
protected async onChatResponse(result: ChatResponseResult) {
if (result.status === "completed") {
console.log("Turn completed:", result.requestId);
}
if (result.status === "error") {
console.error("Turn failed:", result.error);
}
}
} ChatResponseResult содержит:
| Поле | Тип | Описание |
|---|---|---|
message |
UIMessage |
Финализированное сообщение ассистента из этого хода |
requestId |
string |
ID запроса, связанный с этим ходом |
continuation |
boolean |
Был ли этот ход продолжением предыдущего хода ассистента |
status |
"completed" | "error" | "aborted" |
Как завершился ход |
error |
string | undefined |
Сообщение об ошибке, когда status это "error" |
sanitizeMessageForPersistence
Переопределите этот метод, чтобы применять собственные преобразования к сообщениям перед их сохранением в хранилище. Этот хук выполняется после встроенную очистку (удаление метаданных OpenAI, усечение payload инструментов, выполняемых на стороне провайдера Anthropic, фильтрацию пустых частей reasoning).
export class ChatAgent extends AIChatAgent {
sanitizeMessageForPersistence(message) {
return {
...message,
parts: message.parts.map((part) => {
if (
"output" in part &&
typeof part.output === "string" &&
part.output.length > 1000
) {
return { ...part, output: "[redacted]" };
}
return part;
}),
};
}
}export class ChatAgent extends AIChatAgent {
protected sanitizeMessageForPersistence(message: UIMessage): UIMessage {
return {
...message,
parts: message.parts.map((part) => {
if (
"output" in part &&
typeof part.output === "string" &&
part.output.length > 1000
) {
return { ...part, output: "[redacted]" };
}
return part;
}),
};
}
}Вспомогательные функции жизненного цикла хода
Эти методы помогают координировать программные ходы и дожидаться незавершённых взаимодействий.
hasPendingInteraction()
Возвращает true когда сообщение ассистента ожидает результат клиентского инструмента или подтверждение.
if (this.hasPendingInteraction()) {
console.log("Waiting for user to approve or provide tool output");
}if (this.hasPendingInteraction()) {
console.log("Waiting for user to approve or provide tool output");
}waitUntilStable()
Ожидает, пока разговор не станет полностью стабильным: нет активного потока, нет ожидающих взаимодействий client-tool и нет ходов продолжения в очереди. Возвращает true когда стабильно, или false если время ожидания истечёт до того, как будет обработано ожидающее взаимодействие.
const stable = await this.waitUntilStable({ timeout: 30_000 });
if (stable) {
console.log("All turns complete, safe to proceed");
}const stable = await this.waitUntilStable({ timeout: 30_000 });
if (stable) {
console.log("All turns complete, safe to proceed");
}Это особенно полезно с saveMessages для потоков, управляемых сервером:
await this.saveMessages((messages) => [...messages, syntheticUserMessage]);
await this.waitUntilStable({ timeout: 60_000 });
// The assistant has finished respondingawait this.saveMessages((messages) => [...messages, syntheticUserMessage]);
await this.waitUntilStable({ timeout: 60_000 });
// The assistant has finished respondingresetTurnState()
Прерывает активный ход и аннулирует поставленные в очередь continuations. Встроенный CF_AGENT_CHAT_CLEAR обработчик вызывает это автоматически, но при необходимости вы можете вызвать это вручную.
Хуки жизненного цикла
Переопределить onConnect и onClose чтобы добавить собственную логику. Возобновление потока и синхронизация сообщений обрабатываются автоматически:
export class ChatAgent extends AIChatAgent {
async onConnect(connection, ctx) {
// Your custom logic (e.g., logging, auth checks)
console.log("Client connected:", connection.id);
// Stream resumption and message sync are handled automatically
}
async onClose(connection, code, reason, wasClean) {
console.log("Client disconnected:", connection.id);
// Connection cleanup is handled automatically
}
}export class ChatAgent extends AIChatAgent {
async onConnect(connection, ctx) {
// Your custom logic (e.g., logging, auth checks)
console.log("Client connected:", connection.id);
// Stream resumption and message sync are handled automatically
}
async onClose(connection, code, reason, wasClean) {
console.log("Client disconnected:", connection.id);
// Connection cleanup is handled automatically
}
} destroy() метод отменяет все ожидающие запросы чата и очищает состояние потока. Он вызывается автоматически при вытеснении Durable Object, но при необходимости его можно вызвать вручную.
Отмена запроса
Когда пользователь нажимает «стоп» в интерфейсе чата, клиент отправляет CF_AGENT_CHAT_REQUEST_CANCEL сообщение. Сервер передаёт это в abortSignal в options:
export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
abortSignal: options?.abortSignal, // Pass through for cancellation
});
return result.toUIMessageStreamResponse();
}
}export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
abortSignal: options?.abortSignal, // Pass through for cancellation
});
return result.toUIMessageStreamResponse();
}
}Подклассы также могут отменять ходы изнутри Durable Object:
protected abortRequest(requestId: string, reason?: unknown): void
protected abortAllRequests(): voidИспользуйте abortRequest() когда вам известен ID запроса. Используйте abortAllRequests() для узкоспециализированных вспомогательных функций, которые должны отменять текущий выполняющийся ход. Предпочтительнее использовать SaveMessagesOptions.signal для программных ходов, когда сигнал можно передать прямо в месте вызова.
Восстановление потока
Автоматическое возобновление потока ( resume опцию для useAgentChat) это восстановление после переподключения клиента : возобновляет активный поток, когда клиент отключается и подключается снова. Это не относится к вытеснению Durable Object. Если процесс Worker или Durable Object вытесняется во время выполнения вызова модели, сам поток исчезает. Этот случай обрабатывает устойчивое восстановление чата.
Вытеснение Durable Object в середине потока необратимо разрывает соединение с LLM. Устойчивое восстановление оборачивает каждый AIChatAgent и Think ход чата в runFiber(). Fiber предоставляет автоматический keepAlive во время передачи потоком и recovery hook при перезапуске.
Строка fiber сохраняется в SQLite после вытеснения из памяти. При следующей активации framework обнаруживает прерванный fiber. Он восстанавливает частичный ответ из буферизованных фрагментов потока и вызывает onChatRecovery.
Устойчивое восстановление всегда включено. Используйте chatRecovery только для настройки бюджетов восстановления и поведения при завершении:
export class ChatAgent extends AIChatAgent {
chatRecovery = {
maxAttempts: 10,
stableTimeoutMs: 10_000,
terminalMessage: "The assistant was interrupted and could not recover.",
// Primary stuck-turn bound. Resets on every progress-bearing attempt, so a
// turn that keeps producing content survives unbounded interruption.
noProgressTimeoutMs: 5 * 60 * 1000,
// Runaway-loop guard. Defaults to 1,000. Set a higher value for a long
// agentic turn, or Infinity to remove the cap.
maxRecoveryWork: 200,
// Caller policy consulted from the second recovery attempt onward. Return
// false to stop recovery. This is where you enforce a token/cost budget.
// Note: this is called as `config.shouldKeepRecovering(ctx)`, so it is not
// bound to the agent instance — track spend in your own store keyed by the
// incident.
async shouldKeepRecovering(ctx) {
return (await getSpendForTurn(ctx.recoveryRootRequestId)) < MAX_SPEND;
},
async onExhausted(ctx) {
console.warn("Chat recovery exhausted", ctx.incidentId, ctx.reason);
},
};
}export class ChatAgent extends AIChatAgent {
override chatRecovery = {
maxAttempts: 10,
stableTimeoutMs: 10_000,
terminalMessage: "The assistant was interrupted and could not recover.",
// Primary stuck-turn bound. Resets on every progress-bearing attempt, so a
// turn that keeps producing content survives unbounded interruption.
noProgressTimeoutMs: 5 * 60 * 1000,
// Runaway-loop guard. Defaults to 1,000. Set a higher value for a long
// agentic turn, or Infinity to remove the cap.
maxRecoveryWork: 200,
// Caller policy consulted from the second recovery attempt onward. Return
// false to stop recovery. This is where you enforce a token/cost budget.
// Note: this is called as `config.shouldKeepRecovering(ctx)`, so it is not
// bound to the agent instance — track spend in your own store keyed by the
// incident.
async shouldKeepRecovering(ctx) {
return (await getSpendForTurn(ctx.recoveryRootRequestId)) < MAX_SPEND;
},
async onExhausted(ctx) {
console.warn("Chat recovery exhausted", ctx.incidentId, ctx.reason);
},
};
} chatRecovery объект принимает следующие параметры конфигурации:
| Поле | По умолчанию | Описание |
|---|---|---|
maxAttempts |
10 |
Предел числа попыток перед окончательным исчерпанием. Сбрасывается при продвижении вперёд, поэтому отслеживает зацикливание alarm без прогресса, а не полноценный длинный ход. |
stableTimeoutMs |
10_000 |
Сколько времени попытка восстановления ожидает, пока isolate не достигнет стабильного состояния, прежде чем будет запланирована повторно. |
terminalMessage |
общее сообщение | Сообщение, показываемое пользователю, когда попытки восстановления прекращаются. |
noProgressTimeoutMs |
300_000 (5 мин) |
Основной предел зависшего хода: сколько времени инцидент может провести без продвижения вперёд, прежде чем будет запечатан (no_progress_timeout). Сбрасывается при каждой попытке с прогрессом, чтобы ход, который продолжает генерировать контент, переживал неограниченное по времени прерывание. |
maxRecoveryWork |
1,000 |
Защита от неконтролируемого цикла. Максимальное количество произведённых единиц контента и вызовов инструментов с начала инцидента до того, как ещё выполняющийся ход будет опечатан. Задайте более высокое значение или Infinity для длительного агентного хода. |
maxOomRetries |
3 |
Бюджет повторных попыток при сбросах Durable Object из-за ограничения памяти. Задайте 0 чтобы остановиться после первого сброса лимита памяти. |
shouldKeepRecovering |
, | Политика вызывающей стороны учитывается начиная со второй попытки восстановления. Верните false чтобы остановить восстановление. Используйте её для соблюдения бюджета токенов или расходов. ctx.work это приблизительное количество сегментов, а не токенов, поэтому отслеживайте реальные расходы самостоятельно. |
onExhausted |
, | Вызывается один раз, когда восстановление окончательно прекращается, перед доставкой конечного сообщения. Изучите ctx.reason для объяснения причины. |
ChatRecoveryProgressContext ( ctx передаваемый в shouldKeepRecovering) содержит следующие поля:
| Поле | Тип | Описание |
|---|---|---|
incidentId |
string |
Устойчивый ID для этого инцидента восстановления. |
requestId |
string |
Идентификатор запроса для текущего продолжения (изменяется при каждом связанном продолжении). |
recoveryRootRequestId |
string |
Устойчивый ID для всей цепочки продолжений: правильный ключ для отслеживания бюджета по инциденту. |
attempt |
number |
Номер попытки для этого инцидента (2 или больше, когда выполняется этот хук). |
maxAttempts |
number |
Заданный предел количества попыток. |
recoveryKind |
"retry" | "continue" |
Повторяет ли восстановление ход пользователя без ответа или продолжает частичный ход ассистента. |
work |
number |
Грубый монотонный счётчик сегментов контента и инструментов, созданных с момента открытия инцидента (не токенов). |
ageMs |
number |
Миллисекунды астрономического времени с момента первого прерывания инцидента. |
Ход, который продвигается, переживает повторные прерывания, пока остаётся в пределах maxRecoveryWork лимит. Восстановление опечатывается одним из следующих ctx.reason значения:
no_progress_timeout: нет продвижения вперёд в пределах окна без прогресса (застрявший ход).max_attempts_exceeded: лимит попыток был исчерпан в плотном цикле будильника без прогресса.work_budget_exceeded: ход продолжал генерировать контент, но превысилmaxRecoveryWork(неконтролируемый цикл).recovery_aborted: вашshouldKeepRecoveringхук вернулfalse.out_of_memory: восстановление превысило бюджет повторов при ограничении памяти.stable_timeout: попытки восстановления раз за разом упирались в тайм-аут при ожидании стабильного состояния, пока не исчерпался бюджет (экстремальная флуктуация).
Ходы, ожидающие человека, не опечатываются
Ход может приостановиться на клиентском взаимодействии, которое не может разрешить самостоятельно: клиентский вызов инструмента (инструмент без серверного execute, результат которого клиент воспроизводит повторно), либо approval-requested часть. Такой ход ожидает человека, а не завис.
Пока взаимодействие не завершено, на ход не распространяется ни один бюджет восстановления. Окно отсутствия прогресса, лимит попыток, maxRecoveryWork, а также shouldKeepRecovering все приостановлены. Пользователь, отвечающий на запрос спустя несколько минут после того, как его прервало развёртывание, никогда не активирует пломбу. Восстановление откладывает ход вместо того, чтобы завершить его с ошибкой, и итоговое подтверждение пользователя или результат инструмента возобновляет его через обычный путь продолжения.
Это исключение действует только на стороне клиента. Серверный инструмент, чей execute() был принудительно завершён в процессе выполнения и является настоящим осиротевшим, поэтому не считается исключением и вместо этого восстанавливается через исправление транскрипта.
Мониторинг исчерпания терминалов через observability:
import { subscribe } from "agents/observability";
const unsubscribe = subscribe("chat", (event) => {
if (event.type === "chat:recovery:exhausted") {
console.error("Chat recovery exhausted", event.payload);
}
});import { subscribe } from "agents/observability";
const unsubscribe = subscribe("chat", (event) => {
if (event.type === "chat:recovery:exhausted") {
console.error("Chat recovery exhausted", event.payload);
}
});onChatRecovery
Переопределите, чтобы реализовать восстановление, специфичное для провайдера. Поведение по умолчанию сохраняет частичный ответ и планирует продолжение через continueLastTurn().
export class ChatAgent extends AIChatAgent {
async onChatRecovery(ctx) {
console.log(`Recovered ${ctx.partialText.length} chars of partial text`);
// Default: persist partial + schedule continuation
return {};
}
}import type {
ChatRecoveryContext,
ChatRecoveryOptions,
} from "@cloudflare/ai-chat";
export class ChatAgent extends AIChatAgent {
override async onChatRecovery(
ctx: ChatRecoveryContext,
): Promise<ChatRecoveryOptions> {
console.log(`Recovered ${ctx.partialText.length} chars of partial text`);
// Default: persist partial + schedule continuation
return {};
}
}ChatRecoveryContext:
| Поле | Тип | Описание |
|---|---|---|
incidentId |
string |
Устойчивый ID для этого инцидента восстановления |
attempt |
number |
Номер текущей попытки для этого инцидента, начиная с 1 |
maxAttempts |
number |
Заданный предел количества попыток до окончательного исчерпания |
recoveryKind |
"retry" | "continue" |
Повторит ли восстановление ход пользователя без ответа или продолжит частичный ход ассистента |
streamId |
string |
ID прерванного потока |
requestId |
string |
ID исходного запроса чата |
partialText |
string |
Текст, сгенерированный до вытеснения |
partialParts |
MessagePart[] |
Части сообщения (текст, reasoning, вызовы инструментов), сгенерированные до вытеснения |
recoveryData |
unknown | null |
Данные из this.stash() : полностью управляется пользователем |
messages |
ChatMessage[] |
Полная история разговора |
lastBody |
Record<string, unknown> | undefined |
Исходное тело запроса |
lastClientTools |
ClientToolSchema[] | undefined |
Схемы клиентских инструментов из исходного запроса |
createdAt |
number |
Миллисекунды эпохи, когда начался прерванный ход |
ChatRecoveryOptions:
| Поле | По умолчанию | Описание |
|---|---|---|
persist |
true |
Сохранить частичный ответ как сообщение ассистента |
continue |
true |
Запланируйте продолжение через continueLastTurn() |
Типичные возвращаемые значения:
{}: сохраняет частичный результат и продолжает автоматически (по умолчанию, работает у провайдеров, поддерживающих assistant prefill){ continue: false }: сохраняет частичный результат, но не продолжает автоматически (продолжение обрабатывайте самостоятельно){ persist: false, continue: false }: не сохраняйте незавершённый остаток и обрабатывайте всё самостоятельно (например, получайте завершённый ответ от провайдера)
Выполненная работа никогда не теряется: persist: false подавляет сохранение только тех частичных данных, которым нечего терять, поскольку в них нет ничего завершённого. Частичные данные, которые уже содержат завершённые результаты инструментов (выполненную, часто неидемпотентную работу), сохраняются в любом случае, поэтому приложение не может случайно отбросить завершённые вызовы инструментов, и ему никогда не требуется { persist: true } просто для подстраховки.
Если восстановление происходит до того, как были записаны какие-либо части потока, продолжать частичное сообщение ассистента не от чего. Если последнее сохранённое сообщение всё ещё остаётся неотвеченным сообщением пользователя из прерванного хода, фреймворк автоматически повторяет этот ход, если только continue это false.
Используйте ctx.createdAt чтобы пропускать устаревшие восстановления:
override async onChatRecovery(
ctx: ChatRecoveryContext,
): Promise<ChatRecoveryOptions> {
if (Date.now() - ctx.createdAt > 2 * 60 * 1000) {
return { continue: false };
}
return {};
}Управление автоматическим продолжением
Устойчивое ведение учёта остаётся активным, когда автоматическое продолжение неуместно.
- Верните
{ continue: false }когда повторный вызов модели небезопасен. - Сохраните намерение отмены и прочитайте его в
onChatRecovery(). - Записывайте ключи идемпотентности до внешних побочных эффектов.
- Используйте бюджеты восстановления с устойчивыми данными о расходах, чтобы ограничить затраты.
continueLastTurn
Добавляет к последнему сообщению assistant, повторно вызывая onChatMessage с сохраненным телом запроса. Ответ передается потоково как продолжение и добавляется к существующему сообщению ассистента, а не создается как новое сообщение. Синтетическое пользовательское сообщение не создается.
protected continueLastTurn(
body?: Record<string, unknown>,
options?: SaveMessagesOptions,
): Promise<SaveMessagesResult>;Вызывается автоматически стандартным путём восстановления. Также может вызываться вручную из запланированных колбэков или других точек входа. Необязательный body параметр переопределяет сохранённое тело запроса для этого продолжения. Передайте options.signal чтобы отменить продолжение во время его выполнения.
Сохранение данных восстановления в stash
Используйте this.stash() внутри onChatMessage чтобы сохранять специфичные для провайдера данные для восстановления. Stash хранится в строке SQLite fiber, отдельно от состояния агента, и доступен как ctx.recoveryData в onChatRecovery.
export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const result = streamText({
model: openai("gpt-5.4"),
messages: await convertToModelMessages(this.messages),
providerOptions: { openai: { store: true } },
includeRawChunks: true,
onChunk: ({ chunk }) => {
if (chunk.type === "raw") {
const raw = chunk.rawValue;
if (raw?.type === "response.created" && raw.response?.id) {
this.stash({ responseId: raw.response.id });
}
}
},
});
return result.toUIMessageStreamResponse();
}
}export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const result = streamText({
model: openai("gpt-5.4"),
messages: await convertToModelMessages(this.messages),
providerOptions: { openai: { store: true } },
includeRawChunks: true,
onChunk: ({ chunk }) => {
if (chunk.type === "raw") {
const raw = chunk.rawValue as {
type?: string;
response?: { id?: string };
};
if (raw?.type === "response.created" && raw.response?.id) {
this.stash({ responseId: raw.response.id });
}
}
},
});
return result.toUIMessageStreamResponse();
}
}Стратегии восстановления по провайдерам
Правильная стратегия зависит от того, поддерживает ли провайдер assistant prefill и продолжается ли генерация ответа на стороне сервера после разрыва соединения:
| Провайдер | Стратегия | Стоимость токенов |
|---|---|---|
| Workers AI | continueLastTurn() : модель продолжает работу через assistant prefill |
Низкая |
| OpenAI (Responses API) | Получить завершённый ответ по ID: экономия токенов | Ноль |
| Anthropic | Сохраните частичный результат и отправьте синтетическое сообщение пользователя для продолжения | Средняя |
Восстановление статуса на клиенте
Пока ход восстанавливается, агент рассылает cf_agent_chat_recovering фрейм статуса, чтобы клиенты могли показывать индикатор «recovering…» вместо ощущения зависания. Он устанавливается, когда запланирован recovery continuation, и снимается при каждом конечном исходе, поэтому индикатор никогда не крутится бесконечно. Получайте его через useAgentChat: isRecovering флаг (см. Возвращаемые значения). Сигнал носит рекомендательный характер и обратно совместим: клиенты, которые его не понимают, просто игнорируют его.
Восстановление транскрипта: устранение зависших вызовов инструментов (они сохраняются как результаты с ошибкой, а не удаляются, поэтому запись остаётся, а модель незаметно не запускает инструмент повторно) и нормализация некорректных или отсутствующих входных данных инструмента перед обращением к провайдеру. Эти события возникают на transcript канал наблюдаемости.
О том, как восстановление чата вписывается в общую концепцию долгоживущих агентов, читайте в Долгоживущие агенты: восстановление прерванных LLM-потоков. Для базового API fiber см. Durable Execution.
Клиентское API
useAgentChat
React-хук, который подключается к AIChatAgent через WebSocket. Оборачивает useChat с нативным транспортом WebSocket.
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const {
messages,
sendMessage,
clearHistory,
addToolOutput,
addToolApprovalResponse,
setMessages,
status,
isStreaming,
isServerStreaming,
isToolContinuation,
isRecovering,
} = useAgentChat({ agent });
// ...
}import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const {
messages,
sendMessage,
clearHistory,
addToolOutput,
addToolApprovalResponse,
setMessages,
status,
isStreaming,
isServerStreaming,
isToolContinuation,
isRecovering,
} = useAgentChat({ agent });
// ...
}Параметры
| Опция | Тип | По умолчанию | Описание |
|---|---|---|---|
agent |
ReturnType<typeof useAgent> |
Обязательный | Подключение Agent от useAgent |
onToolCall |
({ toolCall, addToolOutput }) => void |
, | Обрабатывайте выполнение инструментов на стороне клиента |
tools |
Record<string, AITool> |
, | Дополнительно: динамическая регистрация инструментов, выполняемых клиентом, из браузера |
autoContinueAfterToolResult |
boolean |
true |
Автоматически продолжать диалог после результатов клиентских инструментов и подтверждений |
resume |
boolean |
true |
Включите автоматическое возобновление потока при переподключении |
cancelOnClientAbort |
boolean |
false |
Отменяет ход сервера при возникновении общего прерывания потока клиента или очистки |
body |
object | () => object |
, | Пользовательские данные, отправляемые с каждым запросом |
prepareSendMessagesRequest |
(options) => { body?, headers? } |
, | Расширенная настройка на уровне запроса |
getInitialMessages |
(options) => Promise<UIMessage[]> или null |
, | Пользовательский загрузчик начальных сообщений. Установите значение null чтобы полностью пропустить HTTP-запрос (это полезно, если вы предоставляете messages напрямую) |
syncMessagesToServer |
boolean |
true |
Когда true, setMessages отправляет транскрипт на сервер. Установите значение false для хостов с хранением транскрипта на стороне сервера, чтобы setMessages обновляет только локальное представление |
Возвращаемые значения
| Свойство | Тип | Описание |
|---|---|---|
messages |
UIMessage[] |
Сообщения текущей беседы |
sendMessage |
(message) => void |
Отправить сообщение |
clearHistory |
() => void |
Очистить диалог (клиент и сервер) |
addToolOutput |
({ toolCallId, output }) => void |
Предоставление вывода для инструмента на стороне клиента |
addToolApprovalResponse |
({ id, approved }) => void |
Подтвердить или отклонить инструмент, требующий подтверждения |
setMessages |
(messages | updater) => void |
Устанавливайте сообщения напрямую (синхронизируется с сервером) |
status |
string |
"ready", "submitted", "streaming", или "error" |
isStreaming |
boolean |
true пока агент выполняет потоковую передачу или ожидает активный клиентский инструмент |
isServerStreaming |
boolean |
true пока выполняется инициированный сервером поток или активна фаза клиентского инструмента |
isToolContinuation |
boolean |
true пока выполняется автоматическое продолжение после результата инструмента или подтверждения |
isRecovering |
boolean |
true пока восстанавливается устойчивый ход (прерванный и возобновляемый). Не путать с isStreaming : восстанавливающийся ход ещё не генерирует токены. Отобразите подсказку «восстановление…»; большинство интерфейсов рассматривают isStreaming || isRecovering как «занято» |
Используйте isToolContinuation когда вашему UI нужно отличать новую отправку от пользователя от продолжения после результата инструмента. Например, показывайте индикатор набора текста только для status === "submitted" && !isToolContinuation, при этом элементы управления загрузкой остаются отключёнными, когда isStreaming равен true.
Инструменты
AIChatAgent поддерживает три паттерна работы с инструментами, все они используют tool() функция:
| Шаблон | Где выполняется | Когда использовать |
|---|---|---|
| На стороне сервера | Сервер (автоматически) | Вызовы API, запросы к базе данных, вычисления |
| На стороне клиента | Браузер (через onToolCall) |
Геолокация, буфер обмена, камера, локальное хранилище |
| Подтверждение | Сервер (после подтверждения пользователем) | Платежи, удаления, внешние действия |
Инструменты на стороне сервера
Инструменты с execute функцию, выполняются на сервере автоматически:
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
tools: {
getWeather: tool({
description: "Get weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => {
const data = await fetchWeather(city);
return { temperature: data.temp, condition: data.condition };
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
tools: {
getWeather: tool({
description: "Get weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => {
const data = await fetchWeather(city);
return { temperature: data.temp, condition: data.condition };
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}Инструменты на стороне клиента
Определите инструмент на сервере без execute, затем обработайте его на клиенте с помощью onToolCall. Используйте это для инструментов, которым нужны API браузера.
Сервер:
tools: {
getLocation: tool({
description: "Get the user's location from the browser",
inputSchema: z.object({}),
// No execute — the client handles it
});
}tools: {
getLocation: tool({
description: "Get the user's location from the browser",
inputSchema: z.object({}),
// No execute — the client handles it
});
}Клиент:
const { messages, sendMessage } = useAgentChat({
agent,
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "getLocation") {
const pos = await new Promise((resolve, reject) =>
navigator.geolocation.getCurrentPosition(resolve, reject),
);
addToolOutput({
toolCallId: toolCall.toolCallId,
output: { lat: pos.coords.latitude, lng: pos.coords.longitude },
});
}
},
});const { messages, sendMessage } = useAgentChat({
agent,
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "getLocation") {
const pos = await new Promise((resolve, reject) =>
navigator.geolocation.getCurrentPosition(resolve, reject),
);
addToolOutput({
toolCallId: toolCall.toolCallId,
output: { lat: pos.coords.latitude, lng: pos.coords.longitude },
});
}
},
});Когда LLM вызывает getLocation, поток приостанавливается. onToolCall колбэк срабатывает, ваш код предоставляет вывод, и разговор продолжается.
Для SDK или платформ, где браузер определяет доступные инструменты во время выполнения, передайте tools объект на useAgentChat. Инструменты с клиентской execute функции автоматически сериализуются и отправляются на сервер. На сервере options.clientTools и createToolsFromClientSchemas() по-прежнему поддерживаются для этого динамического паттерна инструментов.
Подтверждение инструмента (участие человека)
Используйте needsApproval для инструментов, которые перед выполнением требуют подтверждения пользователя.
Сервер:
tools: {
processPayment: tool({
description: "Process a payment",
inputSchema: z.object({
amount: z.coerce.number(),
recipient: z.string(),
}),
needsApproval: async ({ amount }) => amount > 100,
execute: async ({ amount, recipient }) => charge(amount, recipient),
});
}Клиент:
import { getToolName, isToolUIPart } from "ai";
import {
getToolApproval,
getToolCallId,
getToolPartState,
} from "@cloudflare/ai-chat/react";
const { messages, addToolApprovalResponse } = useAgentChat({ agent });
// Render pending approvals from message parts
{
messages.map((msg) =>
msg.parts
.filter(
(part) =>
isToolUIPart(part) && getToolPartState(part) === "waiting-approval",
)
.map((part) => (
<div key={getToolCallId(part)}>
<p>Approve {getToolName(part)}?</p>
<button
onClick={() => {
const approval = getToolApproval(part);
if (!approval) return;
addToolApprovalResponse({
id: approval.id,
approved: true,
});
}}
>
Approve
</button>
<button
onClick={() => {
const approval = getToolApproval(part);
if (!approval) return;
addToolApprovalResponse({
id: approval.id,
approved: false,
});
}}
>
Reject
</button>
</div>
)),
);
}Пользовательские сообщения об отказе с addToolOutput
Когда пользователь отклоняет инструмент, addToolApprovalResponse({ id, approved: false }) устанавливает состояние инструмента в output-denied с универсальным сообщением. Чтобы дать LLM более конкретную причину отказа, используйте addToolOutput с state: "output-error" вместо этого:
const { addToolOutput } = useAgentChat({ agent });
// Reject with a custom error message
addToolOutput({
toolCallId: part.toolCallId,
state: "output-error",
errorText: "User declined: insufficient budget for this quarter",
});const { addToolOutput } = useAgentChat({ agent });
// Reject with a custom error message
addToolOutput({
toolCallId: part.toolCallId,
state: "output-error",
errorText: "User declined: insufficient budget for this quarter",
});Это отправляет tool_result модели LLM вместе с вашим собственным текстом ошибки, чтобы она могла отреагировать соответствующим образом (например, предложить альтернативу или задать уточняющие вопросы).
addToolApprovalResponse (с approved: false) автоматически продолжает разговор, когда autoContinueAfterToolResult включён (значение по умолчанию). addToolOutput с state: "output-error" делает не auto-continue: вызовите sendMessage() впоследствии, если нужно, чтобы LLM отреагировала на ошибку.
Другие паттерны см. в Участие человека.
Пользовательские данные запроса
Включайте произвольные данные в каждый запрос чата с помощью body параметр:
const { messages, sendMessage } = useAgentChat({
agent,
body: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
userId: currentUser.id,
},
});const { messages, sendMessage } = useAgentChat({
agent,
body: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
userId: currentUser.id,
},
});Для динамических значений используйте функцию:
body: () => ({
token: getAuthToken(),
timestamp: Date.now(),
});body: () => ({
token: getAuthToken(),
timestamp: Date.now(),
});Обращайтесь к этим полям на сервере:
export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const { timezone, userId } = options?.body ?? {};
// ...
}
}export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const { timezone, userId } = options?.body ?? {};
// ...
}
}Для расширенной настройки на уровне отдельных запросов (пользовательские заголовки, разное тело запроса) используйте prepareSendMessagesRequest:
const { messages, sendMessage } = useAgentChat({
agent,
prepareSendMessagesRequest: async ({ messages, trigger }) => ({
headers: { Authorization: `Bearer ${await getToken()}` },
body: { requestedAt: Date.now() },
}),
});const { messages, sendMessage } = useAgentChat({
agent,
prepareSendMessagesRequest: async ({ messages, trigger }) => ({
headers: { Authorization: `Bearer ${await getToken()}` },
body: { requestedAt: Date.now() },
}),
});Части данных
Части данных позволяют прикреплять типизированный JSON к сообщениям наряду с текстом: индикаторы прогресса, ссылки на источники, использование токенов или любые структурированные данные, которые нужны вашему пользовательскому интерфейсу.
Запись частей данных (сервер)
Используйте createUIMessageStream с writer.write() чтобы отправлять части данных с сервера:
import {
streamText,
convertToModelMessages,
createUIMessageStream,
createUIMessageStreamResponse,
} from "ai";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
});
// Merge the LLM stream
writer.merge(result.toUIMessageStream());
// Write a data part — persisted to message.parts
writer.write({
type: "data-sources",
id: "src-1",
data: { query: "agents", status: "searching", results: [] },
});
// Later: update the same part in-place (same type + id)
writer.write({
type: "data-sources",
id: "src-1",
data: {
query: "agents",
status: "found",
results: ["Agents SDK docs", "Durable Objects guide"],
},
});
},
});
return createUIMessageStreamResponse({ stream });
}
}import {
streamText,
convertToModelMessages,
createUIMessageStream,
createUIMessageStreamResponse,
} from "ai";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
});
// Merge the LLM stream
writer.merge(result.toUIMessageStream());
// Write a data part — persisted to message.parts
writer.write({
type: "data-sources",
id: "src-1",
data: { query: "agents", status: "searching", results: [] },
});
// Later: update the same part in-place (same type + id)
writer.write({
type: "data-sources",
id: "src-1",
data: {
query: "agents",
status: "found",
results: ["Agents SDK docs", "Durable Objects guide"],
},
});
},
});
return createUIMessageStreamResponse({ stream });
}
}Три паттерна
| Шаблон | Как | Сохраняется? | Сценарий использования |
|---|---|---|---|
| Сверка | Тот же type + id → обновляет на месте |
Да | Прогрессивное состояние (поиск → найдено) |
| Добавить | Нет id, либо другой id → добавляет |
Да | Записи журнала, несколько цитирований |
| Временный | transient: true → не добавляется в message.parts |
Нет | Эфемерный статус (индикатор размышления) |
Временные части передаются подключённым клиентам в реальном времени, но исключаются из хранения в SQLite и message.parts. Используйте onData колбэк, чтобы получить их.
Чтение частей данных (клиент)
Невременные части данных отображаются в message.parts. Используйте UIMessage обобщённый тип для их типизации:
import { useAgentChat } from "@cloudflare/ai-chat/react";
const { messages } = useAgentChat({ agent });
// Typed access — no casts needed
for (const msg of messages) {
for (const part of msg.parts) {
if (part.type === "data-sources") {
console.log(part.data.results); // string[]
}
}
}import { useAgentChat } from "@cloudflare/ai-chat/react";
import type { UIMessage } from "ai";
type ChatMessage = UIMessage<
unknown,
{
sources: { query: string; status: string; results: string[] };
usage: { model: string; inputTokens: number; outputTokens: number };
}
>;
const { messages } = useAgentChat<unknown, ChatMessage>({ agent });
// Typed access — no casts needed
for (const msg of messages) {
for (const part of msg.parts) {
if (part.type === "data-sources") {
console.log(part.data.results); // string[]
}
}
}Временные части с onData
Временные части данных не входят в message.parts. Используйте onData колбэк вместо этого:
const [thinking, setThinking] = useState(false);
const { messages } = useAgentChat({
agent,
onData(part) {
if (part.type === "data-thinking") {
setThinking(true);
}
},
});const [thinking, setThinking] = useState(false);
const { messages } = useAgentChat<unknown, ChatMessage>({
agent,
onData(part) {
if (part.type === "data-thinking") {
setThinking(true);
}
},
});На сервере записывайте временные части с помощью transient: true:
writer.write({
transient: true,
type: "data-thinking",
data: { model: "glm-4.7-flash", startedAt: new Date().toISOString() },
});writer.write({
transient: true,
type: "data-thinking",
data: { model: "glm-4.7-flash", startedAt: new Date().toISOString() },
});onData срабатывает на всех путях выполнения кода: при новых сообщениях, возобновлении потока и трансляциях между вкладками.
Возобновляемый стриминг
Потоки автоматически возобновляются, когда клиент отключается и подключается заново. Никакой настройки не требуется: это работает из коробки.
Когда идёт потоковая передача:
- Все чанки буферизуются в SQLite по мере их создания
- Если клиент отключается, сервер продолжает передавать поток и буферизировать данные
- Когда клиент переподключается, он получает все буферизованные части и возобновляет потоковую передачу в реальном времени
Обычное прерывание или очистка клиентского потока по умолчанию остаются только в браузере, поэтому ход на стороне сервера продолжает выполняться и может быть возобновлён позже. Вызов stop() явно всё равно отменяет ход на сервере:
const { messages, stop } = useAgentChat({ agent });
return <button onClick={stop}>Stop</button>;const { messages, stop } = useAgentChat({ agent });
return <button onClick={stop}>Stop</button>;Задайте cancelOnClientAbort: true когда ваше приложение намеренно хочет, чтобы жизненный цикл браузера управлял жизненным циклом сервера, например в сценариях с временем жизни запроса или экономией токенов. Явный stop() всегда отменяет серверную работу независимо от этого параметра.
Отключите с помощью resume: false:
const { messages } = useAgentChat({ agent, resume: false });const { messages } = useAgentChat({ agent, resume: false });Управление хранилищем
Защита размера строки
Строки Workers SQLite имеют жёсткое ограничение размера в 2 МБ. Чтобы не превышать этот предел, AIChatAgent начинает сжимать сериализованное сообщение при достижении примерно 1.8 MB, например когда инструмент возвращает очень большой результат:
- Компактизация вывода инструмента : большие результаты работы инструмента заменяются кратким изложением, понятным LLM, которое подсказывает модели предложить повторный запуск инструмента
- Усечение текста : если сообщение остаётся слишком большим после компакции инструментов, текстовые части обрезаются с пометкой
Компактизированные сообщения включают metadata.compactedToolOutputs чтобы клиенты могли корректно обнаруживать и отображать это.
Разграничение контекста LLM и хранилища
Хранилище (maxPersistedMessages) и контекст LLM независимы друг от друга:
| Проблема | Управление | Область действия |
|---|---|---|
| Сколько сообщений хранит SQLite | maxPersistedMessages |
Персистентность |
| Что видит модель | pruneMessages() |
Контекст LLM |
| Лимиты размера строки | Автоматическая компактизация | На сообщение |
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: pruneMessages({
// LLM context limit
messages: await convertToModelMessages(this.messages),
reasoning: "before-last-message",
toolCalls: "before-last-2-messages",
}),
});
return result.toUIMessageStreamResponse();
}
}export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: pruneMessages({
// LLM context limit
messages: await convertToModelMessages(this.messages),
reasoning: "before-last-message",
toolCalls: "before-last-2-messages",
}),
});
return result.toUIMessageStreamResponse();
}
}Использование разных ИИ-провайдеров
AIChatAgent работает с любым провайдером, совместимым с AI SDK. Код сервера определяет, какую модель использовать: клиенту не нужно менять ее вручную.
Workers AI (Cloudflare)
import { createWorkersAI } from "workers-ai-provider";
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
});import { createWorkersAI } from "workers-ai-provider";
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
});OpenAI
import { createOpenAI } from "@ai-sdk/openai";
const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY });
const result = streamText({
model: openai.chat("gpt-4o"),
messages: await convertToModelMessages(this.messages),
});import { createOpenAI } from "@ai-sdk/openai";
const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY });
const result = streamText({
model: openai.chat("gpt-4o"),
messages: await convertToModelMessages(this.messages),
});Anthropic
import { createAnthropic } from "@ai-sdk/anthropic";
const anthropic = createAnthropic({ apiKey: this.env.ANTHROPIC_API_KEY });
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"),
messages: await convertToModelMessages(this.messages),
});import { createAnthropic } from "@ai-sdk/anthropic";
const anthropic = createAnthropic({ apiKey: this.env.ANTHROPIC_API_KEY });
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"),
messages: await convertToModelMessages(this.messages),
});Расширенные паттерны
С onChatMessage даёт вам полный контроль над streamText вызова вы можете напрямую использовать любые возможности AI SDK. Все паттерны ниже работают из коробки: никаких специальных AIChatAgent конфигурация требуется.
Динамическое управление моделью и инструментами
Используйте prepareStep ↗ чтобы менять модель, доступные инструменты или системный промпт между шагами в многошаговом цикле агента:
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: cheapModel, // Default model for simple steps
messages: await convertToModelMessages(this.messages),
tools: {
search: searchTool,
analyze: analyzeTool,
summarize: summarizeTool,
},
stopWhen: stepCountIs(10),
prepareStep: async ({ stepNumber, messages }) => {
// Phase 1: Search (steps 0-2)
if (stepNumber <= 2) {
return {
activeTools: ["search"],
toolChoice: "required", // Force tool use
};
}
// Phase 2: Analyze with a stronger model (steps 3-5)
if (stepNumber <= 5) {
return {
model: expensiveModel,
activeTools: ["analyze"],
};
}
// Phase 3: Summarize
return { activeTools: ["summarize"] };
},
});
return result.toUIMessageStreamResponse();
}
}import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: cheapModel, // Default model for simple steps
messages: await convertToModelMessages(this.messages),
tools: {
search: searchTool,
analyze: analyzeTool,
summarize: summarizeTool,
},
stopWhen: stepCountIs(10),
prepareStep: async ({ stepNumber, messages }) => {
// Phase 1: Search (steps 0-2)
if (stepNumber <= 2) {
return {
activeTools: ["search"],
toolChoice: "required", // Force tool use
};
}
// Phase 2: Analyze with a stronger model (steps 3-5)
if (stepNumber <= 5) {
return {
model: expensiveModel,
activeTools: ["analyze"],
};
}
// Phase 3: Summarize
return { activeTools: ["summarize"] };
},
});
return result.toUIMessageStreamResponse();
}
}prepareStep выполняется перед каждым step и может возвращать переопределения для model, activeTools, toolChoice, system, а также messages. Используйте это для:
- Смена моделей : для простых шагов используйте дешёвую модель, для рассуждений эскалируйте на более мощную
- Инструменты для фаз : ограничивает, какие инструменты доступны на каждом шаге
- Управление контекстом : обрезает или преобразует сообщения, чтобы уложиться в лимит токенов
- Принудительные вызовы инструментов, используйте
toolChoice: { type: "tool", toolName: "search" }чтобы требовать конкретный инструмент
Промежуточное ПО для языковой модели
Используйте wrapLanguageModel ↗ чтобы добавить guardrails, RAG, кэширование или логирование, не изменяя логику чата:
import { streamText, convertToModelMessages, wrapLanguageModel } from "ai";
const guardrailMiddleware = {
wrapGenerate: async ({ doGenerate }) => {
const { text, ...rest } = await doGenerate();
// Filter PII or sensitive content from the response
const cleaned = text?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED]");
return { text: cleaned, ...rest };
},
};
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const model = wrapLanguageModel({
model: baseModel,
middleware: [guardrailMiddleware],
});
const result = streamText({
model,
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}import { streamText, convertToModelMessages, wrapLanguageModel } from "ai";
import type { LanguageModelV3Middleware } from "@ai-sdk/provider";
const guardrailMiddleware: LanguageModelV3Middleware = {
wrapGenerate: async ({ doGenerate }) => {
const { text, ...rest } = await doGenerate();
// Filter PII or sensitive content from the response
const cleaned = text?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED]");
return { text: cleaned, ...rest };
},
};
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const model = wrapLanguageModel({
model: baseModel,
middleware: [guardrailMiddleware],
});
const result = streamText({
model,
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}AI SDK включает встроенные middleware:
extractReasoningMiddleware: показывает цепочку рассуждений (chain-of-thought) моделей вроде DeepSeek R1defaultSettingsMiddleware: применяет значения по умолчанию для temperature, max tokens и т. д.simulateStreamingMiddleware: добавляет потоковую передачу моделям, которые её не поддерживают
Несколько middleware применяются по порядку: middleware: [first, second] применяется как first(second(model)).
Структурированный вывод
Используйте generateObject ↗ внутри инструментов для извлечения структурированных данных:
import {
streamText,
generateObject,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: myModel,
messages: await convertToModelMessages(this.messages),
tools: {
extractContactInfo: tool({
description:
"Extract structured contact information from the conversation",
inputSchema: z.object({
text: z.string().describe("The text to extract contact info from"),
}),
execute: async ({ text }) => {
const { object } = await generateObject({
model: myModel,
schema: z.object({
name: z.string(),
email: z.string().email(),
phone: z.string().optional(),
}),
prompt: `Extract contact information from: ${text}`,
});
return object;
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}import {
streamText,
generateObject,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: myModel,
messages: await convertToModelMessages(this.messages),
tools: {
extractContactInfo: tool({
description:
"Extract structured contact information from the conversation",
inputSchema: z.object({
text: z.string().describe("The text to extract contact info from"),
}),
execute: async ({ text }) => {
const { object } = await generateObject({
model: myModel,
schema: z.object({
name: z.string(),
email: z.string().email(),
phone: z.string().optional(),
}),
prompt: `Extract contact information from: ${text}`,
});
return object;
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}Делегирование дочерним агентам в рамках процесса
Инструменты могут делегировать работу узконаправленным подвызовам с собственным контекстом. Используйте ToolLoopAgent ↗ чтобы определить многоразовый агент, а затем вызвать его из инструмента execute:
import {
ToolLoopAgent,
streamText,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
// Define a reusable research agent with its own tools and instructions
const researchAgent = new ToolLoopAgent({
model: researchModel,
instructions: "You are a research assistant. Be thorough and cite sources.",
tools: { webSearch: webSearchTool },
stopWhen: stepCountIs(10),
});
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: orchestratorModel,
messages: await convertToModelMessages(this.messages),
tools: {
deepResearch: tool({
description: "Research a topic in depth",
inputSchema: z.object({
topic: z.string().describe("The topic to research"),
}),
execute: async ({ topic }) => {
const { text } = await researchAgent.generate({
prompt: topic,
});
return { summary: text };
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}import {
ToolLoopAgent,
streamText,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
// Define a reusable research agent with its own tools and instructions
const researchAgent = new ToolLoopAgent({
model: researchModel,
instructions: "You are a research assistant. Be thorough and cite sources.",
tools: { webSearch: webSearchTool },
stopWhen: stepCountIs(10),
});
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: orchestratorModel,
messages: await convertToModelMessages(this.messages),
tools: {
deepResearch: tool({
description: "Research a topic in depth",
inputSchema: z.object({
topic: z.string().describe("The topic to research"),
}),
execute: async ({ topic }) => {
const { text } = await researchAgent.generate({
prompt: topic,
});
return { summary: text };
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}Исследовательский агент выполняется в собственном контексте: его бюджет токенов не зависит от бюджета оркестратора. Родительской модели возвращается только итоговая сводка.
Потоковая передача прогресса с промежуточными результатами
По умолчанию часть инструмента отображается как загружающаяся, пока execute возвращается. Используйте асинхронный генератор (async function*) для потоковой передачи промежуточных обновлений клиенту, пока инструмент ещё работает:
deepResearch: tool({
description: "Research a topic in depth",
inputSchema: z.object({
topic: z.string().describe("The topic to research"),
}),
async *execute({ topic }) {
// Preliminary result — the client sees "searching" immediately
yield { status: "searching", topic, summary: undefined };
const { text } = await researchAgent.generate({ prompt: topic });
// Final result — sent to the model for its next step
yield { status: "done", topic, summary: text };
},
});deepResearch: tool({
description: "Research a topic in depth",
inputSchema: z.object({
topic: z.string().describe("The topic to research"),
}),
async *execute({ topic }) {
// Preliminary result — the client sees "searching" immediately
yield { status: "searching", topic, summary: undefined };
const { text } = await researchAgent.generate({ prompt: topic });
// Final result — sent to the model for its next step
yield { status: "done", topic, summary: text };
},
});Каждый yield обновляет часть инструмента на клиенте в реальном времени (с preliminary: true). Последнее значение, возвращённое через yield, становится итоговым результатом, который видит модель.
Этот паттерн полезен, когда:
- Задача требует изучения больших объёмов информации, которые раздули бы основной контекст
- Вы хотите показывать прогресс в реальном времени для долго выполняющихся инструментов
- Вы хотите распараллелить независимые исследования (несколько вызовов инструментов выполняются одновременно)
- Вам нужны разные модели или системные промпты для разных подзадач
Подробнее см. в Документация AI SDK Agents ↗, Дочерние агенты ↗, а также Предварительные результаты работы инструмента ↗.
Синхронизация нескольких клиентов
Когда к одному и тому же экземпляру агента подключается несколько клиентов, сообщения автоматически рассылаются всем соединениям. Если один клиент отправляет сообщение, обновлённый список сообщений получают все остальные подключённые клиенты.
Client A ──── sendMessage("Hello") ────▶ AIChatAgent
│
persist + stream
│
Client A ◀── CF_AGENT_USE_CHAT_RESPONSE ──────┤
Client B ◀── CF_AGENT_CHAT_MESSAGES ──────────┘Клиент, инициировавший запрос, получает потоковый ответ. Все остальные клиенты получают финальные сообщения через CF_AGENT_CHAT_MESSAGES broadcast.
Справочник по API
Экспорты
| Путь импорта | Экспорты |
|---|---|
@cloudflare/ai-chat |
AIChatAgent, createToolsFromClientSchemas, ClientToolSchema, ChatRecoveryContext, ChatRecoveryOptions, ChatRecoveryConfig, ChatRecoveryExhaustedContext, ResolvedChatRecoveryConfig, типы жизненного цикла |
@cloudflare/ai-chat/react |
useAgentChat, extractClientToolSchemas, getToolPartState, getToolCallId, getToolInput, getToolOutput, getToolApproval |
@cloudflare/ai-chat/types |
MessageType, OutgoingMessage, IncomingMessage |
agents/chat |
Общие продвинутые примитивы чата, такие как SaveMessagesResult, SaveMessagesOptions, CHAT_MESSAGE_TYPES, ROW_MAX_BYTES, а также isReplayChunk() |
Протокол WebSocket
Протокол чата использует типизированные сообщения JSON поверх WebSocket:
| Сообщение | Направление | Назначение |
|---|---|---|
CF_AGENT_USE_CHAT_REQUEST |
Клиент → Сервер | Отправить сообщение в чат |
CF_AGENT_USE_CHAT_RESPONSE |
Сервер → Клиент | Потоковая передача частей ответа |
CF_AGENT_CHAT_MESSAGES |
Сервер → Клиент | Рассылка обновлённых сообщений |
CF_AGENT_CHAT_CLEAR |
Двунаправленный | Очистить диалог |
CF_AGENT_CHAT_REQUEST_CANCEL |
Клиент → Сервер | Отменить активный поток |
CF_AGENT_TOOL_RESULT |
Клиент → Сервер | Предоставление вывода инструмента |
CF_AGENT_TOOL_APPROVAL |
Клиент → Сервер | Подтвердить или отклонить инструмент |
CF_AGENT_MESSAGE_UPDATED |
Сервер → Клиент | Уведомить об обновлении сообщения |
CF_AGENT_STREAM_RESUMING |
Сервер → Клиент | Уведомить о возобновлении потока |
CF_AGENT_STREAM_RESUME_REQUEST |
Клиент → Сервер | Проверка возобновления потока запроса |
CF_AGENT_STREAM_RESUME_ACK |
Сервер → Клиент | Возобновить стриминг с курсора |
CF_AGENT_STREAM_RESUME_NONE |
Сервер → Клиент | Возобновляемый поток не существует |
Устаревшие API
Следующие API считаются устаревшими и при использовании выводят предупреждение в консоль. Они будут удалены в одном из будущих релизов.
| Устарело | Замена | Примечания |
|---|---|---|
addToolResult({ toolCallId, result }) |
addToolOutput({ toolCallId, output }) |
Переименовано для согласованности с терминологией AI SDK |
detectToolsRequiringConfirmation() |
Используйте needsApproval в определении инструмента |
Теперь подтверждение задаётся для каждого инструмента, а не глобальным фильтром |
toolsRequiringConfirmation параметр |
Используйте needsApproval для отдельных инструментов |
Подтверждение для отдельного инструмента заменяет глобальный список |
Если вы обновляетесь с более ранней версии, замените устаревшие вызовы их аналогами. Устаревшие API пока продолжают работать, но будут удалены в одной из будущих основных версий.
createToolsFromClientSchemas(), extractClientToolSchemas(), и tools опцию для useAgentChat по-прежнему поддерживаются для динамических клиентских инструментов. Это продвинутые API, а не устаревшие.