개요
실제 작업을 따라 하며 익히는 장입니다. 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) | tmux | provider CLI를 붙잡아 두는 좌석 |
| PM 프로바이더 CLI | Hermes | 관리자 역할 |
| 워커 프로바이더 CLI | AGY / 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 -qPYTHONIOENCODING=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_ID는 fail-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 — 첫 워커 계약 레인 만들기
목표: 의도 한 줄을 워커 계약으로 바꿉니다.
- 사용자가 의도를 한 줄로 말합니다.
- PM 역할(Hermes)이 현재 보이는 표면(로컬·스레드·워커·전경)을 읽고, 새 계약 레인을 만들지 기존 레인을 재사용할지 판단합니다.
- 워커 프로바이더(AGY)가 공식 전경 CLI로 그 계약을 받습니다.
- 확인 지점: 어느 워커가 어떤 계약 아래 있는지, 다음 판단이 누구 몫인지가 보드에 한 줄로 보입니다.
튜토리얼 2 — 칸반으로 한 단위 진행하기
목표: 한 결정을 접수에서 완료까지 흐르게 합니다.
- 작업 단위를 카드로 선언합니다 — 카드는 곧 결정입니다.
- 워커가 증거를 같은 스레드로 되돌리면, PM이 수용·재작업·보류를 판단합니다.
- 결정이 성숙하면 카드가 레인을 따라(접수 → 진행 → 검증 → 완료) 흐릅니다.
- 확인 지점: 끝난 카드는 지워지지 않고 보관으로 물러나며 이력이 남습니다 (supersede-not-delete).
튜토리얼 3 — 기억에서 맥락 회상하기
목표: 컨텍스트가 리셋돼도 작업을 이어받습니다.
- 결정은 마크다운 노드로 기억에 적힙니다(append-only).
- 컨텍스트가 비워진 뒤, 다룰 영역으로 회상을 호출하면 과거 결정이 다시 떠오릅니다.
- 무관한 프로젝트의 기억은 프로젝트 경계로 갈려 섞이지 않습니다.
- 확인 지점: 종결된 결정은 되살아나지 않고(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