Try this first

Search your agent.py for reply.stop. Count how many values you do something about.

Most agents handle one situation — “does it want a tool?” — and treat everything else as an
answer. Three other things can happen, and each one fails in a way that looks like a bug in
your code rather than a normal outcome.

Every provider has its own words

This is the one place in the course where the vocabulary genuinely differs, so here is the
whole map:

What happened OpenAI / Ollama Anthropic Gemini
Finished normally stop end_turn STOP
Wants a tool tool_calls tool_use STOP — check the calls
Cut off at your limit length max_tokens MAX_TOKENS
Declined the request content_filter refusal SAFETY and others

reply.stop gives you the provider’s own word. Recognise your own column, and remember from
Lesson 1.5 that the Gemini row is why wants_tool looks at the calls rather than the word.

The three that bite

The reply was cut off.

The reply looks normal. The text is truncated mid-sentence. If a tool call was being written
when the limit hit, its arguments may be incomplete too.

CUT_OFF = {"length", "max_tokens", "MAX_TOKENS"}

if reply.stop in CUT_OFF:
    print("[reply cut off — raise max_tokens]")
    # Do not treat the text as an answer, and do not run a partial tool call.
    break

The fix is a bigger limit, or a task split into smaller pieces. What you must not do is carry
on as if the reply were complete.

The model declined.

The request hit a safety boundary. This is usually a normal, successful HTTP response — not an
exception — with the refusal in the stop reason and the text empty or partial. That is what
makes it dangerous: code that reads the text unconditionally crashes or reports nonsense.

DECLINED = {"content_filter", "refusal", "SAFETY", "PROHIBITED_CONTENT"}

if reply.stop in DECLINED:
    print(f"[the model declined this request: {reply.stop}]")
    break

Two rules. Check the stop reason before using the text. And do not retry the same
prompt
— it will be declined again. Change the request or stop.

Benign work occasionally trips this, especially security tooling. Some providers offer a way
to re-run a declined request on a different model automatically; if yours does and you need
it, that is where to look.

It paused part-way.

Some providers pause a long turn and expect you to ask for the rest, rather than finishing in
one reply. Append the assistant turn and send again with no new user message:

if reply.stop in {"pause_turn", "PAUSE"}:
    messages.append({"role": "assistant", "content": reply.text,
                     "tool_calls": reply.tool_calls})
    continue          # no new user message — it resumes on its own

Do not add a “please continue” message. The provider sees the paused turn and resumes; an
extra user message just confuses the transcript.

Handle them all in one place

if reply.wants_tool:
    ...                      # run tools, append results, continue
elif reply.stop in PAUSED:
    continue                 # resume
elif reply.stop in CUT_OFF:
    print("[reply was cut off]")
    break
elif reply.stop in DECLINED:
    print("[the model declined this request]")
    break
else:
    ...                      # verify, then break

Ten lines. That is the difference between an agent that fails clearly and one that fails
mysteriously.

Note the order. wants_tool is checked first, before any stop word, because of the Gemini
problem from Lesson 1.5 — a provider can report “finished normally” while asking for a tool,
and checking words first would silently drop the request.

Three outcomes are silent failures if you ignore them, and every provider names them
differently. Know your own column, and check for tool calls before you check for words.

Try this before the next lesson

Force each one:

  • cut off: set max_tokens to 30 and ask for a long answer.
  • declined: hard to force deliberately, and you should not try very hard. Just make sure the
    branch exists and does not crash on empty content.
  • paused: skip until Module 5, when Rover talks to external services.

Then re-read your handler. If any branch uses reply.text before checking reply.stop, fix it now.