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

  • saveMessages is 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.
  • persistMessages does not trigger a response. Use it to inject context or system messages silently.
  • onChatResponse is for reacting to turns you did not initiate. Use it for user-initiated messages, auto-continuations, or any turn where you did not call saveMessages yourself.
  • onChatResponse does not nest. When saveMessages is called from inside onChatResponse, the inner turn completes and onChatResponse fires again sequentially — not recursively.
  • Messages are persisted before onChatResponse fires. 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 onChatResponse runs. The server-side hook does not delay the client.
  • messageConcurrency does not affect saveMessages. 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

Autonomous responses·续 - 图1Docs

{"@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


Skip to content

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.