From dd66f648c263eefb2ca2b6f3d80df00c556be6d9 Mon Sep 17 00:00:00 2001 From: keaixiaozhu Date: Fri, 3 Jul 2026 14:12:53 +0800 Subject: [PATCH 1/2] fix(tools): raise ToolUsageError instead of bare raise in _original_tool_calling ToolUsage._original_tool_calling executed a bare `raise` outside any except block when _validate_tool_input returned a non-dict and raise_error=True. With no active exception this raised `RuntimeError: No active exception to re-raise` instead of a meaningful error. Raise a constructed ToolUsageError, consistent with the raise_error=False return path. The sole caller (_tool_calling) still catches it via `except Exception` and falls back to function-calling, so behavior is backward compatible. --- lib/crewai/src/crewai/tools/tool_usage.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/crewai/src/crewai/tools/tool_usage.py b/lib/crewai/src/crewai/tools/tool_usage.py index e92ba03eed..ff681de7a9 100644 --- a/lib/crewai/src/crewai/tools/tool_usage.py +++ b/lib/crewai/src/crewai/tools/tool_usage.py @@ -849,9 +849,12 @@ def _original_tool_calling( return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}") if not isinstance(arguments, dict): + tool_arguments_error = ToolUsageError( + f"{I18N_DEFAULT.errors('tool_arguments_error')}" + ) if raise_error: - raise - return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}") + raise tool_arguments_error + return tool_arguments_error return ToolCalling( tool_name=sanitize_tool_name(tool.name), From f4c3aecb012fae876b1a168ba2adb48c9cb7a45e Mon Sep 17 00:00:00 2001 From: keaixiaozhu Date: Fri, 3 Jul 2026 14:12:54 +0800 Subject: [PATCH 2/2] test(tools): cover _original_tool_calling non-dict error path Add regression tests for _original_tool_calling: happy path returns ToolCalling; non-dict with raise_error=True raises ToolUsageError (not RuntimeError); non-dict with raise_error=False returns ToolUsageError. Mirrors the patch.object style of test_tool_validate_input_error_event. --- lib/crewai/tests/tools/test_tool_usage.py | 65 ++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/crewai/tests/tools/test_tool_usage.py b/lib/crewai/tests/tools/test_tool_usage.py index 9d61c93a9b..23c453e656 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 @@ -689,6 +689,69 @@ def event_handler(source, event): assert "must be a valid dictionary" in event.error +def _build_tool_usage_for_original_calling() -> ToolUsage: + """Build a minimal ToolUsage instance for exercising _original_tool_calling.""" + mock_agent = MagicMock() + mock_agent.key = "test_key" + mock_agent.role = "test_role" + mock_agent.verbose = False + + class TestTool(BaseTool): + name: str = "Test Tool" + description: str = "A test tool" + + def _run(self, input: dict) -> str: + return "test result" + + return ToolUsage( + tools_handler=MagicMock(), + tools=[TestTool()], + task=MagicMock(), + function_calling_llm=None, + agent=mock_agent, + action=MagicMock(tool="test_tool", tool_input='{"key": "value"}'), + ) + + +def test_original_tool_calling_valid_dict_returns_tool_calling(): + """A valid dict from _validate_tool_input yields a ToolCalling result.""" + tool_usage = _build_tool_usage_for_original_calling() + + result = tool_usage._original_tool_calling("tool_string", raise_error=False) + + assert isinstance(result, ToolCalling) + assert result.tool_name == "test_tool" + assert result.arguments == {"key": "value"} + + +def test_original_tool_calling_non_dict_raises_tool_usage_error(): + """Non-dict validation result with raise_error=True raises ToolUsageError. + + Regression test: previously this path used a bare ``raise`` outside any + ``except`` block, producing ``RuntimeError: No active exception to re-raise`` + instead of a meaningful error. + """ + tool_usage = _build_tool_usage_for_original_calling() + + with patch.object( + ToolUsage, "_validate_tool_input", return_value=["not", "a", "dict"] + ): + with pytest.raises(ToolUsageError): + tool_usage._original_tool_calling("tool_string", raise_error=True) + + +def test_original_tool_calling_non_dict_returns_error_when_not_raising(): + """Non-dict validation result with raise_error=False returns a ToolUsageError.""" + tool_usage = _build_tool_usage_for_original_calling() + + with patch.object( + ToolUsage, "_validate_tool_input", return_value=["not", "a", "dict"] + ): + result = tool_usage._original_tool_calling("tool_string", raise_error=False) + + assert isinstance(result, ToolUsageError) + + def test_tool_usage_finished_event_with_result(): """Test that ToolUsageFinishedEvent is emitted with correct result attributes.""" mock_agent = MagicMock()