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.
383 lines (382 loc) • 12.9 kB
JavaScript
/**
* Unified TypeScript Coordination Wrapper
*
* Provides a type-safe interface for all Redis-based coordination operations
* in the CFN Loop critical path. Wraps the existing redis-coordinator with
* semantic coordination methods for:
* - Agent lifecycle management
* - Signal/wait primitives
* - Consensus collection
* - Task state management
*
* Key patterns:
* - swarm:* namespace for unified coordination
* - Backward compatible with cfn_loop:task:* legacy namespace
* - Type-safe agent state tracking
* - Automatic timeout handling
*/ import Redis from 'ioredis';
import { EventEmitter } from 'events';
/**
* Unified Coordination Wrapper
*
* Provides semantic coordination methods that abstract away Redis key patterns
* and protocol details while maintaining type safety.
*/ export class CoordinationWrapper extends EventEmitter {
redis;
taskId;
namespace;
defaultTimeout;
isConnected = false;
constructor(config){
super();
this.taskId = config.taskId;
this.namespace = config.namespace || 'swarm';
this.defaultTimeout = config.defaultTimeout || 120000; // 120 seconds
// Create Redis client with provided configuration
this.redis = new Redis({
host: config.redisHost,
port: config.redisPort,
db: config.redisDb || 0,
lazyConnect: true,
maxRetriesPerRequest: null,
enableReadyCheck: false
});
// Set up error handling
this.redis.on('error', (error)=>{
this.emit('redis-error', error);
});
this.redis.on('connect', ()=>{
this.isConnected = true;
this.emit('connected');
});
this.redis.on('disconnect', ()=>{
this.isConnected = false;
this.emit('disconnected');
});
}
/**
* Connect to Redis
*/ async connect() {
if (!this.isConnected) {
await this.redis.connect();
}
}
/**
* Disconnect from Redis
*/ async disconnect() {
if (this.isConnected) {
await this.redis.disconnect();
}
}
/**
* Check if connected to Redis
*/ isReady() {
return this.isConnected;
}
/**
* AGENT LIFECYCLE MANAGEMENT
*/ /**
* Register a spawned agent in the coordination system
*/ async registerAgent(agentId, agentType) {
const state = {
agentId,
type: agentType,
status: 'spawned',
timestamp: new Date().toISOString()
};
const key = this.getAgentStateKey(agentId);
await this.redis.set(key, JSON.stringify(state), 'EX', 86400); // 24h expiry
}
/**
* Update agent status
*/ async updateAgentStatus(agentId, status) {
const key = this.getAgentStateKey(agentId);
const state = await this.getAgentState(agentId);
if (!state) {
throw new Error(`Agent ${agentId} not registered`);
}
state.status = status;
state.timestamp = new Date().toISOString();
await this.redis.set(key, JSON.stringify(state), 'EX', 86400);
}
/**
* Signal agent completion with confidence score
*/ async signalCompletion(agentId, confidence, options) {
const key = this.getAgentStateKey(agentId);
const state = await this.getAgentState(agentId);
if (!state) {
throw new Error(`Agent ${agentId} not registered`);
}
state.status = 'completed';
state.confidence = confidence;
state.testPassRate = options?.testPassRate;
state.testsRun = options?.testsRun;
state.testsPassed = options?.testsPassed;
state.result = options?.result;
state.iteration = options?.iteration;
state.timestamp = new Date().toISOString();
// Store in agent state with 24h expiry
await this.redis.set(key, JSON.stringify(state), 'EX', 86400);
// Also publish completion signal to waiting agents
const channel = this.getCompletionChannel(agentId);
await this.redis.publish(channel, JSON.stringify(state));
// Add to completion leaderboard for consensus collection
const leaderboardKey = this.getCompletionLeaderboardKey();
await this.redis.zadd(leaderboardKey, confidence * 100, JSON.stringify({
agentId,
confidence,
timestamp: state.timestamp
}));
}
/**
* Get agent state
*/ async getAgentState(agentId) {
const key = this.getAgentStateKey(agentId);
const data = await this.redis.get(key);
if (!data) {
return null;
}
return JSON.parse(data);
}
/**
* Get all agents in current task
*/ async getAllAgents() {
const pattern = this.getAgentStateKeyPattern();
const keys = await this.redis.keys(pattern);
const agents = [];
for (const key of keys){
const data = await this.redis.get(key);
if (data) {
agents.push(JSON.parse(data));
}
}
return agents;
}
/**
* COORDINATION PRIMITIVES
*/ /**
* Wait for a coordination signal with timeout
*/ async waitForSignal(channel, timeoutMs) {
const timeout = timeoutMs || this.defaultTimeout;
const timeoutSeconds = Math.ceil(timeout / 1000);
try {
// Use BLPOP to block-wait on a list
const listKey = this.getSignalKey(channel);
const result = await this.redis.blpop(listKey, timeoutSeconds);
if (result === null) {
return {
received: false,
timeout: true
};
}
return {
received: true,
message: result[1],
timestamp: new Date().toISOString(),
timeout: false
};
} catch (error) {
return {
received: false,
timeout: false
};
}
}
/**
* Broadcast a signal to all waiting agents
*/ async broadcastSignal(channel, message) {
// Use pub/sub for broadcast (fire-and-forget to subscribers)
await this.redis.publish(channel, message);
// Also store in a list for new subscribers (via BLPOP pattern)
const listKey = this.getSignalKey(channel);
await this.redis.lpush(listKey, message);
// Keep only recent signals (last 100)
await this.redis.ltrim(listKey, 0, 99);
// Expire the list after 1 hour
await this.redis.expire(listKey, 3600);
}
/**
* Subscribe to signal channel (pub/sub pattern)
*/ subscribeToSignal(channel, callback) {
// Create a separate subscriber for pub/sub
const subscriber = this.redis.duplicate();
subscriber.on('message', (chan, message)=>{
if (chan === channel) {
callback(message);
}
});
subscriber.subscribe(channel);
// Return unsubscribe function
return ()=>{
subscriber.unsubscribe(channel);
subscriber.disconnect();
};
}
/**
* CONSENSUS & VALIDATION
*/ /**
* Collect consensus scores from multiple validators
*/ async collectConsensus(agentIds, timeoutMs) {
const timeout = timeoutMs || this.defaultTimeout;
const startTime = Date.now();
const scores = [];
for (const agentId of agentIds){
const elapsed = Date.now() - startTime;
const remaining = Math.max(1, timeout - elapsed);
const remainingSeconds = Math.ceil(remaining / 1000);
// Wait for each validator to report consensus
const scoreKey = this.getConsensusKey(agentId);
const data = await this.redis.blpop(scoreKey, remainingSeconds);
if (data) {
const score = JSON.parse(data[1]);
scores.push(score);
}
}
return scores;
}
/**
* Report a consensus score
*/ async reportConsensusScore(agentId, score, feedback) {
const consensusScore = {
agentId,
score,
feedback,
timestamp: new Date().toISOString()
};
const key = this.getConsensusKey(agentId);
await this.redis.lpush(key, JSON.stringify(consensusScore));
await this.redis.expire(key, 86400); // 24h expiry
}
/**
* Calculate average consensus from scores
*/ calculateAverageConsensus(scores) {
if (scores.length === 0) {
return 0;
}
const sum = scores.reduce((acc, score)=>acc + score.score, 0);
return sum / scores.length;
}
/**
* TASK STATE MANAGEMENT
*/ /**
* Store task context (usually CFN Loop parameters)
*/ async storeTaskContext(context) {
const key = this.getTaskContextKey();
await this.redis.set(key, JSON.stringify(context), 'EX', 86400);
}
/**
* Load task context
*/ async loadTaskContext() {
const key = this.getTaskContextKey();
const data = await this.redis.get(key);
if (!data) {
return null;
}
return JSON.parse(data);
}
/**
* Update task status
*/ async updateTaskStatus(status, iteration) {
const key = this.getTaskStatusKey();
const state = {
taskId: this.taskId,
status,
updatedAt: new Date().toISOString()
};
if (iteration !== undefined) {
state.iteration = iteration;
}
// Get existing context if available
const contextData = await this.loadTaskContext();
if (contextData) {
state.context = contextData;
}
// Get all agents
state.agents = await this.getAllAgents();
await this.redis.set(key, JSON.stringify(state), 'EX', 86400);
}
/**
* Get full task state snapshot
*/ async getTaskState() {
const key = this.getTaskStatusKey();
const data = await this.redis.get(key);
if (!data) {
return null;
}
return JSON.parse(data);
}
/**
* HELPER METHODS FOR KEY GENERATION
*/ getAgentStateKey(agentId) {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:agent:${agentId}`;
}
return `${this.namespace}:${this.taskId}:agent:${agentId}`;
}
getAgentStateKeyPattern() {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:agent:*`;
}
return `${this.namespace}:${this.taskId}:agent:*`;
}
getCompletionChannel(agentId) {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:completion:${agentId}`;
}
return `${this.namespace}:${this.taskId}:completion`;
}
getCompletionLeaderboardKey() {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:completions`;
}
return `${this.namespace}:${this.taskId}:completions`;
}
getSignalKey(channel) {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:signal:${channel}`;
}
return `${this.namespace}:${this.taskId}:signal:${channel}`;
}
getConsensusKey(agentId) {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:consensus:${agentId}`;
}
return `${this.namespace}:${this.taskId}:consensus:${agentId}`;
}
getTaskContextKey() {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:context`;
}
return `${this.namespace}:${this.taskId}:context`;
}
getTaskStatusKey() {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:status`;
}
return `${this.namespace}:${this.taskId}:status`;
}
/**
* CLEANUP & UTILITIES
*/ /**
* Clear all coordination state for this task
*/ async clearTaskState() {
const pattern = this.getTaskKeysPattern();
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
getTaskKeysPattern() {
if (this.namespace === 'cfn_loop') {
return `cfn_loop:task:${this.taskId}:*`;
}
return `${this.namespace}:${this.taskId}:*`;
}
/**
* Get Redis client for advanced operations
*/ getRedisClient() {
return this.redis;
}
}
export default CoordinationWrapper;
//# sourceMappingURL=coordination-wrapper.js.map