Skip to content

Commit 6cf6672

Browse files
joaomdmouraclaude
andcommitted
fix(skills): resolve non-coroutine awaitables in resolve_response
`inspect.isawaitable()` also matches Tasks, Futures and custom `__await__` types, while `asyncio.run()` takes a coroutine specifically — so a client returning anything other than a raw coroutine raised "a coroutine was expected". Wrap the awaitable instead, which also makes the two `type: ignore[arg-type]` comments unnecessary. Awaitables already bound to a running loop still raise, since resolving one synchronously would block the loop it needs to progress; documented on the function. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d8aa275 commit 6cf6672

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

lib/crewai/src/crewai/utilities/plus_client_factory.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ def resolve_response(response: Any) -> Any:
6969
``PlusAPI`` is synchronous while the Enterprise client is asynchronous, so
7070
call sites accept either and normalize here.
7171
72+
Awaitables that are already bound to a running loop (a Task or Future) can't
73+
be resolved this way and raise — resolving one synchronously would mean
74+
blocking the very loop it needs to make progress. Clients should hand back
75+
coroutines.
76+
7277
Args:
7378
response: A response, or an awaitable resolving to one.
7479
@@ -78,6 +83,12 @@ def resolve_response(response: Any) -> Any:
7883
if not inspect.isawaitable(response):
7984
return response
8085

86+
# asyncio.run() takes a coroutine specifically, while isawaitable() also
87+
# covers Tasks, Futures and custom __await__ objects, so wrap rather than
88+
# passing the awaitable straight through.
89+
async def await_response() -> Any:
90+
return await response
91+
8192
try:
8293
loop = asyncio.get_running_loop()
8394
except RuntimeError:
@@ -87,6 +98,6 @@ def resolve_response(response: Any) -> Any:
8798
# to a worker thread with a loop of its own.
8899
if loop and loop.is_running():
89100
with concurrent.futures.ThreadPoolExecutor() as pool:
90-
return pool.submit(asyncio.run, response).result() # type: ignore[arg-type]
101+
return pool.submit(asyncio.run, await_response()).result()
91102

92-
return asyncio.run(response) # type: ignore[arg-type]
103+
return asyncio.run(await_response())

lib/crewai/tests/utilities/test_plus_client_factory.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,16 @@ async def main() -> Any:
104104
return resolve_response(call())
105105

106106
assert asyncio.run(main()) is response
107+
108+
def test_awaits_a_non_coroutine_awaitable(self) -> None:
109+
"""isawaitable() also covers Tasks, Futures and custom __await__ types."""
110+
response = MagicMock()
111+
112+
class Awaitable:
113+
def __await__(self): # noqa: ANN204
114+
async def inner() -> Any:
115+
return response
116+
117+
return inner().__await__()
118+
119+
assert resolve_response(Awaitable()) is response

0 commit comments

Comments
 (0)