fix(governance): don't open the run span when the host already has one - #169
Conversation
The governance runtime opens a `uipath.governance.run` span around every agent invocation, between the host's span and the agent's own run span. Hosts filter spans on export — the Agents LLMOps exporter keeps only spans carrying `uipath.custom_instrumentation` and drops the rest without re-parenting their children. This span doesn't carry the marker, so it is dropped while the agent run span still references it, leaving the agent run span pointing at a parent id that never reaches the backend. Observed in production: conversational agent run spans stopped appearing under the CAS Exchange span across prod rings 1-5 and staging, and the BEFORE_AGENT guardrail rule spans surfaced at the trace root for the same reason. Skip the span when a valid span context is already current. The host's span supplies the trace_id that was this span's only purpose there, so governance events still correlate and the agent run span attaches directly to the host's span. With no ambient span the wrapper still opens a root span, so standalone runs keep one trace per agent run. Alternative to #168, which fixes the same break by marking the span for export instead. That keeps the span but surfaces it in customer-facing traces as a new level between Exchange and the agent run; this one keeps traces unchanged but gives up the span. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new tests rely on patching a private OpenTelemetry global in a way that may not reliably restore state across OpenTelemetry implementations/versions, making the suite brittle.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adjusts the governance runtime’s OpenTelemetry wrapping so it does not open the uipath.governance.run span when an ambient valid span context already exists, preventing downstream spans from being orphaned when hosts drop the wrapper span during export.
Changes:
- Make
_governance_root_span()a no-op when a valid current span context is already active. - Add tests that validate
execute/streambehavior under a host span vs. no host span, including exported-span assertions.
File summaries
| File | Description |
|---|---|
src/uipath/runtime/governance/runtime.py |
Skip opening uipath.governance.run when a valid ambient span context exists, to avoid creating a dropped parent span. |
tests/test_governance_runtime.py |
Add in-memory-exporter tests covering host-span vs. no-host-span scenarios for both execute and stream. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Drop the None check on get_current_span — OTel returns INVALID_SPAN, never None, so the span-context validity check is sufficient. - Reword the behavior-matrix bullet: the no-op triggers on any valid current span context, including a remotely propagated one, not only a host-opened span. - Snapshot and restore the same private tracer-provider global in the test helper rather than mixing it with get_tracer_provider(). - Assert the agent span's parent off the exported ReadableSpan; the live Span protocol has no parent attribute, which failed mypy in CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|



Alternative to #168 — same bug, other fix. Pick one, close the other.
Problem
Conversational agent run spans stopped showing up under the CAS
Exchangespan — on prod rings 1-5 and staging. (Slack thread)In the sample production trace, the agent run span points at a parent id that isn't in the trace at all:
Exchange380056a28af21b0dConversationConversational agent run52f13179f466cf8f8eb59fa2b10766c8— not presentSo it's a dropped parent, not a mis-parented child.
Cause
_governance_root_spanopens auipath.governance.runspan around everyexecute/stream, which sits between the host's span and the agent's own run span.Hosts filter spans on export. The Agents LLMOps exporter keeps only spans carrying
uipath.custom_instrumentationand drops the rest without re-parenting their children. This span doesn't carry the marker, so it never reaches the backend — while the agent run span still references it. The agent run span and its whole subtree detach from the host's trace.Same reason the BEFORE_AGENT guardrail rule spans (
guardrail-harmful-content,-prompt-injection,-pii) surface at the trace root: they fire before the agent run span exists, so their parent was the dropped span too. The rule spans that fire during the model hooks are parented on the agent run span and look correct.Note this is not the
Governance evaluationspan from LLM Gateway — that one is correctly nested underLLM call. Two similarly-named spans; the thread chased the wrong one for a bit.Fix
Skip the span when a valid span context is already current.
Under a host, the span's only contribution was a shared
trace_id— which the host's span already supplies. Governance events still correlate exactly as before, and the agent run span attaches directly toExchange. With no ambient span (standalone / CLI runs) the wrapper still opens a root span, unchanged.Customer-facing traces look the way they did before the span was introduced.
Versus #168
Exchangeuipath.governance.runin customer tracesExchangeand the agent runagent_name/runtime_idunder a hostComes down to whether governance wants that span in customer-facing traces.
Follow-up worth doing separately (either way)
The export filter in
uipath-agents-pythonshould re-parent survivors to their nearest surviving ancestor. Any future filtered span will orphan its children exactly this way.Tests
Three added to
tests/test_governance_runtime.py:agent_name/runtime_idstreamtakes the same path asexecuteFull suite passes (435).
🤖 Generated with Claude Code