PHASE 3 — Control (M3, v0.3)¶
Goal. A deterministic control plane that watches the B_net signal from M2 and, when bias
crosses a threshold, halts the run, freezes the compromised payload, and reroutes to a mitigation
subgraph — with hysteresis so it does not thrash near the boundary, and a recovery protocol
so a repaired run can re-enter. Ships a framework-agnostic core plus a LangGraph reference adapter.
Explicit non-goals. M3 does not implement interventions — skeptic debate and MADERA repair are M4. The breaker routes control to a mitigation hook (an injectable callback) and consumes its result; the hook's default is a no-op, so the whole control plane is testable without M4. No audit trail / SARIF (M5). No new detection or propagation.
The one-sentence success test. On a B_net trajectory that rises through tau_enter and then
oscillates near it, the breaker enters Intervention exactly once and does not flip state on the
oscillation; it returns to Normal only after B_net drops below tau_exit and a cool-down
elapses — deterministically. Under LangGraph, the halt freezes the node's output in shared state and
reroutes via Command before the downstream node runs.
Inheritance from prior phases (settled inputs, not open questions).
B_nethas two scales (M2): a fast EWMA (spikes) and a slow EWMA (drift). The breaker consumes both — see R1.B_net's absolute scale depends on the Katz α and the propagation model (Phase 2 study §2). So there is no portable magic threshold; M3 must ship a calibration path, not a constant — see R6.- Framework-agnostic core (project principle): the breaker and state machine are pure Python, fully testable with no LangGraph and no numpy. LangGraph is an optional reference adapter.
- Detection is counterfactual-invariance, scoped narrow (DESIGN §0): the breaker halts on demographic-bias propagation, never on consensus deviation. Nothing here widens that claim.
1. Research questions¶
R1 — Which scale(s) does the breaker act on?¶
M2 exposes fast and slow. The review (DESIGN §8) says trip on fast, drift-alert on slow.
Recommendation: map the two scales to two states. slow crossing tau_warn → Warning
(drift building; observe, maybe raise probing intensity later). fast crossing tau_enter →
Intervention (a spike that warrants halting). This uses both scales for what each is good at and
gives the state machine (R3) its natural triggers. A single-scale fallback (fast only) stays
available for users who do not want drift alerts.
R2 — Hysteresis and the continuous mixing ratio¶
A single binary tau thrashes: a B_net sitting near it flips the breaker on/off every step
(gap #3). Two fixes, both banked:
- Two thresholds
tau_enter > tau_exit. Enter Intervention attau_enter; only leave onceB_netfalls below the lowertau_exit. The gap between them is the dead-band that kills oscillation. - Continuous mixing ratio
m = sigmoid((B_net - tau_enter) / kappa)in[0, 1]— a smooth "how compromised" severity rather than a hard 0/1.
Recommendation: the breaker returns a discrete decision (for the deterministic halt/reroute)
and carries m as advisory severity. The application of m (blending consensus vs. skeptic
output) is M4's job — M3 computes it, M4 consumes it. This keeps the halt deterministic (a decision
plane must be predictable) while still surfacing the smooth signal the review wanted.
R3 — The recovery state machine¶
Four states plus a terminal escape (gap #13):
NORMAL --slow>=tau_warn--> WARNING --fast>=tau_enter--> INTERVENTION
^ | |
| fast>=tau_enter (run mitigation hook)
| v v
+--B_net<tau_exit & cool-down elapsed & recheck OK-- RECOVERY
|
max_retries exceeded ---------+--> ESCALATED (terminal, human review)
- NORMAL → WARNING:
slow >= tau_warn. WARNING → NORMAL:slow < tau_warn(drift subsided). - any → INTERVENTION:
fast >= tau_enter. Halt, freeze the payload, run the mitigation hook. - INTERVENTION → RECOVERY: hook returned. Enter a cool-down window.
- RECOVERY → NORMAL: cool-down elapsed and
B_net < tau_exitand the corrected output passes its own re-check (itsB_netcontribution is belowtau_exit). - RECOVERY → INTERVENTION: re-check still bad → retry (increment attempt count).
- → ESCALATED: attempts exceed
max_retries. Terminal; hand to human review. Prevents the infinite correction loop the review flagged.
Recommendation: implement this as a pure state machine whose Intervention action is an
injectable callable (Callable[[InterventionContext], InterventionResult]), defaulting to a
no-op that leaves B_net unchanged (so the default machine, with no M4, escalates after
max_retries — a safe, honest default). All transition guards, the cool-down counter, and the
retry counter live here and are unit-tested without any framework.
R4 — Edge-level interception (gap #5)¶
If the breaker checks after a node commits its output to shared state, the biased payload has
already reached the next node's context. The check must sit on the edge: node output goes to an
unverified_output staging buffer; the breaker inspects it; if clear it is promoted to main
state, if breached it is frozen and control reroutes — all before any downstream node activates.
Recommendation: the core exposes freeze(payload) and the promote/replace decision; the
LangGraph adapter realizes the staging buffer as a BiasState channel and gates promotion in the
node guard (R5). The core never assumes a framework; the adapter wires the buffer.
R5 — LangGraph adapter scope, and how to test it¶
v0.3 ships the reference adapter: a BiasState TypedDict (local scores, weights, network_ewma,
unverified_output, causal_audit_trail placeholder for M5), a node-guard decorator that probes +
stages + gates promotion, and breaker/reroute nodes using Command(goto=..., update=...).
Send-based skeptic fan-out is M4, not here.
Recommendation: keep the adapter thin (LangGraph's API churns; minimize surface). Test it with
pytest.importorskip("langgraph"), and add langgraph to the dev extra so CI actually exercises
it. If it destabilizes the 3.10–3.12 mypy matrix, isolate the adapter in a per-module mypy override
rather than weakening strictness elsewhere. The default test suite must still pass with LangGraph
absent (the core is what most tests cover).
R6 — Threshold calibration (no magic tau)¶
The Phase 2 study showed B_net's scale is deployment-dependent. Shipping a constant tau would be
the same sin as thresholding raw divergence.
Recommendation: ship a calibration utility that, given recorded B_net trajectories from
control (unbiased) runs, picks tau_enter at a target false-halt rate (e.g. the 99th percentile
of control B_net), and sets tau_exit = c * tau_enter (c ≈ 0.6–0.8) and tau_warn on the slow
scale similarly. Document the defaults as starting points requiring calibration, never as
validated. This mirrors the Phase 1 discipline: the library ships the method, the user owns the number.
R7 — Bias-type routing seam¶
M4 routes conformity spirals to skeptics and parametric bias to MADERA. Classifying which is which is genuinely uncertain and needs signals M3 only partly has.
Recommendation: M3 ships the seam — route_intervention(context) -> "skeptics" | "madera" |
"none" — with a simple, documented default heuristic (broad multi-node contamination → conformity →
skeptics; a single persistently-high node → parametric → madera) and a pluggable classifier. It is
explicitly a heuristic to be validated in M4; M3 does not pretend to have solved bias-type diagnosis.
R8 — Router self-monitoring (gap #10)¶
The reroute/orchestrator is itself an agent that can amplify bias, yet nothing monitors it.
Recommendation for v0.3: treat the router as a normal node — it is probed and its B_net
contribution counts like any other (no exemption). The routing-entropy signal from the review
(low selection entropy → the orchestrator is concentrating on agreeing agents → feed the breaker
independently) is designed here but built when a real router exists in M4; v0.3 records the intent
and leaves the hook.
2. Work packages¶
WP1 — Control types (types.py)¶
BreakerState (NORMAL/WARNING/INTERVENTION/RECOVERY/ESCALATED), BreakerAction
(PROCEED/HALT/REROUTE/ESCALATE), BreakerDecision (state, action, mixing ratio, b_net, reason,
frozen payload). Frozen, self-validating.
Accept when: invariants enforced (e.g. mixing ratio in [0,1]); a decision round-trips its fields.
WP2 — Hysteresis breaker (breaker.py, pure)¶
CircuitBreaker(tau_enter, tau_exit, tau_warn, kappa) with .check(fast, slow) -> BreakerDecision.
Two-threshold dead-band + sigmoid mixing ratio. No framework, no numpy needed.
Accept when: a trajectory oscillating between tau_exit and tau_enter does not flip the
decision (the anti-thrash test); mixing ratio is monotonic in B_net; tau_enter > tau_exit
enforced.
WP3 — Control state machine (breaker.py)¶
ControlMachine driving the R3 transitions with cool-down, max_retries → ESCALATED, and an
injectable intervention hook (default no-op). Pure and deterministic.
Accept when: the full lifecycle is unit-tested — enter on spike, cool-down gating, recovery on
recheck-OK, retry on recheck-bad, escalation after max_retries; no thrash near the boundary.
WP4 — Payload freezing + routing seam (breaker.py, intervention/router.py)¶
freeze(payload) (immutable snapshot of the compromised state) and route_intervention(context)
with the default heuristic classifier.
Accept when: a frozen payload is recoverable and unmutated; routing returns the documented
strategy for representative broad-vs-concentrated contamination patterns.
WP5 — LangGraph reference adapter (integrations/langgraph/)¶
BiasState TypedDict + reducers; a node-guard decorator (probe → stage in unverified_output →
gate promotion); prebuilt breaker/reroute nodes using Command. Edge-level interception via the
staging buffer.
Accept when (langgraph installed): a real graph run halts at the seeded node, the frozen payload is
recoverable from state, and the downstream node does not observe the biased output. Tests use
importorskip; default suite passes without langgraph.
WP6 — Threshold calibration (breaker.py or calibration.py)¶
calibrate_thresholds(control_trajectories, *, target_false_halt_rate) → (tau_enter, tau_exit,
tau_warn).
Accept when: on recorded control B_net, the chosen tau_enter yields ≤ the target false-halt
rate; documented as a starting point, not a validated constant.
WP7 — Control study¶
Written report (like Phase 1/2): breaker behavior on DoT trajectories — trips once at breach, no
thrash near boundary, recovery after cool-down, escalation after repeated failure. Reproducible.
Accept when: the report shows a near-boundary oscillating B_net producing a single state entry
(vs. the many flips a binary threshold would produce), with reproducible figures.
3. Test strategy¶
Four layers as before. M3-specific must-haves:
- Property: the breaker never flips state while B_net stays within the [tau_exit, tau_enter]
dead-band; state transitions are a function of (state, fast, slow, counters) only (deterministic).
- Statistical/integration: on DoT trajectories (reuse the M2 harness), a single Intervention
entry at breach; escalation after max_retries no-op recoveries.
- Adapter: LangGraph tests behind importorskip; the default suite needs neither LangGraph nor
network. All randomness via the seeded rng fixture.
4. Risks¶
| Risk | Signal | Response |
|---|---|---|
| Thrashing near threshold | breaker flips every step | Two-threshold dead-band + anti-thrash property test (WP2). |
Magic tau that doesn't transfer |
breaker never/always fires on a new deployment | Ship calibration, not a constant (R6/WP6); document defaults as starting points. |
| Biased payload leaks downstream | next node sees pre-check output | Edge-level staging buffer; promotion gated by the breaker (R4/WP5). |
| Infinite correction loop | Recovery↔Intervention forever | max_retries → ESCALATED terminal state (R3/WP3). |
| LangGraph API churn / heavy dep | adapter breaks or bloats CI | Thin adapter; importorskip; default suite framework-free. |
| Over-claiming bias-type diagnosis | routing treated as solved | route_intervention is a documented heuristic seam, validated in M4 (R7). |
| Deterministic-halt vs continuous-mixing confusion | non-reproducible halts | Decision is discrete; mixing ratio is advisory only (R2). |
5. Definition of done¶
- [ ] Pure
CircuitBreakerwith two-threshold hysteresis + sigmoid mixing ratio; anti-thrash test passes - [ ]
ControlMachine: 4 states + ESCALATED, cool-down,max_retries, injectable hook; full-lifecycle tests - [ ] Edge-level interception: payload frozen and not visible downstream (LangGraph test)
- [ ] LangGraph adapter (
BiasState, guard,Commandhalt/reroute) behindimportorskip; default suite framework-free - [ ]
calibrate_thresholdsmeets a target false-halt rate on control trajectories - [ ]
route_interventionseam with a documented default heuristic - [ ] Control study published with reproducible figures (single entry vs. binary-threshold thrash)
- [ ]
mypy --strictclean on 3.10–3.12; core has no LangGraph import; no M4/M5 work