Skip to content

feat(genai): add main agent attribution propagation#158

Merged
JacksonWeber merged 2 commits into
microsoft:mainfrom
JacksonWeber:feat/main-agent-propagation
May 27, 2026
Merged

feat(genai): add main agent attribution propagation#158
JacksonWeber merged 2 commits into
microsoft:mainfrom
JacksonWeber:feat/main-agent-propagation

Conversation

@JacksonWeber
Copy link
Copy Markdown
Contributor

Summary

Adds main agent attribution propagation to the Microsoft OpenTelemetry JS distro, mirroring the Python distro's #120 (feature) and #171 (LangChain timing fix).

In multi-agent / agent-with-tools scenarios, downstream chat and execute_tool spans need to be attributed to the top-level user-facing agent, not the inner sub-agent or LLM call that emitted them. This PR adds processors that copy microsoft.gen_ai.main_agent.* from parent → child spans (and from active span → log records) at on-start / on-emit time, so the attribution survives through arbitrarily deep span trees.

What's added

GenAIMainAgentSpanProcessor

  • onStart: copies microsoft.gen_ai.main_agent.{name,id,version,conversation_id} from the parent span; falls back to the parent's gen_ai.agent.{name,id,version} / gen_ai.conversation.id if the primary attrs aren't set yet.
  • onEnd: when the span is itself a top-level invoke_agent and has no main_agent.* attrs, self-copies from its own gen_ai.agent.* / gen_ai.conversation.id so the top-level agent identifies itself as the main agent (mutates the attribute map directly since setAttribute is a no-op on an ended JS span).

GenAIMainAgentLogRecordProcessor

  • onEmit: copies all microsoft.gen_ai.main_agent.* attributes from the active/emit-context span onto the emitted log record.

Both processors are prepended (unshift) to the distro's span/log processor lists when azureMonitor.enabled is true — matching the Python distro's gating on enable_azure_monitor.

LangChain integration fixes

  1. PR #171 timing fix (JS port): moved setOperationTypeAttribute / setAgentAttributes / setSessionIdAttribute calls to immediately after tracer.startSpan(...) in startTracing. Previously these only fired in _endTrace, so child chat / execute_tool runs created under an invoke_agent would onStart before the parent had its agent attrs, and propagation silently failed.
  2. JS-only parent-Span fix: child spans now carry the actual parent Span (trace.setSpan(ctx, parent)) rather than just its SpanContext (trace.setSpanContext(...)). The latter wraps the parent in a NonRecordingSpan whose attributes are not readable — so even with the timing fix above, propagation would never have worked. Required for the feature to function at all.

Tests

  • 9 new unit tests for the processors (onStart propagation, primary-vs-fallback precedence, multi-level chains, onEnd self-copy, no-op on non-invoke_agent operations, skip-when-already-set, log record propagation with/without active span).
  • 1 new end-to-end test that drives the real LangChainTracer through the OTel SDK and asserts that a child chat span inherits microsoft.gen_ai.main_agent.name from an invoke_agent parent (would have failed without both fixes above).
  • Full unit suite: 837 passed, 5 todo, 0 failed.
  • Type-check: tsc --noEmit -p tsconfig.src.json clean.

End-to-end validation

Tested against a real LangGraph ReAct agent (createReactAgent({ name: "TravelBot", tools: [get_weather], llm: ChatOpenAI(gpt-4.1) })) running against Azure Foundry OpenAI. Four spans emitted in one trace:

invoke_agent TravelBot          (root, kind=SERVER)
├── chat gpt-4.1                inherits microsoft.gen_ai.main_agent.name = "TravelBot"
├── execute_tool get_weather    inherits microsoft.gen_ai.main_agent.name = "TravelBot"
└── chat gpt-4.1                inherits microsoft.gen_ai.main_agent.name = "TravelBot"

All 3 child spans correctly carry the main-agent name via onStart propagation; the root span carries it via the onEnd self-copy.

Files

  • New: src/genai/mainAgent/{constants,processor,index}.ts
  • New tests: test/internal/unit/genai/mainAgent/processor.test.ts, test/internal/unit/genai/langchain/mainAgentPropagation.test.ts
  • Modified:
    • src/distro/distro.ts — register processors when Azure Monitor is enabled
    • src/genai/instrumentations/langchain/tracer.ts — timing + parent-Span fixes
    • src/genai/index.ts, src/index.ts — re-exports

Mirrors microsoft/opentelemetry-distro-python microsoft#120 (main-agent
processors) and #171 (LangChain timing fix).

Adds:
- GenAIMainAgentSpanProcessor: copies microsoft.gen_ai.main_agent.*
  attributes from parent to child spans at onStart, with fallback to
  the parent's gen_ai.agent.* / gen_ai.conversation.id. At onEnd, a
  top-level invoke_agent span without any main_agent.* attrs
  self-copies from its own gen_ai.agent.* / gen_ai.conversation.id so
  the top-level agent identifies itself as the main agent.
- GenAIMainAgentLogRecordProcessor: copies main_agent.* attributes
  from the active span onto every emitted log record.
- Both processors are prepended to the distro's span/log processor
  lists when azureMonitor is enabled, so they fire before exporters.

LangChain integration fixes:
- Set gen_ai.operation.name, gen_ai.agent.*, and gen_ai.conversation.id
  on the span immediately after startSpan (not at _endTrace), so child
  runs created underneath an invoke_agent see the parent's attrs at
  their onStart and inherit microsoft.gen_ai.main_agent.* via the new
  processor (port of python PR #171).
- Attach the actual parent Span (via trace.setSpan) to the new span's
  context instead of attaching only its SpanContext (trace.setSpanContext).
  The latter wraps the parent in a NonRecordingSpan whose attributes are
  not readable by span processors, which would have blocked propagation
  regardless of the timing fix above.

Tests:
- New unit tests for both processors (onStart propagation, primary-vs-
  fallback precedence, multi-level chains, onEnd self-copy, no-op on
  non-invoke_agent operations, log record propagation, etc.).
- New end-to-end test exercising the LangChain tracer through the real
  SDK to confirm a child chat span inherits microsoft.gen_ai.main_agent
  .name from an invoke_agent parent.

Verified end-to-end against a LangGraph ReAct agent calling Azure OpenAI:
4 spans (1 invoke_agent + 2 chat + 1 execute_tool); all child spans
carry microsoft.gen_ai.main_agent.name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 27, 2026 22:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds GenAI main-agent attribution propagation so child spans and log records can carry top-level agent identity for Azure Monitor scenarios.

Changes:

  • Adds main-agent constants plus span/log processors for propagating microsoft.gen_ai.main_agent.*.
  • Registers the processors when Azure Monitor is enabled.
  • Updates LangChain tracing so parent span identity attributes are available before child runs start, with unit coverage for propagation behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/genai/mainAgent/constants.ts Defines main-agent attribution attribute keys.
src/genai/mainAgent/processor.ts Implements span and log record propagation processors.
src/genai/mainAgent/index.ts Re-exports the new main-agent module API.
src/genai/index.ts Exposes main-agent exports from the GenAI entrypoint.
src/index.ts Exposes main-agent exports from the package entrypoint.
src/distro/distro.ts Registers main-agent processors when Azure Monitor is enabled.
src/genai/instrumentations/langchain/tracer.ts Sets LangChain identity attributes earlier and preserves parent Span objects.
test/internal/unit/genai/mainAgent/processor.test.ts Adds unit tests for span/log processor behavior.
test/internal/unit/genai/langchain/mainAgentPropagation.test.ts Adds an end-to-end LangChain propagation test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/distro/distro.ts
Comment on lines +279 to +281
if (azureMonitorEnabled) {
spanProcessors.unshift(new GenAIMainAgentSpanProcessor());
logRecordProcessors.unshift(new GenAIMainAgentLogRecordProcessor());
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Member

@hectorhdzg hectorhdzg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do an E2E verification once this is merged to ensure everything works fine

@JacksonWeber JacksonWeber merged commit 1e19c94 into microsoft:main May 27, 2026
5 checks passed
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.

3 participants