44(produced by opentelemetry-instrumentation-openai-agents from Traceloop's OpenLLMetry project)
55
66Supports two trace formats:
7- 1. ADOT/CloudWatch : Messages in gen_ai.* attributes
8- 2. Live instrumentation: Messages in gen_ai.* attributes
7+ 1. Live instrumentation : Messages in gen_ai.* attributes
8+ 2. ADOT/CloudWatch: the same as above plus `aws.*` attributes
99"""
1010
1111import logging
@@ -30,9 +30,6 @@ class OpenAIAgentsOtelSessionMapper(GenericGenAISessionMapper):
3030 """Maps OpenAI Agents SDK OTel spans to Session format.
3131
3232 Inherits from GenericGenAISessionMapper to reuse GenAI semantic conventions parsing.
33- Overrides workflow-span skipping and tool back-filling which assume a single-agent-per-
34- workflow trace shape and would break multi-agent traces or frameworks that use empty
35- invoke_agent spans as session anchors if applied in the base class.
3633 """
3734
3835 def map_to_session (self , data : Any , session_id : str ) -> Session :
@@ -45,27 +42,23 @@ def map_to_session(self, data: Any, session_id: str) -> Session:
4542 Returns:
4643 Session object ready for evaluation.
4744 """
48- # Normalize input to flat spans
4945 spans = self ._normalize_to_flat_spans (data )
5046
51- # Filter to only spans from this scope
5247 openai_agents_spans = [s for s in spans if get_scope_name (s ) in (SCOPE_OPENAI_AGENTS , "" )]
5348
5449 return super ().map_to_session (openai_agents_spans , session_id )
5550
5651 def _convert_trace (self , trace_id : str , spans : list [dict ], session_id : str ) -> Trace :
57- """Convert a list of dict spans to a Trace, with per-agent tool back-filling.
58-
59- After base conversion (which sets agent_span_id via Trace.model_post_init),
60- assigns each ToolExecutionSpan's tool only to its owning AgentInvocationSpan.
61- """
52+ """Convert a list of dict spans to a Trace."""
6253 trace = super ()._convert_trace (trace_id , spans , session_id )
6354
64- # Back-fill available_tools scoped per agent using agent_span_id (set by model_post_init).
6555 agent_spans = {
6656 s .span_info .span_id : s for s in trace .spans if isinstance (s , AgentInvocationSpan ) and s .span_info .span_id
6757 }
6858
59+ self ._apply_handoff_reparenting (agent_spans , spans )
60+
61+ # Back-fill agent's available_tools, which is not otherwise provided in the trace.
6962 for span in trace .spans :
7063 if isinstance (span , ToolExecutionSpan ) and span .tool_call .name :
7164 owner_id = span .agent_span_id
@@ -76,15 +69,45 @@ def _convert_trace(self, trace_id: str, spans: list[dict], session_id: str) -> T
7669
7770 return trace
7871
72+ def _apply_handoff_reparenting (
73+ self ,
74+ agent_spans : dict [str , AgentInvocationSpan ],
75+ raw_spans : list [dict ],
76+ ) -> None :
77+ """Re-parent agent invocation spans using OpenAI Agent's agent_handoff span."""
78+ agent_spans_by_name : dict [str , AgentInvocationSpan ] = {}
79+ handoffs : list [tuple [str , str ]] = []
80+
81+ # Collect converted agent spans and handoff edges
82+ for raw_span in raw_spans :
83+ attrs = raw_span .get ("attributes" , {})
84+ op = attrs .get ("gen_ai.operation.name" , "" )
85+ if op == "invoke_agent" :
86+ name = attrs .get ("gen_ai.agent.name" , "" )
87+ span_id = raw_span .get ("span_id" , "" )
88+ if name and span_id and span_id in agent_spans :
89+ agent_spans_by_name [name ] = agent_spans [span_id ]
90+ elif op == "agent_handoff" :
91+ from_name = attrs .get ("gen_ai.handoff.from_agent" , "" )
92+ to_name = attrs .get ("gen_ai.handoff.to_agent" , "" )
93+ if from_name and to_name :
94+ handoffs .append ((from_name , to_name ))
95+
96+ # Re-parent sub-agents to parent agents
97+ for from_name , to_name in handoffs :
98+ from_span = agent_spans_by_name .get (from_name )
99+ to_span = agent_spans_by_name .get (to_name )
100+ if from_span and to_span and from_span .span_info .span_id :
101+ to_span .span_info .parent_span_id = from_span .span_info .span_id
102+
79103 def _convert_agent_invocation_span (self , span : dict , span_info : SpanInfo ) -> AgentInvocationSpan | None :
80104 """Convert an 'invoke_agent' span to AgentInvocationSpan.
81105
82- Skips the root "Agent workflow" span emitted by Traceloop's instrumentation.
83- This span has invoke_agent operation but no agent_name or input messages —
84- it's a structural wrapper, not a real agent invocation.
106+ Skips the root "Agent workflow" wrapper emitted by Traceloop's instrumentation.
85107 """
86108 attrs = span .get ("attributes" , {})
87109
110+ # Agent workflow span has no agent_name or input messages
88111 if not attrs .get ("gen_ai.agent.name" ) and not attrs .get ("gen_ai.input.messages" ):
89112 return None
90113
0 commit comments