Agent Loops Forever on a Flaky Inventory API
A procurement agent uses Claude with tools check_inventory(sku), get_price(sku), and place_order(sku, qty). The check_inventory backend has intermittent 500 errors (~8% of calls) and occasionally returns an empty body with a 200 status. In a recent incident, the agent called check_inventory 41 times for one SKU over 9 minutes, then placed an order for quantity 500 when the user asked for 5. There is no max-iteration cap. Tool errors are passed back to the model as raw JSON {"error": "internal server error"}. The agent runs unattended overnight.
1. Identify the failure modes that produced both the 41-call loop and the wrong quantity, and explain how they compound.
Read the worked answer
Two distinct failure modes compound here.
The loop: the agent receives a raw error string with no guidance on whether the error is retryable or terminal. An 8% transient error rate means retries often succeed, which reinforces retrying - but the model has no stopping rule. With no max-iteration cap and an empty-200 case that looks like "no data" rather than "error," the model can't distinguish "try again" from "give up." It retries indefinitely.
The wrong quantity: after 41 calls the context is stuffed with error JSON and partial results. The original instruction (qty 5) is now a small signal among a large volume of noise - a classic lost-in-the-middle/context-dilution effect. The model may also have latched onto a number from a tool payload or misread a partial response. Quantity 500 for a request of 5 is a 100x error that no downstream check caught because place_order has no validation.
They compound because the loop both burns the time budget and pollutes context, making the eventual action less reliable. The empty-200 case is the worst offender: it's silently misclassified as valid data, so the model may proceed on garbage.
2. Design the guardrails - at the tool layer, the orchestration layer, and the prompt layer - that would have prevented this incident.
Read the worked answer
Defense in depth, because each layer alone is insufficient:
Tool layer (most reliable):
- Return structured, typed errors:
{status: "retryable"|"terminal", code, message, retry_after}so the model knows whether retrying is sensible. - Treat empty-200 as an explicit error, not empty data.
- Add server-side retry with backoff for transient 500s so the model rarely sees them.
- Validate
place_orderserver-side: reject quantities outside a sane band or above a per-order cap, and require the quantity to echo a value the user actually requested. This alone blocks the 500-unit order.
Orchestration layer:
- Hard max-iteration cap (e.g., 8) and a wall-clock timeout; on breach, halt and escalate to a human queue rather than acting.
- Cap consecutive identical tool calls (e.g., 3) and break the loop.
- Require human confirmation for orders above a threshold or any order after a degraded tool session.
Prompt layer:
- State the retry policy explicitly ("retry a retryable error at most twice; on terminal error or repeated failure, stop and report").
- Restate the user's requested quantity in the order step and require the agent to confirm it matches before calling
place_order.
Prompt alone wouldn't have held - the tool-layer validation is the backstop that makes the 100x error impossible regardless of model behavior.