Skip to content

[BUG] Reasoning content is lost when model returns both reasoning and text in the same response #1394

@joelrobin18

Description

@joelrobin18

Checks

  • I have updated to the lastest minor and patch version of Strands
  • I have checked the documentation and this is not expected behavior
  • I have searched ./issues and there are no duplicates of my issue

Strands Version

1.20.0

Python Version

3.10

Operating System

macOS 14.x / Linux

Installation Method

pip

Steps to Reproduce

import mlflow
from strands import Agent
from strands.models.gemini import GeminiModel
import os

# Gemini model with thinking/reasoning enabled
model = GeminiModel(
    client_args={
        "api_key": os.getenv("GOOGLE_API_KEY"),
    },
    model_id="gemini-2.5-flash",
    params={
        "thinking_config": {
            "thinking_budget": 1024,
            "include_thoughts": True,
        },
    },
)

agent = Agent(model=model)

# Simple query that triggers reasoning + text response (no tool calls)
response = agent("What is 2+2")
print(response)
last_message = agent.messages[-1]
print(json.dumps(last_message["content"], indent=2, default=str))

Output:

I've determined the user is asking a simple arithmetic question and I'm ready to supply the accurate solution. I am formulating the simplest and clearest response possible.


2 + 2 = 42 + 2 = 4

[
  {
    "text": "2 + 2 = 4"
  }
]

Install Strands using pip install strands-agents strands-agents-tools
Run the script above with a valid GOOGLE_API_KEY

The reasoning content is not captured here. Here we can see that gemini uses reasoning and in the messages section, we dont see the reasoning. The reasoning content is printed to console but NOT captured in the trace/message content

Expected Behavior

When a model returns both reasoning content (thinking/extended thinking) AND text content in the same response, both should be captured in the message content array:

**Calculating the Answer**

I've got a basic arithmetic question to answer. My focus now is on delivering the correct response without any fuss. The goal is clarity and accuracy in this simple calculation.


2 + 2 = 42 + 2 = 4

[
  {
    "reasoningContent": {
      "reasoningText": {
        "text": "**Calculating the Answer**\n\nI've got a basic arithmetic question to answer. My focus now is on delivering the correct response without any fuss. The goal is clarity and accuracy in this simple calculation.\n\n\n"
      }
    }
  },
  {
    "text": "2 + 2 = 4"
  }
]

The reasoning content should be available for:

  • Tracing/observability tools
  • Conversation history
  • Any downstream processing

Actual Behavior

When a model returns both reasoning and text in the same response (without tool calls), only the text is captured:

message["content"] = [
    {"text": "2 + 2 = 4"}
]
# reasoning content is LOST!

The reasoning content is printed to the console (via callback handler during streaming), but it's discarded when building the final message content.

Note: This bug does NOT occur when:

  • There are tool calls (reasoning is captured correctly alongside tool_use)
  • There's only reasoning content (no text)

The bug specifically affects responses with both reasoning AND text but NO tool calls.

Additional Context

Root Cause Analysis

The issue is in strands/event_loop/streaming.py in the handle_content_block_stop function (around line 271-316):

def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]:
    # ...
    if current_tool_use:
        # handle tool use...
    elif text:                    # ← If text exists, ONLY text is added
        content.append({"text": text})
        state["text"] = ""
    elif reasoning_text:          # ← SKIPPED if text was already added!
        content.append(content_block)
        state["reasoningText"] = ""

The elif chain causes reasoning content to be skipped when text exists. This is problematic because:

  1. Gemini (and other models with thinking capabilities) can return reasoning + text in a single content block
  2. There's only ONE content_stop event for both
  3. When content_stop is called, both state["text"] and state["reasoningText"] may be populated
  4. The elif logic only adds one of them

Trace Evidence

Trace with tool calls (reasoning IS captured):

"outputs": [
  {"toolUse": {"toolUseId": "...", "name": "...", "input": {}}},
  {"reasoningContent": {"reasoningText": {"text": "Thinking..."}}}
]

Trace without tool calls (reasoning is LOST):

"outputs": [
  {"text": "2 + 2 = 4"}
]
// No reasoningContent even though it was streamed!

Possible Solution

Change the elif to independent if statements so both reasoning and text can be captured:

def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]:
    # ...
    if current_tool_use:
        # handle tool use (unchanged)
        content.append({"toolUse": tool_use})
        state["current_tool_use"] = {}

    # Handle reasoning content - must be checked independently of text
    if reasoning_text:
        content_block: ContentBlock = {
            "reasoningContent": {
                "reasoningText": {
                    "text": state["reasoningText"],
                }
            }
        }
        if "signature" in state:
            content_block["reasoningContent"]["reasoningText"]["signature"] = state["signature"]
        content.append(content_block)
        state["reasoningText"] = ""
    elif redacted_content:
        content.append({"reasoningContent": {"redactedContent": redacted_content}})
        state["redactedContent"] = b""

    # Handle text content - checked after reasoning so both can be captured
    if text:
        content.append({"text": text})
        state["text"] = ""
        if citations_content:
            citations_block: CitationsContentBlock = {"citations": citations_content}
            content.append({"citationsContent": citations_block})
            state["citationsContent"] = []

    return state

This ensures:

  1. Tool use is handled first (with early return via original if)
  2. Reasoning content is checked and added if present
  3. Text content is checked and added if present
  4. Both can coexist in the same response

Related Issues

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions