Skip to main content

Command Palette

Search for a command to run...

Reasoning Is Cheap, Side Effects Are Forever

Why safe agentic AI is a distributed systems problem we already solved

Updated
โ€ข15 min read
Reasoning Is Cheap, Side Effects Are Forever
A
AI Architect. I design the boring control plane plumbing that keeps your impressive demos from quietly setting themselves on fire in production.

๐Ÿ—“๏ธ Last updated: June 2026. Reflecting current tool use patterns and saga based agent architectures.

There's a quiet failure mode buried in every agentic system that touches the real world:

The agent's reasoning can be retried. The world cannot.

If your agent books the wrong flight, you can't unbook it cleanly. If it sends the wrong email, the email is already in someone's inbox. If it charges a card twice during a retry storm, refunds become a customer service problem, and customer service rarely thanks engineering for the extra volume.

Everything works until the network blinks. Then it doesn't.

The reframe that makes everything easier, and the reason this article exists, is that we've solved this problem before. Distributed systems engineers have spent decades building patterns for safe side effects in unreliable environments. The agentic AI era doesn't need new fundamentals. It needs to remember the ones we already have.

The Two Categories of Tool Calls

Most agent frameworks treat tool calls as a uniform abstraction. That's the root design error.

There are two fundamentally different categories:

  1. Read only calls: querying a database, fetching a document, listing items. Safe to retry. Safe to replay. Safe to call concurrently.

  2. Mutating calls: charging a card, sending an email, booking a flight, deleting a record, posting to an external API. Every retry is a potential bug. Every concurrent invocation is a potential race condition.

A multi turn agent can issue many tool calls per session, often dozens, sometimes more. If you don't separate these two categories at the architecture level, you've made the entire system unsafe by default.

Idempotency: The First Principle of Safe Mutations

The simplest and most useful guarantee in the distributed systems toolkit is idempotency.

A tool call is idempotent if running it twice has the same effect as running it once. Formally: f(f(x)) = f(x).

Why this matters for agents:

  • Agent frameworks often retry tool calls automatically when a response fails to parse, times out, or returns an error

  • Network failures cause clients to retry without knowing whether the server succeeded

  • Multi agent systems may invoke the same action through different paths

  • Failover and replay during recovery may re issue calls

Without idempotency, every one of these creates a duplicate side effect.

Patterns that make a tool idempotent

  • Idempotency keys. The client sends a unique key with the request. The server stores recent keys and dedupes. Stripe's payments API documented this pattern at industry scale, and it has since become a standard convention across SaaS APIs.

  • Conditional writes (CAS). "Update X only if its current value is Y." Most modern databases (DynamoDB, etcd, Postgres with row versioning) support this directly.

  • Deterministic IDs. Generate the resource ID from a hash of the request fingerprint. Re issuing the same request hits the same ID, and the create becomes an upsert.

  • Dedupe windows. Store recent request fingerprints with a TTL. Drop duplicates within the window.

Idempotency doesn't mean "safe"; it means "retry safe." A retry safe destructive action is still destructive. But retry safety eliminates the entire class of failure then duplicate bugs.

If you're building agentic AI today and your tool layer doesn't enforce idempotency, start there.

Where the idempotency key must live

There's a subtle trap here that defeats naive implementations: the scope and ownership of the key.

"One key per logical operation" sounds simple, but the hard question is who mints the key and where it lives. If the key is generated inside the LLM's context, embedded in the reasoning trace, then a context window summarization or truncation can silently drop it. The next attempt mints a fresh key, the dedupe layer sees a new operation, and you've doubled the side effect. This is exactly the "phantom retry" failure mode described below, and it's the most common way idempotency layers fail in practice.

The rule: the idempotency key must be minted and persisted outside the model, by the tool invocation middleware keyed to a stable logical operation ID, so that it survives context summarization, session replay, and failover. The model may reference the operation; it must never be the sole keeper of the key.

The Saga Pattern: When You Can't Roll Back

Idempotency handles individual mutations. But agents rarely act in isolation; they execute multi step workflows where every step has side effects.

Consider an agent booking travel:

  1. Search flights

  2. Reserve seat on flight

  3. Charge customer card

  4. Reserve hotel room

  5. Send confirmation email

If step 4 fails, what happens to steps 2 and 3? You can't roll back the card charge with a database ROLLBACK. You can't unreserve the seat with a transaction abort. The world doesn't have ACID, and it will not be acquiring it for you.

This problem isn't new. Hector Garcia Molina and Kenneth Salem named it in 1987 in a paper titled Sagas, proposing that a longrunning workflow be broken into a sequence of local transactions, each with a compensating action that semantically undoes it.

  • Release the seat reservation

  • Refund the card

  • Cancel the hotel hold

  • Send a cancellation email instead of a confirmation

That's not rollback. That's forward recovery: executing a sequence of compensating actions to bring the system back to a consistent state.

Agentic multi step workflows are sagas. Most teams writing them today don't frame them that way. The pattern, the literature, and the failure modes have been studied for nearly four decades. Use them.

Choreography vs orchestration

Sagas come in two flavors:

  • Choreography: each step emits an event; the next step listens. Decentralized. Fits naturally into eventdriven architectures.

  • Orchestration: a central coordinator sequences each step explicitly. Easier to reason about, easier to debug.

For agents, orchestration is almost always the right default, but be precise about who the coordinator is. It is tempting to say "the LLM is already the coordinator, so let it drive the saga." That's a mistake, and it contradicts the core thesis of this article: the model is a probabilistic component that may issue duplicate or outoforder calls. You do not want a stochastic component owning compensation ordering.

The correct division of labor: the LLM emits intent ("execute this saga plan"), and a deterministic saga executor owns sequencing, persistence, and the order in which compensations run. The model proposes; the deterministic envelope disposes. Give the LLM visibility into the saga's state and compensations so it can reason about them, but never let it be the thing that remembers what compensation is owed.

When compensations themselves fail

There's a gap most saga writeups skip, and it's the one that bites hardest in incident response: compensating actions can fail too. Refunds time out. Cancellation APIs return 500. The seatrelease endpoint is down for maintenance at the exact moment you need it.

Design for this upfront:

  • Make compensations idempotent and retryable. A compensation is itself a mutating tool call, so it needs the same idempotency contract as the forward action, so retrying a refund doesn't doublerefund.

  • Persist "compensation owed" state durably. The obligation to compensate must outlive the saga executor process, the agent session, and any single retry attempt. Write it to the event log before attempting the compensation, not after.

  • Provide a deadletter / escalation path. When a compensation exhausts its retries, the saga doesn't get to silently give up. It routes to a durable failure queue and escalates to a human. An inconsistent system that knows it's inconsistent is recoverable; one that has forgotten is not.

A saga that only handles the happypath failure (forward step fails, compensation succeeds) is a demo. A saga that handles compensationfailure is production.

The Compensation Catalog

For every mutating tool in your agent's catalog, you need three things documented upfront:

  1. The forward action: what the tool does

  2. The compensating action: how to undo or neutralize its effect

  3. The idempotency contract: how the tool handles repeat invocations (for both the forward action and its compensation)

If a tool has no compensating action (an irreversible operation like sending an email or firing a webhook), that must be explicitly declared in the catalog. It changes how the agent is allowed to use it.

This catalog isn't optional metadata. It's the safety contract that makes the agent's actions auditable, recoverable, and reasonedabout. Treat it like a database schema: versioned, reviewed, and tested.

In the previous article in this series, I called this procedural memory, the tier of agent memory that stores tool definitions and capabilities. The compensation catalog lives there.

Failure Modes That Show Up in Agent Side Effects

LLM agents inherit most of distributed systems' classical failure modes, and amplify them, since the orchestrator is now a probabilistic component that may issue duplicate or outoforder calls. Worth naming the ones you'll meet most often:

Partial execution

The agent calls a tool. The network drops before the response. Did the action happen? If you don't know, you must assume it might have, and design the next step to be safe whether or not it did.

Phantom retries

The model decides on its own to retry a tool call because it didn't "see" the result (helpfulness being, in this one case, a liability). Without an idempotency key shared across both attempts, and crucially one minted outside the model so a context summary can't drop it, you've doubled the side effect.

Cascade abandonment

The agent runs a 5step saga. Step 3 fails. The model's context summary discards the saga state. The agent forgets that compensations are owed, and the system silently stays inconsistent. (This is precisely why "compensation owed" must be persisted by the deterministic executor, not held in the model's context.)

Staleread divergence

The agent reads external state, reasons over it, and writes back, but the state changed between the read and the write. Without optimistic concurrency control, the agent overwrites a newer update with stale data.

Concurrentagent contention

Two agent instances (or two sessions) act on the same external resource simultaneously. Without locks, leases, or partition keys, the result is nondeterministic in the worst possible way.

These aren't theoretical. They surface predictably in any system that combines unreliable orchestration with external side effects, which is exactly what an agent is. Plan for them upfront; don't discover them under incidentresponse pressure.

Patterns That Actually Work

Pattern 1: Tool Risk Classification

Tag every tool in the catalog with a risk class:

| Class | Semantics | Required guard | | | | | | Read | No side effects | Retry freely | | Softmutate | Idempotent mutation (with key) | Idempotency layer | | Hardmutate | Nonidempotent but recoverable | Saga + compensation | | Irreversible | No compensation possible | Confirmation buffer, then HITL / explicit confirmation |

This classification is the foundation that every safety pattern below builds on. Without it, every other guard is bolted on randomly.

Pattern 2: The Idempotency Layer

A thin middleware between the agent and any softmutate tool that:

  • Mints or accepts an idempotency key per logical operation, and owns that key itself, outside the model's context, so it survives summarization and replay

  • Stores the (key โ†’ outcome) mapping for a bounded window

  • Returns the cached outcome on repeat invocations rather than reexecuting

This is one of the highestleverage components you can build. The implementation is small; the failure class it eliminates is large.

Pattern 3: The Saga Executor

A small deterministic orchestrator that sits between the agent's intent and the tool layer:

  • Accepts a saga plan (sequence of steps with compensations) emitted by the LLM

  • Owns sequencing: the model proposes the plan, the executor decides the order of execution and compensation, not the model

  • Executes forward, persisting state after each step

  • On failure, runs compensations in reverse order, treating each compensation as an idempotent, retryable mutation

  • Escalates to a deadletter queue and a human when a compensation exhausts its retries

  • Logs every attempt and outcome to the agent's event log

The saga executor lets the agent reason about intent while the executor handles execution safety. Separating those two concerns, probabilistic intent from deterministic execution, is the design move.

Pattern 4: DryRun and Preview Modes

For highstakes tools, build a dryrun mode that returns "what would happen" without executing. The agent can chain dryruns to validate a plan before committing. As a bonus, this is what makes good explainability possible: show the user the dryrun trace before triggering the real one.

Pattern 5: Confirmation Buffers for Irreversible Actions

Many "irreversible" actions can be made softreversible by inserting a delay. Instead of sending an email or firing a webhook immediately, write it to a deferredaction queue with a short cancellation window. During that window the action can be cancelled, edited, or superseded, converting an irreversible operation into a recoverable one without changing the downstream system.

This is the single highestleverage technique for taming the irreversible class: a 30second deferred send turns "the email is already in their inbox" into "we caught it in the buffer." Pair it with humanintheloop confirmation for the actions where even a buffer isn't enough.

Pattern 6: The Outbox Pattern

If your agent's tool calls trigger downstream events (notifications, webhook fires, messagequeue publishes), use the outbox pattern: write the event to an outbox table in the same transaction as the state change, then have a separate publisher deliver it. This eliminates the dualwrite problem that plagues naively eventdriven agents.

Where MCP Fits, and Where It Doesn't

The Model Context Protocol standardizes how agents discover and invoke external tools. It's a protocol for tool discovery and invocation, and a useful one.

MCP does not standardize safety. It does not require tools to declare:

  • Their risk class

  • Their idempotency contract

  • Their compensating action

  • Their concurrency semantics

A tool exposed over MCP is exactly as safe as the tool itself: no more, no less. Adopting MCP and assuming safety comes with it is a real operational risk; a single network blip is enough to trigger a phantom retry against a nonidempotent endpoint.

The opportunity, however, is real: MCP server authors can voluntarily document safety contracts alongside their tool definitions. As an industry, we should push for that becoming convention. Until then, treat every MCP tool as riskclass unknown until proven otherwise. Trust is not a concurrency primitive.

The Safety Decision Matrix

Before adding any new tool to your agent's catalog, run it through this:

| Question | If YES โ†’ | | | | | Does this tool make a state change anywhere? | Treat as mutating | | Is the mutation reversible by another tool call? | Define the compensating action (and its idempotency contract) | | Can the tool be retried with no additional effect? | Softmutate (add idempotency key, minted outside the model) | | Could running it twice cause customer harm? | Hardmutate or irreversible: require saga or HITL | | Is the action irreversible but tolerant of a short delay? | Add a confirmation buffer to make it softreversible | | Does this tool depend on external state other agents can change? | Add optimistic concurrency control |

If you can't confidently answer all of these, the tool isn't safe to ship yet. That's not bureaucracy; it's the same bar we apply to any production code that touches external systems.

What to Build First

If your agent currently has tools but none of this infrastructure, build in this order:

  1. A tool risk classifier. Even a YAML field per tool: risk: read | softmutate | hardmutate | irreversible. This is metadata. It's free. Add it.

  2. An idempotency middleware. A key generator (owning keys outside the model) and a (key โ†’ outcome) store with TTL. The smallest version of this is a few dozen lines of code and prevents the entire class of duplicatesideeffect bugs.

  3. A compensation catalog. For every hardmutate tool, write its compensating tool, and make that compensation idempotent and retryable. Even if you never need it, the act of writing it forces clarity about what the original tool actually does.

  4. A saga executor. Start with synchronous, deterministic orchestration that owns sequencing and persists "compensation owed" state durably. Add a deadletter / escalation path for failed compensations. Add asynchronous execution later if you need it. Persist saga state to the same event log you built for episodic memory in the previous article.

  5. Auditlog every tool invocation. Every attempt, every retry, every compensation. This is your debugging substrate and your compliance substrate at the same time.

Each of these is small in isolation. Together they form the safety envelope that makes agent actions trustable in production.

The Architect's Mental Model

The shift that separates teams shipping safe agents from teams that don't is this:

The model is a probabilistic component embedded in a deterministic system. The deterministic system is what makes the agent safe.

The LLM does the reasoning. The envelope around it (the idempotency layer, the saga executor, the compensation catalog, the risk classifier, the audit log) does the safety. The model proposes intent; the deterministic envelope owns execution, sequencing, and recovery.

That envelope is not optional, and it is not new. It's the same envelope distributed systems engineers built around databases in the ACID era, around microservices in the saga era, around eventdriven systems in the idempotentconsumer era.

The agentic AI era is the same problem with a new probabilistic component at the center. We don't need new fundamentals. We need to remember the ones we already have.

Reasoning is cheap. Side effects are forever. Design accordingly.

This is part 3 of Architecting Agents, a series on building agentic AI systems that survive production.

Architecting Agents

Part 3 of 3

Agentic AI is the most exciting shift in software in a decade โ€” and one of the easiest to get wrong in production. Architecting Agents is a series of deep-dive essays from a senior software architect's chair: why agentic systems fail under real-world load, which classical distributed-systems patterns still apply, and how to design agents that survive contact with production. Less hype. More engineering. Written for engineers who ship.

Start from the beginning

Why Most Agentic AI Systems Fail in Production โ€” A Software Architect's Perspective

Old engineering principles aren't optional. They're load-bearing