|
11 | 11 |
|
12 | 12 | import boto3 |
13 | 13 | from botocore.config import Config as BotocoreConfig |
| 14 | +from strands.experimental.hooks.events import ( |
| 15 | + BidiAfterInvocationEvent, |
| 16 | + BidiAgentInitializedEvent, |
| 17 | + BidiMessageAddedEvent, |
| 18 | +) |
14 | 19 | from strands.experimental.hooks.multiagent.events import ( |
15 | 20 | AfterMultiAgentInvocationEvent, |
16 | 21 | AfterNodeCallEvent, |
@@ -942,50 +947,56 @@ def register_hooks(self, registry: HookRegistry, **kwargs) -> None: |
942 | 947 |
|
943 | 948 | # Async mode: register async callbacks that offload the existing sync |
944 | 949 | # methods to a worker thread via asyncio.to_thread. AgentInitializedEvent |
945 | | - # must stay sync (Strands disallows async callbacks on this event; see |
946 | | - # strands/hooks/registry.py:174). |
| 950 | + # and BidiAgentInitializedEvent must stay sync (Strands disallows async |
| 951 | + # callbacks for AgentInitializedEvent — see strands/hooks/registry.py:227). |
947 | 952 | logger.warning( |
948 | 953 | "AgentCoreMemorySessionManager async_mode=True: the agent must be invoked " |
949 | 954 | "via the async path (e.g. agent.stream_async(...) or agent.invoke_async(...)). " |
950 | 955 | "Sync invocation will raise RuntimeError from Strands' hook registry." |
951 | 956 | ) |
952 | 957 |
|
| 958 | + def _offload(method, *event_args): |
| 959 | + """Build an async callback that offloads `method(*[a(event) for a in event_args])` to a thread. |
| 960 | +
|
| 961 | + Each entry in `event_args` is a callable that extracts an argument from the event; |
| 962 | + pass none for a zero-arg method. |
| 963 | + """ |
| 964 | + |
| 965 | + async def _callback(event): |
| 966 | + await asyncio.to_thread(method, *(extract(event) for extract in event_args)) |
| 967 | + |
| 968 | + return _callback |
| 969 | + |
953 | 970 | registry.add_callback(AgentInitializedEvent, lambda event: self.initialize(event.agent)) |
954 | 971 |
|
955 | 972 | async def _on_message_added_persist(event: MessageAddedEvent) -> None: |
956 | 973 | await asyncio.to_thread(self.append_message, event.message, event.agent) |
957 | 974 | await asyncio.to_thread(self.sync_agent, event.agent) |
958 | 975 |
|
959 | | - async def _on_message_added_retrieve(event: MessageAddedEvent) -> None: |
960 | | - await asyncio.to_thread(self.retrieve_customer_context, event) |
961 | | - |
962 | | - async def _on_after_invocation_sync(event: AfterInvocationEvent) -> None: |
963 | | - await asyncio.to_thread(self.sync_agent, event.agent) |
964 | | - |
965 | 976 | registry.add_callback(MessageAddedEvent, _on_message_added_persist) |
966 | | - registry.add_callback(AfterInvocationEvent, _on_after_invocation_sync) |
967 | | - registry.add_callback(MessageAddedEvent, _on_message_added_retrieve) |
| 977 | + registry.add_callback(AfterInvocationEvent, _offload(self.sync_agent, lambda e: e.agent)) |
| 978 | + registry.add_callback(MessageAddedEvent, _offload(self.retrieve_customer_context, lambda e: e)) |
968 | 979 |
|
969 | 980 | if self.config.batch_size > 1: |
970 | | - |
971 | | - async def _on_after_invocation_flush(event: AfterInvocationEvent) -> None: |
972 | | - await asyncio.to_thread(self._flush_messages) |
973 | | - |
974 | | - registry.add_callback(AfterInvocationEvent, _on_after_invocation_flush) |
| 981 | + registry.add_callback(AfterInvocationEvent, _offload(self._flush_messages)) |
975 | 982 |
|
976 | 983 | # Register multi-agent callbacks so async-mode parity matches sync-mode |
977 | | - async def _on_multi_agent_initialized(event: MultiAgentInitializedEvent) -> None: |
978 | | - await asyncio.to_thread(self.initialize_multi_agent, event.source) |
| 984 | + registry.add_callback(MultiAgentInitializedEvent, _offload(self.initialize_multi_agent, lambda e: e.source)) |
| 985 | + registry.add_callback(AfterNodeCallEvent, _offload(self.sync_multi_agent, lambda e: e.source)) |
| 986 | + registry.add_callback(AfterMultiAgentInvocationEvent, _offload(self.sync_multi_agent, lambda e: e.source)) |
979 | 987 |
|
980 | | - async def _on_after_node_call(event: AfterNodeCallEvent) -> None: |
981 | | - await asyncio.to_thread(self.sync_multi_agent, event.source) |
| 988 | + # Register BidiAgent callbacks so async-mode parity matches sync-mode. |
| 989 | + # BidiAgentInitializedEvent dispatches through invoke_callbacks (sync), |
| 990 | + # so its callback must stay sync; the other two dispatch through |
| 991 | + # invoke_callbacks_async, so async wrappers are safe. |
| 992 | + registry.add_callback(BidiAgentInitializedEvent, lambda event: self.initialize_bidi_agent(event.agent)) |
982 | 993 |
|
983 | | - async def _on_after_multi_agent_invocation(event: AfterMultiAgentInvocationEvent) -> None: |
984 | | - await asyncio.to_thread(self.sync_multi_agent, event.source) |
| 994 | + async def _on_bidi_message_added(event: BidiMessageAddedEvent) -> None: |
| 995 | + await asyncio.to_thread(self.append_bidi_message, event.message, event.agent) |
| 996 | + await asyncio.to_thread(self.sync_bidi_agent, event.agent) |
985 | 997 |
|
986 | | - registry.add_callback(MultiAgentInitializedEvent, _on_multi_agent_initialized) |
987 | | - registry.add_callback(AfterNodeCallEvent, _on_after_node_call) |
988 | | - registry.add_callback(AfterMultiAgentInvocationEvent, _on_after_multi_agent_invocation) |
| 998 | + registry.add_callback(BidiMessageAddedEvent, _on_bidi_message_added) |
| 999 | + registry.add_callback(BidiAfterInvocationEvent, _offload(self.sync_bidi_agent, lambda e: e.agent)) |
989 | 1000 |
|
990 | 1001 | @override |
991 | 1002 | def initialize(self, agent: "Agent", **kwargs: Any) -> None: |
@@ -1141,6 +1152,7 @@ def _flush_agent_states_only(self) -> list[dict[str, Any]]: |
1141 | 1152 |
|
1142 | 1153 | with self._agent_state_lock: |
1143 | 1154 | agent_states_to_send = list(self._agent_state_buffer) |
| 1155 | + self._agent_state_buffer.clear() |
1144 | 1156 |
|
1145 | 1157 | if not agent_states_to_send: |
1146 | 1158 | return [] |
@@ -1171,11 +1183,10 @@ def _flush_agent_states_only(self) -> list[dict[str, Any]]: |
1171 | 1183 | results.append(event) |
1172 | 1184 | logger.debug("Flushed %d agent states for agent %s: %s", len(payloads), agent_id, event.get("eventId")) |
1173 | 1185 |
|
1174 | | - # Clear agent state buffer only after ALL events succeed |
1175 | | - with self._agent_state_lock: |
1176 | | - self._agent_state_buffer.clear() |
1177 | | - |
1178 | 1186 | except Exception as e: |
| 1187 | + # Restore agent states to buffer so they aren't lost |
| 1188 | + with self._agent_state_lock: |
| 1189 | + self._agent_state_buffer.extend(agent_states_to_send) |
1179 | 1190 | logger.error("Failed to flush agent states to AgentCore Memory: %s", e) |
1180 | 1191 | raise SessionException(f"Failed to flush agent states: {e}") from e |
1181 | 1192 |
|
|
0 commit comments