exoharness, 정책층 executor, cli, cost)와 TypeScript로 짠 실제 에이전트(examples/exo/)로 나뉜다. 샌드박스 프로바이더 7종, 채팅 어댑터 7종, 대체 실행자(Codex·Claude Code·Cursor SDK) 3종이 이미 붙어 있다.
exoharness/exo · 라이선스 MIT(Copyright 2026 Ankur Goyal) · 언어 Rust · 생성 2026-05-20 · ★ 355 · 포크 28 · 이슈 45 · 홈 exoharness.ai · TrendShift 데일리 1위(2026-07-26 기준) · shallow clone 소스 직접 분석)
먼저 하네스라는 말부터 풀자. 요즘 "AI 에이전트"라고 부르는 것은 사실 두 덩어리다. 하나는 모델(GPT·Claude 같은 LLM), 다른 하나는 그 모델을 실제로 일하게 만드는 바깥 껍데기다. 껍데기는 프롬프트를 조립하고, 툴 목록을 넘기고, 모델이 "이 함수 불러줘"라고 하면 진짜로 실행해 주고, 결과를 다시 넣어 준다. 이 껍데기를 하네스라고 부른다.
거의 모든 하네스는 사람이 손으로 짜서 고정해 놓는다. 프롬프트, 툴 목록, 컨텍스트 관리 방식이 코드에 박혀 있고, 모델은 그 안에서만 움직인다. 에이전트가 "자기개선"이라고 하는 것도 대개 메모리에 사실을 하나 적어 두거나, 스킬 파일을 하나 추가하는 정도다.
exo의 주장은 이렇다. 모델이 계속 똑똑해진다면, 하네스를 사람이 고정해 두는 것 자체가 병목이 된다. 그러니 에이전트에게 자기 하네스의 소스코드를 통째로 넘겨 주고, 고치고 다시 빌드해서 재시작하게 하자. 실제로 canonical 설정에서 exo의 소스 트리는 에이전트 샌드박스 안 /workspace/exo에 마운트되어 있고, 에이전트는 그걸 shell 툴로 직접 편집한다.
보통 에이전트 프레임워크는 세입자에게 못 하나 박는 것도 허락받게 한다. 벽지를 바꾸는 것(메모리 갱신) 정도만 자유롭다. 안전하지만, 세입자가 아무리 뛰어난 목수여도 집은 그대로다.
exo는 반대다. 벽을 허물든 배관을 새로 깔든 마음대로 하라고 한다. 다만 등기부등본과 공사 이력만은 구청 금고(= exoharness의 이벤트 로그)에 두고, 세입자는 열람만 가능하고 수정은 못 하게 한다. 공사를 망쳐도 "이 집이 원래 어떤 상태였고, 지난주에 뭘 시도했다 실패했는지"가 남아 있으니 되돌릴 수 있다.
이 금고가 없으면 어떻게 되는지가 중요하다. 세입자가 벽을 허물다가 공사 기록 장부까지 태워 버리면, 되돌릴 방법도 없고 같은 실수를 무한 반복하게 된다. exo가 "이벤트 로그만은 절대 못 건드린다"고 못 박은 이유가 딱 이것이다.
그래서 코드베이스가 두 덩어리로 칼같이 갈라져 있다. 이게 exo를 이해하는 데 필요한 사실상 유일한 개념이다.
공식 문서의 표현이 정확하다 — "히스토리를 저장하는 놈과 프롬프트를 결정하는 놈이 같으면, 에이전트가 프롬프트를 안전하게 다시 쓰게 둘 수 없다. 잘못된 변경 하나가 히스토리까지 날려 버리기 때문이다."(website/docs-src/concepts/exoharness-and-executor.md)
LICENSE 파일의 저작권자는 Ankur Goyal이다. LLM 평가·관측 플랫폼 Braintrust의 창업자다. 그 흔적이 의존성에 그대로 남아 있다 — Cargo.toml이 braintrust-sdk-rust, lingua(멀티프로바이더 메시지 포맷), braintrust-llm-router를 git 의존성으로 직접 가져온다.
이게 왜 중요하냐면, exo의 설계가 "에이전트를 만드는 사람"이 아니라 "에이전트를 관찰하고 평가하는 사람"의 관점에서 나왔다는 뜻이기 때문이다. 그래서 아키텍처의 중심에 프롬프트 엔지니어링이 아니라 이벤트 로그와 재현성이 놓여 있다. 실행 트레이싱(execution_tracing.rs)도 처음부터 1급 시민이다.
docs/RSI.md는 여기에 시비를 건다 — "AI로 GPU 커널을 짜서 AI를 빨리 만든다"는 건 재귀가 아니라 자기촉매(autocatalytic)일 뿐이라는 것이다. 증기기관도, 인터넷도 자기촉매였다. 진짜 재귀는 "완성된 자기 자신"으로 "다음 버전의 완성된 자기 자신"을 만드는 것 — 컴파일러가 자기 자신을 컴파일하는 것(부트스트래핑)처럼.그리고 여기서 exo가 던지는 통찰이 하나 있다. 프로그래밍 언어의 재귀 함수도 런타임 지원이 있어야 성립한다 — 콜 스택과 스코프가 없으면 재귀 함수는 자기 상태를 유지하지 못한다. 마찬가지로 에이전트의 재귀적 자기개선에도 런타임 지원이 필요하고, exo에서 그 역할을 하는 게 지워지지 않는 이벤트 로그다. 콜 스택은 아니지만 "완전한 실행 이력"이다.
재귀 함수가 콜 스택 없이 돌면 어떻게 되는가? 어디서 왔는지 몰라 같은 곳을 무한히 맴돈다. 자기를 고치는 에이전트도 똑같다. 코드를 고쳤다가 망가뜨리고, 되감고, 또 같은 방식으로 고치고... 로그가 있으면 되감은 뒤에도 "이미 이 방법은 해 봤고 실패했다"를 읽을 수 있다. 되감기(rewind)가 로그까지 지워 버리면 이 학습이 통째로 사라진다.
exo 위에서 돌아가는 에이전트는 exo 기본 턴 루프를 쓸 수도 있지만, Codex CLI · Claude Code SDK · Cursor SDK를 실행자로 꽂아 쓸 수도 있다. examples/typescript/codex-harness.ts(1,330줄)는 샌드박스 안에서 codex app-server --listen stdio://를 띄우고 그 알림을 exo 이벤트로 변환한다. claude-code-harness.ts는 아예 Claude Agent SDK의 프로세스 spawn을 가로채서 샌드박스 안 바이너리를 대신 띄운다.
즉 "어떤 코딩 에이전트를 쓰느냐"와 "히스토리·시크릿·샌드박스를 누가 소유하느냐"를 분리했다. 실행자를 바꿔도 에이전트의 정체성과 기억은 그대로다. 이건 지금 시장에서 꽤 드문 포지셔닝이다.
curl -fsSL https://raw.githubusercontent.com/exoharness/exo/main/setup.sh -o setup.sh
bash setup.sh
setup.sh가 git·Docker 유무를 확인하고, 없으면 설치를 제안하고, mise로 Node 22.15.0 / pnpm 10.26.2 / Rust 1.95를 버전 고정해 깔고, 빌드하고, API 키를 물어본 뒤 ./exo.sh 명령을 알려 준다. "Rust 프로젝트라 빌드가 험할 것"이라는 진입장벽을 의도적으로 무너뜨렸다.
이 방식은 원격 스크립트를 내려받아 그대로 실행한다. exo의 setup.sh는 파일로 저장한 뒤 실행하도록(-o setup.sh 후 bash setup.sh) 되어 있어 읽어 볼 기회는 준다. 실행 전에 한 번 열어 보는 습관을 권한다. 게다가 이 스크립트는 리눅스에서 usermod -aG docker를 제안하는데, 이는 사실상 root 권한과 동등한 권한 부여다.
| 지표 | 값 | 해석 |
|---|---|---|
| ★ 스타 | 355 | 절대값은 작다. 다만 저장소 생성이 2026-05-20이라 두 달 남짓의 수치다 |
| 포크 | 28 | 스타 대비 8% — "구경"보다 "직접 돌려 봄"의 비율이 높은 편 |
| 열린 이슈 | 45 | 스타 355 대비 상당히 높다. 활발한 개발 초기 단계의 전형 |
| 최근 커밋 | 2026-07-25 | PR #156 — 하루 단위로 움직이는 중 |
| 라이선스 | MIT | 상업적 이용·수정·재배포 자유. 가장 느슨한 편 |
TrendShift의 순위는 누적 스타가 아니라 최근 증가 모멘텀에 가깝다. 스타 355개짜리 저장소가 스타 10만개짜리보다 위에 올 수 있는 이유가 이것이다. 이 딥다이브에 나온 355·28·45는 2026-07-26 시점 GitHub API 조회값이며, 며칠 뒤면 달라진다.
이 조합에는 명확한 역할 분담이 있다. Rust는 "절대 안 죽어야 하는 것" — 이벤트 로그, 시크릿 암호화, 샌드박스 프로세스 관리, 어댑터 감독. TypeScript는 "자주 바뀔 것" — 프롬프트 조립, 툴 정의, 어댑터 프로토콜 구현.
그리고 이게 자기수정 설계와 직결된다. 에이전트가 가장 자주 손댈 부분(프롬프트·툴)은 TypeScript라 재컴파일 없이 파일만 고치면 되고, 신뢰층은 Rust라 고치려면 cargo build를 거쳐야 한다. 수정 난이도 자체가 안전장치다.
| 크레이트 / 디렉토리 | 줄 수 | 역할 |
|---|---|---|
| crates/exoharness | 19,825 | 신뢰층. 데이터 모델·이벤트 로그·시크릿·샌드박스 7종·HTTP 서버/클라이언트 |
| crates/executor | 18,591 | 정책층. 턴 루프·툴 런타임·어댑터 감독·스케줄러·TS 브리지·RLM |
| crates/cli | 8,967 | exo 명령. 8개 서브커맨드 트리 + rustyline 기반 REPL |
| crates/cost | 374 | LiteLLM 공개 가격 DB를 받아 캐시하고 토큰 비용 계산 |
| examples/exo | TS 다수 | 실제로 돌아가는 canonical 에이전트 — 프롬프트·툴 8종·어댑터 7종 |
| examples/typescript | TS 다수 | 대체 하네스 — codex(1,330줄)·claude-code(752)·cursor-sdk(619)·rlm(714) 등 |
| examples/gameboy-agent | Py+TS | PyBoy 에뮬레이터로 포켓몬을 플레이하는 데모 에이전트 |
| 이름 | 쓰임새 |
|---|---|
| tokio 1.52 | Rust 비동기 런타임. 전체 I/O의 기반 |
| object_store 0.12 | 저장 추상화. 로컬 파일시스템으로 이벤트/레코드 JSON 저장 (fs·aws·gcp·azure 피처 활성) |
| uuid 1.18 (v7) | 모든 ID. 정렬 = 시간순이라는 성질이 로그 설계의 핵심 |
| lingua / braintrust-llm-router | 멀티 프로바이더 메시지 포맷 + LLM 라우팅 (git 의존성) |
| actix-web 4.9 | exoharness를 HTTP 서버로 노출할 때 (exo serve) |
| aws-sdk-bedrockagentcore | AWS Bedrock AgentCore를 샌드박스 프로바이더로 쓸 때 |
| boa_engine | 순수 Rust JS 엔진. RLM 하네스의 JS REPL에 사용 |
| clap 4.5 / rustyline | CLI 파싱 / 대화형 REPL 라인 편집 |
| sqlx 0.8 | 워크스페이스에 선언은 되어 있으나 exoharness의 기본 백엔드는 DB를 쓰지 않는다 (아래 §4 참조) |
| tsx / Node 22 | TypeScript 하네스를 별도 프로세스로 실행 |
보통 챗봇을 만들면 대화를 messages: [{role, content}, ...] 배열로 저장한다. exo는 그러지 않는다. 저장하는 건 이벤트(event)이고, 메시지는 그중 한 종류일 뿐이다.
// crates/exoharness/src/types.rs — EventData는 22종 태그드 enum
pub enum EventData {
ConversationCreated { .. }, ConversationForked { .. },
SessionStarted { .. }, SessionEnded { .. },
TurnStarted { .. }, TurnEnded { .. },
Messages { messages, usage: Option<Box<UsageRecord>> },
ToolRequested { .. }, ToolResult { .. },
ArtifactWritten { .. }, SandboxCreated { .. }, ...
Custom { event_type, payload } // 탈출구
}
이 설계의 요점은 Custom이라는 탈출구다. 예를 들어 "컨텍스트 압축(compaction)"을 생각해 보자. 보통은 프레임워크가 압축을 1급 개념으로 넣는다. exo는 넣지 않는다. 대신 정책층이 "내가 만든 요약본은 여기에 있다"는 커스텀 이벤트를 로그에 append하면 끝이다. 신뢰층은 압축이 뭔지 알 필요가 없다.
전체 이벤트 로그가 곧 모델에 보내는 프롬프트인 것은 아니다. 로그는 지워지지 않는 기록이고, 프롬프트는 정책층이 로그로부터 만들어 내는 하나의 뷰(view)다. 그래서 압축·요약·필터링을 아무리 공격적으로 해도 원본은 안전하다. 이 한 문장이 exo 전체 아키텍처를 압축한다.
계층 구조는 Agent → Conversation → Session → Turn 네 단계다.
| 개념 | 뜻 |
|---|---|
| Agent | 전역 설정 단위. 여러 대화에 공통으로 적용되는 메타데이터와 시크릿을 소유 |
| Conversation | 한 에이전트와의 대화 흐름. 멈췄다 재개할 수 있고, 수백만 건이 쌓일 수 있다 |
| Session | 대화 안에서 살아 있는 한 번의 접속. "지금 켜져 있는 인스턴스" |
| Turn | 사용자 입력 한 번. 그 안에서 LLM 호출은 여러 번 일어날 수 있다 |
Rust 코드에서는 이게 4단계 상속 트레잇으로 나타난다. 재미있는 건 TurnHandle을 따로 둔 점이다 — 진행 중인 턴에서 이벤트를 쓰면 세션/턴 ID가 자동으로 태깅돼서, 나중에 "이 이벤트가 어느 턴 소속이었나"를 잃어버리지 않는다.
// 실제 메서드 이름 (TS 쪽에서는 camelCase로 래핑됨)
trait ConversationHandle: SandboxHandle {
async fn begin_turn(..) -> Arc<dyn TurnHandle>; // 핫패스
async fn get_events(..) -> GetEventsResult; // 커서 스캔
async fn watch_events(..) -> EventStream; // 스트리밍 구독
async fn fork(..) -> Arc<dyn ConversationHandle>;
}
trait TurnHandle: SnapshotHandle {
async fn add_events(data: Vec<EventData>) -> AddEventsResult;
async fn finish() -> EventId;
}
Cargo.toml에 sqlx·sqlite·postgres가 선언돼 있어서 당연히 DB를 쓸 거라 생각하기 쉽다. 그런데 기본 백엔드(BasicExoHarness)를 열어 보면 DB를 전혀 쓰지 않는다. 이벤트 하나가 파일 하나다.
그럼 동시성은 어떻게 처리할까? 두 겹이다.
write_lock: AsyncMutex<()> 하나로 모든 쓰기를 직렬화한다.ConversationRecord.latest_event_id가 대화의 "머리"를 가리키고, 턴이 이벤트를 쓸 때마다 "내가 알던 머리가 아직도 머리인가"를 비교한다. 다르면 "turn is stale and cannot be resumed" 에러.락은 "동시에 쓰는 것"만 막는다. 그런데 턴 하나가 진행되는 동안 에이전트는 샌드박스에서 명령을 돌리거나 HTTP를 호출하며 락을 놓고 한참 기다린다. 그 사이에 스케줄러가 태스크를 끝내고 같은 대화에 이벤트를 append할 수 있다. 락만으로는 이 "내가 자는 사이 히스토리가 앞서갔다"를 못 잡는다. CAS가 그걸 잡는다.
은행 창구에 비유하면, 락은 "한 번에 한 명만 창구 이용"이고 CAS는 "당신이 뽑은 번호표가 아직 유효한가"다. 잠깐 화장실 다녀온 사이 순번이 지나갔으면 다시 뽑아야 한다.
"에이전트의 전체 상태 = 이벤트 로그의 버전"이라는 불변식이 성립하기 때문에, 로그의 임의 지점으로 되감기(rewind)하거나 분기(fork)할 수 있다.
| 동작 | 의미 | 중요한 점 |
|---|---|---|
| rewind | 알려진 정상 상태로 되돌림 | 로그는 append-only라 "그 뒤에 뭘 했는지"는 지워지지 않는다 |
| fork | 기존 대화에서 새 대화를 가지치기 | exo conversation fork <agent> <conv> "이름" |
| snapshot | 샌드박스 파일시스템 스냅샷 | 스냅샷 ID가 로그에 기록돼서, 로그를 되감으면 파일시스템도 같이 되감긴다 |
이름 때문에 전부 되돌아갈 것 같지만, rewind_sandbox가 되돌리는 건 샌드박스 파일시스템뿐이다. 대화 이벤트, 아티팩트, 어댑터·스케줄러 레코드, 시크릿은 그대로 남는다. 저장소의 docs/SELF-CONTROL.md가 이 비대칭성을 가장 큰 주의사항으로 명시하고 있다.
그런데 이건 버그가 아니라 의도된 설계다. 파일시스템은 실험 대상이고 로그는 학습의 근거라, 둘이 같이 지워지면 §2의 "무한 루프" 문제가 되살아난다.
샌드박스는 에이전트가 명령을 실행하는 격리 환경이다. exo는 공통 트레잇 두 개(ManagedSandboxHandle / ManagedSandboxBackend)로 추상화하고 7종을 구현했다.
| 프로바이더 | 위치 | 스냅샷 |
|---|---|---|
| Docker | 로컬 (docker CLI) | ○ docker commit → docker save로 tar 캡처 |
| Apple Container | 로컬 (container CLI) | × 미구현(로드맵으로 명시) |
| LocalProcess | 격리 없이 호스트에서 직접 | × 격리된 FS가 없어 의미 자체가 없음 |
| Daytona | 클라우드 REST | ○ + Docker tar도 받아들임 |
| E2B | 클라우드 (envd Connect API) | ○ 템플릿 ID 참조 |
| Sprites | 클라우드 (REST + WebSocket) | ○ 체크포인트 ID 참조 |
| Vercel / AWS AgentCore | 클라우드 REST | × 미구현 |
스냅샷은 보통 만든 백엔드에서만 복원 가능하다(SnapshotKind 태그로 강제). 그런데 딱 하나 예외가 있다 — Daytona 백엔드가 DockerImageTar 스냅샷을 받아들인다. 내부적으로 임시 격리된 레지스트리 자격증명으로 docker login/push를 돌려 이미지를 클라우드로 밀어 넣고 거기서 복원한다. 호스트의 ~/.docker/config.json은 건드리지 않는다.
실용적 의미: 노트북에서 시작한 에이전트 작업 환경을 그대로 클라우드로 승급시킬 수 있다.
Vercel과 AWS AgentCore는 스트리밍 exec API가 없어서 stdin/stdout을 실시간으로 주고받을 수 없다. exo의 해법은 sandbox_provider/process_bridge.rs에 파이썬 소켓 서버 코드 전문을 Rust 문자열 상수로 넣어 두고, base64로 인코딩해 원격 샌드박스에 설치·실행시킨 뒤 TCP 소켓(127.0.0.1:48765)으로 통신하는 것이다. "API가 없으면 만들어 넣는다"는 접근.
자기 코드를 고칠 수 있는 에이전트에게 API 키를 어떻게 안 들키나? exo의 답이 인상적이다.
시크릿은 AES-256-GCM으로 암호화해 저장하고, 마스터 키는 macOS Keychain 또는 ~/.config/exo/master.key(권한 0600)에 둔다. 이 위치는 harness 데이터 루트와도, git 저장소와도 별개다. 에이전트가 소스를 아무리 고쳐도 이미 암호화된 시크릿을 풀 키에 닿지 못한다.
그런데 진짜 핵심은 다른 데 있다. 샌드박스에서 프로세스를 띄울 때 시크릿은 환경변수로 주입된다. 그럼 그게 로그에 남지 않을까? 남지 않는다 — 필터링하는 게 아니라, 이벤트 타입에 env 필드 자체가 없다.
// SandboxProcessStarted 이벤트가 기록하는 것
sandbox_id, process_id, name, command, cwd, mode,
stdin, output, lifecycle, status, provider_state
// ↑ env 필드가 아예 존재하지 않음 ←←← 이게 격리의 실체
"로그를 쓸 때 시크릿을 마스킹한다"는 흔한 접근은 마스킹 로직에 구멍이 하나만 있어도 유출된다. 정규식이 못 잡는 형태, 새로 추가된 필드, 예외 경로... 계속 관리해야 한다.
exo는 그 대신 담을 그릇을 아예 만들지 않았다. 실수로 시크릿을 로그에 넣으려면 먼저 Rust 타입 정의를 고치고 재컴파일해야 한다. 컴파일러가 정책을 강제하는 셈이다.
이제 하이라이트다. 에이전트가 자기 코드를 고쳤다. 그 다음은?
에이전트는 소스코드를 무엇이든 고칠 수 있다. 그런데 빌드하고 재시작하는 통로는 9개 액션짜리 allowlist뿐이다. 게다가 재시작 명령은 bash -lc 'sleep 2; exec ...'로 2초 지연 후 분리된 프로세스로 실행된다 — 현재 턴이 결과를 기록하고 끝낼 시간을 벌어 주는 것이다. 자기를 재시작하면서도 "재시작했다"는 사실을 로그에 남길 수 있다.
실패하면? 전용 롤백 툴은 없다. 원시 도구를 조합한다 — 코드는 git revert, 파일시스템은 rewind_sandbox, 설치한 툴은 uninstall_agent_tool, 어댑터는 disable_adapter. 문서가 권하는 루틴은 "빌드 전 스냅샷 → 빌드 → status/logs 확인 → 그 다음에 커밋"이다. 참고로 저장소 문서는 "클론된 샌드박스에서 카나리 검증하는 기능은 아직 없다"고 스스로 밝히고 있다.
실제 한 턴이 도는 모습은 이렇다.
// crates/executor/src/basic.rs — 라운드 반복의 뼈대
for round in 0u32.. {
if round > max_tool_round_trips { return; } // 무한루프 방지
let messages = materialize_prompt_history(..); // 로그 → 프롬프트
let response = complete_model_round(..); // LLM 호출
let events = interpret_model_response(response); // 툴콜 파싱
turn.add_events(events).await?; // 로그에 기록
let reqs = collect_tool_requests(&events);
if reqs.is_empty() { return; } // 툴콜 없으면 종료
turn.add_events(execute_tool_round(..)).await?; // 툴 실행 후 기록
}
TypeScript 하네스는 어떻게 붙는가. Rust가 JS 엔진을 내장한 게 아니라, node --import tsx runner.ts <하네스경로>로 Node 자식 프로세스를 띄우고 stdin/stdout 위 NDJSON RPC로 대화한다. 핵심은 권한 분리다 — TypeScript는 "무엇을 호출할지"만 결정하고, 실제 셸·파일시스템 접근은 Rust 호스트가 수행한다.
보통은 툴 레지스트리를 한 번 만들어 재사용한다. exo의 TS 턴 루프는 라운드마다 빈 레지스트리를 새로 생성해 전부 재등록한다. 이유는 하나 — 에이전트가 턴 도중 install_agent_tool로 만든 툴이 바로 다음 라운드에 보이게 하기 위해서다. 스스로 도구를 만들어 즉시 쓰는 루프가 이 한 줄에서 나온다.
어댑터(adapter)는 외부 채팅 채널과의 장기 연결을 담당하는 호스트 프로세스다. ExoChat·IRC·Discord·Slack·WhatsApp·Signal·agent-cli 7종이 있다. 여기서도 프로토콜은 소켓이 아니라 stdin/stdout 위 줄바꿈 구분 JSON이고, 워커는 별도 프로세스 그룹으로 spawn된다(손자 프로세스까지 정리하려고). 재연결은 지수 백오프(5초 → 최대 300초, 60초 이상 안정되면 리셋)로 프로세스를 통째로 재기동하는 방식이다.
에이전트가 Slack에 연결돼 있어도, 모델이 생성한 텍스트가 자동으로 슬랙에 전송되지 않는다. send_adapter_message 툴 호출만 아웃박스 파일에 기록되고, 어댑터 런타임이 1초마다 그걸 드레인해서 보낸다. 프롬프트의 표현대로 "외부 side effect는 명시적이고 검사 가능해야 한다"는 원칙이 코드로 구현된 것.
스케줄러는 "매 시간 뉴스 헤드라인 가져오기" 같은 반복 작업을 돌린다. 이름과 달리 진짜 cron이 아니다 — @every 5m이나 */N * * * *만 받아 내부적으로 interval_ms(고정 간격)로 정규화한다. 동시성 제어는 파일 락이 아니라 시간 기반 리스(lease, 기본 10분)라, 워커가 죽어도 리스가 만료되면 다른 워커가 자동으로 집어 간다. 작업이 끝나면 결과 요약으로 대화를 "깨워서"(wakeup) 새 턴을 시작시킨다.
crates/executor/src/rlm.rs(808줄)는 좀 별종이다. 코드 안 프롬프트에 "You are a subquery model inside a recursive language model"이라고 적혀 있다. 이 하네스에는 셸도 샌드박스도 없다. 대신 대화 전체를 문자열로 평탄화해 순수 Rust JS 엔진(boa)에 넣고, 툴을 딱 3개만 준다 — repl_execute, subquery, subquery_variable.
Basic/TS 하네스는 행동(act)하는 에이전트다 — 셸을 돌리고 파일을 고친다. RLM은 계산(compute)하는 루프다 — 아주 긴 컨텍스트를 JS 코드로 잘라 내고, 조각마다 서브 모델에게 질문을 던지고, 결과를 다시 변수에 담아 조합한다. globalThis.Final이 설정되면 종료한다.
비유하자면 전자는 현장에 나가는 조사관, 후자는 자료실에서 문서 더미를 프로그래밍으로 분해하는 분석가다.
① docs/RSI.md(철학, 60줄) → ② website/docs-src/concepts/exoharness-and-executor.md(핵심 분리) → ③ examples/exo/prompts/me.md(에이전트가 실제로 뭘 지시받는지, 27줄) → ④ examples/exo/harness.ts(프롬프트 조립 실물) → ⑤ crates/exoharness/src/types.rs(데이터 모델). 여기까지가 하루치다.
유연성을 늘리는 정석은 보통 "설정 옵션을 추가하는 것"이다. exo는 반대로 갔다 — 불변으로 남길 최소 집합을 먼저 정하고, 나머지 전부를 열어 버렸다. 불변 집합이 작을수록 위에서 진화할 수 있는 공간이 커진다. 이건 에이전트가 아닌 일반 시스템 설계에도 그대로 적용되는 원칙이다.
시크릿이 로그에 안 남는 이유가 마스킹 함수가 아니라 이벤트 타입에 env 필드가 없어서라는 것. 정책을 데이터 구조로 옮기면 실수할 경로 자체가 사라진다. "검사"보다 "불가능하게 만들기"가 항상 강하다.
contract_tests.rs가 로컬 파일시스템 구현(BasicExoHarness)과 HTTP 클라이언트 구현(HttpExoHarness) 양쪽에 동일한 테스트 함수를 실행한다. "트레잇이 같으니 동작도 같겠지"를 믿지 않고 테스트로 강제한 것. 다중 백엔드를 지원하는 모든 프로젝트가 훔쳐 갈 만한 패턴이다.
guardian이 재시작할 때 프로세스를 바로 죽이지 않는다. .exo/exo-adapters.restart 같은 마커 파일을 쓰면 러너가 그걸 스스로 claim하고, 진행 중 작업을 마친 뒤 종료한다. 15초 안에 안 끝나야 비로소 강제 kill. 게다가 재시작 명령 자체도 2초 지연 실행이라 "내가 나를 재시작한다"는 기록을 남길 시간이 확보된다.
exo에는 컨텍스트 압축 정책이 없다. 대신 메모리·투두·스킬을 프롬프트에 넣을 때 이름과 한 줄 설명 목록만 넣고, 상세 내용은 에이전트가 필요할 때 툴로 가져가게 한다. 압축 알고리즘을 짜기 전에 이 방식부터 시도해 볼 가치가 있다.
UUIDv7을 파일명으로 쓰면 ls | sort가 곧 시간순 정렬이고, ID에서 생성 시각을 역산할 수 있다. 결과적으로 DB도 인덱스도 created_at 컬럼도 없이 append-only 로그가 성립한다. 작은 선택이 아키텍처 전체를 단순하게 만든 사례.
대화 이력을 매 턴 통째로 replay한다. 롤링 요약도, 오래된 메시지 축약도 없다. "수백만 건이 쌓일 수 있다"는 데이터 모델 설명과 실제 프롬프트 조립 방식 사이에 아직 메워지지 않은 간극이 있다. 물론 설계상 이건 정책층 문제라 사용자(또는 에이전트 자신)가 만들어 넣으면 된다 — 그게 exo의 논리다.
BasicExoHarness는 프로세스 전역 뮤텍스로 쓰기를 직렬화한다. 문서도 "코디네이션된 로컬 런타임 전용"이라고 명시한다. Postgres/SQLite 백엔드는 의존성 선언만 있고 실제 구현이 없다. 여러 머신에서 같은 로그를 공유하는 시나리오는 아직 준비돼 있지 않다(README의 "Ongoing Work"에도 이 항목이 있다).
모델명이 claude로 시작하면 Anthropic, base_url에 openrouter.ai가 있으면 OpenRouter, 나머지는 OpenAI 호환 — 이런 하드코딩 분기다. 그래서 Bedrock의 us.anthropic.claude-... 같은 ID는 Anthropic 매칭에서 빠져 OpenAI 경로로 폴백한다(코드 주석에 명시돼 있다).
exo의 안전성은 복구 가능성(recoverability)이지 행동 제한(containment)이 아니다. 에이전트는 샌드박스 안에서 임의 명령을 돌릴 수 있고, 어댑터를 통해 외부와 통신하며, 스케줄러로 반복 작업을 건다. 로그가 남고 되감을 수 있을 뿐, "하면 안 되는 일을 못 하게" 막는 장치는 아니다. 프로덕션 데이터나 실계정 자격증명 근처에서 돌릴 물건이 아직 아니다.
2026-05-20 생성, 열린 이슈 45개, README가 직접 "still in the early stages of development"라고 쓴다. 스스로 밝힌 미완 영역도 명확하다 — 자율적 자기정비, 장애 후 복구·이식성, 멀티 에이전트 오케스트레이션 정책, 그리고 GUI 컴퓨터 사용(윈도우 환경 조작) 미지원.
| 항목 | 요구사항 | 비고 |
|---|---|---|
| OS | macOS 또는 Linux | Windows는 WSL2 경유 권장(공식 안내 없음) |
| 필수 도구 | git, Docker | 없으면 setup.sh가 설치를 제안한다 |
| 런타임 | Node 22.15.0 / pnpm 10.26.2 / Rust 1.95 | mise가 자동으로 버전 고정 설치 |
| Rust edition | 2024, rust-version 1.95 | 상당히 최신. 구버전 툴체인으론 빌드 불가 |
| API 키 | OpenAI 또는 OpenRouter | 필수. 없으면 에이전트가 아무것도 못 한다 |
| 디스크 | 수 GB | Rust 빌드 산출물 + Docker 이미지 + 스냅샷 tar |
| GPU | 불필요 | 모델은 전부 원격 API 호출 |
| 첫 빌드 시간 | 수 분 | Rust 워크스페이스 전체 컴파일 |
exo는 "오래 사는 에이전트"를 지향한다. 스케줄러가 매 시간 태스크를 돌리고, 그 결과가 대화를 깨우고, 그때마다 전체 대화 이력이 프롬프트로 재생된다(압축 없음). cost 크레이트와 REPL의 /cost 명령이 괜히 있는 게 아니다. 처음 며칠은 비용을 꼭 지켜보길 권한다.
설치 후 이 프롬프트를 그대로 넣어 본다 — "샌드박스에 python3와 curl을 apt-get으로 설치해라(sudo 불필요). 그 다음 매 분 BBC RSS에서 헤드라인을 가져와 이전에 출력하지 않은 새 헤드라인만 출력하는 태스크를 스케줄해라."
이 한 문장이 샌드박스 · 툴 실행 · 스케줄러 · 대화 깨우기를 전부 관통한다. 잘 돌면 설치가 정상이라는 뜻이다.
exo conversation events로 로그를 뽑고, 저장 경로의 events/*.json 파일도 직접 열어 본다. 한 턴이 TurnStarted → Messages → ToolRequested → ToolResult → Messages → TurnEnded로 어떻게 쪼개지는지 확인하는 게 목표다. UUIDv7 파일명을 정렬하면 정말 시간순인지도 눈으로 검증해 보자.
examples/exo/prompts/me.md를 열어 정체성 문구를 고치고, guardian_action의 build → 재시작을 거쳐 반영해 본다. 그 다음 .exo/exo-profile.md(git-ignore되는 로컬 프로필)와의 차이도 확인한다. "어느 파일이 어느 시점에 프롬프트에 들어가는가"를 체감하는 게 핵심.
에이전트에게 일부러 위험한 작업을 시킨다(예: 샌드박스에서 중요한 패키지 제거). 그 전에 snapshot_sandbox로 스냅샷을 찍어 두고, 망가진 뒤 rewind_sandbox로 되돌린다. 그리고 확인한다 — 파일시스템은 돌아왔는데 대화 로그에는 "망가뜨렸던 기록"이 그대로 남아 있는가? §4-3의 "반쪽 롤백"을 몸으로 이해하는 실습이다.
examples/exo/의 기존 *-tools.ts 하나를 템플릿 삼아 새 툴 파일을 만들고 harness.ts에 등록한다. 그 다음 같은 툴을 에이전트에게 만들라고 시켜서(install_agent_tool) 비교해 본다. 매 라운드 툴이 재등록되기 때문에 에이전트가 방금 만든 툴을 곧바로 쓸 수 있다는 걸 직접 확인할 수 있다.
examples/typescript/codex-harness.ts와 containers/codex-sandbox/Dockerfile을 읽고, 같은 exoharness 위에서 다른 코딩 에이전트를 실행자로 돌려 본다. 그 다음 같은 대화를 실행자만 바꿔 이어서 진행해 본다. 이게 되면 §1의 "신뢰층/정책층 분리"가 말뿐이 아니라는 걸 확인한 셈이다.
exo의 뼈대는 사실 새로운 발명이 아니라 이벤트 소싱이라는 오래된 패턴이다. "현재 상태를 저장"하는 대신 "상태를 바꾼 사건들을 저장"하고, 현재 상태는 사건을 재생해서 얻는다. 금융 시스템이 오래 써 온 방식이다. Martin Fowler의 Event Sourcing 글과 CQRS 개념을 먼저 보면 exo의 get_events / materialize_prompt_history 짝이 왜 그렇게 생겼는지 바로 이해된다.
"에이전트가 코드를 고칠 수 있는데 시크릿은 못 본다"는 구조는 보안 설계의 고전적 주제다. capability-based security, principle of least privilege, confused deputy problem을 찾아보길 권한다. 특히 guardian의 allowlist 방식이 왜 "임의 셸 실행 + 검증"보다 강한지가 이 개념들로 설명된다.
docker commit / docker save가 실제로 무엇을 하는지, OverlayFS 레이어가 어떻게 쌓이는지 알면 exo의 스냅샷 크기와 속도 특성이 예측 가능해진다. 더 나아가면 E2B·Daytona 같은 클라우드 개발 샌드박스 서비스들의 CRIU 기반 체크포인트 기술이 있다.
exo 저장소 자체에 docs/coding-agent-harnesses.md가 있다. 그리고 Codex CLI, Claude Code, Cursor의 실제 동작을 exo의 대체 하네스 코드로 역추적해 볼 수 있다 — 각각을 어떻게 붙였는지가 그 도구들의 인터페이스를 드러낸다. 남이 만든 어댑터 코드는 그 대상 시스템의 가장 정직한 문서다.
Rich Sutton의 The Bitter Lesson(exo가 명시적으로 인용한다)을 읽고, 그 다음 "하네스를 사람이 짜는 것도 결국 hand-engineering 아닌가"라는 exo의 주장을 스스로 검증해 보자. 반론도 만만치 않다 — 모델이 자기 하네스를 고칠 만큼 신뢰할 수 있는가, 평가(eval) 없이 개선을 어떻게 측정하는가. 저자가 평가 플랫폼 창업자라는 사실이 여기서 다시 의미를 갖는다.
latest_event_id를 이 방식으로 검사해, 락을 놓고 기다리는 사이 히스토리가 앞서갔으면 "이 턴은 낡았다"고 거부한다.guardian_action 툴로 빌드·상태확인·로그조회·서비스 재시작만 요청할 수 있다(9개 액션 allowlist). 임의 셸 명령은 통과하지 못한다.repl_execute·subquery·subquery_variable 3개뿐.setup.sh가 이걸로 Node·pnpm·Rust·Python 버전을 고정해 깔아서, "내 컴퓨터에선 되는데" 문제를 줄인다.web_fetch는 사설 IP·localhost 접근을 차단하고 DNS 리바인딩까지 재검증한다. 에이전트가 "내부 관리 페이지를 대신 열어 줘" 같은 유도에 넘어가는 걸 막는 장치.| 대상 | 위치 |
|---|---|
| 저장소 | github.com/exoharness/exo |
| 공식 문서 | exoharness.ai/docs |
| 설계 철학 | docs/RSI.md — "A Systems View of Recursive Self Improvement" |
| 개념 요약 | docs/EXO-BASICS.md |
| 핵심 분리 설명 | website/docs-src/concepts/exoharness-and-executor.md |
| 데이터 모델 | website/docs-src/concepts/data-model.md, crates/exoharness/src/types.rs |
| 타임 트래블 | website/docs-src/concepts/time-travel.md |
| 자기수정 지침 | examples/exo/SELF.md, examples/exo/docs/SELF-CONTROL.md |
| 에이전트 정체성 | examples/exo/prompts/me.md (27줄) |
| 프롬프트 조립 | examples/exo/harness.ts |
| 커뮤니티 | Discord (README 상단 배지에 초대 링크) |