Info:
This is documentation for Stream Chat React Native SDK v9, which is no longer actively maintained. For up-to-date documentation, see the latest version (v10).

Quickstart

The AI components give your React Native app the screens of an AI assistant: a conversation that streams the agent's replies, a prompt composer with photos and dictation, the agent's reasoning and tool calls, and a face for a voice agent. They ship with stream-chat-react-native and stream-chat-expo in their own entry points: stream-chat-react-native/ai-components, or stream-chat-expo/ai-components with Expo. 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 The Voice agent use case
An agent that shows its work and asks before it acts Reasoning and tool calls
An agent that uses the phone: the clock, the location, a screen of your app 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: dictation listens, the agent answers, the phone reads the answer aloud, and VoiceGlow or VoiceWave shows what is happening.

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 Native 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 app gives the agent on the phone.
  • stream-chat-react-native/ai-components shows it all: AIConversation and ComposerView, the reasoning and tool calls of each reply, the UI the agent writes, and the voice visualizations. AIConversationController keeps the conversation's state between them and your agent.
  • Your agent lives in a folder of plain files that you sync with the Stream CLI: its instructions, knowledge, skills, tools, connectors and models.

Before you start

  • A Stream app, from the Stream Dashboard.
  • The beta Stream CLI, which has the getstream agents commands.
  • A React Native 0.76 or later app, with the Chat SDK's requirements installed. With Expo, the app runs in a development build: Expo Go doesn't have the native code the SDK and react-native-keyboard-controller need.

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 the Chat SDK, which has the views. stream-chat is the Stream Chat client that reads the conversation the agent writes:

# React Native CLI
yarn add @stream-io/stream-agents stream-chat stream-chat-react-native

# Expo
npx expo install @stream-io/stream-agents stream-chat stream-chat-expo

The AI components are available from version 9.10.0 of stream-chat-react-native and stream-chat-expo, VoiceGlow and VoiceWave from 9.11.0, and the reply's steps and generative UI from 9.13.0, and AIConversation's entry point and the conversation controller from 9.14.0, so this page needs 9.14.0 or later. Their JavaScript dependencies are installed with the SDK, so there's no separate package to add. Import them from the AI entry points:

// React Native CLI
import { ComposerView } from "stream-chat-react-native/ai-components";
import { AIConversation } from "stream-chat-react-native/ai-components/conversation";

// Expo
import { ComposerView } from "stream-chat-expo/ai-components";
import { AIConversation } from "stream-chat-expo/ai-components/conversation";

The examples on these pages import from stream-chat-react-native. With Expo, use stream-chat-expo instead. Apps that never import the AI entry points don't bundle any of them, and apps that only use the AI components don't bundle the rest of the chat SDK.

AIConversation has an entry point of its own, because it needs react-native-keyboard-controller 1.21.7 or later, which keeps the user's question in place and makes room for the keyboard below it. Install it, and wrap your app in its KeyboardProvider:

yarn add react-native-keyboard-controller
# With Expo: npx expo install react-native-keyboard-controller

Charts and the voice visualizations draw with Skia, which needs two native libraries that only apps using them have to install. react-native-reanimated, react-native-gesture-handler and react-native-svg are already required by the SDK:

yarn add victory-native @shopify/react-native-skia
# With Expo: npx expo install victory-native @shopify/react-native-skia

The components depend on zod, which uses export * as syntax that the React Native Babel preset doesn't transform. Install the export-namespace-from plugin, and add it to babel.config.js:

yarn add -D @babel/plugin-transform-export-namespace-from
module.exports = {
  presets: ["module:@react-native/babel-preset"],
  plugins: [
    // ... rest of your @babel plugins
    "@babel/plugin-transform-export-namespace-from",
    // react-native-worklets/plugin has to be the last one
    "react-native-worklets/plugin",
  ],
};

Hermes has no crypto.randomUUID, which @stream-io/stream-agents uses for the ids of its requests. Add one where your app starts, before anything else imports the client:

polyfills.ts
type CryptoLike = { randomUUID?: () => string };
const holder = globalThis as { crypto?: CryptoLike };

if (typeof holder.crypto?.randomUUID !== "function") {
  holder.crypto = Object.assign(holder.crypto ?? {}, {
    randomUUID: () =>
      "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
        const random = Math.floor(Math.random() * 16);
        // The bits that mark a version 4 UUID.
        return (char === "x" ? random : (random & 0x3) | 0x8).toString(16);
      }),
  });
}

The composer's microphone button needs setup in the native project: Dictation describes the Podfile line for iOS, the permission for Android and the config plugin for Expo. If your agent answers with generative UI, its A2UI renderer comes with the SDK, in stream-chat-react-native/a2ui.

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. A transport of your own then connects the conversation to it, as described there.

Optional libraries

Two optional libraries add features. The SDK finds whichever you install:

  • Photos. The composer's sheet picks photos from the library and takes them with the camera when react-native-image-picker (React Native CLI) or expo-image-picker (Expo) is installed. Attach photos lists their permissions.
  • Clipboard. Code blocks and replies get a copy button when @react-native-clipboard/clipboard (React Native CLI) or expo-clipboard (Expo) is installed.

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. The same user connects to Stream Chat, and api.use hands that client to the agent client, so the agent's sessions read the conversation on the connection your app already has, rather than on a second one:

agent.ts
import "./polyfills";

import { Client } from "@stream-io/stream-agents";
// Lets session.chat() open the conversation's channel on your Stream Chat client. Import it once.
import "@stream-io/stream-agents/chat";
import { StreamChat } from "stream-chat";

const API_KEY = "your_api_key";

export const api = new Client({ apiKey: API_KEY });
export const chatClient = StreamChat.getInstance(API_KEY);

export const fetchStreamToken = () =>
  fetch("https://your-backend.example.com/stream-token").then((response) =>
    response.text(),
  );

export async function signIn(user: { id: string; name: string }) {
  await api.setUser(user, fetchStreamToken);
  await chatClient.connectUser(user, fetchStreamToken);
  api.use({ chat: chatClient });
}

If your app already has a connected StreamChat client, such as the one Chat takes, pass it to api.use and skip the last two lines of signIn.

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" }, "...") and chatClient.connectUser({ 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";
import type { Channel } from "stream-chat";

export type AgentChat = {
  session: Session;
  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());
  // The channel the agent writes the conversation into.
  const { channel } = await opened.chat();
  const agentChannel = channel as Channel;
  await agentChannel.watch();
  return { session: opened, channel: agentChannel };
}

The views show AIMessage values, and keep them in an AIConversationController. The controller reaches your agent through a transport, which you write: it follows the channel's messages, tells the controller what the agent is doing, and asks and stops through the session. createFromStreamMessage turns each of the channel's messages into an AIMessage: a reply is a message with ai_generated: true, it's being written while generating is true, and its steps are its ai_reasoning and ai_tool_call attachments. The channel's state lists its messages oldest first, which is the order the conversation wants:

agentTransport.ts
import type { ConversationSnapshot, Session } from "@stream-io/stream-agents";
import type { Channel } from "stream-chat";
import {
  type AIAgentActivity,
  AIAgentActivityStates,
  type AIConversationTransport,
  createFromStreamMessage,
} from "stream-chat-react-native/ai-components";

// What the agent says it's doing, from the session's socket.
const activityOf = (
  snapshot: ConversationSnapshot,
): AIAgentActivity | undefined => {
  if (snapshot.kind === "responding") {
    return { state: AIAgentActivityStates.thinking };
  }
  if (snapshot.kind === "working") {
    return {
      state: AIAgentActivityStates.checkingExternalSources,
      text: `Working on ${snapshot.skills.map((skill) => skill.replace(/_/g, " ")).join(" and ")}`,
    };
  }
  return undefined;
};

export const createAgentTransport = (
  session: Session,
  channel: Channel,
): AIConversationTransport => ({
  connect: (feed) => {
    // Gives back the same AIMessage for a message that didn't change,
    // so only the messages an update touched render again.
    const fromStreamMessage = createFromStreamMessage();
    const updateMessages = () =>
      feed.updateMessages(channel.state.messages.map(fromStreamMessage));
    updateMessages();
    // Every event of the channel, so new and updated messages show.
    const subscription = channel.on(updateMessages);

    const updateActivity = () =>
      feed.updateActivity(activityOf(session.state.get()));
    updateActivity();
    const stopFollowing = session.state.subscribe(updateActivity);

    return () => {
      subscription.unsubscribe();
      stopFollowing();
    };
  },
  // The router writes the question into the channel itself.
  send: async ({ text }) => {
    await session.responses.create(text);
  },
  stop: async () => {
    session.interrupt();
  },
});

Conversation state covers the transport and the controller in full: attachments, errors and a retry, ratings, and loading older messages.

Step 5: Show the conversation

Show the conversation with AIConversation, and the composer below it. Both take the controller as their conversation: the composer sends to it, and shows a stop button while the agent answers. The approver answers the tools that ask before they run. The router has no regenerate or edit of its own, so the controller leaves those out:

AgentConversation.tsx
import { useEffect, useState } from "react";
import { StyleSheet, View } from "react-native";
import {
  AIClientIdentity,
  AIConversationController,
  ComposerView,
} from "stream-chat-react-native/ai-components";
import { AIConversation } from "stream-chat-react-native/ai-components/conversation";

import { type AgentChat, chatClient } from "./agent";
import { createAgentTransport } from "./agentTransport";

export const AgentConversation = ({ chat }: { chat: AgentChat }) => {
  const { channel, session } = chat;
  // One controller per conversation.
  const [conversation] = useState(
    () =>
      new AIConversationController({
        actions: { edit: false, regenerate: false },
        approver: {
          clientId: AIClientIdentity.installId(),
          decide: async (call, allowed) => session.decide(call.id, { allowed }),
          userId: chatClient.userID ?? "",
        },
        transport: createAgentTransport(session, channel),
      }),
  );
  useEffect(() => () => conversation.dispose(), [conversation]);

  return (
    <View style={styles.container}>
      {/* The conversation scrolls inside itself and makes room for the keyboard. */}
      <AIConversation conversation={conversation} />
      <ComposerView conversation={conversation} />
    </View>
  );
};

const styles = StyleSheet.create({ container: { flex: 1 } });

AIConversation fills the room its parent gives it and scrolls inside itself, so give the parent a height, here with flex: 1. The keyboard is the SDK's: with KeyboardProvider above it, the conversation makes room for the keyboard and the composer rides it, so don't wrap them in a KeyboardAvoidingView. Make one controller per conversation and dispose of it once it's gone. To switch to another conversation, render the screen again with a key.

The screen renders inside StreamTheme, which gives the AI components their colors, as Theming describes. Sign the user in, then open a conversation, for example from a button:

App.tsx
import { useState } from "react";
import { Pressable, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { StreamTheme } from "stream-chat-react-native/ai-components";

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

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

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

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaProvider>
        <KeyboardProvider>
          <StreamTheme>
            {chat ? (
              <AgentConversation chat={chat} />
            ) : (
              <Pressable onPress={() => void start()}>
                <Text>Ask the agent</Text>
              </Pressable>
            )}
          </StreamTheme>
        </KeyboardProvider>
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}

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 send button is a stop button, 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 suggestions while it's empty, older messages, and an error with a way to try again. The Composer guide adds photos and chat options.

Step 6: Add voice

The composer already takes voice: its microphone button dictates into it with the platform's speech recognizer, once dictation is set up.

To let the user talk to the agent instead, build a voice mode on the same conversation: dictation listens, the controller sends what it heard, and when the reply is done the phone reads it aloud. Give the agent a face with VoiceGlow, which moves differently while the agent is connecting, listening, thinking and speaking:

import { VoiceGlow } from "stream-chat-react-native/ai-components";

<VoiceGlow state="thinking" style={{ width: 240, height: 240 }} />;

The Voice agent use case builds the whole screen: the agent's state, the visualizations, and a hands-free 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 take their colors, fonts and sizes from an AI theme of their own, defined in ai-components/theme.ts, which StreamTheme provides. Place it high in your component tree, and pass a style to change the theme. It's deeply merged over the defaults, so pass only what you change:

<StreamTheme
  style={{
    colors: { accent_blue: "#7c3aed" },
    composer: { inputPill: { borderRadius: 12 } },
    message: { userMessageBackground: "#ede9fe" },
  }}
>
  {children}
</StreamTheme>
Key Styles Guide
colors The palette the rest of the theme follows Dark mode
markdown, aiTypingIndicatorView Markdown, code and the thinking indicator Streaming messages
composer, sheet, chatOptions The composer, its sheet, and the chat options and their chip Composer
conversation, message, suggestions The list, the user's and the agent's messages and their actions, and the suggestions Conversation view
messageParts Reasoning, tool calls and their questions Reasoning and tool calls
generatedUI, a2ui The UI an agent writes for a reply Generative UI
voiceVisualization VoiceGlow and VoiceWave Voice visualizations
icons Any icon the messages and steps draw, by name, over defaultAIIcons Conversation view

Dark mode

The theme has no dark mode of its own: StreamTheme doesn't follow the system color scheme, so pass the colors of the scheme you want to show:

import { useColorScheme } from "react-native";

const darkColors = {
  accent_blue: "#4C9DFF",
  accent_red: "#FF636E",
  black: "#FFFFFF",
  code_block: "#1E1E22",
  grey: "#A1A1AA",
  grey_neutral: "#C5C5C8",
  grey_dark: "#71717A",
  grey_gainsboro: "#3F3F46",
  grey_whisper: "#27272F",
  overlay: "#000000CC",
  transparent: "transparent",
  white: "#050509",
  white_smoke: "#121214",
  shimmer: "#FFFFFF",
};

const Theme = ({ children }: { children: React.ReactNode }) => {
  const scheme = useColorScheme();
  return (
    <StreamTheme style={{ colors: scheme === "dark" ? darkColors : {} }}>
      {children}
    </StreamTheme>
  );
};

Localization

Every text the components show, such as the composer's placeholder or "Thought for 12s", goes through the Chat SDK's translations. Inside Chat, they follow the active language, and you change or translate them with Streami18n, as Localization describes. The keys are the English texts. Outside Chat, they're in English, and the TranslationProvider that the AI entry point exports gives them a translator without it.

The components

Everything below is exported from stream-chat-react-native/ai-components, except AIConversation, which comes from ai-components/conversation, and the A2UI renderer, from stream-chat-react-native/a2ui.

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
AIConversationController, useAIConversationState Keep a conversation's state, and reach your agent through a transport Conversation state
fromStreamMessage, createFromStreamMessage Turn a Stream Chat message into the AIMessage the views show Conversation state
ComposerView, ChatOptionsList, SelectedChatOptionChip, useComposerText, useComposerChatOption, useComposerIsEditing Takes what the user writes, with photos, chat options and dictation Composer
SpeechToTextButton, useDictation, isDictationAvailable Dictate with the platform's speech recognition Composer
StreamingMessageView, Markdown Types out Markdown, code, tables and charts as they stream Streaming messages
AITypingIndicatorView 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, AIToolApproverProvider, AIClientIdentity Asks the user before a tool runs Reasoning and tool calls
AIToolAuthorization, AIToolAuthorizationCard, createAuthSessionAuthorizer Signs the user in to a connector a tool call waits for Reasoning and tool calls
AIClientToolRunner Runs the tools the agent asks the phone to run Reasoning and tool calls
AIUIHost, AIUIView, defineAIUIComponent, AIUIEntrance, createA2UIRenderer Shows UI the agent writes in place of a reply's text Generative UI
VoiceGlow, VoiceWave, useDictationAudioLevel Shows what a voice agent is doing Voice visualizations
StreamTheme, useTheme Set the colors, styles and icons of all of them Theming

The main stream-chat-react-native entry point has AI support of its own, for chats built with MessageList: its own StreamingMessageView and AITypingIndicatorView, a stop button in the default composer, and the useAIState and useStreamingMessage hooks, described in SDK integration and the AI hooks.

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

The components used to ship as the standalone @stream-io/chat-react-native-ai package, which is now deprecated. Their API is unchanged. To migrate:

  1. Uninstall @stream-io/chat-react-native-ai and import from the new entry point:

    - import { ComposerView, StreamingMessageView } from "@stream-io/chat-react-native-ai";
    + import { ComposerView, StreamingMessageView } from "stream-chat-react-native/ai-components";

    With Expo, import from stream-chat-expo/ai-components.

  2. Enable speech-to-text on iOS, as described in Dictation. With Expo, replace the @stream-io/chat-react-native-ai config plugin with stream-chat-expo.

  3. Keep the export-namespace-from Babel plugin. The @babel/plugin-proposal-export-namespace-from package you may already have works too.

  4. ShimmeringView and toHexFromRNColor were removed. AITypingIndicatorView now uses the SDK's native shimmer, so @shopify/react-native-skia is only needed for charts and the voice visualizations.

  5. SpeechToTextButton and isDictationAvailable are now exported.

Next steps

  • Customer support agent: answers from your help center, order lookups, a refund the user confirms, and a handover to your team.
  • Personal assistant: a list of conversations, tools that run on the phone, photos, and a voice mode.
  • Voice agent: a hands-free voice conversation with the agent as a glowing globe.
  • The complete assistants in the ExpoChatGPTSample and ExpoCrewAssistant folders of the SDK repository.
  • Stream Agents: the agent itself, its tools, knowledge, connectors and phone numbers.