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: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ pom.xml.versionsBackup
release.properties
.flattened-pom.xml

#Claude
CLAUDE.md

# Eclipse
.project
.classpath
Expand All @@ -20,6 +23,7 @@ bin/

# NetBeans
nb-configuration.xml
nbactions.xml

# Visual Studio Code
.vscode
Expand Down
5 changes: 5 additions & 0 deletions boms/extras/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
<artifactId>a2a-java-extras-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>a2a-java-sdk-opentelemetry</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>a2a-java-extras-task-store-database-jpa</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions boms/extras/src/it/extras-usage-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-extras-common</artifactId>
</dependency>
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-extras-task-store-database-jpa</artifactId>
Expand Down
57 changes: 52 additions & 5 deletions client/base/src/main/java/io/a2a/client/ClientBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,32 @@
import io.a2a.client.transport.spi.ClientTransportConfig;
import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
import io.a2a.client.transport.spi.ClientTransportProvider;
import io.a2a.client.transport.spi.ClientTransportWrapper;
import io.a2a.spec.A2AClientException;
import io.a2a.spec.AgentCard;
import io.a2a.spec.AgentInterface;
import io.a2a.spec.TransportProtocol;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.ServiceLoader.Provider;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.stream.Collectors;

public class ClientBuilder {

private static final Map<String, ClientTransportProvider<? extends ClientTransport, ? extends ClientTransportConfig<?>>> transportProviderRegistry = new HashMap<>();
private static final Map<Class<? extends ClientTransport>, String> transportProtocolMapping = new HashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(ClientBuilder.class);

static {
ServiceLoader<ClientTransportProvider> loader = ServiceLoader.load(ClientTransportProvider.class);
Expand All @@ -37,7 +43,8 @@ public class ClientBuilder {
private final AgentCard agentCard;

private final List<BiConsumer<ClientEvent, AgentCard>> consumers = new ArrayList<>();
private @Nullable Consumer<Throwable> streamErrorHandler;
private @Nullable
Consumer<Throwable> streamErrorHandler;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a bit strange :-)

The original looks better, but I think

@Nullable
private Consumer<Throwable> streamErrorHandler;

would look better. But I see we have private @Nullable .... 'everywhere' :-)

private ClientConfig clientConfig = new ClientConfig.Builder().build();

private final Map<Class<? extends ClientTransport>, ClientTransportConfig<? extends ClientTransport>> clientTransports = new LinkedHashMap<>();
Expand Down Expand Up @@ -105,7 +112,7 @@ private ClientTransport buildClientTransport() throws A2AClientException {
throw new A2AClientException("Missing required TransportConfig for " + agentInterface.transport());
}

return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url());
return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url()), clientTransportConfig);
}

private Map<String, String> getServerPreferredTransports() {
Expand Down Expand Up @@ -160,10 +167,50 @@ private AgentInterface findBestClientTransport() throws A2AClientException {
if (transportProtocol == null || transportUrl == null) {
throw new A2AClientException("No compatible transport found");
}
if (! transportProviderRegistry.containsKey(transportProtocol)) {
if (!transportProviderRegistry.containsKey(transportProtocol)) {
throw new A2AClientException("No client available for " + transportProtocol);
}

return new AgentInterface(transportProtocol, transportUrl);
}

/**
* Wraps the transport with all available transport wrappers discovered via ServiceLoader.
* Wrappers are applied in priority order (highest priority first).
*
* @param transport the base transport to wrap
* @param clientTransportConfig the transport configuration
* @return the wrapped transport (or original if no wrappers are available/applicable)
*/
private ClientTransport wrap(ClientTransport transport, ClientTransportConfig<? extends ClientTransport> clientTransportConfig) {
ServiceLoader<ClientTransportWrapper> wrapperLoader = ServiceLoader.load(ClientTransportWrapper.class);

// Collect all wrappers and sort by natural order (uses Comparable implementation)
List<ClientTransportWrapper> wrappers = wrapperLoader.stream().map(Provider::get)
.sorted()
.collect(Collectors.toList());

if (wrappers.isEmpty()) {
LOGGER.debug("No client transport wrappers found via ServiceLoader");
return transport;
}

// Apply wrappers in priority order
ClientTransport wrapped = transport;
for (ClientTransportWrapper wrapper : wrappers) {
try {
ClientTransport newWrapped = wrapper.wrap(wrapped, clientTransportConfig);
if (newWrapped != wrapped) {
LOGGER.debug("Applied transport wrapper: {} (priority: {})",
wrapper.getClass().getName(), wrapper.priority());
}
wrapped = newWrapped;
} catch (Exception e) {
LOGGER.warn("Failed to apply transport wrapper {}: {}",
wrapper.getClass().getName(), e.getMessage(), e);
}
}

return wrapped;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import io.a2a.client.transport.spi.ClientTransportConfig;
import org.jspecify.annotations.Nullable;

public class RestTransportConfig extends ClientTransportConfig<RestTransport> {
public class RestTransportConfig extends ClientTransportConfig<RestTransport> {

private final @Nullable A2AHttpClient httpClient;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
import java.util.ArrayList;
import java.util.HashMap;

import java.util.List;
import java.util.Map;

/**
* Configuration for an A2A client transport.
*/
public abstract class ClientTransportConfig<T extends ClientTransport> {

protected List<ClientCallInterceptor> interceptors = new ArrayList<>();
protected Map<String, ? extends Object > parameters = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type Map<String, ? extends Object > is unconventional. Map<String, Object> is more standard and communicates the intent more clearly. The wildcard ? extends Object is equivalent to ?, which can be confusing. Also, there is an extra space before the closing >. Please update this field and its corresponding getter/setter methods to use Map<String, Object>.

Suggested change
protected Map<String, ? extends Object > parameters = new HashMap<>();
protected Map<String, Object> parameters = new HashMap<>();


public void setInterceptors(List<ClientCallInterceptor> interceptors) {
this.interceptors = new ArrayList<>(interceptors);
Expand All @@ -19,4 +22,12 @@ public void setInterceptors(List<ClientCallInterceptor> interceptors) {
public List<ClientCallInterceptor> getInterceptors() {
return interceptors;
}

public void setParameters(Map<String, ? extends Object > parameters) {
this.parameters = new HashMap<>(parameters);
}

public Map<String, ? extends Object > getParameters() {
return parameters;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.a2a.client.transport.spi;

/**
* Service provider interface for wrapping client transports with additional functionality.
* Implementations can add cross-cutting concerns like tracing, metrics, logging, etc.
*
* <p>Wrappers are discovered via Java's ServiceLoader mechanism. To register a wrapper,
* create a file {@code META-INF/services/io.a2a.client.transport.spi.ClientTransportWrapper}
* containing the fully qualified class name of your implementation.
*
* <p>Wrappers are sorted by priority in descending order (highest priority first).
* This interface implements {@link Comparable} to enable natural sorting.
*
* <p>Example implementation:
* <pre>{@code
* public class TracingWrapper implements ClientTransportWrapper {
* @Override
* public ClientTransport wrap(ClientTransport transport, ClientTransportConfig<?> config) {
* if (config.getParameters().containsKey("tracer")) {
* return new TracingTransport(transport, (Tracer) config.getParameters().get("tracer"));
* }
* return transport;
* }
*
* @Override
* public int priority() {
* return 100; // Higher priority = wraps earlier (outermost)
* }
* }
* }</pre>
*/
public interface ClientTransportWrapper extends Comparable<ClientTransportWrapper> {

/**
* Wraps the given transport with additional functionality.
*
* <p>Implementations should check the configuration to determine if they should
* actually wrap the transport. If the wrapper is not applicable (e.g., required
* configuration is missing), return the original transport unchanged.
*
* @param transport the transport to wrap
* @param config the transport configuration, may contain wrapper-specific parameters
* @return the wrapped transport, or the original if wrapping is not applicable
*/
ClientTransport wrap(ClientTransport transport, ClientTransportConfig<?> config);

/**
* Returns the priority of this wrapper. Higher priority wrappers are applied first
* (wrap the transport earlier, resulting in being the outermost wrapper).
*
* <p>Default priority is 0. Suggested ranges:
* <ul>
* <li>1000+ : Critical infrastructure (security, authentication)
* <li>500-999: Observability (tracing, metrics, logging)
* <li>100-499: Enhancement (caching, retry logic)
* <li>0-99: Optional features
* </ul>
*
* @return the priority value, higher values = higher priority
*/
default int priority() {
return 0;
}

/**
* Compares this wrapper with another based on priority.
* Returns a negative integer, zero, or a positive integer as this wrapper
* has higher priority than, equal to, or lower priority than the specified wrapper.
*
* <p>Note: This comparison is reversed (higher priority comes first) to enable
* natural sorting in descending priority order.
*
* @param other the wrapper to compare to
* @return negative if this has higher priority, positive if lower, zero if equal
*/
@Override
default int compareTo(ClientTransportWrapper other) {
// Reverse comparison: higher priority should come first
return Integer.compare(other.priority(), this.priority());
}
}
Loading