Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions src/strands/event_loop/event_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@

from opentelemetry import trace as trace_api

from ..experimental.hooks import (
AfterModelInvocationEvent,
BeforeModelInvocationEvent,
)
from ..hooks import (
MessageAddedEvent,
)
from ..hooks import AfterModelCallEvent, BeforeModelCallEvent, MessageAddedEvent
from ..telemetry.metrics import Trace
from ..telemetry.tracer import get_tracer
from ..tools._validator import validate_and_prepare_tools
Expand Down Expand Up @@ -133,7 +127,7 @@ async def event_loop_cycle(agent: "Agent", invocation_state: dict[str, Any]) ->
)
with trace_api.use_span(model_invoke_span):
agent.hooks.invoke_callbacks(
BeforeModelInvocationEvent(
BeforeModelCallEvent(
agent=agent,
)
)
Expand All @@ -149,9 +143,9 @@ async def event_loop_cycle(agent: "Agent", invocation_state: dict[str, Any]) ->
invocation_state.setdefault("request_state", {})

agent.hooks.invoke_callbacks(
AfterModelInvocationEvent(
AfterModelCallEvent(
agent=agent,
stop_response=AfterModelInvocationEvent.ModelStopResponse(
stop_response=AfterModelCallEvent.ModelStopResponse(
stop_reason=stop_reason,
message=message,
),
Expand All @@ -170,7 +164,7 @@ async def event_loop_cycle(agent: "Agent", invocation_state: dict[str, Any]) ->
tracer.end_span_with_error(model_invoke_span, str(e), e)

agent.hooks.invoke_callbacks(
AfterModelInvocationEvent(
AfterModelCallEvent(
agent=agent,
exception=e,
)
Expand Down
134 changes: 16 additions & 118 deletions src/strands/experimental/hooks/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,121 +3,19 @@
This module defines the events that are emitted as Agents run through the lifecycle of a request.
"""

from dataclasses import dataclass
from typing import Any, Optional

from ...hooks import HookEvent
from ...types.content import Message
from ...types.streaming import StopReason
from ...types.tools import AgentTool, ToolResult, ToolUse


@dataclass
class BeforeToolInvocationEvent(HookEvent):
"""Event triggered before a tool is invoked.

This event is fired just before the agent executes a tool, allowing hook
providers to inspect, modify, or replace the tool that will be executed.
The selected_tool can be modified by hook callbacks to change which tool
gets executed.

Attributes:
selected_tool: The tool that will be invoked. Can be modified by hooks
to change which tool gets executed. This may be None if tool lookup failed.
tool_use: The tool parameters that will be passed to selected_tool.
invocation_state: Keyword arguments that will be passed to the tool.
"""

selected_tool: Optional[AgentTool]
tool_use: ToolUse
invocation_state: dict[str, Any]

def _can_write(self, name: str) -> bool:
return name in ["selected_tool", "tool_use"]


@dataclass
class AfterToolInvocationEvent(HookEvent):
"""Event triggered after a tool invocation completes.

This event is fired after the agent has finished executing a tool,
regardless of whether the execution was successful or resulted in an error.
Hook providers can use this event for cleanup, logging, or post-processing.

Note: This event uses reverse callback ordering, meaning callbacks registered
later will be invoked first during cleanup.

Attributes:
selected_tool: The tool that was invoked. It may be None if tool lookup failed.
tool_use: The tool parameters that were passed to the tool invoked.
invocation_state: Keyword arguments that were passed to the tool
result: The result of the tool invocation. Either a ToolResult on success
or an Exception if the tool execution failed.
"""

selected_tool: Optional[AgentTool]
tool_use: ToolUse
invocation_state: dict[str, Any]
result: ToolResult
exception: Optional[Exception] = None

def _can_write(self, name: str) -> bool:
return name == "result"

@property
def should_reverse_callbacks(self) -> bool:
"""True to invoke callbacks in reverse order."""
return True


@dataclass
class BeforeModelInvocationEvent(HookEvent):
"""Event triggered before the model is invoked.

This event is fired just before the agent calls the model for inference,
allowing hook providers to inspect or modify the messages and configuration
that will be sent to the model.

Note: This event is not fired for invocations to structured_output.
"""

pass


@dataclass
class AfterModelInvocationEvent(HookEvent):
"""Event triggered after the model invocation completes.

This event is fired after the agent has finished calling the model,
regardless of whether the invocation was successful or resulted in an error.
Hook providers can use this event for cleanup, logging, or post-processing.

Note: This event uses reverse callback ordering, meaning callbacks registered
later will be invoked first during cleanup.

Note: This event is not fired for invocations to structured_output.

Attributes:
stop_response: The model response data if invocation was successful, None if failed.
exception: Exception if the model invocation failed, None if successful.
"""

@dataclass
class ModelStopResponse:
"""Model response data from successful invocation.

Attributes:
stop_reason: The reason the model stopped generating.
message: The generated message from the model.
"""

message: Message
stop_reason: StopReason

stop_response: Optional[ModelStopResponse] = None
exception: Optional[Exception] = None

@property
def should_reverse_callbacks(self) -> bool:
"""True to invoke callbacks in reverse order."""
return True
import warnings
from typing import TypeAlias

from ...hooks.events import AfterModelCallEvent, AfterToolCallEvent, BeforeModelCallEvent, BeforeToolCallEvent

warnings.warn(
"These events have been moved to production with updated names. Use BeforeModelCallEvent, "
"AfterModelCallEvent, BeforeToolCallEvent, and AfterToolCallEvent from strands.hooks instead.",
DeprecationWarning,
stacklevel=2,
)

BeforeToolInvocationEvent: TypeAlias = BeforeToolCallEvent
AfterToolInvocationEvent: TypeAlias = AfterToolCallEvent
BeforeModelInvocationEvent: TypeAlias = BeforeModelCallEvent
AfterModelInvocationEvent: TypeAlias = AfterModelCallEvent
8 changes: 8 additions & 0 deletions src/strands/hooks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,23 @@ def log_end(self, event: AfterInvocationEvent) -> None:

from .events import (
AfterInvocationEvent,
AfterModelCallEvent,
AfterToolCallEvent,
AgentInitializedEvent,
BeforeInvocationEvent,
BeforeModelCallEvent,
BeforeToolCallEvent,
MessageAddedEvent,
)
from .registry import HookCallback, HookEvent, HookProvider, HookRegistry

__all__ = [
"AgentInitializedEvent",
"BeforeInvocationEvent",
"BeforeToolCallEvent",
"AfterToolCallEvent",
"BeforeModelCallEvent",
"AfterModelCallEvent",
"AfterInvocationEvent",
"MessageAddedEvent",
"HookEvent",
Expand Down
114 changes: 114 additions & 0 deletions src/strands/hooks/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
"""

from dataclasses import dataclass
from typing import Any, Optional

from ..types.content import Message
from ..types.streaming import StopReason
from ..types.tools import AgentTool, ToolResult, ToolUse
from .registry import HookEvent


Expand Down Expand Up @@ -78,3 +81,114 @@ class MessageAddedEvent(HookEvent):
"""

message: Message


@dataclass
class BeforeToolCallEvent(HookEvent):
"""Event triggered before a tool is invoked.

This event is fired just before the agent executes a tool, allowing hook
providers to inspect, modify, or replace the tool that will be executed.
The selected_tool can be modified by hook callbacks to change which tool
gets executed.

Attributes:
selected_tool: The tool that will be invoked. Can be modified by hooks
to change which tool gets executed. This may be None if tool lookup failed.
tool_use: The tool parameters that will be passed to selected_tool.
invocation_state: Keyword arguments that will be passed to the tool.
"""

selected_tool: Optional[AgentTool]
tool_use: ToolUse
invocation_state: dict[str, Any]

def _can_write(self, name: str) -> bool:
return name in ["selected_tool", "tool_use"]


@dataclass
class AfterToolCallEvent(HookEvent):
"""Event triggered after a tool invocation completes.

This event is fired after the agent has finished executing a tool,
regardless of whether the execution was successful or resulted in an error.
Hook providers can use this event for cleanup, logging, or post-processing.

Note: This event uses reverse callback ordering, meaning callbacks registered
later will be invoked first during cleanup.

Attributes:
selected_tool: The tool that was invoked. It may be None if tool lookup failed.
tool_use: The tool parameters that were passed to the tool invoked.
invocation_state: Keyword arguments that were passed to the tool
result: The result of the tool invocation. Either a ToolResult on success
or an Exception if the tool execution failed.
"""

selected_tool: Optional[AgentTool]
tool_use: ToolUse
invocation_state: dict[str, Any]
result: ToolResult
exception: Optional[Exception] = None

def _can_write(self, name: str) -> bool:
return name == "result"

@property
def should_reverse_callbacks(self) -> bool:
"""True to invoke callbacks in reverse order."""
return True


@dataclass
class BeforeModelCallEvent(HookEvent):
"""Event triggered before the model is invoked.

This event is fired just before the agent calls the model for inference,
allowing hook providers to inspect or modify the messages and configuration
that will be sent to the model.

Note: This event is not fired for invocations to structured_output.
"""

pass


@dataclass
class AfterModelCallEvent(HookEvent):
"""Event triggered after the model invocation completes.

This event is fired after the agent has finished calling the model,
regardless of whether the invocation was successful or resulted in an error.
Hook providers can use this event for cleanup, logging, or post-processing.

Note: This event uses reverse callback ordering, meaning callbacks registered
later will be invoked first during cleanup.

Note: This event is not fired for invocations to structured_output.

Attributes:
stop_response: The model response data if invocation was successful, None if failed.
exception: Exception if the model invocation failed, None if successful.
"""

@dataclass
class ModelStopResponse:
"""Model response data from successful invocation.

Attributes:
stop_reason: The reason the model stopped generating.
message: The generated message from the model.
"""

message: Message
stop_reason: StopReason

stop_response: Optional[ModelStopResponse] = None
exception: Optional[Exception] = None

@property
def should_reverse_callbacks(self) -> bool:
"""True to invoke callbacks in reverse order."""
return True
3 changes: 2 additions & 1 deletion src/strands/hooks/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

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

## Paired Events

Expand All @@ -17,4 +18,4 @@

## Writable Properties

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.
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.
10 changes: 5 additions & 5 deletions src/strands/tools/executors/_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from opentelemetry import trace as trace_api

from ...experimental.hooks import AfterToolInvocationEvent, BeforeToolInvocationEvent
from ...hooks import AfterToolCallEvent, BeforeToolCallEvent
from ...telemetry.metrics import Trace
from ...telemetry.tracer import get_tracer
from ...types._events import ToolResultEvent, ToolStreamEvent, TypedEvent
Expand Down Expand Up @@ -73,7 +73,7 @@ async def _stream(
)

before_event = agent.hooks.invoke_callbacks(
BeforeToolInvocationEvent(
BeforeToolCallEvent(
agent=agent,
selected_tool=tool_func,
tool_use=tool_use,
Expand Down Expand Up @@ -106,7 +106,7 @@ async def _stream(
"content": [{"text": f"Unknown tool: {tool_name}"}],
}
after_event = agent.hooks.invoke_callbacks(
AfterToolInvocationEvent(
AfterToolCallEvent(
agent=agent,
selected_tool=selected_tool,
tool_use=tool_use,
Expand Down Expand Up @@ -137,7 +137,7 @@ async def _stream(
result = cast(ToolResult, event)

after_event = agent.hooks.invoke_callbacks(
AfterToolInvocationEvent(
AfterToolCallEvent(
agent=agent,
selected_tool=selected_tool,
tool_use=tool_use,
Expand All @@ -157,7 +157,7 @@ async def _stream(
"content": [{"text": f"Error: {str(e)}"}],
}
after_event = agent.hooks.invoke_callbacks(
AfterToolInvocationEvent(
AfterToolCallEvent(
agent=agent,
selected_tool=selected_tool,
tool_use=tool_use,
Expand Down
Loading
Loading