Elixir A2A — Roadmap

Copy Markdown View Source

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 areaWhat it coversNotes
agent_cardCard shape, extensions, caching headers—
core_operationsMessage send, task lifecycle, data model, error handling—
jsonrpcJSON-RPC 2.0 envelope, error codes, error info—
grpcgRPC transport bindingSkipped — transport not implemented
http_jsonREST/HTTP+JSON bindingSkipped — 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)

TestsReasonUnblocked by
Extended agent cardsupportsAuthenticatedExtendedCard not declared#100
In-task authenticationAgent doesn't trigger auth-required stateOptional — agent-level decision
TLS / certificate validationTCK server runs plain HTTP on localhostDeploy-time concern, not library
CORE-MULTI-005 context inferenceTasks get no contextId when the client sends none, so the test cannot run#101
gRPC / HTTP+JSON transportsSingle transport (JSON-RPC only)gRPC / REST Transport Bindings (below)
OAuth2 metadata URLNo OAuth2 scheme configuredClient-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.

#FeatureTCK tests enabled
1Authenticated Extended Card (#100)mandatory/protocol/test_extended_agent_card.py; capabilities/ extended card tests
3gRPC Transport Bindingtransport-equivalence category (functional equivalence across transports)
4REST Transport Bindingtransport-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.PushNotificationConfig struct (id, task_id, url, token, authentication)
  • Optional A2A.TaskStore callbacks set_push_config/2, get_push_config/3, list_push_configs/2 and delete_push_config/3, with A2A.TaskStore.ETS keeping configs in a second table so they never reach the task read paths
  • Optional A2A.JSONRPC handler callbacks, so a handler that implements none of them keeps the old -32003 behaviour
  • A2A.Client functions 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_task hook
  • configuration.taskPushNotificationConfig honoured on message/send, so a client can register a webhook on the initial send rather than through CRUD. It runs the same :authorize_task hook under :push_set that 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 webhook
  • A2A.PushNotificationSender behaviour, with A2A.PushNotificationSender.HTTP as the default when :req is available. Every task state change POSTs a StreamResponse status update to each registered webhook, carrying the config's credentials as an Authorization header
  • 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/1 callback on the Agent behaviour — receives authenticated identity, returns an extended card with additional skills/capabilities
  • A2A.Plug serves 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 / TransportProtocol support 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.REST module (or fold into existing A2A.Plug with interface routing based on the request path)
  • AgentInterface / supportedInterfaces in 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_json vs proto_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/get for 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) — apply fun to the current task atomically, returning {:ok, updated_task} or {:error, reason}
  • A2A.TaskStore.ETS implementation 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/2 across 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/get and tasks/cancel call the callback before returning or mutating a task. Denied requests return TaskNotFoundError so task IDs are not leaked.
  • tasks/list filters the returned page through the same callback.
  • The push notification config methods call it as :push_set, :push_get, :push_list and :push_delete. They are distinct from :get so 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) where operation is :get, :cancel, :list, :push_set, :push_get, :push_list, or :push_delete, and context.metadata contains the resolved Plug metadata, including A2A.Plug.Auth identity 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 function
  • AgentCardSignature struct 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
  1. Agent card signature verification
  2. Authenticated extended card endpoint
  3. Client-side OAuth 2.0 flows
  4. 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 Enumerable for lazy consumption
  • Expose cancel/1 to 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-id header 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-Authenticate headers 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 :pg
  • A2A.TaskStore.Redis — requires redix optional 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 raw map on decoded structs (or a dedicated _extra field)
  • 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