All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.3.0] - 2026-09-23
Added
tasks/resubscribe(SubscribeToTask) reattaches to a running task's event stream. The first SSE event is the task as it stands, followed by a status update per state change, ending when the task reaches a terminal state.A2A.Client.resubscribe/3is the client half.An unknown task answers
-32001 TaskNotFoundErrorand an already-finished one-32004 UnsupportedOperationError, replacing the blanket-32004the method used to return for everything. It is gated on the declaredstreamingcapability and runs the:authorize_taskhook under a new:resubscribeoperation.Subscribers live in the agent's own state and are monitored, so a dropped connection deregisters itself.
A2A.Pluggains:resubscribe_timeout(default60_000ms), which closes a stream that goes idle so a task that never terminates cannot pin its connection process open.Two deliberate limits: events produced before the subscription are not replayed, and the agent's own stream enumerable is never re-enumerated — doing so would replay it from the start and duplicate the task's artifacts and history.
Push notification webhook delivery. Every task state change now POSTs a
StreamResponsestatus update to each webhook registered for that task, carrying the config's credentials as anAuthorizationheader. Adds theA2A.PushNotificationSenderbehaviour andA2A.PushNotificationSender.HTTP, the default when the optional:reqdependency is present. Override it withMyAgent.start_link(push_sender: {MyModule, opts}), or passnilto disable delivery.Delivery runs in a process spawned off the agent, so a slow or hanging webhook cannot stall task processing, and each attempt is reported through a new
[:a2a, :push_notification, :delivery]telemetry event. The HTTP sender retries with exponential backoff inside a per-attempt timeout.configuration.taskPushNotificationConfigis now honoured onmessage/send, so a client can register a webhook on the initial send rather than through the CRUD methods. The config attaches to the task the call creates, runs the same:authorize_taskhook under:push_setthat the CRUD method does, and receives the task's current status once on registration.The HTTP sender can reject plain-HTTP URLs (
:require_https) and private or loopback hosts (:block_private_ips). Both are off by default: the spec makes them a SHOULD rather than a MUST, and enabling them by default would break webhook receivers onlocalhost.{:message, parts}agent reply —message/sendcan now answer with a bareMessageinstead of aTask, the other half of the spec'sSendMessageResponseoneof (DM-MSG-001). The runtime discards the task it built for the turn: nothing is persisted,tasks/getwill not find it, and the JSON-RPC result is{"message": …}rather than{"task": …}. The message inherits the request'scontextIdand carries notaskId.On
message/streamthe same reply produces a single SSE event carrying the message, with no task snapshot and no final status event.Return types widen rather than change —
A2A.call/3, the generatedYourAgent.call/3,A2A.Client.send_message/3and theA2A.JSONRPC.handle_send/3callback are now{:ok, A2A.Task.t() | A2A.Message.t()},A2A.stream/3gains an{:ok, A2A.Message.t()}result, andA2A.Extension.handle_response/3accepts either struct. Existing agents are unaffected: nothing returns aMessageunless an agent opts in.Returning
{:message, parts}while continuing an existing task (task_id:) is rejected with-32006 InvalidAgentResponseError— the client holds a task id, so a bare Message would strand the task and drop the turn from its history.Push notification config CRUD: the four
tasks/pushNotificationConfig/*methods (and their v1.0CreateTaskPushNotificationConfig/GetTaskPushNotificationConfig/ListTaskPushNotificationConfigs/DeleteTaskPushNotificationConfignames) now store, serve and delete configs instead of always returning-32003. Adds theA2A.PushNotificationConfigstruct, four optionalA2A.TaskStorecallbacks with anA2A.TaskStore.ETSimplementation, four optionalA2A.JSONRPChandler callbacks, andA2A.Client.set_push_config/3,get_push_config/4,list_push_configs/3anddelete_push_config/4.Webhook delivery is not implemented — nothing POSTs to a registered URL. The methods are therefore gated on the declared capability, so a server that does not opt in behaves exactly as before:
{A2A.Plug, agent: MyAgent, base_url: url, agent_card_opts: [capabilities: %{push_notifications: true}]}A handler that implements none of the new callbacks also keeps the old
-32003. Requests are accepted in both the v1.0 flat form (task_idalongside the config fields, as the TCK sends) and the v0.3 nested form (taskIdpluspushNotificationConfig), andauthenticationis read from either a singularschemeor the spec's pluralschemesarray. Registering a config for a task that does not exist returns-32001 TaskNotFoundError, and deletes are idempotent.:authorize_taskis now called for push notification config operations, under the new:push_set,:push_get,:push_listand:push_deleteoperation atoms. They are distinct from:getso an authorizer can grant read access to a task without also granting the ability to rewrite the webhooks it delivers to — push configs carry credentials, so an unauthorized read would leak them. An authorizer that pattern-matches strictly on:get/:cancel/:listneeds a clause for the new atoms; the hook has not appeared in a release yet, so nothing published breaks.Caching headers on the agent card endpoint per A2A v1.0 §8.6: a quoted
sha256ETagcomputed from the response body, aLast-Modifiedin RFC 7231 IMF-fixdate form, andCache-Control: public, max-age=300. The ETag is computed per request, sinceA2A.Plug.put_base_url/2can change the body. Note theCache-Controlvalue changes even for callers that set nothing: responses previously carried Plug's default ofmax-age=0, private, must-revalidate, which is wrong for a public card.A2A.Plug:last_modifiedoption — theDateTimeserved in the agent card'sLast-Modifiedheader (default:DateTime.utc_now()evaluated ininit/1, so build time under Phoenix's compile-timeplugmacro and boot time otherwise).A2A.Plugtask-level authorization hook fortasks/get,tasks/cancel, andtasks/listA2A v1.0 wire format on encode: flat
Part(nokind, withtext/data/raw/url/mediaType/filename);Task,Message, andArtifactno longer carry akindfield; AgentCard top-levelurlandprotocolVersionremoved (now per-interface undersupportedInterfaces[]). Decoder accepts both v1.0 and the legacy v0.3 nested-fileform, so v0.3 clients keep working.Message.reference_task_ids,Artifact.extensions, andAgentCard.signaturesstruct fields for v1.0 data carriage.A2A v1.0 extension mechanism:
A2A.Extensionbehaviour withdeclaration/1,activate/3,handle_request/3, andhandle_response/3callbacks;A2A.AgentExtensionstruct for declarations;A2A-Extensionsheader negotiation inA2A.PlugandA2A.Client; merge of declared extensions into the agent card'scapabilities.extensions;context.extensionsmap for agents to read per-request activations;A2A.Extension.Timestampas a reference implementation.A2A v1.0
A2A-Versionheader negotiation:A2A.Versionhelper module;A2A.Plug:versionsoption (defaults to["0.3", "1.0"]) validates the request header and returnsVersionNotSupportedError(-32009) for unsupported versions; negotiated version echoed in the response header.A2A.Client:versionoption (defaults to"1.0") sets the request header on every call;A2A.Client.version/1reads the server's echoed value. Missing/empty headers are interpreted as"0.3"(spec §3.6.2) and onlyMajor.Minoris significant.
Changed
Breaking: streaming events now use the v1.0
StreamResponsewrapper. Every SSEresultcarries exactly one oftask,message,statusUpdateorartifactUpdate, instead of a flat object discriminated bykind. Thekindfield is gone from status and artifact events, andfinalis gone from status events — v1.0 removed it, and a stream now ends when the task reaches a terminal or interrupted state.%A2A.Event.StatusUpdate{}keeps its:finalfield: the decoder honours an explicit"final"from a v0.3 peer and otherwise reconstructs it from the state, so matching onfinal: truestill works. Decoding also still accepts the v0.3kindshape, so a v1.0 client can consume an older peer's stream.This fixes a bug in which the opening task snapshot was dropped on every stream: the encoder omitted any discriminator and
A2A.Clientsilently discarded what it could not decode. Undecodable frames are now logged rather than dropped in silence.Breaking: A2A-specific errors (-32001 to -32009) now serialize
dataas an array carrying agoogle.rpc.ErrorInfoobject, per A2A v1.0JSONRPC-ERR-003:"data": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "domain": "a2a-protocol.org", "reason": "TASK_NOT_FOUND"}]Where an error previously carried a free-form string in
data(the rejected version on -32009, the missing extension URIs on -32008, the cancel reason on -32002), that string is preserved under the ErrorInfo'smetadata.detailrather than dropped.A2A.Clientsurfacesdataverbatim, so%A2A.JSONRPC.Error{data: …}is now a list rather than a string ornilfor these codes — readmetadata.detailoff the first entry instead. The five standard JSON-RPC codes (-32700, -32600, -32601, -32602, -32603) have no defined reason and keep their free-formdataunchanged.Breaking:
message/streamis now gated on the declared streaming capability. A server that does not advertisecapabilities.streamingreturns-32004 UnsupportedOperationErrorinstead of opening an SSE stream, per A2A v1.0CORE-CAP-002. Since capabilities default to%{}, every server using the defaults is affected. To keep streaming, pass the capability toA2A.Plug:{A2A.Plug, agent: MyAgent, base_url: url, agent_card_opts: [capabilities: %{streaming: true}]}Note that
use A2A.Agent, opts: [...]does not work for this — the generated card's:optskey is never read (tracked separately).TaskStatus.timestampis now serialized with aZsuffix (UTC) per the v1.0 schema timestamp regex.A2A.Clientnow sends v1.0 PascalCase JSON-RPC method names (SendMessage,SendStreamingMessage,GetTask,CancelTask). The server continues to accept both v1.0 PascalCase and the legacy v0.3 slash-style names. Pointing the client at a strict v0.3-only server that doesn't accept PascalCase is a breaking change.Minimum Erlang/OTP is now 27. CI tests Elixir 1.17 and 1.18 on OTP 27, 1.19 on OTP 28, and 1.20 on OTP 29 — the three OTP majors upstream still maintains. OTP 25 and 26 are no longer supported or tested; both are past end-of-life, and OTP 25 has had no patches, including security fixes, since May 2025. The Elixir requirement is unchanged at
~> 1.17.The TCK compliance suite now targets A2A v1.0 only. Upstream replaced its v0.3 category suite with the v1.0
tests/compatibility/tests, so the v0.3 compliance server (test/tck/server.exs) and the duplicatedbin/tck-v1lane have been removed. Known failures are tracked intest/tck/expected-failures.txtand the job is red until they are closed; it fails only when the failure set differs from that baseline. This affects the compliance harness only — the server still accepts v0.3 on the wire.joseis no longer declared as a direct dependency or pinned to 1.11.10. The pin existed only to keep OTP 25 compiling; this library verifies JWTs through Joken and never calls JOSE directly, so jose is now an ordinary transitive dependency of the optionaljokendep.
Fixed
A
{:stream, …}reply whose enumerable raises, or whose client disconnects mid-stream, no longer stores the task ascompleted. The finalizing hook runs however enumeration ends, so it reported success for every ending —tasks/get, push notifications and resubscribers were all told a failed stream had succeeded, and handed its partial output as the result. The task is now markedfailed; the parts it managed to emit are still kept.Optional callback detection no longer depends on the module happening to be loaded.
A2A.JSONRPCandA2A.Agent.Statecalledfunction_exported?/3withoutCode.ensure_loaded?/1, which answersfalsefor a module that has not been loaded yet — so under lazy loading a handler implementinghandle_list/2, or a task store implementinglist_all/2, could be treated as implementing neither.message/sendnow honoursconfiguration.historyLength, truncating the returned task's history the same waytasks/getalready did. It was previously ignored, so the full history came back regardless.historyLengthis now also accepted under its protobuf spellinghistory_lengthontasks/get,tasks/cancelandtasks/resubscribe. The spec and the REST binding usehistoryLength, but some JSON-RPC clients send the proto field name and the reference implementation accepts both; previously the limit was silently ignored.message/sendandmessage/streamwith an unknowntaskIdnow return-32001 TaskNotFoundErrorinstead of-32603 InternalErrormessage/sendandmessage/streamtargeting a task in a terminal state now return-32004 UnsupportedOperationErrorinstead of-32603 InternalError
[0.2.0] - 2026-03-06
Added
- Telemetry instrumentation for call, message, cancel, and task transitions
- Security scheme data modeling (
A2A.SecurityScheme.*structs) onAgentCard - Auth middleware (
A2A.Plug.Auth) — Bearer, Basic, API key, OAuth2, OpenID Connect - TCK compliance across all categories (mandatory, capabilities, quality, features)
- TCK results posted as PR comments in CI
Fixed
- Accept v1.0 field names (
bytes/uri) inFileContentdecoding - Reject messages with missing
messageIdor emptypartsper spec - Reject negative
historyLengthon all methods - Reject cancel on tasks in terminal states (completed/canceled/failed)
Changed
- CI runs full TCK suite (
bin/tck all) instead of mandatory only
[0.1.1] - 2026-03-03
Added
- Automated Hex publishing to
actioncardorg on GitHub releases - Dependabot configuration for Mix deps and GitHub Actions
- Issue and PR templates
Fixed
- Minor doc cleanups: internal module references, typespec refinement
[0.1.0] - 2026-03-03
Added
- A2A protocol types:
Task,Message,Part,Artifact,Event,FileContent - Agent behaviour (
A2A.Agent) with runtime and state management - Agent card discovery (
A2A.AgentCard) with full wire-format support - JSON-RPC 2.0 transport layer (
A2A.JSONRPC) with request/response/error types - Plug-based HTTP server (
A2A.Plug) with SSE streaming support - HTTP client (
A2A.Client) with SSE streaming via Req - Task store behaviour (
A2A.TaskStore) with ETS implementation - Agent registry and supervisor for multi-agent deployments
- Comprehensive JSON encoding/decoding with
A2A.JSON - A2A TCK (Technology Compatibility Kit) compliance