From 5a33751b0de978aa62a3f978b05abcb702a7a103 Mon Sep 17 00:00:00 2001 From: HumphreySun98 Date: Wed, 8 Jul 2026 15:34:22 -0500 Subject: [PATCH] fix(tools): raise ToolUsageError instead of bare raise on non-dict args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _original_tool_calling had a bare `raise` on the `not isinstance(arguments, dict)` path, which is outside the preceding try/except — so with raise_error=True it fails with "RuntimeError: No active exception to re-raise" instead of surfacing the intended tool-arguments error. Raise the ToolUsageError explicitly. Fixes #6430 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/crewai/src/crewai/tools/tool_usage.py | 8 +++-- lib/crewai/tests/tools/test_tool_usage.py | 42 ++++++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/lib/crewai/src/crewai/tools/tool_usage.py b/lib/crewai/src/crewai/tools/tool_usage.py index e92ba03eed..147e8fa1a2 100644 --- a/lib/crewai/src/crewai/tools/tool_usage.py +++ b/lib/crewai/src/crewai/tools/tool_usage.py @@ -849,9 +849,13 @@ def _original_tool_calling( return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}") if not isinstance(arguments, dict): + error = ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}") + # A bare ``raise`` here would fail with "No active exception to + # re-raise" (this path is outside the except block above), so raise + # the intended ToolUsageError explicitly. if raise_error: - raise - return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}") + raise error + return error return ToolCalling( tool_name=sanitize_tool_name(tool.name), diff --git a/lib/crewai/tests/tools/test_tool_usage.py b/lib/crewai/tests/tools/test_tool_usage.py index 9d61c93a9b..c24acca050 100644 --- a/lib/crewai/tests/tools/test_tool_usage.py +++ b/lib/crewai/tests/tools/test_tool_usage.py @@ -25,7 +25,7 @@ ) from crewai.tools import BaseTool from crewai.tools.tool_calling import ToolCalling -from crewai.tools.tool_usage import ToolUsage +from crewai.tools.tool_usage import ToolUsage, ToolUsageError from crewai.utilities.tool_utils import execute_tool_and_check_finality from pydantic import BaseModel, Field import pytest @@ -261,6 +261,46 @@ def test_last_raw_result_falls_back_only_until_recorded(): assert tool_usage.get_last_raw_result("formatted result") is None +def _tool_usage_for_calling() -> ToolUsage: + return ToolUsage( + tools_handler=MagicMock(), + tools=[], + task=MagicMock(), + function_calling_llm=MagicMock(), + agent=MagicMock(), + action=MagicMock(), + ) + + +def test_original_tool_calling_non_dict_raises_tool_usage_error(): + """When validated arguments are not a dict and raise_error=True, a + ToolUsageError must be raised — not a RuntimeError from a bare `raise` + with no active exception (#6430).""" + tool_usage = _tool_usage_for_calling() + + with ( + patch.object(tool_usage, "_select_tool", return_value=MagicMock()), + patch.object(tool_usage, "_validate_tool_input", return_value=[1, 2, 3]), + ): + with pytest.raises(ToolUsageError): + tool_usage._original_tool_calling("some tool string", raise_error=True) + + +def test_original_tool_calling_non_dict_returns_error_when_not_raising(): + """With raise_error=False, non-dict arguments return a ToolUsageError.""" + tool_usage = _tool_usage_for_calling() + + with ( + patch.object(tool_usage, "_select_tool", return_value=MagicMock()), + patch.object(tool_usage, "_validate_tool_input", return_value=[1, 2, 3]), + ): + result = tool_usage._original_tool_calling( + "some tool string", raise_error=False + ) + + assert isinstance(result, ToolUsageError) + + def test_validate_tool_input_booleans_and_none(): tool_usage = ToolUsage( tools_handler=MagicMock(),