Skip to content

Commit 0de7ae9

Browse files
committed
feat(hooks): add AgentResult to AfterInvocationEvent
1 parent 3061116 commit 0de7ae9

File tree

4 files changed

+70
-4
lines changed

4 files changed

+70
-4
lines changed

src/strands/agent/agent.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,7 @@ async def _run_loop(
733733
"""
734734
await self.hooks.invoke_callbacks_async(BeforeInvocationEvent(agent=self))
735735

736+
agent_result: AgentResult | None = None
736737
try:
737738
yield InitEventLoopEvent()
738739

@@ -761,9 +762,13 @@ async def _run_loop(
761762
self._session_manager.redact_latest_message(self.messages[-1], self)
762763
yield event
763764

765+
# Capture the result from the final event if available
766+
if hasattr(event, "__getitem__") and "stop" in event:
767+
agent_result = AgentResult(*event["stop"])
768+
764769
finally:
765770
self.conversation_manager.apply_management(self)
766-
await self.hooks.invoke_callbacks_async(AfterInvocationEvent(agent=self))
771+
await self.hooks.invoke_callbacks_async(AfterInvocationEvent(agent=self, result=agent_result))
767772

768773
async def _execute_event_loop_cycle(
769774
self, invocation_state: dict[str, Any], structured_output_context: StructuredOutputContext | None = None

src/strands/hooks/events.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55

66
import uuid
77
from dataclasses import dataclass
8-
from typing import Any, Optional
8+
from typing import TYPE_CHECKING, Any, Optional
99

1010
from typing_extensions import override
1111

12+
if TYPE_CHECKING:
13+
from ..agent.agent_result import AgentResult
14+
1215
from ..types.content import Message
1316
from ..types.interrupt import _Interruptible
1417
from ..types.streaming import StopReason
@@ -60,8 +63,15 @@ class AfterInvocationEvent(HookEvent):
6063
- Agent.__call__
6164
- Agent.stream_async
6265
- Agent.structured_output
66+
67+
Attributes:
68+
result: The result of the agent invocation, if available. This will be None
69+
when invoked from structured_output methods, as those return typed output
70+
directly rather than AgentResult.
6371
"""
6472

73+
result: Optional["AgentResult"] = None
74+
6575
@property
6676
def should_reverse_callbacks(self) -> bool:
6777
"""True to invoke callbacks in reverse order."""

tests/strands/agent/hooks/test_events.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
from strands.agent.agent_result import AgentResult
56
from strands.hooks import (
67
AfterInvocationEvent,
78
AfterToolCallEvent,
@@ -10,6 +11,7 @@
1011
BeforeToolCallEvent,
1112
MessageAddedEvent,
1213
)
14+
from strands.types.content import Message
1315
from strands.types.tools import ToolResult, ToolUse
1416

1517

@@ -138,3 +140,46 @@ def test_after_tool_invocation_event_cannot_write_properties(after_tool_event):
138140
after_tool_event.invocation_state = {}
139141
with pytest.raises(AttributeError, match="Property exception is not writable"):
140142
after_tool_event.exception = Exception("test")
143+
144+
145+
def test_after_invocation_event_has_optional_result(agent):
146+
"""Test that AfterInvocationEvent has optional result field."""
147+
# Test with no result (structured_output case)
148+
event_without_result = AfterInvocationEvent(agent=agent)
149+
assert event_without_result.result is None
150+
151+
# Test with result (normal invocation case)
152+
mock_message: Message = {"role": "assistant", "content": [{"text": "test"}]}
153+
mock_result = AgentResult(
154+
stop_reason="end_turn",
155+
message=mock_message,
156+
metrics={},
157+
state={},
158+
)
159+
event_with_result = AfterInvocationEvent(agent=agent, result=mock_result)
160+
assert event_with_result.result == mock_result
161+
assert event_with_result.result.stop_reason == "end_turn"
162+
163+
164+
def test_after_invocation_event_result_not_writable(agent):
165+
"""Test that result property is not writable after initialization."""
166+
mock_message: Message = {"role": "assistant", "content": [{"text": "test"}]}
167+
mock_result = AgentResult(
168+
stop_reason="end_turn",
169+
message=mock_message,
170+
metrics={},
171+
state={},
172+
)
173+
174+
event = AfterInvocationEvent(agent=agent, result=None)
175+
176+
with pytest.raises(AttributeError, match="Property result is not writable"):
177+
event.result = mock_result
178+
179+
180+
def test_after_invocation_event_agent_not_writable(agent):
181+
"""Test that agent property is not writable."""
182+
event = AfterInvocationEvent(agent=agent)
183+
184+
with pytest.raises(AttributeError, match="Property agent is not writable"):
185+
event.agent = Mock()

tests/strands/agent/test_agent_hooks.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,10 @@ def test_agent__call__hooks(agent, hook_provider, agent_tool, mock_model, tool_u
197197
)
198198
assert next(events) == MessageAddedEvent(agent=agent, message=agent.messages[3])
199199

200-
assert next(events) == AfterInvocationEvent(agent=agent)
200+
after_invocation_event = next(events)
201+
assert isinstance(after_invocation_event, AfterInvocationEvent)
202+
assert after_invocation_event.agent == agent
203+
assert after_invocation_event.result is not None
201204

202205
assert len(agent.messages) == 4
203206

@@ -261,7 +264,10 @@ async def test_agent_stream_async_hooks(agent, hook_provider, agent_tool, mock_m
261264
)
262265
assert next(events) == MessageAddedEvent(agent=agent, message=agent.messages[3])
263266

264-
assert next(events) == AfterInvocationEvent(agent=agent)
267+
after_invocation_event = next(events)
268+
assert isinstance(after_invocation_event, AfterInvocationEvent)
269+
assert after_invocation_event.agent == agent
270+
assert after_invocation_event.result is not None
265271

266272
assert len(agent.messages) == 4
267273

0 commit comments

Comments
 (0)