Skip to content

Commit 99cd49b

Browse files
authored
feat(hooks): Mark ModelCall and ToolCall events as non-experimental (#926)
feat(hooks): Mark ModelCall and ToolCall events as non-experimental Addresses #667 - we haven't seen any huge deficiencies in these and we need to stabilize hooks As part of this change we renamed the events to clarify naming and reduce verbosity: - BeforeToolInvocationEvent -> BeforeToolCallEvent - AfterToolInvocationEvent -> AfterToolCallEvent - BeforeModelInvocationEvent -> BeforeModelCallEvent - AfterModelInvocationEvent -> AfterModelCallEvent Part of the motivation of the rename is to avoid confusion with BeforeInvocationEvent and BeforeToolInvocation & BeforeModelInvocationEvent, as we've seen folks confusing them quite a bit. These changes are backwards compatible as the experimental events still exist; we can remove those after a release or two --------- Co-authored-by: Mackenzie Zastrow <[email protected]>
1 parent ecd9eab commit 99cd49b

File tree

18 files changed

+417
-267
lines changed

18 files changed

+417
-267
lines changed

src/strands/event_loop/event_loop.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,7 @@
1515

1616
from opentelemetry import trace as trace_api
1717

18-
from ..experimental.hooks import (
19-
AfterModelInvocationEvent,
20-
BeforeModelInvocationEvent,
21-
)
22-
from ..hooks import (
23-
MessageAddedEvent,
24-
)
18+
from ..hooks import AfterModelCallEvent, BeforeModelCallEvent, MessageAddedEvent
2519
from ..telemetry.metrics import Trace
2620
from ..telemetry.tracer import get_tracer
2721
from ..tools._validator import validate_and_prepare_tools
@@ -133,7 +127,7 @@ async def event_loop_cycle(agent: "Agent", invocation_state: dict[str, Any]) ->
133127
)
134128
with trace_api.use_span(model_invoke_span):
135129
agent.hooks.invoke_callbacks(
136-
BeforeModelInvocationEvent(
130+
BeforeModelCallEvent(
137131
agent=agent,
138132
)
139133
)
@@ -149,9 +143,9 @@ async def event_loop_cycle(agent: "Agent", invocation_state: dict[str, Any]) ->
149143
invocation_state.setdefault("request_state", {})
150144

151145
agent.hooks.invoke_callbacks(
152-
AfterModelInvocationEvent(
146+
AfterModelCallEvent(
153147
agent=agent,
154-
stop_response=AfterModelInvocationEvent.ModelStopResponse(
148+
stop_response=AfterModelCallEvent.ModelStopResponse(
155149
stop_reason=stop_reason,
156150
message=message,
157151
),
@@ -170,7 +164,7 @@ async def event_loop_cycle(agent: "Agent", invocation_state: dict[str, Any]) ->
170164
tracer.end_span_with_error(model_invoke_span, str(e), e)
171165

172166
agent.hooks.invoke_callbacks(
173-
AfterModelInvocationEvent(
167+
AfterModelCallEvent(
174168
agent=agent,
175169
exception=e,
176170
)

src/strands/experimental/hooks/events.py

Lines changed: 16 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -3,121 +3,19 @@
33
This module defines the events that are emitted as Agents run through the lifecycle of a request.
44
"""
55

6-
from dataclasses import dataclass
7-
from typing import Any, Optional
8-
9-
from ...hooks import HookEvent
10-
from ...types.content import Message
11-
from ...types.streaming import StopReason
12-
from ...types.tools import AgentTool, ToolResult, ToolUse
13-
14-
15-
@dataclass
16-
class BeforeToolInvocationEvent(HookEvent):
17-
"""Event triggered before a tool is invoked.
18-
19-
This event is fired just before the agent executes a tool, allowing hook
20-
providers to inspect, modify, or replace the tool that will be executed.
21-
The selected_tool can be modified by hook callbacks to change which tool
22-
gets executed.
23-
24-
Attributes:
25-
selected_tool: The tool that will be invoked. Can be modified by hooks
26-
to change which tool gets executed. This may be None if tool lookup failed.
27-
tool_use: The tool parameters that will be passed to selected_tool.
28-
invocation_state: Keyword arguments that will be passed to the tool.
29-
"""
30-
31-
selected_tool: Optional[AgentTool]
32-
tool_use: ToolUse
33-
invocation_state: dict[str, Any]
34-
35-
def _can_write(self, name: str) -> bool:
36-
return name in ["selected_tool", "tool_use"]
37-
38-
39-
@dataclass
40-
class AfterToolInvocationEvent(HookEvent):
41-
"""Event triggered after a tool invocation completes.
42-
43-
This event is fired after the agent has finished executing a tool,
44-
regardless of whether the execution was successful or resulted in an error.
45-
Hook providers can use this event for cleanup, logging, or post-processing.
46-
47-
Note: This event uses reverse callback ordering, meaning callbacks registered
48-
later will be invoked first during cleanup.
49-
50-
Attributes:
51-
selected_tool: The tool that was invoked. It may be None if tool lookup failed.
52-
tool_use: The tool parameters that were passed to the tool invoked.
53-
invocation_state: Keyword arguments that were passed to the tool
54-
result: The result of the tool invocation. Either a ToolResult on success
55-
or an Exception if the tool execution failed.
56-
"""
57-
58-
selected_tool: Optional[AgentTool]
59-
tool_use: ToolUse
60-
invocation_state: dict[str, Any]
61-
result: ToolResult
62-
exception: Optional[Exception] = None
63-
64-
def _can_write(self, name: str) -> bool:
65-
return name == "result"
66-
67-
@property
68-
def should_reverse_callbacks(self) -> bool:
69-
"""True to invoke callbacks in reverse order."""
70-
return True
71-
72-
73-
@dataclass
74-
class BeforeModelInvocationEvent(HookEvent):
75-
"""Event triggered before the model is invoked.
76-
77-
This event is fired just before the agent calls the model for inference,
78-
allowing hook providers to inspect or modify the messages and configuration
79-
that will be sent to the model.
80-
81-
Note: This event is not fired for invocations to structured_output.
82-
"""
83-
84-
pass
85-
86-
87-
@dataclass
88-
class AfterModelInvocationEvent(HookEvent):
89-
"""Event triggered after the model invocation completes.
90-
91-
This event is fired after the agent has finished calling the model,
92-
regardless of whether the invocation was successful or resulted in an error.
93-
Hook providers can use this event for cleanup, logging, or post-processing.
94-
95-
Note: This event uses reverse callback ordering, meaning callbacks registered
96-
later will be invoked first during cleanup.
97-
98-
Note: This event is not fired for invocations to structured_output.
99-
100-
Attributes:
101-
stop_response: The model response data if invocation was successful, None if failed.
102-
exception: Exception if the model invocation failed, None if successful.
103-
"""
104-
105-
@dataclass
106-
class ModelStopResponse:
107-
"""Model response data from successful invocation.
108-
109-
Attributes:
110-
stop_reason: The reason the model stopped generating.
111-
message: The generated message from the model.
112-
"""
113-
114-
message: Message
115-
stop_reason: StopReason
116-
117-
stop_response: Optional[ModelStopResponse] = None
118-
exception: Optional[Exception] = None
119-
120-
@property
121-
def should_reverse_callbacks(self) -> bool:
122-
"""True to invoke callbacks in reverse order."""
123-
return True
6+
import warnings
7+
from typing import TypeAlias
8+
9+
from ...hooks.events import AfterModelCallEvent, AfterToolCallEvent, BeforeModelCallEvent, BeforeToolCallEvent
10+
11+
warnings.warn(
12+
"These events have been moved to production with updated names. Use BeforeModelCallEvent, "
13+
"AfterModelCallEvent, BeforeToolCallEvent, and AfterToolCallEvent from strands.hooks instead.",
14+
DeprecationWarning,
15+
stacklevel=2,
16+
)
17+
18+
BeforeToolInvocationEvent: TypeAlias = BeforeToolCallEvent
19+
AfterToolInvocationEvent: TypeAlias = AfterToolCallEvent
20+
BeforeModelInvocationEvent: TypeAlias = BeforeModelCallEvent
21+
AfterModelInvocationEvent: TypeAlias = AfterModelCallEvent

src/strands/hooks/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,23 @@ def log_end(self, event: AfterInvocationEvent) -> None:
3131

3232
from .events import (
3333
AfterInvocationEvent,
34+
AfterModelCallEvent,
35+
AfterToolCallEvent,
3436
AgentInitializedEvent,
3537
BeforeInvocationEvent,
38+
BeforeModelCallEvent,
39+
BeforeToolCallEvent,
3640
MessageAddedEvent,
3741
)
3842
from .registry import HookCallback, HookEvent, HookProvider, HookRegistry
3943

4044
__all__ = [
4145
"AgentInitializedEvent",
4246
"BeforeInvocationEvent",
47+
"BeforeToolCallEvent",
48+
"AfterToolCallEvent",
49+
"BeforeModelCallEvent",
50+
"AfterModelCallEvent",
4351
"AfterInvocationEvent",
4452
"MessageAddedEvent",
4553
"HookEvent",

src/strands/hooks/events.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
"""
55

66
from dataclasses import dataclass
7+
from typing import Any, Optional
78

89
from ..types.content import Message
10+
from ..types.streaming import StopReason
11+
from ..types.tools import AgentTool, ToolResult, ToolUse
912
from .registry import HookEvent
1013

1114

@@ -78,3 +81,114 @@ class MessageAddedEvent(HookEvent):
7881
"""
7982

8083
message: Message
84+
85+
86+
@dataclass
87+
class BeforeToolCallEvent(HookEvent):
88+
"""Event triggered before a tool is invoked.
89+
90+
This event is fired just before the agent executes a tool, allowing hook
91+
providers to inspect, modify, or replace the tool that will be executed.
92+
The selected_tool can be modified by hook callbacks to change which tool
93+
gets executed.
94+
95+
Attributes:
96+
selected_tool: The tool that will be invoked. Can be modified by hooks
97+
to change which tool gets executed. This may be None if tool lookup failed.
98+
tool_use: The tool parameters that will be passed to selected_tool.
99+
invocation_state: Keyword arguments that will be passed to the tool.
100+
"""
101+
102+
selected_tool: Optional[AgentTool]
103+
tool_use: ToolUse
104+
invocation_state: dict[str, Any]
105+
106+
def _can_write(self, name: str) -> bool:
107+
return name in ["selected_tool", "tool_use"]
108+
109+
110+
@dataclass
111+
class AfterToolCallEvent(HookEvent):
112+
"""Event triggered after a tool invocation completes.
113+
114+
This event is fired after the agent has finished executing a tool,
115+
regardless of whether the execution was successful or resulted in an error.
116+
Hook providers can use this event for cleanup, logging, or post-processing.
117+
118+
Note: This event uses reverse callback ordering, meaning callbacks registered
119+
later will be invoked first during cleanup.
120+
121+
Attributes:
122+
selected_tool: The tool that was invoked. It may be None if tool lookup failed.
123+
tool_use: The tool parameters that were passed to the tool invoked.
124+
invocation_state: Keyword arguments that were passed to the tool
125+
result: The result of the tool invocation. Either a ToolResult on success
126+
or an Exception if the tool execution failed.
127+
"""
128+
129+
selected_tool: Optional[AgentTool]
130+
tool_use: ToolUse
131+
invocation_state: dict[str, Any]
132+
result: ToolResult
133+
exception: Optional[Exception] = None
134+
135+
def _can_write(self, name: str) -> bool:
136+
return name == "result"
137+
138+
@property
139+
def should_reverse_callbacks(self) -> bool:
140+
"""True to invoke callbacks in reverse order."""
141+
return True
142+
143+
144+
@dataclass
145+
class BeforeModelCallEvent(HookEvent):
146+
"""Event triggered before the model is invoked.
147+
148+
This event is fired just before the agent calls the model for inference,
149+
allowing hook providers to inspect or modify the messages and configuration
150+
that will be sent to the model.
151+
152+
Note: This event is not fired for invocations to structured_output.
153+
"""
154+
155+
pass
156+
157+
158+
@dataclass
159+
class AfterModelCallEvent(HookEvent):
160+
"""Event triggered after the model invocation completes.
161+
162+
This event is fired after the agent has finished calling the model,
163+
regardless of whether the invocation was successful or resulted in an error.
164+
Hook providers can use this event for cleanup, logging, or post-processing.
165+
166+
Note: This event uses reverse callback ordering, meaning callbacks registered
167+
later will be invoked first during cleanup.
168+
169+
Note: This event is not fired for invocations to structured_output.
170+
171+
Attributes:
172+
stop_response: The model response data if invocation was successful, None if failed.
173+
exception: Exception if the model invocation failed, None if successful.
174+
"""
175+
176+
@dataclass
177+
class ModelStopResponse:
178+
"""Model response data from successful invocation.
179+
180+
Attributes:
181+
stop_reason: The reason the model stopped generating.
182+
message: The generated message from the model.
183+
"""
184+
185+
message: Message
186+
stop_reason: StopReason
187+
188+
stop_response: Optional[ModelStopResponse] = None
189+
exception: Optional[Exception] = None
190+
191+
@property
192+
def should_reverse_callbacks(self) -> bool:
193+
"""True to invoke callbacks in reverse order."""
194+
return True

src/strands/hooks/rules.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
- All hook events have a suffix of `Event`
1111
- Paired events follow the naming convention of `Before{Item}Event` and `After{Item}Event`
12+
- Pre actions in the name. i.e. prefer `BeforeToolCallEvent` over `BeforeToolEvent`.
1213

1314
## Paired Events
1415

@@ -17,4 +18,4 @@
1718

1819
## Writable Properties
1920

20-
For events with writable properties, those values are re-read after invoking the hook callbacks and used in subsequent processing. For example, `BeforeToolInvocationEvent.selected_tool` is writable - after invoking the callback for `BeforeToolInvocationEvent`, the `selected_tool` takes effect for the tool call.
21+
For events with writable properties, those values are re-read after invoking the hook callbacks and used in subsequent processing. For example, `BeforeToolEvent.selected_tool` is writable - after invoking the callback for `BeforeToolEvent`, the `selected_tool` takes effect for the tool call.

src/strands/tools/executors/_executor.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from opentelemetry import trace as trace_api
1313

14-
from ...experimental.hooks import AfterToolInvocationEvent, BeforeToolInvocationEvent
14+
from ...hooks import AfterToolCallEvent, BeforeToolCallEvent
1515
from ...telemetry.metrics import Trace
1616
from ...telemetry.tracer import get_tracer
1717
from ...types._events import ToolResultEvent, ToolStreamEvent, TypedEvent
@@ -73,7 +73,7 @@ async def _stream(
7373
)
7474

7575
before_event = agent.hooks.invoke_callbacks(
76-
BeforeToolInvocationEvent(
76+
BeforeToolCallEvent(
7777
agent=agent,
7878
selected_tool=tool_func,
7979
tool_use=tool_use,
@@ -106,7 +106,7 @@ async def _stream(
106106
"content": [{"text": f"Unknown tool: {tool_name}"}],
107107
}
108108
after_event = agent.hooks.invoke_callbacks(
109-
AfterToolInvocationEvent(
109+
AfterToolCallEvent(
110110
agent=agent,
111111
selected_tool=selected_tool,
112112
tool_use=tool_use,
@@ -137,7 +137,7 @@ async def _stream(
137137
result = cast(ToolResult, event)
138138

139139
after_event = agent.hooks.invoke_callbacks(
140-
AfterToolInvocationEvent(
140+
AfterToolCallEvent(
141141
agent=agent,
142142
selected_tool=selected_tool,
143143
tool_use=tool_use,
@@ -157,7 +157,7 @@ async def _stream(
157157
"content": [{"text": f"Error: {str(e)}"}],
158158
}
159159
after_event = agent.hooks.invoke_callbacks(
160-
AfterToolInvocationEvent(
160+
AfterToolCallEvent(
161161
agent=agent,
162162
selected_tool=selected_tool,
163163
tool_use=tool_use,

0 commit comments

Comments
 (0)