To course

The agent loop

One step of an agent loopPractice: one step of the loopThe loop in codePractice: the loop in codeStopping and retryingPractice: stopping and retryingWhat only code can decidePractice: what code decides

Reliability in production

Timeout and idempotency keyPractice: timeout and idempotency keySays done, but nothing happenedPractice: false successDurable agent: checkpoints and resumePractice: durable agent

Agent designs

Workflow or agentPractice: workflow or agentReAct and plan-and-executePractice: ReAct and plan-and-executePlan-and-SolvePractice: Plan-and-SolveReflexionPractice: ReflexionOne agent or a swarmPractice: one agent or a swarm

Context and cost

Why the agent bill growsPractice: tokens and cacheContext engineeringPractice: managing the windowAgent memoryPractice: what the agent keeps

Tools and protocols

MCPPractice: MCPTool descriptions and injectionPractice: tool descriptions and injectionCode instead of a toolsetPractice: code instead of a toolsetAgent skills and A2APractice: agent skills and A2A

Frameworks

LangGraph or the raw loopPractice: LangGraph or the raw loopAgent SDKs: what you are buyingPractice: agent SDKs

Coding agents

Driving a coding agent from a specPractice: driving a coding agent from a spec
←→RUSign in
Agents › Reliability in productionDeep dive

Says done, but nothing happened

Builds onTimeout and idempotency key

This block explains why an agent's "done" proves nothing, how a system finds out what really happened, and how to measure the difference.

The Northwind agent tells a customer: "Your refund of $38.00 for order 4417 has been processed." Five days later the customer writes that no money arrived. The payment records hold no refund for that order, and the run is logged as a success.

The colleague and the statement

Back to the colleague on the phone. They tell you: "All paid." You now have three witnesses to that payment. The colleague's word. The bank clerk who said "OK" on the call. And the line on the bank statement.

Only the statement is the fact. The colleague may be describing what they meant to do. The clerk may have said "OK" to a transfer that never went through.

The final message of a run is the colleague's word. False success is the name for a claim of completion that the actual state does not support.

The truth has to travel from the record to the customer, and each hop can lose it.
The truth has to travel from the record to the customer, and each hop can lose it.

Three things usually go wrong, one on each hop:

The tool reports something false. A service answers 200 although nothing was written, or the write was partial. After an integration change, every call can come back that way.

The code around the model hides the error. This wrapper turns every failure into good news:

try:
    payments.refund(order_id, amount, idempotency_key=key)
except Exception:
    log.warning("refund failed")
return "Refund submitted."

The model retells its intention. It asked for the refund, or only planned to, so it writes that the refund happened, even if the result said pending or an error. No amount of confidence in that sentence tells you which one occurred.

How often? One 2026 workshop paper, from a single author, studied 9,876 runs from 8 model families on the tau2-bench customer-service benchmark. Among failed runs, the share where the agent claimed completion anyway was this high:

Airline and retail against telecom, where the simulated customer can check the state too. The author says more such environments are needed to establish the cause.
Airline and retail against telecom, where the simulated customer can check the state too. The author says more such environments are needed to establish the cause.

Reading the statement

First, a write tool returns fields that code can use, instead of a sentence:

{
  "status": "succeeded",
  "refund_id": "re_31",
  "order_id": "4417",
  "amount_cents": 3800
}

These fields can still be wrong, as the 200 above showed. So after the write your code fetches the record and compares it with what it asked for. A check that runs outside the model and decides the outcome is a verifier.

Write, then read the record back. The model plays no part in the comparison.
Write, then read the record back. The model plays no part in the comparison.
def verify_refund(op):
    if op.refund_id is None:
        return "unknown"    # no confirmed reply, so the refund may still exist
    try:
        refund = payments.get_refund(op.refund_id)
    except (Timeout, ServiceError):
        return "unknown"
    if refund.amount != op.amount_cents or refund.metadata["order_id"] != op.order_id:
        return "not_landed"
    return {"succeeded": "landed", "pending": "pending", "requires_action": "pending"}.get(refund.status, "not_landed")

A failed check is a third answer. If the read times out, you do not know, and the previous block already covered what to do then.

The outcome then decides what the customer reads:

Outcome

What the customer is told

landed

The refund is done, with its amount

pending

The refund is on its way

unknown

A colleague is checking

not_landed

It did not go through, and a person has the case

One read is one moment in time. At Stripe a refund's status can be pending, requires_action, succeeded, failed or canceled, and in rare cases a refund moves to failed because the customer's bank cannot process it. Stripe then sends a refund.failed event, so something on your side has to listen for it.

Verification has a price: an extra read and extra latency on each write. Pay it every time for money and permissions, and check a sample of the rest.

Counting the gap

Each run produces two separate facts: what the agent said and what the check found.

{
  "run_id": "run_9312",
  "order_id": "4417",
  "claimed": "done",
  "effect": "not_landed"
}

Ask the agent for a structured claim at the end of the run, so you do not have to parse sentences. Then count both on the same runs. Here is one week at Northwind:

Metric

Runs

Agent claimed "done"

4,120

Claimed "done" and the check passed

3,950

False success

170 (4.1% of claims)

Self-reported success alone would have looked healthy all week, and so would an uptime graph. Alert on the 170. A run still pending waits until it settles before it is counted.

Why not ask another model

A natural idea is an LLM judge that reads the transcript and decides. In the same paper, no judge configuration exceeded an AUROC of 0.65 on tau2-bench, where 0.5 is a coin flip and 1 is perfect. The author's explanation is that judges lean on confident closing language, which false-success runs have plenty of. The paper proposes lightweight trained detectors as a way to triage runs.

Anthropic's guide to agent evals draws the same line in general terms. The transcript is the record of the run, with what the agent said and did, and the outcome is the final state in the environment. Its example support eval uses a state check for the refund and an LLM rubric for tone. Keep a model for judging tone. Use code to read the record.

Conclusion

Go deeper

From Confident Closing to Silent Failure (arXiv)The measurements behind the chart. A single-author workshop paper, so read it as a signal.Demystifying evals for AI agents (Anthropic)The difference between transcript and outcome, and the kinds of graders.Silent Success (Tian Pan)A practitioner's view on reading state after a write and on what to log per turn.Refund and cancel payments (Stripe)Why an accepted refund can still fail later, and which event tells you.
‹ Previous
Suggest an improvementFound a mistake