Skip to content

Commit 32d1c89

Browse files
committed
fix: preserve tenant and protocolVersion from AgentInterface in ClientBuilder. Also added unit tests around this behavior. Fixes #772
1 parent ed23f55 commit 32d1c89

2 files changed

Lines changed: 152 additions & 38 deletions

File tree

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

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -328,15 +328,16 @@ 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+
Map<String, AgentInterface> serverInterfacesMap = new LinkedHashMap<>();
337+
for (AgentInterface iface : serverInterfaces) {
338+
serverInterfacesMap.putIfAbsent(iface.protocolBinding(), iface);
338339
}
339-
return serverPreferredTransports;
340+
return serverInterfacesMap;
340341
}
341342

342343
private List<String> getClientPreferredTransports() {
@@ -351,40 +352,38 @@ private List<String> getClientPreferredTransports() {
351352
return supportedClientTransports;
352353
}
353354

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)
355+
// Package-private for testing
356+
AgentInterface findBestClientTransport() throws A2AClientException {
357+
Map<String, AgentInterface> serverInterfacesMap = getServerInterfacesMap();
359358
List<String> clientPreferredTransports = getClientPreferredTransports();
360359

361-
String transportProtocol = null;
362-
String transportUrl = null;
360+
AgentInterface matchedInterface = null;
363361
if (clientConfig.isUseClientPreference()) {
362+
// Client preference: iterate client transports first, find first server match
364363
for (String clientPreferredTransport : clientPreferredTransports) {
365-
if (serverPreferredTransports.containsKey(clientPreferredTransport)) {
366-
transportProtocol = clientPreferredTransport;
367-
transportUrl = serverPreferredTransports.get(transportProtocol);
364+
if (serverInterfacesMap.containsKey(clientPreferredTransport)) {
365+
matchedInterface = serverInterfacesMap.get(clientPreferredTransport);
368366
break;
369367
}
370368
}
371369
} else {
372-
for (Map.Entry<String, String> transport : serverPreferredTransports.entrySet()) {
373-
if (clientPreferredTransports.contains(transport.getKey())) {
374-
transportProtocol = transport.getKey();
375-
transportUrl = transport.getValue();
370+
// Server preference: iterate server interfaces first, find first client match
371+
for (AgentInterface iface : serverInterfacesMap.values()) {
372+
if (clientPreferredTransports.contains(iface.protocolBinding())) {
373+
matchedInterface = iface;
376374
break;
377375
}
378376
}
379377
}
380-
if (transportProtocol == null || transportUrl == null) {
378+
379+
if (matchedInterface == null) {
381380
throw new A2AClientException("No compatible transport found");
382381
}
383-
if (!transportProviderRegistry.containsKey(transportProtocol)) {
384-
throw new A2AClientException("No client available for " + transportProtocol);
382+
if (!transportProviderRegistry.containsKey(matchedInterface.protocolBinding())) {
383+
throw new A2AClientException("No client available for " + matchedInterface.protocolBinding());
385384
}
386385

387-
return new AgentInterface(transportProtocol, transportUrl);
386+
return matchedInterface;
388387
}
389388

390389
/**

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

Lines changed: 129 additions & 14 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

@@ -24,26 +23,71 @@ public class ClientBuilderTest {
2423

2524
private AgentCard card = AgentCard.builder()
2625
.name("Hello World Agent")
27-
.description("Just a hello world agent")
28-
.version("1.0.0")
29-
.documentationUrl("http://example.com/docs")
30-
.capabilities(AgentCapabilities.builder()
31-
.streaming(true)
32-
.pushNotifications(true)
33-
.build())
26+
.description("Just a hello world agent")
27+
.version("1.0.0")
28+
.documentationUrl("http://example.com/docs")
29+
.capabilities(AgentCapabilities.builder()
30+
.streaming(true)
31+
.pushNotifications(true)
32+
.build())
3433
.defaultInputModes(Collections.singletonList("text"))
3534
.defaultOutputModes(Collections.singletonList("text"))
3635
.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()))
36+
.id("hello_world")
37+
.name("Returns hello world")
38+
.description("just returns hello world")
39+
.tags(Collections.singletonList("hello world"))
40+
.examples(List.of("hi", "hello world"))
41+
.build()))
4342
.supportedInterfaces(List.of(
4443
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999")))
4544
.build();
4645

46+
private AgentCard cardWithTenant = AgentCard.builder()
47+
.name("Hello World Agent")
48+
.description("Just a hello world agent")
49+
.version("1.0.0")
50+
.documentationUrl("http://example.com/docs")
51+
.capabilities(AgentCapabilities.builder()
52+
.streaming(true)
53+
.pushNotifications(true)
54+
.build())
55+
.defaultInputModes(Collections.singletonList("text"))
56+
.defaultOutputModes(Collections.singletonList("text"))
57+
.skills(Collections.singletonList(AgentSkill.builder()
58+
.id("hello_world")
59+
.name("Returns hello world")
60+
.description("just returns hello world")
61+
.tags(Collections.singletonList("hello world"))
62+
.examples(List.of("hi", "hello world"))
63+
.build()))
64+
.supportedInterfaces(List.of(
65+
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/default-tenant")))
66+
.build();
67+
68+
private AgentCard cardWithMultipleInterfaces = AgentCard.builder()
69+
.name("Multi-Interface Agent")
70+
.description("Agent with multiple interfaces")
71+
.version("1.0.0")
72+
.documentationUrl("http://example.com/docs")
73+
.capabilities(AgentCapabilities.builder()
74+
.streaming(true)
75+
.pushNotifications(true)
76+
.build())
77+
.defaultInputModes(Collections.singletonList("text"))
78+
.defaultOutputModes(Collections.singletonList("text"))
79+
.skills(Collections.singletonList(AgentSkill.builder()
80+
.id("hello_world")
81+
.name("Returns hello world")
82+
.description("just returns hello world")
83+
.tags(Collections.singletonList("hello world"))
84+
.examples(List.of("hi", "hello world"))
85+
.build()))
86+
.supportedInterfaces(List.of(
87+
new AgentInterface(TransportProtocol.GRPC.asString(), "http://localhost:9998", "/grpc-tenant", "1.0"),
88+
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/jsonrpc-tenant", "1.0")))
89+
.build();
90+
4791
@Test
4892
public void shouldNotFindCompatibleTransport() throws A2AClientException {
4993
A2AClientException exception = Assertions.assertThrows(A2AClientException.class,
@@ -91,4 +135,75 @@ public void shouldCreateClient_differentConfigurations() throws A2AClientExcepti
91135

92136
Assertions.assertNotNull(client);
93137
}
138+
139+
@Test
140+
public void shouldPreserveTenantFromAgentInterface() throws A2AClientException {
141+
ClientBuilder builder = Client
142+
.builder(cardWithTenant)
143+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
144+
145+
AgentInterface selectedInterface = builder.findBestClientTransport();
146+
147+
Assertions.assertEquals("/default-tenant", selectedInterface.tenant());
148+
Assertions.assertEquals("http://localhost:9999", selectedInterface.url());
149+
Assertions.assertEquals(TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding());
150+
}
151+
152+
@Test
153+
public void shouldPreserveProtocolVersionFromAgentInterface() throws A2AClientException {
154+
ClientBuilder builder = Client
155+
.builder(cardWithMultipleInterfaces)
156+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
157+
158+
AgentInterface selectedInterface = builder.findBestClientTransport();
159+
160+
Assertions.assertEquals("/jsonrpc-tenant", selectedInterface.tenant());
161+
Assertions.assertEquals("1.0", selectedInterface.protocolVersion());
162+
}
163+
164+
@Test
165+
public void shouldSelectCorrectInterfaceWithServerPreference() throws A2AClientException {
166+
// Server preference (default): iterates server interfaces in order, picks first that client supports
167+
// cardWithMultipleInterfaces has [GRPC, JSONRPC] - GRPC is first
168+
// Client supports both GRPC and JSONRPC, so GRPC should be selected (server's first choice)
169+
ClientBuilder builder = Client
170+
.builder(cardWithMultipleInterfaces)
171+
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(s -> null))
172+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
173+
174+
AgentInterface selectedInterface = builder.findBestClientTransport();
175+
176+
Assertions.assertEquals(TransportProtocol.GRPC.asString(), selectedInterface.protocolBinding());
177+
Assertions.assertEquals("http://localhost:9998", selectedInterface.url());
178+
Assertions.assertEquals("/grpc-tenant", selectedInterface.tenant());
179+
}
180+
181+
@Test
182+
public void shouldSelectCorrectInterfaceWithClientPreference() throws A2AClientException {
183+
// Client preference: iterates client transports in registration order, picks first that server supports
184+
// Client registers [JSONRPC, GRPC] - JSONRPC is first
185+
// Server supports both, so JSONRPC should be selected (client's first choice)
186+
ClientBuilder builder = Client
187+
.builder(cardWithMultipleInterfaces)
188+
.clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
189+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
190+
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(s -> null));
191+
192+
AgentInterface selectedInterface = builder.findBestClientTransport();
193+
194+
Assertions.assertEquals(TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding());
195+
Assertions.assertEquals("http://localhost:9999", selectedInterface.url());
196+
Assertions.assertEquals("/jsonrpc-tenant", selectedInterface.tenant());
197+
}
198+
199+
@Test
200+
public void shouldPreserveEmptyTenant() throws A2AClientException {
201+
ClientBuilder builder = Client
202+
.builder(card)
203+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
204+
205+
AgentInterface selectedInterface = builder.findBestClientTransport();
206+
207+
Assertions.assertEquals("", selectedInterface.tenant());
208+
}
94209
}

0 commit comments

Comments
 (0)