a2aproject

Activity

Stars and mentions · Last 30 days, Oct 7 – Nov 5

GitHub stars51
Mentions11
02468Oct 7Oct 21Nov 5
UTC days · repositories a2aproject owns3 repositories

Top repositories

a2aproject’s repositories ranked by new forks, as on trending.

Loading

Contributes to

Repositories outside a2aproject’s account where a2aproject opened or reviewed pull requests in the past 365 days.

Who noticed a2aproject

Everyone who starred or forked a2aproject’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

11posts
Nov 61 post
  1. The A2A CLI on Azure: One Client for Container Apps, Agent Framework and Localhost --- title: "The A2A CLI on Azure: One Client for Container Apps, Agent Framework and Localhost" published: true description: "A Rust A2A v1.0 agent and a Microsoft Agent Framework A2A v1.0 coordinator on Azure Container Apps, behind Entra ID, driven by the same A2A CLI commands as their localhost copies." tags: a2a, azure, rust, agents cover_image: raw.githubusercontent.com --- This article provides a step by step guide to running A2A agents on Azure and driving them with the A2A CLI. A Rust agent on the official A2A Rust SDK runs in Azure Container Apps. A Microsoft Agent Framework agent on an Azure OpenAI model runs in Container Apps behind Entra ID and hands its work to the Rust agent. Both also run on a laptop, and one set of a2a commands checks all four. github.com/xbill9/rust-dev/tree/main/a2a-agentfra… The A2A CLI drove all four endpoints with the same commands, and 19 of 19 checks passed. Every hop speaks A2A 1.0: the Rust SDK serves it, Agent Framework serves it, and the Agent Framework agent calls the Rust agent with the A2A 1.0 Python client. On Azure the CLI needs one extra flag, an Entra ID bearer token, which az login already provides. Model access, registry pulls and the token all run on Entra ID, with no API key anywhere. --- What Is Being Built? | Agent | Where it runs | A2A | Model | | :--- | :--- | :--- | :--- | | mersenne-agent (Rust) | laptop, port 8304 | 1.0, JSON-RPC and REST | none | | mersenne-agent (same image) | Container Apps, 0.25 vCPU, 0.5 GiB | 1.0, JSON-RPC and REST | none | | Agent Framework coordinator | laptop, port 8309 | 1.0, JSON-RPC and REST | gpt-5.4-mini on Azure OpenAI | | Agent Framework coordinator (same code) | Container Apps, Entra ID auth | 1.0, JSON-RPC and REST | gpt-5.4-mini on Azure OpenAI | a2a CLI ──► Container Apps auth (Entra ID token) ──► Agent Framework Agent ──Entra ID──► Azure OpenAI gpt-5.4-mini └── find_mersenne_primes tool └── a2a-sdk 1.x client ──► Container Apps (A2A 1.0) ──► mersenne-agent a2a CLI ────────────────────────────────────────────────────────────────────────────────► Container Apps (A2A 1.0) The workload is a Lucas-Lehmer test over the first N known Mersenne prime exponents, timed by the agent itself. Every reply carries the line Found first N Mersenne primes in X ms. and a data part with requested, found and elapsed_ms. Versions, read from the machine on 2026-11-06: | Component | Version | | :--- | :--- | | A2A CLI (a2a) | v0.3.1 | | a2a-lf / a2a-server-lf (Rust SDK, A2A 1.0) | 0.4.2 / 0.5.3 | | agent-framework-core / agent-framework-openai / agent-framework-a2a | 1.24.0 / 1.18.0 / 1.0.0b261030 | | a2a-sdk (Python, A2A 1.0) / azure-identity | 1.3.0 / 1.27.0 | | Python / Rust | 3.14.7 / 1.99.1 | | Azure CLI | 2.90.0 | | Model | gpt-5.4-mini 2026-03-17, GlobalStandard | | Region | eastus2 | --- At This Point You Should Have… - The A2A CLI on your PATH as a2a: go install github.com/a2aproject/a2a-cli@latest, then rename a2a-cli to a2a - Rust, Docker and Python 3 - The Azure CLI with the containerapp extension, signed in with az login to a subscription where you can create resource groups and Entra ID app registrations a2a version a2a v0.3.1 --- Step 1 — The Model and the Shared Resources git clone https://github.com/xbill9/rust-dev && cd rust-dev/a2a-agentframework-azure scripts/deploy-model.py The script creates one resource group in eastus2 holding a container registry, a user-assigned managed identity, a Container Apps environment and an AIServices account with a gpt-5.4-mini deployment. It grants "Cognitive Services OpenAI User" on the account twice: to the signed-in user, for the laptop, and to the managed identity, for the containers. $ az group create -n a2a-agentframework-azure-rg -l eastus2 $ az containerapp env create -n a2a-af-env -g a2a-agentframework-azure-rg -l eastus2 --logs-destination none $ az cognitiveservices account create -n a2a-af-openai-defaba -g a2a-agentframework-azure-rg -l eastus2 --kind AIServices --sku S0 --custom-domain a2a-af-openai-defaba --yes $ az cognitiveservices account deployment create ... --model-name gpt-5.4-mini --model-version 2026-03-17 --model-format OpenAI --sku-name GlobalStandard --sku-capacity 50 ✅ https://a2a-af-openai-defaba.cognitiveservices.azure.com/ deployment gpt-5.4-mini The account gets a custom subdomain, and every call to the model authenticates with Entra ID. The coordinator holds no key. --- Step 2 — Run Both Agents on the Laptop scripts/start-local.sh up: 8304 rust agent, 8309 agent framework coordinator -> http://localhost:8304 The coordinator is an Agent Framework Agent with one tool, served over A2A 1.0 by agent_framework_a2a on the a2a-sdk server routes. The same file runs on the laptop and in Container Apps: client = OpenAIChatClient( model=MODEL, azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], credential=DefaultAzureCredential(), ) agent = Agent(client=client, name=NAME, description=DESCRIPTION, instructions=INSTRUCTIONS, tools=[find_mersenne_primes]) handler = DefaultRequestHandler( agent_executor=WholeTextA2AExecutor(agent, stream=True), task_store=InMemoryTaskStore(), agent_card=card, ) return Starlette(routes=[ *create_agent_card_routes(card), *create_rest_routes(handler, path_prefix="/rest"), *create_jsonrpc_routes(handler, "/"), ]) DefaultAzureCredential picks up az login on the laptop and the managed identity in Azure. The CLI reads both cards and shows each one's protocol version: a2a card get -a http://localhost:8304 a2a card get -a http://localhost:8309 Name: Mersenne Prime Agent Rust Version: 0.1.0 Interfaces: JSONRPC (A2A v1.0) http://localhost:8304/jsonrpc HTTP+JSON (A2A v1.0) http://localhost:8304/rest Name: Agent Framework Mersenne Coordinator Version: 0.1.0 Interfaces: JSONRPC (A2A v1.0) http://localhost:8309/ HTTP+JSON (A2A v1.0) http://localhost:8309/rest --- Step 3 — The Tool Calls the Rust Agent With the A2A 1.0 Python Client Agent Framework's A2A package is built on a2a-sdk 1.x, the same protocol version as the Rust agent. The tool sends a data part and reads the Rust agent's structured report straight from the reply: value = Value() value.struct_value.update({"count": count}) request = SendMessageRequest(message=Message( role=Role.ROLE_USER, message_id=os.urandom(16).hex(), parts=[Part(data=value)], )) async with httpx.AsyncClient(timeout=60) as http: client = await create_client( RUST_AGENT_URL, ClientConfig(streaming=False, httpx_client=http)) async for event in client.send_message(request): task = event.task if event.HasField("task") else None for artifact in (task.artifacts if task else []): for part in artifact.parts: if part.HasField("data"): report = MessageToDict(part.data) elif part.HasField("text"): summary = part.text The tool refuses a reply whose requested or found differs from the count the model asked for, and the instructions tell the model to quote the agent's own reply line. The model picks N; the Rust agent does the counting and the timing. Data parts cross a protobuf Struct, so integers arrive as floats and the tool reads them with int(). --- 🔎 Tip: Two A2A SDK Versions on One Python agent-framework-a2a requires a2a-sdk 1.x, and the same machine runs a Strands rig whose a2a-sdk is pinned to 0.3: # agent-framework-a2a wheel metadata (Requires-Dist) Requires-Dist: agent-framework-core>=1.22.0,<2 Requires-Dist: a2a-sdk>=1.0.0,<2 # shared pyenv Python (used by the Strands rig) a2a-sdk 0.3.26 scripts/start-local.sh installs the coordinator's pinned requirements into build/local-site with pip install --target, and starts the coordinator with PYTHONPATH=build/local-site. The two SDK versions never meet, and the container image installs the same pins. --- Step 4 — The Rust Agent on Container Apps scripts/deploy-rust.py builds docker/agent.Dockerfile, pushes it to the registry and creates a container app with external ingress. The app pulls with the managed identity, which holds AcrPull, so there is no registry password. The script computes the app's address from the environment's default domain before creating it and sets ADVERTISE_URL to it, so the card points at Container Apps from the first revision. scripts/deploy-rust.py $ docker build --platform linux/amd64 -f docker/agent.Dockerfile -t a2aafazuredefaba.azurecr.io/mersenne-agent:20261106T153734Z /home/xbill/rust-dev/a2a-agentframework-azure $ docker push a2aafazuredefaba.azurecr.io/mersenne-agent:20261106T153734Z $ az containerapp create -n a2a-rust-mersenne ... --target-port 8080 --ingress external --registry-identity <identity> --cpu 0.25 --memory 0.5Gi --min-replicas 1 --max-replicas 1 --env-vars PORT=8080 HOST=0.0.0.0 ADVERTISE_URL=https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io ✅ https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io/.well-known/agent-card.json The commands are the laptop's, with the Container Apps address: scripts/test-cli.sh aca-rust == aca-rust rust agent (https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io) Interfaces: JSONRPC (A2A v1.0) https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io/jsonrpc HTTP+JSON (A2A v1.0) https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io/rest ✅ aca-rust card (A2A v1.0) ✅ aca-rust send ✅ aca-rust stream ✅ aca-rust rest ✅ aca-rust data-part --- Step 5 — The Coordinator on Container Apps Behind Entra ID scripts/deploy-coordinator.py does four things: - creates an Entra ID app registration with the identifier URI api://<app id>, one delegated scope access_as_user, version 2 access tokens, and the Azure CLI's client id pre-authorized on that scope - builds docker/coordinator.Dockerfile and pushes it - creates the container app with RUST_AGENT_URL set to the Container Apps Rust agent and AZURE_CLIENT_ID set to the managed identity - turns on Container Apps built-in authentication with the Microsoft provider, answering 401 to any request without a valid token scripts/deploy-coordinator.py $ az containerapp auth microsoft update -n a2a-af-coordinator -g a2a-agentframework-azure-rg --client-id 67ce79d7-7432-4261-b394-d32c2fc36890 --issuer https://login.microsoftonline.com/<tenant>/v2.0 --allowed-audiences api://67ce79d7-7432-4261-b394-d32c2fc36890,67ce79d7-7432-4261-b394-d32c2fc36890 --yes $ az containerapp auth update -n a2a-af-coordinator -g a2a-agentframework-azure-rg --enabled true --unauthenticated-client-action Return401 ✅ https://a2a-af-coordinator.braveground-531dbad2.eastus2.azurecontainerapps.io/.well-known/agent-card.json (401 without a token) From the CLI, the coordinator takes one flag beyond the laptop's: a bearer token for the app registration. URL=$(scripts/azure_state.py get coordinator_url) TOKEN=$(scripts/entra-token.py) # az account get-access-token --resource api://<app id> a2a --auth "Bearer $TOKEN" card get -a "$URL" a2a --auth "Bearer $TOKEN" send -a "$URL" --transport rest "Find the first 3 Mersenne primes" Name: Agent Framework Mersenne Coordinator Interfaces: JSONRPC (A2A v1.0) https://a2a-af-coordinator.braveground-531dbad2.eastus2.azurecontainerapps.io/ HTTP+JSON (A2A v1.0) https://a2a-af-coordinator.braveground-531dbad2.eastus2.azurecontainerapps.io/rest Status: completed (2026-11-06T16:20:22Z) Artifacts: [msg_0a2ef6a5893a0e6d006aedfe460ca48195931f337d1ca8db4e] Found first 3 Mersenne primes in 0.0030ms. 2, 3, 5 The CLI takes the base URL, the card sits at the standard /.well-known/agent-card.json path, and each call gets its own A2A context with no session header. Without a token, and with a malformed one, the card itself answers 401: $ curl -s -o /dev/null -w '%{http_code}' $URL/.well-known/agent-card.json 401 $ curl -s -o /dev/null -w '%{http_code}' -H 'Authorization: Bearer not-a-jwt' $URL/.well-known/agent-card.json 401 --- 🔎 Tip: Pre-Authorize the Azure CLI on the App Registration With the Azure CLI's client id listed under preAuthorizedApplications for the access_as_user scope, az account get-access-token --resource api://<app id> returns a token for the coordinator with no consent prompt and no client secret. The token's claims, payload only: { "aud": "67ce79d7-7432-4261-b394-d32c2fc36890", "iss": "https://login.microsoftonline.com/40482c55-d00d-4c6d-8903-643d76a74b9c/v2.0", "ver": "2.0", "scp": "access_as_user", "azp": "04b07795-8ddb-461a-bbee-02f9e1bf7b46" } azp is the Azure CLI. With requestedAccessTokenVersion: 2 the audience is the bare app id and the issuer ends in /v2.0, so the built-in authentication lists both api://<app id> and <app id> as allowed audiences and uses the v2 issuer. --- 🔎 Tip: Pin aiohttp for the Async Azure Credential azure.identity.aio needs aiohttp for managed identity, and nothing in the Agent Framework or a2a-sdk dependency tree installs it. The same requirements without the pin, in a clean python:3.14-slim-trixie container: aiohttp dist-info dirs installed: 0 ImportError: aiohttp package is not installed coordinator/requirements.txt pins aiohttp==3.14.5. A shared Python on a laptop that already has aiohttp from another package hides the gap, so the clean container is the place to check it. --- Step 6 — One Reply, One Artifact Agent Framework's OpenAI client streams a reply as text deltas, then sends one more update holding the whole text under the same message id. One streamed reply, every text update: 1 msg_02d7537ef107 'Found' 2 msg_02d7537ef107 ' first' ... 21 msg_02d7537ef107 ' 7' 22 msg_02d7537ef107 'Found first 4 Mersenne primes in 0.0026ms.\n2, 3, 5, 7' # last update repeats everything streamed before it under the same message_id: True A2AExecutor appends every update to one artifact, which leaves the reply in the task twice. WholeTextA2AExecutor lets the closing update replace the artifact, so streaming clients see the deltas and the finished task holds the reply once, as a single text part: class WholeTextA2AExecutor(A2AExecutor): async def handle_events(self, item, updater, streamed_artifact_ids=None, default_artifact_id=None): if isinstance(item, AgentResponseUpdate): key = item.message_id or default_artifact_id or "" text = "".join(c.text for c in item.contents if c.type == "text" and c.text) so_far = self._streamed.get(key, "") if text and so_far and text == so_far: del self._streamed[key] if streamed_artifact_ids is not None: streamed_artifact_ids.discard(key) # next add_artifact replaces elif text: self._streamed[key] = so_far + text await super().handle_events(item, updater, streamed_artifact_ids, default_artifact_id) a2a --auth "Bearer $TOKEN" send -a "$URL" -o json "Find the first 10 Mersenne primes" "artifacts": [ { "artifactId": "msg_0df9fa92840724f8006aedfe47b9208195b7c9c1698118b7c1", "parts": [ { "text": "Found first 10 Mersenne primes in 0.0549ms.\n2, 3, 5, 7, 13, 17, 19, 31, 61, 89" } ] } ] The coordinator checks in the next step require the reply line exactly once, and the timing script counts a call only when it appears once. --- Step 7 — The Same Checks on All Four Endpoints scripts/test-cli.sh all starts the laptop agents and runs one set of checks against each endpoint. Each Rust agent gets card (A2A v1.0), send, stream, REST and a data part. Each coordinator gets card (A2A v1.0), send, stream and REST, and the Container Apps coordinator also gets a request with no token, which must answer 401. scripts/test-cli.sh all == local rust agent (http://localhost:8304) ✅ local card (A2A v1.0) ✅ local send ✅ local stream ✅ local rest ✅ local data-part == local agent framework coordinator (http://localhost:8309) ✅ local card (A2A v1.0) ✅ local send ✅ local stream ✅ local rest == aca-rust rust agent (https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io) ✅ aca-rust card (A2A v1.0) ✅ aca-rust send ✅ aca-rust stream ✅ aca-rust rest ✅ aca-rust data-part -- without a token (expect 401) ✅ aca-coordinator rejects no token (401) == aca-coordinator agent framework coordinator (https://a2a-af-coordinator.braveground-531dbad2.eastus2.azurecontainerapps.io) ✅ aca-coordinator card (A2A v1.0) ✅ aca-coordinator send ✅ aca-coordinator stream ✅ aca-coordinator rest The Container Apps coordinator checks ask for 7, 11 and 3 primes. All three reached the Container Apps Rust agent, and the coordinator's log shows each one: INFO:coordinator:rust agent https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io: Found first 7 Mersenne primes in 0.0059ms. INFO:coordinator:rust agent https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io: Found first 11 Mersenne primes in 0.0339ms. INFO:coordinator:rust agent https://a2a-rust-mersenne.braveground-531dbad2.eastus2.azurecontainerapps.io: Found first 3 Mersenne primes in 0.0039ms. --- Step 8 — Timing the Same Command on Each Platform scripts/cli-bench.py starts one a2a process per call and times it, so wall time includes starting the CLI, fetching the card, TLS on Azure and the reply. Targets take turns, one call each per round, so drift during the run lands on every target alike. Every reply is checked, and the statistics come from the script (results/summary-20261106.txt). N=10 and 20 calls per target throughout. scripts/cli-bench.py --targets local-rust,aca-rust --reps 20 --n 10 local-rust ok 20/20 wall median 10.5 ms (min 7.9, max 12.3) compute median 0.02268 ms aca-rust ok 20/20 wall median 164.2 ms (min 143.4, max 199.9) compute median 0.02985 ms scripts/cli-bench.py --targets local-coordinator,aca-coordinator --reps 20 --n 10 local-coordinator ok 20/20 wall median 1836.0 ms (min 1374.5, max 4833.5) aca-coordinator ok 20/20 wall median 1891.2 ms (min 1581.2, max 4372.4) The Rust agent's own compute time is a fraction of a millisecond on both machines, so the 153.7 ms between the laptop and Container Apps medians is the network and TLS. A coordinator call is an Azure OpenAI call plus a nested A2A call to a Rust agent. The Container Apps coordinator's median sits 55.2 ms above the laptop's, and both ranges span more than a second: the model call sets the time, and where the coordinator runs moves it by less than the spread between calls. --- Step 9 — Teardown make teardown $ az ad app delete --id 67ce79d7-7432-4261-b394-d32c2fc36890 $ az group delete -n a2a-agentframework-azure-rg --yes $ az cognitiveservices account purge -n a2a-af-openai-defaba -g a2a-agentframework-azure-rg -l eastus2 deleted a2a-agentframework-azure-rg Everything except the app registration lives in one resource group, so one delete removes it. The app registration lives in Entra ID and is deleted first. The script then purges the AIServices account, so no soft-deleted copy is left behind. Afterwards: $ az group exists -n a2a-agentframework-azure-rg false $ az cognitiveservices account list-deleted --query "[].name" [] The Container Apps and model addresses shown in this article belong to that deployment and no longer resolve. make deploy recreates all three steps under new addresses, and make test-cli T=all checks them again. --- Compare and Contrast | Endpoint | CLI flags beyond the laptop's | A2A | Median wall time, N=10 | | :--- | :--- | :--- | ---: | | Rust, laptop 🥇 | none | 1.0 | 10.5 ms | | Rust, Container Apps 🥈 | none, HTTPS address | 1.0 | 164.2 ms | | Agent Framework, laptop | none | 1.0 | 1836.0 ms | | Agent Framework, Container Apps | --auth with an Entra ID token | 1.0 | 1891.2 ms | --- So, Which One? For an agent with no model in it, Container Apps is the short path: one container app with external ingress, a public HTTPS address and no client changes. Every call costs the network trip, 153.7 ms here, on top of a computation that takes a fraction of a millisecond. For an agent built around a model, the same Container Apps environment hosts it behind Entra ID with built-in authentication and no code in the agent. The CLI adds one flag, and az login supplies the token. The model call sets the response time, wherever the agent runs. For talking to any of them, the A2A CLI is the one client. With Agent Framework on A2A 1.0, the agents talk to each other with the Python SDK and the laptop talks to all of them with the CLI, over one protocol version end to end. --- Summary The goal of this article was to show that the A2A CLI drives the same agents on a laptop and on Azure Container Apps with the same commands. The key to the solution was A2A 1.0 on every hop, Entra ID for every credential, and one set of checks run against every endpoint. The results were: - 🟢 19 of 19 CLI checks passed across four endpoints: card, send, stream, REST and data parts on the Rust agent; card, send, stream, REST and a 401 without a token on the coordinator - 🟢 Agent Framework serves A2A 1.0 with JSON-RPC and REST, and its tool calls the Rust 1.0 agent with the a2a-sdk 1.x client - 🟢 The Container Apps coordinator's calls reached the Container Apps Rust agent, confirmed in its log - 🟢 No API keys and no registry password: Entra ID for the model, the image pull and the CLI's token - ⚠️ The coordinator needs WholeTextA2AExecutor, or each reply lands in the task twice - ⚠️ The async Azure credential needs aiohttp pinned; nothing else installs it - ⚠️ Container Apps added 153.7 ms per Rust agent call over the laptop; the coordinators' medians differ by 55.2 ms, inside a spread of more than a second Scope: one laptop (13th Gen Intel Core i7-1360P, Linux), one Azure region (eastus2), one replica of each app at 0.25 vCPU and 0.5 GiB, one model (gpt-5.4-mini), measured on 2026-11-06. Timing used N=10 with 20 calls per target, one run per comparison. Every Azure figure includes the trip from the laptop. Both apps kept one replica running, so cold starts were not measured; cost, gRPC, task commands, push notifications and load were not tested. The strategy for running A2A agents on Azure and driving them with the A2A CLI was validated with an incremental step by step approach. --- References - The rig: github.com/xbill9/rust-dev/tree/main/a2a-agentfra… - CLI check transcript: github.com/xbill9/rust-dev/blob/main/a2a-agentfra… - Timing summary: github.com/xbill9/rust-dev/blob/main/a2a-agentfra… - A2A CLI: a2aproject/a2a-cli - A2A Rust SDK: a2aproject/a2a-rs - A2A Python SDK: a2aproject/a2a-python - Microsoft Agent Framework: microsoft/agent-framework - Authentication and authorization in Azure Container Apps: learn.microsoft.com - Managed identities in Azure Container Apps: learn.microsoft.com - Configure Azure OpenAI with Microsoft Entra ID authentication: learn.microsoft.com

  2. The A2A CLI on AWS: One Client for Lightsail, Bedrock AgentCore and Localhost --- title: "The A2A CLI on AWS: One Client for Lightsail, Bedrock AgentCore and Localhost" published: true description: "A Rust A2A v1.0 agent on Amazon Lightsail and a Strands A2A v0.3 coordinator on Bedrock AgentCore Runtime, driven by the same A2A CLI commands as their localhost copies, with the CLI also carrying the agent-to-agent call between them." tags: a2a, aws, rust, agents cover_image: raw.githubusercontent.com --- This article provides a step by step guide to running A2A agents on AWS and driving them with the A2A CLI. A Rust agent on the official A2A Rust SDK runs in an Amazon Lightsail container. A Strands agent on an Amazon Bedrock model runs in Amazon Bedrock AgentCore Runtime and hands its work to the Rust agent. Both also run on a laptop, and one set of a2a commands checks all four. github.com/xbill9/rust-dev/tree/main/a2a-strands-… The A2A CLI drove all four endpoints with the same commands, 16 of 16 checks passed, and the CLI also carried the call from the Strands agent to the Rust agent. The two agents speak different protocol versions: the Rust SDK serves A2A 1.0 and Strands serves A2A 0.3. The CLI reads both. Strands' own Python client reads only 0.3, so the Strands agent calls the Rust agent by running the same a2a binary the laptop uses. On AWS the CLI needs a login token, a session header and the full agent card URL, and nothing else changes. --- What Is Being Built? | Agent | Where it runs | A2A | Model | | :--- | :--- | :--- | :--- | | mersenne-agent (Rust) | laptop, port 8204 | 1.0, JSON-RPC and REST | none | | mersenne-agent (same image) | Lightsail container service, nano | 1.0, JSON-RPC and REST | none | | Strands coordinator | laptop, port 8209 | 0.3, JSON-RPC | Claude Haiku 5.5 on Bedrock | | Strands coordinator (same code) | Bedrock AgentCore Runtime | 0.3, JSON-RPC | Claude Haiku 5.5 on Bedrock | a2a CLI ──► AgentCore Runtime (A2A 0.3, Cognito token) ──► Strands Agent ──Bedrock──► Claude Haiku 5.5 └── find_mersenne_primes tool └── a2a CLI ──► Lightsail (A2A 1.0) ──► mersenne-agent a2a CLI ──────────────────────────────────────────────────────────────────────────► Lightsail (A2A 1.0) The workload is the one from the sibling rig: a Lucas-Lehmer test over the first N known Mersenne prime exponents, timed by the agent itself. Every reply carries the line Found first N Mersenne primes in X ms. and a data part with requested, found and elapsed_ms. Versions, read from the machine on 2026-11-05: | Component | Version | | :--- | :--- | | A2A CLI (a2a) | v0.3.1 | | a2a-lf / a2a-server-lf (Rust SDK, A2A 1.0) | 0.4.2 / 0.5.3 | | strands-agents / a2a-sdk / bedrock-agentcore | 1.64.0 / 0.3.26 / 1.25.3 | | Rust | 1.99.1, edition 2024 | | Model | us.anthropic.claude-haiku-5-5 on Bedrock | | Region | us-east-1 | --- At This Point You Should Have… - The A2A CLI on your PATH as a2a: go install github.com/a2aproject/a2a-cli@latest, then rename a2a-cli to a2a - Rust, Go, Docker and Python 3 with pip install "strands-agents[a2a]" "bedrock-agentcore[a2a]" - AWS credentials with Bedrock model access in us-east-1, and the lightsailctl plugin for the AWS CLI a2a version a2a v0.3.1 commit: none built: unknown go: go1.26.3 os/arch: linux/amd64 --- Step 1 — Run Both Agents on the Laptop git clone https://github.com/xbill9/rust-dev && cd rust-dev/a2a-strands-aws scripts/start-local.sh up: 8204 rust agent, 8209 strands coordinator -> http://localhost:8204 The coordinator is a Strands Agent with one tool, served over A2A by the AgentCore SDK's serve_a2a. The same file runs on the laptop and in AgentCore: def create_agent(_context_id: str) -> Agent: return Agent( name="Strands Mersenne Coordinator", model=BedrockModel(model_id=MODEL_ID), system_prompt=SYSTEM_PROMPT, tools=[find_mersenne_primes], callback_handler=None, ) serve_a2a(StrandsA2AExecutor(agent_factory=create_agent), agent_card(), port=port) The CLI reads both cards and shows each one's protocol version: a2a card get -a http://localhost:8204 a2a card get -a http://localhost:8209 Name: Mersenne Prime Agent Rust Version: 0.1.0 Interfaces: JSONRPC (A2A v1.0) http://localhost:8204/jsonrpc HTTP+JSON (A2A v1.0) http://localhost:8204/rest Name: Strands Mersenne Coordinator Version: 0.1.0 Interfaces: JSONRPC (A2A v0.3.0) http://localhost:8209/ Streaming: true --- Step 2 — Why the Strands Tool Calls the A2A CLI Strands pins a2a-sdk below 0.4, so its A2A client and server speak protocol 0.3. An A2A 1.0 card lists its endpoints under supportedInterfaces and drops the top-level url field that a 0.3 card requires. scripts/check-sdk03-card.py fetches each card with the 0.3 client: scripts/check-sdk03-card.py http://localhost:8204 scripts/check-sdk03-card.py http://localhost:8209 a2a-sdk 0.3.26, card from http://localhost:8204 A2AClientJSONError: JSON Error: Failed to validate agent card structure from http://localhost:8204/.well-known/agent-card.json: [{"type":"missing","loc":["url"],"msg":"Field required", ... a2a-sdk 0.3.26, card from http://localhost:8209 ok: Strands Mersenne Coordinator, protocolVersion 0.3.0, url http://localhost:8209/ The A2A CLI reads both, so the tool runs it with JSON output and a data part, then reads the Rust agent's structured report: cmd = [ A2A_BIN, "send", "-a", RUST_AGENT_URL, "-o", "json", "--timeout", "60s", "--data-part", json.dumps({"count": count}), ] proc = subprocess.run(cmd, capture_output=True, text=True, timeout=90) The tool refuses a reply whose requested or found differs from the count the model asked for, and the system prompt tells the model to quote the agent's own reply line. The model picks N; the Rust agent does the counting and the timing. a2a send -a http://localhost:8209 "Find the first 9 Mersenne primes" Artifacts: [agent_response] Found first 9 Mersenne primes in 0.0298ms. Exponents: 2, 3, 5, 7, 13, 17, 19, 31, 61 --- Step 3 — The Rust Agent on Lightsail scripts/deploy-lightsail.py creates a nano container service, builds docker/agent.Dockerfile, pushes it with aws lightsail push-container-image and deploys it with ADVERTISE_URL set to the service's own HTTPS address, so the card's interfaces point at Lightsail. The health check is the agent card path. scripts/deploy-lightsail.py creating container service a2a-rust-mersenne (nano, scale 1) $ docker build --platform linux/amd64 -f docker/agent.Dockerfile -t a2a-rust-mersenne:local /home/xbill/rust-dev/a2a-strands-aws $ aws lightsail push-container-image --region us-east-1 --service-name a2a-rust-mersenne --label agent --image a2a-rust-mersenne:local pushed :a2a-rust-mersenne.agent.222 ✅ https://a2a-rust-mersenne.6wpv8vensby5c.us-east-1.cs.amazonlightsail.com/.well-known/agent-card.json The commands are the laptop's, with the Lightsail address: scripts/test-cli.sh lightsail == lightsail rust agent (https://a2a-rust-mersenne.6wpv8vensby5c.us-east-1.cs.amazonlightsail.com) Interfaces: JSONRPC (A2A v1.0) https://a2a-rust-mersenne.6wpv8vensby5c.us-east-1.cs.amazonlightsail.com/jsonrpc HTTP+JSON (A2A v1.0) https://a2a-rust-mersenne.6wpv8vensby5c.us-east-1.cs.amazonlightsail.com/rest ✅ lightsail card (A2A v1.0) ✅ lightsail send ✅ lightsail stream ✅ lightsail rest ✅ lightsail data-part --- Step 4 — An arm64 Image Built on an x86 Laptop AgentCore Runtime runs arm64 containers, serves A2A on port 9000, and expects the card at /.well-known/agent-card.json and a health answer at /ping. serve_a2a provides all three. The image has to be arm64, and this laptop has no arm64 emulator, so scripts/build-coordinator.sh assembles it from parts that are already built for arm64: - pip downloads aarch64 wheels: --platform manylinux_2_28_aarch64 --python-version 3.13 --only-binary=:all: - Go cross-compiles the A2A CLI: GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build - the Dockerfile only copies, so Docker never runs an arm64 binary FROM python:3.13-slim-trixie COPY build/coordinator/site /opt/site COPY build/coordinator/a2a /usr/local/bin/a2a COPY coordinator/main.py /app/main.py ENV PYTHONPATH=/opt/site PYTHONUNBUFFERED=1 DOCKER_CONTAINER=1 A2A_PORT=9000 WORKDIR /app EXPOSE 9000 CMD ["python", "main.py"] file build/coordinator/a2a docker image inspect a2a-strands-coordinator:local --format '{{.Os}}/{{.Architecture}}' build/coordinator/a2a: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID linux/arm64 ARCH=amd64 scripts/build-coordinator.sh builds the same image for x86, which runs on the laptop and checks the image layout before AgentCore sees it. --- Step 5 — The Strands Agent on Bedrock AgentCore Runtime scripts/deploy-agentcore.py pushes the image to ECR and creates an IAM role with ECR pull, CloudWatch Logs and bedrock:InvokeModel*. It creates a Cognito user pool with one app client and one user, and an AgentCore runtime with serverProtocol: A2A, a customJWTAuthorizer on that pool, public networking, and RUST_AGENT_URL set to the Lightsail agent. A second update sets AGENTCORE_RUNTIME_URL once the runtime's ARN exists, so the card advertises the AgentCore address. scripts/deploy-agentcore.py runtime CREATING (version 1) runtime READY (version 1) runtime UPDATING (version 2) runtime READY (version 2) ✅ https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-east-1%3A106059658660%3Aruntime%2Fa2a_strands_coordinator-Rtn6xkF0PS/invocations/ From the CLI, AgentCore takes a bearer token from Cognito and a session id header of at least 33 characters: CARD=$(scripts/aws_state.py get agentcore_card) TOKEN=$(scripts/cognito-token.py) SID="a2a-cli-$(cat /proc/sys/kernel/random/uuid)" a2a --auth "Bearer $TOKEN" --svc-param "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id=$SID" card get -a "$CARD" Name: Strands Mersenne Coordinator Description: Strands agent on Amazon Bedrock (A2A 0.3) that delegates Mersenne prime runs to a Rust A2A 1.0 agent through the a2a CLI. Version: 0.1.0 Interfaces: JSONRPC (A2A v0.3.0) https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-east-1%3A106059658660%3Aruntime%2Fa2a_strands_coordinator-Rtn6xkF0PS/invocations Streaming: true Skills: mersenne-via-rust Mersenne primes via the Rust agent --- 🔎 Tip: Give the CLI the Full Card URL for AgentCore The CLI reads a URL with no card path as a host, and asks that host's origin for /.well-known/agent-card.json. For AgentCore the origin is https://bedrock-agentcore.us-east-1.amazonaws.com, which answers 404: $ a2a --auth "Bearer $TOKEN" --svc-param "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id=$SID" card get -a "https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3A...%2Fa2a_strands_coordinator-Rtn6xkF0PS/invocations/" Error: failed to resolve agent card: card request failed, status: 404 Not Found hint: check the --agent-card reference (host, full card URL, or local file path) Append .well-known/agent-card.json to the invocations URL and the same command returns the card. The deploy script stores that URL as agentcore_card. --- Step 6 — The Same Checks on All Four Endpoints scripts/test-cli.sh all starts the laptop agents and runs one set of checks against each endpoint. Each Rust agent gets card (A2A v1.0), send, stream, REST and a data part. Each coordinator gets card (A2A v0.3), send and stream. Every check looks for the agent's reply line with the right N. scripts/test-cli.sh all == local rust agent (http://localhost:8204) ✅ local card (A2A v1.0) ✅ local send ✅ local stream ✅ local rest ✅ local data-part == local strands coordinator (http://localhost:8209) ✅ local card (A2A v0.3) ✅ local send ✅ local stream == lightsail rust agent (https://a2a-rust-mersenne.6wpv8vensby5c.us-east-1.cs.amazonlightsail.com) ✅ lightsail card (A2A v1.0) ✅ lightsail send ✅ lightsail stream ✅ lightsail rest ✅ lightsail data-part == agentcore strands coordinator (https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3A...%2Fa2a_strands_coordinator-Rtn6xkF0PS/invocations/.well-known/agent-card.json) ✅ agentcore card (A2A v0.3) ✅ agentcore send ✅ agentcore stream The AgentCore checks ask for 7 and 11 primes. Both requests reached Lightsail, through the CLI inside the AgentCore container. The Lightsail container log shows them: INFO mersenne_agent: computed requested=7 found=7 elapsed_ms=0.005935 INFO mersenne_agent: computed requested=11 found=11 elapsed_ms=0.056663000000000005 --- Step 7 — Timing the Same Command on Each Platform scripts/cli-bench.py starts one a2a process per call and times it, so wall time includes starting the CLI, fetching the card, TLS on AWS and the reply. Targets take turns, one call each per round, so drift during the run lands on every target alike. Every reply is checked, and the statistics come from the script (results/summary-20261105.txt). N=10 throughout. scripts/cli-bench.py --targets local-rust,lightsail-rust --reps 20 --n 10 local-rust ok 20/20 wall median 27.7 ms (min 24.1, max 35.1) compute median 0.05682 ms lightsail-rust ok 20/20 wall median 154.4 ms (min 140.4, max 174.3) compute median 0.03928 ms scripts/cli-bench.py --targets local-coordinator,agentcore-coordinator --reps 5 --n 10 scripts/cli-bench.py --targets local-coordinator,agentcore-coordinator --reps 5 --n 10 --reuse-session local-coordinator ok 5/5 wall median 2596.2 ms (min 2288.0, max 2662.2) agentcore-coordinator ok 5/5 wall median 3416.5 ms (min 3364.7, max 3482.8) local-coordinator ok 5/5 wall median 2459.9 ms (min 2184.5, max 2683.8) agentcore-coordinator ok 5/5 wall median 2997.1 ms (min 2849.7, max 12177.7) The Rust agent's own compute time is a fraction of a millisecond on both machines, so the 126.7 ms between the laptop and Lightsail medians is the network and TLS. A coordinator call is a Bedrock model call plus a nested CLI call to the Rust agent. AgentCore added 820.3 ms over the laptop with a new session id per call and 537.2 ms with one session for the run. In the one-session run, the first AgentCore call took 12177.7 ms and the other four stayed near the median. --- Step 8 — Teardown make teardown deleted runtime a2a_strands_coordinator deleted user pool a2a-strands-aws deleted role a2a-strands-coordinator-runtime deleted ECR repository a2a-strands-coordinator deleted a2a-rust-mersenne make deploy recreates both, and make test-cli T=all checks them again. --- Compare and Contrast | Endpoint | CLI flags beyond the laptop's | A2A | Median wall time, N=10 | | :--- | :--- | :--- | ---: | | Rust, laptop 🥇 | none | 1.0 | 27.7 ms | | Rust, Lightsail 🥈 | none, HTTPS address | 1.0 | 154.4 ms | | Strands, laptop | none | 0.3 | 2596.2 ms | | Strands, AgentCore | --auth, session header, full card URL | 0.3 | 3416.5 ms new session per call, 2997.1 ms one session | --- So, Which One? For an agent with no model in it, Lightsail is the short path: one container, a public HTTPS address, no client changes. Every call costs the network trip, 126.7 ms here, on top of a sub-millisecond computation. For an agent built around a model, AgentCore Runtime gives it a managed home with inbound authentication, and costs the CLI two flags and the full card URL. Reuse a session id across calls when a client sends many. For talking to any of them, the A2A CLI is the one client. It reads A2A 1.0 and 0.3 cards, and it fits inside an agent as well as on a laptop: the Strands agent reaches a Rust 1.0 agent with the same binary, where its own 0.3 client cannot read the card. --- Summary The goal of this article was to show that the A2A CLI drives the same agents on a laptop, on Amazon Lightsail and on Amazon Bedrock AgentCore Runtime with the same commands. The key to the solution was one set of checks run against every endpoint, and a Strands tool that calls the Rust agent through the CLI. The results were: - 🟢 16 of 16 CLI checks passed across four endpoints: card, send, stream, REST and data parts on the Rust agent, card, send and stream on the Strands agent - 🟢 The CLI read A2A 1.0 cards (Rust SDK) and A2A 0.3 cards (Strands) with the same commands - 🟢 Inside AgentCore the CLI carried the agent-to-agent call to Lightsail, confirmed in the Lightsail log - 🟢 An arm64 AgentCore image built on an x86 laptop with no emulator - ⚠️ AgentCore needs --auth, a session id header and the full card URL; the invocations URL alone returns 404 - ⚠️ Lightsail added 126.7 ms per call over the laptop, AgentCore 820.3 ms with a new session per call and 537.2 ms with one session - ❌ The a2a-sdk 0.3 Python client that Strands uses cannot parse an A2A 1.0 card Scope: one laptop (13th Gen Intel Core i7-1360P, Linux), one AWS region (us-east-1), one Lightsail power (nano), one model (us.anthropic.claude-haiku-5-5), measured on 2026-11-05. Timing used N=10 with 20 calls per Rust target and 5 per coordinator target, and each coordinator comparison is a single run. Every AWS figure includes the trip from the laptop. gRPC, task commands, push notifications and load were not tested. The strategy for running A2A agents on AWS and driving them with the A2A CLI was validated with an incremental step by step approach. --- References - The rig: github.com/xbill9/rust-dev/tree/main/a2a-strands-… - The sibling rig, the same Rust agent on a laptop: github.com/xbill9/rust-dev/tree/main/a2a-benchmar… - CLI check transcript: github.com/xbill9/rust-dev/blob/main/a2a-strands-… - Timing summary: github.com/xbill9/rust-dev/blob/main/a2a-strands-… - A2A CLI: a2aproject/a2a-cli - A2A Rust SDK: a2aproject/a2a-rs - Strands Agents: strandsagents.com - Deploy A2A servers in AgentCore Runtime: docs.aws.amazon.com - A2A protocol contract for AgentCore Runtime: docs.aws.amazon.com - Amazon Lightsail container services: docs.aws.amazon.com

  3. 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

    12 reactions · 3 comments
  4. 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
  5. @AgenticAIFdn If you want to try it, start with the first lesson in the cookbook, examples/01-exec-demo: a2aproject/a2a-cli

    77 views
  6. @stevekrouse haven't used it but this is a thing a2aproject/a2a

    29 views
  7. 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
  8. Reddit · · r/A2AProtocol · u/Prize-Programmer4207 · a2aproject/A2A

    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

Achievements

Awards earned by the repositories a2aproject owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.