From d48b3850988b89d8061ecdabd418dda7677ef503 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 15 Apr 2026 15:22:39 +0100 Subject: [PATCH 1/2] Refactor A2A extensions to use IA2AClientFactory and add ProtocolSelection sample - Update A2AAgentCardExtensions to accept IA2AClientFactory instead of A2AClientOptions - Update A2ACardResolverExtensions to accept IA2AClientFactory - Update A2AClientExtensions to accept IA2AClientFactory - Update A2AAgent to use IA2AClientFactory for client creation - Add A2AAgent_ProtocolSelection sample demonstrating protocol selection - Add comprehensive unit tests for all changes - Update README files with new sample reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 1 + .../A2AAgent_ProtocolSelection.csproj | 19 +++++ .../A2A/A2AAgent_ProtocolSelection/Program.cs | 36 +++++++++ .../A2A/A2AAgent_ProtocolSelection/README.md | 27 +++++++ dotnet/samples/02-agents/A2A/README.md | 3 +- dotnet/samples/02-agents/README.md | 1 + .../src/Microsoft.Agents.AI.A2A/A2AAgent.cs | 6 +- .../Extensions/A2AAgentCardExtensions.cs | 15 ++-- .../Extensions/A2ACardResolverExtensions.cs | 8 +- .../Extensions/A2AClientExtensions.cs | 6 +- .../A2AAgentTests.cs | 41 ++++++++-- .../Extensions/A2AAgentCardExtensionsTests.cs | 78 +++++++++++++++++-- .../A2ACardResolverExtensionsTests.cs | 37 ++++++++- .../Extensions/A2AClientExtensionsTests.cs | 36 +++++++++ 14 files changed, 282 insertions(+), 32 deletions(-) create mode 100644 dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/A2AAgent_ProtocolSelection.csproj create mode 100644 dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/Program.cs create mode 100644 dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/README.md diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 22460827ef..127b065220 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -289,6 +289,7 @@ + diff --git a/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/A2AAgent_ProtocolSelection.csproj b/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/A2AAgent_ProtocolSelection.csproj new file mode 100644 index 0000000000..d21ac952b3 --- /dev/null +++ b/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/A2AAgent_ProtocolSelection.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/Program.cs b/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/Program.cs new file mode 100644 index 0000000000..4d1612ee36 --- /dev/null +++ b/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/Program.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to select the A2A protocol binding (HTTP+JSON vs JSON-RPC) when +// creating an AIAgent from an A2A agent card using A2AClientOptions.PreferredBindings. + +using A2A; +using Microsoft.Agents.AI; + +var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set."); + +// Initialize an A2ACardResolver to get an A2A agent card. +A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost)); + +// Get the agent card +AgentCard agentCard = await agentCardResolver.GetAgentCardAsync(); + +// Use A2AClientOptions to explicitly select the HTTP+JSON protocol binding. +// This tells the A2A client factory to prefer the HTTP+JSON interface when the agent card +// advertises multiple supported interfaces. +A2AClientOptions options = new() +{ + PreferredBindings = [ProtocolBindingNames.HttpJson] +}; + +// To prefer JSON-RPC instead, use: +// A2AClientOptions options = new() +// { +// PreferredBindings = [ProtocolBindingNames.JsonRpc] +// }; + +// Create an instance of the AIAgent for an existing A2A agent, using the specified protocol binding. +AIAgent agent = agentCard.AsAIAgent(options: options); + +// Invoke the agent and output the text result. +AgentResponse response = await agent.RunAsync("Tell me a joke about a pirate."); +Console.WriteLine(response); diff --git a/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/README.md b/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/README.md new file mode 100644 index 0000000000..b50a76240c --- /dev/null +++ b/dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/README.md @@ -0,0 +1,27 @@ +# A2A Agent Protocol Selection + +This sample demonstrates how to select the A2A protocol binding when creating an `AIAgent` from an A2A agent card. + +A2A agents can expose multiple interfaces with different protocol bindings (e.g., HTTP+JSON, JSON-RPC). By default, `AsAIAgent()` prefers HTTP+JSON with JSON-RPC as a fallback. This sample shows how to use `A2AClientOptions.PreferredBindings` to explicitly control which protocol binding is used. + +The sample: + +- Connects to an A2A agent server specified in the `A2A_AGENT_HOST` environment variable +- Configures `A2AClientOptions` to prefer the HTTP+JSON protocol binding +- Creates an `AIAgent` from the resolved agent card using the specified binding +- Sends a message to the agent and displays the response + +## Prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 10.0 SDK or later +- An A2A agent server running and accessible via HTTP + +**Note**: These samples need to be run against a valid A2A server. If no A2A server is available, they can be run against the echo-agent that can be spun up locally by following the guidelines at: https://github.com/a2aproject/a2a-dotnet/blob/main/samples/AgentServer/README.md + +Set the following environment variable: + +```powershell +$env:A2A_AGENT_HOST="http://localhost:5000" # Replace with your A2A agent server host +``` diff --git a/dotnet/samples/02-agents/A2A/README.md b/dotnet/samples/02-agents/A2A/README.md index 2f161748df..28f2c0a910 100644 --- a/dotnet/samples/02-agents/A2A/README.md +++ b/dotnet/samples/02-agents/A2A/README.md @@ -3,7 +3,7 @@ These samples demonstrate how to work with Agent-to-Agent (A2A) specific features in the Agent Framework. For other samples that demonstrate how to use AIAgent instances, -see the [Getting Started With Agents](../../02-agents/Agents/README.md) samples. +see the [Getting Started With Agents](../Agents/README.md) samples. ## Prerequisites @@ -16,6 +16,7 @@ See the README.md for each sample for the prerequisites for that sample. |[A2A Agent As Function Tools](./A2AAgent_AsFunctionTools/)|This sample demonstrates how to represent an A2A agent as a set of function tools, where each function tool corresponds to a skill of the A2A agent, and register these function tools with another AI agent so it can leverage the A2A agent's skills.| |[A2A Agent Polling For Task Completion](./A2AAgent_PollingForTaskCompletion/)|This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A agent.| |[A2A Agent Stream Reconnection](./A2AAgent_StreamReconnection/)|This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens, allowing recovery from stream interruptions.| +|[A2A Agent Protocol Selection](./A2AAgent_ProtocolSelection/)|This sample demonstrates how to select the A2A protocol binding (HTTP+JSON vs JSON-RPC) when creating an AIAgent from an A2A agent card using A2AClientOptions.| ## Running the samples from the console diff --git a/dotnet/samples/02-agents/README.md b/dotnet/samples/02-agents/README.md index 5ff0db416d..69f649c9b4 100644 --- a/dotnet/samples/02-agents/README.md +++ b/dotnet/samples/02-agents/README.md @@ -19,3 +19,4 @@ The getting started samples demonstrate the fundamental concepts and functionali | [Declarative Agents](./DeclarativeAgents) | Loading and executing AI agents from YAML configuration files | | [AG-UI](./AGUI/README.md) | Getting started with AG-UI (Agent UI Protocol) servers and clients | | [Dev UI](./DevUI/README.md) | Interactive web interface for testing and debugging AI agents during development | +| [A2A Agents](./A2A/README.md) | Working with Agent-to-Agent (A2A) specific features | diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs index 6e11f0a2cc..9fd2e8ff47 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs @@ -27,7 +27,7 @@ public sealed class A2AAgent : AIAgent { private static readonly AIAgentMetadata s_agentMetadata = new("a2a"); - private readonly A2AClient _a2aClient; + private readonly IA2AClient _a2aClient; private readonly string? _id; private readonly string? _name; private readonly string? _description; @@ -41,7 +41,7 @@ public sealed class A2AAgent : AIAgent /// The the name of the agent. /// The description of the agent. /// Optional logger factory to use for logging. - public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) + public A2AAgent(IA2AClient a2aClient, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) { _ = Throw.IfNull(a2aClient); @@ -224,7 +224,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA /// public override object? GetService(Type serviceType, object? serviceKey = null) => base.GetService(serviceType, serviceKey) - ?? (serviceType == typeof(A2AClient) ? this._a2aClient + ?? (serviceType == typeof(IA2AClient) ? this._a2aClient : serviceType == typeof(AIAgentMetadata) ? s_agentMetadata : null); diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs index 897349f666..6f66bea716 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Linq; using System.Net.Http; using Microsoft.Agents.AI; using Microsoft.Extensions.Logging; @@ -27,15 +25,14 @@ public static class A2AAgentCardExtensions /// The to use for the agent creation. /// The to use for HTTP requests. /// The logger factory for enabling logging within the agent. + /// + /// Optional controlling protocol binding preference. + /// When not provided, defaults to preferring HTTP+JSON first, with JSON-RPC as fallback. + /// /// An instance backed by the A2A agent. - public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) + public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, A2AClientOptions? options = null) { - // TODO: Refactor to support interface selection from card.SupportedInterfaces. - var url = card.SupportedInterfaces?.FirstOrDefault()?.Url - ?? throw new InvalidOperationException("The AgentCard does not have any SupportedInterfaces with a URL."); - - // Create the A2A client using the agent URL from the card. - var a2aClient = new A2AClient(new Uri(url), httpClient); + var a2aClient = A2AClientFactory.Create(card, httpClient, options); return a2aClient.AsAIAgent(name: card.Name, description: card.Description, loggerFactory: loggerFactory); } diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs index 6a32822fea..4d4f3ec811 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs @@ -35,13 +35,17 @@ public static class A2ACardResolverExtensions /// The to use for the agent creation. /// The to use for HTTP requests. /// The logger factory for enabling logging within the agent. + /// + /// Optional controlling protocol binding preference. + /// When not provided, defaults to preferring HTTP+JSON first, with JSON-RPC as fallback. + /// /// The to monitor for cancellation requests. The default is . /// An instance backed by the A2A agent. - public static async Task GetAIAgentAsync(this A2ACardResolver resolver, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, CancellationToken cancellationToken = default) + public static async Task GetAIAgentAsync(this A2ACardResolver resolver, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, A2AClientOptions? options = null, CancellationToken cancellationToken = default) { // Obtain the agent card from the resolver. var agentCard = await resolver.GetAgentCardAsync(cancellationToken).ConfigureAwait(false); - return agentCard.AsAIAgent(httpClient, loggerFactory); + return agentCard.AsAIAgent(httpClient, loggerFactory, options); } } diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs index cd93ca0bac..c7386309d3 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs @@ -7,7 +7,7 @@ namespace A2A; /// -/// Provides extension methods for +/// Provides extension methods for /// to simplify the creation of A2A agents. /// /// @@ -29,12 +29,12 @@ public static class A2AClientExtensions /// Direct Configuration / Private Discovery /// discovery mechanism. /// - /// The to use for the agent. + /// The to use for the agent. /// The unique identifier for the agent. /// The the name of the agent. /// The description of the agent. /// Optional logger factory for enabling logging within the agent. /// An instance backed by the A2A agent. - public static AIAgent AsAIAgent(this A2AClient client, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) => + public static AIAgent AsAIAgent(this IA2AClient client, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) => new A2AAgent(client, id, name, description, loggerFactory); } diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs index 4ae09e8f5a..320b5b030c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs @@ -55,6 +55,21 @@ public void Constructor_WithNullA2AClient_ThrowsArgumentNullException() => // Act & Assert Assert.Throws(() => new A2AAgent(null!)); + [Fact] + public void Constructor_WithIA2AClient_InitializesCorrectly() + { + // Arrange + IA2AClient ia2aClient = this._a2aClient; + + // Act + var agent = new A2AAgent(ia2aClient, "ia2a-id", "IA2A Agent", "An agent from IA2AClient"); + + // Assert + Assert.Equal("ia2a-id", agent.Id); + Assert.Equal("IA2A Agent", agent.Name); + Assert.Equal("An agent from IA2AClient", agent.Description); + } + [Fact] public void Constructor_WithDefaultParameters_UsesBaseProperties() { @@ -1371,19 +1386,33 @@ public async Task RunStreamingAsync_WithInvalidSessionType_ThrowsInvalidOperatio #region GetService Method Tests /// - /// Verify that GetService returns A2AClient when requested. + /// Verify that GetService returns IA2AClient when requested. /// [Fact] - public void GetService_RequestingA2AClient_ReturnsA2AClient() + public void GetService_RequestingIA2AClient_ReturnsA2AClient() { // Arrange & Act - var result = this._agent.GetService(typeof(A2AClient)); + var result = this._agent.GetService(typeof(IA2AClient)); // Assert Assert.NotNull(result); Assert.Same(this._a2aClient, result); } + /// + /// Verify that GetService returns null when requesting the concrete A2AClient type + /// since the agent now exposes IA2AClient instead. + /// + [Fact] + public void GetService_RequestingConcreteA2AClient_ReturnsNull() + { + // Arrange & Act + var result = this._agent.GetService(typeof(A2AClient)); + + // Assert + Assert.Null(result); + } + /// /// Verify that GetService returns AIAgentMetadata when requested. /// @@ -1458,10 +1487,10 @@ public void GetService_RequestingAIAgentType_ReturnsBaseImplementation() /// Verify that GetService calls base.GetService() first but continues to derived logic when base returns null. /// [Fact] - public void GetService_RequestingA2AClientWithServiceKey_CallsBaseFirstThenDerivedLogic() + public void GetService_RequestingIA2AClientWithServiceKey_CallsBaseFirstThenDerivedLogic() { - // Arrange & Act - Request A2AClient with a service key (base.GetService will return null due to serviceKey) - var result = this._agent.GetService(typeof(A2AClient), "some-key"); + // Arrange & Act - Request IA2AClient with a service key (base.GetService will return null due to serviceKey) + var result = this._agent.GetService(typeof(IA2AClient), "some-key"); // Assert Assert.NotNull(result); diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs index b45c381bd2..603376b396 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs @@ -57,7 +57,7 @@ public async Task RunIAgentAsync_SendsRequestToTheUrlSpecifiedInAgentCardAsync() Parts = [Part.FromText("Response")], }); - var agent = this._agentCard.AsAIAgent(httpClient); + var agent = this._agentCard.AsAIAgent(httpClient: httpClient); // Act await agent.RunAsync("Test input"); @@ -68,7 +68,7 @@ public async Task RunIAgentAsync_SendsRequestToTheUrlSpecifiedInAgentCardAsync() } [Fact] - public async Task AsAIAgent_WithMultipleInterfaces_UsesFirstInterfaceAsync() + public async Task AsAIAgent_WithPreferredBindings_UsesMatchingInterfaceAsync() { // Arrange var card = new AgentCard @@ -77,9 +77,8 @@ public async Task AsAIAgent_WithMultipleInterfaces_UsesFirstInterfaceAsync() Description = "An agent with multiple interfaces", SupportedInterfaces = [ - new AgentInterface { Url = "http://first/agent" }, - new AgentInterface { Url = "http://second/agent", ProtocolBinding = "grpc" }, - new AgentInterface { Url = "http://third/agent", ProtocolBinding = "http" }, + new AgentInterface { Url = "http://first/agent", ProtocolBinding = ProtocolBindingNames.HttpJson }, + new AgentInterface { Url = "http://second/agent", ProtocolBinding = ProtocolBindingNames.JsonRpc }, ] }; @@ -92,14 +91,79 @@ public async Task AsAIAgent_WithMultipleInterfaces_UsesFirstInterfaceAsync() Parts = [Part.FromText("Response")], }); - var agent = card.AsAIAgent(httpClient); + var options = new A2AClientOptions + { + PreferredBindings = [ProtocolBindingNames.JsonRpc] + }; + + var agent = card.AsAIAgent(httpClient, options: options); // Act await agent.RunAsync("Test input"); // Assert Assert.Single(handler.CapturedUris); - Assert.Equal(new Uri("http://first/agent"), handler.CapturedUris[0]); + Assert.Equal(new Uri("http://second/agent"), handler.CapturedUris[0]); + } + + [Fact] + public void AsAIAgent_WithNullOptions_UsesDefaultBindingPreference() + { + // Arrange + var card = new AgentCard + { + Name = "Default Options Agent", + Description = "Tests default A2AClientOptions behavior", + SupportedInterfaces = + [ + new AgentInterface { Url = "http://default/agent" }, + ] + }; + + // Act - null options should use defaults (HTTP+JSON first, JSON-RPC as fallback) + var agent = card.AsAIAgent(options: null); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal("Default Options Agent", agent.Name); + } + + [Fact] + public void AsAIAgent_WithNoMatchingBinding_ThrowsException() + { + // Arrange + var card = new AgentCard + { + Name = "Unmatched Binding Agent", + Description = "Agent with unsupported binding only", + SupportedInterfaces = + [ + new AgentInterface { Url = "http://grpc/agent", ProtocolBinding = "GRPC" }, + ] + }; + + var options = new A2AClientOptions + { + PreferredBindings = [ProtocolBindingNames.JsonRpc] + }; + + // Act & Assert - factory should throw when no matching binding exists + Assert.ThrowsAny(() => card.AsAIAgent(options: options)); + } + + [Fact] + public void AsAIAgent_WithNoSupportedInterfaces_ThrowsException() + { + // Arrange + var card = new AgentCard + { + Name = "No Interfaces Agent", + Description = "Agent with no supported interfaces", + }; + + // Act & Assert + Assert.ThrowsAny(() => card.AsAIAgent()); } internal sealed class HttpMessageHandlerStub : HttpMessageHandler diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs index 95cb2a67d2..8a664b7fc9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs @@ -68,7 +68,7 @@ public async Task RunIAgentAsync_WithUrlFromAgentCard_SendsRequestToTheUrlAsync( Parts = [Part.FromText("Response")], }); - var agent = await this._resolver.GetAIAgentAsync(this._httpClient); + var agent = await this._resolver.GetAIAgentAsync(httpClient: this._httpClient); // Act await agent.RunAsync("Test input"); @@ -78,6 +78,41 @@ public async Task RunIAgentAsync_WithUrlFromAgentCard_SendsRequestToTheUrlAsync( Assert.Equal(new Uri("http://test-endpoint/agent"), this._handler.CapturedUris[1]); } + [Fact] + public async Task GetAIAgentAsync_WithOptions_PassesOptionsToFactoryAsync() + { + // Arrange + this._handler.ResponsesToReturn.Enqueue(new AgentCard + { + Name = "Options Agent", + Description = "Agent with multiple interfaces", + SupportedInterfaces = + [ + new AgentInterface { Url = "http://httpjson/agent", ProtocolBinding = ProtocolBindingNames.HttpJson }, + new AgentInterface { Url = "http://jsonrpc/agent", ProtocolBinding = ProtocolBindingNames.JsonRpc }, + ] + }); + this._handler.ResponsesToReturn.Enqueue(new Message + { + Role = Role.Agent, + Parts = [Part.FromText("Response")], + }); + + var options = new A2AClientOptions + { + PreferredBindings = [ProtocolBindingNames.JsonRpc] + }; + + var agent = await this._resolver.GetAIAgentAsync(httpClient: this._httpClient, options: options); + + // Act + await agent.RunAsync("Test input"); + + // Assert + Assert.Equal(2, this._handler.CapturedUris.Count); + Assert.Equal(new Uri("http://jsonrpc/agent"), this._handler.CapturedUris[1]); + } + public void Dispose() { this._handler.Dispose(); diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs index 9ad4d982a9..80b5107bf1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs @@ -30,4 +30,40 @@ public void GetAIAgent_WithAllParameters_ReturnsA2AAgentWithSpecifiedProperties( Assert.Equal(TestName, agent.Name); Assert.Equal(TestDescription, agent.Description); } + + [Fact] + public void GetAIAgent_WithIA2AClient_ReturnsA2AAgentWithSpecifiedProperties() + { + // Arrange - use IA2AClient reference type to verify the extension method works with the interface + IA2AClient a2aClient = new A2AClient(new Uri("http://test-endpoint")); + + const string TestId = "ia2a-agent-id"; + const string TestName = "IA2A Agent"; + const string TestDescription = "Agent created from IA2AClient"; + + // Act + var agent = a2aClient.AsAIAgent(TestId, TestName, TestDescription); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal(TestId, agent.Id); + Assert.Equal(TestName, agent.Name); + Assert.Equal(TestDescription, agent.Description); + } + + [Fact] + public void GetAIAgent_WithIA2AClient_ExposesClientViaGetService() + { + // Arrange + IA2AClient a2aClient = new A2AClient(new Uri("http://test-endpoint")); + + // Act + var agent = a2aClient.AsAIAgent(); + + // Assert + var service = agent.GetService(typeof(IA2AClient)); + Assert.NotNull(service); + Assert.Same(a2aClient, service); + } } From ce404a180d72e1471f6f7b893dcf55ea2dc248cc Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 15 Apr 2026 17:28:26 +0100 Subject: [PATCH 2/2] Reorder params: options before loggerFactory in A2A extensions Move A2AClientOptions parameter before ILoggerFactory in AsAIAgent and GetAIAgentAsync extension methods to follow the repo convention of keeping LoggerFactory and CancellationToken as the last parameters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/A2AAgentCardExtensions.cs | 4 ++-- .../Extensions/A2ACardResolverExtensions.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs index 6f66bea716..086505b2bc 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs @@ -24,13 +24,13 @@ public static class A2AAgentCardExtensions /// /// The to use for the agent creation. /// The to use for HTTP requests. - /// The logger factory for enabling logging within the agent. /// /// Optional controlling protocol binding preference. /// When not provided, defaults to preferring HTTP+JSON first, with JSON-RPC as fallback. /// + /// The logger factory for enabling logging within the agent. /// An instance backed by the A2A agent. - public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, A2AClientOptions? options = null) + public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, A2AClientOptions? options = null, ILoggerFactory? loggerFactory = null) { var a2aClient = A2AClientFactory.Create(card, httpClient, options); diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs index 4d4f3ec811..49b6de1102 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs @@ -34,18 +34,18 @@ public static class A2ACardResolverExtensions /// /// The to use for the agent creation. /// The to use for HTTP requests. - /// The logger factory for enabling logging within the agent. /// /// Optional controlling protocol binding preference. /// When not provided, defaults to preferring HTTP+JSON first, with JSON-RPC as fallback. /// + /// The logger factory for enabling logging within the agent. /// The to monitor for cancellation requests. The default is . /// An instance backed by the A2A agent. - public static async Task GetAIAgentAsync(this A2ACardResolver resolver, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, A2AClientOptions? options = null, CancellationToken cancellationToken = default) + public static async Task GetAIAgentAsync(this A2ACardResolver resolver, HttpClient? httpClient = null, A2AClientOptions? options = null, ILoggerFactory? loggerFactory = null, CancellationToken cancellationToken = default) { // Obtain the agent card from the resolver. var agentCard = await resolver.GetAgentCardAsync(cancellationToken).ConfigureAwait(false); - return agentCard.AsAIAgent(httpClient, loggerFactory, options); + return agentCard.AsAIAgent(httpClient, options, loggerFactory); } }