Skip to content

Commit 8c22851

Browse files
renal128cursoragent
andcommitted
refactor: remove feedback availability callback
Feedback is available whenever the conversation is connected, so consumers can derive availability directly from state. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6eea761 commit 8c22851

6 files changed

Lines changed: 17 additions & 96 deletions

File tree

Documentation/Usage.md

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -498,10 +498,6 @@ let callbacks = ConversationCallbacks(
498498
onAudioAlignment: { alignment in
499499
// Real-time word highlighting timing.
500500
},
501-
onCanSendFeedbackChange: { canSend in
502-
// Enable/disable your 'Thumbs Up/Down' buttons in the UI
503-
self.showFeedbackUI = canSend
504-
},
505501
onError: { error in
506502
print("A non-fatal or startup error occurred: \(error)")
507503
}
@@ -553,35 +549,29 @@ let config = ConversationConfig(startupConfiguration: startupConfig)
553549

554550
## Feedback & Context {#feedback-context}
555551

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

558554
```swift
559-
// 1) Setup: react to feedback availability
560-
var canSendFeedback = false
561-
562555
let callbacks = ConversationCallbacks(
563556
onAgentResponse: { text, eventId in
564557
print("Agent:", text, "(event:", eventId, ")")
565-
},
566-
onCanSendFeedbackChange: { can in
567-
canSendFeedback = can
568-
// e.g., refresh your UI
569558
}
570559
)
571560

572-
let conversation = try await ElevenLabs.startConversation(agentId: "agent_123", callbacks: callbacks)
561+
let client = ConversationClient(callbacks: callbacks)
562+
_ = try await client.startConversation(agentId: "agent_123")
573563

574-
// 2) Sending feedback from your UI
575-
func thumbsUp(latestEventId: Int) {
576-
Task { try? await conversation.sendFeedback(.like, eventId: latestEventId) }
564+
func thumbsUp(eventId: Int) async throws {
565+
guard client.state.isConnected else { return }
566+
try await client.sendFeedback(.like, eventId: eventId)
577567
}
578568

579-
func thumbsDown(latestEventId: Int) {
580-
Task { try? await conversation.sendFeedback(.dislike, eventId: latestEventId) }
569+
func thumbsDown(eventId: Int) async throws {
570+
guard client.state.isConnected else { return }
571+
try await client.sendFeedback(.dislike, eventId: eventId)
581572
}
582573

583-
// 3) Contextual updates (e.g., user preferences)
584-
Task { try? await conversation.updateContext("user_prefers_detailed_answers=true") }
574+
try await client.updateContext("user_prefers_detailed_answers=true")
585575
```
586576

587577
---

Sources/ElevenLabs/Internal/Conversation/Conversation+Events.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,8 @@ extension Conversation {
1414

1515
case let .agentResponse(e):
1616
upsertAgentMessage(content: e.response, eventId: e.eventId)
17-
lastAgentEventId = e.eventId
1817
agentStateManager?.processSignal(.agentResponse)
1918
callbacks.onAgentResponse?(e.response, e.eventId)
20-
if lastFeedbackSubmittedEventId.map({ e.eventId > $0 }) ?? true {
21-
callbacks.onCanSendFeedbackChange?(true)
22-
}
2319

2420
case let .agentResponseCorrection(correction):
2521
upsertAgentMessage(content: correction.correctedAgentResponse, eventId: correction.eventId)
@@ -48,7 +44,6 @@ extension Conversation {
4844
speakingTimer?.cancel()
4945
applyStateSignal(.interruption, fallback: .listening)
5046
callbacks.onInterruption?(interruptionEvent.eventId)
51-
callbacks.onCanSendFeedbackChange?(false)
5247

5348
case let .conversationMetadata(metadata):
5449
// Store the conversation metadata for public access

Sources/ElevenLabs/Public/Conversation/Conversation.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ final class Conversation: ObservableObject {
3030
/// Current MCP connection status for all integrations
3131
@Published var mcpConnectionStatus: MCPConnectionStatusEvent?
3232

33-
var lastAgentEventId: Int?
34-
var lastFeedbackSubmittedEventId: Int?
35-
3633
/// Pending mute state to apply after connection completes.
3734
/// Allows setting mute state during connection phase.
3835
private var pendingMuteState: Bool?
@@ -258,7 +255,6 @@ final class Conversation: ObservableObject {
258255

259256
// Call user's onDisconnect callback if provided
260257
callbacks.onDisconnect?(disconnectReason)
261-
callbacks.onCanSendFeedbackChange?(false)
262258
}
263259

264260
/// Send a text message to the agent.
@@ -331,8 +327,6 @@ final class Conversation: ObservableObject {
331327

332328
let event = OutgoingEvent.feedback(FeedbackEvent(score: score, eventId: eventId))
333329
try await publish(event)
334-
lastFeedbackSubmittedEventId = eventId
335-
callbacks.onCanSendFeedbackChange?(false)
336330
}
337331

338332
/// Approve or reject an MCP tool call request from the agent.
@@ -387,7 +381,6 @@ final class Conversation: ObservableObject {
387381
let mode = config.conversationOverrides.textOnly ? "text-only" : "voice"
388382
logger.info("Starting \(mode) conversation", context: activeContext)
389383

390-
callbacks.onCanSendFeedbackChange?(false)
391384
setupAgentStateManager()
392385

393386
connectionManager.onEventReceived = { [weak self, weak connectionManager] event in
@@ -434,10 +427,6 @@ final class Conversation: ObservableObject {
434427
cleanupTransientResources()
435428

436429
pendingToolCalls.removeAll()
437-
438-
lastAgentEventId = nil
439-
lastFeedbackSubmittedEventId = nil
440-
callbacks.onCanSendFeedbackChange?(false)
441430
}
442431

443432
private func cleanupTransientResources() {

Sources/ElevenLabs/Public/Conversation/ConversationCallbacks.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ public struct ConversationCallbacks: Sendable {
4545
/// Called when audio alignment metadata is emitted.
4646
public var onAudioAlignment: (@Sendable (AudioAlignment) -> Void)?
4747

48-
/// Called when feedback availability changes.
49-
public var onCanSendFeedbackChange: (@Sendable (Bool) -> Void)?
50-
5148
/// Called when a client tool call is received without a registered handler.
5249
public var onClientToolCall: (@Sendable (ClientToolCallEvent) -> Void)?
5350

@@ -69,7 +66,6 @@ public struct ConversationCallbacks: Sendable {
6966
onInterruption: (@Sendable (_ eventId: Int) -> Void)? = nil,
7067
onVadScore: (@Sendable (_ score: Double) -> Void)? = nil,
7168
onAudioAlignment: (@Sendable (AudioAlignment) -> Void)? = nil,
72-
onCanSendFeedbackChange: (@Sendable (Bool) -> Void)? = nil,
7369
onClientToolCall: (@Sendable (ClientToolCallEvent) -> Void)? = nil,
7470
onAgentStateChange: (@Sendable (ElevenLabs.AgentState) -> Void)? = nil
7571
) {
@@ -87,7 +83,6 @@ public struct ConversationCallbacks: Sendable {
8783
self.onInterruption = onInterruption
8884
self.onVadScore = onVadScore
8985
self.onAudioAlignment = onAudioAlignment
90-
self.onCanSendFeedbackChange = onCanSendFeedbackChange
9186
self.onClientToolCall = onClientToolCall
9287
self.onAgentStateChange = onAgentStateChange
9388
}

Tests/ElevenLabsTests/Unit/ConversationEventHandlerTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ final class ConversationEventHandlerTests: XCTestCase {
7979
XCTAssertEqual(conversation.messages.last?.content, "I am an AI")
8080
XCTAssertEqual(conversation.messages.last?.role, .agent)
8181
XCTAssertEqual(conversation.messages.last?.eventId, 456)
82-
XCTAssertEqual(conversation.lastAgentEventId, 456)
8382
}
8483

8584
func testAgentResponseFinalizesStreamedMessageInsteadOfDuplicating() async {

Tests/ElevenLabsTests/Unit/ConversationTests.swift

Lines changed: 7 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -552,40 +552,21 @@ final class ConversationTests: XCTestCase {
552552
XCTAssertEqual(errorsAfterInitFailure, [.connectionFailed("Publish failed")])
553553
}
554554

555-
func testAgentResponseCallbackTogglesFeedbackAvailability() async throws {
556-
let gotResponse = expectation(description: "agent response")
557-
// Feedback flips more than once (start→false, response→true, feedback→false).
558-
let feedbackStates = ValueRecorder<Bool>()
559-
560-
let callbacks = makeCallbacks(configure: { callbacks in
561-
callbacks.onAgentResponse = { text, eventId in
562-
XCTAssertEqual(text, "Hello")
563-
XCTAssertEqual(eventId, 42)
564-
gotResponse.fulfill()
565-
}
566-
callbacks.onCanSendFeedbackChange = { canSend in
567-
Task { await feedbackStates.append(canSend) }
568-
}
569-
})
570-
555+
func testSendFeedbackWhileConnected() async throws {
571556
let conversation = Conversation(
572557
dependencyProvider: dependencyProvider,
573-
config: makeConfig(),
574-
callbacks: callbacks
558+
config: makeConfig()
575559
)
576560

577561
_ = try await conversation.start(auth: .publicAgent(id: "test"))
578562

579-
mockWebRTCConnectionManager.deliver(.agentResponse(AgentResponseEvent(response: "Hello", eventId: 42)))
580-
581-
await fulfillment(of: [gotResponse], timeout: 1.0)
582-
let initialFeedbackState = await waitForLastValue(feedbackStates) { $0 == true }
583-
XCTAssertEqual(initialFeedbackState, true)
584-
585563
try await conversation.sendFeedback(FeedbackEvent.Score.like, eventId: 42)
586564

587-
let updatedFeedbackState = await waitForLastValue(feedbackStates) { $0 == false }
588-
XCTAssertEqual(updatedFeedbackState, false)
565+
let payload = try XCTUnwrap(mockWebRTCConnectionManager.publishedPayloads.last)
566+
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: payload) as? [String: Any])
567+
XCTAssertEqual(json["type"] as? String, "feedback")
568+
XCTAssertEqual(json["score"] as? String, "like")
569+
XCTAssertEqual(json["event_id"] as? Int, 42)
589570
}
590571

591572
func testVadScoreCallbackReceivesScores() async throws {
@@ -632,34 +613,6 @@ final class ConversationTests: XCTestCase {
632613
await fulfillment(of: [gotTool], timeout: 1.0)
633614
}
634615

635-
func testInterruptionCallbackDisablesFeedback() async throws {
636-
let gotInterruption = expectation(description: "interruption")
637-
let feedbackStates = ValueRecorder<Bool>()
638-
639-
let callbacks = makeCallbacks(configure: { callbacks in
640-
callbacks.onInterruption = { id in
641-
XCTAssertEqual(id, 7)
642-
gotInterruption.fulfill()
643-
}
644-
callbacks.onCanSendFeedbackChange = { canSend in
645-
Task { await feedbackStates.append(canSend) }
646-
}
647-
})
648-
649-
let conversation = Conversation(
650-
dependencyProvider: dependencyProvider,
651-
config: makeConfig(),
652-
callbacks: callbacks
653-
)
654-
_ = try await conversation.start(auth: .publicAgent(id: "test"))
655-
656-
mockWebRTCConnectionManager.deliver(.interruption(InterruptionEvent(eventId: 7)))
657-
658-
await fulfillment(of: [gotInterruption], timeout: 1.0)
659-
let interruptionFeedbackState = await waitForLastValue(feedbackStates) { $0 == false }
660-
XCTAssertEqual(interruptionFeedbackState, false)
661-
}
662-
663616
@MainActor
664617
func testSendToolResultWhenNotConnected() async {
665618
do {

0 commit comments

Comments
 (0)