-
Notifications
You must be signed in to change notification settings - Fork 577
Description
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:
- Gemini (and other models with thinking capabilities) can return reasoning + text in a single content block
- There's only ONE
content_stopevent for both - When
content_stopis called, bothstate["text"]andstate["reasoningText"]may be populated - The
eliflogic 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 stateThis ensures:
- Tool use is handled first (with early return via original
if) - Reasoning content is checked and added if present
- Text content is checked and added if present
- Both can coexist in the same response
Related Issues
No response