From 05dd45f35325d571d80f63a08667213c2d47b2bc Mon Sep 17 00:00:00 2001 From: Jack Yuan Date: Thu, 15 Jan 2026 16:55:51 -0500 Subject: [PATCH 1/5] feat: graduate multiagent hook events from experimental --- .../experimental/hooks/multiagent/__init__.py | 4 +- .../experimental/hooks/multiagent/events.py | 145 ++++-------------- src/strands/hooks/multiagent/__init__.py | 20 +++ src/strands/hooks/multiagent/events.py | 118 ++++++++++++++ src/strands/multiagent/graph.py | 4 +- src/strands/multiagent/swarm.py | 4 +- src/strands/session/session_manager.py | 4 +- .../fixtures/mock_multiagent_hook_provider.py | 12 +- .../hooks/multiagent/test_events.py | 4 +- .../multiagent/test_multi_agent_hooks.py | 2 +- tests/strands/multiagent/conftest.py | 2 +- tests/strands/multiagent/test_graph.py | 2 +- tests/strands/multiagent/test_swarm.py | 2 +- tests_integ/hooks/multiagent/test_cancel.py | 2 +- tests_integ/hooks/multiagent/test_events.py | 4 +- .../interrupts/multiagent/test_hook.py | 2 +- tests_integ/test_multiagent_swarm.py | 2 +- 17 files changed, 194 insertions(+), 139 deletions(-) create mode 100644 src/strands/hooks/multiagent/__init__.py create mode 100644 src/strands/hooks/multiagent/events.py diff --git a/src/strands/experimental/hooks/multiagent/__init__.py b/src/strands/experimental/hooks/multiagent/__init__.py index d059d0da5..6755db7e4 100644 --- a/src/strands/experimental/hooks/multiagent/__init__.py +++ b/src/strands/experimental/hooks/multiagent/__init__.py @@ -1,6 +1,6 @@ -"""Multi-agent hook events and utilities. +"""Multi-agent hook events. -Provides event classes for hooking into multi-agent orchestrator lifecycle. +Deprecated: Use strands.hooks.multiagent instead. """ from .events import ( diff --git a/src/strands/experimental/hooks/multiagent/events.py b/src/strands/experimental/hooks/multiagent/events.py index fa881bf32..f640793e9 100644 --- a/src/strands/experimental/hooks/multiagent/events.py +++ b/src/strands/experimental/hooks/multiagent/events.py @@ -1,118 +1,35 @@ """Multi-agent execution lifecycle events for hook system integration. -These events are fired by orchestrators (Graph/Swarm) at key points so -hooks can persist, monitor, or debug execution. No intermediate state model -is used—hooks read from the orchestrator directly. +Deprecated: Use strands.hooks.multiagent instead. """ -import uuid -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -from typing_extensions import override - -from ....hooks import BaseHookEvent -from ....types.interrupt import _Interruptible - -if TYPE_CHECKING: - from ....multiagent.base import MultiAgentBase - - -@dataclass -class MultiAgentInitializedEvent(BaseHookEvent): - """Event triggered when multi-agent orchestrator initialized. - - Attributes: - source: The multi-agent orchestrator instance - invocation_state: Configuration that user passes in - """ - - source: "MultiAgentBase" - invocation_state: dict[str, Any] | None = None - - -@dataclass -class BeforeNodeCallEvent(BaseHookEvent, _Interruptible): - """Event triggered before individual node execution starts. - - Attributes: - source: The multi-agent orchestrator instance - node_id: ID of the node about to execute - invocation_state: Configuration that user passes in - cancel_node: A user defined message that when set, will cancel the node execution with status FAILED. - The message will be emitted under a MultiAgentNodeCancel event. If set to `True`, Strands will cancel the - node using a default cancel message. - """ - - source: "MultiAgentBase" - node_id: str - invocation_state: dict[str, Any] | None = None - cancel_node: bool | str = False - - def _can_write(self, name: str) -> bool: - return name in ["cancel_node"] - - @override - def _interrupt_id(self, name: str) -> str: - """Unique id for the interrupt. - - Args: - name: User defined name for the interrupt. - - Returns: - Interrupt id. - """ - node_id = uuid.uuid5(uuid.NAMESPACE_OID, self.node_id) - call_id = uuid.uuid5(uuid.NAMESPACE_OID, name) - return f"v1:before_node_call:{node_id}:{call_id}" - - -@dataclass -class AfterNodeCallEvent(BaseHookEvent): - """Event triggered after individual node execution completes. - - Attributes: - source: The multi-agent orchestrator instance - node_id: ID of the node that just completed execution - invocation_state: Configuration that user passes in - """ - - source: "MultiAgentBase" - node_id: str - invocation_state: dict[str, Any] | None = None - - @property - def should_reverse_callbacks(self) -> bool: - """True to invoke callbacks in reverse order.""" - return True - - -@dataclass -class BeforeMultiAgentInvocationEvent(BaseHookEvent): - """Event triggered before orchestrator execution starts. - - Attributes: - source: The multi-agent orchestrator instance - invocation_state: Configuration that user passes in - """ - - source: "MultiAgentBase" - invocation_state: dict[str, Any] | None = None - - -@dataclass -class AfterMultiAgentInvocationEvent(BaseHookEvent): - """Event triggered after orchestrator execution completes. - - Attributes: - source: The multi-agent orchestrator instance - invocation_state: Configuration that user passes in - """ - - source: "MultiAgentBase" - invocation_state: dict[str, Any] | None = None - - @property - def should_reverse_callbacks(self) -> bool: - """True to invoke callbacks in reverse order.""" - return True +import warnings +from typing import Any + +from ....hooks.multiagent.events import ( + AfterMultiAgentInvocationEvent, + AfterNodeCallEvent, + BeforeMultiAgentInvocationEvent, + BeforeNodeCallEvent, + MultiAgentInitializedEvent, +) + +_DEPRECATED_ALIASES = { + "MultiAgentInitializedEvent": MultiAgentInitializedEvent, + "BeforeNodeCallEvent": BeforeNodeCallEvent, + "BeforeMultiAgentInvocationEvent": BeforeMultiAgentInvocationEvent, + "AfterNodeCallEvent": AfterNodeCallEvent, + "AfterMultiAgentInvocationEvent": AfterMultiAgentInvocationEvent, +} + + +def __getattr__(name: str) -> Any: + if name in _DEPRECATED_ALIASES: + warnings.warn( + f"{name} has been moved to production with an updated name. " + f"Use {_DEPRECATED_ALIASES[name].__name__} from strands.hooks.multiagent instead.", + DeprecationWarning, + stacklevel=2, + ) + return _DEPRECATED_ALIASES[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/strands/hooks/multiagent/__init__.py b/src/strands/hooks/multiagent/__init__.py new file mode 100644 index 000000000..d059d0da5 --- /dev/null +++ b/src/strands/hooks/multiagent/__init__.py @@ -0,0 +1,20 @@ +"""Multi-agent hook events and utilities. + +Provides event classes for hooking into multi-agent orchestrator lifecycle. +""" + +from .events import ( + AfterMultiAgentInvocationEvent, + AfterNodeCallEvent, + BeforeMultiAgentInvocationEvent, + BeforeNodeCallEvent, + MultiAgentInitializedEvent, +) + +__all__ = [ + "AfterMultiAgentInvocationEvent", + "AfterNodeCallEvent", + "BeforeMultiAgentInvocationEvent", + "BeforeNodeCallEvent", + "MultiAgentInitializedEvent", +] diff --git a/src/strands/hooks/multiagent/events.py b/src/strands/hooks/multiagent/events.py new file mode 100644 index 000000000..c544d551f --- /dev/null +++ b/src/strands/hooks/multiagent/events.py @@ -0,0 +1,118 @@ +"""Multi-agent execution lifecycle events for hook system integration. + +These events are fired by orchestrators (Graph/Swarm) at key points so +hooks can persist, monitor, or debug execution. No intermediate state model +is used—hooks read from the orchestrator directly. +""" + +import uuid +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from typing_extensions import override + +from ...types.interrupt import _Interruptible +from ..registry import BaseHookEvent + +if TYPE_CHECKING: + from ...multiagent.base import MultiAgentBase + + +@dataclass +class MultiAgentInitializedEvent(BaseHookEvent): + """Event triggered when multi-agent orchestrator initialized. + + Attributes: + source: The multi-agent orchestrator instance + invocation_state: Configuration that user passes in + """ + + source: "MultiAgentBase" + invocation_state: dict[str, Any] | None = None + + +@dataclass +class BeforeNodeCallEvent(BaseHookEvent, _Interruptible): + """Event triggered before individual node execution starts. + + Attributes: + source: The multi-agent orchestrator instance + node_id: ID of the node about to execute + invocation_state: Configuration that user passes in + cancel_node: A user defined message that when set, will cancel the node execution with status FAILED. + The message will be emitted under a MultiAgentNodeCancel event. If set to `True`, Strands will cancel the + node using a default cancel message. + """ + + source: "MultiAgentBase" + node_id: str + invocation_state: dict[str, Any] | None = None + cancel_node: bool | str = False + + def _can_write(self, name: str) -> bool: + return name in ["cancel_node"] + + @override + def _interrupt_id(self, name: str) -> str: + """Unique id for the interrupt. + + Args: + name: User defined name for the interrupt. + + Returns: + Interrupt id. + """ + node_id = uuid.uuid5(uuid.NAMESPACE_OID, self.node_id) + call_id = uuid.uuid5(uuid.NAMESPACE_OID, name) + return f"v1:before_node_call:{node_id}:{call_id}" + + +@dataclass +class AfterNodeCallEvent(BaseHookEvent): + """Event triggered after individual node execution completes. + + Attributes: + source: The multi-agent orchestrator instance + node_id: ID of the node that just completed execution + invocation_state: Configuration that user passes in + """ + + source: "MultiAgentBase" + node_id: str + invocation_state: dict[str, Any] | None = None + + @property + def should_reverse_callbacks(self) -> bool: + """True to invoke callbacks in reverse order.""" + return True + + +@dataclass +class BeforeMultiAgentInvocationEvent(BaseHookEvent): + """Event triggered before orchestrator execution starts. + + Attributes: + source: The multi-agent orchestrator instance + invocation_state: Configuration that user passes in + """ + + source: "MultiAgentBase" + invocation_state: dict[str, Any] | None = None + + +@dataclass +class AfterMultiAgentInvocationEvent(BaseHookEvent): + """Event triggered after orchestrator execution completes. + + Attributes: + source: The multi-agent orchestrator instance + invocation_state: Configuration that user passes in + """ + + source: "MultiAgentBase" + invocation_state: dict[str, Any] | None = None + + @property + def should_reverse_callbacks(self) -> bool: + """True to invoke callbacks in reverse order.""" + return True diff --git a/src/strands/multiagent/graph.py b/src/strands/multiagent/graph.py index 6156d332c..c9ccc935c 100644 --- a/src/strands/multiagent/graph.py +++ b/src/strands/multiagent/graph.py @@ -26,14 +26,14 @@ from .._async import run_async from ..agent import Agent from ..agent.state import AgentState -from ..experimental.hooks.multiagent import ( +from ..hooks import HookProvider, HookRegistry +from ..hooks.multiagent import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, MultiAgentInitializedEvent, ) -from ..hooks import HookProvider, HookRegistry from ..session import SessionManager from ..telemetry import get_tracer from ..types._events import ( diff --git a/src/strands/multiagent/swarm.py b/src/strands/multiagent/swarm.py index 7eec49649..37fa62bdd 100644 --- a/src/strands/multiagent/swarm.py +++ b/src/strands/multiagent/swarm.py @@ -26,14 +26,14 @@ from .._async import run_async from ..agent import Agent from ..agent.state import AgentState -from ..experimental.hooks.multiagent import ( +from ..hooks import HookProvider, HookRegistry +from ..hooks.multiagent import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, MultiAgentInitializedEvent, ) -from ..hooks import HookProvider, HookRegistry from ..interrupt import Interrupt, _InterruptState from ..session import SessionManager from ..telemetry import get_tracer diff --git a/src/strands/session/session_manager.py b/src/strands/session/session_manager.py index ba4356089..756c90c00 100644 --- a/src/strands/session/session_manager.py +++ b/src/strands/session/session_manager.py @@ -9,12 +9,12 @@ BidiAgentInitializedEvent, BidiMessageAddedEvent, ) -from ..experimental.hooks.multiagent.events import ( +from ..hooks.events import AfterInvocationEvent, AgentInitializedEvent, MessageAddedEvent +from ..hooks.multiagent.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, MultiAgentInitializedEvent, ) -from ..hooks.events import AfterInvocationEvent, AgentInitializedEvent, MessageAddedEvent from ..hooks.registry import HookProvider, HookRegistry from ..types.content import Message diff --git a/tests/fixtures/mock_multiagent_hook_provider.py b/tests/fixtures/mock_multiagent_hook_provider.py index 727d28a48..9f3593789 100644 --- a/tests/fixtures/mock_multiagent_hook_provider.py +++ b/tests/fixtures/mock_multiagent_hook_provider.py @@ -1,16 +1,16 @@ from typing import Iterator, Literal, Tuple, Type -from strands.experimental.hooks.multiagent.events import ( - AfterMultiAgentInvocationEvent, - AfterNodeCallEvent, - BeforeNodeCallEvent, - MultiAgentInitializedEvent, -) from strands.hooks import ( HookEvent, HookProvider, HookRegistry, ) +from strands.hooks.multiagent.events import ( + AfterMultiAgentInvocationEvent, + AfterNodeCallEvent, + BeforeNodeCallEvent, + MultiAgentInitializedEvent, +) class MockMultiAgentHookProvider(HookProvider): diff --git a/tests/strands/experimental/hooks/multiagent/test_events.py b/tests/strands/experimental/hooks/multiagent/test_events.py index 6c4d7c4e7..653dbe0ae 100644 --- a/tests/strands/experimental/hooks/multiagent/test_events.py +++ b/tests/strands/experimental/hooks/multiagent/test_events.py @@ -4,14 +4,14 @@ import pytest -from strands.experimental.hooks.multiagent.events import ( +from strands.hooks import BaseHookEvent +from strands.hooks.multiagent.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, MultiAgentInitializedEvent, ) -from strands.hooks import BaseHookEvent @pytest.fixture diff --git a/tests/strands/experimental/hooks/multiagent/test_multi_agent_hooks.py b/tests/strands/experimental/hooks/multiagent/test_multi_agent_hooks.py index 4e97a9217..42b03f52c 100644 --- a/tests/strands/experimental/hooks/multiagent/test_multi_agent_hooks.py +++ b/tests/strands/experimental/hooks/multiagent/test_multi_agent_hooks.py @@ -1,7 +1,7 @@ import pytest from strands import Agent -from strands.experimental.hooks.multiagent.events import ( +from strands.hooks.multiagent.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, diff --git a/tests/strands/multiagent/conftest.py b/tests/strands/multiagent/conftest.py index 85e0ef7fc..89d9f42b1 100644 --- a/tests/strands/multiagent/conftest.py +++ b/tests/strands/multiagent/conftest.py @@ -1,7 +1,7 @@ import pytest -from strands.experimental.hooks.multiagent import BeforeNodeCallEvent from strands.hooks import HookProvider +from strands.hooks.multiagent import BeforeNodeCallEvent @pytest.fixture diff --git a/tests/strands/multiagent/test_graph.py b/tests/strands/multiagent/test_graph.py index 4875d1bec..e9c19b655 100644 --- a/tests/strands/multiagent/test_graph.py +++ b/tests/strands/multiagent/test_graph.py @@ -6,8 +6,8 @@ from strands.agent import Agent, AgentResult from strands.agent.state import AgentState -from strands.experimental.hooks.multiagent import BeforeNodeCallEvent from strands.hooks import AgentInitializedEvent +from strands.hooks.multiagent import BeforeNodeCallEvent from strands.hooks.registry import HookProvider, HookRegistry from strands.multiagent.base import MultiAgentBase, MultiAgentResult, NodeResult from strands.multiagent.graph import Graph, GraphBuilder, GraphEdge, GraphNode, GraphResult, GraphState, Status diff --git a/tests/strands/multiagent/test_swarm.py b/tests/strands/multiagent/test_swarm.py index f2abed9f7..16f152c83 100644 --- a/tests/strands/multiagent/test_swarm.py +++ b/tests/strands/multiagent/test_swarm.py @@ -6,7 +6,7 @@ from strands.agent import Agent, AgentResult from strands.agent.state import AgentState -from strands.experimental.hooks.multiagent import BeforeNodeCallEvent +from strands.hooks.multiagent import BeforeNodeCallEvent from strands.hooks.registry import HookRegistry from strands.interrupt import Interrupt, _InterruptState from strands.multiagent.base import Status diff --git a/tests_integ/hooks/multiagent/test_cancel.py b/tests_integ/hooks/multiagent/test_cancel.py index 9267330b7..b025ad9ce 100644 --- a/tests_integ/hooks/multiagent/test_cancel.py +++ b/tests_integ/hooks/multiagent/test_cancel.py @@ -1,8 +1,8 @@ import pytest from strands import Agent -from strands.experimental.hooks.multiagent import BeforeNodeCallEvent from strands.hooks import HookProvider +from strands.hooks.multiagent import BeforeNodeCallEvent from strands.multiagent import GraphBuilder, Swarm from strands.multiagent.base import Status from strands.types._events import MultiAgentNodeCancelEvent diff --git a/tests_integ/hooks/multiagent/test_events.py b/tests_integ/hooks/multiagent/test_events.py index e8039444f..339729706 100644 --- a/tests_integ/hooks/multiagent/test_events.py +++ b/tests_integ/hooks/multiagent/test_events.py @@ -1,14 +1,14 @@ import pytest from strands import Agent -from strands.experimental.hooks.multiagent import ( +from strands.hooks import HookProvider +from strands.hooks.multiagent import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, MultiAgentInitializedEvent, ) -from strands.hooks import HookProvider from strands.multiagent import GraphBuilder, Swarm diff --git a/tests_integ/interrupts/multiagent/test_hook.py b/tests_integ/interrupts/multiagent/test_hook.py index be7682082..ee07efdf5 100644 --- a/tests_integ/interrupts/multiagent/test_hook.py +++ b/tests_integ/interrupts/multiagent/test_hook.py @@ -4,8 +4,8 @@ import pytest from strands import Agent, tool -from strands.experimental.hooks.multiagent import BeforeNodeCallEvent from strands.hooks import HookProvider +from strands.hooks.multiagent import BeforeNodeCallEvent from strands.interrupt import Interrupt from strands.multiagent import Swarm from strands.multiagent.base import Status diff --git a/tests_integ/test_multiagent_swarm.py b/tests_integ/test_multiagent_swarm.py index e8e969af1..c5ab5f8f6 100644 --- a/tests_integ/test_multiagent_swarm.py +++ b/tests_integ/test_multiagent_swarm.py @@ -3,7 +3,6 @@ import pytest from strands import Agent, tool -from strands.experimental.hooks.multiagent import BeforeNodeCallEvent from strands.hooks import ( AfterInvocationEvent, AfterModelCallEvent, @@ -13,6 +12,7 @@ BeforeToolCallEvent, MessageAddedEvent, ) +from strands.hooks.multiagent import BeforeNodeCallEvent from strands.multiagent.swarm import Swarm from strands.session.file_session_manager import FileSessionManager from strands.types.content import ContentBlock From 82ff6881b6b15d22b729991cb2a2d28cde03ba5b Mon Sep 17 00:00:00 2001 From: Jack Yuan Date: Fri, 16 Jan 2026 15:11:01 -0500 Subject: [PATCH 2/5] fix: remove unnecessary warnings --- .../experimental/hooks/multiagent/events.py | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/strands/experimental/hooks/multiagent/events.py b/src/strands/experimental/hooks/multiagent/events.py index f640793e9..cc958b653 100644 --- a/src/strands/experimental/hooks/multiagent/events.py +++ b/src/strands/experimental/hooks/multiagent/events.py @@ -4,7 +4,6 @@ """ import warnings -from typing import Any from ....hooks.multiagent.events import ( AfterMultiAgentInvocationEvent, @@ -14,22 +13,16 @@ MultiAgentInitializedEvent, ) -_DEPRECATED_ALIASES = { - "MultiAgentInitializedEvent": MultiAgentInitializedEvent, - "BeforeNodeCallEvent": BeforeNodeCallEvent, - "BeforeMultiAgentInvocationEvent": BeforeMultiAgentInvocationEvent, - "AfterNodeCallEvent": AfterNodeCallEvent, - "AfterMultiAgentInvocationEvent": AfterMultiAgentInvocationEvent, -} - +warnings.warn( + "strands.experimental.hooks.multiagent.events is deprecated. Use strands.hooks.multiagent.events instead.", + DeprecationWarning, + stacklevel=2, +) -def __getattr__(name: str) -> Any: - if name in _DEPRECATED_ALIASES: - warnings.warn( - f"{name} has been moved to production with an updated name. " - f"Use {_DEPRECATED_ALIASES[name].__name__} from strands.hooks.multiagent instead.", - DeprecationWarning, - stacklevel=2, - ) - return _DEPRECATED_ALIASES[name] - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +__all__ = [ + "AfterMultiAgentInvocationEvent", + "AfterNodeCallEvent", + "BeforeMultiAgentInvocationEvent", + "BeforeNodeCallEvent", + "MultiAgentInitializedEvent", +] From 845275edaccb892f4ab622f2c56a6f26fefbd797 Mon Sep 17 00:00:00 2001 From: Jack Yuan Date: Tue, 20 Jan 2026 16:26:59 -0500 Subject: [PATCH 3/5] fix: flatten import path --- .../experimental/hooks/multiagent/events.py | 2 +- src/strands/hooks/__init__.py | 11 ++ src/strands/hooks/events.py | 106 +++++++++++++++- src/strands/hooks/multiagent/__init__.py | 20 --- src/strands/hooks/multiagent/events.py | 118 ------------------ src/strands/multiagent/graph.py | 6 +- src/strands/multiagent/swarm.py | 5 +- src/strands/session/session_manager.py | 6 +- .../fixtures/mock_multiagent_hook_provider.py | 8 +- .../experimental/hooks/multiagent/__init__.py | 0 .../hooks/multiagent => hooks}/test_events.py | 4 +- .../test_multi_agent_hooks.py | 2 +- tests/strands/multiagent/conftest.py | 3 +- tests/strands/multiagent/test_graph.py | 3 +- tests/strands/multiagent/test_swarm.py | 2 +- tests_integ/hooks/multiagent/test_cancel.py | 3 +- tests_integ/hooks/multiagent/test_events.py | 4 +- .../interrupts/multiagent/test_hook.py | 3 +- .../interrupts/multiagent/test_session.py | 3 +- tests_integ/test_multiagent_swarm.py | 2 +- 20 files changed, 142 insertions(+), 169 deletions(-) delete mode 100644 src/strands/hooks/multiagent/__init__.py delete mode 100644 src/strands/hooks/multiagent/events.py delete mode 100644 tests/strands/experimental/hooks/multiagent/__init__.py rename tests/strands/{experimental/hooks/multiagent => hooks}/test_events.py (97%) rename tests/strands/{experimental/hooks/multiagent => hooks}/test_multi_agent_hooks.py (98%) diff --git a/src/strands/experimental/hooks/multiagent/events.py b/src/strands/experimental/hooks/multiagent/events.py index cc958b653..852ce3a1b 100644 --- a/src/strands/experimental/hooks/multiagent/events.py +++ b/src/strands/experimental/hooks/multiagent/events.py @@ -5,7 +5,7 @@ import warnings -from ....hooks.multiagent.events import ( +from ....hooks.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, diff --git a/src/strands/hooks/__init__.py b/src/strands/hooks/__init__.py index 30163f207..96c7f577b 100644 --- a/src/strands/hooks/__init__.py +++ b/src/strands/hooks/__init__.py @@ -32,12 +32,18 @@ def log_end(self, event: AfterInvocationEvent) -> None: from .events import ( AfterInvocationEvent, AfterModelCallEvent, + # Multiagent hook events + AfterMultiAgentInvocationEvent, + AfterNodeCallEvent, AfterToolCallEvent, AgentInitializedEvent, BeforeInvocationEvent, BeforeModelCallEvent, + BeforeMultiAgentInvocationEvent, + BeforeNodeCallEvent, BeforeToolCallEvent, MessageAddedEvent, + MultiAgentInitializedEvent, ) from .registry import BaseHookEvent, HookCallback, HookEvent, HookProvider, HookRegistry @@ -56,4 +62,9 @@ def log_end(self, event: AfterInvocationEvent) -> None: "HookRegistry", "HookEvent", "BaseHookEvent", + "AfterMultiAgentInvocationEvent", + "AfterNodeCallEvent", + "BeforeMultiAgentInvocationEvent", + "BeforeNodeCallEvent", + "MultiAgentInitializedEvent", ] diff --git a/src/strands/hooks/events.py b/src/strands/hooks/events.py index 8aa8a68d6..1faa8a917 100644 --- a/src/strands/hooks/events.py +++ b/src/strands/hooks/events.py @@ -16,7 +16,10 @@ from ..types.interrupt import _Interruptible from ..types.streaming import StopReason from ..types.tools import AgentTool, ToolResult, ToolUse -from .registry import HookEvent +from .registry import BaseHookEvent, HookEvent + +if TYPE_CHECKING: + from ..multiagent.base import MultiAgentBase @dataclass @@ -250,3 +253,104 @@ def _can_write(self, name: str) -> bool: def should_reverse_callbacks(self) -> bool: """True to invoke callbacks in reverse order.""" return True + + +# Multiagent hook events start here +@dataclass +class MultiAgentInitializedEvent(BaseHookEvent): + """Event triggered when multi-agent orchestrator initialized. + + Attributes: + source: The multi-agent orchestrator instance + invocation_state: Configuration that user passes in + """ + + source: "MultiAgentBase" + invocation_state: dict[str, Any] | None = None + + +@dataclass +class BeforeNodeCallEvent(BaseHookEvent, _Interruptible): + """Event triggered before individual node execution starts. + + Attributes: + source: The multi-agent orchestrator instance + node_id: ID of the node about to execute + invocation_state: Configuration that user passes in + cancel_node: A user defined message that when set, will cancel the node execution with status FAILED. + The message will be emitted under a MultiAgentNodeCancel event. If set to `True`, Strands will cancel the + node using a default cancel message. + """ + + source: "MultiAgentBase" + node_id: str + invocation_state: dict[str, Any] | None = None + cancel_node: bool | str = False + + def _can_write(self, name: str) -> bool: + return name in ["cancel_node"] + + @override + def _interrupt_id(self, name: str) -> str: + """Unique id for the interrupt. + + Args: + name: User defined name for the interrupt. + + Returns: + Interrupt id. + """ + node_id = uuid.uuid5(uuid.NAMESPACE_OID, self.node_id) + call_id = uuid.uuid5(uuid.NAMESPACE_OID, name) + return f"v1:before_node_call:{node_id}:{call_id}" + + +@dataclass +class AfterNodeCallEvent(BaseHookEvent): + """Event triggered after individual node execution completes. + + Attributes: + source: The multi-agent orchestrator instance + node_id: ID of the node that just completed execution + invocation_state: Configuration that user passes in + """ + + source: "MultiAgentBase" + node_id: str + invocation_state: dict[str, Any] | None = None + + @property + def should_reverse_callbacks(self) -> bool: + """True to invoke callbacks in reverse order.""" + return True + + +@dataclass +class BeforeMultiAgentInvocationEvent(BaseHookEvent): + """Event triggered before orchestrator execution starts. + + Attributes: + source: The multi-agent orchestrator instance + invocation_state: Configuration that user passes in + """ + + source: "MultiAgentBase" + invocation_state: dict[str, Any] | None = None + + +@dataclass +class AfterMultiAgentInvocationEvent(BaseHookEvent): + """Event triggered after orchestrator execution completes. + + Attributes: + source: The multi-agent orchestrator instance + invocation_state: Configuration that user passes in + """ + + source: "MultiAgentBase" + invocation_state: dict[str, Any] | None = None + + @property + def should_reverse_callbacks(self) -> bool: + """True to invoke callbacks in reverse order.""" + return True diff --git a/src/strands/hooks/multiagent/__init__.py b/src/strands/hooks/multiagent/__init__.py deleted file mode 100644 index d059d0da5..000000000 --- a/src/strands/hooks/multiagent/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Multi-agent hook events and utilities. - -Provides event classes for hooking into multi-agent orchestrator lifecycle. -""" - -from .events import ( - AfterMultiAgentInvocationEvent, - AfterNodeCallEvent, - BeforeMultiAgentInvocationEvent, - BeforeNodeCallEvent, - MultiAgentInitializedEvent, -) - -__all__ = [ - "AfterMultiAgentInvocationEvent", - "AfterNodeCallEvent", - "BeforeMultiAgentInvocationEvent", - "BeforeNodeCallEvent", - "MultiAgentInitializedEvent", -] diff --git a/src/strands/hooks/multiagent/events.py b/src/strands/hooks/multiagent/events.py deleted file mode 100644 index c544d551f..000000000 --- a/src/strands/hooks/multiagent/events.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Multi-agent execution lifecycle events for hook system integration. - -These events are fired by orchestrators (Graph/Swarm) at key points so -hooks can persist, monitor, or debug execution. No intermediate state model -is used—hooks read from the orchestrator directly. -""" - -import uuid -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -from typing_extensions import override - -from ...types.interrupt import _Interruptible -from ..registry import BaseHookEvent - -if TYPE_CHECKING: - from ...multiagent.base import MultiAgentBase - - -@dataclass -class MultiAgentInitializedEvent(BaseHookEvent): - """Event triggered when multi-agent orchestrator initialized. - - Attributes: - source: The multi-agent orchestrator instance - invocation_state: Configuration that user passes in - """ - - source: "MultiAgentBase" - invocation_state: dict[str, Any] | None = None - - -@dataclass -class BeforeNodeCallEvent(BaseHookEvent, _Interruptible): - """Event triggered before individual node execution starts. - - Attributes: - source: The multi-agent orchestrator instance - node_id: ID of the node about to execute - invocation_state: Configuration that user passes in - cancel_node: A user defined message that when set, will cancel the node execution with status FAILED. - The message will be emitted under a MultiAgentNodeCancel event. If set to `True`, Strands will cancel the - node using a default cancel message. - """ - - source: "MultiAgentBase" - node_id: str - invocation_state: dict[str, Any] | None = None - cancel_node: bool | str = False - - def _can_write(self, name: str) -> bool: - return name in ["cancel_node"] - - @override - def _interrupt_id(self, name: str) -> str: - """Unique id for the interrupt. - - Args: - name: User defined name for the interrupt. - - Returns: - Interrupt id. - """ - node_id = uuid.uuid5(uuid.NAMESPACE_OID, self.node_id) - call_id = uuid.uuid5(uuid.NAMESPACE_OID, name) - return f"v1:before_node_call:{node_id}:{call_id}" - - -@dataclass -class AfterNodeCallEvent(BaseHookEvent): - """Event triggered after individual node execution completes. - - Attributes: - source: The multi-agent orchestrator instance - node_id: ID of the node that just completed execution - invocation_state: Configuration that user passes in - """ - - source: "MultiAgentBase" - node_id: str - invocation_state: dict[str, Any] | None = None - - @property - def should_reverse_callbacks(self) -> bool: - """True to invoke callbacks in reverse order.""" - return True - - -@dataclass -class BeforeMultiAgentInvocationEvent(BaseHookEvent): - """Event triggered before orchestrator execution starts. - - Attributes: - source: The multi-agent orchestrator instance - invocation_state: Configuration that user passes in - """ - - source: "MultiAgentBase" - invocation_state: dict[str, Any] | None = None - - -@dataclass -class AfterMultiAgentInvocationEvent(BaseHookEvent): - """Event triggered after orchestrator execution completes. - - Attributes: - source: The multi-agent orchestrator instance - invocation_state: Configuration that user passes in - """ - - source: "MultiAgentBase" - invocation_state: dict[str, Any] | None = None - - @property - def should_reverse_callbacks(self) -> bool: - """True to invoke callbacks in reverse order.""" - return True diff --git a/src/strands/multiagent/graph.py b/src/strands/multiagent/graph.py index db997d71e..862370664 100644 --- a/src/strands/multiagent/graph.py +++ b/src/strands/multiagent/graph.py @@ -27,15 +27,15 @@ from .._async import run_async from ..agent import Agent from ..agent.state import AgentState -from ..hooks import HookProvider, HookRegistry -from ..hooks.multiagent import ( +from ..hooks import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, + HookProvider, + HookRegistry, MultiAgentInitializedEvent, ) -from ..hooks import HookProvider, HookRegistry from ..interrupt import Interrupt, _InterruptState from ..session import SessionManager from ..telemetry import get_tracer diff --git a/src/strands/multiagent/swarm.py b/src/strands/multiagent/swarm.py index 1bcd3db84..ec501f03d 100644 --- a/src/strands/multiagent/swarm.py +++ b/src/strands/multiagent/swarm.py @@ -27,12 +27,13 @@ from .._async import run_async from ..agent import Agent from ..agent.state import AgentState -from ..hooks import HookProvider, HookRegistry -from ..hooks.multiagent import ( +from ..hooks import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, + HookProvider, + HookRegistry, MultiAgentInitializedEvent, ) from ..interrupt import Interrupt, _InterruptState diff --git a/src/strands/session/session_manager.py b/src/strands/session/session_manager.py index 756c90c00..cc954e17d 100644 --- a/src/strands/session/session_manager.py +++ b/src/strands/session/session_manager.py @@ -9,10 +9,12 @@ BidiAgentInitializedEvent, BidiMessageAddedEvent, ) -from ..hooks.events import AfterInvocationEvent, AgentInitializedEvent, MessageAddedEvent -from ..hooks.multiagent.events import ( +from ..hooks.events import ( + AfterInvocationEvent, AfterMultiAgentInvocationEvent, AfterNodeCallEvent, + AgentInitializedEvent, + MessageAddedEvent, MultiAgentInitializedEvent, ) from ..hooks.registry import HookProvider, HookRegistry diff --git a/tests/fixtures/mock_multiagent_hook_provider.py b/tests/fixtures/mock_multiagent_hook_provider.py index 10440041f..a89d5aca8 100644 --- a/tests/fixtures/mock_multiagent_hook_provider.py +++ b/tests/fixtures/mock_multiagent_hook_provider.py @@ -2,14 +2,12 @@ from typing import Literal from strands.hooks import ( - HookEvent, - HookProvider, - HookRegistry, -) -from strands.hooks.multiagent.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeNodeCallEvent, + HookEvent, + HookProvider, + HookRegistry, MultiAgentInitializedEvent, ) diff --git a/tests/strands/experimental/hooks/multiagent/__init__.py b/tests/strands/experimental/hooks/multiagent/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/strands/experimental/hooks/multiagent/test_events.py b/tests/strands/hooks/test_events.py similarity index 97% rename from tests/strands/experimental/hooks/multiagent/test_events.py rename to tests/strands/hooks/test_events.py index 653dbe0ae..90ab205a9 100644 --- a/tests/strands/experimental/hooks/multiagent/test_events.py +++ b/tests/strands/hooks/test_events.py @@ -4,10 +4,10 @@ import pytest -from strands.hooks import BaseHookEvent -from strands.hooks.multiagent.events import ( +from strands.hooks import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, + BaseHookEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, MultiAgentInitializedEvent, diff --git a/tests/strands/experimental/hooks/multiagent/test_multi_agent_hooks.py b/tests/strands/hooks/test_multi_agent_hooks.py similarity index 98% rename from tests/strands/experimental/hooks/multiagent/test_multi_agent_hooks.py rename to tests/strands/hooks/test_multi_agent_hooks.py index 42b03f52c..981d5d813 100644 --- a/tests/strands/experimental/hooks/multiagent/test_multi_agent_hooks.py +++ b/tests/strands/hooks/test_multi_agent_hooks.py @@ -1,7 +1,7 @@ import pytest from strands import Agent -from strands.hooks.multiagent.events import ( +from strands.hooks.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, diff --git a/tests/strands/multiagent/conftest.py b/tests/strands/multiagent/conftest.py index 89d9f42b1..e5dd1b4f9 100644 --- a/tests/strands/multiagent/conftest.py +++ b/tests/strands/multiagent/conftest.py @@ -1,7 +1,6 @@ import pytest -from strands.hooks import HookProvider -from strands.hooks.multiagent import BeforeNodeCallEvent +from strands.hooks import BeforeNodeCallEvent, HookProvider @pytest.fixture diff --git a/tests/strands/multiagent/test_graph.py b/tests/strands/multiagent/test_graph.py index 0120f0edb..cd750865e 100644 --- a/tests/strands/multiagent/test_graph.py +++ b/tests/strands/multiagent/test_graph.py @@ -6,8 +6,7 @@ from strands.agent import Agent, AgentResult from strands.agent.state import AgentState -from strands.hooks import AgentInitializedEvent -from strands.hooks.multiagent import BeforeNodeCallEvent +from strands.hooks import AgentInitializedEvent, BeforeNodeCallEvent from strands.hooks.registry import HookProvider, HookRegistry from strands.interrupt import Interrupt, _InterruptState from strands.multiagent.base import MultiAgentBase, MultiAgentResult, NodeResult diff --git a/tests/strands/multiagent/test_swarm.py b/tests/strands/multiagent/test_swarm.py index 16f152c83..97d07e0b4 100644 --- a/tests/strands/multiagent/test_swarm.py +++ b/tests/strands/multiagent/test_swarm.py @@ -6,7 +6,7 @@ from strands.agent import Agent, AgentResult from strands.agent.state import AgentState -from strands.hooks.multiagent import BeforeNodeCallEvent +from strands.hooks import BeforeNodeCallEvent from strands.hooks.registry import HookRegistry from strands.interrupt import Interrupt, _InterruptState from strands.multiagent.base import Status diff --git a/tests_integ/hooks/multiagent/test_cancel.py b/tests_integ/hooks/multiagent/test_cancel.py index b025ad9ce..ae3008861 100644 --- a/tests_integ/hooks/multiagent/test_cancel.py +++ b/tests_integ/hooks/multiagent/test_cancel.py @@ -1,8 +1,7 @@ import pytest from strands import Agent -from strands.hooks import HookProvider -from strands.hooks.multiagent import BeforeNodeCallEvent +from strands.hooks import BeforeNodeCallEvent, HookProvider from strands.multiagent import GraphBuilder, Swarm from strands.multiagent.base import Status from strands.types._events import MultiAgentNodeCancelEvent diff --git a/tests_integ/hooks/multiagent/test_events.py b/tests_integ/hooks/multiagent/test_events.py index 339729706..3a10b74c1 100644 --- a/tests_integ/hooks/multiagent/test_events.py +++ b/tests_integ/hooks/multiagent/test_events.py @@ -1,12 +1,12 @@ import pytest from strands import Agent -from strands.hooks import HookProvider -from strands.hooks.multiagent import ( +from strands.hooks import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, + HookProvider, MultiAgentInitializedEvent, ) from strands.multiagent import GraphBuilder, Swarm diff --git a/tests_integ/interrupts/multiagent/test_hook.py b/tests_integ/interrupts/multiagent/test_hook.py index 3d731470b..53305b4e8 100644 --- a/tests_integ/interrupts/multiagent/test_hook.py +++ b/tests_integ/interrupts/multiagent/test_hook.py @@ -4,8 +4,7 @@ import pytest from strands import Agent, tool -from strands.hooks import HookProvider -from strands.hooks.multiagent import BeforeNodeCallEvent +from strands.hooks import BeforeNodeCallEvent, HookProvider from strands.interrupt import Interrupt from strands.multiagent import GraphBuilder, Swarm from strands.multiagent.base import Status diff --git a/tests_integ/interrupts/multiagent/test_session.py b/tests_integ/interrupts/multiagent/test_session.py index bab4b428f..2ccff2c12 100644 --- a/tests_integ/interrupts/multiagent/test_session.py +++ b/tests_integ/interrupts/multiagent/test_session.py @@ -4,8 +4,7 @@ import pytest from strands import Agent, tool -from strands.experimental.hooks.multiagent import BeforeNodeCallEvent -from strands.hooks import HookProvider +from strands.hooks import BeforeNodeCallEvent, HookProvider from strands.interrupt import Interrupt from strands.multiagent import GraphBuilder, Swarm from strands.multiagent.base import Status diff --git a/tests_integ/test_multiagent_swarm.py b/tests_integ/test_multiagent_swarm.py index c5ab5f8f6..e9738d3d9 100644 --- a/tests_integ/test_multiagent_swarm.py +++ b/tests_integ/test_multiagent_swarm.py @@ -9,10 +9,10 @@ AfterToolCallEvent, BeforeInvocationEvent, BeforeModelCallEvent, + BeforeNodeCallEvent, BeforeToolCallEvent, MessageAddedEvent, ) -from strands.hooks.multiagent import BeforeNodeCallEvent from strands.multiagent.swarm import Swarm from strands.session.file_session_manager import FileSessionManager from strands.types.content import ContentBlock From 2f978f0334f67fe1306e38e2c23f9b772a398aea Mon Sep 17 00:00:00 2001 From: Jack Yuan Date: Wed, 21 Jan 2026 10:07:44 -0500 Subject: [PATCH 4/5] fix: fix warning path --- src/strands/experimental/hooks/multiagent/events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strands/experimental/hooks/multiagent/events.py b/src/strands/experimental/hooks/multiagent/events.py index 852ce3a1b..2c65c53e3 100644 --- a/src/strands/experimental/hooks/multiagent/events.py +++ b/src/strands/experimental/hooks/multiagent/events.py @@ -5,7 +5,7 @@ import warnings -from ....hooks.events import ( +from ....hooks import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, @@ -14,7 +14,7 @@ ) warnings.warn( - "strands.experimental.hooks.multiagent.events is deprecated. Use strands.hooks.multiagent.events instead.", + "strands.experimental.hooks.multiagent is deprecated. Use strands.hooks instead.", DeprecationWarning, stacklevel=2, ) From 4ff3e316bf5910d813e462c5dde84684355f1845 Mon Sep 17 00:00:00 2001 From: Jack Yuan Date: Wed, 21 Jan 2026 10:25:44 -0500 Subject: [PATCH 5/5] fix: fix unit test path --- src/strands/multiagent/graph.py | 5 ++--- src/strands/multiagent/swarm.py | 5 ++--- tests/strands/hooks/test_multi_agent_hooks.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/strands/multiagent/graph.py b/src/strands/multiagent/graph.py index 862370664..32eca00ff 100644 --- a/src/strands/multiagent/graph.py +++ b/src/strands/multiagent/graph.py @@ -27,15 +27,14 @@ from .._async import run_async from ..agent import Agent from ..agent.state import AgentState -from ..hooks import ( +from ..hooks.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, - HookProvider, - HookRegistry, MultiAgentInitializedEvent, ) +from ..hooks.registry import HookProvider, HookRegistry from ..interrupt import Interrupt, _InterruptState from ..session import SessionManager from ..telemetry import get_tracer diff --git a/src/strands/multiagent/swarm.py b/src/strands/multiagent/swarm.py index ec501f03d..1b883246c 100644 --- a/src/strands/multiagent/swarm.py +++ b/src/strands/multiagent/swarm.py @@ -27,15 +27,14 @@ from .._async import run_async from ..agent import Agent from ..agent.state import AgentState -from ..hooks import ( +from ..hooks.events import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, BeforeNodeCallEvent, - HookProvider, - HookRegistry, MultiAgentInitializedEvent, ) +from ..hooks.registry import HookProvider, HookRegistry from ..interrupt import Interrupt, _InterruptState from ..session import SessionManager from ..telemetry import get_tracer diff --git a/tests/strands/hooks/test_multi_agent_hooks.py b/tests/strands/hooks/test_multi_agent_hooks.py index 981d5d813..3f6e0c940 100644 --- a/tests/strands/hooks/test_multi_agent_hooks.py +++ b/tests/strands/hooks/test_multi_agent_hooks.py @@ -1,7 +1,7 @@ import pytest from strands import Agent -from strands.hooks.events import ( +from strands.hooks import ( AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent,