Skip to content

[BUG] Async callable object hooks are not awaited #4337

Description

@97Three

Checks

  • I have updated to the latest minor and patch version of Strands
  • I have checked the documentation and this is not expected behavior
  • I have searched open and closed issues and found no duplicate

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-asyncRelated to asynchronous flows or multi-threadingarea-hooksFeatures or requests that might be implementable via hooksbugSomething isn't workingpythonPull requests that update python code

    Type

    Fields

    Language

    Python

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions