What an Agent Is
By the end of this lesson
Describe multi-step autonomous behaviour and its risks.
The tool calling lesson ended on a loop. The model asks for a lookup, your code runs it, the result goes back, and the model may ask for another. Cap the rounds, it said, because nothing in the model stops it asking.
An agent is that loop taken seriously. You do not script the steps. You give the model a goal, a set of tools and a stopping condition, and it decides what to do next each time round. That is the whole definition, and it is smaller than the word suggests.
What changes is not the mechanism but who chose the order of operations. In everything so far, you did. In an agent, the model does, at runtime, differently each time. Every property in this lesson — the useful ones and the alarming ones — follows from that single shift.
One turn of the loop, which is all there is to it:
The goal and the tools go in
A system prompt stating the objective and the constraints, the tool schemas, and the transcript of everything that has happened so far this run. The transcript grows every turn, so input cost grows every turn.
The model picks the next action
It returns either a tool call or a final answer. That choice is the autonomy. Nothing in your code decided that a document search should come before an expenses lookup.
Your code validates and executes
Exactly as in the tool calling lesson: name against the allow-list, arguments against the schema, action against the authenticated user's permissions. None of this moves to the model because the model is now choosing the calls.
The result is appended to the transcript
Including failures. A tool that errored should come back as data saying so, because a model told the lookup failed can try something else, where an exception ends the run.
Check the limits, then go round again
Step count, elapsed time, tokens spent. This check is yours and it is not optional. It is the only thing in the design that guarantees the loop ends.
MAX_STEPS = 8
MAX_TOKENS = 20_000
MAX_SECONDS = 60
def run_agent(goal: str, caller: User) -> AgentRun:
transcript = [
{"role": "system", "content": AGENT_SYSTEM_PROMPT},
{"role": "user", "content": goal},
]
run = AgentRun(caller=caller.id, goal=goal)
started = time.monotonic()
for step in range(1, MAX_STEPS + 1):
if run.tokens_used > MAX_TOKENS:
return run.stopped("token budget exhausted")
if time.monotonic() - started > MAX_SECONDS:
return run.stopped("time limit reached")
reply = chat(model=CHAT_MODEL, temperature=0, messages=transcript, tools=TOOL_SCHEMAS)
run.record(step=step, reply=reply) # tokens, tool names, latency
transcript.append(reply.message)
if not reply.tool_calls:
return run.finished(answer=reply.text)
for call in reply.tool_calls:
result = run_tool_call(call, caller) # allow-list, schema, permissions
transcript.append(
{"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)}
)
return run.stopped("step limit reached")- The for loop with MAX_STEPS is the difference between an agent and a runaway process. A while loop waiting for the model to decide it is finished has no upper bound, and a model that keeps finding one more thing to check will keep going until something external stops it.
- Three independent limits, because they fail in different ways. Steps catch a model cycling between two tools. Tokens catch a single step that retrieves something enormous. Time catches a slow provider. Any one of them alone leaves a gap.
- The token check reads accumulated usage from previous turns, so it is measuring the whole run rather than one call. This matters because the transcript is resent every turn — eight steps is not eight times the cost of one call, it is closer to the sum of a growing prompt, and the later steps are the expensive ones.
- run_tool_call is unchanged from the tool calling lesson, deliberately. Autonomy over which tool to call does not relax any check on whether the call is permitted. If anything the checks matter more, because you are no longer able to predict the sequence.
- Every stopping path returns a run object rather than raising. "Stopped at the step limit" is an outcome your interface has to handle and show honestly — a partial result presented as a finished answer is worse than saying the assistant gave up.
- run.record on every step is what makes this debuggable at all. Without a per-step log of tool names, arguments, tokens and latency, a bad run is unreproducible, because the next run will take a different path.
The same task — "summarise this month's travel spend against policy for my team" — built two ways:
| Fixed chain you wrote | Agent loop | |
|---|---|---|
| Who decides the order | You, at design time | The model, at runtime |
| Model calls per request | A known number | Between one and your step limit |
| Cost per request | Predictable within a narrow range | Varies severalfold, and grows as the transcript grows |
| Testing | Each step tested in isolation, the whole thing end to end | You can test outcomes on a fixed set, not the path taken |
| Debugging a bad result | Find the step that failed | Read the whole run, and accept it may not recur |
| Handles a request you did not anticipate | No. It does what it was built to do | Sometimes, which is the entire reason to consider one |
Four risks that are properties of the design rather than bugs to be fixed. Plan for each before you ship one:
- The path is not repeatable
- The same question can take four steps today and seven tomorrow, using different tools, and produce differently worded answers. Two colleagues asking the same thing may get different results. For anything where consistency is part of the requirement, this is disqualifying on its own.
- It can loop without progressing
- Searching, finding nothing useful, rephrasing, searching again is a perfectly reasonable-looking sequence that gets nowhere. The model has no reliable sense that it is going in circles. Your step limit is what ends it, which is why the limit is load-bearing rather than defensive.
- Cost is open-ended without caps
- Each turn resends the whole transcript, so cost per step climbs through the run. A single request can cost ten or twenty times a straightforward call. Multiply by users and by retries and the bill is genuinely hard to forecast, which is a real objection and not a small one.
- Errors compound across steps
- A step that works 95 percent of the time is fine on its own. Six of them in sequence, each depending on the last, gives you roughly a 74 percent chance of a clean run. Nothing in the loop notices the bad step, and later steps reason confidently from it.
That last figure is worth sitting with, because it is arithmetic rather than pessimism. Chain enough steps and per-step reliability that sounds excellent becomes an unreliable whole. It is also why longer runs are not better runs — each additional step adds cost and subtracts from the probability that the answer is sound.
The practical consequence is to keep runs short by design, verify intermediate results in code where you can, and put a human in front of anything the run is about to do rather than say.
Summary
- An agent is a loop where the model chooses the next step at runtime, rather than following an order you wrote
- The mechanism is tool calling; every allow-list, schema and permission check still applies on every call
- Step, token and time limits enforced in your code are the only guarantee that a run ends
- Cost grows through a run because the transcript is resent each turn, and errors compound across dependent steps
- If you can write the steps down at design time, write them down — an agent is for when the path genuinely cannot be known in advance
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
Chain or agent, and what would change your mind
Three requests for the internal assistant. First: when someone asks a policy question, retrieve the relevant passages and answer with citations. Second: when a manager asks why their team's travel spend rose, find out. Third: every Monday, produce a one-page summary of last week's claims over 500 pounds.
Decide chain or agent for each, and for the ones you would build as a chain, say what evidence would make you reconsider.
Show solution
The first is a chain, and not a close call. Embed, retrieve, assemble, generate — four steps, in that order, every time. That is the pipeline from the previous lesson, and wrapping it in a loop would add cost and variance while removing your ability to test it.
The second is the reasonable candidate for an agent. "Find out why" is open: it might be one large claim, a new client site, a team that grew, a policy change, or a coding error. What you look at second depends on what the first lookup showed, and writing out every branch is not practical.
The third is a chain, though it feels like an agent because the output is a report. Query claims over the threshold, group them, summarise each group, assemble the page. The steps are identical every Monday, which is the tell. It also runs unattended, where a loop that decides to investigate something for forty steps has nobody watching.
What would make you reconsider the first: users asking questions that need a document lookup and a data lookup in an order you cannot predict. At that point a router — classify the question, then pick one of a few fixed chains — is usually the next step rather than a full agent. That middle ground is the subject of the next lesson.
Worth noticing what the answers have in common. Two of the three are chains, and the one agent is the one where a human will read the output and judge it. That ratio is normal.
Try it yourself
Watch a run go wrong
Build the loop from this lesson with two read-only tools: a document search and an expenses lookup. Set MAX_STEPS to 10 and log every step's tool name, arguments and token count.
Then ask it something your data cannot answer — a spend figure for a team that does not exist. Read the log rather than the answer.
Show solution
The usual pattern is several attempts with rephrased arguments before it stops. Each one looks locally sensible: the lookup returned nothing, so try a different spelling, then a different month, then search the documents instead. None of it makes progress, and nothing in the loop recognises that.
Check the token count per step. It climbs, sometimes steeply, because the whole transcript including every empty result is resent each turn. Step eight costs several times step one. This is the cost shape that makes an uncapped loop dangerous rather than merely wasteful.
Look at how it ended. If it hit your step limit, the limit did its job. If it produced a confident figure for a team that does not exist, you have the module 3 failure appearing in a new place — a tool returning nothing is not a fact, and a model with no grounding instruction will fill the gap.
The fix that helps most is not a larger limit. It is telling the model, in the system prompt, what to do when a lookup comes back empty: stop and report what was tried. Giving the loop an honest exit is the same move as giving a grounded prompt an explicit refusal.
Doing this once changes how you think about step limits. They stop feeling like a safety net and start feeling like the control that makes the design viable.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.