Protocolo Agent-to-Agent (A2A)

ADK-Rust implementa el A2A Protocol v1.0.0 para la comunicación entre agentes a través de redes. La implementación vive en adk-server detrás del indicador de función a2a-v1 y cubre las 11 operaciones JSON-RPC, los enlaces REST, el descubrimiento de la tarjeta del agente y la negociación de versiones. Consulta Cobertura de operaciones para la única operación cuya semántica es más restringida de lo que permite la especificación. Los tipos de wire los proporciona a2a-protocol-types — el A2A SDK de Rust verificado por la Foundation de @tomtom215 (a2a-rust).

Descripción general

A2A es útil cuando:

  • Integras con servicios de agentes de terceros
  • Construyes arquitecturas de microservicios con agentes especializados
  • Permites la comunicación entre agentes en distintos lenguajes (cualquier lenguaje con un cliente A2A)
  • Haces cumplir contratos formales entre sistemas de agentes

Para una organización interna simple, usa subagentes locales en lugar de A2A para obtener mejor rendimiento.

Conformidad con v1.0.0

La implementación cumple completamente con la especificación A2A Protocol v1.0.0:

FuncionalidadSección de la especificaciónEstado
Tarjeta del agente con declaración de capacidades§8
Marcas de tiempo RFC 3339 en todos los cambios de estado de la tarea§5.6.1
Idempotencia del ID del mensaje para SendMessage§3.3.1
Autenticación de notificación push (Bearer + token)§13.2
Flujo de reanudación de varios turnos INPUT_REQUIRED§3.4.3
Validación de entrada (partes, IDs, tamaño de metadatos)§3.3
Content-Type: application/a2a+json en respuestas§9
Objeto de tarea como primer evento de streaming de SSE§3.1.2
Búsqueda de tarea con ámbito de contexto para multivuelta§3.4.1
Negociación de versión (encabezado A2A-Version)§9.1
Validación de la máquina de estados (estados terminales)§4.1.3

Tarjetas de agente

Cada agente A2A expone una tarjeta de agente en /.well-known/agent-card.json que describe sus capacidades, habilidades e interfaces compatibles.

use adk_server::a2a::v1::card::build_v1_agent_card;
use a2a_protocol_types::{AgentCapabilities, AgentSkill};

let card = build_v1_agent_card(
    "my-agent",
    "A helpful research agent",
    "http://localhost:3001/jsonrpc",
    "1.0.0",
    vec![AgentSkill {
        id: "research".to_string(),
        name: "Research & Summarize".to_string(),
        description: "Researches topics and produces structured summaries".to_string(),
        tags: vec!["research".to_string()],
        examples: None,
        input_modes: None,
        output_modes: None,
        security_requirements: None,
    }],
    AgentCapabilities::none()
        .with_streaming(true)
        .with_push_notifications(true),
);

La tarjeta de agente incluye:

  • Nombre, descripción y versión del agente
  • Interfaces compatibles con enlace de protocolo y versión
  • Capacidades: streaming, pushNotifications, extendedAgentCard
  • Habilidades derivadas de la configuración del agente
  • Modos de entrada/salida predeterminados

Las capacidades ahora se declaran explícitamente mediante el parámetro AgentCapabilities — ya no hay valores predeterminados codificados de forma fija.

Exponer un agente mediante A2A v1

Crea un servidor completo A2A v1.0.0 con integración LLM:

use std::sync::Arc;
use a2a_protocol_types::{AgentCapabilities, AgentSkill};
use adk_agent::LlmAgentBuilder;
use adk_server::a2a::v1::card::{CachedAgentCard, build_v1_agent_card};
use adk_server::a2a::v1::executor::V1Executor;
use adk_server::a2a::v1::jsonrpc_handler::jsonrpc_handler;
use adk_server::a2a::v1::push::NoOpPushNotificationSender;
use adk_server::a2a::v1::request_handler::RequestHandler;
use adk_server::a2a::v1::rest_handler::rest_router;
use adk_server::a2a::v1::task_store::InMemoryTaskStore;
use adk_server::a2a::v1::version::version_negotiation;
use adk_runner::RunnerConfig;
use adk_session::InMemorySessionService;
use axum::Router;
use axum::routing::post;
use tokio::sync::RwLock;

// 1. Create your agent
let model = adk_model::GeminiModel::new(&api_key, "gemini-2.5-flash")?;
let agent = LlmAgentBuilder::new("my-agent")
    .description("A helpful agent")
    .model(Arc::new(model))
    .instruction("You are a helpful assistant.")
    .build()?;

// 2. Set up A2A infrastructure
let task_store = Arc::new(InMemoryTaskStore::new());
let executor = Arc::new(V1Executor::new(task_store.clone()));
let push_sender = Arc::new(NoOpPushNotificationSender);

// 3. Build agent card with capabilities
let card = build_v1_agent_card(
    "my-agent", "A helpful agent",
    "http://localhost:3001/jsonrpc", "1.0.0",
    vec![/* skills */],
    AgentCapabilities::none().with_streaming(true),
);
let cached_card = Arc::new(RwLock::new(CachedAgentCard::new(card)));

// 4. Create runner config for LLM invocation
let session_service = Arc::new(InMemorySessionService::new());
let runner_config = Arc::new(RunnerConfig {
    app_name: "my-agent".to_string(),
    agent: Arc::new(agent),
    session_service,
    artifact_service: None,
    memory_service: None,
    plugin_manager: None,
    run_config: None,
    compaction_config: None,
    context_cache_config: None,
    cache_capable: None,
    request_context: None,
    cancellation_token: None,
});

// 5. Wire up the handler and routes
let handler = Arc::new(RequestHandler::with_runner(
    executor, task_store, push_sender, cached_card, runner_config,
));

let app = Router::new()
    .route("/jsonrpc", post(jsonrpc_handler))
    .with_state(handler.clone())
    .merge(rest_router(handler))
    .layer(axum::middleware::from_fn(version_negotiation));

// 6. Serve
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await?;
axum::serve(listener, app).await?;

Esto expone:

  • GET /.well-known/agent-card.json — Tarjeta de agente con caché ETag
  • POST /jsonrpc — punto de conexión JSON-RPC (las 11 operaciones v1; consulta Cobertura de operaciones)
  • Rutas REST para todas las operaciones
  • Negociación de encabezados A2A-Version en todas las rutas

Operaciones JSON-RPC

Se admiten las 11 operaciones A2A v1.0.0:

MétodoDescripción
SendMessageEnviar un mensaje, crear/reanudar una tarea
SendStreamingMessageIgual que SendMessage pero devuelve un flujo SSE
GetTaskRecuperar una tarea por ID
CancelTaskCancelar una tarea en ejecución
ListTasksListar tareas con filtrado y paginación
SubscribeToTaskSuscribirse a las actualizaciones de la tarea mediante SSE
CreateTaskPushNotificationConfigRegistrar un webhook para notificaciones push
GetTaskPushNotificationConfigRecuperar una configuración de notificación push
ListTaskPushNotificationConfigsListar configuraciones push para una tarea
DeleteTaskPushNotificationConfigEliminar una configuración de notificación push
GetExtendedAgentCardRecuperar la tarjeta ampliada del agente

SendMessage

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg-123",
      "role": "ROLE_USER",
      "parts": [{"text": "Research quantum computing"}]
    }
  }
}

La respuesta incluye un objeto Task con status, history y artifacts. La respuesta usa Content-Type: application/a2a+json.

SendStreamingMessage

El mismo formato de solicitud que SendMessage. Devuelve un flujo SSE donde:

  1. El primer evento es un objeto Task completo (según la especificación §3.1.2)
  2. Los eventos posteriores son TaskStatusUpdateEvent (Working, Completed, etc.)
  3. Los eventos de artifacts son TaskArtifactUpdateEvent

Conversaciones de varios turnos

Cuando una task alcanza el estado INPUT_REQUIRED, envía un mensaje de seguimiento con el mismo contextId para reanudarla:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg-456",
      "role": "ROLE_USER",
      "contextId": "ctx-original",
      "parts": [{"text": "Yes, include more details on error correction"}]
    }
  }
}

El handler encuentra automáticamente la task existente por contextId, la transiciona de INPUT_REQUIRED a Working, añade el nuevo mensaje al history y continúa el procesamiento.

Idempotencia

Las solicitudes duplicadas de SendMessage con el mismo messageId devuelven la task creada previamente sin volver a procesarla. Esto se aplica tanto a SendMessage como a SendStreamingMessage.

Autenticación de notificaciones push

Cuando un cliente registra un webhook mediante CreateTaskPushNotificationConfig, el servidor incluye encabezados de autenticación en las entregas del webhook:

  • Authorization: Bearer <credentials> — cuando el campo authentication tiene credenciales bearer
  • a2a-notification-token: <token> — cuando el campo token está presente

Ambos encabezados pueden configurarse simultáneamente. La protección contra SSRF valida los URLs del webhook frente a rangos de IP privadas y localhost.

Validación de entrada

Todas las solicitudes entrantes se validan antes de procesarse:

ValidaciónError
Mensaje con cero partesInvalidParams (-32602)
messageId vacío o que contiene solo espacios en blancoInvalidParams (-32602)
messageId que supera los 256 caracteresInvalidParams (-32602)
taskId vacío o que contiene solo espacios en blancoInvalidParams (-32602)
taskId que supera los 256 caracteresInvalidParams (-32602)
Metadatos que superan los 64 KBInvalidParams (-32602)

Consumir un agente remoto

Usa RemoteA2aAgent para comunicarte con un agente remoto A2A:

use adk_server::a2a::RemoteA2aAgent;

let remote_agent = RemoteA2aAgent::builder("prime_checker")
    .description("Checks if numbers are prime")
    .agent_url("http://localhost:8001")
    .build()?;

// Use as a sub-agent in a local agent hierarchy
let root_agent = LlmAgentBuilder::new("root")
    .model(Arc::new(model))
    .sub_agent(Arc::new(remote_agent))
    .build()?;

Cliente A2A

Para comunicación directa a nivel de protocolo:

use adk_server::a2a::client::v1_client::A2aV1Client;

// Discover agent card
let card = A2aV1Client::resolve_agent_card("http://localhost:3001").await?;
let client = A2aV1Client::new(card);

// Send message
let task = client.send_message(message).await?;

// Get task
let task = client.get_task(&task_id, Some(10)).await?;

// List tasks
let tasks = client.list_tasks(None, None, None, None).await?;

// Cancel task
client.cancel_task(&task_id).await?;

// Streaming
let response = client.send_streaming_message(message).await?;

// Push notification CRUD
let config = client.create_push_notification_config(config).await?;
client.delete_push_notification_config(&task_id, &config_id).await?;

Manejo de errores

Los errores A2A se asignan tanto a códigos JSON-RPC como a códigos de estado HTTP:

ErrorJSON-RPC CódigoHTTP Estado
TaskNotFound-32001404
TaskNotCancelable-32002409
PushNotificationNotSupported-32003400
UnsupportedOperation-32004400
ContentTypeNotSupported-32005415
InvalidAgentResponse-32006502
VersionNotSupported-32009400
InvalidParams-32602400
MethodNotFound-32601404
Interno-32603500

Ejecutar los ejemplos

Se incluyen dos agentes de ejemplo completos A2A v1.0.0:

cargo run --manifest-path examples/a2a-research-agent/Cargo.toml
cargo run --manifest-path examples/a2a-writing-agent/Cargo.toml --bin a2a-writing-agent
cargo run --manifest-path examples/a2a-writing-agent/Cargo.toml --bin client

El cliente valida: descubrimiento de la tarjeta del agente, SendMessage (ambos agentes con LLM reales), GetTask, ListTasks, ruta de error de CancelTask, SendStreamingMessage, notificación push de CRUD, GetExtendedAgentCard, negociación de versiones y rutas de error.

Mejores prácticas

  1. Declara las capacidades con precisión — establece streaming, pushNotifications según lo que tu agente realmente admite
  2. Usa streaming para operaciones largasSendStreamingMessage ofrece a los clientes progreso en tiempo real
  3. Gestiona flujos de varias vueltas — usa contextId para mantener el estado de la conversación entre mensajes
  4. Valida el webhook URLs — la protección contra SSRF está integrada, pero usa HTTPS en producción
  5. Define tiempos de espera apropiados — configura tiempos de espera de solicitudes para llamadas a agentes remotos
  6. Usa idempotencia — los clientes pueden reintentar de forma segura SendMessage con el mismo messageId

Anterior: ← Servidor | Siguiente: Evaluación →

Cobertura de operaciones

Las 11 operaciones v1 JSON-RPC se despachan e implementan.

OperaciónEstado
SendMessageImpulsa al agente y registra su salida como un artefacto
SendStreamingMessageImpulsa al agente y transmite fragmentos del artefacto a medida que se producen
GetTask, ListTasksCompleto
CancelTaskCompleto
SubscribeToTask (tasks/resubscribe)Solo instantánea — ver abajo
CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfigCompleto
GetExtendedAgentCardCompleto

SubscribeToTask es una instantánea

La operación devuelve la tarea y su estado actual, y luego cierra el flujo. No entrega actualizaciones posteriores, así que un cliente no debe esperar en ella para ver el progreso.

Una nueva reanudación en vivo requiere una cola de eventos por tarea que sobreviva a la solicitud original. Las implementaciones de referencia obtienen esto de su A2A SDKs — adk-python y adk-go delegan tasks/resubscribe por completo al gestor de colas de SDK, y ninguna lo implementa en código ADK. Este servidor está construido a mano sobre a2a-protocol-types, que proporciona tipos de wire en lugar de un runtime de servidor, así que la cola aún no existe.

Usa SendStreamingMessage cuando se requieran actualizaciones en vivo.

Contrato de eventos de streaming

SendStreamingMessage traduce los eventos del agent a medida que llegan:

Evento del agenteA2A evento
primero, antes de la salidaTask, luego TaskStatusUpdateEventWorking
contenido, partial = trueTaskArtifactUpdateEventappend, no el último fragmento
contenido, partial = falseTaskArtifactUpdateEvent — fragmento final
la transmisión terminaTaskStatusUpdateEventCompleted
errores de la transmisiónTaskStatusUpdateEventFailed

Todos los fragmentos de una respuesta comparten un ID de artefacto para que un cliente pueda volver a ensamblarlos. El texto unido se conserva, así que una GetTask posterior devuelve lo que se transmitió. Esto coincide con el contrato que adk-python y adk-go implementan sobre su SDKs.

Protocolo Agent-to-Agent (A2A) - Documentación ADK-Rust | ADK-Rust