Page 3 of 13 · The engine
The agent loop and its tools
Every agent, from a chatbot with search to Claude Code, is the same loop: the model reads its context, picks a tool, your code runs it, the result goes back into the context, repeat until a stop condition. Understand this loop and every framework becomes a variation of it.
6 runnable samples1 prediction3 questions
Level 1 · The rules
Anatomy of one loop
The model never runs anything. It asks; your harness decides and executes.
The four rules of the loop
- The model only proposes a tool call; the harness validates and executes it.
- Every tool result, including errors, goes back into the context.
- The loop ends on a final answer or a budget: max steps, tokens, time or money.
- Anything not in the context does not exist for the model on the next step.
def fake_model(context): if not any(m["role"] == "tool" for m in context): return {"tool": "run_tests", "args": {}} return {"final": "Tests pass: 12 of 12."}TOOLS = {"run_tests": lambda: "12 passed, 0 failed"}def agent(task, max_steps=5): context = [{"role": "user", "content": task}] for step in range(max_steps): action = fake_model(context) if "final" in action: return action["final"], step + 1 result = TOOLS[action["tool"]](**action["args"]) context.append({"role": "tool", "content": result}) raise RuntimeError("budget exhausted")print(agent("Check that the test suite passes"))
[ OUTPUT ]
Press Run to see the output this code printed when the course was built.
Level 1 · The rules
Tools are APIs for a model
A tool is a name, a description and an input schema. The model reads all three to decide when and how to call it, so they are documentation and interface at once.
import jsonread_file = { "name": "read_file", "description": ( "Read a UTF-8 text file from the repo. Use before editing. Returns at most 400" " lines." ), "input_schema": { "type": "object", "properties": {"path": {"type": "string", "description": "Repo-relative path"}}, "required": ["path"], },}print(json.dumps(read_file["input_schema"]["required"]))
[ OUTPUT ]
Press Run to see the output this code printed when the course was built.
When the model asks for a tool that does not exist, a harness without a check crashes. That is one of the most common agent failures: the model invents a plausible tool name.
class ToolNotFoundError(Exception): passTOOLS = {"read_file": lambda path: "contents"}def dispatch(call): if call["tool"] not in TOOLS: raise ToolNotFoundError(call["tool"]) return TOOLS[call["tool"]](**call["args"])dispatch({"tool": "open_file", "args": {"path": "README.md"}})
[ TRACEBACK ]
Press Run to see the output this code printed when the course was built.
TOOLS = {"read_file": lambda path: "# README"}def dispatch(call): if call["tool"] not in TOOLS: return ( "error: unknown tool " + call["tool"] + ". Available: " + ", ".join(TOOLS) ) return TOOLS[call["tool"]](**call["args"])print(dispatch({"tool": "open_file", "args": {"path": "README.md"}}))print(dispatch({"tool": "read_file", "args": {"path": "README.md"}}))
[ OUTPUT ]
Press Run to see the output this code printed when the course was built.
Level 2 · Why it works
Why stop conditions and budgets are not optional
A model can loop forever: retrying a failing command, re-reading the same file, or "almost done" for 80 steps. Only the harness can stop it.
class BudgetExceededError(Exception): passdef stuck_model(context): return {"tool": "run_tests", "args": {}}def agent(max_steps=3): context = [] for _ in range(max_steps): action = stuck_model(context) context.append("tests failed") raise BudgetExceededError(f"stopped after {max_steps} steps")agent()
[ TRACEBACK ]
Press Run to see the output this code printed when the course was built.
Step budget
Max loop iterations. Catches retry loops.Token budget
Max tokens per run. Catches context bloat.Cost budget
Max dollars per run. Catches expensive fan-out.Time budget
Wall-clock limit. Catches hung tools.
Structured outputs (contract)
When another program consumes the model's answer, ask for JSON that matches a schema and validate it. Free text is for humans; schemas are for code.
class SchemaValidationError(Exception): passdef validate(obj, required): missing = [k for k in required if k not in obj] if missing: raise SchemaValidationError("missing: " + ", ".join(missing)) return objmodel_output = {"title": "Add login", "points": 3}validate(model_output, ["title", "points", "acceptance_criteria"])
[ TRACEBACK ]
Press Run to see the output this code printed when the course was built.
Level 3 · Mastery
Trade-offs seniors talk about
| Choice | Gain | Cost |
|---|---|---|
| More tools | More capability | Every definition costs context on every call, and choice gets harder |
| Parallel tool calls | Lower latency | Harder to reason about ordering and side effects |
| Higher step budget | Solves longer tasks | Higher cost and more room to drift |
| Errors returned as results | Self-correction | Can loop on the same error without a budget |
| Strict schemas | Reliable integration | Some loss of nuance; needs retry on validation failure |
Predict the output
Level 1 answer before you look
Check your understanding
Levels 1 to 3 open a question to see the answer
Who executes a tool call: the model or the harness?
The harness. The model only emits a structured request. That separation is what makes permissions, hooks and validation possible.
Why return tool errors to the model instead of raising?
The model can read the error and correct itself on the next step. Raising ends the run and wastes the work so far. Pair it with a budget so a repeated error cannot loop forever.
Name four budgets a harness should enforce.
Steps, tokens, cost and wall-clock time.