Agents, Tools, and Context
Understand the three core components of every Claude interaction: model calls, tool calls, and context. Learn to analyze API requests and responses, interpret tool usage, and manage conversation history to build reliable AI agent loops. This lesson helps you develop a clear mental model essential for architecting Claude-powered systems.
In the last lesson, we reasoned about why code-level checks around tool calls provide a reliable enforcement boundary and when a task requires an agent. Now we’ll make that concrete: we’ll inspect what the API sends and receives, label the key parts, and build the vocabulary you’ll use for the rest of the course. By the end of this lesson, you’ll be able to:
Build a mental model for the three moving parts in every Claude-powered system.
Review annotated examples of a request, a tool-use response, and a tool result.
Read a full conversation transcript labeled from the first message to the final reply.
Complete a short exercise to test whether the labels have landed.
The three things in every Claude interaction
Every interaction with Claude, no matter how complex, is built from three concepts:
Model call: One round trip to the API. We send a request, and Claude returns a response. An agent loop is just many of these in sequence.
Tool call: A request Claude makes in its response, asking us to run a function and report back. Claude does not run the function. We do.
Context: The
messagesarray we send on every model call. It is the only memory Claude has. Whatever is not in that array does not exist as far as Claude is concerned.
These three concepts appear often on the exam. Many questions about agent loops, tool responses, or context degradation come back to one of these concepts.
Anatomy of a request
Here’s a minimal request using the Anthropic Python SDK. We’ll use a customer support scenario throughout this lesson so that each piece of the protocol has a concrete purpose.
import anthropicclient = anthropic.Anthropic()response = client.messages.create(model="claude-opus-4-8",max_tokens=1024,system="You are a customer support agent. Process refunds within policy only.",tools=[{"name": "process_refund","description": "Process a refund for a customer order.","input_schema": {"type": "object","properties": {"order_id": {"type": "string"},"amount": {"type": "number"},"reason": {"type": "string"}},"required": ["order_id", "amount", "reason"]}}],messages=[{"role": "user", "content": "I need a refund for order #12345. It arrived damaged."}])
Let’s walk through each part:
Line 1: We import the official Anthropic Python SDK. This is the only dependency needed to make API calls.
Line 3: We create a client instance. By default, it reads the
ANTHROPIC_API_KEYenvironment variable for authentication. We never hardcode the key.Line 5: We open the
messages.create()call, which sends a request to the API and returns a response object.Line 6: We specify which Claude model to use. The model is set per request, not on the client, so different calls in the same agent loop can target different models if needed.
Line 7: We set a hard cap on how many tokens Claude can generate in this response. This is a budget control, not a quality setting. The response stops at this limit even if Claude is mid-sentence.
Line 8: We provide the system prompt. Claude reads this on every turn of the conversation. It shapes behavior for the whole session, which is why business rules should live here and not in a user message.
Lines 9–22: We define the
toolslist, the functions Claude is allowed to request. Each tool has aname, adescriptionthat Claude reads to decide when to use it, and aninput_schemathat defines the required arguments as a JSON Schema.Lines 13–21: The
input_schematells Claude which arguments to populate, what types they must be, and which are required. Claude uses this schema to construct theinputobject it sends back in atool_useblock.Lines 24–26: We provide the
messagesarray, the conversation history. Right now, it holds one user message. After every round trip, we append to this array and send the full history back. This array is the entire state of the conversation.
When Claude responds directly
If Claude has enough information to reply without calling a tool, stop_reason is "end_turn", and the response looks like this:
{"id": "msg_01ABC123","type": "message","role": "assistant","content": [{"type": "text","text": "I can see order #12345 in the system. Could you confirm the email on the account before I process this?"}],"model": "claude-opus-4-8","stop_reason": "end_turn","usage": {"input_tokens": 89, "output_tokens": 31}}
Here’s what each field tells us:
Line 2:
"id"is a unique identifier for this response. Use it when logging or correlating requests to responses.Line 3:
"type"is the top-level type of the response object. It is always"message"for text and tool interactions.Line 4:
"role": "assistant"confirms this is Claude’s output. When we append this response to the messages array to build context, we use this role value.Lines 5–10:
"content"is an array of content blocks. Here, it holds a singletextblock. The same array can hold multiple blocks of different types, which we’ll see in the next section.Line 7:
"type": "text"inside the content block identifies this as a plain text response. The other content block type we care about is"tool_use", which signals a tool call request.Line 12:
"stop_reason": "end_turn"is the most important field for loop control. It means Claude finished its response and is waiting for input. There is no tool to run. The agent loop should surface this response to the user or advance to the next step.Line 13:
"usage"provides the token counts for this round trip. Tracking these across turns tells us how fast the context window is filling up.
When Claude wants to use a tool
Now, the same scenario with more context provided. Claude decides it has enough information to act. The response shape changes:
{"id": "msg_01DEF456","type": "message","role": "assistant","content": [{"type": "text","text": "I'll process that refund for order #12345 right away."},{"type": "tool_use","id": "toolu_01XYZ789","name": "process_refund","input": {"order_id": "12345","amount": 49.99,"reason": "Item arrived damaged"}}],"model": "claude-opus-4-8","stop_reason": "tool_use","usage": {"input_tokens": 97, "output_tokens": 52}}
Let’s go through the new and changed fields:
Lines 5–20: The
contentarray now holds two blocks: atextblock and atool_useblock. Claude narrated its intention and made the tool call in the same turn. Both blocks must be preserved when we add this response to the messages array.Line 11:
"type": "tool_use"identifies this content block as a tool call request. This is Claude’s way of saying “run this function for me and send me the result.”Line 12:
"id": "toolu_01XYZ789"is the unique identifier for this specific tool call. This is separate from the messageidon line 2. We must echo this exact value back astool_use_idwhen we send the result. Without it, Claude cannot match the result to its request.Line 13:
"name": "process_refund"is the tool Claude wants to run. This must exactly match a tool name in thetoolslist we sent in the request.Lines 14–18:
"input"contains the arguments Claude chose for this call, populated according to theinput_schemawe defined. These are the values our code will use when running the actual function.Line 22:
"stop_reason": "tool_use"signals that Claude is pausing and waiting for a tool result. The agent loop must not surface this to the user. Instead, it runs the requested tool and sends the result back.
Closing the loop: The tool result
We run the function in our code and get a result. Now we build the next API call by appending two new messages to the array and sending everything back.
messages = [{"role": "user","content": "I need a refund for order #12345. It arrived damaged."},{"role": "assistant","content": response.content},{"role": "user","content": [{"type": "tool_result","tool_use_id": "toolu_01XYZ789","content": "Refund of $49.99 processed. Confirmation number: REF-7890."}]}]final_response = client.messages.create(model="claude-opus-4-8",max_tokens=1024,system="You are a customer support agent. Process refunds within policy only.",tools=[...],messages=messages)
Here’s what each new part does:
Lines 1–20: We construct the full messages array to pass to the next API call. The API is stateless; it has no memory between calls, so we send the complete history every time.
Lines 6–9: We append Claudes previous response as an
assistantturn usingresponse.content, which gives us the full content list, including both the text block and thetool_useblock. Claude needs to see its own tool call request in context to understand what the result refers to. Stripping thetool_useblock would break the conversation.Lines 10–19: We add the tool result as a
user-role message. This surprises people at first. Therolefield describes who produced the content. Our application produced this result, so it belongs underuser. Theassistantrole is reserved exclusively for Claudes own responses.Line 14:
"type": "tool_result"identifies this content block as a function output. Claude looks for this type to know the requested function has been executed.Line 15:
"tool_use_id": "toolu_01XYZ789"links this result to the specific tool call that requested it. This must match theidfield from thetool_useblock exactly.Line 16:
"content"holds the output our function returned. This can be a plain string or a list of content blocks. Claude reads this to determine its next action.Lines 22–28: We make the second API call, passing the complete updated messages array. This call will produce Turn 4, Claudes final response to the user.
The complete annotated transcript
Here is the full four-turn conversation after both API calls complete. Read it top to bottom as if you are the agent loop watching the context grow:
messages = [# Turn 1 — User opens the conversation# This is the only input to API call 1.{"role": "user","content": "I need a refund for order #12345. It arrived damaged."},# Turn 2 — Claude requests a tool call (API call 1 response)# stop_reason was "tool_use". We append the full content list unchanged.{"role": "assistant","content": [{"type": "text", "text": "I'll process that refund for order #12345 right away."},{"type": "tool_use", "id": "toolu_01XYZ789", "name": "process_refund","input": {"order_id": "12345", "amount": 49.99, "reason": "Item arrived damaged"}}]},# Turn 3 — We return the tool result# Turns 1, 2, and 3 together are the input to API call 2.{"role": "user","content": [{"type": "tool_result", "tool_use_id": "toolu_01XYZ789","content": "Refund of $49.99 processed. Confirmation number: REF-7890."}]},# Turn 4 — Claude delivers the final response (API call 2 response)# stop_reason is "end_turn". The loop surfaces this to the user.{"role": "assistant","content": [{"type": "text","text": "Done! Your refund of $49.99 has been processed. Confirmation: REF-7890."}]}]
Lines 3–8: Turn 1 is the users opening message. This is the only input to the first API call.
Lines 10–19: Turn 2 is Claudes response to the first API call. The
stop_reasonwas"tool_use", so the loop does not surface this to the user. Instead, it appends the full content list (both the text block and thetool_useblock) unchanged and moves to the next step.Lines 21–28: Turn 3 is the tool result we send back. Turns 1, 2, and 3 together make up the
messagesarray for the second API call. This is the context Claude sees when generating Turn 4.Lines 30–38: Turn 4 is Claudes final response. The
stop_reasonis"end_turn", so the loop surfaces the text to the user and stops iterating.
Complete code
We bring together the request anatomy, both response shapes, and the tool result handshake from this lesson into one runnable file. The script makes two API calls: the first returns a tool_use stop reason with both a text block and a tool_use block in the content array, and the second returns end_turn after the tool result is delivered. The annotated transcript at the end labels each turn exactly as the lesson diagrams show.
Running the script produces output like this:
=== API call 1: user asks for a refund ===stop_reason : tool_usecontent[0] : text — I'll process that refund for order #12345 right away.content[1] : tool_use — process_refund({"order_id": "12345", "amount": 49.99, "reason": "Item arrived damaged"})tool_use_id : toolu_01AbCdEfGhIjKlMnOpQ...Tool result : Refund of $49.99 processed. Confirmation number: REF-5840.=== API call 2: send tool result, receive final reply ===stop_reason : end_turncontent[0] : text — Your refund of $49.99 for order #12345 has been processed. Confirmation number: REF-5840.=== Annotated 4-turn transcript ===Turn 1 [user] : I need a refund for order #12345. It arrived damaged.Turn 2 [assistant] : text — I'll process the refund for order #12345 right away.Turn 2 [assistant] : tool_use — process_refund({"order_id": "12345", "amount": 49.99, "reason": "Item arrived damaged"})Turn 3 [user] : tool_result(toolu_01AbCdEfGhIjKlMnOpQ...) = Refund of $49.99 processed. Confirmation number: REF-5840.Turn 4 [assistant] : text — Your refund of $49.99 for order #12345 has been processed. Confirmation number: REF-5840.
The stop_reason on API call 1 is tool_use, which signals that the loop must not surface anything to the user yet. The tool_use_id on Turn 2 exactly matches the tool_use_id on Turn 3; that match is what allows Claude to associate the result with its request. API call 2 returns end_turn, so the loop surfaces Turn 4 to the user and stops.
Exercise: Label the transcript
Read the transcript below and answer the four questions that follow. The code runs correctly; your job is to read it, not fix it.
messages = [{"role": "user", "content": "What's the status of my order #99001?"},{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_ABCdef", "name": "get_order_status","input": {"order_id": "99001"}}]},{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_ABCdef","content": "Order #99001 is in transit. Expected delivery: June 12."}]},{"role": "assistant", "content": [{"type": "text","text": "Your order #99001 is on its way and should arrive by June 12."}]}]
Try each question before checking the answers.
How many model calls does this transcript represent?
One. The whole conversation is a single session handled in one call.
Two. The assistant appears twice, once per API call.
Three. One for the user message, one for the tool call, and one for the final reply.
Four. One for each turn in the messages array.
What’s next?
We have seen what stop_reason tells us: "tool_use" means keep going, and "end_turn" means we are done. In the next lesson, we will examine every value stop_reason can take, what each one means for loop control, and what the rest of the response envelope exposes before we write a single line of agent logic.