UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

415 lines (405 loc) 15 kB
#!/usr/bin/env node /** * Agent Messaging - Bidirectional Redis Communication * * Enables Main Chat to send commands to running CLI agents, and agents to process them. * * Main Chat → Agent: LPUSH cfn:agent:{taskId}:{agentId}:commands * Agent → Main Chat: SET cfn:agent:{taskId}:{agentId}:status (response) * * Supported Commands: * - status: Request agent status update * - redirect: Redirect agent to new task/context * - abort: Request clean agent abort * - pause: Request agent pause for N seconds * * Usage (Main Chat - send command): * npx tsx src/cli/coordination/agent-messaging.ts send \ * --task-id <id> --agent-id <aid> --command status * * Usage (Agent - process commands): * npx tsx src/cli/coordination/agent-messaging.ts listen \ * --task-id <id> --agent-id <aid> --poll-interval 5 */ import { createClient } from 'redis'; // ============================================================================ // Redis Key Helpers // ============================================================================ function getCommandsKey(taskId, agentId) { return `cfn:agent:${taskId}:${agentId}:commands`; } function getStatusKey(taskId, agentId) { return `cfn:agent:${taskId}:${agentId}:status`; } function getResponseKey(taskId, agentId, commandId) { return `cfn:agent:${taskId}:${agentId}:response:${commandId}`; } // ============================================================================ // Main Chat Side - Send Commands // ============================================================================ /** * Send a command to an agent via Redis */ export async function sendCommand(config, command) { const { taskId, agentId, 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 client = createClient({ socket: { host: redisHost, port: redisPort }, password: redisPassword || undefined }); const commandId = `cmd-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; const fullCommand = { ...command, id: commandId, timestamp: new Date().toISOString(), replyTo: getResponseKey(taskId, agentId, commandId) }; try { await client.connect(); const commandsKey = getCommandsKey(taskId, agentId); await client.lPush(commandsKey, JSON.stringify(fullCommand)); console.log(`[agent-msg] Sent command ${command.type} to ${agentId}`); console.log(`[agent-msg] Commands key: ${commandsKey}`); console.log(`[agent-msg] Command ID: ${commandId}`); return { sent: true, commandId }; } finally{ await client.disconnect(); } } /** * Wait for agent response to a command */ export async function waitForResponse(config, commandId, timeoutSeconds = 30) { const { taskId, agentId, 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 client = createClient({ socket: { host: redisHost, port: redisPort }, password: redisPassword || undefined }); try { await client.connect(); const responseKey = getResponseKey(taskId, agentId, commandId); const result = await client.blPop(responseKey, timeoutSeconds); if (result) { return JSON.parse(result.element); } return null; } finally{ await client.disconnect(); } } /** * Get current agent status (non-blocking) */ export async function getAgentStatus(config) { const { taskId, agentId, 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 client = createClient({ socket: { host: redisHost, port: redisPort }, password: redisPassword || undefined }); try { await client.connect(); const statusKey = getStatusKey(taskId, agentId); const status = await client.get(statusKey); if (status) { return JSON.parse(status); } return null; } finally{ await client.disconnect(); } } /** * Agent command processor - polls for and processes commands */ export class AgentCommandProcessor { config; client = null; running = false; handlers = new Map(); constructor(config){ this.config = config; // Default handlers this.handlers.set('status', this.handleStatus.bind(this)); this.handlers.set('abort', this.handleAbort.bind(this)); this.handlers.set('pause', this.handlePause.bind(this)); } /** * Register a custom command handler */ onCommand(type, handler) { this.handlers.set(type, handler); } /** * Start processing commands (non-blocking poll loop) */ async start(pollIntervalSeconds = 5) { const { taskId, agentId, redisHost = process.env.CFN_REDIS_HOST || 'localhost', redisPort = parseInt(process.env.CFN_REDIS_PORT || '6379', 10), redisPassword = process.env.CFN_REDIS_PASSWORD || undefined } = this.config; this.client = createClient({ socket: { host: redisHost, port: redisPort }, password: redisPassword || undefined }); await this.client.connect(); this.running = true; const commandsKey = getCommandsKey(taskId, agentId); console.log(`[agent-processor] Started listening on ${commandsKey}`); // Non-blocking poll loop while(this.running){ try { // Short BLPOP to check for commands without blocking too long const result = await this.client.blPop(commandsKey, pollIntervalSeconds); if (result) { const command = JSON.parse(result.element); console.log(`[agent-processor] Received command: ${command.type} (${command.id})`); // Process command const handler = this.handlers.get(command.type); if (handler) { const response = await handler(command); // Send response if handler returned status if (response && command.replyTo) { await this.client.lPush(command.replyTo, JSON.stringify(response)); console.log(`[agent-processor] Sent response to ${command.replyTo}`); } } else { console.warn(`[agent-processor] No handler for command type: ${command.type}`); } } } catch (err) { if (this.running) { console.error(`[agent-processor] Error processing command:`, err); } } } } /** * Stop processing commands */ async stop() { this.running = false; if (this.client) { await this.client.disconnect(); this.client = null; } console.log(`[agent-processor] Stopped`); } /** * Update agent status in Redis */ async updateStatus(status) { if (!this.client) return; const { taskId, agentId } = this.config; const fullStatus = { agentId, taskId, status: 'running', timestamp: new Date().toISOString(), ...status }; const statusKey = getStatusKey(taskId, agentId); await this.client.set(statusKey, JSON.stringify(fullStatus)); // Expire after 1 hour await this.client.expire(statusKey, 3600); } // Default handlers async handleStatus(command) { const { taskId, agentId } = this.config; return { agentId, taskId, status: 'running', timestamp: new Date().toISOString(), metadata: { respondingTo: command.id } }; } async handleAbort(_command) { const { taskId, agentId } = this.config; console.log(`[agent-processor] Abort requested, shutting down...`); // Schedule stop after response setTimeout(()=>this.stop(), 100); return { agentId, taskId, status: 'aborting', timestamp: new Date().toISOString() }; } async handlePause(command) { const { taskId, agentId } = this.config; const pauseSeconds = command.payload?.seconds || 10; console.log(`[agent-processor] Pausing for ${pauseSeconds}s...`); await new Promise((resolve)=>setTimeout(resolve, pauseSeconds * 1000)); console.log(`[agent-processor] Resumed after pause`); return { agentId, taskId, status: 'running', timestamp: new Date().toISOString(), metadata: { pausedFor: pauseSeconds } }; } } // ============================================================================ // CLI Entry Point // ============================================================================ function printHelp() { console.log(` Agent Messaging - Bidirectional Redis Communication USAGE: # Send command to agent (Main Chat side) npx tsx src/cli/coordination/agent-messaging.ts send \\ --task-id <id> --agent-id <aid> --command <type> [--payload <json>] # Listen for commands (Agent side) npx tsx src/cli/coordination/agent-messaging.ts listen \\ --task-id <id> --agent-id <aid> [--poll-interval <seconds>] # Get agent status (Main Chat side) npx tsx src/cli/coordination/agent-messaging.ts status \\ --task-id <id> --agent-id <aid> COMMANDS: status Request agent status update redirect Redirect agent to new task (include --payload) abort Request clean agent abort pause Pause agent (include --payload '{"seconds": 10}') custom Custom command (include --payload) EXAMPLES: # Request status from agent npx tsx src/cli/coordination/agent-messaging.ts send \\ --task-id cfn-cli-123 --agent-id backend-dev-456 --command status # Abort agent npx tsx src/cli/coordination/agent-messaging.ts send \\ --task-id cfn-cli-123 --agent-id backend-dev-456 --command abort # Pause agent for 30 seconds npx tsx src/cli/coordination/agent-messaging.ts send \\ --task-id cfn-cli-123 --agent-id backend-dev-456 \\ --command pause --payload '{"seconds": 30}' # Redirect agent to new context npx tsx src/cli/coordination/agent-messaging.ts send \\ --task-id cfn-cli-123 --agent-id backend-dev-456 \\ --command redirect --payload '{"newTask": "Focus on security tests"}' # Start listening for commands (agent side) npx tsx src/cli/coordination/agent-messaging.ts listen \\ --task-id cfn-cli-123 --agent-id backend-dev-456 --poll-interval 5 `); } async function main() { const args = process.argv.slice(2); const action = args[0]; if (!action || action === '--help' || action === '-h') { printHelp(); process.exit(0); } // Parse common args let taskId; let agentId; let command; let payload; let pollInterval = 5; for(let i = 1; i < args.length; i++){ const arg = args[i]; const value = args[i + 1]; switch(arg){ case '--task-id': case '-t': taskId = value; i++; break; case '--agent-id': case '-a': agentId = value; i++; break; case '--command': case '-c': command = value; i++; break; case '--payload': case '-p': try { payload = JSON.parse(value); } catch { console.error('Invalid JSON payload'); process.exit(1); } i++; break; case '--poll-interval': pollInterval = parseInt(value, 10); i++; break; } } if (!taskId || !agentId) { console.error('Error: --task-id and --agent-id are required'); process.exit(1); } const config = { taskId, agentId }; switch(action){ case 'send': { if (!command) { console.error('Error: --command is required for send action'); process.exit(1); } const result = await sendCommand(config, { type: command, payload }); console.log(JSON.stringify(result, null, 2)); // Wait for response console.log('\n[agent-msg] Waiting for response (30s timeout)...'); const response = await waitForResponse(config, result.commandId, 30); if (response) { console.log('[agent-msg] Response received:'); console.log(JSON.stringify(response, null, 2)); } else { console.log('[agent-msg] No response received (timeout)'); } break; } case 'listen': { const processor = new AgentCommandProcessor(config); // Handle shutdown signals process.on('SIGINT', async ()=>{ console.log('\n[agent-msg] Received SIGINT, stopping...'); await processor.stop(); process.exit(0); }); await processor.start(pollInterval); break; } case 'status': { const status = await getAgentStatus(config); if (status) { console.log(JSON.stringify(status, null, 2)); } else { console.log('No status available for agent'); } break; } default: console.error(`Unknown action: ${action}`); printHelp(); process.exit(1); } } // Run if called directly if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, '/') || '')) { main().catch((err)=>{ console.error('[agent-msg] Fatal error:', err); process.exit(2); }); } //# sourceMappingURL=agent-messaging.js.map