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
JavaScript
/**
* 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