Skip to content
Open
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
48 changes: 24 additions & 24 deletions client/base/src/main/java/io/a2a/client/ClientBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -328,15 +328,17 @@ private ClientTransport buildClientTransport() throws A2AClientException {
return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface), clientTransportConfig);
}

private Map<String, String> getServerPreferredTransports() throws A2AClientException {
Map<String, String> serverPreferredTransports = new LinkedHashMap<>();
if(agentCard.supportedInterfaces() == null || agentCard.supportedInterfaces().isEmpty()) {
private Map<String, AgentInterface> getServerInterfacesMap() throws A2AClientException {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a note that only the fist found protocol binding is considered ?

Copy link
Copy Markdown
Author

@Lirons01 Lirons01 Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, let me know if the comment is unclear or needs additional edits. Line 336

List<AgentInterface> serverInterfaces = agentCard.supportedInterfaces();
if (serverInterfaces == null || serverInterfaces.isEmpty()) {
throw new A2AClientException("No server interface available in the AgentCard");
}
for (AgentInterface agentInterface : agentCard.supportedInterfaces()) {
serverPreferredTransports.putIfAbsent(agentInterface.protocolBinding(), agentInterface.url());
// If there are multiple interfaces with the same protocol binding, only the first is considered
Map<String, AgentInterface> serverInterfacesMap = new LinkedHashMap<>();
for (AgentInterface iface : serverInterfaces) {
serverInterfacesMap.putIfAbsent(iface.protocolBinding(), iface);
}
return serverPreferredTransports;
return serverInterfacesMap;
}

private List<String> getClientPreferredTransports() {
Expand All @@ -351,40 +353,38 @@ private List<String> getClientPreferredTransports() {
return supportedClientTransports;
}

private AgentInterface findBestClientTransport() throws A2AClientException {
// Retrieve transport supported by the A2A server
Map<String, String> serverPreferredTransports = getServerPreferredTransports();

// Retrieve transport configured for this client (using withTransport methods)
// Package-private for testing
AgentInterface findBestClientTransport() throws A2AClientException {
Map<String, AgentInterface> serverInterfacesMap = getServerInterfacesMap();
List<String> clientPreferredTransports = getClientPreferredTransports();

String transportProtocol = null;
String transportUrl = null;
AgentInterface matchedInterface = null;
if (clientConfig.isUseClientPreference()) {
// Client preference: iterate client transports first, find first server match
for (String clientPreferredTransport : clientPreferredTransports) {
if (serverPreferredTransports.containsKey(clientPreferredTransport)) {
transportProtocol = clientPreferredTransport;
transportUrl = serverPreferredTransports.get(transportProtocol);
if (serverInterfacesMap.containsKey(clientPreferredTransport)) {
matchedInterface = serverInterfacesMap.get(clientPreferredTransport);
break;
}
}
} else {
for (Map.Entry<String, String> transport : serverPreferredTransports.entrySet()) {
if (clientPreferredTransports.contains(transport.getKey())) {
transportProtocol = transport.getKey();
transportUrl = transport.getValue();
// Server preference: iterate server interfaces first, find first client match
for (AgentInterface iface : serverInterfacesMap.values()) {
if (clientPreferredTransports.contains(iface.protocolBinding())) {
matchedInterface = iface;
break;
}
}
}
if (transportProtocol == null || transportUrl == null) {

if (matchedInterface == null) {
throw new A2AClientException("No compatible transport found");
}
if (!transportProviderRegistry.containsKey(transportProtocol)) {
throw new A2AClientException("No client available for " + transportProtocol);
if (!transportProviderRegistry.containsKey(matchedInterface.protocolBinding())) {
throw new A2AClientException("No client available for " + matchedInterface.protocolBinding());
}

return new AgentInterface(transportProtocol, transportUrl);
return matchedInterface;
}

/**
Expand Down
111 changes: 96 additions & 15 deletions client/base/src/test/java/io/a2a/client/ClientBuilderTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.a2a.client;


import java.util.Collections;
import java.util.List;

Expand All @@ -22,27 +21,38 @@

public class ClientBuilderTest {

private AgentCard card = AgentCard.builder()
.name("Hello World Agent")
private static AgentCard buildCard(List<AgentInterface> interfaces) {
return AgentCard.builder()
.name("Hello World Agent")
.description("Just a hello world agent")
.version("1.0.0")
.documentationUrl("http://example.com/docs")
.capabilities(AgentCapabilities.builder()
.streaming(true)
.pushNotifications(true)
.build())
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.skills(Collections.singletonList(AgentSkill.builder()
.id("hello_world")
.name("Returns hello world")
.description("just returns hello world")
.tags(Collections.singletonList("hello world"))
.examples(List.of("hi", "hello world"))
.build()))
.supportedInterfaces(List.of(
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999")))
.build();
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.skills(Collections.singletonList(AgentSkill.builder()
.id("hello_world")
.name("Returns hello world")
.description("just returns hello world")
.tags(Collections.singletonList("hello world"))
.examples(List.of("hi", "hello world"))
.build()))
.supportedInterfaces(interfaces)
.build();
}

private final AgentCard card = buildCard(List.of(
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999")));

private final AgentCard cardWithTenant = buildCard(List.of(
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/default-tenant")));

private final AgentCard cardWithMultipleInterfaces = buildCard(List.of(
new AgentInterface(TransportProtocol.GRPC.asString(), "http://localhost:9998", "/grpc-tenant", "1.0"),
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/jsonrpc-tenant", "1.0")));

@Test
public void shouldNotFindCompatibleTransport() throws A2AClientException {
Expand Down Expand Up @@ -91,4 +101,75 @@ public void shouldCreateClient_differentConfigurations() throws A2AClientExcepti

Assertions.assertNotNull(client);
}

@Test
public void shouldPreserveTenantFromAgentInterface() throws A2AClientException {
ClientBuilder builder = Client
.builder(cardWithTenant)
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());

AgentInterface selectedInterface = builder.findBestClientTransport();

Assertions.assertEquals("/default-tenant", selectedInterface.tenant());
Assertions.assertEquals("http://localhost:9999", selectedInterface.url());
Assertions.assertEquals(TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding());
}

@Test
public void shouldPreserveProtocolVersionFromAgentInterface() throws A2AClientException {
ClientBuilder builder = Client
.builder(cardWithMultipleInterfaces)
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());

AgentInterface selectedInterface = builder.findBestClientTransport();

Assertions.assertEquals("/jsonrpc-tenant", selectedInterface.tenant());
Assertions.assertEquals("1.0", selectedInterface.protocolVersion());
}

@Test
public void shouldSelectCorrectInterfaceWithServerPreference() throws A2AClientException {
// Server preference (default): iterates server interfaces in order, picks first that client supports
// cardWithMultipleInterfaces has [GRPC, JSONRPC] - GRPC is first
// Client supports both GRPC and JSONRPC, so GRPC should be selected (server's first choice)
ClientBuilder builder = Client
.builder(cardWithMultipleInterfaces)
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(s -> null))
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());

AgentInterface selectedInterface = builder.findBestClientTransport();

Assertions.assertEquals(TransportProtocol.GRPC.asString(), selectedInterface.protocolBinding());
Assertions.assertEquals("http://localhost:9998", selectedInterface.url());
Assertions.assertEquals("/grpc-tenant", selectedInterface.tenant());
}

@Test
public void shouldSelectCorrectInterfaceWithClientPreference() throws A2AClientException {
// Client preference: iterates client transports in registration order, picks first that server supports
// Client registers [JSONRPC, GRPC] - JSONRPC is first
// Server supports both, so JSONRPC should be selected (client's first choice)
ClientBuilder builder = Client
.builder(cardWithMultipleInterfaces)
.clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(s -> null));

AgentInterface selectedInterface = builder.findBestClientTransport();

Assertions.assertEquals(TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding());
Assertions.assertEquals("http://localhost:9999", selectedInterface.url());
Assertions.assertEquals("/jsonrpc-tenant", selectedInterface.tenant());
}

@Test
public void shouldPreserveEmptyTenant() throws A2AClientException {
ClientBuilder builder = Client
.builder(card)
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());

AgentInterface selectedInterface = builder.findBestClientTransport();

Assertions.assertEquals("", selectedInterface.tenant());
}
}
Loading