Continue After Tool Use
Why a Loop Is Needed
After you return tool results (Lesson 87), Claude continues. Its next response can be:
- a final answer (
stop_reason == "end_turn"), or - more tool calls (
stop_reason == "tool_use"), because the results raised new questions or the task needs another step.
For example, to recommend a next course, Claude might first check the student's enrollments, then search the catalog based on what it learned, then answer. That is two rounds of tool calls before the final answer. Your code cannot know in advance how many rounds a task needs, so tool use is implemented as a loop:
send messages + tools
│
▼
response ──► stop_reason == "tool_use"? ──yes──► run tools ──► append results ──┐
│ │
no │
▼ │
final answer ◄─────────────── send again ◄────────────────┘
This loop is the core of every agent: a system in which the model decides, step by step, which actions to take until a task is done.
The Loop, Step by Step
- Send the conversation and the tools.
- Append Claude's response to the conversation, unchanged, as an assistant message.
- Check
stop_reason:tool_use: run every tool call, append one user message with all the results, and go back to step 1;end_turn: the final answer is ready;- anything else (
max_tokens,refusal, …): stop and handle it (Lessons 20 and 43).
- Stop after a maximum number of steps, so a loop can never run forever.
Rules for Continuing Correctly
| Rule | Why |
|---|---|
| Send the same tools in every request | Earlier messages refer to the tools; removing them makes the conversation inconsistent (Lesson 84) |
| Append Claude's responses unchanged | Thinking blocks in the latest assistant turn must be returned exactly as received in tool-use conversations (Lesson 16) |
| Keep the full sequence | User → assistant (tool calls) → user (results) → assistant (more calls or answer) … |
| Limit the number of steps | A confused task, or a tool that keeps failing, could otherwise loop indefinitely, multiplying cost |
| Check the stop reason every time | Only tool_use means "run tools and continue" |
Server Tools and pause_turn
If you also use server tools, such as web search (Lesson 155), a response can end with pause_turn. Continue by sending the conversation again with the paused response appended unchanged, without adding a user message (Lesson 20). The loop below handles this case, although the demo tools here are all client tools.
Cost and Context Growth
Every round adds messages: the tool calls, then the results. Each new request re-sends everything so far, so input tokens grow with each round (Lesson 24). Keep this in mind:
- Concise tool results keep each round small (Lesson 86).
- A step limit caps the worst case.
- Prompt caching makes the growing, unchanged prefix of the conversation much cheaper (Lessons 121–130).
- Parallel tool calls (several calls in one round) save rounds compared with one call per round.
Continuing the Conversation After the Answer
The final answer is not necessarily the end. The messages list, with its tool calls and results, is the conversation history. To ask a follow-up question, append a new user message and run the loop again. Claude can then use what earlier tools returned without calling them again.
Practical Example: A Tool-Using Agent Loop
This agent answers questions with the tools from Lesson 86. It runs as many tool rounds as needed, up to a limit, prints each step, and supports follow-up questions on the same conversation.
"""agent_loop.py: continue the conversation until Claude stops calling tools."""
from anthropic import Anthropic
from anthropic.types import Message
from demo_tools import REGISTRY
from tools_lib import ToolContext, run_tool
client = Anthropic()
MODEL = "claude-opus-5-5"
MAX_STEPS = 6
SYSTEM = (
"You are the GOCOURSE study assistant. Use the tools for course, enrollment, and weather "
"questions; never guess what a tool can tell you. Keep the final answer under 120 words."
)
TOOL_DEFINITIONS = [tool.definition() for tool in REGISTRY.values()]
def text_of(response: Message) -> str:
return "".join(block.text for block in response.content if block.type == "text").strip()
def tool_results_message(response: Message, ctx: ToolContext, step: int) -> dict:
results = []
for block in response.content:
if block.type != "tool_use":
continue
outcome = run_tool(REGISTRY, block.name, dict(block.input or {}), ctx)
flag = " (error)" if outcome.is_error else ""
print(f" [step {step}] {block.name}({block.input}){flag}")
print(f" -> {outcome.content}")
result = {"type": "tool_result", "tool_use_id": block.id, "content": outcome.content}
if outcome.is_error:
result["is_error"] = True
results.append(result)
return {"role": "user", "content": results}
def run_agent(question: str, ctx: ToolContext, history: list[dict] | None = None,
max_steps: int = MAX_STEPS) -> tuple[str, list[dict]]:
"""Answer a question, using tools as needed. Returns (answer, updated history)."""
messages = list(history or []) + [{"role": "user", "content": question}]
for step in range(1, max_steps + 1):
response = client.messages.create(
model=MODEL, max_tokens=4096, system=SYSTEM,
tools=TOOL_DEFINITIONS, messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
messages.append(tool_results_message(response, ctx, step))
continue
if response.stop_reason == "pause_turn":
continue # Server tools paused: resend as-is
if response.stop_reason == "end_turn":
return text_of(response), messages
return f"[Stopped: {response.stop_reason}] {text_of(response)}", messages
return "[Stopped: the task needed too many tool steps.]", messages
if __name__ == "__main__":
ctx = ToolContext(user_id="meena")
question = ("How far am I in my courses, which Django course should I take next, "
"and is the weather good for a walk in Bengaluru today?")
print(f"Q: {question}")
answer, history = run_agent(question, ctx)
print(f"A: {answer}\n")
follow_up = "How many weeks is that next course?"
print(f"Q: {follow_up}")
answer, history = run_agent(follow_up, ctx, history)
print(f"A: {answer}")
print(f"\n[{len(history)} messages in the conversation]")
Example output (wording and tool choices vary):
Q: How far am I in my courses, which Django course should I take next, and is the weather good for a walk in Bengaluru today?
[step 1] get_enrollments({})
-> [{"course": "django-101", "progress": 65}, {"course": "sql-basics", "progress": 100}]
[step 1] get_weather({'city': 'Bengaluru'})
-> 24°C, partly cloudy, no rain expected
[step 2] search_courses({'topic': 'Django'})
-> [{"slug": "django-101", "title": "Django for Beginners", "level": "beginner", "weeks": 6}, {"slug": "django-apis", "title": "Advanced Django APIs", "level": "advanced", "weeks": 4}]
A: You've completed SQL Basics and you're 65% through Django for Beginners, so finish that first. After it, Advanced Django APIs is the natural next step. Bengaluru is 24°C and partly cloudy with no rain, so it's a good day for a walk.
Q: How many weeks is that next course?
A: Advanced Django APIs takes 4 weeks.
[8 messages in the conversation]
How It Works
TOOL_DEFINITIONSis built once from the registry and sent with every request, so the tools are identical across the whole conversation.run_agent()copies the history, adds the question, and loops at mostmax_stepstimes. Each iteration is one request to Claude.- The response is appended unchanged as an assistant message, before anything else happens. That keeps thinking blocks and tool calls intact, as required.
- On
tool_use,tool_results_message()runs every call withrun_tool()(Lesson 86) and builds one user message with all results (Lesson 87). Then the loop sends the conversation again. - On
pause_turn, the loop continues without adding a user message, which is the documented way to resume paused server tools. - On
end_turn, the final text is returned together with the full history. Any other stop reason is reported clearly instead of being shown as a normal answer. - The step limit guarantees that the loop ends. If it is reached, the function says so, rather than returning a misleading answer.
- In the first question, Claude made two parallel calls in step 1 (enrollments and weather, which are independent), then a second round in step 2 (the course search, which depended on what it learned about the student's progress), then answered.
- The follow-up reuses
history. Claude answers from the course data already in the conversation, with no new tool calls. - The 8 messages are: for the first question, the question, the step-1 tool calls, their results, the step-2 tool call, its result, and the final answer (6 messages); for the follow-up, the question and the answer (2 messages).
Note: the SDK's tool runner (Lesson 92) implements this loop for you, including running tools and formatting results. Writing it once by hand makes it clear what the runner automates, and gives you full control when you need custom behavior.
Common Mistakes
Handling Only One Round of Tool Calls
What happens: when Claude needs a second round, the code treats the second tool_use response as the answer.
Fix: loop until stop_reason is no longer tool_use.
No Step Limit
What happens: a task that keeps calling tools, for example because a tool keeps failing, runs and bills indefinitely.
Fix: set a maximum number of steps, and report when it is reached.
Changing or Dropping Tools Between Rounds
What happens: inconsistent conversations, errors, and lost caching benefits.
Fix: send the same tool definitions in every request.
Modifying Assistant Messages
What happens: 400 errors in tool-use conversations, because thinking blocks must be returned unchanged.
Fix: append response.content exactly as received.
Ignoring Non-Tool Stop Reasons in the Loop
What happens: truncated or refused responses are returned as normal answers.
Fix: handle max_tokens, refusal, and others explicitly.
Best Practices
- Implement tool use as a loop that continues while
stop_reasonistool_use. - Append every response unchanged, and all tool results in one message.
- Send the same tools every time.
- Limit the number of steps, and report when the limit is reached.
- Keep tool results concise, because every round re-sends the conversation.
- Return the history so follow-up questions can build on earlier tool results.
- Consider the SDK's tool runner once you understand the loop.
Exercise
- Run the agent with
max_steps=1on the first question. What happens, and why? - Count and print the total
input_tokensandoutput_tokensacross all requests made byrun_agent(). - Ask a question that needs no tools. How many requests does
run_agent()make, and how many messages does it add to the history? - Store
historyas JSON (convert assistant content withto_dict(), Lesson 24), load it again, and ask a follow-up question.
Step 1: the first request returns tool_use, the tools run, and the results are appended, but the loop has no steps left to send them back to Claude. The function returns [Stopped: the task needed too many tool steps.]. A step limit must be large enough for realistic tasks; typical values are between 5 and 20, depending on how many rounds your tools usually need.
Step 2: accumulate usage inside the loop:
total_in = total_out = 0
for step in range(1, max_steps + 1):
response = client.messages.create(...)
total_in += response.usage.input_tokens
total_out += response.usage.output_tokens
...
print(f"[usage: {total_in} input, {total_out} output tokens]")
Input tokens grow with each round, because every request includes all earlier messages, which is why concise tool results and prompt caching matter for agents.