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.2.0.tar.gz (80.7 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.2.0-py3-none-any.whl (68.4 kB view details)

Uploaded Python 3

File details

Details for the file irredux-0.2.0.tar.gz.

File metadata

  • Download URL: irredux-0.2.0.tar.gz
  • Upload date:
  • Size: 80.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for irredux-0.2.0.tar.gz
Algorithm Hash digest
SHA256 066d10e562d8707e42a8e9cfacf312ed00444a1f92b4eddcbc0c376630db3d34
MD5 7564fb0bb021566e53fe59c6ee7d1e5b
BLAKE2b-256 c5cbaedf9ff8b9b9964e6f3c3930657af428763e00b983ea0c6e2e402513b4c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for irredux-0.2.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.2.0-py3-none-any.whl.

File metadata

  • Download URL: irredux-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 68.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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6b930c86f118e7398105284ded584146a2cba79c1c83c9209a07c1b5e47afa4d
MD5 c38648e22c90156f33421a36f226e3ca
BLAKE2b-256 d823c117a66a72c23309ef3ffeb26af3504a511824b1d61e8e64d4fb09bf7fd7

See more details on using hashes here.

Provenance

The following attestation bundles were made for irredux-0.2.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

0.3.0

2 files

0.2.1

2 files

This release

0.2.0 This release

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