Architecture Decision Records

Key design decisions for a2a-rust, documented as ADRs. Each record captures the context, decision, and rationale.

ADR 0001: Workspace Crate Structure

Status: Accepted

Context: The A2A protocol has three distinct concerns with different dependency trees: types (pure data, no I/O), client (HTTP sending), and server (HTTP receiving). A single-crate approach forces every user to compile the full dependency tree regardless of need.

Decision: Four crates in a Cargo workspace:

CratePurposeKey Dependencies
a2a-protocol-typesWire types onlyserde, serde_json
a2a-protocol-clientHTTP clienthyper, tokio
a2a-protocol-serverServer frameworkhyper, tokio
a2a-protocol-sdkUmbrella re-exportsAll above

Rationale: An agent server implementor doesn't pay for the client dependency tree and vice versa. The types crate is usable without any async runtime — useful for codegen, validation, or non-HTTP transports.

ADR 0002: Dependency Philosophy

Status: Accepted

Context: Rust SDK quality is inversely correlated with transitive dependency count. Each dependency adds compile time, supply chain attack surface, version conflicts, and potential license issues.

Decision: Minimal dependency footprint:

  • No web frameworks required (optional Axum integration via axum feature)
  • TLS through pure-Rust rustls, no OpenSSL (tls-rustls, a default feature of the client and SDK; default-features = false removes it)
  • Logging through tracing, a default feature since 0.14 (ADR 0013); default-features = false compiles it out
  • In-tree SSE parser instead of third-party crate
  • serde + hyper as the only mandatory heavyweight deps

Rationale: A production-grade SDK must work in corporate environments with strict dependency audits, embedded/constrained environments, and without forcing users into specific TLS or logging frameworks.

ADR 0003: Async Runtime Strategy

Status: Accepted

Context: Rust async code is runtime-agnostic at the language level, but hyper 1.x uses tokio internally. Making the SDK runtime-agnostic would require wrapping every I/O call behind an abstraction layer.

Decision: Tokio is the mandatory async runtime. It is a required dependency (not optional, not feature-gated) for the I/O crates. a2a-protocol-types has no async runtime dependency.

Rationale: >95% of Rust async production code runs on tokio. The complexity cost of runtime abstraction is not justified by the ~5% of users on other runtimes.

ADR 0004: Transport Abstraction Design

Status: Accepted (superseded in part by ADR 0009 — the gRPC transport is protobuf-native as of 0.7, no longer JSON-over-gRPC)

Context: A2A defines three transport bindings (JSON-RPC, REST, gRPC). Both client and server share protocol logic that must not be duplicated across transport implementations.

Decision: Three-layer architecture:

Dispatcher (transport) → RequestHandler (protocol) → AgentExecutor (user logic)
  • Dispatchers handle HTTP-level concerns (routing, content types, CORS)
  • RequestHandler contains all protocol logic (task lifecycle, stores, streaming)
  • AgentExecutor is the user's entry point

The Transport trait (client) and Dispatcher trait (server) make transports pluggable. Both share the same handler/client core.

Rationale: Adding gRPC support later requires only a new dispatcher/transport — zero changes to protocol logic or user code.

ADR 0005: SSE Streaming Design

Status: Accepted

Context: A2A streaming uses Server-Sent Events (SSE). Rust lacks a battle-tested, zero-dep SSE library that is hyper 1.x native.

Decision: In-tree SSE implementation for both client parsing and server emission:

  • Client parser: ~220 lines, handles partial TCP frames, enforces 16 MiB buffer cap
  • Server emitter: ~200 lines, formats SSE data: lines with proper \n\n terminators
  • Zero additional dependencies beyond hyper

Rationale: Third-party SSE crates either use older hyper versions, carry unnecessary dependencies, or lack proper backpressure. The in-tree implementation is small enough to audit, test, and maintain.

ADR 0006: Mutation Testing as a Required Quality Gate

Status: Accepted

Context: The test suite includes unit, integration, property, fuzz, and E2E dogfood tests — but none of these measure whether the tests actually detect real bugs. A test suite can achieve 100% line coverage with trivial assertions. At multi-data-center deployment scales, the bugs that escape traditional testing have the highest blast radius.

The full sweep runs weekly and on demand (workflow_dispatch); every pull request runs an incremental --in-diff mutation gate on the changed lines.

Rationale: Mutation testing is the only technique that directly measures fault detection capability. It provides an objective, automated answer to "would this test suite catch a real bug at this location?" The tradeoff is CI time: running the full sweep on every commit is punitive given current compute budgets, so the full sweep is scheduled, and pull requests are gated on the mutants in their own diff.

ADR 0007: Axum Integration and TCK Wire Format Tests

Status: Accepted

Context: The SDK lacked formal wire format conformance tests and required raw hyper for HTTP serving. Other SDKs integrate with their ecosystem's dominant web framework.

Decision: Two additions:

  1. TCK conformance tests — 44 golden-fixture tests in crates/a2a-protocol-types/tests/tck_wire_format.rs validating ProtoJSON serialization, SecurityRequirement/StringList format, Part discriminators, cross-SDK interop fixtures, and full round-trip.
  2. Axum integration — Feature-gated A2aRouter (axum feature) wrapping RequestHandler as an idiomatic axum::Router. Zero business logic duplication.

Rationale: TCK tests catch interop regressions before release. Axum integration reduces server setup from ~25 lines to 3 while remaining optional (the raw hyper serve() API is unchanged).

ADR 0008: Object-Safe AgentExecutor Trait Shape

Status: Accepted

Context: AgentExecutor is the primary extension point users implement. The obvious question a reader asks when opening executor.rs is: why not async fn execute(...)? — every method returns Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>, and every example in the repo wraps bodies with Box::pin(async move { ... }) or uses the boxed_future / agent_executor! helpers.

Decision: Keep AgentExecutor as a manual Pin<Box<dyn Future + Send + 'a>> trait (object-safe), and ship two ergonomic helpers (boxed_future and the agent_executor! macro) to compensate for the call-site noise. RequestHandler stores the executor as Arc<dyn AgentExecutor>, which keeps RequestHandler, every dispatcher, the Axum integration, and the builder API non-generic.

Rationale: Async-fn-in-trait is stable since Rust 1.75 but produces per-impl anonymous future types that are not object-safe on stable. Using async fn execute would force RequestHandler<E> to become generic, and that generic parameter would then leak through every dispatcher, the Axum layer, A2aRouter, and the builder API. The async-trait crate produces exactly the same Pin<Box<dyn Future>> shape we have now but hides the heap allocation and forces a proc-macro dep on every downstream user — a non-starter given ADR 0002. The manual shape is honest about the cost (one Box::pin allocation per call, deep in the noise relative to network RTT) and keeps the public API free of trait-erasure machinery. The agent_executor! macro gives trivial executors the same ergonomics as async fn without introducing a second trait. See ADR 0008 full document for the full alternatives analysis and the revisit trigger.

Summary

ADRKey Decision
0001Four-crate workspace (types, client, server, sdk)
0002Minimal mandatory dependencies, optional framework integration
0003Tokio as mandatory runtime
0004Three-layer architecture (dispatcher → handler → executor)
0005In-tree SSE parser/emitter, zero additional deps
0006cargo-mutants as a test-effectiveness gate: weekly full sweep, per-PR diff gate
0007Axum integration + TCK wire format conformance tests
0008AgentExecutor kept object-safe so RequestHandler stays non-generic
0009Protobuf-native gRPC on the canonical lf.a2a.v1.A2AService, wire-compatible with the official SDKs; the pre-0.7 JSON tunnel was deprecated behind grpc-legacy-json in 0.7 and removed in 0.8
0010First-party auth helpers (client OAuth2 client-credentials + token providers; server API-key/bearer/JWT interceptors) built on ring/hyper, not the OAuth-ecosystem crates; its "rejections map to InvalidRequest" consequence is superseded by ADR 0014
0011Task retention is explicit, and the store does not schedule it (Accepted)
0012An append-only event log, with the snapshot kept as the record (Accepted)
0013Spans, metrics and one telemetry entry point (the file's status is Proposed; its open question on making tracing a default feature records the maintainer's decision of 2026-09-23: yes)
0014A refused credential answers each binding's own status: HTTP 401/403, gRPC UNAUTHENTICATED/PERMISSION_DENIED (Accepted; supersedes ADR 0010's InvalidRequest mapping)

Only ADRs 0001–0008 have a section above; the table is the summary for all fourteen.

The full ADR documents are in the docs/adr/ directory.

Next Steps