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.

369 lines (368 loc) 14.4 kB
/** * Coordination Layer for Agent Communication and Tracking * * Provides agent registration, status tracking, message passing, and * coordination protocol enforcement with Redis as the backing store. * * Supports both Task Mode (Redis stubbed) and CLI Mode (full coordination). * * Key Features: * - Agent registration with metadata tracking * - Status updates and health checks * - Message passing between agents (pub/sub) * - Broadcast messages with protocol enforcement * - Wait for completion with timeout support * - Type-safe interfaces using branded types */ import { CoordinationError, CoordinationErrorType } from './types-export'; /** * Coordination Layer Implementation * * Manages agent lifecycle, status tracking, and inter-agent communication. * Gracefully handles Task Mode (no Redis) and CLI Mode (full coordination). */ export class CoordinationLayer { redis; logger; canUseRedis; executionMode; taskId; // In-memory registry for Task Mode taskModeRegistry = new Map(); taskModeMessages = new Map(); constructor(config){ this.redis = config.redis; this.logger = config.logger; this.canUseRedis = config.canUseRedis; this.executionMode = config.executionMode; this.taskId = config.taskId; } /** * Register an agent with the coordination layer * * Creates tracking record for agent lifecycle. */ async registerAgent(agentId, type, metadata, iteration = 1, pid) { const now = new Date().toISOString(); const agentMetadata = { agentId, type, taskId: this.taskId, status: 'registered', iteration, createdAt: now, lastHeartbeat: now, pid, metadata }; if (!this.canUseRedis) { // Task Mode: Store in memory this.taskModeRegistry.set(agentId, agentMetadata); this.logger.info(`Task Mode: Registered agent ${agentId} (type: ${type})`); return; } // CLI Mode: Store in Redis const key = `swarm:${this.taskId}:agents:${agentId}`; try { await this.redis.hset(key, 'type', type, 'status', 'registered', 'iteration', iteration.toString(), 'createdAt', now, 'lastHeartbeat', now, ...pid ? [ 'pid', pid.toString() ] : [], ...metadata ? [ 'metadata', JSON.stringify(metadata) ] : []); // Set 24h TTL await this.redis.expire(key, 86400); this.logger.debug(`Registered agent ${agentId} in Redis`); } catch (error) { throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to register agent ${agentId}`, this.executionMode, true); } } /** * Update agent status * * Tracks status changes during agent lifecycle. */ async updateAgentStatus(agentId, status) { const now = new Date().toISOString(); if (!this.canUseRedis) { // Task Mode: Update in memory const agent = this.taskModeRegistry.get(agentId); if (agent) { agent.status = status; agent.lastHeartbeat = now; this.taskModeRegistry.set(agentId, agent); } this.logger.debug(`Task Mode: Updated ${agentId} status to ${status}`); return; } // CLI Mode: Update in Redis const key = `swarm:${this.taskId}:agents:${agentId}`; try { await this.redis.hset(key, 'status', status, 'lastHeartbeat', now); this.logger.debug(`Updated agent ${agentId} status to ${status}`); } catch (error) { throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to update agent ${agentId} status`, this.executionMode, true); } } /** * Send direct message between agents * * Delivers message from one agent to another. */ async sendMessage(from, to, messageType, payload, correlationId) { const message = { from, to, type: messageType, payload, timestamp: new Date().toISOString(), correlationId }; if (!this.canUseRedis) { // Task Mode: Store in memory const key = `${from}:${to}`; if (!this.taskModeMessages.has(key)) { this.taskModeMessages.set(key, []); } this.taskModeMessages.get(key).push(message); this.logger.debug(`Task Mode: Message from ${from} to ${to}`); return; } // CLI Mode: Store in Redis const key = `swarm:${this.taskId}:messages:${from}:${to}`; try { await this.redis.lpush(key, JSON.stringify(message)); await this.redis.expire(key, 3600); // 1h TTL for messages this.logger.debug(`Message sent from ${from} to ${to}`); } catch (error) { throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to send message from ${from} to ${to}`, this.executionMode, true); } } /** * Broadcast message to all agents in task * * Delivers message to every agent participating in the task. * Used for coordination signals (gate passed, iteration started, etc). */ async broadcastMessage(from, messageType, payload, correlationId) { const message = { from, taskId: this.taskId, type: messageType, payload, timestamp: new Date().toISOString(), correlationId }; if (!this.canUseRedis) { // Task Mode: Log broadcast this.logger.info(`Task Mode: Broadcast ${messageType} from ${from} (payload: ${JSON.stringify(payload)})`); return; } // CLI Mode: Store in Redis as sorted set for ordering const key = `swarm:${this.taskId}:broadcasts`; try { const timestamp = Date.now(); await this.redis.zadd(key, timestamp, JSON.stringify(message)); // Set 1h TTL await this.redis.expire(key, 3600); this.logger.debug(`Broadcast ${messageType} to all agents in task ${this.taskId}`); } catch (error) { throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to broadcast message`, this.executionMode, true); } } /** * Wait for agent completion * * Blocking wait for one or more agents to complete with timeout. * Used by orchestrator to synchronize on agent completion. */ async waitForCompletion(agentIds, timeoutMs = 600000 // 10 minutes default ) { const results = new Map(); const startTime = Date.now(); if (!this.canUseRedis) { // Task Mode: Poll in-memory registry while(Date.now() - startTime < timeoutMs){ let allComplete = true; for (const agentId of agentIds){ const agent = this.taskModeRegistry.get(agentId); const status = agent?.status ?? 'unknown'; if (status === 'complete' || status === 'failed') { results.set(agentId, status); } else { allComplete = false; } } if (allComplete && results.size === agentIds.length) { return results; } // Wait 100ms before checking again await new Promise((resolve)=>setTimeout(resolve, 100)); } // Timeout: Return whatever we have for (const agentId of agentIds){ if (!results.has(agentId)) { const agent = this.taskModeRegistry.get(agentId); results.set(agentId, agent?.status ?? 'timeout'); } } return results; } // CLI Mode: Wait on Redis with blocking operations const channel = `swarm:${this.taskId}:completion`; try { while(Date.now() - startTime < timeoutMs){ for (const agentId of agentIds){ if (results.has(agentId)) continue; const key = `swarm:${this.taskId}:agents:${agentId}`; const statusStr = await this.redis.hget(key, 'status'); const status = statusStr ?? 'unknown'; if (status === 'complete' || status === 'failed' || status === 'timeout') { results.set(agentId, status); } } if (results.size === agentIds.length) { return results; } // Wait 100ms before checking again await new Promise((resolve)=>setTimeout(resolve, 100)); } // Timeout: Mark remaining as timeout for (const agentId of agentIds){ if (!results.has(agentId)) { results.set(agentId, 'timeout'); } } return results; } catch (error) { throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to wait for agent completion`, this.executionMode, true); } } /** * Get agent status * * Returns current status for a single agent. */ async getAgentStatus(agentId) { if (!this.canUseRedis) { // Task Mode: Check in-memory registry const agent = this.taskModeRegistry.get(agentId); return agent?.status ?? 'unknown'; } // CLI Mode: Get from Redis const key = `swarm:${this.taskId}:agents:${agentId}`; try { const status = await this.redis.hget(key, 'status'); return status ?? 'unknown'; } catch (error) { this.logger.error(`Failed to get agent ${agentId} status`, error); return 'unknown'; } } /** * Get agent metadata * * Returns full metadata for a registered agent. */ async getAgentMetadata(agentId) { if (!this.canUseRedis) { // Task Mode: Return from in-memory registry return this.taskModeRegistry.get(agentId) ?? null; } // CLI Mode: Get from Redis const key = `swarm:${this.taskId}:agents:${agentId}`; try { const data = await this.redis.hgetall(key); if (!data || Object.keys(data).length === 0) { return null; } return { agentId, type: data.type, taskId: this.taskId, status: data.status || 'unknown', iteration: parseInt(data.iteration || '1', 10), createdAt: data.createdAt, lastHeartbeat: data.lastHeartbeat, pid: data.pid ? parseInt(data.pid, 10) : undefined, metadata: data.metadata ? JSON.parse(data.metadata) : undefined }; } catch (error) { this.logger.error(`Failed to get agent ${agentId} metadata`, error); return null; } } /** * Get all agents for a task * * Returns metadata for all registered agents. */ async getAllAgents() { if (!this.canUseRedis) { // Task Mode: Return from in-memory registry return Array.from(this.taskModeRegistry.values()); } // CLI Mode: Scan Redis for all agents const agents = []; const pattern = `swarm:${this.taskId}:agents:*`; try { let cursor = '0'; do { const [newCursor, keys] = await this.redis.scan(cursor, 'MATCH', pattern); cursor = newCursor; for (const key of keys){ const agentIdMatch = key.match(/agents:(.+)$/); if (agentIdMatch) { const agentId = agentIdMatch[1]; const metadata = await this.getAgentMetadata(agentId); if (metadata) { agents.push(metadata); } } } }while (cursor !== '0') return agents; } catch (error) { this.logger.error('Failed to get all agents', error); return []; } } /** * Health check for agents * * Returns stale agents that haven't updated status recently. */ async getStaleAgents(staleThresholdMs = 600000) { const agents = await this.getAllAgents(); const now = Date.now(); const stale = []; for (const agent of agents){ const lastHeartbeat = new Date(agent.lastHeartbeat).getTime(); if (now - lastHeartbeat > staleThresholdMs) { stale.push(agent); } } return stale; } /** * Clean up task coordination data * * Removes all Redis keys associated with a task. * Should only be called after task completion. */ async cleanupTask() { if (!this.canUseRedis) { // Task Mode: Clear in-memory structures this.taskModeRegistry.clear(); this.taskModeMessages.clear(); this.logger.info(`Task Mode: Cleaned up in-memory coordination data`); return; } // CLI Mode: Remove Redis keys const pattern = `swarm:${this.taskId}:*`; try { let cursor = '0'; const batch = 100; do { const [newCursor, keys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', batch.toString()); cursor = newCursor; if (keys.length > 0) { await this.redis.del(...keys); } }while (cursor !== '0') this.logger.info(`Cleaned up coordination data for task ${this.taskId}`); } catch (error) { this.logger.error('Failed to cleanup task coordination data', error); } } } //# sourceMappingURL=coordinate.js.map