generated from amazon-archives/__template_Apache-2.0
-
Notifications
You must be signed in to change notification settings - Fork 552
feat: Implement the core system of typed hooks & callbacks #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
19cf581
feat: Implement the core system of typed hooks & callbacks
zastrowm 1e0627e
fix: type errors
zastrowm 6b1c288
fix: strongly type the TEvent
zastrowm cfba781
Trigger StartRequestEvent/EndRequestEvent for agent.structured_output…
zastrowm 348baa4
Clarified when StartRequestEvent & EndRequestEvent are triggered
zastrowm 16e8ebc
Move hooks into an experimental sub-package
zastrowm 2dc4e54
Move MockHookProvider to fixtures directory
zastrowm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| """Experimental features. | ||
| This module implements experimental features that are subject to change in future revisions without notice. | ||
| """ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| """Typed hook system for extending agent functionality. | ||
|
|
||
| This module provides a composable mechanism for building objects that can hook | ||
| into specific events during the agent lifecycle. The hook system enables both | ||
| built-in SDK components and user code to react to or modify agent behavior | ||
| through strongly-typed event callbacks. | ||
|
|
||
| Example Usage: | ||
| ```python | ||
| from strands.hooks import HookProvider, HookRegistry | ||
| from strands.hooks.events import StartRequestEvent, EndRequestEvent | ||
|
|
||
| class LoggingHooks(HookProvider): | ||
| def register_hooks(self, registry: HookRegistry) -> None: | ||
| registry.add_callback(StartRequestEvent, self.log_start) | ||
| registry.add_callback(EndRequestEvent, self.log_end) | ||
|
|
||
| def log_start(self, event: StartRequestEvent) -> None: | ||
| print(f"Request started for {event.agent.name}") | ||
|
|
||
| def log_end(self, event: EndRequestEvent) -> None: | ||
| print(f"Request completed for {event.agent.name}") | ||
|
|
||
| # Use with agent | ||
| agent = Agent(hooks=[LoggingHooks()]) | ||
| ``` | ||
|
|
||
| This replaces the older callback_handler approach with a more composable, | ||
| type-safe system that supports multiple subscribers per event type. | ||
| """ | ||
|
|
||
| from .events import AgentInitializedEvent, EndRequestEvent, StartRequestEvent | ||
| from .registry import HookCallback, HookEvent, HookProvider, HookRegistry | ||
|
|
||
| __all__ = [ | ||
| "AgentInitializedEvent", | ||
| "StartRequestEvent", | ||
| "EndRequestEvent", | ||
| "HookEvent", | ||
| "HookProvider", | ||
| "HookCallback", | ||
| "HookRegistry", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| """Hook events emitted as part of invoking Agents. | ||
|
|
||
| This module defines the events that are emitted as Agents run through the lifecycle of a request. | ||
| """ | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
| from .registry import HookEvent | ||
|
|
||
|
|
||
| @dataclass | ||
| class AgentInitializedEvent(HookEvent): | ||
| """Event triggered when an agent has finished initialization. | ||
|
|
||
| This event is fired after the agent has been fully constructed and all | ||
| built-in components have been initialized. Hook providers can use this | ||
| event to perform setup tasks that require a fully initialized agent. | ||
| """ | ||
|
|
||
| pass | ||
|
|
||
|
|
||
| @dataclass | ||
| class StartRequestEvent(HookEvent): | ||
| """Event triggered at the beginning of a new agent request. | ||
|
|
||
| This event is fired when the agent begins processing a new user request, | ||
| before any model inference or tool execution occurs. Hook providers can | ||
| use this event to perform request-level setup, logging, or validation. | ||
|
|
||
| This event is triggered at the beginning of the following api calls: | ||
| - Agent.__call__ | ||
| - Agent.stream_async | ||
| - Agent.structured_output | ||
| """ | ||
|
|
||
| pass | ||
|
|
||
|
|
||
| @dataclass | ||
| class EndRequestEvent(HookEvent): | ||
| """Event triggered at the end of an agent request. | ||
|
|
||
| This event is fired after the agent has completed processing a request, | ||
| regardless of whether it completed successfully or encountered an error. | ||
| Hook providers can use this event for cleanup, logging, or state persistence. | ||
|
|
||
| Note: This event uses reverse callback ordering, meaning callbacks registered | ||
| later will be invoked first during cleanup. | ||
|
|
||
| This event is triggered at the end of the following api calls: | ||
| - Agent.__call__ | ||
| - Agent.stream_async | ||
| - Agent.structured_output | ||
| """ | ||
|
|
||
| @property | ||
| def should_reverse_callbacks(self) -> bool: | ||
| """Return True to invoke callbacks in reverse order for proper cleanup. | ||
|
|
||
| Returns: | ||
| True, indicating callbacks should be invoked in reverse order. | ||
| """ | ||
| return True |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| """Hook registry system for managing event callbacks in the Strands Agent SDK. | ||
|
|
||
| This module provides the core infrastructure for the typed hook system, enabling | ||
| composable extension of agent functionality through strongly-typed event callbacks. | ||
| The registry manages the mapping between event types and their associated callback | ||
| functions, supporting both individual callback registration and bulk registration | ||
| via hook provider objects. | ||
| """ | ||
|
|
||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, Callable, Generator, Generic, Protocol, Type, TypeVar | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ...agent import Agent | ||
|
|
||
|
|
||
| @dataclass | ||
| class HookEvent: | ||
| """Base class for all hook events. | ||
|
|
||
| Attributes: | ||
| agent: The agent instance that triggered this event. | ||
| """ | ||
|
|
||
| agent: "Agent" | ||
|
|
||
| @property | ||
| def should_reverse_callbacks(self) -> bool: | ||
| """Determine if callbacks for this event should be invoked in reverse order. | ||
|
|
||
| Returns: | ||
| False by default. Override to return True for events that should | ||
| invoke callbacks in reverse order (e.g., cleanup/teardown events). | ||
| """ | ||
| return False | ||
|
|
||
|
|
||
| T = TypeVar("T", bound=Callable) | ||
| TEvent = TypeVar("TEvent", bound=HookEvent, contravariant=True) | ||
|
|
||
|
|
||
| class HookProvider(Protocol): | ||
| """Protocol for objects that provide hook callbacks to an agent. | ||
|
|
||
| Hook providers offer a composable way to extend agent functionality by | ||
| subscribing to various events in the agent lifecycle. This protocol enables | ||
| building reusable components that can hook into agent events. | ||
|
|
||
| Example: | ||
| ```python | ||
| class MyHookProvider(HookProvider): | ||
| def register_hooks(self, registry: HookRegistry) -> None: | ||
| hooks.add_callback(StartRequestEvent, self.on_request_start) | ||
| hooks.add_callback(EndRequestEvent, self.on_request_end) | ||
|
|
||
| agent = Agent(hooks=[MyHookProvider()]) | ||
| ``` | ||
| """ | ||
|
|
||
| def register_hooks(self, registry: "HookRegistry") -> None: | ||
| """Register callback functions for specific event types. | ||
|
|
||
| Args: | ||
| registry: The hook registry to register callbacks with. | ||
| """ | ||
| ... | ||
|
|
||
|
|
||
| class HookCallback(Protocol, Generic[TEvent]): | ||
| """Protocol for callback functions that handle hook events. | ||
|
|
||
| Hook callbacks are functions that receive a single strongly-typed event | ||
| argument and perform some action in response. They should not return | ||
| values and any exceptions they raise will propagate to the caller. | ||
|
|
||
| Example: | ||
| ```python | ||
| def my_callback(event: StartRequestEvent) -> None: | ||
| print(f"Request started for agent: {event.agent.name}") | ||
| ``` | ||
| """ | ||
|
|
||
| def __call__(self, event: TEvent) -> None: | ||
| """Handle a hook event. | ||
|
|
||
| Args: | ||
| event: The strongly-typed event to handle. | ||
| """ | ||
| ... | ||
|
|
||
|
|
||
| class HookRegistry: | ||
| """Registry for managing hook callbacks associated with event types. | ||
|
|
||
| The HookRegistry maintains a mapping of event types to callback functions | ||
| and provides methods for registering callbacks and invoking them when | ||
| events occur. | ||
|
|
||
| The registry handles callback ordering, including reverse ordering for | ||
| cleanup events, and provides type-safe event dispatching. | ||
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
| """Initialize an empty hook registry.""" | ||
| self._registered_callbacks: dict[Type, list[HookCallback]] = {} | ||
|
|
||
| def add_callback(self, event_type: Type[TEvent], callback: HookCallback[TEvent]) -> None: | ||
| """Register a callback function for a specific event type. | ||
|
|
||
| Args: | ||
| event_type: The class type of events this callback should handle. | ||
| callback: The callback function to invoke when events of this type occur. | ||
|
|
||
| Example: | ||
| ```python | ||
| def my_handler(event: StartRequestEvent): | ||
| print("Request started") | ||
|
|
||
| registry.add_callback(StartRequestEvent, my_handler) | ||
| ``` | ||
| """ | ||
| callbacks = self._registered_callbacks.setdefault(event_type, []) | ||
| callbacks.append(callback) | ||
|
|
||
| def add_hook(self, hook: HookProvider) -> None: | ||
| """Register all callbacks from a hook provider. | ||
|
|
||
| This method allows bulk registration of callbacks by delegating to | ||
| the hook provider's register_hooks method. This is the preferred | ||
| way to register multiple related callbacks. | ||
|
|
||
| Args: | ||
| hook: The hook provider containing callbacks to register. | ||
|
|
||
| Example: | ||
| ```python | ||
| class MyHooks(HookProvider): | ||
| def register_hooks(self, registry: HookRegistry): | ||
| registry.add_callback(StartRequestEvent, self.on_start) | ||
| registry.add_callback(EndRequestEvent, self.on_end) | ||
|
|
||
| registry.add_hook(MyHooks()) | ||
| ``` | ||
| """ | ||
| hook.register_hooks(self) | ||
|
|
||
| def invoke_callbacks(self, event: TEvent) -> None: | ||
| """Invoke all registered callbacks for the given event. | ||
|
|
||
| This method finds all callbacks registered for the event's type and | ||
| invokes them in the appropriate order. For events with is_after_callback=True, | ||
| callbacks are invoked in reverse registration order. | ||
|
|
||
| Args: | ||
| event: The event to dispatch to registered callbacks. | ||
|
|
||
| Raises: | ||
| Any exceptions raised by callback functions will propagate to the caller. | ||
|
|
||
| Example: | ||
| ```python | ||
| event = StartRequestEvent(agent=my_agent) | ||
| registry.invoke_callbacks(event) | ||
| ``` | ||
| """ | ||
| for callback in self.get_callbacks_for(event): | ||
| callback(event) | ||
|
|
||
| def get_callbacks_for(self, event: TEvent) -> Generator[HookCallback[TEvent], None, None]: | ||
| """Get callbacks registered for the given event in the appropriate order. | ||
|
|
||
| This method returns callbacks in registration order for normal events, | ||
| or reverse registration order for events that have is_after_callback=True. | ||
| This enables proper cleanup ordering for teardown events. | ||
|
|
||
| Args: | ||
| event: The event to get callbacks for. | ||
|
|
||
| Yields: | ||
| Callback functions registered for this event type, in the appropriate order. | ||
|
|
||
| Example: | ||
| ```python | ||
| event = EndRequestEvent(agent=my_agent) | ||
| for callback in registry.get_callbacks_for(event): | ||
| callback(event) | ||
| ``` | ||
| """ | ||
| event_type = type(event) | ||
|
|
||
| callbacks = self._registered_callbacks.get(event_type, []) | ||
| if event.should_reverse_callbacks: | ||
| yield from reversed(callbacks) | ||
| else: | ||
| yield from callbacks |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.