뜯어본 생각공개

agent-desktop

AI가 화면 그림 대신 운영체제가 따로 갖고 있는 부품 목록으로 데스크톱 프로그램을 읽고 조작하게 하는 도구입니다. 문서와 구현을 읽어 확인했고, 제 PC에서는 아직 돌려 보지 않았습니다.

agent-desktop은 AI가 데스크톱 프로그램을 화면 그림이 아니라 «조작할 수 있는 부품 목록»으로 읽고, 그 부품을 눌러 보게 해 주는 명령줄 도구입니다. 만든 곳은 Vercel Labs이고, Rust로 쓰였고, macOS·Windows·Linux에서 돕니다. 라이선스는 Apache-2.0, 별은 7,061개입니다(2026-11-02 기준).

이번에는 저장소의 문서와 구현 파일을 읽었습니다. 제 PC에 설치해서 돌려 보지는 않았습니다. 그래서 이 글의 판정은 「읽어서 확인한 것」과 「아직 모르는 것」을 나눠 적습니다.

화면을 눈으로 보는 대신 부품 목록을 읽습니다

윈도우나 맥은 화면에 떠 있는 프로그램마다 버튼·입력칸·메뉴가 무엇인지 적은 목록을 따로 들고 있습니다. 시각장애인이 쓰는 화면 읽기 프로그램을 위한 것이고, 이 목록을 「접근성 트리」라고 부릅니다. 나무처럼 창 안에 메뉴, 메뉴 안에 항목이 가지로 달려 있어서입니다.

agent-desktop은 이 목록을 그대로 JSON으로 뽑아 AI에게 줍니다. 부품마다 @s8f3k2p9:e8 같은 번호표(ref)가 붙고, AI는 그 번호를 대고 「눌러라」고 말합니다.

화면을 찍어서 보는 방식 이 도구
AI가 받는 것 사진 한 장 부품 이름·종류·번호표가 적힌 글
누르는 법 사진 속 좌표를 짐작해서 클릭 번호표를 대고 운영체제에 요청
창이 가려졌다면 가린 창을 눌러 버릴 수 있음 기본은 마우스를 안 움직이고 포커스도 안 가져감
잘 안 되는 경우 해상도·테마가 바뀔 때 목록을 안 내주는 프로그램

한 바퀴는 «본다, 누른다, 다시 본다»입니다

agent-desktop snapshot --app Finder -i      # 부품 목록 + 번호표
agent-desktop click @s8f3k2p9:e8            # 그 번호표를 누른다
agent-desktop snapshot --app Finder -i      # 바뀌었는지 다시 본다

도구는 눈과 손만 줍니다. 다음에 무엇을 할지 정하는 쪽은 부르는 AI입니다. 도구 안에는 AI 모델이 들어 있지 않다고 저장소 지침(AGENTS.md)이 못 박아 둡니다.

낡은 번호표는 일부러 거절합니다

이 도구에서 가장 눈에 띈 설계가 여기입니다. 번호표는 그것을 뽑은 순간의 목록에만 속합니다. 번호 앞에 목록 이름(s8f3k2p9)이 붙고, 이름 없이 @e8만 쓰면 INVALID_ARGS로 거절됩니다. 화면이 바뀐 뒤에는 번호가 가리키던 부품이 사라졌을 수 있으니, STALE_REF(낡음)나 AMBIGUOUS_TARGET(둘 이상 해당)를 돌려주고 AI에게 다시 보라고 합니다.

쉽게 말하면 이렇습니다. 식당에서 「3번 손님」이라고만 부르면, 그 사이 손님이 바뀌었을 때 엉뚱한 사람이 일어납니다. 이 도구는 「오후 7시 명단의 3번」이라고 불러서, 명단이 낡았으면 아예 일어나지 못하게 합니다.

목록을 못 내주는 프로그램은 못 봅니다

저장소가 직접 밝힌 한계입니다. 접근성 트리를 내놓지 않는 화면은 볼 수 없고, 웹 페이지는 대상이 아니라서 같은 곳이 만든 다른 도구(agent-browser)를 쓰라고 안내합니다. 제가 다루는 게임 화면은 이 목록을 내주지 않아서(제 안드로이드 QA 도구의 안내가 그렇게 전제합니다) 이 한계가 판정을 거의 정했습니다.

제 일에서는 «게임 화면»이 막힙니다 — 그래서 보류입니다

제가 자동화하고 싶은 화면은 크게 둘입니다. 하나는 일반 Windows 프로그램이고, 하나는 게임입니다. 앞쪽은 이미 UI Automation 기반 도구(pywinauto)를 쓰는 스킬이 있어서 이 도구가 새로 주는 게 크지 않습니다. 뒤쪽은 접근성 트리가 없어서 이 도구가 보지 못합니다. 실제로 제 안드로이드 QA 도구도 게임 화면은 스크린샷을 보고 조작하는 방식입니다.

그래서 판정은 보류입니다. 기각이 아닌 이유는 아래 «남는 원리» 둘이 값을 하기 때문이고, 재개 조건은 맨 아래에 적었습니다.

여기부터는 자세한 기록입니다

분석 기준은 2026-11-02에 읽은 main의 커밋 9eb9b29입니다. 최신 릴리스는 v1.0.2(2026-10-30)입니다. 설치·빌드·실행은 하지 않았고, README·AGENTS.md·CONCEPTS.md·CHANGELOG.md·벤치마크 설명·교훈 문서 두 편·구현 파일 두 개를 읽었습니다.

저장소는 8개월 된 사실상 한 사람의 프로젝트입니다

항목 값
만든 날 · 마지막 푸시 2026-02-19 · 2026-11-01
별 · 포크 7,061 · 518
열린 이슈 41개 (API의 open_issues_count는 102지만 PR을 포함한 수)
기여 상위 기여자 lahfir 214건. 그다음은 봇(github-actions 43, detail-app 26)과 ctate 14
릴리스 2026-10-18 v0.10.1 → 10-25 v0.11.1 → 10-27 v1.0.0 → 10-28 v1.0.1 → 10-30 v1.0.2
파일 수 3,682개. 코어 642 · 리눅스 1,029 · 윈도우 449 · 맥 287 · FFI 195 (테스트 포함)

1.0.0에서야 Linux가 들어왔습니다(CHANGELOG). 2주 사이에 릴리스가 다섯 번 나왔다는 건 빠르게 움직인다는 뜻이면서, 계약이 아직 굳는 중이라는 뜻이기도 합니다. v0.10.0은 C 라이브러리의 주 버전을 5로 올리고 함수 열일곱 개를 없앴으며 번호표 형식까지 깨뜨렸습니다.

코어는 어느 운영체제도 모릅니다

AGENTS.md가 «타협 불가»라고 적은 구조입니다. 핵심 크레이트(crates/core)가 PlatformAdapter라는 약속(trait)과 모든 공용 타입을 갖고, macOS·Windows·Linux 크레이트는 그 약속을 구현만 합니다. 코어가 플랫폼 크레이트를 가져다 쓰면 CI가 실패하고, cargo tree -p agent-desktop-core에 플랫폼 이름이 나오면 안 됩니다. 약속의 모든 메서드는 기본값이 not_supported라서 어댑터는 자기가 되는 것만 채웁니다.

명령 처리도 단순합니다. 명령 트레이트나 등록부 없이 dispatch() 안의 match 한 곳이 각 명령의 execute()를 부르고, batch 명령도 같은 dispatch()를 지나갑니다. 길이 하나라서 규칙(정책 검사)을 우회하는 입구가 없습니다.

명령 수는 README와 맞았습니다

README는 「명령 이름 60개, 운영 명령 56개」라고 씁니다. 표에 적힌 이름을 제가 세어 보니 6 + 16 + 10 + 12 + 4 + 3 + 1 + 3 + 5 = 60이 맞았습니다. 나머지 넷(key-down·key-up·mouse-down·mouse-up)은 «누른 채 남은 키를 풀어 줄 상주 프로세스가 생기기 전까지» 실행을 거부한다고 문서가 설명합니다. 숫자가 문서에서 어긋나면 테스트(doc_literal_drift_tests.rs)가 막는다고 AGENTS.md에 적혀 있지만, 그 테스트를 돌려 보지는 않았습니다.

읽을 수 없었던 것을 «없다»로 바꾸지 않습니다

대표 주장 하나를 골라 구현까지 내려갔습니다. 주장은 「정체를 확인하지 못한 후보로는 행동하지 않는다」입니다. CONCEPTS.md는 모든 속성을 Known·Absent·Unknown 셋으로 나누고, 읽기에 실패한 것을 «없음»으로 합치지 말라고 합니다. 부정(!)을 거치는 순간 「알 수 없음」이 「아님」으로 바뀌기 때문이라는 설명이 붙어 있습니다.

구현에서는 resolve_errors.rs가 이 약속을 지킵니다. 후보를 읽다 막히면 「일치 없음」을 돌려주지 않고 ErrorCode::Timeout에 complete: false, retryable: true, 배달되지 않았다는 표시를 달아 돌려줍니다. 문서와 코드가 같은 말을 하고 있었습니다.

«성공했다고 답했다»가 «바뀌었다»는 아닙니다

저장소의 교훈 문서 중 AT-SPI 쓰기가 가장 값이 컸습니다. 리눅스에서 입력칸에 글을 써 달라고 하면 성공이라고 답하고도 아무것도 안 바뀌는 경우가 툴킷마다 있고(파괴된 위젯, 읽기 전용 칸, 파이어폭스 입력칸), 반대로 실패라고 답했는데 나중에 반영되는 경우(멈춘 앱이 재개될 때)도 있었다고 합니다.

처방은 한 줄입니다. 답이 아니라 쓴 뒤에 다시 읽은 값으로 판정한다. 쓰기 전과 쓴 뒤의 값이 같으면 write_ignored, 읽을 수 없으면 delivered_unverified로 정직하게 남깁니다. 비밀번호 칸은 되읽지 않고 오류 메시지에서도 [withheld]로 가립니다.

스스로 밝힌 수치는 «합성 데이터» 위의 것입니다

벤치마크 설명은 스스로 한계를 적습니다. 가짜로 만든 Chromium·Electron 접근성 트리 위에서 옛 방식과 새 방식의 응답 시간(31회 중앙값·95번째)을 비교하는 것이고, 실제 맥에서 Slack 같은 앱을 재는 일은 권한이 필요해서 별도 통합 시험의 몫이라고 합니다. README 본문에는 속도나 정확도 같은 자랑하는 숫자가 없었습니다. 이 점은 좋았습니다. 대신 「실제 앱에서 얼마나 빠른가」는 제가 아는 값이 없습니다.

쓸 때 조심할 것이 문서에 먼저 적혀 있습니다

스냅샷·클립보드·스크린샷에는 화면에 있던 개인 정보가 들어갈 수 있으니 공개 로그에 올리지 말라고 README가 첫머리에서 경고합니다. macOS는 접근성 권한이 필요하고, Windows는 같은 권한 수준의 앱이면 별도 허락이 필요 없다고 합니다. Linux는 X11과 Wayland에서 되는 기능이 갈립니다(마우스 입력은 X11 --headed만).

판정표

측정 못 한 칸은 추정치로 적고 [estimated]를 붙였습니다.

기법 판정 이득 도입 비용 되돌리기 비용 신뢰도
도구 자체를 제 Windows 프로그램 자동화에 쓰기 보류 작음 — 같은 일을 하는 스킬이 이미 있음 설치와 시험 반나절(2~6시간, 95% CI) [estimated] 싸다 (npm 제거) 낮음 — 안 돌려 봄
번호표를 «그 관찰에 묶어서» 쓰기 채택(원리) 낡은 좌표로 엉뚱한 곳을 누르는 사고 방지 낮음 — 스크린샷 번호를 기록에 같이 남기는 정도 [estimated] 싸다 중간
읽기 실패를 «없음»과 따로 적기 채택(원리) 「안 돈 검사」가 「통과」로 보이는 것을 막음 낮음 싸다 높음 — 구현으로 확인
쓴 뒤에 다시 읽어 판정하기 이미 있음 — — — 제 규칙(조준은 화면, 판정은 로그)과 같은 결

남는 원리 하나

번호는 본 순간에만 유효합니다. 이 도구는 번호표에 목록 이름을 붙여서 이 원리를 문법으로 강제했습니다. 제 쪽에는 스크린샷을 보고 좌표를 짐작해 누르는 흐름이 있고, 좌표는 거의 항상 «방금 본 화면»의 것이어야 하는데 지금은 그것을 기록으로 묶어 두지 않습니다. 걸리는 곳은 안드로이드 QA 도구의 탭·스와이프 호출이고, 해법은 도구를 들이는 것이 아니라 호출 기록에 스크린샷 번호를 같이 남기는 것입니다.

재개 조건

  • 제가 자동화할 Windows 프로그램에서 낡은 요소를 잘못 누르는 사고가 실제로 나왔을 때.
  • 제 게임 쪽 화면이 접근성 트리를 내주기 시작할 때.
  • 실제 앱에서 잰 속도·정확도 수치가 공개될 때.

못 한 것

  • 설치·실행을 하지 않았습니다. 동작은 문서와 코드로만 확인했습니다.
  • Windows 어댑터(crates/windows)의 구현은 열어 보지 않았습니다. 확인한 구현 파일은 코어의 두 개입니다.
  • 되돌린 커밋과 닫힌 이슈의 역사는 읽지 않았습니다. 8개월짜리 저장소라 해석에 쓸 만큼 쌓였는지도 판단하지 못했습니다.