@iflow-mcp/ejmockler-brutalist
Version:
Deploy Claude, Codex & Gemini CLI agents to demolish your work before users do. Real file analysis. Brutal honesty. Now with conversation continuation & intelligent pagination.
438 lines โข 18.2 kB
JavaScript
import { EventEmitter } from 'events';
import { logger } from '../logger.js';
import { CLIAgentOrchestrator } from '../cli-agents.js';
import { SessionManager } from './session-manager.js';
import { EnhancedSSETransport } from './sse-transport.js';
import { ProgressTracker } from './progress-tracker.js';
import { CircuitBreaker, CachedResponseFallback, DegradedServiceFallback } from './circuit-breaker.js';
/**
* Advanced streaming CLI orchestrator with comprehensive real-time capabilities
*
* Integrates all streaming components:
* - Real-time output parsing with semantic boundaries
* - Intelligent buffering with adaptive throttling
* - Session management with lifecycle tracking
* - SSE transport with session isolation
* - Progress tracking with milestone detection
* - Circuit breaker with fallback strategies
* - Comprehensive monitoring and analytics
*/
export class StreamingCLIOrchestrator extends EventEmitter {
cliOrchestrator;
sessionManager;
sseTransport;
circuitBreakers = new Map();
responseCache = new Map();
activeAnalyses = new Map();
config;
metrics = {
totalAnalyses: 0,
successfulAnalyses: 0,
failedAnalyses: 0,
totalEventsSent: 0,
totalCircuitBreakerTrips: 0,
totalFallbacksUsed: 0,
averageAnalysisDuration: 0
};
cleanupTimer;
constructor(config) {
super();
this.config = {
maxConcurrentAnalyses: 10,
defaultTimeout: 1800000, // 30 minutes
circuitBreakerConfig: {
failureThreshold: 5,
recoveryTimeout: 30000,
successThreshold: 3,
timeout: 1800000, // 30 minutes
monitoringWindow: 300000,
minimumRequests: 10
},
enableMetrics: true,
cleanupInterval: 300000, // 5 minutes
...config
};
this.cliOrchestrator = new CLIAgentOrchestrator();
this.sessionManager = new SessionManager();
this.sseTransport = new EnhancedSSETransport(this.sessionManager);
this.setupEventHandlers();
this.setupCircuitBreakers();
this.startCleanupTimer();
logger.info('๐ Streaming CLI Orchestrator initialized', {
maxConcurrentAnalyses: this.config.maxConcurrentAnalyses,
circuitBreakerEnabled: true,
progressTrackingEnabled: true
});
}
/**
* Execute CLI analysis with full streaming capabilities
*/
async executeWithStreaming(analysisType, cliAgents, systemPrompt, userPrompt, options = {}) {
const sessionId = options.sessionId || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const startTime = Date.now();
// Check concurrent analysis limit
if (this.activeAnalyses.size >= this.config.maxConcurrentAnalyses) {
throw new Error(`Maximum concurrent analyses reached (${this.config.maxConcurrentAnalyses})`);
}
logger.info(`๐ฌ Starting streaming analysis: ${analysisType} for session ${sessionId}`, {
agents: cliAgents,
enableProgress: options.enableProgress,
enableCircuitBreaker: options.enableCircuitBreaker
});
// Create session and progress tracker
const sessionContext = this.sessionManager.createSession(sessionId, {
analysisType,
agents: cliAgents,
startTime
});
const progressTracker = options.enableProgress !== false
? new ProgressTracker(sessionId, analysisType)
: undefined;
// Track active analysis
this.activeAnalyses.set(sessionId, {
sessionId,
progressTracker: progressTracker,
startTime,
options
});
// Setup progress tracking if enabled
if (progressTracker) {
progressTracker.on('progress', (progressEvent) => {
this.sessionManager.emitToSession(sessionId, {
type: 'agent_progress',
agent: 'system',
content: `Progress: ${Math.round(progressEvent.progress.overall * 100)}% - ${progressEvent.phase}`,
timestamp: Date.now(),
sessionId,
metadata: {
progressEvent,
milestone: progressEvent.milestone?.name,
estimatedCompletion: progressEvent.estimatedCompletion
}
});
});
}
const analysisResult = {
success: false,
responses: [],
sessionId,
analysis: {
startTime,
endTime: 0,
duration: 0,
eventsEmitted: 0,
progressMilestones: 0,
circuitBreakerTrips: 0,
fallbacksUsed: 0
},
streaming: {
connectedClients: 0,
eventsSent: 0,
averageLatency: 0,
bufferHits: 0
}
};
try {
// Enhanced CLI options with streaming callbacks
const enhancedOptions = {
...options,
sessionId,
onStreamingEvent: this.createStreamingEventHandler(sessionId, progressTracker, analysisResult),
timeout: options.streamingTimeout || this.config.defaultTimeout
};
// Execute CLI agents with circuit breaker protection if enabled
if (options.enableCircuitBreaker !== false) {
analysisResult.responses = await this.executeWithCircuitBreaker(cliAgents, systemPrompt, userPrompt, enhancedOptions);
}
else {
// Direct execution without circuit breaker
analysisResult.responses = await this.cliOrchestrator.executeCLIAgents(cliAgents, systemPrompt, userPrompt, enhancedOptions);
}
analysisResult.success = analysisResult.responses.some(r => r.success);
// Mark progress as complete
if (progressTracker) {
progressTracker.markComplete();
}
this.metrics.successfulAnalyses++;
}
catch (error) {
logger.error(`๐ฅ Streaming analysis failed for session ${sessionId}:`, error);
analysisResult.success = false;
analysisResult.errors = [error instanceof Error ? error.message : String(error)];
// Handle fallback if configured
if (options.fallbackResponse) {
analysisResult.responses = [{
agent: 'claude', // Use a valid agent type for fallback
success: true,
output: typeof options.fallbackResponse === 'string'
? options.fallbackResponse
: JSON.stringify(options.fallbackResponse),
executionTime: Date.now() - startTime,
command: 'fallback',
workingDirectory: options.workingDirectory || process.cwd(),
exitCode: 0
}];
analysisResult.analysis.fallbacksUsed++;
this.metrics.totalFallbacksUsed++;
}
this.metrics.failedAnalyses++;
}
finally {
const endTime = Date.now();
analysisResult.analysis.endTime = endTime;
analysisResult.analysis.duration = endTime - startTime;
// Get streaming stats
const sessionConnections = this.sseTransport.getSessionConnections(sessionId);
analysisResult.streaming.connectedClients = sessionConnections.length;
analysisResult.streaming.eventsSent = sessionConnections.reduce((sum, conn) => sum + conn.eventsSent, 0);
// Get session metrics
const sessionStats = this.sessionManager.getSessionStats(sessionId);
if (sessionStats) {
analysisResult.analysis.eventsEmitted = sessionStats.eventsEmitted;
analysisResult.streaming.bufferHits = sessionStats.bufferStats.flushCount;
}
// Update global metrics
this.metrics.totalAnalyses++;
this.updateAverageAnalysisDuration(analysisResult.analysis.duration);
// Cleanup
this.activeAnalyses.delete(sessionId);
this.sessionManager.completeSession(sessionId);
// Keep SSE connections open briefly for final events
setTimeout(() => {
this.sseTransport.disconnectSession(sessionId, 'analysis_complete');
}, 5000);
logger.info(`๐ Streaming analysis completed: ${sessionId} (${analysisResult.analysis.duration}ms)`, {
success: analysisResult.success,
eventsEmitted: analysisResult.analysis.eventsEmitted,
connectedClients: analysisResult.streaming.connectedClients
});
}
return analysisResult;
}
/**
* Create streaming event handler for session
*/
createStreamingEventHandler(sessionId, progressTracker, result) {
return (event) => {
// Process event through progress tracker
if (progressTracker) {
progressTracker.processEvent(event);
}
// Emit to session manager (which handles SSE transport)
this.sessionManager.emitToSession(sessionId, event);
// Update metrics
result.analysis.eventsEmitted++;
this.metrics.totalEventsSent++;
// Emit orchestrator-level event
this.emit('streamingEvent', { sessionId, event });
};
}
/**
* Execute CLI agents with circuit breaker protection
*/
async executeWithCircuitBreaker(cliAgents, systemPrompt, userPrompt, options) {
const responses = [];
for (const agent of cliAgents) {
const circuitBreaker = this.circuitBreakers.get(agent);
if (!circuitBreaker) {
throw new Error(`No circuit breaker configured for agent: ${agent}`);
}
try {
const response = await circuitBreaker.execute(async () => {
return await this.cliOrchestrator.executeCLIAgent(agent, systemPrompt, userPrompt, options);
}, { id: `${agent}_${options.sessionId}` });
responses.push(response);
}
catch (error) {
logger.warn(`Circuit breaker blocked execution for ${agent}:`, error);
// Circuit breaker handled the error, continue with other agents
responses.push({
agent: agent,
success: false,
output: '',
error: `Circuit breaker: ${error instanceof Error ? error.message : String(error)}`,
executionTime: 0,
command: 'circuit_breaker_blocked',
workingDirectory: options.workingDirectory || process.cwd(),
exitCode: -1
});
this.metrics.totalCircuitBreakerTrips++;
}
}
return responses;
}
/**
* Setup event handlers for internal components
*/
setupEventHandlers() {
// Session manager events
this.sessionManager.on('sessionCreated', (sessionId) => {
logger.debug(`๐ Session created: ${sessionId}`);
this.emit('sessionCreated', sessionId);
});
this.sessionManager.on('sessionCompleted', (sessionId) => {
logger.debug(`โ
Session completed: ${sessionId}`);
this.emit('sessionCompleted', sessionId);
});
// SSE transport events
this.sseTransport.on('connectionClosed', (data) => {
logger.debug(`๐ SSE connection closed: ${data.connectionId} (${data.reason})`);
this.emit('connectionClosed', data);
});
// Circuit breaker events
this.on('circuitBreakerStateChanged', (data) => {
logger.info(`๐ Circuit breaker state changed: ${data.agent} -> ${data.state}`);
});
}
/**
* Setup circuit breakers for each CLI agent
*/
setupCircuitBreakers() {
const agents = ['claude', 'codex', 'gemini'];
for (const agent of agents) {
const circuitBreaker = new CircuitBreaker(this.config.circuitBreakerConfig, `${agent}_breaker`);
// Add fallback strategies
if (this.responseCache.size > 0) {
circuitBreaker.addFallbackStrategy(new CachedResponseFallback(this.responseCache));
}
circuitBreaker.addFallbackStrategy(new DegradedServiceFallback({
agent,
success: false,
output: `${agent.toUpperCase()} is temporarily unavailable. This is a degraded response.`,
error: 'Circuit breaker fallback',
executionTime: 0,
command: 'fallback',
workingDirectory: process.cwd(),
exitCode: -1
}));
// Setup event forwarding
circuitBreaker.on('stateChanged', (data) => {
this.emit('circuitBreakerStateChanged', { agent, ...data });
});
circuitBreaker.on('fallbackSuccess', (data) => {
this.metrics.totalFallbacksUsed++;
this.emit('fallbackUsed', { agent, ...data });
});
this.circuitBreakers.set(agent, circuitBreaker);
logger.debug(`๐ Circuit breaker configured for ${agent}`);
}
}
/**
* Update average analysis duration metric
*/
updateAverageAnalysisDuration(duration) {
const totalAnalyses = this.metrics.totalAnalyses;
const currentAverage = this.metrics.averageAnalysisDuration;
// Calculate new average using online algorithm
this.metrics.averageAnalysisDuration =
(currentAverage * (totalAnalyses - 1) + duration) / totalAnalyses;
}
/**
* Start cleanup timer for stale sessions and cache
*/
startCleanupTimer() {
this.cleanupTimer = setInterval(() => {
this.performCleanup();
}, this.config.cleanupInterval);
// Allow Node.js to exit if this is the only active timer
this.cleanupTimer.unref();
}
/**
* Perform periodic cleanup
*/
performCleanup() {
const now = Date.now();
const staleThreshold = 30 * 60 * 1000; // 30 minutes
// Clean up stale analyses
for (const [sessionId, analysis] of this.activeAnalyses) {
if (now - analysis.startTime > staleThreshold) {
logger.warn(`๐งน Cleaning up stale analysis: ${sessionId}`);
this.activeAnalyses.delete(sessionId);
this.sessionManager.completeSession(sessionId);
this.sseTransport.disconnectSession(sessionId, 'cleanup_stale');
}
}
// Clean up response cache (keep last 100 entries)
if (this.responseCache.size > 100) {
const entries = Array.from(this.responseCache.entries());
entries.splice(0, entries.length - 100);
this.responseCache.clear();
for (const [key, value] of entries) {
this.responseCache.set(key, value);
}
}
logger.debug(`๐งน Cleanup completed: ${this.activeAnalyses.size} active analyses, ${this.responseCache.size} cached responses`);
}
/**
* Get orchestrator statistics
*/
getStats() {
const circuitBreakerStats = {};
for (const [agent, breaker] of this.circuitBreakers) {
circuitBreakerStats[agent] = breaker.getStats();
}
return {
metrics: { ...this.metrics },
activeAnalyses: this.activeAnalyses.size,
sessionManager: this.sessionManager.getGlobalStats(),
sseTransport: this.sseTransport.getStats(),
circuitBreakers: circuitBreakerStats
};
}
/**
* Get active session IDs
*/
getActiveSessions() {
return Array.from(this.activeAnalyses.keys());
}
/**
* Force disconnect session
*/
disconnectSession(sessionId, reason = 'forced_disconnect') {
logger.info(`๐ Force disconnecting session: ${sessionId}`);
this.activeAnalyses.delete(sessionId);
this.sessionManager.completeSession(sessionId);
this.sseTransport.disconnectSession(sessionId, reason);
}
/**
* Reset circuit breaker for specific agent
*/
resetCircuitBreaker(agent) {
const circuitBreaker = this.circuitBreakers.get(agent);
if (circuitBreaker) {
circuitBreaker.reset();
logger.info(`๐ Circuit breaker reset for ${agent}`);
}
}
/**
* Add response to cache for fallback strategies
*/
cacheResponse(key, response) {
this.responseCache.set(key, response);
logger.debug(`๐พ Cached response: ${key}`);
}
/**
* Shutdown orchestrator and cleanup resources
*/
shutdown() {
logger.info('๐ Shutting down Streaming CLI Orchestrator');
// Stop cleanup timer
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
// Disconnect all sessions
for (const sessionId of this.activeAnalyses.keys()) {
this.disconnectSession(sessionId, 'orchestrator_shutdown');
}
// Shutdown components
this.sseTransport.shutdown();
this.sessionManager.shutdown();
// Shutdown circuit breakers
for (const [agent, breaker] of this.circuitBreakers) {
breaker.shutdown();
}
this.removeAllListeners();
logger.info('โ
Streaming CLI Orchestrator shutdown complete');
}
}
//# sourceMappingURL=streaming-orchestrator.js.map