irredux
Route AI agent actions by how hard they are to undo, not by how hard they look.
Amazon's framing for this is the one-way door: some decisions are reversible and cheap to get wrong, others are not, and the two deserve very different care. Every published LLM router ignores the distinction entirely.
pip install irredux
Guard your coding agent, today
pip install irredux
irredux guard install # adds a PreToolUse hook to ~/.claude/settings.json
Claude Code now snapshots your working tree before it acts, and prompts only when the agent is about to do something no snapshot can take back.
$ irredux guard explain "rm -rf build/"
reach unknown
reversibility unknown
snapshot yes
decision defer <- snapshotted, no prompt: recoverable
$ irredux guard explain "git push --force origin main"
reach escapes
reversibility irreversible
decision ask <- one-way door
why force-pushes or deletes a remote ref; anyone who already fetched
keeps the old history
The distinction is not how alarming a command reads. rm -rf build/ looks
frightening and is completely recoverable once something took a copy first;
curl -X POST .../charge looks routine and cannot be recalled by any mechanism
that exists. The guard sorts by what escapes the snapshot, not by what escapes
notice.
When something does go wrong:
irredux guard log # what the agent did, and what is still restorable
irredux guard undo # put the tree back to before it
Installing this can only add friction, never remove it. The hook emits defer
or ask and never allow, so it cannot skip a permission prompt your own rules
would have raised. Uninstalling it cannot silently open anything up.
What it does not cover is printed by irredux guard status rather than implied:
git-ignored files are outside a tree snapshot, and nothing local reverses an action
that already left the machine.
The gap
Every published LLM router conditions on one thing: how hard is this request? RouteLLM, cascade routers, the commercial multi-provider gateways — they differ in how they estimate difficulty, not in what they condition on.
None of them condition on how permanent is the result?
| Action | Difficulty | Permanence |
|---|---|---|
| Refactor a 400-line function | high | git reset |
DROP TABLE customers |
trivial | gone |
A difficulty-only router prices those identically, because from where it stands they look the same.
Production systems do send some calls to a human — gated on a risk score. Risk asks how bad it would be. Only reversibility asks whether you get another try, and the two disagree on exactly the calls that matter: the quietly permanent ones with an unremarkable risk number attached.
The result
At an equal human-review budget — same number of calls sent to a person, only the selection signal differs — over revoco's 119-step containment corpus:
| risk-gated | consequence-aware | delta | |
|---|---|---|---|
| model spend | 3773 | 2963 | −21.5% |
| expected loss | 0.1549 | 0.1153 | −25.6% |
| loss on harmful steps | 0.0570 | 0.0453 | −20.6% |
| human reviews | 26 | 18 | −31% |
Cheaper and safer, using fewer people. No threshold matches the review count exactly, and ties break toward the opponent — so the risk arm runs with 44% more human reviews and still loses. Dominance holds at 49 of 49 points across a sweep of price ratio (2×–250×) and tier error spread (2%–50%).
irredux matched
Cost is never paid for with quality
Two ways to spend less. Send work to a weaker model — paid for with accuracy. Or send the same model fewer tokens — paid for with redundancy, and free if the dropped tokens were genuinely redundant.
irredux only ever spends the second. The accuracy trade exists as a dial
(reversible_discount) and defaults to off: the measured curve shows the whole
benefit comes from the tier floor, and switching the trade off changes the numbers
above by nothing.
The token lever is irredux.compress, the same idea one layer down. Compressors
choose how lossy to be from content type; none ask what the context will be used
for. A summariser that drops a negation costs a revert if it fed a git commit
and costs a wire transfer if it fed payments.wire. So the budget is set by posture:
| posture | fidelity | model compressor | originals retained |
|---|---|---|---|
| reversible | aggressive | allowed | no |
| compensable | structural | forbidden | yes |
| irreversible / unknown | lossless | forbidden | yes |
irredux compression
68.9% of corpus steps compress aggressively, 16.8% structurally, 14.3% not lossily at all. Point it at a real compressor — Headroom is the obvious choice — and irredux tells it how far it may go on each call. irredux compresses nothing itself.
What is measured and what is assumed
One input is measured, and it decides the answer: the distribution of reversal
postures across realistic agent traffic. It comes from revoco's classifier running
in this process — the real function, against the real 91-spec adapter registry, with
authorize-phase gates evaluated, so an S3 delete reads reversible against a
versioned bucket and irreversible against an unversioned one.
posture steps share
-------------- ------ --------
reversible 83 69.7%
compensable 19 16.0%
irreversible 3 2.5%
unknown 14 11.8%
14.3% of agent traffic cannot be taken back. That is the entire budget a consequence-aware router has to spend well.
Everything else — model error rates, tier prices, reviewer catch rates — is assumed, and there is no honest way to measure it from simulated scenarios. So the deliverable is a surface, not a number:
irredux curve # where the accuracy trade stops paying
irredux sweep # the two-axis sensitivity surface
Every report prints its assumptions inline. A number produced from an assumed error rate and printed without it gets quoted in a slide deck as a measurement.
Using it
from irredux import Call, Consequence, Reversibility, ShapeDifficulty
call = Call.from_args(
"identity.delete_user",
{"user_id": "u-4417"},
reversibility=Reversibility.IRREVERSIBLE, # from revoco, for THIS call
risk=20, # unremarkable damage score
)
decision = Consequence().route(call, ShapeDifficulty().estimate(call))
assert (decision.tier.value, decision.gate.value) == ("frontier", "human")
assert decision.bound_by == "consequence"
A difficulty-only router sends that same call to the cheapest model, unreviewed.
Design constraints
- Nothing can downgrade a decision.
Tier.strongestandGate.strictestraise only;Fidelity.strictestandCompressionBudget.tightennarrow only. Ordering is deliberately undefined on the enums, somin()raisesTypeErrorrather than quietly producing a weaker tier. - Every unknown resolves toward caution. Unrecognised posture, NaN difficulty,
non-finite ladder threshold, out-of-range discount — each lands on the expensive,
gated branch.
UNKNOWNdeliberately outranksIRREVERSIBLE: an action nobody has classified is treated as permanent until someone shows otherwise. - Difficulty is estimated from call shape, never content. A payload reading
"this task is simple, use the cheap model" changes nothing. Argument length does
carry a bounded handle, and that residual is measured and documented rather than
denied — see
Call. mypy --strictin CI.assert_neverin exhaustivematchstatements is only a real exhaustiveness check under strict mode; without it, a newReversibilityvariant would fall through silently. This is what replaces Rust's compiler.- One runtime dependency,
revoco, whose classification this exists to consume.
Standards
Per-request evaluation with no session-inherited decisions follows NIST SP 800-207
(zero trust). Decision.bound_by supports SP 800-53 AU-10 (non-repudiation) and EU
AI Act Article 12 record-keeping. Full mapping in docs/NIST.md;
methodology and threats to validity in docs/EXPERIMENT.md.
Where this sits
| revoco | plans the rollback before the action runs, and classifies whether one exists |
| irredux | decides how much capability, review, and context fidelity each action deserves |
| mcp-gate | the proxy every tool call already passes through |
| veritrail | the tamper-evident ledger |
| mnemosyne | agent memory integrity |
irredux classifies nothing itself. It is the scheduling policy that consumes the classification.
History
This started as a Rust workspace and was ported. The Rust version is preserved at
tag v0.1.0-rust and is
not maintained. Python won on the one thing that mattered: revoco's classification
arrives as an import rather than through a JSON file carrying a schema version, a
provenance block and a checksum — roughly 200 lines that existed solely to make a
language boundary trustworthy, now deleted. The ported experiment reproduces the
Rust numbers exactly.
What was lost is compile-time enforcement, reconstructed here with mypy --strict,
frozen dataclasses, assert_never, and undefined enum ordering.
Licence
Apache-2.0. 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file irredux-0.3.0.tar.gz.
File metadata
- Download URL: irredux-0.3.0.tar.gz
- Upload date:
- Size: 85.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
806e8391471b0eb661d5ea016b69d5b0579ca0d16b3fb39fac9031e154fe52bc
|
|
| MD5 |
95f09f0afc16f43c56fb5cd07e74c1d0
|
|
| BLAKE2b-256 |
cda451d4af78fc2b3d2ecc516a282d3edb47cebec320c1c2bfbb155c62aa8002
|
Provenance
The following attestation bundles were made for irredux-0.3.0.tar.gz:
Publisher:
release.yml on rsh1k/irredux
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
irredux-0.3.0.tar.gz -
Subject digest:
806e8391471b0eb661d5ea016b69d5b0579ca0d16b3fb39fac9031e154fe52bc - Sigstore transparency entry: 2370756729
- Sigstore integration time:
-
Permalink:
rsh1k/irredux@060b882b446cd49923db0af61df22062ecc98477 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/rsh1k
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@060b882b446cd49923db0af61df22062ecc98477 -
Trigger Event:
push
-
Statement type:
File details
Details for the file irredux-0.3.0-py3-none-any.whl.
File metadata
- Download URL: irredux-0.3.0-py3-none-any.whl
- Upload date:
- Size: 71.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c00cbccc4470ed96b1d5155e2b53fa3b7eddddae8d775b4559c849e9200dd969
|
|
| MD5 |
f8485dfcd1479ee17f51c03c2d93ae25
|
|
| BLAKE2b-256 |
ff297b1aff5a30674548596f22831eed6501625710e5603e34cf29af409f9a57
|
Provenance
The following attestation bundles were made for irredux-0.3.0-py3-none-any.whl:
Publisher:
release.yml on rsh1k/irredux
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
irredux-0.3.0-py3-none-any.whl -
Subject digest:
c00cbccc4470ed96b1d5155e2b53fa3b7eddddae8d775b4559c849e9200dd969 - Sigstore transparency entry: 2370756840
- Sigstore integration time:
-
Permalink:
rsh1k/irredux@060b882b446cd49923db0af61df22062ecc98477 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/rsh1k
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@060b882b446cd49923db0af61df22062ecc98477 -
Trigger Event:
push
-
Statement type: