Lesson 09 / 22
Executing Tool Calls
Measuring in which form the result a tool returns is given back to the model: on 24 fictional tasks the raw observation gives a task success of 0.9583 with 770 observation tokens and 10103 total tokens. Truncating the observation to seven words drops the tokens to 9160, but because it drops the subscriber type, four commercial invoices come out at exactly half and task success falls to 0.7917. Truncating to four words breaks the chain: tool errors jump from 8 to 116, steps from 97 to 156, total tokens to 15945, and task success stays at 0.2917; making it cheaper makes it more expensive. The summary that declares the fields to carry keeps the task success of the raw observation in both runs while dropping the observation tokens to 401 and the tokens per correct answer from 439 to 401. When the subscriber type is removed from the same summary, task success falls to the truncation level, 0.7917: a summary is only as good as its declaration.
Published Updated
In this lesson
- Writes a layer that turns a tool result into raw, truncated, and summarized observation forms.
- Compares task success, tool errors, steps, and accumulated tokens across the four observation forms.
- Tells apart with numbers that truncation drops fields by position while a summary drops them by declaration.
- Shows that in an accumulating context an early observation is costlier than a late one.
Contents
In the previous lesson the tool definition was split into four levels, and at the highest level the valid call rate was 0.8919 and task success 0.9583. That measurement never questioned the return of the call: whatever the tool returned was written to the state with all its fields and carried to the next step.
In a loop this is a shaky assumption. A tool result is an observation given back to the model, and that observation sits in the prompt again at every step. The full text a document tool returns, the four fields a reading tool returns, the confirmation of a write tool: all of them accumulate as the loop grows. The question of this lesson is this: in what form should the observation be given back, what happens to task success, and how many tokens accumulate across the loop.
- TM9. The environment, the tool set, the task set, and the agent loop are fictional; no real model endpoint, tool server, or runtime is called. 60 fictional subscribers, 5 fictional documents, 5 tools, and 24 tasks each solved by a known tool sequence. Seed 20260218, second run 20260219.
- TM10. A tool returns a dictionary; the observation is that dictionary turned into text. The agent updates its state not from the dictionary but by reading back from the observation text.
- TM11. There are three observation forms. The raw observation carries all the fields. The truncated observation cuts the text at a given number of words; a field whose value is left incomplete at the cut cannot be read back. The summarized observation carries the fields declared per tool.
- TM12. Truncation is measured at two thresholds: seven words and four words.
- TM13. The accumulated observation is sent again in the prompt at every step. The shared loop counts this accumulation with a constant coefficient per step; this lesson puts the token count of the observation actually carried in place of that coefficient. Tokens are counted with the fictional tokenizer.
- TM14. The observation of a call that returns an error also accumulates; an error text is also an observation.
- TM15. The baseline is the raw observation; this is the loop without scaffolding. Truncation and summarizing are the scaffolding added. The step budget is eight in all four forms.
- TM16. Task success is the result the loop leaves being equal to the correct answer.
The Three Forms of Observation
The code below builds the shared environment, the tool set, and the task set, then adds the observation
layer: a writer that turns the dictionary into text, a parser that reads the fields back from the text,
and the three forms. The tool and field names in the code are the read_meter, consumption, tariff, search_docs, and write_record tools of the previous lessons; they are counted in tokens here, so they stay Turkish.
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 # wire words stay Turkish: they are counted in tokens (same rule as the MCP course) # onceki/simdiki/tip/bolge = previous/current/type/region; mesken/isyeri = residential/commercial REGION = ["dogu", "bati", "kuzey", "guney", "merkez"] TYPE = ["mesken", "isyeri"] SUBSCRIBERS = {} for i in range(60): r = rng(SEED + 31 * i) previous = 10000 + int(r() * 80000) SUBSCRIBERS[f"A{i + 1:03d}"] = {"bolge": REGION[int(r() * 5)], "tip": TYPE[int(r() * 2)], "onceki": previous, "simdiki": 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_ == "isyeri" else 1) def t_read_meter(abone=None, **_): a = SUBSCRIBERS.get(abone) if a is None: return {"hata": "abone yok"} return {"onceki": a["onceki"], "simdiki": a["simdiki"], "tip": a["tip"], "bolge": a["bolge"]} def t_consumption(onceki=None, simdiki=None, **_): if onceki is None or simdiki is None: return {"hata": "eksik alan"} f = simdiki - onceki return {"m3": f + 100000 if f < 0 else f} def t_tariff(m3=None, tip="mesken", **_): if m3 is None: return {"hata": "eksik alan"} return {"tutar": _charge(m3, tip)} def t_document(anahtar=None, **_): if not anahtar: return {"hata": "eksik alan"} q = set(tokenize(anahtar)) best, code = -1, None for k, m in DOCUMENTS.items(): o = len(q & set(tokenize(m))) if o > best: best, code = o, k return {"kod": code, "metin": DOCUMENTS[code]} if best > 0 else {"hata": "bulunamadi"} def t_record(abone=None, sutun=None, deger=None, **_): if not (abone and sutun and deger is not None): return {"hata": "eksik alan"} return {"yazildi": True, "abone": abone, "sutun": sutun, "deger": deger} TOOLS = { "sayac_oku": {"function": t_read_meter, "fields": ("abone",), "side_effect": False}, "tuketim": {"function": t_consumption, "fields": ("onceki", "simdiki"), "side_effect": False}, "tarife": {"function": t_tariff, "fields": ("m3", "tip"), "side_effect": False}, "belge_ara": {"function": t_document, "fields": ("anahtar",), "side_effect": False}, "kayit_yaz": {"function": t_record, "fields": ("abone", "sutun", "deger"), "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 {"hata": "arac yok"} 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": ("sayac_oku", "tuketim", "tarife")}) 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": ("sayac_oku", "tuketim")}) elif t == 2: TASKS.append({"name": f"G{i + 1:02d}", "kind": "document", "subscriber": sub, "request": "sayac donusunde endeks farki nasil hesaplanir", "sequence": ("belge_ara",)}) else: TASKS.append({"name": f"G{i + 1:02d}", "kind": "record", "subscriber": sub, "request": f"{sub} abonesinin bedelini hesapla ve kayda yaz", "sequence": ("sayac_oku", "tuketim", "tarife", "kayit_yaz")}) def correct_answer(g): a = SUBSCRIBERS[g["subscriber"]] f = a["simdiki"] - a["onceki"] m3 = f + 100000 if f < 0 else f if g["kind"] == "consumption": return m3 if g["kind"] == "document": return "SY01" return _charge(m3, a["tip"]) # ---- 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 # The dictionary a tool returns is turned into text before it is given back to the model. SUMMARY_FIELDS = {"sayac_oku": ("onceki", "simdiki", "tip"), "tuketim": ("m3",), "tarife": ("tutar",), "belge_ara": ("kod",), "kayit_yaz": ("yazildi",)} FORMAT = {"raw": None, "truncated 7": 7, "truncated 4": 4, "summarized": None} def to_text(s): return " , ".join(f"{k} = {v}" for k, v in s.items()) def parse(text): """Reads the fields back from the observation text; a field whose value was cut off is dropped.""" d = {} for p in text.split(" , "): a, _, v = p.partition(" = ") v = v.strip(" ,") if v: d[a.strip(" ,")] = int(v) if v.lstrip("-").isdigit() else v return d def observe(name, result, form): """Raw , truncated , or summarized observation text.""" if form == "summarized": result = {k: v for k, v in result.items() if k in SUMMARY_FIELDS.get(name, ())} text = to_text(result) if FORMAT[form]: text = " ".join(text.split()[:FORMAT[form]]) return text def agent_observation(g, form="raw", max_steps=8, seed=SEED): """The shared agent loop; the only differences are that the observation is turned into text and read back, and that the accumulated observation tokens are sent again at every step.""" r = rng(seed ^ trace_id(g["name"])) state = {"abone": g["subscriber"], "anahtar": g["request"], "sutun": "bedel"} trace, step, tok, errors, accum = [], 0, 0, 0, 0 seq, k = g["sequence"], 0 while step < max_steps: step += 1 tok += 60 + accum # the accumulated observation is sent again at every step 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))] if choice == "kayit_yaz": state["deger"] = state.get("sonuc") s = call_tool(choice, state) trace.append((choice, "hata" in s)) tok += 30 text = observe(choice, s, form) accum += len(tokenize(text)) if "hata" in s: errors += 1 continue d = parse(text) state.update(d) if choice == "tuketim" and "m3" in d: state["sonuc"] = d["m3"] if choice == "tarife" and "tutar" in d: state["sonuc"] = d["tutar"] if choice == "belge_ara" and "kod" in d: state["sonuc"] = d["kod"] if choice == target: k += 1 return {"steps": step, "tokens": tok, "errors": errors, "observation": accum, "answer": state.get("sonuc")} for name in ("sayac_oku", "belge_ara"): sample = TOOLS[name]["function"](**{"abone": "A007", "anahtar": "endeks farki"}) for form in ("raw", "truncated 7", "summarized"): m = observe(name, sample, form) print(f"{name:<10} {form:<11} {len(tokenize(m)):>2} tokens {m}") print(f"{name:<10} truncated 7 fields read back :", " , ".join(parse(observe(name, sample, "truncated 7"))))
sayac_oku raw 19 tokens onceki = 78189 , simdiki = 78241 , tip = mesken , bolge = merkez sayac_oku truncated 7 9 tokens onceki = 78189 , simdiki = 78241 sayac_oku summarized 14 tokens onceki = 78189 , simdiki = 78241 , tip = mesken sayac_oku truncated 7 fields read back : onceki , simdiki belge_ara raw 19 tokens kod = SY01 , metin = endeks farki tuketimi verir sayac donusu 100000 eklenir belge_ara truncated 7 8 tokens kod = SY01 , metin = endeks belge_ara summarized 3 tokens kod = SY01 belge_ara truncated 7 fields read back : kod , metin
Both tools carry the same raw length, nineteen tokens, and the two forms cut them in very different
places. Truncation looks at position: in the sayac_oku (read meter) observation the last two fields, tip (type)
and bolge (region), drop because they remain after the seventh word. The summary looks at declaration: in
the same tool the type stays because it is declared, and the region goes because it is not. In the
document tool the order is reversed; there truncation keeps the code because it comes first, while the
summary throws away the whole text and drops the nineteen tokens to three.
The Numbers of the Four Forms
The same 24 fictional tasks run in four forms. Observation tokens are the total length of the observation texts produced across the loop; total tokens also include their being sent again.
def measure(form, seed=SEED): CALLS[0] = 0 ok = steps = tok = errors = obs = 0 for g in TASKS: s = agent_observation(g, form, seed=seed) ok += s["answer"] == correct_answer(g) steps += s["steps"] tok += s["tokens"] errors += s["errors"] obs += s["observation"] return ok / len(TASKS), CALLS[0], errors, steps, obs, tok print("observation form calls errors steps observation tokens total tokens" " task success A task success B tokens per answer") for form in FORMAT: a, c = measure(form), measure(form, SEED + 1) print(f"{form:<16} {a[1]:>6} {a[2]:>6} {a[3]:>5} {a[4]:>18} {a[5]:>12}" f" {a[0]:>14.4f} {c[0]:>14.4f} {a[5] / (a[0] * 24):>17.0f}") SUMMARY_FIELDS["sayac_oku"] = ("onceki", "simdiki") # type dropped from the declaration a = measure("summarized") print(f"narrow summary ( type not declared ) : task success {a[0]:.4f} , observation tokens" f" {a[4]} , tool errors {a[2]}")
observation form calls errors steps observation tokens total tokens task success A task success B tokens per answer raw 74 8 97 770 10103 0.9583 0.9583 439 truncated 7 74 8 97 428 9160 0.7917 0.7500 482 truncated 4 149 116 156 616 15945 0.2917 0.2917 2278 summarized 74 8 97 401 9229 0.9583 0.9583 401 narrow summary ( type not declared ) : task success 0.7917 , observation tokens 311 , tool errors 8
The baseline is the first row: the raw observation produces 770 observation tokens, spends 10103 total tokens, and holds task success at 0.9583. The second row puts side by side what truncation adds and what it drops. Observation tokens fall to 428, total tokens to 9160, but task success becomes 0.7917. Calls, steps, and tool errors do not change at all; the four tasks that drop are all commercial subscribers, and in all four the invoice comes out exactly half. When the type drops, the tariff tool uses the declared default, that is residential. Truncation produced no error; it produced a silent wrong answer.
The third row is a clear example of the course’s second claim. Squeezing the truncation to four words
cuts the second index in the sayac_oku observation as well; the tuketim (consumption) tool returns with a
missing field, the chain breaks, and the agent retries the same step. Tool errors rise from 8 to 116,
calls from 74 to 149, steps from 97 to 156. The result: observation tokens are still lower than
in the raw form (616), but total tokens jump to 15945, one and a half times the raw form. Making the
observation cheaper made the loop more expensive.
Declaration and Position
The last row is the number of the summary and it stands alone in the table: task success is 0.9583 in both runs, the same as the raw observation; observation tokens are 401, less than half of the raw; tokens per correct answer fall from 439 to 401. The summary is both the cheapest and the most accurate form, because it reads what to throw away not from position but from declaration.
This advantage is not free, and the last output line writes its cost. When only the tip field is
dropped from the summary’s declaration, task success falls to 0.7917, to where truncation dropped it.
A summary is only as good as its declaration; whoever knows which field will be needed at the next step
must declare it, and if one does not know this, a summary is no safer than truncation.
There is one row in the table that looks reversed: the summary’s observation tokens are lower than those
of truncated 7 (401 against 428) but its total tokens are higher (9229 against 9160). The reason is
the arithmetic of accumulation. An observation is sent again at every step that comes after it; therefore
an early observation is costlier than a late one. The summary keeps the sayac_oku observation at
14 tokens (truncation at 9), and that observation is in the first step of most tasks. In contrast,
the summary’s big gain is in belge_ara (search docs; from 19 to 3), and that observation is the last step of
single-step tasks, so it is never sent again.
Summary
- A tool result is not written to the state directly: it is turned into an observation text, read back, and sent again in the prompt at every step.
- The raw observation gives 770 observation tokens, 10103 total tokens, and a task success of 0.9583; this is the baseline.
- Truncating to seven words drops the tokens to 9160 but, because it drops the subscriber type, halves four commercial invoices and pulls task success to 0.7917; the tool errors do not rise at all, the wrong answer is silent.
- Truncating to four words breaks the chain: tool errors rise to 116, steps to 156, total tokens to 15945, and task success falls to 0.2917; making the observation cheaper makes the loop more expensive.
- The summary with a declaration keeps the task success of the raw observation while dropping the observation tokens to 401 and the tokens per correct answer to 401; when the type is removed from the same summary, task success falls to 0.7917.
- In an accumulating context an early observation is costlier than a late one: an observation is sent again at every step after it.
Next Step
In these two lessons the tools were defined, called, and their returns formatted. All of them had one thing in common: each tool did a fixed job and returned. A reading is a fixed number of fields, a computation a fixed number of operations, a search a fixed text. That is why how much a call would cost was never asked. In a tool that runs a program the agent wrote itself, this assumption lifts: how many steps it will take, how many cells it will occupy, and how many outputs it will produce are not written in the definition and are seen only while it runs. The next lesson builds such a tool inside a sandbox with resource limits, counts the calls that exceed the declared limit, and writes separately how many tasks stopping it drops.
To keep your progress and take notes, Log in
My notes
Log in to take notes.