UNPKG

claude-flow-tbowman01

Version:

Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)

367 lines (301 loc) 10.4 kB
/** * Fallback Coordinator for MCP * Manages graceful degradation to CLI when MCP connection fails */ import { EventEmitter } from 'node:events'; import type { ILogger } from '../../core/logger.js'; import type { MCPRequest } from '../../utils/types.js'; import { exec } from 'node:child_process'; import { promisify } from 'node:util'; const execAsync = promisify(exec); export interface FallbackOperation { id: string; type: 'tool' | 'resource' | 'notification'; method: string; params: unknown; timestamp: Date; priority: 'high' | 'medium' | 'low'; retryable: boolean; } export interface FallbackConfig { enableFallback: boolean; maxQueueSize: number; queueTimeout: number; cliPath: string; fallbackNotificationInterval: number; } export interface FallbackState { isFallbackActive: boolean; queuedOperations: number; failedOperations: number; successfulOperations: number; lastFallbackActivation?: Date; } export class FallbackCoordinator extends EventEmitter { private operationQueue: FallbackOperation[] = []; private state: FallbackState; private notificationTimer?: NodeJS.Timeout; private processingQueue = false; private readonly defaultConfig: FallbackConfig = { enableFallback: true, maxQueueSize: 100, queueTimeout: 300000, // 5 minutes cliPath: 'npx ruv-swarm', fallbackNotificationInterval: 30000, // 30 seconds }; constructor( private logger: ILogger, config?: Partial<FallbackConfig>, ) { super(); this.config = { ...this.defaultConfig, ...config }; this.state = { isFallbackActive: false, queuedOperations: 0, failedOperations: 0, successfulOperations: 0, }; } private config: FallbackConfig; /** * Check if MCP is available */ async isMCPAvailable(): Promise<boolean> { try { // Try to execute a simple MCP command const { stdout } = await execAsync(`${this.config.cliPath} status --json`); const status = JSON.parse(stdout); return status.connected === true; } catch (error) { this.logger.debug('MCP availability check failed', error); return false; } } /** * Enable CLI fallback mode */ enableCLIFallback(): void { if (this.state.isFallbackActive) { this.logger.debug('Fallback already active'); return; } this.logger.warn('Enabling CLI fallback mode'); this.state.isFallbackActive = true; this.state.lastFallbackActivation = new Date(); // Start notification timer this.startNotificationTimer(); this.emit('fallbackEnabled', this.state); } /** * Disable CLI fallback mode */ disableCLIFallback(): void { if (!this.state.isFallbackActive) { return; } this.logger.info('Disabling CLI fallback mode'); this.state.isFallbackActive = false; // Stop notification timer this.stopNotificationTimer(); this.emit('fallbackDisabled', this.state); // Process any queued operations if (this.operationQueue.length > 0) { this.processQueue().catch((error) => { this.logger.error('Error processing queue after fallback disabled', error); }); } } /** * Queue an operation for later execution */ queueOperation(operation: Omit<FallbackOperation, 'id' | 'timestamp'>): void { if (!this.config.enableFallback) { this.logger.debug('Fallback disabled, operation not queued'); return; } if (this.operationQueue.length >= this.config.maxQueueSize) { this.logger.warn('Operation queue full, removing oldest operation'); this.operationQueue.shift(); this.state.failedOperations++; } const queuedOp: FallbackOperation = { ...operation, id: this.generateOperationId(), timestamp: new Date(), }; this.operationQueue.push(queuedOp); this.state.queuedOperations = this.operationQueue.length; this.logger.debug('Operation queued', { id: queuedOp.id, type: queuedOp.type, method: queuedOp.method, queueSize: this.operationQueue.length, }); this.emit('operationQueued', queuedOp); // If in fallback mode, try to execute via CLI if (this.state.isFallbackActive && !this.processingQueue) { this.executeViaCliFallback(queuedOp).catch((error) => { this.logger.error('CLI fallback execution failed', { operation: queuedOp, error }); }); } } /** * Process all queued operations */ async processQueue(): Promise<void> { if (this.processingQueue || this.operationQueue.length === 0) { return; } this.processingQueue = true; this.logger.info('Processing operation queue', { queueSize: this.operationQueue.length, }); this.emit('queueProcessingStart', this.operationQueue.length); const results = { successful: 0, failed: 0, }; // Process operations in order while (this.operationQueue.length > 0) { const operation = this.operationQueue.shift()!; // Check if operation has expired if (this.isOperationExpired(operation)) { this.logger.warn('Operation expired', { id: operation.id }); results.failed++; continue; } try { await this.replayOperation(operation); results.successful++; this.state.successfulOperations++; } catch (error) { this.logger.error('Failed to replay operation', { operation, error, }); results.failed++; this.state.failedOperations++; // Re-queue if retryable if (operation.retryable) { this.operationQueue.push(operation); } } } this.state.queuedOperations = this.operationQueue.length; this.processingQueue = false; this.logger.info('Queue processing complete', results); this.emit('queueProcessingComplete', results); } /** * Get current fallback state */ getState(): FallbackState { return { ...this.state }; } /** * Get queued operations */ getQueuedOperations(): FallbackOperation[] { return [...this.operationQueue]; } /** * Clear operation queue */ clearQueue(): void { const clearedCount = this.operationQueue.length; this.operationQueue = []; this.state.queuedOperations = 0; this.logger.info('Operation queue cleared', { clearedCount }); this.emit('queueCleared', clearedCount); } private async executeViaCliFallback(operation: FallbackOperation): Promise<void> { this.logger.debug('Executing operation via CLI fallback', { id: operation.id, method: operation.method, }); try { // Map MCP operations to CLI commands const cliCommand = this.mapOperationToCli(operation); if (!cliCommand) { throw new Error(`No CLI mapping for operation: ${operation.method}`); } const { stdout, stderr } = await execAsync(cliCommand); if (stderr) { this.logger.warn('CLI command stderr', { stderr }); } this.logger.debug('CLI fallback execution successful', { id: operation.id, stdout: stdout.substring(0, 200), // Log first 200 chars }); this.state.successfulOperations++; this.emit('fallbackExecutionSuccess', { operation, result: stdout }); } catch (error) { this.logger.error('CLI fallback execution failed', { operation, error, }); this.state.failedOperations++; this.emit('fallbackExecutionFailed', { operation, error }); // Re-queue if retryable if (operation.retryable) { this.queueOperation(operation); } } } private async replayOperation(operation: FallbackOperation): Promise<void> { // This would typically use the MCP client to replay the operation // For now, we'll log it this.logger.info('Replaying operation', { id: operation.id, method: operation.method, }); // Emit event for handling by the MCP client this.emit('replayOperation', operation); } private mapOperationToCli(operation: FallbackOperation): string | null { // Map common MCP operations to CLI commands const mappings: Record<string, (params: any) => string> = { // Tool operations 'tools/list': () => `${this.config.cliPath} tools list`, 'tools/call': (params) => `${this.config.cliPath} tools call ${params.name} '${JSON.stringify(params.arguments)}'`, // Resource operations 'resources/list': () => `${this.config.cliPath} resources list`, 'resources/read': (params) => `${this.config.cliPath} resources read ${params.uri}`, // Session operations initialize: () => `${this.config.cliPath} session init`, shutdown: () => `${this.config.cliPath} session shutdown`, // Custom operations heartbeat: () => `${this.config.cliPath} health check`, }; const mapper = mappings[operation.method]; return mapper ? mapper(operation.params) : null; } private isOperationExpired(operation: FallbackOperation): boolean { const age = Date.now() - operation.timestamp.getTime(); return age > this.config.queueTimeout; } private generateOperationId(): string { return `op-${Date.now()}-${Math.random().toString(36).slice(2)}`; } private startNotificationTimer(): void { if (this.notificationTimer) { return; } this.notificationTimer = setInterval(() => { if (this.state.isFallbackActive && this.operationQueue.length > 0) { this.logger.info('Fallback mode active', { queuedOperations: this.operationQueue.length, duration: Date.now() - (this.state.lastFallbackActivation?.getTime() || 0), }); this.emit('fallbackStatus', this.state); } }, this.config.fallbackNotificationInterval); } private stopNotificationTimer(): void { if (this.notificationTimer) { clearInterval(this.notificationTimer); this.notificationTimer = undefined; } } }