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.
369 lines (368 loc) • 14.4 kB
JavaScript
/**
* Coordination Layer for Agent Communication and Tracking
*
* Provides agent registration, status tracking, message passing, and
* coordination protocol enforcement with Redis as the backing store.
*
* Supports both Task Mode (Redis stubbed) and CLI Mode (full coordination).
*
* Key Features:
* - Agent registration with metadata tracking
* - Status updates and health checks
* - Message passing between agents (pub/sub)
* - Broadcast messages with protocol enforcement
* - Wait for completion with timeout support
* - Type-safe interfaces using branded types
*/ import { CoordinationError, CoordinationErrorType } from './types-export';
/**
* Coordination Layer Implementation
*
* Manages agent lifecycle, status tracking, and inter-agent communication.
* Gracefully handles Task Mode (no Redis) and CLI Mode (full coordination).
*/ export class CoordinationLayer {
redis;
logger;
canUseRedis;
executionMode;
taskId;
// In-memory registry for Task Mode
taskModeRegistry = new Map();
taskModeMessages = new Map();
constructor(config){
this.redis = config.redis;
this.logger = config.logger;
this.canUseRedis = config.canUseRedis;
this.executionMode = config.executionMode;
this.taskId = config.taskId;
}
/**
* Register an agent with the coordination layer
*
* Creates tracking record for agent lifecycle.
*/ async registerAgent(agentId, type, metadata, iteration = 1, pid) {
const now = new Date().toISOString();
const agentMetadata = {
agentId,
type,
taskId: this.taskId,
status: 'registered',
iteration,
createdAt: now,
lastHeartbeat: now,
pid,
metadata
};
if (!this.canUseRedis) {
// Task Mode: Store in memory
this.taskModeRegistry.set(agentId, agentMetadata);
this.logger.info(`Task Mode: Registered agent ${agentId} (type: ${type})`);
return;
}
// CLI Mode: Store in Redis
const key = `swarm:${this.taskId}:agents:${agentId}`;
try {
await this.redis.hset(key, 'type', type, 'status', 'registered', 'iteration', iteration.toString(), 'createdAt', now, 'lastHeartbeat', now, ...pid ? [
'pid',
pid.toString()
] : [], ...metadata ? [
'metadata',
JSON.stringify(metadata)
] : []);
// Set 24h TTL
await this.redis.expire(key, 86400);
this.logger.debug(`Registered agent ${agentId} in Redis`);
} catch (error) {
throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to register agent ${agentId}`, this.executionMode, true);
}
}
/**
* Update agent status
*
* Tracks status changes during agent lifecycle.
*/ async updateAgentStatus(agentId, status) {
const now = new Date().toISOString();
if (!this.canUseRedis) {
// Task Mode: Update in memory
const agent = this.taskModeRegistry.get(agentId);
if (agent) {
agent.status = status;
agent.lastHeartbeat = now;
this.taskModeRegistry.set(agentId, agent);
}
this.logger.debug(`Task Mode: Updated ${agentId} status to ${status}`);
return;
}
// CLI Mode: Update in Redis
const key = `swarm:${this.taskId}:agents:${agentId}`;
try {
await this.redis.hset(key, 'status', status, 'lastHeartbeat', now);
this.logger.debug(`Updated agent ${agentId} status to ${status}`);
} catch (error) {
throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to update agent ${agentId} status`, this.executionMode, true);
}
}
/**
* Send direct message between agents
*
* Delivers message from one agent to another.
*/ async sendMessage(from, to, messageType, payload, correlationId) {
const message = {
from,
to,
type: messageType,
payload,
timestamp: new Date().toISOString(),
correlationId
};
if (!this.canUseRedis) {
// Task Mode: Store in memory
const key = `${from}:${to}`;
if (!this.taskModeMessages.has(key)) {
this.taskModeMessages.set(key, []);
}
this.taskModeMessages.get(key).push(message);
this.logger.debug(`Task Mode: Message from ${from} to ${to}`);
return;
}
// CLI Mode: Store in Redis
const key = `swarm:${this.taskId}:messages:${from}:${to}`;
try {
await this.redis.lpush(key, JSON.stringify(message));
await this.redis.expire(key, 3600); // 1h TTL for messages
this.logger.debug(`Message sent from ${from} to ${to}`);
} catch (error) {
throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to send message from ${from} to ${to}`, this.executionMode, true);
}
}
/**
* Broadcast message to all agents in task
*
* Delivers message to every agent participating in the task.
* Used for coordination signals (gate passed, iteration started, etc).
*/ async broadcastMessage(from, messageType, payload, correlationId) {
const message = {
from,
taskId: this.taskId,
type: messageType,
payload,
timestamp: new Date().toISOString(),
correlationId
};
if (!this.canUseRedis) {
// Task Mode: Log broadcast
this.logger.info(`Task Mode: Broadcast ${messageType} from ${from} (payload: ${JSON.stringify(payload)})`);
return;
}
// CLI Mode: Store in Redis as sorted set for ordering
const key = `swarm:${this.taskId}:broadcasts`;
try {
const timestamp = Date.now();
await this.redis.zadd(key, timestamp, JSON.stringify(message));
// Set 1h TTL
await this.redis.expire(key, 3600);
this.logger.debug(`Broadcast ${messageType} to all agents in task ${this.taskId}`);
} catch (error) {
throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to broadcast message`, this.executionMode, true);
}
}
/**
* Wait for agent completion
*
* Blocking wait for one or more agents to complete with timeout.
* Used by orchestrator to synchronize on agent completion.
*/ async waitForCompletion(agentIds, timeoutMs = 600000 // 10 minutes default
) {
const results = new Map();
const startTime = Date.now();
if (!this.canUseRedis) {
// Task Mode: Poll in-memory registry
while(Date.now() - startTime < timeoutMs){
let allComplete = true;
for (const agentId of agentIds){
const agent = this.taskModeRegistry.get(agentId);
const status = agent?.status ?? 'unknown';
if (status === 'complete' || status === 'failed') {
results.set(agentId, status);
} else {
allComplete = false;
}
}
if (allComplete && results.size === agentIds.length) {
return results;
}
// Wait 100ms before checking again
await new Promise((resolve)=>setTimeout(resolve, 100));
}
// Timeout: Return whatever we have
for (const agentId of agentIds){
if (!results.has(agentId)) {
const agent = this.taskModeRegistry.get(agentId);
results.set(agentId, agent?.status ?? 'timeout');
}
}
return results;
}
// CLI Mode: Wait on Redis with blocking operations
const channel = `swarm:${this.taskId}:completion`;
try {
while(Date.now() - startTime < timeoutMs){
for (const agentId of agentIds){
if (results.has(agentId)) continue;
const key = `swarm:${this.taskId}:agents:${agentId}`;
const statusStr = await this.redis.hget(key, 'status');
const status = statusStr ?? 'unknown';
if (status === 'complete' || status === 'failed' || status === 'timeout') {
results.set(agentId, status);
}
}
if (results.size === agentIds.length) {
return results;
}
// Wait 100ms before checking again
await new Promise((resolve)=>setTimeout(resolve, 100));
}
// Timeout: Mark remaining as timeout
for (const agentId of agentIds){
if (!results.has(agentId)) {
results.set(agentId, 'timeout');
}
}
return results;
} catch (error) {
throw new CoordinationError(CoordinationErrorType.REDIS_ERROR, `Failed to wait for agent completion`, this.executionMode, true);
}
}
/**
* Get agent status
*
* Returns current status for a single agent.
*/ async getAgentStatus(agentId) {
if (!this.canUseRedis) {
// Task Mode: Check in-memory registry
const agent = this.taskModeRegistry.get(agentId);
return agent?.status ?? 'unknown';
}
// CLI Mode: Get from Redis
const key = `swarm:${this.taskId}:agents:${agentId}`;
try {
const status = await this.redis.hget(key, 'status');
return status ?? 'unknown';
} catch (error) {
this.logger.error(`Failed to get agent ${agentId} status`, error);
return 'unknown';
}
}
/**
* Get agent metadata
*
* Returns full metadata for a registered agent.
*/ async getAgentMetadata(agentId) {
if (!this.canUseRedis) {
// Task Mode: Return from in-memory registry
return this.taskModeRegistry.get(agentId) ?? null;
}
// CLI Mode: Get from Redis
const key = `swarm:${this.taskId}:agents:${agentId}`;
try {
const data = await this.redis.hgetall(key);
if (!data || Object.keys(data).length === 0) {
return null;
}
return {
agentId,
type: data.type,
taskId: this.taskId,
status: data.status || 'unknown',
iteration: parseInt(data.iteration || '1', 10),
createdAt: data.createdAt,
lastHeartbeat: data.lastHeartbeat,
pid: data.pid ? parseInt(data.pid, 10) : undefined,
metadata: data.metadata ? JSON.parse(data.metadata) : undefined
};
} catch (error) {
this.logger.error(`Failed to get agent ${agentId} metadata`, error);
return null;
}
}
/**
* Get all agents for a task
*
* Returns metadata for all registered agents.
*/ async getAllAgents() {
if (!this.canUseRedis) {
// Task Mode: Return from in-memory registry
return Array.from(this.taskModeRegistry.values());
}
// CLI Mode: Scan Redis for all agents
const agents = [];
const pattern = `swarm:${this.taskId}:agents:*`;
try {
let cursor = '0';
do {
const [newCursor, keys] = await this.redis.scan(cursor, 'MATCH', pattern);
cursor = newCursor;
for (const key of keys){
const agentIdMatch = key.match(/agents:(.+)$/);
if (agentIdMatch) {
const agentId = agentIdMatch[1];
const metadata = await this.getAgentMetadata(agentId);
if (metadata) {
agents.push(metadata);
}
}
}
}while (cursor !== '0')
return agents;
} catch (error) {
this.logger.error('Failed to get all agents', error);
return [];
}
}
/**
* Health check for agents
*
* Returns stale agents that haven't updated status recently.
*/ async getStaleAgents(staleThresholdMs = 600000) {
const agents = await this.getAllAgents();
const now = Date.now();
const stale = [];
for (const agent of agents){
const lastHeartbeat = new Date(agent.lastHeartbeat).getTime();
if (now - lastHeartbeat > staleThresholdMs) {
stale.push(agent);
}
}
return stale;
}
/**
* Clean up task coordination data
*
* Removes all Redis keys associated with a task.
* Should only be called after task completion.
*/ async cleanupTask() {
if (!this.canUseRedis) {
// Task Mode: Clear in-memory structures
this.taskModeRegistry.clear();
this.taskModeMessages.clear();
this.logger.info(`Task Mode: Cleaned up in-memory coordination data`);
return;
}
// CLI Mode: Remove Redis keys
const pattern = `swarm:${this.taskId}:*`;
try {
let cursor = '0';
const batch = 100;
do {
const [newCursor, keys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', batch.toString());
cursor = newCursor;
if (keys.length > 0) {
await this.redis.del(...keys);
}
}while (cursor !== '0')
this.logger.info(`Cleaned up coordination data for task ${this.taskId}`);
} catch (error) {
this.logger.error('Failed to cleanup task coordination data', error);
}
}
}
//# sourceMappingURL=coordinate.js.map