pi-computer-use는 에이전트에게 진짜 마우스 클릭과 키보드 입력을 발생시킬 권한을 준다. macOS에서는 손쉬운 사용(Accessibility)과 화면 기록(Screen Recording) 권한을 헬퍼에 부여해야 하며, 이는 곧 헬퍼가 화면 전체를 캡처하고 어떤 앱에든 입력을 보낼 수 있다는 뜻이다.
그래서 설계는 "실수해도 덜 위험하게"에 집착한다 — 불확실하면 실패로 처리하고(fail closed), 애매한 포인터 동작은 절대 재시도하지 않으며, headless 모드에서는 사용자의 포커스·커서를 건드리지 않는다. 그래도 원리상 에이전트가 오작동하면 실제 앱을 잘못 조작할 수 있으니, 중요한 작업 화면에서는 신중히 쓰는 게 맞다.
Pi 에이전트에 이 패키지를 설치하면 observe_ui, act_ui 같은 도구가 생긴다. 에이전트는 observe_ui로 지금 떠 있는 창의 구조(버튼·텍스트필드·목록)를 글자로 정리된 아웃라인으로 받아 보고, act_ui로 "이 버튼을 눌러", "이 칸에 글자를 넣어" 같은 동작을 시킨다. API가 없는 데스크톱 앱을 상대할 때, 사람이 하듯 화면을 매개로 일을 시키는 것이다.
전화로 원격 지원해 주는 IT 헬프데스크를 떠올리면 된다. 헬프데스크 직원(에이전트)은 당신 화면을 직접 못 보지만, 당신이 "지금 화면에 파란 로그인 버튼이 왼쪽 위에 있고, 그 아래 아이디 칸이 있어요"라고 설명(observe_ui의 아웃라인)하면 "그 아이디 칸에 이걸 입력하고 로그인 버튼을 누르세요"라고 지시(act_ui)한다.
pi-computer-use는 이 "화면을 글자로 설명해 주는 사람"을 네이티브 접근성 API로 자동화한 것이다. 사람 대신 macOS의 AX나 Windows의 UIA가 화면 구조를 읽어 에이전트에게 넘긴다. 그래서 매번 스크린샷 픽셀을 추측하는 것보다 훨씬 정확하다.
pi-computer-use의 정체성은 "Pi의 확장"이라는 데 있다. Pi는 @earendil-works/pi-coding-agent라는 이름의 코딩 에이전트 런타임인데, pi-computer-use는 여기에 pi.registerTool(...)로 컴퓨터 유즈 도구 11개를 등록한다. 즉 이 저장소 자체는 "에이전트"가 아니라 "에이전트에게 손발을 붙이는 부품"이다. 덕분에 프로젝트 범위가 명확하다 — 모델을 만들거나 프롬프트를 설계하는 게 아니라, 관측·행동·검증이라는 세 동작을 얼마나 정확하고 안전하게 제공하느냐에만 집중한다.
컴퓨터 유즈 도구는 이미 여럿 있다. 대부분은 스크린샷을 찍어 모델에게 이미지로 주고, 모델이 "여기 클릭"이라고 좌표를 뱉으면 그 좌표를 클릭하는 방식이다. 단순하지만 약점이 크다 — 화질·배율·폰트에 따라 좌표가 흔들리고, 클릭이 실제로 먹혔는지 확인할 방법이 없다. pi-computer-use는 이 두 약점을 정면으로 겨냥한다.
| 방식 | 화면을 어떻게 주나 | 한계 |
|---|---|---|
| 스크린샷 + 좌표 | 이미지 픽셀만 | 좌표가 배율·테마에 취약, 버튼 "이름"을 모름, 결과 검증 불가 |
| OCR 기반 | 이미지에서 글자 추출 | 글자는 읽지만 "이게 누를 수 있는 버튼인지"는 모름 |
| pi-computer-use | 접근성 트리(AX/UIA)를 글자 아웃라인으로, 필요시 이미지·OCR 보강 | 네이티브 헬퍼를 OS별로 구현해야 함(그게 이 저장소의 무게) |
접근성 트리는 OS가 스크린리더(시각장애인용 화면 낭독기)를 위해 이미 만들어 둔 화면의 의미 구조다. "여기 좌표에 뭔가 있다"가 아니라 "이건 이름이 저장인 버튼이고 누를 수 있다"를 알려준다. pi-computer-use는 이걸 1차 근거로 삼아, 좌표 클릭은 접근성 요소가 없는 그림 영역(pictureOnly)에만 예외적으로 쓴다.
이 프로젝트가 진짜 공들인 지점이다. act_ui는 동작을 보낸 뒤 결과를 worked(먹힘) / didnt(안 먹힘) / unknown(모름) 세 가지로 정직하게 보고한다. "이벤트를 보냈다"는 사실만으로는 성공이라고 절대 치지 않는다. 버튼을 눌렀는데 앱이 그 이벤트를 씹었을 수도 있으니, 원하면 expect로 "누른 뒤 '저장됨'이라는 글자가 뜰 것"이라는 사후조건(postcondition)을 붙일 수 있고, 그게 확인돼야만 성공 처리된다.
택배 기사가 초인종을 눌렀다고 해서 "배송 완료"가 아니다. 문이 열리거나 부재중 표시가 확인돼야 상황이 끝난 것이다. pi-computer-use의 expect가 바로 이 "문이 열렸는지 확인"이다. 이벤트를 보낸 것(초인종)과 결과가 바뀐 것(문 열림)을 엄격히 구분한다.
macOS는 Swift로 AX(Accessibility)와 ScreenCaptureKit를, Windows는 Rust로 UIA(UI Automation)를 쓴다. 내부 구현은 완전히 다르지만, 이 저장소는 두 헬퍼가 똑같은 불변식(invariant) 집합을 지키도록 강제한다. 헬퍼는 시작할 때 자기가 구현한 불변식 목록과 architectureVersion을 보고하고, 필수 불변식이 빠지면 기동 자체가 실패(fail closed)한다. "OS별로 동작이 미묘하게 다른" 흔한 함정을 구조로 막은 것이다.
또 하나 눈에 띄는 건 테스트·검증 스크립트의 밀도다. scripts/ 폴더에 스키마 검사, 세션 생명주기 검사, 런타임 동시성 검사, 출력 상한 검사, 커밋 메시지 검사, 서명 검사까지 10여 개의 자체 검증 러너가 있고, pi-cubench-agent.mjs라는 벤치마크(Cubench)로 두 OS에서 같은 블랙박스 속성을 돌린다. 별 137개짜리 신생 프로젝트치고 엔지니어링 규율이 상당히 촘촘하다는 신호다.
src/)에이전트와 네이티브 헬퍼 사이를 잇는 두뇌다. 모델의 의도를 해석하고, 상태를 저장·복원하고, 자원 스케줄링을 담당한다. 실제 그라운딩(어디를 누를지 확정)과 입력 전달은 네이티브 헬퍼에 맡긴다.
| 모듈 | 줄 수 | 역할 |
|---|---|---|
bridge.ts | 2,283 | 도구 실행 코디네이터 + 자원 스케줄링. 11개 도구의 execute* 진입점 |
outline.ts | 651 | 완전한 UI 트리를 파싱·질의(접힘/펼침, 검색 랭킹) |
cdp.ts | 658 | Chrome DevTools Protocol로 브라우저 페이지를 데스크톱과 같은 아웃라인으로 변환 |
note.ts | 195 | 에이전트에게 주는 프롬프트 스니펫·가이드라인 문구 |
view.ts | 147 | 공개 ref(@r/@e) 안정화 + 전체뷰/변경뷰 렌더링 |
state.ts | 146 | 불변 UI 상태 저장·복원·직렬화, 요청-로컬 상태 |
output.ts | 134 | 모델 출력 상한(48KiB/2000줄) + @o 연속 읽기 |
actions.ts | 130 | 동작 검증·정규화·타깃 해석·의존 포커스·안전 재시도 판정 |
runtime.ts | 129 | 요청 1건의 실행 파이프라인(상태 로드→준비→실행→관측→저장) |
config.ts·permissions.ts | 113·111 | 설정 병합(파일+env) / 권한 상태 점검 |
platform/* | — | OS별 관측·입력·네이티브 프로토콜 번역(플랫폼 중립 seam) |
native/macos/)핵심은 bridge.swift 3,544줄 단일 파일이다. AXRefStore(접근성 요소 참조 저장), LookNode(관측 트리 노드), Bridge(소켓 서버 본체), InputSuppressionGuard(입력 억제 가드)가 들어 있다. 화면 캡처는 ScreenCaptureKit, UI 구조는 AX(Accessibility) API, 유령 커서 오버레이는 AppKit으로 그린다(agent_cursor.swift, agent_cursor_motion.swift). 소켓 서버는 메인 스레드 밖에서 돌고, AppKit이 메인 런루프를 소유한다.
native/windows/bridge-rs/)| 파일 | 줄 수 | 역할 |
|---|---|---|
main.rs | 1,683 | 라인 프로토콜 서버, 요청 디스패치, 물리 입력 락, 워커 상태 |
uia.rs | 1,252 | UIA(UI Automation) 트리 추출·경계 있는 탐색 |
window.rs | 718 | 창 열거·루트 스냅샷·이벤트 저널(SetWinEventHook) |
capture.rs | 518 | 화면 캡처 |
input.rs | 501 | 마우스·키보드 입력 전달 |
refs.rs·state.rs·protocol.rs·error.rs | 237·55·53·81 | ref 관리·헬퍼 상태·프로토콜·에러 |
Windows 헬퍼는 macOS의 TCC 권한 흐름이나 헬퍼 앱을 쓰지 않고, 대화형 데스크톱 세션에서 UIA로 바로 동작한다. 고정 크기 워커 풀을 쓰고 각 워커 스레드마다 UIA를 초기화하며, 물리 입력이 필요한 배치만 전역 입력 락을 잡는다.
| 영역 | 내용 |
|---|---|
| 언어 비중 | TypeScript 65% · Swift 30% · JavaScript 5% (+ Windows용 Rust) |
| 런타임 | Node.js 20.6+ (TS를 --experimental-strip-types로 직접 실행) |
| 피어 의존성 | @earendil-works/pi-ai, @earendil-works/pi-coding-agent, typebox(도구 스키마) |
| 브라우저 | Chrome DevTools Protocol(CDP) — chrome 또는 helium 관리 브라우저 |
| 배포 | npm(@injaneity/pi-computer-use) + prebuilt 헬퍼 바이너리 동봉, 없으면 첫 실행 시 로컬 빌드 |
| 라이선스 | MIT |
이 장이 문서의 심장이다. pi-computer-use의 아키텍처는 한 문장으로 요약된다 — "루트를 찾고 → 한 루트를 관측하고 → 그 관측 상태 안에서 검색/펼침/조사하고 → 그 상태로부터 행동한다." 여기서 반복해서 등장하는 세 가지 참조를 먼저 익히면 나머지가 쉽다.
| 참조 | 뜻 |
|---|---|
@r1 (root) | 제어 가능한 UI 루트 하나 — 데스크톱 창, 메뉴/시트 같은 임시 표면, 또는 CDP 브라우저 페이지 |
@e12 (element) | 한 관측 상태 안의 UI 요소 하나 — 버튼·텍스트필드 등. 그 상태의 stateId에만 속함 |
@o1 (output) | 너무 길어 잘린 텍스트의 연속 읽기용 불변 참조. UI 상태가 아니라 세션-로컬 |
stateId | 관측 한 번의 결과에 붙는 도장. 모든 @e 참조는 자기 stateId와 함께 써야 함 |
@r 참조를 준다. 세션 전체를 관통하는 "지금 보고 있는 창" 같은 전역 상태가 없다는 게 핵심이다. 매 호출이 자기 stateId로부터 필요한 상태를 새로 불러오므로, 서로 다른 호출이 실수로 상대를 덮어쓸 수 없다.가장 독창적인 부분이다. 일반적인 자동화 도구는 "현재 화면"이라는 가변(mutable) 전역 상태를 두고 매번 갱신한다. 그런데 Pi는 도구 호출을 동시에(concurrently) 날릴 수 있다. 이러면 A 호출이 화면을 바꾸는 사이 B 호출이 옛 화면 기준으로 클릭해 버리는 경합(race)이 생긴다.
pi-computer-use의 해법은 "현재 화면을 아무도 공유하지 않는다"이다. 관측 결과는 전부 불변(immutable)으로 저장되고, 각 물리 자원(프로세스·CDP 타깃)에는 단조 증가하는 에포크(epoch)가 붙는다. 화면을 바꾸는 호출은 자기 상태가 캡처한 에포크를 반드시 제시해야 하고, 그 사이 다른 쓰기가 이겼으면 오래된 호출은 전송 전에 stale 에러로 거부된다.
공유 문서를 여럿이 편집할 때의 버전 충돌 감지와 똑같다. 내가 3번 버전을 보고 수정하려는데 그새 누가 4번으로 올려놨으면, 시스템이 "당신은 옛 버전 기준입니다. 다시 불러오세요"라고 막는다. 조용히 덮어써서 남의 변경을 날리는 것보다, 명시적으로 거부하는 편이 안전하다. 에포크가 바로 이 버전 번호다.
동시성을 무조건 막으면 느리다. pi-computer-use는 같은 물리 자원을 건드리는 라이브 동작만 순서대로 처리하고, 서로 다른 자원은 병렬로 흘려보낸다.
| 스케줄링 키 | 규칙 |
|---|---|
| 데스크톱 | 프로세스(pid) 단위로 직렬화. 창이 아니라 프로세스인 이유: 접근성 포커스·메뉴·물리 입력이 한 앱의 창 경계를 넘나들기 때문 |
| 브라우저 | CDP 페이지 타깃 단위로 직렬화 |
| 캐시 질의 | search_ui·expand_ui·inspect_ui는 스케줄러를 완전히 우회(저장된 불변 상태만 읽으므로 얼마든 겹쳐도 안전) |
| 물리 입력 | 전역 물리 입력(HID)은 네이티브 헬퍼에서 뮤텍스로 보호. 의미론적 AX/UIA 작업은 플랫폼이 허용하면 겹칠 수 있음 |
큰 앱의 접근성 트리는 수천 개 노드로 폭발한다. 이걸 통째로 모델에게 주면 토큰이 낭비되고 정확도도 떨어진다. 그래서 observe_ui는 처음에 접힌(folded) 요약뷰만 준다. 세부는 필요할 때만 캐시된 완전한 트리에서 꺼낸다.
// 1) 처음엔 컴팩트한 관측
observe_ui({ root: "@r1" })
// 2) 캡처를 새로 하지 않고 저장된 상태만 질의
search_ui({ stateId, text: "저장" }) // 텍스트/역할/능력으로 랭킹 검색
expand_ui({ stateId, ref: "@e7", depth: 3 }) // 한 요소 주변만 펼침
inspect_ui({ stateId, ref: "@e12" }) // 필드·좌표·능력·증거 자세히
관측 모드는 3단계다 — semantic(접근성만, 가장 쌈), fused(기본, 자동 선택), visual(이미지·OCR 강제). 검색은 정확→접두→부분 일치를 보수적 퍼지 매치보다 우선해 랭킹하고, 고정된 상위 집합 + 총 매치 수를 돌려주며, 검색이 너무 넓으면 "좁혀서 다시 검색하라"고 요구한다(매치를 페이지 넘김하며 훑지 않는다).
act_ui는 의존적인 동작 목록 하나를 받는다. 배열 길이가 1이면 단일 동작, 여러 개면 한 트랜잭션이다. 단, 배치는 "중간 관측이 필요 없을 때만" 허용된다 — 두 번째 동작이 첫 번째의 결과를 봐야 한다면 한 개씩 보내고 관측해야 한다.
// 편집 영역 클릭 → 그 포커스로 타이핑 (ref 생략 = 방금 만든 포커스에 입력)
act_ui({
stateId,
actions: [
{ action: "click", x: 420, y: 300 },
{ action: "typeText", text: "hello" },
],
expect: { text: "hello" }, // 이 글자가 떠야 성공
})
사후조건이 붙으면 헬퍼는 지정한 텍스트·역할이 나타날(또는 until:absent로 사라질) 때까지 기다린 뒤 성공을 보고한다. 검증 결과는 verified(새로 확인됨)·preexisting(이미 그 상태였음, 인과 증거 아님)·failed로 구분된다. 사후조건이 실패하면 실행 결과는 didnt로 바뀐다 — 이벤트를 보낸 것만으로는 절대 의미론적 성공으로 치지 않는다.
여기에 headless 플래그가 두 세계를 가른다. headless: true면 배경 경계가 엄격하다 — 앱을 앞으로 띄우지도, 사용자 포커스 창을 바꾸지도, 전역 커서를 움직이지도, 날 입력을 쏘지도 않는다. headless: false(기본)면 배경에서 의미론적 활성화를 시도하고, 검증에 실패한 부작용 없는 키보드 입력은 전경(foreground)으로 자동 승격해 재시도한다. 단 애매한 포인터 동작은 절대 맹목적으로 재시도하지 않는다(같은 클릭을 두 번 하면 두 번 눌릴 위험).
동작이 끝나면 화면 전체를 다시 주는 건 낭비다. view.ts는 동작 후 상태를 이전 상태와 비교해 신뢰할 수 있는 변경분(added/updated/removed)만 렌더링하고 다음 stateId를 준다. 요소가 확실히 매칭되면 모델이 쓰던 @e 참조를 그대로 유지한다.
단, 루트 자체가 바뀌었거나(창이 통째로 교체) 매칭 신뢰도가 낮거나 변경분이 너무 많으면 접힌 전체뷰로 폴백한다. 어느 경우든 search_ui 등 캐시 질의는 언제나 저장된 완전한 상태를 본다 — 부분 적용된 모델측 트리가 아니라.
브라우저 페이지는 별도의 "두 번째 세계"가 아니라 포레스트의 또 다른 @r 루트다. launch_browser가 관리형 CDP 브라우저를 띄워 페이지 상태를 돌려주면, observe_ui·act_ui·read_text·wait_for가 데스크톱과 똑같이 동작한다. CDP 접근성 트리를 데스크톱과 동일한 아웃라인 모양으로 변환하기 때문이다. navigate_browser·evaluate_browser만 CDP 전용이고, 나머지는 계약(contract)이 완전히 공유된다.
macOS(AX·ScreenCaptureKit·AppKit 커서)와 Windows(UIA·Windows 캡처/입력)는 메커니즘이 다르다. 하지만 상태 소유권·경계·점진적 공개·트랜잭션 경계·정직한 결과는 달라선 안 된다. 각 헬퍼가 architectureVersion과 구현한 불변식 목록을 보고하고, 필수 불변식이 빠지면 기동이 실패한다. TypeScript 백엔드 인터페이스와 적합성 검사가 두 플랫폼에 같은 관측·텍스트 소유·배치·생명주기 연산을 강제한다.
cursor_overlay: true면 비-headless macOS 배경 포인터 동작이 클릭-스루 유령 커서 애니메이션을 네이티브 그라운딩 지점으로 흘려보낸다 — 전달을 지연시키지 않으면서 "에이전트가 지금 여기를 누른다"를 시각적으로 보여준다. 이 오버레이는 헬퍼 안에 산다(그라운딩이 최종 화면 좌표를 소유하므로), 루트 탐색에서는 자기 자신을 제외하고, 애니메이션은 순수 관측용이라 렌더링이 동작 전달·검증을 절대 막지 않는다.
모델에게 보이는 모든 텍스트 결과는 48KiB 또는 2,000줄로 제한된다. 초과분은 16KiB 미리보기 + 좁히기 안내 + 불변 @o 연속 참조를 준다. read_text({ ref: "@o1", offset })로 이어 읽되, @o 오프셋은 UTF-8 바이트 단위, @e 텍스트 오프셋은 유니코드 문자 단위라는 미묘한 차이가 있다.
| 디렉토리 / 파일 | 역할 |
|---|---|
extensions/computer-use.ts | Pi 확장 진입점(203줄). 11개 도구를 typebox 스키마로 정의하고 pi.registerTool로 등록, /computer-use 명령·세션 훅 연결 |
src/ | 브리지 계층 전체 — 상태·행동·뷰·아웃라인·CDP·출력·설정·권한(3-1 표 참고) |
src/platform/ | 플랫폼 중립 seam. types.ts가 공용 타입(PlatformRoot·HelperActPerformed 등), macos/·windows/가 각 백엔드 |
src/contract.ts | 도구 파라미터·동작·조건의 순수 타입 계약 + 에이전트 도구 이름 집합 |
native/macos/ | Swift 헬퍼 — bridge.swift(3,544줄 본체)·agent_cursor*.swift(유령 커서) |
native/windows/bridge-rs/ | Rust 헬퍼 — src/(UIA·창·캡처·입력) + tests/(프로토콜·refs·상태 단위 테스트) |
prebuilt/macos/{arm64,x64}/bridge | 미리 컴파일된 macOS 헬퍼 바이너리(설치 시 ~/Applications로 복사) |
scripts/ | 빌드(build-native.mjs)·설치(setup-helper.mjs) + 10여 개 자체 검증 러너 + Cubench 벤치(pi-cubench-agent.mjs) + 서명 엔타이틀먼트 |
skills/computer-use/ | 에이전트에게 "이 도구들을 어떻게 신뢰성 있게 쓰는지" 가르치는 스킬 파일 |
docs/ | architecture·usage·configuration·development·troubleshooting·windows-bridge 6종 문서 |
notes/ | v0.1.7~v0.4.3 릴리스 노트 아카이브(설계 진화 추적 가능) |
.github/workflows/ | CI·npm-audit·publish-npm 파이프라인 |
src/contract.ts와 src/platform/types.ts가 이 코드베이스의 설계 감각을 보여준다. 계약(contract)을 먼저 순수 타입으로 못 박고, macOS·Windows 백엔드가 그 계약을 각자 구현하게 한다. 그래서 "TypeScript 브리지는 OS를 몰라도 되고, 네이티브 헬퍼는 모델을 몰라도 된다". 이 분리가 두 OS를 같은 규칙으로 묶는 힘의 원천이다 — native/windows에 단위 테스트가 따로 있는 이유이기도 하다.
export default function computerUseExtension(pi: ExtensionAPI): void {
for (const tool of [findTool, observeTool, searchUiTool, expandUiTool,
inspectUiTool, actTool, readTextTool, waitForTool,
launchBrowserTool, navigateBrowserTool, evaluateBrowserTool])
pi.registerTool(tool);
pi.on("session_start", async (_e, ctx) => {
loadComputerUseConfig(ctx.cwd);
reconstructStateFromBranch(ctx); // 이전 대화 분기에서 상태 복원
if (ctx.hasUI) await ensureComputerUseSetup(ctx); // 권한 셋업 안내
});
}
const clickByRef = Type.Object({ action: Type.Literal("click"), ref: Type.String(), … });
const clickByPoint = Type.Object({ action: Type.Literal("click"), x: Type.Number(), y: Type.Number(), … });
const uiAction = Type.Union([
Type.Object({ action: Type.Literal("press"), ref: Type.String() }),
clickByRef, clickByPoint,
Type.Object({ action: Type.Literal("setText"), ref: Type.String(), text: Type.String() }),
Type.Object({ action: Type.Literal("typeText"),ref: Type.Optional(Type.String()), text: Type.String() }),
// keypress, scroll, drag, moveMouse …
]);
act_ui({
stateId,
actions: [{ action: "press", ref: "@e12" }],
expect: { text: "Archive completed", until: "present", timeoutMs: 3000 }
})
// 결과: verified(새로 확인) / preexisting(이미 그랬음) / failed
// 앱이 이벤트를 씹으면 → didnt + postcondition_failed
const DEFAULT_CONFIG = { browser_use: true, headless: false,
cursor_overlay: true, managed_browser: "chrome" };
// ~/.pi/.../pi-computer-use.json → cwd/.pi/computer-use.json → PI_COMPUTER_USE_* env
for (const source of sources) if (source.values) Object.assign(config, source.values);
Object.assign(config, env); // env가 최종 승자
| 주제 | 구체적으로 무엇을 |
|---|---|
| 접근성 API 활용 | macOS AX·Windows UIA로 화면 구조를 트리로 추출하는 저수준 패턴. 스크린리더가 쓰는 그 API를 자동화에 재활용 |
| 불변 상태 + 에포크 | 가변 전역 대신 불변 스냅샷을 저장하고, 자원별 에포크로 동시 쓰기 경합을 전송 전에 거부하는 설계 |
| 자원 단위 스케줄링 | 전체 직렬화도 완전 병렬도 아닌, "같은 물리 자원만 순서 보장" 스케줄러 만들기 |
| 점진적 공개 | 거대한 트리를 접힌 요약 + 지역 펼침으로 다뤄 토큰·정확도를 지키는 LLM 친화 인터페이스 |
| 정직한 결과 계약 | "이벤트 전달"과 "의미론적 성공"을 분리하고 worked/didnt/unknown으로 보고하는 API 설계 |
| 크로스 플랫폼 파리티 | 소스 구조가 아니라 불변식·architectureVersion·적합성 테스트로 두 OS 동작을 통일 |
| typebox 도구 스키마 | LLM 도구 파라미터를 유니온·리터럴로 엄격히 정의하고 프롬프트 스니펫을 함께 붙이기 |
| 네이티브↔JS 브리지 | Swift 소켓 서버 / Rust 라인 프로토콜을 요청 id로 다중화하고 메인 런루프와 공존시키기 |
| 항목 | 요구사항 |
|---|---|
| Pi 런타임 | @earendil-works/pi-coding-agent(Pi 코딩 에이전트). 이 패키지는 그 위의 확장이다 |
| Node.js | 20.6+ (TypeScript를 컴파일 없이 --experimental-strip-types로 직접 실행) |
| macOS | macOS 14+ · 손쉬운 사용 + 화면 기록 권한을 헬퍼 앱에 부여 필요. 헬퍼는 ~/Applications/pi-computer-use.app에 사용자별 설치 |
| Windows | 대화형 데스크톱 세션. UIA 사용, macOS식 TCC 권한 흐름·헬퍼 앱 불필요 |
| 브라우저(선택) | CDP 대상 브라우저 — chrome 또는 helium (browser_use가 켜져 있을 때) |
# npm 배포판
pi install npm:@injaneity/pi-computer-use
# 또는 GitHub에서 직접 (전역/프로젝트-로컬)
pi install git:github.com/injaneity/pi-computer-use
pi install -l git:github.com/injaneity/pi-computer-use
# 환경변수로 오버라이드 (파일 설정보다 우선)
export PI_COMPUTER_USE_HEADLESS=true # 배경 전용, 사용자 포커스 안 건드림
export PI_COMPUTER_USE_CURSOR_OVERLAY=false # 유령 커서 끄기
export PI_COMPUTER_USE_MANAGED_BROWSER=helium
# Pi 안에서 현재 설정 확인
/computer-use
macOS 헬퍼에 부여하는 손쉬운 사용·화면 기록 권한은 곧 헬퍼가 화면 전체를 보고 어떤 앱에든 클릭·타이핑할 수 있다는 뜻이다. 신뢰할 수 없는 프롬프트를 처리하는 에이전트에 이 확장을 붙일 때는 특히 조심해야 한다 — 프롬프트 인젝션이 성공하면 실제 데스크톱을 조작당할 수 있다. 민감한 작업 화면(뱅킹·비밀번호 관리자 등)이 떠 있을 때는 headless로도 화면 관측은 되므로, 필요 없다면 확장을 꺼두는 게 안전하다. 프로젝트는 v0.5.0으로 활발히 바뀌는 중이니 버전 고정을 권한다.
이 저장소를 설치하지 않아도 된다. macOS라면 Accessibility Inspector(Xcode 도구)를, Windows라면 Accessibility Insights나 inspect.exe를 켜서 아무 앱 위에 올려보자. 버튼·텍스트필드가 이름·역할·좌표를 가진 트리로 드러나는 걸 직접 확인하면, pi-computer-use의 observe_ui가 무엇을 하는지 한 번에 이해된다. "픽셀이 아니라 구조"라는 문장이 몸에 들어온다.
extensions/computer-use.ts와 src/contract.ts만 읽고, 11개 도구가 각각 어떤 파라미터를 받고 무엇을 돌려주는지 표로 정리한다. 그다음 "메모장을 열어 제목을 입력하고 저장" 시나리오를 find_roots → observe_ui → search_ui → act_ui(+expect) 호출 시퀀스로 직접 써본다. 실제 실행 없이 인터페이스만으로 워크플로를 설계하는 연습이다.
src/state.ts의 아이디어만 떼어내, 언어 무관하게 "관측 스냅샷을 불변으로 저장하고, 자원별 에포크를 붙여, 오래된 에포크의 쓰기를 거부하는" 작은 상태 저장소를 직접 만든다. 동시 요청 두 개를 흉내 내 한쪽이 stale 에러로 거부되는 걸 재현하면, 이 프로젝트의 핵심 안전장치를 코드로 소유하게 된다.
macOS AX나 Windows UIA(또는 CDP Accessibility 도메인)로 한 창의 트리를 읽어, outline.ts처럼 접힌 요약 + 지역 펼침이 되는 텍스트 아웃라인으로 직렬화한다. 노드마다 역할·이름·능력(canPress/canSetValue)을 붙이고, 검색을 정확>접두>부분 순으로 랭킹해보면 점진적 공개가 왜 토큰과 정확도를 동시에 지키는지 체감한다.
실제 앱을 대상으로, "버튼을 누른 뒤 특정 텍스트가 나타날 때까지 기다렸다가 worked/didnt/unknown을 반환"하는 최소 실행기를 만든다. 이벤트 전달과 결과 변화를 엄격히 분리하고, 부작용 없는 실패만 전경 재시도하되 애매한 포인터 동작은 재시도하지 않는 규칙을 구현하면, 컴퓨터 유즈에서 가장 어려운 "정직한 결과" 문제를 정면으로 다루게 된다.
| 주차 | 주제 | 무엇을 |
|---|---|---|
| 1주차 | 접근성 API 기초 | macOS AX / Windows UIA / 웹 ARIA·CDP Accessibility 도메인. 트리·역할(role)·능력(action)·좌표(rect) 개념 잡기. Accessibility Inspector로 실습 |
| 2주차 | LLM 도구 설계 | typebox/JSON Schema로 도구 파라미터 정의, 프롬프트 스니펫·가이드라인, 경계 있는 출력(토큰 상한·연속 읽기). MCP 도구 설계 원칙과 비교 |
| 3주차 | 동시성·상태 관리 | 불변 스냅샷, 에포크/버전 벡터, 자원 단위 직렬화, stale-write 거부. 낙관적 동시성 제어(OCC) 이론과 연결 |
| 4주차 | 컴퓨터 유즈 & 에이전트 보안 | Anthropic Computer Use, 스크린리더 자동화의 윤리·안전, 프롬프트 인젝션이 데스크톱 제어와 만날 때의 위험, headless 경계·fail-closed 설계 |
@r 참조를 주는 모델. "지금 보는 창"이라는 전역 상태를 두지 않아, 동시 호출이 서로 간섭하지 않는다.act_ui에 expect로 붙이면, 그 조건이 확인돼야만 성공(worked)으로 친다. verified/preexisting/failed로 세분해 "이벤트 전달"과 "실제 결과 변화"를 구분한다.headless: true면 앱을 앞으로 띄우거나, 사용자 포커스·전역 커서를 바꾸거나, 날 입력을 쏘지 않는다. 사용자가 다른 일을 하는 중에도 방해 없이 화면을 관측·조작(가능한 범위에서)하려는 설계.@r 루트가 된다.unknown/didnt로 보고하고, 필수 불변식이 빠진 헬퍼는 기동 자체를 거부한다.