@@ -49,6 +49,70 @@ def test_convert_text_part(self):
4949 assert isinstance (result , genai_types .Part )
5050 assert result .text == "Hello, world!"
5151
52+ def test_convert_text_part_with_thought_metadata (self ):
53+ """Test conversion of A2A TextPart with adk_thought metadata to GenAI Part.
54+
55+ Verifies that the inbound conversion restores thought=True from A2A
56+ metadata, which is essential for the thought-filtering logic in
57+ RemoteA2aAgent._handle_a2a_response. See #4676.
58+ """
59+ # Arrange
60+ a2a_part = a2a_types .Part (
61+ root = a2a_types .TextPart (
62+ text = "internal reasoning" ,
63+ metadata = {_get_adk_metadata_key ("thought" ): True },
64+ )
65+ )
66+
67+ # Act
68+ result = convert_a2a_part_to_genai_part (a2a_part )
69+
70+ # Assert
71+ assert result is not None
72+ assert isinstance (result , genai_types .Part )
73+ assert result .text == "internal reasoning"
74+ assert result .thought is True
75+
76+ def test_convert_text_part_with_thought_false_metadata (self ):
77+ """Test conversion of A2A TextPart with adk_thought=False metadata."""
78+ # Arrange
79+ a2a_part = a2a_types .Part (
80+ root = a2a_types .TextPart (
81+ text = "user-facing text" ,
82+ metadata = {_get_adk_metadata_key ("thought" ): False },
83+ )
84+ )
85+
86+ # Act
87+ result = convert_a2a_part_to_genai_part (a2a_part )
88+
89+ # Assert
90+ assert result is not None
91+ assert result .text == "user-facing text"
92+ # thought=False means it's not a thought part; the filter won't match
93+ assert result .thought is False
94+
95+ def test_convert_text_part_without_thought_metadata (self ):
96+ """Test conversion of A2A TextPart without adk_thought metadata.
97+
98+ When no thought metadata is present, thought should remain None.
99+ """
100+ # Arrange
101+ a2a_part = a2a_types .Part (
102+ root = a2a_types .TextPart (
103+ text = "regular text" ,
104+ metadata = {"some_other_key" : "value" },
105+ )
106+ )
107+
108+ # Act
109+ result = convert_a2a_part_to_genai_part (a2a_part )
110+
111+ # Assert
112+ assert result is not None
113+ assert result .text == "regular text"
114+ assert result .thought is None
115+
52116 def test_convert_file_part_with_uri (self ):
53117 """Test conversion of A2A FilePart with URI to GenAI Part."""
54118 # Arrange
@@ -545,6 +609,36 @@ def test_text_part_with_thought_round_trip(self):
545609 assert result_genai_part .text == original_text
546610 assert result_genai_part .thought
547611
612+ def test_thought_round_trip_enables_filtering (self ):
613+ """Test that thought round-trip enables downstream filtering.
614+
615+ This reproduces the exact scenario from #4676: an A2A response
616+ contains both thought and non-thought parts serialized as artifacts.
617+ After round-tripping through part_converter, the thought flag must
618+ be preserved so that filtering (e.g., in RemoteA2aAgent) can remove
619+ thought parts from user-facing output.
620+ """
621+ # Simulate outbound: GenAI parts -> A2A parts (server side)
622+ thought_genai = genai_types .Part (
623+ text = "<internal reasoning text>" , thought = True
624+ )
625+ answer_genai = genai_types .Part (text = "<final user-facing answer>" )
626+
627+ thought_a2a = convert_genai_part_to_a2a_part (thought_genai )
628+ answer_a2a = convert_genai_part_to_a2a_part (answer_genai )
629+
630+ # Simulate inbound: A2A parts -> GenAI parts (client side)
631+ restored_thought = convert_a2a_part_to_genai_part (thought_a2a )
632+ restored_answer = convert_a2a_part_to_genai_part (answer_a2a )
633+
634+ # Apply the filter that RemoteA2aAgent uses
635+ parts = [restored_thought , restored_answer ]
636+ filtered = [p for p in parts if not p .thought ]
637+
638+ # Only the user-facing answer should survive
639+ assert len (filtered ) == 1
640+ assert filtered [0 ].text == "<final user-facing answer>"
641+
548642 def test_file_uri_round_trip (self ):
549643 """Test round-trip conversion for file parts with URI."""
550644 # Arrange
0 commit comments