에이전트 하네스(harness)를 하나 만들고 나면, 늘 같은 질문에 걸린다. “이건 뭐 하는 물건이냐” 를 나 자신에게, 그리고 반년 뒤의 나에게 어떻게 설명할 것인가. 코드는 어떻게(how) 를 말하지만 왜, 언제(why, when) 는 말해주지 않는다. 그래서 나는 하네스마다 두 장짜리 문서 템플릿을 강제한다. 하나는 하네스가 무엇인지 압축하는 Overview 카드, 다른 하나는 하네스가 실제로 어떻게 흐르는지 못 박는 Workflow Layer.

아래 한 장이 그 템플릿이다.

Runtime-independent Harness Demo 작성 템플릿 — 상단은 항목/내용 2열의 Harness Overview 표(해결하려는 실패·사용할 상황·사용하지 않을 상황·최종 산출물·최종 판단자), 하단은 requirements_contract부터 route까지 화살표로 이어지는 Workflow Layer 파이프라인

두 조각은 성격이 정반대다. Overview는 산문이고, Workflow Layer는 기계다. 그런데 좋은 하네스는 이 둘이 정확히 맞물릴 때만 나온다. 하나씩 뜯어보자.


1. Harness Overview — 다섯 줄로 하네스를 가두기

Overview 카드는 항목 | 내용 2열 표에 딱 다섯 줄이다. 줄을 늘리고 싶은 유혹이 늘 있지만, 다섯 줄로 못 설명하는 하네스는 아직 스스로를 이해하지 못한 하네스다.

항목 이게 강제하는 질문
해결하려는 실패 이 하네스가 없을 때 구체적으로 무엇이 깨지는가
사용할 상황 어떤 입력·맥락에서 이걸 꺼내 드는가
사용하지 않을 상황 어디서부터는 이게 틀린 도구 인가
최종 산출물 끝났을 때 손에 쥐는 물건은 정확히 무엇인가
최종 판단자 “됐다/안 됐다”를 누가 선언하는가

앞의 두 줄은 쉽다. 진짜 설계가 드러나는 건 뒤의 세 줄이다.

“해결하려는 실패”를 실패로 적는 것. “요약을 해준다”가 아니라 “긴 로그를 사람이 직접 읽다가 핵심을 놓치는 실패” 라고 적는다. 기능이 아니라 실패로 정의하면, 하네스의 존재 이유가 검증 가능한 명제가 된다. 그 실패가 더는 안 일어나면 하네스는 성공한 것이고, 그 실패가 아예 없는 곳에선 하네스가 불필요한 것이다.

“사용하지 않을 상황”이 왜 필수인가. 대부분의 문서는 이 줄을 뺀다. 그런데 경계는 바깥 을 그려야 완성된다. “언제 안 쓰는가”를 명시하지 않은 도구는 반드시 오용된다 — 사람은 손에 든 망치로 모든 걸 못으로 본다. 이 줄은 미래의 나(혹은 다른 에이전트)가 이 하네스를 엉뚱한 데 끌어다 쓰는 걸 막는 음(陰)의 스펙이다.

“최종 판단자”가 사람인가 기계인가. 이게 하네스의 성격을 결정한다. 판단자가 결정론적 검증(테스트 통과, 스키마 일치)이면 하네스는 완전 자동 으로 돌 수 있다. 판단자가 사람이면 하네스는 반드시 멈춰서 물어보는 지점(ask-back) 을 가져야 한다. 이 한 줄이 아래 Workflow Layer의 마지막 route가 어떤 갈래를 가질지를 미리 규정한다.

Overview 카드의 다섯 줄은 장식이 아니다. 그건 하네스가 통과해야 할 계약(contract) 의 요약본이고, 실제 그 계약이 코드 안에서 흐르는 모습이 다음 절이다.


2. Workflow Layer — 계약이 흐르는 결정론적 파이프라인

Overview가 산문이라면 Workflow Layer는 한 줄도 흐릴 수 없는 기계 도면이다. 화살표 하나하나가 명시적 단계이고, 각 단계는 앞 단계의 출력을 받아 다음으로 넘긴다.

requirements_contract
  -> spec_contract
  -> session_surface
  -> structured_contract
  -> state_store.load_or_init
  -> fill_handoff
  -> dispatch_plan
  -> invoke_runtime_adapter
  -> collect_handoff
  -> review_gate
  -> review_parallel( L5 citation | L6 domain/scope/FN )
  -> merge_verdict
  -> route( exit / retry / ask-back / blocked exit )

이 사슬은 세 개의 국면(局面)으로 읽힌다.

(1) 계약을 굳히는 앞머리 — requirements_contractstructured_contract

파이프라인은 코드가 아니라 계약 으로 시작한다.

  • requirements_contract — 사람의 모호한 요구를 명제로 고정한다. Overview의 “해결하려는 실패”가 여기로 들어온다.
  • spec_contract — 요구를 검증 가능한 스펙으로 좁힌다. “무엇을 만족해야 성공인가”가 문서가 아니라 데이터가 된다.
  • session_surface — 이번 실행이 볼 수 있는 표면(입력·맥락·도구)을 명시적으로 노출한다. 숨은 상태를 허용하지 않겠다는 선언이다.
  • structured_contract — 위 셋을 하나의 구조화된 계약 객체로 합친다. 이후 모든 단계는 산문이 아니라 이 객체를 주고받는다.

핵심은 순서다. 런타임을 부르기 훨씬 전에 계약이 먼저 굳는다. 모델이 무슨 말을 하기 전에, 무엇을 만족해야 하는지가 이미 데이터로 존재한다. 이게 하네스가 “그럴듯하지만 요구를 빗나간 출력”을 걸러내는 첫 번째 방어선이다.

(2) 상태를 봉인하고 런타임에 넘기는 허리 — state_store.load_or_initcollect_handoff

  • state_store.load_or_init — 상태는 세션이 아니라 바깥 스토어 에 산다. 있으면 로드, 없으면 초기화. 하네스는 stateless하고, 상태는 명시적으로 로드된다. (이전 글에서 다룬 세션으로부터의 결합 끊기가 정확히 이 지점이다.)
  • fill_handoff — 런타임에 넘길 인계장(handoff)을 채운다. 계약 + 로드된 상태가 여기서 하나의 자기완결적 패킷이 된다. “아까 그거”에 기대지 않고, 이번 호출이 필요한 걸 전부 들고 간다.
  • dispatch_plan — 실제 무엇을 시킬지 계획으로 확정한다.
  • invoke_runtime_adapter여기가 유일하게 벤더에 닿는 지점이다. Claude Code든 Codex CLI든, 하네스는 어댑터 인터페이스 하나로만 대화한다. 런타임은 교체 가능한 부품이고, 파이프라인의 나머지는 어떤 런타임이 돌았는지 모른다.
  • collect_handoff — 런타임의 출력을 다시 구조화된 인계장으로 회수한다. 자유 텍스트가 아니라 계약이 기대한 형태로.

이 허리 구간의 미학은 런타임 접점을 한 점으로 좁혔다 는 데 있다. 앞은 계약, 뒤는 검증, 그 사이 딱 한 칸만 벤더에 종속된다. 그 한 칸을 갈아끼우면 하네스 전체가 다른 런타임 위에서 그대로 돈다.

(3) 판단하고 갈라지는 꼬리 — review_gateroute

출력이 나왔다고 끝이 아니다. Overview의 “최종 판단자”가 여기서 실행된다.

  • review_gate — 검토를 통과해야만 다음으로 간다. 게이트다, 통로가 아니다.
  • review_parallel( L5 citation | L6 domain/scope/FN ) — 검토가 병렬이라는 게 중요하다. 서로 다른 축이 독립적으로 본다. L5는 인용·근거가 실제로 붙어 있는지(citation)를, L6는 도메인·범위·거짓양성(domain/scope/FN)을 본다. 하나의 검토자가 모든 걸 보면 놓치지만, 축을 분리하면 각자의 실패 모드에 집중한다.
  • merge_verdict — 병렬 판정을 하나의 평결로 합친다. 어느 축이라도 막으면 통과가 아니다.
  • route( exit / retry / ask-back / blocked exit ) — 평결에 따라 갈라진다.
    • exit — 계약 충족, 정상 종료. 최종 산출물을 손에 쥔다.
    • retry — 고칠 수 있는 실패. 파이프라인 앞으로 되돌린다.
    • ask-back사람에게 되묻는다. Overview의 “최종 판단자”가 사람일 때 반드시 필요한 갈래.
    • blocked exit — 자동으로도 사람으로도 풀 수 없는 막힘. 조용히 실패를 삼키지 않고 막혔다고 명시하며 끝낸다.

마지막 네 갈래가 왜 결정적인가. 나이브한 하네스는 성공/실패 두 갈래만 갖는다. 그런데 현실의 실패는 되돌릴 수 있는 실패, 사람이 결정해야 하는 실패, 아무도 못 푸는 실패 로 나뉜다. 이 셋을 한 갈래로 뭉개면, 하네스는 조용히 틀린 답을 내거나 무한 재시도에 빠진다. route가 네 갈래인 건 실패에도 종류가 있다는 걸 인정하는 설계다.


3. 두 장이 맞물리는 지점

Overview와 Workflow Layer는 따로 노는 두 문서가 아니다. 앞이 뒤를 규정한다.

Overview 항목 대응하는 Workflow 단계
해결하려는 실패 requirements_contract
사용할 상황 / 사용하지 않을 상황 session_surface (표면을 명시 → 경계를 강제)
최종 산출물 route → exit
최종 판단자 review_parallel + route(ask-back)

Overview에서 “최종 판단자 = 사람”이라 적었다면, Workflow에는 반드시 ask-back 갈래가 살아 있어야 한다. Overview에서 “사용하지 않을 상황”을 명시했다면, session_surface가 그 바깥의 입력을 애초에 표면에 올리지 않아야 한다. 문서가 코드를 검증하고, 코드가 문서를 배신하지 못하게 하는 게 이 두 장을 나란히 두는 이유다.

하네스의 진짜 산출물은 출력이 아니다. “이 출력을 왜 믿어도 되는가” 에 대한 계약과 검증의 사슬이다. Overview 카드가 그 계약을 다섯 줄로 약속하고, Workflow Layer가 그 약속이 실제로 지켜지는 경로를 한 줄도 빠짐없이 못 박는다.

런타임은 갈리고, 모델은 바뀌고, CLI는 이름을 바꾼다. 그래도 이 두 장은 남는다 — 계약과 파이프라인은 어떤 런타임 위에서도 같은 모양이기 때문이다. 그게 runtime-independent 하네스가 결국 지키려는 것이다.