I Taught an AI Agent How to Undo Its Mistakes

I Taught an AI Agent How to Undo Its Mistakes

I used to think the hardest part of building an autonomous AI agent was getting the model to reason correctly.I was wrong. The harder problem begins after the model has made a decision. An LLM can decide that a customer order should be processed, inventory should be reserved, a payment should be charged, a CRM record should be updated, and a shipping notification should be sent. That sounds straightforward. Until the third API call times out. Then things get interesting. Imagine an AI agent processing a $12,000 enterprise order. It reserves the inventory. The payment succeeds. Then it calls the CRM. The CRM returns a 504 Gateway Timeout. Did the CRM actually reject the request? Or did the CRM update the record successfully, but the response disappear somewhere between the server and the agent? The model doesn't know. The orchestration layer doesn't know. And meanwhile, the customer's credit card has already been charged and the warehouse has already deducted the inventory. That was the problem that pushed me toward building tx-agent: an attempt to bring distributed transaction safety into LLM tool calling. The key idea is surprisingly old. Teach the agent how to undo what it has already done. The Moment I Realized an AI Agent Isn't a Transaction When we give an LLM tools, the programming model looks deceptively simple: Think → Call Tool → Observe Result → Think → Call Tool → ... From the model's perspective, a tool call looks almost like a normal function call. But enterprise APIs aren't normal function calls. A Python function either returns or raises an exception. A remote API can do something much stranger. It can: Receive the request. Modify the database. Process the payment. Begin returning a response. Lose the network connection. Leave the client believing the operation failed. That distinction is critical. A timeout does not necessarily mean that the operation didn't happen. I started thinking about this through a simple five-step order workflow: 1. Reserve inventory 2. Charge payment 3. Update CRM 4. Send shipping notification 5. Emit completion webhook Suppose the first two operations succeed. Then the CRM call fails. A conventional agent has two bad choices. Choice one: Stop The agent crashes or terminates after the exception. Now I have: inventory deducted, $12,000 charged, no CRM order, no shipping notification, no completion webhook. The system isn't merely "failed." It's inconsistent. Choice two: Retry everything The agent sees the error and tries to complete the original task again. It calls the payment API. Again. If the API isn't idempotent, I may now have charged the customer another $12,000. The agent was trying to recover. Instead, it doubled the damage. This is where I stopped thinking about the problem as "LLM error handling." It was a distributed transaction problem. The Old Distributed Systems Idea That Suddenly Made Sense There is a classic pattern for exactly this kind of problem: the Saga pattern. Sagas were introduced by Hector Garcia-Molina and Kenneth Salem in 1987 to handle long-running distributed transactions without requiring one giant global transaction. The basic idea is simple. Instead of trying to make every service participate in one atomic transaction, I define a forward operation and a compensating operation. For example: T1: Reserve Inventory C1: Release Inventory T2: Charge Payment C2: Refund Payment T3: Update CRM C3: Revert CRM Update If everything succeeds: T1 → T2 → T3 → T4 → T5 If T3 fails: T1 → T2 → T3 ❌ ↓ C2 → C1 The rollback happens in reverse order. That's important because distributed operations have dependencies. If I charged a customer after reserving inventory, I shouldn't release the inventory and leave the payment charged. The compensation sequence should unwind the completed work in LIFO order. The Saga pattern gave me the foundation. But applying it to an LLM agent introduced another set of problems. LLMs Make the Problem Harder Traditional Saga orchestrators usually operate around relatively deterministic workflows. LLM agents don't. An LLM decides which tool to call. It generates the arguments. It can make mistakes. It can retry. It can misunderstand an error. It can hallucinate state. And, perhaps most importantly, it carries its own representation of the world inside its context window. That created three problems I had to solve. Problem 1: The Agent Can Retry Forever Suppose the agent generates: { "amount": "twelve thousand dollars" } when the API expects: { "amount": 12000 } The validation layer rejects the request. A naïve agent architecture sends the error back to the LLM: Validation failed. Please try again. The model generates another payload. Maybe it works. Maybe it doesn't. If the architecture keeps feeding errors back into the model, I can end up with a surprisingly expensive loop: LLM ↓ Invalid tool call ↓ Validation error ↓ LLM ↓ Invalid tool call ↓ Validation error ↓ LLM ↓ ... The model burns tokens without making progress. So I introduced a much stricter boundary. Validation gets one bounded retry. If the corrected request still fails validation, I don't let the model keep guessing. The transaction stops. If previous operations have already succeeded, the Saga begins compensating them. That changes the philosophy from: "Keep asking the model until it gets it right." to: "Give the model one structured opportunity to recover, then fail safely." That's a much healthier failure mode for enterprise systems. Problem 2: The Model Can Believe Something That Is No Longer True This one initially felt even stranger. Imagine the agent successfully executes: Charge $12,000 The tool returns: PAYMENT CONFIRMED That message goes into the model's conversation history. Later, the CRM operation fails. The Saga compensates the payment: REFUND $12,000 The external system now says: Payment = Refunded But the model's context may still contain: Payment = Confirmed From the model's perspective, the payment was successful. From the actual system's perspective, the payment was reversed. Those are two different realities. And if the agent continues reasoning from stale context, it can make the wrong decision. It might say: "The payment has already been processed, so we can continue." No. It has been refunded. The agent's memory is now lying to it. That led me to a second principle: When the physical state of the system rolls back, the agent's context needs to roll back too. I implemented context memory pruning so that when a Saga compensates an operation, the corresponding forward tool dispatches and observations can be removed from the active reasoning context. The goal isn't to erase history for the sake of cleanliness. The goal is to keep the model's working state synchronized with external system state. If the payment has been refunded, the model shouldn't continue reasoning as though the payment is still charged. Problem 3: A Timeout Doesn't Mean Nothing Happened This is probably the most dangerous failure mode. Consider this request: POST /charge The server receives it. The server charges the card. Then something happens to the network. The client receives: Timeout The agent concludes: Payment failed. So it retries. Now the server receives the same request again. Without idempotency, I have: Charge #1 → $12,000 Charge #2 → $12,000 The customer has been charged $24,000. The agent wasn't malicious. It simply couldn't distinguish: The operation failed from: The operation succeeded but the response failed This is why I added deterministic idempotency keys. For each tool invocation, the framework can derive a SHA-256 hash from the tool name and its arguments. Conceptually: idempotency_key = SHA256(tool_name + serialized_arguments) If the same operation is retried with the same arguments, it produces the same identity. The downstream service can use that key to deduplicate the request. This doesn't magically make every third-party API idempotent. But it gives the architecture a mechanism for enforcing idempotency wherever the receiving service supports it. Turning the Idea Into Code Once I had these principles, I wanted to make the transaction boundary explicit in Python. That became tx-agent. The basic abstraction is a Saga-aware tool. A forward operation gets paired with a compensation operation. For example: from tx_agent import saga_tool def undo_charge_payment(user_id: str, amount: float, result=None): # Compensating transaction C2 return { "status": "REFUNDED", "amount": amount, "user_id": user_id } @saga_tool( name="charge_payment", compensate=undo_charge_payment ) def charge_payment(user_id: str, amount: float): # Forward transaction T2 return { "charge_id": f"chg_{user_id}", "amount": amount, "status": "CONFIRMED" } The important part isn't the decorator syntax. It's the contract. I'm explicitly telling the system: If this operation becomes part of a transaction and something later fails, this is how you compensate it. That gives the orchestrator information it otherwise wouldn't have. The Rollback Stack Inside the transaction engine, successful operations are pushed onto a rollback stack. Conceptually: Forward execution T1 → success ↓ stack T2 → success ↓ stack T3 → failure The stack contains the operations that need compensation. So the engine unwinds them: C2 ↓ C1 This is deliberately simple. I don't need a complicated rollback graph for a linear Saga. I need a reliable record of: which operation ran, what arguments it received, what it returned, which compensation belongs to it, and in what order compensation should occur. The result is a transactional boundary around otherwise non-transactional tool calls. Adding Pydantic Validation I also wanted validation to happen before a tool could modify external state. That led me to use Pydantic V2 models through TypeAdapter. The important architectural distinction is: LLM ↓ Tool-call payload ↓ Schema validation ↓ Precondition validation ↓ External side effect rather than: LLM ↓ External side effect ↓ Oops, the arguments were invalid The first architecture prevents many errors from becoming transactional failures in the first place. Pydantic V2 was useful here because its validation core is implemented in Rust and is designed for high-performance data validation. For an agent system that may make many tool calls, keeping this boundary lightweight matters. The Architecture I Ended Up With The resulting architecture has several pieces working together. 1. Saga Tool Registry The @saga_tool decorator connects a forward operation with its compensation. 2. Saga Execution Engine SagaEngine tracks successful operations and maintains the LIFO rollback stack. 3. Tool Call Validator Pydantic V2 validates tool arguments before the tool reaches the external system. 4. Context Memory Synchronization When an operation is compensated, corresponding stale tool observations can be removed from the active model context. 5. Pre/Postcondition Validation A separate validation layer can check whether the system is in an appropriate state before execution and whether the resulting state satisfies the expected conditions afterward. The architecture therefore looks less like a simple agent loop and more like: ┌─────────────────┐ │ LLM │ └────────┬────────┘ │ Tool invocation │ ▼ ┌─────────────────────┐ │ Schema Validation │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Preconditions │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Saga Engine │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ External Tool/API │ └──────────┬──────────┘ │ Success? / \ Yes No │ │ ▼ ▼ Push to Roll back stack LIFO stack │ │ └────┬─────┘ ▼ Synchronize Context The important shift is that tool execution becomes transactional state management, rather than simply another message in the agent loop. Then I Wanted to Know: How Much Does This Cost? Architecture is one thing. Performance is another. So I ran concurrent workflows using Python's asyncio runtime with simulated 10 ms API latency per microservice. The benchmark used 50 concurrent workflows. The results were: Metric Result Concurrent workflows 50 Duration 1.529 s Throughput 32.71 workflows/sec Successful workflows 50/50 Mean latency 30.55 ms P95 latency 1.49 ms The important observation for me wasn't simply the throughput. It was the relative overhead of the transaction machinery. The in-memory Saga stack, SHA-256 idempotency hashing, and local Pydantic validation are tiny compared with the latency of real external services, where network calls can easily take tens or hundreds of milliseconds. In other words, the safety mechanisms don't have to become the bottleneck. That matters because one of the easiest excuses for not adding reliability infrastructure is: "It will make the agent too slow." In many architectures, the external APIs are already dominating the latency budget. What Changed in My Mental Model of AI Agents Building this system changed the way I think about autonomous agents. Initially, I thought about an agent as a reasoning loop: Prompt ↓ Reason ↓ Tool ↓ Observation ↓ Reason Now I think about it more like a distributed system: Intent ↓ Validated action ↓ Transactional side effect ↓ Durable state ↓ Compensation if necessary ↓ Synchronized agent context The LLM is still the reasoning engine. But it shouldn't be the transaction manager. That's an important distinction. We shouldn't expect a probabilistic model to provide guarantees that belong in deterministic infrastructure. The model can decide: "Charge this customer." The transaction layer should decide: "Under what conditions is that action allowed, how do I record it, how do I prevent duplicate execution, and what do I do if the next operation fails?" Those are fundamentally different responsibilities. The Bigger Lesson: AI Agents Need Distributed Systems Engineering As AI agents move from answering questions to taking actions, the consequences of failure change dramatically. A chatbot that hallucinates a product description is annoying. An agent that hallucinates whether a payment succeeded is dangerous. An agent that retries a non-idempotent API call can create real financial damage. An agent that remembers a transaction after it has been rolled back can make subsequent decisions using a fictional system state. These aren't merely prompt-engineering problems. They're distributed systems problems. That's why I think some of the most important techniques for production AI agents may come from software engineering disciplines that existed long before LLMs: transactions, idempotency, compensation, validation, durable state, concurrency control, observability, failure recovery, and consistency models. The interesting part is that LLMs don't replace these ideas. They make them more important. Where I Think Agent Architecture Is Going I don't think the future of autonomous agents is simply: Bigger models + more tools. I think it is closer to: Better models + deterministic safety boundaries + transactional infrastructure. The model should be allowed to reason probabilistically. The infrastructure around it should provide deterministic guarantees wherever possible. That means an enterprise agent shouldn't simply have access to: charge_payment() It should have something closer to: validate() → authorize() → execute() → record() → compensate_if_needed() → synchronize_context() The LLM doesn't need to understand every implementation detail. It just needs a reliable environment in which its actions can be executed safely. That's the distinction between an AI demo and an AI system that I would trust with production infrastructure. Final Thought The most important thing I learned while building tx-agent wasn't how to make an LLM better at recovering from errors. It was realizing that the LLM shouldn't be responsible for recovering from every error in the first place. When an agent interacts with the real world, failure is inevitable. Networks fail. APIs time out. Tokens expire. Databases reject writes. Models generate invalid arguments. Servers process requests but lose responses. The goal isn't to pretend these things won't happen. The goal is to build an architecture where failure doesn't automatically become corruption. For me, the Saga pattern provided the missing mental model. The agent can move forward. The system records what happened. If something goes wrong, the system knows how to move backward. And most importantly, the agent's memory can be brought back into agreement with reality. That is the direction I believe production-grade AI agents need to move toward: not agents that never fail, but agents whose failures are bounded, observable, reversible, and safe. About tx-agent tx-agent is an open-source Python reference implementation exploring transactional safety for LLM tool calling, including Saga-style compensating transactions, idempotency, Pydantic V2 validation, and context synchronization. The original implementation and references are listed in the accompanying project material. https://github.com/aibysid/tx-agent By Siddharth Chauhan, Computer Scientist at Adobe.

Original Source

Read the full article at Hackernoon →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.