Checks
SDK Language
Python
Strands Version
Latest main at 39da4bbc6772b6c42e51b19907b2f0e31375dac6 (python/v1.55.1-29-g39da4bb)
Language Runtime Version
Python 3.12.3
Operating System
Linux 6.18.33.2-microsoft-standard-WSL2 x86_64
Installation Method
git clone / Hatch test environment
Description
A callable object whose __call__ method is async def can be registered as a hook, but HookRegistry.invoke_callbacks_async() does not await it. The hook body never runs and Python reports that its coroutine was never awaited.
This conflicts with the HookCallback protocol, which permits Awaitable[None], and with Agent.add_hook(), which documents support for asynchronous callbacks.
Steps to reproduce
Run this offline from the Python SDK development environment:
import asyncio
import gc
import warnings
from unittest.mock import Mock
from strands.hooks import BeforeInvocationEvent, HookRegistry
class AsyncCallableHook:
def __init__(self) -> None:
self.call_count = 0
async def __call__(self, event: BeforeInvocationEvent) -> None:
self.call_count += 1
async def main() -> None:
registry = HookRegistry()
callback = AsyncCallableHook()
registry.add_callback(BeforeInvocationEvent, callback)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always", RuntimeWarning)
await registry.invoke_callbacks_async(
BeforeInvocationEvent(agent=Mock(), invocation_state={})
)
gc.collect()
print(callback.call_count)
print([str(item.message) for item in caught])
asyncio.run(main())
Observed output:
0
["coroutine 'AsyncCallableHook.__call__' was never awaited"]
The same result occurs when the callback is registered through Agent.add_hook() and an offline mock model is invoked: the Agent completes, but the hook body is skipped.
Actual behavior
inspect.iscoroutinefunction(callback) returns False for the callable instance, even though inspect.iscoroutinefunction(callback.__call__) is True. The registry therefore calls the object through the synchronous branch and discards the returned coroutine.
Expected behavior
Async hook callbacks should be awaited regardless of whether they are async functions, bound async methods, or callable objects with an async __call__.
Impact
State updates, validation, telemetry, cleanup, or intervention logic placed in such a hook can be silently skipped while the Agent continues running. The only signal may be a runtime warning.
This affects hook dispatch across the Agent lifecycle because lifecycle events share HookRegistry.
Relevant code location
strands-py/src/strands/hooks/registry.py
HookRegistry.invoke_callbacks_async()
HookRegistry.invoke_callbacks()
- the
AgentInitializedEvent async-callback registration guard
strands-py/tests/strands/hooks/test_registry.py
Possible minimal fix direction
Invoke the callback once and determine whether its returned value is awaitable (for example with inspect.isawaitable) before awaiting it. The synchronous dispatcher and the AgentInitializedEvent restriction may need corresponding handling so callable objects behave consistently.
This is only a suggested direction; the final approach should preserve existing interrupt aggregation and synchronous callback behavior.
Checks
SDK Language
Python
Strands Version
Latest
mainat39da4bbc6772b6c42e51b19907b2f0e31375dac6(python/v1.55.1-29-g39da4bb)Language Runtime Version
Python 3.12.3
Operating System
Linux 6.18.33.2-microsoft-standard-WSL2 x86_64
Installation Method
git clone / Hatch test environment
Description
A callable object whose
__call__method isasync defcan be registered as a hook, butHookRegistry.invoke_callbacks_async()does not await it. The hook body never runs and Python reports that its coroutine was never awaited.This conflicts with the
HookCallbackprotocol, which permitsAwaitable[None], and withAgent.add_hook(), which documents support for asynchronous callbacks.Steps to reproduce
Run this offline from the Python SDK development environment:
Observed output:
The same result occurs when the callback is registered through
Agent.add_hook()and an offline mock model is invoked: the Agent completes, but the hook body is skipped.Actual behavior
inspect.iscoroutinefunction(callback)returnsFalsefor the callable instance, even thoughinspect.iscoroutinefunction(callback.__call__)isTrue. The registry therefore calls the object through the synchronous branch and discards the returned coroutine.Expected behavior
Async hook callbacks should be awaited regardless of whether they are async functions, bound async methods, or callable objects with an async
__call__.Impact
State updates, validation, telemetry, cleanup, or intervention logic placed in such a hook can be silently skipped while the Agent continues running. The only signal may be a runtime warning.
This affects hook dispatch across the Agent lifecycle because lifecycle events share
HookRegistry.Relevant code location
strands-py/src/strands/hooks/registry.pyHookRegistry.invoke_callbacks_async()HookRegistry.invoke_callbacks()AgentInitializedEventasync-callback registration guardstrands-py/tests/strands/hooks/test_registry.pyPossible minimal fix direction
Invoke the callback once and determine whether its returned value is awaitable (for example with
inspect.isawaitable) before awaiting it. The synchronous dispatcher and theAgentInitializedEventrestriction may need corresponding handling so callable objects behave consistently.This is only a suggested direction; the final approach should preserve existing interrupt aggregation and synchronous callback behavior.