Skip to content

A2A.agentTools

Java · package io.github.muthuishere:toolnexus · SPEC §7A · A2A.java

public static List<Tool> agentTools(A2A.Agent ag) throws Exception

GETs the Agent Card at ag.card(), reads its skills[], and returns one Tool per skill — named sanitize(card.name) + "_" + sanitize(skill.id), source:"a2a", with a fixed one-field input schema {task: string}. Calling the tool performs one JSON-RPC SendMessage against card.url (falling back to the card’s origin), then polls GetTask until the remote Task reaches a terminal state.

When you want the raw List<Tool> a peer advertises — to inspect them, filter them, or hand them somewhere other than a Toolkit (a custom router, a different aggregator). It throws on a network/parse failure, so callers that want isolation (one bad peer never breaking the rest) should catch around it — which is exactly what Toolkit.Options.agents(...) does for you.

1. The smallest useful call — one skill, one tool

Section titled “1. The smallest useful call — one skill, one tool”
import com.sun.net.httpserver.HttpServer;
import io.github.muthuishere.toolnexus.*;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class Example {
public static void main(String[] args) throws Exception {
HttpServer peer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
peer.createContext("/.well-known/agent-card.json", ex -> {
String card = "{\"name\":\"reviewer\",\"skills\":[{\"id\":\"review\",\"description\":\"Review some code\"}]}";
send(ex, 200, card);
});
peer.start();
int port = peer.getAddress().getPort();
try {
List<Tool> tools = A2A.agentTools(
A2A.agent("http://127.0.0.1:" + port + "/.well-known/agent-card.json"));
if (tools.size() != 1) throw new AssertionError("expected 1 tool: " + tools.size());
Tool review = tools.get(0);
if (!review.name().equals("reviewer_review")) throw new AssertionError(review.name());
if (!review.source().equals("a2a")) throw new AssertionError(review.source());
if (!review.inputSchema().get("required").equals(List.of("task"))) {
throw new AssertionError(review.inputSchema());
}
System.out.println("ok: " + review.name());
} finally {
peer.stop(0);
}
}
private static void send(com.sun.net.httpserver.HttpExchange ex, int status, String body) {
try {
byte[] b = body.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().add("Content-Type", "application/json");
ex.sendResponseHeaders(status, b.length);
try (OutputStream os = ex.getResponseBody()) { os.write(b); }
} catch (Exception ignored) { }
}
}

2. The realistic case — a full submit→poll round trip

Section titled “2. The realistic case — a full submit→poll round trip”

The stub Task starts working, then flips to completed on the second GetTask poll — proving agentTools actually polls rather than trusting the first response.

import com.sun.net.httpserver.HttpServer;
import io.github.muthuishere.toolnexus.*;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class Example {
public static void main(String[] args) throws Exception {
AtomicInteger polls = new AtomicInteger();
HttpServer peer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
peer.createContext("/", ex -> {
try {
String path = ex.getRequestURI().getPath();
if ("/.well-known/agent-card.json".equals(path)) {
send(ex, 200, "{\"name\":\"planner\",\"skills\":["
+ "{\"id\":\"plan\",\"description\":\"Plan a task\"}]}");
return;
}
String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
Map<String, Object> rpc = Json.toMap(body);
Object id = rpc.get("id");
String method = String.valueOf(rpc.get("method"));
if ("SendMessage".equals(method)) {
send(ex, 200, "{\"jsonrpc\":\"2.0\",\"id\":\"" + id + "\",\"result\":"
+ "{\"id\":\"t1\",\"status\":{\"state\":\"working\"}}}");
} else {
int n = polls.incrementAndGet();
String state = n < 2 ? "working" : "completed";
String extra = n < 2 ? "" : ",\"artifacts\":[{\"parts\":[{\"kind\":\"text\",\"text\":\"3 steps planned\"}]}]";
send(ex, 200, "{\"jsonrpc\":\"2.0\",\"id\":\"" + id + "\",\"result\":"
+ "{\"id\":\"t1\",\"status\":{\"state\":\"" + state + "\"}" + extra + "}}");
}
} catch (Exception e) {
send(ex, 500, "err");
}
});
peer.start();
int port = peer.getAddress().getPort();
try {
List<Tool> tools = A2A.agentTools(A2A.agent(
"http://127.0.0.1:" + port + "/.well-known/agent-card.json", null, 5_000L, 10L));
Tool plan = tools.get(0);
ToolResult r = plan.execute(Map.of("task", "ship the release"), null);
if (r.isError()) throw new AssertionError(r.output());
if (!r.output().equals("3 steps planned")) throw new AssertionError(r.output());
if (polls.get() < 2) throw new AssertionError("expected at least 2 polls, got " + polls.get());
if (!"completed".equals(r.metadata().get("state"))) throw new AssertionError(r.metadata());
System.out.println("ok: " + r.output() + " after " + polls.get() + " poll(s)");
} finally {
peer.stop(0);
}
}
private static void send(com.sun.net.httpserver.HttpExchange ex, int status, String body) {
try {
byte[] b = body.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().add("Content-Type", "application/json");
ex.sendResponseHeaders(status, b.length);
try (OutputStream os = ex.getResponseBody()) { os.write(b); }
} catch (Exception ignored) { }
}
}

3. The full surface — multiple skills, and a failed Task maps to isError

Section titled “3. The full surface — multiple skills, and a failed Task maps to isError”

failed/canceled map to isError:true output that carries the remote’s own status message text — never a thrown exception once the tool is resolved.

import com.sun.net.httpserver.HttpServer;
import io.github.muthuishere.toolnexus.*;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
public class Example {
public static void main(String[] args) throws Exception {
HttpServer peer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
peer.createContext("/", ex -> {
try {
String path = ex.getRequestURI().getPath();
if ("/.well-known/agent-card.json".equals(path)) {
send(ex, 200, "{\"name\":\"reviewer\",\"skills\":["
+ "{\"id\":\"review\",\"description\":\"Review some code\"},"
+ "{\"id\":\"fail\",\"description\":\"Always fails\"}]}");
return;
}
String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
Map<String, Object> rpc = Json.toMap(body);
Object id = rpc.get("id");
String method = String.valueOf(rpc.get("method"));
if ("SendMessage".equals(method)) {
send(ex, 200, "{\"jsonrpc\":\"2.0\",\"id\":\"" + id + "\",\"result\":"
+ "{\"id\":\"t1\",\"status\":{\"state\":\"submitted\"}}}");
} else {
send(ex, 200, "{\"jsonrpc\":\"2.0\",\"id\":\"" + id + "\",\"result\":"
+ "{\"id\":\"t1\",\"status\":{\"state\":\"failed\",\"message\":"
+ "{\"role\":\"agent\",\"parts\":[{\"kind\":\"text\",\"text\":\"diff would not apply\"}]}}}}");
}
} catch (Exception e) {
send(ex, 500, "err");
}
});
peer.start();
int port = peer.getAddress().getPort();
try {
List<Tool> tools = A2A.agentTools(A2A.agent(
"http://127.0.0.1:" + port + "/.well-known/agent-card.json", null, 5_000L, 5L));
if (tools.size() != 2) throw new AssertionError("expected 2 skills: " + tools.size());
Tool fail = tools.stream().filter(t -> t.name().equals("reviewer_fail")).findFirst().orElseThrow();
ToolResult r = fail.execute(Map.of("task", "please fail"), null);
if (!r.isError()) throw new AssertionError("expected isError");
if (!r.output().contains("diff would not apply")) throw new AssertionError(r.output());
if (!"failed".equals(r.metadata().get("state"))) throw new AssertionError(r.metadata());
System.out.println("ok: " + r.output());
} finally {
peer.stop(0);
}
}
private static void send(com.sun.net.httpserver.HttpExchange ex, int status, String body) {
try {
byte[] b = body.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().add("Content-Type", "application/json");
ex.sendResponseHeaders(status, b.length);
try (OutputStream os = ex.getResponseBody()) { os.write(b); }
} catch (Exception ignored) { }
}
}
Member Type What it is
agentTools(ag) List<Tool> Fetch the card, one Tool per skills[] entry. Throws on network/parse failure.
tool name String sanitize(card.name) + "_" + sanitize(skill.id ?? skill.name).
tool inputSchema() Map Fixed: {type:"object", properties:{task:{type:"string"}}, required:["task"]}.
tool source() String Always "a2a".
ToolResult.metadata() Map {agent, taskId, state, polls, ms} on every result, success or failure.
  • A2A.agent — build the descriptor agentTools resolves.
  • A2A.parseAgentsConfig — parse a whole agents config block into descriptors.
  • Toolkit.createagents(...) calls agentTools for you, with per-peer failure isolation.
  • A2AServer.start — the inbound mirror: expose your own toolkit as an A2A peer.