// Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Foundry; /// /// Delegating that captures any x-client-* headers stored on /// by callers of /// and pushes /// them onto a for the lifetime of the run. The scope is read by /// inside the SCM transport pipeline and stamped onto the /// outbound request. /// /// /// /// The decorator snapshots the header dictionary at scope-push time so concurrent runs that share /// the same reference are isolated; mutating the source dictionary after /// RunAsync begins does not leak into in-flight requests. /// /// /// Streaming uses the async-iterator pattern so the AsyncLocal scope stays alive across yields, /// which is required because the underlying HTTP send happens during enumeration. /// /// internal sealed class ClientHeadersAgent : DelegatingAIAgent { public ClientHeadersAgent(AIAgent innerAgent) : base(innerAgent) { } /// protected override Task RunCoreAsync( IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { var snapshot = TrySnapshot(options); if (snapshot is not null) { // AsyncLocal mutations made inside an awaited async method do not leak back to the // caller after the method returns, so we do not need an explicit restore step here. // See ClientHeadersScope remarks. ClientHeadersScope.Current = snapshot; } return this.InnerAgent.RunAsync(messages, session, options, cancellationToken); } /// protected override async IAsyncEnumerable RunCoreStreamingAsync( IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var snapshot = TrySnapshot(options); if (snapshot is not null) { ClientHeadersScope.Current = snapshot; } await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false)) { yield return update; } } /// Reads the header dictionary stamped by WithClientHeader(s) and returns an immutable snapshot, or if none. private static Dictionary? TrySnapshot(AgentRunOptions? options) { if (options is not ChatClientAgentRunOptions { ChatOptions: { } chatOptions }) { return null; } var headers = chatOptions.GetClientHeaders(); if (headers is null || headers.Count == 0) { return null; } // Copy to defeat caller mutation after RunAsync starts. var copy = new Dictionary(headers.Count, System.StringComparer.OrdinalIgnoreCase); foreach (var kvp in headers) { copy[kvp.Key] = kvp.Value; } return copy; } }