Skip to content

Conversation

@fjuma
Copy link
Collaborator

@fjuma fjuma commented Aug 14, 2025

Description

This is still work in progress and builds upon the changes from #228 (thanks very much for the PR @brasseld!).

This PR essentially does the following:

  • Introduces ClientTransport and ClientTransportProvider interfaces
  • Introduces implementations of these classes for JSON-RPC and gRPC, split out into separate client modules
  • Introduces a ClientFactory that can be used to create a Client based on the agent card, client preferences, and available client transport providers from service loader discovery (i.e., we check what transports are supported by the server using the agent card, compare to preferences that can be specified via ClientConfig and what client transport providers are available and then instantiate the client based on that) (similar to the Python SDK client refactoring)
  • Significant changes to the Client API have been introduced, following a similar approach to the changes and concepts introduced in the Python SDK client refactoring (e.g. see refactor: Refactor client code to support multi-transport interactions and simplify client interfaces a2a-python#342).
  • One of the bigger changes to the client is to have a single sendMessage method and detect whether or not the streaming or non-streaming transport method should be used based on the capabilities specified by the agent card and client preferences.

I still need to do the following:

  • * Update AbstractA2AServerTest so that it makes use of a Client created using the appropriate transport for the test. This will allow us to use the same end-to-end tests for all transports.
  • * Update ClientConfig to no longer depend on io.grpc.Channel - I'll look into a different way to configure the Channel for gRPC to avoid needing the grpc dependency in client-config. While looking at this, we should also do something similar for the A2AHttpClient config to avoid that dep too.
  • * Add documentation on how to use the new client code

Fixes #230 🦕

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @fjuma, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the A2A Java client to be transport-agnostic, enabling seamless communication with agents using either JSON-RPC or gRPC. It introduces a modular design with dedicated client transport implementations and a ClientFactory for dynamic client instantiation. The changes aim to enhance flexibility, align the Java client API with the Python SDK, and improve overall extensibility by leveraging Java's ServiceLoader for transport discovery.

Highlights

  • Transport Agnostic Client: The client has been refactored to be transport-agnostic, allowing it to communicate with A2A agents using different protocols (e.g., JSON-RPC, gRPC) interchangeably.
  • New Transport Interfaces and Implementations: New interfaces ClientTransport and ClientTransportProvider have been introduced, along with concrete implementations for JSON-RPC and gRPC, each residing in its own dedicated client module.
  • Dynamic Client Creation with ClientFactory: A ClientFactory is now responsible for dynamically creating Client instances based on the agent's capabilities (from its agent card), client preferences, and available transport providers discovered via Java's ServiceLoader mechanism.
  • Client API Refactoring: The core Client API has undergone significant changes, adopting a design similar to the Python SDK client refactoring, enhancing consistency across SDKs.
  • Enhanced Client Configuration and Interception: New configuration and interception capabilities have been added through ClientCallContext, ClientCallInterceptor, and PayloadAndHeaders classes, enabling fine-grained control over client calls.
  • Module Restructuring and Class Relocation: Existing HTTP client and Server-Sent Events (SSE) related classes have been renamed and relocated into new, more organized modules (client-http, client-transport/grpc, client-transport/jsonrpc).
  • Removal of Legacy Client Implementations: The previous monolithic A2AClient and A2AGrpcClient classes have been removed, replaced by the new modular and transport-agnostic Client implementation.
  • New Specification Classes: Several new specification-related classes have been introduced, including A2AClientException (and its specialized subclasses), TransportProtocol, and UpdateEvent, to support the new client architecture and error handling.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@fjuma
Copy link
Collaborator Author

fjuma commented Aug 14, 2025

Hi @brasseld, thanks very much for your PR! I've taken your branch and combined our changes together as described in the PR description. Please take a look and let me know what you think.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a significant and well-structured refactoring to make the client transport-agnostic. The introduction of ClientTransport and ClientTransportProvider interfaces using Java's ServiceLoader is a great approach for extensibility. The separation of concerns into different modules (config, http, transports) improves the project structure and maintainability.

My review focuses on a few key areas for improvement:

  • Immutability: The new configuration and context classes (ClientConfig, ClientCallContext, PayloadAndHeaders) should be made immutable to prevent unexpected side effects and improve thread safety. I've provided suggestions to create defensive copies and return unmodifiable collections.
  • Performance: There's a potential performance issue in the JSONRPCTransport where the agent card might be fetched repeatedly. I've pointed this out with a suggestion for caching.

Overall, this is a solid piece of work that greatly improves the client architecture. Addressing the points above will make it even more robust and efficient.

}

@Override
public AgentCard getAgentCard(ClientCallContext context) throws A2AClientException {
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The current implementation of getAgentCard can be inefficient. If the agent supports an extended card, this method will make an HTTP request to fetch it on every call, even if it has been fetched before because the check for supportsAuthenticatedExtendedCard and subsequent fetch is outside the if (agentCard == null) block. The agent card should be fetched and cached on the first call. Consider refactoring the logic to fetch the agent card (and the extended one if supported) only if it hasn't been fetched before.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

Comment on lines 15 to 18
public ClientCallContext(Map<String, Object> state, Map<String, String> headers) {
this.state = state;
this.headers = headers;
}
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 ClientCallContext class is not immutable because the maps passed to the constructor are assigned directly. This could lead to them being modified from outside the class after construction. To improve robustness, you should create defensive copies of these collections and handle null inputs gracefully.

Suggested change
public ClientCallContext(Map<String, Object> state, Map<String, String> headers) {
this.state = state;
this.headers = headers;
}
public ClientCallContext(Map<String, Object> state, Map<String, String> headers) {
this.state = state == null ? java.util.Collections.emptyMap() : new java.util.HashMap<>(state);
this.headers = headers == null ? java.util.Collections.emptyMap() : new java.util.HashMap<>(headers);
}

Comment on lines 20 to 26
public Map<String, Object> getState() {
return state;
}

public Map<String, String> getHeaders() {
return headers;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To ensure immutability, the getters for collections should return unmodifiable views. This prevents callers from modifying the internal state of the ClientCallContext object.

Suggested change
public Map<String, Object> getState() {
return state;
}
public Map<String, String> getHeaders() {
return headers;
}
public Map<String, Object> getState() {
return java.util.Collections.unmodifiableMap(state);
}
public Map<String, String> getHeaders() {
return java.util.Collections.unmodifiableMap(headers);
}

Comment on lines 26 to 40
public ClientConfig(Boolean streaming, Boolean polling, A2AHttpClient httpClient, Channel channel,
List<String> supportedTransports, Boolean useClientPreference,
List<String> acceptedOutputModes, PushNotificationConfig pushNotificationConfig,
Integer historyLength, Map<String, Object> metadata) {
this.streaming = streaming == null ? true : streaming;
this.polling = polling == null ? false : polling;
this.httpClient = httpClient;
this.channel = channel;
this.supportedTransports = supportedTransports;
this.useClientPreference = useClientPreference == null ? false : useClientPreference;
this.acceptedOutputModes = acceptedOutputModes;
this.pushNotificationConfig = pushNotificationConfig;
this.historyLength = historyLength;
this.metadata = metadata;
}
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 ClientConfig class is not fully immutable. The collections (supportedTransports, acceptedOutputModes, metadata) are assigned directly in the constructor. This could lead to them being modified from outside the class after construction.

To improve robustness, you should create defensive copies of these collections. Also, consider handling null inputs for these collections gracefully, for example by initializing them to empty collections.

Suggested change
public ClientConfig(Boolean streaming, Boolean polling, A2AHttpClient httpClient, Channel channel,
List<String> supportedTransports, Boolean useClientPreference,
List<String> acceptedOutputModes, PushNotificationConfig pushNotificationConfig,
Integer historyLength, Map<String, Object> metadata) {
this.streaming = streaming == null ? true : streaming;
this.polling = polling == null ? false : polling;
this.httpClient = httpClient;
this.channel = channel;
this.supportedTransports = supportedTransports;
this.useClientPreference = useClientPreference == null ? false : useClientPreference;
this.acceptedOutputModes = acceptedOutputModes;
this.pushNotificationConfig = pushNotificationConfig;
this.historyLength = historyLength;
this.metadata = metadata;
}
public ClientConfig(Boolean streaming, Boolean polling, A2AHttpClient httpClient, Channel channel,
List<String> supportedTransports, Boolean useClientPreference,
List<String> acceptedOutputModes, PushNotificationConfig pushNotificationConfig,
Integer historyLength, Map<String, Object> metadata) {
this.streaming = streaming == null ? true : streaming;
this.polling = polling == null ? false : polling;
this.httpClient = httpClient;
this.channel = channel;
this.supportedTransports = supportedTransports == null ? java.util.Collections.emptyList() : new java.util.ArrayList<>(supportedTransports);
this.useClientPreference = useClientPreference == null ? false : useClientPreference;
this.acceptedOutputModes = acceptedOutputModes == null ? java.util.Collections.emptyList() : new java.util.ArrayList<>(acceptedOutputModes);
this.pushNotificationConfig = pushNotificationConfig;
this.historyLength = historyLength;
this.metadata = metadata == null ? java.util.Collections.emptyMap() : new java.util.HashMap<>(metadata);
}

Comment on lines 58 to 80
public List<String> getSupportedTransports() {
return supportedTransports;
}

public boolean isUseClientPreference() {
return useClientPreference;
}

public List<String> getAcceptedOutputModes() {
return acceptedOutputModes;
}

public PushNotificationConfig getPushNotificationConfig() {
return pushNotificationConfig;
}

public Integer getHistoryLength() {
return historyLength;
}

public Map<String, Object> getMetadata() {
return metadata;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To ensure immutability, the getters for collections should return unmodifiable views. This prevents callers from modifying the internal state of the ClientConfig object.

    public List<String> getSupportedTransports() {
        return java.util.Collections.unmodifiableList(supportedTransports);
    }

    public boolean isUseClientPreference() {
        return useClientPreference;
    }

    public List<String> getAcceptedOutputModes() {
        return java.util.Collections.unmodifiableList(acceptedOutputModes);
    }

    public PushNotificationConfig getPushNotificationConfig() {
        return pushNotificationConfig;
    }

    public Integer getHistoryLength() {
        return historyLength;
    }

    public Map<String, Object> getMetadata() {
        return java.util.Collections.unmodifiableMap(metadata);
    }

Comment on lines 10 to 21
public PayloadAndHeaders(Object payload, Map<String, String> headers) {
this.payload = payload;
this.headers = headers;
}

public Object getPayload() {
return payload;
}

public Map<String, String> getHeaders() {
return headers;
}
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 PayloadAndHeaders class is not immutable. The headers map is assigned directly in the constructor and returned directly by the getter. This can lead to unexpected behavior if the map is modified outside the class.

To make the class immutable, you should create a defensive copy of the map in the constructor and return an unmodifiable view of the map from the getter.

Suggested change
public PayloadAndHeaders(Object payload, Map<String, String> headers) {
this.payload = payload;
this.headers = headers;
}
public Object getPayload() {
return payload;
}
public Map<String, String> getHeaders() {
return headers;
}
public PayloadAndHeaders(Object payload, Map<String, String> headers) {
this.payload = payload;
this.headers = headers == null ? java.util.Collections.emptyMap() : new java.util.HashMap<>(headers);
}
public Object getPayload() {
return payload;
}
public Map<String, String> getHeaders() {
return java.util.Collections.unmodifiableMap(headers);
}

@brasseld
Copy link
Contributor

brasseld commented Aug 14, 2025

Looks OK @fjuma

My only concern at the moment is the accumultation of client-* modules, I'm wondering if that would not be easier to have the following structure instead:

-> client
    -> config
    -> transport
        -> jsonrpc
        -> grpc

Also, I'm a bit unsure about having the client-http module outside a transport... Even if I do get the fact that retrieving an AgentCard can only be done through a normal HTTP call from the A2ACardResolver.

But what if we were to provide an other HTTP implementation based on vertx, okhttp, .... ?

Looking at the client structure, I would probably opt for something similar for the server side...

@brasseld
Copy link
Contributor

I'm also not a big fan of the client instanciation with the ClientFactory and the ClientConfig as these have referenbce to A2AHttpClient and Channel, potentially forcing to load useless dependencies.

We probably need some inheritance here, make ClientConfig as abstract, and provide specific implementation for GrpcClientConfig and JsonRpcClientConfig

@brasseld
Copy link
Contributor

Anyway, what you did is already a great start, we can look at improving it next!

👏 @fjuma

@fjuma
Copy link
Collaborator Author

fjuma commented Aug 14, 2025

Looks OK @fjuma

My only concern at the moment is the accumultation of client-* modules, I'm wondering if that would not be easier to have the following structure instead:

-> client
    -> config
    -> transport
        -> jsonrpc
        -> grpc

Thanks for the feedback!

Good point, yes I can update this. There's also other code in the client directory right now, I can probably just move that into its own directory within client.

Also, I'm a bit unsure about having the client-http module outside a transport... Even if I do get the fact that retrieving an AgentCard can only be done through a normal HTTP call from the A2ACardResolver.

But what if we were to provide an other HTTP implementation based on vertx, okhttp, .... ?

Looking at the client structure, I would probably opt for something similar for the server side...

I kept the http related code outside of the jsonrpc transport because it will also be used for the HTTP+JSON transport (e.g., #229). cc @ehsavoie

@fjuma
Copy link
Collaborator Author

fjuma commented Aug 14, 2025

I'm also not a big fan of the client instanciation with the ClientFactory and the ClientConfig as these have referenbce to A2AHttpClient and Channel, potentially forcing to load useless dependencies.

We probably need some inheritance here, make ClientConfig as abstract, and provide specific implementation for GrpcClientConfig and JsonRpcClientConfig

Right, this is part of my todo list in the PR description, to avoid the unneeded dependencies. Will make sure to also avoid the http client dependency here as well, thanks for pointing that out!

@brasseld
Copy link
Contributor

Also, I'm a bit unsure about having the client-http module outside a transport... Even if I do get the fact that retrieving an AgentCard can only be done through a normal HTTP call from the A2ACardResolver.
But what if we were to provide an other HTTP implementation based on vertx, okhttp, .... ?
Looking at the client structure, I would probably opt for something similar for the server side...

I kept the http related code outside of the jsonrpc transport because it will also be used for the HTTP+JSON transport (e.g., #229). cc @ehsavoie

Yeah, I do get your point, but still wondering if that's the right approach. Again, my use-case is for us to provide other HTTP client implementation, pretty sure that this may come sooner rather than later.

What about having something like this:

client 
    - transport
        - http
            - jdk implementation
            - vertx implementation
            - ...
        - jsonrpc with a dependency to http
        - json with a dependency to http
        - grpc 

Then the agentCardResolver would make use of an HttpTransport.

(just htinking, need to look a bit more into it).

@fjuma
Copy link
Collaborator Author

fjuma commented Aug 14, 2025

Also, I'm a bit unsure about having the client-http module outside a transport... Even if I do get the fact that retrieving an AgentCard can only be done through a normal HTTP call from the A2ACardResolver.
But what if we were to provide an other HTTP implementation based on vertx, okhttp, .... ?
Looking at the client structure, I would probably opt for something similar for the server side...

I kept the http related code outside of the jsonrpc transport because it will also be used for the HTTP+JSON transport (e.g., #229). cc @ehsavoie

Yeah, I do get your point, but still wondering if that's the right approach. Again, my use-case is for us to provide other HTTP client implementation, pretty sure that this may come sooner rather than later.

What about having something like this:

client 
    - transport
        - http
            - jdk implementation
            - vertx implementation
            - ...
        - jsonrpc with a dependency to http
        - json with a dependency to http
        - grpc 

Then the agentCardResolver would make use of an HttpTransport.

(just htinking, need to look a bit more into it).

Yeah I see your point, that structure makes sense, will think more about that.

@fjuma
Copy link
Collaborator Author

fjuma commented Aug 14, 2025

Also, I'm a bit unsure about having the client-http module outside a transport... Even if I do get the fact that retrieving an AgentCard can only be done through a normal HTTP call from the A2ACardResolver.
But what if we were to provide an other HTTP implementation based on vertx, okhttp, .... ?
Looking at the client structure, I would probably opt for something similar for the server side...

I kept the http related code outside of the jsonrpc transport because it will also be used for the HTTP+JSON transport (e.g., #229). cc @ehsavoie

Yeah, I do get your point, but still wondering if that's the right approach. Again, my use-case is for us to provide other HTTP client implementation, pretty sure that this may come sooner rather than later.
What about having something like this:

client 
    - transport
        - http
            - jdk implementation
            - vertx implementation
            - ...
        - jsonrpc with a dependency to http
        - json with a dependency to http
        - grpc 

Then the agentCardResolver would make use of an HttpTransport.
(just htinking, need to look a bit more into it).

Yeah I see your point, that structure makes sense, will think more about that.

Actually, A2AHttpClient is also used in the server code. So maybe it makes sense to have the HTTP client transport code outside of the client transport protocol module.

@brasseld
Copy link
Contributor

Ok, let's keep it outside for now, we can look into it later, after this refactor.

@fjuma fjuma force-pushed the client_agnostic_transport branch 2 times, most recently from 8b35a6e to d2f875e Compare August 18, 2025 14:48
@fjuma
Copy link
Collaborator Author

fjuma commented Aug 18, 2025

I've updated the directory structure for the client side as discussed at the end of last week. I'm now working through the other items from the todo list in the PR description.

@fjuma
Copy link
Collaborator Author

fjuma commented Aug 18, 2025

I've updated ClientConfig so it no longer requires dependencies on http-client or grpc by introducing a ClientTransportConfig interface.

@fjuma fjuma force-pushed the client_agnostic_transport branch 6 times, most recently from 161453e to d6796bc Compare August 20, 2025 18:05
@fjuma
Copy link
Collaborator Author

fjuma commented Aug 20, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a significant and well-structured refactoring to make the client transport-agnostic. The introduction of the ClientTransport SPI and the modularization of JSON-RPC and gRPC transports are excellent improvements. The overall architecture is much cleaner and more extensible. My review focuses on the new client implementation, where I've identified a couple of bugs related to state management in ClientTaskManager and some areas for code improvement to reduce duplication and enhance robustness, particularly in the gRPC transport implementation. The refactoring of the test suite to use the new client API is also a great step forward.

Comment on lines 73 to 75
List<Message> history = task.getHistory();
history.add(taskStatusUpdateEvent.getStatus().message());
taskBuilder.history(history);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The history list from the original task object is being modified directly. This is a side effect that can lead to unexpected behavior, as it alters the state of an object that should be treated as immutable within this context. It's safer to create a new list from the existing history before adding new items.

Suggested change
List<Message> history = task.getHistory();
history.add(taskStatusUpdateEvent.getStatus().message());
taskBuilder.history(history);
List<Message> history = new ArrayList<>(task.getHistory());
history.add(taskStatusUpdateEvent.getStatus().message());
taskBuilder.history(history);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines 47 to 57
public void sendMessage(Message request, ClientCallContext context) throws A2AClientException {
MessageSendConfiguration messageSendConfiguration = new MessageSendConfiguration.Builder()
.acceptedOutputModes(clientConfig.getAcceptedOutputModes())
.blocking(clientConfig.isPolling())
.historyLength(clientConfig.getHistoryLength())
.pushNotification(clientConfig.getPushNotificationConfig())
.build();

MessageSendParams messageSendParams = new MessageSendParams.Builder()
.message(request)
.configuration(messageSendConfiguration)
.metadata(clientConfig.getMetadata())
.build();

sendMessage(messageSendParams, null, null, context);
}

@Override
public void sendMessage(Message request, List<BiConsumer<ClientEvent, AgentCard>> consumers,
Consumer<Throwable> streamingErrorHandler, ClientCallContext context) throws A2AClientException {
MessageSendConfiguration messageSendConfiguration = new MessageSendConfiguration.Builder()
.acceptedOutputModes(clientConfig.getAcceptedOutputModes())
.blocking(clientConfig.isPolling())
.historyLength(clientConfig.getHistoryLength())
.pushNotification(clientConfig.getPushNotificationConfig())
.build();

MessageSendParams messageSendParams = new MessageSendParams.Builder()
.message(request)
.configuration(messageSendConfiguration)
.metadata(clientConfig.getMetadata())
.build();

sendMessage(messageSendParams, consumers, streamingErrorHandler, context);
}
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 logic for creating MessageSendParams is duplicated in the sendMessage overloads. To improve maintainability and reduce redundancy, this logic could be extracted into a private helper method.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +30 to +55
// Extract the actual error type from the description if possible
// (using description because the same code can map to multiple errors -
// see GrpcHandler#handleError)
if (description != null) {
if (description.contains("TaskNotFoundError")) {
return new A2AClientException(errorPrefix + description, new TaskNotFoundError());
} else if (description.contains("UnsupportedOperationError")) {
return new A2AClientException(errorPrefix + description, new UnsupportedOperationError());
} else if (description.contains("InvalidParamsError")) {
return new A2AClientException(errorPrefix + description, new InvalidParamsError());
} else if (description.contains("InvalidRequestError")) {
return new A2AClientException(errorPrefix + description, new InvalidRequestError());
} else if (description.contains("MethodNotFoundError")) {
return new A2AClientException(errorPrefix + description, new MethodNotFoundError());
} else if (description.contains("TaskNotCancelableError")) {
return new A2AClientException(errorPrefix + description, new TaskNotCancelableError());
} else if (description.contains("PushNotificationNotSupportedError")) {
return new A2AClientException(errorPrefix + description, new PushNotificationNotSupportedError());
} else if (description.contains("JSONParseError")) {
return new A2AClientException(errorPrefix + description, new JSONParseError());
} else if (description.contains("ContentTypeNotSupportedError")) {
return new A2AClientException(errorPrefix + description, new ContentTypeNotSupportedError(null, description, null));
} else if (description.contains("InvalidAgentResponseError")) {
return new A2AClientException(errorPrefix + description, new InvalidAgentResponseError(null, description, null));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Relying on string matching (.contains()) on the error description to determine the error type is brittle and can easily break if the server-side error messages change. A more robust approach would be to use gRPC's rich error model (e.g., Status with details in Metadata) to pass structured error details from the server to the client. This would allow for reliable error handling without depending on specific string formats.

Copy link
Collaborator Author

@fjuma fjuma Aug 20, 2025

Choose a reason for hiding this comment

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

Added a comment on why this is the approach we're taking for now.

Comment on lines +216 to +219
public AgentCard getAgentCard(ClientCallContext context) throws A2AClientException {
// TODO: Determine how to handle retrieving the authenticated extended agent card
return agentCard;
}
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 getAgentCard method has a TODO and currently returns the cached agentCard without attempting to fetch an authenticated extended card. This means the gRPC transport will not support extended agent cards, which might be a feature gap compared to other transports. This should be implemented to ensure feature parity.

Comment on lines 21 to 23
public ClientTransport create(ClientConfig clientConfig, AgentCard agentCard,
String agentUrl, List<ClientCallInterceptor> interceptors) {
// not making use of the interceptors for gRPC for now
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 gRPC transport provider currently ignores the interceptors parameter. While gRPC has its own interceptor mechanism (ClientInterceptor), not using the common ClientCallInterceptor interface could lead to inconsistencies for cross-cutting concerns like authentication or logging across different transports. It would be beneficial to adapt the provided interceptors to gRPC's ClientInterceptor to maintain a unified interception model.

@fjuma fjuma force-pushed the client_agnostic_transport branch 2 times, most recently from 2f075ee to 79cbb48 Compare August 20, 2025 20:45
brasseld and others added 28 commits August 25, 2025 09:49
… and also split out the http client code into a separate module
…text, ClientConfig, and ClientCallInterceptor similar to the Python SDK. Introduce a ClientTransportProvider and update the JSONRPC and gRPC transport implementations. Introduce a new Client and ClientFactory implementations.
… to make use of it, and remove the http-client and grpc dependencies from the client config module
…t to be able to make use of the appropriate client based on the transport and update the JSONRPC and gRPC tests to extend this
…d some convenience methods to AbstractClient
…t other server implementations that also support JSONRPC can make use of these tests. These tests will be skipped when testing other transports
@fjuma fjuma force-pushed the client_agnostic_transport branch from c826f3a to 26ab76d Compare August 25, 2025 13:50
@fjuma fjuma merged commit 26ab76d into a2aproject:main Aug 25, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the client transport agnostic

4 participants