Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
581eca4
refactor: nest startup progress under ConversationState
renal128 Jul 20, 2026
a37a4b1
refactor: return conversation startup result
renal128 Jul 20, 2026
644e264
test: discard asynchronous startup result
renal128 Jul 20, 2026
0393069
refactor: consolidate startup result type
renal128 Jul 20, 2026
0bd9c3e
refactor: require handling startup results
renal128 Jul 20, 2026
1e5e4c1
fix: preserve terminal conversation state
renal128 Jul 20, 2026
d4da27a
test: make startup timeout test deterministic
renal128 Jul 20, 2026
4261d37
test: serialize mock startup synchronization
renal128 Jul 20, 2026
6485df8
feat: wait for conversation initiation metadata
renal128 Jul 20, 2026
61ee5bb
feat: introduce reusable ConversationClient
renal128 Jul 20, 2026
20b7801
fix: make disconnected mic controls no-op
renal128 Jul 20, 2026
e732582
fix: make latest conversation start win
renal128 Jul 21, 2026
37525e3
fix: preserve client microphone state
renal128 Jul 21, 2026
f2de945
fix: treat end during agent wait as cancellation
renal128 Jul 22, 2026
5300150
test: dissolve BusinessLogicTests into focused suites
renal128 Jul 22, 2026
fa88abb
refactor: fold Conversation event handling into Conversation.swift
renal128 Jul 22, 2026
e4e84cb
refactor: move Conversation session type under Internal
renal128 Jul 22, 2026
8ae6cc2
feat: add ConversationAudioObserver for mic and agent audio
renal128 Jul 20, 2026
8c2effb
refactor: remove bundled audio level monitor
renal128 Jul 20, 2026
ef4ce4a
chore: remove public LiveKit audio track re-exports
renal128 Jul 19, 2026
fa62a42
refactor: remove unused audio device state
renal128 Jul 20, 2026
40b921e
refactor: centralize client tool result construction
renal128 Jul 20, 2026
97d8172
refactor: simplify client tool callback name
renal128 Jul 20, 2026
218b1b0
refactor: remove feedback availability callback
renal128 Jul 20, 2026
c7e0283
refactor: make conversation endpoints configurable per session
renal128 Jul 25, 2026
6e11766
refactor: remove ElevenLabs enum namespace and global configure
renal128 Jul 25, 2026
744a9f9
move files
renal128 Jul 25, 2026
7b518ed
refactor: merge DisconnectionReason into EndReason
renal128 Jul 25, 2026
6471f77
refactor: collapse ConversationAgentReadyReport into elapsed
renal128 Jul 25, 2026
e31cb64
refactor: colocate CallInfo and EndReason with ConversationState
renal128 Jul 25, 2026
c610633
refactor: colocate session config types in ConversationConfig
renal128 Jul 25, 2026
72bac94
refactor: merge startup types into ConversationStartup
renal128 Jul 25, 2026
57cc551
chore: fix swiftformat trailing newline in ConversationConfig
renal128 Jul 25, 2026
c6af73b
feat: add minimal ElevenLabsWidget target and DemoApp example
renal128 Jul 25, 2026
bdddbcd
feat: audio-reactive orb in the widget
renal128 Jul 25, 2026
cc8e3d4
feat: widget conversation modes
renal128 Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ xcuserdata/
xcshareddata/
Package.resolved
*.bak
DerivedData/
*.xcuserstate
95 changes: 47 additions & 48 deletions Documentation/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,20 @@ conversation.$agentState

### Connection State

Handle transitions between idle, connecting, connected, and ended states.
Handle transitions between idle, connecting, connected, ended, and error states.
Startup stages are nested under `.connecting`.

```swift
conversation.$state
.sink { state in
switch state {
case .idle:
break
case .connecting:
break
case .connecting(let stage):
print("Connecting: \(stage)")
case .connected(let callInfo):
print("Connected to agent: \(callInfo.agentId)")
print("Conversation ID: \(callInfo.conversationId)")
case .ended(let reason):
print("Conversation ended: \(reason)")
case .error(let error):
Expand Down Expand Up @@ -190,21 +192,29 @@ struct AudioControlView: View {
}
```

### Raw Audio Tracks
### Raw Audio Observers

Access the underlying LiveKit audio tracks for advanced visualization (e.g., audio visualizers or level meters).
Observe decoded PCM from the agent output or local microphone without taking a LiveKit dependency in app code. Prefer this over reaching for transport tracks.

```swift
// Use these with LiveKit view components or custom processors
if let inputTrack = conversation.inputTrack as? LocalAudioTrack {
// Access local microphone track
final class SpectrumObserver: ConversationAudioObserver, @unchecked Sendable {
func didReceive(_ buffer: AVAudioPCMBuffer) {
// Time-critical path: copy what you need, then hop off this callback.
let copy = buffer.copy()
Task { @MainActor in
updateVisualizer(copy)
}
}
}

if let agentTrack = conversation.agentAudioTrack as? RemoteAudioTrack {
// Access agent's audio track
}
let client = ConversationClient()
let observer = SpectrumObserver()
client.addAgentAudioObserver(observer)
client.addMicAudioObserver(observer)
```

`didReceive(_:)` runs on the audio callback path (not the main actor). The buffer is borrowed and read-only.

---

## Tool Calls
Expand Down Expand Up @@ -277,13 +287,17 @@ class ConversationViewModel: ObservableObject {
result = "Unknown tool: \(toolCall.toolName)"
}

try await conversation.sendToolResult(for: toolCall.toolCallId, result: result)
try await conversation.sendToolResult(
.init(toolCallId: toolCall.toolCallId, result: result)
)
} catch {
print("Tool execution failed: \(error)")
try? await conversation.sendToolResult(
for: toolCall.toolCallId,
result: "Error: \(error.localizedDescription)",
isError: true
.init(
toolCallId: toolCall.toolCallId,
result: "Error: \(error.localizedDescription)",
isError: true
)
)
}
}
Expand Down Expand Up @@ -484,10 +498,6 @@ let callbacks = ConversationCallbacks(
onAudioAlignment: { alignment in
// Real-time word highlighting timing.
},
onCanSendFeedbackChange: { canSend in
// Enable/disable your 'Thumbs Up/Down' buttons in the UI
self.showFeedbackUI = canSend
},
onError: { error in
print("A non-fatal or startup error occurred: \(error)")
}
Expand Down Expand Up @@ -519,20 +529,17 @@ let config = ConversationConfig(audioConfiguration: audioConfig)

---

## Startup Performance Tuning {#startup-tuning}
## Startup Timeouts {#startup-tuning}

Control the connection handshake and retry behavior.
Control how long startup waits for the agent and conversation metadata.

```swift
let startupConfig = ConversationStartupConfiguration(
// Time to wait for agent to be 'ready' after room connection
// Time to wait for the agent after the room connects
agentReadyTimeout: 10.0,

// Backoff strategy for protocol initialization
initRetryDelays: [0, 1.0, 2.0, 5.0],

// Whether to fail early if agent takes too long
failIfAgentNotReady: true

// Time to wait for the server to initialize the conversation
initiationMetadataTimeout: 5.0
)

let config = ConversationConfig(startupConfiguration: startupConfig)
Expand All @@ -542,35 +549,29 @@ let config = ConversationConfig(startupConfiguration: startupConfig)

## Feedback & Context {#feedback-context}

Handle feedback (like/dislike) and contextual updates to the agent.
Feedback can be sent whenever the conversation is connected.

```swift
// 1) Setup: react to feedback availability
var canSendFeedback = false

let callbacks = ConversationCallbacks(
onAgentResponse: { text, eventId in
print("Agent:", text, "(event:", eventId, ")")
},
onCanSendFeedbackChange: { can in
canSendFeedback = can
// e.g., refresh your UI
}
)

let conversation = try await ElevenLabs.startConversation(agentId: "agent_123", callbacks: callbacks)
let client = ConversationClient(callbacks: callbacks)
_ = try await client.startConversation(agentId: "agent_123")

// 2) Sending feedback from your UI
func thumbsUp(latestEventId: Int) {
Task { try? await conversation.sendFeedback(.like, eventId: latestEventId) }
func thumbsUp(eventId: Int) async throws {
guard client.state.isConnected else { return }
try await client.sendFeedback(.like, eventId: eventId)
}

func thumbsDown(latestEventId: Int) {
Task { try? await conversation.sendFeedback(.dislike, eventId: latestEventId) }
func thumbsDown(eventId: Int) async throws {
guard client.state.isConnected else { return }
try await client.sendFeedback(.dislike, eventId: eventId)
}

// 3) Contextual updates (e.g., user preferences)
Task { try? await conversation.updateContext("user_prefers_detailed_answers=true") }
try await client.updateContext("user_prefers_detailed_answers=true")
```

---
Expand Down Expand Up @@ -609,7 +610,7 @@ class ReconnectionManager: ObservableObject {
self.showReconnectButton = false
self.isReconnecting = false
self.reconnectAttempts = 0
default:
case .idle, .connecting, .error:
break
}
}
Expand Down Expand Up @@ -715,9 +716,7 @@ The SDK uses `os.Logger` for high-performance logging. You can filter logs in Xc
Adjust the verbosity of the SDK:

```swift
ElevenLabs.configure(
ElevenLabs.Configuration(logLevel: .debug) // .trace for full event logs
)
let client = ConversationClient(logLevel: .debug) // .trace for full event logs
```

---
Expand Down
Loading
Loading