Skip to content

Consensus ​

The consensus engine evaluates proposals and votes after each round using one of 5 pluggable strategies. Dissent is always preserved in the result -- the losing side is never discarded. This page covers each strategy's algorithm in detail, the debate runner, configuration, and how to write custom strategies.

Configuration ​

typescript
interface ConsensusConfig {
  /** Strategy name. Default: 'confidence-weighted' */
  strategy?: 'confidence-weighted' | 'voting' | 'hierarchical' | 'bayesian' | 'entropy'
  /** 0..1, how strong consensus must be. Default: 0.7 */
  threshold?: number
  /** Per-round timeout in ms. Default: 30,000 */
  timeoutMs?: number
  /** Minimum unique voters required before evaluation. Default: 2 */
  minVoters?: number
  /** Max rounds for structured debate. Default: 3 */
  maxDebateRounds?: number
  /** What to do when conflicting proposals are detected. Default: 'debate' */
  conflictResolution?: 'debate' | 'escalate' | 'majority'
}
OptionDefaultDescription
strategy'confidence-weighted'Which voting algorithm to use.
threshold0.7Minimum confidence/ratio to declare consensus.
timeoutMs30,000Per-round timeout.
minVoters2canEvaluate() returns false until this many unique agents have voted.
maxDebateRounds3How many structured debate rounds before giving up.
conflictResolution'debate'How to handle detected proposal conflicts.

ConsensusEngine API ​

typescript
class ConsensusEngine {
  constructor(
    config?: ConsensusConfig,
    events?: TypedEventEmitter<SwarmEventMap>,
    customStrategies?: readonly ConsensusStrategy[],
  )

  /** Evaluate proposals and votes. Returns ConsensusResult. */
  evaluate(
    proposals: readonly Proposal[],
    votes: readonly VoteRecord[],
  ): ConsensusResult

  /** Check if enough votes have been collected to attempt evaluation. */
  canEvaluate(
    proposals: readonly Proposal[],
    votes: readonly VoteRecord[],
  ): boolean

  /** Get the active strategy ID. */
  get activeStrategy(): string

  /** Get available strategy IDs. */
  get availableStrategies(): readonly string[]
}

How the Orchestrator Uses It ​

The orchestrator calls canEvaluate() + evaluate() at the end of each round:

Round completes
  │
  ├── Extract proposals from signal history
  ├── Extract votes from signal history
  ├── Apply attention weights (surprise-based, clamped [0.8, 1.2])
  ├── Apply reputation weights (if advisor enabled)
  │
  ├── canEvaluate(proposals, votes)?
  │     └── Need >= minVoters unique voters + >= 1 proposal
  │
  └── evaluate(proposals, votes) → ConsensusResult
        ├── decided: true → STOP solve loop
        └── decided: false → continue (or trigger debate if 2+ proposals)

Strategies ​

Confidence Weighted (default) ​

typescript
consensus: { strategy: 'confidence-weighted', threshold: 0.7 }

Algorithm:

For each proposal:

  1. Collect all votes targeting this proposal
  2. Sum agreeWeight = Sigma(weight) for all stance === 'agree'
  3. Sum totalWeight = Sigma(weight) for all votes (agree + disagree + abstain)
  4. Compute weightedRatio = agreeWeight / totalWeight
  5. The proposal with the highest weightedRatio wins if it exceeds threshold
weightedRatio = Sum(weight_i, where stance_i = 'agree')
              / Sum(weight_i, for all votes)

Key property: agents with higher vote weight have more influence. An agent that votes agree with weight: 0.9 contributes more than one with weight: 0.3.

Important nuance: causal levels (correlation/intervention/counterfactual) do NOT affect vote weight in this strategy. The source code explicitly documents: "causal levels affect signal routing priority, NOT vote weight." This prevents counterfactual challenges from drowning out agreement.

Best for: General use. Naturally amplifies high-confidence signals and dampens uncertain ones.


Bayesian ​

typescript
consensus: { strategy: 'bayesian', threshold: 0.75 }

Algorithm:

Uses a BeliefNetwork from @cognitive-swarm/math to maintain posterior probabilities over proposals:

  1. Initialize uniform prior: P(proposal_i) = 1/N for all N proposals
  2. For each vote, convert to a likelihood ratio:
    typescript
    voteToLikelihoodRatio(stance, weight):
      agree  → 1 + weight        // evidence FOR
      disagree → 1 / (1 + weight) // evidence AGAINST
      abstain → 1.0               // uninformative
  3. Update posterior via Bayes' theorem:
    P(H|E) = P(E|H) * P(H) / P(E)
  4. The MAP (maximum a posteriori) estimate wins if its probability exceeds threshold

Key property: multiple weak votes can outweigh one strong vote. Abstentions are genuinely uninformative (LR=1). Confidence is a real posterior probability, not a vote ratio.

Best for: Tasks where agents have calibrated confidence scores. Works well with reputation-weighted voting. Integrates naturally with the Math Bridge's Bayesian analysis module.


Entropy-Based ​

typescript
consensus: { strategy: 'entropy', threshold: 0.7 }

Algorithm:

Uses information theory to measure agreement quality, not just majority size:

  1. For each proposal, sum weight of all agree votes to get support_i
  2. Compute Shannon entropy: H = -Sum(p_i * log2(p_i)) where p_i = support_i / total_support
  3. Compute max entropy: H_max = log2(N) for N proposals
  4. Confidence = 1 - H/H_max
  5. Consensus reached when confidence >= threshold
H = -Sum(p_i * log2(p_i))    where p_i = support_i / total_support
confidence = 1 - H / H_max

Key property: 60% agreement across 2 proposals (H low, confidence high) is very different from 60% across 10 proposals (H high, confidence low). This strategy captures that distinction.

Best for: Exploratory tasks where you want the swarm to keep deliberating until genuinely uncertain agents make up their minds. Good when proposal count varies.


Hierarchical ​

typescript
consensus: { strategy: 'hierarchical', threshold: 0.65 }
// Agents configured with weight: 2.0 count double

Algorithm:

  1. Find the voter with the highest vote.weight across all votes
  2. If that top voter voted agree on any proposal, that proposal wins immediately with confidence = the top voter's weight
  3. If the top voter did NOT agree with anything, fall back to ConfidenceWeightedStrategy
top_voter = argmax(vote.weight for all votes)

if top_voter.stance == 'agree':
    winner = top_voter.proposalId
    confidence = top_voter.weight
else:
    fallback to confidence-weighted

Key property: a single high-authority agent can override the group. This is intentional for scenarios with domain experts.

Best for: Mixed-expertise swarms where some agents (e.g., a security expert reviewing security proposals) should have veto/override authority.


Simple Voting ​

typescript
consensus: { strategy: 'voting', threshold: 0.6 }

Algorithm:

  1. For each proposal, count votes: agrees = count(stance === 'agree'), total = count(all stances)
  2. Compute ratio = agrees / total
  3. Best proposal is the one with the highest ratio
  4. Consensus reached when bestRatio >= threshold
ratio = count(agree) / count(all_votes)

Key property: each agent gets exactly one vote regardless of weight or confidence. Pure democracy.

Best for: Democratic decision-making, or when you don't trust agent confidence calibration. Simplest to reason about.

Strategy Comparison ​

StrategyWeights matter?How confidence is computedConvergence speed
confidence-weightedYesagreeWeight / totalWeightMedium
bayesianYes (via LR)Posterior probabilitySlower (needs evidence)
entropyYes (support sums)1 - H/H_maxDepends on proposal count
hierarchicalYes (top voter overrides)Top voter's weightFast (if expert agrees)
votingNoagrees / totalFast

Conflict Resolution ​

When the signal bus detects conflicting proposals (two proposals from different agents), the configured resolution mode kicks in:

Structured Debate (conflictResolution: 'debate') ​

The DebateRunner runs adversarial rounds between the two top-scoring proposals. This is the default.

How it works internally:

DebateRunner.runDebate(context)
  │
  ├── Emit 'debate:start' event
  │
  └── For round 1..maxDebateRounds:
       │
       ├── 1. Create two challenge signals
       │     ├── Challenge A: "Examine weaknesses in Proposal A: ..."
       │     └── Challenge B: "Examine weaknesses in Proposal B: ..."
       │     Source: 'debate-moderator'
       │     Confidence: 0.9
       │
       ├── 2. Publish challenges to signal bus
       │
       ├── 3. Run a normal round via RoundRunner
       │     (agents respond naturally to challenges)
       │
       ├── 4. Feed new signals to MathBridge for Bayesian updates
       │
       ├── 5. Check convergence:
       │     Does either proposal's Bayesian posterior >= 0.8?
       │     (DEFAULT_CONVERGENCE_THRESHOLD = 0.8)
       │     ├── YES → Return DebateResult(resolved: true)
       │     └── NO  → Continue to next round
       │
       └── Emit 'debate:round' with Bayesian posteriors
  │
  └── If maxRounds exhausted: return DebateResult(resolved: false)

In subsequent rounds (round > 0), challenges become more pointed: "Previous arguments have not resolved this. Provide new evidence or reasoning against: ..."

Debate result:

typescript
interface DebateResult {
  readonly resolved: boolean
  readonly winningProposalId: string | null
  readonly confidence: number
  readonly roundsUsed: number
  readonly signals: readonly Signal[]  // all signals emitted during debate
}

After a resolved debate, the orchestrator re-runs consensus evaluation with all accumulated votes and signals.

Majority Vote (conflictResolution: 'majority') ​

Skips debate entirely. Immediate majority vote using the configured strategy to decide.

Escalate (conflictResolution: 'escalate') ​

Emits an escalate signal and returns an unresolved consensus result. The calling system decides what to do.

ConsensusResult ​

typescript
interface ConsensusResult {
  readonly decided: boolean                          // was consensus reached?
  readonly decision?: string                         // winning proposal content
  readonly proposalId?: string                       // winning proposal ID
  readonly confidence: number                        // 0..1
  readonly votingRecord: readonly VoteRecord[]       // every vote, preserved
  readonly dissent: readonly string[]                // REASONING of agents that disagreed
  readonly reasoning: string                         // human-readable explanation
  readonly resolvedConflicts: readonly ConflictPair[]
  readonly durationMs: number                        // how long evaluation took
}

Dissent is first-class. The dissent array contains the reasoning strings from votes that disagreed with the winning proposal. This is not just agent IDs -- it's the actual counter-arguments. The full votingRecord preserves every individual vote with stance, weight, and reasoning.

Input Types ​

typescript
interface Proposal {
  readonly id: string
  readonly content: string
  readonly reasoning: string
  readonly sourceAgentId: string
  readonly sourceSignalId: string
  readonly confidence: number
  readonly timestamp: number
}

interface VoteRecord {
  readonly agentId: string
  readonly proposalId: string
  readonly vote: VotePayload        // stance, weight, reasoning
  readonly timestamp: number
  readonly causalLevel?: CausalLevel
}

Custom Strategies ​

Implement ConsensusStrategy and pass it to the constructor:

typescript
import type {
  ConsensusStrategy,
  ConsensusEvaluation,
  Proposal,
  VoteRecord,
  ResolvedConsensusConfig,
} from '@cognitive-swarm/core'

const expertVetoStrategy: ConsensusStrategy = {
  id: 'expert-veto',
  evaluate(
    proposals: readonly Proposal[],
    votes: readonly VoteRecord[],
    config: ResolvedConsensusConfig,
  ): ConsensusEvaluation {
    // Find votes from 'expert' agent
    const expertVotes = votes.filter(v => v.agentId === 'expert')
    const vetoed = expertVotes
      .filter(v => v.vote.stance === 'disagree')
      .map(v => v.proposalId)

    // Remove vetoed proposals
    const viable = proposals.filter(p => !vetoed.includes(p.id))

    if (viable.length === 0) {
      return {
        reached: false,
        confidence: 0,
        reasoning: 'Expert vetoed all proposals',
      }
    }

    // Pick highest-confidence viable proposal
    const winner = viable.sort((a, b) => b.confidence - a.confidence)[0]!
    return {
      reached: true,
      winningProposalId: winner.id,
      confidence: winner.confidence,
      reasoning: `Expert approved; confidence ${winner.confidence}`,
    }
  },
}

// Register when creating the engine
const engine = new ConsensusEngine(
  { strategy: 'expert-veto', threshold: 0.5 },
  events,
  [expertVetoStrategy],  // custom strategies array
)

The ConsensusEngine stores all strategies in a Map<string, ConsensusStrategy> and looks up the active one by config.strategy. Custom strategies are merged into the same map, so they can override built-in strategies.

Vote Weight Pipeline ​

Before votes reach the consensus engine, the orchestrator applies two optional transformations:

Raw votes from signal bus
  │
  ├── 1. Attention weights (surprise-based)
  │     weight *= clamp(attentionWeight, 0.8, 1.2)
  │     Mild nudge — surprise informs routing, not votes
  │
  ├── 2. Reputation weights (if advisor enabled)
  │     weight *= reputationScore
  │     Based on agent's historical accuracy
  │
  └── Final votes → ConsensusEngine.evaluate()

The attention weight clamping to [0.8, 1.2] is intentionally conservative. Unbounded attention (up to 3x in earlier versions) was causing surprising agents to drown out agreement and block consensus.

Released under the Apache 2.0 License.