Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,18 @@ The AgentCore CLI generates AWS CDK under the hood. For full infrastructure-as-c

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

Strands currently uses a2a-sdk 0.3:

```bash
pip install "bedrock-agentcore[a2a]"
```

For A2A protocol v1, install the mutually exclusive `a2a-v1` extra:

```bash
pip install "bedrock-agentcore[a2a-v1]"
```

```python
from strands import Agent
from strands.a2a import StrandsA2AExecutor
Expand Down
11 changes: 10 additions & 1 deletion docs/examples/a2a_protocol_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ This document explains how to serve your agent using the [A2A (Agent-to-Agent) p

## Installation

A2A support requires the optional `a2a` extra:
Use the `a2a` extra for a2a-sdk 0.3, including the current Strands A2A integration:

```bash
pip install "bedrock-agentcore[a2a]"
```

Use the mutually exclusive `a2a-v1` extra to opt in to A2A protocol v1:

```bash
pip install "bedrock-agentcore[a2a-v1]"
```

The runtime adapter detects the installed SDK major version. Do not install both
extras in the same environment.

## Quick Start

### Strands Agent
Expand Down
17 changes: 15 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,14 @@ dev = [
"langchain>=1.0.0",
"langgraph>=1.0.0",
"langchain-mcp-adapters>=0.1.0",
"a2a-sdk[http-server]>=0.3,<1.0",
"a2a-sdk[http-server]>=1.0.1,<2.0",
"ag-ui-protocol>=0.1.10",
"mcp-proxy-for-aws>=0.1.0",
]

[project.optional-dependencies]
a2a = ["a2a-sdk[http-server]>=0.3,<1.0"]
a2a = ["a2a-sdk[http-server]>=0.3,<0.4"]
a2a-v1 = ["a2a-sdk[http-server]>=1.0.1,<2.0"]
ag-ui = ["ag-ui-protocol>=0.1.10"]
strands-agents = [
"strands-agents>=1.20.0",
Expand All @@ -182,3 +183,15 @@ simulation = [
datasets = [
"requests>=2.31.0",
]

[tool.uv]
conflicts = [
[
{ extra = "a2a" },
{ extra = "a2a-v1" },
],
[
{ extra = "a2a" },
{ group = "dev" },
],
]
141 changes: 108 additions & 33 deletions src/bedrock_agentcore/runtime/a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import logging
import uuid
from importlib import import_module
from typing import Any, Callable, Optional

from ..config_bundle.baggage import _extract_baggage
Expand Down Expand Up @@ -34,12 +35,23 @@ def _check_a2a_sdk() -> None:
import a2a # noqa: F401
except ImportError:
raise ImportError(
'a2a-sdk is required for A2A protocol support. Install it with: pip install "bedrock-agentcore[a2a]"'
"a2a-sdk is required for A2A protocol support. Install "
'"bedrock-agentcore[a2a]" for a2a-sdk 0.3 or '
'"bedrock-agentcore[a2a-v1]" for a2a-sdk 1.x.'
) from None


def _is_a2a_v1() -> bool:
"""Return whether the installed a2a-sdk uses the v1 protocol API."""
try:
from a2a.types import StreamResponse # noqa: F401
except ImportError:
return False
return True


def _build_agent_card(executor: Any, url: str) -> Any:
Comment thread
jariy17 marked this conversation as resolved.
"""Build an AgentCard by introspecting a StrandsA2AExecutor.
"""Build an AgentCard by introspecting an executor.

Extracts name/description from ``executor.agent``. Falls back to generic
defaults for other executors.
Expand All @@ -54,15 +66,53 @@ def _build_agent_card(executor: Any, url: str) -> Any:
name = getattr(agent, "name", None) or name
description = getattr(agent, "description", None) or description

return AgentCard(
name=name,
description=description,
url=url,
version="0.1.0",
capabilities=AgentCapabilities(streaming=True),
skills=[AgentSkill(id="main", name=name, description=description, tags=["main"])],
default_input_modes=["text"],
default_output_modes=["text"],
card_kwargs = {
"name": name,
"description": description,
"version": "0.1.0",
"capabilities": AgentCapabilities(streaming=True),
"skills": [AgentSkill(id="main", name=name, description=description, tags=["main"])],
"default_input_modes": ["text"],
"default_output_modes": ["text"],
}
agent_card_type: Any = AgentCard

if not _is_a2a_v1():
return agent_card_type(url=url, **card_kwargs)

from a2a.types import AgentInterface

return agent_card_type(
**card_kwargs,
supported_interfaces=[
AgentInterface(
protocol_binding="JSONRPC",
protocol_version="1.0",
url=url,
)
],
)


def _set_jsonrpc_url(agent_card: Any, url: str) -> None:
"""Set the runtime URL on an AgentCard."""
if not _is_a2a_v1():
agent_card.url = url
return

from a2a.types import AgentInterface

for interface in agent_card.supported_interfaces:
if interface.protocol_binding == "JSONRPC":
interface.url = url
return

agent_card.supported_interfaces.append(
AgentInterface(
protocol_binding="JSONRPC",
protocol_version="1.0",
url=url,
)
)


Expand Down Expand Up @@ -97,7 +147,7 @@ def build_runtime_url(agent_arn: str, region: Optional[str] = None) -> str:
class BedrockCallContextBuilder:
"""Extracts Bedrock runtime headers and propagates them into BedrockAgentCoreContext.

Implements the a2a-sdk CallContextBuilder ABC so the A2A server
Implements the a2a-sdk ServerCallContextBuilder ABC so the A2A server
automatically calls ``build()`` on every incoming request.
"""

Expand Down Expand Up @@ -160,6 +210,8 @@ def build(self, request: Any) -> Any:
_ensure_baggage_processor_registered()

state = {
"headers": dict(headers),
"bedrock_request_id": request_id,
"request_id": request_id,
"session_id": session_id,
}
Expand All @@ -174,9 +226,13 @@ def build(self, request: Any) -> Any:
# Register as a virtual subclass so isinstance checks pass without
# requiring a2a-sdk to be importable at class-definition time.
try:
from a2a.server.apps import CallContextBuilder
if _is_a2a_v1():
from a2a.server.routes import ServerCallContextBuilder

CallContextBuilder.register(BedrockCallContextBuilder)
ServerCallContextBuilder.register(BedrockCallContextBuilder)
else:
apps_module = import_module("a2a.server.apps")
apps_module.CallContextBuilder.register(BedrockCallContextBuilder)
except Exception: # pragma: no cover
pass

Expand All @@ -196,7 +252,7 @@ def build_a2a_app(
agent_card: Optional ``a2a.types.AgentCard`` describing the agent.
If ``None``, one is built automatically by introspecting the executor.
task_store: Optional ``TaskStore``; defaults to ``InMemoryTaskStore``.
context_builder: Optional ``CallContextBuilder``; defaults to
context_builder: Optional ``ServerCallContextBuilder``; defaults to
``BedrockCallContextBuilder``.
ping_handler: Optional callback returning a ``PingStatus``.

Expand All @@ -207,35 +263,56 @@ def build_a2a_app(

_check_a2a_sdk()

from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

runtime_url = os.environ.get(AGENTCORE_RUNTIME_URL_ENV, "http://localhost:9000/")
is_a2a_v1 = _is_a2a_v1()

if agent_card is None:
agent_card = _build_agent_card(executor, runtime_url)
elif os.environ.get(AGENTCORE_RUNTIME_URL_ENV):
agent_card.url = runtime_url
_set_jsonrpc_url(agent_card, runtime_url)

if task_store is None:
task_store = InMemoryTaskStore()
if context_builder is None:
context_builder = BedrockCallContextBuilder()

http_handler = DefaultRequestHandler(
agent_executor=executor,
task_store=task_store,
)

a2a_app = A2AStarletteApplication(
agent_card=agent_card,
http_handler=http_handler,
context_builder=context_builder,
)
request_handler_type: Any = DefaultRequestHandler
if is_a2a_v1:
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes

http_handler = request_handler_type(
agent_executor=executor,
task_store=task_store,
agent_card=agent_card,
)
routes = create_agent_card_routes(agent_card)
routes.extend(
create_jsonrpc_routes(
request_handler=http_handler,
rpc_url="/",
context_builder=context_builder,
enable_v0_3_compat=True,
)
)
else:
a2a_app_type = import_module("a2a.server.apps").A2AStarletteApplication

http_handler = request_handler_type(
agent_executor=executor,
task_store=task_store,
)
a2a_app = a2a_app_type(
agent_card=agent_card,
http_handler=http_handler,
context_builder=context_builder,
)
routes = []

def _handle_ping(request: Any) -> JSONResponse:
try:
Expand All @@ -248,11 +325,9 @@ def _handle_ping(request: Any) -> JSONResponse:
status = PingStatus.HEALTHY
return JSONResponse({"status": status.value})

# Build the Starlette app with /ping included upfront, then add A2A routes,
# so we don't depend on mutating app.routes after build().
app = Starlette(routes=[Route("/ping", _handle_ping, methods=["GET"])])
a2a_app.add_routes_to_app(app)

app = Starlette(routes=[Route("/ping", _handle_ping, methods=["GET"]), *routes])
if not is_a2a_v1:
a2a_app.add_routes_to_app(app)
return app


Expand All @@ -276,7 +351,7 @@ def serve_a2a(
port: Port to serve on (default 9000).
host: Host to bind to; auto-detected if ``None``.
task_store: Optional ``TaskStore``; defaults to ``InMemoryTaskStore``.
context_builder: Optional ``CallContextBuilder``; defaults to
context_builder: Optional ``ServerCallContextBuilder``; defaults to
``BedrockCallContextBuilder``.
ping_handler: Optional callback returning a ``PingStatus``.
**kwargs: Additional arguments forwarded to ``uvicorn.run()``.
Expand Down
Loading
Loading