-----------------------------------

Acquista i software ArcGIS tramite Studio A&T srl, rivenditore autorizzato dei prodotti Esri.

I migliori software GIS, il miglior supporto tecnico!

I migliori software GIS, il miglior supporto tecnico!
Azienda operante nel settore GIS dal 2001, specializzata nell’utilizzo della tecnologia ArcGIS e aderente ai programmi Esri Italia Business Network ed Esri Partner Network

-----------------------------------



Visualizzazione post con etichetta AI Agents. Mostra tutti i post
Visualizzazione post con etichetta AI Agents. Mostra tutti i post

lunedì 2 novembre 2026

NicoGIS Tech Weekly — .NET 11 Runtime Async, GeoAI Proximity, Agent Skills e Microsoft Copilot Harness

Settimana di riferimento: 26 ottobre – 1 novembre 2026. Le novità datate di questo numero sono l'arrivo di GPT-6.1 Terra (26 ottobre) e l'annuncio Microsoft del Copilot harness in Foundry (28 ottobre). Gli approfondimenti su .NET 11 e ArcGIS riprendono documentazione tecnica pubblicata nelle settimane precedenti: non vengono presentati come rilasci dell'ultima settimana.

.NET 11 RC2: quando il confine di un Task non coincide più con un'allocazione

Il documento che vale la lettura integrale resta Performance Improvements in .NET 11, pubblicato da Stephen Toub il 15 settembre 2026. Non è un catalogo di percentuali da trasferire alle applicazioni: mostra come piccoli cambiamenti nella forma dell'IL, nella politica del JIT, nei percorsi del GC e nell'I/O compongano il costo effettivo di un servizio. Al 2 novembre 2026 .NET 11 è ancora nella fase RC2; la disponibilità generale è attesa nel mese di novembre, non va presunta già avvenuta. Fonti: Microsoft Learn, panoramica .NET 11, release notes del runtime.

Runtime Async: cambia il lowering, non il contratto pubblico

Nel lowering classico, Roslyn trasforma ogni metodo async in un entry point e una macchina a stati, con un MoveNext che salva i locali sopravvissuti ai punti di sospensione, registra le continuation e trasferisce il risultato a un AsyncTaskMethodBuilder<T>. Il JIT riceve questa rappresentazione già trasformata. Con Runtime Async V2 il compilatore può invece emettere un IL più compatto, marcato come asincrono, contenente primitive di sospensione riconosciute dal runtime. Il JIT decide la rappresentazione della continuation sulla base della liveness effettiva e può evitare di materializzare alcuni Task intermedi nei collegamenti diretti call → await.

È utile pensare a due identità dello stesso metodo: l'entry point pubblico che continua a rispettare il ritorno Task<T>, e la variante interna AsyncCall, che consente al chiamante runtime-async di ricevere direttamente il risultato o una sospensione. Il contratto di Task, eccezioni, cancellazione e ConfigureAwait rimane valido; l'ottimizzazione elimina confini intermedi non osservabili, non l'astrazione asincrona. Una Task conservata in una collezione, restituita a un consumer esterno o usata per una continuation esplicita può richiedere ancora materializzazione. Fonte primaria: spiegazione del lowering, MethodDesc e AsyncCall.

Compiler lowering classico
 A() -> Task[A] -> MoveNext(A) -> Task[B] -> MoveNext(B) -> Task[C]

Runtime Async (caso ottimizzabile)
 A() -> Task osservabile all'esterno
             |
        AsyncCall(B) -> AsyncCall(C) -> risultato / continuation

Obiettivo: togliere materializzazioni intermedie, NON eliminare ogni Task.

L'opt-in applicativo documentato resta esplicito e limitato a progetti net11.0:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net11.0</TargetFramework>
    <Features>$(Features);runtime-async=on</Features>
  </PropertyGroup>
</Project>

Le librerie del framework sono già in larga parte compilate con Runtime Async. Non occorre attivare LangVersion=preview o EnablePreviewFeatures solo per questa opzione. La documentazione indica che async void, gli iteratori asincroni e i task-like personalizzati con builder proprietari continuano a usare il lowering tradizionale. Se si disabilita, il percorso supportato è una proprietà di progetto (UseRuntimeAsync=false), non i vecchi interruttori d'ambiente rimossi. Fonti: blog sulle performance, Microsoft Learn, Runtime Async.

I benchmark pubblicati da Microsoft sono un buon esempio di lettura corretta dei numeri: nella specifica catena di due livelli mostrata nell'articolo, il caso con completamento sincrono passa da 21,221 ns / 144 B a 6,151 ns / 0 B; con Task.Yield() passa da 254,139 ns / 248 B a 116,927 ns / 168 B. Sono risultati del microbenchmark Microsoft, non un incremento stimabile per qualsiasi API ASP.NET Core, transazione SQL o chiamata remota. La differenza dipende da profondità dei livelli async, tasso di completamento sincrono, generazione JIT, CPU e combinazione delle librerie. Fonte e codice del benchmark.

Analisi architetturale. Le catene di middleware e adapter I/O diventano un candidato più interessante del semplice metodo business: autenticazione, tracing, retry, trasporto e serializzazione spesso si compongono in più wrapper asincroni. Proprio quei wrapper, prima potenzialmente costosi in oggetti intermedi, sono il territorio dell'ottimizzazione. La scelta Task versus ValueTask resta però una decisione di API design: ValueTask<T> ha vincoli di consumo e può essere più grande da trasportare; non è un sostituto sistematico di Task<T>.

JIT, hardware e deployment: una breaking change meno appariscente

C'è un cambiamento operativo più importante di molti microbenchmark: la baseline x86-64 del JIT/AOT passa da v1 a v2, mentre il target ReadyToRun precompilato per Windows e Linux sale a x86-64-v3. La v2 comprende fra gli altri SSE3, SSSE3, SSE4.1, SSE4.2 e POPCNT; la v3 aggiunge tra l'altro AVX, AVX2, FMA e BMI. Il runtime può quindi non avviarsi su CPU che non rispettano la nuova baseline; su CPU supportate ma prive del target R2R ideale può aumentare la quota di codice che deve essere JIT-compilata all'avvio. La matrice Arm64 è diversa per Windows, Linux e Apple: non va generalizzata. Fonte: requisiti hardware .NET 11.

Valutazione. Nelle infrastrutture on-premises e nelle VM con CPU feature masking, il test di compatibilità deve essere eseguito sul guest effettivo, non soltanto sulla scheda tecnica dell'host. Anche una pubblicazione Native AOT non aggira la necessità che il codice nativo sia compatibile con l'ISA disponibile. La migrazione da .NET 10 a .NET 11 non va quindi pianificata come semplice sostituzione di runtime in ambienti eterogenei.

Un secondo dettaglio sorprendentemente rilevante per i servizi è l'analyzer CA2027: identifica alcuni pattern Task.WhenAny(work, Task.Delay(timeout)) che lasciano timer inutilmente pendenti quando work finisce per primo. Il percorso raccomandato è WaitAsync, già disponibile da .NET 6. Nel seguente esempio il CancellationToken del chiamante resta distinto dal timeout locale:

// .NET 10/11: evitare un Task.Delay pendente dopo la risposta rapida.
static async Task<T> WithTimeout<T>(
    Task<T> operation,
    TimeSpan timeout,
    CancellationToken cancellationToken)
{
    return await operation.WaitAsync(timeout, cancellationToken);
}

WaitAsync termina l'attesa in modo più pulito ma non garantisce che l'operazione sottostante venga interrotta se non supporta o non riceve la cancellazione. In un servizio che apre richieste HTTP o query SQL il token deve essere propagato alle API I/O per evitare che un timeout del consumer lasci lavoro inutile sul server. Fonte: Performance Improvements in .NET 11, CA2027.

Diagnostica async: il profiling è parte della prestazione

Il nuovo stream leggero per l'async profiling usa registrazioni compatte con buffer per thread e informazioni che consentono di ricostruire lo stack logico a partire dalle continuation. L'articolo Microsoft segnala, in alcune misure, overhead sotto l'1% e dati di tracing ridotti di un ordine di grandezza; non è un SLA del profiler. Conta perché gli approcci molto verbosi basati sugli eventi per ogni transizione Task rischiano di perturbare il workload in produzione. Il problema che si risolve non è solo la velocità del codice: è osservare catene asincrone reali senza trasformare la misurazione in un secondo carico. Riferimenti: Microsoft, async profiler internals, PR dotnet/runtime #127238.

ArcGIS 3.7: prossimità spaziale e prossimità semantica non sono la stessa query

L'aggiornamento concettuale più sostanzioso di ArcGIS Pro 3.7 non è “aggiungere l'AI alla mappa”: è disporre di due diversi spazi di vicinanza. Un vicino geografico minimizza una distanza definita su geometrie e sistema di riferimento; un vicino semantico massimizza una somiglianza fra rappresentazioni apprese da un modello. Il toolset Embeddings Based Analysis introduce strumenti per generare, cercare, aggregare ed estrarre embedding, senza rendere equivalenti tali significati. Fonti: Novità ArcGIS Pro 3.7, toolset GeoAI.

Dalla feature class al vettore: schema, query, indice e licenza

Find Similar Features Using Embeddings costruisce il vettore di query come media degli embedding delle feature che intersecano i poligoni di ricerca; poi calcola cosine similarity rispetto agli embedding nello spazio di ricerca e applica una soglia. Non misura la distanza in metri tra feature né confronta direttamente i pixel dell'immagine originale: nel caso imagery lavora sulle celle dell'embedding grid. Il pannello interattivo Find Similar opera sull'estensione corrente della vista, mentre il tool di geoprocessing è dedicato alle analisi batch estese. Non è corretto confrontare il tempo del pannello con un job batch senza controllare il numero di candidati. Fonti: Funzionamento del tool batch, pannello interattivo.

Spatial retrieval                Semantic retrieval
----------------                ------------------
query geometry                   query polygon -> mean embedding
    |                                  |
spatial index / extent            embedding space
    |                                  |
ST_DWithin / buffer?              cosine(query, candidate)
    |                                  |
distance in spatial units         score without physical unit

Hybrid retrieval (analisi architetturale):
  autorizzazione + filtro territoriale -> candidati
  candidati -> similarità -> ranking -> controllo umano / validazione

Il BLOB di embedding generato dai tool ArcGIS può essere trasformato in dimensioni numeriche con Extract Embedding To Fields; non è una scelta gratuita sul piano fisico. Tabelle con centinaia di colonne allargano record e trasferimenti, possono penalizzare il query planner e rendere fragile lo schema di interoperabilità. La documentazione Esri raccomanda cautela su dataset molto grandi e segnala che il campo BLOB originale viene conservato. Il tool di ricerca batch richiede ArcGIS Pro Advanced; la generazione di image embeddings presenta ulteriori dipendenze di licenza e dei framework di deep learning. Fonti: estrazione BLOB verso campi, matrice licensing.

Analisi architetturale. Un servizio spatial-semantic per dati aziendali dovrebbe separare tre passaggi: restrizione geografica e autorizzativa dei candidati, recupero/ranking vettoriale, validazione delle regole di dominio. La geometria può fare da filtro obbligatorio o da feature di ranking, ma non è opportuno sommare banalmente metri e coseno in uno score senza una calibrazione statistica: hanno scale, distribuzioni e significati diversi. L'eventuale vettorializzazione in un database terzo richiede una politica esplicita di allineamento di model_id, versione, schema, metriche e normalization; embedding provenienti da modelli differenti non sono confrontabili per definizione.

256 dimensioni, quattro viste: cosa significa davvero interpretare un embedding

Nel post Esri del 6 ottobre 2026, le USA Geodemographic Embeddings sono descritte come quattro segmenti da 64 dimensioni: Housing & Household, Environment, American Community Survey, Age & Race Census, per 256 componenti in totale. La somma delle importanze globali assolute SHAP per segmento dà una misura utile della distribuzione dell'attribuzione del modello predittivo. Non corrisponde però né alla SHAP di un segmento trattato come gruppo né alla perdita di accuratezza eliminando quel segmento; inoltre una singola componente, ad esempio embedding_137, non diventa automaticamente “il reddito”. Fonte primaria: Esri, Interpret embeddings using AutoML and SHAP.

Per verificare il contributo incrementale di una vista occorre un esperimento diverso: ablation di un segmento con riaddestramento, oppure permutazione congiunta delle dimensioni del segmento, mantenendo una strategia di cross-validation adatta al territorio. Split casuali possono introdurre spatial leakage quando aree vicine o derivate dagli stessi dati appaiono contemporaneamente in train e test. Quest'ultima è una valutazione metodologica, non una proprietà dichiarata dal tool Esri. Un'analisi scientificamente utile esplicita metrica target, unità geografiche di split, dipendenza spaziale residua, periodo di riferimento e intervalli di confidenza.

Un'implementazione minima della similarità in C# è volutamente indipendente dallo storage; per dati reali occorre controllare provenienza e normalizzazione del vettore:

using System;

static double Cosine(ReadOnlySpan<float> a, ReadOnlySpan<float> b)
{
    if (a.Length == 0 || a.Length != b.Length)
        throw new ArgumentException("Dimensioni incompatibili.");

    double dot = 0, aa = 0, bb = 0;
    for (int i = 0; i < a.Length; i++)
    {
        if (!float.IsFinite(a[i]) || !float.IsFinite(b[i]))
            throw new ArgumentException("Embedding non finito.");
        dot += (double)a[i] * b[i];
        aa += (double)a[i] * a[i];
        bb += (double)b[i] * b[i];
    }
    if (aa == 0 || bb == 0) throw new ArgumentException("Vettore nullo.");
    return Math.Clamp(dot / Math.Sqrt(aa * bb), -1.0, 1.0);
}

La funzione è un baseline CPU esatto O(d) per coppia e non implementa un indice ANN. Il costo di recupero su milioni di candidati si governa a livello di indice e piano di query; la formula del coseno, da sola, non dà un retrieval scalabile. Ancora più importante, non permette di attribuire semantica affidabile a un embedding numerico senza il modello che lo ha prodotto.

Experience Builder Agent Skill: dalla documentazione come testo al contratto verificabile di sviluppo

Il 14 settembre 2026 Esri ha pubblicato un Agent Skill per custom widget Experience Builder, disponibile nel repository ufficiale arcgis-experience-builder-sdk-resources, percorso skills/arcgis-exb-widget-dev. La struttura non contiene solo un prompt generico: include regole sul layout delle estensioni, file (manifest.json, config.json, runtime e settings), mapping delle API, selezione di mappe/layer, accessibilità e gestione dello stato. È pensato per agenti compatibili con il formato SKILL.md. Fonte: Esri, Building Custom Experience Builder Widgets with an Agent Skill.

La proprietà importante è che gli invarianti tecnologici diventano contesto specializzato caricato durante l'implementazione: meno spazio per indovinare path, import e convenzioni, ma nessuna garanzia automatica di correttezza. Il modello resta capace di generare codice sbagliato, in particolare quando l'API si è evoluta. Il skill va quindi affiancato a build, TypeScript checks, test, review del diff e verifica delle versioni.

Un breaking change concreto: 1.21, pnpm, componenti mappa e popup

Le note ufficiali della Developer Edition 1.21 stabiliscono che, da questa versione, per installare le dipendenze serve pnpm ci, non npm ci. Per jimu-arcgis, la migrazione ai componenti <arcgis-map> e <arcgis-scene> introduce jimuMapView.mapComponent. jimuMapView.view rimane accessibile, ma i componenti web devono riferirsi a mapComponent; il vecchio accesso jimuMapView.view.popup non è più disponibile e il popup si governa tramite mapComponent.popupElement, openPopup() e closePopup(). Non si tratta di una previsione di migrazione futura: sono indicazioni e breaking changes documentati per 1.21.

// Frammento illustrativo del nuovo accesso in Experience Builder 1.21.
// jimuMapView va acquisito dal lifecycle del widget; non persisterlo in config.
const mapElement = jimuMapView.mapComponent;
mapElement.openPopup();
const popupElement = mapElement.popupElement;
// I listener reali dipendono dagli eventi documentati del popupElement.
// Registrare i relativi handler nel lifecycle del widget e rimuoverli all'unmount.

Il frammento evidenzia il cambio di surface API, non rappresenta da solo un widget compilabile: non inventa un evento popup ufficiale. La guida dello skill Esri specifica inoltre che i riferimenti JimuMapView e LayerView vanno risolti a runtime, non serializzati in configurazione, e che cambi di vista richiedono pulizia di watcher, highlight, graphic, handle e layer temporanei. Questa disciplina è critica quando l'utente alterna mappe 2D/3D o la pagina ricarica il widget. Fonti: breaking changes 1.21, reference Map/Layer View nello skill.

Valutazione. Una CI credibile per custom widget verifica almeno la configurazione di useMapWidgetIds, il montaggio/smontaggio React, il passaggio fra viste, la liberazione di reactiveUtils handle, la presenza di fallback su dati e layer non pronti, e l'assenza di loop fra DataRecordsSelectionChangeMessage e azioni che cambiano a loro volta la selezione. L'ultima categoria è rilevante perché nella 1.21 i data source possono pubblicare eventi di filtro e selezione: la stessa documentazione avverte del rischio di loop se i callback rimodificano la sorgente.

La convergenza con il mondo degli agenti è evidente: uno skill è utile quando trasferisce all'agente regole con un valore di verifica, non quando pretende che la generazione di codice sia una sostituzione del test.

28 ottobre: Microsoft porta il Copilot harness in Foundry; il confine vero è fra gestione e controllo

La novità infrastrutturale della settimana arriva dal post Microsoft del 28 ottobre 2026: il Microsoft Copilot harness in Foundry Agent Service entra in public preview, mentre la resilienza per workflow lunghi degli hosted agents viene dichiarata generalmente disponibile. Queste sono due notizie diverse: la seconda non rende automaticamente GA il nuovo harness. Fonti: Microsoft Foundry, annuncio tecnico, documentazione operativa.

Un harness integra agent loop, scelta degli strumenti, gestione di file e contesto, skill, sandbox, subagent e delega. Un prompt agent con Copilot harness resta un prompt agent invocabile mediante Responses API; Foundry gestisce runtime e sandbox. Un hosted agent resta l'alternativa per chi ha bisogno di possedere framework, middleware, dipendenze, ciclo di esecuzione e policy di processo. La documentazione segnala che l'harness richiede un progetto/regione e un modello compatibili, che l'opzione si sceglie alla creazione e non si applica retroattivamente agli agenti esistenti nella preview, e che il bring-your-own-model non è attualmente supportato dal Copilot harness. Fonte Microsoft Learn.

                 ASP.NET Core / client C# / Web app
                               |
                     Identity + Authorization
                               |
                        Foundry project
                     /                     \
  Prompt Agent + Copilot harness             Hosted Agent
  loop/sandbox gestiti da Foundry            framework/loop propri
      |             |                             |
   skills       MCP tools / approvals           checkpoint applicativo
      |             |                             |
  workspace per conversazione                  external systems
      \_____________|_____________________________/
                  Audit, policy, idempotenza, eval

Per una prova con .NET, la documentazione Microsoft identifica esplicitamente i pacchetti preview Azure.AI.Projects.Agents e Azure.AI.Extensions.OpenAI alla versione 3.0.0-beta.5 nel percorso C#. L'API di creazione agente riportata nella guida usa MicrosoftCopilotHarness() e la feature preview indicata:

dotnet add package Azure.AI.Projects.Agents --version 3.0.0-beta.5
dotnet add package Azure.AI.Extensions.OpenAI --version 3.0.0-beta.5
#pragma warning disable AAIP001
using Azure.AI.Projects.Agents;
using Azure.Identity;

var endpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")!);
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL_DEPLOYMENT")!;

var admin = new AgentAdministrationClient(
    endpoint: endpoint,
    tokenProvider: new DefaultAzureCredential());

ProjectsAgentDefinition definition = new DeclarativeAgentDefinition(model)
{
    Instructions = "Produce a structured result; request approval for side effects.",
    Harness = new MicrosoftCopilotHarness()
};

var created = admin.CreateAgentVersion(
    "architecture-review-agent",
    options: new(definition),
    foundryFeatures: "MicrosoftCopilot=V1Preview");
Console.WriteLine($"{created.Name} / {created.Version}");

Il listato adatta l'esempio C# ufficiale; presuppone progetto, modello, permessi e preview disponibili nella regione scelta. Non sostituisce la configurazione di identity per un servizio di produzione; DefaultAzureCredential va legato a identità gestite, ruoli minimi e ambiente in cui il processo gira. Non è corretto esporre chiavi di servizio nel frontend. Fonte: istruzioni e snippet ufficiale Foundry C#.

Recovery non significa exactly-once: il test che un'architettura agentica deve superare

Per gli hosted agents, Microsoft indica che Foundry può rilevare un'interruzione e riavviare un'attività con l'input originario; il codice dell'applicazione deve salvare checkpoint e risultati intermedi per poter riprendere. Se un tool scrive su un sistema esterno, bisogna prevenire una seconda scrittura quando il processo riparte. Una conversazione salvata non equivale a un checkpoint transazionale. La semantica operativa descritta è quindi più vicina al retry con responsabilità applicativa sull'idempotenza che a un exactly-once distribuito. Fonte: Foundry, long-running resilience.

Analisi architetturale. Ogni tool con side effect dovrebbe ricevere una operation_id stabile derivata dal workflow, salvare lo stato in un datastore durabile, verificare se l'operazione è già stata eseguita e registrare esito e hash canonico della richiesta. Per email, acquisti, update GIS, scritture SQL o azioni MCP questo è più importante della formulazione del prompt. operation_id non deve diventare una stringa generata casualmente a ogni retry, altrimenti non deduplica nulla. Le autorizzazioni del tool rimangono indipendenti da quelle del modello; skill e prompt sono istruzioni, non confini di sicurezza.

Azure cost accounting: sandbox, modello e tool vanno fatturati su righe distinte

Il blog Microsoft dichiara che il costo di esercizio dell'harness scatterà il 1 dicembre 2026; fino a quel momento l'utilizzo del runtime harness è gratuito ma model inference e servizi/tool separatamente fatturati continuano a costare. La tariffa annunciata per il sandbox è USD 0,0994 per vCPU-ora + USD 0,0118 per GiB-ora, con possibili differenze per regione e contratto. Un esempio puramente aritmetico — 4 vCPU e 8 GiB mantenuti contabilmente per 720 ore — produce 720 × (4×0,0994 + 8×0,0118) = USD 354,24 di sole due componenti compute/memory. Non è una previsione di fattura né implica che una sessione venga fatturata per l'intero mese: scheduling, idle e unità di consumo effettive devono essere misurate. Fonte: pricing nell'annuncio Foundry del 28 ottobre.

Per una piattaforma con modelli open-weight sul proprio hardware o su GPU Azure, il costo ha una struttura diversa: VM GPU, dischi, networking, orchestrazione, capacità inattiva e personale operativo. La serie Azure NCads H100 v5 documenta configurazioni con una o due H100 NVL da 94 GB ciascuna, non una garanzia di disponibilità in ogni regione. Un modello ipotetico da 70 miliardi di parametri in BF16 richiede circa 140 GB decimali solo per i pesi (70e9 × 2 byte); una quantizzazione ideale a quattro bit ne richiede circa 35 GB decimali, prima di metadati, attivazioni, KV cache, riserva del framework e overhead del serving. Il fit di memoria non dimostra capacità di throughput o p99. Fonte SKU Azure, vLLM, struttura della KV cache e prefix caching.

Valutazione. Un confronto managed harness contro un orchestratore .NET su GPU propria è affidabile soltanto usando cost per accepted task e non solo dollari per token o dollari per ora GPU. Vanno inclusi timeout, retry, errori di tool, costi di licenza, tempo operativo e costo del controllo di qualità. In un'architettura ibrida il modello locale può occuparsi di classificazione e filtraggio a basso rischio, lasciando al modello hosted i task che giustificano il costo; la scelta va governata da eval su dati di dominio, non dal numero di parametri.

GPT-6.1 Terra: il costo per task comincia a contare più del prezzo per token

OpenAI ha documentato il rilascio di GPT-6.1 Terra il 26 ottobre 2026; l'identificatore API è gpt-6.1-terra. La comunicazione tecnica evidenzia attività con computer use e visual reasoning, insieme a una riduzione del prezzo rispetto ai modelli di confronto precedenti. I prezzi API standard annunciati sono USD 1 per milione di input token, USD 0,05 per milione di input cached, USD 5 per milione di output. Fonti: OpenAI, introduzione del modello, Model Release Notes, 26 ottobre.

Nel benchmark Terminal-Bench 4.0 riportato dal produttore, Terra risolve il 50,3% dei task al massimo effort, contro il 21,5% pubblicato per GPT-5.6 Terra. Il produttore riporta anche un miglioramento sul proprio set interno di prompt difficili. Sono metriche dichiarate dal fornitore e non un test indipendente sul software dell'organizzazione: non autorizzano inferenze sull'affidabilità di uno specifico agente che accede a CRM, GIS o DB. Fonte primaria: OpenAI.

Model routing ed economics: un esempio di costo che non confonde cache hit e qualità

Per un task ipotetico con 20.000 token input, di cui 16.000 effettivamente fatturati come cached, e 2.000 output, la sola componente token ai prezzi standard Terra vale:

input non cached:    4.000 × $1,00 / 1.000.000 = $0,0040
input cached:       16.000 × $0,05 / 1.000.000 = $0,0008
output:              2.000 × $5,00 / 1.000.000 = $0,0100
                                     totale = $0,0148

Senza input cached, lo stesso conteggio teorico costerebbe $0,0300. Ma questo esempio presuppone che 16.000 token risultino davvero contabilizzati come cached: un prefisso simile non garantisce una cache hit, e non si possono trasferire acriticamente al modello hosted le regole del cache_salt documentate per vLLM. Il costo osservato deve provenire dai campi usage/billing reali e includere eventuali prezzi aggiuntivi dei tool. La qualità deve essere misurata con una metrica distinta:

C_accepted = (C_model + C_tools + C_retry + C_review) / N_task_accettati

Goodput_agent = N_task_accettati / tempo_totale
SLO = p95/p99 end-to-end, non il solo TTFT del modello.

Analisi architetturale. Un router di modelli ha almeno tre obiettivi in tensione: costo, tempo di completamento e probabilità di accettazione del risultato. Per richieste con effetti esterni servono anche un policy gate indipendente dal modello e il controllo umano dove previsto. Un modello più economico che fallisce spesso e richiede molte ripetizioni può essere più costoso di un modello superiore al primo tentativo. La comparazione va eseguita con lo stesso harness, stessi tool, stessa policy, stesso dataset e distribuzione identica delle difficoltà, altrimenti si attribuisce al modello ciò che è merito dell'orchestrazione.

Technical Takeaways

  1. Runtime Async non è un nuovo pattern C#: è una differente divisione del lavoro fra compilatore, runtime e JIT. Misurare le allocazioni e i percorsi di sospensione nel codice reale, senza trattare i microbenchmark come percentuali trasferibili.
  2. Il vero breaking change .NET può essere una CPU instruction baseline. Fare inventory dei guest e dei target di pubblicazione prima di modificare il runtime, specialmente in host virtualizzati o distribuiti.
  3. Similarity semantica ≠ prossimità geografica. Inserire esplicitamente confini di autorizzazione, filtro territoriale, versione degli embedding e validazione spatial-aware.
  4. Le Agent Skills trasferiscono convenzioni, non garanzie. In Experience Builder 1.21 le migrazioni a mapComponent e pnpm devono entrare nella matrice automatica di compatibilità.
  5. Managed harness e hosted agent hanno confini operativi differenti. Il recovery non sostituisce checkpoint, deduplicazione e controlli sui tool; in preview verificare regioni, modelli, SDK e assenza di SLA.
  6. Il KPI economico per gli agenti è il task accettato. Token price, cache discount e costo GPU sono solo componenti: tool, retry, audit e review determinano l'economia complessiva.

Lab della settimana

Lab 1 — Runtime Async A/B: due lowering nello stesso processo

Obiettivo. Misurare differenze di latenza e allocazioni fra compiler-lowered async e Runtime Async nello stesso processo .NET 11 RC2, evitando che due VM, due versioni di runtime o due scheduler interferiscano nel confronto. Il metodo segue l'esperimento pubblicato da Stephen Toub, che usa un attributo riconosciuto dal compilatore per forzare il lowering classico soltanto sui metodi di controllo. Nota: l'attributo impiegato da Toub è dichiaratamente sperimentale e non rappresenta un'API pubblica della BCL.

dotnet new console -n AsyncProbe -f net11.0
cd AsyncProbe
dotnet add package BenchmarkDotNet

Nel file .csproj aggiungere l'opt-in, mantenendo tutte le proprietà e il PackageReference esistente:

<PropertyGroup>
  <Features>$(Features);runtime-async=on</Features>
</PropertyGroup>
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<AsyncProbe>();

[MemoryDiagnoser]
public class AsyncProbe
{
    private static readonly Task<int> Completed = Task.FromResult(42);

    [Benchmark(Baseline = true)]
    public Task<int> ClassicSync() => ClassicA();

    [Benchmark]
    public Task<int> RuntimeSync() => RuntimeA();

    [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)]
    private static async Task<int> ClassicA() => await ClassicB();

    [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)]
    private static async Task<int> ClassicB() => await Completed;

    private static async Task<int> RuntimeA() => await RuntimeB();
    private static async Task<int> RuntimeB() => await Completed;
}

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method)]
    internal sealed class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync)
        : Attribute
    {
        public bool RuntimeAsync => runtimeAsync;
    }
}
dotnet run -c Release -- --filter '*'

Il progetto usa intenzionalmente una superficie sperimentale del compilatore; se una SDK successiva ne modifica il contratto, utilizzare in alternativa due progetti identici che differiscono solo nella feature e verificare in IL il lowering. Ripetere con una seconda coppia di metodi che fa await Task.Yield() per distinguere completion sincrona e sospensione reale, come nell'articolo originale. Conservare dotnet --info, log BDN, parametri CPU, dimensioni assembly e numero di allocazioni. In un servizio reale la riduzione delle allocazioni va inoltre correlata a Gen0/Gen1, throughput e tail latency sotto carico, non soltanto al Mean del microbenchmark.

Lab 2 — Spatial versus semantic proximity con ArcGIS Pro 3.7

Obiettivo. Usare un dataset con embedding generati e due poligoni di query per confrontare un buffer geografico e una soglia cosine. In ArcGIS Pro 3.7 Advanced, usare il pannello interattivo per determinare la query e poi il batch Find Similar Features Using Embeddings su un'estensione più ampia. Il seguente comando ArcPy usa percorsi da sostituire con dati già disponibili; non genera embedding da zero:

import arcpy

embedding_features = r"C:\GIS\Lab.gdb\location_embeddings"
query_polygons    = r"C:\GIS\Lab.gdb\query_areas"
output_features   = r"C:\GIS\Lab.gdb\semantic_matches"

arcpy.geoai.FindSimilarFeaturesUsingEmbeddings(
    embedding_features,
    query_polygons,
    output_features,
    0.80
)

Confrontare i risultati con un filtro di distanza geodetica appropriato al sistema di coordinate e riportare precision@k, recall@k, overlap@k, oltre alla distanza geografica dei match. Il valore 0.80 è una soglia di laboratorio, non una soglia raccomandata universalmente: deve essere calibrata sul modello e sul dataset. Per i geodemographic embedding aggiungere un secondo esperimento con split territoriali a blocchi, ripetendo il training con e senza il segmento da 64 dimensioni più importante secondo SHAP. Documentazione: Funzionamento del batch, licenze, interpretazione SHAP dei segmenti.

Lab 3 — Agent Skills e harness: test di failure, non demo di prompting

Obiettivo. Preparare una suite A/B con stesso task, stesso modello, stessi strumenti, variando solo disponibilità dello skill Experience Builder. Il task proposto al lettore è un custom widget che reagisce alla selezione di feature e si ripulisce correttamente quando cambia la MapView. Richiedere l'implementazione compatibile con Developer Edition 1.21, eseguire pnpm ci e la build prevista dal progetto, quindi valutare: errori TypeScript, accessi residui a view.popup, riferimenti a viste non rilasciati, loop di selezione/filtro e diff non intenzionali. Fonte: skill ufficiale Esri, breaking changes 1.21.

Per chi dispone della preview Foundry in una regione supportata, affiancare un test di recovery degli hosted agents che simuli un riavvio dopo una scrittura su un servizio fittizio: il contatore degli effetti collaterali deve restare 1 dopo il retry, grazie a una operation_id persistente. Separare i risultati dello skill (qualità del codice) da quelli del harness (recovery e orchestrazione) e registrare il costo per task accettato. Documentazione: Microsoft Copilot harness, prerequisiti e C#, resilienza dei hosted agents.

venerdì 30 ottobre 2026

LINQPad 9 — Beyond LINQ - Runtime Internals, C# 15 Preview, SQL Server 2025 Vector Search, AI Agents, MCP & Advanced .NET Engineering

Un laboratorio per software architect: dalle expression trees al SQL generato, dal toolchain preview agli agenti con strumenti, fino a performance, sicurezza e risultati verificabili.

Executive summary: LINQPad come runtime di sperimentazione

LINQPad 9 unifica le codebase Windows e macOS e introduce un agente AI integrato. Il valore architetturale non consiste nel sostituire una IDE, ma nel ridurre il costo di una verifica tecnica: una query SQL, una pipeline LINQ, un parser, una API, un esperimento con un assembly o un modello linguistico diventano script eseguibili e osservabili.

Questo articolo separa sistematicamente tre piani: funzionalità documentate nelle note di rilascio fornite, laboratori C# riproducibili e considerazioni ingegneristiche da validare nel proprio ambiente. Non sono presentati benchmark numerici non misurati.

1. Architettura: script host, compilazione, runtime e confini

Uno script LINQPad vive in un contesto che risolve riferimenti, package NuGet, namespace, connessioni e modalità di esecuzione. La compilazione è distinta dal provider di accesso ai dati: il compilatore verifica C#, mentre il driver e il provider EF Core o LINQ-to-SQL decidono come una expression tree diventa SQL.

Script .linq / file .cs
↓
Compiler + references + source generators
↓
CLR / .NET runtime selezionato
↓
Query providers · HTTP · NuGet · external assemblies
↓
Dump / DataGrid / Charts / diagnostics

Non bisogna confondere un processo di scripting con un application host completo: lifecycle, dependency injection, secret management, osservabilità, deployment e isolamento richiedono scelte esplicite. Gli script che mutano sistemi esterni hanno effetti reali e devono essere trattati come codice operativo.

Le note 9.8 descrivono un host CLR C++ personalizzato per la selezione della versione .NET; le note principali descrivono una DataGrid riscritta con feed IPC ad alte prestazioni. Queste sono proprietà del prodotto, non garanzie di isolamento di sicurezza.

2. C# 15 preview: come verificare il toolchain effettivo

Le note 9.8 indicano: installare .NET 10 e .NET 11 Preview, attivare entrambe le opzioni preview in Settings → Scripts e utilizzare i sample “What’s new in C# 15”. Gli esempi includono union types, closed hierarchies, collection expression arguments e memory safety; la sintassi preview può cambiare e non va copiata senza verificare il compilatore.

using System;
using System.Runtime.InteropServices;

new
{
    Framework = RuntimeInformation.FrameworkDescription,
    Runtime = Environment.Version.ToString(),
    OS = RuntimeInformation.OSDescription,
    Architecture = RuntimeInformation.ProcessArchitecture.ToString()
}.Dump();

Il runtime effettivo non identifica da solo la versione del linguaggio. La versione del compilatore, la configurazione preview e il target framework vanno controllati separatamente. Per test di regressione, fissare esplicitamente le versioni e conservare lo script insieme ai risultati.

3. IEnumerable vs IQueryable: il punto in cui cambia l’esecuzione

IEnumerable<T> rappresenta un flusso di elementi elaborato con delegate .NET; IQueryable<T> conserva un albero di espressioni che un provider può tradurre. Il confine fra i due determina dove vengono eseguiti filtri, proiezioni e aggregazioni. AsEnumerable() non materializza da solo, ma sposta le operazioni successive verso LINQ-to-Objects.

var source = Enumerable.Range(1, 1_000_000);
var query = source.Where(x => x % 7 == 0).Select(x => x * 2);

// Deferred execution: la pipeline non è stata ancora enumerata.
var first = query.Take(5).ToArray();
first.Dump("Prime cinque righe");

// Per misurazioni attendibili: warm-up, più iterazioni,
// evitare Dump all'interno della regione misurata.

Per EF Core, verificare il SQL generato tramite ToQueryString() quando disponibile e confrontare il piano di esecuzione reale nel database. Una query apparentemente elegante può introdurre cardinalità elevate, round trip multipli, proiezioni eccessive o client-side processing.

4. SQL Server 2025: JSON, VECTOR e semantica dei provider

La versione 9.10 dichiara il mapping dei tipi SQL Server 2025 JSON e VECTOR nei driver LINQ-to-SQL ed EF Core. Le funzioni SQL sono esposte tramite SqlMethods nel driver LINQ-to-SQL e EF.Functions con EF Core 11+. Questo non significa che tutte le funzioni siano disponibili su ogni versione di EF Core o SQL Server.

-- Laboratorio SQL Server 2025: eseguire in SQL mode
-- su un database di test con supporto VECTOR abilitato.
SELECT TOP (10)
    Id,
    VECTOR_DISTANCE('cosine', Embedding, @queryVector) AS Distance
FROM dbo.Documents
ORDER BY Distance ASC;

La similarità vettoriale non equivale alla correttezza semantica. Occorre valutare normalizzazione degli embedding, scelta della metrica, dimensione dei vettori, filtri relazionali, cardinalità, piani SQL, costo CPU e qualità del retrieval con un dataset di valutazione. LINQPad è utile per ispezionare query e risultati, non sostituisce un benchmark ripetibile.

5. Operatori LINQ-to-SQL 9.10: traduzione e rischi

Le note 9.10 elencano LeftJoin, RightJoin, FullJoin (.NET 11), CountBy, MinBy, MaxBy, DistinctBy, Shuffle e altri operatori. string.Join su gruppi può essere tradotto in STRING_AGG. Queste sono capacità del driver LINQ-to-SQL LINQPad, non promesse generalizzabili a qualsiasi provider IQueryable.

// Query da eseguire con una connessione LINQ-to-SQL LINQPad
// che esponga la tabella Products.
var report = Products
    .GroupBy(p => p.Category)
    .Select(g => new
    {
        Category = g.Key,
        Names = string.Join(", ", g.Select(p => p.Name))
    });
report.Dump();

Controllare ordinamento, gestione dei null, collation, tipi numerici e traduzione del provider. Il fatto che una query compili non implica che sia stata eseguita sul server né che produca un piano efficiente.

6. DML set-based: ExecuteUpdate e ExecuteDelete

LINQPad 9.10 aggiunge ExecuteUpdate e ExecuteDelete al proprio driver LINQ-to-SQL. Operano senza caricare entità e senza passare per un change tracker. È un vantaggio per batch ampi, ma elimina anche eventuali controlli applicativi che normalmente avvengono nel ciclo di vita dell’entità.

// SOLO su database di test e dentro una transazione controllata.
// Esempi documentati dal driver LINQ-to-SQL di LINQPad:
Products.Where(p => p.Discontinued).ExecuteDelete();

Products.Where(p => p.Stock == 0)
    .ExecuteUpdate(s => s.SetProperty(
        p => p.Price, p => p.Price * 0.9m));

La 9.10 introduce anche TagWith e TagWithCallSite per correlare il LINQ al SQL generato. I tag migliorano la diagnostica, ma non sono un meccanismo di sicurezza né un sostituto della telemetria lato database.

7. PostgreSQL e scaffolding: enum, timestamptz e differenze temporali

LINQPad 9 introduce uno scaffolder PostgreSQL capace di gestire enum e timestamptz, oltre al filtro degli oggetti nello schema. È utile per database con migliaia di oggetti e per evitare conflitti fra nomi. La semantica di timestamp with time zone va distinta dalla rappresentazione di un fuso orario nominato: l’istante e l’offset originale non sono la stessa informazione.

Il confronto con SQL Server dovrebbe coprire mapping CLR, nullability, precisione temporale, collation, query parametrizzate, transazioni e comportamento del provider durante la traduzione.

8. LINQPad 9 come ambiente di test HTTP e JSON

Una query C# può invocare un endpoint REST senza scaffolding di un progetto. Questo rende LINQPad utile per isolare errori di serializzazione, autenticazione, negoziazione TLS e parsing. Occorre tuttavia imporre timeout, cancellation e gestione degli errori.

using System.Net.Http;
using System.Text.Json;

using var client = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(10)
};

using var response = await client.GetAsync(
    "https://httpbin.org/json",
    HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();

await using var stream = await response.Content.ReadAsStreamAsync();
using var json = await JsonDocument.ParseAsync(stream);
json.RootElement.Clone().Dump(true);

Questo laboratorio richiede connettività Internet. Per dati aziendali o API protette usare endpoint di test autorizzati; non stampare bearer token, segreti o risposte contenenti informazioni riservate. Dump(true) su JsonDocument offre, dalla 9.9, un visualizzatore con outlining interattivo.

9. AI Agent: dal completamento al ciclo di verifica

La versione 9 introduce un coding agent capace di ottenere contesto, applicare diff, verificare errori di compilazione e proporre correzioni. Con 9.8 sono documentati tool per interrogare assembly, decompilare, leggere file, eseguire script esplorativi e recuperare URI. La 9.9 aggiunge Claude Code integrato con oltre 20 tool personalizzati; la 9.11 permette a Claude Code e Copilot, previo consenso, di accedere ai risultati del pannello principale e delle DataGrid.

Intent + scope autorizzato
↓
Agent → tool LINQPad → codice / query
↓
Compile → run → inspect output
↓
Tests + invarianti + diff review
↓
Accettazione umana / commit

Un compilatore può dimostrare che il codice è sintatticamente e tipologicamente valido, non che il comportamento sia corretto. Anche test generati dall’agente possono essere incompleti. Occorre separare osservazione, modifica e approvazione; un agente non deve poter autoattribuirsi privilegi attraverso testo o output di tool.

Le note 9.10 aggiungono controlli di sicurezza per Mark-of-the-Web, percorsi UNC e source mapping NuGet. Le note 9.11 introducono l’auto-approvazione persistente per operazioni classificate non distruttive: una lettura di file o una fetch web può comunque comportare divulgazione di dati; valutare l’ambiente e le policy prima di abilitarla.

10. AI Runtime: Util.AI.Ask e separazione fra API e provider

LINQPad 9.6 introduce una API per richiamare direttamente dagli script i modelli configurati nell’interfaccia. La documentazione conferma selezione del modello, streaming e cronologia, oltre agli esempi minimi seguenti.

// Richiede un provider AI configurato in LINQPad.
Util.AI.Ask("Explain special relativity").Dump();

var response = await Util.AI.Ask(
    "Explain why IQueryable is not IEnumerable").GetResponseAsync();
response.Text.Dump();

L’API è utile per prototipare classificazione, estrazione, RAG e agent workflows. Per un’applicazione di produzione è però preferibile separare il client LLM, le policy di retry, il controllo costi, la validazione JSON/schema e la telemetria dalla UI di sviluppo. La 9.0 dichiara supporto a OpenAI, Anthropic, Azure OpenAI, Azure Foundry, OpenRouter, GitHub Copilot ed endpoint compatibili; l’effettiva disponibilità dipende dalla configurazione e dalle credenziali.

11. MCP e reverse engineering controllato

Le note 9.9 indicano che Claude Code può essere personalizzato con istruzioni, skill e server MCP. Questo consente di utilizzare LINQPad come superficie di esplorazione di SDK e assembly: interrogare tipi, metodi, attributi e comportamenti con piccoli esperimenti, quindi produrre test ripetibili. MCP è un protocollo di collegamento ai tool, non una garanzia di correttezza o autorizzazione.

using System.Reflection;

var t = typeof(HttpClient);
var api = t.GetMethods(BindingFlags.Public | BindingFlags.Instance)
    .Where(m => m.Name.Contains("Send", StringComparison.Ordinal))
    .Select(m => new
    {
        m.Name,
        ReturnType = m.ReturnType.FullName,
        Parameters = string.Join(", ", m.GetParameters()
            .Select(p => $"{p.ParameterType.Name} {p.Name}"))
    });
api.Dump();

Per assembly esterni, il reverse engineering deve rispettare licenze, confidenzialità e perimetro autorizzato. Il modello non deve essere considerato una fonte autorevole per le firme delle API: verificarle contro metadata, IntelliSense, sorgente e test compilati.

12. Source generators e file-based apps

La 9.6 dichiara il supporto ai source generator .NET integrati, tra cui quelli per regex, JSON e interop. La versione principale supporta le direttive #:project e #:package e file-based apps C# 14, salvabili come .cs e avviabili anche con dotnet run.

#:package Humanizer.Core@*
using Humanizer;
Console.WriteLine(1234567.ToWords());

Per evitare accoppiamenti non necessari, separare il nucleo di calcolo in metodi o librerie .NET standard e lasciare .Dump() soltanto nel layer di ispezione. Il simbolo di compilazione LINQPAD permette percorsi diversi per l’esecuzione nell’host o nella CLI.

13. Visualizzazione: Dump, ECharts, HTML controls e Dataflow

LINQPad 9.7 introduce EChart e un rendering moderno tramite Apache ECharts; in Windows Util.Chart(...).DumpInline() oppure .ToEChart().Dump() selezionano il nuovo backend documentato. La versione include FlexBox, TabControl, MarkdownViewer e LatexViewer. La 9.6 aggiunge Util.Markdown.

Util.Markdown("# Report\n\n**Status:** OK").Dump();
Util.Latex(@"\int_0^\infty e^{-x^2} dx").Dump();

new [] {
    new { Stage = "Parse", Milliseconds = 12 },
    new { Stage = "Validate", Milliseconds = 8 },
    new { Stage = "Persist", Milliseconds = 23 }
}.Dump();

I grafici sono un mezzo per capire una pipeline, non una prova di performance. Separare la raccolta dei dati dal rendering; in particolare, misurare l’algoritmo senza Dump, serializzazione, UI e trasferimento IPC.

14. Benchmarking: metodologia per risultati difendibili

Per un confronto serio occorrono almeno: warm-up, numero sufficiente di iterazioni, controllo del garbage collector, stessa architettura CPU, stessa build .NET, stesso dataset, stesse condizioni del database, misurazioni P50/P95/P99 e intervalli di confidenza quando applicabili. Per microbenchmark del codice .NET, BenchmarkDotNet è normalmente più adatto di un singolo Stopwatch; LINQPad resta eccellente per esplorare il fenomeno.

using System.Diagnostics;

static long SumSquares(int n)
{
    long sum = 0;
    for (int i = 0; i < n; i++) sum += (long)i * i;
    return sum;
}

for (int i = 0; i < 100; i++) _ = SumSquares(100_000); // warm-up
var samples = new List<double>();
for (int i = 0; i < 50; i++)
{
    var sw = Stopwatch.StartNew();
    _ = SumSquares(100_000);
    sw.Stop();
    samples.Add(sw.Elapsed.TotalMilliseconds);
}
samples.Sort();
new { MedianMs = samples[samples.Count / 2],
      P95Ms = samples[(int)Math.Ceiling(samples.Count * .95) - 1] }.Dump();

15. Security engineering: trust boundaries, segreti e supply chain

La versione 9 memorizza i segreti nel Password Manager integrato, basato su Windows DPAPI o macOS Keychain. La 9.10 introduce screening di file provenienti da Internet, percorsi UNC e regole opzionali di source mapping per NuGet; la 9.11 avvisa prima di aprire link con schemi non riconosciuti. Sono mitigazioni importanti, ma non sostituiscono il principio del minimo privilegio.

SuperficieRischioControllo
SQL scriptsMutazioni non intenzionaliAccount read-only, transazioni e ambienti di test
AI tool callsEsfiltrazione, modifiche non approvatePermission gating, review, audit
NuGetSupply-chain compromiseVersion pinning, feed policy, source mapping
Dump / DataGridEsposizione dati sensibiliMascheramento, dataset sintetici
File esterniScript non attendibiliMark-of-the-Web, UNC screening, sandbox operativo

Un database connection string con privilegi amministrativi, uno script con DML e un agente con accesso al file system costituiscono un insieme ad alto rischio. Applicare controlli fuori dal prompt: credenziali limitate, segregazione degli ambienti, approvazione delle operazioni e logging indipendente.

16. LINQPad vs Visual Studio: complementarità, non sostituzione

LINQPad eccelle in sperimentazione, introspezione, query ad hoc, test di API, piccoli parser e analisi di output. Visual Studio e toolchain CI/CD restano centrali per grandi codebase, debugging distribuito, packaging, test suite strutturate, profiling, code review e deployment. La strategia più robusta è: esplorare in LINQPad → formalizzare in libreria/test → validare in CI.

Un prototipo che funziona in un host interattivo non dimostra che funzionerà sotto carico, in un servizio Windows, in un container, con un diverso account o dietro un reverse proxy. Portare in produzione richiede gestione delle dipendenze, configurazione, cancellazione, timeout, osservabilità e gestione degli errori.

17. Roadmap delle release 9.5–9.11

ReleaseInnovazioni documentate
9.5 / 9.4SQL-to-LINQ con AI, SQL completion, schema-aware agent
9.6Util.AI.Ask, source generators, Util.Markdown
9.7GitHub Copilot provider, ECharts, nuovi HTML controls
9.8Tool agentici, .NET 11/C# 15 preview, CLR host
9.9Claude Code, strumenti su assembly e progetti esterni, JSON outlining
9.10SQL Server 2025 JSON/VECTOR, LINQ-to-SQL avanzato, sicurezza file/NuGet
9.11Self-contained scripts, AI access ai risultati con consenso, nuovi controlli sicurezza

La disponibilità di singole funzioni può dipendere da licenza, piattaforma, runtime, provider e impostazioni. Verificare sempre la build installata e la pagina delle release note del prodotto.

18. Conclusioni: un laboratorio tecnico che produce evidenze

Il contributo più importante di LINQPad 9 è l’unione di esecuzione rapida, introspezione, accesso dati, visualizzazione e assistenza AI in un singolo ciclo di lavoro. Il valore reale emerge quando ogni ipotesi viene trasformata in un esperimento ripetibile e ogni risultato in un’evidenza verificabile.

Per un senior engineer, la regola resta invariata: non fidarsi di una query perché compila, di un agente perché risponde, o di un benchmark perché produce un numero. Verificare semantica, autorizzazioni, piani di esecuzione, condizioni di misura e limiti del toolchain.

Fonti e note metodologiche

  1. LINQPad 9 — What’s New, file HTML di release note fornito per la redazione dell’articolo (versioni 9.4–9.11).
  2. LINQPad — sito ufficiale.
  3. Microsoft Learn — C#.
  4. Microsoft Learn — Entity Framework Core.
  5. Microsoft Learn — SQL Server.
  6. Model Context Protocol — specifica.

Nota editoriale: le descrizioni delle funzionalità LINQPad sono basate sulle release note fornite. I laboratori C# sono esempi didattici; non è stata eseguita una sessione LINQPad 9 nel presente ambiente e non vengono dichiarati test di esecuzione o benchmark verificati. Le osservazioni architetturali sono analisi tecniche, non dichiarazioni del produttore.

venerdì 23 ottobre 2026

Cache Engineering for AI Agents

NICOGIS TECH WEEKLY · DEEP DIVE · 23 OTTOBRE 2026

Cache Engineering for AI Agents: KV Cache, Position Encoding, Prefill Optimization e Inference Serving

Un agente che esegue centinaia di tool call non consuma risorse soltanto quando genera token. Ogni osservazione allunga la traiettoria, può aumentare lo stato di attenzione da conservare e rende più costoso il successivo prefill. Il problema diventa: quale stato computazionale conservare, riutilizzare, eliminare o riposizionare senza compromettere la correttezza?

Questo deep dive parte dalla matematica dei transformer, analizza il paper Cache Engineering for Agents e arriva a un laboratorio riproducibile con vLLM e C#/.NET 10. Distingueremo rigorosamente il prefix caching già disponibile nei server di inferenza dalle operazioni sperimentali drop e repos proposte da AKV.

1. La traiettoria di un agente è diversa da una chat

In una chat tradizionale il contesto aumenta con messaggi relativamente brevi. In un agente, una singola tool response può introdurre migliaia di token: log, file sorgente, risultati di ricerca, JSON, documenti. Un ciclo LLM → tool → observation → LLM ripetuto decine o centinaia di volte amplifica il problema.

System + task + tools
        |
        v
     Agent LLM ----> Tool A ----> Observation A
        |                            |
        +----------------------------+
        |
        +-------------> Tool B ----> Observation B
        |                            |
        +----------------------------+
        |
        v
   Next prefill / decode

È necessario separare quattro grandezze: token della cronologia logica, token realmente elaborati durante il prefill, byte di KV cache allocati in GPU e byte trasferiti fra GPU, CPU e storage. Una API commerciale può fatturare token cached senza esporre la KV cache fisica. Un inference server self-hosted può riutilizzare blocchi KV senza cambiare il testo del prompt.

2. KV cache: matematica e dimensionamento

In un transformer autoregressivo, per ogni layer si calcolano le proiezioni Q = XWq, K = XWk e V = XWv. La self-attention causale è, schematicamente, softmax(QKᵀ / sqrt(d) + mask) V. Durante il decode i token precedenti non cambiano: ricalcolarne ogni volta key e value sarebbe uno spreco. La KV cache conserva queste rappresentazioni per riutilizzarle.

Per L layer, Hkv KV head, dimensione D per head, N token e B byte per elemento, una stima è:

M_KV ≈ 2 × L × Hkv × D × N × B

Il fattore due rappresenta K e V. Consideriamo 32 layer, 8 KV head, dimensione 128, BF16 (2 byte), 32.768 token:

2 × 32 × 8 × 128 × 32768 × 2
= 4,294,967,296 byte ≈ 4 GiB per sequenza

Otto sequenze indipendenti di quella lunghezza richiederebbero circa 32 GiB di sola KV cache, prima di pesi del modello, buffer, frammentazione e altre allocazioni. GQA e MQA riducono il numero di KV head; la quantizzazione KV riduce i byte per elemento ma introduce vincoli di precisione e compatibilità. La dimensione massima di contesto pubblicizzata dal modello non coincide con la capacità di servire contemporaneamente molte richieste a quella lunghezza.

3. Prefill e decode: due workload differenti

FaseAttivitàCollo di bottiglia tipicoMetriche
PrefillElabora il prompt e costruisce KVCompute e lunghezza inputPrefill latency, TTFT
DecodeGenera token incrementaliBandwidth, letture KV, batchingITL, output token/s
SchedulerDistribuisce risorse fra richiesteMemoria, queueing, preemptionP95/P99, queue time

Il Time to First Token osservato dal client include rete, eventuale coda, prefill e prima emissione. Ridurre il prefill può migliorare il TTFT senza incidere proporzionalmente sulla durata totale di una risposta lunga. Il continuous batching permette di introdurre nuove richieste mentre altre sono in decode; il chunked prefill suddivide prompt lunghi per condividere meglio la GPU.

4. RoPE: perché eliminare token non risolve il problema delle posizioni

Le Rotary Position Embeddings (RoPE) codificano la posizione ruotando query e key: q′p = R(p)qp e k′p = R(p)kp. Per ogni coppia di coordinate, R(p) è una rotazione il cui angolo dipende dalla posizione e dalla frequenza. Il prodotto scalare fra query e key incorpora così una relazione posizionale.

Supponiamo di avere una traiettoria di 100.000 token e di eliminare dalla cache gli stati di 80.000 token intermedi. La cache attiva è più piccola, ma le posizioni dei token rimasti possono essere ancora molto elevate. Indice logico della traiettoria, numero di elementi KV attivi e indice RoPE non sono la stessa cosa.

Nel caso RoPE standard, un key già ruotato alla posizione p può essere idealmente trasportato alla posizione q applicando:

k′q = R(q) · R(p)^(-1) · k′p

È una proprietà della composizione delle rotazioni. Non implica però che un runtime generico possa spostare arbitrariamente tutti gli stati KV senza conseguenze: gli hidden state dipendono dalle informazioni che erano visibili quando sono stati calcolati, e varianti di RoPE possono avere requisiti differenti.

Errore concettuale frequente: ricollocare un key nella dimensione posizionale non equivale a ricalcolare il token con una storia di attenzione diversa.

5. Prefix caching e PagedAttention non sono sinonimi

Il prefix caching evita di ripetere il prefill quando due richieste condividono lo stesso prefisso tokenizzato. È particolarmente efficace con system prompt, istruzioni sugli strumenti e documenti statici. La somiglianza semantica non basta: serve l'identità dei token e degli altri fattori rilevanti per la cache.

Request A: [system][tools][document][question A]
Request B: [system][tools][document][question B]
            +----- shared prefix -----+
                     cached KV

In vLLM l'Automatic Prefix Caching usa hash dei blocchi che dipendono dai token e dal prefisso precedente. La gestione comprende reference count ed eviction. La documentazione descrive anche un cache_salt opzionale per isolare gruppi di fiducia, riducendo il rischio di canali laterali temporali fra tenant.

PagedAttention affronta invece l'allocazione: divide la KV cache in blocchi fisici non necessariamente contigui, mappati a una sequenza logica. Riduce sprechi di memoria e facilita la condivisione. Prefix caching è una tecnica di riutilizzo; PagedAttention è una tecnica di gestione della memoria. Possono essere combinate.

SGLang utilizza strutture radix per il riuso dei prefissi. Nei modelli ibridi, che combinano full attention, sliding-window attention e stati ricorrenti, la semantica del riuso cambia a seconda della componente: un checkpoint ricorrente non è intercambiabile con un normale blocco KV.

6. Il paper Cache Engineering for Agents: append, drop, repos

Il lavoro Cache Engineering for Agents propone di gestire la cache direttamente, senza riscrivere continuamente la traiettoria testuale dell'agente. Le operazioni fondamentali sono:

OperazioneEffettoObiettivo
append(x)Aggiunge KV dei nuovi tokenProseguire l'esecuzione
drop(P)Esclude selettivamente KV dalla futura attenzioneRidurre contesto attivo e memoria
reposModifica gli encoding posizionali dei key conservatiCompattare le posizioni senza re-prefill completo

La differenza rispetto alla summarization è decisiva. Se si sostituisce un tratto del prompt con un riassunto, il prefisso cambia e può essere necessario ricalcolare stati successivi. Se il runtime supporta drop, può escludere porzioni della cache dalle attenzioni future senza necessariamente rigenerare gli stati conservati.

Context engineering:
[A][B][C][D][E] -> summarize -> [A][S][E]
                              prefix modified / possible re-prefill

Cache engineering:
[A][B][C][D][E] -> drop KV(B,C,D) -> active [A][E]
                              retained states may be reused
                  -> repos -> adjusted position encodings

Negli esperimenti degli autori, la policy tool-response rolling-drop ottiene +1,7 punti di score medio e −27,7% di costo medio rispetto alla summarization a soglia. AKV riporta fino a 1,2× di throughput di output e fino a 1,5× di miglioramento relativo della latenza P95 nel proprio setup. Sono risultati sperimentali specifici, non prestazioni garantite di qualsiasi inference server.

7. History dependence: stessi token, cache diversa

Questo è il passaggio più sottile del paper. Immaginiamo due richieste che contengono gli stessi token A, B e C. Nel primo caso C è stato elaborato quando B era visibile; nel secondo B era già stato escluso con drop. Il vettore KV di C può essere diverso perché è diversa la storia dell'attenzione. Di conseguenza la sola identità dei token non determina più lo stato della cache.

Il serving engine AKV, costruito su SGLang, utilizza una struttura radix estesa con nodi che rappresentano le operazioni di drop, gestione dell'eviction consapevole dei riferimenti e segment-batch attention per ricostruire stati coerenti. Il riposizionamento richiede inoltre versioni dei key con encoding posizionali appropriati. È un problema di provenienza dello stato, non semplicemente di cache hit.

8. Inference serving: scheduling, eviction e offloading

TecnicaBeneficioTrade-off
Prefix cachingEvita prefill duplicatoMemoria per blocchi riutilizzabili
PagedAttentionRiduce frammentazioneGestione tabelle di blocchi
Chunked prefillMigliora coesistenza con decodeScheduling più complesso
KV quantizationMeno byte/tokenPossibili effetti numerici
CPU offloadingMaggiore capacità effettivaTrasferimenti e latenza
Disaggregated prefill/decodeWorker specializzatiTrasferimento KV fra worker
AKV drop/reposControllo del contesto attivoRuntime specializzato e semantica complessa

In un sistema disaggregato il costo di trasferimento della KV cache può diventare di primo ordine. Un risparmio nel calcolo non garantisce quindi un miglioramento end-to-end. Occorre misurare anche traffico GPU-GPU, GPU-CPU, congestione di rete, code e preemption.

9. Laboratorio: vLLM e client C#/.NET 10

Obiettivo: confrontare richieste con prefisso invariato e prefisso variabile, misurando TTFT e latenza totale. Il laboratorio verifica il prefix caching standard di vLLM, non implementa le operazioni AKV drop/repos. Per replicare AKV è necessario il repository degli autori e l'infrastruttura SGLang modificata.

9.1 Prerequisiti e server

Occorrono una GPU NVIDIA supportata, ambiente CUDA/driver compatibile, vLLM e .NET SDK 10. Il modello seguente è relativamente piccolo ma richiede comunque VRAM adeguata. Verificare le opzioni effettive con vllm serve --help, perché possono cambiare fra versioni.

vllm serve Qwen/Qwen2.5-1.5B-Instruct \
  --host 0.0.0.0 --port 8000 \
  --enable-prefix-caching --max-model-len 8192

# In un altro terminale:
dotnet new console -n KvCacheBench --framework net10.0
cd KvCacheBench

9.2 Program.cs completo, senza pacchetti esterni

using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.Json;

var endpoint = Environment.GetEnvironmentVariable("LLM_URL")
    ?? "http://localhost:8000";
var model = Environment.GetEnvironmentVariable("LLM_MODEL")
    ?? "Qwen/Qwen2.5-1.5B-Instruct";

using var http = new HttpClient {
    BaseAddress = new Uri(endpoint.TrimEnd('/') + "/"),
    Timeout = TimeSpan.FromMinutes(5)
};

// Il prefisso lungo rimane identico nei casi A e B.
var shared = string.Join("\n", Enumerable.Range(0, 100)
    .Select(i => $"Regola {i:D3}: descrivi trade-off " +
        "fra KV cache, prefill, decode e scheduling GPU."));

Console.WriteLine("case,iteration,ttft_ms,total_ms,output_chars");
foreach (var testCase in new[] { "A", "B", "C" }) {
    for (int i = 0; i < 14; i++) {
        var prefix = testCase == "C"
            ? $"Variante {i:D4}\n" + shared : shared;
        var payload = new {
            model,
            stream = true,
            temperature = 0,
            max_tokens = 120,
            messages = new object[] {
                new { role = "system", content =
                    "Rispondi in italiano, in modo tecnico e conciso." },
                new { role = "user", content = prefix +
                    $"\n\nSpiega il concetto {i:D2} di KV cache." }
            }
        };
        using var req = new HttpRequestMessage(
            HttpMethod.Post, "v1/chat/completions") {
            Content = new StringContent(JsonSerializer.Serialize(payload),
                Encoding.UTF8, "application/json")
        };
        var watch = Stopwatch.StartNew();
        using var res = await http.SendAsync(req,
            HttpCompletionOption.ResponseHeadersRead);
        res.EnsureSuccessStatusCode();
        await using var stream = await res.Content.ReadAsStreamAsync();
        using var reader = new StreamReader(stream);
        double? ttft = null;
        int chars = 0;
        while (await reader.ReadLineAsync() is { } line) {
            if (!line.StartsWith("data: ", StringComparison.Ordinal))
                continue;
            var data = line[6..];
            if (data == "[DONE]") break;
            using var doc = JsonDocument.Parse(data);
            if (!doc.RootElement.TryGetProperty("choices", out var choices)
                || choices.GetArrayLength() == 0) continue;
            var choice = choices[0];
            if (!choice.TryGetProperty("delta", out var delta)
                || !delta.TryGetProperty("content", out var content)
                || content.ValueKind != JsonValueKind.String) continue;
            var fragment = content.GetString();
            if (string.IsNullOrEmpty(fragment)) continue;
            ttft ??= watch.Elapsed.TotalMilliseconds;
            chars += fragment.Length;
        }
        watch.Stop();
        if (i >= 2) Console.WriteLine(string.Join(",",
            testCase, (i - 2).ToString(),
            ttft?.ToString("F2", CultureInfo.InvariantCulture) ?? "",
            watch.Elapsed.TotalMilliseconds.ToString("F2",
                CultureInfo.InvariantCulture), chars.ToString()));
    }
}

Il programma esegue 2 richieste di warm-up e 12 richieste misurate per ciascun caso. A e B condividono il prefisso; C lo altera prima del blocco comune. Le richieste sono sequenziali per isolare il riuso, quindi non costituiscono un test di concorrenza. L'ordine A-B-C può favorire il warm-up della cache: ripetere i test invertendo l'ordine e riavviando il server fra configurazioni.

9.3 Esecuzione e protocollo

# Linux / macOS
export LLM_URL=http://localhost:8000
export LLM_MODEL=Qwen/Qwen2.5-1.5B-Instruct
dotnet run -c Release > results.csv

# PowerShell
$env:LLM_URL = "http://localhost:8000"
$env:LLM_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
dotnet run -c Release | Out-File results.csv

Ripetere con prefix caching abilitato e disabilitato, mantenendo identici modello, precisione, GPU e limiti di contesto. Eseguire più repliche indipendenti, annotare la versione del runtime e acquisire anche le metriche server. Il ttft_ms misura il primo frammento di contenuto nello stream, non il primo byte di rete. output_chars non equivale ai token generati: per token/s usare i contatori del server o un tokenizer compatibile.

10. Metriche e interpretazione corretta

MetricaSignificatoAttenzione
TTFTTempo al primo token visibileInclude rete, coda e prefill
ITL / TPOTTempo fra token successiviDipende dal batching
P50/P95/P99Distribuzione delle latenzeServono campioni sufficienti
Prefix cache hit rateQuota di prefisso riutilizzataDefinizione runtime-specifica
KV cache usagePressione sulla cacheNon è tutta la VRAM usata
Output token/sThroughput di generazioneDistinguere per richiesta e aggregato
Cost per correct taskCosto per task verificatoRichiede un valutatore esterno

vLLM espone metriche Prometheus su /metrics; nomi e disponibilità dipendono dalla versione. Fra le famiglie rilevanti: utilizzo KV, query e hit del prefix cache, richieste attive, TTFT e latenza end-to-end. Un hit rate su finestra temporale va calcolato usando variazioni dei contatori nella stessa finestra, non confrontando valori cumulativi di intervalli differenti.

La metrica decisiva per gli agenti non è il solo costo/token: costo per task completato correttamente. Una policy aggressiva può ridurre il contesto attivo e peggiorare l'accuratezza. La valutazione deve includere task fissi, verificatori deterministici, errori di tool calling e regressioni funzionali.

11. Architettura di produzione: cache non significa memoria durevole

ASP.NET Core / Agent API
          |
    AuthN / AuthZ / Tenant
          |
    Agent Orchestrator ---- Durable state / checkpoint
          |                       |
      Model Gateway        Memory / tool results
          |
    Cache policy + routing
          |
   +------+-------+
   |              |
GPU worker A   GPU worker B
KV cache       KV cache
   |              |
   +------+-------+
          |
 Metrics / tracing / audit

Lo stato durevole dell'agente descrive decisioni, tool call, risultati e checkpoint necessari al recovery. La KV cache è un'accelerazione computazionale, spesso volatile. Se un worker GPU si arresta, l'agente deve poter ripartire dalla traiettoria persistita anche quando tutta la cache è andata persa.

Per il multi-tenant, attenzione al riuso di prefissi fra gruppi di fiducia: differenze di latenza possono rivelare indirettamente informazioni. Isolamento della cache, autorizzazioni e politiche di retention devono essere coerenti. Inoltre una tool response malevola rimane non attendibile anche se è già stata memorizzata nella cache.

12. Conclusioni

Il Cache Engineering sposta l'ottimizzazione degli agenti dal solo testo del prompt allo stato computazionale dell'attenzione. Prefix caching e PagedAttention sono oggi strumenti concreti di ottimizzazione del serving; le operazioni drop e repos di AKV rappresentano una direzione di ricerca più avanzata che richiede un runtime specializzato.

Le tre regole operative sono: misurare separatamente prefill, decode e occupazione KV; non riutilizzare stati senza rispettarne la storia; non confondere mai cache GPU e persistenza dell'agente. Il miglioramento più importante non è il costo per token, ma il costo per task corretto e verificabile.

Fonti tecniche e scientifiche

  1. Cache Engineering for Agents — paper (2026)
  2. AKV — codice degli autori
  3. vLLM — Automatic Prefix Caching
  4. vLLM — documentazione e serving
  5. PagedAttention — Hugging Face
  6. Rotary Embeddings — Hugging Face
  7. Transformers — Cache Explanation
  8. SGLang — Unified Radix Cache

Nota metodologica: i numeri AKV sono risultati sperimentali degli autori. Il laboratorio proposto verifica il prefix caching standard, non replica AKV. Controllare la versione delle CLI e la compatibilità GPU prima dell'esecuzione.

giovedì 22 ottobre 2026

Durable AI Agents in .NET

NicoGIS Tech Deep Dive · 22 ottobre 2026 · .NET / Distributed Systems / Agentic AI · Livello: Advanced

Durable AI Agents in .NET: Replay, Checkpointing, Human-in-the-Loop and Exactly-Once Illusions

Un agente AI che risponde in tre secondi può vivere dentro una normale richiesta HTTP. Un agente che interroga più sistemi, attende un'approvazione e infine esegue un'operazione amministrativa è invece un processo distribuito di lunga durata. La differenza non è quantitativa: cambiano le garanzie richieste, il modello di errore e il modo in cui viene rappresentato lo stato.

Il 12 ottobre 2026 Microsoft ha pubblicato un'analisi di cinque casi reali per Durable Task. Il messaggio più importante è che l'esecuzione durevole permette di ricordare e riprendere il coordinamento, ma non certifica l'esito delle operazioni esterne. In questo approfondimento costruiremo il modello architetturale e un laboratorio C# verificabile anche senza credenziali cloud, poi vedremo come portarlo su Durable Task Scheduler e Microsoft Agent Framework.

1. Il failure model: dove si perde davvero lo stato?

Consideriamo un agente di incident response. Raccoglie log e metriche, chiede a un LLM una proposta di remediation, attende l'approvazione di un operatore e chiama una API per applicarla. L'operazione può richiedere ore. Se il processo viene terminato durante l'attesa, una variabile C# e una continuation await non sono più disponibili.

HTTP request → Evidence collection → LLM proposal → Approval wait → Execute tool → Verify │ │ │ │ ▼ ▼ ▼ ▼ I/O failure Non-determinism Worker crash Unknown outcome

La difficoltà non è riavviare il metodo: è distinguere le attività completate, quelle in corso e quelle il cui risultato è sconosciuto. Un timeout HTTP, per esempio, non dimostra che il server remoto non abbia eseguito l'operazione.

2. Durable Task: orchestrator, activities, history e scheduler

Durable Task separa il coordinamento dall'esecuzione degli effetti collaterali. L'orchestrator descrive la sequenza logica; le activities effettuano I/O, chiamate LLM e accessi al database. Il Durable Task Scheduler (DTS) mantiene lo stato durevole delle istanze e rende disponibile il lavoro ai worker.

Durable Task Scheduler ┌────────────────────────┐ Client / API ───────►│ Instance ID + History │◄──── Worker restart │ Events / Timers / Waits │ └────────────┬───────────┘ │ Orchestrator │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ Evidence Activity LLM Activity Tool Activity │ │ │ ▼ ▼ ▼ Telemetry store Proposal store External system

La cronologia è un log di eventi, non un dump della memoria CLR. Al riavvio il runtime riesegue il codice dell'orchestrator utilizzando i risultati già registrati nella history. Non ripristina lo stack di un thread sospeso.

Replay deterministico

Durante il replay, il codice dell'orchestrator deve produrre la stessa sequenza logica di decisioni. Chiamate dirette a DateTime.UtcNow, generatori casuali, HTTP, query SQL e inferenza LLM possono compromettere questa proprietà. Usare invece il tempo logico fornito dal contesto, timer durevoli e attività per gli effetti esterni.

// Schema delle responsabilità: esempio illustrativo.
// L'I/O deve avvenire in una activity, NON nell'orchestrator.
public override async Task<IncidentOutcome> RunAsync(
    TaskOrchestrationContext context, IncidentRequest request)
{
    var evidence = await context.CallActivityAsync<EvidenceRef>(
        "CollectEvidence", request);

    var proposal = await context.CallActivityAsync<ProposalRef>(
        "GenerateAndPersistProposal",
        new ProposalRequest(request.IncidentId, evidence));

    await context.CallActivityAsync(
        "OpenApproval", proposal);

    // L'evento segnala che la decisione potrebbe essere disponibile.
    // L'activity seguente legge la decisione AUTOREVOLE.
    await context.WaitForExternalEvent<string>(
        $"approval:{proposal.ApprovalId}");

    var decision = await context.CallActivityAsync<ApprovalDecision>(
        "ReadCommittedDecision", proposal);

    if (!decision.Approved || decision.ProposalHash != proposal.Hash)
        return new IncidentOutcome("Rejected");

    return await context.CallActivityAsync<IncidentOutcome>(
        "ExecuteApprovedAction", proposal);
}

Nota sul codice: lo snippet mostra le boundary architetturali, non un progetto compilabile autonomamente: DTO, registrazioni, autenticazione e policy di timeout sono intenzionalmente omessi. La gestione di timeout e segnali duplicati va implementata prima dell'uso in produzione.

3. Quattro crash window che cambiano tutto

InterruzioneComportamento correttoResponsabile
Dopo il completamento registrato di un'activityIl replay riutilizza il risultato registrato.Durable Task
Dopo la risposta del modello, prima del commit del risultatoRecuperare la proposta già persistita con una chiave stabile; non sostituirla silenziosamente.Adapter LLM + proposal store
Durante l'attesa di approvazioneRiprendere l'attesa senza occupare un thread; leggere la decisione persistita.Scheduler + approval store
Dopo l'esecuzione del tool, prima dell'acknowledgementRiconciliare l'operazione con il sistema remoto prima di tentare un retry.Tool adapter + operation journal

4. Exactly-once: la garanzia che non si può dedurre dalla history

La history permette al runtime di evitare la riesecuzione di activity già confermate. Non può impedire che un effetto esterno sia avvenuto prima della conferma. Perciò, nei sistemi distribuiti, le chiamate esterne devono essere considerate potenzialmente ripetibili.

Worker Remote API Durable History │ POST /remediate (key=K) │ │ ├─────────────────────────────►│ │ │ ├── Commit action │ │ [connection lost] │ │ X │ │ │ worker restart │ │ ├── Retry? ────────────────────┤ │ │ │ │ └── FIRST reconcile K ◄────────┘ │

La soluzione è combinare identificativo stabile, idempotency key, fingerprint dei parametri e reconciliation. Se la API remota non espone uno stato interrogabile né supporta deduplicazione, non si può promettere exactly-once soltanto aggiungendo Durable Task.

Invarianti da applicare

  • La stessa chiave di operazione con parametri differenti deve essere rifiutata.
  • Un'approvazione deve riferirsi alla versione esatta della proposta, non genericamente all'incidente.
  • Una proposta approvata può diventare non più sicura: la policy deve essere rivalutata prima dell'esecuzione.
  • Un esito Unknown non deve essere trasformato automaticamente in Failed.
  • Il risultato remoto deve poter essere correlato all'operation ID.

5. Human-in-the-Loop: approvare un'azione immutabile

L'approvazione umana non è un semplice evento UI. È una transazione applicativa autenticata. L'endpoint di approvazione deve verificare l'identità del revisore, il suo ruolo, la scadenza, lo stato corrente e l'hash della proposta. Solo dopo aver registrato una decisione terminale può inviare il segnale al workflow.

public sealed record RemediationProposal(
    string IncidentId,
    string ProposalId,
    string OperationId,
    string ActionName,
    string CanonicalArgumentsJson,
    string Sha256,
    DateTimeOffset ExpiresAt);

public sealed record ApprovalRecord(
    string ProposalId,
    string ProposalSha256,
    string ReviewerId,
    bool Approved,
    DateTimeOffset DecidedAt);

Un token di approvazione non deve diventare una credenziale illimitata per gli strumenti. Il backend deve continuare ad applicare RBAC/ABAC e controllare gli argomenti al momento dell'esecuzione. L'evento durevole è un hint: il database delle decisioni rimane la fonte autorevole.

6. Laboratorio C#/.NET 10: simulare un crash e verificare il recovery

Prima di introdurre un runtime specifico, isoliamo la proprietà più importante: una chiamata remota con acknowledgement perso deve essere riconciliata, non eseguita nuovamente. Il seguente laboratorio è autonomo e compilabile con .NET 10; simula una API remota e un crash mediante due esecuzioni del programma. Non è un emulatore Durable Task: verifica deliberatamente soltanto l'invariante di idempotenza.

6.1 Creazione del progetto

dotnet new console -n DurableRecoveryLab -f net10.0
cd DurableRecoveryLab

6.2 Sostituire Program.cs

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

const string journalPath = "operation-journal.json";
const string operationId = "incident-42:proposal-7:remediate";
const string payload = "restart-service:service-A";

var fingerprint = Convert.ToHexString(
    SHA256.HashData(Encoding.UTF8.GetBytes(payload)));

var journal = File.Exists(journalPath)
    ? JsonSerializer.Deserialize<Dictionary<string, string>>(
        File.ReadAllText(journalPath))!
    : new Dictionary<string, string>();

// Il journal rappresenta lo stato AUTOREVOLE della API remota
// e sopravvive a un riavvio del processo in questo laboratorio.
if (journal.TryGetValue(operationId, out var previous))
{
    if (previous != fingerprint)
        throw new InvalidOperationException(
            "Operation ID reused with different arguments.");

    Console.WriteLine("RECONCILED: operation already committed.");
    return;
}

Console.WriteLine("Executing external operation once...");

// Simulazione di commit remoto: file atomico nel filesystem locale.
journal[operationId] = fingerprint;
var temp = journalPath + ".tmp";
File.WriteAllText(temp, JsonSerializer.Serialize(journal));
File.Move(temp, journalPath, overwrite: true);

if (args.Contains("--crash-after-commit"))
{
    Console.WriteLine("SIMULATED CRASH: acknowledgement lost.");
    Environment.FailFast("Crash after remote commit");
}

Console.WriteLine("COMMITTED: operation completed.");

Limite esplicito: il file locale rappresenta una simulazione di ledger remoto, non un registro transazionale multi-processo. Per un ambiente reale occorre una transazione nel sistema autorevole, vincolo univoco sull'operation ID, fingerprint immutabile e, dove necessario, un meccanismo di fencing.

6.3 Fault injection

# Prima esecuzione: commit e crash intenzionale.
dotnet run -- --crash-after-commit

# Seconda esecuzione: riconciliazione, nessun secondo commit.
dotnet run

La prima esecuzione termina intenzionalmente con errore dopo aver scritto il journal. La seconda trova la stessa operazione già registrata e stampa RECONCILED. È il comportamento che l'adapter di una activity dovrebbe ottenere interrogando una API remota. Eliminando il file si riparte da uno stato iniziale.

6.4 Test aggiuntivi

TestInvarianteRisultato atteso
Crash prima del commitNon esiste alcuna operazione registrataIl retry può iniziare la prima esecuzione
Crash dopo il commitL'operation ID esisteRiconciliazione senza nuovo effetto
Stessa chiave, payload diversoFingerprint immutabileRifiuto della richiesta
Due worker concorrentiDeduplicazione atomica lato serverUn solo commit; richiede DB reale
Approvazione scadutaDeadline e versione della propostaNessuna esecuzione

7. Dal laboratorio a Durable Task Scheduler

Per utilizzare la vera orchestrazione durevole si può seguire il quickstart Microsoft per il portable Durable Task SDK, che configura l'emulatore DTS e un worker .NET. Le dipendenze principali appartengono alle famiglie Microsoft.DurableTask.*; per l'integrazione con DTS sono disponibili i pacchetti client e worker AzureManaged. Le versioni e i comandi di avvio dell'emulatore devono essere allineati al quickstart ufficiale, poiché la toolchain è in evoluzione.

# Esempio di pacchetti per un host .NET standalone.
dotnet add package Microsoft.DurableTask.Client.AzureManaged
dotnet add package Microsoft.DurableTask.Worker.AzureManaged
dotnet add package Microsoft.Extensions.Hosting

La stringa di connessione usata dagli esempi locali è:

Endpoint=http://localhost:8080;TaskHub=default;Authentication=None

Questa configurazione senza autenticazione è appropriata soltanto per un emulatore locale isolato. In Azure utilizzare autenticazione Entra ID e accessi di rete adeguati.

La trasposizione del laboratorio richiede tre activity distinte: GenerateAndPersistProposal, ResolveApproval e ExecuteOrReconcile. L'ultima deve ricevere un operation ID determinato prima del primo tentativo e verificare il journal remoto a ogni redelivery. La sola history DTS non può sostituire questo controllo.

8. Microsoft Agent Framework: Durable Agents e Durable Workflows

La Durable Extension di Microsoft Agent Framework (MAF) integra il modello agentico con Durable Task. È utile distinguere due astrazioni:

  • Durable Agent: conserva sessioni e contesto conversazionale attraverso più richieste e riavvii.
  • Durable Workflow: esegue grafi di executor/agenti con checkpoint, routing, fan-out/fan-in e passaggi di approvazione.

Per il modello self-hosted, la documentazione indica il pacchetto Microsoft.Agents.AI.DurableTask e le API ConfigureDurableAgents e ConfigureDurableWorkflows. Sono disponibili anche hosting Azure Functions e campioni ufficiali per console app.

// Frammento dalla configurazione self-hosted documentata da Microsoft.
// Presuppone un oggetto `agent` già creato e i package richiesti.
string connectionString =
    Environment.GetEnvironmentVariable(
        "DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
    ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.ConfigureDurableAgents(
            options => options.AddAIAgent(agent),
            workerBuilder: builder =>
                builder.UseDurableTaskScheduler(connectionString),
            clientBuilder: builder =>
                builder.UseDurableTaskScheduler(connectionString));
    })
    .Build();

await host.StartAsync();

La differenza è significativa: il primo laboratorio mostrava come coordinare in modo durevole attività attorno a un agente; la Durable Extension aggiunge persistenza anche alle sessioni e ai workflow dell'Agent Framework. Non elimina comunque la necessità di autorizzare tool call e riconciliare gli effetti esterni.

9. Durable orchestration, queue worker o state machine SQL?

ScenarioScelta da valutareMotivazione
Task indipendente di pochi secondiBackgroundService / queueMinore complessità
Processo con più step e attese di giorniDurable TaskTimer, eventi, history e recovery
Stato già gestito da un motore transazionale consolidatoState machine SQL + outboxEvitare una seconda fonte di verità
Agenti con sessioni e workflow distribuitiMAF Durable ExtensionPersistenza del modello agentico
Operazione remota potenzialmente duplicabileIdempotency + reconciliationNecessaria in ogni architettura

Durable Task diventa conveniente quando il codice necessario a ricordare cosa fare dopo sta diventando un sistema a sé. Non è una scelta obbligata per ogni async/await.

10. Osservabilità: misurare il comportamento, non solo l'esito

Una dashboard che mostra Completed non prova che l'operazione esterna sia stata eseguita una sola volta. La telemetria dovrebbe correlare OrchestrationInstanceId, IncidentId, ProposalId, OperationId e l'eventuale receipt remoto.

  • Workflow: durata totale, tempo in attesa di approvazione, replay e retry.
  • LLM: modello, token, latenza, costo e fingerprint della proposta.
  • Tools: esecuzioni richieste, esiti incerti, reconciliation e deduplicazioni.
  • Governance: identità del revisore, versione della policy, decisione e timestamp.

Non inserire automaticamente prompt completi, credenziali o documenti sensibili nei log. Anche la history e le tracce dell'agente sono dati da proteggere e sottoporre a retention.

11. Deployment, versioning e sicurezza

Un workflow può rimanere attivo mentre il codice viene aggiornato. Modificare la sequenza delle activity o le condizioni di branching può rendere incompatibile il replay di istanze esistenti. Usare versioning delle orchestrazioni, deployment coordinati e test di replay. Il repository durabletask-dotnet documenta supporto per orchestrator versionati e migrazione attraverso ContinueAsNew in punti sicuri.

Per gli agenti in produzione è inoltre opportuno implementare: admission idempotente delle richieste, outbox per il passaggio fra database applicativo e scheduler, timeout e scadenze fisse, separazione delle identità utente/servizio, autorizzazione server-side, cifratura dei dati persistiti e limiti di concorrenza.

12. Checklist di fault injection prima del go-live

  1. Terminare il worker dopo il salvataggio della proposta LLM ma prima dell'acknowledgement dell'activity.
  2. Inviare due volte la stessa approvazione e verificarne la deduplicazione.
  3. Inviare una approvazione riferita a un hash diverso e verificarne il rifiuto.
  4. Interrompere il worker durante l'attesa e riprendere su un'altra istanza.
  5. Simulare una risposta HTTP persa dopo il commit remoto.
  6. Ritardare un evento oltre la deadline e verificare la decisione autorevole.
  7. Distribuire una nuova versione del worker con workflow ancora attivi.
  8. Verificare che log e dashboard non espongano segreti o dati sensibili.

13. Conclusioni

Un Durable AI Agent non è semplicemente un LLM con memoria persistente. È un sistema distribuito che coordina attività nondeterministiche, decisioni umane e side effect esterni attraverso interruzioni e retry.

La proprietà da progettare non è «il workflow riparte», ma «dopo il riavvio viene eseguita soltanto l'operazione effettivamente autorizzata, e ogni effetto incerto viene riconciliato prima di procedere».

Durable Task risolve la persistenza del coordinamento. Microsoft Agent Framework fornisce le astrazioni agentiche. Idempotenza, autorizzazione, ledger applicativo e riconciliazione restano responsabilità dell'architettura. È questa separazione che rende un agente affidabile.

Riferimenti tecnici

  1. Microsoft .NET Blog — Durable Task in the Real World (12 ottobre 2026).
  2. Microsoft Learn — Durable Task for AI Agents.
  3. Microsoft Learn — Microsoft Agent Framework Durable Extension.
  4. Microsoft Learn — Portable Durable Task SDK quickstart.
  5. GitHub — microsoft/durabletask-dotnet.
  6. GitHub — Microsoft Agent Framework Durable Extension e sample.