diff --git a/client/base/src/main/java/io/a2a/client/ClientBuilder.java b/client/base/src/main/java/io/a2a/client/ClientBuilder.java index 81c812c91..5c67277c4 100644 --- a/client/base/src/main/java/io/a2a/client/ClientBuilder.java +++ b/client/base/src/main/java/io/a2a/client/ClientBuilder.java @@ -328,15 +328,16 @@ private ClientTransport buildClientTransport() throws A2AClientException { return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface), clientTransportConfig); } - private Map getServerPreferredTransports() throws A2AClientException { - Map serverPreferredTransports = new LinkedHashMap<>(); - if(agentCard.supportedInterfaces() == null || agentCard.supportedInterfaces().isEmpty()) { + private Map getServerInterfacesMap() throws A2AClientException { + List 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 serverInterfacesMap = new LinkedHashMap<>(); + for (AgentInterface iface : serverInterfaces) { + serverInterfacesMap.putIfAbsent(iface.protocolBinding(), iface); } - return serverPreferredTransports; + return serverInterfacesMap; } private List getClientPreferredTransports() { @@ -351,40 +352,38 @@ private List getClientPreferredTransports() { return supportedClientTransports; } - private AgentInterface findBestClientTransport() throws A2AClientException { - // Retrieve transport supported by the A2A server - Map serverPreferredTransports = getServerPreferredTransports(); - - // Retrieve transport configured for this client (using withTransport methods) + // Package-private for testing + AgentInterface findBestClientTransport() throws A2AClientException { + Map serverInterfacesMap = getServerInterfacesMap(); List 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 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; } /** diff --git a/client/base/src/test/java/io/a2a/client/ClientBuilderTest.java b/client/base/src/test/java/io/a2a/client/ClientBuilderTest.java index 3be9d4918..e9a3be0b2 100644 --- a/client/base/src/test/java/io/a2a/client/ClientBuilderTest.java +++ b/client/base/src/test/java/io/a2a/client/ClientBuilderTest.java @@ -1,6 +1,5 @@ package io.a2a.client; - import java.util.Collections; import java.util.List; @@ -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, @@ -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()); + } }