Before you start
This lesson uses everything so far: the Messages call from lesson 1, the tool round trip from lesson 2 and the cache from lesson 3. If the word agent is new, what is an AI agent comes first.
The Agent SDK overview defines an agent as an application that completes a task by planning its own steps and calling tools. On the Claude API that is a loop around one call: Claude picks the next step, your code runs it and the result goes back. The sample answers questions about a folder of Markdown files with two tools, list_files and read_file.
Write the agent loop
The loop follows the canonical shape on the how tool use works page: keep calling while stop_reason is tool_use. It adds three things the basic loop in the docs' tool-using agent tutorial leaves to you: a turn limit, a path guard and is_error results.
The code is a sample written for this lesson. Put a few Markdown files in a docs folder, export your key and run it.
from pathlib import Path
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-opus-5-5"
MAX_TURNS = 12
ROOT = Path("docs").resolve()
SYSTEM = "Answer only from the files in the docs folder. If the answer is not there, say so."
TOOLS = [
{
"name": "list_files",
"description": (
"List the Markdown files in the docs folder. Use it first to see what "
"exists. Pass a glob such as 'guides/*.md' to narrow the list. It returns "
"paths relative to the docs folder, one per line."
),
"input_schema": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Optional glob, default **/*.md"},
},
},
},
{
"name": "read_file",
"description": (
"Read one Markdown file from the docs folder and return its text. Use a "
"path from list_files. It fails for any path outside the folder."
),
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path relative to the docs folder"},
},
"required": ["path"],
},
},
]
def safe_path(relative: str) -> Path:
path = (ROOT / relative).resolve()
if not path.is_relative_to(ROOT):
raise ValueError("That path is outside the docs folder.")
return path
def run_tool(name: str, args: dict) -> str:
if name == "list_files":
pattern = args.get("pattern", "**/*.md")
files = sorted(p.relative_to(ROOT).as_posix() for p in ROOT.glob(pattern) if p.is_file())
return "\n".join(files) or "No files match."
if name == "read_file":
return safe_path(args["path"]).read_text(encoding="utf-8")[:20000]
raise ValueError(f"Unknown tool: {name}")
def run_agent(task: str) -> str:
messages = [{"role": "user", "content": task}]
for turn in range(1, MAX_TURNS + 1):
response = client.messages.create(
model=MODEL,
max_tokens=2048,
system=SYSTEM,
tools=TOOLS,
cache_control={"type": "ephemeral"},
messages=messages,
)
print(f"turn {turn}: {response.stop_reason}, cache read {response.usage.cache_read_input_tokens}")
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break
results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
content, is_error = run_tool(block.name, block.input), False
except Exception as error:
content, is_error = str(error), True
results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": content, "is_error": is_error}
)
messages.append({"role": "user", "content": results})
else:
return f"Stopped after {MAX_TURNS} turns."
if response.stop_reason != "end_turn":
return f"Stopped early: {response.stop_reason}"
return "".join(block.text for block in response.content if block.type == "text")
print(run_agent("Which file explains how to reset a password, and what is its first heading?"))- Define the two tools with long descriptions, as lesson 2 explains.
- Guard every path.
safe_pathresolves the path and refuses anything outsidedocs, because the model chooses the argument and your code is the only check. - Keep one
messageslist and append to it each turn: the assistant'sresponse.contentwhole, then one user message that holds everytool_result. - Leave the loop on any stop reason except
tool_use, and return the text only forend_turn. - Count turns. If the
forloop runs out, theelsebranch reports the limit instead of looping on. - Run it and watch the cache-read number. It starts at 0 and climbs once the history passes the model minimum, 512 tokens on Opus 5.5.
What happens on each turn
The walk-through sketches the message flow for the sample's question. It illustrates the protocol and is not a recording of a real run.
Claude asks for the file list
The first response ends with stop_reason tool_use and a list_files call. Your code runs it and the output goes back as a tool_result in a user message.
Claude opens one file
The second response calls read_file with a path from that list. After its result goes back, the history is five messages: the task, two assistant turns and two result turns.
Claude answers
The third response has stop_reason end_turn and a text block. The loop leaves and returns the text.
Every call resends the history
The third request is larger than the first. With the top-level cache_control, the part already seen is read from the cache once the prefix reaches the model minimum.
Stop conditions
The API decides when Claude is done. You decide when to give up. The docs' basic loop is a while on stop_reason, and the tool runner's max_iterations is optional, so a loop with no counter of its own can run until the context fills.
Count turns and cap them. The stop reasons page lists no signal for a turn count or a budget, so both limits belong in your code. If you carry a budget, add up the usage of every response as well.
| Stop signal | Where it comes from | What the loop does |
|---|---|---|
end_turn | Claude finished | Return the text |
tool_use | Claude wants a tool | Run it, append the results, call again |
max_tokens | The reply hit your limit | Stop and report, or raise the limit |
refusal | Claude declined | Read stop_details, then retry on a fallback model |
model_context_window_exceeded | The context window filled | Treat the reply as truncated, compact earlier |
pause_turn | A server-tool loop reached its limit | Send the response back as it came, then continue |
| Turn count, token budget, clock | Your own code | Stop and say why |
Context growth
Every turn resends the whole history, so each call is larger than the one before. Tool results do most of the growing: a file that read_file returns stays in every later request. The sample cuts a file at 20,000 characters for that reason. Prompt caching makes the repeated part cheaper but does not make it smaller.
Size matters beyond the window too. The compaction overview says response quality degrades as a conversation grows, which is why it offers to replace older turns with a summary Claude writes on the server. Both kinds are in beta.
Threshold compaction is the smaller change to a loop like this one: one parameter on ordinary requests, and the response with its compaction block goes into the history whole. On-demand compaction suits an application that must pick the moment, and the docs say to use it wherever it is available.
1M
tokens of context on each current model
Source: Anthropic Docs: Models overview (read November 5, 2026)
150,000
input tokens before threshold compaction starts, by default
Source: Anthropic Docs: Compaction at a token threshold (read November 4, 2026)
50,000
the lowest trigger value you can set
Source: Anthropic Docs: Compaction at a token threshold (read November 4, 2026)
response = client.beta.messages.create(
betas=["compact-2026-01-12"],
model="claude-opus-5-5",
max_tokens=4096,
messages=messages,
context_management={"edits": [{"type": "compact_20260112"}]},
)
messages.append({"role": "assistant", "content": response.content}) # the compaction block stays in| Option | Who decides when | What happens |
|---|---|---|
| Compaction at a token threshold | The API, at your trigger | It summarizes inside an ordinary request. Beta header compact-2026-01-12, edit type compact_20260112 |
| Compaction on demand | Your application | A separate request with compaction set to summarize returns one summary block to put first in messages. Beta header compact-2026-09-04 |
| Context editing | Rules you set | It clears old tool results or old thinking blocks instead of summarizing them |
When to stop hand-writing the loop
Write the loop by hand once, as above, so you know what a library does for you. The tool runner, in beta in the SDKs, runs the same loop from client.beta.messages.tool_runner(...). It turns a tool exception into an is_error result and stops at max_iterations when you set it. The docs still send you back to the manual loop for human approval, custom logging or conditional execution (tool runner page).
The Agent SDK is a different kind of tool. It is Claude Code as a library, with its built-in file, shell and web tools, hooks, subagents, MCP, permissions and sessions, in Python and TypeScript (overview). Use it when you want all of that built in. The Claude Agent SDK lesson shows how.
| Manual loop | Tool runner Our pick | Agent SDK | |
|---|---|---|---|
| Runs the loop for you | No | Yes | Yes |
| Approval before a tool runs | Yes | No | Yes |
| Built-in file, shell and web tools | No | No | Yes |
| Hooks, subagents and sessions | No | No | Yes |
| Works over plain HTTP | Yes | No | No |
Our pick: Tool runner. For your own tools with no approval step, it removes the loop code and keeps your requests. Take the manual loop when a person approves each call, and the Agent SDK when you want Claude Code's own tools.
Sources: Anthropic Docs: Tool runner (read November 4, 2026), Claude Code Docs: Agent SDK overview (read November 4, 2026)
Simon Willison and Anthropic on what an agent loop is
Simon Willison, creator of Datasette and co-creator of Django, posted on X on 18 September 2025 that he was ready to accept one definition of an agent. He had dismissed the word as hopelessly ambiguous for years. The definition is the line quoted below, and the loop in this lesson fits it: the tools are list_files and read_file, the goal is the user's question.
Anthropic posted on 30 September 2025 to share an engineering blog piece on context engineering. It names the problem behind context growth: as the window fills, the model recalls less of what is in it, which the authors call context rot. Among the safest and lightest kinds of compaction it lists clearing old tool results, the job of the context editing row in the compaction table. Try that before you summarize.
An LLM agent runs tools in a loop to achieve a goal

Screenshots of X's own embeds of the posts, taken on 5 November 2026, with the time in UTC. Select one to open the post on X. The Anthropic embed leaves out the link that ends the post.
Common mistakes
- Running the loop with no turn cap, and finding out about it on the invoice.
- Rebuilding the assistant message from text blocks and dropping the thinking blocks the API wants back.
- Sending each tool_result in its own user message instead of one message per turn.
- Trusting a path, a command or a URL the model chose without checking it in code.
- Returning a whole file or page as one result and letting it ride along in every later request.
- Treating text inside a tool result as instructions. It is untrusted data.
- Reading stop_reason max_tokens as a finished answer.
Try it yourself
- Set
MAX_TURNSto 2 and ask a question that needs three tool calls. What does the function return? - Ask the agent to read
../secrets.txtand watch the error result. Does Claude recover or explain? - Run the same question twice in a row and compare the cache-read numbers per turn.
- Rewrite
run_agentwith the tool runner and compare how much code is left.
Next
This agent finds answers by listing and opening files, which stops working once the folder holds thousands of pages. The next course builds agents on OpenAI's API, starting with the OpenAI API quickstart. The RAG tutorial after it covers retrieval over your own documents. If you want the tools reachable from other clients too, build an MCP server around them.
Check yourself
What keeps an agent loop from running forever when Claude keeps calling tools?
Show answer
B. A turn limit in your own code The client loop exits only on a stop reason other than tool_use, so you count the turns yourself.
Where do all the tool_result blocks for one assistant turn go?
Show answer
C. In one user message, each with its tool_use id One user message per turn keeps the ids matched and keeps Claude making parallel calls.
Why append response.content whole instead of only the text blocks?
Show answer
A. The tool_use and thinking blocks have to go back unchanged The API needs the tool_use blocks to match the results and requires thinking blocks back unchanged.
You need a person to approve each tool call in an agent on the Client SDK. What do the docs recommend?
Show answer
D. A manual loop The tool runner page sends you to the manual loop for human approval, custom logging or conditional execution.
Frequently asked questions
How many turns should the loop allow?
The docs give no number. Their tool runner examples use 10 for max_iterations, and this lesson's sample uses 12. Set yours a little above the longest legitimate run in your logs.
Where do I add human approval?
In a manual loop, between reading the tool_use blocks and running them. If the person says no, send a tool_result with is_error true and the reason, because every tool_use id still needs an answer.
Can the same loop use Anthropic's server tools such as web search?
Yes, but handle pause_turn. A server-tool loop stops after its iteration limit, which the docs put at 10 by default, and you continue by sending the response back as it came.
Can I sign users in with their claude.ai account?
No. Unless Anthropic has approved it beforehand, third-party products, agents built on the Agent SDK included, cannot offer claude.ai login or its rate limits. Use API key authentication.
Sources
- Anthropic Docs: How tool use works https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works
- Anthropic Docs: Tutorial, build a tool-using agent https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent
- Anthropic Docs: Tool runner https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner
- Anthropic Docs: Stop reasons and fallback https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons
- Anthropic Docs: Prompt caching https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- Anthropic Docs: Compaction overview https://platform.claude.com/docs/en/build-with-claude/compaction
- Anthropic Docs: Compaction at a token threshold https://platform.claude.com/docs/en/build-with-claude/compaction-threshold
- Anthropic Docs: Compaction on demand https://platform.claude.com/docs/en/build-with-claude/compaction-on-demand
- Anthropic Engineering: Effective context engineering for AI agents https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
- X: Simon Willison on the definition of an LLM agent https://x.com/simonw/status/1968755421201711588
- X: Anthropic shares its context engineering article https://x.com/AnthropicAI/status/1973098580060631341
- YouTube (AI Engineer): How We Build Effective Agents, Barry Zhang, Anthropic https://www.youtube.com/watch?v=D7_ipDqhtwk
- Anthropic Docs: Models overview https://platform.claude.com/docs/en/models/overview
- Claude Code Docs: Agent SDK overview https://code.claude.com/docs/en/agent-sdk/overview


