Lesson 05 / 22
Self-Critique and Verification
What it adds and what it costs for an agent to look at its own trace after it stops: when three observable signs are read on twenty-four fictional tasks, the round flags every wrong answer at narrow budgets but cannot see at all a wrong answer with a clean trace at eight steps. At a budget of four steps success rises from 21/24 to 23/24, in return for steps going from 84 to 152 and tokens from 9402 to 17052. When the same steps are given to a single longer run, the eight-step budget gives the same 23/24 with 97 steps and 11978 tokens. At six steps the round lowers success from 24/24 to 23/24; at eight steps it changes nothing, and six of the seven flags are unnecessary reruns. The run-to-run difference is one task, so the difference at six and eight steps counts as unmeasured.
Published Updated
In this lesson
- Defines the observable signs from which an agent loop can look at its own trace after it stops, and shows that they are independent of the correct answer.
- Reads the self-critique round through four cases: a wrong answer that is flagged, an unnecessary flag, a wrong answer that stays silent and a correct answer that stays silent.
- Calculates side by side, on the same task set, the success the round adds and the steps, calls and tokens it costs.
- Separates the budgets where the gain stays below the run-to-run difference and does not count them as findings.
Contents
In the previous four lessons the loop was built and where it stops was written. The agent picks a tool, writes the returned observation to its state, takes the next step by looking at it; it stops when its job is done or the budget is full. When it closes, an answer is left in hand.
The one thing that was not asked in four lessons is this: is that answer correct. When the agent stops, it does not turn back and look at its own trace. This lesson adds that missing round and measures two things: what the round catches and what the round costs. Running the same prompt several times and voting on the answers is a separate arrangement and was measured in the Prompt Engineering course; here there are no several answers to vote on, and the trace left by a single loop is looked at.
- AA41. The setup is common: the fictional subscriber records of the measurement network, the fictional document vocabulary, five fictional tools, twenty-four fictional tasks and an agent simulator written inside the lesson. No real model endpoint or tool server is called.
- AA42. The baseline is the scaffolding-free loop: the same set, the same budget, the round closed. The plain row with the budget doubled also stays in the table, because the steps the round spends could also have been given to a single longer run.
- AA43. The round cannot see the correct answer; it looks only at three observable signs: did the loop stop by itself, did a tool error occur, was an answer produced.
- AA44. If one of the three is broken, the task counts as flagged and the loop runs a second time. The round is single, and it can corrupt the answer as well as correct it.
- AA45. The cost of the round after every run is one step and
45 + 18 x len(trace)tokens; in a run with the round, every tool error adds 40 tokens for reading the trace. - AA46. The second run seed is 20260219; in this set the run-to-run difference is one task (0.0417) and every difference below it counts as unmeasured.
The Three Signs the Round Can Look At
The round has no correct answer in hand; if it did, there would be no need for the agent. It has only the trace.
# COMMON FICTIONAL SETUP -- the 60 fictional subscribers of the measurement network, # 5 fictional documents, 5 fictional tools, 24 fictional tasks and the agent simulator # written inside the lesson. No real model endpoint, tool server or runtime 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 # ---- FICTIONAL environment: records of the measurement network 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) # ---- FICTIONAL tasks: each one is solved by a known tool sequence 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"]) # ---- FICTIONAL agent: at each step it picks a tool or finishes 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)} # ---- Part added in this lesson: the self-critique round def flagged(s): """The three signs the round can look at: did the loop stop by itself, did a tool error occur, was an answer produced. The round does not have the correct answer.""" return (not s["done"]) or s["errors"] > 0 or s["answer"] is None def review_cost(s): return 45 + 18 * len(s["trace"]) print("task kind done error answer flagged correct") for g in TASKS[:6]: s = agent(g, max_steps=2) print(f"{g['name']:<7}{g['kind']:<12}{str(s['done']):<7}{s['errors']:<6}" f"{str(s['answer'] is not None):<7}{str(flagged(s)):<9}" f"{s['answer'] == correct_answer(g)}") def four_cells(b, seed): c = {"fw": 0, "fr": 0, "sw": 0, "sr": 0} for g in TASKS: s = agent(g, max_steps=b, seed=seed) c[("f" if flagged(s) else "s") + ("r" if s["answer"] == correct_answer(g) else "w")] += 1 return c print("\nbudget seed flagged+wrong flagged+right silent+wrong silent+right") for b in (2, 4, 8): for t in (SEED, SEED + 1): c = four_cells(b, t) print(f"{b:>6}{t:>10}{c['fw']:>14}{c['fr']:>15}{c['sw']:>15}{c['sr']:>14}") print("\nsilent wrong answer (budget 8, seed 20260219):") for g in TASKS: s = agent(g, max_steps=8, seed=SEED + 1) if not flagged(s) and s["answer"] != correct_answer(g): print(" ", g["name"], g["kind"], [a for a, h in s["trace"]]) print(" answer", s["answer"], "-- correct", correct_answer(g))
task kind done error answer flagged correct
G01 document False 2 False True False
G02 document True 0 True False True
G03 consumption False 1 False True False
G04 consumption False 0 True True True
G05 charge False 0 True True False
G06 charge False 0 True True False
budget seed flagged+wrong flagged+right silent+wrong silent+right
2 20260218 15 6 0 3
2 20260219 14 7 0 3
4 20260218 3 7 0 14
4 20260219 4 6 0 14
8 20260218 1 6 0 17
8 20260219 0 6 1 17
silent wrong answer (budget 8, seed 20260219):
G15 record ['read_meter', 'consumption', 'tariff', 'consumption', 'write_record']
answer 57 -- correct 582
The first table shows why the round is not cheap. At a narrow budget five of the six tasks are
flagged; four are really wrong, and one (G04) has produced a correct answer. A broken sign is
not proof of a wrong answer, it is a reason to look.
The four-cell table confirms this on the whole set. At narrow budgets there is no silent wrong
answer; the cost is unnecessary flags, seven against three wrong answers at four steps. At eight
steps the table turns around, and the last lines of the block print the task that stayed silent.
G15 is a record task; its sequence is complete, the loop stopped by itself, and no tool error
occurred. But after the job was done the agent wandered and called the consumption tool again, this
call overwrote the accumulated result with consumption instead of the charge, and that value was
written to the record. The trace looks clean because every step is valid on its own; what is
broken is the order of the steps.
What the Round Adds and Multiplies
The round is now run as an arrangement.
def run(b, seed, critique=False): """critique=False is the plain loop. critique=True: the loop ends, the round looks at the trace, and if flagged the same task runs once more in a second loop.""" CALLS[0] = 0 d = {"right": 0, "steps": 0, "tokens": 0, "flags": 0, "rescued": 0, "broke": 0} for g in TASKS: s = agent(g, max_steps=b, seed=seed, self_critique=critique) d["steps"] += s["steps"] + critique d["tokens"] += s["tokens"] + (review_cost(s) if critique else 0) first = s["answer"] == correct_answer(g) if critique and flagged(s): d["flags"] += 1 s = agent(g, max_steps=b, seed=seed ^ trace_id("tur2|" + g["name"]), self_critique=True) d["steps"] += s["steps"] + 1 d["tokens"] += s["tokens"] + review_cost(s) final = s["answer"] == correct_answer(g) d["right"] += final d["rescued"] += final and not first d["broke"] += first and not final d["calls"] = CALLS[0] return d for t in (SEED, SEED + 1): print(f"\nseed {t}") print("setup success step calls tokens flags rescued broke") for b in (2, 4, 6, 8): for label, critique in (("plain", False), ("round", True)): d = run(b, t, critique) extra = (f"{d['flags']:>8}{d['rescued']:>9}{d['broke']:>6}" if critique else f"{'-':>8}{'-':>9}{'-':>6}") print(f"budget {b:>2}, {label}{d['right']:>6}/24{d['steps']:>7}" f"{d['calls']:>7}{d['tokens']:>10}{extra}")
seed 20260218 setup success step calls tokens flags rescued broke budget 2, plain 9/24 48 45 4758 - - - budget 2, round 13/24 135 84 13007 21 4 0 budget 4, plain 21/24 84 64 9402 - - - budget 4, round 23/24 152 94 17052 10 2 0 budget 6, plain 24/24 92 71 10884 - - - budget 6, round 23/24 164 102 19671 9 0 1 budget 8, plain 23/24 97 74 11978 - - - budget 8, round 23/24 155 94 18821 7 1 1 seed 20260219 setup success step calls tokens flags rescued broke budget 2, plain 10/24 48 45 4758 - - - budget 2, round 12/24 135 83 12839 21 4 2 budget 4, plain 20/24 83 67 9366 - - - budget 4, round 22/24 151 96 16822 10 2 0 budget 6, plain 23/24 96 74 11610 - - - budget 6, round 23/24 155 96 18493 7 0 0 budget 8, plain 23/24 98 74 11994 - - - budget 8, round 23/24 152 92 18312 6 0 0
The budget of four steps is the row where the round looks its best: success goes from 21/24 to 23/24, two tasks on both runs. The cost: steps from 84 to 152, calls from 64 to 94, tokens from 9402 to 17052. Of the ten flags only two correct an answer.
The plain row at eight steps puts this gain in context: 23/24, 97 steps, 11978 tokens. The plain loop reaches in 97 steps where the round arrived with 152 steps; when the surplus is given to a longer single run, the same success is bought more cheaply. At the budget of two steps the gap is sharper: the run with the round gives 13/24 with 135 steps, while the plain run at four steps gives 21/24 with 84 steps.
The row at six steps is the clear example of the second claim of the course: the plain loop makes 24/24 there, and when the round is added it goes down to 23/24. None of the nine flags corrects a wrong answer, and one corrupts a correct one.
Below the Band
Not every difference in this table is a finding. The success of the plain loop is 24/24 and 23/24 on the two seeds at six steps and 23/24 and 23/24 at eight; the measurement band is one task. The one-task fall the round costs at six steps and the zero difference at eight are inside this band and count as unmeasured: the mechanism can be shown, its effect cannot be measured at this set size. The only gain that stays above the band is the budget of four steps. At eight steps the only concrete number of the round is the cost: seven flags, six of them unnecessary, and zero tasks gained.
Summary
- A stopped agent can look at its own trace but cannot see the correct answer; the three signs it can look at are whether it stopped by itself, whether a tool error occurred and whether it produced an answer.
- A broken sign is not proof of a wrong answer: at the budget of four steps only three of the ten flags correspond to a wrong answer.
- At wide budgets a wrong answer with a clean trace appears; a loop that stopped by itself without errors can overwrite the result it accumulated while wandering, and the round cannot see this.
- The highest gain of the round is two tasks at the budget of four steps (21/24 to 23/24); steps go from 84 to 152 and tokens from 9402 to 17052. The plain loop at eight steps gives the same 23/24 with 97 steps.
- The differences at six and eight steps are inside the band of two runs (one task) and are not counted as findings.
Next Step
In this lesson the one that checks and the one that is checked were the same loop, and the round stayed limited to the surface of the trace. One way to take the looking eye outside the loop is to split the job and give it to a second agent. The next lesson puts a single agent and two agents side by side on the same task set. What will be measured is not only success; the tokens of the handed over context and the number of fields lost in the handoff are counted separately, because the cost of a second agent is paid not in steps but in communication.
To keep your progress and take notes, Log in
My notes
Log in to take notes.