Skip to content

[FEATURE] Expose input messages to BeforeInvocationEvent hook #8

@Unshure

Description

@Unshure

Overview

To implement standalone input-side guardrails (for e.g. PII, toxic content, prompt attack prevention), we'd like to place a Hook as early as possible in the invocation. In particular, we want to make sure it runs (and has the opportunity to redact the user's input message) before the message gets added to memory by e.g. AgentCoreMemorySessionManager, which hooks on MessageAddedEvent.

However, the current BeforeInvocationEvent hook only receives a reference to the agent and has no visibility of the incoming messages because they haven't been added to agent.messages yet.


Implementation Requirements

Based on clarification discussion and repository analysis:

Technical Approach

1. Extend BeforeInvocationEvent to include messages

Modify src/strands/hooks/events.py:

  • Add a messages attribute to BeforeInvocationEvent dataclass
  • Use None as default value for backward compatibility
  • Make messages writable to allow hooks to modify messages in-place for redaction
  • Type: Messages | None (where Messages = list[Message])
@dataclass
class BeforeInvocationEvent(HookEvent):
    """Event triggered at the beginning of a new agent request.
    
    Attributes:
        messages: The input messages for this invocation. Can be modified by hooks
            to redact or transform content before processing. May be None for
            backward compatibility or when invoked from deprecated methods.
    """
    messages: Messages | None = None

    def _can_write(self, name: str) -> bool:
        return name == "messages"

2. Update _run_loop() to pass messages

Modify src/strands/agent/agent.py in the _run_loop() method:

  • Pass the messages parameter when invoking BeforeInvocationEvent
  • The event should be raised before messages are appended to agent.messages
# Current (line ~647):
await self.hooks.invoke_callbacks_async(BeforeInvocationEvent(agent=self))

# Updated:
await self.hooks.invoke_callbacks_async(BeforeInvocationEvent(agent=self, messages=messages))

3. Do NOT modify structured_output_async()

This method is deprecated and should not be updated. The messages attribute will remain None when BeforeInvocationEvent is invoked from this deprecated code path.

Error Handling

  • If a guardrail hook raises an exception in BeforeInvocationEvent, AfterInvocationEvent should still be triggered (maintaining the paired event guarantee)
  • Hooks can raise exceptions to abort the entire agent invocation (useful when redaction leaves nothing useful)

Files to Modify

File Changes
src/strands/hooks/events.py Add messages attribute to BeforeInvocationEvent, implement _can_write()
src/strands/agent/agent.py Pass messages to BeforeInvocationEvent in _run_loop()
tests/strands/agent/hooks/test_events.py Add tests for new messages attribute and writability
tests/strands/agent/test_agent_hooks.py Update existing tests to verify messages are passed correctly
docs/HOOKS.md Document the new messages attribute and its use for input guardrails

Acceptance Criteria

  • BeforeInvocationEvent has a messages attribute with None default
  • messages attribute is writable (hooks can modify in-place)
  • _run_loop() passes messages to BeforeInvocationEvent
  • Existing hooks that don't use messages continue to work (backward compatibility)
  • AfterInvocationEvent fires even if BeforeInvocationEvent hook raises an exception
  • Unit tests cover the new functionality
  • docs/HOOKS.md is updated with guidance on input guardrails

Example Usage

from strands import Agent
from strands.hooks import BeforeInvocationEvent, HookProvider, HookRegistry

class InputGuardrailHook(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeInvocationEvent, self.check_input)
    
    async def check_input(self, event: BeforeInvocationEvent) -> None:
        if event.messages is None:
            return
        
        for message in event.messages:
            if contains_pii(message["content"]):
                # Option 1: Redact in-place
                message["content"] = redact_pii(message["content"])
                
                # Option 2: Abort invocation
                # raise ValueError("PII detected in input")

agent = Agent(hooks=[InputGuardrailHook()])
agent("Process this message")  # Guardrail runs before message is added to memory

Original Problem Statement

Today the next-earliest workaround is for an input guardrail to hook on to MessageAddedEvent instead (since this'll get called as soon as the agent initializes its messages list, before BeforeModelCallEvent)... But this is not ideal because MessageAdded is a typical place for session/memory managers (like AgentCoreMemorySessionManager) to hook - so relies on users to connect their guardrail and memory hooks in the right sequence to avoid leakage of sensitive/malicious input into memory. It should work, but is easy to mis-configure without realizing.

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions