The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

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:

  1. 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 and io.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.

  2. The DiceAgentCardProducer, which defines the AgentCard for our A2A server agent.

  3. 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.

解决方案

The addition of multi-transport support to the A2A Java SDK, as demonstrated in the new Dice Agent sample, is a big step towards creating more flexible, performant, polyglot multi-agent systems.