// 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;
}
}