crewAIInc / crewAIInc/crewAI

[BUG] Async tools are awaited via asyncio.run() in a thread on the native-function-calling path, not natively in the running loop

Open
#7,630 0 comments 0 reactions 0 assignees View on GitHub

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_tools calls sync self._handle_native_tool_calls(...) instead of an async variant.
  • _handle_native_tool_calls_execute_single_native_tool_calltool.run()BaseTool.runasyncio.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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.