From 9cdc8f8e4038913cf376131d0b6c7e4e6d83d3c1 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 28 Sep 2026 14:31:49 +0000 Subject: [PATCH 1/3] fix(memory): remove experimental Strands hooks --- .../integrations/strands/session_manager.py | 16 ++---- .../test_agentcore_memory_session_manager.py | 57 ++++--------------- 2 files changed, 16 insertions(+), 57 deletions(-) diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index 5e6ac7bc..60f7bb0a 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -11,14 +11,13 @@ import boto3 from botocore.config import Config as BotocoreConfig -from strands.experimental.bidi import BidiAgent -from strands.experimental.bidi.hooks import BidiAgentStopEvent -from strands.experimental.hooks.multiagent.events import ( +from strands.hooks import ( + AfterInvocationEvent, AfterMultiAgentInvocationEvent, AfterNodeCallEvent, + MessageAddedEvent, MultiAgentInitializedEvent, ) -from strands.hooks import AfterInvocationEvent, MessageAddedEvent from strands.hooks.events import AgentInitializedEvent from strands.hooks.registry import HookRegistry from strands.session.repository_session_manager import RepositorySessionManager @@ -846,14 +845,11 @@ def append_message(self, message: Message, agent: "LocalAgent", **kwargs: Any) - self._latest_agent_message[agent.agent_id] = session_message def retrieve_customer_context(self, event: MessageAddedEvent) -> None: - """Retrieve customer LTM context for regular Agent invocations. + """Retrieve customer LTM context before processing a support query. Args: event (MessageAddedEvent): The message added event containing the agent and message data. """ - if isinstance(event.agent, BidiAgent): - return None - messages = event.agent.messages if not messages or messages[-1].get("role") != "user": return None @@ -949,7 +945,6 @@ def register_hooks(self, registry: HookRegistry, **kwargs) -> None: if self.config.batch_size > 1: # Completion callbacks run in reverse order, so register flushes before state syncs. registry.add_callback(AfterInvocationEvent, lambda event: self._flush_messages()) - registry.add_callback(BidiAgentStopEvent, lambda event: self._flush_messages()) RepositorySessionManager.register_hooks(self, registry, **kwargs) registry.add_callback(MessageAddedEvent, lambda event: self.retrieve_customer_context(event)) @@ -979,7 +974,6 @@ async def _callback(event): if self.config.batch_size > 1: # Completion callbacks run in reverse order, so register flushes before state syncs. registry.add_callback(AfterInvocationEvent, _offload(self._flush_messages)) - registry.add_callback(BidiAgentStopEvent, _offload(self._flush_messages)) registry.add_callback(AgentInitializedEvent, lambda event: self.initialize(event.agent)) @@ -996,8 +990,6 @@ async def _on_message_added_persist(event: MessageAddedEvent) -> None: registry.add_callback(AfterNodeCallEvent, _offload(self.sync_multi_agent, lambda e: e.source)) registry.add_callback(AfterMultiAgentInvocationEvent, _offload(self.sync_multi_agent, lambda e: e.source)) - registry.add_callback(BidiAgentStopEvent, _offload(self.sync_agent, lambda e: e.agent)) - @override def initialize(self, agent: "LocalAgent", **kwargs: Any) -> None: if self.has_existing_agent: diff --git a/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py b/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py index 9a68530e..0bd2a888 100644 --- a/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py +++ b/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py @@ -12,14 +12,14 @@ from botocore.config import Config as BotocoreConfig from botocore.exceptions import ClientError from strands.agent.agent import Agent -from strands.experimental.bidi import BidiAgent -from strands.experimental.bidi.hooks import BidiAgentStopEvent -from strands.experimental.hooks.multiagent.events import ( +from strands.hooks import ( + AfterInvocationEvent, AfterMultiAgentInvocationEvent, AfterNodeCallEvent, + AgentInitializedEvent, + MessageAddedEvent, MultiAgentInitializedEvent, ) -from strands.hooks import AfterInvocationEvent, AgentInitializedEvent, MessageAddedEvent from strands.hooks.registry import HookRegistry from strands.types.exceptions import SessionException from strands.types.session import Session, SessionAgent, SessionMessage, SessionType @@ -2693,33 +2693,6 @@ def test_retrieve_customer_context_default_context_tag(self, mock_memory_client) class TestSessionHooks: """Test session lifecycle hook integration.""" - @pytest.mark.parametrize("async_mode", [False, True]) - async def test_bidi_message_persists_without_retrieval( - self, agentcore_config_with_retrieval, mock_memory_client, async_mode - ): - """Bidi messages are persisted without retrieving or injecting context.""" - agentcore_config_with_retrieval.async_mode = async_mode - manager = _create_session_manager(agentcore_config_with_retrieval, mock_memory_client) - manager.session_repository = Mock() - manager._latest_agent_message = {} - agent = Mock( - spec=BidiAgent, - agent_id="test-agent", - messages=[{"role": "user", "content": [{"text": "Hello"}]}], - state=Mock(), - ) - agent.state.get.return_value = {} - mock_memory_client.retrieve_memories.return_value = [{"content": {"text": "User prefers blue"}, "score": 1.0}] - registry = HookRegistry() - manager.register_hooks(registry) - - await registry.invoke_callbacks_async(MessageAddedEvent(agent=agent, message=agent.messages[0])) - - mock_memory_client.retrieve_memories.assert_not_called() - assert agent.messages == [{"role": "user", "content": [{"text": "Hello"}]}] - mock_memory_client.create_event.assert_called_once() - manager.session_repository.update_agent.assert_called_once() - def test_after_invocation_hook_registered(self, batching_session_manager): """Test that AfterInvocationEvent hook is registered when batching is enabled.""" registry = HookRegistry() @@ -2784,10 +2757,7 @@ def spy_add_callback(event_type, callback): assert len(flush_callbacks) == 0 @pytest.mark.parametrize("async_mode", [False, True]) - @pytest.mark.parametrize("event_type", [AfterInvocationEvent, BidiAgentStopEvent]) - async def test_completion_flushes_messages_and_final_state( - self, batching_config, mock_memory_client, async_mode, event_type - ): + async def test_completion_flushes_messages_and_final_state(self, batching_config, mock_memory_client, async_mode): """Completion flushes a partial batch, including the final state update.""" batching_config.async_mode = async_mode manager = _create_session_manager(batching_config, mock_memory_client) @@ -2805,7 +2775,7 @@ async def test_completion_flushes_messages_and_final_state( agent.state.get.return_value = {"status": "stopped"} registry = HookRegistry() manager.register_hooks(registry) - await registry.invoke_callbacks_async(event_type(agent=agent)) + await registry.invoke_callbacks_async(AfterInvocationEvent(agent=agent)) assert manager.pending_message_count() == 0 assert manager.pending_agent_state_count() == 0 @@ -3809,8 +3779,8 @@ def test_async_mode_logs_sync_invocation_warning(self, mock_memory_client, caplo assert any("async_mode=True" in rec.message and "stream_async" in rec.message for rec in caplog.records) - def test_async_mode_registers_bidi_agent_callbacks(self, mock_memory_client): - """async_mode=True: BidiAgent events get callbacks; init stays sync, others are async.""" + def test_async_mode_registers_agent_callbacks(self, mock_memory_client): + """async_mode=True: initialization stays sync while message callbacks are async.""" config = AgentCoreMemoryConfig(memory_id="m", session_id="s", actor_id="a", async_mode=True) manager = _create_session_manager(config, mock_memory_client) registry = HookRegistry() @@ -3820,13 +3790,10 @@ def test_async_mode_registers_bidi_agent_callbacks(self, mock_memory_client): assert init_callbacks assert not any(asyncio.iscoroutinefunction(cb) for cb in init_callbacks) - for event in ( - MessageAddedEvent(agent=Mock(), message={"role": "user", "content": [{"text": "x"}]}), - BidiAgentStopEvent(agent=Mock()), - ): - callbacks = list(registry.get_callbacks_for(event)) - assert callbacks, f"No callbacks registered for {type(event).__name__}" - assert all(asyncio.iscoroutinefunction(cb) for cb in callbacks) + event = MessageAddedEvent(agent=Mock(), message={"role": "user", "content": [{"text": "x"}]}) + callbacks = list(registry.get_callbacks_for(event)) + assert callbacks + assert all(asyncio.iscoroutinefunction(cb) for cb in callbacks) class TestFlushAgentStatesRaceCondition: From 42b35b19f4a0d77d457361c3101cd9279f282154 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 28 Sep 2026 14:31:52 +0000 Subject: [PATCH 2/3] chore(lint): ban experimental Strands imports --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 683a1e11..85fc7825 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,8 +77,12 @@ select = [ "G", # logging format "I", # isort "LOG", # logging + "TID251", # flake8-tidy-imports banned-api ] +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"strands.experimental" = { msg = "Use stable Strands APIs instead." } + [tool.ruff.lint.per-file-ignores] "!src/**/*.py" = ["D"] "src/bedrock_agentcore/memory/metadata-workflow.ipynb" = ["E501"] From 9029e23d2c54e4376ab5ec2d51a3f2e8d529437a Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 28 Sep 2026 19:02:27 +0000 Subject: [PATCH 3/3] docs: restore session context wording --- .../memory/integrations/strands/session_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index 60f7bb0a..190ab27e 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -845,7 +845,7 @@ def append_message(self, message: Message, agent: "LocalAgent", **kwargs: Any) - self._latest_agent_message[agent.agent_id] = session_message def retrieve_customer_context(self, event: MessageAddedEvent) -> None: - """Retrieve customer LTM context before processing a support query. + """Retrieve customer LTM context for regular Agent invocations. Args: event (MessageAddedEvent): The message added event containing the agent and message data.