Area 03 // AI AgentsLesson 64 / 14025 min read
Capstone: build an agent without a framework
You build a complete agent in one file (three tools, the agent loop, stop conditions, error handling and a real Claude model, plus an offline scripted mode for tests), so you understand exactly what every agent framework does under the hood.
Think of it like this
Analogy
Learning to drive in a car with an automatic gearbox is easy, but a mechanic who has rebuilt a manual gearbox understands what every automatic is doing. This capstone is rebuilding the gearbox: once you've written an agent by hand, every framework becomes "oh, that's just the loop with extras".
The problem it solves
Agent frameworks hide the loop, which makes them quick to start and hard to debug: when an agent misbehaves, you need to know exactly what was sent to the model, what came back, how tool results were returned and why the loop stopped. Building one small agent by hand, with tests, gives you that understanding permanently.
How it works, step by step
The finished agent is examples/mini-agent/agent.py in the course repository (about 120 lines), with test_agent.py alongside. It answers travel-cost questions using three tools.
- Define the tools as plain Python functions:
hotel_price(city, nights),calculator(expression)(arithmetic only, no code execution), andfinish(answer). - Describe them for the model: name, description and a strict JSON Schema each (tool calling).
- Write a system prompt: role, "use the tools, never guess numbers", "call finish when you know the answer".
- Hide the model behind one method,
next_calls(messages) -> (tool_calls, tokens_used):AnthropicModelcallsclient.messages.create(model=..., system=..., tools=..., messages=...), appends the assistant turn to the history, and returns itstool_useblocks.ScriptedModelreplays a fixed plan, for offline tests and CI.
- Write the loop (agent loop):
- Ask the model for its next calls.
- Run each call; on an exception, return
{"error": ...}withis_error: Trueinstead of crashing. - If
finishsucceeded, return its answer. - Append all tool results in one user message.
- Add stop conditions (stop conditions): a step limit, a token budget, and "the model answered without calling finish".
- Test the loop's behaviour, not the model's: reaching an answer, the step limit, the token budget, error recovery, and the calculator refusing code.
- Run it for real:
AGENT_PROVIDER=anthropic python agent.py "…".
See it in code
- Install
pip install anthropic pytest- Needs
- Python 3.10+. Tests and the default scripted run need no key. The real run needs ANTHROPIC_API_KEY. Model ID as of November 2026.
The loop at the heart of agent.py (tools, schemas and the two model classes are in the file):
def run(question: str, model, max_steps: int = 6, max_tokens: int = 30_000, log=print) -> str:
messages = [{"role": "user", "content": question}]
used = 0
for step in range(1, max_steps + 1): # stop condition: step limit
calls, tokens = model.next_calls(messages)
used += tokens
if not calls:
return "Stopped: the model answered without calling finish."
results = []
for call in calls: # run every call from this turn
try:
output = TOOLS[call.name](**call.input)
ok = True
except Exception as e: # errors go back to the model
output, ok = {"error": str(e)}, False
log(f"step {step}: {call.name}({json.dumps(call.input)}) -> {json.dumps(output)}")
if call.name == "finish" and ok: # stop condition: task done
return output["answer"]
results.append({"type": "tool_result", "tool_use_id": call.id,
"content": json.dumps(output), "is_error": not ok})
messages.append({"role": "user", "content": results}) # all results in ONE message
if used > max_tokens: # stop condition: budget
return "Stopped: token budget used up."
return "Stopped: step limit reached without an answer."cd examples/mini-agent
pytest -q
python agent.py
AGENT_PROVIDER=anthropic python agent.py "Cost of 2 nights in Porto plus a 150 GBP train?"..... [100%]
5 passed in 0.01s
step 1: hotel_price({"city": "Lisbon", "nights": 3}) -> {"city": "Lisbon", "nights": 3, "total_gbp": 285}
step 2: calculator({"expression": "285 + 400"}) -> {"result": 685}
step 3: finish({"answer": "About 685 GBP: 285 for 3 hotel nights plus the 400 flight."}) -> {"answer": "About 685 GBP: 285 for 3 hotel nights plus the 400 flight."}
answer: About 685 GBP: 285 for 3 hotel nights plus the 400 flight.With a real model, the steps are chosen by Claude instead of the script; the loop, tools and limits are identical.
A real example
Example
From capstone to a real internal agent
A team needed an agent that answers "how much will this customer's renewal cost?" using their pricing API, discount rules and a calculator. They started from exactly this file: swapped the hotel tool for get_plan_price and get_customer_discounts, kept calculator and finish, added tracing and a cost budget, and wrote twenty scripted-model tests for edge cases (unknown plans, expired discounts). Two weeks later it was in production behind a human approval step for quotes over £50,000.
The easy way
Start here
Copy examples/mini-agent, run the tests, then change one thing at a time: add a tool, change the question, set AGENT_PROVIDER=anthropic, lower max_steps and watch it stop. Print messages after each turn to see exactly what the model receives.
When you grow: the production setup
Turning this into a production agent means adding: tracing of every step, prompt caching for the system prompt and tools, retries with backoff and model fallbacks, idempotent action tools with permission checks, human approval for risky tools, context management for long tasks, durable state if it must pause, and evals of outcomes and trajectories, or moving to a framework once those needs pile up.
Trade-offs
What you gain
- Complete understanding and control of every request and decision.
- No framework dependency; easy to test with a scripted model.
- Small enough to read in one sitting.
What it costs you
- You build tracing, durability, multi-agent support and approvals yourself.
- More code to maintain as requirements grow.
- Easy to miss edge cases frameworks already handle.
When not to use it
- You need durable checkpoints, complex graphs or many agents from day one: start from a framework, ideally after building this once.
Common mistakes
- Forgetting to append the assistant turn, breaking tool_use/tool_result pairing.
- Sending each tool result as a separate message.
- No
finishsignal, so the loop's end is ambiguous. - Testing only with the real model (slow, costly, flaky) instead of a scripted one.
- Letting a calculator tool run arbitrary code with
evalon unvalidated input.
Interview questions
Try answering out loud first, then open the model answer.
Walk me through implementing an agent from scratch.
Model answer
Define tools as functions with clear descriptions and strict JSON Schemas. Keep a message list starting with the user's request. Loop with a step limit: call the model with system prompt, tools and messages; append its full turn; if it requested tools, execute each (in parallel where safe), catching errors into error results; append all results in one user message; stop on a finish tool or final answer, budget exhaustion or limits, returning partial progress when stopping early. Wrap the model in an interface so tests use a scripted model, and add tracing.
How do you test an agent?
Model answer
Unit-test tools directly. Test the loop with a scripted model covering success, tool errors, step and token limits, malformed calls and refusals, all offline. Then run evals with the real model on representative tasks, scoring final outcomes and trajectories (tool selection, arguments, efficiency), several runs per case to measure variance, tracked over time and in CI where cheap enough.
When would you move from a hand-written agent to a framework?
Model answer
When requirements outgrow the loop: durable state and resumption, human-in-the-loop pauses across processes, multi-agent coordination, complex branching, built-in tracing and evaluation integrations, or team-wide standardisation. Until then a small hand-written loop is clearer and easier to debug.
Quiz
Pick an answer to see whether it’s right and why.
0 of 5 answered
Key terms
- Agent
- A system where a model decides, in a loop, which tools to call and what to do next until a task is done.
- Agent loop
- The cycle at the heart of every agent. The model picks an action, the action runs, the result is added to the context, repeat until a stop condition.
- Tool calling (function calling)
- A model replying with a request to run one of your functions, with arguments; your app runs it and sends the result back.
- Stop condition
- A rule that ends an agent loop, such as a final answer, a maximum number of steps, a token or time budget, or an error.
- Tool result
- The output of a tool your app ran, sent back to the model linked to the call's ID so it can continue.
Before and after
Progress is saved in this browser only.