[BUG] Async tools are awaited via asyncio.run() in a thread on the native-function-calling path, not natively in the running loop
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 58.8k
- Forks
- 8.5k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 109
Description
Description
When an agent uses a model with native/parallel function calling (OpenAI, Gemini, Anthropic) and is invoked via ainvoke()/kickoff_async(), the async native-tools loop _ainvoke_loop_native_tools (lib/crewai/src/crewai/agents/crew_agent_executor.py:1354) calls the synchronous self._handle_native_tool_calls, which runs tools through tool.run(). For a genuinely async tool (async def _run), BaseTool.run gets a coroutine back and bridges it with asyncio.run() inside a worker thread — so the tool is never awaited on the executor's running event loop.
The ReAct executor does the opposite: CrewStructuredTool.ainvoke detects coroutine functions and awaits them natively (structured_tool.py: if inspect.iscoroutinefunction(self.func): return await self.func(...)).
So the same async tool behaves differently depending purely on whether the model supports native function calling:
- ReAct path: tool coroutine awaited on the running loop (native).
- Native-tools path: thread hop + a fresh nested event loop per call (
asyncio.run()), losing single-loop concurrency for I/O-bound async tools and paying loop setup/teardown on every invocation.
Steps to Reproduce
import asyncio, threading
from typing import Type
from pydantic import BaseModel, Field
from crewai.tools import BaseTool
from crewai import Agent, Task, Crew
class Inp(BaseModel):
label: str = Field(..., description="the label")
class AsyncTool(BaseTool):
name: str = "timer"
description: str = "Call once with your label."
args_schema: Type[BaseModel] = Inp
async def _run(self, label: str) -> str:
print(f"thread={threading.current_thread().name}, running loop: {asyncio.get_running_loop() is asyncio.get_event_loop()}")
await asyncio.sleep(0.2)
return "recorded"
a = Agent(role="Timer", goal="call the timer tool once",
backstory="You must call the timer tool.",
tools=[AsyncTool()], llm="openai/gpt-4o-mini")
t = Task(description="Call the timer tool once with label=x",
expected_output="confirmation", agent=a)
c = Crew(agents=[a], tasks=[t])
asyncio.run(c.kickoff_async())
Observed: the async _run executes inside a worker thread with a brand-new event loop (created by asyncio.run() in BaseTool.run), not on the loop that is running kickoff_async().
Expected behavior
_ainvoke_loop_native_tools should await the tool's async entry point (await tool.arun(...)) — matching the ReAct executor's native async behavior.
Root cause
agents/crew_agent_executor.py:1354—_ainvoke_loop_native_toolscalls syncself._handle_native_tool_calls(...)instead of an async variant._handle_native_tool_calls→_execute_single_native_tool_call→tool.run()→BaseTool.run→asyncio.run(result)in a thread.
Possible fix direction
Add async variants of _handle_native_tool_calls / _execute_single_native_tool_call that use await tool.arun() and have _ainvoke_loop_native_tools call them. (I have a working implementation in the closed PR #6622 — happy to reopen/rebase it against the current main once there's an open issue to link, per the require-issue gate.)
Related-but-distinct (per search): #195 predates async-tool support; #3988 is async @agent/@task definition methods; #1644 is the asyncio.run() cannot be called from a running event loop symptom.
- Model: any native-function-calling model reproduces; verified with OpenAI-compatible models.
Disclosure (per the AI-Generated Contributions policy in CONTRIBUTING.md): this report was written with AI assistance (opencode coding agent helped analyze the code path and draft the writeup); I verified the root cause by reading the source on current main and confirmed the reproduction. Maintainers: please apply the llm-generated label if appropriate — I don't have permission to apply labels on this repo.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/crewai/src/crewai/agents/crew_agent_executor.py at _ainvoke_loop_native_tools and trace the synchronous native-tool handlers into BaseTool.run. Compare that path with structured_tool.py and the async tool entry point; done means native-function-calling async tools are awaited on the existing running loop rather than through a worker thread and nested asyncio.run().
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100