← Back to sungyongcho.com

PetV3 — 데스크톱 펫 실험에서 에이전트 공장으로

약 10분 읽기
#petv3#agents#rust#orchestration

진행 중인 프로젝트 · 2026년 8월–9월

PetV3는 ChatGPT Desktop과 Claude Desktop의 작업을 보이게 하고 조율하기 쉽게 만들 수 있는지를 시험하며 시작했다. 처음부터 ‘AI 공장’을 만들려던 것은 아니다. 데스크톱 펫이 각 제공자의 작업을 보이게 하고 같은 화면에서 답장을 받아 여러 AI 보조 작업을 사람이 확인할 수 있는지가 첫 질문이었다.

필요한 기능이 쌓이면서 프로젝트는 에이전트 작업을 조율하는 제어면의 형태로 발전하고 있다. 이는 아직 완성된 자율성을 선언하는 말이 아니라, 진행 중인 방향이다.

현재의 답은 구체적이지만 혼합돼 있다. 렌더링된 데스크톱 데모와 로컬 오버레이, 제공자 활동·전달 확인·세션/사용 기록, 격리 작업과 안전한 인계를 위한 일부 기능이 구현됐다. 동시에 실패, 일관되지 않은 동작, 한계도 분명히 남았다. 이것은 완성된 제어면이 아니며, 사람이 없이 모든 작업을 배정·검토·병합·복구하도록 둘 수 있는 제품도 아니다.

8월 29일 — 실제 데스크톱부터 확인하다

첫 Tauri/React 계획은 Ubuntu GNOME Wayland, XWayland, 듀얼 6K 환경에서 펫이 안정적으로 보이고 조작되기 전에 브라우저 같은 제어 창을 먼저 만들었다. 이 실패로 첫 기준이 생겼다. 화면 시안이 아니라 실제 운영 환경이 먼저 통과해야 한다는 것이다.

렌더링과 입력은 네이티브 Rust 경계로 옮겼다. 이식 가능한 core는 좌표·상태·애니메이션·IPC를, Linux surface는 X11/XRender/XFixes/XRandR를 통한 창·입력·모니터·투명 합성을 맡는다. 초기 20초 release 데모에서는 28.3fps와 38.7MiB RSS를 기록했고, 두 번째 모니터의 (6500, 200) 위치에서 ARGB always-on-top 오버레이를 확인했다. 이는 당시 데모의 측정값이며 현재 성능을 뜻하지는 않는다.

이 과정은 학습 프로젝트이기도 했다. 소프트웨어·분산 시스템 배경은 있었지만 Rust, MCP, 프롬프트로 움직이는 다중 에이전트 운영은 처음이었다. 실제 방식은 대화, 작은 시도, 결과 확인, 수정의 반복으로 생겼다. 무엇을 근거로 볼지 정하고, 범위가 닫힌 지시를 내리고, 결과를 확인한 뒤 실제 동작이 예상과 다르면 계획을 고쳤다. 구현이 빨라졌다고 범위·검토·재시도 판단의 책임이 사라진 것은 아니었다.

상태 언어도 의도적으로 겸손하게 만들었다. 작업중?는 최근 활동이지 프로세스 생존 증명이 아니고, 진행 근거 없음은 0%가 아니며, “구현됐지만 아직 호출되지 않음”은 구현과 통합을 구분한다. Wayland 제약 때문에 화면 확인을 못 하면 상태는 사람 확인 필요로 남는다. 이것은 말투 문제가 아니라 다음 세션이나 다른 제공자가 불확실성을 성공으로 바꾸지 않고 이어받게 하는 데이터 모델이었다.

8월 29일–30일 — 병렬 작업을 검증 가능한 착지로 바꾸다

작업 속도가 달라지면서 핵심 문제는 틀린 성공을 만들지 않는 것이 됐다. 브랜치에 커밋이 있어도 게이트를 통과하지 않을 수 있고, 기능이 있어도 호출되지 않을 수 있으며, 대시보드는 옛 바이너리를 실행할 수 있다. 검사 뒤 커밋 전에도 파일은 바뀔 수 있다.

그래서 흐름을 다음처럼 만들었다.

격리된 worktree → 겹침을 보이는 ledger → 중앙 land gate

워커에는 다른 사람의 변경을 숨길 수 있는 broad stash 대신, 닫힌 작업·브랜치·worktree를 준다. ledger는 파일 겹침을 기록하되 잠그지는 않아, 관제자가 충돌한 의도를 보고 중재할 수 있게 한다. land.sh는 워커 끝 게이트, 시작 main 상태, 병합 결과 게이트, main HEAD 단독 링크를 차례로 확인한다. 마지막 검사는 좁은 패키지 게이트는 통과했지만 다른 crate가 새 enum variant를 처리하지 못했던 실제 재현에서 나왔다.

격리는 소스만의 문제가 아니었다. source archive, Cargo target, TMPDIR, 로그와 보존 진단도 함께 나눴다. 문서도 마찬가지였다. 여러 워커가 색인 한 줄을 더하는 방식은 충돌했기에, history는 사건별 파일 하나와 생성된 색인으로 바꿨다.

착지 경로는 편의 스크립트가 아니라 트랜잭션으로 만들었다. 워커 브랜치에서 범위 게이트를 돌리고, 관제자가 시작 main HEAD와 clean 상태를 다시 확인하고, 충돌 없이 병합한 뒤, 병합 결과에서 같은 게이트를 다시 돌리고, 마지막으로 main HEAD가 단독으로 링크돼야 했다. 이 마지막 단계는 좁은 패키지 게이트는 통과했지만 다른 crate가 새 enum variant를 처리하지 못했던 실제 재현에서 나왔다. 브랜치·병합 게이트는 통과했지만 단독 링크는 실패했고, 안전한 시작점으로 되돌아갔다. 게이트는 있다고 말하는 것보다 약속한 실패를 실제로 잡을 때 신뢰할 수 있다.

소스 디렉터리만 나누는 것으로는 부족했다. archive 둘이 Cargo target을 공유하면서 이전 바이너리가 이미 지운 경로를 CARGO_MANIFEST_DIR로 잡은 적이 있었다. 그래서 착지 한 번은 branch/merged source archive, 각각의 Cargo target, Rust용 임시 디렉터리, 단계 로그, 보존 실패 진단을 함께 격리했다. 성공한 실행은 자기 run root만 지우고, 실패는 근거를 남겼다. 보존 실패가 한계에 닿으면 진단을 자동 삭제하지 않고 다음 착지를 거부했다.

기록도 같은 병렬성 문제를 가졌다. 사건 파일은 나뉘어도 여러 워커가 공용 색인 한 줄을 덧붙이면 충돌했고, 각자가 “다음 번호”를 골라도 겹쳤다. 이를 사건당 YYYY-MM-DD-slug.md 파일 하나, 생성되는 색인, Git에서 계산하는 착지 커밋으로 바꿨다. 문서도 코드처럼 동시에 바뀌면 충돌한다.

8월 30일–9월 1일 — 근거와 제어를 보이게 하다

터미널 대시보드는 별도 브라우저 런타임 없이 task, worker, file, backlog, ledger, 세션 근거를 한곳에 모았다. 모든 파일과 충돌을 한꺼번에 펼치지 않고, 필요한 정보만 단계적으로 연다.

TASK R-3  overlay tick order 고정
 └─ WORKER work/r3-tick-order  커밋됨 · 착지 대기
     └─ 파일 3 · 충돌 0

대시보드는 근거 없는 진행률 대신 작업중?, 커밋됨 · 착지 대기, 진행 근거 없음처럼 관찰된 상태를 보여 준다. 애니메이션은 빠르게, Git·세션 수집은 느리게 갱신해 큰 기록을 매 프레임 다시 읽지 않는다. 오래된 release 바이너리와 모든 파일을 펼친 초기 UI는 실제 실패였고, launcher와 화면 구조를 바꾸는 계기가 됐다.

작업 분배, 분리 작업장 보존, 진행 확인, 중단과 재개는 실제로 동작했다. 다만 일관되지는 않았다. 일부 대시보드 상태는 재시도가 필요했고, 지시는 흔들렸으며, 에이전트를 수정하려던 기능은 기대대로 동작하지 않았다. 방향을 정하고, 검토하고, 다시 시도하거나 멈출지 결정하는 책임은 계속 사람에게 있었다.

대시보드는 별도 브라우저 런타임 대신 터미널 TUI로 만들었다. 관제 대상이 이미 로컬 Git, 프로세스, 터미널 세션이었기 때문이다. 첫 작업 그래프는 모든 파일과 충돌을 펼쳐 끝없이 스크롤됐고 마우스 대상도 흔들렸다. 이후 task → worker → file 관계는 남기되 필요한 정보만 단계적으로 열고, 긴 목록은 +N more로 접고, 렌더러는 완성된 그래프를 화면 높이만큼만 잘랐다.

진행률은 관찰 가능한 근거로만 표시했다. 새 신고는 작업중? · 새 신고, WIP 커밋은 여전히 작업중?, 일반 브랜치 커밋은 커밋됨 · 착지 대기, 근거가 없으면 그대로 진행 근거 없음이다. mtime은 생존 신호가 아니므로 물음표를 지우지 않았다. 애니메이션은 주입된 160ms frame phase로, Git·backlog·ledger·세션 후보 수집은 느린 주기로 분리했다. 덕분에 렌더링은 결정적이고 수 GB 세션 기록을 매 프레임 다시 읽지 않는다.

실제 실패도 있었다. 다른 task에서 돌아오면 I-beam 커서와 클릭 어긋남이 남아 focus gain, resize, editor resume에서 raw mode·mouse protocol·cursor·전체 repaint를 하나의 재진입 경로로 복구해야 했다. 새 대시보드가 main에 착지했는데 화면은 옛 UI를 계속 보인 적도 있었다. launcher가 release 바이너리의 존재만 보고 재사용한 것이 원인이어서, 시작할 때 소스와 바이너리를 비교하고 재빌드 이유를 보이게 했다. 화면에는 provider와 확인된 짧은 모델 정보만 두고 prompt 전문, cwd, resume id는 숨겼다.

진행 근거 기능을 넣은 release bench에서는 worktree 16개, 추적 파일 1,398개, 세션 후보 1,684개(약 2.28GB)를 조건으로 읽기 루프 중앙 5.47ms·최대 10.38ms, 렌더 루프 중앙 0.38ms, 입력→재렌더 중앙 0.45ms, 유휴 render fork 0, RSS 12,592KiB를 기록했다. 이는 그 커밋·그 저장소 규모·그 터미널의 조건부 측정이지 일반 성능 약속이 아니다.

9월 1일 — 예산, 체크포인트, 인계

governor는 제품 성능과 관제 정책을 구분한다. 모델 강도와 워커 수는 닫힌 작업, 충돌, 제공받은 사용량, 로컬 자원에 따라 결정되며 제공자의 공식 한도를 뜻하지 않는다. 알 수 없는 사용량을 0으로 바꾸지 않고, 관제자가 usage_cap pause를 기록하면 다음 세션이 이를 따를 수 있다. 그렇다고 어느 제공자가 모든 구독 게이지를 스스로 읽어 자동 정지한다고 주장하지는 않는다.

Claude Fable 5.1 재개 맥락에서 드러난 현재 운영상 발견은 에이전트가 실행하는 제어 스크립트에 명시적인 프로젝트 로컬 권한 계약이 필요하다는 점이다. 대시보드 제어, 라우팅, ledger, checkpoint, refactor audit을 위한 좁은 allow-list가 없으면 워크플로는 예측 가능하게 이어지지 못하고 승인 경계에서 멈출 수 있다. 프로젝트에는 이미 .claude/settings.local.json에 이 다섯 스크립트 항목이 있으며, 이 경계는 실행을 감사 가능하게 할 뿐 자동 실행이나 성공을 보장하지 않는다. 이를 설명하는 구성은 새로 적용한 권한이 아니라 의도한 계약을 문서화한 것이다.

프로젝트 로컬 Claude 권한 경계는 다음처럼 의도적으로 좁다.

{
  "permissions": {
    "allow": [
      "Bash(scripts/dash-control.sh:*)",
      "Bash(scripts/agent-route.sh:*)",
      "Bash(scripts/ledger.sh:*)",
      "Bash(scripts/checkpoint.sh:*)",
      "Bash(scripts/refactor-audit.sh:*)"
    ]
  }
}

이는 이 범위의 제어 스크립트만 허용하는 경계다. 스크립트를 설치하거나 광범위한 실행 권한을 주거나 자율 동작을 증명하는 것은 아니다.

일시정지는 append-only worklog와 resume 기록 쌍으로 남긴다. 결과, 실행 기록, 다음 행동, 중지 조건을 보존하지만 새 ChatGPT/Codex 또는 Claude 세션은 여전히 Git과 제어 상태를 다시 측정해야 한다. 인계는 끝나지 않은 일을 억지로 커밋·병합·정리·빌드하지 않고 보존하는 절차다.

2026년 9월 1일 정책은 planning gauge의 내부 일일 상한을 small 1%p에서 max 5%p로 두고, 5시간 값은 5/24를 곱한 0.208333·0.416667·0.625000·0.833333·1.041667%p로 계산했다. 이것은 제공자의 관측 한도나 현재 계정 한도가 아니다. 작고 결정적인 일에는 Luna/Haiku, 중간 판단에는 Terra/Sonnet, 상태기계·보안 경계·충돌 중재 같은 관제 검토에는 Sol/Opus를 제한해 배치했다. 큐는 목표 동시성이 아니라 닫힌 작업, 파일 충돌, 사용량, 로컬 자원으로 더 줄어드는 소프트 상한이었다.

관제자는 파동 전후 /status/usage에서 사용량을 가져와야 했고, 저장소가 모든 구독 게이지를 스스로 읽지는 못했다. unknown을 0으로 바꾸지 않고 small 보정 파동 하나만 허용한 뒤 계속 모르면 다음 파동을 열지 않았다. stop_on_cap=true가 기본이며 manualusage_cap pause를 분리했다. 실제 기록에서 사용자가 준 근사 게이지를 검토해 revision 43에 usage-cap pause를 적었지만, 일일 baseline과 5시간 창의 세부값이 없던 부분은 만들지 않았다. 나중 Claude 세션이 그 pause를 지킨 것은 인계 연속성의 근거이지 무인 사용량 감지의 증거는 아니다.

비추적 control.ini는 pause, mode, brainstorm, refactor 상태의 원문이었다. scripts/dash-control.sh poll --since 42는 revision이 같으면 0바이트, 바뀌면 작은 JSON snapshot만 냈다. 렌더링 비용과 모델 컨텍스트 비용을 다른 경계에서 줄인 것이다. 여섯 번째 착지마다 diff-only 리팩터링 필요성 감사를 열고, none·inspect·refactor가 기록될 때까지 일곱 번째 착지를 거부했다. refactor 판정도 새 코드 작업을 자동 승인하지는 않았다.

체크포인트는 같은 id의 worklogresume 두 파일이다. 전자는 사용자에게 보인 결과, 정확한 명령, 결정과 승인을, 후자는 검증 전 snapshot, 다음 행동, 사용량 원장, 중지 조건을 남긴다. 게시 과정은 detached handoff worktree에서 이 두 파일만 stage하고 미작성 표식이나 키처럼 보이는 문자열을 거부했다. 재개 관제자는 지시·routing·checkpoint·backlog를 읽은 뒤 Git/control 상태를 다시 측정한다. checkpoint의 HEAD와 dirty state는 과거 관측이지 현재 사실이 아니다.

사람의 역할은 프롬프트를 던지고 기다리는 일이 아니었다. 닫힌 작업을 정의하고, 근거 기준을 정하고, 실제 결과를 확인하고, 다른 사람의 작업을 보존하고, 실패를 열린 상태로 둘지, 리팩터링·pause·재개를 허용할지를 결정하는 일이었다. 모델은 그 경계가 분명할 때 놀랄 만큼 많은 구현을 만들었지만, 빠른 구현이나 그럴듯한 보고는 그 판단을 대신하지 못했다. PetV3는 실물과 반례를 가진 운영 실험이지, 완성된 자율 공장을 선언하는 글이 아니다.