> parts, Message.Role role, String messageId, String contextId, String taskId) {
- Message.Builder messageBuilder = Message.builder()
- .role(role)
- .parts(parts)
- .contextId(contextId)
- .taskId(taskId);
- if (messageId != null) {
- messageBuilder.messageId(messageId);
- }
- return messageBuilder.build();
- }
-
- /**
- * Retrieve the agent card for an A2A agent.
- *
- * This is the standard way to discover an agent's capabilities before creating a client.
- * The agent card is fetched from the well-known endpoint: {@code /.well-known/agent-card.json}
- *
- * Example:
- *
{@code
- * // Get agent card
- * AgentCard card = A2A.getAgentCard("http://localhost:9999");
- *
- * // Check capabilities
- * System.out.println("Agent: " + card.name());
- * System.out.println("Supports streaming: " + card.capabilities().streaming());
- *
- * // Create client
- * Client client = Client.builder(card)
- * .withTransport(...)
- * .build();
- * }
- *
- * @param agentUrl the base URL for the agent whose agent card we want to retrieve
- * @return the agent card
- * @throws io.a2a.spec.A2AClientError if an HTTP error occurs fetching the card
- * @throws io.a2a.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema
- * @see #getAgentCard(A2AHttpClient, String)
- * @see #getAgentCard(String, String, java.util.Map)
- * @see AgentCard
- */
- public static AgentCard getAgentCard(String agentUrl) throws A2AClientError, A2AClientJSONError {
- return getAgentCard(A2AHttpClientFactory.create(), agentUrl);
- }
-
- /**
- * Retrieve the agent card using a custom HTTP client.
- *
- * Use this variant when you need to customize HTTP behavior (timeouts, SSL configuration,
- * connection pooling, etc.).
- *
- * Example:
- *
{@code
- * A2AHttpClient customClient = new CustomHttpClient()
- * .withTimeout(Duration.ofSeconds(10))
- * .withSSLContext(mySSLContext);
- *
- * AgentCard card = A2A.getAgentCard(customClient, "https://secure-agent.com");
- * }
- *
- * @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 io.a2a.spec.A2AClientError if an HTTP error occurs fetching the card
- * @throws io.a2a.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema
- * @see io.a2a.client.http.A2AHttpClient
- */
- public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl) throws A2AClientError, A2AClientJSONError {
- return getAgentCard(httpClient, agentUrl, null, null);
- }
-
- /**
- * Retrieve the agent card with custom path and authentication.
- *
- * Use this variant when:
- *
- * - The agent card is at a non-standard location
- * - Authentication is required to access the agent card
- *
- *
- * Example with authentication:
- *
{@code
- * Map authHeaders = Map.of(
- * "Authorization", "Bearer my-api-token",
- * "X-API-Key", "my-api-key"
- * );
- *
- * AgentCard card = A2A.getAgentCard(
- * "https://secure-agent.com",
- * null, // Use default path
- * authHeaders
- * );
- * }
- *
- * Example with custom path:
- *
{@code
- * AgentCard card = A2A.getAgentCard(
- * "https://agent.com",
- * "api/v2/agent-info", // Custom path
- * null // No auth needed
- * );
- * // Fetches from: https://agent.com/api/v2/agent-info
- * }
- *
- * @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 io.a2a.spec.A2AClientError if an HTTP error occurs fetching the card
- * @throws io.a2a.spec.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(A2AHttpClientFactory.create(), agentUrl, relativeCardPath, authHeaders);
- }
-
- /**
- * Retrieve the agent card with full customization options.
- *
- * This is the most flexible variant, allowing customization of:
- *
- * - HTTP client implementation
- * - Agent card endpoint path
- * - Authentication headers
- *
- *
- * Example:
- *
{@code
- * A2AHttpClient customClient = new CustomHttpClient();
- * Map authHeaders = Map.of("Authorization", "Bearer token");
- *
- * AgentCard card = A2A.getAgentCard(
- * customClient,
- * "https://agent.com",
- * "custom/agent-card",
- * authHeaders
- * );
- * }
- *
- * @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 io.a2a.spec.A2AClientError if an HTTP error occurs fetching the card
- * @throws io.a2a.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema
- */
- public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError {
- A2ACardResolver resolver = new A2ACardResolver(httpClient, agentUrl, "", relativeCardPath, authHeaders);
- return resolver.getAgentCard();
- }
-}
diff --git a/client/base/src/main/java/io/a2a/client/AbstractClient.java b/client/base/src/main/java/io/a2a/client/AbstractClient.java
deleted file mode 100644
index 27cbadc7d..000000000
--- a/client/base/src/main/java/io/a2a/client/AbstractClient.java
+++ /dev/null
@@ -1,440 +0,0 @@
-package io.a2a.client;
-
-import static io.a2a.util.Assert.checkNotNullParam;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-
-import io.a2a.client.transport.spi.interceptors.ClientCallContext;
-import io.a2a.jsonrpc.common.wrappers.ListTasksResult;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
-import io.a2a.spec.GetTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigResult;
-import io.a2a.spec.ListTasksParams;
-import io.a2a.spec.Message;
-import io.a2a.spec.MessageSendParams;
-import io.a2a.spec.PushNotificationConfig;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskPushNotificationConfig;
-import io.a2a.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 implements AutoCloseable {
-
- protected final @NonNull List> consumers;
- protected final @Nullable Consumer streamingErrorHandler;
-
- public AbstractClient(@NonNull 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(@NonNull 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
- * @throws A2AClientException if sending the message fails for any reason
- */
- public void sendMessage(@NonNull Message request,
- @Nullable ClientCallContext context) throws A2AClientException {
- sendMessage(request, consumers, streamingErrorHandler, context);
- }
-
- /**
- * 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(@NonNull Message request,
- @NonNull List> consumers,
- @Nullable 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
- * @throws A2AClientException if sending the message fails for any reason
- */
- public abstract void sendMessage(@NonNull Message request,
- @NonNull List> consumers,
- @Nullable 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(@NonNull Message request,
- @Nullable PushNotificationConfig pushNotificationConfiguration,
- @Nullable 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
- * @throws A2AClientException if sending the message fails for any reason
- */
- public abstract void sendMessage(@NonNull Message request,
- @Nullable PushNotificationConfig pushNotificationConfiguration,
- @Nullable Map metadata,
- @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 params the request parameters
- * @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
- * @throws A2AClientException if sending the message fails for any reason
- */
- public abstract void sendMessage(@NonNull MessageSendParams params,
- @NonNull List> consumers,
- @Nullable Consumer streamingErrorHandler,
- @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;
-
- /**
- * List tasks with optional filtering and pagination.
- *
- * @param request the list tasks parameters including filters and pagination
- * @return the list tasks result containing tasks and pagination information
- * @throws A2AClientException if listing tasks fails for any reason
- */
- public ListTasksResult listTasks(ListTasksParams request) throws A2AClientException {
- return listTasks(request, null);
- }
-
- /**
- * List tasks with optional filtering and pagination.
- *
- * @param request the list tasks parameters including filters and pagination
- * @param context optional client call context for the request (may be {@code null})
- * @return the list tasks result containing tasks and pagination information
- * @throws A2AClientException if listing tasks fails for any reason
- */
- public abstract ListTasksResult listTasks(ListTasksParams 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;
-
- /**
- * Create 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 createTaskPushNotificationConfiguration(
- TaskPushNotificationConfig request) throws A2AClientException {
- return createTaskPushNotificationConfiguration(request, null);
- }
-
- /**
- * Create 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 createTaskPushNotificationConfiguration(
- 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 with pagination support.
- *
- * @param request the parameters specifying which task's notification configs to retrieve
- * @return the result containing the list of task push notification configs and pagination information
- * @throws A2AClientException if getting the task push notification configs fails for any reason
- */
- public ListTaskPushNotificationConfigResult listTaskPushNotificationConfigurations(
- ListTaskPushNotificationConfigParams request) throws A2AClientException {
- return listTaskPushNotificationConfigurations(request, null);
- }
-
- /**
- * Retrieve the list of push notification configurations for a specific task with pagination support.
- *
- * @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 result containing the list of task push notification configs and pagination information
- * @throws A2AClientException if getting the task push notification configs fails for any reason
- */
- public abstract ListTaskPushNotificationConfigResult 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;
-
- /**
- * Subscribe 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 subscribeToTask(@NonNull TaskIdParams request) throws A2AClientException {
- subscribeToTask(request, consumers, streamingErrorHandler, null);
- }
-
- /**
- * Subscribe 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
- * @throws A2AClientException if resubscribing fails for any reason
- */
- public void subscribeToTask(@NonNull TaskIdParams request,
- @Nullable ClientCallContext context) throws A2AClientException {
- subscribeToTask(request, consumers, streamingErrorHandler, context);
- }
-
- /**
- * Subscribe 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 subscribeToTask(@NonNull TaskIdParams request,
- @NonNull List> consumers,
- @Nullable Consumer streamingErrorHandler) throws A2AClientException {
- subscribeToTask(request, consumers, streamingErrorHandler, null);
- }
-
- /**
- * Subscribe 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
- * @throws A2AClientException if resubscribing fails for any reason
- */
- public abstract void subscribeToTask(@NonNull TaskIdParams request,
- @NonNull List> consumers,
- @Nullable Consumer streamingErrorHandler,
- @Nullable ClientCallContext context) throws A2AClientException;
-
- /**
- * Retrieve the extended AgentCard.
- *
- * @return the extended AgentCard
- * @throws A2AClientException if retrieving the extended agent card fails for any reason
- */
- public AgentCard getExtendedAgentCard() throws A2AClientException {
- return getExtendedAgentCard(null);
- }
-
- /**
- * Retrieve the extended AgentCard.
- *
- * @param context optional client call context for the request (may be {@code null})
- * @return the extended AgentCard
- * @throws A2AClientException if retrieving the extended agent card fails for any reason
- */
- public abstract AgentCard getExtendedAgentCard(@Nullable ClientCallContext context) throws A2AClientException;
-
- /**
- * Close the transport and release any associated resources.
- */
- public abstract void close();
-
- /**
- * 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/client/base/src/main/java/io/a2a/client/Client.java b/client/base/src/main/java/io/a2a/client/Client.java
deleted file mode 100644
index 70ed9cf18..000000000
--- a/client/base/src/main/java/io/a2a/client/Client.java
+++ /dev/null
@@ -1,722 +0,0 @@
-package io.a2a.client;
-
-import static io.a2a.util.Assert.checkNotNullParam;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-
-import io.a2a.client.config.ClientConfig;
-import io.a2a.client.transport.spi.ClientTransport;
-import io.a2a.client.transport.spi.interceptors.ClientCallContext;
-import io.a2a.jsonrpc.common.wrappers.ListTasksResult;
-import io.a2a.spec.A2AClientError;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.A2AClientInvalidStateError;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
-import io.a2a.spec.EventKind;
-import io.a2a.spec.GetTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigResult;
-import io.a2a.spec.ListTasksParams;
-import io.a2a.spec.Message;
-import io.a2a.spec.MessageSendConfiguration;
-import io.a2a.spec.MessageSendParams;
-import io.a2a.spec.PushNotificationConfig;
-import io.a2a.spec.StreamingEventKind;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskArtifactUpdateEvent;
-import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskPushNotificationConfig;
-import io.a2a.spec.TaskQueryParams;
-import io.a2a.spec.TaskStatusUpdateEvent;
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
-/**
- * A client for communicating with A2A agents using the Agent2Agent Protocol.
- *
- * The Client class provides the primary API for sending messages to agents, managing tasks,
- * configuring push notifications, and subscribing to task updates. It abstracts the underlying
- * transport protocol (JSON-RPC, gRPC, REST) and provides a consistent interface for all
- * agent interactions.
- *
- * Key capabilities:
- *
- * - Message exchange: Send messages to agents and receive responses via event consumers
- * - Task management: Query, list, and cancel tasks
- * - Streaming support: Real-time event streaming when both client and server support it
- * - Push notifications: Configure webhooks for task state changes
- * - Resubscription: Resume receiving events for ongoing tasks after disconnection
- *
- *
- * Resource management: Client implements {@link AutoCloseable} and should be used with
- * try-with-resources to ensure proper cleanup:
- *
{@code
- * AgentCard card = A2A.getAgentCard("http://localhost:9999");
- *
- * try (Client client = Client.builder(card)
- * .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
- * .addConsumer((event, agentCard) -> {
- * if (event instanceof MessageEvent me) {
- * System.out.println("Response: " + me.getMessage().parts());
- * }
- * })
- * .build()) {
- *
- * // Send messages - client automatically closed when done
- * client.sendMessage(A2A.toUserMessage("Tell me a joke"));
- * }
- * }
- *
- * Manual resource management: If not using try-with-resources, call {@link #close()}
- * explicitly when done:
- *
{@code
- * Client client = Client.builder(card)
- * .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
- * .addConsumer((event, agentCard) -> {
- * // Handle events
- * })
- * .build();
- *
- * try {
- * client.sendMessage(A2A.toUserMessage("Tell me a joke"));
- * } finally {
- * client.close(); // Always close to release resources
- * }
- * }
- *
- * Event consumption model: Responses from the agent are delivered as {@link ClientEvent}
- * instances to the registered consumers:
- *
- * - {@link MessageEvent} - contains agent response messages with content parts
- * - {@link TaskEvent} - contains complete task state (typically final state)
- * - {@link TaskUpdateEvent} - contains incremental task updates (status or artifact changes)
- *
- *
- * Streaming vs blocking: The client supports two communication modes:
- *
- * - Blocking: {@link #sendMessage} blocks until the agent completes the task
- * - Streaming: {@link #sendMessage} returns immediately, events delivered asynchronously
- * to consumers as the agent processes the request
- *
- * The mode is determined by {@link ClientConfig#isStreaming()} AND {@link io.a2a.spec.AgentCapabilities#streaming()}.
- * Both must be {@code true} for streaming mode; otherwise blocking mode is used.
- *
- * Task lifecycle example:
- *
{@code
- * client.addConsumer((event, card) -> {
- * if (event instanceof TaskUpdateEvent tue) {
- * TaskState state = tue.getTask().status().state();
- * switch (state) {
- * case SUBMITTED -> System.out.println("Task created");
- * case WORKING -> System.out.println("Agent is processing...");
- * case COMPLETED -> System.out.println("Task finished");
- * case FAILED -> System.err.println("Task failed: " +
- * tue.getTask().status().message());
- * }
- *
- * // Check for new artifacts
- * if (tue.getUpdateEvent() instanceof TaskArtifactUpdateEvent update) {
- * Artifact artifact = update.artifact();
- * System.out.println("New content: " + artifact.parts());
- * }
- * }
- * });
- * }
- *
- * Push notifications: Configure webhooks to receive task updates:
- *
{@code
- * // Configure push notifications for a task
- * PushNotificationConfig pushConfig = new PushNotificationConfig(
- * "https://my-app.com/webhooks/task-updates",
- * Map.of("Authorization", "Bearer my-token")
- * );
- *
- * // Send message with push notifications
- * client.sendMessage(
- * A2A.toUserMessage("Process this data"),
- * pushConfig,
- * null, // metadata
- * null // context
- * );
- * }
- *
- * Resubscription after disconnection:
- *
{@code
- * // Original request
- * client.sendMessage(A2A.toUserMessage("Long-running task"));
- * // ... client disconnects ...
- *
- * // Later, reconnect and resume receiving events
- * String taskId = "task-123"; // From original request
- * client.subscribeToTask(
- * new TaskIdParams(taskId),
- * List.of((event, card) -> {
- * // Process events from where we left off
- * }),
- * null, // error handler
- * null // context
- * );
- * }
- *
- * Thread safety: Client instances are thread-safe and can be used concurrently from
- * multiple threads. Event consumers must also be thread-safe as they may be invoked concurrently
- * for different tasks.
- *
- * Resource management: Clients hold resources (HTTP connections, gRPC channels, etc.)
- * and should be closed when no longer needed:
- *
{@code
- * try (Client client = Client.builder(card)...build()) {
- * client.sendMessage(...);
- * } // Automatically closed
- * }
- *
- * @see ClientBuilder
- * @see ClientEvent
- * @see MessageEvent
- * @see TaskEvent
- * @see TaskUpdateEvent
- * @see io.a2a.A2A
- */
-public class Client extends AbstractClient {
-
- private final ClientConfig clientConfig;
- private final ClientTransport clientTransport;
- private AgentCard agentCard;
-
- /**
- * Package-private constructor used by {@link ClientBuilder#build()}.
- *
- * @param agentCard the agent card for the target agent
- * @param clientConfig the client configuration
- * @param clientTransport the transport protocol implementation
- * @param consumers the event consumers
- * @param streamingErrorHandler the error handler for streaming scenarios
- */
- 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;
- }
-
- /**
- * Create a new builder for constructing a client instance.
- *
- * This is the primary entry point for creating clients. The builder provides a fluent
- * API for configuring transports, event consumers, and client behavior.
- *
- * Example:
- *
{@code
- * AgentCard card = A2A.getAgentCard("http://localhost:9999");
- * Client client = Client.builder(card)
- * .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
- * .addConsumer((event, agentCard) -> processEvent(event))
- * .build();
- * }
- *
- * @param agentCard the agent card describing the agent to communicate with
- * @return a new builder instance
- * @see ClientBuilder
- */
- public static ClientBuilder builder(AgentCard agentCard) {
- return new ClientBuilder(agentCard);
- }
-
- @Override
- public void sendMessage(@NonNull Message request,
- @NonNull List> consumers,
- @Nullable Consumer streamingErrorHandler,
- @Nullable ClientCallContext context) throws A2AClientException {
- MessageSendConfiguration messageSendConfiguration = createMessageSendConfiguration(clientConfig.getPushNotificationConfig());
-
- MessageSendParams messageSendParams = MessageSendParams.builder()
- .message(request)
- .configuration(messageSendConfiguration)
- .metadata(clientConfig.getMetadata())
- .build();
- sendMessage(messageSendParams, consumers, streamingErrorHandler, context);
- }
-
- /**
- * Send a message to the agent.
- *
- * This is the primary method for communicating with an agent. The behavior depends on
- * whether streaming is enabled:
- *
- * - Streaming mode: Returns immediately, events delivered asynchronously to consumers
- * - Blocking mode: Blocks until the agent completes the task, then invokes consumers
- *
- * Streaming mode is active when both {@link ClientConfig#isStreaming()} AND
- * {@link io.a2a.spec.AgentCapabilities#streaming()} are {@code true}.
- *
- * Simple example:
- *
{@code
- * Message userMessage = A2A.toUserMessage("What's the weather?");
- * client.sendMessage(userMessage, null, null, null);
- * // Events delivered to consumers registered during client construction
- * }
- *
- * With push notifications:
- *
{@code
- * PushNotificationConfig pushConfig = new PushNotificationConfig(
- * "https://my-app.com/webhook",
- * Map.of("Authorization", "Bearer token")
- * );
- * client.sendMessage(userMessage, pushConfig, null, null);
- * }
- *
- * With metadata:
- *
{@code
- * Map metadata = Map.of(
- * "userId", "user-123",
- * "sessionId", "session-456"
- * );
- * client.sendMessage(userMessage, null, metadata, null);
- * }
- *
- * @param request the message to send (required)
- * @param pushNotificationConfiguration webhook configuration for task updates (optional)
- * @param metadata custom metadata to attach to the request (optional)
- * @param context custom call context for request interceptors (optional)
- * @throws A2AClientException if the message cannot be sent or if the agent returns an error
- * @see #sendMessage(Message, List, Consumer, ClientCallContext)
- * @see PushNotificationConfig
- */
- @Override
- public void sendMessage(@NonNull Message request,
- @Nullable PushNotificationConfig pushNotificationConfiguration,
- @Nullable Map metadata,
- @Nullable ClientCallContext context) throws A2AClientException {
- MessageSendConfiguration messageSendConfiguration = createMessageSendConfiguration(pushNotificationConfiguration);
- MessageSendParams messageSendParams = MessageSendParams.builder()
- .message(request)
- .configuration(messageSendConfiguration)
- .metadata(metadata)
- .build();
-
- sendMessage(messageSendParams, consumers, streamingErrorHandler, context);
- }
-
- @Override
- public void sendMessage(@NonNull MessageSendParams messageSendParams,
- @NonNull List> consumers,
- @Nullable Consumer streamingErrorHandler,
- @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(streamingErrorHandler);
- 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);
- }
- }
-
-
- /**
- * Retrieve a specific task by ID.
- *
- * This method queries the agent for the current state of a task. It's useful for:
- *
- * - Checking the status of a task after disconnection
- * - Retrieving task results without subscribing to events
- * - Polling for task completion (when streaming is not available)
- *
- *
- * Example:
- *
{@code
- * Task task = client.getTask(new TaskQueryParams("task-123"));
- * if (task.status().state() == TaskState.COMPLETED) {
- * Artifact result = task.artifact();
- * System.out.println("Result: " + result.parts());
- * } else if (task.status().state() == TaskState.FAILED) {
- * System.err.println("Task failed: " + task.status().message());
- * }
- * }
- *
- * @param request the task query parameters containing the task ID
- * @param context custom call context for request interceptors (optional)
- * @return the current task state
- * @throws A2AClientException if the task is not found or if a communication error occurs
- * @see TaskQueryParams
- * @see Task
- */
- @Override
- public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException {
- return clientTransport.getTask(request, context);
- }
-
- /**
- * List tasks for the current session or context.
- *
- * This method retrieves multiple tasks based on filter criteria. Useful for:
- *
- * - Viewing all tasks in a session/context
- * - Finding tasks by state (e.g., all failed tasks)
- * - Paginating through large task lists
- *
- *
- * Example:
- *
{@code
- * // List all tasks for a context
- * ListTasksParams params = new ListTasksParams(
- * "session-123", // contextId
- * null, // state filter (null = all states)
- * 10, // limit
- * null // offset
- * );
- * ListTasksResult result = client.listTasks(params);
- * for (Task task : result.tasks()) {
- * System.out.println(task.id() + ": " + task.status().state());
- * }
- * }
- *
- * @param request the list parameters with optional filters
- * @param context custom call context for request interceptors (optional)
- * @return the list of tasks matching the criteria
- * @throws A2AClientException if a communication error occurs
- * @see ListTasksParams
- * @see ListTasksResult
- */
- @Override
- public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException {
- return clientTransport.listTasks(request, context);
- }
-
- /**
- * Request cancellation of a task.
- *
- * This method sends a cancellation request to the agent for the specified task. The agent
- * may or may not honor the request depending on its implementation and the task's current state.
- *
- * Important notes:
- *
- * - Cancellation is a request, not a guarantee - agents may decline or be unable to cancel
- * - Some agents don't support cancellation and will return {@link io.a2a.spec.UnsupportedOperationError}
- * - Tasks in final states (COMPLETED, FAILED, CANCELED) cannot be canceled
- * - The returned task will have state CANCELED if the cancellation succeeded
- *
- *
- * Example:
- *
{@code
- * try {
- * Task canceledTask = client.cancelTask(new TaskIdParams("task-123"));
- * if (canceledTask.status().state() == TaskState.CANCELED) {
- * System.out.println("Task successfully canceled");
- * }
- * } catch (A2AClientException e) {
- * if (e.getCause() instanceof UnsupportedOperationError) {
- * System.err.println("Agent does not support cancellation");
- * } else if (e.getCause() instanceof TaskNotFoundError) {
- * System.err.println("Task not found");
- * }
- * }
- * }
- *
- * @param request the task ID to cancel
- * @param context custom call context for request interceptors (optional)
- * @return the task with CANCELED status if successful
- * @throws A2AClientException if the task cannot be canceled or if a communication error occurs
- * @see TaskIdParams
- * @see io.a2a.spec.UnsupportedOperationError
- * @see io.a2a.spec.TaskNotFoundError
- */
- @Override
- public Task cancelTask(TaskIdParams request, @Nullable ClientCallContext context) throws A2AClientException {
- return clientTransport.cancelTask(request, context);
- }
-
- /**
- * Configure push notifications for a task.
- *
- * Push notifications allow your application to receive task updates via webhook instead
- * of maintaining an active connection. When configured, the agent will POST events to
- * the specified URL as the task progresses.
- *
- * Example:
- *
{@code
- * TaskPushNotificationConfig config = new TaskPushNotificationConfig(
- * "task-123",
- * new PushNotificationConfig(
- * "https://my-app.com/webhooks/task-updates",
- * Map.of(
- * "Authorization", "Bearer my-webhook-secret",
- * "X-App-ID", "my-app"
- * )
- * )
- * );
- * client.createTaskPushNotificationConfiguration(config);
- * }
- *
- * @param request the push notification configuration for the task
- * @param context custom call context for request interceptors (optional)
- * @return the stored configuration (may include server-assigned IDs)
- * @throws A2AClientException if the configuration cannot be set
- * @see TaskPushNotificationConfig
- * @see PushNotificationConfig
- */
- @Override
- public TaskPushNotificationConfig createTaskPushNotificationConfiguration(
- TaskPushNotificationConfig request, @Nullable ClientCallContext context) throws A2AClientException {
- return clientTransport.createTaskPushNotificationConfiguration(request, context);
- }
-
- /**
- * Retrieve the push notification configuration for a task.
- *
- * Example:
- *
{@code
- * GetTaskPushNotificationConfigParams params =
- * new GetTaskPushNotificationConfigParams("task-123");
- * TaskPushNotificationConfig config =
- * client.getTaskPushNotificationConfiguration(params);
- * System.out.println("Webhook URL: " +
- * config.pushNotificationConfig().url());
- * }
- *
- * @param request the parameters specifying which task's configuration to retrieve
- * @param context custom call context for request interceptors (optional)
- * @return the push notification configuration for the task
- * @throws A2AClientException if the configuration cannot be retrieved
- * @see GetTaskPushNotificationConfigParams
- */
- @Override
- public TaskPushNotificationConfig getTaskPushNotificationConfiguration(
- GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
- return clientTransport.getTaskPushNotificationConfiguration(request, context);
- }
-
- /**
- * List all push notification configurations, optionally filtered by task or context.
- *
- * Example:
- *
{@code
- * // List all configurations for a context
- * ListTaskPushNotificationConfigParams params =
- * new ListTaskPushNotificationConfigParams("session-123", null, 10, null);
- * ListTaskPushNotificationConfigResult result =
- * client.listTaskPushNotificationConfigurations(params);
- * for (TaskPushNotificationConfig config : result.configurations()) {
- * System.out.println("Task " + config.taskId() + " -> " +
- * config.pushNotificationConfig().url());
- * }
- * }
- *
- * @param request the list parameters with optional filters
- * @param context custom call context for request interceptors (optional)
- * @return the list of push notification configurations
- * @throws A2AClientException if the configurations cannot be retrieved
- * @see ListTaskPushNotificationConfigParams
- */
- @Override
- public ListTaskPushNotificationConfigResult listTaskPushNotificationConfigurations(
- ListTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
- return clientTransport.listTaskPushNotificationConfigurations(request, context);
- }
-
- /**
- * Delete push notification configurations.
- *
- * This method removes push notification configurations for the specified tasks or context.
- * After deletion, the agent will stop sending webhook notifications for those tasks.
- *
- * Example:
- *
{@code
- * // Delete configuration for a specific task
- * DeleteTaskPushNotificationConfigParams params =
- * new DeleteTaskPushNotificationConfigParams(
- * null, // contextId (null = not filtering by context)
- * List.of("task-123", "task-456") // specific task IDs
- * );
- * client.deleteTaskPushNotificationConfigurations(params);
- * }
- *
- * @param request the delete parameters specifying which configurations to remove
- * @param context custom call context for request interceptors (optional)
- * @throws A2AClientException if the configurations cannot be deleted
- * @see DeleteTaskPushNotificationConfigParams
- */
- @Override
- public void deleteTaskPushNotificationConfigurations(
- DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
- clientTransport.deleteTaskPushNotificationConfigurations(request, context);
- }
-
- /**
- * Subscribe to an existing task to receive remaining events.
- *
- * This method is useful when a client disconnects during a long-running task and wants to
- * resume receiving events without starting a new task. The agent will deliver any events
- * that occurred since the original subscription.
- *
- * Requirements:
- *
- * - Both {@link ClientConfig#isStreaming()} and {@link io.a2a.spec.AgentCapabilities#streaming()}
- * must be {@code true}
- * - The task must still exist and not be in a final state (or the agent must support
- * historical event replay)
- *
- *
- * Example:
- *
{@code
- * // Original request (client1)
- * client1.sendMessage(A2A.toUserMessage("Analyze this dataset"));
- * String taskId = ...; // Save task ID from TaskEvent
- * // ... client1 disconnects ...
- *
- * // Later, reconnect (client2)
- * client2.subscribeToTask(
- * new TaskIdParams(taskId),
- * List.of((event, card) -> {
- * if (event instanceof TaskUpdateEvent tue) {
- * System.out.println("Resumed - status: " +
- * tue.getTask().status().state());
- * }
- * }),
- * throwable -> System.err.println("Subscribe error: " + throwable),
- * null
- * );
- * }
- *
- * @param request the task ID to subscribe to
- * @param consumers the event consumers for processing events (required)
- * @param streamingErrorHandler error handler for streaming errors (optional)
- * @param context custom call context for request interceptors (optional)
- * @throws A2AClientException if subscription is not supported or if the task cannot be found
- */
- @Override
- public void subscribeToTask(@NonNull TaskIdParams request,
- @NonNull List> consumers,
- @Nullable Consumer streamingErrorHandler,
- @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(streamingErrorHandler);
- Consumer eventHandler = event -> {
- try {
- ClientEvent clientEvent = getClientEvent(event, tracker);
- consume(clientEvent, agentCard, consumers);
- } catch (A2AClientError e) {
- overriddenErrorHandler.accept(e);
- }
- };
- clientTransport.subscribeToTask(request, eventHandler, overriddenErrorHandler, context);
- }
-
- /**
- * Retrieve the agent's extended agent card.
- *
- * This method fetches the extended agent card from the agent (if the extendedAgentCard capability is supported).
- * The card may have changed since
- * client construction (e.g., new skills added, capabilities updated). The client's internal
- * reference is updated to the newly retrieved card.
- *
- * Example:
- *
{@code
- * AgentCard updatedCard = client.getExtendedAgentCard(null);
- * System.out.println("Agent version: " + updatedCard.version());
- * System.out.println("Skills: " + updatedCard.skills().size());
- * }
- *
- * @param context custom call context for request interceptors (optional)
- * @return the agent's extended agent card
- * @throws A2AClientException if the extended agent card cannot be retrieved
- * @see AgentCard
- */
- @Override
- public AgentCard getExtendedAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
- agentCard = clientTransport.getExtendedAgentCard(context);
- return agentCard;
- }
-
- /**
- * Close this client and release all associated resources.
- *
- * This method closes the underlying transport (HTTP connections, gRPC channels, etc.)
- * and releases any other resources held by the client. After calling this method, the
- * client instance should not be used further.
- *
- * Important: Always close clients when done to avoid resource leaks:
- *
{@code
- * Client client = Client.builder(card)...build();
- * try {
- * client.sendMessage(...);
- * } finally {
- * client.close();
- * }
- * // Or use try-with-resources if Client implements AutoCloseable
- * }
- */
- @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 MessageSendConfiguration.builder()
- .acceptedOutputModes(clientConfig.getAcceptedOutputModes())
- .blocking(!clientConfig.isPolling())
- .historyLength(clientConfig.getHistoryLength())
- .pushNotificationConfig(pushNotificationConfig)
- .build();
- }
-
- 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, @NonNull List> consumers) {
- for (BiConsumer consumer : consumers) {
- consumer.accept(clientEvent, agentCard);
- }
- }
-}
diff --git a/client/base/src/main/java/io/a2a/client/ClientBuilder.java b/client/base/src/main/java/io/a2a/client/ClientBuilder.java
deleted file mode 100644
index 81c812c91..000000000
--- a/client/base/src/main/java/io/a2a/client/ClientBuilder.java
+++ /dev/null
@@ -1,434 +0,0 @@
-package io.a2a.client;
-
-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.ServiceLoader.Provider;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import io.a2a.client.config.ClientConfig;
-import io.a2a.client.transport.spi.ClientTransport;
-import io.a2a.client.transport.spi.ClientTransportConfig;
-import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
-import io.a2a.client.transport.spi.ClientTransportProvider;
-import io.a2a.client.transport.spi.ClientTransportWrapper;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentInterface;
-import io.a2a.spec.TransportProtocol;
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Builder for creating instances of {@link Client} to communicate with A2A agents.
- *
- * ClientBuilder provides a fluent API for configuring and creating client instances that
- * communicate with A2A agents. It handles transport negotiation, event consumer registration,
- * and client configuration in a type-safe manner.
- *
- * Key responsibilities:
- *
- * - Transport selection and negotiation between client and server capabilities
- * - Event consumer registration for processing agent responses
- * - Error handler configuration for streaming scenarios
- * - Client behavior configuration (streaming, polling, preferences)
- *
- *
- * Transport Selection: The builder automatically negotiates the best transport protocol
- * based on the agent's {@link AgentCard} and the client's configured transports. By default,
- * the server's preferred transport (first in {@link AgentCard#supportedInterfaces()}) is used.
- * This can be changed by setting {@link ClientConfig#isUseClientPreference()} to {@code true}.
- *
- * Typical usage pattern:
- *
{@code
- * // 1. Get the agent card
- * AgentCard card = A2A.getAgentCard("http://localhost:9999");
- *
- * // 2. Build client with transport and event consumer
- * Client client = Client.builder(card)
- * .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
- * .addConsumer((event, agentCard) -> {
- * if (event instanceof MessageEvent me) {
- * System.out.println("Received: " + me.getMessage().parts());
- * } else if (event instanceof TaskUpdateEvent tue) {
- * System.out.println("Task status: " + tue.getTask().status().state());
- * }
- * })
- * .build();
- *
- * // 3. Send messages
- * client.sendMessage(A2A.toUserMessage("Hello agent!"));
- * }
- *
- * Multiple transports: You can configure multiple transports for fallback:
- *
{@code
- * Client client = Client.builder(card)
- * .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder()
- * .channelFactory(ManagedChannelBuilder::forAddress))
- * .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
- * .clientConfig(new ClientConfig.Builder()
- * .setUseClientPreference(true) // Try client's preferred order
- * .build())
- * .build();
- * }
- *
- * Error handling: For streaming scenarios, configure an error handler to process exceptions:
- *
{@code
- * Client client = Client.builder(card)
- * .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
- * .streamingErrorHandler(throwable -> {
- * System.err.println("Stream error: " + throwable.getMessage());
- * })
- * .build();
- * }
- *
- * Thread safety: ClientBuilder is not thread-safe and should only be used from a single
- * thread during client construction. The resulting {@link Client} instance is thread-safe.
- *
- * @see Client
- * @see ClientConfig
- * @see ClientEvent
- * @see io.a2a.client.transport.spi.ClientTransport
- */
-public class ClientBuilder {
-
- private static final Map>> transportProviderRegistry = new HashMap<>();
- private static final Map, String> transportProtocolMapping = new HashMap<>();
- private static final Logger LOGGER = LoggerFactory.getLogger(ClientBuilder.class);
-
- 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<>();
-
- /**
- * Package-private constructor used by {@link Client#builder(AgentCard)}.
- *
- * @param agentCard the agent card for the agent this client will communicate with (required)
- */
- ClientBuilder(@NonNull AgentCard agentCard) {
- this.agentCard = agentCard;
- }
-
- /**
- * Configure a transport protocol using a builder for type-safe configuration.
- *
- * Multiple transports can be configured to support fallback scenarios. The actual transport
- * used is negotiated based on the agent's capabilities and the {@link ClientConfig}.
- *
- * Example:
- *
{@code
- * builder.withTransport(JSONRPCTransport.class,
- * new JSONRPCTransportConfigBuilder()
- * .httpClient(customHttpClient)
- * .addInterceptor(loggingInterceptor));
- * }
- *
- * @param clazz the transport class to configure
- * @param configBuilder the transport configuration builder
- * @param the transport type
- * @return this builder for method chaining
- */
- public ClientBuilder withTransport(Class clazz, ClientTransportConfigBuilder extends ClientTransportConfig, ?> configBuilder) {
- return withTransport(clazz, configBuilder.build());
- }
-
- /**
- * Configure a transport protocol with a pre-built configuration.
- *
- * Multiple transports can be configured to support fallback scenarios. The actual transport
- * used is negotiated based on the agent's capabilities and the {@link ClientConfig}.
- *
- * Example:
- *
{@code
- * JSONRPCTransportConfig config = new JSONRPCTransportConfig(myHttpClient);
- * builder.withTransport(JSONRPCTransport.class, config);
- * }
- *
- * @param clazz the transport class to configure
- * @param config the transport configuration
- * @param the transport type
- * @return this builder for method chaining
- */
- public ClientBuilder withTransport(Class clazz, ClientTransportConfig config) {
- clientTransports.put(clazz, config);
-
- return this;
- }
-
- /**
- * Add a single event consumer to process events from the agent.
- *
- * Consumers receive {@link ClientEvent} instances (MessageEvent, TaskEvent, TaskUpdateEvent)
- * along with the agent's {@link AgentCard}. Multiple consumers can be registered and will
- * be invoked in registration order.
- *
- * Example:
- *
{@code
- * builder.addConsumer((event, card) -> {
- * if (event instanceof MessageEvent me) {
- * String text = me.getMessage().parts().stream()
- * .filter(p -> p instanceof TextPart)
- * .map(p -> ((TextPart) p).text())
- * .collect(Collectors.joining());
- * System.out.println("Agent: " + text);
- * }
- * });
- * }
- *
- * @param consumer the event consumer to add
- * @return this builder for method chaining
- * @see ClientEvent
- * @see MessageEvent
- * @see TaskEvent
- * @see TaskUpdateEvent
- */
- public ClientBuilder addConsumer(BiConsumer consumer) {
- this.consumers.add(consumer);
- return this;
- }
-
- /**
- * Add multiple event consumers to process events from the agent.
- *
- * Consumers receive {@link ClientEvent} instances and are invoked in the order they
- * appear in the list.
- *
- * @param consumers the list of event consumers to add
- * @return this builder for method chaining
- * @see #addConsumer(BiConsumer)
- */
- public ClientBuilder addConsumers(List> consumers) {
- this.consumers.addAll(consumers);
- return this;
- }
-
- /**
- * Configure an error handler for streaming scenarios.
- *
- * This handler is invoked when errors occur during streaming event consumption. It's only
- * applicable when the client and agent both support streaming. For non-streaming scenarios,
- * errors are thrown directly as {@link A2AClientException}.
- *
- * Example:
- *
{@code
- * builder.streamingErrorHandler(throwable -> {
- * if (throwable instanceof A2AClientException e) {
- * log.error("A2A error: " + e.getMessage(), e);
- * } else {
- * log.error("Unexpected error: " + throwable.getMessage(), throwable);
- * }
- * });
- * }
- *
- * @param streamErrorHandler the error handler for streaming errors
- * @return this builder for method chaining
- */
- public ClientBuilder streamingErrorHandler(Consumer streamErrorHandler) {
- this.streamErrorHandler = streamErrorHandler;
- return this;
- }
-
- /**
- * Configure client behavior such as streaming mode, polling, and transport preference.
- *
- * The configuration controls how the client communicates with the agent:
- *
- * - Streaming vs blocking mode
- * - Polling for updates vs receiving events
- * - Client vs server transport preference
- * - Output modes, history length, and metadata
- *
- *
- * Example:
- *
{@code
- * ClientConfig config = new ClientConfig.Builder()
- * .setStreaming(true) // Enable streaming if server supports it
- * .setUseClientPreference(true) // Use client's transport order
- * .setHistoryLength(10) // Request last 10 messages of context
- * .build();
- * builder.clientConfig(config);
- * }
- *
- * @param clientConfig the client configuration
- * @return this builder for method chaining
- * @see ClientConfig
- */
- public ClientBuilder clientConfig(@NonNull ClientConfig clientConfig) {
- this.clientConfig = clientConfig;
- return this;
- }
-
- /**
- * Build the configured {@link Client} instance.
- *
- * This method performs transport negotiation between the client's configured transports
- * and the agent's {@link AgentCard#supportedInterfaces()}. The selection algorithm:
- *
- * - If {@link ClientConfig#isUseClientPreference()} is {@code true}, iterate through
- * client transports in registration order and select the first one the server supports
- * - Otherwise, iterate through server interfaces in preference order (first entry
- * in {@link AgentCard#supportedInterfaces()}) and select the first one the client supports
- *
- *
- * Important: At least one transport must be configured via {@link #withTransport},
- * otherwise this method throws {@link A2AClientException}.
- *
- * @return the configured client instance
- * @throws A2AClientException if no compatible transport is found or if transport configuration is missing
- */
- 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.protocolBinding());
- if (clientTransportProvider == null) {
- throw new A2AClientException("No client available for " + agentInterface.protocolBinding());
- }
- Class extends ClientTransport> transportProtocolClass = clientTransportProvider.getTransportProtocolClass();
-
- // Retrieve the configuration associated with the preferred transport
- ClientTransportConfig extends ClientTransport> clientTransportConfig = clientTransports.get(transportProtocolClass);
-
- if (clientTransportConfig == null) {
- throw new A2AClientException("Missing required TransportConfig for " + agentInterface.protocolBinding());
- }
-
- return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface), clientTransportConfig);
- }
-
- private Map getServerPreferredTransports() throws A2AClientException {
- Map serverPreferredTransports = new LinkedHashMap<>();
- if(agentCard.supportedInterfaces() == null || agentCard.supportedInterfaces().isEmpty()) {
- throw new A2AClientException("No server interface available in the AgentCard");
- }
- for (AgentInterface agentInterface : agentCard.supportedInterfaces()) {
- serverPreferredTransports.putIfAbsent(agentInterface.protocolBinding(), 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);
- }
-
- /**
- * Wraps the transport with all available transport wrappers discovered via ServiceLoader.
- * Wrappers are applied in reverse priority order (lowest priority first) to build a stack
- * where the highest priority wrapper is the outermost layer.
- *
- * @param transport the base transport to wrap
- * @param clientTransportConfig the transport configuration
- * @return the wrapped transport (or original if no wrappers are available/applicable)
- */
- private ClientTransport wrap(ClientTransport transport, ClientTransportConfig extends ClientTransport> clientTransportConfig) {
- ServiceLoader wrapperLoader = ServiceLoader.load(ClientTransportWrapper.class);
-
- // Collect all wrappers, sort by priority, then reverse for stack application
- List wrappers = wrapperLoader.stream().map(Provider::get)
- .sorted()
- .collect(Collectors.toList());
-
- if (wrappers.isEmpty()) {
- LOGGER.debug("No client transport wrappers found via ServiceLoader");
- return transport;
- }
- LOGGER.debug(wrappers.size() + " client transport wrappers found via ServiceLoader");
-
- // Reverse to apply lowest priority first (building stack with highest priority outermost)
- java.util.Collections.reverse(wrappers);
-
- // Apply wrappers to build stack
- ClientTransport wrapped = transport;
- for (ClientTransportWrapper wrapper : wrappers) {
- try {
- ClientTransport newWrapped = wrapper.wrap(wrapped, clientTransportConfig);
- if (newWrapped != wrapped) {
- LOGGER.debug("Applied transport wrapper: {} (priority: {})",
- wrapper.getClass().getName(), wrapper.priority());
- }
- wrapped = newWrapped;
- } catch (Exception e) {
- LOGGER.warn("Failed to apply transport wrapper {}: {}",
- wrapper.getClass().getName(), e.getMessage(), e);
- }
- }
-
- return wrapped;
- }
-}
diff --git a/client/base/src/main/java/io/a2a/client/ClientEvent.java b/client/base/src/main/java/io/a2a/client/ClientEvent.java
deleted file mode 100644
index 2275a7f2e..000000000
--- a/client/base/src/main/java/io/a2a/client/ClientEvent.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package io.a2a.client;
-
-/**
- * A sealed interface representing events received by an A2A client from an agent.
- *
- * ClientEvent is the base type for all events that clients receive during agent interactions.
- * The sealed interface ensures type safety by restricting implementations to three known subtypes:
- *
- * - {@link MessageEvent} - contains complete messages with content parts
- * - {@link TaskEvent} - contains complete task state, typically final states
- * - {@link TaskUpdateEvent} - contains incremental task updates (status or artifact changes)
- *
- *
- * Event flow: When a client sends a message to an agent, the agent's response is delivered
- * as a stream of ClientEvent instances to registered event consumers. The event type and sequence
- * depend on the agent's capabilities and the task's lifecycle:
- *
- * Simple blocking response:
- *
- * User → Agent
- * Agent → MessageEvent (contains agent's text response)
- *
- *
- * Streaming task execution:
- *
- * User → Agent
- * Agent → TaskEvent (SUBMITTED)
- * Agent → TaskUpdateEvent (WORKING)
- * Agent → TaskUpdateEvent (artifact update with partial results)
- * Agent → TaskUpdateEvent (artifact update with more results)
- * Agent → TaskUpdateEvent (COMPLETED)
- *
- *
- * Typical usage pattern:
- *
{@code
- * client.addConsumer((event, agentCard) -> {
- * switch (event) {
- * case MessageEvent me -> {
- * // Simple message response
- * System.out.println("Response: " + me.getMessage().parts());
- * }
- * case TaskEvent te -> {
- * // Complete task state (usually final)
- * Task task = te.getTask();
- * System.out.println("Task " + task.id() + ": " + task.status().state());
- * }
- * case TaskUpdateEvent tue -> {
- * // Incremental update
- * Task currentTask = tue.getTask();
- * UpdateEvent update = tue.getUpdateEvent();
- *
- * if (update instanceof TaskStatusUpdateEvent statusUpdate) {
- * System.out.println("Status changed to: " +
- * currentTask.status().state());
- * } else if (update instanceof TaskArtifactUpdateEvent artifactUpdate) {
- * System.out.println("New content: " +
- * artifactUpdate.artifact().parts());
- * }
- * }
- * }
- * });
- * }
- *
- * Legacy vs current protocol: In older versions of the A2A protocol, agents returned
- * {@link MessageEvent} for simple responses and {@link TaskEvent} for task-based responses.
- * The current protocol (v1.0+) uses {@link TaskUpdateEvent} for streaming updates during
- * task execution, providing finer-grained visibility into agent progress.
- *
- * @see MessageEvent
- * @see TaskEvent
- * @see TaskUpdateEvent
- * @see ClientBuilder#addConsumer(java.util.function.BiConsumer)
- */
-public sealed interface ClientEvent permits MessageEvent, TaskEvent, TaskUpdateEvent {
-}
diff --git a/client/base/src/main/java/io/a2a/client/ClientTaskManager.java b/client/base/src/main/java/io/a2a/client/ClientTaskManager.java
deleted file mode 100644
index 0e49fc4f7..000000000
--- a/client/base/src/main/java/io/a2a/client/ClientTaskManager.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package io.a2a.client;
-
-import static io.a2a.util.Utils.appendArtifactToTask;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import io.a2a.spec.A2AClientError;
-import io.a2a.spec.A2AClientInvalidArgsError;
-import io.a2a.spec.A2AClientInvalidStateError;
-import io.a2a.spec.Message;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskArtifactUpdateEvent;
-import io.a2a.spec.TaskState;
-import io.a2a.spec.TaskStatus;
-import io.a2a.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.taskId();
- }
- if (contextId == null) {
- contextId = taskStatusUpdateEvent.contextId();
- }
- Task task = currentTask;
- if (task == null) {
- task = Task.builder()
- .status(new TaskStatus(TaskState.UNRECOGNIZED))
- .id(taskId)
- .contextId(contextId == null ? "" : contextId)
- .build();
- }
-
- Task.Builder taskBuilder = Task.builder(task);
- if (taskStatusUpdateEvent.status().message() != null) {
- if (task.history() == null) {
- taskBuilder.history(taskStatusUpdateEvent.status().message());
- } else {
- List history = new ArrayList<>(task.history());
- history.add(taskStatusUpdateEvent.status().message());
- taskBuilder.history(history);
- }
- }
- if (taskStatusUpdateEvent.metadata() != null) {
- Map newMetadata = task.metadata() != null ? new HashMap<>(task.metadata()) : new HashMap<>();
- newMetadata.putAll(taskStatusUpdateEvent.metadata());
- taskBuilder.metadata(newMetadata);
- }
- taskBuilder.status(taskStatusUpdateEvent.status());
- currentTask = taskBuilder.build();
- return currentTask;
- }
-
- public Task saveTaskEvent(TaskArtifactUpdateEvent taskArtifactUpdateEvent) {
- if (taskId == null) {
- taskId = taskArtifactUpdateEvent.taskId();
- }
- if (contextId == null) {
- contextId = taskArtifactUpdateEvent.contextId();
- }
- Task task = currentTask;
- if (task == null) {
- task = Task.builder()
- .status(new TaskStatus(TaskState.UNRECOGNIZED))
- .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 = Task.builder(task);
- List history = new ArrayList<>(task.history());
- if (task.status().message() != null) {
- history.add(task.status().message());
- taskBuilder.status(new TaskStatus(task.status().state(), null, task.status().timestamp()));
- }
- history.add(message);
- taskBuilder.history(history);
- currentTask = taskBuilder.build();
- return currentTask;
- }
-
- private void saveTask(Task task) {
- currentTask = task;
- if (taskId == null) {
- taskId = currentTask.id();
- contextId = currentTask.contextId();
- }
- }
-}
\ No newline at end of file
diff --git a/client/base/src/main/java/io/a2a/client/MessageEvent.java b/client/base/src/main/java/io/a2a/client/MessageEvent.java
deleted file mode 100644
index 1db7b39fd..000000000
--- a/client/base/src/main/java/io/a2a/client/MessageEvent.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package io.a2a.client;
-
-import io.a2a.spec.Message;
-
-/**
- * A client event containing an agent's message response.
- *
- * MessageEvent represents a complete message from the agent, typically containing text, images,
- * or other content parts. This event type is used in two scenarios:
- *
- * - Simple blocking responses: When the agent completes a request immediately and
- * returns a message without task tracking
- * - Legacy protocol support: Older agents may return messages instead of task updates
- *
- *
- * Example usage:
- *
{@code
- * client.addConsumer((event, agentCard) -> {
- * if (event instanceof MessageEvent me) {
- * Message msg = me.getMessage();
- *
- * // Extract text content
- * String text = msg.parts().stream()
- * .filter(p -> p instanceof TextPart)
- * .map(p -> ((TextPart) p).text())
- * .collect(Collectors.joining());
- *
- * System.out.println("Agent response: " + text);
- *
- * // Check for images
- * msg.parts().stream()
- * .filter(p -> p instanceof ImagePart)
- * .forEach(p -> System.out.println("Image: " + ((ImagePart) p).url()));
- * }
- * });
- * }
- *
- * Message structure: The contained {@link Message} includes:
- *
- * - role: AGENT (indicating it's from the agent)
- * - parts: List of content parts (text, images, files, etc.)
- * - contextId: Optional session identifier
- * - taskId: Optional associated task ID
- * - metadata: Optional custom metadata from the agent
- *
- *
- * Streaming vs blocking: In streaming mode with task tracking, you're more likely to
- * receive {@link TaskUpdateEvent} instances instead of MessageEvent. MessageEvent is primarily
- * used for simple, synchronous request-response interactions.
- *
- * @see ClientEvent
- * @see Message
- * @see io.a2a.spec.Part
- * @see io.a2a.spec.TextPart
- */
-public final class MessageEvent implements ClientEvent {
-
- private final Message message;
-
- /**
- * Create a message event.
- *
- * @param message the message received from the agent (required)
- */
- public MessageEvent(Message message) {
- this.message = message;
- }
-
- /**
- * Get the message contained in this event.
- *
- * @return the agent's message
- */
- public Message getMessage() {
- return message;
- }
-
- @Override
- public String toString() {
- String messageAsString = "{"
- + "role=" + message.role()
- + ", parts=" + message.parts()
- + ", messageId=" + message.messageId()
- + ", contextId=" + message.contextId()
- + ", taskId=" + message.taskId()
- + ", metadata=" + message.metadata()
- + ", kind=" + message.kind()
- + ", referenceTaskIds=" + message.referenceTaskIds()
- + ", extensions=" + message.extensions() + '}';
- return "MessageEvent{" + "message=" + messageAsString + '}';
- }
-}
diff --git a/client/base/src/main/java/io/a2a/client/TaskEvent.java b/client/base/src/main/java/io/a2a/client/TaskEvent.java
deleted file mode 100644
index 4da4ef04f..000000000
--- a/client/base/src/main/java/io/a2a/client/TaskEvent.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package io.a2a.client;
-
-import static io.a2a.util.Assert.checkNotNullParam;
-
-import io.a2a.spec.Task;
-
-/**
- * A client event containing the complete state of a task.
- *
- * TaskEvent represents a snapshot of a task's full state at a point in time. This event type
- * is typically received in two scenarios:
- *
- * - Final task state: When a task reaches a terminal state (COMPLETED, FAILED, CANCELED),
- * the agent may send a TaskEvent with the complete final state
- * - Non-streaming mode: When streaming is disabled, the client receives a single
- * TaskEvent containing the final result after the agent completes processing
- *
- *
- * Contrast with TaskUpdateEvent: While {@link TaskUpdateEvent} provides incremental
- * updates during task execution (status changes, new artifacts), TaskEvent provides the
- * complete task state in a single event.
- *
- * Example usage:
- *
{@code
- * client.addConsumer((event, agentCard) -> {
- * if (event instanceof TaskEvent te) {
- * Task task = te.getTask();
- *
- * // Check task state
- * TaskState state = task.status().state();
- * switch (state) {
- * case COMPLETED -> {
- * // Task finished successfully
- * if (task.artifact() != null) {
- * System.out.println("Result: " + task.artifact().parts());
- * }
- * }
- * case FAILED -> {
- * // Task failed
- * String error = task.status().message();
- * System.err.println("Task failed: " + error);
- * }
- * case CANCELED -> {
- * System.out.println("Task was canceled");
- * }
- * default -> {
- * System.out.println("Task in state: " + state);
- * }
- * }
- * }
- * });
- * }
- *
- * Task contents: The contained {@link Task} includes:
- *
- * - id: Unique task identifier
- * - status: Current state (SUBMITTED, WORKING, COMPLETED, FAILED, CANCELED, etc.)
- * - artifact: Task results (if available)
- * - contextId: Associated session/context identifier
- * - metadata: Custom task metadata
- * - history: Optional state transition history
- *
- *
- * Terminal states: When a task reaches a final state, no further updates will be
- * received for that task:
- *
- * - COMPLETED - task finished successfully
- * - FAILED - task encountered an error
- * - CANCELED - task was canceled by user or system
- * - REJECTED - task was rejected (e.g., authorization failure)
- *
- *
- * @see ClientEvent
- * @see Task
- * @see TaskUpdateEvent
- * @see io.a2a.spec.TaskState
- * @see io.a2a.spec.TaskStatus
- */
-public final class TaskEvent implements ClientEvent {
-
- private final Task task;
-
- /**
- * Create a task event.
- *
- * @param task the task state received from the agent (required)
- */
- public TaskEvent(Task task) {
- checkNotNullParam("task", task);
- this.task = task;
- }
-
- /**
- * Get the task contained in this event.
- *
- * @return the complete task state
- */
- public Task getTask() {
- return task;
- }
-}
diff --git a/client/base/src/main/java/io/a2a/client/TaskUpdateEvent.java b/client/base/src/main/java/io/a2a/client/TaskUpdateEvent.java
deleted file mode 100644
index e9efe2404..000000000
--- a/client/base/src/main/java/io/a2a/client/TaskUpdateEvent.java
+++ /dev/null
@@ -1,154 +0,0 @@
-package io.a2a.client;
-
-import static io.a2a.util.Assert.checkNotNullParam;
-
-import io.a2a.spec.Task;
-import io.a2a.spec.UpdateEvent;
-
-/**
- * A client event containing an incremental update to a task.
- *
- * TaskUpdateEvent represents a change to a task's state during execution. It provides both
- * the current complete task state and the specific update that triggered this event. This
- * event type is the primary mechanism for tracking task progress in streaming scenarios.
- *
- * Two types of updates:
- *
- * - {@link io.a2a.spec.TaskStatusUpdateEvent} - task state changed (e.g., SUBMITTED → WORKING → COMPLETED)
- * - {@link io.a2a.spec.TaskArtifactUpdateEvent} - new content/results available
- *
- *
- * Streaming task lifecycle example:
- *
{@code
- * client.sendMessage(A2A.toUserMessage("Summarize this document"));
- *
- * // Client receives sequence of TaskUpdateEvents:
- * 1. TaskUpdateEvent(task=Task[status=SUBMITTED], updateEvent=TaskStatusUpdateEvent)
- * 2. TaskUpdateEvent(task=Task[status=WORKING], updateEvent=TaskStatusUpdateEvent)
- * 3. TaskUpdateEvent(task=Task[status=WORKING, artifact=[partial]], updateEvent=TaskArtifactUpdateEvent)
- * 4. TaskUpdateEvent(task=Task[status=WORKING, artifact=[more content]], updateEvent=TaskArtifactUpdateEvent)
- * 5. TaskUpdateEvent(task=Task[status=COMPLETED, artifact=[final]], updateEvent=TaskStatusUpdateEvent)
- * }
- *
- * Example usage - tracking progress:
- *
{@code
- * client.addConsumer((event, agentCard) -> {
- * if (event instanceof TaskUpdateEvent tue) {
- * Task currentTask = tue.getTask();
- * UpdateEvent update = tue.getUpdateEvent();
- *
- * // Handle status changes
- * if (update instanceof TaskStatusUpdateEvent statusUpdate) {
- * TaskState newState = currentTask.status().state();
- * System.out.println("Task " + currentTask.id() + " → " + newState);
- *
- * if (newState == TaskState.COMPLETED) {
- * System.out.println("Final result: " +
- * currentTask.artifact().parts());
- * } else if (newState == TaskState.FAILED) {
- * System.err.println("Error: " +
- * currentTask.status().message());
- * }
- * }
- *
- * // Handle new content
- * if (update instanceof TaskArtifactUpdateEvent artifactUpdate) {
- * Artifact newContent = artifactUpdate.artifact();
- * System.out.println("New content received: " + newContent.parts());
- *
- * // For streaming text generation
- * newContent.parts().stream()
- * .filter(p -> p instanceof TextPart)
- * .map(p -> ((TextPart) p).text())
- * .forEach(System.out::print); // Print incrementally
- * }
- * }
- * });
- * }
- *
- * Reconstructing complete state: The {@link #getTask()} method returns the task with
- * all updates applied up to this point. The client automatically maintains the complete
- * task state by merging updates, so consumers don't need to manually track changes:
- *
{@code
- * // Each TaskUpdateEvent contains the fully updated task
- * TaskUpdateEvent event1 // task has status=WORKING, artifact=null
- * TaskUpdateEvent event2 // task has status=WORKING, artifact=[chunk1]
- * TaskUpdateEvent event3 // task has status=WORKING, artifact=[chunk1, chunk2]
- * TaskUpdateEvent event4 // task has status=COMPLETED, artifact=[chunk1, chunk2, final]
- * }
- *
- * Artifact updates: When {@link io.a2a.spec.TaskArtifactUpdateEvent} is received,
- * the artifact may be:
- *
- * - Incremental: New parts appended to existing artifact (common for streaming text)
- * - Replacement: Entire artifact replaced (less common)
- *
- * The {@link #getTask()} always reflects the current complete artifact state.
- *
- * Status transitions: Common task state transitions:
- *
- * SUBMITTED → WORKING → COMPLETED
- * SUBMITTED → WORKING → FAILED
- * SUBMITTED → WORKING → CANCELED
- * SUBMITTED → AUTH_REQUIRED → (waiting for auth) → WORKING → COMPLETED
- *
- *
- * @see ClientEvent
- * @see Task
- * @see io.a2a.spec.UpdateEvent
- * @see io.a2a.spec.TaskStatusUpdateEvent
- * @see io.a2a.spec.TaskArtifactUpdateEvent
- * @see io.a2a.spec.TaskState
- */
-public final class TaskUpdateEvent implements ClientEvent {
-
- private final Task task;
- private final UpdateEvent updateEvent;
-
- /**
- * Create a task update event.
- *
- * This constructor is typically called internally by the client framework when processing
- * update events from the agent. The {@code task} parameter contains the complete current
- * state with all updates applied, while {@code updateEvent} contains the specific change
- * that triggered this event.
- *
- * @param task the current complete task state with all updates applied (required)
- * @param updateEvent the specific update that triggered this event (required)
- */
- public TaskUpdateEvent(Task task, UpdateEvent updateEvent) {
- checkNotNullParam("task", task);
- checkNotNullParam("updateEvent", updateEvent);
- this.task = task;
- this.updateEvent = updateEvent;
- }
-
- /**
- * Get the current complete task state.
- *
- * The returned task reflects all updates received up to this point, including the
- * update contained in this event. Consumers can use this method to access the
- * complete current state without manually tracking changes.
- *
- * @return the task with all updates applied
- */
- public Task getTask() {
- return task;
- }
-
- /**
- * Get the specific update that triggered this event.
- *
- * This will be either:
- *
- * - {@link io.a2a.spec.TaskStatusUpdateEvent} - indicates a state transition
- * - {@link io.a2a.spec.TaskArtifactUpdateEvent} - indicates new content available
- *
- *
- * @return the update event
- */
- public UpdateEvent getUpdateEvent() {
- return updateEvent;
- }
-
-}
diff --git a/client/base/src/main/java/io/a2a/client/config/ClientConfig.java b/client/base/src/main/java/io/a2a/client/config/ClientConfig.java
deleted file mode 100644
index d9ffd7e6e..000000000
--- a/client/base/src/main/java/io/a2a/client/config/ClientConfig.java
+++ /dev/null
@@ -1,410 +0,0 @@
-package io.a2a.client.config;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import io.a2a.spec.PushNotificationConfig;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Configuration for controlling A2A client behavior and communication preferences.
- *
- * ClientConfig defines how the client communicates with agents, including streaming mode,
- * transport preference, output modes, and request metadata. The configuration is immutable
- * and constructed using the {@link Builder} pattern.
- *
- * Key configuration options:
- *
- * - Streaming: Enable/disable real-time event streaming (default: true)
- * - Polling: Use polling instead of blocking for updates (default: false)
- * - Transport preference: Client vs server transport priority (default: server preference)
- * - Output modes: Acceptable content types (text, audio, image, etc.)
- * - History length: Number of previous messages to include as context
- * - Push notifications: Default webhook configuration for task updates
- * - Metadata: Custom metadata attached to all requests
- *
- *
- * Streaming mode: Controls whether the client uses streaming or blocking communication.
- * Streaming mode requires both the client configuration AND the agent's capabilities to support it:
- *
{@code
- * // Enable streaming (if agent also supports it)
- * ClientConfig config = new ClientConfig.Builder()
- * .setStreaming(true)
- * .build();
- *
- * // Actual mode = config.streaming && agentCard.capabilities().streaming()
- * }
- * When streaming is enabled and supported, the client receives events asynchronously as the
- * agent processes the request. When disabled, the client blocks until the task completes.
- *
- * Transport preference: Controls which transport protocol is selected when multiple
- * options are available:
- *
{@code
- * // Default: Use server's preferred transport (first in AgentCard.supportedInterfaces)
- * ClientConfig serverPref = new ClientConfig.Builder()
- * .setUseClientPreference(false)
- * .build();
- *
- * // Use client's preferred transport (order of withTransport() calls)
- * ClientConfig clientPref = new ClientConfig.Builder()
- * .setUseClientPreference(true)
- * .build();
- *
- * Client client = Client.builder(card)
- * .withTransport(GrpcTransport.class, grpcConfig) // Client preference 1
- * .withTransport(JSONRPCTransport.class, jsonConfig) // Client preference 2
- * .clientConfig(clientPref)
- * .build();
- * // With useClientPreference=true, tries gRPC first, then JSON-RPC
- * // With useClientPreference=false, uses server's order from AgentCard
- * }
- *
- * Output modes: Specify which content types the client can handle:
- *
{@code
- * ClientConfig config = new ClientConfig.Builder()
- * .setAcceptedOutputModes(List.of("text", "image", "audio"))
- * .build();
- * // Agent will only return text, image, or audio content
- * }
- *
- * Conversation history: Request previous messages as context:
- *
{@code
- * ClientConfig config = new ClientConfig.Builder()
- * .setHistoryLength(10) // Include last 10 messages
- * .build();
- * }
- * This is useful for maintaining conversation context across multiple requests in the same session.
- *
- * Push notifications: Configure default webhook for all task updates:
- *
{@code
- * PushNotificationConfig pushConfig = new PushNotificationConfig(
- * "https://my-app.com/webhooks/tasks",
- * Map.of("Authorization", "Bearer my-token")
- * );
- * ClientConfig config = new ClientConfig.Builder()
- * .setPushNotificationConfig(pushConfig)
- * .build();
- * // All sendMessage() calls will use this webhook config
- * }
- *
- * Custom metadata: Attach metadata to all requests:
- *
{@code
- * Map metadata = Map.of(
- * "userId", "user-123",
- * "sessionId", "session-456",
- * "clientVersion", "1.0.0"
- * );
- * ClientConfig config = new ClientConfig.Builder()
- * .setMetadata(metadata)
- * .build();
- * // Metadata is included in every message sent
- * }
- *
- * Complete example:
- *
{@code
- * ClientConfig config = new ClientConfig.Builder()
- * .setStreaming(true) // Enable streaming
- * .setUseClientPreference(true) // Use client transport order
- * .setAcceptedOutputModes(List.of("text")) // Text responses only
- * .setHistoryLength(5) // Last 5 messages as context
- * .setMetadata(Map.of("userId", "user-123")) // Custom metadata
- * .build();
- *
- * Client client = Client.builder(agentCard)
- * .clientConfig(config)
- * .withTransport(JSONRPCTransport.class, transportConfig)
- * .build();
- * }
- *
- * Default values:
- *
- * - streaming: {@code true}
- * - polling: {@code false}
- * - useClientPreference: {@code false} (server preference)
- * - acceptedOutputModes: empty list (accept all)
- * - historyLength: {@code null} (no history)
- * - pushNotificationConfig: {@code null} (no push notifications)
- * - metadata: empty map
- *
- *
- * Thread safety: ClientConfig is immutable and thread-safe. Multiple clients can
- * share the same configuration instance.
- *
- * @see io.a2a.client.Client
- * @see io.a2a.client.ClientBuilder
- * @see PushNotificationConfig
- */
-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;
- }
-
- /**
- * Check if streaming mode is enabled.
- *
- * Note: Actual streaming requires both this configuration AND agent support
- * ({@link io.a2a.spec.AgentCapabilities#streaming()}).
- *
- * @return {@code true} if streaming is enabled (default)
- */
- public boolean isStreaming() {
- return streaming;
- }
-
- /**
- * Check if polling mode is enabled for task updates.
- *
- * When polling is enabled, the client can poll for task status updates instead of
- * blocking or streaming. This is useful for asynchronous workflows where the client
- * doesn't need immediate results.
- *
- * @return {@code true} if polling is enabled, {@code false} by default
- */
- public boolean isPolling() {
- return polling;
- }
-
- /**
- * Check if client transport preference is enabled.
- *
- * When {@code true}, the client iterates through its configured transports (in the order
- * they were added via {@link io.a2a.client.ClientBuilder#withTransport}) and selects the first one
- * the agent supports.
- *
- * When {@code false} (default), the agent's preferred transport is used (first entry
- * in {@link io.a2a.spec.AgentCard#supportedInterfaces()}).
- *
- * @return {@code true} if using client preference, {@code false} for server preference (default)
- */
- public boolean isUseClientPreference() {
- return useClientPreference;
- }
-
- /**
- * Get the list of accepted output modes.
- *
- * This list specifies which content types the client can handle (e.g., "text", "audio",
- * "image", "video"). An empty list means all modes are accepted.
- *
- * The agent will only return content in the specified modes. For example, if only "text"
- * is specified, the agent won't return images or audio.
- *
- * @return the list of accepted output modes (never null, but may be empty)
- */
- public List getAcceptedOutputModes() {
- return acceptedOutputModes;
- }
-
- /**
- * Get the default push notification configuration.
- *
- * If set, this webhook configuration will be used for all sendMessage
- * calls unless overridden with a different configuration.
- *
- * @return the push notification config, or {@code null} if not configured
- * @see io.a2a.client.Client#sendMessage(io.a2a.spec.Message, io.a2a.spec.PushNotificationConfig, java.util.Map, io.a2a.client.transport.spi.interceptors.ClientCallContext)
- */
- public @Nullable PushNotificationConfig getPushNotificationConfig() {
- return pushNotificationConfig;
- }
-
- /**
- * Get the conversation history length.
- *
- * This value specifies how many previous messages should be included as context
- * when sending a new message. For example, a value of 10 means the agent receives
- * the last 10 messages in the conversation for context.
- *
- * @return the history length, or {@code null} if not configured (no history)
- */
- public @Nullable Integer getHistoryLength() {
- return historyLength;
- }
-
- /**
- * Get the custom metadata attached to all requests.
- *
- * This metadata is included in every message sent by the client. It can contain
- * user IDs, session identifiers, client version, or any other custom data.
- *
- * @return the metadata map (never null, but may be empty)
- */
- public Map getMetadata() {
- return metadata;
- }
-
- /**
- * Create a new builder for constructing ClientConfig instances.
- *
- * @return a new builder
- */
- public static Builder builder() {
- return new Builder();
- }
-
- /**
- * Builder for creating {@link ClientConfig} instances.
- *
- * All configuration options have sensible defaults and are optional. Use this builder
- * to override specific settings as needed.
- *
- * Example:
- *
{@code
- * ClientConfig config = new ClientConfig.Builder()
- * .setStreaming(true)
- * .setHistoryLength(10)
- * .build();
- * }
- */
- 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<>();
-
- /**
- * Enable or disable streaming mode.
- *
- * When enabled, the client will use streaming communication if the agent also
- * supports it. When disabled, the client uses blocking request-response mode.
- *
- * @param streaming {@code true} to enable streaming (default), {@code false} to disable
- * @return this builder for method chaining
- */
- public Builder setStreaming(@Nullable Boolean streaming) {
- this.streaming = streaming;
- return this;
- }
-
- /**
- * Enable or disable polling mode for task updates.
- *
- * When enabled, the client can poll for task status instead of blocking or streaming.
- * Useful for asynchronous workflows.
- *
- * @param polling {@code true} to enable polling, {@code false} otherwise (default)
- * @return this builder for method chaining
- */
- public Builder setPolling(@Nullable Boolean polling) {
- this.polling = polling;
- return this;
- }
-
- /**
- * Set whether to use client or server transport preference.
- *
- * When {@code true}, the client's transport order (from {@link io.a2a.client.ClientBuilder#withTransport}
- * calls) takes priority. When {@code false} (default), the server's preferred transport
- * (first in {@link io.a2a.spec.AgentCard#supportedInterfaces()}) is used.
- *
- * @param useClientPreference {@code true} for client preference, {@code false} for server preference (default)
- * @return this builder for method chaining
- */
- public Builder setUseClientPreference(@Nullable Boolean useClientPreference) {
- this.useClientPreference = useClientPreference;
- return this;
- }
-
- /**
- * Set the accepted output modes.
- *
- * Specify which content types the client can handle (e.g., "text", "audio", "image").
- * An empty list (default) means all modes are accepted.
- *
- * The provided list is copied, so subsequent modifications won't affect this configuration.
- *
- * @param acceptedOutputModes the list of accepted output modes
- * @return this builder for method chaining
- */
- public Builder setAcceptedOutputModes(List acceptedOutputModes) {
- this.acceptedOutputModes = new ArrayList<>(acceptedOutputModes);
- return this;
- }
-
- /**
- * Set the default push notification configuration.
- *
- * This webhook configuration will be used for all sendMessage calls
- * unless overridden. The agent will POST task update events to the specified URL.
- *
- * @param pushNotificationConfig the push notification configuration
- * @return this builder for method chaining
- * @see io.a2a.client.Client#sendMessage(io.a2a.spec.Message, io.a2a.spec.PushNotificationConfig, java.util.Map, io.a2a.client.transport.spi.interceptors.ClientCallContext)
- */
- public Builder setPushNotificationConfig(PushNotificationConfig pushNotificationConfig) {
- this.pushNotificationConfig = pushNotificationConfig;
- return this;
- }
-
- /**
- * Set the conversation history length.
- *
- * Specify how many previous messages should be included as context when sending
- * a new message. For example, 10 means the last 10 messages are sent to the agent
- * for context.
- *
- * @param historyLength the number of previous messages to include (must be positive)
- * @return this builder for method chaining
- */
- public Builder setHistoryLength(Integer historyLength) {
- this.historyLength = historyLength;
- return this;
- }
-
- /**
- * Set custom metadata to be included in all requests.
- *
- * This metadata is attached to every message sent by the client. Useful for
- * tracking user IDs, session identifiers, client version, etc.
- *
- * The provided map is copied, so subsequent modifications won't affect this configuration.
- *
- * @param metadata the custom metadata map
- * @return this builder for method chaining
- */
- public Builder setMetadata(Map metadata) {
- this.metadata = metadata;
- return this;
- }
-
- /**
- * Build the ClientConfig with the configured settings.
- *
- * Any unset options will use their default values:
- *
- * - streaming: {@code true}
- * - polling: {@code false}
- * - useClientPreference: {@code false}
- * - acceptedOutputModes: empty list
- * - pushNotificationConfig: {@code null}
- * - historyLength: {@code null}
- * - metadata: empty map
- *
- *
- * @return the configured ClientConfig instance
- */
- public ClientConfig build() {
- return new ClientConfig(this);
- }
- }
-}
\ No newline at end of file
diff --git a/client/base/src/main/java/io/a2a/client/config/package-info.java b/client/base/src/main/java/io/a2a/client/config/package-info.java
deleted file mode 100644
index ab8c28600..000000000
--- a/client/base/src/main/java/io/a2a/client/config/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-@NullMarked
-package io.a2a.client.config;
-
-import org.jspecify.annotations.NullMarked;
-
diff --git a/client/base/src/main/java/io/a2a/client/package-info.java b/client/base/src/main/java/io/a2a/client/package-info.java
deleted file mode 100644
index 73cc3eef8..000000000
--- a/client/base/src/main/java/io/a2a/client/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-@NullMarked
-package io.a2a.client;
-
-import org.jspecify.annotations.NullMarked;
-
diff --git a/client/base/src/main/resources/META-INF/beans.xml b/client/base/src/main/resources/META-INF/beans.xml
deleted file mode 100644
index e69de29bb..000000000
diff --git a/client/base/src/test/java/io/a2a/A2ATest.java b/client/base/src/test/java/io/a2a/A2ATest.java
deleted file mode 100644
index 1052ad76d..000000000
--- a/client/base/src/test/java/io/a2a/A2ATest.java
+++ /dev/null
@@ -1,147 +0,0 @@
-package io.a2a;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import io.a2a.spec.Message;
-import io.a2a.spec.Part;
-import io.a2a.spec.TextPart;
-import org.junit.jupiter.api.Test;
-
-public class A2ATest {
-
- @Test
- public void testToUserMessage() {
- String text = "Hello, world!";
- Message message = A2A.toUserMessage(text);
-
- assertEquals(Message.Role.ROLE_USER, message.role());
- assertEquals(1, message.parts().size());
- assertEquals(text, ((TextPart) message.parts().get(0)).text());
- assertNotNull(message.messageId());
- assertNull(message.contextId());
- assertNull(message.taskId());
- }
-
- @Test
- public void testToUserMessageWithId() {
- String text = "Hello, world!";
- String messageId = "test-message-id";
- Message message = A2A.toUserMessage(text, messageId);
-
- assertEquals(Message.Role.ROLE_USER, message.role());
- assertEquals(messageId, message.messageId());
- }
-
- @Test
- public void testToAgentMessage() {
- String text = "Hello, I'm an agent!";
- Message message = A2A.toAgentMessage(text);
-
- assertEquals(Message.Role.ROLE_AGENT, message.role());
- assertEquals(1, message.parts().size());
- assertEquals(text, ((TextPart) message.parts().get(0)).text());
- assertNotNull(message.messageId());
- }
-
- @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.ROLE_AGENT, message.role());
- assertEquals(messageId, message.messageId());
- }
-
- @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.ROLE_USER, message.role());
- assertEquals(contextId, message.contextId());
- assertEquals(taskId, message.taskId());
- assertEquals(1, message.parts().size());
- assertEquals(text, ((TextPart) message.parts().get(0)).text());
- assertNotNull(message.messageId());
- assertNull(message.metadata());
- assertNull(message.referenceTaskIds());
- }
-
- @Test
- public void testCreateUserTextMessageWithNullParams() {
- String text = "Simple user message";
-
- Message message = A2A.createUserTextMessage(text, null, null);
-
- assertEquals(Message.Role.ROLE_USER, message.role());
- assertNull(message.contextId());
- assertNull(message.taskId());
- assertEquals(1, message.parts().size());
- assertEquals(text, ((TextPart) message.parts().get(0)).text());
- }
-
- @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.ROLE_AGENT, message.role());
- assertEquals(contextId, message.contextId());
- assertEquals(taskId, message.taskId());
- assertEquals(1, message.parts().size());
- assertEquals(text, ((TextPart) message.parts().get(0)).text());
- assertNotNull(message.messageId());
- }
-
- @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.ROLE_AGENT, message.role());
- assertEquals(contextId, message.contextId());
- assertEquals(taskId, message.taskId());
- assertEquals(2, message.parts().size());
- assertEquals("Part 1", ((TextPart) message.parts().get(0)).text());
- assertEquals("Part 2", ((TextPart) message.parts().get(1)).text());
- }
-
- @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/client/base/src/test/java/io/a2a/client/AuthenticationAuthorizationTest.java b/client/base/src/test/java/io/a2a/client/AuthenticationAuthorizationTest.java
deleted file mode 100644
index 5d0e9b8e9..000000000
--- a/client/base/src/test/java/io/a2a/client/AuthenticationAuthorizationTest.java
+++ /dev/null
@@ -1,378 +0,0 @@
-package io.a2a.client;
-
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockserver.model.HttpRequest.request;
-import static org.mockserver.model.HttpResponse.response;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-import io.a2a.client.config.ClientConfig;
-import io.a2a.client.transport.grpc.GrpcTransport;
-import io.a2a.client.transport.grpc.GrpcTransportConfigBuilder;
-import io.a2a.client.transport.jsonrpc.JSONRPCTransport;
-import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfigBuilder;
-import io.a2a.client.transport.rest.RestTransport;
-import io.a2a.client.transport.rest.RestTransportConfigBuilder;
-import io.a2a.grpc.A2AServiceGrpc;
-import io.a2a.grpc.SendMessageRequest;
-import io.a2a.grpc.SendMessageResponse;
-import io.a2a.grpc.StreamResponse;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCapabilities;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentInterface;
-import io.a2a.spec.AgentSkill;
-import io.a2a.spec.Message;
-import io.a2a.spec.TextPart;
-import io.a2a.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;
-
-/**
- * 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 = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("test message")))
- .contextId("context-1234")
- .messageId("message-1234")
- .build();
-
- grpcServerName = InProcessServerBuilder.generateName();
-
- agentCard = AgentCard.builder()
- .name("Test Agent")
- .description("Test agent for auth tests")
- .version("1.0.0")
- .capabilities(AgentCapabilities.builder()
- .streaming(true) // Support streaming for all tests
- .build())
- .defaultInputModes(Collections.singletonList("text"))
- .defaultOutputModes(Collections.singletonList("text"))
- .skills(Collections.singletonList(AgentSkill.builder()
- .id("test_skill")
- .name("Test skill")
- .description("Test skill")
- .tags(Collections.singletonList("test"))
- .build()))
- .supportedInterfaces(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("/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("/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("/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("/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/client/base/src/test/java/io/a2a/client/ClientBuilderTest.java b/client/base/src/test/java/io/a2a/client/ClientBuilderTest.java
deleted file mode 100644
index 3be9d4918..000000000
--- a/client/base/src/test/java/io/a2a/client/ClientBuilderTest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package io.a2a.client;
-
-
-import java.util.Collections;
-import java.util.List;
-
-import io.a2a.client.config.ClientConfig;
-import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.transport.grpc.GrpcTransport;
-import io.a2a.client.transport.grpc.GrpcTransportConfigBuilder;
-import io.a2a.client.transport.jsonrpc.JSONRPCTransport;
-import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfig;
-import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfigBuilder;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCapabilities;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentInterface;
-import io.a2a.spec.AgentSkill;
-import io.a2a.spec.TransportProtocol;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-public class ClientBuilderTest {
-
- private AgentCard card = AgentCard.builder()
- .name("Hello World Agent")
- .description("Just a hello world agent")
- .version("1.0.0")
- .documentationUrl("http://example.com/docs")
- .capabilities(AgentCapabilities.builder()
- .streaming(true)
- .pushNotifications(true)
- .build())
- .defaultInputModes(Collections.singletonList("text"))
- .defaultOutputModes(Collections.singletonList("text"))
- .skills(Collections.singletonList(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()))
- .supportedInterfaces(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(A2AHttpClientFactory.create()))
- .build();
-
- Assertions.assertNotNull(client);
- }
-}
diff --git a/client/base/src/test/java/io/a2a/client/ClientTaskManagerTest.java b/client/base/src/test/java/io/a2a/client/ClientTaskManagerTest.java
deleted file mode 100644
index 74699b97e..000000000
--- a/client/base/src/test/java/io/a2a/client/ClientTaskManagerTest.java
+++ /dev/null
@@ -1,323 +0,0 @@
-package io.a2a.client;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import io.a2a.spec.A2AClientInvalidArgsError;
-import io.a2a.spec.A2AClientInvalidStateError;
-import io.a2a.spec.Artifact;
-import io.a2a.spec.Message;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskArtifactUpdateEvent;
-import io.a2a.spec.TaskState;
-import io.a2a.spec.TaskStatus;
-import io.a2a.spec.TaskStatusUpdateEvent;
-import io.a2a.spec.TextPart;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-public class ClientTaskManagerTest {
-
- private ClientTaskManager taskManager;
- private Task sampleTask;
- private Message sampleMessage;
-
- @BeforeEach
- public void setUp() {
- taskManager = new ClientTaskManager();
-
- sampleTask = Task.builder()
- .id("task123")
- .contextId("context456")
- .status(new TaskStatus(TaskState.TASK_STATE_WORKING))
- .build();
-
- sampleMessage = Message.builder()
- .messageId("msg1")
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("Hello")))
- .build();
- }
-
- @Test
- public void testGetCurrentTaskNoTaskRaisesError() {
- A2AClientInvalidStateError exception = assertThrows(
- A2AClientInvalidStateError.class,
- () -> taskManager.getCurrentTask()
- );
- assertTrue(exception.getMessage().contains("No current task"));
- }
-
- @Test
- public void testSaveTaskEventWithTask() throws Exception {
- Task result = taskManager.saveTaskEvent(sampleTask);
-
- assertEquals(sampleTask, taskManager.getCurrentTask());
- assertEquals(sampleTask, result);
- }
-
- @Test
- public void testSaveTaskEventWithTaskAlreadySetRaisesError() throws Exception {
- taskManager.saveTaskEvent(sampleTask);
-
- A2AClientInvalidArgsError exception = assertThrows(
- A2AClientInvalidArgsError.class,
- () -> taskManager.saveTaskEvent(sampleTask)
- );
- assertTrue(exception.getMessage().contains("Task is already set, create new manager for new tasks."));
- }
-
- @Test
- public void testSaveTaskEventWithStatusUpdate() throws Exception {
- taskManager.saveTaskEvent(sampleTask);
-
- TaskStatusUpdateEvent statusUpdate = TaskStatusUpdateEvent.builder()
- .taskId(sampleTask.id())
- .contextId(sampleTask.contextId())
- .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, sampleMessage, null))
- .build();
-
- Task updatedTask = taskManager.saveTaskEvent(statusUpdate);
-
- assertEquals(TaskState.TASK_STATE_COMPLETED, updatedTask.status().state());
- assertNotNull(updatedTask.history());
- assertEquals(1, updatedTask.history().size());
- assertEquals(sampleMessage, updatedTask.history().get(0));
- }
-
- @Test
- public void testSaveTaskEventWithStatusUpdateAndMetadata() throws Exception {
- taskManager.saveTaskEvent(sampleTask);
-
- Map metadata = new HashMap<>();
- metadata.put("key1", "value1");
- metadata.put("key2", 42);
-
- TaskStatusUpdateEvent statusUpdate = TaskStatusUpdateEvent.builder()
- .taskId(sampleTask.id())
- .contextId(sampleTask.contextId())
- .status(new TaskStatus(TaskState.TASK_STATE_WORKING))
- .metadata(metadata)
- .build();
-
- Task updatedTask = taskManager.saveTaskEvent(statusUpdate);
-
- assertNotNull(updatedTask.metadata());
- assertEquals("value1", updatedTask.metadata().get("key1"));
- assertEquals(42, updatedTask.metadata().get("key2"));
- }
-
- @Test
- public void testSaveTaskEventWithArtifactUpdate() throws Exception {
- taskManager.saveTaskEvent(sampleTask);
-
- Artifact artifact = Artifact.builder()
- .artifactId("art1")
- .parts(Collections.singletonList(new TextPart("artifact content")))
- .build();
-
- TaskArtifactUpdateEvent artifactUpdate = TaskArtifactUpdateEvent.builder()
- .taskId(sampleTask.id())
- .contextId(sampleTask.contextId())
- .artifact(artifact)
- .build();
-
- Task updatedTask = taskManager.saveTaskEvent(artifactUpdate);
-
- assertNotNull(updatedTask);
- assertNotNull(updatedTask.artifacts());
- assertEquals(1, updatedTask.artifacts().size());
- assertEquals("art1", updatedTask.artifacts().get(0).artifactId());
- }
-
- @Test
- public void testSaveTaskEventCreatesTaskIfNotExists() throws Exception {
- TaskStatusUpdateEvent statusUpdate = TaskStatusUpdateEvent.builder()
- .taskId("new_task")
- .contextId("new_context")
- .status(new TaskStatus(TaskState.TASK_STATE_WORKING))
- .build();
-
- Task updatedTask = taskManager.saveTaskEvent(statusUpdate);
-
- assertNotNull(updatedTask);
- assertEquals("new_task", updatedTask.id());
- assertEquals("new_context", updatedTask.contextId());
- assertEquals(TaskState.TASK_STATE_WORKING, updatedTask.status().state());
- }
-
- @Test
- public void testSaveTaskEventCreatesTaskFromArtifactUpdateIfNotExists() {
- Artifact artifact = Artifact.builder()
- .artifactId("art1")
- .parts(Collections.singletonList(new TextPart("artifact content")))
- .build();
-
- TaskArtifactUpdateEvent artifactUpdate = TaskArtifactUpdateEvent.builder()
- .taskId("new_task_id")
- .contextId("new_context_id")
- .artifact(artifact)
- .build();
-
- Task updatedTask = taskManager.saveTaskEvent(artifactUpdate);
-
- assertNotNull(updatedTask);
- assertEquals("new_task_id", updatedTask.id());
- assertEquals("new_context_id", updatedTask.contextId());
- assertNotNull(updatedTask.artifacts());
- assertEquals(1, updatedTask.artifacts().size());
- }
-
- @Test
- public void testUpdateWithMessage() {
- // Use a task with mutable history list initialized properly
- Task taskWithHistory = Task.builder()
- .id("task123")
- .contextId("context456")
- .status(new TaskStatus(TaskState.TASK_STATE_WORKING))
- .build();
-
- Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithHistory);
-
- assertNotNull(updatedTask.history());
- assertEquals(1, updatedTask.history().size());
- assertEquals(sampleMessage, updatedTask.history().get(0));
- }
-
- @Test
- public void testUpdateWithMessageMovesStatusMessage() {
- Message statusMessage = Message.builder()
- .messageId("status_msg")
- .role(Message.Role.ROLE_AGENT)
- .parts(Collections.singletonList(new TextPart("Status")))
- .build();
-
- Task taskWithStatusMessage = Task.builder()
- .id("task123")
- .contextId("context456")
- .status(new TaskStatus(TaskState.TASK_STATE_WORKING, statusMessage, null))
- .build();
-
- Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithStatusMessage);
-
- assertNotNull(updatedTask.history());
- assertEquals(2, updatedTask.history().size());
- assertEquals(statusMessage, updatedTask.history().get(0));
- assertEquals(sampleMessage, updatedTask.history().get(1));
- assertNull(updatedTask.status().message());
- }
-
- @Test
- public void testUpdateWithMessagePreservesExistingHistory() {
- Message existingMessage = Message.builder()
- .messageId("existing_msg")
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("Existing")))
- .build();
-
- Task taskWithHistory = Task.builder()
- .id("task123")
- .contextId("context456")
- .status(new TaskStatus(TaskState.TASK_STATE_WORKING))
- .history(List.of(existingMessage))
- .build();
-
- Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithHistory);
-
- assertNotNull(updatedTask.history());
- assertEquals(2, updatedTask.history().size());
- assertEquals(existingMessage, updatedTask.history().get(0));
- assertEquals(sampleMessage, updatedTask.history().get(1));
- }
-
- @Test
- public void testSaveTaskEventMultipleStatusUpdates() throws Exception {
- taskManager.saveTaskEvent(sampleTask);
-
- // First status update
- TaskStatusUpdateEvent statusUpdate1 = TaskStatusUpdateEvent.builder()
- .taskId(sampleTask.id())
- .contextId(sampleTask.contextId())
- .status(new TaskStatus(TaskState.TASK_STATE_WORKING, sampleMessage, null))
- .build();
-
- Task updatedTask1 = taskManager.saveTaskEvent(statusUpdate1);
- assertEquals(TaskState.TASK_STATE_WORKING, updatedTask1.status().state());
- assertEquals(1, updatedTask1.history().size());
-
- // Second status update
- Message secondMessage = Message.builder()
- .messageId("msg2")
- .role(Message.Role.ROLE_AGENT)
- .parts(Collections.singletonList(new TextPart("Second message")))
- .build();
-
- TaskStatusUpdateEvent statusUpdate2 = TaskStatusUpdateEvent.builder()
- .taskId(sampleTask.id())
- .contextId(sampleTask.contextId())
- .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, secondMessage, null))
- .build();
-
- Task updatedTask2 = taskManager.saveTaskEvent(statusUpdate2);
- assertEquals(TaskState.TASK_STATE_COMPLETED, updatedTask2.status().state());
- assertEquals(2, updatedTask2.history().size());
- }
-
- @Test
- public void testSaveTaskEventMultipleArtifactUpdates() throws Exception {
- taskManager.saveTaskEvent(sampleTask);
-
- // First artifact update
- Artifact artifact1 = Artifact.builder()
- .artifactId("art1")
- .parts(Collections.singletonList(new TextPart("First artifact")))
- .build();
-
- TaskArtifactUpdateEvent artifactUpdate1 = TaskArtifactUpdateEvent.builder()
- .taskId(sampleTask.id())
- .contextId(sampleTask.contextId())
- .artifact(artifact1)
- .build();
-
- Task updatedTask1 = taskManager.saveTaskEvent(artifactUpdate1);
- assertEquals(1, updatedTask1.artifacts().size());
-
- // Second artifact update
- Artifact artifact2 = Artifact.builder()
- .artifactId("art2")
- .parts(Collections.singletonList(new TextPart("Second artifact")))
- .build();
-
- TaskArtifactUpdateEvent artifactUpdate2 = TaskArtifactUpdateEvent.builder()
- .taskId(sampleTask.id())
- .contextId(sampleTask.contextId())
- .artifact(artifact2)
- .build();
-
- Task updatedTask2 = taskManager.saveTaskEvent(artifactUpdate2);
- assertEquals(2, updatedTask2.artifacts().size());
- }
-
- @Test
- public void testSaveTaskEventWithEmptyContextId() throws Exception {
- TaskStatusUpdateEvent statusUpdate = TaskStatusUpdateEvent.builder()
- .taskId("task_with_empty_context")
- .contextId("")
- .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED))
- .build();
-
- Task updatedTask = taskManager.saveTaskEvent(statusUpdate);
-
- assertNotNull(updatedTask);
- assertEquals("task_with_empty_context", updatedTask.id());
- assertEquals("", updatedTask.contextId());
- }
-}
diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml
deleted file mode 100644
index 8ca49f274..000000000
--- a/client/transport/grpc/pom.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
- 4.0.0
-
-
- io.github.a2asdk
- a2a-java-sdk-parent
- 1.0.0.Alpha4-SNAPSHOT
- ../../../pom.xml
-
- a2a-java-sdk-client-transport-grpc
- jar
-
- Java SDK A2A Client Transport: gRPC
- Java SDK for the Agent2Agent Protocol (A2A) - gRPC Client Transport
-
-
-
- ${project.groupId}
- a2a-java-sdk-common
-
-
- ${project.groupId}
- a2a-java-sdk-spec
-
-
- ${project.groupId}
- a2a-java-sdk-jsonrpc-common
- ${project.version}
-
-
- ${project.groupId}
- a2a-java-sdk-spec-grpc
-
-
- ${project.groupId}
- a2a-java-sdk-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/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/EventStreamObserver.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/EventStreamObserver.java
deleted file mode 100644
index 0c928bd75..000000000
--- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/EventStreamObserver.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package io.a2a.client.transport.grpc;
-
-
-import static io.a2a.grpc.utils.ProtoUtils.FromProto;
-
-import java.util.function.Consumer;
-import java.util.logging.Logger;
-
-import io.a2a.grpc.StreamResponse;
-import io.a2a.spec.StreamingEventKind;
-import io.grpc.stub.StreamObserver;
-
-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 MESSAGE:
- event = FromProto.message(response.getMessage());
- 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/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java
deleted file mode 100644
index b9ad3325b..000000000
--- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package io.a2a.client.transport.grpc;
-
-import io.a2a.common.A2AErrorMessages;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.ContentTypeNotSupportedError;
-import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
-import io.a2a.spec.ExtensionSupportRequiredError;
-import io.a2a.spec.InvalidAgentResponseError;
-import io.a2a.spec.InvalidParamsError;
-import io.a2a.spec.InvalidRequestError;
-import io.a2a.spec.JSONParseError;
-import io.a2a.spec.MethodNotFoundError;
-import io.a2a.spec.PushNotificationNotSupportedError;
-import io.a2a.spec.TaskNotCancelableError;
-import io.a2a.spec.TaskNotFoundError;
-import io.a2a.spec.UnsupportedOperationError;
-import io.a2a.spec.VersionNotSupportedError;
-import io.grpc.Status;
-
-/**
- * Utility class to map gRPC exceptions to appropriate A2A error types
- */
-public class GrpcErrorMapper {
-
- public static A2AClientException mapGrpcError(Throwable e) {
- return mapGrpcError(e, "gRPC error: ");
- }
-
- public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) {
- Status status = Status.fromThrowable(e);
- Status.Code code = status.getCode();
- String description = status.getDescription();
-
- // 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));
- } else if (description.contains("ExtendedCardNotConfiguredError")) {
- return new A2AClientException(errorPrefix + description, new ExtendedAgentCardNotConfiguredError(null, description, null));
- } else if (description.contains("ExtensionSupportRequiredError")) {
- return new A2AClientException(errorPrefix + description, new ExtensionSupportRequiredError(null, description, null));
- } else if (description.contains("VersionNotSupportedError")) {
- return new A2AClientException(errorPrefix + description, new VersionNotSupportedError(null, description, null));
- }
- }
-
- // Fall back to mapping based on status code
- switch (code) {
- case NOT_FOUND:
- return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new TaskNotFoundError());
- case UNIMPLEMENTED:
- return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new UnsupportedOperationError());
- case INVALID_ARGUMENT:
- return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new InvalidParamsError());
- case INTERNAL:
- return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new io.a2a.spec.InternalError(null, e.getMessage(), 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 + e.getMessage(), e);
- }
- }
-}
diff --git a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java
deleted file mode 100644
index ca849d99c..000000000
--- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java
+++ /dev/null
@@ -1,469 +0,0 @@
-package io.a2a.client.transport.grpc;
-
-import static io.a2a.spec.A2AMethods.CANCEL_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD;
-import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.LIST_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.SEND_MESSAGE_METHOD;
-import static io.a2a.spec.A2AMethods.SEND_STREAMING_MESSAGE_METHOD;
-import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD;
-import static io.a2a.util.Assert.checkNotNullParam;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import io.a2a.client.transport.spi.ClientTransport;
-import io.a2a.client.transport.spi.interceptors.ClientCallContext;
-import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
-import io.a2a.client.transport.spi.interceptors.PayloadAndHeaders;
-import io.a2a.client.transport.spi.interceptors.auth.AuthInterceptor;
-import io.a2a.common.A2AHeaders;
-import io.a2a.grpc.A2AServiceGrpc;
-import io.a2a.grpc.A2AServiceGrpc.A2AServiceBlockingV2Stub;
-import io.a2a.grpc.A2AServiceGrpc.A2AServiceStub;
-import io.a2a.grpc.GetExtendedAgentCardRequest;
-import io.a2a.grpc.utils.ProtoUtils.FromProto;
-import io.a2a.grpc.utils.ProtoUtils.ToProto;
-import io.a2a.jsonrpc.common.wrappers.ListTasksResult;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
-import io.a2a.spec.EventKind;
-import io.a2a.spec.GetTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigResult;
-import io.a2a.spec.ListTasksParams;
-import io.a2a.spec.MessageSendParams;
-import io.a2a.spec.StreamingEventKind;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskPushNotificationConfig;
-import io.a2a.spec.TaskQueryParams;
-import io.grpc.Channel;
-import io.grpc.Metadata;
-import io.grpc.StatusException;
-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(
- A2AHeaders.X_A2A_EXTENSIONS,
- Metadata.ASCII_STRING_MARSHALLER);
- private static final Metadata.Key VERSION_KEY = Metadata.Key.of(
- A2AHeaders.X_A2A_VERSION,
- Metadata.ASCII_STRING_MARSHALLER);
- private final A2AServiceBlockingV2Stub blockingStub;
- private final A2AServiceStub asyncStub;
- private final @Nullable List interceptors;
- private final AgentCard agentCard;
- private final String agentTenant;
-
- public GrpcTransport(Channel channel, AgentCard agentCard) {
- this(channel, agentCard, "", null);
- }
-
- public GrpcTransport(Channel channel, AgentCard agentCard, @Nullable String agentTenant, @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;
- this.agentTenant = agentTenant == null || agentTenant.isBlank() ? "" : agentTenant;
- }
-
- /**
- * Resolves the tenant to use, preferring the request tenant over the agent default.
- *
- * @param requestTenant the tenant from the request, may be null or blank
- * @return the tenant to use (request tenant if provided, otherwise agent default)
- */
- private String resolveTenant(@Nullable String requestTenant) {
- return (requestTenant == null || requestTenant.isBlank()) ? agentTenant : requestTenant;
- }
-
- @Override
- public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- MessageSendParams tenantRequest = createRequestWithTenant(request);
-
- io.a2a.grpc.SendMessageRequest sendMessageRequest = createGrpcSendMessageRequest(tenantRequest, context);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_MESSAGE_METHOD, sendMessageRequest,
- agentCard, context);
-
- try {
- A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
- io.a2a.grpc.SendMessageResponse response = stubWithMetadata.sendMessage(sendMessageRequest);
- if (response.hasMessage()) {
- return FromProto.message(response.getMessage());
- } 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);
- MessageSendParams tenantRequest = createRequestWithTenant(request);
-
- io.a2a.grpc.SendMessageRequest grpcRequest = createGrpcSendMessageRequest(tenantRequest, context);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_STREAMING_MESSAGE_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);
- io.a2a.grpc.GetTaskRequest.Builder requestBuilder = io.a2a.grpc.GetTaskRequest.newBuilder();
- requestBuilder.setId(request.id());
- if (request.historyLength() != null) {
- requestBuilder.setHistoryLength(request.historyLength());
- }
- requestBuilder.setTenant(resolveTenant(request.tenant()));
- io.a2a.grpc.GetTaskRequest getTaskRequest = requestBuilder.build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_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);
-
- io.a2a.grpc.CancelTaskRequest cancelTaskRequest = io.a2a.grpc.CancelTaskRequest.newBuilder()
- .setId(request.id())
- .setTenant(resolveTenant(request.tenant()))
- .build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(CANCEL_TASK_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 ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
-
- io.a2a.grpc.ListTasksRequest.Builder requestBuilder = io.a2a.grpc.ListTasksRequest.newBuilder();
- if (request.contextId() != null) {
- requestBuilder.setContextId(request.contextId());
- }
- if (request.status() != null) {
- requestBuilder.setStatus(ToProto.taskState(request.status()));
- }
- if (request.pageSize() != null) {
- requestBuilder.setPageSize(request.pageSize());
- }
- if (request.pageToken() != null) {
- requestBuilder.setPageToken(request.pageToken());
- }
- if (request.historyLength() != null) {
- requestBuilder.setHistoryLength(request.historyLength());
- }
- if (request.statusTimestampAfter() != null) {
- requestBuilder.setStatusTimestampAfter(
- com.google.protobuf.Timestamp.newBuilder()
- .setSeconds(request.statusTimestampAfter().getEpochSecond())
- .setNanos(request.statusTimestampAfter().getNano())
- .build());
- }
- if (request.includeArtifacts() != null) {
- requestBuilder.setIncludeArtifacts(request.includeArtifacts());
- }
- requestBuilder.setTenant(resolveTenant(request.tenant()));
- io.a2a.grpc.ListTasksRequest listTasksRequest = requestBuilder.build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_METHOD, listTasksRequest, agentCard, context);
-
- try {
- A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
- io.a2a.grpc.ListTasksResponse grpcResponse = stubWithMetadata.listTasks(listTasksRequest);
-
- return new ListTasksResult(
- grpcResponse.getTasksList().stream()
- .map(FromProto::task)
- .collect(Collectors.toList()),
- grpcResponse.getTotalSize(),
- grpcResponse.getTasksCount(),
- grpcResponse.getNextPageToken().isEmpty() ? null : grpcResponse.getNextPageToken()
- );
- } catch (StatusRuntimeException | StatusException e) {
- throw GrpcErrorMapper.mapGrpcError(e, "Failed to list tasks: ");
- }
- }
-
- @Override
- public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
- @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
-
- String configId = request.config().id();
- io.a2a.grpc.CreateTaskPushNotificationConfigRequest grpcRequest = io.a2a.grpc.CreateTaskPushNotificationConfigRequest.newBuilder()
- .setTaskId(request.taskId())
- .setConfig(ToProto.taskPushNotificationConfig(request).getPushNotificationConfig())
- .setConfigId(configId != null ? configId : request.taskId())
- .setTenant(resolveTenant(request.tenant()))
- .build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SET_TASK_PUSH_NOTIFICATION_CONFIG_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);
- checkNotNullParam("taskId", request.taskId());
- if(request.id() == null) {
- throw new IllegalArgumentException("Id must not be null");
- }
-
- io.a2a.grpc.GetTaskPushNotificationConfigRequest grpcRequest = io.a2a.grpc.GetTaskPushNotificationConfigRequest.newBuilder()
- .setTaskId(request.taskId())
- .setTenant(resolveTenant(request.tenant()))
- .setId(request.id())
- .build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_PUSH_NOTIFICATION_CONFIG_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 ListTaskPushNotificationConfigResult listTaskPushNotificationConfigurations(
- ListTaskPushNotificationConfigParams request,
- @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
-
- io.a2a.grpc.ListTaskPushNotificationConfigRequest grpcRequest = io.a2a.grpc.ListTaskPushNotificationConfigRequest.newBuilder()
- .setTaskId(request.id())
- .setTenant(resolveTenant(request.tenant()))
- .setPageSize(request.pageSize())
- .setPageToken(request.pageToken())
- .build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
- grpcRequest, agentCard, context);
-
- try {
- A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
- io.a2a.grpc.ListTaskPushNotificationConfigResponse grpcResponse = stubWithMetadata.listTaskPushNotificationConfig(grpcRequest);
- return FromProto.listTaskPushNotificationConfigResult(grpcResponse);
- } 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);
-
- io.a2a.grpc.DeleteTaskPushNotificationConfigRequest grpcRequest = io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder()
- .setTaskId(request.taskId())
- .setId(request.id())
- .setTenant(resolveTenant(request.tenant()))
- .build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(DELETE_TASK_PUSH_NOTIFICATION_CONFIG_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 subscribeToTask(TaskIdParams request, Consumer eventConsumer,
- Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- checkNotNullParam("eventConsumer", eventConsumer);
-
- io.a2a.grpc.SubscribeToTaskRequest grpcRequest = io.a2a.grpc.SubscribeToTaskRequest.newBuilder()
- .setTenant(resolveTenant(request.tenant()))
- .setId(request.id())
- .build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SUBSCRIBE_TO_TASK_METHOD, grpcRequest, agentCard, context);
-
- StreamObserver streamObserver = new EventStreamObserver(eventConsumer, errorConsumer);
-
- try {
- A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders);
- stubWithMetadata.subscribeToTask(grpcRequest, streamObserver);
- } catch (StatusRuntimeException e) {
- throw GrpcErrorMapper.mapGrpcError(e, "Failed to subscribe task push notification config: ");
- }
- }
-
- /**
- * Ensure tenant is set, using agent default if not provided in request
- *
- * @param request the initial request.
- * @return the updated request with the tenant set.
- */
- private MessageSendParams createRequestWithTenant(MessageSendParams request) {
- return MessageSendParams.builder()
- .configuration(request.configuration())
- .message(request.message())
- .metadata(request.metadata())
- .tenant(resolveTenant(request.tenant()))
- .build();
- }
-
- @Override
- public AgentCard getExtendedAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
- GetExtendedAgentCardRequest request = GetExtendedAgentCardRequest.newBuilder()
- .build();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_EXTENDED_AGENT_CARD_METHOD, request, agentCard, context);
-
- try {
- A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
- io.a2a.grpc.AgentCard response = stubWithMetadata.getExtendedAgentCard(request);
-
- return FromProto.agentCard(response);
- } catch (StatusRuntimeException | StatusException e) {
- throw GrpcErrorMapper.mapGrpcError(e, "Failed to get extended agent card: ");
- }
- }
-
- @Override
- public void close() {
- }
-
- private io.a2a.grpc.SendMessageRequest createGrpcSendMessageRequest(MessageSendParams messageSendParams, @Nullable ClientCallContext context) {
- return ToProto.sendMessageRequest(messageSendParams);
- }
-
- /**
- * 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-Version header if present
- String versionHeader = context.getHeaders().get(A2AHeaders.X_A2A_VERSION);
- if (versionHeader != null) {
- metadata.put(VERSION_KEY, versionHeader);
- }
-
- // Set X-A2A-Extensions header if present
- String extensionsHeader = context.getHeaders().get(A2AHeaders.X_A2A_EXTENSIONS);
- if (extensionsHeader != null) {
- metadata.put(EXTENSIONS_KEY, extensionsHeader);
- }
-
- // Add other headers as needed in the future
- }
- 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 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;
- }
-
-}
diff --git a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportConfig.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportConfig.java
deleted file mode 100644
index 605f7abb0..000000000
--- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportConfig.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package io.a2a.client.transport.grpc;
-
-import java.util.function.Function;
-
-import io.a2a.client.transport.spi.ClientTransportConfig;
-import io.a2a.util.Assert;
-import io.grpc.Channel;
-
-/**
- * Configuration for the gRPC transport protocol.
- *
- * This configuration class allows customization of the gRPC channel factory used for
- * communication with A2A agents. Unlike other transports, gRPC requires a channel factory
- * to be explicitly provided - there is no default implementation.
- *
- * Channel Factory Requirement: You must provide a {@code Function}
- * that creates gRPC channels from agent URLs. This gives you full control over channel
- * configuration including connection pooling, TLS, load balancing, and interceptors.
- *
- * Basic usage with ManagedChannel:
- *
{@code
- * // Simple insecure channel for development
- * Function channelFactory = url -> {
- * String target = extractTarget(url); // e.g., "localhost:9999"
- * return ManagedChannelBuilder.forTarget(target)
- * .usePlaintext()
- * .build();
- * };
- *
- * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
- * .channelFactory(channelFactory)
- * .build();
- *
- * Client client = Client.builder(agentCard)
- * .withTransport(GrpcTransport.class, config)
- * .build();
- * }
- *
- * Production configuration with TLS and timeouts:
- *
{@code
- * Function channelFactory = url -> {
- * String target = extractTarget(url);
- * return ManagedChannelBuilder.forTarget(target)
- * .useTransportSecurity()
- * .keepAliveTime(30, TimeUnit.SECONDS)
- * .idleTimeout(5, TimeUnit.MINUTES)
- * .maxInboundMessageSize(10 * 1024 * 1024) // 10MB
- * .build();
- * };
- *
- * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
- * .channelFactory(channelFactory)
- * .build();
- * }
- *
- * With load balancing and connection pooling:
- *
{@code
- * Function channelFactory = url -> {
- * String target = extractTarget(url);
- * return ManagedChannelBuilder.forTarget(target)
- * .defaultLoadBalancingPolicy("round_robin")
- * .maxInboundMessageSize(50 * 1024 * 1024)
- * .keepAliveTime(30, TimeUnit.SECONDS)
- * .keepAliveTimeout(10, TimeUnit.SECONDS)
- * .build();
- * };
- * }
- *
- * With interceptors:
- *
{@code
- * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
- * .channelFactory(channelFactory)
- * .addInterceptor(new LoggingInterceptor())
- * .addInterceptor(new AuthInterceptor(apiKey))
- * .build();
- * }
- *
- * Channel Lifecycle: The channel factory creates channels on-demand when the client
- * connects to an agent. You are responsible for shutting down channels when the client is
- * closed. Consider using {@code ManagedChannel.shutdown()} in a cleanup hook.
- *
- * @see GrpcTransportConfigBuilder
- * @see GrpcTransport
- * @see io.a2a.client.transport.spi.ClientTransportConfig
- * @see io.grpc.ManagedChannelBuilder
- */
-public class GrpcTransportConfig extends ClientTransportConfig {
-
- private final Function channelFactory;
-
- /**
- * Create a gRPC transport configuration with a custom channel factory.
- *
- * Consider using {@link GrpcTransportConfigBuilder} instead for a more fluent API.
- *
- * @param channelFactory function to create gRPC channels from agent URLs (must not be null)
- * @throws IllegalArgumentException if channelFactory is null
- */
- public GrpcTransportConfig(Function channelFactory) {
- Assert.checkNotNullParam("channelFactory", channelFactory);
- this.channelFactory = channelFactory;
- }
-
- /**
- * Get the configured channel factory.
- *
- * @return the channel factory function
- */
- public Function getChannelFactory() {
- return this.channelFactory;
- }
-}
\ No newline at end of file
diff --git a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportConfigBuilder.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportConfigBuilder.java
deleted file mode 100644
index 9ffcc1285..000000000
--- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportConfigBuilder.java
+++ /dev/null
@@ -1,209 +0,0 @@
-package io.a2a.client.transport.grpc;
-
-import java.util.function.Function;
-
-import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
-import io.a2a.util.Assert;
-import io.grpc.Channel;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Builder for creating {@link GrpcTransportConfig} instances.
- *
- * This builder provides a fluent API for configuring the gRPC transport protocol.
- * Unlike other transports, gRPC requires a channel factory to be explicitly provided -
- * the {@link #channelFactory(Function)} method must be called before {@link #build()}.
- *
- * The channel factory gives you complete control over gRPC channel configuration:
- *
- * - Connection management: Connection pooling, keep-alive settings
- * - Security: TLS configuration, client certificates
- * - Performance: Message size limits, compression, load balancing
- * - Timeouts: Deadline configuration, idle timeout
- * - Interceptors: Request/response transformation, authentication
- *
- *
- * Basic development setup (insecure):
- *
{@code
- * // Simple channel for local development
- * Function channelFactory = url -> {
- * // Extract "localhost:9999" from "http://localhost:9999"
- * String target = url.replaceAll("^https?://", "");
- * return ManagedChannelBuilder.forTarget(target)
- * .usePlaintext() // No TLS
- * .build();
- * };
- *
- * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
- * .channelFactory(channelFactory)
- * .build();
- *
- * Client client = Client.builder(agentCard)
- * .withTransport(GrpcTransport.class, config)
- * .build();
- * }
- *
- * Production setup with TLS and connection pooling:
- *
{@code
- * Function channelFactory = url -> {
- * String target = extractTarget(url);
- * return ManagedChannelBuilder.forTarget(target)
- * .useTransportSecurity() // Enable TLS
- * .keepAliveTime(30, TimeUnit.SECONDS)
- * .keepAliveTimeout(10, TimeUnit.SECONDS)
- * .idleTimeout(5, TimeUnit.MINUTES)
- * .maxInboundMessageSize(10 * 1024 * 1024) // 10MB messages
- * .build();
- * };
- *
- * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
- * .channelFactory(channelFactory)
- * .build();
- * }
- *
- * With custom SSL certificates:
- *
{@code
- * SslContext sslContext = GrpcSslContexts.forClient()
- * .trustManager(new File("ca.crt"))
- * .keyManager(new File("client.crt"), new File("client.key"))
- * .build();
- *
- * Function channelFactory = url -> {
- * String target = extractTarget(url);
- * return NettyChannelBuilder.forTarget(target)
- * .sslContext(sslContext)
- * .build();
- * };
- *
- * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
- * .channelFactory(channelFactory)
- * .build();
- * }
- *
- * With load balancing and health checks:
- *
{@code
- * Function channelFactory = url -> {
- * String target = extractTarget(url);
- * return ManagedChannelBuilder.forTarget(target)
- * .defaultLoadBalancingPolicy("round_robin")
- * .enableRetry()
- * .maxRetryAttempts(3)
- * .build();
- * };
- *
- * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
- * .channelFactory(channelFactory)
- * .build();
- * }
- *
- * With A2A interceptors:
- *
{@code
- * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
- * .channelFactory(channelFactory)
- * .addInterceptor(new LoggingInterceptor())
- * .addInterceptor(new MetricsInterceptor())
- * .addInterceptor(new AuthenticationInterceptor(apiKey))
- * .build();
- * }
- *
- * Direct usage in ClientBuilder:
- *
{@code
- * // Channel factory inline
- * Client client = Client.builder(agentCard)
- * .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder()
- * .channelFactory(url -> ManagedChannelBuilder
- * .forTarget(extractTarget(url))
- * .usePlaintext()
- * .build())
- * .addInterceptor(loggingInterceptor))
- * .build();
- * }
- *
- * Channel Lifecycle Management:
- *
{@code
- * // Store channels for cleanup
- * Map channels = new ConcurrentHashMap<>();
- *
- * Function channelFactory = url -> {
- * return channels.computeIfAbsent(url, u -> {
- * String target = extractTarget(u);
- * return ManagedChannelBuilder.forTarget(target)
- * .usePlaintext()
- * .build();
- * });
- * };
- *
- * // Cleanup when done
- * Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- * channels.values().forEach(ManagedChannel::shutdown);
- * }));
- * }
- *
- * @see GrpcTransportConfig
- * @see GrpcTransport
- * @see io.a2a.client.transport.spi.ClientTransportConfigBuilder
- * @see io.grpc.ManagedChannelBuilder
- * @see io.grpc.Channel
- */
-public class GrpcTransportConfigBuilder extends ClientTransportConfigBuilder {
-
- private @Nullable Function channelFactory;
-
- /**
- * Set the channel factory for creating gRPC channels.
- *
- * This method is required - {@link #build()} will throw {@link IllegalStateException}
- * if the channel factory is not set.
- *
- * The factory function receives the agent's URL (e.g., "http://localhost:9999") and must
- * return a configured {@link Channel}. You are responsible for:
- *
- * - Extracting the target address from the URL
- * - Configuring TLS and security settings
- * - Setting connection pool and timeout parameters
- * - Managing channel lifecycle and shutdown
- *
- *
- * Example:
- *
{@code
- * Function factory = url -> {
- * String target = url.replaceAll("^https?://", "");
- * return ManagedChannelBuilder.forTarget(target)
- * .usePlaintext()
- * .build();
- * };
- *
- * builder.channelFactory(factory);
- * }
- *
- * @param channelFactory function to create gRPC channels from agent URLs (must not be null)
- * @return this builder for method chaining
- * @throws IllegalArgumentException if channelFactory is null
- */
- public GrpcTransportConfigBuilder channelFactory(Function channelFactory) {
- Assert.checkNotNullParam("channelFactory", channelFactory);
-
- this.channelFactory = channelFactory;
-
- return this;
- }
-
- /**
- * Build the gRPC transport configuration.
- *
- * The channel factory must have been set via {@link #channelFactory(Function)} before
- * calling this method. Any configured interceptors are transferred to the configuration.
- *
- * @return the configured gRPC transport configuration
- * @throws IllegalStateException if the channel factory was not set
- */
- @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/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportProvider.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportProvider.java
deleted file mode 100644
index d331d0874..000000000
--- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportProvider.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package io.a2a.client.transport.grpc;
-
-import io.a2a.client.transport.spi.ClientTransportProvider;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentInterface;
-import io.a2a.spec.TransportProtocol;
-import io.grpc.Channel;
-
-/**
- * Provider for gRPC transport implementation.
- */
-public class GrpcTransportProvider implements ClientTransportProvider {
-
- @Override
- public GrpcTransport create(GrpcTransportConfig grpcTransportConfig, AgentCard agentCard, AgentInterface agentInterface) throws A2AClientException {
- // not making use of the interceptors for gRPC for now
-
- Channel channel = grpcTransportConfig.getChannelFactory().apply(agentInterface.url());
- if (channel != null) {
- return new GrpcTransport(channel, agentCard, agentInterface.tenant(), 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/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/package-info.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/package-info.java
deleted file mode 100644
index c0c12ccb5..000000000
--- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-@NullMarked
-package io.a2a.client.transport.grpc;
-
-import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/client/transport/grpc/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider b/client/transport/grpc/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider
deleted file mode 100644
index 86d4fa7e5..000000000
--- a/client/transport/grpc/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider
+++ /dev/null
@@ -1 +0,0 @@
-io.a2a.client.transport.grpc.GrpcTransportProvider
\ No newline at end of file
diff --git a/client/transport/grpc/src/test/java/io/a2a/client/transport/grpc/GrpcErrorMapperTest.java b/client/transport/grpc/src/test/java/io/a2a/client/transport/grpc/GrpcErrorMapperTest.java
deleted file mode 100644
index 864e406d2..000000000
--- a/client/transport/grpc/src/test/java/io/a2a/client/transport/grpc/GrpcErrorMapperTest.java
+++ /dev/null
@@ -1,194 +0,0 @@
-package io.a2a.client.transport.grpc;
-
-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 io.a2a.spec.A2AClientException;
-import io.a2a.spec.ContentTypeNotSupportedError;
-import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
-import io.a2a.spec.ExtensionSupportRequiredError;
-import io.a2a.spec.InvalidParamsError;
-import io.a2a.spec.TaskNotFoundError;
-import io.a2a.spec.UnsupportedOperationError;
-import io.a2a.spec.VersionNotSupportedError;
-import io.grpc.Status;
-import io.grpc.StatusRuntimeException;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests for GrpcErrorMapper - verifies correct unmarshalling of gRPC errors to A2A error types
- */
-public class GrpcErrorMapperTest {
-
- @Test
- public void testExtensionSupportRequiredErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with ExtensionSupportRequiredError in description
- String errorMessage = "ExtensionSupportRequiredError: Extension required: https://example.com/test-extension";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
-
- // Map the gRPC error to A2A error
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
-
- // Verify the result
- assertNotNull(result);
- assertNotNull(result.getCause());
- assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause());
-
- ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) result.getCause();
- assertNotNull(extensionError.getMessage());
- assertTrue(extensionError.getMessage().contains("https://example.com/test-extension"));
- assertTrue(result.getMessage().contains(errorMessage));
- }
-
- @Test
- public void testVersionNotSupportedErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with VersionNotSupportedError in description
- String errorMessage = "VersionNotSupportedError: Version 2.0 is not supported";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
-
- // Map the gRPC error to A2A error
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
-
- // Verify the result
- assertNotNull(result);
- assertNotNull(result.getCause());
- assertInstanceOf(VersionNotSupportedError.class, result.getCause());
-
- VersionNotSupportedError versionError = (VersionNotSupportedError) result.getCause();
- assertNotNull(versionError.getMessage());
- assertTrue(versionError.getMessage().contains("Version 2.0 is not supported"));
- }
-
- @Test
- public void testExtendedCardNotConfiguredErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with ExtendedCardNotConfiguredError in description
- String errorMessage = "ExtendedCardNotConfiguredError: Extended card not configured for this agent";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
-
- // Map the gRPC error to A2A error
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
-
- // Verify the result
- assertNotNull(result);
- assertNotNull(result.getCause());
- assertInstanceOf(ExtendedAgentCardNotConfiguredError.class, result.getCause());
-
- ExtendedAgentCardNotConfiguredError extendedCardError = (ExtendedAgentCardNotConfiguredError) result.getCause();
- assertNotNull(extendedCardError.getMessage());
- assertTrue(extendedCardError.getMessage().contains("Extended card not configured"));
- }
-
- @Test
- public void testTaskNotFoundErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with TaskNotFoundError in description
- String errorMessage = "TaskNotFoundError: Task task-123 not found";
- StatusRuntimeException grpcException = Status.NOT_FOUND
- .withDescription(errorMessage)
- .asRuntimeException();
-
- // Map the gRPC error to A2A error
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
-
- // Verify the result
- assertNotNull(result);
- assertNotNull(result.getCause());
- assertInstanceOf(TaskNotFoundError.class, result.getCause());
- }
-
- @Test
- public void testUnsupportedOperationErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with UnsupportedOperationError in description
- String errorMessage = "UnsupportedOperationError: Operation not supported";
- StatusRuntimeException grpcException = Status.UNIMPLEMENTED
- .withDescription(errorMessage)
- .asRuntimeException();
-
- // Map the gRPC error to A2A error
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
-
- // Verify the result
- assertNotNull(result);
- assertNotNull(result.getCause());
- assertInstanceOf(UnsupportedOperationError.class, result.getCause());
- }
-
- @Test
- public void testInvalidParamsErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with InvalidParamsError in description
- String errorMessage = "InvalidParamsError: Invalid parameters provided";
- StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
- .withDescription(errorMessage)
- .asRuntimeException();
-
- // Map the gRPC error to A2A error
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
-
- // Verify the result
- assertNotNull(result);
- assertNotNull(result.getCause());
- assertInstanceOf(InvalidParamsError.class, result.getCause());
- }
-
- @Test
- public void testContentTypeNotSupportedErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with ContentTypeNotSupportedError in description
- String errorMessage = "ContentTypeNotSupportedError: Content type application/xml not supported";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
-
- // Map the gRPC error to A2A error
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
-
- // Verify the result
- 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 testFallbackToStatusCodeMapping() {
- // Create a gRPC StatusRuntimeException without specific error type in description
- StatusRuntimeException grpcException = Status.NOT_FOUND
- .withDescription("Generic not found error")
- .asRuntimeException();
-
- // Map the gRPC error to A2A error
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
-
- // Verify fallback to status code mapping
- assertNotNull(result);
- assertNotNull(result.getCause());
- assertInstanceOf(TaskNotFoundError.class, result.getCause());
- }
-
- @Test
- public void testCustomErrorPrefix() {
- // Create a gRPC StatusRuntimeException
- String errorMessage = "ExtensionSupportRequiredError: Extension required: https://example.com/ext";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
-
- // Map with custom error prefix
- String customPrefix = "Custom Error: ";
- A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException, customPrefix);
-
- // Verify custom prefix is used
- assertNotNull(result);
- assertTrue(result.getMessage().startsWith(customPrefix));
- assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause());
- }
-}
diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml
deleted file mode 100644
index f0ffd3407..000000000
--- a/client/transport/jsonrpc/pom.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
- 4.0.0
-
-
- io.github.a2asdk
- a2a-java-sdk-parent
- 1.0.0.Alpha4-SNAPSHOT
- ../../../pom.xml
-
- a2a-java-sdk-client-transport-jsonrpc
- jar
-
- Java SDK A2A Client Transport: JSONRPC
- Java SDK for the Agent2Agent Protocol (A2A) - JSONRPC Client Transport
-
-
-
- ${project.groupId}
- a2a-java-sdk-http-client
-
-
- ${project.groupId}
- a2a-java-sdk-client-transport-spi
-
-
- ${project.groupId}
- a2a-java-sdk-common
-
-
- ${project.groupId}
- a2a-java-sdk-spec
-
-
- ${project.groupId}
- a2a-java-sdk-jsonrpc-common
- ${project.version}
-
-
- ${project.groupId}
- a2a-java-sdk-spec-grpc
-
-
- com.google.protobuf
- protobuf-java-util
- provided
-
-
- com.google.protobuf
- protobuf-java
- provided
-
-
- org.junit.jupiter
- junit-jupiter-api
- test
-
-
-
- org.mock-server
- mockserver-netty
- test
-
-
-
-
\ No newline at end of file
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
deleted file mode 100644
index 9b8af4ea2..000000000
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
+++ /dev/null
@@ -1,374 +0,0 @@
-package io.a2a.client.transport.jsonrpc;
-
-import static io.a2a.spec.A2AMethods.CANCEL_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD;
-import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.LIST_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.SEND_MESSAGE_METHOD;
-import static io.a2a.spec.A2AMethods.SEND_STREAMING_MESSAGE_METHOD;
-import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD;
-import static io.a2a.util.Assert.checkNotNullParam;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-import com.google.protobuf.MessageOrBuilder;
-
-import io.a2a.client.http.A2AHttpClient;
-import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.http.A2AHttpResponse;
-import io.a2a.client.transport.jsonrpc.sse.SSEEventListener;
-import io.a2a.client.transport.spi.ClientTransport;
-import io.a2a.client.transport.spi.interceptors.ClientCallContext;
-import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
-import io.a2a.client.transport.spi.interceptors.PayloadAndHeaders;
-import io.a2a.grpc.utils.JSONRPCUtils;
-import io.a2a.grpc.utils.ProtoUtils;
-import io.a2a.jsonrpc.common.json.JsonProcessingException;
-import io.a2a.jsonrpc.common.wrappers.A2AResponse;
-import io.a2a.jsonrpc.common.wrappers.CancelTaskResponse;
-import io.a2a.jsonrpc.common.wrappers.DeleteTaskPushNotificationConfigResponse;
-import io.a2a.jsonrpc.common.wrappers.GetExtendedAgentCardResponse;
-import io.a2a.jsonrpc.common.wrappers.GetTaskPushNotificationConfigResponse;
-import io.a2a.jsonrpc.common.wrappers.GetTaskResponse;
-import io.a2a.jsonrpc.common.wrappers.ListTaskPushNotificationConfigResponse;
-import io.a2a.jsonrpc.common.wrappers.ListTasksResponse;
-import io.a2a.jsonrpc.common.wrappers.ListTasksResult;
-import io.a2a.jsonrpc.common.wrappers.SendMessageResponse;
-import io.a2a.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse;
-import io.a2a.spec.A2AClientError;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.A2AError;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentInterface;
-import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
-import io.a2a.spec.EventKind;
-import io.a2a.spec.GetTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigResult;
-import io.a2a.spec.ListTasksParams;
-import io.a2a.spec.MessageSendParams;
-import io.a2a.spec.StreamingEventKind;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskPushNotificationConfig;
-import io.a2a.spec.TaskQueryParams;
-import io.a2a.util.Utils;
-import org.jspecify.annotations.Nullable;
-
-public class JSONRPCTransport implements ClientTransport {
-
- private final A2AHttpClient httpClient;
- private final AgentInterface agentInterface;
- private final @Nullable List interceptors;
- private final @Nullable AgentCard agentCard;
-
- public JSONRPCTransport(String agentUrl) {
- this(null, null, new AgentInterface("JSONRPC", agentUrl), null);
- }
-
- public JSONRPCTransport(AgentCard agentCard) {
- this(null, agentCard, Utils.getFavoriteInterface(agentCard), null);
- }
-
- public JSONRPCTransport(@Nullable A2AHttpClient httpClient, @Nullable AgentCard agentCard,
- AgentInterface agentInterface, @Nullable List interceptors) {
- this.httpClient = httpClient == null ? A2AHttpClientFactory.create() : httpClient;
- this.agentCard = agentCard;
- this.agentInterface = agentInterface;
- this.interceptors = interceptors;
- }
-
- @Override
- public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_MESSAGE_METHOD, ProtoUtils.ToProto.sendMessageRequest(request),
- agentCard, context);
-
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, SEND_MESSAGE_METHOD);
- SendMessageResponse response = unmarshalResponse(httpResponseBody, SEND_MESSAGE_METHOD);
- 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,
- @Nullable Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- checkNotNullParam("eventConsumer", eventConsumer);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_STREAMING_MESSAGE_METHOD,
- ProtoUtils.ToProto.sendMessageRequest(request), agentCard, context);
-
- final AtomicReference> ref = new AtomicReference<>();
- SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);
-
- try {
- A2AHttpClient.PostBuilder builder = createPostBuilder(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, SEND_STREAMING_MESSAGE_METHOD);
- 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, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_METHOD, ProtoUtils.ToProto.getTaskRequest(request),
- agentCard, context);
-
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, GET_TASK_METHOD);
- GetTaskResponse response = unmarshalResponse(httpResponseBody, GET_TASK_METHOD);
- 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, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(CANCEL_TASK_METHOD, ProtoUtils.ToProto.cancelTaskRequest(request),
- agentCard, context);
-
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, CANCEL_TASK_METHOD);
- CancelTaskResponse response = unmarshalResponse(httpResponseBody, CANCEL_TASK_METHOD);
- return response.getResult();
- } catch (A2AClientException e) {
- throw e;
- } catch (IOException | InterruptedException | JsonProcessingException e) {
- throw new A2AClientException("Failed to cancel task: " + e, e);
- }
- }
-
- @Override
- public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_METHOD, ProtoUtils.ToProto.listTasksParams(request),
- agentCard, context);
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, LIST_TASK_METHOD);
- ListTasksResponse response = unmarshalResponse(httpResponseBody, LIST_TASK_METHOD);
- return response.getResult();
- } catch (IOException | InterruptedException | JsonProcessingException e) {
- throw new A2AClientException("Failed to list tasks: " + e, e);
- }
- }
-
- @Override
- public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
- @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
- ProtoUtils.ToProto.createTaskPushNotificationConfigRequest(request), agentCard, context);
-
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
- CreateTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
- 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,
- @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
- ProtoUtils.ToProto.getTaskPushNotificationConfigRequest(request), agentCard, context);
-
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
- GetTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
- 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 ListTaskPushNotificationConfigResult listTaskPushNotificationConfigurations(
- ListTaskPushNotificationConfigParams request,
- @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
- ProtoUtils.ToProto.listTaskPushNotificationConfigRequest(request), agentCard, context);
-
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
- ListTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody, LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
- 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,
- @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD,
- ProtoUtils.ToProto.deleteTaskPushNotificationConfigRequest(request), agentCard, context);
-
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
- DeleteTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
- // Response validated (no error), but no result to return
- } 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 subscribeToTask(TaskIdParams request, Consumer eventConsumer,
- Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- checkNotNullParam("eventConsumer", eventConsumer);
- checkNotNullParam("errorConsumer", errorConsumer);
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SUBSCRIBE_TO_TASK_METHOD, ProtoUtils.ToProto.subscribeToTaskRequest(request), agentCard, context);
-
- AtomicReference> ref = new AtomicReference<>();
- SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);
-
- try {
- A2AHttpClient.PostBuilder builder = createPostBuilder(Utils.buildBaseUrl(agentInterface, request.tenant()), payloadAndHeaders, SUBSCRIBE_TO_TASK_METHOD);
- 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 getExtendedAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
- try {
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_EXTENDED_AGENT_CARD_METHOD,
- ProtoUtils.ToProto.extendedAgentCard(), agentCard, context);
-
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, ""), payloadAndHeaders, GET_EXTENDED_AGENT_CARD_METHOD);
- GetExtendedAgentCardResponse response = unmarshalResponse(httpResponseBody, GET_EXTENDED_AGENT_CARD_METHOD);
- return response.getResult();
- } 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, @Nullable Object payload,
- @Nullable AgentCard agentCard, @Nullable 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(String url, PayloadAndHeaders payloadAndHeaders, String method) throws IOException, InterruptedException, JsonProcessingException {
- A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders,method);
- A2AHttpResponse response = builder.post();
- if (!response.success()) {
- throw new IOException("Request failed " + response.status());
- }
- return response.body();
- }
-
- private A2AHttpClient.PostBuilder createPostBuilder(String url, PayloadAndHeaders payloadAndHeaders, String method) throws JsonProcessingException {
- A2AHttpClient.PostBuilder postBuilder = httpClient.createPost()
- .url(url)
- .addHeader("Content-Type", "application/json")
- .body(JSONRPCUtils.toJsonRPCRequest(null, method, (MessageOrBuilder) payloadAndHeaders.getPayload()));
-
- if (payloadAndHeaders.getHeaders() != null) {
- for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
- postBuilder.addHeader(entry.getKey(), entry.getValue());
- }
- }
-
- return postBuilder;
- }
-
- /**
- * Unmarshals a JSON-RPC response string into a type-safe response object.
- *
- * This method parses the JSON-RPC response body and returns the appropriate
- * response type based on the method parameter. If the response contains an error,
- * an A2AClientException is thrown.
- *
- * @param the expected response type, must extend JSONRPCResponse
- * @param response the JSON-RPC response body as a string
- * @param method the method name used to determine the response type
- * @return the parsed response object of type T
- * @throws A2AClientException if the response contains an error or parsing fails
- * @throws JsonProcessingException if the JSON cannot be processed
- */
- @SuppressWarnings("unchecked")
- private > T unmarshalResponse(String response, String method)
- throws A2AClientException, JsonProcessingException {
- A2AResponse> value = JSONRPCUtils.parseResponseBody(response, method);
- A2AError error = value.getError();
- if (error != null) {
- throw new A2AClientException(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), error);
- }
- // Safe cast: JSONRPCUtils.parseResponseBody returns the correct concrete type based on method
- return (T) value;
- }
-
- private @Nullable Map getHttpHeaders(@Nullable ClientCallContext context) {
- return context != null ? context.getHeaders() : null;
- }
-}
\ No newline at end of file
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfig.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfig.java
deleted file mode 100644
index 909ff079e..000000000
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfig.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package io.a2a.client.transport.jsonrpc;
-
-import io.a2a.client.http.A2AHttpClient;
-import io.a2a.client.transport.spi.ClientTransportConfig;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Configuration for the JSON-RPC transport protocol.
- *
- * This configuration class allows customization of the HTTP client used for JSON-RPC
- * communication with A2A agents. If no HTTP client is specified, the default JDK-based
- * implementation is used.
- *
- * Basic usage:
- *
{@code
- * // Use default HTTP client
- * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
- * .build();
- *
- * Client client = Client.builder(agentCard)
- * .withTransport(JSONRPCTransport.class, config)
- * .build();
- * }
- *
- * Custom HTTP client:
- *
{@code
- * // Custom HTTP client with timeouts
- * A2AHttpClient customClient = new CustomHttpClient()
- * .withConnectTimeout(Duration.ofSeconds(10))
- * .withReadTimeout(Duration.ofSeconds(30));
- *
- * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
- * .httpClient(customClient)
- * .build();
- * }
- *
- * With interceptors:
- *
{@code
- * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
- * .httpClient(customClient)
- * .addInterceptor(new LoggingInterceptor())
- * .addInterceptor(new AuthInterceptor("Bearer token"))
- * .build();
- * }
- *
- * @see JSONRPCTransportConfigBuilder
- * @see JSONRPCTransport
- * @see A2AHttpClient
- * @see io.a2a.client.http.JdkA2AHttpClient
- */
-public class JSONRPCTransportConfig extends ClientTransportConfig {
-
- private final @Nullable A2AHttpClient httpClient;
-
- /**
- * Create a JSON-RPC transport configuration with the default HTTP client.
- *
- * The default JDK-based HTTP client will be used. Consider using
- * {@link JSONRPCTransportConfigBuilder} instead for a more fluent API.
- */
- public JSONRPCTransportConfig() {
- this.httpClient = null;
- }
-
- /**
- * Create a JSON-RPC transport configuration with a custom HTTP client.
- *
- * Consider using {@link JSONRPCTransportConfigBuilder} instead for a more fluent API.
- *
- * @param httpClient the HTTP client to use for JSON-RPC requests
- */
- public JSONRPCTransportConfig(A2AHttpClient httpClient) {
- this.httpClient = httpClient;
- }
-
- /**
- * Get the configured HTTP client.
- *
- * @return the HTTP client, or {@code null} if using the default
- */
- public @Nullable A2AHttpClient getHttpClient() {
- return httpClient;
- }
-}
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
deleted file mode 100644
index 7f73b4c2d..000000000
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package io.a2a.client.transport.jsonrpc;
-
-import io.a2a.client.http.A2AHttpClient;
-import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Builder for creating {@link JSONRPCTransportConfig} instances.
- *
- * This builder provides a fluent API for configuring the JSON-RPC transport protocol.
- * All configuration options are optional - if not specified, sensible defaults are used:
- *
- * - HTTP client: Auto-selected via {@link A2AHttpClientFactory} (prefers Vert.x, falls back to JDK)
- * - Interceptors: None
- *
- *
- * Basic usage:
- *
{@code
- * // Minimal configuration (uses all defaults)
- * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
- * .build();
- *
- * Client client = Client.builder(agentCard)
- * .withTransport(JSONRPCTransport.class, config)
- * .build();
- * }
- *
- * Custom HTTP client:
- *
{@code
- * // Configure custom HTTP client for connection pooling, timeouts, etc.
- * A2AHttpClient httpClient = new ApacheHttpClient()
- * .withConnectionTimeout(Duration.ofSeconds(10))
- * .withMaxConnections(50);
- *
- * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
- * .httpClient(httpClient)
- * .build();
- * }
- *
- * With interceptors:
- *
{@code
- * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
- * .addInterceptor(new LoggingInterceptor())
- * .addInterceptor(new MetricsInterceptor())
- * .addInterceptor(new RetryInterceptor(3))
- * .build();
- * }
- *
- * Direct usage in ClientBuilder:
- *
{@code
- * // Can pass builder directly to withTransport()
- * Client client = Client.builder(agentCard)
- * .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()
- * .httpClient(customClient)
- * .addInterceptor(loggingInterceptor))
- * .build();
- * }
- *
- * @see JSONRPCTransportConfig
- * @see JSONRPCTransport
- * @see A2AHttpClient
- * @see io.a2a.client.http.JdkA2AHttpClient
- */
-public class JSONRPCTransportConfigBuilder extends ClientTransportConfigBuilder {
-
- private @Nullable A2AHttpClient httpClient;
-
- /**
- * Set the HTTP client to use for JSON-RPC requests.
- *
- * Custom HTTP clients can provide:
- *
- * - Connection pooling and reuse
- * - Custom timeout configuration
- * - SSL/TLS configuration
- * - Proxy support
- * - Custom header handling
- *
- *
- * If not specified, a client is auto-selected via {@link A2AHttpClientFactory}.
- *
- * Example:
- *
{@code
- * A2AHttpClient client = new CustomHttpClient()
- * .withConnectTimeout(Duration.ofSeconds(5))
- * .withReadTimeout(Duration.ofSeconds(30))
- * .withConnectionPool(10, 50);
- *
- * builder.httpClient(client);
- * }
- *
- * @param httpClient the HTTP client to use
- * @return this builder for method chaining
- */
- public JSONRPCTransportConfigBuilder httpClient(A2AHttpClient httpClient) {
- this.httpClient = httpClient;
- return this;
- }
-
- /**
- * Build the JSON-RPC transport configuration.
- *
- * If no HTTP client was configured, one is auto-selected via {@link A2AHttpClientFactory}.
- * Any configured interceptors are transferred to the configuration.
- *
- * @return the configured JSON-RPC transport configuration
- */
- @Override
- public JSONRPCTransportConfig build() {
- // No HTTP client provided, use factory to get best available implementation
- if (httpClient == null) {
- httpClient = A2AHttpClientFactory.create();
- }
-
- JSONRPCTransportConfig config = new JSONRPCTransportConfig(httpClient);
- config.setInterceptors(this.interceptors);
- return config;
- }
-}
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportProvider.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportProvider.java
deleted file mode 100644
index 2dc1a9733..000000000
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportProvider.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package io.a2a.client.transport.jsonrpc;
-
-import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.transport.spi.ClientTransportProvider;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentInterface;
-import io.a2a.spec.TransportProtocol;
-import org.jspecify.annotations.Nullable;
-
-public class JSONRPCTransportProvider implements ClientTransportProvider {
-
- @Override
- public JSONRPCTransport create(@Nullable JSONRPCTransportConfig clientTransportConfig, AgentCard agentCard, AgentInterface agentInterface) throws A2AClientException {
- JSONRPCTransportConfig currentClientTransportConfig = clientTransportConfig;
- if (currentClientTransportConfig == null) {
- currentClientTransportConfig = new JSONRPCTransportConfig(A2AHttpClientFactory.create());
- }
- return new JSONRPCTransport(currentClientTransportConfig.getHttpClient(), agentCard, agentInterface, currentClientTransportConfig.getInterceptors());
- }
-
- @Override
- public String getTransportProtocol() {
- return TransportProtocol.JSONRPC.asString();
- }
-
- @Override
- public Class getTransportProtocolClass() {
- return JSONRPCTransport.class;
- }
-}
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/package-info.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/package-info.java
deleted file mode 100644
index 3fc8e35ec..000000000
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-@NullMarked
-package io.a2a.client.transport.jsonrpc;
-
-import org.jspecify.annotations.NullMarked;
-
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java
deleted file mode 100644
index 2c61d082e..000000000
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package io.a2a.client.transport.jsonrpc.sse;
-
-import java.util.concurrent.Future;
-import java.util.function.Consumer;
-import java.util.logging.Logger;
-
-import io.a2a.client.transport.spi.sse.AbstractSSEEventListener;
-import io.a2a.grpc.StreamResponse;
-import io.a2a.grpc.utils.JSONRPCUtils;
-import io.a2a.grpc.utils.ProtoUtils;
-import io.a2a.jsonrpc.common.json.JsonProcessingException;
-import io.a2a.spec.A2AError;
-import io.a2a.spec.StreamingEventKind;
-import org.jspecify.annotations.Nullable;
-
-/**
- * JSON-RPC transport implementation of SSE event listener.
- * Handles parsing of JSON-RPC formatted messages from SSE streams.
- */
-public class SSEEventListener extends AbstractSSEEventListener {
-
- private static final Logger log = Logger.getLogger(SSEEventListener.class.getName());
- private volatile boolean completed = false;
-
- public SSEEventListener(Consumer eventHandler,
- @Nullable Consumer errorHandler) {
- super(eventHandler, errorHandler);
- }
-
- @Override
- public void onMessage(String message, @Nullable Future completableFuture) {
- parseAndHandleMessage(message, completableFuture);
- }
-
- 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 (getErrorHandler() != null) {
- log.fine("Calling errorHandler.accept(null) to signal successful completion");
- getErrorHandler().accept(null);
- } else {
- log.warning("errorHandler is null, cannot signal completion");
- }
- }
-
- /**
- * Parses a JSON-RPC message and delegates to the base class for event handling.
- *
- * @param message The raw JSON-RPC message string
- * @param future Optional future for controlling the SSE connection
- */
- private void parseAndHandleMessage(String message, @Nullable Future future) {
- try {
- StreamResponse response = JSONRPCUtils.parseResponseEvent(message);
- StreamingEventKind event = ProtoUtils.FromProto.streamingEventKind(response);
-
- // Delegate to base class for common event handling and auto-close logic
- handleEvent(event, future);
- } catch (A2AError error) {
- if (getErrorHandler() != null) {
- getErrorHandler().accept(error);
- }
- } catch (JsonProcessingException e) {
- throw new RuntimeException(e);
- }
- }
-
-}
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/package-info.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/package-info.java
deleted file mode 100644
index 956b8992a..000000000
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-@NullMarked
-package io.a2a.client.transport.jsonrpc.sse;
-
-import org.jspecify.annotations.NullMarked;
-
diff --git a/client/transport/jsonrpc/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider b/client/transport/jsonrpc/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider
deleted file mode 100644
index b2904cb45..000000000
--- a/client/transport/jsonrpc/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider
+++ /dev/null
@@ -1 +0,0 @@
-io.a2a.client.transport.jsonrpc.JSONRPCTransportProvider
\ No newline at end of file
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java
deleted file mode 100644
index 1f28d9201..000000000
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package io.a2a.client.transport.jsonrpc;
-
-import static io.a2a.client.transport.jsonrpc.JsonStreamingMessages.SEND_MESSAGE_STREAMING_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonStreamingMessages.SEND_MESSAGE_STREAMING_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonStreamingMessages.TASK_SUBSCRIPTION_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonStreamingMessages.TASK_SUBSCRIPTION_REQUEST_TEST_RESPONSE;
-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 io.a2a.spec.Artifact;
-import io.a2a.spec.Message;
-import io.a2a.spec.MessageSendConfiguration;
-import io.a2a.spec.MessageSendParams;
-import io.a2a.spec.Part;
-import io.a2a.spec.StreamingEventKind;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskState;
-import io.a2a.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 = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("test message")))
- .contextId("context-test")
- .messageId("message-test")
- .build();
-
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(false)
- .build();
-
- MessageSendParams params = MessageSendParams.builder()
- .message(message)
- .configuration(configuration)
- .build();
-
- assertNotNull(params);
- assertEquals(message, params.message());
- assertEquals(configuration, params.configuration());
- assertEquals(Message.Role.ROLE_USER, params.message().role());
- assertEquals("test message", ((TextPart) params.message().parts().get(0)).text());
- }
-
- @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 = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("tell me some jokes")))
- .contextId("context-1234")
- .messageId("message-1234")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(false)
- .build();
- MessageSendParams params = 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 testA2AClientSubscribeToTask() throws Exception {
- this.server.when(request()
- .withMethod("POST")
- .withPath("/")
- .withBody(JsonBody.json(TASK_SUBSCRIPTION_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
-
- )
- .respond(response()
- .withStatusCode(200)
- .withHeader("Content-Type", "text/event-stream")
- .withBody(TASK_SUBSCRIPTION_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.subscribeToTask(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.id());
- assertEquals("context-1234", task.contextId());
- assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state());
- List artifacts = task.artifacts();
- assertEquals(1, artifacts.size());
- Artifact artifact = artifacts.get(0);
- assertEquals("artifact-1", artifact.artifactId());
- assertEquals("joke", artifact.name());
- Part> part = artifact.parts().get(0);
- assertTrue(part instanceof TextPart);
- assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text());
- }
-}
\ No newline at end of file
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java
deleted file mode 100644
index 0bd42a978..000000000
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java
+++ /dev/null
@@ -1,703 +0,0 @@
-package io.a2a.client.transport.jsonrpc;
-
-import static io.a2a.client.transport.jsonrpc.JsonMessages.CANCEL_TASK_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.CANCEL_TASK_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.GET_TASK_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.GET_TASK_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_ERROR_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_ERROR_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST;
-import static io.a2a.client.transport.jsonrpc.JsonMessages.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
-import static io.a2a.spec.AgentInterface.CURRENT_PROTOCOL_VERSION;
-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 io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.ExtensionSupportRequiredError;
-import io.a2a.spec.VersionNotSupportedError;
-import io.a2a.spec.AgentSkill;
-import io.a2a.spec.Artifact;
-import io.a2a.spec.AuthenticationInfo;
-import io.a2a.spec.DataPart;
-import io.a2a.spec.EventKind;
-import io.a2a.spec.FileContent;
-import io.a2a.spec.FilePart;
-import io.a2a.spec.FileWithBytes;
-import io.a2a.spec.FileWithUri;
-import io.a2a.spec.GetTaskPushNotificationConfigParams;
-import io.a2a.spec.Message;
-import io.a2a.spec.MessageSendConfiguration;
-import io.a2a.spec.MessageSendParams;
-import io.a2a.spec.OpenIdConnectSecurityScheme;
-import io.a2a.spec.Part;
-import io.a2a.spec.PushNotificationConfig;
-import io.a2a.spec.SecurityRequirement;
-import io.a2a.spec.SecurityScheme;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskPushNotificationConfig;
-import io.a2a.spec.TaskQueryParams;
-import io.a2a.spec.TaskState;
-import io.a2a.spec.TextPart;
-import io.a2a.util.Utils;
-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 = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("tell me a joke")))
- .contextId("context-1234")
- .messageId("message-1234")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(true)
- .build();
- MessageSendParams params = 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.id());
- assertNotNull(task.contextId());
- assertEquals(TaskState.TASK_STATE_COMPLETED,task.status().state());
- assertEquals(1, task.artifacts().size());
- Artifact artifact = task.artifacts().get(0);
- assertEquals("artifact-1", artifact.artifactId());
- assertEquals("joke", artifact.name());
- assertEquals(1, artifact.parts().size());
- Part> part = artifact.parts().get(0);
- assertTrue(part instanceof TextPart);
- assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text());
- assertTrue(task.metadata().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 = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("tell me a joke")))
- .contextId("context-1234")
- .messageId("message-1234")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(true)
- .build();
- MessageSendParams params = MessageSendParams.builder()
- .message(message)
- .configuration(configuration)
- .build();
-
- EventKind result = client.sendMessage(params, null);
- assertInstanceOf(Message.class, result);
- Message agentMessage = (Message) result;
- assertEquals(Message.Role.ROLE_AGENT, agentMessage.role());
- Part> part = agentMessage.parts().get(0);
- assertTrue(part instanceof TextPart);
- assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text());
- assertEquals("msg-456", agentMessage.messageId());
- }
-
-
- @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 = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("tell me a joke")))
- .contextId("context-1234")
- .messageId("message-1234")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(true)
- .build();
- MessageSendParams params = 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\""),e.getMessage());
- }
- }
-
- @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.id());
- assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.contextId());
- assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state());
- assertEquals(1, task.artifacts().size());
- Artifact artifact = task.artifacts().get(0);
- assertEquals(1, artifact.parts().size());
- assertEquals("artifact-1", artifact.artifactId());
- Part> part = artifact.parts().get(0);
- assertTrue(part instanceof TextPart);
- assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text());
- assertTrue(task.metadata().isEmpty());
- List history = task.history();
- assertNotNull(history);
- assertEquals(1, history.size());
- Message message = history.get(0);
- assertEquals(Message.Role.ROLE_USER, message.role());
- List> parts = message.parts();
- assertNotNull(parts);
- assertEquals(3, parts.size());
- part = parts.get(0);
- assertTrue(part instanceof TextPart);
- assertEquals("tell me a joke", ((TextPart)part).text());
- part = parts.get(1);
- assertTrue(part instanceof FilePart);
- FileContent filePart = ((FilePart) part).file();
- assertEquals("file:///path/to/file.txt", ((FileWithUri) filePart).uri());
- assertEquals("text/plain", filePart.mimeType());
- part = parts.get(2);
- assertTrue(part instanceof FilePart);
- filePart = ((FilePart) part).file();
- assertEquals("aGVsbG8=", ((FileWithBytes) filePart).bytes());
- assertEquals("hello.txt", filePart.name());
- assertTrue(task.metadata().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"), null);
- assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id());
- assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.contextId());
- assertEquals(TaskState.TASK_STATE_CANCELED, task.status().state());
- assertTrue(task.metadata().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", "c295ea44-7543-4f78-b524-7a38915ad6e4"), null);
- PushNotificationConfig pushNotificationConfig = taskPushNotificationConfig.config();
- assertNotNull(pushNotificationConfig);
- assertEquals("https://example.com/callback", pushNotificationConfig.url());
- AuthenticationInfo authenticationInfo = pushNotificationConfig.authentication();
- assertEquals("jwt", authenticationInfo.scheme());
- }
-
- @Test
- public void testA2AClientCreateTaskPushNotificationConfig() 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.createTaskPushNotificationConfiguration(
- new TaskPushNotificationConfig("de38c76d-d54c-436c-8b9f-4c2703648d64",
- PushNotificationConfig.builder()
- .id("c295ea44-7543-4f78-b524-7a38915ad6e4")
- .url("https://example.com/callback")
- .authentication(new AuthenticationInfo("jwt", null))
- .build(), ""), null);
- PushNotificationConfig pushNotificationConfig = taskPushNotificationConfig.config();
- assertNotNull(pushNotificationConfig);
- assertEquals("https://example.com/callback", pushNotificationConfig.url());
- AuthenticationInfo authenticationInfo = pushNotificationConfig.authentication();
- assertEquals("jwt", authenticationInfo.scheme());
- }
-
- @Test
- public void testA2AClientGetExtendedAgentCard() throws Exception {
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/")
- .withBody(JsonBody.json(GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE)
- );
-
- JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
- AgentCard agentCard = client.getExtendedAgentCard(null);
- assertEquals("GeoSpatial Route Planner Agent Extended", agentCard.name());
- assertEquals("Extended description", agentCard.description());
- assertEquals("https://georoute-agent.example.com/a2a/v1", Utils.getFavoriteInterface(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());
- assertTrue(agentCard.capabilities().extendedAgentCard());
- Map securitySchemes = agentCard.securitySchemes();
- assertNotNull(securitySchemes);
- OpenIdConnectSecurityScheme google = (OpenIdConnectSecurityScheme) securitySchemes.get("google");
- assertEquals("https://accounts.google.com/.well-known/openid-configuration", google.openIdConnectUrl());
- List security = agentCard.securityRequirements();
- assertEquals(1, security.size());
- Map> securityMap = security.get(0).schemes();
- List scopes = securityMap.get("google");
- List expectedScopes = List.of("openid", "profile", "email");
- assertEquals(expectedScopes, scopes);
- List defaultInputModes = List.of("application/json", "text/plain");
- assertEquals(defaultInputModes, agentCard.defaultInputModes());
- List defaultOutputModes = List.of("application/json", "image/png");
- assertEquals(defaultOutputModes, agentCard.defaultOutputModes());
- List skills = agentCard.skills();
- assertEquals("route-optimizer-traffic", skills.get(0).id());
- assertEquals("Traffic-Aware Route Optimizer", skills.get(0).name());
- assertEquals("Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).", skills.get(0).description());
- List tags = List.of("maps", "routing", "navigation", "directions", "traffic");
- assertEquals(tags, skills.get(0).tags());
- List examples = List.of("Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.",
- "{\"origin\": {\"lat\": 37.422, \"lng\": -122.084}, \"destination\": {\"lat\": 37.7749, \"lng\": -122.4194}, \"preferences\": [\"avoid_ferries\"]}");
- assertEquals(examples, skills.get(0).examples());
- assertEquals(defaultInputModes, skills.get(0).inputModes());
- List outputModes = List.of("application/json", "application/vnd.geo+json", "text/html");
- assertEquals(outputModes, skills.get(0).outputModes());
- assertEquals("custom-map-generator", skills.get(1).id());
- assertEquals("Personalized Map Generator", skills.get(1).name());
- assertEquals("Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.", skills.get(1).description());
- tags = List.of("maps", "customization", "visualization", "cartography");
- assertEquals(tags, skills.get(1).tags());
- examples = List.of("Generate a map of my upcoming road trip with all planned stops highlighted.",
- "Show me a map visualizing all coffee shops within a 1-mile radius of my current location.");
- assertEquals(examples, skills.get(1).examples());
- List inputModes = List.of("application/json");
- assertEquals(inputModes, skills.get(1).inputModes());
- outputModes = List.of("image/png", "image/jpeg", "application/json", "text/html");
- assertEquals(outputModes, skills.get(1).outputModes());
- assertEquals("skill-extended", skills.get(2).id());
- assertEquals("Extended Skill", skills.get(2).name());
- assertEquals("This is an extended skill.", skills.get(2).description());
- assertEquals(List.of("extended"), skills.get(2).tags());
- assertEquals("https://georoute-agent.example.com/icon.png", agentCard.iconUrl());
- assertEquals(CURRENT_PROTOCOL_VERSION, agentCard.supportedInterfaces().get(0).protocolVersion());
- }
-
- @Test
- public void testA2AClientSendMessageWithFilePart() throws Exception {
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/")
- .withBody(JsonBody.json(SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
-
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE)
- );
-
- JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
- Message message = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(List.of(
- new TextPart("analyze this image"),
- new FilePart(new FileWithUri("image/jpeg", null, "file:///path/to/image.jpg"))
- ))
- .contextId("context-1234")
- .messageId("message-1234-with-file")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(true)
- .build();
- MessageSendParams params = 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.id());
- assertNotNull(task.contextId());
- assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state());
- assertEquals(1, task.artifacts().size());
- Artifact artifact = task.artifacts().get(0);
- assertEquals("artifact-1", artifact.artifactId());
- assertEquals("image-analysis", artifact.name());
- assertEquals(1, artifact.parts().size());
- Part> part = artifact.parts().get(0);
- assertTrue(part instanceof TextPart);
- assertEquals("This is an image of a cat sitting on a windowsill.", ((TextPart) part).text());
- assertFalse(task.metadata().isEmpty());
- assertEquals(1, task.metadata().size());
- assertEquals("metadata-test", task.metadata().get("test"));
- }
-
- @Test
- public void testA2AClientSendMessageWithDataPart() throws Exception {
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/")
- .withBody(JsonBody.json(SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
-
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE)
- );
-
- JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
-
- Map data = new HashMap<>();
- data.put("temperature", 25.5);
- data.put("humidity", 60.2);
- data.put("location", "San Francisco");
- data.put("timestamp", "2024-01-15T10:30:00Z");
-
- Message message = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(List.of(
- new TextPart("process this data"),
- new DataPart(data)
- ))
- .contextId("context-1234")
- .messageId("message-1234-with-data")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(true)
- .build();
- MessageSendParams params = 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.id());
- assertNotNull(task.contextId());
- assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state());
- assertEquals(1, task.artifacts().size());
- Artifact artifact = task.artifacts().get(0);
- assertEquals("artifact-1", artifact.artifactId());
- assertEquals("data-analysis", artifact.name());
- assertEquals(1, artifact.parts().size());
- Part> part = artifact.parts().get(0);
- assertTrue(part instanceof TextPart);
- assertEquals("Processed weather data: Temperature is 25.5°C, humidity is 60.2% in San Francisco.", ((TextPart) part).text());
- assertTrue(task.metadata().isEmpty());
- }
-
- @Test
- public void testA2AClientSendMessageWithMixedParts() throws Exception {
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/")
- .withBody(JsonBody.json(SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
-
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE)
- );
-
- JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
-
- Map data = new HashMap<>();
- data.put("chartType", "bar");
- data.put("dataPoints", List.of(10, 20, 30, 40));
- data.put("labels", List.of("Q1", "Q2", "Q3", "Q4"));
-
- Message message = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(List.of(
- new TextPart("analyze this data and image"),
- new FilePart(new FileWithBytes("image/png", "chart.png", "aGVsbG8=")),
- new DataPart(data)
- ))
- .contextId("context-1234")
- .messageId("message-1234-with-mixed")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(true)
- .build();
- MessageSendParams params = 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.id());
- assertNotNull(task.contextId());
- assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state());
- assertEquals(1, task.artifacts().size());
- Artifact artifact = task.artifacts().get(0);
- assertEquals("artifact-1", artifact.artifactId());
- assertEquals("mixed-analysis", artifact.name());
- assertEquals(1, artifact.parts().size());
- Part> part = artifact.parts().get(0);
- assertTrue(part instanceof TextPart);
- assertEquals("Analyzed chart image and data: Bar chart showing quarterly data with values [10, 20, 30, 40].", ((TextPart) part).text());
- assertTrue(task.metadata().isEmpty());
- }
-
- /**
- * Test that ExtensionSupportRequiredError is properly unmarshalled from JSON-RPC error response.
- */
- @Test
- public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception {
- // Mock server returns JSON-RPC error with code -32008 (EXTENSION_SUPPORT_REQUIRED_ERROR)
- String errorResponseBody = """
- {
- "jsonrpc": "2.0",
- "id": 1,
- "error": {
- "code": -32008,
- "message": "Extension required: https://example.com/test-extension"
- }
- }
- """;
-
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/")
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(errorResponseBody)
- );
-
- JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
- Message message = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("test message")))
- .contextId("context-test")
- .messageId("message-test")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(true)
- .build();
- MessageSendParams params = MessageSendParams.builder()
- .message(message)
- .configuration(configuration)
- .build();
-
- // Should throw A2AClientException with ExtensionSupportRequiredError as cause
- try {
- client.sendMessage(params, null);
- fail("Expected A2AClientException to be thrown");
- } catch (A2AClientException e) {
- // Verify the cause is ExtensionSupportRequiredError
- assertInstanceOf(ExtensionSupportRequiredError.class, e.getCause());
- ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) e.getCause();
- assertTrue(extensionError.getMessage().contains("https://example.com/test-extension"));
- }
- }
-
- /**
- * Test that VersionNotSupportedError is properly unmarshalled from JSON-RPC error response.
- */
- @Test
- public void testVersionNotSupportedErrorUnmarshalling() throws Exception {
- // Mock server returns JSON-RPC error with code -32009 (VERSION_NOT_SUPPORTED_ERROR)
- String errorResponseBody = """
- {
- "jsonrpc": "2.0",
- "id": 1,
- "error": {
- "code": -32009,
- "message": "Protocol version 2.0 is not supported. This agent supports version 1.0"
- }
- }
- """;
-
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/")
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(errorResponseBody)
- );
-
- JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
- Message message = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("test message")))
- .contextId("context-test")
- .messageId("message-test")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(true)
- .build();
- MessageSendParams params = MessageSendParams.builder()
- .message(message)
- .configuration(configuration)
- .build();
-
- // Should throw A2AClientException with VersionNotSupportedError as cause
- try {
- client.sendMessage(params, null);
- fail("Expected A2AClientException to be thrown");
- } catch (A2AClientException e) {
- // Verify the cause is VersionNotSupportedError
- assertInstanceOf(VersionNotSupportedError.class, e.getCause());
- VersionNotSupportedError versionError = (VersionNotSupportedError) e.getCause();
- assertTrue(versionError.getMessage().contains("2.0"));
- assertTrue(versionError.getMessage().contains("1.0"));
- }
- }
-}
\ No newline at end of file
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java
deleted file mode 100644
index b55c0f9d1..000000000
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java
+++ /dev/null
@@ -1,724 +0,0 @@
-package io.a2a.client.transport.jsonrpc;
-
-import static io.a2a.spec.AgentInterface.CURRENT_PROTOCOL_VERSION;
-
-/**
- * Request and response messages used by the tests. These have been created following examples from
- * the A2A sample messages.
- */
-public class JsonMessages {
-
- static final String AGENT_CARD = """
- {
- "name": "GeoSpatial Route Planner Agent",
- "description": "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.",
- "supportedInterfaces" : [
- {"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "JSONRPC", "tenant": ""},
- {"url": "https://georoute-agent.example.com/a2a/grpc", "protocolBinding": "GRPC", "tenant": ""},
- {"url": "https://georoute-agent.example.com/a2a/json", "protocolBinding": "HTTP+JSON", "tenant": ""}
- ],
- "provider": {
- "organization": "Example Geo Services Inc.",
- "url": "https://www.examplegeoservices.com"
- },
- "iconUrl": "https://georoute-agent.example.com/icon.png",
- "version": "1.2.0",
- "documentationUrl": "https://docs.examplegeoservices.com/georoute-agent/api",
- "capabilities": {
- "streaming": true,
- "pushNotifications": true,
- "extendedAgentCard": false
- },
- "securitySchemes": {
- "google": {
- "openIdConnectSecurityScheme": {
- "openIdConnectUrl": "https://accounts.google.com/.well-known/openid-configuration"
- }
- }
- },
- "securityRequirements": [{ "schemes": { "google": { "list": ["openid", "profile", "email"] } } }],
- "defaultInputModes": ["application/json", "text/plain"],
- "defaultOutputModes": ["application/json", "image/png"],
- "skills": [
- {
- "id": "route-optimizer-traffic",
- "name": "Traffic-Aware Route Optimizer",
- "description": "Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).",
- "tags": ["maps", "routing", "navigation", "directions", "traffic"],
- "examples": [
- "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.",
- "{\\"origin\\": {\\"lat\\": 37.422, \\"lng\\": -122.084}, \\"destination\\": {\\"lat\\": 37.7749, \\"lng\\": -122.4194}, \\"preferences\\": [\\"avoid_ferries\\"]}"
- ],
- "inputModes": ["application/json", "text/plain"],
- "outputModes": [
- "application/json",
- "application/vnd.geo+json",
- "text/html"
- ]
- },
- {
- "id": "custom-map-generator",
- "name": "Personalized Map Generator",
- "description": "Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.",
- "tags": ["maps", "customization", "visualization", "cartography"],
- "examples": [
- "Generate a map of my upcoming road trip with all planned stops highlighted.",
- "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."
- ],
- "inputModes": ["application/json"],
- "outputModes": [
- "image/png",
- "image/jpeg",
- "application/json",
- "text/html"
- ]
- }
- ],
- "signatures": [
- {
- "protected": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJrZXktMSIsImprdSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWdlbnQvandrcy5qc29uIn0",
- "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ"
- }
- ]
- }""";
-
- static final String SEND_MESSAGE_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"SendMessage",
- "params":{
- "message":{
- "messageId":"message-1234",
- "contextId":"context-1234",
- "role":"ROLE_USER",
- "parts":[
- {
- "text":"tell me a joke"
- }
- ],
- "metadata":{
-
- }
- }
- }
- }""";
-
- static final String SEND_MESSAGE_TEST_RESPONSE = """
- {
- "jsonrpc":"2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "result":{
- "task":{
- "id":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4",
- "status":{
- "state":"TASK_STATE_COMPLETED"
- },
- "artifacts":[
- {
- "artifactId":"artifact-1",
- "name":"joke",
- "parts":[
- {
- "text":"Why did the chicken cross the road? To get to the other side!"
- }
- ]
- }
- ],
- "metadata":{
-
- }
- }
- }
- }""";
-
- static final String SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE = """
- {
- "jsonrpc":"2.0",
- "method":"SendMessage",
- "params":{
- "message":{
- "messageId":"message-1234",
- "contextId":"context-1234",
- "role":"ROLE_USER",
- "parts":[
- {
- "text":"tell me a joke"
- }
- ],
- "metadata":{
- }
- },
- "configuration":{
- "acceptedOutputModes":[
- "text"
- ],
- "blocking":true
- },
- "metadata":{
-
- }
- }
- }""";
-
- static final String SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE = """
- {
- "jsonrpc":"2.0",
- "id":1,
- "result":{
- "message": {
- "messageId":"msg-456",
- "contextId":"context-1234",
- "role":"ROLE_AGENT",
- "parts":[
- {
- "text":"Why did the chicken cross the road? To get to the other side!"
- }
- ],
- "metadata":{
- }
- }
- }
- }""";
-
- static final String SEND_MESSAGE_WITH_ERROR_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"SendMessage",
- "params":{
- "message":{
- "messageId":"message-1234",
- "contextId":"context-1234",
- "role":"ROLE_USER",
- "parts":[
- {
- "text":"tell me a joke"
- }
- ],
- "metadata":{
-
- }
- },
- "configuration":{
- "acceptedOutputModes":[
- "text"
- ],
- "blocking":true
- },
- "metadata":{
-
- }
- }
- }""";
-
- static final String SEND_MESSAGE_ERROR_TEST_RESPONSE = """
- {
- "jsonrpc": "2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "error": {
- "code": -32702,
- "message": "Invalid parameters",
- "data": "Hello world"
- }
- }""";
-
- static final String GET_TASK_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"GetTask",
- "params":{
- "id":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "historyLength":10
- }
- }
- """;
-
- static final String GET_TASK_TEST_RESPONSE = """
- {
- "jsonrpc":"2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "result":{
- "id":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4",
- "status":{
- "state":"TASK_STATE_COMPLETED"
- },
- "artifacts":[
- {
- "artifactId":"artifact-1",
- "parts":[
- {
- "text":"Why did the chicken cross the road? To get to the other side!"
- }
- ]
- }
- ],
- "history":[
- {
- "role":"ROLE_USER",
- "parts":[
- {
- "text":"tell me a joke"
- },
- {
- "url":"file:///path/to/file.txt",
- "mediaType":"text/plain"
- },
- {
- "raw":"aGVsbG8=",
- "filename":"hello.txt"
- }
- ],
- "messageId":"message-123"
- }
- ],
- "metadata":{
-
- }
- }
- }
- """;
-
- static final String CANCEL_TASK_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"CancelTask",
- "params":{
- "id":"de38c76d-d54c-436c-8b9f-4c2703648d64"
- }
- }
- """;
-
- static final String CANCEL_TASK_TEST_RESPONSE = """
- {
- "jsonrpc":"2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "result":{
- "id":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4",
- "status":{
- "state":"TASK_STATE_CANCELED"
- },
- "metadata":{
-
- }
- }
- }
- """;
-
- static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"GetTaskPushNotificationConfig",
- "params":{
- "taskId":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "id":"c295ea44-7543-4f78-b524-7a38915ad6e4"
- }
- }""";
-
- static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """
- {
- "jsonrpc": "2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "result": {
- "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64",
- "id": "c295ea44-7543-4f78-b524-7a38915ad6e4",
- "pushNotificationConfig": {
- "url": "https://example.com/callback",
- "authentication": {
- "scheme": "jwt"
- }
- }
- }
- }
- """;
-
- static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"CreateTaskPushNotificationConfig",
- "params":{
- "taskId":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "configId":"c295ea44-7543-4f78-b524-7a38915ad6e4",
- "config":{
- "url":"https://example.com/callback",
- "authentication":{
- "scheme":"jwt"
- }
- }
- }
- }""";
-
- static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """
- {
- "jsonrpc": "2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "result": {
- "taskId":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "id":"c295ea44-7543-4f78-b524-7a38915ad6e4",
- "pushNotificationConfig": {
- "url": "https://example.com/callback",
- "authentication": {
- "scheme": "jwt"
- }
- }
- }
- }
- """;
-
- static final String SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"SendMessage",
- "params":{
- "message":{
- "messageId":"message-1234-with-file",
- "contextId":"context-1234",
- "role":"ROLE_USER",
- "parts":[
- {
- "text":"analyze this image"
- },
- {
- "url":"file:///path/to/image.jpg",
- "mediaType":"image/jpeg"
- }
- ],
- "metadata":{
-
- }
- },
- "configuration":{
- "acceptedOutputModes":[
- "text"
- ],
- "blocking":true
- },
- "metadata":{
-
- }
- }
- }""";
-
- static final String SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE = """
- {
- "jsonrpc":"2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "result":{
- "task":{
- "id":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4",
- "status":{
- "state":"TASK_STATE_COMPLETED"
- },
- "artifacts":[
- {
- "artifactId":"artifact-1",
- "name":"image-analysis",
- "parts":[
- {
- "text":"This is an image of a cat sitting on a windowsill."
- }
- ]
- }
- ],
- "metadata":{
- "test":"metadata-test"
- }
- }
- }
- }""";
-
- static final String SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"SendMessage",
- "params":{
- "message":{
- "messageId":"message-1234-with-data",
- "contextId":"context-1234",
- "role":"ROLE_USER",
- "parts":[
- {
- "text":"process this data"
- },
- {
- "data":{
- "temperature":25.5,
- "humidity":60.2,
- "location":"San Francisco",
- "timestamp":"2024-01-15T10:30:00Z"
- }
- }
- ],
- "metadata":{
-
- }
- },
- "configuration":{
- "acceptedOutputModes":[
- "text"
- ],
- "blocking":true
- },
- "metadata":{
-
- }
- }
- }""";
-
- static final String SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE = """
- {
- "jsonrpc":"2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "result":{
- "task":{
- "id":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4",
- "status":{
- "state":"TASK_STATE_COMPLETED"
- },
- "artifacts":[
- {
- "artifactId":"artifact-1",
- "name":"data-analysis",
- "parts":[
- {
- "text":"Processed weather data: Temperature is 25.5°C, humidity is 60.2% in San Francisco."
- }
- ]
- }
- ],
- "metadata":{
-
- }
- }
- }
- }""";
-
- static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"SendMessage",
- "params":{
- "message":{
- "messageId":"message-1234-with-mixed",
- "contextId":"context-1234",
- "role":"ROLE_USER",
- "parts":[
- {
- "text":"analyze this data and image"
- },
- {
- "raw":"aGVsbG8=",
- "mediaType":"image/png",
- "filename":"chart.png"
- },
- {
- "data":{
- "chartType":"bar",
- "dataPoints":[10.0, 20.0, 30.0, 40.0],
- "labels":["Q1", "Q2", "Q3", "Q4"]
- }
- }
- ],
- "metadata":{
-
- }
- },
- "configuration":{
- "acceptedOutputModes":[
- "text"
- ],
- "blocking":true
- },
- "metadata":{
-
- }
- }
- }""";
-
- static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE = """
- {
- "jsonrpc":"2.0",
- "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
- "result":{
- "task":{
- "id":"de38c76d-d54c-436c-8b9f-4c2703648d64",
- "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4",
- "status":{
- "state":"TASK_STATE_COMPLETED"
- },
- "artifacts":[
- {
- "artifactId":"artifact-1",
- "name":"mixed-analysis",
- "parts":[
- {
- "text":"Analyzed chart image and data: Bar chart showing quarterly data with values [10, 20, 30, 40]."
- }
- ]
- }
- ],
- "metadata":{
-
- }
- }
- }
- }""";
-
- static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST = """
- {
- "jsonrpc": "2.0",
- "method": "GetExtendedAgentCard"
- }
- """;
-
- static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE = """
- {
- "jsonrpc": "2.0",
- "id": "1",
- "result": {
- "name": "GeoSpatial Route Planner Agent Extended",
- "description": "Extended description",
- "supportedInterfaces": [
- {"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "JSONRPC", "tenant": ""}
- ],
- "provider": {
- "organization": "Example Geo Services Inc.",
- "url": "https://www.examplegeoservices.com"
- },
- "iconUrl": "https://georoute-agent.example.com/icon.png",
- "version": "1.2.0",
- "documentationUrl": "https://docs.examplegeoservices.com/georoute-agent/api",
- "capabilities": {
- "streaming": true,
- "pushNotifications": true,
- "extendedAgentCard": true
- },
- "securitySchemes": {
- "google": {
- "openIdConnectSecurityScheme": {
- "openIdConnectUrl": "https://accounts.google.com/.well-known/openid-configuration"
- }
- }
- },
- "securityRequirements": [{ "schemes": { "google": { "list": ["openid", "profile", "email"] } } }],
- "defaultInputModes": ["application/json", "text/plain"],
- "defaultOutputModes": ["application/json", "image/png"],
- "skills": [
- {
- "id": "route-optimizer-traffic",
- "name": "Traffic-Aware Route Optimizer",
- "description": "Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).",
- "tags": ["maps", "routing", "navigation", "directions", "traffic"],
- "examples": [
- "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.",
- "{\\"origin\\": {\\"lat\\": 37.422, \\"lng\\": -122.084}, \\"destination\\": {\\"lat\\": 37.7749, \\"lng\\": -122.4194}, \\"preferences\\": [\\"avoid_ferries\\"]}"
- ],
- "inputModes": ["application/json", "text/plain"],
- "outputModes": [
- "application/json",
- "application/vnd.geo+json",
- "text/html"
- ]
- },
- {
- "id": "custom-map-generator",
- "name": "Personalized Map Generator",
- "description": "Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.",
- "tags": ["maps", "customization", "visualization", "cartography"],
- "examples": [
- "Generate a map of my upcoming road trip with all planned stops highlighted.",
- "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."
- ],
- "inputModes": ["application/json"],
- "outputModes": [
- "image/png",
- "image/jpeg",
- "application/json",
- "text/html"
- ]
- },
- {
- "id": "skill-extended",
- "name": "Extended Skill",
- "description": "This is an extended skill.",
- "tags": ["extended"]
- }
- ],
- "signatures": [
- {
- "protected": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJrZXktMSIsImprdUI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWdlbnQvandrcy5qc29uIn0",
- "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ"
- }
- ]
- }
- }""";
-
- static final String AGENT_CARD_SUPPORTS_EXTENDED = """
- {
- "name": "GeoSpatial Route Planner Agent",
- "description": "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.",
- "supportedInterfaces": [
- {"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "JSONRPC"}
- ],
- "provider": {
- "organization": "Example Geo Services Inc.",
- "url": "https://www.examplegeoservices.com"
- },
- "iconUrl": "https://georoute-agent.example.com/icon.png",
- "version": "1.2.0",
- "documentationUrl": "https://docs.examplegeoservices.com/georoute-agent/api",
- "capabilities": {
- "streaming": true,
- "pushNotifications": true,
- "extendedAgentCard": true
- },
- "securitySchemes": {
- "google": {
- "openIdConnectSecurityScheme": {
- "openIdConnectUrl": "https://accounts.google.com/.well-known/openid-configuration"
- }
- }
- },
- "security": [{ "schemes": { "google": { "list": ["openid", "profile", "email"] } } }],
- "defaultInputModes": ["application/json", "text/plain"],
- "defaultOutputModes": ["application/json", "image/png"],
- "skills": [
- {
- "id": "route-optimizer-traffic",
- "name": "Traffic-Aware Route Optimizer",
- "description": "Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).",
- "tags": ["maps", "routing", "navigation", "directions", "traffic"],
- "examples": [
- "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.",
- "{\\"origin\\": {\\"lat\\": 37.422, \\"lng\\": -122.084}, \\"destination\\": {\\"lat\\": 37.7749, \\"lng\\": -122.4194}, \\"preferences\\": [\\"avoid_ferries\\"]}"
- ],
- "inputModes": ["application/json", "text/plain"],
- "outputModes": [
- "application/json",
- "application/vnd.geo+json",
- "text/html"
- ]
- },
- {
- "id": "custom-map-generator",
- "name": "Personalized Map Generator",
- "description": "Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.",
- "tags": ["maps", "customization", "visualization", "cartography"],
- "examples": [
- "Generate a map of my upcoming road trip with all planned stops highlighted.",
- "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."
- ],
- "inputModes": ["application/json"],
- "outputModes": [
- "image/png",
- "image/jpeg",
- "application/json",
- "text/html"
- ]
- }
- ]
- }""";
-}
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java
deleted file mode 100644
index 39f2d7859..000000000
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package io.a2a.client.transport.jsonrpc;
-
-/**
- * Contains JSON strings for testing SSE streaming.
- */
-public class JsonStreamingMessages {
-
- public static final String STREAMING_TASK_EVENT = """
- data: {
- "jsonrpc": "2.0",
- "id": "1234",
- "result": {
- "task": {
- "id": "task-123",
- "contextId": "context-456",
- "status": {
- "state": "TASK_STATE_WORKING"
- }
- }
- }
- }
- """;
-
-
- public static final String STREAMING_MESSAGE_EVENT = """
- data: {
- "jsonrpc": "2.0",
- "id": "1234",
- "result": {
- "message": {
- "role": "ROLE_AGENT",
- "messageId": "msg-123",
- "contextId": "context-456",
- "parts": [
- {
- "text": "Hello, world!"
- }
- ]
- }
- }
- }""";
-
- public static final String STREAMING_STATUS_UPDATE_EVENT = """
- data: {
- "jsonrpc": "2.0",
- "id": "1234",
- "result": {
- "statusUpdate": {
- "taskId": "1",
- "contextId": "2",
- "status": {
- "state": "TASK_STATE_SUBMITTED"
- }
- }
- }
- }""";
-
- public static final String STREAMING_STATUS_UPDATE_EVENT_FINAL = """
- data: {
- "jsonrpc": "2.0",
- "id": "1234",
- "result": {
- "statusUpdate": {
- "taskId": "1",
- "contextId": "2",
- "status": {
- "state": "TASK_STATE_COMPLETED"
- }
- }
- }
- }""";
-
- public static final String STREAMING_ARTIFACT_UPDATE_EVENT = """
- data: {
- "jsonrpc": "2.0",
- "id": "1234",
- "result": {
- "artifactUpdate": {
- "taskId": "1",
- "contextId": "2",
- "append": false,
- "lastChunk": true,
- "artifact": {
- "artifactId": "artifact-1",
- "parts": [
- {
- "text": "Why did the chicken cross the road? To get to the other side!"
- }
- ]
- }
- }
- }
- }""";
-
- public static final String STREAMING_ERROR_EVENT = """
- data: {
- "jsonrpc": "2.0",
- "id": "1234",
- "error": {
- "code": -32602,
- "message": "Invalid parameters",
- "data": "Missing required field"
- }
- }""";
-
- public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"SendStreamingMessage",
- "params":{
- "message":{
- "messageId":"message-1234",
- "contextId":"context-1234",
- "role":"ROLE_USER",
- "parts":[
- {
- "text":"tell me some jokes"
- }
- ],
- "metadata":{
-
- }
- },
- "configuration":{
- "acceptedOutputModes":[
- "text"
- ]
- },
- "metadata":{
-
- }
- }
- }""";
-
- static final String SEND_MESSAGE_STREAMING_TEST_RESPONSE =
- """
- event: message
- data: {"jsonrpc":"2.0","id":1,"result":{"task":{"id":"2","contextId":"context-1234","status":{"state":"TASK_STATE_COMPLETED"},"artifacts":[{"artifactId":"artifact-1","name":"joke","parts":[{"text":"Why did the chicken cross the road? To get to the other side!"}]}],"metadata":{}}}}
-
- """;
-
- static final String TASK_SUBSCRIPTION_REQUEST_TEST_RESPONSE =
- """
- event: message
- data: {"jsonrpc":"2.0","id":1,"result":{"task":{"id":"2","contextId":"context-1234","status":{"state":"TASK_STATE_COMPLETED"},"artifacts":[{"artifactId":"artifact-1","name":"joke","parts":[{"text":"Why did the chicken cross the road? To get to the other side!"}]}],"metadata":{}}}}
-
- """;
-
- public static final String TASK_SUBSCRIPTION_TEST_REQUEST = """
- {
- "jsonrpc":"2.0",
- "method":"SubscribeToTask",
- "params":{
- "id":"task-1234"
- }
- }""";
-}
\ No newline at end of file
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java
deleted file mode 100644
index e9e74d12f..000000000
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java
+++ /dev/null
@@ -1,284 +0,0 @@
-package io.a2a.client.transport.jsonrpc.sse;
-
-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 java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-import io.a2a.client.transport.jsonrpc.JsonStreamingMessages;
-import io.a2a.spec.A2AError;
-import io.a2a.spec.Artifact;
-import io.a2a.spec.Message;
-import io.a2a.spec.StreamingEventKind;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskArtifactUpdateEvent;
-import io.a2a.spec.TaskState;
-import io.a2a.spec.TaskStatus;
-import io.a2a.spec.TaskStatusUpdateEvent;
-import io.a2a.spec.TextPart;
-import org.junit.jupiter.api.Test;
-
-public class SSEEventListenerTest {
-
- @Test
- public void testOnEventWithTaskResult() throws Exception {
- // Set up event handler
- AtomicReference receivedEvent = new AtomicReference<>();
- SSEEventListener listener = new SSEEventListener(
- event -> receivedEvent.set(event),
- error -> {}
- );
-
- // Parse the task event JSON
- String eventData = JsonStreamingMessages.STREAMING_TASK_EVENT.substring(
- JsonStreamingMessages.STREAMING_TASK_EVENT.indexOf("{"));
-
- // Call the onEvent method directly
- listener.onMessage(eventData, null);
-
- // Verify the event was processed correctly
- assertNotNull(receivedEvent.get());
- assertTrue(receivedEvent.get() instanceof Task);
- Task task = (Task) receivedEvent.get();
- assertEquals("task-123", task.id());
- assertEquals("context-456", task.contextId());
- assertEquals(TaskState.TASK_STATE_WORKING, task.status().state());
- }
-
- @Test
- public void testOnEventWithMessageResult() throws Exception {
- // Set up event handler
- AtomicReference receivedEvent = new AtomicReference<>();
- SSEEventListener listener = new SSEEventListener(
- event -> receivedEvent.set(event),
- error -> {}
- );
-
- // Parse the message event JSON
- String eventData = JsonStreamingMessages.STREAMING_MESSAGE_EVENT.substring(
- JsonStreamingMessages.STREAMING_MESSAGE_EVENT.indexOf("{"));
-
- // Call onEvent method
- listener.onMessage(eventData, null);
-
- // Verify the event was processed correctly
- assertNotNull(receivedEvent.get());
- assertTrue(receivedEvent.get() instanceof Message);
- Message message = (Message) receivedEvent.get();
- assertEquals(Message.Role.ROLE_AGENT, message.role());
- assertEquals("msg-123", message.messageId());
- assertEquals("context-456", message.contextId());
- assertEquals(1, message.parts().size());
- assertTrue(message.parts().get(0) instanceof TextPart);
- assertEquals("Hello, world!", ((TextPart) message.parts().get(0)).text());
- }
-
- @Test
- public void testOnEventWithTaskStatusUpdateEventEvent() throws Exception {
- // Set up event handler
- AtomicReference receivedEvent = new AtomicReference<>();
- SSEEventListener listener = new SSEEventListener(
- event -> receivedEvent.set(event),
- error -> {}
- );
-
- // Parse the message event JSON
- String eventData = JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT.substring(
- JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT.indexOf("{"));
-
- // Call onEvent method
- listener.onMessage(eventData, null);
-
- // Verify the event was processed correctly
- assertNotNull(receivedEvent.get());
- assertTrue(receivedEvent.get() instanceof TaskStatusUpdateEvent);
- TaskStatusUpdateEvent taskStatusUpdateEvent = (TaskStatusUpdateEvent) receivedEvent.get();
- assertEquals("1", taskStatusUpdateEvent.taskId());
- assertEquals("2", taskStatusUpdateEvent.contextId());
- assertFalse(taskStatusUpdateEvent.isFinal());
- assertEquals(TaskState.TASK_STATE_SUBMITTED, taskStatusUpdateEvent.status().state());
- }
-
- @Test
- public void testOnEventWithTaskArtifactUpdateEventEvent() throws Exception {
- // Set up event handler
- AtomicReference receivedEvent = new AtomicReference<>();
- SSEEventListener listener = new SSEEventListener(
- event -> receivedEvent.set(event),
- error -> {}
- );
-
- // Parse the message event JSON
- String eventData = JsonStreamingMessages.STREAMING_ARTIFACT_UPDATE_EVENT.substring(
- JsonStreamingMessages.STREAMING_ARTIFACT_UPDATE_EVENT.indexOf("{"));
-
- // Call onEvent method
- listener.onMessage(eventData, null);
-
- // Verify the event was processed correctly
- assertNotNull(receivedEvent.get());
- assertTrue(receivedEvent.get() instanceof TaskArtifactUpdateEvent);
-
- TaskArtifactUpdateEvent taskArtifactUpdateEvent = (TaskArtifactUpdateEvent) receivedEvent.get();
- assertEquals("1", taskArtifactUpdateEvent.taskId());
- assertEquals("2", taskArtifactUpdateEvent.contextId());
- assertFalse(taskArtifactUpdateEvent.append());
- assertTrue(taskArtifactUpdateEvent.lastChunk());
- Artifact artifact = taskArtifactUpdateEvent.artifact();
- assertEquals("artifact-1", artifact.artifactId());
- assertEquals(1, artifact.parts().size());
- assertTrue(artifact.parts().get(0) instanceof TextPart);
- assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) artifact.parts().get(0)).text());
- }
-
- @Test
- public void testOnEventWithError() throws Exception {
- // Set up event handler
- AtomicReference receivedError = new AtomicReference<>();
- SSEEventListener listener = new SSEEventListener(
- event -> {},
- error -> receivedError.set(error)
- );
-
- // Parse the error event JSON
- String eventData = JsonStreamingMessages.STREAMING_ERROR_EVENT.substring(
- JsonStreamingMessages.STREAMING_ERROR_EVENT.indexOf("{"));
-
- // Call onEvent method
- listener.onMessage(eventData, null);
-
- // Verify the error was processed correctly
- assertNotNull(receivedError.get());
- assertInstanceOf(A2AError.class, receivedError.get());
- A2AError jsonrpcError = (A2AError) receivedError.get();
- assertEquals(-32602, jsonrpcError.getCode());
- assertEquals("Invalid parameters", jsonrpcError.getMessage());
- assertEquals("\"Missing required field\"", jsonrpcError.getData());
- }
-
- @Test
- public void testOnFailure() {
- AtomicBoolean failureHandlerCalled = new AtomicBoolean(false);
- SSEEventListener listener = new SSEEventListener(
- event -> {},
- error -> failureHandlerCalled.set(true)
- );
-
- // Simulate a failure
- CancelCapturingFuture future = new CancelCapturingFuture();
- listener.onError(new RuntimeException("Test exception"), future);
-
- // Verify the failure handler was called
- assertTrue(failureHandlerCalled.get());
- // Verify it got cancelled
- assertTrue(future.cancelHandlerCalled);
- }
-
- @Test
- public void testFinalTaskStatusUpdateEventCancels() {
- TaskStatus completedStatus = new TaskStatus(TaskState.TASK_STATE_COMPLETED);
- // Use constructor since Builder doesn't have isFinal method
- TaskStatusUpdateEvent tsue = new TaskStatusUpdateEvent(
- "1234",
- completedStatus,
- "xyz",
- completedStatus.state().isFinal(), // Derive from state
- null
- );
-
- // Set up event handler
- AtomicReference receivedEvent = new AtomicReference<>();
- SSEEventListener listener = new SSEEventListener(
- event -> receivedEvent.set(event),
- error -> {}
- );
-
- // Parse the message event JSON
- String eventData = JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring(
- JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{"));
-
- // Call onMessage with a cancellable future
- CancelCapturingFuture future = new CancelCapturingFuture();
- listener.onMessage(eventData, future);
-
- // Verify the event was received and processed
- assertNotNull(receivedEvent.get());
- assertTrue(receivedEvent.get() instanceof TaskStatusUpdateEvent);
- TaskStatusUpdateEvent received = (TaskStatusUpdateEvent) receivedEvent.get();
- assertTrue(received.isFinal());
-
- // Verify the future was cancelled (auto-close on final event)
- assertTrue(future.cancelHandlerCalled);
- }
-
- @Test
- public void testOnEventWithFinalTaskStatusUpdateEventEventCancels() throws Exception {
- // Set up event handler
- AtomicReference receivedEvent = new AtomicReference<>();
- SSEEventListener listener = new SSEEventListener(
- event -> receivedEvent.set(event),
- error -> {}
- );
-
- // Parse the message event JSON
- String eventData = JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring(
- JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{"));
-
- // Call onEvent method
- CancelCapturingFuture future = new CancelCapturingFuture();
- listener.onMessage(eventData, future);
-
- // Verify the event was processed correctly
- assertNotNull(receivedEvent.get());
- assertTrue(receivedEvent.get() instanceof TaskStatusUpdateEvent);
- TaskStatusUpdateEvent taskStatusUpdateEvent = (TaskStatusUpdateEvent) receivedEvent.get();
- assertEquals("1", taskStatusUpdateEvent.taskId());
- assertEquals("2", taskStatusUpdateEvent.contextId());
- assertTrue(taskStatusUpdateEvent.isFinal());
- assertEquals(TaskState.TASK_STATE_COMPLETED, taskStatusUpdateEvent.status().state());
-
- assertTrue(future.cancelHandlerCalled);
- }
-
-
- private static class CancelCapturingFuture implements Future {
- private boolean cancelHandlerCalled;
-
- public CancelCapturingFuture() {
- }
-
- @Override
- public boolean cancel(boolean mayInterruptIfRunning) {
- cancelHandlerCalled = true;
- return true;
- }
-
- @Override
- public boolean isCancelled() {
- return false;
- }
-
- @Override
- public boolean isDone() {
- return false;
- }
-
- @Override
- public Void get() throws InterruptedException, ExecutionException {
- return null;
- }
-
- @Override
- public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
- return null;
- }
- }
-}
\ No newline at end of file
diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml
deleted file mode 100644
index ecdf7befd..000000000
--- a/client/transport/rest/pom.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
- 4.0.0
-
-
- io.github.a2asdk
- a2a-java-sdk-parent
- 1.0.0.Alpha4-SNAPSHOT
- ../../../pom.xml
-
- a2a-java-sdk-client-transport-rest
- jar
-
- Java SDK A2A Client Transport: JSON+HTTP/REST
- Java SDK for the Agent2Agent Protocol (A2A) - JSON+HTTP/REST Client Transport
-
-
-
- ${project.groupId}
- a2a-java-sdk-common
-
-
- ${project.groupId}
- a2a-java-sdk-spec
-
-
- ${project.groupId}
- a2a-java-sdk-jsonrpc-common
- ${project.version}
-
-
- ${project.groupId}
- a2a-java-sdk-spec-grpc
-
-
- ${project.groupId}
- a2a-java-sdk-client-transport-spi
-
-
- io.github.a2asdk
- a2a-java-sdk-http-client
-
-
- com.google.protobuf
- protobuf-java-util
-
-
- org.junit.jupiter
- junit-jupiter-api
- test
-
-
-
- org.mock-server
- mockserver-netty
- test
-
-
- org.slf4j
- slf4j-jdk14
- test
-
-
-
-
\ No newline at end of file
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
deleted file mode 100644
index 92ce45416..000000000
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package io.a2a.client.transport.rest;
-
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import com.google.gson.JsonObject;
-import io.a2a.client.http.A2AHttpResponse;
-import io.a2a.jsonrpc.common.json.JsonProcessingException;
-import io.a2a.jsonrpc.common.json.JsonUtil;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
-import io.a2a.spec.ContentTypeNotSupportedError;
-import io.a2a.spec.ExtensionSupportRequiredError;
-import io.a2a.spec.InternalError;
-import io.a2a.spec.InvalidAgentResponseError;
-import io.a2a.spec.InvalidParamsError;
-import io.a2a.spec.InvalidRequestError;
-import io.a2a.spec.JSONParseError;
-import io.a2a.spec.MethodNotFoundError;
-import io.a2a.spec.PushNotificationNotSupportedError;
-import io.a2a.spec.TaskNotCancelableError;
-import io.a2a.spec.TaskNotFoundError;
-import io.a2a.spec.UnsupportedOperationError;
-import io.a2a.spec.VersionNotSupportedError;
-
-/**
- * Utility class to A2AHttpResponse to appropriate A2A error types
- */
-public class RestErrorMapper {
-
- public static A2AClientException mapRestError(A2AHttpResponse response) {
- return RestErrorMapper.mapRestError(response.body(), response.status());
- }
-
- public static A2AClientException mapRestError(String body, int code) {
- try {
- if (body != null && !body.isBlank()) {
- JsonObject node = JsonUtil.fromJson(body, JsonObject.class);
- String className = node.has("error") ? node.get("error").getAsString() : "";
- String errorMessage = node.has("message") ? node.get("message").getAsString() : "";
- return mapRestError(className, errorMessage, code);
- }
- return mapRestError("", "", code);
- } catch (JsonProcessingException ex) {
- Logger.getLogger(RestErrorMapper.class.getName()).log(Level.SEVERE, null, ex);
- return new A2AClientException("Failed to parse error response: " + ex.getMessage());
- }
- }
-
- public static A2AClientException mapRestError(String className, String errorMessage, int code) {
- return switch (className) {
- case "io.a2a.spec.TaskNotFoundError" -> new A2AClientException(errorMessage, new TaskNotFoundError());
- case "io.a2a.spec.ExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, null));
- case "io.a2a.spec.ContentTypeNotSupportedError" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, null, errorMessage));
- case "io.a2a.spec.InternalError" -> new A2AClientException(errorMessage, new InternalError(errorMessage));
- case "io.a2a.spec.InvalidAgentResponseError" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, null, errorMessage));
- case "io.a2a.spec.InvalidParamsError" -> new A2AClientException(errorMessage, new InvalidParamsError());
- case "io.a2a.spec.InvalidRequestError" -> new A2AClientException(errorMessage, new InvalidRequestError());
- case "io.a2a.spec.JSONParseError" -> new A2AClientException(errorMessage, new JSONParseError());
- case "io.a2a.spec.MethodNotFoundError" -> new A2AClientException(errorMessage, new MethodNotFoundError());
- case "io.a2a.spec.PushNotificationNotSupportedError" -> new A2AClientException(errorMessage, new PushNotificationNotSupportedError());
- case "io.a2a.spec.TaskNotCancelableError" -> new A2AClientException(errorMessage, new TaskNotCancelableError());
- case "io.a2a.spec.UnsupportedOperationError" -> new A2AClientException(errorMessage, new UnsupportedOperationError());
- case "io.a2a.spec.ExtensionSupportRequiredError" -> new A2AClientException(errorMessage, new ExtensionSupportRequiredError(null, errorMessage, null));
- case "io.a2a.spec.VersionNotSupportedError" -> new A2AClientException(errorMessage, new VersionNotSupportedError(null, errorMessage, null));
- default -> new A2AClientException(errorMessage);
- };
- }
-}
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java
deleted file mode 100644
index 0ec8f2eb1..000000000
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java
+++ /dev/null
@@ -1,485 +0,0 @@
-package io.a2a.client.transport.rest;
-
-import static io.a2a.spec.A2AMethods.CANCEL_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD;
-import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.LIST_TASK_METHOD;
-import static io.a2a.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.SEND_MESSAGE_METHOD;
-import static io.a2a.spec.A2AMethods.SEND_STREAMING_MESSAGE_METHOD;
-import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
-import static io.a2a.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD;
-import static io.a2a.util.Assert.checkNotNullParam;
-
-import java.io.IOException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.logging.Logger;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.protobuf.MessageOrBuilder;
-import com.google.protobuf.util.JsonFormat;
-import io.a2a.client.http.A2AHttpClient;
-import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.http.A2AHttpResponse;
-import io.a2a.client.transport.rest.sse.SSEEventListener;
-import io.a2a.client.transport.spi.ClientTransport;
-import io.a2a.client.transport.spi.interceptors.ClientCallContext;
-import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
-import io.a2a.client.transport.spi.interceptors.PayloadAndHeaders;
-import io.a2a.grpc.utils.ProtoUtils;
-import io.a2a.jsonrpc.common.json.JsonProcessingException;
-import io.a2a.jsonrpc.common.json.JsonUtil;
-import io.a2a.jsonrpc.common.wrappers.ListTasksResult;
-import io.a2a.spec.A2AClientError;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentInterface;
-import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
-import io.a2a.spec.EventKind;
-import io.a2a.spec.GetTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigResult;
-import io.a2a.spec.ListTasksParams;
-import io.a2a.spec.MessageSendParams;
-import io.a2a.spec.StreamingEventKind;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskPushNotificationConfig;
-import io.a2a.spec.TaskQueryParams;
-import io.a2a.util.Utils;
-import org.jspecify.annotations.Nullable;
-
-public class RestTransport implements ClientTransport {
-
- private static final Logger log = Logger.getLogger(RestTransport.class.getName());
- private final A2AHttpClient httpClient;
- private final AgentInterface agentInterface;
- private @Nullable final List interceptors;
- private final AgentCard agentCard;
-
- public RestTransport(AgentCard agentCard) {
- this(null, agentCard, Utils.getFavoriteInterface(agentCard), null);
- }
-
- public RestTransport(@Nullable A2AHttpClient httpClient, AgentCard agentCard,
- AgentInterface agentInterface, @Nullable List interceptors) {
- this.httpClient = httpClient == null ? A2AHttpClientFactory.create() : httpClient;
- this.agentCard = agentCard;
- this.agentInterface = agentInterface;
- this.interceptors = interceptors;
- }
-
- @Override
- public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("messageSendParams", messageSendParams);
- io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(ProtoUtils.ToProto.sendMessageRequest(messageSendParams));
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_MESSAGE_METHOD, builder, agentCard, context);
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, messageSendParams.tenant()) + "/message:send", payloadAndHeaders);
- io.a2a.grpc.SendMessageResponse.Builder responseBuilder = io.a2a.grpc.SendMessageResponse.newBuilder();
- JsonFormat.parser().merge(httpResponseBody, responseBuilder);
- if (responseBuilder.hasMessage()) {
- return ProtoUtils.FromProto.message(responseBuilder.getMessage());
- }
- if (responseBuilder.hasTask()) {
- return ProtoUtils.FromProto.task(responseBuilder.getTask());
- }
- throw new A2AClientException("Failed to send message, wrong response:" + httpResponseBody);
- } catch (A2AClientException e) {
- throw e;
- } catch (IOException | InterruptedException | JsonProcessingException e) {
- throw new A2AClientException("Failed to send message: " + e, e);
- }
- }
-
- @Override
- public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer eventConsumer, Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", messageSendParams);
- checkNotNullParam("eventConsumer", eventConsumer);
- checkNotNullParam("messageSendParams", messageSendParams);
- io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(ProtoUtils.ToProto.sendMessageRequest(messageSendParams));
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_STREAMING_MESSAGE_METHOD, builder, agentCard, context);
- AtomicReference> ref = new AtomicReference<>();
- SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);
- try {
- A2AHttpClient.PostBuilder postBuilder = createPostBuilder(Utils.buildBaseUrl(agentInterface, messageSendParams.tenant()) + "/message:stream", payloadAndHeaders);
- ref.set(postBuilder.postAsyncSSE(
- msg -> sseEventListener.onMessage(msg, ref.get()),
- throwable -> sseEventListener.onError(throwable, ref.get()),
- () -> {
- // We don't need to do anything special on completion
- }));
- } 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 taskQueryParams, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("taskQueryParams", taskQueryParams);
- io.a2a.grpc.GetTaskRequest.Builder builder = io.a2a.grpc.GetTaskRequest.newBuilder();
- builder.setId(taskQueryParams.id());
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_METHOD, builder, agentCard, context);
- try {
- StringBuilder url = new StringBuilder(Utils.buildBaseUrl(agentInterface, taskQueryParams.tenant()));
- if (taskQueryParams.historyLength() != null && taskQueryParams.historyLength() > 0) {
- url.append(String.format("/tasks/%1s?historyLength=%2d", taskQueryParams.id(), taskQueryParams.historyLength()));
- } else {
- url.append(String.format("/tasks/%1s", taskQueryParams.id()));
- }
- A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url.toString());
- if (payloadAndHeaders.getHeaders() != null) {
- for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
- getBuilder.addHeader(entry.getKey(), entry.getValue());
- }
- }
- A2AHttpResponse response = getBuilder.get();
- if (!response.success()) {
- throw RestErrorMapper.mapRestError(response);
- }
- String httpResponseBody = response.body();
- io.a2a.grpc.Task.Builder responseBuilder = io.a2a.grpc.Task.newBuilder();
- JsonFormat.parser().merge(httpResponseBody, responseBuilder);
- return ProtoUtils.FromProto.task(responseBuilder);
- } catch (A2AClientException e) {
- throw e;
- } catch (IOException | InterruptedException e) {
- throw new A2AClientException("Failed to get task: " + e, e);
- }
- }
-
- @Override
- public Task cancelTask(TaskIdParams taskIdParams, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("taskIdParams", taskIdParams);
- io.a2a.grpc.CancelTaskRequest.Builder builder = io.a2a.grpc.CancelTaskRequest.newBuilder();
- builder.setId(taskIdParams.id());
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(CANCEL_TASK_METHOD, builder, agentCard, context);
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, taskIdParams.tenant()) + String.format("/tasks/%1s:cancel", taskIdParams.id()), payloadAndHeaders);
- io.a2a.grpc.Task.Builder responseBuilder = io.a2a.grpc.Task.newBuilder();
- JsonFormat.parser().merge(httpResponseBody, responseBuilder);
- return ProtoUtils.FromProto.task(responseBuilder);
- } catch (A2AClientException e) {
- throw e;
- } catch (IOException | InterruptedException | JsonProcessingException e) {
- throw new A2AClientException("Failed to cancel task: " + e, e);
- }
- }
-
- @Override
- public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- io.a2a.grpc.ListTasksRequest.Builder builder = io.a2a.grpc.ListTasksRequest.newBuilder();
- if (request.contextId() != null) {
- builder.setContextId(request.contextId());
- }
- if (request.status() != null) {
- builder.setStatus(ProtoUtils.ToProto.taskState(request.status()));
- }
- if (request.pageSize() != null) {
- builder.setPageSize(request.pageSize());
- }
- if (request.pageToken() != null) {
- builder.setPageToken(request.pageToken());
- }
- if (request.historyLength() != null) {
- builder.setHistoryLength(request.historyLength());
- }
- if (request.tenant() != null) {
- builder.setTenant(request.tenant());
- }
- if (request.includeArtifacts() != null && request.includeArtifacts()) {
- builder.setIncludeArtifacts(true);
- }
-
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_METHOD, builder, agentCard, context);
-
- try {
- // Build query string
- StringBuilder urlBuilder = new StringBuilder(Utils.buildBaseUrl(agentInterface, request.tenant()));
- urlBuilder.append("/tasks");
- String queryParams = buildListTasksQueryString(request);
- if (!queryParams.isEmpty()) {
- urlBuilder.append("?").append(queryParams);
- }
-
- A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(urlBuilder.toString());
- if (payloadAndHeaders.getHeaders() != null) {
- for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
- getBuilder.addHeader(entry.getKey(), entry.getValue());
- }
- }
- A2AHttpResponse response = getBuilder.get();
- if (!response.success()) {
- throw RestErrorMapper.mapRestError(response);
- }
- String httpResponseBody = response.body();
- io.a2a.grpc.ListTasksResponse.Builder responseBuilder = io.a2a.grpc.ListTasksResponse.newBuilder();
- JsonFormat.parser().merge(httpResponseBody, responseBuilder);
-
- return new ListTasksResult(
- responseBuilder.getTasksList().stream()
- .map(ProtoUtils.FromProto::task)
- .toList(),
- responseBuilder.getTotalSize(),
- responseBuilder.getTasksCount(),
- responseBuilder.getNextPageToken().isEmpty() ? null : responseBuilder.getNextPageToken()
- );
- } catch (IOException | InterruptedException e) {
- throw new A2AClientException("Failed to list tasks: " + e, e);
- }
- }
-
-
- private String buildListTasksQueryString(ListTasksParams request) {
- java.util.List queryParts = new java.util.ArrayList<>();
- if (request.contextId() != null) {
- queryParts.add("contextId=" + URLEncoder.encode(request.contextId(), StandardCharsets.UTF_8));
- }
- if (request.status() != null) {
- queryParts.add("status=" + request.status());
- }
- if (request.pageSize() != null) {
- queryParts.add("pageSize=" + request.pageSize());
- }
- if (request.pageToken() != null) {
- queryParts.add("pageToken=" + URLEncoder.encode(request.pageToken(), StandardCharsets.UTF_8));
- }
- if (request.historyLength() != null) {
- queryParts.add("historyLength=" + request.historyLength());
- }
- if (request.includeArtifacts() != null && request.includeArtifacts()) {
- queryParts.add("includeArtifacts=true");
- }
- return String.join("&", queryParts);
- }
-
- @Override
- public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- io.a2a.grpc.CreateTaskPushNotificationConfigRequest.Builder builder
- = io.a2a.grpc.CreateTaskPushNotificationConfigRequest.newBuilder();
- builder.setConfig(ProtoUtils.ToProto.taskPushNotificationConfig(request).getPushNotificationConfig())
- .setTaskId(request.taskId());
- if (request.config().id() != null) {
- builder.setConfigId(request.config().id());
- }
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, agentCard, context);
- try {
- String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs", request.taskId()), payloadAndHeaders);
- io.a2a.grpc.TaskPushNotificationConfig.Builder responseBuilder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder();
- JsonFormat.parser().merge(httpResponseBody, responseBuilder);
- return ProtoUtils.FromProto.taskPushNotificationConfig(responseBuilder);
- } 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, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- io.a2a.grpc.GetTaskPushNotificationConfigRequest.Builder builder
- = io.a2a.grpc.GetTaskPushNotificationConfigRequest.newBuilder();
- StringBuilder url = new StringBuilder(Utils.buildBaseUrl(agentInterface, request.tenant()));
- String configId = request.id();
- if (configId != null && !configId.isEmpty()) {
- builder.setId(configId).setTaskId(request.taskId());
- url.append(String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.taskId(), configId));
- } else {
- // Use trailing slash to distinguish GET from LIST
- builder.setTaskId(request.taskId());
- url.append(String.format("/tasks/%1s/pushNotificationConfigs/", request.taskId()));
- }
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder,
- agentCard, context);
- try {
- A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url.toString());
- if (payloadAndHeaders.getHeaders() != null) {
- for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
- getBuilder.addHeader(entry.getKey(), entry.getValue());
- }
- }
- A2AHttpResponse response = getBuilder.get();
- if (!response.success()) {
- throw RestErrorMapper.mapRestError(response);
- }
- String httpResponseBody = response.body();
- io.a2a.grpc.TaskPushNotificationConfig.Builder responseBuilder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder();
- JsonFormat.parser().merge(httpResponseBody, responseBuilder);
- return ProtoUtils.FromProto.taskPushNotificationConfig(responseBuilder);
- } catch (A2AClientException e) {
- throw e;
- } catch (IOException | InterruptedException e) {
- throw new A2AClientException("Failed to get push notifications: " + e, e);
- }
- }
-
- @Override
- public ListTaskPushNotificationConfigResult listTaskPushNotificationConfigurations(ListTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- io.a2a.grpc.ListTaskPushNotificationConfigRequest.Builder builder
- = io.a2a.grpc.ListTaskPushNotificationConfigRequest.newBuilder();
- builder.setTaskId(request.id());
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder,
- agentCard, context);
- try {
- String url = Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs", request.id());
- A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url);
- if (payloadAndHeaders.getHeaders() != null) {
- for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
- getBuilder.addHeader(entry.getKey(), entry.getValue());
- }
- }
- A2AHttpResponse response = getBuilder.get();
- if (!response.success()) {
- throw RestErrorMapper.mapRestError(response);
- }
- String httpResponseBody = response.body();
- io.a2a.grpc.ListTaskPushNotificationConfigResponse.Builder responseBuilder = io.a2a.grpc.ListTaskPushNotificationConfigResponse.newBuilder();
- JsonFormat.parser().merge(httpResponseBody, responseBuilder);
- return ProtoUtils.FromProto.listTaskPushNotificationConfigResult(responseBuilder);
- } catch (A2AClientException e) {
- throw e;
- } catch (IOException | InterruptedException e) {
- throw new A2AClientException("Failed to list push notifications: " + e, e);
- }
- }
-
- @Override
- public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- io.a2a.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder builder = io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder();
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder,
- agentCard, context);
- try {
- String url = Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.taskId(), request.id());
- A2AHttpClient.DeleteBuilder deleteBuilder = httpClient.createDelete().url(url);
- if (payloadAndHeaders.getHeaders() != null) {
- for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
- deleteBuilder.addHeader(entry.getKey(), entry.getValue());
- }
- }
- A2AHttpResponse response = deleteBuilder.delete();
- if (!response.success()) {
- throw RestErrorMapper.mapRestError(response);
- }
- } catch (A2AClientException e) {
- throw e;
- } catch (IOException | InterruptedException e) {
- throw new A2AClientException("Failed to delete push notification config: " + e, e);
- }
- }
-
- @Override
- public void subscribeToTask(TaskIdParams request, Consumer eventConsumer,
- Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
- checkNotNullParam("request", request);
- io.a2a.grpc.SubscribeToTaskRequest.Builder builder = io.a2a.grpc.SubscribeToTaskRequest.newBuilder();
- builder.setId(request.id());
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(SUBSCRIBE_TO_TASK_METHOD, builder,
- agentCard, context);
- AtomicReference> ref = new AtomicReference<>();
- SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);
- try {
- String url = Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s:subscribe", request.id());
- A2AHttpClient.PostBuilder postBuilder = createPostBuilder(url, payloadAndHeaders);
- ref.set(postBuilder.postAsyncSSE(
- msg -> sseEventListener.onMessage(msg, ref.get()),
- throwable -> sseEventListener.onError(throwable, ref.get()),
- () -> {
- // We don't need to do anything special on completion
- }));
- } 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 AgentCard getExtendedAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
- try {
- PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_EXTENDED_AGENT_CARD_METHOD, null, agentCard, context);
- String url = Utils.buildBaseUrl(agentInterface, "") + "/extendedAgentCard";
- A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url);
- if (payloadAndHeaders.getHeaders() != null) {
- for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
- getBuilder.addHeader(entry.getKey(), entry.getValue());
- }
- }
- A2AHttpResponse response = getBuilder.get();
- if (!response.success()) {
- throw RestErrorMapper.mapRestError(response);
- }
- String httpResponseBody = response.body();
- return JsonUtil.fromJson(httpResponseBody, AgentCard.class);
- } 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, @Nullable MessageOrBuilder payload,
- AgentCard agentCard, @Nullable 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(String url, PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException {
- A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders);
- A2AHttpResponse response = builder.post();
- if (!response.success()) {
- log.fine("Error on POST processing " + JsonFormat.printer().print((MessageOrBuilder) payloadAndHeaders.getPayload()));
- throw RestErrorMapper.mapRestError(response);
- }
- return response.body();
- }
-
- private A2AHttpClient.PostBuilder createPostBuilder(String url, PayloadAndHeaders payloadAndHeaders) throws JsonProcessingException, InvalidProtocolBufferException {
- log.fine(JsonFormat.printer().print((MessageOrBuilder) payloadAndHeaders.getPayload()));
- A2AHttpClient.PostBuilder postBuilder = httpClient.createPost()
- .url(url)
- .addHeader("Content-Type", "application/json")
- .body(JsonFormat.printer().print((MessageOrBuilder) payloadAndHeaders.getPayload()));
-
- if (payloadAndHeaders.getHeaders() != null) {
- for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
- postBuilder.addHeader(entry.getKey(), entry.getValue());
- }
- }
- return postBuilder;
- }
-
- private Map getHttpHeaders(@Nullable ClientCallContext context) {
- return context != null ? context.getHeaders() : Collections.emptyMap();
- }
-}
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfig.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfig.java
deleted file mode 100644
index e78c67271..000000000
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfig.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package io.a2a.client.transport.rest;
-
-import io.a2a.client.http.A2AHttpClient;
-import io.a2a.client.transport.spi.ClientTransportConfig;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Configuration for the REST transport protocol.
- *
- * This configuration class allows customization of the HTTP client used for RESTful
- * communication with A2A agents. If no HTTP client is specified, the default JDK-based
- * implementation is used.
- *
- * Basic usage:
- *
{@code
- * // Use default HTTP client
- * RestTransportConfig config = new RestTransportConfigBuilder()
- * .build();
- *
- * Client client = Client.builder(agentCard)
- * .withTransport(RestTransport.class, config)
- * .build();
- * }
- *
- * Custom HTTP client:
- *
{@code
- * // Custom HTTP client with timeouts
- * A2AHttpClient customClient = new CustomHttpClient()
- * .withConnectTimeout(Duration.ofSeconds(10))
- * .withReadTimeout(Duration.ofSeconds(30));
- *
- * RestTransportConfig config = new RestTransportConfigBuilder()
- * .httpClient(customClient)
- * .build();
- * }
- *
- * With interceptors:
- *
{@code
- * RestTransportConfig config = new RestTransportConfigBuilder()
- * .httpClient(customClient)
- * .addInterceptor(new LoggingInterceptor())
- * .addInterceptor(new AuthInterceptor("Bearer token"))
- * .build();
- * }
- *
- * @see RestTransportConfigBuilder
- * @see RestTransport
- * @see A2AHttpClient
- * @see io.a2a.client.http.JdkA2AHttpClient
- */
-public class RestTransportConfig extends ClientTransportConfig {
-
- private final @Nullable A2AHttpClient httpClient;
-
- /**
- * Create a REST transport configuration with the default HTTP client.
- *
- * The default JDK-based HTTP client will be used. Consider using
- * {@link RestTransportConfigBuilder} instead for a more fluent API.
- */
- public RestTransportConfig() {
- this.httpClient = null;
- }
-
- /**
- * Create a REST transport configuration with a custom HTTP client.
- *
- * Consider using {@link RestTransportConfigBuilder} instead for a more fluent API.
- *
- * @param httpClient the HTTP client to use for REST requests
- */
- public RestTransportConfig(A2AHttpClient httpClient) {
- this.httpClient = httpClient;
- }
-
- /**
- * Get the configured HTTP client.
- *
- * @return the HTTP client, or {@code null} if using the default
- */
- public @Nullable A2AHttpClient getHttpClient() {
- return httpClient;
- }
-}
\ No newline at end of file
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java
deleted file mode 100644
index 257836199..000000000
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package io.a2a.client.transport.rest;
-
-import io.a2a.client.http.A2AHttpClient;
-import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Builder for creating {@link RestTransportConfig} instances.
- *
- * This builder provides a fluent API for configuring the REST transport protocol.
- * All configuration options are optional - if not specified, sensible defaults are used:
- *
- * - HTTP client: Auto-selected via {@link A2AHttpClientFactory} (prefers Vert.x, falls back to JDK)
- * - Interceptors: None
- *
- *
- * Basic usage:
- *
{@code
- * // Minimal configuration (uses all defaults)
- * RestTransportConfig config = new RestTransportConfigBuilder()
- * .build();
- *
- * Client client = Client.builder(agentCard)
- * .withTransport(RestTransport.class, config)
- * .build();
- * }
- *
- * Custom HTTP client:
- *
{@code
- * // Configure custom HTTP client for connection pooling, timeouts, etc.
- * A2AHttpClient httpClient = new ApacheHttpClient()
- * .withConnectionTimeout(Duration.ofSeconds(10))
- * .withMaxConnections(50);
- *
- * RestTransportConfig config = new RestTransportConfigBuilder()
- * .httpClient(httpClient)
- * .build();
- * }
- *
- * With interceptors:
- *
{@code
- * RestTransportConfig config = new RestTransportConfigBuilder()
- * .addInterceptor(new LoggingInterceptor())
- * .addInterceptor(new MetricsInterceptor())
- * .addInterceptor(new RetryInterceptor(3))
- * .build();
- * }
- *
- * Direct usage in ClientBuilder:
- *
{@code
- * // Can pass builder directly to withTransport()
- * Client client = Client.builder(agentCard)
- * .withTransport(RestTransport.class, new RestTransportConfigBuilder()
- * .httpClient(customClient)
- * .addInterceptor(loggingInterceptor))
- * .build();
- * }
- *
- * @see RestTransportConfig
- * @see RestTransport
- * @see A2AHttpClient
- * @see io.a2a.client.http.JdkA2AHttpClient
- */
-public class RestTransportConfigBuilder extends ClientTransportConfigBuilder {
-
- private @Nullable A2AHttpClient httpClient;
-
- /**
- * Set the HTTP client to use for REST requests.
- *
- * Custom HTTP clients can provide:
- *
- * - Connection pooling and reuse
- * - Custom timeout configuration
- * - SSL/TLS configuration
- * - Proxy support
- * - Custom header handling
- *
- *
- * If not specified, a client is auto-selected via {@link A2AHttpClientFactory}.
- *
- * Example:
- *
{@code
- * A2AHttpClient client = new CustomHttpClient()
- * .withConnectTimeout(Duration.ofSeconds(5))
- * .withReadTimeout(Duration.ofSeconds(30))
- * .withConnectionPool(10, 50);
- *
- * builder.httpClient(client);
- * }
- *
- * @param httpClient the HTTP client to use
- * @return this builder for method chaining
- */
- public RestTransportConfigBuilder httpClient(A2AHttpClient httpClient) {
- this.httpClient = httpClient;
- return this;
- }
-
- /**
- * Build the REST transport configuration.
- *
- * If no HTTP client was configured, one is auto-selected via {@link A2AHttpClientFactory}.
- * Any configured interceptors are transferred to the configuration.
- *
- * @return the configured REST transport configuration
- */
- @Override
- public RestTransportConfig build() {
- // No HTTP client provided, use factory to get best available implementation
- if (httpClient == null) {
- httpClient = A2AHttpClientFactory.create();
- }
-
- RestTransportConfig config = new RestTransportConfig(httpClient);
- config.setInterceptors(this.interceptors);
- return config;
- }
-}
\ No newline at end of file
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportProvider.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportProvider.java
deleted file mode 100644
index b6e6d7a47..000000000
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportProvider.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package io.a2a.client.transport.rest;
-
-import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.transport.spi.ClientTransportProvider;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentInterface;
-import io.a2a.spec.TransportProtocol;
-
-public class RestTransportProvider implements ClientTransportProvider {
-
- @Override
- public String getTransportProtocol() {
- return TransportProtocol.HTTP_JSON.asString();
- }
-
- @Override
- public RestTransport create(RestTransportConfig clientTransportConfig, AgentCard agentCard, AgentInterface agentInterface) throws A2AClientException {
- RestTransportConfig transportConfig = clientTransportConfig;
- if (transportConfig == null) {
- transportConfig = new RestTransportConfig(A2AHttpClientFactory.create());
- }
- return new RestTransport(transportConfig.getHttpClient(), agentCard, agentInterface, transportConfig.getInterceptors());
- }
-
- @Override
- public Class getTransportProtocolClass() {
- return RestTransport.class;
- }
-}
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/package-info.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/package-info.java
deleted file mode 100644
index b7892c239..000000000
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-@NullMarked
-package io.a2a.client.transport.rest;
-
-import org.jspecify.annotations.NullMarked;
-
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/SSEEventListener.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/SSEEventListener.java
deleted file mode 100644
index 2c9167b63..000000000
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/SSEEventListener.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package io.a2a.client.transport.rest.sse;
-
-import java.util.concurrent.Future;
-import java.util.function.Consumer;
-import java.util.logging.Logger;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.protobuf.util.JsonFormat;
-import io.a2a.client.transport.spi.sse.AbstractSSEEventListener;
-import io.a2a.client.transport.rest.RestErrorMapper;
-import io.a2a.grpc.StreamResponse;
-import io.a2a.grpc.utils.ProtoUtils;
-import io.a2a.spec.StreamingEventKind;
-import org.jspecify.annotations.Nullable;
-
-/**
- * REST transport implementation of SSE event listener.
- * Handles parsing of JSON-formatted protobuf messages from REST SSE streams.
- */
-public class SSEEventListener extends AbstractSSEEventListener {
-
- private static final Logger log = Logger.getLogger(SSEEventListener.class.getName());
-
- public SSEEventListener(Consumer eventHandler,
- @Nullable Consumer errorHandler) {
- super(eventHandler, errorHandler);
- }
-
- @Override
- public void onMessage(String message, @Nullable Future completableFuture) {
- try {
- log.fine("Streaming message received: " + message);
- io.a2a.grpc.StreamResponse.Builder builder = io.a2a.grpc.StreamResponse.newBuilder();
- JsonFormat.parser().merge(message, builder);
- parseAndHandleMessage(builder.build(), completableFuture);
- } catch (InvalidProtocolBufferException e) {
- if (getErrorHandler() != null) {
- getErrorHandler().accept(RestErrorMapper.mapRestError(message, 500));
- }
- }
- }
-
- /**
- * Parses a StreamResponse protobuf message and delegates to the base class for event handling.
- *
- * @param response The parsed StreamResponse
- * @param future Optional future for controlling the SSE connection
- */
- private void parseAndHandleMessage(StreamResponse response, @Nullable Future future) {
- StreamingEventKind event;
- switch (response.getPayloadCase()) {
- case MESSAGE ->
- event = ProtoUtils.FromProto.message(response.getMessage());
- case TASK ->
- event = ProtoUtils.FromProto.task(response.getTask());
- case STATUS_UPDATE ->
- event = ProtoUtils.FromProto.taskStatusUpdateEvent(response.getStatusUpdate());
- case ARTIFACT_UPDATE ->
- event = ProtoUtils.FromProto.taskArtifactUpdateEvent(response.getArtifactUpdate());
- default -> {
- log.warning("Invalid stream response " + response.getPayloadCase());
- if (getErrorHandler() != null) {
- getErrorHandler().accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase()));
- }
- return;
- }
- }
-
- // Delegate to base class for common event handling and auto-close logic
- handleEvent(event, future);
- }
-
-}
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/package-info.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/package-info.java
deleted file mode 100644
index e1bb6c3b2..000000000
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-@NullMarked
-package io.a2a.client.transport.rest.sse;
-
-import org.jspecify.annotations.NullMarked;
-
diff --git a/client/transport/rest/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider b/client/transport/rest/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider
deleted file mode 100644
index 894866aab..000000000
--- a/client/transport/rest/src/main/resources/META-INF/services/io.a2a.client.transport.spi.ClientTransportProvider
+++ /dev/null
@@ -1 +0,0 @@
-io.a2a.client.transport.rest.RestTransportProvider
\ No newline at end of file
diff --git a/client/transport/rest/src/test/java/io/a2a/client/transport/rest/JsonRestMessages.java b/client/transport/rest/src/test/java/io/a2a/client/transport/rest/JsonRestMessages.java
deleted file mode 100644
index 35ac4a9c5..000000000
--- a/client/transport/rest/src/test/java/io/a2a/client/transport/rest/JsonRestMessages.java
+++ /dev/null
@@ -1,399 +0,0 @@
-package io.a2a.client.transport.rest;
-
-/**
- * Request and response messages used by the tests. These have been created following examples from
- * the A2A sample messages.
- */
-public class JsonRestMessages {
-
- static final String SEND_MESSAGE_TEST_REQUEST = """
- {
- "message":
- {
- "messageId": "message-1234",
- "contextId": "context-1234",
- "role": "ROLE_USER",
- "parts": [{
- "text": "tell me a joke"
- }],
- "metadata": {
- }
- }
- }""";
-
- static final String SEND_MESSAGE_TEST_RESPONSE = """
- {
- "task": {
- "id": "9b511af4-b27c-47fa-aecf-2a93c08a44f8",
- "contextId": "context-1234",
- "status": {
- "state": "TASK_STATE_SUBMITTED"
- },
- "history": [
- {
- "messageId": "message-1234",
- "contextId": "context-1234",
- "taskId": "9b511af4-b27c-47fa-aecf-2a93c08a44f8",
- "role": "ROLE_USER",
- "parts": [
- {
- "text": "tell me a joke"
- }
- ],
- "metadata": {}
- }
- ]
- }
- }""";
-
- static final String CANCEL_TASK_TEST_REQUEST = """
- {
- "id": "de38c76d-d54c-436c-8b9f-4c2703648d64"
- }""";
-
- static final String CANCEL_TASK_TEST_RESPONSE = """
- {
- "id": "de38c76d-d54c-436c-8b9f-4c2703648d64",
- "contextId": "c295ea44-7543-4f78-b524-7a38915ad6e4",
- "status": {
- "state": "TASK_STATE_CANCELED"
- },
- "metadata": {}
- }""";
-
- static final String GET_TASK_TEST_RESPONSE = """
- {
- "id": "de38c76d-d54c-436c-8b9f-4c2703648d64",
- "contextId": "c295ea44-7543-4f78-b524-7a38915ad6e4",
- "status": {
- "state": "TASK_STATE_COMPLETED"
- },
- "artifacts": [
- {
- "artifactId": "artifact-1",
- "parts": [
- {
- "text": "Why did the chicken cross the road? To get to the other side!"
- }
- ]
- }
- ],
- "history": [
- {
- "role": "ROLE_USER",
- "parts": [
- {
- "text": "tell me a joke"
- },
- {
- "url": "file:///path/to/file.txt",
- "mediaType": "text/plain"
- },
- {
- "raw": "aGVsbG8=",
- "mediaType": "text/plain"
- }
- ],
- "messageId": "message-123"
- }
- ],
- "metadata": {}
- }
- """;
-
- static final String AGENT_CARD = """
- {
- "name": "GeoSpatial Route Planner Agent",
- "description": "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.",
- "supportedInterfaces": [
- {"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "HTTP+JSON"}
- ],
- "provider": {
- "organization": "Example Geo Services Inc.",
- "url": "https://www.examplegeoservices.com"
- },
- "iconUrl": "https://georoute-agent.example.com/icon.png",
- "version": "1.2.0",
- "documentationUrl": "https://docs.examplegeoservices.com/georoute-agent/api",
- "capabilities": {
- "streaming": true,
- "pushNotifications": true
- },
- "securitySchemes": {
- "google": {
- "type": "openIdConnect",
- "openIdConnectUrl": "https://accounts.google.com/.well-known/openid-configuration"
- }
- },
- "securityRequirements": [{ "schemes": { "google": { "list": ["openid", "profile", "email"] } } }],
- "defaultInputModes": ["application/json", "text/plain"],
- "defaultOutputModes": ["application/json", "image/png"],
- "skills": [
- {
- "id": "route-optimizer-traffic",
- "name": "Traffic-Aware Route Optimizer",
- "description": "Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).",
- "tags": ["maps", "routing", "navigation", "directions", "traffic"],
- "examples": [
- "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.",
- "{\\"origin\\": {\\"lat\\": 37.422, \\"lng\\": -122.084}, \\"destination\\": {\\"lat\\": 37.7749, \\"lng\\": -122.4194}, \\"preferences\\": [\\"avoid_ferries\\"]}"
- ],
- "inputModes": ["application/json", "text/plain"],
- "outputModes": [
- "application/json",
- "application/vnd.geo+json",
- "text/html"
- ]
- },
- {
- "id": "custom-map-generator",
- "name": "Personalized Map Generator",
- "description": "Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.",
- "tags": ["maps", "customization", "visualization", "cartography"],
- "examples": [
- "Generate a map of my upcoming road trip with all planned stops highlighted.",
- "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."
- ],
- "inputModes": ["application/json"],
- "outputModes": [
- "image/png",
- "image/jpeg",
- "application/json",
- "text/html"
- ]
- }
- ],
- "supportsExtendedAgentCard": false,
- "protocolVersion": "0.2.5"
- }""";
-
- static final String AGENT_CARD_SUPPORTS_EXTENDED = """
- {
- "name": "GeoSpatial Route Planner Agent",
- "description": "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.",
- "supportedInterfaces": [
- {"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "HTTP+JSON"}
- ],
- "provider": {
- "organization": "Example Geo Services Inc.",
- "url": "https://www.examplegeoservices.com"
- },
- "iconUrl": "https://georoute-agent.example.com/icon.png",
- "version": "1.2.0",
- "documentationUrl": "https://docs.examplegeoservices.com/georoute-agent/api",
- "capabilities": {
- "streaming": true,
- "pushNotifications": true
- },
- "securitySchemes": {
- "google": {
- "type": "openIdConnect",
- "openIdConnectUrl": "https://accounts.google.com/.well-known/openid-configuration"
- }
- },
- "securityRequirements": [{ "schemes": { "google": { "list": ["openid", "profile", "email"] } } }],
- "defaultInputModes": ["application/json", "text/plain"],
- "defaultOutputModes": ["application/json", "image/png"],
- "skills": [
- {
- "id": "route-optimizer-traffic",
- "name": "Traffic-Aware Route Optimizer",
- "description": "Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).",
- "tags": ["maps", "routing", "navigation", "directions", "traffic"],
- "examples": [
- "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.",
- "{\\"origin\\": {\\"lat\\": 37.422, \\"lng\\": -122.084}, \\"destination\\": {\\"lat\\": 37.7749, \\"lng\\": -122.4194}, \\"preferences\\": [\\"avoid_ferries\\"]}"
- ],
- "inputModes": ["application/json", "text/plain"],
- "outputModes": [
- "application/json",
- "application/vnd.geo+json",
- "text/html"
- ]
- },
- {
- "id": "custom-map-generator",
- "name": "Personalized Map Generator",
- "description": "Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.",
- "tags": ["maps", "customization", "visualization", "cartography"],
- "examples": [
- "Generate a map of my upcoming road trip with all planned stops highlighted.",
- "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."
- ],
- "inputModes": ["application/json"],
- "outputModes": [
- "image/png",
- "image/jpeg",
- "application/json",
- "text/html"
- ]
- }
- ],
- "supportsExtendedAgentCard": true,
- "protocolVersion": "0.2.5"
- }""";
-
- static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """
- {
- "name": "GeoSpatial Route Planner Agent Extended",
- "description": "Extended description",
- "supportedInterfaces": [
- {"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "HTTP+JSON"}
- ],
- "provider": {
- "organization": "Example Geo Services Inc.",
- "url": "https://www.examplegeoservices.com"
- },
- "iconUrl": "https://georoute-agent.example.com/icon.png",
- "version": "1.2.0",
- "documentationUrl": "https://docs.examplegeoservices.com/georoute-agent/api",
- "capabilities": {
- "streaming": true,
- "pushNotifications": true
- },
- "securitySchemes": {
- "google": {
- "type": "openIdConnect",
- "openIdConnectUrl": "https://accounts.google.com/.well-known/openid-configuration"
- }
- },
- "securityRequirements": [{ "schemes": { "google": { "list": ["openid", "profile", "email"] } } }],
- "defaultInputModes": ["application/json", "text/plain"],
- "defaultOutputModes": ["application/json", "image/png"],
- "skills": [
- {
- "id": "route-optimizer-traffic",
- "name": "Traffic-Aware Route Optimizer",
- "description": "Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).",
- "tags": ["maps", "routing", "navigation", "directions", "traffic"],
- "examples": [
- "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.",
- "{\\"origin\\": {\\"lat\\": 37.422, \\"lng\\": -122.084}, \\"destination\\": {\\"lat\\": 37.7749, \\"lng\\": -122.4194}, \\"preferences\\": [\\"avoid_ferries\\"]}"
- ],
- "inputModes": ["application/json", "text/plain"],
- "outputModes": [
- "application/json",
- "application/vnd.geo+json",
- "text/html"
- ]
- },
- {
- "id": "custom-map-generator",
- "name": "Personalized Map Generator",
- "description": "Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.",
- "tags": ["maps", "customization", "visualization", "cartography"],
- "examples": [
- "Generate a map of my upcoming road trip with all planned stops highlighted.",
- "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."
- ],
- "inputModes": ["application/json"],
- "outputModes": [
- "image/png",
- "image/jpeg",
- "application/json",
- "text/html"
- ]
- },
- {
- "id": "skill-extended",
- "name": "Extended Skill",
- "description": "This is an extended skill.",
- "tags": ["extended"]
- }
- ],
- "supportsExtendedAgentCard": true,
- "protocolVersion": "0.2.5"
- }""";
-
- static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """
- {
- "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64",
- "id": "10",
- "pushNotificationConfig": {
- "url": "https://example.com/callback",
- "authentication": {
- "scheme": "jwt"
- }
- }
- }""";
- static final String LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """
- {
- "configs":[
- {
- "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64",
- "id": "10",
- "pushNotificationConfig": {
- "url": "https://example.com/callback",
- "authentication": {
- "scheme": "jwt"
- }
- }
- },
- {
- "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64",
- "id": "5",
- "pushNotificationConfig": {
- "url": "https://test.com/callback"
- }
- }
- ]
- }""";
-
-
- static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """
- {
- "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64",
- "configId": "default-config-id",
- "config": {
- "url": "https://example.com/callback",
- "authentication": {
- "scheme": "jwt"
- }
- }
- }""";
-
- static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """
- {
- "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64",
- "id": "10",
- "pushNotificationConfig": {
- "url": "https://example.com/callback",
- "authentication": {
- "scheme": "jwt"
- }
- }
- }""";
-
-
- public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = """
- {
- "message": {
- "role": "ROLE_USER",
- "parts": [
- {
- "text": "tell me some jokes"
- }
- ],
- "messageId": "message-1234",
- "contextId": "context-1234"
- },
- "configuration": {
- "acceptedOutputModes": ["text"]
- }
- }""";
- static final String SEND_MESSAGE_STREAMING_TEST_RESPONSE
- = "event: message\n"
- + "data: {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_SUBMITTED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why did the chicken cross the road? To get to the other side!\"}]}],\"metadata\":{}}}\n\n";
-
- static final String TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE
- = "event: message\n"
- + "data: {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_COMPLETED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why did the chicken cross the road? To get to the other side!\"}]}],\"metadata\":{}}}\n\n";
- public static final String TASK_RESUBSCRIPTION_TEST_REQUEST = """
- {
- "jsonrpc": "2.0",
- "method": "SubscribeToTask",
- "params": {
- "id": "task-1234"
- }
- }""";
-}
diff --git a/client/transport/rest/src/test/java/io/a2a/client/transport/rest/RestTransportTest.java b/client/transport/rest/src/test/java/io/a2a/client/transport/rest/RestTransportTest.java
deleted file mode 100644
index 2f3ae18db..000000000
--- a/client/transport/rest/src/test/java/io/a2a/client/transport/rest/RestTransportTest.java
+++ /dev/null
@@ -1,611 +0,0 @@
-package io.a2a.client.transport.rest;
-
-
-import static io.a2a.client.transport.rest.JsonRestMessages.CANCEL_TASK_TEST_REQUEST;
-import static io.a2a.client.transport.rest.JsonRestMessages.CANCEL_TASK_TEST_RESPONSE;
-import static io.a2a.client.transport.rest.JsonRestMessages.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
-import static io.a2a.client.transport.rest.JsonRestMessages.GET_TASK_TEST_RESPONSE;
-import static io.a2a.client.transport.rest.JsonRestMessages.LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
-import static io.a2a.client.transport.rest.JsonRestMessages.SEND_MESSAGE_STREAMING_TEST_REQUEST;
-import static io.a2a.client.transport.rest.JsonRestMessages.SEND_MESSAGE_STREAMING_TEST_RESPONSE;
-import static io.a2a.client.transport.rest.JsonRestMessages.SEND_MESSAGE_TEST_REQUEST;
-import static io.a2a.client.transport.rest.JsonRestMessages.SEND_MESSAGE_TEST_RESPONSE;
-import static io.a2a.client.transport.rest.JsonRestMessages.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST;
-import static io.a2a.client.transport.rest.JsonRestMessages.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
-import static io.a2a.client.transport.rest.JsonRestMessages.TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE;
-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.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockserver.model.HttpRequest.request;
-import static org.mockserver.model.HttpResponse.response;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.logging.Logger;
-
-import io.a2a.client.transport.spi.interceptors.ClientCallContext;
-import io.a2a.spec.A2AClientException;
-import io.a2a.spec.AgentCapabilities;
-import io.a2a.spec.AgentCard;
-import io.a2a.spec.AgentSkill;
-import io.a2a.spec.Artifact;
-import io.a2a.spec.AuthenticationInfo;
-import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
-import io.a2a.spec.EventKind;
-import io.a2a.spec.ExtensionSupportRequiredError;
-import io.a2a.spec.VersionNotSupportedError;
-import io.a2a.spec.FilePart;
-import io.a2a.spec.FileWithBytes;
-import io.a2a.spec.FileWithUri;
-import io.a2a.spec.GetTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigParams;
-import io.a2a.spec.ListTaskPushNotificationConfigResult;
-import io.a2a.spec.Message;
-import io.a2a.spec.MessageSendConfiguration;
-import io.a2a.spec.MessageSendParams;
-import io.a2a.spec.Part;
-import io.a2a.spec.PushNotificationConfig;
-import io.a2a.spec.StreamingEventKind;
-import io.a2a.spec.Task;
-import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskPushNotificationConfig;
-import io.a2a.spec.TaskQueryParams;
-import io.a2a.spec.TaskState;
-import io.a2a.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 RestTransportTest {
-
- private static final Logger log = Logger.getLogger(RestTransportTest.class.getName());
- private ClientAndServer server;
- private static final AgentCard CARD = AgentCard.builder()
- .name("Hello World Agent")
- .description("Just a hello world agent")
- .supportedInterfaces(Collections.singletonList(new io.a2a.spec.AgentInterface("HTTP+JSON", "http://localhost:4001")))
- .version("1.0.0")
- .documentationUrl("http://example.com/docs")
- .capabilities(AgentCapabilities.builder()
- .streaming(true)
- .pushNotifications(true)
- .build())
- .defaultInputModes(Collections.singletonList("text"))
- .defaultOutputModes(Collections.singletonList("text"))
- .skills(Collections.singletonList(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()))
- .build();
-
- @BeforeEach
- public void setUp() throws IOException {
- server = new ClientAndServer(4001);
- }
-
- @AfterEach
- public void tearDown() {
- server.stop();
- }
-
- public RestTransportTest() {
- }
-
- /**
- * Test of sendMessage method, of class JSONRestTransport.
- */
- @Test
- public void testSendMessage() throws Exception {
- Message message = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("tell me a joke")))
- .contextId("context-1234")
- .messageId("message-1234")
- .taskId("")
- .build();
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/message:send")
- .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(SEND_MESSAGE_TEST_RESPONSE)
- );
- MessageSendParams messageSendParams = new MessageSendParams(message, null, null, "");
- ClientCallContext context = null;
-
- RestTransport instance = new RestTransport(CARD);
- EventKind result = instance.sendMessage(messageSendParams, context);
- assertEquals("task", result.kind());
- Task task = (Task) result;
- assertEquals("9b511af4-b27c-47fa-aecf-2a93c08a44f8", task.id());
- assertEquals("context-1234", task.contextId());
- assertEquals(TaskState.TASK_STATE_SUBMITTED, task.status().state());
- assertNull(task.status().message());
- assertNull(task.metadata());
- assertEquals(true, task.artifacts().isEmpty());
- assertEquals(1, task.history().size());
- Message history = task.history().get(0);
- assertEquals("message", history.kind());
- assertEquals(Message.Role.ROLE_USER, history.role());
- assertEquals("context-1234", history.contextId());
- assertEquals("message-1234", history.messageId());
- assertEquals("9b511af4-b27c-47fa-aecf-2a93c08a44f8", history.taskId());
- assertEquals(1, history.parts().size());
- assertTrue(history.parts().get(0) instanceof io.a2a.spec.TextPart);
- assertEquals("tell me a joke", ((TextPart) history.parts().get(0)).text());
- assertNull(task.metadata());
- assertNull(history.referenceTaskIds());
- }
-
- /**
- * Test of cancelTask method, of class JSONRestTransport.
- */
- @Test
- public void testCancelTask() throws Exception {
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64:cancel")
- .withBody(JsonBody.json(CANCEL_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(CANCEL_TASK_TEST_RESPONSE)
- );
- ClientCallContext context = null;
- RestTransport instance = new RestTransport(CARD);
- Task task = instance.cancelTask(new TaskIdParams("de38c76d-d54c-436c-8b9f-4c2703648d64"), context);
- assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id());
- assertEquals(TaskState.TASK_STATE_CANCELED, task.status().state());
- assertNull(task.status().message());
- assertNotNull(task.metadata());
- assertTrue(task.metadata().isEmpty());
- }
-
- /**
- * Test of getTask method, of class JSONRestTransport.
- */
- @Test
- public void testGetTask() throws Exception {
- this.server.when(
- request()
- .withMethod("GET")
- .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64")
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(GET_TASK_TEST_RESPONSE)
- );
- ClientCallContext context = null;
- TaskQueryParams request = new TaskQueryParams("de38c76d-d54c-436c-8b9f-4c2703648d64", 10);
- RestTransport instance = new RestTransport(CARD);
- Task task = instance.getTask(request, context);
- assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id());
- assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state());
- assertNull(task.status().message());
- assertNotNull(task.metadata());
- assertTrue(task.metadata().isEmpty());
- assertEquals(false, task.artifacts().isEmpty());
- assertEquals(1, task.artifacts().size());
- Artifact artifact = task.artifacts().get(0);
- assertEquals("artifact-1", artifact.artifactId());
- assertNull(artifact.name());
- assertEquals(false, artifact.parts().isEmpty());
- assertTrue(artifact.parts().get(0) instanceof io.a2a.spec.TextPart);
- assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) artifact.parts().get(0)).text());
- assertEquals(1, task.history().size());
- Message history = task.history().get(0);
- assertEquals("message", history.kind());
- assertEquals(Message.Role.ROLE_USER, history.role());
- assertEquals("message-123", history.messageId());
- assertEquals(3, history.parts().size());
- assertTrue(history.parts().get(0) instanceof io.a2a.spec.TextPart);
- assertEquals("tell me a joke", ((TextPart) history.parts().get(0)).text());
- assertTrue(history.parts().get(1) instanceof FilePart);
- FilePart part = (FilePart) history.parts().get(1);
- assertEquals("text/plain", part.file().mimeType());
- assertEquals("file:///path/to/file.txt", ((FileWithUri) part.file()).uri());
- part = (FilePart) history.parts().get(2);
- assertTrue(part instanceof FilePart);
- assertEquals("text/plain", part.file().mimeType());
- assertEquals("aGVsbG8=", ((FileWithBytes) part.file()).bytes());
- assertNull(history.metadata());
- assertNull(history.referenceTaskIds());
- }
-
- /**
- * Test of sendMessageStreaming method, of class JSONRestTransport.
- */
- @Test
- public void testSendMessageStreaming() throws Exception {
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/message:stream")
- .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)
- );
-
- RestTransport client = new RestTransport(CARD);
- Message message = Message.builder()
- .role(Message.Role.ROLE_USER)
- .parts(Collections.singletonList(new TextPart("tell me some jokes")))
- .contextId("context-1234")
- .messageId("message-1234")
- .build();
- MessageSendConfiguration configuration = MessageSendConfiguration.builder()
- .acceptedOutputModes(List.of("text"))
- .blocking(false)
- .build();
- MessageSendParams params = 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());
- assertEquals("task", receivedEvent.get().kind());
- Task task = (Task) receivedEvent.get();
- assertEquals("2", task.id());
- }
-
- /**
- * Test of CreateTaskPushNotificationConfiguration method, of class JSONRestTransport.
- */
- @Test
- public void testCreateTaskPushNotificationConfiguration() throws Exception {
- log.info("Testing CreateTaskPushNotificationConfiguration");
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/tenant/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")
- .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)
- );
- RestTransport client = new RestTransport(CARD);
- TaskPushNotificationConfig pushedConfig = new TaskPushNotificationConfig(
- "de38c76d-d54c-436c-8b9f-4c2703648d64",
- PushNotificationConfig.builder()
- .id("default-config-id")
- .url("https://example.com/callback")
- .authentication(
- new AuthenticationInfo("jwt", null))
- .build(), "tenant");
- TaskPushNotificationConfig taskPushNotificationConfig = client.createTaskPushNotificationConfiguration(pushedConfig, null);
- PushNotificationConfig pushNotificationConfig = taskPushNotificationConfig.config();
- assertNotNull(pushNotificationConfig);
- assertEquals("https://example.com/callback", pushNotificationConfig.url());
- AuthenticationInfo authenticationInfo = pushNotificationConfig.authentication();
- assertEquals("jwt", authenticationInfo.scheme());
- }
-
- /**
- * Test of getTaskPushNotificationConfiguration method, of class JSONRestTransport.
- */
- @Test
- public void testGetTaskPushNotificationConfiguration() throws Exception {
- this.server.when(
- request()
- .withMethod("GET")
- .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)
- );
-
- RestTransport client = new RestTransport(CARD);
- TaskPushNotificationConfig taskPushNotificationConfig = client.getTaskPushNotificationConfiguration(
- new GetTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), null);
- PushNotificationConfig pushNotificationConfig = taskPushNotificationConfig.config();
- assertNotNull(pushNotificationConfig);
- assertEquals("https://example.com/callback", pushNotificationConfig.url());
- AuthenticationInfo authenticationInfo = pushNotificationConfig.authentication();
- assertEquals("jwt", authenticationInfo.scheme());
- }
-
- /**
- * Test of listTaskPushNotificationConfigurations method, of class JSONRestTransport.
- */
- @Test
- public void testListTaskPushNotificationConfigurations() throws Exception {
- this.server.when(
- request()
- .withMethod("GET")
- .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)
- );
-
- RestTransport client = new RestTransport(CARD);
- ListTaskPushNotificationConfigResult result = client.listTaskPushNotificationConfigurations(
- new ListTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64"), null);
- assertEquals(2, result.configs().size());
- PushNotificationConfig pushNotificationConfig = result.configs().get(0).config();
- assertNotNull(pushNotificationConfig);
- assertEquals("https://example.com/callback", pushNotificationConfig.url());
- assertEquals("10", pushNotificationConfig.id());
- AuthenticationInfo authenticationInfo = pushNotificationConfig.authentication();
- assertEquals("jwt", authenticationInfo.scheme());
- assertEquals("", authenticationInfo.credentials());
- pushNotificationConfig = result.configs().get(1).config();
- assertNotNull(pushNotificationConfig);
- assertEquals("https://test.com/callback", pushNotificationConfig.url());
- assertEquals("5", pushNotificationConfig.id());
- authenticationInfo = pushNotificationConfig.authentication();
- assertNull(authenticationInfo);
- }
-
- /**
- * Test of deleteTaskPushNotificationConfigurations method, of class JSONRestTransport.
- */
- @Test
- public void testDeleteTaskPushNotificationConfigurations() throws Exception {
- log.info("Testing deleteTaskPushNotificationConfigurations");
- this.server.when(
- request()
- .withMethod("DELETE")
- .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")
- )
- .respond(
- response()
- .withStatusCode(200)
- );
- ClientCallContext context = null;
- RestTransport instance = new RestTransport(CARD);
- instance.deleteTaskPushNotificationConfigurations(new DeleteTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), context);
- }
-
- /**
- * Test of subscribe method, of class JSONRestTransport.
- */
- @Test
- public void testSubscribe() throws Exception {
- log.info("Testing subscribeToTask");
-
- this.server.when(
- request()
- .withMethod("POST")
- .withPath("/tasks/task-1234:subscribe")
- )
- .respond(
- response()
- .withStatusCode(200)
- .withHeader("Content-Type", "text/event-stream")
- .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE)
- );
-
- RestTransport client = new RestTransport(CARD);
- 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.subscribeToTask(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.id());
- assertEquals("context-1234", task.contextId());
- assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state());
- List