.NET: Split A2A endpoint mapping into protocol-specific methods (#5413)

* .NET: Refactor A2A hosting registration into A2AServerServiceCollectionExtensions

- Rename A2AHostingOptions to A2AServerRegistrationOptions
- Move server registration logic from A2AEndpointRouteBuilderExtensions
  and AIAgentExtensions into new A2AServerServiceCollectionExtensions
- Remove A2AProtocolBinding and AIAgentExtensions (consolidated)
- Update samples and tests to use the new registration API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address copilot comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
SergeyMenshykh
2026-04-22 10:38:48 +01:00
committed by GitHub
co-authored by Copilot
parent 275363d15e
commit c54483f81e
10 changed files with 746 additions and 891 deletions
@@ -6,183 +6,133 @@ using A2A;
using A2A.AspNetCore;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Hosting.A2A;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.AspNetCore.Builder;
/// <summary>
/// Provides extension methods for configuring A2A endpoints for AI agents.
/// Provides extension methods for mapping A2A protocol endpoints for AI agents.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
public static class A2AEndpointRouteBuilderExtensions
{
/// <summary>
/// Maps A2A endpoints for the specified agent to the given path.
/// Maps A2A HTTP+JSON endpoints for the specified agent to the given path.
/// An <see cref="A2AServer"/> for the agent must be registered first by calling
/// <c>AddA2AServer</c> during service registration.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentBuilder">The configuration builder for <see cref="AIAgent"/>.</param>
/// <param name="agentBuilder">The configuration builder for the agent.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <param name="protocolBindings">The A2A protocol binding(s) to expose. When <see langword="null"/>, defaults to <see cref="A2AProtocolBinding.HttpJson"/>.</param>
/// <param name="agentRunMode">The agent run mode that controls how the agent responds to A2A requests. When <see langword="null"/>, defaults to <see cref="AgentRunMode.DisallowBackground"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, A2AProtocolBinding? protocolBindings, AgentRunMode? agentRunMode = null)
public static IEndpointConventionBuilder MapA2AHttpJson(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path)
{
ArgumentNullException.ThrowIfNull(agentBuilder);
return endpoints.MapA2A(agentBuilder.Name, path, protocolBindings, agentRunMode);
return endpoints.MapA2AHttpJson(agentBuilder.Name, path);
}
/// <summary>
/// Maps A2A endpoints for the specified agent to the given path.
/// Maps A2A HTTP+JSON endpoints for the specified agent to the given path.
/// An <see cref="A2AServer"/> for the agent must be registered first by calling
/// <c>AddA2AServer</c> during service registration.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentBuilder">The configuration builder for <see cref="AIAgent"/>.</param>
/// <param name="agent">The agent whose name identifies the registered A2A server.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <param name="configureOptions">An optional callback to configure <see cref="A2AHostingOptions"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, Action<A2AHostingOptions>? configureOptions = null)
public static IEndpointConventionBuilder MapA2AHttpJson(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
{
ArgumentNullException.ThrowIfNull(agentBuilder);
return endpoints.MapA2A(agentBuilder.Name, path, configureOptions);
}
/// <summary>
/// Maps A2A endpoints for the agent with the specified name to the given path.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <param name="protocolBindings">The A2A protocol binding(s) to expose. When <see langword="null"/>, defaults to <see cref="A2AProtocolBinding.HttpJson"/>.</param>
/// <param name="agentRunMode">The agent run mode that controls how the agent responds to A2A requests. When <see langword="null"/>, defaults to <see cref="AgentRunMode.DisallowBackground"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, A2AProtocolBinding? protocolBindings, AgentRunMode? agentRunMode = null)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentException.ThrowIfNullOrEmpty(agentName);
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
return endpoints.MapA2A(agent, path, protocolBindings, agentRunMode);
}
/// <summary>
/// Maps A2A endpoints for the agent with the specified name to the given path.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <param name="configureOptions">An optional callback to configure <see cref="A2AHostingOptions"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, Action<A2AHostingOptions>? configureOptions = null)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentException.ThrowIfNullOrEmpty(agentName);
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
return endpoints.MapA2A(agent, path, configureOptions);
}
/// <summary>
/// Maps A2A endpoints for the specified agent to the given path.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agent">The agent to use for A2A protocol integration.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <param name="protocolBindings">The A2A protocol binding(s) to expose. When <see langword="null"/>, defaults to <see cref="A2AProtocolBinding.HttpJson"/>.</param>
/// <param name="agentRunMode">The agent run mode that controls how the agent responds to A2A requests. When <see langword="null"/>, defaults to <see cref="AgentRunMode.DisallowBackground"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, A2AProtocolBinding? protocolBindings, AgentRunMode? agentRunMode = null)
{
Action<A2AHostingOptions>? configureOptions = null;
if (protocolBindings is not null || agentRunMode is not null)
{
configureOptions = options =>
{
options.ProtocolBindings = protocolBindings;
options.AgentRunMode = agentRunMode;
};
}
return endpoints.MapA2A(agent, path, configureOptions);
}
/// <summary>
/// Maps A2A endpoints for the specified agent to the given path.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agent">The agent to use for A2A protocol integration.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <param name="configureOptions">An optional callback to configure <see cref="A2AHostingOptions"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action<A2AHostingOptions>? configureOptions = null)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agent);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent) + "." + nameof(agent.Name));
A2AHostingOptions? options = null;
if (configureOptions is not null)
{
options = new A2AHostingOptions();
configureOptions(options);
}
var a2aServer = CreateA2AServer(endpoints, agent, options);
return MapA2AEndpoints(endpoints, a2aServer, path, options?.ProtocolBindings);
return endpoints.MapA2AHttpJson(agent.Name, path);
}
private static A2AServer CreateA2AServer(IEndpointRouteBuilder endpoints, AIAgent agent, A2AHostingOptions? options)
/// <summary>
/// Maps A2A HTTP+JSON endpoints for the agent with the specified name to the given path.
/// An <see cref="A2AServer"/> for the agent must be registered first by calling
/// <c>AddA2AServer</c> during service registration.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2AHttpJson(this IEndpointRouteBuilder endpoints, string agentName, string path)
{
var agentHandler = endpoints.ServiceProvider.GetKeyedService<IAgentHandler>(agent.Name);
if (agentHandler is null)
{
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService<AgentSessionStore>(agent.Name);
agentHandler = agent.MapA2A(agentSessionStore: agentSessionStore, runMode: options?.AgentRunMode);
}
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentException.ThrowIfNullOrEmpty(agentName);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
var loggerFactory = endpoints.ServiceProvider.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
var taskStore = endpoints.ServiceProvider.GetKeyedService<ITaskStore>(agent.Name) ?? new InMemoryTaskStore();
var a2aServer = endpoints.ServiceProvider.GetKeyedService<A2AServer>(agentName)
?? throw new InvalidOperationException(
$"No A2AServer is registered for agent '{agentName}'. " +
$"Call services.AddA2AServer(\"{agentName}\") or agentBuilder.AddA2AServer() during service registration to register one.");
return new A2AServer(
agentHandler,
taskStore,
new ChannelEventNotifier(),
loggerFactory.CreateLogger<A2AServer>(),
options?.ServerOptions);
// TODO: The stub AgentCard is temporary and will be removed once the A2A SDK either removes the
// agentCard parameter of MapHttpA2A or makes it optional. MapHttpA2A exposes the agent card via a
// GET {path}/card endpoint that is not part of the A2A spec, so it is not expected to be consumed
// by any agent - returning a stub agent card here is safe.
var stubAgentCard = new AgentCard { Name = "A2A Agent" };
return endpoints.MapHttpA2A(a2aServer, stubAgentCard, path);
}
private static IEndpointConventionBuilder MapA2AEndpoints(IEndpointRouteBuilder endpoints, A2AServer a2aServer, string path, A2AProtocolBinding? protocolBindings)
/// <summary>
/// Maps A2A JSON-RPC endpoints for the specified agent to the given path.
/// An <see cref="A2AServer"/> for the agent must be registered first by calling
/// <c>AddA2AServer</c> during service registration.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentBuilder">The configuration builder for the agent.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2AJsonRpc(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path)
{
protocolBindings ??= A2AProtocolBinding.HttpJson;
ArgumentNullException.ThrowIfNull(agentBuilder);
IEndpointConventionBuilder? result = null;
return endpoints.MapA2AJsonRpc(agentBuilder.Name, path);
}
if (protocolBindings.Value.HasFlag(A2AProtocolBinding.JsonRpc))
{
result = endpoints.MapA2A(a2aServer, path);
}
/// <summary>
/// Maps A2A JSON-RPC endpoints for the specified agent to the given path.
/// An <see cref="A2AServer"/> for the agent must be registered first by calling
/// <c>AddA2AServer</c> during service registration.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agent">The agent whose name identifies the registered A2A server.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2AJsonRpc(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
{
ArgumentNullException.ThrowIfNull(agent);
ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent) + "." + nameof(agent.Name));
if (protocolBindings.Value.HasFlag(A2AProtocolBinding.HttpJson))
{
// TODO: The stub AgentCard is temporary and will be removed once the A2A SDK either removes the
// agentCard parameter of MapHttpA2A or makes it optional. MapHttpA2A exposes the agent card via a
// GET {path}/card endpoint that is not part of the A2A spec, so it is not expected to be consumed
// by any agent - returning a stub agent card here is safe.
var stubAgentCard = new AgentCard { Name = "A2A Agent" };
return endpoints.MapA2AJsonRpc(agent.Name, path);
}
result = endpoints.MapHttpA2A(a2aServer, stubAgentCard, path);
}
/// <summary>
/// Maps A2A JSON-RPC endpoints for the agent with the specified name to the given path.
/// An <see cref="A2AServer"/> for the agent must be registered first by calling
/// <c>AddA2AServer</c> during service registration.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route path prefix for A2A endpoints.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for further endpoint configuration.</returns>
public static IEndpointConventionBuilder MapA2AJsonRpc(this IEndpointRouteBuilder endpoints, string agentName, string path)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentException.ThrowIfNullOrEmpty(agentName);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
return result ?? throw new InvalidOperationException("At least one A2A protocol binding must be specified.");
var a2aServer = endpoints.ServiceProvider.GetKeyedService<A2AServer>(agentName)
?? throw new InvalidOperationException(
$"No A2AServer is registered for agent '{agentName}'. " +
$"Call services.AddA2AServer(\"{agentName}\") or agentBuilder.AddA2AServer() during service registration to register one.");
return endpoints.MapA2A(a2aServer, path);
}
}
@@ -1,9 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<RootNamespace>Microsoft.Agents.AI.Hosting.A2A.AspNetCore</RootNamespace>
<VersionSuffix>preview</VersionSuffix>
<!-- RT0002: Microsoft.Agents.AI.Hosting.A2A is intentionally referenced as a transitive dependency
so that consumers of this package automatically get the AddA2AServer registration extensions. -->
<NoWarn>$(NoWarn);RT0002</NoWarn>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
@@ -13,7 +16,7 @@
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A.AspNetCore" />
</ItemGroup>
@@ -21,7 +24,7 @@
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Hosting.A2A\Microsoft.Agents.AI.Hosting.A2A.csproj" />
</ItemGroup>
@@ -1,29 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Hosting.A2A;
/// <summary>
/// Specifies which A2A protocol binding(s) to expose when mapping A2A endpoints.
/// </summary>
/// <remarks>
/// This is a flags enum. Combine values using the bitwise OR operator to enable multiple bindings
/// (e.g., <c>A2AProtocolBinding.HttpJson | A2AProtocolBinding.JsonRpc</c>).
/// </remarks>
[Flags]
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
public enum A2AProtocolBinding
{
/// <summary>
/// Expose the agent via the HTTP+JSON/REST protocol binding.
/// </summary>
HttpJson = 1,
/// <summary>
/// Expose the agent via the JSON-RPC protocol binding.
/// </summary>
JsonRpc = 2,
}
@@ -7,21 +7,11 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Hosting.A2A;
/// <summary>
/// Options for configuring A2A endpoint hosting behavior.
/// Options for configuring A2A server registration.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
public sealed class A2AHostingOptions
public sealed class A2AServerRegistrationOptions
{
/// <summary>
/// Gets or sets the A2A protocol binding(s) to expose.
/// </summary>
/// <remarks>
/// When <see langword="null"/>, defaults to <see cref="A2AProtocolBinding.HttpJson"/>.
/// Use the bitwise OR operator to enable multiple bindings
/// (e.g., <c>A2AProtocolBinding.HttpJson | A2AProtocolBinding.JsonRpc</c>).
/// </remarks>
public A2AProtocolBinding? ProtocolBindings { get; set; }
/// <summary>
/// Gets or sets the agent run mode that controls how the agent responds to A2A requests.
/// </summary>
@@ -0,0 +1,160 @@
// 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;
/// <summary>
/// Provides extension methods for registering A2A server instances in the dependency injection container.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
public static class A2AServerServiceCollectionExtensions
{
/// <summary>
/// Registers an <see cref="A2AServer"/> in the dependency injection container, keyed by the agent name
/// specified in the <paramref name="agentBuilder"/>. This method only registers the server; to expose it
/// as an HTTP endpoint, call one of the <c>MapA2AHttpJson</c> or <c>MapA2AJsonRpc</c> endpoint mapping
/// methods during application startup.
/// </summary>
/// <param name="agentBuilder">The agent builder whose name identifies the agent.</param>
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
/// <returns>The <paramref name="agentBuilder"/> for chaining.</returns>
public static IHostedAgentBuilder AddA2AServer(this IHostedAgentBuilder agentBuilder, Action<A2AServerRegistrationOptions>? configureOptions = null)
{
ArgumentNullException.ThrowIfNull(agentBuilder);
agentBuilder.ServiceCollection.AddA2AServer(agentBuilder.Name, configureOptions);
return agentBuilder;
}
/// <summary>
/// Registers an <see cref="A2AServer"/> 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
/// <c>MapA2AHttpJson</c> or <c>MapA2AJsonRpc</c> endpoint mapping methods during application startup.
/// </summary>
/// <param name="builder">The host application builder to configure.</param>
/// <param name="agentName">The name of the agent to create an A2A server for.</param>
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
/// <returns>The <paramref name="builder"/> for chaining.</returns>
public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, string agentName, Action<A2AServerRegistrationOptions>? configureOptions = null)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddA2AServer(agentName, configureOptions);
return builder;
}
/// <summary>
/// Registers an <see cref="A2AServer"/> in the dependency injection container for the specified
/// <see cref="AIAgent"/> instance, keyed by the agent's <see cref="AIAgent.Name"/>. This method only
/// registers the server; to expose it as an HTTP endpoint, call one of the <c>MapA2AHttpJson</c> or
/// <c>MapA2AJsonRpc</c> endpoint mapping methods during application startup.
/// </summary>
/// <param name="builder">The host application builder to configure.</param>
/// <param name="agent">The agent instance to create an A2A server for.</param>
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
/// <returns>The <paramref name="builder"/> for chaining.</returns>
public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, AIAgent agent, Action<A2AServerRegistrationOptions>? configureOptions = null)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddA2AServer(agent, configureOptions);
return builder;
}
/// <summary>
/// Registers an <see cref="A2AServer"/> 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
/// <c>MapA2AHttpJson</c> or <c>MapA2AJsonRpc</c> endpoint mapping methods during application startup.
/// </summary>
/// <param name="services">The service collection to add the A2A server to.</param>
/// <param name="agentName">The name of the agent to create an A2A server for.</param>
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
/// <returns>The <paramref name="services"/> for chaining.</returns>
public static IServiceCollection AddA2AServer(this IServiceCollection services, string agentName, Action<A2AServerRegistrationOptions>? configureOptions = null)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentException.ThrowIfNullOrEmpty(agentName);
A2AServerRegistrationOptions? options = null;
if (configureOptions is not null)
{
options = new A2AServerRegistrationOptions();
configureOptions(options);
}
services.AddKeyedSingleton(agentName, (sp, _) =>
{
var agent = sp.GetRequiredKeyedService<AIAgent>(agentName);
return CreateA2AServer(sp, agent, options);
});
return services;
}
/// <summary>
/// Registers an <see cref="A2AServer"/> in the dependency injection container for the specified
/// <see cref="AIAgent"/> instance, keyed by the agent's <see cref="AIAgent.Name"/>. This method only
/// registers the server; to expose it as an HTTP endpoint, call one of the <c>MapA2AHttpJson</c> or
/// <c>MapA2AJsonRpc</c> endpoint mapping methods during application startup.
/// </summary>
/// <param name="services">The service collection to add the A2A server to.</param>
/// <param name="agent">The agent instance to create an A2A server for.</param>
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
/// <returns>The <paramref name="services"/> for chaining.</returns>
public static IServiceCollection AddA2AServer(this IServiceCollection services, AIAgent agent, Action<A2AServerRegistrationOptions>? 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<IAgentHandler>(agent.Name);
if (agentHandler is null)
{
var agentSessionStore = serviceProvider.GetKeyedService<AgentSessionStore>(agent.Name);
var runMode = options?.AgentRunMode ?? AgentRunMode.DisallowBackground;
var hostAgent = new AIHostAgent(
innerAgent: agent,
sessionStore: agentSessionStore ?? new InMemoryAgentSessionStore());
agentHandler = new A2AAgentHandler(hostAgent, runMode);
}
var loggerFactory = serviceProvider.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
var taskStore = serviceProvider.GetKeyedService<ITaskStore>(agent.Name) ?? new InMemoryTaskStore();
return new A2AServer(
agentHandler,
taskStore,
new ChannelEventNotifier(),
loggerFactory.CreateLogger<A2AServer>(),
options?.ServerOptions);
}
}
@@ -1,40 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using A2A;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Hosting.A2A;
/// <summary>
/// Provides extension methods for attaching A2A (Agent2Agent) messaging capabilities to an <see cref="AIAgent"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
public static class AIAgentExtensions
{
/// <summary>
/// Creates an <see cref="IAgentHandler"/> that bridges the specified <see cref="AIAgent"/> to
/// the A2A (Agent2Agent) protocol.
/// </summary>
/// <param name="agent">Agent to attach A2A messaging processing capabilities to.</param>
/// <param name="agentSessionStore">The store to store session contents and metadata.</param>
/// <param name="runMode">Controls the response behavior of the agent run.</param>
/// <returns>An <see cref="IAgentHandler"/> that handles A2A message execution and cancellation.</returns>
public static IAgentHandler MapA2A(
this AIAgent agent,
AgentSessionStore? agentSessionStore = null,
AgentRunMode? runMode = null)
{
ArgumentNullException.ThrowIfNull(agent);
ArgumentNullException.ThrowIfNull(agent.Name);
runMode ??= AgentRunMode.DisallowBackground;
var hostAgent = new AIHostAgent(
innerAgent: agent,
sessionStore: agentSessionStore ?? new InMemoryAgentSessionStore());
return new A2AAgentHandler(hostAgent, runMode);
}
}