@@ -867,3 +867,78 @@ async def boom() -> AsyncIterator[Any]:
867867 await stream_pydantic_ai_events (boom (), TASK_ID )
868868
869869 assert streaming .contexts [0 ].closed is True
870+
871+
872+ # ---------------------------------------------------------------------------
873+ # Characterization test: lock the wire-level delivery shape for a representative
874+ # pydantic-ai run (text + tool call + tool response + more text).
875+ #
876+ # Step 1 (CURRENT behavior): written against the original implementation.
877+ # - Text/reasoning use adk.streaming.streaming_task_message_context.
878+ # - Tool messages use adk.messages.create (FakeMessagesModule.created list).
879+ # - Final text is the last text segment.
880+ #
881+ # Step 2 (POST-reimplementation on UnifiedEmitter / auto_send):
882+ # The assertions in TestCharacterizeWireShapeNew (below) lock the new shape.
883+ # Tool messages no longer go through adk.messages.create; they arrive via
884+ # streaming_task_message_context open+close pairs (Start+Done envelope).
885+ # This is the AGX1-373 accepted envelope change: logical content is identical.
886+ # ---------------------------------------------------------------------------
887+
888+
889+ class TestCharacterizeWireShapeCurrent :
890+ """Characterization tests: lock the CURRENT wire-level delivery shape.
891+
892+ Uses FakeStreamingModule + FakeMessagesModule (the existing fake pair).
893+ Tool messages arrive via adk.messages.create; text via adk.streaming.
894+ """
895+
896+ async def test_text_tool_text_current_wire_shape (
897+ self , fake_adk : tuple [FakeStreamingModule , FakeMessagesModule ]
898+ ) -> None :
899+ """Representative run: text -> tool call -> tool response -> more text.
900+
901+ Records the CURRENT delivery shape before reimplementation:
902+ - Two streaming contexts (text segments).
903+ - Two adk.messages.create calls (tool_request, tool_response).
904+ - Final text == "It's sunny." (last segment only).
905+ """
906+ from pydantic_ai .messages import ToolReturnPart
907+
908+ streaming , messages = fake_adk
909+ events = [
910+ PartStartEvent (index = 0 , part = TextPart (content = "" )),
911+ PartDeltaEvent (index = 0 , delta = TextPartDelta (content_delta = "Looking up..." )),
912+ PartEndEvent (index = 0 , part = TextPart (content = "Looking up..." )),
913+ PartStartEvent (
914+ index = 1 ,
915+ part = ToolCallPart (tool_name = "get_weather" , args = None , tool_call_id = "c1" ),
916+ ),
917+ PartEndEvent (
918+ index = 1 ,
919+ part = ToolCallPart (tool_name = "get_weather" , args = "{}" , tool_call_id = "c1" ),
920+ ),
921+ FunctionToolResultEvent (
922+ part = ToolReturnPart (tool_name = "get_weather" , content = "Sunny" , tool_call_id = "c1" ),
923+ ),
924+ PartStartEvent (index = 0 , part = TextPart (content = "" )),
925+ PartDeltaEvent (index = 0 , delta = TextPartDelta (content_delta = "It's sunny." )),
926+ PartEndEvent (index = 0 , part = TextPart (content = "It's sunny." )),
927+ ]
928+
929+ final = await stream_pydantic_ai_events (_aiter (events ), TASK_ID )
930+
931+ assert final == "It's sunny." , "multi-step: only the last text segment is returned"
932+ assert len (streaming .contexts ) == 2 , "two text segments -> two streaming contexts"
933+ assert all (ctx .closed for ctx in streaming .contexts )
934+ assert _text_deltas (streaming .contexts [0 ]) == ["Looking up..." ]
935+ assert _text_deltas (streaming .contexts [1 ]) == ["It's sunny." ]
936+
937+ # CURRENT shape: tool messages arrive via adk.messages.create
938+ assert len (messages .created ) == 2 , "one tool_request + one tool_response"
939+ assert isinstance (messages .created [0 ]["content" ], ToolRequestContent )
940+ assert messages .created [0 ]["content" ].tool_call_id == "c1"
941+ assert messages .created [0 ]["content" ].name == "get_weather"
942+ assert isinstance (messages .created [1 ]["content" ], ToolResponseContent )
943+ assert messages .created [1 ]["content" ].tool_call_id == "c1"
944+ assert messages .created [1 ]["content" ].content == "Sunny"
0 commit comments