Ai logo

JetBrains AI

Supercharge your tools with AI-powered features inside many JetBrains products

News Releases

Koog × A2A : Créer des agents d’IA connectés en Kotlin

Read this post in other languages:

Si vous avez déjà essayé de créer un système de plusieurs agents d’IA, vous avez probablement rencontré le problème. Cela commence assez simplement : vous avez un agent qui rédige des articles de blog, un autre qui les relit, et peut-être un troisième qui suggère ou génère des images. Individuellement, ils sont efficaces. Mais pour les faire travailler ensemble ? C’est là que les choses peuvent se compliquer.

Chaque agent parle son propre « langage » : l’un utilise une interface API différente, un autre a son propre format de message, et tous peuvent avoir des exigences d’authentification spécifiques. Les faire communiquer implique d’écrire un code d’intégration personnalisé pour chaque connexion. Au lieu de vous concentrer sur l’amélioration de l’intelligence, de la rapidité ou de l’utilité de vos agents, vous perdez du temps à construire des passerelles entre eux.

Ce qu’apporte A2A : la couche de communication entre agents

C’est là que le protocole Agent2Agent (A2A) entre en jeu.

A2A permet à vos agents de communiquer directement via un protocole normalisé, qui fonctionne comme un traducteur universel pour votre écosystème d’IA. Votre agent de rédaction de blog transmet de manière transparente le contenu à votre relecteur, qui déclenche votre générateur d’images, tandis que le relecteur revient avec des corrections et que le générateur d’images demande des clarifications sur le style. Le tout orchestré via une seule couche de communication unifiée.

A2A évite d’avoir à gérer des dizaines de connexions point à point en offrant les avantages suivants :

  • Connectivité plug-and-play : les agents se découvrent et se connectent automatiquement les uns aux autres.
  • Messagerie standardisée : format unifié, protocole clair et aucun problème de traduction.
  • Orchestration intégrée : définissez les workflows une seule fois, puis laissez A2A gérer la coordination.
  • Évolutivité sans complexité : ajoutez ou réutilisez des agents sans réécrire les connexions existantes.

Résultat ? Vous pouvez vous consacrer à l’amélioration des capacités de vos agents au lieu de perdre du temps à déboguer leurs conversations. Mieux encore : vous pouvez implémenter vos agents en utilisant le langage ou le framework de votre choix. Pour les utilisateurs de la JVM, Koog est un choix évident et, à partir de sa version 0.5.0, il s’intègre parfaitement à l’écosystème A2A.

Ce qu’apporte Koog : le moteur d’orchestration interne

Koog est un framework basé sur Kotlin qui permet de créer des agents d’IA ciblant les applications JVM, Android, iOS, WebAssembly et dans le navigateur. Il se distingue sur les points suivants :

  • La gestion des workflows complexes : concevez des stratégies basées sur des graphiques avec prise en charge des boucles, des branches, des révisions et de l’exécution de branches parallèles.
  • Les composants prêts à l’emploi : bénéficiez de ses nœuds intégrés pour appeler des LLM et des outils externes, résumer l’historique des messages et exécuter des stratégies entières.
  • L’orchestration d’outils : transformez n’importe quelle fonction de votre code en un outil pour votre agent d’IA, utilisable de manière séquentielle ou même en parallèle.
  • L’intégration native du protocole MCP : connectez-vous de manière fluide à n’importe quel serveur MCP avec le SDK Kotlin MCP.
  • La prise en charge de la mémoire et du stockage : prise en charge intégrée de la mémoire de l’agent et des workflows RAG (génération augmentée de récupération) avec une gestion efficace du contexte.
  • Tolérance aux pannes : intégration des nouvelles tentatives, points de contrôle, mécanismes de récupération et persistance de l’état pour garantir une exécution fiable.
  • L’observabilité : gestion complète des événements de l’agent, journalisation et prise en charge d’OpenTelemetry avec des intégrations directes avec Langfuse et W&B Weave.

En bref, Koog est idéal pour créer des agents d’IA fiables.

Pourquoi associer Koog et A2A

Koog et A2A couvrent différentes couches de la pile des agents d’IA. Utilisés ensemble, ils se complètent et comblent leurs lacunes respectives.

Koog gère déjà les parties les plus complexes de l’orchestration IA nécessaire à une utilisation concrète en entreprise.

A2A ajoute la pièce manquante : il permet à vos agents Koog de communiquer avec tous les autres agents compatibles A2A de votre écosystème. Au lieu de créer des intégrations personnalisées pour chaque service externe, vos workflows d’IA Koog peuvent automatiquement découvrir et utiliser d’autres agents.

Le résultat est une combinaison parfaite : les workflows avancés de Koog deviennent des tâches A2A que n’importe quel agent peut déclencher, tandis que vos agents Koog exploitent toute la puissance de l’écosystème A2A. Et comme Koog s’exécute en arrière-plan, sur l’appareil lui-même et dans les environnements de navigateur, vous pouvez fournir une IA interconnectée de manière plus large et plus efficace que jamais.

Comment est-ce possible ? Voyons cela !

Protocole A2A

Le protocole A2A définit les composantes essentielles de la communication entre agents :

  • Découverte des agents via des cartes d’agents standardisés (documents JSON décrivant leurs capacités).
  • Formats de messages pour les requêtes et les réponses avec des schémas cohérents.
  • Gestion du cycle de vie des tâches avec des états clairs : soumis → en cours → terminé/en échec.
  • Couches de transport telles que JSON-RPC, gRPC et REST.
  • Schémas de sécurité utilisant la norme OAuth2, des clés API et des jetons JWT.
  • Gestion des erreurs avec des codes d’erreur standardisés.

Cartes d’agents : des cartes de visite numériques

Chaque agent de l’écosystème A2A publie ses capacités à l’aide d’une « carte d’agent » : un fichier JSON standardisé hébergé à une URL, par exemple /.well-known/carte-agent.json, sur le domaine de l’agent. La carte d’agent agit comme une carte de visite numérique qui permet aux autres agents de découvrir les services qu’il propose.

Une carte d’agent contient généralement les éléments suivants :

  • Informations de base : le nom de l’agent, sa description et sa version.
  • Compétences : ce que l’agent peut faire (par exemple, rédiger des documents, relire du texte, analyser des données et générer des images).
  • Points de terminaison : commentcommuniquer avec l’agent. 
  • Autres informations facultatives : fonctionnalités activées, authentification, etc.

Ce mécanisme de découverte élimine le travail d’intégration manuel. Lorsqu’un agent a besoin d’une compétence spécifique, il consulte simplement la carte d’agent correspondante pour comprendre comment interagir avec ce service.

Dans Koog, les cartes d’agents sont définies à l’aide de classes de données Kotlin :

val agentCard = AgentCard(
    name = "Rédacteur de blog",
    description = "Agent IA qui crée des articles et des publications de blog de haute qualité",
    url = "https://api.blog-writer.com/a2a/v1",
    version = "1.0.0",
    capabilities = AgentCapabilities(streaming = true),
    defaultInputModes = listOf("text/plain"),
    defaultOutputModes = listOf("text/markdown"),
    skills = listOf(
        AgentSkill(
            id = "write-post",
            name = "Rédaction d'articles de blog",
            description = "Générer des articles de blog intéressants sur tout sujet",
            tags = listOf("rédaction", "contenu", "blog"),
            examples = listOf("Écrire un article sur les tendances de l'IA")
        )
    )
)

Messagerie universelle : un schéma simple

A2A utilise un format de message unique et standardisé pour toutes les communications entre agents. Cette simplicité offre un avantage conséquent : au lieu de devoir apprendre des dizaines d’API différentes, les agents n’ont qu’un seul schéma de communication à comprendre.

Chaque interaction suit le même flux :

  1. Envoyer un message avec la demande de tâche et les paramètres.
  2. Recevoir soit des résultats immédiats, soit une tâche à suivre.
  3. Obtenir des mises à jour sur des canaux en temps réel pour les opérations plus longues.

Grâce à cette approche universelle, l’ajout de nouvelles fonctionnalités d’agent ne nécessite pas de changer de protocole de communication. Que vous demandiez à un agent de résumer un texte ou de générer un rapport complexe, la structure du message reste cohérente.

Dans Koog, créer et envoyer un message est simple, car cela utilise des objets et des protocoles déjà implémentés :

val message = Message(
    role = Role.User,
    parts = listOf(
        TextPart("Rédiger un article de blog sur l'avenir des agents d'IA")
    ),
    contextId = "blog-project-456"
)

val request = Request(
    data = MessageSendParams(
        message = message,
        configuration = MessageConfiguration(
            blocking = false, // Get first response
            historyLength = 5 // Include context
        )
    )
)

val response = client.sendMessage(request)

Le format du message prend en charge le contenu enrichi via différents types de Part, notamment TextPart pour le contenu en texte brut, FilePart pour les pièces jointes et DataPart pour les données JSON structurées.

Cette structure unifiée permet à vos agents Koog de communiquer de manière transparente avec n’importe quel agent compatible A2A, que ce soit pour le traitement de texte, l’analyse de fichiers ou les transformations de données complexes.

Cycle de vie des tâches : workflows intelligents

A2A gère intelligemment différents types de tâches en fonction de leur complexité et de leur durée :

Messages immédiats : des opérations simples comme la mise en forme de texte ou des calculs rapides renvoient des résultats directement dans la réponse de l’IA. Aucune attente, aucun suivi ne sont nécessaires.

Tâches de longue durée : des opérations complexes telles que l’analyse de documents ou des workflows en plusieurs étapes sont planifiées et renvoient une tâche. L’agent demandeur peut alors surveiller la progression et récupérer les résultats de la tâche une fois qu’ils sont prêts.

Mises à jour en temps réel : pour les opérations chronophages, les événements envoyés par le serveur (SSE) fournissent des mises à jour de progression en direct. Cela permet d’informer les agents sans qu’ils aient besoin de sonder constamment le serveur.

class BlogWriterExecutor : AgentExecutor {
    override suspend fun execute(
        context: RequestContext,
        eventProcessor: SessionEventProcessor
    ) {
        val task = Task(
            contextId = context.contextId,
            status = TaskStatus(
                state = TaskState.Submitted,
                message = Message(
                    role = Role.Agent,
                    parts = listOf(TextPart("Demande de rédaction d'article de blog reçue")),
                    contextId = context.contextId,
    			taskId = context.taskId,
                )
            )
        )

        eventProcessor.sendTaskEvent(task)
	...
    }
}

Sécurité intégrée : normes industrielles uniquement

A2A ne réinvente pas la sécurité. Il s’appuie sur des normes éprouvées et largement adoptées comme OAuth2, les clés d’API et le protocole HTTPS standard.

Grâce à cette approche, les développeurs n’ont pas besoin d’apprendre de nouveaux schémas d’authentification. Si vous connaissez la sécurité des API web modernes, alors vous comprenez déjà la sécurité A2A. Le système hérite de tous les outils, meilleures pratiques et audits de sécurité qui entourent ces normes établies.

"securitySchemes": {
   "google": {
       "openIdConnectUrl": "https://accounts.google.com/.well-known/openid-configuration",
       "type": "openIdConnect"
   }
}
class AuthorizedA2AServer(
    agentExecutor: AgentExecutor,
    agentCard: AgentCard,
    agentCardExtended: AgentCard? = null,
    taskStorage: TaskStorage = InMemoryTaskStorage(),
    messageStorage: MessageStorage = InMemoryMessageStorage(),
    private val authService: AuthService, // Service responsable de l'authentification
) : A2AServer(
    agentExecutor = agentExecutor,
    agentCard = agentCard,
    agentCardExtended = agentCardExtended,
    taskStorage = taskStorage,
    messageStorage = messageStorage,
) {

    private suspend fun authenticateAndAuthorize(
        ctx: ServerCallContext,
        requiredPermission: String
    ): AuthenticatedUser {
        val token = ctx.headers[« Authorisation"]?.firstOrNull()
            ?: throw A2AInvalidParamsException("Jeton d'autorisation manquant")

        val user = authService.authenticate(token)
            ?: throw A2AInvalidParamsException("Jeton d'autorisation non valide")

        if (requiredPermission !in user.permissions) {
            throw A2AUnsupportedOperationException("Autorisations insuffisantes")
        }

        return user
    }

   override suspend fun onSendMessage(
        request: Request,
        ctx: ServerCallContext
    ): Response {
        val user = authenticateAndAuthorize(ctx, requiredPermission = "send_message")

        // Transmet les données de l'utilisateur à l'outil d'exécution de l'agent via l'état du contexte
        val enrichedCtx = ctx.copy(
            state = ctx.state + (AuthStateKeys.USER to user)
        )

        // Délègue à l'implémentation parente avec un contexte enrichi
        return super.onSendMessage(request, enrichedCtx)
    }

   // le reste des méthodes A2A encapsulées
   //...
}

Comment intégrer des agents Koog avec A2A

Le framework Koog intègre le client et le serveur A2A. Vos agents Koog peuvent ainsi communiquer de manière transparente avec d’autres agents compatibles A2A et devenir détectables. Voici un exemple simple montrant comment implémenter cela.

Comment intégrer des agents Koog dans des serveurs A2A

Commencez par définir une stratégie pour l’agent. Koog fournit des convertisseurs pratiques (toKoogMessage, toA2AMessage) pour transformer simplement les formats de message Koog et A2A, ce qui élimine le besoin de sérialisation manuelle. Des nœuds spécialisés tels que nodeA2ASendMessage gèrent le processus d’échange de messages, ce qui facilite l’implémentation des workflows de communication :

fun blogpostWritingStrategy() = strategy("strategie-redaction-article-blog") {
    val blogpostRequest by node { input ->
        val userMessage = input.toKoogMessage().content

        llm.writeSession {
            user {
                +"Rédiger un article de blog à partir de la demande de l'utilisateur"
                +xml {
                    tag("user_request") {
                        +userMessage
                    }
                }
            }

            requestLLM().toA2AMessage()
        }
    }

    val sendMessage by nodeA2ARespondMessage()

    nodeStart then blogpostRequest then sendMessage the nodeFinish
}

Ensuite, définissez l’agent lui-même. Une fois la fonctionnalité A2AServer installée, votre agent devient détectable et accessible aux autres acteurs de l’écosystème, ce qui permet de créer des réseaux sophistiqués dans lesquels les agents spécialisés collaborent de manière transparente.

fun createBlogpostWritingAgent(
    requestContext: RequestContext,
    eventProcessor: SessionEventProcessor
): AIAgent {
     // Récupère les messages existants pour le contexte de la conversation actuelle
     val messageHistory = requestContext.messageStorage.getAll().map { it.toKoogMessage() }

     val agentConfig = AIAgentConfig(
        prompt = prompt("article de blog") {
            system("Vous êtes un agent de rédaction d'articles de blog")

            messages(messageHistory)
        },
        model = GoogleModels.Gemini2_5Flash,
        maxAgentIterations = 5
    )

    return agent = AIAgent(
        promptExecutor = MultiLLMPromptExecutor(
            LLMProvider.Google to GoogleLLMClient(System.getenv("GOOGLE_API_KEY")),
        ),
        strategy = blogpostWritingStrategy(),
        agentConfig = agentConfig
    ) {
        install(A2AAgentServer) {
            this.context = requestContext
            this.eventProcessor = eventProcessor
        }

        handleEvents {
            onAgentFinished { ctx ->
                // Met à jour le contexte de la conversation avec la réponse de l'agent
                val resultMessge = ctx.result as A2AMessage
                requestContext.messageStorage.save(resultMessge)
            }
        }
    }
}

Nous devons encapsuler l’agent dans l’outil d’exécution, puis définir un serveur.

class BlogpostAgentExecutor : AgentExecutor {
    override suspend fun execute(
        context: RequestContext,
        eventProcessor: SessionEventProcessor
    ) {
        createBlogpostWritingAgent(context, eventProcessor)
            .run(context.params.message)
    }
}

val a2aServer = A2AServer(
    agentExecutor = BlogpostAgentExecutor(),
    agentCard = agentCard,
)

L’étape finale consiste à définir un transport de serveur et à exécuter le serveur.

val transport = HttpJSONRPCServerTransport(
    requestHandler = a2aServer
)

transport.start(
    engineFactory = Netty,
    port = 8080,
    path = "/a2a",
    wait = true,
    agentCard = agentCard,
    agentCardPath = A2AConsts.AGENT_CARD_WELL_KNOWN_PATH
)

Votre agent est maintenant prêt à gérer les demandes ! 

Comment appeler d’autres agents compatibles A2A à partir d’un agent Koog

Commencez par configurer un client A2A et connectez-le pour récupérer une carte d’agent.

val agentUrl = « https://exemple.com"

val cardResolver = UrlAgentCardResolver(
    baseUrl = agentUrl,
    path = A2AConsts.AGENT_CARD_WELL_KNOWN_PATH,
)

val transport = HttpJSONRPCClientTransport(
    url = agentUrl,
)

val a2aClient = A2AClient(
    transport = transport,
    agentCardResolver = cardResolver
)

// Initialise le client et récupère la carte
a2aClient.connect()

Vous pouvez ensuite utiliser nodeA2ASendMessage ou nodeA2ASendMessageStreaming dans votre stratégie pour appeler ces clients et recevoir un message ou une réponse à une tâche.

val agentId = "agent_id"
val agent = AIAgent(
    promptExecutor = MultiLLMPromptExecutor(
        LLMProvider.Google to GoogleLLMClient(System.getenv("GOOGLE_API_KEY")),
    ),
    strategy = strategy("a2a") {
        val nodePrepareRequest by node<String, A2AClientRequest> { input ->
            A2AClientRequest(
                agentId = agentId,
                callContext = ClientCallContext.Default,
                params = MessageSendParams(
                    message = A2AMessage(
                        messageId = Uuid.random().toString(),
                        role = Role.User,
                        parts = listOf(
                            TextPart(input)
                        )
                    )
                )
            )
        }
        val nodeA2A by nodeA2AClientSendMessage(agentId)
        
        val nodeProcessResponse by node {
            // Traite l'événement
            when (it) {
                is A2AMessage -> it.parts
                    .filterIsInstance()
                    .joinToString(separator = "n") { it.text }
                
                is Task -> it.artifacts
                    .orEmpty()
                    .flatMap { it.parts }
                    .filterIsInstance()
                    .joinToString(separator = "n") { it.text }
            }
        }

        nodeStart then nodePrepareRequest then nodeA2A then nodeProcessResponse then nodeFinish

    },
    agentConfig = agentConfig
) {
   install(A2AAgentClient) {
        this.a2aClients = mapOf(agentId to client)
    }
}

agent.run("Rédiger un article de blog sur A2A et son intégration avec Koog")

Prochaines étapes

Pour approfondir vos connaissances sur Koog et A2A, consultez ces ressources utiles :

Documentation de Koog

Spécification d’A2A

Exemples d’A2A avec Koog

Auteur de l’article original en anglais :

Andrey Bragin

Andrey Bragin

image description

Discover more