Skip to content

feat(plugins): improve plugin creation devex with @hook and @tool decorators#1740

Open
Unshure wants to merge 9 commits intomainfrom
agent-tasks/1739
Open

feat(plugins): improve plugin creation devex with @hook and @tool decorators#1740
Unshure wants to merge 9 commits intomainfrom
agent-tasks/1739

Conversation

@Unshure
Copy link
Member

@Unshure Unshure commented Feb 19, 2026

Motivation

Currently, plugin authors must manually register hooks in their init_plugin() method, which is verbose and error-prone:

class MyPlugin:
    name = "my-plugin"
    
    def init_plugin(self, agent: Agent) -> None:
        agent.add_hook(self.log_call, BeforeModelCallEvent)

This PR enables declarative hook registration using a @hook decorator, making plugin development more intuitive and reducing boilerplate:

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

    @hook
    def log_call(self, event: BeforeModelCallEvent):
        print(event)

    @tool
    def printer(self, log: str):
        print(log)
        return "Printed log"

Resolves: #1739

Public API Changes

New @hook decorator

The @hook decorator marks methods for automatic registration:

from strands.plugins import Plugin, hook
from strands.hooks import BeforeModelCallEvent, AfterModelCallEvent

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

    # Single event type - inferred from type hint
    @hook
    def on_model_call(self, event: BeforeModelCallEvent):
        print(event)

    # Union types - registers for multiple events
    @hook
    def on_any_model_event(self, event: BeforeModelCallEvent | AfterModelCallEvent):
        print(event)
``

@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 94.50549% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/strands/hooks/_type_inference.py 91.42% 3 Missing ⚠️
src/strands/plugins/plugin.py 93.33% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@mkmeral
Copy link
Contributor

mkmeral commented Feb 23, 2026

/strands don't say mutable or immutable for in the comment. just say this is a list of hooks/tools the plugin provides, autogenerated from decorators

mkmeral
mkmeral previously approved these changes Feb 23, 2026
@mkmeral
Copy link
Contributor

mkmeral commented Feb 25, 2026

/strands for discovering decorated methods, instead of using dir, let's use MRO (method resolution order), so we keep the order of hooks as they are defined in the class. See example code

def _discover_decorated_methods(self) -> None:
    """Scan class for @hook and @tool decorated methods in declaration order."""
    seen = set()
    # Walk MRO so parent class hooks come first, child overrides win
    for cls in reversed(type(self).__mro__):
        for name, attr in cls.__dict__.items():
            if name in seen:
                continue
            seen.add(name)

            # Get the bound method from self
            try:
                bound = getattr(self, name)
            except Exception:
                continue

            if hasattr(bound, "_hook_event_types") and callable(bound):
                self._hooks.append(bound)

            if isinstance(bound, DecoratedFunctionTool):
                self._tools.append(bound)

@github-actions
Copy link

Updated hook/tool discovery to use MRO instead of dir() to preserve definition order. Parent class hooks come first, child overrides win. Added test to verify definition order is preserved.

mkmeral
mkmeral previously approved these changes Feb 25, 2026
@mkmeral mkmeral mentioned this pull request Feb 25, 2026
7 tasks
strands-agent and others added 9 commits February 25, 2026 14:00
- Create @hook decorator for declarative hook registration in plugins
- Convert Plugin from Protocol to base class (breaking change)
- Add auto-discovery of @hook and @tool decorated methods in Plugin.__init__()
- Add auto-registration of hooks and tools in Plugin.init_plugin()
- Support union types for multiple event types (e.g., BeforeModelCallEvent | AfterModelCallEvent)
- Export hook from strands.plugins and strands namespaces
- Update existing tests to use inheritance-based approach
- Add comprehensive test coverage for new functionality

BREAKING CHANGE: Plugin is now a base class instead of a Protocol. Existing
plugins must inherit from Plugin instead of just implementing the protocol.
…properties

Address PR feedback:
- Add public 'hooks' and 'tools' properties returning tuples for user customization
- Move hook/tool auto-registration from Plugin.init_plugin() to _PluginRegistry.add_and_init()
- Remove need for super().init_plugin(agent) - users only implement custom logic
- Update steering handler to use new simpler pattern
- Update all tests to use registry-based registration

This simplifies plugin development:
- Before: Users had to call super().init_plugin(agent) for auto-registration
- After: init_plugin() is purely for custom logic, registry handles auto-registration
Address additional PR feedback:
- Make hooks and tools properties return mutable lists for filtering/customization
- Fix type annotation: _hook_event_types is list[type[TEvent]] not list[TEvent]
- Export @hook from top-level strands package (from strands import hook)
- Fix docstring typo: 'argument' -> 'attribute'
- Add tests for filtering hooks and tools
- Fix Plugin class docstring: 'read-only' -> 'mutable for filtering'
- Add _type_inference.py to AGENTS.md hooks directory listing
Remove mutable/immutable language, just describe what they are:
- List of hooks/tools the plugin provides
- Auto-discovered from decorated methods
- Replace dir() with __mro__ iteration to maintain declaration order
- Parent class hooks come first, child overrides win
- Add test verifying definition order is preserved (not alphabetical)
@github-actions
Copy link

Review Update - Commit 703cece

All feedback from @mkmeral has been addressed:

Feedback Status
Type annotation bug (list[TEvent]list[type[TEvent]]) ✅ Fixed
@hook export from top-level strands ✅ Fixed
MRO-based discovery for definition order ✅ Fixed
Docstring wording ("read-only" removed) ✅ Fixed
Architecture Improvements

The refactoring brought a cleaner design:

  • Registration moved to _PluginRegistry: Hooks/tools are now auto-registered by the registry, not the plugin itself
  • No super() needed: Plugin.init_agent() is purely for custom initialization
  • MRO ordering: Parent class hooks come first, child overrides win
  • Public hooks/tools properties: Expose discovered methods for inspection

Recommendation: ✅ Ready for approval - Clean implementation with all feedback incorporated.

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.

[FEATURE] Create @hook decorator for Plugins

3 participants