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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@
<Project Path="samples/02-agents/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
<Project Path="samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj" />
<Project Path="samples/02-agents/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj" />
<Project Path="samples/02-agents/A2A/A2AAgent_ProtocolSelection/A2AAgent_ProtocolSelection.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/">
<Project Path="samples/05-end-to-end/AgentWithPurview/AgentWithPurview.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>

<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

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

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
27 changes: 27 additions & 0 deletions dotnet/samples/02-agents/A2A/A2AAgent_ProtocolSelection/README.md
Original file line number Diff line number Diff line change
@@ -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
```
3 changes: 2 additions & 1 deletion dotnet/samples/02-agents/A2A/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/02-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
6 changes: 3 additions & 3 deletions dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,7 +41,7 @@ public sealed class A2AAgent : AIAgent
/// <param name="name">The the name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
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);

Expand Down Expand Up @@ -224,7 +224,7 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
/// <inheritdoc/>
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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,16 +24,15 @@ public static class A2AAgentCardExtensions
/// </remarks>
/// <param name="card">The <see cref="AgentCard" /> to use for the agent creation.</param>
/// <param name="httpClient">The <see cref="HttpClient"/> to use for HTTP requests.</param>
/// <param name="options">
/// Optional <see cref="A2AClientOptions"/> controlling protocol binding preference.
/// When not provided, defaults to preferring HTTP+JSON first, with JSON-RPC as fallback.
/// </param>
/// <param name="loggerFactory">The logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null)
public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, A2AClientOptions? options = null, ILoggerFactory? loggerFactory = 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,18 @@ public static class A2ACardResolverExtensions
/// </remarks>
/// <param name="resolver">The <see cref="A2ACardResolver" /> to use for the agent creation.</param>
/// <param name="httpClient">The <see cref="HttpClient"/> to use for HTTP requests.</param>
/// <param name="options">
/// Optional <see cref="A2AClientOptions"/> controlling protocol binding preference.
/// When not provided, defaults to preferring HTTP+JSON first, with JSON-RPC as fallback.
/// </param>
/// <param name="loggerFactory">The logger factory for enabling logging within the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
public static async Task<AIAgent> GetAIAgentAsync(this A2ACardResolver resolver, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, CancellationToken cancellationToken = default)
public static async Task<AIAgent> 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);
return agentCard.AsAIAgent(httpClient, options, loggerFactory);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace A2A;

/// <summary>
/// Provides extension methods for <see cref="A2AClient"/>
/// Provides extension methods for <see cref="IA2AClient"/>
/// to simplify the creation of A2A agents.
/// </summary>
/// <remarks>
Expand All @@ -29,12 +29,12 @@ public static class A2AClientExtensions
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#3-direct-configuration--private-discovery">Direct Configuration / Private Discovery</see>
/// discovery mechanism.
/// </remarks>
/// <param name="client">The <see cref="A2AClient" /> to use for the agent.</param>
/// <param name="client">The <see cref="IA2AClient" /> to use for the agent.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The the name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
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);
}
41 changes: 35 additions & 6 deletions dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ public void Constructor_WithNullA2AClient_ThrowsArgumentNullException() =>
// Act & Assert
Assert.Throws<ArgumentNullException>(() => 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()
{
Expand Down Expand Up @@ -1371,19 +1386,33 @@ public async Task RunStreamingAsync_WithInvalidSessionType_ThrowsInvalidOperatio
#region GetService Method Tests

/// <summary>
/// Verify that GetService returns A2AClient when requested.
/// Verify that GetService returns IA2AClient when requested.
/// </summary>
[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);
}

/// <summary>
/// Verify that GetService returns null when requesting the concrete A2AClient type
/// since the agent now exposes IA2AClient instead.
/// </summary>
[Fact]
public void GetService_RequestingConcreteA2AClient_ReturnsNull()
{
// Arrange & Act
var result = this._agent.GetService(typeof(A2AClient));

// Assert
Assert.Null(result);
}

/// <summary>
/// Verify that GetService returns AIAgentMetadata when requested.
/// </summary>
Expand Down Expand Up @@ -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.
/// </summary>
[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);
Expand Down
Loading