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.

232 lines (226 loc) 8.38 kB
#!/usr/bin/env node /** * Wait for Threshold Completion - Parallel Agent Coordination * * Waits for N/M agents to complete (e.g., 3/4 = 75% threshold) before continuing. * Enables parallel agent spawning with graceful degradation on partial completion. * * Usage: * npx tsx src/cli/coordination/wait-for-threshold.ts \ * --task-id <id> \ * --total-agents <n> \ * --threshold <0.0-1.0> \ * --timeout <seconds> * * Example: * # Wait for 3/4 agents (75%) with 120s timeout * npx tsx src/cli/coordination/wait-for-threshold.ts \ * --task-id cfn-cli-12345 \ * --total-agents 4 \ * --threshold 0.75 \ * --timeout 120 */ import { createClient } from 'redis'; /** * Wait for threshold completion of agents * * Uses Redis BLPOP with short timeouts to poll for completion signals * while tracking progress toward the threshold. */ export async function waitForThreshold(config) { const { taskId, totalAgents, threshold, timeoutSeconds, redisHost = process.env.CFN_REDIS_HOST || 'localhost', redisPort = parseInt(process.env.CFN_REDIS_PORT || '6379', 10), redisPassword = process.env.CFN_REDIS_PASSWORD || undefined } = config; const requiredCount = Math.ceil(totalAgents * threshold); const signalKey = `cfn-completion:${taskId}`; const completed = []; const startTime = Date.now(); const timeoutMs = timeoutSeconds * 1000; // Connect to Redis const client = createClient({ socket: { host: redisHost, port: redisPort }, password: redisPassword || undefined }); try { await client.connect(); console.log(`[wait-threshold] Connected to Redis at ${redisHost}:${redisPort}`); console.log(`[wait-threshold] Waiting for ${requiredCount}/${totalAgents} agents (${(threshold * 100).toFixed(0)}% threshold)`); console.log(`[wait-threshold] Signal key: ${signalKey}`); console.log(`[wait-threshold] Timeout: ${timeoutSeconds}s`); // Poll loop with short BLPOP timeouts const pollIntervalSeconds = 5; // Check every 5 seconds while(completed.length < requiredCount){ const elapsed = Date.now() - startTime; // Check overall timeout if (elapsed >= timeoutMs) { console.log(`[wait-threshold] Timeout reached after ${(elapsed / 1000).toFixed(1)}s`); break; } // Calculate remaining time for this poll const remainingMs = timeoutMs - elapsed; const pollTimeout = Math.min(pollIntervalSeconds, Math.ceil(remainingMs / 1000)); try { // BLPOP with short timeout - returns null on timeout const result = await client.blPop(signalKey, pollTimeout); if (result) { try { const signal = JSON.parse(result.element); completed.push(signal); console.log(`[wait-threshold] Received signal ${completed.length}/${requiredCount}: ${signal.agentId} (${signal.status})`); // Check if threshold met if (completed.length >= requiredCount) { console.log(`[wait-threshold] Threshold met! ${completed.length}/${totalAgents} agents completed`); break; } } catch (parseErr) { console.warn(`[wait-threshold] Failed to parse signal: ${result.element}`); } } else { // Timeout on BLPOP - no signal received, continue polling const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`[wait-threshold] Polling... ${completed.length}/${requiredCount} completed (${elapsedSec}s elapsed)`); } } catch (blpopErr) { // Redis error during BLPOP console.error(`[wait-threshold] BLPOP error:`, blpopErr); break; } } const elapsedMs = Date.now() - startTime; const thresholdMet = completed.length >= requiredCount; return { success: thresholdMet, completed, timedOut: !thresholdMet && Date.now() - startTime >= timeoutMs, thresholdMet, completedCount: completed.length, requiredCount, totalAgents, elapsedMs }; } finally{ await client.disconnect(); } } /** * Parse CLI arguments */ function parseArgs(args) { const config = { threshold: 0.75, timeoutSeconds: 120 }; for(let i = 0; i < args.length; i++){ const arg = args[i]; const value = args[i + 1]; switch(arg){ case '--task-id': case '-t': config.taskId = value; i++; break; case '--total-agents': case '-n': config.totalAgents = parseInt(value, 10); i++; break; case '--threshold': config.threshold = parseFloat(value); i++; break; case '--timeout': config.timeoutSeconds = parseInt(value, 10); i++; break; case '--redis-host': config.redisHost = value; i++; break; case '--redis-port': config.redisPort = parseInt(value, 10); i++; break; case '--help': case '-h': printHelp(); process.exit(0); } } // Validate required fields if (!config.taskId) { console.error('Error: --task-id is required'); return null; } if (!config.totalAgents || config.totalAgents < 1) { console.error('Error: --total-agents must be a positive integer'); return null; } if (config.threshold < 0 || config.threshold > 1) { console.error('Error: --threshold must be between 0.0 and 1.0'); return null; } return config; } function printHelp() { console.log(` Wait for Threshold Completion - Parallel Agent Coordination USAGE: npx tsx src/cli/coordination/wait-for-threshold.ts [OPTIONS] OPTIONS: -t, --task-id <id> Task ID for coordination (required) -n, --total-agents <n> Total number of agents spawned (required) --threshold <0.0-1.0> Completion threshold (default: 0.75 = 75%) --timeout <seconds> Overall timeout (default: 120) --redis-host <host> Redis host (default: localhost) --redis-port <port> Redis port (default: 6379) -h, --help Show this help message EXAMPLES: # Wait for 3/4 agents (75%) with 120s timeout npx tsx src/cli/coordination/wait-for-threshold.ts \\ --task-id cfn-cli-12345 \\ --total-agents 4 \\ --threshold 0.75 \\ --timeout 120 # Wait for all agents (100%) with 300s timeout npx tsx src/cli/coordination/wait-for-threshold.ts \\ --task-id cfn-cli-12345 \\ --total-agents 4 \\ --threshold 1.0 \\ --timeout 300 OUTPUT: JSON result with completion status: { "success": true, "thresholdMet": true, "completedCount": 3, "requiredCount": 3, "totalAgents": 4, "elapsedMs": 45000, "completed": [...] } `); } /** * CLI entry point */ async function main() { const config = parseArgs(process.argv.slice(2)); if (!config) { console.error('Use --help for usage information'); process.exit(1); } try { const result = await waitForThreshold(config); // Output result as JSON for scripting console.log('\n[wait-threshold] Result:'); console.log(JSON.stringify(result, null, 2)); // Exit with appropriate code process.exit(result.success ? 0 : 1); } catch (error) { console.error('[wait-threshold] Fatal error:', error); process.exit(2); } } // Run if called directly if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, '/') || '')) { main(); } export { parseArgs }; //# sourceMappingURL=wait-for-threshold.js.map