Agent2Agent (A2A) protocol v1.0 for Erlang/OTP: the protocol objects,
a server that exposes your agent over the JSON-RPC and HTTP+JSON
bindings, and a client that talks to any A2A agent. The server runs
on h1/h2 (HTTP/1.1 and HTTP/2 on one port), the client on
hackney. Requires OTP 27 or later.
A2A is a protocol for one autonomous agent to communicate and collaborate with another autonomous agent: discover it through an Agent Card, send it messages, follow the task it runs, receive the artifacts it produces. It differs from MCP: MCP lets an agent invoke tools and capabilities, A2A lets agents talk to each other as peers. This library implements the A2A boundary only; see a2a-protocol.org for the specification.
- Protocol objects as plain JSON maps (wire shape, binary camelCase
keys) with accessor modules:
barrel_a2a_message,barrel_a2a_part,barrel_a2a_artifact,barrel_a2a_task,barrel_a2a_agent_card,barrel_a2a_event,barrel_a2a_error. - A server (
barrel_a2a_server) with one handler per agent, task processes, streaming over SSE, push notifications, authentication and authorization hooks, extended cards, card signing, extensions, versioning and multi-tenancy, on both HTTP bindings. - A client (
barrel_a2a_client,barrel_a2a_remote_task) with card discovery and caching, signature verification, interface selection, blocking and streaming sends, follow-ups, task listing, cancel, push configs and a webhook receiver helper. - Validation: structural checks plus JSON Schema 2020-12 validation
against the official
a2a.json. - Pluggable task storage (
barrel_a2a_task_store): in memory by default, DETS for tasks that survive a restart.
- No agent runtime, planner, memory, MCP implementation, LLM abstraction or orchestration. The application owns agent execution; the library exposes it over A2A.
- No gRPC binding in this package. It is planned as
livery_grpc_a2a, a separate package onlivery_grpc, built on the binding-neutral contracts described in guides/embedding.md.
{deps, [
{barrel_a2a, "0.1.0"}
]}.Or from git:
{deps, [
{barrel_a2a,
{git, "https://github.com/barrel-platform/barrel_a2a.git", {tag, "v0.1.0"}}}
]}.Card = barrel_a2a_agent_card:new(#{
name => <<"Echo Agent">>,
description => <<"Repeats whatever you say">>,
version => <<"1.0.0">>,
skills => [#{id => <<"echo">>, name => <<"Echo">>, tags => [<<"demo">>]}]
}),
{ok, Server} = barrel_a2a_server:start(Card, #{
handler => fun(_Ctx, Message) -> {ok, barrel_a2a_message:text(Message)} end,
http => #{port => 8080}
}).The card is served at http://127.0.0.1:8080/.well-known/agent-card.json,
JSON-RPC at /a2a/jsonrpc, REST under /a2a/v1. See
guides/server.md.
{ok, Agent} = barrel_a2a_client:connect(<<"http://127.0.0.1:8080">>),
Card = barrel_a2a_client:card(Agent),
%% Blocking send: the task (or a direct message) when it settles.
{ok, {task, Task}} = barrel_a2a_client:send(Agent, <<"hello">>),
completed = barrel_a2a_task:state(Task),
%% Streaming: a remote task handle that pushes events to a process.
{ok, RT} = barrel_a2a_client:start(Agent, <<"review this repository">>),
ok = barrel_a2a_remote_task:stream_to(RT, self()),
{ok, Final} = barrel_a2a_remote_task:result(RT, 60000),
Text = barrel_a2a_remote_task:text(RT).See guides/client.md.
Every transition goes through barrel_a2a_task_state:transition/2:
submitted -> working | input_required | auth_required
| completed | failed | canceled | rejected
working -> working | input_required | auth_required
| completed | failed | canceled
input_required -> working | canceled | failed
auth_required -> working | canceled | failed
terminal -> nothing
Handler results map to states:
| Result | State |
|---|---|
{ok, Result} |
completed, Result becomes the final artifact |
{message, Message} |
no task, a direct message reply |
{input_required, Message} |
input_required, next message re-enters the handler |
{auth_required, Message} |
auth_required, resumable with barrel_a2a_ctx:resume/1 |
{reject, Message} |
rejected |
ok |
as left by the ctx calls; a task still working completes |
{error, Reason} or a crash |
failed |
SendStreamingMessage and SubscribeToTask return SSE on both
bindings (JSON-RPC frames {"jsonrpc":"2.0","id":..,"result":<StreamResponse>},
REST frames the bare StreamResponse). The stream opens with the
Task snapshot, then statusUpdate and artifactUpdate events, and
closes on a terminal state. Handlers publish with
barrel_a2a_ctx:status/2,3, barrel_a2a_ctx:artifact/2,3 and
barrel_a2a_ctx:message/2. On the client barrel_a2a_remote_task
pushes events (stream_to/2) or lets you pull them (next/2), and
falls back to polling when the agent does not stream. See
guides/streaming.md.
Enable with push_notifications => #{...} on the server; clients
register webhooks at send time or with
barrel_a2a_client:create_push_config/3, and decode deliveries with
barrel_a2a_webhook:receive_notification/3. Delivery is ordered per
config, retried with backoff, and guarded against SSRF. See
guides/push-notifications.md.
Server side (auth option):
none
| {bearer, fun((Token) -> {ok, Principal} | {error, unauthenticated | forbidden})}
| {api_key, HeaderName, fun((Key) -> ...)}
| {basic, fun((User, Password) -> ...)}
| {Module, State} %% Module:authenticate(Request, State)
| fun((Request) -> ...) %% Request = #{headers, op, binding, peer}Client side (auth option): {bearer, Token | fun(() -> Token)},
{api_key, HeaderName, Value}, {basic, User, Password},
fun((Op) -> Headers) or none; or credentials, a map keyed by
security scheme name resolved against the card. See
guides/authentication.md.
- Protocol objects:
barrel_a2a_message,_part,_artifact,_task,_task_state,_agent_card,_event,_error. - Codec and validation:
barrel_a2a_json,_jsonrpc,_rest,_sse,_validate,_schema,_jsonschema,_canonical,_card_sign,_version,_extensions,_tenant. - Server:
barrel_a2a_listener(accept, TLS, ALPN, h1/h2) ->barrel_a2a_http_engine(routes, bindings, SSE loops) ->barrel_a2a_server_core(auth, tenant, version, extensions, capabilities, validation, scoping, operations) ->barrel_a2a_task_proc(one process per task) andbarrel_a2a_push. - Client:
barrel_a2a_client(facade) ->barrel_a2a_client_transportbehaviour (barrel_a2a_client_httpfor both HTTP bindings) ->barrel_a2a_remote_task.
The engine never touches sockets: it writes through a responder map
(reply, stream_start, stream_chunk, stream_end,
disconnected) that the listener builds over h1/h2 and that an
embedding application such as livery_a2a builds over its own
adapter. Embedding into livery uses barrel_a2a_server:start/2 with
listen => false, barrel_a2a_server:engine_config/2,
barrel_a2a_http_engine:routes/1 and
barrel_a2a_http_engine:handle/6. See
docs/architecture.md and
guides/embedding.md.
Every operation, object and normative behaviour of A2A v1.0.1 is
handled here, except the gRPC binding which is planned as
livery_grpc_a2a. The section-by-section table is in
docs/features.md.
make compile # rebar3 compile
make lint # rebar3 lint
make xref # rebar3 xref
make dialyzer # rebar3 dialyzer
make eunit # rebar3 eunit
make ct # rebar3 ct
make examples-test # build and test examples/* against this checkout
make interop-python # a2a-sdk interop suite (needs python3)
make check # fmt compile lint xref dialyzer eunit ctBefore changing the library itself, read docs/internals/tour.md: it says where to start, what you can skip, and which files a given kind of change touches. docs/internals/invariants.md lists the rules the runtime relies on that no single module shows, and docs/decisions records why the library is shaped as it is.
Apache-2.0