Unimplemented work organized by category. Phases 1-7 (core types, agent runtime, JSON codec, JSON-RPC, HTTP server, HTTP client, registry/supervisor) and telemetry instrumentation are complete. See the codebase and README for current functionality.
TCK Compliance
CI runs bin/tck all against test/tck/server_v1.exs, pinned to a known
A2A TCK revision (TCK_REF in
bin/tck). The suite targets A2A v1.0 only — upstream replaced its
category-based v0.3 suite with RFC 2119 requirement levels, so MUST failures
are hard, SHOULD failures are expected failures, and MAY tests skip when the
capability isn't declared.
Known failures are listed in
test/tck/expected-failures.txt; bin/tck
compares the actual failure set against that baseline and fails only when they
differ, so both a new regression and a newly-fixed gap are surfaced. A gap is
closed by deleting its line in the same commit as the fix.
Current Results
bin/tck all — 99 passed, 0 failed, 166 skipped. The skips are capability-
and transport-gated tests, not failures.
The TCK server declares capabilities.streaming (#84) and
capabilities.pushNotifications (#93), which is why those suites run at all.
The authenticated extended card is still undeclared, so that family continues
to skip — see below.
| Suite area | What it covers | Notes |
|---|---|---|
| agent_card | Card shape, extensions, caching headers | — |
| core_operations | Message send, task lifecycle, data model, error handling | — |
| jsonrpc | JSON-RPC 2.0 envelope, error codes, error info | — |
| grpc | gRPC transport binding | Skipped — transport not implemented |
| http_json | REST/HTTP+JSON binding | Skipped — transport not implemented |
Known Gaps
None. test/tck/expected-failures.txt holds no entries — every test the suite
runs against us passes, so any new failure is a regression.
Skipped (Expected)
| Tests | Reason | Unblocked by |
|---|---|---|
| Extended agent card | supportsAuthenticatedExtendedCard not declared | #100 |
| In-task authentication | Agent doesn't trigger auth-required state | Optional — agent-level decision |
| TLS / certificate validation | TCK server runs plain HTTP on localhost | Deploy-time concern, not library |
CORE-MULTI-005 context inference | Tasks get no contextId when the client sends none, so the test cannot run | #101 |
| gRPC / HTTP+JSON transports | Single transport (JSON-RPC only) | gRPC / REST Transport Bindings (below) |
| OAuth2 metadata URL | No OAuth2 scheme configured | Client-Side OAuth 2.0 Flows (below) |
Roadmap: Feature → TCK Tests Unlocked
The test paths below predate upstream's restructure into tests/compatibility/
and have not been re-derived; treat them as intent, not literal paths.
| # | Feature | TCK tests enabled |
|---|---|---|
| 1 | Authenticated Extended Card (#100) | mandatory/protocol/test_extended_agent_card.py; capabilities/ extended card tests |
| 3 | gRPC Transport Binding | transport-equivalence category (functional equivalence across transports) |
| 4 | REST Transport Binding | transport-equivalence category |
Protocol
Push Notifications
Config CRUD and webhook delivery are both implemented. The methods stay gated
on the declared capability, and a server that does not opt in returns
-32003 PushNotificationNotSupportedError:
{A2A.Plug, agent: MyAgent, base_url: url,
agent_card_opts: [capabilities: %{push_notifications: true}]}Implemented:
A2A.PushNotificationConfigstruct (id, task_id, url, token, authentication)- Optional
A2A.TaskStorecallbacksset_push_config/2,get_push_config/3,list_push_configs/2anddelete_push_config/3, withA2A.TaskStore.ETSkeeping configs in a second table so they never reach the task read paths - Optional
A2A.JSONRPChandler callbacks, so a handler that implements none of them keeps the old-32003behaviour A2A.Clientfunctions for all four methods- Configs are scoped to an existing task: registering one for an unknown task
returns
-32001 TaskNotFoundError, and every operation runs through the:authorize_taskhook configuration.taskPushNotificationConfighonoured onmessage/send, so a client can register a webhook on the initial send rather than through CRUD. It runs the same:authorize_taskhook under:push_setthat the CRUD method does, and delivers the task's current status once on registration — a task that finishes in a single turn changed state before the webhook existed, and the spec wants at least one delivery per configured webhookA2A.PushNotificationSenderbehaviour, withA2A.PushNotificationSender.HTTPas the default when:reqis available. Every task state change POSTs aStreamResponsestatus update to each registered webhook, carrying the config's credentials as anAuthorizationheader- Delivery runs in a spawned process, never the agent's, so a hanging webhook
cannot stall task processing. Each attempt is reported through
[:a2a, :push_notification, :delivery]telemetry - Bounded retry with exponential backoff and a per-attempt timeout
Optional hardening on the HTTP sender, off by default because the spec makes
both a SHOULD and enabling them breaks local development and the compliance
suite's own localhost receiver:
:require_https— reject plain-HTTP webhook URLs:block_private_ips— reject loopback, link-local and RFC 1918 hosts
Still open, and tracked separately since neither the spec nor the TCK requires them and no consumer is driving a signature format yet:
- HMAC-SHA256 signature generation/verification on webhook payloads
- Replay protection via timestamp and nonce headers
- Constant-time signature comparison to avoid timing attacks
Authenticated Extended Card
agent/getAuthenticatedExtendedCard currently returns -32004 UnsupportedOperationError. Full implementation requires:
- Optional
extended_card/1callback on the Agent behaviour — receives authenticated identity, returns an extended card with additional skills/capabilities A2A.Plugserves it at the spec-defined endpoint, gated by auth middleware- Public card advertises
supportsAuthenticatedExtendedCard: true - Enables per-client capability disclosure (two-tier card model)
gRPC Transport Binding
A2A v0.3 defines gRPC as an alternative transport. Not started. Would require:
- Protobuf schema definitions mirroring the JSON wire format
- gRPC server module (parallel to
A2A.Plug) AgentInterface/TransportProtocolsupport in agent cards
REST Transport Binding
The A2A spec defines REST as a first-class transport alongside JSON-RPC. Our library only supports JSON-RPC. Full implementation requires:
- REST endpoint definitions per the spec (
POST /message:send,POST /message:stream,GET /tasks/{id},POST /tasks/{id}:cancel,GET /tasks) - An
A2A.Plug.RESTmodule (or fold into existingA2A.Plugwith interface routing based on the request path) AgentInterface/supportedInterfacesin the agent card to advertise which transports are available (JSON-RPC, REST, gRPC)
Version & Wire Format Negotiation
The server accepts both A2A v0.3 and v1.0 wire formats and method names,
and the A2A-Version header is parsed and validated per spec §3.6
(unsupported versions return VersionNotSupportedError -32009). Still
outstanding:
- Per-interface version selection from
supportedInterfaces[]— agents exposing multiple interfaces at different URLs with different versions - Wire format option (
spec_jsonvsproto_json) controlling field naming conventions (camelCase vs snake_case) - Query-parameter fallback (
?A2A-Version=…) — spec §3.6.1 says clients MAY use it instead of the header
Task Resubscribe Streaming
tasks/resubscribe (SubscribeToTask) is implemented. Subscribing opens an
SSE stream whose first event is the task as it stands, followed by a status
update per state change, ending when the task reaches a terminal state. An
unknown task answers -32001 TaskNotFoundError and one that has already
finished answers -32004 UnsupportedOperationError; both are gated on the
declared streaming capability and run the :authorize_task hook under
:resubscribe.
Subscribers are held in the agent's own state — AGENTS.md rules out a
supervision tree, so there is nowhere else to keep them — and each is
monitored, so a dropped connection deregisters itself. A2A.Plug's
:resubscribe_timeout (default 60s) closes a stream that goes idle, since a
task that never terminates would otherwise pin its connection process open.
Two deliberate limits:
- Events before the subscription are not replayed. A subscriber sees the
task snapshot and everything after it, not the artifacts already produced.
Pair with
tasks/getfor the full history. - The agent's own stream is never re-enumerated. A task still holds its
source enumerable in
metadata[:stream], and enumerating it replays from the start rather than attaching — which would duplicate the task's artifacts and history. Resubscribe reads the stored task and waits for pushed events instead.
Agent Runtime
{:delegate, agent, msg} Reply Type
Phase 8 — not started. First-class agent-to-agent forwarding from within
handle_message/2. The runtime would dispatch the message to the target agent
and relay the response back to the original caller transparently.
Atomic Task Updates
The A2A.TaskStore behaviour only has put/2 and get/2. An atomic
update_task/3 callback that accepts a transformation function would enable
safe concurrent modifications:
update_task(store, task_id, fun)— applyfunto the current task atomically, returning{:ok, updated_task}or{:error, reason}A2A.TaskStore.ETSimplementation using optimistic locking (separate lock table, retry on conflict) to avoid serializing all updates through a single process
Discovery
Multi-Agent Plug / Directory Endpoint
Currently each A2A.Plug mount exposes a single agent's card. A remote client
can't discover all agents from one endpoint. Options:
- JSON array at
/.well-known/agent-card.json(non-standard but practical — the spec doesn't prohibit it) - Per-agent cards at
/.well-known/agents/{name}/agent-card.json - Query endpoint (e.g.,
GET /agents?skill=finance) — a step toward the curated registry concept, though the spec doesn't prescribe an API yet
This is the most impactful discovery improvement — it connects the local registry to the spec's Open Discovery mechanism.
Client-Side Agent Cache (A2A.Client.Registry)
A2A.Client.discover/2 fetches a remote card but doesn't store it — repeated
discovery hits the network every time. A client-side cache would:
- Store discovered
%AgentCard{}structs - Support
find_by_skill/2across remote agents - Enable patterns like: discover 10 agents at startup, route to the best one based on skill tags at call time
Registry Change Notifications
The current A2A.Registry is static after init. A Phoenix.PubSub or
:pg-based notification system would:
- Let Plug endpoints update when agents come and go
- Let distributed registries sync across nodes
Standard Registry API
The A2A community is exploring standardizing registry interactions. If a standard emerges, implement it as a Plug endpoint so agents can be registered in external catalogs.
Security
Client-Side OAuth 2.0 Flows
A2A.Client already supports Bearer/API key auth via Req options:
A2A.Client.new(card, headers: [{"authorization", "Bearer tok"}])More structured support could include: reading securitySchemes from a
discovered card and prompting for credentials, or OAuth 2.0 Client Credentials
flow built into the client.
Task-Level Access Control
The spec states: "Servers MUST NOT reveal the existence of resources the client
is not authorized to access." A2A.Plug now supports an :authorize_task
callback for task-scoped JSON-RPC operations.
tasks/getandtasks/cancelcall the callback before returning or mutating a task. Denied requests returnTaskNotFoundErrorso task IDs are not leaked.tasks/listfilters the returned page through the same callback.- The push notification config methods call it as
:push_set,:push_get,:push_listand:push_delete. 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. - The callback receives
(operation, task, context)whereoperationis:get,:cancel,:list,:push_set,:push_get,:push_list, or:push_delete, andcontext.metadatacontains the resolved Plug metadata, includingA2A.Plug.Authidentity under"a2a.auth"when that plug is used.
Remaining hardening:
- Move authorization down into task stores that can filter before pagination.
- Add store-specific examples for tenant and user ownership policies.
Agent Card Signature Verification
The spec allows agent cards to carry JWS signatures for authenticity verification. Not yet implemented. Would require:
A2A.AgentCard.verify_signatures/2— validate JWS signatures on a decoded agent card against a caller-supplied verifier functionAgentCardSignaturestruct for signature metadata (algorithm, key ID, signature value)- Verification is opt-in — callers choose whether to verify after decoding
- Depends on a JOSE library (e.g.,
jose) as an optional dependency
Recommended Security Order
- Agent card signature verification
- Authenticated extended card endpoint
- Client-side OAuth 2.0 flows
- Store-level authorization filters
Client
Stream Cancellation
A2A.Client.send_message_streaming/3 returns a Stream but provides no way
to explicitly cancel an in-flight SSE connection. A wrapper struct (similar to
a2a_ex's A2A.Client.Stream) would:
- Implement
Enumerablefor lazy consumption - Expose
cancel/1to abort the underlying HTTP connection - Clean up resources on early termination
SSE Reconnection with Backoff
Dropped SSE connections currently fail permanently. Production-grade streaming needs:
- Exponential backoff on connection failures (configurable base/max/jitter)
- Resume from
last-event-idheader on reconnect so no events are lost - Configurable max retry attempts before giving up
Challenge-Response Auth
When a server returns 401 with auth challenge headers, the client should be
able to auto-retry with appropriate credentials:
- Parse
WWW-Authenticateheaders from 401 responses - Invoke a caller-supplied auth callback to obtain credentials
- Retry the original request with the new credentials
- Integrates with the existing Req middleware pipeline
Observability
LiveDashboard Page (Optional)
An A2A.DashboardPage module implementing Phoenix.LiveDashboard.PageBuilder,
compiled only when phoenix_live_dashboard is available (same pattern as Oban).
Would show active agents, task counts, recent tasks with status/duration/errors,
and live state transitions.
Usability
A2A.Server Convenience Module
Wrap Bandit + Plug into a single startable child spec:
{A2A.Server, agent: MyApp.InvoiceAgent, port: 4001}Additional TaskStore Backends
Current: A2A.TaskStore.ETS (single-node). Potential additions:
A2A.TaskStore.PG— distributed via:pgA2A.TaskStore.Redis— requiresredixoptional dep
Forward-Compatible Type Decoding
A2A.JSON.decode/2 currently discards unrecognized JSON fields. To support
forward compatibility with newer spec versions:
- Preserve unknown fields in a
rawmap on decoded structs (or a dedicated_extrafield) - Round-trip unknown fields through encode/decode so data isn't silently lost
- Enables interop with agents running newer spec versions that include fields this library doesn't yet model
Extension Metadata Handling
Implemented. See A2A.Extension for the behaviour, A2A.Plug and
A2A.Client for A2A-Extensions header negotiation, and
A2A.Extension.Timestamp for a reference profile-extension. Method
extensions (registering new RPC methods) and state-machine extensions
remain deferred until a concrete user emerges.
Out of Scope
These are not planned for this library:
- LLM integration — use
instructor,langchain, etc. - Tool/function calling — use MCP via
hermes-mcp - Agent reasoning, planning, or memory — application-level concerns
- UI rendering — A2UI is a separate spec