@@ -1278,6 +1278,163 @@ async def test_handle_a2a_response_with_partial_artifact_update(self):
12781278
12791279 assert result is None
12801280
1281+ @pytest .mark .asyncio
1282+ async def test_handle_a2a_response_filters_thought_parts_from_completed_task (
1283+ self ,
1284+ ):
1285+ """Test that thought parts are filtered from completed task response.
1286+
1287+ When an A2A server returns a completed task with both thought and
1288+ non-thought parts, the client should only include non-thought parts
1289+ in the user-facing event. Fixes #4676.
1290+ """
1291+ mock_a2a_task = Mock (spec = A2ATask )
1292+ mock_a2a_task .id = "task-123"
1293+ mock_a2a_task .context_id = "context-123"
1294+ mock_a2a_task .status = Mock (spec = A2ATaskStatus )
1295+ mock_a2a_task .status .state = TaskState .completed
1296+
1297+ # Create event with mixed thought/non-thought parts
1298+ thought_part = genai_types .Part (text = "internal reasoning" , thought = True )
1299+ answer_part = genai_types .Part (text = "final answer" )
1300+ mock_event = Event (
1301+ author = self .agent .name ,
1302+ invocation_id = self .mock_context .invocation_id ,
1303+ branch = self .mock_context .branch ,
1304+ content = genai_types .Content (
1305+ role = "model" , parts = [thought_part , answer_part ]
1306+ ),
1307+ )
1308+
1309+ with patch .object (
1310+ remote_a2a_agent ,
1311+ "convert_a2a_task_to_event" ,
1312+ autospec = True ,
1313+ ) as mock_convert :
1314+ mock_convert .return_value = mock_event
1315+
1316+ result = await self .agent ._handle_a2a_response (
1317+ (mock_a2a_task , None ), self .mock_context
1318+ )
1319+
1320+ # Only non-thought parts should remain
1321+ assert len (result .content .parts ) == 1
1322+ assert result .content .parts [0 ].text == "final answer"
1323+ assert result .content .parts [0 ].thought is None
1324+
1325+ @pytest .mark .asyncio
1326+ async def test_handle_a2a_response_filters_thought_parts_from_status_update (
1327+ self ,
1328+ ):
1329+ """Test that thought parts are filtered from completed status update.
1330+
1331+ Fixes #4676.
1332+ """
1333+ mock_a2a_task = Mock (spec = A2ATask )
1334+ mock_a2a_task .id = "task-123"
1335+ mock_a2a_task .context_id = "context-123"
1336+
1337+ mock_update = Mock (spec = TaskStatusUpdateEvent )
1338+ mock_update .status = Mock (spec = A2ATaskStatus )
1339+ mock_update .status .state = TaskState .completed
1340+ mock_update .status .message = Mock (spec = A2AMessage )
1341+
1342+ # Create event with mixed thought/non-thought parts
1343+ thought_part = genai_types .Part (text = "thinking..." , thought = True )
1344+ answer_part = genai_types .Part (text = "the answer" )
1345+ mock_event = Event (
1346+ author = self .agent .name ,
1347+ invocation_id = self .mock_context .invocation_id ,
1348+ branch = self .mock_context .branch ,
1349+ content = genai_types .Content (
1350+ role = "model" , parts = [thought_part , answer_part ]
1351+ ),
1352+ )
1353+
1354+ with patch (
1355+ "google.adk.agents.remote_a2a_agent.convert_a2a_message_to_event"
1356+ ) as mock_convert :
1357+ mock_convert .return_value = mock_event
1358+
1359+ result = await self .agent ._handle_a2a_response (
1360+ (mock_a2a_task , mock_update ), self .mock_context
1361+ )
1362+
1363+ # Only non-thought parts should remain
1364+ assert len (result .content .parts ) == 1
1365+ assert result .content .parts [0 ].text == "the answer"
1366+
1367+ @pytest .mark .asyncio
1368+ async def test_handle_a2a_response_preserves_all_thought_parts_for_working (
1369+ self ,
1370+ ):
1371+ """Test that working state events keep all parts as thoughts.
1372+
1373+ Intermediate events (working/submitted) should retain all parts
1374+ marked as thought for streaming progress display.
1375+ """
1376+ mock_a2a_task = Mock (spec = A2ATask )
1377+ mock_a2a_task .id = "task-123"
1378+ mock_a2a_task .context_id = "context-123"
1379+ mock_a2a_task .status = Mock (spec = A2ATaskStatus )
1380+ mock_a2a_task .status .state = TaskState .working
1381+
1382+ part = genai_types .Part (text = "still thinking" )
1383+ mock_event = Event (
1384+ author = self .agent .name ,
1385+ invocation_id = self .mock_context .invocation_id ,
1386+ branch = self .mock_context .branch ,
1387+ content = genai_types .Content (role = "model" , parts = [part ]),
1388+ )
1389+
1390+ with patch .object (
1391+ remote_a2a_agent ,
1392+ "convert_a2a_task_to_event" ,
1393+ autospec = True ,
1394+ ) as mock_convert :
1395+ mock_convert .return_value = mock_event
1396+
1397+ result = await self .agent ._handle_a2a_response (
1398+ (mock_a2a_task , None ), self .mock_context
1399+ )
1400+
1401+ # All parts should be marked as thought and preserved
1402+ assert len (result .content .parts ) == 1
1403+ assert result .content .parts [0 ].thought is True
1404+
1405+ @pytest .mark .asyncio
1406+ async def test_handle_a2a_response_filters_thought_from_a2a_message (self ):
1407+ """Test thought filtering for regular A2AMessage responses.
1408+
1409+ Fixes #4676.
1410+ """
1411+ mock_a2a_message = Mock (spec = A2AMessage )
1412+ mock_a2a_message .context_id = "context-123"
1413+
1414+ thought_part = genai_types .Part (text = "reasoning" , thought = True )
1415+ answer_part = genai_types .Part (text = "response" )
1416+ mock_event = Event (
1417+ author = self .agent .name ,
1418+ invocation_id = self .mock_context .invocation_id ,
1419+ branch = self .mock_context .branch ,
1420+ content = genai_types .Content (
1421+ role = "model" , parts = [thought_part , answer_part ]
1422+ ),
1423+ )
1424+
1425+ with patch (
1426+ "google.adk.agents.remote_a2a_agent.convert_a2a_message_to_event"
1427+ ) as mock_convert :
1428+ mock_convert .return_value = mock_event
1429+
1430+ result = await self .agent ._handle_a2a_response (
1431+ mock_a2a_message , self .mock_context
1432+ )
1433+
1434+ # Only non-thought parts should remain
1435+ assert len (result .content .parts ) == 1
1436+ assert result .content .parts [0 ].text == "response"
1437+
12811438
12821439class TestRemoteA2aAgentMessageHandlingFromFactory :
12831440 """Test message handling functionality."""
0 commit comments