AI エージェントとは、ループ処理で動く言語モデルのことです。タスクを読み取り、「このファイルを読む」「あのファイルを削除する」といったツールを使い、結果を確認して、作業が終わるまで動き続けます。モデルがツールの使用を要求するたびに、そのリクエストは tool call と呼ばれます。
このループを実行するコードは harness と呼ばれます。何をするかを決めるのはモデルですが、それを実行し、さらにモデルにどこまでの権限を与えるかを決めるのが harness です。
優れた harness は、途中で数多くの小さな判断を下します。このリクエストはどのモデルが処理すべきか? この tool call は安全に実行できるか? この回答は返却するのに十分な品質か? 多くの harness は、チャットモデルに質問してその応答を読むことでこれらに答えます。しかし、毎回フルのモデル呼び出しコストがかかるため、実際にはほとんどのチェックが省略されてしまいます。
TypeSafe AI の Jev は、こうした判断だけのために作られた小さなモデルです。状況を説明していくつか質問すると、それぞれに対して数値で答えてくれます。テキストを生成することは一切ありません。
これが最も重要になるのは、既製のエージェントではなく、自分でエージェントループを組む カスタム harness を構築するときです。カスタム harness では、どのモデルを動かすか、エージェントがどこに触れられるか、何を「完了」とみなすかを自分で決められます。Jev を使えば、そうした判断の裏にあるチェックを、すべてのステップで実行できるほど低コストに抑えられます。
このチュートリアルでは、エージェント構築用の TypeScript ツールキットである Pi SDK を使って harness を構築し、3 か所で Jev を活用します。最後には、完成した harness をライブサンドボックスで動かし、自分で設定を変更してみます。
完全なインタラクティブチュートリアルとプレイグラウンドはこちら:
https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
本ガイドは、LangChain ブログに掲載された Sydney Runkle 氏の Building a Harness with Jev に着想を得ています。同記事では、モデルルーティングやツールゲートを LangChain のミドルウェアとしてすぐに使える形で紹介しています。ここでは同じコンセプトを Pi SDK 上で自ら実装し、さらに失敗時の処理と回答検証という 2 つのパターンを追加します。
作るもの
harness は 3 つの部分で構成されます。それぞれが異なるタイミングで Jev に質問を行うため、以降のガイドではこれらの名前で呼びます。

サンプルとして使うのは、登山道の調査メモが入ったフォルダで作業するエージェントです。1 回の調査につき 1 ファイルとなっています。エージェントはこれらのメモを読み書きでき、実際に削除もできるため、ゲートの存在が重要になります。
Jev とは何か
TypeSafe は Jev を System One モデル と呼んでいます。この名前は、2 つの思考モードを提唱した心理学者ダニエル・カーネマンに由来します。システム 1 は、フライパンが熱いと瞬時にわかるような、速く自動的な思考です。システム 2 は、筆算で割り算をするような、遅く意図的な思考です。
今回の harness では、通常の言語モデルがファイルを読み込んで回答を書くという遅い作業を担当します。Jev はその周囲で素早い判断を下します。Jev は安価で高速なため、危険だと予想したものだけでなく、すべての tool call について確認できます。

各数値は 0 から 1 の間の確率です。0.83 であれば、Jev は「はい」とかなり確信していることを意味します。0.03 であれば、「いいえ」とかなり確信していることになります。
3 種類の質問タイプ
Jev へのリクエストは常に 2 つの部分から成ります。state は判断してほしい状況(tool call やユーザーのリクエストなど)です。questions はそれについて知りたいことです。Jev は 1 回の呼び出しですべての質問に同時に答えるため、3 つ質問しても 1 つ質問するのとほぼ同じ時間で済みます。
Jev は 3 種類の質問をサポートしています。1 つ目は Jev が noul と呼ぶもので、はい/いいえで答える質問です。

Jev が知っているのはあなたが伝えたことだけなので、すべての選択肢とレベルをわかりやすい言葉で説明してください。この説明自体がプロンプトになります。
セットアップ
Jev は OpenRouter というサービスを通じて利用できます。OpenRouter は 1 つの API キーで多数の AI モデルを利用できるサービスで、この 1 つのキーで言語モデルと Jev の両方をカバーできます。2 つの Pi パッケージをインストールし、キーを設定しましょう。
1npm install @earendil-works/pi-agent-core @earendil-works/pi-ai
1OPENROUTER_API_KEY=...
Jev には通常のチャット用とは別の専用エンドポイントがあります。typesafe/jev-1.13 のように、必ず正確なバージョンを指定してください。Jev クライアント全体は 1 回の 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 の回答を待つため、呼び出しは 2 秒でタイムアウトします。通常の回答は 200〜400 ミリ秒で返ってきます。
Jev を組み込む場所
Pi の Agent クラスがループを実行してくれます。そして、ループ内の特定のタイミングで自分のコードを実行できます。この箇所は hook と呼ばれます。今回の harness では、3 つの部分それぞれに 1 つの hook を使います。

最初のゲート
まずはゲートの最小限かつ実用的なバージョンから始めます。すべての tool call の前に、Jev に 1 つのはい/いいえ質問をし、答えが「はい」に近い場合は呼び出しをブロックします。
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});
0.65 は threshold(閾値)であり、harness が呼び出しを信頼しなくなる境界線です。Jev がこの値以上のスコアを出したものはすべてブロックされます。
これで、メモを削除しようとするエージェントは、実際に削除される前に停止します。メモの削除は約 0.83、読み込みは 0.01 となります。ただし、このゲートはかなり大雑把です。完全に新しいファイルを作成する場合でも約 0.70 となるため、これもブロックされてしまいます。
質問のコストはごくわずかです。Jev は入力トークン 100 万個あたり $0.042 で、この harness が使用する 2 つのモデルのうち安い方である GLM 5.3 Flash の入力価格の 3 分の 1 以下です。
最初のゲートから完全な harness へ
最初のゲートは機能しますが、4 つの課題が残っています。
- 閾値が hook の中に埋め込まれているため、調整やテストが難しい。
- 簡単なリクエストでも難しいリクエストでも、すべて同じモデルで処理される。
- Jev に接続できない場合の挙動が定義されていない。
- 最終的な回答の品質をチェックする仕組みがない。
以下の各セクションで、これらの課題を 1 つずつ解決していきます。
1. 閾値を 1 か所にまとめる
このセクションではゲートを改善します。
閾値はエージェントの行動範囲を決めるものであり、実際の結果を見てから頻繁に調整することになります。そこで、hook の外に出して decideGate() というシンプルな関数にまとめましょう。この関数は Jev の数値を受け取り、verdict(判定)、つまり harness の最終決定を返します。こうしたルールを 1 か所にまとめることを policy と呼びます。
また、policy には中間の選択肢も追加します。閾値が 1 つだと「許可」か「ブロック」しか選べませんが、2 つにすれば 3 つの判定が可能になります。
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. リクエストごとにモデルを選ぶ
このセクションではルーターを追加します。
ファイル 1 つを読むような簡単なリクエストもあれば、何が壊れたのか原因を追跡するような難しいリクエストもあります。すべてを最も強力なモデルで処理するのはコストの無駄ですし、すべてを安いモデルで処理すると、難しいリクエストに対して弱い回答しか得られません。ルーターは、各リクエストを適切な tier(高速で安いモデルか、強力で高価なモデルか)に振り分けます。
リクエストが始まる前に、ルーターは 1 回の呼び出しで Jev に 2 つの質問をします。choice で tier を選び、score でリクエストの複雑度を評価します。

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};
ルーターの policy は、この 2 つの回答を次のように使います。
- 複雑度のスコアが高ければ、Jev が fast を選んでいても強力なモデルを使う。
- Jev の選択に対する確信度が低ければ、安全策として強力なモデルを使う。
- それ以外の場合は、Jev が選んだモデルを使う。
1if (complexity.score >= policy.escalateAtComplexity) return powerful;2if (tierAnswer.confidence < policy.minConfidence) return policy.fallbackTier;3return tierAnswer.choice;
これは policy の一例です。あなたのユースケースに合わせて、回答の重み付けを変えることができます。たとえば、大量処理のバッチジョブなら安い方に寄せたり、本番環境に触れるものは常にエスカレーションしたりといった具合です。Jev はあくまで回答を提供するだけで、それをどう使うかはあなたのコードが決めます。
なぜ 1 回だけ選ぶのか
ルーターはリクエスト開始時に 1 回だけモデルを選び、完了までそれを使い続けます。これは prompt caching によるものです。
エージェントが 1 ステップ進むたびに、モデルはこれまでの会話全体を読み直します。AI プロバイダーは最近読まれた会話を保存しており、再読み込みのコストを抑えています。しかし、保存先はモデルごとに独立しています。途中でモデルを切り替えると、新しいモデルはすべてを通常料金で読み直す必要があります。
Jev の創設者は、コーディングエージェントに関する設計ドキュメント でこの計算を示しています。ある長いセッションにおいて、Claude Opus からより安い Sonnet へ切り替えて戻した場合、ずっと Opus を使い続けたときより約 50% コストが高くなりました。したがって、会話がまだ短いうちに最初にモデルを選び、そのまま使い続けるべきなのです。
3. Jev がダウンした場合に備える
このセクションでは、ゲートとルーターの両方を変更します。
harness がすべての tool call について Jev に質問するようになると、エージェントは Jev に依存します。他のオンラインサービスと同様に、Jev も遅くなったりダウンしたりする可能性があります。回答が得られない場合に各部分がどう振る舞うかを、事前に決めておきましょう。適切な対応は部分ごとに異なります。
ゲートは呼び出しをブロックする。 ゲートが Jev に質問できない場合、その呼び出しが安全かどうかはまったくわかりません。通してしまえばファイルが削除される恐れがあるため、ゲートは拒否します。エンジニアはこれを fail closed(停電時にロックがかかるドアのような挙動)と呼びます。
ルーターは強力なモデルを使う。 ルーターが Jev に質問できない場合、リクエストの難易度はわかりません。強力なモデルなら何でも処理できるため、リクエストにはきちんとした回答が返り、コストが少し増えるだけです。これは fail open であり、処理を続行させるアプローチです。

4. 回答を検証する
このセクションでは verifier(検証器)を追加します。エージェントの harness において、verifier とは、エージェントの作業を「完了」とみなす前にチェックするステップのことです。
エージェントは、何かを抜け落とした回答や、実際にはファイルで確認していないことを断定してしまう回答で終了することがあります。読む側はそれに気づけないことがよくあります。回答を返す前に検証すれば、エージェントがやり直せる段階でこれを捕捉できます。
verifier は、完成した回答とともに、その根拠となったファイルやツールの結果を Jev に送ります。Jev は回答の品質をスコア化し、その主張が grounded(裏付けがある)、つまりエージェントが実際に読んだ内容に基づいているかを判定します。

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};
エージェントが無限にリトライしないよう、2 つのルールを設けています。合計で最大 2 回までしか試行できません。また、Jev が自身の評価に確信を持てない場合、harness はもう一度コストを払ってやり直すのではなく、その回答を受け入れます。
安全性とログ
Jev が返すのは確率であり、確率は間違えることがあります。そのため、通常のコードで確実にチェックできることは、コードでチェックすべきです。この harness では、Jev が何と言おうと、すべてのファイル操作ツールがプロジェクトフォルダ外のパスを拒否します。Jev は、コードでは下せない判断のために取っておきましょう。
ゲートは tool call 自体、つまりツール名とその入力のみを見ます。これは、ファイルや Web ページに隠されたテキストがモデルを騙して有害な動作をさせる prompt injection 対策に役立ちます。ゲートはトリック自体を見ることはありませんが、それが引き起こす有害な呼び出しは確実に捉えます。
すべての判断と、その根拠となった数値をログに記録しましょう。ログがあれば、なぜブロックされたのかが説明でき、閾値を設定するための実際の数値もわかります。harness は 1 つの判断につき 1 行を decisions.jsonl というファイルに書き出します。ゲートはエージェントが試みるすべての操作を見るため、ログではメールアドレスやキーを隠し、長い入力は短縮しています。
試してみる
以下のサンドボックスでは、本チュートリアルで完成した harness が実際の Jev に接続された状態で動きます。登山道の調査メモが入ったフォルダを対象とし、delete_path ツールは本当にファイルを削除できます。
こちらでお試しください:https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
なぜ自作の harness を作るのか
既製のエージェントは、組み込みのルールでこうした判断を行います。カスタム harness では、それを自分のコードの中に置けます。モデルを選び、閾値を設定し、人間が介入するタイミングを決め、ログから各呼び出しが行われた理由を正確に読み取ることができます。
それを実用的にしてくれるのが Jev です。1 つの判断にかかる時間は数百ミリ秒、コストは 1 セントのごく一部なので、フルのモデル呼び出しが予算的に許される場所だけでなく、必要なところならどこにでもチェックを追加できます。本チュートリアルの 3 つの部分は出発点に過ぎません。あなた自身の領域向けのカスタム harness で、そこで重要な質問を Jev に投げかけてみてください。
その他の活用法
同じ 3 つの部分は、コーディングエージェント以外でも使えます。
- コードレビュー bot: 提案された変更ごとにスコアを付け、読む価値のあるものだけを人間に表示する。
- レコードの整理: 2 つのレコードが同じものを指しているか統合前に質問し、判断がつかないペアは人間に任せる。
- ドキュメントパイプライン: 抽出されたページごとにスコアを付け、スコアの低いものだけを再処理する。
- 承認キュー: 「人間に尋ねる」範囲に入った呼び出しだけが、人間のレビュアーに届く。
いずれの場合も、言語モデルが自由形式の作業を行い、Jev がその周囲の小さな質問に答えます。
本チュートリアルで紹介した質問、閾値、policy は学習用の例であり、チューニング済みの本番設定ではありません。現在、これらの harness の変更がコストと回答品質にどう影響するかをベンチマークしており、その結果をまとめた続編ガイドも近日公開予定です。
Opus 5.5 と一緒に一晩かけて、このガイドとサンドボックスを作り上げました。問題を見つけた方は DM で教えてください。この記事の内容をコピーして自分のエージェントに渡し、アイデアの実験を続けてもらっても構いません。





