From 3f9cdc8544aa658c75b243c19163b7a1a54df43e Mon Sep 17 00:00:00 2001 From: dor-eitan <164745144+dor-eitan@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:00:51 +0300 Subject: [PATCH 1/6] feature: rename analytics `name` to `agentName` for consistency (#354) Aligns the Mixpanel property name with agents-ui convention (agentId, agentType, agentName) so both SDK and UI events use the same key. Co-authored-by: Claude Opus 4.6 (1M context) --- src/utils/analytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 657e8963..ea29dc26 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -54,7 +54,7 @@ export function getAgentInfo(agent: Agent) { maxResponseLength: promptCustomization?.max_response_length, agentId: agent.id, access: agent.access, - name: agent.preview_name, + agentName: agent.preview_name, ...(agent.access === 'public' ? { from: 'agent-template' } : {}), }; } From 5540b0c541d3739c1ff4cb718603f3e8e1247b5e Mon Sep 17 00:00:00 2001 From: dariusz-did Date: Tue, 7 Apr 2026 09:35:08 +0200 Subject: [PATCH 2/6] chore: remove triggersAvailable from SDK (#349) * chore: remove triggersAvailable from SDK Triggers availability is now computed in agents-ui directly from the agent object. Remove the unused triggersAvailable flag, related types, and getIsTriggersAvailable from the agent manager. * fix: prettier formatting in common.ts --- src/services/agent-manager/index.ts | 1 - src/services/streaming-manager/common.ts | 5 ----- src/services/streaming-manager/livekit-manager.ts | 1 - src/services/streaming-manager/webrtc-manager.ts | 2 -- src/types/entities/agents/manager.ts | 5 ----- src/types/stream/rtc.ts | 1 - 6 files changed, 15 deletions(-) diff --git a/src/services/agent-manager/index.ts b/src/services/agent-manager/index.ts index c8f43615..ccfa69e0 100644 --- a/src/services/agent-manager/index.ts +++ b/src/services/agent-manager/index.ts @@ -232,7 +232,6 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt agent: agentEntity, getStreamType: () => items.streamingManager?.streamType, getIsInterruptAvailable: () => items.streamingManager?.interruptAvailable ?? false, - getIsTriggersAvailable: () => items.streamingManager?.triggersAvailable ?? false, starterMessages: agentEntity.knowledge?.starter_message || [], getSTTToken: () => agentsApi.getSTTToken(agentEntity.id), changeMode, diff --git a/src/services/streaming-manager/common.ts b/src/services/streaming-manager/common.ts index 63295562..e777011a 100644 --- a/src/services/streaming-manager/common.ts +++ b/src/services/streaming-manager/common.ts @@ -85,9 +85,4 @@ export type StreamingManager boolean; - /** - * Get if the stream supports triggers - */ - getIsTriggersAvailable: () => boolean; - /** * Array of starter messages that will be sent to the agent when the chat starts */ diff --git a/src/types/stream/rtc.ts b/src/types/stream/rtc.ts index 0cf603bb..e74db024 100644 --- a/src/types/stream/rtc.ts +++ b/src/types/stream/rtc.ts @@ -39,7 +39,6 @@ export interface ICreateStreamRequestResponse extends StickyRequest { ice_servers: IceServer[]; fluent?: boolean; interrupt_enabled?: boolean; - triggers_enabled?: boolean; } export interface IceCandidate { From 28cc8dd230c6481c9e0a2f8c21d552d1a9fc904f Mon Sep 17 00:00:00 2001 From: omrizitrin-did Date: Thu, 9 Apr 2026 13:07:37 +0300 Subject: [PATCH 3/6] Merge pull request #355 from de-id/feature/disable-interrupt-support --- src/services/agent-manager/index.test.ts | 1 + src/services/agent-manager/index.ts | 3 +++ src/services/streaming-manager/livekit-manager.ts | 6 ++++-- src/types/stream/streams-v2.ts | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/services/agent-manager/index.test.ts b/src/services/agent-manager/index.test.ts index f6a7e584..10c50f5b 100644 --- a/src/services/agent-manager/index.test.ts +++ b/src/services/agent-manager/index.test.ts @@ -511,6 +511,7 @@ describe('createAgentManager', () => { describe('interrupt', () => { beforeEach(async () => { + mockStreamingManager.interruptAvailable = true; await manager.connect(); }); diff --git a/src/services/agent-manager/index.ts b/src/services/agent-manager/index.ts index ccfa69e0..d0756123 100644 --- a/src/services/agent-manager/index.ts +++ b/src/services/agent-manager/index.ts @@ -104,6 +104,9 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt }; const interrupt = ({ type }: Interrupt) => { + if (!items.streamingManager?.interruptAvailable) { + return; + } if (!items.streamingManager?.isInterruptible) return; const lastMessage = items.messages[items.messages.length - 1]; diff --git a/src/services/streaming-manager/livekit-manager.ts b/src/services/streaming-manager/livekit-manager.ts index a2c950fb..2840340e 100644 --- a/src/services/streaming-manager/livekit-manager.ts +++ b/src/services/streaming-manager/livekit-manager.ts @@ -129,6 +129,7 @@ export async function createLiveKitStreamingManager Date: Tue, 14 Apr 2026 17:37:46 +0300 Subject: [PATCH 4/6] fix: no-op unpublish when streaming manager is unavailable (#357) * fix: no-op unpublish when streaming manager is unavailable During session restart, the SDK already unpublishes streams in its own disconnect flow. When the UI cleanup effects then call unpublish a second time, the streaming manager is already deleted, causing an error. Return early instead of throwing to make the calls idempotent. Co-Authored-By: Claude Opus 4.6 (1M context) * test: update unpublish tests to expect no-op instead of throw Align tests with the graceful no-op behavior introduced in e4f8a74. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/services/agent-manager/index.test.ts | 12 ++++-------- src/services/agent-manager/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/services/agent-manager/index.test.ts b/src/services/agent-manager/index.test.ts index 10c50f5b..ffb524ab 100644 --- a/src/services/agent-manager/index.test.ts +++ b/src/services/agent-manager/index.test.ts @@ -743,12 +743,10 @@ describe('createAgentManager', () => { expect(mockUnpublish).toHaveBeenCalled(); }); - it('should throw error when unpublishMicrophoneStream is not available', async () => { + it('should no-op when unpublishMicrophoneStream is not available', async () => { mockStreamingManager.unpublishMicrophoneStream = undefined; - await expect(manager.unpublishMicrophoneStream?.()).rejects.toThrow( - 'unpublishMicrophoneStream is not available for this streaming manager' - ); + await expect(manager.unpublishMicrophoneStream?.()).resolves.toBeUndefined(); }); }); @@ -796,12 +794,10 @@ describe('createAgentManager', () => { expect(mockUnpublish).toHaveBeenCalled(); }); - it('should throw error when unpublishCameraStream is not available', async () => { + it('should no-op when unpublishCameraStream is not available', async () => { mockStreamingManager.unpublishCameraStream = undefined; - await expect(manager.unpublishCameraStream?.()).rejects.toThrow( - 'unpublishCameraStream is not available for this streaming manager' - ); + await expect(manager.unpublishCameraStream?.()).resolves.toBeUndefined(); }); }); diff --git a/src/services/agent-manager/index.ts b/src/services/agent-manager/index.ts index d0756123..db328c63 100644 --- a/src/services/agent-manager/index.ts +++ b/src/services/agent-manager/index.ts @@ -288,7 +288,7 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt }, async unpublishMicrophoneStream() { if (!items.streamingManager?.unpublishMicrophoneStream) { - throw new Error('unpublishMicrophoneStream is not available for this streaming manager'); + return; } return items.streamingManager.unpublishMicrophoneStream(); }, @@ -300,7 +300,7 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt }, async unpublishCameraStream() { if (!items.streamingManager?.unpublishCameraStream) { - throw new Error('unpublishCameraStream is not available for this streaming manager'); + return; } return items.streamingManager.unpublishCameraStream(); }, From 927c88c30f86430f6c9211e06ac254a342637781 Mon Sep 17 00:00:00 2001 From: dor-eitan <164745144+dor-eitan@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:26:46 +0300 Subject: [PATCH 5/6] feature: Add client tool registration via LiveKit RPC (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: add client tool registration via LiveKit RPC Add registerClientTool/unregisterClientTool to AgentManager. Each tool is registered as a per-tool RPC method on the LiveKit room. A Map provides reconnect safety — tools are re-flushed to new rooms on hard reconnect. Co-Authored-By: Claude Opus 4.6 (1M context) * test: add unit tests for client tool registration Cover registerClientTool, unregisterClientTool, pre-connect buffering, flush on connect, re-registration with latest handler dispatch. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/services/agent-manager/index.test.ts | 71 +++++++++++++++++++ src/services/agent-manager/index.ts | 42 +++++++++++ src/services/streaming-manager/common.ts | 13 ++++ .../streaming-manager/livekit-manager.ts | 7 ++ src/types/entities/agents/manager.ts | 15 ++++ src/types/stream/stream.ts | 2 + 6 files changed, 150 insertions(+) diff --git a/src/services/agent-manager/index.test.ts b/src/services/agent-manager/index.test.ts index ffb524ab..e5b914c0 100644 --- a/src/services/agent-manager/index.test.ts +++ b/src/services/agent-manager/index.test.ts @@ -832,4 +832,75 @@ describe('createAgentManager', () => { }); }); }); + + describe('registerClientTool', () => { + let manager: AgentManager; + + beforeEach(async () => { + mockStreamingManager.registerRpcMethod = jest.fn(); + mockStreamingManager.unregisterRpcMethod = jest.fn(); + manager = await createAgentManager('agent-123', mockOptions); + }); + + it('should register tool and call registerRpcMethod after connect', async () => { + const handler = jest.fn().mockResolvedValue('result'); + + await manager.connect(); + manager.registerClientTool('testTool', handler); + + expect(mockStreamingManager.registerRpcMethod).toHaveBeenCalledWith('testTool', expect.any(Function)); + }); + + it('should buffer tool registration before connect and flush on connect', async () => { + const handler = jest.fn().mockResolvedValue('result'); + + manager.registerClientTool('testTool', handler); + expect(mockStreamingManager.registerRpcMethod).not.toHaveBeenCalled(); + + await manager.connect(); + expect(mockStreamingManager.registerRpcMethod).toHaveBeenCalledWith('testTool', expect.any(Function)); + }); + + it('should not call registerRpcMethod twice for same tool name', async () => { + const handler1 = jest.fn().mockResolvedValue('result1'); + const handler2 = jest.fn().mockResolvedValue('result2'); + + await manager.connect(); + manager.registerClientTool('testTool', handler1); + manager.registerClientTool('testTool', handler2); + + expect(mockStreamingManager.registerRpcMethod).toHaveBeenCalledTimes(1); + }); + + it('should unregister tool from map and room', async () => { + const handler = jest.fn().mockResolvedValue('result'); + + await manager.connect(); + manager.registerClientTool('testTool', handler); + manager.unregisterClientTool('testTool'); + + expect(mockStreamingManager.unregisterRpcMethod).toHaveBeenCalledWith('testTool'); + }); + + it('should invoke latest handler when RPC is called after re-registration', async () => { + const handler1 = jest.fn().mockResolvedValue('result1'); + const handler2 = jest.fn().mockResolvedValue('result2'); + + await manager.connect(); + manager.registerClientTool('testTool', handler1); + + // Get the RPC handler that was registered + const rpcHandler = mockStreamingManager.registerRpcMethod.mock.calls[0][1]; + + // Re-register with new handler (same name — only updates Map) + manager.registerClientTool('testTool', handler2); + + // Invoke the RPC handler — should use handler2 from Map + const result = await rpcHandler({ payload: '{"key": "val"}' }); + + expect(handler1).not.toHaveBeenCalled(); + expect(handler2).toHaveBeenCalledWith({ key: 'val' }); + expect(result).toBe('result2'); + }); + }); }); diff --git a/src/services/agent-manager/index.ts b/src/services/agent-manager/index.ts index db328c63..0b719f37 100644 --- a/src/services/agent-manager/index.ts +++ b/src/services/agent-manager/index.ts @@ -5,6 +5,7 @@ import { Chat, ChatMode, ChatResponse, + ClientToolHandler, ConnectionState, CreateSessionV2Options, CreateStreamOptions, @@ -128,6 +129,43 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt } }; + const clientToolHandlers = new Map(); + + function createRpcHandler(toolName: string) { + return async (data: { payload: string }): Promise => { + const handler = clientToolHandlers.get(toolName); + if (!handler) { + throw new Error(`No handler registered for client tool: ${toolName}`); + } + try { + const args = JSON.parse(data.payload); + return await handler(args); + } catch (error) { + throw new Error(`Client tool "${toolName}" failed: ${(error as Error).message}`); + } + }; + } + + function flushClientToolsToRoom() { + for (const [name] of clientToolHandlers) { + items.streamingManager?.unregisterRpcMethod?.(name); + items.streamingManager?.registerRpcMethod?.(name, createRpcHandler(name)); + } + } + + function registerClientTool(name: string, handler: ClientToolHandler): void { + const isNew = !clientToolHandlers.has(name); + clientToolHandlers.set(name, handler); + if (isNew) { + items.streamingManager?.registerRpcMethod?.(name, createRpcHandler(name)); + } + } + + function unregisterClientTool(name: string): void { + clientToolHandlers.delete(name); + items.streamingManager?.unregisterRpcMethod?.(name); + } + const loadedTimestamp = Date.now(); defer(() => { analytics.track('agent-sdk', { event: 'loaded', ...getAnalyticsInfo(agentEntity) }, loadedTimestamp); @@ -197,6 +235,8 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt items.socketManager = socketManager; items.chat = chat; + flushClientToolsToRoom(); + firstConnection = false; analytics.enrich({ @@ -554,5 +594,7 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt }); }, interrupt, + registerClientTool, + unregisterClientTool, }; } diff --git a/src/services/streaming-manager/common.ts b/src/services/streaming-manager/common.ts index e777011a..a8c4a432 100644 --- a/src/services/streaming-manager/common.ts +++ b/src/services/streaming-manager/common.ts @@ -85,4 +85,17 @@ export type StreamingManager Promise): void; + + /** + * Unregister a previously registered RPC method. + * supported only for livekit manager + */ + unregisterRpcMethod?(method: string): void; }; diff --git a/src/services/streaming-manager/livekit-manager.ts b/src/services/streaming-manager/livekit-manager.ts index 2840340e..e714d376 100644 --- a/src/services/streaming-manager/livekit-manager.ts +++ b/src/services/streaming-manager/livekit-manager.ts @@ -721,6 +721,13 @@ export async function createLiveKitStreamingManager Promise) { + room?.registerRpcMethod(method, handler); + }, + unregisterRpcMethod(method: string) { + room?.unregisterRpcMethod(method); + }, + sessionId, streamId: sessionId, streamType, diff --git a/src/types/entities/agents/manager.ts b/src/types/entities/agents/manager.ts index cc349233..7ef2b6cd 100644 --- a/src/types/entities/agents/manager.ts +++ b/src/types/entities/agents/manager.ts @@ -2,6 +2,7 @@ import { STTTokenResponse } from '@sdk/types'; import { Auth } from '@sdk/types/auth'; import { AgentActivityState, + ClientToolHandler, CompatibilityMode, ConnectionState, ConnectivityState, @@ -279,4 +280,18 @@ export interface AgentManager { * Only available for Fluent streams and when there's an active video to interrupt */ interrupt: (interrupt: Interrupt) => void; + + /** + * Register a handler for a client tool. When the agent's LLM calls this tool, + * the handler executes on the client and returns the result to the LLM. + * @param name - Tool name (must match the tool name defined in the agent config) + * @param handler - Async function receiving args, must return a JSON string (max 15KiB) + */ + registerClientTool: (name: string, handler: ClientToolHandler) => void; + + /** + * Remove a previously registered client tool handler. + * @param name - Tool name to unregister + */ + unregisterClientTool: (name: string) => void; } diff --git a/src/types/stream/stream.ts b/src/types/stream/stream.ts index 9fef29de..55f3c5df 100644 --- a/src/types/stream/stream.ts +++ b/src/types/stream/stream.ts @@ -194,6 +194,8 @@ export interface StreamInterruptPayload { timestamp: number; } +export type ClientToolHandler = (args: Record) => Promise; + export interface ToolCallingPayload { execution_id: string; tool_name: string; From 7aa69ae2decb6df54ffac2b33db5c853069594c2 Mon Sep 17 00:00:00 2001 From: Dor Eitan Date: Tue, 14 Apr 2026 18:30:11 +0300 Subject: [PATCH 6/6] bump 1.1.57 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f6b8de21..7e3f4db0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@d-id/client-sdk", "private": false, - "version": "1.1.56", + "version": "1.1.57", "type": "module", "description": "d-id client sdk", "repository": {