Skip to content

Commit 289abae

Browse files
authored
Mark hooks as non-experimental (#410)
We've keep model & tool events as experimental but *Invocation and Message events are now marked as 'stable'
1 parent a0f7c24 commit 289abae

File tree

13 files changed

+184
-173
lines changed

13 files changed

+184
-173
lines changed

src/strands/agent/agent.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@
2020
from pydantic import BaseModel
2121

2222
from ..event_loop.event_loop import event_loop_cycle, run_tool
23-
from ..experimental.hooks import (
23+
from ..handlers.callback_handler import PrintingCallbackHandler, null_callback_handler
24+
from ..hooks import (
2425
AfterInvocationEvent,
2526
AgentInitializedEvent,
2627
BeforeInvocationEvent,
28+
HookProvider,
2729
HookRegistry,
2830
MessageAddedEvent,
2931
)
30-
from ..handlers.callback_handler import PrintingCallbackHandler, null_callback_handler
3132
from ..models.bedrock import BedrockModel
3233
from ..telemetry.metrics import EventLoopMetrics
3334
from ..telemetry.tracer import get_tracer
@@ -202,6 +203,7 @@ def __init__(
202203
name: Optional[str] = None,
203204
description: Optional[str] = None,
204205
state: Optional[Union[AgentState, dict]] = None,
206+
hooks: Optional[list[HookProvider]] = None,
205207
):
206208
"""Initialize the Agent with the specified configuration.
207209
@@ -238,6 +240,8 @@ def __init__(
238240
Defaults to None.
239241
state: stateful information for the agent. Can be either an AgentState object, or a json serializable dict.
240242
Defaults to an empty AgentState object.
243+
hooks: hooks to be added to the agent hook registry
244+
Defaults to None.
241245
"""
242246
self.model = BedrockModel() if not model else BedrockModel(model_id=model) if isinstance(model, str) else model
243247
self.messages = messages if messages is not None else []
@@ -301,9 +305,11 @@ def __init__(
301305
self.name = name or _DEFAULT_AGENT_NAME
302306
self.description = description
303307

304-
self._hooks = HookRegistry()
305-
# Register built-in hook providers (like ConversationManager) here
306-
self._hooks.invoke_callbacks(AgentInitializedEvent(agent=self))
308+
self.hooks = HookRegistry()
309+
if hooks:
310+
for hook in hooks:
311+
self.hooks.add_hook(hook)
312+
self.hooks.invoke_callbacks(AgentInitializedEvent(agent=self))
307313

308314
@property
309315
def tool(self) -> ToolCaller:
@@ -424,7 +430,7 @@ async def structured_output_async(
424430
Raises:
425431
ValueError: If no conversation history or prompt is provided.
426432
"""
427-
self._hooks.invoke_callbacks(BeforeInvocationEvent(agent=self))
433+
self.hooks.invoke_callbacks(BeforeInvocationEvent(agent=self))
428434

429435
try:
430436
if not self.messages and not prompt:
@@ -443,7 +449,7 @@ async def structured_output_async(
443449
return event["output"]
444450

445451
finally:
446-
self._hooks.invoke_callbacks(AfterInvocationEvent(agent=self))
452+
self.hooks.invoke_callbacks(AfterInvocationEvent(agent=self))
447453

448454
async def stream_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: Any) -> AsyncIterator[Any]:
449455
"""Process a natural language prompt and yield events as an async iterator.
@@ -509,7 +515,7 @@ async def _run_loop(self, message: Message, kwargs: dict[str, Any]) -> AsyncGene
509515
Yields:
510516
Events from the event loop cycle.
511517
"""
512-
self._hooks.invoke_callbacks(BeforeInvocationEvent(agent=self))
518+
self.hooks.invoke_callbacks(BeforeInvocationEvent(agent=self))
513519

514520
try:
515521
yield {"callback": {"init_event_loop": True, **kwargs}}
@@ -523,7 +529,7 @@ async def _run_loop(self, message: Message, kwargs: dict[str, Any]) -> AsyncGene
523529

524530
finally:
525531
self.conversation_manager.apply_management(self)
526-
self._hooks.invoke_callbacks(AfterInvocationEvent(agent=self))
532+
self.hooks.invoke_callbacks(AfterInvocationEvent(agent=self))
527533

528534
async def _execute_event_loop_cycle(self, kwargs: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]:
529535
"""Execute the event loop cycle with retry logic for context window limits.
@@ -653,4 +659,4 @@ def _end_agent_trace_span(
653659
def _append_message(self, message: Message) -> None:
654660
"""Appends a message to the agent's list of messages and invokes the callbacks for the MessageCreatedEvent."""
655661
self.messages.append(message)
656-
self._hooks.invoke_callbacks(MessageAddedEvent(agent=self, message=message))
662+
self.hooks.invoke_callbacks(MessageAddedEvent(agent=self, message=message))

src/strands/event_loop/event_loop.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
AfterToolInvocationEvent,
1919
BeforeModelInvocationEvent,
2020
BeforeToolInvocationEvent,
21+
)
22+
from ..hooks import (
2123
MessageAddedEvent,
22-
get_registry,
2324
)
2425
from ..telemetry.metrics import Trace
2526
from ..telemetry.tracer import get_tracer
@@ -120,7 +121,7 @@ async def event_loop_cycle(agent: "Agent", kwargs: dict[str, Any]) -> AsyncGener
120121

121122
tool_specs = agent.tool_registry.get_all_tool_specs()
122123

123-
get_registry(agent).invoke_callbacks(
124+
agent.hooks.invoke_callbacks(
124125
BeforeModelInvocationEvent(
125126
agent=agent,
126127
)
@@ -136,7 +137,7 @@ async def event_loop_cycle(agent: "Agent", kwargs: dict[str, Any]) -> AsyncGener
136137
stop_reason, message, usage, metrics = event["stop"]
137138
kwargs.setdefault("request_state", {})
138139

139-
get_registry(agent).invoke_callbacks(
140+
agent.hooks.invoke_callbacks(
140141
AfterModelInvocationEvent(
141142
agent=agent,
142143
stop_response=AfterModelInvocationEvent.ModelStopResponse(
@@ -154,7 +155,7 @@ async def event_loop_cycle(agent: "Agent", kwargs: dict[str, Any]) -> AsyncGener
154155
if model_invoke_span:
155156
tracer.end_span_with_error(model_invoke_span, str(e), e)
156157

157-
get_registry(agent).invoke_callbacks(
158+
agent.hooks.invoke_callbacks(
158159
AfterModelInvocationEvent(
159160
agent=agent,
160161
exception=e,
@@ -188,7 +189,7 @@ async def event_loop_cycle(agent: "Agent", kwargs: dict[str, Any]) -> AsyncGener
188189

189190
# Add the response message to the conversation
190191
agent.messages.append(message)
191-
get_registry(agent).invoke_callbacks(MessageAddedEvent(agent=agent, message=message))
192+
agent.hooks.invoke_callbacks(MessageAddedEvent(agent=agent, message=message))
192193
yield {"callback": {"message": message}}
193194

194195
# Update metrics
@@ -308,7 +309,7 @@ async def run_tool(agent: "Agent", tool_use: ToolUse, kwargs: dict[str, Any]) ->
308309
}
309310
)
310311

311-
before_event = get_registry(agent).invoke_callbacks(
312+
before_event = agent.hooks.invoke_callbacks(
312313
BeforeToolInvocationEvent(
313314
agent=agent,
314315
selected_tool=tool_func,
@@ -342,7 +343,7 @@ async def run_tool(agent: "Agent", tool_use: ToolUse, kwargs: dict[str, Any]) ->
342343
"content": [{"text": f"Unknown tool: {tool_name}"}],
343344
}
344345
# for every Before event call, we need to have an AfterEvent call
345-
after_event = get_registry(agent).invoke_callbacks(
346+
after_event = agent.hooks.invoke_callbacks(
346347
AfterToolInvocationEvent(
347348
agent=agent,
348349
selected_tool=selected_tool,
@@ -359,7 +360,7 @@ async def run_tool(agent: "Agent", tool_use: ToolUse, kwargs: dict[str, Any]) ->
359360

360361
result = event
361362

362-
after_event = get_registry(agent).invoke_callbacks(
363+
after_event = agent.hooks.invoke_callbacks(
363364
AfterToolInvocationEvent(
364365
agent=agent,
365366
selected_tool=selected_tool,
@@ -377,7 +378,7 @@ async def run_tool(agent: "Agent", tool_use: ToolUse, kwargs: dict[str, Any]) ->
377378
"status": "error",
378379
"content": [{"text": f"Error: {str(e)}"}],
379380
}
380-
after_event = get_registry(agent).invoke_callbacks(
381+
after_event = agent.hooks.invoke_callbacks(
381382
AfterToolInvocationEvent(
382383
agent=agent,
383384
selected_tool=selected_tool,
@@ -454,7 +455,7 @@ def tool_handler(tool_use: ToolUse) -> ToolGenerator:
454455
}
455456

456457
agent.messages.append(tool_result_message)
457-
get_registry(agent).invoke_callbacks(MessageAddedEvent(agent=agent, message=tool_result_message))
458+
agent.hooks.invoke_callbacks(MessageAddedEvent(agent=agent, message=tool_result_message))
458459
yield {"callback": {"message": tool_result_message}}
459460

460461
if cycle_span:
Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,15 @@
1-
"""Typed hook system for extending agent functionality.
2-
3-
This module provides a composable mechanism for building objects that can hook
4-
into specific events during the agent lifecycle. The hook system enables both
5-
built-in SDK components and user code to react to or modify agent behavior
6-
through strongly-typed event callbacks.
7-
8-
Example Usage:
9-
```python
10-
from strands.hooks import HookProvider, HookRegistry
11-
from strands.hooks.events import StartRequestEvent, EndRequestEvent
12-
13-
class LoggingHooks(HookProvider):
14-
def register_hooks(self, registry: HookRegistry) -> None:
15-
registry.add_callback(StartRequestEvent, self.log_start)
16-
registry.add_callback(EndRequestEvent, self.log_end)
17-
18-
def log_start(self, event: StartRequestEvent) -> None:
19-
print(f"Request started for {event.agent.name}")
20-
21-
def log_end(self, event: EndRequestEvent) -> None:
22-
print(f"Request completed for {event.agent.name}")
23-
24-
# Use with agent
25-
agent = Agent(hooks=[LoggingHooks()])
26-
```
27-
28-
This replaces the older callback_handler approach with a more composable,
29-
type-safe system that supports multiple subscribers per event type.
30-
"""
1+
"""Experimental hook functionality that has not yet reached stability."""
312

323
from .events import (
33-
AfterInvocationEvent,
344
AfterModelInvocationEvent,
355
AfterToolInvocationEvent,
36-
AgentInitializedEvent,
37-
BeforeInvocationEvent,
386
BeforeModelInvocationEvent,
397
BeforeToolInvocationEvent,
40-
MessageAddedEvent,
418
)
42-
from .registry import HookCallback, HookEvent, HookProvider, HookRegistry, get_registry
439

4410
__all__ = [
45-
"AgentInitializedEvent",
46-
"BeforeInvocationEvent",
47-
"AfterInvocationEvent",
48-
"BeforeModelInvocationEvent",
49-
"AfterModelInvocationEvent",
5011
"BeforeToolInvocationEvent",
5112
"AfterToolInvocationEvent",
52-
"MessageAddedEvent",
53-
"HookEvent",
54-
"HookProvider",
55-
"HookCallback",
56-
"HookRegistry",
57-
"get_registry",
13+
"BeforeModelInvocationEvent",
14+
"AfterModelInvocationEvent",
5815
]

src/strands/experimental/hooks/events.py

Lines changed: 2 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,15 @@
1-
"""Hook events emitted as part of invoking Agents.
1+
"""Experimental hook events emitted as part of invoking Agents.
22
33
This module defines the events that are emitted as Agents run through the lifecycle of a request.
44
"""
55

66
from dataclasses import dataclass
77
from typing import Any, Optional
88

9+
from ...hooks import HookEvent
910
from ...types.content import Message
1011
from ...types.streaming import StopReason
1112
from ...types.tools import AgentTool, ToolResult, ToolUse
12-
from .registry import HookEvent
13-
14-
15-
@dataclass
16-
class AgentInitializedEvent(HookEvent):
17-
"""Event triggered when an agent has finished initialization.
18-
19-
This event is fired after the agent has been fully constructed and all
20-
built-in components have been initialized. Hook providers can use this
21-
event to perform setup tasks that require a fully initialized agent.
22-
"""
23-
24-
pass
25-
26-
27-
@dataclass
28-
class BeforeInvocationEvent(HookEvent):
29-
"""Event triggered at the beginning of a new agent request.
30-
31-
This event is fired before the agent begins processing a new user request,
32-
before any model inference or tool execution occurs. Hook providers can
33-
use this event to perform request-level setup, logging, or validation.
34-
35-
This event is triggered at the beginning of the following api calls:
36-
- Agent.__call__
37-
- Agent.stream_async
38-
- Agent.structured_output
39-
"""
40-
41-
pass
42-
43-
44-
@dataclass
45-
class AfterInvocationEvent(HookEvent):
46-
"""Event triggered at the end of an agent request.
47-
48-
This event is fired after the agent has completed processing a request,
49-
regardless of whether it completed successfully or encountered an error.
50-
Hook providers can use this event for cleanup, logging, or state persistence.
51-
52-
Note: This event uses reverse callback ordering, meaning callbacks registered
53-
later will be invoked first during cleanup.
54-
55-
This event is triggered at the end of the following api calls:
56-
- Agent.__call__
57-
- Agent.stream_async
58-
- Agent.structured_output
59-
"""
60-
61-
@property
62-
def should_reverse_callbacks(self) -> bool:
63-
"""True to invoke callbacks in reverse order."""
64-
return True
6513

6614

6715
@dataclass
@@ -173,22 +121,3 @@ class ModelStopResponse:
173121
def should_reverse_callbacks(self) -> bool:
174122
"""True to invoke callbacks in reverse order."""
175123
return True
176-
177-
178-
@dataclass
179-
class MessageAddedEvent(HookEvent):
180-
"""Event triggered when a message is added to the agent's conversation.
181-
182-
This event is fired whenever the agent adds a new message to its internal
183-
message history, including user messages, assistant responses, and tool
184-
results. Hook providers can use this event for logging, monitoring, or
185-
implementing custom message processing logic.
186-
187-
Note: This event is only triggered for messages added by the framework
188-
itself, not for messages manually added by tools or external code.
189-
190-
Attributes:
191-
message: The message that was added to the conversation history.
192-
"""
193-
194-
message: Message

src/strands/hooks/__init__.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Typed hook system for extending agent functionality.
2+
3+
This module provides a composable mechanism for building objects that can hook
4+
into specific events during the agent lifecycle. The hook system enables both
5+
built-in SDK components and user code to react to or modify agent behavior
6+
through strongly-typed event callbacks.
7+
8+
Example Usage:
9+
```python
10+
from strands.hooks import HookProvider, HookRegistry
11+
from strands.hooks.events import StartRequestEvent, EndRequestEvent
12+
13+
class LoggingHooks(HookProvider):
14+
def register_hooks(self, registry: HookRegistry) -> None:
15+
registry.add_callback(StartRequestEvent, self.log_start)
16+
registry.add_callback(EndRequestEvent, self.log_end)
17+
18+
def log_start(self, event: StartRequestEvent) -> None:
19+
print(f"Request started for {event.agent.name}")
20+
21+
def log_end(self, event: EndRequestEvent) -> None:
22+
print(f"Request completed for {event.agent.name}")
23+
24+
# Use with agent
25+
agent = Agent(hooks=[LoggingHooks()])
26+
```
27+
28+
This replaces the older callback_handler approach with a more composable,
29+
type-safe system that supports multiple subscribers per event type.
30+
"""
31+
32+
from .events import (
33+
AfterInvocationEvent,
34+
AgentInitializedEvent,
35+
BeforeInvocationEvent,
36+
MessageAddedEvent,
37+
)
38+
from .registry import HookCallback, HookEvent, HookProvider, HookRegistry
39+
40+
__all__ = [
41+
"AgentInitializedEvent",
42+
"BeforeInvocationEvent",
43+
"AfterInvocationEvent",
44+
"MessageAddedEvent",
45+
"HookEvent",
46+
"HookProvider",
47+
"HookCallback",
48+
"HookRegistry",
49+
]

0 commit comments

Comments
 (0)