Guardrails over a whole rule set
The front page shows one puzzle and one check. This page is about the other half of the system: three static analyses that ask questions of an entire rule set at once, each returning a counterexample — the exact input that breaks the property — rather than a bare pass or fail. All three reduce to the same solver primitive how reasoning works describes, asked a different way.
| Analysis | Question | Returns |
|---|---|---|
| Coverage | Does every input route somewhere? | total, plus a witness input inside any gap |
| Decision | Can two guards in an ordered table both fire, and can any never fire? | overlapping pairs with the input that fires both; unreachable guard indices |
| Invariant | Does this transition preserve its obligation? | preserved, plus a violating state when it does not |
Both halves of every example below come from one place. The code is quoted
directly out of guardrail_calculus.examples.guardrail_analyses, and the result
blocks are produced by running that same module's analyses during this site's
build — the module tests/examples/test_guardrail_analyses.py pins. Nothing on
this page is retyped, so the snippets cannot describe one workflow while the
verdicts below them report another, and a solver change that moved any verdict
or witness would fail that test rather than quietly rot this page.
Coverage: does every input route somewhere?
A triage workflow routes an insurance claim on two signals — a suspicion score and a claim value:
def coverage_triage_block() -> AgentBlock[Any]:
"""An insurance triage workflow with a genuine hole in it.
Moderate suspicion (in ``[0.20, 0.75)``) on a claim under £10,000 matches
none of the three guards -- the gap the coverage analysis finds.
"""
return (
agent()
.given(_.suspicion_score >= probability(0.75))
.then(_.routes @ routes.to(_triage.fraud_review))
.given(
(_.suspicion_score < probability(0.75))
& (_.claim_value >= money_gbp(10_000))
)
.then(_.routes @ routes.to(_triage.underwriter))
.given(
(_.suspicion_score < probability(0.20))
& (_.claim_value < money_gbp(10_000))
)
.then(_.routes @ routes.to(_triage.auto_approve))
.block()
)
…against a declared input contract, which bounds both signals:
def coverage_domain() -> list[object]:
"""The declared input contract: a suspicion probability and a claim value."""
return [
_.suspicion_score >= probability(0.0),
_.suspicion_score <= probability(1.0),
_.claim_value >= money_gbp(0),
]
Question: does every claim consistent with that domain match at least one of the three guards?
Running workflow_coverage_request(triage, domain) through the coverage
analysis:
total = False
status = UNKNOWN
gap = { claim_value: 0, suspicion_score: 1/5 }
It is not total. A claim with moderate suspicion (at or above 0.20, below
0.75) and a value under £10,000 matches none of the three guards, and the
solver hands back exactly such a claim rather than just the word "no" — the
gap above is a real witness, not an illustration. The workflow's own hole,
one specific claim wide, found without anyone having to guess it or write a
test that happened to hit it.
Decision: can two guards fire at once, or can one never fire?
A second triage table, this time routing on amount and on customer intent:
def decision_triage_block() -> AgentBlock[Any]:
"""A ticket router whose two guards overlap.
``amount > £10,000`` and ``intent == "refund"`` are independent conditions,
so a large refund satisfies both -- the overlap the decision analysis finds.
"""
return (
agent()
.given(_.amount > money_gbp(10_000))
.then(_.routes @ routes.to(_router.escalate))
.given(_.intent @ "refund")
.then(_.routes @ routes.to(_router.billing))
.block()
)
Question: can the escalate and billing guards both fire for the same
ticket?
overlaps = guards 0 and 1 co-fire, witness { amount: 10001, intent: "refund" }
unreachable = none
They can. Any refund over £10,000 satisfies amount > £10,000 and has
intent equal to "refund" — it matches both guards, and under a first-match
ordering one of the two branches silently shadows the other. The decision
analysis doesn't resolve which one wins; it surfaces the overlap, with the real
witness input above, so the author decides deliberately rather than by accident
of rule order.
The same primitive also catches the opposite mistake — a guard that can never fire because an earlier, broader guard already claims every input it would match — reported as an unreachable index rather than an overlap.
Invariant: does this transition preserve its obligation?
The obligation balance >= money_gbp(0), checked against three different
preconditions for the same transition:
cases: tuple[tuple[str, list[WireProp]], ...] = (
(
"balance >= money_gbp(100)",
[WireProp(expr=encode(_.balance >= money_gbp(100)))],
),
("(none)", []),
(
"balance <= money_gbp(-1)",
[WireProp(expr=encode(_.balance <= money_gbp(-1)))],
),
)
| Precondition | Verdict | Counterexample |
|---|---|---|
balance >= money_gbp(100) |
VERIFIED |
none |
| (none) | UNKNOWN |
{ balance: -1 } |
balance <= money_gbp(-1) |
FALSIFIED |
{ balance: -1 } |
Three different situations, three different verdicts, none of them collapsed
into a single pass/fail. The first precondition already entails the obligation.
The third makes it impossible outright. The middle row is the one worth sitting
with: nothing bounds balance, so the obligation holds for some states and
fails for others — a genuine gap the analysis makes visible, with the exact
state that breaks it, rather than a false sense of safety from a transition
nobody thought to bound.
Why this matters for agent-generated software
In an agent graph, an LLM picks the next node non-deterministically. A guardrail is a separately verified, symbolic layer over the route guards themselves — it proves, independent of what the model chose, that routing stays total (coverage), unambiguous (decision), and state-safe (invariant). When a property fails, the guardrail hands back the exact ticket or state that breaks it, the same way the examples above do.
The aim is not a more trustworthy agent. It is a smaller trusted surface: constraints authored and analysed independently of whatever implementation produces the routing behaviour. Guardrail Calculus does not today check generated code against the model directly — that connection is intent, not a shipped capability — but the three analyses above are real, and they compose with any router, hand-written or generated, that emits the same guard structure.