UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

477 lines (398 loc) 13.2 kB
/** * V3 Agent Pool * Manages agent lifecycle, pooling, and auto-scaling */ import { EventEmitter } from 'events'; import { AgentState, AgentType, AgentStatus, AgentCapabilities, AgentMetrics, AgentPoolConfig, AgentPoolState, IAgentPool, SWARM_CONSTANTS, } from './types.js'; interface PooledAgent { agent: AgentState; acquiredAt?: Date; lastUsed: Date; usageCount: number; } export class AgentPool extends EventEmitter implements IAgentPool { private config: AgentPoolConfig; private pooledAgents: Map<string, PooledAgent> = new Map(); private available: Set<string> = new Set(); private busy: Set<string> = new Set(); private pendingScale: number = 0; private lastScaleOperation?: Date; private healthCheckInterval?: NodeJS.Timeout; private agentCounter: number = 0; constructor(config: Partial<AgentPoolConfig> = {}) { super(); this.config = { name: config.name ?? 'default-pool', type: config.type ?? 'worker', minSize: config.minSize ?? 1, maxSize: config.maxSize ?? 10, scaleUpThreshold: config.scaleUpThreshold ?? 0.8, scaleDownThreshold: config.scaleDownThreshold ?? 0.2, cooldownMs: config.cooldownMs ?? 30000, healthCheckIntervalMs: config.healthCheckIntervalMs ?? SWARM_CONSTANTS.DEFAULT_HEALTH_CHECK_INTERVAL_MS, }; } async initialize(config?: AgentPoolConfig): Promise<void> { if (config) { this.config = { ...this.config, ...config }; } // Start with minimum pool size - create agents in parallel const createPromises = Array.from( { length: this.config.minSize }, () => this.createPooledAgent() ); await Promise.all(createPromises); // Start health checks this.startHealthChecks(); this.emit('initialized', { poolName: this.config.name, size: this.pooledAgents.size }); } async shutdown(): Promise<void> { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.healthCheckInterval = undefined; } // Mark all agents as terminated for (const pooled of this.pooledAgents.values()) { pooled.agent.status = 'terminated'; } this.pooledAgents.clear(); this.available.clear(); this.busy.clear(); this.emit('shutdown'); } async acquire(): Promise<AgentState | undefined> { // Try to get an available agent const availableId = this.available.values().next().value as string | undefined; if (availableId) { const pooled = this.pooledAgents.get(availableId); if (pooled) { this.available.delete(availableId); this.busy.add(availableId); pooled.acquiredAt = new Date(); pooled.usageCount++; pooled.agent.status = 'busy'; this.emit('agent.acquired', { agentId: availableId }); // Check if we need to scale up await this.checkScaling(); return pooled.agent; } } // No available agents, try to scale up if (this.pooledAgents.size < this.config.maxSize) { const agent = await this.createPooledAgent(); if (agent) { const pooled = this.pooledAgents.get(agent.id.id)!; this.available.delete(agent.id.id); this.busy.add(agent.id.id); pooled.acquiredAt = new Date(); pooled.usageCount++; agent.status = 'busy'; this.emit('agent.acquired', { agentId: agent.id.id }); return agent; } } // Pool exhausted this.emit('pool.exhausted'); return undefined; } async release(agentId: string): Promise<void> { const pooled = this.pooledAgents.get(agentId); if (!pooled) { return; } this.busy.delete(agentId); this.available.add(agentId); pooled.acquiredAt = undefined; pooled.lastUsed = new Date(); pooled.agent.status = 'idle'; pooled.agent.currentTask = undefined; this.emit('agent.released', { agentId }); // Check if we need to scale down await this.checkScaling(); } async add(agent: AgentState): Promise<void> { if (this.pooledAgents.size >= this.config.maxSize) { throw new Error(`Pool ${this.config.name} is at maximum capacity`); } const pooled: PooledAgent = { agent, lastUsed: new Date(), usageCount: 0, }; this.pooledAgents.set(agent.id.id, pooled); this.available.add(agent.id.id); this.emit('agent.added', { agentId: agent.id.id }); } async remove(agentId: string): Promise<void> { const pooled = this.pooledAgents.get(agentId); if (!pooled) { return; } this.pooledAgents.delete(agentId); this.available.delete(agentId); this.busy.delete(agentId); pooled.agent.status = 'terminated'; this.emit('agent.removed', { agentId }); } async scale(delta: number): Promise<void> { const now = new Date(); // Check cooldown if (this.lastScaleOperation) { const timeSinceLastScale = now.getTime() - this.lastScaleOperation.getTime(); if (timeSinceLastScale < this.config.cooldownMs) { return; } } if (delta > 0) { // Scale up - create agents in parallel const targetSize = Math.min( this.pooledAgents.size + delta, this.config.maxSize ); const toCreate = targetSize - this.pooledAgents.size; const createPromises = Array.from( { length: toCreate }, () => this.createPooledAgent() ); await Promise.all(createPromises); this.emit('pool.scaled_up', { added: toCreate }); } else if (delta < 0) { // Scale down - remove agents in parallel const targetSize = Math.max( this.pooledAgents.size + delta, this.config.minSize ); const toRemove = this.pooledAgents.size - targetSize; // Remove least recently used available agents const sortedAvailable = Array.from(this.available) .map(id => this.pooledAgents.get(id)!) .filter(p => p !== undefined) .sort((a, b) => a.lastUsed.getTime() - b.lastUsed.getTime()); const agentsToRemove = sortedAvailable.slice(0, toRemove); await Promise.all(agentsToRemove.map(pooled => this.remove(pooled.agent.id.id))); this.emit('pool.scaled_down', { removed: agentsToRemove.length }); } this.lastScaleOperation = now; } getState(): AgentPoolState { return { id: `pool_${this.config.name}`, config: { ...this.config }, agents: new Map( Array.from(this.pooledAgents.entries()).map( ([id, pooled]) => [id, pooled.agent] ) ), availableAgents: Array.from(this.available), busyAgents: Array.from(this.busy), pendingScale: this.pendingScale, lastScaleOperation: this.lastScaleOperation, }; } getAvailableCount(): number { return this.available.size; } getBusyCount(): number { return this.busy.size; } getTotalCount(): number { return this.pooledAgents.size; } getUtilization(): number { if (this.pooledAgents.size === 0) return 0; return this.busy.size / this.pooledAgents.size; } // ===== PRIVATE METHODS ===== private async createPooledAgent(): Promise<AgentState | undefined> { if (this.pooledAgents.size >= this.config.maxSize) { return undefined; } this.agentCounter++; const agentId = `${this.config.name}_agent_${this.agentCounter}`; const agent: AgentState = { id: { id: agentId, swarmId: 'pool', type: this.config.type, instance: this.agentCounter, }, name: `${this.config.name}-${this.agentCounter}`, type: this.config.type, status: 'idle', capabilities: this.createDefaultCapabilities(), metrics: this.createDefaultMetrics(), workload: 0, health: 1.0, lastHeartbeat: new Date(), connections: [], }; const pooled: PooledAgent = { agent, lastUsed: new Date(), usageCount: 0, }; this.pooledAgents.set(agentId, pooled); this.available.add(agentId); this.emit('agent.created', { agentId }); return agent; } private createDefaultCapabilities(): AgentCapabilities { return { codeGeneration: true, codeReview: true, testing: true, documentation: true, research: true, analysis: true, coordination: this.config.type === 'coordinator', languages: ['typescript', 'javascript', 'python'], frameworks: ['node', 'deno', 'react'], domains: ['development', 'testing', 'analysis'], tools: ['git', 'npm', 'editor'], maxConcurrentTasks: 3, maxMemoryUsage: 512 * 1024 * 1024, maxExecutionTime: SWARM_CONSTANTS.DEFAULT_TASK_TIMEOUT_MS, reliability: 0.95, speed: 1.0, quality: 0.9, }; } private createDefaultMetrics(): AgentMetrics { return { tasksCompleted: 0, tasksFailed: 0, averageExecutionTime: 0, successRate: 1.0, cpuUsage: 0, memoryUsage: 0, messagesProcessed: 0, lastActivity: new Date(), responseTime: 0, health: 1.0, }; } private async checkScaling(): Promise<void> { const utilization = this.getUtilization(); if (utilization >= this.config.scaleUpThreshold && this.pooledAgents.size < this.config.maxSize) { // Scale up by 1 this.pendingScale = 1; await this.scale(1); this.pendingScale = 0; } else if (utilization <= this.config.scaleDownThreshold && this.pooledAgents.size > this.config.minSize) { // Scale down by 1 this.pendingScale = -1; await this.scale(-1); this.pendingScale = 0; } } private startHealthChecks(): void { this.healthCheckInterval = setInterval(() => { this.performHealthChecks(); }, this.config.healthCheckIntervalMs); } private performHealthChecks(): void { const now = new Date(); const unhealthyThresholdMs = this.config.healthCheckIntervalMs * 3; for (const [agentId, pooled] of this.pooledAgents) { const timeSinceLastActivity = now.getTime() - pooled.agent.lastHeartbeat.getTime(); if (timeSinceLastActivity > unhealthyThresholdMs) { // Agent is unhealthy pooled.agent.health = Math.max(0, pooled.agent.health - 0.2); pooled.agent.status = 'error'; this.emit('agent.unhealthy', { agentId, health: pooled.agent.health }); // If completely unhealthy, remove and replace if (pooled.agent.health <= 0) { this.replaceUnhealthyAgent(agentId); } } else { // Update health positively pooled.agent.health = Math.min(1.0, pooled.agent.health + 0.1); pooled.agent.lastHeartbeat = now; } } } private async replaceUnhealthyAgent(agentId: string): Promise<void> { const pooled = this.pooledAgents.get(agentId); if (!pooled) return; const wasBusy = this.busy.has(agentId); await this.remove(agentId); // Create replacement if below min size or was busy if (this.pooledAgents.size < this.config.minSize || wasBusy) { await this.createPooledAgent(); } this.emit('agent.replaced', { oldAgentId: agentId }); } // ===== UTILITY METHODS ===== getAgent(agentId: string): AgentState | undefined { return this.pooledAgents.get(agentId)?.agent; } getAllAgents(): AgentState[] { return Array.from(this.pooledAgents.values()).map(p => p.agent); } getAvailableAgents(): AgentState[] { return Array.from(this.available) .map(id => this.pooledAgents.get(id)?.agent) .filter((a): a is AgentState => a !== undefined); } getBusyAgents(): AgentState[] { return Array.from(this.busy) .map(id => this.pooledAgents.get(id)?.agent) .filter((a): a is AgentState => a !== undefined); } updateAgentHeartbeat(agentId: string): void { const pooled = this.pooledAgents.get(agentId); if (pooled) { pooled.agent.lastHeartbeat = new Date(); pooled.agent.health = Math.min(1.0, pooled.agent.health + 0.05); } } updateAgentMetrics(agentId: string, metrics: Partial<AgentMetrics>): void { const pooled = this.pooledAgents.get(agentId); if (pooled) { pooled.agent.metrics = { ...pooled.agent.metrics, ...metrics }; pooled.agent.lastHeartbeat = new Date(); } } getPoolStats(): { total: number; available: number; busy: number; utilization: number; avgHealth: number; avgUsageCount: number; } { const agents = Array.from(this.pooledAgents.values()); const avgHealth = agents.length > 0 ? agents.reduce((sum, p) => sum + p.agent.health, 0) / agents.length : 1.0; const avgUsageCount = agents.length > 0 ? agents.reduce((sum, p) => sum + p.usageCount, 0) / agents.length : 0; return { total: this.pooledAgents.size, available: this.available.size, busy: this.busy.size, utilization: this.getUtilization(), avgHealth, avgUsageCount, }; } } export function createAgentPool(config?: Partial<AgentPoolConfig>): AgentPool { return new AgentPool(config); }