Orkas Orkas
Accueil Blog Architecture
Architecture

L’orchestration multi-agents en pratique : comment Orkas exécute un agent principal et ses sous-agents

Au cœur de l’orchestration multi-Agents d’Orkas : un Agent principal transforme une demande en plan, lance les sous-Agents selon les dépendances, transmet le contexte entre les étapes et rétablit le fonctionnement après un échec.

Le précédent article portait sur la fiabilisation d’un Agent unique : boucle d’exécution, routage des outils, compression du contexte, sessions résistantes aux plantages. Cette couche répond à « comment un Agent accomplit-il une tâche sans échouer ? ». Cet article porte sur la couche supérieure : ce qui se passe lorsqu’un seul Agent ne suffit pas et qu’un travail doit être réparti au sein d’une équipe.

C’est ce que l’on appelle généralement l’orchestration multi-Agents : un Agent principal responsable de la conversation découpe une demande, confie chaque partie à un sous-Agent spécialisé, attend que les bonnes parties soient terminées avant de lancer la suite, transmet les résultats et maintient l’ensemble sur la bonne voie lorsqu’une étape échoue. Orkas exécute tout cela sur la machine de l’utilisateur. Voici comment cette couche d’orchestration est construite — le code a été nettoyé et généralisé, mais la structure est réelle.

En bref Un Agent principal et ses sous-Agents dans un même espace de travail C’est ainsi qu’Orkas répartit le travail aujourd’hui : Commander recrute des spécialistes, les fait travailler en parallèle ou en série, et vous observez le déroulement.
Télécharger Orkas — gratuit

Agent principal et sous-Agents

Imaginez une petite équipe avec une chaîne de commandement claire. Un Agent principal (que nous appelons le Commander) porte la conversation et le contexte global. Il ne fait pas tout lui-même ; son rôle est de décider ce qui doit être fait, dans quel ordre et par qui. Les sous-Agents sont des spécialistes : chacun possède sa propre consigne système, ses outils autorisés et son ensemble de Skills. Un sous-Agent maîtrise une partie du travail et intervient lorsque cette partie se présente.

Deux éléments en font plus qu’un mot à la mode. D’abord, les sous-Agents et les Skills sont des unités à part entière, pas des astuces de consigne : un sous-Agent est un véritable Agent configuré séparément, et lui confier du travail est un véritable passage de relais avec son propre contexte. Ensuite, la coordination ne repose pas sur les bonnes intentions du modèle : elle est pilotée par un élément explicite dont le système, et non le modèle, garantit l’exactitude. Cet élément est le plan.

Un plan est un graphe, pas un script

Lorsque l’Agent principal décide qu’une demande nécessite plusieurs étapes, il écrit un plan. Le plan n’est ni de la prose libre ni une liste linéaire : c’est un petit graphe de dépendances (un DAG). Chaque nœud est une étape, qui contient tout ce dont l’orchestrateur a besoin pour l’attribuer :

interface PlanStep {
  index: number;            // 1-based, stable, never renumbered
  title: string;            // human-readable, shown in the UI
  assignee: string;         // who runs it: "user" | "commander" | a sub-agent
  input?: string;           // the dispatch payload — a template (see below)
  wait_for?: number[];      // upstream step indexes; defaults to [index - 1]
  on_failure?: "abort_plan" | "continue" | "ask_commander";

  // --- runtime state, owned by the orchestrator, NOT written by the model ---
  status: "pending" | "in_progress" | "done" | "failed" | "skipped" | "blocked";
  output_summary?: string;  // short summary of what the step produced
  output_files?: string[];  // files the step produced
  failure_reason?: string;
}

Le champ wait_for est ce qui transforme une liste en graphe. Par défaut, une étape attend la précédente (une simple chaîne), mais elle peut déclarer dépendre de plusieurs étapes antérieures — « résumer » peut attendre à la fois « étudier le marché » et « analyser les concurrents ». C’est un losange, pas une ligne, et l’orchestrateur le traite comme tel.

Qui détient la vérité : l’orchestrateur, pas le modèle

Regardez à nouveau la séparation dans cette structure. Le modèle renseigne l’intention — titres, responsables, entrées, dépendances — lorsqu’il écrit le plan. Mais tout ce qui concerne l’état d’exécutionstatus, output_summary, failure_reason — appartient exclusivement à l’orchestrateur. Le modèle propose le plan une fois ; il ne peut jamais marquer lui-même ses étapes comme « terminées ».

Cette séparation est volontaire, et c’est la décision la plus importante de toute la conception. Un modèle de langage peut parfaitement annoncer avec assurance « étape 3 terminée » alors que l’étape 3 a échoué, ou perdre le fil des étapes restantes au milieu d’une longue conversation. Si l’état résidait dans la tête du modèle, le plan s’écarterait de la réalité. En faisant de l’état un élément structuré que seul l’exécuteur écrit — et uniquement lorsqu’une étape se termine réellement — le plan reste un reflet exact de ce qui s’est passé. Le modèle décide de la forme du travail ; le moteur d’exécution décide de la vérité de sa progression.

Attribuer les étapes prêtes

Une fois le plan créé, un petit moteur — l’exécuteur — le fait avancer. Son opération centrale est « trouver les étapes prêtes et les attribuer ». Une étape est prête lorsqu’elle est encore pending et que toutes les étapes dont elle dépend ont atteint un état final suffisamment réussi :

function findReadySteps(plan): PlanStep[] {
  return plan.steps.filter((s) => {
    if (s.status !== "pending") return false;
    const deps = s.wait_for ?? (s.index > 1 ? [s.index - 1] : []);
    return deps.every((d) => isTerminal(plan.step(d).status)); // done or skipped
  });
}

Cela s’exécute en réponse à des événements, pas à un minuteur. Chaque fois qu’une étape se termine — retour d’un sous-Agent, fin d’un tour de synthèse du Commander — l’exécuteur réconcilie l’état : il enregistre le résultat de l’étape terminée, recherche à nouveau les étapes devenues prêtes et les attribue. L’orchestration est cette boucle de réconciliation, répétée jusqu’à ce qu’il ne reste plus d’étapes.

L’attribution passe par le même chat, pas par un canal secondaire

Voici un choix qui garde le système fidèle à la réalité : attribuer une étape n’utilise pas de canal RPC caché. Cela publie un message dans la conversation de groupe que l’utilisateur regarde, envoyé par l’Agent principal, avec une @mention du sous-Agent. Pour le sous-Agent, recevoir une étape est identique au fait d’être interpellé dans le chat : il exécute simplement son tour habituel. Il n’y a pas de second chemin d’exécution à maintenir synchronisé avec le premier.

Il existe trois types de responsables, avec un mode d’attribution légèrement différent pour chacun :

  • Un sous-Agent — le cas courant. L’exécuteur résout le nom de l’Agent en identifiant et publie @<agent> <rendered input> au nom du Commander. Le sous-Agent reçoit le message et exécute un tour d’Agent complet.
  • L’utilisateur — lorsqu’une étape nécessite réellement une intervention humaine, elle devient un formulaire et suspend le plan (nous y revenons plus bas). Rien en aval n’avance tant que l’utilisateur n’a pas répondu.
  • Le Commander lui-même — pour les étapes de synthèse ou de décision (« lisez tout ce qui précède et rédigez le résumé »). Il s’agit d’un réveil privé qui ne publie pas de message redondant visible par l’utilisateur ; l’Agent principal prend simplement un tour avec le contexte réuni.

Transmettre le contexte d’une étape à l’autre

Une équipe n’est utile que si le travail circule entre ses membres. Le mécanisme repose sur le modèle input. Lorsque l’Agent principal écrit une étape, l’entrée n’est pas une chaîne figée : elle peut référencer des résultats antérieurs, et l’exécuteur résout ces références au moment de l’attribution :

// step 3.input, as written by the lead agent
"Using the findings below, draft the launch note.\n\n{{step_1.output_summary}}"

// what the sub-agent actually receives at dispatch time
"Using the findings below, draft the launch note.\n\n- Market is growing ~20% YoY; two incumbents…"

Notez ce qui est transmis : output_summary, un court résumé de chaque étape terminée, pas sa transcription complète. C’est un choix de budget, dans le même esprit que la compression du contexte de l’environnement d’exécution. Si chaque étape en aval héritait de tout l’historique, token par token, de ce qui la précède, le contexte gonflerait et le coût exploserait après quelques relais. Les résumés rendent chaque passage peu coûteux et concentrent chaque sous-Agent sur ce dont il a réellement besoin en amont, sans l’obliger à parcourir tout le cheminement. Le message initial de l’utilisateur et les pièces jointes sont aussi transmis : une étape trois relais plus loin connaît donc toujours la demande d’origine.

En série par défaut — et pourquoi

Vous pourriez vous attendre à ce que, lorsque plusieurs étapes deviennent prêtes en même temps — deux branches d’un losange, par exemple — l’orchestrateur les lance toutes en parallèle. Il le pourrait ; aujourd’hui, il attribue plutôt une seule étape prête à la fois, celle dont l’indice est le plus petit, et laisse les autres attendre la prochaine réconciliation. Faire travailler toute une équipe strictement une étape à la fois est un choix volontairement prudent, dont il faut expliquer honnêtement la raison.

La raison est la correction en présence de concurrence. Imaginez deux sous-Agents terminant presque simultanément. Les deux fins déclenchent une réconciliation ; les deux réconciliations lisent le plan ; les deux voient la même étape en aval encore à l’état pending — et toutes deux l’attribuent. La même étape s’exécute alors deux fois. Pour rendre cela impossible, chaque cycle lecture-modification-attribution d’une conversation est sérialisé derrière un verrou propre à cette conversation :

// all state-changing paths for one conversation run under one mutex
planLock(uid, cid).runExclusive(async () => {
  const plan = await readPlan(uid, cid);
  applyOutcomeOfFinishedStep(plan);   // mark done / failed / skipped
  await dispatchReady(plan);          // dispatch the next ready step
});

Le verrou garantit que « enregistrer ce qui est terminé » et « décider de la suite » forment une unité indivisible : une étape en aval ne peut donc jamais être attribuée deux fois. Avec ce verrou, attribuer les étapes une par une est la solution la plus simple dont la correction est évidente. Une véritable distribution parallèle est une extension réalisable sur cette base ; mais la base reste un exécuteur sérialisé sans condition de concurrence, et cet ordre de priorités — correct d’abord, rapide ensuite — est essentiel.

Lorsqu’une étape tourne mal

Sur une machine réelle utilisant des API de modèles externes, les échecs sont courants ; l’orchestrateur les répartit en quelques cas au lieu de traiter toutes les erreurs de la même façon.

Il détermine d’abord si l’échec était simplement transitoire — connexion interrompue, limite de débit, incident passager. Si oui, et si l’étape n’a pas épuisé un petit budget de nouvelles tentatives, elle revient discrètement à l’état pending pour être réattribuée à la prochaine réconciliation. (Cela se situe au-dessus des nouvelles tentatives internes à l’exécution de l’environnement ; le plan ne réattribue l’étape qu’après épuisement des tentatives de l’Agent, avec un plafond strict pour qu’une étape réellement défaillante ne boucle pas indéfiniment.)

Si l’échec est réel, la politique déclarée dans on_failure détermine ce que l’équipe fait ensuite :

  • abort_plan — cette étape était assez importante pour que rien en aval n’ait de sens sans elle. Marquez-la comme échouée, puis propagez : chaque étape encore en attente est marquée skipped. Le plan s’arrête proprement plutôt que de construire sur une base manquante.
  • continue — cette étape était facultative. Marquez-la skipped et laissez les étapes en aval continuer comme si elle n’avait simplement rien produit.
  • ask_commander (par défaut) — ni abandonner aveuglément, ni continuer aveuglément. Marquez l’étape comme échouée et réveillez l’Agent principal pour qu’il examine ce qui s’est passé et décide : réessayer autrement, contourner l’obstacle ou s’arrêter pour interroger l’utilisateur.

Un sixième état mérite d’être mentionné : blocked. Un sous-Agent en cours d’étape peut se rendre compte qu’il a besoin d’un élément que seul l’utilisateur peut fournir, et afficher un formulaire ou une question. L’étape n’échoue pas : elle passe à blocked, et tout le plan est suspendu. Dès que l’utilisateur répond, l’exécuteur réconcilie l’état et l’équipe reprend exactement là où elle s’était arrêtée. Un plan bloqué est un plan en pause, pas un plan cassé.

Chaque étape est une exécution complète d’Agent

Il est utile de revenir au précédent article. Lorsque l’orchestrateur attribue une étape à un sous-Agent, celui-ci n’exécute pas une routine allégée : il exécute la boucle complète de l’environnement d’exécution : sa propre boucle en flux, ses propres appels d’outils, sa propre fenêtre de contexte avec compression, sa propre session résistante aux plantages. L’orchestration se place proprement au-dessus du moteur d’exécution mono-Agent ; elle n’intervient jamais à l’intérieur. L’Agent principal décide de la forme du travail et de l’ordre des passages de relais ; chaque sous-Agent, une fois sa partie confiée, est un Agent complet à part entière.

C’est cette superposition qui relie les deux articles. L’environnement d’exécution rend un Agent fiable pour une tâche. L’orchestrateur réunit plusieurs Agents fiables en une équipe capable de traiter une tâche trop grande ou trop variée pour un seul d’entre eux.

Quelques décisions déterminantes

L’orchestrateur détient l’état d’exécution, le modèle détient l’intention. Le modèle propose le plan ; seul le moteur d’exécution marque les étapes comme terminées, échouées ou ignorées, et uniquement en réponse à un événement réel. Cette limite garde le plan fidèle à la réalité plutôt qu’aux suppositions optimistes du modèle.

Attribuez par la même conversation, pas par un canal secondaire. Une étape attribuée n’est qu’un message de l’Agent principal à un sous-Agent. Un seul chemin d’exécution, rien de caché qui puisse se désynchroniser, et l’utilisateur peut voir l’équipe travailler dans le fil qu’il lit déjà.

Des résumés, pas des transcriptions, entre les étapes. Chaque relais transmet un court résumé du résultat en amont. Cela garde des budgets de contexte raisonnables dans les longues chaînes et concentre chaque sous-Agent sur ses besoins, pas sur le cheminement du précédent.

La correction avant le parallélisme. Un verrou par conversation sérialise chaque cycle lecture-modification-attribution, et les étapes sont attribuées une à une. La version strictement séquentielle est manifestement exempte de doubles attributions concurrentes ; la distribution parallèle est une optimisation à ajouter sur une base déjà correcte.

Pour conclure

La couche d’orchestration d’Orkas ne repose sur aucun algorithme exotique. Sa valeur tient à quelques limites fermement maintenues : un plan sous forme de graphe de dépendances plutôt que de script ; un état d’exécution détenu par le moteur plutôt que par le modèle ; une attribution passant par le même chat que regarde l’utilisateur ; un contexte transmis entre étapes sous forme de résumés ; et un exécuteur qui privilégie la correction à la concurrence. Chacun de ces choix est simple. Ensemble, ils transforment un Agent fiable en une équipe qui répartit les tâches, transmet le travail et se rétablit lorsqu’une partie échoue.

Pour découvrir la couche inférieure, lisez comment un Agent est conçu pour fonctionner de manière fiable. Pour découvrir la couche qui améliore chaque Agent avec l’usage, lisez comment les Agents Orkas apprennent de leur propre travail. Et si vous préférez diriger cette couche plutôt que la construire, Orkas la fournit sous la forme d’une orchestration d’Agents IA open source qui s’exécute sur votre propre machine.