Engineering · Dentro Kibbo / 02
Oltre il prompt: stato e idempotenza nell’AI di Kibbo
Cosa succede quando l’utente conferma due volte o una richiesta fallisce a metà? Il codice e i compromessi dietro una transizione di stato.

In questo articolo
Il problema inizia quando il modello ha risposto
Nel primo capitolo ho raccontato i cinque pilastri di Kibbo: luoghi reali, contratti espliciti, controllo umano, affidabilità e qualità misurabile. Qui entro nel codice che li rende concreti.
Kibbo trasforma una richiesta in un’avventura urbana: una storia, un percorso e checkpoint da raggiungere con il telefono. Una risposta convincente è utile, ma non basta. Il luogo deve essere quello giusto, il quiz deve essere utilizzabile, la conferma dell’utente deve riferirsi alla proposta corretta. E un doppio tap non dovrebbe far partire due generazioni.
Nel costruire questo flusso ho dovuto separare due problemi: lasciare al modello spazio per creare e mantenere nel software il controllo su stato, dati ed effetti. È in questo confine che l’harness diventa parte del prodotto.
Cosa intendo per harness
Chiamo harness l’insieme di codice e controlli che prepara il contesto, chiama il modello, interpreta il risultato e decide come proseguire. In Kibbo non è una libreria con quel nome: è il lavoro distribuito tra orchestratore, handler, tool, persistenza e suite di valutazione.
Per costruire questo sistema sono partito da una domanda: quali responsabilità devono restare al software quando una parte del comportamento dipende da un modello? Nella generazione delle avventure, i pillar prendono forma in cinque scelte architetturali:
- Orchestrazione e controllo umano: un run persistente e una proposta da confermare.
- Tool e grounding: ricerca dei luoghi e verifica dei riferimenti restituiti.
- Contratti: parsing e validazione applicativa prima di usare il contenuto.
- Recovery e idempotenza: controllo delle conferme concorrenti e tentativi limitati.
- Osservabilità, versioning ed eval: tracce per spiegare un risultato e casi fissi per confrontare le modifiche.
Il flusso seguente è una sintesi architetturale, non un log di esecuzione:
richiesta → orchestratore → chiarimenti / anteprima
↓
waiting_human
↓ conferma + claim
running
↓
concept → Places → shortlist → generazione → controlli
↓
persistenza → esito del run
Il modello contribuisce a decidere e a generare. Non è il proprietario dello stato applicativo.
Una conferma è una transizione di stato
Il pulsante Conferma sembra un dettaglio di interfaccia. Sul backend è un confine tra una proposta e un’operazione con costi ed effetti persistenti.
La sessione conserva contesto, fase e versione. Prima di riprenderla, l’orchestratore cerca il run per ID e utente, verifica la scadenza e controlla la compatibilità della versione dello stato. Non basta ricevere un “sì”: bisogna sapere a cosa si riferisce.
Per la conferma della preview uso un aggiornamento condizionale sul database. Questo estratto mostra il nucleo del claim: l’operazione con cui una richiesta acquisisce il diritto di proseguire con la generazione:
const { data } = await supabase
.from('ai_runs')
.update({ status: 'running' })
.eq('id', runId)
.eq('user_id', userId)
.eq('status', 'waiting_human')
.select('id')
return Array.isArray(data) && data.length > 0
Se due richieste provano a confermare lo stesso run, solo quella che riesce a cambiarne lo stato da waiting_human a running può procedere. Il controllo non è “leggo lo stato e poi scrivo”: la condizione fa parte dell’aggiornamento.
L’orchestratore controlla il risultato prima di invocare la generazione completa. Un claim non acquisito viene attualmente restituito come ALREADY_PROCESSING. Disabilitare il pulsante sul telefono resta utile, ma non sostituisce questo controllo server.
Idempotenza: dichiarare il confine della garanzia
Per questo problema, idempotenza non significa ottenere ogni volta le stesse parole dal modello. Significa evitare che la ripetizione della stessa intenzione produca effetti duplicati.
Il claim protegge una fase precisa: la conferma concorrente di un run. Non rende automaticamente idempotente ogni chiamata della pipeline e non garantisce esecuzione exactly-once.
Nel database esiste anche un indice parziale sulla chiave di idempotenza. La condizione è importante quanto il nome:
create unique index if not exists ai_runs_idempotency_key_unique
on public.ai_runs (idempotency_key)
where idempotency_key is not null
and status in ('success', 'waiting_human');
I run running non rientrano in questo vincolo. Non è quindi un lock generale su tutte le richieste in corso. E il ramo di conferma duplicata non costituisce, da solo, un meccanismo di replay del risultato già prodotto.
Resta inoltre il caso del fallimento parziale: l’avventura e i checkpoint vengono salvati con chiamate distinte, e l’esito del run viene persistito successivamente. Se una fase riesce e quella dopo fallisce, il solo claim non ricompone l’operazione.
Qui distinguo ciò che c’è da ciò che vorrei consolidare: salvataggio transazionale dove possibile, recupero dei run interrotti e replay di un risultato già persistito. Sono evoluzioni dell’harness, non garanzie che attribuisco al codice attuale. Anche distinguere un errore del database da un claim perso renderebbe la diagnosi più precisa.
Un confine piccolo, ma decisivo
Una conferma non è soltanto un messaggio da interpretare: è una transizione che autorizza il lavoro successivo. In Kibbo il claim atomico rende esplicito quel confine, mentre i controlli su utente, scadenza e versione mantengono la conferma legata alla sessione corretta.
La lezione è circoscritta: posso proteggere questa transizione senza fingere di aver risolto ogni possibile fallimento della pipeline. L’harness serve anche a questo, a distinguere le garanzie già presenti dalle parti ancora da consolidare.
Grounding, valutazione e osservabilità meritano lo stesso livello di attenzione, ma rispondono a domande diverse. Nei prossimi tre capitoli approfondirò come collegare i checkpoint ai luoghi reali, come confrontare la qualità delle generazioni e come misurare costi e latenza di un’avventura.
DENTRO KIBBO · LA SERIE CONTINUA
I prossimi approfondimenti
Dal luogo plausibile al checkpoint reale
Il grounding in Kibbo: ricerca dei luoghi, shortlist e controlli sui place ID. Cosa cambia quando una città offre pochi candidati.
Come capire se l’AI sta migliorando
Le eval di Kibbo: casi ripetibili, baseline e confronto tra versioni. Come cercare regressioni oltre la singola risposta convincente.
Quanto costa un’avventura AI?
Token, latenza e retry: misurare il workflow e distinguere le stime del modello dal costo complessivo. Il ruolo delle tracce e del fingerprint.