4343because:
4444 - StreamingTaskMessageContext.close() persists initial_content when no deltas
4545 have been streamed, so the message IS correctly persisted.
46- - It mirrors the pattern already used by the real _langgraph_async.py harness,
47- keeping behavioural parity.
46+ - It mirrors the pattern already used by the real langgraph streaming helper
47+ (now in _langgraph_turn.py), keeping behavioural parity.
4848 - Switching to adk.messages.create would require an additional injectable
4949 dependency, adding surface area for no observable benefit.
5050The conformance test treats this as an ACCEPTABLE envelope difference: at the
5353identical because both adapters drive the same SpanDeriver.observe() call
5454sequence and forward every signal to their tracer.
5555
56- AGX1-377 fix: auto_send now DELIVERS streamed tool-request messages (Start+Done)
57- instead of dropping them. The conformance normaliser previously suppressed the
58- delivery for Start(tool_request)+Done on the yield channel to match auto_send's
59- old drop behaviour. That suppression is now removed: both channels produce a
60- LogicalDelivery for a streamed tool_request, and the cross-channel assertion
61- verifies it is delivered on both.
56+ auto_send DELIVERS streamed tool-request messages (Start+Done): both channels
57+ produce a LogicalDelivery for a streamed tool_request, and the cross-channel
58+ assertion verifies it is delivered on both.
6259"""
6360
6461from __future__ import annotations
6562
6663import json
67- import types as _types
6864from typing import Any , NamedTuple , override
6965from dataclasses import dataclass
7066
8177from agentex .types .reasoning_content_delta import ReasoningContentDelta
8278from agentex .lib .core .harness .span_derivation import SpanDeriver
8379
80+ from .._fakes import FakeTracing
81+
8482
8583@dataclass
8684class Fixture :
@@ -99,6 +97,25 @@ def all_fixtures() -> list[Fixture]:
9997 return list (_REGISTRY )
10098
10199
100+ def run_pure_async (coro : Any ) -> Any :
101+ """Drive a *pure* (I/O-free) coroutine to completion without an event loop.
102+
103+ Conformance fixtures are built at import time so they can parametrize the
104+ tests below. The fixture-building coroutines only iterate in-memory events
105+ and never suspend on a real future, so we step them by hand instead of
106+ ``asyncio.run()``. ``asyncio.run()`` at import raises ``RuntimeError`` when a
107+ loop is already running (programmatic pytest, a Jupyter kernel, or a
108+ session-scoped asyncio loop); this driver is unaffected by ambient loop
109+ state. It raises if the coroutine ever suspends on real I/O.
110+ """
111+ try :
112+ coro .send (None )
113+ except StopIteration as stop :
114+ return stop .value
115+ coro .close ()
116+ raise RuntimeError ("conformance fixture build unexpectedly suspended on real I/O" )
117+
118+
102119def derive_all (events : list [StreamTaskMessage ]) -> list [SpanSignal ]:
103120 d = SpanDeriver ()
104121 out : list [SpanSignal ] = []
@@ -145,8 +162,8 @@ def _yield_logical_deliveries(events: list[StreamTaskMessage]) -> list[LogicalDe
145162 - reasoning: initial_content.summary joined (from Start) prepended to
146163 accumulated reasoning-content deltas (this catches a channel that drops
147164 the summary)
148- - tool_request: JSON-sorted arguments from the Start content (AGX1-377: now
149- delivered on both channels, no longer suppressed )
165+ - tool_request: JSON-sorted arguments from the Start content (delivered on
166+ both channels)
150167 - tool_response: str(content) from Full event
151168 """
152169 from agentex .types .text_content import TextContent
@@ -191,9 +208,9 @@ def _yield_logical_deliveries(events: list[StreamTaskMessage]) -> list[LogicalDe
191208 )
192209 )
193210 elif ctype == "tool_request" and isinstance (content , ToolRequestContent ):
194- # AGX1-377 fix: auto_send now delivers streamed tool-request
195- # messages. Emit a delivery here so the cross-channel
196- # assertion verifies it is present on both channels.
211+ # auto_send delivers streamed tool-request messages. Emit a
212+ # delivery here so the cross-channel assertion verifies it is
213+ # present on both channels.
197214 deliveries .append (
198215 LogicalDelivery (
199216 content_type = ctype ,
@@ -296,30 +313,6 @@ def streaming_task_message_context(
296313 return _FakeCtx (self .sink , ctype , initial_content )
297314
298315
299- class _FakeTracing :
300- """Minimal tracing backend: records started/ended span names + outputs."""
301-
302- def __init__ (self ) -> None :
303- self .started : list [str ] = []
304- self .ended : list [Any ] = []
305-
306- async def start_span (
307- self ,
308- * ,
309- trace_id : str ,
310- name : str ,
311- input : Any = None ,
312- parent_id : Any = None ,
313- data : Any = None ,
314- task_id : Any = None ,
315- ) -> Any :
316- self .started .append (name )
317- return _types .SimpleNamespace ()
318-
319- async def end_span (self , * , trace_id : str , span : Any ) -> None :
320- self .ended .append (getattr (span , "output" , None ))
321-
322-
323316class _RecordingTracer (SpanTracer ):
324317 """SpanTracer that records every SpanSignal it actually receives.
325318
@@ -486,7 +479,7 @@ async def run_cross_channel_conformance(
486479 from agentex .lib .core .harness .yield_delivery import yield_events
487480
488481 # --- yield channel ---
489- tracer_yield = _RecordingTracer (tracing = _FakeTracing ())
482+ tracer_yield = _RecordingTracer (tracing = FakeTracing ())
490483 yield_out = [e async for e in yield_events (_gen (fixture .events ), tracer = tracer_yield )]
491484
492485 # Span signals the yield channel actually emitted to its tracer
@@ -496,7 +489,7 @@ async def run_cross_channel_conformance(
496489 yield_deliveries = _yield_text_reasoning_seq (_yield_logical_deliveries (yield_out ))
497490
498491 # --- auto_send channel ---
499- tracer_auto = _RecordingTracer (tracing = _FakeTracing ())
492+ tracer_auto = _RecordingTracer (tracing = FakeTracing ())
500493 fake_streaming = _FakeStreaming ()
501494 await auto_send (
502495 _gen (fixture .events ),
0 commit comments