Les agents IA se multiplient dans les entreprises : l'un trie les tickets de support, un autre audite les factures, un troisième rédige les notes de version. Le problème, c'est qu'ils ne peuvent pas communiquer entre eux. Chacun vit derrière une API REST maison avec ses propres formats de payload, ses particularités d'authentification et sa propre définition de ce qu'est une « tâche ». Connecter l'agent A à l'agent B revient à écrire encore une intégration fragile de plus.
Le protocole Agent2Agent (A2A) résout exactement ce problème. Annoncé à l'origine par Google en 2025 et désormais gouverné par la Linux Foundation, A2A est un standard ouvert qui permet à des agents opaques de se découvrir mutuellement, d'échanger des messages, de déléguer des tâches et de diffuser des mises à jour de progression — sans exposer leur état interne, leurs prompts ni leurs outils. Voyez-le comme le troisième pilier manquant de la pile d'interopérabilité des agents :
- MCP connecte un agent à ses outils et données (voir notre tutoriel serveur MCP)
- ACP connecte un éditeur ou une interface à un agent (voir notre tutoriel agent ACP)
- A2A connecte les agents à d'autres agents, à travers les équipes, les fournisseurs et les clouds
Dans ce tutoriel, vous allez construire une installation A2A complète en TypeScript avec le SDK officiel @a2a-js/sdk : un serveur d'agent qui publie une Agent Card, exécute des tâches et diffuse des mises à jour de statut et des artefacts, plus un client qui découvre l'agent et consomme son flux d'événements. Dans la dernière étape, vous brancherez Claude dans l'exécuteur pour que votre agent produise de vraies réponses plutôt que des chaînes figées.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Les bases de TypeScript (interfaces, async/await, génériques)
- Une familiarité avec Express ou tout autre framework de serveur HTTP
- Une clé API Anthropic pour la dernière étape (optionnelle mais recommandée)
- Un éditeur de code (VS Code recommandé)
Ce que vous allez construire
Vous allez construire un agent de synthèse de recherche exposé via A2A :
- Un serveur d'agent sur le port 4000 qui s'annonce via une Agent Card à une URL bien connue
- Un AgentExecutor qui accepte un sujet, traverse le cycle de vie d'une tâche (soumise, en cours, terminée) et émet un document de synthèse en tant qu'artefact
- Un client qui découvre l'agent à partir de sa carte, envoie des messages et consomme les mises à jour en streaming en temps réel
- Une variante de l'exécuteur propulsée par Claude qui génère de véritables synthèses
À la fin, tout orchestrateur compatible A2A — LangGraph, CrewAI, Google ADK ou un autre agent que vous avez écrit vous-même — pourra découvrir votre agent et lui déléguer du travail sans aucun code d'intégration spécifique.
Étape 1 : configuration du projet
Créez un nouveau projet et installez le SDK. Express est une dépendance pair requise pour l'intégration serveur HTTP :
mkdir a2a-research-agent && cd a2a-research-agent
npm init -y
npm install @a2a-js/sdk express uuid
npm install -D typescript tsx @types/express @types/node @types/uuid
npx tsc --initDéfinissez "type": "module" dans votre package.json et ajoutez deux scripts :
{
"type": "module",
"scripts": {
"server": "tsx src/server.ts",
"client": "tsx src/client.ts"
}
}Le paquet @a2a-js/sdk expose trois points d'entrée : l'export racine pour les types partagés (Message, Task, AgentCard), @a2a-js/sdk/server pour les classes d'exécuteur et de gestion des requêtes, et @a2a-js/sdk/client pour la fabrique de clients. Importer depuis le mauvais point d'entrée est l'erreur de démarrage la plus fréquente.
Étape 2 : définir l'Agent Card
L'Agent Card est l'identité publique de votre agent : un document JSON servi à /.well-known/agent-card.json qui indique aux autres agents qui vous êtes, quelles compétences vous offrez, quels transports vous parlez et quels modes d'entrée et de sortie vous acceptez. La découverte dans A2A commence ici — un client récupère d'abord la carte et négocie tout le reste à partir d'elle.
Créez src/card.ts :
// src/card.ts
import { AgentCard } from '@a2a-js/sdk';
export const researchAgentCard: AgentCard = {
name: 'Research Summary Agent',
description:
'Accepts a topic and returns a structured research summary as a markdown artifact.',
protocolVersion: '0.3.0',
version: '1.0.0',
// L'URL publique du point de terminaison de transport par défaut
url: 'http://localhost:4000/a2a/jsonrpc',
skills: [
{
id: 'summarize-topic',
name: 'Summarize Topic',
description:
'Produces a concise, sourced summary for a given research topic.',
tags: ['research', 'summarization'],
examples: ['Summarize the current state of solid-state batteries'],
},
],
capabilities: {
streaming: true,
pushNotifications: false,
},
defaultInputModes: ['text'],
defaultOutputModes: ['text'],
additionalInterfaces: [
{ url: 'http://localhost:4000/a2a/jsonrpc', transport: 'JSONRPC' },
{ url: 'http://localhost:4000/a2a/rest', transport: 'HTTP+JSON' },
],
};Quelques champs méritent votre attention :
- skills est le mécanisme de routage du travail pour les orchestrateurs. Un orchestrateur détenant dix agent cards choisit l'agent dont les tags et descriptions de compétences correspondent le mieux à la sous-tâche à déléguer. Rédigez-les comme vous rédigeriez de bonnes descriptions d'outils pour un LLM — car c'est souvent exactement ce qui les lit.
- capabilities.streaming indique aux clients qu'ils peuvent appeler le point de terminaison de streaming et recevoir des Server-Sent Events au lieu d'une réponse bloquante unique.
- additionalInterfaces liste tous les transports exposés. Le SDK JS prend en charge JSON-RPC (par défaut), HTTP+JSON/REST et gRPC.
Étape 3 : implémenter l'AgentExecutor
L'interface AgentExecutor est le cœur du SDK serveur. Vous implémentez une méthode, execute, qui reçoit un RequestContext (le message entrant, l'identifiant de tâche et l'identifiant de contexte) et un ExecutionEventBus sur lequel vous publiez des événements. Le SDK gère autour de vous le parsing JSON-RPC, la persistance des tâches et la tuyauterie SSE.
Commencez par le cycle de vie complet d'une tâche. Créez src/executor.ts :
// src/executor.ts
import { Task } from '@a2a-js/sdk';
import {
AgentExecutor,
RequestContext,
ExecutionEventBus,
} from '@a2a-js/sdk/server';
export class ResearchExecutor implements AgentExecutor {
async execute(
requestContext: RequestContext,
eventBus: ExecutionEventBus
): Promise<void> {
const { taskId, contextId, userMessage, task } = requestContext;
// Extraire le sujet de la première partie texte du message utilisateur.
const firstPart = userMessage.parts[0];
const topic = firstPart.kind === 'text' ? firstPart.text : 'unknown topic';
// 1. Publier la tâche initiale s'il s'agit d'un nouveau tour de conversation.
if (!task) {
const initialTask: Task = {
kind: 'task',
id: taskId,
contextId,
status: {
state: 'submitted',
timestamp: new Date().toISOString(),
},
history: [userMessage],
};
eventBus.publish(initialTask);
}
// 2. Signaler que le travail a commencé.
eventBus.publish({
kind: 'status-update',
taskId,
contextId,
status: { state: 'working', timestamp: new Date().toISOString() },
final: false,
});
// 3. Effectuer le vrai travail (placeholder pour l'instant — Claude arrive à l'étape 7).
const summary = await this.summarize(topic);
// 4. Émettre le résultat en tant qu'artefact.
eventBus.publish({
kind: 'artifact-update',
taskId,
contextId,
artifact: {
artifactId: 'summary.md',
name: 'Research Summary',
parts: [{ kind: 'text', text: summary }],
},
});
// 5. Marquer la tâche comme terminée et fermer le flux d'événements.
eventBus.publish({
kind: 'status-update',
taskId,
contextId,
status: { state: 'completed', timestamp: new Date().toISOString() },
final: true,
});
eventBus.finished();
}
private async summarize(topic: string): Promise<string> {
await new Promise((r) => setTimeout(r, 800)); // simuler du travail
return `# Summary: ${topic}\n\nThis is a placeholder summary.`;
}
cancelTask = async (): Promise<void> => {
// Appelé quand un client annule une tâche en cours.
};
}La séquence d'événements ici — tâche, statut en cours, artefact, statut final terminé, finished() — constitue le cycle de vie canonique d'une tâche A2A. Deux règles comptent :
- Toujours définir
final: truesur la mise à jour de statut terminale. C'est ce qui indique au SDK (et au flux du client) que la tâche a atteint un état terminal. - Toujours appeler
eventBus.finished()quand votre exécuteur a fini de publier. L'oublier laisse les clients en streaming suspendus jusqu'à expiration de leur délai.
Pour des agents rapides de type requête/réponse, vous pouvez ignorer entièrement la mécanique des tâches : publiez un unique événement Message avec le rôle « agent » et appelez finished(). Le client reçoit alors un message direct au lieu d'une tâche. Utilisez les tâches dès que le travail prend plus d'un instant ou produit des artefacts dignes d'être persistés.
Étape 4 : assembler le serveur Express
Assemblez maintenant les pièces : la carte, un magasin de tâches, votre exécuteur et les gestionnaires de transport. Créez src/server.ts :
// src/server.ts
import express from 'express';
import { AGENT_CARD_PATH } from '@a2a-js/sdk';
import {
DefaultRequestHandler,
InMemoryTaskStore,
} from '@a2a-js/sdk/server';
import {
agentCardHandler,
jsonRpcHandler,
restHandler,
UserBuilder,
} from '@a2a-js/sdk/server/express';
import { researchAgentCard } from './card.js';
import { ResearchExecutor } from './executor.js';
const requestHandler = new DefaultRequestHandler(
researchAgentCard,
new InMemoryTaskStore(),
new ResearchExecutor()
);
const app = express();
// Sert la carte à /.well-known/agent-card.json
app.use(
`/${AGENT_CARD_PATH}`,
agentCardHandler({ agentCardProvider: requestHandler })
);
// Transport JSON-RPC (le défaut A2A)
app.use(
'/a2a/jsonrpc',
jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication })
);
// Transport HTTP+JSON/REST
app.use(
'/a2a/rest',
restHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication })
);
app.listen(4000, () => {
console.log('A2A research agent listening on http://localhost:4000');
});Lancez-le et vérifiez que la carte est découvrable :
npm run server
# dans un autre terminal :
curl http://localhost:4000/.well-known/agent-card.jsonVous devriez voir votre Agent Card complète en JSON. Cette URL unique est tout ce dont un autre agent a besoin pour commencer à travailler avec le vôtre.
InMemoryTaskStore conserve l'état des tâches en mémoire du processus, ce qui est parfait en développement. En production, vous implémenteriez l'interface TaskStore au-dessus de Redis ou Postgres afin que les tâches survivent aux redémarrages et passent à l'échelle sur plusieurs réplicas.
UserBuilder.noAuthentication convient en localhost, mais jamais en production. La spécification A2A délègue l'authentification aux mécanismes HTTP standards — le champ securitySchemes de l'Agent Card annonce ce que vous acceptez (jetons bearer, OAuth 2, clés API), et le hook UserBuilder du SDK est l'endroit où vous validez les identifiants entrants.
Étape 5 : construire le client
Le côté client est étonnamment compact. ClientFactory.createFromUrl récupère l'Agent Card, choisit un transport pris en charge des deux côtés et retourne un client prêt à l'emploi. Créez src/client.ts :
// src/client.ts
import { ClientFactory } from '@a2a-js/sdk/client';
import { MessageSendParams, Task } from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
const factory = new ClientFactory();
const client = await factory.createFromUrl('http://localhost:4000');
const params: MessageSendParams = {
message: {
kind: 'message',
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'The state of small modular reactors' }],
},
};
const result = await client.sendMessage(params);
if (result.kind === 'task') {
const task = result as Task;
console.log(`Task ${task.id} finished: ${task.status.state}`);
for (const artifact of task.artifacts ?? []) {
const part = artifact.parts[0];
if (part.kind === 'text') {
console.log(`--- ${artifact.name} ---\n${part.text}`);
}
}
} else {
console.log('Direct message reply:', result);
}Lancez npm run client avec le serveur actif et vous verrez la tâche se terminer et afficher l'artefact de synthèse placeholder. Remarquez ce que vous n'avez pas écrit : pas de chemins de points de terminaison, pas de schémas de payload, pas de logique de polling. La carte a tout dit au client.
Étape 6 : diffuser la progression en temps réel
Bloquer sur sendMessage convient pour les tâches rapides, mais un travail de type recherche peut durer. A2A diffuse les événements de tâche via Server-Sent Events, et le client les expose sous forme de générateur asynchrone. Ajoutez src/client-stream.ts :
// src/client-stream.ts
import { ClientFactory } from '@a2a-js/sdk/client';
import { MessageSendParams } from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
const factory = new ClientFactory();
const client = await factory.createFromUrl('http://localhost:4000');
const params: MessageSendParams = {
message: {
kind: 'message',
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'Progress in desalination tech' }],
},
};
for await (const event of client.sendMessageStream(params)) {
if (event.kind === 'task') {
console.log(`[task] created ${event.id} (${event.status.state})`);
} else if (event.kind === 'status-update') {
console.log(`[status] ${event.status.state}`);
} else if (event.kind === 'artifact-update') {
console.log(`[artifact] received ${event.artifact.artifactId}`);
}
}
console.log('Stream closed.');Les événements arrivent exactement dans l'ordre où votre exécuteur les a publiés : tâche créée, en cours, artefact, terminée. Dans une véritable interface d'orchestrateur, vous les mapperiez sur un indicateur de progression, et les agents de longue durée peuvent publier autant de mises à jour intermédiaires et d'artefacts partiels qu'ils le souhaitent.
Étape 7 : donner une vraie intelligence à l'agent avec Claude
Jusqu'ici, l'exécuteur retourne une chaîne figée. Faisons-lui mériter son Agent Card. Installez le SDK Anthropic :
npm install @anthropic-ai/sdk
export ANTHROPIC_API_KEY=sk-ant-...Puis remplacez la méthode summarize dans src/executor.ts :
// src/executor.ts (mis à jour)
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
// ...dans ResearchExecutor :
private async summarize(topic: string): Promise<string> {
const response = await anthropic.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
system:
'You are a research assistant. Produce a concise markdown summary ' +
'with sections: Overview, Key Developments, Open Questions.',
messages: [{ role: 'user', content: `Topic: ${topic}` }],
});
const block = response.content[0];
return block.type === 'text' ? block.text : 'No summary generated.';
}Pour des générations plus longues, vous pouvez aller plus loin et diffuser la sortie de Claude à travers le bus d'événements A2A sous forme de mises à jour d'artefact incrémentales :
private async streamSummary(
topic: string,
publish: (chunk: string) => void
): Promise<void> {
const stream = anthropic.messages.stream({
model: 'claude-sonnet-5',
max_tokens: 1024,
messages: [{ role: 'user', content: `Summarize: ${topic}` }],
});
stream.on('text', (delta) => publish(delta));
await stream.finalMessage();
}Chaque fragment devient un événement artifact-update, et un client en streaming affiche la synthèse token par token — la même expérience qu'une application de chat, mais circulant d'agent à agent via un protocole standard.
Cette composition est l'enseignement clé. L'exécuteur n'est qu'une fonction asynchrone : à l'intérieur, vous pouvez appeler Claude, interroger une base vectorielle, invoquer des outils MCP ou déléguer à un autre agent A2A. Le protocole ne se soucie pas de la façon dont le travail est effectué, seulement de la façon dont il est décrit, suivi et livré.
Tester votre implémentation
Vérifiez la boucle complète de bout en bout :
- Découverte de la carte :
curl http://localhost:4000/.well-known/agent-card.jsonretourne votre carte avecprotocolVersionetskillsrenseignés. - Flux bloquant :
npm run clientaffiche une tâche terminée avec un artefactsummary.md. - Flux en streaming :
tsx src/client-stream.tsaffiche les quatre événements du cycle de vie dans l'ordre puis « Stream closed ». - État de la tâche : envoyez un message, notez l'identifiant de tâche, puis récupérez-la via un appel JSON-RPC
tasks/getpour confirmer que le magasin a conservé l'historique et les artefacts.
Pour une validation au niveau du protocole, le projet A2A publie un TCK (Technology Compatibility Kit) qui teste votre serveur contre la spécification — à exécuter avant de déclarer votre agent prêt pour la production.
Dépannage
Le client lève une erreur sur createFromUrl. La récupération de la carte a échoué. Confirmez que le serveur tourne et que vous n'avez pas monté agentCardHandler sur un chemin personnalisé alors que le client attend le chemin bien connu par défaut.
Les clients en streaming restent suspendus après le dernier événement. Votre exécuteur n'a jamais appelé eventBus.finished(), ou la mise à jour de statut terminale n'a pas final: true.
Erreurs « Cannot find module » sur les imports serveur. Vérifiez le point d'entrée : les classes d'exécuteur et de gestionnaire viennent de @a2a-js/sdk/server, les gestionnaires Express de @a2a-js/sdk/server/express, et les types partagés de la racine du paquet.
Les artefacts arrivent vides. Chaque partie a besoin d'un champ kind explicite (text, file ou data). Une partie sans son kind est silencieusement ignorée par les clients stricts.
Erreurs d'import ESM avec tsx. Assurez-vous que "type": "module" est défini et que les imports locaux utilisent l'extension .js (./card.js), que TypeScript résout vers la source .ts avec la résolution de modules moderne.
Prochaines étapes
- Implémentez un
TaskStorepersistant sur Redis et une logiquecancelTaskqui interrompt réellement les appels Claude en cours - Annoncez
securitySchemessur votre carte et validez les jetons bearer dans unUserBuilderpersonnalisé - Construisez un second agent et faites déléguer des sous-tâches par le premier — les pipelines multi-agents ne sont que des clients A2A à l'intérieur d'exécuteurs
- Combinez les protocoles : exposez les sources de données de votre agent via un serveur MCP et gardez A2A pour la délégation d'agent à agent
- Explorez le Claude Agent SDK si vous voulez une boucle d'agent complète derrière votre exécuteur
Conclusion
Vous avez construit un agent A2A complet en TypeScript : une Agent Card pour la découverte, un exécuteur qui parcourt le cycle de vie complet d'une tâche avec mises à jour de statut en streaming et artefacts, un client qui consomme les flux bloquants et en streaming, et un cerveau propulsé par Claude derrière le tout. Le motif à retenir est la séparation des responsabilités — A2A standardise la façon dont les agents communiquent et vous laisse entièrement la façon dont les agents réfléchissent. À mesure que les frameworks d'orchestration et les plateformes d'entreprise convergent vers ce protocole, un agent conforme à A2A est un agent que n'importe lequel d'entre eux peut découvrir, authentifier et mettre au travail.