mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Migrate A2A hosting to A2A SDK v1 (#5363)
* .NET: Migrate A2A hosting to A2A SDK v1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * remove unused agent card --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
7417eeb7e6
commit
275363d15e
@@ -153,12 +153,23 @@ internal static class HostAgentFactory
|
||||
|
||||
private static List<AgentInterface> CreateAgentInterfaces(string[] agentUrls)
|
||||
{
|
||||
return agentUrls.Select(url => new AgentInterface
|
||||
List<AgentInterface> agentInterfaces = [];
|
||||
|
||||
agentInterfaces.AddRange(agentUrls.Select(url => new AgentInterface
|
||||
{
|
||||
Url = url,
|
||||
ProtocolBinding = "JSONRPC",
|
||||
ProtocolVersion = "1.0",
|
||||
}).ToList();
|
||||
}));
|
||||
|
||||
agentInterfaces.AddRange(agentUrls.Select(url => new AgentInterface
|
||||
{
|
||||
Url = url,
|
||||
ProtocolBinding = "HTTP+JSON",
|
||||
ProtocolVersion = "1.0",
|
||||
}));
|
||||
|
||||
return agentInterfaces;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using A2A;
|
||||
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;
|
||||
@@ -107,7 +108,8 @@ else
|
||||
|
||||
app.MapA2A(
|
||||
hostA2AAgent,
|
||||
path: "/",
|
||||
agentCard: hostA2AAgentCard);
|
||||
path: "/", protocolBindings: A2AProtocolBinding.JsonRpc | A2AProtocolBinding.HttpJson);
|
||||
|
||||
app.MapWellKnownAgentCard(hostA2AAgentCard);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using A2A;
|
||||
using A2A.AspNetCore;
|
||||
using AgentWebChat.AgentHost;
|
||||
using AgentWebChat.AgentHost.Custom;
|
||||
@@ -156,15 +157,7 @@ app.UseExceptionHandler();
|
||||
|
||||
// attach a2a with simple message communication
|
||||
app.MapA2A(pirateAgentBuilder, path: "/a2a/pirate");
|
||||
app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard: new()
|
||||
{
|
||||
Name = "Knights and Knaves",
|
||||
Description = "An agent that helps you solve the knights and knaves puzzle.",
|
||||
Version = "1.0",
|
||||
|
||||
// Url can be not set, and SDK will help assign it.
|
||||
// Url = "http://localhost:5390/a2a/knights-and-knaves"
|
||||
});
|
||||
app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves");
|
||||
|
||||
app.MapDevUI();
|
||||
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
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.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public static class A2AEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <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="agentBuilder">The configuration builder for <see cref="AIAgent"/>.</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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agentBuilder);
|
||||
|
||||
return endpoints.MapA2A(agentBuilder.Name, path, protocolBindings, agentRunMode);
|
||||
}
|
||||
|
||||
/// <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="agentBuilder">The configuration builder for <see cref="AIAgent"/>.</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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return result ?? throw new InvalidOperationException("At least one A2A protocol binding must be specified.");
|
||||
}
|
||||
}
|
||||
-385
@@ -1,385 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using A2A;
|
||||
using A2A.AspNetCore;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Hosting.A2A;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring A2A (Agent2Agent) communication in a host application builder.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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="path">The route group to use for A2A endpoints.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path)
|
||||
=> endpoints.MapA2A(agentBuilder, path, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agentBuilder);
|
||||
return endpoints.MapA2A(agentBuilder.Name, path, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
|
||||
=> endpoints.MapA2A(agentName, path, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, _ => { }, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, Action<ITaskManager> configureTaskManager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agentBuilder);
|
||||
return endpoints.MapA2A(agentBuilder.Name, path, configureTaskManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, Action<ITaskManager> configureTaskManager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, configureTaskManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard)
|
||||
=> endpoints.MapA2A(agentBuilder, path, agentCard, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
|
||||
=> endpoints.MapA2A(agentName, path, agentCard, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agentBuilder);
|
||||
return endpoints.MapA2A(agentBuilder.Name, path, agentCard, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, agentCard, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agentBuilder);
|
||||
return endpoints.MapA2A(agentBuilder.Name, path, agentCard, configureTaskManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
|
||||
=> endpoints.MapA2A(agentName, path, agentCard, configureTaskManager, AgentRunMode.DisallowBackground);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, agentCard, configureTaskManager, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
|
||||
=> endpoints.MapA2A(agent, path, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentRunMode agentRunMode)
|
||||
=> endpoints.MapA2A(agent, path, _ => { }, agentRunMode);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action<ITaskManager> configureTaskManager)
|
||||
=> endpoints.MapA2A(agent, path, configureTaskManager, AgentRunMode.DisallowBackground);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action<ITaskManager> configureTaskManager, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService<AgentSessionStore>(agent.Name);
|
||||
var taskManager = agent.MapA2A(loggerFactory: loggerFactory, agentSessionStore: agentSessionStore, runMode: agentRunMode);
|
||||
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
|
||||
|
||||
configureTaskManager(taskManager);
|
||||
return endpointConventionBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
|
||||
=> endpoints.MapA2A(agent, path, agentCard, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, AgentRunMode agentRunMode)
|
||||
=> endpoints.MapA2A(agent, path, agentCard, _ => { }, agentRunMode);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
|
||||
=> endpoints.MapA2A(agent, path, agentCard, configureTaskManager, AgentRunMode.DisallowBackground);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </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 group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService<AgentSessionStore>(agent.Name);
|
||||
var taskManager = agent.MapA2A(agentCard: agentCard, agentSessionStore: agentSessionStore, loggerFactory: loggerFactory, runMode: agentRunMode);
|
||||
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
|
||||
|
||||
configureTaskManager(taskManager);
|
||||
|
||||
return endpointConventionBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps HTTP A2A communication endpoints to the specified path using the provided TaskManager.
|
||||
/// TaskManager should be preconfigured before calling this method.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="taskManager">Pre-configured A2A TaskManager to use for A2A endpoints handling.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, ITaskManager taskManager, string path)
|
||||
{
|
||||
// note: current SDK version registers multiple `.well-known/agent.json` handlers here.
|
||||
// it makes app return HTTP 500, but will be fixed once new A2A SDK is released.
|
||||
// see https://github.com/microsoft/agent-framework/issues/476 for details
|
||||
A2ARouteBuilderExtensions.MapA2A(endpoints, taskManager, path);
|
||||
return endpoints.MapHttpA2A(taskManager, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IAgentHandler"/> implementation that bridges an <see cref="AIAgent"/> to the
|
||||
/// A2A (Agent2Agent) protocol. Handles message execution and cancellation by delegating to
|
||||
/// the underlying agent and translating responses into A2A events.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
internal sealed class A2AAgentHandler : IAgentHandler
|
||||
{
|
||||
private readonly AIHostAgent _hostAgent;
|
||||
private readonly AgentRunMode _runMode;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="A2AAgentHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hostAgent">The hosted agent that provides the execution logic.</param>
|
||||
/// <param name="runMode">Controls whether the agent runs in background mode.</param>
|
||||
public A2AAgentHandler(
|
||||
AIHostAgent hostAgent,
|
||||
AgentRunMode runMode)
|
||||
{
|
||||
this._hostAgent = hostAgent;
|
||||
this._runMode = runMode;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task ExecuteAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.IsContinuation)
|
||||
{
|
||||
return this.HandleTaskUpdateAsync(context, eventQueue, cancellationToken);
|
||||
}
|
||||
|
||||
return this.HandleNewMessageAsync(context, eventQueue, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task CancelAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken)
|
||||
{
|
||||
var taskUpdater = new TaskUpdater(eventQueue, context.TaskId, context.ContextId);
|
||||
await taskUpdater.CancelAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleNewMessageAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken)
|
||||
{
|
||||
var contextId = context.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var session = await this._hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// AIAgent does not support resuming from arbitrary prior tasks.
|
||||
// Throw explicitly so the client gets a clear error rather than a response
|
||||
// that silently ignores the referenced task context.
|
||||
if (context.Message?.ReferenceTaskIds is { Count: > 0 })
|
||||
{
|
||||
throw new NotSupportedException("ReferenceTaskIds is not supported. AIAgent cannot resume from arbitrary prior task context.");
|
||||
}
|
||||
|
||||
List<ChatMessage> chatMessages = context.Message is not null ? [context.Message.ToChatMessage()] : [];
|
||||
|
||||
// Decide whether to run in background based on user preferences and agent capabilities
|
||||
var decisionContext = new A2ARunDecisionContext(context);
|
||||
var allowBackgroundResponses = await this._runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var options = context.Metadata is not { Count: > 0 }
|
||||
? new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses }
|
||||
: new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = context.Metadata.ToAdditionalProperties() };
|
||||
|
||||
var response = await this._hostAgent.RunAsync(
|
||||
chatMessages,
|
||||
session: session,
|
||||
options: options,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this._hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.ContinuationToken is null)
|
||||
{
|
||||
// Return a lightweight message response (no task lifecycle needed).
|
||||
var message = CreateMessageFromResponse(contextId, response);
|
||||
await eventQueue.EnqueueMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Long-running operation: emit task lifecycle events.
|
||||
var taskUpdater = new TaskUpdater(eventQueue, context.TaskId, contextId);
|
||||
await taskUpdater.SubmitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Message? progressMessage = response.Messages.Count > 0
|
||||
? CreateMessageFromResponse(contextId, response)
|
||||
: null;
|
||||
|
||||
await taskUpdater.StartWorkAsync(progressMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken)
|
||||
{
|
||||
var contextId = context.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var session = await this._hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
List<ChatMessage> chatMessages = ExtractChatMessagesFromTaskHistory(context.Task);
|
||||
|
||||
var decisionContext = new A2ARunDecisionContext(context);
|
||||
var allowBackgroundResponses = await this._runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var options = context.Metadata is not { Count: > 0 }
|
||||
? new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses }
|
||||
: new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = context.Metadata.ToAdditionalProperties() };
|
||||
|
||||
AgentResponse response;
|
||||
try
|
||||
{
|
||||
response = await this._hostAgent.RunAsync(
|
||||
chatMessages,
|
||||
session: session,
|
||||
options: options,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
var failUpdater = new TaskUpdater(eventQueue, context.TaskId, contextId);
|
||||
await failUpdater.FailAsync(message: null, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
await this._hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.ContinuationToken is null)
|
||||
{
|
||||
// Complete the task with an artifact containing the response.
|
||||
var taskUpdater = new TaskUpdater(eventQueue, context.TaskId, contextId);
|
||||
await taskUpdater.AddArtifactAsync(response.Messages.ToParts(), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await taskUpdater.CompleteAsync(message: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Still working: emit progress status.
|
||||
var taskUpdater = new TaskUpdater(eventQueue, context.TaskId, contextId);
|
||||
|
||||
Message? progressMessage = response.Messages.Count > 0
|
||||
? CreateMessageFromResponse(contextId, response)
|
||||
: null;
|
||||
|
||||
await taskUpdater.StartWorkAsync(progressMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static Message CreateMessageFromResponse(string contextId, AgentResponse response) =>
|
||||
new()
|
||||
{
|
||||
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
|
||||
ContextId = contextId,
|
||||
Role = Role.Agent,
|
||||
Parts = response.Messages.ToParts(),
|
||||
Metadata = response.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
|
||||
private static List<ChatMessage> ExtractChatMessagesFromTaskHistory(AgentTask? agentTask)
|
||||
{
|
||||
if (agentTask?.History is not { Count: > 0 })
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var chatMessages = new List<ChatMessage>(agentTask.History.Count);
|
||||
foreach (var message in agentTask.History)
|
||||
{
|
||||
chatMessages.Add(message.ToChatMessage());
|
||||
}
|
||||
|
||||
return chatMessages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using A2A;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring A2A endpoint hosting behavior.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public sealed class A2AHostingOptions
|
||||
{
|
||||
/// <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>
|
||||
/// <remarks>
|
||||
/// When <see langword="null"/>, defaults to <see cref="AgentRunMode.DisallowBackground"/>.
|
||||
/// </remarks>
|
||||
public AgentRunMode? AgentRunMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the A2A server options used to configure the underlying <see cref="A2AServer"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="null"/>, no custom server options are applied.
|
||||
/// </remarks>
|
||||
public A2AServerOptions? ServerOptions { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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,
|
||||
}
|
||||
@@ -9,13 +9,13 @@ namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
/// </summary>
|
||||
public sealed class A2ARunDecisionContext
|
||||
{
|
||||
internal A2ARunDecisionContext(MessageSendParams messageSendParams)
|
||||
internal A2ARunDecisionContext(RequestContext requestContext)
|
||||
{
|
||||
this.MessageSendParams = messageSendParams;
|
||||
this.RequestContext = requestContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters of the incoming A2A message that triggered this run.
|
||||
/// Gets the request context of the incoming A2A request that triggered this run.
|
||||
/// </summary>
|
||||
public MessageSendParams MessageSendParams { get; }
|
||||
public RequestContext RequestContext { get; }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
@@ -20,27 +13,18 @@ namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public static class AIAgentExtensions
|
||||
{
|
||||
// Metadata key used to store continuation tokens for long-running background operations
|
||||
// in the AgentTask.Metadata dictionary, persisted by the task store.
|
||||
private const string ContinuationTokenMetadataKey = "__a2a__continuationToken";
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) messaging capabilities via Message processing to the specified <see cref="AIAgent"/>.
|
||||
/// 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="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
|
||||
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</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>
|
||||
/// <param name="jsonSerializerOptions">Optional <see cref="JsonSerializerOptions"/> for serializing and deserializing continuation tokens. Use this when the agent's continuation token contains custom types not registered in the default options. Falls back to <see cref="A2AHostingJsonUtilities.DefaultOptions"/> if not provided.</param>
|
||||
/// <returns>The configured <see cref="TaskManager"/>.</returns>
|
||||
public static ITaskManager MapA2A(
|
||||
/// <returns>An <see cref="IAgentHandler"/> that handles A2A message execution and cancellation.</returns>
|
||||
public static IAgentHandler MapA2A(
|
||||
this AIAgent agent,
|
||||
ITaskManager? taskManager = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
AgentSessionStore? agentSessionStore = null,
|
||||
AgentRunMode? runMode = null,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
AgentRunMode? runMode = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
ArgumentNullException.ThrowIfNull(agent.Name);
|
||||
@@ -49,261 +33,8 @@ public static class AIAgentExtensions
|
||||
|
||||
var hostAgent = new AIHostAgent(
|
||||
innerAgent: agent,
|
||||
sessionStore: agentSessionStore ?? new NoopAgentSessionStore());
|
||||
sessionStore: agentSessionStore ?? new InMemoryAgentSessionStore());
|
||||
|
||||
taskManager ??= new TaskManager();
|
||||
|
||||
// Resolve the JSON serializer options for continuation token serialization. May be custom for the user's agent.
|
||||
JsonSerializerOptions continuationTokenJsonOptions = jsonSerializerOptions ?? A2AHostingJsonUtilities.DefaultOptions;
|
||||
|
||||
// OnMessageReceived handles both message-only and task-based flows.
|
||||
// The A2A SDK prioritizes OnMessageReceived over OnTaskCreated when both are set,
|
||||
// so we consolidate all initial message handling here and return either
|
||||
// an AgentMessage or AgentTask depending on the agent response.
|
||||
// When the agent returns a ContinuationToken (long-running operation), a task is
|
||||
// created for stateful tracking. Otherwise a lightweight AgentMessage is returned.
|
||||
// See https://github.com/a2aproject/a2a-dotnet/issues/275
|
||||
taskManager.OnMessageReceived += (p, ct) => OnMessageReceivedAsync(p, hostAgent, runMode, taskManager, continuationTokenJsonOptions, ct);
|
||||
|
||||
// Task flow for subsequent updates and cancellations
|
||||
taskManager.OnTaskUpdated += (t, ct) => OnTaskUpdatedAsync(t, hostAgent, taskManager, continuationTokenJsonOptions, ct);
|
||||
taskManager.OnTaskCancelled += OnTaskCancelledAsync;
|
||||
|
||||
return taskManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) messaging capabilities via Message processing to the specified <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="agent">Agent to attach A2A messaging processing capabilities to.</param>
|
||||
/// <param name="agentCard">The agent card to return on query.</param>
|
||||
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
|
||||
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</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>
|
||||
/// <param name="jsonSerializerOptions">Optional <see cref="JsonSerializerOptions"/> for serializing and deserializing continuation tokens. Use this when the agent's continuation token contains custom types not registered in the default options. Falls back to <see cref="A2AHostingJsonUtilities.DefaultOptions"/> if not provided.</param>
|
||||
/// <returns>The configured <see cref="TaskManager"/>.</returns>
|
||||
public static ITaskManager MapA2A(
|
||||
this AIAgent agent,
|
||||
AgentCard agentCard,
|
||||
ITaskManager? taskManager = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
AgentSessionStore? agentSessionStore = null,
|
||||
AgentRunMode? runMode = null,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
taskManager = agent.MapA2A(taskManager, loggerFactory, agentSessionStore, runMode, jsonSerializerOptions);
|
||||
|
||||
taskManager.OnAgentCardQuery += (context, query) =>
|
||||
{
|
||||
// A2A SDK assigns the url on its own
|
||||
// we can help user if they did not set Url explicitly.
|
||||
if (string.IsNullOrEmpty(agentCard.Url))
|
||||
{
|
||||
agentCard.Url = context.TrimEnd('/');
|
||||
}
|
||||
|
||||
return Task.FromResult(agentCard);
|
||||
};
|
||||
return taskManager;
|
||||
}
|
||||
|
||||
private static async Task<A2AResponse> OnMessageReceivedAsync(
|
||||
MessageSendParams messageSendParams,
|
||||
AIHostAgent hostAgent,
|
||||
AgentRunMode runMode,
|
||||
ITaskManager taskManager,
|
||||
JsonSerializerOptions continuationTokenJsonOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// AIAgent does not support resuming from arbitrary prior tasks.
|
||||
// Throw explicitly so the client gets a clear error rather than a response
|
||||
// that silently ignores the referenced task context.
|
||||
// Follow-ups on the *same* task are handled via OnTaskUpdated instead.
|
||||
if (messageSendParams.Message.ReferenceTaskIds is { Count: > 0 })
|
||||
{
|
||||
throw new NotSupportedException("ReferenceTaskIds is not supported. AIAgent cannot resume from arbitrary prior task context. Use OnTaskUpdated for follow-ups on the same task.");
|
||||
}
|
||||
|
||||
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var session = await hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Decide whether to run in background based on user preferences and agent capabilities
|
||||
var decisionContext = new A2ARunDecisionContext(messageSendParams);
|
||||
var allowBackgroundResponses = await runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var options = messageSendParams.Metadata is not { Count: > 0 }
|
||||
? new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses }
|
||||
: new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = messageSendParams.Metadata.ToAdditionalProperties() };
|
||||
|
||||
var response = await hostAgent.RunAsync(
|
||||
messageSendParams.ToChatMessages(),
|
||||
session: session,
|
||||
options: options,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.ContinuationToken is null)
|
||||
{
|
||||
return CreateMessageFromResponse(contextId, response);
|
||||
}
|
||||
|
||||
var agentTask = await InitializeTaskAsync(contextId, messageSendParams.Message, taskManager, cancellationToken).ConfigureAwait(false);
|
||||
StoreContinuationToken(agentTask, response.ContinuationToken, continuationTokenJsonOptions);
|
||||
await TransitionToWorkingAsync(agentTask.Id, contextId, response, taskManager, cancellationToken).ConfigureAwait(false);
|
||||
return agentTask;
|
||||
}
|
||||
|
||||
private static async Task OnTaskUpdatedAsync(
|
||||
AgentTask agentTask,
|
||||
AIHostAgent hostAgent,
|
||||
ITaskManager taskManager,
|
||||
JsonSerializerOptions continuationTokenJsonOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var contextId = agentTask.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var session = await hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
// Discard any stale continuation token — the incoming user message supersedes
|
||||
// any previous background operation. AF agents don't support updating existing
|
||||
// background responses (long-running operations); we start a fresh run from the
|
||||
// existing session using the full chat history (which includes the new message).
|
||||
agentTask.Metadata?.Remove(ContinuationTokenMetadataKey);
|
||||
|
||||
await taskManager.UpdateStatusAsync(agentTask.Id, TaskState.Working, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = await hostAgent.RunAsync(
|
||||
ExtractChatMessagesFromTaskHistory(agentTask),
|
||||
session: session,
|
||||
options: new AgentRunOptions { AllowBackgroundResponses = true },
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.ContinuationToken is not null)
|
||||
{
|
||||
StoreContinuationToken(agentTask, response.ContinuationToken, continuationTokenJsonOptions);
|
||||
await TransitionToWorkingAsync(agentTask.Id, contextId, response, taskManager, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CompleteWithArtifactAsync(agentTask.Id, response, taskManager, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await taskManager.UpdateStatusAsync(
|
||||
agentTask.Id,
|
||||
TaskState.Failed,
|
||||
final: true,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static Task OnTaskCancelledAsync(AgentTask agentTask, CancellationToken cancellationToken)
|
||||
{
|
||||
// Remove the continuation token from metadata if present.
|
||||
// The task has already been marked as cancelled by the TaskManager.
|
||||
agentTask.Metadata?.Remove(ContinuationTokenMetadataKey);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static AgentMessage CreateMessageFromResponse(string contextId, AgentResponse response) =>
|
||||
new()
|
||||
{
|
||||
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
|
||||
ContextId = contextId,
|
||||
Role = MessageRole.Agent,
|
||||
Parts = response.Messages.ToParts(),
|
||||
Metadata = response.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
|
||||
// Task outputs should be returned as artifacts rather than messages:
|
||||
// https://a2a-protocol.org/latest/specification/#37-messages-and-artifacts
|
||||
private static Artifact CreateArtifactFromResponse(AgentResponse response) =>
|
||||
new()
|
||||
{
|
||||
ArtifactId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
|
||||
Parts = response.Messages.ToParts(),
|
||||
Metadata = response.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
|
||||
private static async Task<AgentTask> InitializeTaskAsync(
|
||||
string contextId,
|
||||
AgentMessage originalMessage,
|
||||
ITaskManager taskManager,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
AgentTask agentTask = await taskManager.CreateTaskAsync(contextId, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Add the original user message to the task history.
|
||||
// The A2A SDK does this internally when it creates tasks via OnTaskCreated.
|
||||
agentTask.History ??= [];
|
||||
agentTask.History.Add(originalMessage);
|
||||
|
||||
// Notify subscribers of the Submitted state per the A2A spec: https://a2a-protocol.org/latest/specification/#413-taskstate
|
||||
await taskManager.UpdateStatusAsync(agentTask.Id, TaskState.Submitted, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return agentTask;
|
||||
}
|
||||
|
||||
private static void StoreContinuationToken(
|
||||
AgentTask agentTask,
|
||||
ResponseContinuationToken token,
|
||||
JsonSerializerOptions continuationTokenJsonOptions)
|
||||
{
|
||||
// Serialize the continuation token into the task's metadata so it survives
|
||||
// across requests and is cleaned up with the task itself.
|
||||
agentTask.Metadata ??= [];
|
||||
agentTask.Metadata[ContinuationTokenMetadataKey] = JsonSerializer.SerializeToElement(
|
||||
token,
|
||||
continuationTokenJsonOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
|
||||
}
|
||||
|
||||
private static async Task TransitionToWorkingAsync(
|
||||
string taskId,
|
||||
string contextId,
|
||||
AgentResponse response,
|
||||
ITaskManager taskManager,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Include any intermediate progress messages from the response as a status message.
|
||||
AgentMessage? progressMessage = response.Messages.Count > 0 ? CreateMessageFromResponse(contextId, response) : null;
|
||||
await taskManager.UpdateStatusAsync(taskId, TaskState.Working, message: progressMessage, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task CompleteWithArtifactAsync(
|
||||
string taskId,
|
||||
AgentResponse response,
|
||||
ITaskManager taskManager,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var artifact = CreateArtifactFromResponse(response);
|
||||
await taskManager.ReturnArtifactAsync(taskId, artifact, cancellationToken).ConfigureAwait(false);
|
||||
await taskManager.UpdateStatusAsync(taskId, TaskState.Completed, final: true, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static List<ChatMessage> ExtractChatMessagesFromTaskHistory(AgentTask agentTask)
|
||||
{
|
||||
if (agentTask.History is not { Count: > 0 })
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var chatMessages = new List<ChatMessage>(agentTask.History.Count);
|
||||
foreach (var message in agentTask.History)
|
||||
{
|
||||
chatMessages.Add(message.ToChatMessage());
|
||||
}
|
||||
|
||||
return chatMessages;
|
||||
return new A2AAgentHandler(hostAgent, runMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class AgentRunMode : IEquatable<AgentRunMode>
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dissallows the background responses from the agent. Is equivalent to configuring <see cref="AgentRunOptions.AllowBackgroundResponses"/> as <c>false</c>.
|
||||
/// Disallows the background responses from the agent. Is equivalent to configuring <see cref="AgentRunOptions.AllowBackgroundResponses"/> as <c>false</c>.
|
||||
/// In the A2A protocol terminology will make responses be returned as <c>AgentMessage</c>.
|
||||
/// </summary>
|
||||
public static AgentRunMode DisallowBackground => new(MessageValue);
|
||||
@@ -79,7 +79,7 @@ public sealed class AgentRunMode : IEquatable<AgentRunMode>
|
||||
}
|
||||
|
||||
// No delegate provided — fall back to "message" behavior.
|
||||
return ValueTask.FromResult(true);
|
||||
return ValueTask.FromResult(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -31,21 +31,21 @@ internal static class MessageConverter
|
||||
return parts;
|
||||
}
|
||||
/// <summary>
|
||||
/// Converts A2A MessageSendParams to a collection of Microsoft.Extensions.AI ChatMessage objects.
|
||||
/// Converts A2A SendMessageRequest to a collection of Microsoft.Extensions.AI ChatMessage objects.
|
||||
/// </summary>
|
||||
/// <param name="messageSendParams">The A2A message send parameters to convert.</param>
|
||||
/// <param name="sendMessageRequest">The A2A send message request to convert.</param>
|
||||
/// <returns>A read-only collection of ChatMessage objects.</returns>
|
||||
public static List<ChatMessage> ToChatMessages(this MessageSendParams messageSendParams)
|
||||
public static List<ChatMessage> ToChatMessages(this SendMessageRequest sendMessageRequest)
|
||||
{
|
||||
if (messageSendParams is null)
|
||||
if (sendMessageRequest is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var result = new List<ChatMessage>();
|
||||
if (messageSendParams.Message?.Parts is not null)
|
||||
if (sendMessageRequest.Message?.Parts is not null)
|
||||
{
|
||||
result.Add(messageSendParams.Message.ToChatMessage());
|
||||
result.Add(sendMessageRequest.Message.ToChatMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
+327
-191
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -10,9 +9,9 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions.MapA2A method.
|
||||
/// Tests for A2AEndpointRouteBuilderExtensions.MapA2A method.
|
||||
/// </summary>
|
||||
public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
public sealed class A2AEndpointRouteBuilderExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A throws ArgumentNullException for null endpoints.
|
||||
@@ -57,7 +56,7 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder correctly maps the agent with default task manager configuration.
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder correctly maps the agent with default configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_DefaultConfiguration_Succeeds()
|
||||
@@ -73,14 +72,13 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a");
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder and custom task manager configuration succeeds.
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder and custom A2AHostingOptions succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_CustomTaskManagerConfiguration_Succeeds()
|
||||
public void MapA2A_WithAgentBuilder_CustomA2AHostingOptionsConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
@@ -91,61 +89,8 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", taskManager => { });
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", options => { });
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder and agent card succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_WithAgentCard_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A test agent for A2A communication"
|
||||
};
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", agentCard);
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder, agent card, and custom task manager configuration succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_WithAgentCardAndCustomConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A test agent for A2A communication"
|
||||
};
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", agentCard, taskManager => { });
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -181,14 +126,13 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A("agent", "/a2a");
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with string agent name and custom task manager configuration succeeds.
|
||||
/// Verifies that MapA2A with string agent name and custom A2AHostingOptions succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_CustomTaskManagerConfiguration_Succeeds()
|
||||
public void MapA2A_WithAgentName_CustomA2AHostingOptionsConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
@@ -199,61 +143,8 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A("agent", "/a2a", taskManager => { });
|
||||
var result = app.MapA2A("agent", "/a2a", options => { });
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with string agent name and agent card succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_WithAgentCard_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A test agent for A2A communication"
|
||||
};
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A("agent", "/a2a", agentCard);
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with string agent name, agent card, and custom task manager configuration succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_WithAgentCardAndCustomConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A test agent for A2A communication"
|
||||
};
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A("agent", "/a2a", agentCard, taskManager => { });
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -290,14 +181,13 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agent, "/a2a");
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with AIAgent and custom task manager configuration succeeds.
|
||||
/// Verifies that MapA2A with AIAgent and custom A2AHostingOptions succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAIAgent_CustomTaskManagerConfiguration_Succeeds()
|
||||
public void MapA2A_WithAIAgent_CustomA2AHostingOptionsConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
@@ -309,16 +199,34 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agent, "/a2a", taskManager => { });
|
||||
var result = app.MapA2A(agent, "/a2a", options => { });
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with AIAgent and agent card succeeds.
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder and A2AHostingOptions with AgentRunMode succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAIAgent_WithAgentCard_Succeeds()
|
||||
public void MapA2A_WithAgentBuilder_CustomOptionsAndRunMode_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", options => options.AgentRunMode = AgentRunMode.DisallowBackground);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with string agentName and A2AHostingOptions with AgentRunMode succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_CustomOptionsAndRunMode_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
@@ -327,62 +235,10 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A test agent for A2A communication"
|
||||
};
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agent, "/a2a", agentCard);
|
||||
var result = app.MapA2A("agent", "/a2a", options => options.AgentRunMode = AgentRunMode.DisallowBackground);
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with AIAgent, agent card, and custom task manager configuration succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAIAgent_WithAgentCardAndCustomConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A test agent for A2A communication"
|
||||
};
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agent, "/a2a", agentCard, taskManager => { });
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A throws ArgumentNullException for null endpoints when using ITaskManager.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithTaskManager_NullEndpoints_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
|
||||
ITaskManager taskManager = null!;
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
endpoints.MapA2A(taskManager, "/a2a"));
|
||||
|
||||
Assert.Equal("endpoints", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -426,10 +282,10 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that task manager configuration callback is invoked correctly.
|
||||
/// Verifies that A2AHostingOptions configuration callback is invoked correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_TaskManagerConfigurationCallbackInvoked()
|
||||
public void MapA2A_WithAgentBuilder_A2AHostingOptionsConfigurationCallbackInvoked()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
@@ -442,10 +298,10 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
bool configureCallbackInvoked = false;
|
||||
|
||||
// Act
|
||||
app.MapA2A(agentBuilder, "/a2a", taskManager =>
|
||||
app.MapA2A(agentBuilder, "/a2a", options =>
|
||||
{
|
||||
configureCallbackInvoked = true;
|
||||
Assert.NotNull(taskManager);
|
||||
Assert.NotNull(options);
|
||||
});
|
||||
|
||||
// Assert
|
||||
@@ -453,10 +309,10 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that agent card with all properties is accepted.
|
||||
/// Verifies that MapA2A with JsonRpc protocolBindings succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_FullAgentCard_Succeeds()
|
||||
public void MapA2A_WithJsonRpcProtocol_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
@@ -466,14 +322,294 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A comprehensive test agent"
|
||||
};
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", agentCard);
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", options => options.ProtocolBindings = A2AProtocolBinding.JsonRpc);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with both protocols succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithBothProtocols_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", options => options.ProtocolBindings = A2AProtocolBinding.HttpJson | A2AProtocolBinding.JsonRpc);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder and direct protocolBindings parameter succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_DirectProtocol_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", A2AProtocolBinding.HttpJson);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder and direct protocolBindings and run mode parameters succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_DirectProtocolAndRunMode_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", A2AProtocolBinding.HttpJson, AgentRunMode.AllowBackgroundIfSupported);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with IHostedAgentBuilder, null protocolBindings, and direct run mode parameter succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentBuilder_NullProtocolAndDirectRunMode_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", protocolBindings: null, agentRunMode: AgentRunMode.DisallowBackground);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with string agent name and direct protocolBindings parameter succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_DirectProtocol_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A("agent", "/a2a", A2AProtocolBinding.JsonRpc);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with string agent name and direct protocolBindings and run mode parameters succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_DirectProtocolAndRunMode_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A("agent", "/a2a", A2AProtocolBinding.HttpJson, AgentRunMode.AllowBackgroundIfSupported);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with AIAgent and direct protocolBindings parameter succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAIAgent_DirectProtocol_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agent, "/a2a", A2AProtocolBinding.HttpJson);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with AIAgent and direct protocolBindings and run mode parameters succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAIAgent_DirectProtocolAndRunMode_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agent, "/a2a", A2AProtocolBinding.HttpJson, AgentRunMode.AllowBackgroundIfSupported);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A with AIAgent, null protocolBindings, and direct run mode defaults correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAIAgent_NullProtocolAndDirectRunMode_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2A(agent, "/a2a", protocolBindings: null, agentRunMode: AgentRunMode.DisallowBackground);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A throws ArgumentNullException for null agentName (string overload with configureOptions).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_NullAgentName_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2A((string)null!, "/a2a"));
|
||||
|
||||
Assert.Equal("agentName", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A throws ArgumentNullException for null agentName (string overload with protocolBindings).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_NullAgentName_ProtocolOverload_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2A((string)null!, "/a2a", A2AProtocolBinding.HttpJson));
|
||||
|
||||
Assert.Equal("agentName", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A throws ArgumentException for empty agentName (string overload with configureOptions).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_EmptyAgentName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(() =>
|
||||
app.MapA2A(string.Empty, "/a2a"));
|
||||
|
||||
Assert.Equal("agentName", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A throws ArgumentException for empty agentName (string overload with protocolBindings).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAgentName_EmptyAgentName_ProtocolOverload_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(() =>
|
||||
app.MapA2A(string.Empty, "/a2a", A2AProtocolBinding.HttpJson));
|
||||
|
||||
Assert.Equal("agentName", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A throws ArgumentException for null path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAIAgent_NullPath_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2A(agent, null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2A throws ArgumentException for whitespace-only path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2A_WithAIAgent_WhitespacePath_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
app.MapA2A(agent, " "));
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
|
||||
|
||||
public sealed class A2AIntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that calling the A2A card endpoint with MapA2A returns an agent card with a URL populated.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapA2A_WithAgentCard_CardEndpointReturnsCardWithUrlAsync()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.WebHost.UseTestServer();
|
||||
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("test-agent", "Test instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A test agent for A2A communication",
|
||||
Version = "1.0"
|
||||
};
|
||||
|
||||
// Map A2A with the agent card
|
||||
app.MapA2A(agentBuilder, "/a2a/test-agent", agentCard);
|
||||
|
||||
await app.StartAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Get the test server client
|
||||
TestServer testServer = app.Services.GetRequiredService<IServer>() as TestServer
|
||||
?? throw new InvalidOperationException("TestServer not found");
|
||||
var httpClient = testServer.CreateClient();
|
||||
|
||||
// Act - Query the agent card endpoint
|
||||
var requestUri = new Uri("/a2a/test-agent/v1/card", UriKind.Relative);
|
||||
var response = await httpClient.GetAsync(requestUri);
|
||||
|
||||
// Assert
|
||||
Assert.True(response.IsSuccessStatusCode, $"Expected successful response but got {response.StatusCode}");
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var jsonDoc = JsonDocument.Parse(content);
|
||||
var root = jsonDoc.RootElement;
|
||||
|
||||
// Verify the card has expected properties
|
||||
Assert.True(root.TryGetProperty("name", out var nameProperty));
|
||||
Assert.Equal("Test Agent", nameProperty.GetString());
|
||||
|
||||
Assert.True(root.TryGetProperty("description", out var descProperty));
|
||||
Assert.Equal("A test agent for A2A communication", descProperty.GetString());
|
||||
|
||||
// Verify the card has a URL property and it's not null/empty
|
||||
Assert.True(root.TryGetProperty("url", out var urlProperty));
|
||||
Assert.NotEqual(JsonValueKind.Null, urlProperty.ValueKind);
|
||||
|
||||
var url = urlProperty.GetString();
|
||||
Assert.NotNull(url);
|
||||
Assert.NotEmpty(url);
|
||||
Assert.StartsWith("http", url, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// agentCard's URL matches the agent endpoint
|
||||
Assert.Equal($"{testServer.BaseAddress.ToString().TrimEnd('/')}/a2a/test-agent", url);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await app.StopAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
+646
-615
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AgentRunMode"/> class.
|
||||
/// </summary>
|
||||
public sealed class AgentRunModeTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that AllowBackgroundWhen throws ArgumentNullException for null delegate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AllowBackgroundWhen_NullDelegate_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
AgentRunMode.AllowBackgroundWhen(null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DisallowBackground equals another DisallowBackground instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_DisallowBackground_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
var mode1 = AgentRunMode.DisallowBackground;
|
||||
var mode2 = AgentRunMode.DisallowBackground;
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode1.Equals(mode2));
|
||||
Assert.True(mode1 == mode2);
|
||||
Assert.False(mode1 != mode2);
|
||||
Assert.Equal(mode1.GetHashCode(), mode2.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AllowBackgroundIfSupported equals another AllowBackgroundIfSupported instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_AllowBackgroundIfSupported_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
var mode1 = AgentRunMode.AllowBackgroundIfSupported;
|
||||
var mode2 = AgentRunMode.AllowBackgroundIfSupported;
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode1.Equals(mode2));
|
||||
Assert.True(mode1 == mode2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DisallowBackground and AllowBackgroundIfSupported are not equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_DifferentModes_AreNotEqual()
|
||||
{
|
||||
// Arrange
|
||||
var disallow = AgentRunMode.DisallowBackground;
|
||||
var allow = AgentRunMode.AllowBackgroundIfSupported;
|
||||
|
||||
// Act & Assert
|
||||
Assert.False(disallow.Equals(allow));
|
||||
Assert.False(disallow == allow);
|
||||
Assert.True(disallow != allow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Equals returns false for null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_Null_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var mode = AgentRunMode.DisallowBackground;
|
||||
|
||||
// Act & Assert
|
||||
Assert.False(mode.Equals(null));
|
||||
Assert.False(mode.Equals((object?)null));
|
||||
Assert.False(mode == null);
|
||||
Assert.True(mode != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that two null AgentRunMode values are equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_BothNull_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
AgentRunMode? mode1 = null;
|
||||
AgentRunMode? mode2 = null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode1 == mode2);
|
||||
Assert.False(mode1 != mode2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ToString returns expected values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ToString_ReturnsExpectedValues()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Equal("message", AgentRunMode.DisallowBackground.ToString());
|
||||
Assert.Equal("task", AgentRunMode.AllowBackgroundIfSupported.ToString());
|
||||
Assert.Equal("dynamic", AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(true)).ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Equals works correctly with object parameter.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_WithObjectParameter_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var mode = AgentRunMode.DisallowBackground;
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode.Equals((object)AgentRunMode.DisallowBackground));
|
||||
Assert.False(mode.Equals((object)AgentRunMode.AllowBackgroundIfSupported));
|
||||
Assert.False(mode.Equals("not a run mode"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that two AllowBackgroundWhen instances with different delegates are considered equal,
|
||||
/// because equality is based on the mode value ("dynamic"), not the delegate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_AllowBackgroundWhen_DifferentDelegates_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
var mode1 = AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(true));
|
||||
var mode2 = AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(false));
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode1.Equals(mode2));
|
||||
Assert.True(mode1 == mode2);
|
||||
Assert.Equal(mode1.GetHashCode(), mode2.GetHashCode());
|
||||
}
|
||||
}
|
||||
+17
-17
@@ -10,66 +10,66 @@ namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Converters;
|
||||
public class MessageConverterTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToChatMessages_MessageSendParams_Null_ReturnsEmptyCollection()
|
||||
public void ToChatMessages_SendMessageRequest_Null_ReturnsEmptyCollection()
|
||||
{
|
||||
MessageSendParams? messageSendParams = null;
|
||||
SendMessageRequest? sendMessageRequest = null;
|
||||
|
||||
var result = messageSendParams!.ToChatMessages();
|
||||
var result = sendMessageRequest!.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToChatMessages_MessageSendParams_WithNullMessage_ReturnsEmptyCollection()
|
||||
public void ToChatMessages_SendMessageRequest_WithNullMessage_ReturnsEmptyCollection()
|
||||
{
|
||||
var messageSendParams = new MessageSendParams
|
||||
var sendMessageRequest = new SendMessageRequest
|
||||
{
|
||||
Message = null!
|
||||
};
|
||||
|
||||
var result = messageSendParams.ToChatMessages();
|
||||
var result = sendMessageRequest.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToChatMessages_MessageSendParams_WithMessageWithoutParts_ReturnsEmptyCollection()
|
||||
public void ToChatMessages_SendMessageRequest_WithMessageWithoutParts_ReturnsEmptyCollection()
|
||||
{
|
||||
var messageSendParams = new MessageSendParams
|
||||
var sendMessageRequest = new SendMessageRequest
|
||||
{
|
||||
Message = new AgentMessage
|
||||
Message = new Message
|
||||
{
|
||||
MessageId = "test-id",
|
||||
Role = MessageRole.User,
|
||||
Role = Role.User,
|
||||
Parts = null!
|
||||
}
|
||||
};
|
||||
|
||||
var result = messageSendParams.ToChatMessages();
|
||||
var result = sendMessageRequest.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToChatMessages_MessageSendParams_WithValidTextMessage_ReturnsCorrectChatMessage()
|
||||
public void ToChatMessages_SendMessageRequest_WithValidTextMessage_ReturnsCorrectChatMessage()
|
||||
{
|
||||
var messageSendParams = new MessageSendParams
|
||||
var sendMessageRequest = new SendMessageRequest
|
||||
{
|
||||
Message = new AgentMessage
|
||||
Message = new Message
|
||||
{
|
||||
MessageId = "test-id",
|
||||
Role = MessageRole.User,
|
||||
Role = Role.User,
|
||||
Parts =
|
||||
[
|
||||
new TextPart { Text = "Hello, world!" }
|
||||
new Part { Text = "Hello, world!" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var result = messageSendParams.ToChatMessages();
|
||||
var result = sendMessageRequest.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
|
||||
Reference in New Issue
Block a user