Skip to content

fix(tools): run async tool results safely under a running event loop - #6979

Open
Shailendra005 wants to merge 2 commits into
crewAIInc:mainfrom
Shailendra005:fix/tool-run-under-running-loop
Open

fix(tools): run async tool results safely under a running event loop#6979
Shailendra005 wants to merge 2 commits into
crewAIInc:mainfrom
Shailendra005:fix/tool-run-under-running-loop

Conversation

@Shailendra005

Copy link
Copy Markdown

What

Tool execution paths that resolve a coroutine result now use the project's established running-loop guard instead of a bare asyncio.run(), which raised RuntimeError: asyncio.run() cannot be called from a running event loop when a tool was invoked inside a running loop (FastAPI, Jupyter, any async def).

Why

BaseTool.run, the @tool-decorator Tool.run, and CrewStructuredTool.invoke all did asyncio.run(result) unconditionally. The codebase already has the correct guard (tasks/llm_guardrail.py::_run_coroutine_sync, also in mcp_native_tool.py, a2a/utils/*, mcp/tool_resolver.py, project/*); the tool paths just didn't use it.

How

  • Added crewai/utilities/async_utils.py::run_coroutine_sync — a generic (TypeVar) version of the existing guard. It lives in utilities because base_tool already imports structured_tool, so a helper in either module would create a circular import; both now import from the shared util.
  • When no loop is running it falls back to asyncio.run; when a loop is running it runs the coroutine to completion in a one-worker ThreadPoolExecutor with a copied contextvars context, so the caller's loop is neither blocked nor re-entered.
  • No public API change.

Tests

New TestToolRunUnderRunningLoop in tests/tools/test_async_tools.py exercises all three call sites from inside asyncio.run(...). Verified failing before the change (RuntimeError) and passing after.

BEFORE: 3 failed (RuntimeError: asyncio.run() cannot be called from a running event loop)
AFTER : 15 passed  (test_async_tools.py)
        73 passed  (test_base_tool.py + test_structured_tool.py)

Notes

The other duplicate guard call sites are intentionally left untouched to keep this to one logical change; consolidating them onto run_coroutine_sync could be a follow-up.

Fixes #6978

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: db450798-6137-4aa0-8d23-cc2fd58ad99a

📥 Commits

Reviewing files that changed from the base of the PR and between ab73c0f and 81cab34.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/utilities/async_utils.py
  • lib/crewai/tests/tools/test_async_tools.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai/tests/tools/test_async_tools.py

📝 Walkthrough

Walkthrough

Synchronous tool execution now resolves coroutines safely when an event loop is already running. A shared utility handles direct execution and worker-thread execution. Tool invocation tests cover async functions and coroutine-returning functions.

Changes

Async tool execution

Layer / File(s) Summary
Coroutine execution utility
lib/crewai/src/crewai/utilities/async_utils.py
Adds run_coroutine_sync, which runs coroutines directly or in a context-preserving worker thread when an event loop is active.
Tool invocation integration and validation
lib/crewai/src/crewai/tools/base_tool.py, lib/crewai/src/crewai/tools/structured_tool.py, lib/crewai/tests/tools/test_async_tools.py
Routes synchronous BaseTool, Tool, and CrewStructuredTool execution through the utility. Adds regression tests for execution inside an active event loop.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant BaseTool
  participant run_coroutine_sync
  participant WorkerThread
  Caller->>BaseTool: call run or invoke
  BaseTool->>run_coroutine_sync: execute coroutine synchronously
  run_coroutine_sync->>WorkerThread: run coroutine when loop is active
  WorkerThread-->>run_coroutine_sync: return result
  run_coroutine_sync-->>BaseTool: return result
  BaseTool-->>Caller: return tool result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: safely handling asynchronous tool results when an event loop is already running.
Description check ✅ Passed The description directly explains the problem, implementation, affected tool paths, tests, and issue addressed by the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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: 2

🤖 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/utilities/async_utils.py`:
- Around line 20-21: Update the documentation around the synchronous execution
helper and its fallback description to state that Future.result() blocks the
calling thread, including an active event-loop thread; the worker thread only
avoids re-entering the loop. Direct asynchronous callers to use arun() or
ainvoke() where applicable.

In `@lib/crewai/tests/tools/test_async_tools.py`:
- Around line 187-203: Add a test alongside
test_structured_tool_invoke_inside_running_loop using a regular def tool
function that returns an inner coroutine, then invoke it from the active loop
through CrewStructuredTool.invoke and assert the resolved result. Keep the test
focused on observable behavior and cover the returned-coroutine path separately
from the async def case.
🪄 Autofix

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: e41ba2ee-f6c5-4a13-a3f6-cfeb50eee851

📥 Commits

Reviewing files that changed from the base of the PR and between 27083f4 and ab73c0f.

📒 Files selected for processing (4)
  • lib/crewai/src/crewai/tools/base_tool.py
  • lib/crewai/src/crewai/tools/structured_tool.py
  • lib/crewai/src/crewai/utilities/async_utils.py
  • lib/crewai/tests/tools/test_async_tools.py

Comment thread lib/crewai/src/crewai/utilities/async_utils.py Outdated
Comment thread lib/crewai/tests/tools/test_async_tools.py
@Shailendra005

Copy link
Copy Markdown
Author

Thanks for the review — both points addressed in 81cab34:

  1. Docstring accuracy (async_utils.py): you're right that Future.result() blocks the calling thread, so the running loop makes no progress while it waits. Reworded the docstring to say exactly that — it now states the worker-thread execution avoids re-entering the running loop, and that the calling thread blocks on Future.result() until the coroutine completes (so the loop does not progress meanwhile). The blocking behaviour matches the existing llm_guardrail._run_coroutine_sync pattern this generalizes.

  2. Untested branch in CrewStructuredTool.invoke: added test_structured_tool_invoke_returned_coroutine_inside_running_loop, which uses a plain (non-async def) function that returns a coroutine, exercising the result = self.func(...)asyncio.iscoroutine(result)run_coroutine_sync(result) branch at structured_tool.py:444-447 (the existing test only hit the iscoroutinefunction path). Full file now 16 passed; ruff clean.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BaseTool.run / StructuredTool raise 'asyncio.run() cannot be called from a running event loop' when a tool returns a coroutine

1 participant