UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

512 lines (511 loc) 19.6 kB
/** * Agent State Management Infrastructure * * Manages agent state persistence, state change events, heartbeat monitoring, * and state aggregation for dashboard consumption * Phase 2: Interactive Observation System Component */ import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; // ===== AGENT STATE MANAGER CLASS ===== export class AgentStateManager extends EventEmitter { redis; logger; heartbeatInterval = null; cleanupInterval = null; stateCache = new Map(); heartbeatTimeouts = new Map(); // Configuration HEARTBEAT_INTERVAL = 5000; HEARTBEAT_TIMEOUT = 15000; STATE_TTL = 3600; CACHE_TTL = 30000; CLEANUP_INTERVAL = 60000; constructor(redisClient, logger){ super(); this.redis = redisClient; this.logger = logger; this.initializeStateMonitoring(); } async initializeStateMonitoring() { // Start heartbeat monitoring this.heartbeatInterval = setInterval(()=>{ this.checkHeartbeats(); }, this.HEARTBEAT_INTERVAL); // Start cleanup process this.cleanupInterval = setInterval(()=>{ this.performCleanup(); }, this.CLEANUP_INTERVAL); // Subscribe to state change events await this.subscribeToStateEvents(); this.logger.info('Agent state manager initialized'); } async subscribeToStateEvents() { const subscriber = this.redis.duplicate(); await subscriber.connect(); await subscriber.pSubscribe('agent-state:*', (message, channel)=>{ try { const event = JSON.parse(message); this.handleStateEvent(channel, event); } catch (error) { this.logger.error('Failed to parse state event', { channel, error }); } }); } handleStateEvent(channel, event) { const agentId = this.extractAgentIdFromChannel(channel); if (!agentId) return; switch(event.type){ case 'state_update': this.updateAgentState(agentId, event.state); break; case 'heartbeat': this.processHeartbeat(agentId, event.heartbeat); break; case 'task_update': this.updateAgentTask(agentId, event.task); break; default: this.logger.warn('Unknown state event type', { type: event.type, agentId }); } } extractAgentIdFromChannel(channel) { const match = channel.match(/^agent-state:([^:]+):/); return match ? match[1] : null; } // ===== STATE MANAGEMENT ===== async updateAgentState(agentId, newState, reason) { const currentState = await this.getAgentState(agentId); const previousState = { ...currentState }; // Merge with existing state const updatedState = { id: agentId, type: newState.type || currentState?.type || 'unknown', status: newState.status || currentState?.status || 'idle', capabilities: newState.capabilities || currentState?.capabilities || [], metrics: newState.metrics || currentState?.metrics || {}, connections: newState.connections || currentState?.connections || [], lastHeartbeat: Date.now(), ...currentState, ...newState }; // Store in Redis const stateKey = `agent-state:${agentId}`; await this.redis.setEx(stateKey, this.STATE_TTL, JSON.stringify(updatedState)); // Update cache this.stateCache.set(agentId, updatedState); setTimeout(()=>{ this.stateCache.delete(agentId); }, this.CACHE_TTL); // Create state change event const changeEvent = { agentId, previousState, newState: updatedState, changeType: this.determineChangeType(previousState, updatedState), timestamp: Date.now(), reason }; // Emit state change event await this.publishStateChange(changeEvent); // Update heartbeat timeout this.updateHeartbeatTimeout(agentId); this.emit('stateChange', changeEvent); this.logger.debug('Agent state updated', { agentId, changeType: changeEvent.changeType }); } determineChangeType(previousState, newState) { if (previousState.status !== newState.status) return 'status'; const previousTask = previousState.currentTask ? JSON.stringify(previousState.currentTask) : null; const newTask = newState.currentTask ? JSON.stringify(newState.currentTask) : null; if (previousTask !== newTask) return 'task'; if ((previousState.progress ?? 0) !== (newState.progress ?? 0)) return 'progress'; if ((previousState.confidence ?? 0) !== (newState.confidence ?? 0)) return 'confidence'; const previousResources = previousState.resources ? JSON.stringify(previousState.resources) : null; const newResources = newState.resources ? JSON.stringify(newState.resources) : null; if (previousResources !== newResources) return 'resource'; return 'heartbeat'; } async updateAgentTask(agentId, task) { const currentState = await this.getAgentState(agentId); if (!currentState) return; const updatedTask = { id: task.id || uuidv4(), type: task.type || 'unknown', description: task.description || '', startedAt: task.startedAt || Date.now(), estimatedCompletion: task.estimatedCompletion, progress: task.progress || 0, confidence: task.confidence || 0 }; await this.updateAgentState(agentId, { currentTask: updatedTask, progress: task.progress || 0, confidence: task.confidence || 0 }, 'Task update'); } async setAgentStatus(agentId, status, reason) { await this.updateAgentState(agentId, { status }, reason); } async updateAgentResources(agentId, resources) { const currentState = await this.getAgentState(agentId); if (!currentState) return; const updatedResources = { ...currentState.resources, ...resources }; await this.updateAgentState(agentId, { resources: updatedResources }, 'Resource update'); } // ===== HEARTBEAT MANAGEMENT ===== async processHeartbeat(agentId, heartbeat) { const currentState = await this.getAgentState(agentId); if (!currentState) { // Create new agent state if not exists await this.createAgentState(agentId, heartbeat.status, heartbeat.resources); } else { await this.updateAgentState(agentId, { lastHeartbeat: heartbeat.timestamp, status: heartbeat.status, resources: heartbeat.resources, metadata: { ...currentState.metadata, ...heartbeat.metrics } }, 'Heartbeat'); } this.updateHeartbeatTimeout(agentId); this.emit('heartbeat', heartbeat); } updateHeartbeatTimeout(agentId) { // Clear existing timeout const existingTimeout = this.heartbeatTimeouts.get(agentId); if (existingTimeout) { clearTimeout(existingTimeout); } // Set new timeout const timeout = setTimeout(()=>{ this.handleHeartbeatTimeout(agentId); }, this.HEARTBEAT_TIMEOUT); this.heartbeatTimeouts.set(agentId, timeout); } async handleHeartbeatTimeout(agentId) { const currentState = await this.getAgentState(agentId); if (!currentState) return; if (currentState.status !== 'offline') { await this.setAgentStatus(agentId, 'offline', 'Heartbeat timeout'); this.logger.warn('Agent marked as offline due to heartbeat timeout', { agentId }); } this.heartbeatTimeouts.delete(agentId); this.emit('heartbeatTimeout', agentId); } async checkHeartbeats() { const now = Date.now(); const agentsToCheck = await this.queryAgentStates({}); for (const agent of agentsToCheck){ const timeSinceHeartbeat = now - agent.lastHeartbeat; if (timeSinceHeartbeat > this.HEARTBEAT_TIMEOUT && agent.status !== 'offline') { await this.handleHeartbeatTimeout(agent.id); } } } // ===== STATE QUERIES ===== async getAgentState(agentId) { // Check cache first const cachedState = this.stateCache.get(agentId); if (cachedState) { return cachedState; } // Get from Redis const stateKey = `agent-state:${agentId}`; const stateData = await this.redis.get(stateKey); if (stateData) { const state = JSON.parse(stateData); this.stateCache.set(agentId, state); setTimeout(()=>{ this.stateCache.delete(agentId); }, this.CACHE_TTL); return state; } return null; } async queryAgentStates(query) { const allAgents = await this.getAllAgentStates(); let filteredAgents = allAgents; // Apply filters if (query.agentIds && query.agentIds.length > 0) { filteredAgents = filteredAgents.filter((agent)=>query.agentIds.includes(agent.id)); } if (query.status && query.status.length > 0) { filteredAgents = filteredAgents.filter((agent)=>query.status.includes(agent.status)); } if (query.type && query.type.length > 0) { filteredAgents = filteredAgents.filter((agent)=>query.type.includes(agent.type)); } if (query.hasActiveTask !== undefined) { filteredAgents = filteredAgents.filter((agent)=>query.hasActiveTask ? !!agent.currentTask : !agent.currentTask); } if (query.minConfidence !== undefined) { filteredAgents = filteredAgents.filter((agent)=>agent.confidence >= query.minConfidence); } if (query.maxAge !== undefined) { const cutoffTime = Date.now() - query.maxAge; filteredAgents = filteredAgents.filter((agent)=>agent.lastHeartbeat >= cutoffTime); } // Apply pagination if (query.offset !== undefined) { filteredAgents = filteredAgents.slice(query.offset); } if (query.limit !== undefined) { filteredAgents = filteredAgents.slice(0, query.limit); } return filteredAgents; } async getAllAgentStates() { const keys = await this.redis.keys('agent-state:*'); const states = []; for (const key of keys){ const stateData = await this.redis.get(key); if (stateData) { try { const state = JSON.parse(stateData); states.push(state); } catch (error) { this.logger.error('Failed to parse agent state', { key, error }); } } } return states; } // ===== STATE AGGREGATION ===== async getStateAggregation() { const agents = await this.getAllAgentStates(); const statusBreakdown = { idle: 0, active: 0, busy: 0, error: 0, offline: 0, maintenance: 0 }; const typeBreakdown = {}; let totalConfidence = 0; let totalProgress = 0; let totalActiveTasks = 0; let totalResources = { cpu: 0, memory: 0, tokens: 0 }; for (const agent of agents){ // Status breakdown statusBreakdown[agent.status]++; // Type breakdown typeBreakdown[agent.type] = (typeBreakdown[agent.type] || 0) + 1; // Confidence and progress totalConfidence += agent.confidence; totalProgress += agent.progress; // Active tasks if (agent.currentTask) { totalActiveTasks++; } // Resources totalResources.cpu += agent.resources.cpu; totalResources.memory += agent.resources.memory; totalResources.tokens += agent.resources.tokensUsed; } const agentCount = agents.length; const healthScore = this.calculateHealthScore(agents); return { totalAgents: agentCount, statusBreakdown, typeBreakdown, averageConfidence: agentCount > 0 ? totalConfidence / agentCount : 0, averageProgress: agentCount > 0 ? totalProgress / agentCount : 0, totalActiveTasks, totalResources, healthScore }; } calculateHealthScore(agents) { if (agents.length === 0) return 0; let healthScore = 0; for (const agent of agents){ let agentScore = 100; // Deduct for offline/error status if (agent.status === 'offline' || agent.status === 'error') { agentScore -= 50; } else if (agent.status === 'maintenance') { agentScore -= 25; } // Deduct for low confidence if (agent.confidence < 0.5) { agentScore -= 30; } else if (agent.confidence < 0.7) { agentScore -= 15; } // Deduct for resource pressure if (agent.resources.cpu > 0.9 || agent.resources.memory > 0.9) { agentScore -= 20; } else if (agent.resources.cpu > 0.7 || agent.resources.memory > 0.7) { agentScore -= 10; } // Check heartbeat age const heartbeatAge = Date.now() - agent.lastHeartbeat; if (heartbeatAge > 30000) { agentScore -= 40; } else if (heartbeatAge > 15000) { agentScore -= 20; } healthScore += Math.max(0, agentScore); } return healthScore / agents.length; } // ===== EVENT PUBLISHING ===== async publishStateChange(event) { const channel = `agent-state:${event.agentId}:change`; await this.redis.publish(channel, JSON.stringify(event)); } async publishHeartbeat(heartbeat) { const channel = `agent-state:${heartbeat.agentId}:heartbeat`; await this.redis.publish(channel, JSON.stringify({ type: 'heartbeat', heartbeat })); } // ===== CLEANUP AND MAINTENANCE ===== async performCleanup() { const now = Date.now(); const keys = await this.redis.keys('agent-state:*'); for (const key of keys){ const stateData = await this.redis.get(key); if (stateData) { try { const state = JSON.parse(stateData); const age = now - state.lastHeartbeat; // Remove very old states (older than 24 hours) if (age > 24 * 60 * 60 * 1000) { await this.redis.del(key); this.stateCache.delete(state.id); this.logger.debug('Cleaned up old agent state', { agentId: state.id }); } } catch (error) { // Remove corrupted state data await this.redis.del(key); this.logger.error('Cleaned up corrupted agent state', { key, error }); } } } } // ===== AGENT LIFECYCLE ===== async createAgentState(agentId, status = 'idle', resources = {}) { if (!agentId) { agentId = uuidv4(); // Generate a UUID if no ID provided } const state = { id: agentId, type: 'unknown', status, progress: 0, confidence: 0, lastHeartbeat: Date.now(), metadata: {}, resources: { cpu: 0, memory: 0, tokensUsed: 0, ...resources }, capabilities: [] }; await this.updateAgentState(agentId, state, 'Agent created'); this.emit('agentCreated', state); return state; } async removeAgentState(agentId) { const stateKey = `agent-state:${agentId}`; await this.redis.del(stateKey); this.stateCache.delete(agentId); // Clear heartbeat timeout const timeout = this.heartbeatTimeouts.get(agentId); if (timeout) { clearTimeout(timeout); this.heartbeatTimeouts.delete(agentId); } this.emit('agentRemoved', agentId); this.logger.info('Agent state removed', { agentId }); } // ===== HEALTH CHECKS ===== async healthCheck() { try { await this.redis.ping(); const aggregation = await this.getStateAggregation(); const activeAgents = aggregation.statusBreakdown.active + aggregation.statusBreakdown.busy; const now = Date.now(); const agents = await this.getAllAgentStates(); const heartbeatAges = agents.map((agent)=>now - agent.lastHeartbeat); const averageHeartbeatAge = heartbeatAges.length > 0 ? heartbeatAges.reduce((sum, age)=>sum + age, 0) / heartbeatAges.length : 0; let status = 'healthy'; if (averageHeartbeatAge > 30000) status = 'degraded'; if (averageHeartbeatAge > 60000) status = 'unhealthy'; if (aggregation.healthScore < 70) status = 'degraded'; if (aggregation.healthScore < 50) status = 'unhealthy'; return { status, totalAgents: aggregation.totalAgents, activeAgents, averageHeartbeatAge, redisConnected: true }; } catch (error) { return { status: 'unhealthy', totalAgents: 0, activeAgents: 0, averageHeartbeatAge: 0, redisConnected: false }; } } // ===== LIFECYCLE MANAGEMENT ===== async shutdown() { this.logger.info('Shutting down agent state manager'); // Clear intervals if (this.heartbeatInterval) clearInterval(this.heartbeatInterval); if (this.cleanupInterval) clearInterval(this.cleanupInterval); // Clear timeouts for (const timeout of this.heartbeatTimeouts.values()){ clearTimeout(timeout); } this.heartbeatTimeouts.clear(); // Clear cache this.stateCache.clear(); this.removeAllListeners(); this.logger.info('Agent state manager shutdown complete'); } } //# sourceMappingURL=agent-state-management.js.map