@@ -44,6 +44,12 @@ final class Conversation: ObservableObject {
4444 let agentObserverRegistry = AudioObserverRegistry ( )
4545 let micObserverRegistry = AudioObserverRegistry ( )
4646
47+ private let dependencyProvider : any ConversationDependencyProvider
48+ private var activeConnectionManager : ( any ConnectionManaging ) ?
49+ private var activeWebRTCConnectionManager : ( any WebRTCConnectionManaging ) ? {
50+ activeConnectionManager as? any WebRTCConnectionManaging
51+ }
52+
4753 /// Internal LiveKit tracks used to attach ``ConversationAudioObserver``s.
4854 private var inputTrack : ( any AudioTrackProtocol ) ? {
4955 activeWebRTCConnectionManager? . inputTrack
@@ -83,26 +89,30 @@ final class Conversation: ObservableObject {
8389
8490 func startVoiceConversation( _ auth: ConversationAuth . Voice ) async throws -> ConversationStartResult {
8591 let manager = dependencyProvider. webRTCConnectionManager
86- let audioManager = ConversationAudioManager ( logger: logger)
87- self . audioManager = audioManager
92+ let result = try await start ( agentId: auth. agentId, isTextOnly: false , using: manager) { config in
93+ let audioManager = ConversationAudioManager ( logger: logger)
94+ self . audioManager = audioManager
8895
89- manager. onRemoteSpeakingChanged = { [ weak self] isSpeaking in
90- Task { @MainActor in
91- self ? . handleRemoteSpeakingUpdate ( isSpeaking: isSpeaking)
96+ manager. onRemoteSpeakingChanged = { [ weak self] isSpeaking in
97+ Task { @MainActor in
98+ self ? . handleRemoteSpeakingUpdate ( isSpeaking: isSpeaking)
99+ }
92100 }
93- }
94- manager. onTracksChanged = { [ weak self] in
95- Task { @MainActor in
96- self ? . refreshAudioObservers ( )
101+ manager. onTracksChanged = { [ weak self] in
102+ Task { @MainActor in
103+ self ? . refreshAudioObservers ( )
104+ }
105+ }
106+ await audioManager. configure ( with: config, callbacks: callbacks)
107+ guard state. isConnecting else {
108+ audioManager. cleanup ( )
109+ throw CancellationError ( )
110+ }
111+ if let pendingMuteState {
112+ audioManager. softwareMuteProcessor? . setMuted ( pendingMuteState)
97113 }
98- }
99- await audioManager. configure ( with: config, callbacks: callbacks)
100- if let pendingMuteState {
101- audioManager. softwareMuteProcessor? . setMuted ( pendingMuteState)
102- }
103114
104- let result = try await start ( agentId: auth. agentId, isTextOnly: false , using: manager) { config in
105- try await manager. connect (
115+ return try await manager. connect (
106116 auth: auth,
107117 config: config,
108118 onStartupStateChange: { [ weak self] in self ? . updateStartupStage ( $0) }
@@ -112,7 +122,7 @@ final class Conversation: ObservableObject {
112122 if let pendingMute = pendingMuteState {
113123 pendingMuteState = nil
114124 do {
115- if let softwareMuteProcessor = audioManager. softwareMuteProcessor {
125+ if let softwareMuteProcessor = audioManager? . softwareMuteProcessor {
116126 softwareMuteProcessor. setMuted ( pendingMute)
117127 } else {
118128 try await manager. setMicrophoneMuted ( pendingMute)
@@ -277,54 +287,16 @@ final class Conversation: ObservableObject {
277287
278288 // MARK: - Private
279289
280- private let dependencyProvider : any ConversationDependencyProvider
281- private var activeConnectionManager : ( any ConnectionManaging ) ?
282- private var activeWebRTCConnectionManager : ( any WebRTCConnectionManaging ) ? {
283- activeConnectionManager as? any WebRTCConnectionManaging
284- }
285-
286- private func updateStartupStage( _ stage: ConversationStartupState ) {
287- guard state. isConnecting, state != . connecting( stage) else { return }
288- state = . connecting( stage)
289- }
290-
291- private func setupAgentStateManager( ) {
292- guard let configuration = config. agentStateConfiguration else { return }
293- let manager = AgentStateManager ( configuration: configuration)
294- manager. onStateChange = { [ weak self] state in
295- self ? . agentState = state
296- self ? . callbacks. onAgentStateChange ? ( state)
297- }
298- agentStateManager = manager
299- }
300-
301- /// Forward a signal to the event-based state manager, or fall back to directly setting `agentState`.
302- private func applyStateSignal( _ signal: AgentStateSignal , fallback: AgentState ) {
303- if let manager = agentStateManager {
304- manager. processSignal ( signal)
305- } else {
306- agentState = fallback
307- }
308- }
309-
310- private func handleRemoteSpeakingUpdate( isSpeaking: Bool ) {
311- if let manager = agentStateManager {
312- manager. processSignal ( isSpeaking ? . agentStartedSpeaking : . agentStoppedSpeaking)
313- } else if isSpeaking {
314- speakingTimer? . cancel ( )
315- agentState = . speaking
316- } else {
317- scheduleBackToListening ( delay: 1.0 )
318- }
319- }
320-
321290 private func start(
322291 agentId: String ,
323292 isTextOnly: Bool ,
324293 using manager: any ConnectionManaging ,
325294 connect: ( ConversationConfig ) async throws -> ConversationStartResult
326295 ) async throws -> ConversationStartResult {
327- guard state == . idle else {
296+ if state != . idle {
297+ if state. isEnded, activeConnectionManager == nil {
298+ throw CancellationError ( )
299+ }
328300 throw ConversationError . alreadyStarted
329301 }
330302
@@ -372,6 +344,41 @@ final class Conversation: ObservableObject {
372344 return result
373345 }
374346
347+ private func updateStartupStage( _ stage: ConversationStartupState ) {
348+ guard state. isConnecting, state != . connecting( stage) else { return }
349+ state = . connecting( stage)
350+ }
351+
352+ private func setupAgentStateManager( ) {
353+ guard let configuration = config. agentStateConfiguration else { return }
354+ let manager = AgentStateManager ( configuration: configuration)
355+ manager. onStateChange = { [ weak self] state in
356+ self ? . agentState = state
357+ self ? . callbacks. onAgentStateChange ? ( state)
358+ }
359+ agentStateManager = manager
360+ }
361+
362+ /// Forward a signal to the event-based state manager, or fall back to directly setting `agentState`.
363+ private func applyStateSignal( _ signal: AgentStateSignal , fallback: AgentState ) {
364+ if let manager = agentStateManager {
365+ manager. processSignal ( signal)
366+ } else {
367+ agentState = fallback
368+ }
369+ }
370+
371+ private func handleRemoteSpeakingUpdate( isSpeaking: Bool ) {
372+ if let manager = agentStateManager {
373+ manager. processSignal ( isSpeaking ? . agentStartedSpeaking : . agentStoppedSpeaking)
374+ } else if isSpeaking {
375+ speakingTimer? . cancel ( )
376+ agentState = . speaking
377+ } else {
378+ scheduleBackToListening ( delay: 1.0 )
379+ }
380+ }
381+
375382 private func handleIncomingEvent( _ event: IncomingEvent , from connectionManagerID: ObjectIdentifier ) async {
376383 guard let activeConnectionManager,
377384 ObjectIdentifier ( activeConnectionManager) == connectionManagerID,
0 commit comments