Skip to content

feat(transport): gRPC + REST transports for A2A multi-transport#41

Open
zeroasterisk wants to merge 1 commit into
actioncard:mainfrom
zeroasterisk:feat/grpc-rest-transports
Open

feat(transport): gRPC + REST transports for A2A multi-transport#41
zeroasterisk wants to merge 1 commit into
actioncard:mainfrom
zeroasterisk:feat/grpc-rest-transports

Conversation

@zeroasterisk
Copy link
Copy Markdown
Contributor

Summary

Adds two additional A2A transports alongside the existing JSON-RPC binding, advancing ITK multi-transport interoperability (gap #4).

REST / HTTP-JSON transport (lib/a2a/transport/rest/)

  • Direct HTTP+JSON client and Plug-based server (no JSON-RPC wrapper), compatible with the Python/Go REST implementations.
  • Endpoints: message:send, messages (poll), agents (register/get), tasks:cancel, card.

gRPC transport (lib/a2a/grpc.ex, lib/a2a/grpc/)

  • Client + server with priv/proto/a2a.proto defining the A2A gRPC service contract.

Transport abstraction

  • A2A.Transport behaviour (lib/a2a/transport.ex) unifying transport selection; rest/grpc adapters under lib/a2a/transport/.
  • All optional deps guarded with Code.ensure_loaded?/1 (library stays dependency-light).

Bug fixes found during finalization

  • encode_agent_card/2 requires opts[:url] via Keyword.fetch!/2, but several callers passed [] → runtime KeyError. Corrected to pass url: agent_card.url.
  • Tightened encode_agent_card/2 input spec from A2A.Agent.card() (requires an :opts field) to A2A.AgentCard.t() — the struct actually passed by all callers (resolves a dialyzer contract error).

Verification

  • mix test: 438 tests, 0 failures (stable across repeated runs)
  • mix quality: format + credo (--strict) + dialyzer all pass, 0 errors

Notes

Opened from fork zeroasterisk:feat/grpc-rest-transports. CI on fork PRs requires maintainer approval (action_required).

Implements two additional A2A transports alongside the existing JSON-RPC
binding, advancing ITK multi-transport interoperability (gap actioncard#4):

- REST/HTTP-JSON transport (lib/a2a/transport/rest/): direct HTTP+JSON
  client and Plug-based server, compatible with Python/Go REST impls.
  Endpoints: message:send, messages (poll), agents (register/get),
  tasks:cancel, card.
- gRPC transport (lib/a2a/grpc.ex, lib/a2a/grpc/): client + server with
  priv/proto/a2a.proto defining the A2A gRPC service contract.
- A2A.Transport behaviour (lib/a2a/transport.ex) unifying transport
  selection; rest/grpc adapters under lib/a2a/transport/.
- All optional deps guarded with Code.ensure_loaded?/1.

Fixes a latent bug: encode_agent_card/2 requires opts[:url] via
Keyword.fetch!/2, but several callers passed []; corrected to pass
url: agent_card.url. Also tightened encode_agent_card/2 input spec from
A2A.Agent.card() (which requires an :opts field) to A2A.AgentCard.t(),
the struct actually passed by all callers (clears a dialyzer contract
error).

mix test: 438 tests, 0 failures. mix quality: format + credo + dialyzer
all pass, 0 errors.
@maxekman
Copy link
Copy Markdown
Contributor

maxekman commented Jun 4, 2026

Thanks for the contribution! After reviewing this against the v1.0 tracking issue (#13) and the existing transport layer, I'd like to request changes and propose splitting this up.

Scope vs #13

This PR isn't part of the v1.0 roadmap tracked in #13 — that issue tracks: A2A-Version header, push notification CRUD, multi-tenant server. Multi-transport work is explicitly deferred per docs/ITK_BASELINE.md:149 ("gRPC / REST transports… deferred"). The PR body ties it to ITK interop gap #4, which is a separate initiative. I'd like to keep #13 focused on the remaining v1.0 protocol items and track transports under their own issue.

gRPC is currently a stub

Every gRPC client method returns {:error, :grpc_not_implemented} (six methods in lib/a2a/grpc/client.ex), every server service handler returns {:error, :unimplemented} (lib/a2a/grpc/server.ex), and the server loop is acknowledged in-source as a placeholder. The tests assert this stub behavior verbatim:

# All methods should return :grpc_not_implemented for now
assert {:error, :grpc_not_implemented} = Client.send_message(client, message)

priv/proto/a2a.proto ships but isn't wired into the build (no protobuf compiler in mix.exs, no generated modules used) — the runtime encoding is hand-rolled maps in A2A.GRPC that duplicate A2A.JSON. Meanwhile the README advertises gRPC as a working transport. I'd rather ship nothing than a transport that returns hardcoded errors.

REST bypasses the existing agent contract

A2A.JSONRPC is already documented as a transport-agnostic dispatch layer with callbacks (handle_send / handle_get / handle_cancel / handle_listlib/a2a/jsonrpc.ex:62-77), and A2A.Plug is a thin HTTP adapter over it. A REST transport should reuse that dispatch and the existing A2A.Agent behaviour, only differing in wire framing.

A2A.Transport.REST.Server instead invents a parallel handler interface (handle_message/2, poll_messages/1, register_agent/1, get_agent/1, get_card/0, get_task/1, cancel_task/1). This is a second, incompatible agent contract. It also means the A2A.Extension pipeline (wired into A2A.Plug) silently doesn't run on REST.

Other smaller issues:

  • lib/a2a/grpc.ex uses Process.put(:a2a_grpc_agent, …) to pass per-request state — should thread context through callback args like JSON-RPC does.
  • REST streaming is a TODO that silently downgrades to non-streaming (lib/a2a/transport/rest/server.ex, handle_send_message_streaming).
  • A2A.Transport ends up being a discovery enum rather than a behaviour — REST and gRPC don't share a contract; the abstraction adds no leverage as-is.
  • mix.exs declares {:grpcbox, \"~> 0.16\"} but mix.lock resolved to 0.17.1.
  • No end-to-end test for either transport (REST tests use a mock handler; gRPC tests assert stubs).

The encode_agent_card/2 fix

The lib/a2a/json.ex spec change (A2A.Agent.card()A2A.AgentCard.t()) is a legit bug fix unrelated to transports — worth keeping. Could you split that into its own small PR? It can land independently.

Proposed split

  1. PR A — the encode_agent_card/2 spec/caller fix on its own (small, mergeable immediately).
  2. PR B — REST transport, reusing A2A.JSONRPC dispatch + the A2A.Agent behaviour, with real SSE streaming and an end-to-end test. The A2A.Extension pipeline needs to be wired in.
  3. Defer gRPC — close the gRPC portion until grpcbox is actually integrated and priv/proto/a2a.proto is compiled into generated modules. Track under a separate issue.

Happy to help shape PR B once we agree on the split. Thanks again for the work and apologies for the rework ask — most of this is about keeping the existing transport-agnostic seam (A2A.JSONRPC + A2A.Agent) load-bearing rather than introducing a parallel one.

@zeroasterisk
Copy link
Copy Markdown
Contributor Author

@maxekman Thank you for the comprehensive review. Your feedback is spot-on. I agree with the proposed split approach:

Split Plan:

  1. PR A (immediate): Extract the encode_agent_card/2 spec/caller fix to a standalone PR
  2. PR B (refactor): REST transport reusing A2A.JSONRPC dispatch + A2A.Agent behaviour
  3. Defer gRPC: Close gRPC portion until proper protobuf compilation + generated modules

Acknowledged Issues:

  • gRPC is indeed stub-only (hardcoded :grpc_not_implemented returns)
  • REST bypasses existing agent contract (should reuse A2A.JSONRPC callbacks)
  • Missing A2A.Extension pipeline integration for REST
  • mix.exs vs mix.lock version mismatch on grpcbox
  • No end-to-end tests

Next Steps:
I'll close this PR and start with PR A (the agent card fix) as the immediate mergeable piece, then design PR B properly around the existing transport-agnostic seam.

Current Quality Gate Status:

  • mix test: Running (dependencies resolved)
  • mix format: Fixed untracked file, re-running
  • Need to validate ITK compliance once tests complete

Will update with standalone PR for the agent card fix shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants