An undo system for AI agents, built on the saga/compensating-transaction pattern.
Every tool call an AI agent makes is logged as a saga step with a paired compensating action. If a task fails, or you just want to bail, the orchestrator walks the log backward and runs compensations in reverse order — full rollback, or partial rollback to any checkpoint.
A single LLM call is easy to retry — throw away the response, ask again. A multi-step agent task is not: by the time you notice something went wrong, the agent may have already written a database row, created a file, and sent an email. There's no built-in "undo" for a sequence of real side effects across multiple systems.
Distributed systems solved a version of this problem decades ago with the saga pattern: instead of a single ACID transaction spanning every service (usually impossible across independent systems), each step gets a paired compensating action, and a failure anywhere triggers those compensations in reverse. This project applies that same idea to an agent's tool-calling loop — every tool call is a saga step, and the tool registry defines its compensation up front.
agent.py Claude tool-use loop. Never calls tools directly —
every tool_use block is routed through the orchestrator.
registry.py ToolSpec per tool: forward_fn, compensate_fn (or
irreversible=True), capture_state_fn (snapshots what the
compensation will need, taken *before* forward_fn runs).
saga_log.py Append-only SQLite table (sandbox/saga.db, saga_steps).
Rows are never mutated except to advance `status`:
pending -> committed -> compensated (or failed /
awaiting_confirmation).
orchestrator.py execute_step(): write pending row -> run forward_fn ->
mark committed with result. rollback(): read steps for a
saga_id in reverse, run compensate_fn using the captured
state + recorded result, mark compensated. Halts with a
clear error the moment a compensation itself fails —
it never silently skips ahead.
db.py The sandboxed "real world": a SQLite business database
(users/outbox/payments/webhook_log) and a filesystem
directory, both confined to ./sandbox.
api.py (FastAPI) POST /saga/run, GET /saga/{id}/steps,
POST /saga/{id}/rollback (?to_step=N for partial),
POST /saga/{id}/confirm_irreversible.
frontend/ React + Vite. Live saga timeline, a "Rollback to here"
button per committed step, and a confirmation modal for
irreversible actions.
Idempotency. Compensating functions check the actual state before
acting (e.g. "is this file still there?", "is this payment still charged?")
rather than assuming they're running for the first time — so re-running a
rollback on an already-compensated saga is a safe no-op. The orchestrator
also only ever compensates steps still marked committed, which is a second
line of defense against double-compensation.
Not all actions can be undone the same way, so the registry groups tools into three categories that each compensate differently:
| Category | Tools | Compensation |
|---|---|---|
| Reversible | create_file, write_db_row, rename_file |
The compensation exactly undoes the forward action — delete the file, delete the row, rename back. State fully returns to how it was. |
| Compensable but not reversible | send_email, charge_card |
The forward effect already left the sandbox (an email was "sent", a card was "charged") and can't be un-sent or un-charged. The compensation instead issues a corrective follow-up action — a correction email, a refund — that neutralizes the effect going forward without pretending the original action never happened. |
| Irreversible | permanently_delete_file, call_external_webhook |
No compensation exists at all (compensate_fn=None). These are flagged irreversible=True in the registry and the orchestrator refuses to execute them until the API/UI receives an explicit confirmation via POST /saga/{id}/confirm_irreversible. Rolling back a saga that contains a committed irreversible step is impossible by design — rollback() raises CompensationError and halts rather than skipping past it. |
This taxonomy is why "undo" can't be one generic mechanism: reversible actions get a true inverse, compensable actions get a corrective action, and irreversible actions get a hard gate instead of a promise the system can't keep.
Requires Python 3.11+ and Node 18+.
# Backend
python -m venv .venv
.venv\Scripts\activate # or `source .venv/bin/activate` on macOS/Linux
pip install -r requirements.txt
export ANTHROPIC_API_KEY=sk-ant-... # only needed to run the live agent loop
# Frontend
cd frontend
npm install# Terminal 1 — backend (creates ./sandbox on first run)
uvicorn backend.api:app --reload
# Terminal 2 — frontend
cd frontend
npm run devOpen the printed Vite URL (default http://localhost:5173). Type a task
("Onboard a new user named Ada Lovelace...") and click Run task — the
agent will call write_db_row, create_file, and send_email in sequence,
each showing up in the timeline as it commits. Click Rollback to here on
any committed step for a partial rollback, or Rollback entire saga to
undo everything.
The demo drives the orchestrator directly with the exact tool calls the "onboard a new user" task makes, so it's deterministic and doesn't need a live API key:
python -m demo.run_demoIt creates a user row and a welcome file, simulates an interruption before the welcome email step, rolls the saga back, and asserts — against the real SQLite database and filesystem, not just an HTTP 200 — that both the row and the file are gone.
python -m backend.test_orchestratorCovers full rollback, partial rollback to a checkpoint, rollback idempotency, the irreversible-confirmation gate, and that a failed compensation halts the rollback instead of silently continuing past it.
This is a v1 built to demonstrate the pattern, not a production saga engine. Known gaps:
- Third-party-observed effects. If
send_emailactually left the sandbox and a real human read it before the compensation ran, no amount of "correction email" un-reads it. Compensation can correct the system's own record of what happened; it can't reach into a human's inbox or memory. The same applies to any effect a downstream system reacted to before the rollback caught up (e.g. a webhook that triggered another service's own irreversible action). - Truly irreversible real-world actions. The registry can flag an action as irreversible and gate it behind confirmation, but it can't stop the agent from causing real, permanent, external harm once that confirmation is granted. The safety here is entirely in the confirmation step being taken seriously — the system has no way to verify that in the real world.
- Single-process, single-database sagas only. The saga log and the sandbox live in one SQLite file each, executed synchronously in one process. A real multi-agent or multi-service saga — steps executed by different agents, on different machines, against different databases — needs distributed coordination (a saga step that's "committed" locally but whose compensation message never reaches a remote worker, partial network failures mid-compensation, etc.) that this project doesn't attempt.
- No resumption after an irreversible confirmation.
confirm_irreversibleexecutes that one step, but the agent's multi-turn conversation loop doesn't resume automatically afterward — the demo scenario doesn't need this since it only uses reversible/compensable tools, but a real irreversible workflow would need that resumption built out. - No auth, no persistence beyond SQLite/the filesystem, no concurrent sagas sharing state. Out of scope for v1 by design — see the project brief.