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.)
Features
- 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
Quick Start
# 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.
Serving over HTTP
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.
Calling Remote Agents
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!").
Multi-Turn & Streaming
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()
Supervision & Registry
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]
Installation
Add a2a to your list of dependencies in mix.exs:
def deps do [ {:a2a, "~> 0.2.0"} ] end
Optional Dependencies
Include 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"} ] end
Examples
The 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
Development
# 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 dialyzer
Requires Elixir ~> 1.17.
TCK (Protocol Compliance)
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, all
To 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.exs
The TCK runs on every PR in CI. Reports are uploaded as build artifacts.
Not Yet Implemented
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.
Alternative Implementations
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.
Links
License
Apache-2.0 — see LICENSE.