L’articolo precedente spiegava come rendere affidabile un singolo agente: il ciclo di esecuzione, l’instradamento degli strumenti, la compattazione del contesto, le sessioni resistenti agli arresti anomali. Quel livello risponde alla domanda "come fa un singolo agente a portare a termine un’attività senza bloccarsi?" Questo articolo riguarda il livello superiore: cosa succede quando un agente non basta e un lavoro deve essere suddiviso tra i membri di un gruppo.
È ciò che di solito si intende per orchestrazione multiagente: un agente principale che gestisce la conversazione suddivide una richiesta in parti, affida ciascuna parte a un sottoagente specializzato, attende che quelli necessari finiscano prima di avviare il passaggio successivo, trasmette i risultati e mantiene l’intero processo sotto controllo quando un passaggio fallisce. Orkas esegue tutto questo interamente sul computer dell’utente. Di seguito vediamo come è costruito questo livello di orchestrazione: il codice è stato ripulito e generalizzato, ma la struttura è reale.
Agente principale e sottoagenti
Il modello da immaginare è quello di un piccolo gruppo con una chiara catena di comando. Un agente principale (che chiamiamo Commander) gestisce la conversazione e il contesto complessivo. Non svolge tutto il lavoro da solo; il suo compito è decidere che cosa va fatto, in quale ordine e da chi. I sottoagenti sono specialisti: ciascuno è configurato con un proprio prompt di sistema, i propri strumenti consentiti e il proprio insieme di abilità. Un sottoagente è competente in una parte del lavoro e viene invocato quando serve occuparsi di quella parte.
Due aspetti fanno sì che non sia solo una parola di moda. Primo, sottoagenti e abilità sono unità di prima classe, non espedienti nei prompt: un sottoagente è un agente reale, configurato separatamente, e assegnargli un lavoro è un vero passaggio di consegne con un contesto proprio. Secondo, il coordinamento non è lasciato alle buone intenzioni del modello: è guidato da un artefatto esplicito la cui coerenza con la realtà è garantita dal sistema, non dal modello. Questo artefatto è il piano.
Un piano è un grafo, non uno script
Quando l’agente principale decide che una richiesta richiede più di un passaggio, scrive un piano. Il piano non è un testo libero né un elenco di controllo lineare: è un piccolo grafo di dipendenze (un DAG). Ogni nodo è un passaggio, e ogni passaggio contiene tutto ciò che serve all’orchestratore per assegnarne l’esecuzione:
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;
}Il campo wait_for è ciò che trasforma un elenco in un grafo. Per impostazione predefinita, un passaggio attende quello precedente (una semplice catena), ma può dichiarare di dipendere da diversi passaggi anteriori: "riassumere" potrebbe attendere sia "studiare il mercato" sia "analizzare i concorrenti". È una struttura a rombo, non una linea, e l’orchestratore la tratta come tale.
Chi stabilisce la verità: l’orchestratore, non il modello
Osserva di nuovo la divisione all’interno di quella struttura. Il modello definisce l’intento — titoli, assegnatari, dati in ingresso, dipendenze — quando scrive il piano per la prima volta. Ma tutto ciò che riguarda lo stato di esecuzione — status, output_summary, failure_reason — è gestito esclusivamente dall’orchestratore. Il modello propone il piano una volta; non può mai contrassegnare da solo i propri passaggi come "completati".
Questa separazione è deliberata ed è la decisione più importante dell’intera progettazione. Un modello linguistico è perfettamente capace di annunciare con entusiasmo "passaggio 3 completato" quando il passaggio 3 ha generato un errore, oppure di perdere di vista i passaggi ancora da completare a metà di una lunga conversazione. Se lo stato risiedesse nella testa del modello, il piano si allontanerebbe dalla realtà. Rendendo lo stato un artefatto strutturato che solo l’esecutore può scrivere — e solo in risposta all’effettiva conclusione di un passaggio — il piano rimane uno specchio fedele di ciò che è realmente accaduto. Il modello decide la forma del lavoro; l’ambiente di esecuzione stabilisce la verità sul suo avanzamento.
Assegnare l’esecuzione dei passaggi pronti
Una volta creato un piano, un piccolo motore — l’esecutore — lo porta avanti. L’operazione fondamentale è "trovare i passaggi pronti e assegnarne l’esecuzione". Un passaggio è pronto quando è ancora pending e ogni passaggio che attende ha raggiunto uno stato finale con un esito sufficientemente positivo:
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
});
}Questo processo non si attiva a intervalli regolari, ma in risposta agli eventi. Ogni volta che un passaggio termina — un sottoagente restituisce un risultato, Commander conclude un turno di sintesi — l’esecutore riconcilia lo stato: registra l’esito del passaggio appena terminato, poi verifica di nuovo quali passaggi siano diventati pronti di conseguenza e ne assegna l’esecuzione. L’orchestrazione è questo ciclo di riconciliazione, ripetuto finché non rimangono più passaggi.
L’assegnazione passa dalla stessa chat, non da un canale separato
Ecco una scelta che mantiene il sistema fedele alla realtà: l’assegnazione di un passaggio non usa un canale RPC nascosto. Pubblica un messaggio nella stessa conversazione di gruppo che l’utente sta osservando, inviato dall’agente principale, con una @menzione del sottoagente. Per il sottoagente, ricevere un’assegnazione è indistinguibile dall’essere interpellato in chat: esegue semplicemente il suo normale turno. Non c’è un secondo percorso di esecuzione da mantenere sincronizzato con il primo.
L’assegnatario può essere di tre tipi, ciascuno con una modalità di assegnazione leggermente diversa:
- Un sottoagente — il caso comune. L’esecutore risolve il nome dell’agente nel suo ID e pubblica
@<agent> <rendered input>a nome di Commander. Il sottoagente riceve il messaggio ed esegue un turno completo da agente. - L’utente — quando un passaggio richiede davvero un contributo umano, si trasforma in un modulo e mette in pausa il piano (approfondiremo più avanti). Nessun passaggio successivo prosegue finché l’utente non risponde.
- Commander stesso — per i passaggi di sintesi o decisione ("leggi tutto quanto sopra e scrivi il riepilogo"). Si tratta di una riattivazione privata che non pubblica un messaggio ridondante visibile all’utente; l’agente principale esegue semplicemente un turno con il contesto raccolto a disposizione.
Trasmettere il contesto da un passaggio al successivo
Un gruppo è utile solo se il lavoro passa da un membro all’altro. Il meccanismo è il modello di input. Quando l’agente principale scrive un passaggio, il contenuto in ingresso non è una stringa immutabile: può fare riferimento a risultati precedenti, e l’esecutore sostituisce quei riferimenti al momento dell’assegnazione:
// 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…"Nota che cosa viene trasmesso: output_summary, un breve riepilogo di ogni passaggio concluso, non la sua intera trascrizione. È una decisione legata al budget, dettata dalla stessa logica della compattazione del contesto nel sistema di esecuzione. Se ogni passaggio successivo ereditasse l’intera cronologia, token per token, di tutto ciò che lo precede, il contesto crescerebbe a dismisura e i costi esploderebbero dopo pochi passaggi di consegne. I riepiloghi mantengono contenuto il costo di ogni passaggio di consegne e permettono a ciascun sottoagente di concentrarsi su ciò che gli serve davvero dai passaggi precedenti, invece di dover ricostruire come siano arrivati a quel risultato. Vengono trasmessi anche il messaggio iniziale dell’utente e gli eventuali allegati, così un passaggio che si trova tre anelli più avanti nella catena conosce ancora la richiesta originale.
Esecuzione seriale per impostazione predefinita — e perché
Potresti aspettarti che, quando diversi passaggi risultano pronti contemporaneamente — per esempio due rami di un rombo — l’orchestratore li avvii tutti in parallelo. Potrebbe farlo; oggi, invece, assegna l’esecuzione di un solo passaggio pronto alla volta, quello con l’indice più basso, e lascia che gli altri attendano la riconciliazione successiva. Far lavorare un intero gruppo rigorosamente un membro alla volta è una scelta deliberata e prudente, ed è giusto essere chiari sul perché.
Il motivo è garantire la correttezza in presenza di concorrenza. Immagina due sottoagenti che terminano quasi nello stesso istante. Entrambe le conclusioni attivano una riconciliazione; entrambe le riconciliazioni leggono il piano; entrambe vedono lo stesso passaggio successivo ancora in stato pending e ne assegnano l’esecuzione. Ora lo stesso passaggio viene eseguito due volte. Per renderlo impossibile, ogni ciclo di lettura, modifica e assegnazione in una determinata conversazione viene serializzato mediante un blocco specifico per quella conversazione:
// 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
});Il blocco garantisce che "registrare ciò che è terminato" e "decidere cosa viene dopo" avvengano come un’unica operazione indivisibile, così l’esecuzione di un passaggio successivo non può mai essere assegnata due volte. Con questo blocco, assegnare i passaggi uno alla volta è la soluzione più semplice la cui correttezza è evidente. Un’autentica distribuzione parallela del lavoro è un’estensione realizzabile su queste fondamenta, ma le fondamenta sono un esecutore serializzato e privo di condizioni di competizione. È questo l’ordine delle priorità che conta: prima la correttezza, poi la velocità.
Quando un passaggio va storto
Quando si lavora su un computer reale usando API di modelli esterni, gli errori sono normali, e l’orchestratore li distingue in alcuni casi anziché trattarli tutti allo stesso modo.
Per prima cosa verifica se il problema è semplicemente transitorio: una connessione interrotta, un limite di frequenza, un malfunzionamento momentaneo. In tal caso, e se il passaggio non ha già esaurito un numero limitato di tentativi, viene riportato senza notifiche allo stato pending, così la riconciliazione successiva ne assegna nuovamente l’esecuzione. (Questo meccanismo si colloca al di sopra dei tentativi effettuati dal sistema di esecuzione durante la singola esecuzione; il piano riassegna il passaggio solo dopo che l’agente ha esaurito i propri tentativi, con un limite rigido affinché un passaggio realmente guasto non possa ripetersi all’infinito.)
Se il fallimento è effettivo, la regola on_failure dichiarata per il passaggio decide cosa farà il gruppo:
abort_plan— questo passaggio era abbastanza importante da rendere insensato tutto ciò che viene dopo in sua assenza. Viene contrassegnato come fallito e l’effetto si propaga: tutti i passaggi ancora in attesa vengono contrassegnati comeskipped. Il piano si arresta in modo ordinato, anziché proseguire su fondamenta mancanti.continue— questo passaggio era facoltativo. Viene contrassegnato comeskippede i passaggi successivi possono proseguire come se non avesse semplicemente prodotto nulla.ask_commander(l’impostazione predefinita) — né interrompere né proseguire alla cieca. Il passaggio viene contrassegnato come fallito e l’agente principale viene riattivato per esaminare l’accaduto e decidere: riprovare in modo diverso, aggirare l’ostacolo oppure fermarsi e chiedere all’utente.
C’è un sesto stato che merita attenzione: blocked. Durante un passaggio, un sottoagente può accorgersi di aver bisogno di qualcosa che solo l’utente può fornire e presentare un modulo o una domanda. Il passaggio non fallisce: passa allo stato blocked e l’intero piano viene messo in pausa. Non appena l’utente risponde, l’esecutore riconcilia lo stato e il gruppo riprende esattamente da dove si era fermato. Un piano bloccato è un piano in pausa, non un piano guasto.
Ogni passaggio è un’esecuzione completa di un agente
Vale la pena ricollegarsi all’articolo precedente. Quando l’orchestratore assegna un passaggio a un sottoagente, quest’ultimo non esegue una procedura ridotta all’essenziale: esegue il ciclo completo del sistema di esecuzione, con il proprio ciclo di esecuzione in streaming, le proprie chiamate agli strumenti, la propria finestra di contesto con compattazione e la propria sessione resistente agli arresti anomali. L’orchestrazione si colloca nettamente al di sopra dell’ambiente di esecuzione del singolo agente; non interviene mai al suo interno. L’agente principale decide la forma del lavoro e l’ordine dei passaggi di consegne; ogni sottoagente, una volta ricevuta la propria parte, è un agente completo a tutti gli effetti.
È questa suddivisione in livelli che rende complementari i due articoli. Il sistema di esecuzione rende affidabile un agente per una singola attività. L’orchestratore riunisce diversi agenti affidabili in un gruppo capace di affrontare un’attività troppo grande o troppo varia per ciascuno di loro preso singolarmente.
Alcune decisioni che hanno fatto la differenza
L’orchestratore gestisce lo stato di esecuzione, il modello definisce l’intento. Il modello propone il piano; solo l’ambiente di esecuzione contrassegna i passaggi come completati, falliti o saltati, e solo in risposta a qualcosa che è realmente accaduto. È questo confine a mantenere il piano uno specchio fedele della realtà anziché una supposizione ottimistica del modello.
Assegnare i passaggi attraverso la stessa conversazione, non un canale separato. Un passaggio assegnato è semplicemente un messaggio dell’agente principale a un sottoagente. Un solo percorso di esecuzione, nessun elemento nascosto che possa perdere la sincronizzazione, e l’utente può osservare il gruppo al lavoro nella stessa discussione che sta già leggendo.
Riepiloghi, non trascrizioni, tra un passaggio e l’altro. Ogni passaggio di consegne trasmette un breve riepilogo dei risultati precedenti. Questo mantiene ragionevoli i budget di contesto anche lungo catene estese e permette a ogni sottoagente di concentrarsi su ciò che gli serve, non su come quello precedente sia arrivato al risultato.
Prima la correttezza, poi il parallelismo. Un blocco per conversazione serializza ogni ciclo di lettura, modifica e assegnazione, e i passaggi vengono assegnati uno alla volta. L’esecuzione rigorosamente seriale è la versione chiaramente priva di condizioni di competizione che portano a doppie assegnazioni; la distribuzione parallela è un’ottimizzazione da aggiungere a fondamenta già corrette.
Conclusione
Al cuore del livello di orchestrazione di Orkas non c’è alcun algoritmo esotico. Il suo valore sta in alcuni confini mantenuti saldi: un piano che è un grafo di dipendenze anziché uno script; uno stato di esecuzione gestito dall’ambiente di esecuzione anziché dal modello; un’assegnazione che passa dalla stessa chat osservata dall’utente; un contesto che si sposta tra i passaggi sotto forma di riepiloghi; e un esecutore che antepone la correttezza alla concorrenza. Preso singolarmente, ciascun elemento è semplice. Insieme trasformano un singolo agente affidabile in un gruppo che suddivide il lavoro, lo trasmette al membro successivo e si riprende quando una parte va storta.
Se vuoi conoscere il livello sottostante, leggi come viene progettato un singolo agente per funzionare in modo affidabile. Se ti interessa il livello che permette a ogni agente di migliorare con l’uso, leggi come gli agenti Orkas imparano dal proprio lavoro. E se preferisci dirigere questo livello anziché costruirlo, Orkas lo offre come orchestrazione open source di agenti IA che viene eseguita sul tuo computer.