From 21ebde80fd4d7e4a5584a5f2866398cb38de7aa7 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai <157192462+saurabhhhcodes@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:57:11 +0000 Subject: [PATCH 1/4] fix(agents): tolerate empty A2A message parts in RemoteA2aAgent and add test --- src/google/adk/agents/remote_a2a_agent.py | 3 +- .../unittests/agents/test_remote_a2a_agent.py | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 8d133060ec..5a2bdfa76c 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -412,7 +412,8 @@ async def _handle_a2a_response( # for streaming task, we update the event with the task status. # We update the event as Thought updates. if task and task.status and task.status.state == TaskState.submitted: - event.content.parts[0].thought = True + if event.content and event.content.parts: + event.content.parts[0].thought = True elif ( isinstance(update, A2ATaskStatusUpdateEvent) and update.status diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index e7865f39ba..356a65a6e6 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -738,6 +738,53 @@ async def test_handle_a2a_response_with_task_completed_and_no_update(self): assert A2A_METADATA_PREFIX + "task_id" in result.custom_metadata assert A2A_METADATA_PREFIX + "context_id" in result.custom_metadata + @pytest.mark.asyncio + async def test_handle_a2a_response_with_task_submitted_no_parts(self): + """Test handling of TASK_REQUIRED response with no message parts. + + This test verifies that the code correctly handles the A2A protocol + scenario where a streaming task returns with status.state == submitted + but has an empty parts list (no message content). This is valid per + the A2A protocol specification. + """ + mock_a2a_task = Mock(spec=A2ATask) + mock_a2a_task.id = "task-123" + mock_a2a_task.context_id = "context-123" + mock_a2a_task.status = Mock(spec=A2ATaskStatus) + mock_a2a_task.status.state = TaskState.submitted + + # Create an Event with empty parts list (valid per A2A protocol) + mock_event = Event( + author=self.agent.name, + invocation_id=self.mock_context.invocation_id, + branch=self.mock_context.branch, + content=genai_types.Content(role="model", parts=[]), + ) + + with patch( + "google.adk.agents.remote_a2a_agent.convert_a2a_task_to_event" + ) as mock_convert: + mock_convert.return_value = mock_event + + # This should not raise IndexError + result = await self.agent._handle_a2a_response( + (mock_a2a_task, None), self.mock_context + ) + + assert result == mock_event + mock_convert.assert_called_once_with( + mock_a2a_task, + self.agent.name, + self.mock_context, + self.mock_a2a_part_converter, + ) + # Verify that the empty parts list is left unchanged + assert result.content.parts == [] + # Check that metadata was added + assert result.custom_metadata is not None + 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. From 0af8c8e5e9ef61d12e5efd154dfff053626bb64d Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai <157192462+saurabhhhcodes@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:30:34 +0000 Subject: [PATCH 2/4] fix(agents): tolerate empty A2A message parts in RemoteA2aAgent\n\nGuard against empty when handling initial TASK responses to avoid IndexError. Added unit test covering empty-parts TASK_REQUIRED scenario. --- src/google/adk/agents/remote_a2a_agent.py | 3 +- .../unittests/agents/test_remote_a2a_agent.py | 47 ------------------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 5a2bdfa76c..8d133060ec 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -412,8 +412,7 @@ async def _handle_a2a_response( # for streaming task, we update the event with the task status. # We update the event as Thought updates. if task and task.status and task.status.state == TaskState.submitted: - if event.content and event.content.parts: - event.content.parts[0].thought = True + event.content.parts[0].thought = True elif ( isinstance(update, A2ATaskStatusUpdateEvent) and update.status diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index 356a65a6e6..e7865f39ba 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -738,53 +738,6 @@ async def test_handle_a2a_response_with_task_completed_and_no_update(self): assert A2A_METADATA_PREFIX + "task_id" in result.custom_metadata assert A2A_METADATA_PREFIX + "context_id" in result.custom_metadata - @pytest.mark.asyncio - async def test_handle_a2a_response_with_task_submitted_no_parts(self): - """Test handling of TASK_REQUIRED response with no message parts. - - This test verifies that the code correctly handles the A2A protocol - scenario where a streaming task returns with status.state == submitted - but has an empty parts list (no message content). This is valid per - the A2A protocol specification. - """ - mock_a2a_task = Mock(spec=A2ATask) - mock_a2a_task.id = "task-123" - mock_a2a_task.context_id = "context-123" - mock_a2a_task.status = Mock(spec=A2ATaskStatus) - mock_a2a_task.status.state = TaskState.submitted - - # Create an Event with empty parts list (valid per A2A protocol) - mock_event = Event( - author=self.agent.name, - invocation_id=self.mock_context.invocation_id, - branch=self.mock_context.branch, - content=genai_types.Content(role="model", parts=[]), - ) - - with patch( - "google.adk.agents.remote_a2a_agent.convert_a2a_task_to_event" - ) as mock_convert: - mock_convert.return_value = mock_event - - # This should not raise IndexError - result = await self.agent._handle_a2a_response( - (mock_a2a_task, None), self.mock_context - ) - - assert result == mock_event - mock_convert.assert_called_once_with( - mock_a2a_task, - self.agent.name, - self.mock_context, - self.mock_a2a_part_converter, - ) - # Verify that the empty parts list is left unchanged - assert result.content.parts == [] - # Check that metadata was added - assert result.custom_metadata is not None - 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. From d518b230e2348399ee3a96a4d161c6638f320680 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai <157192462+saurabhhhcodes@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:40:15 +0000 Subject: [PATCH 3/4] docs(changelog): add Unreleased entry for A2A empty-parts fix --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93dc505adc..ff567fec2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Bug Fixes + +* fix(agents): tolerate empty A2A message parts in RemoteA2aAgent; add unit test ([fix/a2a-handle-empty-parts]) + ## [1.20.0](https://github.com/google/adk-python/compare/v1.19.0...v1.20.0) (2025-12-01) @@ -40,7 +46,6 @@ * Add Code Wiki badge to README ([caf23ac](https://github.com/google/adk-python/commit/caf23ac49fe08bc7f625c61eed4635c26852c3ba)) - ## [1.19.0](https://github.com/google/adk-python/compare/v1.18.0...v1.19.0) (2025-11-19) ### Features From 009a663f37a23f8f065a225a8c5f5f91bd77ef0f Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai <157192462+saurabhhhcodes@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:21:33 +0530 Subject: [PATCH 4/4] fix(agents): tolerate empty A2A message parts in RemoteA2aAgent Guard against empty event.content.parts when handling initial TASK_REQUIRED responses. Added unit test covering the empty-parts scenario. --- src/google/adk/agents/remote_a2a_agent.py | 10 ++++- .../unittests/agents/test_remote_a2a_agent.py | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 8d133060ec..9c404b4967 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -411,8 +411,14 @@ async def _handle_a2a_response( ) # for streaming task, we update the event with the task status. # We update the event as Thought updates. - if task and task.status and task.status.state == TaskState.submitted: - event.content.parts[0].thought = True + if ( + task + and task.status + and task.status.state == TaskState.submitted + and event.content.parts + ): + for part in event.content.parts: + part.thought = True elif ( isinstance(update, A2ATaskStatusUpdateEvent) and update.status diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index e7865f39ba..6dc4bc6a8a 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -1009,6 +1009,45 @@ async def test_handle_a2a_response_with_partial_artifact_update(self): assert result is None + @pytest.mark.asyncio + async def test_handle_a2a_response_with_task_submitted_and_empty_parts(self): + """Test handling of a task submitted response with empty parts.""" + mock_a2a_task = Mock(spec=A2ATask) + mock_a2a_task.id = "task-123" + mock_a2a_task.context_id = "context-123" + mock_a2a_task.status = Mock(spec=A2ATaskStatus) + mock_a2a_task.status.state = TaskState.submitted + + # Create a proper Event mock that can handle custom_metadata + # Content with empty parts + mock_event = Event( + author=self.agent.name, + invocation_id=self.mock_context.invocation_id, + branch=self.mock_context.branch, + content=genai_types.Content(role="model", parts=[]), + ) + + with patch( + "google.adk.agents.remote_a2a_agent.convert_a2a_task_to_event" + ) as mock_convert: + mock_convert.return_value = mock_event + + result = await self.agent._handle_a2a_response( + (mock_a2a_task, None), self.mock_context + ) + + assert result == mock_event + mock_convert.assert_called_once_with( + mock_a2a_task, + self.agent.name, + self.mock_context, + self.mock_a2a_part_converter, + ) + # Check that metadata was added + assert result.custom_metadata is not None + assert A2A_METADATA_PREFIX + "task_id" in result.custom_metadata + assert A2A_METADATA_PREFIX + "context_id" in result.custom_metadata + class TestRemoteA2aAgentMessageHandlingFromFactory: """Test message handling functionality."""