diff --git a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/test/ExtrasBomVerifier.java b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/test/ExtrasBomVerifier.java
index 8357ce447..c5b727633 100644
--- a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/test/ExtrasBomVerifier.java
+++ b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/test/ExtrasBomVerifier.java
@@ -17,6 +17,7 @@ public class ExtrasBomVerifier extends DynamicBomVerifier {
"tck/", // TCK test suite
"tests/", // Integration tests
"test-utils-docker/", // Test utilities for Docker-based tests
+ "compat-0.3/", // Compat 0.3 modules (part of SDK BOM, not extras BOM)
"extras/queue-manager-replicated/tests-multi-instance/", // Test harness applications
"extras/queue-manager-replicated/tests-single-instance/", // Test harness applications
"extras/opentelemetry/integration-tests/" // Test harness applications
diff --git a/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/test/ReferenceBomVerifier.java b/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/test/ReferenceBomVerifier.java
index 6b20bb50e..d25473846 100644
--- a/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/test/ReferenceBomVerifier.java
+++ b/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/test/ReferenceBomVerifier.java
@@ -16,7 +16,8 @@ public class ReferenceBomVerifier extends DynamicBomVerifier {
"examples/", // Example applications
"tck/", // TCK test suite
"tests/", // Integration tests
- "test-utils-docker/" // Test utilities for Docker-based tests
+ "test-utils-docker/", // Test utilities for Docker-based tests
+ "compat-0.3/" // Compat 0.3 modules (part of SDK BOM, not reference BOM)
// Note: reference/ is NOT in this list - we want to verify those classes load
);
diff --git a/boms/sdk/pom.xml b/boms/sdk/pom.xml
index cbc5fb075..0ad2031ab 100644
--- a/boms/sdk/pom.xml
+++ b/boms/sdk/pom.xml
@@ -108,6 +108,91 @@
${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec-grpc
+ ${project.version}
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-http-client
+ ${project.version}
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-rest
+ ${project.version}
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-transport-jsonrpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-transport-grpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-transport-rest
+ ${project.version}
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-reference-common
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-reference-jsonrpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-reference-grpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-reference-rest
+ ${project.version}
+
+
${project.groupId}
diff --git a/boms/sdk/src/it/sdk-usage-test/pom.xml b/boms/sdk/src/it/sdk-usage-test/pom.xml
index 5da56e93e..6ed3768bb 100644
--- a/boms/sdk/src/it/sdk-usage-test/pom.xml
+++ b/boms/sdk/src/it/sdk-usage-test/pom.xml
@@ -103,6 +103,76 @@
a2a-java-sdk-transport-rest
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-spec
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-spec-grpc
+
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-http-client
+
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-rest
+
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-transport-jsonrpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-transport-grpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-transport-rest
+
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-reference-common
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-reference-jsonrpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-reference-grpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-reference-rest
+
+
org.slf4j
diff --git a/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/test/SdkBomVerifier.java b/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/test/SdkBomVerifier.java
index f69d70b85..7f0607b8d 100644
--- a/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/test/SdkBomVerifier.java
+++ b/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/test/SdkBomVerifier.java
@@ -15,6 +15,7 @@ public class SdkBomVerifier extends DynamicBomVerifier {
"boms/", // BOM test modules themselves
"examples/", // Example applications
"tck/", // TCK test suite
+ "compat-0.3/tck/", // Compat 0.3 TCK (not yet enabled)
"tests/", // Integration tests
"test-utils-docker/" // Test utilities for Docker-based tests
);
diff --git a/compat-0.3/client/base/pom.xml b/compat-0.3/client/base/pom.xml
new file mode 100644
index 000000000..d25627f63
--- /dev/null
+++ b/compat-0.3/client/base/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.0.0.Beta1-SNAPSHOT
+ ../..
+
+ a2a-java-sdk-compat-0.3-client
+
+ jar
+
+ Java SDK A2A Compat 0.3 Client
+ Java SDK for the Agent2Agent Protocol (A2A) - Client
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-http-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+ test
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-rest
+ test
+
+
+ ${project.groupId}
+ a2a-java-sdk-common
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec-grpc
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.mock-server
+ mockserver-netty
+ test
+
+
+ org.slf4j
+ slf4j-jdk14
+ test
+
+
+ io.grpc
+ grpc-testing
+ test
+
+
+ io.grpc
+ grpc-inprocess
+ test
+
+
+
+
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A.java
new file mode 100644
index 000000000..48921f535
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A.java
@@ -0,0 +1,189 @@
+package org.a2aproject.sdk.compat03;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.a2aproject.sdk.compat03.client.http.A2ACardResolver;
+import org.a2aproject.sdk.compat03.client.http.A2AHttpClient;
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient;
+import org.a2aproject.sdk.compat03.spec.A2AClientError;
+import org.a2aproject.sdk.compat03.spec.A2AClientJSONError;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.Message;
+import org.a2aproject.sdk.compat03.spec.Part;
+import org.a2aproject.sdk.compat03.spec.TextPart;
+
+
+/**
+ * Constants and utility methods related to the A2A protocol.
+ */
+public class A2A {
+
+ /**
+ * Convert the given text to a user message.
+ *
+ * @param text the message text
+ * @return the user message
+ */
+ public static Message toUserMessage(String text) {
+ return toMessage(text, Message.Role.USER, null);
+ }
+
+ /**
+ * Convert the given text to a user message.
+ *
+ * @param text the message text
+ * @param messageId the message ID to use
+ * @return the user message
+ */
+ public static Message toUserMessage(String text, String messageId) {
+ return toMessage(text, Message.Role.USER, messageId);
+ }
+
+ /**
+ * Convert the given text to an agent message.
+ *
+ * @param text the message text
+ * @return the agent message
+ */
+ public static Message toAgentMessage(String text) {
+ return toMessage(text, Message.Role.AGENT, null);
+ }
+
+ /**
+ * Convert the given text to an agent message.
+ *
+ * @param text the message text
+ * @param messageId the message ID to use
+ * @return the agent message
+ */
+ public static Message toAgentMessage(String text, String messageId) {
+ return toMessage(text, Message.Role.AGENT, messageId);
+ }
+
+ /**
+ * Create a user message with text content and optional context and task IDs.
+ *
+ * @param text the message text (required)
+ * @param contextId the context ID to use (optional)
+ * @param taskId the task ID to use (optional)
+ * @return the user message
+ */
+ public static Message createUserTextMessage(String text, String contextId, String taskId) {
+ return toMessage(text, Message.Role.USER, null, contextId, taskId);
+ }
+
+ /**
+ * Create an agent message with text content and optional context and task IDs.
+ *
+ * @param text the message text (required)
+ * @param contextId the context ID to use (optional)
+ * @param taskId the task ID to use (optional)
+ * @return the agent message
+ */
+ public static Message createAgentTextMessage(String text, String contextId, String taskId) {
+ return toMessage(text, Message.Role.AGENT, null, contextId, taskId);
+ }
+
+ /**
+ * Create an agent message with custom parts and optional context and task IDs.
+ *
+ * @param parts the message parts (required)
+ * @param contextId the context ID to use (optional)
+ * @param taskId the task ID to use (optional)
+ * @return the agent message
+ */
+ public static Message createAgentPartsMessage(List> parts, String contextId, String taskId) {
+ if (parts == null || parts.isEmpty()) {
+ throw new IllegalArgumentException("Parts cannot be null or empty");
+ }
+ return toMessage(parts, Message.Role.AGENT, null, contextId, taskId);
+ }
+
+ private static Message toMessage(String text, Message.Role role, String messageId) {
+ return toMessage(text, role, messageId, null, null);
+ }
+
+ private static Message toMessage(String text, Message.Role role, String messageId, String contextId, String taskId) {
+ Message.Builder messageBuilder = new Message.Builder()
+ .role(role)
+ .parts(Collections.singletonList(new TextPart(text)))
+ .contextId(contextId)
+ .taskId(taskId);
+ if (messageId != null) {
+ messageBuilder.messageId(messageId);
+ }
+ return messageBuilder.build();
+ }
+
+ private static Message toMessage(List> parts, Message.Role role, String messageId, String contextId, String taskId) {
+ Message.Builder messageBuilder = new Message.Builder()
+ .role(role)
+ .parts(parts)
+ .contextId(contextId)
+ .taskId(taskId);
+ if (messageId != null) {
+ messageBuilder.messageId(messageId);
+ }
+ return messageBuilder.build();
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard getAgentCard(String agentUrl) throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(new JdkA2AHttpClient(), agentUrl);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param httpClient the http client to use
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl) throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(httpClient, agentUrl, null, null);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @param relativeCardPath optional path to the agent card endpoint relative to the base
+ * agent URL, defaults to ".well-known/agent-card.json"
+ * @param authHeaders the HTTP authentication headers to use
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(new JdkA2AHttpClient(), agentUrl, relativeCardPath, authHeaders);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param httpClient the http client to use
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @param relativeCardPath optional path to the agent card endpoint relative to the base
+ * agent URL, defaults to ".well-known/agent-card.json"
+ * @param authHeaders the HTTP authentication headers to use
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError {
+ A2ACardResolver resolver = new A2ACardResolver(httpClient, agentUrl, relativeCardPath, authHeaders);
+ return resolver.getAgentCard();
+ }
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient.java
new file mode 100644
index 000000000..931d50825
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient.java
@@ -0,0 +1,392 @@
+package org.a2aproject.sdk.compat03.client;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.Message;
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig;
+import org.a2aproject.sdk.compat03.spec.Task;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Abstract class representing an A2A client. Provides a standard set
+ * of methods for interacting with an A2A agent, regardless of the underlying
+ * transport protocol. It supports sending messages, managing tasks, and
+ * handling event streams.
+ */
+public abstract class AbstractClient {
+
+ private final List> consumers;
+ private final @Nullable Consumer streamingErrorHandler;
+
+ public AbstractClient(List> consumers) {
+ this(consumers, null);
+ }
+
+ public AbstractClient(@NonNull List> consumers, @Nullable Consumer streamingErrorHandler) {
+ checkNotNullParam("consumers", consumers);
+ this.consumers = consumers;
+ this.streamingErrorHandler = streamingErrorHandler;
+ }
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The configured client consumers
+ * will be used to handle messages, tasks, and update events received
+ * from the remote agent. The configured streaming error handler will be used
+ * if an error occurs during streaming. The configured client push notification
+ * configuration will get used for streaming.
+ *
+ * @param request the message
+ * @throws A2AClientException if sending the message fails for any reason
+ */
+ public void sendMessage(Message request) throws A2AClientException {
+ sendMessage(request, null);
+ }
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The configured client consumers
+ * will be used to handle messages, tasks, and update events received
+ * from the remote agent. The configured streaming error handler will be used
+ * if an error occurs during streaming. The configured client push notification
+ * configuration will get used for streaming.
+ *
+ * @param request the message
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException if sending the message fails for any reason
+ */
+ public abstract void sendMessage(Message request, @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The specified client consumers
+ * will be used to handle messages, tasks, and update events received
+ * from the remote agent. The specified streaming error handler will be used
+ * if an error occurs during streaming. The configured client push notification
+ * configuration will get used for streaming.
+ *
+ * @param request the message
+ * @param consumers a list of consumers to pass responses from the remote agent to
+ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs
+ * @throws A2AClientException if sending the message fails for any reason
+ */
+ public void sendMessage(Message request,
+ List> consumers,
+ Consumer streamingErrorHandler) throws A2AClientException {
+ sendMessage(request, consumers, streamingErrorHandler, null);
+ }
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The specified client consumers
+ * will be used to handle messages, tasks, and update events received
+ * from the remote agent. The specified streaming error handler will be used
+ * if an error occurs during streaming. The configured client push notification
+ * configuration will get used for streaming.
+ *
+ * @param request the message
+ * @param consumers a list of consumers to pass responses from the remote agent to
+ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException if sending the message fails for any reason
+ */
+ public abstract void sendMessage(Message request,
+ List> consumers,
+ Consumer streamingErrorHandler,
+ @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The configured client consumers
+ * will be used to handle messages, tasks, and update events received from
+ * the remote agent. The configured streaming error handler will be used
+ * if an error occurs during streaming.
+ *
+ * @param request the message
+ * @param pushNotificationConfiguration the push notification configuration that should be
+ * used if the streaming approach is used
+ * @param metadata the optional metadata to include when sending the message
+ * @throws A2AClientException if sending the message fails for any reason
+ */
+ public void sendMessage(Message request, PushNotificationConfig pushNotificationConfiguration,
+ Map metadata) throws A2AClientException {
+ sendMessage(request, pushNotificationConfiguration, metadata, null);
+ }
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The configured client consumers
+ * will be used to handle messages, tasks, and update events received from
+ * the remote agent. The configured streaming error handler will be used
+ * if an error occurs during streaming.
+ *
+ * @param request the message
+ * @param pushNotificationConfiguration the push notification configuration that should be
+ * used if the streaming approach is used
+ * @param metadata the optional metadata to include when sending the message
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException if sending the message fails for any reason
+ */
+ public abstract void sendMessage(Message request, PushNotificationConfig pushNotificationConfiguration,
+ Map metadata, @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Retrieve the current state and history of a specific task.
+ *
+ * @param request the task query parameters specifying which task to retrieve
+ * @return the task
+ * @throws A2AClientException if retrieving the task fails for any reason
+ */
+ public Task getTask(TaskQueryParams request) throws A2AClientException {
+ return getTask(request, null);
+ }
+
+ /**
+ * Retrieve the current state and history of a specific task.
+ *
+ * @param request the task query parameters specifying which task to retrieve
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the task
+ * @throws A2AClientException if retrieving the task fails for any reason
+ */
+ public abstract Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Request the agent to cancel a specific task.
+ *
+ * @param request the task ID parameters specifying which task to cancel
+ * @return the cancelled task
+ * @throws A2AClientException if cancelling the task fails for any reason
+ */
+ public Task cancelTask(TaskIdParams request) throws A2AClientException {
+ return cancelTask(request, null);
+ }
+
+ /**
+ * Request the agent to cancel a specific task.
+ *
+ * @param request the task ID parameters specifying which task to cancel
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the cancelled task
+ * @throws A2AClientException if cancelling the task fails for any reason
+ */
+ public abstract Task cancelTask(TaskIdParams request, @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Set or update the push notification configuration for a specific task.
+ *
+ * @param request the push notification configuration to set for the task
+ * @return the configured TaskPushNotificationConfig
+ * @throws A2AClientException if setting the task push notification configuration fails for any reason
+ */
+ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(
+ TaskPushNotificationConfig request) throws A2AClientException {
+ return setTaskPushNotificationConfiguration(request, null);
+ }
+
+ /**
+ * Set or update the push notification configuration for a specific task.
+ *
+ * @param request the push notification configuration to set for the task
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the configured TaskPushNotificationConfig
+ * @throws A2AClientException if setting the task push notification configuration fails for any reason
+ */
+ public abstract TaskPushNotificationConfig setTaskPushNotificationConfiguration(
+ TaskPushNotificationConfig request,
+ @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Retrieve the push notification configuration for a specific task.
+ *
+ * @param request the parameters specifying which task's notification config to retrieve
+ * @return the task push notification config
+ * @throws A2AClientException if getting the task push notification config fails for any reason
+ */
+ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(
+ GetTaskPushNotificationConfigParams request) throws A2AClientException {
+ return getTaskPushNotificationConfiguration(request, null);
+ }
+
+ /**
+ * Retrieve the push notification configuration for a specific task.
+ *
+ * @param request the parameters specifying which task's notification config to retrieve
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the task push notification config
+ * @throws A2AClientException if getting the task push notification config fails for any reason
+ */
+ public abstract TaskPushNotificationConfig getTaskPushNotificationConfiguration(
+ GetTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Retrieve the list of push notification configurations for a specific task.
+ *
+ * @param request the parameters specifying which task's notification configs to retrieve
+ * @return the list of task push notification configs
+ * @throws A2AClientException if getting the task push notification configs fails for any reason
+ */
+ public List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams request) throws A2AClientException {
+ return listTaskPushNotificationConfigurations(request, null);
+ }
+
+ /**
+ * Retrieve the list of push notification configurations for a specific task.
+ *
+ * @param request the parameters specifying which task's notification configs to retrieve
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the list of task push notification configs
+ * @throws A2AClientException if getting the task push notification configs fails for any reason
+ */
+ public abstract List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Delete the list of push notification configurations for a specific task.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @throws A2AClientException if deleting the task push notification configs fails for any reason
+ */
+ public void deleteTaskPushNotificationConfigurations(
+ DeleteTaskPushNotificationConfigParams request) throws A2AClientException {
+ deleteTaskPushNotificationConfigurations(request, null);
+ }
+
+ /**
+ * Delete the list of push notification configurations for a specific task.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException if deleting the task push notification configs fails for any reason
+ */
+ public abstract void deleteTaskPushNotificationConfigurations(
+ DeleteTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Resubscribe to a task's event stream.
+ * This is only available if both the client and server support streaming.
+ * The configured client consumers will be used to handle messages, tasks,
+ * and update events received from the remote agent. The configured streaming
+ * error handler will be used if an error occurs during streaming.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @throws A2AClientException if resubscribing fails for any reason
+ */
+ public void resubscribe(TaskIdParams request) throws A2AClientException {
+ resubscribe(request, null);
+ }
+
+ /**
+ * Resubscribe to a task's event stream.
+ * This is only available if both the client and server support streaming.
+ * The configured client consumers will be used to handle messages, tasks,
+ * and update events received from the remote agent. The configured streaming
+ * error handler will be used if an error occurs during streaming.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException if resubscribing fails for any reason
+ */
+ public abstract void resubscribe(TaskIdParams request, @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Resubscribe to a task's event stream.
+ * This is only available if both the client and server support streaming.
+ * The specified client consumers will be used to handle messages, tasks, and
+ * update events received from the remote agent. The specified streaming error
+ * handler will be used if an error occurs during streaming.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @param consumers a list of consumers to pass responses from the remote agent to
+ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs
+ * @throws A2AClientException if resubscribing fails for any reason
+ */
+ public void resubscribe(TaskIdParams request, List> consumers,
+ Consumer streamingErrorHandler) throws A2AClientException {
+ resubscribe(request, consumers, streamingErrorHandler, null);
+ }
+
+ /**
+ * Resubscribe to a task's event stream.
+ * This is only available if both the client and server support streaming.
+ * The specified client consumers will be used to handle messages, tasks, and
+ * update events received from the remote agent. The specified streaming error
+ * handler will be used if an error occurs during streaming.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @param consumers a list of consumers to pass responses from the remote agent to
+ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException if resubscribing fails for any reason
+ */
+ public abstract void resubscribe(TaskIdParams request, List> consumers,
+ Consumer streamingErrorHandler, @Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Retrieve the AgentCard.
+ *
+ * @return the AgentCard
+ * @throws A2AClientException if retrieving the agent card fails for any reason
+ */
+ public AgentCard getAgentCard() throws A2AClientException {
+ return getAgentCard(null);
+ }
+
+ /**
+ * Retrieve the AgentCard.
+ *
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the AgentCard
+ * @throws A2AClientException if retrieving the agent card fails for any reason
+ */
+ public abstract AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2AClientException;
+
+ /**
+ * Close the transport and release any associated resources.
+ */
+ public abstract void close();
+
+ /**
+ * Process the event using all configured consumers.
+ */
+ void consume(ClientEvent clientEventOrMessage, AgentCard agentCard) {
+ for (BiConsumer consumer : consumers) {
+ consumer.accept(clientEventOrMessage, agentCard);
+ }
+ }
+
+ /**
+ * Get the error handler that should be used during streaming.
+ *
+ * @return the streaming error handler
+ */
+ public @Nullable Consumer getStreamingErrorHandler() {
+ return streamingErrorHandler;
+ }
+
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client.java
new file mode 100644
index 000000000..2623527ee
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client.java
@@ -0,0 +1,243 @@
+package org.a2aproject.sdk.compat03.client;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.compat03.client.config.ClientConfig;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.compat03.spec.A2AClientError;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.A2AClientInvalidStateError;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.EventKind;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.Message;
+import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams;
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind;
+import org.a2aproject.sdk.compat03.spec.Task;
+import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams;
+import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+public class Client extends AbstractClient {
+
+ private final ClientConfig clientConfig;
+ private final ClientTransport clientTransport;
+ private AgentCard agentCard;
+
+ Client(AgentCard agentCard, ClientConfig clientConfig, ClientTransport clientTransport,
+ List> consumers, @Nullable Consumer streamingErrorHandler) {
+ super(consumers, streamingErrorHandler);
+ checkNotNullParam("agentCard", agentCard);
+
+ this.agentCard = agentCard;
+ this.clientConfig = clientConfig;
+ this.clientTransport = clientTransport;
+ }
+
+ public static ClientBuilder builder(AgentCard agentCard) {
+ return new ClientBuilder(agentCard);
+ }
+
+ @Override
+ public void sendMessage(Message request, @Nullable ClientCallContext context) throws A2AClientException {
+ MessageSendParams messageSendParams = getMessageSendParams(request, clientConfig);
+ sendMessage(messageSendParams, null, null, context);
+ }
+
+ @Override
+ public void sendMessage(Message request, List> consumers,
+ Consumer streamingErrorHandler, @Nullable ClientCallContext context) throws A2AClientException {
+ MessageSendParams messageSendParams = getMessageSendParams(request, clientConfig);
+ sendMessage(messageSendParams, consumers, streamingErrorHandler, context);
+ }
+
+ @Override
+ public void sendMessage(Message request, PushNotificationConfig pushNotificationConfiguration,
+ Map metatadata, @Nullable ClientCallContext context) throws A2AClientException {
+ MessageSendConfiguration messageSendConfiguration = createMessageSendConfiguration(pushNotificationConfiguration);
+
+ MessageSendParams messageSendParams = new MessageSendParams.Builder()
+ .message(request)
+ .configuration(messageSendConfiguration)
+ .metadata(metatadata)
+ .build();
+
+ sendMessage(messageSendParams, null, null, context);
+ }
+
+ @Override
+ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ return clientTransport.getTask(request, context);
+ }
+
+ @Override
+ public Task cancelTask(TaskIdParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ return clientTransport.cancelTask(request, context);
+ }
+
+ @Override
+ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(
+ TaskPushNotificationConfig request, @Nullable ClientCallContext context) throws A2AClientException {
+ return clientTransport.setTaskPushNotificationConfiguration(request, context);
+ }
+
+ @Override
+ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(
+ GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ return clientTransport.getTaskPushNotificationConfiguration(request, context);
+ }
+
+ @Override
+ public List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ return clientTransport.listTaskPushNotificationConfigurations(request, context);
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(
+ DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ clientTransport.deleteTaskPushNotificationConfigurations(request, context);
+ }
+
+ @Override
+ public void resubscribe(TaskIdParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ resubscribeToTask(request, null, null, context);
+ }
+
+ @Override
+ public void resubscribe(TaskIdParams request, @Nullable List> consumers,
+ @Nullable Consumer streamingErrorHandler, @Nullable ClientCallContext context) throws A2AClientException {
+ resubscribeToTask(request, consumers, streamingErrorHandler, context);
+ }
+
+ @Override
+ public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
+ agentCard = clientTransport.getAgentCard(context);
+ return agentCard;
+ }
+
+ @Override
+ public void close() {
+ clientTransport.close();
+ }
+
+ private ClientEvent getClientEvent(StreamingEventKind event, ClientTaskManager taskManager) throws A2AClientError {
+ if (event instanceof Message message) {
+ return new MessageEvent(message);
+ } else if (event instanceof Task task) {
+ taskManager.saveTaskEvent(task);
+ return new TaskEvent(taskManager.getCurrentTask());
+ } else if (event instanceof TaskStatusUpdateEvent updateEvent) {
+ taskManager.saveTaskEvent(updateEvent);
+ return new TaskUpdateEvent(taskManager.getCurrentTask(), updateEvent);
+ } else if (event instanceof TaskArtifactUpdateEvent updateEvent) {
+ taskManager.saveTaskEvent(updateEvent);
+ return new TaskUpdateEvent(taskManager.getCurrentTask(), updateEvent);
+ } else {
+ throw new A2AClientInvalidStateError("Invalid client event");
+ }
+ }
+
+ private MessageSendConfiguration createMessageSendConfiguration(@Nullable PushNotificationConfig pushNotificationConfig) {
+ return new MessageSendConfiguration.Builder()
+ .acceptedOutputModes(clientConfig.getAcceptedOutputModes())
+ .blocking(!clientConfig.isPolling())
+ .historyLength(clientConfig.getHistoryLength())
+ .pushNotificationConfig(pushNotificationConfig)
+ .build();
+ }
+
+ private void sendMessage(MessageSendParams messageSendParams, @Nullable List> consumers,
+ @Nullable Consumer errorHandler, @Nullable ClientCallContext context) throws A2AClientException {
+ if (! clientConfig.isStreaming() || ! agentCard.capabilities().streaming()) {
+ EventKind eventKind = clientTransport.sendMessage(messageSendParams, context);
+ ClientEvent clientEvent;
+ if (eventKind instanceof Task task) {
+ clientEvent = new TaskEvent(task);
+ } else {
+ // must be a message
+ clientEvent = new MessageEvent((Message) eventKind);
+ }
+ consume(clientEvent, agentCard, consumers);
+ } else {
+ ClientTaskManager tracker = new ClientTaskManager();
+ Consumer overriddenErrorHandler = getOverriddenErrorHandler(errorHandler);
+ Consumer eventHandler = event -> {
+ try {
+ ClientEvent clientEvent = getClientEvent(event, tracker);
+ consume(clientEvent, agentCard, consumers);
+ } catch (A2AClientError e) {
+ overriddenErrorHandler.accept(e);
+ }
+ };
+ clientTransport.sendMessageStreaming(messageSendParams, eventHandler, overriddenErrorHandler, context);
+ }
+ }
+
+ private void resubscribeToTask(TaskIdParams request, @Nullable List> consumers,
+ @Nullable Consumer errorHandler, @Nullable ClientCallContext context) throws A2AClientException {
+ if (! clientConfig.isStreaming() || ! agentCard.capabilities().streaming()) {
+ throw new A2AClientException("Client and/or server does not support resubscription");
+ }
+ ClientTaskManager tracker = new ClientTaskManager();
+ Consumer overriddenErrorHandler = getOverriddenErrorHandler(errorHandler);
+ Consumer eventHandler = event -> {
+ try {
+ ClientEvent clientEvent = getClientEvent(event, tracker);
+ consume(clientEvent, agentCard, consumers);
+ } catch (A2AClientError e) {
+ overriddenErrorHandler.accept(e);
+ }
+ };
+ clientTransport.resubscribe(request, eventHandler, overriddenErrorHandler, context);
+ }
+
+ private @NonNull Consumer getOverriddenErrorHandler(@Nullable Consumer errorHandler) {
+ return e -> {
+ if (errorHandler != null) {
+ errorHandler.accept(e);
+ } else {
+ if (getStreamingErrorHandler() != null) {
+ getStreamingErrorHandler().accept(e);
+ }
+ }
+ };
+ }
+
+ private void consume(ClientEvent clientEvent, AgentCard agentCard, @Nullable List> consumers) {
+ if (consumers != null) {
+ // use specified consumers
+ for (BiConsumer consumer : consumers) {
+ consumer.accept(clientEvent, agentCard);
+ }
+ } else {
+ // use configured consumers
+ consume(clientEvent, agentCard);
+ }
+ }
+
+ private MessageSendParams getMessageSendParams(Message request, ClientConfig clientConfig) {
+ MessageSendConfiguration messageSendConfiguration = createMessageSendConfiguration(clientConfig.getPushNotificationConfig());
+
+ return new MessageSendParams.Builder()
+ .message(request)
+ .configuration(messageSendConfiguration)
+ .metadata(clientConfig.getMetadata())
+ .build();
+ }
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder.java
new file mode 100644
index 000000000..24443bbb8
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder.java
@@ -0,0 +1,169 @@
+package org.a2aproject.sdk.compat03.client;
+
+import org.a2aproject.sdk.compat03.client.config.ClientConfig;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.AgentInterface;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+public class ClientBuilder {
+
+ private static final Map>> transportProviderRegistry = new HashMap<>();
+ private static final Map, String> transportProtocolMapping = new HashMap<>();
+
+ static {
+ ServiceLoader loader = ServiceLoader.load(ClientTransportProvider.class);
+ for (ClientTransportProvider, ?> transport : loader) {
+ transportProviderRegistry.put(transport.getTransportProtocol(), transport);
+ transportProtocolMapping.put(transport.getTransportProtocolClass(), transport.getTransportProtocol());
+ }
+ }
+
+ private final AgentCard agentCard;
+
+ private final List> consumers = new ArrayList<>();
+ private @Nullable Consumer streamErrorHandler;
+ private ClientConfig clientConfig = new ClientConfig.Builder().build();
+
+ private final Map, ClientTransportConfig extends ClientTransport>> clientTransports = new LinkedHashMap<>();
+
+ ClientBuilder(@NonNull AgentCard agentCard) {
+ this.agentCard = agentCard;
+ }
+
+ public ClientBuilder withTransport(Class clazz, ClientTransportConfigBuilder extends ClientTransportConfig, ?> configBuilder) {
+ return withTransport(clazz, configBuilder.build());
+ }
+
+ public ClientBuilder withTransport(Class clazz, ClientTransportConfig config) {
+ clientTransports.put(clazz, config);
+
+ return this;
+ }
+
+ public ClientBuilder addConsumer(BiConsumer consumer) {
+ this.consumers.add(consumer);
+ return this;
+ }
+
+ public ClientBuilder addConsumers(List> consumers) {
+ this.consumers.addAll(consumers);
+ return this;
+ }
+
+ public ClientBuilder streamingErrorHandler(Consumer streamErrorHandler) {
+ this.streamErrorHandler = streamErrorHandler;
+ return this;
+ }
+
+ public ClientBuilder clientConfig(@NonNull ClientConfig clientConfig) {
+ this.clientConfig = clientConfig;
+ return this;
+ }
+
+ public Client build() throws A2AClientException {
+ if (this.clientConfig == null) {
+ this.clientConfig = new ClientConfig.Builder().build();
+ }
+
+ ClientTransport clientTransport = buildClientTransport();
+
+ return new Client(agentCard, clientConfig, clientTransport, consumers, streamErrorHandler);
+ }
+
+ @SuppressWarnings("unchecked")
+ 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.transport());
+ if (clientTransportProvider == null) {
+ throw new A2AClientException("No client available for " + agentInterface.transport());
+ }
+ 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.transport());
+ }
+
+ return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url());
+ }
+
+ private Map getServerPreferredTransports() {
+ Map serverPreferredTransports = new LinkedHashMap<>();
+ serverPreferredTransports.put(agentCard.preferredTransport(), agentCard.url());
+ if (agentCard.additionalInterfaces() != null) {
+ for (AgentInterface agentInterface : agentCard.additionalInterfaces()) {
+ serverPreferredTransports.putIfAbsent(agentInterface.transport(), agentInterface.url());
+ }
+ }
+ return serverPreferredTransports;
+ }
+
+ private List getClientPreferredTransports() {
+ List supportedClientTransports = new ArrayList<>();
+
+ if (clientTransports.isEmpty()) {
+ // default to JSONRPC if not specified
+ supportedClientTransports.add(TransportProtocol.JSONRPC.asString());
+ } else {
+ clientTransports.forEach((aClass, clientTransportConfig) -> supportedClientTransports.add(transportProtocolMapping.get(aClass)));
+ }
+ return supportedClientTransports;
+ }
+
+ private AgentInterface findBestClientTransport() throws A2AClientException {
+ // Retrieve transport supported by the A2A server
+ Map serverPreferredTransports = getServerPreferredTransports();
+
+ // Retrieve transport configured for this client (using withTransport methods)
+ List clientPreferredTransports = getClientPreferredTransports();
+
+ String transportProtocol = null;
+ String transportUrl = null;
+ if (clientConfig.isUseClientPreference()) {
+ for (String clientPreferredTransport : clientPreferredTransports) {
+ if (serverPreferredTransports.containsKey(clientPreferredTransport)) {
+ transportProtocol = clientPreferredTransport;
+ transportUrl = serverPreferredTransports.get(transportProtocol);
+ break;
+ }
+ }
+ } else {
+ for (Map.Entry transport : serverPreferredTransports.entrySet()) {
+ if (clientPreferredTransports.contains(transport.getKey())) {
+ transportProtocol = transport.getKey();
+ transportUrl = transport.getValue();
+ break;
+ }
+ }
+ }
+ if (transportProtocol == null || transportUrl == null) {
+ throw new A2AClientException("No compatible transport found");
+ }
+ if (! transportProviderRegistry.containsKey(transportProtocol)) {
+ throw new A2AClientException("No client available for " + transportProtocol);
+ }
+
+ return new AgentInterface(transportProtocol, transportUrl);
+ }
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent.java
new file mode 100644
index 000000000..ffaf18cd6
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent.java
@@ -0,0 +1,4 @@
+package org.a2aproject.sdk.compat03.client;
+
+public sealed interface ClientEvent permits MessageEvent, TaskEvent, TaskUpdateEvent {
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager.java
new file mode 100644
index 000000000..b3604b53d
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager.java
@@ -0,0 +1,139 @@
+package org.a2aproject.sdk.compat03.client;
+
+import static org.a2aproject.sdk.compat03.util.Utils.appendArtifactToTask;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientError;
+import org.a2aproject.sdk.compat03.spec.A2AClientInvalidArgsError;
+import org.a2aproject.sdk.compat03.spec.A2AClientInvalidStateError;
+import org.a2aproject.sdk.compat03.spec.Message;
+import org.a2aproject.sdk.compat03.spec.Task;
+import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent;
+import org.a2aproject.sdk.compat03.spec.TaskState;
+import org.a2aproject.sdk.compat03.spec.TaskStatus;
+import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Helps manage a task's lifecycle during the execution of a request.
+ * Responsible for retrieving, saving, and updating the task based on
+ * events received from the agent.
+ */
+public class ClientTaskManager {
+
+ private @Nullable Task currentTask;
+ private @Nullable String taskId;
+ private @Nullable String contextId;
+
+ public ClientTaskManager() {
+ this.currentTask = null;
+ this.taskId = null;
+ this.contextId = null;
+ }
+
+ public Task getCurrentTask() throws A2AClientInvalidStateError {
+ if (currentTask == null) {
+ throw new A2AClientInvalidStateError("No current task");
+ }
+ return currentTask;
+ }
+
+ public Task saveTaskEvent(Task task) throws A2AClientInvalidArgsError {
+ if (currentTask != null) {
+ throw new A2AClientInvalidArgsError("Task is already set, create new manager for new tasks.");
+ }
+ saveTask(task);
+ return task;
+ }
+
+ public Task saveTaskEvent(TaskStatusUpdateEvent taskStatusUpdateEvent) throws A2AClientError {
+ if (taskId == null) {
+ taskId = taskStatusUpdateEvent.getTaskId();
+ }
+ if (contextId == null) {
+ contextId = taskStatusUpdateEvent.getContextId();
+ }
+ Task task = currentTask;
+ if (task == null) {
+ task = new Task.Builder()
+ .status(new TaskStatus(TaskState.UNKNOWN))
+ .id(taskId)
+ .contextId(contextId == null ? "" : contextId)
+ .build();
+ }
+
+ Task.Builder taskBuilder = new Task.Builder(task);
+ if (taskStatusUpdateEvent.getStatus().message() != null) {
+ if (task.getHistory() == null) {
+ taskBuilder.history(taskStatusUpdateEvent.getStatus().message());
+ } else {
+ List history = new ArrayList<>(task.getHistory());
+ history.add(taskStatusUpdateEvent.getStatus().message());
+ taskBuilder.history(history);
+ }
+ }
+ if (taskStatusUpdateEvent.getMetadata() != null) {
+ Map newMetadata = task.getMetadata() != null ? new HashMap<>(task.getMetadata()) : new HashMap<>();
+ newMetadata.putAll(taskStatusUpdateEvent.getMetadata());
+ taskBuilder.metadata(newMetadata);
+ }
+ taskBuilder.status(taskStatusUpdateEvent.getStatus());
+ currentTask = taskBuilder.build();
+ return currentTask;
+ }
+
+ public Task saveTaskEvent(TaskArtifactUpdateEvent taskArtifactUpdateEvent) {
+ if (taskId == null) {
+ taskId = taskArtifactUpdateEvent.getTaskId();
+ }
+ if (contextId == null) {
+ contextId = taskArtifactUpdateEvent.getContextId();
+ }
+ Task task = currentTask;
+ if (task == null) {
+ task = new Task.Builder()
+ .status(new TaskStatus(TaskState.UNKNOWN))
+ .id(taskId)
+ .contextId(contextId == null ? "" : contextId)
+ .build();
+ }
+ currentTask = appendArtifactToTask(task, taskArtifactUpdateEvent, taskId);
+ return currentTask;
+ }
+
+ /**
+ * Update a task by adding a message to its history. If the task has a message in its current status,
+ * that message is moved to the history first.
+ *
+ * @param message the new message to add to the history
+ * @param task the task to update
+ * @return the updated task
+ */
+ public Task updateWithMessage(Message message, Task task) {
+ Task.Builder taskBuilder = new Task.Builder(task);
+ List history = task.getHistory();
+ if (history == null) {
+ history = new ArrayList<>();
+ }
+ if (task.getStatus().message() != null) {
+ history.add(task.getStatus().message());
+ taskBuilder.status(new TaskStatus(task.getStatus().state(), null, task.getStatus().timestamp()));
+ }
+ history.add(message);
+ taskBuilder.history(history);
+ currentTask = taskBuilder.build();
+ return currentTask;
+ }
+
+ private void saveTask(Task task) {
+ currentTask = task;
+ if (taskId == null) {
+ taskId = currentTask.getId();
+ contextId = currentTask.getContextId();
+ }
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent.java
new file mode 100644
index 000000000..0c94bc95f
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent.java
@@ -0,0 +1,26 @@
+package org.a2aproject.sdk.compat03.client;
+
+import org.a2aproject.sdk.compat03.spec.Message;
+
+/**
+ * A message event received by a client.
+ */
+public final class MessageEvent implements ClientEvent {
+
+ private final Message message;
+
+ /**
+ * A message event.
+ *
+ * @param message the message received
+ */
+ public MessageEvent(Message message) {
+ this.message = message;
+ }
+
+ public Message getMessage() {
+ return message;
+ }
+}
+
+
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskEvent.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskEvent.java
new file mode 100644
index 000000000..1406ad619
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskEvent.java
@@ -0,0 +1,27 @@
+package org.a2aproject.sdk.compat03.client;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import org.a2aproject.sdk.compat03.spec.Task;
+
+/**
+ * A task event received by a client.
+ */
+public final class TaskEvent implements ClientEvent {
+
+ private final Task task;
+
+ /**
+ * A client task event.
+ *
+ * @param task the task received
+ */
+ public TaskEvent(Task task) {
+ checkNotNullParam("task", task);
+ this.task = task;
+ }
+
+ public Task getTask() {
+ return task;
+ }
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent.java
new file mode 100644
index 000000000..1cd0aff23
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent.java
@@ -0,0 +1,37 @@
+package org.a2aproject.sdk.compat03.client;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import org.a2aproject.sdk.compat03.spec.Task;
+import org.a2aproject.sdk.compat03.spec.UpdateEvent;
+
+/**
+ * A task update event received by a client.
+ */
+public final class TaskUpdateEvent implements ClientEvent {
+
+ private final Task task;
+ private final UpdateEvent updateEvent;
+
+ /**
+ * A task update event.
+ *
+ * @param task the current task
+ * @param updateEvent the update event received for the current task
+ */
+ public TaskUpdateEvent(Task task, UpdateEvent updateEvent) {
+ checkNotNullParam("task", task);
+ checkNotNullParam("updateEvent", updateEvent);
+ this.task = task;
+ this.updateEvent = updateEvent;
+ }
+
+ public Task getTask() {
+ return task;
+ }
+
+ public UpdateEvent getUpdateEvent() {
+ return updateEvent;
+ }
+
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig.java
new file mode 100644
index 000000000..20dd1c991
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig.java
@@ -0,0 +1,114 @@
+package org.a2aproject.sdk.compat03.client.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig;
+import java.util.ArrayList;
+import java.util.HashMap;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Configuration for the A2A client factory.
+ */
+public class ClientConfig {
+
+ private final Boolean streaming;
+ private final Boolean polling;
+ private final Boolean useClientPreference;
+ private final List acceptedOutputModes;
+ private final @Nullable PushNotificationConfig pushNotificationConfig;
+ private final @Nullable Integer historyLength;
+ private final Map metadata;
+
+ private ClientConfig(Builder builder) {
+ this.streaming = builder.streaming == null ? true : builder.streaming;
+ this.polling = builder.polling == null ? false : builder.polling;
+ this.useClientPreference = builder.useClientPreference == null ? false : builder.useClientPreference;
+ this.acceptedOutputModes = builder.acceptedOutputModes;
+ this.pushNotificationConfig = builder.pushNotificationConfig;
+ this.historyLength = builder.historyLength;
+ this.metadata = builder.metadata;
+ }
+
+ public boolean isStreaming() {
+ return streaming;
+ }
+
+ public boolean isPolling() {
+ return polling;
+ }
+
+ public boolean isUseClientPreference() {
+ return useClientPreference;
+ }
+
+ public List getAcceptedOutputModes() {
+ return acceptedOutputModes;
+ }
+
+ public @Nullable PushNotificationConfig getPushNotificationConfig() {
+ return pushNotificationConfig;
+ }
+
+ public @Nullable Integer getHistoryLength() {
+ return historyLength;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private @Nullable Boolean streaming;
+ private @Nullable Boolean polling;
+ private @Nullable Boolean useClientPreference;
+ private List acceptedOutputModes = new ArrayList<>();
+ private @Nullable PushNotificationConfig pushNotificationConfig;
+ private @Nullable Integer historyLength;
+ private Map metadata = new HashMap<>();
+
+ public Builder setStreaming(@Nullable Boolean streaming) {
+ this.streaming = streaming;
+ return this;
+ }
+
+ public Builder setPolling(@Nullable Boolean polling) {
+ this.polling = polling;
+ return this;
+ }
+
+ public Builder setUseClientPreference(@Nullable Boolean useClientPreference) {
+ this.useClientPreference = useClientPreference;
+ return this;
+ }
+
+ public Builder setAcceptedOutputModes(List acceptedOutputModes) {
+ this.acceptedOutputModes = new ArrayList<>(acceptedOutputModes);
+ return this;
+ }
+
+ public Builder setPushNotificationConfig(PushNotificationConfig pushNotificationConfig) {
+ this.pushNotificationConfig = pushNotificationConfig;
+ return this;
+ }
+
+ public Builder setHistoryLength(Integer historyLength) {
+ this.historyLength = historyLength;
+ return this;
+ }
+
+ public Builder setMetadata(Map metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ public ClientConfig build() {
+ return new ClientConfig(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java
new file mode 100644
index 000000000..bfae93f4b
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java
@@ -0,0 +1,5 @@
+@NullMarked
+package org.a2aproject.sdk.compat03.client.config;
+
+import org.jspecify.annotations.NullMarked;
+
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java
new file mode 100644
index 000000000..9bd22f637
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java
@@ -0,0 +1,5 @@
+@NullMarked
+package org.a2aproject.sdk.compat03.client;
+
+import org.jspecify.annotations.NullMarked;
+
diff --git a/compat-0.3/client/base/src/main/resources/META-INF/beans.xml b/compat-0.3/client/base/src/main/resources/META-INF/beans.xml
new file mode 100644
index 000000000..e69de29bb
diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2ATest.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2ATest.java
new file mode 100644
index 000000000..76c93df96
--- /dev/null
+++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2ATest.java
@@ -0,0 +1,147 @@
+package org.a2aproject.sdk.compat03;
+
+import org.a2aproject.sdk.compat03.spec.Message;
+import org.a2aproject.sdk.compat03.spec.Part;
+import org.a2aproject.sdk.compat03.spec.TextPart;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class A2ATest {
+
+ @Test
+ public void testToUserMessage() {
+ String text = "Hello, world!";
+ Message message = A2A.toUserMessage(text);
+
+ assertEquals(Message.Role.USER, message.getRole());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart) message.getParts().get(0)).getText());
+ assertNotNull(message.getMessageId());
+ assertNull(message.getContextId());
+ assertNull(message.getTaskId());
+ }
+
+ @Test
+ public void testToUserMessageWithId() {
+ String text = "Hello, world!";
+ String messageId = "test-message-id";
+ Message message = A2A.toUserMessage(text, messageId);
+
+ assertEquals(Message.Role.USER, message.getRole());
+ assertEquals(messageId, message.getMessageId());
+ }
+
+ @Test
+ public void testToAgentMessage() {
+ String text = "Hello, I'm an agent!";
+ Message message = A2A.toAgentMessage(text);
+
+ assertEquals(Message.Role.AGENT, message.getRole());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart) message.getParts().get(0)).getText());
+ assertNotNull(message.getMessageId());
+ }
+
+ @Test
+ public void testToAgentMessageWithId() {
+ String text = "Hello, I'm an agent!";
+ String messageId = "agent-message-id";
+ Message message = A2A.toAgentMessage(text, messageId);
+
+ assertEquals(Message.Role.AGENT, message.getRole());
+ assertEquals(messageId, message.getMessageId());
+ }
+
+ @Test
+ public void testCreateUserTextMessage() {
+ String text = "User message with context";
+ String contextId = "context-123";
+ String taskId = "task-456";
+
+ Message message = A2A.createUserTextMessage(text, contextId, taskId);
+
+ assertEquals(Message.Role.USER, message.getRole());
+ assertEquals(contextId, message.getContextId());
+ assertEquals(taskId, message.getTaskId());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart) message.getParts().get(0)).getText());
+ assertNotNull(message.getMessageId());
+ assertNull(message.getMetadata());
+ assertNull(message.getReferenceTaskIds());
+ }
+
+ @Test
+ public void testCreateUserTextMessageWithNullParams() {
+ String text = "Simple user message";
+
+ Message message = A2A.createUserTextMessage(text, null, null);
+
+ assertEquals(Message.Role.USER, message.getRole());
+ assertNull(message.getContextId());
+ assertNull(message.getTaskId());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart) message.getParts().get(0)).getText());
+ }
+
+ @Test
+ public void testCreateAgentTextMessage() {
+ String text = "Agent message with context";
+ String contextId = "context-789";
+ String taskId = "task-012";
+
+ Message message = A2A.createAgentTextMessage(text, contextId, taskId);
+
+ assertEquals(Message.Role.AGENT, message.getRole());
+ assertEquals(contextId, message.getContextId());
+ assertEquals(taskId, message.getTaskId());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart) message.getParts().get(0)).getText());
+ assertNotNull(message.getMessageId());
+ }
+
+ @Test
+ public void testCreateAgentPartsMessage() {
+ List> parts = Arrays.asList(
+ new TextPart("Part 1"),
+ new TextPart("Part 2")
+ );
+ String contextId = "context-parts";
+ String taskId = "task-parts";
+
+ Message message = A2A.createAgentPartsMessage(parts, contextId, taskId);
+
+ assertEquals(Message.Role.AGENT, message.getRole());
+ assertEquals(contextId, message.getContextId());
+ assertEquals(taskId, message.getTaskId());
+ assertEquals(2, message.getParts().size());
+ assertEquals("Part 1", ((TextPart) message.getParts().get(0)).getText());
+ assertEquals("Part 2", ((TextPart) message.getParts().get(1)).getText());
+ }
+
+ @Test
+ public void testCreateAgentPartsMessageWithNullParts() {
+ try {
+ A2A.createAgentPartsMessage(null, "context", "task");
+ org.junit.jupiter.api.Assertions.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Parts cannot be null or empty", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCreateAgentPartsMessageWithEmptyParts() {
+ try {
+ A2A.createAgentPartsMessage(Collections.emptyList(), "context", "task");
+ org.junit.jupiter.api.Assertions.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Parts cannot be null or empty", e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorizationTest.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorizationTest.java
new file mode 100644
index 000000000..2d1764374
--- /dev/null
+++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorizationTest.java
@@ -0,0 +1,380 @@
+package org.a2aproject.sdk.compat03.client;
+
+import org.a2aproject.sdk.compat03.client.config.ClientConfig;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder;
+import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport;
+import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc;
+import org.a2aproject.sdk.compat03.grpc.SendMessageRequest;
+import org.a2aproject.sdk.compat03.grpc.SendMessageResponse;
+import org.a2aproject.sdk.compat03.grpc.StreamResponse;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCapabilities;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.AgentInterface;
+import org.a2aproject.sdk.compat03.spec.AgentSkill;
+import org.a2aproject.sdk.compat03.spec.Message;
+import org.a2aproject.sdk.compat03.spec.TextPart;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol;
+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.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+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;
+
+/**
+ * Tests for handling HTTP 401 (Unauthorized) and 403 (Forbidden) responses
+ * when the client sends streaming and non-streaming messages.
+ *
+ * These tests verify that the client properly fails when the server returns
+ * authentication or authorization errors.
+ */
+public class AuthenticationAuthorizationTest {
+
+ private static final String AGENT_URL = "http://localhost:4001";
+ private static final String AUTHENTICATION_FAILED_MESSAGE = "Authentication failed";
+ private static final String AUTHORIZATION_FAILED_MESSAGE = "Authorization failed";
+
+ private ClientAndServer server;
+ private Message MESSAGE;
+ private AgentCard agentCard;
+ private Server grpcServer;
+ private ManagedChannel grpcChannel;
+ private String grpcServerName;
+
+ @BeforeEach
+ public void setUp() {
+ server = new ClientAndServer(4001);
+ MESSAGE = new Message.Builder()
+ .role(Message.Role.USER)
+ .parts(Collections.singletonList(new TextPart("test message")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+
+ grpcServerName = InProcessServerBuilder.generateName();
+
+ agentCard = new AgentCard.Builder()
+ .name("Test Agent")
+ .description("Test agent for auth tests")
+ .url(AGENT_URL)
+ .version("1.0.0")
+ .capabilities(new AgentCapabilities.Builder()
+ .streaming(true) // Support streaming for all tests
+ .build())
+ .defaultInputModes(Collections.singletonList("text"))
+ .defaultOutputModes(Collections.singletonList("text"))
+ .skills(Collections.singletonList(new AgentSkill.Builder()
+ .id("test_skill")
+ .name("Test skill")
+ .description("Test skill")
+ .tags(Collections.singletonList("test"))
+ .build()))
+ .protocolVersion("0.3.0")
+ .additionalInterfaces(java.util.Arrays.asList(
+ new AgentInterface(TransportProtocol.JSONRPC.asString(), AGENT_URL),
+ new AgentInterface(TransportProtocol.HTTP_JSON.asString(), AGENT_URL),
+ new AgentInterface(TransportProtocol.GRPC.asString(), grpcServerName)))
+ .build();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ server.stop();
+ if (grpcChannel != null) {
+ grpcChannel.shutdownNow();
+ }
+ if (grpcServer != null) {
+ grpcServer.shutdownNow();
+ }
+ }
+
+ // ========== JSON-RPC Transport Tests ==========
+
+ @Test
+ public void testJsonRpcNonStreamingUnauthenticated() throws A2AClientException {
+ // Mock server to return 401 for non-streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ ).respond(
+ response()
+ .withStatusCode(401)
+ );
+
+ Client client = getJSONRPCClientBuilder(false).build();
+
+ A2AClientException exception = assertThrows(A2AClientException.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testJsonRpcNonStreamingUnauthorized() throws A2AClientException {
+ // Mock server to return 403 for non-streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ ).respond(
+ response()
+ .withStatusCode(403)
+ );
+
+ Client client = getJSONRPCClientBuilder(false).build();
+
+ A2AClientException exception = assertThrows(A2AClientException.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testJsonRpcStreamingUnauthenticated() throws Exception {
+ // Mock server to return 401 for streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ ).respond(
+ response()
+ .withStatusCode(401)
+ );
+
+ assertStreamingError(
+ getJSONRPCClientBuilder(true),
+ AUTHENTICATION_FAILED_MESSAGE);
+ }
+
+ @Test
+ public void testJsonRpcStreamingUnauthorized() throws Exception {
+ // Mock server to return 403 for streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ ).respond(
+ response()
+ .withStatusCode(403)
+ );
+
+ assertStreamingError(
+ getJSONRPCClientBuilder(true),
+ AUTHORIZATION_FAILED_MESSAGE);
+ }
+
+ // ========== REST Transport Tests ==========
+
+ @Test
+ public void testRestNonStreamingUnauthenticated() throws A2AClientException {
+ // Mock server to return 401 for non-streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/v1/message:send")
+ ).respond(
+ response()
+ .withStatusCode(401)
+ );
+
+ Client client = getRestClientBuilder(false).build();
+
+ A2AClientException exception = assertThrows(A2AClientException.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testRestNonStreamingUnauthorized() throws A2AClientException {
+ // Mock server to return 403 for non-streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/v1/message:send")
+ ).respond(
+ response()
+ .withStatusCode(403)
+ );
+
+ Client client = getRestClientBuilder(false).build();
+
+ A2AClientException exception = assertThrows(A2AClientException.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testRestStreamingUnauthenticated() throws Exception {
+ // Mock server to return 401 for streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/v1/message:stream")
+ ).respond(
+ response()
+ .withStatusCode(401)
+ );
+
+ assertStreamingError(
+ getRestClientBuilder(true),
+ AUTHENTICATION_FAILED_MESSAGE);
+ }
+
+ @Test
+ public void testRestStreamingUnauthorized() throws Exception {
+ // Mock server to return 403 for streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/v1/message:stream")
+ ).respond(
+ response()
+ .withStatusCode(403)
+ );
+
+ assertStreamingError(
+ getRestClientBuilder(true),
+ AUTHORIZATION_FAILED_MESSAGE);
+ }
+
+ // ========== gRPC Transport Tests ==========
+
+ @Test
+ public void testGrpcNonStreamingUnauthenticated() throws Exception {
+ setupGrpcServer(Status.UNAUTHENTICATED);
+
+ Client client = getGrpcClientBuilder(false).build();
+
+ A2AClientException exception = assertThrows(A2AClientException.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testGrpcNonStreamingUnauthorized() throws Exception {
+ setupGrpcServer(Status.PERMISSION_DENIED);
+
+ Client client = getGrpcClientBuilder(false).build();
+
+ A2AClientException exception = assertThrows(A2AClientException.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testGrpcStreamingUnauthenticated() throws Exception {
+ setupGrpcServer(Status.UNAUTHENTICATED);
+
+ assertStreamingError(
+ getGrpcClientBuilder(true),
+ AUTHENTICATION_FAILED_MESSAGE);
+ }
+
+ @Test
+ public void testGrpcStreamingUnauthorized() throws Exception {
+ setupGrpcServer(Status.PERMISSION_DENIED);
+
+ assertStreamingError(
+ getGrpcClientBuilder(true),
+ AUTHORIZATION_FAILED_MESSAGE);
+ }
+
+ private ClientBuilder getJSONRPCClientBuilder(boolean streaming) {
+ return Client.builder(agentCard)
+ .clientConfig(new ClientConfig.Builder().setStreaming(streaming).build())
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
+ }
+
+ private ClientBuilder getRestClientBuilder(boolean streaming) {
+ return Client.builder(agentCard)
+ .clientConfig(new ClientConfig.Builder().setStreaming(streaming).build())
+ .withTransport(RestTransport.class, new RestTransportConfigBuilder());
+ }
+
+ private ClientBuilder getGrpcClientBuilder(boolean streaming) {
+ return Client.builder(agentCard)
+ .clientConfig(new ClientConfig.Builder().setStreaming(streaming).build())
+ .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder()
+ .channelFactory(target -> grpcChannel));
+ }
+
+ private void assertStreamingError(ClientBuilder clientBuilder, String expectedErrorMessage) throws Exception {
+ AtomicReference errorRef = new AtomicReference<>();
+ CountDownLatch errorLatch = new CountDownLatch(1);
+
+ Consumer errorHandler = error -> {
+ errorRef.set(error);
+ errorLatch.countDown();
+ };
+
+ Client client = clientBuilder.streamingErrorHandler(errorHandler).build();
+
+ try {
+ client.sendMessage(MESSAGE);
+ // If no immediate exception, wait for async error
+ assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called");
+ Throwable error = errorRef.get();
+ assertTrue(error.getMessage().contains(expectedErrorMessage),
+ "Expected error message to contain '" + expectedErrorMessage + "' but got: " + error.getMessage());
+ } catch (Exception e) {
+ // Immediate exception is also acceptable
+ assertTrue(e.getMessage().contains(expectedErrorMessage),
+ "Expected error message to contain '" + expectedErrorMessage + "' but got: " + e.getMessage());
+ }
+ }
+
+ private void setupGrpcServer(Status status) throws IOException {
+ grpcServerName = InProcessServerBuilder.generateName();
+ grpcServer = InProcessServerBuilder.forName(grpcServerName)
+ .directExecutor()
+ .addService(new A2AServiceGrpc.A2AServiceImplBase() {
+ @Override
+ public void sendMessage(SendMessageRequest request, StreamObserver responseObserver) {
+ responseObserver.onError(status.asRuntimeException());
+ }
+
+ @Override
+ public void sendStreamingMessage(SendMessageRequest request, StreamObserver responseObserver) {
+ responseObserver.onError(status.asRuntimeException());
+ }
+ })
+ .build()
+ .start();
+
+ grpcChannel = InProcessChannelBuilder.forName(grpcServerName)
+ .directExecutor()
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilderTest.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilderTest.java
new file mode 100644
index 000000000..71cec5f9b
--- /dev/null
+++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilderTest.java
@@ -0,0 +1,96 @@
+package org.a2aproject.sdk.compat03.client;
+
+import org.a2aproject.sdk.compat03.client.config.ClientConfig;
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfig;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCapabilities;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.AgentInterface;
+import org.a2aproject.sdk.compat03.spec.AgentSkill;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ClientBuilderTest {
+
+ private AgentCard card = new AgentCard.Builder()
+ .name("Hello World Agent")
+ .description("Just a hello world agent")
+ .url("http://localhost:9999")
+ .version("1.0.0")
+ .documentationUrl("http://example.com/docs")
+ .capabilities(new AgentCapabilities.Builder()
+ .streaming(true)
+ .pushNotifications(true)
+ .stateTransitionHistory(true)
+ .build())
+ .defaultInputModes(Collections.singletonList("text"))
+ .defaultOutputModes(Collections.singletonList("text"))
+ .skills(Collections.singletonList(new AgentSkill.Builder()
+ .id("hello_world")
+ .name("Returns hello world")
+ .description("just returns hello world")
+ .tags(Collections.singletonList("hello world"))
+ .examples(List.of("hi", "hello world"))
+ .build()))
+ .protocolVersion("0.3.0")
+ .additionalInterfaces(List.of(
+ new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999")))
+ .build();
+
+ @Test
+ public void shouldNotFindCompatibleTransport() throws A2AClientException {
+ A2AClientException exception = Assertions.assertThrows(A2AClientException.class,
+ () -> Client
+ .builder(card)
+ .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
+ .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder()
+ .channelFactory(s -> null))
+ .build());
+
+ Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().contains("No compatible transport found"));
+ }
+
+ @Test
+ public void shouldNotFindConfigurationTransport() throws A2AClientException {
+ A2AClientException exception = Assertions.assertThrows(A2AClientException.class,
+ () -> Client
+ .builder(card)
+ .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
+ .build());
+
+ Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().startsWith("Missing required TransportConfig for"));
+ }
+
+ @Test
+ public void shouldCreateJSONRPCClient() throws A2AClientException {
+ Client client = Client
+ .builder(card)
+ .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build())
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()
+ .addInterceptor(null)
+ .httpClient(null))
+ .build();
+
+ Assertions.assertNotNull(client);
+ }
+
+ @Test
+ public void shouldCreateClient_differentConfigurations() throws A2AClientException {
+ Client client = Client
+ .builder(card)
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig(new JdkA2AHttpClient()))
+ .build();
+
+ Assertions.assertNotNull(client);
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/pom.xml b/compat-0.3/client/transport/grpc/pom.xml
new file mode 100644
index 000000000..e0ca84574
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.0.0.Beta1-SNAPSHOT
+ ../../..
+
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+ jar
+
+ Java SDK A2A Compat 0.3 Client Transport: gRPC
+ Java SDK for the Agent2Agent Protocol (A2A) - gRPC Client Transport
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-common
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec-grpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ io.grpc
+ grpc-stub
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.mock-server
+ mockserver-netty
+ test
+
+
+
+
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver.java
new file mode 100644
index 000000000..f9ff2e1b9
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver.java
@@ -0,0 +1,64 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+
+import org.a2aproject.sdk.compat03.grpc.StreamResponse;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind;
+import io.grpc.stub.StreamObserver;
+
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+import static org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils.FromProto;
+
+public class EventStreamObserver implements StreamObserver {
+
+ private static final Logger log = Logger.getLogger(EventStreamObserver.class.getName());
+ private final Consumer eventHandler;
+ private final Consumer errorHandler;
+
+ public EventStreamObserver(Consumer eventHandler, Consumer errorHandler) {
+ this.eventHandler = eventHandler;
+ this.errorHandler = errorHandler;
+ }
+
+ @Override
+ public void onNext(StreamResponse response) {
+ StreamingEventKind event;
+ switch (response.getPayloadCase()) {
+ case MSG:
+ event = FromProto.message(response.getMsg());
+ break;
+ case TASK:
+ event = FromProto.task(response.getTask());
+ break;
+ case STATUS_UPDATE:
+ event = FromProto.taskStatusUpdateEvent(response.getStatusUpdate());
+ break;
+ case ARTIFACT_UPDATE:
+ event = FromProto.taskArtifactUpdateEvent(response.getArtifactUpdate());
+ break;
+ default:
+ log.warning("Invalid stream response " + response.getPayloadCase());
+ errorHandler.accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase()));
+ return;
+ }
+ eventHandler.accept(event);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ if (errorHandler != null) {
+ // Map gRPC errors to proper A2A exceptions
+ if (t instanceof io.grpc.StatusRuntimeException) {
+ errorHandler.accept(GrpcErrorMapper.mapGrpcError((io.grpc.StatusRuntimeException) t));
+ } else {
+ errorHandler.accept(t);
+ }
+ }
+ }
+
+ @Override
+ public void onCompleted() {
+ // done
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper.java
new file mode 100644
index 000000000..ab9e6de18
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper.java
@@ -0,0 +1,101 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.a2aproject.sdk.common.A2AErrorMessages;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError;
+import org.a2aproject.sdk.compat03.spec.InvalidAgentResponseError;
+import org.a2aproject.sdk.compat03.spec.InvalidParamsError;
+import org.a2aproject.sdk.compat03.spec.InvalidRequestError;
+import org.a2aproject.sdk.compat03.spec.JSONParseError;
+import org.a2aproject.sdk.compat03.spec.MethodNotFoundError;
+import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError;
+import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError;
+import org.a2aproject.sdk.compat03.spec.TaskNotFoundError;
+import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError;
+import io.grpc.Status;
+import io.grpc.StatusException;
+import io.grpc.StatusRuntimeException;
+
+/**
+ * Utility class to map gRPC StatusRuntimeException to appropriate A2A error types
+ */
+public class GrpcErrorMapper {
+
+ // Overload for StatusRuntimeException (original 0.3.x signature)
+ public static A2AClientException mapGrpcError(StatusRuntimeException e) {
+ return mapGrpcError(e, "gRPC error: ");
+ }
+
+ public static A2AClientException mapGrpcError(StatusRuntimeException e, String errorPrefix) {
+ return mapGrpcErrorInternal(e.getStatus().getCode(), e.getStatus().getDescription(), e, errorPrefix);
+ }
+
+ // Overload for StatusException (gRPC 1.77+ compatibility)
+ public static A2AClientException mapGrpcError(StatusException e) {
+ return mapGrpcError(e, "gRPC error: ");
+ }
+
+ public static A2AClientException mapGrpcError(StatusException e, String errorPrefix) {
+ return mapGrpcErrorInternal(e.getStatus().getCode(), e.getStatus().getDescription(), e, errorPrefix);
+ }
+
+ // Dispatcher for multi-catch (StatusRuntimeException | StatusException)
+ public static A2AClientException mapGrpcError(Exception e, String errorPrefix) {
+ if (e instanceof StatusRuntimeException) {
+ return mapGrpcError((StatusRuntimeException) e, errorPrefix);
+ } else if (e instanceof StatusException) {
+ return mapGrpcError((StatusException) e, errorPrefix);
+ } else {
+ return new A2AClientException(errorPrefix + e.getMessage(), e);
+ }
+ }
+
+ private static A2AClientException mapGrpcErrorInternal(Status.Code code, @org.jspecify.annotations.Nullable String description, @org.jspecify.annotations.Nullable Throwable cause, String errorPrefix) {
+
+ // Extract the actual error type from the description if possible
+ // (using description because the same code can map to multiple errors -
+ // see GrpcHandler#handleError)
+ if (description != null) {
+ if (description.contains("TaskNotFoundError")) {
+ return new A2AClientException(errorPrefix + description, new TaskNotFoundError());
+ } else if (description.contains("UnsupportedOperationError")) {
+ return new A2AClientException(errorPrefix + description, new UnsupportedOperationError());
+ } else if (description.contains("InvalidParamsError")) {
+ return new A2AClientException(errorPrefix + description, new InvalidParamsError());
+ } else if (description.contains("InvalidRequestError")) {
+ return new A2AClientException(errorPrefix + description, new InvalidRequestError());
+ } else if (description.contains("MethodNotFoundError")) {
+ return new A2AClientException(errorPrefix + description, new MethodNotFoundError());
+ } else if (description.contains("TaskNotCancelableError")) {
+ return new A2AClientException(errorPrefix + description, new TaskNotCancelableError());
+ } else if (description.contains("PushNotificationNotSupportedError")) {
+ return new A2AClientException(errorPrefix + description, new PushNotificationNotSupportedError());
+ } else if (description.contains("JSONParseError")) {
+ return new A2AClientException(errorPrefix + description, new JSONParseError());
+ } else if (description.contains("ContentTypeNotSupportedError")) {
+ return new A2AClientException(errorPrefix + description, new ContentTypeNotSupportedError(null, description, null));
+ } else if (description.contains("InvalidAgentResponseError")) {
+ return new A2AClientException(errorPrefix + description, new InvalidAgentResponseError(null, description, null));
+ }
+ }
+
+ // Fall back to mapping based on status code
+ String message = description != null ? description : (cause != null ? cause.getMessage() : "Unknown error");
+ switch (code) {
+ case NOT_FOUND:
+ return new A2AClientException(errorPrefix + message, new TaskNotFoundError());
+ case UNIMPLEMENTED:
+ return new A2AClientException(errorPrefix + message, new UnsupportedOperationError());
+ case INVALID_ARGUMENT:
+ return new A2AClientException(errorPrefix + message, new InvalidParamsError());
+ case INTERNAL:
+ return new A2AClientException(errorPrefix + message, new org.a2aproject.sdk.compat03.spec.InternalError(null, message, null));
+ case UNAUTHENTICATED:
+ return new A2AClientException(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED);
+ case PERMISSION_DENIED:
+ return new A2AClientException(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED);
+ default:
+ return new A2AClientException(errorPrefix + message, cause);
+ }
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport.java
new file mode 100644
index 000000000..bb0de3eca
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport.java
@@ -0,0 +1,384 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor;
+import org.a2aproject.sdk.common.A2AHeaders;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc.A2AServiceBlockingV2Stub;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc.A2AServiceStub;
+import org.a2aproject.sdk.compat03.grpc.CancelTaskRequest;
+import org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest;
+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.grpc.SendMessageRequest;
+import org.a2aproject.sdk.compat03.grpc.SendMessageResponse;
+import org.a2aproject.sdk.compat03.grpc.StreamResponse;
+import org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest;
+import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils.FromProto;
+import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils.ToProto;
+import io.grpc.StatusException;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.EventKind;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams;
+import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest;
+import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind;
+import org.a2aproject.sdk.compat03.spec.Task;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams;
+import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest;
+import io.grpc.Channel;
+import io.grpc.Metadata;
+import io.grpc.StatusRuntimeException;
+import io.grpc.stub.MetadataUtils;
+import io.grpc.stub.StreamObserver;
+import org.jspecify.annotations.Nullable;
+
+public class GrpcTransport implements ClientTransport {
+
+ private static final Metadata.Key AUTHORIZATION_METADATA_KEY = Metadata.Key.of(
+ AuthInterceptor.AUTHORIZATION,
+ Metadata.ASCII_STRING_MARSHALLER);
+ private static final Metadata.Key EXTENSIONS_KEY = Metadata.Key.of(
+ "X-A2A-Extensions",
+ Metadata.ASCII_STRING_MARSHALLER);
+ private final A2AServiceBlockingV2Stub blockingStub;
+ private final A2AServiceStub asyncStub;
+ private final @Nullable List interceptors;
+ private AgentCard agentCard;
+
+ public GrpcTransport(Channel channel, AgentCard agentCard) {
+ this(channel, agentCard, null);
+ }
+
+ public GrpcTransport(Channel channel, AgentCard agentCard, @Nullable List interceptors) {
+ checkNotNullParam("channel", channel);
+ checkNotNullParam("agentCard", agentCard);
+ this.asyncStub = A2AServiceGrpc.newStub(channel);
+ this.blockingStub = A2AServiceGrpc.newBlockingV2Stub(channel);
+ this.agentCard = agentCard;
+ this.interceptors = interceptors;
+ }
+
+ @Override
+ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+
+ SendMessageRequest sendMessageRequest = createGrpcSendMessageRequest(request, context);
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(org.a2aproject.sdk.compat03.spec.SendMessageRequest.METHOD, sendMessageRequest,
+ agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ SendMessageResponse response = stubWithMetadata.sendMessage(sendMessageRequest);
+ if (response.hasMsg()) {
+ return FromProto.message(response.getMsg());
+ } else if (response.hasTask()) {
+ return FromProto.task(response.getTask());
+ } else {
+ throw new A2AClientException("Server response did not contain a message or task");
+ }
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to send message: ");
+ }
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer,
+ Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ checkNotNullParam("eventConsumer", eventConsumer);
+ SendMessageRequest grpcRequest = createGrpcSendMessageRequest(request, context);
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(SendStreamingMessageRequest.METHOD,
+ grpcRequest, agentCard, context);
+ StreamObserver streamObserver = new EventStreamObserver(eventConsumer, errorConsumer);
+
+ try {
+ A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders);
+ stubWithMetadata.sendStreamingMessage(grpcRequest, streamObserver);
+ } catch (StatusRuntimeException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to send streaming message request: ");
+ }
+ }
+
+ @Override
+ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+
+ GetTaskRequest.Builder requestBuilder = GetTaskRequest.newBuilder();
+ requestBuilder.setName("tasks/" + request.id());
+ requestBuilder.setHistoryLength(request.historyLength());
+ GetTaskRequest getTaskRequest = requestBuilder.build();
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(org.a2aproject.sdk.compat03.spec.GetTaskRequest.METHOD, getTaskRequest,
+ agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return FromProto.task(stubWithMetadata.getTask(getTaskRequest));
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to get task: ");
+ }
+ }
+
+ @Override
+ public Task cancelTask(TaskIdParams request, @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+
+ CancelTaskRequest cancelTaskRequest = CancelTaskRequest.newBuilder()
+ .setName("tasks/" + request.id())
+ .build();
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(org.a2aproject.sdk.compat03.spec.CancelTaskRequest.METHOD, cancelTaskRequest,
+ agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return FromProto.task(stubWithMetadata.cancelTask(cancelTaskRequest));
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to cancel task: ");
+ }
+ }
+
+ @Override
+ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
+ @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+
+ String configId = request.pushNotificationConfig().id();
+ CreateTaskPushNotificationConfigRequest grpcRequest = CreateTaskPushNotificationConfigRequest.newBuilder()
+ .setParent("tasks/" + request.taskId())
+ .setConfig(ToProto.taskPushNotificationConfig(request))
+ .setConfigId(configId != null ? configId : request.taskId())
+ .build();
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest.METHOD,
+ grpcRequest, agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return FromProto.taskPushNotificationConfig(stubWithMetadata.createTaskPushNotificationConfig(grpcRequest));
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to create task push notification config: ");
+ }
+ }
+
+ @Override
+ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(
+ GetTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+
+ GetTaskPushNotificationConfigRequest grpcRequest = GetTaskPushNotificationConfigRequest.newBuilder()
+ .setName(getTaskPushNotificationConfigName(request))
+ .build();
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest.METHOD,
+ grpcRequest, agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return FromProto.taskPushNotificationConfig(stubWithMetadata.getTaskPushNotificationConfig(grpcRequest));
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to get task push notification config: ");
+ }
+ }
+
+ @Override
+ public List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+
+ ListTaskPushNotificationConfigRequest grpcRequest = ListTaskPushNotificationConfigRequest.newBuilder()
+ .setParent("tasks/" + request.id())
+ .build();
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest.METHOD,
+ grpcRequest, agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return stubWithMetadata.listTaskPushNotificationConfig(grpcRequest).getConfigsList().stream()
+ .map(FromProto::taskPushNotificationConfig)
+ .collect(Collectors.toList());
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to list task push notification config: ");
+ }
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request,
+ @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+
+ DeleteTaskPushNotificationConfigRequest grpcRequest = DeleteTaskPushNotificationConfigRequest.newBuilder()
+ .setName(getTaskPushNotificationConfigName(request.id(), request.pushNotificationConfigId()))
+ .build();
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest.METHOD,
+ grpcRequest, agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ stubWithMetadata.deleteTaskPushNotificationConfig(grpcRequest);
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to delete task push notification config: ");
+ }
+ }
+
+ @Override
+ public void resubscribe(TaskIdParams request, Consumer eventConsumer,
+ Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ checkNotNullParam("eventConsumer", eventConsumer);
+
+ TaskSubscriptionRequest grpcRequest = TaskSubscriptionRequest.newBuilder()
+ .setName("tasks/" + request.id())
+ .build();
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(TaskResubscriptionRequest.METHOD,
+ grpcRequest, agentCard, context);
+
+ StreamObserver streamObserver = new EventStreamObserver(eventConsumer, errorConsumer);
+
+ try {
+ A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders);
+ stubWithMetadata.taskSubscription(grpcRequest, streamObserver);
+ } catch (StatusRuntimeException e) {
+ throw GrpcErrorMapper.mapGrpcError(e, "Failed to resubscribe task push notification config: ");
+ }
+ }
+
+ @Override
+ public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
+ // TODO: Determine how to handle retrieving the authenticated extended agent card
+ return agentCard;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ private SendMessageRequest createGrpcSendMessageRequest(MessageSendParams messageSendParams, @Nullable ClientCallContext context) {
+ SendMessageRequest.Builder builder = SendMessageRequest.newBuilder();
+ builder.setRequest(ToProto.message(messageSendParams.message()));
+ if (messageSendParams.configuration() != null) {
+ builder.setConfiguration(ToProto.messageSendConfiguration(messageSendParams.configuration()));
+ }
+ if (messageSendParams.metadata() != null) {
+ builder.setMetadata(ToProto.struct(messageSendParams.metadata()));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Creates gRPC metadata from ClientCallContext headers.
+ * Extracts headers like X-A2A-Extensions and sets them as gRPC metadata.
+ * @param context the client call context containing headers, may be null
+ * @param payloadAndHeaders the payload and headers wrapper, may be null
+ * @return the gRPC metadata
+ */
+ private Metadata createGrpcMetadata(@Nullable ClientCallContext context, @Nullable PayloadAndHeaders payloadAndHeaders) {
+ Metadata metadata = new Metadata();
+
+ if (context != null && context.getHeaders() != null) {
+ // Set X-A2A-Extensions header if present
+ String extensionsHeader = context.getHeaders().get("X-A2A-Extensions");
+ if (extensionsHeader != null) {
+ metadata.put(EXTENSIONS_KEY, extensionsHeader);
+ }
+
+ // Add other headers as needed in the future
+ // For now, we only handle X-A2A-Extensions
+ }
+ if (payloadAndHeaders != null && payloadAndHeaders.getHeaders() != null) {
+ // Handle all headers from interceptors (including auth headers)
+ for (Map.Entry headerEntry : payloadAndHeaders.getHeaders().entrySet()) {
+ String headerName = headerEntry.getKey();
+ String headerValue = headerEntry.getValue();
+
+ if (headerValue != null) {
+ // Use static key for common Authorization header, create dynamic keys for others
+ if (AuthInterceptor.AUTHORIZATION.equals(headerName)) {
+ 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.put(metadataKey, headerValue);
+ }
+ }
+ }
+ }
+
+ return metadata;
+ }
+
+ /**
+ * Creates a blocking stub with metadata attached from the ClientCallContext.
+ *
+ * @param context the client call context
+ * @param payloadAndHeaders the payloadAndHeaders after applying any interceptors
+ * @return blocking stub with metadata interceptor
+ */
+ private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(@Nullable ClientCallContext context,
+ PayloadAndHeaders payloadAndHeaders) {
+ Metadata metadata = createGrpcMetadata(context, payloadAndHeaders);
+ return blockingStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
+ }
+
+ /**
+ * Creates an async stub with metadata attached from the ClientCallContext.
+ *
+ * @param context the client call context
+ * @param payloadAndHeaders the payloadAndHeaders after applying any interceptors
+ * @return async stub with metadata interceptor
+ */
+ private A2AServiceStub createAsyncStubWithMetadata(@Nullable ClientCallContext context,
+ PayloadAndHeaders payloadAndHeaders) {
+ Metadata metadata = createGrpcMetadata(context, payloadAndHeaders);
+ return asyncStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
+ }
+
+ private String getTaskPushNotificationConfigName(GetTaskPushNotificationConfigParams params) {
+ return getTaskPushNotificationConfigName(params.id(), params.pushNotificationConfigId());
+ }
+
+ 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);
+ }
+ //name.append("/pushNotificationConfigs/");
+ // Use taskId as default config ID if none provided
+ //name.append(pushNotificationConfigId != null ? pushNotificationConfigId : taskId);
+ return name.toString();
+ }
+
+ private PayloadAndHeaders applyInterceptors(String methodName, Object payload,
+ AgentCard agentCard, @Nullable ClientCallContext clientCallContext) {
+ PayloadAndHeaders payloadAndHeaders = new PayloadAndHeaders(payload,
+ clientCallContext != null ? clientCallContext.getHeaders() : null);
+ if (interceptors != null && ! interceptors.isEmpty()) {
+ for (ClientCallInterceptor interceptor : interceptors) {
+ payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(),
+ payloadAndHeaders.getHeaders(), agentCard, clientCallContext);
+ }
+ }
+ return payloadAndHeaders;
+ }
+
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig.java
new file mode 100644
index 000000000..cb50e1fbf
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig.java
@@ -0,0 +1,21 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.util.Assert;
+import io.grpc.Channel;
+
+import java.util.function.Function;
+
+public class GrpcTransportConfig extends ClientTransportConfig {
+
+ private final Function channelFactory;
+
+ public GrpcTransportConfig(Function channelFactory) {
+ Assert.checkNotNullParam("channelFactory", channelFactory);
+ this.channelFactory = channelFactory;
+ }
+
+ public Function getChannelFactory() {
+ return this.channelFactory;
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder.java
new file mode 100644
index 000000000..4ec282d4c
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder.java
@@ -0,0 +1,32 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder;
+import org.a2aproject.sdk.util.Assert;
+import io.grpc.Channel;
+
+import java.util.function.Function;
+
+import org.jspecify.annotations.Nullable;
+
+public class GrpcTransportConfigBuilder extends ClientTransportConfigBuilder {
+
+ private @Nullable Function channelFactory;
+
+ public GrpcTransportConfigBuilder channelFactory(Function channelFactory) {
+ Assert.checkNotNullParam("channelFactory", channelFactory);
+
+ this.channelFactory = channelFactory;
+
+ return this;
+ }
+
+ @Override
+ public GrpcTransportConfig build() {
+ if (channelFactory == null) {
+ throw new IllegalStateException("channelFactory must be set");
+ }
+ GrpcTransportConfig config = new GrpcTransportConfig(channelFactory);
+ config.setInterceptors(interceptors);
+ return config;
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider.java
new file mode 100644
index 000000000..ce4d79c4c
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider.java
@@ -0,0 +1,35 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol;
+import io.grpc.Channel;
+
+/**
+ * Provider for gRPC transport implementation.
+ */
+public class GrpcTransportProvider implements ClientTransportProvider {
+
+ @Override
+ public GrpcTransport create(GrpcTransportConfig grpcTransportConfig, AgentCard agentCard, String agentUrl) throws A2AClientException {
+ // not making use of the interceptors for gRPC for now
+
+ Channel channel = grpcTransportConfig.getChannelFactory().apply(agentUrl);
+ if (channel != null) {
+ return new GrpcTransport(channel, agentCard, grpcTransportConfig.getInterceptors());
+ }
+
+ throw new A2AClientException("Missing required GrpcTransportConfig");
+ }
+
+ @Override
+ public String getTransportProtocol() {
+ return TransportProtocol.GRPC.asString();
+ }
+
+ @Override
+ public Class getTransportProtocolClass() {
+ return GrpcTransport.class;
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java
new file mode 100644
index 000000000..71bb5c883
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider b/compat-0.3/client/transport/grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider
new file mode 100644
index 000000000..021357f18
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider
@@ -0,0 +1 @@
+org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportProvider
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapperTest.java b/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapperTest.java
new file mode 100644
index 000000000..4bc2659a9
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapperTest.java
@@ -0,0 +1,293 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError;
+import org.a2aproject.sdk.compat03.spec.InternalError;
+import org.a2aproject.sdk.compat03.spec.InvalidAgentResponseError;
+import org.a2aproject.sdk.compat03.spec.InvalidParamsError;
+import org.a2aproject.sdk.compat03.spec.InvalidRequestError;
+import org.a2aproject.sdk.compat03.spec.JSONParseError;
+import org.a2aproject.sdk.compat03.spec.MethodNotFoundError;
+import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError;
+import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError;
+import org.a2aproject.sdk.compat03.spec.TaskNotFoundError;
+import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for GrpcErrorMapper - verifies correct mapping of gRPC StatusRuntimeException
+ * to v0.3 A2A error types based on description string matching and status codes.
+ */
+public class GrpcErrorMapperTest {
+
+ @Test
+ public void testTaskNotFoundErrorByDescription() {
+ String errorMessage = "TaskNotFoundError: Task task-123 not found";
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(TaskNotFoundError.class, result.getCause());
+ assertTrue(result.getMessage().contains(errorMessage));
+ }
+
+ @Test
+ public void testTaskNotFoundErrorByStatusCode() {
+ // Test fallback to status code mapping when description doesn't contain error type
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .withDescription("Generic not found error")
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(TaskNotFoundError.class, result.getCause());
+ }
+
+ @Test
+ public void testUnsupportedOperationErrorByDescription() {
+ String errorMessage = "UnsupportedOperationError: Operation not supported";
+ StatusRuntimeException grpcException = Status.UNIMPLEMENTED
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(UnsupportedOperationError.class, result.getCause());
+ }
+
+ @Test
+ public void testUnsupportedOperationErrorByStatusCode() {
+ StatusRuntimeException grpcException = Status.UNIMPLEMENTED
+ .withDescription("Generic unimplemented error")
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(UnsupportedOperationError.class, result.getCause());
+ }
+
+ @Test
+ public void testInvalidParamsErrorByDescription() {
+ String errorMessage = "InvalidParamsError: Invalid parameters provided";
+ StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InvalidParamsError.class, result.getCause());
+ }
+
+ @Test
+ public void testInvalidParamsErrorByStatusCode() {
+ StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
+ .withDescription("Generic invalid argument")
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InvalidParamsError.class, result.getCause());
+ }
+
+ @Test
+ public void testInvalidRequestError() {
+ String errorMessage = "InvalidRequestError: Request is malformed";
+ StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InvalidRequestError.class, result.getCause());
+ }
+
+ @Test
+ public void testMethodNotFoundError() {
+ String errorMessage = "MethodNotFoundError: Method does not exist";
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(MethodNotFoundError.class, result.getCause());
+ }
+
+ @Test
+ public void testTaskNotCancelableError() {
+ String errorMessage = "TaskNotCancelableError: Task cannot be cancelled";
+ StatusRuntimeException grpcException = Status.UNIMPLEMENTED
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(TaskNotCancelableError.class, result.getCause());
+ }
+
+ @Test
+ public void testPushNotificationNotSupportedError() {
+ String errorMessage = "PushNotificationNotSupportedError: Push notifications not supported";
+ StatusRuntimeException grpcException = Status.UNIMPLEMENTED
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(PushNotificationNotSupportedError.class, result.getCause());
+ }
+
+ @Test
+ public void testJSONParseError() {
+ String errorMessage = "JSONParseError: Failed to parse JSON";
+ StatusRuntimeException grpcException = Status.INTERNAL
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(JSONParseError.class, result.getCause());
+ }
+
+ @Test
+ public void testContentTypeNotSupportedError() {
+ String errorMessage = "ContentTypeNotSupportedError: Content type application/xml not supported";
+ StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(ContentTypeNotSupportedError.class, result.getCause());
+
+ ContentTypeNotSupportedError contentTypeError = (ContentTypeNotSupportedError) result.getCause();
+ assertNotNull(contentTypeError.getMessage());
+ assertTrue(contentTypeError.getMessage().contains("Content type application/xml not supported"));
+ }
+
+ @Test
+ public void testInvalidAgentResponseError() {
+ String errorMessage = "InvalidAgentResponseError: Agent response is invalid";
+ StatusRuntimeException grpcException = Status.INTERNAL
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InvalidAgentResponseError.class, result.getCause());
+
+ InvalidAgentResponseError agentResponseError = (InvalidAgentResponseError) result.getCause();
+ assertNotNull(agentResponseError.getMessage());
+ assertTrue(agentResponseError.getMessage().contains("Agent response is invalid"));
+ }
+
+ @Test
+ public void testInternalErrorByStatusCode() {
+ StatusRuntimeException grpcException = Status.INTERNAL
+ .withDescription("Internal server error")
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InternalError.class, result.getCause());
+ }
+
+ @Test
+ public void testCustomErrorPrefix() {
+ String errorMessage = "TaskNotFoundError: Task not found";
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ String customPrefix = "Custom Error: ";
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException, customPrefix);
+
+ assertNotNull(result);
+ assertTrue(result.getMessage().startsWith(customPrefix));
+ assertInstanceOf(TaskNotFoundError.class, result.getCause());
+ }
+
+ @Test
+ public void testAuthenticationFailed() {
+ StatusRuntimeException grpcException = Status.UNAUTHENTICATED
+ .withDescription("Authentication failed")
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertTrue(result.getMessage().contains("Authentication failed"));
+ }
+
+ @Test
+ public void testAuthorizationFailed() {
+ StatusRuntimeException grpcException = Status.PERMISSION_DENIED
+ .withDescription("Permission denied")
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertTrue(result.getMessage().contains("Authorization failed"));
+ }
+
+ @Test
+ public void testUnknownStatusCode() {
+ StatusRuntimeException grpcException = Status.DEADLINE_EXCEEDED
+ .withDescription("Request timeout")
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertTrue(result.getMessage().contains("Request timeout"));
+ }
+
+ @Test
+ public void testNullDescription() {
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .asRuntimeException();
+
+ A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(TaskNotFoundError.class, result.getCause());
+ }
+}
diff --git a/compat-0.3/client/transport/jsonrpc/pom.xml b/compat-0.3/client/transport/jsonrpc/pom.xml
new file mode 100644
index 000000000..83f0ac416
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.0.0.Beta1-SNAPSHOT
+ ../../..
+
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+ jar
+
+ Java SDK A2A Compat 0.3 Client Transport: JSONRPC
+ Java SDK for the Agent2Agent Protocol (A2A) - JSONRPC Client Transport
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-http-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ ${project.groupId}
+ a2a-java-sdk-common
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.mock-server
+ mockserver-netty
+ test
+
+
+
+
\ 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/JSONRPCTransport.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport.java
new file mode 100644
index 000000000..1090112c4
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport.java
@@ -0,0 +1,424 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.compat03.json.JsonProcessingException;
+import org.a2aproject.sdk.compat03.json.JsonUtil;
+
+import org.a2aproject.sdk.compat03.client.http.A2ACardResolver;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders;
+import org.a2aproject.sdk.compat03.client.http.A2AHttpClient;
+import org.a2aproject.sdk.compat03.client.http.A2AHttpResponse;
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport;
+import org.a2aproject.sdk.compat03.spec.A2AClientError;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.CancelTaskRequest;
+import org.a2aproject.sdk.compat03.spec.CancelTaskResponse;
+
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.EventKind;
+import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardRequest;
+import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardResponse;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigResponse;
+import org.a2aproject.sdk.compat03.spec.GetTaskRequest;
+import org.a2aproject.sdk.compat03.spec.GetTaskResponse;
+import org.a2aproject.sdk.compat03.spec.JSONRPCError;
+import org.a2aproject.sdk.compat03.spec.JSONRPCMessage;
+import org.a2aproject.sdk.compat03.spec.JSONRPCResponse;
+
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigResponse;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigResponse;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams;
+import org.a2aproject.sdk.compat03.spec.SendMessageRequest;
+import org.a2aproject.sdk.compat03.spec.SendMessageResponse;
+import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest;
+import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigResponse;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind;
+import org.a2aproject.sdk.compat03.spec.Task;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams;
+import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.sse.SSEEventListener;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class JSONRPCTransport implements ClientTransport {
+
+ private static final Class SEND_MESSAGE_RESPONSE_REFERENCE = SendMessageResponse.class;
+ private static final Class GET_TASK_RESPONSE_REFERENCE = GetTaskResponse.class;
+ private static final Class CANCEL_TASK_RESPONSE_REFERENCE = CancelTaskResponse.class;
+ private static final Class GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = GetTaskPushNotificationConfigResponse.class;
+ private static final Class SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = SetTaskPushNotificationConfigResponse.class;
+ private static final Class LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = ListTaskPushNotificationConfigResponse.class;
+ private static final Class DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = DeleteTaskPushNotificationConfigResponse.class;
+ private static final Class GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE = GetAuthenticatedExtendedCardResponse.class;
+
+ private final A2AHttpClient httpClient;
+ private final String agentUrl;
+ private final List interceptors;
+ private AgentCard agentCard;
+ private boolean needsExtendedCard = false;
+
+ public JSONRPCTransport(String agentUrl) {
+ this(null, null, agentUrl, null);
+ }
+
+ public JSONRPCTransport(AgentCard agentCard) {
+ this(null, agentCard, agentCard.url(), null);
+ }
+
+ public JSONRPCTransport(A2AHttpClient httpClient, AgentCard agentCard,
+ String agentUrl, List interceptors) {
+ this.httpClient = httpClient == null ? new JdkA2AHttpClient() : httpClient;
+ this.agentCard = agentCard;
+ this.agentUrl = agentUrl;
+ this.interceptors = interceptors;
+ this.needsExtendedCard = agentCard == null || agentCard.supportsAuthenticatedExtendedCard();
+ }
+
+ @Override
+ public EventKind sendMessage(MessageSendParams request, ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ SendMessageRequest sendMessageRequest = new SendMessageRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(SendMessageRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(SendMessageRequest.METHOD, sendMessageRequest,
+ agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ SendMessageResponse response = unmarshalResponse(httpResponseBody, SEND_MESSAGE_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException e) {
+ throw new A2AClientException("Failed to send message: " + e, e);
+ }
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer,
+ Consumer errorConsumer, ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ checkNotNullParam("eventConsumer", eventConsumer);
+ SendStreamingMessageRequest sendStreamingMessageRequest = new SendStreamingMessageRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(SendStreamingMessageRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(SendStreamingMessageRequest.METHOD,
+ sendStreamingMessageRequest, agentCard, context);
+
+ AtomicReference> ref = new AtomicReference<>();
+ SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);
+
+ try {
+ A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders);
+ ref.set(builder.postAsyncSSE(
+ msg -> sseEventListener.onMessage(msg, ref.get()),
+ throwable -> sseEventListener.onError(throwable, ref.get()),
+ () -> {
+ // Signal normal stream completion to error handler (null error means success)
+ sseEventListener.onComplete();
+ }));
+ } catch (IOException e) {
+ throw new A2AClientException("Failed to send streaming message request: " + e, e);
+ } catch (InterruptedException e) {
+ throw new A2AClientException("Send streaming message request timed out: " + e, e);
+ } catch (JsonProcessingException e) {
+ throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e);
+ }
+ }
+
+ @Override
+ public Task getTask(TaskQueryParams request, ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ GetTaskRequest getTaskRequest = new GetTaskRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(GetTaskRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(GetTaskRequest.METHOD, getTaskRequest,
+ agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ GetTaskResponse response = unmarshalResponse(httpResponseBody, GET_TASK_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException e) {
+ throw new A2AClientException("Failed to get task: " + e, e);
+ }
+ }
+
+ @Override
+ public Task cancelTask(TaskIdParams request, ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ CancelTaskRequest cancelTaskRequest = new CancelTaskRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(CancelTaskRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(CancelTaskRequest.METHOD, cancelTaskRequest,
+ agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ CancelTaskResponse response = unmarshalResponse(httpResponseBody, CANCEL_TASK_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException e) {
+ throw new A2AClientException("Failed to cancel task: " + e, e);
+ }
+ }
+
+ @Override
+ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
+ ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ SetTaskPushNotificationConfigRequest setTaskPushNotificationRequest = new SetTaskPushNotificationConfigRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(SetTaskPushNotificationConfigRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest.METHOD,
+ setTaskPushNotificationRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ SetTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody,
+ SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException e) {
+ throw new A2AClientException("Failed to set task push notification config: " + e, e);
+ }
+ }
+
+ @Override
+ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request,
+ ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ GetTaskPushNotificationConfigRequest getTaskPushNotificationRequest = new GetTaskPushNotificationConfigRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(GetTaskPushNotificationConfigRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(GetTaskPushNotificationConfigRequest.METHOD,
+ getTaskPushNotificationRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ GetTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody,
+ GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException e) {
+ throw new A2AClientException("Failed to get task push notification config: " + e, e);
+ }
+ }
+
+ @Override
+ public List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams request,
+ ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ ListTaskPushNotificationConfigRequest listTaskPushNotificationRequest = new ListTaskPushNotificationConfigRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(ListTaskPushNotificationConfigRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(ListTaskPushNotificationConfigRequest.METHOD,
+ listTaskPushNotificationRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ ListTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody,
+ LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException e) {
+ throw new A2AClientException("Failed to list task push notification configs: " + e, e);
+ }
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request,
+ ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ DeleteTaskPushNotificationConfigRequest deleteTaskPushNotificationRequest = new DeleteTaskPushNotificationConfigRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(DeleteTaskPushNotificationConfigRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(DeleteTaskPushNotificationConfigRequest.METHOD,
+ deleteTaskPushNotificationRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ unmarshalResponse(httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ } catch (A2AClientException e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException e) {
+ throw new A2AClientException("Failed to delete task push notification configs: " + e, e);
+ }
+ }
+
+ @Override
+ public void resubscribe(TaskIdParams request, Consumer eventConsumer,
+ Consumer errorConsumer, ClientCallContext context) throws A2AClientException {
+ checkNotNullParam("request", request);
+ checkNotNullParam("eventConsumer", eventConsumer);
+ checkNotNullParam("errorConsumer", errorConsumer);
+ TaskResubscriptionRequest taskResubscriptionRequest = new TaskResubscriptionRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(TaskResubscriptionRequest.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(TaskResubscriptionRequest.METHOD,
+ taskResubscriptionRequest, agentCard, context);
+
+ AtomicReference> ref = new AtomicReference<>();
+ SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);
+
+ try {
+ A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders);
+ ref.set(builder.postAsyncSSE(
+ msg -> sseEventListener.onMessage(msg, ref.get()),
+ throwable -> sseEventListener.onError(throwable, ref.get()),
+ () -> {
+ // Signal normal stream completion to error handler (null error means success)
+ sseEventListener.onComplete();
+ }));
+ } catch (IOException e) {
+ throw new A2AClientException("Failed to send task resubscription request: " + e, e);
+ } catch (InterruptedException e) {
+ throw new A2AClientException("Task resubscription request timed out: " + e, e);
+ } catch (JsonProcessingException e) {
+ throw new A2AClientException("Failed to process JSON for task resubscription request: " + e, e);
+ }
+ }
+
+ @Override
+ public AgentCard getAgentCard(ClientCallContext context) throws A2AClientException {
+ A2ACardResolver resolver;
+ try {
+ if (agentCard == null) {
+ resolver = new A2ACardResolver(httpClient, agentUrl, null, getHttpHeaders(context));
+ agentCard = resolver.getAgentCard();
+ needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard();
+ }
+ if (!needsExtendedCard) {
+ return agentCard;
+ }
+
+ GetAuthenticatedExtendedCardRequest getExtendedAgentCardRequest = new GetAuthenticatedExtendedCardRequest.Builder()
+ .jsonrpc(JSONRPCMessage.JSONRPC_VERSION)
+ .method(GetAuthenticatedExtendedCardRequest.METHOD)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders payloadAndHeaders = applyInterceptors(GetAuthenticatedExtendedCardRequest.METHOD,
+ getExtendedAgentCardRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ GetAuthenticatedExtendedCardResponse response = unmarshalResponse(httpResponseBody,
+ GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE);
+ agentCard = response.getResult();
+ needsExtendedCard = false;
+ return agentCard;
+ } catch (IOException | InterruptedException | JsonProcessingException e) {
+ throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e);
+ }
+ } catch(A2AClientError e){
+ throw new A2AClientException("Failed to get agent card: " + e, e);
+ }
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ private PayloadAndHeaders applyInterceptors(String methodName, Object payload,
+ AgentCard agentCard, ClientCallContext clientCallContext) {
+ PayloadAndHeaders payloadAndHeaders = new PayloadAndHeaders(payload, getHttpHeaders(clientCallContext));
+ if (interceptors != null && ! interceptors.isEmpty()) {
+ for (ClientCallInterceptor interceptor : interceptors) {
+ payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(),
+ payloadAndHeaders.getHeaders(), agentCard, clientCallContext);
+ }
+ }
+ return payloadAndHeaders;
+ }
+
+ private String sendPostRequest(PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException {
+ A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders);
+ A2AHttpResponse response = builder.post();
+ if (!response.success()) {
+ throw new IOException("Request failed " + response.status());
+ }
+ return response.body();
+ }
+
+ private A2AHttpClient.PostBuilder createPostBuilder(PayloadAndHeaders payloadAndHeaders) throws JsonProcessingException {
+ A2AHttpClient.PostBuilder postBuilder = httpClient.createPost()
+ .url(agentUrl)
+ .addHeader("Content-Type", "application/json")
+ .body(JsonUtil.toJson(payloadAndHeaders.getPayload()));
+
+ if (payloadAndHeaders.getHeaders() != null) {
+ for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
+ postBuilder.addHeader(entry.getKey(), entry.getValue());
+ }
+ }
+
+ return postBuilder;
+ }
+
+ private > T unmarshalResponse(String response, Class responseClass)
+ throws A2AClientException, JsonProcessingException {
+ T value = JsonUtil.fromJson(response, responseClass);
+ JSONRPCError error = value.getError();
+ if (error != null) {
+ throw new A2AClientException(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), error);
+ }
+ return value;
+ }
+
+ private Map getHttpHeaders(ClientCallContext context) {
+ return context != null ? context.getHeaders() : null;
+ }
+}
\ 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/JSONRPCTransportConfig.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfig.java
new file mode 100644
index 000000000..85e15deb5
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfig.java
@@ -0,0 +1,21 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig;
+import org.a2aproject.sdk.compat03.client.http.A2AHttpClient;
+
+public class JSONRPCTransportConfig extends ClientTransportConfig {
+
+ private final A2AHttpClient httpClient;
+
+ public JSONRPCTransportConfig() {
+ this.httpClient = null;
+ }
+
+ public JSONRPCTransportConfig(A2AHttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ public A2AHttpClient getHttpClient() {
+ return httpClient;
+ }
+}
\ 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/JSONRPCTransportConfigBuilder.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
new file mode 100644
index 000000000..c602094f7
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
@@ -0,0 +1,28 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import org.a2aproject.sdk.compat03.client.http.A2AHttpClient;
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder;
+
+public class JSONRPCTransportConfigBuilder extends ClientTransportConfigBuilder {
+
+ private A2AHttpClient httpClient;
+
+ public JSONRPCTransportConfigBuilder httpClient(A2AHttpClient httpClient) {
+ this.httpClient = httpClient;
+
+ return this;
+ }
+
+ @Override
+ public JSONRPCTransportConfig build() {
+ // No HTTP client provided, fallback to the default one (JDK-based implementation)
+ if (httpClient == null) {
+ httpClient = new JdkA2AHttpClient();
+ }
+
+ JSONRPCTransportConfig config = new JSONRPCTransportConfig(httpClient);
+ config.setInterceptors(this.interceptors);
+ return config;
+ }
+}
\ 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/JSONRPCTransportProvider.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider.java
new file mode 100644
index 000000000..5e7eab1f6
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider.java
@@ -0,0 +1,29 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider;
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol;
+
+public class JSONRPCTransportProvider implements ClientTransportProvider {
+
+ @Override
+ public JSONRPCTransport create(JSONRPCTransportConfig clientTransportConfig, AgentCard agentCard, String agentUrl) throws A2AClientException {
+ if (clientTransportConfig == null) {
+ clientTransportConfig = new JSONRPCTransportConfig(new JdkA2AHttpClient());
+ }
+
+ return new JSONRPCTransport(clientTransportConfig.getHttpClient(), agentCard, agentUrl, clientTransportConfig.getInterceptors());
+ }
+
+ @Override
+ public String getTransportProtocol() {
+ return TransportProtocol.JSONRPC.asString();
+ }
+
+ @Override
+ public Class getTransportProtocolClass() {
+ return JSONRPCTransport.class;
+ }
+}
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener.java
new file mode 100644
index 000000000..3e9a9e745
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener.java
@@ -0,0 +1,88 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc.sse;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import org.a2aproject.sdk.compat03.json.JsonProcessingException;
+import org.a2aproject.sdk.compat03.json.JsonUtil;
+import org.a2aproject.sdk.compat03.spec.JSONRPCError;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind;
+import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent;
+
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+public class SSEEventListener {
+ private static final Logger log = Logger.getLogger(SSEEventListener.class.getName());
+ private final Consumer eventHandler;
+ private final Consumer errorHandler;
+ private volatile boolean completed = false;
+
+ public SSEEventListener(Consumer eventHandler,
+ Consumer errorHandler) {
+ this.eventHandler = eventHandler;
+ this.errorHandler = errorHandler;
+ }
+
+ public void onMessage(String message, Future completableFuture) {
+ try {
+ handleMessage(JsonParser.parseString(message).getAsJsonObject(), completableFuture);
+ } catch (JsonSyntaxException e) {
+ log.warning("Failed to parse JSON message: " + message);
+ } catch (JsonProcessingException e) {
+ log.warning("Failed to process JSON message: " + message);
+ } catch (IllegalArgumentException e) {
+ log.warning("Invalid message format: " + message);
+ if (errorHandler != null) {
+ errorHandler.accept(e);
+ }
+ completableFuture.cancel(true); // close SSE channel
+ }
+ }
+
+ public void onError(Throwable throwable, Future future) {
+ if (errorHandler != null) {
+ errorHandler.accept(throwable);
+ }
+ future.cancel(true); // close SSE channel
+ }
+
+ public void onComplete() {
+ // Idempotent: only signal completion once, even if called multiple times
+ if (completed) {
+ log.fine("SSEEventListener.onComplete() called again - ignoring (already completed)");
+ return;
+ }
+ completed = true;
+
+ // Signal normal stream completion (null error means successful completion)
+ log.fine("SSEEventListener.onComplete() called - signaling successful stream completion");
+ if (errorHandler != null) {
+ log.fine("Calling errorHandler.accept(null) to signal successful completion");
+ errorHandler.accept(null);
+ } else {
+ log.warning("errorHandler is null, cannot signal completion");
+ }
+ }
+
+ private void handleMessage(JsonObject jsonObject, Future future) throws JsonProcessingException {
+ if (jsonObject.has("error")) {
+ JSONRPCError error = JsonUtil.fromJson(jsonObject.get("error").toString(), JSONRPCError.class);
+ if (errorHandler != null) {
+ errorHandler.accept(error);
+ }
+ } else if (jsonObject.has("result")) {
+ // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent
+ String resultJson = jsonObject.get("result").toString();
+ StreamingEventKind event = JsonUtil.fromJson(resultJson, StreamingEventKind.class);
+ eventHandler.accept(event);
+ if (event instanceof TaskStatusUpdateEvent && ((TaskStatusUpdateEvent) event).isFinal()) {
+ future.cancel(true); // close SSE channel
+ }
+ } else {
+ throw new IllegalArgumentException("Unknown message type");
+ }
+ }
+
+}
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider b/compat-0.3/client/transport/jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider
new file mode 100644
index 000000000..30ec70de9
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider
@@ -0,0 +1 @@
+org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportProvider
\ No newline at end of file
diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java
new file mode 100644
index 000000000..cf7ae69c6
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java
@@ -0,0 +1,174 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages.SEND_MESSAGE_STREAMING_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages.SEND_MESSAGE_STREAMING_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages.TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages.TASK_RESUBSCRIPTION_TEST_REQUEST;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+import java.util.Collections;
+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 org.a2aproject.sdk.compat03.spec.Artifact;
+import org.a2aproject.sdk.compat03.spec.Message;
+import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams;
+import org.a2aproject.sdk.compat03.spec.Part;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind;
+import org.a2aproject.sdk.compat03.spec.Task;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams;
+import org.a2aproject.sdk.compat03.spec.TaskState;
+import org.a2aproject.sdk.compat03.spec.TextPart;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.matchers.MatchType;
+import org.mockserver.model.JsonBody;
+
+public class JSONRPCTransportStreamingTest {
+
+ private ClientAndServer server;
+
+ @BeforeEach
+ public void setUp() {
+ server = new ClientAndServer(4001);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ server.stop();
+ }
+
+ @Test
+ public void testSendStreamingMessageParams() {
+ // The goal here is just to verify the correct parameters are being used
+ // This is a unit test of the parameter construction, not the streaming itself
+ Message message = new Message.Builder()
+ .role(Message.Role.USER)
+ .parts(Collections.singletonList(new TextPart("test message")))
+ .contextId("context-test")
+ .messageId("message-test")
+ .build();
+
+ MessageSendConfiguration configuration = new MessageSendConfiguration.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(false)
+ .build();
+
+ MessageSendParams params = new MessageSendParams.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ assertNotNull(params);
+ assertEquals(message, params.message());
+ assertEquals(configuration, params.configuration());
+ assertEquals(Message.Role.USER, params.message().getRole());
+ assertEquals("test message", ((TextPart) params.message().getParts().get(0)).getText());
+ }
+
+ @Test
+ public void testA2AClientSendStreamingMessage() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SEND_MESSAGE_STREAMING_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withHeader("Content-Type", "text/event-stream")
+ .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ Message message = new Message.Builder()
+ .role(Message.Role.USER)
+ .parts(Collections.singletonList(new TextPart("tell me some jokes")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+ MessageSendConfiguration configuration = new MessageSendConfiguration.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(false)
+ .build();
+ MessageSendParams params = new MessageSendParams.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ AtomicReference receivedEvent = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ Consumer eventHandler = event -> {
+ receivedEvent.set(event);
+ latch.countDown();
+ };
+ Consumer errorHandler = error -> {};
+ client.sendMessageStreaming(params, eventHandler, errorHandler, null);
+
+ boolean eventReceived = latch.await(10, TimeUnit.SECONDS);
+ assertTrue(eventReceived);
+ assertNotNull(receivedEvent.get());
+ }
+
+ @Test
+ public void testA2AClientResubscribeToTask() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(TASK_RESUBSCRIPTION_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withHeader("Content-Type", "text/event-stream")
+ .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ TaskIdParams taskIdParams = new TaskIdParams("task-1234");
+
+ AtomicReference receivedEvent = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ Consumer eventHandler = event -> {
+ receivedEvent.set(event);
+ latch.countDown();
+ };
+ Consumer errorHandler = error -> {};
+ client.resubscribe(taskIdParams, eventHandler, errorHandler, null);
+
+ boolean eventReceived = latch.await(10, TimeUnit.SECONDS);
+ assertTrue(eventReceived);
+
+ StreamingEventKind eventKind = receivedEvent.get();;
+ assertNotNull(eventKind);
+ assertInstanceOf(Task.class, eventKind);
+ Task task = (Task) eventKind;
+ assertEquals("2", task.getId());
+ assertEquals("context-1234", task.getContextId());
+ assertEquals(TaskState.COMPLETED, task.getStatus().state());
+ List artifacts = task.getArtifacts();
+ assertEquals(1, artifacts.size());
+ Artifact artifact = artifacts.get(0);
+ assertEquals("artifact-1", artifact.artifactId());
+ assertEquals("joke", artifact.name());
+ Part> part = artifact.parts().get(0);
+ assertEquals(Part.Kind.TEXT, part.getKind());
+ assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).getText());
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportTest.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportTest.java
new file mode 100644
index 000000000..f82f2eacf
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportTest.java
@@ -0,0 +1,683 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.AGENT_CARD;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.AGENT_CARD_SUPPORTS_EXTENDED;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.CANCEL_TASK_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.CANCEL_TASK_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.GET_TASK_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.GET_TASK_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_ERROR_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_ERROR_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
+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.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientException;
+import org.a2aproject.sdk.compat03.spec.AgentCard;
+import org.a2aproject.sdk.compat03.spec.AgentInterface;
+import org.a2aproject.sdk.compat03.spec.AgentSkill;
+import org.a2aproject.sdk.compat03.spec.Artifact;
+import org.a2aproject.sdk.compat03.spec.DataPart;
+import org.a2aproject.sdk.compat03.spec.EventKind;
+import org.a2aproject.sdk.compat03.spec.FileContent;
+import org.a2aproject.sdk.compat03.spec.FilePart;
+import org.a2aproject.sdk.compat03.spec.FileWithBytes;
+import org.a2aproject.sdk.compat03.spec.FileWithUri;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams;
+import org.a2aproject.sdk.compat03.spec.Message;
+import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams;
+import org.a2aproject.sdk.compat03.spec.OpenIdConnectSecurityScheme;
+import org.a2aproject.sdk.compat03.spec.Part;
+import org.a2aproject.sdk.compat03.spec.PushNotificationAuthenticationInfo;
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig;
+import org.a2aproject.sdk.compat03.spec.SecurityScheme;
+import org.a2aproject.sdk.compat03.spec.Task;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams;
+import org.a2aproject.sdk.compat03.spec.TaskState;
+import org.a2aproject.sdk.compat03.spec.TextPart;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.matchers.MatchType;
+import org.mockserver.model.JsonBody;
+
+public class JSONRPCTransportTest {
+
+ private ClientAndServer server;
+
+ @BeforeEach
+ public void setUp() {
+ server = new ClientAndServer(4001);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ server.stop();
+ }
+
+ @Test
+ public void testA2AClientSendMessage() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(SEND_MESSAGE_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ Message message = new Message.Builder()
+ .role(Message.Role.USER)
+ .parts(Collections.singletonList(new TextPart("tell me a joke")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+ MessageSendConfiguration configuration = new MessageSendConfiguration.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(true)
+ .build();
+ MessageSendParams params = new MessageSendParams.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ EventKind result = client.sendMessage(params, null);
+ assertInstanceOf(Task.class, result);
+ Task task = (Task) result;
+ assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.getId());
+ assertNotNull(task.getContextId());
+ assertEquals(TaskState.COMPLETED,task.getStatus().state());
+ assertEquals(1, task.getArtifacts().size());
+ Artifact artifact = task.getArtifacts().get(0);
+ assertEquals("artifact-1", artifact.artifactId());
+ assertEquals("joke", artifact.name());
+ assertEquals(1, artifact.parts().size());
+ Part> part = artifact.parts().get(0);
+ assertEquals(Part.Kind.TEXT, part.getKind());
+ assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).getText());
+ assertTrue(task.getMetadata().isEmpty());
+ }
+
+ @Test
+ public void testA2AClientSendMessageWithMessageResponse() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ Message message = new Message.Builder()
+ .role(Message.Role.USER)
+ .parts(Collections.singletonList(new TextPart("tell me a joke")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+ MessageSendConfiguration configuration = new MessageSendConfiguration.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(true)
+ .build();
+ MessageSendParams params = new MessageSendParams.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ EventKind result = client.sendMessage(params, null);
+ assertInstanceOf(Message.class, result);
+ Message agentMessage = (Message) result;
+ assertEquals(Message.Role.AGENT, agentMessage.getRole());
+ Part> part = agentMessage.getParts().get(0);
+ assertEquals(Part.Kind.TEXT, part.getKind());
+ assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).getText());
+ assertEquals("msg-456", agentMessage.getMessageId());
+ }
+
+
+ @Test
+ public void testA2AClientSendMessageWithError() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SEND_MESSAGE_WITH_ERROR_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(SEND_MESSAGE_ERROR_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ Message message = new Message.Builder()
+ .role(Message.Role.USER)
+ .parts(Collections.singletonList(new TextPart("tell me a joke")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+ MessageSendConfiguration configuration = new MessageSendConfiguration.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(true)
+ .build();
+ MessageSendParams params = new MessageSendParams.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ try {
+ client.sendMessage(params, null);
+ fail(); // should not reach here
+ } catch (A2AClientException e) {
+ assertTrue(e.getMessage().contains("Invalid parameters: Hello world"));
+ }
+ }
+
+ @Test
+ public void testA2AClientGetTask() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(GET_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(GET_TASK_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ Task task = client.getTask(new TaskQueryParams("de38c76d-d54c-436c-8b9f-4c2703648d64",
+ 10), null);
+ assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.getId());
+ assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.getContextId());
+ assertEquals(TaskState.COMPLETED, task.getStatus().state());
+ assertEquals(1, task.getArtifacts().size());
+ Artifact artifact = task.getArtifacts().get(0);
+ assertEquals(1, artifact.parts().size());
+ assertEquals("artifact-1", artifact.artifactId());
+ Part> part = artifact.parts().get(0);
+ assertEquals(Part.Kind.TEXT, part.getKind());
+ assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).getText());
+ assertTrue(task.getMetadata().isEmpty());
+ List history = task.getHistory();
+ assertNotNull(history);
+ assertEquals(1, history.size());
+ Message message = history.get(0);
+ assertEquals(Message.Role.USER, message.getRole());
+ List> parts = message.getParts();
+ assertNotNull(parts);
+ assertEquals(3, parts.size());
+ part = parts.get(0);
+ assertEquals(Part.Kind.TEXT, part.getKind());
+ assertEquals("tell me a joke", ((TextPart)part).getText());
+ part = parts.get(1);
+ assertEquals(Part.Kind.FILE, part.getKind());
+ FileContent filePart = ((FilePart) part).getFile();
+ assertEquals("file:///path/to/file.txt", ((FileWithUri) filePart).uri());
+ assertEquals("text/plain", filePart.mimeType());
+ part = parts.get(2);
+ assertEquals(Part.Kind.FILE, part.getKind());
+ filePart = ((FilePart) part).getFile();
+ assertEquals("aGVsbG8=", ((FileWithBytes) filePart).bytes());
+ assertEquals("hello.txt", filePart.name());
+ assertTrue(task.getMetadata().isEmpty());
+ }
+
+ @Test
+ public void testA2AClientCancelTask() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(CANCEL_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(CANCEL_TASK_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ Task task = client.cancelTask(new TaskIdParams("de38c76d-d54c-436c-8b9f-4c2703648d64",
+ new HashMap<>()), null);
+ assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.getId());
+ assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.getContextId());
+ assertEquals(TaskState.CANCELED, task.getStatus().state());
+ assertTrue(task.getMetadata().isEmpty());
+ }
+
+ @Test
+ public void testA2AClientGetTaskPushNotificationConfig() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ TaskPushNotificationConfig taskPushNotificationConfig = client.getTaskPushNotificationConfiguration(
+ new GetTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64", null,
+ new HashMap<>()), null);
+ PushNotificationConfig pushNotificationConfig = taskPushNotificationConfig.pushNotificationConfig();
+ assertNotNull(pushNotificationConfig);
+ assertEquals("https://example.com/callback", pushNotificationConfig.url());
+ PushNotificationAuthenticationInfo authenticationInfo = pushNotificationConfig.authentication();
+ assertTrue(authenticationInfo.schemes().size() == 1);
+ assertEquals("jwt", authenticationInfo.schemes().get(0));
+ }
+
+ @Test
+ public void testA2AClientSetTaskPushNotificationConfig() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ TaskPushNotificationConfig taskPushNotificationConfig = client.setTaskPushNotificationConfiguration(
+ new TaskPushNotificationConfig("de38c76d-d54c-436c-8b9f-4c2703648d64",
+ new PushNotificationConfig.Builder()
+ .url("https://example.com/callback")
+ .authenticationInfo(new PushNotificationAuthenticationInfo(Collections.singletonList("jwt"),
+ null))
+ .build()), null);
+ PushNotificationConfig pushNotificationConfig = taskPushNotificationConfig.pushNotificationConfig();
+ assertNotNull(pushNotificationConfig);
+ assertEquals("https://example.com/callback", pushNotificationConfig.url());
+ PushNotificationAuthenticationInfo authenticationInfo = pushNotificationConfig.authentication();
+ assertEquals(1, authenticationInfo.schemes().size());
+ assertEquals("jwt", authenticationInfo.schemes().get(0));
+ }
+
+
+ @Test
+ public void testA2AClientGetAgentCard() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("GET")
+ .withPath("/.well-known/agent-card.json")
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(AGENT_CARD)
+ );
+
+ JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
+ AgentCard agentCard = client.getAgentCard(null);
+ assertEquals("GeoSpatial Route Planner Agent", agentCard.name());
+ assertEquals("Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", agentCard.description());
+ assertEquals("https://georoute-agent.example.com/a2a/v1", agentCard.url());
+ assertEquals("Example Geo Services Inc.", agentCard.provider().organization());
+ assertEquals("https://www.examplegeoservices.com", agentCard.provider().url());
+ assertEquals("1.2.0", agentCard.version());
+ assertEquals("https://docs.examplegeoservices.com/georoute-agent/api", agentCard.documentationUrl());
+ assertTrue(agentCard.capabilities().streaming());
+ assertTrue(agentCard.capabilities().pushNotifications());
+ assertFalse(agentCard.capabilities().stateTransitionHistory());
+ Map securitySchemes = agentCard.securitySchemes();
+ assertNotNull(securitySchemes);
+ OpenIdConnectSecurityScheme google = (OpenIdConnectSecurityScheme) securitySchemes.get("google");
+ assertEquals("openIdConnect", google.getType());
+ assertEquals("https://accounts.google.com/.well-known/openid-configuration", google.getOpenIdConnectUrl());
+ List