UNPKG

claude-code-collective

Version:

Sub-agent collective framework for Claude Code with TDD validation, TaskMaster Task ID integration, hub-spoke coordination, and deterministic handoffs

827 lines (700 loc) 22.7 kB
/** * Claude Code Sub-Agent Collective - Agent Lifecycle Manager * * Phase 7: Dynamic Agent Creation * * This module provides automated lifecycle management including agent cleanup, * resource deallocation, and garbage collection for inactive agents. * * @author Claude Code Sub-Agent Collective * @version 1.0.0 */ const EventEmitter = require('events'); const fs = require('fs-extra'); const path = require('path'); class AgentLifecycleManager extends EventEmitter { constructor(spawner, registry, options = {}) { super(); this.spawner = spawner; this.registry = registry; // Configuration this.config = { // Auto-cleanup policies autoCleanupEnabled: options.autoCleanupEnabled !== false, idleTimeout: options.idleTimeout || 3600000, // 1 hour maxAge: options.maxAge || 86400000, // 24 hours minPerformanceThreshold: options.minPerformanceThreshold || 0.7, // Resource limits maxMemoryUsage: options.maxMemoryUsage || 100 * 1024 * 1024, // 100MB maxCpuUsage: options.maxCpuUsage || 80, // 80% maxDiskUsage: options.maxDiskUsage || 50 * 1024 * 1024, // 50MB // Health monitoring healthCheckInterval: options.healthCheckInterval || 300000, // 5 minutes maxFailures: options.maxFailures || 3, // Scaling policies autoScalingEnabled: options.autoScalingEnabled || false, maxActiveAgents: options.maxActiveAgents || 20, scaleUpThreshold: options.scaleUpThreshold || 0.8, scaleDownThreshold: options.scaleDownThreshold || 0.3, // Cleanup schedules cleanupInterval: options.cleanupInterval || 1800000, // 30 minutes deepCleanupInterval: options.deepCleanupInterval || 43200000, // 12 hours ...options.config }; // Internal state this.policies = new Map(); this.schedules = new Map(); this.monitors = new Map(); this.cleanupHistory = []; this.statistics = { totalCleanups: 0, agentsRecycled: 0, resourcesReclaimed: 0, healthChecksPerformed: 0, scalingEvents: 0 }; this.initialized = false; this.timers = new Map(); // Initialize default policies this.initializeDefaultPolicies(); } /** * Initialize default lifecycle policies */ initializeDefaultPolicies() { // Idle timeout policy this.policies.set('idle-timeout', { enabled: true, name: 'Idle Timeout Cleanup', description: 'Remove agents that have been inactive for too long', condition: (agent) => { const lastSeen = new Date(agent.activity.lastSeen); const idleTime = Date.now() - lastSeen.getTime(); return idleTime > this.config.idleTimeout; }, action: 'cleanup', priority: 3 }); // Max age policy this.policies.set('max-age', { enabled: true, name: 'Maximum Age Cleanup', description: 'Remove agents that have exceeded maximum lifespan', condition: (agent) => { const created = new Date(agent.metadata.registeredAt); const age = Date.now() - created.getTime(); return age > this.config.maxAge; }, action: 'cleanup', priority: 2 }); // Performance threshold policy this.policies.set('performance-threshold', { enabled: true, name: 'Low Performance Cleanup', description: 'Remove agents with consistently poor performance', condition: (agent) => { return ( agent.performance.successRate < this.config.minPerformanceThreshold && agent.activity.invocations > 10 // Minimum sample size ); }, action: 'cleanup', priority: 1 }); // Resource usage policy this.policies.set('resource-usage', { enabled: true, name: 'High Resource Usage', description: 'Flag or cleanup agents consuming excessive resources', condition: (agent) => { return ( agent.resources.memoryUsage > this.config.maxMemoryUsage || agent.resources.cpuUsage > this.config.maxCpuUsage || agent.resources.diskUsage > this.config.maxDiskUsage ); }, action: 'warn', // First warn, then cleanup on repeated violations priority: 1 }); // Health failure policy this.policies.set('health-failures', { enabled: true, name: 'Health Check Failures', description: 'Remove agents that repeatedly fail health checks', condition: (agent) => { return agent.health.failureCount >= this.config.maxFailures; }, action: 'cleanup', priority: 1 }); } /** * Initialize the lifecycle manager */ async initialize() { if (this.initialized) { return { success: true, message: 'Already initialized' }; } // Set up event listeners this.registry.on('agent-registered', (agent) => this.onAgentRegistered(agent)); this.registry.on('agent-unregistered', (data) => this.onAgentUnregistered(data)); this.registry.on('agent-activity-updated', (data) => this.onAgentActivity(data)); this.registry.on('agent-health-checked', (result) => this.onHealthCheck(result)); // Start scheduled tasks await this.startScheduledTasks(); this.initialized = true; this.emit('lifecycle-manager-initialized', { policies: this.policies.size, config: this.config }); return { success: true, message: 'AgentLifecycleManager initialized successfully', policies: Array.from(this.policies.keys()) }; } /** * Start scheduled tasks */ async startScheduledTasks() { // Cleanup timer if (this.config.autoCleanupEnabled && this.config.cleanupInterval > 0) { this.timers.set('cleanup', setInterval(async () => { await this.runCleanup(); }, this.config.cleanupInterval).unref()); } // Deep cleanup timer if (this.config.deepCleanupInterval > 0) { this.timers.set('deep-cleanup', setInterval(async () => { await this.runDeepCleanup(); }, this.config.deepCleanupInterval).unref()); } // Health monitoring timer if (this.config.healthCheckInterval > 0) { this.timers.set('health-check', setInterval(async () => { await this.runHealthMonitoring(); }, this.config.healthCheckInterval).unref()); } // Scaling evaluation timer if (this.config.autoScalingEnabled) { this.timers.set('scaling', setInterval(async () => { await this.evaluateScaling(); }, 60000).unref()); // Every minute } } /** * Run cleanup cycle */ async runCleanup() { const startTime = Date.now(); try { const agents = this.registry.query({ status: 'active' }); const cleanupActions = []; // Evaluate each agent against policies for (const agent of agents) { const violations = await this.evaluatePolicies(agent); if (violations.length > 0) { cleanupActions.push({ agent, violations, action: this.determineAction(violations) }); } } // Execute cleanup actions const results = await this.executeCleanupActions(cleanupActions); // Update statistics this.statistics.totalCleanups++; // Log cleanup results this.logCleanup({ timestamp: new Date().toISOString(), duration: Date.now() - startTime, agentsEvaluated: agents.length, actionsExecuted: cleanupActions.length, results }); this.emit('cleanup-completed', { agentsEvaluated: agents.length, actionsExecuted: cleanupActions.length, duration: Date.now() - startTime }); return results; } catch (error) { this.emit('cleanup-failed', { error: error.message }); throw error; } } /** * Run deep cleanup cycle */ async runDeepCleanup() { const startTime = Date.now(); try { // Clean up orphaned files await this.cleanupOrphanedFiles(); // Clean up old registry backups await this.cleanupOldBackups(); // Clean up test artifacts await this.cleanupTestArtifacts(); // Defragment registry await this.defragmentRegistry(); this.emit('deep-cleanup-completed', { duration: Date.now() - startTime }); } catch (error) { this.emit('deep-cleanup-failed', { error: error.message }); } } /** * Run health monitoring */ async runHealthMonitoring() { try { const agents = this.registry.query({ status: 'active' }); const healthResults = []; for (const agent of agents) { try { const healthResult = await this.registry.checkHealth(agent.id); healthResults.push(healthResult); // Update monitor data this.monitors.set(agent.id, { ...this.monitors.get(agent.id), lastHealthCheck: healthResult.timestamp, healthStatus: healthResult.status, issues: healthResult.issues }); } catch (error) { console.warn(`Health check failed for agent ${agent.id}:`, error.message); } } this.statistics.healthChecksPerformed += healthResults.length; this.emit('health-monitoring-completed', { agentsChecked: healthResults.length, healthyAgents: healthResults.filter(r => r.status === 'healthy').length, unhealthyAgents: healthResults.filter(r => r.status === 'unhealthy').length }); } catch (error) { this.emit('health-monitoring-failed', { error: error.message }); } } /** * Evaluate scaling needs */ async evaluateScaling() { if (!this.config.autoScalingEnabled) { return; } try { const agents = this.registry.query({ status: 'active' }); const activeCount = agents.length; // Calculate system load metrics const loadMetrics = this.calculateLoadMetrics(agents); // Determine scaling action let scalingAction = null; if (loadMetrics.avgLoad > this.config.scaleUpThreshold && activeCount < this.config.maxActiveAgents) { scalingAction = 'scale-up'; } else if (loadMetrics.avgLoad < this.config.scaleDownThreshold && activeCount > 1) { scalingAction = 'scale-down'; } if (scalingAction) { await this.executeScalingAction(scalingAction, loadMetrics); } } catch (error) { this.emit('scaling-evaluation-failed', { error: error.message }); } } /** * Evaluate policies for an agent * @param {object} agent - Agent to evaluate * @returns {array} Policy violations */ async evaluatePolicies(agent) { const violations = []; for (const [policyId, policy] of this.policies) { if (!policy.enabled) continue; try { if (await policy.condition(agent)) { violations.push({ policyId, policy, agent: agent.id, timestamp: new Date().toISOString() }); } } catch (error) { console.warn(`Policy evaluation failed for ${policyId}:`, error.message); } } return violations; } /** * Determine action from violations * @param {array} violations - Policy violations * @returns {string} Action to take */ determineAction(violations) { if (violations.length === 0) return 'none'; // Sort by priority and determine action const sorted = violations.sort((a, b) => a.policy.priority - b.policy.priority); // Check for cleanup actions if (sorted.some(v => v.policy.action === 'cleanup')) { return 'cleanup'; } // Check for warn actions if (sorted.some(v => v.policy.action === 'warn')) { return 'warn'; } return 'monitor'; } /** * Execute cleanup actions * @param {array} actions - Cleanup actions to execute * @returns {array} Execution results */ async executeCleanupActions(actions) { const results = []; for (const actionItem of actions) { const { agent, violations, action } = actionItem; try { let result; switch (action) { case 'cleanup': result = await this.cleanupAgent(agent.id, violations); break; case 'warn': result = await this.warnAgent(agent.id, violations); break; case 'monitor': result = await this.monitorAgent(agent.id, violations); break; default: result = { success: false, message: 'Unknown action' }; } results.push({ agentId: agent.id, action, violations: violations.length, ...result }); } catch (error) { results.push({ agentId: agent.id, action, success: false, error: error.message }); } } return results; } /** * Cleanup agent * @param {string} agentId - Agent ID * @param {array} violations - Policy violations * @returns {object} Cleanup result */ async cleanupAgent(agentId, violations) { const reasons = violations.map(v => v.policy.name); // Attempt graceful shutdown first await this.gracefulShutdown(agentId); // Unregister agent await this.registry.unregister(agentId, { reason: `Policy violations: ${reasons.join(', ')}`, cleanup: true }); this.statistics.agentsRecycled++; this.emit('agent-cleaned-up', { agentId, violations, reasons }); return { success: true, message: `Agent cleaned up due to: ${reasons.join(', ')}` }; } /** * Warn agent (log warning for now) * @param {string} agentId - Agent ID * @param {array} violations - Policy violations * @returns {object} Warning result */ async warnAgent(agentId, violations) { const reasons = violations.map(v => v.policy.name); // Update monitor data const monitor = this.monitors.get(agentId) || {}; monitor.warnings = (monitor.warnings || []).concat({ timestamp: new Date().toISOString(), violations: reasons }); this.monitors.set(agentId, monitor); this.emit('agent-warned', { agentId, violations, reasons }); return { success: true, message: `Agent warned for: ${reasons.join(', ')}` }; } /** * Monitor agent (increase monitoring frequency) * @param {string} agentId - Agent ID * @param {array} violations - Policy violations * @returns {object} Monitoring result */ async monitorAgent(agentId, violations) { const monitor = this.monitors.get(agentId) || {}; monitor.enhancedMonitoring = true; monitor.monitoringSince = new Date().toISOString(); monitor.violations = violations; this.monitors.set(agentId, monitor); return { success: true, message: 'Agent under enhanced monitoring' }; } /** * Graceful shutdown of agent * @param {string} agentId - Agent ID */ async gracefulShutdown(agentId) { // Give agent time to complete current tasks // This is a placeholder - in real implementation would send shutdown signal await new Promise(resolve => setTimeout(resolve, 1000).unref()); } /** * Calculate system load metrics * @param {array} agents - Active agents * @returns {object} Load metrics */ calculateLoadMetrics(agents) { if (agents.length === 0) { return { avgLoad: 0, totalInvocations: 0, avgResponseTime: 0 }; } const totalInvocations = agents.reduce((sum, agent) => sum + agent.activity.invocations, 0 ); const totalResponseTime = agents.reduce((sum, agent) => sum + agent.performance.avgResponseTime, 0 ); return { avgLoad: totalInvocations / agents.length, totalInvocations, avgResponseTime: totalResponseTime / agents.length }; } /** * Execute scaling action * @param {string} action - Scaling action (scale-up or scale-down) * @param {object} loadMetrics - Current load metrics */ async executeScalingAction(action, loadMetrics) { try { if (action === 'scale-up') { // Spawn additional general-purpose agent await this.spawner.spawn({ name: 'auto-scaled-agent', agentType: 'auto-scaled', purpose: 'Load balancing and overflow handling', type: 'general' }); this.emit('scaled-up', { loadMetrics }); } else if (action === 'scale-down') { // Find least active agent to remove const agents = this.registry.query({ status: 'active', sortBy: 'activity.lastSeen', sortOrder: 'asc', limit: 1 }); if (agents.length > 0 && agents[0].name.includes('auto-scaled')) { await this.cleanupAgent(agents[0].id, [{ policy: { name: 'Auto-scaling down' } }]); this.emit('scaled-down', { loadMetrics }); } } this.statistics.scalingEvents++; } catch (error) { this.emit('scaling-failed', { action, error: error.message }); } } /** * Clean up orphaned files */ async cleanupOrphanedFiles() { const agentsDir = path.join(process.cwd(), '.claude', 'agents'); const registeredAgents = this.registry.query({}).map(a => `${a.id}.md`); if (await fs.pathExists(agentsDir)) { const files = await fs.readdir(agentsDir); const agentFiles = files.filter(f => f.endsWith('.md')); for (const file of agentFiles) { if (!registeredAgents.includes(file)) { await fs.remove(path.join(agentsDir, file)); this.statistics.resourcesReclaimed++; } } } } /** * Clean up old backups */ async cleanupOldBackups() { // This would be implemented based on registry backup cleanup // For now, just emit event this.emit('old-backups-cleaned'); } /** * Clean up test artifacts */ async cleanupTestArtifacts() { const testsDir = path.join(process.cwd(), '.claude-collective', 'tests', 'agents'); const registeredAgents = this.registry.query({}).map(a => a.id); if (await fs.pathExists(testsDir)) { const testDirs = await fs.readdir(testsDir); for (const testDir of testDirs) { if (!registeredAgents.includes(testDir)) { await fs.remove(path.join(testsDir, testDir)); this.statistics.resourcesReclaimed++; } } } } /** * Defragment registry */ async defragmentRegistry() { // Trigger registry cleanup and optimization await this.registry.saveRegistry(); this.emit('registry-defragmented'); } /** * Log cleanup activity * @param {object} cleanupData - Cleanup data */ logCleanup(cleanupData) { this.cleanupHistory.push(cleanupData); // Keep history limited if (this.cleanupHistory.length > 100) { this.cleanupHistory = this.cleanupHistory.slice(-50); } } /** * Event handlers */ onAgentRegistered(agent) { this.monitors.set(agent.id, { registeredAt: new Date().toISOString(), warnings: [], enhancedMonitoring: false }); } onAgentUnregistered(data) { this.monitors.delete(data.agentId); } onAgentActivity(data) { const monitor = this.monitors.get(data.agentId); if (monitor) { monitor.lastActivity = new Date().toISOString(); this.monitors.set(data.agentId, monitor); } } onHealthCheck(result) { const monitor = this.monitors.get(result.agentId); if (monitor) { monitor.lastHealthCheck = result.timestamp; monitor.healthStatus = result.status; this.monitors.set(result.agentId, monitor); } } /** * Get lifecycle statistics * @returns {object} Lifecycle statistics */ getStatistics() { return { ...this.statistics, policies: this.policies.size, monitoredAgents: this.monitors.size, activeTimers: this.timers.size, recentCleanups: this.cleanupHistory.slice(-10), config: { autoCleanupEnabled: this.config.autoCleanupEnabled, autoScalingEnabled: this.config.autoScalingEnabled, idleTimeout: this.config.idleTimeout, maxAge: this.config.maxAge, minPerformanceThreshold: this.config.minPerformanceThreshold } }; } /** * Update policy configuration * @param {string} policyId - Policy ID * @param {object} updates - Policy updates * @returns {object} Update result */ updatePolicy(policyId, updates) { const policy = this.policies.get(policyId); if (!policy) { throw new Error(`Policy '${policyId}' not found`); } Object.assign(policy, updates); this.policies.set(policyId, policy); this.emit('policy-updated', { policyId, updates }); return { success: true, policyId, message: `Policy '${policyId}' updated successfully` }; } /** * Add custom policy * @param {string} policyId - Policy ID * @param {object} policyConfig - Policy configuration * @returns {object} Addition result */ addPolicy(policyId, policyConfig) { if (this.policies.has(policyId)) { throw new Error(`Policy '${policyId}' already exists`); } // Validate policy config if (!policyConfig.condition || typeof policyConfig.condition !== 'function') { throw new Error('Policy must have a condition function'); } if (!policyConfig.action) { throw new Error('Policy must specify an action'); } this.policies.set(policyId, { enabled: true, priority: 5, ...policyConfig }); this.emit('policy-added', { policyId }); return { success: true, policyId, message: `Policy '${policyId}' added successfully` }; } /** * Shutdown lifecycle manager */ async shutdown() { // Clear all timers for (const [name, timer] of this.timers) { clearInterval(timer); } this.timers.clear(); this.emit('lifecycle-manager-shutdown'); return { success: true, message: 'Lifecycle manager shutdown completed' }; } } module.exports = AgentLifecycleManager;