// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel.Primitives;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Azure.AI.AgentServer.Responses;
using Azure.Core;
using Azure.Identity;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Foundry.Hosting;
///
/// Extension methods for registering agent-framework agents as Foundry Hosted Agents
/// using the Azure AI Responses Server SDK.
///
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public static class FoundryHostingExtensions
{
///
/// Registers the Azure AI Responses Server SDK and
/// as the . Agents are resolved from keyed DI services
/// using the agent.name or metadata["entity_id"] from incoming requests.
///
///
///
/// This method calls AddResponsesServer() internally, so you do not need to
/// call it separately. Register your instances before calling this.
///
///
/// Example:
///
/// builder.Services.AddKeyedSingleton<AIAgent>("my-agent", myAgent);
/// builder.Services.AddFoundryResponses();
///
/// var app = builder.Build();
/// app.MapFoundryResponses();
///
///
///
/// The service collection.
/// The service collection for chaining.
public static IServiceCollection AddFoundryResponses(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddResponsesServer();
services.TryAddSingleton(_ => FileSystemAgentSessionStore.CreateDefault());
services.TryAddSingleton();
return services;
}
///
/// Registers the Azure AI Responses Server SDK and a specific
/// as the handler for all incoming requests, regardless of the agent.name in the request.
///
///
///
/// Use this overload when hosting a single agent. The provided agent instance is
/// registered as both a keyed service and the default .
/// This method calls AddResponsesServer() internally.
///
///
/// Example:
///
/// builder.Services.AddFoundryResponses(myAgent);
///
/// var app = builder.Build();
/// app.MapFoundryResponses();
///
///
///
/// The service collection.
/// The agent instance to register.
/// The agent session store to use for managing agent sessions server-side. If null, a file-system session store is used, rooted at /.checkpoints when running in a Foundry hosted environment and {cwd}/.checkpoints locally.
/// The service collection for chaining.
public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent, AgentSessionStore? agentSessionStore = null)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(agent);
services.AddResponsesServer();
agentSessionStore ??= FileSystemAgentSessionStore.CreateDefault();
if (!string.IsNullOrWhiteSpace(agent.Name))
{
services.TryAddKeyedSingleton(agent.Name, agent);
services.TryAddKeyedSingleton(agent.Name, agentSessionStore);
}
// Also register as the default (non-keyed) agent so requests
// without an agent name can resolve it (e.g., local dev tooling).
services.TryAddSingleton(agent);
services.TryAddSingleton(agentSessionStore);
services.TryAddSingleton();
return services;
}
///
/// Registers the Foundry Toolbox service, which eagerly connects to the Foundry Toolboxes
/// MCP proxy at startup and provides MCP tools to .
///
///
///
/// Each string in is a toolbox name registered in the Foundry
/// project. The proxy URL per toolbox is constructed as:
/// {FOUNDRY_AGENT_TOOLSET_ENDPOINT}/{toolboxName}/mcp?api-version=2025-05-01-preview
///
///
/// When FOUNDRY_AGENT_TOOLSET_ENDPOINT is absent, startup succeeds without error and
/// no tools are loaded (the container remains healthy per spec §2).
///
///
/// Example:
///
/// builder.Services.AddFoundryToolboxes("my-toolbox", "another-toolbox");
///
///
///
/// The service collection.
/// Names of the Foundry toolboxes to connect to.
/// The service collection for chaining.
public static IServiceCollection AddFoundryToolboxes(
this IServiceCollection services,
params string[] toolboxNames)
=> services.AddFoundryToolboxes(configureOptions: null, toolboxNames);
///
/// Registers the Foundry Toolbox service with additional options configuration.
///
/// The service collection.
/// Callback to further configure (e.g. set ).
/// Names of the Foundry toolboxes to pre-register at startup.
/// The service collection for chaining.
public static IServiceCollection AddFoundryToolboxes(
this IServiceCollection services,
Action? configureOptions,
params string[] toolboxNames)
{
ArgumentNullException.ThrowIfNull(services);
services.Configure(opt =>
{
foreach (var name in toolboxNames)
{
if (!string.IsNullOrWhiteSpace(name))
{
opt.ToolboxNames.Add(name);
}
}
configureOptions?.Invoke(opt);
});
// Register DefaultAzureCredential as the default TokenCredential if not already registered
services.TryAddSingleton(_ => new DefaultAzureCredential());
// Register FoundryToolboxService as a singleton so it can be injected into the handler
services.TryAddSingleton();
// AddHostedService uses TryAddEnumerable internally, so calling AddFoundryToolboxes
// multiple times will not invoke StartAsync twice on the same singleton.
services.AddHostedService(sp => sp.GetRequiredService());
return services;
}
///
/// Maps the Responses API routes for the agent-framework handler to the endpoint routing pipeline.
///
/// The endpoint route builder.
/// Optional route prefix (e.g., "/openai/v1"). Default: empty (routes at /responses).
/// The endpoint route builder for chaining.
public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuilder endpoints, string prefix = "")
{
ArgumentNullException.ThrowIfNull(endpoints);
endpoints.MapResponsesServer(prefix);
return endpoints;
}
///
/// The ActivitySource name for the Responses hosting pipeline.
///
private const string ResponsesSourceName = "Azure.AI.AgentServer.Responses";
///
/// Wraps with instrumentation
/// so that agent invocations emit spans into the pipeline registered by
/// Azure.AI.AgentServer.Core's AddAgentHostTelemetry().
/// If the agent is already instrumented the original instance is returned unchanged.
///
internal static AIAgent ApplyOpenTelemetry(AIAgent agent)
{
if (agent.GetService() is not null)
{
return agent;
}
return agent.AsBuilder()
.UseOpenTelemetry(sourceName: ResponsesSourceName)
.Build();
}
///
/// Registers the hosted-agent User-Agent supplement policy
/// () on the agent's underlying chat client via the
/// MEAI 10.5.1 hook so every outgoing OpenAI Responses
/// request carries the segment foundry-hosting/agent-framework-dotnet/{version}.
///
///
///
/// Best-effort and idempotent. The method is a no-op when:
///
/// - exposes no ;
/// - the chat client is not OpenAI-backed (the service lookup returns );
/// - the policy was already registered on this client by a prior invocation (deduped via reflection on OpenAIRequestPolicies._entries).
///
///
///
/// Returns the same instance unchanged. The policy is installed
/// on the chat client; the agent itself is not wrapped.
///
///
internal static AIAgent TryApplyUserAgent(AIAgent agent)
{
var chatClient = agent.GetService();
if (chatClient?.GetService() is { } policies)
{
// Hosted agents are typically singletons resolved per request, so AddPolicy must be
// called at most once per OpenAIRequestPolicies instance to avoid unbounded growth of
// the policy list (each entry adds per-request CPU work even though the User-Agent
// value stays stable). Track which instances we have already wired with a
// ConditionalWeakTable keyed on the OpenAIRequestPolicies reference; the table holds
// weak references so it does not extend the lifetime of the chat client.
if (s_userAgentRegistrations.TryAdd(policies, s_boxedTrue))
{
policies.AddPolicy(HostedAgentUserAgentPolicy.Instance, PipelinePosition.PerCall);
}
}
return agent;
}
private static readonly object s_boxedTrue = new();
private static readonly ConditionalWeakTable s_userAgentRegistrations = new();
}