Skip to content

Python: execute_tool OTel spans intermittently lost due to missing context propagation in asyncio.gather #6357

@rongrong77

Description

@rongrong77

Description

When an agent uses multiple tools in a single turn, execute_tool spans are intermittently missing from exported traces. The root cause is that _try_execute_function_calls in
_tools.py
executes tool calls concurrently via asyncio.gather without copying the OTel context into each task. This causes span context race conditions — some execute_tool spans fail to attach to their parent and are silently dropped.

The issue is non-deterministic: the same agent invocation sometimes produces complete traces (with all execute_tool spans) and sometimes produces incomplete traces (missing one or more execute_tool spans).

Expected behavior: Every tool execution should produce a correctly-parented execute_tool span, regardless of concurrent execution.

Proposed fix: Wrap each concurrent task with contextvars.copy_context() to ensure OTel context isolation:

Code Sample

from dotenv import load_dotenv
load_dotenv()

import os
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = os.getenv("AAMS_OTLP_TRACES_ENDPOINT")
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"x-api-key={os.getenv('AAMS_API_KEY')}"
os.environ["OTEL_EXPORTER_OTLP_PROTOCOL"] = "http/protobuf"

from agent_framework.observability import configure_otel_providers
configure_otel_providers(enable_sensitive_data="true")

from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient

@tool(approval_mode="never_require")
async def tool_a(city: str) -> str:
    return f"Result A for {city}"

@tool(approval_mode="never_require")
async def tool_b(city: str) -> str:
    return f"Result B for {city}"

client = OpenAIChatClient(model="gpt-4o-mini", api_key=os.getenv("API_KEY"), base_url="...")
agent = Agent(client=client, name="TestAgent", instructions="...", tools=[tool_a, tool_b])

# When LLM returns parallel tool calls (tool_a + tool_b),
# sometimes only one execute_tool span appears in the trace.
result = await agent.run("Call both tools", session=agent.create_session())

Error Messages / Stack Traces

No error is raised. The issue is silent span loss. In the exported trace, execute_tool spans are intermittently missing. Example:

Expected spans: invoke_agent → chat → execute_tool tool_a + execute_tool tool_b → chat
Actual (intermittent): invoke_agent → chat → execute_tool tool_a → chat (tool_b span missing)

Root cause location: 
_tools.py
, function _try_execute_function_calls, line:

execution_results = await asyncio.gather(*[
    invoke_with_termination_handling(function_call, seq_idx)
    for seq_idx, function_call in enumerate(function_calls)
])
Python Version: 3.12

Package Versions

agent-framework-core==1.1.0 agent-framework-openai==1.1.0 opentelemetry-sdk==1.40.0 opentelemetry-exporter-otlp-proto-http==1.40.0

Python Version

3.12

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions