Skip to content

feat(plugins): add plugins#1692

Draft
mkmeral wants to merge 2 commits intostrands-agents:mainfrom
mkmeral:feature/plugins
Draft

feat(plugins): add plugins#1692
mkmeral wants to merge 2 commits intostrands-agents:mainfrom
mkmeral:feature/plugins

Conversation

@mkmeral
Copy link
Contributor

@mkmeral mkmeral commented Feb 13, 2026

Description

Introduces the Plugin base class — a new abstraction that lets plugin authors bundle @tool and @hook decorated methods into a single, self-contained unit that registers with an agent in one step.

Key additions:

  • src/strands/plugins/plugin.pyPlugin base class with auto-discovery. On instantiation, it scans the subclass MRO for methods decorated with @tool and @hook, collecting them into mutable .tools and .hooks lists. These lists can be filtered/replaced before handing the plugin to an Agent. An init_plugin(agent, **kwargs) lifecycle hook is called after registration for additional setup (e.g. mutating system_prompt). **kwargs is included for forward compatibility.
  • src/strands/hooks/decorator.py — The @hook decorator (from PR feat(hooks): Add hook decorator #1581) that transforms functions into HookProvider implementations 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 — New plugins keyword parameter on Agent.__init__. For each plugin, the agent registers its tools into tool_registry, adds its hooks to the HookRegistry, and calls init_plugin(agent) — all before firing AgentInitializedEvent.
  • src/strands/__init__.py and src/strands/hooks/__init__.py — Exports Plugin and hook from the top-level package.

Usage:

from strands import Agent, Plugin, tool, hook
from strands.hooks import BeforeInvocationEvent

class MyPlugin(Plugin):
    name = "my-plugin"

    @tool
    def my_tool(self, x: str) -> str:
        """Do something useful."""
        return x.upper()

    @hook
    def on_invoke(self, event: BeforeInvocationEvent) -> None:
        """React to invocations."""
        print(f"Invocation starting for {event.agent.name}")

plugin = MyPlugin()
# Optionally filter before passing to agent:
# plugin.tools = [t for t in plugin.tools if t.tool_name != "my_tool"]
agent = Agent(plugins=[plugin])

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/@hook methods, tool binding and callability, hook binding, property setter filtering, init_plugin lifecycle, name attribute, __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_plugin called 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.

  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Containerized Agent added 2 commits February 13, 2026 19:57
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.
@mkmeral mkmeral changed the title Feature/plugins feat(plugins): add plugins Feb 13, 2026
@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 82.42424% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/strands/hooks/decorator.py 73.87% 19 Missing and 10 partials ⚠️

📢 Thoughts on this report? Let us know!

"HookCallback",
"HookRegistry",
"HookEvent",
"BaseHookEvent",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = ""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 subclass

Alternatively, document more clearly that subclasses should always override this.

@github-actions
Copy link

Review Summary

Assessment: 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 Observations

API Bar Raising

  • This PR introduces a new public API (Plugin, hook decorator) that customers will use frequently. Consider adding the needs-api-review label per API_BAR_RAISING.md.

Code Quality

  • The decorator pattern implementation is clean and follows the existing @tool decorator pattern well.
  • Logging statements should follow the structured format defined in AGENTS.md (field=<value> style).
  • The name class attribute default of empty string could be improved (see inline comment).

Documentation

  • Good docstrings and usage examples in the PR description.
  • Consider updating AGENTS.md directory structure to include the new plugins/ directory.

The implementation demonstrates thoughtful API design with **kwargs for forward compatibility and proper descriptor protocol support. Nice work on the comprehensive test suite covering edge cases like inheritance, multi-event hooks, and filtering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant