// La Resa dei Conti
Sezione 00. Il mondo e' cambiatoOk ragazzi, parliamoci chiaro. E' aprile 2026 e un LLM scrive codice funzionante in qualsiasi linguaggio. Non roba giocattolo. Codice di produzione vero. Moduli interi, test inclusi, in secondi. Per tutto il codice standard, il CRUD, il glue code, il boilerplate, le integrazioni, gli script, e' gia' piu' veloce e spesso migliore della mediana di noi sviluppatori.
Non tutto il codice, ovvio. Il debugging di una race condition in un sistema distribuito, l'ottimizzazione di un hot path che deve stare sotto i 100 microsecondi, il design di un protocollo di consenso. Li' ci serve ancora il cervello, e ci servira' per un bel po'. Ma quella roba e' il 10% del lavoro quotidiano. Il restante 90% e' commodity. E la commodity la fa la macchina.
Se state ancora spendendo il vostro tempo per diventare piu' veloci a scrivere quel 90%, state ottimizzando la metrica sbagliata. Il compilatore non ci ha sostituiti negli anni '50, ma ha reso irrilevanti quelli che scrivevano assembly a mano per campare. Stesso film, un livello sopra.
Il valore si e' spostato. Si e' spostato nell'orchestrazione. Nel saper costruire sistemi dove gli LLM lavorano come agenti autonomi: ricevono un obiettivo, scelgono gli strumenti, eseguono, guardano il risultato, iterano. Nel 2026 scriviamo meno codice a mano, ma validiamo, integriamo, correggiamo e vincoliamo. Soprattutto, progettiamo i sistemi che lo fanno al posto nostro.
Questo articolo non e' teoria. Tutto quello che segue viene dallo stato dell'arte dell'ingegneria degli agent: il codice sorgente di Claude Code, l'agent CLI di Anthropic. 512.000 righe di TypeScript. Non e' l'unica architettura possibile, ma ad oggi e' una delle piu' complete e leggibili che si possano studiare. L'abbiamo smontato nell'articolo precedente. Oggi lo usiamo come riferimento ingegneristico.
// Il Loop
Sezione 01. Il cuore di ogni agentOgni agent system che si rispetti ha lo stesso cuore. Un ciclo infinito. Punto. L'LLM riceve un messaggio, decide cosa fare, esegue un'azione, guarda il risultato, e decide di nuovo. Ripete finche' l'obiettivo e' raggiunto o le risorse finiscono. E' cosi' semplice che fa quasi rabbia.
Il pattern si chiama ReAct (Reason + Act). In Claude Code vive dentro query.ts. Vi faccio vedere lo scheletro:
Pero' attenzione, nel codice reale il loop non e' un semplice while. E' un AsyncGenerator. Ogni iterazione yield-a eventi man mano che arrivano: lo streaming del testo, i progressi dei tool, i messaggi di sistema. Chi chiama riceve un flusso continuo senza aspettare che il turno finisca. Roba che se non l'avete usata vi cambia la prospettiva.
| 1 | async function* queryLoop( |
| 2 | params: QueryParams, |
| 3 | ): AsyncGenerator<StreamEvent | Message, Terminal> { |
| 4 | |
| 5 | let state: State = { |
| 6 | messages: params.messages, |
| 7 | toolUseContext: params.toolUseContext, |
| 8 | turnCount: 1, |
| 9 | // ... altro stato mutabile |
| 10 | } |
| 11 | |
| 12 | while (true) { |
| 13 | let { toolUseContext } = state |
| 14 | const { messages, turnCount } = state |
| 15 | |
| 16 | // 1. Chiama l'LLM |
| 17 | const response = yield* queryModelWithStreaming({ |
| 18 | messages, systemPrompt, tools, maxTokens |
| 19 | }) |
| 20 | |
| 21 | // 2. Estrai i tool_use dalla risposta |
| 22 | const toolUseBlocks = extractToolUse(response) |
| 23 | |
| 24 | // 3. Esegui i tool |
| 25 | for await (const update of runTools( |
| 26 | toolUseBlocks, canUseTool, toolUseContext |
| 27 | )) { |
| 28 | yield update.message // stream il risultato |
| 29 | } |
| 30 | |
| 31 | // 4. Valuta se continuare |
| 32 | const transition = evaluateTransition(response, state) |
| 33 | if (transition.type === 'terminal') return transition |
| 34 | state = transition.nextState |
| 35 | } |
| 36 | } |
Perche' AsyncGenerator e non una Promise? Perche' l'agent deve comunicare mentre lavora. Se un tool ci mette 30 secondi, l'utente vede i progressi in tempo reale. Il generator yield-a eventi senza bloccare nessuno. Stesso pattern dello streaming HTTP, applicato all'orchestrazione. Elegante.
Lo stato del loop e' tutto esplicito. Un oggetto State che porta i messaggi, il contesto dei tool, il contatore dei turni, i flag di recovery. A ogni iterazione lo destrutturi in cima e lo ricostruisci ai punti di continue. Niente variabili globali. Niente side effect nascosti. Il loop e' deterministico nell'orchestrazione, non deterministico nella decisione. L'infrastruttura e' prevedibile. Il motore no. Tenetevelo a mente, perche' e' la chiave di tutto.
// I Tool
Sezione 02. Le mani dell'agentUn LLM senza tool e' un poeta. Scrive roba bellissima ma non puo' toccare niente nel mondo reale. I tool sono le mani. Ogni tool e' un'interfaccia tipizzata che l'agent puo' invocare per fare qualcosa di concreto: leggere un file, eseguire un comando, cercare nel codice, scrivere su disco.
In Claude Code, un tool e' un oggetto TypeScript con un contratto preciso. Il file Tool.ts definisce l'interfaccia. Guardatela bene:
| 1 | type Tool<Input, Output> = { |
| 2 | name: string // identificatore unico |
| 3 | inputSchema: ZodType // schema dell'input (validazione) |
| 4 | description(): Promise<string> // descrizione per l'LLM |
| 5 | call(args, context): Promise<ToolResult> // esecuzione |
| 6 | checkPermissions(): PermissionResult // puo' eseguire? |
| 7 | isReadOnly(input): boolean // modifica lo stato? |
| 8 | isConcurrencySafe(input): boolean // esecuzione parallela? |
| 9 | isDestructive?(input): boolean // operazione irreversibile? |
| 10 | maxResultSizeChars: number // limite risultato |
| 11 | } |
Ogni campo ha un motivo di esistere. Lo inputSchema usa Zod: l'input che arriva dall'LLM viene validato a runtime prima di eseguire qualsiasi cosa. Se l'LLM ti manda un JSON fatto male, il tool non parte nemmeno. L'errore torna nell'history e l'LLM si corregge al giro dopo. Meccanismo di self-healing gratis.
Il campo isReadOnly e' il discriminante critico per l'orchestrazione. Determina se il tool puo' girare in parallelo con altri o no. Un Grep e' read-only. Un Write no. Due Grep li fai partire insieme senza pensarci. Un Write deve aspettare da solo. Semplice, ma se non ce l'hai ti si corrompono i file.
La description e' dinamica. Non e' una stringa hardcoded. E' una funzione asincrona che riceve il contesto corrente e genera la descrizione al volo. La descrizione di Bash cambia in base al sistema operativo su cui giri. Quella di Agent cambia in base a quali agent sono disponibili in quel momento. L'LLM vede sempre una descrizione aggiornata, calibrata sulla situazione reale. Non e' un dettaglio, e' design.
Il pattern da portarvi a casa: un tool non e' una funzione. E' un contratto. Ha uno schema, ha permessi, ha metadati di concorrenza, ha limiti di output. Senza questo contratto, l'orchestratore non sa come comporre i tool tra loro. Il tool non e' la funzione che esegue. E' l'interfaccia che espone. E i contratti sono l'unica cosa che puoi comporre in modo affidabile.
// L'Orchestrazione dei Tool
Sezione 03. Parallelo vs serialeL'LLM in un singolo turno puo' chiedere di usare piu' tool contemporaneamente. "Leggimi questi tre file." Oppure: "Leggi questo file e scrivi quest'altro." La differenza e' enorme. I tre file li leggi in parallelo tranquillamente. Leggere e scrivere insieme no, perche' la scrittura potrebbe dipendere dalla lettura. Sembra ovvio, ma il diavolo e' nei dettagli.
Claude Code risolve con un partizionamento intelligente dentro toolOrchestration.ts:
| 1 | async function* runTools(toolUseBlocks, canUseTool, context) { |
| 2 | for (const { isConcurrencySafe, blocks } of |
| 3 | partitionToolCalls(toolUseBlocks, context) |
| 4 | ) { |
| 5 | if (isConcurrencySafe) { |
| 6 | // Batch read-only: esegui in parallelo (max 10) |
| 7 | for await (const update of runToolsConcurrently(blocks)) { |
| 8 | yield update |
| 9 | } |
| 10 | } else { |
| 11 | // Batch non-read-only: esegui uno alla volta |
| 12 | for await (const update of runToolsSerially(blocks)) { |
| 13 | yield update |
| 14 | } |
| 15 | } |
| 16 | } |
| 17 | } |
partitionToolCalls scannerizza i tool_use blocks e li raggruppa. Tool consecutivi read-only finiscono in un batch concorrente. Appena appare un tool non-read-only, il batch precedente viene eseguito, poi il tool distruttivo gira da solo. Poi si ricomincia a raggruppare.
La concorrenza massima e' 10 per default, configurabile via CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY. Non e' un numero tirato a caso. E' il compromesso tra velocita' e risorse: ogni tool potrebbe leggere file, consumare file descriptor, allocare memoria. Dieci in parallelo, non cinquanta.
Il pattern: non far decidere all'LLM l'ordine di esecuzione. L'LLM dice "fai queste 5 cose". L'orchestratore decide come farle. L'LLM ragiona. L'orchestratore esegue. Tenere separate queste responsabilita' e' tutto. Se le mischi, hai il caos.
// I Permessi
Sezione 04. La catena della fiduciaUn agent che puo' fare tutto e' una bomba a orologeria. Senza permessi, prima o poi ti cancella un file, ti esegue un comando folle, ti modifica il database. E' questione di quando, non di se. Il sistema di permessi e' il constraint che rende l'agent utilizzabile senza farsi venire l'ansia.
Claude Code implementa un modello a strati. Ogni tool call passa attraverso quattro livelli di controllo prima di eseguire qualsiasi cosa:
Primo livello: regole dichiarative. In settings.json scrivi roba tipo "il tool Bash puo' sempre eseguire git status" oppure "il tool Write non tocca niente dentro /etc". Pattern matching puro, zero ambiguita', zero sorprese.
Secondo livello: gli hook. Script che girano prima dell'esecuzione del tool. Ricevono l'input in JSON su stdin, restituiscono una decisione su stdout. Vuoi un hook che blocca qualsiasi rm -rf? Dieci righe di bash. Vuoi uno che logga ogni operazione di scrittura su un servizio esterno? Fattibile. Gli hook sono codice arbitrario, puoi metterci quello che vuoi.
Terzo livello: un classificatore machine learning. Analizza il comando Bash e decide se e' sicuro. Non e' pattern matching stupido. E' un modello che capisce il contesto. rm -rf node_modules e' diverso da rm -rf /. Uno lo fai dieci volte al giorno, l'altro ti manda in pensione anticipata.
Quarto livello: l'utente. Se tutti i livelli precedenti dicono "boh, non saprei", appare un prompt. Vedi esattamente cosa sta per eseguire e approvi o rifiuti. Puoi dire "approva sempre per questa classe di operazioni", e la regola si aggiunge al primo livello. Il sistema impara.
Principio di design. I permessi sono conservativi per default. Un tool nuovo, senza regole, richiede approvazione. Il sistema e' opt-in per la fiducia, opt-out per il controllo. L'opposto di come funziona la maggior parte del software. E per una buona ragione.
// I Sub-Agent
Sezione 05. L'agent che spawna agentEcco il pattern piu' potente di tutto il sistema: la ricorsione. Un agent puo' creare altri agent. Ogni sub-agent ha il suo loop, i suoi tool, la sua history. Lavora su un sotto-problema e riporta il risultato al padre. Come un processo che fa fork, ma con un cervello LLM dentro.
In Claude Code, lo spawn avviene tramite il tool Agent. E' un tool come tutti gli altri. L'LLM decide di usarlo, l'orchestratore lo esegue. Ma l'esecuzione non e' una singola operazione. E' un intero ciclo di agent autonomo che parte, gira, e ritorna.
| 1 | // Schema dell'AgentTool. Questo e' quello che l'LLM vede. |
| 2 | const baseInputSchema = z.object({ |
| 3 | description: z.string() // "Cerca i file di test" |
| 4 | .describe('A short (3-5 word) description of the task'), |
| 5 | prompt: z.string() // Il brief completo per il sub-agent |
| 6 | .describe('The task for the agent to perform'), |
| 7 | subagent_type: z.string().optional() |
| 8 | .describe('The type of specialized agent to use'), |
| 9 | model: z.enum(['sonnet', 'opus', 'haiku']).optional(), |
| 10 | run_in_background: z.boolean().optional(), |
| 11 | }) |
L'LLM non ha la minima idea di come funzioni il sub-agent sotto il cofano. Vede uno schema: dammi una descrizione, un prompt, opzionalmente un tipo e un modello. Tutto qui. Sotto, l'orchestratore crea un contesto isolato, clona lo stato dei file, lancia un nuovo queryLoop() e streama i risultati verso il padre. L'LLM sa fare il briefing, l'infrastruttura fa il resto.
La parte davvero interessante e' l'isolamento. Ogni sub-agent ha:
History separata. I messaggi del sub-agent non inquinano la conversazione del padre. Il padre vede solo il risultato finale. Niente rumore.
File state clonato. Il sub-agent parte con una fotografia dello stato dei file al momento dello spawn. Se il padre modifica un file dopo lo spawn, il sub-agent non lo vede. Zero race condition.
Permessi ereditati. Il sub-agent eredita le regole di permesso dal padre. Non puo' fare nulla che il padre non potrebbe fare. La catena di fiducia e' monotona: si restringe, non si espande mai. Principio del minimo privilegio applicato alla ricorsione.
Worktree opzionale. Con isolation: 'worktree', il sub-agent lavora in una copia git separata del repository. Puo' modificare file senza toccare il working tree del padre. Le modifiche vengono integrate solo se il padre decide di farlo. Come un branch, ma per l'agent.
Perche' il sub-agent e' un tool? Perche' e' l'LLM a decidere quando usarlo. Non c'e' un orchestratore esterno che "smista" il lavoro dall'alto. L'agent stesso valuta: "questo task e' grosso, lo delego a un sotto-agente". E' auto-organizzazione pura. L'architettura non prescrive quando parallelizzare. L'agent lo scopre da solo.
// La Comunicazione
Sezione 06. Come parlano tra loroOk, avere agent che spawnano agent e' gia' potente. Ma la roba diventa seria quando gli agent si parlano tra loro. Non solo padre-figlio, ma peer-to-peer. E qui Claude Code fa una cosa che la maggior parte dei framework agent nemmeno contempla: ha un vero sistema di messaggistica inter-agent.
Il tool si chiama SendMessage. Un agent puo' mandare un messaggio a un altro agent per nome, per ID, o in broadcast a tutto il team. La sintassi e' semplice: SendMessage(to: "researcher", message: "ho finito, ecco il risultato"). Sotto il cofano, il messaggio finisce in una mailbox su file.
Si', avete letto bene. Mailbox su file. Ogni agent ha una inbox: ~/.claude/teams/{team}/inboxes/{agent}.json. I messaggi sono JSON con timestamp, flag di lettura, e un campo summary per l'anteprima. File locking con retry e backoff per gestire le scritture concorrenti. Niente shared memory, niente socket complicati. File su disco. Funziona ovunque, e' debuggabile con cat, e non si rompe se un processo crasha. A volte la soluzione semplice e' quella giusta.
Ma non sono solo messaggi di testo. SendMessage supporta messaggi strutturati:
shutdown_request / shutdown_response. Un agent puo' chiedere a un altro di chiudersi con grazia. L'altro puo' accettare o rifiutare con una motivazione. Protocollo di spegnimento coordinato.
plan_approval_response. Il team lead puo' approvare o rifiutare il piano di un teammate con feedback. L'agent riceve il feedback e si adegua. Supervisione asincrona.
Broadcast. to: "*" manda a tutti i teammate del team. Utile per annunci tipo "ho modificato il file X, aggiornatevi".
La cosa bella e' che se mandi un messaggio a un agent che si era fermato, il sistema lo risveglia automaticamente. resumeAgentBackground() lo riprende con il messaggio come nuovo prompt. L'agent non deve stare in polling. Arriva il messaggio, si sveglia, lavora, risponde, si rimette in attesa. Come un servizio event-driven, ma fatto di LLM.
Il pattern da capire: la comunicazione inter-agent non e' sincrona tipo chiamata di funzione. E' asincrona tipo email. Ogni agent ha il suo loop indipendente, la sua inbox, il suo ritmo. Si coordinano scambiandosi messaggi, non condividendo stato. Se avete mai lavorato con Erlang o con gli actor model, riconoscerete il pattern. E non e' un caso.
Per i permessi c'e' un meccanismo doppio. Se il team lead ha l'UI aperta, il teammate che ha bisogno di approvazione mette la richiesta in una coda condivisa e il leader la vede a schermo. Se il leader e' headless, la richiesta finisce nella mailbox e il teammate si mette in polling ogni 500ms finche' non arriva la risposta. Due path, stesso risultato. Resilienza.
// La Memoria
Sezione 07. Sopravvivere al context windowIl context window e' il collo di bottiglia di ogni agent. 200K token sembrano tanti finche' non hai letto 15 file, eseguito 30 comandi e fatto 10 turni di conversazione. A quel punto sei gia' a meta' finestra. I messaggi vecchi devono uscire. Ma non puoi buttarli via a caso: potresti perdere una decisione critica presa 20 turni fa che cambia tutto.
Claude Code gestisce la memoria su tre livelli. Ognuno risolve un problema diverso.
Primo livello: compaction automatica. Quando il contesto si avvicina al limite, parte un processo che riassume i messaggi vecchi. Non li cancella, li compatta. Un agent separato (un runForkedAgent) riceve l'history completa e produce un riassunto strutturato. Il riassunto sostituisce i messaggi originali con un CompactBoundaryMessage. I messaggi recenti restano intatti. Il vecchio diventa un riassunto, il nuovo resta com'era.
Secondo livello: file state cache. Una cache LRU dei file letti. Se l'agent legge lo stesso file due volte, la seconda volta il contenuto arriva dalla cache, senza consumare token per un nuovo tool_use. La cache viene clonata per i sub-agent e invalidata quando un file viene modificato. Semplice, ma ti salva un sacco di token.
Terzo livello: memoria persistente. I file CLAUDE.md nella directory del progetto. Markdown puro, caricati automaticamente nel system prompt a ogni sessione. L'agent ci puo' scrivere tramite il tool Edit. Questa memoria sopravvive tra sessioni. E' dove l'agent salva convenzioni, decisioni architetturali, le vostre preferenze. La memoria a lungo termine della bestia.
| 1 | // Strategia di gestione della memoria |
| 2 | |
| 3 | if (tokenCount > threshold) { |
| 4 | // Livello 1: compatta la history |
| 5 | summary = await runForkedAgent('summarize', messages) |
| 6 | messages = [CompactBoundary(summary), ...recentMessages] |
| 7 | } |
| 8 | |
| 9 | if (fileAlreadyRead(path)) { |
| 10 | // Livello 2: usa la cache |
| 11 | return fileStateCache.get(path) |
| 12 | } |
| 13 | |
| 14 | // Livello 3: memoria persistente |
| 15 | systemPrompt += loadCLAUDE_MD(projectDir) |
La compaction non e' un truncate stupido. E' un riassunto intelligente fatto da un LLM. Preserva le decisioni chiave, le istruzioni dell'utente, i vincoli scoperti durante l'esecuzione. E' lossy, ok, l'informazione si perde. Ma la perdita e' guidata dalla rilevanza, non dalla posizione cronologica. E questo fa tutta la differenza.
// Il Retry
Sezione 08. Fallire con graziaUn agent che chiama un'API chiamera' un'API che fallisce. E' una certezza statistica. Rate limit, timeout, server sovraccarichi. Il retry e' l'ammortizzatore tra il mondo perfetto del loop e la realta' dell'infrastruttura che se ne frega dei tuoi piani.
Il modulo withRetry.ts implementa una strategia differenziata. Non tratta tutti gli errori allo stesso modo, e qui c'e' il bello.
429 (rate limit): fino a 10 retry con backoff esponenziale. Il delay parte da 500ms e raddoppia a ogni tentativo. Il rate limit e' temporaneo per definizione. Aspetti e riprovi. Fine.
529 (server sovraccarico): massimo 3 retry, e solo per query in foreground dove l'utente sta aspettando. Se e' una query di background (riassunti, suggerimenti, classificatori), il retry viene saltato completamente. Perche'? Ogni retry su un server sovraccarico amplifica il carico di 3-10x. Se non e' critico, non aggravare il problema. Questo e' pensiero da system engineer, non da tutorial.
Errori di connessione: un singolo retry. La connessione potrebbe essere andata stale. Un secondo tentativo basta per verificare.
Il dettaglio che fa la differenza. I retry sulle query di background sono disabilitati. Il commento nel codice lo spiega meglio di me: "during a capacity cascade each retry is 3-10x gateway amplification, and the user never sees those fail anyway." Non e' solo resilienza. E' responsabilita' verso l'infrastruttura condivisa. Se non avete mai ragionato cosi', iniziate adesso.
// Il Protocollo dei Messaggi
Sezione 09. La lingua francaL'agent e l'LLM comunicano attraverso un protocollo strutturato. Non testo libero. Oggetti JSON tipizzati che seguono lo schema dell'Anthropic Messages API. Se pensate che sia "solo JSON", vi state perdendo il pezzo piu' importante.
Un messaggio dell'assistente contiene un array di content blocks. Guardate cosa ci puo' finire dentro:
| 1 | { |
| 2 | "role": "assistant", |
| 3 | "content": [ |
| 4 | { |
| 5 | "type": "thinking", |
| 6 | "thinking": "Devo leggere il file prima di modificarlo..." |
| 7 | }, |
| 8 | { |
| 9 | "type": "text", |
| 10 | "text": "Leggo il file di configurazione." |
| 11 | }, |
| 12 | { |
| 13 | "type": "tool_use", |
| 14 | "id": "toolu_01ABC", |
| 15 | "name": "Read", |
| 16 | "input": { "file_path": "/src/config.ts" } |
| 17 | } |
| 18 | ] |
| 19 | } |
Tre tipi di content block in un singolo messaggio. Il thinking e' il ragionamento interno dell'LLM, il "pensiero ad alta voce" che normalmente non vedi. Il text e' quello che l'utente legge a schermo. Il tool_use e' l'istruzione per l'orchestratore: "esegui questo tool con questi parametri".
Dopo l'esecuzione, l'orchestratore aggiunge un messaggio con ruolo user contenente un tool_result:
| 1 | { |
| 2 | "role": "user", |
| 3 | "content": [{ |
| 4 | "type": "tool_result", |
| 5 | "tool_use_id": "toolu_01ABC", |
| 6 | "content": "export default { port: 3000, ... }" |
| 7 | }] |
| 8 | } |
Il tool_use_id lega il risultato alla richiesta. L'LLM vede la sua richiesta, poi il risultato, e decide il passo successivo. Il protocollo e' stateless dal punto di vista dell'LLM: l'intera history viene mandata a ogni turno. L'agent gestisce lo stato, l'LLM ragiona sulla foto completa.
Implicazione pesante: l'LLM non ha memoria. Ha contesto. La differenza e' che il contesto viene costruito dall'agent a ogni turno. L'agent decide cosa includere, cosa compattare, cosa buttare. L'LLM ragiona solo su quello che riceve. Il potere vero non e' nel modello. E' in chi costruisce il contesto che il modello vede.
// Il Tool Search
Sezione 10. Tool on demandUn agent con 100 tool ha un problema concreto: 100 descrizioni nel system prompt occupano token. Token che potrebbero servire per il lavoro vero. La soluzione di Claude Code e' il deferred loading, e vi spiego perche' e' geniale.
All'avvio, solo i tool essenziali vengono caricati nel prompt, quelli con alwaysLoad: true. I tool meno usati vengono registrati con shouldDefer: true: l'LLM vede solo il nome, non lo schema completo.
Quando l'LLM ha bisogno di un tool deferrito, usa il meta-tool ToolSearch. Cerca per keyword, riceve lo schema completo, e a quel punto puo' invocarlo normalmente. Due fasi: discovery e invocazione. Lazy loading applicato ai tool di un agent.
Ogni tool deferrito ha un searchHint: una frase di 3-10 parole che aiuta il matching. Per esempio, NotebookEdit ha come hint "jupyter". L'LLM cerca "jupyter", trova NotebookEdit, ne riceve lo schema, lo usa. Zero token sprecati per tool che non servono nella sessione corrente. Se non vi serve il notebook, il notebook non vi costa niente.
// Mettere Tutto Insieme
Sezione 11. L'architettura completaOk, ricapitoliamo. Un agent system completo ha otto componenti:
L'ottavo lo abbiamo visto passare senza isolarlo, ma e' il pezzo che chiude il cerchio. Un agent senza evaluation e' un loop cieco. Esegue, ma non sa se ha fatto bene.
In Claude Code l'evaluation e' ovunque, ma non ha un modulo dedicato con la scritta "evaluation" sopra. E' distribuita. Quando il loop valuta evaluateTransition(), sta decidendo: il risultato e' sufficiente per fermarsi? L'LLM stesso, vedendo il tool_result nel contesto, valuta se il file letto contiene quello che cercava, se il comando ha restituito l'output atteso, se il codice scritto compila. I sub-agent di tipo "verification" rileggono il lavoro del padre e cercano errori. Gli hook post-tool-use ispezionano il risultato e possono forzare un rollback.
Il pattern e' questo: l'evaluation non e' un passo separato alla fine. E' un feedback loop continuo integrato in ogni iterazione. Il tool restituisce un risultato, l'LLM lo valuta, decide se e' soddisfacente o se serve un altro giro. Ogni turno del loop e' implicitamente un ciclo plan-execute-evaluate. Se togliete l'evaluation, avete un agent che scrive codice senza mai leggerlo. E sapete tutti come va a finire.
Nessuno di questi pezzi e' complesso da solo. Il while(true) e' banale. L'interfaccia di un tool sono 10 campi. La partizione parallelo/seriale e' un if. I permessi sono una catena di if. I sub-agent sono lo stesso loop in un contesto clonato. La comunicazione e' una mailbox su file JSON. La memoria e' una cache LRU piu' un riassunto. L'evaluation e' l'LLM che guarda il proprio output e decide se basta.
La complessita' vera e' nella composizione. Nel fatto che il loop gestisce lo streaming. Che i tool producono side effect che modificano il contesto del prossimo turno. Che i sub-agent ereditano permessi ma isolano lo stato. Che gli agent si parlano via mailbox senza condividere memoria. Che la compaction perde informazione ma la perde in modo controllato. Che il retry discrimina tra query critiche e background. Che l'evaluation e' distribuita in ogni turno invece che concentrata alla fine. Ogni pezzo e' semplice. Il sistema no.
Queste sono decisioni di design, non di codice. Non si tratta di scrivere TypeScript piu' veloce. Si tratta di sapere quando un tool e' read-only, quando un retry amplifica il carico, quando un sub-agent ha bisogno di un worktree isolato. Sono decisioni architetturali. E l'architettura non si genera con un prompt.
// Costruisci il Tuo
Sezione 12. Lo stack realeOk, avete capito i pattern. Adesso la domanda e': con cosa li implemento? Vi do la mappa completa. Non i cinque passi da tutorial, ma lo stack vero con i trade-off veri.
Il runtime e il loop. Se partite da zero, l'Anthropic Agent SDK (claude_agent_sdk in Python) vi da' il loop ReAct gia' fatto con streaming, gestione dei tool_use, e retry integrato. In TypeScript c'e' @anthropic-ai/sdk che vi da' lo streaming nativo dei content block. Se volete il controllo totale, il loop e' un while(true) che chiama messages.create() con stream=True, parsea i tool_use blocks dalla risposta, esegue i tool, e appende i tool_result alla history. Cinquanta righe di codice. Il punto non e' la complessita' del loop, e' tutto quello che ci costruite attorno.
I tool e il protocollo. Qui avete due strade. La prima: definite i tool inline come JSON Schema nell'array tools della chiamata API. Funziona, e' veloce, avete il controllo. La seconda: usate MCP, il Model Context Protocol. MCP e' un protocollo aperto per esporre tool, risorse e prompt a qualsiasi agent. Un server MCP e' un processo separato che risponde via JSON-RPC su stdio o HTTP. Il vantaggio e' che lo stesso server MCP funziona con Claude Code, con Cursor, con qualsiasi client compatibile. Se costruite un tool per accedere al vostro database, con MCP lo scrivete una volta e lo usate ovunque. L'SDK ufficiale e' @modelcontextprotocol/sdk in TypeScript e mcp in Python.
L'orchestrazione. Se avete tool che possono girare in parallelo, vi serve il partizionamento che abbiamo visto. Il trucco e' taggare ogni tool con un flag isReadOnly. I read-only li sparate con Promise.all() o asyncio.gather(). I non-read-only li eseguite in serie. Mettete un cap sulla concorrenza (10 e' un buon default). Se non fate questo, il vostro agent sara' 3-5x piu' lento del necessario su qualsiasi task che coinvolga piu' file.
I permessi. Al minimo: un if che chiede conferma per i comandi distruttivi. Ma se fate le cose sul serio, vi serve una catena. Le regole statiche le mettete in un file di config (allow/deny per tool e pattern). Gli hook li implementate come script shell che ricevono JSON su stdin e rispondono su stdout: {"decision": "allow"} o {"decision": "block", "reason": "..."}. Per il classificatore ML, se non avete un modello vostro, potete fare una chiamata a un LLM economico (Haiku) con un prompt tipo "questo comando bash e' sicuro da eseguire in un ambiente di sviluppo?". Costa quasi niente e vi salva dai disastri.
La memoria. Livello zero: quando i token superano il 70% della finestra, troncate i messaggi piu' vecchi. Livello uno: invece di troncare, fate una chiamata a un modello piccolo con il prompt "riassumi questa conversazione preservando le decisioni chiave e i vincoli". Livello due: aggiungete un file di memoria persistente (un markdown nel progetto) che l'agent puo' leggere e scrivere. Livello tre: se volete la microcompact nativa, l'API di Anthropic supporta strategie come clear_tool_uses e clear_thinking che rimuovono blocchi vecchi lato server senza che voi dobbiate toccare la history.
La comunicazione. Se i vostri agent girano nello stesso processo, una coda in-memory (un array con push/shift) basta. Se girano in processi separati, il pattern mailbox su file che abbiamo visto funziona sorprendentemente bene: un JSON per agent, file locking, polling ogni 500ms. Se avete bisogno di scalare, Redis pub/sub o NATS. Ma non partite da li'. Il 90% dei sistemi agent multi-processo funziona benissimo con file su disco.
I sub-agent. Aggiungete un tool "Agent" al vostro loop che lancia una nuova istanza dello stesso loop con un prompt diverso. Il punto critico e' l'isolamento: clonate la cache dei file (non condividetela), date al figlio una history separata, e fate in modo che i permessi del figlio siano un sottoinsieme di quelli del padre. Se volete il worktree isolation, git worktree add vi crea una copia del repo in tre secondi. Il figlio lavora li', il padre non vede niente finche' non integra.
Sui framework. LangChain, CrewAI, AutoGen, Semantic Kernel. Li conoscete tutti. Il mio consiglio: usateli per prototipare, non per produzione. Vi danno il loop e i tool gratis, ma vi nascondono le decisioni critiche. Come gestisce la concorrenza? Come compatta il contesto? Come isola i sub-agent? Quando queste domande diventano importanti (e diventano importanti), il framework vi rema contro perche' dovete lottare con le sue astrazioni invece di risolvere il problema. Se avete capito i pattern di questo articolo, potete costruire il vostro stack con l'SDK del provider e 200 righe di orchestrazione. E avrete il controllo su ogni decisione.
// Dove Questi Sistemi Falliscono
Sezione 13. Il lato oscuroSarebbe disonesto chiudere senza parlarvi dei modi in cui un agent system vi esplode in mano. Sono modi specifici, diversi dai bug tradizionali, e se non li conoscete vi colpiscono nel momento peggiore. Ve li elenco perche' ci sono passato.
Hallucination sui tool. L'LLM invoca un tool con input plausibile ma sbagliato. Chiede di leggere un file che non esiste. Passa un flag inventato a un comando. Scrive codice che chiama una funzione con la firma sbagliata. Lo schema Zod cattura gli errori di formato, ma non quelli semantici. L'agent riceve un errore, lo vede nel contesto, e spesso si corregge al giro dopo. Ma "spesso" non e' "sempre". A volte l'LLM entra in un loop dove ripete lo stesso errore con variazioni minime, convinto che il problema sia altrove. L'avete visto tutti, lo so.
Error propagation nei sub-agent. Un sub-agent fallisce silenziosamente. Restituisce un risultato parziale o sbagliato. Il padre lo usa come fatto acquisito e ci costruisce sopra. L'errore si propaga verso l'alto, amplificato a ogni livello. E' l'equivalente agent di un bug che passa i test unitari ma rompe il sistema in integrazione. Claude Code mitiga con i tipi di risultato e la validazione, ma il rischio e' strutturale: piu' livelli di agent, piu' superficie per errori composti.
Loop fuori controllo. L'agent decide che non ha finito. Continua a iterare. Legge file, esegue comandi, spawna sub-agent, compatta il contesto, ricomincia. Senza un limite di turni (maxTurns in Claude Code), un loop puo' girare per sempre. Con un limite, l'agent si ferma ma il lavoro potrebbe essere incompleto. Non c'e' una soluzione perfetta. C'e' il monitoraggio: quanti turni, quanti token, quante tool call. E c'e' il kill switch: voi che interrompete.
Cost explosion. Ogni turno del loop e' una chiamata API. Ogni sub-agent e' una serie di chiamate. La compaction stessa e' una chiamata. Un task ambiguo su un codebase grande puo' generare centinaia di turni, decine di sub-agent, migliaia di tool call. Il costo scala con (turni x sub-agent x tool call), non con le righe di codice prodotte. Un maxTurns di 200 con un modello grosso puo' costarvi piu' di quello che pensavate di spendere in un mese. Controllate la dashboard, gente.
Perdita di contesto dopo compaction. La compaction e' lossy. Il riassunto perde dettagli. Dopo 3-4 compaction in una sessione lunga, l'agent puo' dimenticare un vincolo stabilito 50 turni fa. "Non modificare quel file" diventa rumore nel riassunto di un riassunto. La memoria persistente (CLAUDE.md) aiuta, ma solo se l'agent ci ha scritto il vincolo. E non sempre lo fa.
La regola pratica. Un agent system e' affidabile in proporzione alla specificita' del task. "Leggi questo file e aggiungi un test per questa funzione" funziona. "Riscrivi il modulo di autenticazione" e' una scommessa. La differenza non e' nella capacita' dell'LLM. E' nella quantita' di decisioni ambigue che l'agent deve prendere senza che nessuno lo guardi.
// Cosa C'e' Di Nuovo Qui
Sezione 14. Oltre ReAct"Ok, ma un loop ReAct lo fanno tutti. Cosa porta di veramente nuovo Claude Code?" Domanda giusta. Ve lo dico io cosa c'e' che non avete visto altrove.
Tool loading lazy. Nessun framework agent carica i tool on-demand. AutoGen, CrewAI, LangChain: tutti mandano la lista completa dei tool nel system prompt a ogni turno. Se hai 100 tool MCP, sono 100 descrizioni che bruciano token. Claude Code ha il ToolSearch: i tool partono deferiti, l'LLM li scopre quando servono. E' lazy loading applicato all'intelligenza artificiale. Sembra niente, ma su sessioni lunghe vi risparmia migliaia di token a turno.
Microcompact nativa. La gestione del context window non e' fatta lato client tagliando messaggi. E' fatta lato API con strategie native: clear_tool_uses rimuove i blocchi tool_use vecchi preservando i metadati, clear_thinking toglie i blocchi di ragionamento mantenendo quelli recenti. Il server sa cosa puo' togliere senza rompere la coerenza. Il client non deve indovinare. Questo e' lo stato dell'arte nella gestione del contesto, e nessun altro framework ce l'ha perche' richiede integrazione col provider del modello.
Content replacement con prompt cache parity. Quando un tool restituisce un risultato enorme, Claude Code lo salva su disco e mette un'anteprima nel contesto. Fin qui niente di speciale. Il pezzo intelligente e' che il ContentReplacementState viene clonato ai sub-agent con le stesse decisioni di troncamento del padre. Se il padre ha messo un'anteprima per il file X, il figlio vede la stessa identica anteprima. Perche'? Per il prompt cache. Se il figlio vedesse un'anteprima diversa, il prefisso del prompt cambierebbe e la cache del modello andrebbe invalidata. Un dettaglio che vi fa risparmiare latenza e soldi su ogni singola chiamata API dei sub-agent.
Classificatore ML per i permessi. Nessun sistema agent che conosco usa un modello per decidere se un comando bash e' sicuro. Tutti usano pattern matching: se contiene rm, chiedi conferma. Claude Code ha un classificatore che capisce il contesto. rm -rf node_modules passa. rm -rf / no. chmod 777 /tmp/build passa. chmod 777 /etc/shadow no. Non e' regex. E' comprensione semantica del rischio. E se il classificatore non e' sicuro, fallback al prompt utente. Belt and suspenders.
Hook system estensibile. Pre-tool, post-tool, pre-compact, post-compact. Script shell, webhook HTTP, o prompt LLM che valutano l'input del tool. Potete agganciare qualsiasi logica esterna al ciclo di vita dell'agent senza toccare il codice. Un hook che manda un messaggio Slack ogni volta che l'agent fa un git push? Dieci righe. Un hook che blocca qualsiasi scrittura fuori dalla directory del progetto? Cinque righe. L'hook system trasforma l'agent da prodotto chiuso a piattaforma componibile.
AsyncLocalStorage per isolamento in-process. I teammate che girano nello stesso processo Node non usano variabili globali per distinguersi. Usano AsyncLocalStorage, lo stesso meccanismo che i framework web usano per il request context. Ogni agent ha il suo contesto isolato anche se condivide l'event loop con gli altri. Zero race condition sullo stato, zero mutex, zero shared memory. Concorrenza cooperativa senza i bug della concorrenza.
Fork semantics per sub-agent. Quando un sub-agent viene creato con il flag fork, eredita l'intera conversazione del padre. Non solo il prompt: tutta la history, tutti i tool_use, tutti i risultati. Parte sapendo tutto quello che sa il padre. Come un fork() Unix: il processo figlio e' una copia del padre e diverge da li'. Questo permette al sub-agent di continuare un ragionamento complesso senza ripartire da zero. Nessun altro sistema che conosco fa questo.
Il punto. Ognuna di queste innovazioni risolve un problema che trovate solo quando costruite agent system in produzione, non in demo. Token che finiscono, cache che si invalida, permessi che non scalano, agent che non si parlano. Se state progettando il vostro sistema, questa e' la lista dei problemi che vi aspettano. Claude Code li ha gia' risolti. Studiate le soluzioni.
Un while(true), un LLM, e una lista di tool. Questo e' un agent.
Il resto e' ingegneria: permessi per non distruggere, memoria per non dimenticare,
sub-agent per parallelizzare, retry per sopravvivere. Non servono 512.000 righe.
Servono i pattern giusti. E la capacita' di comporli.
Signal Pirate