---
title: 'What Is an Agent'
source: 'https://academia.sh/en/courses/ai-agents/what-is-an-agent'
course: 'AI Agents'
language: en
updated: '2026-10-21T05:02:26+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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.'
---

# 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.

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.

```python
# 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.

```python
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.
