@@ -93,12 +93,6 @@ final class Conversation: ObservableObject {
9393 self . callbacks = callbacks
9494 pendingMuteState = initialMicMuted
9595 logger = dependencyProvider. logger
96- setupAudioManager ( )
97- }
98-
99- private func setupAudioManager( ) {
100- guard !config. conversationOverrides. textOnly else { return }
101- audioManager = ConversationAudioManager ( logger: logger)
10296 }
10397
10498 private func setupAgentStateManager( ) {
@@ -113,67 +107,36 @@ final class Conversation: ObservableObject {
113107
114108 // MARK: - API
115109
116- /// Start a conversation using authentication configuration.
117- func start( auth: ConversationCredentials ) async throws -> ConversationStartResult {
118- guard state == . idle else {
119- throw ConversationError . alreadyStarted
120- }
121-
122- let result : ConversationStartResult = if config. conversationOverrides. textOnly {
123- try await startTextOnlyConversation ( auth: auth)
124- } else {
125- try await startVoiceConversation ( auth: auth)
126- }
127-
128- guard !Task. isCancelled, state. isConnecting else {
129- await activeConnectionManager? . disconnect ( )
130- throw CancellationError ( )
131- }
132- state = . connected( result. callInfo)
133- callbacks. onAgentReady ? ( )
134- return result
135- }
110+ func startVoiceConversation( _ auth: ConversationAuth . Voice ) async throws -> ConversationStartResult {
111+ let manager = dependencyProvider. webRTCConnectionManager
112+ let result = try await start ( agentId: auth. agentId, isTextOnly: false , using: manager) { config in
113+ let audioManager = ConversationAudioManager ( logger: logger)
114+ self . audioManager = audioManager
136115
137- private func startVoiceConversation(
138- auth: ConversationCredentials
139- ) async throws -> ConversationStartResult {
140- let webRTCConnectionManager = dependencyProvider. webRTCConnectionManager
141- prepareConversationStart (
142- auth: auth,
143- connectionManager: webRTCConnectionManager
144- )
145-
146- webRTCConnectionManager. onRemoteSpeakingChanged = { [ weak self] isSpeaking in
147- Task { @MainActor in
148- self ? . handleRemoteSpeakingUpdate ( isSpeaking: isSpeaking)
116+ manager. onRemoteSpeakingChanged = { [ weak self] isSpeaking in
117+ Task { @MainActor in
118+ self ? . handleRemoteSpeakingUpdate ( isSpeaking: isSpeaking)
119+ }
149120 }
150- }
151- webRTCConnectionManager. onTracksChanged = { [ weak self] in
152- Task { @MainActor in
153- self ? . refreshAudioObservers ( )
121+ manager. onTracksChanged = { [ weak self] in
122+ Task { @MainActor in
123+ self ? . refreshAudioObservers ( )
124+ }
125+ }
126+ await audioManager. configure ( with: config, callbacks: callbacks)
127+ guard state. isConnecting else {
128+ audioManager. cleanup ( )
129+ throw CancellationError ( )
130+ }
131+ if let pendingMuteState {
132+ audioManager. softwareMuteProcessor? . setMuted ( pendingMuteState)
154133 }
155- }
156-
157- await audioManager? . configure ( with: config, callbacks: callbacks)
158- if let pendingMuteState {
159- audioManager? . softwareMuteProcessor? . setMuted ( pendingMuteState)
160- }
161134
162- let result : ConversationStartResult
163- do {
164- result = try await webRTCConnectionManager. connect (
135+ return try await manager. connect (
165136 auth: auth,
166137 config: config,
167- onStartupStateChange: { [ weak self] stage in
168- self ? . updateStartupStage ( stage)
169- }
138+ onStartupStateChange: { [ weak self] in self ? . updateStartupStage ( $0) }
170139 )
171- } catch let error as ConversationError {
172- await handleStartupFailure ( error, disconnecting: webRTCConnectionManager)
173- throw error
174- } catch is CancellationError {
175- await handleStartupCancellation ( disconnecting: webRTCConnectionManager)
176- throw CancellationError ( )
177140 }
178141
179142 if let pendingMute = pendingMuteState {
@@ -182,15 +145,15 @@ final class Conversation: ObservableObject {
182145 if let softwareMuteProcessor = audioManager? . softwareMuteProcessor {
183146 softwareMuteProcessor. setMuted ( pendingMute)
184147 } else {
185- try await webRTCConnectionManager . setMicrophoneMuted ( pendingMute)
148+ try await manager . setMicrophoneMuted ( pendingMute)
186149 }
187150 } catch {
188151 logger. warning ( " Failed to apply pending mute state " , context: [ " error " : " \( error) " ] )
189152 }
190153 }
191154
192155 refreshAudioObservers ( )
193- return result
156+ return try await setConnected ( result)
194157 }
195158
196159 // MARK: - Audio observers
@@ -224,30 +187,16 @@ final class Conversation: ObservableObject {
224187 micObserverRegistry. attach ( to: inputTrack)
225188 }
226189
227- private func startTextOnlyConversation(
228- auth: ConversationCredentials
229- ) async throws -> ConversationStartResult {
230- let connectionManager = dependencyProvider. webSocketConnectionManager
231- prepareConversationStart (
232- auth: auth,
233- connectionManager: connectionManager
234- )
235-
236- do {
237- return try await connectionManager. connect (
190+ func startTextOnlyConversation( _ auth: ConversationAuth . TextOnly ) async throws -> ConversationStartResult {
191+ let manager = dependencyProvider. webSocketConnectionManager
192+ let result = try await start ( agentId: auth. agentId, isTextOnly: true , using: manager) { config in
193+ try await manager. connect (
238194 auth: auth,
239195 config: config,
240- onStartupStateChange: { [ weak self] stage in
241- self ? . updateStartupStage ( stage)
242- }
196+ onStartupStateChange: { [ weak self] in self ? . updateStartupStage ( $0) }
243197 )
244- } catch let error as ConversationError {
245- await handleStartupFailure ( error, disconnecting: connectionManager)
246- throw error
247- } catch is CancellationError {
248- await handleStartupCancellation ( disconnecting: connectionManager)
249- throw CancellationError ( )
250198 }
199+ return try await setConnected ( result)
251200 }
252201
253202 /// End and clean up.
@@ -375,30 +324,61 @@ final class Conversation: ObservableObject {
375324 state = . connecting( stage)
376325 }
377326
378- /// Common preparation shared by voice and text-only startup paths.
379- private func prepareConversationStart(
380- auth: ConversationCredentials ,
381- connectionManager: any ConnectionManaging
382- ) {
327+ private func start(
328+ agentId: String ,
329+ isTextOnly: Bool ,
330+ using manager: any ConnectionManaging ,
331+ connect: ( ConversationConfig ) async throws -> ConversationStartResult
332+ ) async throws -> ConversationStartResult {
333+ if state != . idle {
334+ if state. isEnded, activeConnectionManager == nil {
335+ throw CancellationError ( )
336+ }
337+ throw ConversationError . alreadyStarted
338+ }
339+
340+ var startConfig = config
341+ startConfig. conversationOverrides. textOnly = isTextOnly
342+
383343 state = . connecting( . preparing)
384- activeConnectionManager = connectionManager
344+ activeConnectionManager = manager
385345
386- activeContext = [ " agentId " : auth . agentId]
387- let mode = config . conversationOverrides . textOnly ? " text-only " : " voice "
346+ activeContext = [ " agentId " : agentId]
347+ let mode = isTextOnly ? " text-only " : " voice "
388348 logger. info ( " Starting \( mode) conversation " , context: activeContext)
389349
390350 setupAgentStateManager ( )
391351
392- let connectionManagerID = ObjectIdentifier ( connectionManager )
393- connectionManager . onEventReceived = { [ weak self] event in
352+ let connectionManagerID = ObjectIdentifier ( manager )
353+ manager . onEventReceived = { [ weak self] event in
394354 Task { @MainActor [ weak self] in
395355 await self ? . handleIncomingEvent ( event, from: connectionManagerID)
396356 }
397357 }
398- connectionManager . onDisconnected = { [ weak self] in
358+ manager . onDisconnected = { [ weak self] in
399359 guard let self else { return }
400360 await endConversation ( reason: . remoteDisconnected)
401361 }
362+
363+ do {
364+ return try await connect ( startConfig)
365+ } catch let error as ConversationError {
366+ await handleStartupFailure ( error, disconnecting: manager)
367+ throw error
368+ } catch is CancellationError {
369+ await handleStartupCancellation ( disconnecting: manager)
370+ throw CancellationError ( )
371+ }
372+ }
373+
374+ private func setConnected( _ result: ConversationStartResult ) async throws -> ConversationStartResult {
375+ guard !Task. isCancelled, state. isConnecting else {
376+ await activeConnectionManager? . disconnect ( )
377+ throw CancellationError ( )
378+ }
379+ state = . connected( result. callInfo)
380+ callbacks. onAgentReady ? ( )
381+ return result
402382 }
403383
404384 private func handleIncomingEvent( _ event: IncomingEvent , from connectionManagerID: ObjectIdentifier ) async {
0 commit comments