Quickstart
StreamChatAI gives your iOS app the screens of an AI assistant: a conversation that streams the agent's replies, a prompt composer with dictation and photos, the agent's reasoning and tool calls, and a face for a voice agent. 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 StreamAgents 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 uses the device: the calendar, 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 |
Voice runs through all of them. Every 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, AIVoiceGlowView or AIVoiceWaveView shows what it's doing, and AIConversationView 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 as it goes, and the components show it:
sequenceDiagram
participant App as iOS app
participant Router as Stream Agents
participant LLM as Model
App->>Router: agents.chat() with StreamAgents
App->>Router: responses.create("How do I...?")
Router->>LLM: The conversation so far
LLM-->>Router: Reasoning, tool calls, tokens
Router-->>App: The reply as it streams, with its steps
Note over App: AIConversationView shows it- StreamAgents signs the user in, opens and lists 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.
- StreamChatAI shows it all:
AIConversationViewandAIComposerView, the reasoning and tool calls of each reply, 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 phone. The agent hears the user and answers out loud, and what is said is written into the session's conversation.
Before you start
- A Stream app, from the Stream Dashboard.
- The beta Stream CLI, which has the
getstream agentscommands. - Xcode 27 or later, and an app that targets iOS 15 or later.
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 and its system prompt in instructions.md. Write what the agent is for in instructions.md, 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 two packages to your app: StreamAgents, the client for your agent, and StreamChatAI, the views. In Xcode, select "Add Package Dependencies..." in the File menu for each, or add them to your package file:
dependencies: [
.package(url: "https://github.com/GetStream/Vision-Agents.git", branch: "accelerate"),
.package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "5.16.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "StreamAgents", package: "Vision-Agents"),
.product(name: "StreamChatAI", package: "stream-chat-swift")
]
)
]StreamAgents brings Stream Chat and Stream Video with it, which keep the conversation and carry the voice. SwiftPM clones the whole Vision-Agents repository the first time, which takes a while.
Is your agent on a backend of your own? Add only StreamChatAI, and connect the views to your agent with a transport, as described in Conversation State. StreamChatAI doesn't depend on the other Chat libraries, so it works next to StreamChatSwiftUI or a UI of your own. If your agent answers with generative UI, also add StreamChatA2UI or StreamChatOpenUI.
The composer can attach photos and dictate, so add these keys to your Info.plist, each with a description of why your app needs it:
NSCameraUsageDescriptionandNSPhotoLibraryUsageDescription, for photos.NSMicrophoneUsageDescriptionandNSSpeechRecognitionUsageDescription, for dictation and voice.
Step 3: Sign the user in
Create one StreamAgents 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:
import StreamAgents
let agents = StreamAgents(apiKey: "<# Your API key #>")
func signIn(userID: String, name: String) {
agents.setUser(User(id: userID, name: name)) {
try await yourBackend.streamToken()
}
}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 agents.setUser(User(id: "jane"), token: "...").
Step 4: Connect a conversation to the agent
AIConversation holds a conversation's state and drives the views, and a transport connects it to your agent. Add these two types to your project. AgentMessage is one message, as the views show it:
import StreamChat
import StreamChatAI
struct AgentMessage: AIMessage {
let message: ChatMessage
var id: String { message.id }
var role: AIMessageRole { message.extraData["ai_generated"]?.boolValue == true ? .assistant : .user }
var text: String { message.text }
var isGenerating: Bool { message.extraData["generating"]?.boolValue == true }
// The reasoning and tool calls the agent writes on its reply.
var parts: [AIMessagePart] {
AIMessagePart.parts(from: message.allAttachments.map { ($0.type.rawValue, $0.payload) })
}
}AgentTransport asks the agent, stops it, and shows the conversation as the agent writes it. Its approver answers the tools that ask before they run:
import Combine
import StreamChat
import StreamChatAI
import StreamAgents
@MainActor
final class AgentTransport: AIConversationTransport {
let session: AgentSession
private let channel: ChatChannelController
private var subscription: AnyCancellable?
init(_ session: AgentSession) async throws {
self.session = session
// The agent writes the conversation into a channel, which the views read.
channel = try await session.chat()
// Follows the agent, and runs the tools the app gives it.
await session.start()
}
func connect(_ feed: AIConversationFeed<AgentMessage>) {
let publish = { [channel] in
feed.update(messages: channel.messages.reversed().map(AgentMessage.init))
}
subscription = channel.messagesChangesPublisher.sink { _ in publish() }
channel.synchronize { _ in publish() }
}
func send(_ request: AIConversationRequest<AgentMessage>) async throws {
_ = try await session.responses.create(request.text)
}
func stop() async throws {
try await session.interrupt()
}
var approver: AIToolApprover {
AIToolApprover(userID: agents.backend.user?.id ?? "", clientID: AIClientIdentity.installID) { call, allowed in
try await self.session.decide(call.id, allowed: allowed)
}
}
}Step 5: Show the conversation
Open a session with your agent, and show it with AIConversationView and the composer:
import StreamChatAI
import SwiftUI
import StreamAgents
struct AssistantScreen: View {
@State private var transport: AgentTransport?
var body: some View {
if let transport {
AssistantView(transport: transport)
} else {
ProgressView()
.task { transport = try? await AgentTransport(agents.chat(agent: "my-agent")) }
}
}
}
struct AssistantView: View {
@StateObject private var conversation: AIConversation<AgentMessage>
init(transport: AgentTransport) {
_conversation = StateObject(wrappedValue: AIConversation(transport: transport, approver: transport.approver))
}
var body: some View {
AIConversationView(conversation: conversation)
.safeAreaInset(edge: .bottom) {
AIComposerView(conversation: conversation)
}
}
}Run the app and ask something. The agent thinks, then its 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, share and rate it.
The Conversation View guide covers what else the screen can show, such as suggestions while the conversation is empty.
Step 6: Add voice
The composer already takes voice: the microphone button in its text field dictates into it with Apple's Speech framework.
To let the user talk to the agent instead, start a voice session. The agent joins a call right away, and the phone joins it with the microphone on:
let voice = try await VoiceSession.start(agents: agents, agent: "my-agent")
await voice.session.start()
await voice.join()Give the agent a face with AIVoiceGlowView, which moves differently while the agent is connecting, listening, thinking and speaking. 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 components are styled with AIAppearance. It builds on DesignSystemTokens, the color, layout and typography tokens shared by Stream's SDKs, and adds the colors, fonts and icons of the AI components. Set it once, before the components first show, for example when your app starts:
let tokens = DesignSystemTokens()
tokens.colors.accentPrimary = .systemPurple
let appearance = AIAppearance(tokens: tokens)
appearance.colors.reasoningRule = .systemPurple
appearance.colors.suggestionBackground = .systemMint.withAlphaComponent(0.3)
appearance.fonts.suggestion = .footnote
appearance.images.composerSend = Image(systemName: "paperplane.circle.fill")
InjectedValues[\.aiAppearance] = appearanceThe AI colors and fonts are derived from the tokens, so change the tokens before you create the appearance. StreamChatAI re-exports StreamCore and DesignSystemTokens, so this code needs no other import besides SwiftUI.
The colors, fonts and icons are named after the component that uses them, such as composerBackground or toolCallAccent, and each component's guide lists its own under Appearance. The syntax highlighting colors (code...) have light and dark variants.
Localization
Every text the components show, such as the composer's "Ask anything" placeholder or "Thought for 12s", goes through AIAppearance.localizationProvider, which reads the library's own Localizable strings table by default. Replace it to change or translate the texts. For example, to read your own strings first and fall back to the library's:
let libraryText = AIAppearance.localizationProvider
AIAppearance.localizationProvider = { key, table in
let text = Bundle.main.localizedString(forKey: key, value: nil, table: "StreamChatAI")
return text == key ? libraryText(key, table) : text
}With this provider, a StreamChatAI.strings table in your app overrides the keys it lists, such as "composer.placeholder.ask_anything" = "Message the assistant";, and every other text stays as it is. The keys are listed in the library's Localizable.strings.
The components
| Component | What it does | Guide |
|---|---|---|
AIConversationView |
Lays out a conversation the way AI assistants do | Conversation View |
AIConversation |
Holds a conversation's state, and reaches your agent through a transport | Conversation State |
AIComposerView, SpeechToTextButton |
Takes what the user writes, with photos, chat options and dictation | Composer |
StreamingMessageView, AITypingIndicatorView |
Types out Markdown, code and charts as they stream | Streaming Messages |
AIMessagePartsView, StreamingReasoningView, AIToolApprovalCard |
Shows a reply's reasoning and tool calls, and asks before a tool runs | Reasoning and Tool Calls |
AIClientToolRunner |
Runs the tools the agent asks the device to run | Reasoning and Tool Calls |
AIUIHost |
Shows UI the agent writes in place of a reply's text | Generative UI |
AIVoiceGlowView, AIVoiceWaveView |
Shows what a voice agent is doing | Voice Visualizations |
SuggestionsView, SidebarView |
Offers conversation starters, and puts a side menu beside the conversation | Conversation View |
AIOnDeviceModel |
Answers with Apple's on-device model when your agent can't | Conversation State |
AIAppearance |
Sets the colors, fonts, icons and texts of all of them | Theming |
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, the calendar and reminders as tools on the phone, and a voice mode.
- Voice agent: a voice conversation with the agent as a glowing globe, and what was said written out.
- Stream Agents: the agent itself, its tools, knowledge, connectors and phone numbers.