A2A.agentTools
Java · package io.github.muthuishere:toolnexus · SPEC §7A · A2A.java
public static List<Tool> agentTools(A2A.Agent ag) throws ExceptionGETs 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 to use it
Section titled “When to use it”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.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”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) { } }}Fields and overloads
Section titled “Fields and overloads”| 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. |
See also
Section titled “See also”A2A.agent— build the descriptoragentToolsresolves.A2A.parseAgentsConfig— parse a wholeagentsconfig block into descriptors.Toolkit.create—agents(...)callsagentToolsfor you, with per-peer failure isolation.A2AServer.start— the inbound mirror: expose your own toolkit as an A2A peer.