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.

383 lines (382 loc) 12.9 kB
/** * Unified TypeScript Coordination Wrapper * * Provides a type-safe interface for all Redis-based coordination operations * in the CFN Loop critical path. Wraps the existing redis-coordinator with * semantic coordination methods for: * - Agent lifecycle management * - Signal/wait primitives * - Consensus collection * - Task state management * * Key patterns: * - swarm:* namespace for unified coordination * - Backward compatible with cfn_loop:task:* legacy namespace * - Type-safe agent state tracking * - Automatic timeout handling */ import Redis from 'ioredis'; import { EventEmitter } from 'events'; /** * Unified Coordination Wrapper * * Provides semantic coordination methods that abstract away Redis key patterns * and protocol details while maintaining type safety. */ export class CoordinationWrapper extends EventEmitter { redis; taskId; namespace; defaultTimeout; isConnected = false; constructor(config){ super(); this.taskId = config.taskId; this.namespace = config.namespace || 'swarm'; this.defaultTimeout = config.defaultTimeout || 120000; // 120 seconds // Create Redis client with provided configuration this.redis = new Redis({ host: config.redisHost, port: config.redisPort, db: config.redisDb || 0, lazyConnect: true, maxRetriesPerRequest: null, enableReadyCheck: false }); // Set up error handling this.redis.on('error', (error)=>{ this.emit('redis-error', error); }); this.redis.on('connect', ()=>{ this.isConnected = true; this.emit('connected'); }); this.redis.on('disconnect', ()=>{ this.isConnected = false; this.emit('disconnected'); }); } /** * Connect to Redis */ async connect() { if (!this.isConnected) { await this.redis.connect(); } } /** * Disconnect from Redis */ async disconnect() { if (this.isConnected) { await this.redis.disconnect(); } } /** * Check if connected to Redis */ isReady() { return this.isConnected; } /** * AGENT LIFECYCLE MANAGEMENT */ /** * Register a spawned agent in the coordination system */ async registerAgent(agentId, agentType) { const state = { agentId, type: agentType, status: 'spawned', timestamp: new Date().toISOString() }; const key = this.getAgentStateKey(agentId); await this.redis.set(key, JSON.stringify(state), 'EX', 86400); // 24h expiry } /** * Update agent status */ async updateAgentStatus(agentId, status) { const key = this.getAgentStateKey(agentId); const state = await this.getAgentState(agentId); if (!state) { throw new Error(`Agent ${agentId} not registered`); } state.status = status; state.timestamp = new Date().toISOString(); await this.redis.set(key, JSON.stringify(state), 'EX', 86400); } /** * Signal agent completion with confidence score */ async signalCompletion(agentId, confidence, options) { const key = this.getAgentStateKey(agentId); const state = await this.getAgentState(agentId); if (!state) { throw new Error(`Agent ${agentId} not registered`); } state.status = 'completed'; state.confidence = confidence; state.testPassRate = options?.testPassRate; state.testsRun = options?.testsRun; state.testsPassed = options?.testsPassed; state.result = options?.result; state.iteration = options?.iteration; state.timestamp = new Date().toISOString(); // Store in agent state with 24h expiry await this.redis.set(key, JSON.stringify(state), 'EX', 86400); // Also publish completion signal to waiting agents const channel = this.getCompletionChannel(agentId); await this.redis.publish(channel, JSON.stringify(state)); // Add to completion leaderboard for consensus collection const leaderboardKey = this.getCompletionLeaderboardKey(); await this.redis.zadd(leaderboardKey, confidence * 100, JSON.stringify({ agentId, confidence, timestamp: state.timestamp })); } /** * Get agent state */ async getAgentState(agentId) { const key = this.getAgentStateKey(agentId); const data = await this.redis.get(key); if (!data) { return null; } return JSON.parse(data); } /** * Get all agents in current task */ async getAllAgents() { const pattern = this.getAgentStateKeyPattern(); const keys = await this.redis.keys(pattern); const agents = []; for (const key of keys){ const data = await this.redis.get(key); if (data) { agents.push(JSON.parse(data)); } } return agents; } /** * COORDINATION PRIMITIVES */ /** * Wait for a coordination signal with timeout */ async waitForSignal(channel, timeoutMs) { const timeout = timeoutMs || this.defaultTimeout; const timeoutSeconds = Math.ceil(timeout / 1000); try { // Use BLPOP to block-wait on a list const listKey = this.getSignalKey(channel); const result = await this.redis.blpop(listKey, timeoutSeconds); if (result === null) { return { received: false, timeout: true }; } return { received: true, message: result[1], timestamp: new Date().toISOString(), timeout: false }; } catch (error) { return { received: false, timeout: false }; } } /** * Broadcast a signal to all waiting agents */ async broadcastSignal(channel, message) { // Use pub/sub for broadcast (fire-and-forget to subscribers) await this.redis.publish(channel, message); // Also store in a list for new subscribers (via BLPOP pattern) const listKey = this.getSignalKey(channel); await this.redis.lpush(listKey, message); // Keep only recent signals (last 100) await this.redis.ltrim(listKey, 0, 99); // Expire the list after 1 hour await this.redis.expire(listKey, 3600); } /** * Subscribe to signal channel (pub/sub pattern) */ subscribeToSignal(channel, callback) { // Create a separate subscriber for pub/sub const subscriber = this.redis.duplicate(); subscriber.on('message', (chan, message)=>{ if (chan === channel) { callback(message); } }); subscriber.subscribe(channel); // Return unsubscribe function return ()=>{ subscriber.unsubscribe(channel); subscriber.disconnect(); }; } /** * CONSENSUS & VALIDATION */ /** * Collect consensus scores from multiple validators */ async collectConsensus(agentIds, timeoutMs) { const timeout = timeoutMs || this.defaultTimeout; const startTime = Date.now(); const scores = []; for (const agentId of agentIds){ const elapsed = Date.now() - startTime; const remaining = Math.max(1, timeout - elapsed); const remainingSeconds = Math.ceil(remaining / 1000); // Wait for each validator to report consensus const scoreKey = this.getConsensusKey(agentId); const data = await this.redis.blpop(scoreKey, remainingSeconds); if (data) { const score = JSON.parse(data[1]); scores.push(score); } } return scores; } /** * Report a consensus score */ async reportConsensusScore(agentId, score, feedback) { const consensusScore = { agentId, score, feedback, timestamp: new Date().toISOString() }; const key = this.getConsensusKey(agentId); await this.redis.lpush(key, JSON.stringify(consensusScore)); await this.redis.expire(key, 86400); // 24h expiry } /** * Calculate average consensus from scores */ calculateAverageConsensus(scores) { if (scores.length === 0) { return 0; } const sum = scores.reduce((acc, score)=>acc + score.score, 0); return sum / scores.length; } /** * TASK STATE MANAGEMENT */ /** * Store task context (usually CFN Loop parameters) */ async storeTaskContext(context) { const key = this.getTaskContextKey(); await this.redis.set(key, JSON.stringify(context), 'EX', 86400); } /** * Load task context */ async loadTaskContext() { const key = this.getTaskContextKey(); const data = await this.redis.get(key); if (!data) { return null; } return JSON.parse(data); } /** * Update task status */ async updateTaskStatus(status, iteration) { const key = this.getTaskStatusKey(); const state = { taskId: this.taskId, status, updatedAt: new Date().toISOString() }; if (iteration !== undefined) { state.iteration = iteration; } // Get existing context if available const contextData = await this.loadTaskContext(); if (contextData) { state.context = contextData; } // Get all agents state.agents = await this.getAllAgents(); await this.redis.set(key, JSON.stringify(state), 'EX', 86400); } /** * Get full task state snapshot */ async getTaskState() { const key = this.getTaskStatusKey(); const data = await this.redis.get(key); if (!data) { return null; } return JSON.parse(data); } /** * HELPER METHODS FOR KEY GENERATION */ getAgentStateKey(agentId) { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:agent:${agentId}`; } return `${this.namespace}:${this.taskId}:agent:${agentId}`; } getAgentStateKeyPattern() { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:agent:*`; } return `${this.namespace}:${this.taskId}:agent:*`; } getCompletionChannel(agentId) { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:completion:${agentId}`; } return `${this.namespace}:${this.taskId}:completion`; } getCompletionLeaderboardKey() { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:completions`; } return `${this.namespace}:${this.taskId}:completions`; } getSignalKey(channel) { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:signal:${channel}`; } return `${this.namespace}:${this.taskId}:signal:${channel}`; } getConsensusKey(agentId) { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:consensus:${agentId}`; } return `${this.namespace}:${this.taskId}:consensus:${agentId}`; } getTaskContextKey() { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:context`; } return `${this.namespace}:${this.taskId}:context`; } getTaskStatusKey() { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:status`; } return `${this.namespace}:${this.taskId}:status`; } /** * CLEANUP & UTILITIES */ /** * Clear all coordination state for this task */ async clearTaskState() { const pattern = this.getTaskKeysPattern(); const keys = await this.redis.keys(pattern); if (keys.length > 0) { await this.redis.del(...keys); } } getTaskKeysPattern() { if (this.namespace === 'cfn_loop') { return `cfn_loop:task:${this.taskId}:*`; } return `${this.namespace}:${this.taskId}:*`; } /** * Get Redis client for advanced operations */ getRedisClient() { return this.redis; } } export default CoordinationWrapper; //# sourceMappingURL=coordination-wrapper.js.map