diff --git a/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java b/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java index b4847cd40..6cbcd949e 100644 --- a/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java +++ b/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java @@ -19,6 +19,55 @@ import io.a2a.spec.AgentInterface; +/** + * Utility for fetching agent cards from A2A agents. + * + *

Retrieves agent cards from the standard {@code /.well-known/agent-card.json} endpoint + * with support for tenant-specific paths and authentication headers. + * + *

Features

+ * + * + *

Usage Examples

+ *
{@code
+ * // Basic usage - fetch agent card
+ * A2ACardResolver resolver = new A2ACardResolver("http://localhost:9999");
+ * AgentCard card = resolver.getAgentCard();
+ *
+ * // With tenant path
+ * A2ACardResolver resolver = new A2ACardResolver("http://localhost:9999", "my-tenant");
+ * AgentCard card = resolver.getAgentCard();
+ *
+ * // With custom HTTP client
+ * A2AHttpClient httpClient = A2AHttpClientFactory.create();
+ * A2ACardResolver resolver = new A2ACardResolver(httpClient, "http://localhost:9999", "my-tenant");
+ * AgentCard card = resolver.getAgentCard();
+ *
+ * // With authentication headers
+ * A2AHttpClient httpClient = A2AHttpClientFactory.create();
+ * Map authHeaders = Map.of("Authorization", "Bearer token");
+ * A2ACardResolver resolver = new A2ACardResolver(
+ *     httpClient,
+ *     "http://localhost:9999",
+ *     "my-tenant",
+ *     null,  // use default agent card path
+ *     authHeaders
+ * );
+ * AgentCard card = resolver.getAgentCard();
+ *
+ * // Fetch extended agent card (if available)
+ * AgentCard extendedCard = resolver.getExtendedAgentCard();
+ * }
+ * + * @see AgentCard + * @see A2AHttpClient + */ public class A2ACardResolver { private final A2AHttpClient httpClient; private final String url; @@ -27,7 +76,7 @@ public class A2ACardResolver { private static final String DEFAULT_AGENT_CARD_PATH = "/.well-known/agent-card.json"; /** - * Get the agent card for an A2A agent. An HTTP client will be auto-selected via {@link A2AHttpClientFactory}. + * Creates an agent card resolver. An HTTP client will be auto-selected via {@link A2AHttpClientFactory}. * * @param baseUrl the base URL for the agent whose agent card we want to retrieve, must not be null * @throws A2AClientError if the URL for the agent is invalid diff --git a/http-client/src/main/java/io/a2a/client/http/A2AHttpClient.java b/http-client/src/main/java/io/a2a/client/http/A2AHttpClient.java index 9e4f5f705..1529f3e14 100644 --- a/http-client/src/main/java/io/a2a/client/http/A2AHttpClient.java +++ b/http-client/src/main/java/io/a2a/client/http/A2AHttpClient.java @@ -5,43 +5,194 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +/** + * HTTP client interface for making HTTP requests to A2A agents. + * + *

Provides a fluent builder API for constructing and executing HTTP requests + * with support for GET, POST, and DELETE methods. Includes support for both + * synchronous requests and asynchronous Server-Sent Events (SSE) streaming. + * + *

Usage Example

+ *
{@code
+ * A2AHttpClient client = A2AHttpClientFactory.create();
+ *
+ * // Synchronous GET request
+ * A2AHttpResponse response = client.createGet()
+ *     .url("http://localhost:9999/api/endpoint")
+ *     .addHeader("Authorization", "Bearer token")
+ *     .get();
+ *
+ * // Synchronous POST request
+ * A2AHttpResponse response = client.createPost()
+ *     .url("http://localhost:9999/message:send")
+ *     .body("{\"message\": \"Hello\"}")
+ *     .post();
+ *
+ * // Asynchronous SSE streaming
+ * CompletableFuture future = client.createPost()
+ *     .url("http://localhost:9999/message:stream")
+ *     .body(jsonBody)
+ *     .postAsyncSSE(
+ *         message -> System.out.println("Event: " + message),
+ *         error -> System.err.println("Error: " + error),
+ *         () -> System.out.println("Stream complete")
+ *     );
+ * }
+ * + * @see A2AHttpClientFactory + * @see A2AHttpResponse + */ public interface A2AHttpClient { + /** HTTP Content-Type header name. */ String CONTENT_TYPE= "Content-Type"; + /** JSON content type value. */ String APPLICATION_JSON= "application/json"; + /** HTTP Accept header name. */ String ACCEPT = "Accept"; + /** SSE event stream content type. */ String EVENT_STREAM = "text/event-stream"; + /** + * Creates a builder for GET requests. + * + * @return a new GetBuilder instance + */ GetBuilder createGet(); + /** + * Creates a builder for POST requests. + * + * @return a new PostBuilder instance + */ PostBuilder createPost(); + /** + * Creates a builder for DELETE requests. + * + * @return a new DeleteBuilder instance + */ DeleteBuilder createDelete(); + /** + * Base builder interface for HTTP requests. + * + * @param the concrete builder type for method chaining + */ interface Builder> { + /** + * Sets the target URL for the request. + * + * @param s the URL string + * @return this builder for chaining + */ T url(String s); + + /** + * Adds multiple HTTP headers to the request. + * + * @param headers map of header names to values + * @return this builder for chaining + */ T addHeaders(Map headers); + + /** + * Adds a single HTTP header to the request. + * + * @param name the header name + * @param value the header value + * @return this builder for chaining + */ T addHeader(String name, String value); } + /** + * Builder for HTTP GET requests. + * + *

Supports both synchronous requests and asynchronous Server-Sent Events (SSE) streaming. + */ interface GetBuilder extends Builder { + /** + * Executes a synchronous GET request. + * + * @return the HTTP response + * @throws IOException if an I/O error occurs + * @throws InterruptedException if the operation is interrupted + */ A2AHttpResponse get() throws IOException, InterruptedException; + + /** + * Executes an asynchronous GET request expecting Server-Sent Events (SSE). + * + *

The request will stream SSE messages asynchronously, invoking the provided + * consumers for each event, error, or completion. + * + * @param messageConsumer callback for each SSE message received + * @param errorConsumer callback for errors during streaming + * @param completeRunnable callback when the stream completes normally + * @return a CompletableFuture that completes when streaming ends + * @throws IOException if an I/O error occurs + * @throws InterruptedException if the operation is interrupted + */ CompletableFuture getAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException; } + /** + * Builder for HTTP POST requests. + * + *

Supports both synchronous requests and asynchronous Server-Sent Events (SSE) streaming. + */ interface PostBuilder extends Builder { + /** + * Sets the request body content. + * + * @param body the request body string (typically JSON) + * @return this builder for chaining + */ PostBuilder body(String body); + + /** + * Executes a synchronous POST request. + * + * @return the HTTP response + * @throws IOException if an I/O error occurs + * @throws InterruptedException if the operation is interrupted + */ A2AHttpResponse post() throws IOException, InterruptedException; + + /** + * Executes an asynchronous POST request expecting Server-Sent Events (SSE). + * + *

The request will stream SSE messages asynchronously, invoking the provided + * consumers for each event, error, or completion. + * + * @param messageConsumer callback for each SSE message received + * @param errorConsumer callback for errors during streaming + * @param completeRunnable callback when the stream completes normally + * @return a CompletableFuture that completes when streaming ends + * @throws IOException if an I/O error occurs + * @throws InterruptedException if the operation is interrupted + */ CompletableFuture postAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException; } + /** + * Builder for HTTP DELETE requests. + */ interface DeleteBuilder extends Builder { + /** + * Executes a synchronous DELETE request. + * + * @return the HTTP response + * @throws IOException if an I/O error occurs + * @throws InterruptedException if the operation is interrupted + */ A2AHttpResponse delete() throws IOException, InterruptedException; } } diff --git a/http-client/src/main/java/io/a2a/client/http/A2AHttpResponse.java b/http-client/src/main/java/io/a2a/client/http/A2AHttpResponse.java index 171fceebd..837636bcc 100644 --- a/http-client/src/main/java/io/a2a/client/http/A2AHttpResponse.java +++ b/http-client/src/main/java/io/a2a/client/http/A2AHttpResponse.java @@ -1,9 +1,47 @@ package io.a2a.client.http; +/** + * HTTP response wrapper containing status code and response body. + * + *

Provides access to the HTTP status code, a success indicator, and the + * response body content. + * + *

Usage Example

+ *
{@code
+ * A2AHttpResponse response = client.createGet()
+ *     .url("http://localhost:9999/api/endpoint")
+ *     .get();
+ *
+ * if (response.success()) {
+ *     String body = response.body();
+ *     // Process successful response
+ * } else {
+ *     int status = response.status();
+ *     // Handle error based on status code
+ * }
+ * }
+ */ public interface A2AHttpResponse { + /** + * Returns the HTTP status code. + * + * @return the HTTP status code (e.g., 200, 404, 500) + */ int status(); + /** + * Indicates whether the request was successful. + * + *

Typically returns {@code true} for 2xx status codes. + * + * @return {@code true} if the request was successful, {@code false} otherwise + */ boolean success(); + /** + * Returns the response body content as a string. + * + * @return the response body, may be empty but not null + */ String body(); } diff --git a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java index d5bc68651..c04596360 100644 --- a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java +++ b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java @@ -25,10 +25,41 @@ import io.a2a.common.A2AErrorMessages; +/** + * Default HTTP client implementation using JDK 11+ {@link HttpClient}. + * + *

This is the fallback implementation used when no higher-priority + * {@link A2AHttpClientProvider} is available. It provides full support for: + *

    + *
  • HTTP/2 with automatic fallback to HTTP/1.1
  • + *
  • Synchronous GET, POST, and DELETE requests
  • + *
  • Asynchronous Server-Sent Events (SSE) streaming
  • + *
  • Automatic redirect following
  • + *
+ * + *

Provider Priority: 0 (lowest - used as fallback) + * + *

This implementation is registered via {@link JdkA2AHttpClientProvider} + * in the ServiceLoader system and is automatically used by {@link A2AHttpClientFactory} + * when no other provider is available. + * + * @see A2AHttpClient + * @see A2AHttpClientFactory + * @see JdkA2AHttpClientProvider + */ public class JdkA2AHttpClient implements A2AHttpClient { private final HttpClient httpClient; + /** + * Creates a new JDK-based HTTP client. + * + *

Configures the client with: + *

    + *
  • HTTP/2 preferred (with HTTP/1.1 fallback)
  • + *
  • Normal redirect following
  • + *
+ */ public JdkA2AHttpClient() { httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) diff --git a/http-client/src/main/java/io/a2a/client/http/package-info.java b/http-client/src/main/java/io/a2a/client/http/package-info.java index 525333cf1..667c28f5a 100644 --- a/http-client/src/main/java/io/a2a/client/http/package-info.java +++ b/http-client/src/main/java/io/a2a/client/http/package-info.java @@ -1,3 +1,58 @@ +/** + * HTTP client utilities for A2A protocol communication. + * + *

This package provides a pluggable HTTP client abstraction for making HTTP requests + * to A2A agents, including support for fetching agent cards, synchronous requests, and + * Server-Sent Events (SSE) streaming. + * + *

Core Components

+ *
    + *
  • {@link io.a2a.client.http.A2AHttpClient} - Main HTTP client interface with builder pattern
  • + *
  • {@link io.a2a.client.http.A2AHttpClientFactory} - Factory for creating client instances via ServiceLoader
  • + *
  • {@link io.a2a.client.http.A2ACardResolver} - Utility for fetching agent cards from standard endpoints
  • + *
  • {@link io.a2a.client.http.A2AHttpResponse} - Response wrapper with status and body
  • + *
+ * + *

Provider System

+ *

The module uses a ServiceLoader-based provider system allowing different HTTP client + * implementations to be plugged in: + *

    + *
  • {@link io.a2a.client.http.JdkA2AHttpClient} - Default implementation using JDK 11+ HttpClient (priority 0)
  • + *
  • VertxA2AHttpClient - Vertx-based implementation when available (priority 100)
  • + *
+ * + *

Usage Example

+ *
{@code
+ * // Fetch an agent card
+ * A2ACardResolver resolver = new A2ACardResolver("http://localhost:9999");
+ * AgentCard card = resolver.getAgentCard();
+ *
+ * // Make HTTP requests
+ * A2AHttpClient client = A2AHttpClientFactory.create();
+ * A2AHttpResponse response = client.createGet()
+ *     .url("http://localhost:9999/api/endpoint")
+ *     .addHeader("Authorization", "Bearer token")
+ *     .get();
+ *
+ * // Server-Sent Events (SSE) streaming
+ * client.createPost()
+ *     .url("http://localhost:9999/message:stream")
+ *     .body(jsonBody)
+ *     .postAsyncSSE(
+ *         message -> System.out.println("Received: " + message),
+ *         error -> System.err.println("Error: " + error),
+ *         () -> System.out.println("Stream complete")
+ *     );
+ * }
+ * + *

Agent Card Resolution

+ *

Agent cards are fetched from the standard {@code /.well-known/agent-card.json} endpoint + * by default, with support for tenant-specific paths and custom authentication headers. + * + * @see io.a2a.client.http.A2AHttpClient + * @see io.a2a.client.http.A2ACardResolver + * @see io.a2a.spec.AgentCard + */ @NullMarked package io.a2a.client.http;