Skip to content

fix(conversation_loop): detect structured reasoning exhaustion from Ollama fallback models - #28133

Closed
renanserpa wants to merge 1 commit into
NousResearch:mainfrom
renanserpa:fix/structured-reasoning-fallback-exhaustion
Closed

fix(conversation_loop): detect structured reasoning exhaustion from Ollama fallback models#28133
renanserpa wants to merge 1 commit into
NousResearch:mainfrom
renanserpa:fix/structured-reasoning-fallback-exhaustion

Conversation

@renanserpa

Copy link
Copy Markdown

Summary

  • Extends _thinking_exhausted detection in conversation_loop.py to recognize structured reasoning (message.reasoning / message.reasoning_content) in addition to inline <think> tags
  • When exhaustion is triggered via structured reasoning, appends a 600-char preview of the captured reasoning to the user-facing error message

Problem

Models like qwen3:8b via Ollama return reasoning in the structured message.reasoning field instead of inline <think> tags. The existing detection only checked for <think> tags, so these responses fell through to 3 useless continuation retries + 2 prefills before returning "(empty)" to the user — making the fallback appear "dumb."

Changes

agent/conversation_loop.py — two blocks added:

  1. Detection (line ~1391): New _has_structured_reasoning and _content_is_empty checks. _thinking_exhausted now triggers on (has_think_tags AND no_visible_content) OR (has_structured_reasoning AND content_is_empty).

  2. Reasoning preview (line ~1431): When exhaustion is detected via structured reasoning (not think tags), the user sees the first 600 chars of what the model was thinking instead of a blank response.

Test plan

  • Verify with a model that returns content="" + reasoning="..." (e.g., qwen3:8b via Ollama with /thinkon enabled)
  • Verify existing <think> tag exhaustion detection still works unchanged
  • Verify models that return content="" without any reasoning (e.g., GLM-4.7 truncation) still get normal continuation retries, not false-positive exhaustion

Reproduction

# Force fallback to qwen3:8b and ask a question
# Before fix: returns "(empty)" after 5 retries
# After fix: returns "Thinking Budget Exhausted" + reasoning preview

🤖 Generated with Claude Code

…llama fallback models

Models like qwen3:8b via Ollama return reasoning in the structured
`message.reasoning` / `message.reasoning_content` fields instead of
inline `<think>` tags. The existing `_thinking_exhausted` detection
only checked for inline think tags, so these responses fell through
to 3 useless continuation retries + 2 prefills before returning
"(empty)" to the user.

This patch:
1. Extends `_thinking_exhausted` to also trigger when structured
   reasoning is present but content is empty (no think tags needed).
2. When exhaustion is detected via structured reasoning (not think
   tags), appends a 600-char preview of the captured reasoning to
   the user-facing error message, so the user sees what the model
   was thinking instead of getting a blank response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/ollama Ollama / local models labels May 18, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related: #9452 — also addresses structured reasoning budget exhaustion bypass. #9452 is still open; this PR covers the same _thinking_exhausted detection gap with a slightly different approach (adds reasoning preview in error message). Consider consolidating.

@outsourc-e outsourc-e left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structured-reasoning exhaustion detection itself makes sense, but I’m uneasy about appending raw message.reasoning / reasoning_content back into the user-facing error. That can surface internal scratchpad / provider reasoning text directly to end users, which is a pretty different policy choice than just detecting exhaustion. I’d be more comfortable either (a) landing the detection without the preview, or (b) gating the preview behind an explicit debug/developer mode.

@teknium1

Copy link
Copy Markdown
Contributor

Closing — the bug you're chasing is real (Ollama qwen3 returns reasoning in message.reasoning not <think> tags, and the exhaustion detector doesn't catch it), but the fix as proposed doesn't quite land it correctly, and we want to surface the shape gap rather than merge and untangle later.

The detector signature is wrong. _thinking_exhausted is gated on not _trunc_has_tool_calls AND (think_tags_branch OR structured_reasoning_branch). The structured-reasoning branch tests _has_structured_reasoning AND _content_is_empty. But _has_structured_reasoning is True for every reasoning model on every turn that produced reasoning, not just exhausted ones. Combined with _content_is_empty (content empty), the branch will fire whenever a reasoning model returns no visible text — including legitimate non-exhausted cases:

  • Tool-call turns where the model produced reasoning + a tool call (the model isn't talking to the user, it's invoking a tool — empty content is correct, NOT exhaustion). The not _trunc_has_tool_calls outer gate catches most of these but not all (depends on how the structured response is normalized for the truncation check).
  • Refusal / safety filter responses that come back with reasoning but no content.
  • Provider quirks where reasoning is present but content arrives in a later chunk that hasn't been concatenated yet.

The inline <think> tag branch doesn't have this problem because the regex requires the tags to be in _trunc_content — so absence of content + presence of think tags is a tight signal. Structured reasoning is in a different field, so "reasoning present + content empty" is a much looser signal that doesn't distinguish "model ran out of tokens" from "model is fine but produced no visible text this turn."

The right fix needs an actual exhaustion signal — finish_reason == 'length' on the truncated chunk, or the continuation loop's own retry-budget counter — combined with structured-reasoning presence. Just the conjunction "has reasoning + content empty" is too broad.

The 600-char reasoning preview is a separate good idea worth keeping, but it should be gated on the exhaustion signal once that's correct.

If you'd like to take another pass with finish_reason='length' as the gate (or another tight signal), happy to look. The current shape would fire false-positive "Thinking Budget Exhausted" messages on normal turns, which is a worse UX than the original missed-detection bug. Thanks for the work.

@teknium1 teknium1 closed this May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/ollama Ollama / local models type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants