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