Skip to content

fix(llm): prevent text response from overriding native tool calls#4806

Open
mvanhorn wants to merge 3 commits intocrewAIInc:mainfrom
mvanhorn:fix/tool-calls-text-override
Open

fix(llm): prevent text response from overriding native tool calls#4806
mvanhorn wants to merge 3 commits intocrewAIInc:mainfrom
mvanhorn:fix/tool-calls-text-override

Conversation

@mvanhorn
Copy link

@mvanhorn mvanhorn commented Mar 11, 2026

Summary

  • When the LLM returns both a text response and tool_calls, the executor passes available_functions=None. The existing condition (not tool_calls or not available_functions) and text_response at step 5 matched because not available_functions was True, so the text response was returned and tool calls were silently discarded.
  • Fix: reorder the checks so that "tool_calls present but no available_functions" is evaluated before the text-return path. This ensures tool calls are returned to the caller (executor) for handling.
  • No new dependencies or breaking changes.

Test plan

  • Verify with an LLM that returns both text and tool_calls when available_functions=None -- tool calls should be returned, not text
  • Verify that when only text is returned (no tool_calls), behavior is unchanged
  • Verify that when available_functions is provided, tool execution still works normally

Fixes #4788

🤖 Generated with Claude Code


Note

Medium Risk
Changes LLM non-streaming return-path precedence so responses with tool_calls no longer fall back to text when available_functions is None, which may alter what downstream callers receive in this edge case.

Overview
Prevents native tool_calls from being silently dropped when the LLM returns both text and tool calls but available_functions is not provided.

Reorders the non-streaming and async non-streaming response handling to return tool_calls first (and emit LLMCallType.TOOL_CALL completion events) before the text-return path, while keeping existing behavior when there are no tool calls or when tool execution is available.

Written by Cursor Bugbot for commit 3398561. This will update automatically on new commits. Configure here.

When the LLM returns both text and tool_calls, the executor passes
available_functions=None. The condition `not available_functions` was
True, so the text-return path matched first and tool calls were silently
discarded. Reorder the checks so tool_calls are returned before the
text-return path when available_functions is None.

Fixes crewAIInc#4788

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@alvinttang alvinttang left a comment

Choose a reason for hiding this comment

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

The ordering fix is correct: by checking "tool_calls and not available_functions" before the text-return path, native tool calls are no longer silently discarded when the LLM returns both a text response and tool calls in the same message. This is a subtle but important behavior difference — many LLMs (especially Claude) routinely include a preamble text alongside tool calls. One concern: the new early-return path returns tool_calls without emitting any call events via _handle_emit_call_events, whereas the old code path did emit them before returning text. Verify that callers handling the returned tool_calls list are responsible for their own event emission, otherwise observability/tracing could silently drop tool call spans.

…ents

Address review feedback:
- Reorder async _ahandle_non_streaming_response to check tool_calls
  before text-return path, matching the sync method fix
- Emit TOOL_CALL events via _handle_emit_call_events when returning
  raw tool_calls in both sync and async paths
@mvanhorn
Copy link
Author

Addressed both issues in c01d4e3:

  1. Async path reordering - _ahandle_non_streaming_response now checks tool_calls and not available_functions before the text-return path, matching the sync method. Good catch, @cursor[bot].

  2. Event emission - Both sync and async paths now emit TOOL_CALL events via _handle_emit_call_events before returning raw tool calls. This ensures monitoring and callbacks still fire when the caller handles tool execution externally. @alvinttang - valid point, thanks.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

After the early return for tool_calls-without-available_functions,
the `not available_functions` sub-condition is dead code. Simplify
to `not tool_calls and text_response` in both sync and async paths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Native tool calls are discarded if LLM returns a text response

2 participants