Culminando la serie de artículos que trata la orquestación de multiagentes en Strands Agents, voy a explicar cómo implementar agentes remotos con A2A y algunas de sus variantes, utilizando routing determinístico y discovery dinámico de agentes. Lo ejemplifiqué con la construcción de dos casos de uso: privacy-aware-routing y multi-agent-trip-planner. Para probarlos utilicé Gemma4 + Ollama (local) y la API de Gemini, capa gratuita.
Introducción
La idea principal es experimentar con ejemplos sencillos algunas casuísticas del protocolo de comunicación A2A en tu notebook, sin necesidad de una cuenta de AWS ni de servicios como Bedrock. Strands hace de wrapper sobre el protocolo, permitiendo una comunicación fluida con agentes de IA remotos, más allá del framework o lenguaje con el que estén implementados.
🗒 Los casos de usos que elegí
-
Privacy aware routing: Un asistente financiero-bancario de atención al cliente (orquestador, Gemini vía API key) necesita resolver consultas que a veces incluyen datos sensibles (ej. número de cuenta, DNI, monto de la cuenta). En lugar de resolver el agente general, el orquestador detecta cuándo la consulta toca datos sensibles y delega esa parte a un agente A2A local (servidor separado, Gemma vía Ollama, corriendo en
localhost). - Multi agent trip planner: Un asistente de viajes de atención al cliente donde el orquestador (Gemini vía API key) no conoce de antemano a qué agente delegar cada parte de la consulta. En su lugar, descubre en runtime tres agentes especialistas: vuelos, hoteles e itinerario. Cada uno expuesto como servidor A2A local (Gemma vía Ollama), y decide dinámicamente a cuál invocar según lo que necesita resolver.
En las secciones siguientes cubro los conceptos clave del protocolo A2A en Strands y el diseño e implementación de los casos de uso.
Conceptos Clave
➡ Protocolo A2A en Strands
A2A (Agent-to-Agent) es un protocolo abierto —no una invención de Strands ni de AWS— que define cómo agentes que corren en procesos, máquinas o incluso organizaciones distintas se descubren y comunican entre sí por HTTP/JSON-RPC.
Strands no reinventa el protocolo: lo envuelve (wraps) con una interfaz familiar, como si invocáramos a un agente normal de Strands. El desarrollador no ve protocolo: visualiza un agente. Un ejemplo sencillo:
from strands.agent.a2a_agent import A2AAgent
# Create an A2AAgent pointing to a remote A2A server
privacy_agent = A2AAgent(endpoint="http://localhost:9000")
# Invoked exactly like a local Agent — Strands' wrapper keeps
# the A2A protocol completely hidden from the caller
result = privacy_agent("I need the balance for account 1234-5678-9")
print(result.message)
# {'role': 'assistant', 'content': [{'text': 'The balance for account 1234-5678-9 is $85,400'}]}
El constructor de A2AAgent acepta varios parámetros:
| Parameter | Type | Default | Description |
|---|---|---|---|
endpoint |
str |
Required | Base URL of the remote A2A agent |
name |
str |
None | Agent name (auto-populated from agent card if not provided) |
description |
str |
None | Agent description (auto-populated from agent card if not provided) |
timeout |
int |
300 | Timeout for HTTP operations in seconds |
| a2a_client_factory | ClientFactory | None | Optional pre-configured A2A client factory |
Dos piezas hacen posible el wrap en Strands:
A2AServer: expone cualquierAgentde Strands como servicio A2A, publicando automáticamente un agent card en/.well-known/agent-card.json→ es la carta de presentación del agente (nombre, descripción, skills que ofrece).A2AAgent: el lado cliente. Envuelve el endpoint de un agente remoto y lo invocás con la misma interfaz que un agente local.
Al igual que un Agent local, A2AAgent soporta invocación asíncrona (invoke_async) y streaming de respuestas (stream_async). En los ejemplos de este artículo uso invocación sincrónica por simplicidad, estas variantes están disponibles si tu caso necesita no bloquear el hilo principal o mostrar la respuesta token a token.
La metadata del agente remoto es la agent card y se puede consultar explícitamente con get_agent_card():
card = await privacy_agent.get_agent_card()
print(f"Agent: {card.name}")
print(f"Description: {card.description}")
print(f"Skills: {card.skills}")
Cada skill dentro del agent card describe una capacidad puntual del agente: un identificador, una descripción en lenguaje natural, y opcionalmente ejemplos de uso. Es lo que le permite al orquestador razonar "este agente sabe buscar vuelos" sin necesidad de que el desarrollador lo codifique explícitamente. La decisión de a quién delegar se basa en hacer match entre la consulta del usuario y las skills descubiertas.
El método anterior el que después va a cobrar protagonismo real en el ejemplo multi-agent-trip-planner: ahí el orquestador no invoca un endpoint conocido de antemano, sino que consulta las agent cards de varios agentes remotos para decidir dinámicamente a cuál delegar cada tarea.
➡ A2AAgent dentro de otros patrones de orquestación en Strands
A2AAgent se puede combinar dentro de otros patrones de orquestación multiagente en Strands.
-
Como tool de un agente orquestador: es exactamente el enfoque que desarrollamos en la POC
privacy-aware-routing— elA2AAgentse envuelve en un@tooly el agente padre lo invoca como una función más, como si fuera un subagente. -
Como nodo dentro de un Graph: un
A2AAgentpuede integrarse como nodo remoto en un flujo de Graph, mezclando agentes locales y remotos dentro del mismo grafo de dependencias. - En Swarm: al día de hoy NO está soportado, ya que ese patrón depende de handoffs basados en tools que el protocolo A2A todavía no contempla.
Habiendo visto el lado CLIENTE, veamos ahora cómo se expone un agente para que otros puedan consumirlo vía A2A.
➡ Creando un A2A Server
Del lado servidor, exponer un agente de Strands como servicio A2A es tan simple como envolverlo con A2AServer:
from strands import Agent
from strands.multiagent.a2a import A2AServer
def create_agent(context_id: str) -> Agent:
return Agent(
name="Privacy Agent",
description="Resolves queries containing sensitive customer data",
)
a2a_server = A2AServer(agent_factory=create_agent)
a2a_server.serve()
El servidor publica automáticamente el agent card en /.well-known/agent-card.json y queda escuchando requests A2A en el puerto configurado (por default, 9000).
Un detalle de diseño importante: se recomienda pasar un agent_factory (una función que crea un agente nuevo) en lugar de una instancia única de Agent. La razón es el aislamiento por conversación: A2A identifica cada conversación con un context_id, y el servidor crea y reutiliza un agente dedicado por cada context_id, para que distintas conversaciones nunca mezclen su historial entre sí. Pasar un único Agent compartido está deprecado!
Para los casos de uso de este artículo, ese aislamiento no es el foco central: cada servidor atiende un tipo de tarea puntual, sin mantener contexto conversacional extenso.
Pero es un concepto clave a tener presente si tu agente remoto necesita recordar el hilo de una conversación a lo largo de varios turnos.
➡ Strands A2A Tool: descubrimiento dinámico
Cuando hay varios agentes remotos disponibles y no querés hardcodear el mapeo "qué tarea va a qué agente", Strands ofrece A2AClientToolProvider (parte del paquete strands-agents-tools). Le da al agente orquestador un conjunto de tools para descubrir agentes A2A y comunicarse con ellos usando lenguaje natural:
from strands import Agent
from strands_tools.a2a_client import A2AClientToolProvider
# known_agent_urls es opcional: podés pasar endpoints conocidos
# o dejar que el provider los descubra en runtime
provider = A2AClientToolProvider(
known_agent_urls=[
"http://localhost:9001", # Flights Agent
"http://localhost:9002", # Hotels Agent
"http://localhost:9003", # Itinerary Agent
]
)
orchestrator = Agent(tools=provider.tools)
response = orchestrator("Quiero ir a Barcelona en marzo, 5 días")
El provider expone tres capacidades:
- Agent Discovery: descubre automáticamente los agentes A2A disponibles y sus capacidades (leyendo sus agent cards).
- Protocol Communication: envía mensajes usando el protocolo A2A estandarizado.
- Natural Language Interface: el orquestador interactúa con los agentes remotos en lenguaje natural, sin que el desarrollador tenga que codear explícitamente "si la consulta es sobre vuelos, llamá a este endpoint".
Esta es la pieza central de la POC de este articulo multi-agent-trip-planner: acá el agent card deja de ser un detalle de implementación y pasa a ser el mecanismo que le permite al orquestador razonar sobre a quién delegar cada subtarea, sin que ese mapeo esté escrito en el código.
➡ Evaluaciones (Evals)
Strands cuenta con un marco oficial para realizar evaluaciones, desde resultados simples hasta el análisis de interacciones multiagente complejas.
Para evaluar los workflows usé strands-agents-evals con el mismo provider configurado en .env (Gemini) como juez via LiteLLM para mantener el proyecto sobre una sola credencial y sin cuenta de AWS. En producción la práctica estándar es usar un modelo más capaz e independiente como judge.
En ambos proyectos combiné dos tipos de evaluación, para mostrar las dos familias que ofrece el SDK:
- Determinística (código puro, sin LLM): rápida y barata, ideal para CI (Continuous Integration). Verifica la decisión del orquestador inspeccionando qué herramientas invocó (su trayectoria). Es el corazón de cada patrón A2A.
- LLM-as-a-judge: un modelo puntúa, con una rúbrica, cualidades más subjetivas de la respuesta final: Correctness (¿el dato coincide con la referencia?) y Faithfulness (¿se apoya en los datos de las tools y no inventa ni evade?).
La diferencia está en qué mide la capa determinística en cada patrón, porque cada uno decide algo distinto:
| privacy-aware-routing | multi-agent-trip-planner | |
|---|---|---|
| Qué decide el orquestador | Si delega o responde directo | A quién descubre y a quién delega |
| Eval determinística | La tool handle_sensitive_query se llama solo ante datos sensibles, y NO ante consultas genéricas |
El orquestador primero descubre (a2a_list_discovered_agents) y luego delega (a2a_send_message) |
| Evaluadores usados |
ToolCalled + ToolNotCalled (custom) |
ToolCalled (discovery y delegación) |
| Eval LLM-judge | Correctness + Faithfulness sobre la respuesta del agente local | Correctness + Faithfulness sobre la propuesta de viaje integrada |
| Problema que detecta | El modelo local inventa un saldo en lugar de usar la tool | Un especialista inventa vuelos/hoteles/atracciones en lugar de usar su tool |
Descripción de la solución
📦 Un único repositorio de GitHub con los dos proyectos: github.com/reinalau/strands-a2a-patterns

El repositorio reúne dos proyectos independientes, cada uno es un caso de uso de A2A:
-
privacy-aware-routing- ruteo determinístico hacia un único agente remoto: el orquestador decide, según el contenido de la consulta, si responde él mismo o delega la parte sensible a un agente que corre 100% local. -
multi-agent-trip-planner- descubrimiento dinámico de agentes: el orquestador no conoce de antemano a los especialistas, sino que los descubre en runtime leyendo sus agent cards y delega cada subtarea al que corresponda. A su vez cada agente tiene tools disponibles.
Ambos comparten la misma base técnica (Strands, con Gemini como orquestador y Gemma local como agente remoto) y la misma forma de ejecutarse. El siguiente setup aplica a los dos por igual. Se usan los dos modelos, Ollama local y Gemini via Apikey:
a. Ollama + el pequeño modelo gemma4:e2b-it-qat (que pesa poco más de 4gb) ejecutando en Docker. Para esto necesitás tener Docker Desktop instalado y corriendo; una vez levantado, los comandos de abajo crean el contenedor de Ollama y descargan el modelo.
En el caso de multi-agent-trip-planner hay tres agentes remotos que pueden recibir requests casi al mismo tiempo; si querés que esas llamadas se ejecuten en paralelo, hay que indicárselo a Ollama, cuyo comportamiento por default procesa una inferencia por vez:
# Start the Ollama server with a persistent volume
docker run -d --name ollama -p 11434:11434 -v ollama_data:/root/.ollama ollama/ollama
# Start the Ollama server with real parallelism (3 simultaneous requests)
# docker run -d --name ollama -p 11434:11434 -v ollama_data:/root/.ollama -e OLLAMA_NUM_PARALLEL=3 ollama/ollama
# Download the model
docker exec -it ollama ollama pull gemma4:e2b-it-qat
# Test that the model responds
# If the container has already been created and
# is currently stopped, simply use: docker start ollama
docker exec -it ollama ollama run gemma4:e2b-it-qat
# Verify that the model is running
docker exec -it ollama ollama ps
b. API de Gemini. La API key se puede generar de manera gratuita desde aquí y utilizar al menos estos modelos (experimentá con los que te permita tu cuenta):
gemini-3.6-flash
gemini-3.5-flash-lite
El código de los 2 proyectos está en Python y la estructura se diseñó de manera que su lógica sea más explicativa; todo el detalle lo encontrás en el README.md de cada proyecto:
strands-a2a-patterns/
├── README.md # Overview of both use cases
│
├── privacy-aware-routing/ # Case 1: deterministic routing, single remote agent
│ ├── README.md
│ ├── requirements.txt
│ ├── .env.example
│ ├── common/ # shared: prompts, config, logging
│ ├── remote_agent/
│ │ ├── server.py # A2AServer — Gemma via Ollama
│ │ └── bank_tools.py # mock bank tools with fixed data
│ ├── orchestrator/
│ │ └── main.py # Gemini — A2AAgent as tool
│ ├── tests/
│ │ ├── test_routing_logic.py # does the orchestrator delegate when it should?
│ │ └── test_a2a_integration.py # real integration test against the running local server
│ ├── evals/
│ │ ├── eval_routing.py # deterministic: routing decision
│ │ └── eval_response_quality.py # LLM-judge: correctness + faithfulness
│ └── logs/
│ └── .gitkeep # run logs land here (gitignored)
│
└── multi-agent-trip-planner/ # Case 2: dynamic discovery, 3 remote agents
├── README.md
├── requirements.txt
├── .env.example
├── common/ # shared: prompts, config, logging
├── remote_agents/
│ ├── travel_tools.py # mock travel tools with fixed data
│ ├── flights_server.py # A2AServer — Gemma via Ollama (port 9001)
│ ├── hotels_server.py # A2AServer — Gemma via Ollama (port 9002)
│ └── itinerary_server.py # A2AServer — Gemma via Ollama (port 9003)
├── orchestrator/
│ └── main.py # Gemini — A2AClientToolProvider
├── tests/
│ ├── test_discovery.py # does the orchestrator discover before delegating?
│ └── test_a2a_integration.py # real integration test against the 3 running servers
├── evals/
│ ├── eval_discovery.py # deterministic: discovery + delegation decision
│ └── eval_response_quality.py # LLM-judge: correctness + faithfulness
└── logs/
└── .gitkeep # run logs land here (gitignored)
Ejecución Local
Una vez que tenemos el repositorio clonado y el Docker de Ollama con el modelo gemma 4 (o la API key de Gemini generada), pasamos a armar el ambiente. Los pasos son los mismos en ambos proyectos; cada uno tiene su propio requirements.txt y su .env, así que hay que pararse dentro de la carpeta del proyecto que quieras correr.
En requirements.txt (con comentarios que explican para qué se necesita cada dependencia) encontramos:
strands-agents[a2a,ollama,litellm]>=1.0.0
strands-agents-tools>=0.1.0
python-dotenv>=1.0.0
pytest>=8.0.0
pytest-asyncio>=0.23.0
strands-agents-evals>=1.2.0
Conviene instalar las dependencias en un entorno virtual, para aislarlas del resto del sistema:
python -m venv .venv
# Activar: source .venv/bin/activate (Linux/macOS)
pip install -r requirements.txt
En .env se configuran, entre otros, la API key y el modelo del orquestador (Gemini), el modelo local (Gemma vía Ollama) y el nivel de log. Alcanza con copiar el ejemplo y completar la key:
cp .env.example .env
En common/config.py está la carga y validación de esas variables (falla temprano con un mensaje si falta la API key), y en common/prompts.py los system prompts del orquestador y de cada agente remoto.
📝 Sobre cambiar de modelo: el orquestador usa LiteLLMModel, así que no está atado a Gemini: LiteLLM soporta muchos proveedores (Anthropic, OpenAI, Bedrock, etc.). Alcanza con cambiar ORCHESTRATOR_MODEL_ID en el .env por el identificador del proveedor que quieras (con su prefijo, p. ej. anthropic/…, openai/…) y setear su API key. La lista completa está en la documentación de model providers de Strands. Del mismo modo, el agente remoto no tiene por qué correr en Ollama: podés reemplazar OllamaModel por otro provider en el código del servidor (server.py / los *_server.py).
Las pruebas están en tests/ y se pueden correr una a una (recomendado! ya que puede ser algo lento localmente) o todas juntas: unitarias rápidas (deciden si el orquestador delega/descubre cuando corresponde) e integración real contra los servidores A2A (arrancan solos como subprocesos; requieren Ollama corriendo).
python -m pytest
Procedemos a ejecutar el caso real. Acá está la diferencia práctica entre los dos proyectos: cada uno corre como varios procesos independientes, uno por terminal. Primero se levantan los agentes remotos (cada A2AServer queda escuchando en su puerto) y, con todos activos, se lanza el orquestador que los descubre y les habla por la red. El orden de arranque de los servidores no importa, pero deben estar todos activos antes de correr el orquestador.
privacy-aware-routing — 2 procesos (2 terminales):
| Terminal - Comando | Rol |
|---|---|
Terminal 1: python -m remote_agent.server
|
Agente remoto local (Gemma), expone su agent card en el puerto 9000. Cuenta con tools para responder sobre datos sensibles |
Terminal 2: python -m orchestrator.main "Quiero consultar el saldo de mi cuenta 1234-5678-9"
|
Orquestador (Gemini): decide si delega y arma la respuesta |
multi-agent-trip-planner — 4 procesos (4 terminales):
| Terminal - Comando | Rol |
|---|---|
Terminal 1: python -m remote_agents.flights_server
|
Especialista de vuelos (Gemma), puerto 9001 |
Terminal 2: python -m remote_agents.hotels_server
|
Especialista de hoteles (Gemma), puerto 9002 |
Terminal 3: python -m remote_agents.itinerary_server
|
Especialista de itinerario (Gemma), puerto 9003 |
Terminal 4: python -m orchestrator.main "Quiero ir a Barcelona en marzo, 5 dias"
|
Orquestador (Gemini): descubre los 3 agentes, delega y arma la propuesta |
El orquestador acepta la consulta como argumento o, si se lo llama sin argumentos, abre un chat interactivo.
Para salir de los servidores remotos: Ctrl+C en su terminal; para salir del chat interactivo: escribí exit.
Los logs de las llamadas están en logs/, separado por proceso. Contiene la data cruda que permite seguir el intercambio A2A: qué descubrió y delegó el orquestador, y qué tool ejecutó cada agente remoto (líneas TOOL CALL: ...). Permite verificar que la respuesta salió de los datos y no de una alucinación del modelo.
El resultado final se visualiza en pantalla, en streaming (token a token).
Por último, hacemos una evaluación sobre el sistema multiagente real con el concepto de "Evals". Cada proyecto trae dos scripts: uno determinístico (verifica la decisión del orquestador) y uno LLM-as-judge (puntúa la calidad de la respuesta final). Ejecutan el flujo real contra el modelo configurado, muestran los resultados en consola y guardan el reporte completo en evals/outputs/<nombre>_<timestamp>.json. (Leer más detalle arriba en Conceptos Clave)
# privacy-aware-routing
python -m evals.eval_routing
python -m evals.eval_response_quality
# multi-agent-trip-planner
python -m evals.eval_discovery
python -m evals.eval_response_quality
Conclusiones
Los dos proyectos implementan casos de uso distintos que se apoyan en la misma base exponer agentes con A2AServer, pero difieren en cómo el orquestador consume esos agentes remotos: uno con ruteo determinístico hacia un agente conocido (A2AAgent, endpoint fijo); y el otro, mediante un descubrimiento dinámico de varios especialistas (A2AClientToolProvider, que lee los agent cards en runtime).
⭐ En ambos casos de uso, la abstracción de Strands esconde la mecánica del protocolo (resolución del agent card, HTTP, JSON-RPC). Del lado del orquestador, delegar a un proceso remoto se ve casi igual que llamar a una función local.
Algunas lecciones que dejaron las ejecuciones:
-
El agent card es el contrato. En el caso de discovery, la calidad de las
skillspublicadas (nombre y descripción) es lo que le permite al orquestador rutear bien sin mapeos hardcodeados. Un agent card incompleta degrada la decisión. -
Los modelos locales chicos usan tools de forma intermitente. Con
gemma4:e2b-it-qat, a veces el agente responde sin llamar la tool y ahí puede inventar datos. Por eso importa (a) instruirlo en el prompt a usar siempre la tool, y (b) verificarlo: los logsTOOL CALL: ...distinguen un dato real de una alucinación. -
Evaluar los agentes exige leer el campo "reasons", no solo el score. Un
0.0puede venir de la infraestructura (un503del juez, un modelo dado de baja), no de una mala respuesta. -
Latencia y concurrencia son decisiones de arquitectura. La inferencia local es lenta; con tres especialistas compartiendo un Ollama secuencial, las tareas se encolan. Habilitar paralelismo (
OLLAMA_NUM_PARALLEL) y ajustar los timeouts del cliente A2A cambia notablemente la experiencia.
Los dos casos ejemplificados son solo algunas variantes de A2A. Strands habilita capacidades que dejo anotadas para futuras pruebas:
-
Agentes remotos dentro de patrones multiagente: un
A2AAgentpuede ser un nodo más en un Graph workflow, mezclando agentes locales y remotos en un mismo pipeline. - Interrupciones (input_required): Similar a "human-in-the-loop", un agente remoto puede pausar una tarea y esperar una aprobación antes de continuar (por ejemplo, confirmar una reserva), y reanudar exactamente donde quedó.
-
Persistencia de conversaciones: con un
SessionManagerporcontext_id, cada conversación puede sobrevivir al reinicio del proceso en lugar de vivir solo en memoria. - Notificaciones push y despliegue distribuido: para tareas largas y para exponer cada agente productivamente (por ejemplo, detrás de balanceadores, en AWS Fargate o vía AgentCore), escalando el sistema más allá de una notebook.
- Utilizar MCP de manera complementaria: Cada especialista puede usar MCP para sus tools y A2A para colaborar con otros.
⭐ Por último, aquí construyo los agentes en Python con Strands, pero como A2A es un estándar abierto (hoy bajo la Linux Foundation) y viaja sobre HTTP/JSON-RPC, la interoperabilidad ocurre a nivel del protocolo, no del framework. Un agente Strands puede comunicarse con agentes construidos en otros frameworks y lenguajes que también implementen A2A. El protocolo tiene SDKs oficiales en Python, JavaScript, Java, C#/.NET, Go y Rust, sin que ninguno conozca la lógica interna del otro.
Recursos
Curso Fundamentos Strands
Building AI Agent Harnesses with Strands Agents
Building AI Agent Harnesses – Video CourseDocumentación Strands Agents
Evals
Agent-to-Agent (A2A) ProtocolProtocolo A2A (Agent2Agent)
Sitio oficial del protocolo A2A

Top comments (0)