Fix asyncio hanging issue when using crew.kickoff() with asyncio.to_thread()
Fix asyncio hanging issue when using crew.kickoff() with asyncio.to_thread()
Summary
Fixes issue #3730 where crews would hang indefinitely when run with asyncio.to_thread(crew.kickoff) if they used async tools.
Root Cause: The tools code was calling asyncio.run() to execute async tool functions. When a crew is run via asyncio.to_thread(), it executes in a thread spawned by an existing event loop. Calling asyncio.run() in this context fails because it tries to create a new event loop in a thread that's conceptually part of an already-running loop, causing a hang.
Solution: Added a run_coroutine_sync() utility that:
- Uses
asyncio.run()when no event loop is running (normal case) - When a loop IS running, spawns a new thread with its own event loop to run the coroutine
Changes:
- Added
src/crewai/utilities/asyncio_utils.pywithrun_coroutine_sync()helper - Updated
CrewStructuredTool.invoke()to use the new helper (2 call sites) - Updated
BaseTool.run()to use the new helper (1 call site) - Added comprehensive test suite in
tests/test_asyncio_tools.py
Review & Testing Checklist for Human
⚠️ Risk Level: MEDIUM-HIGH - This modifies a core execution path affecting all async tool usage
-
[ ] Review the threading approach in
run_coroutine_sync()- Is spawning a new thread with a new event loop the right solution? Consider:- Performance implications (creates a thread for every async tool call when in nested async context)
- Potential thread safety issues
- Resource leak risks (event loop cleanup)
- Whether there's a better alternative (e.g., using
asyncio.get_event_loop().run_until_complete()or other approaches)
-
[ ] Test the actual issue scenario end-to-end - The new tests mock
Agent.execute_task, so they don't verify full integration. Test manually:# Run this with a real LLM configured import asyncio from crewai import Agent, Crew, Task from crewai.tools import tool @tool async def test_tool(x: str) -> str: """Test async tool""" await asyncio.sleep(0.1) return f"Result: {x}" crew = Crew( agents=[Agent(role="Test", goal="Test", backstory="Test")], tasks=[Task(description="Use test_tool", expected_output="Result", tools=[test_tool])] ) async def run(): result = await asyncio.to_thread(crew.kickoff) print(result) asyncio.run(run())Verify it completes without hanging.
-
[ ] Verify the lock file regeneration didn't break anything - The
uv.lockwas completely regenerated (not part of core fix). Run the full test suite to ensure no regressions. -
[ ] Check exception handling - Verify that exceptions raised in async tools are properly propagated with full tracebacks through the new threading approach.
Notes
- Issue reported by user with local Ollama setup experiencing hangs when running crew in background with
asyncio.to_thread() - The existing
kickoff_async()method already worked correctly; this fixes the manualasyncio.to_thread(crew.kickoff)pattern - All new tests pass, and existing thread safety tests pass
Link to Devin run: https://app.devin.ai/sessions/fa61a1f74deb410faa495932f4a86cad
Requested by: João ([email protected])
🤖 Devin AI Engineer
I'll be helping with this pull request! Here's what you should know:
✅ I will automatically:
- Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
- Look at CI failures and help fix them
Note: I can only respond to comments from users who have write access to this repository.
⚙️ Control Options:
- [ ] Disable automatic comment and CI monitoring
Just stumbled upon this problem, +1 for merging this PR <3
When is this getting merged and released?