Elixir implementation of the Agent-to-Agent (A2A) protocol — a standard for AI agents to communicate over JSON-RPC 2.0.
A2A gives you behaviour-based agents that run as GenServer processes. Define an agent, serve it over HTTP, or call remote agents — all with idiomatic Elixir patterns.
Pre-release: This library is under active development. The API may change before 1.0.
Note
This project is developed with significant AI assistance (Claude, Copilot, etc.)
- Behaviour-based agents —
use A2A.Agentgenerates a full GenServer with task lifecycle management - Multi-turn conversations — continue tasks with
task_idfor stateful back-and-forth - Streaming — return
{:stream, enumerable}from agents; SSE over HTTP - HTTP serving —
A2A.Plughandles agent card discovery, JSON-RPC dispatch, and SSE streaming - HTTP client —
A2A.Clientfor discovering and calling remote A2A agents - Agent registry —
A2A.Registryfor skill-based agent discovery - Supervision —
A2A.AgentSupervisorstarts a fleet of agents with one call - Pluggable storage —
A2A.TaskStorebehaviour with built-in ETS implementation - Telemetry —
:telemetryspans and events for calls, messages, cancels, and state transitions
# Define an agent
defmodule MyAgent do
use A2A.Agent,
name: "my-agent",
description: "Does things"
@impl A2A.Agent
def handle_message(message, _context) do
{:reply, [A2A.Part.Text.new("Got: #{A2A.Message.text(message)}")]}
end
end
# Start and call it
{:ok, _pid} = MyAgent.start_link()
{:ok, task} = A2A.call(MyAgent, "hello")Agents return {:reply, parts}, {:input_required, parts}, or {:stream, enumerable} from handle_message/2. The runtime handles task creation, state transitions, and history.
A2A.Plug exposes your agent as an A2A-compliant HTTP endpoint with agent card discovery and JSON-RPC dispatch.
# Standalone with Bandit
{:ok, _pid} = MyAgent.start_link()
Bandit.start_link(
plug: {A2A.Plug, agent: MyAgent, base_url: "http://localhost:4000"}
)
# Or in a Phoenix router
forward "/a2a", A2A.Plug,
agent: MyAgent, base_url: "http://localhost:4000/a2a"The agent card is served at GET /.well-known/agent-card.json by default.
A2A.Client discovers and communicates with remote A2A agents over HTTP. Requires the req optional dependency.
# Discover an agent
{:ok, card} = A2A.Client.discover("https://agent.example.com")
# Send a message
client = A2A.Client.new(card)
{:ok, task} = A2A.Client.send_message(client, "Hello!")
# Stream a response
{:ok, stream} = A2A.Client.stream_message(client, "Count to 5")
Enum.each(stream, &IO.inspect/1)All functions also accept a URL string directly: A2A.Client.send_message("https://agent.example.com", "Hello!").
Continue an existing task by passing task_id:
{:ok, task} = A2A.call(MyAgent, "order pizza")
# task.status.state => :input_required
{:ok, task} = A2A.call(MyAgent, "large", task_id: task.id)For streaming agents, return {:stream, enumerable} and consume with A2A.stream/3:
{:ok, task, stream} = A2A.stream(MyAgent, "research topic")
stream |> Stream.each(&process/1) |> Stream.run()Start a fleet of agents with a shared registry for skill-based discovery. The current registry is a minimal in-memory implementation covering basic lookup and skill-based routing — production use cases with many agents may warrant a custom registry backed by persistent storage.
{:ok, _sup} =
A2A.AgentSupervisor.start_link(
agents: [MyApp.PricingAgent, MyApp.RiskAgent, MyApp.SummaryAgent]
)
# Find agents by skill tag
A2A.Registry.find_by_skill(A2A.Registry, "finance")
#=> [MyApp.PricingAgent, MyApp.RiskAgent]Add a2a to your list of dependencies in mix.exs:
def deps do
[
{:a2a, "~> 0.2.0"}
]
endInclude only what you need:
def deps do
[
{:a2a, "~> 0.2.0"},
# For serving A2A endpoints
{:plug, "~> 1.16"},
{:bandit, "~> 1.5"},
# For calling remote A2A agents
{:req, "~> 0.5"}
]
endThe examples/ directory contains runnable scripts:
demo.exs— local agents: simple call, multi-turn, and streamingclient_server.exs— full HTTP client/server with Bandit andA2A.Clientsupervisor_demo.exs—A2A.AgentSupervisor, registry, and skill-based routing
Run any example with:
mix run examples/demo.exs# Fetch dependencies
mix deps.get
# Run tests
mix test
# Run the full quality suite (format + credo + dialyzer)
mix quality
# Run checks individually
mix format --check-formatted
mix credo --strict
mix dialyzerRequires Elixir ~> 1.17.
The A2A TCK is the official compliance test suite for the A2A protocol. It runs against a live server and validates protocol conformance.
Prerequisites: uv (Python package manager)
# Run mandatory compliance tests (clones TCK on first run)
bin/tck mandatory
# Run all categories
bin/tck all
# Available categories: mandatory, capabilities, quality, features, allTo run the server manually (e.g. for debugging):
# Default port 9999
mix run test/tck/server.exs
# Custom port
A2A_TCK_PORT=8080 mix run test/tck/server.exsThe TCK runs on every PR in CI. Reports are uploaded as build artifacts.
Key A2A spec features not yet covered:
- Push notifications — webhook delivery on task state changes
- Authenticated extended cards — per-client capability disclosure
- REST / gRPC transports — only JSON-RPC is supported
- Version negotiation — hardcoded to A2A v0.3
- Task resubscribe — reconnecting to active SSE streams
- Security middleware — auth plug, agent card signatures, task-level ACL (security scheme data modeling is complete)
See SPEC.md for full details and roadmap.
a2a_ex (Hex) takes a different approach to implementing A2A in Elixir. It supports both REST and JSON-RPC transports, covers the v0.3 and v1.0-rc specs, includes a protobuf-style JSON compatibility mode, and is end-to-end tested against the official JavaScript SDK. Where this library focuses on agent runtime and OTP integration, a2a_ex focuses on protocol codec and transport coverage — the two complement each other well.
Apache-2.0 — see LICENSE.