Continue After Tool Use

Su Suriya Ravichandran Updated 10 Nov 2026
9 min read ·Lesson 88 of 200

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

  1. Send the conversation and the tools.
  2. Append Claude's response to the conversation, unchanged, as an assistant message.
  3. 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).
  4. Stop after a maximum number of steps, so a loop can never run forever.

Rules for Continuing Correctly

RuleWhy
Send the same tools in every requestEarlier messages refer to the tools; removing them makes the conversation inconsistent (Lesson 84)
Append Claude's responses unchangedThinking blocks in the latest assistant turn must be returned exactly as received in tool-use conversations (Lesson 16)
Keep the full sequenceUser → assistant (tool calls) → user (results) → assistant (more calls or answer) …
Limit the number of stepsA confused task, or a tool that keeps failing, could otherwise loop indefinitely, multiplying cost
Check the stop reason every timeOnly 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

  1. TOOL_DEFINITIONS is built once from the registry and sent with every request, so the tools are identical across the whole conversation.
  2. run_agent() copies the history, adds the question, and loops at most max_steps times. Each iteration is one request to Claude.
  3. The response is appended unchanged as an assistant message, before anything else happens. That keeps thinking blocks and tool calls intact, as required.
  4. On tool_use, tool_results_message() runs every call with run_tool() (Lesson 86) and builds one user message with all results (Lesson 87). Then the loop sends the conversation again.
  5. On pause_turn, the loop continues without adding a user message, which is the documented way to resume paused server tools.
  6. 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.
  7. The step limit guarantees that the loop ends. If it is reached, the function says so, rather than returning a misleading answer.
  8. 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.
  9. The follow-up reuses history. Claude answers from the course data already in the conversation, with no new tool calls.
  10. 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_reason is tool_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

  1. Run the agent with max_steps=1 on the first question. What happens, and why?
  2. Count and print the total input_tokens and output_tokens across all requests made by run_agent().
  3. Ask a question that needs no tools. How many requests does run_agent() make, and how many messages does it add to the history?
  4. Store history as JSON (convert assistant content with to_dict(), Lesson 24), load it again, and ask a follow-up question.
Show answers for steps 1 and 2

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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Continue After Tool Use and get answers drawn from it.

Signed-in readers only.