The A2A CLI and Rust: The Same Code, Three Ways to Serve It
---
title: "The A2A CLI and Rust: The Same Code, Three Ways to Serve It"
published: true
description: "A tour of the A2A CLI against Rust agents on A2A v1.0: the official Rust SDK, a plain stdin/stdout program wrapped by a2a server --exec, and an ADK coordinator on Gemini, each timed from the CLI and from a Rust client."
tags: rust, a2a, ai, agents
cover_image: raw.githubusercontent.com
---
This article provides a step by step guide to the A2A CLI, driven against Rust agents that speak A2A Protocol v1.0. The same Rust code, a Lucas-Lehmer test over the first N Mersenne prime exponents, is served three ways: on the official A2A Rust SDK, as a plain stdin/stdout program wrapped by a2a server --exec, and behind an ADK coordinator on Gemini started with the ADK CLI. Each one is timed from the A2A CLI and from a Rust client.
github.com/xbill9/rust-dev/tree/main/a2a-benchmar…
The A2A CLI talks to all three with the same commands, and a2a server --exec turns a 30-line program with no A2A code into an agent that ADK can call. One a2a send from a shell takes a median 20.74 ms against the SDK agent, of which 7.62 ms is starting the CLI. From a Rust client the SDK agent adds 0.55 to 1.40 ms per call, the wrapped program 3.33 to 5.05 ms, and the ADK coordinator a median 1328 ms on gemini-3.5-flash and 2283 ms on gemini-3.8-flash. Every call through N=26 returned the right count of primes.
---
What Is Being Built?
| Agent | Port | How it speaks A2A |
| :--- | :--- | :--- |
| mersenne-agent | 8104 | Official Rust SDK, JSON-RPC at /jsonrpc and HTTP+JSON at /rest |
| mersenne-exec | 8105 | A stdin/stdout Rust program served by a2a server --exec |
| rust_bench | 8106 | ADK LlmAgent on Gemini, served by adk api_server --a2a |
| echo | 8108 | a2a server --echo, for the first command |
a2a CLI ──► adk api_server --a2a (:8106) ──Gemini──► RemoteA2aAgent ──► mersenne-agent (:8104)
rust_bench └──► a2a server --exec (:8105) ──► mersenne-exec
└── run_benchmark tool ──► mersenne-bench ──► both Rust agents
Versions, read from the machine on 2026-11-03:
| Component | Version |
| :--- | :--- |
| A2A protocol | 1.0 |
| A2A CLI (a2a) | v0.3.1 |
| a2a-lf / a2a-server-lf / a2a-client-lf | 0.4.2 / 0.5.3 / 0.2.9 |
| Rust | 1.99.1, edition 2024 |
| google-adk / a2a-sdk / google-genai | 2.13.0 / 1.3.0 / 2.34.0 |
| Model | gemini-3.8-flash on Vertex AI |
The a2a-*-lf crates are the official SDK from a2aproject/a2a-rs. Agent cards carry the protocol as Major.Minor, so they read 1.0.
---
Step 1 — Install the A2A CLI
The CLI is one Go binary. Homebrew, WinGet and release binaries work; from source:
go install github.com/a2aproject/a2a-cli@latest
mv "$(command -v a2a-cli)" "$(dirname "$(command -v a2a-cli)")/a2a"
a2a version
a2a v0.3.1
commit: none
built: unknown
go: go1.26.3
os/arch: linux/amd64
go install names the binary a2a-cli; the docs and the CLI's own skill call it a2a.
---
Step 2 — Talk to an Echo Server
a2a server --echo starts an agent that sends back what it receives, so the client can be checked before any agent code exists:
a2a server --echo --port 8108 &
a2a send -a http://localhost:8108 "hello world from A2A"
Task: 01a1a35f-2b2b-7f3a-92a2-e3dc6c69e359
Context: 01a1a35f-2b2c-7120-ac45-a74adab9d1d2
Status: completed (2026-11-03T16:44:45-05:00)
Artifacts:
[01a1a35f-2b2c-71ba-8e6c-22c4b7c210df] hello world from A2A
History:
[user] hello world from A2A
Every send creates a task with an id, a context, a final status and its artifacts. The Go servers started by the CLI stamp local time; the Rust SDK agent stamps UTC.
---
Step 3 — Build and Start the Rust Agents
The rig is one Cargo project with four binaries: the SDK agent, the stdin/stdout program, the benchmark client, and a shared library holding the Lucas-Lehmer code.
git clone https://github.com/xbill9/rust-dev
cd rust-dev/a2a-benchmark-rust
cargo build --release
scripts/start-all.sh
agents up: 8104 native, 8105 exec
start-all.sh rebuilds, stops any running copy, starts the SDK agent on 8104 and a2a server --exec on 8105 with logs in logs/, and waits until both cards answer. scripts/test-cli.sh runs the CLI commands below against them and checks each reply.
---
Step 4 — Read an Agent Card
card get fetches /.well-known/agent-card.json and prints what the agent offers:
a2a card get -a http://localhost:8104
Name: Mersenne Prime Agent Rust
Description: A Rust A2A v1.0 agent (official a2a-rs SDK) that confirms the first n Mersenne primes with the Lucas-Lehmer test and reports the elapsed time.
Version: 0.1.0
Interfaces:
JSONRPC (A2A v1.0) http://localhost:8104/jsonrpc
HTTP+JSON (A2A v1.0) http://localhost:8104/rest
Streaming: true
Skills:
find-mersenne-rust Find Mersenne Primes in Rust
The card lists two interfaces. The CLI picks one, and --transport chooses.
---
Step 5 — Send, Stream, Switch Transport, Send Data
A plain message:
a2a send -a http://localhost:8104 "Calculate the first 12 Mersenne primes"
Status: completed (2026-11-03T21:44:45Z)
Found first 12 Mersenne primes in 0.0902ms.
Artifacts:
[mersenne-report] Found first 12 Mersenne primes in 0.0902ms. {"elapsed_ms":0.090239,"exponents":[2,3,5,7,13,17,19,31,61,89,107,127],"found":12,"requested":12}
The mersenne-report artifact carries two parts: the summary as text, and the same result as a data part, so a client reads the timing from a field.
--stream prints each event as it arrives, here the working status before the result:
a2a send -a http://localhost:8104 --stream "first 8"
[status] working
Task: 01a1a35f-2b64-77b5-9417-18b1f425b17d
Context: 01a1a35f-2b64-77b5-9417-18b24253b719
Status: completed (2026-11-03T21:44:45Z)
Found first 8 Mersenne primes in 0.0164ms.
--transport rest sends the same request over HTTP+JSON, and --data-part sends structured input:
a2a send -a http://localhost:8104 --transport rest "first 4"
a2a send -a http://localhost:8104 --data-part '{"count": 6}'
Found first 4 Mersenne primes in 0.0080ms.
Found first 6 Mersenne primes in 0.0108ms.
---
🔎 Tip: Data Parts Arrive as Floats
A2A v1.0 carries data parts as a protobuf Struct, whose only number type is a double. {"count": 6} sent by the CLI reaches the agent as 6.0, so a strict integer field fails to deserialise. Read numbers with as_f64 and check for a whole value:
if let PartContent::Data(v) = &part.content
&& let Some(n) = v.get("count").and_then(|n| n.as_f64())
&& n >= 1.0
&& n.fract() == 0.0
{
return n as usize;
}
---
Step 6 — Script It With JSON Output
-o json prints the whole task, which pipes into anything:
a2a send -a http://localhost:8104 -o json 'first 3' | python3 -c 'import sys,json; t=json.load(sys.stdin)["task"]; print(t["status"]["state"], [p for a in t["artifacts"] for p in a["parts"] if "data" in p][0]["data"])'
TASK_STATE_COMPLETED {'elapsed_ms': 0.007166, 'exponents': [2, 3, 5], 'found': 3, 'requested': 3}
---
Step 7 — Tasks: List, Start in the Background, Wait
The SDK agent keeps its tasks in memory, and the CLI reads them back:
a2a task list -a http://localhost:8104
ID STATUS CONTEXT
01a1a35f-2b4f-7190-b6df-81bb7b76c2cc completed 01a1a35f-2b4f-7190-b6df-81bca35e8b6d
01a1a35f-2b64-77b5-9417-18b1f425b17d completed 01a1a35f-2b64-77b5-9417-18b24253b719
01a1a35f-2bab-7012-b094-1022dce75f36 completed 01a1a35f-2bab-7012-b094-1023824b35eb
--async returns as soon as the task exists. N=25 takes over a second, long enough to catch it running:
a2a send -a http://localhost:8104 --async "first 25"
Task: 01a1a3f5-9da1-746b-abae-84307e4e1735
Context: 01a1a3f5-9da1-746b-abae-8431b422e72e
Status: working (2026-11-04T00:29:05Z)
History:
[user] first 25
task get --wait polls until the task finishes:
a2a task get -a http://localhost:8104 01a1a3f5-9da1-746b-abae-84307e4e1735 --wait --poll-interval 500ms
Task: 01a1a3f5-9da1-746b-abae-84307e4e1735
Context: 01a1a3f5-9da1-746b-abae-8431b422e72e
Status: completed (2026-11-04T00:29:06Z)
Found first 25 Mersenne primes in 1509.7683ms.
---
Step 8 — The Native Rust Agent
The SDK agent is one AgentExecutor. It emits a working status, runs the big-integer work on a blocking thread, then returns the finished task:
let done = async move {
// Big-integer work is CPU-bound; keep it off the async workers.
let report = tokio::task::spawn_blocking(move || find_mersenne_primes(count))
.await
.map_err(|e| A2AError::internal(format!("worker panicked: {e}")))?;
let summary = report.summary();
let data = serde_json::to_value(&report)
.map_err(|e| A2AError::internal(format!("serialise report: {e}")))?;
// ... Task { status: Completed, artifacts: [Part::text(summary), Part::data(data)] }
};
Box::pin(stream::once(async { Ok(working) }).chain(stream::once(done)))
The server wires the SDK's routers into axum, one per transport plus the card:
let handler = Arc::new(DefaultRequestHandler::new(MersenneExecutor, InMemoryTaskStore::new()));
let card = Arc::new(StaticAgentCard::new(agent_card(&base)));
let app = axum::Router::new()
.nest("/jsonrpc", a2a_server::jsonrpc::jsonrpc_router(handler.clone()))
.nest("/rest", a2a_server::rest::rest_router(handler))
.merge(a2a_server::agent_card::agent_card_router(card));
The Lucas-Lehmer test runs on num-bigint with the reduction mod 2^p − 1 done by folding the high bits onto the low ones. The timed region holds only the tests, on a monotonic clock, and no prime is ever turned into a decimal string. Two slow tests check the arithmetic: one runs the test on every prime exponent up to 9941 and asserts it accepts exactly the first 22 known ones, the other confirms all 26:
cargo test --release --lib -- --ignored
test tests::all_26_exponents_prime ... ok
test tests::exhaustive_through_n22 ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 25.70s
---
Step 9 — Serve a Plain Program With a2a server --exec
mersenne-exec knows nothing about A2A. It reads the message on stdin and prints two lines:
echo 'first 5' | target/release/mersenne-exec
{"requested":5,"found":5,"elapsed_ms":0.007023000000000001,"exponents":[2,3,5,7,13]}
Found first 5 Mersenne primes in 0.0070ms.
The A2A CLI makes it an agent:
a2a server --exec "$PWD/target/release/mersenne-exec" --chunk=$'\n' \
--name "Mersenne Prime Exec Rust" \
--advertise-address "http://localhost:8105" --port 8105
The same client commands work against it:
a2a send -a http://localhost:8105 --stream "first 8"
Status: submitted
History:
[user] first 8
[status] working
[artifact] {"requested":8,"found":8,"elapsed_ms":0.01543,"exponents":[2,3,5,7,13,17,19,31]}Found first 8 Mersenne primes in 0.0154ms.
[status] completed
The exec server starts the program once per message, hands it the text, and returns stdout as the artifact.
---
🔎 Tip: Two --exec Flags ADK Needs
- --advertise-address http://localhost:8105. The exec server's card names 127.0.0.1 by default, and ADK's RemoteA2aAgent refuses a card whose RPC URL has a different origin from the one the card came from.
- --chunk=$'\n'. With it, each output line streams as its own chunk. Without it, stream requests get UNSUPPORTED_OPERATION: the Go CLI falls back to polling and ADK's Python client fails. ADK keeps only the last chunk, so mersenne-exec prints the JSON line first and the summary last.
---
Step 10 — Put ADK in Front
ADK needs its Python package and Gemini on Vertex AI, signed in with application-default credentials:
pip install -U "google-adk[a2a]"
gcloud auth application-default login
source scripts/adk-env.sh
adk-env.sh sets GOOGLE_GENAI_USE_VERTEXAI=TRUE, GOOGLE_CLOUD_LOCATION=global, the project, and MODEL_NAME, which defaults to gemini-3.8-flash.
adk/rust_bench/agent.py is an ADK LlmAgent with both Rust agents as RemoteA2aAgent sub-agents and one tool:
native_agent = RemoteA2aAgent(
name="native_rust_agent",
description="Rust A2A v1.0 agent on the official a2a-rs SDK. ...",
agent_card=f"{NATIVE_URL}{AGENT_CARD_WELL_KNOWN_PATH}",
)
root_agent = LlmAgent(
name="rust_bench",
model=os.getenv("MODEL_NAME", "gemini-3.8-flash"),
tools=[run_benchmark],
sub_agents=[native_agent, exec_agent],
)
The ADK CLI runs it three ways:
adk run rust_bench # terminal
adk web --port 8107 # dev UI
adk api_server --a2a --port 8106 adk # A2A server, from the rig root
adk api_server --a2a serves each agent folder that holds an agent.json card, at /a2a/<name>; scripts/start-adk.sh starts it in the background. The A2A CLI then treats the coordinator like any other agent:
a2a send -a http://localhost:8106/a2a/rust_bench/.well-known/agent-card.json \
--timeout 120s "Find the first 9 Mersenne primes with the exec agent"
Status: completed (2026-11-03T21:44:51Z)
Artifacts:
[f2d2f27d-24ce-4120-8368-d966c99617a9] Found first 9 Mersenne primes in 0.0236ms.
History:
[user] Find the first 9 Mersenne primes with the exec agent
[agent] {"args":{"agent_name":"exec_rust_agent"},"id":"call_1332004","name":"transfer_to_agent"}
[agent] {"id":"call_1332004","name":"transfer_to_agent","response":{"result":null}}
[agent] {"requested":9,"found":9,"elapsed_ms":0.023635,"exponents":[2,3,5,7,13,17,19,31,61]}
[agent] Found first 9 Mersenne primes in 0.0236ms.
The history shows the hand-off: Gemini calls transfer_to_agent, the Rust agent answers, and its JSON report comes back through two A2A hops.
---
Step 11 — Keep the Model Away From the Numbers
A benchmark through an LLM can time the model writing a number. Two rules keep the timing in code:
- The benchmark client checks every reply. mersenne-bench reads the timing from the agent's data part or JSON line, rejects a report where requested differs from N or found differs from min(N, 26), and rejects an LLM-path reply with no transfer_to_agent to a Rust agent. Each row records where its report came from and who it was routed to.
- ADK benchmarks go through a tool. run_benchmark runs mersenne-bench and returns medians already computed and rounded. The instruction tells Gemini to quote them, and scripts/check-adk-reply.py verifies a reply: one run_benchmark call, with the arguments asked for, and every number in the answer a value the tool returned.
A benchmark request sent from the CLI, saved with -o json. The tool call and the head of the answer:
a2a send -a $CARD --timeout 300s -o json "Benchmark both agents for N=3 to 6 with 2 reps" > reply.json
[tool call] {"max_n": 6, "min_n": 3, "reps": 2}
### Arguments Used
- `min_n`: 3
- `max_n`: 6
- `reps`: 2
The answer goes on with a summary per agent and a row per N. The check:
scripts/check-adk-reply.py reply.json 3 6 2
state: TASK_STATE_COMPLETED
run_benchmark calls: 1
args sent: {'max_n': 6, 'min_n': 3, 'reps': 2} requested: {'min_n': 3, 'max_n': 6, 'reps': 2}
numbers in reply: 57 not from the tool: 0
PASS
scripts/adk-reliability.sh sends ten benchmark requests with varied ranges and ten requests that name no agent. On gemini-3.8-flash, 10 of 10 benchmark replies passed the checker and 10 of 10 unnamed requests went to native_rust_agent.
The sweep covers the whole exponent table, N=1 to 26. Exponent 23209 is a 6,987-digit number, and the arithmetic stays in BigUint throughout.
---
Step 12 — Benchmark From a Rust Client
REPS=20 scripts/bench.sh # native + exec, N=1..26
ADK=1 REPS=3 scripts/bench.sh # adds the ADK series
mersenne-bench is built on the SDK's client crate, a2a-client-lf. Each call records the agent's own compute time and the client's round trip; overhead is the round trip minus compute, per call. Direct calls, medians of 20 reps:
| N | native compute ms | native overhead ms | exec compute ms | exec overhead ms |
| ---: | ---: | ---: | ---: | ---: |
| 1 | 0.001039 | 0.731 | 0.0004555 | 4.046 |
| 10 | 0.04249 | 0.793 | 0.04200 | 4.391 |
| 20 | 26.33 | 0.970 | 60.21 | 4.374 |
| 22 | 181.1 | 1.255 | 264.6 | 3.558 |
| 24 | 781.1 | 1.073 | 868.9 | 3.335 |
| 26 | 2484 | 1.396 | 2619 | 3.588 |
Through ADK, medians of 3 reps, one run per model. All 26 rows in each run went to native_rust_agent and read its data part:
| N | 3.5-flash compute ms | 3.5-flash round trip ms | 3.8-flash compute ms | 3.8-flash round trip ms |
| ---: | ---: | ---: | ---: | ---: |
| 1 | 0.002051 | 1665.053 | 0.002615 | 2073.666 |
| 10 | 0.04513 | 1394.245 | 0.04390 | 2458.155 |
| 20 | 72.95 | 1332.994 | 68.52 | 2336.569 |
| 22 | 279.8 | 1868.178 | 287.3 | 2679.324 |
| 26 | 2226 | 3455.492 | 2241 | 4123.353 |
The slowest single call on gemini-3.8-flash took 29.0 s, at N=9.
---
Step 13 — Time the A2A CLI Itself
scripts/cli-bench.py runs one a2a send -o json process per call, the way a shell script would, and applies the same checks to each reply. Wall time covers starting the CLI, fetching the card, sending, and exiting. Two reference commands, 20 runs each:
| Command | Median ms | Min ms | Max ms |
| :--- | ---: | ---: | ---: |
| a2a version | 7.17 | 5.57 | 9.62 |
| a2a card get (native) | 9.18 | 8.30 | 11.17 |
The full sweep, N=1 to 26, 5 reps direct and 3 through ADK:
| Agent | Calls | Overhead per call, per-N medians | Median of the per-N medians |
| :--- | ---: | :--- | ---: |
| native | 130 | 10.92 to 21.82 ms | 20.28 ms |
| exec | 130 | 9.43 to 20.81 ms | 17.51 ms |
| ADK, 3.8-flash | 78 | 1825.22 to 3017.92 ms | 2217.55 ms |
Every ADK call went to native_rust_agent; the slowest took 22693.6 ms. The sweep times each agent in its own block, so native and exec ran minutes apart. Run in alternating order, N=1, 30 runs each (scripts/cli-transport.py):
native, default (JSON-RPC): median 20.74 ms, min 18.60, max 26.51
native, --transport rest: median 21.49 ms, min 19.63, max 26.15
exec (HTTP+JSON only): median 20.81 ms, min 18.67, max 28.07
a2a version: median 7.62 ms, min 6.07, max 10.71
From a shell, the transport and the serving path make no difference to the CLI's cost: about 21 ms per call, about a third of it starting the process. Against the Rust client's 0.55 to 1.40 ms, the CLI fits scripts and checks, and the Rust client fits a benchmark.
The CLI's overhead in the sweep drops to about 11 ms for both agents once compute passes 100 ms (N=21 and up). Why is untested.
---
🔎 Tip: Compare Compute Only Between Runs Paced Alike
The same native process at N=20 takes 26.07 ms when calls arrive back to back and 68.09 ms when each call follows a 1.3 s idle gap (mersenne-bench --gap-ms 1300). ADK's model round trip spaces calls out too, and its mid-range compute times sit close to the gap series: 68.52 ms at N=20 on gemini-3.8-flash. The CPU-level cause, clock scaling or core placement on a hybrid laptop CPU, is untested.
---
Compare and Contrast
| Serving path | Code the agent needs | Overhead from a Rust client | a2a send from a shell, N=1 |
| :--- | :--- | ---: | ---: |
| Official Rust SDK 🥇 | an AgentExecutor and three routers | 0.55 to 1.40 ms | 20.74 ms |
| a2a server --exec 🥈 | none: stdin in, stdout out | 3.33 to 5.05 ms | 20.81 ms |
| ADK coordinator 🥉 | an agent.py, an agent.json card | 1328 ms on 3.5-flash, 2283 ms on 3.8-flash | 2664.00 ms on 3.8-flash |
Rust-client ranges are the smallest and largest per-N medians; the ADK figures are the medians of its per-N medians. The exec figure includes starting a process for every message; how it splits between process start and the HTTP hop was not measured. At N=26 the work itself dominates every path: 2484 ms native and 2619 ms exec, direct.
---
So, Which One?
For an agent built to be one, use the SDK: about a millisecond of protocol cost, both transports, tasks the CLI can list and wait on.
For a program that already exists, a2a server --exec makes it an agent with no code change. A few milliseconds per call buys that, and ADK can call it once the two flags are set.
For routing by natural language, put ADK in front and keep the timing out of the model: a tool for anything computed, and a check that the model sent the arguments asked for.
For talking to any of them, the A2A CLI is the one client: card get, send, --stream, task get --wait, and -o json for scripts. Each call costs about 21 ms, so time agents from a long-lived client.
---
Summary
The goal of this article was to drive the A2A CLI against Rust agents on A2A v1.0, and to measure what each way of serving the same Rust code costs per call. The key to the solution was one benchmark client that reads timing from a structured report and checks that every reply answers the question asked. The results on one laptop were:
- 🟢 The A2A CLI drove all three agents with the same commands: card, send, stream, REST, data parts, JSON output, task list, --async and task get --wait
- 🟢 Every call through N=26, from the Rust client and from the CLI, reported found = min(N, 26)
- 🟢 The SDK agent added 0.55 to 1.40 ms per call from a Rust client
- 🟢 a2a server --exec served an A2A-unaware program to the CLI and to ADK
- 🟢 On gemini-3.8-flash, 10 of 10 benchmark replies quoted only tool values, and 10 of 10 unnamed requests went to the native agent
- ⚠️ One a2a send from a shell costs about 21 ms whatever the agent, 7.62 ms of it starting the CLI
- ⚠️ The wrapped program added 3.33 to 5.05 ms per call from a Rust client
- ⚠️ ADK added a median 1328 ms per call on gemini-3.5-flash and 2283 ms on gemini-3.8-flash, with one call at 29.0 s
- ❌ Compute times from calls spaced by idle gaps run slower and do not compare with back-to-back runs
Scope: one 13th Gen Intel Core i7-1360P laptop on Linux, release build, every agent and client on the same machine, 2026-11-03 and 2026-11-04. Rust-client series have 20 reps per N direct and 3 through ADK, with one run per model on Vertex AI's shared global endpoint, so the difference between the two Gemini models is a single sample. The CLI series has 5 reps per N direct and 3 through ADK, on gemini-3.8-flash only, and reliability was checked on that model only, ten requests of each kind. The gRPC transport, task cancel, task subscribe, push notifications and authentication were not exercised.
The strategy for serving and benchmarking Rust A2A agents with the A2A CLI was validated with an incremental step by step approach.
---
References
- The rig: github.com/xbill9/rust-dev/tree/main/a2a-benchmar…
- CLI tour transcripts: github.com/xbill9/rust-dev/blob/main/a2a-benchmar… and github.com/xbill9/rust-dev/blob/main/a2a-benchmar…
- CLI timing: github.com/xbill9/rust-dev/blob/main/a2a-benchmar…
- Introducing the A2A CLI: a2a-protocol.org
- A2A CLI: a2aproject/a2a-cli
- A2A Rust SDK: a2aproject/a2a-rs
- Agent Development Kit: google.github.io
- Why Did My Benchmark Stop at N=22?: dev.to
A2A (Agent2Agent Protocol): Building Interoperable Multi-Agent Systems
Understanding how AI agents discover, communicate, delegate tasks, and collaborate across different frameworks and platforms.
1. What Is A2A, and What Problem Does It Solve?
As AI applications become more capable, we are moving beyond simple interactions between a user and a single AI model.
Today, an AI agent can search documentation, query databases, interact with APIs, analyze code, and execute multi-step workflows. However, as these capabilities grow, a new architectural question emerges:
How can independently developed AI agents communicate and work together?
Consider an application that needs to analyze a GitLab merge request. The task may involve several different responsibilities:
- A security agent reviews potential vulnerabilities.
- A testing agent analyzes test coverage and identifies missing cases.
- An architecture agent evaluates design decisions and code structure.
- A review agent combines the findings and prepares a final report.
Each agent might use a different framework, model provider, toolset, or programming language.
Without a standardized communication mechanism, integrating these agents usually requires custom APIs, shared data contracts, and application-specific orchestration logic.
This is where Agent2Agent (A2A) Protocol becomes relevant.
A2A is an open protocol designed to enable communication and collaboration between independent AI agent systems. It provides a common interaction model regardless of the framework, programming language, or vendor used to build an agent.
The protocol was originally developed by Google and contributed to the Linux Foundation. It is designed around interoperability without requiring agents to expose their internal reasoning, memory, tools, or implementation details.
The important distinction is that A2A does not define how an agent thinks or performs its internal tasks. It defines how one agent can communicate with another.
In other words, an agent can be implemented using LangGraph, Google ADK, CrewAI, Semantic Kernel, or a completely custom solution and still communicate with another compatible agent through A2A.
Reference: A2A Protocol — Official Documentation
2. Why Do We Need A2A?
Most agent frameworks provide mechanisms for calling tools, delegating work, or coordinating multiple agents.
However, these mechanisms are generally designed around the framework's own execution model.
For example, an agent built with one framework may have its own task execution logic, state management, and internal tool definitions. Connecting it directly to an agent developed using another framework can require custom integration work.
A2A addresses the interoperability problem.
Instead of requiring agents to understand each other's internal implementations, they communicate through a standardized protocol.
This gives us several important capabilities:
- Discover agents and understand their capabilities.
- Delegate tasks to remote agents.
- Exchange messages and structured data.
- Track the lifecycle of long-running tasks.
- Receive progress updates through streaming.
- Exchange files and generated artifacts.
- Support authentication and authorization across agent boundaries.
A2A is particularly useful when agents belong to different teams, applications, organizations, or technology stacks.
The goal is not to turn every agent into the same implementation. The goal is to allow different implementations to collaborate.
3. A2A vs. MCP vs. ACP
These three protocols are often discussed together, but they solve different architectural problems.
| Technology | Main responsibility |
|---|---|
| MCP | Agent ↔ Tools and external context |
| ACP | Coding Agent ↔ Client Application |
| A2A | Agent ↔ Agent |
Let's consider a practical example.
Imagine a research agent that needs to investigate a topic, retrieve information from internal documentation, and delegate a part of its work to a specialized analysis agent.
In this scenario:
1. MCP provides access to documentation, APIs, databases, and other tools.
2. A2A allows the research agent to communicate with the analysis agent.
3. ACP could be used separately when integrating a coding agent with an editor or another client application.
A simplified architecture:
User
|
v
Main AI Application
|
v
Coordinator Agent
/ \
/ \
A2A A2A
/ \
v v
Research Agent Analysis Agent
| |
MCP MCP
| |
Internal APIs Database / APIs
| |
v v
Results Results
\ /
\ /
Coordinator
|
v
Final Response
Each agent can use its own tools and internal architecture. A2A provides the communication layer between them.
One important point: A2A is not a replacement for an agent framework or an internal sub-agent mechanism. It focuses on communication between independent agent applications. Internal orchestration can still be handled by the framework that runs the agent.
References: A2A and MCP · MCP Documentation · ACP Documentation
4. How Does A2A Work?
To understand A2A, we need to understand a few core concepts.
4.1. Agent Card: Discovering Other Agents
One of the most important concepts in A2A is the Agent Card.
An Agent Card is a JSON document that describes an agent and its capabilities.
It acts as a machine-readable description of what an agent can do and how another agent can communicate with it.
An Agent Card can contain:
- Agent name and description
- Service endpoint
- Supported protocol versions and transports
- Available capabilities
- Supported input and output formats
- Skills and example tasks
- Authentication requirements
By default, an A2A server exposes its Agent Card at:
https://agent.example.com/.well-known/agent-card.json
For example, a research agent might expose the following description:
{
"name": "Research Agent",
"description": "An agent specialized in research and source analysis.",
"version": "1.0.0",
"supportedInterfaces": [
{
"url": "https://research.example.com/a2a",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"capabilities": {
"streaming": true,
"pushNotifications": true,
"extensions": []
},
"defaultInputModes": [
"text/plain"
],
"defaultOutputModes": [
"text/plain",
"application/json"
],
"skills": [
{
"id": "market-research",
"name": "Market Research",
"description": "Researches topics and produces structured findings.",
"tags": [
"research",
"analysis"
],
"examples": [
"Research the latest developments in AI agents."
],
"inputModes": [
"text/plain"
],
"outputModes": [
"text/plain",
"application/json"
]
}
]
}
This is a simplified public Agent Card example. Authentication and additional security requirements can also be described through the protocol's security fields.
A client can retrieve this document and understand whether the remote agent is suitable for a particular task.
For example, a coordinator agent looking for an agent capable of analyzing financial documents can inspect registered Agent Cards and evaluate their declared skills.
Agent discovery can happen through:
- Well-known URLs
- Private or public agent registries
- Directly configured Agent Card URLs
An Agent Card describes an agent's advertised capabilities; it does not prove that the agent can successfully perform every advertised task. Capability selection should be combined with authorization, availability checks, and runtime evaluation.
Reference: A2A — Agent Discovery
4.2. Messages and Tasks
A2A distinguishes between a message and a task.
Message
A message represents communication between a client and an agent. It can contain instructions, clarification requests, or additional information.
Task
A task represents a stateful unit of work with its own lifecycle.
For example, when an agent receives a request to analyze a large document, it may need several minutes to complete the work. Instead of treating the interaction as a simple request-response operation, A2A allows the agent to create a task and report its progress.
A task has a lifecycle that can include:
| State | Meaning |
|---|---|
| Submitted | Task has been accepted |
| Working | Agent is processing the task |
| Input Required | Additional information is needed |
| Auth Required | Additional authentication is required |
| Completed | Task finished successfully |
| Failed | Task execution failed |
| Canceled | Task was canceled |
| Rejected | Agent declined the task |
Some states represent interruptions that can be resolved through further interaction, while others represent terminal states.
This is important for long-running workflows because the client does not need to keep a single request open until every operation is complete.
A2A also provides two useful identifiers:
- taskId: Identifies an individual task.
- contextId: Groups related messages and tasks within a broader interaction context.
The task identifier is generated by the agent server. The context identifier helps maintain continuity across multiple related interactions.
4.3. Artifacts: Sharing Task Results
A2A also introduces the concept of Artifacts.
An artifact represents the output produced by an agent while completing a task.
For example:
- A research agent can produce a research report.
- A coding agent can return a generated file.
- An analysis agent can return structured JSON.
- A document processing agent can return an extracted document.
An artifact can contain multiple parts, including text, binary content, URLs, or structured data.
This distinction is important:
Messages are primarily used for communication and interaction, while artifacts represent the actual outputs of a task.
It allows agents to exchange more than just text-based responses.
4.4. Streaming and Asynchronous Communication
Not every agent task finishes immediately.
A research process, document analysis, or code review can take a considerable amount of time.
A2A supports multiple ways of receiving task updates:
Streaming
The client can subscribe to task updates and receive events while the agent is working.
Depending on the protocol binding, streaming is supported through Server-Sent Events (SSE) or gRPC streaming.
Polling
The client can periodically request the current task state.
Push Notifications
For long-running tasks where maintaining a persistent connection is not practical, the agent can send updates to a client-provided webhook.
These mechanisms allow applications to support different execution patterns without inventing custom communication protocols for each agent.
A2A also defines operations for sending messages, retrieving tasks, canceling tasks, subscribing to updates, and managing push notification configurations.
References: A2A Specification · Task Lifecycle · Streaming and Asynchronous Operations
5. How to Integrate A2A?
A2A provides official SDKs for several programming languages, including:
- Python
- JavaScript / TypeScript
- Java
- Go
- C# / .NET
- Rust
For TypeScript applications, the official package is @a2a-js/sdk.
The current SDK implements the A2A v1.0 specification and supports client and server development.
Step 1: Install the package
pnpm add @a2a-js/sdk
The official JavaScript SDK requires Node.js 20 or later.
For this example, we will implement a client that communicates with an existing remote research agent.
Step 2: Connect to an A2A agent
The SDK provides ClientFactory to create a client.
import { ClientFactory } from '@a2a-js/sdk/client';
const factory = new ClientFactory();
const client = await factory.createFromUrl(
'https://research-agent.example.com'
);
The SDK retrieves the Agent Card and selects a compatible transport based on the agent's advertised interfaces and the client's supported transports.
This means we do not need to hardcode every protocol detail into our application.
Step 3: Send a message to the agent
Once the connection is established, we can send a task request.
import {
Role,
type Message,
type SendMessageRequest,
} from '@a2a-js/sdk';
const message: Message = {
messageId: crypto.randomUUID(),
role: Role.ROLE_USER,
parts: [
{
content: {
$case: 'text',
value: 'Research the latest developments in multi-agent systems.',
},
mediaType: 'text/plain',
filename: '',
metadata: undefined,
},
],
taskId: '',
contextId: '',
extensions: [],
metadata: {},
referenceTaskIds: [],
};
const request: SendMessageRequest = {
tenant: '',
message,
configuration: undefined,
metadata: {},
};
const result = await client.sendMessage(request);
console.log(result);
The example uses the SDK's generated TypeScript message types. Depending on the agent's behavior, the response can contain a message or a task.
For production integrations, authentication and request timeouts should be configured as well.
For example:
const result = await client.sendMessage(request, {
serviceParameters: {
Authorization: `Bearer ${process.env.A2A_TOKEN}`,
},
signal: AbortSignal.timeout(60_000),
});
The client can now communicate with a remote agent without needing to know its internal framework or model implementation.
Step 4: Stream task updates
For long-running tasks, we can use sendMessageStream.
const stream = client.sendMessageStream(request);
for await (const event of stream) {
// Handle task events:
// - task
// - statusUpdate
// - artifactUpdate
console.log(event);
}
The actual event union should be handled using the SDK's generated types.
This allows us to display task progress, process generated artifacts, and update the application as the remote agent works.
The SDK also supports task retrieval, cancellation, and push notification configuration.
Step 5: Build your own A2A agent
A2A is not limited to consuming existing agents. We can also expose our own agent through the protocol.
The official JavaScript SDK separates server-side responsibilities into three main components:
AgentExecutor
Contains the agent's business logic. It receives the request context, executes the task, and publishes messages, status updates, and artifacts.
DefaultRequestHandler
Handles protocol-level responsibilities such as message routing, task storage, cancellation, and push notifications.
Transport Adapters
Expose the agent through supported communication protocols.
The SDK provides adapters for JSON-RPC and HTTP+JSON/REST through Express, as well as gRPC for Node.js.
The architecture looks like this:
Incoming A2A Request
|
v
Transport Adapter
|
v
DefaultRequestHandler
|
+----> Task Store
|
+----> Execution Event Bus
|
v
AgentExecutor
|
v
Agent Logic
|
+----> LLM
+----> MCP Tools
+----> Internal APIs
+----> Business Logic
|
v
Messages / Status / Artifacts
|
v
A2A Response
The SDK also supports custom task stores and event buses, which allows us to integrate persistent databases, caches, or message brokers instead of relying exclusively on in-memory implementations.
For production, this separation is particularly useful because protocol handling, task execution, and persistence can evolve independently.
Reference: Official A2A JavaScript SDK
6. Integrating A2A with the Vercel AI SDK
There is another interesting integration worth looking at.
The Vercel AI SDK lists an A2A community provider called a2a-ai-provider.
This package allows an A2A-compatible agent to be consumed through familiar AI SDK functions such as generateText and streamText.
This is conceptually similar to the ACP provider we discussed previously.
Instead of implementing every A2A interaction manually, we can use the community provider as an adapter between the A2A protocol and the AI SDK.
Installation
pnpm add ai a2a-ai-provider
Provider configuration
import { a2a } from 'a2a-ai-provider';
import { generateText } from 'ai';
const result = await generateText({
model: a2a(
'https://research-agent.example.com/.well-known/agent-card.json'
),
prompt: 'Research the latest developments in agentic AI.',
});
console.log(result.text);
Streaming responses
We can also use streamText:
import { a2a } from 'a2a-ai-provider';
import { streamText } from 'ai';
const result = streamText({
model: a2a(
'https://research-agent.example.com/.well-known/agent-card.json'
),
prompt: 'Analyze the current multi-agent ecosystem.',
providerOptions: {
a2a: {
contextId: crypto.randomUUID(),
},
},
});
await result.consumeStream();
console.log(await result.content);
Here, contextId is used to associate related interactions with the same context.
This integration makes it easier to use A2A agents in existing AI SDK applications.
However, there is an important distinction:
The official A2A SDK is the protocol implementation, whereas a2a-ai-provider is a community-maintained adapter.
The community provider's documentation describes it as under active development, and its GitHub repository currently labels it alpha and advises against production workloads.
For that reason, I would use it for experimentation and evaluate the official SDK directly for production-critical integrations until the provider's maturity and compatibility are established.
References: AI SDK — A2A Community Provider · a2a-ai-provider GitHub
7. Supported Communication Protocols
A2A separates its common data model and operations from the transport used to communicate.
The current specification defines three standard protocol bindings:
| Protocol | Description |
|---|---|
| JSON-RPC | JSON-based remote procedure calls |
| HTTP+JSON / REST | Resource-oriented HTTP communication |
| gRPC | RPC communication using Protocol Buffers and gRPC |
An agent can support more than one binding.
For example, a service could expose JSON-RPC and REST interfaces using the same underlying task execution logic.
This separation is valuable because clients and servers can select a compatible transport without changing the fundamental meaning of tasks, messages, and artifacts.
The official JavaScript SDK supports all three transports, with its gRPC integration requiring a Node.js environment and additional dependencies.
A2A also defines protocol versioning. Clients and agents use the A2A-Version header to identify the version they are communicating with.
The current v1.0 specification includes changes from the earlier v0.3 interface, so compatibility and migration should be considered when connecting existing implementations.
Reference: A2A Protocol Specification
8. Advantages of A2A
Interoperability across frameworks
One of A2A's biggest advantages is the ability to connect agents built using different frameworks and programming languages.
We do not have to standardize every team's internal agent implementation just to enable collaboration.
A Python-based research agent can communicate with a TypeScript-based coordinator, while another agent may be implemented using Go or .NET.
This reduces framework-level coupling.
Independent agent implementations
Agents do not need to expose their internal prompts, model configurations, memory, or tools.
They only need to expose their supported capabilities and implement the agreed communication contract.
This allows teams to evolve their internal implementations without forcing all consumers to change their integration code.
Better support for long-running workflows
A2A is designed around stateful tasks.
Instead of implementing custom job tracking, status polling, event streaming, and result retrieval for every remote agent, we can use standardized operations.
This is particularly useful for research, document processing, code analysis, and other workflows that may take several minutes or longer.
More flexible output handling
Agents can exchange text, files, structured data, and other supported content types through message parts and artifacts.
This makes the protocol useful beyond conventional text-based chat applications.
Centralized agent discovery and integration
Agent Cards allow applications to discover what an agent offers and how to communicate with it.
Combined with a curated internal agent registry, this can provide a common discovery layer for enterprise AI platforms.
9. Limitations and Trade-offs
A2A solves a real interoperability problem, but it also introduces architectural complexity.
It does not provide orchestration by itself
A2A defines how agents communicate, not how an entire multi-agent system should be planned and executed.
It does not decide which agent should receive a task, how many agents should participate, or how conflicting results should be resolved.
Those responsibilities remain with an orchestrator or an agent framework.
For example, LangGraph, Google ADK, or a custom coordinator can decide when to delegate a task through A2A.
Distributed execution introduces more failure scenarios
When agents communicate across network boundaries, new failure scenarios appear:
- A remote agent may become unavailable.
- A request may time out while the task continues running.
- A task may fail after partial execution.
- A client may disconnect before receiving the final artifact.
- A downstream agent may return an incomplete or unexpected result.
Therefore, retries, cancellation, idempotency, timeout policies, and persistent task storage need to be considered carefully.
More communication can increase latency and cost
Every agent handoff introduces additional work.
A complex multi-agent workflow may involve several model calls, network requests, and tool executions.
Using more agents does not automatically improve the result.
We should introduce agent delegation only when the additional specialization or parallelism justifies the extra latency, cost, and operational complexity.
Agent compatibility does not guarantee equivalent capabilities
Two agents may both implement A2A but provide very different skills and execution behavior.
A2A standardizes communication, not the quality or accuracy of an agent.
Clients still need to evaluate supported capabilities, input/output formats, and the reliability of the remote service.
Version compatibility requires attention
The transition from earlier protocol versions to v1.0 introduced changes to operation names, Agent Cards, and other protocol details.
The official TypeScript SDK provides an opt-in v0.3 compatibility layer, which can help during staged migration.
However, version compatibility should be tested explicitly rather than assumed.
Security does not come automatically
A2A supports authentication and authorization mechanisms, but the protocol does not automatically make remote agents trustworthy.
The application must decide which agents can communicate, what data can be shared, and which actions are permitted.
Reference: A2A v1.0 Specification · A2A Security Considerations
10. Security and Production Considerations
This is one of the most important parts of adopting A2A in an enterprise environment.
An agent may receive data from another agent, process documents, invoke tools, and return results that influence subsequent decisions.
We should not treat every remote agent as trusted simply because it implements the same protocol.
Some important considerations:
Authentication
A2A relies on established authentication mechanisms such as HTTP authentication, OAuth 2.0, OpenID Connect, API keys, and mutual TLS where appropriate.
The agent can advertise supported authentication schemes through its Agent Card.
Credentials are handled at the transport layer, not embedded directly into protocol messages.
Authorization
Authentication and authorization are different responsibilities.
Knowing who is calling an agent does not automatically mean that the caller should be allowed to invoke every skill.
Authorization should consider:
- Which application or user initiated the task?
- Which agent is allowed to communicate with the target agent?
- Which skills can be invoked?
- Which business operations are permitted?
- Which data can be accessed or shared?
For financial or other sensitive enterprise environments, authorization should be enforced at the skill and business-action levels.
Data isolation
Agents should receive only the information required to complete their tasks.
A research agent should not automatically receive the entire conversation history, database access, or credentials of another agent.
Task and context storage should also respect user and tenant boundaries.
Agent Card validation
Agent Cards describe service identity and capabilities.
For sensitive environments, clients should verify the origin of Agent Cards and validate their signatures when available.
A discovery result should not automatically be treated as a trusted execution target.
Observability and auditing
A multi-agent system needs visibility beyond individual model calls.
We should record:
- Caller and target agent identifiers
- Task and context identifiers
- Correlation and trace identifiers
- Task state transitions
- Request duration and failure reasons
- Artifact metadata and access
- Sensitive action approvals
OpenTelemetry and distributed tracing can help follow a request across multiple agents.
Push notification security
Push notification webhooks need their own security controls.
For example, the receiver should authenticate the notification source, validate the task identifier, and process duplicate notifications safely.
Webhook endpoints should also be protected against SSRF and unauthorized access.
These controls are application and infrastructure responsibilities that must be designed alongside the protocol integration.
Reference: A2A — Enterprise Implementation
11. What Can an AI Chapter Lead Contribute?
I think this is where A2A becomes especially interesting from an engineering leadership perspective.
The value is not simply adding another protocol to the technology stack. It is establishing a reusable architecture for teams that want to build specialized agents independently and allow them to collaborate.
There are several areas where we can contribute.
1. Establish a common agent communication standard
Different teams may build agents using different languages and frameworks.
Instead of forcing everyone to adopt the same framework, we can define a common communication contract based on A2A.
This allows teams to own their agent implementations while maintaining a consistent integration approach.
2. Build an internal agent registry
We can create a centralized catalog of available agents and their capabilities.
For example:
| Agent | Responsibility | Available skills |
|---|---|---|
| Research Agent | Information gathering | Research, source analysis |
| Database Agent | Data analysis | SQL analysis, reporting |
| Code Review Agent | Code quality | Review, recommendations |
| Security Agent | Security analysis | Vulnerability assessment |
| Documentation Agent | Documentation | Search, summarization |
Each agent can publish an Agent Card, while the internal registry handles discovery, ownership, versioning, authorization, and availability.
This would help teams reuse existing capabilities instead of implementing the same functionality repeatedly.
3. Define a reusable orchestration architecture
A2A should be combined with a clear orchestration strategy.
We can establish a reference architecture that separates:
- Agent discovery
- Task delegation
- Agent communication
- Execution state management
- Tool and data access
- Result aggregation
- Human approval
The coordinator should remain responsible for task planning and result aggregation. Specialized agents should focus on their assigned responsibilities.
4. Establish security and governance standards
A2A introduces communication between independently managed systems.
We should define clear standards for agent authentication, authorization, data sharing, tenant isolation, and audit logging.
This is especially important when agents interact with internal systems or execute business operations.
5. Run a practical multi-agent pilot
Rather than introducing A2A across every team immediately, we can start with a well-defined workflow.
A good example would be an automated merge request review process.
Example workflow:
1. A developer submits a merge request.
2. The Coordinator Agent receives the review request.
3. The Security Agent analyzes potential vulnerabilities.
4. The Testing Agent evaluates test coverage and identifies missing cases.
5. The Architecture Agent reviews design and implementation decisions.
6. Each specialist returns findings through A2A.
7. The Coordinator combines the results into a structured review report.
8. A developer reviews the recommendations before any changes are merged.
Each agent can use its own MCP servers to access GitLab, documentation, test tools, or other required services.
This allows us to evaluate the actual value of agent specialization and interoperability.
6. Measure the results
A successful pilot should be evaluated using real measurements.
Some useful metrics:
- Task completion rate
- Agent-to-agent communication latency
- Overall workflow duration
- Failed or timed-out tasks
- Duplicate execution rate
- Token and infrastructure costs
- Human intervention rate
- Quality of the final aggregated result
The objective is to determine whether distributing work across specialized agents produces better results than a single-agent implementation.
7. Create reusable learning resources
A practical reference implementation, architecture documentation, and internal workshops can help developers understand when A2A is useful.
Teams should learn not only how to implement the protocol, but also when not to use it.
Not every internal function needs to become a remote agent. For simple operations within the same runtime, a regular function or framework-native tool call may be more appropriate.
12. When Should We Use A2A?
A2A is most useful when communication between independent agents is an actual architectural requirement.
| Scenario | Recommendation |
|---|---|
| Agents built with different frameworks need to collaborate | A2A |
| Different teams expose independently maintained agents | A2A |
| Remote, long-running agent tasks need tracking | A2A |
| Agents need to exchange structured outputs and files | A2A |
| An agent needs access to APIs and databases | MCP |
| A single agent needs internal tool execution | Native tools or MCP |
| Multiple agents operate within one tightly coupled workflow | Framework-native orchestration may be sufficient |
The important thing is to avoid introducing network boundaries and protocol complexity when a simpler implementation would work.
A2A should solve an interoperability requirement, not become an additional abstraction by default.
Conclusion
A2A addresses an important challenge in the evolution of agentic AI systems: enabling independently developed agents to communicate and collaborate.
As AI applications become more specialized, a single agent will not necessarily be responsible for every task. Different agents may own different capabilities, use different frameworks, and operate in different environments.
A2A provides a standardized way for these systems to discover each other, delegate tasks, exchange information, and track execution.
Combined with MCP, it creates a useful architectural separation: MCP connects agents to tools and external context, while A2A connects agents to other agents.
For an AI Chapter Lead, the opportunity is to establish common integration standards, build an internal agent ecosystem, define security boundaries, and help teams develop reusable capabilities.
My recommendation: Start with one practical multi-agent workflow, implement it using the official A2A SDK, and evaluate the operational benefits against a conventional single-agent or framework-native orchestration approach.
The real question is not how many agents we can connect. It is whether connecting specialized agents makes the system more reliable, maintainable, and effective.
---
References and Further Reading
1. A2A Protocol — Official Website — Overview, architecture, ecosystem, and documentation.
2. A2A Protocol — Specification v1.0 — Protocol operations, data models, task lifecycle, transports, and security.
3. A2A — Agent Discovery — Agent Cards, discovery mechanisms, and capability descriptions.
4. A2A — Life of a Task — Task lifecycle and interaction patterns.
5. A2A — Streaming and Asynchronous Operations — Streaming, task updates, and push notifications.
6. A2A — Enterprise Implementation — Authentication, authorization, observability, and enterprise security.
7. A2A — Official JavaScript SDK — TypeScript client/server implementation, transport adapters, and runnable examples.
8. A2A — Official GitHub Organization — SDKs, samples, and project development.
9. AI SDK — A2A Community Provider — Integrating A2A agents with Vercel AI SDK.
10. A2A AI Provider — Community provider implementation and current maturity status.
11. Model Context Protocol — Complementary agent-to-tool integration protocol.
1 reaction · 1 comment
One extra field turned an A2A error into a TypeError. Fixed in a2a-sdk 1.3.0
---
title: One extra field turned an A2A error into a TypeError. Fixed in a2a-sdk 1.3.0
published: true
description: I fed the A2A Python client seven bad agent replies. A valid error code came back as a TypeError. The fix shipped in a2a-sdk 1.3.0.
tags: python, ai, agents, opensource
cover_image: raw.githubusercontent.com
---
<!-- Images are pushed to Sonica-B/Sonica-B under blog/, so the URLs below resolve. The interactive version of figure 1 is visibility/anatomy/a2a-sorter/a2a-sorter.html. -->
On October 27, 2026, SAP announced general availability of inbound Agent2Agent (A2A) interoperability, so third-party agents can now invoke SAP Agents through its Agent Gateway. Every launch like this means more A2A clients calling agents their authors do not control. Two days before SAP's announcement, I had opened a pull request for that exact situation in the A2A Python SDK. An agent could send a valid A2A error, and the calling code would receive a TypeError. My fix shipped the same day as SAP's news, in a2a-sdk 1.3.0.
A2A is an open protocol that lets AI agents discover each other and exchange messages and tasks over JSON-RPC, REST or gRPC. It joined the Agentic AI Foundation in August 2026, and a2a-sdk is its official Python SDK.
The short version
When an agent sent back an error, the SDK handed the reply to a third-party library before looking at it itself. That library is strict about which fields an error may carry, so one extra field, the kind the A2A spec says to ignore, made it throw a raw TypeError. The SDK's own translation of error codes into named exceptions never got a turn. Version 1.3.0 reads the reply itself first, at one place, and the library only builds requests now.
The concept, animated
!Animated figure. The reply's path through JsonRpcTransport as labelled blocks: agent reply, the json-rpc package's JSONRPC20Response(**body), the SDK's _create_jsonrpc_error(), your code with its except TaskNotFoundError branch, and three bins for TaskNotFoundError, A2AClientError and result. A token labelled {"code": -32001, "reason": "x"} drops into a raw TypeError tray at the json-rpc block. In 1.3.0 that block lifts out, _check_jsonrpc_response() takes its place, and the token reaches the TaskNotFoundError bin; a body that is a list reaches A2AClientError.
Figure 1. The blocks are the real call chain in client/transports/jsonrpc.py, in order. The token is the reply body. The three states: an error object with one extra field before 1.3.0, the same reply in 1.3.0, and a body that is not an object in 1.3.0.
A client talks to agents it does not control, so odd replies are normal. The SDK's promise about them is clear: a failure reaches your code either as A2AClientError or as a typed A2A error such as TaskNotFoundError. The A2A spec (section 5.7) adds that implementations should ignore fields they do not recognise.
Now follow the token in the first state. It carries the error code -32001, which means TaskNotFoundError, plus one extra field, reason, that the spec says to ignore. The first block it reaches is JSONRPC20Response(**body) from the json-rpc package. It validates the envelope before the SDK sees anything, and it builds the error with a constructor that accepts only code, message and data. Any other member is, to that library, a programming mistake. So the token drops into the tray labelled raw TypeError: JSONRPCError.__init__() got an unexpected keyword argument 'reason'. The SDK's _create_jsonrpc_error() block never receives it, and your except TaskNotFoundError: branch never runs.
The library was correct by its own rules. It simply ran first, so its rules won and the SDK's contract lost.
How I found seven of these
I was exploring the client with one question: what reaches my code when an agent answers wrong? No issue existed, so I played the misbehaving agent myself, wrote seven bad response bodies by hand, and fed them to the client through a mock transport. All seven came back as raw Python exceptions.
| What the agent sent | Before | After |
|---|---|---|
| a list, or a string (say, from a proxy) | TypeError | A2AClientError |
| neither result nor error | ValueError | A2AClientError |
| "error": "boom" | TypeError | A2AClientError |
| error object with an extra member | TypeError | TaskNotFoundError |
| error object without message | ValueError | TaskNotFoundError |
| SSE data: that is not JSON | JSONDecodeError | A2AClientError |
| event: error with an extra member | TypeError | TaskNotFoundError |
Following one reply through the code explained the whole table. Nine request methods handed the body straight to the json-rpc validator, and streaming used the same validator on each event. The SDK's own mapping from error codes to exceptions even had a fallback for a missing message, but it could never execute, because the library failed first. Streaming had a second twist: the JSON parse for each event ran in a place where its exception escaped the handler that was supposed to catch it, so a broken data: line leaked a JSONDecodeError.
The fix, in one idea
!Still from the third state: the json-rpc block is lifted out and faded, the SDK's _check_jsonrpc_response() block stands in its place, and a token labelled "a list, not an object" sits in the A2AClientError bin.
Figure 2. In 1.3.0 the json-rpc validator is out of the chain. The SDK reads the envelope itself at one choke point, so an error object becomes the typed A2A error whatever extra fields it carries, and a body that is not even an object becomes A2AClientError.
Check the wire envelope yourself, once, and hand libraries only what they can handle. Every reply already passes through one sending function or the streaming loop, so I put one check there. It asks three plain questions in order: is the body an object; if it has an error, is that an object, and if so, map it to the A2A exception by its code; otherwise, is there a result. Anything that fails those questions becomes A2AClientError with a message that says what was wrong. The streaming loop turns a JSONDecodeError into A2AClientError the same way. A falsy error next to a result still counts as success, as before. The client no longer builds JSONRPC20Response objects at all; requests still use JSONRPC20Request.
Wrapping each of the nine calls in its own try/except would have turned TaskNotFoundError into a generic error and copied the same code nine times. One choke point keeps the typed exceptions.
The tests live in a new class, TestMalformedJsonRpcResponses, with 11 cases. Ten fail on main with the exceptions in the table and pass with the change. The eleventh guards that delete_task_push_notification_config still raises on an error response. The existing tests pass unchanged. I ran everything on Windows 11 with Python 3.10.19: lint.sh passed, the transport tests gave 51 passed, and the wider run gave 2213 passed at 93% total coverage. Three failures and four setup errors fail the same way on main, and two integration suites do not run on Windows, so I left those to Linux CI. All 17 checks passed there, and the coverage bot showed client/transports/jsonrpc.py going from 92.66% to 95.51%.
What changed for people using a2a-sdk
If your client calls agents you do not control, version 1.3.0 behaves differently in three ways:
- An agent that adds its own field to an error object, or leaves out message, now raises the mapped A2A error, so your except TaskNotFoundError: branch runs.
- A malformed body, such as a list, a string, a missing result or a non-object error, raises A2AClientError, so one except clause covers all of them.
- A stream with a broken data: line raises A2AClientError where it used to leak a JSONDecodeError.
One behaviour change is worth knowing. A body with "error": null and no result used to parse as an empty object and now raises A2AClientError. That body breaks JSON-RPC 2.0 anyway. Upgrading is pip install -U a2a-sdk.
The v0.3 compatibility transport still uses the same validator-first pattern on main. I kept this PR to the 1.0 client and offered a fix(compat) follow-up in the PR body.
A thank-you to Iwaniukooo11 and the A2A maintainers
I opened PR #1375 on October 25 with no linked issue, just the table and the tests. On October 26, Mateusz Iwaniuk (@Iwaniukooo11) brought the branch up to date with main, approved it and merged it. Thank you. Getting a first PR to a repo reviewed and merged the day after it opens is a generous welcome.
Thanks also to the A2A maintainers for keeping the door open to outside contributors. Of the 40 merges before mine, 29 came from non-members, with a median of 3.4 days from open to merge. Release 1.3.0 carries fixes from at least three outside contributors, mine among them.
What I took away
Where a third-party validator sits in your call chain decides which exceptions your callers see. The stack trace ended inside json-rpc, but the decision that mattered was where the SDK called it.
When you own an error contract, check the wire envelope yourself at one choke point and hand libraries only what they can handle. As more vendors open their agents to outside callers, the exception a client can catch by name is part of the interface, and that boundary is the kind of problem I like to work on.
I'm Shreya Boyane, an ML engineer who builds agent systems, and I'm open to ML and AI engineering roles. You can find my work on GitHub, more about me at shreyaboyane.dev, and I'm happy to connect on LinkedIn.
<!-- optional disclosure she may keep: An AI coding agent helped me probe the package and draft this fix; I reviewed and verified the change before submitting. -->
@AgenticAIFdn If you want to try it, start with the first lesson in the cookbook, examples/01-exec-demo: a2aproject/a2a-cli
77 views
MCP vs A2A vs AG-UI: The Protocol Stack, Fact-Checked
> Originally published on DevToolHub.
MCP vs A2A vs AG-UI is the comparison chart making the rounds right now, and the framing is right — these are three different, complementary layers, not competing standards. But a couple of its headline numbers are either stale or don't hold up against the protocols' own sources, and it skips the actual news: MCP's July 2026 spec update is a real breaking-change event, not a one-word "stateless" bullet point. Here's what each protocol actually does, fact-checked against their own docs and GitHub repos, plus what the chart got right and what's worth correcting. For the agent concepts underneath all three, see our AI agents explained guide.
The Three Protocols, What Each One Actually Connects
The "vs" in every comparison title is misleading — you don't pick one. Each protocol solves a different connection problem, and a real agentic system typically uses all three at once:
- MCP (Model Context Protocol) — built by Anthropic — connects an agent to tools and data. Anthropic's own docs describe it as a standardized way to connect AI applications to external systems, "like a USB-C port for AI applications." If your agent needs to read a file, query a database, or call an API, this is the layer — see our MCP registry breakdown for how big that ecosystem already is.
- A2A (Agent2Agent Protocol) — built by Google, now governed by the Linux Foundation — connects one agent to another. It's how two independent agents, possibly built by different teams on different frameworks, discover each other and hand off work.
- AG-UI (Agent-User Interaction Protocol) — built by CopilotKit and its open-source community, not Google (a detail the viral chart gets right, worth double-checking since AG-UI's name invites confusion with Google's A2A) — connects an agent to the human watching it work. It's an event-based protocol for streaming an agent's state and output into a real UI, not a chat log.
None of these three compete for the same job. A production agent that reads your CRM (MCP), delegates a sub-task to a specialist agent (A2A), and shows a human the live progress bar while it works (AG-UI) is using all three simultaneously.
MCP's July 2026 Spec Update Was Bigger Than "Stateless"
The chart's MCP row says "Stateless spec, July 2026" and leaves it there. That's the right headline, but it's doing a lot of work in one line — per MCP's own 2026-07-28 changelog, this is the largest rewrite since launch: protocol-level sessions and the Mcp-Session-Id header are gone, the initialize handshake is gone, and Roots, Sampling, and Logging are now formally deprecated with a twelve-month removal window. We already covered the full breaking-change list and a real server-migration checklist in MCP Server Authentication: What the 2026-07-28 Spec Broke — if you're migrating an MCP server, that's the piece to work from.
What's actually new to check, and what the chart doesn't carry at all: per Anthropic's own announcement, MCP is at 400M monthly SDK downloads as of that July 28 update — a real, sourced number that's meaningfully below the chart's "close to 500M" claim, and the real reason for the stateless move is architectural: "MCP moves from a bidirectional stateful protocol to a request/response model. Servers can now deploy on serverless and edge infrastructure" — something the old session-pinned design couldn't do cleanly.
A2A's Real Status — v1.0, Signed Agent Cards, and a Number Worth Correcting
The chart says A2A hit "v1.0 with signed Agent Cards, March 2026." The version and date both check out — A2A's GitHub releases show v1.0.0 published March 12, 2026, and the live spec's section 8.4 ("Agent Card Signing") does define canonicalization, signature format, and signature verification requirements so agents can cryptographically sign their Agent Cards for authenticity. Worth knowing if you're building against it today: v1.0 isn't the current patch anymore — v1.0.1 shipped May 28, 2026 with HTTP binding and TaskStatus fixes.
Governance checks out too: Google originally built A2A and donated it to the Linux Foundation, and as of August 20, 2026 it formally joined the Agentic AI Foundation (AAIF) — the same body that already hosts MCP — putting both protocols under one neutral governance structure instead of two separate vendor-led ones. A2A's Technical Steering Committee includes representatives from AWS, Cisco, Google, IBM Research, Microsoft, Salesforce, SAP, and ServiceNow.
The one number worth double-checking: the chart lists A2A at "150+ organizations behind it." I couldn't find a primary source attributing that specific figure to A2A alone. What I could verify, directly from the Linux Foundation's own press release, is AAIF's total membership — the body that now governs both A2A and MCP — which states it "grows to 247 member organizations with the addition of 57 new members over the past three months." That's a materially different, larger number than "150+," just measuring the whole foundation rather than one protocol's backers specifically. If the chart's figure is from an earlier AAIF member count, it's stale now.
AG-UI's Real Adoption — 16.7k Stars and a Fourth Integration the Chart Skips
AG-UI's chart row says "15K+ GitHub stars." Checking the ag-ui-protocol/ag-ui repo directly, it's actually at 16.7k stars — the chart's number is accurate but already a bit behind. More interesting: the chart lists three 1st-party framework integrations (Google ADK, Microsoft Agent Framework, AWS AgentCore). The repo documents a fourth — AWS Strands Agents is also officially supported, alongside AWS Bedrock AgentCore, not instead of it. If you're picking an agent framework specifically because it has AG-UI support baked in, Strands is a real option the chart doesn't mention.
Where ACP Went
The chart's footer note — "Where's ACP? IBM merged it into A2A in August 2025" — checks out exactly. IBM Research's Agent Communication Protocol, built to power its BeeAI platform, overlapped heavily with A2A (both covered agent-to-agent discovery and messaging). The original ACP repository itself confirms it: archived August 27, 2025, with a banner reading "ACP is now part of A2A under the Linux Foundation!" and a link to the migration guide. If you see ACP referenced in older material, it's a dead end now — the real destination is A2A.
Do You Need All Three? A Practical Decision Guide
- Your agent needs to read or write something external — a CRM, your docs, a GitHub repo, a database: that's MCP. One server, any MCP-compatible client can use it — our best MCP servers by category guide is a starting point.
- Your agent needs to hand off a task to a different, independently-built agent — possibly on a different framework, possibly a different vendor: that's A2A. The Agent Card is how the other side learns what your agent can do before the handoff.
- A person needs to watch your agent work and approve or steer its steps in real time, not just read a final answer: that's AG-UI. It's the layer between the agent's internal state and whatever UI framework renders it.
- You're building a single-tool integration with no multi-agent handoff and no live UI — you may genuinely only need MCP. Don't add A2A or AG-UI to a system that doesn't have another agent or a human-in-the-loop UI to connect to.
Frequently Asked Questions
Q: Are MCP, A2A, and AG-UI competing standards?
A: No. They connect different things — MCP connects an agent to tools and data, A2A connects one agent to another, AG-UI connects an agent to the person watching it. A single system can use all three at once.
Q: What actually changed when MCP went stateless in July 2026?
A: Sessions, the initialize handshake, and three features (Roots, Sampling, Logging) all changed at once — see our full MCP server authentication breaking-change checklist for the complete list. The number worth knowing that isn't in that piece: Anthropic's own blog puts MCP at 400M monthly SDK downloads as of this update.
Q: Who governs MCP and A2A now?
A: Both are hosted projects under the Agentic AI Foundation (AAIF), part of the Linux Foundation. A2A joined AAIF on August 20, 2026, after Google donated it; MCP was already a founding AAIF project. AAIF's own press release puts total membership at 247 organizations.
Q: Does AG-UI only work with Google, Microsoft, and AWS AgentCore?
A: Those three are 1st-party supported, but so is AWS Strands Agents — a fourth framework with official AG-UI support that's easy to miss if you're only going by popular comparison charts.
Q: What happened to ACP (IBM's Agent Communication Protocol)?
A: It merged into A2A. The original repository was archived August 27, 2025, with a notice directing developers to A2A's migration guide.
Quick Summary:
- MCP, A2A, and AG-UI are complementary layers (tools, agent-to-agent, agent-to-human), not competing alternatives
- MCP's July 28, 2026 spec update (sessions, the initialize handshake, Roots/Sampling/Logging all changed) is covered in full in our MCP server authentication piece
- Per Anthropic's own blog, MCP is at 400M monthly SDK downloads as of that update — not "close to 500M"
- A2A hit v1.0.0 in March 2026 (now on patch v1.0.1) with real signed Agent Cards; AAIF's own total membership is 247 organizations, a different number than "150+"
- AG-UI is at 16.7k GitHub stars with four 1st-party framework integrations (Google ADK, Microsoft Agent Framework, AWS Bedrock AgentCore, and AWS Strands Agents)
- ACP (IBM) merged into A2A in August 2025 and is a dead end if you see it referenced
The real answer to MCP vs A2A vs AG-UI is: pick protocols by the connection you actually need, not by which chart you saw first — and if you're touching an MCP integration built before July 2026, check it against the real breaking-change list above before assuming "stateless" just means a minor version bump.
@stevekrouse haven't used it but this is a thing a2aproject/a2a
29 views
Controlling Access to A2A Agents with agentgateway
At Qodo, we've publicly described how PR-Agent became an A2A agent for MOSAICO, a research project coordinating software-engineering agents. Other agents can delegate code reviews and receive results. When you expose an agent this way, how do you control who can call it and verify that rejected requests never reach it?
This walkthrough puts a sample agent behind agentgateway, checks caller credentials, and records which requests reach the backend!
What does A2A make possible for a code-review agent?
Agent2Agent, or A2A, defines how independently deployed agents exchange messages, tasks, and results. An agent card advertises an agent's capabilities and connection details. A client can discover a reviewer, send work, and receive its response without sharing the reviewer's implementation.
Qodo's public PR-Agent integration illustrates that boundary. MOSAICO can delegate a review to PR-Agent and receive review output. That establishes a useful interaction, but operating an endpoint also requires decisions about callers, credentials, and the resources each request may access.
For the access tests, a deterministic Hello World agent is enough. It returns a greeting instead of reviewing code. That keeps model behavior out of the experiment and makes backend execution easy to observe.
Where does agentgateway sit in the request path?
Agentgateway's A2A example places a proxy between the client and the agent. The tested setup uses agentgateway 1.6.0, Python 3.13.4, A2A Python SDK 1.1.0, and the A2A 1.0 JSON-RPC binding. Its executor comes from this pinned upstream sample.
Requests go to the gateway on port 18760; the sample backend listens on 127.0.0.1:18761. Request logging and an executor-entry record let the test compare what the gateway returned with what the agent actually received and ran.
!Client requests pass through JWT validation and a reviewer-role check in agentgateway. Invalid credentials return 401; rejected roles return 403; accepted requests reach the sample agent.
The tested request path. The backend remains reachable directly from the local machine; the diagram doesn't imply network isolation.
Fetching the agent card through the gateway returned a supportedInterfaces URL pointing back to port 18760. That helps clients continue through the proxy. It doesn't prevent someone who knows a reachable backend address from calling it directly.
How do you define what can reach the agent?
The route combines JWT authentication with an authorization rule. A JSON Web Token carries signed claims about its issuer, intended audience, and caller. The gateway checks the signature against a JSON Web Key Set, or JWKS, containing the issuer's public key.
Here's the route portion of the tested configuration:
binds:
- port: 18760
listeners:
- routes:
- policies:
a2a: {}
jwtAuth:
mode: strict
issuer: https://issuer.example.test
audiences: [a2a-demo]
jwks:
file: ./jwks.json
authorization:
rules:
- require: 'jwt.role == "reviewer"'
backends:
- host: 127.0.0.1:18761
The issuer here is a synthetic test identity, not a service to sign into. The runner creates an RSA key and short-lived tokens locally. In this release, the JWKS file path resolves from the process's working directory.
strict requires a valid token. The issuer and audience constrain which tokens qualify; a separate rule requires the signed role claim to equal reviewer. That role is this demo's convention, not an A2A-defined permission.
Keep those checks explicit. Agentgateway's default optional authentication mode permits requests without a token. A configuration that validates credentials when present can behave differently from one that requires credentials on every request.
How do you prove a policy rejects the right requests?
Each test sends a real SendMessage request with a unique message ID. The same identifier connects the request to the backend's records. Accepted cases must return the greeting without a JSON-RPC error; an HTTP 200 alone doesn't establish successful A2A execution.
These were the observed outcomes with the configuration above:
| Caller credentials | HTTP response | Backend requests / executor calls |
|---|---:|---:|
| Valid token, reviewer role | 200 | 1 / 1 |
| No token | 401 | 0 / 0 |
| Wrong audience | 401 | 0 / 0 |
| Wrong role | 403 | 0 / 0 |
| Missing role claim | 403 | 0 / 0 |
| Invalid signature or expired token, tested separately | 401 each | 0 / 0 each |
A valid request after the rejection cases also completed. Removing only the authorization rule let a valid token with the wrong role reach the executor. That control checks whether the role rule actually explains the rejection.
One configuration change exposed a useful failure case. Replacing the requirement with this rule changed the result for a valid token missing role:
authorization:
rules:
- deny: 'jwt.role != "reviewer"'
That request returned 200 and executed once. With require, it returned 403 and never reached the backend.
The documented evaluation rules explain why: accessing a missing claim makes the expression fail. A failed requirement rejects the request. A failed deny expression doesn't match; with this single deny rule and no allow rules, the request proceeds.
JWT authentication stayed strict in both configurations. A missing token still returned 401. The finding concerns a missing application claim in an otherwise valid token, under these exact policies.
What permissions belong inside the agent?
The gateway established which tested requests could reach this endpoint. It didn't decide whether a caller may read a particular repository, retrieve a private diff, or write a change. A real reviewer needs those checks where it accesses the repository and invokes its tools.
The bypass control matters too: calling this local backend directly without a token returned 200 and executed once. A deployment needs to restrict direct backend access or enforce equivalent checks there. Rewriting the agent card isn't a network access control.
This reproduction used synthetic credentials, HTTP on a development machine, and a greeting executor. It didn't validate production TLS, identity-provider integration, streaming recovery, or repository permissions. The gateway's standalone listener binds a wildcard address; the backend and administrative endpoints were configured on loopback.
For your own agent, start with an allowed request and specific rejected requests. Check both the response and backend execution. Then remove or change the relevant policy in an isolated test to verify which check caused the rejection. That gives you evidence for the boundary you've configured—and a clear account of what still needs protection.
3 reactions
Week 42: New A2A partners and Community Integrations
Hi All, sharing a few A2A Updates.
This week we have made some progress in our A2A v1.1 protocol and also added a new list of partners.
Partners Added @ https://a2a-protocol.org/latest/partners/
- Agent Commons: A free, public home for independent AI agents offering directory profiles, restart-proof inboxes, and shared knowledge bases, accessible via an open JSON-RPC message/send endpoint.
- DEUSPROOF: A public, immutable notary service that acts as a certificate of birth for AI agents, providing ownership verification, Ed25519 signing, and cryptographic time-stamping.
- Neuronto: A registry and endpoint provider that serves a conformant A2A agent card and endpoint for agent discovery and communication.
Community Integrations Added @ https://a2a-protocol.org/latest/community/
- Quilzo: A community-integrated framework featuring built-in A2A 1.0 support for agent cards, governance extensions, and secure JSON-RPC task execution.
- CUGA Integration: Added CUGA (IBM Research's open-source agent harness) to A2A community integrations, allowing CUGA agents to be served over A2A and delegate tasks to remote agents.
1 point
The example has been replaced (PR #2319 approved), and the Python SDK will warn on the affected cards. Aligning the bytes waits for the next major release.
I did not argue it. I measured it.
A2A#2122
Not a claim about the work. The work.
Mentions
The A2A CLI and Rust: The Same Code, Three Ways to Serve It --- title: "The A2A CLI and Rust: The Same Code, Three Ways to Serve It" published: true description: "A tour of the A2A CLI against Rust agents on A2A v1.0: the official Rust SDK, a plain stdin/stdout program wrapped by a2a server --exec, and an ADK coordinator on Gemini, each timed from the CLI and from a Rust client." tags: rust, a2a, ai, agents cover_image: raw.githubusercontent.com --- This article provides a step by step guide to the A2A CLI, driven against Rust agents that speak A2A Protocol v1.0. The same Rust code, a Lucas-Lehmer test over the first N Mersenne prime exponents, is served three ways: on the official A2A Rust SDK, as a plain stdin/stdout program wrapped by a2a server --exec, and behind an ADK coordinator on Gemini started with the ADK CLI. Each one is timed from the A2A CLI and from a Rust client. github.com/xbill9/rust-dev/tree/main/a2a-benchmar… The A2A CLI talks to all three with the same commands, and a2a server --exec turns a 30-line program with no A2A code into an agent that ADK can call. One a2a send from a shell takes a median 20.74 ms against the SDK agent, of which 7.62 ms is starting the CLI. From a Rust client the SDK agent adds 0.55 to 1.40 ms per call, the wrapped program 3.33 to 5.05 ms, and the ADK coordinator a median 1328 ms on gemini-3.5-flash and 2283 ms on gemini-3.8-flash. Every call through N=26 returned the right count of primes. --- What Is Being Built? | Agent | Port | How it speaks A2A | | :--- | :--- | :--- | | mersenne-agent | 8104 | Official Rust SDK, JSON-RPC at /jsonrpc and HTTP+JSON at /rest | | mersenne-exec | 8105 | A stdin/stdout Rust program served by a2a server --exec | | rust_bench | 8106 | ADK LlmAgent on Gemini, served by adk api_server --a2a | | echo | 8108 | a2a server --echo, for the first command | a2a CLI ──► adk api_server --a2a (:8106) ──Gemini──► RemoteA2aAgent ──► mersenne-agent (:8104) rust_bench └──► a2a server --exec (:8105) ──► mersenne-exec └── run_benchmark tool ──► mersenne-bench ──► both Rust agents Versions, read from the machine on 2026-11-03: | Component | Version | | :--- | :--- | | A2A protocol | 1.0 | | A2A CLI (a2a) | v0.3.1 | | a2a-lf / a2a-server-lf / a2a-client-lf | 0.4.2 / 0.5.3 / 0.2.9 | | Rust | 1.99.1, edition 2024 | | google-adk / a2a-sdk / google-genai | 2.13.0 / 1.3.0 / 2.34.0 | | Model | gemini-3.8-flash on Vertex AI | The a2a-*-lf crates are the official SDK from a2aproject/a2a-rs. Agent cards carry the protocol as Major.Minor, so they read 1.0. --- Step 1 — Install the A2A CLI The CLI is one Go binary. Homebrew, WinGet and release binaries work; from source: go install github.com/a2aproject/a2a-cli@latest mv "$(command -v a2a-cli)" "$(dirname "$(command -v a2a-cli)")/a2a" a2a version a2a v0.3.1 commit: none built: unknown go: go1.26.3 os/arch: linux/amd64 go install names the binary a2a-cli; the docs and the CLI's own skill call it a2a. --- Step 2 — Talk to an Echo Server a2a server --echo starts an agent that sends back what it receives, so the client can be checked before any agent code exists: a2a server --echo --port 8108 & a2a send -a http://localhost:8108 "hello world from A2A" Task: 01a1a35f-2b2b-7f3a-92a2-e3dc6c69e359 Context: 01a1a35f-2b2c-7120-ac45-a74adab9d1d2 Status: completed (2026-11-03T16:44:45-05:00) Artifacts: [01a1a35f-2b2c-71ba-8e6c-22c4b7c210df] hello world from A2A History: [user] hello world from A2A Every send creates a task with an id, a context, a final status and its artifacts. The Go servers started by the CLI stamp local time; the Rust SDK agent stamps UTC. --- Step 3 — Build and Start the Rust Agents The rig is one Cargo project with four binaries: the SDK agent, the stdin/stdout program, the benchmark client, and a shared library holding the Lucas-Lehmer code. git clone https://github.com/xbill9/rust-dev cd rust-dev/a2a-benchmark-rust cargo build --release scripts/start-all.sh agents up: 8104 native, 8105 exec start-all.sh rebuilds, stops any running copy, starts the SDK agent on 8104 and a2a server --exec on 8105 with logs in logs/, and waits until both cards answer. scripts/test-cli.sh runs the CLI commands below against them and checks each reply. --- Step 4 — Read an Agent Card card get fetches /.well-known/agent-card.json and prints what the agent offers: a2a card get -a http://localhost:8104 Name: Mersenne Prime Agent Rust Description: A Rust A2A v1.0 agent (official a2a-rs SDK) that confirms the first n Mersenne primes with the Lucas-Lehmer test and reports the elapsed time. Version: 0.1.0 Interfaces: JSONRPC (A2A v1.0) http://localhost:8104/jsonrpc HTTP+JSON (A2A v1.0) http://localhost:8104/rest Streaming: true Skills: find-mersenne-rust Find Mersenne Primes in Rust The card lists two interfaces. The CLI picks one, and --transport chooses. --- Step 5 — Send, Stream, Switch Transport, Send Data A plain message: a2a send -a http://localhost:8104 "Calculate the first 12 Mersenne primes" Status: completed (2026-11-03T21:44:45Z) Found first 12 Mersenne primes in 0.0902ms. Artifacts: [mersenne-report] Found first 12 Mersenne primes in 0.0902ms. {"elapsed_ms":0.090239,"exponents":[2,3,5,7,13,17,19,31,61,89,107,127],"found":12,"requested":12} The mersenne-report artifact carries two parts: the summary as text, and the same result as a data part, so a client reads the timing from a field. --stream prints each event as it arrives, here the working status before the result: a2a send -a http://localhost:8104 --stream "first 8" [status] working Task: 01a1a35f-2b64-77b5-9417-18b1f425b17d Context: 01a1a35f-2b64-77b5-9417-18b24253b719 Status: completed (2026-11-03T21:44:45Z) Found first 8 Mersenne primes in 0.0164ms. --transport rest sends the same request over HTTP+JSON, and --data-part sends structured input: a2a send -a http://localhost:8104 --transport rest "first 4" a2a send -a http://localhost:8104 --data-part '{"count": 6}' Found first 4 Mersenne primes in 0.0080ms. Found first 6 Mersenne primes in 0.0108ms. --- 🔎 Tip: Data Parts Arrive as Floats A2A v1.0 carries data parts as a protobuf Struct, whose only number type is a double. {"count": 6} sent by the CLI reaches the agent as 6.0, so a strict integer field fails to deserialise. Read numbers with as_f64 and check for a whole value: if let PartContent::Data(v) = &part.content && let Some(n) = v.get("count").and_then(|n| n.as_f64()) && n >= 1.0 && n.fract() == 0.0 { return n as usize; } --- Step 6 — Script It With JSON Output -o json prints the whole task, which pipes into anything: a2a send -a http://localhost:8104 -o json 'first 3' | python3 -c 'import sys,json; t=json.load(sys.stdin)["task"]; print(t["status"]["state"], [p for a in t["artifacts"] for p in a["parts"] if "data" in p][0]["data"])' TASK_STATE_COMPLETED {'elapsed_ms': 0.007166, 'exponents': [2, 3, 5], 'found': 3, 'requested': 3} --- Step 7 — Tasks: List, Start in the Background, Wait The SDK agent keeps its tasks in memory, and the CLI reads them back: a2a task list -a http://localhost:8104 ID STATUS CONTEXT 01a1a35f-2b4f-7190-b6df-81bb7b76c2cc completed 01a1a35f-2b4f-7190-b6df-81bca35e8b6d 01a1a35f-2b64-77b5-9417-18b1f425b17d completed 01a1a35f-2b64-77b5-9417-18b24253b719 01a1a35f-2bab-7012-b094-1022dce75f36 completed 01a1a35f-2bab-7012-b094-1023824b35eb --async returns as soon as the task exists. N=25 takes over a second, long enough to catch it running: a2a send -a http://localhost:8104 --async "first 25" Task: 01a1a3f5-9da1-746b-abae-84307e4e1735 Context: 01a1a3f5-9da1-746b-abae-8431b422e72e Status: working (2026-11-04T00:29:05Z) History: [user] first 25 task get --wait polls until the task finishes: a2a task get -a http://localhost:8104 01a1a3f5-9da1-746b-abae-84307e4e1735 --wait --poll-interval 500ms Task: 01a1a3f5-9da1-746b-abae-84307e4e1735 Context: 01a1a3f5-9da1-746b-abae-8431b422e72e Status: completed (2026-11-04T00:29:06Z) Found first 25 Mersenne primes in 1509.7683ms. --- Step 8 — The Native Rust Agent The SDK agent is one AgentExecutor. It emits a working status, runs the big-integer work on a blocking thread, then returns the finished task: let done = async move { // Big-integer work is CPU-bound; keep it off the async workers. let report = tokio::task::spawn_blocking(move || find_mersenne_primes(count)) .await .map_err(|e| A2AError::internal(format!("worker panicked: {e}")))?; let summary = report.summary(); let data = serde_json::to_value(&report) .map_err(|e| A2AError::internal(format!("serialise report: {e}")))?; // ... Task { status: Completed, artifacts: [Part::text(summary), Part::data(data)] } }; Box::pin(stream::once(async { Ok(working) }).chain(stream::once(done))) The server wires the SDK's routers into axum, one per transport plus the card: let handler = Arc::new(DefaultRequestHandler::new(MersenneExecutor, InMemoryTaskStore::new())); let card = Arc::new(StaticAgentCard::new(agent_card(&base))); let app = axum::Router::new() .nest("/jsonrpc", a2a_server::jsonrpc::jsonrpc_router(handler.clone())) .nest("/rest", a2a_server::rest::rest_router(handler)) .merge(a2a_server::agent_card::agent_card_router(card)); The Lucas-Lehmer test runs on num-bigint with the reduction mod 2^p − 1 done by folding the high bits onto the low ones. The timed region holds only the tests, on a monotonic clock, and no prime is ever turned into a decimal string. Two slow tests check the arithmetic: one runs the test on every prime exponent up to 9941 and asserts it accepts exactly the first 22 known ones, the other confirms all 26: cargo test --release --lib -- --ignored test tests::all_26_exponents_prime ... ok test tests::exhaustive_through_n22 ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 25.70s --- Step 9 — Serve a Plain Program With a2a server --exec mersenne-exec knows nothing about A2A. It reads the message on stdin and prints two lines: echo 'first 5' | target/release/mersenne-exec {"requested":5,"found":5,"elapsed_ms":0.007023000000000001,"exponents":[2,3,5,7,13]} Found first 5 Mersenne primes in 0.0070ms. The A2A CLI makes it an agent: a2a server --exec "$PWD/target/release/mersenne-exec" --chunk=$'\n' \ --name "Mersenne Prime Exec Rust" \ --advertise-address "http://localhost:8105" --port 8105 The same client commands work against it: a2a send -a http://localhost:8105 --stream "first 8" Status: submitted History: [user] first 8 [status] working [artifact] {"requested":8,"found":8,"elapsed_ms":0.01543,"exponents":[2,3,5,7,13,17,19,31]}Found first 8 Mersenne primes in 0.0154ms. [status] completed The exec server starts the program once per message, hands it the text, and returns stdout as the artifact. --- 🔎 Tip: Two --exec Flags ADK Needs - --advertise-address http://localhost:8105. The exec server's card names 127.0.0.1 by default, and ADK's RemoteA2aAgent refuses a card whose RPC URL has a different origin from the one the card came from. - --chunk=$'\n'. With it, each output line streams as its own chunk. Without it, stream requests get UNSUPPORTED_OPERATION: the Go CLI falls back to polling and ADK's Python client fails. ADK keeps only the last chunk, so mersenne-exec prints the JSON line first and the summary last. --- Step 10 — Put ADK in Front ADK needs its Python package and Gemini on Vertex AI, signed in with application-default credentials: pip install -U "google-adk[a2a]" gcloud auth application-default login source scripts/adk-env.sh adk-env.sh sets GOOGLE_GENAI_USE_VERTEXAI=TRUE, GOOGLE_CLOUD_LOCATION=global, the project, and MODEL_NAME, which defaults to gemini-3.8-flash. adk/rust_bench/agent.py is an ADK LlmAgent with both Rust agents as RemoteA2aAgent sub-agents and one tool: native_agent = RemoteA2aAgent( name="native_rust_agent", description="Rust A2A v1.0 agent on the official a2a-rs SDK. ...", agent_card=f"{NATIVE_URL}{AGENT_CARD_WELL_KNOWN_PATH}", ) root_agent = LlmAgent( name="rust_bench", model=os.getenv("MODEL_NAME", "gemini-3.8-flash"), tools=[run_benchmark], sub_agents=[native_agent, exec_agent], ) The ADK CLI runs it three ways: adk run rust_bench # terminal adk web --port 8107 # dev UI adk api_server --a2a --port 8106 adk # A2A server, from the rig root adk api_server --a2a serves each agent folder that holds an agent.json card, at /a2a/<name>; scripts/start-adk.sh starts it in the background. The A2A CLI then treats the coordinator like any other agent: a2a send -a http://localhost:8106/a2a/rust_bench/.well-known/agent-card.json \ --timeout 120s "Find the first 9 Mersenne primes with the exec agent" Status: completed (2026-11-03T21:44:51Z) Artifacts: [f2d2f27d-24ce-4120-8368-d966c99617a9] Found first 9 Mersenne primes in 0.0236ms. History: [user] Find the first 9 Mersenne primes with the exec agent [agent] {"args":{"agent_name":"exec_rust_agent"},"id":"call_1332004","name":"transfer_to_agent"} [agent] {"id":"call_1332004","name":"transfer_to_agent","response":{"result":null}} [agent] {"requested":9,"found":9,"elapsed_ms":0.023635,"exponents":[2,3,5,7,13,17,19,31,61]} [agent] Found first 9 Mersenne primes in 0.0236ms. The history shows the hand-off: Gemini calls transfer_to_agent, the Rust agent answers, and its JSON report comes back through two A2A hops. --- Step 11 — Keep the Model Away From the Numbers A benchmark through an LLM can time the model writing a number. Two rules keep the timing in code: - The benchmark client checks every reply. mersenne-bench reads the timing from the agent's data part or JSON line, rejects a report where requested differs from N or found differs from min(N, 26), and rejects an LLM-path reply with no transfer_to_agent to a Rust agent. Each row records where its report came from and who it was routed to. - ADK benchmarks go through a tool. run_benchmark runs mersenne-bench and returns medians already computed and rounded. The instruction tells Gemini to quote them, and scripts/check-adk-reply.py verifies a reply: one run_benchmark call, with the arguments asked for, and every number in the answer a value the tool returned. A benchmark request sent from the CLI, saved with -o json. The tool call and the head of the answer: a2a send -a $CARD --timeout 300s -o json "Benchmark both agents for N=3 to 6 with 2 reps" > reply.json [tool call] {"max_n": 6, "min_n": 3, "reps": 2} ### Arguments Used - `min_n`: 3 - `max_n`: 6 - `reps`: 2 The answer goes on with a summary per agent and a row per N. The check: scripts/check-adk-reply.py reply.json 3 6 2 state: TASK_STATE_COMPLETED run_benchmark calls: 1 args sent: {'max_n': 6, 'min_n': 3, 'reps': 2} requested: {'min_n': 3, 'max_n': 6, 'reps': 2} numbers in reply: 57 not from the tool: 0 PASS scripts/adk-reliability.sh sends ten benchmark requests with varied ranges and ten requests that name no agent. On gemini-3.8-flash, 10 of 10 benchmark replies passed the checker and 10 of 10 unnamed requests went to native_rust_agent. The sweep covers the whole exponent table, N=1 to 26. Exponent 23209 is a 6,987-digit number, and the arithmetic stays in BigUint throughout. --- Step 12 — Benchmark From a Rust Client REPS=20 scripts/bench.sh # native + exec, N=1..26 ADK=1 REPS=3 scripts/bench.sh # adds the ADK series mersenne-bench is built on the SDK's client crate, a2a-client-lf. Each call records the agent's own compute time and the client's round trip; overhead is the round trip minus compute, per call. Direct calls, medians of 20 reps: | N | native compute ms | native overhead ms | exec compute ms | exec overhead ms | | ---: | ---: | ---: | ---: | ---: | | 1 | 0.001039 | 0.731 | 0.0004555 | 4.046 | | 10 | 0.04249 | 0.793 | 0.04200 | 4.391 | | 20 | 26.33 | 0.970 | 60.21 | 4.374 | | 22 | 181.1 | 1.255 | 264.6 | 3.558 | | 24 | 781.1 | 1.073 | 868.9 | 3.335 | | 26 | 2484 | 1.396 | 2619 | 3.588 | Through ADK, medians of 3 reps, one run per model. All 26 rows in each run went to native_rust_agent and read its data part: | N | 3.5-flash compute ms | 3.5-flash round trip ms | 3.8-flash compute ms | 3.8-flash round trip ms | | ---: | ---: | ---: | ---: | ---: | | 1 | 0.002051 | 1665.053 | 0.002615 | 2073.666 | | 10 | 0.04513 | 1394.245 | 0.04390 | 2458.155 | | 20 | 72.95 | 1332.994 | 68.52 | 2336.569 | | 22 | 279.8 | 1868.178 | 287.3 | 2679.324 | | 26 | 2226 | 3455.492 | 2241 | 4123.353 | The slowest single call on gemini-3.8-flash took 29.0 s, at N=9. --- Step 13 — Time the A2A CLI Itself scripts/cli-bench.py runs one a2a send -o json process per call, the way a shell script would, and applies the same checks to each reply. Wall time covers starting the CLI, fetching the card, sending, and exiting. Two reference commands, 20 runs each: | Command | Median ms | Min ms | Max ms | | :--- | ---: | ---: | ---: | | a2a version | 7.17 | 5.57 | 9.62 | | a2a card get (native) | 9.18 | 8.30 | 11.17 | The full sweep, N=1 to 26, 5 reps direct and 3 through ADK: | Agent | Calls | Overhead per call, per-N medians | Median of the per-N medians | | :--- | ---: | :--- | ---: | | native | 130 | 10.92 to 21.82 ms | 20.28 ms | | exec | 130 | 9.43 to 20.81 ms | 17.51 ms | | ADK, 3.8-flash | 78 | 1825.22 to 3017.92 ms | 2217.55 ms | Every ADK call went to native_rust_agent; the slowest took 22693.6 ms. The sweep times each agent in its own block, so native and exec ran minutes apart. Run in alternating order, N=1, 30 runs each (scripts/cli-transport.py): native, default (JSON-RPC): median 20.74 ms, min 18.60, max 26.51 native, --transport rest: median 21.49 ms, min 19.63, max 26.15 exec (HTTP+JSON only): median 20.81 ms, min 18.67, max 28.07 a2a version: median 7.62 ms, min 6.07, max 10.71 From a shell, the transport and the serving path make no difference to the CLI's cost: about 21 ms per call, about a third of it starting the process. Against the Rust client's 0.55 to 1.40 ms, the CLI fits scripts and checks, and the Rust client fits a benchmark. The CLI's overhead in the sweep drops to about 11 ms for both agents once compute passes 100 ms (N=21 and up). Why is untested. --- 🔎 Tip: Compare Compute Only Between Runs Paced Alike The same native process at N=20 takes 26.07 ms when calls arrive back to back and 68.09 ms when each call follows a 1.3 s idle gap (mersenne-bench --gap-ms 1300). ADK's model round trip spaces calls out too, and its mid-range compute times sit close to the gap series: 68.52 ms at N=20 on gemini-3.8-flash. The CPU-level cause, clock scaling or core placement on a hybrid laptop CPU, is untested. --- Compare and Contrast | Serving path | Code the agent needs | Overhead from a Rust client | a2a send from a shell, N=1 | | :--- | :--- | ---: | ---: | | Official Rust SDK 🥇 | an AgentExecutor and three routers | 0.55 to 1.40 ms | 20.74 ms | | a2a server --exec 🥈 | none: stdin in, stdout out | 3.33 to 5.05 ms | 20.81 ms | | ADK coordinator 🥉 | an agent.py, an agent.json card | 1328 ms on 3.5-flash, 2283 ms on 3.8-flash | 2664.00 ms on 3.8-flash | Rust-client ranges are the smallest and largest per-N medians; the ADK figures are the medians of its per-N medians. The exec figure includes starting a process for every message; how it splits between process start and the HTTP hop was not measured. At N=26 the work itself dominates every path: 2484 ms native and 2619 ms exec, direct. --- So, Which One? For an agent built to be one, use the SDK: about a millisecond of protocol cost, both transports, tasks the CLI can list and wait on. For a program that already exists, a2a server --exec makes it an agent with no code change. A few milliseconds per call buys that, and ADK can call it once the two flags are set. For routing by natural language, put ADK in front and keep the timing out of the model: a tool for anything computed, and a check that the model sent the arguments asked for. For talking to any of them, the A2A CLI is the one client: card get, send, --stream, task get --wait, and -o json for scripts. Each call costs about 21 ms, so time agents from a long-lived client. --- Summary The goal of this article was to drive the A2A CLI against Rust agents on A2A v1.0, and to measure what each way of serving the same Rust code costs per call. The key to the solution was one benchmark client that reads timing from a structured report and checks that every reply answers the question asked. The results on one laptop were: - 🟢 The A2A CLI drove all three agents with the same commands: card, send, stream, REST, data parts, JSON output, task list, --async and task get --wait - 🟢 Every call through N=26, from the Rust client and from the CLI, reported found = min(N, 26) - 🟢 The SDK agent added 0.55 to 1.40 ms per call from a Rust client - 🟢 a2a server --exec served an A2A-unaware program to the CLI and to ADK - 🟢 On gemini-3.8-flash, 10 of 10 benchmark replies quoted only tool values, and 10 of 10 unnamed requests went to the native agent - ⚠️ One a2a send from a shell costs about 21 ms whatever the agent, 7.62 ms of it starting the CLI - ⚠️ The wrapped program added 3.33 to 5.05 ms per call from a Rust client - ⚠️ ADK added a median 1328 ms per call on gemini-3.5-flash and 2283 ms on gemini-3.8-flash, with one call at 29.0 s - ❌ Compute times from calls spaced by idle gaps run slower and do not compare with back-to-back runs Scope: one 13th Gen Intel Core i7-1360P laptop on Linux, release build, every agent and client on the same machine, 2026-11-03 and 2026-11-04. Rust-client series have 20 reps per N direct and 3 through ADK, with one run per model on Vertex AI's shared global endpoint, so the difference between the two Gemini models is a single sample. The CLI series has 5 reps per N direct and 3 through ADK, on gemini-3.8-flash only, and reliability was checked on that model only, ten requests of each kind. The gRPC transport, task cancel, task subscribe, push notifications and authentication were not exercised. The strategy for serving and benchmarking Rust A2A agents with the A2A CLI was validated with an incremental step by step approach. --- References - The rig: github.com/xbill9/rust-dev/tree/main/a2a-benchmar… - CLI tour transcripts: github.com/xbill9/rust-dev/blob/main/a2a-benchmar… and github.com/xbill9/rust-dev/blob/main/a2a-benchmar… - CLI timing: github.com/xbill9/rust-dev/blob/main/a2a-benchmar… - Introducing the A2A CLI: a2a-protocol.org - A2A CLI: a2aproject/a2a-cli - A2A Rust SDK: a2aproject/a2a-rs - Agent Development Kit: google.github.io - Why Did My Benchmark Stop at N=22?: dev.to
A2A (Agent2Agent Protocol): Building Interoperable Multi-Agent Systems Understanding how AI agents discover, communicate, delegate tasks, and collaborate across different frameworks and platforms. 1. What Is A2A, and What Problem Does It Solve? As AI applications become more capable, we are moving beyond simple interactions between a user and a single AI model. Today, an AI agent can search documentation, query databases, interact with APIs, analyze code, and execute multi-step workflows. However, as these capabilities grow, a new architectural question emerges: How can independently developed AI agents communicate and work together? Consider an application that needs to analyze a GitLab merge request. The task may involve several different responsibilities: - A security agent reviews potential vulnerabilities. - A testing agent analyzes test coverage and identifies missing cases. - An architecture agent evaluates design decisions and code structure. - A review agent combines the findings and prepares a final report. Each agent might use a different framework, model provider, toolset, or programming language. Without a standardized communication mechanism, integrating these agents usually requires custom APIs, shared data contracts, and application-specific orchestration logic. This is where Agent2Agent (A2A) Protocol becomes relevant. A2A is an open protocol designed to enable communication and collaboration between independent AI agent systems. It provides a common interaction model regardless of the framework, programming language, or vendor used to build an agent. The protocol was originally developed by Google and contributed to the Linux Foundation. It is designed around interoperability without requiring agents to expose their internal reasoning, memory, tools, or implementation details. The important distinction is that A2A does not define how an agent thinks or performs its internal tasks. It defines how one agent can communicate with another. In other words, an agent can be implemented using LangGraph, Google ADK, CrewAI, Semantic Kernel, or a completely custom solution and still communicate with another compatible agent through A2A. Reference: A2A Protocol — Official Documentation 2. Why Do We Need A2A? Most agent frameworks provide mechanisms for calling tools, delegating work, or coordinating multiple agents. However, these mechanisms are generally designed around the framework's own execution model. For example, an agent built with one framework may have its own task execution logic, state management, and internal tool definitions. Connecting it directly to an agent developed using another framework can require custom integration work. A2A addresses the interoperability problem. Instead of requiring agents to understand each other's internal implementations, they communicate through a standardized protocol. This gives us several important capabilities: - Discover agents and understand their capabilities. - Delegate tasks to remote agents. - Exchange messages and structured data. - Track the lifecycle of long-running tasks. - Receive progress updates through streaming. - Exchange files and generated artifacts. - Support authentication and authorization across agent boundaries. A2A is particularly useful when agents belong to different teams, applications, organizations, or technology stacks. The goal is not to turn every agent into the same implementation. The goal is to allow different implementations to collaborate. 3. A2A vs. MCP vs. ACP These three protocols are often discussed together, but they solve different architectural problems. | Technology | Main responsibility | |---|---| | MCP | Agent ↔ Tools and external context | | ACP | Coding Agent ↔ Client Application | | A2A | Agent ↔ Agent | Let's consider a practical example. Imagine a research agent that needs to investigate a topic, retrieve information from internal documentation, and delegate a part of its work to a specialized analysis agent. In this scenario: 1. MCP provides access to documentation, APIs, databases, and other tools. 2. A2A allows the research agent to communicate with the analysis agent. 3. ACP could be used separately when integrating a coding agent with an editor or another client application. A simplified architecture: User | v Main AI Application | v Coordinator Agent / \ / \ A2A A2A / \ v v Research Agent Analysis Agent | | MCP MCP | | Internal APIs Database / APIs | | v v Results Results \ / \ / Coordinator | v Final Response Each agent can use its own tools and internal architecture. A2A provides the communication layer between them. One important point: A2A is not a replacement for an agent framework or an internal sub-agent mechanism. It focuses on communication between independent agent applications. Internal orchestration can still be handled by the framework that runs the agent. References: A2A and MCP · MCP Documentation · ACP Documentation 4. How Does A2A Work? To understand A2A, we need to understand a few core concepts. 4.1. Agent Card: Discovering Other Agents One of the most important concepts in A2A is the Agent Card. An Agent Card is a JSON document that describes an agent and its capabilities. It acts as a machine-readable description of what an agent can do and how another agent can communicate with it. An Agent Card can contain: - Agent name and description - Service endpoint - Supported protocol versions and transports - Available capabilities - Supported input and output formats - Skills and example tasks - Authentication requirements By default, an A2A server exposes its Agent Card at: https://agent.example.com/.well-known/agent-card.json For example, a research agent might expose the following description: { "name": "Research Agent", "description": "An agent specialized in research and source analysis.", "version": "1.0.0", "supportedInterfaces": [ { "url": "https://research.example.com/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" } ], "capabilities": { "streaming": true, "pushNotifications": true, "extensions": [] }, "defaultInputModes": [ "text/plain" ], "defaultOutputModes": [ "text/plain", "application/json" ], "skills": [ { "id": "market-research", "name": "Market Research", "description": "Researches topics and produces structured findings.", "tags": [ "research", "analysis" ], "examples": [ "Research the latest developments in AI agents." ], "inputModes": [ "text/plain" ], "outputModes": [ "text/plain", "application/json" ] } ] } This is a simplified public Agent Card example. Authentication and additional security requirements can also be described through the protocol's security fields. A client can retrieve this document and understand whether the remote agent is suitable for a particular task. For example, a coordinator agent looking for an agent capable of analyzing financial documents can inspect registered Agent Cards and evaluate their declared skills. Agent discovery can happen through: - Well-known URLs - Private or public agent registries - Directly configured Agent Card URLs An Agent Card describes an agent's advertised capabilities; it does not prove that the agent can successfully perform every advertised task. Capability selection should be combined with authorization, availability checks, and runtime evaluation. Reference: A2A — Agent Discovery 4.2. Messages and Tasks A2A distinguishes between a message and a task. Message A message represents communication between a client and an agent. It can contain instructions, clarification requests, or additional information. Task A task represents a stateful unit of work with its own lifecycle. For example, when an agent receives a request to analyze a large document, it may need several minutes to complete the work. Instead of treating the interaction as a simple request-response operation, A2A allows the agent to create a task and report its progress. A task has a lifecycle that can include: | State | Meaning | |---|---| | Submitted | Task has been accepted | | Working | Agent is processing the task | | Input Required | Additional information is needed | | Auth Required | Additional authentication is required | | Completed | Task finished successfully | | Failed | Task execution failed | | Canceled | Task was canceled | | Rejected | Agent declined the task | Some states represent interruptions that can be resolved through further interaction, while others represent terminal states. This is important for long-running workflows because the client does not need to keep a single request open until every operation is complete. A2A also provides two useful identifiers: - taskId: Identifies an individual task. - contextId: Groups related messages and tasks within a broader interaction context. The task identifier is generated by the agent server. The context identifier helps maintain continuity across multiple related interactions. 4.3. Artifacts: Sharing Task Results A2A also introduces the concept of Artifacts. An artifact represents the output produced by an agent while completing a task. For example: - A research agent can produce a research report. - A coding agent can return a generated file. - An analysis agent can return structured JSON. - A document processing agent can return an extracted document. An artifact can contain multiple parts, including text, binary content, URLs, or structured data. This distinction is important: Messages are primarily used for communication and interaction, while artifacts represent the actual outputs of a task. It allows agents to exchange more than just text-based responses. 4.4. Streaming and Asynchronous Communication Not every agent task finishes immediately. A research process, document analysis, or code review can take a considerable amount of time. A2A supports multiple ways of receiving task updates: Streaming The client can subscribe to task updates and receive events while the agent is working. Depending on the protocol binding, streaming is supported through Server-Sent Events (SSE) or gRPC streaming. Polling The client can periodically request the current task state. Push Notifications For long-running tasks where maintaining a persistent connection is not practical, the agent can send updates to a client-provided webhook. These mechanisms allow applications to support different execution patterns without inventing custom communication protocols for each agent. A2A also defines operations for sending messages, retrieving tasks, canceling tasks, subscribing to updates, and managing push notification configurations. References: A2A Specification · Task Lifecycle · Streaming and Asynchronous Operations 5. How to Integrate A2A? A2A provides official SDKs for several programming languages, including: - Python - JavaScript / TypeScript - Java - Go - C# / .NET - Rust For TypeScript applications, the official package is @a2a-js/sdk. The current SDK implements the A2A v1.0 specification and supports client and server development. Step 1: Install the package pnpm add @a2a-js/sdk The official JavaScript SDK requires Node.js 20 or later. For this example, we will implement a client that communicates with an existing remote research agent. Step 2: Connect to an A2A agent The SDK provides ClientFactory to create a client. import { ClientFactory } from '@a2a-js/sdk/client'; const factory = new ClientFactory(); const client = await factory.createFromUrl( 'https://research-agent.example.com' ); The SDK retrieves the Agent Card and selects a compatible transport based on the agent's advertised interfaces and the client's supported transports. This means we do not need to hardcode every protocol detail into our application. Step 3: Send a message to the agent Once the connection is established, we can send a task request. import { Role, type Message, type SendMessageRequest, } from '@a2a-js/sdk'; const message: Message = { messageId: crypto.randomUUID(), role: Role.ROLE_USER, parts: [ { content: { $case: 'text', value: 'Research the latest developments in multi-agent systems.', }, mediaType: 'text/plain', filename: '', metadata: undefined, }, ], taskId: '', contextId: '', extensions: [], metadata: {}, referenceTaskIds: [], }; const request: SendMessageRequest = { tenant: '', message, configuration: undefined, metadata: {}, }; const result = await client.sendMessage(request); console.log(result); The example uses the SDK's generated TypeScript message types. Depending on the agent's behavior, the response can contain a message or a task. For production integrations, authentication and request timeouts should be configured as well. For example: const result = await client.sendMessage(request, { serviceParameters: { Authorization: `Bearer ${process.env.A2A_TOKEN}`, }, signal: AbortSignal.timeout(60_000), }); The client can now communicate with a remote agent without needing to know its internal framework or model implementation. Step 4: Stream task updates For long-running tasks, we can use sendMessageStream. const stream = client.sendMessageStream(request); for await (const event of stream) { // Handle task events: // - task // - statusUpdate // - artifactUpdate console.log(event); } The actual event union should be handled using the SDK's generated types. This allows us to display task progress, process generated artifacts, and update the application as the remote agent works. The SDK also supports task retrieval, cancellation, and push notification configuration. Step 5: Build your own A2A agent A2A is not limited to consuming existing agents. We can also expose our own agent through the protocol. The official JavaScript SDK separates server-side responsibilities into three main components: AgentExecutor Contains the agent's business logic. It receives the request context, executes the task, and publishes messages, status updates, and artifacts. DefaultRequestHandler Handles protocol-level responsibilities such as message routing, task storage, cancellation, and push notifications. Transport Adapters Expose the agent through supported communication protocols. The SDK provides adapters for JSON-RPC and HTTP+JSON/REST through Express, as well as gRPC for Node.js. The architecture looks like this: Incoming A2A Request | v Transport Adapter | v DefaultRequestHandler | +----> Task Store | +----> Execution Event Bus | v AgentExecutor | v Agent Logic | +----> LLM +----> MCP Tools +----> Internal APIs +----> Business Logic | v Messages / Status / Artifacts | v A2A Response The SDK also supports custom task stores and event buses, which allows us to integrate persistent databases, caches, or message brokers instead of relying exclusively on in-memory implementations. For production, this separation is particularly useful because protocol handling, task execution, and persistence can evolve independently. Reference: Official A2A JavaScript SDK 6. Integrating A2A with the Vercel AI SDK There is another interesting integration worth looking at. The Vercel AI SDK lists an A2A community provider called a2a-ai-provider. This package allows an A2A-compatible agent to be consumed through familiar AI SDK functions such as generateText and streamText. This is conceptually similar to the ACP provider we discussed previously. Instead of implementing every A2A interaction manually, we can use the community provider as an adapter between the A2A protocol and the AI SDK. Installation pnpm add ai a2a-ai-provider Provider configuration import { a2a } from 'a2a-ai-provider'; import { generateText } from 'ai'; const result = await generateText({ model: a2a( 'https://research-agent.example.com/.well-known/agent-card.json' ), prompt: 'Research the latest developments in agentic AI.', }); console.log(result.text); Streaming responses We can also use streamText: import { a2a } from 'a2a-ai-provider'; import { streamText } from 'ai'; const result = streamText({ model: a2a( 'https://research-agent.example.com/.well-known/agent-card.json' ), prompt: 'Analyze the current multi-agent ecosystem.', providerOptions: { a2a: { contextId: crypto.randomUUID(), }, }, }); await result.consumeStream(); console.log(await result.content); Here, contextId is used to associate related interactions with the same context. This integration makes it easier to use A2A agents in existing AI SDK applications. However, there is an important distinction: The official A2A SDK is the protocol implementation, whereas a2a-ai-provider is a community-maintained adapter. The community provider's documentation describes it as under active development, and its GitHub repository currently labels it alpha and advises against production workloads. For that reason, I would use it for experimentation and evaluate the official SDK directly for production-critical integrations until the provider's maturity and compatibility are established. References: AI SDK — A2A Community Provider · a2a-ai-provider GitHub 7. Supported Communication Protocols A2A separates its common data model and operations from the transport used to communicate. The current specification defines three standard protocol bindings: | Protocol | Description | |---|---| | JSON-RPC | JSON-based remote procedure calls | | HTTP+JSON / REST | Resource-oriented HTTP communication | | gRPC | RPC communication using Protocol Buffers and gRPC | An agent can support more than one binding. For example, a service could expose JSON-RPC and REST interfaces using the same underlying task execution logic. This separation is valuable because clients and servers can select a compatible transport without changing the fundamental meaning of tasks, messages, and artifacts. The official JavaScript SDK supports all three transports, with its gRPC integration requiring a Node.js environment and additional dependencies. A2A also defines protocol versioning. Clients and agents use the A2A-Version header to identify the version they are communicating with. The current v1.0 specification includes changes from the earlier v0.3 interface, so compatibility and migration should be considered when connecting existing implementations. Reference: A2A Protocol Specification 8. Advantages of A2A Interoperability across frameworks One of A2A's biggest advantages is the ability to connect agents built using different frameworks and programming languages. We do not have to standardize every team's internal agent implementation just to enable collaboration. A Python-based research agent can communicate with a TypeScript-based coordinator, while another agent may be implemented using Go or .NET. This reduces framework-level coupling. Independent agent implementations Agents do not need to expose their internal prompts, model configurations, memory, or tools. They only need to expose their supported capabilities and implement the agreed communication contract. This allows teams to evolve their internal implementations without forcing all consumers to change their integration code. Better support for long-running workflows A2A is designed around stateful tasks. Instead of implementing custom job tracking, status polling, event streaming, and result retrieval for every remote agent, we can use standardized operations. This is particularly useful for research, document processing, code analysis, and other workflows that may take several minutes or longer. More flexible output handling Agents can exchange text, files, structured data, and other supported content types through message parts and artifacts. This makes the protocol useful beyond conventional text-based chat applications. Centralized agent discovery and integration Agent Cards allow applications to discover what an agent offers and how to communicate with it. Combined with a curated internal agent registry, this can provide a common discovery layer for enterprise AI platforms. 9. Limitations and Trade-offs A2A solves a real interoperability problem, but it also introduces architectural complexity. It does not provide orchestration by itself A2A defines how agents communicate, not how an entire multi-agent system should be planned and executed. It does not decide which agent should receive a task, how many agents should participate, or how conflicting results should be resolved. Those responsibilities remain with an orchestrator or an agent framework. For example, LangGraph, Google ADK, or a custom coordinator can decide when to delegate a task through A2A. Distributed execution introduces more failure scenarios When agents communicate across network boundaries, new failure scenarios appear: - A remote agent may become unavailable. - A request may time out while the task continues running. - A task may fail after partial execution. - A client may disconnect before receiving the final artifact. - A downstream agent may return an incomplete or unexpected result. Therefore, retries, cancellation, idempotency, timeout policies, and persistent task storage need to be considered carefully. More communication can increase latency and cost Every agent handoff introduces additional work. A complex multi-agent workflow may involve several model calls, network requests, and tool executions. Using more agents does not automatically improve the result. We should introduce agent delegation only when the additional specialization or parallelism justifies the extra latency, cost, and operational complexity. Agent compatibility does not guarantee equivalent capabilities Two agents may both implement A2A but provide very different skills and execution behavior. A2A standardizes communication, not the quality or accuracy of an agent. Clients still need to evaluate supported capabilities, input/output formats, and the reliability of the remote service. Version compatibility requires attention The transition from earlier protocol versions to v1.0 introduced changes to operation names, Agent Cards, and other protocol details. The official TypeScript SDK provides an opt-in v0.3 compatibility layer, which can help during staged migration. However, version compatibility should be tested explicitly rather than assumed. Security does not come automatically A2A supports authentication and authorization mechanisms, but the protocol does not automatically make remote agents trustworthy. The application must decide which agents can communicate, what data can be shared, and which actions are permitted. Reference: A2A v1.0 Specification · A2A Security Considerations 10. Security and Production Considerations This is one of the most important parts of adopting A2A in an enterprise environment. An agent may receive data from another agent, process documents, invoke tools, and return results that influence subsequent decisions. We should not treat every remote agent as trusted simply because it implements the same protocol. Some important considerations: Authentication A2A relies on established authentication mechanisms such as HTTP authentication, OAuth 2.0, OpenID Connect, API keys, and mutual TLS where appropriate. The agent can advertise supported authentication schemes through its Agent Card. Credentials are handled at the transport layer, not embedded directly into protocol messages. Authorization Authentication and authorization are different responsibilities. Knowing who is calling an agent does not automatically mean that the caller should be allowed to invoke every skill. Authorization should consider: - Which application or user initiated the task? - Which agent is allowed to communicate with the target agent? - Which skills can be invoked? - Which business operations are permitted? - Which data can be accessed or shared? For financial or other sensitive enterprise environments, authorization should be enforced at the skill and business-action levels. Data isolation Agents should receive only the information required to complete their tasks. A research agent should not automatically receive the entire conversation history, database access, or credentials of another agent. Task and context storage should also respect user and tenant boundaries. Agent Card validation Agent Cards describe service identity and capabilities. For sensitive environments, clients should verify the origin of Agent Cards and validate their signatures when available. A discovery result should not automatically be treated as a trusted execution target. Observability and auditing A multi-agent system needs visibility beyond individual model calls. We should record: - Caller and target agent identifiers - Task and context identifiers - Correlation and trace identifiers - Task state transitions - Request duration and failure reasons - Artifact metadata and access - Sensitive action approvals OpenTelemetry and distributed tracing can help follow a request across multiple agents. Push notification security Push notification webhooks need their own security controls. For example, the receiver should authenticate the notification source, validate the task identifier, and process duplicate notifications safely. Webhook endpoints should also be protected against SSRF and unauthorized access. These controls are application and infrastructure responsibilities that must be designed alongside the protocol integration. Reference: A2A — Enterprise Implementation 11. What Can an AI Chapter Lead Contribute? I think this is where A2A becomes especially interesting from an engineering leadership perspective. The value is not simply adding another protocol to the technology stack. It is establishing a reusable architecture for teams that want to build specialized agents independently and allow them to collaborate. There are several areas where we can contribute. 1. Establish a common agent communication standard Different teams may build agents using different languages and frameworks. Instead of forcing everyone to adopt the same framework, we can define a common communication contract based on A2A. This allows teams to own their agent implementations while maintaining a consistent integration approach. 2. Build an internal agent registry We can create a centralized catalog of available agents and their capabilities. For example: | Agent | Responsibility | Available skills | |---|---|---| | Research Agent | Information gathering | Research, source analysis | | Database Agent | Data analysis | SQL analysis, reporting | | Code Review Agent | Code quality | Review, recommendations | | Security Agent | Security analysis | Vulnerability assessment | | Documentation Agent | Documentation | Search, summarization | Each agent can publish an Agent Card, while the internal registry handles discovery, ownership, versioning, authorization, and availability. This would help teams reuse existing capabilities instead of implementing the same functionality repeatedly. 3. Define a reusable orchestration architecture A2A should be combined with a clear orchestration strategy. We can establish a reference architecture that separates: - Agent discovery - Task delegation - Agent communication - Execution state management - Tool and data access - Result aggregation - Human approval The coordinator should remain responsible for task planning and result aggregation. Specialized agents should focus on their assigned responsibilities. 4. Establish security and governance standards A2A introduces communication between independently managed systems. We should define clear standards for agent authentication, authorization, data sharing, tenant isolation, and audit logging. This is especially important when agents interact with internal systems or execute business operations. 5. Run a practical multi-agent pilot Rather than introducing A2A across every team immediately, we can start with a well-defined workflow. A good example would be an automated merge request review process. Example workflow: 1. A developer submits a merge request. 2. The Coordinator Agent receives the review request. 3. The Security Agent analyzes potential vulnerabilities. 4. The Testing Agent evaluates test coverage and identifies missing cases. 5. The Architecture Agent reviews design and implementation decisions. 6. Each specialist returns findings through A2A. 7. The Coordinator combines the results into a structured review report. 8. A developer reviews the recommendations before any changes are merged. Each agent can use its own MCP servers to access GitLab, documentation, test tools, or other required services. This allows us to evaluate the actual value of agent specialization and interoperability. 6. Measure the results A successful pilot should be evaluated using real measurements. Some useful metrics: - Task completion rate - Agent-to-agent communication latency - Overall workflow duration - Failed or timed-out tasks - Duplicate execution rate - Token and infrastructure costs - Human intervention rate - Quality of the final aggregated result The objective is to determine whether distributing work across specialized agents produces better results than a single-agent implementation. 7. Create reusable learning resources A practical reference implementation, architecture documentation, and internal workshops can help developers understand when A2A is useful. Teams should learn not only how to implement the protocol, but also when not to use it. Not every internal function needs to become a remote agent. For simple operations within the same runtime, a regular function or framework-native tool call may be more appropriate. 12. When Should We Use A2A? A2A is most useful when communication between independent agents is an actual architectural requirement. | Scenario | Recommendation | |---|---| | Agents built with different frameworks need to collaborate | A2A | | Different teams expose independently maintained agents | A2A | | Remote, long-running agent tasks need tracking | A2A | | Agents need to exchange structured outputs and files | A2A | | An agent needs access to APIs and databases | MCP | | A single agent needs internal tool execution | Native tools or MCP | | Multiple agents operate within one tightly coupled workflow | Framework-native orchestration may be sufficient | The important thing is to avoid introducing network boundaries and protocol complexity when a simpler implementation would work. A2A should solve an interoperability requirement, not become an additional abstraction by default. Conclusion A2A addresses an important challenge in the evolution of agentic AI systems: enabling independently developed agents to communicate and collaborate. As AI applications become more specialized, a single agent will not necessarily be responsible for every task. Different agents may own different capabilities, use different frameworks, and operate in different environments. A2A provides a standardized way for these systems to discover each other, delegate tasks, exchange information, and track execution. Combined with MCP, it creates a useful architectural separation: MCP connects agents to tools and external context, while A2A connects agents to other agents. For an AI Chapter Lead, the opportunity is to establish common integration standards, build an internal agent ecosystem, define security boundaries, and help teams develop reusable capabilities. My recommendation: Start with one practical multi-agent workflow, implement it using the official A2A SDK, and evaluate the operational benefits against a conventional single-agent or framework-native orchestration approach. The real question is not how many agents we can connect. It is whether connecting specialized agents makes the system more reliable, maintainable, and effective. --- References and Further Reading 1. A2A Protocol — Official Website — Overview, architecture, ecosystem, and documentation. 2. A2A Protocol — Specification v1.0 — Protocol operations, data models, task lifecycle, transports, and security. 3. A2A — Agent Discovery — Agent Cards, discovery mechanisms, and capability descriptions. 4. A2A — Life of a Task — Task lifecycle and interaction patterns. 5. A2A — Streaming and Asynchronous Operations — Streaming, task updates, and push notifications. 6. A2A — Enterprise Implementation — Authentication, authorization, observability, and enterprise security. 7. A2A — Official JavaScript SDK — TypeScript client/server implementation, transport adapters, and runnable examples. 8. A2A — Official GitHub Organization — SDKs, samples, and project development. 9. AI SDK — A2A Community Provider — Integrating A2A agents with Vercel AI SDK. 10. A2A AI Provider — Community provider implementation and current maturity status. 11. Model Context Protocol — Complementary agent-to-tool integration protocol.
One extra field turned an A2A error into a TypeError. Fixed in a2a-sdk 1.3.0 --- title: One extra field turned an A2A error into a TypeError. Fixed in a2a-sdk 1.3.0 published: true description: I fed the A2A Python client seven bad agent replies. A valid error code came back as a TypeError. The fix shipped in a2a-sdk 1.3.0. tags: python, ai, agents, opensource cover_image: raw.githubusercontent.com --- <!-- Images are pushed to Sonica-B/Sonica-B under blog/, so the URLs below resolve. The interactive version of figure 1 is visibility/anatomy/a2a-sorter/a2a-sorter.html. --> On October 27, 2026, SAP announced general availability of inbound Agent2Agent (A2A) interoperability, so third-party agents can now invoke SAP Agents through its Agent Gateway. Every launch like this means more A2A clients calling agents their authors do not control. Two days before SAP's announcement, I had opened a pull request for that exact situation in the A2A Python SDK. An agent could send a valid A2A error, and the calling code would receive a TypeError. My fix shipped the same day as SAP's news, in a2a-sdk 1.3.0. A2A is an open protocol that lets AI agents discover each other and exchange messages and tasks over JSON-RPC, REST or gRPC. It joined the Agentic AI Foundation in August 2026, and a2a-sdk is its official Python SDK. The short version When an agent sent back an error, the SDK handed the reply to a third-party library before looking at it itself. That library is strict about which fields an error may carry, so one extra field, the kind the A2A spec says to ignore, made it throw a raw TypeError. The SDK's own translation of error codes into named exceptions never got a turn. Version 1.3.0 reads the reply itself first, at one place, and the library only builds requests now. The concept, animated !Animated figure. The reply's path through JsonRpcTransport as labelled blocks: agent reply, the json-rpc package's JSONRPC20Response(**body), the SDK's _create_jsonrpc_error(), your code with its except TaskNotFoundError branch, and three bins for TaskNotFoundError, A2AClientError and result. A token labelled {"code": -32001, "reason": "x"} drops into a raw TypeError tray at the json-rpc block. In 1.3.0 that block lifts out, _check_jsonrpc_response() takes its place, and the token reaches the TaskNotFoundError bin; a body that is a list reaches A2AClientError. Figure 1. The blocks are the real call chain in client/transports/jsonrpc.py, in order. The token is the reply body. The three states: an error object with one extra field before 1.3.0, the same reply in 1.3.0, and a body that is not an object in 1.3.0. A client talks to agents it does not control, so odd replies are normal. The SDK's promise about them is clear: a failure reaches your code either as A2AClientError or as a typed A2A error such as TaskNotFoundError. The A2A spec (section 5.7) adds that implementations should ignore fields they do not recognise. Now follow the token in the first state. It carries the error code -32001, which means TaskNotFoundError, plus one extra field, reason, that the spec says to ignore. The first block it reaches is JSONRPC20Response(**body) from the json-rpc package. It validates the envelope before the SDK sees anything, and it builds the error with a constructor that accepts only code, message and data. Any other member is, to that library, a programming mistake. So the token drops into the tray labelled raw TypeError: JSONRPCError.__init__() got an unexpected keyword argument 'reason'. The SDK's _create_jsonrpc_error() block never receives it, and your except TaskNotFoundError: branch never runs. The library was correct by its own rules. It simply ran first, so its rules won and the SDK's contract lost. How I found seven of these I was exploring the client with one question: what reaches my code when an agent answers wrong? No issue existed, so I played the misbehaving agent myself, wrote seven bad response bodies by hand, and fed them to the client through a mock transport. All seven came back as raw Python exceptions. | What the agent sent | Before | After | |---|---|---| | a list, or a string (say, from a proxy) | TypeError | A2AClientError | | neither result nor error | ValueError | A2AClientError | | "error": "boom" | TypeError | A2AClientError | | error object with an extra member | TypeError | TaskNotFoundError | | error object without message | ValueError | TaskNotFoundError | | SSE data: that is not JSON | JSONDecodeError | A2AClientError | | event: error with an extra member | TypeError | TaskNotFoundError | Following one reply through the code explained the whole table. Nine request methods handed the body straight to the json-rpc validator, and streaming used the same validator on each event. The SDK's own mapping from error codes to exceptions even had a fallback for a missing message, but it could never execute, because the library failed first. Streaming had a second twist: the JSON parse for each event ran in a place where its exception escaped the handler that was supposed to catch it, so a broken data: line leaked a JSONDecodeError. The fix, in one idea !Still from the third state: the json-rpc block is lifted out and faded, the SDK's _check_jsonrpc_response() block stands in its place, and a token labelled "a list, not an object" sits in the A2AClientError bin. Figure 2. In 1.3.0 the json-rpc validator is out of the chain. The SDK reads the envelope itself at one choke point, so an error object becomes the typed A2A error whatever extra fields it carries, and a body that is not even an object becomes A2AClientError. Check the wire envelope yourself, once, and hand libraries only what they can handle. Every reply already passes through one sending function or the streaming loop, so I put one check there. It asks three plain questions in order: is the body an object; if it has an error, is that an object, and if so, map it to the A2A exception by its code; otherwise, is there a result. Anything that fails those questions becomes A2AClientError with a message that says what was wrong. The streaming loop turns a JSONDecodeError into A2AClientError the same way. A falsy error next to a result still counts as success, as before. The client no longer builds JSONRPC20Response objects at all; requests still use JSONRPC20Request. Wrapping each of the nine calls in its own try/except would have turned TaskNotFoundError into a generic error and copied the same code nine times. One choke point keeps the typed exceptions. The tests live in a new class, TestMalformedJsonRpcResponses, with 11 cases. Ten fail on main with the exceptions in the table and pass with the change. The eleventh guards that delete_task_push_notification_config still raises on an error response. The existing tests pass unchanged. I ran everything on Windows 11 with Python 3.10.19: lint.sh passed, the transport tests gave 51 passed, and the wider run gave 2213 passed at 93% total coverage. Three failures and four setup errors fail the same way on main, and two integration suites do not run on Windows, so I left those to Linux CI. All 17 checks passed there, and the coverage bot showed client/transports/jsonrpc.py going from 92.66% to 95.51%. What changed for people using a2a-sdk If your client calls agents you do not control, version 1.3.0 behaves differently in three ways: - An agent that adds its own field to an error object, or leaves out message, now raises the mapped A2A error, so your except TaskNotFoundError: branch runs. - A malformed body, such as a list, a string, a missing result or a non-object error, raises A2AClientError, so one except clause covers all of them. - A stream with a broken data: line raises A2AClientError where it used to leak a JSONDecodeError. One behaviour change is worth knowing. A body with "error": null and no result used to parse as an empty object and now raises A2AClientError. That body breaks JSON-RPC 2.0 anyway. Upgrading is pip install -U a2a-sdk. The v0.3 compatibility transport still uses the same validator-first pattern on main. I kept this PR to the 1.0 client and offered a fix(compat) follow-up in the PR body. A thank-you to Iwaniukooo11 and the A2A maintainers I opened PR #1375 on October 25 with no linked issue, just the table and the tests. On October 26, Mateusz Iwaniuk (@Iwaniukooo11) brought the branch up to date with main, approved it and merged it. Thank you. Getting a first PR to a repo reviewed and merged the day after it opens is a generous welcome. Thanks also to the A2A maintainers for keeping the door open to outside contributors. Of the 40 merges before mine, 29 came from non-members, with a median of 3.4 days from open to merge. Release 1.3.0 carries fixes from at least three outside contributors, mine among them. What I took away Where a third-party validator sits in your call chain decides which exceptions your callers see. The stack trace ended inside json-rpc, but the decision that mattered was where the SDK called it. When you own an error contract, check the wire envelope yourself at one choke point and hand libraries only what they can handle. As more vendors open their agents to outside callers, the exception a client can catch by name is part of the interface, and that boundary is the kind of problem I like to work on. I'm Shreya Boyane, an ML engineer who builds agent systems, and I'm open to ML and AI engineering roles. You can find my work on GitHub, more about me at shreyaboyane.dev, and I'm happy to connect on LinkedIn. <!-- optional disclosure she may keep: An AI coding agent helped me probe the package and draft this fix; I reviewed and verified the change before submitting. -->
@AgenticAIFdn If you want to try it, start with the first lesson in the cookbook, examples/01-exec-demo: a2aproject/a2a-cli
MCP vs A2A vs AG-UI: The Protocol Stack, Fact-Checked > Originally published on DevToolHub. MCP vs A2A vs AG-UI is the comparison chart making the rounds right now, and the framing is right — these are three different, complementary layers, not competing standards. But a couple of its headline numbers are either stale or don't hold up against the protocols' own sources, and it skips the actual news: MCP's July 2026 spec update is a real breaking-change event, not a one-word "stateless" bullet point. Here's what each protocol actually does, fact-checked against their own docs and GitHub repos, plus what the chart got right and what's worth correcting. For the agent concepts underneath all three, see our AI agents explained guide. The Three Protocols, What Each One Actually Connects The "vs" in every comparison title is misleading — you don't pick one. Each protocol solves a different connection problem, and a real agentic system typically uses all three at once: - MCP (Model Context Protocol) — built by Anthropic — connects an agent to tools and data. Anthropic's own docs describe it as a standardized way to connect AI applications to external systems, "like a USB-C port for AI applications." If your agent needs to read a file, query a database, or call an API, this is the layer — see our MCP registry breakdown for how big that ecosystem already is. - A2A (Agent2Agent Protocol) — built by Google, now governed by the Linux Foundation — connects one agent to another. It's how two independent agents, possibly built by different teams on different frameworks, discover each other and hand off work. - AG-UI (Agent-User Interaction Protocol) — built by CopilotKit and its open-source community, not Google (a detail the viral chart gets right, worth double-checking since AG-UI's name invites confusion with Google's A2A) — connects an agent to the human watching it work. It's an event-based protocol for streaming an agent's state and output into a real UI, not a chat log. None of these three compete for the same job. A production agent that reads your CRM (MCP), delegates a sub-task to a specialist agent (A2A), and shows a human the live progress bar while it works (AG-UI) is using all three simultaneously. MCP's July 2026 Spec Update Was Bigger Than "Stateless" The chart's MCP row says "Stateless spec, July 2026" and leaves it there. That's the right headline, but it's doing a lot of work in one line — per MCP's own 2026-07-28 changelog, this is the largest rewrite since launch: protocol-level sessions and the Mcp-Session-Id header are gone, the initialize handshake is gone, and Roots, Sampling, and Logging are now formally deprecated with a twelve-month removal window. We already covered the full breaking-change list and a real server-migration checklist in MCP Server Authentication: What the 2026-07-28 Spec Broke — if you're migrating an MCP server, that's the piece to work from. What's actually new to check, and what the chart doesn't carry at all: per Anthropic's own announcement, MCP is at 400M monthly SDK downloads as of that July 28 update — a real, sourced number that's meaningfully below the chart's "close to 500M" claim, and the real reason for the stateless move is architectural: "MCP moves from a bidirectional stateful protocol to a request/response model. Servers can now deploy on serverless and edge infrastructure" — something the old session-pinned design couldn't do cleanly. A2A's Real Status — v1.0, Signed Agent Cards, and a Number Worth Correcting The chart says A2A hit "v1.0 with signed Agent Cards, March 2026." The version and date both check out — A2A's GitHub releases show v1.0.0 published March 12, 2026, and the live spec's section 8.4 ("Agent Card Signing") does define canonicalization, signature format, and signature verification requirements so agents can cryptographically sign their Agent Cards for authenticity. Worth knowing if you're building against it today: v1.0 isn't the current patch anymore — v1.0.1 shipped May 28, 2026 with HTTP binding and TaskStatus fixes. Governance checks out too: Google originally built A2A and donated it to the Linux Foundation, and as of August 20, 2026 it formally joined the Agentic AI Foundation (AAIF) — the same body that already hosts MCP — putting both protocols under one neutral governance structure instead of two separate vendor-led ones. A2A's Technical Steering Committee includes representatives from AWS, Cisco, Google, IBM Research, Microsoft, Salesforce, SAP, and ServiceNow. The one number worth double-checking: the chart lists A2A at "150+ organizations behind it." I couldn't find a primary source attributing that specific figure to A2A alone. What I could verify, directly from the Linux Foundation's own press release, is AAIF's total membership — the body that now governs both A2A and MCP — which states it "grows to 247 member organizations with the addition of 57 new members over the past three months." That's a materially different, larger number than "150+," just measuring the whole foundation rather than one protocol's backers specifically. If the chart's figure is from an earlier AAIF member count, it's stale now. AG-UI's Real Adoption — 16.7k Stars and a Fourth Integration the Chart Skips AG-UI's chart row says "15K+ GitHub stars." Checking the ag-ui-protocol/ag-ui repo directly, it's actually at 16.7k stars — the chart's number is accurate but already a bit behind. More interesting: the chart lists three 1st-party framework integrations (Google ADK, Microsoft Agent Framework, AWS AgentCore). The repo documents a fourth — AWS Strands Agents is also officially supported, alongside AWS Bedrock AgentCore, not instead of it. If you're picking an agent framework specifically because it has AG-UI support baked in, Strands is a real option the chart doesn't mention. Where ACP Went The chart's footer note — "Where's ACP? IBM merged it into A2A in August 2025" — checks out exactly. IBM Research's Agent Communication Protocol, built to power its BeeAI platform, overlapped heavily with A2A (both covered agent-to-agent discovery and messaging). The original ACP repository itself confirms it: archived August 27, 2025, with a banner reading "ACP is now part of A2A under the Linux Foundation!" and a link to the migration guide. If you see ACP referenced in older material, it's a dead end now — the real destination is A2A. Do You Need All Three? A Practical Decision Guide - Your agent needs to read or write something external — a CRM, your docs, a GitHub repo, a database: that's MCP. One server, any MCP-compatible client can use it — our best MCP servers by category guide is a starting point. - Your agent needs to hand off a task to a different, independently-built agent — possibly on a different framework, possibly a different vendor: that's A2A. The Agent Card is how the other side learns what your agent can do before the handoff. - A person needs to watch your agent work and approve or steer its steps in real time, not just read a final answer: that's AG-UI. It's the layer between the agent's internal state and whatever UI framework renders it. - You're building a single-tool integration with no multi-agent handoff and no live UI — you may genuinely only need MCP. Don't add A2A or AG-UI to a system that doesn't have another agent or a human-in-the-loop UI to connect to. Frequently Asked Questions Q: Are MCP, A2A, and AG-UI competing standards? A: No. They connect different things — MCP connects an agent to tools and data, A2A connects one agent to another, AG-UI connects an agent to the person watching it. A single system can use all three at once. Q: What actually changed when MCP went stateless in July 2026? A: Sessions, the initialize handshake, and three features (Roots, Sampling, Logging) all changed at once — see our full MCP server authentication breaking-change checklist for the complete list. The number worth knowing that isn't in that piece: Anthropic's own blog puts MCP at 400M monthly SDK downloads as of this update. Q: Who governs MCP and A2A now? A: Both are hosted projects under the Agentic AI Foundation (AAIF), part of the Linux Foundation. A2A joined AAIF on August 20, 2026, after Google donated it; MCP was already a founding AAIF project. AAIF's own press release puts total membership at 247 organizations. Q: Does AG-UI only work with Google, Microsoft, and AWS AgentCore? A: Those three are 1st-party supported, but so is AWS Strands Agents — a fourth framework with official AG-UI support that's easy to miss if you're only going by popular comparison charts. Q: What happened to ACP (IBM's Agent Communication Protocol)? A: It merged into A2A. The original repository was archived August 27, 2025, with a notice directing developers to A2A's migration guide. Quick Summary: - MCP, A2A, and AG-UI are complementary layers (tools, agent-to-agent, agent-to-human), not competing alternatives - MCP's July 28, 2026 spec update (sessions, the initialize handshake, Roots/Sampling/Logging all changed) is covered in full in our MCP server authentication piece - Per Anthropic's own blog, MCP is at 400M monthly SDK downloads as of that update — not "close to 500M" - A2A hit v1.0.0 in March 2026 (now on patch v1.0.1) with real signed Agent Cards; AAIF's own total membership is 247 organizations, a different number than "150+" - AG-UI is at 16.7k GitHub stars with four 1st-party framework integrations (Google ADK, Microsoft Agent Framework, AWS Bedrock AgentCore, and AWS Strands Agents) - ACP (IBM) merged into A2A in August 2025 and is a dead end if you see it referenced The real answer to MCP vs A2A vs AG-UI is: pick protocols by the connection you actually need, not by which chart you saw first — and if you're touching an MCP integration built before July 2026, check it against the real breaking-change list above before assuming "stateless" just means a minor version bump.
@stevekrouse haven't used it but this is a thing a2aproject/a2a
Controlling Access to A2A Agents with agentgateway At Qodo, we've publicly described how PR-Agent became an A2A agent for MOSAICO, a research project coordinating software-engineering agents. Other agents can delegate code reviews and receive results. When you expose an agent this way, how do you control who can call it and verify that rejected requests never reach it? This walkthrough puts a sample agent behind agentgateway, checks caller credentials, and records which requests reach the backend! What does A2A make possible for a code-review agent? Agent2Agent, or A2A, defines how independently deployed agents exchange messages, tasks, and results. An agent card advertises an agent's capabilities and connection details. A client can discover a reviewer, send work, and receive its response without sharing the reviewer's implementation. Qodo's public PR-Agent integration illustrates that boundary. MOSAICO can delegate a review to PR-Agent and receive review output. That establishes a useful interaction, but operating an endpoint also requires decisions about callers, credentials, and the resources each request may access. For the access tests, a deterministic Hello World agent is enough. It returns a greeting instead of reviewing code. That keeps model behavior out of the experiment and makes backend execution easy to observe. Where does agentgateway sit in the request path? Agentgateway's A2A example places a proxy between the client and the agent. The tested setup uses agentgateway 1.6.0, Python 3.13.4, A2A Python SDK 1.1.0, and the A2A 1.0 JSON-RPC binding. Its executor comes from this pinned upstream sample. Requests go to the gateway on port 18760; the sample backend listens on 127.0.0.1:18761. Request logging and an executor-entry record let the test compare what the gateway returned with what the agent actually received and ran. !Client requests pass through JWT validation and a reviewer-role check in agentgateway. Invalid credentials return 401; rejected roles return 403; accepted requests reach the sample agent. The tested request path. The backend remains reachable directly from the local machine; the diagram doesn't imply network isolation. Fetching the agent card through the gateway returned a supportedInterfaces URL pointing back to port 18760. That helps clients continue through the proxy. It doesn't prevent someone who knows a reachable backend address from calling it directly. How do you define what can reach the agent? The route combines JWT authentication with an authorization rule. A JSON Web Token carries signed claims about its issuer, intended audience, and caller. The gateway checks the signature against a JSON Web Key Set, or JWKS, containing the issuer's public key. Here's the route portion of the tested configuration: binds: - port: 18760 listeners: - routes: - policies: a2a: {} jwtAuth: mode: strict issuer: https://issuer.example.test audiences: [a2a-demo] jwks: file: ./jwks.json authorization: rules: - require: 'jwt.role == "reviewer"' backends: - host: 127.0.0.1:18761 The issuer here is a synthetic test identity, not a service to sign into. The runner creates an RSA key and short-lived tokens locally. In this release, the JWKS file path resolves from the process's working directory. strict requires a valid token. The issuer and audience constrain which tokens qualify; a separate rule requires the signed role claim to equal reviewer. That role is this demo's convention, not an A2A-defined permission. Keep those checks explicit. Agentgateway's default optional authentication mode permits requests without a token. A configuration that validates credentials when present can behave differently from one that requires credentials on every request. How do you prove a policy rejects the right requests? Each test sends a real SendMessage request with a unique message ID. The same identifier connects the request to the backend's records. Accepted cases must return the greeting without a JSON-RPC error; an HTTP 200 alone doesn't establish successful A2A execution. These were the observed outcomes with the configuration above: | Caller credentials | HTTP response | Backend requests / executor calls | |---|---:|---:| | Valid token, reviewer role | 200 | 1 / 1 | | No token | 401 | 0 / 0 | | Wrong audience | 401 | 0 / 0 | | Wrong role | 403 | 0 / 0 | | Missing role claim | 403 | 0 / 0 | | Invalid signature or expired token, tested separately | 401 each | 0 / 0 each | A valid request after the rejection cases also completed. Removing only the authorization rule let a valid token with the wrong role reach the executor. That control checks whether the role rule actually explains the rejection. One configuration change exposed a useful failure case. Replacing the requirement with this rule changed the result for a valid token missing role: authorization: rules: - deny: 'jwt.role != "reviewer"' That request returned 200 and executed once. With require, it returned 403 and never reached the backend. The documented evaluation rules explain why: accessing a missing claim makes the expression fail. A failed requirement rejects the request. A failed deny expression doesn't match; with this single deny rule and no allow rules, the request proceeds. JWT authentication stayed strict in both configurations. A missing token still returned 401. The finding concerns a missing application claim in an otherwise valid token, under these exact policies. What permissions belong inside the agent? The gateway established which tested requests could reach this endpoint. It didn't decide whether a caller may read a particular repository, retrieve a private diff, or write a change. A real reviewer needs those checks where it accesses the repository and invokes its tools. The bypass control matters too: calling this local backend directly without a token returned 200 and executed once. A deployment needs to restrict direct backend access or enforce equivalent checks there. Rewriting the agent card isn't a network access control. This reproduction used synthetic credentials, HTTP on a development machine, and a greeting executor. It didn't validate production TLS, identity-provider integration, streaming recovery, or repository permissions. The gateway's standalone listener binds a wildcard address; the backend and administrative endpoints were configured on loopback. For your own agent, start with an allowed request and specific rejected requests. Check both the response and backend execution. Then remove or change the relevant policy in an isolated test to verify which check caused the rejection. That gives you evidence for the boundary you've configured—and a clear account of what still needs protection.
Week 42: New A2A partners and Community Integrations Hi All, sharing a few A2A Updates. This week we have made some progress in our A2A v1.1 protocol and also added a new list of partners. Partners Added @ https://a2a-protocol.org/latest/partners/ - Agent Commons: A free, public home for independent AI agents offering directory profiles, restart-proof inboxes, and shared knowledge bases, accessible via an open JSON-RPC message/send endpoint. - DEUSPROOF: A public, immutable notary service that acts as a certificate of birth for AI agents, providing ownership verification, Ed25519 signing, and cryptographic time-stamping. - Neuronto: A registry and endpoint provider that serves a conformant A2A agent card and endpoint for agent discovery and communication. Community Integrations Added @ https://a2a-protocol.org/latest/community/ - Quilzo: A community-integrated framework featuring built-in A2A 1.0 support for agent cards, governance extensions, and secure JSON-RPC task execution. - CUGA Integration: Added CUGA (IBM Research's open-source agent harness) to A2A community integrations, allowing CUGA agents to be served over A2A and delegate tasks to remote agents.
The example has been replaced (PR #2319 approved), and the Python SDK will warn on the affected cards. Aligning the bytes waits for the next major release. I did not argue it. I measured it. A2A#2122 Not a claim about the work. The work.