Skip to content

fix: await async tools natively in _ainvoke_loop_native_tools#6622

Open
rkfshakti wants to merge 1 commit into
crewAIInc:mainfrom
rkfshakti:fix/async-native-tools-6611
Open

fix: await async tools natively in _ainvoke_loop_native_tools#6622
rkfshakti wants to merge 1 commit into
crewAIInc:mainfrom
rkfshakti:fix/async-native-tools-6611

Conversation

@rkfshakti

Copy link
Copy Markdown

What this PR does

Adds async variants of _handle_native_tool_calls and _execute_single_native_tool_call that use await tool.arun() instead of tool.run(), and updates _ainvoke_loop_native_tools to call them.

Why it's needed

When the async native tool path (_ainvoke_loop_native_tools) calls the sync _handle_native_tool_calls, which calls tool.run()asyncio.run(), the asyncio.run() call crashes with RuntimeError: asyncio.run() cannot be called from a running event loop when the agent is invoked from an already-running event loop (e.g. via ainvoke()).

The ReAct executor (_ainvoke_loop_react) already handles this correctly via aexecute_tool_and_check_finality()tool_usage.ause()await.

What changed

  1. _ahandle_native_tool_calls() — async variant that uses asyncio.gather instead of ThreadPoolExecutor for parallel tool execution, so async tools are properly awaited rather than run through asyncio.run().

  2. _aexecute_single_native_tool_call() — async variant that calls await tool.arun() instead of tool.run(), avoiding the nested asyncio.run() crash.

  3. _ainvoke_loop_native_tools() now calls _ahandle_native_tool_calls() instead of _handle_native_tool_calls().

Reviewer Test Plan

How to verify

  1. Create a crew with an async tool (a tool whose _run method is a coroutine)
  2. Call crew.kickoff_async() with function_calling_llm set to a model that supports native function calling
  3. Before the fix: RuntimeError: asyncio.run() cannot be called from a running event loop
  4. After the fix: the tool executes successfully

Tested on

OS Status
macOS
Windows ⚠️
Linux ⚠️

Risk & Scope

  • Main risk or tradeoff: The async path now uses asyncio.gather instead of ThreadPoolExecutor for parallel execution. This is the correct approach for async code but means parallel tool execution is cooperative rather than preemptive.
  • Not validated / out of scope: The experimental AgentExecutor (Flow-based) has the same bug but is in a separate code path.
  • Breaking changes / migration notes: None. The sync path is unchanged.

Linked Issues

Fixes #6611

When the async native tool path (_ainvoke_loop_native_tools) calls the
sync _handle_native_tool_calls, which calls tool.run() -> asyncio.run(),
the asyncio.run() call crashes with 'RuntimeError: asyncio.run() cannot
be called from a running event loop' when the agent is invoked from an
already-running event loop (e.g. via ainvoke()).

The fix adds three async methods:

1. _ahandle_native_tool_calls() — async variant that uses asyncio.gather
   instead of ThreadPoolExecutor for parallel tool execution, so async
   tools are properly awaited rather than run through asyncio.run().

2. _aexecute_single_native_tool_call() — async variant that calls
   await tool.arun() instead of tool.run(), avoiding the nested
   asyncio.run() crash.

3. _ainvoke_loop_native_tools() now calls _ahandle_native_tool_calls()
   instead of _handle_native_tool_calls().

The ReAct executor (_ainvoke_loop_react) already handles this correctly
via aexecute_tool_and_check_finality() -> tool_usage.ause() -> await.

Fixes crewAIInc#6611
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

CrewAgentExecutor adds asynchronous native-tool batching and individual tool execution, including concurrent dispatch, caching, limits, hooks, lifecycle events, and async tool invocation. The async native-tools loop now awaits the new batch handler.

Changes

Async native tool execution

Layer / File(s) Summary
Async native tool batch orchestration
lib/crewai/src/crewai/agents/crew_agent_executor.py
Native tool calls are parsed and executed concurrently when allowed, ordered results are appended to the prompt, and final results return an AgentFinish.
Async single-tool execution and lifecycle
lib/crewai/src/crewai/agents/crew_agent_executor.py
Individual calls parse arguments, enforce usage limits, read cached results, emit lifecycle events, run hooks, and use await output_tool.arun(...) when available.
Async native-tools loop integration
lib/crewai/src/crewai/agents/crew_agent_executor.py
The async native-tools loop awaits _ahandle_native_tool_calls(...) instead of calling the synchronous handler.

Sequence Diagram(s)

sequenceDiagram
  participant NativeToolsLoop
  participant CrewAgentExecutor
  participant Tool
  NativeToolsLoop->>CrewAgentExecutor: await _ahandle_native_tool_calls(tool_calls)
  CrewAgentExecutor->>Tool: await _aexecute_single_native_tool_call(...)
  Tool-->>CrewAgentExecutor: tool result
  CrewAgentExecutor-->>NativeToolsLoop: AgentFinish or updated tool messages
Loading

Suggested reviewers: lucasgomide, joaomdmoura, greysonlalonde

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the core async native-tool fix.
Description check ✅ Passed The description clearly matches the code changes and explains the async tool fix.
Linked Issues check ✅ Passed The async native-tool path now awaits async tools and aligns with #6611's expected behavior.
Out of Scope Changes check ✅ Passed The changes stay focused on async native-tool execution and do not introduce unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/crewai/src/crewai/agents/crew_agent_executor.py (1)

1184-1377: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Heavy duplication with _execute_single_native_tool_call.

_aexecute_single_native_tool_call is a near-verbatim copy of the sync method (parsing, tool resolution, usage limits, cache, events, hooks) with only the invocation block (1301-1304) differing. Same applies to _ahandle_native_tool_calls vs _handle_native_tool_calls. This ~190-line duplication will drift as either path changes. Consider extracting the shared pre/post logic into helpers that accept an invocation callable, keeping only the sync-vs-async invocation distinct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/agents/crew_agent_executor.py` around lines 1184 -
1377, Refactor _aexecute_single_native_tool_call and
_execute_single_native_tool_call to share their common parsing, tool resolution,
usage-limit, cache, event, hook, and result-processing logic through a helper
that accepts the tool invocation operation. Keep only synchronous versus awaited
invocation in separate callables, and apply the same deduplication to
_ahandle_native_tool_calls and _handle_native_tool_calls while preserving
existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/src/crewai/agents/crew_agent_executor.py`:
- Around line 1298-1304: Update the synchronous fallback in the tool execution
branch of the crew agent executor to run available_functions[func_name] via the
existing ThreadPoolExecutor or equivalent worker-thread mechanism, preserving
the current arguments and awaited result handling. Keep the output_tool.arun
path unchanged so synchronous tools no longer block the running event loop or
serialize gathered executions.

---

Nitpick comments:
In `@lib/crewai/src/crewai/agents/crew_agent_executor.py`:
- Around line 1184-1377: Refactor _aexecute_single_native_tool_call and
_execute_single_native_tool_call to share their common parsing, tool resolution,
usage-limit, cache, event, hook, and result-processing logic through a helper
that accepts the tool invocation operation. Keep only synchronous versus awaited
invocation in separate callables, and apply the same deduplication to
_ahandle_native_tool_calls and _handle_native_tool_calls while preserving
existing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e1f60bd-6faa-43c5-80fd-84b52fe84664

📥 Commits

Reviewing files that changed from the base of the PR and between b14d36b and 88e3f91.

📒 Files selected for processing (1)
  • lib/crewai/src/crewai/agents/crew_agent_executor.py

Comment on lines +1298 to +1304
try:
# Use arun() instead of run() to properly await async tools
# inside a running event loop.
if hasattr(output_tool, "arun") and callable(output_tool.arun):
raw_result = await output_tool.arun(**(args_dict or {}))
else:
raw_result = available_functions[func_name](**(args_dict or {}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Sync tool fallback blocks the event loop and regresses batch parallelism.

When output_tool has no arun, the synchronous callable is invoked directly on the running loop. Inside asyncio.gather, this serializes sync tools and blocks the loop for their full duration — a regression from the previous sync path, which offloaded these calls to a ThreadPoolExecutor (true parallelism for I/O-bound sync tools). Offload to a worker thread instead.

♻️ Offload the sync fallback
             try:
                 # Use arun() instead of run() to properly await async tools
                 # inside a running event loop.
                 if hasattr(output_tool, "arun") and callable(output_tool.arun):
                     raw_result = await output_tool.arun(**(args_dict or {}))
                 else:
-                    raw_result = available_functions[func_name](**(args_dict or {}))
+                    raw_result = await asyncio.to_thread(
+                        available_functions[func_name], **(args_dict or {})
+                    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
# Use arun() instead of run() to properly await async tools
# inside a running event loop.
if hasattr(output_tool, "arun") and callable(output_tool.arun):
raw_result = await output_tool.arun(**(args_dict or {}))
else:
raw_result = available_functions[func_name](**(args_dict or {}))
try:
# Use arun() instead of run() to properly await async tools
# inside a running event loop.
if hasattr(output_tool, "arun") and callable(output_tool.arun):
raw_result = await output_tool.arun(**(args_dict or {}))
else:
raw_result = await asyncio.to_thread(
available_functions[func_name], **(args_dict or {})
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/agents/crew_agent_executor.py` around lines 1298 -
1304, Update the synchronous fallback in the tool execution branch of the crew
agent executor to run available_functions[func_name] via the existing
ThreadPoolExecutor or equivalent worker-thread mechanism, preserving the current
arguments and awaited result handling. Keep the output_tool.arun path unchanged
so synchronous tools no longer block the running event loop or serialize
gathered executions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant