Behaviour for A2A v1.0 protocol extensions.
An extension module advertises a URI in its declaration/1, optionally
participates in per-request activation via activate/3, and can hook into
the request/response pipeline via handle_request/3 and
handle_response/3. Configured on A2A.Plug and A2A.Client as a list
of module() or {module(), keyword()} tuples.
See the A2A v1.0 extensions topic for protocol-level semantics. This module implements the data-only and profile extension categories; method extensions (registering new RPC methods) and state-machine extensions are not yet supported.
Defining an extension
defmodule MyApp.TimestampExtension do
@behaviour A2A.Extension
@uri "https://example.com/ext/timestamp"
@impl true
def declaration(_state) do
%A2A.AgentExtension{uri: @uri, description: "Adds request timestamps"}
end
@impl true
def activate(_requested_uris, _ctx, _state) do
{:ok, System.system_time(:millisecond)}
end
@impl true
def handle_response(task, _params, started_at) do
{:ok, A2A.Extension.put_metadata(task, __MODULE__, %{started_at: started_at}),
started_at}
end
endActivation model
At Plug/Client init time each configured extension's init/1 is called
with its opts; the returned state is kept alongside the module. Per
request, the server parses the A2A-Extensions header and for each
configured extension whose declared URI is in the requested set, calls
activate/3 with the requested URI list, the request context, and the
init state. activate/3 may return {:ok, activation} to participate,
:skip to opt out for this request, or {:error, error} to abort with
a JSON-RPC error. The list of activated URIs is echoed in the response
A2A-Extensions header.
Required extensions (required: true in declaration/1) that are not
declared in the client's request header trigger
ExtensionSupportRequiredError (-32008) before activation runs.
Hooks
Activated extensions can mutate the request or response:
handle_request/3— receives the decoded message, JSON-RPC params, and the extension's activation. Runs in declaration order before the request is dispatched to the agent.handle_response/3— receives the task, JSON-RPC params, and the activation. Runs in declaration order after the agent replies and before the task is encoded.
Both callbacks are optional; data-only extensions implement neither.
Reading activations inside an agent
Activations are surfaced to the agent's handle_message/2 as
context.extensions, a %{uri => activation} map. The
A2A.Extension.fetch/2 and A2A.Extension.activated?/2 helpers look up
by module rather than URI string.
def handle_message(message, context) do
case A2A.Extension.fetch(context, MyApp.TimestampExtension) do
{:ok, started_at} -> use_timestamp(message, started_at)
:error -> handle_without(message)
end
endConfiguring extensions
Server side, pass :extensions to A2A.Plug. The configured
declarations are merged into capabilities.extensions on the served
agent card, and the negotiation pipeline runs around every JSON-RPC
dispatch.
# Standalone with Bandit
Bandit.start_link(
plug: {A2A.Plug,
agent: MyAgent,
base_url: "http://localhost:4000",
extensions: [A2A.Extension.Timestamp, {MyApp.Passport, issuer: "acme"}]}
)
# Or in a Phoenix router
forward "/a2a", A2A.Plug,
agent: MyAgent,
base_url: "http://localhost:4000/a2a",
extensions: [A2A.Extension.Timestamp]Client side, pass :extensions to A2A.Client.new/2. The configured
URIs are sent in the A2A-Extensions request header on every call.
A2A.Client.parse_extensions_header/1 and
A2A.Client.activated/2 read the server's response header to discover
which extensions actually ran.
client = A2A.Client.new("http://localhost:4000",
extensions: [A2A.Extension.Timestamp])
{:ok, task} = A2A.Client.send_message(client, "hi")
task.metadata[A2A.Extension.Timestamp.uri()]
#=> %{"received_at" => ..., "completed_at" => ...}Reference
A2A.Extension.Timestamp ships in-tree as a complete profile-extension
example covering declaration/1, activate/3,
handle_request/3, and handle_response/3. See
examples/extensions.exs
for an end-to-end runnable demo.
Summary
Types
An extension's per-request activation value (opaque to the framework).
Ordered list of activations for hook chaining.
Internal compiled extension entry. Pipeline state: the module, its init state, and its (cached) declaration.
User-facing extension configuration entry.
Callbacks
Called once per request when the client has declared this extension in
its A2A-Extensions header. Returns the activation value that is
threaded through subsequent hooks, or :skip to opt out for this
request, or {:error, error} to abort the request.
Returns the %A2A.AgentExtension{} describing this extension. The URI is
used both for agent-card advertisement and for matching against the
A2A-Extensions request header.
Optional. Mutate the inbound message or JSON-RPC params before dispatch. Runs in declaration order across activated extensions.
Optional. Mutate the outbound task or message before encoding. Runs in declaration order across activated extensions.
Validates and compiles the extension's options. Called once when the
parent plug or client is initialised. The returned value is threaded
back into the remaining callbacks as the state argument.
Functions
Runs activate/3 for each configured extension whose declared URI is in
the requested list. Returns an ordered list of {module, activation, uri}
tuples and the matching URI list, or an error if any extension's
activate/3 returned {:error, ...}.
Returns true if the given extension module is activated in the context.
Returns the declarations of all compiled extensions, in declaration order.
Returns the URIs declared by all compiled extensions, in declaration order.
Fetches the activation value for the given extension module from a
request context. Returns {:ok, activation} or :error.
Parses a list of raw A2A-Extensions header values (HTTP allows multiple
values, each potentially comma-separated) into a deduplicated list of
requested URIs.
Convenience for namespacing a value under an extension's URI inside the
metadata field of a Message, Artifact, or Task.
Returns the URIs of declarations that are marked required: true.
Runs the handle_request/3 chain over an activated extension list.
Returns possibly-mutated message, params, and the (possibly-updated)
activations list, or {:error, error} if any extension aborted.
Runs the handle_response/3 chain over an activated extension list.
Returns the possibly-mutated task or message and the updated activations list.
Converts the ordered activations list into the %{uri => activation}
map exposed to the agent via context.extensions.
Validates required-extension presence against the client's requested URIs.
Returns :ok or {:error, missing_uris}.
Types
@type activation() :: term()
An extension's per-request activation value (opaque to the framework).
@type activations() :: [{module(), activation(), String.t()}]
Ordered list of activations for hook chaining.
@type compiled() :: {module(), state(), A2A.AgentExtension.t()}
Internal compiled extension entry. Pipeline state: the module, its init state, and its (cached) declaration.
User-facing extension configuration entry.
@type state() :: term()
An extension's init state (returned from init/1).
Callbacks
@callback activate(requested_uris :: [String.t()], ctx :: map(), state()) :: {:ok, activation()} | :skip | {:error, A2A.JSONRPC.Error.t()}
Called once per request when the client has declared this extension in
its A2A-Extensions header. Returns the activation value that is
threaded through subsequent hooks, or :skip to opt out for this
request, or {:error, error} to abort the request.
Defaults to {:ok, nil} (always activate, no per-request state).
@callback declaration(state()) :: A2A.AgentExtension.t()
Returns the %A2A.AgentExtension{} describing this extension. The URI is
used both for agent-card advertisement and for matching against the
A2A-Extensions request header.
@callback handle_request(A2A.Message.t(), params :: map(), activation()) :: {:ok, A2A.Message.t(), params :: map(), activation()} | {:error, A2A.JSONRPC.Error.t()}
Optional. Mutate the inbound message or JSON-RPC params before dispatch. Runs in declaration order across activated extensions.
@callback handle_response(A2A.Task.t() | A2A.Message.t(), params :: map(), activation()) :: {:ok, A2A.Task.t() | A2A.Message.t(), activation()}
Optional. Mutate the outbound task or message before encoding. Runs in declaration order across activated extensions.
Validates and compiles the extension's options. Called once when the
parent plug or client is initialised. The returned value is threaded
back into the remaining callbacks as the state argument.
Defaults to ignoring opts and returning nil.
Functions
@spec activate([compiled()], [String.t()], map()) :: {:ok, activations(), [String.t()]} | {:error, A2A.JSONRPC.Error.t()}
Runs activate/3 for each configured extension whose declared URI is in
the requested list. Returns an ordered list of {module, activation, uri}
tuples and the matching URI list, or an error if any extension's
activate/3 returned {:error, ...}.
Returns true if the given extension module is activated in the context.
@spec declarations([compiled()]) :: [A2A.AgentExtension.t()]
Returns the declarations of all compiled extensions, in declaration order.
Returns the URIs declared by all compiled extensions, in declaration order.
@spec fetch(map(), module()) :: {:ok, activation()} | :error
Fetches the activation value for the given extension module from a
request context. Returns {:ok, activation} or :error.
Parses a list of raw A2A-Extensions header values (HTTP allows multiple
values, each potentially comma-separated) into a deduplicated list of
requested URIs.
@spec put_metadata( A2A.Message.t() | A2A.Artifact.t() | A2A.Task.t(), module(), term() ) :: A2A.Message.t() | A2A.Artifact.t() | A2A.Task.t()
Convenience for namespacing a value under an extension's URI inside the
metadata field of a Message, Artifact, or Task.
Returns the URIs of declarations that are marked required: true.
@spec run_request(activations(), A2A.Message.t(), map()) :: {:ok, A2A.Message.t(), map(), activations()} | {:error, A2A.JSONRPC.Error.t()}
Runs the handle_request/3 chain over an activated extension list.
Returns possibly-mutated message, params, and the (possibly-updated)
activations list, or {:error, error} if any extension aborted.
@spec run_response(activations(), A2A.Task.t() | A2A.Message.t(), map()) :: {:ok, A2A.Task.t() | A2A.Message.t(), activations()}
Runs the handle_response/3 chain over an activated extension list.
Returns the possibly-mutated task or message and the updated activations list.
@spec to_context_map(activations()) :: %{required(String.t()) => activation()}
Converts the ordered activations list into the %{uri => activation}
map exposed to the agent via context.extensions.
Validates required-extension presence against the client's requested URIs.
Returns :ok or {:error, missing_uris}.