From 5eca72f9bfd05c7c28a3d738391138a59a31167d Mon Sep 17 00:00:00 2001 From: Ayush Agrawal Date: Thu, 30 Oct 2025 11:36:04 -0700 Subject: [PATCH] fix: Correct message part ordering in A2A history Merges test case from https://github.com/google/adk-python/pull/3262 Fixes: #3260 Co-authored-by: Ayush Agrawal PiperOrigin-RevId: 826119626 --- src/google/adk/agents/remote_a2a_agent.py | 14 ++-- .../unittests/agents/test_remote_a2a_agent.py | 65 +++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index bee8904c..d3aea5d3 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -356,29 +356,33 @@ class RemoteA2aAgent(BaseAgent): """ message_parts: list[A2APart] = [] context_id = None + + events_to_process = [] for event in reversed(ctx.session.events): - if _is_other_agent_reply(self.name, event): - event = _present_other_agent_message(event) - elif event.author == self.name: + if event.author == self.name: # stop on content generated by current a2a agent given it should already # be in remote session if event.custom_metadata: metadata = event.custom_metadata context_id = metadata.get(A2A_METADATA_PREFIX + "context_id") break + events_to_process.append(event) + + for event in reversed(events_to_process): + if _is_other_agent_reply(self.name, event): + event = _present_other_agent_message(event) if not event.content or not event.content.parts: continue for part in event.content.parts: - converted_part = self._genai_part_converter(part) if converted_part: message_parts.append(converted_part) else: logger.warning("Failed to convert part to A2A format: %s", part) - return message_parts[::-1], context_id + return message_parts, context_id async def _handle_a2a_response( self, a2a_response: A2AClientEvent | A2AMessage, ctx: InvocationContext diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index 597b5f1b..65359dc8 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -735,6 +735,71 @@ class TestRemoteA2aAgentMessageHandling: assert A2A_METADATA_PREFIX + "task_id" in result.custom_metadata assert A2A_METADATA_PREFIX + "context_id" in result.custom_metadata + def test_construct_message_parts_from_session_preserves_order(self): + """Test that message parts are in correct order with multi-part messages. + + This test verifies the fix for the bug where _present_other_agent_message + creates multi-part messages with "For context:" prefix, and ensures the + parts are in the correct chronological order (not reversed). + """ + # Create mock events with multiple parts + # Event 1: User message + user_part = Mock() + user_part.text = "User question" + user_content = Mock() + user_content.parts = [user_part] + user_event = Mock() + user_event.content = user_content + user_event.author = "user" + + # Event 2: Other agent message (will be transformed by + # _present_other_agent_message) + other_agent_part1 = Mock() + other_agent_part1.text = "For context:" + other_agent_part2 = Mock() + other_agent_part2.text = "[other_agent] said: Response text" + other_agent_content = Mock() + other_agent_content.parts = [other_agent_part1, other_agent_part2] + other_agent_event = Mock() + other_agent_event.content = other_agent_content + other_agent_event.author = "other_agent" + + self.mock_session.events = [user_event, other_agent_event] + + with patch( + "google.adk.agents.remote_a2a_agent._present_other_agent_message" + ) as mock_present: + # Mock _present_other_agent_message to return the transformed event + mock_present.return_value = other_agent_event + + # Mock the converter to track the order of parts + converted_parts = [] + + def mock_converter(part): + mock_a2a_part = Mock() + mock_a2a_part.original_text = part.text + converted_parts.append(mock_a2a_part) + return mock_a2a_part + + self.mock_genai_part_converter.side_effect = mock_converter + + result = self.agent._construct_message_parts_from_session( + self.mock_context + ) + + # Verify the parts are in correct order + assert len(result) == 2 # Returns tuple of (parts, context_id) + assert len(result[0]) == 3 # 1 user part + 2 other agent parts + assert result[1] is None # context_id + + # Verify order: user part, then "For context:", then agent message + assert converted_parts[0].original_text == "User question" + assert converted_parts[1].original_text == "For context:" + assert ( + converted_parts[2].original_text + == "[other_agent] said: Response text" + ) + @pytest.mark.asyncio async def test_handle_a2a_response_with_task_submitted_and_no_update(self): """Test successful A2A response handling with streaming task and no update."""