Skip to content

Commit 0a256b8

Browse files
committed
refactor(plugins): move Plugin to dedicated module with PluginRegistry
- Move Plugin Protocol from hooks/registry.py to plugins/plugin.py - Create PluginRegistry class for tracking plugins attached to agents - Create dedicated src/strands/plugins/ module with proper exports - Update top-level strands package to export Plugin from new location - Move Plugin tests to tests/strands/plugins/ - Add PluginRegistry tests for add, get, has, and list operations - Remove accidentally committed install.log build artifact - Update AGENTS.md with new plugins directory structure
1 parent dd0c572 commit 0a256b8

11 files changed

Lines changed: 383 additions & 405 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ strands-agents/
126126
│ │ ├── events.py # Hook event definitions
127127
│ │ └── registry.py # Hook registration
128128
│ │
129+
│ ├── plugins/ # Plugin system
130+
│ │ ├── plugin.py # Plugin Protocol definition
131+
│ │ └── registry.py # PluginRegistry for tracking plugins
132+
│ │
129133
│ ├── handlers/ # Event handlers
130134
│ │ └── callback_handler.py # Callback handling
131135
│ │
@@ -171,6 +175,7 @@ strands-agents/
171175
│ ├── session/
172176
│ ├── telemetry/
173177
│ ├── hooks/
178+
│ ├── plugins/
174179
│ ├── handlers/
175180
│ ├── experimental/
176181
│ └── utils/

install.log

Lines changed: 0 additions & 260 deletions
This file was deleted.

src/strands/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .agent.agent import Agent
55
from .agent.base import AgentBase
66
from .event_loop._retry import ModelRetryStrategy
7-
from .hooks.registry import Plugin
7+
from .plugins import Plugin
88
from .tools.decorator import tool
99
from .types.tools import ToolContext
1010

src/strands/hooks/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def log_end(self, event: AfterInvocationEvent) -> None:
4545
MessageAddedEvent,
4646
MultiAgentInitializedEvent,
4747
)
48-
from .registry import BaseHookEvent, HookCallback, HookEvent, HookProvider, HookRegistry, Plugin
48+
from .registry import BaseHookEvent, HookCallback, HookEvent, HookProvider, HookRegistry
4949

5050
__all__ = [
5151
"AgentInitializedEvent",
@@ -67,5 +67,4 @@ def log_end(self, event: AfterInvocationEvent) -> None:
6767
"BeforeMultiAgentInvocationEvent",
6868
"BeforeNodeCallEvent",
6969
"MultiAgentInitializedEvent",
70-
"Plugin",
7170
]

src/strands/hooks/registry.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -122,35 +122,6 @@ def register_hooks(self, registry: "HookRegistry", **kwargs: Any) -> None:
122122
...
123123

124124

125-
@runtime_checkable
126-
class Plugin(Protocol):
127-
"""Protocol for objects that extend agent functionality.
128-
129-
Plugins provide a composable way to add behavior changes to agents.
130-
They are initialized with an agent instance and can register hooks,
131-
modify agent attributes, or perform other setup tasks.
132-
133-
Example:
134-
```python
135-
class MyPlugin:
136-
name = "my-plugin"
137-
138-
def init_plugin(self, agent: Agent) -> None:
139-
agent.add_hook(self.on_model_call, BeforeModelCallEvent)
140-
```
141-
"""
142-
143-
name: str
144-
145-
def init_plugin(self, agent: "Agent") -> None | Awaitable[None]:
146-
"""Initialize the plugin with an agent instance.
147-
148-
Args:
149-
agent: The agent instance to extend.
150-
"""
151-
...
152-
153-
154125
class HookCallback(Protocol, Generic[TEvent]):
155126
"""Protocol for callback functions that handle hook events.
156127

src/strands/plugins/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Plugin system for extending agent functionality.
2+
3+
This module provides a composable mechanism for building objects that can
4+
extend agent behavior through a standardized initialization pattern.
5+
6+
Example Usage:
7+
```python
8+
from strands.plugins import Plugin, PluginRegistry
9+
10+
class LoggingPlugin:
11+
name = "logging"
12+
13+
def init_plugin(self, agent: Agent) -> None:
14+
agent.add_hook(self.on_model_call, BeforeModelCallEvent)
15+
16+
def on_model_call(self, event: BeforeModelCallEvent) -> None:
17+
print(f"Model called for {event.agent.name}")
18+
19+
# Use with registry
20+
registry = PluginRegistry()
21+
plugin = LoggingPlugin()
22+
registry.add_plugin(plugin, agent)
23+
```
24+
"""
25+
26+
from .plugin import Plugin
27+
from .registry import PluginRegistry
28+
29+
__all__ = [
30+
"Plugin",
31+
"PluginRegistry",
32+
]

src/strands/plugins/plugin.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Plugin protocol for extending agent functionality.
2+
3+
This module defines the Plugin Protocol, which provides a composable way to
4+
add behavior changes to agents through a standardized initialization pattern.
5+
"""
6+
7+
from collections.abc import Awaitable
8+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
9+
10+
if TYPE_CHECKING:
11+
from ..agent import Agent
12+
13+
14+
@runtime_checkable
15+
class Plugin(Protocol):
16+
"""Protocol for objects that extend agent functionality.
17+
18+
Plugins provide a composable way to add behavior changes to agents.
19+
They are initialized with an agent instance and can register hooks,
20+
modify agent attributes, or perform other setup tasks.
21+
22+
Example:
23+
```python
24+
class MyPlugin:
25+
name = "my-plugin"
26+
27+
def init_plugin(self, agent: Agent) -> None:
28+
agent.add_hook(self.on_model_call, BeforeModelCallEvent)
29+
```
30+
"""
31+
32+
name: str
33+
34+
def init_plugin(self, agent: "Agent") -> None | Awaitable[None]:
35+
"""Initialize the plugin with an agent instance.
36+
37+
Args:
38+
agent: The agent instance to extend.
39+
"""
40+
...

src/strands/plugins/registry.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Plugin registry for managing plugins attached to an agent.
2+
3+
This module provides the PluginRegistry class for tracking and managing
4+
plugins that have been initialized with an agent instance.
5+
"""
6+
7+
import logging
8+
from typing import TYPE_CHECKING
9+
10+
from .plugin import Plugin
11+
12+
if TYPE_CHECKING:
13+
from ..agent import Agent
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class PluginRegistry:
19+
"""Registry for managing plugins attached to an agent.
20+
21+
The PluginRegistry tracks plugins that have been initialized with an agent,
22+
providing methods to add, retrieve, and check for plugins by name.
23+
24+
Example:
25+
```python
26+
registry = PluginRegistry()
27+
28+
class MyPlugin:
29+
name = "my-plugin"
30+
31+
def init_plugin(self, agent: Agent) -> None:
32+
pass
33+
34+
plugin = MyPlugin()
35+
registry.add_plugin(plugin, agent)
36+
37+
# Check if plugin is registered
38+
if registry.has_plugin("my-plugin"):
39+
retrieved = registry.get_plugin("my-plugin")
40+
```
41+
"""
42+
43+
def __init__(self) -> None:
44+
"""Initialize an empty plugin registry."""
45+
self._plugins: dict[str, Plugin] = {}
46+
47+
def add_plugin(self, plugin: Plugin, agent: "Agent") -> None:
48+
"""Add and initialize a plugin with the given agent.
49+
50+
Args:
51+
plugin: The plugin to add and initialize.
52+
agent: The agent instance to initialize the plugin with.
53+
54+
Raises:
55+
ValueError: If a plugin with the same name is already registered.
56+
"""
57+
if plugin.name in self._plugins:
58+
raise ValueError(f"plugin_name=<{plugin.name}> | plugin already registered")
59+
60+
logger.debug("plugin_name=<%s> | registering plugin", plugin.name)
61+
self._plugins[plugin.name] = plugin
62+
63+
def get_plugin(self, name: str) -> Plugin | None:
64+
"""Get a plugin by name.
65+
66+
Args:
67+
name: The name of the plugin to retrieve.
68+
69+
Returns:
70+
The plugin if found, None otherwise.
71+
"""
72+
return self._plugins.get(name)
73+
74+
def has_plugin(self, name: str) -> bool:
75+
"""Check if a plugin with the given name is registered.
76+
77+
Args:
78+
name: The name of the plugin to check.
79+
80+
Returns:
81+
True if the plugin is registered, False otherwise.
82+
"""
83+
return name in self._plugins
84+
85+
def list_plugins(self) -> list[str]:
86+
"""Get a list of all registered plugin names.
87+
88+
Returns:
89+
A list of plugin names in registration order.
90+
"""
91+
return list(self._plugins.keys())

tests/strands/hooks/test_registry.py

Lines changed: 1 addition & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from strands.hooks import AgentInitializedEvent, BeforeInvocationEvent, BeforeToolCallEvent, HookRegistry, Plugin
5+
from strands.hooks import AgentInitializedEvent, BeforeInvocationEvent, BeforeToolCallEvent, HookRegistry
66
from strands.interrupt import Interrupt, _InterruptState
77

88

@@ -168,115 +168,3 @@ def __call__(self, event: "NonExistentType") -> None: # noqa: F821
168168

169169
with pytest.raises(ValueError, match="failed to get type hints for callback"):
170170
registry.add_callback(None, callback)
171-
172-
173-
# Plugin Protocol Tests
174-
175-
176-
def test_plugin_protocol_is_runtime_checkable():
177-
"""Test that Plugin Protocol is runtime checkable with isinstance."""
178-
179-
class MyPlugin:
180-
name = "my-plugin"
181-
182-
def init_plugin(self, agent):
183-
pass
184-
185-
plugin = MyPlugin()
186-
assert isinstance(plugin, Plugin)
187-
188-
189-
def test_plugin_protocol_sync_implementation():
190-
"""Test Plugin Protocol works with synchronous init_plugin."""
191-
192-
class SyncPlugin:
193-
name = "sync-plugin"
194-
195-
def init_plugin(self, agent):
196-
agent.custom_attribute = "initialized by plugin"
197-
198-
plugin = SyncPlugin()
199-
mock_agent = unittest.mock.Mock()
200-
201-
# Verify the plugin matches the protocol
202-
assert isinstance(plugin, Plugin)
203-
assert plugin.name == "sync-plugin"
204-
205-
# Execute init_plugin synchronously
206-
plugin.init_plugin(mock_agent)
207-
assert mock_agent.custom_attribute == "initialized by plugin"
208-
209-
210-
@pytest.mark.asyncio
211-
async def test_plugin_protocol_async_implementation():
212-
"""Test Plugin Protocol works with asynchronous init_plugin."""
213-
214-
class AsyncPlugin:
215-
name = "async-plugin"
216-
217-
async def init_plugin(self, agent):
218-
agent.custom_attribute = "initialized by async plugin"
219-
220-
plugin = AsyncPlugin()
221-
mock_agent = unittest.mock.Mock()
222-
223-
# Verify the plugin matches the protocol
224-
assert isinstance(plugin, Plugin)
225-
assert plugin.name == "async-plugin"
226-
227-
# Execute init_plugin asynchronously
228-
await plugin.init_plugin(mock_agent)
229-
assert mock_agent.custom_attribute == "initialized by async plugin"
230-
231-
232-
def test_plugin_protocol_requires_name():
233-
"""Test that Plugin Protocol requires a name property."""
234-
235-
class PluginWithoutName:
236-
def init_plugin(self, agent):
237-
pass
238-
239-
plugin = PluginWithoutName()
240-
# A class without 'name' should not pass isinstance check
241-
assert not isinstance(plugin, Plugin)
242-
243-
244-
def test_plugin_protocol_requires_init_plugin_method():
245-
"""Test that Plugin Protocol requires an init_plugin method."""
246-
247-
class PluginWithoutInitPlugin:
248-
name = "incomplete-plugin"
249-
250-
plugin = PluginWithoutInitPlugin()
251-
# A class without 'init_plugin' should not pass isinstance check
252-
assert not isinstance(plugin, Plugin)
253-
254-
255-
def test_plugin_protocol_with_class_attribute_name():
256-
"""Test Plugin Protocol works when name is a class attribute."""
257-
258-
class PluginWithClassAttribute:
259-
name: str = "class-attr-plugin"
260-
261-
def init_plugin(self, agent):
262-
pass
263-
264-
plugin = PluginWithClassAttribute()
265-
assert isinstance(plugin, Plugin)
266-
assert plugin.name == "class-attr-plugin"
267-
268-
269-
def test_plugin_protocol_with_property_name():
270-
"""Test Plugin Protocol works when name is a property."""
271-
272-
class PluginWithProperty:
273-
@property
274-
def name(self):
275-
return "property-plugin"
276-
277-
def init_plugin(self, agent):
278-
pass
279-
280-
plugin = PluginWithProperty()
281-
assert isinstance(plugin, Plugin)
282-
assert plugin.name == "property-plugin"

tests/strands/plugins/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for the plugins module."""

0 commit comments

Comments
 (0)