Un agent IA est un modèle de langage qui fonctionne en boucle. Il lit la tâche, utilise un outil comme « lire ce fichier » ou « supprimer ce fichier », examine le résultat et continue jusqu'à ce que le travail soit terminé. Chaque fois que le modèle demande à utiliser un outil, cette requête s'appelle un tool call (appel d'outil).
Le code qui exécute cette boucle s'appelle le harness. Le modèle décide de ce qu'il veut faire. Le harness l'exécute et détermine également ce que le modèle a le droit de faire.
Un bon harness prend une multitude de petites décisions en cours de route. Quel modèle doit traiter cette requête ? Ce tool call est-il sûr à exécuter ? Cette réponse est-elle assez bonne pour être renvoyée ? La plupart des harness y répondent en interrogeant un modèle de chat et en lisant sa réponse. Cela coûte un appel complet au modèle à chaque fois, si bien qu'en pratique, la majorité des vérifications sont ignorées.
Jev, développé par TypeSafe AI, est un petit modèle conçu uniquement pour ces décisions. Vous décrivez la situation, posez quelques questions, et il répond à chacune par un nombre. Il ne génère jamais de texte.
Cela prend tout son sens lorsque vous créez un harness personnalisé, c'est-à-dire votre propre boucle d'agent plutôt que d'utiliser un agent clé en main. Un harness personnalisé vous permet de choisir les modèles à exécuter, ce que l'agent peut modifier et ce qui définit une tâche terminée. Jev rend les vérifications derrière ces choix suffisamment peu coûteuses pour être exécutées à chaque étape.
Dans ce tutoriel, vous allez construire un harness avec le Pi SDK, une boîte à outils TypeScript dédiée à la création d'agents, et utiliser Jev à trois endroits. À la fin, vous exécuterez le harness terminé dans un bac à sable interactif et modifierez ses paramètres vous-même.
Accédez au tutoriel interactif complet et au playground ici :
https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
Ce guide s'inspire de l'article de Sydney Runkle, Building a Harness with Jev, publié sur le blog LangChain, qui présente le routage de modèles et le contrôle des outils sous forme de middlewares LangChain prêts à l'emploi. Ici, vous implémenterez ces mêmes concepts vous-même sur le Pi SDK, puis ajouterez deux autres patterns pour gérer les échecs et vérifier les réponses.
Ce que vous allez construire
Le harness se compose de trois parties. Chacune pose une question à Jev à un moment différent, et le reste de ce guide les désignera par ces noms.

L'exemple fil rouge est un agent travaillant dans un dossier contenant des notes de relevés de sentiers, un fichier par sortie. Il peut lire, écrire et réellement supprimer ces notes, ce qui explique pourquoi le contrôle d'accès est crucial.
Qu'est-ce que Jev ?
TypeSafe qualifie Jev de modèle Système 1. Le nom vient du psychologue Daniel Kahneman, qui a décrit deux modes de pensée. Le Système 1 est rapide et automatique, comme savoir qu'une poêle est brûlante. Le Système 2 est lent et réfléchi, comme poser une division longue.
Dans ce harness, un modèle de langage classique effectue le travail de fond consistant à lire les fichiers et à rédiger les réponses. Jev prend les décisions rapides autour de lui. Jev est suffisamment économique et rapide pour être interrogé sur chaque tool call, et pas seulement sur ceux que vous jugiez risqués.

Chaque nombre est une probabilité comprise entre 0 et 1. Un score de 0,83 signifie que Jev est presque certain que la réponse est oui. Un score de 0,03 signifie qu'il est quasi certain que la réponse est non.
Trois types de questions
Chaque requête envoyée à Jev comporte deux parties. Le state (état) est la situation que vous souhaitez évaluer, comme un tool call ou une requête utilisateur. Les questions correspondent à ce que vous voulez en savoir. Jev répond à toutes les questions en un seul appel, simultanément : poser trois questions prend donc à peu près le même temps qu'en poser une.
Jev prend en charge trois types de questions. Le premier, que Jev appelle un noul, est une question fermée (oui/non).

Jev ne sait que ce que vous lui dites : décrivez donc chaque option et chaque niveau en termes clairs. Ces descriptions constituent le prompt.
Configuration
Jev est accessible via OpenRouter, un service qui donne accès à de nombreux modèles d'IA avec une seule clé API. Cette unique clé couvre à la fois le modèle de langage et Jev. Installez les deux packages Pi et configurez votre clé.
1npm install @earendil-works/pi-agent-core @earendil-works/pi-ai
1OPENROUTER_API_KEY=...
Jev possède sa propre URL, distincte de celle habituellement utilisée pour le chat. Spécifiez toujours une version exacte, comme typesafe/jev-1.13. Le client Jev complet tient en un seul appel 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'agent patiente pendant que Jev répond ; l'appel abandonne donc au bout de deux secondes. Une réponse normale prend entre 200 et 400 millisecondes.
Où intégrer Jev
La classe Agent de Pi exécute la boucle pour vous. Elle permet à votre propre code de s'exécuter à des moments précis de cette boucle. Ces points d'entrée s'appellent des hooks. Le harness utilise un hook pour chacune de ses trois parties.

Un premier contrôle d'accès
Commençons par la version la plus simple mais utile du contrôle d'accès (gate). Avant chaque tool call, posez à Jev une question oui/non et bloquez l'appel si la réponse penche vers oui.
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});
La valeur 0,65 est un seuil, la limite à partir de laquelle le harness cesse de faire confiance à un appel. Tout ce que Jev note à ce niveau ou au-dessus est bloqué.
Désormais, un agent qui tente de supprimer vos notes s'arrête avant que la suppression n'ait lieu. Supprimer une note obtient un score d'environ 0,83, tandis qu'en lire une obtient 0,01. Le contrôle reste toutefois rudimentaire. Créer un tout nouveau fichier obtient environ 0,70, et se retrouve donc bloqué lui aussi.
Interroger Jev coûte très peu. Jev facture 0,042 $ par million de tokens en entrée, soit moins d'un tiers du prix d'entrée de GLM 5.3 Flash, le moins cher des deux modèles utilisés par ce harness.
Du premier contrôle au harness complet
Ce premier contrôle fonctionne, mais il laisse quatre lacunes.
- Le seuil est enfoui dans le hook, ce qui le rend difficile à ajuster ou à tester.
- Chaque requête s'exécute sur le même modèle, qu'elle soit simple ou complexe.
- Rien ne prévoit ce qui se passe si Jev est injoignable.
- Rien ne vérifie si la réponse finale est pertinente.
Chaque section numérotée ci-dessous comble l'une de ces lacunes.
1. Centraliser les seuils
Cette section améliore le contrôle d'accès.
Les seuils déterminent ce que l'agent a le droit de faire, et vous les ajusterez souvent dès que vous observerez des résultats concrets. Sortez-les donc du hook pour les placer dans une simple fonction, decideGate(). Elle prend les scores de Jev et renvoie un verdict : la décision finale du harness concernant un appel. Regrouper ces règles au même endroit s'appelle la politique (policy).
La politique ajoute également une option intermédiaire. Un seul seuil ne peut dire qu'autoriser ou bloquer. Deux seuils offrent trois verdicts.
- Au niveau de blockAt ou au-dessus, l'appel est bloqué.
- En dessous de askAt, l'appel s'exécute.
- Entre les deux, l'appel attend l'approbation d'un humain.
Cette plage intermédiaire intercepte les appels sur lesquels Jev hésite, et qu'un seuil unique aurait mal évalués dans un sens ou dans l'autre.

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}
Comme il s'agit d'une simple fonction, vous pouvez la tester en lui passant des nombres comme ceux ci-dessus, sans impliquer de modèle en direct.
2. Choisir un modèle par requête
Cette section ajoute le routeur.
Certaines requêtes sont simples, comme lire un fichier. D'autres sont complexes, comme identifier pourquoi quelque chose a planté. Tout exécuter sur le modèle le plus puissant gaspille de l'argent, et tout exécuter sur un modèle bon marché donne des réponses faibles aux requêtes difficiles. Le routeur associe chaque requête au bon tier (niveau), c'est-à-dire le modèle rapide et économique ou le modèle puissant et coûteux.
Avant le lancement d'une requête, le routeur pose deux questions à Jev en un seul appel. Un choix sélectionne le tier, et un score évalue la complexité de la requête.

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 politique du routeur exploite ces deux réponses de la manière suivante.
- Si le score de complexité est élevé, utilisez le modèle puissant, même si Jev a choisi le modèle rapide.
- Si Jev manque de confiance dans son choix, utilisez le modèle puissant par sécurité.
- Sinon, utilisez le modèle choisi par Jev.
1if (complexity.score >= policy.escalateAtComplexity) return powerful;2if (tierAnswer.confidence < policy.minConfidence) return policy.fallbackTier;3return tierAnswer.choice;
Il s'agit d'un exemple de politique. La vôtre peut pondérer les réponses différemment selon votre domaine : privilégier l'économie pour un traitement par lots à fort volume, ou toujours escalader tout ce qui touche la production, par exemple. Jev fournit uniquement les réponses ; c'est votre code qui décide quoi en faire.
Pourquoi ne choisir qu'une seule fois ?
Le routeur choisit un modèle une seule fois, au début de la requête, et le conserve jusqu'à ce qu'elle soit terminée. Cela s'explique par le prompt caching (mise en cache des prompts).
À chaque étape franchie par l'agent, le modèle relit l'intégralité de la conversation jusqu'à ce point. Les fournisseurs d'IA stockent les conversations récemment lues afin que leur relecture coûte moins cher. Mais chaque modèle possède son propre stockage. Changez de modèle en cours de route, et le nouveau devra tout relire au tarif plein.
Le créateur de Jev détaille les calculs dans un document de conception sur les agents de codage. Lors d'une longue session, passer de Claude Opus au Sonnet moins cher, puis revenir en arrière, a coûté environ 50 % de plus que de rester sur Opus tout du long. Choisissez donc le modèle au début, quand la conversation est encore courte, et tenez-vous-y.
3. Anticiper les indisponibilités de Jev
Cette section modifie à la fois le contrôle d'accès et le routeur.
Dès lors que le harness interroge Jev sur chaque tool call, l'agent dépend de Jev. Comme tout service en ligne, Jev peut être lent ou indisponible. Décidez à l'avance de ce que fait chaque composant lorsqu'il n'obtient aucune réponse. Le bon choix diffère selon le composant.
Le contrôle bloque l'appel. S'il ne peut pas interroger Jev, il ignore totalement si l'appel est sûr. Le laisser passer pourrait supprimer des fichiers : le contrôle refuse donc. Les ingénieurs appellent cela un échec fermé (failing closed), comme une porte qui se verrouille en cas de coupure de courant.
Le routeur utilise le modèle puissant. S'il ne peut pas interroger Jev, il ignore la difficulté de la requête. Le modèle puissant pouvant tout gérer, la requête obtient tout de même une bonne réponse, moyennant un léger surcoût. C'est un échec ouvert (failing open), qui laisse le travail se poursuivre.

4. Vérifier la réponse
Cette section ajoute le vérificateur. Dans les harness d'agents, un vérificateur est l'étape qui contrôle le travail de l'agent avant de le considérer comme terminé.
Un agent peut aboutir à une réponse incomplète, ou affirmer des choses qu'il n'a jamais réellement vérifiées dans les fichiers. Le lecteur humain peine souvent à s'en rendre compte. Vérifier la réponse avant de la renvoyer permet de détecter cela tant que l'agent peut encore retenter sa chance.
Le vérificateur envoie à Jev la réponse finalisée ainsi que les fichiers et les résultats d'outils sur lesquels elle s'appuie. Jev évalue la qualité de la réponse et indique si ses affirmations sont fondées (grounded), c'est-à-dire étayées par ce que l'agent a réellement lu.

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};
Deux règles empêchent l'agent de réessayer indéfiniment. Il dispose de deux tentatives maximum au total. Et lorsque Jev manque de confiance dans sa propre évaluation, le harness accepte la réponse plutôt que de payer pour un nouvel essai.
Sécurité et journalisation
Jev fournit une probabilité, et une probabilité peut être erronée. Tout ce que du code classique peut vérifier avec certitude doit donc être vérifié dans le code. Dans ce harness, chaque outil lié aux fichiers refuse tout chemin situé hors du dossier du projet, quoi qu'en dise Jev. Réservez Jev aux jugements que le code ne peut pas trancher.
Le contrôle n'examine que le tool call lui-même, c'est-à-dire le nom de l'outil et ses paramètres. Cela aide face à l'injection de prompt, où du texte dissimulé dans un fichier ou une page web pousse le modèle à effectuer une action nuisible. Le contrôle ne voit jamais la ruse, mais il voit l'appel dangereux auquel elle mène.
Journalisez chaque décision avec les chiffres qui la justifient. Le journal explique pourquoi un élément a été bloqué et fournit des données concrètes pour calibrer les seuils. Le harness écrit une ligne par décision dans un fichier nommé decisions.jsonl. Le contrôle voyant tout ce que l'agent tente de faire, le journal masque les adresses e-mail et les clés, et raccourcit les entrées trop longues.
À vous de jouer
Le bac à sable ci-dessous exécute le harness finalisé de ce tutoriel, connecté au véritable Jev. Il travaille sur le dossier de notes de relevés de sentiers, et son outil delete_path peut réellement les supprimer.
Essayez-le ici : https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
Pourquoi créer votre propre harness ?
Les agents clé en main prennent ces décisions via leurs propres règles intégrées. Un harness personnalisé les intègre directement dans votre code. Vous choisissez les modèles, définissez les seuils, décidez quand un humain doit intervenir et lisez dans le journal la raison exacte de chaque appel.
Jev rend tout cela réalisable. Chaque décision prend quelques centaines de millisecondes et coûte une fraction de centime : vous pouvez donc ajouter une vérification partout où votre travail l'exige, et pas seulement là où vous avez les moyens de payer un appel complet au modèle. Les trois parties de ce tutoriel constituent un point de départ. Un harness personnalisé pour votre propre domaine peut poser à Jev toutes les questions pertinentes.
Autres cas d'usage
Ces trois mêmes composants s'appliquent au-delà des agents de codage.
- Bots de revue de code. Évaluez chaque modification suggérée et ne montrez à un humain que celles qui valent la peine d'être lues.
- Nettoyage de données. Demandez si deux enregistrements décrivent la même chose avant de les fusionner, et laissez les cas incertains à un humain.
- Pipelines de documents. Évaluez chaque page extraite et ne relancez le traitement que sur celles ayant obtenu un faible score.
- Files d'approbation. Seuls les appels situés dans la plage « demander à un humain » parviennent à un validateur.
Dans chaque cas, le modèle de langage effectue le travail ouvert, et Jev répond aux petites questions périphériques.
Les questions, seuils et politiques présentés dans ce tutoriel sont des exemples pédagogiques, et non des paramètres de production optimisés. Nous mesurons actuellement l'impact de ces modifications du harness sur les coûts et la qualité des réponses ; un guide de suivi présentant ces résultats paraîtra bientôt.
J'ai passé une soirée avec Opus 5.5 à concevoir ce guide et ce bac à sable. Si vous rencontrez le moindre problème, envoyez-moi un message privé. N'hésitez pas à copier cet article et à le transmettre à vos agents pour continuer à explorer ces concepts.





