Quickstart

The AI components give your React app the screens of an AI assistant: a conversation that streams the agent's replies, a prompt composer with attachments and dictation, the agent's reasoning and tool calls, and a face for a voice agent. They ship with stream-chat-react in its stream-chat-react/ai-components entry point. The agent behind them runs on Stream Agents: you describe it once, with its instructions, knowledge, tools and voice, and your app talks to it with the @stream-io/stream-agents SDK.

This page shows what you can build, how the pieces fit together, and takes you from an empty project to a working assistant.

What you can build

You want to build Start with
An AI-first app, like ChatGPT or Claude: one user, one assistant, many conversations This page, then the Personal assistant use case
A support agent that answers from your help center, looks up orders and hands over to your team The Customer support agent use case
A voice agent the user talks to, with a visual presence and a written transcript The Voice agent use case
An agent that shows its work and asks before it acts Reasoning and tool calls
An agent that runs tools in the browser, on the page the user has open The Personal assistant use case
Replies that are UI, such as a trip plan with a map and a form Generative UI
AI replies in a chat you already built with MessageList SDK integration

Voice runs through all of them. The composer has a microphone button that dictates into it, and the same agent can hold the conversation out loud: VoiceSession puts it on a call with the user, VoiceGlow or VoiceWave shows what it's doing, and AIConversation writes out what was said.

How it fits together

Your app never talks to the model, and it doesn't run the agent. It opens a session with your agent and asks it things. The agent runs on Stream's router, which calls the model, runs the agent's tools and writes the conversation into a Stream Chat channel as it goes. The components show that channel:

sequenceDiagram
    participant App as React app
    participant Router as Stream Agents
    participant LLM as Model
    participant Chat as Stream Chat
    App->>Router: sessions.create() with @stream-io/stream-agents
    App->>Router: responses.create("How do I...?")
    Router->>LLM: The conversation so far
    LLM-->>Router: Reasoning, tool calls, tokens
    Router-->>Chat: The question, and the reply as it streams, with its steps
    Chat-->>App: The channel's messages
    Note over App: AIConversation shows them
  • @stream-io/stream-agents signs the user in, opens sessions, asks, stops a reply, answers the questions of tools that ask first, and runs the tools your page gives the agent.
  • stream-chat-react/ai-components shows it all: AIConversation and AIMessageComposer, the reasoning and tool calls of each reply, the UI the agent writes, and the voice visualizations.
  • Your agent lives in a folder of plain files that you sync with the Stream CLI: its instructions, knowledge, skills, tools, connectors and models.

For voice, VoiceSession starts the agent on a call and joins it from the browser. The agent hears the user and answers out loud, and what is said is written into the session's channel.

Before you start

Step 1: Create the agent

Install the beta CLI, pick your Stream app, and make an agent from the text template:

curl -fsSL https://getstream.io/cli/beta/install.sh | bash
getstream init                                     # pick or create a Stream app
getstream agents init my-agent --template text     # or --template voice

That creates agents/my-agent/, with the agent's settings in agent.yaml, its system prompt in instructions.md, and a guardrail in guardrail.md. The template's guardrail turns away anything that isn't about Acme Corp, its example company, so write what the agent is for in both files, then sync it to Stream and try it in the terminal:

getstream agents sync my-agent
getstream agents test my-agent

The Stream Agents quickstart covers the agent's folder: knowledge, skills, tools, connectors and guardrails.

Step 2: Install the SDKs

Add @stream-io/stream-agents, the client for your agent, and stream-chat-react, which has the views. stream-chat is the Stream Chat client that reads the conversation the agent writes:

npm install @stream-io/stream-agents stream-chat stream-chat-react

This page uses stream-chat-react v15, with stream-chat v10. The stream-chat-react/ai-components entry point needs no other package. Load its stylesheet next to the default SDK styles, once, where your app starts:

import "stream-chat-react/dist/css/index.css";
import "stream-chat-react/dist/css/ai-components.css";

If your agent answers with generative UI, also install the packages of the format it writes, A2UI or OpenUI, as that guide describes. The composer's microphone button dictates with the browser's Web Speech API, so it needs no package, and it renders nothing in browsers without it, such as Firefox.

Is your agent on a backend of your own? Skip @stream-io/stream-agents, and have your backend write the agent's replies into a Stream Chat channel, by hand or with the Stream Chat AI SDK or the Stream Chat LangChain SDK. Conversation state describes what it sends: the ai_generated and generating fields of a reply, and the AI indicator events that say what the agent is doing. The client keeps the latest of those on the channel, as one of the states stream-chat exports as AIStates:

  • AIStates.Thinking, AI_STATE_THINKING: the model is working out an answer.
  • AIStates.ExternalSources, AI_STATE_EXTERNAL_SOURCES: the model is checking external sources, such as with a tool call.
  • AIStates.Generating, AI_STATE_GENERATING: the answer is being written into the message.
  • AIStates.Error, AI_STATE_ERROR: the reply failed.
  • AIStates.Stop, AI_STATE_STOP: the user asked to stop the reply.
  • AIStates.Idle, AI_STATE_IDLE: nothing is happening.

The useAIState hook from stream-chat-react reads it.

Step 3: Sign the user in

Create one Client for your app with your API key, and set the user once they sign in. The user's Stream token comes from your backend, which mints it with your API secret, as described in authentication. The client asks for it again when it expires:

agent.ts
import { Client } from "@stream-io/stream-agents";

export const api = new Client({ apiKey: "your_api_key" });

export async function signIn(user: { id: string; name: string }) {
  await api.setUser(user, () =>
    fetch("/api/stream-token").then((response) => response.text()),
  );
}

To try things out before you have a backend, mint a token for a test user with the CLI, getstream token jane --ttl 2h, and pass it as api.setUser({ id: "jane" }, "...").

Step 4: Connect a conversation to the agent

A session is one conversation with your agent, and its channel is where the agent writes it: the user's questions, each reply as it streams, and the reasoning and tool calls of each reply as the reply's attachments. Add a function to agent.ts that opens a session and its channel:

agent.ts
import type { Session } from "@stream-io/stream-agents";
// Lets session.chat() connect to Stream Chat. Import it once.
import "@stream-io/stream-agents/chat";
import type { Channel, StreamChat } from "stream-chat";

export type AgentChat = {
  session: Session;
  client: StreamChat;
  channel: Channel;
};

/** Opens a conversation with the agent, or the channel of one already open. */
export async function openConversation(session?: Session): Promise<AgentChat> {
  const opened = session ?? (await api.agent("my-agent").sessions.create());
  // Connects to Stream Chat as the user signIn() set.
  const { client, channel } = await opened.chat();
  const agentChannel = channel as Channel;
  await agentChannel.watch();
  return {
    session: opened,
    client: client as StreamChat,
    channel: agentChannel,
  };
}

The views show AIMessage values. fromStreamMessage turns each of the channel's messages into one: a reply is a message with custom.ai_generated: true, it's being written while custom.generating is true, and its steps are its ai_reasoning and ai_tool_call attachments. The channel's message paginator holds its messages oldest first, which is the order the conversation wants:

useAgentMessages.ts
import { useMemo } from "react";
import type { Channel, MessagePaginatorState } from "stream-chat";
import { useStateStore } from "stream-chat-react";
import { fromStreamMessage } from "stream-chat-react/ai-components";

const itemsSelector = (state: MessagePaginatorState) => ({
  items: state.items,
});

export function useAgentMessages(channel: Channel) {
  const { items } = useStateStore(
    channel.messagePaginator.state,
    itemsSelector,
  );
  return useMemo(
    () => (items ?? []).map((message) => fromStreamMessage(message)),
    [items],
  );
}

Step 5: Show the conversation

Show the conversation with AIConversation, and the composer below it. The composer asks through the session, and the router writes the question into the channel itself. The approver answers the tools that ask before they run:

AgentConversation.tsx
import { useMemo, useSyncExternalStore, type FormEvent } from "react";
import { Button, Chat } from "stream-chat-react";
import {
  AIClientIdentity,
  AIConversation,
  AIMessageComposer,
  type AIToolApprover,
} from "stream-chat-react/ai-components";

import type { AgentChat } from "./agent";
import { useAgentMessages } from "./useAgentMessages";

export const AgentConversation = ({ chat }: { chat: AgentChat }) => {
  const { session, client, channel } = chat;
  const messages = useAgentMessages(channel);
  // What the agent is doing: idle, listening, responding, working or ended.
  const { kind } = useSyncExternalStore(
    session.state.subscribe,
    session.state.get,
  );
  const isAnswering = kind === "responding" || kind === "working";

  const approver = useMemo<AIToolApprover>(
    () => ({
      userId: client.userId ?? "",
      clientId: AIClientIdentity.installId(),
      decide: async (call, allowed) => session.decide(call.id, { allowed }),
    }),
    [client, session],
  );

  const ask = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const text = String(new FormData(event.currentTarget).get("message") ?? "");
    if (!text.trim() || isAnswering) return;
    event.currentTarget.reset();
    void session.responses.create(text);
  };

  return (
    <Chat client={client}>
      {/* `str-chat` is the SDK's theme scope: its fonts, colors and spacing. */}
      <div
        className="str-chat"
        style={{
          display: "flex",
          flexDirection: "column",
          height: "100dvh",
          maxWidth: "48rem",
          marginInline: "auto",
        }}
      >
        <div style={{ flex: 1, minHeight: 0 }}>
          <AIConversation approver={approver} messages={messages} />
        </div>
        <div style={{ padding: "0 1rem 1rem" }}>
          <AIMessageComposer onSubmit={ask}>
            <AIMessageComposer.TextInput name="message" />
            {/* The composer stacks its children: put the buttons in a row. */}
            <div
              style={{
                display: "flex",
                gap: "0.25rem",
                justifyContent: "flex-end",
              }}
            >
              <AIMessageComposer.SpeechToTextButton />
              {isAnswering ? (
                <Button
                  appearance="outline"
                  onClick={() => session.interrupt()}
                  size="sm"
                  type="button"
                  variant="secondary"
                >
                  Stop
                </Button>
              ) : (
                <AIMessageComposer.SubmitButton />
              )}
            </div>
          </AIMessageComposer>
        </div>
      </div>
    </Chat>
  );
};

AIConversation scrolls inside itself, so give it a height, here as the growing child of a column. The composer's form grows to fill a flex container it's placed in, so it sits in a div of its own below the conversation, and it stacks what's inside it, so its buttons go in a row of their own. The column is at most 48rem wide and centered, so lines stay short enough to read on a wide window.

The screen renders inside an element with the str-chat class, the SDK's theme scope. Inside it, the AI components and the SDK's Button take the SDK's fonts, colors and spacing. Chat provides the client and the translations but renders no element, so without one the page keeps the browser's default font. Chat gives the components the SDK's translations, as described in Localization.

Sign the user in, then open a conversation, for example from a button:

App.tsx
import { useState } from "react";
import { Button } from "stream-chat-react";

import { openConversation, signIn, type AgentChat } from "./agent";
import { AgentConversation } from "./AgentConversation";

export const App = () => {
  const [chat, setChat] = useState<AgentChat>();

  const start = async () => {
    await signIn({ id: "jane", name: "Jane" });
    setChat(await openConversation());
  };

  return chat ? (
    <AgentConversation chat={chat} />
  ) : (
    <div className="str-chat">
      <Button
        appearance="solid"
        onClick={() => void start()}
        size="md"
        type="button"
        variant="primary"
      >
        Ask the agent
      </Button>
    </div>
  );
};

Run the app and ask something. While the agent works on a reply that shows nothing yet, the conversation shows it thinking. Then the reply writes itself out, with Markdown, code and charts, and with the reasoning and tool calls that led to it. While it writes, the Stop button stops it, and under each finished reply the user can copy and share it.

The Conversation view guide covers what else the screen can show, such as the question while it's sent, older messages, and an empty state. The Composer guide adds attachments and chat options.

Step 6: Add voice

The composer already takes voice: its microphone button dictates into it.

To let the user talk to the agent instead, start a voice session. The agent joins a call right away, and the page joins it with the microphone on. VoiceSession lives in the @stream-io/stream-agents/video entry point, and the Video React SDK plays the agent's audio:

npm install @stream-io/video-react-sdk

Give the agent a face with VoiceGlow, which moves differently while the agent is connecting, listening, thinking and speaking:

AgentVoice.tsx
import { VoiceSession } from "@stream-io/stream-agents/video";
import {
  ParticipantsAudio,
  StreamCall,
  StreamVideo,
  useCallStateHooks,
} from "@stream-io/video-react-sdk";
import { useEffect, useSyncExternalStore } from "react";
import { Button } from "stream-chat-react";
import { VoiceGlow } from "stream-chat-react/ai-components";

import { api } from "./agent";

/** Starts a session with voice on. Call it from a click. */
export const startVoice = () => VoiceSession.start(api.agent("my-agent"));

export const AgentVoice = ({ voice }: { voice: VoiceSession }) => {
  const { call, client, muted, status } = useSyncExternalStore(
    voice.subscribe,
    voice.get,
  );

  // Joins the call with the microphone on. Run twice, it joins once.
  useEffect(() => {
    voice.join().catch((error: unknown) => console.error(error));
  }, [voice]);

  return (
    <div className="str-chat">
      <VoiceGlow state={status} style={{ width: 240, height: 240 }} />
      {call && client && (
        <StreamVideo client={client}>
          <StreamCall call={call}>
            <AgentAudio />
          </StreamCall>
        </StreamVideo>
      )}
      <Button
        appearance="outline"
        onClick={() => void voice.setMuted(!muted)}
        size="md"
        type="button"
        variant="secondary"
      >
        {muted ? "Unmute" : "Mute"}
      </Button>
      {/* Leaves the call and ends the session startVoice() opened. */}
      <Button
        appearance="solid"
        onClick={() => void voice.leave()}
        size="md"
        type="button"
        variant="danger"
      >
        Hang up
      </Button>
    </div>
  );
};

/** Plays the agent. */
const AgentAudio = () => {
  const { useRemoteParticipants } = useCallStateHooks();
  return <ParticipantsAudio participants={useRemoteParticipants()} />;
};

Call startVoice() from a click: a browser asks for the microphone and plays audio only after one. join() rejects when joining fails, for example when the user denies the microphone.

What is said on the call is written into the session's channel, so openConversation(voice.session) and the views from step 5 show it as a transcript. The Voice agent use case builds the whole screen: the agent's state, the visualizations, mute and hang up, and what was said written out as a conversation. An agent made with --template voice also lets you pick its speech-to-text and text-to-speech models, its voice and its greeting.

Theming

The AI components follow the stream-chat-react theme. Inside .str-chat they pick up the SDK's light and dark theme tokens, and outside of it they fall back to built-in light colors.

To change them, set the --str-chat__ai-* CSS variables. The stylesheet never declares these variables itself, so your values win at any scope, including :root:

:root {
  --str-chat__ai-accent: #7c3aed;
  --str-chat__ai-user-message-bg: #ede9fe;
  --str-chat__ai-syntax-keyword: #c084fc;
  --str-chat__ai-voice-color: #7c3aed;
}
Variable Styles
--str-chat__ai-text-primary, --str-chat__ai-text-secondary Text
--str-chat__ai-accent Accents, such as the icons of a reply's steps
--str-chat__ai-user-message-bg, --str-chat__ai-user-message-text The user's message bubbles
--str-chat__ai-border Borders, such as the composer's
--str-chat__ai-syntax-* Code highlighting
--str-chat__ai-voice-color, --str-chat__ai-voice-backdrop VoiceGlow and VoiceWave, and the surface they sit on, which picks their look

The AI components section of the component variables reference lists more of them, with the SDK token each follows, and the stylesheet's _tokens.scss has every one. Focus rings use the SDK's --str-chat__border-utility-focused token.

The rules use the str-chat__ai- class prefix (for example str-chat__ai-message-composer__form and str-chat__ai-streaming-message), so you can target them with your own CSS, in a CSS layer like the rest of the SDK styles. The components draw stream-chat-react icons, which you replace through WithComponents like any other SDK icon.

Localization

Every text the components show, such as the composer's "Ask a question..." placeholder or "Thought for 12s", goes through Streami18n like the rest of the SDK. Inside Chat (or a TranslationProvider) they follow the active language, and outside it they show their built-in English copy.

The keys are namespaced under aiComponents.. To change some of the English copy, pass Chat an instance whose translationsForLanguage lists the keys to change:

import { Chat, Streami18n } from "stream-chat-react";

const streamI18n = new Streami18n({
  translationsForLanguage: {
    "aiComponents.messageComposer.textInput.placeholder":
      "Message the assistant",
  },
});

<Chat client={client} i18nInstance={streamI18n}>
  {/* the conversation and the composer */}
</Chat>;

Every other text stays as it is. English is the only bundled language, so register a dictionary with registerTranslation() to translate them, as described in the Localization guide. The library's README lists every key with its English copy.

The components

Everything below is exported from stream-chat-react/ai-components.

Component or hook What it does Guide
AIConversation Lays out a conversation the way AI assistants do Conversation view
AIUserMessage, AIAssistantMessage, AIReplyActions, AIMessageAttachment Show the user's messages and the agent's replies, their images and files, and the actions under each reply Conversation view
AISuggestions Offers conversation starters Conversation view
fromStreamMessage Turns a Stream Chat message into the AIMessage the views show Conversation state
AIMessageComposer, ChatOptionsList, SelectedChatOptionChip, useText, useAttachments, useIsDisabled, useAIMessageComposerContext Takes what the user writes, with attachments, chat options and a model picker Composer
useSpeechToText Dictates with the browser's Web Speech API Composer
StreamingMessage, AIMarkdown Types out Markdown, code, tables and charts as they stream Streaming messages
AIStateIndicator, AITypingIndicator Shows what the agent is doing before its reply has text Streaming messages
AIMessageParts, AIMessagePart, AIReasoning, AIThinking, AIToolCall Shows a reply's reasoning and tool calls Reasoning and tool calls
AIToolApproval, AIToolApprovalCard, AIToolArguments, AIToolApproverProvider, useAIToolApprover, AIClientIdentity Asks the user before a tool runs Reasoning and tool calls
AIToolAuthorization, AIToolAuthorizationCard, createPopupAuthorizer, completeConnectorSignIn Signs the user in to a connector a tool call waits for Reasoning and tool calls
AIUIHost, AIUIView, defineAIUIComponent, AIUIComponentView, AIUIRuntimeView, AIUIEntrance, AIUIEntranceStage, useAIUIEntrance, useAIUIHostRefresh Shows UI the agent writes in place of a reply's text Generative UI
VoiceGlow, VoiceWave, useAudioLevel Shows what a voice agent is doing Voice visualizations
useHeardText Reveals a spoken reply at the pace the user hears it Voice agent
--str-chat__ai-* CSS variables Set the colors of all of them Theming

The main stream-chat-react entry point has AI support of its own, for chats built with MessageList: StreamedMessageText, an AIStateIndicator that follows the channel's AI state, and the StopAIGenerationButton the default composer shows, described in SDK integration. Its useAIState and useMessageTextStreaming hooks are in the AI hooks reference, with useSpeechToText from this entry point.

Migrating from @stream-io/chat-react-ai

The components used to ship as the standalone @stream-io/chat-react-ai package, which they replace. To move to stream-chat-react/ai-components:

  1. Dependencies. Remove @stream-io/chat-react-ai, and upgrade stream-chat-react to v15. Remove material-symbols as well if you only installed it for these components: they render stream-chat-react icons now.

  2. Imports. Replace the package name in imports, and the stylesheet path:

    Before After
    @stream-io/chat-react-ai stream-chat-react/ai-components
    @stream-io/chat-react-ai/styles/index.css stream-chat-react/dist/css/ai-components.css
  3. Class names and CSS variables. The aicr__ prefix became str-chat__ai-. aicr__ai-message-composer… becomes str-chat__ai-message-composer…, every other aicr__… class becomes str-chat__ai-…, and --aicr__… variables become --str-chat__ai-…. The full mapping table lists every renamed class and variable.

  4. useMessageTextStreaming. The hook is no longer exported next to the AI components. Import it from stream-chat-react and rename its letterIntervalMs option to streamingLetterIntervalMs. The StreamingMessage props haven't changed.

  5. SpeechToTextButton. It's only available as AIMessageComposer.SpeechToTextButton, and its options prop is now passed to the speech recognizer.

The component APIs are otherwise unchanged.

Coming from stream-chat-react/ai-components on v14? Its exports, props, class names and CSS variables are the same on v15. Only the translation keys changed, to the namespaced keys described in Localization, so rename the ones you registered translations for.

Next steps