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
text/typescript
/**
* 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);
}