Getting Started with A2A Java SDK and gRPC
The ability for AI agents to communicate across different frameworks and languages is key to building polyglot multi-agent systems. The recent 0.3.0.Alpha1 and 0.3.0.Beta1 releases of the A2A Java SDK take a significant step forward in this area by adding support for the gRPC transport and the HTTP+JSON/REST transport, offering greater flexibility and improved performance.
In this post, we’ll demonstrate how to create an A2A server agent and an A2A client that support multiple transports, where the gRPC transport will be selected.
Dice Agent Sample
To see the multi-transport support in action, we’re going to take a look at the new Dice Agent sample from the a2a-samples repo.
The DiceAgent
is a simple Quarkus LangChain4j AI service that can make use of tools to roll dice of different sizes and check if the result of a roll is a prime number.
A2A Server Agent
There are three key things in our sample application that turn our Quarkus LangChain4j AI service into an A2A server agent:
-
A dependency on at least one A2A Java SDK Server Reference implementation in the server application’s pom.xml file. In this sample, we’ve added dependencies on both
io.github.a2asdk:a2a-java-sdk-reference-grpc
andio.github.a2asdk:a2a-java-sdk-reference-jsonrpc
since we want our A2A server agent to be able to support both the gRPC and JSON-RPC transports. -
The DiceAgentCardProducer, which defines the
AgentCard
for our A2A server agent. -
The DiceAgentExecutorProducer, which calls our
DiceAgent
AI service.
Let’s look closer at the DiceAgentCardProducer
:
/**
* Producer for dice agent card configuration.
*/
@ApplicationScoped
public final class DiceAgentCardProducer {
/** The HTTP port for the agent service. */
@Inject
@ConfigProperty(name = "quarkus.http.port")
private int httpPort;
/**
* Produces the agent card for the dice agent.
*
* @return the configured agent card
*/
@Produces
@PublicAgentCard
public AgentCard agentCard() {
return new AgentCard.Builder()
.name("Dice Agent")
.description(
"Rolls an N-sided dice and answers questions about the "
+ "outcome of the dice rolls. Can also answer questions "
+ "about prime numbers.")
.preferredTransport(TransportProtocol.GRPC.asString()) (1)
.url("localhost:" + httpPort) (2)
.version("1.0.0")
.documentationUrl("http://example.com/docs")
.capabilities(
new AgentCapabilities.Builder()
.streaming(true)
.pushNotifications(false)
.stateTransitionHistory(false)
.build())
.defaultInputModes(List.of("text"))
.defaultOutputModes(List.of("text"))
.skills(
List.of(
new AgentSkill.Builder()
.id("dice_roller")
.name("Roll dice")
.description("Rolls dice and discusses outcomes")
.tags(List.of("dice", "games", "random"))
.examples(List.of("Can you roll a 6-sided die?"))
.build(),
new AgentSkill.Builder()
.id("prime_checker")
.name("Check prime numbers")
.description("Checks if given numbers are prime")
.tags(List.of("math", "prime", "numbers"))
.examples(
List.of("Is 17 a prime number?"))
.build()))
.protocolVersion("0.3.0")
.additionalInterfaces( (3)
List.of(
new AgentInterface(TransportProtocol.GRPC.asString(), (4)
"localhost:" + httpPort),
new AgentInterface(
TransportProtocol.JSONRPC.asString(), (5)
"http://localhost:" + httpPort)))
.build();
}
}
1 | The preferred transport for our A2A server agent, gRPC in this sample. This is the transport protocol available at the primary endpoint URL. |
2 | This is the primary endpoint URL for our A2A server agent. Since gRPC is our preferred transport and since
we’ll be using the HTTP port for gRPC and JSON-RPC, we’re specifying "localhost:" + httpPort here. |
3 | We can optionally specify additional interfaces supported by our A2A server agent here. Since we also want to support the JSON-RPC transport, we’ll be adding that in this section. |
4 | The primary endpoint URL can optionally be specified in the additional interfaces section for completeness. |
5 | The JSON-RPC transport URL. Notice that we’re using the HTTP port for both JSON-RPC and gRPC. |
Port Configuration for the Transports
In the previous section, we mentioned that we’re using the HTTP port for both the gRPC and JSON-RPC transports.
This is configured in our application.properties
file as shown here:
# Use the same port for gRPC and HTTP
quarkus.grpc.server.use-separate-server=false
quarkus.http.port=11000
This setting allows serving both plain HTTP and gRPC requests from the same HTTP server. Underneath it uses a Vert.x based gRPC server. If you set this setting to true, gRPC requests will be served on port 9000 (and gRPC Java will be used instead).
Starting the A2A Server Agent
Once we start our Quarkus application, our A2A server agent will be available at localhost:11000 for clients that would like to use gRPC and at http://localhost:11000 for clients that would like to use JSON-RPC.
A2A clients can now send queries to our A2A server agent using either the gRPC or JSON-RPC transport.
The complete source code and instructions for starting the server application are available here.
Now that we have our multi-transport server agent configured and ready to go, let’s take a look at how to create an A2A client that can communicate with it.
A2A Client
The dice_agent_multi_transport
sample also includes a TestClient that can be used to send messages to the Dice Agent.
Notice that the client’s pom.xml file contains dependencies on io.github.a2asdk:a2a-java-sdk-client
and io.github.a2asdk:a2a-java-sdk-client-transport-grpc
.
The a2a-java-sdk-client
dependency provides access to a Client.builder
that we’ll use to create our A2A client and also provides the ability for the client to support the JSON-RPC transport.
The a2a-java-sdk-client-transport-grpc
dependency provides the ability for the client to support the gRPC transport.
Let’s see how the TestClient
uses the A2A Java SDK to create a Client
that supports both gRPC and JSON-RPC:
...
// Fetch the public agent card
AgentCard publicAgentCard = new A2ACardResolver(serverUrl).getAgentCard();
// Create a CompletableFuture to handle async response
final CompletableFuture<String> messageResponse = new CompletableFuture<>();
// Create consumers for handling client events
List<BiConsumer<ClientEvent, AgentCard>> consumers = getConsumers(messageResponse);
// Create error handler for streaming errors
Consumer<Throwable> streamingErrorHandler = (error) -> {
System.out.println("Streaming error occurred: " + error.getMessage());
error.printStackTrace();
messageResponse.completeExceptionally(error);
};
// Create channel factory for gRPC transport
Function<String, Channel> channelFactory = agentUrl -> {
return ManagedChannelBuilder.forTarget(agentUrl).usePlaintext().build();
};
ClientConfig clientConfig = new ClientConfig.Builder()
.setAcceptedOutputModes(List.of("Text"))
.build();
// Create the client with both JSON-RPC and gRPC transport support.
// The A2A server agent's preferred transport is gRPC, since the client
// also supports gRPC, this is the transport that will get used
Client client = Client.builder(publicAgentCard) (1)
.addConsumers(consumers) (2)
.streamingErrorHandler(streamingErrorHandler) (3)
.withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) (4)
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) (5)
.clientConfig(clientConfig) (6)
.build();
// Create and send the message
Message message = A2A.toUserMessage(messageText);
System.out.println("Sending message: " + messageText);
client.sendMessage(message); (7)
System.out.println("Message sent successfully. Waiting for response...");
try {
// Wait for response with timeout
String responseText = messageResponse.get();
System.out.println("Final response: " + responseText);
} catch (Exception e) {
System.err.println("Failed to get response: " + e.getMessage());
e.printStackTrace();
}
...
1 | We can use Client.builder(publicAgentCard) to create our A2A client. We need to pass in the AgentCard retrieved from the A2A server agent this client will be communicating with. |
2 | We need to specify event consumers that will be used to handle the responses that will be received from the A2A server agent. This will be explained in more detail in the next section. |
3 | The A2A client created by the Client.builder will automatically send streaming messages, as opposed to
non-streaming messages, if it’s supported by both the server and the client. We need to specify a handler that will be used for any errors that might occur during streaming. |
4 | We’re indicating that we’d like our client to support the gRPC transport. |
5 | We’re indicating that we’d like our client to also support the JSON-RPC transport. When communicating with an A2A server agent that doesn’t support gRPC, this is the transport that would get used. |
6 | We can optionally specify general client configuration and preferences here. |
7 | Once our Client has been created, we can send a message to the A2A server agent. The client will automatically use streaming if it’s supported by both the server and the client. If the server doesn’t
support streaming, the client will send a non-streaming message instead. |
Defining the Event Consumers
When creating our A2A client, we need to specify event consumers that will be used to handle the responses that will be received from the A2A server agent. Let’s see how to define a consumer that handles the different types of events:
private static List<BiConsumer<ClientEvent, AgentCard>> getConsumers(
final CompletableFuture<String> messageResponse) {
List<BiConsumer<ClientEvent, AgentCard>> consumers = new ArrayList<>();
consumers.add(
(event, agentCard) -> {
if (event instanceof MessageEvent messageEvent) { (1)
Message responseMessage = messageEvent.getMessage();
String text = extractTextFromParts(responseMessage.getParts());
System.out.println("Received message: " + text);
messageResponse.complete(text);
} else if (event instanceof TaskUpdateEvent taskUpdateEvent) { (2)
UpdateEvent updateEvent = taskUpdateEvent.getUpdateEvent();
if (updateEvent
instanceof TaskStatusUpdateEvent taskStatusUpdateEvent) { (3)
System.out.println("Received status-update: "
+ taskStatusUpdateEvent.getStatus().state().asString());
if (taskStatusUpdateEvent.isFinal()) {
StringBuilder textBuilder = new StringBuilder();
List<Artifact> artifacts
= taskUpdateEvent.getTask().getArtifacts();
for (Artifact artifact : artifacts) {
textBuilder.append(extractTextFromParts(artifact.parts()));
}
String text = textBuilder.toString();
messageResponse.complete(text);
}
} else if (updateEvent
instanceof TaskArtifactUpdateEvent taskArtifactUpdateEvent) { (4)
List<Part<?>> parts = taskArtifactUpdateEvent
.getArtifact()
.parts();
String text = extractTextFromParts(parts);
System.out.println("Received artifact-update: " + text);
}
} else if (event instanceof TaskEvent taskEvent) { (5)
System.out.println("Received task event: "
+ taskEvent.getTask().getId());
}
});
return consumers;
}
1 | This defines how to handle a Message received from the server agent. The server agent will send a response that contains a Message for immediate, self-contained interactions that are stateless. |
2 | This defines how to handle an UpdateEvent received from the server agent for a specific task. There are
two types of UpdateEvents that can be received. |
3 | A TaskStatusUpdateEvent notifies the client of a change in a task’s status. This is typically used in streaming interactions. If this is the final event in the stream for this interaction, taskStatusUpdateEvent.isFinal()
will return true . |
4 | A TaskArtifactUpdateEvent notifies the client that an artifact has been generated or updated. An artifact contains output generated by an agent during a task. This is typically used in streaming interactions. |
5 | This defines how to handle a Task received from the server agent. A Task will be processed by the server agent through a defined lifecycle until it reaches an interrupted state or a terminal state. |
Transport Selection
When creating our Client
, we used the withTransport
method to specify that we want the client
to support both gRPC and JSON-RPC, in that order. The Client.builder
selects the appropriate
transport protocol to use based on information obtained from the A2A server agent’s AgentCard
,
taking into account the transports configured for the client. In this sample application, because
the server agent’s preferred transport is gRPC, the gRPC transport will be used.
Using the A2A Client
The sample application contains a TestClientRunner
that can be run using JBang
:
jbang TestClientRunner.java
You should see output similar to this:
Connecting to dice agent at: http://localhost:11000
Successfully fetched public agent card:
...
Sending message: Can you roll a 5 sided die?
Message sent successfully. Waiting for response...
Received status-update: submitted
Received status-update: working
Received artifact-update: Sure! I rolled a 5 sided die and got a 3.
Received status-update: completed
Final response: Sure! I rolled a 5 sided die and got a 3.
You can also experiment with sending different messages to the A2A server agent using the --message
option
as follows:
jbang TestClientRunner.java --message "Can you roll a 13-sided die and check if the result is a prime number?"
Connecting to dice agent at: http://localhost:11000
Successfully fetched public agent card:
...
Sending message: Can you roll a 13-sided die and check if the result is a prime number?
Message sent successfully. Waiting for response...
Received status-update: submitted
Received status-update: working
Received artifact-update: I rolled a 13 sided die and got a 3. 3 is a prime number.
Received status-update: completed
Final response: I rolled a 13 sided die and got a 3. 3 is a prime number.
The complete source code and instructions for starting the client are available here. There are also details on how to use an A2A client that uses the A2A Python SDK instead of the A2A Java SDK to communicate with our A2A server agent.