// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Foundry.Hosting;
///
/// Pipeline policy that emits the hosted-agent User-Agent segment
/// ("foundry-hosting/agent-framework-dotnet/{version}"), matching Python's hosted
/// contract (foundry-hosting/agent-framework-python/{version}, see
/// python/packages/core/agent_framework/_telemetry.py: the hosted prefix is joined
/// with the base agent-framework segment into a single combined User-Agent value).
///
///
///
/// The supplement value is computed once from the Microsoft.Agents.AI.Foundry.Hosting
/// assembly's informational version. The policy is idempotent on retries: if the segment
/// is already present in the User-Agent header, the policy does not append it again.
///
///
/// When a bare agent-framework-dotnet/{version} segment is already present (stamped by
/// the framework-wide AgentFrameworkUserAgentPolicy registered by
/// FoundryChatClient), this policy replaces that segment with the combined
/// hosted form so the wire never carries both forms simultaneously, preserving Python parity.
///
///
/// This policy is added at hosted-agent resolution time via the MEAI 10.5.1
/// hook on the agent's underlying chat client. It is only
/// registered when an agent is resolved by the Foundry hosting layer.
///
///
internal sealed class HostedAgentUserAgentPolicy : PipelinePolicy
{
public static HostedAgentUserAgentPolicy Instance { get; } = new HostedAgentUserAgentPolicy();
private static readonly string s_supplementValue = CreateSupplementValue();
/// Bare segment stamped by AgentFrameworkUserAgentPolicy in the non-hosted scenario; this policy upgrades it in-place when both run.
private const string BareAgentFrameworkPrefix = "agent-framework-dotnet/";
/// Combined hosted segment that this policy emits. Recognized in-place so callers whose pipelines already carry a (possibly different-version) combined segment get it replaced rather than double-prefixed (Q-D fix).
private const string CombinedHostedPrefix = "foundry-hosting/agent-framework-dotnet/";
public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
{
AppendHeader(message);
ProcessNext(message, pipeline, currentIndex);
}
public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
{
AppendHeader(message);
await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
}
private static void AppendHeader(PipelineMessage message)
{
if (message.Request.Headers.TryGetValue("User-Agent", out var existing) && !string.IsNullOrEmpty(existing))
{
// Guard against double-append on retries or when the policy is registered on
// multiple pipeline positions.
if (existing!.Contains(s_supplementValue))
{
return;
}
// Combined-form check first: if the caller's pipeline already has
// `foundry-hosting/agent-framework-dotnet/{version}` (with a version that differs
// from ours — otherwise the .Contains above would have returned early), replace the
// entire combined span in place. Without this, the bare-prefix search below would
// match `agent-framework-dotnet/` *inside* the combined segment and produce a
// malformed `foundry-hosting/foundry-hosting/agent-framework-dotnet/...` value.
var combinedIdx = existing.IndexOf(CombinedHostedPrefix, StringComparison.Ordinal);
if (combinedIdx >= 0)
{
var combinedEnd = existing.IndexOf(' ', combinedIdx);
if (combinedEnd < 0)
{
combinedEnd = existing.Length;
}
var replacedCombined = string.Concat(existing.AsSpan(0, combinedIdx), s_supplementValue.AsSpan(), existing.AsSpan(combinedEnd));
message.Request.Headers.Set("User-Agent", replacedCombined);
return;
}
// If the bare agent-framework segment is present (stamped by
// AgentFrameworkUserAgentPolicy when not hosted), upgrade it in place to the
// combined hosted form so the wire never carries both segments simultaneously.
// Mirrors Python where get_user_agent() returns a single combined string when the
// hosted prefix is registered.
var idx = existing.IndexOf(BareAgentFrameworkPrefix, StringComparison.Ordinal);
if (idx >= 0)
{
var end = existing.IndexOf(' ', idx);
if (end < 0)
{
end = existing.Length;
}
var replaced = string.Concat(existing.AsSpan(0, idx), s_supplementValue.AsSpan(), existing.AsSpan(end));
message.Request.Headers.Set("User-Agent", replaced);
return;
}
message.Request.Headers.Set("User-Agent", $"{existing} {s_supplementValue}");
}
else
{
message.Request.Headers.Set("User-Agent", s_supplementValue);
}
}
private static string CreateSupplementValue()
{
const string Name = "foundry-hosting/agent-framework-dotnet";
if (typeof(HostedAgentUserAgentPolicy).Assembly.GetCustomAttribute()?.InformationalVersion is string version)
{
int pos = version.IndexOf('+');
if (pos >= 0)
{
version = version.Substring(0, pos);
}
if (version.Length > 0)
{
return $"{Name}/{version}";
}
}
return Name;
}
}