Polyweave

@polyweave/quorum (1.0.6)

Published 2026-07-11 15:30:35 +00:00 by Dvorak

Installation

@polyweave:registry=https://hub.kl1.tenere.ai/api/packages/Polyweave/npm/
npm install @polyweave/quorum@1.0.6
"@polyweave/quorum": "1.0.6"

About this package

@polyweave/quorum

First-ahead-by-k quorum voting primitives with push-stream duplex integration for Polyweave orchestration.

Published as @polyweave/quorum.

Synopsis

import {
  createQuorumSession,
  firstAheadByK,
  stableCandidateKey,
  summarizeVoteCounts,
  createQuorumDuplex
} from '@polyweave/quorum';

const session = createQuorumSession({ k: 2, maxVotes: 10 });

session.addVote('candidate-a');
session.addVote('candidate-b');
session.addVote('candidate-a');

const state = session.getState();
console.log(state.decided);         // true
console.log(state.stoppedBecause);  // 'ahead-by-k'
console.log(state.selectedCandidate); // 'candidate-a'

// Convenience: one-shot first-ahead-by-k
const result = firstAheadByK(['A', 'B', 'A', 'A'], { k: 2 });
console.log(result.selectedCandidate); // 'A'

// Deterministic object key for voting on complex candidates
const key1 = stableCandidateKey({ name: 'Alice', score: 95 });
const key2 = stableCandidateKey({ score: 95, name: 'Alice' });
console.log(key1 === key2); // true (stable ordering)

// Frame-native quorum as a tool
const quorumTool = createQuorumDuplex({ k: 3 });
quorumTool.sink.write({ type: 'TOOL_CALL', content: { arguments: { candidates: ['A', 'B', 'A', 'B', 'A'], k: 2 } } });

Install

npm install @polyweave/quorum

Why

LLM multi-agent voting needs to stop early — you don't need all N votes when one candidate pulls ahead by k. This module implements first-ahead-by-k: as votes arrive, the session decides immediately when the leader's count exceeds the runner-up's by at least k. It handles min-votes (don't decide too early), max-votes (fallback to plurality), deterministic stable keys for complex objects (sorted property traversal), and a full push-stream duplex (createQuorumDuplex) that exposes quorum as a TOOL_CALL handler — feeding candidates through an optional keyFn duplex for embedding-based clustering before voting.

API

createQuorumSession(options?)

Creates a stateful quorum voting session.

Signature

function createQuorumSession(options?: {
  k?: number,
  keyFn?: (value: any) => string,
  maxVotes?: number,
  minVotes?: number
}): QuorumSession

Parameters

Name Type Required Default Description
options.k number No 1 Lead margin required to decide. Must be a positive integer. A leader with count C wins when C >= runnerUpCount + k.
options.keyFn function No stableCandidateKey Function that maps a candidate value to a stable string key for grouping votes.
options.maxVotes number No Infinity Maximum total votes before auto-deciding. When reached, the current leader is selected with stoppedBecause: 'max-votes'. Must be a positive integer.
options.minVotes number No 1 Minimum votes required before any decision is evaluated. Must be a positive integer.

Error Conditions

Condition Error Type Message
k is not a positive integer Error 'k must be a positive integer'
maxVotes is not a positive integer Error 'maxVotes must be a positive integer'
minVotes is not a positive integer Error 'minVotes must be a positive integer'

Return Value — QuorumSession

{
  addVote: (candidate: any, metadata?: object) => SessionState,
  getState: () => SessionState,
  isDecided: () => boolean
}

addVote(candidate, metadata?)

Name Type Required Default Description
candidate any Yes The vote value. Passed through keyFn to determine its canonical key for counting.
metadata object No {} Arbitrary metadata stored with this vote (e.g., attemptId, timestamp, confidence).

Returns the current SessionState. If the session is already decided, the vote is silently ignored — the vote count does not increase and the state is returned unchanged. This makes addVote idempotent after decision.

getState()

Returns the current SessionState:

{
  k: number,              // configured lead margin
  totalVotes: number,     // total votes received
  decided: boolean,       // whether a decision has been reached
  stoppedBecause: string | null, // 'ahead-by-k', 'max-votes', or null (if not decided)
  selectedKey: string | null,    // canonical key of selected candidate
  selectedCandidate: any | null, // the selected candidate value (raw, not key)
  leader: VoteRow | null,        // { key, count, candidate, votes }
  runnerUp: VoteRow | null,      // second-place row (null if only 1 candidate)
  counts: VoteRow[]              // all candidates sorted by count desc, then key asc
}

isDecided()

Returns boolean. True once stoppedBecause is set. Monotonic — once true, never becomes false.

Decision Logic

  1. If totalVotes < minVotes — do not evaluate (return undecided).
  2. Sort counts descending by count, ascending by key for ties.
  3. If leader.count >= runnerUp.count + kahead-by-k decision. Set selectedKey, decided = true.
  4. If totalVotes >= maxVotesmax-votes fallback. Leader wins even if not ahead by k. Set decided = true.

firstAheadByK(votes, options?)

Convenience function: creates a session, feeds all votes, and returns the final state.

Signature

function firstAheadByK(
  votes: any[],
  options?: { k?: number, keyFn?: (value: any) => string, maxVotes?: number, minVotes?: number }
): SessionState

Parameters

Name Type Required Default Description
votes any[] Yes Array of candidate values or keys. Each element is passed to session.addVote(element).
options object No {} Forwarded to createQuorumSession. Same parameters.

Return Value

The final SessionState after all votes have been added (or decision is reached mid-iteration).

Error Conditions

Same as createQuorumSession (forwarded parameter validation).


stableCandidateKey(value)

Returns a deterministic string key for any JavaScript value. Useful for voting on complex objects with consistent identity across serialization boundaries.

Signature

function stableCandidateKey(value: any): string

Key Generation Rules

Input Type Key Format
null 'null'
undefined 'undefined'
string, number, boolean JSON.stringify(value)
Array '[' + elements.map(stableCandidateKey).join(',') + ']'
Object '{' + sorted keys with stableCandidateKey values + '}'
Nested objects Recursively traversed

Object keys are sorted lexicographically before encoding, so { b: 2, a: 1 } and { a: 1, b: 2 } produce identical keys.

Error Conditions

No errors thrown. All inputs produce a string.


summarizeVoteCounts(counts)

Sorts and materializes a Map of vote entries into a ranked array.

Signature

function summarizeVoteCounts(
  counts: Map<string, { key: string, candidate: any, count: number, votes: any[] }>
): VoteRow[]

Return Value

Array of VoteRow sorted by count descending, then by key ascending for ties:

[
  { key: string, count: number, candidate: any, votes: any[] },
  ...
]

The votes array in each row is a shallow copy of the accumulated vote metadata entries { candidate, metadata }.


createQuorumDuplex(options?)

Creates a push-stream frame duplex that exposes quorum voting as a tool-call handler. Supports two modes: identity (direct voting on candidates) and keyFn (embedding-based clustering before voting).

Signature

function createQuorumDuplex(options?: {
  k?: number,
  maxVotes?: number,
  minVotes?: number,
  keyFn?: IFrameDuplex,
  name?: string
}): IFrameDuplex

Parameters

Name Type Required Default Description
options.k number No 1 Default lead margin (overridable via args.k in TOOL_CALL).
options.maxVotes number No Infinity Default max votes (overridable via args.maxVotes).
options.minVotes number No 1 Default min votes (overridable via args.minVotes).
options.keyFn IFrameDuplex No undefined Optional key-function duplex for voting. See below.
options.name string No null Node name exposed as duplex.name.

Mode 1: Identity (no keyFn)

When keyFn is not provided, the duplex operates in identity mode:

const tool = createQuorumDuplex({ k: 2 });
// tool._toolName === 'quorum'

Frames handled:

Inbound Outbound Description
TOOL_CALL TOOL_RESULT Runs first-ahead-by-k on args.candidates. Responds with full SessionState. Uses args.k, args.maxVotes, args.minVotes as overrides.
DESCRIBE DESCRIPTION Returns schema: { name: 'quorum', description: 'First-ahead-by-k quorum consensus over candidate frames.', capabilities: ['execute'], schema: { type: 'object', properties: { candidates: { type: 'array' }, k: { type: 'number', default: 3 }, maxVotes: { type: 'number' } } } }
TICK, CANCEL, ABORT No-op.
Any other frame type TOOL_RESULT Echoes frame.content or the frame itself.

Mode 2: keyFn (embedding-based voting)

When keyFn is provided (as an IFrameDuplex), candidates are piped through keyFn to obtain cluster labels or embedding-derived keys before quorum counting:

  1. All TOOL_CALL candidates are written to keyFn.sink as individual TOOL_CALL frames: { type: 'TOOL_CALL', content: { arguments: { candidate: item } } }.
  2. keyFn.source is listened to via a frame sink. Each TOOL_RESULT frame's content.key (or content) is collected as the canonical key.
  3. Keys are consumed in FIFO order, falling back to the raw candidate if the key was not yet available.
  4. A createQuorumSession is created with the keys, and voting proceeds as in identity mode.

This allows embedding/classifier adapters to serve as the key function — the quorum runs on semantic clusters rather than raw values.

Additional frames handled vs identity mode:

Inbound Outbound Description
CANCEL, ABORT Resets keyResults array.

Error Conditions

No explicit errors thrown. Parameter validation only occurs in the inner createQuorumSession call when processing a TOOL_CALL (see createQuorumSession error conditions).

Tests

npm test

Covers:

  • quorum.test.jsstableCandidateKey order-invariance, firstAheadByK ahead-by-k selection, session vote metadata tracking and post-decision no-op, maxVotes fallback to plurality.
  • rigor.test.js — Extensive property tests: stable key determinism (null, undefined, strings, numbers, arrays, objects, sorted-key invariance, distinct-value uniqueness), summarizeVoteCounts sort order, session invariants (starts not-decided, decision monotonicity, k=1 first-vote-decides, k=2 needs-two-same, selected key matches leader, minVotes delays decision, totalVotes accuracy, maxVotes triggers decision).

Requirements

  • Node.js 22 or later.
  • ESM only ("type": "module").
  • Dependencies: @polyweave/core.

Dependencies

Dependencies

ID Version
@polyweave/core *

Development Dependencies

ID Version
@rigor/core *

Keywords

polyweave quorum voting llm
Details
npm
2026-07-11 15:30:35 +00:00
90
John Dvorak
SEE LICENSE IN LICENSE
latest
6.8 KiB
Assets (1)
Versions (4) View all
1.0.6 2026-07-11
1.0.3 2026-07-11
1.0.2 2026-07-11
1.0.1 2026-07-10