Rétro-ingénierie de Claude Code Agent Teams : architecture et protocole

Traduction automatique Article original.

Banner for Rétro-ingénierie de Claude Code Agent Teams : architecture et protocole

contenu

Introduction

Claude Code (v2.1.47) intègre une fonctionnalité expérimentale appelée Agent Teams : plusieurs sessions Claude Code se coordonnent sur un travail commun selon une topologie avec un responsable et des coéquipiers. Je développe Hive, un orchestrateur multi-agent pour la programmation aux objectifs similaires, mais à l’architecture très différente. Je voulais donc comprendre comment fonctionne l’approche d’Anthropic sous le capot.

Cet article présente ce que j’ai découvert grâce à :

  1. La lecture de la documentation officielle
  2. L’examen des artefacts réels laissés sur le disque par de précédentes sessions en équipe
  3. Une analyse du binaire de Claude Code (v2.1.47) que j’ai confiée à Claude pour obtenir des détails d’implémentation (ha !)

Sommaire

1. Vue d’ensemble de l’architecture

Une équipe d’agents se compose de quatre éléments :

Composant Rôle
Responsable d’équipe La session principale de Claude Code, qui crée l’équipe et lance les coéquipiers
Coéquipiers Des instances distinctes de Claude Code, chacune avec sa propre fenêtre de contexte
Liste de tâches Des éléments de travail partagés, stockés dans des fichiers JSON individuels sur le disque
Boîte aux lettres Des fichiers de boîte de réception propres à chaque agent pour acheminer les messages

Toute la couche de coordination est fondée sur des fichiers. Le système de fichiers à l’emplacement ~/.claude/ est l’unique support de coordination :

~/.claude/
├── teams/{team-name}/
│   ├── config.json                  # team membership registry
│   └── inboxes/{agent-name}.json    # per-agent mailbox
└── tasks/{team-name}/
    ├── .lock                        # flock() for concurrent task claiming
    ├── .highwatermark               # auto-increment counter
    ├── 1.json                       # individual task files
    ├── 2.json
    └── ...

Cette conception est fondamentalement décentralisée. Le responsable est simplement une autre session Claude dotée d’outils supplémentaires (TeamCreate, TeamDelete, SendMessage). Il n’y a aucun processus en arrière-plan. La coordination découle de l’accès partagé aux fichiers.

Dans une session active, si vous demandez à Claude de lancer une équipe pour accomplir une tâche, puis exécutez ce qui suit dans une autre fenêtre, vous pouvez observer les modifications du système de fichiers en temps réel.

watch -n 0.5 'tree ~/.claude/teams/ 2>/dev/null; echo "---"; tree ~/.claude/tasks/ 2>/dev/null'

Par exemple, avec le prompt suivant :

can you spawn an agent team to examine this codebase?
  - have one look for bugs
  - have one look for complexity
  - have one look for good things to call out and play devil's advocate against the other two agents

J’ai observé ceci :

teams
└── code-review
    ├── config.json
    └── inboxes
        ├── bug-hunter.json
        ├── complexity-analyst.json
        ├── devils-advocate.json
        └── team-lead.json

Configuration de l’équipe

La configuration de l’équipe à l’emplacement ~/.claude/teams/{team-name}/config.json contient un tableau members que les coéquipiers lisent pour découvrir les autres membres :

{
  "members": [
    { "name": "team-lead", "agentId": "abc-123", "agentType": "leader" },
    {
      "name": "researcher",
      "agentId": "def-456",
      "agentType": "general-purpose"
    }
  ]
}

Les noms constituent le principal mécanisme d’adressage (les UUID existent, mais ne sont pas utilisés pour le routage). Tous les échanges de messages et toutes les attributions de tâches utilisent le champ name.

2. La liste de tâches partagée

Format des fichiers

Chaque tâche est stockée dans un fichier JSON individuel dans ~/.claude/tasks/{team-name}/. Voici un exemple réel tiré d’une session précédente :

{
  "id": "1",
  "subject": "Hunt for bugs across the codebase",
  "description": "...",
  "activeForm": "Hunting for bugs",
  "owner": "bug-hunter",
  "status": "completed",
  "blocks": [],
  "blockedBy": []
}

Schéma des tâches :

Champ Type Description
id string Identifiant numérique, incrémenté automatiquement via .highwatermark
subject string Titre à l’impératif (p. ex., "Exécutez les tests")
description string Exigences détaillées et critères d’acceptation
activeForm string Formulation au présent progressif pour l’indicateur d’activité ("Tests en cours d’exécution")
status string pending → in_progress → completed (ou deleted)
blocks string[] Identifiants des tâches que cette tâche bloque
blockedBy string[] Identifiants des tâches qui doivent être terminées avant que cette tâche puisse commencer

Gestion de la concurrence

Deux fichiers spéciaux assurent la coordination :

  • .lock : un fichier de 0 octet utilisé pour assurer l’exclusion mutuelle au niveau du système de fichiers (flock()). Présent dans les 42 répertoires de tâches observés sur ma machine.
  • .highwatermark : contient un seul entier (p. ex., "3", "13"). Il s’agit du prochain identifiant de tâche disponible pour l’incrémentation automatique.

Prise en charge des tâches

La prise en charge des tâches utilise le verrouillage de fichiers pour éviter les conditions de concurrence. Les coéquipiers privilégient les tâches dont l’identifiant est le plus petit. Une tâche dont le tableau blockedBy n’est pas vide ne peut pas être prise en charge tant que toutes les tâches bloquantes n’ont pas atteint un état final.

Observation : la plupart des répertoires de tâches sont vides

Sur les 42 répertoires de tâches présents sur ma machine, seuls 5 contenaient effectivement des fichiers JSON de tâches. Les 37 autres ne contenaient que .lock et .highwatermark. Cela signifie probablement que les tâches sont supprimées une fois terminées, ou qu’il s’agissait de sessions où Claude utilisait la liste de tâches interne (disponible depuis le lancement de la fonctionnalité de liste de tâches), sans décomposition en fichiers de sous-tâches.

3. Communication entre agents

Modèle de boîte aux lettres

Chaque agent dispose d’un fichier contenant un tableau JSON à l’emplacement ~/.claude/teams/{team-name}/inboxes/{agent-name}.json. Voici une véritable boîte de réception issue d’une session précédente, où un responsable d’équipe avait confié du travail à un controlplane-agent :

[
  {
    "from": "team-lead",
    "text": "{\"type\":\"task_assignment\",\"taskId\":\"1\",\"subject\":\"Phase 2: Control-plane - remove participants/presence\",\"description\":\"Remove multiplayer code from the control-plane package...\",\"assignedBy\":\"team-lead\",\"timestamp\":\"2026-02-18T02:37:16.890Z\"}",
    "timestamp": "2026-02-18T02:37:16.890Z",
    "read": false
  }
]

Remarquez l’encodage JSON dans du JSON : le champ text est une chaîne JSON contenant un objet de message sérialisé. L’enveloppe externe comporte les champs from, text, timestamp et read.

Types de messages

Le champ type dans la charge utile text prend en charge les types suivants :

Type Sens Fonction
task_assignment responsable → coéquipier Attribuer une tâche avec tous ses détails
message n’importe quel agent → n’importe quel agent Envoyer un message direct à un destinataire
broadcast responsable → tous Envoyer le même message à chaque coéquipier
shutdown_request responsable → coéquipier Demander un arrêt propre
shutdown_response coéquipier → responsable Accepter ou refuser l’arrêt
plan_approval_request coéquipier → responsable Soumettre un plan pour examen
plan_approval_response responsable → coéquipier Approuver ou refuser avec des commentaires
idle_notification coéquipier → responsable Envoyé automatiquement à la fin du tour du coéquipier

Mécanisme de remise des messages

Chemin d’écriture : l’expéditeur ajoute une entrée au fichier contenant le tableau JSON de la boîte de réception du destinataire.

Chemin de lecture : le destinataire interroge régulièrement son propre fichier de boîte de réception. Les nouveaux messages sont injectés sous forme de tours de conversation synthétiques (ils apparaissent comme s’ils avaient été envoyés par un utilisateur).

Diffusion : écrit littéralement le même message dans le fichier de boîte de réception de chaque coéquipier. Le coût en tokens augmente linéairement avec la taille de l’équipe.

La communication se résume à un ajout dans un fichier + une lecture de fichier. La latence entre l’envoi et la réception dépend de l’intervalle d’interrogation du destinataire.

Visibilité des messages privés entre coéquipiers

Lorsqu’un coéquipier envoie un message privé à un autre coéquipier, un bref résumé est inclus dans la notification d’inactivité adressée au responsable. Cela permet au responsable de suivre la collaboration entre pairs sans recevoir le contenu intégral du message.

4. Lancement et cycle de vie des agents

Création des coéquipiers

Chaque coéquipier est un processus en ligne de commande claude distinct. Le responsable les lance avec l’outil Task et les paramètres team_name et name. Des variables d’environnement sont définies pour le processus lancé :

  • CLAUDE_CODE_TEAM_NAME : définie automatiquement pour les coéquipiers lancés
  • CLAUDE_CODE_PLAN_MODE_REQUIRED : définie à true si l’approbation du plan est requise

Initialisation du contexte

Les coéquipiers chargent le même contexte de projet que toute nouvelle session :

  • Les fichiers CLAUDE.md du répertoire de travail
  • Les serveurs MCP
  • Les skills
  • Le prompt de lancement fourni par le responsable

L’historique de conversation du responsable n’est PAS transmis. Chaque coéquipier démarre de zéro, avec pour seul contexte le prompt de lancement.

Implémentation interne

D’après l’analyse du binaire de Claude Code v2.1.47, le contexte des coéquipiers est géré via AsyncLocalStorage avec les champs suivants :

  • agentId, agentName, teamName
  • parentSessionId, color
  • planModeRequired

Principales fonctions internes :

  • isTeammate() / isTeamLead() : détection du rôle
  • waitForTeammatesToBecomeIdle() : primitive de synchronisation pour le responsable
  • getTeammateContext() / setDynamicTeamContext() : gestion du contexte d’exécution

Détection de l’inactivité

Après chaque tour du LLM, un coéquipier passe automatiquement à l’état inactif et envoie un idle_notification au responsable. Il s’agit de son état de repos normal, plutôt que d’une erreur ou d’un état périmé. L’envoi d’un message à un coéquipier inactif le réveille (le cycle d’interrogation suivant récupère le message de sa boîte de réception).

Protocole d’arrêt

  1. Le responsable envoie shutdown_request à un coéquipier
  2. Le coéquipier peut accepter (il s’arrête proprement) ou refuser (il continue à travailler en fournissant une explication)
  3. Le nettoyage de l’équipe via TeamDelete supprime ~/.claude/teams/{team-name}/ et ~/.claude/tasks/{team-name}/
  4. Le nettoyage échoue si des coéquipiers sont encore actifs ; ils doivent d’abord être arrêtés

Héritage des autorisations

Les coéquipiers héritent du mode d’autorisation du responsable au moment de leur lancement. Si le responsable utilise --dangerously-skip-permissions, tous les coéquipiers l’utilisent aussi. Les modes individuels peuvent être modifiés après le lancement, mais ne peuvent pas être configurés séparément pour chaque coéquipier au moment du lancement.

5. Contrôles qualité et hooks

Agent Teams s’intègre au système de hooks de Claude Code pour faire respecter les exigences de qualité :

Hook TeammateIdle

Se déclenche lorsqu’un coéquipier s’apprête à devenir inactif. Le code de sortie 2 transmet le contenu de stderr en guise de retour et empêche le passage à l’état inactif, ce qui maintient le coéquipier au travail.

{
  "hook_event_name": "TeammateIdle",
  "teammate_name": "researcher",
  "team_name": "my-project"
}

Hook TaskCompleted

Se déclenche lorsqu’une tâche est sur le point d’être marquée comme terminée. Le code de sortie 2 empêche cette validation et renvoie le contenu de stderr en guise de retour.

{
  "hook_event_name": "TaskCompleted",
  "task_id": "task-001",
  "task_subject": "Implement user authentication",
  "task_description": "Add login and signup endpoints",
  "teammate_name": "implementer",
  "team_name": "my-project"
}

Ce hook se déclenche dans deux situations : (1) lorsqu’un agent marque explicitement une tâche comme terminée via TaskUpdate, ou (2) lorsqu’un coéquipier d’une équipe d’agents termine son tour avec des tâches en cours.

Types de gestionnaires de hooks

Type Description
command Script shell. JSON sur stdin, codes de sortie pour les décisions.
prompt Évaluation par un LLM en un seul tour. Renvoie {ok, reason}.
agent Sous-agent sur plusieurs tours avec des outils de lecture. Jusqu’à 50 tours.

6. Coût des tokens

Les équipes d’agents utilisent environ 7 fois plus de tokens que les sessions standard lorsque les coéquipiers fonctionnent en mode planification. Chaque coéquipier conserve sa propre fenêtre de contexte complète en tant qu’instance Claude distincte.

Valeurs de référence

  • Utilisation moyenne de Claude Code : environ 6 $/développeur/jour
  • Équipes d’agents : surcoût approximativement proportionnel à la taille de l’équipe, en plus du coût de base

7. Synthèse de l’architecture

Dimension Claude Code Agent Teams
Support de coordination Fichiers plats (~/.claude/tasks/, ~/.claude/teams/)
Format des tâches Un fichier JSON par tâche + .lock pour la prise en charge
Messagerie Fichiers de boîte de réception JSON (ajout + interrogation périodique)
Cycle de vie des agents Processus en ligne de commande qui gèrent leur propre cycle de vie
Isolation du travail Répertoire de travail partagé
Stratégie de fusion Aucune (les agents modifient directement les fichiers)
Nouvelle tentative/escalade Manuelle (le responsable décide, ou l’utilisateur intervient)
Topologie Responsable + pairs sur un même niveau, messagerie entre pairs
Ordonnancement Prise en charge autonome (les coéquipiers prennent la tâche suivante)
Persistance de l’état Fichiers uniquement ; pas de reprise des coéquipiers au sein du processus
Contrôles qualité Hooks shell (TeammateIdle, TaskCompleted)
Suivi des tokens Par session uniquement, sans agrégation entre agents
Détection des blocages Manuelle (l’utilisateur remarque qu’un coéquipier s’est arrêté)
Gestion de la concurrence Implicite (taille de l’équipe = nombre de coéquipiers)
Modèle de dépendances blocks/blockedBy dans les fichiers de tâches

Sources

Documentation officielle

Artefacts sur disque (Claude Code v2.1.47)

Observés à l’emplacement /Users/tau/.claude/ :

  • Répertoires d’équipes contenant des fichiers config.json et inboxes/{agent-name}.json
  • Répertoires de tâches contenant .lock, .highwatermark et des fichiers JSON de tâches individuels
  • Exemple de message d’attribution de tâche de team-lead à cp-agent, horodaté 2026-02-18T02:37:16.890Z

Analyse du binaire

Binaire de Claude Code v2.1.47. Fonctions internes identifiées par analyse des chaînes de caractères : getTeamName, getAgentName, getAgentId, isTeammate, isTeamLead, waitForTeammatesToBecomeIdle, getTeammateContext, setDynamicTeamContext, createTeammateContext. Champs du contexte AsyncLocalStorage : agentId, agentName, teamName, parentSessionId, color, planModeRequired.

Code source de Hive