Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ Different transport protocols can be configured with specific settings using spe

##### JSON-RPC Transport Configuration

For the JSON-RPC transport, to use the default `JdkA2AHttpClient`, provide a `JSONRPCTransportConfig` created with its default constructor.
For the JSON-RPC transport, to use the default HTTP client (resolved automatically by `A2AHttpClientFactory`), provide a `JSONRPCTransportConfig` created with its default constructor.

To use a custom HTTP client implementation, simply create a `JSONRPCTransportConfig` as follows:

Expand Down Expand Up @@ -441,7 +441,7 @@ Client client = Client

##### HTTP+JSON/REST Transport Configuration

For the HTTP+JSON/REST transport, if you'd like to use the default `JdkA2AHttpClient`, provide a `RestTransportConfig` created with its default constructor.
For the HTTP+JSON/REST transport, to use the default HTTP client (resolved automatically by `A2AHttpClientFactory`), provide a `RestTransportConfig` created with its default constructor.

To use a custom HTTP client implementation, simply create a `RestTransportConfig` as follows:

Expand Down
7 changes: 3 additions & 4 deletions client/base/src/main/java/io/a2a/A2A.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import io.a2a.client.http.A2ACardResolver;
import io.a2a.client.http.A2AHttpClient;
import io.a2a.client.http.JdkA2AHttpClient;
import io.a2a.client.http.A2AHttpClientFactory;
import io.a2a.spec.A2AClientError;
import io.a2a.spec.A2AClientJSONError;
import io.a2a.spec.AgentCard;
Expand Down Expand Up @@ -139,7 +138,7 @@ private static Message toMessage(List<Part<?>> parts, Message.Role role, String
* @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
*/
public static AgentCard getAgentCard(String agentUrl) throws A2AClientError, A2AClientJSONError {
return getAgentCard(new JdkA2AHttpClient(), agentUrl);
return getAgentCard(A2AHttpClientFactory.create(), agentUrl);
}

/**
Expand Down Expand Up @@ -167,7 +166,7 @@ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl)
* @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
*/
public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, Map<String, String> authHeaders) throws A2AClientError, A2AClientJSONError {
return getAgentCard(new JdkA2AHttpClient(), agentUrl, relativeCardPath, authHeaders);
return getAgentCard(A2AHttpClientFactory.create(), agentUrl, relativeCardPath, authHeaders);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
import io.a2a.client.transport.spi.interceptors.PayloadAndHeaders;
import io.a2a.client.http.A2AHttpClient;
import io.a2a.client.http.A2AHttpClientFactory;
import io.a2a.client.http.A2AHttpResponse;
import io.a2a.client.http.JdkA2AHttpClient;
import io.a2a.client.transport.spi.ClientTransport;
import io.a2a.spec.A2AClientError;
import io.a2a.spec.A2AClientException;
Expand Down Expand Up @@ -84,7 +84,7 @@ public JSONRPCTransport(AgentCard agentCard) {

public JSONRPCTransport(A2AHttpClient httpClient, AgentCard agentCard,
String agentUrl, List<ClientCallInterceptor> interceptors) {
this.httpClient = httpClient == null ? new JdkA2AHttpClient() : httpClient;
this.httpClient = httpClient == null ? A2AHttpClientFactory.create() : httpClient;
this.agentCard = agentCard;
this.agentUrl = agentUrl;
this.interceptors = interceptors;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.a2a.client.transport.jsonrpc;

import io.a2a.client.http.A2AHttpClient;
import io.a2a.client.http.A2AHttpClientFactory;
import io.a2a.client.http.JdkA2AHttpClient;
import io.a2a.client.transport.spi.ClientTransportConfigBuilder;

Expand All @@ -18,7 +19,7 @@ public JSONRPCTransportConfigBuilder httpClient(A2AHttpClient httpClient) {
public JSONRPCTransportConfig build() {
// No HTTP client provided, fallback to the default one (JDK-based implementation)
if (httpClient == null) {
httpClient = new JdkA2AHttpClient();
httpClient = A2AHttpClientFactory.create();
}

JSONRPCTransportConfig config = new JSONRPCTransportConfig(httpClient);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.a2a.client.transport.jsonrpc;

import io.a2a.client.http.JdkA2AHttpClient;
import io.a2a.client.http.A2AHttpClientFactory;
import io.a2a.client.transport.spi.ClientTransportProvider;
import io.a2a.spec.A2AClientException;
import io.a2a.spec.AgentCard;
Expand All @@ -11,7 +11,7 @@ public class JSONRPCTransportProvider implements ClientTransportProvider<JSONRPC
@Override
public JSONRPCTransport create(JSONRPCTransportConfig clientTransportConfig, AgentCard agentCard, String agentUrl) throws A2AClientException {
if (clientTransportConfig == null) {
clientTransportConfig = new JSONRPCTransportConfig(new JdkA2AHttpClient());
clientTransportConfig = new JSONRPCTransportConfig(A2AHttpClientFactory.create());
}

return new JSONRPCTransport(clientTransportConfig.getHttpClient(), agentCard, agentUrl, clientTransportConfig.getInterceptors());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import com.google.protobuf.util.JsonFormat;
import io.a2a.client.http.A2ACardResolver;
import io.a2a.client.http.A2AHttpClient;
import io.a2a.client.http.A2AHttpClientFactory;
import io.a2a.client.http.A2AHttpResponse;
import io.a2a.client.http.JdkA2AHttpClient;
import io.a2a.client.transport.rest.sse.RestSSEEventListener;
import io.a2a.client.transport.spi.ClientTransport;
import io.a2a.client.transport.spi.interceptors.ClientCallContext;
Expand Down Expand Up @@ -63,7 +63,7 @@ public RestTransport(AgentCard agentCard) {

public RestTransport(@Nullable A2AHttpClient httpClient, AgentCard agentCard,
String agentUrl, @Nullable List<ClientCallInterceptor> interceptors) {
this.httpClient = httpClient == null ? new JdkA2AHttpClient() : httpClient;
this.httpClient = httpClient == null ? A2AHttpClientFactory.create() : httpClient;
this.agentCard = agentCard;
this.agentUrl = agentUrl.endsWith("/") ? agentUrl.substring(0, agentUrl.length() - 1) : agentUrl;
this.interceptors = interceptors;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.a2a.client.transport.rest;

import io.a2a.client.http.A2AHttpClient;
import io.a2a.client.http.A2AHttpClientFactory;
import io.a2a.client.http.JdkA2AHttpClient;
import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
import org.jspecify.annotations.Nullable;
Expand All @@ -16,9 +17,8 @@ public RestTransportConfigBuilder httpClient(A2AHttpClient httpClient) {

@Override
public RestTransportConfig build() {
// No HTTP client provided, fallback to the default one (JDK-based implementation)
if (httpClient == null) {
httpClient = new JdkA2AHttpClient();
httpClient = A2AHttpClientFactory.create();
}

RestTransportConfig config = new RestTransportConfig(httpClient);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.a2a.client.transport.rest;

import io.a2a.client.http.JdkA2AHttpClient;
import io.a2a.client.http.A2AHttpClientFactory;
import io.a2a.client.transport.spi.ClientTransportProvider;
import io.a2a.spec.A2AClientException;
import io.a2a.spec.AgentCard;
Expand All @@ -17,7 +17,7 @@ public String getTransportProtocol() {
public RestTransport create(RestTransportConfig clientTransportConfig, AgentCard agentCard, String agentUrl) throws A2AClientException {
RestTransportConfig transportConfig = clientTransportConfig;
if (transportConfig == null) {
transportConfig = new RestTransportConfig(new JdkA2AHttpClient());
transportConfig = new RestTransportConfig(A2AHttpClientFactory.create());
}
return new RestTransport(clientTransportConfig.getHttpClient(), agentCard, agentUrl, transportConfig.getInterceptors());
}
Expand Down
45 changes: 27 additions & 18 deletions http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,49 +21,58 @@ public class A2ACardResolver {

/**
* Get the agent card for an A2A agent.
* The {@code JdkA2AHttpClient} will be used to fetch the agent card.
* The {@link A2AHttpClientFactory#create()} will be used to fetch the agent
* card if available.
*
* @param baseUrl the base URL for the agent whose agent card we want to retrieve
* @param baseUrl the base URL for the agent whose agent card we want to
* retrieve
* @throws A2AClientError if the URL for the agent is invalid
*/
public A2ACardResolver(String baseUrl) throws A2AClientError {
this(new JdkA2AHttpClient(), baseUrl, null, null);
this(A2AHttpClientFactory.create(), baseUrl, null, null);
}

/**
* Constructs an A2ACardResolver with a specific HTTP client and base URL.
*
* @param httpClient the http client to use
* @param baseUrl the base URL for the agent whose agent card we want to retrieve
* @param baseUrl the base URL for the agent whose agent card we want to
* retrieve
* @throws A2AClientError if the URL for the agent is invalid
*/
public A2ACardResolver(A2AHttpClient httpClient, String baseUrl) throws A2AClientError {
this(httpClient, baseUrl, null, null);
}

/**
* @param httpClient the http client to use
* @param baseUrl the base URL for the agent whose agent card we want to retrieve
* @param agentCardPath optional path to the agent card endpoint relative to the base
* agent URL, defaults to ".well-known/agent-card.json"
* @param httpClient the http client to use
* @param baseUrl the base URL for the agent whose agent card we want to
* retrieve
* @param agentCardPath optional path to the agent card endpoint relative to the
* base
* agent URL, defaults to ".well-known/agent-card.json"
* @throws A2AClientError if the URL for the agent is invalid
*/
public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCardPath) throws A2AClientError {
this(httpClient, baseUrl, agentCardPath, null);
}

/**
* @param httpClient the http client to use
* @param baseUrl the base URL for the agent whose agent card we want to retrieve
* @param agentCardPath 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. May be {@code null}
* @param httpClient the http client to use
* @param baseUrl the base URL for the agent whose agent card we want to
* retrieve
* @param agentCardPath 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. May be
* {@code null}
* @throws A2AClientError if the URL for the agent is invalid
*/
public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, @Nullable String agentCardPath,
@Nullable Map<String, String> authHeaders) throws A2AClientError {
@Nullable Map<String, String> authHeaders) throws A2AClientError {
this.httpClient = httpClient;
String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? DEFAULT_AGENT_CARD_PATH : agentCardPath;
String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? DEFAULT_AGENT_CARD_PATH
: agentCardPath;
try {
this.url = new URI(baseUrl).resolve(effectiveAgentCardPath).toString();
} catch (URISyntaxException e) {
Expand All @@ -76,8 +85,9 @@ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, @Nullable Strin
* Get the agent card for the configured A2A agent.
*
* @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 A2AClientError If an HTTP error occurs fetching the card
* @throws A2AClientJSONError If the response body cannot be decoded as JSON or
* validated against the AgentCard schema
*/
public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError {
A2AHttpClient.GetBuilder builder = httpClient.createGet()
Expand Down Expand Up @@ -109,5 +119,4 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError {

}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.a2a.client.http;

import java.util.Comparator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
* Factory for creating {@link A2AHttpClient} instances using the ServiceLoader mechanism.
*/
public final class A2AHttpClientFactory {

private static final List<A2AHttpClientProvider> PROVIDERS;

static {
ServiceLoader<A2AHttpClientProvider> loader = ServiceLoader.load(A2AHttpClientProvider.class);
PROVIDERS = StreamSupport.stream(loader.spliterator(), false)
.collect(Collectors.toList());
}

private A2AHttpClientFactory() {
// Utility class
}

/**
* Creates a new A2AHttpClient instance using the highest priority provider available.
* If no providers are found, it throws an {@link IllegalStateException}.
*/
public static A2AHttpClient create() {
return PROVIDERS.stream()
.max(Comparator.comparingInt(A2AHttpClientProvider::priority))
.map(A2AHttpClientProvider::create)
.orElseThrow(() -> new IllegalStateException("No A2AHttpClientProvider found"));
}

/**
* Creates a new A2AHttpClient instance using a specific provider by name.
*/
public static A2AHttpClient create(String providerName) {
if (providerName == null || providerName.isEmpty()) {
throw new IllegalArgumentException("Provider name must not be null or empty");
}

return PROVIDERS.stream()
.filter(provider -> providerName.equals(provider.name()))
.findFirst()
.map(A2AHttpClientProvider::create)
.orElseThrow(() -> new IllegalArgumentException(
"No A2AHttpClientProvider found with name: " + providerName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.a2a.client.http;

/**
* Provider interface for creating {@link A2AHttpClient} instances.
*/
public interface A2AHttpClientProvider {
/**
* Creates a new A2AHttpClient instance.
*/
A2AHttpClient create();

/**
* Returns the priority of this provider. Higher priority providers are preferred.
*/
default int priority() {
return 0;
}

/**
* Returns the name of this provider.
*/
String name();
}
Loading
Loading