diff --git a/lib/crewai/src/crewai/tools/structured_tool.py b/lib/crewai/src/crewai/tools/structured_tool.py index c7191de40c..41f1da4e21 100644 --- a/lib/crewai/src/crewai/tools/structured_tool.py +++ b/lib/crewai/src/crewai/tools/structured_tool.py @@ -407,9 +407,16 @@ async def ainvoke( return await self.func(**parsed_args, **kwargs) import asyncio - return await asyncio.get_event_loop().run_in_executor( + result = await asyncio.get_event_loop().run_in_executor( None, lambda: self.func(**parsed_args, **kwargs) ) + # A sync wrapper around an async tool (e.g. BaseTool.to_structured_tool + # storing Tool._run as func) hands a bare coroutine back from the + # executor; awaiting it here keeps the caller from receiving the + # coroutine object itself. + if inspect.isawaitable(result): + return await result + return result except Exception: raise diff --git a/lib/crewai/tests/tools/test_structured_tool.py b/lib/crewai/tests/tools/test_structured_tool.py index 0241abbcff..9a911a2fab 100644 --- a/lib/crewai/tests/tools/test_structured_tool.py +++ b/lib/crewai/tests/tools/test_structured_tool.py @@ -519,3 +519,42 @@ def failing_function(should_fail: bool) -> str: tool.invoke({"should_fail": True}) assert call_count == 1 + + +@pytest.mark.asyncio +async def test_ainvoke_awaits_coroutine_from_sync_wrapper(): + """ainvoke must await a coroutine handed back by a sync wrapper function.""" + calls: list[str] = [] + + async def async_body(q: str) -> str: + calls.append(q) + return f"data:{q}" + + def sync_wrapper(q: str) -> object: + """Sync wrapper that returns the coroutine from async_body.""" + return async_body(q) + + tool = CrewStructuredTool.from_function(func=sync_wrapper, name="wrapped_async_tool") + + result = await tool.ainvoke(input={"q": "hello"}) + + assert result == "data:hello" + assert calls == ["hello"] + + +@pytest.mark.asyncio +async def test_ainvoke_through_to_structured_tool_resolves_async_tool_result(): + """An @tool-decorated async function wraps the sync Tool._run wrapper as func; ainvoke + must resolve the coroutine it returns instead of handing the caller a bare coroutine.""" + from crewai.tools import tool + + @tool + async def fetch_data(q: str) -> str: + """Fetch data for q.""" + return f"data:{q}" + + structured = fetch_data.to_structured_tool() + + result = await structured.ainvoke(input={"q": "hello"}) + + assert result == "data:hello"