Files
dotfiles/dot_claude/skills/agents-sdk/references/streaming-chat.md
T
Prad NukalaandClaude Fable 5 20891f3910 config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config
gitconfig: name/email from chezmoi data, gh credential helper by PATH
lookup (portable to darwin). claude: no credentials, history, projects,
sessions, or plugin caches. ssh config as private_ (0600).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:53:46 -04:00

5.0 KiB

Streaming Chat with AIChatAgent

Fetch https://developers.cloudflare.com/agents/api-reference/chat-agents/ for complete documentation.

AIChatAgent from @cloudflare/ai-chat provides streaming chat with automatic message persistence and resumable streams.

Basic Chat Agent

import { AIChatAgent } from "@cloudflare/ai-chat";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";

export class Chat extends AIChatAgent<Env> {
  async onChatMessage(onFinish, options) {
    const result = streamText({
      model: openai("gpt-4o"),
      system: "You are a helpful assistant.",
      messages: await convertToModelMessages(this.messages),
      abortSignal: options?.abortSignal,
      onFinish
    });
    return result.toUIMessageStreamResponse();
  }
}

Important: Always pass abortSignal and onFinish — they enable proper cleanup and message persistence.

With Tools

import { tool } from "ai";
import { z } from "zod";

const tools = {
  getWeather: tool({
    description: "Get weather for a location",
    parameters: z.object({ location: z.string() }),
    execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`
  })
};

export class Chat extends AIChatAgent<Env> {
  async onChatMessage(onFinish, options) {
    const result = streamText({
      model: openai("gpt-4o"),
      messages: await convertToModelMessages(this.messages),
      tools,
      abortSignal: options?.abortSignal,
      onFinish
    });
    return result.toUIMessageStreamResponse();
  }
}

With Workers AI (no API keys)

import { createWorkersAI } from "workers-ai-provider";

export class Chat extends AIChatAgent<Env> {
  async onChatMessage(onFinish, options) {
    const workersai = createWorkersAI({ binding: this.env.AI });
    const result = streamText({
      model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),
      messages: await convertToModelMessages(this.messages),
      abortSignal: options?.abortSignal,
      onFinish
    });
    return result.toUIMessageStreamResponse();
  }
}

Custom UI Message Stream

For more control, use createUIMessageStream:

import { createUIMessageStream, createUIMessageStreamResponse } from "ai";

export class Chat extends AIChatAgent<Env> {
  async onChatMessage(onFinish) {
    const stream = createUIMessageStream({
      execute: async ({ writer }) => {
        const result = streamText({
          model: openai("gpt-4o"),
          messages: await convertToModelMessages(this.messages),
          onFinish
        });
        writer.merge(result.toUIMessageStream());
      }
    });
    return createUIMessageStreamResponse({ stream });
  }
}

Resumable Streaming

Streams automatically resume if client disconnects and reconnects:

  1. Chunks buffered to SQLite during streaming
  2. On reconnect, buffered chunks sent immediately
  3. Live streaming continues from where it left off

Enabled by default. To disable:

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

React Client

import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";

function ChatUI() {
  const agent = useAgent({
    agent: "Chat",
    name: "my-chat-session"
  });

  const { 
    messages, 
    input, 
    handleInputChange, 
    handleSubmit, 
    status 
  } = useAgentChat({ agent });

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong> {m.content}
        </div>
      ))}
      
      <form onSubmit={handleSubmit}>
        <input 
          value={input} 
          onChange={handleInputChange}
          disabled={status === "streaming"}
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Streaming RPC Methods

For non-chat streaming, use @callable({ streaming: true }):

import { Agent, callable, StreamingResponse } from "agents";

export class MyAgent extends Agent<Env> {
  @callable({ streaming: true })
  async streamData(stream: StreamingResponse, query: string) {
    for (let i = 0; i < 10; i++) {
      stream.send(`Result ${i}: ${query}`);
      await sleep(100);
    }
    stream.close();
  }
}

Client receives streamed messages via WebSocket RPC.

Key Properties

Property Purpose
this.messages All persisted messages
maxPersistedMessages Limit stored messages (prune oldest)
messageConcurrency "queue" (default), "latest", "merge", "drop"
chatRecovery "persist" (default) or "continue" on reconnect
waitForMcpConnections Wait for MCP servers before first turn

Status Values

useAgentChat status:

Status Meaning
ready Idle, ready for input
streaming Response streaming
submitted Request sent, waiting
error Error occurred

Also: isStreaming, isServerStreaming for distinguishing user vs server-initiated streams.