supportedProtocolVersions)
+ throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(A2AHttpClientFactory.create(), agentUrl, supportedProtocolVersions);
+ }
+
/**
* Retrieve the agent card using a custom HTTP client.
*
@@ -315,6 +326,14 @@ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl)
return getAgentCard(httpClient, agentUrl, null, null);
}
+ /**
+ * Retrieves an agent card with a custom HTTP client and explicit protocol-version policy.
+ */
+ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl,
+ Set supportedProtocolVersions) throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(httpClient, agentUrl, null, null, supportedProtocolVersions);
+ }
+
/**
* Retrieve the agent card with custom path and authentication.
*
@@ -360,6 +379,16 @@ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, M
return getAgentCard(A2AHttpClientFactory.create(), agentUrl, relativeCardPath, authHeaders);
}
+ /**
+ * Retrieves an agent card with custom endpoint/authentication settings and explicit protocol versions.
+ */
+ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath,
+ Map authHeaders, Set supportedProtocolVersions)
+ throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(A2AHttpClientFactory.create(), agentUrl, relativeCardPath, authHeaders,
+ supportedProtocolVersions);
+ }
+
/**
* Retrieve the agent card with full customization options.
*
@@ -393,11 +422,21 @@ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, M
* @throws org.a2aproject.sdk.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema
*/
public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(httpClient, agentUrl, relativeCardPath, authHeaders, Set.of("1.0"));
+ }
+
+ /**
+ * Retrieves an agent card with full HTTP, endpoint, authentication, and protocol-version settings.
+ */
+ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath,
+ Map authHeaders, Set supportedProtocolVersions)
+ throws A2AClientError, A2AClientJSONError {
A2ACardResolver resolver = A2ACardResolver.builder()
.httpClient(httpClient)
.baseUrl(agentUrl)
.agentCardPath(relativeCardPath)
.authHeaders(authHeaders)
+ .supportedProtocolVersions(supportedProtocolVersions)
.build();
return resolver.getAgentCard();
}
diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/ClientBuilder.java b/client/base/src/main/java/org/a2aproject/sdk/client/ClientBuilder.java
index 42614c262..7526cd730 100644
--- a/client/base/src/main/java/org/a2aproject/sdk/client/ClientBuilder.java
+++ b/client/base/src/main/java/org/a2aproject/sdk/client/ClientBuilder.java
@@ -17,6 +17,7 @@
import org.a2aproject.sdk.client.transport.spi.ClientTransportConfigBuilder;
import org.a2aproject.sdk.client.transport.spi.ClientTransportProvider;
import org.a2aproject.sdk.client.transport.spi.ClientTransportWrapper;
+import org.a2aproject.sdk.client.http.A2ACardResolver;
import org.a2aproject.sdk.spec.A2AClientException;
import org.a2aproject.sdk.spec.AgentCard;
import org.a2aproject.sdk.spec.AgentInterface;
@@ -102,6 +103,7 @@ public class ClientBuilder {
private static final Map>> transportProviderRegistry = new HashMap<>();
private static final Map, String> transportProtocolMapping = new HashMap<>();
+ private static final Map versionedTransportProviderRegistry = new HashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(ClientBuilder.class);
static {
@@ -110,6 +112,14 @@ public class ClientBuilder {
transportProviderRegistry.put(transport.getTransportProtocol(), transport);
transportProtocolMapping.put(transport.getTransportProtocolClass(), transport.getTransportProtocol());
}
+ ServiceLoader versionedLoader = ServiceLoader.load(VersionedClientTransportProvider.class);
+ for (VersionedClientTransportProvider provider : versionedLoader) {
+ VersionKey key = new VersionKey(provider.protocolBinding(),
+ A2ACardResolver.normalizeSupportedProtocolVersion(provider.protocolVersion()));
+ if (versionedTransportProviderRegistry.putIfAbsent(key, provider) != null) {
+ throw new IllegalStateException("Duplicate versioned client transport provider for " + key);
+ }
+ }
}
private final AgentCard agentCard;
@@ -286,8 +296,10 @@ public ClientBuilder clientConfig(@NonNull ClientConfig clientConfig) {
*
* - If {@link ClientConfig#isUseClientPreference()} is {@code true}, iterate through
* client transports in registration order and select the first one the server supports
- * - Otherwise, iterate through server interfaces in preference order (first entry
- * in {@link AgentCard#supportedInterfaces()}) and select the first one the client supports
+ * - Otherwise, iterate through server interfaces in their declared preference order (first entry
+ * in {@link AgentCard#supportedInterfaces()}) and select the first one the client supports.
+ * This order is preserved across protocol versions; a 1.0 interface does not supersede an
+ * earlier compatible legacy interface.
*
*
* Important: At least one transport must be configured via {@link #withTransport},
@@ -311,34 +323,65 @@ private ClientTransport buildClientTransport() throws A2AClientException {
// Get the preferred transport
AgentInterface agentInterface = findBestClientTransport();
- // Get the transport provider associated with the protocol
- ClientTransportProvider clientTransportProvider = transportProviderRegistry.get(agentInterface.protocolBinding());
- if (clientTransportProvider == null) {
- throw new A2AClientException("No client available for " + agentInterface.protocolBinding());
- }
- Class extends ClientTransport> transportProtocolClass = clientTransportProvider.getTransportProtocolClass();
-
- // Retrieve the configuration associated with the preferred transport
- ClientTransportConfig extends ClientTransport> clientTransportConfig = clientTransports.get(transportProtocolClass);
-
- if (clientTransportConfig == null) {
- throw new A2AClientException("Missing required TransportConfig for " + agentInterface.protocolBinding());
+ String protocolVersion = normalizeInterfaceVersionForClient(agentInterface);
+ Class extends ClientTransport> transportProtocolClass;
+ ClientTransportConfig extends ClientTransport> clientTransportConfig;
+ ClientTransport transport;
+ if ("1.0".equals(protocolVersion)) {
+ ClientTransportProvider clientTransportProvider = transportProviderRegistry.get(agentInterface.protocolBinding());
+ if (clientTransportProvider == null) {
+ throw new A2AClientException("No client available for " + agentInterface.protocolBinding());
+ }
+ transportProtocolClass = clientTransportProvider.getTransportProtocolClass();
+ clientTransportConfig = clientTransports.get(transportProtocolClass);
+ if (clientTransportConfig == null) {
+ throw new A2AClientException("Missing required TransportConfig for " + agentInterface.protocolBinding());
+ }
+ transport = clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface);
+ } else {
+ VersionedClientTransportProvider provider = versionedTransportProviderRegistry.get(
+ new VersionKey(agentInterface.protocolBinding(), protocolVersion));
+ if (provider == null) {
+ throw new A2AClientException("No client available for " + agentInterface.protocolBinding()
+ + " protocol version " + protocolVersion);
+ }
+ transportProtocolClass = provider.configuredTransportClass();
+ clientTransportConfig = clientTransports.get(transportProtocolClass);
+ if (clientTransportConfig == null) {
+ throw new A2AClientException("Missing required TransportConfig for " + agentInterface.protocolBinding());
+ }
+ transport = provider.create(clientTransportConfig, agentCard, agentInterface);
}
- return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface), clientTransportConfig);
+ return wrap(transport, clientTransportConfig);
}
- private Map getServerInterfacesMap() throws A2AClientException {
+ /**
+ * Returns supported interfaces in the AgentCard's declared order, omitting unsupported versions
+ * and duplicate binding/version pairs. Preserving this order is required for server-preference
+ * negotiation.
+ */
+ private List getServerInterfaces() throws A2AClientException {
List serverInterfaces = agentCard.supportedInterfaces();
if (serverInterfaces == null || serverInterfaces.isEmpty()) {
throw new A2AClientException("No server interface available in the AgentCard");
}
- // If there are multiple interfaces with the same protocol binding, only the first is considered
- Map serverInterfacesMap = new LinkedHashMap<>();
+ List ordered = new ArrayList<>();
for (AgentInterface iface : serverInterfaces) {
- serverInterfacesMap.putIfAbsent(iface.protocolBinding(), iface);
+ final String version;
+ try {
+ version = normalizeInterfaceVersion(iface);
+ } catch (IllegalArgumentException e) {
+ LOGGER.debug("Ignoring unsupported protocol version '{}' for {}", iface.protocolVersion(),
+ iface.protocolBinding());
+ continue;
+ }
+ if (ordered.stream().noneMatch(existing -> existing.protocolBinding().equals(iface.protocolBinding())
+ && normalizeInterfaceVersion(existing).equals(version))) {
+ ordered.add(iface);
+ }
}
- return serverInterfacesMap;
+ return ordered;
}
private List getClientPreferredTransports() {
@@ -355,22 +398,36 @@ private List getClientPreferredTransports() {
// Package-private for testing
AgentInterface findBestClientTransport() throws A2AClientException {
- Map serverInterfacesMap = getServerInterfacesMap();
+ final List serverInterfaces;
+ try {
+ serverInterfaces = getServerInterfaces();
+ } catch (IllegalArgumentException e) {
+ throw new A2AClientException("Unsupported protocol version in AgentCard", e);
+ }
List clientPreferredTransports = getClientPreferredTransports();
AgentInterface matchedInterface = null;
if (clientConfig.isUseClientPreference()) {
// Client preference: iterate client transports first, find first server match
+ List nativeInterfaces = serverInterfaces.stream()
+ .filter(iface -> "1.0".equals(normalizeInterfaceVersion(iface))
+ && clientPreferredTransports.contains(iface.protocolBinding())
+ && hasTransportProvider(iface))
+ .toList();
+ List preferredInterfaces = nativeInterfaces.isEmpty() ? serverInterfaces : nativeInterfaces;
for (String clientPreferredTransport : clientPreferredTransports) {
- if (serverInterfacesMap.containsKey(clientPreferredTransport)) {
- matchedInterface = serverInterfacesMap.get(clientPreferredTransport);
- break;
+ for (AgentInterface iface : preferredInterfaces) {
+ if (clientPreferredTransport.equals(iface.protocolBinding()) && hasTransportProvider(iface)) {
+ matchedInterface = iface;
+ break;
+ }
}
+ if (matchedInterface != null) break;
}
} else {
// Server preference: iterate server interfaces first, find first client match
- for (AgentInterface iface : serverInterfacesMap.values()) {
- if (clientPreferredTransports.contains(iface.protocolBinding())) {
+ for (AgentInterface iface : serverInterfaces) {
+ if (clientPreferredTransports.contains(iface.protocolBinding()) && hasTransportProvider(iface)) {
matchedInterface = iface;
break;
}
@@ -378,15 +435,76 @@ AgentInterface findBestClientTransport() throws A2AClientException {
}
if (matchedInterface == null) {
+ for (AgentInterface iface : serverInterfaces) {
+ String adapter = missingCompatibilityAdapter(iface);
+ if (adapter != null) {
+ throw new A2AClientException(iface.protocolBinding() + " "
+ + normalizeInterfaceVersion(iface) + " requires "
+ + adapter);
+ }
+ }
throw new A2AClientException("No compatible transport found");
}
- if (!transportProviderRegistry.containsKey(matchedInterface.protocolBinding())) {
- throw new A2AClientException("No client available for " + matchedInterface.protocolBinding());
+ String version = normalizeInterfaceVersionForClient(matchedInterface);
+ if ("1.0".equals(version)) {
+ if (!transportProviderRegistry.containsKey(matchedInterface.protocolBinding())) {
+ throw new A2AClientException("No client available for " + matchedInterface.protocolBinding());
+ }
+ } else if (!versionedTransportProviderRegistry.containsKey(
+ new VersionKey(matchedInterface.protocolBinding(), version))) {
+ throw new A2AClientException("No client available for " + matchedInterface.protocolBinding()
+ + " protocol version " + version);
}
return matchedInterface;
}
+ private static String normalizeInterfaceVersion(AgentInterface agentInterface) {
+ return A2ACardResolver.normalizeSupportedProtocolVersion(agentInterface.protocolVersion());
+ }
+
+ private static boolean hasTransportProvider(AgentInterface agentInterface) {
+ String version = normalizeInterfaceVersion(agentInterface);
+ return "1.0".equals(version)
+ ? transportProviderRegistry.containsKey(agentInterface.protocolBinding())
+ : versionedTransportProviderRegistry.containsKey(
+ new VersionKey(agentInterface.protocolBinding(), version));
+ }
+
+ private @Nullable String missingCompatibilityAdapter(AgentInterface agentInterface) {
+ String version = normalizeInterfaceVersion(agentInterface);
+ if ("1.0".equals(version)
+ || hasTransportProvider(agentInterface)
+ || !isConfiguredBinding(agentInterface.protocolBinding())) {
+ return null;
+ }
+ return switch (agentInterface.protocolBinding()) {
+ case "JSONRPC" -> "a2a-java-sdk-compat-0.3-client-adapter-jsonrpc";
+ case "HTTP+JSON" -> "a2a-java-sdk-compat-0.3-client-adapter-rest";
+ case "GRPC" -> "a2a-java-sdk-compat-0.3-client-adapter-grpc";
+ default -> "a2a-java-sdk-compat-0.3-client-adapter-" + agentInterface.protocolBinding().toLowerCase();
+ };
+ }
+
+ private boolean isConfiguredBinding(String binding) {
+ if (clientTransports.isEmpty() && TransportProtocol.JSONRPC.asString().equals(binding)) {
+ return true;
+ }
+ return clientTransports.keySet().stream()
+ .anyMatch(clazz -> binding.equals(transportProtocolMapping.get(clazz)));
+ }
+
+ private static String normalizeInterfaceVersionForClient(AgentInterface agentInterface) throws A2AClientException {
+ try {
+ return normalizeInterfaceVersion(agentInterface);
+ } catch (IllegalArgumentException e) {
+ throw new A2AClientException("Unsupported protocol version '" + agentInterface.protocolVersion() + "'", e);
+ }
+ }
+
+ private record VersionKey(String binding, String version) {
+ }
+
/**
* Wraps the transport with all available transport wrappers discovered via ServiceLoader.
* Wrappers are applied in reverse priority order (lowest priority first) to build a stack
diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/VersionedClientTransportProvider.java b/client/base/src/main/java/org/a2aproject/sdk/client/VersionedClientTransportProvider.java
new file mode 100644
index 000000000..23f64d1d1
--- /dev/null
+++ b/client/base/src/main/java/org/a2aproject/sdk/client/VersionedClientTransportProvider.java
@@ -0,0 +1,19 @@
+package org.a2aproject.sdk.client;
+
+import org.a2aproject.sdk.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+
+/** Provider SPI for client transports targeting a protocol version other than 1.0. */
+public interface VersionedClientTransportProvider {
+ String protocolBinding();
+
+ String protocolVersion();
+
+ Class extends ClientTransport> configuredTransportClass();
+
+ ClientTransport create(ClientTransportConfig> config, AgentCard card, AgentInterface agentInterface)
+ throws A2AClientException;
+}
diff --git a/client/base/src/test/java/org/a2aproject/sdk/client/VersionedClientTransportProviderTest.java b/client/base/src/test/java/org/a2aproject/sdk/client/VersionedClientTransportProviderTest.java
new file mode 100644
index 000000000..34fc14ea3
--- /dev/null
+++ b/client/base/src/test/java/org/a2aproject/sdk/client/VersionedClientTransportProviderTest.java
@@ -0,0 +1,170 @@
+package org.a2aproject.sdk.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.lang.reflect.Proxy;
+import java.util.List;
+
+import org.a2aproject.sdk.client.config.ClientConfig;
+import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport;
+import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransportConfigBuilder;
+import org.a2aproject.sdk.client.transport.grpc.GrpcTransport;
+import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder;
+import org.a2aproject.sdk.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.AgentCapabilities;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+import org.a2aproject.sdk.spec.AgentSkill;
+import org.junit.jupiter.api.Test;
+
+class VersionedClientTransportProviderTest {
+ @Test
+ void selectsVersionedProviderForPatchFormAndUsesOrdinaryConfig() throws Exception {
+ AgentCard card = AgentCard.builder()
+ .name("agent").description("agent").version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of(AgentSkill.builder().id("skill").name("skill").description("skill")
+ .tags(List.of("tag")).build()))
+ .supportedInterfaces(List.of(new AgentInterface("JSONRPC", "http://example.test", null, "0.3.0")))
+ .build();
+
+ ClientBuilder builder = Client.builder(card)
+ .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
+
+ assertEquals("0.3.0", builder.findBestClientTransport().protocolVersion());
+ assertNotNull(builder.build());
+ }
+
+ @Test
+ void rejectsUnknownProtocolVersionBeforeNativeFallback() {
+ AgentCard card = AgentCard.builder()
+ .name("agent").description("agent").version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of(AgentSkill.builder().id("skill").name("skill").description("skill")
+ .tags(List.of("tag")).build()))
+ .supportedInterfaces(List.of(new AgentInterface("JSONRPC", "http://example.test", null, "0.2.9")))
+ .build();
+
+ assertThrows(A2AClientException.class, () -> Client.builder(card).findBestClientTransport());
+ }
+
+ @Test
+ void preservesAgentCardOrderAcrossProtocolVersionsWithServerPreference() throws Exception {
+ AgentCard card = AgentCard.builder()
+ .name("agent").description("agent").version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of(AgentSkill.builder().id("skill").name("skill").description("skill")
+ .tags(List.of("tag")).build()))
+ .supportedInterfaces(List.of(
+ new AgentInterface("JSONRPC", "http://legacy.example", null, "0.3"),
+ new AgentInterface("GRPC", "http://native.example", null, "1.0")))
+ .build();
+
+ ClientBuilder builder = Client.builder(card)
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
+ .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> null));
+
+ assertEquals("JSONRPC", builder.findBestClientTransport().protocolBinding());
+ }
+
+ @Test
+ void selectsNativeInterfaceBeforeLegacyInterfaceWithClientPreference() throws Exception {
+ AgentCard card = AgentCard.builder()
+ .name("agent").description("agent").version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of(AgentSkill.builder().id("skill").name("skill").description("skill")
+ .tags(List.of("tag")).build()))
+ .supportedInterfaces(List.of(
+ new AgentInterface("JSONRPC", "http://legacy.example", null, "0.3"),
+ new AgentInterface("GRPC", "http://native.example", null, "1.0")))
+ .build();
+
+ ClientBuilder builder = Client.builder(card)
+ .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
+ .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> null));
+
+ assertEquals("GRPC", builder.findBestClientTransport().protocolBinding());
+ }
+
+ @Test
+ void fallsBackToLegacyInterfaceWhenNoNativeInterfaceUsesAConfiguredTransport() throws Exception {
+ AgentCard card = AgentCard.builder()
+ .name("agent").description("agent").version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of(AgentSkill.builder().id("skill").name("skill").description("skill")
+ .tags(List.of("tag")).build()))
+ .supportedInterfaces(List.of(
+ new AgentInterface("GRPC", "http://native.example", null, "1.0"),
+ new AgentInterface("JSONRPC", "http://legacy.example", null, "0.3")))
+ .build();
+
+ ClientBuilder builder = Client.builder(card)
+ .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
+
+ assertEquals("JSONRPC", builder.findBestClientTransport().protocolBinding());
+ assertEquals("0.3", builder.findBestClientTransport().protocolVersion());
+ }
+
+ @Test
+ void skipsLegacyInterfaceWhoseBindingAdapterIsNotInstalled() throws Exception {
+ AgentCard card = AgentCard.builder()
+ .name("agent").description("agent").version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of(AgentSkill.builder().id("skill").name("skill").description("skill")
+ .tags(List.of("tag")).build()))
+ .supportedInterfaces(List.of(
+ new AgentInterface("GRPC", "http://grpc.example", null, "0.3"),
+ new AgentInterface("JSONRPC", "http://jsonrpc.example", null, "0.3")))
+ .build();
+
+ ClientBuilder builder = Client.builder(card)
+ .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> null))
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
+
+ assertEquals("JSONRPC", builder.findBestClientTransport().protocolBinding());
+ }
+
+ @Test
+ void ignoresUnknownInterfaceVersionWhenACompatibleInterfaceExists() throws Exception {
+ AgentCard card = AgentCard.builder()
+ .name("agent").description("agent").version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of(AgentSkill.builder().id("skill").name("skill").description("skill")
+ .tags(List.of("tag")).build()))
+ .supportedInterfaces(List.of(
+ new AgentInterface("JSONRPC", "http://future.example", null, "2.0"),
+ new AgentInterface("GRPC", "http://grpc.example", null, "1.0")))
+ .build();
+
+ ClientBuilder builder = Client.builder(card)
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
+ .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> null));
+
+ assertEquals("GRPC", builder.findBestClientTransport().protocolBinding());
+ }
+
+ public static final class FakeVersionedProvider implements VersionedClientTransportProvider {
+ @Override public String protocolBinding() { return "JSONRPC"; }
+ @Override public String protocolVersion() { return "0.3"; }
+ @Override public Class extends ClientTransport> configuredTransportClass() { return JSONRPCTransport.class; }
+ @Override public ClientTransport create(ClientTransportConfig> config, AgentCard card,
+ AgentInterface agentInterface) throws A2AClientException {
+ return (ClientTransport) Proxy.newProxyInstance(ClientTransport.class.getClassLoader(),
+ new Class>[] {ClientTransport.class}, (proxy, method, args) -> null);
+ }
+ }
+}
diff --git a/client/base/src/test/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider b/client/base/src/test/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider
new file mode 100644
index 000000000..7d9ea7bb8
--- /dev/null
+++ b/client/base/src/test/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider
@@ -0,0 +1 @@
+org.a2aproject.sdk.client.VersionedClientTransportProviderTest$FakeVersionedProvider
diff --git a/compat-0.3/client/adapter-grpc/pom.xml b/compat-0.3/client/adapter-grpc/pom.xml
new file mode 100644
index 000000000..92638bf54
--- /dev/null
+++ b/compat-0.3/client/adapter-grpc/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.3.3.Final-SNAPSHOT
+ ../../pom.xml
+
+ a2a-java-sdk-compat-0.3-client-adapter-grpc
+ Java SDK A2A Compat 0.3 Client Adapter: gRPC
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-adapter
+
+
+ ${project.groupId}
+ a2a-java-sdk-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-client-transport-grpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-spec-grpc
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ io.grpc
+ grpc-inprocess
+ test
+
+
+
diff --git a/compat-0.3/client/adapter-grpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransport.java b/compat-0.3/client/adapter-grpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransport.java
new file mode 100644
index 000000000..74a2d0529
--- /dev/null
+++ b/compat-0.3/client/adapter-grpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransport.java
@@ -0,0 +1,186 @@
+package org.a2aproject.sdk.compat03.client.adapter.grpc;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientCallContextMapper;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportBase;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportSupport;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03InterceptorSupport;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3;
+import org.a2aproject.sdk.grpc.utils.ProtoUtils;
+import org.a2aproject.sdk.spec.A2AMethods;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.CancelTaskParams;
+import org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.EventKind;
+import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.StreamingEventKind;
+import org.a2aproject.sdk.spec.Task;
+import org.a2aproject.sdk.spec.TaskIdParams;
+import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+import org.jspecify.annotations.Nullable;
+
+/** 1.0 client facade backed by the legacy gRPC transport. */
+public final class GrpcCompat03ClientTransport extends Compat03ClientTransportBase {
+ public GrpcCompat03ClientTransport(GrpcTransport_v0_3 delegate, AgentCard card,
+ List interceptors) {
+ super(delegate, card, interceptors);
+ }
+
+ @Override
+ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateMessageSend(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SEND_MESSAGE_METHOD,
+ ProtoUtils.ToProto.sendMessageRequest(request), org.a2aproject.sdk.grpc.SendMessageRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(delegate.sendMessage(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.messageSendParams(payloadPayload(payload))),
+ legacyContext(context, payload))));
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateMessageSend(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SEND_STREAMING_MESSAGE_METHOD,
+ ProtoUtils.ToProto.sendMessageRequest(request), org.a2aproject.sdk.grpc.SendMessageRequest.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.sendMessageStreaming(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.messageSendParams(payloadPayload(payload))),
+ event -> events.accept(Compat03ClientTransportSupport.toV10(event)),
+ Compat03ClientTransportSupport.mapAsyncError(errors), legacyContext(context, payload)));
+ }
+
+ @Override
+ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateTaskQuery(request);
+ PayloadAndHeaders payload = apply(A2AMethods.GET_TASK_METHOD, ProtoUtils.ToProto.getTaskRequest(request),
+ org.a2aproject.sdk.grpc.GetTaskRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> TaskMapper_v0_3.INSTANCE.toV10(delegate.getTask(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.taskQueryParams(
+ taskQueryPayload(payload))), legacyContext(context, payload))));
+ }
+
+ @Override
+ public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateCancel(request);
+ PayloadAndHeaders payload = apply(A2AMethods.CANCEL_TASK_METHOD, ProtoUtils.ToProto.cancelTaskRequest(request),
+ org.a2aproject.sdk.grpc.CancelTaskRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> TaskMapper_v0_3.INSTANCE.toV10(delegate.cancelTask(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.cancelTaskParams(cancelPayload(payload))),
+ legacyContext(context, payload))));
+ }
+
+ @Override
+ public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validatePushConfig(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ ProtoUtils.ToProto.taskPushNotificationConfig(request), org.a2aproject.sdk.grpc.TaskPushNotificationConfig.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(
+ delegate.setTaskPushNotificationConfiguration(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.taskPushNotificationConfig(pushConfigPayload(payload))),
+ legacyContext(context, payload))));
+ }
+
+ @Override
+ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateGetPush(request);
+ PayloadAndHeaders payload = apply(A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ ProtoUtils.ToProto.getTaskPushNotificationConfigRequest(request),
+ org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(
+ delegate.getTaskPushNotificationConfiguration(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.getTaskPushNotificationConfigParams(getPushPayload(payload))),
+ legacyContext(context, payload))));
+ }
+
+ @Override
+ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validatePushList(request);
+ PayloadAndHeaders payload = apply(A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ ProtoUtils.ToProto.listTaskPushNotificationConfigsRequest(request),
+ org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10PushList(
+ delegate.listTaskPushNotificationConfigurations(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.listTaskPushNotificationConfigsParams(listPayload(payload))),
+ legacyContext(context, payload))));
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateDeletePush(request);
+ PayloadAndHeaders payload = apply(A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ ProtoUtils.ToProto.deleteTaskPushNotificationConfigRequest(request),
+ org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.deleteTaskPushNotificationConfigurations(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.deleteTaskPushNotificationConfigParams(
+ deletePayload(payload))), legacyContext(context, payload)));
+ }
+
+ @Override
+ public void subscribeToTask(TaskIdParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateTenant("subscribeToTask", request.tenant());
+ PayloadAndHeaders payload = apply(A2AMethods.SUBSCRIBE_TO_TASK_METHOD,
+ ProtoUtils.ToProto.subscribeToTaskRequest(request), org.a2aproject.sdk.grpc.SubscribeToTaskRequest.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.resubscribe(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.taskIdParams(subscribePayload(payload))),
+ event -> events.accept(Compat03ClientTransportSupport.toV10(event)),
+ Compat03ClientTransportSupport.mapAsyncError(errors), legacyContext(context, payload)));
+ }
+
+ private PayloadAndHeaders apply(String method, Object payload, Class> expected,
+ @Nullable ClientCallContext context) {
+ return Compat03InterceptorSupport.apply(interceptors, method, payload, agentCard, context, expected);
+ }
+
+ private static org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3 legacyContext(
+ @Nullable ClientCallContext original, PayloadAndHeaders payload) {
+ return Compat03ClientCallContextMapper.toV03(
+ new ClientCallContext(original == null ? Map.of() : original.getState(), payload.getHeaders()));
+ }
+
+ private static org.a2aproject.sdk.grpc.SendMessageRequest payloadPayload(PayloadAndHeaders payload) {
+ return (org.a2aproject.sdk.grpc.SendMessageRequest) payload.getPayload();
+ }
+
+ private static org.a2aproject.sdk.grpc.GetTaskRequest taskQueryPayload(PayloadAndHeaders payload) {
+ return (org.a2aproject.sdk.grpc.GetTaskRequest) payload.getPayload();
+ }
+
+ private static org.a2aproject.sdk.grpc.CancelTaskRequest cancelPayload(PayloadAndHeaders payload) {
+ return (org.a2aproject.sdk.grpc.CancelTaskRequest) payload.getPayload();
+ }
+
+ private static org.a2aproject.sdk.grpc.TaskPushNotificationConfig pushConfigPayload(PayloadAndHeaders payload) {
+ return (org.a2aproject.sdk.grpc.TaskPushNotificationConfig) payload.getPayload();
+ }
+
+ private static org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest getPushPayload(PayloadAndHeaders payload) {
+ return (org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest) payload.getPayload();
+ }
+
+ private static org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest listPayload(PayloadAndHeaders payload) {
+ return (org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest) payload.getPayload();
+ }
+
+ private static org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest deletePayload(PayloadAndHeaders payload) {
+ return (org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest) payload.getPayload();
+ }
+
+ private static org.a2aproject.sdk.grpc.SubscribeToTaskRequest subscribePayload(PayloadAndHeaders payload) {
+ return (org.a2aproject.sdk.grpc.SubscribeToTaskRequest) payload.getPayload();
+ }
+}
diff --git a/compat-0.3/client/adapter-grpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransportProvider.java b/compat-0.3/client/adapter-grpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransportProvider.java
new file mode 100644
index 000000000..17e4b6ed0
--- /dev/null
+++ b/compat-0.3/client/adapter-grpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransportProvider.java
@@ -0,0 +1,48 @@
+package org.a2aproject.sdk.compat03.client.adapter.grpc;
+
+import java.util.Objects;
+
+import org.a2aproject.sdk.client.VersionedClientTransportProvider;
+import org.a2aproject.sdk.client.transport.grpc.GrpcTransport;
+import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfig;
+import org.a2aproject.sdk.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportBase;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportSupport;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+
+/** ServiceLoader provider for the optional gRPC 0.3 adapter. */
+public final class GrpcCompat03ClientTransportProvider implements VersionedClientTransportProvider {
+ @Override
+ public String protocolBinding() {
+ return "GRPC";
+ }
+
+ @Override
+ public String protocolVersion() {
+ return "0.3";
+ }
+
+ @Override
+ public Class extends ClientTransport> configuredTransportClass() {
+ return GrpcTransport.class;
+ }
+
+ @Override
+ public ClientTransport create(ClientTransportConfig> config, AgentCard card, AgentInterface agentInterface)
+ throws A2AClientException {
+ Compat03ClientTransportSupport.validateAgentInterfaceTenant(agentInterface.tenant());
+ if (!(config instanceof GrpcTransportConfig grpcConfig)) {
+ throw new A2AClientException("Expected GrpcTransportConfig for the gRPC 0.3 adapter");
+ }
+ Compat03ClientTransportSupport.validateConfig(grpcConfig);
+ GrpcTransport_v0_3 legacy = new GrpcTransport_v0_3(
+ Objects.requireNonNull(grpcConfig.getChannelFactory().apply(agentInterface.url()),
+ "channelFactory returned null"),
+ Compat03ClientTransportBase.legacyCard(card));
+ return new GrpcCompat03ClientTransport(legacy, card, grpcConfig.getInterceptors());
+ }
+}
diff --git a/compat-0.3/client/adapter-grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider b/compat-0.3/client/adapter-grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider
new file mode 100644
index 000000000..78db0eb42
--- /dev/null
+++ b/compat-0.3/client/adapter-grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider
@@ -0,0 +1 @@
+org.a2aproject.sdk.compat03.client.adapter.grpc.GrpcCompat03ClientTransportProvider
diff --git a/compat-0.3/client/adapter-grpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransportProviderTest.java b/compat-0.3/client/adapter-grpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransportProviderTest.java
new file mode 100644
index 000000000..ca3aa4917
--- /dev/null
+++ b/compat-0.3/client/adapter-grpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransportProviderTest.java
@@ -0,0 +1,119 @@
+package org.a2aproject.sdk.compat03.client.adapter.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.a2aproject.sdk.client.transport.grpc.GrpcTransport;
+import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder;
+import org.a2aproject.sdk.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc;
+import org.a2aproject.sdk.compat03.grpc.Message;
+import org.a2aproject.sdk.compat03.grpc.Part;
+import org.a2aproject.sdk.compat03.grpc.Role;
+import org.a2aproject.sdk.compat03.grpc.SendMessageResponse;
+import org.a2aproject.sdk.spec.AgentCapabilities;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+import org.a2aproject.sdk.spec.AgentSkill;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.TextPart;
+import org.a2aproject.sdk.spec.TransportProtocol;
+import io.grpc.ManagedChannel;
+import io.grpc.Server;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.stub.StreamObserver;
+
+import org.junit.jupiter.api.Test;
+
+class GrpcCompat03ClientTransportProviderTest {
+ @Test
+ void providerTargetsLegacyGrpcBindingAndVersion() {
+ GrpcCompat03ClientTransportProvider provider = new GrpcCompat03ClientTransportProvider();
+
+ assertEquals(TransportProtocol.GRPC.asString(), provider.protocolBinding());
+ assertEquals("0.3", provider.protocolVersion());
+ assertEquals(GrpcTransport.class, provider.configuredTransportClass());
+ }
+
+ @Test
+ void sendsThroughLegacyServiceAndDoesNotOwnCallerChannel() throws Exception {
+ String name = InProcessServerBuilder.generateName();
+ AtomicBoolean called = new AtomicBoolean();
+ Server server = InProcessServerBuilder.forName(name).directExecutor()
+ .addService(new A2AServiceGrpc.A2AServiceImplBase() {
+ @Override
+ public void sendMessage(org.a2aproject.sdk.compat03.grpc.SendMessageRequest request,
+ StreamObserver responseObserver) {
+ called.set(true);
+ responseObserver.onNext(SendMessageResponse.newBuilder().setMsg(Message.newBuilder()
+ .setMessageId("response")
+ .setRole(Role.ROLE_AGENT)
+ .addContent(Part.newBuilder().setText("hello").build())
+ .build()).build());
+ responseObserver.onCompleted();
+ }
+ }).build().start();
+ ManagedChannel channel = InProcessChannelBuilder.forName(name).directExecutor().build();
+ AgentCard card = card(name);
+ ClientTransport transport = new GrpcCompat03ClientTransportProvider().create(
+ new GrpcTransportConfigBuilder().channelFactory(ignored -> channel).build(), card,
+ card.supportedInterfaces().get(0));
+ try {
+ var result = transport.sendMessage(new MessageSendParams(
+ new org.a2aproject.sdk.spec.Message(org.a2aproject.sdk.spec.Message.Role.ROLE_USER,
+ List.of(new TextPart("hello")), "request", null, null, null, null, null),
+ null, null, null), null);
+ assertEquals("response", ((org.a2aproject.sdk.spec.Message) result).messageId());
+ assertFalse(channel.isShutdown());
+ assertEquals(true, called.get());
+ } finally {
+ transport.close();
+ channel.shutdownNow();
+ server.shutdownNow();
+ }
+ }
+
+ @Test
+ void rejectsTenantOnLegacyAgentInterface() {
+ AgentCard card = cardWithInterfaceTenant();
+
+ org.a2aproject.sdk.spec.A2AClientException exception = assertThrows(org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> new GrpcCompat03ClientTransportProvider().create(null, card, card.supportedInterfaces().get(0)));
+
+ assertTrue(exception.getMessage().contains("tenant"));
+ }
+
+ private static AgentCard card(String endpoint) {
+ return AgentCard.builder()
+ .name("legacy")
+ .description("legacy")
+ .version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .skills(List.of(AgentSkill.builder().id("skill").name("skill").description("skill").tags(List.of()).build()))
+ .url(endpoint)
+ .preferredTransport(TransportProtocol.GRPC.asString())
+ .supportedInterfaces(List.of(new AgentInterface(TransportProtocol.GRPC.asString(), endpoint, null, "0.3")))
+ .build();
+ }
+
+ private static AgentCard cardWithInterfaceTenant() {
+ return AgentCard.builder()
+ .name("legacy")
+ .description("legacy")
+ .version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .skills(List.of())
+ .supportedInterfaces(List.of(new AgentInterface(TransportProtocol.GRPC.asString(), "in-process", "tenant", "0.3")))
+ .build();
+ }
+}
diff --git a/compat-0.3/client/adapter-jsonrpc/pom.xml b/compat-0.3/client/adapter-jsonrpc/pom.xml
new file mode 100644
index 000000000..b9e3b7fd6
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.3.3.Final-SNAPSHOT
+ ../../pom.xml
+
+ a2a-java-sdk-compat-0.3-client-adapter-jsonrpc
+ Java SDK A2A Compat 0.3 Client Adapter: JSON-RPC
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-adapter
+
+
+ ${project.groupId}
+ a2a-java-sdk-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-client-transport-jsonrpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-spec-grpc
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
diff --git a/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransport.java b/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransport.java
new file mode 100644
index 000000000..c6d3a1d50
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransport.java
@@ -0,0 +1,157 @@
+package org.a2aproject.sdk.compat03.client.adapter.jsonrpc;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientCallContextMapper;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportBase;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportSupport;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03InterceptorSupport;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3;
+import org.a2aproject.sdk.grpc.utils.ProtoUtils;
+import org.a2aproject.sdk.spec.A2AMethods;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.CancelTaskParams;
+import org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.EventKind;
+import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.StreamingEventKind;
+import org.a2aproject.sdk.spec.Task;
+import org.a2aproject.sdk.spec.TaskIdParams;
+import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+import org.jspecify.annotations.Nullable;
+
+/** 1.0 client facade backed by the legacy JSON-RPC transport. */
+public class JSONRPCCompat03ClientTransport extends Compat03ClientTransportBase {
+ public JSONRPCCompat03ClientTransport(ClientTransport_v0_3 delegate, AgentCard card,
+ List interceptors) {
+ super(delegate, card, interceptors);
+ }
+
+ @Override
+ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateMessageSend(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SEND_MESSAGE_METHOD,
+ ProtoUtils.ToProto.sendMessageRequest(request), org.a2aproject.sdk.grpc.SendMessageRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(delegate.sendMessage(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.messageSendParams(
+ (org.a2aproject.sdk.grpc.SendMessageRequest) payload.getPayload())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateMessageSend(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SEND_STREAMING_MESSAGE_METHOD,
+ ProtoUtils.ToProto.sendMessageRequest(request), org.a2aproject.sdk.grpc.SendMessageRequest.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.sendMessageStreaming(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.messageSendParams(
+ (org.a2aproject.sdk.grpc.SendMessageRequest) payload.getPayload())),
+ event -> events.accept(Compat03ClientTransportSupport.toV10(event)),
+ Compat03ClientTransportSupport.mapAsyncError(errors),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload))));
+ }
+
+ @Override
+ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateTaskQuery(request);
+ PayloadAndHeaders payload = apply(A2AMethods.GET_TASK_METHOD, ProtoUtils.ToProto.getTaskRequest(request),
+ org.a2aproject.sdk.grpc.GetTaskRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> TaskMapper_v0_3.INSTANCE.toV10(delegate.getTask(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.taskQueryParams(
+ (org.a2aproject.sdk.grpc.GetTaskRequest) payload.getPayload())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateCancel(request);
+ PayloadAndHeaders payload = apply(A2AMethods.CANCEL_TASK_METHOD, ProtoUtils.ToProto.cancelTaskRequest(request),
+ org.a2aproject.sdk.grpc.CancelTaskRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> TaskMapper_v0_3.INSTANCE.toV10(delegate.cancelTask(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.cancelTaskParams(
+ (org.a2aproject.sdk.grpc.CancelTaskRequest) payload.getPayload())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validatePushConfig(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ ProtoUtils.ToProto.taskPushNotificationConfig(request), org.a2aproject.sdk.grpc.TaskPushNotificationConfig.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(
+ delegate.setTaskPushNotificationConfiguration(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.taskPushNotificationConfig((org.a2aproject.sdk.grpc.TaskPushNotificationConfig) payload.getPayload())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateGetPush(request);
+ PayloadAndHeaders payload = apply(A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ ProtoUtils.ToProto.getTaskPushNotificationConfigRequest(request), org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(
+ delegate.getTaskPushNotificationConfiguration(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.getTaskPushNotificationConfigParams((org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest) payload.getPayload())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validatePushList(request);
+ PayloadAndHeaders payload = apply(A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ ProtoUtils.ToProto.listTaskPushNotificationConfigsRequest(request), org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10PushList(
+ delegate.listTaskPushNotificationConfigurations(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.listTaskPushNotificationConfigsParams((org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest) payload.getPayload())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateDeletePush(request);
+ PayloadAndHeaders payload = apply(A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ ProtoUtils.ToProto.deleteTaskPushNotificationConfigRequest(request), org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.deleteTaskPushNotificationConfigurations(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.deleteTaskPushNotificationConfigParams(
+ (org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest) payload.getPayload())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload))));
+ }
+
+ @Override
+ public void subscribeToTask(TaskIdParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateTenant("subscribeToTask", request.tenant());
+ PayloadAndHeaders payload = apply(A2AMethods.SUBSCRIBE_TO_TASK_METHOD,
+ ProtoUtils.ToProto.subscribeToTaskRequest(request), org.a2aproject.sdk.grpc.SubscribeToTaskRequest.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.resubscribe(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.taskIdParams(
+ (org.a2aproject.sdk.grpc.SubscribeToTaskRequest) payload.getPayload())),
+ event -> events.accept(Compat03ClientTransportSupport.toV10(event)),
+ Compat03ClientTransportSupport.mapAsyncError(errors),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload))));
+ }
+
+ private PayloadAndHeaders apply(String method, Object payload, Class> expected,
+ @Nullable ClientCallContext context) {
+ return Compat03InterceptorSupport.apply(interceptors, method, payload, agentCard, context, expected);
+ }
+
+ private static ClientCallContext contextWithHeaders(@Nullable ClientCallContext original, PayloadAndHeaders payload) {
+ return new ClientCallContext(original == null ? Map.of() : original.getState(), payload.getHeaders());
+ }
+}
diff --git a/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransportProvider.java b/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransportProvider.java
new file mode 100644
index 000000000..fa7bff6ab
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransportProvider.java
@@ -0,0 +1,35 @@
+package org.a2aproject.sdk.compat03.client.adapter.jsonrpc;
+
+import org.a2aproject.sdk.client.VersionedClientTransportProvider;
+import org.a2aproject.sdk.client.http.A2AHttpClient;
+import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport;
+import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransportConfig;
+import org.a2aproject.sdk.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportBase;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportSupport;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfig_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+
+/** ServiceLoader provider for the optional JSON-RPC 0.3 adapter. */
+public final class JSONRPCCompat03ClientTransportProvider implements VersionedClientTransportProvider {
+ @Override public String protocolBinding() { return "JSONRPC"; }
+ @Override public String protocolVersion() { return "0.3"; }
+ @Override public Class extends ClientTransport> configuredTransportClass() { return JSONRPCTransport.class; }
+
+ @Override
+ public ClientTransport create(ClientTransportConfig> config, AgentCard card, AgentInterface agentInterface)
+ throws A2AClientException {
+ Compat03ClientTransportSupport.validateAgentInterfaceTenant(agentInterface.tenant());
+ JSONRPCTransportConfig nativeConfig = config == null ? new JSONRPCTransportConfig() :
+ (JSONRPCTransportConfig) config;
+ Compat03ClientTransportSupport.validateConfig(nativeConfig);
+ A2AHttpClient httpClient = nativeConfig.getHttpClient();
+ JSONRPCTransport_v0_3 legacy = new JSONRPCTransport_v0_3(httpClient,
+ Compat03ClientTransportBase.legacyCard(card), agentInterface.url(), java.util.List.of());
+ return new JSONRPCCompat03ClientTransport(legacy, card, nativeConfig.getInterceptors());
+ }
+}
diff --git a/compat-0.3/client/adapter-jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider b/compat-0.3/client/adapter-jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider
new file mode 100644
index 000000000..d85332a18
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider
@@ -0,0 +1 @@
+org.a2aproject.sdk.compat03.client.adapter.jsonrpc.JSONRPCCompat03ClientTransportProvider
diff --git a/compat-0.3/client/adapter-jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransportTest.java b/compat-0.3/client/adapter-jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransportTest.java
new file mode 100644
index 000000000..6a3abf392
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransportTest.java
@@ -0,0 +1,119 @@
+package org.a2aproject.sdk.compat03.client.adapter.jsonrpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders;
+import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.EventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3;
+import org.a2aproject.sdk.spec.AgentCapabilities;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+import org.junit.jupiter.api.Test;
+
+class JSONRPCCompat03ClientTransportTest {
+ @Test
+ void providerTargetsJsonRpcAndOrdinaryJsonRpcConfiguration() {
+ JSONRPCCompat03ClientTransportProvider provider = new JSONRPCCompat03ClientTransportProvider();
+
+ assertEquals("JSONRPC", provider.protocolBinding());
+ assertEquals("0.3", provider.protocolVersion());
+ assertEquals(JSONRPCTransport.class, provider.configuredTransportClass());
+ assertTrue(provider.getClass().getPackageName().contains("adapter.jsonrpc"));
+ }
+
+ @Test
+ void rejectsTenantBeforeCallingLegacyDelegate() {
+ RecordingDelegate delegate = new RecordingDelegate();
+ JSONRPCCompat03ClientTransport transport = new JSONRPCCompat03ClientTransport(
+ delegate, testCard(), List.of());
+
+ assertThrows(org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> transport.subscribeToTask(new org.a2aproject.sdk.spec.TaskIdParams("task", "tenant"),
+ event -> { }, error -> { }, (ClientCallContext) null));
+ assertFalse(delegate.called);
+ }
+
+ @Test
+ void providerRejectsTenantOnLegacyAgentInterface() {
+ AgentCard card = cardWithInterfaceTenant();
+
+ org.a2aproject.sdk.spec.A2AClientException exception = assertThrows(org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> new JSONRPCCompat03ClientTransportProvider().create(null, card, card.supportedInterfaces().get(0)));
+
+ assertTrue(exception.getMessage().contains("tenant"));
+ }
+
+ @Test
+ void rejectsUnsupportedHistoryLengthIntroducedByAnInterceptor() {
+ ClientCallInterceptor interceptor = new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, java.util.Map headers,
+ AgentCard card, ClientCallContext context) {
+ org.a2aproject.sdk.grpc.GetTaskRequest request =
+ (org.a2aproject.sdk.grpc.GetTaskRequest) payload;
+ return new PayloadAndHeaders(request.toBuilder().setHistoryLength(0).build(), headers);
+ }
+ };
+ RecordingDelegate delegate = new RecordingDelegate();
+ JSONRPCCompat03ClientTransport transport = new JSONRPCCompat03ClientTransport(
+ delegate, testCard(), List.of(interceptor));
+
+ assertThrows(org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> transport.getTask(new TaskQueryParams("task", 1), null));
+ assertFalse(delegate.called);
+ }
+
+ private static AgentCard testCard() {
+ return AgentCard.builder().name("agent").description("description").version("1")
+ .capabilities(new AgentCapabilities(false, false, false, null))
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of()).supportedInterfaces(List.of(new AgentInterface("JSONRPC", "https://example.test", null, "0.3")))
+ .build();
+ }
+
+ private static AgentCard cardWithInterfaceTenant() {
+ return AgentCard.builder().name("agent").description("description").version("1")
+ .capabilities(new AgentCapabilities(false, false, false, null))
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of()).supportedInterfaces(List.of(
+ new AgentInterface("JSONRPC", "https://example.test", "tenant", "0.3")))
+ .build();
+ }
+
+ private static final class RecordingDelegate implements ClientTransport_v0_3 {
+ boolean called;
+
+ @Override public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, ClientCallContext_v0_3 context) { return null; }
+ @Override public void sendMessageStreaming(MessageSendParams_v0_3 request, java.util.function.Consumer events,
+ java.util.function.Consumer errors, ClientCallContext_v0_3 context) { }
+ @Override public org.a2aproject.sdk.compat03.spec.Task_v0_3 getTask(TaskQueryParams_v0_3 request, ClientCallContext_v0_3 context) { called = true; return null; }
+ @Override public org.a2aproject.sdk.compat03.spec.Task_v0_3 cancelTask(TaskIdParams_v0_3 request, ClientCallContext_v0_3 context) { return null; }
+ @Override public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(TaskPushNotificationConfig_v0_3 request, ClientCallContext_v0_3 context) { return null; }
+ @Override public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams_v0_3 request, ClientCallContext_v0_3 context) { return null; }
+ @Override public List listTaskPushNotificationConfigurations(ListTaskPushNotificationConfigParams_v0_3 request, ClientCallContext_v0_3 context) { return List.of(); }
+ @Override public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams_v0_3 request, ClientCallContext_v0_3 context) { }
+ @Override public void resubscribe(TaskIdParams_v0_3 request, java.util.function.Consumer events,
+ java.util.function.Consumer errors, ClientCallContext_v0_3 context) { called = true; }
+ @Override public AgentCard_v0_3 getAgentCard(ClientCallContext_v0_3 context) { return null; }
+ @Override public void close() { }
+ }
+}
diff --git a/compat-0.3/client/adapter-rest/pom.xml b/compat-0.3/client/adapter-rest/pom.xml
new file mode 100644
index 000000000..b3c5d4262
--- /dev/null
+++ b/compat-0.3/client/adapter-rest/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.3.3.Final-SNAPSHOT
+ ../../pom.xml
+
+ a2a-java-sdk-compat-0.3-client-adapter-rest
+ Java SDK A2A Compat 0.3 Client Adapter: REST
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-adapter
+
+
+ ${project.groupId}
+ a2a-java-sdk-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-client-transport-rest
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-rest
+
+
+ ${project.groupId}
+ a2a-java-sdk-spec-grpc
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
diff --git a/compat-0.3/client/adapter-rest/src/main/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransport.java b/compat-0.3/client/adapter-rest/src/main/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransport.java
new file mode 100644
index 000000000..e26dd55b2
--- /dev/null
+++ b/compat-0.3/client/adapter-rest/src/main/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransport.java
@@ -0,0 +1,172 @@
+package org.a2aproject.sdk.compat03.client.adapter.rest;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientCallContextMapper;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportBase;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportSupport;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03InterceptorSupport;
+import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3;
+import org.a2aproject.sdk.grpc.utils.ProtoUtils;
+import org.a2aproject.sdk.spec.A2AMethods;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.CancelTaskParams;
+import org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.EventKind;
+import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.StreamingEventKind;
+import org.a2aproject.sdk.spec.Task;
+import org.a2aproject.sdk.spec.TaskIdParams;
+import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+import org.jspecify.annotations.Nullable;
+
+/** 1.0 client facade backed by the legacy REST transport. */
+public final class RestCompat03ClientTransport extends Compat03ClientTransportBase {
+ public RestCompat03ClientTransport(RestTransport_v0_3 delegate, AgentCard card,
+ List interceptors) {
+ super(delegate, card, interceptors);
+ }
+
+ @Override
+ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateMessageSend(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SEND_MESSAGE_METHOD,
+ org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(ProtoUtils.ToProto.sendMessageRequest(request)),
+ org.a2aproject.sdk.grpc.SendMessageRequest.Builder.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(delegate.sendMessage(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.messageSendParams(
+ ((org.a2aproject.sdk.grpc.SendMessageRequest.Builder) payload.getPayload()).build())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateMessageSend(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SEND_STREAMING_MESSAGE_METHOD,
+ org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(ProtoUtils.ToProto.sendMessageRequest(request)),
+ org.a2aproject.sdk.grpc.SendMessageRequest.Builder.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.sendMessageStreaming(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.messageSendParams(
+ ((org.a2aproject.sdk.grpc.SendMessageRequest.Builder) payload.getPayload()).build())),
+ event -> events.accept(Compat03ClientTransportSupport.toV10(event)),
+ Compat03ClientTransportSupport.mapAsyncError(errors),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload))));
+ }
+
+ @Override
+ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateTaskQuery(request);
+ PayloadAndHeaders payload = apply(A2AMethods.GET_TASK_METHOD,
+ org.a2aproject.sdk.grpc.GetTaskRequest.newBuilder(ProtoUtils.ToProto.getTaskRequest(request)),
+ org.a2aproject.sdk.grpc.GetTaskRequest.Builder.class, context);
+ return Compat03ClientTransportSupport.call(() -> TaskMapper_v0_3.INSTANCE.toV10(delegate.getTask(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.taskQueryParams(
+ ((org.a2aproject.sdk.grpc.GetTaskRequest.Builder) payload.getPayload()).build())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateCancel(request);
+ PayloadAndHeaders payload = apply(A2AMethods.CANCEL_TASK_METHOD,
+ org.a2aproject.sdk.grpc.CancelTaskRequest.newBuilder(ProtoUtils.ToProto.cancelTaskRequest(request)),
+ org.a2aproject.sdk.grpc.CancelTaskRequest.Builder.class, context);
+ return Compat03ClientTransportSupport.call(() -> TaskMapper_v0_3.INSTANCE.toV10(delegate.cancelTask(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.cancelTaskParams(
+ ((org.a2aproject.sdk.grpc.CancelTaskRequest.Builder) payload.getPayload()).build())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validatePushConfig(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(ProtoUtils.ToProto.taskPushNotificationConfig(request)),
+ org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(
+ delegate.setTaskPushNotificationConfiguration(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.taskPushNotificationConfig(
+ ((org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder) payload.getPayload()).build())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateGetPush(request);
+ PayloadAndHeaders payload = apply(A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.newBuilder(
+ ProtoUtils.ToProto.getTaskPushNotificationConfigRequest(request)),
+ org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.Builder.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(
+ delegate.getTaskPushNotificationConfiguration(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.getTaskPushNotificationConfigParams(
+ ((org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.Builder) payload.getPayload()).build())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validatePushList(request);
+ PayloadAndHeaders payload = apply(A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.newBuilder(
+ ProtoUtils.ToProto.listTaskPushNotificationConfigsRequest(request)),
+ org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10PushList(
+ delegate.listTaskPushNotificationConfigurations(Compat03ClientTransportSupport.toV03(
+ ProtoUtils.FromProto.listTaskPushNotificationConfigsParams(
+ ((org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder) payload.getPayload()).build())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload)))));
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateDeletePush(request);
+ PayloadAndHeaders payload = apply(A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
+ org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder(
+ ProtoUtils.ToProto.deleteTaskPushNotificationConfigRequest(request)),
+ org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.Builder.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.deleteTaskPushNotificationConfigurations(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.deleteTaskPushNotificationConfigParams(
+ ((org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.Builder) payload.getPayload()).build())),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload))));
+ }
+
+ @Override
+ public void subscribeToTask(TaskIdParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateTenant("subscribeToTask", request.tenant());
+ PayloadAndHeaders payload = apply(A2AMethods.SUBSCRIBE_TO_TASK_METHOD,
+ org.a2aproject.sdk.grpc.SubscribeToTaskRequest.newBuilder(ProtoUtils.ToProto.subscribeToTaskRequest(request)),
+ org.a2aproject.sdk.grpc.SubscribeToTaskRequest.Builder.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.resubscribe(
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.taskIdParams(
+ ((org.a2aproject.sdk.grpc.SubscribeToTaskRequest.Builder) payload.getPayload()).build())),
+ event -> events.accept(Compat03ClientTransportSupport.toV10(event)),
+ Compat03ClientTransportSupport.mapAsyncError(errors),
+ Compat03ClientCallContextMapper.toV03(contextWithHeaders(context, payload))));
+ }
+
+ private PayloadAndHeaders apply(String method, Object payload, Class> expected,
+ @Nullable ClientCallContext context) {
+ return Compat03InterceptorSupport.apply(interceptors, method, payload, agentCard, context, expected);
+ }
+
+ private static ClientCallContext contextWithHeaders(@Nullable ClientCallContext original, PayloadAndHeaders payload) {
+ return new ClientCallContext(original == null ? Map.of() : original.getState(), payload.getHeaders());
+ }
+}
diff --git a/compat-0.3/client/adapter-rest/src/main/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransportProvider.java b/compat-0.3/client/adapter-rest/src/main/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransportProvider.java
new file mode 100644
index 000000000..78c29aabe
--- /dev/null
+++ b/compat-0.3/client/adapter-rest/src/main/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransportProvider.java
@@ -0,0 +1,34 @@
+package org.a2aproject.sdk.compat03.client.adapter.rest;
+
+import org.a2aproject.sdk.client.VersionedClientTransportProvider;
+import org.a2aproject.sdk.client.http.A2AHttpClient;
+import org.a2aproject.sdk.client.transport.rest.RestTransport;
+import org.a2aproject.sdk.client.transport.rest.RestTransportConfig;
+import org.a2aproject.sdk.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportBase;
+import org.a2aproject.sdk.compat03.client.adapter.Compat03ClientTransportSupport;
+import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+
+/** ServiceLoader provider for the optional REST 0.3 adapter. */
+public final class RestCompat03ClientTransportProvider implements VersionedClientTransportProvider {
+ @Override public String protocolBinding() { return "HTTP+JSON"; }
+ @Override public String protocolVersion() { return "0.3"; }
+ @Override public Class extends ClientTransport> configuredTransportClass() { return RestTransport.class; }
+
+ @Override
+ public ClientTransport create(ClientTransportConfig> config, AgentCard card, AgentInterface agentInterface)
+ throws A2AClientException {
+ Compat03ClientTransportSupport.validateAgentInterfaceTenant(agentInterface.tenant());
+ RestTransportConfig nativeConfig = config == null ? new RestTransportConfig() :
+ (RestTransportConfig) config;
+ Compat03ClientTransportSupport.validateConfig(nativeConfig);
+ A2AHttpClient httpClient = nativeConfig.getHttpClient();
+ RestTransport_v0_3 legacy = new RestTransport_v0_3(httpClient,
+ Compat03ClientTransportBase.legacyCard(card), agentInterface.url(), java.util.List.of());
+ return new RestCompat03ClientTransport(legacy, card, nativeConfig.getInterceptors());
+ }
+}
diff --git a/compat-0.3/client/adapter-rest/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider b/compat-0.3/client/adapter-rest/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider
new file mode 100644
index 000000000..7eb59dcfa
--- /dev/null
+++ b/compat-0.3/client/adapter-rest/src/main/resources/META-INF/services/org.a2aproject.sdk.client.VersionedClientTransportProvider
@@ -0,0 +1 @@
+org.a2aproject.sdk.compat03.client.adapter.rest.RestCompat03ClientTransportProvider
diff --git a/compat-0.3/client/adapter-rest/src/test/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransportTest.java b/compat-0.3/client/adapter-rest/src/test/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransportTest.java
new file mode 100644
index 000000000..a986f0feb
--- /dev/null
+++ b/compat-0.3/client/adapter-rest/src/test/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransportTest.java
@@ -0,0 +1,42 @@
+package org.a2aproject.sdk.compat03.client.adapter.rest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.a2aproject.sdk.client.transport.rest.RestTransport;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.AgentCapabilities;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+import org.junit.jupiter.api.Test;
+
+class RestCompat03ClientTransportTest {
+ @Test
+ void providerTargetsHttpJsonAndOrdinaryRestConfiguration() {
+ RestCompat03ClientTransportProvider provider = new RestCompat03ClientTransportProvider();
+
+ assertEquals("HTTP+JSON", provider.protocolBinding());
+ assertEquals("0.3", provider.protocolVersion());
+ assertEquals(RestTransport.class, provider.configuredTransportClass());
+ }
+
+ @Test
+ void rejectsTenantOnLegacyAgentInterface() {
+ AgentCard card = cardWithTenant();
+
+ A2AClientException exception = assertThrows(A2AClientException.class,
+ () -> new RestCompat03ClientTransportProvider().create(null, card, card.supportedInterfaces().get(0)));
+
+ assertTrue(exception.getMessage().contains("tenant"));
+ }
+
+ private static AgentCard cardWithTenant() {
+ return AgentCard.builder().name("agent").description("description").version("1")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(java.util.List.of("text")).defaultOutputModes(java.util.List.of("text"))
+ .skills(java.util.List.of())
+ .supportedInterfaces(java.util.List.of(new AgentInterface("HTTP+JSON", "https://example.test", "tenant", "0.3")))
+ .build();
+ }
+}
diff --git a/compat-0.3/client/adapter/pom.xml b/compat-0.3/client/adapter/pom.xml
new file mode 100644
index 000000000..a1d8ed941
--- /dev/null
+++ b/compat-0.3/client/adapter/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.3.3.Final-SNAPSHOT
+ ../..
+
+ a2a-java-sdk-compat-0.3-client-adapter
+ Java SDK A2A Compat 0.3 Client Adapter
+ Optional 0.3 agent-card parser and client adapter support
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-conversion
+
+
+ ${project.groupId}
+ a2a-java-sdk-spec
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+
+
+ ${project.groupId}
+ a2a-java-sdk-http-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-client-transport-spi
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
diff --git a/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03AgentCardCompatibilityParser.java b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03AgentCardCompatibilityParser.java
new file mode 100644
index 000000000..1ca2dde11
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03AgentCardCompatibilityParser.java
@@ -0,0 +1,41 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.util.Optional;
+import java.util.Set;
+
+import org.a2aproject.sdk.client.http.AgentCardCompatibilityParser;
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.AgentCardMapper_v0_3;
+import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.spec.A2AClientJSONError;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.jspecify.annotations.Nullable;
+
+/** Parses a legacy 0.3 JSON card and projects it into the public 1.0 card model. */
+public final class Compat03AgentCardCompatibilityParser implements AgentCardCompatibilityParser {
+ @Override
+ public String supportedProtocolVersion() {
+ return "0.3";
+ }
+
+ @Override
+ public Optional parse(String rawCardJson, @Nullable AgentCard parsedV10Card,
+ Set requestedProtocolVersions) {
+ try {
+ AgentCard_v0_3 legacyCard = JsonUtil_v0_3.fromJson(rawCardJson, AgentCard_v0_3.class);
+ if (!"0.3".equals(normalize(legacyCard.protocolVersion()))) {
+ return Optional.empty();
+ }
+ return Optional.of(AgentCardMapper_v0_3.INSTANCE.toV10(legacyCard));
+ } catch (Exception e) {
+ throw new A2AClientJSONError("Could not convert A2A 0.3 agent card to the unified client model", e);
+ }
+ }
+
+ private static String normalize(String version) {
+ return switch (version) {
+ case "0.3", "0.3.0" -> "0.3";
+ default -> version;
+ };
+ }
+}
diff --git a/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientCallContextMapper.java b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientCallContextMapper.java
new file mode 100644
index 000000000..bd50d042f
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientCallContextMapper.java
@@ -0,0 +1,21 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.util.Map;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.jspecify.annotations.Nullable;
+
+/** Converts public 1.0 call contexts to the equivalent 0.3 context. */
+public final class Compat03ClientCallContextMapper {
+
+ private Compat03ClientCallContextMapper() {
+ }
+
+ public static ClientCallContext_v0_3 toV03(@Nullable ClientCallContext context) {
+ if (context == null) {
+ return null;
+ }
+ return new ClientCallContext_v0_3(Map.copyOf(context.getState()), Map.copyOf(context.getHeaders()));
+ }
+}
diff --git a/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientErrorMapper.java b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientErrorMapper.java
new file mode 100644
index 000000000..ba7fe6c94
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientErrorMapper.java
@@ -0,0 +1,76 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AErrorCodes_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.A2AError;
+import org.a2aproject.sdk.spec.ContentTypeNotSupportedError;
+import org.a2aproject.sdk.spec.ExtendedAgentCardNotConfiguredError;
+import org.a2aproject.sdk.spec.InvalidParamsError;
+import org.a2aproject.sdk.spec.InvalidRequestError;
+import org.a2aproject.sdk.spec.InternalError;
+import org.a2aproject.sdk.spec.InvalidAgentResponseError;
+import org.a2aproject.sdk.spec.JSONParseError;
+import org.a2aproject.sdk.spec.MethodNotFoundError;
+import org.a2aproject.sdk.spec.PushNotificationNotSupportedError;
+import org.a2aproject.sdk.spec.TaskNotCancelableError;
+import org.a2aproject.sdk.spec.TaskNotFoundError;
+import org.a2aproject.sdk.spec.UnsupportedOperationError;
+
+/** Maps errors from a 0.3 delegate into the public 1.0 exception hierarchy. */
+public final class Compat03ClientErrorMapper {
+
+ private Compat03ClientErrorMapper() {
+ }
+
+ public static A2AClientException toV10(A2AClientException_v0_3 exception) {
+ Throwable cause = exception.getCause();
+ String message = exception.getMessage() == null ? "A2A 0.3 client operation failed" : exception.getMessage();
+ if (cause instanceof JSONRPCError_v0_3 error) {
+ return new A2AClientException(message, toV10(error));
+ }
+ return cause == null ? new A2AClientException(message) : new A2AClientException(message, cause);
+ }
+
+ public static A2AError toV10(JSONRPCError_v0_3 error) {
+ Integer code = error.getCode();
+ String message = error.getMessage();
+ Map details = details(error.getData());
+ return switch (code) {
+ case A2AErrorCodes_v0_3.JSON_PARSE_ERROR_CODE -> new JSONParseError(code, message, details);
+ case A2AErrorCodes_v0_3.INVALID_REQUEST_ERROR_CODE -> new InvalidRequestError(code, message, details);
+ case A2AErrorCodes_v0_3.METHOD_NOT_FOUND_ERROR_CODE -> new MethodNotFoundError(code, message, details);
+ case A2AErrorCodes_v0_3.INVALID_PARAMS_ERROR_CODE -> new InvalidParamsError(code, message, details);
+ case A2AErrorCodes_v0_3.INTERNAL_ERROR_CODE -> new InternalError(code, message, details);
+ case A2AErrorCodes_v0_3.TASK_NOT_FOUND_ERROR_CODE -> new TaskNotFoundError(message, details);
+ case A2AErrorCodes_v0_3.TASK_NOT_CANCELABLE_ERROR_CODE -> new TaskNotCancelableError(code, message, details);
+ case A2AErrorCodes_v0_3.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE ->
+ new PushNotificationNotSupportedError(code, message, details);
+ case A2AErrorCodes_v0_3.UNSUPPORTED_OPERATION_ERROR_CODE -> new UnsupportedOperationError(code, message, details);
+ case A2AErrorCodes_v0_3.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE ->
+ new ContentTypeNotSupportedError(code, message, details);
+ case A2AErrorCodes_v0_3.INVALID_AGENT_RESPONSE_ERROR_CODE ->
+ new InvalidAgentResponseError(code, message, details);
+ case A2AErrorCodes_v0_3.AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED_ERROR_CODE ->
+ new ExtendedAgentCardNotConfiguredError(code, message, details);
+ default -> new A2AError(code, message, details);
+ };
+ }
+
+ private static Map details(Object data) {
+ if (!(data instanceof Map, ?> rawDetails)) {
+ return Map.of();
+ }
+ Map details = new LinkedHashMap<>();
+ rawDetails.forEach((key, value) -> {
+ if (key instanceof String stringKey) {
+ details.put(stringKey, value);
+ }
+ });
+ return details;
+ }
+}
diff --git a/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportBase.java b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportBase.java
new file mode 100644
index 000000000..714c9c79c
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportBase.java
@@ -0,0 +1,151 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.EventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.CancelTaskParams;
+import org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.EventKind;
+import org.a2aproject.sdk.spec.GetExtendedAgentCardParams;
+import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult;
+import org.a2aproject.sdk.spec.ListTasksParams;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.StreamingEventKind;
+import org.a2aproject.sdk.spec.Task;
+import org.a2aproject.sdk.spec.TaskIdParams;
+import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult;
+import org.jspecify.annotations.Nullable;
+
+/** Common public 1.0 transport facade over a legacy 0.3 transport. */
+public abstract class Compat03ClientTransportBase implements ClientTransport {
+ protected final ClientTransport_v0_3 delegate;
+ protected final AgentCard agentCard;
+ protected final List interceptors;
+ private final AtomicBoolean closed = new AtomicBoolean();
+
+ protected Compat03ClientTransportBase(ClientTransport_v0_3 delegate, AgentCard agentCard,
+ List interceptors) {
+ this.delegate = delegate;
+ this.agentCard = agentCard;
+ this.interceptors = List.copyOf(interceptors);
+ }
+
+ @Override
+ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateMessageSend(request);
+ return Compat03ClientTransportSupport.call(() ->
+ Compat03ClientTransportSupport.toV10(delegate.sendMessage(
+ Compat03ClientTransportSupport.toV03(request), Compat03ClientTransportSupport.toV03Context(context))));
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateMessageSend(request);
+ Compat03ClientTransportSupport.run(() -> delegate.sendMessageStreaming(
+ Compat03ClientTransportSupport.toV03(request),
+ event -> events.accept(Compat03ClientTransportSupport.toV10(event)),
+ Compat03ClientTransportSupport.mapAsyncError(errors),
+ Compat03ClientTransportSupport.toV03Context(context)));
+ }
+
+ @Override
+ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateTaskQuery(request);
+ return Compat03ClientTransportSupport.call(() ->
+ org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3.INSTANCE.toV10(delegate.getTask(
+ Compat03ClientTransportSupport.toV03(request), Compat03ClientTransportSupport.toV03Context(context))));
+ }
+
+ @Override
+ public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateCancel(request);
+ return Compat03ClientTransportSupport.call(() ->
+ org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3.INSTANCE.toV10(delegate.cancelTask(
+ Compat03ClientTransportSupport.toV03(request), Compat03ClientTransportSupport.toV03Context(context))));
+ }
+
+ @Override
+ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateListTasks(request);
+ throw new A2AClientException("listTasks is not supported by A2A protocol 0.3");
+ }
+
+ @Override
+ public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validatePushConfig(request);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(
+ delegate.setTaskPushNotificationConfiguration(Compat03ClientTransportSupport.toV03(request),
+ Compat03ClientTransportSupport.toV03Context(context))));
+ }
+
+ @Override
+ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateGetPush(request);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(
+ delegate.getTaskPushNotificationConfiguration(Compat03ClientTransportSupport.toV03(request),
+ Compat03ClientTransportSupport.toV03Context(context))));
+ }
+
+ @Override
+ public org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validatePushList(request);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10PushList(
+ delegate.listTaskPushNotificationConfigurations(Compat03ClientTransportSupport.toV03(request),
+ Compat03ClientTransportSupport.toV03Context(context))));
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateDeletePush(request);
+ Compat03ClientTransportSupport.run(() -> delegate.deleteTaskPushNotificationConfigurations(
+ Compat03ClientTransportSupport.toV03(request), Compat03ClientTransportSupport.toV03Context(context)));
+ }
+
+ @Override
+ public void subscribeToTask(TaskIdParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateTenant("subscribeToTask", request.tenant());
+ Compat03ClientTransportSupport.run(() -> delegate.resubscribe(
+ Compat03ClientTransportSupport.toV03(request),
+ event -> events.accept(Compat03ClientTransportSupport.toV10(event)),
+ Compat03ClientTransportSupport.mapAsyncError(errors),
+ Compat03ClientTransportSupport.toV03Context(context)));
+ }
+
+ @Override
+ public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateExtendedAgentCard(params);
+ throw new A2AClientException("getExtendedAgentCard is not supported by A2A protocol 0.3");
+ }
+
+ @Override
+ public void close() {
+ if (closed.compareAndSet(false, true)) {
+ delegate.close();
+ }
+ }
+
+ public static AgentCard_v0_3 legacyCard(AgentCard card) {
+ return org.a2aproject.sdk.compat03.conversion.mappers.domain.AgentCardMapper_v0_3.INSTANCE.fromV10(card);
+ }
+}
diff --git a/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportSupport.java b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportSupport.java
new file mode 100644
index 000000000..39346fc71
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportSupport.java
@@ -0,0 +1,235 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.EventKindMapper_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.StreamingEventKindMapper_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskPushNotificationConfigMapper_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.params.CancelTaskParamsMapper_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.params.MessageSendParamsMapper_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.params.TaskIdParamsMapper_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.params.TaskQueryParamsMapper_v0_3;
+import org.a2aproject.sdk.compat03.conversion.mappers.result.ListTaskPushNotificationConfigsResultMapper_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.CancelTaskParams;
+import org.a2aproject.sdk.spec.InvalidParamsError;
+import org.a2aproject.sdk.spec.InvalidRequestError;
+import org.a2aproject.sdk.spec.InternalError;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult;
+import org.a2aproject.sdk.spec.ListTasksParams;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.TaskIdParams;
+import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+import org.a2aproject.sdk.spec.StreamingEventKind;
+import org.jspecify.annotations.Nullable;
+
+/** Shared, binding-independent behavior for adapters from the 1.0 client to 0.3 transports. */
+public final class Compat03ClientTransportSupport {
+
+ private Compat03ClientTransportSupport() {
+ }
+
+ public static void validateParameters(Map parameters) {
+ if (!parameters.isEmpty()) {
+ throw new A2AClientException("0.3 client adapters do not support generic transport parameters");
+ }
+ }
+
+ public static void validateListTasks(@Nullable ListTasksParams request) {
+ throw unsupported("listTasks");
+ }
+
+ public static void validateExtendedAgentCard(@Nullable Object request) {
+ throw unsupported("getExtendedAgentCard");
+ }
+
+ public static void validateTenant(String operation, @Nullable String tenant) {
+ if (tenant != null && !tenant.isEmpty()) {
+ throw unsupported(operation + " with a tenant");
+ }
+ }
+
+ public static void validateMessageSend(MessageSendParams request) {
+ validateTenant("sendMessage", request.tenant());
+ if (request.configuration() != null
+ && request.configuration().taskPushNotificationConfig() != null) {
+ validateTenant("sendMessage with a task push notification configuration",
+ request.configuration().taskPushNotificationConfig().tenant());
+ }
+ if (request.configuration() != null && Integer.valueOf(0).equals(request.configuration().historyLength())) {
+ throw unsupported("sendMessage with historyLength 0, which A2A protocol 0.3 interprets as unlimited history");
+ }
+ }
+
+ public static void validateTaskQuery(TaskQueryParams request) {
+ validateTenant("getTask", request.tenant());
+ if (Integer.valueOf(0).equals(request.historyLength())) {
+ throw unsupported("getTask with historyLength 0, which A2A protocol 0.3 interprets as unlimited history");
+ }
+ }
+
+ public static void validateCancel(CancelTaskParams request) {
+ validateTenant("cancelTask", request.tenant());
+ }
+
+ public static void validatePushConfig(TaskPushNotificationConfig request) {
+ validateTenant("createTaskPushNotificationConfiguration", request.tenant());
+ }
+
+ public static void validateGetPush(org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams request) {
+ validateTenant("getTaskPushNotificationConfiguration", request.tenant());
+ }
+
+ public static void validateDeletePush(org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams request) {
+ validateTenant("deleteTaskPushNotificationConfigurations", request.tenant());
+ }
+
+ public static void validatePushList(ListTaskPushNotificationConfigsParams request) {
+ validateTenant("listTaskPushNotificationConfigurations", request.tenant());
+ if (request.pageSize() > 0 || (request.pageToken() != null && !request.pageToken().isEmpty())) {
+ throw unsupported("listTaskPushNotificationConfigurations pagination");
+ }
+ }
+
+ public static void validateConfig(ClientTransportConfig> config) {
+ validateParameters(config.getParameters());
+ }
+
+ public static void validateAgentInterfaceTenant(@Nullable String tenant) {
+ validateTenant("AgentInterface", tenant);
+ }
+
+ public static ClientCallContext_v0_3 toV03Context(@Nullable ClientCallContext context) {
+ if (context == null) {
+ return null;
+ }
+ return Compat03ClientCallContextMapper.toV03(context);
+ }
+
+ public static MessageSendParams_v0_3 toV03(MessageSendParams request) {
+ validateMessageSend(request);
+ return MessageSendParamsMapper_v0_3.INSTANCE.fromV10(request);
+ }
+
+ public static TaskQueryParams_v0_3 toV03(TaskQueryParams request) {
+ validateTaskQuery(request);
+ return TaskQueryParamsMapper_v0_3.INSTANCE.fromV10(request);
+ }
+
+ public static TaskIdParams_v0_3 toV03(TaskIdParams request) {
+ validateTenant("TaskIdParams", request.tenant());
+ return TaskIdParamsMapper_v0_3.INSTANCE.fromV10(request);
+ }
+
+ public static TaskIdParams_v0_3 toV03(org.a2aproject.sdk.spec.CancelTaskParams request) {
+ validateCancel(request);
+ return CancelTaskParamsMapper_v0_3.INSTANCE.fromV10(request);
+ }
+
+ public static TaskPushNotificationConfig_v0_3 toV03(TaskPushNotificationConfig request) {
+ validatePushConfig(request);
+ return TaskPushNotificationConfigMapper_v0_3.INSTANCE.fromV10(request);
+ }
+
+ public static GetTaskPushNotificationConfigParams_v0_3 toV03(
+ org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams request) {
+ validateGetPush(request);
+ return new GetTaskPushNotificationConfigParams_v0_3(request.taskId(), request.id());
+ }
+
+ public static ListTaskPushNotificationConfigParams_v0_3 toV03(
+ ListTaskPushNotificationConfigsParams request) {
+ validatePushList(request);
+ return new ListTaskPushNotificationConfigParams_v0_3(request.id());
+ }
+
+ public static DeleteTaskPushNotificationConfigParams_v0_3 toV03(
+ org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams request) {
+ validateDeletePush(request);
+ return new DeleteTaskPushNotificationConfigParams_v0_3(request.taskId(), request.id());
+ }
+
+ public static org.a2aproject.sdk.spec.EventKind toV10(
+ org.a2aproject.sdk.compat03.spec.EventKind_v0_3 event) {
+ return EventKindMapper_v0_3.INSTANCE.toV10(event);
+ }
+
+ public static StreamingEventKind toV10(StreamingEventKind_v0_3 event) {
+ return StreamingEventKindMapper_v0_3.INSTANCE.toV10(event);
+ }
+
+ public static TaskPushNotificationConfig toV10(TaskPushNotificationConfig_v0_3 config) {
+ return TaskPushNotificationConfigMapper_v0_3.INSTANCE.toV10(config);
+ }
+
+ public static ListTaskPushNotificationConfigsResult toV10PushList(
+ List configs) {
+ ListTaskPushNotificationConfigsResult result =
+ ListTaskPushNotificationConfigsResultMapper_v0_3.INSTANCE.toV10(configs);
+ return result;
+ }
+
+ public static A2AClientException mapLegacyException(A2AClientException_v0_3 exception) {
+ return Compat03ClientErrorMapper.toV10(exception);
+ }
+
+ public static Consumer mapAsyncError(Consumer errors) {
+ return error -> {
+ if (error instanceof A2AClientException_v0_3 legacy) {
+ errors.accept(mapLegacyException(legacy));
+ } else if (error instanceof JSONRPCError_v0_3 legacy) {
+ errors.accept(new A2AClientException(legacy.getMessage(), Compat03ClientErrorMapper.toV10(legacy)));
+ } else {
+ errors.accept(error);
+ }
+ };
+ }
+
+ public static T call(ThrowingSupplier delegate) {
+ try {
+ return delegate.get();
+ } catch (A2AClientException_v0_3 exception) {
+ throw mapLegacyException(exception);
+ }
+ }
+
+ public static void run(ThrowingRunnable delegate) {
+ try {
+ delegate.run();
+ } catch (A2AClientException_v0_3 exception) {
+ throw mapLegacyException(exception);
+ }
+ }
+
+ private static A2AClientException unsupported(String operation) {
+ return new A2AClientException(operation + " is not supported by A2A protocol 0.3",
+ new org.a2aproject.sdk.spec.UnsupportedOperationError());
+ }
+
+ @FunctionalInterface
+ public interface ThrowingSupplier {
+ T get() throws A2AClientException_v0_3;
+ }
+
+ @FunctionalInterface
+ public interface ThrowingRunnable {
+ void run() throws A2AClientException_v0_3;
+ }
+}
diff --git a/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03InterceptorSupport.java b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03InterceptorSupport.java
new file mode 100644
index 000000000..9eb6efa9e
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03InterceptorSupport.java
@@ -0,0 +1,54 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders;
+import org.a2aproject.sdk.common.A2AHeaders;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.jspecify.annotations.Nullable;
+
+/** Applies ordinary 1.0 interceptors while enforcing the 0.3 routing contract. */
+public final class Compat03InterceptorSupport {
+ private Compat03InterceptorSupport() {
+ }
+
+ public static PayloadAndHeaders apply(List interceptors, String method,
+ Object payload, AgentCard card, @Nullable ClientCallContext context, Class> expectedType) {
+ Map headers = new HashMap<>();
+ if (context != null) {
+ context.getHeaders().forEach((name, value) -> {
+ if (!A2AHeaders.A2A_VERSION.equalsIgnoreCase(name)) {
+ headers.put(name, value);
+ }
+ });
+ }
+ ClientCallContext sanitizedContext = context == null ? null
+ : new ClientCallContext(context.getState(), headers);
+ PayloadAndHeaders result = new PayloadAndHeaders(payload, headers);
+ for (ClientCallInterceptor interceptor : interceptors) {
+ result = interceptor.intercept(method, result.getPayload(), result.getHeaders(), card, sanitizedContext);
+ if (result == null || result.getPayload() == null) {
+ throw new A2AClientException("0.3 interceptor returned a forbidden null payload for " + method);
+ }
+ if (!expectedType.equals(result.getPayload().getClass())) {
+ throw new A2AClientException("0.3 interceptor returned " + result.getPayload().getClass().getName()
+ + "; expected " + expectedType.getName() + " for " + method);
+ }
+ validateVersionHeader(result.getHeaders());
+ }
+ return result;
+ }
+
+ private static void validateVersionHeader(Map headers) {
+ for (String name : headers.keySet()) {
+ if (A2AHeaders.A2A_VERSION.equalsIgnoreCase(name)) {
+ throw new A2AClientException("0.3 client interceptors may not override A2A-Version");
+ }
+ }
+ }
+}
diff --git a/compat-0.3/client/adapter/src/main/resources/META-INF/services/org.a2aproject.sdk.client.http.AgentCardCompatibilityParser b/compat-0.3/client/adapter/src/main/resources/META-INF/services/org.a2aproject.sdk.client.http.AgentCardCompatibilityParser
new file mode 100644
index 000000000..f13cddd7b
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/resources/META-INF/services/org.a2aproject.sdk.client.http.AgentCardCompatibilityParser
@@ -0,0 +1 @@
+org.a2aproject.sdk.compat03.client.adapter.Compat03AgentCardCompatibilityParser
diff --git a/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03AgentCardCompatibilityParserTest.java b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03AgentCardCompatibilityParserTest.java
new file mode 100644
index 000000000..a516f2985
--- /dev/null
+++ b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03AgentCardCompatibilityParserTest.java
@@ -0,0 +1,107 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentSkill_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3;
+import org.a2aproject.sdk.compat03.spec.HTTPAuthSecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.ImplicitOAuthFlow_v0_3;
+import org.a2aproject.sdk.compat03.spec.OAuth2SecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.OAuthFlows_v0_3;
+import org.a2aproject.sdk.spec.A2AClientJSONError;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.HTTPAuthSecurityScheme;
+import org.junit.jupiter.api.Test;
+
+class Compat03AgentCardCompatibilityParserTest {
+ @Test
+ void parsesDeclaredPatchVersionAndProjectsInterface() throws Exception {
+ AgentCard_v0_3 card = new AgentCard_v0_3.Builder()
+ .name("legacy")
+ .description("legacy")
+ .url("https://example.test/a2a")
+ .version("1")
+ .capabilities(new AgentCapabilities_v0_3.Builder().build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .skills(List.of(new AgentSkill_v0_3.Builder().id("skill").name("skill")
+ .description("skill").tags(List.of("tag")).build()))
+ .additionalInterfaces(List.of(new AgentInterface_v0_3("JSONRPC", "https://example.test/a2a")))
+ .protocolVersion("0.3.0")
+ .build();
+
+ var result = new Compat03AgentCardCompatibilityParser().parse(
+ JsonUtil_v0_3.toJson(card), null, Set.of("1.0", "0.3"));
+
+ assertTrue(result.isPresent());
+ assertEquals("0.3", result.orElseThrow().supportedInterfaces().get(0).protocolVersion());
+ }
+
+ @Test
+ void parsesPrimaryOnlyCardAndProjectsCanonicalInterface() throws Exception {
+ AgentCard_v0_3 card = primaryOnlyCard("rest");
+
+ var result = new Compat03AgentCardCompatibilityParser().parse(
+ JsonUtil_v0_3.toJson(card), null, Set.of("1.0", "0.3"));
+
+ assertTrue(result.isPresent());
+ AgentCard projected = result.orElseThrow();
+ assertEquals(1, projected.supportedInterfaces().size());
+ assertEquals("HTTP+JSON", projected.supportedInterfaces().get(0).protocolBinding());
+ assertEquals("0.3", projected.supportedInterfaces().get(0).protocolVersion());
+ }
+
+ @Test
+ void parsesPrimaryOnlyCardWithHttpAuthScheme() throws Exception {
+ AgentCard_v0_3 card = primaryOnlyCard("http", Map.of("basicAuth", new HTTPAuthSecurityScheme_v0_3.Builder()
+ .scheme("basic").bearerFormat("none").description("HTTP Basic authentication").build()));
+
+ var result = new Compat03AgentCardCompatibilityParser().parse(
+ JsonUtil_v0_3.toJson(card), null, Set.of("1.0", "0.3"));
+
+ assertTrue(result.isPresent());
+ HTTPAuthSecurityScheme projectedScheme = (HTTPAuthSecurityScheme)
+ result.orElseThrow().securitySchemes().get("basicAuth");
+ assertEquals("basic", projectedScheme.scheme());
+ assertEquals("none", projectedScheme.bearerFormat());
+ assertEquals("HTTP Basic authentication", projectedScheme.description());
+ }
+
+ @Test
+ void reportsUnsupportedSecuritySchemeConversion() throws Exception {
+ AgentCard_v0_3 card = primaryOnlyCard("jsonrpc", Map.of("oauth", new OAuth2SecurityScheme_v0_3(
+ new OAuthFlows_v0_3(null, null,
+ new ImplicitOAuthFlow_v0_3("https://example.test/authorize", null, Map.of()), null),
+ "OAuth", null)));
+
+ A2AClientJSONError exception = assertThrows(A2AClientJSONError.class,
+ () -> new Compat03AgentCardCompatibilityParser().parse(
+ JsonUtil_v0_3.toJson(card), null, Set.of("1.0", "0.3")));
+
+ assertTrue(exception.getMessage().contains("Could not convert A2A 0.3 agent card"));
+ assertTrue(exception.getCause().getMessage().contains("implicit"));
+ }
+
+ private static AgentCard_v0_3 primaryOnlyCard(String preferredTransport) {
+ return primaryOnlyCard(preferredTransport, null);
+ }
+
+ private static AgentCard_v0_3 primaryOnlyCard(String preferredTransport,
+ Map securitySchemes) {
+ return new AgentCard_v0_3.Builder()
+ .name("legacy").description("legacy").url("http://localhost:8081")
+ .version("1.0.0").preferredTransport(preferredTransport).protocolVersion("0.3.0")
+ .capabilities(new AgentCapabilities_v0_3.Builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of()).securitySchemes(securitySchemes).additionalInterfaces(List.of()).build();
+ }
+}
diff --git a/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientErrorMapperTest.java b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientErrorMapperTest.java
new file mode 100644
index 000000000..5de6e4f19
--- /dev/null
+++ b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientErrorMapperTest.java
@@ -0,0 +1,56 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3;
+import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.A2AError;
+import org.a2aproject.sdk.spec.UnsupportedOperationError;
+import org.junit.jupiter.api.Test;
+
+class Compat03ClientErrorMapperTest {
+
+ @Test
+ void mapsUnsupportedOperationError() {
+ A2AClientException_v0_3 legacy = new A2AClientException_v0_3(
+ "unsupported", new UnsupportedOperationError_v0_3());
+
+ A2AClientException mapped = Compat03ClientErrorMapper.toV10(legacy);
+
+ assertInstanceOf(UnsupportedOperationError.class, mapped.getCause());
+ }
+
+ @Test
+ void mapsGenericJsonRpcErrorsByTheirLegacyCodes() {
+ List cases = List.of(
+ new ErrorCase(-32700, org.a2aproject.sdk.spec.JSONParseError.class),
+ new ErrorCase(-32600, org.a2aproject.sdk.spec.InvalidRequestError.class),
+ new ErrorCase(-32601, org.a2aproject.sdk.spec.MethodNotFoundError.class),
+ new ErrorCase(-32602, org.a2aproject.sdk.spec.InvalidParamsError.class),
+ new ErrorCase(-32603, org.a2aproject.sdk.spec.InternalError.class),
+ new ErrorCase(-32001, org.a2aproject.sdk.spec.TaskNotFoundError.class),
+ new ErrorCase(-32002, org.a2aproject.sdk.spec.TaskNotCancelableError.class),
+ new ErrorCase(-32003, org.a2aproject.sdk.spec.PushNotificationNotSupportedError.class),
+ new ErrorCase(-32004, org.a2aproject.sdk.spec.UnsupportedOperationError.class),
+ new ErrorCase(-32005, org.a2aproject.sdk.spec.ContentTypeNotSupportedError.class),
+ new ErrorCase(-32006, org.a2aproject.sdk.spec.InvalidAgentResponseError.class),
+ new ErrorCase(-32007, org.a2aproject.sdk.spec.ExtendedAgentCardNotConfiguredError.class));
+
+ for (ErrorCase errorCase : cases) {
+ A2AClientException mapped = Compat03ClientErrorMapper.toV10(new A2AClientException_v0_3(
+ "legacy failure", new JSONRPCError_v0_3(errorCase.code(), "legacy error", Map.of("key", "value"))));
+
+ A2AError error = assertInstanceOf(A2AError.class, mapped.getCause());
+ assertInstanceOf(errorCase.expectedType(), error);
+ org.junit.jupiter.api.Assertions.assertEquals(Map.of("key", "value"), error.getDetails());
+ }
+ }
+
+ private record ErrorCase(int code, Class extends A2AError> expectedType) {
+ }
+}
diff --git a/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportSupportTest.java b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportSupportTest.java
new file mode 100644
index 000000000..9b2c3c6ec
--- /dev/null
+++ b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportSupportTest.java
@@ -0,0 +1,175 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.CancelTaskParams;
+import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
+import org.a2aproject.sdk.spec.Message;
+import org.a2aproject.sdk.spec.MessageSendConfiguration;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.TaskIdParams;
+import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+import org.a2aproject.sdk.spec.TextPart;
+import org.junit.jupiter.api.Test;
+
+class Compat03ClientTransportSupportTest {
+
+ @Test
+ void rejectsUnsupportedOperationsBeforeDelegateUse() {
+ assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validateListTasks(null));
+ assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validateExtendedAgentCard(null));
+ assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validateTenant("getTask", "tenant"));
+ assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validatePushConfig(
+ TaskPushNotificationConfig.builder().taskId("task").url("https://example.test")
+ .tenant("tenant").build()));
+ assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validatePushList(
+ new ListTaskPushNotificationConfigsParams("task", 10, "", null)));
+ }
+
+ @Test
+ void acceptsDefaultPushListAndReturnsNoPageToken() {
+ var result = Compat03ClientTransportSupport.toV10PushList(java.util.List.of());
+
+ assertEquals(java.util.List.of(), result.configs());
+ assertNull(result.nextPageToken());
+ Compat03ClientTransportSupport.validatePushList(
+ new ListTaskPushNotificationConfigsParams("task", 0, "", null));
+ }
+
+ @Test
+ void acceptsNegativeDefaultPushListPageSize() {
+ Compat03ClientTransportSupport.validatePushList(
+ new ListTaskPushNotificationConfigsParams("task", -1, "", null));
+ }
+
+ @Test
+ void mapsContextsAndRequestParametersInBothDirections() {
+ ClientCallContext context = new ClientCallContext(
+ Map.of("trace", "one"), Map.of("Authorization", "Bearer token"));
+
+ ClientCallContext_v0_3 legacyContext = Compat03ClientTransportSupport.toV03Context(context);
+
+ assertEquals(context.getState(), legacyContext.getState());
+ assertEquals(context.getHeaders(), legacyContext.getHeaders());
+ assertEquals("task", Compat03ClientTransportSupport.toV03(
+ new TaskQueryParams("task", 3, null)).id());
+ assertEquals("task", Compat03ClientTransportSupport.toV03(
+ new TaskIdParams("task", null)).id());
+ assertEquals("task", Compat03ClientTransportSupport.toV03(
+ new CancelTaskParams("task", null, Map.of())).id());
+ MessageSendParams message = new MessageSendParams(
+ new org.a2aproject.sdk.spec.Message(
+ org.a2aproject.sdk.spec.Message.Role.ROLE_USER,
+ java.util.List.of(new TextPart("hello")), "message", null, null, null, null, null),
+ null, null, null);
+ assertEquals(null, Compat03ClientTransportSupport.toV03(message).metadata());
+ }
+
+ @Test
+ void rejectsExplicitZeroHistoryLengthBecauseLegacyZeroMeansUnlimited() {
+ A2AClientException exception = assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validateTaskQuery(new TaskQueryParams("task", 0, null)));
+
+ assertTrue(exception.getMessage().contains("historyLength"));
+ }
+
+ @Test
+ void rejectsMessageSendZeroHistoryLengthBecauseLegacyZeroMeansUnlimited() {
+ MessageSendParams request = new MessageSendParams(
+ new Message(Message.Role.ROLE_USER, java.util.List.of(new TextPart("hello")), "message", null, null,
+ null, null, null),
+ MessageSendConfiguration.builder().historyLength(0).build(), null);
+
+ A2AClientException exception = assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validateMessageSend(request));
+
+ assertTrue(exception.getMessage().contains("historyLength"));
+ }
+
+ @Test
+ void rejectsMessageSendWithTenantInNestedPushConfiguration() {
+ MessageSendParams request = new MessageSendParams(
+ new Message(Message.Role.ROLE_USER, java.util.List.of(new TextPart("hello")), "message", null, null,
+ null, null, null),
+ MessageSendConfiguration.builder()
+ .taskPushNotificationConfig(TaskPushNotificationConfig.builder()
+ .url("https://callback.example.test")
+ .tenant("tenant")
+ .build())
+ .build(),
+ null);
+
+ A2AClientException exception = assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validateMessageSend(request));
+
+ assertTrue(exception.getMessage().contains("tenant"));
+ }
+
+ @Test
+ void rejectsGenericParametersAndMapsLegacyErrors() {
+ assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.validateParameters(Map.of("unsupported", true)));
+ Compat03ClientTransportSupport.validateParameters(Map.of());
+
+ A2AClientException mapped = Compat03ClientTransportSupport.mapLegacyException(
+ new org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3(
+ "legacy failure", new org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3()));
+ assertTrue(mapped.getMessage().contains("legacy failure"));
+ assertTrue(mapped.getCause() instanceof org.a2aproject.sdk.spec.TaskNotFoundError);
+ }
+
+ @Test
+ void retainsNonProtocolLegacyFailureCause() {
+ IOException cause = new IOException("connection reset");
+
+ A2AClientException mapped = Compat03ClientTransportSupport.mapLegacyException(
+ new A2AClientException_v0_3("request failed", cause));
+
+ assertSame(cause, mapped.getCause());
+ }
+
+ @Test
+ void mapsAsynchronousLegacyErrors() {
+ AtomicReference mapped = new AtomicReference<>();
+
+ Compat03ClientTransportSupport.mapAsyncError(mapped::set).accept(
+ new org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3(
+ "legacy stream failure", new org.a2aproject.sdk.compat03.spec.InternalError_v0_3("legacy")));
+
+ assertTrue(mapped.get() instanceof A2AClientException);
+ assertTrue(mapped.get().getCause() instanceof org.a2aproject.sdk.spec.InternalError);
+ }
+
+ @Test
+ void mapsAsynchronousGenericJsonRpcErrorsByTheirLegacyCodes() {
+ AtomicReference mapped = new AtomicReference<>();
+
+ Compat03ClientTransportSupport.mapAsyncError(mapped::set).accept(
+ new org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3(
+ -32004, "unsupported", Map.of("operation", "listTasks")));
+
+ A2AClientException exception = assertInstanceOf(A2AClientException.class, mapped.get());
+ org.a2aproject.sdk.spec.UnsupportedOperationError error = assertInstanceOf(
+ org.a2aproject.sdk.spec.UnsupportedOperationError.class, exception.getCause());
+ assertEquals(Map.of("operation", "listTasks"), error.getDetails());
+ }
+}
diff --git a/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03InterceptorSupportTest.java b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03InterceptorSupportTest.java
new file mode 100644
index 000000000..0afc11968
--- /dev/null
+++ b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03InterceptorSupportTest.java
@@ -0,0 +1,45 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders;
+import org.junit.jupiter.api.Test;
+
+class Compat03InterceptorSupportTest {
+
+ @Test
+ void stripsVersionHeaderFromInitialContextBeforeInterceptors() {
+ AtomicReference