Anti-Pattern: Parsing Text for Completion
Why scanning output for 'done' is fragile and wrong.
Anti-Pattern: Parsing Text for Completion is a free Claude Architect lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Claude Architect learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Trap
You build an agent. It runs tools, thinks, replies. Now you need to know when it's done. A tempting shortcut: scan the model's text for a word like done, finished, or complete, and stop the loop when you see it.
This is the anti-pattern Parsing Text for Completion. It feels intuitive, but it is fragile and wrong. The Claude API already hands you a precise, structured completion signal. In this lesson you'll learn why text-scanning breaks and what to use instead.
How the Agentic Loop Really Works
The agentic loop is driven by one field on the response: stop_reason. The loop is simple and deterministic:
- Send the request (with the full message history every turn).
- Inspect
stop_reason. - If it is
tool_use, run the tools, append the results to history, and repeat. - If it is
end_turn, the task is complete. Stop.
The model itself tells you, through structured metadata, whether it wants to keep going or is finished. You never have to guess by reading prose.
while True:
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break # complete
# else: tool_use -> run tools, append results, loopThe Four Stop Reasons
Terminate on the signal, not on the words. Claude returns one of four stop reasons:
end_turn— Claude finished naturally. The task is complete.tool_use— Claude wants a tool run; execute it and continue the loop.max_tokens— the output was truncated by the token cap.stop_sequence— a configured stop sequence was hit.
Each is unambiguous and machine-readable. Compare that to scanning free text, where the meaning depends entirely on how the model happened to phrase itself this time.
Why Text-Scanning Is Fragile
Natural language is not a control protocol. The same completed task can end a dozen different ways:
- "All done!"
- "That completes the migration."
- "I've finished the analysis."
- "Everything is in place now."
Your keyword check for done matches the first, maybe the second, and silently misses the rest. The loop never stops. You burn tokens and time, or hit a hard cap, on a task that was actually finished turns ago.
# ANTI-PATTERN: brittle keyword scan
if "done" in response_text.lower():
break # misses "finished", "complete", "all set"...False Positives Are Worse
Missing the signal wastes resources. The opposite failure is more dangerous: stopping too early.
Suppose the user asks Claude to "check whether the deployment is done." Claude might reply, mid-investigation, "Let me verify the deploy is done before I continue." Your scanner sees done and kills the loop — abandoning the task halfway, before any tool ran or any answer was produced.
The model uses words like done conversationally. A keyword match cannot tell narration apart from a genuine completion signal. stop_reason can.
The Model Keeps No State
Here is the deeper reason text-scanning is the wrong layer. The Claude API is stateless: the model keeps no memory between requests. You send the entire conversation — system, every prior messages turn, tool results — on every single call.
So "is the agent done?" is a question about the loop you control, answered by the structured stop_reason the API returns — not something to be reverse-engineered from prose. The control plane (your loop) and the content plane (the model's text) are separate. Keep completion logic in the control plane.
# Stateless: full history goes up every turn
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
system=system_prompt,
tools=tools,
messages=messages, # ENTIRE history, every call
)
messages.append({"role": "assistant", "content": response.content})The Correct Termination Check
Drive the loop off stop_reason alone. When it is tool_use, run the requested tools and append their results to the history with the matching tool_use_id. When it is end_turn, you are done.
This works no matter how Claude phrases its final message — "All set," "Finished," or nothing at all. The decision is made by metadata you can trust, not by string matching you have to babysit.
while True:
response = client.messages.create(
model="claude-opus-4-8", max_tokens=16000,
tools=tools, messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
results = run_tools(response.content) # each with tool_use_id
messages.append({"role": "user", "content": results})
continue
if response.stop_reason == "end_turn":
break # done -- no text parsing anywhereDecisions Are Model-Driven
A key architectural principle: in an agentic loop, the flow is decided by the model, surfaced through stop_reason. You don't dictate "after 3 tools, you must be finished." Claude signals when it needs another tool and when it's done.
Reserve hard-coded control for things that need a guarantee — a deterministic invariant your business requires. Completion detection is not one of those things; the API already provides it cleanly. Hand-rolling it with text parsing replaces a reliable signal with a guess.
Iteration Caps Are a Safety Net
"Fine," you say, "I'll just stop after N iterations." Careful: an iteration cap is a safety net, not the primary stop mechanism. It exists to bound runaway loops and protect against bugs — not to decide that work is complete.
If your loop relies on the cap to terminate normally, you have the same disease as text-scanning: you're substituting an arbitrary heuristic for the real, model-driven signal. Terminate on stop_reason first; let the cap catch only the pathological case.
MAX_ITERS = 20 # safety net, NOT the primary stop
for _ in range(MAX_ITERS):
response = client.messages.create(...)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
break # primary: model-driven
# ... handle tool_use ...
else:
log.warning("hit iteration cap -- investigate, don't trust as 'done'")Handle the Other Stop Reasons
A robust loop branches on every stop reason, not just the happy path. In particular, max_tokens means the response was cut off — it is not a completion. Treating a truncated turn as "done" silently ships half an answer.
When you see max_tokens, the fix is to raise max_tokens or switch to streaming for large outputs, then continue — never to break out of the loop as if the task succeeded.
if response.stop_reason == "end_turn":
break
elif response.stop_reason == "tool_use":
messages.append({"role": "user", "content": run_tools(response.content)})
elif response.stop_reason == "max_tokens":
# truncated, NOT complete -- raise cap or stream, then continue
raise OutputTruncated("increase max_tokens or stream")Need a Structured Done Flag? Use a Tool
Sometimes you genuinely want the model to report a structured outcome — a status, a confidence, a summary — when it finishes. The answer is still not to parse prose. Give Claude a tool (or a JSON Schema via structured outputs) and let it emit a typed result.
With tool_choice: "any" you can force the model to call some tool, guaranteeing structured output instead of free text. The loop still terminates on stop_reason; the tool just carries the machine-readable verdict. Structure replaces string-matching everywhere it matters.
tools = [{
"name": "submit_result",
"description": "Report the final task outcome.",
"input_schema": {
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["success", "failed"]},
"summary": {"type": "string"},
},
"required": ["status", "summary"],
},
}]
# tool_choice={"type": "any"} forces a structured call, not proseQuick Check
Test your judgment on a realistic design decision.
Key Takeaways
You now know why scanning output for completion is the wrong tool for the job:
- Terminate on
stop_reason, never on text.end_turn= complete;tool_use= run tools and continue. - Text-scanning is fragile both ways — it misses real completions (varied phrasing) and triggers false positives (conversational use of "done").
- The API is stateless and model-driven. Send full history every turn; let the structured signal, not prose, drive the loop.
- Iteration caps are a safety net, not the primary stop. Handle
max_tokensas truncation, not success. - Need a structured outcome? Use a tool or JSON Schema (force it with
tool_choice: "any") — never parse free text.
Frequently asked questions
Is the “Anti-Pattern: Parsing Text for Completion” lesson free?
Yes — the full text of “Anti-Pattern: Parsing Text for Completion” is free to read here on the web, and the Claude Architect course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Claude Architect course, upgrade to CoddyKit PRO.
What will I learn in “Anti-Pattern: Parsing Text for Completion”?
Why scanning output for 'done' is fragile and wrong. You practise Claude Architect with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Claude Architect?
No prior experience is required. Claude Architect on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Anti-Pattern: Parsing Text for Completion” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Claude Architect lesson?
Yes. Every Claude Architect lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- The Core Loop
- Terminating on stop_reason
- Anti-Pattern: Parsing Text for Completion
- Anti-Pattern: Arbitrary Iteration Caps