Skip to main content

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.

CI PyPI Apache-2.0

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.strongest and Gate.strictest raise only; Fidelity.strictest and CompressionBudget.tighten narrow only. Ordering is deliberately undefined on the enums, so min() raises TypeError rather 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. UNKNOWN deliberately outranks IRREVERSIBLE: 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 --strict in CI. assert_never in exhaustive match statements is only a real exhaustiveness check under strict mode; without it, a new Reversibility variant 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

irredux-0.3.0.tar.gz (85.4 kB view details)

Uploaded Source

Built Distribution

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

irredux-0.3.0-py3-none-any.whl (71.4 kB view details)

Uploaded Python 3

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

Hashes for irredux-0.3.0.tar.gz
Algorithm Hash digest
SHA256 806e8391471b0eb661d5ea016b69d5b0579ca0d16b3fb39fac9031e154fe52bc
MD5 95f09f0afc16f43c56fb5cd07e74c1d0
BLAKE2b-256 cda451d4af78fc2b3d2ecc516a282d3edb47cebec320c1c2bfbb155c62aa8002

See more details on using hashes here.

Provenance

The following attestation bundles were made for irredux-0.3.0.tar.gz:

Publisher: release.yml on rsh1k/irredux

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

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

Hashes for irredux-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c00cbccc4470ed96b1d5155e2b53fa3b7eddddae8d775b4559c849e9200dd969
MD5 f8485dfcd1479ee17f51c03c2d93ae25
BLAKE2b-256 ff297b1aff5a30674548596f22831eed6501625710e5603e34cf29af409f9a57

See more details on using hashes here.

Provenance

The following attestation bundles were made for irredux-0.3.0-py3-none-any.whl:

Publisher: release.yml on rsh1k/irredux

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

Release history Release notifications | RSS feed

0.5.0

2 files

0.4.0

2 files

This release

0.3.0 This release

2 files

0.2.1

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page