Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions lib/crewai/src/crewai/agents/crew_agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,10 @@ def _invoke_loop_react(self) -> AgentFinish:
)

self._invoke_step_callback(formatted_answer)
self._append_message(formatted_answer.text)
self._append_message(
formatted_answer.text,
reasoning_content=self._get_llm_reasoning_content(),
)

except OutputParserError as e:
formatted_answer = handle_output_parser_exception( # type: ignore[assignment]
Expand Down Expand Up @@ -525,8 +528,9 @@ def _invoke_loop_native_tools(self) -> AgentFinish:
output=answer,
text=answer,
)
reasoning = self._get_llm_reasoning_content()
self._invoke_step_callback(formatted_answer)
self._append_message(answer)
self._append_message(answer, reasoning_content=reasoning)
self._show_logs(formatted_answer)
return formatted_answer

Expand All @@ -537,8 +541,9 @@ def _invoke_loop_native_tools(self) -> AgentFinish:
output=answer,
text=output_json,
)
reasoning = self._get_llm_reasoning_content()
self._invoke_step_callback(formatted_answer)
self._append_message(output_json)
self._append_message(output_json, reasoning_content=reasoning)
self._show_logs(formatted_answer)
return formatted_answer

Expand All @@ -547,8 +552,9 @@ def _invoke_loop_native_tools(self) -> AgentFinish:
output=str(answer),
text=str(answer),
)
reasoning = self._get_llm_reasoning_content()
self._invoke_step_callback(formatted_answer)
self._append_message(str(answer))
self._append_message(str(answer), reasoning_content=reasoning)
self._show_logs(formatted_answer)
return formatted_answer

Expand Down Expand Up @@ -1234,7 +1240,10 @@ async def _ainvoke_loop_react(self) -> AgentFinish:
)

await self._ainvoke_step_callback(formatted_answer)
self._append_message(formatted_answer.text)
self._append_message(
formatted_answer.text,
reasoning_content=self._get_llm_reasoning_content(),
)

except OutputParserError as e:
formatted_answer = handle_output_parser_exception( # type: ignore[assignment]
Expand Down Expand Up @@ -1336,8 +1345,9 @@ async def _ainvoke_loop_native_tools(self) -> AgentFinish:
output=answer,
text=answer,
)
reasoning = self._get_llm_reasoning_content()
await self._ainvoke_step_callback(formatted_answer)
self._append_message(answer)
self._append_message(answer, reasoning_content=reasoning)
self._show_logs(formatted_answer)
return formatted_answer

Expand All @@ -1348,8 +1358,9 @@ async def _ainvoke_loop_native_tools(self) -> AgentFinish:
output=answer,
text=output_json,
)
reasoning = self._get_llm_reasoning_content()
await self._ainvoke_step_callback(formatted_answer)
self._append_message(output_json)
self._append_message(output_json, reasoning_content=reasoning)
self._show_logs(formatted_answer)
return formatted_answer

Expand All @@ -1358,8 +1369,9 @@ async def _ainvoke_loop_native_tools(self) -> AgentFinish:
output=str(answer),
text=str(answer),
)
reasoning = self._get_llm_reasoning_content()
await self._ainvoke_step_callback(formatted_answer)
self._append_message(str(answer))
self._append_message(str(answer), reasoning_content=reasoning)
self._show_logs(formatted_answer)
return formatted_answer

Expand Down Expand Up @@ -1473,16 +1485,26 @@ async def _ainvoke_step_callback(
if inspect.iscoroutine(cb_result):
await cb_result

def _get_llm_reasoning_content(self) -> str | None:
"""Return reasoning_content from the last LLM response, if any."""
return getattr(self.llm, "reasoning_content", None)

def _append_message(
self, text: str, role: Literal["user", "assistant", "system"] = "assistant"
self,
text: str,
role: Literal["user", "assistant", "system"] = "assistant",
reasoning_content: str | None = None,
) -> None:
"""Add message to conversation history.

Args:
text: Message content.
role: Message role (default: assistant).
reasoning_content: Optional reasoning content from the LLM response.
"""
self.messages.append(format_message_for_llm(text, role=role))
self.messages.append(
format_message_for_llm(text, role=role, reasoning_content=reasoning_content)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _show_start_logs(self) -> None:
"""Emit agent start event."""
Expand Down
24 changes: 19 additions & 5 deletions lib/crewai/src/crewai/experimental/agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,14 +1330,18 @@ def call_llm_native_tools(
self.state.pending_tool_calls = list(answer)
return "native_tool_calls"

reasoning = self._get_llm_reasoning_content()

if isinstance(answer, BaseModel):
self.state.current_answer = AgentFinish(
thought="",
output=answer,
text=answer.model_dump_json(),
)
self._invoke_step_callback(self.state.current_answer)
self._append_message_to_state(answer.model_dump_json())
self._append_message_to_state(
answer.model_dump_json(), reasoning_content=reasoning
)
Comment on lines +1333 to +1344
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tool-call responses still lose reasoning_content in native flow.

This propagation only covers final-answer branches. If the LLM returns tool calls, routing exits before using reasoning_content, and the assistant tool-call message later added to state does not include it.

Suggested fix
@@
-            # Check if the response is a list of tool calls
+            reasoning = self._get_llm_reasoning_content()
+
+            # Check if the response is a list of tool calls
             if isinstance(answer, list) and answer and self._is_tool_call_list(answer):
                 # Store tool calls for sequential processing
                 self.state.pending_tool_calls = list(answer)
                 return "native_tool_calls"
-
-            reasoning = self._get_llm_reasoning_content()
@@
         if tool_calls_to_report:
             assistant_message: LLMMessage = {
                 "role": "assistant",
                 "content": None,
                 "tool_calls": tool_calls_to_report,
             }
+            reasoning = self._get_llm_reasoning_content()
+            if reasoning is not None:
+                assistant_message["reasoning_content"] = reasoning
             if all(type(tc).__qualname__ == "Part" for tc in pending_tool_calls):
                 assistant_message["raw_tool_call_parts"] = list(pending_tool_calls)
             self.state.messages.append(assistant_message)

Also applies to: 1355-1367

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/experimental/agent_executor.py` around lines 1333 -
1344, Final-answer branch only sets reasoning_content; tool-call branches skip
it causing loss of reasoning. Update the branches that handle LLM tool calls so
that when creating and appending the assistant tool-call message and when
setting state for tool-invocation you also call _get_llm_reasoning_content() and
pass that reasoning_content into _append_message_to_state (and any
AgentAction/assistant tool-call constructors) just like the AgentFinish path
does; look for usages in methods around _get_llm_reasoning_content,
_append_message_to_state, AgentFinish and AgentAction/assistant tool-call
creation and ensure every path that adds an assistant/tool message forwards the
reasoning_content parameter.

return self._route_finish_with_todos("native_finished")

# Text response - this is the final answer
Expand All @@ -1348,7 +1352,7 @@ def call_llm_native_tools(
text=answer,
)
self._invoke_step_callback(self.state.current_answer)
self._append_message_to_state(answer)
self._append_message_to_state(answer, reasoning_content=reasoning)

return self._route_finish_with_todos("native_finished")

Expand All @@ -1359,7 +1363,7 @@ def call_llm_native_tools(
text=str(answer),
)
self._invoke_step_callback(self.state.current_answer)
self._append_message_to_state(str(answer))
self._append_message_to_state(str(answer), reasoning_content=reasoning)

return self._route_finish_with_todos("native_finished")

Expand Down Expand Up @@ -2813,16 +2817,26 @@ def _handle_step_callback_task_result(self, task: asyncio.Task[Any]) -> None:
color="red",
)

def _get_llm_reasoning_content(self) -> str | None:
"""Return reasoning_content from the last LLM response, if any."""
return getattr(self.llm, "reasoning_content", None)

def _append_message_to_state(
self, text: str, role: Literal["user", "assistant", "system"] = "assistant"
self,
text: str,
role: Literal["user", "assistant", "system"] = "assistant",
reasoning_content: str | None = None,
) -> None:
"""Add message to state conversation history.

Args:
text: Message content.
role: Message role (default: assistant).
reasoning_content: Optional reasoning content from the LLM response.
"""
self.state.messages.append(format_message_for_llm(text, role=role))
self.state.messages.append(
format_message_for_llm(text, role=role, reasoning_content=reasoning_content)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _show_start_logs(self) -> None:
"""Emit agent start event."""
Expand Down
10 changes: 10 additions & 0 deletions lib/crewai/src/crewai/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,15 @@ def _handle_non_streaming_response(
0
].message
text_response = response_message.content or ""

# Store reasoning_content for models that return it (e.g. DeepSeek thinking mode)
self.reasoning_content: str | None = getattr(
response_message, "reasoning_content", None
) or (
response_message.get("reasoning_content")
if hasattr(response_message, "get")
else None
)
Comment on lines +1236 to +1243
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Async path is missing reasoning_content reset/extraction parity.

Line 1754 resets state only in call(), and Lines 1236-1243 extract only in sync non-streaming. acall() / _ahandle_non_streaming_response() currently don’t mirror this, so async flows can miss new reasoning_content or carry stale values across turns.

Suggested parity patch
@@
     async def _ahandle_non_streaming_response(
@@
         response_message = cast(Choices, cast(ModelResponse, response).choices)[
             0
         ].message
         text_response = response_message.content or ""
+
+        # Store reasoning_content for models that return it (e.g. DeepSeek thinking mode)
+        self.reasoning_content = getattr(
+            response_message, "reasoning_content", None
+        ) or (
+            response_message.get("reasoning_content")
+            if hasattr(response_message, "get")
+            else None
+        )
@@
     async def acall(
@@
-        with llm_call_context() as call_id:
+        self.reasoning_content: str | None = None
+        with llm_call_context() as call_id:

Also applies to: 1754-1754

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/llm.py` around lines 1236 - 1243, The async code path
doesn't reset or extract reasoning_content like the sync path does, causing
stale values; update acall() and _ahandle_non_streaming_response() to mirror the
sync logic used in call() by resetting self.reasoning_content before the call
and extracting reasoning_content from response_message (using
getattr(response_message, "reasoning_content", None) or
response_message.get("reasoning_content") if it has get) after receiving the
non-streaming response so async flows receive the same reasoning_content
handling as sync flows.

# --- 3) Handle callbacks with usage info
if callbacks and len(callbacks) > 0:
for callback in callbacks:
Expand Down Expand Up @@ -1742,6 +1751,7 @@ def call(
ValueError: If response format is not supported
LLMContextLengthExceededError: If input exceeds model's context limit
"""
self.reasoning_content = None
with llm_call_context() as call_id:
crewai_event_bus.emit(
self,
Expand Down
12 changes: 10 additions & 2 deletions lib/crewai/src/crewai/utilities/agent_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,20 +347,28 @@ def handle_max_iterations_exceeded(


def format_message_for_llm(
prompt: str, role: Literal["user", "assistant", "system"] = "user"
prompt: str,
role: Literal["user", "assistant", "system"] = "user",
reasoning_content: str | None = None,
) -> LLMMessage:
"""Format a message for the LLM.

Args:
prompt: The message content.
role: The role of the message sender, either 'user' or 'assistant'.
reasoning_content: Optional reasoning content for assistant messages
(e.g. from DeepSeek thinking mode). Only included when role is
'assistant' and the value is non-empty.

Returns:
A dictionary with 'role' and 'content' keys.

"""
prompt = prompt.rstrip()
return {"role": role, "content": prompt}
msg: LLMMessage = {"role": role, "content": prompt}
if reasoning_content and role == "assistant":
msg["reasoning_content"] = reasoning_content
return msg


def format_answer(answer: str) -> AgentAction | AgentFinish:
Expand Down
1 change: 1 addition & 0 deletions lib/crewai/src/crewai/utilities/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ class LLMMessage(TypedDict):
name: NotRequired[str]
tool_calls: NotRequired[list[dict[str, Any]]]
raw_tool_call_parts: NotRequired[list[Any]]
reasoning_content: NotRequired[str | None]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix ruff formatting before merge (CI is failing).

Pipeline is failing on uv run ruff format --check lib/. Please run formatter and commit the result to unblock merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/utilities/types.py` at line 30, The file contains a
ruff formatting violation around the type declaration reasoning_content:
NotRequired[str | None]; run the ruff formatter (uv run ruff format) to reformat
the code and commit the resulting changes so CI passes, ensuring the declaration
for reasoning_content and surrounding imports/whitespace follow the project's
ruff style rules.

files: NotRequired[dict[str, FileInput]]
Loading
Loading