The Agent That Loops on a Flaky Tool
You built an agent that reconciles invoices. It has tools: fetch_invoice, fetch_purchase_order, match_line_items, post_adjustment, and notify_ap_team. It runs against Claude with a 15-step budget and a system prompt that says "reconcile the invoice; if you cannot, notify the AP team." In production, 6% of runs hit the step budget and terminate with no action. Logs show a pattern: fetch_purchase_order returns a 503 about 2% of the time; when it does, the agent retries it, sometimes 3-4 times, then tries match_line_items anyway, gets an error because no PO is loaded, and re-fetches. Separately, on invoices with a legitimate $0.02 rounding difference, the agent calls post_adjustment for $0.02, which the downstream system rejects as below its $0.05 minimum, and the agent retries the adjustment until the budget runs out.
1. The agent has two distinct loop causes. Identify each and the correct layer to fix it.
Read the worked answer
These are different bugs and need fixes at different layers - conflating them is the trap.
Loop 1 - transient tool failure. fetch_purchase_order 503s 2% of the time. The agent's response (retry, then proceed to match_line_items without a PO, get an error, re-fetch) is the agent improvising error handling it shouldn't own. Fix at the tool/orchestration layer, not the prompt: wrap the tool with bounded retry + backoff (e.g., 3 attempts, jittered), and on exhaustion return a structured, terminal error - {status: "unavailable", retryable: false, guidance: "PO service down; notify AP"} - so the agent gets a clear signal instead of a raw 503 it interprets as "try again." Also make match_line_items fail fast with a clear precondition error if no PO is loaded, rather than a generic error that invites a re-fetch. Prompt tweaks ("don't retry more than twice") are a soft fix on a hard problem; the retry policy belongs in code.
Loop 2 - semantically impossible action. The $0.02 difference is below the downstream $0.05 minimum. The agent is behaving reasonably given what it knows; it just doesn't know the constraint. Fix at the tool contract: post_adjustment should validate and return a structured rejection ({status: "rejected", reason: "below_minimum", minimum: 0.05, suggested: "accept_or_escalate"}) - and the system prompt/tool description should state the tolerance rule so the agent can choose "accept the rounding difference and close" instead of adjusting. The retry here is the agent not understanding that the rejection is permanent; a typed non-retryable error fixes that.
One-line summary: transient failures → retry policy in the wrapper; permanent failures → typed errors and explicit domain rules in the tool contract.
2. Beyond fixing the loops, how would you make the 15-step budget failure observable and safe?
Read the worked answer
A budget-exhaustion termination that "does nothing" is the worst outcome - the invoice silently stalls. Make it both visible and safe by default:
Observability:
- Log every step with tool name, args, result status, and a step index, so a post-mortem shows the loop shape (the logs already revealed it - formalize that as a structured trace, not free text).
- Emit a metric per termination reason:
completed,budget_exhausted,tool_unavailable,permanent_rejection. Alert onbudget_exhaustedrate - 6% is a signal, and you want to know if it moves. - Tag traces with the invoice ID so a stuck invoice is traceable end to end.
Safety:
- Budget exhaustion must be a defined terminal state that takes an action, not a silent stop. The system prompt already says "if you cannot, notify the AP team" - but the agent can't act after the budget is gone. So the orchestrator should catch budget exhaustion and call
notify_ap_teamitself with the trace summary. Never let the loop consume the step that would have escalated. - Reserve a step: if the budget is 15, treat step 15 as reserved for a terminal action (notify or close), so the agent always has an exit.
- Idempotency:
post_adjustmentandnotify_ap_teammust be idempotent on invoice ID, so a retry after a partial success doesn't double-post.
Then add a regression test: inject a 503 on fetch_purchase_order and a $0.02 difference, and assert the run terminates in notified or closed, never budget_exhausted.