-
Notifications
You must be signed in to change notification settings - Fork 836
feat(plugins): add MultiAgentPlugin for Swarm and Graph orchestrators #2280
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
Open
zastrowm
wants to merge
9
commits into
strands-agents:main
Choose a base branch
from
zastrowm:multi_agent_plugins
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
1c5dcc2
Implement plugins for MultiAgent
zastrowm 7aae080
Address self-review
zastrowm 57d89d1
Move to kwarg
zastrowm d413d52
fix: address review feedback - coverage gaps and param ordering
zastrowm 9cc15af
feat: add add_hook to Graph, Swarm, and MultiAgentBase
zastrowm 14ff42a
refactor: remove redundant hasattr check from plugin registry
zastrowm 51613f3
refactor: remove dead register_hooks helper and improve add_hook erro…
zastrowm 9cff053
refactor: DRY up discovery helpers and add guard test
zastrowm 1d79ed0
fix: remove unused TypeVar import and variable
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
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
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 |
|---|---|---|
| @@ -1,13 +1,16 @@ | ||
| """Plugin system for extending agent functionality. | ||
| """Plugin system for extending agent and orchestrator functionality. | ||
|
|
||
| This module provides a composable mechanism for building objects that can | ||
| extend agent behavior through automatic hook and tool registration. | ||
| extend agent and multi-agent orchestrator behavior through automatic hook | ||
| and tool registration. | ||
| """ | ||
|
|
||
| from .decorator import hook | ||
| from .multiagent_plugin import MultiAgentPlugin | ||
| from .plugin import Plugin | ||
|
|
||
| __all__ = [ | ||
| "MultiAgentPlugin", | ||
| "Plugin", | ||
| "hook", | ||
| ] |
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,119 @@ | ||
| """Shared utility for discovering decorated methods on plugin instances. | ||
|
|
||
| This module provides helper functions used by both Plugin and MultiAgentPlugin | ||
| to scan for @hook (and optionally @tool) decorated methods, and shared registry | ||
| utilities for plugin initialization and hook registration. | ||
| """ | ||
|
|
||
| import inspect | ||
| import logging | ||
| from collections.abc import Awaitable, Callable | ||
| from typing import Any, cast | ||
|
|
||
| from .._async import run_async | ||
| from ..hooks.registry import HookCallback, HookRegistry | ||
| from ..tools.decorator import DecoratedFunctionTool | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def discover_hooks(instance: object, plugin_name: str) -> list[HookCallback]: | ||
| """Scan an instance's class hierarchy for @hook decorated methods. | ||
|
|
||
| Walks the MRO in reverse so parent class hooks come first, but child | ||
| overrides win (only the child's version is included). | ||
|
|
||
| Args: | ||
| instance: The plugin instance to scan. | ||
| plugin_name: The plugin name (used for debug logging). | ||
|
|
||
| Returns: | ||
| List of bound hook callback methods in declaration order. | ||
| """ | ||
| hooks: list[HookCallback] = [] | ||
| seen: set[str] = set() | ||
|
|
||
| for cls in reversed(type(instance).__mro__): | ||
| for attr_name in cls.__dict__: | ||
| if attr_name in seen: | ||
| continue | ||
| seen.add(attr_name) | ||
|
|
||
| try: | ||
| bound = getattr(instance, attr_name) | ||
| except Exception: | ||
| continue | ||
|
|
||
| if hasattr(bound, "_hook_event_types") and callable(bound): | ||
| hooks.append(bound) | ||
| logger.debug("plugin=<%s>, hook=<%s> | discovered hook method", plugin_name, attr_name) | ||
|
|
||
| return hooks | ||
|
|
||
|
|
||
| def discover_tools(instance: object, plugin_name: str) -> list[DecoratedFunctionTool]: | ||
| """Scan an instance's class hierarchy for @tool decorated methods. | ||
|
|
||
| Walks the MRO in reverse so parent class tools come first, but child | ||
| overrides win (only the child's version is included). | ||
|
|
||
| Args: | ||
| instance: The plugin instance to scan. | ||
| plugin_name: The plugin name (used for debug logging). | ||
|
|
||
| Returns: | ||
| List of DecoratedFunctionTool instances in declaration order. | ||
| """ | ||
| tools: list[DecoratedFunctionTool] = [] | ||
| seen: set[str] = set() | ||
|
|
||
| for cls in reversed(type(instance).__mro__): | ||
| for attr_name in cls.__dict__: | ||
| if attr_name in seen: | ||
| continue | ||
| seen.add(attr_name) | ||
|
|
||
| try: | ||
| bound = getattr(instance, attr_name) | ||
| except Exception: | ||
| continue | ||
|
|
||
| if isinstance(bound, DecoratedFunctionTool): | ||
| tools.append(bound) | ||
| logger.debug("plugin=<%s>, tool=<%s> | discovered tool method", plugin_name, attr_name) | ||
|
|
||
| return tools | ||
|
|
||
|
|
||
| def call_init_method(init_method: Callable[..., Any], target: Any) -> None: | ||
| """Call a plugin's init method, handling both sync and async implementations. | ||
|
|
||
| Args: | ||
| init_method: The init_agent or init_multi_agent method to call. | ||
| target: The agent or orchestrator instance to pass to the init method. | ||
| """ | ||
| if inspect.iscoroutinefunction(init_method): | ||
| async_init = cast(Callable[..., Awaitable[None]], init_method) | ||
| run_async(lambda: async_init(target)) | ||
| else: | ||
| init_method(target) | ||
|
|
||
|
|
||
| def register_hooks(plugin_name: str, hooks: list[HookCallback], registry: HookRegistry) -> None: | ||
|
zastrowm marked this conversation as resolved.
Outdated
zastrowm marked this conversation as resolved.
Outdated
|
||
| """Register discovered hook callbacks with a hook registry. | ||
|
|
||
| Args: | ||
| plugin_name: The plugin name (used for debug logging). | ||
| hooks: List of hook callbacks to register. | ||
| registry: The HookRegistry to register callbacks with. | ||
| """ | ||
| for hook_callback in hooks: | ||
| event_types = getattr(hook_callback, "_hook_event_types", []) | ||
| for event_type in event_types: | ||
| registry.add_callback(event_type, hook_callback) | ||
| logger.debug( | ||
| "plugin=<%s>, hook=<%s>, event_type=<%s> | registered hook", | ||
| plugin_name, | ||
| getattr(hook_callback, "__name__", repr(hook_callback)), | ||
| event_type.__name__, | ||
| ) | ||
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,119 @@ | ||
| """MultiAgentPlugin base class for extending multi-agent orchestrator functionality. | ||
|
|
||
| This module defines the MultiAgentPlugin base class, which provides a composable way to | ||
| add behavior changes to multi-agent orchestrators (Swarm, Graph) through automatic hook | ||
| registration and custom initialization. | ||
|
|
||
| MultiAgentPlugin is the orchestrator-level counterpart to Plugin (which targets individual agents). | ||
| A class can implement both Plugin and MultiAgentPlugin to provide functionality at both levels. | ||
| """ | ||
|
|
||
| from abc import ABC, abstractmethod | ||
| from collections.abc import Awaitable | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from ..hooks.registry import HookCallback | ||
| from ._discovery import discover_hooks | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ..multiagent.base import MultiAgentBase | ||
|
|
||
|
|
||
| class MultiAgentPlugin(ABC): | ||
| """Base class for objects that extend multi-agent orchestrator functionality. | ||
|
|
||
| MultiAgentPlugins provide a composable way to add behavior changes to orchestrators | ||
| (Swarm, Graph). They support automatic discovery and registration of methods decorated | ||
| with @hook. | ||
|
|
||
| Unlike agent-level Plugin, MultiAgentPlugin does not support @tool decorated methods | ||
| since orchestrators do not have tool registries. | ||
|
|
||
| Attributes: | ||
| name: A stable string identifier for the plugin (must be provided by subclass) | ||
| hooks: Hooks attached to the orchestrator, auto-discovered from @hook decorated methods | ||
|
|
||
| Example using decorators (recommended): | ||
| ```python | ||
| from strands.plugins import MultiAgentPlugin, hook | ||
| from strands.hooks import BeforeNodeCallEvent, AfterNodeCallEvent | ||
|
|
||
| class MonitoringPlugin(MultiAgentPlugin): | ||
| name = "monitoring" | ||
|
|
||
| @hook | ||
| def on_before_node(self, event: BeforeNodeCallEvent): | ||
| print(f"Node {event.node_id} starting") | ||
|
|
||
| @hook | ||
| def on_after_node(self, event: AfterNodeCallEvent): | ||
| print(f"Node {event.node_id} completed") | ||
| ``` | ||
|
|
||
| Example with custom initialization: | ||
| ```python | ||
| class MyPlugin(MultiAgentPlugin): | ||
| name = "my-plugin" | ||
|
|
||
| def init_multi_agent(self, orchestrator: MultiAgentBase) -> None: | ||
| # Custom initialization logic | ||
| pass | ||
| ``` | ||
|
|
||
| Dual-use example (both agent and orchestrator): | ||
| ```python | ||
| from strands.plugins import Plugin, MultiAgentPlugin, hook | ||
| from strands.hooks import BeforeInvocationEvent, BeforeNodeCallEvent | ||
|
|
||
| class ObservabilityPlugin(Plugin, MultiAgentPlugin): | ||
| name = "observability" | ||
|
|
||
| @hook | ||
| def on_agent_invocation(self, event: BeforeInvocationEvent): | ||
| print("Agent invocation started") | ||
|
|
||
| @hook | ||
| def on_node_call(self, event: BeforeNodeCallEvent): | ||
| print(f"Node {event.node_id} starting") | ||
|
|
||
| def init_agent(self, agent): | ||
| pass # Agent-level setup | ||
|
|
||
| def init_multi_agent(self, orchestrator): | ||
| pass # Orchestrator-level setup | ||
| ``` | ||
| """ | ||
|
|
||
| @property | ||
| @abstractmethod | ||
| def name(self) -> str: | ||
| """A stable string identifier for the plugin.""" | ||
| ... | ||
|
|
||
| def __init__(self) -> None: | ||
| """Initialize the plugin and discover decorated hook methods. | ||
|
|
||
| Scans the class for methods decorated with @hook and stores references | ||
| for later registration when the plugin is attached to an orchestrator. | ||
|
|
||
| Uses a guard to prevent double-discovery when used with multiple inheritance | ||
|
zastrowm marked this conversation as resolved.
|
||
| (e.g., a class that inherits from both Plugin and MultiAgentPlugin). | ||
| """ | ||
| if not hasattr(self, "_hooks"): | ||
| self._hooks: list[HookCallback] = discover_hooks(self, self.name) | ||
|
|
||
| @property | ||
| def hooks(self) -> list[HookCallback]: | ||
| """List of hooks the plugin provides, auto-discovered from @hook decorated methods.""" | ||
| return self._hooks | ||
|
|
||
| def init_multi_agent(self, orchestrator: "MultiAgentBase") -> None | Awaitable[None]: | ||
|
zastrowm marked this conversation as resolved.
|
||
| """Initialize the plugin with the orchestrator instance. | ||
|
|
||
| Override this method to add custom initialization logic. Decorated | ||
| hooks are automatically registered by the plugin registry. | ||
|
|
||
| Args: | ||
| orchestrator: The multi-agent orchestrator instance to initialize with. | ||
| """ | ||
| return None | ||
Oops, something went wrong.
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.