Skip to content
academia.sh

Lesson 01 / 22

What Is an Agent

Building the combination of model, tool and loop and counting the whole loop instead of a single answer: on 24 fictional tasks a single-step tool-less call gives a task success of 0.0417 while a loop with a budget of eight steps gives 0.9583, and on the second run the baseline falls to 0.0000 while the loop stays at 0.9583. The cost is a rise from 24 steps to 97 steps, from zero tool calls to 74 calls, from 1440 tokens to 11978 tokens and from zero to 8 tool errors. Read by kind, the loop finishes every task of three steps or fewer and stays at two thirds on four-step write tasks: the place where success falls is step depth. The agent does not learn; the only thing that changes over a run is the observations it holds.

Published Updated

In this lesson

  • Defines an agent as the combination of model, tool and loop and tells what is left when one of the three parts is missing.
  • Builds a fictional measurement network environment, a set of five tools, a set of twenty-four tasks and the agent loop with the standard library.
  • Counts the task success, steps, tool calls, tokens and tool errors of a single-step tool-less call and of the loop side by side.
  • Reads task success separately across four task kinds to show where the fall comes from.
Contents

The Embeddings and Retrieval-Augmented Generation course measured a single question over sixteen lessons: which information goes into the model. Chunking, embedding, retrieval and reranking were the settings of that choice, and the cost of every setting was written in tokens. But that pipeline carried two silent assumptions. The choice was always made by us: code decided which chunk would be retrieved, and the model only read what was retrieved. And the pipeline was single-pass: the request arrived, the context was built, one answer was produced, and the job was over.

This course removes both assumptions. The model itself decides which tool to call, sees the result the tool returns, and when it finds that result insufficient it tries another tool. What comes out is not a single answer but a loop, and its measurable surface is not that of a single answer but of the whole loop. This lesson builds that loop. Where the loop stops is a separate question and it stays open until the fourth lesson.

  • AA1. The code is a simulator and the environment is fictional: the fictional subscriber records of the measurement network and a fictional document collection. No real model endpoint, no real tool server and no real runtime is called. The seed is 20260218, the second run 20260219.
  • AA2. The environment holds 60 fictional subscribers, five regions, two subscriber types and 5 fictional documents.
  • AA3. The tool set is five tools and each tool sees only the fields it declares; fields outside the schema are filtered out, and a missing field is the tool’s own error.
  • AA4. The task set is 24 tasks and each task is solved by a known tool sequence. There are four kinds: document (one step), consumption (two steps), charge (three steps), record (four steps).
  • AA5. The share of picking the right tool falls with depth. It is 0.80 at the first step, then falls by 0.09 at each step and levels off at 0.34. A wrong selection is a random one of the five tools.
  • AA6. Observations accumulate in a state dictionary; the input of the next tool is the output of the previous step.
  • AA7. When the job is done the agent stops with a share of 0.88; in the remaining share it keeps wandering and can corrupt the accumulated result.
  • AA8. write_record is the only tool with a side effect, and when approval is on each of its calls counts as an approval point.
  • AA9. Task success is the correctness of the answer: whether the final result the loop produced equals the known correct answer of the task.
  • AA10. The baseline is a single-step, tool-less call. The model sees only the request text.
  • AA11. The counted units are steps, tool calls, tokens, tool errors and task success. Time is not written.
  • AA12. Every measurement is run with two seeds; a difference below the run-to-run difference counts as unmeasured.

Model, Tool and Loop

An agent is the combination of three parts. The model decides what to do at each step. The tool is the only hand the model has reaching outside: looking at a record, doing a calculation, writing a row. The loop is the control flow that binds the two: the model picks a tool, the tool runs, its result comes back as an observation, and at the next step the model sees that observation too.

If one of the three is missing there is no agent. A loop without a model is a fixed script. A tool without a loop is the single tool call measured in the Building Applications with Model APIs course: that lesson counted the shape of the call, and this course counts the sequence of calls. A model without tools is a single-pass call, and that is exactly the baseline of this lesson.

The Reinforcement Learning and Recommender Systems course of the Machine Learning and Data Science curriculum also speaks of an agent; the agent there learns from experience and its behavior changes from run to run, while the agent here does not learn — the only thing that changes over a run is the observations it holds.

Fictional Environment, Tool Set and Loop

The code below builds the environment, the five tools, the twenty-four tasks and the agent loop. The same definition is used throughout the course.

# SIMULATOR -- FICTIONAL. No real model endpoint, tool server or runtime
# is called; the environment, the tools and the tasks are all fictional.
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)}


g = TASKS[0]
s = agent(g)
print(f"environment : {len(SUBSCRIBERS)} subscribers , {len(DOCUMENTS)} documents ,"
      f" {len(TOOLS)} tools , {len(TASKS)} tasks")
print(f"{g['name']} ({g['kind']}) : {g['request']}")
print("  trace :", " -> ".join(f"{a}{' [error]' if h else ''}" for a, h in s["trace"]))
print(f"  answer {s['answer']}  correct {correct_answer(g)}  steps {s['steps']}"
      f"  tokens {s['tokens']}  tool errors {s['errors']}")
environment : 60 subscribers , 5 documents , 5 tools , 24 tasks
G01 (document) : sayac donusunde endeks farki nasil hesaplanir
  trace : consumption [error] -> write_record [error] -> search_docs
  answer SY01  correct SY01  steps 4  tokens 462  tool errors 2

A single trace shows the whole mechanics of the loop. The task is one step long: calling search_docs once would be enough. In the first two steps the agent reaches for other tools, and both give a tool error, because the state dictionary does not hold the fields those tools declare. At the third step it finds the right tool, and at the fourth step it stops. A one-step job cost four steps and 462 tokens. A measurement that looks at the count of a single answer never sees these three steps.

The Count of a Single-Step Tool-Less Call and of the Loop

The baseline is tool-less: the model sees only the request text, no tool is called and no observation comes back. The same twenty-four tasks are run in two setups and with two seeds.

def single_step(g, seed=SEED):
    """BASELINE: single-step, TOOL-LESS call; no tool is called."""
    r = rng(seed ^ trace_id("taban" + g["name"]))
    answer = (list(DOCUMENTS)[int(r() * len(DOCUMENTS))] if g["kind"] == "document"
              else 10 + int(r() * 900))
    return {"steps": 1, "tokens": 60, "errors": 0, "answer": answer}


def measure(build, seed=SEED):
    CALLS[0] = 0
    success = steps = tok = errors = 0
    for g in TASKS:
        s = build(g, seed)
        steps, tok, errors = steps + s["steps"], tok + s["tokens"], errors + s["errors"]
        success += s["answer"] == correct_answer(g)
    return success / len(TASKS), steps, CALLS[0], tok, errors


def loop(g, seed):
    return agent(g, max_steps=8, seed=seed)


print("setup                 success A  success B   step  calls  tokens error")
for name, build in (("single-step tool-less", single_step), ("agent loop", loop)):
    a, b = measure(build), measure(build, SEED + 1)
    print(f"{name:<21} {a[0]:>9.4f} {b[0]:>10.4f} {a[1]:>6} {a[2]:>6} {a[3]:>9} {a[4]:>5}")
print("loop , by kind :", " , ".join(
    f"{t} {sum(agent(g)['answer'] == correct_answer(g) for g in TASKS if g['kind'] == t)}"
    f"/{sum(g['kind'] == t for g in TASKS)}"
    for t in ("document", "consumption", "charge", "record")))
setup                 success A  success B   step  calls  tokens error
single-step tool-less    0.0417     0.0000     24      0      1440     0
agent loop               0.9583     0.9583     97     74     11978     8
loop , by kind : document 7/7 , consumption 8/8 , charge 6/6 , record 2/3

The baseline is almost zero, and why it is zero is written in the definition itself: the meter readings are in the environment, not in the request text. A tool-less call can produce a number, but that number is a guess. The 0.0417 of the first run comes from a single hit, and that hit is a guess of a document code; on the second run it drops to 0.0000. The run-to-run difference of the baseline is 0.0417, so the baseline’s only success lies inside the measurement band and does not count as a capability.

The loop gives 0.9583 on the same task set, and this number is the same on both runs. The difference is 0.9166 and stands twenty times above the band; this gain is measured. Its cost sits in the same row: steps from 24 to 97 (4.04 times), tool calls from zero to 74, tokens from 1440 to 11978 (8.32 times) and tool errors from zero to 8. A tool-less call cannot have a tool error; if the error count is above zero, there is a loop.

The last row shows where the gain ends. Every task of one, two and three steps is completed. On the four-step record tasks, two of the three tasks are correct. Because the share of picking the right tool falls with depth, the chance of missing a step builds up as the sequence grows. In a subset of three tasks the gap between 2/3 and 3/3 is not a finding by itself; but it says on which axis the fall lies, and that axis is step depth.

Summary

  • An agent is the combination of model, tool and loop; a tool without a loop is a single tool call, and a model without tools is a single-pass call.
  • The environment, the five tools, the twenty-four tasks and the loop are built inside the lesson with the standard library and all of them are fictional; no real model endpoint or tool server is called.
  • The single-step tool-less call gives a task success of 0.0417, and 0.0000 on the second run; this single hit lies inside the measurement band and does not count as a capability.
  • The loop with a budget of eight steps gives 0.9583 on both runs; the gain of 0.9166 stands far above the band.
  • The cost is a rise from 24 to 97 in steps, from zero to 74 in tool calls, from 1440 to 11978 in tokens and from zero to 8 in tool errors.
  • By kind, every task of one, two and three steps is completed, and two thirds of the four-step write tasks are completed.

Next Step

In this lesson the loop was measured as a box: the task going in, the answer coming out and the total cost in between. The inside of the box showed up in a single trace, and that trace carried two wrong tool selections. The last row by kind points the same way: as the job grows longer, success falls. The next lesson opens the inside of the loop step by step — it writes which part each step corresponds to as perception, reasoning, action and observation, and counts the share of picking the right tool by step depth. The same number is read separately across the four task kinds, because the kinds have different sequence lengths and that is exactly where the fall hides in a single average.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close