UNPKG

@just-every/ensemble

Version:

LLM provider abstraction layer with unified streaming interface

541 lines 21.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ensembleLive = ensembleLive; exports.ensembleLiveAudio = ensembleLiveAudio; exports.ensembleLiveText = ensembleLiveText; const model_provider_js_1 = require("../model_providers/model_provider.cjs"); const model_provider_js_2 = require("../model_providers/model_provider.cjs"); const message_history_js_1 = require("../utils/message_history.cjs"); const tool_execution_manager_js_1 = require("../utils/tool_execution_manager.cjs"); const tool_result_processor_js_1 = require("../utils/tool_result_processor.cjs"); const event_controller_js_1 = require("../utils/event_controller.cjs"); const trace_context_js_1 = require("../utils/trace_context.cjs"); const crypto_1 = require("crypto"); async function* ensembleLive(config, agent, options) { const startTime = Date.now(); const trace = (0, trace_context_js_1.createTraceContext)(agent, 'live_session'); const requestId = (0, crypto_1.randomUUID)(); let session = null; let messageHistory = null; let totalToolCalls = 0; let currentTurnToolCalls = 0; let isSessionActive = true; let totalCost = 0; let totalTokens = 0; let requestStarted = false; let turnStatus = 'completed'; let turnEndReason = 'completed'; let requestStatus = 'completed'; let requestError; let resolvedModel; let resolvedProviderId; await trace.emitTurnStart({ config, options, }); try { const model = await (0, model_provider_js_1.getModelFromAgent)(agent); if (!model) { throw new Error('No model specified in agent configuration'); } resolvedModel = model; const provider = (0, model_provider_js_2.getModelProvider)(model); if (!provider) { throw new Error(`No provider found for model: ${model}`); } resolvedProviderId = provider.provider_id; if (!provider.createLiveSession) { throw new Error(`Provider ${provider.provider_id} does not support Live API`); } await trace.emitRequestStart(requestId, { agent_id: agent.agent_id, provider: provider.provider_id, model, payload: { config, options, history_count: options?.messageHistory?.length ?? 0, }, }); requestStarted = true; if (options?.messageHistory) { messageHistory = new message_history_js_1.MessageHistory(); for (const message of options.messageHistory) { if ('content' in message && message.content) { await messageHistory.add(message); } } } session = await provider.createLiveSession(config, agent, model, options); const startEvent = { type: 'live_start', timestamp: new Date().toISOString(), sessionId: session.sessionId, config, }; yield startEvent; (0, event_controller_js_1.emitEvent)({ type: 'agent_start', agent: { agent_id: agent.agent_id, name: agent.name, model: agent.model, modelClass: agent.modelClass, }, timestamp: new Date().toISOString(), }, agent); if (messageHistory) { const historyMessages = await messageHistory.getMessages(); if (historyMessages.length > 0) { for (const message of historyMessages) { if ('role' in message && message.role && 'content' in message) { const role = message.role === 'assistant' ? 'assistant' : 'user'; const content = typeof message.content === 'string' ? message.content : ''; if (content) { await session.sendText(content, role); } } } } } for await (const event of session.getEventStream()) { if (!isSessionActive) { break; } if (event.type === 'cost_update') { const costEvent = event; if (costEvent.usage.totalCost) { totalCost += costEvent.usage.totalCost; } if (costEvent.usage.totalTokens) { totalTokens += costEvent.usage.totalTokens; } } if (event.type === 'tool_call') { const toolCallEvent = event; const toolResults = []; const maxToolCalls = options?.maxToolCalls ?? agent.maxToolCalls ?? 200; const maxToolCallRoundsPerTurn = options?.maxToolCallRoundsPerTurn ?? agent.maxToolCallRoundsPerTurn ?? Infinity; if (totalToolCalls >= maxToolCalls) { turnStatus = 'error'; turnEndReason = 'max_tool_calls_exceeded'; requestStatus = 'error'; requestError = `Maximum tool calls (${maxToolCalls}) exceeded`; const errorEvent = { type: 'error', timestamp: new Date().toISOString(), error: requestError, code: 'MAX_TOOL_CALLS_EXCEEDED', recoverable: false, }; yield errorEvent; break; } if (currentTurnToolCalls >= maxToolCallRoundsPerTurn) { const errorEvent = { type: 'error', timestamp: new Date().toISOString(), error: `Maximum tool call rounds per turn (${maxToolCallRoundsPerTurn}) exceeded`, code: 'MAX_TURN_TOOL_CALLS_EXCEEDED', recoverable: true, }; yield errorEvent; continue; } for (const toolCall of toolCallEvent.toolCalls) { await trace.emitToolStart(requestId, toolCall.id, { tool_name: toolCall.function.name, call_id: toolCall.call_id || toolCall.id, arguments: toolCall.function.arguments, }); const toolStartEvent = { type: 'tool_start', timestamp: new Date().toISOString(), toolCall, }; yield toolStartEvent; try { const tools = agent.tools || []; const tool = tools.find(t => t.definition.function.name === toolCall.function.name); if (!tool) { throw new Error(`Tool not found: ${toolCall.function.name}`); } const result = await (0, tool_execution_manager_js_1.handleToolCall)(toolCall, tool, agent); const processedResult = await (0, tool_result_processor_js_1.processToolResult)(toolCall, result, agent, tool.allowSummary); const toolCallResult = { toolCall, id: toolCall.id, call_id: toolCall.call_id || toolCall.id, output: processedResult, }; toolResults.push(toolCallResult); totalToolCalls++; currentTurnToolCalls++; await trace.emitToolDone(requestId, toolCall.id, { tool_name: toolCall.function.name, call_id: toolCallResult.call_id, output: toolCallResult.output, }); const toolResultEvent = { type: 'tool_result', timestamp: new Date().toISOString(), toolCallResult, }; yield toolResultEvent; if (agent.onToolResult) { await agent.onToolResult(toolCallResult); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const toolCallResult = { toolCall, id: toolCall.id, call_id: toolCall.call_id || toolCall.id, error: errorMessage, }; toolResults.push(toolCallResult); await trace.emitToolDone(requestId, toolCall.id, { tool_name: toolCall.function.name, call_id: toolCallResult.call_id, error: errorMessage, }); const errorEvent = { type: 'error', timestamp: new Date().toISOString(), error: `Tool call failed: ${errorMessage}`, code: 'TOOL_CALL_ERROR', recoverable: true, }; yield errorEvent; if (agent.onToolError) { await agent.onToolError(toolCallResult); } } } if (toolResults.length > 0 && session.isActive()) { await session.sendToolResponse(toolResults); } const toolDoneEvent = { type: 'tool_done', timestamp: new Date().toISOString(), totalCalls: totalToolCalls, }; yield toolDoneEvent; } if (event.type === 'turn_complete') { currentTurnToolCalls = 0; const turnEvent = event; if (messageHistory && turnEvent.message) { await messageHistory.add(turnEvent.message); } } if (event.type === 'interrupted') { currentTurnToolCalls = 0; } if (event.type === 'live_ready') { (0, event_controller_js_1.emitEvent)({ type: 'agent_status', agent: { agent_id: agent.agent_id, name: agent.name, model: agent.model, modelClass: agent.modelClass, }, status: 'ready', timestamp: new Date().toISOString(), }, agent); } yield event; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); turnStatus = 'error'; turnEndReason = 'exception'; requestStatus = 'error'; requestError = errorMessage; const errorEvent = { type: 'error', timestamp: new Date().toISOString(), error: errorMessage, code: error instanceof Error && 'code' in error ? String(error.code) : 'UNKNOWN_ERROR', recoverable: false, }; yield errorEvent; throw error; } finally { if (session && session.isActive()) { await session.close(); } isSessionActive = false; const duration = Date.now() - startTime; if (requestStarted) { await trace.emitRequestEnd(requestId, { status: requestStatus, error: requestError, duration_ms: duration, total_tokens: totalTokens, total_cost: totalCost > 0 ? totalCost : undefined, total_tool_calls: totalToolCalls, session_id: session?.sessionId, model: resolvedModel, provider: resolvedProviderId, }); } await trace.emitTurnEnd(turnStatus, turnEndReason, { error: requestError, duration_ms: duration, total_tokens: totalTokens, total_cost: totalCost > 0 ? totalCost : undefined, total_tool_calls: totalToolCalls, session_id: session?.sessionId, model: resolvedModel, provider: resolvedProviderId, }); const endEvent = { type: 'live_end', timestamp: new Date().toISOString(), reason: turnStatus === 'completed' ? 'completed' : 'error', duration, totalTokens, totalCost: totalCost > 0 ? totalCost : undefined, }; yield endEvent; (0, event_controller_js_1.emitEvent)({ type: 'agent_done', agent: { agent_id: agent.agent_id, name: agent.name, model: agent.model, modelClass: agent.modelClass, }, duration_with_tools: duration, request_cost: totalCost > 0 ? totalCost : undefined, timestamp: new Date().toISOString(), }, agent); } } async function* ensembleLiveAudio(audioSource, agent, options) { const trace = (0, trace_context_js_1.createTraceContext)(agent, 'live_audio_session'); const requestId = (0, crypto_1.randomUUID)(); let requestStarted = false; let turnStatus = 'completed'; let turnEndReason = 'completed'; let requestStatus = 'completed'; let requestError; let totalCost = 0; let totalTokens = 0; const startTime = Date.now(); const config = { responseModalities: ['AUDIO'], speechConfig: options?.voice ? { voiceConfig: { prebuiltVoiceConfig: { voiceName: options.voice }, }, languageCode: options.language, } : undefined, inputAudioTranscription: {}, outputAudioTranscription: {}, }; if (options?.enableAffectiveDialog) { config.enableAffectiveDialog = true; } if (options?.enableProactivity) { config.proactivity = { proactiveAudio: true, }; } await trace.emitTurnStart({ config, options, audio_source_type: 'async_iterable', }); let session = null; let model; let providerId; let audioChunkCount = 0; let totalAudioBytes = 0; let audioProcessingTask = null; try { model = await (0, model_provider_js_1.getModelFromAgent)(agent); console.log('[ensembleLiveAudio] Using model:', model); if (!model) { throw new Error('No model specified in agent configuration'); } const provider = (0, model_provider_js_2.getModelProvider)(model); providerId = provider?.provider_id; console.log('[ensembleLiveAudio] Provider:', provider?.provider_id); if (!provider || !provider.createLiveSession) { throw new Error(`Provider does not support Live API for model: ${model}`); } await trace.emitRequestStart(requestId, { agent_id: agent.agent_id, provider: provider.provider_id, model, payload: { config, options, audio_source_type: 'async_iterable', }, }); requestStarted = true; console.log('[ensembleLiveAudio] Creating live session...'); session = await provider.createLiveSession(config, agent, model, options); console.log('[ensembleLiveAudio] Session created:', session.sessionId); audioProcessingTask = (async () => { try { console.log('[ensembleLiveAudio] Starting audio processing task...'); for await (const chunk of audioSource) { if (!session || !session.isActive()) { console.log('[ensembleLiveAudio] Session inactive, stopping audio processing'); break; } audioChunkCount++; totalAudioBytes += chunk.length; const base64Data = Buffer.from(chunk).toString('base64'); console.log(`[ensembleLiveAudio] Sending audio chunk ${audioChunkCount}, size: ${chunk.length} bytes, total: ${totalAudioBytes} bytes`); await session.sendAudio({ data: base64Data, mimeType: 'audio/pcm;rate=16000', }); } console.log(`[ensembleLiveAudio] Audio processing completed. Total chunks: ${audioChunkCount}, Total bytes: ${totalAudioBytes}`); } catch (error) { console.error('[ensembleLiveAudio] Error processing audio:', error); } })(); yield { type: 'live_start', timestamp: new Date().toISOString(), sessionId: session.sessionId, config, }; console.log('[ensembleLiveAudio] Starting event processing...'); let eventCount = 0; for await (const event of session.getEventStream()) { eventCount++; console.log(`[ensembleLiveAudio] Event ${eventCount}:`, event.type); if (event.type === 'cost_update') { const costEvent = event; if (costEvent.usage.totalCost) { totalCost += costEvent.usage.totalCost; } if (costEvent.usage.totalTokens) { totalTokens += costEvent.usage.totalTokens; } } yield event; } console.log(`[ensembleLiveAudio] Event processing completed. Total events: ${eventCount}`); } catch (error) { turnStatus = 'error'; turnEndReason = 'exception'; requestStatus = 'error'; requestError = error instanceof Error ? error.message : String(error); throw error; } finally { if (audioProcessingTask) { await audioProcessingTask; } if (session && session.isActive()) { await session.close(); } const duration = Date.now() - startTime; if (requestStarted) { await trace.emitRequestEnd(requestId, { status: requestStatus, error: requestError, duration_ms: duration, total_tokens: totalTokens, total_cost: totalCost > 0 ? totalCost : undefined, audio_chunks_sent: audioChunkCount, audio_bytes_sent: totalAudioBytes, session_id: session?.sessionId, model, provider: providerId, }); } await trace.emitTurnEnd(turnStatus, turnEndReason, { error: requestError, duration_ms: duration, total_tokens: totalTokens, total_cost: totalCost > 0 ? totalCost : undefined, audio_chunks_sent: audioChunkCount, audio_bytes_sent: totalAudioBytes, session_id: session?.sessionId, model, provider: providerId, }); yield { type: 'live_end', timestamp: new Date().toISOString(), reason: turnStatus === 'completed' ? 'completed' : 'error', }; } } async function ensembleLiveText(agent, options) { const config = { responseModalities: ['TEXT'], }; let session = null; const sessionGenerator = ensembleLive(config, agent, options); const eventQueue = []; let eventPromiseResolve = null; const firstEvent = await sessionGenerator.next(); if (!firstEvent.done && firstEvent.value) { eventQueue.push(firstEvent.value); } (async () => { for await (const event of sessionGenerator) { if (event.type === 'live_start') { } if (eventPromiseResolve) { eventPromiseResolve({ value: event, done: false }); eventPromiseResolve = null; } else { eventQueue.push(event); } } if (eventPromiseResolve) { eventPromiseResolve({ value: undefined, done: true }); } })(); return { sendMessage: async (text) => { if (!session) { throw new Error('Session not initialized'); } await session.sendText(text, 'user'); }, getEvents: async function* () { while (true) { if (eventQueue.length > 0) { yield eventQueue.shift(); } else { const result = await new Promise(resolve => { eventPromiseResolve = resolve; }); if (result.done) break; if (result.value) yield result.value; } } }, close: async () => { if (session && session.isActive()) { await session.close(); } }, }; } //# sourceMappingURL=ensemble_live.js.map