I registered an A2A agent in the Agent Registry and tried calling it dynamically from Strands Agents

I registered an A2A agent in the Agent Registry and tried calling it dynamically from Strands Agents

2026.04.22

This page has been translated by machine translation. View original

Introduction

Hello, I'm Jinno from the consulting department who loves supermarkets.

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

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

This time, as a continuation, I'll cover the A2A version. We'll quickly create two Strands Agents A2A servers deployed to AgentCore Runtime using Terraform, register them with Agent Registry, and call them dynamically from an orchestrator agent via the Registry.

Here's a visual representation:

CleanShot 2026-04-22 at 10.12.06@2x

The pleasant point is that the orchestrator doesn't need to hold any URLs or tool definitions for A2A interactions, as it can dynamically reference other agents' information from the Registry. This means the code can be used as is even when the number of agents you want to support increases or decreases.

As the number of A2A agents increases throughout the organization, what the orchestrator can do automatically increases as well. Of course, there's a trade-off with the latency at execution time and the unpredictability of what gets called, so it depends on the use case. It seems like orchestrator tuning will become more challenging as capabilities increase...

Prerequisites

Here are the versions used in this article:

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

We'll authenticate to the AgentCore Runtime A2A server using IAM (SigV4).

We'll continue using the auto-approval Registry created in the previous article. Please keep note of your Registry ID. For details on creating a Registry, refer to my previous article.

The complete code used in this article is available in this repository. If you want to try it locally, please clone it and proceed:

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

Here's the implementation flow:

  1. Deploy A2A servers to ECR & AgentCore Runtime using Terraform (Calculator / Weather)
  2. Register A2A Agent Cards to Agent Registry
  3. Local orchestrator agent searches the Registry and connects to A2A servers
  4. Put the orchestrator on AgentCore Runtime for Runtime→Runtime calls

The file structure is as follows:

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 Runtime orchestrator using 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 app. For the calculation tool, we'll use strands_tools.calculator provided by Strands as is.

AgentCore Runtime injects the environment variable AGENTCORE_RUNTIME_URL for A2A, which we pass to A2AServer's http_url. This URL goes into the url field of the Agent Card, and the client sends JSON-RPC requests there.

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. The endpoints /ping and /.well-known/agent-card.json are automatically created by A2AServer, so you don't need to write them yourself.

Weather Agent

The second agent returns weather information. For this example, we'll create a tool with a mock function that returns fixed 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:
    """日本の都市の現在の天気情報を返します。対応都市: 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. Don't forget --platform=linux/arm64 as AgentCore Runtime requires 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

Terraform Deployment

We'll create two ECRs with Docker build & push, and two agents running on AgentCore Runtime. We specify server_protocol = "A2A" in 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 is granted 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 set the Runtime ARN for A2A:

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 client URL is constructed in the form https://bedrock-agentcore.<region>.amazonaws.com/runtimes/<arn>/invocations (with ARN URL-encoded using urllib.parse.quote).

Let's deploy:

cd terraform
terraform init
terraform apply

After deployment, let's register these agents in the Registry.

Register A2A Agent Cards in Registry

A2A descriptor structure

Agent Registry provides A2A as a descriptorType. The A2A descriptor puts the Agent Card JSON in a2a.agentCard.inlineContent.

Before registration, we'll fetch the actual Agent Card from the Runtime and register its contents in the Registry. This registration allows clients to understand agent names, skills, connection URLs, and so on.

For details on A2A, please refer to my previous blog:

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

SigV4 Authentication Helper

Since Runtime uses IAM authentication for bedrock-agentcore, both Agent Card fetching and JSON-RPC calls require SigV4 signatures. 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 requires_request_body = True is necessary; otherwise, the JSON-RPC body won't be included in the signature, resulting in a 4xx error 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 build the base URL from the Runtime ARN, fetch the Agent Card, and register it with the Registry. Since this Registry is set for auto-approval, it follows the create → submit → auto-approve flow to reach APPROVED status.

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 Loading A2A Agents from Registry

A2A Loader

This loader searches A2A records in the Registry, extracts the url field from Agent Cards, 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")

It follows a simple flow: Registry search → extract url from Agent Card → pass to A2AClientToolProvider. Authentication is set using httpx_client_args.

Orchestrator

Let's connect A2A tools obtained from the Registry to a local Strands Agent and execute a complex task.
We'll try a request like: "Please tell me the weather in Tokyo. After that, 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()

Functionality Check

Now that we've implemented it, let's run it. I'll specify the ID for a Registry I created beforehand.

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 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 equals 71.6°F!

The orchestrator successfully used A2A tools fetched from the Registry, calling the weather agent and then the calculator agent in sequence to complete the composite task!

It's great that we didn't have to write any individual tool definitions in the agent code - it can connect using only the endpoint information dynamically retrieved from the Registry.

Deploying the Orchestrator on AgentCore Runtime

So far we've been running the orchestrator locally, but the same logic can be deployed as a server on the HTTP protocol AgentCore Runtime. We 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()

We set up BedrockAgentCoreApp and implement a simple agent that connects via Registry with A2A.

Here's the Dockerfile implementation:

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, we'll add a new Runtime with server_protocol = "HTTP". The Registry ID is set as an 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 to other AgentCore Runtimes via A2A, bedrock-agentcore:InvokeAgentRuntime alone is not sufficient. The A2A specification first calls GET /.well-known/agent-card.json to get agent information, but this API is assigned a different action: bedrock-agentcore:GetAgentCard.

I got stuck in a pattern where Registry search worked but Agent Card retrieval resulted in 403 Forbidden, so I'm sharing this information for reference. It's something to be 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/*",
      ]
    }]
  })
}

Calling the Orchestrator (via Runtime)

Let's call the orchestrator deployed on AgentCore.
We'll prepare a simple client invoke_orchestrator.py to format the response:

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 "東京の天気を教えて、気温を華氏に変換して。"
    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 check the operation:

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! Weather and calculator agents are available. I'll get 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/"}

Great! I've got Tokyo's weather information. Now 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! I've completed the Tokyo weather information and temperature conversion to Fahrenheit.

## 📍 Tokyo Weather Information

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

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

We can confirm the flow: retrieve agent list from Registry via a2a_list_discovered_agents → message to weather agent → message to calculator agent!

Looking at the CloudWatch Logs on the Runtime side, we can also confirm the internal flow of Registry search → Agent Card retrieval → JSON-RPC sending:

Runtime Orchestrator Logs (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...

Conclusion

Following the previous Skills version, this time we tried dynamic loading with A2A.
It's interesting how an orchestrator agent can connect to necessary components dynamically through Registry lookup, without directly holding information about its A2A counterparts. This seems to make management easier when the number of connections increases. It appears to be a useful foundation for centrally managing organization-approved agents. (It's unfortunate that Registry doesn't support HTTPS yet, so hopefully that will be supported soon...)

I hope this article has been helpful. Thank you for reading to the end!

Share this article