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
74 changes: 66 additions & 8 deletions lib/crewai/src/crewai/agents/crew_agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,11 @@ def _invoke_loop_native_tools(self) -> AgentFinish:

enforce_rpm_limit(self.request_within_rpm_limit)

# Suppress response_model while tools are active: providers such as
# Gemini and Anthropic treat response_format (structured output) as
# higher priority than tools, causing tool calls to be skipped when
# both are passed together. response_model is applied after the tool
# loop completes via a final extraction call below.
answer = get_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
Expand All @@ -502,7 +507,7 @@ def _invoke_loop_native_tools(self) -> AgentFinish:
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=None,
executor_context=self,
verbose=self.agent.verbose,
)
Expand All @@ -520,13 +525,39 @@ def _invoke_loop_native_tools(self) -> AgentFinish:
continue

if isinstance(answer, str):
# Tool loop is done. If structured output is required, make one
# final call without tools so the provider can apply the schema.
if self.response_model is not None:
answer = get_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
callbacks=self.callbacks,
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)
if isinstance(answer, BaseModel):
output_json = answer.model_dump_json()
formatted_answer = AgentFinish(
thought="",
output=answer,
text=output_json,
)
self._invoke_step_callback(formatted_answer)
self._append_message(output_json)
self._show_logs(formatted_answer)
return formatted_answer
answer_str = str(answer) if not isinstance(answer, str) else answer
formatted_answer = AgentFinish(
thought="",
output=answer,
text=answer,
output=answer_str,
text=answer_str,
)
self._invoke_step_callback(formatted_answer)
self._append_message(answer)
self._append_message(answer_str)
self._show_logs(formatted_answer)
return formatted_answer

Expand Down Expand Up @@ -1305,6 +1336,8 @@ async def _ainvoke_loop_native_tools(self) -> AgentFinish:

enforce_rpm_limit(self.request_within_rpm_limit)

# Suppress response_model while tools are active (mirrors sync fix).
# See _invoke_loop_native_tools for the full explanation.
answer = await aget_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
Expand All @@ -1314,7 +1347,7 @@ async def _ainvoke_loop_native_tools(self) -> AgentFinish:
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=None,
executor_context=self,
verbose=self.agent.verbose,
)
Expand All @@ -1331,13 +1364,38 @@ async def _ainvoke_loop_native_tools(self) -> AgentFinish:
continue

if isinstance(answer, str):
if self.response_model is not None:
enforce_rpm_limit(self.request_within_rpm_limit)
answer = await aget_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
callbacks=self.callbacks,
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)
if isinstance(answer, BaseModel):
output_json = answer.model_dump_json()
formatted_answer = AgentFinish(
thought="",
output=answer,
text=output_json,
)
await self._ainvoke_step_callback(formatted_answer)
self._append_message(output_json)
self._show_logs(formatted_answer)
return formatted_answer
answer_str = str(answer) if not isinstance(answer, str) else answer
formatted_answer = AgentFinish(
thought="",
output=answer,
text=answer,
output=answer_str,
text=answer_str,
)
await self._ainvoke_step_callback(formatted_answer)
self._append_message(answer)
self._append_message(answer_str)
self._show_logs(formatted_answer)
return formatted_answer

Expand Down
40 changes: 33 additions & 7 deletions lib/crewai/src/crewai/experimental/agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1309,7 +1309,11 @@ def call_llm_native_tools(

enforce_rpm_limit(self.request_within_rpm_limit)

# Call LLM with native tools
# Call LLM with native tools. Suppress response_model here so that
# providers like Gemini and Anthropic don't skip tool calls in favour
# of returning structured output immediately. response_model is applied
# in a separate extraction call once the tool loop produces a text answer,
# consistent with how the Plan-and-Execute path handles it (synthesis step).
answer = get_llm_response(
llm=self.llm,
messages=list(self.state.messages),
Expand All @@ -1319,7 +1323,7 @@ def call_llm_native_tools(
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=None,
executor_context=self,
verbose=self.agent.verbose,
)
Expand All @@ -1340,16 +1344,38 @@ def call_llm_native_tools(
self._append_message_to_state(answer.model_dump_json())
return self._route_finish_with_todos("native_finished")

# Text response - this is the final answer
# Text response - tool loop is done. Apply response_model now if set.
if isinstance(answer, str):
if self.response_model is not None:
enforce_rpm_limit(self.request_within_rpm_limit)
answer = get_llm_response(
llm=self.llm,
messages=list(self.state.messages),
callbacks=self.callbacks,
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)
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())
return self._route_finish_with_todos("native_finished")
answer_str = str(answer) if not isinstance(answer, str) else answer
self.state.current_answer = AgentFinish(
thought="",
output=answer,
text=answer,
output=answer_str,
text=answer_str,
)
self._invoke_step_callback(self.state.current_answer)
self._append_message_to_state(answer)

self._append_message_to_state(answer_str)
return self._route_finish_with_todos("native_finished")

# Unexpected response type, treat as final answer
Expand Down