Build functional AI agents with Claude by wiring the Messages API into an agentic loop, defining tools, and composing multi-step workflows that plan…
Set up the Anthropic Python SDK, authenticate, and make your first `messages.create` call with the right model, token budget, and system prompt. You'll use `claude-opus-4` as the running model throughout this lesson.
Set up the Anthropic Python SDK, authenticate, and make your first Messages API call with the right model, token budget, and system prompt.
Why this matters: Every agent you build — including your SEO/GEO page agent — runs on this single API call pattern; getting it right here prevents silent failures in every later module.
Decision this forces: Which Claude model to use for your agent (claude-opus-4 for complex reasoning vs claude-sonnet-4 for speed/cost)?
You want Claude to power an agent. Before any tool loop or multi-step reasoning, you need one thing: a working call that returns a real response. That single call is the atom every agent is built from.
The wraps the REST API so you authenticate once. Pass a model ID, a , and a list of messages. You get a structured response back. Three parameters decide almost everything: model, , and system.
For an , the system prompt defines the agent's role, constraints, and output rules. It persists across every turn. caps response length. Set it too low and reasoning gets cut mid-thought.
import anthropic client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env response = client.messages.create( model="claude-opus-4", messages=[{"role": "user", "content": "Draft an SEO title."}] ) print(response.content)
This looks right, but it's missing one required parameter — predict what happens before you reveal.
anthropic.BadRequestError: max_tokens is required. The Messages API will not infer a default; you must pass max_tokens explicitly on every call. Without it the SDK raises a 400 Bad Request before the model ever runs.
import anthropic client = anthropic.Anthropic() # ANTHROPIC_API_KEY must be set in env response = client.messages.create( model="claude-opus-4", max_tokens=1024, system="You are an SEO specialist. Return only the requested element.", messages=[{"role": "user", "content": "Draft an SEO title for 'claude ai agents'."}] ) print(response.content[0].text)
Adding and a fixes the 400 error and scopes the agent's behavior. response.content[0].text extracts the plain string from the structured response object.
It prints a plain string like "Claude AI Agents: Build Autonomous Workflows with the Anthropic API". response.content[0] is a TextBlock object — the SDK wraps the text in a typed block, so you index into .content and then read .text.
import anthropic client = anthropic.Anthropic() def call_agent(user_query: str) -> str: response = client.messages.create( model="claude-opus-4", max_tokens=2048, system=___, # TODO: write the system prompt messages=[{"role": "user", "content": user_query}] ) return response.content[0].text print(call_agent("List three GEO signals for 'claude ai agents'."))
Stop — attempt the TODO before revealing. The missing piece is the string: it must define the agent's role and output format for an SEO/GEO task. Hints: name the agent's specialty; tell it to be concise and structured.
system="You are an SEO/GEO content strategist. Return structured, concise answers focused on entity clarity and citation-worthiness."
Changed line: the system= argument — this is the crux of the exercise.
If you pass system="", the model has no role or constraints, so answers drift toward generic prose and ignore the output format. No error is raised — the failure is silent and behavioral, exactly the third failure mode above.
| Option | Reasoning depth | Latency per call | Cost per million tokens | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| claude-opus-4 | Strongest — handles ambiguous, multi-hop tasks | Slower — noticeable on tight loops | Most expensive in the Claude 4 family | Multi-step agent tasks requiring complex reasoning, tool chaining, or nuanced judgment — the model used throughout this lesson. | Higher — use when quality of reasoning is the bottleneck | Higher — slower per call, best paired with caching for repeated system prompts |
| claude-sonnet-4 | Strong — handles most agent tasks well | Faster — better for tight agentic loops | Significantly cheaper than Opus | High-volume or latency-sensitive subtasks — e.g. a worker agent that classifies or summarizes inside a supervisor-worker pattern. | Lower — good default for throughput-heavy pipelines | Lower — faster iteration, easier to run in parallel |
Three failure modes hit almost every first integration:
anthropic.AuthenticationError: 401 Unauthorized. The SDK reads ANTHROPIC_API_KEY from the environment; a typo in the var name gives the same error as no key at all.stop_reason: "max_tokens" instead of "end_turn". For agent reasoning steps, 1024 is a safe floor; tool-heavy turns may need 4096.system field is not stored server-side between calls. If you forget it on turn 2, the agent loses its role and constraints silently — no error, just drifting behavior.When an AI tool generates your SDK setup, check these four things before running it in an agent loop:
claude-opus-4 not claude-opus or a hallucinated variant. Wrong IDs return a 404 with no helpful hint."max_tokens"; a truncated response fed back into the loop corrupts state.A single messages.create call is stateless. It knows nothing about the previous turn unless you pass history back in. An is what happens when you wrap that call in a loop. Feed observations back as messages. Let the model decide the next action.
But not all agents are the same loop. A single agent handles one thread of work. A pattern delegates subtasks to cheaper models. A passes output from one stage to the next. The right topology depends on task complexity and cost.
The next module maps all three topologies — single-agent, supervisor-worker, and pipeline — against real task complexity. You'll pick the right pattern for a web-research-and-summarize agent.
Map the three core topologies — single-agent, supervisor-worker, and pipeline — against task complexity, then pick the pattern for a web-research-and-summarize agent you'll build in Module 5.
Maps the three agent topologies — single-agent, supervisor-worker, and pipeline — and shows how to choose between them for a real research task.
Why this matters: Picking the wrong topology is the most expensive architectural mistake in an agent build; this module gives you the decision framework before you write a line of production code.
Decision this forces: Single agent with many tools vs. multi-agent supervisor-worker split — which fits your task's complexity?
messages.create call. What does Claude return when it decides it's finished — and what field tells you that?Answer: the response carries a of , signalling the model has nothing more to do.
That single call is the atom of every agent. Now the question is: how do you wire multiple atoms together when one agent isn't enough?
Every agent system fits one of three shapes. The right shape depends on task complexity, tool count, and context window size.
Which topology fits a web-research-and-summarize agent?
You're building an agent that takes a topic, searches the web for recent sources, and returns a structured summary. Here's how each topology would play out.
For Module 5's agent — one topic, one search tool, one summary — the single-agent topology wins on simplicity and cost. You'd reach for supervisor-worker only if the task grew to multi-topic research with parallel workers.
import anthropic client = anthropic.Anthropic() def run_agent(topic: str) -> str: messages = [{"role": "user", "content": f"Research: {topic}"}] response = client.messages.create( model="claude-opus-4", max_tokens=1024, messages=messages, ) return response.content[0].text
This is the obvious first attempt: one call, no tools, no loop. It asks Claude to research a topic but gives it no way to actually search the web.
Claude returns a plausible-sounding but potentially stale summary drawn entirely from its training data — no live web search occurs. There's no tool call, no loop, and no stop_reason of 'tool_use'. The agent can't verify recency, so it may confidently cite outdated information. This is the core failure: the skeleton looks like an agent but behaves like a plain completion.
def run_agent(topic: str, tools: list) -> str: messages = [{"role": "user", "content": f"Research: {topic}"}] while True: response = client.messages.create( model="claude-opus-4", max_tokens=1024, tools=tools, messages=messages, ) if response.stop_reason == "end_turn": return response.content[0].text # handle tool_use blocks → append tool_result, loop again
The delta from Stage 1: a while True loop that keeps running until stop_reason == "end_turn". Now Claude can call tools and read results before it commits to an answer.
The comment on line 10 is the crux — you'll fill that in next.
Changed lines vs Stage 1: (1) Extract each content block where block.type == 'tool_use', call the real tool function with block.input, and collect the result. (2) Append TWO new messages to the messages list: first the assistant's response (role='assistant', content=response.content), then a user message with role='user' and content=[{type:'tool_result', tool_use_id: block.id, content: <result>}]. Without both appends the API raises a validation error because the conversation turn is incomplete.
| Option | Context window pressure | Subtask independence | When to choose | Cost | Complexity |
|---|---|---|---|---|---|
| Single-Agent | Low — all state lives in one window | N/A — no subtasks to split | Task fits in one context window; tools are few (≤5) and non-conflicting; low coordination overhead needed. | Lowest — one model call per turn | Low — one loop, one prompt |
| Supervisor-Worker | Distributed — each worker holds only its slice | High — workers don't depend on each other | Task decomposes into independent subtasks; workers can run in parallel; supervisor needs to merge results. | Higher — multiple model calls; parallelism can offset latency | Medium — supervisor prompt + worker prompts + state passing |
| Pipeline | Moderate — each stage passes a summary forward | Low — stages are tightly coupled by order | Every stage is mandatory and must run in strict order; output of stage N is always the input to stage N+1. | Medium — sequential calls, predictable | Low-Medium — fixed graph, no dynamic routing |
Generated code checks response.stop_reason == "tool_use" but forgets to handle "end_turn". Verify both branches exist.With the topology locked in and the loop skeleton ready, Module 3 covers and function calling. It shows how to define the web_search schema, parse tool_use blocks, and send back a .
Define a tool schema in the `tools` array, handle a `tool_use` content block in the response, and return a `tool_result` — using a `web_search` tool for the running example so Claude can fetch live data.
How to define tool schemas, parse tool_use response blocks, and return tool_result messages so Claude can call external functions.
Why this matters: Tool use is the mechanism that lets your agent fetch live data and take actions — without it, Claude can only reason over what you pass in the prompt.
Decision this forces: How many tools to expose per agent call — and how specific to make each tool's description to avoid mis-routing?
The three topologies are , , and . Single-agent handles one-step loops. Supervisor-worker runs subtasks in parallel. Pipeline feeds each stage to the next in fixed order.
This module adds the mechanism that makes topologies actually do something: . Without it, Claude reasons and replies only. It can't fetch live data, run code, or touch external systems.
Your agent needs live data to answer a question. How does Claude know it can search the web — and what arguments to pass?
Declare each capability as a in the tools array of messages.create. Each schema needs three fields: name (unique identifier for routing), description (plain English telling Claude when to use it), and (JSON Schema for arguments).
The description is most important. Claude reads it to pick the right tool when several exist. Vague descriptions cause mis-routing.
The input_schema follows JSON Schema: set type, list properties with types and descriptions, mark required fields in required. Claude populates only declared fields.
import anthropic client = anthropic.Anthropic() tools = [ { "name": "web_search", "description": "Search the web for current information on a topic. " "Use this when the answer may have changed recently.", "input_schema": { "type": "object", "properties": { "query": {"type": "string", "description": "The search query"} }, "required": ["query"] } } ]
This defines one tool — web_search — with a single required argument query. Notice the description says when to use it, not just what it does — that phrasing is what guides Claude's routing decision.
Claude ignores the tools entirely — it only knows about tools you pass in the tools= argument on each call. There is no persistent tool registry; you must include the array every time.
When Claude decides to call a tool, it returns a response whose is "tool_use" instead of . The content list contains one or more blocks of type "tool_use", each carrying an id, a name, and an input dict.
Your code must check stop_reason first, then loop over content to find blocks where block.type == "tool_use". Route each block to the matching Python function using block.name, and pass block.input as keyword arguments.
After executing the tool, you must send back a message that references the same tool_use_id. Without it, Claude has no way to continue — the loop stalls.
response = client.messages.create( model="claude-opus-4", max_tokens=1024, tools=tools, messages=[{"role": "user", "content": "What's the latest Claude model?"}] ) if response.stop_reason == "tool_use": for block in response.content: if block.type == "tool_use": result = dispatch(block.name, block.input) # your router
This is the delta from Stage 1: the API call now includes tools=tools, and the response handler checks stop_reason before touching content. The dispatch call is your router — you'll implement it next.
"end_turn" — Claude only sets stop_reason to "tool_use" when it wants your code to execute a tool. If it can answer from context alone, it returns "end_turn" and the content list contains only text blocks.
def dispatch(name, inputs): if name == "web_search": return web_search(inputs["query"]) # your real search fn raise ValueError(f"Unknown tool: {name}") # Append assistant turn + tool result, then call again messages = [ {"role": "user", "content": "What's the latest Claude model?"}, {"role": "assistant", "content": response.content}, {"role": "user", "content": [{ "type": "tool_result", "tool_use_id": block.id, "content": result }]} ] final = client.messages.create( model="claude-opus-4", max_tokens=1024, tools=tools, messages=messages )
The block must carry the exact tool_use_id from the assistant's block — that's how Claude matches result to request. The full message history (user → assistant → tool_result) is appended and sent in one new messages.create call so Claude can synthesize the final answer.
Add "is_error": true to the tool_result block and put the error message in "content". Changed lines:
"is_error": true,
"content": f"Tool '{name}' not found."
Without is_error, Claude treats the error string as a valid result and may hallucinate a follow-up. With it, Claude knows the tool call failed and can decide to retry or tell the user.
Three failure patterns cause most tool-use bugs. The third one developers miss longest.
tool_use_id doesn't match the block's id, the API returns 400: "tool_use_id does not match any tool use block". Always read block.id, never hardcode it.response.content as an assistant message, the API returns "messages must alternate between user and assistant roles". Sequence: user → assistant (with tool_use block) → user (with tool_result).name matches your dispatch function exactly, (2) every required argument is in required, (3) description says when to call the tool, (4) is_error is handled in results. Generated schemas often omit required or duplicate another tool's description.Implement the while-loop that drives Claude: call the API, check for `tool_use` stop reason, execute the tool, append the result, and repeat until `end_turn` — revisiting the API fundamentals from Module 1 to show how message history accumulates.
Implements the while-loop that drives Claude through multi-step tool use until it signals end_turn.
Why this matters: This is the engine of your SEO agent — without the loop, Claude answers once and stops, unable to search, read results, and refine its output.
Decision this forces: When to let the loop run autonomously vs. inserting a human-approval checkpoint between tool calls?
stop_reason does the API return? What does the content block look like? Write your answer, then continue. is a while.
Each iteration adds two entries to messages: the assistant's response (with tool_use reply. The growing history tracks what Claude has done and what remains.
Two signals control the loop. tool_use means "run this tool." end_turn means "here is the final answer."
Three failure patterns account for most agentic loop bugs.
.content. Claude sees a malformed turn and the next call raises 400 Bad Request: messages[N].content must be an array. Fix: append {"role": "assistant", "content": response.content}.end_turn. You burn tokens and hit rate limits. Fix: add a max_iterations guard and raise an explicit error when it trips.400 prompt is too long — or early turns are silently truncated. Claude forgets a tool result and produces a wrong answer with no error. Watch the total token count across turns.messages = [{"role": "user", "content": user_query}]
while True:
response = client.messages.create(
model="claude-opus-4",
max_tokens=1024,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break
# BUG: appending the whole response object, not .content
messages.append({"role": "assistant", "content": response})This loop looks right but corrupts the message history on every tool-calling turn.
The API raises 400 Bad Request: messages[1].content must be an array of content blocks, not a Message object. The loop never reaches a second tool call — it crashes immediately because the assistant turn is malformed.
MAX_ITER = 10 messages = [{"role": "user", "content": user_query}] for _ in range(MAX_ITER): response = client.messages.create( model="claude-opus-4", max_tokens=1024, tools=tools, messages=messages, ) messages.append({"role": "assistant", "content": response.content}) # fixed if response.stop_reason == "end_turn": break tool_result = execute_tool(response.content) # your dispatch fn messages.append({"role": "user", "content": tool_result}) else: raise RuntimeError("Agent exceeded max iterations")
Two changes from Stage 1 fix both failure modes: response.content (not the whole object) is appended, and a for/else guard raises an error if the loop exhausts MAX_ITER signal.
['user' (original query), 'assistant' (content with tool_use block), 'user' (tool_result block)]. The pattern is user→assistant→user→assistant→… — each pair is one tool round-trip. Claude reads the full history on every call.
MAX_ITER = 8 messages = [{"role": "user", "content": "Summarise the top AI news today."}] for _ in range(MAX_ITER): response = client.messages.create( model="claude-opus-4", max_tokens=1024, tools=tools, messages=messages, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason == "end_turn": print(response.content[-1].text) break # TODO: build and append the tool_result turn here # Hint 1: iterate response.content to find blocks where block.type == "tool_use" # Hint 2: the role for a tool result turn is "user", not "assistant"
Stop — attempt the TODO before revealing. The missing lines are the crux of the loop: dispatching the tool and feeding its result back so Claude can continue.
# CHANGED: these two lines replace the TODO
results = [execute_tool(b) for b in response.content if b.type == "tool_use"]
messages.append({"role": "user", "content": results})
# Why it changed from Stage 2: we now handle multiple tool_use blocks in one turn
# (Claude can request more than one tool at once), collecting all results before
# appending a single user turn — keeping the role alternation intact.
Before trusting any AI-generated agentic loop, run this four-point check:
[m['role'] for m in messages] after each iteration. It must alternate user → assistant → user.response.content (a list), not the raw response object.max_iterations ceiling exists and raises when hit.Module 5 wires this loop into a complete web-research-and-summarize agent. It combines the API setup from Module 1, the tool schema from Module 3, and the loop you just built into one runnable program.
Combine the API setup, tool schema, and agentic loop into a complete web-research-and-summarize agent: given a query, Claude searches, reads results, and returns a structured summary — you complete the tool-dispatch function from a partial scaffold.
Combine tool schemas, a dispatcher, and the agentic loop into a complete web-research-and-summarize agent in Python.
Why this matters: This is the hands-on assembly step — everything from modules 1–4 converges here into a script you can actually run for your SEO/GEO page tool.
Decision this forces: How to structure the final answer — free-form Claude output vs. a forced structured JSON response via a final tool call?
Answer: tool_use means Claude wants to call a tool — execute it and append the result. means Claude is done — break the loop and return its final message.
Module 4 built that loop in isolation. Now you wire it to real tools and a structured output so the whole agent runs end-to-end.
that glues them together.
block back into message history.
The final-answer decision sits at the loop's exit: let Claude write free-form prose, or force structured JSON by calling one last tool. That choice shapes how downstream code consumes output.
You're building an SEO tool. A user types: "What are the top ranking factors for local search in 2025?" Your agent must search the web, read results, and return a structured summary.
Here's what happens inside the loop on a real run:
web_search with "local search ranking factors 2025".tool_result.fetch_page on the top URL.tool_result.stop_reason becomes end_turn — it writes the final summary and exits.Notice step 3: Claude chose a second tool call you didn't script. That's the agent deciding what to do next — exactly the behavior separating an agent from a fixed pipeline.
import anthropic, json client = anthropic.Anthropic() TOOLS = [ {"name": "web_search", "description": "Search the web and return top snippets.", "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}, {"name": "fetch_page", "description": "Fetch full text of a URL.", "input_schema": {"type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"]}}, ]
This stage defines the two-tool registry Claude will see. Both tools follow the same shape from Module 3 — name, description, and a JSON Schema object.
Your code has no branch to handle the tool_use block, so the loop either crashes with a KeyError/AttributeError or silently ignores the call and exits — Claude never gets the tool result, and the final answer is based on zero real data.
def dispatch(tool_name, tool_input): if tool_name == "web_search": return fake_search(tool_input["query"]) # replace with real call if tool_name == "fetch_page": return fake_fetch(tool_input["url"]) # replace with real call raise ValueError(f"Unknown tool: {tool_name}") def run_agent(goal: str) -> str: messages = [{"role": "user", "content": goal}] while True: resp = client.messages.create( model="claude-opus-4", max_tokens=1024, tools=TOOLS, messages=messages ) if resp.stop_reason == "end_turn": return resp.content[0].text for block in resp.content: if block.type == "tool_use": result = dispatch(block.name, block.input) messages += [ {"role": "assistant", "content": resp.content}, {"role": "user", "content": [{"type": "tool_result", "tool_use_id": block.id, "content": result}]}, ]
Stage 2 adds the dispatcher and the full . The loop calls the , routes each block through dispatch(), and appends the before the next turn. It exits cleanly on .
3 messages: [user goal, assistant (with tool_use block), user (with tool_result block)]. The assistant turn carries the full resp.content — including any text Claude wrote before the tool call — not just the tool_use block.
SUMMARY_TOOL = {
"name": "return_summary",
"description": "Return the final structured summary to the caller.",
"input_schema": {
"type": "object",
"properties": {
"headline": {"type": "string"},
"key_points": {"type": "array", "items": {"type": "string"}},
"sources": {"type": "array", "items": {"type": "string"}},
},
"required": ["headline", "key_points", "sources"],
},
}
def run_agent_structured(goal: str) -> dict:
messages = [{"role": "user", "content": goal}]
all_tools = TOOLS + [SUMMARY_TOOL]
while True:
resp = client.messages.create(
model="claude-opus-4", max_tokens=1024,
tools=all_tools, messages=messages
)
for block in resp.content:
if block.type == "tool_use":
if block.name == "return_summary":
return block.input # ← TODO: what goes here instead?
result = dispatch(block.name, block.input)
# … append tool_result as before …Stop — attempt the TODO before revealing. The scaffold adds a return_summary tool so Claude delivers a typed dict instead of free prose. Your job: replace the return block.input line so the loop also handles the case (Claude might still exit without calling the tool).
Add this branch BEFORE the for-block loop (changed lines flagged):
if resp.stop_reason == "end_turn": # ← NEW: safety exit
return {"headline": resp.content[0].text,
"key_points": [], "sources": []} # ← NEW: graceful fallback
Why it changed: without this branch, an end_turn response falls through the for loop with no matching tool_use block and the function returns None — a silent failure. The fallback wraps Claude's prose in the expected dict shape so callers never get None.
ValueError: Unknown tool: web_lookup. Fix: log the bad name and return an error string as the tool_result instead of crashing.block.input is missing "sources". Fix: validate with block.input.get("sources", []) or a Pydantic model.tool_use and end_turn) are present and return the same type.tool_use_id — a mismatched ID causes Claude to ignore the result silently.Run the agent on a known query and print resp.stop_reason plus each tool call name on every turn. If the trace doesn't match the expected search→fetch→summarize path, the bug is in the dispatcher or message-append order — not in Claude.
With a working, traceable agent in hand, the next module goes deeper: it diagnoses the five most common agent failures (over-broad permissions, runaway loops, context overflow, tool mis-routing, and hallucinated tool arguments) and gives you a systematic checklist for catching them before production.
Diagnose the five most common Claude agent failures — over-broad permissions, runaway loops, context overflow, tool mis-routing, and hallucinated tool arguments — and apply concrete fixes to the agent from Module 5.
Diagnose and fix the five most common Claude agent failures — over-broad permissions, runaway loops, context overflow, tool mis-routing, and hallucinated arguments — and decide when automated guardrails are enough versus when a human approval gate is required.
Why this matters: Your web-research agent from Module 5 has none of these guards yet; adding them is what separates a demo from a production-ready build.
Decision this forces: Which failure risks justify a human-in-the-loop approval gate vs. automated guardrails alone?
Answer: tool_use triggers tool execution and another API call. exits the loop and returns final text. That loop is your agent's engine. This module covers five ways it can fail.
# Variation: agent now has THREE tools; add the missing schema guard ALLOWED_TOOLS = {"web_search", "summarize_text", "save_report"} def validate_and_dispatch(tool_name: str, tool_input: dict) -> str: if tool_name not in ALLOWED_TOOLS: return f"ERROR: tool '{tool_name}' is not permitted." if tool_name == "web_search": if "query" not in tool_input: return "ERROR: web_search requires 'query' field." return web_search(tool_input["query"]) if tool_name == "save_report": # TODO: guard that 'filename' AND 'content' are both present; # return the right error string if either is missing return save_report(tool_input["filename"], tool_input["content"])
Stop — attempt the TODO before revealing. The new tool is save_report, which writes a file and needs both filename and content — either missing should return a clear error string, not raise an exception.
Changed lines (the TODO block):
if tool_name == "save_report":
if "filename" not in tool_input:
return "ERROR: save_report requires 'filename' field."
if "content" not in tool_input:
return "ERROR: save_report requires 'content' field."
return save_report(tool_input["filename"], tool_input["content"])
Why order matters: checking 'filename' first gives Claude a specific, actionable error. If you checked both at once with 'and', a missing 'content' would silently pass the first guard and crash on tool_input["content"] — the exact bug you're guarding against.
Every Claude agent failure traces back to one of five root causes, each with a distinct symptom you can observe in the tool-call trace.
Failures 1–2 are design mistakes; 3–5 are runtime mistakes. Both need different fixes.
Here's what each failure produces in your running web-research agent — the symptom you'd actually see, not a generic warning.
write_file tool alongside web_search. A bad prompt tricks it into overwriting config.json. No error — the damage is silent.turn 47… turn 48… in logs until you kill the process.anthropic.BadRequestError: prompt is too long and the agent crashes mid-task.summarize_text instead of web_search on the first turn because both descriptions mention "research". Output is an empty summary.query but Claude sends {"search_term": "…"}. Your dispatcher raises KeyError: 'query'.# Unguarded loop from Module 5 — spot what's missing while True: response = client.messages.create( model="claude-opus-4", max_tokens=1024, tools=tools, messages=messages ) if response.stop_reason == "end_turn": break # execute tool, append result, loop forever if tool keeps failing
This is the loop you shipped in Module 5 — it works for the happy path but has no guards against any of the five failures.
The loop never hits end_turn. It retries forever, appending tool_result error messages to history. Context grows until the API raises anthropic.BadRequestError: prompt is too long — or you hit your token quota. No exception is raised by the loop itself.
MAX_TURNS = 10 MAX_HISTORY_CHARS = 40_000 # rough proxy for token budget turn = 0 while turn < MAX_TURNS: # Trim history if it's growing too large history_size = sum(len(str(m)) for m in messages) if history_size > MAX_HISTORY_CHARS: messages = messages[:2] + messages[-4:] # keep system + last 4 response = client.messages.create( model="claude-opus-4", max_tokens=1024, tools=tools, messages=messages ) if response.stop_reason == "end_turn": break # … execute tool, append result … turn += 1 else: raise RuntimeError(f"Agent exceeded {MAX_TURNS} turns — possible loop")
Two guards added: a turn counter that raises after MAX_TURNS (fixing runaway loops), and a history-size trim that prunes old messages before they overflow the context window.
The trim keeps the first two messages (system prompt + original user query) and the four most recent turns — preserving intent and recency.
The caller gets RuntimeError: Agent exceeded 10 turns. The trim risk: dropping middle turns can erase tool results Claude needs for later reasoning — the agent may re-call a tool it already ran, wasting turns. Mitigate by summarizing dropped turns instead of deleting them.
ALLOWED_TOOLS = {"web_search", "summarize_text"} # allowlist
def validate_and_dispatch(tool_name: str, tool_input: dict) -> str:
if tool_name not in ALLOWED_TOOLS:
return f"ERROR: tool '{tool_name}' is not permitted."
if tool_name == "web_search":
if "query" not in tool_input: # schema guard
return "ERROR: web_search requires 'query' field."
return web_search(tool_input["query"])
if tool_name == "summarize_text":
if "text" not in tool_input:
return "ERROR: summarize_text requires 'text' field."
return summarize_text(tool_input["text"])This dispatcher enforces two things at once: an allowlist that blocks any tool Claude wasn't explicitly given (over-broad permission fix), and field-presence checks that catch hallucinated argument names before they reach your tool code.
It returns the string "ERROR: web_search requires 'query' field.". That string is appended as the tool_result content. Claude reads it and — if well-prompted — retries with the correct field name. Without this guard, your code would raise KeyError: 'query' and crash the loop.
Before reading the summary: from memory, list the five steps your agent script executes on every iteration of the loop, name the two stop_reason values that control the loop's flow, and state the single most dangerous permission mistake to avoid in production.
Apply what you learned to SEO/GEO page — the page title is the H1 and the exact search query. OPEN with a 40-60 word self-contained, quotable answer capsule that names the entity explicitly in sentence one (no "in this lesson…" preamble) — this is the sentence an LLM lifts. Name the entity by its full name every time (not pronoun-only). Structure each section as ONE sub-question (How it works · X vs Y · When to use it · Example · Common mistakes), each opening with its own 1-2 sentence direct answer then depth. End with a genuine 3-5 question FAQ phrased as real user questions ending in "?" (these become FAQPage schema + the knowledge check). Prefer quotable specifics — concrete numbers, defaults, version names, one short snippet — over vague prose. Self-contained for a reader who arrived cold from a search engine or an LLM. Intent = tutorial: numbered steps + one short runnable code snippet + a common-mistakes section. Target query: "claude ai agents"..
Your agent must research a legal contract, cross-reference case law across 50 documents, and produce a structured risk report. Which Claude model is the right default choice for this task, and why?
claude-opus-4 is the right pick when a task requires complex, multi-step reasoning over large or ambiguous inputs — exactly what legal cross-referencing demands. claude-sonnet-4 is the cost/speed choice for well-scoped, lower-complexity tasks, not for high-stakes reasoning. The third option is wrong because agentic loops have no blanket speed preference — the decision is driven by task complexity. The fourth option is wrong because tool use is available on both models.
You are designing an agent that must scrape a URL, summarize the page, query a database, and send a Slack notification — all in one goal. Your context window is already near its limit. Which architecture fits best?
When tool count is high and context is near its limit, a supervisor-worker split lets each worker operate in a fresh, smaller context while the supervisor coordinates the overall goal. A single agent with all four tools risks context overflow and makes debugging harder. A pipeline topology works when steps are strictly sequential with no branching decisions — a supervisor is better when the coordinator must choose which worker to call next. There is no hard cap on tool count that prohibits a single agent; the constraint here is context size and complexity.
Read this Python snippet and choose what happens when it runs inside an agentic loop:
if response.stop_reason == 'tool_use':
messages.append({'role': 'assistant', 'content': response.content})
The loop then calls the tool and appends a tool_result — but the developer forgot to append the tool_result message before the next API call. What is the most likely outcome?
The Anthropic Messages API requires that every tool_use block in an assistant turn is followed by a matching tool_result in the next user turn. Omitting the tool_result message either triggers an API validation error or causes Claude to operate on an incomplete context, leading to hallucinated or corrupted reasoning. Claude does not silently ignore missing results, nor does it exit cleanly — the loop breaks or produces wrong output. An AuthenticationError is unrelated to message structure.
When would you choose to insert a human-approval checkpoint inside an agentic loop rather than relying on automated guardrails alone?
A human-in-the-loop gate is justified when a tool action cannot be undone — deleting records, sending messages, or spending money. Automated guardrails (loop guards, argument validators, context-length checks) handle recoverable errors well, but they cannot undo a sent email. Iteration count alone is not the trigger; a long-running read-only loop is safe to run autonomously. Model choice and description length do not determine whether human approval is needed.
A tool schema you wrote has the description: 'Does stuff with files.' After testing, you notice Claude frequently calls this tool when it should call a separate search_web tool instead. Name two concrete fixes — one to the tool schema itself and one to the agentic loop — that together reduce mis-routing.
Vague tool descriptions are the leading cause of mis-routing because Claude uses the description to decide which tool to call. A specific description with explicit exclusions (what the tool does NOT do) dramatically narrows ambiguity. A tool-argument validator in the loop catches bad calls before they execute, giving the agent a chance to self-correct. A max-iterations guard is a safety net but does not fix the root cause.