Swift Quickstart
Build a voice session on macOS or iOS with CosmoRealtime in five minutes.
Prerequisites
- macOS 13+ or iOS 16+, Swift 5.9+
- A workspace API key with the
realtime:usescope — create one in the dashboard (API keys)
Add the package
In Package.swift, add the dependency and link the target:
// Package.swift
dependencies: [
.package(
url: "https://github.com/socratic-ai/cosmo-swift-sdk",
// Pre-1.0: a minor bump may break API, so pin to the minor.
.upToNextMinor(from: "0.1.0")
),
],
targets: [
.executableTarget(
name: "MyApp",
dependencies: [
.product(name: "CosmoRealtime", package: "cosmo-swift-sdk"),
]
),
]Or use Xcode: File → Add Package Dependencies and paste the URL.
Set your API key
export COSMO_API_KEY=cosmo_...The API key scopes your workspace server-side — there is no project ID to pass.
An API key is a server-side secret — fine for a CLI or prototype on your own machine, but a distributed app must use a minted end-user token (Options.Credential.token) instead. See End-user credentials.
Start a session
One call starts the session: RealtimeSession.start performs the REST session-start and joins the LiveKit room, publishing your microphone during the join. Options holds the credential and base URL; SessionConfig is the agent — instructions, voice, greeting, tools.
import CosmoRealtime
import Foundation
let apiKey = ProcessInfo.processInfo.environment["COSMO_API_KEY"] ?? {
fputs("error: set COSMO_API_KEY\n", stderr)
exit(1)
}()
print("Connecting…")
let session = try await RealtimeSession.start(
.init(apiKey: apiKey, baseURL: URL(string: "https://app.askcosmo.ai")!),
config: SessionConfig(
instructions: "You are a terse voice assistant.",
greeting: "Hi — how can I help?"
)
)Consume the event stream
Everything the server says arrives on one typed stream, session.events. No listeners to register up front — the stream buffers from session start, so nothing is missed. .sessionEnded is the terminal element and finishes the stream.
var turns: [String: String] = [:]
let events = Task {
do {
for try await event in session.events {
switch event {
case .ready(let ready):
print("Session ready — id: \(ready.sessionId)")
print("Speak into your microphone. Press Enter to end.")
case .transcript(let delta):
// Streaming deltas append; the final replaces the turn.
// Rendering both the same way duplicates every turn in a UI.
let role = delta.role == .user ? "user" : "assistant"
if delta.isFinal {
// An empty final means the turn produced nothing — close
// it rather than printing a blank line.
if !delta.text.isEmpty { print("[\(role)] \(delta.text)") }
turns[role] = nil
} else {
turns[role] = (turns[role] ?? "") + delta.text
print("[\(role)]… \(turns[role]!)")
}
case .error(let err):
fputs("Server error (\(err.code.rawValue)): \(err.message)\n", stderr)
case .sessionEnded(let ended):
print("Session ended: \(ended.reason ?? "")")
default:
break
}
}
} catch {
fputs("event stream error: \(error)\n", stderr)
}
}Append-non-finals, replace-on-final is right for a voice session like this one, and the empty-final case above is the one wrinkle it still has to handle.
It is not enough on a text-only session (audio.output: false): there, a user final that arrives after the endpointer already committed an utterance carries only the remainder, so replacing on it drops the committed prefix. Read Transcripts before building a real transcript view, and prefer TranscriptReducer over hand-rolling one.
End the session
_ = readLine() // block until Enter
await session.end()
events.cancel()Run it
COSMO_API_KEY=cosmo_... swift runGrant the microphone permission when prompted and say hello.
start(...) publishes the microphone during the join unless you pass micMuted: true — pass it for a push-to-talk UX, or whenever your UI presents the session as muted, and unmute later with setMuted(false).
A bare swift run binary has no app bundle, so it has no NSMicrophoneUsageDescription of its own and runs under the host terminal's microphone grant. Fine for this quickstart; a shipped .app needs its own purpose string and embedded frameworks — see Packaging a macOS app.
What you should see
Connecting…
Session ready — id: rs_...
Speak into your microphone. Press Enter to end.
[assistant] Hi — how can I help?
[user]… what's the tallest mountain
[user] what's the tallest mountain in the world
[assistant] Mount Everest, at 8,849 meters.
Session ended: client_endedThe complete runnable version of this program — including a client tool the agent can call — lives at sdks/cosmo-realtime/swift/Examples/HelloRealtime in the SDK repo.
Session methods
| Method | What it does |
|---|---|
send(text:) | Send a text turn the agent answers. |
send(context:) | Give the agent context without asking it anything — no turn, no speech. |
setMuted(_:) | Mute or unmute the microphone. |
ping() | Send a keepalive ping. |
end() | End the session gracefully. |
Next steps
- Clients, agents, and sessions — the three-tier model in depth
- Events — the full typed event stream
- Tools — define client tools with
SessionConfig.Tool.define - Hooks — intercept session start, tool use, and speech timeouts
- End-user credentials — the minted-token flow for shipped apps