{"slug": "google-adk-as-the-master-agent-calling-amazon-bedrock-over-a2a", "title": "Google ADK as the Master Agent, Calling Amazon Bedrock over A2A", "summary": "A developer's cross-cloud benchmark revealed that reversing the master-worker roles between Google ADK and Amazon Bedrock exposed six interoperability defects, five of which local tests could not catch. The defects included an A2A protocol version mismatch, a missing server dependency, and an AWS IAM condition key mapping error that caused silent authentication failures. The fixes involved pinning the a2a-sdk version, adding the http-server extra, and using the correct OIDC condition key.", "body_md": "This is the third run of the same cross-cloud currency benchmark, and the first\n\nwith the arrow pointing the other way. A **Google ADK master** on Cloud Run\n\n(Gemini 2.5 Flash, `us-central1`\n\n) owns the benchmark policy. It calls an\n\n**Amazon Bedrock AgentCore worker** (Strands Agents on Nova Micro, `us-east-1`\n\n)\n\nover **A2A v1.0**, and cross-checks the answer against an MCP exchange-rate\n\ntool.\n\nThe previous run had Bedrock as the master and ADK as the worker. Reversing it\n\nwas supposed to be a deployment change — the domain core is\n\nframework-independent, so the comparison logic never moved. What actually\n\nhappened is that reversal exposed six interoperability defects, **five of which\nno local test could have caught**. That is the interesting part, so this article\n\nMost A2A demos stop at \"the HTTP 200 came back.\" That is a smoke test, not an\n\ninteroperability benchmark.\n\nThe benchmark runs three modes so the cost of remote verification is separable\n\nfrom the cost of the work:\n\n| Mode | What runs | Purpose |\n|---|---|---|\n`mcp_only` |\nADK master calls the rate tool over MCP stdio | Baseline |\n`a2a_only` |\nADK master delegates to the AgentCore worker | Remote-agent cost |\n`verified` |\nBoth, concurrently, then compared deterministically | Accuracy/overhead |\n\nComparison is arithmetic, not a model judgement. Amounts and rates are\n\n`Decimal`\n\n. A model is never asked whether two numbers agree.\n\nThis one was caught before deploying, and it dictated the whole worker design.\n\n`strands-agents[a2a]`\n\npins `a2a-sdk<0.4`\n\n— the A2A **v0.3** wire methods\n\n(`message/send`\n\n). `google-adk[a2a]==2.5.0`\n\nuses `a2a-sdk`\n\n**1.x** — v1.0\n\n(`SendMessage`\n\n). A v1.0 client cannot call a v0.3 server, and the agent card\n\ncarries no version negotiation.\n\nThis is the same split that forced the A2UI extension out of the ADK agent in\n\nthe first run of this project. It reappeared from the opposite side.\n\nThe fix: use `strands-agents`\n\nfor the agent loop only, and build the A2A v1.0\n\nserver surface from `a2a-sdk`\n\ndirectly.\n\n```\n# app/CurrencyWorker/main.py -- NOT strands.multiagent.a2a.A2AServer\nfrom a2a.server.request_handlers import DefaultRequestHandler\nfrom a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes\n\napp = Starlette(routes=[\n    *create_agent_card_routes(AGENT_CARD),\n    *create_jsonrpc_routes(_request_handler, rpc_url=\"/\"),\n])\n```\n\nA test asserts the lockfile keeps `a2a-sdk`\n\non 1.x, so a dependency bump cannot\n\nquietly reintroduce the mismatch.\n\n`google-adk[a2a]`\n\ndoes not install its own server dependency\nThe Cloud Run container built fine, passed every local test, and then died on\n\nstartup:\n\n```\nModuleNotFoundError: No module named 'sse_starlette'\n  File \".../google/adk/a2a/_compat.py\", line 769, in attach_a2a_routes_to_app\n```\n\nADK's `to_a2a()`\n\nimports `a2a.server.routes`\n\n, which needs `sse_starlette`\n\n, which\n\narrives only with the `a2a-sdk[http-server]`\n\nextra. Neither `google-adk[a2a]`\n\nnor bare `a2a-sdk`\n\npulls it in.\n\nCost: one failed rollout. Both halves now declare `a2a-sdk[http-server]`\n\n.\n\nThe coordinator holds no AWS keys. It federates: Cloud Run's metadata server\n\nmints a Google OIDC token, STS `AssumeRoleWithWebIdentity`\n\nexchanges it for\n\ntemporary credentials, and requests are SigV4-signed.\n\nThe trust policy looked obviously correct and was silently impossible:\n\n```\n\"Condition\": { \"StringEquals\": {\n  \"accounts.google.com:aud\": \"currencybench-agentcore-worker\",\n  \"accounts.google.com:sub\": \"1019138736740282766..\"\n}}\n```\n\nAWS does not map those condition keys to the claims their names suggest:\n\n| Condition key | Actual Google claim |\n|---|---|\n`accounts.google.com:oaud` |\nthe token's `aud`\n|\n`accounts.google.com:aud` |\nthe token's (numeric client id)`azp` |\n`accounts.google.com:sub` |\nthe token's `sub`\n|\n\nThe audience *string* was being compared against a *number*. Pin the audience\n\nwith `oaud`\n\n, not `aud`\n\n.\n\nThe natural next step — register `accounts.google.com`\n\nas an IAM OIDC identity\n\nprovider — is wrong. AWS federates with Google natively. Creating an explicit\n\nprovider switched STS to validating against that provider's thumbprint, and\n\nevery exchange failed:\n\n```\n<Code>InvalidIdentityToken</Code>\n<Message>The web identity token provided could not be validated.</Message>\n```\n\nDeleting the provider fixed it. The principal is the bare domain:\n\n```\n\"Principal\": { \"Federated\": \"accounts.google.com\" }\n```\n\nWorth noting the failure mode: an invalid *token* and a rejected *trust policy*\n\nare different errors (`InvalidIdentityToken`\n\nvs `AccessDenied`\n\n), and that\n\ndistinction is the fastest way to tell these two bugs apart.\n\nWith federation working and SigV4 signing accepted, the worker rejected its own\n\ncorrectly-versioned client:\n\n```\nA2A version '0.3' is not supported by this handler. Expected version '1.0'.\n```\n\nThe client was `a2a-sdk`\n\n1.1.2 and does send the right header\n\n(`client_factory.py`\n\nsets `A2A-Version: 1.0`\n\n). The worker was v1.0. The card\n\nadvertised `\"protocolVersion\": \"1.0\"`\n\n.\n\nThe header never arrived. AgentCore's A2A runtime forwards only allow-listed\n\nrequest headers — and the server-side validator treats a **missing** header as\n\nv0.3:\n\n```\n# a2a/utils/version_validator.py\nif not actual_version:\n    return constants.PROTOCOL_VERSION_0_3\n```\n\nA dropped header and an old client are indistinguishable to the server. The fix\n\nis one line of runtime config:\n\n```\n\"requestHeaderAllowlist\": [\"A2A-Version\"]\n```\n\nThis is the version-skew theme of the whole project arriving a third way:\n\nfirst through a transitive pin, then through a framework extra, now through a\n\nproxy.\n\nThe AgentCore worker's card advertises `http://127.0.0.1:9000`\n\n— its container's\n\nown bind address. ADK's `to_a2a()`\n\ndoes the same with `http://127.0.0.1:8080`\n\n.\n\n`a2a-sdk`\n\nclients route by card URL, so cross-cloud calls fail unless the client\n\nrewrites the interfaces to the endpoint it actually dialled.\n\nDocumented in the first run for ADK; it is not an ADK quirk.\n\n`agentcore status`\n\nprints a URL with the runtime ARN percent-encoded into the\n\npath and an `/invocations`\n\nsuffix. That full URL, suffix included, is the A2A\n\nbase:\n\n```\nhttps://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3A...%2F<id>/invocations\n                                                                       └── card at <base>/.well-known/agent-card.json\n```\n\nDropping `/invocations`\n\n— the tidier-looking base — returns\n\n`UnknownOperationException`\n\n. So does using a bare runtime id instead of the\n\nencoded ARN. The percent-encoding also breaks naive `${VAR##*/runtimes/}`\n\nshell\n\nparsing when deriving the runtime id for a scoped IAM policy.\n\nFive warm invocations per mode, driven through the deployed Cloud Run\n\ncoordinator, `us-central1`\n\n→ `us-east-1`\n\n, on 2026-07-31:\n\n| Mode | Median | Min | Max |\n|---|---|---|---|\n`mcp_only` |\n2.96 s |\n2.37 s | 3.84 s |\n`a2a_only` |\n18.92 s |\n16.30 s | 19.68 s |\n`verified` |\n16.19 s |\n15.64 s | 22.86 s |\n\nTwo things stand out.\n\n**The remote-agent hop costs about 6× the tool baseline.** That is not network\n\nlatency — `us-central1`\n\nto `us-east-1`\n\nis tens of milliseconds. It is a second\n\nmodel turn: the worker is an agent, so delegation means Nova Micro plans a tool\n\ncall, calls it, and writes a structured reply, on top of Gemini doing the same.\n\nA2A delegation buys independence, and independence costs an inference.\n\n** verified is not slower than a2a_only.** Verified mode does the MCP call\n\n```\nmcp_task = self._call(\"mcp\", self._mcp, request, failures)\na2a_task = self._call(\"a2a\", self._a2a, request, failures)\nmcp_quotes, a2a_quotes = await asyncio.gather(mcp_task, a2a_task)\n```\n\nSo verified ≈ max(mcp, a2a), not the sum. Independent verification is close to\n\nfree once you are already paying for the remote agent. The two medians differ by\n\nless than the run-to-run spread, so read them as equal, not as verified being\n\ngenuinely faster.\n\nCorrectness, verified mode, 100 USD → EUR and CHF:\n\n``` php\nEUR  rate 0.87138  ->  87.138    CHF  rate 0.81248  ->  81.248\nprimary:  mcp-stdio:frankfurter-live\nverifier: aws-agentcore-a2a-worker\n```\n\nBoth targets agreed. Both also carried `rate is stale; check the observation`\n\n— the MCP side returned Frankfurter's daily reference rate stamped\n\ntimestamp\n\n`2026-07-30T00:00:00Z`\n\nagainst a run at `04:09Z`\n\non the 31st, over the 24 h\n\nthreshold. The staleness rule firing on a real published rate, rather than a\n\nfixture, is the more useful signal here.\n\n`bedrock-agentcore:InvokeAgentRuntime`\n\nto `runtime/<id>`\n\nand `runtime/<id>/*`\n\nwas denied with 403; only `Resource: \"*\"`\n\nworked. AgentCore authorises against\nsome ARN shape I have not identified, and data-plane events are not in\nCloudTrail by default. The measurements above were taken with the wide policy.The domain core did not change. `CurrencyCoordinator`\n\ntakes two duck-typed\n\nadapters, so which cloud hosts the master is a deployment decision. That held\n\nup: the comparison logic, the staleness rule, the failure taxonomy, and the\n\nevaluation harness all survived the reversal untouched.\n\nEverything that broke was at the seams — protocol versions, dependency extras,\n\nidentity federation, header forwarding, URL shapes. Five of the six defects were\n\ninvisible until something real was deployed. If you take one thing from this:\n\n**an interoperability test that never leaves your laptop is testing your\nmocks.**", "url": "https://wpnews.pro/news/google-adk-as-the-master-agent-calling-amazon-bedrock-over-a2a", "canonical_source": "https://dev.to/gde/google-adk-as-the-master-agent-calling-amazon-bedrock-over-a2a-4ahc", "published_at": "2026-07-31 17:42:40+00:00", "updated_at": "2026-07-31 18:02:59.430756+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-infrastructure", "developer-tools", "ai-agents"], "entities": ["Google ADK", "Amazon Bedrock", "AgentCore", "Gemini 2.5 Flash", "Nova Micro", "A2A", "MCP", "AWS IAM"], "alternates": {"html": "https://wpnews.pro/news/google-adk-as-the-master-agent-calling-amazon-bedrock-over-a2a", "markdown": "https://wpnews.pro/news/google-adk-as-the-master-agent-calling-amazon-bedrock-over-a2a.md", "text": "https://wpnews.pro/news/google-adk-as-the-master-agent-calling-amazon-bedrock-over-a2a.txt", "jsonld": "https://wpnews.pro/news/google-adk-as-the-master-agent-calling-amazon-bedrock-over-a2a.jsonld"}}