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:
| Funcionalidad | Sección de la especificación | Estado |
|---|---|---|
| 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é ETagPOST /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-Versionen todas las rutas
Operaciones JSON-RPC
Se admiten las 11 operaciones A2A v1.0.0:
| Método | Descripción |
|---|---|
SendMessage | Enviar un mensaje, crear/reanudar una tarea |
SendStreamingMessage | Igual que SendMessage pero devuelve un flujo SSE |
GetTask | Recuperar una tarea por ID |
CancelTask | Cancelar una tarea en ejecución |
ListTasks | Listar tareas con filtrado y paginación |
SubscribeToTask | Suscribirse a las actualizaciones de la tarea mediante SSE |
CreateTaskPushNotificationConfig | Registrar un webhook para notificaciones push |
GetTaskPushNotificationConfig | Recuperar una configuración de notificación push |
ListTaskPushNotificationConfigs | Listar configuraciones push para una tarea |
DeleteTaskPushNotificationConfig | Eliminar una configuración de notificación push |
GetExtendedAgentCard | Recuperar 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:
- El primer evento es un objeto
Taskcompleto (según la especificación §3.1.2) - Los eventos posteriores son
TaskStatusUpdateEvent(Working, Completed, etc.) - 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 campoauthenticationtiene credenciales bearera2a-notification-token: <token>— cuando el campotokenestá 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ón | Error |
|---|---|
| Mensaje con cero partes | InvalidParams (-32602) |
| messageId vacío o que contiene solo espacios en blanco | InvalidParams (-32602) |
| messageId que supera los 256 caracteres | InvalidParams (-32602) |
| taskId vacío o que contiene solo espacios en blanco | InvalidParams (-32602) |
| taskId que supera los 256 caracteres | InvalidParams (-32602) |
| Metadatos que superan los 64 KB | InvalidParams (-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:
| Error | JSON-RPC Código | HTTP Estado |
|---|---|---|
| TaskNotFound | -32001 | 404 |
| TaskNotCancelable | -32002 | 409 |
| PushNotificationNotSupported | -32003 | 400 |
| UnsupportedOperation | -32004 | 400 |
| ContentTypeNotSupported | -32005 | 415 |
| InvalidAgentResponse | -32006 | 502 |
| VersionNotSupported | -32009 | 400 |
| InvalidParams | -32602 | 400 |
| MethodNotFound | -32601 | 404 |
| Interno | -32603 | 500 |
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
- Declara las capacidades con precisión — establece
streaming,pushNotificationssegún lo que tu agente realmente admite - Usa streaming para operaciones largas —
SendStreamingMessageofrece a los clientes progreso en tiempo real - Gestiona flujos de varias vueltas — usa
contextIdpara mantener el estado de la conversación entre mensajes - Valida el webhook URLs — la protección contra SSRF está integrada, pero usa HTTPS en producción
- Define tiempos de espera apropiados — configura tiempos de espera de solicitudes para llamadas a agentes remotos
- Usa idempotencia — los clientes pueden reintentar de forma segura
SendMessagecon el mismomessageId
Relacionado
- LlmAgent — Creación de agentes
- Sistemas multiagente — Subagentes y jerarquías
- Despliegue del servidor — Ejecutar agentes como servidores HTTP
Anterior: ← Servidor | Siguiente: Evaluación →
Cobertura de operaciones
Las 11 operaciones v1 JSON-RPC se despachan e implementan.
| Operación | Estado |
|---|---|
SendMessage | Impulsa al agente y registra su salida como un artefacto |
SendStreamingMessage | Impulsa al agente y transmite fragmentos del artefacto a medida que se producen |
GetTask, ListTasks | Completo |
CancelTask | Completo |
SubscribeToTask (tasks/resubscribe) | Solo instantánea — ver abajo |
CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfig | Completo |
GetExtendedAgentCard | Completo |
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 agente | A2A evento |
|---|---|
| primero, antes de la salida | Task, luego TaskStatusUpdateEvent — Working |
contenido, partial = true | TaskArtifactUpdateEvent — append, no el último fragmento |
contenido, partial = false | TaskArtifactUpdateEvent — fragmento final |
| la transmisión termina | TaskStatusUpdateEvent — Completed |
| errores de la transmisión | TaskStatusUpdateEvent — Failed |
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.