Conversation
Introduces the Plugin abstraction for the Strands Agents SDK that allows bundling @tool and @hook decorated methods into a single reusable unit. Key changes: - New src/strands/plugins/ package with Plugin base class - Plugin auto-discovers @tool and @hook decorated methods on subclasses - Plugin.tools and Plugin.hooks are filterable lists - Plugin.init_plugin(agent) lifecycle callback for post-registration setup - Agent.__init__ accepts plugins=[] parameter to register plugin tools/hooks - Includes @hook decorator (from PR strands-agents#1581) for decorator-based hooks - Exports Plugin and hook from top-level strands package Usage: class MyPlugin(Plugin): name = 'my-plugin' @tool def my_tool(self, x: str) -> str: return x @hook def on_invoke(self, event: BeforeInvocationEvent) -> None: pass agent = Agent(plugins=[MyPlugin()])
Plugin.init_plugin now accepts **kwargs so that future SDK versions can pass additional keyword arguments without breaking existing plugin subclasses. Updated all test overrides to follow the same pattern.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| "HookCallback", | ||
| "HookRegistry", | ||
| "HookEvent", | ||
| "BaseHookEvent", |
There was a problem hiding this comment.
Issue: The __all__ list isn't alphabetically sorted, making it harder to maintain.
Suggestion: Sort alphabetically for consistency with other modules in the codebase. The Events and Registry sections are good, but entries within each section could be sorted.
| # returns a properly bound DecoratedFunctionTool. | ||
| bound_tool: DecoratedFunctionTool[..., Any] = getattr(self, attr_name) | ||
| self._tools.append(bound_tool) | ||
| logger.debug( |
There was a problem hiding this comment.
Issue: Logging format doesn't follow the structured logging style defined in AGENTS.md.
Suggestion: Use the standard format with field=<value> pairs:
logger.debug(
"plugin=<%s>, tool=<%s> | discovered tool",
self.name or type(self).__name__,
bound_tool.tool_name,
)Same applies to the hook discovery log on line 134.
| """ | ||
| # Try to extract from type hints | ||
| try: | ||
| type_hints = get_type_hints(self.func) |
There was a problem hiding this comment.
Issue: Silent exception swallowing makes debugging harder when type hints fail to resolve.
Suggestion: Consider logging a debug message when get_type_hints fails, so developers can understand why their type hints aren't being resolved:
try:
type_hints = get_type_hints(self.func)
except Exception as e:
logger.debug("func=<%s>, error=<%s> | failed to resolve type hints", self.func.__name__, e)
type_hints = {}| override this with a meaningful value. | ||
| """ | ||
|
|
||
| name: str = "" |
There was a problem hiding this comment.
Issue: Default empty string for name makes it easy to forget setting it, and empty strings can be confusing in logs/debugging.
Suggestion: Consider making name a required class attribute by raising an error if not set, or use the class name as a default:
@property
def name(self) -> str:
"""Plugin name, defaults to class name if not set."""
return self._name or type(self).__name__
name: str = "" # Override in subclassAlternatively, document more clearly that subclasses should always override this.
Review SummaryAssessment: Comment (Draft PR) This is a solid implementation of the Plugin system that aligns well with the design document and SDK tenets. The code is well-organized with good test coverage (39 tests). Key ObservationsAPI Bar Raising
Code Quality
Documentation
The implementation demonstrates thoughtful API design with |
Description
Introduces the
Pluginbase class — a new abstraction that lets plugin authors bundle@tooland@hookdecorated methods into a single, self-contained unit that registers with an agent in one step.Key additions:
src/strands/plugins/plugin.py—Pluginbase class with auto-discovery. On instantiation, it scans the subclass MRO for methods decorated with@tooland@hook, collecting them into mutable.toolsand.hookslists. These lists can be filtered/replaced before handing the plugin to anAgent. Aninit_plugin(agent, **kwargs)lifecycle hook is called after registration for additional setup (e.g. mutatingsystem_prompt).**kwargsis included for forward compatibility.src/strands/hooks/decorator.py— The@hookdecorator (from PR feat(hooks): Add hook decorator #1581) that transforms functions intoHookProviderimplementations with automatic event type detection from type hints. Supports single events, union types, async, and class method binding via the descriptor protocol.src/strands/agent/agent.py— Newpluginskeyword parameter onAgent.__init__. For each plugin, the agent registers its tools intotool_registry, adds its hooks to theHookRegistry, and callsinit_plugin(agent)— all before firingAgentInitializedEvent.src/strands/__init__.pyandsrc/strands/hooks/__init__.py— ExportsPluginandhookfrom the top-level package.Usage:
Related Issues
Design doc: https://github.com/strands-agents/docs/blob/main/designs/0001-plugins.md
Documentation PR
N/A — docs PR to follow.
Type of Change
New feature
Testing
39 new tests across two files:
tests/strands/plugins/test_plugin.py(23 tests) — auto-discovery of@tool/@hookmethods, tool binding and callability, hook binding, property setter filtering,init_pluginlifecycle,nameattribute,__repr__, inheritance, multi-event hooks, empty plugins.tests/strands/plugins/test_plugin_agent_integration.py(16 tests) — tool registration with agent, combining plugin tools with standalone tools, multiple plugins, hook registration,init_plugincalled and can modify agent, filtering before agent creation, no-plugins/empty-plugins/omitted-plugins backwards compatibility, top-level import verification.Full existing test suite (1517 tests) passes with no regressions.
hatch run prepareChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.