a2a-rust — Agent2Agent (A2A) Protocol SDK for Rust

Introduction

a2a-rust is a pure Rust implementation of the Agent2Agent (A2A) protocol — an open standard for connecting AI agents over the network. It is written against the v1.0.1 specification; the version carried on the wire is 1.0, since §3.6 excludes patch numbers from requests, responses and Agent Cards.

If you're building AI agents that need to talk to each other, discover capabilities, delegate tasks, and stream results — this library gives you the full protocol stack — all eleven methods over four bindings — with no unsafe in its library code and hardening measured by the gates this book describes. It is 0.x: Stability says what may still change, and the readiness bar says what "ready for production" would have to mean and how much of it is measured.

What is the A2A Protocol?

The Agent2Agent protocol defines how AI agents discover, communicate with, and delegate work to each other. Think of it as HTTP for AI agents: a shared language that lets any agent talk to any other, regardless of implementation.

The protocol defines:

  • Agent Cards — Discovery documents describing what an agent can do
  • Tasks — Units of work with well-defined lifecycle states
  • Messages — Structured payloads carrying text, data, or binary content
  • Streaming — Real-time progress via Server-Sent Events (SSE)
  • Push Notifications — Webhook delivery for async results

Why Rust?

What Rust gives this library, stated as narrowly as it holds:

  • Predictable cost — no garbage collector or runtime beyond tokio. The extension points are trait objects (Arc<dyn AgentExecutor>, boxed futures), so each call pays a dynamic dispatch and an allocation; the benchmark pages measure what that costs
  • No unsafe in library code — every published crate is #![forbid(unsafe_code)]. The attribute does not reach build scripts: the three protobuf-compiling crates' build.rs wrap std::env::set_var("PROTOC", …) in unsafe, which edition 2024 requires, and so do the TCK runner's and the ITK's
  • Thread safety at compile time — the compiler checks every cross-thread use; Send + Sync is asserted by test for the core types (Task, TaskState, Message, Part, AgentCard and the three error types), not for every public type
  • Forward-compatible enums — protocol enums such as TaskState are #[non_exhaustive], so a variant a later specification revision adds is a patch-level addition (Stability §4), not a break for a match that has a wildcard arm. The cost is that your match needs that wildcard arm, and the compiler cannot tell you a state is new. A state number this SDK does not know is refused by the gRPC conversion (unknown TaskState number), not mapped to a variant
  • Errors, not panics — HTTP is hyper's. Fallible operations return Result; a CI gate (scripts/check_panic_paths.py) freezes the set of unwrap, expect, panic!, unreachable! and todo! in library code, and the 13 expect calls it allows each assert an internal invariant (a poisoned lock, a retry loop that always runs once, a TLS provider that supports its own defaults). The gate cannot see arithmetic overflow or slice indexing, so "never panics" is not a claim it supports

Architecture at a Glance

a2a-rust is organized around four published library crates in one Cargo workspace:

┌─────────────────────────────────────────────┐
│  a2a-protocol-sdk                           │
│  umbrella re-exports + prelude              │
├──────────────────────┬──────────────────────┤
│  a2a-protocol-client │  a2a-protocol-server │
│  HTTP client         │  agent framework     │
├──────────────────────┴──────────────────────┤
│  a2a-protocol-types                         │
│  wire types, serde, no I/O                  │
└─────────────────────────────────────────────┘
CratePurpose
a2a-protocol-typesAll A2A wire types with serde serialization. Pure data — no I/O, no async.
a2a-protocol-clientHTTP client for calling remote A2A agents. Supports JSON-RPC and REST transports, plus WebSocket and gRPC behind feature flags.
a2a-protocol-serverServer framework for building A2A agents. Pluggable stores, interceptors, and dispatchers.
a2a-protocol-sdkUmbrella crate that re-exports everything with a convenient prelude module.

Key Features

  • v1.0 wire types — every A2A type, serialized to the specification's JSON and protobuf shapes and checked by golden-byte fixtures and the official TCK; the TCK's gaps are listed in the README's Project Status
  • Quad transport — JSON-RPC 2.0, REST and gRPC (grpc feature flag), the three bindings the specification names, plus WebSocket (websocket feature flag), a custom binding this project defines under spec §12; client and server. A fifth, SLIMRPC, is a community-contributed crate outside the workspace
  • SSE streaming — Real-time SendStreamingMessage and SubscribeToTask
  • Push notifications — Pluggable PushSender with SSRF protection
  • Agent card discovery — Static and dynamic card handlers with HTTP caching (ETag, Last-Modified, 304)
  • Pluggable stores — TaskStore and PushConfigStore traits with in-memory, SQLite, PostgreSQL, and tenant-aware backends
  • Multi-tenancy — TenantAwareInMemoryTaskStore, TenantAwareSqliteTaskStore, and TenantAwarePostgresTaskStore with full tenant isolation
  • Interceptor chains — Client and server middleware for auth, logging, metrics, rate limiting
  • Rate limiting — Built-in RateLimitInterceptor with per-caller fixed-window limiting
  • Client retry — Configurable RetryPolicy with exponential backoff for transient failures
  • Server startup helper — serve() reduces ~25 lines of hyper boilerplate to one call
  • Request ID propagation — CallContext::request_id auto-extracted from X-Request-ID header
  • Task store metrics — TaskStore::count() for monitoring and capacity management
  • Task state machine — TaskState::can_transition_to enforces the two rules the specification implies: terminal states are final, and nothing re-enters Submitted (or the proto default, Unspecified). The specification itself defines no transition matrix
  • Executor ergonomics — boxed_future, agent_executor! macro, EventEmitter reduce boilerplate
  • Executor timeout — an executor still running after the limit (default one hour, with_executor_timeout) has its task marked failed with a timeout error
  • CORS support — Configurable cross-origin policies
  • Configurable — the handler's timeouts, limits and intervals are overridable via builders, and the book's defaults table is checked against the code. A few bounds are fixed constants (the client's 2 MiB cap on an agent card body is one)
  • Mutation-tested — every pull request must pass cargo-mutants over the functions it changes: the shards run once the PR carries the mutation-test label, the required aggregator fails until they have, and a surviving mutant fails them. That gate is --in-diff: code no pull request has touched since the gate existed is covered only by the weekly full sweep, which is advisory and has recorded survivors (mutation history)

All 11 Protocol Methods

MethodDescription
SendMessageSynchronous message send; returns the task once it is terminal or interrupted (INPUT_REQUIRED, AUTH_REQUIRED), or a direct Message
SendStreamingMessageStreaming send with real-time SSE events
GetTaskRetrieve a task by ID
ListTasksQuery tasks with filtering and pagination
CancelTaskRequest cancellation of a running task
SubscribeToTaskRe-subscribe to an existing task's event stream
CreateTaskPushNotificationConfigRegister a webhook for push delivery
GetTaskPushNotificationConfigRetrieve a push config by ID
ListTaskPushNotificationConfigsList all push configs for a task
DeleteTaskPushNotificationConfigRemove a push config
GetExtendedAgentCardFetch authenticated agent card

What's Next?