Try this first
Look back at the loop from Lesson 1.1 and find the line that decides whether to keep going:
if not reply.wants_tool:
break
One question. That is the entire control flow of an agent.
What is underneath it
wants_tool is true when the reply contains at least one tool call. Read that again,
because it is deliberately not the obvious implementation.
Each provider also reports a reason it stopped, and the adapter keeps that word in
reply.stop so you can see it:
reply.stop |
Provider | What happened |
|---|---|---|
tool_calls |
OpenAI, Ollama | It wants a tool run |
tool_use |
Anthropic | The same thing |
STOP |
Gemini | Also the same thing — see below |
stop / end_turn |
OpenAI / Anthropic | It finished naturally |
length / max_tokens |
OpenAI / Anthropic | It hit your output limit mid-sentence |
refusal |
Anthropic | It declined the request |
Why the loop does not check the stop word
Look at the Gemini row. When Gemini asks for a tool, it reports FinishReason.STOP — the
same value it uses for a perfectly ordinary finished answer. There is no special word.
So an agent written like this:
if reply.stop in ("tool_calls", "tool_use"): # looks sensible, silently wrong
run_the_tools()
works on two providers and quietly breaks on a third. It never runs a tool, never errors, and
the model appears to ignore the tools you gave it.
The reliable question is not “what word did it stop with?” but “did it ask for anything?”:
if reply.tool_calls: # what wants_tool actually does
run_the_tools()
This is worth more than the portability. Even on one provider, the stop word is a report
about the conversation and the tool calls are the request itself. Check the request.
Two other rows deserve attention now, because ignoring them produces bugs that look like
something else.
The truncation trap
If the model runs out of output budget, you still get a normal-looking reply with
normal-looking text in it. The text is just cut off.
Code that only asks “does it want a tool?” treats that truncated text as a finished answer.
The learner then spends an hour wondering why the model “forgot” the end of its own sentence.
It did not forget. You cut it off.
if reply.stop in ("length", "max_tokens"):
print("Warning: the reply was cut off. Raise max_tokens.")
The one that will bite you in Module 4
A reply that does not want a tool means the model stopped talking. It does not mean the job
is done.
Read that again, because it is the failure that Module 4 is built around. The model can write
“I have updated the file and everything is working” and stop, having written nothing to disk.
The stop signal is correct. The claim is wrong.
Stop signals tell you about the conversation. They tell you nothing about the world. Checking
the world is your job, and we build that check in Module 4.
The stop signal tells you why the model stopped talking. It never tells you whether the work
got done.
Try this before the next lesson
Set max_tokens=20 and ask something that needs a long answer.
Print reply.text and reply.stop together. Then look up your provider’s word for
truncation in the table above and confirm you got it. That is the value your loop has to
handle, and now you have seen it with your own model.