Skip to content
academia.sh

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close