@animakit/sprt
v0.1.0
Published
Sequential Probability Ratio Test (Wald 1947) — Bernoulli SPRT gating in <1ms, 0 deps. Extracted from 53 production sprints.
Maintainers
Readme
@animakit/sprt
Sequential hypothesis testing in <1ms with zero dependencies — before you evolve your agent's config.
Wald (1947) Bernoulli SPRT: accumulate log-likelihood ratio per boolean sample, stop at upper/lower boundaries. Pure functions, zero runtime deps, zero I/O. Extracted from the production agent of ANIMA — 53 sprints, three verbatim copies of the same math (source + two test files that re-implemented it inline). This package is the single source of truth.
npm install @animakit/sprtimport { createSprt, computeSprt, presets } from '@animakit/sprt';
// Streaming accumulator (freezes after terminal decision)
const sprt = createSprt(presets.animaProduction);
for (const success of [true, true, false, true, /* … */]) {
const { llr, decision } = sprt.update(success);
if (decision === 'accept') break; // production: 'apply'
if (decision === 'reject') break; // production: 'reject'
}
// Batch mode (parity with AgentConfigEvolver's trace loop)
const state = computeSprt(
traces.map((t) => t.quality_proxy > 0.70),
presets.animaProduction,
previousLlr,
);Why this exists
@animakit/neuromorphic-router applies score multipliers from statistically approved adjustments. @animakit/sprt-evolution (future) will compute those adjustments from traces. Both need the same SPRT core and the same KeywordWeightAdjustment type. Extracting @animakit/sprt first (PACKAGE_INVENTORY order 0) unblocks both without coupling them.
Production preset — honest asymmetry
presets.animaProduction uses the exact constants from AgentConfigEvolver.evaluatePendingRecommendations():
| Boundary | Value | Meaning |
|---|---|---|
| upperLlr | log(19) ≈ +2.944 | Accept H1 — matches Wald α=β=0.05: log((1-β)/α) |
| lowerLlr | log(0.2) ≈ -1.609 | Reject H1 — faster than symmetric Wald |
The upper boundary is textbook Wald with α=β=0.05. The lower boundary is not the symmetric partner (log(β/(1-α)) ≈ log(1/19) ≈ -2.944). Production chose faster rejection — we preserve that exactly via explicit lowerLlr, not by silently re-deriving from α/β.
For new deployments, pass alpha + beta to get symmetric Wald boundaries:
createSprt({ p0: 0.5, p1: 0.62, alpha: 0.05, beta: 0.05 });
// upperLlr ≈ +2.944, lowerLlr ≈ -2.944Decision mapping
| Anima Core | @animakit/sprt |
|---|---|
| apply | accept |
| reject | reject |
| gathering_data | continue |
Shared type for router + evolver
import type { KeywordWeightAdjustment } from '@animakit/sprt';
const adj: KeywordWeightAdjustment = {
agent: 'JEFE',
direction: 'boost',
delta: 0.05,
};API
interface SprtConfig {
p0: number;
p1: number;
upperLlr?: number;
lowerLlr?: number;
alpha?: number;
beta?: number;
}
type SprtDecision = 'accept' | 'reject' | 'continue';
interface SprtState {
llr: number;
samples: number;
decision: SprtDecision;
}
function createSprt(config: SprtConfig): {
update(success: boolean): SprtState;
updateMany(successes: boolean[]): SprtState;
state(): SprtState;
reset(): void;
};
function computeSprt(
successes: boolean[],
config: SprtConfig,
startLlr?: number,
): SprtState;
const presets: { animaProduction: SprtConfig };Accumulator vs batch: createSprt freezes after the first terminal decision (streaming). computeSprt always processes the full array (batch parity with production's trace loop).
Latency
100k iterations (Node 20+, consumer CPU):
createSprt().update(true) p99 << 1ms
computeSprt(20 samples) p99 << 1msRun pnpm bench in the package directory.
Parity
The test suite reproduces every scenario from:
Anima_core/tests/sprint48-sprt.test.ts— LLR within 1e-12, identical decisions viapresets.animaProductionAnima_core/tests/sprint48-router-multipliers.test.ts—KeywordWeightAdjustmentstructural contract
License
MIT
