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.
*
@@ -401,4 +430,20 @@ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl,
.build();
return resolver.getAgentCard();
}
+
+ /**
+ * 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..1e3d24d3b 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
@@ -12,6 +12,7 @@
import java.util.stream.Collectors;
import org.a2aproject.sdk.client.config.ClientConfig;
+import org.a2aproject.sdk.client.http.A2ACardResolver;
import org.a2aproject.sdk.client.transport.spi.ClientTransport;
import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
import org.a2aproject.sdk.client.transport.spi.ClientTransportConfigBuilder;
@@ -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;
@@ -170,6 +180,9 @@ public ClientBuilder withTransport(Class clazz, C
* @return this builder for method chaining
*/
public ClientBuilder withTransport(Class clazz, ClientTransportConfig config) {
+ if (!transportProtocolMapping.containsKey(clazz)) {
+ throw new IllegalArgumentException("No ClientTransportProvider is registered for " + clazz.getName());
+ }
clientTransports.put(clazz, config);
return this;
@@ -286,8 +299,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 +326,69 @@ 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);
+ }
+ }
+ if (ordered.isEmpty()) {
+ throw new A2AClientException(
+ "AgentCard does not advertise a supported protocol version (supported: 1.0 and 0.3)");
}
- return serverInterfacesMap;
+ return ordered;
}
private List getClientPreferredTransports() {
@@ -355,22 +405,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 +442,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..9f3650829
--- /dev/null
+++ b/client/base/src/main/java/org/a2aproject/sdk/client/VersionedClientTransportProvider.java
@@ -0,0 +1,45 @@
+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 {
+ /**
+ * Returns the protocol binding supported by this provider.
+ *
+ * @return the protocol binding name
+ */
+ String protocolBinding();
+
+ /**
+ * Returns the protocol version supported by this provider.
+ *
+ * @return the protocol version
+ */
+ String protocolVersion();
+
+ /**
+ * Returns the native transport class whose configuration this provider consumes.
+ *
+ * @return the configured transport class
+ */
+ Class extends ClientTransport> configuredTransportClass();
+
+ /**
+ * Creates a transport for the supplied agent interface.
+ *
+ * @param config the native transport configuration
+ * @param card the agent card
+ * @param agentInterface the selected agent interface
+ * @return the configured transport
+ * @throws A2AClientException if the configuration or agent interface is invalid
+ */
+ ClientTransport create(ClientTransportConfig> config, AgentCard card, AgentInterface agentInterface)
+ throws A2AClientException;
+}
diff --git a/client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java b/client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java
index 3f1c120cc..ed708b7a8 100644
--- a/client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java
+++ b/client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java
@@ -7,6 +7,8 @@
import org.a2aproject.sdk.client.http.A2AHttpClientFactory;
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.client.transport.jsonrpc.JSONRPCTransport;
import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransportConfig;
import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransportConfigBuilder;
@@ -78,6 +80,16 @@ public void shouldNotFindConfigurationTransport() throws A2AClientException {
Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().startsWith("Missing required TransportConfig for"));
}
+ @Test
+ public void shouldRejectUnregisteredTransportAtConfigurationTime() {
+ IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class,
+ () -> Client.builder(card).withTransport(UnregisteredTransport.class,
+ new ClientTransportConfig() { }));
+
+ Assertions.assertTrue(exception.getMessage().contains(UnregisteredTransport.class.getName()));
+ Assertions.assertTrue(exception.getMessage().contains("No ClientTransportProvider is registered"));
+ }
+
@Test
public void shouldCreateJSONRPCClient() throws A2AClientException {
Client client = Client
@@ -172,4 +184,7 @@ public void shouldHaveNullTenantWhenNotSet() throws A2AClientException {
Assertions.assertNull(selectedInterface.tenant());
}
+
+ private abstract static class UnregisteredTransport implements ClientTransport {
+ }
}
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..e822a08a9
--- /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.4.1.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..4b7e21127
--- /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.validateProtobufMessageSend(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.toV03Protobuf(ProtoUtils.FromProto.messageSendParams(payloadPayload(payload))),
+ legacyContext(context, payload))));
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext context) {
+ Compat03ClientTransportSupport.validateProtobufMessageSend(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.toV03Protobuf(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.validateProtobufCancel(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.toV03Protobuf(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..134d0a6e8
--- /dev/null
+++ b/compat-0.3/client/adapter-grpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03ClientTransportProviderTest.java
@@ -0,0 +1,196 @@
+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.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import io.grpc.ManagedChannel;
+import io.grpc.Server;
+import io.grpc.Status;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.stub.StreamObserver;
+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.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.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.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.TextPart;
+import org.a2aproject.sdk.spec.TransportProtocol;
+import org.junit.jupiter.api.Test;
+
+class GrpcCompat03ClientTransportProviderTest {
+ @Test
+ void sendsCompletePushConfigurationResourceNamesIncludingDefaultAndClearedIds() throws Exception {
+ String name = InProcessServerBuilder.generateName();
+ AtomicReference receivedName = new AtomicReference<>();
+ Server server = InProcessServerBuilder.forName(name).directExecutor()
+ .addService(new A2AServiceGrpc.A2AServiceImplBase() {
+ @Override
+ public void getTaskPushNotificationConfig(
+ org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequest request,
+ StreamObserver observer) {
+ receivedName.set(request.getName());
+ // Validate the contract directly rather than using the permissive reference parser.
+ if (!request.getName().matches("tasks/task-123/pushNotificationConfigs/(task-123|specific)")) {
+ observer.onError(Status.INVALID_ARGUMENT.withDescription("Invalid config resource name")
+ .asRuntimeException());
+ return;
+ }
+ String configId = request.getName().substring(request.getName().lastIndexOf('/') + 1);
+ observer.onNext(org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder()
+ .setName(request.getName())
+ .setPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.PushNotificationConfig.newBuilder()
+ .setId(configId).setUrl("https://example.test/callback"))
+ .build());
+ observer.onCompleted();
+ }
+ }).build().start();
+ ManagedChannel channel = InProcessChannelBuilder.forName(name).directExecutor().build();
+ try {
+ AgentCard card = card(name);
+ var provider = new GrpcCompat03ClientTransportProvider();
+ ClientTransport transport = provider.create(
+ new GrpcTransportConfigBuilder().channelFactory(ignored -> channel).build(), card,
+ card.supportedInterfaces().get(0));
+ try {
+ assertEquals("task-123", transport.getTaskPushNotificationConfiguration(
+ new GetTaskPushNotificationConfigParams("task-123"), null).id());
+ assertEquals("tasks/task-123/pushNotificationConfigs/task-123", receivedName.get());
+ assertEquals("specific", transport.getTaskPushNotificationConfiguration(
+ new GetTaskPushNotificationConfigParams("task-123", "specific"), null).id());
+ assertEquals("tasks/task-123/pushNotificationConfigs/specific", receivedName.get());
+ } finally {
+ transport.close();
+ }
+
+ ClientCallInterceptor clearingInterceptor = new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, Map headers,
+ AgentCard agentCard, ClientCallContext context) {
+ var request = (org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest) payload;
+ return new PayloadAndHeaders(request.toBuilder().clearId().build(), headers);
+ }
+ };
+ ClientTransport intercepted = provider.create(new GrpcTransportConfigBuilder()
+ .channelFactory(ignored -> channel).addInterceptor(clearingInterceptor).build(), card,
+ card.supportedInterfaces().get(0));
+ try {
+ assertEquals("task-123", intercepted.getTaskPushNotificationConfiguration(
+ new GetTaskPushNotificationConfigParams("task-123", "specific"), null).id());
+ assertEquals("tasks/task-123/pushNotificationConfigs/task-123", receivedName.get());
+ } finally {
+ intercepted.close();
+ }
+ } finally {
+ channel.shutdownNow();
+ server.shutdownNow();
+ assertTrue(channel.awaitTermination(5, TimeUnit.SECONDS));
+ assertTrue(server.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+
+ @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-grpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03UnsupportedFieldsTest.java b/compat-0.3/client/adapter-grpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03UnsupportedFieldsTest.java
new file mode 100644
index 000000000..de2cdad37
--- /dev/null
+++ b/compat-0.3/client/adapter-grpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/grpc/GrpcCompat03UnsupportedFieldsTest.java
@@ -0,0 +1,105 @@
+package org.a2aproject.sdk.compat03.client.adapter.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+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.function.Consumer;
+
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+import io.grpc.ManagedChannel;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+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.Compat03ClientTransportBase;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.spec.EventKind_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.Task_v0_3;
+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.CancelTaskParams;
+import org.a2aproject.sdk.spec.Message;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.TextPart;
+import org.a2aproject.sdk.spec.UnsupportedOperationError;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+
+class GrpcCompat03UnsupportedFieldsTest {
+ @Test
+ void rejectsOriginalFieldsAndInterceptorMutationsBeforeDelegation() {
+ AgentCard card = AgentCard.builder().name("legacy").description("legacy").version("1")
+ .url("https://example.test").capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text")).skills(List.of())
+ .supportedInterfaces(List.of(new AgentInterface("GRPC", "https://example.test", null, "0.3"))).build();
+ ManagedChannel channel = InProcessChannelBuilder.forName(InProcessServerBuilder.generateName())
+ .directExecutor().build();
+ var delegate = new GrpcTransport_v0_3(channel, Compat03ClientTransportBase.legacyCard(card)) {
+ @Override
+ public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) {
+ throw new AssertionError("Unsupported request reached the delegate");
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams_v0_3 request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext_v0_3 context) {
+ throw new AssertionError("Unsupported streaming request reached the delegate");
+ }
+
+ @Override
+ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) {
+ throw new AssertionError("Unsupported cancellation reached the delegate");
+ }
+ };
+ try {
+ for (boolean intercepted : List.of(false, true)) {
+ var transport = new GrpcCompat03ClientTransport(delegate, card,
+ intercepted ? List.of(unsupportedFieldsInterceptor()) : List.of());
+ var request = new MessageSendParams(new Message(Message.Role.ROLE_USER, List.of(new TextPart("hello")),
+ "request", null, null, intercepted ? null : List.of("referenced-task"), null, null), null, null);
+ assertUnsupported("referenceTaskIds", () -> transport.sendMessage(request, null));
+ assertUnsupported("referenceTaskIds", () -> transport.sendMessageStreaming(request,
+ event -> { }, error -> { throw new AssertionError(error); }, null));
+ var cancel = new CancelTaskParams("task", null, intercepted ? Map.of() : Map.of("reason", "stop"));
+ assertUnsupported("metadata", () -> transport.cancelTask(cancel, null));
+ }
+ } finally {
+ channel.shutdownNow();
+ }
+ }
+
+ private static void assertUnsupported(String field, Runnable call) {
+ A2AClientException error = assertThrows(A2AClientException.class, call::run);
+ assertInstanceOf(UnsupportedOperationError.class, error.getCause());
+ assertTrue(error.getMessage().contains(field));
+ }
+
+ private static ClientCallInterceptor unsupportedFieldsInterceptor() {
+ return new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, @Nullable Object payload, Map headers,
+ @Nullable AgentCard card, @Nullable ClientCallContext context) {
+ if (payload instanceof org.a2aproject.sdk.grpc.SendMessageRequest) {
+ var request = ((org.a2aproject.sdk.grpc.SendMessageRequest) payload).toBuilder();
+ request.getMessageBuilder().addReferenceTaskIds("referenced-task");
+ return new PayloadAndHeaders(request.build(), headers);
+ }
+ Struct metadata = Struct.newBuilder().putFields("reason", Value.newBuilder().setStringValue("stop").build()).build();
+ var request = ((org.a2aproject.sdk.grpc.CancelTaskRequest) payload).toBuilder();
+ request.setMetadata(metadata);
+ return new PayloadAndHeaders(request.build(), headers);
+ }
+ };
+ }
+}
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..ee608d323
--- /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.4.1.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..0b5960f64
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransport.java
@@ -0,0 +1,183 @@
+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.compat03.conversion.mappers.params.MessageSendParamsMapper_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_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) {
+ MessageSendParams_v0_3 legacyRequest = Compat03ClientTransportSupport.toV03(request);
+ org.a2aproject.sdk.grpc.SendMessageRequest original = ProtoUtils.ToProto.sendMessageRequest(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SEND_MESSAGE_METHOD,
+ original, org.a2aproject.sdk.grpc.SendMessageRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> Compat03ClientTransportSupport.toV10(delegate.sendMessage(
+ adaptMessage(legacyRequest, original,
+ (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) {
+ MessageSendParams_v0_3 legacyRequest = Compat03ClientTransportSupport.toV03(request);
+ org.a2aproject.sdk.grpc.SendMessageRequest original = ProtoUtils.ToProto.sendMessageRequest(request);
+ PayloadAndHeaders payload = apply(A2AMethods.SEND_STREAMING_MESSAGE_METHOD,
+ original, org.a2aproject.sdk.grpc.SendMessageRequest.class, context);
+ Compat03ClientTransportSupport.run(() -> delegate.sendMessageStreaming(
+ adaptMessage(legacyRequest, original,
+ (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);
+ org.a2aproject.sdk.grpc.CancelTaskRequest original = ProtoUtils.ToProto.cancelTaskRequest(request);
+ PayloadAndHeaders payload = apply(A2AMethods.CANCEL_TASK_METHOD, original,
+ org.a2aproject.sdk.grpc.CancelTaskRequest.class, context);
+ return Compat03ClientTransportSupport.call(() -> TaskMapper_v0_3.INSTANCE.toV10(delegate.cancelTask(
+ adaptCancel(request, original, (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 static MessageSendParams_v0_3 adaptMessage(MessageSendParams_v0_3 legacyRequest,
+ org.a2aproject.sdk.grpc.SendMessageRequest original, org.a2aproject.sdk.grpc.SendMessageRequest modified) {
+ if (original.equals(modified)) {
+ return legacyRequest;
+ }
+ return JSONRPCCompat03PayloadSupport.preserveUnchangedValues(legacyRequest,
+ MessageSendParamsMapper_v0_3.INSTANCE.fromV10(ProtoUtils.FromProto.messageSendParams(original)),
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.messageSendParams(modified)));
+ }
+
+ private static TaskIdParams_v0_3 adaptCancel(CancelTaskParams request,
+ org.a2aproject.sdk.grpc.CancelTaskRequest original, org.a2aproject.sdk.grpc.CancelTaskRequest modified) {
+ TaskIdParams_v0_3 legacy = Compat03ClientTransportSupport.toV03(request);
+ if (original.equals(modified)) {
+ return legacy;
+ }
+ return JSONRPCCompat03PayloadSupport.preserveUnchangedValues(legacy,
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.cancelTaskParams(original)),
+ Compat03ClientTransportSupport.toV03(ProtoUtils.FromProto.cancelTaskParams(modified)));
+ }
+
+ 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..6135b8937
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransportProvider.java
@@ -0,0 +1,36 @@
+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());
+ if (!(config instanceof JSONRPCTransportConfig nativeConfig)) {
+ throw new A2AClientException("Expected JSONRPCTransportConfig for the JSON-RPC 0.3 adapter");
+ }
+ 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/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03PayloadSupport.java b/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03PayloadSupport.java
new file mode 100644
index 000000000..8feabfd0b
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03PayloadSupport.java
@@ -0,0 +1,148 @@
+package org.a2aproject.sdk.compat03.client.adapter.jsonrpc;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.a2aproject.sdk.compat03.spec.DataPart_v0_3;
+import org.a2aproject.sdk.compat03.spec.FilePart_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.Part_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TextPart_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.jspecify.annotations.Nullable;
+
+/** Retains original JSON values in fields that native protobuf interceptors have not changed. */
+final class JSONRPCCompat03PayloadSupport {
+ private JSONRPCCompat03PayloadSupport() {
+ }
+
+ static MessageSendParams_v0_3 preserveUnchangedValues(MessageSendParams_v0_3 original,
+ MessageSendParams_v0_3 before, MessageSendParams_v0_3 after) {
+ Message_v0_3 source = original.message();
+ Message_v0_3 baseline = before.message();
+ Message_v0_3 modified = after.message();
+ List> parts = modified.parts();
+ if (baseline.parts().equals(modified.parts())) {
+ parts = source.parts();
+ } else if (source.parts().size() == 1 && baseline.parts().size() == 1 && modified.parts().size() == 1) {
+ parts = List.of(restorePart(source.parts().get(0), baseline.parts().get(0), modified.parts().get(0)));
+ } else if (hasLossyNumbers(source.parts(), baseline.parts())) {
+ throw ambiguousArray();
+ }
+ Message_v0_3 message = new Message_v0_3(modified.role(), parts, modified.messageId(), modified.contextId(),
+ modified.taskId(), modified.referenceTaskIds(),
+ restoreMap(source.metadata(), baseline.metadata(), modified.metadata()), modified.extensions());
+ return new MessageSendParams_v0_3(message, after.configuration(),
+ restoreMap(original.metadata(), before.metadata(), after.metadata()));
+ }
+
+ static TaskIdParams_v0_3 preserveUnchangedValues(TaskIdParams_v0_3 original,
+ TaskIdParams_v0_3 before, TaskIdParams_v0_3 after) {
+ return new TaskIdParams_v0_3(after.id(), restoreMap(original.metadata(), before.metadata(), after.metadata()));
+ }
+
+ private static Part_v0_3> restorePart(Part_v0_3> original, Part_v0_3> before, Part_v0_3> after) {
+ if (before.equals(after)) {
+ return original;
+ }
+ @Nullable Map metadata = restoreMap(partMetadata(original), partMetadata(before), partMetadata(after));
+ if (after instanceof DataPart_v0_3 modified) {
+ Map data = modified.data();
+ if (original instanceof DataPart_v0_3 source && before instanceof DataPart_v0_3 baseline) {
+ data = Objects.requireNonNull(restoreMap(source.data(), baseline.data(), modified.data()));
+ }
+ return new DataPart_v0_3(data, metadata);
+ }
+ if (after instanceof TextPart_v0_3 modified) {
+ return new TextPart_v0_3(modified.text(), metadata);
+ }
+ FilePart_v0_3 modified = (FilePart_v0_3) after;
+ return new FilePart_v0_3(modified.file(), metadata);
+ }
+
+ private static @Nullable Map partMetadata(Part_v0_3> part) {
+ if (part instanceof DataPart_v0_3 data) {
+ return data.metadata();
+ }
+ if (part instanceof TextPart_v0_3 text) {
+ return text.metadata();
+ }
+ return ((FilePart_v0_3) part).metadata();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static @Nullable Map restoreMap(@Nullable Map original,
+ @Nullable Map before, @Nullable Map after) {
+ return (Map) restore(original, before, after);
+ }
+
+ private static @Nullable Object restore(@Nullable Object original, @Nullable Object before, @Nullable Object after) {
+ // Struct/Value uses doubles. Restore unchanged values directly from the original objects,
+ // avoiding another JSON parse that could itself round large integers or decimal numbers.
+ if (Objects.equals(before, after)) {
+ return original;
+ }
+ if (original instanceof Map, ?> source && before instanceof Map, ?> baseline
+ && after instanceof Map, ?> modified) {
+ Map result = new LinkedHashMap<>();
+ modified.forEach((key, value) -> result.put(key, source.containsKey(key) && baseline.containsKey(key)
+ ? restore(source.get(key), baseline.get(key), value) : value));
+ return result;
+ }
+ if (original instanceof List> source && before instanceof List> baseline && after instanceof List> modified) {
+ if (source.size() == 1 && baseline.size() == 1 && modified.size() == 1) {
+ // Array slots have no identity. Only a singleton has an unambiguous slot.
+ List<@Nullable Object> result = new ArrayList<>();
+ result.add(restore(source.get(0), baseline.get(0), modified.get(0)));
+ return result;
+ }
+ if (hasLossyNumbers(source, baseline)) {
+ throw ambiguousArray();
+ }
+ }
+ return after;
+ }
+
+ private static boolean hasLossyNumbers(@Nullable Object original, @Nullable Object projected) {
+ if (original instanceof Number source && projected instanceof Number baseline) {
+ return !Double.isFinite(baseline.doubleValue())
+ || new BigDecimal(source.toString()).compareTo(new BigDecimal(baseline.toString())) != 0;
+ }
+ if (original instanceof DataPart_v0_3 source && projected instanceof DataPart_v0_3 baseline) {
+ return hasLossyNumbers(source.metadata(), baseline.metadata())
+ || hasLossyNumbers(source.data(), baseline.data());
+ }
+ if (original instanceof TextPart_v0_3 source && projected instanceof TextPart_v0_3 baseline) {
+ return hasLossyNumbers(source.metadata(), baseline.metadata());
+ }
+ if (original instanceof FilePart_v0_3 source && projected instanceof FilePart_v0_3 baseline) {
+ return hasLossyNumbers(source.metadata(), baseline.metadata());
+ }
+ if (original instanceof Map, ?> source && projected instanceof Map, ?> baseline) {
+ for (var entry : source.entrySet()) {
+ if (hasLossyNumbers(entry.getValue(), baseline.get(entry.getKey()))) {
+ return true;
+ }
+ }
+ }
+ if (original instanceof List> source && projected instanceof List> baseline) {
+ for (int i = 0; i < source.size() && i < baseline.size(); i++) {
+ if (hasLossyNumbers(source.get(i), baseline.get(i))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static A2AClientException ambiguousArray() {
+ return new A2AClientException("Cannot preserve exact JSON numbers after a protobuf interceptor edits "
+ + "a non-singleton array. Keep that array unchanged or use strings for exact numeric identifiers.");
+ }
+}
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..fa43cbee4
--- /dev/null
+++ b/compat-0.3/client/adapter-jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/adapter/jsonrpc/JSONRPCCompat03ClientTransportTest.java
@@ -0,0 +1,441 @@
+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.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport;
+import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
+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.transport.spi.ClientTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.json.JsonUtil_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.Message_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.CancelTaskParams;
+import org.a2aproject.sdk.spec.DataPart;
+import org.a2aproject.sdk.spec.FilePart;
+import org.a2aproject.sdk.spec.FileWithBytes;
+import org.a2aproject.sdk.spec.Message;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+import org.a2aproject.sdk.spec.TextPart;
+import org.a2aproject.sdk.spec.UnsupportedOperationError;
+import org.junit.jupiter.api.Test;
+
+class JSONRPCCompat03ClientTransportTest {
+ @Test
+ void preservesSingletonArrayNumbersAndAppliesMapAdditionsRemovalsAndReplacements() {
+ long id = 9007199254740993L;
+ double projectedId = (double) id;
+ var original = new TaskIdParams_v0_3("task", Map.of(
+ "items", List.of(Map.of("id", id, "counter", 1L)), "remove", "old", "replace", id));
+ var before = new TaskIdParams_v0_3("task", Map.of(
+ "items", List.of(Map.of("id", projectedId, "counter", 1.0)), "remove", "old", "replace", projectedId));
+ var after = new TaskIdParams_v0_3("task", Map.of(
+ "items", List.of(Map.of("id", projectedId, "counter", 2.0)), "add", true, "replace", 42.0));
+ var result = JSONRPCCompat03PayloadSupport.preserveUnchangedValues(original, before, after);
+ assertEquals(Map.of("items", List.of(Map.of("id", id, "counter", 2.0)),
+ "add", true, "replace", 42.0), result.metadata());
+ }
+
+ @Test
+ void acceptsLosslessArrayEditsAndTreatsIdenticalProtobufNumbersAsUnchanged() {
+ var original = new TaskIdParams_v0_3("task", Map.of("ids", List.of(1L, 7L)));
+ var before = new TaskIdParams_v0_3("task", Map.of("ids", List.of(1.0, 7.0)));
+ var after = new TaskIdParams_v0_3("task", Map.of("ids", List.of(7.0)));
+ assertEquals(after, JSONRPCCompat03PayloadSupport.preserveUnchangedValues(original, before, after));
+
+ // The interceptor contract treats identical protobuf values as unchanged, even for replacements.
+ var exact = new TaskIdParams_v0_3("task", Map.of("id", 9007199254740993L));
+ var projected = new TaskIdParams_v0_3("task", Map.of("id", 9007199254740992.0));
+ assertEquals(exact, JSONRPCCompat03PayloadSupport.preserveUnchangedValues(exact, projected, projected));
+ }
+
+ @Test
+ void preservesMetadataWhenAnInterceptorChangesASingletonPartKind() {
+ ClientCallInterceptor interceptor = new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, Map headers,
+ AgentCard card, ClientCallContext context) {
+ var request = (org.a2aproject.sdk.grpc.SendMessageRequest) payload;
+ var part = request.getMessage().getParts(0).toBuilder().setData(
+ com.google.protobuf.Value.newBuilder().setStructValue(
+ com.google.protobuf.Struct.newBuilder().putFields("new",
+ com.google.protobuf.Value.newBuilder().setBoolValue(true).build())));
+ return new PayloadAndHeaders(request.toBuilder().setMessage(request.getMessage().toBuilder()
+ .setParts(0, part)).build(), headers);
+ }
+ };
+ RecordingDelegate delegate = new RecordingDelegate();
+ delegate.response = new Message_v0_3.Builder().role(Message_v0_3.Role.AGENT)
+ .messageId("response").parts(List.of(new org.a2aproject.sdk.compat03.spec.TextPart_v0_3("ok"))).build();
+ var transport = new JSONRPCCompat03ClientTransport(delegate, testCard(), List.of(interceptor));
+ var request = new MessageSendParams(new Message(Message.Role.ROLE_USER,
+ List.of(new TextPart("original", Map.of("id", 9007199254740993L))),
+ "request", null, null, null, null, null), null, null);
+ transport.sendMessage(request, null);
+ var part = assertInstanceOf(org.a2aproject.sdk.compat03.spec.DataPart_v0_3.class,
+ delegate.sent.message().parts().get(0));
+ assertEquals(Map.of("new", true), part.data());
+ assertEquals(Map.of("id", 9007199254740993L), part.metadata());
+ transport.sendMessageStreaming(request, event -> { }, failure -> { }, null);
+ part = assertInstanceOf(org.a2aproject.sdk.compat03.spec.DataPart_v0_3.class,
+ delegate.sent.message().parts().get(0));
+ assertEquals(Map.of("id", 9007199254740993L), part.metadata());
+ }
+
+ @Test
+ void rejectsAmbiguousArrayEditsBeforeBlockingOrStreamingDelegation() {
+ // Removing A and editing B must neither round B's untouched ID nor restore A's stale ID.
+ assertAmbiguousMutationRejected(
+ List.of(new DataPart(Map.of("id", 1L, "counter", 1L)),
+ new DataPart(Map.of("id", 9007199254740993L, "counter", 1L))),
+ List.of(new DataPart(Map.of("id", 9007199254740992L, "counter", 2L))));
+ assertAmbiguousMutationRejected(
+ List.of(new DataPart(Map.of("id", 9007199254740993L, "counter", 1L)),
+ new DataPart(Map.of("id", 7L, "counter", 1L))),
+ List.of(new DataPart(Map.of("id", 9007199254740992L, "counter", 2L))));
+ // Both original integers project to the same double, so exact matching cannot identify the survivor.
+ assertAmbiguousMutationRejected(
+ List.of(new DataPart(Map.of("nested", List.of(9007199254740992L, 9007199254740993L)))),
+ List.of(new DataPart(Map.of("nested", List.of(9007199254740992L)))));
+ assertAmbiguousMutationRejected(
+ List.of(new DataPart(Map.of("id", 9007199254740993L)), new TextPart("move me")),
+ List.of(new TextPart("move me"), new DataPart(Map.of("id", 9007199254740992L))));
+ }
+
+ private static void assertAmbiguousMutationRejected(List> before,
+ List> after) {
+ Message message = new Message(Message.Role.ROLE_USER, before, "request", null, null, null, null, null);
+ Message replacement = Message.builder(message).parts(after).build();
+ ClientCallInterceptor interceptor = new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, Map headers,
+ AgentCard card, ClientCallContext context) {
+ var request = (org.a2aproject.sdk.grpc.SendMessageRequest) payload;
+ return new PayloadAndHeaders(request.toBuilder()
+ .setMessage(org.a2aproject.sdk.grpc.utils.ProtoUtils.ToProto.message(replacement)).build(), headers);
+ }
+ };
+ RecordingDelegate delegate = new RecordingDelegate();
+ var transport = new JSONRPCCompat03ClientTransport(delegate, testCard(), List.of(interceptor));
+ var request = new MessageSendParams(message, null, null);
+ var error = assertThrows(org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> transport.sendMessage(request, null));
+ assertTrue(error.getMessage().contains("non-singleton array"));
+ assertThrows(org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> transport.sendMessageStreaming(request, event -> { }, failure -> { }, null));
+ assertNull(delegate.sent);
+ assertFalse(delegate.called);
+ }
+
+ @Test
+ void rejectsAmbiguousCancellationMetadataArrayEditsBeforeDelegation() {
+ ClientCallInterceptor interceptor = new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, Map headers,
+ AgentCard card, ClientCallContext context) {
+ var request = (org.a2aproject.sdk.grpc.CancelTaskRequest) payload;
+ var values = request.getMetadata().getFieldsOrThrow("ids").getListValue();
+ var remaining = com.google.protobuf.Value.newBuilder().setListValue(
+ com.google.protobuf.ListValue.newBuilder().addValues(values.getValues(1))).build();
+ return new PayloadAndHeaders(request.toBuilder().setMetadata(request.getMetadata().toBuilder()
+ .putFields("ids", remaining)).build(), headers);
+ }
+ };
+ RecordingDelegate delegate = new RecordingDelegate();
+ var transport = new JSONRPCCompat03ClientTransport(delegate, testCard(), List.of(interceptor));
+ assertThrows(org.a2aproject.sdk.spec.A2AClientException.class, () -> transport.cancelTask(
+ new CancelTaskParams("task", null, Map.of("ids", List.of(9007199254740992L, 9007199254740993L))), null));
+ assertNull(delegate.cancelled);
+ assertFalse(delegate.called);
+ }
+
+ @Test
+ void preservesLargeIntegersForBlockingStreamingAndCancellation() throws Exception {
+ checkNumericPrecision(List.of(), false, false);
+ checkNumericPrecision(List.of(new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, Map headers,
+ AgentCard card, ClientCallContext context) {
+ return new PayloadAndHeaders(payload, Map.of("A2A-Extensions", "urn:example:extension"));
+ }
+ }), false, true);
+ checkNumericPrecision(List.of(new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, Map headers,
+ AgentCard card, ClientCallContext context) {
+ var value = com.google.protobuf.Value.newBuilder().setNumberValue(2).build();
+ if (payload instanceof org.a2aproject.sdk.grpc.SendMessageRequest request) {
+ return new PayloadAndHeaders(request.toBuilder().setMessage(request.getMessage().toBuilder()
+ .setMessageId("changed"))
+ .setMetadata(request.getMetadata().toBuilder().putFields("counter", value)).build(), headers);
+ }
+ var request = (org.a2aproject.sdk.grpc.CancelTaskRequest) payload;
+ return new PayloadAndHeaders(request.toBuilder().setId("changed-task")
+ .setMetadata(request.getMetadata().toBuilder().putFields("counter", value)).build(), headers);
+ }
+ }), true, false);
+ }
+
+ private static void checkNumericPrecision(List interceptors,
+ boolean changedPayload, boolean extensionHeader) throws Exception {
+ long id = 9007199254740993L;
+ Map metadata = Map.of("id", id);
+ BigDecimal decimal = new BigDecimal("0.12345678901234567890123456789");
+ Map requestMetadata = Map.of("id", id, "counter", 1L, "decimal", decimal);
+ RecordingDelegate delegate = new RecordingDelegate();
+ delegate.response = new Message_v0_3.Builder().role(Message_v0_3.Role.AGENT)
+ .messageId("response").parts(List.of(new org.a2aproject.sdk.compat03.spec.TextPart_v0_3("ok"))).build();
+ var transport = new JSONRPCCompat03ClientTransport(delegate, testCard(), interceptors);
+ var message = new Message(Message.Role.ROLE_USER,
+ List.of(new DataPart(Map.of("nested", List.of(metadata)), metadata)),
+ "request", null, null, null, metadata, null);
+ var request = new MessageSendParams(message, null, requestMetadata);
+
+ transport.sendMessage(request, null);
+ assertExactNumbers(delegate.sent, id);
+ assertEquals(changedPayload ? "changed" : "request", delegate.sent.message().messageId());
+ assertEquals(changedPayload ? 2.0 : 1.0, ((Number) delegate.sent.metadata().get("counter")).doubleValue());
+ assertEquals(decimal, delegate.sent.metadata().get("decimal"));
+ transport.sendMessageStreaming(request, event -> { }, error -> { throw new AssertionError(error); }, null);
+ assertExactNumbers(delegate.sent, id);
+ assertEquals(changedPayload ? "changed" : "request", delegate.sent.message().messageId());
+ assertEquals(changedPayload ? 2.0 : 1.0, ((Number) delegate.sent.metadata().get("counter")).doubleValue());
+ assertEquals(decimal, delegate.sent.metadata().get("decimal"));
+ transport.cancelTask(new CancelTaskParams("task", null, requestMetadata), null);
+ assertEquals(id, delegate.cancelled.metadata().get("id"));
+ assertEquals(decimal, delegate.cancelled.metadata().get("decimal"));
+ assertEquals(changedPayload ? 2.0 : 1.0, ((Number) delegate.cancelled.metadata().get("counter")).doubleValue());
+ assertTrue(JsonUtil_v0_3.toJson(delegate.cancelled).contains("9007199254740993"));
+ assertEquals(changedPayload ? "changed-task" : "task", delegate.cancelled.id());
+ if (extensionHeader) {
+ assertEquals("urn:example:extension", delegate.context.getHeaders().get("X-A2A-Extensions"));
+ }
+ assertFalse(delegate.context.getHeaders().containsKey("A2A-Extensions"));
+ }
+
+ private static void assertExactNumbers(MessageSendParams_v0_3 request, long id) throws Exception {
+ assertEquals(id, request.metadata().get("id"));
+ assertEquals(Map.of("id", id), request.message().metadata());
+ var part = assertInstanceOf(org.a2aproject.sdk.compat03.spec.DataPart_v0_3.class, request.message().parts().get(0));
+ assertEquals(Map.of("id", id), part.metadata());
+ assertEquals(Map.of("nested", List.of(Map.of("id", id))), part.data());
+ assertTrue(JsonUtil_v0_3.toJson(request).contains("9007199254740993"));
+ }
+
+ @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 providerRejectsWrongConfigurationType() {
+ org.a2aproject.sdk.spec.A2AClientException exception = assertThrows(
+ org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> new JSONRPCCompat03ClientTransportProvider().create(
+ new ClientTransportConfig() { },
+ testCard(), testCard().supportedInterfaces().get(0)));
+
+ assertTrue(exception.getMessage().contains("JSONRPCTransportConfig"));
+ }
+
+ @Test
+ void providerRejectsNullConfiguration() {
+ org.a2aproject.sdk.spec.A2AClientException exception = assertThrows(
+ org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> new JSONRPCCompat03ClientTransportProvider().create(
+ null, testCard(), testCard().supportedInterfaces().get(0)));
+
+ assertTrue(exception.getMessage().contains("JSONRPCTransportConfig"));
+ }
+
+ @Test
+ void receivesInlineFilesWithoutMimeTypeForBlockingAndStreamingSends() throws Exception {
+ RecordingDelegate delegate = new RecordingDelegate();
+ delegate.response = JsonUtil_v0_3.fromJson("""
+ {"kind":"message","role":"agent","messageId":"response",
+ "parts":[{"kind":"file","file":{"bytes":"aGVsbG8="}}]}
+ """, Message_v0_3.class);
+ JSONRPCCompat03ClientTransport transport = new JSONRPCCompat03ClientTransport(
+ delegate, testCard(), List.of());
+ MessageSendParams request = new MessageSendParams(
+ new Message(Message.Role.ROLE_USER, List.of(new TextPart("hello")), "request",
+ null, null, null, null, null), null, null);
+
+ Message blocking = assertInstanceOf(Message.class, transport.sendMessage(request, null));
+ AtomicReference received = new AtomicReference<>();
+ AtomicReference error = new AtomicReference<>();
+ transport.sendMessageStreaming(request, received::set, error::set, null);
+
+ assertNull(error.get());
+ Message streaming = assertInstanceOf(Message.class, received.get());
+ for (Message response : List.of(blocking, streaming)) {
+ assertEquals("response", response.messageId());
+ FilePart part = assertInstanceOf(FilePart.class, response.parts().get(0));
+ FileWithBytes file = assertInstanceOf(FileWithBytes.class, part.file());
+ assertEquals("", file.mimeType());
+ assertEquals("", file.name());
+ assertEquals("aGVsbG8=", file.bytes());
+ }
+ }
+
+ @Test
+ void rejectsNonObjectDataIntroducedByAnInterceptorBeforeDelegation() {
+ ClientCallInterceptor interceptor = new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, java.util.Map headers,
+ AgentCard card, ClientCallContext context) {
+ Message replacement = new Message(Message.Role.ROLE_USER, List.of(new DataPart(List.of("item"))),
+ "request", null, null, null, null, null);
+ org.a2aproject.sdk.grpc.SendMessageRequest request =
+ (org.a2aproject.sdk.grpc.SendMessageRequest) payload;
+ return new PayloadAndHeaders(request.toBuilder()
+ .setMessage(org.a2aproject.sdk.grpc.utils.ProtoUtils.ToProto.message(replacement)).build(), headers);
+ }
+ };
+ RecordingDelegate delegate = new RecordingDelegate();
+ JSONRPCCompat03ClientTransport transport = new JSONRPCCompat03ClientTransport(
+ delegate, testCard(), List.of(interceptor));
+ MessageSendParams request = new MessageSendParams(
+ new Message(Message.Role.ROLE_USER, List.of(new TextPart("hello")), "request",
+ null, null, null, null, null), null, null);
+
+ var blocking = assertThrows(org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> transport.sendMessage(request, null));
+ var streaming = assertThrows(org.a2aproject.sdk.spec.A2AClientException.class,
+ () -> transport.sendMessageStreaming(request, event -> { }, error -> { }, null));
+
+ assertInstanceOf(UnsupportedOperationError.class, blocking.getCause());
+ assertInstanceOf(UnsupportedOperationError.class, streaming.getCause());
+ assertFalse(delegate.called);
+ }
+
+ @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;
+ Message_v0_3 response;
+ MessageSendParams_v0_3 sent;
+ TaskIdParams_v0_3 cancelled;
+ ClientCallContext_v0_3 context;
+
+ @Override public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, ClientCallContext_v0_3 context) {
+ called = true;
+ sent = request;
+ this.context = context;
+ assertNotNull(response);
+ return response;
+ }
+ @Override public void sendMessageStreaming(MessageSendParams_v0_3 request, java.util.function.Consumer events,
+ java.util.function.Consumer errors, ClientCallContext_v0_3 context) {
+ called = true;
+ sent = request;
+ this.context = context;
+ assertNotNull(response);
+ events.accept(response);
+ }
+ @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) {
+ cancelled = request;
+ this.context = context;
+ return new org.a2aproject.sdk.compat03.spec.Task_v0_3(request.id(), "context",
+ new org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3(org.a2aproject.sdk.compat03.spec.TaskState_v0_3.CANCELED),
+ List.of(), List.of(), 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..dd09554af
--- /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.4.1.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..8a9c880d3
--- /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.validateProtobufMessageSend(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.toV03Protobuf(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.validateProtobufMessageSend(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.toV03Protobuf(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.validateProtobufCancel(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.toV03Protobuf(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..cddae9e27
--- /dev/null
+++ b/compat-0.3/client/adapter-rest/src/main/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransportProvider.java
@@ -0,0 +1,35 @@
+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());
+ if (!(config instanceof RestTransportConfig nativeConfig)) {
+ throw new A2AClientException("Expected RestTransportConfig for the REST 0.3 adapter");
+ }
+ 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..c944916d8
--- /dev/null
+++ b/compat-0.3/client/adapter-rest/src/test/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03ClientTransportTest.java
@@ -0,0 +1,71 @@
+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.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.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"));
+ }
+
+ @Test
+ void providerRejectsWrongConfigurationType() {
+ AgentCard card = card();
+ A2AClientException exception = assertThrows(A2AClientException.class,
+ () -> new RestCompat03ClientTransportProvider().create(
+ new ClientTransportConfig() { }, card, card.supportedInterfaces().get(0)));
+
+ assertTrue(exception.getMessage().contains("RestTransportConfig"));
+ }
+
+ @Test
+ void providerRejectsNullConfiguration() {
+ AgentCard card = card();
+ A2AClientException exception = assertThrows(A2AClientException.class,
+ () -> new RestCompat03ClientTransportProvider().create(null, card, card.supportedInterfaces().get(0)));
+
+ assertTrue(exception.getMessage().contains("RestTransportConfig"));
+ }
+
+ private static AgentCard card() {
+ 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", null, "0.3")))
+ .build();
+ }
+
+ 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-rest/src/test/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03UnsupportedFieldsTest.java b/compat-0.3/client/adapter-rest/src/test/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03UnsupportedFieldsTest.java
new file mode 100644
index 000000000..2e0493829
--- /dev/null
+++ b/compat-0.3/client/adapter-rest/src/test/java/org/a2aproject/sdk/compat03/client/adapter/rest/RestCompat03UnsupportedFieldsTest.java
@@ -0,0 +1,96 @@
+package org.a2aproject.sdk.compat03.client.adapter.rest;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+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.function.Consumer;
+
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+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.Compat03ClientTransportBase;
+import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.spec.EventKind_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.Task_v0_3;
+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.CancelTaskParams;
+import org.a2aproject.sdk.spec.Message;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.a2aproject.sdk.spec.TextPart;
+import org.a2aproject.sdk.spec.UnsupportedOperationError;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+
+class RestCompat03UnsupportedFieldsTest {
+ @Test
+ void rejectsOriginalFieldsAndInterceptorMutationsBeforeDelegation() {
+ AgentCard card = AgentCard.builder().name("legacy").description("legacy").version("1")
+ .url("https://example.test").capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text")).skills(List.of())
+ .supportedInterfaces(List.of(new AgentInterface("HTTP+JSON", "https://example.test", null, "0.3"))).build();
+ var delegate = new RestTransport_v0_3(null, Compat03ClientTransportBase.legacyCard(card), "https://example.test", List.of()) {
+ @Override
+ public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) {
+ throw new AssertionError("Unsupported request reached the delegate");
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams_v0_3 request, Consumer events,
+ Consumer errors, @Nullable ClientCallContext_v0_3 context) {
+ throw new AssertionError("Unsupported streaming request reached the delegate");
+ }
+
+ @Override
+ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) {
+ throw new AssertionError("Unsupported cancellation reached the delegate");
+ }
+ };
+ for (boolean intercepted : List.of(false, true)) {
+ var transport = new RestCompat03ClientTransport(delegate, card,
+ intercepted ? List.of(unsupportedFieldsInterceptor()) : List.of());
+ var request = new MessageSendParams(new Message(Message.Role.ROLE_USER, List.of(new TextPart("hello")),
+ "request", null, null, intercepted ? null : List.of("referenced-task"), null, null), null, null);
+ assertUnsupported("referenceTaskIds", () -> transport.sendMessage(request, null));
+ assertUnsupported("referenceTaskIds", () -> transport.sendMessageStreaming(request,
+ event -> { }, error -> { throw new AssertionError(error); }, null));
+ var cancel = new CancelTaskParams("task", null, intercepted ? Map.of() : Map.of("reason", "stop"));
+ assertUnsupported("metadata", () -> transport.cancelTask(cancel, null));
+ }
+ }
+
+ private static void assertUnsupported(String field, Runnable call) {
+ A2AClientException error = assertThrows(A2AClientException.class, call::run);
+ assertInstanceOf(UnsupportedOperationError.class, error.getCause());
+ assertTrue(error.getMessage().contains(field));
+ }
+
+ private static ClientCallInterceptor unsupportedFieldsInterceptor() {
+ return new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, @Nullable Object payload, Map headers,
+ @Nullable AgentCard card, @Nullable ClientCallContext context) {
+ if (payload instanceof org.a2aproject.sdk.grpc.SendMessageRequest.Builder) {
+ var request = (org.a2aproject.sdk.grpc.SendMessageRequest.Builder) payload;
+ request.getMessageBuilder().addReferenceTaskIds("referenced-task");
+ return new PayloadAndHeaders(request, headers);
+ }
+ Struct metadata = Struct.newBuilder().putFields("reason", Value.newBuilder().setStringValue("stop").build()).build();
+ var request = (org.a2aproject.sdk.grpc.CancelTaskRequest.Builder) payload;
+ request.setMetadata(metadata);
+ return new PayloadAndHeaders(request, headers);
+ }
+ };
+ }
+}
diff --git a/compat-0.3/client/adapter/pom.xml b/compat-0.3/client/adapter/pom.xml
new file mode 100644
index 000000000..dc0360677
--- /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.4.1.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..13971dccb
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03AgentCardCompatibilityParser.java
@@ -0,0 +1,43 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.util.Optional;
+import java.util.Set;
+
+import org.a2aproject.sdk.client.http.A2ACardResolver;
+import org.a2aproject.sdk.client.http.AgentCardCompatibilityParser;
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.AgentCardMapper_v0_3;
+import org.a2aproject.sdk.compat03.json.JsonProcessingException_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) {
+ final AgentCard_v0_3 legacyCard;
+ try {
+ legacyCard = JsonUtil_v0_3.fromJson(rawCardJson, AgentCard_v0_3.class);
+ } catch (JsonProcessingException_v0_3 e) {
+ throw new A2AClientJSONError("Could not convert A2A 0.3 agent card to the unified client model", e);
+ }
+ final String version;
+ try {
+ version = A2ACardResolver.normalizeSupportedProtocolVersion(legacyCard.protocolVersion());
+ } catch (IllegalArgumentException e) {
+ return Optional.empty();
+ }
+ if (!"0.3".equals(version)) {
+ return Optional.empty();
+ }
+ return Optional.of(AgentCardMapper_v0_3.INSTANCE.toV10(legacyCard));
+ }
+}
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..c3eb74691
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientCallContextMapper.java
@@ -0,0 +1,37 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.common.A2AHeaders;
+import org.a2aproject.sdk.compat03.common.A2AHeaders_v0_3;
+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 @Nullable ClientCallContext_v0_3 toV03(@Nullable ClientCallContext context) {
+ if (context == null) {
+ return null;
+ }
+ Map headers = new HashMap<>();
+ context.getHeaders().forEach((name, value) -> {
+ if (!A2AHeaders.A2A_EXTENSIONS.equalsIgnoreCase(name)) {
+ headers.put(A2AHeaders_v0_3.X_A2A_EXTENSIONS.equalsIgnoreCase(name)
+ ? A2AHeaders_v0_3.X_A2A_EXTENSIONS : name, value);
+ }
+ });
+ // The native header, including interceptor replacements, takes precedence over a legacy alias.
+ context.getHeaders().forEach((name, value) -> {
+ if (A2AHeaders.A2A_EXTENSIONS.equalsIgnoreCase(name)) {
+ headers.put(A2AHeaders_v0_3.X_A2A_EXTENSIONS, value);
+ }
+ });
+ return new ClientCallContext_v0_3(Map.copyOf(context.getState()), Map.copyOf(headers));
+ }
+}
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..ba30a8c55
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientErrorMapper.java
@@ -0,0 +1,89 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientHTTPError_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.A2AClientHTTPError;
+import org.a2aproject.sdk.spec.A2AError;
+import org.a2aproject.sdk.spec.ContentTypeNotSupportedError;
+import org.a2aproject.sdk.spec.ExtendedAgentCardNotConfiguredError;
+import org.a2aproject.sdk.spec.InternalError;
+import org.a2aproject.sdk.spec.InvalidAgentResponseError;
+import org.a2aproject.sdk.spec.InvalidParamsError;
+import org.a2aproject.sdk.spec.InvalidRequestError;
+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 A2AClientHTTPError_v0_3 error) {
+ return new A2AClientException(message, new A2AClientHTTPError(error.getCode(), error.getMessage(),
+ error.getResponseBody(), error.getResponseHeaders()));
+ }
+ if (cause instanceof IOException && cause.getCause() instanceof A2AClientHTTPError error) {
+ return new A2AClientException(message, error);
+ }
+ 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 == null) {
+ return Map.of();
+ }
+ if (!(data instanceof Map, ?> rawDetails)) {
+ return Map.of("data", data);
+ }
+ 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..0c06cfa06
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportBase.java
@@ -0,0 +1,148 @@
+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.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) {
+ return Compat03ClientTransportSupport.unsupportedOperation("listTasks");
+ }
+
+ @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) {
+ return Compat03ClientTransportSupport.unsupportedOperation("getExtendedAgentCard");
+ }
+
+ @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..18e3b7b1d
--- /dev/null
+++ b/compat-0.3/client/adapter/src/main/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportSupport.java
@@ -0,0 +1,268 @@
+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.JSONRPCError_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.A2AClientException;
+import org.a2aproject.sdk.spec.CancelTaskParams;
+import org.a2aproject.sdk.spec.DataPart;
+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.TaskIdParams;
+import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.spec.TaskQueryParams;
+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 T unsupportedOperation(String operation) {
+ throw unsupported(operation);
+ }
+
+ 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());
+ for (var part : request.message().parts()) {
+ if (part instanceof DataPart data && !(data.data() instanceof Map, ?>)) {
+ throw unsupported("sendMessage with non-object DataPart data");
+ }
+ }
+ 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");
+ }
+ }
+
+ /** REST uses the same 0.3 protobuf schema as gRPC. */
+ public static void validateProtobufMessageSend(MessageSendParams request) {
+ validateMessageSend(request);
+ if (request.message().referenceTaskIds() != null && !request.message().referenceTaskIds().isEmpty()) {
+ throw unsupported("sendMessage with referenceTaskIds over REST or gRPC");
+ }
+ }
+
+ public static void validateProtobufCancel(CancelTaskParams request) {
+ validateCancel(request);
+ if (request.metadata() != null && !request.metadata().isEmpty()) {
+ throw unsupported("cancelTask with metadata over REST or gRPC");
+ }
+ }
+
+ 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 @Nullable 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 MessageSendParams_v0_3 toV03Protobuf(MessageSendParams request) {
+ validateProtobufMessageSend(request);
+ return MessageSendParamsMapper_v0_3.INSTANCE.fromV10(request);
+ }
+
+ public static TaskIdParams_v0_3 toV03Protobuf(CancelTaskParams request) {
+ validateProtobufCancel(request);
+ return CancelTaskParamsMapper_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);
+ @Nullable String id = request.id();
+ // An omitted optional protobuf string is read back as empty after interceptors.
+ return new GetTaskPushNotificationConfigParams_v0_3(request.taskId(),
+ id == null || id.isEmpty() ? null : 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) {
+ return ListTaskPushNotificationConfigsResultMapper_v0_3.INSTANCE.toV10(configs);
+ }
+
+ 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..5f60e8f0c
--- /dev/null
+++ b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03AgentCardCompatibilityParserTest.java
@@ -0,0 +1,156 @@
+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.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.sun.net.httpserver.HttpServer;
+import org.a2aproject.sdk.client.http.A2ACardResolver;
+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.AgentInterface_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentSkill_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 resolverUsesProductionParserWithARealLegacyCardAndOneFetch() throws Exception {
+ AtomicInteger fetches = new AtomicInteger();
+ AgentCard card = resolveCard(JsonUtil_v0_3.toJson(primaryOnlyCard("rest")), fetches);
+ assertEquals("legacy", card.name());
+ assertEquals("HTTP+JSON", card.supportedInterfaces().get(0).protocolBinding());
+ assertEquals("0.3", card.supportedInterfaces().get(0).protocolVersion());
+ assertEquals(1, fetches.get());
+ }
+
+ @Test
+ void resolverRejectsMalformedShapeAndUnrequestedLegacyVersion() throws Exception {
+ AtomicInteger fetches = new AtomicInteger();
+ assertThrows(A2AClientJSONError.class, () -> resolveCard("{\"legacy\":true}", fetches));
+ assertEquals(1, fetches.get());
+ fetches.set(0);
+ String unsupported = JsonUtil_v0_3.toJson(primaryOnlyCard("rest"))
+ .replace("\"0.3.0\"", "\"0.2.0\"");
+ assertThrows(A2AClientJSONError.class, () -> resolveCard(unsupported, fetches));
+ assertEquals(1, fetches.get());
+ }
+
+ private static AgentCard resolveCard(String rawCard, AtomicInteger fetches) throws Exception {
+ HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+ server.createContext("/.well-known/agent-card.json", exchange -> {
+ fetches.incrementAndGet();
+ byte[] body = rawCard.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, body.length);
+ try (var output = exchange.getResponseBody()) {
+ output.write(body);
+ } finally {
+ exchange.close();
+ }
+ });
+ server.start();
+ try {
+ return A2ACardResolver.builder().baseUrl("http://localhost:" + server.getAddress().getPort())
+ .supportedProtocolVersions(Set.of("1.0", "0.3")).build().getAgentCard();
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @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/Compat03ClientCallContextMapperTest.java b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientCallContextMapperTest.java
new file mode 100644
index 000000000..f942c5281
--- /dev/null
+++ b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientCallContextMapperTest.java
@@ -0,0 +1,48 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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.spec.AgentCard;
+import org.junit.jupiter.api.Test;
+
+class Compat03ClientCallContextMapperTest {
+ @Test
+ void translatesExtensionHeadersWithoutChangingTheNativeContext() {
+ for (String name : List.of("A2A-Extensions", "a2a-extensions", "A2A-EXTENSIONS")) {
+ ClientCallContext context = new ClientCallContext(Map.of("request-id", "request-1"),
+ Map.of(name, "urn:example:extension", "Authorization", "Bearer token"));
+
+ var legacy = Compat03ClientCallContextMapper.toV03(context);
+
+ assertEquals(Map.of("X-A2A-Extensions", "urn:example:extension",
+ "Authorization", "Bearer token"), legacy.getHeaders());
+ assertEquals(context.getState(), legacy.getState());
+ assertEquals("urn:example:extension", context.getHeaders().get(name));
+ }
+ }
+
+ @Test
+ void translatesExtensionsAfterNativeInterceptorsAndPrefersTheNativeHeader() {
+ ClientCallInterceptor interceptor = new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String method, Object payload, Map headers,
+ AgentCard card, ClientCallContext context) {
+ assertEquals("urn:original", headers.get("A2A-Extensions"));
+ return new PayloadAndHeaders(payload,
+ Map.of("a2a-extensions", "urn:replacement", "x-a2a-extensions", "urn:stale"));
+ }
+ };
+ var result = Compat03InterceptorSupport.apply(List.of(interceptor), "message/send", "payload", null,
+ new ClientCallContext(Map.of(), Map.of("A2A-Extensions", "urn:original")), String.class);
+
+ var legacy = Compat03ClientCallContextMapper.toV03(new ClientCallContext(Map.of(), result.getHeaders()));
+
+ assertEquals(Map.of("X-A2A-Extensions", "urn:replacement"), legacy.getHeaders());
+ }
+}
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..aec925158
--- /dev/null
+++ b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientErrorMapperTest.java
@@ -0,0 +1,112 @@
+package org.a2aproject.sdk.compat03.client.adapter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientHTTPError_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.A2AClientHTTPError;
+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 exposesHttpErrorsWrappedByTheJdkClient() {
+ var httpError = new A2AClientHTTPError(401, "authentication failed", "unauthorized",
+ Map.of("WWW-Authenticate", List.of("Bearer")));
+ var legacy = new A2AClientException_v0_3("getTask failed", new IOException("authentication failed", httpError));
+ var exception = Compat03ClientErrorMapper.toV10(legacy);
+ var cause = assertInstanceOf(A2AClientHTTPError.class, exception.getCause());
+ assertEquals(401, cause.getCode());
+ assertEquals("unauthorized", cause.getResponseBody());
+ assertEquals(List.of("Bearer"), cause.getResponseHeaders().get("www-authenticate"));
+ }
+
+ @Test
+ void preservesHttpFailureDetailsForBlockingAndStreamingErrors() {
+ var legacy = new A2AClientException_v0_3("HTTP 429", new A2AClientHTTPError_v0_3(
+ 429, "HTTP 429", "rate limited", Map.of("Retry-After", List.of("30"))));
+ AtomicReference streaming = new AtomicReference<>();
+ Compat03ClientTransportSupport.mapAsyncError(streaming::set).accept(legacy);
+ for (A2AClientException exception : List.of(Compat03ClientErrorMapper.toV10(legacy),
+ assertInstanceOf(A2AClientException.class, streaming.get()))) {
+ var cause = assertInstanceOf(A2AClientHTTPError.class, exception.getCause());
+ assertEquals(429, cause.getCode());
+ assertEquals("rate limited", cause.getResponseBody());
+ assertEquals(List.of("30"), cause.getResponseHeaders().get("retry-after"));
+ }
+ }
+
+ @Test
+ void preservesScalarAndArrayDataForSynchronousAndStreamingErrors() {
+ for (Object data : List.of("missingScope", 42, true, List.of("missingScope", "write"))) {
+ JSONRPCError_v0_3 legacy = new JSONRPCError_v0_3(-32602, "invalid parameters", data);
+
+ A2AClientException synchronous = Compat03ClientErrorMapper.toV10(
+ new A2AClientException_v0_3("request failed", legacy));
+ assertEquals(Map.of("data", data),
+ assertInstanceOf(A2AError.class, synchronous.getCause()).getDetails());
+
+ AtomicReference received = new AtomicReference<>();
+ Compat03ClientTransportSupport.mapAsyncError(received::set).accept(legacy);
+ A2AClientException streaming = assertInstanceOf(A2AClientException.class, received.get());
+ assertEquals(Map.of("data", data),
+ assertInstanceOf(A2AError.class, streaming.getCause()).getDetails());
+ }
+ }
+
+ @Test
+ void absentErrorDataRemainsEmpty() {
+ assertEquals(Map.of(), Compat03ClientErrorMapper.toV10(
+ new JSONRPCError_v0_3(-32602, "invalid parameters", null)).getDetails());
+ }
+
+ @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..f25208ca9
--- /dev/null
+++ b/compat-0.3/client/adapter/src/test/java/org/a2aproject/sdk/compat03/client/adapter/Compat03ClientTransportSupportTest.java
@@ -0,0 +1,264 @@
+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.List;
+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.compat03.spec.EventKind_v0_3;
+import org.a2aproject.sdk.spec.A2AClientException;
+import org.a2aproject.sdk.spec.CancelTaskParams;
+import org.a2aproject.sdk.spec.DataPart;
+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.a2aproject.sdk.spec.UnsupportedOperationError;
+import org.junit.jupiter.api.Test;
+
+class Compat03ClientTransportSupportTest {
+
+ @Test
+ void normalizesDefaultPushConfigurationIdAfterProtobufConversion() {
+ var original = new org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams("task");
+ var proto = org.a2aproject.sdk.grpc.utils.ProtoUtils.ToProto.getTaskPushNotificationConfigRequest(original);
+ assertNull(Compat03ClientTransportSupport.toV03(
+ org.a2aproject.sdk.grpc.utils.ProtoUtils.FromProto.getTaskPushNotificationConfigParams(proto))
+ .pushNotificationConfigId());
+ assertEquals("specific", Compat03ClientTransportSupport.toV03(
+ org.a2aproject.sdk.grpc.utils.ProtoUtils.FromProto.getTaskPushNotificationConfigParams(
+ proto.toBuilder().setId("specific").build())).pushNotificationConfigId());
+ assertNull(Compat03ClientTransportSupport.toV03(
+ org.a2aproject.sdk.grpc.utils.ProtoUtils.FromProto.getTaskPushNotificationConfigParams(
+ proto.toBuilder().setId("specific").clearId().build())).pushNotificationConfigId());
+ }
+
+ @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 rejectsNonObjectDataPartsThroughClientExceptionBoundary() {
+ for (Object data : List.of("value", 42, true, List.of("item"))) {
+ MessageSendParams request = new MessageSendParams(
+ new Message(Message.Role.ROLE_USER, List.of(new DataPart(data)), "message", null, null,
+ null, null, null), null, null);
+
+ A2AClientException exception = assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.toV03(request));
+
+ assertInstanceOf(UnsupportedOperationError.class, exception.getCause());
+ }
+ }
+
+ @Test
+ void preservesObjectDataParts() {
+ MessageSendParams request = new MessageSendParams(
+ new Message(Message.Role.ROLE_USER, List.of(new DataPart(Map.of("key", "value"))),
+ "message", null, null, null, null, null), null, null);
+
+ var legacy = Compat03ClientTransportSupport.toV03(request);
+
+ var data = assertInstanceOf(org.a2aproject.sdk.compat03.spec.DataPart_v0_3.class,
+ legacy.message().parts().get(0));
+ assertEquals(Map.of("key", "value"), data.data());
+ }
+
+ @Test
+ void preservesNullValuedDataAndMetadataAcrossVersionConversion() {
+ Map values = new java.util.LinkedHashMap<>();
+ values.put("nil", null);
+ MessageSendParams request = new MessageSendParams(
+ new Message(Message.Role.ROLE_USER, List.of(new DataPart(values, values)), "message",
+ null, null, null, values, null), null, values);
+ var legacy = Compat03ClientTransportSupport.toV03(request);
+ var data = assertInstanceOf(org.a2aproject.sdk.compat03.spec.DataPart_v0_3.class,
+ legacy.message().parts().get(0));
+ assertEquals(values, data.data());
+ assertEquals(values, data.metadata());
+ assertEquals(values, legacy.message().metadata());
+ assertEquals(values, legacy.metadata());
+ assertEquals(request.message(), Compat03ClientTransportSupport.toV10((EventKind_v0_3) legacy.message()));
+ }
+
+ @Test
+ void rejectsFieldsMissingFromLegacyProtobufButPreservesThemForJsonRpc() {
+ MessageSendParams request = new MessageSendParams(
+ new Message(Message.Role.ROLE_USER, List.of(new TextPart("hello")), "message",
+ null, null, List.of("referenced-task"), null, null), null, null);
+ assertEquals(List.of("referenced-task"), Compat03ClientTransportSupport.toV03(request).message().referenceTaskIds());
+ A2AClientException send = assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.toV03Protobuf(request));
+ assertInstanceOf(UnsupportedOperationError.class, send.getCause());
+ assertTrue(send.getMessage().contains("referenceTaskIds"));
+
+ CancelTaskParams cancel = new CancelTaskParams("task", null, Map.of("reason", "stop"));
+ assertEquals(cancel.metadata(), Compat03ClientTransportSupport.toV03(cancel).metadata());
+ A2AClientException cancellation = assertThrows(A2AClientException.class,
+ () -> Compat03ClientTransportSupport.toV03Protobuf(cancel));
+ assertInstanceOf(UnsupportedOperationError.class, cancellation.getCause());
+ assertTrue(cancellation.getMessage().contains("metadata"));
+ Compat03ClientTransportSupport.toV03Protobuf(new CancelTaskParams("task", null, Map.of()));
+ }
+
+ @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 mapsNullContextsToNull() {
+ assertNull(Compat03ClientTransportSupport.toV03Context(null));
+ assertNull(Compat03ClientCallContextMapper.toV03(null));
+ }
+
+ @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> interceptedHeaders = new AtomicReference<>();
+ AtomicReference interceptedContext = new AtomicReference<>();
+ Map state = Map.of("request-id", "request-1");
+ ClientCallInterceptor interceptor = new ClientCallInterceptor() {
+ @Override
+ public PayloadAndHeaders intercept(String methodName, Object payload, Map headers,
+ org.a2aproject.sdk.spec.AgentCard agentCard, ClientCallContext clientCallContext) {
+ interceptedHeaders.set(headers);
+ interceptedContext.set(clientCallContext);
+ return new PayloadAndHeaders(payload, headers);
+ }
+ };
+
+ Compat03InterceptorSupport.apply(
+ java.util.List.of(interceptor), "message/send", "payload", null,
+ new ClientCallContext(state, Map.of("a2a-version", "1.0", "Authorization", "Bearer token")),
+ String.class);
+
+ assertFalse(interceptedHeaders.get().keySet().stream()
+ .anyMatch(name -> name.equalsIgnoreCase("A2A-Version")));
+ assertEquals("Bearer token", interceptedHeaders.get().get("Authorization"));
+ assertSame(state, interceptedContext.get().getState());
+ assertFalse(interceptedContext.get().getHeaders().keySet().stream()
+ .anyMatch(name -> name.equalsIgnoreCase("A2A-Version")));
+ assertEquals("Bearer token", interceptedContext.get().getHeaders().get("Authorization"));
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java
index 50f73e641..c5509d1f1 100644
--- a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java
@@ -3,6 +3,7 @@
import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -319,7 +320,8 @@ private Metadata createGrpcMetadata(@Nullable ClientCallContext_v0_3 context, @N
metadata.put(AUTHORIZATION_METADATA_KEY, headerValue);
} else {
// Create a metadata key dynamically for API keys and other custom headers
- Metadata.Key metadataKey = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
+ Metadata.Key metadataKey = Metadata.Key.of(
+ headerName.toLowerCase(Locale.ROOT), Metadata.ASCII_STRING_MARSHALLER);
metadata.put(metadataKey, headerValue);
}
}
@@ -360,17 +362,10 @@ private String getTaskPushNotificationConfigName(GetTaskPushNotificationConfigPa
}
private String getTaskPushNotificationConfigName(String taskId, @Nullable String pushNotificationConfigId) {
- StringBuilder name = new StringBuilder();
- name.append("tasks/");
- name.append(taskId);
- if (pushNotificationConfigId != null) {
- name.append("/pushNotificationConfigs/");
- name.append(pushNotificationConfigId);
+ if (pushNotificationConfigId == null) {
+ return "tasks/" + taskId;
}
- //name.append("/pushNotificationConfigs/");
- // Use taskId as default config ID if none provided
- //name.append(pushNotificationConfigId != null ? pushNotificationConfigId : taskId);
- return name.toString();
+ return "tasks/" + taskId + "/pushNotificationConfigs/" + pushNotificationConfigId;
}
private PayloadAndHeaders_v0_3 applyInterceptors(String methodName, Object payload,
@@ -386,4 +381,4 @@ private PayloadAndHeaders_v0_3 applyInterceptors(String methodName, Object paylo
return payloadAndHeaders;
}
-}
\ No newline at end of file
+}
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java
index 991769a2d..a9372a52b 100644
--- a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java
@@ -7,7 +7,9 @@
import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3;
import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3;
+import org.jspecify.annotations.Nullable;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -26,23 +28,25 @@ public SSEEventListener_v0_3(Consumer eventHandler,
this.errorHandler = errorHandler;
}
- public void onMessage(String message, Future completableFuture) {
+ public void onMessage(String message, @Nullable Future completableFuture) {
try {
handleMessage(JsonParser.parseString(message).getAsJsonObject(), completableFuture);
} catch (JsonSyntaxException e) {
- LOGGER.warning("Failed to parse JSON message: " + message);
+ fail(e, completableFuture);
} catch (JsonProcessingException_v0_3 e) {
- LOGGER.warning("Failed to process JSON message: " + message);
+ fail(e, completableFuture);
} catch (IllegalArgumentException e) {
- LOGGER.warning("Invalid message format: " + message);
- signalTerminal(e);
- completableFuture.cancel(true); // close SSE channel
+ fail(e, completableFuture);
+ } catch (IllegalStateException e) {
+ fail(e, completableFuture);
}
}
- public void onError(Throwable throwable, Future future) {
+ public void onError(Throwable throwable, @Nullable Future future) {
signalTerminal(throwable);
- future.cancel(true); // close SSE channel
+ if (future != null) {
+ future.cancel(true); // close SSE channel
+ }
}
private void signalTerminal(Throwable error) {
@@ -62,22 +66,35 @@ public void onComplete() {
signalTerminal(null);
}
- private void handleMessage(JsonObject jsonObject, Future future) throws JsonProcessingException_v0_3 {
+ private void handleMessage(JsonObject jsonObject, @Nullable Future future) throws JsonProcessingException_v0_3 {
if (jsonObject.has("error")) {
JSONRPCError_v0_3 error = JsonUtil_v0_3.fromJson(jsonObject.get("error").toString(), JSONRPCError_v0_3.class);
signalTerminal(error);
+ if (future != null) {
+ future.cancel(true); // close SSE channel
+ }
} else if (jsonObject.has("result")) {
// result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent
String resultJson = jsonObject.get("result").toString();
StreamingEventKind_v0_3 event = JsonUtil_v0_3.fromJson(resultJson, StreamingEventKind_v0_3.class);
eventHandler.accept(event);
- if (event instanceof TaskStatusUpdateEvent_v0_3 tsue && tsue.isFinal()) {
+ if ((event instanceof TaskStatusUpdateEvent_v0_3 tsue && tsue.isFinal())
+ || (event instanceof Task_v0_3 task && task.status().state().isFinal())) {
signalTerminal(null);
- future.cancel(true); // close SSE channel
+ if (future != null) {
+ future.cancel(true); // close SSE channel
+ }
}
} else {
throw new IllegalArgumentException("Unknown message type");
}
}
+ private void fail(Throwable throwable, @Nullable Future future) {
+ signalTerminal(throwable);
+ if (future != null) {
+ future.cancel(true);
+ }
+ }
+
}
diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3_Test.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3_Test.java
index 0f22828a5..5ea6b078a 100644
--- a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3_Test.java
+++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3_Test.java
@@ -60,6 +60,21 @@ public void testOnEventWithTaskResult() throws Exception {
assertEquals(TaskState_v0_3.WORKING, task.status().state());
}
+ @Test
+ public void testFinalTaskResultCancelsStream() {
+ AtomicReference receivedEvent = new AtomicReference<>();
+ SSEEventListener_v0_3 listener = new SSEEventListener_v0_3(receivedEvent::set, error -> {});
+ CancelCapturingFuture future = new CancelCapturingFuture();
+
+ String eventData = JsonStreamingMessages_v0_3.STREAMING_TASK_EVENT
+ .replace("\"working\"", "\"completed\"")
+ .substring(JsonStreamingMessages_v0_3.STREAMING_TASK_EVENT.indexOf("{"));
+ listener.onMessage(eventData, future);
+
+ assertInstanceOf(Task_v0_3.class, receivedEvent.get());
+ assertTrue(future.cancelHandlerCalled);
+ }
+
@Test
public void testOnEventWithMessageResult() throws Exception {
// Set up event handler
@@ -160,7 +175,8 @@ public void testOnEventWithError() throws Exception {
JsonStreamingMessages_v0_3.STREAMING_ERROR_EVENT.indexOf("{"));
// Call onEvent method
- listener.onMessage(eventData, null);
+ CancelCapturingFuture future = new CancelCapturingFuture();
+ listener.onMessage(eventData, future);
// Verify the error was processed correctly
assertNotNull(receivedError.get());
@@ -169,6 +185,33 @@ public void testOnEventWithError() throws Exception {
assertEquals(-32602, jsonrpcError.getCode());
assertEquals("Invalid parameters", jsonrpcError.getMessage());
assertEquals("Missing required field", jsonrpcError.getData());
+ assertTrue(future.cancelHandlerCalled);
+ }
+
+ @Test
+ public void testMalformedEventReportsAndCancels() {
+ AtomicReference receivedError = new AtomicReference<>();
+ SSEEventListener_v0_3 listener = new SSEEventListener_v0_3(
+ event -> {}, receivedError::set);
+ CancelCapturingFuture future = new CancelCapturingFuture();
+
+ listener.onMessage("{not-json", future);
+
+ assertNotNull(receivedError.get());
+ assertTrue(future.cancelHandlerCalled);
+ }
+
+ @Test
+ public void testNonObjectJsonEventReportsAndCancels() {
+ AtomicReference receivedError = new AtomicReference<>();
+ SSEEventListener_v0_3 listener = new SSEEventListener_v0_3(
+ event -> {}, receivedError::set);
+ CancelCapturingFuture future = new CancelCapturingFuture();
+
+ listener.onMessage("[]", future);
+
+ assertNotNull(receivedError.get());
+ assertTrue(future.cancelHandlerCalled);
}
@Test
@@ -189,6 +232,14 @@ public void testOnFailure() {
assertTrue(future.cancelHandlerCalled);
}
+ @Test
+ public void testOnFailureWithNullFutureDoesNotThrow() {
+ SSEEventListener_v0_3 listener = new SSEEventListener_v0_3(
+ event -> {}, error -> {});
+
+ listener.onError(new RuntimeException("Test exception"), null);
+ }
+
@Test
public void testFinalTaskStatusUpdateEventCancels() {
TaskStatusUpdateEvent_v0_3 tsue = new TaskStatusUpdateEvent_v0_3.Builder()
@@ -237,6 +288,17 @@ public void testOnEventWithFinalTaskStatusUpdateEventEventCancels() throws Excep
assertTrue(future.cancelHandlerCalled);
}
+ @Test
+ public void testFinalTaskStatusUpdateWithNullFutureDoesNotThrow() {
+ SSEEventListener_v0_3 listener = new SSEEventListener_v0_3(
+ event -> {}, error -> {});
+
+ String eventData = JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring(
+ JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{"));
+
+ listener.onMessage(eventData, null);
+ }
+
@Test
diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3.java
index d9b6d1368..b66387831 100644
--- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3.java
+++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3.java
@@ -1,11 +1,17 @@
package org.a2aproject.sdk.compat03.client.transport.rest;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
+import org.a2aproject.sdk.client.http.A2AHttpResponse;
import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3;
import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
-import org.a2aproject.sdk.client.http.A2AHttpResponse;
import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientHTTPError_v0_3;
import org.a2aproject.sdk.compat03.spec.AuthenticatedExtendedCardNotConfiguredError_v0_3;
import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3;
import org.a2aproject.sdk.compat03.spec.InternalError_v0_3;
@@ -18,31 +24,44 @@
import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3;
import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3;
import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3;
-import java.util.logging.Level;
-import java.util.logging.Logger;
/**
* Utility class to A2AHttpResponse to appropriate A2A error types
*/
public class RestErrorMapper_v0_3 {
+ private static final Logger LOGGER = Logger.getLogger(RestErrorMapper_v0_3.class.getName());
public static A2AClientException_v0_3 mapRestError(A2AHttpResponse response) {
- return RestErrorMapper_v0_3.mapRestError(response.body(), response.status());
+ return mapRestError(response.body(), response.status(), response.headers().toMap());
}
public static A2AClientException_v0_3 mapRestError(String body, int code) {
+ return mapRestError(body, code, Map.of());
+ }
+
+ private static A2AClientException_v0_3 mapRestError(String body, int code, Map> headers) {
try {
if (body != null && !body.isBlank()) {
- JsonObject node = JsonUtil_v0_3.fromJson(body, JsonObject.class);
- String className = safeGetString(node, "error");
- String errorMessage = safeGetString(node, "message");
- return mapRestError(className, errorMessage, code);
+ JsonElement node = JsonUtil_v0_3.fromJson(body, JsonElement.class);
+ if (node != null && node.isJsonObject()) {
+ String className = safeGetString(node.getAsJsonObject(), "error");
+ String errorMessage = safeGetString(node.getAsJsonObject(), "message");
+ A2AClientException_v0_3 mapped = mapRestError(className, errorMessage, code);
+ if (!(mapped.getCause() instanceof A2AClientHTTPError_v0_3)) {
+ return mapped;
+ }
+ }
}
- return mapRestError("", "", code);
- } catch (JsonProcessingException_v0_3 ex) {
- Logger.getLogger(RestErrorMapper_v0_3.class.getName()).log(Level.SEVERE, null, ex);
- return new A2AClientException_v0_3("Failed to parse error response: " + ex.getMessage());
+ } catch (JsonProcessingException_v0_3 e) {
+ // A non-JSON error body is still an HTTP failure with useful response details.
+ LOGGER.log(Level.SEVERE, "Failed to parse REST error response body as JSON", e);
}
+ return httpError(body, code, headers);
+ }
+
+ private static A2AClientException_v0_3 httpError(String body, int code, Map> headers) {
+ String message = "HTTP " + code + (body == null || body.isBlank() ? "" : ": " + body);
+ return new A2AClientException_v0_3(message, new A2AClientHTTPError_v0_3(code, message, body, headers));
}
private static String safeGetString(JsonObject obj, String fieldName) {
@@ -69,7 +88,7 @@ public static A2AClientException_v0_3 mapRestError(String className, String erro
case "org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3" -> new A2AClientException_v0_3(errorMessage, new PushNotificationNotSupportedError_v0_3());
case "org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3" -> new A2AClientException_v0_3(errorMessage, new TaskNotCancelableError_v0_3());
case "org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3" -> new A2AClientException_v0_3(errorMessage, new UnsupportedOperationError_v0_3());
- default -> new A2AClientException_v0_3(errorMessage);
+ default -> httpError(errorMessage, code, Map.of());
};
}
}
diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3.java
index 7a8e6a1ec..a29e91c60 100644
--- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3.java
+++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3.java
@@ -2,14 +2,24 @@
import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
-import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageOrBuilder;
import com.google.protobuf.util.JsonFormat;
import org.a2aproject.sdk.client.http.A2AHttpClient;
import org.a2aproject.sdk.client.http.A2AHttpClientFactory;
import org.a2aproject.sdk.client.http.A2AHttpResponse;
-import org.a2aproject.sdk.client.http.ServerSentEvent;
import org.a2aproject.sdk.compat03.client.http.A2ACardResolver_v0_3;
import org.a2aproject.sdk.compat03.client.transport.rest.sse.RestSSEEventListener_v0_3;
import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3;
@@ -18,44 +28,37 @@
import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders_v0_3;
import org.a2aproject.sdk.compat03.grpc.CancelTaskRequest;
import org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequestOrBuilder;
import org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequest;
import org.a2aproject.sdk.compat03.grpc.GetTaskRequest;
import org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigRequest;
-import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3;
-import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3;
-import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest_v0_3;
-import org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3;
-import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3;
-import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3;
-import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.grpc.utils.ProtoJsonUtils_v0_3;
+import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3;
+import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3;
+import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3;
import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3;
import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3;
import org.a2aproject.sdk.compat03.spec.EventKind_v0_3;
import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardRequest_v0_3;
import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3;
import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3;
import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3;
import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
-import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3;
-import org.a2aproject.sdk.compat03.spec.Task_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.compat03.grpc.utils.ProtoJsonUtils_v0_3;
-import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3;
-import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3;
-import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3;
-import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3;
-import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
+import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
import org.a2aproject.sdk.grpc.utils.ProtoJsonUtils;
import org.jspecify.annotations.Nullable;
@@ -194,8 +197,14 @@ public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(Task
builder.setConfigId(request.pushNotificationConfig().id());
}
PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest_v0_3.METHOD, builder, agentCard, context);
+ CreateTaskPushNotificationConfigRequestOrBuilder payload =
+ checkNotNullParam("payload", (CreateTaskPushNotificationConfigRequestOrBuilder) payloadAndHeaders.getPayload());
+ PayloadAndHeaders_v0_3 configBody = new PayloadAndHeaders_v0_3(payload.getConfig(), payloadAndHeaders.getHeaders());
+ String configId = payload.getConfigId().isEmpty() ? request.taskId() : payload.getConfigId();
+ String url = agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs", request.taskId())
+ + "?configId=" + URLEncoder.encode(configId, StandardCharsets.UTF_8);
try {
- String httpResponseBody = sendPostRequest(agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs", request.taskId()), payloadAndHeaders);
+ String httpResponseBody = sendPostRequest(url, configBody);
org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.Builder responseBuilder = org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder();
JsonFormat.parser().merge(httpResponseBody, responseBuilder);
return ProtoUtils_v0_3.FromProto.taskPushNotificationConfig(responseBuilder);
@@ -313,11 +322,11 @@ public void resubscribe(TaskIdParams_v0_3 request, Consumer completableFuture)
LOGGER.fine("Streaming message received: " + message);
org.a2aproject.sdk.compat03.grpc.StreamResponse.Builder builder = org.a2aproject.sdk.compat03.grpc.StreamResponse.newBuilder();
JsonFormat.parser().merge(message, builder);
- handleMessage(builder.build());
+ handleMessage(builder.build(), completableFuture);
} catch (InvalidProtocolBufferException e) {
- errorHandler.accept(RestErrorMapper_v0_3.mapRestError(message, 500));
+ if (errorHandler != null) {
+ errorHandler.accept(RestErrorMapper_v0_3.mapRestError(message, 500));
+ }
+ cancel(completableFuture);
}
}
@@ -44,7 +49,7 @@ public void onError(Throwable throwable, @Nullable Future future) {
}
}
- private void handleMessage(StreamResponse response) {
+ private void handleMessage(StreamResponse response, @Nullable Future future) {
StreamingEventKind_v0_3 event;
switch (response.getPayloadCase()) {
case MSG ->
@@ -57,11 +62,24 @@ private void handleMessage(StreamResponse response) {
event = ProtoUtils_v0_3.FromProto.taskArtifactUpdateEvent(response.getArtifactUpdate());
default -> {
LOGGER.warning("Invalid stream response " + response.getPayloadCase());
- errorHandler.accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase()));
+ if (errorHandler != null) {
+ errorHandler.accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase()));
+ }
+ cancel(future);
return;
}
}
eventHandler.accept(event);
+ if ((event instanceof TaskStatusUpdateEvent_v0_3 statusUpdate && statusUpdate.isFinal())
+ || (event instanceof Task_v0_3 task && task.status().state().isFinal())) {
+ cancel(future);
+ }
+ }
+
+ private static void cancel(@Nullable Future future) {
+ if (future != null) {
+ future.cancel(true);
+ }
}
}
diff --git a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/JsonRestMessages_v0_3.java b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/JsonRestMessages_v0_3.java
index f061d1d58..635b686de 100644
--- a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/JsonRestMessages_v0_3.java
+++ b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/JsonRestMessages_v0_3.java
@@ -417,14 +417,11 @@ public class JsonRestMessages_v0_3 {
static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """
{
- "parent": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64",
- "config": {
- "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs",
- "pushNotificationConfig": {
- "url": "https://example.com/callback",
- "authentication": {
- "schemes": [ "jwt" ]
- }
+ "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs",
+ "pushNotificationConfig": {
+ "url": "https://example.com/callback",
+ "authentication": {
+ "schemes": [ "jwt" ]
}
}
}""";
diff --git a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3_Test.java b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3_Test.java
new file mode 100644
index 000000000..b473f9358
--- /dev/null
+++ b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3_Test.java
@@ -0,0 +1,60 @@
+package org.a2aproject.sdk.compat03.client.transport.rest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.client.http.A2AHttpHeaders;
+import org.a2aproject.sdk.client.http.A2AHttpResponse;
+import org.a2aproject.sdk.compat03.spec.A2AClientHTTPError_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3;
+import org.junit.jupiter.api.Test;
+
+class RestErrorMapper_v0_3_Test {
+ @Test
+ void preservesStatusRawBodyAndHeadersForUnrecognizedResponses() {
+ for (String body : List.of("", "rate limited", "null", "[]", "{\"message\":\"rate limited\"}",
+ "{\"error\":\"UNKNOWN\",\"message\":\"rate limited\"}")) {
+ var exception = RestErrorMapper_v0_3.mapRestError(response(429, body,
+ Map.of("Retry-After", List.of("30"), "X-Request-Id", List.of("request"))));
+ var cause = assertInstanceOf(A2AClientHTTPError_v0_3.class, exception.getCause());
+ assertEquals(429, cause.getCode());
+ assertEquals(body, cause.getResponseBody());
+ assertEquals(List.of("30"), cause.getResponseHeaders().get("retry-after"));
+ assertTrue(exception.getMessage().contains("429"));
+ }
+ var unauthorized = RestErrorMapper_v0_3.mapRestError(response(401, "",
+ Map.of("WWW-Authenticate", List.of("Bearer"))));
+ var cause = assertInstanceOf(A2AClientHTTPError_v0_3.class, unauthorized.getCause());
+ assertEquals(401, cause.getCode());
+ assertEquals(List.of("Bearer"), cause.getResponseHeaders().get("www-authenticate"));
+ }
+
+ @Test
+ void retainsRecognizedProtocolErrors() {
+ var exception = RestErrorMapper_v0_3.mapRestError(response(404,
+ "{\"error\":\"org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3\",\"message\":\"missing\"}", Map.of()));
+ assertInstanceOf(TaskNotFoundError_v0_3.class, exception.getCause());
+ assertEquals("missing", exception.getMessage());
+ }
+
+ @Test
+ void bodyAndClassNameOverloadsRetainHttpStatus() {
+ assertEquals(503, assertInstanceOf(A2AClientHTTPError_v0_3.class,
+ RestErrorMapper_v0_3.mapRestError("unavailable", 503).getCause()).getCode());
+ assertEquals(401, assertInstanceOf(A2AClientHTTPError_v0_3.class,
+ RestErrorMapper_v0_3.mapRestError("unknown", "unauthorized", 401).getCause()).getCode());
+ }
+
+ private static A2AHttpResponse response(int status, String body, Map> headers) {
+ return new A2AHttpResponse() {
+ @Override public int status() { return status; }
+ @Override public boolean success() { return false; }
+ @Override public String body() { return body; }
+ @Override public A2AHttpHeaders headers() { return A2AHttpHeaders.of(headers); }
+ };
+ }
+}
diff --git a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3_Test.java b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3_Test.java
index af841eac6..c39610b0f 100644
--- a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3_Test.java
+++ b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3_Test.java
@@ -6,10 +6,10 @@
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.GET_TASK_TEST_RESPONSE;
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_STREAMING_TEST_REQUEST;
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_STREAMING_TEST_RESPONSE;
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_TEST_REQUEST;
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_TEST_RESPONSE;
-import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_STREAMING_TEST_REQUEST;
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST;
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE;
@@ -17,11 +17,25 @@
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_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;
@@ -33,29 +47,21 @@
import org.a2aproject.sdk.compat03.spec.FileWithUri_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.Message_v0_3;
import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3;
import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
-import org.a2aproject.sdk.compat03.spec.Part_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
import org.a2aproject.sdk.compat03.spec.Part_v0_3.Kind;
+import org.a2aproject.sdk.compat03.spec.Part_v0_3;
import org.a2aproject.sdk.compat03.spec.PushNotificationAuthenticationInfo_v0_3;
import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3;
import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
-import org.a2aproject.sdk.compat03.spec.Task_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.compat03.spec.TaskState_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
import org.a2aproject.sdk.compat03.spec.TextPart_v0_3;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.logging.Logger;
+import org.a2aproject.sdk.spec.A2AClientHTTPError;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -227,7 +233,7 @@ public void testGetTask() throws Exception {
assertEquals(Kind.FILE, part.kind());
assertEquals("text/plain", part.file().mimeType());
assertEquals("greeting.txt", part.file().name());
- assertEquals("hello", ((FileWithBytes_v0_3) part.file()).bytes());
+ assertEquals("aGVsbG8=", ((FileWithBytes_v0_3) part.file()).bytes());
assertNull(history.metadata());
assertNull(history.referenceTaskIds());
}
@@ -293,7 +299,8 @@ public void testSetTaskPushNotificationConfiguration() throws Exception {
request()
.withMethod("POST")
.withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")
- .withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+ .withQueryStringParameter("configId", "de38c76d-d54c-436c-8b9f-4c2703648d64")
+ .withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.STRICT))
)
.respond(
response()
@@ -401,6 +408,49 @@ public void testDeleteTaskPushNotificationConfigurations() throws Exception {
instance.deleteTaskPushNotificationConfigurations(new DeleteTaskPushNotificationConfigParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), context);
}
+ @Test
+ public void testJdkAuthenticationFailuresRetainResponseDetails() {
+ for (int code : List.of(401, 403)) {
+ String taskId = "auth-" + code;
+ server.when(request().withMethod("GET").withPath("/v1/tasks/" + taskId))
+ .respond(response().withStatusCode(code).withBody("unauthorized")
+ .withHeader("WWW-Authenticate", "Bearer"));
+ var client = new RestTransport_v0_3(CARD);
+ var exception = assertThrows(A2AClientException_v0_3.class,
+ () -> client.getTask(new TaskQueryParams_v0_3(taskId), null));
+ var io = assertInstanceOf(IOException.class, exception.getCause());
+ var cause = assertInstanceOf(A2AClientHTTPError.class, io.getCause());
+ assertEquals(code, cause.getCode());
+ assertEquals("unauthorized", cause.getResponseBody());
+ assertEquals(List.of("Bearer"), cause.getResponseHeaders().get("www-authenticate"));
+ }
+ }
+
+ @Test
+ public void testSetPushConfigPreservesIdAndImmutableInterceptorPayload() throws Exception {
+ String body = """
+ {"name":"tasks/task/pushNotificationConfigs/config-1",
+ "pushNotificationConfig":{"id":"config-1","url":"https://example.com/callback"}}
+ """;
+ server.when(request().withMethod("POST").withPath("/v1/tasks/task/pushNotificationConfigs")
+ .withQueryStringParameter("configId", "config-1")
+ .withHeader("X-Trace", "trace")
+ .withBody(JsonBody.json(body, MatchType.STRICT)))
+ .respond(response().withStatusCode(201).withBody(body));
+ ClientCallInterceptor_v0_3 interceptor = new ClientCallInterceptor_v0_3() {
+ @Override
+ public PayloadAndHeaders_v0_3 intercept(String method, Object payload, java.util.Map headers,
+ AgentCard_v0_3 card, ClientCallContext_v0_3 context) {
+ var builder = (org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest.Builder) payload;
+ return new PayloadAndHeaders_v0_3(builder.build(), java.util.Map.of("X-Trace", "trace"));
+ }
+ };
+ var client = new RestTransport_v0_3(null, CARD, CARD.url(), List.of(interceptor));
+ var result = client.setTaskPushNotificationConfiguration(new TaskPushNotificationConfig_v0_3("task",
+ new PushNotificationConfig_v0_3.Builder().id("config-1").url("https://example.com/callback").build()), null);
+ assertEquals("config-1", result.pushNotificationConfig().id());
+ }
+
/**
* Test of resubscribe method, of class JSONRestTransport.
*/
@@ -408,11 +458,14 @@ public void testDeleteTaskPushNotificationConfigurations() throws Exception {
public void testResubscribe() throws Exception {
LOGGER.info("Testing resubscribe");
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/v1/tasks/task-1234:subscribe")
- )
+ this.server.when(request()
+ .withMethod("GET")
+ .withPath("/v1/tasks/task-1234:subscribe"))
+ .respond(response().withStatusCode(405));
+ this.server.when(request()
+ .withMethod("POST")
+ .withPath("/v1/tasks/task-1234:subscribe")
+ .withBody(JsonBody.json("{\"name\":\"tasks/task-1234\"}", MatchType.ONLY_MATCHING_FIELDS)))
.respond(
response()
.withStatusCode(200)
@@ -429,10 +482,15 @@ public void testResubscribe() throws Exception {
receivedEvent.set(event);
latch.countDown();
};
- Consumer errorHandler = error -> {};
+ AtomicReference failure = new AtomicReference<>();
+ Consumer errorHandler = error -> {
+ failure.set(error);
+ latch.countDown();
+ };
client.resubscribe(taskIdParams, eventHandler, errorHandler, null);
boolean eventReceived = latch.await(10, TimeUnit.SECONDS);
+ assertNull(failure.get());
assertTrue(eventReceived);
StreamingEventKind_v0_3 eventKind = receivedEvent.get();;
diff --git a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/RestSSEEventListener_v0_3_Test.java b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/RestSSEEventListener_v0_3_Test.java
new file mode 100644
index 000000000..89e4853f9
--- /dev/null
+++ b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/RestSSEEventListener_v0_3_Test.java
@@ -0,0 +1,124 @@
+package org.a2aproject.sdk.compat03.client.transport.rest.sse;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+
+class RestSSEEventListener_v0_3_Test {
+ @Test
+ void malformedEventReportsAndCancels() {
+ AtomicReference receivedError = new AtomicReference<>();
+ RestSSEEventListener_v0_3 listener = new RestSSEEventListener_v0_3(
+ event -> {}, receivedError::set);
+ CancelCapturingFuture future = new CancelCapturingFuture();
+
+ listener.onMessage("{not-json", future);
+
+ assertNotNull(receivedError.get());
+ assertTrue(future.cancelled);
+ }
+
+ @Test
+ void invalidPayloadReportsAndCancels() {
+ AtomicReference receivedError = new AtomicReference<>();
+ RestSSEEventListener_v0_3 listener = new RestSSEEventListener_v0_3(
+ event -> {}, receivedError::set);
+ CancelCapturingFuture future = new CancelCapturingFuture();
+
+ listener.onMessage("{}", future);
+
+ assertNotNull(receivedError.get());
+ assertTrue(future.cancelled);
+ }
+
+ @Test
+ void finalStatusUpdateCancels() {
+ RestSSEEventListener_v0_3 listener = new RestSSEEventListener_v0_3(
+ event -> {}, error -> {});
+ CancelCapturingFuture future = new CancelCapturingFuture();
+
+ listener.onMessage("""
+ {
+ "status_update": {
+ "task_id": "task-1",
+ "context_id": "context-1",
+ "status": {"state": "TASK_STATE_COMPLETED"},
+ "final": true
+ }
+ }""", future);
+
+ assertTrue(future.cancelled);
+ }
+
+ @Test
+ void finalTaskCancels() {
+ RestSSEEventListener_v0_3 listener = new RestSSEEventListener_v0_3(
+ event -> {}, error -> {});
+ CancelCapturingFuture future = new CancelCapturingFuture();
+
+ listener.onMessage("""
+ {
+ "task": {
+ "id": "task-1",
+ "contextId": "context-1",
+ "status": {"state": "TASK_STATE_COMPLETED"}
+ }
+ }""", future);
+
+ assertTrue(future.cancelled);
+ }
+
+ @Test
+ void malformedEventWithoutErrorHandlerDoesNotThrow() {
+ RestSSEEventListener_v0_3 listener = new RestSSEEventListener_v0_3(
+ event -> {}, null);
+
+ listener.onMessage("{not-json", null);
+ }
+
+ @Test
+ void invalidPayloadWithoutErrorHandlerDoesNotThrow() {
+ RestSSEEventListener_v0_3 listener = new RestSSEEventListener_v0_3(
+ event -> {}, null);
+
+ listener.onMessage("{}", null);
+ }
+
+ private static final class CancelCapturingFuture implements Future {
+ private boolean cancelled;
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ cancelled = true;
+ return true;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public boolean isDone() {
+ return cancelled;
+ }
+
+ @Override
+ public Void get() throws InterruptedException, ExecutionException {
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return null;
+ }
+ }
+}
diff --git a/compat-0.3/conversion/pom.xml b/compat-0.3/conversion/pom.xml
new file mode 100644
index 000000000..a438195fa
--- /dev/null
+++ b/compat-0.3/conversion/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.4.1.Final-SNAPSHOT
+ ..
+
+ a2a-java-sdk-compat-0.3-conversion
+ Java SDK A2A Compat 0.3 Conversion
+ Neutral 0.3 to 1.0 type conversion mappings
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+
+
+ ${project.groupId}
+ a2a-java-sdk-spec
+
+
+ org.mapstruct
+ mapstruct
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A03ToV10MapperConfig.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A03ToV10MapperConfig.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A03ToV10MapperConfig.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A03ToV10MapperConfig.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A2AMappers_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A2AMappers_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A2AMappers_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A2AMappers_v0_3.java
diff --git a/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AgentCardMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AgentCardMapper_v0_3.java
new file mode 100644
index 000000000..5b3d44032
--- /dev/null
+++ b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AgentCardMapper_v0_3.java
@@ -0,0 +1,240 @@
+package org.a2aproject.sdk.compat03.conversion.mappers.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.a2aproject.sdk.compat03.spec.APIKeySecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCardSignature_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentExtension_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentProvider_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentSkill_v0_3;
+import org.a2aproject.sdk.compat03.spec.HTTPAuthSecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.SecurityScheme_v0_3;
+import org.a2aproject.sdk.spec.APIKeySecurityScheme;
+import org.a2aproject.sdk.spec.AgentCapabilities;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentCardSignature;
+import org.a2aproject.sdk.spec.AgentExtension;
+import org.a2aproject.sdk.spec.AgentInterface;
+import org.a2aproject.sdk.spec.AgentProvider;
+import org.a2aproject.sdk.spec.AgentSkill;
+import org.a2aproject.sdk.spec.AuthorizationCodeOAuthFlow;
+import org.a2aproject.sdk.spec.ClientCredentialsOAuthFlow;
+import org.a2aproject.sdk.spec.HTTPAuthSecurityScheme;
+import org.a2aproject.sdk.spec.Legacy_0_3_AgentInterface;
+import org.a2aproject.sdk.spec.MutualTLSSecurityScheme;
+import org.a2aproject.sdk.spec.OAuth2SecurityScheme;
+import org.a2aproject.sdk.spec.OAuthFlows;
+import org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme;
+import org.a2aproject.sdk.spec.SecurityRequirement;
+import org.a2aproject.sdk.spec.SecurityScheme;
+
+/** Converts the protocol card model without depending on server components. */
+public final class AgentCardMapper_v0_3 {
+
+ public static final AgentCardMapper_v0_3 INSTANCE = new AgentCardMapper_v0_3();
+
+ private AgentCardMapper_v0_3() {
+ }
+
+ public AgentCard toV10(AgentCard_v0_3 source) {
+ List interfaces = new ArrayList<>();
+ AgentInterface primaryInterface = new AgentInterface(
+ canonicalBinding(source.preferredTransport()), source.url(), null, "0.3");
+ interfaces.add(primaryInterface);
+ if (source.additionalInterfaces() != null) {
+ source.additionalInterfaces().stream()
+ .map(i -> new AgentInterface(canonicalBinding(i.transport()), i.url(), null, "0.3"))
+ .filter(i -> !interfaces.contains(i))
+ .forEach(interfaces::add);
+ }
+ String preferredTransport = canonicalBinding(source.preferredTransport());
+ return AgentCard.builder()
+ .name(source.name()).description(source.description()).provider(toV10(source.provider()))
+ .version(source.version()).documentationUrl(source.documentationUrl())
+ .capabilities(toV10Capabilities(source))
+ .defaultInputModes(source.defaultInputModes()).defaultOutputModes(source.defaultOutputModes())
+ .skills(source.skills().stream().map(this::toV10).toList())
+ .securitySchemes(toV10Security(source.securitySchemes()))
+ .securityRequirements(toV10Requirements(source.security()))
+ .iconUrl(source.iconUrl()).supportedInterfaces(interfaces)
+ .signatures(source.signatures() == null ? null : source.signatures().stream().map(this::toV10).toList())
+ .url(source.url()).preferredTransport(preferredTransport)
+ .additionalInterfaces(source.additionalInterfaces() == null ? List.of() : source.additionalInterfaces().stream()
+ .map(i -> new Legacy_0_3_AgentInterface(canonicalBinding(i.transport()), i.url())).toList())
+ .build();
+ }
+
+ public AgentCard_v0_3 fromV10(AgentCard source) {
+ List legacyInterfaces = source.supportedInterfaces().stream()
+ .filter(AgentCardMapper_v0_3::isV03Interface)
+ .toList();
+ AgentInterface primary = source.url() == null
+ ? legacyInterfaces.stream().findFirst().orElseThrow(
+ () -> new IllegalArgumentException("Agent card has no A2A 0.3 interface"))
+ : new AgentInterface(canonicalBinding(source.preferredTransport()), source.url(), null, "0.3");
+ String primaryBinding = canonicalBinding(primary.protocolBinding());
+ List interfaces = legacyInterfaces.stream()
+ .filter(i -> !primary.url().equals(i.url())
+ || !Objects.equals(primaryBinding, canonicalBinding(i.protocolBinding())))
+ .map(i -> new AgentInterface_v0_3(i.protocolBinding(), i.url())).toList();
+ return new AgentCard_v0_3(source.name(), source.description(), primary.url(), fromV10(source.provider()),
+ source.version(), source.documentationUrl(), fromV10(source.capabilities()), source.defaultInputModes(),
+ source.defaultOutputModes(), source.skills().stream().map(this::fromV10).toList(),
+ source.capabilities().extendedAgentCard(), fromV03Security(source.securitySchemes()),
+ fromV10Requirements(source.securityRequirements()), source.iconUrl(), interfaces,
+ primary.protocolBinding(), "0.3", source.signatures() == null ? null : source.signatures().stream()
+ .map(this::fromV10).toList());
+ }
+
+ private static boolean isV03Interface(AgentInterface agentInterface) {
+ return switch (agentInterface.protocolVersion().trim()) {
+ case "0.3", "0.3.0" -> true;
+ default -> false;
+ };
+ }
+
+ private AgentProvider toV10(AgentProvider_v0_3 value) {
+ return value == null ? null : new AgentProvider(value.organization(), value.url());
+ }
+
+ private AgentProvider_v0_3 fromV10(AgentProvider value) {
+ return value == null ? null : new AgentProvider_v0_3(value.organization(), value.url());
+ }
+
+ private AgentCapabilities toV10Capabilities(AgentCard_v0_3 value) {
+ AgentCapabilities_v0_3 capabilities = value.capabilities();
+ List extensions = capabilities.extensions() == null ? null : capabilities.extensions().stream()
+ .map(e -> new AgentExtension(e.description(), e.params(), e.required(), e.uri())).toList();
+ return new AgentCapabilities(capabilities.streaming(), capabilities.pushNotifications(),
+ value.supportsAuthenticatedExtendedCard(), extensions);
+ }
+
+ private AgentCapabilities_v0_3 fromV10(AgentCapabilities value) {
+ List extensions = value.extensions() == null ? null : value.extensions().stream()
+ .map(e -> new AgentExtension_v0_3(e.description(), e.params(), e.required(), e.uri())).toList();
+ return new AgentCapabilities_v0_3(value.streaming(), value.pushNotifications(), false, extensions);
+ }
+
+ private AgentSkill toV10(AgentSkill_v0_3 value) {
+ return AgentSkill.builder().id(value.id()).name(value.name()).description(value.description()).tags(value.tags())
+ .examples(value.examples()).inputModes(value.inputModes()).outputModes(value.outputModes())
+ .securityRequirements(toV10Requirements(value.security())).build();
+ }
+
+ private AgentSkill_v0_3 fromV10(AgentSkill value) {
+ return new AgentSkill_v0_3(value.id(), value.name(), value.description(), value.tags(), value.examples(),
+ value.inputModes(), value.outputModes(), fromV10Requirements(value.securityRequirements()));
+ }
+
+ private AgentCardSignature toV10(AgentCardSignature_v0_3 value) {
+ return new AgentCardSignature(value.header(), value.protectedHeader(), value.signature());
+ }
+
+ private AgentCardSignature_v0_3 fromV10(AgentCardSignature value) {
+ return new AgentCardSignature_v0_3(value.header(), value.protectedHeader(), value.signature());
+ }
+
+ private Map toV10Security(Map source) {
+ if (source == null) return null;
+ return source.entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> {
+ if (e.getValue() instanceof APIKeySecurityScheme_v0_3 api) {
+ return new APIKeySecurityScheme(APIKeySecurityScheme.Location.fromString(api.in()), api.name(), api.description());
+ }
+ if (e.getValue() instanceof HTTPAuthSecurityScheme_v0_3 http) {
+ return new HTTPAuthSecurityScheme(http.bearerFormat(), http.scheme(), http.description());
+ }
+ if (e.getValue() instanceof org.a2aproject.sdk.compat03.spec.OpenIdConnectSecurityScheme_v0_3 oidc) {
+ return new OpenIdConnectSecurityScheme(oidc.openIdConnectUrl(), oidc.description());
+ }
+ if (e.getValue() instanceof org.a2aproject.sdk.compat03.spec.MutualTLSSecurityScheme_v0_3 mtls) {
+ return new MutualTLSSecurityScheme(mtls.description());
+ }
+ if (e.getValue() instanceof org.a2aproject.sdk.compat03.spec.OAuth2SecurityScheme_v0_3 oauth) {
+ return new OAuth2SecurityScheme(toV10OAuthFlows(oauth.flows()), oauth.description(), oauth.oauth2MetadataUrl());
+ }
+ throw new IllegalArgumentException("Unsupported 0.3 security scheme: " + e.getValue().type());
+ }));
+ }
+
+ private Map fromV03Security(Map source) {
+ if (source == null) return null;
+ return source.entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> {
+ if (e.getValue() instanceof APIKeySecurityScheme api) {
+ return new APIKeySecurityScheme_v0_3(api.location().asString(), api.name(), api.description());
+ }
+ if (e.getValue() instanceof HTTPAuthSecurityScheme http) {
+ return new HTTPAuthSecurityScheme_v0_3(http.bearerFormat(), http.scheme(), http.description());
+ }
+ if (e.getValue() instanceof OpenIdConnectSecurityScheme oidc) {
+ return new org.a2aproject.sdk.compat03.spec.OpenIdConnectSecurityScheme_v0_3(
+ oidc.openIdConnectUrl(), oidc.description());
+ }
+ if (e.getValue() instanceof MutualTLSSecurityScheme mtls) {
+ return new org.a2aproject.sdk.compat03.spec.MutualTLSSecurityScheme_v0_3(mtls.description());
+ }
+ if (e.getValue() instanceof OAuth2SecurityScheme oauth) {
+ return new org.a2aproject.sdk.compat03.spec.OAuth2SecurityScheme_v0_3(
+ fromV10OAuthFlows(oauth.flows()), oauth.description(), oauth.oauth2MetadataUrl());
+ }
+ throw new IllegalArgumentException("Unsupported 1.0 security scheme: " + e.getValue().type());
+ }));
+ }
+
+ private List toV10Requirements(List>> source) {
+ return source == null ? null : source.stream().map(SecurityRequirement::new).toList();
+ }
+
+ private List>> fromV10Requirements(List source) {
+ return source == null ? null : source.stream().map(SecurityRequirement::schemes).toList();
+ }
+
+ private OAuthFlows toV10OAuthFlows(org.a2aproject.sdk.compat03.spec.OAuthFlows_v0_3 source) {
+ if (source.implicit() != null || source.password() != null) {
+ throw new IllegalArgumentException("OAuth implicit and password flows are not supported by A2A protocol 1.0");
+ }
+ AuthorizationCodeOAuthFlow authorizationCode = source.authorizationCode() == null ? null
+ : new AuthorizationCodeOAuthFlow(source.authorizationCode().authorizationUrl(), source.authorizationCode().refreshUrl(),
+ source.authorizationCode().scopes(), source.authorizationCode().tokenUrl(), false);
+ ClientCredentialsOAuthFlow clientCredentials = source.clientCredentials() == null ? null
+ : new ClientCredentialsOAuthFlow(source.clientCredentials().refreshUrl(), source.clientCredentials().scopes(),
+ source.clientCredentials().tokenUrl());
+ return new OAuthFlows(authorizationCode, clientCredentials, null);
+ }
+
+ private org.a2aproject.sdk.compat03.spec.OAuthFlows_v0_3 fromV10OAuthFlows(OAuthFlows source) {
+ if (source.deviceCode() != null) {
+ throw new IllegalArgumentException("OAuth device code flow is not supported by A2A protocol 0.3");
+ }
+ if (source.authorizationCode() != null && source.authorizationCode().pkceRequired()) {
+ throw new IllegalArgumentException("PKCE-required OAuth authorization code flow is not supported by A2A protocol 0.3");
+ }
+ org.a2aproject.sdk.compat03.spec.AuthorizationCodeOAuthFlow_v0_3 authorizationCode =
+ source.authorizationCode() == null ? null : new org.a2aproject.sdk.compat03.spec.AuthorizationCodeOAuthFlow_v0_3(
+ source.authorizationCode().authorizationUrl(), source.authorizationCode().refreshUrl(),
+ source.authorizationCode().scopes(), source.authorizationCode().tokenUrl());
+ org.a2aproject.sdk.compat03.spec.ClientCredentialsOAuthFlow_v0_3 clientCredentials =
+ source.clientCredentials() == null ? null : new org.a2aproject.sdk.compat03.spec.ClientCredentialsOAuthFlow_v0_3(
+ source.clientCredentials().refreshUrl(), source.clientCredentials().scopes(), source.clientCredentials().tokenUrl());
+ return new org.a2aproject.sdk.compat03.spec.OAuthFlows_v0_3(authorizationCode, clientCredentials, null, null);
+ }
+
+ private static String canonicalBinding(String transport) {
+ if (transport == null) {
+ return null;
+ }
+ return switch (transport.toLowerCase(Locale.ROOT)) {
+ case "jsonrpc" -> "JSONRPC";
+ case "http", "rest", "http+json" -> "HTTP+JSON";
+ case "grpc" -> "GRPC";
+ default -> transport;
+ };
+ }
+}
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/ArtifactMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/ArtifactMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/ArtifactMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/ArtifactMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AuthenticationInfoMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AuthenticationInfoMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AuthenticationInfoMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AuthenticationInfoMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/EventKindMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/EventKindMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/EventKindMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/EventKindMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java
similarity index 91%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java
index a39f840cf..37fa0bf3f 100644
--- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java
+++ b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java
@@ -1,7 +1,7 @@
package org.a2aproject.sdk.compat03.conversion.mappers.domain;
-import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3;
import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig;
+import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3;
import org.a2aproject.sdk.compat03.spec.FileContent_v0_3;
import org.a2aproject.sdk.compat03.spec.FileWithBytes_v0_3;
import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3;
@@ -51,11 +51,13 @@ default FileContent toV10(FileContent_v0_3 v03) {
}
if (v03 instanceof FileWithBytes_v0_3 v03Bytes) {
+ String mimeType = v03Bytes.mimeType() != null ? v03Bytes.mimeType() : "";
String name = v03Bytes.name() != null ? v03Bytes.name() : "";
- return new FileWithBytes(v03Bytes.mimeType(), name, v03Bytes.bytes());
+ return new FileWithBytes(mimeType, name, v03Bytes.bytes());
} else if (v03 instanceof FileWithUri_v0_3 v03Uri) {
+ String mimeType = v03Uri.mimeType() != null ? v03Uri.mimeType() : "";
String name = v03Uri.name() != null ? v03Uri.name() : "";
- return new FileWithUri(v03Uri.mimeType(), name, v03Uri.uri());
+ return new FileWithUri(mimeType, name, v03Uri.uri());
}
throw new InvalidRequestError(null, "Unrecognized FileContent type: " + v03.getClass().getName(), null);
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/MessageMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/MessageMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/MessageMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/MessageMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/PartMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/PartMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/PartMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/PartMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/RoleMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/RoleMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/RoleMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/RoleMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/StreamingEventKindMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/StreamingEventKindMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/StreamingEventKindMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/StreamingEventKindMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskArtifactUpdateEventMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskArtifactUpdateEventMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskArtifactUpdateEventMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskArtifactUpdateEventMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java
similarity index 86%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java
index 0b9534a30..e91ff1108 100644
--- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java
+++ b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java
@@ -1,7 +1,7 @@
package org.a2aproject.sdk.compat03.conversion.mappers.domain;
-import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3;
import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig;
+import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3;
import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3;
import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
@@ -73,17 +73,19 @@ default TaskPushNotificationConfig_v0_3 fromV10(
return null;
}
- PushNotificationConfig_v0_3 pushConfig =
- new PushNotificationConfig_v0_3(
- v10.url(),
- v10.token(),
- AuthenticationInfoMapper_v0_3.INSTANCE.fromV10ToPushNotification(v10.authentication()),
- v10.id()
- );
-
return new TaskPushNotificationConfig_v0_3(
v10.taskId(),
- pushConfig
+ fromV10PushNotificationConfig(v10)
+ );
+ }
+
+ /** Converts callback fields without requiring a task ID for inline message configuration. */
+ default PushNotificationConfig_v0_3 fromV10PushNotificationConfig(TaskPushNotificationConfig v10) {
+ return new PushNotificationConfig_v0_3(
+ v10.url(),
+ v10.token(),
+ AuthenticationInfoMapper_v0_3.INSTANCE.fromV10ToPushNotification(v10.authentication()),
+ v10.id()
);
}
}
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStateMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStateMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStateMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStateMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/CancelTaskParamsMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/CancelTaskParamsMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/CancelTaskParamsMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/CancelTaskParamsMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java
similarity index 94%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java
index c1228087e..f3aaa28ec 100644
--- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java
+++ b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java
@@ -1,7 +1,7 @@
package org.a2aproject.sdk.compat03.conversion.mappers.params;
-import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3;
import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig;
+import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3;
import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskPushNotificationConfigMapper_v0_3;
import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3;
import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3;
@@ -89,9 +89,8 @@ default MessageSendConfiguration_v0_3 fromV10(
// Extract PushNotificationConfig from TaskPushNotificationConfig if present
PushNotificationConfig_v0_3 pushConfig = null;
if (v10.taskPushNotificationConfig() != null) {
- TaskPushNotificationConfig_v0_3 v03TaskConfig =
- TaskPushNotificationConfigMapper_v0_3.INSTANCE.fromV10(v10.taskPushNotificationConfig());
- pushConfig = v03TaskConfig.pushNotificationConfig();
+ pushConfig = TaskPushNotificationConfigMapper_v0_3.INSTANCE
+ .fromV10PushNotificationConfig(v10.taskPushNotificationConfig());
}
// Convert returnImmediately to blocking (inverse semantics)
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskIdParamsMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskIdParamsMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskIdParamsMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskIdParamsMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskQueryParamsMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskQueryParamsMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskQueryParamsMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskQueryParamsMapper_v0_3.java
diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/result/ListTaskPushNotificationConfigsResultMapper_v0_3.java b/compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/result/ListTaskPushNotificationConfigsResultMapper_v0_3.java
similarity index 100%
rename from compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/result/ListTaskPushNotificationConfigsResultMapper_v0_3.java
rename to compat-0.3/conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/result/ListTaskPushNotificationConfigsResultMapper_v0_3.java
diff --git a/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AgentCardMapper_v0_3_Test.java b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AgentCardMapper_v0_3_Test.java
new file mode 100644
index 000000000..49ea7c227
--- /dev/null
+++ b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AgentCardMapper_v0_3_Test.java
@@ -0,0 +1,194 @@
+package org.a2aproject.sdk.compat03.conversion.mappers.domain;
+
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.APIKeySecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCardSignature_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3;
+import org.a2aproject.sdk.compat03.spec.AuthorizationCodeOAuthFlow_v0_3;
+import org.a2aproject.sdk.compat03.spec.ClientCredentialsOAuthFlow_v0_3;
+import org.a2aproject.sdk.compat03.spec.HTTPAuthSecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentSkill_v0_3;
+import org.a2aproject.sdk.compat03.spec.MutualTLSSecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.OAuth2SecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.OAuthFlows_v0_3;
+import org.a2aproject.sdk.compat03.spec.OpenIdConnectSecurityScheme_v0_3;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+import org.a2aproject.sdk.spec.APIKeySecurityScheme;
+import org.a2aproject.sdk.spec.HTTPAuthSecurityScheme;
+import org.a2aproject.sdk.spec.MutualTLSSecurityScheme;
+import org.a2aproject.sdk.spec.OAuth2SecurityScheme;
+import org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class AgentCardMapper_v0_3_Test {
+
+ @Test
+ void projectsCompleteLegacyCardAndConvertsItBack() {
+ AgentCard_v0_3 legacy = new AgentCard_v0_3(
+ "agent", "description", "https://agent.example", null, "1", "https://docs.example",
+ new AgentCapabilities_v0_3(true, true, false, null), List.of("text"), List.of("text"),
+ List.of(new AgentSkill_v0_3("skill", "Skill", "Does things", List.of("tag"),
+ List.of("example"), List.of("text"), List.of("text"), List.of(Map.of("auth", List.of("read"))))),
+ true, Map.of("auth", new APIKeySecurityScheme_v0_3("header", "Authorization", "token")),
+ List.of(Map.of("auth", List.of("read"))), "https://icon.example",
+ List.of(new AgentInterface_v0_3("JSONRPC", "https://agent.example/rpc")), "JSONRPC", "0.3.0",
+ List.of(new AgentCardSignature_v0_3(Map.of("alg", "none"), "protected", "signature")));
+
+ AgentCard current = AgentCardMapper_v0_3.INSTANCE.toV10(legacy);
+
+ assertEquals("https://agent.example", current.url());
+ assertEquals("JSONRPC", current.preferredTransport());
+ assertEquals("0.3", current.supportedInterfaces().get(0).protocolVersion());
+ assertEquals(true, current.capabilities().extendedAgentCard());
+ assertEquals("Authorization", ((APIKeySecurityScheme) current.securitySchemes().get("auth")).name());
+ assertNotNull(current.signatures());
+
+ AgentCard_v0_3 roundTrip = AgentCardMapper_v0_3.INSTANCE.fromV10(current);
+ assertEquals("https://agent.example", roundTrip.url());
+ assertEquals("0.3", roundTrip.protocolVersion());
+ assertEquals(true, roundTrip.supportsAuthenticatedExtendedCard());
+ assertEquals(false, roundTrip.capabilities().stateTransitionHistory());
+ assertEquals("Authorization", ((APIKeySecurityScheme_v0_3) roundTrip.securitySchemes().get("auth")).name());
+ assertEquals("signature", roundTrip.signatures().get(0).signature());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "jsonrpc,JSONRPC",
+ "http,HTTP+JSON",
+ "rest,HTTP+JSON",
+ "grpc,GRPC",
+ "HTTP+JSON,HTTP+JSON"
+ })
+ void primaryEndpointBecomesSupportedInterface(String preferred, String expectedBinding) {
+ AgentCard_v0_3 card = primaryOnlyCard(preferred);
+
+ AgentCard projected = AgentCardMapper_v0_3.INSTANCE.toV10(card);
+
+ assertEquals(1, projected.supportedInterfaces().size());
+ assertEquals(expectedBinding, projected.supportedInterfaces().get(0).protocolBinding());
+ assertEquals("0.3", projected.supportedInterfaces().get(0).protocolVersion());
+ assertEquals(expectedBinding, projected.preferredTransport());
+ }
+
+ @Test
+ void projectsPrimaryInterfaceBeforeAdditionalInterfaces() {
+ AgentCard_v0_3 card = new AgentCard_v0_3(
+ "legacy", "legacy", "https://agent.example/jsonrpc", null, "1", null,
+ new AgentCapabilities_v0_3.Builder().build(), List.of("text"), List.of("text"), List.of(),
+ false, null, null, null,
+ List.of(new AgentInterface_v0_3("grpc", "https://agent.example/grpc")), "jsonrpc", "0.3", null);
+
+ AgentCard projected = AgentCardMapper_v0_3.INSTANCE.toV10(card);
+
+ assertEquals(List.of("JSONRPC", "GRPC"), projected.supportedInterfaces().stream()
+ .map(AgentInterface::protocolBinding).toList());
+ assertEquals("https://agent.example/jsonrpc", projected.supportedInterfaces().get(0).url());
+ }
+
+ @Test
+ void projectsCardWhenAdditionalInterfacesAreOmitted() {
+ AgentCard_v0_3 card = new AgentCard_v0_3(
+ "legacy", "legacy", "https://agent.example/jsonrpc", null, "1", null,
+ new AgentCapabilities_v0_3.Builder().build(), List.of("text"), List.of("text"), List.of(),
+ false, null, null, null, null, "jsonrpc", "0.3", null);
+
+ AgentCard projected = AgentCardMapper_v0_3.INSTANCE.toV10(card);
+
+ assertEquals(1, projected.supportedInterfaces().size());
+ assertEquals("JSONRPC", projected.supportedInterfaces().get(0).protocolBinding());
+ }
+
+ @Test
+ void doesNotCopyPrimaryInterfaceIntoAdditionalInterfacesOnRoundTrip() {
+ AgentCard_v0_3 card = primaryOnlyCard("jsonrpc");
+
+ AgentCard_v0_3 roundTrip = AgentCardMapper_v0_3.INSTANCE.fromV10(
+ AgentCardMapper_v0_3.INSTANCE.toV10(card));
+
+ assertEquals(List.of(), roundTrip.additionalInterfaces());
+ }
+
+ @Test
+ void convertsPatchVersionLegacyInterfaceWhenV1CardHasNoLegacyUrl() {
+ AgentCard card = AgentCard.builder()
+ .name("agent").description("description").version("1")
+ .capabilities(new org.a2aproject.sdk.spec.AgentCapabilities(false, false, false, null))
+ .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text"))
+ .skills(List.of())
+ .supportedInterfaces(List.of(new AgentInterface("JSONRPC", "https://agent.example/rpc", null, "0.3.0")))
+ .build();
+
+ AgentCard_v0_3 legacy = AgentCardMapper_v0_3.INSTANCE.fromV10(card);
+
+ assertEquals("https://agent.example/rpc", legacy.url());
+ assertEquals("JSONRPC", legacy.preferredTransport());
+ assertEquals(List.of(), legacy.additionalInterfaces());
+ }
+
+ @Test
+ void convertsHttpAuthSecuritySchemeBothWays() {
+ HTTPAuthSecurityScheme_v0_3 legacyScheme = new HTTPAuthSecurityScheme_v0_3.Builder()
+ .scheme("basic").bearerFormat("none").description("HTTP Basic authentication").build();
+ AgentCard_v0_3 card = primaryOnlyCard("http", Map.of("basicAuth", legacyScheme));
+
+ AgentCard projected = AgentCardMapper_v0_3.INSTANCE.toV10(card);
+ HTTPAuthSecurityScheme projectedScheme =
+ (HTTPAuthSecurityScheme) projected.securitySchemes().get("basicAuth");
+ assertEquals("basic", projectedScheme.scheme());
+ assertEquals("none", projectedScheme.bearerFormat());
+ assertEquals("HTTP Basic authentication", projectedScheme.description());
+
+ HTTPAuthSecurityScheme_v0_3 roundTripScheme = (HTTPAuthSecurityScheme_v0_3)
+ AgentCardMapper_v0_3.INSTANCE.fromV10(projected).securitySchemes().get("basicAuth");
+ assertEquals("basic", roundTripScheme.scheme());
+ assertEquals("none", roundTripScheme.bearerFormat());
+ assertEquals("HTTP Basic authentication", roundTripScheme.description());
+ }
+
+ @Test
+ void convertsOpenIdConnectMutualTlsAndCompatibleOAuthSecuritySchemes() {
+ OAuthFlows_v0_3 flows = new OAuthFlows_v0_3(
+ new AuthorizationCodeOAuthFlow_v0_3("https://auth.example", "https://refresh.example",
+ Map.of("read", "Read"), "https://token.example"),
+ new ClientCredentialsOAuthFlow_v0_3("https://refresh.example", Map.of("write", "Write"),
+ "https://token.example"), null, null);
+ AgentCard_v0_3 card = primaryOnlyCard("jsonrpc", Map.of(
+ "oidc", new OpenIdConnectSecurityScheme_v0_3("https://oidc.example", "OIDC"),
+ "mtls", new MutualTLSSecurityScheme_v0_3("mTLS"),
+ "oauth", new OAuth2SecurityScheme_v0_3(flows, "OAuth", "https://metadata.example")));
+
+ AgentCard projected = AgentCardMapper_v0_3.INSTANCE.toV10(card);
+
+ assertEquals("https://oidc.example", ((OpenIdConnectSecurityScheme) projected.securitySchemes().get("oidc"))
+ .openIdConnectUrl());
+ assertEquals("mTLS", ((MutualTLSSecurityScheme) projected.securitySchemes().get("mtls")).description());
+ OAuth2SecurityScheme oauth = (OAuth2SecurityScheme) projected.securitySchemes().get("oauth");
+ assertEquals("https://auth.example", oauth.flows().authorizationCode().authorizationUrl());
+ assertEquals("https://token.example", oauth.flows().clientCredentials().tokenUrl());
+ }
+
+ 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/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java
similarity index 70%
rename from compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java
rename to compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java
index 2b562ccc0..4070c6b05 100644
--- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java
+++ b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java
@@ -1,5 +1,8 @@
package org.a2aproject.sdk.compat03.conversion.mappers.domain;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
import org.a2aproject.sdk.compat03.spec.FileWithBytes_v0_3;
import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3;
import org.a2aproject.sdk.spec.FileContent;
@@ -7,9 +10,6 @@
import org.a2aproject.sdk.spec.FileWithUri;
import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
-
class FileContentMapper_v0_3_Test {
@Test
@@ -50,6 +50,32 @@ void fileWithUriFromV10PreservesName() {
assertEquals("file:///path/to/readme.txt", result.uri());
}
+ @Test
+ void inlineFileWithoutMimeTypeConvertsToV10() {
+ FileWithBytes_v0_3 legacy = new FileWithBytes_v0_3(null, null, "aGVsbG8=");
+
+ FileWithBytes current = assertInstanceOf(FileWithBytes.class,
+ FileContentMapper_v0_3.INSTANCE.toV10(legacy));
+
+ assertEquals("", current.mimeType());
+ assertEquals("", current.name());
+ assertEquals("aGVsbG8=", current.bytes());
+ assertEquals(new FileWithBytes_v0_3("", "", "aGVsbG8="),
+ FileContentMapper_v0_3.INSTANCE.fromV10(current));
+ }
+
+ @Test
+ void uriFileWithoutMimeTypeConvertsToV10() {
+ FileWithUri_v0_3 legacy = new FileWithUri_v0_3(null, null, "https://example.test/file");
+
+ FileWithUri current = assertInstanceOf(FileWithUri.class,
+ FileContentMapper_v0_3.INSTANCE.toV10(legacy));
+
+ assertEquals(new FileWithUri("", "", "https://example.test/file"), current);
+ assertEquals(new FileWithUri_v0_3("", "", "https://example.test/file"),
+ FileContentMapper_v0_3.INSTANCE.fromV10(current));
+ }
+
@Test
void nullNameInV03BecomesEmptyStringInV10() {
FileWithBytes_v0_3 v03 = new FileWithBytes_v0_3("image/png", null, "aGVsbG8=");
diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3_Test.java b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3_Test.java
similarity index 100%
rename from compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3_Test.java
rename to compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3_Test.java
diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3_Test.java b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3_Test.java
similarity index 100%
rename from compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3_Test.java
rename to compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3_Test.java
diff --git a/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3_Test.java b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3_Test.java
new file mode 100644
index 000000000..2f894c5f7
--- /dev/null
+++ b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3_Test.java
@@ -0,0 +1,67 @@
+package org.a2aproject.sdk.compat03.conversion.mappers.params;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+
+import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskPushNotificationConfigMapper_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3;
+import org.a2aproject.sdk.compat03.spec.PushNotificationAuthenticationInfo_v0_3;
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.spec.AuthenticationInfo;
+import org.a2aproject.sdk.spec.MessageSendConfiguration;
+import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+
+class MessageSendConfigurationMapper_v0_3_Test {
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void inlinePushConfigurationDoesNotRequireTaskId(@Nullable String taskId) {
+ TaskPushNotificationConfig push = new TaskPushNotificationConfig(
+ "config", taskId, "https://callback.example.test", "token",
+ new AuthenticationInfo("Bearer", "credentials"), null);
+ MessageSendConfiguration current = new MessageSendConfiguration(List.of("text/plain"), 3, push, false);
+
+ MessageSendConfiguration_v0_3 legacy = MessageSendConfigurationMapper_v0_3.INSTANCE.fromV10(current);
+
+ assertEquals(new MessageSendConfiguration_v0_3(List.of("text/plain"), 3,
+ new PushNotificationConfig_v0_3("https://callback.example.test", "token",
+ new PushNotificationAuthenticationInfo_v0_3(List.of("Bearer"), "credentials"), "config"),
+ true), legacy);
+ MessageSendConfiguration roundTrip = MessageSendConfigurationMapper_v0_3.INSTANCE.toV10(legacy);
+ assertEquals(new TaskPushNotificationConfig("config", "", "https://callback.example.test", "token",
+ new AuthenticationInfo("Bearer", "credentials"), ""), roundTrip.taskPushNotificationConfig());
+ assertEquals(List.of("text/plain"), roundTrip.acceptedOutputModes());
+ assertEquals(3, roundTrip.historyLength());
+ assertEquals(false, roundTrip.returnImmediately());
+ }
+
+ @Test
+ void standalonePushConfigurationStillRequiresTaskId() {
+ TaskPushNotificationConfig push = TaskPushNotificationConfig.builder()
+ .url("https://callback.example.test").build();
+
+ assertThrows(IllegalArgumentException.class,
+ () -> TaskPushNotificationConfigMapper_v0_3.INSTANCE.fromV10(push));
+ }
+
+ @Test
+ void standalonePushConfigurationRetainsTaskIdAndCallbackFields() {
+ TaskPushNotificationConfig current = new TaskPushNotificationConfig(
+ "config", "task", "https://callback.example.test", "token",
+ new AuthenticationInfo("Bearer", "credentials"), "");
+
+ TaskPushNotificationConfig_v0_3 legacy = TaskPushNotificationConfigMapper_v0_3.INSTANCE.fromV10(current);
+
+ assertEquals(new TaskPushNotificationConfig_v0_3("task",
+ new PushNotificationConfig_v0_3("https://callback.example.test", "token",
+ new PushNotificationAuthenticationInfo_v0_3(List.of("Bearer"), "credentials"), "config")), legacy);
+ assertEquals(current, TaskPushNotificationConfigMapper_v0_3.INSTANCE.toV10(legacy));
+ }
+}
diff --git a/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3_Test.java b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3_Test.java
new file mode 100644
index 000000000..05371e643
--- /dev/null
+++ b/compat-0.3/conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3_Test.java
@@ -0,0 +1,27 @@
+package org.a2aproject.sdk.compat03.conversion.mappers.params;
+
+import java.util.List;
+
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.TextPart_v0_3;
+import org.a2aproject.sdk.spec.MessageSendParams;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MessageSendParamsMapper_v0_3_Test {
+
+ @Test
+ void roundTripPreservesMessageAndAddsOnlyDefaultTenant() {
+ Message_v0_3 message = new Message_v0_3(
+ Message_v0_3.Role.USER, List.of(new TextPart_v0_3("hello")), "message", "context",
+ null, null, null, null);
+ MessageSendParams_v0_3 legacy = new MessageSendParams_v0_3(message, null, null);
+
+ MessageSendParams current = MessageSendParamsMapper_v0_3.INSTANCE.toV10(legacy);
+
+ assertEquals("", current.tenant());
+ assertEquals(legacy, MessageSendParamsMapper_v0_3.INSTANCE.fromV10(current));
+ }
+}
diff --git a/compat-0.3/pom.xml b/compat-0.3/pom.xml
index 87b7c1ca4..d7a7f9e4f 100644
--- a/compat-0.3/pom.xml
+++ b/compat-0.3/pom.xml
@@ -24,6 +24,31 @@
a2a-java-sdk-compat-0.3-spec
${project.version}