0Pricing
Claude Architect · Lezione

Terminare con stop_reason

Lasci che end_turn termini il ciclo, invece di basarsi sul confronto di stringhe.

Terminare con stop_reason è una lezione Claude Architect gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Claude Architect, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Claude Architect include 4 lezioni in totale.

Parti di questa lezione non sono ancora state tradotte e vengono mostrate in inglese.

The Loop Needs an Exit

An agentic loop is simple: you send a request, Claude responds, and you decide whether to keep going. The hard question is when to stop.

Every API response carries a stop_reason field. This is the model's own signal about why it stopped generating. Your loop should listen to that signal — not guess by reading the words in the reply.

This lesson teaches one rule that separates robust agents from fragile ones: let end_turn end the loop, not your string matching.

The Four Stop Reasons

Claude returns one of four stop_reason values on every turn:

  • end_turn — the model finished its response naturally. The task turn is complete.
  • tool_use — the model wants to call a tool. Run it, append the result, and continue.
  • max_tokens — output was truncated by your max_tokens limit.
  • stop_sequence — a custom stop sequence you configured was hit.

These four values are a complete, reliable contract. Your control flow should branch on them directly.

resp = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)
print(resp.stop_reason)  # end_turn | tool_use | max_tokens | stop_sequence

The Anti-Pattern: Parsing Text

A tempting shortcut is to read the model's text and look for a keyword like "done" or "finished" to decide the loop is over.

This is a classic anti-pattern. Text is probabilistic. The model might say "I'm done thinking, now let me call a tool" — and your matcher stops too early. Or it phrases completion differently and your loop runs forever.

Never parse text for completion signals. The structured stop_reason exists precisely so you don't have to.

# ANTI-PATTERN: do NOT do this
text = resp.content[0].text
if "done" in text.lower():
    break  # fragile, unreliable, exam-wrong

The Canonical Loop

Here is the correct shape of the loop. You inspect stop_reason each turn. When it is tool_use, you run the tools and append their results to the conversation. You repeat until stop_reason is end_turn.

Notice the loop is driven entirely by the structured signal — no text inspection decides termination.

while True:
    resp = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )
    messages.append({"role": "assistant", "content": resp.content})

    if resp.stop_reason == "end_turn":
        break

    if resp.stop_reason == "tool_use":
        results = run_tools(resp.content)
        messages.append({"role": "user", "content": results})
        continue

Full History, Every Turn

One reason the loop works: the model keeps no state between requests. Each API call must include the FULL messages history.

That is why, after a tool_use turn, you append the assistant's tool-call content AND the tool results back into messages before the next request. The model re-reads the entire conversation and decides whether more tools are needed or it can finish with end_turn.

Drop history, and the model loses the thread — it can't reach a coherent end_turn.

# Each request resends EVERYTHING
messages = [
    {"role": "user",      "content": "Refund order 4471."},
    {"role": "assistant", "content": [tool_use_block]},   # prior turn
    {"role": "user",      "content": [tool_result_block]}, # prior turn
]
resp = client.messages.create(model=MODEL, max_tokens=1024,
                              tools=tools, messages=messages)

tool_use Is Not a Stop

A common mistake is treating tool_use as a terminal state. It is not. It means "pause, run this tool, then come back to me."

When you see tool_use, you:

  • Execute the requested tool(s) in your own code.
  • Append the tool_result blocks to messages.
  • Send the request again so the model can continue.

Only end_turn means the work for this turn is genuinely finished.

if resp.stop_reason == "tool_use":
    tool_results = []
    for block in resp.content:
        if block.type == "tool_use":
            output = dispatch(block.name, block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": output,
            })
    messages.append({"role": "user", "content": tool_results})
    # loop continues -> next request

Decisions Are Model-Driven

The deeper principle: let the model decide when it is done. The model has the full context — the user's goal, the tool results, the conversation so far. It is better positioned than a hard-coded rule to judge whether the task is complete.

Your job as the architect is to provide good tools and clear instructions, then trust the end_turn signal. Reserve hard-coded control flow for guarantees you cannot leave to probability.

The Iteration Cap Is a Safety Net

You should still add a maximum-iteration guard — but understand its role. An iteration cap is a safety net to prevent runaway loops or cost blowouts. It is NOT the primary stop mechanism.

The primary stop is always end_turn. The cap only fires in the rare pathological case where the model never converges. If your loop relies on the cap to end normally, your design is broken.

MAX_ITERS = 20  # safety net, NOT the normal exit
for i in range(MAX_ITERS):
    resp = client.messages.create(model=MODEL, max_tokens=1024,
                                  tools=tools, messages=messages)
    messages.append({"role": "assistant", "content": resp.content})
    if resp.stop_reason == "end_turn":
        break  # normal exit
    # ... handle tool_use ...
else:
    log.warning("Hit iteration cap without end_turn")

Handling max_tokens

max_tokens is a distinct case that needs its own handling. It means the response was truncated mid-generation — the model did not finish its thought.

Treating this as a clean completion would silently cut off the agent's work. Depending on your design you might raise max_tokens, ask the model to continue, or flag the turn. What you must NOT do is fall through and assume the task is done.

if resp.stop_reason == "max_tokens":
    # output was cut off - NOT a completion
    log.warning("Response truncated; consider raising max_tokens or continuing")
    # handle explicitly; do not treat as end_turn

Where Hard Code Belongs

If model-driven decisions are the default, when do you reach for deterministic code?

For guarantees — outcomes that must hold every single time regardless of the model's judgment. Examples: a precondition that blocks a refund until get_customer returns a verified ID, or a hook that rejects any refund over a policy threshold.

These are 100% deterministic enforcement points. Termination, by contrast, is a model-driven decision you read from stop_reason. Don't confuse the two: hard-code guarantees, trust end_turn for flow.

Putting It Together

A production-grade agentic loop combines all the pieces:

  • Branch on stop_reason — never on text.
  • Resend full history each turn (the model is stateless).
  • tool_use → run, append, continue. end_turn → stop.
  • Handle max_tokens explicitly — truncation is not completion.
  • Keep an iteration cap as a safety net only.

This is the backbone of every reliable agent you'll architect.

for _ in range(MAX_ITERS):
    resp = client.messages.create(model=MODEL, max_tokens=2048,
                                  tools=tools, messages=messages)
    messages.append({"role": "assistant", "content": resp.content})

    if resp.stop_reason == "end_turn":
        break
    if resp.stop_reason == "max_tokens":
        handle_truncation(resp); break
    if resp.stop_reason == "tool_use":
        messages.append({"role": "user",
                         "content": run_tools(resp.content)})
        continue

Quick Check: Ending the Loop

A support agent built on the Agent SDK sometimes loops forever and sometimes stops before calling a needed tool. The loop currently breaks when the assistant's text contains the word "resolved". What is the correct fix?

Recap: Trust the Signal

Key takeaways:

  • Terminate on stop_reason, never by parsing text for words like "done" or "resolved".
  • end_turn ends the loop; tool_use means run tools, append results, and continue.
  • Handle max_tokens explicitly — truncation is not completion.
  • The model is stateless: resend the full message history every turn.
  • Termination is a model-driven decision; the iteration cap is only a safety net.
  • Reserve hard-coded enforcement for guarantees (preconditions, hooks), not for ending the loop.

Let end_turn end the loop. That single discipline makes your agents predictable and production-ready.

Domande Frequenti

La lezione «Terminare con stop_reason» è gratuita?

Sì — il testo completo di «Terminare con stop_reason» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Claude Architect, passa a CoddyKit PRO. Il corso Claude Architect include 4 lezioni in totale.

Cosa imparerò in «Terminare con stop_reason»?

Lasci che end_turn termini il ciclo, invece di basarsi sul confronto di stringhe. Eserciti Claude Architect con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare Claude Architect?

Non è richiesta alcuna esperienza precedente. Claude Architect su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.

Quanto tempo richiede la lezione «Terminare con stop_reason»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione Claude Architect?

Sì. Ogni lezione Claude Architect include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Il ciclo principale
  2. Terminare con stop_reason
  3. Anti-pattern: analizzare il testo per rilevare il completamento
  4. Anti-pattern: limiti arbitrari alle iterazioni
← Torna a Claude Architect