Packages
a2a_elixir_sdk
1.0.1
Elixir implementation of the Agent-to-Agent (A2A) protocol. Exposes ADK agents as A2A-compatible HTTP endpoints and consumes remote A2A agents as local ADK agents.
Current section
Files
Jump to
Current section
Files
a2a_elixir_sdk
usage-rules.md
usage-rules.md
# A2A Elixir SDK Usage Rules
`a2a_elixir_sdk` (module namespace `A2AEx.*`) is an Elixir implementation of the [Agent-to-Agent (A2A) protocol](https://github.com/a2aproject/A2A). It is a full server + client: expose ADK agents as A2A-compatible HTTP endpoints, and consume remote A2A agents as local ADK agents. Depends on [`adk_ex`](https://hex.pm/packages/adk_ex).
## Architecture
```
Server side:
A2AEx.Server (@behaviour Plug)
-> A2AEx.JSONRPC (parse/encode)
-> A2AEx.RequestHandler (dispatch for 10 JSON-RPC methods)
-> A2AEx.AgentExecutor behaviour (execute/cancel)
| {A2AEx.ADKExecutor, config} bridges ADK.Runner
-> A2AEx.TaskStore (CRUD tasks)
-> A2AEx.EventQueue (SSE delivery)
-> A2AEx.PushConfigStore (webhook config)
-> A2AEx.PushSender (webhook delivery)
Client side:
A2AEx.Client (Req HTTP + SSE)
-> A2AEx.Client.SSE (line parser with buffering)
-> A2AEx.RemoteAgent (ADK agent backed by remote A2A)
-> A2AEx.Converter (A2A -> ADK event conversion)
```
## Core Patterns
### Expose an ADK agent as an A2A endpoint
```elixir
# 1. Build an ADK agent + runner (see adk_ex usage-rules for details)
agent = %ADK.Agent.LlmAgent{name: "my-agent", ...}
{:ok, session_svc} = ADK.Session.InMemory.start_link()
{:ok, runner} = ADK.Runner.new(
app_name: "my-app", root_agent: agent, session_service: session_svc
)
# 2. Start stores
{:ok, store} = A2AEx.TaskStore.InMemory.start_link()
# 3. Wire up the request handler — note the {module, config} executor tuple
config = %A2AEx.ADKExecutor.Config{runner: runner, app_name: "my-app"}
handler = %A2AEx.RequestHandler{
executor: {A2AEx.ADKExecutor, config},
task_store: {A2AEx.TaskStore.InMemory, store},
agent_card: %A2AEx.AgentCard{
name: "My Agent",
url: "http://localhost:4000",
version: "1.0.0"
}
}
# 4. Start the HTTP server with Bandit (pure-Elixir, recommended)
{:ok, _} = Bandit.start_link(plug: {A2AEx.Server, handler: handler}, port: 4000)
```
### Consume a remote A2A agent (standalone client)
```elixir
# Default receive_timeout is 120s — tuned for LLM-backed agents.
client = A2AEx.Client.new("http://remote:4000")
# Override for fast/local agents:
client = A2AEx.Client.new("http://remote:4000", receive_timeout: 5_000)
{:ok, card} = A2AEx.Client.get_agent_card(client)
params = %{"message" => %{
"role" => "user",
"parts" => [%{"kind" => "text", "text" => "Hello"}]
}}
{:ok, task} = A2AEx.Client.send_message(client, params)
# Streaming: always returns {:ok, stream}; errors surface inside the stream.
{:ok, stream} = A2AEx.Client.stream_message(client, params)
Enum.each(stream, &IO.inspect/1)
```
### Use a remote A2A agent as a local ADK sub-agent
```elixir
remote = A2AEx.RemoteAgent.new(%A2AEx.RemoteAgent.Config{
name: "remote-helper",
url: "http://remote:4000",
description: "Remote helper"
})
# Use as sub-agent inside SequentialAgent/ParallelAgent/etc.
```
### Custom executor (without ADK)
```elixir
defmodule MyExecutor do
@behaviour A2AEx.AgentExecutor
@impl true
def execute(ctx, task_id) do
A2AEx.EventQueue.enqueue(task_id,
A2AEx.TaskStatusUpdateEvent.new(task_id, ctx.context_id, :working))
reply = A2AEx.Message.new(:agent, [%A2AEx.TextPart{text: "Hi"}])
final = A2AEx.TaskStatusUpdateEvent.new(task_id, ctx.context_id, :completed, reply)
A2AEx.EventQueue.enqueue(task_id, %{final | final: true})
:ok
end
@impl true
def cancel(_ctx, _task_id), do: :ok
end
```
## Critical Rules
1. **Store references are `{module, server}` tuples.** Pass
`{A2AEx.TaskStore.InMemory, pid}` — not a bare pid or a bare module.
Same shape for `push_config_store`.
2. **Executor can be a bare module OR a `{module, config}` tuple.** Bare module
is called as `execute(ctx, task_id)` (2-arity). A tuple is called as
`execute(ctx, task_id, config)` (3-arity). `A2AEx.ADKExecutor` *requires*
the tuple form because it needs runner + app_name config.
3. **Default client timeout is 120s (`receive_timeout: 120_000`).** Req's
default of 15s is too short for LLM-backed agents. The server's
`collect_events` timeout in `RequestHandler` also defaults to 120s.
Override via `A2AEx.Client.new/2` opts if you need faster failures.
4. **`Client.stream_message/2` always returns `{:ok, stream}`.** Do not
pattern-match on `{:error, _}` at the call site — errors surface inside the
stream as it is enumerated.
5. **JSON uses camelCase on the wire, snake_case in Elixir.** All A2A types
implement `Jason.Encoder` + `from_map/1` to translate. Never hand-roll
JSON — go through the type constructors.
6. **Every polymorphic type has a `"kind"` discriminator.** Parts
(`text`/`file`/`data`), events (`status-update`/`artifact-update`), tasks
(`task`/`message`). Use `A2AEx.Part.from_map/1`, `A2AEx.Event.from_map/1`
to decode — they dispatch on `"kind"`.
7. **No Phoenix, no Plug.Router.** `A2AEx.Server` implements `@behaviour Plug`
directly with manual routing. Mount it with Bandit (pure-Elixir, recommended):
`Bandit.start_link(plug: {A2AEx.Server, handler: handler}, port: 4000)`.
Bandit is not a hard dep — add `{:bandit, "~> 1.0"}` in your app.
8. **Terminal task states are absorbing.** `:completed`, `:canceled`,
`:failed`, `:rejected` cannot transition further. `A2AEx.Converter` tests
terminal state when mapping ADK events.
9. **Mark final SSE event with `final: true`.** `%{final_event | final: true}`
tells the event queue and SSE writer to terminate the stream after
delivering the event. Forgetting this leaves the client hanging.
10. **Agent card is served at `/.well-known/agent.json`.** Populate at
minimum: `name`, `url`, `version`. Capabilities (streaming,
pushNotifications) default to false — set them explicitly if supported.
## Common Gotchas
- `TaskStore.InMemory` is a `GenServer` + ETS — call `start_link/1` and pass
the returned pid in the tuple. It is not automatically started by the
application supervisor.
- `EventQueue` uses `Registry` + `DynamicSupervisor`; these *are* started by
the `:a2a_elixir_sdk` application supervisor, so just call `EventQueue.enqueue/2`.
- Push notifications are opt-in. Only wire `push_config_store` + `push_sender`
into the `RequestHandler` if you actually want webhook delivery.
- Bandit has no public `stop/1`. In tests use
`Process.exit(pid, :normal)` + `Process.sleep(10)` for cleanup.
- `defstruct` keyword defaults must come last: `[:a, :b, c: default]`, never
`[:a, c: default, :b]`.
## Sub-rules
For deeper guidance on specific topics, see the `usage-rules/` directory:
- `a2a_elixir_sdk:server` — building a server, custom executors, stores, push
- `a2a_elixir_sdk:client` — client + RemoteAgent, streaming, timeouts
- `a2a_elixir_sdk:adk_bridge` — ADKExecutor and Converter semantics
- `a2a_elixir_sdk:types` — message/part/task/event encode/decode conventions