Calling A2A Services
Any ASTRA agent can call an A2A service. It needs no server of its own: the A2A module sends the request on a background thread and delivers the outcome to the agent as an event. This page calls the greeter service from an ASTRA agent, then uses a remote agent as a ReAct tool.
The project
The caller is an ordinary ASTRA project. Add astra-langchain4j-tools to the POM, together with a matching jackson-databind:
<parent>
<groupId>com.astralanguage</groupId>
<artifactId>astra-base</artifactId>
<version>2.6.1</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>com.astralanguage</groupId>
<artifactId>astra-langchain4j-tools</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.20.1</version>
</dependency>
</dependencies>
Without the explicit jackson-databind dependency, Maven can resolve an older Jackson version than the library needs, and the ASTRA compiler reports a NoClassDefFoundError for a Jackson class.
Send a request
Save this file as src/main/astra/Main.astra:
import com.fasterxml.jackson.databind.JsonNode;
agent Main {
module Console console;
module System S;
module astra.langchain4j.tools.a2a.A2A a2a;
module astra.langchain4j.tools.JSONConverter json;
types calls {
formula reply(string, boolean, string);
}
rule +!main(list args) {
JsonNode payload = json.with(json.object(), "name", "Ada");
a2a.send("http://localhost:8080", "greet", json.toJsonString(payload), string call);
// Suspend this intention until the reply for this call arrives
wait(reply(call, boolean ok, string result));
-reply(call, ok, result);
if (ok) {
console.println(json.valueAsString(result, "/greeting"));
} else {
console.println("The request failed: " + result);
}
S.exit();
}
rule $a2a.response(string call, string task, string result) {
+reply(call, true, result);
}
rule $a2a.failure(string call, string task, string reason) {
+reply(call, false, reason);
}
rule $a2a.progress(string call, string task, string text) {
console.println("Progress: " + text);
}
}
With the greeter service running, run the caller with mvn. The output ends with:
[main] Hello, Ada!
It may be preceded by [main] Progress: Preparing a greeting. The client reports progress it sees while polling the task, and the greeter often completes before the first poll.
If the service is not running, the caller prints The request failed: Failed to obtain agent card.
How the call works
send(url, skill, json, call) returns immediately, binding call to a correlation identifier for the request. It fetches the service’s AgentCard, checks that the card advertises the skill, sends the JSON object as a data part, and polls the task until it ends. The skill argument can be omitted, as in send(url, json, call), to use the service’s default skill.
The outcome arrives as one of three events, each carrying the correlation identifier and the remote task identifier:
Event |
When |
|---|---|
|
The task completed. |
|
The task failed or was canceled, the service could not be reached, or the card does not advertise the skill |
|
The service reported progress. There may be any number of these before the response or failure |
The module name forms the event’s prefix: with module ... A2A transport;, the events are $transport.response(...) and so on.
Network I/O runs on the library’s own threads, so the agent keeps running while the request is in flight. The pattern above turns the events into a belief and uses wait(...) to suspend only the intention that needs the reply. Other intentions, including ones that send further requests, carry on. Because the belief includes the correlation identifier, each waiting intention resumes on its own reply. Remove the belief once it has been used.
A client gives up on a task that has not finished after 65 seconds and reports a failure. At most four requests are in flight at once and sixteen more can be queued; a request beyond that fails immediately with “A2A I/O is at capacity”.
Reading results
Results are JSON strings. JSONConverter reads them with JSON Pointer paths:
string greeting = json.valueAsString(result, "/greeting");
if (json.hasField(result, "/items")) {
int count = json.length(result, "/items");
string first = json.valueAsString(result, "/items/0/name");
}
The typed readers (valueAsString, valueAsInt, valueAsDouble, valueAsBoolean) fail if the field is missing, null or of the wrong type. Use hasField(...) and isNull(...) to test first, or handle the failure with try/recover. Invalid JSON, duplicate fields and trailing content also cause a failure, rather than being silently accepted. Numbers sent through A2A may arrive as floating-point values, which valueAsInt rejects; read them with valueAsDouble.
A2A agents as ReAct tools
A ReAct tool is a plan, so a tool can call a remote agent. The model decides when to delegate, and the remote agent can be built with any A2A framework. This coordinator, adapted from the A2A example project, offers the model a remote_solver tool:
import astra.langchain4j.reasoning.*;
import com.fasterxml.jackson.databind.JsonNode;
agent Coordinator extends ReAct {
module System S;
module astra.langchain4j.tools.a2a.A2A a2a;
module astra.langchain4j.tools.JSONConverter json;
types coordination {
formula remoteResult(string, boolean, string);
}
initial tool("remote_solver", "Delegate a mathematical question to a remote A2A solver.");
rule +!main(list args) {
try {
model.loadApiKeyFromEnv("OPENAI_API_KEY");
model.initialize("gpt-4o-mini",
"astra.langchain4j.configuration.OpenAiConfiguration", []);
!solve("What is 6 * 7?", string answer);
console.println("Final answer: " + answer);
} recover {
console.println("The coordinator failed.");
}
S.exit();
}
rule +!invoke("remote_solver", string question, string observation) {
JsonNode payload = json.with(json.object(), "question", question);
a2a.send("http://localhost:8080", json.toJsonString(payload), string callId);
wait(remoteResult(callId, boolean success, string response));
-remoteResult(callId, success, response);
if (success) {
observation = json.valueAsString(response, "/answer");
} else {
observation = "Remote A2A request failed: " + response;
}
}
rule $a2a.response(string callId, string taskId, string payload) {
+remoteResult(callId, true, payload);
}
rule $a2a.failure(string callId, string taskId, string reason) {
+remoteResult(callId, false, reason);
}
rule $a2a.progress(string callId, string taskId, string text) {
console.println(text);
}
}
The remote_solver tool expects a service whose default skill reads a question field and returns an answer field, as the example project’s solver does. A failed call becomes an observation rather than a failure, so the model can try another approach.
The example project runs the solver as a service and the coordinator as a separate program, and uses a scripted model so that both run without an API key. It also tests the coordinator against a service written directly with the A2A Java SDK, and the service against non-ASTRA A2A clients.
Where next?
The API reference lists the A2A and JSON operations.
The sample projects include the complete A2A example.