Skip to content

feat: add A2A protocol support via serve_a2a#349

Merged
tejaskash merged 9 commits intomainfrom
feat/a2a-serve-a2a
Mar 17, 2026
Merged

feat: add A2A protocol support via serve_a2a#349
tejaskash merged 9 commits intomainfrom
feat/a2a-serve-a2a

Conversation

@tejaskash
Copy link
Contributor

@tejaskash tejaskash commented Mar 16, 2026

Summary

  • Adds serve_a2a, build_a2a_app, build_runtime_url, and BedrockCallContextBuilder to bedrock_agentcore.runtime
  • ~130 lines of Bedrock-specific glue delegating protocol handling to the official a2a-sdk
  • Optional dependency: pip install "bedrock-agentcore[a2a]" — lazy-loaded so non-A2A users are unaffected
  • Auto-builds AgentCard from StrandsA2AExecutor when not provided
  • Auto-populates agent_card.url from AGENTCORE_RUNTIME_URL env var on deploy
  • build_runtime_url(agent_arn) utility for constructing invocation URLs from ARNs
  • Docker/container host detection for 0.0.0.0 vs 127.0.0.1
  • /ping health check endpoint with Bedrock-compatible response format
  • Propagates Bedrock session, request, token, and custom headers via BedrockAgentCoreContext contextvars
  • Documentation: docs/examples/a2a_protocol_examples.md + README section

Why

PR #217 reimplemented A2A from scratch (~1700 lines). The industry (ADK, Strands, CrewAI) has converged on the official a2a-sdk. This replaces that approach with minimal glue that delegates protocol handling to the SDK.

Strands

from strands import Agent
from strands.multiagent.a2a.executor import StrandsA2AExecutor

from bedrock_agentcore.runtime import serve_a2a

agent = Agent(
    name="Calculator Agent",
    description="A calculator agent that can perform basic arithmetic operations.",
    callback_handler=None,
)

if __name__ == "__main__":
    serve_a2a(StrandsA2AExecutor(agent))

Google ADK

from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService

from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from bedrock_agentcore.runtime import serve_a2a

agent = Agent(
    name="calculator_agent",
    model="gemini-2.0-flash",
    description="A calculator agent that can perform basic arithmetic operations.",
    instruction="You are a helpful calculator. Answer math questions clearly and concisely.",
)

runner = Runner(
    app_name=agent.name,
    agent=agent,
    session_service=InMemorySessionService(),
)

card = AgentCard(
    name=agent.name,
    description=agent.description,
    url="http://localhost:9000/",
    version="0.1.0",
    capabilities=AgentCapabilities(streaming=True),
    skills=[AgentSkill(id="calc", name="calculator", description="Arithmetic operations", tags=["math"])],
    default_input_modes=["text"],
    default_output_modes=["text"],
)

if __name__ == "__main__":
    serve_a2a(A2aAgentExecutor(runner=runner), card)

LangGraph

from langchain_aws import ChatBedrockConverse
from langgraph.prebuilt import create_react_agent

from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import AgentCapabilities, AgentCard, AgentSkill, Part, TextPart
from a2a.utils import new_task
from bedrock_agentcore.runtime import serve_a2a

llm = ChatBedrockConverse(model="us.anthropic.claude-sonnet-4-20250514")

graph = create_react_agent(llm, tools=[], prompt="You are a helpful calculator.")


class LangGraphA2AExecutor(AgentExecutor):
    """Wraps a LangGraph CompiledGraph as an a2a-sdk AgentExecutor."""

    def __init__(self, graph):
        self.graph = graph

    async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
        task = context.current_task or new_task(context.message)
        if not context.current_task:
            await event_queue.enqueue_event(task)
        updater = TaskUpdater(event_queue, task.id, task.context_id)

        user_text = context.get_user_input()
        result = await self.graph.ainvoke({"messages": [("user", user_text)]})
        response = result["messages"][-1].content

        await updater.add_artifact([Part(root=TextPart(text=response))])
        await updater.complete()

    async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
        pass


card = AgentCard(
    name="langgraph-agent",
    description="A LangGraph agent on Bedrock AgentCore",
    url="http://localhost:9000/",
    version="0.1.0",
    capabilities=AgentCapabilities(streaming=True),
    skills=[AgentSkill(id="calc", name="calculator", description="Arithmetic operations", tags=["math"])],
    default_input_modes=["text"],
    default_output_modes=["text"],
)

if __name__ == "__main__":
    serve_a2a(LangGraphA2AExecutor(graph), card)

Test plan

  • 24 unit tests in tests/bedrock_agentcore/runtime/test_a2a.py
  • 10 integration tests in tests/integration/runtime/test_a2a_integration.py
  • Full test suite passes (1112 passed, 1 skipped)
  • Deployed and verified on Bedrock AgentCore with Strands, ADK, and LangGraph executors
  • Agent card URL auto-populated correctly from AGENTCORE_RUNTIME_URL
  • ruff check / ruff format clean

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)
@sundargthb
Copy link
Contributor

Your proposal listed three components: serve_a2a, BedrockCallContextBuilder, and build_runtime_url(agent_arn, region).

The third one isn’t here. Intentionally dropped, or coming in a follow-up? Customers deploying to AgentCore need to construct the runtime URL from their ARN — without this utility they’ll do it themselves (and can get the URL-encoding of the ARN wrong).

…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().
@tejaskash
Copy link
Contributor Author

Addressed review comment from @sundargthb in 8588bb9: refactored to build the Starlette app with /ping included upfront in the constructor, then use a2a_app.add_routes_to_app(app) to wire the A2A endpoints. No more post-build() route mutation.

Copy link
Contributor

@notgitika notgitika left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on @sundargthb's earlier comment. what's the plan for build_runtime_url(agent_arn, region)?

If it's coming in a follow-up, might be worth tracking it in an issue so it doesn't get lost.

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.
@tejaskash
Copy link
Contributor Author

tejaskash commented Mar 17, 2026

Added build_runtime_url(agent_arn, region=None) in 1892db0. Extracts region from the ARN, URL-encodes the ARN, and returns the full invocation URL. Exported from bedrock_agentcore.runtime.

from bedrock_agentcore.runtime import build_runtime_url

url = build_runtime_url('arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/my-agent-abc123')
# https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3A.../invocations

@sundargthb and @notgitika this addresses your comment about the missing third component from the proposal.

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.
Copy link
Contributor

@notgitika notgitika left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tejaskash tejaskash merged commit be1be55 into main Mar 17, 2026
23 checks passed
@tejaskash tejaskash deleted the feat/a2a-serve-a2a branch March 17, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants