claude-flow
Version:
Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)
621 lines (516 loc) • 17.9 kB
JavaScript
/**
* Hive Mind Consensus Engine
*
* Implements multiple consensus algorithms for distributed decision making:
* - Majority voting (simple and weighted)
* - Byzantine fault tolerance
* - Dynamic threshold adjustment
* - Quorum management
*/
import { EventEmitter } from 'node:events';
import { generateId } from '../utils/helpers.js';
export class ConsensusEngine extends EventEmitter {
constructor(config = {}) {
super();
this.config = {
defaultThreshold: 0.6,
byzantineTolerance: 0.33, // Max 33% Byzantine actors
quorumSize: 0.75, // Minimum 75% participation
votingTimeout: 30000, // 30 seconds
maxRetries: 3,
weightDecay: 0.95, // Weight decay for agents with failures
...config
};
this.proposals = new Map();
this.agents = new Map();
this.votingHistory = new Map();
this.metrics = {
totalProposals: 0,
successfulConsensus: 0,
failedConsensus: 0,
byzantineDetected: 0,
avgVotingTime: 0
};
}
/**
* Register agent with initial reputation weight
*/
registerAgent(agentId, initialWeight = 1.0, capabilities = []) {
this.agents.set(agentId, {
id: agentId,
weight: initialWeight,
reputation: 1.0,
capabilities,
votescast: 0,
correctVotes: 0,
byzantineFlags: 0,
lastActivity: Date.now(),
isOnline: true
});
this.emit('agent:registered', { agentId, weight: initialWeight });
}
/**
* Create a new consensus proposal
*/
async createProposal(data) {
const proposalId = generateId('proposal');
const proposal = {
id: proposalId,
type: data.type || 'general',
content: data.content,
threshold: data.threshold || this.config.defaultThreshold,
algorithm: data.algorithm || 'weighted_majority',
creator: data.creator,
requiredCapabilities: data.requiredCapabilities || [],
metadata: data.metadata || {},
// Voting state
votes: new Map(),
eligibleAgents: new Set(),
startTime: Date.now(),
deadline: Date.now() + (data.timeout || this.config.votingTimeout),
status: 'active',
// Results
result: null,
finalRatio: 0,
participationRate: 0,
consensus: false,
// Byzantine detection
suspiciousVotes: new Set(),
consistencyChecks: new Map()
};
// Determine eligible agents based on capabilities
this.determineEligibleAgents(proposal);
this.proposals.set(proposalId, proposal);
this.metrics.totalProposals++;
this.emit('proposal:created', proposal);
// Set timeout for proposal
setTimeout(() => this.finalizeProposal(proposalId), proposal.deadline - proposal.startTime);
return proposalId;
}
/**
* Determine which agents are eligible to vote
*/
determineEligibleAgents(proposal) {
for (const [agentId, agent] of this.agents) {
if (!agent.isOnline) continue;
// Check capabilities if required
if (proposal.requiredCapabilities.length > 0) {
const hasRequiredCapability = proposal.requiredCapabilities.some(cap =>
agent.capabilities.includes(cap)
);
if (!hasRequiredCapability) continue;
}
// Exclude agents with too many Byzantine flags
if (agent.byzantineFlags > 3) continue;
proposal.eligibleAgents.add(agentId);
}
console.log(`Proposal ${proposal.id}: ${proposal.eligibleAgents.size} eligible agents`);
}
/**
* Submit a vote for a proposal
*/
async submitVote(proposalId, agentId, vote, reasoning = '') {
const proposal = this.proposals.get(proposalId);
if (!proposal) {
throw new Error(`Proposal ${proposalId} not found`);
}
if (proposal.status !== 'active') {
throw new Error(`Proposal ${proposalId} is no longer active`);
}
if (!proposal.eligibleAgents.has(agentId)) {
throw new Error(`Agent ${agentId} is not eligible to vote on this proposal`);
}
if (Date.now() > proposal.deadline) {
throw new Error(`Voting deadline has passed for proposal ${proposalId}`);
}
const agent = this.agents.get(agentId);
if (!agent) {
throw new Error(`Agent ${agentId} not registered`);
}
// Record the vote
const voteRecord = {
agentId,
vote: Boolean(vote),
weight: agent.weight,
reasoning,
timestamp: Date.now(),
confidence: this.calculateVoteConfidence(agent, proposal)
};
proposal.votes.set(agentId, voteRecord);
agent.votescast++;
agent.lastActivity = Date.now();
this.emit('vote:submitted', { proposalId, agentId, vote, reasoning });
// Perform Byzantine detection
this.detectByzantineBehavior(proposal, voteRecord);
// Check if we can finalize early
if (this.canFinalizeEarly(proposal)) {
return this.finalizeProposal(proposalId);
}
return { status: 'recorded', proposal: proposal.id };
}
/**
* Calculate vote confidence based on agent history
*/
calculateVoteConfidence(agent, proposal) {
const reputationFactor = agent.reputation;
const experienceFactor = Math.min(agent.votescast / 10, 1.0);
const consistencyFactor = agent.votescast > 0 ? agent.correctVotes / agent.votescast : 0.5;
const recencyFactor = Math.max(0.1, 1.0 - (Date.now() - agent.lastActivity) / (24 * 60 * 60 * 1000));
return (reputationFactor + experienceFactor + consistencyFactor + recencyFactor) / 4;
}
/**
* Detect Byzantine behavior patterns
*/
detectByzantineBehavior(proposal, voteRecord) {
const { agentId, vote, confidence } = voteRecord;
const agent = this.agents.get(agentId);
// Pattern 1: Vote flipping (changing votes frequently)
const recentVotes = Array.from(this.votingHistory.values())
.filter(v => v.agentId === agentId && Date.now() - v.timestamp < 3600000) // Last hour
.slice(-5);
if (recentVotes.length >= 3) {
const voteChanges = recentVotes.reduce((changes, v, i) =>
i > 0 && v.vote !== recentVotes[i-1].vote ? changes + 1 : changes, 0);
if (voteChanges >= 2) {
this.flagByzantineAgent(agentId, 'vote_flipping', proposal.id);
}
}
// Pattern 2: Extremely low confidence with definitive votes
if (confidence < 0.3 && Math.abs(vote ? 1 : 0) === 1) {
this.flagByzantineAgent(agentId, 'confidence_mismatch', proposal.id);
}
// Pattern 3: Consistent minority voting (contrarian behavior)
const agentHistory = Array.from(this.votingHistory.values())
.filter(v => v.agentId === agentId)
.slice(-10);
if (agentHistory.length >= 5) {
const minorityVotes = agentHistory.filter(v => {
const proposalResult = this.proposals.get(v.proposalId);
return proposalResult && proposalResult.consensus !== v.vote;
}).length;
if (minorityVotes / agentHistory.length > 0.8) {
this.flagByzantineAgent(agentId, 'contrarian_pattern', proposal.id);
}
}
// Store vote in history
this.votingHistory.set(`${proposal.id}:${agentId}`, {
proposalId: proposal.id,
agentId,
vote,
timestamp: Date.now()
});
}
/**
* Flag an agent for Byzantine behavior
*/
flagByzantineAgent(agentId, reason, proposalId) {
const agent = this.agents.get(agentId);
if (!agent) return;
agent.byzantineFlags++;
agent.weight *= this.config.weightDecay;
this.metrics.byzantineDetected++;
console.warn(`Byzantine behavior detected: Agent ${agentId}, Reason: ${reason}, Proposal: ${proposalId}`);
this.emit('byzantine:detected', { agentId, reason, proposalId, newWeight: agent.weight });
// Quarantine agent if too many flags
if (agent.byzantineFlags >= 5) {
agent.isOnline = false;
this.emit('agent:quarantined', { agentId, flags: agent.byzantineFlags });
}
}
/**
* Check if proposal can be finalized early
*/
canFinalizeEarly(proposal) {
const totalEligible = proposal.eligibleAgents.size;
const votesReceived = proposal.votes.size;
// Early finalization conditions:
// 1. Unanimous agreement
// 2. Impossible to change outcome
// 3. Sufficient participation with clear majority
if (votesReceived < Math.ceil(totalEligible * this.config.quorumSize)) {
return false;
}
const { positiveVotes, negativeVotes } = this.calculateVotes(proposal);
const totalWeightedVotes = positiveVotes + negativeVotes;
const ratio = totalWeightedVotes > 0 ? positiveVotes / totalWeightedVotes : 0;
// Unanimous or overwhelming majority
if (ratio >= 0.95 || ratio <= 0.05) {
return true;
}
// Mathematically impossible to change outcome
const remainingVotes = totalEligible - votesReceived;
const maxPossibleChange = remainingVotes * Math.max(...Array.from(this.agents.values()).map(a => a.weight));
if (ratio > proposal.threshold && positiveVotes - negativeVotes > maxPossibleChange) {
return true;
}
if (ratio < proposal.threshold && negativeVotes - positiveVotes > maxPossibleChange) {
return true;
}
return false;
}
/**
* Calculate weighted votes
*/
calculateVotes(proposal) {
let positiveVotes = 0;
let negativeVotes = 0;
let totalWeight = 0;
for (const [agentId, vote] of proposal.votes) {
const agent = this.agents.get(agentId);
if (!agent) continue;
const voteWeight = agent.weight;
totalWeight += voteWeight;
if (vote.vote) {
positiveVotes += voteWeight;
} else {
negativeVotes += voteWeight;
}
}
return { positiveVotes, negativeVotes, totalWeight };
}
/**
* Apply different consensus algorithms
*/
applyConsensusAlgorithm(proposal) {
switch (proposal.algorithm) {
case 'simple_majority':
return this.simpleMajorityConsensus(proposal);
case 'weighted_majority':
return this.weightedMajorityConsensus(proposal);
case 'byzantine_tolerant':
return this.byzantineTolerantConsensus(proposal);
case 'unanimous':
return this.unanimousConsensus(proposal);
default:
return this.weightedMajorityConsensus(proposal);
}
}
/**
* Simple majority consensus (one vote per agent)
*/
simpleMajorityConsensus(proposal) {
const votes = Array.from(proposal.votes.values());
const positiveVotes = votes.filter(v => v.vote).length;
const negativeVotes = votes.filter(v => !v.vote).length;
const totalVotes = votes.length;
const ratio = totalVotes > 0 ? positiveVotes / totalVotes : 0;
const consensus = ratio >= proposal.threshold;
return {
consensus,
ratio,
positiveVotes,
negativeVotes,
totalVotes,
algorithm: 'simple_majority'
};
}
/**
* Weighted majority consensus
*/
weightedMajorityConsensus(proposal) {
const { positiveVotes, negativeVotes, totalWeight } = this.calculateVotes(proposal);
const ratio = totalWeight > 0 ? positiveVotes / totalWeight : 0;
const consensus = ratio >= proposal.threshold;
return {
consensus,
ratio,
positiveVotes,
negativeVotes,
totalWeight,
algorithm: 'weighted_majority'
};
}
/**
* Byzantine fault tolerant consensus
*/
byzantineTolerantConsensus(proposal) {
const votes = Array.from(proposal.votes.values());
const trustedVotes = votes.filter(v => {
const agent = this.agents.get(v.agentId);
return agent && agent.byzantineFlags === 0 && agent.reputation > 0.7;
});
if (trustedVotes.length === 0) {
return this.weightedMajorityConsensus(proposal);
}
const positiveVotes = trustedVotes.filter(v => v.vote).length;
const totalTrusted = trustedVotes.length;
const ratio = positiveVotes / totalTrusted;
// Require higher threshold for Byzantine tolerance
const byzantineThreshold = Math.max(proposal.threshold, 0.67);
const consensus = ratio >= byzantineThreshold;
return {
consensus,
ratio,
positiveVotes,
negativeVotes: totalTrusted - positiveVotes,
totalVotes: totalTrusted,
algorithm: 'byzantine_tolerant',
trustedVotesOnly: true
};
}
/**
* Unanimous consensus
*/
unanimousConsensus(proposal) {
const votes = Array.from(proposal.votes.values());
const allAgree = votes.every(v => v.vote) || votes.every(v => !v.vote);
const consensus = allAgree && votes.length > 0;
return {
consensus,
ratio: consensus ? 1.0 : 0.0,
positiveVotes: votes.filter(v => v.vote).length,
negativeVotes: votes.filter(v => !v.vote).length,
totalVotes: votes.length,
algorithm: 'unanimous'
};
}
/**
* Finalize a proposal
*/
async finalizeProposal(proposalId) {
const proposal = this.proposals.get(proposalId);
if (!proposal || proposal.status !== 'active') {
return null;
}
proposal.status = 'finalized';
proposal.endTime = Date.now();
// Check quorum
const participationRate = proposal.votes.size / proposal.eligibleAgents.size;
proposal.participationRate = participationRate;
if (participationRate < this.config.quorumSize) {
proposal.result = this.handleInsufficientQuorum(proposal);
} else {
proposal.result = this.applyConsensusAlgorithm(proposal);
}
proposal.consensus = proposal.result.consensus;
proposal.finalRatio = proposal.result.ratio;
// Update metrics
if (proposal.consensus) {
this.metrics.successfulConsensus++;
} else {
this.metrics.failedConsensus++;
}
const votingTime = proposal.endTime - proposal.startTime;
this.metrics.avgVotingTime =
(this.metrics.avgVotingTime * (this.metrics.totalProposals - 1) + votingTime) /
this.metrics.totalProposals;
// Update agent reputations
this.updateAgentReputations(proposal);
this.emit('proposal:finalized', proposal);
return proposal;
}
/**
* Handle insufficient quorum
*/
handleInsufficientQuorum(proposal) {
return {
consensus: false,
ratio: 0,
positiveVotes: 0,
negativeVotes: 0,
totalVotes: proposal.votes.size,
algorithm: 'quorum_failed',
error: `Insufficient quorum: ${proposal.participationRate.toFixed(2)} < ${this.config.quorumSize}`
};
}
/**
* Update agent reputations based on consensus accuracy
*/
updateAgentReputations(proposal) {
if (!proposal.consensus) return;
const majorityVote = proposal.finalRatio >= 0.5;
for (const [agentId, vote] of proposal.votes) {
const agent = this.agents.get(agentId);
if (!agent) continue;
const votedWithMajority = vote.vote === majorityVote;
if (votedWithMajority) {
agent.correctVotes++;
agent.reputation = Math.min(2.0, agent.reputation * 1.05);
agent.weight = Math.min(2.0, agent.weight * 1.02);
} else {
agent.reputation *= 0.98;
agent.weight *= 0.99;
}
}
}
/**
* Get consensus metrics
*/
getMetrics() {
const activeProposals = Array.from(this.proposals.values()).filter(p => p.status === 'active');
const totalAgents = this.agents.size;
const onlineAgents = Array.from(this.agents.values()).filter(a => a.isOnline).length;
const byzantineAgents = Array.from(this.agents.values()).filter(a => a.byzantineFlags > 0).length;
return {
...this.metrics,
activeProposals: activeProposals.length,
totalAgents,
onlineAgents,
byzantineAgents,
successRate: this.metrics.totalProposals > 0 ?
this.metrics.successfulConsensus / this.metrics.totalProposals : 0,
avgParticipationRate: this.calculateAverageParticipation()
};
}
/**
* Calculate average participation rate
*/
calculateAverageParticipation() {
const finalizedProposals = Array.from(this.proposals.values())
.filter(p => p.status === 'finalized');
if (finalizedProposals.length === 0) return 0;
return finalizedProposals.reduce((sum, p) => sum + p.participationRate, 0) /
finalizedProposals.length;
}
/**
* Get proposal status
*/
getProposal(proposalId) {
return this.proposals.get(proposalId);
}
/**
* Get agent information
*/
getAgent(agentId) {
return this.agents.get(agentId);
}
/**
* List all proposals
*/
listProposals(filter = {}) {
let proposals = Array.from(this.proposals.values());
if (filter.status) {
proposals = proposals.filter(p => p.status === filter.status);
}
if (filter.type) {
proposals = proposals.filter(p => p.type === filter.type);
}
if (filter.creator) {
proposals = proposals.filter(p => p.creator === filter.creator);
}
return proposals.sort((a, b) => b.startTime - a.startTime);
}
/**
* Cleanup expired proposals and old history
*/
cleanup() {
const now = Date.now();
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
// Remove old proposals
for (const [id, proposal] of this.proposals) {
if (now - proposal.startTime > maxAge && proposal.status === 'finalized') {
this.proposals.delete(id);
}
}
// Remove old voting history
for (const [key, vote] of this.votingHistory) {
if (now - vote.timestamp > maxAge) {
this.votingHistory.delete(key);
}
}
this.emit('cleanup:completed', {
proposalsRemoved: this.proposals.size,
historyRemoved: this.votingHistory.size
});
}
}