Lesson 22 / 22
Error Recovery
Measuring what a failed tool call does to the loop with three recovery policies: in a production stream of two hundred forty runs, stopping at the first error gives a task success of 0.7500, 727 steps, 53 tool errors, and 81996 tokens; retrying the same tool raises success to 0.9375 and moves the steps to 900, the tool errors to 61, and the tokens to 106352. Dropping the failed tool and moving on to another does the opposite of what is expected: tool errors rise to 64, steps to 907, and success falls to 0.8917. The step efficiency that stopping gains stays within the band of the two runs. The lesson is the close of the AI Agents course and collects the without-scaffolding and with-scaffolding numbers of twenty-two lessons in a single table.
Published Updated
In this lesson
- Defines the three recovery policies on the error branch of the loop and counts task success, steps, and tool errors in each.
- Shows with its mechanism that disabling the failed tool raises the tool errors instead of lowering them.
- Collects the without-scaffolding and with-scaffolding numbers of the course's twenty-two lessons in a single table.
Contents
One column stayed silent from start to finish in the cost table of the previous lesson: the tool error. Errors occurred in all six setups, and in none of them was it written what to do. When the loop of the shared definition sees an error, it turns toward the same target again; this is a behavior but not a policy, because it was not chosen. The retry, backoff, and circuit breaker procedure was measured in the Resilience and Reliability course; it is not repeated. The only thing measured is what the failed tool does to the loop.
- AQ51. The environment, tools, tasks, and loop are fictional. The production stream is the same as that of the previous lesson’s: twenty-four tasks run ten times, the budget is 6, and the total is 240 runs. The seeds are 20260218 and 20260219.
- AQ52. Stop: the loop ends at the first error; this is the baseline. Retry: the target does not change; at the next step the loop turns to the same target again. Skip: the tool that returned an error becomes unselectable for the rest of the run; if the draw gives it, the next tool in the list is used.
- AQ53. There is no second tool in the set that does the same job; “moving on to another tool” means dropping the failed tool, and if the plan needs it, the plan cannot be completed.
Three Policies, One Stream
The block sets up the shared definition: the fictional environment, five tools, twenty-four tasks, and the loop.
# FICTIONAL. The measurement network's subscriber records, document set, five tools and # twenty-four tasks are defined within the lesson; no real endpoint or tool server is called. SEED, M32 = 20260218, 0xFFFFFFFF def rng(t): x = ((t ^ (t >> 16)) * 2246822507) & M32 x = ((x ^ (x >> 13)) * 3266489909) & M32 s = [(x ^ (x >> 16)) & M32] def next_value(): s[0] = (s[0] * 1664525 + 1013904223) & M32 return s[0] / 4294967296 return next_value def pick(u, w): t = 0.0 for i, x in enumerate(w): t += x if u < t: return i return len(w) - 1 def tokenize(m): b = [] for s in str(m).lower().replace(",", " , ").split(): while len(s) > 5: b.append(s[:5]) s = s[5:] b.append(s) return b def trace_id(m): h = 2166136261 for c in str(m): h = ((h ^ ord(c)) * 16777619) & M32 return h REGION = ["east", "west", "north", "south", "central"] TYPE = ["residential", "commercial"] SUBSCRIBERS = {} for i in range(60): r = rng(SEED + 31 * i) previous = 10000 + int(r() * 80000) SUBSCRIBERS[f"A{i + 1:03d}"] = {"region": REGION[int(r() * 5)], "type": TYPE[int(r() * 2)], "previous": previous, "current": previous + int(r() * 70)} TIERS, ABOVE = ((10, 4), (25, 7), (40, 11)), 16 COMMERCIAL_FACTOR = 2 DOCUMENTS = {"TR01": "tarife basamaklari 10 25 40 metrekup ve ustu", "TR02": "isyeri abonesinde birim ucret iki katidir", "SY01": "endeks farki tuketimi verir sayac donusu 100000 eklenir", "AR01": "ariza kaydi acildiginda saha ekibi iki gun icinde gider", "KS01": "planli kesinti duyurusu bir hafta once yayimlanir"} CALLS = [0] # tool call counter def _charge(m3, type_): remaining, lower, total = m3, 0, 0 for upper, unit in TIERS: band = min(remaining, upper - lower) if band <= 0: break total += band * unit remaining -= band lower = upper total += remaining * ABOVE return total * (COMMERCIAL_FACTOR if type_ == "commercial" else 1) def t_read_meter(subscriber=None, **_): a = SUBSCRIBERS.get(subscriber) if a is None: return {"error": "no such subscriber"} return {"previous": a["previous"], "current": a["current"], "type": a["type"], "region": a["region"]} def t_consumption(previous=None, current=None, **_): if previous is None or current is None: return {"error": "missing field"} f = current - previous return {"m3": f + 100000 if f < 0 else f} def t_tariff(m3=None, type="residential", **_): if m3 is None: return {"error": "missing field"} return {"amount": _charge(m3, type)} def t_document(query=None, **_): if not query: return {"error": "missing field"} q = set(tokenize(query)) best, code = -1, None for k, m in DOCUMENTS.items(): o = len(q & set(tokenize(m))) if o > best: best, code = o, k return {"code": code, "text": DOCUMENTS[code]} if best > 0 else {"error": "not found"} def t_record(subscriber=None, column=None, value=None, **_): if not (subscriber and column and value is not None): return {"error": "missing field"} return {"written": True, "subscriber": subscriber, "column": column, "value": value} TOOLS = { "read_meter": {"function": t_read_meter, "fields": ("subscriber",), "side_effect": False}, "consumption": {"function": t_consumption, "fields": ("previous", "current"), "side_effect": False}, "tariff": {"function": t_tariff, "fields": ("m3", "type"), "side_effect": False}, "search_docs": {"function": t_document, "fields": ("query",), "side_effect": False}, "write_record": {"function": t_record, "fields": ("subscriber", "column", "value"), "side_effect": True}, } TOOL_NAMES = list(TOOLS) def call_tool(name, state): """Fields outside the schema are filtered out; a missing field is the tool's own error.""" CALLS[0] += 1 if name not in TOOLS: return {"error": "no such tool"} kw = {a: state[a] for a in TOOLS[name]["fields"] if a in state} return TOOLS[name]["function"](**kw) TASKS = [] for i in range(24): r = rng(SEED + 9973 * i) sub = f"A{1 + int(r() * 60):03d}" t = pick(r(), [0.30, 0.22, 0.20, 0.28]) if t == 0: TASKS.append({"name": f"G{i + 1:02d}", "kind": "charge", "subscriber": sub, "request": f"{sub} abonesinin bu donem bedeli nedir", "sequence": ("read_meter", "consumption", "tariff")}) elif t == 1: TASKS.append({"name": f"G{i + 1:02d}", "kind": "consumption", "subscriber": sub, "request": f"{sub} abonesinin bu donem tuketimi kac metrekup", "sequence": ("read_meter", "consumption")}) elif t == 2: TASKS.append({"name": f"G{i + 1:02d}", "kind": "document", "subscriber": sub, "request": "sayac donusunde endeks farki nasil hesaplanir", "sequence": ("search_docs",)}) else: TASKS.append({"name": f"G{i + 1:02d}", "kind": "record", "subscriber": sub, "request": f"{sub} abonesinin bedelini hesapla ve kayda yaz", "sequence": ("read_meter", "consumption", "tariff", "write_record")}) def correct_answer(g): a = SUBSCRIBERS[g["subscriber"]] f = a["current"] - a["previous"] m3 = f + 100000 if f < 0 else f if g["kind"] == "consumption": return m3 if g["kind"] == "document": return "SY01" return _charge(m3, a["type"]) def selection(d): return max(0.34, 0.80 - 0.09 * (d - 1)) # share of picking the right tool, falls with depth def agent(g, max_steps=8, seed=SEED, self_critique=False, approval=False): """FICTIONAL agent loop. No real model is called; tool selection comes from a single draw that depends on the task's sequence and the depth.""" r = rng(seed ^ trace_id(g["name"])) state = {"subscriber": g["subscriber"], "query": g["request"], "column": "charge"} trace, step, tok, errors = [], 0, 0, 0 seq, k = g["sequence"], 0 while step < max_steps: step += 1 tok += 60 + 22 * len(trace) target = seq[k] if k < len(seq) else None if target is None: # the job is done , the agent should stop if r() < 0.88: return {"done": True, "steps": step, "trace": trace, "tokens": tok, "errors": errors, "answer": state.get("result"), "approvals": state.get("approvals", 0)} choice = TOOL_NAMES[int(r() * len(TOOL_NAMES))] else: choice = target if r() < selection(k + 1) else TOOL_NAMES[int(r() * len(TOOL_NAMES))] if TOOLS[choice]["side_effect"] and approval: state["approvals"] = state.get("approvals", 0) + 1 if choice == "write_record": state["value"] = state.get("result") s = call_tool(choice, state) trace.append((choice, "error" in s)) tok += 30 if "error" in s: errors += 1 if self_critique: tok += 40 continue state.update({kk: vv for kk, vv in s.items() if kk != "error"}) if choice == "consumption": state["result"] = s["m3"] if choice == "tariff": state["result"] = s["amount"] if choice == "search_docs": state["result"] = s["code"] if choice == target: k += 1 return {"done": False, "steps": step, "trace": trace, "tokens": tok, "errors": errors, "answer": state.get("result"), "approvals": state.get("approvals", 0)}
The policy changes only the error branch.
# The recovery policy changes only the error branch of the shared loop. BUDGET, RUNS = 6, 10 RESULT_FIELD = {"consumption": "m3", "tariff": "amount", "search_docs": "code"} # tools that write the result def agent_policy(g, policy, max_steps=BUDGET, seed=SEED): r = rng(seed ^ trace_id(g["name"])) state = {"subscriber": g["subscriber"], "query": g["request"], "column": "charge"} trace, step, tok, errors, banned = [], 0, 0, 0, set() seq, k = g["sequence"], 0 while step < max_steps: step += 1 tok += 60 + 22 * len(trace) target = seq[k] if k < len(seq) else None if target is None: if r() < 0.88: break choice = TOOL_NAMES[int(r() * len(TOOL_NAMES))] else: choice = target if r() < selection(k + 1) else TOOL_NAMES[int(r() * len(TOOL_NAMES))] while choice in banned and len(banned) < len(TOOL_NAMES): choice = TOOL_NAMES[(TOOL_NAMES.index(choice) + 1) % len(TOOL_NAMES)] if choice == "write_record": state["value"] = state.get("result") s = call_tool(choice, state) trace.append((choice, "error" in s)) tok += 30 if "error" in s: errors += 1 if policy == "stop": break if policy == "skip": banned.add(choice) continue state.update({kk: vv for kk, vv in s.items() if kk != "error"}) if choice in RESULT_FIELD: state["result"] = s[RESULT_FIELD[choice]] if choice == target: k += 1 return {"steps": step, "tokens": tok, "errors": errors, "answer": state.get("result")} FIELDS = ("steps", "tokens", "errors", "answer") print("retry is the same as the shared agent:", all(agent_policy(g, "retry") == {a: agent(g, BUDGET, SEED)[a] for a in FIELDS} for g in TASKS)) print("\nseed policy success steps errors tokens steps per correct answer") for base_seed in (20260218, 20260219): for p in ("stop", "retry", "skip"): correct, steps, tok, errors = 0, 0, 0, 0 for d in range(RUNS): for g in TASKS: s = agent_policy(g, p, BUDGET, base_seed + 7 * d) steps += s["steps"] tok += s["tokens"] errors += s["errors"] correct += s["answer"] == correct_answer(g) n = RUNS * len(TASKS) print(f"{base_seed} {p:<8} {correct / n:>8.4f} {steps:>5} {errors:>6} {tok:>9}" f" {steps / correct:>24.2f}")
retry is the same as the shared agent: True seed policy success steps errors tokens steps per correct answer 20260218 stop 0.7500 727 53 81996 4.04 20260218 retry 0.9375 900 61 106352 4.00 20260218 skip 0.8917 907 64 108030 4.24 20260219 stop 0.7667 768 54 89180 4.17 20260219 retry 0.9542 944 66 114430 4.12 20260219 skip 0.8625 954 73 116776 4.61
What the Policy Adds and What It Costs
The first line of the output is a check: the loop we used throughout the course had an unwritten policy, and it was retry.
Stop is the cheapest in all the cost columns: 727 steps, 53 tool errors, 81996 tokens. Its task success is 0.1875 below the 0.9375 of retry; since the difference between the two runs is 0.0167, the drop is ten times the band. Read per correct answer, the saving disappears: 4.04 steps for stop, 4.00 for retry, and the difference is within stop’s own difference between runs (4.04 against 4.17). Stopping lowers the spending of steps; it does not change the efficiency.
Skip is the example for the course’s second claim: intuition expects the errors to decrease, and the measurement gives the opposite. The tool errors rise from 61 to 64, and in the second run from 66 to 73; the steps go up to 907, and the task success falls to 0.8917. The mechanism works in two directions: the tool that gets banned has often been selected wrongly already, and the one that replaces it is wrong too, that is, the error slides onto another name; if a tool in the plan is banned, the plan ends. The loss is 0.0458 and 0.0917 in the two runs; since the widest band is 0.0292, both are above it, but the first is only one and a half times the band.
Summary
- The recovery policy is the error branch of the loop; even if it is not written, a policy runs, and here the one that runs is retry.
- Stop is the cheapest (727 steps, 53 errors, 81996 tokens), lowers task success by 0.1875, and does not improve step efficiency.
- Dropping the failed tool raises the tool errors to 64 and lowers task success to 0.8917.
Course Wrap-Up
| Lesson | Without scaffolding | With scaffolding | Cost |
|---|---|---|---|
| What Is an Agent — task success | no tools 0.0417 | loop 0.9583 (+0.9166) | steps from 24 to 97 |
| The Agent Loop — share of right selection | depth 1: 0.8000 | depth 4: 0.5000 (−0.3000) | steps from 2.86 to 7.67 |
| The Planner and Executor Split — task success | single loop 0.9583 | planner–executor 0.8750 (−0.0833) | steps from 97 to 118 |
| Stopping Criteria — task success | budget 2: 0.3750 | budget 6: 1.0000 (+0.6250) | tokens from 4758 to 10884 |
| Self-Critique and Verification — task success (24) | no rounds 21/24 | self-critique 23/24 (+2) | steps from 84 to 152 |
| Multi-Agent Patterns — task success (24) | single agent 23/24 | two agents 23/24 (0) | steps from 97 to 140 |
| Keeping the Human in the Loop — wrong record | approval off 3 | approval on side effects 1 (−2) | 7 approval points |
| Tool Definition — valid call rate | name only 0.2803 | schema 0.8919 (+0.6116) | tool errors from 113 to 8 |
| Executing Tool Calls — observation tokens | raw 770 | summarized 401 (−369) | task success the same: 0.9583 |
| Code Execution Tools — code task success | loose limit 0.7778 | medium limit 0.3333 (−0.4445) | 24 stopped calls |
| File System and Terminal Access — completed tasks | full authority 0.9583 | allowlist 0.8333 (−0.1250) | 20 stopped calls |
| Web Search and Browsing — correct answer | no external knowledge 0.2083 | primary source 0.7500 (+0.5417) | calls from 91 to 121 |
| Short-Term Memory — correct answer | unlimited window 0.9583 | window 120: 0.7083 (−0.2500) | tokens from 9316 to 12026 |
| Long-Term Memory — steps gained (48) | no memory 0 | selective write 118 (+118) | 24 records |
| Context Compression — total tokens | no compaction 21913 | selective 16569 (−5344) | 8 tasks with the field dropped |
| Memory Decay — stale read share | no threshold 0.4528 | threshold 1: 0.0000 (−0.4528) | valid records dropped 0.9701 |
| Measuring Task Success and Step Efficiency Separately — task success | toolless baseline 0.2917 | course budget 0.9583 (+0.6666) | per answer from 3.4286 to 4.2174 |
| Testing Each Tool Separately from the Loop — defects caught (11) | untested 0 | tool level 7 (+7) | 0.0000 on definition defects |
| Scenario-Based End-to-End Validation — defects caught (11) | tool level 7 | two layers 10 (+3) | residual D11 |
| Monitoring and Logging — share with cause found | result only 0.0000 | full observation 1.0000 (+1.0000) | tokens from 2738 to 13623 |
| Cost and Latency Control — cost per step | large class 118.30 | small class 79.25 (−39.05) | per answer from 453.5 to 488.0 |
| Error Recovery — task success (240 runs) | stop 0.7500 | retry 0.9375 (+0.1875) | steps from 727 to 900 |
The rule of the course is written line by line in the table: the number of an agent is the number of the whole loop, not of a single answer, and an agent whose stopping criterion is not written counts as unmeasured. The numbers in the without-scaffolding column are not low because of a bad model; they come out low because where the loop will stop, how the tool introduces itself, what will be remembered, and what happens when a step breaks are not written.
The second claim is read from the same table too: an added step can make the work worse. The rows
that say it by name are these: Executing Tool Calls (truncation lowers 0.9583 to 0.2917 and raises
the token to 15945), Web Search and Browsing (the secondary class lowers 0.7500 to 0.3750), Short-Term
Memory (narrowing raises the token to 12026), Long-Term Memory (result only lowers 0.9167 to 0.5833),
Context Compression (the tightest summarization spends 1.53 times the baseline), Multi-Agent Patterns
(the second agent brings no gain), Keeping the Human in the Loop (spreading the scope raises the
question to 74), Cost and Latency Control (the small class is expensive per answer), and this lesson
(skip raises the errors to 64). In nine places the added step cost, because it was added.
Differences below the measurement band are also written by name and do not count as findings: in What Is an Agent the baseline’s single hit, in The Agent Loop the 0.0917 and 0.0160 of the neighboring depths, in The Planner and Executor Split the 0.0833 of the split, in Stopping Criteria the 0.0417 drop from six to eight, in Self-Critique and Verification the round differences, in Short-Term Memory the 0.0833 at the window of 200, in Memory Decay the gain of tightening the threshold below eight, and in this lesson the step efficiency of stop. None of them is zero, none of them is measured.
In twenty-two lessons the agent was built, equipped, and measured. One thing was never asked: every
tool was defined by hand within a lesson. The name, description, and schema of read_meter were
written in this course and only the agent here saw them. How the same tool would be handed to
another agent, another team, or another application, who would hold the definition, and who would
break when the interface changed were not asked. The next course turns the tool definition from a
dictionary inside a lesson into a shared surface and counts its cost.
To keep your progress and take notes, Log in
My notes
Log in to take notes.