mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
6cd81286a9
* dotnet: Add hosted-agent User-Agent supplement to outgoing requests
When an agent runs inside a Foundry Hosted Agent, the outgoing
User-Agent header now includes 'agent-framework-hosted/{version}'
alongside the existing 'MEAI/{version}' segment.
- Add HostedAgentContext with AsyncLocal<string?> property
- MeaiUserAgentPolicy reads the supplement per-call
- AgentFrameworkResponseHandler sets/restores the context
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: update hosted UA format to foundry-hosting/agent-framework-dotnet/{version}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Trying to get UA flowing, no luck yet.
* .NET: Polyfill MEAI OpenAIResponsesChatClient to add hosted-agent User-Agent supplement
When AgentFrameworkResponseHandler resolves an agent (i.e. we are running in a
hosted context), TryApplyUserAgent walks the agent's IChatClient decorator chain
to find MEAI's internal OpenAIResponsesChatClient and reflectively swaps its
inner _responseClient field with a DelegatingResponsesClient wrapper. The
wrapper overrides the public-virtual protocol methods to add a per-call
HostedAgentUserAgentPolicy to the RequestOptions and delegate to the inner
ResponsesClient. The OpenAI SDK's internal streaming overloads bottom out in
calls to the public-virtual non-streaming overloads via virtual dispatch on
this, so streaming is covered without overriding any non-virtual member.
The wrapper accepts any ResponsesClient-derived inner — both the Foundry
ProjectResponsesClient and the native OpenAI ResponsesClient — and preserves
the inner client's full pipeline (Transport, RetryPolicy, NetworkTimeout,
OrganizationId / ProjectId / UserAgentApplicationId, custom policies).
- Add DelegatingResponsesClient + HostedAgentUserAgentPolicy in Microsoft.Agents.AI.Foundry.Hosting.
- Add TryApplyUserAgent next to ApplyOpenTelemetry in FoundryHostingExtensions; wire it into AgentFrameworkResponseHandler.GetAgent for both keyed and default-agent paths.
- Drop earlier-iteration dead code: AddHostedAgentTelemetry extension, HostedUserAgentPolicy class, HostedAgentContext.cs, and the never-called ToRequestOptions helper.
- Revert RequestOptionsExtensions.MeaiUserAgentPolicy to MEAI-only (the supplement is now injected by the polyfill).
- Revert unrelated whitespace change in Agent_Step25_ToolboxServerSideTools sample.
- Tests cover streaming AND non-streaming, retry policy preservation, OrganizationId/ProjectId/UserAgentApplicationId pass-through, idempotency, native OpenAI ResponsesClient, and reflection guards for MEAI/OpenAI shape drift.
* .NET: Address review feedback on hosted-agent User-Agent polyfill
- TryApplyUserAgent: replace silent null-return with ArgumentNullException to match the codebase's convention.
- Add idempotency test (TryApplyUserAgent_CalledTwiceOnSameAgent_DoesNotDoubleWrap) — runs the polyfill twice on the same agent and asserts the wire UA contains exactly one foundry-hosting segment, proving the 'current is DelegatingResponsesClient' guard prevents nested wrapping.
- Add retry-double-append test (Polyfill_RetryWithinCall_DoesNotDuplicateSupplementInUserAgent) — exercises the HostedAgentUserAgentPolicy Contains-guard via a custom retry policy that re-runs the inner pipeline on the same message.
- Replace TryApplyUserAgent_NullAgent_ReturnsNullWithoutThrowing with TryApplyUserAgent_NullAgent_ThrowsArgumentNullException to match the new contract.
* .NET: Drop null check from TryApplyUserAgent and its now-redundant test
The two call sites in AgentFrameworkResponseHandler.GetAgent already null-check the agent before invoking TryApplyUserAgent, so the defensive ArgumentNullException is unreachable. Remove it and the corresponding test.
* .NET: Remove unused Microsoft.Shared.Diagnostics import in ServiceCollectionExtensions
The Throw.IfNull helper from this namespace was used by the now-removed null check in TryApplyUserAgent. Drop the unused import to satisfy IDE0005 in CI's full-project dotnet format run.
---------
Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
116 lines
4.5 KiB
C#
116 lines
4.5 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.ClientModel.Primitives;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Microsoft.Agents.AI.Foundry.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Verifies the per-call <c>MeaiUserAgentPolicy</c> exposed via
|
|
/// <see cref="RequestOptionsExtensions.UserAgentPolicy"/>. The policy is reachable through the
|
|
/// public <see cref="FoundryAgent"/> constructors (which add it to the internally-built
|
|
/// <see cref="Azure.AI.Projects.AIProjectClient"/>'s pipeline), so its behavior is part of the
|
|
/// public API surface.
|
|
/// </summary>
|
|
public sealed class RequestOptionsExtensionsTests
|
|
{
|
|
[Fact]
|
|
public async Task MeaiUserAgentPolicy_AddsMeaiSegment_ToOutgoingRequestAsync()
|
|
{
|
|
// Arrange
|
|
using var handler = new RecordingHandler();
|
|
#pragma warning disable CA5399
|
|
using var httpClient = new HttpClient(handler);
|
|
#pragma warning restore CA5399
|
|
var pipeline = ClientPipeline.Create(
|
|
new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) },
|
|
perCallPolicies: [RequestOptionsExtensions.UserAgentPolicy],
|
|
perTryPolicies: default,
|
|
beforeTransportPolicies: default);
|
|
|
|
// Act
|
|
var message = pipeline.CreateMessage();
|
|
message.Request.Method = "POST";
|
|
message.Request.Uri = new System.Uri("https://example.test/anything");
|
|
await pipeline.SendAsync(message);
|
|
|
|
// Assert
|
|
Assert.Equal(1, handler.Count);
|
|
Assert.NotNull(handler.LastUserAgent);
|
|
Assert.Contains("MEAI/", handler.LastUserAgent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MeaiUserAgentPolicy_DoesNotAddFoundryHostingSegmentAsync()
|
|
{
|
|
// Arrange
|
|
using var handler = new RecordingHandler();
|
|
#pragma warning disable CA5399
|
|
using var httpClient = new HttpClient(handler);
|
|
#pragma warning restore CA5399
|
|
var pipeline = ClientPipeline.Create(
|
|
new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) },
|
|
perCallPolicies: [RequestOptionsExtensions.UserAgentPolicy],
|
|
perTryPolicies: default,
|
|
beforeTransportPolicies: default);
|
|
|
|
// Act
|
|
var message = pipeline.CreateMessage();
|
|
message.Request.Method = "POST";
|
|
message.Request.Uri = new System.Uri("https://example.test/anything");
|
|
await pipeline.SendAsync(message);
|
|
|
|
// Assert: the policy is MEAI-only; the foundry-hosting supplement is added elsewhere
|
|
// (by the polyfill DelegatingResponsesClient → HostedAgentUserAgentPolicy).
|
|
Assert.NotNull(handler.LastUserAgent);
|
|
Assert.DoesNotContain("foundry-hosting/agent-framework-dotnet", handler.LastUserAgent);
|
|
}
|
|
|
|
[Fact]
|
|
public void UserAgentPolicy_ExposesSingletonInstance()
|
|
{
|
|
// Two reads of the static property must return the same instance — the policy is stateless and shared.
|
|
var first = RequestOptionsExtensions.UserAgentPolicy;
|
|
var second = RequestOptionsExtensions.UserAgentPolicy;
|
|
Assert.Same(first, second);
|
|
}
|
|
|
|
[Fact]
|
|
public void MeaiUserAgentPolicy_ValueIncludesAFFoundryAssemblyVersion_ReflectionGuard()
|
|
{
|
|
// The policy emits "MEAI/{Microsoft.Agents.AI.Foundry assembly InformationalVersion}".
|
|
// If the assembly metadata stops being readable, the policy falls back to "MEAI" without a version,
|
|
// which is a measurable telemetry regression.
|
|
var attr = typeof(RequestOptionsExtensions).Assembly
|
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
|
Assert.NotNull(attr);
|
|
Assert.False(string.IsNullOrEmpty(attr!.InformationalVersion));
|
|
}
|
|
|
|
private sealed class RecordingHandler : HttpClientHandler
|
|
{
|
|
public int Count { get; private set; }
|
|
public string? LastUserAgent { get; private set; }
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
this.Count++;
|
|
this.LastUserAgent = request.Headers.TryGetValues("User-Agent", out var values)
|
|
? string.Join(",", values)
|
|
: null;
|
|
|
|
var resp = new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
|
|
RequestMessage = request,
|
|
};
|
|
return Task.FromResult(resp);
|
|
}
|
|
}
|
|
}
|