Guard pipeline — one ordered acceptance gate¶
The repo has several independent acceptance checks (leakage, capability
regression, verifier/oracle agreement, human approval, the default statistical
gate). Stacking them by hand makes the order implicit and loses which ring fired.
GuardPipeline is the one place the order lives.
from agentdescent import GuardPipeline, Policies
from agentdescent.defaults import DefaultAcceptance
from agentdescent.policies import AcceptDecision
def no_gold_in_candidate(ctx):
... # supply the guard; shipped guards live in their own modules
return AcceptDecision(True)
policies = Policies(acceptance=GuardPipeline([
("leakage", no_gold_in_candidate),
("stats", DefaultAcceptance()),
]))
- Guards run in the listed order; the first refusal is the pipeline's decision.
categorystays a real merge category accepted by the aggregator (falling back tobelow-threshold), while the refusing guard's name and detail are carried indetail. collect_all=Truekeeps evaluating after a refusal for a full trail; the candidate is still refused, andaccept()reports the first refusal rather than whichever guard ran last.- An empty pipeline is the identity gate and accepts.
bind/configureare forwarded to every guard, so the engine's verifier and thresholds reach nested policies.checkpoint_state,restore_stateandon_screen_rejectare forwarded to the guards that implement them.evaluate(ctx)returns aGuardPipelineResultwith every named decision andfailed_at;accept(ctx)is the ordinary acceptance-policy shape.
The pipeline is deliberately generic: it imports no specific guard, so a
third-party guard is just another (name, policy) pair and the order is visible
in one place rather than in caller code.