INTEGRITY Dokumentace

Chat agenti

Vytvářejte chatovací rozhraní poháněná AI pomocí AIChatAgent a useAgentChat. Zprávy se automaticky ukládají do SQLite, streamy se po odpojení obnoví a volání nástrojů fungují napříč serverem i klientem.

Přehled

@cloudflare/ai-chat balíček poskytuje dvě hlavní API:

Export Import Účel
AIChatAgent @cloudflare/ai-chat Třída agenta na straně serveru s perzistencí zpráv a streamováním
useAgentChat @cloudflare/ai-chat/react React hook pro tvorbu chatovacích uživatelských rozhraní

Pokročilé pomocné funkce jsou také k dispozici z @cloudflare/ai-chat/react, @cloudflare/ai-chat/types, a agents/chat; viz Exporty pro úplné rozhraní balíčku.

Postaveno na AI SDK ↗ a Cloudflare Durable Objects získáte:

Rychlý start

Instalovat

npm i @cloudflare/ai-chat agents ai @ai-sdk/react workers-ai-provider

Server

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();
	}
}

Klient

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

Konfigurace Wrangleru

// wrangler.jsonc
{
	"ai": { "binding": "AI" },
	"durable_objects": {
		"bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }],
	},
	"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }],
}

new_sqlite_classes migrace je nutná: AIChatAgent používá SQLite pro perzistenci zpráv a ukládání chunků streamu do vyrovnávací paměti.

Jak to funguje

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. Klient odešle zprávu přes WebSocket
  2. AIChatAgent ukládá zprávy do SQLite a volá váš onChatMessage metoda
  3. Vaše metoda vrací streamovanou Response (obvykle z streamText)
  4. Chunky se v reálném čase streamují zpět přes WebSocket
  5. Když se stream dokončí, závěrečná zpráva se uloží a odešle všem připojením

Server API

AIChatAgent

Rozšiřuje Agent z agents balíček. Spravuje stav konverzace, perzistenci a streamování.

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

Toto je hlavní metoda, kterou přepisujete. Přijímá kontext konverzace a měla by vracet Response.

Streamovaná odpověď (nejběžnější):

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();
	}
}

Odpověď v prostém textu:

export class ChatAgent extends AIChatAgent {
	async onChatMessage() {
		return new Response("Hello! I am a simple agent.", {
			headers: { "Content-Type": "text/plain" },
		});
	}
}

Přístup k vlastním datům těla a ID požadavku:

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 je true pro automatická pokračování po výsledcích nástrojů nebo schváleních, volání continueLastTurn(), a obnovená kola. Použijte ji k výběru jiného modelu, úpravě systémového promptu nebo přeskočení nákladného sestavování kontextu u navazujících kol.

this.messages

Aktuální historie konverzace, načtená ze SQLite. Jde o pole UIMessage objekty z AI SDK. Zprávy se po každé interakci automaticky ukládají.

maxPersistedMessages

Omezí počet zpráv uložených v SQLite. Když je limit překročen, nejstarší zprávy se smažou. Toto řízení se týká pouze úložiště, neovlivňuje to, co se posílá LLM.

export class ChatAgent extends AIChatAgent {
	maxPersistedMessages = 200;
}
export class ChatAgent extends AIChatAgent {
	maxPersistedMessages = 200;
}

Chcete-li ovlivnit, co se odešle modelu, použijte 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

Ovládá, zda AIChatAgent počká, až se ustálí připojení serverů MCP, a teprve poté zavolá onChatMessage. Tím se zajistí this.mcp.getAITools() vrátí úplnou sadu nástrojů, zejména po hibernaci Durable Object, kdy se připojení obnovují na pozadí.

Hodnota Chování
{ timeout: 10_000 } Čekejte až 10 sekund (výchozí)
{ timeout: N } Čekejte až N milisekund
true Čekejte neomezeně dlouho, dokud nejsou všechna připojení připravena
false Nečekat (staré chování před verzí 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;
}

Pro kontrolu na nižší úrovni volejte this.mcp.waitForConnections() přímo uvnitř vašeho onChatMessage místo toho.

messageConcurrency

Řídí, jak se chovají překrývající se uživatelská odeslání, pokud je kolo chatu již aktivní nebo zařazené ve frontě.

export class ChatAgent extends AIChatAgent {
	messageConcurrency = "queue";
}
export class ChatAgent extends AIChatAgent {
	messageConcurrency = "queue";
}
Strategie Chování
"queue" (výchozí) Zařadí každé podání do fronty a zpracuje je v pořadí
"latest" Ponechte pouze nejnovější překrývající se odeslání; nahrazená odeslání si i nadále uchovávají své uživatelské zprávy, ale nezahajují kolo modelu
"merge" Zařadí překrývající se podání do fronty a poté sloučí jejich koncové uživatelské zprávy do jednoho společného kola ještě před spuštěním posledního zařazeného kola
"drop" Překrývající se odeslání zcela ignorujte. Zprávy se neukládají.
{ strategy: "debounce", debounceMs?: number } Poslední hodnota na konci intervalu s tichým oknem (výchozí 750 ms)

Toto nastavení se týká pouze sendMessage() odeslání. Regenerace, pokračování nástrojů, schválení, vymazání a programové saveMessages() volání si zachovávají své dosavadní serializované chování.

persistMessages a saveMessages

persistMessages uchovává zprávy v SQLite a odesílá aktualizaci všem připojeným klientům, ale ne spustí tah modelu. Použijte ho, pokud chcete do konverzace vkládat zprávy, aniž byste spouštěli novou odpověď.

saveMessages ukládá zprávy a spouští onChatMessage() pro novou odpověď. Před spuštěním počká na dokončení každého probíhajícího chatového kola, takže naplánované nebo programové zprávy se nikdy nepřekrývají s právě probíhajícím streamem.

// 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 přijímá buď pole zpráv, nebo funkci, která odvodí další seznam zpráv z naposledy uložené this.messages. Když se ve frontě nahromadí více volání, použijte formu funkce, abyste se vyhnuli zastaralému výchozímu stavu:

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 vrací { requestId, status, error? } kde status je "completed" pokud kolo proběhlo, "error" pokud stream nahlásil chybu, "skipped" pokud byl chat vymazán ještě předtím, než začal, nebo "aborted" pokud externí AbortSignal ho zrušil ještě před dokončením. Když status je "error", error obsahuje chybovou zprávu streamu, je-li k dispozici.

Předejte options.signal pro zrušení programového tahu mimo chatovacího agenta. To se hodí, když potřebuje volání nadřazeného nástroje zrušit tah podřízeného agenta, aniž by znalo interně vygenerované ID požadavku:

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() přijímá stejné options.signal argument. AbortSignal objekty nemohou překročit hranice RPC Durable Object, proto vytvořte controller uvnitř Durable Object, který volá saveMessages() nebo continueLastTurn(). Signál existuje pouze v paměti. Pokud Durable Object zahibernuje uprostřed kola, obnovené kolo poběží bez původního signálu. Má-li zrušení přežít restart, uložte záměr zrušení trvale.

onChatResponse

Volá se poté, co kolo chatu vytvoří a uloží zprávu asistenta. Zámek kola se uvolní ještě před spuštěním tohoto hooku, takže je bezpečné zavolat saveMessages zevnitř. Spouští se pro cesty kola, které ukládají zprávu asistenta: požadavky chatu přes WebSocket, saveMessages, a automatické pokračování. Pokud kolo selže dříve, než vytvoří jakoukoli část odpovědi asistenta, chyba se místo toho projeví prostřednictvím původního požadavku.

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 obsahuje:

Pole Typ Popis
message UIMessage Finalizovaná zpráva asistenta z tohoto kola
requestId string ID požadavku spojené s tímto kolem
continuation boolean Zda toto kolo navazovalo na předchozí kolo asistenta
status "completed" | "error" | "aborted" Jak kolo skončilo
error string | undefined Chybová zpráva, když status je "error"

sanitizeMessageForPersistence

Přepište tuto metodu, chcete-li na zprávy použít vlastní transformace před jejich uložením do úložiště. Tento hook se spouští po vestavěné čištění dat (odstranění metadat OpenAI, oříznutí payloadu nástrojů spouštěných poskytovatelem Anthropic, filtrování prázdných reasoning částí).

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;
			}),
		};
	}
}

Pomocníci pro životní cyklus kola

Tyto metody pomáhají koordinovat programová kola a čekat na nevyřízené interakce.

hasPendingInteraction()

Vrátí true když zpráva asistenta čeká na výsledek klientského nástroje nebo na schválení.

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()

Čeká, dokud není konverzace zcela stabilní: žádný aktivní stream, žádné čekající interakce client-tool a žádná fronta pokračovacích kol. Vrací true jakmile je stabilní, nebo false pokud časový limit vyprší dříve, než se nevyřízená interakce dokončí.

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");
}

Toto je obzvlášť užitečné s saveMessages pro toky řízené serverem:

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()

Přeruší aktivní kolo a znehodnotí pokračování čekající ve frontě. Vestavěný CF_AGENT_CHAT_CLEAR handler toto volá automaticky, ale v případě potřeby to můžete zavolat i ručně.

Hooky životního cyklu

Přepsat onConnect a onClose pro přidání vlastní logiky. Obnovení streamu a synchronizaci zpráv za vás zajišťuje systém:

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() metoda zruší veškeré čekající požadavky chatu a vyčistí stav streamu. Volá se automaticky, když je Durable Object odstraněn z paměti, ale v případě potřeby ji můžete zavolat i ručně.

Zrušení požadavku

Když uživatel v uživatelském rozhraní chatu klikne na "stop", klient odešle CF_AGENT_CHAT_REQUEST_CANCEL zprávu. Server to předá do abortSignal v 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();
	}
}

Podtřídy mohou také rušit kola zevnitř Durable Object:

protected abortRequest(requestId: string, reason?: unknown): void
protected abortAllRequests(): void

Použijte abortRequest() když znáte ID požadavku. Použijte abortAllRequests() pro jednoúčelové pomocné funkce, které mají zrušit právě probíhající kolo, ať je jakékoli. Upřednostněte SaveMessagesOptions.signal pro programová kola, pokud můžete signál předat přímo v místě volání.

Obnovení streamu

Automatické obnovení streamu (resume možnost na useAgentChat) je obnovení po opětovném připojení klienta : obnoví aktivní stream, když se klient odpojí a znovu připojí. Nezahrnuje vyřazení Durable Object z paměti. Pokud je proces Workeru nebo Durable Object vyřazen z paměti během probíhajícího volání modelu, samotný stream zanikne. Tento případ řeší trvalé obnovení chatu.

Vyřazení Durable Object z paměti uprostřed streamu trvale přeruší spojení s LLM. Trvalé obnovení obaluje každé AIChatAgent a Think kolo chatu v runFiber(). Fiber poskytuje automatické keepAlive během streamování a hook pro obnovení při restartu.

Řádek fiberu přežije v SQLite i po vyřazení z paměti. Při další aktivaci framework rozpozná přerušený fiber. Sestaví částečnou odpověď z bufferovaných částí streamu a zavolá onChatRecovery.

Trvalé obnovení je vždy zapnuté. Použijte chatRecovery pouze pro ladění rozpočtů na zotavení a koncového chování:

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 objekt přijímá následující konfigurační možnosti:

Pole Výchozí Popis
maxAttempts 10 Limit počtu pokusů před trvalým vyčerpáním. Resetuje se při jakémkoli postupu, takže zachytí těsnou smyčku alarmů bez postupu, nikoli zdravé dlouhé kolo.
stableTimeoutMs 10_000 Jak dlouho pokus o obnovení čeká, než izolát dosáhne stabilního stavu, než dojde k novému naplánování.
terminalMessage obecná zpráva Zpráva zobrazená uživateli, když se od obnovení upustí.
noProgressTimeoutMs 300_000 (5 min) Primární limit pro zaseknuté kolo: jak dlouho může incident zůstat bez postupu vpřed, než je uzavřen (no_progress_timeout). Resetuje se při každém pokusu, který přináší postup, takže tah, který stále generuje obsah, přežije i neomezené přerušení.
maxRecoveryWork 1,000 Pojistka proti nekontrolované smyčce. Maximální počet vytvořených jednotek obsahu/nástrojů od začátku incidentu, než se ještě probíhající kolo uzavře. Nastavte vyšší hodnotu nebo Infinity pro dlouhé agentní kolo.
maxOomRetries 3 Rozpočet na opakování pro resety limitu paměti Durable Object. Nastavte 0 a zastavit se po prvním resetu limitu paměti.
shouldKeepRecovering , Zásady volajícího se konzultují od druhého pokusu o obnovení dál. Vraťte false a zastavit obnovení. Použijte ho k vynucení rozpočtu tokenů nebo nákladů. ctx.work je hrubý počet segmentů, nikoli tokenů, proto si skutečné výdaje sledujte sami.
onExhausted , Volá se jednou, když se obnovení vzdá, ještě před doručením koncové zprávy. Prozkoumejte ctx.reason pro důvod.

ChatRecoveryProgressContext ( ctx předané do shouldKeepRecovering) obsahuje následující pole:

Pole Typ Popis
incidentId string Stabilní ID pro tento incident obnovy.
requestId string ID požadavku pro aktuální pokračování (mění se s každým zřetězeným pokračováním).
recoveryRootRequestId string Stabilní ID pro celý řetězec pokračování: správný klíč pro sledování rozpočtu podle jednotlivých incidentů.
attempt number Číslo pokusu pro tento incident (2 nebo vyšší, pokud tento hook běží).
maxAttempts number Nakonfigurovaný limit počtu pokusů.
recoveryKind "retry" | "continue" Zda obnovení opakuje nezodpovězené uživatelské kolo, nebo pokračuje v částečném kole asistenta.
work number Hrubý, monotónní počet segmentů obsahu nebo nástrojů vytvořených od otevření incidentu (nikoli tokenů).
ageMs number Milisekundy reálného času od prvního přerušení incidentu.

Postupující kolo přežije opakovaná přerušení, dokud zůstává v rámci maxRecoveryWork limit. Obnovení se uzavře jedním z těchto ctx.reason hodnoty:

Kola čekající na člověka nejsou uzavřená

Kolo se může pozastavit na interakci s klientem, kterou nedokáže vyřešit samo: volání nástroje na straně klienta (nástroj bez serverové execute, jehož výsledek klient přehraje), nebo approval-requested část. Takové kolo čeká na člověka, není zaseknuté.

Dokud interakce čeká na vyřízení, je kolo vyňato z každého rozpočtu na obnovení. Okno bez postupu, limit počtu pokusů, maxRecoveryWork, a shouldKeepRecovering jsou všechny pozastaveny. Uživatel, kterému zabere několik minut odpovědět na dotaz přerušený nasazením, nikdy nespustí zapečetění. Zotavení dané kolo raději pozastaví, než aby jej nechalo selhat, a jakmile uživatel nakonec schválí akci nebo dorazí výsledek nástroje, kolo pokračuje běžnou cestou pokračování.

Tato výjimka platí pouze na straně klienta. Serverový nástroj, jehož execute() byl zabit uprostřed běhu, je to skutečný osiřelý záznam, takže není z opravy vyňat a místo toho se zotaví přes opravu přepisu.

Sledujte vyčerpání terminálů prostřednictvím 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

Přepište pro implementaci recovery specifického pro poskytovatele. Výchozí chování uloží částečnou odpověď a naplánuje pokračování přes 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:

Pole Typ Popis
incidentId string Stabilní ID pro tento incident obnovy
attempt number Aktuální číslo pokusu pro tento incident, počínaje 1
maxAttempts number Nakonfigurovaný limit počtu pokusů před definitivním vyčerpáním
recoveryKind "retry" | "continue" Zda obnovení zopakuje nezodpovězené uživatelské kolo, nebo bude pokračovat v částečném kole asistenta
streamId string ID přerušeného streamu
requestId string ID původního chatového požadavku
partialText string Text vygenerovaný před vyřazením z paměti
partialParts MessagePart[] Části zprávy (text, reasoning, volání nástrojů) vygenerované před vyřazením
recoveryData unknown | null Data z this.stash() : zcela pod kontrolou uživatele
messages ChatMessage[] Úplná historie konverzace
lastBody Record<string, unknown> | undefined Původní tělo požadavku
lastClientTools ClientToolSchema[] | undefined Schémata klientských nástrojů z původního požadavku
createdAt number Milisekundy epochy, kdy začalo přerušené kolo

ChatRecoveryOptions:

Pole Výchozí Popis
persist true Uložit částečnou odpověď jako zprávu asistenta
continue true Naplánujte pokračování prostřednictvím continueLastTurn()

Běžné návratové hodnoty:

Dokončená práce se nikdy neztratí: persist: false pouze potlačuje uložení částečného výsledku, který nemá co ztratit, protože nic není potvrzené. Částečný výsledek, který již obsahuje potvrzené výsledky nástrojů (dokončenou, často neidempotentní práci), se uloží vždy, takže aplikace nemůže omylem zahodit dokončená volání nástrojů, a nikdy nepotřebuje { persist: true } jen pro jistotu.

Když k obnovení dojde dříve, než se zapíší jakékoli části streamu, neexistuje žádná částečná zpráva asistenta, na kterou by šlo navázat. Pokud je poslední uložená zpráva stále nezodpovězenou zprávou uživatele z přerušeného kola, framework toto kolo automaticky zopakuje, pokud continue je false.

Použijte ctx.createdAt a přeskočit zastaralá obnovení:

override async onChatRecovery(
	ctx: ChatRecoveryContext,
): Promise<ChatRecoveryOptions> {
	if (Date.now() - ctx.createdAt > 2 * 60 * 1000) {
		return { continue: false };
	}
	return {};
}

Řiďte automatické pokračování

Trvalé vedení evidence zůstává aktivní, když automatické pokračování není vhodné.

continueLastTurn

Připojí se k poslední zprávě asistenta opětovným voláním onChatMessage s uloženým tělem požadavku. Odpověď se streamuje jako pokračování: připojí se k existující zprávě asistenta, ne k nové. Nevytváří se žádná syntetická zpráva uživatele.

protected continueLastTurn(
	body?: Record<string, unknown>,
	options?: SaveMessagesOptions,
): Promise<SaveMessagesResult>;

Volá se automaticky výchozí cestou obnovy. Lze ho také zavolat ručně z naplánovaných callbacků nebo jiných vstupních bodů. Volitelný body parametr přepisuje uložené tělo požadavku pro toto pokračování. Předejte options.signal pro zrušení pokračování během jeho běhu.

Ukládání dat pro obnovu do stashe

Použijte this.stash() uvnitř onChatMessage k trvalému uložení dat specifických pro poskytovatele za účelem zotavení. Stash se ukládá v řádku SQLite daného fiberu, odděleně od stavu agenta, a je dostupný jako ctx.recoveryData v 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();
	}
}

Strategie obnovení podle poskytovatele

Správná strategie závisí na tom, zda poskytovatel podporuje assistant prefill a zda odpověď pokračuje na straně serveru i po odpojení:

Poskytovatel Strategie Náklady na tokeny
Workers AI continueLastTurn() : model pokračuje pomocí assistant prefill Low
OpenAI (Responses API) Načíst dokončenou odpověď podle ID: bez plýtvání tokeny Nula
Anthropic Uložte částečný výsledek a odešlete syntetickou uživatelskou zprávu pro pokračování Medium

Stav obnovování na klientovi

Zatímco probíhá obnovení kola, agent vysílá cf_agent_chat_recovering stavový rámec, aby klienti mohli zobrazit indikátor „obnovuje se…“ místo toho, aby aplikace působila zaseknutě. Nastaví se, když je naplánováno pokračování obnovy, a vymaže se při každém konečném výsledku, takže se indikátor nikdy nekroutí do nekonečna. Zpracujte ho pomocí useAgentChat: isRecovering příznak (viz Návratové hodnoty). Signál je informativní a zpětně kompatibilní, klienti, kteří mu nerozumí, ho ignorují.

Opravy transkriptu, tedy zacelení osiřelých volání nástrojů (zachovaných jako chybové výsledky místo smazání, takže záznam zůstává a model nástroj tiše znovu nespustí) a normalizace poškozených nebo chybějících vstupů nástrojů před voláním poskytovatele, se odesílají na transcript kanál pozorovatelnosti.

Jak zotavení chatu zapadá do širšího konceptu dlouhoběžících agentů, se dozvíte v Dlouhodobě běžící agenti: obnova přerušených streamů LLM. Pro podkladové fiber API si přečtěte Durable Execution.

Klientské API

useAgentChat

React hook, který se připojuje k AIChatAgent přes WebSocket. Obaluje useChat s nativním transportem 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 });

	// ...
}

Možnosti

Možnost Typ Výchozí Popis
agent ReturnType<typeof useAgent> Povinné Připojení agenta z useAgent
onToolCall ({ toolCall, addToolOutput }) => void , Zpracování spouštění nástrojů na straně klienta
tools Record<string, AITool> , Pokročilé: dynamická registrace nástrojů spouštěných klientem z prohlížeče
autoContinueAfterToolResult boolean true Automaticky pokračovat v konverzaci po výsledcích klientských nástrojů a schváleních
resume boolean true Povolte automatické obnovení streamu při opětovném připojení
cancelOnClientAbort boolean false Zruší kolo serveru, když dojde k obecnému přerušení streamu na straně klienta nebo k úklidu
body object | () => object , Vlastní data odesílaná s každým požadavkem
prepareSendMessagesRequest (options) => { body?, headers? } , Pokročilé přizpůsobení pro jednotlivé požadavky
getInitialMessages (options) => Promise<UIMessage[]> nebo null , Vlastní zavaděč počáteční zprávy. Nastavte na null a zcela přeskočit HTTP požadavek (užitečné, pokud poskytujete messages přímo)
syncMessagesToServer boolean true Když true, setMessages odesílá přepis na server. Nastavte na false pro hostitele, kde je úložiště přepisů řízeno serverem, aby setMessages aktualizuje pouze lokální zobrazení

Návratové hodnoty

Vlastnost Typ Popis
messages UIMessage[] Aktuální zprávy konverzace
sendMessage (message) => void Odeslat zprávu
clearHistory () => void Vymazat konverzaci (klient a server)
addToolOutput ({ toolCallId, output }) => void Poskytnutí výstupu pro nástroj na straně klienta
addToolApprovalResponse ({ id, approved }) => void Schválit nebo zamítnout nástroj vyžadující schválení
setMessages (messages | updater) => void Nastavte zprávy přímo (synchronizuje se se serverem)
status string "ready", "submitted", "streaming", nebo "error"
isStreaming boolean true zatímco agent streamuje nebo čeká na aktivní klientský nástroj
isServerStreaming boolean true zatímco probíhá stream iniciovaný serverem nebo aktivní fáze klientského nástroje
isToolContinuation boolean true zatímco běží automatické pokračování po výsledku nástroje nebo schválení
isRecovering boolean true zatímco probíhá obnova trvalého kola (přerušeného a navazujícího). Na rozdíl od isStreaming : Obnovující se tah zatím negeneruje tokeny. Zobrazte nápovědu "obnovuje se…"; většina uživatelských rozhraní zachází s isStreaming || isRecovering jako "busy"

Použijte isToolContinuation když má vaše UI rozlišit čerstvé odeslání uživatelem od pokračování po výsledku nástroje. Zobrazte například indikátor psaní pouze pro status === "submitted" && !isToolContinuation, přičemž ovládací prvky načítání ponecháte deaktivované, kdykoli isStreaming je true.

Nástroje

AIChatAgent podporuje tři vzory nástrojů, všechny využívají AI SDK tool() funkce:

Vzor Kde běží Kdy použít
Na straně serveru Server (automaticky) volání API, databázové dotazy, výpočty
Na straně klienta Prohlížeč (přes onToolCall) Geolokace, schránka, kamera, místní úložiště
Schválení Server (po schválení uživatelem) Platby, mazání, externí akce

Nástroje na straně serveru

Nástroje s execute funkce se na serveru spouští automaticky:

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();
	}
}

Nástroje na straně klienta

Definujte nástroj na serveru bez execute, poté ho na klientovi zpracujte pomocí onToolCall. Použijte to pro nástroje, které potřebují API prohlížeče.

Server:

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

Klient:

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 },
			});
		}
	},
});

Když LLM zavolá getLocation, stream se pozastaví. Aktivuje se onToolCall callback spustí, váš kód poskytne výstup a konverzace pokračuje.

Pro SDK nebo platformy, kde prohlížeč rozhoduje o dostupných nástrojích za běhu, předejte tools objekt na useAgentChat. Nástroje s klientskými execute funkce se automaticky serializují a odesílají na server. Na serveru se options.clientTools a createToolsFromClientSchemas() zůstávají podporovány pro tento dynamický vzor nástrojů.

Schvalování nástrojů (human-in-the-loop)

Použijte needsApproval pro nástroje, které před spuštěním vyžadují potvrzení uživatele.

Server:

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),
	});
}

Klient:

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>
			)),
	);
}

Vlastní zprávy o zamítnutí s addToolOutput

Když uživatel odmítne nástroj, addToolApprovalResponse({ id, approved: false }) nastaví stav nástroje na output-denied s obecnou zprávou. Chcete-li LLM poskytnout konkrétnější důvod zamítnutí, použijte addToolOutput hodnotou state: "output-error" místo toho:

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",
});

Tím se odešle tool_result modelu LLM s vlastním textem chyby, aby mohl odpovědět přiměřeně (například navrhnout alternativu nebo položit doplňující otázky).

addToolApprovalResponse (s approved: false) automaticky pokračuje v konverzaci, když autoContinueAfterToolResult je zapnuto (výchozí nastavení). addToolOutput hodnotou state: "output-error" provádí ne automatické pokračování: zavolejte sendMessage() následně, pokud chcete, aby na chybu reagoval LLM.

Další vzory najdete v Lidský zásah.

Vlastní data požadavku

Vlastní data zahrňte do každého chatového požadavku pomocí body možnost:

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,
	},
});

Pro dynamické hodnoty použijte funkci:

body: () => ({
	token: getAuthToken(),
	timestamp: Date.now(),
});
body: () => ({
	token: getAuthToken(),
	timestamp: Date.now(),
});

Přistupte k těmto polím na serveru:

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 ?? {};
		// ...
	}
}

Pro pokročilé přizpůsobení pro jednotlivé požadavky (vlastní hlavičky, odlišné tělo pro každý požadavek) použijte 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() },
	}),
});

Datové části

Datové části vám umožňují ke zprávám připojit typovaný JSON vedle textu: indikátory průběhu, citace zdrojů, využití tokenů nebo jakákoli strukturovaná data, která vaše uživatelské rozhraní potřebuje.

Zápis částí dat (server)

Použijte createUIMessageStream hodnotou writer.write() k odesílání datových částí ze serveru:

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

Tři vzory

Vzor Jak Uloženo? Případ použití
Sesouhlasení Stejný type + id → aktualizuje na místě Ano Postupný stav (hledání → nalezeno)
Připojení Ne id, nebo jiné id → připojí Ano Záznamy protokolu, více citací
Dočasný transient: true → nepřidá se do message.parts Ne Dočasný stav (indikátor přemýšlení)

Dočasné části se v reálném čase odesílají připojeným klientům, ale jsou vyloučeny z perzistence SQLite a message.parts. Použijte onData callback, který je zpracuje.

Čtení částí dat (klient)

Trvalé datové části se objevují v message.parts. Použijte UIMessage generický typ pro jejich typování:

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

Dočasné části s onData

Dočasné datové části nejsou v message.parts. Použijte onData callback:

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);
		}
	},
});

Na serveru zapisujte dočasné části pomocí 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 se spustí ve všech větvích kódu: u nových zpráv, obnovení streamu i vysílání mezi kartami.

Obnovitelné streamování

Streamy se automaticky obnoví, jakmile se klient odpojí a znovu připojí. Není potřeba žádná konfigurace, funguje to bez dalšího nastavování.

Když probíhá streamování:

  1. Všechny chunky se při svém vzniku ukládají do vyrovnávací paměti v SQLite
  2. Pokud se klient odpojí, server dál streamuje a ukládá data do vyrovnávací paměti
  3. Když se klient znovu připojí, obdrží všechny uložené části dat a naváže na živé streamování

Obecné přerušení nebo vyčištění klientského streamu ve výchozím nastavení zůstává pouze v prohlížeči, takže kolo na straně serveru běží dál a lze jej později obnovit. Volání stop() výslovně přesto zruší kolo na serveru:

const { messages, stop } = useAgentChat({ agent });

return <button onClick={stop}>Stop</button>;
const { messages, stop } = useAgentChat({ agent });

return <button onClick={stop}>Stop</button>;

Nastavte cancelOnClientAbort: true když vaše aplikace záměrně chce, aby životní cyklus prohlížeče řídil životní cyklus serveru, například u toků vázaných na dobu trvání požadavku nebo šetřících tokeny. Explicitní stop() vždy zruší práci na serveru bez ohledu na tuto možnost.

Zakažte pomocí resume: false:

const { messages } = useAgentChat({ agent, resume: false });
const { messages } = useAgentChat({ agent, resume: false });

Správa úložiště

Ochrana velikosti řádku

Řádky Workers SQLite mají pevně danou maximální velikost 2 MB. Abyste zůstali pod tímto limitem, AIChatAgent začne komprimovat serializovanou zprávu přibližně od 1.8 MB, například když nástroj vrátí velmi velký výstup:

  1. Komprimace výstupu nástrojů : Velké výstupy nástrojů se nahradí shrnutím vhodným pro LLM, které modelu doporučí navrhnout opětovné spuštění nástroje
  2. Zkrácení textu : Pokud je zpráva i po komprimaci nástrojů stále příliš velká, textové části se zkrátí a doplní poznámkou

Komprimované zprávy obsahují metadata.compactedToolOutputs aby to klienti mohli rozpoznat a elegantně zobrazit.

Řízení kontextu LLM oproti úložišti

Úložiště (maxPersistedMessages) a kontext LLM jsou nezávislé:

Aspekt Ovládání Rozsah
Kolik zpráv SQLite ukládá maxPersistedMessages Perzistence
Co vidí model pruneMessages() Kontext LLM
Limity velikosti řádku Automatická komprimace Na zprávu
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();
	}
}

Používání různých poskytovatelů AI

AIChatAgent funguje s libovolným poskytovatelem kompatibilním s AI SDK. Který model se použije, určuje kód na serveru: klient ho nemusí ručně měnit.

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),
});

Pokročilé vzory

Protože onChatMessage vám dává plnou kontrolu nad streamText volání můžete přímo použít jakoukoli funkci AI SDK. Všechny níže uvedené vzory fungují bez úprav, není potřeba žádné speciální AIChatAgent konfigurace je nutná.

Dynamické řízení modelu a nástrojů

Použijte prepareStep ↗ pro změnu modelu, dostupných nástrojů nebo systémového promptu mezi kroky ve vícekrokové smyčce agenta:

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 se spustí před každým krokem a může vrátit přepsání pro model, activeTools, toolChoice, system, a messages. Použijte ho k:

Middleware jazykového modelu

Použijte wrapLanguageModel ↗ pro přidání guardrails, RAG, cachování nebo logování, aniž byste museli upravovat logiku chatu:

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 obsahuje vestavěné middlewary:

Middlewary se skládají v pořadí: middleware: [first, second] platí jako first(second(model)).

Strukturovaný výstup

Použijte generateObject ↗ uvnitř nástrojů pro strukturovanou extrakci dat:

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();
	}
}

Delegování dílčích agentů v rámci procesu

Nástroje mohou delegovat práci na zaměřená dílčí volání s vlastním kontextem. Použijte ToolLoopAgent ↗ k definování znovupoužitelného agenta a poté ho volejte z nástroje 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();
	}
}

Výzkumný agent běží ve vlastním kontextu, jeho rozpočet tokenů je oddělený od rozpočtu orchestrátoru. Zpět k nadřazenému modelu se vrací pouze shrnutí.

Streamování průběhu s předběžnými výsledky

Ve výchozím nastavení se tool part zobrazuje jako načítající se, dokud execute vrátí. Použijte asynchronní generátor (async function*) ke streamování průběžných aktualizací klientovi, zatímco nástroj ještě pracuje:

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 };
	},
});

Každý yield aktualizuje část nástroje na klientovi v reálném čase (s preliminary: true). Poslední hodnota vrácená pomocí yield se stává konečným výstupem, který model vidí.

Tento vzor se hodí, když:

Další informace najdete v dokumentace AI SDK Agents ↗, Podřízení agenti ↗, a Předběžné výsledky nástroje ↗.

Synchronizace více klientů

Když se k téže instanci agenta připojí více klientů, zprávy se automaticky odesílají všem připojením. Když jeden klient odešle zprávu, aktualizovaný seznam zpráv obdrží všichni ostatní připojení klienti.

Client A ──── sendMessage("Hello") ────▶ AIChatAgent
                                              │
                                        persist + stream
                                              │
Client A ◀── CF_AGENT_USE_CHAT_RESPONSE ──────┤
Client B ◀── CF_AGENT_CHAT_MESSAGES ──────────┘

Klient, který požadavek odeslal, dostává streamovanou odpověď. Všichni ostatní klienti dostávají finální zprávy prostřednictvím CF_AGENT_CHAT_MESSAGES vysílání.

Referenční dokumentace API

Exporty

Cesta importu Exporty
@cloudflare/ai-chat AIChatAgent, createToolsFromClientSchemas, ClientToolSchema, ChatRecoveryContext, ChatRecoveryOptions, ChatRecoveryConfig, ChatRecoveryExhaustedContext, ResolvedChatRecoveryConfig, typy životního cyklu
@cloudflare/ai-chat/react useAgentChat, extractClientToolSchemas, getToolPartState, getToolCallId, getToolInput, getToolOutput, getToolApproval
@cloudflare/ai-chat/types MessageType, OutgoingMessage, IncomingMessage
agents/chat Sdílené pokročilé primitivy chatu, jako například SaveMessagesResult, SaveMessagesOptions, CHAT_MESSAGE_TYPES, ROW_MAX_BYTES, a isReplayChunk()

Protokol WebSocket

Protokol chatu používá typované zprávy JSON přes WebSocket:

Zpráva Směr Účel
CF_AGENT_USE_CHAT_REQUEST Klient → Server Odeslat chatovou zprávu
CF_AGENT_USE_CHAT_RESPONSE Server → Klient Streamování částí odpovědi
CF_AGENT_CHAT_MESSAGES Server → Klient Rozesílání aktualizovaných zpráv
CF_AGENT_CHAT_CLEAR Obousměrné Vymazat konverzaci
CF_AGENT_CHAT_REQUEST_CANCEL Klient → Server Zrušení aktivního streamu
CF_AGENT_TOOL_RESULT Klient → Server Poskytnutí výstupu nástroje
CF_AGENT_TOOL_APPROVAL Klient → Server Schválit nebo zamítnout nástroj
CF_AGENT_MESSAGE_UPDATED Server → Klient Upozornit na aktualizaci zprávy
CF_AGENT_STREAM_RESUMING Server → Klient Upozornit na obnovení streamu
CF_AGENT_STREAM_RESUME_REQUEST Klient → Server Kontrola obnovení proudu požadavku
CF_AGENT_STREAM_RESUME_ACK Server → Klient Obnovit proud od kurzoru
CF_AGENT_STREAM_RESUME_NONE Server → Klient Neexistuje žádný obnovitelný stream

Zastaralá API

Následující API jsou zastaralá a při použití vypíšou varování do konzole. V některé z budoucích verzí budou odstraněna.

Zastaralé Náhrada Poznámky
addToolResult({ toolCallId, result }) addToolOutput({ toolCallId, output }) Přejmenováno kvůli konzistenci s terminologií AI SDK
detectToolsRequiringConfirmation() Použijte needsApproval na definici nástroje Schvalování je nyní nastaveno pro jednotlivé nástroje, nikoli jako globální filtr
toolsRequiringConfirmation možnost Použijte needsApproval na jednotlivých nástrojích Schválení pro jednotlivé nástroje nahrazuje globální seznam

Pokud přecházíte ze starší verze, nahraďte zastaralá volání jejich náhradami. Zastaralá API stále fungují, ale v některé z budoucích hlavních verzí budou odstraněna.

createToolsFromClientSchemas(), extractClientToolSchemas(), a tools možnost na useAgentChat jsou stále podporovány pro dynamické nástroje na straně klienta. Jde o pokročilá API, nikoli o zastaralá API.

Další kroky

Client SDK

hook useAgent a třída AgentClient.

Lidský zásah

Postupy schvalování a vzory ruční intervence.

Sestavte chatovacího agenta

Podrobný tutoriál krok za krokem pro vytvoření vašeho prvního chatovacího agenta.

Trvalé provádění

runFiber(), stash() a zotavení po havárii pro dlouhotrvající úlohy.

Dlouhodobě běžící agenti

Životní cyklus, vzory obnovení a strategie specifické pro daného poskytovatele.