Files
wifi-densepose/npm/packages/agentic-integration/coordination-protocol.js
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

546 lines
20 KiB
JavaScript

"use strict";
/**
* Coordination Protocol - Inter-agent communication and consensus
*
* Handles:
* - Inter-agent messaging
* - Consensus for critical operations
* - Event-driven coordination
* - Pub/Sub integration
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CoordinationProtocol = void 0;
const events_1 = require("events");
const child_process_1 = require("child_process");
const util_1 = require("util");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class CoordinationProtocol extends events_1.EventEmitter {
constructor(config) {
super();
this.config = config;
this.messageQueue = [];
this.sentMessages = new Map();
this.pendingResponses = new Map();
this.consensusProposals = new Map();
this.pubSubTopics = new Map();
this.knownNodes = new Set();
this.lastHeartbeat = new Map();
this.messageCounter = 0;
this.initialize();
}
/**
* Initialize coordination protocol
*/
async initialize() {
console.log(`[CoordinationProtocol:${this.config.nodeId}] Initializing protocol...`);
// Initialize pub/sub topics
for (const topicName of this.config.pubSubTopics) {
this.createTopic(topicName);
}
// Start heartbeat
this.startHeartbeat();
// Start message processing
this.startMessageProcessing();
if (this.config.enableClaudeFlowHooks) {
try {
await execAsync(`npx claude-flow@alpha hooks pre-task --description "Initialize coordination protocol for node ${this.config.nodeId}"`);
}
catch (error) {
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Claude-flow hooks not available`);
}
}
this.emit('protocol:initialized');
console.log(`[CoordinationProtocol:${this.config.nodeId}] Protocol initialized`);
}
/**
* Send message to another node
*/
async sendMessage(to, type, payload, options = {}) {
const message = {
id: `msg-${this.config.nodeId}-${this.messageCounter++}`,
type,
from: this.config.nodeId,
to,
topic: options.topic,
payload,
timestamp: Date.now(),
ttl: options.ttl || this.config.messageTimeout,
priority: options.priority || 0,
};
console.log(`[CoordinationProtocol:${this.config.nodeId}] Sending ${type} message ${message.id} to ${to}`);
// Add to queue
this.enqueueMessage(message);
// Track sent message
this.sentMessages.set(message.id, message);
// If expecting response, create promise
if (options.expectResponse) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingResponses.delete(message.id);
reject(new Error(`Message ${message.id} timed out`));
}, message.ttl);
this.pendingResponses.set(message.id, {
resolve,
reject,
timeout,
});
});
}
this.emit('message:sent', message);
}
/**
* Broadcast message to all nodes
*/
async broadcastMessage(type, payload, options = {}) {
const recipients = Array.from(this.knownNodes);
console.log(`[CoordinationProtocol:${this.config.nodeId}] Broadcasting ${type} message to ${recipients.length} nodes`);
for (const recipient of recipients) {
await this.sendMessage(recipient, type, payload, {
...options,
expectResponse: false,
});
}
this.emit('message:broadcast', { type, recipientCount: recipients.length });
}
/**
* Receive and handle message
*/
async receiveMessage(message) {
// Check if message is expired
if (Date.now() - message.timestamp > message.ttl) {
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Received expired message ${message.id}`);
return;
}
console.log(`[CoordinationProtocol:${this.config.nodeId}] Received ${message.type} message ${message.id} from ${message.from}`);
// Handle different message types
switch (message.type) {
case 'request':
await this.handleRequest(message);
break;
case 'response':
await this.handleResponse(message);
break;
case 'broadcast':
await this.handleBroadcast(message);
break;
case 'consensus':
await this.handleConsensusMessage(message);
break;
default:
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Unknown message type: ${message.type}`);
}
// Update last contact time
this.lastHeartbeat.set(message.from, Date.now());
this.knownNodes.add(message.from);
this.emit('message:received', message);
}
/**
* Handle request message
*/
async handleRequest(message) {
this.emit('request:received', message);
// Application can handle request and send response
// Example auto-response for health checks
if (message.payload.type === 'health_check') {
await this.sendResponse(message.id, message.from, {
status: 'healthy',
timestamp: Date.now(),
});
}
}
/**
* Send response to a request
*/
async sendResponse(requestId, to, payload) {
const response = {
id: `resp-${requestId}`,
type: 'response',
from: this.config.nodeId,
to,
payload: {
requestId,
...payload,
},
timestamp: Date.now(),
ttl: this.config.messageTimeout,
priority: 1,
};
await this.sendMessage(to, 'response', response.payload);
}
/**
* Handle response message
*/
async handleResponse(message) {
const requestId = message.payload.requestId;
const pending = this.pendingResponses.get(requestId);
if (pending) {
clearTimeout(pending.timeout);
pending.resolve(message.payload);
this.pendingResponses.delete(requestId);
}
this.emit('response:received', message);
}
/**
* Handle broadcast message
*/
async handleBroadcast(message) {
// If message has topic, deliver to topic subscribers
if (message.topic) {
const topic = this.pubSubTopics.get(message.topic);
if (topic) {
this.deliverToTopic(message, topic);
}
}
this.emit('broadcast:received', message);
}
/**
* Propose consensus for critical operation
*/
async proposeConsensus(type, data, requiredVotes = Math.floor(this.knownNodes.size / 2) + 1) {
const proposal = {
id: `consensus-${this.config.nodeId}-${Date.now()}`,
proposer: this.config.nodeId,
type,
data,
requiredVotes,
deadline: Date.now() + this.config.consensusTimeout,
votes: new Map([[this.config.nodeId, true]]), // Proposer votes yes
status: 'pending',
};
this.consensusProposals.set(proposal.id, proposal);
console.log(`[CoordinationProtocol:${this.config.nodeId}] Proposing consensus ${proposal.id} (type: ${type})`);
// Broadcast consensus proposal
await this.broadcastMessage('consensus', {
action: 'propose',
proposal: {
id: proposal.id,
proposer: proposal.proposer,
type: proposal.type,
data: proposal.data,
requiredVotes: proposal.requiredVotes,
deadline: proposal.deadline,
},
});
// Wait for consensus
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
const currentProposal = this.consensusProposals.get(proposal.id);
if (!currentProposal) {
clearInterval(checkInterval);
resolve(false);
return;
}
if (currentProposal.status === 'accepted') {
clearInterval(checkInterval);
resolve(true);
}
else if (currentProposal.status === 'rejected' ||
currentProposal.status === 'expired') {
clearInterval(checkInterval);
resolve(false);
}
else if (Date.now() > currentProposal.deadline) {
currentProposal.status = 'expired';
clearInterval(checkInterval);
resolve(false);
}
}, 100);
});
}
/**
* Handle consensus message
*/
async handleConsensusMessage(message) {
const { action, proposal, vote } = message.payload;
switch (action) {
case 'propose':
// New proposal received
await this.handleConsensusProposal(proposal, message.from);
break;
case 'vote':
// Vote received for proposal
await this.handleConsensusVote(vote.proposalId, message.from, vote.approve);
break;
default:
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Unknown consensus action: ${action}`);
}
}
/**
* Handle consensus proposal
*/
async handleConsensusProposal(proposalData, from) {
console.log(`[CoordinationProtocol:${this.config.nodeId}] Received consensus proposal ${proposalData.id} from ${from}`);
// Store proposal
const proposal = {
...proposalData,
votes: new Map([[proposalData.proposer, true]]),
status: 'pending',
};
this.consensusProposals.set(proposal.id, proposal);
// Emit event for application to decide
this.emit('consensus:proposed', proposal);
// Auto-approve for demo (in production, application decides)
const approve = true;
// Send vote
await this.sendMessage(proposal.proposer, 'consensus', {
action: 'vote',
vote: {
proposalId: proposal.id,
approve,
voter: this.config.nodeId,
},
});
}
/**
* Handle consensus vote
*/
async handleConsensusVote(proposalId, voter, approve) {
const proposal = this.consensusProposals.get(proposalId);
if (!proposal || proposal.status !== 'pending') {
return;
}
console.log(`[CoordinationProtocol:${this.config.nodeId}] Received ${approve ? 'approval' : 'rejection'} vote from ${voter} for proposal ${proposalId}`);
// Record vote
proposal.votes.set(voter, approve);
// Count votes
const approvals = Array.from(proposal.votes.values()).filter(v => v).length;
const rejections = proposal.votes.size - approvals;
// Check if consensus reached
if (approvals >= proposal.requiredVotes) {
proposal.status = 'accepted';
console.log(`[CoordinationProtocol:${this.config.nodeId}] Consensus ${proposalId} accepted (${approvals}/${proposal.requiredVotes} votes)`);
this.emit('consensus:accepted', proposal);
}
else if (rejections > this.knownNodes.size - proposal.requiredVotes) {
proposal.status = 'rejected';
console.log(`[CoordinationProtocol:${this.config.nodeId}] Consensus ${proposalId} rejected (${rejections} rejections)`);
this.emit('consensus:rejected', proposal);
}
}
/**
* Create pub/sub topic
*/
createTopic(name, maxHistorySize = 100) {
if (this.pubSubTopics.has(name)) {
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Topic ${name} already exists`);
return;
}
const topic = {
name,
subscribers: new Set(),
messageHistory: [],
maxHistorySize,
};
this.pubSubTopics.set(name, topic);
console.log(`[CoordinationProtocol:${this.config.nodeId}] Created topic: ${name}`);
}
/**
* Subscribe to pub/sub topic
*/
subscribe(topicName, subscriberId) {
const topic = this.pubSubTopics.get(topicName);
if (!topic) {
throw new Error(`Topic ${topicName} does not exist`);
}
topic.subscribers.add(subscriberId);
console.log(`[CoordinationProtocol:${this.config.nodeId}] Node ${subscriberId} subscribed to topic ${topicName}`);
this.emit('topic:subscribed', { topicName, subscriberId });
}
/**
* Unsubscribe from pub/sub topic
*/
unsubscribe(topicName, subscriberId) {
const topic = this.pubSubTopics.get(topicName);
if (!topic) {
return;
}
topic.subscribers.delete(subscriberId);
console.log(`[CoordinationProtocol:${this.config.nodeId}] Node ${subscriberId} unsubscribed from topic ${topicName}`);
this.emit('topic:unsubscribed', { topicName, subscriberId });
}
/**
* Publish message to topic
*/
async publishToTopic(topicName, payload) {
const topic = this.pubSubTopics.get(topicName);
if (!topic) {
throw new Error(`Topic ${topicName} does not exist`);
}
console.log(`[CoordinationProtocol:${this.config.nodeId}] Publishing to topic ${topicName} (${topic.subscribers.size} subscribers)`);
// Broadcast to all subscribers
for (const subscriber of topic.subscribers) {
await this.sendMessage(subscriber, 'broadcast', payload, {
topic: topicName,
});
}
// Store in message history
const message = {
id: `topic-${topicName}-${Date.now()}`,
type: 'broadcast',
from: this.config.nodeId,
topic: topicName,
payload,
timestamp: Date.now(),
ttl: this.config.messageTimeout,
priority: 0,
};
topic.messageHistory.push(message);
// Trim history if needed
if (topic.messageHistory.length > topic.maxHistorySize) {
topic.messageHistory.shift();
}
this.emit('topic:published', { topicName, message });
}
/**
* Deliver message to topic subscribers
*/
deliverToTopic(message, topic) {
// Store in history
topic.messageHistory.push(message);
if (topic.messageHistory.length > topic.maxHistorySize) {
topic.messageHistory.shift();
}
// Emit to local subscribers
this.emit('topic:message', {
topicName: topic.name,
message,
});
}
/**
* Enqueue message for processing
*/
enqueueMessage(message) {
if (this.messageQueue.length >= this.config.maxMessageQueueSize) {
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Message queue full, dropping lowest priority message`);
// Remove lowest priority message
this.messageQueue.sort((a, b) => b.priority - a.priority);
this.messageQueue.pop();
}
// Insert message by priority
let insertIndex = this.messageQueue.findIndex(m => m.priority < message.priority);
if (insertIndex === -1) {
this.messageQueue.push(message);
}
else {
this.messageQueue.splice(insertIndex, 0, message);
}
}
/**
* Start message processing loop
*/
startMessageProcessing() {
this.messageProcessingTimer = setInterval(() => {
this.processMessages();
}, 10); // Process every 10ms
}
/**
* Process queued messages
*/
async processMessages() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
// Check if message expired
if (Date.now() - message.timestamp > message.ttl) {
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Message ${message.id} expired before processing`);
continue;
}
// Simulate message transmission (replace with actual network call)
this.emit('message:transmit', message);
}
}
/**
* Start heartbeat mechanism
*/
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
this.checkNodeHealth();
}, this.config.heartbeatInterval);
}
/**
* Send heartbeat to all known nodes
*/
async sendHeartbeat() {
await this.broadcastMessage('request', {
type: 'heartbeat',
nodeId: this.config.nodeId,
timestamp: Date.now(),
});
}
/**
* Check health of known nodes
*/
checkNodeHealth() {
const now = Date.now();
const unhealthyThreshold = this.config.heartbeatInterval * 3;
for (const [nodeId, lastSeen] of this.lastHeartbeat.entries()) {
if (now - lastSeen > unhealthyThreshold) {
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Node ${nodeId} appears unhealthy (last seen ${Math.floor((now - lastSeen) / 1000)}s ago)`);
this.emit('node:unhealthy', { nodeId, lastSeen });
}
}
}
/**
* Register a node in the network
*/
registerNode(nodeId) {
this.knownNodes.add(nodeId);
this.lastHeartbeat.set(nodeId, Date.now());
console.log(`[CoordinationProtocol:${this.config.nodeId}] Registered node: ${nodeId}`);
this.emit('node:registered', { nodeId });
}
/**
* Unregister a node from the network
*/
unregisterNode(nodeId) {
this.knownNodes.delete(nodeId);
this.lastHeartbeat.delete(nodeId);
console.log(`[CoordinationProtocol:${this.config.nodeId}] Unregistered node: ${nodeId}`);
this.emit('node:unregistered', { nodeId });
}
/**
* Get protocol status
*/
getStatus() {
return {
nodeId: this.config.nodeId,
knownNodes: this.knownNodes.size,
queuedMessages: this.messageQueue.length,
pendingResponses: this.pendingResponses.size,
activeConsensus: Array.from(this.consensusProposals.values()).filter(p => p.status === 'pending').length,
topics: Array.from(this.pubSubTopics.keys()),
};
}
/**
* Shutdown protocol gracefully
*/
async shutdown() {
console.log(`[CoordinationProtocol:${this.config.nodeId}] Shutting down protocol...`);
// Stop timers
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
if (this.messageProcessingTimer) {
clearInterval(this.messageProcessingTimer);
}
// Process remaining messages
await this.processMessages();
// Clear pending responses
for (const [messageId, pending] of this.pendingResponses.entries()) {
clearTimeout(pending.timeout);
pending.reject(new Error('Protocol shutting down'));
}
this.pendingResponses.clear();
if (this.config.enableClaudeFlowHooks) {
try {
await execAsync(`npx claude-flow@alpha hooks post-task --task-id "protocol-${this.config.nodeId}-shutdown"`);
}
catch (error) {
console.warn(`[CoordinationProtocol:${this.config.nodeId}] Error executing shutdown hooks`);
}
}
this.emit('protocol:shutdown');
}
}
exports.CoordinationProtocol = CoordinationProtocol;
//# sourceMappingURL=coordination-protocol.js.map