@@ -102,6 +102,39 @@ public class ElevenLabsSDK {
102102 Data ( base64Encoded: base64)
103103 }
104104
105+ // MARK: - Client Tools
106+
107+ public typealias ClientToolHandler = @Sendable ( Parameters) async throws -> String ?
108+
109+ public typealias Parameters = [ String : Any ]
110+
111+ public struct ClientTools : Sendable {
112+ private var tools : [ String : ClientToolHandler ] = [ : ]
113+ private let lock = NSLock ( ) // Ensure thread safety
114+
115+ public init ( ) { }
116+
117+ public mutating func register( _ name: String , handler: @escaping @Sendable ClientToolHandler ) {
118+ lock. withLock {
119+ tools [ name] = handler
120+ }
121+ }
122+
123+ public func handle( _ name: String , parameters: Parameters ) async throws -> String ? {
124+ let handler : ClientToolHandler ? = lock. withLock { tools [ name] }
125+ guard let handler = handler else {
126+ throw ClientToolError . handlerNotFound ( name)
127+ }
128+ return try await handler ( parameters)
129+ }
130+ }
131+
132+ public enum ClientToolError : Error {
133+ case handlerNotFound( String )
134+ case invalidParameters
135+ case executionFailed( String )
136+ }
137+
105138 // MARK: - Audio Processing
106139
107140 public class AudioConcatProcessor {
@@ -190,14 +223,14 @@ public class ElevenLabsSDK {
190223 public let overrides : ConversationConfigOverride ?
191224 public let customLlmExtraBody : [ String : LlmExtraBodyValue ] ?
192225
193- public init ( signedUrl: String , overrides: ConversationConfigOverride ? = nil , customLlmExtraBody: [ String : LlmExtraBodyValue ] ? = nil ) {
226+ public init ( signedUrl: String , overrides: ConversationConfigOverride ? = nil , customLlmExtraBody: [ String : LlmExtraBodyValue ] ? = nil , clientTools _ : ClientTools = ClientTools ( ) ) {
194227 self . signedUrl = signedUrl
195228 agentId = nil
196229 self . overrides = overrides
197230 self . customLlmExtraBody = customLlmExtraBody
198231 }
199232
200- public init ( agentId: String , overrides: ConversationConfigOverride ? = nil , customLlmExtraBody: [ String : LlmExtraBodyValue ] ? = nil ) {
233+ public init ( agentId: String , overrides: ConversationConfigOverride ? = nil , customLlmExtraBody: [ String : LlmExtraBodyValue ] ? = nil , clientTools _ : ClientTools = ClientTools ( ) ) {
201234 self . agentId = agentId
202235 signedUrl = nil
203236 self . overrides = overrides
@@ -559,6 +592,7 @@ public class ElevenLabsSDK {
559592 private let input : Input
560593 private let output : Output
561594 private let callbacks : Callbacks
595+ private let clientTools : ClientTools ?
562596
563597 private let modeLock = NSLock ( )
564598 private let statusLock = NSLock ( )
@@ -649,11 +683,12 @@ public class ElevenLabsSDK {
649683 }
650684 }
651685
652- private init ( connection: Connection , input: Input , output: Output , callbacks: Callbacks ) {
686+ private init ( connection: Connection , input: Input , output: Output , callbacks: Callbacks , clientTools : ClientTools ? ) {
653687 self . connection = connection
654688 self . input = input
655689 self . output = output
656690 self . callbacks = callbacks
691+ self . clientTools = clientTools
657692
658693 // Set the onProcess callback
659694 audioConcatProcessor. onProcess = { [ weak self] finished in
@@ -672,8 +707,9 @@ public class ElevenLabsSDK {
672707 /// - Parameters:
673708 /// - config: Session configuration
674709 /// - callbacks: Callbacks for conversation events
710+ /// - clientTools: Client tools callbacks (optional)
675711 /// - Returns: A started `Conversation` instance
676- public static func startSession( config: SessionConfig , callbacks: Callbacks = Callbacks ( ) ) async throws -> Conversation {
712+ public static func startSession( config: SessionConfig , callbacks: Callbacks = Callbacks ( ) , clientTools : ClientTools ? = nil ) async throws -> Conversation {
677713 // Step 1: Configure the audio session
678714 try ElevenLabsSDK . configureAudioSession ( )
679715
@@ -687,7 +723,7 @@ public class ElevenLabsSDK {
687723 let output = try await Output . create ( sampleRate: Double ( connection. sampleRate) )
688724
689725 // Step 5: Initialize the Conversation
690- let conversation = Conversation ( connection: connection, input: input, output: output, callbacks: callbacks)
726+ let conversation = Conversation ( connection: connection, input: input, output: output, callbacks: callbacks, clientTools : clientTools )
691727
692728 // Step 6: Start the AVAudioEngine
693729 try output. engine. start ( )
@@ -740,6 +776,9 @@ public class ElevenLabsSDK {
740776 }
741777
742778 switch type {
779+ case " client_tool_call " :
780+ handleClientToolCall ( json)
781+
743782 case " interruption " :
744783 handleInterruptionEvent ( json)
745784
@@ -776,6 +815,52 @@ public class ElevenLabsSDK {
776815 }
777816 }
778817
818+ private func handleClientToolCall( _ json: [ String : Any ] ) {
819+ guard let toolCall = json [ " client_tool_call " ] as? [ String : Any ] ,
820+ let toolName = toolCall [ " tool_name " ] as? String ,
821+ let toolCallId = toolCall [ " tool_call_id " ] as? String ,
822+ let parameters = toolCall [ " parameters " ] as? [ String : Any ]
823+ else {
824+ callbacks. onError ( " Invalid client tool call format " , json)
825+ return
826+ }
827+
828+ // Serialize parameters to JSON Data for thread-safety
829+ let serializedParameters : Data
830+ do {
831+ serializedParameters = try JSONSerialization . data ( withJSONObject: parameters, options: [ ] )
832+ } catch {
833+ callbacks. onError ( " Failed to serialize parameters " , error)
834+ return
835+ }
836+
837+ // Execute in a Task (now safe because of serializedParameters)
838+ Task { [ toolName, toolCallId, serializedParameters] in
839+ do {
840+ // Deserialize within the Task to pass into clientTools.handle
841+ let deserializedParameters = try JSONSerialization . jsonObject ( with: serializedParameters) as? [ String : Any ] ?? [ : ]
842+
843+ let result = try await clientTools? . handle ( toolName, parameters: deserializedParameters)
844+
845+ let response : [ String : Any ] = [
846+ " type " : " client_tool_result " ,
847+ " tool_call_id " : toolCallId,
848+ " result " : result ?? " " ,
849+ " is_error " : false ,
850+ ]
851+ sendWebSocketMessage ( response)
852+ } catch {
853+ let response : [ String : Any ] = [
854+ " type " : " client_tool_result " ,
855+ " tool_call_id " : toolCallId,
856+ " result " : error. localizedDescription,
857+ " is_error " : true ,
858+ ]
859+ sendWebSocketMessage ( response)
860+ }
861+ }
862+ }
863+
779864 private func handleInterruptionEvent( _ json: [ String : Any ] ) {
780865 guard let event = json [ " interruption_event " ] as? [ String : Any ] ,
781866 let eventId = event [ " event_id " ] as? Int else { return }
0 commit comments