Skip to content

Commit adfb93e

Browse files
fix: preserve tenant and protocolVersion from AgentInterface in ClientBuilder (#773)
# Description `ClientBuilder.findBestClientTransport()` was creating a new AgentInterface with only protocolBinding and url, discarding tenant and protocolVersion from the matched interface. This broke the intended fallback behavior where the AgentCard's default tenant should be used when no request-level tenant is provided. The fix returns the actual matched AgentInterface from the AgentCard's supportedInterfaces list instead of creating a new one. In addition, I added some tests in order to validate some of this behavior. In order to have the tests actually test this thing, I had to make the `findBestClientTransport()` method package-private. LMK if any of this needs edits or does not match the actual intended behavior and I can make appropriate updates! Fixes #772 --------- Co-authored-by: Luke Irons <luke.irons@genesys.com>
1 parent c8ba7df commit adfb93e

2 files changed

Lines changed: 120 additions & 39 deletions

File tree

client/base/src/main/java/io/a2a/client/ClientBuilder.java

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -328,15 +328,17 @@ private ClientTransport buildClientTransport() throws A2AClientException {
328328
return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface), clientTransportConfig);
329329
}
330330

331-
private Map<String, String> getServerPreferredTransports() throws A2AClientException {
332-
Map<String, String> serverPreferredTransports = new LinkedHashMap<>();
333-
if(agentCard.supportedInterfaces() == null || agentCard.supportedInterfaces().isEmpty()) {
331+
private Map<String, AgentInterface> getServerInterfacesMap() throws A2AClientException {
332+
List<AgentInterface> serverInterfaces = agentCard.supportedInterfaces();
333+
if (serverInterfaces == null || serverInterfaces.isEmpty()) {
334334
throw new A2AClientException("No server interface available in the AgentCard");
335335
}
336-
for (AgentInterface agentInterface : agentCard.supportedInterfaces()) {
337-
serverPreferredTransports.putIfAbsent(agentInterface.protocolBinding(), agentInterface.url());
336+
// If there are multiple interfaces with the same protocol binding, only the first is considered
337+
Map<String, AgentInterface> serverInterfacesMap = new LinkedHashMap<>();
338+
for (AgentInterface iface : serverInterfaces) {
339+
serverInterfacesMap.putIfAbsent(iface.protocolBinding(), iface);
338340
}
339-
return serverPreferredTransports;
341+
return serverInterfacesMap;
340342
}
341343

342344
private List<String> getClientPreferredTransports() {
@@ -351,40 +353,38 @@ private List<String> getClientPreferredTransports() {
351353
return supportedClientTransports;
352354
}
353355

354-
private AgentInterface findBestClientTransport() throws A2AClientException {
355-
// Retrieve transport supported by the A2A server
356-
Map<String, String> serverPreferredTransports = getServerPreferredTransports();
357-
358-
// Retrieve transport configured for this client (using withTransport methods)
356+
// Package-private for testing
357+
AgentInterface findBestClientTransport() throws A2AClientException {
358+
Map<String, AgentInterface> serverInterfacesMap = getServerInterfacesMap();
359359
List<String> clientPreferredTransports = getClientPreferredTransports();
360360

361-
String transportProtocol = null;
362-
String transportUrl = null;
361+
AgentInterface matchedInterface = null;
363362
if (clientConfig.isUseClientPreference()) {
363+
// Client preference: iterate client transports first, find first server match
364364
for (String clientPreferredTransport : clientPreferredTransports) {
365-
if (serverPreferredTransports.containsKey(clientPreferredTransport)) {
366-
transportProtocol = clientPreferredTransport;
367-
transportUrl = serverPreferredTransports.get(transportProtocol);
365+
if (serverInterfacesMap.containsKey(clientPreferredTransport)) {
366+
matchedInterface = serverInterfacesMap.get(clientPreferredTransport);
368367
break;
369368
}
370369
}
371370
} else {
372-
for (Map.Entry<String, String> transport : serverPreferredTransports.entrySet()) {
373-
if (clientPreferredTransports.contains(transport.getKey())) {
374-
transportProtocol = transport.getKey();
375-
transportUrl = transport.getValue();
371+
// Server preference: iterate server interfaces first, find first client match
372+
for (AgentInterface iface : serverInterfacesMap.values()) {
373+
if (clientPreferredTransports.contains(iface.protocolBinding())) {
374+
matchedInterface = iface;
376375
break;
377376
}
378377
}
379378
}
380-
if (transportProtocol == null || transportUrl == null) {
379+
380+
if (matchedInterface == null) {
381381
throw new A2AClientException("No compatible transport found");
382382
}
383-
if (!transportProviderRegistry.containsKey(transportProtocol)) {
384-
throw new A2AClientException("No client available for " + transportProtocol);
383+
if (!transportProviderRegistry.containsKey(matchedInterface.protocolBinding())) {
384+
throw new A2AClientException("No client available for " + matchedInterface.protocolBinding());
385385
}
386386

387-
return new AgentInterface(transportProtocol, transportUrl);
387+
return matchedInterface;
388388
}
389389

390390
/**

client/base/src/test/java/io/a2a/client/ClientBuilderTest.java

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.a2a.client;
22

3-
43
import java.util.Collections;
54
import java.util.List;
65

@@ -22,27 +21,38 @@
2221

2322
public class ClientBuilderTest {
2423

25-
private AgentCard card = AgentCard.builder()
26-
.name("Hello World Agent")
24+
private static AgentCard buildCard(List<AgentInterface> interfaces) {
25+
return AgentCard.builder()
26+
.name("Hello World Agent")
2727
.description("Just a hello world agent")
2828
.version("1.0.0")
2929
.documentationUrl("http://example.com/docs")
3030
.capabilities(AgentCapabilities.builder()
3131
.streaming(true)
3232
.pushNotifications(true)
3333
.build())
34-
.defaultInputModes(Collections.singletonList("text"))
35-
.defaultOutputModes(Collections.singletonList("text"))
36-
.skills(Collections.singletonList(AgentSkill.builder()
37-
.id("hello_world")
38-
.name("Returns hello world")
39-
.description("just returns hello world")
40-
.tags(Collections.singletonList("hello world"))
41-
.examples(List.of("hi", "hello world"))
42-
.build()))
43-
.supportedInterfaces(List.of(
44-
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999")))
45-
.build();
34+
.defaultInputModes(Collections.singletonList("text"))
35+
.defaultOutputModes(Collections.singletonList("text"))
36+
.skills(Collections.singletonList(AgentSkill.builder()
37+
.id("hello_world")
38+
.name("Returns hello world")
39+
.description("just returns hello world")
40+
.tags(Collections.singletonList("hello world"))
41+
.examples(List.of("hi", "hello world"))
42+
.build()))
43+
.supportedInterfaces(interfaces)
44+
.build();
45+
}
46+
47+
private final AgentCard card = buildCard(List.of(
48+
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999")));
49+
50+
private final AgentCard cardWithTenant = buildCard(List.of(
51+
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/default-tenant")));
52+
53+
private final AgentCard cardWithMultipleInterfaces = buildCard(List.of(
54+
new AgentInterface(TransportProtocol.GRPC.asString(), "http://localhost:9998", "/grpc-tenant", "1.0"),
55+
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/jsonrpc-tenant", "1.0")));
4656

4757
@Test
4858
public void shouldNotFindCompatibleTransport() throws A2AClientException {
@@ -91,4 +101,75 @@ public void shouldCreateClient_differentConfigurations() throws A2AClientExcepti
91101

92102
Assertions.assertNotNull(client);
93103
}
104+
105+
@Test
106+
public void shouldPreserveTenantFromAgentInterface() throws A2AClientException {
107+
ClientBuilder builder = Client
108+
.builder(cardWithTenant)
109+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
110+
111+
AgentInterface selectedInterface = builder.findBestClientTransport();
112+
113+
Assertions.assertEquals("/default-tenant", selectedInterface.tenant());
114+
Assertions.assertEquals("http://localhost:9999", selectedInterface.url());
115+
Assertions.assertEquals(TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding());
116+
}
117+
118+
@Test
119+
public void shouldPreserveProtocolVersionFromAgentInterface() throws A2AClientException {
120+
ClientBuilder builder = Client
121+
.builder(cardWithMultipleInterfaces)
122+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
123+
124+
AgentInterface selectedInterface = builder.findBestClientTransport();
125+
126+
Assertions.assertEquals("/jsonrpc-tenant", selectedInterface.tenant());
127+
Assertions.assertEquals("1.0", selectedInterface.protocolVersion());
128+
}
129+
130+
@Test
131+
public void shouldSelectCorrectInterfaceWithServerPreference() throws A2AClientException {
132+
// Server preference (default): iterates server interfaces in order, picks first that client supports
133+
// cardWithMultipleInterfaces has [GRPC, JSONRPC] - GRPC is first
134+
// Client supports both GRPC and JSONRPC, so GRPC should be selected (server's first choice)
135+
ClientBuilder builder = Client
136+
.builder(cardWithMultipleInterfaces)
137+
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(s -> null))
138+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
139+
140+
AgentInterface selectedInterface = builder.findBestClientTransport();
141+
142+
Assertions.assertEquals(TransportProtocol.GRPC.asString(), selectedInterface.protocolBinding());
143+
Assertions.assertEquals("http://localhost:9998", selectedInterface.url());
144+
Assertions.assertEquals("/grpc-tenant", selectedInterface.tenant());
145+
}
146+
147+
@Test
148+
public void shouldSelectCorrectInterfaceWithClientPreference() throws A2AClientException {
149+
// Client preference: iterates client transports in registration order, picks first that server supports
150+
// Client registers [JSONRPC, GRPC] - JSONRPC is first
151+
// Server supports both, so JSONRPC should be selected (client's first choice)
152+
ClientBuilder builder = Client
153+
.builder(cardWithMultipleInterfaces)
154+
.clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
155+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
156+
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(s -> null));
157+
158+
AgentInterface selectedInterface = builder.findBestClientTransport();
159+
160+
Assertions.assertEquals(TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding());
161+
Assertions.assertEquals("http://localhost:9999", selectedInterface.url());
162+
Assertions.assertEquals("/jsonrpc-tenant", selectedInterface.tenant());
163+
}
164+
165+
@Test
166+
public void shouldPreserveEmptyTenant() throws A2AClientException {
167+
ClientBuilder builder = Client
168+
.builder(card)
169+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
170+
171+
AgentInterface selectedInterface = builder.findBestClientTransport();
172+
173+
Assertions.assertEquals("", selectedInterface.tenant());
174+
}
94175
}

0 commit comments

Comments
 (0)