From e3223816f6ae8771343ea9399a4bf21725feae52 Mon Sep 17 00:00:00 2001 From: liweiguang Date: Tue, 3 Mar 2026 08:10:31 +0800 Subject: [PATCH 1/4] fix: filter thought parts from A2A client user-facing response When an A2A server returns a completed response containing both thought parts (metadata.adk_thought=true) and final answer parts, the client now filters out thought parts before yielding the event to consumers. Intermediate (submitted/working) events are preserved as-is since all their parts are already marked as thoughts for streaming progress. Fixes #4676 --- src/google/adk/agents/remote_a2a_agent.py | 13 ++ .../unittests/agents/test_remote_a2a_agent.py | 157 ++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 9b3a7b2244..d93af62339 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -514,6 +514,19 @@ async def _handle_a2a_response( invocation_id=ctx.invocation_id, branch=ctx.branch, ) + # Filter out thought parts from user-facing response content. + # Intermediate (submitted/working) events have all parts marked as + # thought, so non_thought_parts will be empty and we preserve them. + if ( + event.content is not None + and event.content.parts + ): + non_thought_parts = [ + p for p in event.content.parts if not p.thought + ] + if non_thought_parts: + event.content.parts = non_thought_parts + return event except A2AClientError as e: logger.error("Failed to handle A2A response: %s", e) diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index 0f1ce896a3..c42a755a7d 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -1278,6 +1278,163 @@ async def test_handle_a2a_response_with_partial_artifact_update(self): assert result is None + @pytest.mark.asyncio + async def test_handle_a2a_response_filters_thought_parts_from_completed_task( + self, + ): + """Test that thought parts are filtered from completed task response. + + When an A2A server returns a completed task with both thought and + non-thought parts, the client should only include non-thought parts + in the user-facing event. Fixes #4676. + """ + 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.completed + + # Create event with mixed thought/non-thought parts + thought_part = genai_types.Part(text="internal reasoning", thought=True) + answer_part = genai_types.Part(text="final answer") + 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=[thought_part, answer_part] + ), + ) + + with patch.object( + remote_a2a_agent, + "convert_a2a_task_to_event", + autospec=True, + ) as mock_convert: + mock_convert.return_value = mock_event + + result = await self.agent._handle_a2a_response( + (mock_a2a_task, None), self.mock_context + ) + + # Only non-thought parts should remain + assert len(result.content.parts) == 1 + assert result.content.parts[0].text == "final answer" + assert result.content.parts[0].thought is None + + @pytest.mark.asyncio + async def test_handle_a2a_response_filters_thought_parts_from_status_update( + self, + ): + """Test that thought parts are filtered from completed status update. + + Fixes #4676. + """ + mock_a2a_task = Mock(spec=A2ATask) + mock_a2a_task.id = "task-123" + mock_a2a_task.context_id = "context-123" + + mock_update = Mock(spec=TaskStatusUpdateEvent) + mock_update.status = Mock(spec=A2ATaskStatus) + mock_update.status.state = TaskState.completed + mock_update.status.message = Mock(spec=A2AMessage) + + # Create event with mixed thought/non-thought parts + thought_part = genai_types.Part(text="thinking...", thought=True) + answer_part = genai_types.Part(text="the answer") + 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=[thought_part, answer_part] + ), + ) + + with patch( + "google.adk.agents.remote_a2a_agent.convert_a2a_message_to_event" + ) as mock_convert: + mock_convert.return_value = mock_event + + result = await self.agent._handle_a2a_response( + (mock_a2a_task, mock_update), self.mock_context + ) + + # Only non-thought parts should remain + assert len(result.content.parts) == 1 + assert result.content.parts[0].text == "the answer" + + @pytest.mark.asyncio + async def test_handle_a2a_response_preserves_all_thought_parts_for_working( + self, + ): + """Test that working state events keep all parts as thoughts. + + Intermediate events (working/submitted) should retain all parts + marked as thought for streaming progress display. + """ + 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.working + + part = genai_types.Part(text="still thinking") + 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=[part]), + ) + + with patch.object( + remote_a2a_agent, + "convert_a2a_task_to_event", + autospec=True, + ) as mock_convert: + mock_convert.return_value = mock_event + + result = await self.agent._handle_a2a_response( + (mock_a2a_task, None), self.mock_context + ) + + # All parts should be marked as thought and preserved + assert len(result.content.parts) == 1 + assert result.content.parts[0].thought is True + + @pytest.mark.asyncio + async def test_handle_a2a_response_filters_thought_from_a2a_message(self): + """Test thought filtering for regular A2AMessage responses. + + Fixes #4676. + """ + mock_a2a_message = Mock(spec=A2AMessage) + mock_a2a_message.context_id = "context-123" + + thought_part = genai_types.Part(text="reasoning", thought=True) + answer_part = genai_types.Part(text="response") + 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=[thought_part, answer_part] + ), + ) + + with patch( + "google.adk.agents.remote_a2a_agent.convert_a2a_message_to_event" + ) as mock_convert: + mock_convert.return_value = mock_event + + result = await self.agent._handle_a2a_response( + mock_a2a_message, self.mock_context + ) + + # Only non-thought parts should remain + assert len(result.content.parts) == 1 + assert result.content.parts[0].text == "response" + class TestRemoteA2aAgentMessageHandlingFromFactory: """Test message handling functionality.""" From 2f37142bde5539a6631663ac7586fb200d7f63a5 Mon Sep 17 00:00:00 2001 From: liweiguang Date: Tue, 3 Mar 2026 16:54:03 +0800 Subject: [PATCH 2/4] chore: trigger CLA re-check From 30ee034b0f5070120d7903e6bc93f92fce2530a7 Mon Sep 17 00:00:00 2001 From: OiPunk Date: Thu, 5 Mar 2026 10:44:18 +0800 Subject: [PATCH 3/4] style: fix formatting --- src/google/adk/agents/remote_a2a_agent.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index d93af62339..606faf2e8d 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -517,13 +517,8 @@ async def _handle_a2a_response( # Filter out thought parts from user-facing response content. # Intermediate (submitted/working) events have all parts marked as # thought, so non_thought_parts will be empty and we preserve them. - if ( - event.content is not None - and event.content.parts - ): - non_thought_parts = [ - p for p in event.content.parts if not p.thought - ] + if event.content is not None and event.content.parts: + non_thought_parts = [p for p in event.content.parts if not p.thought] if non_thought_parts: event.content.parts = non_thought_parts From 5f82958bf611a82de2c4fd3a4ff314bbaf807f17 Mon Sep 17 00:00:00 2001 From: OiPunk Date: Thu, 5 Mar 2026 15:33:42 +0800 Subject: [PATCH 4/4] style: fix import formatting in contributing samples --- contributing/samples/gepa/experiment.py | 1 - contributing/samples/gepa/run_experiment.py | 1 - 2 files changed, 2 deletions(-) diff --git a/contributing/samples/gepa/experiment.py b/contributing/samples/gepa/experiment.py index f3751206a8..2710c3894c 100644 --- a/contributing/samples/gepa/experiment.py +++ b/contributing/samples/gepa/experiment.py @@ -43,7 +43,6 @@ from tau_bench.types import EnvRunResult from tau_bench.types import RunConfig import tau_bench_agent as tau_bench_agent_lib - import utils diff --git a/contributing/samples/gepa/run_experiment.py b/contributing/samples/gepa/run_experiment.py index d857da9635..e31db15788 100644 --- a/contributing/samples/gepa/run_experiment.py +++ b/contributing/samples/gepa/run_experiment.py @@ -25,7 +25,6 @@ from absl import flags import experiment from google.genai import types - import utils _OUTPUT_DIR = flags.DEFINE_string(