Build Your First AI Agent in Java: Quarkus, Langchain4j, and the A2A SDK
How to create a summarizing agent that speaks the A2A protocol and harnesses local or cloud LLMs
AI Agents aren't just hype. They're the foundation for the next generation of autonomous and semi-autonomous applications. In this tutorial, you'll build your first agent using modern Java tooling: Quarkus for blazing-fast cloud-native development, Langchain4j for easy LLM interaction, and the A2A Agent SDK, a promising new protocol for peer-to-peer software agent communication.
We’ll build a real, functioning agent that accepts a block of text and returns a concise summary using an LLM. And by the end, you'll understand what it means to expose AI capabilities in a way that's interoperable, reactive, and easy to test.
Before I forget it: Thank you
for kicking me off into the right direction with this tutorial when I was stuck!What You'll Build
You’ll create a "Summarization Agent" that does four things:
Exposes itself as a capable A2A-compliant agent
Accepts a text payload via the
/a2a
endpointUses Langchain4j to summarize the input using an LLM
Returns the summary in a structured A2A response
Prerequisites
Before diving in, make sure you have:
JDK 17+
Apache Maven 3.8+
A Java IDE (e.g., IntelliJ, VS Code)
A local model via Ollama (or just the Quarkus Dev Service)
Bootstrap the Quarkus Project
Open your terminal and scaffold a new project:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=summarization-agent \
-DclassName="com.example.SummarizationResource" \
-Dpath="/summarize"
-Dextensions="quarkus-rest-jackson, quarkus-langchain4j-ollama,quarkus-smallrye-openapi"
cd summarization-agent
You need to manually add the following dependency to your pom.xml after you created the project.
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-server-quarkus</artifactId>
<version>0.2.3.Beta1</version>
</dependency>
Configure Your LLM
Configure your local Olama based LLM in src/main/resources/application.properties
:
quarkus.langchain4j.ollama.chat-model.model-id=llama3.2:latest
quarkus.langchain4j.ollama.timeout=60s
quarkus.swagger-ui.always-include=true
quarkus.log.category."io.a2a".level=DEBUG
quarkus.rest-client.logging.scope=ALL
# Binding Quarkus explicitly
quarkus.http.host=0.0.0.0
Create the Summarization Agent
Langchain4j with Quarkus makes AI integration look like a normal Java interface. Create SummarizationAgent.java
:
package com.example;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import jakarta.enterprise.context.ApplicationScoped;
@RegisterAiService
@ApplicationScoped
public interface SummarizationAgent {
@SystemMessage("You are a professional text summarizer. Your task is to provide a concise summary of the given text.")
@UserMessage("Summarize the following text: {text}")
String summarize(String text);
}
This is a simple interface that defines a summarize
method. The @RegisterAiService
annotation tells Quarkus to create a proxy for this service that will interact with the configured LLM. The @SystemMessage
and @UserMessage
annotations provide instructions to the LLM. The @ApplicationScoped
annotation makes it a CDI bean.
Define the Agent
Now it's time to create our agent. In the A2A world, an agent has two main components:
Agent Card: This is like a business card for the agent. It contains information about the agent, such as its name, description, and capabilities.
Agent Executor: This is the brain of the agent. It contains the logic for how the agent should handle requests.
Let's create a new class called SummarizationAgentCardProducer
in the src/main/java/com/example
directory:
package com.example;
import java.util.Collections;
import io.a2a.server.PublicAgentCard;
import io.a2a.spec.AgentCapabilities;
import io.a2a.spec.AgentCard;
import io.a2a.spec.AgentSkill;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
@ApplicationScoped
public class SummarizationAgentCardProducer {
@Produces
@PublicAgentCard
public AgentCard agentCard() {
Log.info("agentCard() called");
return new AgentCard.Builder()
.name("Summarization Agent")
.description("An agent that summarizes text.")
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.url("http://host.containers.internal:8080/")
.version("1.0.0")
.capabilities(new AgentCapabilities.Builder()
.streaming(false)
.pushNotifications(false)
.stateTransitionHistory(false)
.build())
.skills(Collections.singletonList(new AgentSkill.Builder()
.id("summarize_text")
.name("Summarize Text")
.description("Summarizes the provided text.")
.tags(Collections.singletonList("text_summarization"))
.build()))
.build();
}
}
This class registers your agent with the A2A runtime and defines how it handles requests. One thing is important here. I am binding the agent to
http://host.containers.internal:8080/
. This is necessary for testing with the Google A2A-inspector later on. Usually, you would set this to the host url of your agent.
And now we need the Agent executor. Create SummarizationAgentExecutorProducer
in the src/main/java/com/example
directory:
package com.example;
import java.util.List;
import io.a2a.server.agentexecution.AgentExecutor;
import io.a2a.server.agentexecution.RequestContext;
import io.a2a.server.events.EventQueue;
import io.a2a.server.tasks.TaskUpdater;
import io.a2a.spec.JSONRPCError;
import io.a2a.spec.Message;
import io.a2a.spec.Part;
import io.a2a.spec.Task;
import io.a2a.spec.TaskNotCancelableError;
import io.a2a.spec.TaskState;
import io.a2a.spec.TextPart;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
@ApplicationScoped
public class SummarizationAgentExecutorProducer {
@Inject
SummarizationAgent summarizationAgent;
@Produces
public AgentExecutor agentExecutor() {
Log.info("agentExecutor() called");
return new SummarizationAgentExecutor(summarizationAgent);
}
private static class SummarizationAgentExecutor implements AgentExecutor {
private final SummarizationAgent summarizationAgent;
public SummarizationAgentExecutor(SummarizationAgent summarizationAgent) {
Log.info("SummarizationAgentExecutor() called");
this.summarizationAgent = summarizationAgent;
}
@Override
public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
Log.infof("execute() called %s", context.getTaskId());
TaskUpdater updater = new TaskUpdater(context, eventQueue);
// mark the task as submitted and start working on it
if (context.getTask() == null) {
updater.submit();
}
updater.startWork();
// extract the text from the message
String userMessage = extractTextFromMessage(context.getMessage());
// call the summarization agent
String response = summarizationAgent.summarize(userMessage);
// create the response part
TextPart responsePart = new TextPart(response, null);
List<Part<?>> parts = List.of(responsePart);
// add the response as an artifact and complete the task
updater.addArtifact(parts, null, null, null);
updater.complete();
}
@Override
public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
Log.infof("cancel() called %s", context.getTaskId());
Task task = context.getTask();
if (task.getStatus().state() == TaskState.CANCELED) {
// task already cancelled
throw new TaskNotCancelableError();
}
if (task.getStatus().state() == TaskState.COMPLETED) {
// task already completed
throw new TaskNotCancelableError();
}
// cancel the task
TaskUpdater updater = new TaskUpdater(context, eventQueue);
updater.cancel();
}
private String extractTextFromMessage(Message message) {
Log.infof("extractTextFromMessage() called %s", message.getTaskId());
StringBuilder textBuilder = new StringBuilder();
if (message.getParts() != null) {
for (Part<?> part : message.getParts()) {
if (part instanceof TextPart textPart) {
textBuilder.append(textPart.getText());
}
}
}
return textBuilder.toString();
}
}
}
A2A Endpoint Activation
The a2a-java-sdk-server-quarkus
dependency automatically exposes your agent at:
POST /a2a
No need to write a controller. This is handled via JAX-RS behind the scenes.
Test Your Agent
This is getting a little more tricky, but you can manage. We are going to use Google’s A2A-inspector tool. Clone it locally and build a container that we can run. Because, we don’t really want to install Python or Node on our systems, don’t we?
git clone https://github.com/a2aproject/a2a-inspector.git
cd a2a-inspector
Now let Podman do the container build:
podman build -t a2a-inspector .
And when that is done, all you have to do is to launch it locally.
podman run -d -p 8081:8080 a2a-inspector
Note that I have changed the external port hier because we are going to run our Agent on port 8080 already. Check if the inspector is running by going to: http://localhost:8081/. You should see something like this:
Now it’s time to start our Agent:
./mvnw quarkus:dev
Go to the A2A inspector and enter: host.containers.internal:8080 as the URL. This is the “standard” url that Podman exposes the host (at least on machines where it uses a VM internally).
Now you can input a message. For example:
Shorten below text: Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of \"de Finibus Bonorum et Malorum\" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, \"Lorem ipsum dolor sit amet..\", comes from a line in section 1.10.32. The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from \"de Finibus Bonorum et Malorum\" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
You can send it and inspect the output in the debug console:
{
"artifacts": [
{
"artifactId": "2a0cd14f-74b6-4ef3-afee-3a27cf2abfa9",
"parts": [
{
"kind": "text",
"text": "Here's a shortened version:\n\nLorem Ipsum is not random text, but rather an ancient Latin phrase dating back to 45 BC. A professor discovered its connection to Cicero's book \"The Extremes of Good and Evil\". The first line of Lorem Ipsum comes from section 1.10.32 of this work."
}
]
}
]
Just like that, your AI agent is live and answering your calls.
What's Next?
Now that you’ve built and exposed your first Java agent, here’s where you can go:
Add more capabilities to your agent by defining new
@UserMessage
methodsAllow it to call other tools or agents
Chain multiple agents into workflows using A2A routing
Agents are a powerful abstraction that unlock real-time, goal-driven, distributed AI programming. And you’ve just built one in Java.