Repository navigation
.NET: fix: preserve agent continuation token when wrapped with UseOpenTelemetry - #8265
Atharva Vichare (atty57) wants to merge 5 commits into
Conversation
OpenTelemetryAgent routes runs through OpenTelemetryChatClient, so every response makes an AgentResponse -> ChatResponse -> AgentResponse round trip. AsChatResponse/AsChatResponseUpdate return the inner agent's own ChatResponse when that is the response's raw representation, so the round trip dropped agent-level state: for a ChatClientAgent the caller received the provider's raw continuation token instead of the ChatClientAgentContinuationToken, and resuming with it threw. AgentId was lost the same way. Wrap the inner agent's response in a ChatResponse that keeps the response itself as the raw representation, so RunCoreAsync/RunCoreStreamingAsync hand back the original instance unchanged. Fixes microsoft#8243
There was a problem hiding this comment.
🟡 Changes recommended
Both new wrappers omit ModelId, causing response-model telemetry loss for regular and streaming calls.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes #8243 by preserving agent-level continuation state through OpenTelemetry wrappers.
Changes:
- Preserves original agent responses and streaming updates.
- Adds continuation-token serialization/resumption tests.
- Retains conversation IDs for telemetry.
File summaries
| File | Description |
|---|---|
dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs |
Wraps responses while retaining agent state. |
dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs |
Tests token preservation and resumption. |
Review details
Suppressed comments (1)
dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs:360
- The streaming wrapper similarly omits
ChatResponseUpdate.ModelId.OpenTelemetryChatClientconsumes the model ID from updates for chunk metrics and from the assembled response forgen_ai.response.model, so streamed calls lose that telemetry after this change. Preserve the raw update's model ID here as well.
ConversationId = (update.RawRepresentation as ChatResponseUpdate)?.ConversationId,
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Streaming conversion omits ModelId, causing response-model telemetry loss.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs:361
ModelIdis omitted from the streaming wrapper. Providers such asFoundryChatClientsetChatResponseUpdate.ModelId, andOpenTelemetryChatClientreads it to emit the response-model span and metric tags. Because this new wrapper is what telemetry processes, streaming runs now lose that metadata while the non-streaming helper preserves it. Copy the raw update'sModelIdas well.
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Balanced
| MessageId = update.MessageId, | ||
| RawRepresentation = update, | ||
| ResponseId = update.ResponseId, | ||
| Role = update.Role, |
There was a problem hiding this comment.
Could we also copy ModelId from the underlying ChatResponseUpdate here? The previous conversion preserved it.
There was a problem hiding this comment.
Good catch - done in 516aef2. ModelId is now copied from the inner ChatResponseUpdate the same way ConversationId is, and I added a test asserting gen_ai.response.model shows up on the streaming activity (nothing covered that before).
The ChatResponseUpdate the agent's update is wrapped in for telemetry dropped ModelId, so gen_ai.response.model was missing on streaming runs.
Motivation & Context
Wrapping an agent with
UseOpenTelemetrybreaks background responses. The continuation token returned from a backgroundRunAsyncis the underlying provider's raw token rather than the agent's ownChatClientAgentContinuationToken, so an opaqueToBytes/FromBytessave-and-restore followed by a resume throws:The same flow works when OpenTelemetry is not in the pipeline. This blocks the scenario where background responses are started by one process and polled or resumed by another, since that requires persisting a serialized session and token while still emitting telemetry for logging and traceability.
OpenTelemetryAgentdelegates to the inner agent throughOpenTelemetryChatClientand aForwardingChatClient, so every response makes anAgentResponse->ChatResponse->AgentResponseround trip.AgentResponseExtensions.AsChatResponsereturns the inner agent's ownChatResponsedirectly whenever that is the response's raw representation, which is exactly the case forChatClientAgent. The wrapped continuation token andAgentIdlive only on theAgentResponse, so both were discarded on the way back out andRunCoreAsyncrebuilt a response from the provider'sChatResponse, carrying the provider's raw token.RunStreamingAsynclost them the same way throughAsChatResponseUpdate.Description & Review Guide
What are the major changes?
ForwardingChatClientno longer usesAsChatResponse/AsChatResponseUpdate. It now builds theChatResponse/ChatResponseUpdatethat is handed toOpenTelemetryChatClientwith the inner agent's own response as theRawRepresentation.RunCoreAsyncandRunCoreStreamingAsyncalready looked for anAgentResponse/AgentResponseUpdatethere, so they now return the inner agent's instance unchanged instead of reconstructing a lossy copy.ConversationIdis copied across from the innerChatResponse/ChatResponseUpdateso the chat span keeps emittinggen_ai.conversation.id.OpenTelemetryAgentTests: continuation token preservation forRunAsyncandRunStreamingAsync, and the reported scenario end to end - serialize the token withToBytes, restore it withFromBytes, resume, and assert the chat client receives its own token back. All three fail without the source change.What is the impact of these changes?
AgentId, and any other agent-level response state now survive aUseOpenTelemetrywrapper. Callers no longer need workarounds to reconstruct the agent token.ForwardingChatClient;AsChatResponse/AsChatResponseUpdatekeep their documented short-circuit behavior and are untouched.What do you want reviewers to focus on?
ConversationIdfrom the raw representation is the right way to keep the chat span intact, or whether the span should take it from the request options instead.DelegatingAIAgentimplementations that perform the same round trip should share a helper rather than keeping this private toOpenTelemetryAgent.Related Issue
Fixes #8243
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.