|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: 'Getting Started with A2A Java SDK and gRPC' |
| 4 | +date: 2025-09-25 |
| 5 | +tags: ai a2a grpc |
| 6 | +synopsis: Let's learn how to create an A2A server agent and an A2A client that can communicate using the gRPC transport. |
| 7 | +author: fjuma |
| 8 | +--- |
| 9 | + |
| 10 | +The ability for AI agents to communicate across different frameworks and languages is key to |
| 11 | +building polyglot multi-agent systems. The recent https://quarkus.io/blog/quarkus-a2a-java-0-3-0-alpha-release/[0.3.0.Alpha1] and https://quarkus.io/blog/quarkus-a2a-java-0-3-0-beta-release/[0.3.0.Beta1] releases of the A2A Java SDK take a |
| 12 | +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. |
| 13 | + |
| 14 | +In this post, we'll demonstrate how to create an A2A server agent and an A2A client that support |
| 15 | +multiple transports, where the gRPC transport will be selected. |
| 16 | + |
| 17 | +== Dice Agent Sample |
| 18 | + |
| 19 | +To see the multi-transport support in action, we're going to take a look at the new |
| 20 | +https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport[Dice Agent] |
| 21 | +sample from the https://github.com/a2aproject/a2a-samples[a2a-samples] repo. |
| 22 | + |
| 23 | +The `DiceAgent` is a simple https://github.com/a2aproject/a2a-samples/blob/main/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgent.java[Quarkus LangChain4j AI service] that can make use of https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceTools.java[tools] to roll dice of different sizes and check if the result of a roll is a prime number. |
| 24 | + |
| 25 | +=== A2A Server Agent |
| 26 | + |
| 27 | +There are three key things in our sample application that turn our Quarkus LangChain4j AI service into an A2A |
| 28 | +server agent: |
| 29 | + |
| 30 | +1. A dependency on at least one A2A Java SDK Server Reference implementation in the server application's |
| 31 | +https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport/server/pom.xml[pom.xml] file. In this sample, we've added dependencies on both `io.github.a2asdk:a2a-java-sdk-reference-grpc` |
| 32 | +and `io.github.a2asdk:a2a-java-sdk-reference-jsonrpc` since we want our A2A server agent to be able to support |
| 33 | +both the gRPC and JSON-RPC transports. |
| 34 | +2. The https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgentCardProducer.java[DiceAgentCardProducer], which defines the `AgentCard` for our A2A server agent. |
| 35 | +3. The https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgentExecutorProducer.java[DiceAgentExecutorProducer], which calls our `DiceAgent` AI service. |
| 36 | + |
| 37 | +Let's look closer at the `DiceAgentCardProducer`: |
| 38 | + |
| 39 | +[source,java] |
| 40 | +---- |
| 41 | +/** |
| 42 | + * Producer for dice agent card configuration. |
| 43 | + */ |
| 44 | +@ApplicationScoped |
| 45 | +public final class DiceAgentCardProducer { |
| 46 | +
|
| 47 | + /** The HTTP port for the agent service. */ |
| 48 | + @Inject |
| 49 | + @ConfigProperty(name = "quarkus.http.port") |
| 50 | + private int httpPort; |
| 51 | +
|
| 52 | + /** |
| 53 | + * Produces the agent card for the dice agent. |
| 54 | + * |
| 55 | + * @return the configured agent card |
| 56 | + */ |
| 57 | + @Produces |
| 58 | + @PublicAgentCard |
| 59 | + public AgentCard agentCard() { |
| 60 | + return new AgentCard.Builder() |
| 61 | + .name("Dice Agent") |
| 62 | + .description( |
| 63 | + "Rolls an N-sided dice and answers questions about the " |
| 64 | + + "outcome of the dice rolls. Can also answer questions " |
| 65 | + + "about prime numbers.") |
| 66 | + .preferredTransport(TransportProtocol.GRPC.asString()) <1> |
| 67 | + .url("localhost:" + httpPort) <2> |
| 68 | + .version("1.0.0") |
| 69 | + .documentationUrl("http://example.com/docs") |
| 70 | + .capabilities( |
| 71 | + new AgentCapabilities.Builder() |
| 72 | + .streaming(true) |
| 73 | + .pushNotifications(false) |
| 74 | + .stateTransitionHistory(false) |
| 75 | + .build()) |
| 76 | + .defaultInputModes(List.of("text")) |
| 77 | + .defaultOutputModes(List.of("text")) |
| 78 | + .skills( |
| 79 | + List.of( |
| 80 | + new AgentSkill.Builder() |
| 81 | + .id("dice_roller") |
| 82 | + .name("Roll dice") |
| 83 | + .description("Rolls dice and discusses outcomes") |
| 84 | + .tags(List.of("dice", "games", "random")) |
| 85 | + .examples(List.of("Can you roll a 6-sided die?")) |
| 86 | + .build(), |
| 87 | + new AgentSkill.Builder() |
| 88 | + .id("prime_checker") |
| 89 | + .name("Check prime numbers") |
| 90 | + .description("Checks if given numbers are prime") |
| 91 | + .tags(List.of("math", "prime", "numbers")) |
| 92 | + .examples( |
| 93 | + List.of("Is 17 a prime number?")) |
| 94 | + .build())) |
| 95 | + .protocolVersion("0.3.0") |
| 96 | + .additionalInterfaces( <3> |
| 97 | + List.of( |
| 98 | + new AgentInterface(TransportProtocol.GRPC.asString(), <4> |
| 99 | + "localhost:" + httpPort), |
| 100 | + new AgentInterface( |
| 101 | + TransportProtocol.JSONRPC.asString(), <5> |
| 102 | + "http://localhost:" + httpPort))) |
| 103 | + .build(); |
| 104 | + } |
| 105 | +} |
| 106 | +
|
| 107 | +---- |
| 108 | +<1> The preferred transport for our A2A server agent, `gRPC` in this sample. This is the transport protocol available at the primary endpoint URL. |
| 109 | +<2> This is the primary endpoint URL for our A2A server agent. Since `gRPC` is our preferred transport and since |
| 110 | +we'll be using the HTTP port for gRPC and JSON-RPC, we're specifying `"localhost:" + httpPort` here. |
| 111 | +<3> We can optionally specify additional interfaces supported by our A2A server agent here. Since we also want |
| 112 | +to support the JSON-RPC transport, we'll be adding that in this section. |
| 113 | +<4> The primary endpoint URL can optionally be specified in the additional interfaces section for completeness. |
| 114 | +<5> The JSON-RPC transport URL. Notice that we're using the HTTP port for both JSON-RPC and gRPC. |
| 115 | + |
| 116 | +==== Port Configuration for the Transports |
| 117 | + |
| 118 | +In the previous section, we mentioned that we're using the HTTP port for both the gRPC and JSON-RPC transports. |
| 119 | +This is configured in our `application.properties` file as shown here: |
| 120 | + |
| 121 | +[source,properties] |
| 122 | +---- |
| 123 | +# Use the same port for gRPC and HTTP |
| 124 | +quarkus.grpc.server.use-separate-server=false |
| 125 | +quarkus.http.port=11000 |
| 126 | +---- |
| 127 | + |
| 128 | +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). |
| 129 | + |
| 130 | +==== Starting the A2A Server Agent |
| 131 | + |
| 132 | +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. |
| 133 | + |
| 134 | +A2A clients can now send queries to our A2A server agent using either the gRPC or JSON-RPC transport. |
| 135 | + |
| 136 | +The complete source code and instructions for starting the server application are available https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport/server[here]. |
| 137 | + |
| 138 | +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. |
| 139 | + |
| 140 | +== A2A Client |
| 141 | + |
| 142 | +The `dice_agent_multi_transport` sample also includes a https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClient.java[TestClient] that can be used to send messages to the Dice Agent. |
| 143 | + |
| 144 | +Notice that the client's https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport/client/pom.xml[pom.xml] file contains dependencies on `io.github.a2asdk:a2a-java-sdk-client` and `io.github.a2asdk:a2a-java-sdk-client-transport-grpc`. |
| 145 | + |
| 146 | +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. |
| 147 | + |
| 148 | +The `a2a-java-sdk-client-transport-grpc` dependency provides the ability for the client to support the gRPC transport. |
| 149 | + |
| 150 | +Let's see how the `TestClient` uses the A2A Java SDK to create a `Client` that supports both gRPC and JSON-RPC: |
| 151 | + |
| 152 | +[source,java] |
| 153 | +---- |
| 154 | +... |
| 155 | +// Fetch the public agent card |
| 156 | +AgentCard publicAgentCard = new A2ACardResolver(serverUrl).getAgentCard(); |
| 157 | +
|
| 158 | +// Create a CompletableFuture to handle async response |
| 159 | +final CompletableFuture<String> messageResponse = new CompletableFuture<>(); |
| 160 | +
|
| 161 | +// Create consumers for handling client events |
| 162 | +List<BiConsumer<ClientEvent, AgentCard>> consumers = getConsumers(messageResponse); |
| 163 | +
|
| 164 | +// Create error handler for streaming errors |
| 165 | +Consumer<Throwable> streamingErrorHandler = (error) -> { |
| 166 | + System.out.println("Streaming error occurred: " + error.getMessage()); |
| 167 | + error.printStackTrace(); |
| 168 | + messageResponse.completeExceptionally(error); |
| 169 | +}; |
| 170 | +
|
| 171 | +// Create channel factory for gRPC transport |
| 172 | +Function<String, Channel> channelFactory = agentUrl -> { |
| 173 | + return ManagedChannelBuilder.forTarget(agentUrl).usePlaintext().build(); |
| 174 | +}; |
| 175 | +
|
| 176 | +ClientConfig clientConfig = new ClientConfig.Builder() |
| 177 | + .setAcceptedOutputModes(List.of("Text")) |
| 178 | + .build(); |
| 179 | +
|
| 180 | +// Create the client with both JSON-RPC and gRPC transport support. |
| 181 | +// The A2A server agent's preferred transport is gRPC, since the client |
| 182 | +// also supports gRPC, this is the transport that will get used |
| 183 | +Client client = Client.builder(publicAgentCard) <1> |
| 184 | + .addConsumers(consumers) <2> |
| 185 | + .streamingErrorHandler(streamingErrorHandler) <3> |
| 186 | + .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) <4> |
| 187 | + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) <5> |
| 188 | + .clientConfig(clientConfig) <6> |
| 189 | + .build(); |
| 190 | +
|
| 191 | +// Create and send the message |
| 192 | +Message message = A2A.toUserMessage(messageText); |
| 193 | +
|
| 194 | +System.out.println("Sending message: " + messageText); |
| 195 | +client.sendMessage(message); <7> |
| 196 | +System.out.println("Message sent successfully. Waiting for response..."); |
| 197 | +
|
| 198 | +try { |
| 199 | + // Wait for response with timeout |
| 200 | + String responseText = messageResponse.get(); |
| 201 | + System.out.println("Final response: " + responseText); |
| 202 | +} catch (Exception e) { |
| 203 | + System.err.println("Failed to get response: " + e.getMessage()); |
| 204 | + e.printStackTrace(); |
| 205 | +} |
| 206 | +... |
| 207 | +---- |
| 208 | +<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. |
| 209 | +<2> We need to specify event consumers that will be used to handle the responses that will be received from the |
| 210 | +A2A server agent. This will be explained in more detail in the next section. |
| 211 | +<3> The A2A client created by the `Client.builder` will automatically send streaming messages, as opposed to |
| 212 | +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. |
| 213 | +<4> We're indicating that we'd like our client to support the gRPC transport. |
| 214 | +<5> We're indicating that we'd like our client to also support the JSON-RPC transport. When communicating with |
| 215 | +an A2A server agent that doesn't support gRPC, this is the transport that would get used. |
| 216 | +<6> We can optionally specify general client configuration and preferences here. |
| 217 | +<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 |
| 218 | +support streaming, the client will send a non-streaming message instead. |
| 219 | + |
| 220 | +=== Defining the Event Consumers |
| 221 | + |
| 222 | +When creating our A2A client, we need to specify event consumers that will be used to handle the responses |
| 223 | +that will be received from the A2A server agent. Let's see how to define a consumer that handles the different |
| 224 | +types of events: |
| 225 | + |
| 226 | +[source,java] |
| 227 | +---- |
| 228 | + private static List<BiConsumer<ClientEvent, AgentCard>> getConsumers( |
| 229 | + final CompletableFuture<String> messageResponse) { |
| 230 | + List<BiConsumer<ClientEvent, AgentCard>> consumers = new ArrayList<>(); |
| 231 | + consumers.add( |
| 232 | + (event, agentCard) -> { |
| 233 | + if (event instanceof MessageEvent messageEvent) { <1> |
| 234 | + Message responseMessage = messageEvent.getMessage(); |
| 235 | + String text = extractTextFromParts(responseMessage.getParts()); |
| 236 | + System.out.println("Received message: " + text); |
| 237 | + messageResponse.complete(text); |
| 238 | + } else if (event instanceof TaskUpdateEvent taskUpdateEvent) { <2> |
| 239 | + UpdateEvent updateEvent = taskUpdateEvent.getUpdateEvent(); |
| 240 | + if (updateEvent |
| 241 | + instanceof TaskStatusUpdateEvent taskStatusUpdateEvent) { <3> |
| 242 | + System.out.println("Received status-update: " |
| 243 | + + taskStatusUpdateEvent.getStatus().state().asString()); |
| 244 | + if (taskStatusUpdateEvent.isFinal()) { |
| 245 | + StringBuilder textBuilder = new StringBuilder(); |
| 246 | + List<Artifact> artifacts |
| 247 | + = taskUpdateEvent.getTask().getArtifacts(); |
| 248 | + for (Artifact artifact : artifacts) { |
| 249 | + textBuilder.append(extractTextFromParts(artifact.parts())); |
| 250 | + } |
| 251 | + String text = textBuilder.toString(); |
| 252 | + messageResponse.complete(text); |
| 253 | + } |
| 254 | + } else if (updateEvent |
| 255 | + instanceof TaskArtifactUpdateEvent taskArtifactUpdateEvent) { <4> |
| 256 | + List<Part<?>> parts = taskArtifactUpdateEvent |
| 257 | + .getArtifact() |
| 258 | + .parts(); |
| 259 | + String text = extractTextFromParts(parts); |
| 260 | + System.out.println("Received artifact-update: " + text); |
| 261 | + } |
| 262 | + } else if (event instanceof TaskEvent taskEvent) { <5> |
| 263 | + System.out.println("Received task event: " |
| 264 | + + taskEvent.getTask().getId()); |
| 265 | + } |
| 266 | + }); |
| 267 | + return consumers; |
| 268 | + } |
| 269 | +---- |
| 270 | +<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. |
| 271 | +<2> This defines how to handle an `UpdateEvent` received from the server agent for a specific task. There are |
| 272 | +two types of `UpdateEvents` that can be received. |
| 273 | +<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()` |
| 274 | +will return `true`. |
| 275 | +<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. |
| 276 | +<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. |
| 277 | + |
| 278 | +=== Transport Selection |
| 279 | + |
| 280 | +When creating our `Client`, we used the `withTransport` method to specify that we want the client |
| 281 | +to support both gRPC and JSON-RPC, in that order. The `Client.builder` selects the appropriate |
| 282 | +transport protocol to use based on information obtained from the A2A server agent's `AgentCard`, |
| 283 | +taking into account the transports configured for the client. In this sample application, because |
| 284 | +the server agent's preferred transport is gRPC, the gRPC transport will be used. |
| 285 | + |
| 286 | +=== Using the A2A Client |
| 287 | + |
| 288 | +The sample application contains a `TestClientRunner` that can be run using `JBang`: |
| 289 | + |
| 290 | +[source,shell] |
| 291 | +---- |
| 292 | +jbang TestClientRunner.java |
| 293 | +---- |
| 294 | + |
| 295 | +You should see output similar to this: |
| 296 | + |
| 297 | +[source,shell] |
| 298 | +---- |
| 299 | +Connecting to dice agent at: http://localhost:11000 |
| 300 | +Successfully fetched public agent card: |
| 301 | +... |
| 302 | +Sending message: Can you roll a 5 sided die? |
| 303 | +Message sent successfully. Waiting for response... |
| 304 | +Received status-update: submitted |
| 305 | +Received status-update: working |
| 306 | +Received artifact-update: Sure! I rolled a 5 sided die and got a 3. |
| 307 | +Received status-update: completed |
| 308 | +Final response: Sure! I rolled a 5 sided die and got a 3. |
| 309 | +---- |
| 310 | + |
| 311 | +You can also experiment with sending different messages to the A2A server agent using the `--message` option |
| 312 | +as follows: |
| 313 | + |
| 314 | +[source,shell] |
| 315 | +---- |
| 316 | +jbang TestClientRunner.java --message "Can you roll a 13-sided die and check if the result is a prime number?" |
| 317 | +Connecting to dice agent at: http://localhost:11000 |
| 318 | +Successfully fetched public agent card: |
| 319 | +... |
| 320 | +Sending message: Can you roll a 13-sided die and check if the result is a prime number? |
| 321 | +Message sent successfully. Waiting for response... |
| 322 | +Received status-update: submitted |
| 323 | +Received status-update: working |
| 324 | +Received artifact-update: I rolled a 13 sided die and got a 3. 3 is a prime number. |
| 325 | +Received status-update: completed |
| 326 | +Final response: I rolled a 13 sided die and got a 3. 3 is a prime number. |
| 327 | +---- |
| 328 | + |
| 329 | +The complete source code and instructions for starting the client are available https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport/client[here]. There |
| 330 | +are also details on how to use an A2A client that uses the A2A Python SDK instead of the A2A Java SDK |
| 331 | +to communicate with our A2A server agent. |
| 332 | + |
| 333 | +== Conclusion |
| 334 | + |
| 335 | +The addition of multi-transport support to the A2A Java SDK, as demonstrated in the new Dice Agent |
| 336 | +sample, is a big step towards creating more flexible, performant, polyglot multi-agent systems. |
| 337 | + |
| 338 | +=== Further Reading |
| 339 | + |
| 340 | +* https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/dice_agent_multi_transport[Dice Agent Sample] |
| 341 | +* https://quarkus.io/blog/quarkus-a2a-java-0-3-0-alpha-release/[Getting Started with Quarkus and A2A Java SDK 0.3.0] |
| 342 | +* https://quarkus.io/blog/quarkus-a2a-java-0-3-0-beta-release/[A2A Java SDK: Support for the REST Transport is Now Here] |
| 343 | +* https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents[A2A Java SDK Samples] |
| 344 | +* https://github.com/a2aproject/a2a-java/blob/main/README.md[A2A Java SDK Documentation] |
| 345 | +* https://a2a-protocol.org/latest/specification/[A2A Specification] |
| 346 | + |
| 347 | + |
0 commit comments