Un agente AI è un modello linguistico che lavora in loop. Legge il task, usa uno strumento come "leggi questo file" o "elimina quel file", analizza il risultato e va avanti finché il lavoro non è finito. Ogni volta che il modello chiede di usare uno strumento, quella richiesta viene chiamata tool call.
Il codice che gestisce questo loop si chiama harness. Il modello decide cosa vuole fare. L'harness lo esegue e stabilisce anche cosa il modello ha il permesso di fare.
Un buon harness prende tantissime piccole decisioni lungo il percorso. Quale modello dovrebbe gestire questa richiesta? Questa tool call è sicura da eseguire? Questa risposta è abbastanza valida da restituire all'utente? La maggior parte degli harness risponde a queste domande interrogando un modello chat e leggendone la risposta. Ma ogni volta costa una chiamata completa al modello, quindi nella pratica molti controlli vengono saltati.
Jev di TypeSafe AI è un piccolo modello costruito esclusivamente per prendere queste decisioni. Tu descrivi la situazione e fai qualche domanda, e lui risponde a ciascuna con un numero. Non genera mai testo.
Questo aspetto diventa cruciale quando crei un harness personalizzato, ovvero il tuo loop per agenti invece di usarne uno già pronto. Un harness personalizzato ti permette di scegliere quali modelli far girare, su cosa può mettere le mani l'agente e cosa si considera un lavoro completato. Jev rende i controlli dietro queste scelte così economici da poterli eseguire a ogni singolo passaggio.
In questo tutorial costruirai un harness con il Pi SDK, un toolkit TypeScript per creare agenti, e userai Jev in tre punti diversi. Alla fine, eseguirai l'harness completato in una sandbox live e ne modificherai tu stesso le impostazioni.
Accedi al tutorial interattivo completo e al playground qui:
https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
Questa guida è ispirata all'articolo di Sydney Runkle Building a Harness with Jev sul blog di LangChain, che mostra il model routing e il tool gating sotto forma di middleware LangChain pronti all'uso. Qui ricostruirai gli stessi concetti da zero con il Pi SDK, aggiungendo poi altri due pattern per gestire i fallimenti e verificare le risposte.
Cosa costruirai
L'harness è composto da tre parti. Ognuna fa una domanda a Jev in un momento diverso, e nel resto della guida ci riferiremo a esse con questi nomi.

L'esempio pratico è un agente che lavora in una cartella di appunti sui rilievi dei sentieri, con un file per ogni escursione. Può leggere, scrivere ed effettivamente eliminare quegli appunti: ecco perché il gate è fondamentale.
Cos'è Jev
TypeSafe definisce Jev un modello System One. Il nome deriva dallo psicologo Daniel Kahneman, che ha descritto due modalità di pensiero. Il System One è veloce e automatico, come sapere che una padella scotta. Il System Two è lento e ragionato, come fare una divisione lunga.
In questo harness, un normale modello linguistico si occupa del lavoro più lento: leggere i file e scrivere le risposte. Jev prende le decisioni rapide che ruotano attorno a questo processo. Jev è così economico e veloce che puoi consultarlo per ogni singola tool call, non solo per quelle che prevedi essere rischiose.

Ogni numero è una probabilità compresa tra 0 e 1. Uno 0,83 significa che Jev è piuttosto sicuro che la risposta sia sì. Uno 0,03 significa che è abbastanza certo che la risposta sia no.
Tre tipi di domande
Ogni richiesta a Jev ha due parti. Lo state (stato) è la situazione che vuoi far valutare, ad esempio una tool call o la richiesta di un utente. Le questions (domande) sono ciò che vuoi sapere al riguardo. Jev risponde a tutte le domande in una sola chiamata, contemporaneamente, quindi fare tre domande richiede circa lo stesso tempo che farne una.
Jev supporta tre tipi di domande. La prima, che Jev chiama noul, è una domanda a risposta chiusa (sì o no).

Jev sa solo quello che gli dici tu, quindi descrivi ogni opzione e ogni livello in modo chiaro e discorsivo. Le descrizioni sono il prompt.
Setup
Jev è disponibile tramite OpenRouter, un servizio che ti dà accesso a moltissimi modelli AI con una sola chiave API. Quell'unica chiave copre sia il modello linguistico sia Jev. Installa i due pacchetti Pi e imposta la tua chiave.
1npm install @earendil-works/pi-agent-core @earendil-works/pi-ai
1OPENROUTER_API_KEY=...
Jev ha un suo indirizzo web, separato da quello classico per le chat. Specifica sempre una versione esatta, come typesafe/jev-1.13. L'intero client Jev è una semplice chiamata fetch.
1const JEV = "typesafe/jev-1.13";23async function ask(state: unknown, questions: unknown) {4 const response = await fetch("https://openrouter.ai/api/alpha/decisions", {5 method: "POST",6 headers: {7 "Content-Type": "application/json",8 Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,9 },10 body: JSON.stringify({ model: JEV, state, questions }),11 signal: AbortSignal.timeout(2_000),12 });13 if (!response.ok) throw new Error(await response.text());14 return response.json();15}
L'agente resta in attesa mentre Jev risponde, quindi la chiamata va in timeout dopo due secondi. Una risposta normale impiega dai 200 ai 400 millisecondi.
Dove si inserisce Jev
La classe Agent di Pi gestisce il loop al posto tuo e permette al tuo codice di intervenire in momenti precisi di quel ciclo. Questi punti di intervento si chiamano hook. L'harness usa un hook per ciascuna delle sue tre parti.

Un primo gate
Partiamo dalla versione più essenziale ma utile del gate. Prima di ogni tool call, fai a Jev una domanda a risposta chiusa e blocca la chiamata se la risposta sembra essere un sì.
1import { Agent } from "@earendil-works/pi-agent-core";23const agent = new Agent({4 initialState: { systemPrompt, model, tools },5 streamFn: models.streamSimple.bind(models),67 beforeToolCall: async ({ toolCall, args }) => {8 const { answers } = await ask(9 { tool: toolCall.name, arguments: args },10 {11 destructive: {12 type: "noul",13 instructions: "This tool call destroys or overwrites data that was not created by this run.",14 criteria: {15 true: "Deletes files, truncates or overwrites existing content, drops data, or force-pushes over history.",16 false: "Reads, lists, creates a new file, or appends to a file this run already created.",17 },18 },19 },20 );2122 if (answers.destructive.noul >= 0.65) {23 return { block: true, reason: "Blocked: this looks destructive.", terminate: true };24 }25 },26});
Lo 0,65 è una soglia (threshold), ovvero il limite oltre il quale l'harness smette di fidarsi di una chiamata. Tutto ciò che Jev valuta con un punteggio pari o superiore a questo valore viene bloccato.
Ora un agente che prova a eliminare i tuoi appunti si ferma prima che la cancellazione avvenga. Eliminare un appunto ottiene un punteggio di circa 0,83, mentre leggerne uno si ferma a 0,01. Il gate però è un po' grezzo. Creare un file completamente nuovo ottiene circa 0,70, quindi viene bloccato anche quello.
Fare domande costa pochissimo. Jev costa 0,042 $ per milione di token in input, meno di un terzo del prezzo di input di GLM 5.3 Flash, il più economico dei due modelli usati da questo harness.
Dal primo gate all'harness completo
Il primo gate funziona, ma lascia quattro lacune.
- La soglia è nascosta dentro l'hook, quindi è difficile da modificare o testare.
- Ogni richiesta gira sullo stesso modello, che sia facile o complessa.
- Non c'è nulla che spieghi cosa succede se Jev non è raggiungibile.
- Nulla verifica se la risposta finale è davvero valida.
Ciascuna sezione numerata qui sotto colma una di queste lacune.
1. Tieni le soglie in un unico punto
Questa sezione migliora il gate.
Le soglie decidono cosa può fare l'agente, e le dovrai ritoccare spesso appena vedrai i risultati reali. Quindi spostale fuori dall'hook e mettile in una semplice funzione, decideGate(). Prende i numeri di Jev e restituisce un verdetto: la decisione finale dell'harness su una chiamata. Mantenere queste regole in un unico posto si chiama policy.
La policy aggiunge anche una via di mezzo. Con una sola soglia puoi solo permettere o bloccare. Con due soglie ottieni tre verdetti.
- A partire da blockAt o oltre, la chiamata viene bloccata.
- Sotto askAt, la chiamata viene eseguita.
- Nel mezzo, la chiamata aspetta l'approvazione di una persona.
Quella fascia intermedia intercetta le chiamate su cui Jev è incerto, che con un singolo limite verrebbero gestite male in un senso o nell'altro.

1export function decideGate(signals: GateSignals, policy = DEFAULT_GATE_POLICY): GateVerdict {2 const worst = Math.max(3 signals.destructive.noul,4 signals.irreversible.noul,5 signals.outsideWorkspace.noul,6 );7 if (worst >= policy.blockAt) return { action: "block", ... };8 if (worst >= policy.askAt) return { action: "ask", ... };9 return { action: "allow", ... };10}
Essendo una semplice funzione, puoi testarla passandole numeri come quelli visti sopra, senza dover coinvolgere alcun modello live.
2. Scegli un modello per ogni richiesta
Questa sezione aggiunge il router.
Alcune richieste sono semplici, come leggere un singolo file. Altre sono complesse, come capire perché qualcosa si è rotto. Far girare tutto sul modello più potente è uno spreco di soldi, ma far girare tutto su uno economico significa dare risposte deboli alle richieste difficili. Il router abbina ogni richiesta al tier giusto, ovvero al modello veloce ed economico oppure a quello potente e costoso.
Prima che inizi una richiesta, il router fa due domande a Jev in una sola chiamata. Una scelta (choice) seleziona il tier, mentre un punteggio (score) valuta la complessità della richiesta.

1const ROUTER_QUESTIONS = {2 tier: choice("Which model tier should handle this request?", {3 fast: "Reading one file, pulling a fact out of it, or a small edit in a single place.",4 powerful: "Work that spans several files, or a failure with no obvious cause.",5 }),6 complexity: score("How much reasoning does this request need?", [7 "Mechanical. One step, no judgement.",8 "Localized. A few steps inside one area.",9 "Architectural. Many moving parts or an unknown root cause.",10 ]),11};
La policy del router usa le due risposte in questo modo.
- Se il punteggio di complessità è alto, usa il modello potente, anche se Jev aveva scelto quello veloce.
- Se Jev non è sicuro della sua scelta, usa il modello potente per sicurezza.
- Altrimenti, usa il modello scelto da Jev.
1if (complexity.score >= policy.escalateAtComplexity) return powerful;2if (tierAnswer.confidence < policy.minConfidence) return policy.fallbackTier;3return tierAnswer.choice;
Questa è solo una policy di esempio. La tua può pesare le risposte in modo diverso per adattarsi al tuo dominio: ad esempio, puntando al risparmio per un job batch ad alto volume o facendo sempre l'escalation per qualsiasi operazione che tocchi la produzione. Jev fornisce solo le risposte; è il tuo codice a decidere cosa farne.
Perché scegliere una sola volta
Il router sceglie il modello una volta sola, all'inizio della richiesta, e lo mantiene fino alla fine. Questo dipende dalla prompt caching.
A ogni passo dell'agente, il modello rilegge l'intera conversazione avuta fino a quel momento. I provider AI memorizzano le conversazioni lette di recente in modo che rileggerle costi poco. Ma ogni modello ha il proprio archivio. Se cambi modello a metà strada, quello nuovo dovrà rileggere tutto da capo a prezzo pieno.
Il fondatore di Jev fa i conti in un documento di progettazione sugli agenti di coding. In una sessione lunga, passare da Claude Opus al più economico Sonnet e tornare indietro è costato circa il 50% in più rispetto a restare su Opus per tutto il tempo. Quindi scegli il modello all'inizio, quando la conversazione è ancora breve, e mantienilo.
3. Preparati ai downtime di Jev
Questa sezione modifica sia il gate sia il router.
Nel momento in cui l'harness interroga Jev per ogni tool call, l'agente diventa dipendente da Jev. Come qualsiasi servizio online, Jev può essere lento o irraggiungibile. Decidi in anticipo cosa deve fare ogni componente quando non riceve risposta. La scelta giusta cambia da componente a componente.
Il gate blocca la chiamata. Se il gate non riesce a consultare Jev, non ha idea se la chiamata sia sicura. Lasciarla passare potrebbe significare cancellare dei file, quindi il gate la rifiuta. Gli ingegneri lo chiamano failing closed, come una porta che si blocca quando salta la corrente.
Il router usa il modello potente. Se il router non riesce a consultare Jev, non sa quanto sia difficile la richiesta. Il modello potente può gestire qualsiasi cosa, quindi la richiesta otterrà comunque una buona risposta, pagando solo un po' di più. Questo è il failing open, ovvero lasciar proseguire il lavoro.

4. Verifica la risposta
Questa sezione aggiunge il verifier. Negli harness per agenti, un verifier è il passaggio che controlla il lavoro dell'agente prima di considerarlo concluso.
Un agente può terminare con una risposta che tralascia qualcosa, o che afferma cose che in realtà non ha mai verificato nei file. Chi legge spesso non se ne accorge. Controllare la risposta prima di restituirla intercetta questi errori quando l'agente ha ancora la possibilità di riprovare.
Il verifier invia a Jev la risposta finita insieme ai file e ai risultati degli strumenti su cui si basa. Jev valuta la qualità della risposta e indica se le sue affermazioni sono grounded, cioè supportate da ciò che l'agente ha effettivamente letto.

1const VERIFY_QUESTIONS = {2 quality: score("How well does the answer satisfy the request?", [3 "Does not answer the request.",4 "Partly answers it, with a gap the reader would notice.",5 "Fully answers the request.",6 ]),7 grounded: noul("Every factual claim is supported by the files or tool results in the transcript."),8};
Due regole impediscono all'agente di riprovare all'infinito. Ha a disposizione un massimo di due tentativi in totale. E quando Jev non è sicuro del voto che ha dato, l'harness accetta la risposta invece di pagare per un altro tentativo.
Sicurezza e logging
Jev ti fornisce una probabilità, e una probabilità può essere sbagliata. Quindi tutto ciò che il codice tradizionale può verificare con certezza dovrebbe essere controllato direttamente nel codice. In questo harness, ogni strumento per i file rifiuta qualsiasi percorso esterno alla cartella del progetto, indipendentemente da cosa dica Jev. Riserva Jev per quelle valutazioni che il codice non può fare.
Il gate guarda solo la tool call in sé, ovvero il nome dello strumento e i suoi input. Questo aiuta contro la prompt injection, dove un testo nascosto in un file o in una pagina web inganna il modello spingendolo a fare qualcosa di dannoso. Il gate non vede mai l'inganno, ma vede comunque la chiamata pericolosa che ne consegue.
Logga ogni decisione insieme ai numeri che l'hanno generata. Il log spiega perché qualcosa è stato bloccato e fornisce dati reali per impostare le soglie. L'harness scrive una riga per ogni decisione in un file chiamato decisions.jsonl. Poiché il gate vede tutto ciò che l'agente tenta di fare, il log nasconde indirizzi email e chiavi e abbrevia gli input troppo lunghi.
Provalo
La sandbox qui sotto esegue l'harness completato di questo tutorial, collegato al vero Jev. Lavora sulla cartella degli appunti dei rilievi dei sentieri, e il suo strumento delete_path può davvero eliminarli.
Provalo qui: https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
Perché costruire il tuo harness
Gli agenti già pronti prendono queste decisioni usando le loro regole interne. Un harness personalizzato le mette direttamente nel tuo codice. Sei tu a scegliere i modelli, impostare le soglie, decidere quando deve intervenire una persona e leggere nel log esattamente perché è stata fatta ogni singola chiamata.
È Jev a rendere tutto questo fattibile. Ogni decisione richiede poche centinaia di millisecondi e costa una frazione infinitesimale di centesimo, così puoi aggiungere un controllo ovunque il tuo lavoro ne abbia bisogno, e non solo dove puoi permetterti una chiamata completa al modello. Le tre parti di questo tutorial sono solo un punto di partenza. Un harness personalizzato per il tuo dominio può fare a Jev tutte le domande che contano in quel contesto.
Altri utilizzi
Queste stesse tre parti funzionano anche al di fuori degli agenti di coding.
- Bot per code review. Valuta ogni modifica suggerita e mostra a una persona solo quelle che vale la pena leggere.
- Pulizia dei record. Chiedi se due record descrivono la stessa cosa prima di unirli, e lascia i casi dubbi a una persona.
- Pipeline documentali. Assegna un punteggio a ogni pagina estratta e rielabora solo quelle con il punteggio più basso.
- Code di approvazione. Solo le chiamate che rientrano nella fascia "chiedi a una persona" arrivano a un revisore umano.
In tutti questi casi, il modello linguistico fa il lavoro aperto e creativo, mentre Jev risponde alle piccole domande che gli ruotano attorno.
Le domande, le soglie e le policy di questo tutorial sono esempi didattici, non configurazioni ottimizzate per la produzione. Stiamo misurando come queste modifiche all'harness influiscono sui costi e sulla qualità delle risposte, e a breve pubblicheremo una guida di approfondimento con i risultati.
Ho passato una serata con Opus 5.5 per mettere insieme questa guida e la sandbox. Se riscontri problemi, scrivimi in DM. Sentiti libero di copiare l'articolo e darlo in pasto ai tuoi agenti per continuare a sperimentare con queste idee.





