Building your first agent
An agent is a model in a loop with tools. That sentence is the whole architecture, and in this lesson we make it concrete. We define one tool, write the loop, and watch a model use the tool to answer a question it couldn’t answer alone.
Every example below runs against a fixture: a fake model that behaves
deterministically, so you can predict what happens and check yourself. The
complete program is site/examples/building-agents/agent-loop/agent.py in
the repository. Swapping in a real model changes one function.
A tool is a function plus a description
Section titled “A tool is a function plus a description”The model can’t run code. What it can do is emit text that says “call this function with these arguments”. So a tool has two halves: the function you run, and the description the model reads to decide when to ask for it.
def get_weather(city: str) -> str: data = {"Amsterdam": "14°C, rain", "Lisbon": "27°C, sun"} if city not in data: raise LookupError(f"no data for {city}") return data[city]
TOOLS = { "get_weather": { "fn": get_weather, "description": "Current weather for a city. Args: city (str).", },}Predict before you run.
Predict the output
Section titled “Predict the output”In the lesson, TOOLS is a Python dict that maps a tool name to its description and to the plain function fn that runs it. get_weather returns a fixed string per city.
What does this print?
print(TOOLS["get_weather"]["fn"]("Lisbon"))27°C, sun
Output verified in CI from site/examples/building-agents/agent-loop/tool_call.py.
The tool is a plain function. Look up Lisbon in the dict.
What the model gets
Section titled “What the model gets”In the lesson, TOOLS is a Python dict that maps each tool name to a description and to the plain function that runs it. A real model API receives the tool definitions with every request.
What does the model get to see of get_weather?
Which part of the tool is text for the model, and which part is code for your loop?
The description matters as much as the code. The model chooses tools by reading descriptions, so a vague one (“weather stuff”) gets called at the wrong times and with the wrong arguments.
The loop
Section titled “The loop”Now the fixture model. It looks at the last message and either asks for a tool or gives a final answer.
def fake_model(messages): last = messages[-1] if last["role"] == "user" and "weather" in last["content"]: city = last["content"].split(" in ")[-1].rstrip("?") return {"tool": "get_weather", "args": {"city": city}} if last["role"] == "tool": if last["content"].startswith("error:"): return {"answer": f"I could not check. The tool said: {last['content']}"} return {"answer": f"It is {last['content']} there."} return {"answer": "I can only help with weather."}And the loop itself. Read it slowly. Every agent framework you ever use is this with more error handling.
def run(question, model=fake_model, max_steps=5): messages = [{"role": "user", "content": question}] for _ in range(max_steps): reply = model(messages) if "answer" in reply: return reply["answer"] tool = TOOLS.get(reply["tool"]) if tool is None: result = f"error: unknown tool {reply['tool']}" else: try: result = tool["fn"](**reply.get("args", {})) except Exception as exc: result = f"error: {exc}" if str(exc) else f"error: {type(exc).__name__}" messages.append({"role": "assistant", "content": str(reply)}) messages.append({"role": "tool", "content": str(result)}) return "stopped: step limit"This reuses TOOLS and fake_model from above.
Predict the loop
Section titled “Predict the loop”In the lesson, run() is a hand-written agent loop over a scripted model: the model asks for the get_weather tool, the loop runs it and appends the result, and the model answers from that result.
What does this print?
print(run("What is the weather in Amsterdam?"))It is 14°C, rain there.
Output verified in CI from site/examples/building-agents/agent-loop/loop.py.
Trace two rounds: first the model asks for a tool, then it sees the tool result and answers.
A real model API returns the same two cases in a different format. The
reply has a stop reason field that says whether the model finished or
wants a tool, and each tool result names the id of the request it
answers, so the model can match them up when it asked for more than one
tool at once [1]. With a stateless model API,
the usual kind, every round sends the whole message list and the tool
definitions again, because the model doesn’t keep state between calls
[2] [3]. The
answer key in fake_model
plays the role of the stop reason, and translating that field is most of
the work of swapping in a real model.
Notice max_steps. Without it, a model that keeps asking for tools runs
forever and spends your money. A real loop has a budget, and when the
budget runs out it returns a string that names the limit
(stopped: step limit) rather than an empty answer. The lesson
Stopping the loop on purpose replaces the bare string with a dict that
has a stop reason field.
The loop also keeps going when a tool fails. A tool name that isn’t in
TOOLS, or a function that raises, becomes a tool result that starts with
error: and goes back to the model like any other result. The model can
then retry with better arguments or tell the user what went wrong. A tool
error never escapes the loop, so the run always ends with something to show.
The same path handles a reply that names a tool but has no args. The
loop calls the function with no arguments. Because get_weather has a
required parameter, Python reports the missing argument, and that report
goes back to the model as a tool error it can repair. An exception with
no message is named by its type. The model then reads error: ValueError
rather than a bare error:. The loop stores every tool result as text.
Text is what the model reads.
Predict the error path
Section titled “Predict the error path”In the lesson, run() is a hand-written agent loop over a scripted model. get_weather raises LookupError for a city it has no data for, run() turns a raising tool into a tool result that starts with error:, and fake_model answers differently when the tool result starts with error:.
What does this print?
print(run("What is the weather in Oslo?"))I could not check. The tool said: error: no data for Oslo
Output verified in CI from site/examples/building-agents/agent-loop/tool_error.py.
get_weather has no entry for Oslo. Follow the exception into the loop, then read the tool branch of fake_model.
A vendor SDK (software development kit) can run this loop for you. The Claude API SDKs call it the tool runner. You hand it your functions and it returns the final answer. In Python a decorator builds each tool definition from the function’s arguments and docstring, and the runner runs the request and result rounds [4] [5]. That saves the code above and hides the same decisions: how many steps it takes, what it does on a tool error, when it stops, and what it puts in the message list. Having written the loop once, you know which of those to check in the SDK’s version.
Order one iteration
Section titled “Order one iteration”Put the steps of one iteration of the agent loop in order.
- Send the message list to the model
- Check whether the reply is a final answer; if so, return it
- Look up the requested tool by name
- Run the tool function with the model's arguments
- Append the tool call and the tool result to the messages
What does the loop need to know before it can decide whether to run a tool at all?
Which jobs are the loop's?
Section titled “Which jobs are the loop's?”The lesson's hand-written agent loop sends messages to a model, runs the tools the model asks for and appends the results, until the model answers.
Which two of these does the loop do?
Which of these can a model do by writing text, and which need code that runs?
The tool raises
Section titled “The tool raises”In the lesson's hand-written agent loop, get_weather raises LookupError for a city it has no data for.
The model asks for the weather in a city that get_weather has no data
for, and the function raises. What does the loop do?
What can the model do with an exception it never sees?
Who does it?
Section titled “Who does it?”The lesson's agent loop is code that calls a model, runs tools and checks a budget. The model reads the messages and writes a reply.
Match each job to the part that does it.
Is it text the model writes, or code that runs in your loop?
Exercise
Copy agent.py from the repository. Add a second tool, get_time(city),
returning a fixed string per city. Extend fake_model so a question
containing “time” asks for it. Run the loop on “What is the time in
Lisbon?” and check the answer.
A good result: the new tool has a description written for the caller, the loop itself is unchanged, and the question returns the fixed string for Lisbon in one tool round.
Stretch: Replace fake_model with a call to a real model API that supports tool use, keeping run unchanged. The only new work is translating the API's tool-call format into the shape the loop already understands.
Recap
- A tool is a function plus a description written for the model [6].
- The loop is: ask the model, run the tool it asks for, append the result, repeat until it answers or the budget runs out.
- The model never runs anything; your loop does. That is where safety controls go.
You can now
- Defines a tool with a schema the model uses correctly
- Implements the loop with error handling and a stop condition
References
Section titled “References”- Anthropic. Handle tool calls. Claude Platform documentation. Reference.
Claude docs handle-tool-calls - Anthropic. Building with the Claude API. Claude Academy. Course.
Academy building-with-the-claude-api - Anthropic. Messages. Claude Platform documentation. Reference.
Claude docs messages - Anthropic. Claude Platform 101. Claude Academy. Course.
Academy claude-platform-101 - Anthropic. Tool runner (SDK). Claude Platform documentation. Reference.
Claude docs tool-runner - Addy Osmani, Ivar Soares Urdalen, Leo Simons. Building your first agent: the loop from scratch, then with an SDK. Agent Engineer Course. Course.
AEC-13