@@ -337,20 +337,7 @@ def _convert_agent_invocation_span(self, span: dict, span_info: SpanInfo) -> Age
337337
338338 user_prompt = ""
339339 agent_response = ""
340- available_tools : list [ToolConfig ] = []
341-
342- # Parse available tools from gen_ai.agent.tools or gen_ai.tool.definitions
343- try :
344- tools_json = attrs .get ("gen_ai.agent.tools" , "" ) or attrs .get ("gen_ai.tool.definitions" , "[]" )
345- tool_list = json .loads (str (tools_json )) if isinstance (tools_json , str ) else tools_json
346- if isinstance (tool_list , list ):
347- for item in tool_list :
348- if isinstance (item , str ):
349- available_tools .append (ToolConfig (name = item ))
350- elif isinstance (item , dict ):
351- available_tools .append (ToolConfig (name = item .get ("name" , "" )))
352- except (json .JSONDecodeError , TypeError ):
353- pass
340+ available_tools = self ._parse_tool_definitions (attrs )
354341
355342 # Extract from span_events (manual instrumentation format)
356343 for event in span .get ("span_events" , []):
@@ -444,6 +431,31 @@ def _convert_agent_invocation_span(self, span: dict, span_info: SpanInfo) -> Age
444431 # Content Processing Helpers
445432 # =========================================================================
446433
434+ @staticmethod
435+ def _parse_tool_definitions (attributes : dict ) -> list [ToolConfig ]:
436+ """Parse available tools from a span's gen_ai.agent.tools or gen_ai.tool.definitions."""
437+ raw = attributes .get ("gen_ai.agent.tools" , "" ) or attributes .get ("gen_ai.tool.definitions" , "[]" )
438+ try :
439+ tool_list = json .loads (raw ) if isinstance (raw , str ) else raw
440+ except (json .JSONDecodeError , TypeError ):
441+ return []
442+ if not isinstance (tool_list , list ):
443+ return []
444+
445+ configs : list [ToolConfig ] = []
446+ for item in tool_list :
447+ if isinstance (item , str ):
448+ if item :
449+ configs .append (ToolConfig (name = item ))
450+ elif isinstance (item , dict ):
451+ fn = item ["function" ] if isinstance (item .get ("function" ), dict ) else item
452+ name = fn .get ("name" , "" )
453+ if name :
454+ configs .append (
455+ ToolConfig (name = name , description = fn .get ("description" ), parameters = fn .get ("parameters" ))
456+ )
457+ return configs
458+
447459 def _convert_message_list_from_event (self , event_attrs : dict ) -> list [UserMessage | AssistantMessage ]:
448460 """Parse operation.details event attrs and return typed messages via _convert_message_list."""
449461 messages : list [UserMessage | AssistantMessage ] = []
@@ -636,23 +648,16 @@ def _enrich_openai_agent_spans(self, trace: Trace, raw_spans: list[dict]) -> Non
636648
637649 This enriches by:
638650 1. Re-parenting sub-agents using ``agent_handoff`` spans.
639- 2. Deriving each agent's ``available_tools`` from its owned tool spans.
651+ 2. Backfilling ``user_prompt`` / ``agent_response`` from child chat spans.
652+ 3. Deriving each agent's ``available_tools`` from child spans
640653 """
641654 agent_spans = {
642655 s .span_info .span_id : s for s in trace .spans if isinstance (s , AgentInvocationSpan ) and s .span_info .span_id
643656 }
644657
645658 self ._reparent_by_handoff (agent_spans , raw_spans )
646-
647659 self ._backfill_agent_prompts (trace , agent_spans )
648-
649- for span in trace .spans :
650- if isinstance (span , ToolExecutionSpan ) and span .tool_call .name :
651- owner_id = span .agent_span_id
652- if owner_id and owner_id in agent_spans :
653- agent = agent_spans [owner_id ]
654- if not any (t .name == span .tool_call .name for t in agent .available_tools ):
655- agent .available_tools .append (ToolConfig (name = span .tool_call .name ))
660+ self ._backfill_agent_tools (trace , agent_spans , raw_spans )
656661
657662 def _reparent_by_handoff (
658663 self ,
@@ -689,6 +694,32 @@ def _reparent_by_handoff(
689694 if from_span and to_span and from_span .span_info .span_id :
690695 to_span .span_info .parent_span_id = from_span .span_info .span_id
691696
697+ def _backfill_agent_tools (
698+ self ,
699+ trace : Trace ,
700+ agent_spans : dict [str , AgentInvocationSpan ],
701+ raw_spans : list [dict ],
702+ ) -> None :
703+ """Populate available_tools on agent spans using their child spans"""
704+ # Fill agent spans' available tools from child chat spans' tool definitions.
705+ raw_attrs_by_id = {raw .get ("span_id" ): raw .get ("attributes" , {}) for raw in raw_spans }
706+ for span in trace .spans :
707+ if not isinstance (span , InferenceSpan ):
708+ continue
709+ agent = agent_spans .get (span .span_info .parent_span_id or "" )
710+ if agent is None or agent .available_tools :
711+ continue
712+ configs = self ._parse_tool_definitions (raw_attrs_by_id .get (span .span_info .span_id , {}))
713+ if configs :
714+ agent .available_tools = list (configs )
715+
716+ # Fallback: get the names of tools the agents called.
717+ for span in trace .spans :
718+ if isinstance (span , ToolExecutionSpan ) and span .tool_call .name :
719+ agent = agent_spans .get (span .agent_span_id or "" )
720+ if agent and not any (t .name == span .tool_call .name for t in agent .available_tools ):
721+ agent .available_tools .append (ToolConfig (name = span .tool_call .name ))
722+
692723 def _backfill_agent_prompts (
693724 self ,
694725 trace : Trace ,
0 commit comments