Skip to content

Commit 3c6c204

Browse files
committed
feat: add a new client project, that defines extensions to discover A2A agents
Signed-off-by: Charles d'Avernas <[email protected]>
1 parent 57e01ce commit 3c6c204

File tree

14 files changed

+289
-23
lines changed

14 files changed

+289
-23
lines changed

README.md

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@ This repository provides a complete set of libraries and components for building
1818

1919
### 📡 Client
2020

21+
- **`a2a-net.Client.Abstractions`**
22+
Contains core interfaces and contracts for implementing A2A clients.
23+
2124
- **`a2a-net.Client`**
22-
Provides the default implementation of an A2A client for sending tasks and interacting with agents via JSON-RPC.
25+
Includes client-side functionality for A2A agent discovery and metadata resolution.
26+
27+
- **`a2a-net.Client.Http`**
28+
Implements the HTTP transport for `IA2AProtocolClient`
29+
Allows establishing persistent agent-to-agent communication over HTTP connections.
2330

24-
- **`a2a-net.Client.Transport.WebSocket`**
25-
Implements the WebSocket transport for `a2a-net.Client`.
31+
- **`a2a-net.Client.WebSocket`**
32+
Implements the WebSocket transport for `IA2AProtocolClient`
2633
Allows establishing persistent agent-to-agent communication over WebSocket connections.
2734

2835
---
@@ -57,23 +64,52 @@ This repository provides a complete set of libraries and components for building
5764

5865
```
5966
dotnet add package a2a-net.Client
60-
dotnet add package a2a-net.Client.Transport.WebSocket
61-
dotnet add package a2a-net.Server
67+
dotnet add package a2a-net.Client.Http
68+
dotnet add package a2a-net.Client.WebSocket
69+
dotnet add package a2a-net.Server.Infrastructure.DistributedCache
6270
dotnet add package a2a-net.Server.AspNetCore
6371
```
6472

65-
### Configure the client
73+
### Discover a remote agent
6674

6775
```csharp
68-
services.AddA2ProtocolClient(builder =>
76+
var discoveryDocument = await httpClient.GetA2ADiscoveryDocumentAsync(new Uri("http://localhost"));
77+
```
78+
79+
### Configure and use a client
80+
81+
```csharp
82+
services.AddA2ProtocolHttpClient(options =>
6983
{
70-
builder.UseWebSocketTransport(options =>
71-
{
72-
options.Endpoint = new("ws://localhost/a2a");
73-
});
84+
options.Endpoint = new("http://localhost/a2a");
7485
});
7586
```
7687

88+
```csharp
89+
services.AddA2ProtocolWebSocketClient(options =>
90+
{
91+
options.Endpoint = new("ws://localhost/a2a");
92+
});
93+
```
94+
95+
```csharp
96+
var request = new SendTaskRequest()
97+
{
98+
Params = new()
99+
{
100+
Message = new()
101+
{
102+
Role = MessageRole.User,
103+
Parts =
104+
[
105+
new TextPart("tell me a joke")
106+
]
107+
}
108+
}
109+
};
110+
var response = await Client.SendTaskAsync(request);
111+
```
112+
77113
### Host an agent
78114

79115
#### Configure services
@@ -83,15 +119,19 @@ services.AddDistributedMemoryCache();
83119
services.AddA2AProtocolServer(builder =>
84120
{
85121
builder
86-
.UseAgentRuntime<CustomAgentRuntime>()
122+
.SupportsStreaming()
123+
.SupportsPushNotifications()
124+
.SupportsStateTransitionHistory()
125+
.UseAgentRuntime<MockAgentRuntime>()
87126
.UseDistributedCacheTaskRepository();
88127
});
89128
```
90129

91130
#### Map A2A Endpoints
92131

93132
```csharp
94-
app.MapA2AEndpoint();
133+
app.MapA2AAgentHttpEndpoint("/a2a");
134+
app.MapA2AAgentWebSocketEndpoint("/a2a/ws")
95135
```
96136

97137
---

a2a-net.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "a2a-net.Client.Abstractions
5555
EndProject
5656
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "a2a-net.Client.WebSocket", "src\a2a-net.Client.WebSocket\a2a-net.Client.WebSocket.csproj", "{1A8D202B-4921-1974-FB12-7811F4BDEA47}"
5757
EndProject
58+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "a2a-net.Client", "src\a2a-net.Client\a2a-net.Client.csproj", "{DBF98B98-D0C2-46C3-93A8-F42CB3CF0E97}"
59+
EndProject
5860
Global
5961
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6062
Debug|Any CPU = Debug|Any CPU
@@ -101,6 +103,10 @@ Global
101103
{1A8D202B-4921-1974-FB12-7811F4BDEA47}.Debug|Any CPU.Build.0 = Debug|Any CPU
102104
{1A8D202B-4921-1974-FB12-7811F4BDEA47}.Release|Any CPU.ActiveCfg = Release|Any CPU
103105
{1A8D202B-4921-1974-FB12-7811F4BDEA47}.Release|Any CPU.Build.0 = Release|Any CPU
106+
{DBF98B98-D0C2-46C3-93A8-F42CB3CF0E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
107+
{DBF98B98-D0C2-46C3-93A8-F42CB3CF0E97}.Debug|Any CPU.Build.0 = Debug|Any CPU
108+
{DBF98B98-D0C2-46C3-93A8-F42CB3CF0E97}.Release|Any CPU.ActiveCfg = Release|Any CPU
109+
{DBF98B98-D0C2-46C3-93A8-F42CB3CF0E97}.Release|Any CPU.Build.0 = Release|Any CPU
104110
EndGlobalSection
105111
GlobalSection(SolutionProperties) = preSolution
106112
HideSolutionNode = FALSE
@@ -118,6 +124,7 @@ Global
118124
{3F970C17-AB49-468C-B6DF-8A0045FC77D2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
119125
{D83DCD5A-67A0-752D-711A-54F244C6100A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
120126
{1A8D202B-4921-1974-FB12-7811F4BDEA47} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
127+
{DBF98B98-D0C2-46C3-93A8-F42CB3CF0E97} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
121128
EndGlobalSection
122129
GlobalSection(ExtensibilityGlobals) = postSolution
123130
SolutionGuid = {698C2979-1A16-437B-8C04-1357EB80CC7F}

src/a2a-net.Client.Asbtractions/Services/Interfaces/IA2AProtocolClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ public interface IA2AProtocolClient
2222

2323

2424

25-
}
25+
}

src/a2a-net.Client.Asbtractions/a2a-net.Client.Abstractions.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
1414
<IsPackable>true</IsPackable>
1515
<Title>A2A-NET Protocol Client</Title>
16-
<Description>Contains the A2A Protocol client</Description>
17-
<PackageTags>a2a;client</PackageTags>
16+
<Description>Contains the A2A Protocol client abstractions</Description>
17+
<PackageTags>a2a;client;abstractions</PackageTags>
1818
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
1919
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
2020
<PackageReadmeFile>README.md</PackageReadmeFile>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright � 2025-Present the a2a-net Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
namespace A2A.Client;
15+
16+
/// <summary>
17+
/// Represents the result of an A2A agent discovery operation
18+
/// </summary>
19+
public class A2ADiscoveryDocument
20+
{
21+
22+
/// <summary>
23+
/// Gets the endpoint from which the discovery document was retrieved
24+
/// </summary>
25+
public virtual required Uri Endpoint { get; init; }
26+
27+
/// <summary>
28+
/// Gets a list contained the discovered <see cref="AgentCard"/> entries returned by the remote agent
29+
/// </summary>
30+
public virtual required IReadOnlyList<AgentCard> Agents { get; init; }
31+
32+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright � 2025-Present the a2a-net Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
namespace A2A.Client;
15+
16+
/// <summary>
17+
/// Represents a request to retrieve an A2A discovery document from a remote server
18+
/// </summary>
19+
public class A2ADiscoveryDocumentRequest
20+
{
21+
22+
/// <summary>
23+
/// Gets/sets the base URI of the remote server to query for discovery metadata
24+
/// </summary>
25+
public virtual Uri? Address { get; init; }
26+
27+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright � 2025-Present the a2a-net Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
namespace A2A.Client;
15+
16+
/// <summary>
17+
/// Defines extensions for <see cref="HttpClient"/>s
18+
/// </summary>
19+
public static class HttpClientExtensions
20+
{
21+
22+
/// <summary>
23+
/// Retrieves the A2A discovery document from a remote agent
24+
/// </summary>
25+
/// <param name="httpClient">The <see cref="HttpClient"/> to use to perform the request</param>
26+
/// <param name="request">The discovery request containing the base URI and optional configuration</param>
27+
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
28+
/// <returns>The retrieved <see cref="A2ADiscoveryDocument"/></returns>
29+
public static async Task<A2ADiscoveryDocument> GetA2ADiscoveryDocumentAsync(this HttpClient httpClient, A2ADiscoveryDocumentRequest request, CancellationToken cancellationToken = default)
30+
{
31+
ArgumentNullException.ThrowIfNull(httpClient);
32+
ArgumentNullException.ThrowIfNull(request);
33+
try
34+
{
35+
var path = "/.well-known/agent.json";
36+
var endpoint = request.Address == null ? new Uri(path, UriKind.Relative) : new Uri(request.Address, path);
37+
var agentCard = await httpClient.GetFromJsonAsync<AgentCard>(endpoint, cancellationToken).ConfigureAwait(false);
38+
return new()
39+
{
40+
Endpoint = endpoint.IsAbsoluteUri ? endpoint : new(httpClient.BaseAddress!, path),
41+
Agents = agentCard == null ? [] : [agentCard]
42+
};
43+
}
44+
catch (HttpRequestException ex) when(ex.StatusCode == HttpStatusCode.NotFound)
45+
{
46+
var path = "/.well-known/agents.json";
47+
var endpoint = request.Address == null ? new Uri(path, UriKind.Relative) : new Uri(request.Address, path);
48+
var agentCards = await httpClient.GetFromJsonAsync<List<AgentCard>>(endpoint, cancellationToken).ConfigureAwait(false);
49+
return new()
50+
{
51+
Endpoint = endpoint.IsAbsoluteUri ? endpoint : new(httpClient.BaseAddress!, path),
52+
Agents = agentCards ?? []
53+
};
54+
}
55+
}
56+
57+
/// <summary>
58+
/// Retrieves the A2A discovery document from a remote agent
59+
/// </summary>
60+
/// <param name="httpClient">The <see cref="HttpClient"/> to use to perform the request</param>
61+
/// <param name="address">The base URI of the remote server to query for discovery metadata</param>
62+
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
63+
/// <returns>The retrieved <see cref="A2ADiscoveryDocument"/></returns>
64+
public static Task<A2ADiscoveryDocument> GetA2ADiscoveryDocumentAsync(this HttpClient httpClient, Uri address, CancellationToken cancellationToken = default)
65+
{
66+
ArgumentNullException.ThrowIfNull(httpClient);
67+
ArgumentNullException.ThrowIfNull(address);
68+
var request = new A2ADiscoveryDocumentRequest()
69+
{
70+
Address = address
71+
};
72+
return httpClient.GetA2ADiscoveryDocumentAsync(request, cancellationToken);
73+
}
74+
75+
/// <summary>
76+
/// Retrieves the A2A discovery document from a remote agent
77+
/// </summary>
78+
/// <param name="httpClient">The <see cref="HttpClient"/> to use to perform the request</param>
79+
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
80+
/// <returns>The retrieved <see cref="A2ADiscoveryDocument"/></returns>
81+
public static Task<A2ADiscoveryDocument> GetA2ADiscoveryDocumentAsync(this HttpClient httpClient, CancellationToken cancellationToken = default)
82+
{
83+
ArgumentNullException.ThrowIfNull(httpClient);
84+
var request = new A2ADiscoveryDocumentRequest(); ;
85+
return httpClient.GetA2ADiscoveryDocumentAsync(request, cancellationToken);
86+
}
87+
88+
}

src/a2a-net.Client/Usings.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright � 2025-Present the a2a-net Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
global using A2A.Models;
15+
global using System.Net;
16+
global using System.Net.Http.Json;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>A2A.Client</RootNamespace>
8+
<VersionPrefix>0.3.0</VersionPrefix>
9+
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
10+
<FileVersion>$(VersionPrefix)</FileVersion>
11+
<NeutralLanguage>en</NeutralLanguage>
12+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
13+
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
14+
<IsPackable>true</IsPackable>
15+
<Title>A2A-NET Protocol Client</Title>
16+
<Description>Contains the A2A Protocol client</Description>
17+
<PackageTags>a2a;client</PackageTags>
18+
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
19+
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
20+
<PackageReadmeFile>README.md</PackageReadmeFile>
21+
<Copyright>Copyright © 2025-Present the a2a-net Authors. All rights reserved.</Copyright>
22+
<PackageProjectUrl>https://github.com/neuroglia-io/a2a</PackageProjectUrl>
23+
<RepositoryUrl>https://github.com/neuroglia-io/a2a</RepositoryUrl>
24+
<RepositoryType>git</RepositoryType>
25+
<DebugType>embedded</DebugType>
26+
</PropertyGroup>
27+
28+
<ItemGroup>
29+
<None Include="..\..\README.md">
30+
<Pack>True</Pack>
31+
<PackagePath>\</PackagePath>
32+
</None>
33+
</ItemGroup>
34+
35+
<ItemGroup>
36+
<ProjectReference Include="..\a2a-net.Client.Asbtractions\a2a-net.Client.Abstractions.csproj" />
37+
</ItemGroup>
38+
39+
</Project>

src/a2a-net.Core/Models/TaskSendParameters.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public record TaskSendParameters
2525
/// </summary>
2626
[Required, MinLength(1)]
2727
[DataMember(Name = "id", Order = 1), JsonPropertyName("id"), JsonPropertyOrder(1), YamlMember(Alias = "id", Order = 1)]
28-
public virtual string Id { get; set; } = null!;
28+
public virtual string Id { get; set; } = Guid.NewGuid().ToString("N");
2929

3030
/// <summary>
3131
/// Gets/sets the unique identifier of the session the task belongs to<para></para>

0 commit comments

Comments
 (0)