// Copyright (c) Microsoft. All rights reserved. using System; using System.Diagnostics.CodeAnalysis; using A2A; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Hosting.A2A; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.DependencyInjection; /// /// Provides extension methods for registering A2A server instances in the dependency injection container. /// [Experimental(DiagnosticIds.Experiments.AIResponseContinuations)] public static class A2AServerServiceCollectionExtensions { /// /// Registers an in the dependency injection container, keyed by the agent name /// specified in the . This method only registers the server; to expose it /// as an HTTP endpoint, call one of the MapA2AHttpJson or MapA2AJsonRpc endpoint mapping /// methods during application startup. /// /// The agent builder whose name identifies the agent. /// An optional callback to configure . /// The for chaining. /// /// /// Trust model. The A2A contextId arrives from the wire /// and is treated as a chain-resume identifier — not as an authorization /// token. The contract carries no principal/owner /// dimension, so when a persistent store is registered any caller who knows or /// guesses another caller's contextId can resume that other caller's /// persisted thread. Hosts that serve more than one user must compose a principal /// dimension into the lookup key — typically by calling /// UseClaimsBasedSessionIsolation(...) from /// Microsoft.Agents.AI.Hosting.AspNetCore (or by registering a custom /// ). When no isolation provider is /// registered, behavior is unchanged — the bare contextId is used as the /// conversation identifier, which is appropriate for first-run / single-user / /// prototyping scenarios but unsafe for multi-user hosts. /// /// public static IHostedAgentBuilder AddA2AServer(this IHostedAgentBuilder agentBuilder, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(agentBuilder); agentBuilder.ServiceCollection.AddA2AServer(agentBuilder.Name, configureOptions); return agentBuilder; } /// /// Registers an in the dependency injection container, keyed by the specified /// agent name. This method only registers the server; to expose it as an HTTP endpoint, call one of the /// MapA2AHttpJson or MapA2AJsonRpc endpoint mapping methods during application startup. /// /// The host application builder to configure. /// The name of the agent to create an A2A server for. /// An optional callback to configure . /// The for chaining. /// /// See the trust-model remarks on /// for guidance on multi-user hosts (the wire contextId is a chain-resume /// identifier, not an authorization token; multi-user hosts must compose a /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom /// ). /// public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, string agentName, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(builder); builder.Services.AddA2AServer(agentName, configureOptions); return builder; } /// /// Registers an in the dependency injection container for the specified /// instance, keyed by the agent's . This method only /// registers the server; to expose it as an HTTP endpoint, call one of the MapA2AHttpJson or /// MapA2AJsonRpc endpoint mapping methods during application startup. /// /// The host application builder to configure. /// The agent instance to create an A2A server for. /// An optional callback to configure . /// The for chaining. /// /// See the trust-model remarks on /// for guidance on multi-user hosts (the wire contextId is a chain-resume /// identifier, not an authorization token; multi-user hosts must compose a /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom /// ). /// public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, AIAgent agent, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(builder); builder.Services.AddA2AServer(agent, configureOptions); return builder; } /// /// Registers an in the dependency injection container, keyed by the specified /// agent name. This method only registers the server; to expose it as an HTTP endpoint, call one of the /// MapA2AHttpJson or MapA2AJsonRpc endpoint mapping methods during application startup. /// /// The service collection to add the A2A server to. /// The name of the agent to create an A2A server for. /// An optional callback to configure . /// The for chaining. /// /// See the trust-model remarks on /// for guidance on multi-user hosts (the wire contextId is a chain-resume /// identifier, not an authorization token; multi-user hosts must compose a /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom /// ). /// public static IServiceCollection AddA2AServer(this IServiceCollection services, string agentName, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(services); ArgumentException.ThrowIfNullOrWhiteSpace(agentName); A2AServerRegistrationOptions? options = null; if (configureOptions is not null) { options = new A2AServerRegistrationOptions(); configureOptions(options); } services.AddKeyedSingleton(agentName, (sp, _) => { var agent = sp.GetRequiredKeyedService(agentName); return CreateA2AServer(sp, agent, options); }); return services; } /// /// Registers an in the dependency injection container for the specified /// instance, keyed by the agent's . This method only /// registers the server; to expose it as an HTTP endpoint, call one of the MapA2AHttpJson or /// MapA2AJsonRpc endpoint mapping methods during application startup. /// /// The service collection to add the A2A server to. /// The agent instance to create an A2A server for. /// An optional callback to configure . /// The for chaining. /// /// See the trust-model remarks on /// for guidance on multi-user hosts (the wire contextId is a chain-resume /// identifier, not an authorization token; multi-user hosts must compose a /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom /// ). /// public static IServiceCollection AddA2AServer(this IServiceCollection services, AIAgent agent, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(agent); ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent) + "." + nameof(agent.Name)); A2AServerRegistrationOptions? options = null; if (configureOptions is not null) { options = new A2AServerRegistrationOptions(); configureOptions(options); } services.AddKeyedSingleton(agent.Name, (sp, _) => CreateA2AServer(sp, agent, options)); return services; } private static A2AServer CreateA2AServer(IServiceProvider serviceProvider, AIAgent agent, A2AServerRegistrationOptions? options) { var agentHandler = serviceProvider.GetKeyedService(agent.Name); if (agentHandler is null) { var agentSessionStore = serviceProvider.GetKeyedService(agent.Name); var runMode = options?.AgentRunMode ?? AgentRunMode.DisallowBackground; // Ensure that we have an IsolationKeyScopedAgentSessionStore registered. var isolationKeyProvider = serviceProvider.GetService(); if (agentSessionStore?.GetService() is null) { agentSessionStore ??= new InMemoryAgentSessionStore(); agentSessionStore = new IsolationKeyScopedAgentSessionStore(agentSessionStore, isolationKeyProvider, new() { Strict = isolationKeyProvider != null }); } var hostAgent = new AIHostAgent( innerAgent: agent, sessionStore: agentSessionStore); agentHandler = new A2AAgentHandler(hostAgent, runMode); } var loggerFactory = serviceProvider.GetService() ?? NullLoggerFactory.Instance; var taskStore = serviceProvider.GetKeyedService(agent.Name) ?? new InMemoryTaskStore(); return new A2AServer( agentHandler, taskStore, new ChannelEventNotifier(), loggerFactory.CreateLogger(), options?.ServerOptions); } }