2727import inspect
2828import json
2929import logging
30+ import sys
3031import timeit
32+ import weakref
33+ from time import perf_counter , time_ns
3134from typing import Any , AsyncGenerator , Callable , Generator , Mapping , Optional
3235
3336from opentelemetry import trace as otel_trace
102105_original_create_mcp_client_span : Any = None
103106_original_tools_get_function_span : Any = None
104107_original_mcp_create_mcp_client_span : Any = None
108+ _original_agent_trace_invocation : Any = None
109+ _legacy_response_stream_originals : dict [str , Any ] = {}
105110
106111_FINALIZED_ATTR = "_loongsuite_util_genai_finalized"
107112_END_WRAPPED_ATTR = "_loongsuite_util_genai_end_wrapped"
@@ -129,6 +134,7 @@ def apply_util_genai_bridge() -> None:
129134 global _original_get_span
130135 global _original_start_streaming_span
131136 global _original_tools_get_function_span
137+ global _original_agent_trace_invocation
132138
133139 if _applied :
134140 return
@@ -157,6 +163,11 @@ def apply_util_genai_bridge() -> None:
157163 _original_create_mcp_client_span = getattr (
158164 observability , "create_mcp_client_span" , None
159165 )
166+ _original_agent_trace_invocation = getattr (
167+ getattr (observability , "AgentTelemetryLayer" , None ),
168+ "_trace_agent_invocation" ,
169+ None ,
170+ )
160171
161172 wrapped_get_span = (
162173 _wrap_get_span (_original_get_span )
@@ -192,6 +203,16 @@ def apply_util_genai_bridge() -> None:
192203 if wrapped_create_mcp_client_span is not None :
193204 observability .create_mcp_client_span = wrapped_create_mcp_client_span # type: ignore[attr-defined]
194205
206+ legacy_response_stream_patched = _patch_legacy_response_stream ()
207+ if legacy_response_stream_patched and _original_agent_trace_invocation :
208+ agent_cls = getattr (observability , "AgentTelemetryLayer" , None )
209+ if agent_cls is not None :
210+ agent_cls ._trace_agent_invocation = (
211+ _wrap_legacy_agent_trace_invocation ( # type: ignore[attr-defined]
212+ _original_agent_trace_invocation
213+ )
214+ )
215+
195216 try :
196217 import agent_framework ._tools as tools_mod # type: ignore
197218
@@ -228,6 +249,7 @@ def revert_util_genai_bridge() -> None:
228249 global _original_get_span
229250 global _original_start_streaming_span
230251 global _original_tools_get_function_span
252+ global _original_agent_trace_invocation
231253
232254 if not _applied :
233255 return
@@ -248,6 +270,14 @@ def revert_util_genai_bridge() -> None:
248270 observability .create_mcp_client_span = (
249271 _original_create_mcp_client_span # type: ignore[attr-defined]
250272 )
273+ agent_cls = getattr (observability , "AgentTelemetryLayer" , None )
274+ if (
275+ agent_cls is not None
276+ and _original_agent_trace_invocation is not None
277+ ):
278+ agent_cls ._trace_agent_invocation = ( # type: ignore[attr-defined]
279+ _original_agent_trace_invocation
280+ )
251281 except ImportError :
252282 pass
253283 try :
@@ -275,6 +305,301 @@ def revert_util_genai_bridge() -> None:
275305 _original_create_mcp_client_span = None
276306 _original_tools_get_function_span = None
277307 _original_mcp_create_mcp_client_span = None
308+ _original_agent_trace_invocation = None
309+ _restore_legacy_response_stream ()
310+
311+
312+ def _patch_legacy_response_stream () -> bool :
313+ """Add per-pull context support to older MAF ``ResponseStream``.
314+
315+ MAF 1.0 streaming spans were created detached and the stream type had no
316+ pull-context hook, so child spans created while resolving/iterating the
317+ stream became roots. MAF 1.10 added ``with_pull_context_manager``; this
318+ compatibility shim backports only that context propagation surface.
319+ """
320+ global _legacy_response_stream_originals
321+
322+ if _legacy_response_stream_originals :
323+ return True
324+ try :
325+ from agent_framework ._types import ResponseStream # type: ignore
326+ except ImportError :
327+ return False
328+ if hasattr (ResponseStream , "with_pull_context_manager" ):
329+ return False
330+
331+ originals = {
332+ "__init__" : ResponseStream .__init__ ,
333+ "_get_stream" : ResponseStream ._get_stream ,
334+ "__anext__" : ResponseStream .__anext__ ,
335+ "_run_cleanup_hooks" : ResponseStream ._run_cleanup_hooks ,
336+ }
337+ _legacy_response_stream_originals = originals
338+
339+ original_init = originals ["__init__" ]
340+ original_get_stream = originals ["_get_stream" ]
341+ original_anext = originals ["__anext__" ]
342+ original_run_cleanup_hooks = originals ["_run_cleanup_hooks" ]
343+
344+ def _init (self : Any , * args : Any , ** kwargs : Any ) -> None :
345+ original_init (self , * args , ** kwargs )
346+ self ._pull_context_manager_factories = []
347+ self ._stream_error = None
348+
349+ async def _get_stream (self : Any ) -> Any :
350+ if getattr (self , "_stream" , None ) is not None :
351+ # The stream is already resolved; child spans are created while
352+ # pulling updates, which is covered by the patched ``__anext__``.
353+ return await original_get_stream (self )
354+ with contextlib .ExitStack () as stack :
355+ for factory in getattr (
356+ self , "_pull_context_manager_factories" , ()
357+ ):
358+ stack .enter_context (factory ())
359+ return await original_get_stream (self )
360+
361+ async def _anext (self : Any ) -> Any :
362+ with contextlib .ExitStack () as stack :
363+ for factory in getattr (
364+ self , "_pull_context_manager_factories" , ()
365+ ):
366+ stack .enter_context (factory ())
367+ return await original_anext (self )
368+
369+ async def _run_cleanup_hooks (self : Any ) -> Any :
370+ _ , exc , _ = sys .exc_info ()
371+ if exc is not None and not isinstance (exc , StopAsyncIteration ):
372+ self ._stream_error = exc
373+ try :
374+ with contextlib .ExitStack () as stack :
375+ for factory in getattr (
376+ self , "_pull_context_manager_factories" , ()
377+ ):
378+ stack .enter_context (factory ())
379+ return await original_run_cleanup_hooks (self )
380+ finally :
381+ if exc is not None :
382+ self ._stream_error = None
383+
384+ def _with_pull_context_manager (self : Any , cm_factory : Callable [[], Any ]):
385+ self ._pull_context_manager_factories .append (cm_factory )
386+ return self
387+
388+ ResponseStream .__init__ = _init # type: ignore[assignment]
389+ ResponseStream ._get_stream = _get_stream # type: ignore[assignment]
390+ ResponseStream .__anext__ = _anext # type: ignore[assignment]
391+ ResponseStream ._run_cleanup_hooks = _run_cleanup_hooks # type: ignore[assignment]
392+ ResponseStream .with_pull_context_manager = _with_pull_context_manager # type: ignore[attr-defined]
393+ return True
394+
395+
396+ def _restore_legacy_response_stream () -> None :
397+ global _legacy_response_stream_originals
398+ if not _legacy_response_stream_originals :
399+ return
400+ try :
401+ from agent_framework ._types import ResponseStream # type: ignore
402+ except ImportError :
403+ _legacy_response_stream_originals = {}
404+ return
405+ ResponseStream .__init__ = _legacy_response_stream_originals ["__init__" ] # type: ignore[assignment]
406+ ResponseStream ._get_stream = _legacy_response_stream_originals [
407+ "_get_stream"
408+ ] # type: ignore[assignment]
409+ ResponseStream .__anext__ = _legacy_response_stream_originals ["__anext__" ] # type: ignore[assignment]
410+ ResponseStream ._run_cleanup_hooks = _legacy_response_stream_originals [ # type: ignore[assignment]
411+ "_run_cleanup_hooks"
412+ ]
413+ try :
414+ delattr (ResponseStream , "with_pull_context_manager" )
415+ except AttributeError :
416+ pass
417+ _legacy_response_stream_originals = {}
418+
419+
420+ def _wrap_legacy_agent_trace_invocation (
421+ original : Callable [..., Any ],
422+ ) -> Callable [..., Any ]:
423+ def _trace_agent_invocation (self : Any , * args : Any , ** kwargs : Any ) -> Any :
424+ if args or not kwargs .get ("stream" ):
425+ return original (self , * args , ** kwargs )
426+ return _legacy_streaming_agent_invocation (self , ** kwargs )
427+
428+ return _trace_agent_invocation
429+
430+
431+ def _legacy_streaming_agent_invocation (self : Any , ** kwargs : Any ) -> Any :
432+ import agent_framework .observability as observability # type: ignore
433+ from agent_framework ._types import ResponseStream # type: ignore
434+
435+ execute = kwargs ["execute" ]
436+ messages = kwargs .get ("messages" )
437+ session = kwargs .get ("session" )
438+ merged_options = kwargs .get ("merged_options" ) or {}
439+ client_kwargs = kwargs .get ("client_kwargs" )
440+ if not observability .OBSERVABILITY_SETTINGS .ENABLED :
441+ return execute ()
442+
443+ provider_name = str (getattr (self , "otel_provider_name" , "unknown" ))
444+ merged_client_kwargs = (
445+ dict (client_kwargs ) if client_kwargs is not None else {}
446+ )
447+ OtelAttr = observability .OtelAttr
448+ attributes = observability ._get_span_attributes (
449+ operation_name = OtelAttr .AGENT_INVOKE_OPERATION ,
450+ provider_name = provider_name ,
451+ agent_id = getattr (self , "id" , "unknown" ),
452+ agent_name = getattr (self , "name" , None )
453+ or getattr (self , "id" , "unknown" ),
454+ agent_description = getattr (self , "description" , None ),
455+ thread_id = session .service_session_id if session else None ,
456+ all_options = dict (merged_options ),
457+ ** merged_client_kwargs ,
458+ )
459+
460+ operation = attributes .get (OtelAttr .OPERATION , "operation" )
461+ span_name = attributes .get (OtelAttr .AGENT_NAME , "unknown" )
462+ span = observability .get_tracer ().start_span (
463+ f"{ operation } { span_name } " ,
464+ kind = otel_trace .SpanKind .INTERNAL ,
465+ attributes = attributes ,
466+ )
467+ _mark_maf_live_span (span )
468+ _wrap_span_end (span )
469+
470+ if (
471+ observability .OBSERVABILITY_SETTINGS .SENSITIVE_DATA_ENABLED
472+ and messages
473+ and span .is_recording ()
474+ ):
475+ observability ._capture_messages (
476+ span = span ,
477+ provider_name = provider_name ,
478+ messages = messages ,
479+ system_instructions = observability ._get_instructions_from_options (
480+ dict (merged_options )
481+ ),
482+ )
483+
484+ span_state = {"closed" : False }
485+ duration_state : dict [str , float ] = {}
486+ start_time = perf_counter ()
487+ inner_response_telemetry_captured_fields : set [str ] = set ()
488+ inner_accumulated_usage : dict [str , Any ] = {}
489+
490+ def _close_span () -> None :
491+ if span_state ["closed" ]:
492+ return
493+ span_state ["closed" ] = True
494+ span .end ()
495+
496+ def _record_duration () -> None :
497+ duration_state ["duration" ] = perf_counter () - start_time
498+
499+ try :
500+ with _activate_live_span (span ):
501+ run_result = execute ()
502+ if isinstance (run_result , ResponseStream ):
503+ result_stream = run_result
504+ elif inspect .isawaitable (run_result ):
505+ result_stream = ResponseStream .from_awaitable (run_result )
506+ else :
507+ raise RuntimeError (
508+ "Streaming telemetry requires a ResponseStream result."
509+ )
510+ except Exception as exception :
511+ observability .capture_exception (
512+ span = span , exception = exception , timestamp = time_ns ()
513+ )
514+ _close_span ()
515+ raise
516+
517+ async def _finalize_stream () -> None :
518+ try :
519+ stream_error = getattr (result_stream , "_stream_error" , None )
520+ if stream_error is not None :
521+ observability .capture_exception (
522+ span = span , exception = stream_error , timestamp = time_ns ()
523+ )
524+ return
525+ response = await result_stream .get_final_response ()
526+ response_attributes = observability ._get_response_attributes (
527+ attributes ,
528+ response ,
529+ capture_response_id = (
530+ observability .INNER_RESPONSE_ID_CAPTURED_FIELD
531+ not in inner_response_telemetry_captured_fields
532+ ),
533+ capture_usage = (
534+ observability .INNER_USAGE_CAPTURED_FIELD
535+ not in inner_response_telemetry_captured_fields
536+ ),
537+ )
538+ observability ._apply_accumulated_usage (
539+ response_attributes ,
540+ inner_response_telemetry_captured_fields ,
541+ )
542+ observability ._capture_response (
543+ span = span ,
544+ attributes = response_attributes ,
545+ duration = duration_state .get ("duration" ),
546+ )
547+ if (
548+ observability .OBSERVABILITY_SETTINGS .SENSITIVE_DATA_ENABLED
549+ and getattr (response , "messages" , None )
550+ and span .is_recording ()
551+ ):
552+ observability ._capture_messages (
553+ span = span ,
554+ provider_name = provider_name ,
555+ messages = response .messages ,
556+ output = True ,
557+ )
558+ except Exception as exception :
559+ observability .capture_exception (
560+ span = span , exception = exception , timestamp = time_ns ()
561+ )
562+ finally :
563+ _close_span ()
564+
565+ @contextlib .contextmanager
566+ def _inner_telemetry_pull_context () -> Any :
567+ fields_token = (
568+ observability .INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS .set (
569+ inner_response_telemetry_captured_fields
570+ )
571+ )
572+ usage_token = observability .INNER_ACCUMULATED_USAGE .set (
573+ inner_accumulated_usage
574+ )
575+ try :
576+ with _activate_live_span (span ):
577+ yield
578+ finally :
579+ observability .INNER_ACCUMULATED_USAGE .reset (usage_token )
580+ observability .INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS .reset (
581+ fields_token
582+ )
583+
584+ wrapped_stream = (
585+ result_stream .with_cleanup_hook (_record_duration )
586+ .with_cleanup_hook (_finalize_stream )
587+ .with_pull_context_manager (_inner_telemetry_pull_context )
588+ )
589+ try :
590+ weakref .finalize (wrapped_stream , _close_span )
591+ except TypeError :
592+ logger .debug ("MAF ResponseStream is not weak-referenceable" )
593+ return wrapped_stream
594+
595+
596+ def _activate_live_span (span : OtelSpan ) -> Any :
597+ return otel_trace .use_span (
598+ span = span ,
599+ end_on_exit = False ,
600+ record_exception = False ,
601+ set_status_on_exception = False ,
602+ )
278603
279604
280605def _wrap_get_span (original : Callable [..., Any ]) -> Callable [..., Any ]:
0 commit comments