Agents · ·40 min ·Advanced

Agent to Agent

Agent to agent es un protocolo más o menos acertado que nos permite acercarnos a la problemática de la comunicación entre agentes: el descubrimiento, la asincronía, la interrupción controlada, etcétera.

1. Introducción

El protocolo A2A, Agent-to-Agent, es un estándar abierto cada vez más popular que tiene como facilitar la comunicación e interoperabilidad entre agentes de inteligencia artificial generativa; es decir, es una propuesta para que dos agentes se comuniquen mejor entre sí siguiendo determinadas reglas conocidas por ambas partes. Lo propuso Google en abril de 2025 y más tarde se incorporó como proyecto libre de la Linux Foundation.

Aunque nació para establecer una forma de comunicación entre diferentes empresas o frameworks, ocurre igual que con un api REST, que puede utilizarse para comunicar agentes distintos de un mismo entorno. Y esto es lo primero que conviene aclarar: cuándo tiene sentido implementarlo.

El primer caso en el que puede ser interesante usar este protocolo está claro, es por el que nació, cuando se necesita que un agente se comunique con agentes externos (de proveedores, partners, etc.). De esta manera, cada parte puede desarrollar su sistema agéntico como quiera y lo único que debe hacerse es adaptarse a un protocolo para comunicarse. Lo que suceda más allá de esa capa de comunicación es transparente para cada parte.

La segunda es más sutil y sería el mismo caso que ya usan las grandes corporaciones cuando distribuyen responsabilidades en microservicios. Cuando existen varios agentes internos, que pueden estar construidos con distintos frameworks o equipos o no, y se busca que se comuniquen sin acoplamientos ad-hoc.

Las ventajas de este escenario son las mismas que proporcionan los microservicios:

  • Cada agente es independiente, desplegable por separado.
  • Se puede cambiar la implementación interna sin que nadie se vea afectado.
  • Permite escalar cada agente de forma independiente.
  • Equipos distintos pueden desarrollar agentes distintos sin necesidad de coordinarse.

Y los problemas también son los mismos:

  • Latencia de red donde antes había una llamada a función.
  • Complejidad operacional: más cosas que desplegar, monitorizar y autenticar.
  • Depuración más difícil cuando algo falla en medio de una cadena de agentes.

Así que conviene tener también muy claro cuándo implementario nos llevaría a un problema de sobreingenería. Y esto es cuando el sistema es pequeño y la complejidad del protocolo supera el beneficio, sobre todo si todos los agentes son del mismo framework, ya que suelen tener sus propios mecanismos internos para encadenar agentes, tal y como sucede con LangChain.

Por estas fechas, creo que la documentación oficial quizás sea aún un poco farragosa, sobre todo porque mezclan la teoría con una herramienta para implementar el protocolo, un SDK, pero el proyecto es interesante. Y lo es no tanto por la propuesta en sí, que también, sino por dónde pone el foco: en la necesidad de definir una manera estandarizada para la comunicación entre agentes de distintos sistemas. Es decir, la propuesta nos podrá gustar más o menos, pero el problema existe y seguirá existiendo y habrá que resolverlo, ya sea con esta propuesta o con cualquier otra forma similar.

De ahí la naturaleza tan teórica con la que arranco esta serie dedicada al protocolo. Han desarrollado un SDK y se puede utilizar sin necesidad de conocer la teoría, pero creo que es fundamental entender antes qué trata de resolver: cómo descubre un agente las habilidades de otro agente, como le dice uno a otro que necesita más información o que va a demorar en preparar la respuesta y que ya se la dará cuando esté terminado. Ese es uno de los grandes méritos del protocolo: identificar los problemas de este escenario nuevo.

Este tutorial se divide en tres partes. Las primeras dos serán teóricas, un análisis del protocolo que sirva de aterrizaje en la materia, la tercera, que no sé cuándo escribiré, estará dedicada al SDK.

Sin más preámbulos, vamos ya los conceptos core del protocolo.

En este tutorial trataré de explicar cómo funciona A2A de principio a fin: los conceptos core del protocolo, cómo fluye una tarea entre agentes, cómo se gestiona la seguridad, y cómo implementar un sistema completo con el SDK oficial de Python.

2. Los fundamentos

En esencia, la propuesta viene a indicar que cuando un agente le solicita a otro algo, el primero asume el papel de agente Client y el segundo de Server. La comunicación se establece siguiendo el protocolo JSON-RPC y los pasos son estos:

  1. El Client descubre qué puede hacer el Server. Por ejemplo, un Client puede descubrir que un Server es especialista en dinosaurios.
  2. Opcionalmente, el Client se autentifica.
  3. Una vez que sabe qué puede hacer el Server, le envía una primera petición: “Dime la diferencia entre un estegosaurio y un triceratops”.
  4. Si el Server piensa que le puede dar una respuesta inmediata responde enseguida: “A los triceratops les gusta el blues, pero los estegosaurios son más de soul”.
  5. Sin embargo, si le va a demorar responde avisando que le llevará un tiempo preparar la tarea.
  6. A medida que vaya teniendo datos de la tarea, el Server se los irá enviando al Client.
  7. Cuando tiene las respuestas, el Client puede seguir con la conversación o darla por terminada.

Traducido lo anterior en terminología del protocolo:

  1. El Client averigua cuáles son las habilidades del Server consultando un json llamado Agent Card.
  2. El Client lee la Agent Card y comprueba el campo securityRequirements. Si existe, obtiene las credenciales fuera del protocolo A2A y las incluye en la cabecera HTTP de cada petición.
  3. Una vez que sabe qué puede hacer el Server, le envía un Message con una primera petición.
  4. Si el Server piensa que le puede dar una respuesta inmediata responde con otro Message.
  5. Sin embargo, si le va a llevar tiempo responde, envía una Task para que el Client pueda hacer un seguimiento. Cuando tenga los resultados, los enviará en Artifacts.
  6. En el caso de que el Client necesite más cosas se establecerá una conversación mediante interacciones multi-turno.

En las siguientes líneas vamos a ir aterrizando cada uno de esos términos.

3. Quién es quién

A2A Client y A2A Server son los dos participantes del protocolo. El Client actúa en nombre de un usuario (humano o sistema automatizado) e inicia la comunicación. Es el que llama. El Server, también llamado Remote Agent, expone un endpoint HTTP que implementa el protocolo, recibe tareas y devuelve resultados. Es el que escucha.

Podría resultar tentador equiparar estos dos roles con los papeles de emisor y receptor de una comunicación, pero no es exactamente lo mismo. Durante una interacción ninguno de los dos cambiará de rol: quien inició la conversación será siempre el Client y quien la respondió, el Server. Equivale más bien a la comunicación vía REST entre un front y un back desacoplados.

Eso no quita que en otra ocasión el que actuó como Client pudiera ser el Server y viceversa o que, a su vez, el Server deba comportarse como Client ante un tercero para atender a la cuestión planteada por el Client.

client y server

4. Agent Card

4.1. Qué son

La Agent Card es un documento JSON que actúa como la tarjeta de presentación digital de un A2A Server. Es el mecanismo central de descubrimiento: antes de que un Client interactúe con un Remote Agent, lee su Agent Card para entender qué hace, cómo contactarle y cómo autenticarse.

Esta, por ejemplo, podría ser un Agent Card de un agente especializado en dar recetas de tofu.

{
  "name": "Tofu Recipe Agent",
  "description": "Provides tofu-based recipes based on ingredients, cooking style or dietary preferences.",
  "version": "1.0.0",
  "supportedInterfaces": [
    {
      "url": "https://agents.example.com/tofu-recipes",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "capabilities": {
    "streaming": false,
    "pushNotifications": false
  },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {
      "id": "get-tofu-recipe",
      "name": "Get Tofu Recipe",
      "description": "Returns a tofu recipe based on the user's request. Accepts natural language queries about cooking style, ingredients or dietary needs.",
      "tags": ["tofu", "recipes", "cooking", "vegan"],
      "examples": [
        "Give me a quick tofu stir fry recipe",
        "How do I make crispy tofu?",
        "A tofu recipe with spinach and garlic"
      ]
    }
  ]
}

4.2. Campos

La Agent Card tiene varios grupos de campos. Agrupados por función:

  • Identidad

    • name: nombre legible del agente. Requerido.
    • description: descripción de su propósito. Requerido.
    • version: versión del agente (no del protocolo). Requerido.
    • provider: organización y URL del proveedor. Opcional.
    • documentationUrl: enlace a documentación adicional. Opcional.
    • iconUrl: icono del agente. Opcional.
  • Dónde contactarle

    • supportedInterfaces: lista ordenada de interfaces soportadas. El primer elemento es el preferido. Cada interfaz tiene una URL, un protocol binding (JSONRPC, GRPC, HTTP+JSON) y la versión del protocolo. Requerido.
  • Qué puede hacer

    • skills: lista de skills del agente. Cada skill tiene: id, name, description, tags, ejemplos opcionales, y los tipos de input/output que acepta. Requerido.
    • defaultInputModes: tipos MIME que acepta como input por defecto. Requerido.
    • defaultOutputModes: tipos MIME que produce como output por defecto. Requerido.
  • Capacidades opcionales del protocolo

    • capabilities: objeto que declara si el agente soporta: streaming, pushNotifications, extendedAgentCard, y extensiones. Opcional, pero su ausencia equivale a declarar todas las capacidades como false.
  • Seguridad

    • securitySchemes: mapa de esquemas de autenticación disponibles (API key, Bearer, OAuth2, OpenID Connect, mTLS). Opcional, pero muy recomendable.
    • securityRequirements: qué esquemas son necesarios para contactar al agente. Opcional. Puede sobreescribirse por skill.
  • Integridad

    • signatures: firmas JWS de la card para verificar que no ha sido manipulada. Opcional.

Aparte de la identidad, de todos esos parámetros el más importante es el de las skills, ya que le indican al Client qué puede pedirle al Server. Cada skill tiene:

  • id: identificador único de la skill.
  • name: nombre legible.
  • description: descripción detallada de qué hace.
  • tags: palabras clave para búsqueda y filtrado.
  • examples: ejemplos de prompts que esa skill puede manejar. Muy útil para que otro LLM decida si delegar o no.
  • inputModes / outputModes: sobreescriben los defaults del agente para esa skill concreta.

4.3. Dónde vive

La estrategia recomendada para agentes públicos es publicar la Agent Card en https://{agent-server-domain}/.well-known/agent-card.json, siguiendo el estándar RFC 8615. El Client hace un GET a esa URL y obtiene la card como JSON.

Este estándar web define cómo publicar metadatos de un servicio en una URL predecible. La idea es simple: si quieres que cualquiera pueda descubrir información sobre tu servicio sin que tengas que decírsela explícitamente, la publicas en /.well-known/ en la raíz de tu dominio.

Se usa para más cosas además de A2A. Por ejemplo:

  • /.well-known/openid-configuration: para descubrir endpoints de autenticación OAuth2/OpenID
  • /.well-known/security.txt: para reportar vulnerabilidades de seguridad
  • /.well-known/apple-app-site-association: para deep links en iOS

A2A sigue el mismo patrón con /.well-known/agent-card.json. La ventaja es que cualquier Client que conozca el dominio de un agente puede descubrir su card automáticamente sin que nadie le haya dicho la URL exacta.

Aclaremos cómo sería la ruta en la vida real. En esencia, pueden darse dos casos:

  • Tienes todo un dominio dedicado al agente: entonces sí que puede vivir sin más en well-known.
  • En el dominio hay más cosas aparte del agente, como otros agentes, microservicios, whatever. En ese caso, habría que seguir situando la card en well-known, pero podrías hacer subdirectorios: /.well-known/agent-card/patatas. La restricción es que /.well-known/ va en la raíz, pero lo que va después es flexible.

Para entornos internos o privados se pueden usar otras dos estrategias:

  • Registro centralizado: existe un servicio intermediario, tipo catálogo, donde los agentes publican sus cards. Los Clients consultan ese catálogo buscando por criterios: “dame todos los agentes que tengan la skill X” o “agentes del proveedor Y”. Útil en entornos enterprise con muchos agentes. La spec no define cómo tiene que ser ese catálogo, cada organización lo implementa como quiere.
  • Configuración directa: la más simple. El Client tiene la URL o, mucho peor, los datos de la card hardcodeados, en un fichero de configuración o en variables de entorno. No hay descubrimiento dinámico. Si la card cambia, hay que reconfigurar el Client a mano. La URL puede ser una alternativa sencilla muy razonable. Tener los datos hardcodeados, no.

Bueno, ya sabemos quiénes son los dos participantes de la comunicación -el Client y el Server- y cómo van a conocerse mediante la Agent Card. Vamos ahora con la manera en la que van hablar.

5. El protocolo JSON-RPC

En general, estamos acostumbrados a trabajar los servicios mediante el estándar REST, en el que todo es un recurso (un usuario, un producto, una imagen), identificado por una URL única, por ejemplo: https://dinosaurios.com/users/123. El cliente envía llamadas, request, indicando con un verbo qué quiere hacer. Por ejemplo, GET para obtener información y POST para crear información. El servicio envía una response en el que entre otra información indica si el proceso ha salido mal o bien con un status (404, 200, 500, etcétera). Los datos se envían en formatos estándar como JSON o XML y una de sus características principales es que es stateless, sin estado, cada petición es independiente.

En este caso, se usa otro sistema distinto, JSON-RPC, que es un protocolo ligero de comunicación remota que permite que un programa llame funciones o métodos que se ejecutan en otro sistema, usando JSON como formato de intercambio de datos. La idea básica es que en la comunicación entre dos aplicaciones, cliente y servidor, con JSON-RPC, el cliente puede decir:

Ejecuta esta función con estos parámetros y devuélveme el resultado.

En este caso solo hay una url, un endpoint y lo que hay que hacer, la función que hay que ejecutar, se indica en la request.

{
  "jsonrpc": "2.0",
  "method": "sumar",
  "params": [3, 5],
  "id": "req-001"
}

Una respuesta exitosa sería algo así como

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": { ... }
}

Y una respuesta con error:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "error": {
    "code": -32601,
    "message": "Method not found"
  }
}

El id es clave: el servidor lo devuelve idéntico en la respuesta para que el cliente pueda saber a qué petición corresponde, lo que es especialmente útil cuando hay múltiples peticiones en vuelo.

Bueno, pues eso, A2A usa JSON-RPC 2.0 sobre HTTPS como binding principal. Todas las operaciones del protocolo (SendMessage, GetTask, CancelTask…) son métodos JSON-RPC que van en el campo method. Los objetos del modelo de datos (Message, Task, Artifact…) van dentro de params o result.

6. SendMessage

6.1. Messages

SendMessage es la operación principal del protocolo. El Client la usa para enviar un mensaje al Server y esperar su respuesta.

La petición tiene tres campos:

  1. message: el Message a enviar. Requerido.
  2. configuration: configuración opcional de la petición. Opcional.
  3. metadata: datos adicionales. Opcional.

Entonces, volviendo al ejemplo de antes, un agente Client que está recopilando recetas contacta con un agente Server especializado en recetas de tofu. El Client ha descubierto, ha averiguado, qué sabe el agente Server consultando su Agent Card y entonces envía una primera petición al único endpoint que expone el Server.

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg-001",
      "role": "user",
      "parts": [
        {
          "text": "Dame una receta de tofu con espinacas"
        }
      ]
    }
  }
}

El server ve que el método invocado es SendMessage, el que define el protocolo. Fijémonos en que las skills no se llaman directamente. No hay un method: “get-tofu-recipe”. Las skills son descriptivas, están en la Agent Card para que el Client sepa qué puede pedirle al agente. Pero la forma de comunicarse siempre es a través de SendMessage.

Ese nodo message se denomina Message y tiene 3 parámetros principales:

  • messageId: requerido, lo genera el Client
  • role: requerido, user o agent
  • parts: el contenido en sí. Lo que quieres decir o enviar.

Parts es un array de objetos, de Parts, cada uno de los cuales puede incluir uno de los siguientes nodos:

  • text: texto plano.
  • raw: bytes en bruto, codificados en base64 (para ficheros inline).
  • url: referencia a un fichero externo.
  • data: JSON estructurado.

Y opcionalmente:

  • mediaType: el MIME type del contenido.
  • filename: nombre del fichero.
  • metadata: datos adicionales.

El caso más habitual es un solo Part de tipo text. Pero también se podría mandar texto más una imagen en dos Parts: uno de text y otro de raw con mediaType: “image/png”.

6.2. Configuration

Aunque es opcional, este conjunto de datos es la parte más interesante de la petición SendMessage. Es donde el Client le pone las reglas de juego al Server para esa interacción específica. En el Message se ha indicado qué se quiere, aquí cómo y cuándo se quiere que respondan.

A diferencia del message, que debería ir directo al LLM del agente, la configuration la lee el SDK o la capa de orquestación del servidor antes de decidir si puede o no aceptar el encargo. Es el contrato de servicio de la llamada y sus parámetros principales son los siguientes.

1. returnImmediately (Booleano).

Como veremos luego con más detalle, es el parámetro que controla la sincronía.

  • true: Le dices al servidor: “No me hagas esperar. En cuanto crees la tarea, dame el ID y suelta la conexión”. Es ideal para sistemas que no pueden permitirse conexiones HTTP largas.
  • false (por defecto): Es un “quédate a la espera”. El servidor intentará procesar la tarea y no cerrará la respuesta hasta que termine o esté a punto de dar un timeout.

2. acceptedOutputModes (Lista de tipos MIME)

Este parámetro sirve para la interoperabilidad entre distintos agentes. Aquí el cliente declara qué formatos es capaz de procesar Por ejemplo: [“text/plain”, “image/png”, “application/json”].

Si el servidor es un agente generador de imágenes pero el cliente le envía en la configuración que solo acepta text/plain, el servidor debería rechazar la tarea (rejected) o adaptar su respuesta antes de empezar a trabajar.

3. historyLength (Entero)

Indica cuántos mensajes previos del hilo de conversación (el Context) debe incluir Server en la respuesta. Si no se especifica, el Server decide; si se pone 0, se pide expresamente no recibir historial. A2A está diseñado para ser eficiente. En lugar de que el cliente tenga que guardar todo el historial y reenviarlo (como ocurre a veces en APIs de LLMs básicas), el servidor gestiona el contexto y el cliente le pide: “Respóndeme, pero inclúyeme los últimos 5 mensajes para que yo pueda refrescar mi memoria local”.

4. taskPushNotificationConfig (Objeto)

Si el cliente no quiere estar preguntando “¿Ya has terminado?” (polling), usa este campo para pasarle al servidor un Webhook URL. Contiene la URL y el tipo de autenticación que el servidor debe usar para avisar al cliente cuando el estado de la Task cambie (por ejemplo, de working a completed).

6.3. Metadata

El tercer parámetro de la petición es de configuración libre, un cajón de sastre que se puede adaptar a cada necesidad. La spec lo define como un objeto JSON libre, sin estructura predefinida. Las claves son strings y los valores pueden ser cualquier cosa representable en JSON. Su propósito es permitir pasar contexto adicional que el protocolo base no contempla, sin necesidad de crear extensiones formales.

Por ejemplo, se puede aprovechar para trazabilidad y observabilidad:

"metadata": {
  "traceId": "a1-b2-c3-d4",
  "parentSpanId": "98765"
}

Para cuestiones relacionadas con la atribución.

"metadata": {
  "tenantId": "customer-acme-corp",
  "projectId": "marketing-tofu-campaign"
}

Por poner un último ejemplo, para identificar al usuario final.

"metadata": {
  "endUserId": "user_99",
  "userRole": "admin"
}

En síntesis, sirve para incluir cualquier información extra no estandarizada que se necesite y, a diferencia de lo indicado en configuration, no debería cambiar el comportamiento del server, que puede limitarse a loguearla.

Sigamos con el ciclo de vida.

6.4. Respuesta inmediata

Recordemos lo que ha pasado hasta ahora:

  1. El agente Client ha identificado qué habilidades tiene el agente Server consultando su Agent Card.
  2. Ha visto que el Server es un especialista en recetas de tofu
  3. Así que le ha pedido en una llamada POST siguiendo el protocolo JSON-RPC que le diga una receta de tofu con espinacas.

Ahora es el turno del Server, que puede responder de dos maneras.

  • Si ve que la respuesta es muy sencilla, que no le va a demorar nada prepararla, puede responder sin más con otro Message, pero indicando agent en el campo role.
{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": {
    "message": {
      "messageId": "msg-002",
      "role": "agent",
      "contextId": "ctx-xyz",
      "parts": [
        {
          "text": "Aquí tienes una receta de tofu con espinacas: ..."
        }
      ]
    }
  }
}

Sin embargo, también puede pensar que resolver la petición le va a llevar mucho tiempo y entonces decide enviar una Task en vez de un Message. De hecho, según la spec, si el agente remoto ve que la respuesta va a tardar más de lo que permite el timeout de la conexión (normalmente 30-60 segundos), está obligado a devolver una Task y cerrar la petición de SendMessage, forzando al cliente a seguir el ciclo de vida asíncrono. Veamos qué es esto.

Veamos qué es eso de Task.

7. Tasks

7.1. Qué son

El agente Server puede demorarse mucho en contestar si tiene que realizar razonamientos muy complejos, llamar a herramientas o generar archivos. Si el protocolo solo permitiera mensajes directos (como una API de chat básica), la conexión HTTP se cortaría por timeout constantemente. En esos casos, el Server crea una Task, le asigna un ID, y la devuelve al Client. A partir de ahí el Client puede hacer polling, suscribirse via streaming, o recibir push notifications para seguir el progreso.

Podemos equiparar el proceso a uno de esos restaurantes donde te asignan un pedido. Para no tenerte en la barra esperando, toman nota y te indican que tu pedido es algún número identificativo, el 22. Cuando lo tengan, avisarán de que ya está listo el 22. Y mientras tanto, tú puedes dar la lata preguntando si ya lo tienen listo. La Task, en síntesis, permite desacoplar la petición del resultado. El servidor te dice: “He aceptado tu petición, aquí tienes el ticket de seguimiento (Task). No te quedes colgado en la línea si no quieres; yo seguiré trabajando”.

7.2. Paramétrica

Una Task tiene los siguientes parámetros:

  • id: El identificador único de la task. Es la llave para cualquier operación posterior (cancelar, pausar, consultar).
  • contextId: Une varias Tasks en una misma conversación. Si el agente es un asistente de viajes, la Task de “buscar hotel” y la de “alquilar coche” compartirán el mismo contextId.
  • status: un objeto con estos parámetros:
    • state: El estado actual.
    • message: Un objeto Message opcional con información descriptiva sobre el estado actual (ej: “Analizando PDF de recetas”).
    • timestamp: Marca de tiempo del último cambio de estado.
  • artifacts: Una lista de los resultados que la Task ha generado.
  • history: Una lista de Messages que han ocurrido dentro de esa tarea (por ejemplo, si el agente te hizo una pregunta aclaratoria mientras trabajaba).

Veamos alguno de estos con más detalle.

En el status se indica cómo va la tarea, que puede tener dos estados principales: en proceso y terminada. Cada uno de estos estados, a su vez, puede tener distintos subestados. Estos son:

  1. Estados de transición (siguen activos)
  • submitted: La tarea está en la cola del servidor pero el agente aún no ha empezado a pensar.
  • working: El agente está activamente procesando.
  • input_required: El agente se ha detenido porque necesita algo del Client (ej: “¿Eres alérgico a los cacahuetes?”).
  • auth_required: El agente necesita que te autentiques en un servicio externo para continuar.

Estados finales (la Task termina aquí)

  • completed: Éxito. El trabajo ha terminado y los resultados están listos.
  • failed: Error técnico o del modelo.
  • canceled: El cliente pidió detener la tarea.
  • rejected: El servidor vio la tarea y la rechazó por la razón que sea, desde que no puede realizarla a razones de seguridad o lo que sea.

Por ejemplo, esta podría ser una Task en estado working, recién creada por el Server en respuesta al SendMessage en el que le pedían una receta de tofu con espinacas:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": {
    "id": "task-001",
    "contextId": "ctx-001",
    "status": {
      "state": "working",
      "message": {
        "messageId": "msg-002",
        "role": "agent",
        "parts": [
          {
            "text": "Estoy preparando tu receta de tofu con espinacas..."
          }
        ]
      },
      "timestamp": "2026-05-07T10:00:00Z"
    },
    "artifacts": [],
    "history": [
      {
        "messageId": "msg-001",
        "role": "user",
        "parts": [
          {
            "text": "Dame una receta de tofu con espinacas"
          }
        ]
      }
    ],
    "metadata": {
      "estimatedDuration": "30s"
    }
  }
}

7.3. ¿Cómo recoge el cliente los resultados?

El Client tiene tres mecanismos para enterarse de los cambios de estado de una Task, y ya los hemos mencionado de pasada. Vamos a verlos con detalle.

7.3.1. Polling

El Client hace llamadas periódicas a GetTask con el id de la task para consultar el estado actual. Es el mecanismo más simple y el único que funciona sin ninguna capacidad especial declarada en la Agent Card. El único inconveniente, poco relevante, es que siempre habrá cierta latencia entre que la Task cambia de estado y que el Client se entera.

El cliente, por ejemplo, puede enviar algo así.

{
  "jsonrpc": "2.0",
  "id": "req-002",
  "method": "GetTask",
  "params": {
    "id": "task-001",
    "historyLength": 0
  }
}

Y el Server responde con un working, que no sea pesado:

{
  "jsonrpc": "2.0",
  "id": "req-002",
  "result": {
    "id": "task-001",
    "contextId": "ctx-001",
    "status": {
      "state": "working",
      "timestamp": "2026-05-07T10:00:15Z"
    },
    "artifacts": []
  }
}

7.3.2. Streaming

En esta opción, el servidor mantiene la conexión abierta y va enviando eventos: “Estado cambiado a working”, “Progreso 50%”, “Nuevo Artifact generado”, “Estado: completed”. Es un sistema más eficiente y fluido que el polling, pero para entenderlo bien es importante recordar cómo funciona el protocolo Server-Sent Events (SSE).

SSE es un protocolo estándar del navegador que permite al servidor enviar datos al cliente de forma continua y unidireccional a través de una sola conexión HTTP. El cliente abre una conexión HTTP normal, pero el servidor nunca la cierra. En cambio, va enviando datos en un formato especial cada vez que tiene algo nuevo que decir:

data: Hola mundo\n\n
data: Segundo mensaje\n\n
event: alerta
data: {"tipo": "error", "msg": "algo falló"}\n\n

El cliente (en el navegador) lo consume así:

const source = new EventSource('/stream');

source.onmessage = (event) => {
  console.log(event.data);
};

source.addEventListener('alerta', (event) => {
  const data = JSON.parse(event.data);
  console.log(data.msg);
});

Entonces, cuando se utiliza este sistema, que se denomina SendMessageStream, el cliente realiza una petición POST y el servidor responde con un flujo de eventos (stream), donde cada evento es un objeto JSON-RPC.

A diferencia de una API de chat normal que solo hace streaming de texto, A2A hace streaming de todo el ciclo de vida de la Task:

  • Eventos de estado: “La tarea ha pasado de submitted a working”.
  • Eventos de progreso: “Llevo el 25%… 50%…”.
  • Eventos de contenido (Tokens): El texto que el LLM va generando palabra por palabra.
  • Eventos de artifacts: “He terminado de generar el primer archivo PDF; aquí tienes el enlace”.

Y esto se traduce en 3 tipos de mensajes que escucha el Client.

A. TaskStatusUpdateEvent. Informa sobre cambios en el estado de la Task. Por ejemplo: cambio a input_required porque el agente necesita que confirmes algo.

B. Message. El texto generado por el agente llega dentro de un objeto Message incluido en el stream. No es un tipo de evento separado: el servidor envía el Message completo (o en fragmentos sucesivos) como parte del StreamResponse.

C. TaskArtifactUpdateEvent. Si el agente está creando un archivo (un Artifact), avisa cuando empieza a crearlo, cuando hay contenido parcial y cuando está finalizado.

7.3.3. Webhooks

El Client registra una URL webhook y cuando la Task cambia de estado, el Server hace un HTTP POST a esa URL con el mismo formato de eventos que el streaming. Para activar este mecanismo, el cliente debe incluir en la configuration del SendMessage, en el parámetro taskPushNotificationConfig, un objeto que le diga al servidor a dónde tiene que llamar.

"taskPushNotificationConfig": {
  "url": "https://mi-sistema-cliente.com/api/webhooks/a2a",
  "authentication": {
    "schemes": ["bearer"],
    "credentials": "mi-token-secreto"
  }
}

Este sistema no requiere conexión persistente, lo que lo hace ideal para integraciones servidor a servidor o tareas muy largas.

Artifacts, han salido una y otra vez mencionados y es hora de explicar en detalle qué son : ).

8. Artifacts

8.1. Qué son

Los artifacts son los resultados que produce una Task. El Client pidió algo, el Server trabajó en ello, y los artifacts son lo que entrega. Si la Task es el proceso de cocina, el Artifact es el plato terminado que sale de la cocina.

Al igual que los mensajes, los Artifacts se basan en Parts. Esta es la paramétrica:

  • artifactId: ID único (generado por el servidor).
  • name: Un nombre legible (ej. “Informe_Trimestral.pdf”), opcional.
  • parts: Una lista de componentes. Debe haber al menos una y cada Part, a su vez, puede ser:
    • text: Contenido en texto plano (como un CSV o un JSON string).
    • raw: Datos binarios codificados en Base64 (ideal para archivos pequeños).
    • url: Un enlace a un almacenamiento externo (S3, Google Cloud Storage) donde el cliente puede descargar el archivo pesado.
  • metadata: Datos extra de libre configuración (ej. quién lo creó, fecha de expiración del enlace), opcional.

Por ejemplo:

{
  "artifactId": "artifact-001",
  "name": "Receta de tofu con espinacas",
  "description": "Receta completa con ingredientes y pasos",
  "parts": [
    {
      "text": "Ingredientes: 400g tofu firme, 200g espinacas...",
      "mediaType": "text/plain"
    },
    {
      "url": "https://agents.example.com/images/tofu-espinacas.jpg",
      "mediaType": "image/jpeg",
      "filename": "tofu-espinacas.jpg"
    }
  ]
}

Y esto debe ser coherente con lo que indicó el Client en acceptedOutputModes. Por ejemplo, si el cliente puso en su configuración:

acceptedOutputModes: ["application/json"]

El servidor no debería generar un Artifact de tipo image/jpeg. Si el agente solo sabe generar imágenes, la Task debería fallar con un estado rejected o failed porque no puede cumplir con el contrato de salida del cliente.

8.2. Una respuesta incremental

La diferencia fundamental con un sistema request-response normal es que, en A2A, los resultados no van en la respuesta a la petición inicial. Van apareciendo a lo largo de la vida de la Task, asociados a ella, y el Client los recibe según se van generando, ya sea por polling, streaming o push notifications.

Esto tiene sentido cuando el trabajo es complejo. Si le pides a un agente que analice un documento, prepare un informe ejecutivo, y genere una presentación, esos son tres resultados distintos que probablemente estarán listos en momentos diferentes. No tiene sentido esperar a que los tres estén listos para entregarlos todos a la vez. Con artifacts, el agente puede ir entregando cada resultado en cuanto está listo, y el Client puede empezar a trabajar con él sin esperar a los demás.

Ahora bien, este descubrimiento del estado de los Artifacts, depende del sistema que se esté usando para ir recibiendo las novedades.

8.2.1. Polling

Recordemos que esta estrategia consiste en que el Client vaya preguntando al Server: “¿Cómo vas con lo mío?”. Es el método recomendable si no puedes mantener conexiones abiertas (Stream) o si no tienes una IP pública para recibir avisos (Webhooks).

En este caso, normalmente, los Artifacts solo aparecen en la lista de artifacts de la Task cuando están completos o cuando el agente ha terminado una versión estable de los mismos. Y esto es algo ineficiente para archivos grandes. No se sabe si el agente está por la mitad de la generación o si se ha quedado colgado, a menos que el statusMessage de la Task dé pistas. Algo así:

  1. El Client envía una petición, que debería llevar el returnImmediately seteado a true.

  2. El servidor responde al instante con el “ticket”:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": {
    "task": {
      "id": "task-789",
      "status": { "state": "submitted" }
    }
  }
}

A partir de ahí, cada n segundos, el Client va preguntando por lo suyo definiendo GetTask en el method y usando el id de la task que recibió.

{
  "jsonrpc": "2.0",
  "id": "poll-001",
  "method": "GetTask",
  "params": {
    "id": "task-789"
  }
}

Y el ciclo de respuestas podría ser este.

Intento 1: El servidor responde con state: working. La lista artifacts está vacía [].

Intento 2: El servidor sigue en state: working, pero ya aparece algo:

"artifacts": [
  {
    "artifactId": "art-01",
    "name": "Borrador de Receta",
    "status": "partial"  // O simplemente aparece sin estar terminado
  }
]

Intento N (Final): El servidor responde con state: completed.

"status": { "state": "completed" },
  "artifacts": [
    {
      "artifactId": "art-01",
      "name": "Receta Final Tofu",
      "parts": [
          { "text": "Pasos para cocinar...", "mediaType": "text/markdown" },
          { "url": "https://storage.com/foto.jpg", "mediaType": "image/jpeg" }
      ]
    }
  ]

8.2.2. Streaming

Cuando el Client usa streaming, los artifacts no llegan como un objeto completo de golpe. Llegan a través de eventos TaskArtifactUpdateEvent, que tienen estos campos adicionales sobre el artifact en sí:

  • taskId y contextId: para saber a qué Task pertenece el evento.
  • artifact: el artifact o el trozo de artifact.
  • append:
    • si es true, el contenido de este evento debe concatenarse al artifact con el mismo artifactId que ya se había recibido antes,
    • si es false, el contenido actual debe sobrescribir lo anterior (útil para actualizaciones de estado o previsualizaciones que cambian totalmente).
  • lastChunk: si es true, este es el último trozo. El artifact está completo.

Esto se entiende bien con un ejemplo.

  1. En el primer envío se inicia el Artifact. El servidor anuncia el Artifact y envía el primer bloque de texto.
{
  "taskId": "task-001",
  "artifact": {
    "artifactId": "recipe-pdf",
    "name": "Receta de Tofu",
    "parts": [{ "text": "Ingredientes:\n- Tofu firme\n- Soja\n" }]
  },
  "append": false,
  "lastChunk": false
}
  1. El servidor envía un segundo bloque. El cliente ve append: true y lo pega a continuación del anterior.
{
  "taskId": "task-001",
  "artifact": {
    "artifactId": "recipe-pdf",
    "parts": [{ "text": "Instrucciones:\n1. Prensar el tofu durante 15 min.\n" }]
  },
  "append": true,
  "lastChunk": false
}
  1. El servidor envía el último fragmento y marca el final de la construcción.
{
  "taskId": "task-001",
  "artifact": {
    "artifactId": "recipe-pdf",
    "parts": [{ "text": "2. Marinar y freír hasta que esté crujiente." }]
  },
  "append": true,
  "lastChunk": true
}

Este sistema de “chunks” con los flags append y lastChunk está muy bien pensado, ya que permite que el Cliente sea muy eficiente. No necesita esperar a que un informe de 50 páginas esté terminado para empezar a procesarlo; puede ir guardando los trozos en un buffer o en un archivo temporal.

8.2.3. Webhooks

Los Webhooks se parecen mucho más al Streaming en cuanto a su filosofía de empuje (push), pero comparten con el Polling la naturaleza de las conexiones HTTP discretas.

Para entenderlo mejor, se pueden comparar basándose en quién tiene la iniciativa y cómo fluye la información:

1. La filosofía de “Push”, que es el parecido con Streaming

Tanto en los Webhooks como en el Streaming, el Client adopta una actitud pasiva. Una vez enviada la instrucción inicial, el Client se sienta a esperar. Es el Server quien, de forma proactiva, envía la información en cuanto ocurre algo relevante (un cambio de estado o la creación de un Artifact).

En ambos casos, el sistema es orientado a eventos: la información viaja del Server al Client sin que este la pida explícitamente cada vez.

2. La naturaleza de la conexión, el parecido con Polling

A diferencia del Streaming, donde hay un único grifo abierto por el que sale todo el chorro de datos, los Webhooks funcionan mediante peticiones HTTP independientes, igual que el Polling.

Cada vez que el Server tiene algo que comunicar, abre una nueva conexión, entrega el paquete (POST) y la cierra. Esto evita el problema de los timeouts de las conexiones largas que sufre el Streaming, pero a cambio requiere que el Client tenga un servidor escuchando.

Bueno, pues con esto espero que haya quedado claro qué es un Artifact. Vamos ahora con otro tema chulo: la manera en la que el Server puede solicitar más información, tal y como a veces hacen los agentes en los chats web.

9. Una conversación

9.1. Input required

A veces puede ocurrir que el Server necesite algo del Client antes de seguir con la Task y entonces puede solicitárselo enviando un Message con status en input_required explicando qué necesita. Este mecanismo es lo que permite que una tarea no sea simplemente un proceso lineal, sino una conversación interactiva.

Por ejemplo, está preparando una receta y se da cuenta de que no sabe si el usuario tiene alguna alergia, o si prefiere la versión picante o suave. El Server entonces transiciona la Task a input_required y adjunta un Message en el status explicando qué necesita. Ese Message es lo que el Client debe mostrar al usuario.

{
  "status": {
    "state": "input_required",
    "message": {
      "messageId": "msg-003",
      "role": "agent",
      "parts": [{ "text": "¿Tienes alguna alergia o preferencia dietética que deba tener en cuenta?" }]
    },
    "timestamp": "2026-05-07T10:00:20Z"
  }
}

El Client responde con un nuevo SendMessage referenciando el taskId y el contextId de esa Task:

{
  "jsonrpc": "2.0",
  "id": "req-003",
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg-004",
      "role": "user",
      "taskId": "task-001",
      "contextId": "ctx-001",
      "parts": [{ "text": "Soy alérgico al sésamo." }]
    }
  }
}

9.2. Multi-turn

También puede suceder que sea el Client quien siga necesitando cosas del Server una vez que haya respondido. Se inicia entonces una conversación multi-turn, que es la capacidad del protocolo de mantener una conversación coherente a lo largo de varios intercambios. No es una operación especial, es simplemente seguir usando SendMessage pero con los identificadores adecuados para que el Server entienda que es una continuación.

Hay dos identificadores clave:

  • contextId: agrupa toda la conversación. Todos los mensajes y Tasks que pertenecen a la misma sesión comparten el mismo contextId. Lo genera el Server en la primera respuesta y el Client lo incluye en los mensajes siguientes.
  • taskId: referencia una Task concreta. Si el Client lo incluye, está hablando específicamente de esa Task.

Se pueden combinar de varias formas:

  • solo contextId: el Client continúa la conversación pero arranca una Task nueva dentro del mismo contexto. “Ahora hazme una receta de tempeh.”
  • taskId y contextId: el Client se refiere a una Task concreta dentro del contexto. “Esa receta que me diste antes, hazla más picante.”
  • solo taskId: el Server infiere el contextId a partir de la Task.

Lo que no puede ocurrir es que el contextId y el taskId no coincidan, es decir, que el taskId pertenezca a un contexto diferente al contextId proporcionado. El Server rechazaría esa petición.

10. Autenticación

La filosofía de A2A sobre la autenticación es clara: el protocolo no define cómo autenticarse, define cómo descubrir qué autenticación se requiere. El mecanismo en sí ocurre fuera de A2A usando estándares existentes.

El flujo básico tiene tres pasos:

  • El Client lee la Agent Card y encuentra los esquemas de autenticación declarados.
  • El Client obtiene las credenciales por su cuenta usando el mecanismo correspondiente.
  • El Client incluye esas credenciales en las cabeceras HTTP de cada petición.

Los esquemas soportados son los estándar de la industria, declarados en el campo securitySchemes de la Agent Card:

  • API Key: una clave fija que el Client incluye en una cabecera o parámetro. Simple pero menos seguro.
  • Bearer / HTTP Auth: un token en la cabecera Authorization. El más habitual para APIs modernas.
  • OAuth2: el Client hace el flujo OAuth contra el servidor de autorización, obtiene un JWT, y lo manda como Bearer token. Soporta varios flujos: authorization code, client credentials, device code. OpenID Connect: similar a OAuth2 pero orientado a identidad.
  • Etcétera.

A veces se puede definir una extendedAgentCard para usuarios autenticados con datos más sensibles. La idea es simple, un agente puede querer mostrar información pública básica a cualquiera, pero revelar detalles adicionales solo a Clients de confianza. Por ejemplo, skills internas que no deben ser públicas, configuraciones específicas para partners, o información sensible sobre capacidades que no quieres exponer libremente.

El mecanismo funciona así: si la Agent Card pública declara capabilities.extendedAgentCard: true, el Client sabe que existe una versión ampliada. Para obtenerla, se autentica y llama a GetExtendedAgentCard. El Server devuelve una Agent Card con información adicional.

{
  "capabilities": {
    "streaming": true,
    "pushNotifications": false,
    "extendedAgentCard": true
  }
}

Una vez el Client obtiene la Extended Agent Card, debe reemplazar la card pública que tenía cacheada con esta nueva versión para el resto de la sesión. Hay dos cosas importantes a tener en cuenta.

La primera es que si el agente declara extendedAgentCard: true pero no tiene ninguna card extendida configurada, devolverá un ExtendedAgentCardNotConfiguredError. Es decir, declarar el soporte y tenerlo implementado son dos cosas distintas.

La segunda es que la Extended Agent Card no es un mecanismo de autorización granular. No es que cada Client vea una card diferente según sus permisos. Es simplemente una card pública y una card autenticada. Si necesitas algo más granular, tendrás que gestionarlo fuera del protocolo.

Más o menos con esto estaría concluida esta primera aproximación a la teoría. A lo largo de este tutorial hemos visto los conceptos fundamentales del protocolo: cómo un agente se presenta al mundo mediante su Agent Card, cómo fluye una tarea desde el primer SendMessage hasta el último artifact, cómo se gestiona la conversación multi-turn, y cómo se resuelven situaciones como la necesidad de más información o de autorización adicional a mitad de un proceso.

Es mucho andamiaje conceptual, pero tiene una lógica interna coherente. Cada pieza existe para resolver un problema concreto del escenario de comunicación entre agentes: el descubrimiento, la asincronía, la entrega incremental de resultados, la interrupción controlada, la autenticación sin acoplamientos.

En el siguiente tutorial veremos el SDK oficial de Python, que permite implementar todo esto sin tener que construir desde cero el protocolo. Y comprobaremos que, conociendo la teoría, el SDK resulta mucho más legible: cada clase, cada método, cada evento tiene un correlato directo con lo que hemos visto aquí.

Porque ese es precisamente el valor de entender la base antes de usar la herramienta: no es que el SDK sea difícil sin ella, es que con ella sabes exactamente qué está pasando debajo y por qué.

De momento vamos a dejarlo aquí.

Si alguien quiere profundizar, hay dos referencias fundamentales: