Publishing an A2A Service
This page publishes an ASTRA agent as an A2A service. The agent has one skill, greet, which returns a greeting for a name. It needs no model or API key, so the A2A parts can be seen on their own. The last part of the page puts a reasoning agent behind a skill.
The project
An A2A service runs inside a Quarkus application, which provides the HTTP server and the A2A Java SDK’s JSON-RPC endpoint. The project needs:
the
astra-langchain4j-toolsandastra-apisdependencies;the Quarkus BOM and the
quarkus-resteasydependency;the
astra-maven-plugin, to compile the ASTRA code, and thequarkus-maven-plugin, to build the application.
The essential parts of pom.xml are:
<properties>
<maven.compiler.release>17</maven.compiler.release>
<astra.version>2.6.1</astra.version>
<quarkus.version>3.39.2</quarkus.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.astralanguage</groupId>
<artifactId>astra-langchain4j-tools</artifactId>
<version>${astra.version}</version>
</dependency>
<dependency>
<groupId>com.astralanguage</groupId>
<artifactId>astra-apis</artifactId>
<version>${astra.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
</dependencies>
The <build> section adds src/main/astra as a resource directory and declares the two plugins; see the downloadable project for the complete file. The project does not use an ASTRA parent POM, because the application is started by Quarkus rather than by astra:run.
Configure the service
src/main/resources/application.properties tells the library which ASTRA class implements the service, and installs the library’s request handling in the A2A server:
# Listen on the local machine only: this example has no authentication.
quarkus.http.host=127.0.0.1
quarkus.http.port=8080
a2a.authorization.required=false
# The ASTRA class that declares the service and handles its skills
astra.a2a.agent-class=greeter.Greeter
# The URL advertised in the AgentCard
astra.a2a.url=http://localhost:8080
# Install the ASTRA request handling in the A2A server
%prod.quarkus.arc.selected-alternatives=astra.langchain4j.tools.a2a.AgentExecutorProducer,astra.langchain4j.tools.a2a.AgentCardProducer,astra.langchain4j.tools.a2a.AsyncRequestHandler
%dev.quarkus.arc.selected-alternatives=astra.langchain4j.tools.a2a.AgentExecutorProducer,astra.langchain4j.tools.a2a.AgentCardProducer,astra.langchain4j.tools.a2a.AsyncRequestHandler
quarkus.class-loading.reloadable-artifacts=com.astralanguage:astra-langchain4j,com.astralanguage:astra-langchain4j-tools
astra.a2a.url is the address clients are told to use. When the service runs in a container or behind a proxy, set it to an address that clients can reach; localhost inside a container refers to the container.
Write the service agent
Save this file as src/main/astra/greeter/Greeter.astra:
package greeter;
import com.fasterxml.jackson.databind.JsonNode;
agent Greeter {
module astra.langchain4j.tools.a2a.A2A a2a;
module astra.langchain4j.tools.JSONConverter json;
// Declare the AgentCard and the skills this agent handles
rule +!main(["describe"]) {
try {
a2a.describe("ASTRA greeter", "Greets people by name.", "0.1.0",
["text/plain", "application/json"], ["application/json"]);
a2a.skill("greet", "Greet",
"Returns a greeting for a name sent as text or as a JSON name field.",
["greeting"], ["Ada"]);
a2a.publish("greet");
} recover {
a2a.registrationFailed("Invalid A2A declarations");
}
}
// Each request is handled by a new worker agent
rule +!main([string id, string contextId, string skill, string payload]) {
a2a.listen(id, contextId, skill, payload);
}
rule $a2a.request(string id, string contextId, "greet", string payload) {
try {
a2a.progress(id, "Preparing a greeting");
string name = "";
if (json.hasField(payload, "/name")) {
name = json.valueAsString(payload, "/name");
} else {
name = json.valueAsString(payload, "/text");
}
JsonNode result = json.with(json.object(), "greeting", "Hello, " + name + "!");
a2a.complete(id, json.toJsonString(result));
} recover {
a2a.fail(id, "Send a name as text or as a JSON name field");
}
}
}
The class has three rules, one for each role an instance can play:
Describing the service. The instance created at start-up receives
!main(["describe"]).describe(...)sets the card’s name, description, version, input content types and output content types.skill(...)declares a skill with an identifier, name, description, tags and example inputs.publish(...)publishes the card, naming the default skill used when a request does not select one. If the declarations are invalid,registrationFailed(...)stops the service from starting.Receiving a request. Each worker receives
!main([id, contextId, skill, payload]).listen(...)turns these into a$a2a.request(...)event. Handling requests as events lets the class choose a rule for each skill by matching on the skill identifier.Handling a request. The
$a2a.requestrule reads the name from the JSON payload, reports progress and completes the task with a JSON object. If anything fails, such as a payload with neither anamenor atextfield,fail(...)ends the task with a reason.
Every task must end with exactly one call to complete(...) or fail(...). A task that does neither fails when its 60-second deadline passes.
JSONConverter reads the payload with JSON Pointer paths such as /name, and builds the result immutably: with(...) returns a new object with one more field. The API reference lists its operations.
Note
ASTRA reserves the FIPA performatives, such as request, inform and agree, as keywords, so they cannot be used as variable names. Use names such as payload and result for JSON values.
Build and run
Build the application and start it:
mvn clean package
java -jar target/quarkus-app/quarkus-run.jar
When Quarkus reports that it is listening on http://127.0.0.1:8080, the service is ready. Stop it with Ctrl+C.
Try it with curl
The AgentCard is an ordinary JSON document:
curl http://localhost:8080/.well-known/agent-card.json
The reply includes the declarations made by the Greeter agent (some fields are omitted here):
{
"name": "ASTRA greeter",
"description": "Greets people by name.",
"version": "0.1.0",
"capabilities": { "streaming": true, "pushNotifications": false, ... },
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["application/json"],
"skills": [
{ "id": "greet", "name": "Greet", "description": "Returns a greeting ...",
"tags": ["greeting"], "examples": ["Ada"] }
],
"supportedInterfaces": [
{ "protocolBinding": "JSONRPC", "url": "http://localhost:8080", "protocolVersion": "1.0" }
],
...
}
To send a request, post a JSON-RPC SendMessage call with the A2A-Version header. This one sends a text part:
curl -X POST http://localhost:8080 \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":
{"role":"ROLE_USER","messageId":"m1","parts":[{"text":"Ada"}]}}}'
The reply contains the completed task. Its history holds the progress message and its artifact holds the result (some fields are omitted here):
{
"jsonrpc": "2.0", "id": 1,
"result": { "task": {
"id": "dcbf867d-...",
"status": { "state": "TASK_STATE_COMPLETED" },
"artifacts": [ { "parts": [ { "data": { "greeting": "Hello, Ada!" } } ],
"metadata": { "skill": "greet" } } ],
"history": [ { "role": "ROLE_AGENT",
"parts": [ { "text": "Preparing a greeting" } ] } ]
} }
}
A data part is passed to the agent unchanged, so {"data":{"name":"Grace"}} in place of the text part returns Hello, Grace!. A data part without a name field, such as {"data":{"age":3}}, produces a task in the TASK_STATE_FAILED state, whose status message is the reason given to fail(...).
A request selects a skill other than the default by including "metadata":{"skill":"<id>"} in the SendMessage parameters. A request for an undeclared skill is rejected.
Inspect the service
The A2A Inspector is a web-based debugging tool for A2A agents. It shows an agent’s card, validates it, and lets you send messages and watch the task events. Enter http://localhost:8080 as the agent URL.
The astra.langchain4j.tools.a2a.Inspector module starts and stops a prepared checkout of the Inspector from an ASTRA agent:
agent Main {
module Console console;
module System S;
module astra.langchain4j.tools.a2a.Inspector inspector;
rule +!main(list args) {
try {
inspector.start("/absolute/path/to/a2a-inspector", 5001);
console.println("A2A Inspector: " + inspector.url());
inspector.awaitExit();
} recover {
console.println("Unable to run A2A Inspector.");
}
inspector.stop();
S.exit();
}
}
The module does not install the Inspector. Prepare the checkout first, following the Inspector’s instructions or those in the A2A example project, which includes a pinned copy. The module binds the Inspector to the local machine and stops it when the agent stops it or the program exits.
Put a reasoning agent behind a skill
A service agent can extend any ASTRA class, including the reasoning agents. The worker that handles a request initializes the model and adopts !solve(...), then completes the task with the answer:
package greeter;
import astra.langchain4j.reasoning.*;
import com.fasterxml.jackson.databind.JsonNode;
agent Answerer extends ZeroShotChainOfThought {
module astra.langchain4j.tools.a2a.A2A a2a;
module astra.langchain4j.tools.JSONConverter json;
rule +!main(["describe"]) {
try {
a2a.describe("ASTRA answerer", "Answers questions step by step.", "0.1.0",
["text/plain", "application/json"], ["application/json"]);
a2a.skill("answer", "Answer a question",
"Answers a question sent as text, reasoning step by step.",
["reasoning"], ["How many legs do three spiders have?"]);
a2a.publish("answer");
} recover {
a2a.registrationFailed("Invalid A2A declarations");
}
}
rule +!main([string id, string contextId, string skill, string payload]) {
a2a.listen(id, contextId, skill, payload);
}
rule $a2a.request(string id, string contextId, "answer", string payload) {
try {
a2a.progress(id, "Reasoning about the question");
model.loadApiKeyFromEnv("OPENAI_API_KEY");
model.initialize("gpt-4o-mini",
"astra.langchain4j.configuration.OpenAiConfiguration", []);
!solve(json.valueAsString(payload, "/text"), string answer);
JsonNode result = json.with(json.object(), "answer", answer);
a2a.complete(id, json.toJsonString(result));
} recover {
a2a.fail(id, "The question could not be answered");
}
}
}
To publish it, save the file as src/main/astra/greeter/Answerer.astra, set astra.a2a.agent-class=greeter.Answerer and set OPENAI_API_KEY in the environment of the service. Because each request has its own worker, each initializes its own model. Requests are answered concurrently, up to the limit of four tasks; a reasoning strategy that makes many calls must finish within the 60-second deadline.
The A2A example project publishes a MultiStepChainOfThought solver in the same way, with two skills, and reads the model settings from system properties.
Where next?
Calling A2A services calls this service from an ASTRA agent.