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

Чат-агенты

Создавайте чат-интерфейсы на базе ИИ с помощью 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 вы получаете:

быстрый старт

Установить

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)
  1. Клиент отправляет сообщение через WebSocket
  2. AIChatAgent сохраняет сообщения в SQLite и вызывает ваш onChatMessage метод
  3. Ваш метод возвращает потоковый Response (обычно от streamText)
  4. Чанки потоково передаются обратно через WebSocket в реальном времени
  5. Когда поток завершается, итоговое сообщение сохраняется и рассылается всем соединениям

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 responding
await this.saveMessages((messages) => [...messages, syntheticUserMessage]);
await this.waitUntilStable({ timeout: 60_000 });
// The assistant has finished responding

resetTurnState()

Прерывает активный ход и аннулирует поставленные в очередь 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 значения:

Ходы, ожидающие человека, не опечатываются

Ход может приостановиться на клиентском взаимодействии, которое не может разрешить самостоятельно: клиентский вызов инструмента (инструмент без серверного 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()

Типичные возвращаемые значения:

Выполненная работа никогда не теряется: 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 {};
}

Управление автоматическим продолжением

Устойчивое ведение учёта остаётся активным, когда автоматическое продолжение неуместно.

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 срабатывает на всех путях выполнения кода: при новых сообщениях, возобновлении потока и трансляциях между вкладками.

Возобновляемый стриминг

Потоки автоматически возобновляются, когда клиент отключается и подключается заново. Никакой настройки не требуется: это работает из коробки.

Когда идёт потоковая передача:

  1. Все чанки буферизуются в SQLite по мере их создания
  2. Если клиент отключается, сервер продолжает передавать поток и буферизировать данные
  3. Когда клиент переподключается, он получает все буферизованные части и возобновляет потоковую передачу в реальном времени

Обычное прерывание или очистка клиентского потока по умолчанию остаются только в браузере, поэтому ход на стороне сервера продолжает выполняться и может быть возобновлён позже. Вызов 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, например когда инструмент возвращает очень большой результат:

  1. Компактизация вывода инструмента : большие результаты работы инструмента заменяются кратким изложением, понятным LLM, которое подсказывает модели предложить повторный запуск инструмента
  2. Усечение текста : если сообщение остаётся слишком большим после компакции инструментов, текстовые части обрезаются с пометкой

Компактизированные сообщения включают 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. Используйте это для:

Промежуточное ПО для языковой модели

Используйте 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:

Несколько 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, а не устаревшие.

Следующие шаги

Клиентский SDK

хук useAgent и класс AgentClient.

Участие человека

Потоки подтверждения и паттерны ручного вмешательства.

Создайте чат-агента

Пошаговый туториал по созданию вашего первого чат-агента.

Устойчивое выполнение

runFiber(), stash() и восстановление после сбоя для долго выполняющихся задач.

Долгоживущие агенты

Жизненный цикл, шаблоны восстановления и стратегии для конкретных провайдеров.