Skip to content

Prototype showing gen_ai.conversation_root span attribute to mark the root GenAI span of a conversation (WIP)#187

Draft
wrisa wants to merge 3 commits into
open-telemetry:mainfrom
wrisa:conversation-root
Draft

Prototype showing gen_ai.conversation_root span attribute to mark the root GenAI span of a conversation (WIP)#187
wrisa wants to merge 3 commits into
open-telemetry:mainfrom
wrisa:conversation-root

Conversation

@wrisa

@wrisa wrisa commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes # (issue356)
``
Adds gen_ai.conversation_root as a boolean span attribute automatically set on `WorkflowInvocation` and `AgentInvocation` spans that are the outermost GenAI span in a trace — i.e., the entry point of a conversation.

Detection uses a context-scoped attribute key (following the API shape of open-telemetry/opentelemetry-specification#4931) stored under a private OTel context key rather than raw OTel span parentage. This correctly handles the common production case where a GenAI root span lives under a non-GenAI parent (HTTP, gRPC, queue consumer, etc.) — a plain OTel parent span never writes the key, so it never suppresses conversation_root.


Design

Context-scoped attribute key

A private OTel context key is used to propagate a gen_ai.conversation_root marker through the OTel context:

  context_attributes.py
  _GENAI_CONTEXT_ATTRS_KEY = otel_context.create_key(
      "opentelemetry.util.genai.context_scoped_attrs"
  )

set_context_scoped_attributes uses existing-key-wins semantics (outermost writer wins), matching the CSA spec. This means once the root sets the key, any nested WorkflowInvocation or AgentInvocation sees it already present and does not mark itself as root.

Auto-detection in WorkflowInvocation and AgentInvocation

Before _start():

  csa = get_context_scoped_attributes()
  if "gen_ai.conversation_root" not in csa:
      # No enclosing GenAI root — this is the root
      self.conversation_root = True

Propagate the key so nested invocations do not mark themselves root

extra_ctx = set_context_scoped_attributes({"gen_ai.conversation_root": True})
self._start(..., extra_context=extra_ctx)

The key is attached alongside the span in a single attach() call and automatically detached when the invocation ends (same lifecycle as the span context token), so the next independent conversation starts fresh.

Emission

In GenAIInvocation._finish():

  if self.conversation_root is not None:
      self.span.set_attribute("gen_ai.conversation_root", self.conversation_root)

Only emitted when explicitly set — None means "not applicable" and produces no attribute.

Scope

Only WorkflowInvocation and AgentInvocation participate in root detection. InferenceInvocation (chat), ToolInvocation (execute_tool), EmbeddingInvocation, and RetrievalInvocation never set conversation_root — even when called at the top level with no GenAI parent — because they represent model/tool operations, not conversation-level entry points.

Example traces

Agent under HTTP span (common production case):

  HTTP GET /plan-trip                              ← plain OTel span, ignored by detection
    invoke_agent Travel Planner   conversation_root=True   ← root: no CSA key in context
      invoke_agent Flight Search                           ← not root: key already in context
        chat                                              ← never eligible
        execute_tool search_flights                       ← never eligible
      invoke_agent Hotel Booking                          ← not root: key already in context
        chat                                              ← never eligible

Workflow wrapping agents:

  invoke_workflow Travel Planner  conversation_root=True   ← root
    invoke_agent Flight Search                            ← not root
      chat                                               ← never eligible
    invoke_agent Hotel Booking                           ← not root
      chat                                               ← never eligible

What this does NOT do

  • Does not mark chat, execute_tool, retrieval, or embedding spans as root even when called at the top level — those are model/tool operations, not conversation entry points
  • Does not add gen_ai.conversation_root to the semconv (a separate semconv PR/issue should track that)
  • Does not use ContextVar — uses the OTel context system directly so propagation is consistent with async frameworks and distributed context

Relation to prior art

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How has this been tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. List any relevant details for your test
configuration.

  • Test A

Checklist

See CONTRIBUTING.md
for the style guide, changelog guidance, and more.

  • Followed the style guidelines of this project
  • Changelog updated if the change requires an entry
  • Unit tests added
  • Documentation updated

@wrisa wrisa changed the title Add conversation root Add conversation root(WIP) Jun 30, 2026
@wrisa wrisa changed the title Add conversation root(WIP) Prototype showing gen_ai.conversation_root span attribute to mark the root GenAI span of a conversation (WIP) Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant