AI-агент — це мовна модель, яка працює в циклі. Вона читає завдання, використовує інструмент на кшталт «прочитай цей файл» або «видали той файл», дивиться на результат і продовжує роботу, доки все не буде виконано. Щоразу, коли модель просить використати інструмент, такий запит називається викликом інструмента (tool call).
Код, який керує цим циклом, називають обв'язкою (harness). Модель вирішує, що саме вона хоче зробити. Обв'язка виконує це рішення, а також визначає, що моделі взагалі дозволено робити.
Хороша обв'язка ухвалює безліч дрібних рішень по ходу роботи. Яка модель має опрацювати цей запит? Чи безпечно виконувати цей виклик інструмента? Чи достатньо якісна ця відповідь, щоб повернути її користувачу? Більшість обв'язок відповідають на ці питання, звертаючись до чат-моделі та читаючи її відповідь. Але кожен такий крок коштує повноцінного виклику моделі, тому на практиці більшість перевірок просто пропускають.
Jev від TypeSafe AI — це маленька модель, створена виключно для таких рішень. Ви описуєте ситуацію, ставите кілька запитань, і вона відповідає на кожне числом. Жодного тексту вона ніколи не генерує.
Це найважливіше тоді, коли ви будуєте власну обв'язку — свій цикл агента замість готового коробкового рішення. Власна обв'язка дає змогу обирати, які моделі працюватимуть, до чого агент матиме доступ і що вважатиметься завершеною роботою. Jev робить перевірки, що стоять за цими рішеннями, настільки дешевими, що їх можна запускати на кожному кроці.
У цьому туторіалі ви побудуєте обв'язку за допомогою Pi SDK — TypeScript-інструментарію для створення агентів — і використаєте Jev у трьох місцях. Наприкінці ви запустите готову обв'язку в живій пісочниці та самостійно зміните її налаштування.
Повний інтерактивний туторіал і пісочницю шукайте тут:
https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
Цей посібник натхненний статтею Сідні Ранкл Building a Harness with Jev у блозі LangChain, де маршрутизація моделей і контроль викликів інструментів показані як готові проміжні шари LangChain. Тут ви реалізуєте ті самі ідеї власноруч на базі Pi SDK, а потім додасте ще два патерни: для обробки помилок і перевірки відповідей.
Що ми побудуємо
Обв'язка складається з трьох частин. Кожна з них ставить Jev запитання в різний момент часу, і далі в посібнику ми будемо називати їх саме так.

Наш приклад — агент, який працює з папкою нотаток про туристичні маршрути: один файл на кожен похід. Він може читати, записувати й реально видаляти ці нотатки, тому контроль тут критично важливий.
Що таке Jev
TypeSafe називає Jev моделлю Системи 1. Назва походить від психолога Даніеля Канемана, який описав два режими мислення. Система 1 — швидка й автоматична, як розуміння того, що сковорода гаряча. Система 2 — повільна й усвідомлена, як ділення в стовпчик.
У нашій обв'язці звичайна мовна модель виконує повільну роботу: читає файли та формує відповіді. А Jev ухвалює швидкі рішення навколо цього процесу. Jev достатньо дешевий і швидкий, щоб запитувати його про кожен виклик інструмента, а не лише про ті, які ви заздалегідь вважаєте ризикованими.

Кожне число — це ймовірність від 0 до 1. Значення 0.83 означає, що Jev майже впевнений у відповіді «так». А 0.03 — що він майже впевнений у відповіді «ні».
Три типи запитань
Кожен запит до Jev складається з двох частин. Стан (state) — це ситуація, яку треба оцінити, наприклад виклик інструмента чи запит користувача. Запитання (questions) — це те, що ви хочете про неї дізнатися. Jev відповідає на всі запитання одним викликом одночасно, тому три запитання займають приблизно стільки ж часу, скільки й одне.
Jev підтримує три типи запитань. Перший, який Jev називає noul, — це запитання з відповіддю «так» або «ні».

Jev знає лише те, що ви йому скажете, тому описуйте кожен варіант і кожен рівень простими словами. Ці описи і є промптом.
Налаштування
Jev доступний через OpenRouter — сервіс, який надає доступ до багатьох AI-моделей за одним API-ключем. Цього ключа вистачить і для мовної моделі, і для Jev. Встановіть два пакети Pi та задайте свій ключ.
1npm install @earendil-works/pi-agent-core @earendil-works/pi-ai
1OPENROUTER_API_KEY=...
У Jev є власна веб-адреса, окрема від звичайної чат-моделі. Завжди вказуйте точну версію, наприклад typesafe/jev-1.13. Увесь клієнт Jev — це один виклик 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}
Агент чекає на відповідь Jev, тому виклик переривається через дві секунди. Звичайна відповідь займає від 200 до 400 мілісекунд.
Де підключається Jev
Клас Agent у Pi запускає цикл за вас. Він дозволяє вашому коду виконуватися у визначені моменти цього циклу. Такі точки називаються хуками (hooks). Наша обв'язка використовує по одному хуку для кожної зі своїх трьох частин.

Перший бар'єр
Почнемо з найпростішої корисної версії бар'єра. Перед кожним викликом інструмента ставимо Jev одне запитання «так/ні» і блокуємо виклик, якщо відповідь схиляється до «так».
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: "Цей виклик інструмента знищує або перезаписує дані, які не були створені під час цього запуску.",14 criteria: {15 true: "Видаляє файли, обрізає чи перезаписує наявний контент, скидає дані або примусово пушить поверх історії.",16 false: "Читає, виводить список, створює новий файл або дописує у файл, вже створений під час цього запуску.",17 },18 },19 },20 );2122 if (answers.destructive.noul >= 0.65) {23 return { block: true, reason: "Заблоковано: схоже на руйнівну дію.", terminate: true };24 }25 },26});
Значення 0.65 — це поріг, межа, після якої обв'язка перестає довіряти виклику. Усе, що Jev оцінює на цьому рівні або вище, блокується.
Тепер агент, який спробує видалити ваші нотатки, зупиниться ще до видалення. Видалення нотатки отримує оцінку близько 0.83, а читання — 0.01. Проте бар'єр виходить грубуватим. Створення абсолютно нового файла оцінюється приблизно в 0.70, тому воно теж буде заблоковане.
Запити коштують дуже мало. Jev бере $0.042 за мільйон вхідних токенів — це менше третини ціни вхідних токенів GLM 5.3 Flash, дешевшої з двох моделей, які використовує наша обв'язка.
Від першого бар'єра до повноцінної обв'язки
Перший бар'єр працює, але залишає чотири прогалини.
- Поріг зашитий прямо в хук, тому його складно змінювати чи тестувати.
- Кожен запит виконується на одній і тій самій моделі, незалежно від складності.
- Ніде не сказано, що робити, якщо Jev недоступний.
- Ніхто не перевіряє, чи хороша фінальна відповідь.
Кожен пронумерований розділ нижче закриває одну з цих прогалин.
1. Тримаємо пороги в одному місці
Цей розділ покращує бар'єр.
Пороги визначають, що дозволено агенту, і ви часто їх коригуватимете, щойно побачите реальні результати. Тому винесіть їх із хука в окрему просту функцію decideGate(). Вона приймає числа від Jev і повертає вердикт: остаточне рішення обв'язки щодо виклику. Зберігання цих правил в одному місці називається політикою.
Політика також додає проміжний варіант. Один поріг може сказати лише «дозволити» або «заблокувати». Два пороги дають три вердикти.
- На рівні blockAt або вище виклик блокується.
- Нижче askAt виклик виконується.
- Між ними виклик чекає на схвалення людини.
Цей середній діапазон ловить ті виклики, щодо яких Jev сумнівається, і які один поріг неминуче оцінив би хибно в той чи інший бік.

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}
Оскільки це звичайна функція, ви можете тестувати її, передаючи числа на кшталт наведених вище, без залучення живої моделі.
2. Обираємо модель для кожного запиту
Цей розділ додає маршрутизатор.
Деякі запити прості, як-от прочитати один файл. Деякі складні, як-от з'ясувати, чому щось зламалося. Виконувати все на найпотужнішій моделі — марнувати гроші, а виконувати все на дешевій — отримувати слабкі відповіді на складні запити. Маршрутизатор підбирає кожному запиту правильний рівень (tier): швидку дешеву модель або потужну дорогу.
Перед початком запиту маршрутизатор ставить Jev два запитання одним викликом. Одне (choice) обирає рівень, інше (score) оцінює складність запиту.

1const ROUTER_QUESTIONS = {2 tier: choice("Який рівень моделі має опрацювати цей запит?", {3 fast: "Читання одного файла, витягування факту з нього або невелика правка в одному місці.",4 powerful: "Робота, що охоплює кілька файлів, або помилка без очевидної причини.",5 }),6 complexity: score("Скільки логічних міркувань потребує цей запит?", [7 "Механічна. Один крок, без оцінювання.",8 "Локалізована. Кілька кроків в одній області.",9 "Архітектурна. Багато рухомих частин або невідома першопричина.",10 ]),11};
Політика маршрутизатора використовує ці дві відповіді так:
- Якщо оцінка складності висока, використовуємо потужну модель, навіть якщо Jev обрав швидку.
- Якщо Jev не впевнений у своєму виборі, для безпеки беремо потужну модель.
- Інакше використовуємо модель, яку обрав Jev.
1if (complexity.score >= policy.escalateAtComplexity) return powerful;2if (tierAnswer.confidence < policy.minConfidence) return policy.fallbackTier;3return tierAnswer.choice;
Це лише приклад політики. Ваша може зважувати відповіді інакше, підлаштовуючись під вашу предметну область: наприклад, схилятися до дешевого варіанта для масових пакетних задач або завжди ескалювати все, що торкається продакшену. Jev лише дає відповіді; ваш код вирішує, що з ними робити.
Чому обираємо лише раз
Маршрутизатор обирає модель один раз, на початку запиту, і тримає її до завершення. Причина — кешування промптів.
Щоразу, коли агент робить крок, модель перечитує всю попередню розмову. Провайдери AI зберігають нещодавно прочитані розмови, щоб їх повторне читання було дешевим. Але кожна модель має власне сховище. Якщо змінити модель посеред роботи, нова доведеться читати все заново за повною ціною.
Засновник Jev детально розбирає ці цифри в документі про дизайн агентів для програмування. В одній довгій сесії перемикання з Claude Opus на дешевший Sonnet і назад обійшлося приблизно на 50% дорожче, ніж якби весь час залишалися на Opus. Тому обирайте модель на старті, поки розмова ще коротка, і не змінюйте її.
3. Готуємося до падіння Jev
Цей розділ змінює і бар'єр, і маршрутизатор.
Щойно обв'язка починає питати Jev про кожен виклик інструмента, агент стає залежним від Jev. Як і будь-який онлайн-сервіс, Jev може працювати повільно або бути недоступним. Заздалегідь вирішіть, що робитиме кожна частина, якщо відповіді немає. Для кожної частини правильний вибір свій.
Бар'єр блокує виклик. Якщо бар'єр не може запитати Jev, він гадки не має, чи безпечний виклик. Якщо пропустити його, можна видалити файли, тому бар'єр відмовляє. Інженери називають це failing closed (безпечна відмова) — як двері, що автоматично зачиняються, коли зникає світло.
Маршрутизатор використовує потужну модель. Якщо маршрутизатор не може запитати Jev, він не знає, наскільки складний запит. Потужна модель впорається з будь-чим, тому запит усе одно отримає хорошу відповідь, просто ви заплатите трохи більше. Це failing open (відкрита відмова) — дозволяємо роботі продовжуватися.

4. Перевіряємо відповідь
Цей розділ додає верифікатор. В обв'язках агентів верифікатор — це етап, який перевіряє роботу агента перед тим, як визнати її завершеною.
Агент може завершити роботу з відповіддю, у якій щось пропущено, або де стверджується те, чого він насправді не перевіряв у файлах. Людина, яка це читає, часто не здатна помітити різницю. Перевірка відповіді перед її поверненням ловить такі помилки, поки агент ще має змогу спробувати знову.
Верифікатор надсилає Jev готову відповідь разом із файлами та результатами роботи інструментів, на яких вона базується. Jev оцінює якість відповіді та каже, чи є її твердження обґрунтованими (grounded), тобто підтвердженими тим, що агент дійсно прочитав.

1const VERIFY_QUESTIONS = {2 quality: score("Наскільки добре відповідь задовольняє запит?", [3 "Не відповідає на запит.",4 "Відповідає частково, з прогалиною, яку читач помітить.",5 "Повністю відповідає на запит.",6 ]),7 grounded: noul("Кожне фактичне твердження підтверджується файлами або результатами інструментів у транскрипті."),8};
Два правила не дають агенту нескінченно повторювати спроби. Загалом він отримує максимум дві спроби. А якщо Jev не впевнений у власній оцінці, обв'язка приймає відповідь, замість того щоб платити за ще одну спробу.
Безпека та логування
Jev дає вам ймовірність, а ймовірність може бути хибною. Тому все, що звичайний код може перевірити напевно, слід перевіряти саме кодом. У цій обв'язці кожен файловий інструмент відхиляє будь-який шлях за межами папки проєкту, незважаючи на те, що каже Jev. Залиште Jev для тих рішень, які код ухвалити не здатний.
Бар'єр дивиться лише на сам виклик інструмента, тобто на його назву та вхідні дані. Це допомагає проти промпт-ін'єкцій, коли текст, прихований у файлі чи на веб-сторінці, змушує модель зробити щось шкідливе. Бар'єр ніколи не бачить самої хитрості, але бачить шкідливий виклик, до якого вона призводить.
Логуйте кожне рішення разом із числами, що за ним стоять. Лог пояснює, чому щось було заблоковано, і дає реальні цифри для налаштування порогів. Обв'язка записує по одному рядку на рішення у файл decisions.jsonl. Бар'єр бачить усе, що намагається зробити агент, тому лог приховує email-адреси та ключі, а також скорочує довгі вхідні дані.
Спробуйте самі
Пісочниця нижче запускає готову обв'язку з цього туторіалу, підключену до справжнього Jev. Вона працює з папкою нотаток про маршрути, а її інструмент delete_path дійсно може їх видаляти.
Спробувати можна тут: https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
Навіщо будувати власну обв'язку
Готові агенти ухвалюють ці рішення за власними вбудованими правилами. Власна обв'язка переносить їх у ваш код. Ви обираєте моделі, задаєте пороги, вирішуєте, коли втручається людина, і читаєте в логах точну причину кожного виклику.
Jev робить це практично можливим. Кожне рішення займає кілька сотень мілісекунд і коштує крихітну частку цента, тому ви можете додати перевірку скрізь, де вона потрібна, а не лише там, де можете дозволити собі повноцінний виклик моделі. Три частини з цього туторіалу — лише відправна точка. Власна обв'язка для вашої предметної області може ставити Jev будь-які запитання, які там важливі.
Інші сценарії використання
Ті самі три частини працюють і поза агентами для програмування.
- Боти для код-рев'ю. Оцінюйте кожну запропоновану зміну і показуйте людині лише ті, що варті уваги.
- Очищення записів. Питайте, чи описують два записи одне й те саме, перш ніж об'єднувати їх, а сумнівні пари залишайте людині.
- Конвеєри обробки документів. Оцінюйте кожну розпізнану сторінку і перезапускайте лише ті, що отримали низький бал.
- Черги на погодження. До людського рецензента потрапляють лише виклики з діапазону «запитати людину».
У кожному випадку мовна модель виконує творчу роботу, а Jev відповідає на дрібні запитання навколо неї.
Запитання, пороги та політики в цьому туторіалі — це навчальні приклади, а не налаштовані параметри для продакшену. Ми зараз тестуємо, як ці зміни в обв'язці впливають на вартість і якість відповідей, і незабаром випустимо продовження з результатами.
Я витратив вечір разом з Opus 5.5, щоб зібрати цей посібник і пісочницю. Якщо зіткнетеся з проблемами, пишіть мені в особисті повідомлення. Сміливо копіюйте статтю та годуйте її своїм агентам, щоб продовжити експерименти з цими ідеями.





