Protocole Agent-à-Agent (A2A)

ADK-Rust implémente le protocole A2A v1.0.0 pour la communication inter-agents entre réseaux. L’implémentation se trouve dans adk-server derrière l’indicateur de fonctionnalité a2a-v1 et couvre les 11 opérations JSON-RPC, les liaisons REST, la découverte des fiches d’agent et la négociation de version. Consultez Couverture des opérations pour connaître l’opération dont la sémantique est plus restreinte que ne l’autorise la spécification. Les types filaires sont fournis par a2a-protocol-types — le A2A SDK Rust vérifié par la Foundation, par @tomtom215 (a2a-rust).

Vue d’ensemble

A2A est utile pour :

  • Intégrer des services d’agents tiers
  • Concevoir des architectures de microservices avec des agents spécialisés
  • Permettre la communication inter-agents entre différents langages (tout langage disposant d’un client A2A)
  • Faire respecter des contrats formels entre les systèmes d’agents

Pour une organisation interne simple, utilisez des sous-agents locaux plutôt que A2A afin d’obtenir de meilleures performances.

Conformité v1.0.0

L’implémentation est entièrement conforme à la spécification du protocole A2A v1.0.0 :

FonctionnalitéSection de la spécificationStatut
Fiche d’agent avec déclaration des capacités§8✅
Horodatages RFC 3339 pour tous les changements d’état des tâches§5.6.1✅
Idempotence de l’ID de message pour SendMessage§3.3.1✅
Authentification des notifications push (Bearer + jeton)§13.2✅
Flux de reprise multi-tours de INPUT_REQUIRED§3.4.3✅
Validation des entrées (parties, identifiants, taille des métadonnées)§3.3✅
Content-Type: application/a2a+json dans les réponses§9✅
Objet de tâche comme premier événement de streaming SSE§3.1.2✅
Recherche de tâche limitée au contexte pour les interactions multi-tours§3.4.1✅
Négociation de version (en-tête A2A-Version)§9.1✅
Validation de la machine à états (états terminaux)§4.1.3✅

Cartes d’agent

Chaque agent A2A expose une carte d’agent à /.well-known/agent-card.json décrivant ses capacités, ses compétences et ses interfaces prises en charge.

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 carte d’agent comprend :

  • Le nom, la description et la version de l’agent
  • Les interfaces prises en charge avec la liaison de protocole et la version
  • Les capacités : streaming, pushNotifications, extendedAgentCard
  • Les compétences dérivées de la configuration de l’agent
  • Les modes d’entrée et de sortie par défaut

Les capacités sont désormais déclarées explicitement via le paramètre AgentCapabilities — fini les valeurs par défaut codées en dur.

Exposer un agent via A2A v1

Construisez un serveur A2A v1.0.0 complet avec l’intégration 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?;

Cela expose :

  • GET /.well-known/agent-card.json — Carte d’agent avec mise en cache ETag
  • POST /jsonrpc — Point de terminaison JSON-RPC (les 11 opérations de la v1 ; voir Couverture des opérations)
  • Les routes REST pour toutes les opérations
  • La négociation des en-têtes A2A-Version sur toutes les routes

Opérations JSON-RPC

Les 11 opérations A2A v1.0.0 sont prises en charge :

MéthodeDescription
SendMessageEnvoyer un message, créer/reprendre une tâche
SendStreamingMessageIdentique à SendMessage, mais renvoie le flux SSE
GetTaskRécupérer une tâche par ID
CancelTaskAnnuler une tâche en cours d’exécution
ListTasksLister les tâches avec filtrage et pagination
SubscribeToTaskS’abonner aux mises à jour de tâche via SSE
CreateTaskPushNotificationConfigEnregistrer un webhook pour les notifications push
GetTaskPushNotificationConfigRécupérer une configuration de notification push
ListTaskPushNotificationConfigsRépertorier les configurations push d’une tâche
DeleteTaskPushNotificationConfigSupprimer une configuration de notification push
GetExtendedAgentCardRécupérer la fiche étendue de l’agent

SendMessage

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

La réponse inclut un objet Task avec un statut, un historique et des artefacts. La réponse utilise Content-Type: application/a2a+json.

SendStreamingMessage

Même format de requête que SendMessage. Renvoie un flux SSE où :

  1. Le premier événement est un objet Task complet (conformément à la spécification §3.1.2)
  2. Les événements suivants sont des TaskStatusUpdateEvent (Working, Completed, etc.)
  3. Les événements d’artefact sont des TaskArtifactUpdateEvent

Conversations multi-tours

Lorsqu’une tâche atteint l’état INPUT_REQUIRED, envoyez un message de suivi avec le même contextId pour la reprendre :

{
  "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"}]
    }
  }
}

Le gestionnaire recherche automatiquement la tâche existante par contextId, la fait passer de INPUT_REQUIRED à Working, ajoute le nouveau message à l’historique et poursuit le traitement.

Idempotence

Les requêtes SendMessage en double avec le même messageId renvoient la tâche précédemment créée sans effectuer de nouveau traitement. Cela s’applique à la fois à SendMessage et à SendStreamingMessage.

Authentification des notifications push

Lorsqu’un client enregistre un webhook via CreateTaskPushNotificationConfig, le serveur inclut des en-têtes d’authentification dans les livraisons au webhook :

  • Authorization: Bearer <credentials> — lorsque le champ authentication contient des identifiants bearer
  • a2a-notification-token: <token> — lorsque le champ token est présent

Les deux en-têtes peuvent être définis simultanément. La protection SSRF valide URLs du webhook par rapport aux plages d’adresses IP privées et à localhost.

Validation des entrées

Toutes les requêtes entrantes sont validées avant leur traitement :

ValidationErreur
Message sans partiesInvalidParams (-32602)
messageId vide ou composée uniquement d’espacesInvalidParams (-32602)
messageId dépassant 256 caractèresInvalidParams (-32602)
taskId vide ou contenant uniquement des espacesInvalidParams (-32602)
taskId dépassant 256 caractèresInvalidParams (-32602)
Métadonnées dépassant 64 KoInvalidParams (-32602)

Consommer un agent distant

Utilisez RemoteA2aAgent pour communiquer avec un agent A2A distant :

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()?;

Client A2A

Pour une communication directe au niveau du protocole :

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

Gestion des erreurs

Les erreurs A2A correspondent à la fois aux codes JSON-RPC et aux codes d’état HTTP :

ErreurJSON-RPC CodeHTTP Statut
TaskNotFound-32001404
TaskNotCancelable-32002409
PushNotificationNotSupported-32003400
UnsupportedOperation-32004400
ContentTypeNotSupported-32005415
InvalidAgentResponse-32006502
VersionNotSupported-32009400
InvalidParams-32602400
MethodNotFound-32601404
Interne-32603500

Exécution des exemples

Deux agents d’exemple complets A2A v1.0.0 sont inclus :

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

Le client valide : la découverte de la fiche de l’agent, SendMessage (les deux agents avec de véritables LLM), GetTask, ListTasks, le chemin d’erreur CancelTask, SendStreamingMessage, la CRUD de notification push, GetExtendedAgentCard, la négociation de version et les chemins d’erreur.

Bonnes pratiques

  1. Déclarer précisément les capacités — définir streaming et pushNotifications en fonction de ce que votre agent prend réellement en charge
  2. Utiliser le streaming pour les opérations longues — SendStreamingMessage fournit aux clients une progression en temps réel
  3. Gérer les flux à plusieurs tours — utiliser contextId pour maintenir l’état de la conversation entre les messages
  4. Valider URLs du webhook — la protection contre les SSRF est intégrée, mais utiliser HTTPS en production
  5. Définir des délais d’expiration appropriés — configurer les délais d’expiration des requêtes pour les appels à des agents distants
  6. Utiliser l’idempotence — les clients peuvent réessayer SendMessage en toute sécurité avec le même messageId

Précédent : ← Serveur | Suivant : Évaluation →

Couverture des opérations

Les 11 opérations v1 JSON-RPC sont toutes distribuées et implémentées.

OpérationStatut
SendMessagePilote l’agent et enregistre sa sortie en tant qu’artefact
SendStreamingMessagePilote l’agent et diffuse les fragments de l’artefact au fur et à mesure de leur production
GetTask, ListTasksComplet
CancelTaskComplet
SubscribeToTask (tasks/resubscribe)Instantané uniquement — voir ci-dessous
CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfigComplet
GetExtendedAgentCardComplet

SubscribeToTask est un instantané

L’opération renvoie la tâche et son état actuel, puis ferme le flux. Elle ne fournit pas les mises à jour ultérieures ; un client ne doit donc pas l’attendre pour suivre la progression.

Une reconnexion active nécessite une file d’événements par tâche qui survive à la requête à l’origine de celle-ci. Les implémentations de référence l’obtiennent depuis leur A2A SDKs — adk-python et adk-go délèguent toutes deux entièrement tasks/resubscribe au gestionnaire de files de SDK, et aucune ne l’implémente dans du code ADK. Ce serveur est développé manuellement sur a2a-protocol-types, qui fournit des types filaires plutôt qu’un environnement d’exécution serveur ; la file n’existe donc pas encore.

Utilisez SendStreamingMessage lorsque des mises à jour en direct sont nécessaires.

Contrat des événements diffusés

SendStreamingMessage traduit les événements de l’agent au fur et à mesure de leur arrivée :

Événement de l’agentÉvénement A2A
premier, avant la sortieTask, puis TaskStatusUpdateEvent — Working
contenu, partial = trueTaskArtifactUpdateEvent — append, pas le dernier fragment
contenu, partial = falseTaskArtifactUpdateEvent — dernier fragment
fin du fluxTaskStatusUpdateEvent — Completed
erreurs du fluxTaskStatusUpdateEvent — Failed

Tous les fragments d’une même réponse partagent un identifiant d’artefact, afin qu’un client puisse les réassembler. Le texte assemblé est conservé, de sorte qu’un GetTask ultérieur renvoie ce qui a été diffusé. Cela correspond au contrat que adk-python et adk-go implémentent au-dessus de leur SDKs.