Skip to content

A2A Protocol

The A2A (Agent-to-Agent) protocol enables agents to discover each other, exchange tasks, and stream results over HTTP. Reactive Agents implements the A2A specification with full JSON-RPC 2.0 support.

A2A communication follows this flow:

Agent B Agent A (Server)
│ │
│─── GET /.well-known/agent.json ─▶│ 1. Discovery
│◀── AgentCard ───────────────────│
│ │
│─── POST / (message/send) ──────▶│ 2. Send Task
│◀── { taskId } ─────────────────│
│ │
│─── POST / (tasks/get) ─────────▶│ 3. Poll Result
│◀── { status, result } ─────────│

Every A2A agent publishes an Agent Card — a JSON document describing its name, capabilities, and skills.

import { generateAgentCard, toolsToSkills } from "@reactive-agents/a2a";
const card = generateAgentCard({
name: "research-agent",
description: "An agent that researches topics thoroughly",
url: "https://my-agent.example.com",
organization: "My Org",
capabilities: {
streaming: true,
pushNotifications: false,
},
skills: [
{ id: "web-search", name: "Web Search", description: "Search the web", tags: ["search"] },
{ id: "summarize", name: "Summarize", description: "Summarize documents", tags: ["nlp"] },
],
});

Cards are served at GET /.well-known/agent.json (standard) and GET /agent/card (fallback).

Convert existing tool definitions to skills:

const skills = toolsToSkills([
{ name: "calculator", description: "Perform math", parameters: [{ name: "expression" }] },
{ name: "web-search", description: "Search the web", parameters: [{ name: "query" }] },
]);
// [{ id: "calculator", name: "calculator", description: "Perform math", tags: [] }, ...]

The simplest way to expose an agent via A2A:

Terminal window
rax serve --name my-agent --provider anthropic --port 3000
rax serve --name my-agent --provider anthropic --port 3000 --with-tools # Start A2A server with built-in tools enabled

This starts a fully functional A2A HTTP server with:

  • Agent Card at /.well-known/agent.json
  • JSON-RPC endpoint at POST /
  • Supported methods: message/send, tasks/get, tasks/cancel, agent/card
const agent = await ReactiveAgents.create()
.withName("my-agent")
.withProvider("anthropic")
.withA2A({ port: 3000 })
.build();

For full control, use the A2A server directly:

import { generateAgentCard } from "@reactive-agents/a2a";
const card = generateAgentCard({ name: "my-agent", url: "http://localhost:3000" });
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/.well-known/agent.json") {
return Response.json(card);
}
if (req.method === "POST" && url.pathname === "/") {
const body = await req.json();
// Handle JSON-RPC methods...
}
return new Response("Not Found", { status: 404 });
},
});
import { discoverAgent, discoverMultipleAgents } from "@reactive-agents/a2a";
import { Effect } from "effect";
// Discover a single agent
const card = await Effect.runPromise(
discoverAgent("https://agent.example.com")
);
console.log(card.name, card.skills);
// Discover multiple agents (up to 5 concurrently)
const cards = await Effect.runPromise(
discoverMultipleAgents([
"https://agent-a.example.com",
"https://agent-b.example.com",
])
);
import { A2AClient, createA2AClient } from "@reactive-agents/a2a";
import { Effect } from "effect";
const layer = createA2AClient({ baseUrl: "https://agent.example.com" });
const result = await Effect.gen(function* () {
const client = yield* A2AClient;
// Send a task
const { taskId } = yield* client.sendMessage({
message: {
role: "user",
parts: [{ kind: "text", text: "Research quantum computing" }],
},
});
// Poll for result
const task = yield* client.getTask({ id: taskId });
return task;
}).pipe(Effect.provide(layer), Effect.runPromise);
const layer = createA2AClient({
baseUrl: "https://agent.example.com",
auth: {
type: "bearer",
token: "my-secret-token",
},
});
// Or API key auth:
const layer2 = createA2AClient({
baseUrl: "https://agent.example.com",
auth: {
type: "apiKey",
apiKey: "my-api-key",
},
});

Find the best agent for a task based on skills and capabilities:

import { matchCapabilities, findBestAgent } from "@reactive-agents/a2a";
const agents = [card1, card2, card3]; // AgentCard[]
// Score and rank all agents
const ranked = matchCapabilities(agents, {
skillIds: ["web-search"],
tags: ["research", "nlp"],
inputModes: ["text/plain"],
});
// Returns: [{ agent, score, matchedSkills }]
// Get the single best match
const best = findBestAgent(agents, { skillIds: ["web-search"] });
if (best) {
console.log(`Best agent: ${best.agent.name} (score: ${best.score})`);
}

Scoring:

  • Skill ID match: 10 points
  • Tag overlap: 5 points per matching tag
  • Input mode support: 2 points per matching mode

Register a remote agent as a callable tool on your agent:

const agent = await ReactiveAgents.create()
.withName("coordinator")
.withProvider("anthropic")
.withRemoteAgent("researcher", "https://research-agent.example.com")
.withReasoning()
.build();
// The coordinator can now delegate research tasks to the remote agent
const result = await agent.run("Research and summarize recent AI breakthroughs");

Or register a local agent as a tool:

const agent = await ReactiveAgents.create()
.withName("coordinator")
.withProvider("anthropic")
.withAgentTool("specialist", {
name: "data-analyst",
description: "Analyzes data and produces insights",
})
.build();

For real-time task updates, use Server-Sent Events:

import { createSSEStream, formatSSEEvent } from "@reactive-agents/a2a";
// Server side: create an SSE stream
const { stream, enqueue, close } = createSSEStream();
// Push events as the task progresses
enqueue({ type: "status", taskId: "abc", data: { state: "working" } });
enqueue({ type: "artifact", taskId: "abc", data: { parts: [{ kind: "text", text: "Partial result..." }] } });
enqueue({ type: "status", taskId: "abc", data: { state: "completed" } });
close();
// Return as SSE response
return new Response(stream, {
headers: { "Content-Type": "text/event-stream" },
});

When connecting to MCP (Model Context Protocol) tool servers, Reactive Agents supports four transport modes:

TransportWhen to Use
stdioSubprocess — MCP server launched as a child process
sseHTTP Server-Sent Events — remote server over HTTP
websocketWebSocket — low-latency bidirectional connection
streamable-httpStreaming HTTP — persistent connection with multiplexed streams
// stdio (subprocess)
.withMCP({ name: "local-tools", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem"] })
// SSE (HTTP server-sent events)
.withMCP({ name: "remote-tools", transport: "sse", url: "https://mcp.example.com/sse" })
// WebSocket
.withMCP({ name: "my-server", transport: "websocket", url: "ws://localhost:8080" })
// Streamable HTTP (persistent connection with multiplexed streams)
.withMCP({ name: "streaming-tools", transport: "streamable-http", url: "https://mcp.example.com/stream" })
MethodDescriptionParams
message/sendSend a message and create a task{ message: A2AMessage }
message/streamSend and subscribe to SSE updates{ message: A2AMessage }
tasks/getGet task status and result{ id: string }
tasks/cancelCancel an in-progress task{ id: string }
agent/cardGet the agent’s card via RPC
ErrorWhen
A2AErrorGeneral protocol errors
DiscoveryErrorAgent card fetch failed
TransportErrorHTTP/network failure
TaskNotFoundErrorTask ID doesn’t exist
TaskCanceledErrorTask was already canceled
InvalidTaskStateErrorInvalid state transition
AuthenticationErrorAuth credentials invalid