Skip to main content

weighted-emergent-bias

A runtime circuit-breaker for the Degeneration-of-Thought (DoT) problem in multi-agent LLM systems.

CI Docs PyPI Python Typing License

📖 Documentation · 📦 PyPI · 📚 Wiki · 🗺 Roadmap · 📐 Design

Status: alpha, v0.6 — all five modules shipped. Detect per-node bias, weight + accumulate it, halt/reroute with hysteresis + a recovery machine, repair via a skeptic panel or MADERA, and emit an append-only audit trail with SARIF 2.1.0 export and HTML/JSON reports. This library makes no validated performance claims — see Prior work.


Contents

The problem

In a multi-agent LLM pipeline, one agent's mildly stereotyped output becomes the next agent's ground truth. Downstream agents do not re-litigate the premise they were handed — they build on it, and the bias compounds through the graph until every stage has homogenized around the same skewed register. This failure mode is called Degeneration-of-Thought (DoT), and single-model alignment does not catch it: the bias is not a property of any one model's weights, it is a property of how the agents are wired together.

weighted-emergent-bias is a runtime circuit-breaker for that failure. It probes each node with a demographically perturbed counterfactual to get a local bias score, weights that score by the node's downstream blast radius via graph centrality, accumulates the weighted scores into a network-level moving average as the graph executes, and halts the run deterministically when that average crosses a threshold — freezing the compromised payload and rerouting control to a mitigation subgraph instead of letting the contaminated state propagate.

flowchart LR
    A["Agent A<br/>mild skew"] -->|"output becomes<br/>A's premise"| B["Agent B"]
    B --> C["Agent C"]
    C --> D["Agent D<br/>fully homogenized"]
    A -.->|"no agent re-litigates<br/>what it was handed"| D
    classDef bad fill:#7a1f1f,color:#fff,stroke:#3d0d0d,stroke-width:2px;
    classDef warn fill:#7a5a1f,color:#fff,stroke:#3d2d0d,stroke-width:2px;
    class A warn
    class D bad

Scope — what this detects, and what it does not

The detector is a within-node counterfactual invariance test: it compares a node's output to its own output on a demographically perturbed input, anchored to the node's own sampling-noise floor — never to the other agents' consensus.

✅ Detects ❌ Does not detect
Demographic / stereotype bias — a node treating an input differently because of a protected attribute or its proxy Factual error — a node can be perfectly invariant and still wrong (needs an external oracle; offered only as an optional injectable verifier)
Bias amplified through the graph topology Consensus deviation — a lone correct dissenter is never flagged for disagreeing
Slow drift and sudden spikes (multi-scale accumulation) Style drift as a bias signal — kept as a separate axis, not conflated

Keeping the claim this narrow is what makes it defensible: the whole noise-floor apparatus supports this statement and no broader one. See docs/DESIGN.md §0.

How it works

flowchart LR
    subgraph M1["Detect · M1"]
        O["Node output"] --> PR["LOOC probe<br/>standard · counterfactual · null"]
        PR --> DV["JSD / embedding<br/>vs noise floor"]
        DV --> BI["BiasScore Bᵢ<br/>effect size + CI"]
    end
    subgraph M2["Weight + Accumulate · M2"]
        BI --> KW["Katz weight wᵢ<br/>+ error history"]
        KW --> EW["Multi-scale EWMA<br/>B_net"]
    end
    subgraph M3["Control · M3"]
        EW --> TH{"B_net ≥ τ<br/>(hysteresis)?"}
        TH -->|below| GO["Promote → next node"]
        TH -->|breach| FR["Freeze + reroute"]
    end
    subgraph M4["Intervene + Recover · M4"]
        FR --> SK["Skeptic panel<br/>/ MADERA repair"]
        SK --> RC["Recovery + re-entry"]
        RC --> GO
    end
    GO -. audit .-> AU[("Causal audit trail<br/>SARIF · M5")]
    FR -. audit .-> AU
Stage Mechanism
Detect LOOC probes each node with a demographically perturbed counterfactual and measures the divergence (true Jensen–Shannon over a shared candidate support, or an embedding distance for free-form output) net of the node's own sampling noise.
Weight Blast-radius centrality gives each node a dependency weight wᵢ — row-sums of the Katz walk matrix, counting the walks leaving a node, i.e. Katz on the reversed graph relative to networkx's incoming-influence convention. Optionally complemented by an injected error-history prior.
Accumulate Fast + slow bias-corrected EWMAs track B_net across supersteps — the fast scale catches spikes, the slow scale catches drift.
Break A two-threshold hysteresis controller (τ_enter > τ_exit) halts execution deterministically and freezes the payload — before the downstream node consumes it.
Intervene Conformity spirals route to parallel Skeptic Agents under a trust graph; parametric bias routes to a MADERA-style diagnose → retrieve → rewrite repair, then a guarded re-entry.
Audit Every probe, divergence, weight, and routing decision lands in an append-only causal trail, exportable as SARIF or HTML.

The control lifecycle

ControlMachine is a deterministic four-state machine with a terminal escalation. Recovery is re-measured, never assumed: the cool-down must elapse and B_net must independently fall back below tau_exit before the run resumes.

stateDiagram-v2
    [*] --> Normal
    Normal --> Warning: slow B_net >= tau_warn (drift)
    Warning --> Normal: drift clears
    Normal --> Intervention: fast B_net >= tau_enter
    Warning --> Intervention: fast B_net >= tau_enter
    Intervention --> Recovery: repair hook fires<br/>(payload frozen)
    Recovery --> Recovery: cool-down pending -> HALT
    Recovery --> Normal: B_net < tau_exit<br/>(incident closed, retries reset)
    Recovery --> Intervention: still breached,<br/>attempts < max_retries
    Recovery --> Escalated: attempts == max_retries
    Escalated --> [*]: human review

The gap between tau_enter and tau_exit is the hysteresis dead-band — without it a B_net sitting near the line would flip the breaker every superstep. max_retries bounds the repair attempts within one incident; a clean recovery closes the incident and resets the counter.

Design principles

  • Framework-agnostic core. Scoring, topology, accumulation, and breaker logic depend only on numpy and networkx. LangGraph is the reference adapter, shipped as an optional extra.
  • Bring your own model. No bundled LLM SDK. You supply a client callable; the library supplies the protocols, the math, and reference agents.
  • Noise floor, always. An LLM sampled twice on identical input diverges from itself. Every score is reported net of an empirically estimated per-node null, as a standardized effect size with a confidence interval. Thresholding raw divergence would just threshold temperature.
  • No silent coverage gaps. When probing is sampled or skipped for cost, the sampling rate is recorded in the audit trail. A partial scan never reports as a full one.

Install

pip install weighted-emergent-bias

Requires Python 3.10+. Runtime dependencies are just numpy and networkx — no LLM SDK is bundled, and no network call happens unless you make one.

Extra Install Pulls in
(none) pip install weighted-emergent-bias numpy, networkx — the whole core
langgraph pip install "weighted-emergent-bias[langgraph]" Reference LangGraph adapter
anthropic pip install "weighted-emergent-bias[anthropic]" Real-model adapter + validation harness (makes billable API calls)
study pip install "weighted-emergent-bias[study]" matplotlib, for study plots
dev pip install -e ".[dev]" Test, lint, and type-check toolchain

Optional integrations are never imported by the core; their tests sit behind importorskip, so the default suite needs no network and no API key.

Quickstart

The perturbation engine and the ground-truth fake client are usable immediately:

from weighted_emergent_bias import AxisSpec, Substitution, perturb

gender = AxisSpec(
    name="gender",
    substitutions=(Substitution("he", "she"), Substitution("his", "her")),
)

perts = perturb("He submitted his application", [gender])
print(perts[0].perturbed)  # -> "She submitted her application"

Perturbation walks nested payloads (dicts, lists) and edits only string leaves; structure, keys, and non-string values are held fixed. Explicit and proxy substitutions produce separate perturbations. See docs/example-axes.md for illustrative axis sets (no axis list ships as a default — that is a deliberate choice).

Propagation (M2) weights each node's bias by its downstream blast radius and accumulates a network-level signal across execution:

from weighted_emergent_bias import AgentDAG, NetworkAccumulator, dependency_weights

dag = AgentDAG([("router", "worker"), ("router", "judge"), ("worker", "judge")])
weights = dependency_weights(dag).weights  # Katz blast radius, normalized

acc = NetworkAccumulator()  # fast + slow bias-corrected EWMA
state = acc.update({"router": 0.4}, weights)  # a biased central node fires
print(round(state.fast, 3))  # B_net rises with weighted node bias

The end-to-end detect→weight→accumulate path is exercised by the DoT simulation harness; results are in the propagation study.

Control (M3) halts deterministically on breach, with hysteresis to avoid thrashing and a recovery state machine:

from weighted_emergent_bias import CircuitBreaker, ControlMachine

machine = ControlMachine(CircuitBreaker(tau_enter=0.3, tau_exit=0.15))
decision = machine.step(fast=0.5, slow=0.1)  # a spike above tau_enter
print(decision.action, decision.state)  # BreakerAction.REROUTE BreakerState.INTERVENTION

The breaker trips once and stays tripped until B_net falls below the lower tau_exit; a persistent breach escalates instead of looping. Thresholds come from calibrate_thresholds on control runs (no magic constant). The optional LangGraph adapter stages each node's output and only promotes it once the breaker clears — so a biased payload never reaches the next node. See the control study.

Intervention (M4) repairs a halted run: a conformity spiral routes to a skeptic panel under a trust graph that prunes overconfident agents (but never a correct dissenter); entrenched parametric bias routes to a MADERA-style diagnose→retrieve→rewrite loop. Everything is protocol + reference implementation over an injected LLM callable. The InterventionRunner wires this into the M3 recovery hook; because M3 re-measures B_net, a genuine repair drives the run back to Normal. See the intervention study, where trust-weighting recovers the correct output that plain majority-voting loses.

Roadmap

Five layered modules — see docs/ROADMAP.md. Each earlier module is a number the later ones transform, so the order is not negotiable and M1 carries the real risk.

flowchart TD
    M1["M1 · Detection core<br/>perturbation · divergence · noise floor"]:::done
    M2["M2 · Propagation<br/>Katz weight · multi-scale EWMA"]:::done
    M3["M3 · Control<br/>hysteresis breaker · state machine"]:::done
    M4["M4 · Intervention<br/>skeptics · trust graph · MADERA"]:::done
    M5["M5 · Evidence<br/>audit trail · SARIF · reports"]:::done
    M1 --> M2 --> M3 --> M4 --> M5
    M3 --> M5
    classDef done fill:#1f7a1f,color:#fff,stroke:#0d3d0d,stroke-width:2px;
    classDef todo fill:#2b2b2b,color:#ccc,stroke:#555,stroke-dasharray:4 3;
Module Ships as Status
M1 Detection core — perturbation, divergence, noise floor, probe v0.1 ✅ shipped (calibration study)
M2 Propagation — Katz weighting, multi-scale EWMA v0.2 ✅ shipped (propagation study)
M3 Control — hysteresis breaker, recovery state machine, LangGraph adapter v0.3 ✅ shipped (control study)
M4 Intervention — skeptic panel, trust-graph pruning, MADERA v0.4 ✅ shipped (intervention study)
M5 Evidence — causal trail, SARIF 2.1.0 export, reporting v0.5 ✅ shipped (evidence study)

All five modules are shipped. What remains is deliberately not code: real-model validation (the harness is one command, but needs an API key and makes billable calls — and a single run is evidence, not validation) and benchmark reproduction (MALIBU / BBQ-Hard, which needs datasets and its own budgeted workstream). See the CHANGELOG for release history.

Module plans: PHASE-1, PHASE-2, PHASE-3, PHASE-4, PHASE-5. The 2026-07 external-review triage is in docs/reviews/.

Prior work, and what this does not claim

This design implements and adapts mechanisms from published research. Those papers' results are theirs, measured on their setups — not evidence that this implementation works.

  • LOOC + Synthetic Data CalibrationBeyond Generation (ACL 2025 Findings). Reports a 57.5% label-bias reduction in a single-model classification setting — a different metric on a different unit of analysis than multi-agent emergent bias.
  • MADERATowards Fairer AI (AAAI-SS). Reports BBQ-Hard improvements for its own pipeline; the reimplementation here is unvalidated.
  • MALIBUMulti-Agent LLM Implicit Bias Uncovered (arXiv:2507.01019). Cited as motivation; this library has not been evaluated on it.
  • CortexDebate — source of the trust-graph pruning. Its T = (C+R+I)/S formula is a consulting heuristic, treated here as a heuristic that must beat uniform aggregation in an ablation before it is believed.

Benchmark reproduction is deliberately not on the v0.x roadmap. Until it happens, the only claim made here is that the mechanics are implemented and demonstrable on a synthetic DoT harness.

Contributing

See CONTRIBUTING.md. In short: pip install -e ".[dev]", then ruff check . && ruff format --check . && mypy && pytest. CI runs the same across Python 3.10/3.11/3.12; the matrix is load-bearing (numpy's type stubs differ across versions).

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

weighted_emergent_bias-0.6.0.tar.gz (597.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

weighted_emergent_bias-0.6.0-py3-none-any.whl (76.7 kB view details)

Uploaded Python 3

File details

Details for the file weighted_emergent_bias-0.6.0.tar.gz.

File metadata

  • Download URL: weighted_emergent_bias-0.6.0.tar.gz
  • Upload date:
  • Size: 597.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for weighted_emergent_bias-0.6.0.tar.gz
Algorithm Hash digest
SHA256 3736a3f35576e662a36c7d1eee8c80774e0e98620738f6363c95cf3a24358469
MD5 ded060558e5cce81c19d5d374e63a339
BLAKE2b-256 c89243ec54a830cca61cc9721a52d0881eaf89623dda5c09948f304d7fb69896

See more details on using hashes here.

Provenance

The following attestation bundles were made for weighted_emergent_bias-0.6.0.tar.gz:

Publisher: release.yml on krishddd/weighted-emergent-bias

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file weighted_emergent_bias-0.6.0-py3-none-any.whl.

File metadata

File hashes

Hashes for weighted_emergent_bias-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 09ec68558aef94479d247a3ec81f77cb68f667dbc1c9c4508999e5fe255ebee5
MD5 01b14891a4242cbbe06284efaa03259c
BLAKE2b-256 1a86a79b577bed61ce0f6e748464356f35312112c43abcbbc85974cce7015bd5

See more details on using hashes here.

Provenance

The following attestation bundles were made for weighted_emergent_bias-0.6.0-py3-none-any.whl:

Publisher: release.yml on krishddd/weighted-emergent-bias

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page