diff --git a/client/base/src/main/java/io/a2a/A2A.java b/client/base/src/main/java/io/a2a/A2A.java index 086db1c6a..02f212656 100644 --- a/client/base/src/main/java/io/a2a/A2A.java +++ b/client/base/src/main/java/io/a2a/A2A.java @@ -16,47 +16,121 @@ /** - * Constants and utility methods related to the A2A protocol. + * Utility class providing convenience methods for working with the A2A Protocol. + *
+ * This class offers static helper methods for common A2A operations: + *
+ * These utilities simplify client code by providing concise alternatives to the builder + * APIs for routine operations. + *
+ * Example usage: + *
{@code
+ * // Get agent card
+ * AgentCard card = A2A.getAgentCard("http://localhost:9999");
+ *
+ * // Create and send a user message
+ * Message userMsg = A2A.toUserMessage("What's the weather today?");
+ * client.sendMessage(userMsg);
+ *
+ * // Create a message with context and task IDs
+ * Message contextMsg = A2A.createUserTextMessage(
+ * "Continue the conversation",
+ * "session-123", // contextId
+ * "task-456" // taskId
+ * );
+ * client.sendMessage(contextMsg);
+ * }
+ *
+ * @see Message
+ * @see AgentCard
+ * @see io.a2a.client.Client
*/
public class A2A {
/**
- * Convert the given text to a user message.
+ * Create a simple user message from text.
+ * + * This is the most common way to create messages when sending requests to agents. + * The message will have: + *
+ * Example: + *
{@code
+ * Message msg = A2A.toUserMessage("Tell me a joke");
+ * client.sendMessage(msg);
+ * }
*
- * @param text the message text
- * @return the user message
+ * @param text the message text (required)
+ * @return a user message with the specified text
+ * @see #toUserMessage(String, String)
+ * @see #createUserTextMessage(String, String, String)
*/
public static Message toUserMessage(String text) {
return toMessage(text, Message.Role.USER, null);
}
/**
- * Convert the given text to a user message.
+ * Create a user message from text with a specific message ID.
+ * + * Use this when you need to control the message ID for tracking or correlation purposes. + *
+ * Example: + *
{@code
+ * String messageId = UUID.randomUUID().toString();
+ * Message msg = A2A.toUserMessage("Process this request", messageId);
+ * // Store messageId for later correlation
+ * client.sendMessage(msg);
+ * }
*
- * @param text the message text
+ * @param text the message text (required)
* @param messageId the message ID to use
- * @return the user message
+ * @return a user message with the specified text and ID
+ * @see #toUserMessage(String)
*/
public static Message toUserMessage(String text, String messageId) {
return toMessage(text, Message.Role.USER, messageId);
}
/**
- * Convert the given text to an agent message.
+ * Create a simple agent message from text.
+ * + * This is typically used in testing or when constructing agent responses programmatically. + * Most client applications receive agent messages via {@link io.a2a.client.MessageEvent} + * rather than creating them manually. + *
+ * Example: + *
{@code
+ * // Testing scenario
+ * Message agentResponse = A2A.toAgentMessage("Here's the answer: 42");
+ * }
*
- * @param text the message text
- * @return the agent message
+ * @param text the message text (required)
+ * @return an agent message with the specified text
+ * @see #toAgentMessage(String, String)
*/
public static Message toAgentMessage(String text) {
return toMessage(text, Message.Role.AGENT, null);
}
/**
- * Convert the given text to an agent message.
+ * Create an agent message from text with a specific message ID.
+ * + * Example: + *
{@code
+ * Message agentResponse = A2A.toAgentMessage("Processing complete", "msg-789");
+ * }
*
- * @param text the message text
+ * @param text the message text (required)
* @param messageId the message ID to use
- * @return the agent message
+ * @return an agent message with the specified text and ID
*/
public static Message toAgentMessage(String text, String messageId) {
return toMessage(text, Message.Role.AGENT, messageId);
@@ -64,11 +138,46 @@ public static Message toAgentMessage(String text, String messageId) {
/**
* Create a user message with text content and optional context and task IDs.
+ * + * This method is useful when continuing a conversation or working with a specific task: + *
+ * Example - continuing a conversation: + *
{@code
+ * // First message creates context
+ * Message msg1 = A2A.toUserMessage("What's your name?");
+ * client.sendMessage(msg1);
+ * String contextId = ...; // Get from response
+ *
+ * // Follow-up message uses contextId
+ * Message msg2 = A2A.createUserTextMessage(
+ * "What else can you do?",
+ * contextId,
+ * null // no specific task
+ * );
+ * client.sendMessage(msg2);
+ * }
+ * + * Example - adding to an existing task: + *
{@code
+ * Message msg = A2A.createUserTextMessage(
+ * "Add this information too",
+ * "session-123",
+ * "task-456" // Continue working on this task
+ * );
+ * client.sendMessage(msg);
+ * }
*
* @param text the message text (required)
* @param contextId the context ID to use (optional)
* @param taskId the task ID to use (optional)
- * @return the user message
+ * @return a user message with the specified text, context, and task IDs
+ * @see #createAgentTextMessage(String, String, String)
+ * @see Message#contextId()
+ * @see Message#taskId()
*/
public static Message createUserTextMessage(String text, String contextId, String taskId) {
return toMessage(text, Message.Role.USER, null, contextId, taskId);
@@ -76,11 +185,14 @@ public static Message createUserTextMessage(String text, String contextId, Strin
/**
* Create an agent message with text content and optional context and task IDs.
+ * + * This is typically used in testing or when constructing agent responses programmatically. * * @param text the message text (required) * @param contextId the context ID to use (optional) * @param taskId the task ID to use (optional) - * @return the agent message + * @return an agent message with the specified text, context, and task IDs + * @see #createUserTextMessage(String, String, String) */ public static Message createAgentTextMessage(String text, String contextId, String taskId) { return toMessage(text, Message.Role.AGENT, null, contextId, taskId); @@ -88,11 +200,26 @@ public static Message createAgentTextMessage(String text, String contextId, Stri /** * Create an agent message with custom parts and optional context and task IDs. + *
+ * This method allows creating messages with multiple parts (text, images, files, etc.) + * instead of just simple text. Useful for complex agent responses or testing. + *
+ * Example - message with text and image: + *
{@code
+ * List> parts = List.of(
+ * new TextPart("Here's a chart of the data:"),
+ * new ImagePart("https://example.com/chart.png", "Chart showing sales data")
+ * );
+ * Message msg = A2A.createAgentPartsMessage(parts, "session-123", "task-456");
+ * }
*
- * @param parts the message parts (required)
+ * @param parts the message parts (required, must not be empty)
* @param contextId the context ID to use (optional)
* @param taskId the task ID to use (optional)
- * @return the agent message
+ * @return an agent message with the specified parts, context, and task IDs
+ * @throws IllegalArgumentException if parts is null or empty
+ * @see io.a2a.spec.Part
+ * @see io.a2a.spec.TextPart
*/
public static Message createAgentPartsMessage(List
+ * 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
+ * Example:
+ *
+ * Use this variant when you need to customize HTTP behavior (timeouts, SSL configuration,
+ * connection pooling, etc.).
+ *
+ * Example:
+ *
+ * Use this variant when:
+ *
+ * Example with authentication:
+ *
+ * Example with custom path:
+ *
+ * This is the most flexible variant, allowing customization of:
+ *
+ * Example:
+ *
+ * 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:
+ *
+ * Resource management: Client implements {@link AutoCloseable} and should be used with
+ * try-with-resources to ensure proper cleanup:
+ *
+ * Manual resource management: If not using try-with-resources, call {@link #close()}
+ * explicitly when done:
+ *
+ * Event consumption model: Responses from the agent are delivered as {@link ClientEvent}
+ * instances to the registered consumers:
+ *
+ * Streaming vs blocking: The client supports two communication modes:
+ *
+ * Task lifecycle example:
+ *
+ * Push notifications: Configure webhooks to receive task updates:
+ *
+ * Resubscription after disconnection:
+ *
+ * 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:
+ *
+ * This is the primary entry point for creating clients. The builder provides a fluent
+ * API for configuring transports, event consumers, and client behavior.
+ *
+ * Example:
+ *
+ * This is the primary method for communicating with an agent. The behavior depends on
+ * whether streaming is enabled:
+ *
+ * Simple example:
+ *
+ * With push notifications:
+ *
+ * With metadata:
+ *
+ * This method queries the agent for the current state of a task. It's useful for:
+ *
+ * Example:
+ *
+ * This method retrieves multiple tasks based on filter criteria. Useful for:
+ *
+ * Example:
+ *
+ * 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:
+ *
+ * Example:
+ *
+ * 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:
+ *
+ * Example:
+ *
+ * Example:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * Example:
+ *
+ * This method fetches the latest agent card from the agent. 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:
+ *
+ * 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:
+ *
+ * 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: 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:
+ *
+ * Multiple transports: You can configure multiple transports for fallback:
+ *
+ * Error handling: For streaming scenarios, configure an error handler to process exceptions:
+ *
+ * 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
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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
+ * 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:
+ *
+ * The configuration controls how the client communicates with the agent:
+ *
+ * Example:
+ *
+ * This method performs transport negotiation between the client's configured transports
+ * and the agent's {@link AgentCard#supportedInterfaces()}. The selection algorithm:
+ *
+ * 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();
diff --git a/client/base/src/main/java/io/a2a/client/ClientEvent.java b/client/base/src/main/java/io/a2a/client/ClientEvent.java
index dcaae9495..2275a7f2e 100644
--- a/client/base/src/main/java/io/a2a/client/ClientEvent.java
+++ b/client/base/src/main/java/io/a2a/client/ClientEvent.java
@@ -1,4 +1,75 @@
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:
+ *
+ * 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:
+ *
+ * Streaming task execution:
+ *
+ * Typical usage pattern:
+ *
+ * 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/MessageEvent.java b/client/base/src/main/java/io/a2a/client/MessageEvent.java
index 9a0370995..1db7b39fd 100644
--- a/client/base/src/main/java/io/a2a/client/MessageEvent.java
+++ b/client/base/src/main/java/io/a2a/client/MessageEvent.java
@@ -3,21 +3,74 @@
import io.a2a.spec.Message;
/**
- * A message event received by a client.
+ * 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:
+ *
+ * Example usage:
+ *
+ * Message structure: The contained {@link Message} includes:
+ *
+ * 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;
/**
- * A message event.
+ * Create a message event.
*
- * @param message the message received
+ * @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;
}
diff --git a/client/base/src/main/java/io/a2a/client/TaskEvent.java b/client/base/src/main/java/io/a2a/client/TaskEvent.java
index a18392841..4da4ef04f 100644
--- a/client/base/src/main/java/io/a2a/client/TaskEvent.java
+++ b/client/base/src/main/java/io/a2a/client/TaskEvent.java
@@ -5,22 +5,96 @@
import io.a2a.spec.Task;
/**
- * A task event received by a client.
+ * 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:
+ *
+ * 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:
+ *
+ * Task contents: The contained {@link Task} includes:
+ *
+ * Terminal states: When a task reaches a final state, no further updates will be
+ * received for that 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:
+ *
+ * Streaming task lifecycle example:
+ *
+ * Example usage - tracking progress:
+ *
+ * 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:
+ *
+ * Artifact updates: When {@link io.a2a.spec.TaskArtifactUpdateEvent} is received,
+ * the artifact may be:
+ *
+ * Status transitions: Common task state transitions:
+ *
+ * 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 task
- * @param updateEvent the update event received for the current task
+ * @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);
@@ -26,10 +123,30 @@ public TaskUpdateEvent(Task task, UpdateEvent updateEvent) {
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:
+ *
+ * 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 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:
+ *
+ * Transport preference: Controls which transport protocol is selected when multiple
+ * options are available:
+ *
+ * Output modes: Specify which content types the client can handle:
+ *
+ * Conversation history: Request previous messages as context:
+ *
+ * Push notifications: Configure default webhook for all task updates:
+ *
+ * Custom metadata: Attach metadata to all requests:
+ *
+ * Complete example:
+ *
+ * Default values:
+ *
+ * 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 {
@@ -31,38 +156,123 @@ private ClientConfig(Builder builder) {
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
+ * 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
+ * All configuration options have sensible defaults and are optional. Use this builder
+ * to override specific settings as needed.
+ *
+ * Example:
+ *
+ * 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
+ * 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
+ * Any unset options will use their default values:
+ *
+ * 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
+ * Basic usage with ManagedChannel:
+ *
+ * Production configuration with TLS and timeouts:
+ *
+ * With load balancing and connection pooling:
+ *
+ * With interceptors:
+ *
+ * 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
+ * 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
+ * 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:
+ *
+ * Basic development setup (insecure):
+ *
+ * Production setup with TLS and connection pooling:
+ *
+ * With custom SSL certificates:
+ *
+ * With load balancing and health checks:
+ *
+ * With A2A interceptors:
+ *
+ * Direct usage in ClientBuilder:
+ *
+ * Channel Lifecycle Management:
+ *
+ * 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:
+ *
+ * Example:
+ *
+ * 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) {
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
index 0705faf20..909ff079e 100644
--- 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
@@ -4,19 +4,81 @@
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:
+ *
+ * Custom HTTP client:
+ *
+ * With interceptors:
+ *
+ * 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;
}
-}
\ No newline at end of file
+}
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
index 24ced1242..9cd5fae5a 100644
--- 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
@@ -5,15 +5,107 @@
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:
+ *
+ * Basic usage:
+ *
+ * Custom HTTP client:
+ *
+ * With interceptors:
+ *
+ * Direct usage in ClientBuilder:
+ *
+ * Custom HTTP clients can provide:
+ *
+ * If not specified, the default {@link JdkA2AHttpClient} is used.
+ *
+ * Example:
+ *
+ * If no HTTP client was configured, the default {@link JdkA2AHttpClient} is used.
+ * Any configured interceptors are transferred to the configuration.
+ *
+ * @return the configured JSON-RPC transport configuration
+ */
@Override
public JSONRPCTransportConfig build() {
// No HTTP client provided, fallback to the default one (JDK-based implementation)
@@ -25,4 +117,4 @@ public JSONRPCTransportConfig build() {
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/RestTransportConfig.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfig.java
index d097b010f..241541a32 100644
--- 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
@@ -4,18 +4,80 @@
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:
+ *
+ * Custom HTTP client:
+ *
+ * With interceptors:
+ *
+ * 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;
}
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
index 68150f189..855de0ca6 100644
--- 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
@@ -5,15 +5,107 @@
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:
+ *
+ * Basic usage:
+ *
+ * Custom HTTP client:
+ *
+ * With interceptors:
+ *
+ * Direct usage in ClientBuilder:
+ *
+ * Custom HTTP clients can provide:
+ *
+ * If not specified, the default {@link JdkA2AHttpClient} is used.
+ *
+ * Example:
+ *
+ * If no HTTP client was configured, the default {@link JdkA2AHttpClient} is used.
+ * Any configured interceptors are transferred to the configuration.
+ *
+ * @return the configured REST transport configuration
+ */
@Override
public RestTransportConfig build() {
// No HTTP client provided, fallback to the default one (JDK-based implementation)
diff --git a/client/transport/spi/src/main/java/io/a2a/client/transport/spi/ClientTransportConfig.java b/client/transport/spi/src/main/java/io/a2a/client/transport/spi/ClientTransportConfig.java
index c04b52882..9c05fac59 100644
--- a/client/transport/spi/src/main/java/io/a2a/client/transport/spi/ClientTransportConfig.java
+++ b/client/transport/spi/src/main/java/io/a2a/client/transport/spi/ClientTransportConfig.java
@@ -6,17 +6,61 @@
import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
/**
- * Configuration for an A2A client transport.
+ * Base configuration class for A2A client transport protocols.
+ *
+ * This abstract class provides common configuration functionality for all transport implementations
+ * (JSON-RPC, gRPC, REST). It manages request/response interceptors that can be used for logging,
+ * metrics, authentication, and other cross-cutting concerns.
+ *
+ * Interceptors: Transport configurations support adding interceptors that can inspect and
+ * modify requests/responses. Interceptors are invoked in the order they were added:
+ *
+ * Concrete implementations typically extend this class to add transport-specific configuration
+ * such as HTTP clients, gRPC channels, or connection pools.
+ *
+ * Thread safety: Configuration instances should be treated as immutable after construction.
+ * The interceptor list is copied defensively to prevent external modification.
+ *
+ * @param
+ * Interceptors are invoked in the order they appear in the list, allowing for
+ * controlled processing chains (e.g., authentication before logging).
+ *
+ * The provided list is copied to prevent external modifications from affecting
+ * this configuration.
+ *
+ * @param interceptors the list of interceptors to use (will be copied)
+ * @see ClientTransportConfigBuilder#addInterceptor(ClientCallInterceptor)
+ */
public void setInterceptors(List
+ * Returns an unmodifiable view of the interceptor list. Attempting to modify
+ * the returned list will throw {@link UnsupportedOperationException}.
+ *
+ * @return an unmodifiable list of configured interceptors (never null, but may be empty)
+ */
public List
+ * This abstract builder provides common functionality for building transport configurations,
+ * particularly interceptor management. Concrete builders extend this class to add
+ * transport-specific configuration options.
+ *
+ * Self-typed builder pattern: This class uses the "self-typed" or "curiously recurring
+ * template pattern" to enable method chaining in subclasses while maintaining type safety:
+ *
+ * Interceptor ordering: Interceptors are invoked in the order they were added:
+ *
+ * Interceptors can be used for cross-cutting concerns such as:
+ *
+ * Interceptors are invoked in the order they were added. If {@code interceptor} is
+ * {@code null}, this method is a no-op (for convenience in conditional addition).
+ *
+ * Example:
+ *
+ * Concrete implementations should:
+ * {@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 A2AClientError If an HTTP error occurs fetching the card
- * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ * @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(new JdkA2AHttpClient(), agentUrl);
}
/**
- * Get the agent card for an A2A agent.
+ * Retrieve the agent card using a custom HTTP client.
+ * {@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 A2AClientError If an HTTP error occurs fetching the card
- * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ * @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);
}
/**
- * Get the agent card for an A2A agent.
+ * Retrieve the agent card with custom path and authentication.
+ *
+ *
+ * {@code
+ * Map
+ * {@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 A2AClientError If an HTTP error occurs fetching the card
- * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ * @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
+ *
+ * {@code
+ * A2AHttpClient customClient = new CustomHttpClient();
+ * Map
*
* @param httpClient the http client to use
* @param agentUrl the base URL for the agent whose agent card we want to retrieve
@@ -178,8 +389,8 @@ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, M
* agent URL, defaults to ".well-known/agent-card.json"
* @param authHeaders the HTTP authentication headers to use
* @return the agent card
- * @throws A2AClientError If an HTTP error occurs fetching the card
- * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ * @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
+ *
+ * {@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"));
+ * }
+ * }
+ * {@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
+ * }
+ * }
+ *
+ *
+ *
+ *
+ * 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.
+ * {@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());
+ * }
+ * }
+ * });
+ * }
+ * {@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
+ * );
+ * }
+ * {@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.resubscribe(
+ * new TaskIdParams(taskId),
+ * List.of((event, card) -> {
+ * // Process events from where we left off
+ * }),
+ * null, // error handler
+ * null // context
+ * );
+ * }
+ * {@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{@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);
}
@@ -64,6 +238,51 @@ public void sendMessage(@NonNull Message request,
sendMessage(messageSendParams, consumers, streamingErrorHandler, context);
}
+ /**
+ * Send a message to the agent.
+ *
+ *
+ * Streaming mode is active when both {@link ClientConfig#isStreaming()} AND
+ * {@link io.a2a.spec.AgentCapabilities#streaming()} are {@code true}.
+ * {@code
+ * Message userMessage = A2A.toUserMessage("What's the weather?");
+ * client.sendMessage(userMessage, null, null, null);
+ * // Events delivered to consumers registered during client construction
+ * }
+ * {@code
+ * PushNotificationConfig pushConfig = new PushNotificationConfig(
+ * "https://my-app.com/webhook",
+ * Map.of("Authorization", "Bearer token")
+ * );
+ * client.sendMessage(userMessage, pushConfig, null, null);
+ * }
+ * {@code
+ * Map
+ *
+ * @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,
@@ -80,45 +299,277 @@ public void sendMessage(@NonNull Message request,
sendMessage(messageSendParams, consumers, streamingErrorHandler, context);
}
+ /**
+ * Retrieve a specific task by ID.
+ *
+ *
+ * {@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.
+ *
+ *
+ * {@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.
+ *
+ *
+ * {@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.
+ * {@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.setTaskPushNotificationConfiguration(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 setTaskPushNotificationConfiguration(
TaskPushNotificationConfig request, @Nullable ClientCallContext context) throws A2AClientException {
return clientTransport.setTaskPushNotificationConfiguration(request, context);
}
+ /**
+ * Retrieve the push notification configuration for a task.
+ * {@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.
+ * {@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.
+ * {@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);
}
+ /**
+ * Resubscribe to an existing task to receive remaining events.
+ *
+ *
+ * {@code
+ * // Original request (client1)
+ * client1.sendMessage(A2A.toUserMessage("Analyze this dataset"));
+ * String taskId = ...; // Save task ID from TaskEvent
+ * // ... client1 disconnects ...
+ *
+ * // Later, reconnect (client2)
+ * client2.resubscribe(
+ * 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("Resubscribe error: " + throwable),
+ * null
+ * );
+ * }
+ *
+ * @param request the task ID to resubscribe 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 resubscription is not supported or if the task cannot be found
+ */
@Override
public void resubscribe(@NonNull TaskIdParams request,
@NonNull List{@code
+ * AgentCard updatedCard = client.getAgentCard(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 current agent card
+ * @throws A2AClientException if the agent card cannot be retrieved
+ * @see AgentCard
+ */
@Override
public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
agentCard = clientTransport.getAgentCard(context);
return agentCard;
}
+ /**
+ * Close this client and release all associated resources.
+ * {@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();
diff --git a/client/base/src/main/java/io/a2a/client/ClientBuilder.java b/client/base/src/main/java/io/a2a/client/ClientBuilder.java
index b05b44ca1..c8d2ab6be 100644
--- a/client/base/src/main/java/io/a2a/client/ClientBuilder.java
+++ b/client/base/src/main/java/io/a2a/client/ClientBuilder.java
@@ -21,6 +21,77 @@
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
+/**
+ * Builder for creating instances of {@link Client} to communicate with A2A agents.
+ *
+ *
+ * {@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!"));
+ * }
+ * {@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();
+ * }
+ * {@code
+ * Client client = Client.builder(card)
+ * .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder())
+ * .streamingErrorHandler(throwable -> {
+ * System.err.println("Stream error: " + throwable.getMessage());
+ * })
+ * .build();
+ * }
+ * {@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 {@code
+ * JSONRPCTransportConfig config = new JSONRPCTransportConfig(myHttpClient);
+ * builder.withTransport(JSONRPCTransport.class, config);
+ * }
+ *
+ * @param clazz the transport class to configure
+ * @param config the transport configuration
+ * @param {@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{@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
+ *
+ * {@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.
+ *
+ *
+ *
+ *
+ *
+ * User → Agent
+ * Agent → MessageEvent (contains agent's text response)
+ *
+ *
+ * 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)
+ *
+ * {@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());
+ * }
+ * }
+ * }
+ * });
+ * }
+ *
+ *
+ * {@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()));
+ * }
+ * });
+ * }
+ *
+ *
+ *
+ *
+ * {@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);
+ * }
+ * }
+ * }
+ * });
+ * }
+ *
+ *
+ *
+ *
+ *
+ * @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;
/**
- * A client task event.
+ * Create a task event.
*
- * @param task the task received
+ * @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
index c45650822..e9efe2404 100644
--- a/client/base/src/main/java/io/a2a/client/TaskUpdateEvent.java
+++ b/client/base/src/main/java/io/a2a/client/TaskUpdateEvent.java
@@ -6,7 +6,99 @@
import io.a2a.spec.UpdateEvent;
/**
- * A task update event received by a client.
+ * A client event containing an incremental update to a task.
+ *
+ *
+ * {@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)
+ * }
+ * {@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
+ * }
+ * }
+ * });
+ * }
+ * {@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]
+ * }
+ *
+ *
+ * The {@link #getTask()} always reflects the current complete artifact state.
+ *
+ * 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 {
@@ -14,10 +106,15 @@ public final class TaskUpdateEvent implements ClientEvent {
private final UpdateEvent updateEvent;
/**
- * A task update event.
+ * Create a task update event.
+ *
+ *
+ *
+ * @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
index 823548c23..d9ffd7e6e 100644
--- a/client/base/src/main/java/io/a2a/client/config/ClientConfig.java
+++ b/client/base/src/main/java/io/a2a/client/config/ClientConfig.java
@@ -9,7 +9,132 @@
import org.jspecify.annotations.Nullable;
/**
- * Configuration for the A2A client factory.
+ * Configuration for controlling A2A client behavior and communication preferences.
+ *
+ *
+ * {@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.
+ * {@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
+ * }
+ * {@code
+ * ClientConfig config = new ClientConfig.Builder()
+ * .setAcceptedOutputModes(List.of("text", "image", "audio"))
+ * .build();
+ * // Agent will only return text, image, or audio content
+ * }
+ * {@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.
+ * {@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
+ * }
+ * {@code
+ * Map
+ * {@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();
+ * }
+ *
+ *
+ * {@code
+ * ClientConfig config = new ClientConfig.Builder()
+ * .setStreaming(true)
+ * .setHistoryLength(10)
+ * .build();
+ * }
+ */
public static class Builder {
private @Nullable Boolean streaming;
private @Nullable Boolean polling;
@@ -72,41 +282,127 @@ public static class Builder {
private @Nullable Integer historyLength;
private Map
+ *
+ *
+ * @return the configured ClientConfig instance
+ */
public ClientConfig build() {
return new ClientConfig(this);
}
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
index a1bc3373c..605f7abb0 100644
--- 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
@@ -6,15 +6,106 @@
import io.a2a.util.Assert;
import io.grpc.Channel;
+/**
+ * Configuration for the gRPC transport protocol.
+ * {@code
+ * // Simple insecure channel for development
+ * Function
+ * {@code
+ * Function
+ * {@code
+ * Function
+ * {@code
+ * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
+ * .channelFactory(channelFactory)
+ * .addInterceptor(new LoggingInterceptor())
+ * .addInterceptor(new AuthInterceptor(apiKey))
+ * .build();
+ * }
+ *
+ *
+ * {@code
+ * // Simple channel for local development
+ * Function
+ * {@code
+ * Function
+ * {@code
+ * SslContext sslContext = GrpcSslContexts.forClient()
+ * .trustManager(new File("ca.crt"))
+ * .keyManager(new File("client.crt"), new File("client.key"))
+ * .build();
+ *
+ * Function
+ * {@code
+ * Function
+ * {@code
+ * GrpcTransportConfig config = new GrpcTransportConfigBuilder()
+ * .channelFactory(channelFactory)
+ * .addInterceptor(new LoggingInterceptor())
+ * .addInterceptor(new MetricsInterceptor())
+ * .addInterceptor(new AuthenticationInterceptor(apiKey))
+ * .build();
+ * }
+ * {@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();
+ * }
+ * {@code
+ * // Store channels for cleanup
+ * Map
+ *
+ * @see GrpcTransportConfig
+ * @see GrpcTransport
+ * @see io.a2a.client.transport.spi.ClientTransportConfigBuilder
+ * @see io.grpc.ManagedChannelBuilder
+ * @see io.grpc.Channel
+ */
public class GrpcTransportConfigBuilder extends ClientTransportConfigBuilder
+ *
+ * {@code
+ * Function
+ *
+ * @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{@code
+ * // Use default HTTP client
+ * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
+ * .build();
+ *
+ * Client client = Client.builder(agentCard)
+ * .withTransport(JSONRPCTransport.class, config)
+ * .build();
+ * }
+ * {@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();
+ * }
+ * {@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
+ *
+ * {@code
+ * // Minimal configuration (uses all defaults)
+ * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
+ * .build();
+ *
+ * Client client = Client.builder(agentCard)
+ * .withTransport(JSONRPCTransport.class, config)
+ * .build();
+ * }
+ * {@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();
+ * }
+ * {@code
+ * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
+ * .addInterceptor(new LoggingInterceptor())
+ * .addInterceptor(new MetricsInterceptor())
+ * .addInterceptor(new RetryInterceptor(3))
+ * .build();
+ * }
+ * {@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
+ *
+ * {@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.
+ * {@code
+ * // Use default HTTP client
+ * RestTransportConfig config = new RestTransportConfigBuilder()
+ * .build();
+ *
+ * Client client = Client.builder(agentCard)
+ * .withTransport(RestTransport.class, config)
+ * .build();
+ * }
+ * {@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();
+ * }
+ * {@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
+ *
+ * {@code
+ * // Minimal configuration (uses all defaults)
+ * RestTransportConfig config = new RestTransportConfigBuilder()
+ * .build();
+ *
+ * Client client = Client.builder(agentCard)
+ * .withTransport(RestTransport.class, config)
+ * .build();
+ * }
+ * {@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();
+ * }
+ * {@code
+ * RestTransportConfig config = new RestTransportConfigBuilder()
+ * .addInterceptor(new LoggingInterceptor())
+ * .addInterceptor(new MetricsInterceptor())
+ * .addInterceptor(new RetryInterceptor(3))
+ * .build();
+ * }
+ * {@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
+ *
+ * {@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.
+ * {@code
+ * // Example: Add logging and authentication interceptors
+ * config.setInterceptors(List.of(
+ * new LoggingInterceptor(),
+ * new AuthenticationInterceptor("Bearer token")
+ * ));
+ * }
+ * {@code
+ * JSONRPCTransportConfig config = new JSONRPCTransportConfigBuilder()
+ * .addInterceptor(loggingInterceptor) // Returns JSONRPCTransportConfigBuilder
+ * .httpClient(myHttpClient) // Returns JSONRPCTransportConfigBuilder
+ * .build(); // Returns JSONRPCTransportConfig
+ * }
+ * {@code
+ * builder
+ * .addInterceptor(authInterceptor) // Runs first
+ * .addInterceptor(loggingInterceptor) // Runs second
+ * .addInterceptor(metricsInterceptor);// Runs third
+ * }
+ *
+ * @param
+ *
+ * {@code
+ * builder
+ * .addInterceptor(new LoggingInterceptor())
+ * .addInterceptor(authToken != null ? new AuthInterceptor(authToken) : null)
+ * .addInterceptor(new MetricsInterceptor());
+ * }
+ *
+ * @param interceptor the interceptor to add (null values are ignored)
+ * @return this builder for method chaining
+ * @see io.a2a.client.transport.spi.interceptors.ClientCallInterceptor
+ */
public B addInterceptor(ClientCallInterceptor interceptor) {
if (interceptor != null) {
this.interceptors.add(interceptor);
@@ -18,5 +74,19 @@ public B addInterceptor(ClientCallInterceptor interceptor) {
return (B) this;
}
+ /**
+ * Build the transport configuration with all configured options.
+ *
+ *
+ *
+ * @return the configured transport configuration instance
+ * @throws IllegalStateException if required configuration is missing
+ */
public abstract T build();
}