AI 에이전트는 루프 형태로 작동하는 언어 모델입니다. 작업을 읽고, "이 파일 읽기"나 "저 파일 삭제하기" 같은 도구를 사용하고, 결과를 확인한 뒤 작업이 끝날 때까지 이 과정을 반복합니다. 모델이 도구 사용을 요청할 때마다 그 요청을 툴 콜(tool call) 이라고 부릅니다.
이 루프를 실행하는 코드를 하네스(harness) 라고 합니다. 무엇을 할지는 모델이 결정하고, 하네스는 이를 실행하면서 동시에 모델이 어디까지 할 수 있는지도 결정합니다.
좋은 하네스는 과정 전반에 걸쳐 수많은 작은 결정을 내립니다. 이 요청은 어떤 모델이 처리해야 할까? 이 툴 콜은 실행해도 안전할까? 이 답변은 사용자에게 넘겨줄 만큼 충분히 좋을까? 대부분의 하네스는 챗 모델에 질문하고 그 응답을 읽어오는 방식으로 이를 해결합니다. 하지만 매번 전체 모델 호출 비용이 발생하기 때문에, 실제로는 대부분의 검사가 생략되곤 합니다.
TypeSafe AI 의 Jev 는 오직 이런 결정을 위해 만들어진 소형 모델입니다. 상황을 설명하고 몇 가지 질문을 던지면, Jev 는 각 질문에 숫자로 답합니다. 텍스트를 생성하는 일은 절대 없습니다.
이러한 특징은 기성 에이전트 대신 나만의 에이전트 루프인 커스텀 하네스를 구축할 때 가장 빛을 발합니다. 커스텀 하네스를 사용하면 어떤 모델을 실행할지, 에이전트가 어디까지 접근할 수 있는지, 무엇이 '완료' 상태인지 직접 선택할 수 있습니다. Jev 는 이러한 선택의 근거가 되는 검사 비용을 크게 낮춰주어, 모든 단계에서 부담 없이 실행할 수 있게 해줍니다.
이 튜토리얼에서는 에이전트 구축용 TypeScript 툴킷인 Pi SDK 로 하네스를 만들고, 세 곳에서 Jev 를 활용해 봅니다. 마지막에는 완성된 하네스를 라이브 샌드박스에서 실행하고 설정을 직접 변경해 볼 수 있습니다.
전체 인터랙티브 튜토리얼과 플레이그라운드는 여기서 확인할 수 있습니다:
https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
이 가이드는 LangChain 블로그에 게재된 Sydney Runkle 의 Building a Harness with Jev 에서 영감을 받았습니다. 해당 글에서는 모델 라우팅과 도구 게이트를 LangChain 미들웨어 형태로 바로 사용할 수 있도록 소개했습니다. 여기서는 Pi SDK 위에서 동일한 개념을 직접 구현하고, 여기에 실패 처리와 답변 검증 패턴 두 가지를 추가로 다룹니다.
무엇을 만들 것인가
하네스는 세 부분으로 구성됩니다. 각 부분은 서로 다른 시점에 Jev 에게 질문을 던지며, 이 가이드의 나머지 부분에서는 아래 이름들로 이들을 지칭합니다.

예제로는 등산로 조사 기록 폴더에서 작업하는 에이전트를 사용합니다. 외출 한 번당 파일 하나가 존재하며, 에이전트는 이 기록을 읽고 쓸 수 있고 실제로 삭제할 수도 있기 때문에 게이트가 중요합니다.
Jev 란 무엇인가
TypeSafe 는 Jev 를 시스템 1(System One) 모델이라고 부릅니다. 이 명칭은 두 가지 사고 모드를 설명한 심리학자 Daniel Kahneman 에서 유래했습니다. 시스템 1 은 프라이팬이 뜨겁다는 것을 아는 것처럼 빠르고 자동적입니다. 시스템 2 는 나눗셈을 계산하는 것처럼 느리고 신중합니다.
이 하네스에서 일반 언어 모델은 파일을 읽고 답변을 작성하는 느린 작업을 담당합니다. Jev 는 그 주변에서 빠른 판단을 내립니다. Jev 는 저렴하고 빠르기 때문에 위험할 것으로 예상되는 툴 콜뿐만 아니라 모든 툴 콜에 대해 질문할 수 있습니다.

각 숫자는 0 과 1 사이의 확률입니다. 0.83 은 Jev 가 '예'라고 꽤 확신한다는 뜻이고, 0.03 은 '아니오'라고 꽤 확신한다는 뜻입니다.
세 가지 질문 유형
모든 Jev 요청은 두 부분으로 나뉩니다. state(상태) 는 툴 콜이나 사용자 요청처럼 판단을 받고 싶은 상황입니다. questions(질문) 은 그 상황에 대해 알고 싶은 내용입니다. Jev 는 한 번의 호출로 모든 질문에 동시에 답하므로, 질문 세 개를 던져도 하나를 던지는 것과 거의 같은 시간이 걸립니다.
Jev 는 세 종류의 질문을 지원합니다. 첫 번째는 Jev 가 noul 이라고 부르는 예/아니오 질문입니다.

Jev 는 사용자가 알려준 정보만 알 수 있으므로, 모든 옵션과 단계를 평이한 문장으로 설명해야 합니다. 이 설명들이 곧 프롬프트가 됩니다.
설정
Jev 는 하나의 API 키로 다양한 AI 모델을 제공하는 서비스인 OpenRouter 를 통해 이용할 수 있습니다. 이 키 하나로 언어 모델과 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 가 답할 때까지 대기하므로, 호출은 2 초 후에 타임아웃됩니다. 일반적인 응답에는 200~400 밀리초가 소요됩니다.
Jev 가 연결되는 위치
Pi 의 Agent 클래스가 루프를 대신 실행해 줍니다. 이 루프의 정해진 시점에 사용자 코드가 실행될 수 있도록 허용하는데, 이 지점들을 훅(hook) 이라고 부릅니다. 하네스는 세 부분에 각각 하나의 훅을 사용합니다.

첫 번째 게이트
가장 작고 유용한 형태의 게이트부터 시작해 보겠습니다. 모든 툴 콜 전에 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: "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), 즉 하네스가 호출을 더 이상 신뢰하지 않는 기준점입니다. Jev 가 이 값 이상으로 평가하면 모두 차단됩니다.
이제 메모를 삭제하려는 에이전트는 실제 삭제가 일어나기 전에 멈춥니다. 메모 삭제는 약 0.83 점, 읽기는 0.01 점을 받습니다. 다만 이 게이트는 다소 투박합니다. 완전히 새로운 파일을 작성하는 것도 약 0.70 점을 받아 차단됩니다.
질문하는 데 드는 비용은 매우 적습니다. Jev 는 입력 토큰 100 만 개당 $0.042 를 청구하는데, 이는 이 하네스가 사용하는 두 모델 중 더 저렴한 GLM 5.3 Flash 의 입력 가격보다 3 분의 1 미만입니다.
첫 번째 게이트에서 완전한 하네스로
첫 번째 게이트는 잘 작동하지만 네 가지 빈틈이 있습니다.
- 임계값이 훅 내부에 묻혀 있어 조정하거나 테스트하기 어렵습니다.
- 쉬운 요청이든 어려운 요청이든 모든 요청이 동일한 모델에서 실행됩니다.
- Jev 에 연결할 수 없을 때 어떻게 되는지 정의되어 있지 않습니다.
- 최종 답변의 품질을 검사하는 과정이 없습니다.
아래 번호가 매겨진 각 섹션에서 이 빈틈을 하나씩 메워보겠습니다.
1. 임계값을 한곳에 모으기
이 섹션에서는 게이트를 개선합니다.
임계값은 에이전트가 무엇을 할 수 있는지를 결정하며, 실제 결과를 확인하게 되면 자주 조정하게 됩니다. 따라서 이를 훅 밖으로 빼내 decideGate() 라는 단순한 함수로 옮깁니다. 이 함수는 Jev 의 숫자를 받아 판정(verdict), 즉 해당 호출에 대한 하네스의 최종 결정을 반환합니다. 이러한 규칙을 한곳에 모아두는 것을 정책(policy) 이라고 부릅니다.
정책은 중간 옵션도 추가합니다. 임계값이 하나면 허용 또는 차단만 가능합니다. 임계값이 두 개면 세 가지 판정이 생깁니다.
- 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("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};
라우터의 정책은 이 두 답변을 다음과 같이 활용합니다.
- 복잡도 점수가 높으면 Jev 가 fast 를 선택했더라도 강력한 모델을 사용합니다.
- 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 사이를 오간 경우, 내내 Opus 만 사용한 것보다 비용이 약 50% 더 들었습니다. 따라서 대화가 아직 짧을 때 시작 단계에서 모델을 선택하고 그대로 유지하세요.
3. Jev 장애에 대비하기
이 섹션에서는 게이트와 라우터를 모두 변경합니다.
하네스가 모든 툴 콜에 대해 Jev 에게 질문하게 되면, 에이전트는 Jev 에 의존하게 됩니다. 다른 온라인 서비스와 마찬가지로 Jev 도 느려지거나 다운될 수 있습니다. 답변을 받지 못했을 때 각 부분이 어떻게 행동할지 미리 정해두세요. 올바른 선택은 부분마다 다릅니다.
게이트는 호출을 차단합니다. 게이트가 Jev 에게 질문할 수 없다면 해당 호출이 안전한지 알 방법이 없습니다. 통과시켰다가는 파일이 삭제될 수 있으므로 게이트는 거부합니다. 엔지니어들은 이를 페일 클로즈드(failing closed), 즉 정전 시 잠기는 문에 비유합니다.
라우터는 강력한 모델을 사용합니다. 라우터가 Jev 에게 질문할 수 없다면 요청의 난이도를 알 수 없습니다. 강력한 모델은 무엇이든 처리할 수 있으므로 요청은 여전히 좋은 답변을 얻고, 비용만 조금 더 지불하면 됩니다. 이는 페일 오픈(failing open), 즉 작업을 계속 진행시키는 방식입니다.

4. 답변 검증하기
이 섹션에서는 검증기를 추가합니다. 에이전트 하네스에서 검증기(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};
에이전트가 무한히 재시도하는 것을 막기 위한 두 가지 규칙이 있습니다. 총 시도 횟수는 최대 두 번으로 제한됩니다. 또한 Jev 가 스스로 매긴 점수에 확신이 없을 때는, 비용을 들여 다시 시도하기보다 하네스가 해당 답변을 수용합니다.
안전성과 로깅
Jev 는 확률을 제공하며, 확률은 틀릴 수 있습니다. 따라서 일반 코드로 확실하게 검사할 수 있는 것은 코드로 검사해야 합니다. 이 하네스에서 모든 파일 도구는 Jev 가 뭐라고 하든 프로젝트 폴더 밖의 경로를 거부합니다. 코드가 내릴 수 없는 판단에만 Jev 를 아껴두세요.
게이트는 도구 이름과 입력값, 즉 툴 콜 자체만 살펴봅니다. 이는 파일이나 웹 페이지에 숨겨진 텍스트가 모델을 속여 유해한 행동을 하게 만드는 프롬프트 인젝션 방어에 도움이 됩니다. 게이트는 속임수 자체는 보지 못하지만, 그로 인해 발생하는 유해한 호출은 감지합니다.
모든 결정과 그 근거가 된 숫자를 함께 로그로 남기세요. 로그는 무언가가 차단된 이유를 설명하고, 임계값을 설정하기 위한 실제 수치를 보여줍니다. 하네스는 결정 하나당 한 줄씩 decisions.jsonl 파일에 기록합니다. 게이트는 에이전트가 시도하는 모든 것을 보기 때문에, 로그는 이메일 주소와 키를 숨기고 긴 입력값을 축약합니다.
직접 사용해 보기
아래 샌드박스는 이 튜토리얼에서 완성한 하네스를 실제 Jev 와 연결하여 실행합니다. 등산로 조사 기록 폴더를 대상으로 작동하며, delete_path 도구는 실제로 해당 파일들을 삭제할 수 있습니다.
여기서 체험해 보세요: https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
나만의 하네스를 만들어야 하는 이유
기성 에이전트는 자체 내장 규칙으로 이러한 결정을 내립니다. 커스텀 하네스는 이를 여러분의 코드 안에 둡니다. 모델을 직접 고르고, 임계값을 설정하고, 사람이 언제 개입할지 결정하며, 로그를 통해 각 호출이 이루어진 정확한 이유를 확인할 수 있습니다.
Jev 는 이를 현실적으로 가능하게 만듭니다. 각 결정에는 수백 밀리초가 걸리고 비용은 1 센트의 극히 일부이므로, 전체 모델 호출 비용을 감당할 수 있는 곳이 아니라 작업상 검사가 필요한 곳이라면 어디든 체크를 추가할 수 있습니다. 이 튜토리얼의 세 부분은 출발점일 뿐입니다. 여러분 도메인의 커스텀 하네스라면 그곳에서 중요한 어떤 질문이든 Jev 에게 던질 수 있습니다.
다른 활용 사례
동일한 세 부분은 코딩 에이전트 외부에서도 작동합니다.
- 코드 리뷰 봇. 제안된 변경 사항마다 점수를 매기고, 읽어볼 만한 항목만 사람에게 보여줍니다.
- 레코드 정리. 두 레코드를 병합하기 전 동일한 대상을 설명하는지 질문하고, 애매한 쌍은 사람에게 맡깁니다.
- 문서 파이프라인. 추출된 각 페이지에 점수를 매기고, 낮은 점수의 페이지만 다시 실행합니다.
- 승인 대기열. 사람에게 물어보는 범위에 해당하는 호출만 인간 검토자에게 전달됩니다.
어떤 경우든 언어 모델은 자유 형식의 작업을 수행하고, Jev 는 그 주변의 작은 질문에 답합니다.
이 튜토리얼의 질문, 임계값, 정책은 학습용 예시이며 프로덕션 환경에 맞춰 튜닝된 설정이 아닙니다. 저희는 이러한 하네스 변경이 비용과 답변 품질에 미치는 영향을 벤치마킹하고 있으며, 해당 결과가 담긴 후속 가이드를 곧 공개할 예정입니다.
Opus 5.5 와 함께 저녁 시간을 보내며 이 가이드와 샌드박스를 만들었습니다. 문제가 발생하면 DM 으로 연락해 주세요. 이 글을 복사해서 에이전트에게 전달하고 아이디어를 계속 실험해 보셔도 좋습니다.





