// SPDX-FileCopyrightText: 2026 LiveKit, Inc. // // SPDX-License-Identifier: Apache-2.0 import { Behavior, FunctionResponseScheduling } from '@google/genai'; import { llm } from '@livekit/agents'; import { describe, expect, it, vi } from 'vitest'; import { RealtimeSession } from './realtime_api.js'; type ToolCallStatus = { name: string; status: 'pending' | 'continuing' | 'completed' | 'cancelled'; willContinueSent: boolean; createdAt: number; }; type RealtimeSessionInternals = { options: { toolBehavior?: Behavior; toolResponseScheduling?: FunctionResponseScheduling; vertexai?: boolean; }; currentGeneration?: { functionChannel: { closed: boolean; write: ReturnType; }; }; pendingToolCallIds: Set; toolCallStatuses: Map; toolResponseCallIds: WeakMap, string>; sendClientEvent: ReturnType; markCurrentGenerationDone: ReturnType; getToolResultsForRealtime( ctx: llm.ChatContext, vertexai: boolean, ): { functionResponses: Array> } | undefined; handleToolCall(toolCall: { functionCalls?: Array<{ id?: string; name?: string; args?: Record; }>; }): void; clearPendingToolCallIdsForResponses(functionResponses: Array>): void; }; const schedulingModes = [ FunctionResponseScheduling.SILENT, FunctionResponseScheduling.WHEN_IDLE, FunctionResponseScheduling.INTERRUPT, ]; function createSessionForTest( toolResponseScheduling: FunctionResponseScheduling, ): RealtimeSessionInternals { const session = Object.create(RealtimeSession.prototype) as RealtimeSessionInternals; session.options = { toolBehavior: Behavior.NON_BLOCKING, toolResponseScheduling, vertexai: false, }; session.pendingToolCallIds = new Set(); session.toolCallStatuses = new Map(); session.toolResponseCallIds = new WeakMap(); session.sendClientEvent = vi.fn(); session.markCurrentGenerationDone = vi.fn(); session.currentGeneration = { functionChannel: { closed: false, write: vi.fn(), }, }; return session; } describe('Google Realtime non-blocking tool scheduling', () => { it.each(schedulingModes)( 'sends %s on the immediate willContinue response', (toolResponseScheduling) => { const session = createSessionForTest(toolResponseScheduling); session.handleToolCall({ functionCalls: [ { id: 'call_123', name: 'getWeather', args: { location: 'Seattle' }, }, ], }); expect(session.sendClientEvent).toHaveBeenCalledWith({ type: 'tool_response', value: { functionResponses: [ { id: 'call_123', name: 'getWeather', response: {}, scheduling: toolResponseScheduling, willContinue: true, }, ], }, }); expect(session.toolCallStatuses.get('call_123')).toMatchObject({ name: 'getWeather', status: 'continuing', willContinueSent: true, }); expect(session.pendingToolCallIds.has('call_123')).toBe(true); }, ); it.each(schedulingModes)( 'sends %s on the final non-blocking tool response', (toolResponseScheduling) => { const session = createSessionForTest(toolResponseScheduling); session.toolCallStatuses.set('call_123', { name: 'getWeather', status: 'continuing', willContinueSent: true, createdAt: Date.now(), }); const ctx = llm.ChatContext.empty(); ctx.insert( llm.FunctionCallOutput.create({ callId: 'call_123', name: 'getWeather', output: 'The weather in Seattle is sunny today.', isError: false, }), ); const result = session.getToolResultsForRealtime(ctx, false); expect(result?.functionResponses).toEqual([ { id: 'call_123', name: 'getWeather', response: { output: 'The weather in Seattle is sunny today.' }, scheduling: toolResponseScheduling, willContinue: false, }, ]); expect(session.toolCallStatuses.get('call_123')).toMatchObject({ status: 'completed', willContinueSent: true, }); }, ); it('clears pending tool calls for VertexAI responses without ids', () => { const session = createSessionForTest(FunctionResponseScheduling.WHEN_IDLE); session.pendingToolCallIds.add('call_123'); const ctx = llm.ChatContext.empty(); ctx.insert( llm.FunctionCallOutput.create({ callId: 'call_123', name: 'getWeather', output: 'The weather in Seattle is sunny today.', isError: false, }), ); const result = session.getToolResultsForRealtime(ctx, true); expect(result?.functionResponses).toEqual([ { name: 'getWeather', response: { output: 'The weather in Seattle is sunny today.' }, scheduling: FunctionResponseScheduling.WHEN_IDLE, }, ]); session.clearPendingToolCallIdsForResponses(result?.functionResponses ?? []); expect(session.pendingToolCallIds.has('call_123')).toBe(false); }); });