-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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
messagesattribute toBeforeInvocationEventdataclass - Use
Noneas default value for backward compatibility - Make
messageswritable to allow hooks to modify messages in-place for redaction - Type:
Messages | None(whereMessages = 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
messagesparameter when invokingBeforeInvocationEvent - 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,AfterInvocationEventshould 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
-
BeforeInvocationEventhas amessagesattribute withNonedefault -
messagesattribute is writable (hooks can modify in-place) -
_run_loop()passes messages toBeforeInvocationEvent - Existing hooks that don't use
messagescontinue to work (backward compatibility) -
AfterInvocationEventfires even ifBeforeInvocationEventhook raises an exception - Unit tests cover the new functionality
-
docs/HOOKS.mdis 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 memoryOriginal Problem Statement
Today the next-earliest workaround is for an input guardrail to hook on to
MessageAddedEventinstead (since this'll get called as soon as the agent initializes its messages list, beforeBeforeModelCallEvent)... But this is not ideal becauseMessageAddedis a typical place for session/memory managers (likeAgentCoreMemorySessionManager) 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
- [FEATURE] Add ability to bypass LLM invocation and provide custom responses in hooks strands-agents/sdk-python#758 (guardrail hook aborting invocation)