@@ -82,7 +82,9 @@ class FakeStreamingModule:
8282 def __init__ (self ) -> None :
8383 self .contexts : list [FakeContext ] = []
8484
85- def streaming_task_message_context (self , * , task_id : str , initial_content : Any ) -> FakeContext :
85+ def streaming_task_message_context (
86+ self , * , task_id : str , initial_content : Any , streaming_mode : str = "coalesced" , created_at : Any = None
87+ ) -> FakeContext :
8688 tm = TaskMessage (
8789 id = f"m{ len (self .contexts ) + 1 } " ,
8890 task_id = task_id ,
@@ -255,26 +257,25 @@ async def test_empty_thinking_delta_is_skipped(
255257
256258
257259class TestToolCallEmission :
258- async def test_tool_call_emits_full_tool_request_message_on_part_end (
260+ async def test_tool_call_opens_streaming_context_with_identity (
259261 self , fake_adk : tuple [FakeStreamingModule , FakeMessagesModule ]
260262 ) -> None :
261- """Tool requests are posted as full messages (open+close on streaming context).
263+ """Tool requests are delivered as a streaming context (Start+Delta+Done ).
262264
263- AGX1-373 envelope change: tool messages now arrive via
264- streaming_task_message_context (open+close pair) instead of
265- adk.messages.create. The logical content (tool_call_id, name,
266- arguments, author) is identical; only the delivery channel changed.
265+ AGX1-377 fix: auto_send now delivers streamed tool-request messages
266+ natively (Start+ToolRequestDelta+Done). The streaming context is opened
267+ at the Start event with the initial ToolRequestContent (tool_call_id +
268+ name + empty arguments), argument tokens are streamed as deltas, and the
269+ context is closed on Done.
267270
268271 This test uses a realistic pydantic-ai event sequence: args arrive as a
269272 PartDeltaEvent fragment (the way OpenAI/Anthropic actually stream JSON
270- tool-call arguments). The new implementation accumulates them correctly.
271-
272- Parts-manager invariant: PartEnd.part is the accumulated snapshot; real
273- pydantic-ai conveys args via PartStart + PartDeltaEvent, so a
274- PartStart(None)+PartEnd(json) with no delta is not realizable.
273+ tool-call arguments).
275274 """
276275 from pydantic_ai .messages import ToolCallPartDelta
277276
277+ from agentex .types .tool_request_delta import ToolRequestDelta
278+
278279 streaming , messages = fake_adk
279280 events = [
280281 PartStartEvent (
@@ -293,23 +294,27 @@ async def test_tool_call_emits_full_tool_request_message_on_part_end(
293294 ]
294295 await stream_pydantic_ai_events (_aiter (events ), TASK_ID )
295296
296- # AGX1-373: tool messages arrive via streaming_task_message_context,
297- # NOT via adk.messages.create.
298- assert messages .created == [], "adk.messages.create must not be called after reimplementation"
299- assert len (streaming .contexts ) == 1 , "tool_request opens a streaming context (open+close)"
297+ # AGX1-373: tool messages arrive via streaming_task_message_context.
298+ assert messages .created == [], "adk.messages.create must not be called"
299+ assert len (streaming .contexts ) == 1 , "tool_request opens a streaming context"
300300 ctx = streaming .contexts [0 ]
301301 assert ctx .closed is True
302302 content = ctx .initial_content
303303 assert isinstance (content , ToolRequestContent )
304304 assert content .tool_call_id == "c1"
305305 assert content .name == "get_weather"
306- assert content .arguments == {"city" : "Paris" }
307306 assert content .author == "agent"
308- assert ctx .updates == [], "open+close only — no deltas for tool messages"
307+ # AGX1-377 streamed shape: initial_content has empty args (args come via delta)
308+ assert content .arguments == {}
309+ # The arg delta is delivered as a stream_update
310+ assert len (ctx .updates ) == 1
311+ assert isinstance (ctx .updates [0 ].delta , ToolRequestDelta )
312+ assert ctx .updates [0 ].delta .arguments_delta == '{"city":"Paris"}'
309313
310314 async def test_tool_call_with_dict_args_passes_through (
311315 self , fake_adk : tuple [FakeStreamingModule , FakeMessagesModule ]
312316 ) -> None :
317+ """When args arrive pre-populated as a dict in PartStart, they're in initial_content."""
313318 streaming , messages = fake_adk
314319 events = [
315320 PartStartEvent (
@@ -326,25 +331,26 @@ async def test_tool_call_with_dict_args_passes_through(
326331 # AGX1-373: tool messages via streaming_task_message_context
327332 assert messages .created == []
328333 assert len (streaming .contexts ) == 1
334+ # Dict args present at PartStart land directly in initial_content.arguments
329335 assert streaming .contexts [0 ].initial_content .arguments == {"q" : "weather" }
336+ assert streaming .contexts [0 ].updates == [], "no delta for pre-populated dict args"
330337
331338 async def test_tool_call_with_invalid_json_args_surfaces_raw (
332339 self , fake_adk : tuple [FakeStreamingModule , FakeMessagesModule ]
333340 ) -> None :
334- """Don't drop the tool call when the model emits malformed JSON args .
341+ """Malformed JSON arg delta is surfaced as a ToolRequestDelta with the raw string .
335342
336- The arguments field is preserved under ``_raw`` so the failure is
337- visible to the UI rather than silently truncated.
338-
339- Uses a PartDeltaEvent to deliver the invalid string (the way pydantic-ai
340- actually surfaces arg tokens) so the coalescer picks it up.
343+ The argument delta is delivered as-is by auto_send; the client-side
344+ accumulator or the streaming backend handles malformed JSON gracefully.
341345
342346 Parts-manager invariant: PartEnd.part is the accumulated snapshot; real
343347 pydantic-ai conveys args via PartStart + PartDeltaEvent, so a
344348 PartStart(None)+PartEnd(json) with no delta is not realizable.
345349 """
346350 from pydantic_ai .messages import ToolCallPartDelta
347351
352+ from agentex .types .tool_request_delta import ToolRequestDelta
353+
348354 streaming , messages = fake_adk
349355 events = [
350356 PartStartEvent (
@@ -366,7 +372,13 @@ async def test_tool_call_with_invalid_json_args_surfaces_raw(
366372 # AGX1-373: tool messages via streaming_task_message_context
367373 assert messages .created == []
368374 assert len (streaming .contexts ) == 1
369- assert streaming .contexts [0 ].initial_content .arguments == {"_raw" : "not-json{" }
375+ ctx = streaming .contexts [0 ]
376+ # Initial content has empty args (args come via delta)
377+ assert ctx .initial_content .arguments == {}
378+ # The malformed JSON is surfaced verbatim in the ToolRequestDelta
379+ assert len (ctx .updates ) == 1
380+ assert isinstance (ctx .updates [0 ].delta , ToolRequestDelta )
381+ assert ctx .updates [0 ].delta .arguments_delta == "not-json{"
370382
371383 async def test_tool_call_with_none_args_defaults_to_empty_dict (
372384 self , fake_adk : tuple [FakeStreamingModule , FakeMessagesModule ]
@@ -388,6 +400,7 @@ async def test_tool_call_with_none_args_defaults_to_empty_dict(
388400 assert messages .created == []
389401 assert len (streaming .contexts ) == 1
390402 assert streaming .contexts [0 ].initial_content .arguments == {}
403+ assert streaming .contexts [0 ].updates == [], "no delta when args are absent"
391404
392405
393406class TestToolResult :
0 commit comments