3030from langchain_core .messages import ToolCall , ToolMessage
3131from langchain_core .tools import BaseTool
3232from langgraph .types import Command
33+ from opentelemetry import trace as otel_trace
3334from pydantic import BaseModel , Field
3435from uipath ._utils ._ssl_context import get_httpx_client_kwargs
3536from uipath .agent .models .agent import AgentA2aResourceConfig
37+ from uipath .core .tracing .span_utils import UiPathSpanUtils
3638
3739from uipath_langchain ._utils import get_execution_folder_path
3840from uipath_langchain .agent .react .types import AgentGraphState
@@ -251,10 +253,50 @@ def _create_a2a_tool(
251253 "slug" : config .slug ,
252254 }
253255
256+ async def _invoke (
257+ * , message : str , task_id : str | None , context_id : str | None
258+ ) -> tuple [str , str , str | None , str | None ]:
259+ """Send one message to the remote agent inside an A2A trace span.
260+
261+ The span is parented under the active tool-call span (via
262+ ``UiPathSpanUtils.get_parent_context``) so the remote call nests under
263+ the tool in the Execution Trace, and is marked
264+ ``uipath.custom_instrumentation`` so the LLMOps exporter keeps it. The
265+ a2a-sdk's own transport spans are disabled in this package's __init__,
266+ so this is the single node representing the call.
267+ """
268+ parent_ctx = UiPathSpanUtils .get_parent_context ()
269+ tracer = otel_trace .get_tracer (__name__ )
270+ with tracer .start_as_current_span (raw_name , context = parent_ctx ) as span :
271+ # "openinference.span.kind" drives the SpanType shown in the UI;
272+ # "toolCall" is the recognized type for a tool invocation.
273+ span .set_attribute ("openinference.span.kind" , "toolCall" )
274+ span .set_attribute ("type" , "toolCall" )
275+ span .set_attribute ("span_type" , "toolCall" )
276+ span .set_attribute ("uipath.custom_instrumentation" , True )
277+ span .set_attribute ("tool_type" , "a2a" )
278+ span .set_attribute ("input" , message )
279+ span .set_attribute ("input.value" , message )
280+
281+ client = await a2a_client .get ()
282+ text , response_state , new_task_id , new_context_id = await _send_a2a_message (
283+ client ,
284+ agent_label ,
285+ message = message ,
286+ task_id = task_id ,
287+ context_id = context_id ,
288+ )
289+
290+ span .set_attribute ("output" , text )
291+ span .set_attribute ("output.value" , text )
292+ span .set_attribute ("task_state" , response_state )
293+ if response_state == "error" :
294+ span .set_status (otel_trace .StatusCode .ERROR , text )
295+ return text , response_state , new_task_id , new_context_id
296+
254297 async def _send (* , message : str ) -> str :
255- client = await a2a_client .get ()
256- text , state , _ , _ = await _send_a2a_message (
257- client , agent_label , message = message , task_id = None , context_id = None
298+ text , state , _ , _ = await _invoke (
299+ message = message , task_id = None , context_id = None
258300 )
259301 return _format_response (text , state )
260302
@@ -267,10 +309,7 @@ async def _a2a_wrapper(
267309 task_id = prior .get ("task_id" )
268310 context_id = prior .get ("context_id" )
269311
270- client = await a2a_client .get ()
271- text , task_state , new_task_id , new_context_id = await _send_a2a_message (
272- client ,
273- agent_label ,
312+ text , task_state , new_task_id , new_context_id = await _invoke (
274313 message = call ["args" ]["message" ],
275314 task_id = task_id ,
276315 context_id = context_id ,
0 commit comments