UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

135 lines (122 loc) 5.12 kB
/** * Self-consistency orchestrator (Wang et al. 2022, "Self-Consistency Improves * Chain of Thought Reasoning in Language Models"). * * Runs an arbitrary stochastic operation N times and aggregates the results. * Reduces variance from single-shot stochastic predictions; on reasoning * benchmarks the technique typically gains 5–15pp accuracy at the cost of * Nx compute. The package's RL algorithms, pattern matching, and routing * all benefit since they sample randomly and can produce different outputs * across calls. * * Aggregators: * - 'majority': pick the most common output (for discrete answers / labels). * Uses JSON.stringify for grouping; stable for primitive and * plain-object outputs. Float32Array values aggregate via * their array-form encoding. * - 'mean': numeric average (operation must return number). * Agreement = 1 - normalized stddev. * - 'first': take the first sample (no aggregation; useful for testing * or deterministic ops). * * Pair with `setGlobalRng(new Mulberry32(seed))` to make a self-consistency * run reproducible across machines. */ import type { RNG } from './rng.js'; import { random } from './rng.js'; export type SelfConsistencyAggregator = 'majority' | 'mean' | 'first'; export interface SelfConsistencyConfig { /** Number of samples to draw. Required. Larger N = more stable, more compute. */ N: number; /** How to aggregate samples into a single answer. Default: 'majority'. */ aggregator?: SelfConsistencyAggregator; /** * Optional RNG to advance per sample (e.g. to differentiate seeds across * samples when the operation reads from the global RNG). The orchestrator * itself doesn't directly call this RNG; it's provided so callers can * thread per-sample state (e.g. by reseeding before each call). */ rng?: RNG; } export interface SelfConsistencyResult<T> { /** The aggregated answer (majority vote, mean, or first). */ finalAnswer: T; /** Every sample produced, in the order produced. Length === config.N. */ samples: T[]; /** * For 'majority': fraction of samples matching finalAnswer (0..1). * For 'mean': 1 - normalized stddev (rough confidence proxy). * For 'first': always 1. */ agreement: number; /** The config used for this run. */ config: SelfConsistencyConfig; } /** * Run an operation N times and aggregate. The operation is awaited * sequentially — if the caller wants concurrency they can wrap it themselves. */ export async function selfConsistency<T>( operation: () => Promise<T> | T, config: SelfConsistencyConfig, ): Promise<SelfConsistencyResult<T>> { if (!Number.isInteger(config.N) || config.N <= 0) { throw new Error(`selfConsistency: N must be a positive integer, got ${config.N}`); } const samples: T[] = []; for (let i = 0; i < config.N; i++) { samples.push(await operation()); } const aggregator: SelfConsistencyAggregator = config.aggregator ?? 'majority'; let finalAnswer: T; let agreement: number; if (aggregator === 'majority') { // Group by JSON-stringified value (handles primitives, plain objects, // arrays). Float32Array does NOT JSON-encode losslessly by default — // callers wanting f32 majority should pre-convert via Array.from. const counts = new Map<string, { value: T; count: number }>(); for (const s of samples) { const key = canonicalKey(s); const existing = counts.get(key); if (existing) existing.count += 1; else counts.set(key, { value: s, count: 1 }); } let best: { value: T; count: number } = { value: samples[0], count: 0 }; for (const c of counts.values()) { if (c.count > best.count) best = c; } finalAnswer = best.value; agreement = best.count / samples.length; } else if (aggregator === 'mean') { const nums = samples as unknown as number[]; if (!nums.every((v) => typeof v === 'number' && Number.isFinite(v))) { throw new Error("selfConsistency: aggregator='mean' requires every sample to be a finite number"); } const mean = nums.reduce((a, b) => a + b, 0) / nums.length; const variance = nums.reduce((s, v) => s + (v - mean) ** 2, 0) / nums.length; const stddev = Math.sqrt(variance); const range = Math.max(1e-9, Math.abs(mean) || 1); // avoid div-by-0 finalAnswer = mean as unknown as T; agreement = Math.max(0, Math.min(1, 1 - stddev / range)); } else { finalAnswer = samples[0]; agreement = 1; } return { finalAnswer, samples, agreement, config }; } /** * Canonical-form key for grouping. JSON.stringify is enough for primitives, * arrays, and plain objects with stable key order. Callers wanting locale- * insensitive or shape-aware aggregation should pre-canonicalize their * outputs before calling selfConsistency. */ function canonicalKey(v: unknown): string { try { return JSON.stringify(v); } catch { // Cyclic structures, BigInt, etc. return String(v); } } // Re-export for callers that need it export { random };