Autonomous responses·续
Chained reasoning
An agent can inspect its own response and decide whether to continue. This works for user-initiated messages too — you cannot predict what the user will ask, but you can react to what the agent said.
protected async onChatResponse(result: ChatResponseResult) {
if (result.status !== "completed") return;
const lastText = result.message.parts
.filter((p) => p.type === "text")
.map((p) => p.text)
.join("");
if (lastText.includes("[NEEDS_MORE_RESEARCH]")) {
await this.saveMessages((messages) => [
...messages,
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Continue your research." }],
createdAt: new Date(),
},
]);
}
}
When saveMessages is called from inside onChatResponse, the inner turn runs to completion and saveMessages returns. After the current onChatResponse call returns, the framework fires onChatResponse again for the inner response. This continues until no more work is queued. The framework never nests onChatResponse calls — results are drained sequentially.
Reactive queue processing
When queue items can be added by external events (user messages, webhooks) at any time, onChatResponse lets you drain the queue after every response regardless of who triggered it:
protected async onChatResponse(result: ChatResponseResult) {
if (result.status === "completed" && this.taskQueue.length > 0) {
const next = this.taskQueue.shift()!;
await this.saveMessages((messages) => [
...messages,
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: next }],
createdAt: new Date(),
},
]);
}
}
ChatResponseResult fields
| Field | Type | Description |
|---|---|---|
message |
UIMessage |
The finalized assistant message |
requestId |
string |
Unique ID for this turn |
continuation |
boolean |
true if this was an auto-continuation |
status |
"completed" \| "error" \| "aborted" |
How the turn ended |
error |
string \| undefined |
Error details when status is "error" |
Client-side: detecting server-initiated streams
When the server triggers a stream via saveMessages, the AI SDK's status stays "ready" because the client did not initiate the request. The useAgentChat hook provides two additional flags to handle this:
| Flag | What it tracks |
|---|---|
status |
AI SDK lifecycle: "submitted", "streaming", "ready", "error" — only for client-initiated requests |
isServerStreaming |
true when a server-initiated stream is active |
isStreaming |
true when either client or server streaming is active — use this for a universal indicator |
Use isStreaming for most UI concerns (disabling the send button, showing a loading indicator). Use isServerStreaming only when you need to distinguish between user-initiated and server-initiated streams (for example, to show a different indicator like "Agent is working in the background...").
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const { messages, sendMessage, isStreaming, isServerStreaming } =
useAgentChat({ agent });
return (
<div>
{messages.map((m) => (
<div key={m.id}>{/* render message */}</div>
))}
{isServerStreaming && <div>Agent is working in the background...</div>}
{!isServerStreaming && isStreaming && <div>Agent is responding...</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={isStreaming}>
Send
</button>
</form>
</div>
);
}
When a server-driven response arrives while the user is idle, connected clients see the new messages appear in real time. The isStreaming flag transitions from false to true to false as the stream runs, so UI elements like the send button automatically disable and re-enable.
Interaction with messageConcurrency
The messageConcurrency setting on AIChatAgent controls how overlapping user submissions behave ("queue", "latest", "merge", "drop", "debounce"). This setting only applies to sendMessage() — user-initiated messages from the client.
saveMessages() always uses serialized (queued) behavior regardless of the messageConcurrency setting. This means server-driven messages never get dropped, merged, or debounced — they always queue up and execute in order.
Combining with other Agent primitives
| Primitive | How to combine |
|---|---|
schedule() |
Schedule a callback that calls saveMessages — see the cron example above |
queue() |
Queue a method that calls saveMessages for deferred processing |
startFiber() |
Durably accept and inspect application-owned work around a message turn |
runWorkflow() |
Start a Workflow; use AgentWorkflow.agent RPC to call a method that triggers saveMessages or submitMessages |
onEmail() |
Convert email content to a chat message and call saveMessages |
onRequest() |
Handle webhooks and call saveMessages or submitMessages |
this.broadcast() |
Broadcast custom state from onChatResponse |
Cancelling a server-driven turn
Pass an AbortSignal when the same Durable Object starts and controls the turn:
const controller = new AbortController();
const result = await this.saveMessages(
[
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Run the long analysis." }],
},
],
{ signal: controller.signal },
);
if (result.status === "aborted") {
// Partial chunks already streamed are persisted.
}
const controller = new AbortController();
const result = await this.saveMessages(
[
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Run the long analysis." }],
},
],
{ signal: controller.signal },
);
if (result.status === "aborted") {
// Partial chunks already streamed are persisted.
}
continueLastTurn() accepts the same options.signal argument. AbortSignal objects cannot cross Durable Object RPC boundaries, and the signal is in memory only. If the Durable Object hibernates mid-turn, durable recovery usually continues without the original signal. For pre-stream interruptions, recovery can retry the latest unanswered user message. An abort fired after restart has no effect on the recovered turn.
Persist cancellation intent when cancellation must survive a restart. Read that state in onChatRecovery() and return { continue: false } to prevent another model call.
Use cancelSubmission(submissionId) for durable cancellation when work was accepted with submitMessages() or when cancellation must cross Worker and Durable Object RPC boundaries.
Use cancelFiber(fiberId) when the durable unit was accepted with startFiber() and the cancellation should apply to the surrounding application job rather than a Think turn.
Important notes
saveMessagesis awaitable. After it returns, the LLM has responded and the message is persisted. Use this when you control the trigger.- Use the function form of
saveMessages.saveMessages((messages) => [...messages, newMsg])reads the latest persisted messages at execution time, avoiding stale baselines when multiple calls queue up. persistMessagesdoes not trigger a response. Use it to inject context or system messages silently.onChatResponseis for reacting to turns you did not initiate. Use it for user-initiated messages, auto-continuations, or any turn where you did not callsaveMessagesyourself.onChatResponsedoes not nest. WhensaveMessagesis called from insideonChatResponse, the inner turn completes andonChatResponsefires again sequentially — not recursively.- Messages are persisted before
onChatResponsefires. If the Durable Object evicts during the hook, the conversation is safe in SQLite — only the hook callback is lost. waitUntilStable()before injecting. Always call this from schedule callbacks, webhooks, or other non-chat entry points to avoid overlapping with an in-flight stream or pending tool interaction.- The client sees the completed response before
onChatResponseruns. The server-side hook does not delay the client. messageConcurrencydoes not affectsaveMessages. Server-driven messages always queue and execute in order.
Next steps
Chat agents
Full API reference for AIChatAgent, saveMessages, persistMessages, and onChatResponse.
Schedule tasks
Delayed, cron, and interval scheduling for agent callbacks.
Webhooks
Receive webhook events and route them to agent instances.
Email routing
Handle inbound emails in your agent.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/communication-channels/chat/autonomous-responses/#page","headline":"Autonomous responses","description":"Send server-initiated messages and trigger LLM responses from Cloudflare Agents without user action.","url":"https://developers.cloudflare.com/agents/communication-channels/chat/autonomous-responses/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/communication-channels/chat/autonomous-responses/og.png?v=18349dcac05967bf","dateModified":"2026-08-20","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Build AI chat interfaces with AIChatAgent and useAgentChat, including message persistence, streaming, and tool support. title: Chat agents image: https://developers.cloudflare.com/agents/communication-channels/chat/chat-agents/og.png?v=7e7ad14729f7160e
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.
Docs