하네스는 알았지만 속은 몰랐습니다 — OpenCode 2와 Hermes로 배운 코딩 에이전트의 구조와 흐름#

2026-10-25

책상 위 지도 위에 덮개를 연 기계 두 대가 놓여 있고, 두 기계 모두 똑같이 빛나는 고리 모양 엔진을 품고 있습니다. 왼쪽 기계는 리벳 박힌 금고 같은 외피와 서류 서랍에, 오른쪽 기계는 자라나는 덩굴과 편지를 문 비둘기에 둘러싸여 있고, 작은 로봇이 돋보기로 두 기계의 속을 들여다보는 그림

뉴스 브리프에서 시작된 질문#

지난주 2026-10-18 AI 뉴스 브리프에 OpenCode 2.0 소식을 적었습니다. OpenCode는 터미널에서 쓰는 오픈소스 코딩 에이전트입니다. 2.0에서는 터미널, 웹, 데스크톱 앱이 모두 하나의 백그라운드 서버에 붙는 구조로 바뀌었고, MCP(Model Context Protocol, 에이전트에 외부 도구를 연결하는 표준) 서버의 도구를 작은 JavaScript로 엮어 부르는 Code Mode와 백그라운드 하위 에이전트가 들어왔습니다.

정리하다 보니 질문이 하나 생겼습니다. 필자가 매일 쓰는 Hermes Agent와는 뭐가 다를까? 필자는 Nous Research의 오픈소스 에이전트 Hermes를 두 곳에서 씁니다. 직접 만든 PR 에이전트는 Hermes 위에서 돌며 Discord에 연결해 두었고, 회사 업무용 Hermes 서버는 Slack과 웹챗에 연결해 팀이 함께 씁니다. 상주 서버, 백그라운드 작업, 하위 에이전트는 Hermes에서 이미 쓰고 있는 것들이라, OpenCode 2.0의 변화가 Hermes와 같은 방향인지 다른 방향인지 궁금했습니다.

그런데 막상 비교하려니 기준이 없었습니다. 하네스 엔지니어링의 개념은 알고 있었습니다. 지침 파일, 훅, 검증 루프로 에이전트를 안전하게 굴리는 방법은 이 블로그에도 여러 번 썼습니다. 코딩 에이전트의 주요 기능도 압니다. 하지만 코딩 에이전트가 안에서 어떤 부품으로 구성되어 있고, 요청 하나가 어떤 순서로 흘러가는지는 직접 확인해 본 적이 없었습니다. 기능 목록을 나란히 놓을 수는 있어도, 그 차이가 어디서 왜 생기는지는 설명할 수 없는 상태였습니다.

그래서 순서를 바꿨습니다. 비교보다 감 잡기를 먼저 하기로 했습니다. 두 프로젝트의 최신 소스 코드를 받아 AI와 함께 읽으며 코딩 에이전트의 공통 구조와 흐름을 익히고, 그 지도를 들고 두 에이전트를 각각 읽은 뒤, 마지막에 비교했습니다. 이 글은 그 학습 기록입니다.

기준 시점 · OpenCode v2.1.4와 Hermes Agent v0.22.5(모두 2026-10-23 태그)의 소스 코드 기준입니다. Hermes는 필자가 평소 운영하는 에이전트이고, 이번에 새로 한 것은 그 내부를 코드로 들여다본 일입니다. 내부 동작은 코드와 주석으로 확인했으며, 로그나 디버거로 실행 과정을 직접 따라가지는 않았습니다.

출발점: API 호출 한 번에는 무엇이 없나#

LLM API를 직접 호출해 보면 그것이 함수 하나와 같다는 것을 알게 됩니다. 대화 전체를 텍스트로 보내면 다음 응답 하나가 텍스트로 돌아옵니다. 여기에는 세 가지가 없습니다.

  • 손이 없습니다. 모델은 파일을 읽거나 명령을 실행하지 못합니다. “이렇게 고치세요"라고 말할 뿐입니다.
  • 기억이 없습니다. API는 상태를 갖지 않습니다. 이전 대화를 기억하는 것처럼 보이는 것은 매번 대화 전체를 다시 보내기 때문입니다.
  • 반복이 없습니다. 한 번 답하면 끝납니다. 결과를 확인하고 다시 시도하지 않습니다.

비유하면 전화로만 연결된 뛰어난 전문가입니다. 조언은 훌륭하지만 현장에 손을 댈 수 없고, 통화가 끝나면 잊어버립니다. 코딩 에이전트는 이 세 빈칸을 채우는 프로그램입니다. 손은 도구로, 기억은 저장과 컨텍스트 조립으로, 반복은 루프로 채웁니다. 에이전트의 개념 자체는 책의 「에이전트와 하네스」 장에서 다뤘으므로, 여기서는 그 빈칸을 채우는 부품과 순서에 집중합니다.

코딩 에이전트의 기본 구성: 모델과 하네스#

빈칸을 채우는 프로그램을 하네스라고 부릅니다. 원래는 말에 채우는 마구를 뜻합니다. 힘은 말이 내고, 그 힘을 수레에 연결하고 방향을 잡는 것은 마구의 몫입니다. 코딩 에이전트에서는 모델이 판단하고, 하네스가 그 판단을 실제 세계에 연결합니다.

핵심은 역할 분담입니다. 모델은 “이 파일을 읽어 줘”, “이 명령을 실행해 줘"라고 요청만 합니다. 이 기능을 tool calling(도구 호출)이라고 부릅니다. 요청을 받아 실제로 실행하고 결과를 모델에게 다시 보여 주는 것은 하네스입니다. 두 프로젝트의 코드를 열어 보니 하네스는 대략 여섯 부품으로 나눌 수 있었습니다.

화면: 터미널 · 웹 · 데스크톱 · 메신저바깥: 하네스 엔지니어링지침 파일 · 훅 · 권한 규칙 · 검증안쪽: 하네스 내부루프 엔진끝날 때까지 반복컨텍스트 조립지침 파일 · 대화 기록도구함파일 · 셸 · 검색 · MCP권한 판정막기 · 묻기 · 허용저장소대화 · 진행 상태모델 어댑터제공자별 형식 변환LLM API
필자가 알던 하네스 엔지니어링은 바깥 점선 영역의 일이었고, 이번에 들여다본 것은 안쪽 상자의 부품들입니다.
부품하는 일
루프 엔진모델 호출과 도구 실행을 일이 끝날 때까지 반복합니다
컨텍스트 조립매 호출마다 모델에게 보낼 내용을 만듭니다. 시스템 지침, 프로젝트 지침 파일, 지금까지의 대화가 들어갑니다
도구함파일 읽기와 수정, 셸 명령, 검색, 웹 조회, MCP로 연결한 외부 도구를 갖춥니다
권한 판정위험한 요청을 막거나 사람에게 묻습니다
저장소대화와 진행 상태를 DB에 기록합니다
모델 어댑터하네스 내부 형식을 Anthropic, OpenAI, Gemini 같은 제공자별 API 형식으로 바꿉니다

이 그림을 그리고 나서야 필자가 알던 것과 몰랐던 것의 경계가 보였습니다. 필자가 해 온 하네스 엔지니어링은 지침 파일을 쓰고, 훅을 걸고, 권한 규칙을 정하는 일이었습니다. 모두 하네스의 바깥에서 하는 설정입니다. AGENTS.md를 쓰면 에이전트가 따른다는 것은 알았지만, 그 파일이 언제 어떤 순서로 읽혀 모델에게 들어가는지는 하네스 안쪽의 일이었고, 필자는 그 안을 본 적이 없었습니다.

기본 흐름: 요청 하나가 흘러가는 길#

“테스트가 실패하는 버그를 고쳐 줘"라는 요청이 들어오면 다음과 같이 흘러갑니다.

사용자: 테스트 실패 버그를 고쳐 줘
모델: 테스트를 돌려 볼게
  → 결과: 3번 테스트 실패
모델: foo.py를 읽어 볼게
  → 결과: 파일 내용
모델: 12번째 줄을 고칠게
  → 결과: 수정 완료
모델: 테스트를 다시 돌려 볼게
  → 결과: 모두 통과
모델: 고쳤습니다. 원인은 ...
  → 도구 요청 없음, 종료

모델은 매 단계에서 지금까지의 결과를 보고 다음 행동 하나만 정합니다. 화살표 뒤의 결과는 모두 하네스가 실제로 실행해서 얻은 것입니다. 목표에 닿을 때까지 이 왕복을 반복하는 것이 에이전트 루프이고, 코드로 줄이면 10줄 남짓입니다.

대화 = [시스템_지침, 사용자_요청]
while True:
    응답 = LLM(대화, 도구_목록)  # 매번 대화 전체
    대화.append(응답)
    if 응답에 도구 요청이 없으면:
        사용자에게 전달하고 종료
    for 요청 in 응답의_도구_요청들:
        if 위험하면: 사람에게 허락받기
        결과 = 실행(요청)  # 파일 수정, 명령 실행
        대화.append(결과)

이 10줄만으로도 에이전트는 돌아갑니다. 문제는 실제 프로젝트에서 오래 돌릴 때 생깁니다. 두 프로젝트 모두 핵심 코드만 십만 줄이 넘는데, 따라가 보면 상당 부분이 이 루프가 현실에서 부딪히는 문제를 막는 장치였습니다.

루프를 오래 돌리면 생기는 문제막는 장치
대화가 계속 길어져 모델의 컨텍스트 한도를 넘습니다압축: 오래된 부분을 요약으로 바꿉니다(compaction)
rm -rf 같은 명령도 그대로 실행합니다권한 판정: 위험한 요청을 막거나 사람에게 묻습니다
일이 커서 대화 하나에 다 담기 어렵습니다하위 에이전트: 새 대화에 맡기고 결과만 받습니다
프로그램이 꺼지면 진행 상황이 사라집니다저장: 단계마다 DB에 기록합니다
오래 걸리는 명령이 대화를 막습니다백그라운드 실행: 끝나면 알림으로 결과를 받습니다

여기까지가 필자가 얻은 지도입니다. 이제 이 지도를 들고 두 에이전트를 각각 읽어 보겠습니다.

OpenCode 2: 끊기지 않는 작업 현장#

OpenCode 2는 TypeScript로 작성되었고 Bun 런타임에서 돕니다. 공식 발표의 핵심은 서버가 이제 내 컴퓨터에서 하나의 백그라운드 프로세스로 자동 실행된다는 것입니다. 코드를 보면 opencode를 실행할 때 먼저 이미 떠 있는 서버를 찾고, 없으면 서버를 띄운 뒤 거기에 접속합니다. 서버는 기본 설정에서 내 컴퓨터 안에서만 접속을 받고, 터미널, 웹, 데스크톱 앱은 모두 이 서버에 HTTP로 요청을 보내고 실시간 이벤트를 받아 화면을 그리는 클라이언트입니다.

지도에 대입하면 OpenCode가 가장 공을 들인 곳은 루프 엔진과 저장소의 결합이었습니다.

  • 입력은 바로 처리되지 않고 먼저 DB의 수신함(inbox)에 한 줄로 기록됩니다. 실행 중에 새 요청이 들어오면 진행 방향을 바꾸거나(steer) 대기열에 넣습니다(queue).
  • 루프는 단계마다 대화 기록을 메모리에서 이어 쓰지 않고 DB에서 다시 읽습니다. 저장소 안의 설계 문서는 메모리 안에서만 도는 도구 루프를 만들지 말라고 명시합니다.
  • 실행을 시작하기 전에 “이 세션을 실행 중"이라는 기록을 먼저 남깁니다. 서버가 죽었다 살아나면 이 기록을 보고 이어서 실행합니다.

이 설계가 노리는 것은 분명합니다. 창을 닫아도, 서버가 재시작되어도 작업이 끊기지 않는 것입니다. 브리프에 적은 백그라운드 기능도 같은 맥락이었습니다. 셸 명령과 하위 에이전트를 백그라운드로 돌리면 바로 응답이 돌아오고, 끝났을 때 결과가 부모 대화에 메시지로 들어옵니다. 백그라운드 작업 정보도 저장되어 재시작 뒤에 이어집니다. 도구 실행 방식에서도 눈에 띄는 점이 있었습니다. 모델의 응답이 스트리밍으로 들어오는 도중에 도구 요청 하나가 완성되면, 응답 전체를 기다리지 않고 바로 실행을 시작합니다.

Code Mode는 코드로 확인하니 공식 발표와 정확히 일치했고, 모든 도구 호출을 바꾸는 기능은 아니었습니다. 파일 읽기와 수정, 셸 같은 내장 도구는 모두 일반 도구 호출로 남아 있습니다. MCP 서버와 플러그인의 도구만 하나의 execute 도구로 묶여, 모델이 작은 JavaScript로 엮어 부릅니다. 이 스크립트는 일반 JavaScript 엔진이 아니라 OpenCode가 직접 만든 제한된 인터프리터에서 돌고, 파일 시스템이나 프로세스에는 접근할 수 없습니다. 다만 그 안에서 쓸 수 있는 fetch는 파일 URL만 막혀 있고, 도구 권한 판정을 거치는 경로는 코드에서 찾지 못했습니다. 어떤 MCP 서버를 연결하느냐에 따라 의미가 달라질 수 있는 부분이라 기억해 두려 합니다.

권한은 “어떤 행동을, 어떤 대상에, 어떻게 처리할지"를 적은 규칙 목록으로 판정하고, 맞는 규칙이 없으면 사람에게 묻는 것이 기본값입니다. 사람이 거절하면서 이유를 적으면 그 피드백이 모델에게 전달되어 다른 방법을 찾습니다.

Hermes: 필자가 쓰던 기능은 어디서 나오고 있었나#

Hermes는 Python으로 작성되었습니다. 필자에게는 이 부분이 가장 흥미로웠습니다. 써 오면서 당연하게 여긴 동작들이 지도 위 어느 부품에서 나오는지 하나씩 대응되었기 때문입니다.

노트북을 접어도 일하는 이유: 게이트웨이가 루프를 소유합니다#

Hermes의 중심은 게이트웨이라는 상주 프로세스입니다. 프로필마다 하나씩 돌며, Discord, Slack, Telegram 같은 메신저 연결을 모두 이 프로세스가 맡습니다. 메신저 연결 플러그인만 27종이었습니다. 코드 주석은 CLI 프로세스가 “에이전트를 만들지도, 도구를 실행하지도, 상태 DB에 쓰지도 않는다"고 분명히 적어 두었습니다. 터미널에서 hermes를 실행해도 실제 대화는 게이트웨이 안에서 돌고, 터미널은 그 결과를 받아 보여 줄 뿐입니다.

필자가 Discord나 Slack에서 말을 걸 때도 같은 게이트웨이의 같은 루프가 돕니다. 필자가 Hermes를 클라우드 서버에 올려 노트북을 접어도 일하게 만들 수 있었던 것도 이 게이트웨이가 상주하는 구조이기 때문이었습니다.

Discord나 Slack으로 오는 승인 요청: 권한 판정의 마지막 단계입니다#

Hermes가 셸 명령을 실행하기 전에 거치는 판정은 생각보다 여러 겹이었습니다.

  1. 호스트 폴더를 연결하지 않은 Docker 컨테이너나 클라우드 샌드박스처럼 격리된 환경의 명령은 묻지 않습니다. 사용자가 정한 거부 규칙만 적용합니다.
  2. 시스템 디렉터리 삭제 같은 치명적 패턴과 사용자의 거부 규칙에 걸리면 무조건 막습니다.
  3. cron 작업처럼 사람이 지켜보지 않는 실행에서는 위험 패턴에 걸린 명령을 기본으로 거부합니다.
  4. 사람이 있는 대화에서는 위험 패턴에 걸리지 않은 명령을 그대로 실행합니다.
  5. 위험 패턴에 걸리면, 기본 설정(smart)에서는 보조 LLM이 먼저 승인, 거부, 사람에게 넘기기 중 하나를 판단합니다.
  6. 사람에게 넘어오면 그제야 필자가 쓰는 채널로 승인 요청이 옵니다. Discord나 Slack이라면 대화창에 승인 요청이 오고, 버튼이나 /approve, /deny로 답합니다.

필자에게 승인 요청은 메신저로 오는 메시지 하나였습니다. 실제로는 여러 겹의 판정을 통과하고도 판단이 서지 않은 것만 사람에게 오는 마지막 단계였습니다.

기억과 스킬이 쌓이는 이유: 대화가 끝난 뒤 도는 두 번째 루프#

Hermes가 다른 코딩 에이전트와 가장 다른 점이 여기에 있었습니다. 대화 루프가 끝나고 응답이 전달된 뒤, Hermes는 카운터를 확인합니다. 기본 설정에서는 사용자 턴 10번마다 메모리를, 도구 반복 10번마다 스킬을 검토할 때가 됩니다. 때가 되면 현재 대화를 복제한 에이전트를 백그라운드에서 띄워 “이번 대화에서 기억할 것이 있는가”, “스킬로 만들거나 고칠 것이 있는가"를 검토하게 합니다. 이 복제 에이전트는 메모리와 스킬 도구, 읽기 도구만 쓸 수 있습니다. 결과는 MEMORY.md, USER.md, 스킬 파일로 저장되고, 다음 대화를 시작할 때 시스템 지침에 들어갑니다.

대화 루프모델 호출도구 요청실행결과 추가끝날 때까지 반복한 뒤 응답 전달종료 후회고 루프백그라운드에서 실행턴 10번마다 메모리 검토도구 반복 10번마다 스킬 검토대화를 복제한 에이전트가메모리 · 스킬 도구로 작업MEMORY.md · USER.md · 스킬 파일파일로 저장다음 대화의지침에 반영
대화 루프가 끝난 뒤 회고 루프가 돌고, 그 결과가 다음 대화의 출발점이 됩니다.

사람이 없는 cron 작업에서는 이 회고 루프를 건너뜁니다. 코드 주석은 회고 한 번에 약 3만 토큰이 드는데 사람이 관여하지 않으니 얻을 것이 없다는 이유를 적어 두었습니다. 또 필자는 원격 개발 환경 글에서 에이전트가 만든 스킬도 사람이 한 번 읽고 승인하는 절차가 좋다고 적었는데, Hermes에는 이미 그런 옵션(write_approval)이 있었습니다. 켜면 메모리와 스킬 쓰기를 바로 반영하지 않고 검토 대기열에 올립니다. 기본값은 꺼져 있습니다.

그 밖에 코드에서 확인한 것#

  • 프로젝트 지침 파일은 하나만 읽습니다. .hermes.md, AGENTS.md, CLAUDE.md, .cursorrules 순서로 찾아 처음 찾은 종류 하나만 시스템 지침에 넣습니다. 여러 코딩 도구를 함께 쓰는 저장소라면 어느 파일이 실제로 들어가는지 알아 둘 만합니다.
  • README보다 코드가 앞서 있었습니다. README는 지난 대화 검색이 LLM 요약을 쓴다고 설명하지만, 현재 코드는 LLM을 부르지 않고 SQLite의 전문 검색만 씁니다. 빠르게 바뀌는 오픈소스에서는 문서보다 코드가 1차 자료였습니다.

나란히 놓고 보기: 같은 뼈대, 다른 강조#

두 에이전트를 지도에 대입하고 나니 처음에 하고 싶던 비교를 할 수 있었습니다. 먼저 같은 점입니다. 두 프로젝트 모두 루프의 뼈대는 앞의 10줄과 같았습니다. OpenCode의 단계 실행 함수와 Hermes의 대화 루프 함수를 열어 보면, 언어도 설계 철학도 다른데 같은 반복문이 들어 있습니다. 하위 에이전트의 깊이를 기본 1단계로 제한하고, 하위 에이전트의 중간 과정 대신 결과만 부모에게 돌려주는 점도 같았습니다.

다른 점은 루프를 둘러싼 장치에 있었습니다.

항목OpenCode 2Hermes
정체성코딩 에이전트메신저로 부르는 범용 에이전트, 코딩은 능력 중 하나
공을 들인 곳끊기지 않는 실행: DB 수신함, 단계마다 다시 읽기, 재시작 뒤 재개자라는 비서: 메신저 게이트웨이, 대화 뒤 회고 루프
도구 실행스트리밍 중 요청이 완성되면 바로 실행응답을 다 받은 뒤 함께 돌려도 안전한 것끼리 병렬 실행
압축 시작컨텍스트 창의 약 85%기본 50%, 창이 512K 미만인 모델은 75%
권한 판정규칙과 사람규칙, 보조 LLM, 사람
지식 축적사람이 관리하는 지침 파일과 스킬에이전트가 스스로 쓰는 메모리와 스킬
외부 도구 조합Code Mode (JavaScript)execute_code (Python 스크립트에서 도구 호출)

프로세스 구조와 실행 위치는 다른 축입니다#

두 에이전트가 모두 상주 서버 구조라는 점만 보면 “터미널 안에서 돌던 에이전트가 서버로 옮겨 갔다"고 한 줄로 요약하고 싶어집니다. 그런데 이 요약에는 서로 독립적인 두 축이 섞여 있습니다.

  • 프로세스 구조: 하네스가 화면을 그리는 앱 안에 들어 있는가(앱 내장형), 별도의 상주 프로세스로 떨어져 있는가(데몬 분리형)
  • 실행 위치: 그 프로세스가 내 컴퓨터에서 도는가(로컬), 원격 서버에서 도는가(리모트)
구분로컬리모트
앱 내장형노트북에서 실행한 터미널 에이전트원격 서버에 SSH로 접속해 실행한 터미널 에이전트
데몬 분리형OpenCode 2와 Hermes의 기본 설정클라우드 서버에 올린 필자의 Hermes, 클라우드형 코딩 에이전트

OpenCode 2와 Hermes는 기본 설정에서 모두 로컬 데몬입니다. OpenCode 서버는 내 컴퓨터 안에서만 접속을 받고, Hermes는 상태를 홈 디렉터리 아래(~/.hermes)에 둡니다. 그러니 OpenCode 2.0의 변화는 원격으로 간 것이 아니라 같은 컴퓨터 안에서 프로세스 구조를 바꾼 것입니다. 필자가 Hermes를 클라우드 서버에 올린 것은 실행 위치를 바꾼 별개의 선택이었고, 노트북을 접어도 일하게 된 것은 두 선택이 겹친 결과였습니다.

데몬 분리형이 앱 내장형보다 앞선 단계인 것도 아닙니다. 데몬 분리형은 창을 닫아도 작업이 이어지고 여러 화면이 같은 세션을 볼 수 있는 대신 비용이 따릅니다. OpenCode 2 코드에는 클라이언트와 서버의 버전이 다를 때 서버를 교체하는 로직, 여러 프로세스가 동시에 서버를 띄우려 할 때 하나만 남기는 경합 처리, 내 컴퓨터 안의 통신에도 붙는 인증이 들어 있습니다. 앱 내장형이 더 단순하고 충분한 상황도 많습니다. 둘은 발전 단계가 아니라 득실이 있는 설계 선택입니다.

맺으며: 비교의 기준이 생겼습니다#

처음 질문은 “OpenCode 2.0과 Hermes는 뭐가 다른가"였습니다. 기능 목록으로 답하면 상주 서버, 백그라운드 작업, 하위 에이전트, 플러그인까지 겹치는 것이 많아 차이가 잘 보이지 않습니다. 구조와 흐름을 먼저 익히고 나니 답이 달라졌습니다. 루프는 같고, 차이는 그 루프를 둘러싼 장치가 무엇을 지키려 하느냐에 있었습니다. OpenCode 2는 작업이 끊기지 않는 것을, Hermes는 에이전트가 쓸수록 자라는 것을 지키려 합니다.

이 기준은 앞으로 새 코딩 에이전트를 볼 때도 쓸 수 있을 것 같습니다. 루프는 어차피 비슷하니 압축, 권한, 하위 에이전트, 저장, 백그라운드 실행을 각각 어떻게 처리하는지 보면 그 에이전트가 무엇을 중요하게 여기는지 보입니다. 하네스 엔지니어링을 하는 입장에서도 달라지는 점이 있습니다. 바깥에서 거는 설정이 안쪽의 어느 지점에 들어가는지 알면, 지침 파일을 어디에 둘지, 승인 규칙을 어떻게 정할지 더 정확히 판단할 수 있습니다.

한계도 적어 둡니다. 이번에는 내부 동작을 코드와 주석으로만 확인했고, 실제 실행 과정을 로그로 따라가 보지는 않았습니다. 다음에는 운영 중인 Hermes에서 대화 하나를 골라 루프가 도는 순서를 로그로 따라가 보려 합니다. 또 운영 중인 두 Hermes에서 회고 루프가 실제로 어떤 메모리와 스킬을 만들어 왔는지 들여다보는 것이, 미뤄 둔 Hermes 운영 경험 후속기의 출발점이 될 것 같습니다.

참고 자료#