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-agentssigns 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-componentsshows it all:AIConversationandComposerView, the reasoning and tool calls of each reply, the UI the agent writes, and the voice visualizations.AIConversationControllerkeeps 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 agentscommands. - 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-controllerneed.
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 voiceThat 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-agentThe 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-expoThe 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-controllerCharts 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-skiaThe 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-frommodule.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:
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) orexpo-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) orexpo-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:
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:
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:
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:
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:
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:
-
Uninstall
@stream-io/chat-react-native-aiand 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. -
Enable speech-to-text on iOS, as described in Dictation. With Expo, replace the
@stream-io/chat-react-native-aiconfig plugin withstream-chat-expo. -
Keep the
export-namespace-fromBabel plugin. The@babel/plugin-proposal-export-namespace-frompackage you may already have works too. -
ShimmeringViewandtoHexFromRNColorwere removed.AITypingIndicatorViewnow uses the SDK's native shimmer, so@shopify/react-native-skiais only needed for charts and the voice visualizations. -
SpeechToTextButtonandisDictationAvailableare 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
ExpoChatGPTSampleandExpoCrewAssistantfolders of the SDK repository. - Stream Agents: the agent itself, its tools, knowledge, connectors and phone numbers.