Your agent just booked a flight, reserved a hotel, and drafted the itinerary email. Then it crashed on step eight — sending the email — and now you have a phantom reservation, a half-built trip, and zero idea which steps actually stuck.
Sound familiar? Welcome to the multi-step failure problem, and it's about to haunt every single agent you deploy.
Why Agents Fail in the Middle (Not the Beginning)
Here's the thing nobody warns you about: single-step agents almost never fail. They call one tool, get a result, done. The chaos starts when your agent chains five, ten, or twenty steps together — because each step assumes every previous step succeeded perfectly.
Think of it like a relay race where the runners can't see each other. Runner three doesn't know runner two dropped the baton. They just keep sprinting with empty hands.
Most agent frameworks treat a pipeline like a straight line: step one feeds step two feeds step three. No checkpoints. No rollback. No "hey, step four failed — what do we salvage?" Just a stack trace and a prayer.
And the failure rate compounds. If each step has a 95% success rate, a ten-step pipeline succeeds only 60% of the time. Twenty steps? 36%. Your agent isn't unreliable because it's dumb. It's unreliable because probability is unforgiving.
The Counterintuitive Fix: Save Points Beat Smarts
The instinct is to make the agent smarter — better prompts, more retries, fancier error handling inside each tool call. That helps at the margins. But the real fix is structural: give your agent a transaction log.
It's the same pattern databases figured out decades ago. Before you do something irreversible, write down what you're about to do. If you crash, replay the log.
Here's a minimal checkpoint system in Python:
import json
from pathlib import Path
class AgentCheckpoint:
def __init__(self, task_id: str):
self.path = Path(f"checkpoints/{task_id}.json")
self.path.parent.mkdir(exist_ok=True)
self.log = self._load()
def _load(self):
if self.path.exists():
return json.loads(self.path.read_text())
return {"completed": [], "pending": [], "results": {}}
def save(self):
self.path.write_text(json.dumps(self.log, indent=2))
def mark_complete(self, step_name: str, result: dict):
self.log["completed"].append(step_name)
self.log["results"][step_name] = result
if step_name in self.log["pending"]:
self.log["pending"].remove(step_name)
self.save()
def should_skip(self, step_name: str) -> bool:
return step_name in self.log["completed"]
Now your agent can crash at step eight, restart, and skip steps one through seven because their results are already checkpointed. No duplicate bookings. No phantom reservations.
The Three Failure Modes You Actually Need to Handle
Not all mid-task failures are created equal. Here's the taxonomy that saved my sanity:
1. Retriable failures — the API timed out, rate limit hit, network blip. These fix themselves. Add exponential backoff and move on:
import time
def retry_step(func, max_attempts=3):
for attempt in range(max_attempts):
try:
return func()
except (TimeoutError, ConnectionError) as e:
if attempt == max_attempts - 1:
raise
wait = 2 ** attempt
print(f"Retry {attempt+1}/{max_attempts} in {wait}s: {e}")
time.sleep(wait)
2. Compensatable failures — the step failed, but you can undo the previous steps. Cancel the hotel. Void the flight. Delete the draft. This is where most teams panic, because they never built the "undo" path:
COMPENSATIONS = {
"book_flight": cancel_flight,
"reserve_hotel": cancel_hotel,
"charge_card": refund_card,
}
def rollback(checkpoint):
for step in reversed(checkpoint.log["completed"]):
if step in COMPENSATIONS:
print(f"Rolling back: {step}")
COMPENSATIONS[step](checkpoint.log["results"][step])
3. Non-compensatable failures — you sent the email. You posted the tweet. You submitted the regulatory filing. It's done. For these, the only sane strategy is confirm before execute: make the agent pause and request human approval on any irreversible action. Think of it as the "are you sure?" dialog, but for robots.
The Orchestrator That Ties It All Together
Here's the thirty-line safety net that combines checkpoints, retries, and rollback into one runner:
def run_with_safety_net(steps, task_id):
cp = AgentCheckpoint(task_id)
for step in steps:
if cp.should_skip(step["name"]):
print(f"Skipping (already done): {step['name']}")
continue
try:
result = retry_step(step["action"])
cp.mark_complete(step["name"], result)
except Exception as e:
print(f"Failed at {step['name']}: {e}")
if step.get("compensatable", True):
rollback(cp)
raise
No external dependencies. Your agent can now survive a crash at any point and either resume cleanly or undo what it did. You define your steps as a list of dicts, each with a name, action callable, and a compensatable flag — and the runner handles the rest.
Why This Matters More Than You Think
As agents get more autonomous — booking things, moving money, writing to databases, sending messages on your behalf — the cost of a mid-task failure goes from "annoying log entry" to "angry customer with a double charge."
The frameworks will eventually build this in. LangChain has rudimentary retry logic; CrewAI has task callbacks. But none of them ship with a checkpoint-and-rollback pattern out of the box. Until they do, those thirty lines above are your seatbelt.
It's boring infrastructure. It's decades-old database thinking. And it's the difference between an agent you demo and an agent you actually deploy on Monday morning.
What's the worst mid-task failure your agent has caused? Drop the step number where everything went sideways — I want to hear your war story. 👇
Top comments (0)