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
47 changes: 23 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,16 @@ 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 {
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());
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 +352,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
143 changes: 129 additions & 14 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 @@ -24,26 +23,71 @@ public class ClientBuilderTest {

private AgentCard card = 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())
.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()))
.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();

private AgentCard cardWithTenant = 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", "/default-tenant")))
.build();

private AgentCard cardWithMultipleInterfaces = AgentCard.builder()
.name("Multi-Interface Agent")
.description("Agent with multiple interfaces")
.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.GRPC.asString(), "http://localhost:9998", "/grpc-tenant", "1.0"),
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/jsonrpc-tenant", "1.0")))
.build();

@Test
public void shouldNotFindCompatibleTransport() throws A2AClientException {
A2AClientException exception = Assertions.assertThrows(A2AClientException.class,
Expand Down Expand Up @@ -91,4 +135,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