Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using A2A.AspNetCore;
using A2AServer;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.A2A;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
Expand All @@ -26,10 +25,6 @@

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
var app = builder.Build();

var httpClient = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
var logger = app.Logger;

IConfigurationRoot configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
Expand All @@ -39,7 +34,7 @@
string? apiKey = configuration["OPENAI_API_KEY"];
string model = configuration["OPENAI_CHAT_MODEL_NAME"] ?? "gpt-5.4-mini";
string? endpoint = configuration["AZURE_AI_PROJECT_ENDPOINT"];
string[] agentUrls = (app.Configuration["urls"] ?? "http://localhost:5000").Split(';');
string[] agentUrls = (builder.Configuration["urls"] ?? "http://localhost:5000").Split(';');

var invoiceQueryPlugin = new InvoiceQuery();
IList<AITool> tools =
Expand Down Expand Up @@ -106,9 +101,11 @@ You specialize in handling queries related to logistics.
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentName must be provided");
}

app.MapA2A(
hostA2AAgent,
path: "/", protocolBindings: A2AProtocolBinding.JsonRpc | A2AProtocolBinding.HttpJson);
builder.AddA2AServer(hostA2AAgent);

var app = builder.Build();
app.MapA2AHttpJson(hostA2AAgent, "/");
app.MapA2AJsonRpc(hostA2AAgent, "/");

app.MapWellKnownAgentCard(hostA2AAgentCard);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te
instructions: "you are a dependency inject agent. Tell me all about dependency injection.");
});

pirateAgentBuilder.AddA2AServer();
knightsKnavesAgentBuilder.AddA2AServer();

var app = builder.Build();

app.MapOpenApi();
Expand All @@ -155,9 +158,9 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te
// Configure the HTTP request pipeline.
app.UseExceptionHandler();

// attach a2a with simple message communication
app.MapA2A(pirateAgentBuilder, path: "/a2a/pirate");
app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves");
// Expose A2A servers over HTTP with JSON payloads
app.MapA2AHttpJson(pirateAgentBuilder, path: "/a2a/pirate");
app.MapA2AHttpJson(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves");

app.MapDevUI();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,183 +6,133 @@
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);
ArgumentNullException.ThrowIfNull(agent);
ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent) + "." + nameof(agent.Name));

return endpoints.MapA2A(agentBuilder.Name, path, configureOptions);
return endpoints.MapA2AHttpJson(agent.Name, path);
}

/// <summary>
/// Maps A2A endpoints for the agent with the specified name to the given path.
/// 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>
/// <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)
public static IEndpointConventionBuilder MapA2AHttpJson(this IEndpointRouteBuilder endpoints, string agentName, string path)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentException.ThrowIfNullOrEmpty(agentName);
ArgumentException.ThrowIfNullOrWhiteSpace(path);

var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
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(agent, path, protocolBindings, agentRunMode);
// 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);
}

/// <summary>
/// Maps A2A endpoints for the agent with the specified name to the given path.
/// 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="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="agentBuilder">The configuration builder for the agent.</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)
public static IEndpointConventionBuilder MapA2AJsonRpc(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentException.ThrowIfNullOrEmpty(agentName);

var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
ArgumentNullException.ThrowIfNull(agentBuilder);

return endpoints.MapA2A(agent, path, configureOptions);
return endpoints.MapA2AJsonRpc(agentBuilder.Name, path);
}

/// <summary>
/// Maps A2A endpoints for the specified agent to the given path.
/// 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 to use for A2A protocol integration.</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="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)
public static IEndpointConventionBuilder MapA2AJsonRpc(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
{
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);
ArgumentNullException.ThrowIfNull(agent);
ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent) + "." + nameof(agent.Name));

return endpoints.MapA2AJsonRpc(agent.Name, path);
}

/// <summary>
/// Maps A2A endpoints for the specified agent to the given path.
/// 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="agent">The agent to use for A2A protocol integration.</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, AIAgent agent, string path, Action<A2AHostingOptions>? configureOptions = null)
public static IEndpointConventionBuilder MapA2AJsonRpc(this IEndpointRouteBuilder endpoints, string agentName, string path)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agent);
ArgumentException.ThrowIfNullOrEmpty(agentName);
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);
}

private static A2AServer CreateA2AServer(IEndpointRouteBuilder endpoints, AIAgent agent, A2AHostingOptions? options)
{
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);
}

var loggerFactory = endpoints.ServiceProvider.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
var taskStore = endpoints.ServiceProvider.GetKeyedService<ITaskStore>(agent.Name) ?? new InMemoryTaskStore();

return new A2AServer(
agentHandler,
taskStore,
new ChannelEventNotifier(),
loggerFactory.CreateLogger<A2AServer>(),
options?.ServerOptions);
}

private static IEndpointConventionBuilder MapA2AEndpoints(IEndpointRouteBuilder endpoints, A2AServer a2aServer, string path, A2AProtocolBinding? protocolBindings)
{
protocolBindings ??= A2AProtocolBinding.HttpJson;

IEndpointConventionBuilder? result = null;

if (protocolBindings.Value.HasFlag(A2AProtocolBinding.JsonRpc))
{
result = endpoints.MapA2A(a2aServer, path);
}

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" };

result = endpoints.MapHttpA2A(a2aServer, stubAgentCard, path);
}
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 result ?? throw new InvalidOperationException("At least one A2A protocol binding must be specified.");
return endpoints.MapA2A(a2aServer, path);
}
}
Original file line number Diff line number Diff line change
@@ -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" />
Expand All @@ -13,15 +16,15 @@
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="A2A.AspNetCore" />
</ItemGroup>

<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>
Expand Down
29 changes: 0 additions & 29 deletions dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AProtocolBinding.cs

This file was deleted.

Loading
Loading