Description
When using stream=True with FoundryAgent.run(), the chat and execute_tool spans appear as siblings of invoke_agent instead of children. The span tree is flat instead of nested.
Expected:
invoke_agent (WeatherAgent) ← parent
├── chat (gpt-5.4) ← child
├── execute_tool (get_weather) ← child
└── chat (gpt-5.4) ← child
Actual (streaming):
invoke_agent (WeatherAgent) ← sibling
chat (gpt-5.4) ← sibling
execute_tool (get_weather) ← sibling
chat (gpt-5.4) ← sibling
Root Cause:
In AgentTelemetryLayer._trace_agent_invocation() (observability.py), the streaming path creates the invoke_agent span with:
span = get_tracer().start_span(f"{operation} {span_name}")
This creates the span but does not activate it as the current context. Child spans (chat, execute_tool) have no way to discover invoke_agent as their parent.
Compare with the non-streaming path which correctly uses _get_span():
with _get_span(attributes=attributes, span_name_attribute=OtelAttr.AGENT_NAME) as span:
# ... execute() runs here, child spans auto-parent correctly
_get_span() calls start_span() + trace.use_span(), which activates the span as the current context. The streaming path skips use_span() entirely.
The same pattern exists in ChatTelemetryLayer.get_response() streaming path, with this comment:
# Create span directly without trace.use_span() context attachment.
# Streaming spans are closed asynchronously in cleanup hooks, which run
# in a different async context than creation — using use_span() would
# cause "Failed to detach context" errors from OpenTelemetry.
This is a known trade-off, but it results in broken span hierarchy for all streaming agent invocations.
Code Sample
agent = FoundryAgent(...) # or Agent(...)
session = agent.create_session()
# Streaming — spans are siblings (broken)
async for update in agent.run("Hello", session=session, stream=True):
pass
# Non-streaming — spans are correctly nested
response = await agent.run("Hello", session=session, stream=False)
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.0.0, agent-framework-foundry: 1.0.0
Python Version
3.10
Additional Context
Suggested Fix
Consider using trace.use_span() with end_on_exit=False during the synchronous setup phase to establish parent context, then end the span asynchronously in the cleanup hook. The context detach would happen before yielding the stream, avoiding the cross-async-context issue.
Environment
- agent-framework: 1.0.0
- Python: 3.10
- OS: Windows
Description
When using
stream=TruewithFoundryAgent.run(), thechatandexecute_toolspans appear as siblings ofinvoke_agentinstead of children. The span tree is flat instead of nested.Expected:
Actual (streaming):
Root Cause:
In
AgentTelemetryLayer._trace_agent_invocation()(observability.py), the streaming path creates theinvoke_agentspan with:This creates the span but does not activate it as the current context. Child spans (
chat,execute_tool) have no way to discoverinvoke_agentas their parent.Compare with the non-streaming path which correctly uses
_get_span():_get_span()callsstart_span()+trace.use_span(), which activates the span as the current context. The streaming path skipsuse_span()entirely.The same pattern exists in
ChatTelemetryLayer.get_response()streaming path, with this comment:This is a known trade-off, but it results in broken span hierarchy for all streaming agent invocations.
Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.0.0, agent-framework-foundry: 1.0.0
Python Version
3.10
Additional Context
Suggested Fix
Consider using
trace.use_span()withend_on_exit=Falseduring the synchronous setup phase to establish parent context, then end the span asynchronously in the cleanup hook. The context detach would happen before yielding the stream, avoiding the cross-async-context issue.Environment