@just-every/ensemble
Version:
LLM provider abstraction layer with unified streaming interface
541 lines • 21.8 kB
JavaScript
"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