Skip to content

Commit 5eca72f

Browse files
speedstorm1copybara-github
authored andcommitted
fix: Correct message part ordering in A2A history
Merges test case from #3262 Fixes: #3260 Co-authored-by: Ayush Agrawal <[email protected]> PiperOrigin-RevId: 826119626
1 parent b0017ae commit 5eca72f

File tree

2 files changed

+74
-5
lines changed

2 files changed

+74
-5
lines changed

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,29 +356,33 @@ def _construct_message_parts_from_session(
356356
"""
357357
message_parts: list[A2APart] = []
358358
context_id = None
359+
360+
events_to_process = []
359361
for event in reversed(ctx.session.events):
360-
if _is_other_agent_reply(self.name, event):
361-
event = _present_other_agent_message(event)
362-
elif event.author == self.name:
362+
if event.author == self.name:
363363
# stop on content generated by current a2a agent given it should already
364364
# be in remote session
365365
if event.custom_metadata:
366366
metadata = event.custom_metadata
367367
context_id = metadata.get(A2A_METADATA_PREFIX + "context_id")
368368
break
369+
events_to_process.append(event)
370+
371+
for event in reversed(events_to_process):
372+
if _is_other_agent_reply(self.name, event):
373+
event = _present_other_agent_message(event)
369374

370375
if not event.content or not event.content.parts:
371376
continue
372377

373378
for part in event.content.parts:
374-
375379
converted_part = self._genai_part_converter(part)
376380
if converted_part:
377381
message_parts.append(converted_part)
378382
else:
379383
logger.warning("Failed to convert part to A2A format: %s", part)
380384

381-
return message_parts[::-1], context_id
385+
return message_parts, context_id
382386

383387
async def _handle_a2a_response(
384388
self, a2a_response: A2AClientEvent | A2AMessage, ctx: InvocationContext

tests/unittests/agents/test_remote_a2a_agent.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,71 @@ async def test_handle_a2a_response_with_task_completed_and_no_update(self):
735735
assert A2A_METADATA_PREFIX + "task_id" in result.custom_metadata
736736
assert A2A_METADATA_PREFIX + "context_id" in result.custom_metadata
737737

738+
def test_construct_message_parts_from_session_preserves_order(self):
739+
"""Test that message parts are in correct order with multi-part messages.
740+
741+
This test verifies the fix for the bug where _present_other_agent_message
742+
creates multi-part messages with "For context:" prefix, and ensures the
743+
parts are in the correct chronological order (not reversed).
744+
"""
745+
# Create mock events with multiple parts
746+
# Event 1: User message
747+
user_part = Mock()
748+
user_part.text = "User question"
749+
user_content = Mock()
750+
user_content.parts = [user_part]
751+
user_event = Mock()
752+
user_event.content = user_content
753+
user_event.author = "user"
754+
755+
# Event 2: Other agent message (will be transformed by
756+
# _present_other_agent_message)
757+
other_agent_part1 = Mock()
758+
other_agent_part1.text = "For context:"
759+
other_agent_part2 = Mock()
760+
other_agent_part2.text = "[other_agent] said: Response text"
761+
other_agent_content = Mock()
762+
other_agent_content.parts = [other_agent_part1, other_agent_part2]
763+
other_agent_event = Mock()
764+
other_agent_event.content = other_agent_content
765+
other_agent_event.author = "other_agent"
766+
767+
self.mock_session.events = [user_event, other_agent_event]
768+
769+
with patch(
770+
"google.adk.agents.remote_a2a_agent._present_other_agent_message"
771+
) as mock_present:
772+
# Mock _present_other_agent_message to return the transformed event
773+
mock_present.return_value = other_agent_event
774+
775+
# Mock the converter to track the order of parts
776+
converted_parts = []
777+
778+
def mock_converter(part):
779+
mock_a2a_part = Mock()
780+
mock_a2a_part.original_text = part.text
781+
converted_parts.append(mock_a2a_part)
782+
return mock_a2a_part
783+
784+
self.mock_genai_part_converter.side_effect = mock_converter
785+
786+
result = self.agent._construct_message_parts_from_session(
787+
self.mock_context
788+
)
789+
790+
# Verify the parts are in correct order
791+
assert len(result) == 2 # Returns tuple of (parts, context_id)
792+
assert len(result[0]) == 3 # 1 user part + 2 other agent parts
793+
assert result[1] is None # context_id
794+
795+
# Verify order: user part, then "For context:", then agent message
796+
assert converted_parts[0].original_text == "User question"
797+
assert converted_parts[1].original_text == "For context:"
798+
assert (
799+
converted_parts[2].original_text
800+
== "[other_agent] said: Response text"
801+
)
802+
738803
@pytest.mark.asyncio
739804
async def test_handle_a2a_response_with_task_submitted_and_no_update(self):
740805
"""Test successful A2A response handling with streaming task and no update."""

0 commit comments

Comments
 (0)