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: AIConversationView and AIComposerView, 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 agents commands.
  • 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 voice

That 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-agent

The 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:

  • NSCameraUsageDescription and NSPhotoLibraryUsageDescription, for photos.
  • NSMicrophoneUsageDescription and NSSpeechRecognitionUsageDescription, 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] = appearance

The 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.