When an agent running in a workflow calls a tool after a model call, the trace shows the tool's span nested inside the model call's temporal:startActivity span instead of next to it under the turn. This happens because the tracing interceptor makes temporal:startActivity the current Agents SDK span while it writes the span id into the activity header and never restores the previous span, so every span created afterwards in that context is parented to it. The fix is to keep the span current only while the header is captured.
Details
_ContextPropagationWorkflowOutboundInterceptor.start_activity (likewise start_child_workflow and start_local_activity) in temporalio/contrib/openai_agents/_trace_interceptor.py does:
span = custom_span(name="temporal:startActivity", data={...})
span.start(mark_as_current=True)
self.root().set_header_from_context(input) # reads get_current_span()
handle = self.next.start_activity(input)
handle.add_done_callback(lambda _: span.finish())
Nothing resets the current span, and finish() in the done callback does not either (it could not safely: the callback runs in a copy of the contextvars context, so a token reset there raises ValueError). The span stays current until the enclosing span exits, and every span created in between without an explicit parent (tool calls, handoffs, guardrails within the same turn) is parented to it.
Observed:
turn
└─ temporal:startActivity (model call)
├─ temporal:executeActivity
└─ lookup_account (tool)
└─ temporal:startActivity → temporal:executeActivity
Expected:
turn
├─ temporal:startActivity (model call)
│ └─ temporal:executeActivity
└─ lookup_account (tool)
└─ temporal:startActivity → temporal:executeActivity
The interceptor is shared by the OpenAI platform and OpenTelemetry tracing modes, so both show the same nesting. tests/contrib/openai_agents/test_openai_tracing.py::test_tracing asserts turn → temporal:startActivity → temporal:executeActivity, which is the intended shape, but its workflow makes no tool calls, so this was not covered.
Related but separate: #1852. Found while validating temporalio/samples-python#365.
When an agent running in a workflow calls a tool after a model call, the trace shows the tool's span nested inside the model call's
temporal:startActivityspan instead of next to it under the turn. This happens because the tracing interceptor makestemporal:startActivitythe current Agents SDK span while it writes the span id into the activity header and never restores the previous span, so every span created afterwards in that context is parented to it. The fix is to keep the span current only while the header is captured.Details
_ContextPropagationWorkflowOutboundInterceptor.start_activity(likewisestart_child_workflowandstart_local_activity) intemporalio/contrib/openai_agents/_trace_interceptor.pydoes:Nothing resets the current span, and
finish()in the done callback does not either (it could not safely: the callback runs in a copy of the contextvars context, so a token reset there raisesValueError). The span stays current until the enclosing span exits, and every span created in between without an explicit parent (tool calls, handoffs, guardrails within the same turn) is parented to it.Observed:
Expected:
The interceptor is shared by the OpenAI platform and OpenTelemetry tracing modes, so both show the same nesting.
tests/contrib/openai_agents/test_openai_tracing.py::test_tracingassertsturn → temporal:startActivity → temporal:executeActivity, which is the intended shape, but its workflow makes no tool calls, so this was not covered.Related but separate: #1852. Found while validating temporalio/samples-python#365.