I tried registering A2A agents in Agent Registry and dynamically calling them from Strands Agents

I tried registering A2A agents in Agent Registry and dynamically calling them from Strands Agents

2026.04.23

This page has been translated by machine translation. View original

Introduction

Hello, I'm Jinno from the Consulting Department, and I love supermarkets.

In my previous article, I introduced how to register Agent Skills in the Agent Registry and dynamically load them from Strands Agents.

https://dev.classmethod.jp/articles/aws-agent-registry-dynamic-skills-strands-agents/

This time is the sequel, an A2A version. We'll quickly create 2 Strands Agents A2A servers deployed on AgentCore Runtime using Terraform, register them in the Agent Registry, and then dynamically call them from an orchestrator agent via the Registry.

The image looks something like this.

CleanShot 2026-04-22 at 10.12.06@2x

Since the orchestrator side doesn't hold any URLs or tool definitions for A2A partners and can dynamically reference other agents' information via the Registry, the code can remain unchanged even when the number of agents to support increases or decreases — that's the nice part.

The more A2A agents grow across the organization, the more the orchestrator's capabilities automatically expand as well. Of course, there's a trade-off with the latency at runtime and the difficulty of predicting what gets called due to dynamic referencing, so that depends on the use case. As capabilities increase, tuning the orchestrator might also become more challenging...

Prerequisites

The versions used this time are as follows.

Item Version / Setting
Python 3.12
Package Management uv
Terraform >= 1.5.0
AWS Provider >= 6.25.0
strands-agents[a2a] >= 1.36.0
strands-agents-tools >= 0.5.0
Region us-east-1
Model us.anthropic.claude-haiku-4-5-20251001-v1:0

Authentication to the AgentCore Runtime A2A server is done via IAM (SigV4).

We'll use the auto-approval Registry created in the previous article as-is. Please keep a note of the Registry ID. Refer to the previous article for instructions on creating the Registry.

The complete code used this time is available in this repository. Please clone it before proceeding if you want to try it locally.

https://github.com/yuu551/a2a-agentcore-strands

git clone https://github.com/yuu551/a2a-agentcore-strands.git
cd a2a-agentcore-strands
uv sync

Overall Flow

The implementation flow proceeds as follows!

  1. Deploy A2A servers to ECR & AgentCore Runtime using Terraform (Calculator / Weather)
  2. Register A2A Agent Cards in the Agent Registry
  3. Local orchestrator agent searches the Registry and connects to A2A servers
  4. Mount the orchestrator on AgentCore Runtime as well, and call Runtime→Runtime

The file structure looks like this.

Project Structure
.
├── register_a2a.py             # Register A2A Agent Cards to Registry
├── registry_a2a_loader.py      # Build A2AClientToolProvider from Registry
├── a2a_client.py               # Run orchestrator locally
├── invoke_orchestrator.py      # Call orchestrator on Runtime via SSE
├── sigv4_auth.py               # SigV4 Auth for httpx
└── terraform/
    ├── versions.tf
    ├── variables.tf
    ├── iam.tf
    ├── runtime.tf
    ├── outputs.tf
    └── agent/
        ├── calculator/         # A2A: Calculator agent
   ├── server.py
   ├── Dockerfile
   └── requirements.txt
        ├── weather/            # A2A: Weather agent
   ├── server.py
   ├── Dockerfile
   └── requirements.txt
        └── orchestrator/       # HTTP: Orchestrator
            ├── server.py
            ├── registry_a2a_loader.py  # Copied from root
            ├── sigv4_auth.py           # Copied from root
            ├── Dockerfile
            └── requirements.txt

A2A Server Implementation

Calculator Agent

We wrap the Agent with Strands Agents' A2AServer and expose it as a FastAPI application. The calculation tool uses strands_tools.calculator provided by Strands as-is.

AgentCore Runtime injects the environment variable AGENTCORE_RUNTIME_URL for A2A, so we pass it to the http_url of A2AServer. This URL goes into the url field of the Agent Card, and the client sends JSON-RPC to that endpoint.

terraform/agent/calculator/server.py
from __future__ import annotations

import os

import uvicorn
from strands import Agent
from strands.models import BedrockModel
from strands.multiagent.a2a import A2AServer
from strands_tools.calculator import calculator

MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
REGION = os.environ.get("AWS_REGION", "us-east-1")
RUNTIME_URL = os.environ.get("AGENTCORE_RUNTIME_URL", "http://127.0.0.1:9000/")

model = BedrockModel(model_id=MODEL_ID, region_name=REGION)

agent = Agent(
    name="計算エージェント",
    description="四則演算・代数・微積分・記号計算を行うエージェントです。",
    model=model,
    tools=[calculator],
    callback_handler=None,
)

server = A2AServer(
    agent=agent,
    host="0.0.0.0",
    port=9000,
    http_url=RUNTIME_URL,
    serve_at_root=True,
)

app = server.to_fastapi_app()

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=9000)

To run A2A on AgentCore Runtime, port 9000 and root path are required. A2AServer automatically creates endpoints for /ping and /.well-known/agent-card.json, so there's no need to write them yourself.

Weather Agent

The second agent returns weather information. This time we'll tool-ify a function that returns fixed mock data.

terraform/agent/weather/server.py
from __future__ import annotations

import os

import uvicorn
from strands import Agent, tool
from strands.models import BedrockModel
from strands.multiagent.a2a import A2AServer

MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
REGION = os.environ.get("AWS_REGION", "us-east-1")
RUNTIME_URL = os.environ.get("AGENTCORE_RUNTIME_URL", "http://127.0.0.1:9000/")

_MOCK_WEATHER = {
    "tokyo": {"condition": "Sunny", "temperature_c": 22, "humidity": 55},
    "osaka": {"condition": "Cloudy", "temperature_c": 20, "humidity": 65},
    "sapporo": {"condition": "Snowy", "temperature_c": -2, "humidity": 80},
    "fukuoka": {"condition": "Rainy", "temperature_c": 18, "humidity": 75},
    "naha": {"condition": "Sunny", "temperature_c": 27, "humidity": 70},
}

@tool
def get_weather(city: str) -> dict:
    """Returns current weather information for cities in Japan. Supported cities: Tokyo, Osaka, Sapporo, Fukuoka, Naha."""
    data = _MOCK_WEATHER.get(city.strip().lower())
    if data is None:
        return {"error": f"Unknown city: {city}. Supported: {list(_MOCK_WEATHER)}"}
    return {"city": city, **data}

model = BedrockModel(model_id=MODEL_ID, region_name=REGION)

agent = Agent(
    name="天気エージェント",
    description="日本の主要都市の現在の天気情報を返すエージェントです。",
    model=model,
    tools=[get_weather],
    callback_handler=None,
)

server = A2AServer(
    agent=agent,
    host="0.0.0.0",
    port=9000,
    http_url=RUNTIME_URL,
    serve_at_root=True,
)

app = server.to_fastapi_app()

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=9000)

Common Dockerfile

Both agents use the same Dockerfile structure. Since AgentCore Runtime requires ARM64, don't forget --platform=linux/arm64.

terraform/agent/calculator/Dockerfile
FROM --platform=linux/arm64 public.ecr.aws/docker/library/python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY server.py .

EXPOSE 9000

CMD ["python", "server.py"]
terraform/agent/calculator/requirements.txt
strands-agents[a2a]>=1.36.0
strands-agents-tools>=0.5.0
fastapi>=0.115.0
uvicorn>=0.32.0

Deploy with Terraform

We'll create 2 ECRs with Docker build & push, and 2 agents running on AgentCore Runtime. Specify server_protocol = "A2A" in the Runtime's protocol_configuration.

terraform/runtime.tf
locals {
  agents = {
    calculator = { source_dir = "${path.module}/agent/calculator" }
    weather    = { source_dir = "${path.module}/agent/weather" }
  }
}

resource "aws_ecr_repository" "calculator" {
  name                 = "${var.project_name}-calculator-${random_string.suffix.result}"
  image_tag_mutability = "MUTABLE"
  force_delete         = true
}

resource "aws_ecr_repository" "weather" {
  name                 = "${var.project_name}-weather-${random_string.suffix.result}"
  image_tag_mutability = "MUTABLE"
  force_delete         = true
}

resource "null_resource" "calculator_build_push" {
  triggers = {
    dockerfile   = filemd5("${local.agents.calculator.source_dir}/Dockerfile")
    server       = filemd5("${local.agents.calculator.source_dir}/server.py")
    requirements = filemd5("${local.agents.calculator.source_dir}/requirements.txt")
  }

  provisioner "local-exec" {
    working_dir = local.agents.calculator.source_dir
    command     = <<-EOT
      aws ecr get-login-password --region ${data.aws_region.current.region} | \
        docker login --username AWS --password-stdin ${aws_ecr_repository.calculator.repository_url}
      docker buildx build --platform linux/arm64 \
        -t ${aws_ecr_repository.calculator.repository_url}:latest --push .
    EOT
  }

  depends_on = [aws_ecr_repository.calculator]
}

# weather_build_push has the same structure, so it's omitted

resource "aws_bedrockagentcore_agent_runtime" "calculator" {
  agent_runtime_name = replace("${var.project_name}_calculator_${random_string.suffix.result}", "-", "_")
  description        = "Calculator A2A server"
  role_arn           = aws_iam_role.agent_runtime.arn

  agent_runtime_artifact {
    container_configuration {
      container_uri = "${aws_ecr_repository.calculator.repository_url}:latest"
    }
  }

  network_configuration {
    network_mode = "PUBLIC"
  }

  protocol_configuration {
    server_protocol = "A2A"
  }

  depends_on = [
    null_resource.calculator_build_push,
    aws_iam_role_policy.runtime_ecr,
    aws_iam_role_policy.runtime_logs,
    aws_iam_role_policy.runtime_bedrock,
  ]
}

# aws_bedrockagentcore_agent_runtime.weather has the same structure, so it's omitted

The Runtime execution role grants three types of permissions: ECR pull, CloudWatch Logs write, and Bedrock InvokeModel.

terraform/iam.tf
resource "aws_iam_role" "agent_runtime" {
  name = "${var.project_name}-runtime-${random_string.suffix.result}"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Action    = "sts:AssumeRole"
      Principal = { Service = "bedrock-agentcore.amazonaws.com" }
      Condition = {
        StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id }
      }
    }]
  })
}

resource "aws_iam_role_policy" "runtime_bedrock" {
  name = "bedrock-invoke"
  role = aws_iam_role.agent_runtime.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
      Resource = "*"
    }]
  })
}

# runtime_ecr / runtime_logs are omitted. See the GitHub repository for details

In outputs, we expose the Runtime ARN for A2A use.

terraform/outputs.tf
output "calculator_runtime_arn" {
  value = aws_bedrockagentcore_agent_runtime.calculator.agent_runtime_arn
}

output "weather_runtime_arn" {
  value = aws_bedrockagentcore_agent_runtime.weather.agent_runtime_arn
}

The URL passed to the client is assembled in the form https://bedrock-agentcore.<region>.amazonaws.com/runtimes/<arn>/invocations (the ARN is URL-encoded with urllib.parse.quote).

Let's deploy.

cd terraform
terraform init
terraform apply

Once deployment is complete, register these agents in the Registry.

Register A2A Agent Cards in the Registry

A2A descriptor structure

The descriptorType in Agent Registry has A2A available. The A2A descriptor puts the Agent Card JSON in a2a.agentCard.inlineContent.

Before registering, we fetch the actual Agent Card from the Runtime and register its contents in the Registry. This registration allows the client side to know the agent name, skills, connection URL, and so on.

For A2A details, please refer to the blog I wrote previously.

https://dev.classmethod.jp/articles/strands-agents-amazon-bedrock-agentcore-a2a/

SigV4 Authentication Helper

Since the Runtime uses IAM authentication for bedrock-agentcore, both Agent Card retrieval and JSON-RPC calls require SigV4 signing. We'll create a custom httpx Auth.

sigv4_auth.py
from __future__ import annotations

import boto3
import httpx
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

class SigV4HttpxAuth(httpx.Auth):
    requires_request_body = True

    def __init__(self, region: str, service: str = "bedrock-agentcore") -> None:
        credentials = boto3.Session().get_credentials()
        if credentials is None:
            raise RuntimeError("No AWS credentials found.")
        self._signer = SigV4Auth(credentials, service, region)

    def auth_flow(self, request: httpx.Request):
        aws_request = AWSRequest(
            method=request.method,
            url=str(request.url),
            data=request.content,
            headers=dict(request.headers),
        )
        self._signer.add_auth(aws_request)
        for key, value in aws_request.headers.items():
            request.headers[key] = value
        yield request

Note that if requires_request_body = True is not set, the JSON-RPC body won't be included in the signature target, causing 4xx errors on POST requests.

Registration Script

register_a2a.py
"""Register A2A agents (Calculator / Weather) to AWS Agent Registry."""
from __future__ import annotations

import json
import logging
import os
import time
from urllib.parse import quote

import boto3
import httpx

from sigv4_auth import SigV4HttpxAuth

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

REGISTRY_ID = os.environ["AGENT_REGISTRY_ID"]
REGION = os.environ.get("AWS_REGION", "us-east-1")

def runtime_base_url(arn: str) -> str:
    return f"https://bedrock-agentcore.{REGION}.amazonaws.com/runtimes/{quote(arn, safe='')}/invocations"

def fetch_agent_card(base_url: str, auth: SigV4HttpxAuth) -> dict:
    card_url = f"{base_url}/.well-known/agent-card.json"
    response = httpx.get(card_url, auth=auth, timeout=30.0)
    response.raise_for_status()
    return response.json()

def wait_until_not(control, registry_id: str, record_id: str, transient: str, *, timeout: float = 30.0) -> str:
    deadline = time.monotonic() + timeout
    while True:
        status = control.get_registry_record(registryId=registry_id, recordId=record_id)["status"]
        if status != transient:
            return status
        if time.monotonic() > deadline:
            raise TimeoutError(f"record {record_id} stuck in {transient}")
        time.sleep(1.0)

def register(control, name: str, description: str, card: dict) -> str:
    created = control.create_registry_record(
        registryId=REGISTRY_ID,
        name=name,
        description=description,
        recordVersion="1.0",
        descriptorType="A2A",
        descriptors={"a2a": {"agentCard": {"inlineContent": json.dumps(card)}}},
    )
    record_id = created["recordArn"].rsplit("/", 1)[-1]
    logger.info("Created record %s (status=%s)", record_id, created["status"])

    wait_until_not(control, REGISTRY_ID, record_id, "CREATING")
    control.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=record_id)
    status = wait_until_not(control, REGISTRY_ID, record_id, "SUBMITTING")
    logger.info("Record %s reached status %s", record_id, status)
    return record_id

def main() -> None:
    control = boto3.client("bedrock-agentcore-control", region_name=REGION)
    auth = SigV4HttpxAuth(region=REGION)

    targets = [
        ("calculator-a2a", "Calculator A2A agent (SymPy)", os.environ["CALCULATOR_RUNTIME_ARN"]),
        ("weather-a2a", "Weather A2A agent (mock for Japan cities)", os.environ["WEATHER_RUNTIME_ARN"]),
    ]

    for name, description, arn in targets:
        base = runtime_base_url(arn)
        card = fetch_agent_card(base, auth)
        logger.info("Fetched agent card for %s: %s", name, card["name"])
        record_id = register(control, name, description, card)
        print(f"{name}: {record_id}")

if __name__ == "__main__":
    main()

We assemble the base URL from the Runtime ARN, fetch the Agent Card, and register it directly in the Registry. Since this Registry is set to auto-approve, the flow goes create → submit → auto-approve → APPROVED.

Execution Command
export AGENT_REGISTRY_ID="xxx"
export CALCULATOR_RUNTIME_ARN="$(cd terraform && terraform output -raw calculator_runtime_arn)"
export WEATHER_RUNTIME_ARN="$(cd terraform && terraform output -raw weather_runtime_arn)"
uv run python register_a2a.py
Execution Result
INFO: Fetched agent card for calculator-a2a: 計算エージェント
INFO: Created record ul4ncy9CGo0p (status=CREATING)
INFO: Record ul4ncy9CGo0p reached status APPROVED
INFO: Fetched agent card for weather-a2a: 天気エージェント
INFO: Created record vZspJcEwxhy5 (status=CREATING)
INFO: Record vZspJcEwxhy5 reached status APPROVED
calculator-a2a: ul4ncy9CGo0p
weather-a2a: vZspJcEwxhy5

Dynamically Load A2A Agents from Registry

A2A Loader

This loader searches the Registry for A2A records, extracts the url field from the Agent Card, and builds an A2AClientToolProvider.

registry_a2a_loader.py
"""Search AWS Agent Registry for A2A records and build A2AClientToolProvider."""
from __future__ import annotations

import json
import logging
from dataclasses import dataclass
from typing import Any

import boto3
from strands_tools.a2a_client import A2AClientToolProvider

from sigv4_auth import SigV4HttpxAuth

logger = logging.getLogger(__name__)

@dataclass
class RegistryA2ALoaderConfig:
    registry_id: str
    region_name: str = "us-east-1"
    max_results: int = 10

class RegistryA2ALoader:
    def __init__(self, config: RegistryA2ALoaderConfig) -> None:
        self._config = config
        self._client = boto3.client("bedrock-agentcore", region_name=config.region_name)

    def search(self, query: str) -> A2AClientToolProvider:
        response = self._client.search_registry_records(
            registryIds=[self._config.registry_id],
            searchQuery=query,
            maxResults=self._config.max_results,
            filters={"descriptorType": {"$eq": "A2A"}},
        )
        records = response.get("registryRecords") or []
        logger.info("Fetched %d A2A record(s) from registry", len(records))

        urls: list[str] = []
        for record in records:
            url = self._extract_url(record)
            if url:
                urls.append(url)
                logger.info("Loaded A2A: %s -> %s", record.get("name"), url[:80])

        auth = SigV4HttpxAuth(region=self._config.region_name)
        return A2AClientToolProvider(
            known_agent_urls=urls,
            httpx_client_args={"auth": auth},
        )

    @staticmethod
    def _extract_url(record: dict[str, Any]) -> str | None:
        descriptors = record.get("descriptors") or {}
        a2a_desc = descriptors.get("a2a") or {}
        card_json = (a2a_desc.get("agentCard") or {}).get("inlineContent")
        if not card_json:
            return None
        card = json.loads(card_json)
        return card.get("url")

The flow is simple: search Registry → extract url from Agent Card → pass to A2AClientToolProvider. Authentication is configured via httpx_client_args.

Orchestrator

We bind A2A tools retrieved from the Registry to a local Strands Agent and try executing a compound task.
We'll send an instruction like: Please tell me the weather in Tokyo. Then, convert the current temperature (Celsius) to Fahrenheit (Fahrenheit = Celsius × 9 / 5 + 32).

a2a_client.py
"""Orchestrator Strands Agent that loads A2A tools from Agent Registry."""
from __future__ import annotations

import logging
import os

from strands import Agent
from strands.models import BedrockModel

from registry_a2a_loader import RegistryA2ALoader, RegistryA2ALoaderConfig

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"

def main() -> None:
    region = os.environ.get("AWS_REGION", "us-east-1")

    config = RegistryA2ALoaderConfig(
        registry_id=os.environ["AGENT_REGISTRY_ID"],
        region_name=region,
    )
    loader = RegistryA2ALoader(config)
    provider = loader.search("calculator weather")

    model = BedrockModel(model_id=MODEL_ID, region_name=region)

    agent = Agent(
        model=model,
        tools=provider.tools,
        system_prompt=(
            "You are an orchestrator that delegates tasks to remote A2A agents. "
            "Use the A2A tools to send requests to the appropriate agent discovered from the registry."
        ),
    )

    user_input = (
        "東京の天気を教えてください。その後、現在の気温(摂氏)を華氏に変換してください(華氏 = 摂氏 × 9 / 5 + 32)。"
    )

    logger.info("Query: %s", user_input)
    result = agent(user_input)
    print("\n--- result ---")
    print(result.message)

if __name__ == "__main__":
    main()

Operation Verification

Now that the implementation is complete, let's run it. For the ID, specify the Registry created in advance.

Execution Command
AGENT_REGISTRY_ID="xxx" uv run python a2a_client.py
Execution Log
registry_a2a_loader INFO: Fetched 2 A2A record(s) from registry
registry_a2a_loader INFO: Loaded A2A: calculator-a2a -> https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/...
registry_a2a_loader INFO: Loaded A2A: weather-a2a -> https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/...
strands_tools.a2a_client INFO: Successfully discovered and cached agent card for (Weather)
strands_tools.a2a_client INFO: Sending message to (Weather)
strands_tools.a2a_client INFO: Successfully discovered and cached agent card for (Calculator)
strands_tools.a2a_client INFO: Sending message to (Calculator)
Execution Result
## Tokyo Weather Information
- Weather: Sunny
- Temperature: 22°C
- Humidity: 55%

## Temperature Conversion
Converting Celsius 22°C to Fahrenheit:
Formula: Fahrenheit = Celsius × 9 / 5 + 32
22 × 9 ÷ 5 + 32 = 71.6°F

Therefore, Tokyo's current temperature of 22°C is 71.6°F in Fahrenheit!

The orchestrator used A2A tools retrieved from the Registry to sequentially call the weather agent → calculator agent, successfully completing the composite task!

It's great that the agent-side code has no individual tool definitions at all, yet can integrate using only the endpoint information dynamically retrieved from the Registry.

Hosting the Orchestrator on AgentCore Runtime

Up until now we've been running the orchestrator locally, but the same logic can be hosted on AgentCore Runtime with the HTTP protocol and served as-is. You just need to wrap it with BedrockAgentCoreApp from the bedrock-agentcore SDK.

terraform/agent/orchestrator/server.py
from __future__ import annotations

import logging
import os

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands.models import BedrockModel

from registry_a2a_loader import RegistryA2ALoader, RegistryA2ALoaderConfig

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
REGION = os.environ.get("AWS_REGION", "us-east-1")
REGISTRY_ID = os.environ["AGENT_REGISTRY_ID"]

app = BedrockAgentCoreApp()

@app.entrypoint
async def handler(payload: dict, context):
    query = payload.get("prompt")
    if not query:
        yield {"error": "`prompt` is required"}
        return

    search_query = payload.get("search_query", query)

    config = RegistryA2ALoaderConfig(registry_id=REGISTRY_ID, region_name=REGION)
    loader = RegistryA2ALoader(config)
    provider = loader.search(search_query)

    model = BedrockModel(model_id=MODEL_ID, region_name=REGION)
    agent = Agent(
        model=model,
        tools=provider.tools,
        system_prompt=(
            "You are an orchestrator that delegates tasks to remote A2A agents."
        ),
    )

    async for event in agent.stream_async(query):
        yield event

if __name__ == "__main__":
    app.run()

Configure BedrockAgentCoreApp and implement a simple agent that integrates via A2A through the Registry.

The Dockerfile is also implemented as follows.

terraform/agent/orchestrator/Dockerfile
FROM --platform=linux/arm64 public.ecr.aws/docker/library/python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir aws-opentelemetry-distro==0.12.2

COPY server.py .
COPY registry_a2a_loader.py .
COPY sigv4_auth.py .

EXPOSE 8080

CMD ["opentelemetry-instrument", "python", "server.py"]

On the Terraform side, add a new Runtime with server_protocol = "HTTP". The Registry ID is set via environment variable.

terraform/runtime.tf (excerpt)
resource "aws_bedrockagentcore_agent_runtime" "orchestrator" {
  agent_runtime_name = replace("${var.project_name}_orchestrator_${random_string.suffix.result}", "-", "_")
  description        = "Orchestrator HTTP server calling A2A agents via Registry"
  role_arn           = aws_iam_role.orchestrator_runtime.arn

  agent_runtime_artifact {
    container_configuration {
      container_uri = "${aws_ecr_repository.orchestrator.repository_url}:latest"
    }
  }

  network_configuration {
    network_mode = "PUBLIC"
  }

  protocol_configuration {
    server_protocol = "HTTP"
  }

  environment_variables = {
    AGENT_REGISTRY_ID = var.agent_registry_id
  }
}

GetAgentCard Permission is Also Required

When connecting via A2A to other AgentCore Runtimes, bedrock-agentcore:InvokeAgentRuntime alone is not enough. According to the A2A specification, it first calls GET /.well-known/agent-card.json to retrieve agent information, and this API has a separate action bedrock-agentcore:GetAgentCard assigned to it.

I'm noting this as a heads-up because I ran into the pattern where Registry search works fine but then get a 403 Forbidden at the Agent Card retrieval stage. Worth being careful about.

terraform/iam.tf (excerpt)
resource "aws_iam_role_policy" "orchestrator_invoke_runtime" {
  name = "invoke-a2a-runtime"
  role = aws_iam_role.orchestrator_runtime.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "bedrock-agentcore:InvokeAgentRuntime",
        "bedrock-agentcore:InvokeAgentRuntimeForUser",
        "bedrock-agentcore:GetAgentCard",  # ← Required for A2A Agent Card retrieval
      ]
      Resource = [
        aws_bedrockagentcore_agent_runtime.calculator.agent_runtime_arn,
        "${aws_bedrockagentcore_agent_runtime.calculator.agent_runtime_arn}/runtime-endpoint/*",
        aws_bedrockagentcore_agent_runtime.weather.agent_runtime_arn,
        "${aws_bedrockagentcore_agent_runtime.weather.agent_runtime_arn}/runtime-endpoint/*",
      ]
    }]
  })
}

Invoking the Orchestrator (via Runtime)

Let's try invoking the orchestrator deployed on AgentCore.
Prepare a simple client invoke_orchestrator.py for formatting the response and run it.

invoke_orchestrator.py
"""Invoke the Orchestrator Runtime and pretty-print the SSE stream."""
from __future__ import annotations

import json
import os
import sys

import boto3

REGION = os.environ.get("AWS_REGION", "us-east-1")

def stream_events(body: bytes):
    for chunk in body.decode("utf-8", errors="replace").split("\n\n"):
        chunk = chunk.strip()
        if not chunk.startswith("data:"):
            continue
        try:
            yield json.loads(chunk[5:].strip())
        except json.JSONDecodeError:
            continue

def pretty_print(body: bytes) -> None:
    tool_use: dict[str, dict] = {}
    for event in stream_events(body):
        if not isinstance(event, dict):
            continue
        ev = event.get("event") or {}

        if "contentBlockStart" in ev:
            tu = (ev["contentBlockStart"].get("start") or {}).get("toolUse")
            if tu:
                tool_use[tu["toolUseId"]] = {"name": tu.get("name"), "input": ""}
                print(f"\n[tool_call] {tu.get('name')}", flush=True)

        if "contentBlockDelta" in ev:
            delta = ev["contentBlockDelta"].get("delta") or {}
            if "text" in delta:
                sys.stdout.write(delta["text"])
                sys.stdout.flush()
            if "toolUse" in delta:
                frag = delta["toolUse"].get("input", "")
                for tu in tool_use.values():
                    tu["input"] += frag
                    break

        if "contentBlockStop" in ev:
            for tu in list(tool_use.values()):
                if tu["input"]:
                    try:
                        args = json.loads(tu["input"])
                        print(f"  args: {json.dumps(args, ensure_ascii=False)[:200]}", flush=True)
                    except json.JSONDecodeError:
                        pass
                    tu["input"] = ""

        if "messageStop" in ev:
            print(flush=True)

def main() -> None:
    arn = os.environ["ORCHESTRATOR_RUNTIME_ARN"]
    prompt = " ".join(sys.argv[1:]) or "Tell me the weather in Tokyo and convert the temperature to Fahrenheit."
    search_query = os.environ.get("SEARCH_QUERY", "calculator weather")

    client = boto3.client("bedrock-agentcore", region_name=REGION)
    response = client.invoke_agent_runtime(
        agentRuntimeArn=arn,
        qualifier="DEFAULT",
        payload=json.dumps({"prompt": prompt, "search_query": search_query}).encode(),
        contentType="application/json",
    )
    pretty_print(response["response"].read())

if __name__ == "__main__":
    main()

Let's verify this works.

Execution Command
export ORCHESTRATOR_RUNTIME_ARN="$(cd terraform && terraform output -raw orchestrator_runtime_arn)"
uv run python invoke_orchestrator.py "東京の天気を教えて、気温を華氏に変換して。"
Execution Result
I'll help you with Tokyo's weather information and temperature conversion to Fahrenheit. First, let me check the available agents.
[tool_call] a2a_list_discovered_agents

Perfect! The weather agent and calculator agent are available. I'll retrieve Tokyo's weather information and then convert the temperature to Fahrenheit.
[tool_call] a2a_send_message
  args: {"message_text": "東京の現在の天気情報を教えてください。", "target_agent_url": "https://.../a2a_demo_weather_.../invocations/"}

Excellent! I've retrieved Tokyo's weather information. Next, I'll convert the temperature of 22°C to Fahrenheit.
[tool_call] a2a_send_message
  args: {"message_text": "22度セルシウスを華氏に変換してください。計算式は (22 * 9/5) + 32 です。", "target_agent_url": "https://.../a2a_demo_calculator_.../invocations/"}

Perfect! Tokyo's weather information and temperature conversion to Fahrenheit are complete.

## 📍 Tokyo Weather Information

**Current Weather:**
- **Weather:** Sunny
- **Temperature:** 22°C **71.6°F (Fahrenheit)**
- **Humidity:** 55%

The nice weather continues with a comfortable temperature. 22 degrees Celsius is 71.6 degrees Fahrenheit, which is a comfortable room temperature.

You can confirm the flow of retrieving the agent list from the Registry via a2a_list_discovered_agents → sending a message to the weather agent → sending a message to the calculator agent!

Looking at the CloudWatch Logs on the Runtime side, I was also able to confirm the internal flow of Registry search → Agent Card retrieval → JSON-RPC transmission being executed!

Orchestrator Log Inside Runtime (excerpt)
Fetched 2 A2A record(s) from registry
Loaded A2A: calculator-a2a -> https://bedrock-agentcore.us-east-1.amazonaws.com/...
Loaded A2A: weather-a2a -> https://bedrock-agentcore.us-east-1.amazonaws.com/...
Successfully discovered and cached agent card for ...weather...
Successfully discovered and cached agent card for ...calculator...
Sending message to ...weather...
Sending message to ...calculator...

Closing Remarks

Following the previous Skills version, this time I tried dynamic loading with the A2A version.
It's interesting that an orchestrator-type agent doesn't need to directly hold information about the A2A targets it wants to connect with, but can reference them via the Registry and dynamically connect to what it needs. It seems like it would be easier to manage as the number of connected components grows. It also looks useful as a foundation for centrally managing approved agents within an organization. (It's a shame that Registry doesn't support HTTPS — I hope that gets supported someday...)

I hope this article was helpful in some way. Thank you for reading to the end!

Share this article

AWSのお困り事はクラスメソッドへ