개요

실제 작업을 따라 하며 익히는 장입니다. OpenArms의 조율은 다중 에이전트 분야에서 2026년 생산 기본값으로 자리 잡은 관리자·워커(Supervisor/Worker) 패턴에 가깝습니다 — 관리자 (PM 역할)가 의도를 분해해 워커에게 맡기고 결과를 종합합니다. 이 분야의 핵심 교훈은 “대부분의 실패는 에이전트 자체가 아니라 핸드오프 지점의 맥락 전달 문제”라는 것입니다. OpenArms가 전경 세션을 권위로 두고, 증거를 같은 스레드로 되돌리며, 이력을 supersede로 보존하는 이유가 여기 있습니다.

이 장은 튜토리얼을 따라 하기 전에, OpenArms가 어떤 도구로 무엇을 하고 왜 그걸 골랐는지 부터 정리합니다. 이름만 스쳐 지나가면 첫 설치에서 막히기 때문입니다. 더 넓은 개념은 핵심 개념에서, 설치 절차 자체는 빠른 시작·설치에서 다룹니다.


0. 스택 구성 — 무엇을 왜 골랐나

OpenArms 라이브 런타임은 다섯 갈래의 도구로 짜여 있습니다. 역할(role)은 안정 개념이고, 그 자리를 채우는 프로바이더(provider)는 교체 가능하다는 규칙이 전체를 관통합니다. 지금 라이브 배치에서 PM 자리는 Hermes, 워커 자리는 AGY(Antigravity)가 채우고 있지만, 이건 제품 기본값이 아니라 한 인스턴스일 뿐입니다.

갈래현재 채우는 것역할
Core 패키지Python ≥3.9 + telethon + jsonschema중립 엔진 (provider-agnostic)
메시징 프로바이더Telegram (telethon MTProto)사용자↔시스템 표면
실행석(execution seat)tmuxprovider CLI를 붙잡아 두는 좌석
PM 프로바이더 CLIHermes관리자 역할
워커 프로바이더 CLIAGY / Antigravity워커 역할

1. Python 패키지 + venv/uv — 의존성 경계

왜 이 구성인가

OpenArms Core는 의존성을 세 등급으로 강하게 분리합니다. 권위는 코드 메타데이터 (pyproject.toml, requirements-dev.txt)이며, 문서는 그 위의 사용자용 가이드입니다.

  • 필수 — 없으면 스위트가 아예 안 돕니다: telethon, jsonschema(둘 다 런타임 의존), 테스트 러너 pytest.
  • 권장 — 없으면 실패가 아니라 SKIP 되어 커버리지가 조용히 빠집니다: [networkx](https://networkx.org/) (wiki_chain 그래프 substrate, pytest.importorskip로 게이트), pyyaml(node_contract YAML-safe 렌더 경로).
  • 선택 — 없어도 통과하고 분기만 켜집니다: leidenalg+python-igraph(Leiden 커뮤니티 분기), resvg_py 또는 cairosvg(graph_network PNG 래스터화).

이렇게 나눈 이유: 첫 설치자가 “필수만 깔고 일단 돌려본다”와 “그래프·렌더까지 풀로 켠다”를 스스로 고를 수 있어야 하기 때문입니다. SKIP과 FAIL을 구분해 둔 덕에, 권장 의존성이 빠져도 스위트가 빨간불을 내지 않고 조용히 줄어든 커버리지를 정직하게 드러냅니다.

pyproject.toml의 실제 핀:

requires-python = ">=3.9"
dependencies = [
    "telethon>=1.24,<2",
    "jsonschema>=4.18",   # Draft202012Validator 가 4.18+ 필요
]

어떻게 구성하나 — venv (기본 경로)

표준 라이브러리만으로 끝나는, 모든 머신에서 동작하는 기본 경로입니다.

python -m [venv](https://docs.python.org/3/library/venv.html) .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest -q

-e(editable) 설치를 쓰는 이유: OpenArms는 scripts.<module>을 PEP 420 네임스페이스 패키지로 import 하는 테스트가 있어, editable 설치가 레포 경로를 그대로 살려 두어야 그 import가 깨지지 않습니다.

대안 — uv (속도가 중요할 때)

Astral의 [uv](https://docs.astral.sh/uv/)는 Rust로 작성된 통합 패키지 매니저로, 의존성 해석·설치가 pip 대비 10–100배, 환경 생성이 venv 대비 수백 배 빠릅니다. pip 호환 인터페이스(uv pip install)를 제공하므로 위 절차를 구조 변경 없이 그대로 가속할 수 있습니다.

uv venv .venv && source .venv/bin/activate
uv pip install -e ".[dev]"

선택 기준: 학습·호환성 우선이면 pip/venv, CI·재현성·속도 우선이면 uv. OpenArms는 어느 쪽이든 동일한 pyproject.toml을 권위로 읽으므로 둘은 상호 대체(supersede) 가능합니다.

설치 직후 점검 (preflight)

테스트를 돌릴 바로 그 인터프리터로 import-only 검사를 한 번 돌립니다(설치·네트워크 없음):

python3 scripts/dev_preflight.py     # 필수 모두 있으면 exit 0, 하나라도 없으면 비-0
PYTHONIOENCODING=utf-8 python3 -m pytest -q

PYTHONIOENCODING=utf-8는 한글 리터럴 출력이 깨지지 않도록 둡니다(특히 Windows 콘솔로 흘러가는 경우). Windows의 hermes-agent venv는 지원 러너가 아닙니다 — 거기엔 pytest/telethon이 없어 수집 자체가 안 됩니다. 테스트는 WSL/Linux/macOS의 python3로 레포 루트에서 돌립니다.


2. telethon (Telegram MTProto) — 메시징 표면

왜 telethon / MTProto인가

OpenArms의 첫(그리고 현재 라이브) 메시징 프로바이더는 Telegram이고, 그 클라이언트로 telethon(순수 Python 3 MTProto 라이브러리)을 씁니다. 두 통신 경로가 한 시스템 안에 공존합니다.

  • Bot HTTP API (getUpdates 롱폴링)api.telegram.org/bot<token>/getUpdates로 업데이트를 받습니다. 토큰만 있으면 되고 의존성이 가볍습니다. OpenArms의 인바운드 인젝터가 이 경로를 씁니다(urllib만으로 충분, 외부 의존성 0).
  • MTProto 사용자 세션 (telethon) — Telegram 서버에 직접 연결합니다. HTTP·폴링·웹훅이 없고, 봇 HTTP API가 노출하지 않는 동작(토픽 열거/생성/삭제, 미디어 다운로드, 소유자 권한 작업)에 접근할 수 있습니다. OpenArms는 토픽(스레드) 라이프사이클과 미디어 처리에 이 경로를 씁니다.

MTProto와 getUpdates는 서로 다른 id 공간을 갖습니다. 그래서 OpenArms 리스너는 둘의 seen-set을 절대 공유하지 않습니다 — MTProto의 last_seen이 getUpdates 필터를 오염시키면 메시지가 유실되기 때문입니다.

어떻게 구성하나

telethon은 StringSession으로 on-disk 파일 없이 세션을 적재/내보낼 수 있습니다. 라이브 배치에서는 소유자 MTProto 세션 하나를 PM/워커가 공유합니다(토픽 인벤토리·소유자 권한 작업의 단일 권위).

from telethon import TelegramClient
from telethon.sessions import StringSession
 
client = TelegramClient(StringSession(session_str), int(api_id), api_hash)

소유자 세션은 토픽 삭제 같은 파괴적 작업의 권위이므로, 절대규칙이 따라붙습니다: 열거 → 보고 → 명시적 id 리스트만 삭제, 통째 sweep 금지. 봇 토큰·세션 문자열·chat destination은 모두 owner-local secret으로, 레포에 커밋하지 않고 .env.example을 참조해 ~/.openarms/credentials/에 둡니다(권한 600).

메시징 표면 전반과 다른 플랫폼(Discord/Slack/LINE 등 미래 프로바이더)은 메시징 플랫폼 에서 다룹니다.


3. tmux — 실행석(execution seat)

왜 tmux인가

provider CLI(Hermes·AGY·Claude)는 전경 대화형 세션으로 돌아갑니다. 이 세션을 사람이 계속 붙잡고 있지 않아도 살아 있게 하고, 외부에서 입력을 흘려 넣을 좌석이 필요합니다. tmux가 그 backend입니다.

  • 분리 영속성: tmux new-session -d로 만든 세션은 터미널이 붙어 있지 않아도 계속 실행됩니다. 노트북을 닫고 다른 곳에서 SSH로 다시 붙어도 에이전트는 그대로 일하고 있습니다.
  • 입력 주입: tmux send-keys는 사람이 타이핑하듯 페인 입력 버퍼에 텍스트를 넣습니다. 관리자가 워커에게 작업을 전달하는 자연스러운 통로입니다.

중요한 위치 규정: tmux는 제품 의존성이 아니라 현재 구현의 backend입니다. 0.0.2.x 이후 방향은 execution_seat을 tmux/pty/ssh/windows-terminal/provider-native로 추상화해 tmux를 제품 정체성으로 굳히지 않는 것입니다. Windows native는 비주력(tmux가 기본 환경이 아님)이고, WSL/Linux/macOS/Mac VM이 선호 환경입니다.

어떻게 구성하나 — 그리고 흔한 안티패턴

OpenArms의 인젝터는 순수 인바운드 전송만 합니다. 텍스트를 페인에 넣을 때, 텍스트와 Enter 를 분리해 보냅니다 — -l(literal) 플래그로 텍스트가 키 이름으로 해석되는 것을 막고, Enter는 별도 send-keys로 보냅니다:

TMUX = os.environ.get("OPENARMS_TMUX", "tmux")
PANE = os.environ.get("OPENARMS_PM_PANE", "fleet:openarms-pm.0")
subprocess.run([TMUX, "send-keys", "-t", PANE, "-l", text])   # 텍스트는 리터럴로
subprocess.run([TMUX, "send-keys", "-t", PANE, "Enter"])      # Enter는 별도로

업계의 잘 알려진 안티패턴은 send-keys 뒤에 sleep 3 && capture-pane | grep ... 으로 응답을 긁어 babysit 하는 루프입니다 — tmux는 사람에겐 훌륭하지만 기계가 출력을 스크레이핑하기엔 깨지기 쉽습니다. OpenArms는 이 함정을 피하려고 인젝터가 응답을 capture/scrape 하지 않습니다. 주입받은 PM 워커(openarms-pm 스킬)가 자기 Bash 도구로 스스로 답을 송신합니다. 그 결과 인젝터는 brain-specific 파싱이 없는 순수 전송층으로 남고, 어떤 좌석에도 그대로 붙습니다.

배포판에서 훅을 어떻게 쓰나 — 인젝터 릴리스 연동

릴리스에서 인젝터는 nohup로 띄워 대화형 세션과 분리합니다. 핵심 훅은 백로그 스킵입니다 — 시작 시 getUpdates offset을 현재 최대 update_id + 1로 초기화해, 예전 테스트 메시지가 다시 주입되지 않게 합니다.

# run_pm_injector.sh (요지)
export OPENARMS_HOME="$HOME/.openarms"
set -a; . "$OPENARMS_HOME/credentials/telegram.env"; set +a   # 토큰 적재
export OPENARMS_PM_PANE="${OPENARMS_PM_PANE:-fleet:openarms-pm.0}"
 
pkill -f "openarms_provider/inject_listener.py" || true       # 이전 인스턴스 정리
nohup python3 .../inject_listener.py >> "$OPENARMS_HOME/logs/pm_injector.log" 2>&1 &
disown

좌석 부트스트랩(setup_pm_seat.sh)은 멱등(idempotent)하게 스킬을 배치하고 ~/.openarms를 만든 뒤, 토큰을 credentials/telegram.env로 옮기고(권한 600) 원본을 지웁니다. 토큰은 절대 출력하지 않습니다.


4. Hermes — PM 프로바이더 CLI

왜 Hermes를 PM에 두었나

Hermes는 두 파일럿 프로바이더 중 하나로, 라이브 테스트베드에서 PM(관리자·조율자) 역할에 바인딩되어 있습니다. 다시 강조하면 이건 인스턴스 선택이지 제품 기본값이 아닙니다 — PM은 안정 개념, hermes는 그 자리를 채우는 교체 가능한 프로바이더입니다.

Hermes를 PM에 둔 실무적 이유: Hermes는 공식 플러그인 hook 레이어를 제공해, Hermes 코어를 패치하지 않고도 OpenArms로 라우팅할 수 있습니다. PM은 인바운드를 받아 의도를 분해하고 결정을 내리는 자리라, 코어 침습 없이 결정 지점에 끼어드는 hook 모델이 정확히 들어맞습니다.

어떻게 구성하나

Hermes 연동은 integrations/hermes/openarms_contract_bridge/공식 플러그인 브리지가 담당합니다. plugin.yaml(매니페스트) + __init__.py(hook 어댑터) 구조이고, Hermes의 실제 플러그인 로더가 이걸 적재합니다. 설정 경계는 환경 변수로 그어집니다:

  • 바이너리/프로세스: OPENARMS_HERMES_BIN, OPENARMS_HERMES_SESSION_ID
  • tmux 표면: OPENARMS_HERMES_CLI_PANE, OPENARMS_HERMES_FOREGROUND_SESSION
  • 라우팅/계약: OPENARMS_HERMES_ROUTE_TARGET, OPENARMS_HERMES_ROUTE_EXECUTOR, OPENARMS_HERMES_CONTRACT

env 키는 OPENARMS_HERMES_*(현재 라이브)에서 role-first OPENARMS_*(중립 후계)로 일반화되는 중입니다. 런타임 env 경계 템플릿은 config/openarms.hermes.env.example입니다.

배포판에서 훅을 어떻게 쓰나 — 플러그인 매니페스트 + 4개 hook

릴리스에서 브리지는 매니페스트로 자신을 선언합니다. Hermes 코어를 패치하지 않고 공식 hook 4개에 붙는 것이 핵심입니다:

# integrations/hermes/openarms_contract_bridge/plugin.yaml
name: openarms-contract-bridge
version: "0.2.0"
pip_dependencies: [telethon>=1.36]
requires_env: [OPENARMS_ROOT, OPENARMS_HERMES_ROUTE_TARGET,
               OPENARMS_HERMES_ROUTE_EXECUTOR, OPENARMS_TMUX_BIN]
hooks:
  - pre_gateway_dispatch       # 인바운드 도착 → OpenArms 결정으로 라우팅
  - transform_llm_output       # 모델 출력 변형
  - cli_input_before_submit    # 전경 CLI 제출 직전
  - cli_response_after_render  # 전경 CLI 렌더 직후 (미러)

pre_gateway_dispatch hook은 인바운드 이벤트를 받아 OpenArms 결정으로 바꿉니다. Hermes는 이 hook에서 skip·rewrite·allow 셋 중 하나를 받습니다 — 즉 OpenArms가 “이 메시지를 어떻게 처리할지”를 코어 밖에서 판정해 돌려주는 구조입니다(전경 라우트가 바쁘면 queued로 보류 등). 이 hook 계약 덕에 OpenArms는 Hermes를 fork 하지 않고 플러그인으로만 얹힙니다.

Hermes/AGY가 채우는 역할 모델과 어댑터 구조 전반은 연동에서 다룹니다.


5. AGY / Antigravity — 워커 프로바이더 CLI

왜 AGY를 워커에 두었나

Antigravity CLI(agy)는 다른 파일럿 프로바이더로, 라이브 테스트베드에서 워커 역할을 채웁니다. 워커는 “전경 표면이 곧 권위”라는 규율로 동작합니다 — 현재 대화·첨부·워크스페이스 파일·가용 도구만 권위로 삼고, 오래된 스크린샷이나 stale 터미널 출력에서 추론하지 않습니다. AGY는 native 첨부 처리와 도구 게이트가 있어 이 “전경 = 권위” 워커 계약에 잘 맞습니다.

어떻게 구성하나

AGY 연동은 integrations/antigravity/openarms_provider/provider-native 플러그인 번들입니다: plugin.json(매니페스트) + commands/openarms-worker.md(명령) + hooks/openarms.json(hook 설정) + skills/openarms-worker/SKILL.md(워커 페르소나). Telegram/foreground/라우팅 설정은 OPENARMS_AGY_* env 키로 긋습니다. 그중 OPENARMS_AGY_TELEGRAM_CHAT_IDfail-closed 게이트 키로, AGY 복구에 반드시 필요합니다(없으면 AGY가 닫힌 채로 멈춤). 이 키들도 중립 OPENARMS_WORKER_* 후계로 일반화되는 중입니다(예: OPENARMS_WORKER_BOT_TOKEN).

배포판에서 훅을 어떻게 쓰나 — hook 설정 선언

릴리스에서 AGY 워커는 hook 설정 파일로 자신의 역할·권위·첨부 정책을 선언합니다:

// integrations/antigravity/openarms_provider/hooks/openarms.json
{
  "openarms_provider_bridge": {
    "role": "worker",
    "provider": "antigravity",
    "authority": "official_antigravity_cli_surface",
    "attachment_policy": "use_native_attachment_surface_before_fallback"
  }
}

authority가 “공식 Antigravity CLI 표면”으로 못박혀 있는 게 핵심입니다 — 워커는 자체 스크린샷이나 옆 폴더가 아니라 공식 CLI 표면을 통해 들어온 것만 권위로 봅니다. attachment_policy는 fallback 전에 native 첨부 표면을 먼저 쓰게 강제합니다. 예를 들어 이미지의 인물/출처/링크를 물으면 워커는 “제 눈으로 보거나 구글렌즈에 드래그”하지 않고 지정된 OpenArms 렌즈 도구(openarms-lens-identify)를 한 번 실행합니다 — 즉 hook 설정이 워커의 도구 사용 규율을 배포 시점에 고정합니다.


튜토리얼 1 — 첫 워커 계약 레인 만들기

목표: 의도 한 줄을 워커 계약으로 바꿉니다.

  1. 사용자가 의도를 한 줄로 말합니다.
  2. PM 역할(Hermes)이 현재 보이는 표면(로컬·스레드·워커·전경)을 읽고, 새 계약 레인을 만들지 기존 레인을 재사용할지 판단합니다.
  3. 워커 프로바이더(AGY)가 공식 전경 CLI로 그 계약을 받습니다.
  4. 확인 지점: 어느 워커가 어떤 계약 아래 있는지, 다음 판단이 누구 몫인지가 보드에 한 줄로 보입니다.

튜토리얼 2 — 칸반으로 한 단위 진행하기

목표: 한 결정을 접수에서 완료까지 흐르게 합니다.

  1. 작업 단위를 카드로 선언합니다 — 카드는 곧 결정입니다.
  2. 워커가 증거를 같은 스레드로 되돌리면, PM이 수용·재작업·보류를 판단합니다.
  3. 결정이 성숙하면 카드가 레인을 따라(접수 → 진행 → 검증 → 완료) 흐릅니다.
  4. 확인 지점: 끝난 카드는 지워지지 않고 보관으로 물러나며 이력이 남습니다 (supersede-not-delete).

튜토리얼 3 — 기억에서 맥락 회상하기

목표: 컨텍스트가 리셋돼도 작업을 이어받습니다.

  1. 결정은 마크다운 노드로 기억에 적힙니다(append-only).
  2. 컨텍스트가 비워진 뒤, 다룰 영역으로 회상을 호출하면 과거 결정이 다시 떠오릅니다.
  3. 무관한 프로젝트의 기억은 프로젝트 경계로 갈려 섞이지 않습니다.
  4. 확인 지점: 종결된 결정은 되살아나지 않고(supersede 인지), 회상이 맥락을 이어 줍니다.

무한 핸드오프를 피하는 규율

다중 에이전트의 대표 실패는 무한 핸드오프입니다 — A가 B에게, B가 C에게 넘기는데 누구도 단위를 소유하지 않아 맥락이 매 전달마다 닳습니다. OpenArms는 마이크로 버전마다 정지 결정 (continue·hold·rollback·stop_and_report)을 두고, 증거 강도가 오르지 않으면 멈춰 사람 판단으로 넘깁니다.

실제 운영에서 이 규율은 cron 하트비트로도 드러납니다. RAG 순환 칸반 루프의 한 틱은 워커 페인에 send-keys로 찔러 넣지 않고, 의도적으로 Telegram 메시지로 nudge를 보내 워커가 OpenArms 인그레스를 폴링해서만 받게 합니다 — 페인 직접 주입은 “가짜 시나리오 오염”으로 금지되어 있습니다. 즉 자동화조차 실제 메시징 경로를 통과하도록 강제됩니다.


출처(프로젝트 1차): docs/20_installation/README.md · integrations/hermes/README.md · integrations/antigravity/README.md · integrations/hermes/openarms_contract_bridge/plugin.yaml · integrations/claude_code/openarms_provider/inject_listener.py

출처(외부 모범사례): Multi-Agent Orchestration Patterns 2026 · How tmux Became the Runtime for AI Agent Teams · HTTP Bot API vs MTProto — Telethon docs · uv vs pip — Real Python

0건의 항목