An agent that fails on step four has already sent the email, pushed the commit and provisioned the box on steps one to three. There is no ROLLBACK for a live API call. We built a one-file, dependency-free saga wrapper that records a compensating action for every tool call and, on failure, unwinds them in reverse. It journals a durable, named undo for each step, so even a process that dies mid-run gets cleaned up afterwards. What it will not do: write your undo logic for you.
The problem nobody rolls back
A database transaction has a ROLLBACK. A sequence of agent tool calls does not. When your agent provisions a server, points DNS at it, warms a cache and then the health check fails, the first three actions are already real. Most agent code handles this with a try and a shrug: log the error, stop, and leave the half-built thing sitting in production for a human to find. The more actions an agent takes on the real world, the more this bites, and agents are taking more actions every month.
The old answer, pointed at agents
Distributed systems solved this shape years ago with the saga pattern. You cannot make five calls to five services atomic, so instead you make each one reversible: for every forward action, define a compensating action that undoes it. If the sequence fails, you run the compensations for the completed steps in reverse order. The booking is cancelled, the charge is refunded, the reservation is released. Nobody pretends it was atomic. It just cleans up after itself.
The same idea maps straight onto agent tool calls, and a handful of projects have started building it: a compensation per tool, run in reverse on failure. We wanted the smallest honest version we could actually drop into the fleet, so we wrote one.
What we built
A Saga you wrap your steps in, one Python file, no dependencies.
Each step runs its action and, on success, records how to undo it.
Use it as a context manager and the behaviour is the part that matters: a clean
exit commits and nothing is undone, but any exception unwinds every completed
step in reverse and then re-raises the original error. The step that actually
failed is not compensated, because its action never finished.
with Saga(journal_path="run.jsonl", registry=HANDLERS) as saga:
saga.step("provision", action=lambda: provision(),
compensate=lambda: deprovision(rid),
comp_name="deprovision", comp_args={"rid": rid})
saga.step("dns", action=lambda: add_dns(rid),
compensate=lambda: del_dns(rid),
comp_name="del_dns", comp_args={"rid": rid})
# clean exit commits; any exception rolls back in reverse
Compensation is best-effort on purpose. If one undo throws, the rest still run, and the failures are gathered into a single error that names them. Undoing four of five steps and telling you the one that needs a hand is strictly better than stopping at the first failed undo and leaving three more side effects live.
The crash case
An exception is the easy case. A crash is the real one. If the process dies,
your in-memory undo closures die with it, and the side effects are still out
there. So every step also journals a named compensation and its arguments to a
file, as plain JSON. A separate recover() reads a journal that
never committed and replays those compensations from a registry of handlers, in
reverse, using nothing but what is on disk. It is idempotent: the journal
records what has already been undone, so a retried recovery does not undo twice.
In the demo a run dies after two steps and a fresh process cleans both up from
the journal alone.
The honest limits
This is not magic and the README says so. It does not make an individual tool call atomic, and it does not retry. If a forward action is itself half-done when it fails, a file partly written, the compensation has to cope with that, exactly as any saga must. And the compensations are yours to write correctly: the wrapper gives you ordering, reversal, best-effort execution and a durable record of what it undid, but a wrong undo is still a wrong undo. It removes the plumbing, not the thinking.
What's now in the stack
A reliability primitive the fleet did not have: risky, hard-to-reverse tool calls can be wrapped so a failed run rolls itself back instead of leaving a half-built mess, and a crashed run is recoverable from its journal. Ten tests, a runnable demo, no dependencies, in the public mirror under MIT.
What is the saga pattern for AI agents?
A reliability pattern where every forward tool call is paired with a compensating action that undoes it. If a multi-step agent run fails partway, the compensations for the completed steps run in reverse order, cleaning up side effects that are already live. It does not make the calls atomic, it makes them reversible.
How do you roll back an agent that has already sent an email or pushed a commit?
You cannot un-send it with a database ROLLBACK, so you register a compensating action per step and run those in reverse when the run fails. For a process that crashes mid-run, journal a named compensation and its arguments to disk so a separate recovery process can replay them from the journal alone.