Wrapper

Wrap any async function with a consensus gate using multiple reviewers and configurable strategies.

Overview

@consensus-tools/wrapper wraps any async function with a consensus gate. Multiple reviewers score the function's output, and a configurable strategy (unanimous, majority, or threshold) decides whether to allow, block, retry, or escalate.

Installation

pnpm add @consensus-tools/wrapper

Quick start

import { consensus } from "@consensus-tools/wrapper";

const safeDeploy = consensus({
  name: "deploy-to-prod",
  fn: deploy,
  reviewers: [securityReview, complianceReview],
  strategy: { strategy: "threshold", threshold: 0.7 },
  maxRetries: 2,
});

const result = await safeDeploy(env, version);
// result.action => "allow" | "block" | "retry" | "escalate"
// result.output => the return value of deploy()
// result.scores => [{ score: 0.9, rationale: "..." }, ...]
// result.aggregateScore => 0.85

API reference

consensus()

Wraps a function with a consensus gate. Returns an async function with the same signature that produces a DecisionResult.

function consensus<T, A extends any[]>(
  opts: ConsensusOptions<T, A>
): (...args: A) => Promise<DecisionResult<T>>;

Options:

FieldTypeDescription
namestringIdentifier for this consensus gate
fn(...args: A) => Promise<T>The function to wrap
reviewersReviewerFn<T>[]Array of reviewer functions
strategyStrategyConfigStrategy and threshold
maxRetriesnumberMax retry attempts before escalation
hooksLifecycleHooks<T>Optional lifecycle hooks

Writing reviewers

A reviewer receives the function output and context, and returns a score (0-1):

import type { ReviewerFn } from "@consensus-tools/wrapper";

const securityReview: ReviewerFn<DeployResult> = async (output, ctx) => {
  if (output.touchesProdDb) return { score: 0.1, rationale: "Modifies prod DB", block: true };
  return { score: 0.9, rationale: "Safe change" };
};

Setting block: true on any reviewer immediately blocks regardless of strategy.

Strategies

StrategyPasses when...
"threshold"Average score >= threshold (default 0.5)
"majority"More than half of reviewers score >= threshold
"unanimous"Every reviewer scores >= threshold
consensus({
  name: "risky-op",
  fn: riskyOp,
  reviewers: [r1, r2, r3],
  strategy: { strategy: "unanimous", threshold: 0.8 },
});

Lifecycle hooks

consensus({
  name: "managed-op",
  fn: myFn,
  reviewers: [r1],
  hooks: {
    beforeSubmit: (args) => console.log("About to call fn with", args),
    afterResolve: (result) => audit.log("Allowed", result),
    onBlock: (result) => alert("Blocked!", result.scores),
    onEscalate: (result) => pagerduty.trigger(result),
  },
});

aggregateScores()

Use the aggregation logic directly without the wrapper:

import { aggregateScores } from "@consensus-tools/wrapper";

const decision = aggregateScores(
  scores,                                    // ReviewResult[]
  output,                                    // the value being evaluated
  1,                                         // attempt number
  { strategy: "majority", threshold: 0.6 },  // strategy config
  2,                                         // maxRetries
);
// decision.action => "allow" | "block" | "retry" | "escalate"

Exports reference

ExportDescription
consensus(opts)Wrap a function with a consensus gate; returns an async function
aggregateScores(scores, output, attempt, config, maxRetries)Score aggregation utility
ConsensusOptionsOptions for consensus()
ReviewerFn<T>Reviewer function type
ReviewContextContext passed to reviewers (name, args, attempt)
ReviewResultReviewer return type (score, rationale, block)
Strategy"unanimous" | "majority" | "threshold"
StrategyConfigStrategy + threshold
DecisionResult<T>Final decision (action, output, scores, aggregateScore)
LifecycleHooks<T>Hook functions for beforeSubmit, afterResolve, onBlock, onEscalate

Looking for a simpler entry point?

@consensus-tools/universal wraps this package with sensible defaults — built-in guard reviewers and fail-safe behavior out of the box. Use universal for quick setup; use wrapper directly when you need full control over reviewers and strategies.

  • universal -- 3-line facade that uses wrapper with built-in guard reviewers
  • guards -- createGuardTemplate().asReviewer() bridges guards into wrapper
  • core -- job engine and policy resolution