diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index 8ff21e28..4c93246c 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -797,7 +797,11 @@ def list_messages( return [] def _filter_restored_tool_context(self, messages: list[SessionMessage]) -> list[SessionMessage]: - """Strip historical toolUse/toolResult context from restored messages.""" + """Strip toolUse/toolResult context, merging same-role turns into a Converse-valid sequence. + + The trailing turn is left as-is: the runtime appends the next user turn before the model + call, so forcing a user tail here would create two adjacent user turns. + """ filtered_messages: list[SessionMessage] = [] for session_message in messages: message = session_message.to_message() @@ -810,6 +814,9 @@ def _filter_restored_tool_context(self, messages: list[SessionMessage]) -> list[ if not filtered_content: continue + if filtered_messages and filtered_messages[-1].to_message()["role"] == message["role"]: + filtered_content = filtered_messages.pop().to_message()["content"] + filtered_content + filtered_message: Message = {"role": message["role"], "content": filtered_content} filtered_messages.append( SessionMessage( @@ -821,6 +828,9 @@ def _filter_restored_tool_context(self, messages: list[SessionMessage]) -> list[ ) ) + while filtered_messages and filtered_messages[0].to_message()["role"] != "user": + filtered_messages.pop(0) + return filtered_messages # endregion SessionRepository interface implementation 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 fc93984f..7b04477d 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 @@ -394,6 +394,72 @@ def test_list_messages_returns_values_in_correct_reverse_order(self, session_man assert messages[1].message["role"] == "user" assert messages[0].message["role"] == "assistant" + def test_filter_restored_drops_tool_only_turn_and_merges_assistants(self, session_manager): + """A toolResult-only user turn is dropped, so the surrounding assistant turns merge.""" + messages = [ + SessionMessage(message={"role": "user", "content": [{"text": "hi"}]}, message_id=0), + SessionMessage( + message={ + "role": "assistant", + "content": [{"text": "calling"}, {"toolUse": {"toolUseId": "t1", "name": "f", "input": {}}}], + }, + message_id=1, + ), + SessionMessage( + message={ + "role": "user", + "content": [{"toolResult": {"toolUseId": "t1", "status": "success", "content": [{"text": "ok"}]}}], + }, + message_id=2, + ), + SessionMessage(message={"role": "assistant", "content": [{"text": "done"}]}, message_id=3), + ] + + result = session_manager._filter_restored_tool_context(messages) + + assert [m.message for m in result] == [ + {"role": "user", "content": [{"text": "hi"}]}, + {"role": "assistant", "content": [{"text": "calling"}, {"text": "done"}]}, + ] + + def test_filter_restored_drops_leading_non_user_turns(self, session_manager): + """Restored history must start on a user turn.""" + messages = [ + SessionMessage(message={"role": "assistant", "content": [{"text": "orphan"}]}, message_id=0), + SessionMessage(message={"role": "user", "content": [{"text": "hi"}]}, message_id=1), + SessionMessage(message={"role": "assistant", "content": [{"text": "answer"}]}, message_id=2), + ] + + result = session_manager._filter_restored_tool_context(messages) + + assert [m.message for m in result] == [ + {"role": "user", "content": [{"text": "hi"}]}, + {"role": "assistant", "content": [{"text": "answer"}]}, + ] + + def test_filter_restored_preserves_trailing_assistant_turn(self, session_manager): + """The trailing assistant turn is kept; the runtime appends the next user turn.""" + messages = [ + SessionMessage(message={"role": "user", "content": [{"text": "hi"}]}, message_id=0), + SessionMessage(message={"role": "assistant", "content": [{"text": "answer"}]}, message_id=1), + ] + + result = session_manager._filter_restored_tool_context(messages) + + assert [m.message for m in result] == [ + {"role": "user", "content": [{"text": "hi"}]}, + {"role": "assistant", "content": [{"text": "answer"}]}, + ] + + def test_filter_restored_all_assistant_history_becomes_empty(self, session_manager): + """History with no user turn is dropped entirely.""" + messages = [ + SessionMessage(message={"role": "assistant", "content": [{"text": "a"}]}, message_id=0), + SessionMessage(message={"role": "assistant", "content": [{"text": "b"}]}, message_id=1), + ] + + assert session_manager._filter_restored_tool_context(messages) == [] + def test_events_to_messages_empty_payload(self, session_manager): """Test converting Bedrock events with empty payload.""" events = [ diff --git a/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager_openai_converter.py b/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager_openai_converter.py index e16fd25a..972e1cf1 100644 --- a/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager_openai_converter.py +++ b/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager_openai_converter.py @@ -121,6 +121,5 @@ def test_list_messages_filters_restored_tool_context(): assert [m.message for m in messages] == [ {"role": "user", "content": [{"text": "hello"}]}, - {"role": "assistant", "content": [{"text": "calling tool"}]}, - {"role": "assistant", "content": [{"text": "done"}]}, + {"role": "assistant", "content": [{"text": "calling tool"}, {"text": "done"}]}, ]