Skip to content

Commit be1be55

Browse files
authored
feat: add A2A protocol support via serve_a2a (#349)
* feat: add A2A protocol support via serve_a2a and build_a2a_app Adds ~130 lines of Bedrock-specific glue around the official a2a-sdk, replacing the need for a custom protocol implementation. The industry (ADK, Strands, CrewAI) has converged on the a2a-sdk, and this delegates all protocol handling to it. New exports from bedrock_agentcore.runtime: - serve_a2a(executor, agent_card=None, ...) — one-liner to start an A2A server - build_a2a_app(executor, agent_card=None, ...) — returns a Starlette app - BedrockCallContextBuilder — extracts Bedrock headers into contextvars Key features: - Optional a2a-sdk dependency: pip install "bedrock-agentcore[a2a]" - Auto-builds AgentCard from StrandsA2AExecutor when not provided - Auto-populates agent_card.url from AGENTCORE_RUNTIME_URL env var - Docker/container host detection (0.0.0.0 vs 127.0.0.1) - /ping health check endpoint with optional custom handler - Propagates session, request, token, and custom headers via contextvars Works with any framework that provides an a2a-sdk AgentExecutor: - Strands: serve_a2a(StrandsA2AExecutor(agent)) - ADK: serve_a2a(A2aAgentExecutor(runner=runner), card) - LangGraph: serve_a2a(CustomExecutor(graph), card) * chore: update uv.lock for a2a optional dependency * fix: sort a2a/starlette imports for ruff 0.12.0 compatibility * refactor: build Starlette app with /ping upfront instead of mutating routes Create the Starlette app with the /ping route included in the constructor, then use add_routes_to_app() to wire A2A endpoints. This avoids depending on route mutation after build(). * feat: add build_runtime_url utility for ARN-to-URL conversion Adds build_runtime_url(agent_arn, region=None) that constructs the Bedrock AgentCore runtime invocation URL from an agent ARN, properly URL-encoding the ARN. Extracts region from the ARN if not provided. * refactor: lazy-import A2A symbols so a2a-sdk is not required at import time * docs: add A2A protocol documentation and README section * fix: sort integration test imports for ruff 0.12.0 compatibility * fix: add known-third-party config for a2a and fix import ordering Adds [tool.ruff.lint.isort] known-third-party = ["a2a"] so ruff classifies a2a imports consistently regardless of whether a2a-sdk is installed in the lint environment.
1 parent cd2f2a0 commit be1be55

9 files changed

Lines changed: 1306 additions & 4 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ app.run() # Ready to run on Bedrock AgentCore
7979

8080
**Production:** [AWS CDK](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_bedrockagentcore-readme.html).
8181

82+
## A2A Protocol Support
83+
84+
Serve your agent using the [A2A (Agent-to-Agent) protocol](https://google.github.io/A2A/) on Bedrock AgentCore Runtime. Works with any framework that provides an a2a-sdk `AgentExecutor` (Strands, LangGraph, Google ADK, or custom).
85+
86+
```bash
87+
pip install "bedrock-agentcore[a2a]"
88+
```
89+
90+
```python
91+
from strands import Agent
92+
from strands.a2a import StrandsA2AExecutor
93+
from bedrock_agentcore.runtime import serve_a2a
94+
95+
agent = Agent(model="us.anthropic.claude-sonnet-4-20250514", system_prompt="You are a helpful assistant.")
96+
serve_a2a(StrandsA2AExecutor(agent))
97+
```
98+
99+
See [A2A Protocol Examples](docs/examples/a2a_protocol_examples.md) for LangGraph, Google ADK, and advanced usage.
82100

83101
## 📝 License & Contributing
84102

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# A2A Protocol Support
2+
3+
This document explains how to serve your agent using the [A2A (Agent-to-Agent) protocol](https://google.github.io/A2A/) on Bedrock AgentCore Runtime.
4+
5+
## Installation
6+
7+
A2A support requires the optional `a2a` extra:
8+
9+
```bash
10+
pip install "bedrock-agentcore[a2a]"
11+
```
12+
13+
## Quick Start
14+
15+
### Strands Agent
16+
17+
Strands provides a built-in `StrandsA2AExecutor` that wraps a Strands `Agent` as an A2A executor. When no `AgentCard` is provided, one is auto-built from the agent's `name` and `description`.
18+
19+
```python
20+
from strands import Agent
21+
from strands.a2a import StrandsA2AExecutor
22+
from bedrock_agentcore.runtime import serve_a2a
23+
24+
agent = Agent(
25+
model="us.anthropic.claude-sonnet-4-20250514",
26+
system_prompt="You are a helpful calculator.",
27+
)
28+
29+
if __name__ == "__main__":
30+
serve_a2a(StrandsA2AExecutor(agent))
31+
```
32+
33+
### LangGraph Agent
34+
35+
LangGraph requires a thin `AgentExecutor` wrapper (~15 lines) and an explicit `AgentCard`:
36+
37+
```python
38+
from langchain_aws import ChatBedrockConverse
39+
from langgraph.prebuilt import create_react_agent
40+
41+
from a2a.server.agent_execution import AgentExecutor, RequestContext
42+
from a2a.server.events import EventQueue
43+
from a2a.server.tasks import TaskUpdater
44+
from a2a.types import AgentCapabilities, AgentCard, AgentSkill, Part, TextPart
45+
from a2a.utils import new_task
46+
from bedrock_agentcore.runtime import serve_a2a
47+
48+
llm = ChatBedrockConverse(model="us.anthropic.claude-sonnet-4-20250514")
49+
graph = create_react_agent(llm, tools=[], prompt="You are a helpful calculator.")
50+
51+
52+
class LangGraphA2AExecutor(AgentExecutor):
53+
def __init__(self, graph):
54+
self.graph = graph
55+
56+
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
57+
task = context.current_task or new_task(context.message)
58+
if not context.current_task:
59+
await event_queue.enqueue_event(task)
60+
updater = TaskUpdater(event_queue, task.id, task.context_id)
61+
user_text = context.get_user_input()
62+
result = await self.graph.ainvoke({"messages": [("user", user_text)]})
63+
response = result["messages"][-1].content
64+
await updater.add_artifact([Part(root=TextPart(text=response))])
65+
await updater.complete()
66+
67+
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
68+
pass
69+
70+
71+
card = AgentCard(
72+
name="langgraph-agent",
73+
description="A LangGraph agent on Bedrock AgentCore",
74+
url="http://localhost:9000/",
75+
version="0.1.0",
76+
capabilities=AgentCapabilities(streaming=True),
77+
skills=[AgentSkill(id="calc", name="calculator", description="Arithmetic", tags=["math"])],
78+
default_input_modes=["text"],
79+
default_output_modes=["text"],
80+
)
81+
82+
if __name__ == "__main__":
83+
serve_a2a(LangGraphA2AExecutor(graph), card)
84+
```
85+
86+
### Google ADK Agent
87+
88+
Google ADK provides `A2aAgentExecutor` built-in. You supply an explicit `AgentCard`:
89+
90+
```python
91+
from google.adk.agents import LlmAgent
92+
from google.adk.runners import Runner
93+
from google.adk.a2a import A2aAgentExecutor
94+
95+
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
96+
from bedrock_agentcore.runtime import serve_a2a
97+
98+
agent = LlmAgent(
99+
model="gemini-2.0-flash",
100+
name="calculator",
101+
description="A calculator agent",
102+
instruction="You are a helpful calculator.",
103+
)
104+
runner = Runner(agent=agent, app_name="calculator", session_service=None)
105+
106+
card = AgentCard(
107+
name="adk-agent",
108+
description="A Google ADK agent on Bedrock AgentCore",
109+
url="http://localhost:9000/",
110+
version="0.1.0",
111+
capabilities=AgentCapabilities(streaming=True),
112+
skills=[AgentSkill(id="calc", name="calculator", description="Arithmetic", tags=["math"])],
113+
default_input_modes=["text"],
114+
default_output_modes=["text"],
115+
)
116+
117+
if __name__ == "__main__":
118+
serve_a2a(A2aAgentExecutor(runner=runner), card)
119+
```
120+
121+
## API Reference
122+
123+
### `serve_a2a(executor, agent_card=None, *, port=9000, host=None, ...)`
124+
125+
Starts a Bedrock-compatible A2A server with `uvicorn`.
126+
127+
| Parameter | Type | Default | Description |
128+
|-----------|------|---------|-------------|
129+
| `executor` | `AgentExecutor` | required | An a2a-sdk `AgentExecutor` that implements the agent logic |
130+
| `agent_card` | `AgentCard` | `None` | Agent metadata. Auto-built from executor if omitted (works best with Strands) |
131+
| `port` | `int` | `9000` | Port to serve on |
132+
| `host` | `str` | `None` | Host to bind to. Auto-detected: `0.0.0.0` in Docker, `127.0.0.1` otherwise |
133+
| `task_store` | `TaskStore` | `None` | Custom task store; defaults to `InMemoryTaskStore` |
134+
| `context_builder` | `CallContextBuilder` | `None` | Custom context builder; defaults to `BedrockCallContextBuilder` |
135+
| `ping_handler` | `Callable[[], PingStatus]` | `None` | Custom health check callback |
136+
| `**kwargs` | | | Additional arguments forwarded to `uvicorn.run()` |
137+
138+
### `build_a2a_app(executor, agent_card=None, *, task_store=None, context_builder=None, ping_handler=None)`
139+
140+
Builds a Starlette ASGI application without starting a server. Useful for testing or embedding in a larger app.
141+
142+
Returns a `Starlette` application with routes:
143+
- `POST /` — A2A JSON-RPC endpoint (`message/send`, `message/stream`, `tasks/get`, `tasks/cancel`)
144+
- `GET /.well-known/agent-card.json` — Agent card discovery
145+
- `GET /ping` — Bedrock health check
146+
147+
### `build_runtime_url(agent_arn, region=None)`
148+
149+
Builds the Bedrock AgentCore runtime invocation URL from an agent ARN.
150+
151+
```python
152+
from bedrock_agentcore.runtime import build_runtime_url
153+
154+
url = build_runtime_url("arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/my-agent-abc123")
155+
# https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3A.../invocations
156+
```
157+
158+
### `BedrockCallContextBuilder`
159+
160+
Extracts Bedrock runtime headers from incoming requests and propagates them into `BedrockAgentCoreContext` contextvars. This is the default `context_builder` used by `build_a2a_app` and `serve_a2a`.
161+
162+
Headers extracted:
163+
- `X-Amzn-Bedrock-AgentCore-Runtime-Session-Id` — session ID
164+
- `X-Amzn-Bedrock-AgentCore-Runtime-Request-Id` — request ID (auto-generated UUID if missing)
165+
- `WorkloadAccessToken` — workload access token
166+
- `OAuth2CallbackUrl` — OAuth2 callback URL
167+
- `Authorization` — authorization header
168+
- `X-Amzn-Bedrock-AgentCore-Runtime-Custom-*` — custom headers
169+
170+
## Behavior Details
171+
172+
### Agent Card Auto-Population
173+
174+
When deployed on Bedrock AgentCore, the `AGENTCORE_RUNTIME_URL` environment variable is set automatically. The agent card's `url` field is updated to match, so you don't need to hardcode the deployed URL.
175+
176+
### Docker Host Detection
177+
178+
When `host` is not specified, `serve_a2a` automatically binds to `0.0.0.0` inside Docker containers (detected via `/.dockerenv` or `DOCKER_CONTAINER` env var) and `127.0.0.1` otherwise.
179+
180+
### Custom Ping Handler
181+
182+
```python
183+
from bedrock_agentcore.runtime import serve_a2a
184+
from bedrock_agentcore.runtime.models import PingStatus
185+
186+
def my_ping():
187+
if is_overloaded():
188+
return PingStatus.HEALTHY_BUSY
189+
return PingStatus.HEALTHY
190+
191+
serve_a2a(executor, ping_handler=my_ping)
192+
```
193+
194+
If the ping handler raises an exception, the server falls back to `PingStatus.HEALTHY`.

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ select = [
8686
"!src/**/*.py" = ["D"]
8787
"src/bedrock_agentcore/memory/metadata-workflow.ipynb" = ["E501"]
8888

89+
[tool.ruff.lint.isort]
90+
known-third-party = ["a2a"]
91+
8992
[tool.ruff.lint.pydocstyle]
9093
convention = "google"
9194

@@ -150,9 +153,11 @@ dev = [
150153
"wheel>=0.45.1",
151154
"strands-agents>=1.18.0",
152155
"strands-agents-evals>=0.1.0",
156+
"a2a-sdk[http-server]>=0.3",
153157
]
154158

155159
[project.optional-dependencies]
160+
a2a = ["a2a-sdk[http-server]>=0.3"]
156161
strands-agents = [
157162
"strands-agents>=1.1.0"
158163
]

src/bedrock_agentcore/runtime/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,21 @@
1414
__all__ = [
1515
"AgentCoreRuntimeClient",
1616
"BedrockAgentCoreApp",
17+
"BedrockCallContextBuilder",
1718
"RequestContext",
1819
"BedrockAgentCoreContext",
1920
"PingStatus",
21+
"build_a2a_app",
22+
"build_runtime_url",
23+
"serve_a2a",
2024
]
25+
26+
27+
def __getattr__(name: str):
28+
"""Lazy imports for A2A symbols so the a2a-sdk optional dependency is not required at import time."""
29+
_a2a_exports = {"BedrockCallContextBuilder", "build_a2a_app", "build_runtime_url", "serve_a2a"}
30+
if name in _a2a_exports:
31+
from . import a2a as _a2a_module
32+
33+
return getattr(_a2a_module, name)
34+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

0 commit comments

Comments
 (0)