Skip to content

[FEATURE] Create @hook decorator for Plugins #1739

@Unshure

Description

@Unshure

Overview

Add a @hook decorator for Plugins to simplify hook registration, and convert the Plugin from a Protocol to a base class that auto-discovers and registers decorated hooks and tools.

Problem Statement

Currently, plugin authors must manually register hooks in their init_plugin() method:

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

Proposed Solution

Enable declarative hook and tool registration using decorators:

class Steering(Plugin):
    name = "steering"

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

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

Implementation Requirements

1. Create @hook Decorator

Location: src/strands/plugins/decorator.py (new file)

Behavior:

  • Mark methods as hook callbacks for automatic registration
  • Infer event type from the callback's type hint (consistent with HookRegistry.add_callback)
  • Support both @hook and @hook() syntax
  • Support union types for multiple event types (e.g., BeforeModelCallEvent | AfterModelCallEvent)
  • Store hook metadata on the decorated method for later discovery

Example Usage:

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

@hook
def on_any_model_event(self, event: BeforeModelCallEvent | AfterModelCallEvent):
    print(event)

2. Convert Plugin from Protocol to Base Class

Location: src/strands/plugins/plugin.py

Breaking Change: This is an intentional breaking change from the current Protocol-based approach.

Behavior:

  • __init__(): Scan the class for @hook and @tool decorated methods and store references
  • init_plugin(agent): Default implementation that:
    • Registers all discovered @hook methods with the agent's hook registry
    • Adds all discovered @tool methods to the agent's tools list
  • Subclasses can override init_plugin() and call super().init_plugin(agent) for custom behavior

Requirements:

  • name: str attribute still required (can be class attribute or property)
  • Support both sync and async init_plugin() implementations
  • Each agent gets its own registration (plugin can be attached to multiple agents)

3. Auto-Registration of Tools

Behavior:

  • Methods decorated with existing @tool decorator should be auto-discovered
  • Tools are added to the agent's tools list during init_plugin()
  • The bound method (with self) should be registered, not the unbound function

4. Public API Exports

Update src/strands/plugins/__init__.py:

from .plugin import Plugin
from .decorator import hook

__all__ = ["Plugin", "hook"]

Update src/strands/__init__.py (if appropriate):

  • Consider exporting hook from the top-level strands namespace

Acceptance Criteria

  • @hook decorator created and functional
  • @hook infers event types from type hints
  • @hook supports union types for multiple events
  • Plugin converted from Protocol to base class
  • Plugin.__init__() discovers decorated methods
  • Plugin.init_plugin() auto-registers hooks and tools
  • @tool decorated methods in plugins are auto-added to agent's tools
  • Multiple agents can use the same plugin instance (each gets its own registrations)
  • Unit tests cover all new functionality
  • Existing @tool decorator continues to work standalone (no breaking changes to tool system)

Files to Create/Modify

New Files

  • src/strands/plugins/decorator.py - @hook decorator implementation

Modified Files

  • src/strands/plugins/plugin.py - Convert Protocol to base class
  • src/strands/plugins/__init__.py - Export hook decorator
  • src/strands/__init__.py - Optionally export hook
  • tests/strands/plugins/test_plugins.py - Update existing tests for new class-based approach
  • tests/strands/plugins/test_hook_decorator.py - New tests for @hook decorator

Technical Notes

Decorator Implementation Pattern

Follow the existing @tool decorator pattern in src/strands/tools/decorator.py:

  • Support both @hook and @hook() call patterns
  • Use functools.wraps to preserve function metadata
  • Store metadata as attributes on the decorated function (e.g., _hook_event_types)

Method Discovery in __init__

def __init__(self):
    self._hooks = []
    self._tools = []
    for name in dir(self):
        attr = getattr(self, name)
        if hasattr(attr, '_hook_event_types'):
            self._hooks.append(attr)
        if isinstance(attr, DecoratedFunctionTool):
            self._tools.append(attr)

Backward Compatibility Consideration

This is a breaking change from the Protocol-based approach. Users with existing plugins implementing the Protocol will need to:

  1. Inherit from Plugin class instead of implementing the protocol
  2. Their existing init_plugin() implementations will continue to work

Additional Context

  • Related documentation: docs/HOOKS.md
  • Existing hook events: src/strands/hooks/events.py
  • Existing tool decorator: src/strands/tools/decorator.py

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status

Just Shipped

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions