Protocolo de agente a agente (A2A)

ADK-Rust implementa el A2A Protocol v1.0.0 para la comunicación de agentes entre redes. La implementación se encuentra en adk-server detrás del indicador de funcionalidad a2a-v1 y abarca las 11 operaciones JSON-RPC, los enlaces REST, el descubrimiento de tarjetas de agente y la negociación de versiones. Consulta Cobertura de operaciones para conocer la única operación cuya semántica es más limitada de lo que permite la especificación. Los tipos de protocolo son proporcionados por a2a-protocol-types: el A2A SDK de Rust verificado por la Foundation creado por @tomtom215 (a2a-rust).

Descripción general

A2A resulta útil para:

  • Integrarse con servicios de agentes de terceros
  • Crear arquitecturas de microservicios con agentes especializados
  • Habilitar la comunicación entre agentes escritos en distintos lenguajes (cualquier lenguaje con un cliente A2A)
  • Aplicar contratos formales entre sistemas de agentes

Para una organización interna sencilla, utiliza subagentes locales en lugar de A2A para obtener un mejor rendimiento.

Cumplimiento de v1.0.0

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

FunciónSecció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 notificaciones push (Bearer + token)§13.2✅
Flujo de reanudación de varios turnos de INPUT_REQUIRED§3.4.3✅
Validación de entradas (partes, ID, tamaño de metadatos)§3.3✅
Content-Type: application/a2a+json en las respuestas§9✅
Objeto de tarea como primer evento de transmisión de SSE§3.1.2✅
Búsqueda de tareas con alcance de contexto para múltiples turnos§3.4.1✅
Negociación de versiones (encabezado A2A-Version)§9.1✅
Validación de la máquina de estados (estados terminales)§4.1.3✅

Tarjetas de agentes

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 del 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.

Exponer un agente mediante A2A v1

Construye un servidor A2A v1.0.0 completo con integración de 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-3.7-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 del agente con almacenamiento en caché ETag
  • POST /jsonrpc — Endpoint JSON-RPC (las 11 operaciones de v1; consulta Cobertura de operaciones)
  • Rutas REST para todas las operaciones
  • Negociación del encabezado A2A-Version en todas las rutas

Operaciones JSON-RPC

Se admiten las 11 operaciones de A2A v1.0.0:

MétodoDescripción
SendMessageEnviar un mensaje, crear o reanudar una tarea
SendStreamingMessageIgual que SendMessage, pero devuelve un flujo de SSE
GetTaskRecuperar una tarea por ID
CancelTaskCancelar una tarea en ejecución
ListTasksEnumerar tareas con filtrado y paginación
SubscribeToTaskSuscribirse a las actualizaciones de tareas mediante SSE
CreateTaskPushNotificationConfigRegistrar un webhook para notificaciones push
GetTaskPushNotificationConfigRecuperar una configuración de notificaciones push
ListTaskPushNotificationConfigsEnumerar las configuraciones push de una tarea
DeleteTaskPushNotificationConfigEliminar una configuración de notificaciones push
GetExtendedAgentCardRecuperar la tarjeta extendida 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 estado, historial y artefactos. La respuesta utiliza 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 (En progreso, Completado, etc.)
  3. Los eventos de artefactos son TaskArtifactUpdateEvent

Conversaciones de varios turnos

Cuando una tarea 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 controlador encuentra automáticamente la tarea existente mediante contextId, la cambia de INPUT_REQUIRED a Working, añade el nuevo mensaje al historial y continúa el procesamiento.

Idempotencia

Las solicitudes SendMessage duplicadas con el mismo messageId devuelven la tarea creada anteriormente 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 contiene credenciales de portador
  • a2a-notification-token: <token> — cuando el campo token está presente

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

Validación de entradas

Todas las solicitudes entrantes se validan antes del procesamiento:

ValidaciónError
Mensaje sin partesInvalidParams (-32602)
messageId vacío o compuesto únicamente por espacios en blancoInvalidParams (-32602)
messageId con más de 256 caracteresInvalidParams (-32602)
taskId vacío o compuesto únicamente por espacios en blancoInvalidParams (-32602)
taskId con más de 256 caracteresInvalidParams (-32602)
Metadatos de más de 64 KBInvalidParams (-32602)

Consumir un agente remoto

Usa RemoteA2aAgent para comunicarte con un agente A2A remoto:

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 la 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?;

Gestión de errores

Los errores de 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

Ejecución de los ejemplos

Se incluyen dos agentes de ejemplo completos de 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: el descubrimiento de la tarjeta del agente, SendMessage (ambos agentes con LLM reales), GetTask, ListTasks, la ruta de error de CancelTask, SendStreamingMessage, la CRUD de notificaciones push, GetExtendedAgentCard, la negociación de versiones y las rutas de error.

Prácticas recomendadas

  1. Declara las capacidades con precisión — establece streaming y pushNotifications según lo que tu agente admita realmente
  2. Usa streaming para operaciones largas — SendStreamingMessage proporciona a los clientes información del progreso en tiempo real
  3. Gestiona los flujos de varios turnos — usa contextId para mantener el estado de la conversación entre mensajes
  4. Valida URLs de webhook — la protección contra SSRF está integrada, pero usa HTTPS en producción
  5. Establece tiempos de espera adecuados — configura tiempos de espera para las solicitudes a agentes remotos
  6. Usa la idempotencia — los clientes pueden reintentar de forma segura SendMessage con el mismo messageId

Anterior: ← Servidor | Siguiente: Evaluación →

Cobertura de operaciones

Las 11 operaciones de JSON-RPC v1 se envían y se implementan.

OperaciónEstado
SendMessageDirige al agente y registra su salida como un artefacto
SendStreamingMessageDirige al agente y transmite fragmentos del artefacto a medida que se producen
GetTask, ListTasksCompleto
CancelTaskCompleto
SubscribeToTask (tasks/resubscribe)Solo instantánea — consulta 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, por lo que el cliente no debe esperar progreso de ella.

Una reconexió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: tanto adk-python como adk-go delegan por completo tasks/resubscribe en el gestor de colas de SDK, y ninguna lo implementa en código ADK. Este servidor está implementado manualmente sobre a2a-protocol-types, que proporciona tipos de protocolo en lugar de un entorno de ejecución del servidor, por lo que la cola aún no existe.

Usa SendStreamingMessage cuando se requieran actualizaciones en vivo.

Contrato de eventos de transmisión

SendStreamingMessage traduce los eventos del agente a medida que llegan:

Evento del agenteevento A2A
primero, antes de la salidaTask, luego TaskStatusUpdateEvent — Working
contenido, partial = trueTaskArtifactUpdateEvent — append, no es el último fragmento
contenido, partial = falseTaskArtifactUpdateEvent — fragmento final
el flujo terminaTaskStatusUpdateEvent — Completed
errores del flujoTaskStatusUpdateEvent — Failed

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