# agent-desktop

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

- 수치: 보류 · 실행 전
- 사람이 보는 화면: https://aitryed.com/ko/teardowns/qa/agent-desktop/
- 반대 언어: https://aitryed.com/en/teardowns/qa/agent-desktop/index.md
- 저장소: https://github.com/vercel-labs/agent-desktop
- 이 사이트 안내(AI용): https://aitryed.com/ko/llms.txt

---

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

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

```bash
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](https://github.com/vercel-labs/agent-desktop/commit/9eb9b296192fcb70bda25e918cd6dd66abfa3d0d)입니다. 최신 릴리스는 `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에 적혀 있지만, 그 테스트를 돌려 보지는 않았습니다.

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

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

구현에서는 [`resolve_errors.rs`](https://github.com/vercel-labs/agent-desktop/blob/9eb9b296192fcb70bda25e918cd6dd66abfa3d0d/crates/core/src/refs/resolve_errors.rs)가 이 약속을 지킵니다. 후보를 읽다 막히면 「일치 없음」을 돌려주지 않고 `ErrorCode::Timeout`에 `complete: false`, `retryable: true`, 배달되지 않았다는 표시를 달아 돌려줍니다. 문서와 코드가 같은 말을 하고 있었습니다.

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

저장소의 교훈 문서 중 [AT-SPI 쓰기](https://github.com/vercel-labs/agent-desktop/blob/9eb9b296192fcb70bda25e918cd6dd66abfa3d0d/docs/solutions/runtime-errors/an-atspi-write-that-returns-success-has-not-changed-anything.md)가 가장 값이 컸습니다. 리눅스에서 입력칸에 글을 써 달라고 하면 성공이라고 답하고도 아무것도 안 바뀌는 경우가 툴킷마다 있고(파괴된 위젯, 읽기 전용 칸, 파이어폭스 입력칸), 반대로 실패라고 답했는데 나중에 반영되는 경우(멈춘 앱이 재개될 때)도 있었다고 합니다.

처방은 한 줄입니다. **답이 아니라 쓴 뒤에 다시 읽은 값으로 판정한다.** 쓰기 전과 쓴 뒤의 값이 같으면 `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개월짜리 저장소라 해석에 쓸 만큼 쌓였는지도 판단하지 못했습니다.
