UNPKG

veas

Version:

Veas CLI - Command-line interface for Veas platform

442 lines 20.8 kB
import { createClient } from '@supabase/supabase-js'; import { logger } from '../utils/logger.js'; export class RealtimeService { supabase; destinationId = null; onTaskAssigned; verbose = false; constructor(config, onTaskAssigned) { this.onTaskAssigned = onTaskAssigned; this.verbose = config.verbose || false; if (!config.supabaseUrl || !config.supabaseAnonKey) { throw new Error('Supabase URL and anon key are required for realtime service'); } if (this.verbose) { logger.info(`[VERBOSE] Initializing Supabase client:`); logger.info(`[VERBOSE] URL: ${config.supabaseUrl}`); logger.info(`[VERBOSE] Organization: ${config.organizationId}`); } this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, { auth: { persistSession: false, autoRefreshToken: false, }, realtime: { params: { eventsPerSecond: 10, }, }, }); } setDestinationId(destinationId) { this.destinationId = destinationId; logger.info(`Destination ID set: ${destinationId}`); if (this.verbose) { logger.info(`[VERBOSE] Agent ready to poll for tasks with destination: ${destinationId}`); } } async start() { if (!this.destinationId) { throw new Error('Destination ID must be set before starting realtime service'); } logger.info('Starting task detection service...'); try { const subscriptionSuccess = await this.setupRealtimeSubscriptions(); if (!subscriptionSuccess) { logger.warn('Realtime subscriptions failed, falling back to polling-only mode'); } logger.info('Starting polling for task detection (backup mechanism)'); this.startPolling(); } catch (error) { logger.error('Failed to start task detection service:', error); throw error; } } async stop() { logger.info('Stopping task detection service...'); this.stopPolling(); if (this.channels) { for (const channel of this.channels) { await this.supabase.removeChannel(channel); } this.channels = []; } } channels = []; async setupRealtimeSubscriptions() { try { logger.info('Setting up realtime subscriptions...'); const assignedChannel = this.supabase .channel(`agent-executions-${this.destinationId}`) .on('postgres_changes', { event: '*', schema: 'agents', table: 'executions', filter: `destination_id=eq.${this.destinationId}`, }, async (payload) => { if (this.verbose) { logger.info(`[VERBOSE] Realtime event (assigned): ${payload.eventType}`); logger.info(`[VERBOSE] Payload:`, JSON.stringify(payload, null, 2)); } if (payload.eventType === 'INSERT' || payload.eventType === 'UPDATE') { const execution = payload.new; if (execution && (execution.status === 'pending' || execution.status === 'queued')) { logger.info(`🎯 Realtime: New assigned execution detected: ${execution.id}`); const taskExecution = { id: execution.id, taskId: execution.task_id, trigger: execution.trigger || 'manual', status: execution.status, queuedAt: execution.queued_at, startedAt: execution.started_at, completedAt: execution.completed_at, inputParams: execution.input_params || {}, outputResult: execution.output_result, errorMessage: execution.error_message, executionLogs: execution.execution_logs || [], toolCalls: execution.tool_calls || [], retryCount: execution.retry_count || 0, context: execution.context || {}, }; this.onTaskAssigned(taskExecution); } } }) .subscribe((status) => { if (status === 'SUBSCRIBED') { logger.info('✅ Subscribed to assigned executions (realtime)'); } else { logger.warn(`⚠️ Subscription status for assigned executions: ${status}`); } }); this.channels.push(assignedChannel); const unassignedChannel = this.supabase .channel(`agent-unassigned-${this.destinationId}`) .on('postgres_changes', { event: 'INSERT', schema: 'agents', table: 'executions', }, async (payload) => { const execution = payload.new; if (this.verbose) { logger.info(`[VERBOSE] Realtime event (unassigned): INSERT`); logger.info(`[VERBOSE] Execution destination: ${execution?.destination_id || 'none'}`); } if (execution && !execution.destination_id && (execution.status === 'pending' || execution.status === 'queued')) { logger.info(`🔍 Realtime: New unassigned execution detected: ${execution.id}`); await this.handlePendingTask(execution); } }) .subscribe((status) => { if (status === 'SUBSCRIBED') { logger.info('✅ Subscribed to unassigned executions (realtime)'); } else { logger.warn(`⚠️ Subscription status for unassigned executions: ${status}`); } }); this.channels.push(unassignedChannel); await new Promise(resolve => setTimeout(resolve, 1000)); logger.info('✅ Realtime subscriptions set up successfully'); return true; } catch (error) { logger.error('Failed to set up realtime subscriptions:', error); return false; } } async handlePendingTask(execution) { const exec = execution; if (!exec || exec.destination_id) return; logger.debug(`Found pending task without destination: ${exec.id}`); try { const { data, error } = await this.supabase .from('executions') .update({ destination_id: this.destinationId, assigned_at: new Date().toISOString(), claimed_at: new Date().toISOString(), }) .eq('id', exec.id) .is('destination_id', null) .eq('status', 'pending') .select() .single(); if (!error && data) { logger.info(`Successfully claimed task: ${execution.id}`); this.onTaskAssigned(data); } } catch (error) { logger.debug(`Failed to claim task ${execution.id}:`, error); } } pollingInterval = null; startPolling() { logger.info(`Starting polling for destination: ${this.destinationId}`); this.pollingInterval = setInterval(async () => { await this.pollForTasks(); }, 2000); this.pollForTasks(); } stopPolling() { if (this.pollingInterval) { clearInterval(this.pollingInterval); this.pollingInterval = null; } } async pollForTasks() { try { if (this.verbose) { logger.info(`[VERBOSE] Polling for tasks - Destination ID: ${this.destinationId}`); } const query = this.supabase .schema('agents') .from('executions') .select('*') .or(`destination_id.eq.${this.destinationId},destination_id.is.null`) .in('status', ['pending', 'queued']) .limit(10); if (this.verbose) { logger.info(`[VERBOSE] Query: agents.executions WHERE (destination_id='${this.destinationId}' OR destination_id IS NULL) AND status IN ('pending', 'queued')`); } const { data: agentExecutions, error: agentError } = await query; if (agentError) { logger.error('Error querying agents.executions:', agentError.message); if (this.verbose) { logger.error('[VERBOSE] Full error:', agentError); } } else { if (this.verbose) { logger.info(`[VERBOSE] Query returned ${agentExecutions?.length || 0} executions`); if (agentExecutions && agentExecutions.length > 0) { logger.info('[VERBOSE] Executions found:', JSON.stringify(agentExecutions, null, 2)); } } if (agentExecutions?.length > 0) { logger.info(`Found ${agentExecutions.length} executions in agents schema`); for (const execution of agentExecutions) { if (this.verbose) { logger.info(`[VERBOSE] Processing execution:`); logger.info(`[VERBOSE] ID: ${execution.id}`); logger.info(`[VERBOSE] Task ID: ${execution.task_id}`); logger.info(`[VERBOSE] Status: ${execution.status}`); logger.info(`[VERBOSE] Destination: ${execution.destination_id}`); logger.info(`[VERBOSE] Claimed at: ${execution.claimed_at}`); } if (!execution.destination_id || execution.destination_id === this.destinationId) { if (!execution.claimed_at) { const { data: claimedExecution, error: claimError } = await this.supabase .schema('agents') .from('executions') .update({ destination_id: this.destinationId, claimed_at: new Date().toISOString(), }) .eq('id', execution.id) .is('claimed_at', null) .select() .single(); if (!claimError && claimedExecution) { logger.info(`Successfully claimed execution: ${execution.id}`); if (this.verbose) { logger.info('[VERBOSE] Claimed execution details:', JSON.stringify(claimedExecution, null, 2)); } const taskExecution = { id: claimedExecution.id, taskId: claimedExecution.task_id, trigger: claimedExecution.trigger || 'manual', status: claimedExecution.status, queuedAt: claimedExecution.queued_at, startedAt: claimedExecution.started_at, completedAt: claimedExecution.completed_at, inputParams: claimedExecution.input_params || {}, outputResult: claimedExecution.output_result, errorMessage: claimedExecution.error_message, executionLogs: claimedExecution.execution_logs || [], toolCalls: claimedExecution.tool_calls || [], retryCount: claimedExecution.retry_count || 0, context: claimedExecution.context || {}, }; if (this.verbose) { logger.info('[VERBOSE] Calling onTaskAssigned with TaskExecution:', JSON.stringify(taskExecution, null, 2)); } this.onTaskAssigned(taskExecution); } else if (claimError) { logger.debug(`Could not claim execution ${execution.id}: ${claimError.message}`); } } else if (execution.destination_id === this.destinationId) { logger.info(`Execution ${execution.id} already assigned to us, processing...`); const taskExecution = { id: execution.id, taskId: execution.task_id, trigger: execution.trigger || 'manual', status: execution.status, queuedAt: execution.queued_at, startedAt: execution.started_at, completedAt: execution.completed_at, inputParams: execution.input_params || {}, outputResult: execution.output_result, errorMessage: execution.error_message, executionLogs: execution.execution_logs || [], toolCalls: execution.tool_calls || [], retryCount: execution.retry_count || 0, context: execution.context || {}, }; this.onTaskAssigned(taskExecution); } } } } } if (this.verbose) { logger.info(`[VERBOSE] Checking fallback: default schema executions table`); } const { data: assignedTasks, error: assignedError } = await this.supabase .from('executions') .select('*') .eq('destination_id', this.destinationId) .eq('status', 'pending') .is('claimed_at', null) .limit(10); if (assignedError) { if (this.verbose) { logger.info(`[VERBOSE] Fallback query error: ${assignedError.message}`); } } else if (this.verbose) { logger.info(`[VERBOSE] Fallback query returned ${assignedTasks?.length || 0} tasks`); } if (!assignedError && assignedTasks?.length > 0) { for (const task of assignedTasks) { const { data: claimedTask, error: claimError } = await this.supabase .from('executions') .update({ claimed_at: new Date().toISOString() }) .eq('id', task.id) .is('claimed_at', null) .select() .single(); if (!claimError && claimedTask) { logger.info(`Claimed task via polling: ${task.id}`); this.onTaskAssigned(claimedTask); } } } const { data: unassignedTasks, error: unassignedError } = await this.supabase .from('executions') .select('*') .is('destination_id', null) .eq('status', 'pending') .limit(5); if (!unassignedError && unassignedTasks?.length > 0) { for (const task of unassignedTasks) { await this.handlePendingTask(task); } } } catch (error) { logger.debug('Error polling for tasks:', error); } } async updateExecutionStatus(executionId, status, updates = {}) { try { const { error: agentsError } = await this.supabase .schema('agents') .from('executions') .update({ status, ...updates, ...(status === 'running' && !updates.started_at ? { started_at: new Date().toISOString() } : {}), ...(status === 'completed' || status === 'failed' ? { completed_at: new Date().toISOString() } : {}), }) .eq('id', executionId); if (agentsError) { const { error } = await this.supabase .from('executions') .update({ status, ...updates, ...(status === 'running' && !updates.started_at ? { started_at: new Date().toISOString() } : {}), ...(status === 'completed' || status === 'failed' ? { completed_at: new Date().toISOString() } : {}), }) .eq('id', executionId); if (error) { logger.error(`Failed to update execution status: ${error.message}`); throw error; } } } catch (error) { logger.error('Error updating execution status:', error); throw error; } } async addExecutionLog(executionId, level, message, data) { try { let execution = null; let useAgentsSchema = true; const { data: agentsExecution, error: agentsFetchError } = await this.supabase .schema('agents') .from('executions') .select('execution_logs') .eq('id', executionId) .single(); if (!agentsFetchError && agentsExecution) { execution = agentsExecution; } else { const { data: defaultExecution, error: defaultFetchError } = await this.supabase .from('executions') .select('execution_logs') .eq('id', executionId) .single(); if (!defaultFetchError && defaultExecution) { execution = defaultExecution; useAgentsSchema = false; } else { logger.error(`Failed to fetch execution: ${agentsFetchError?.message || defaultFetchError?.message}`); return; } } const logs = execution?.execution_logs || []; logs.push({ timestamp: new Date().toISOString(), level, message, data, }); if (useAgentsSchema) { const { error: updateError } = await this.supabase .schema('agents') .from('executions') .update({ execution_logs: logs }) .eq('id', executionId); if (updateError) { logger.error(`Failed to add execution log: ${updateError.message}`); } } else { const { error: updateError } = await this.supabase .from('executions') .update({ execution_logs: logs }) .eq('id', executionId); if (updateError) { logger.error(`Failed to add execution log: ${updateError.message}`); } } } catch (error) { logger.error('Error adding execution log:', error); } } } //# sourceMappingURL=realtime-service.js.map