A2A
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 - Protocol extensions — A2A v1.0
A2A-Extensionsheader negotiation; ship your ownA2A.Extensionmodules or use the bundledA2A.Extension.Timestamp - Protocol versioning — A2A v1.0
A2A-Versionheader negotiation; server accepts0.3and1.0by default and returnsVersionNotSupportedError(-32009) for others, client sends1.0on every request - 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}, {:message, parts}, {:input_required, parts}, or {:stream, enumerable} from handle_message/2. The runtime handles task creation, state transitions, and history — except for {:message, parts}, which answers with a bare A2A.Message and creates no task at all.
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.
Task Access Control
A2A.Plug accepts an optional :authorize_task callback for task-scoped
operations:
forward "/a2a", A2A.Plug,
agent: MyAgent,
base_url: "http://localhost:4000/a2a",
authorize_task: fn operation, task, %{metadata: metadata} ->
same_tenant? = metadata["tenant_id"] == task.metadata["tenant_id"]
same_tenant? and operation in [:get, :cancel, :list]
end
The callback runs before tasks/get, tasks/cancel, and tasks/list responses.
Denied tasks/get and tasks/cancel requests return TaskNotFoundError so
callers cannot distinguish nonexistent tasks from tasks they cannot access.
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 routingextensions.exs—A2A-Extensionsheader negotiation end-to-end usingA2A.Extension.Timestamp
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 and Erlang/OTP 27 or later.
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 MUST-level compliance tests (clones TCK on first run)
bin/tck must
# Run every requirement level
bin/tck all
# Available levels: must, should, may, all (RFC 2119)
The TCK is pinned to a known revision so runs are reproducible; bump TCK_REF
in bin/tck to move to a newer suite.
To run the server manually (e.g. for debugging):
# Default port 9999
mix run test/tck/server_v1.exs
# Custom port
A2A_TCK_PORT=8080 mix run test/tck/server_v1.exs
The TCK runs on every PR in CI and is expected to be green. Known failures
live in test/tck/expected-failures.txt; the
run fails only when the actual failure set differs from that baseline, so a new
regression and a newly-fixed gap are both surfaced. Reports are uploaded as
build artifacts.
Not Yet Implemented
Key A2A spec features not yet covered:
- Push notifications — webhook delivery on task state changes (config CRUD is implemented; nothing POSTs to a registered URL yet)
- Authenticated extended cards — per-client capability disclosure
- REST / gRPC transports — only JSON-RPC is supported
- Security middleware — agent card signatures and OAuth flows (auth plug, task ACL hook, and security scheme data modeling are 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.