Reverse engineering di Claude Code Agent Teams: architettura e protocollo

Traduzione automatica Articolo originale.

Banner for Reverse engineering di Claude Code Agent Teams: architettura e protocollo

contenuto

Introduzione

Claude Code (v2.1.47) include una funzionalità sperimentale chiamata Agent Teams: più sessioni di Claude Code si coordinano su un lavoro condiviso attraverso una topologia con un coordinatore e più membri del team. Sto sviluppando Hive, un orchestratore di agenti di programmazione con obiettivi simili ma un'architettura molto diversa, quindi volevo capire come funziona internamente l'approccio di Anthropic.

Questo articolo documenta ciò che ho scoperto:

  1. Leggendo la documentazione ufficiale
  2. Esaminando gli artefatti effettivamente lasciati su disco dalle precedenti sessioni dei team
  3. Facendo analizzare a Claude il binario di Claude Code (v2.1.47) per ricavare dettagli sull'implementazione (ah ah!)

Indice

1. Panoramica dell'architettura

Un team di agenti è composto da quattro elementi:

Componente Ruolo
Coordinatore del team La sessione principale di Claude Code che crea il team e ne avvia i membri
Membri del team Istanze separate di Claude Code, ciascuna con la propria finestra di contesto
Elenco delle attività Unità di lavoro condivise, memorizzate su disco come singoli file JSON
Casella di posta File della posta in arrivo per ciascun agente, usati per la consegna dei messaggi

L'intero livello di coordinamento è basato su file. Il filesystem in ~/.claude/ è l'unico substrato di coordinamento:

~/.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
    └── ...

Si tratta di un'architettura fondamentalmente decentralizzata. Il coordinatore è semplicemente un'altra sessione di Claude con strumenti aggiuntivi (TeamCreate, TeamDelete, SendMessage). Non esiste alcun processo in background. Il coordinamento emerge dall'accesso condiviso ai file.

In una sessione attiva, se chiedi a Claude di avviare un team per svolgere un'attività e poi esegui quanto segue in un'altra finestra, puoi osservare gli aggiornamenti del filesystem in tempo reale.

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

Per esempio, con il seguente prompt:

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

Ho osservato questo:

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

Configurazione del team

La configurazione del team in ~/.claude/teams/{team-name}/config.json contiene un array members che i membri del team leggono per individuarsi a vicenda:

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

I nomi sono il principale meccanismo di indirizzamento (gli UUID esistono, ma non vengono usati per l'instradamento). Tutti i messaggi e le assegnazioni delle attività usano il campo name.

2. L'elenco condiviso delle attività

Formato dei file

Ogni attività viene memorizzata come singolo file JSON in ~/.claude/tasks/{team-name}/. Ecco un esempio reale tratto da una sessione precedente:

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

Schema delle attività:

Campo Tipo Descrizione
id string ID numerico, incrementato automaticamente tramite .highwatermark
subject string Titolo in forma imperativa (ad esempio, "Esegui i test")
description string Requisiti dettagliati e criteri di accettazione
activeForm string Forma progressiva per l'indicatore di attività ("Esecuzione dei test in corso")
status string pending → in_progress → completed (oppure deleted)
blocks string[] ID delle attività bloccate da questa attività
blockedBy string[] ID delle attività che devono essere completate prima che questa possa iniziare

Controllo della concorrenza

Due file speciali garantiscono il coordinamento:

  • .lock: Un file di 0 byte usato per la mutua esclusione a livello di filesystem (flock()). Presente in tutte le 42 directory delle attività osservate sulla mia macchina.
  • .highwatermark: Contiene un singolo intero (ad esempio, "3", "13"). Il prossimo ID di attività disponibile per l'incremento automatico.

Presa in carico delle attività

La presa in carico delle attività usa il blocco dei file per evitare condizioni di competizione. I membri del team preferiscono procedere a partire dagli ID più bassi. Un'attività con un array blockedBy non vuoto non può essere presa in carico finché tutte le attività che la bloccano non si trovano in uno stato terminale.

Osservazione: la maggior parte delle directory delle attività è vuota

Delle 42 directory delle attività sulla mia macchina, solo 5 contenevano veri e propri file JSON delle attività. Le altre 37 contenevano solo .lock e .highwatermark. Questo probabilmente significa che le attività vengono eliminate dopo il completamento, oppure che si trattava di sessioni in cui Claude ha usato l'elenco interno delle attività (disponibile dal lancio della funzionalità dell'elenco delle attività) senza suddividere il lavoro in file di sottoattività.

3. Comunicazione tra agenti

Modello a caselle di posta

Ogni agente ha un file contenente un array JSON in ~/.claude/teams/{team-name}/inboxes/{agent-name}.json. Ecco una casella di posta reale di una sessione precedente, in cui un coordinatore ha assegnato del lavoro a 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
  }
]

Nota la codifica JSON-in-JSON: il campo text è una stringa JSON che contiene un oggetto messaggio serializzato. L'involucro esterno contiene i campi from, text, timestamp e read.

Tipi di messaggi

Il campo type all'interno del payload text supporta:

Tipo Direzione Scopo
task_assignment coordinatore → membro Assegnare un'attività con tutti i dettagli
message qualsiasi → qualsiasi Messaggio diretto a un singolo destinatario
broadcast coordinatore → tutti Lo stesso messaggio a ogni membro del team
shutdown_request coordinatore → membro Richiedere un arresto ordinato
shutdown_response membro → coordinatore Approvare o rifiutare l'arresto
plan_approval_request membro → coordinatore Sottoporre un piano a revisione
plan_approval_response coordinatore → membro Approvare o rifiutare fornendo feedback
idle_notification membro → coordinatore Inviato automaticamente alla fine del turno del membro del team

Meccanismo di consegna

Percorso di scrittura: Il mittente aggiunge una nuova voce in coda al file contenente l'array JSON della casella di posta del destinatario.

Percorso di lettura: Il destinatario controlla periodicamente il proprio file della casella di posta. I nuovi messaggi vengono inseriti come turni sintetici della conversazione (appaiono come se li avesse inviati un utente).

Broadcast: Scrive letteralmente lo stesso messaggio nel file della casella di posta di ogni membro del team. Il costo in token cresce in modo lineare con le dimensioni del team.

La comunicazione è semplicemente un'aggiunta in coda a un file + una lettura di un file. La latenza tra invio e ricezione dipende dall'intervallo di polling del destinatario.

Visibilità dei messaggi diretti tra pari

Quando un membro del team invia un messaggio diretto a un altro membro, un breve riepilogo viene incluso nella notifica di inattività inviata al coordinatore. Questo permette al coordinatore di seguire la collaborazione tra pari senza il contenuto completo del messaggio.

4. Avvio degli agenti e ciclo di vita

Come vengono creati i membri del team

Ogni membro del team è un processo CLI claude separato. Il coordinatore li avvia tramite lo strumento Task con i parametri team_name e name. Nel processo avviato vengono impostate queste variabili d'ambiente:

  • CLAUDE_CODE_TEAM_NAME: impostata automaticamente per i membri del team avviati
  • CLAUDE_CODE_PLAN_MODE_REQUIRED: impostata su true se è richiesta l'approvazione del piano

Inizializzazione del contesto

I membri del team caricano lo stesso contesto del progetto di una nuova sessione:

  • File CLAUDE.md dalla directory di lavoro
  • Server MCP
  • Skill
  • Il prompt di avvio del coordinatore

La cronologia della conversazione del coordinatore NON viene trasferita. Ogni membro del team parte da zero, con il solo prompt di avvio come contesto.

Implementazione interna

Dall'analisi del binario di Claude Code v2.1.47 risulta che il contesto dei membri del team viene gestito tramite AsyncLocalStorage con questi campi:

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

Funzioni interne principali:

  • isTeammate() / isTeamLead(): rilevamento del ruolo
  • waitForTeammatesToBecomeIdle(): primitiva di sincronizzazione per il coordinatore
  • getTeammateContext() / setDynamicTeamContext(): gestione del contesto a runtime

Rilevamento dell'inattività

Dopo ogni turno dell'LLM, un membro del team passa automaticamente allo stato inattivo e invia un idle_notification al coordinatore. È il normale stato di riposo, non indica un errore o uno stato obsoleto. Inviare un messaggio a un membro del team inattivo lo riattiva (il ciclo di polling successivo rileva il messaggio nella casella di posta).

Protocollo di arresto

  1. Il coordinatore invia shutdown_request a un membro del team
  2. Il membro del team può approvare (termina in modo ordinato) o rifiutare (continua a lavorare fornendo una spiegazione)
  3. La pulizia del team tramite TeamDelete rimuove ~/.claude/teams/{team-name}/ e ~/.claude/tasks/{team-name}/
  4. La pulizia non riesce se alcuni membri del team sono ancora attivi; devono essere prima arrestati

Ereditarietà dei permessi

I membri del team ereditano la modalità dei permessi del coordinatore al momento dell'avvio. Se il coordinatore viene eseguito con --dangerously-skip-permissions, lo stesso vale per tutti i membri del team. Le singole modalità possono essere modificate dopo l'avvio, ma non configurate per ciascun membro del team al momento dell'avvio.

5. Controlli di qualità e hook

Agent Teams si integra con il sistema di hook di Claude Code per applicare i controlli di qualità:

Hook TeammateIdle

Si attiva quando un membro del team sta per passare allo stato inattivo. Il codice di uscita 2 invia stderr come feedback e impedisce il passaggio allo stato inattivo, mantenendo il membro del team al lavoro.

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

Hook TaskCompleted

Si attiva quando un'attività viene contrassegnata come completata. Il codice di uscita 2 impedisce il completamento e restituisce stderr come feedback.

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

Questo hook si attiva in due casi: (1) quando un qualsiasi agente contrassegna esplicitamente un'attività come completata tramite TaskUpdate, oppure (2) quando un membro di un team di agenti termina il proprio turno con attività in corso.

Tipi di gestori degli hook

Tipo Descrizione
command Script shell. JSON su stdin, codici di uscita per le decisioni.
prompt Valutazione tramite LLM in un solo turno. Restituisce {ok, reason}.
agent Subagente con più turni e strumenti di lettura. Fino a 50 turni.

6. Economia dei token

I team di agenti usano circa 7 volte più token rispetto alle sessioni standard quando i membri del team operano in modalità di pianificazione. Ogni membro del team mantiene la propria finestra di contesto completa come istanza separata di Claude.

Valori di riferimento

  • Utilizzo medio di Claude Code: ~6 $/sviluppatore/giorno
  • Team di agenti: consumo aggiuntivo rispetto al valore di base all'incirca proporzionale alle dimensioni del team

7. Riepilogo dell'architettura

Aspetto Claude Code Agent Teams
Substrato di coordinamento File flat (~/.claude/tasks/, ~/.claude/teams/)
Formato delle attività Un file JSON per attività + .lock per la presa in carico
Messaggistica File JSON della posta in arrivo (aggiunta in coda + polling)
Ciclo di vita degli agenti Processi CLI che si gestiscono autonomamente
Isolamento del lavoro Directory di lavoro condivisa
Strategia di merge Nessuna (gli agenti modificano direttamente i file)
Nuovi tentativi/escalation Gestione manuale (decide il coordinatore o interviene l'utente)
Topologia Coordinatore + membri di pari livello, messaggistica tra pari
Pianificazione Presa in carico autonoma (i membri del team prendono l'attività successiva)
Persistenza dello stato Solo file; nessuna ripresa dei membri del team all'interno del processo
Controlli di qualità Hook della shell (TeammateIdle, TaskCompleted)
Monitoraggio dei token Solo per sessione, nessuna aggregazione tra agenti
Rilevamento degli stalli Manuale (l'utente nota che un membro del team si è fermato)
Controllo della concorrenza Implicito (dimensioni del team = numero di membri del team)
Modello delle dipendenze blocks/blockedBy nei file delle attività

Fonti

Documentazione ufficiale

Artefatti su disco (Claude Code v2.1.47)

Osservati in /Users/tau/.claude/:

  • Directory dei team con file config.json e inboxes/{agent-name}.json
  • Directory delle attività con .lock, .highwatermark e singoli file JSON delle attività
  • Messaggio di esempio per l'assegnazione di un'attività da team-lead a cp-agent, con timestamp 2026-02-18T02:37:16.890Z

Analisi del binario

Binario di Claude Code v2.1.47. Funzioni interne identificate tramite l'analisi delle stringhe: getTeamName, getAgentName, getAgentId, isTeammate, isTeamLead, waitForTeammatesToBecomeIdle, getTeammateContext, setDynamicTeamContext, createTeammateContext. Campi del contesto AsyncLocalStorage: agentId, agentName, teamName, parentSessionId, color, planModeRequired.

Codice sorgente di Hive