TRENDSHIFT DAILY #13 · 2026-07-25 · TUTTI · 에이전트 공유 워크스페이스

tutti-os/tutti 딥다이브
— 여러 AI 에이전트가 맥락을 복붙하지 않고 한 곳에서 함께 일하게 하는 실시간 협업 OS

Tutti는 Claude Code · Codex 같은 코딩 에이전트들을 하나의 실시간 공유 워크스페이스에 모아, 맥락(context)·파일·앱·작업(task)이 에이전트 사이를 자동으로 흐르게 하는 로컬 우선 데스크탑 앱이다. 이름 tutti는 "전원 합주"를 뜻하는 음악 용어 — 부제 그대로 "사람과 에이전트가 한 박자로(in tune) 함께 만든다".

이 프로젝트의 급소는 이것이다: Tutti는 코딩 에이전트를 '대체'하지 않는다. 에이전트들 사이의 '심부름꾼'이 돼 버린 당신을 대체한다. Claude가 API를 짜면 Codex가 프론트를 만들어야 하는데, 그때마다 당신이 문서를 복사하고, 진행 상황을 설명하고, 이미지를 내려받아 다시 올리는 "에이전트 사이의 메신저" 역할을 없앤다.

(저장소 tutti-os/tutti · 언어 TypeScript 65% · Go 29% · 데스크탑 Electron 43 + React 19 · 백엔드 Go 데몬 tuttid · 라이선스 Apache-2.0 · 분석 시점 v0.2.2-rc.23 · shallow clone 소스 직접 분석)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — '에이전트'가 아니라 '그 사이'를 고친다
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

에이전트는 늘어나는데, 그 사이를 나르는 건 결국 나였다

요즘은 코딩 에이전트를 한 개만 쓰지 않는다. Claude Code로 백엔드를 짜고, Codex로 프론트를 만들고, 디자인 앱으로 UI 초안을 뽑는다. 문제는 여기서 시작된다. Claude가 API를 끝내면, 이제 Codex에게 그 API 문서를 붙여 넣고, 지금까지 뭘 했는지 설명하고, 왜 그렇게 설계했는지 다시 브리핑해야 한다. 프론트가 시작되기도 전에 쓸 만한 UI를 원하면 디자인·에셋도 필요하니, 또 전체를 요약해 이미지 앱에 넣고, 내려받고, 올리고, 붙여 넣는다. 에이전트가 나 대신 일하라고 있는 건데, 정작 나는 에이전트들 사이를 오가는 메신저가 돼 있다.

Tutti는 이 반복을 끊는다. 맥락·파일·실행 중인 앱·작업이 전부 연결된 실시간 공유 워크스페이스를 두어, Codex가 Claude의 결과물을 맥락 손실 없이 이어받는다. 마치 두 에이전트가 하나의 뇌를 공유하는 것처럼. 그 위에 이미지 생성·UI 디자인·문서·PPT 같은 자체 앱 생태계가 얹혀, 사람도 에이전트도 같은 앱을 호출할 수 있다.

핵심 비유

"통역만 하다 지친 프로젝트 매니저" vs "모두가 같은 화이트보드를 보는 회의실"

에이전트가 여럿인 지금의 작업은, 각자 다른 방에 있는 전문가들에게 내가 뛰어다니며 말을 전하는 것과 같다. "저쪽 방에서 이렇게 정했어요", "이 파일 좀 전해 주세요" — 일 자체보다 정보를 나르는 일이 사람을 지치게 한다.

Tutti는 전문가들을 하나의 회의실(공유 워크스페이스)에 모으고, 모두가 같은 화이트보드를 실시간으로 본다. 누가 뭘 바꿨는지, 무엇이 돌아가는지, 지금 프로젝트 상태가 어떤지 서로 보인다. 심부름꾼이 사라진다.

중요한 설계 선택이 하나 있다: Tutti는 모델을 되팔지 않는다. 당신이 이미 가진 Claude·Codex 구독 위에서 모든 앱과 에이전트가 추가 비용 없이 돌아간다. 에이전트는 여전히 로컬에서 당신의 구독·설정으로 실행되고, Tutti는 그 위를 감싸는 워크스페이스일 뿐이다. "또 하나의 AI 구독"이 아니라 "이미 가진 구독들을 엮는 판(板)"인 셈이다.

용어 풀이
에이전트 오케스트레이션 (agent orchestration)
여러 AI 에이전트를 지휘자처럼 조율해, 각자 잘하는 일을 맡기고 결과를 이어 붙이는 것. 핵심 난제는 "맥락 전달" — A가 한 일을 B가 알게 만드는 것이다. Tutti는 이걸 사람의 복붙이 아니라 공유 상태(shared state)로 푼다. "누가 무엇을 언제 만들었나"가 워크스페이스에 남아, 다음 에이전트가 그대로 참조한다.

2왜 주목받는가 — '에이전트'가 아니라 '그 사이'를 고친다

TrendShift Daily 상위권(#13)에 오른 이유 4가지

코딩 에이전트 자체는 이미 넘친다(Claude Code, Codex, Cursor, OpenCode…). 그런데 Tutti가 트렌딩에 오른 건 겨냥하는 지점이 다르기 때문이다. 남들이 "더 똑똑한 에이전트"를 만들 때, Tutti는 에이전트와 에이전트 사이의 빈틈을 노렸다. 네 가지 결이 겹친다.

① 실시간 공유 워크스페이스 — 요약이 아니라 '상태'를 넘긴다

기존 핸드오프는 "이전 대화를 요약해서 다음 에이전트에게 전달"하는 방식이라, 요약하는 순간 디테일이 샌다. Tutti는 맥락·파일·실행 중인 작업·앱을 같은 실시간 상태로 공유한다. 당신의 Codex는 Claude가 무엇을 바꿨는지, 무엇이 돌고 있는지, 프로젝트의 현재 상태를 그대로 본다. 여기서 Big @(과거 대화·파일·앱 호출·작업을 @로 불러오기)와 + 참조(로컬 파일·앱 산출물 끌어오기)가 나온다.

② 사람도 에이전트도 쓰는 앱 생태계

완결된 워크플로는 에이전트 하나로 안 끝난다. 페이지 하나 만들려 해도 프로토타입 → 코드 → 비주얼이 필요하고, 그때마다 다른 툴을 열고 내려받고 올린다. Tutti는 워크스페이스 안에 앱 센터를 두어, 이미지 생성·프로토타입 디자인·문서·PPT 앱을 사람이 직접 쓰거나 에이전트가 호출하게 한다. Codex가 프로토타입 디자인 앱을 @하면, 사실상 Codex가 디자인 능력을 얻고, 그 산출물이 곧바로 개발로 이어진다. 앱 산출물은 전부 같은 워크스페이스에 남아 +로 다음 단계에 참조된다.

③ provider 중립 — 서로 다른 에이전트를 한 판에

Tutti는 특정 벤더에 묶이지 않는다. 내부적으로 에이전트를 딱 3가지 런타임 프로토콜(Claude SDK 사이드카 · Codex 앱서버 · 표준 ACP)로 추상화해, Claude Code·Codex·Cursor·OpenCode 같은 이질적인 에이전트를 같은 워크스페이스에서 협업시킨다. 서로 충돌을 피하거나 해소하고, 병렬로 갈지 순차로 갈지도 스스로 판단한다. "특정 툴에 락인되고 싶지 않은" 개발자에게 정확히 꽂히는 지점이다.

④ 로컬 우선 + 기존 구독 재사용

에이전트는 로컬에서 당신의 구독·설정으로 돌고, 작업 상태도 로컬에 남는다(오픈소스판 기준). 모델을 묶어 되파는 대신 이미 가진 Claude·Codex 구독 위에서 추가 비용 없이 앱과 에이전트가 동작한다. 나중에 팀·다기기 협업이 필요하면 클라우드판 Tutti · VM으로 확장하는 2단 구성이다.

한 걸음 더 — 'CRDT 없는' 실시간의 정직함

실시간 협업이라 하면 흔히 Google Docs식 CRDT(문서 병합 알고리즘)를 떠올린다. 그런데 Tutti 소스에는 yjs·automerge 같은 CRDT가 없다. 대신 "무엇이 바뀌었다"는 작은 신호(signal)만 이벤트로 쏘고, 클라이언트가 HTTP로 권위 있는 최신 상태를 다시 읽는(reconcile) 방식이다. 화려한 알고리즘 대신, "서버가 진실의 단일 출처, 이벤트는 알림일 뿐"이라는 단순하고 견고한 선택을 했다. 이 절제가 오히려 신뢰도를 높인다(§4에서 상술).

구분전형적 '에이전트 툴'Tutti
겨냥하는 문제에이전트 하나를 더 똑똑하게에이전트 '사이'의 맥락 전달
핸드오프 방식대화 요약을 복붙실시간 공유 상태 + @/+ 참조
다중 에이전트대개 단일 벤더Claude·Codex·Cursor·OpenCode 혼합
앱/도구툴마다 따로 구독·전환워크스페이스 공용 앱 센터(사람+에이전트)
과금모델을 묶어 재판매내 기존 구독 재사용, 추가 비용 0
데이터 위치클라우드로컬 우선(오픈소스판)

3기술 스택 전체 지도

TypeScript 프론트 + Go 백엔드 — pnpm/Go 이중 모노레포

Tutti는 거대한 모노레포다. 하지만 아무렇게나 큰 게 아니라, TypeScript 워크스페이스와 Go 워크스페이스가 한 저장소 안에 겹쳐 있는 이중 구조다. pnpm이 apps/*·packages/*/*(두 단계!)를 묶고, 동시에 go.work가 16개 Go 모듈을 묶는다. UI는 TypeScript/React가, 백엔드·에이전트 런타임 코어는 Go가 맡는 역할 분담이 명확하다.

tutti-os/tutti (모노레포, TypeScript 65% · Go 29%) ├─ apps/ 4개 — 사용자가 실행하는 껍데기(shell) │ ├─ desktop Electron 43 + React 19 + Vite 6 ← 메인 데스크탑 앱 │ ├─ mobile React Native + NativeWind ← iOS/Android │ ├─ cli Go — 'tutti' 명령(에이전트가 호출) │ └─ ui-storyboard Vite 스토리북(디자인 시스템) ├─ packages/ 45개 — domain/name 2단계 (agent·workspace·workbench·events·ui…) ├─ services/tuttid Go 데몬 — "로컬 제품의 유일한 비즈니스 계층" └─ tools/·docs/·config/ 빌드 스크립트 · 30개 아키텍처 문서 · 9개 ADR 빌드 도구: pnpm 10.11.0 · Node ≥24 · Go 1.24.3 린트/포맷: oxlint · oxfmt (Rust 기반 oxc 스택 — ESLint/Prettier 아님) 타입체크: tsgo (@typescript/native-preview — 네이티브 TS 컴파일러) 릴리즈: changesets (25개 @tutti-os/* 패키지 고정 그룹으로 함께 버전업)

스택에서 눈에 띄는 선택 두 가지. 첫째, 린트·포맷·타입체크가 전부 차세대(네이티브) 도구다 — ESLint/Prettier 대신 Rust로 짠 oxlint·oxfmt, TypeScript 컴파일러도 Go로 재작성된 tsgo. 거대 모노레포의 CI 속도를 위한 포석으로 읽힌다. 둘째, SQLite 드라이버가 순수 Go 구현 modernc.org/sqlite라, C 컴파일러 없이 크로스 빌드된다(paxm와 같은 선택 — 데스크탑 배포에 유리).

핵심 의존성 (분석 시점 v0.2.2-rc.23)

영역선택버전 / 메모
데스크탑 셸Electron^43.2.0 (+ electron-vite ^3.1 · builder ^26.8 · updater ^6.8)
UI 프레임워크React^19.1.0 · Vite ^6.3.5
모바일React Nativebare(Expo 아님) + NativeWind + Reanimated/Hermes
백엔드 언어Go1.24.3 (toolchain 1.24.5) · go.work 16개 모듈
DBmodernc.org/sqlite1.45.0 · 순수 Go(cgo 없음) · ~/.tutti/tuttid.db
Claude 연동@anthropic-ai/claude-agent-sdk0.3.201 (사이드카가 감쌈) + zod 4
패키지 매니저pnpm10.11.0 (workspace 2단계 glob)
런타임Node≥24 (.node-version=24)
린트·포맷oxlint · oxfmt1.70.0 · 0.55.0 (JSON/MD/YAML만 Prettier)
계약 코드젠OpenAPI · JSON-Schema · Codex export@hey-api/openapi-ts · 이벤트 스키마 · codexproto
용어 풀이
go.work (Go 워크스페이스)
여러 Go 모듈을 한 번에 개발할 때 쓰는 상위 설정 파일. 각 모듈이 서로를 replace 지시어 없이 로컬 소스로 바로 참조하게 해 준다. Tutti는 services/tuttid, packages/agent/host, packages/device-link16개 Go 모듈을 하나의 워크스페이스로 묶어, TS 모노레포와 Go 모노레포가 한 저장소에서 공존한다.

4아키텍처 심화 분석

데스크탑 껍데기 · 로컬 데몬 · 에이전트 코어 · 3가지 런타임 프로토콜

Tutti의 구조를 한 장으로 압축하면 이렇다. Electron 데스크탑이 화면을 그리고, 그 안에서 Go 데몬 tuttid를 자식 프로세스로 띄운다. 화면(React)은 데몬과 로컬 HTTP/WebSocket으로만 대화하고, 데몬이 모든 비즈니스 로직·에이전트 세션·공유 상태를 쥔다. 에이전트들은 데몬 아래 런타임 어댑터를 통해 붙는다.

┌──────────────────────────────────────────────────────────┐ │ apps/desktop (Electron 43) │ │ renderer(React 19) ── preload(타입 브리지) ── main │ │ │ 화면은 데몬에 HTTP/WS로만 접근 │ │ ▼ (127.0.0.1:임의포트 · Bearer 토큰) │ ├──────────────────────────────────────────────────────────┤ │ services/tuttid (Go 데몬 — 유일한 비즈니스 계층) │ │ api → service(30개) → biz(20개) → data(SQLite) │ │ · HTTP /v1/... 권위 있는 상태(read-model) │ │ · WS /v1/events/ws "무엇이 바뀌었다" 신호 │ │ · WS terminal 터미널 스트림(별도 채널) │ │ │ 세션 수명은 tuttid가 아니라 ↓ 에 위임 │ │ ▼ │ │ packages/agent/host (Go — 세션/턴/골 수명의 단일 코어) │ │ │ │ │ ┌────┴─────────────┬──────────────────┐ 런타임 3종 │ │ ▼ ▼ ▼ │ │ Claude SDK 사이드카 Codex 앱서버 표준 ACP │ │ (claude-agent-sdk) (JSON-RPC/codexproto) (Cursor·OpenCode│ │ = Claude Code = Codex·Tutti Agent ·OpenClaw) │ └──────────────────────────────────────────────────────────┘ device-link(ICE+QUIC P2P) → 클라우드판 Tutti·VM 다기기 협업

축 ① 단일 비즈니스 코어 + 어댑터 경계 (CI가 강제한다)

가장 인상적인 설계 규율이다. 에이전트의 세션·턴·골(goal) 수명 로직 전부packages/agent/host 한 곳에 산다. 데몬 tuttid는 그 코어를 감싸는 어댑터일 뿐 — HTTP를 받아 ApplicationHost()로 넘긴다. 놀라운 건 이 경계를 CI가 빌드 실패로 강제한다는 점이다. 어댑터에 *Coordinator·*Worker 같은 오케스트레이션이 새로 새면 pnpm check:agent-host-boundary가 빌드를 깬다. 새 수명 로직은 반드시 host/conformance에 시나리오부터 추가해야 한다.

왜 이렇게까지?

같은 에이전트 코어를 오픈소스 데스크탑판과 비공개 클라우드판(Tutti·VM, 코드명 TSH)이 함께 쓰기 때문이다. 코어를 순수하게 지켜야 두 제품이 갈라지지 않는다. "비즈니스 로직은 한 곳, 나머지는 다 어댑터"라는 헥사고날 아키텍처를 말로만이 아니라 CI 게이트로 못 박은 드문 사례다.

축 ② 2-plane 트랜스포트 — 상태와 신호를 섞지 않는다

데몬은 화면·CLI와 세 갈래로 대화하는데, 각각 역할이 엄격히 분리된다. 이 분리가 실시간의 핵심이다.

세 채널 모두 데스크탑이 발급한 실행별 Bearer 토큰으로 인증한다. 그 토큰과 접속 주소는 데몬이 부팅 때 <state-dir>/run/tuttid.listener.json0600으로 원자적으로 써 두고(임시파일+rename), Electron과 CLI가 그 파일을 읽어 접속한다. 리스너는 127.0.0.1:0(OS가 임의 포트 배정)에 바인딩 — 외부에 열리지 않는 루프백 전용이다.

축 ③ signal-and-reconcile — CRDT 없는 실시간

실시간 공유의 실제 구현이 여기다. Tutti는 CRDT를 쓰지 않는다. 대신 데몬이 agent.activity.updated 같은 "더티 신호"를 이벤트 WS로 쏘면, 클라이언트가 HTTP로 권위 있는 최신 상태를 다시 읽어(reconcile) 맞춘다. 이때 버전 커서(메시지의 afterVersion, 앱별 stateRevision 등)로 "내가 가진 것보다 새 것"일 때만 적용한다. 이벤트는 알림, HTTP 스냅샷이 진실이다.

축 ④ 3가지 런타임 프로토콜로 이질적 에이전트를 통일

provider 중립의 실체다. 어떤 에이전트든 packages/agent/daemon/providerregistryRuntimeDescriptor.Kind 3종 중 하나로 붙는다. 에이전트를 추가하려면 분기문이 아니라 ProviderDescriptor 한 개를 선언하면 된다(전략 패턴).

런타임 종류대상 에이전트어떻게 붙나
claude_sdkClaude CodeNode 사이드카가 공식 claude-agent-sdk를 감싸고, 데몬↔사이드카는 stdio 위 개행 구분 JSON(모든 메시지 version:2)
codex_app_serverCodex · Tutti AgentCodex의 app-server(JSON-RPC). Rust export에서 코드젠codexproto 바인딩(전체 생성, 쓰는 부분만 배선)
standard_acpCursor · OpenCode · OpenClawAgent Client Protocol(ACP) + 벤더별 어댑터 전략(cursor-agent acp 등)

캐노니컬 provider 정체성은 8개(codex·claude-code·cursor·tutti-agent·opencode·nexight·hermes·openclaw)인데, 실제 런타임이 배선된 건 7개다. README가 지원한다고 적은 hermes는 정체성만 있고 런타임 서술자가 없다 — 실제 배선은 비공개 Tutti·VM 쪽에 있는 것으로 보인다. 이런 "문서와 코드의 미세한 어긋남"까지 소스로 확인해야 정확하다.

용어 풀이
사이드카 (sidecar)
본체 옆에 붙어 특정 역할만 대신 처리하는 보조 프로세스. Tutti는 Go 데몬이 직접 Claude SDK를 부르지 않고, @tutti-os/claude-sdk-sidecar라는 Node 프로세스를 옆에 띄워 SDK 호출을 맡긴다. 이유는 환경 호환 — 사이드카가 ~/.claude/settings.jsonenv(예: ANTHROPIC_BASE_URL)를 native claude CLI와 똑같이 읽어 병합하기 때문. 그래서 바이너리가 아니라 SDK를 감싼다.

진짜 재미있는 부품 — @-멘션 지연 해석 & 앱 = CLI 매니페스트

@-멘션 / +-참조의 지연(lazy) 해석. 멘션은 파일 목록·경로·자격증명을 전혀 담지 않는 짧은 URI다 — 예: mention://workspace-reference/<id>(+ source·workspaceId 쿼리). 에이전트는 이걸 쓰는 시점에 주입된 reference 스킬과 tutti CLI로 펼친다. 그래서 멘션을 적은 뒤 파일이 생기거나 지워져도, 해석 결과는 항상 최신 상태를 반영한다.

워크스페이스 앱 = CLI 매니페스트. 이미지 생성·PPT·디자인 같은 앱은 tutti.app.cli.v1 매니페스트로 에이전트가 호출 가능한 명령을 선언한다({schemaVersion, scope, commands:[{path, summary, inputSchema, visibility}]}). 데몬이 이를 광고하고, 얇은 tutti CLI가 데몬이 준 inputSchema로 플래그를 파싱해 구조화된 결과를 돌려준다. "에이전트가 프로토타입 디자인 앱을 호출한다"의 정체가 바로 이 CLI 계약이다.

잘 만든 지점
작업 오케스트레이션을 두 도메인으로 쪼갠다

"골 → 작업" 분해(Tutti Mode)는 목표를 검토 가능한 Markdown 계획으로 바꾼 뒤 Issue로 materialize한다. 여기서 Workspace Issue(Issue·Task·Run·의존성·예산)와 Agent Host(Session·Turn)를 분리하고, IssueExecutionCoordinator가 Issue별 뮤텍스 아래 2단계로 잇는다(내구성 Run 청구 → 락 해제 → 워크트리 준비 + 세션 생성). 취소도 issue-run:<runID> 턴을 정확히 짚어 끊는다. 실행 상태와 대화 상태를 섞지 않은 깔끔한 분리다.

5디렉토리 구조 해부

4개 앱 · 45개 패키지(2단계) · 유일한 비즈니스 코어 tuttid

모노레포지만 규칙이 뚜렷하다. apps/사용자가 실행하는 껍데기, packages/재사용 로직(도메인/이름 2단계), services/tuttid비즈니스 두뇌. 패키지 대부분은 TS지만, 여러 도메인이 TS 패키지와 Go 모듈을 나란히 둔다(예: events/stream-core + events/stream-go).

tutti/ ├─ apps/ │ ├─ desktop/ Electron 메인 앱 (@tutti-os/desktop) │ │ └─ src/{main, preload, renderer, shared} │ │ main/daemon/ ← tuttid를 띄우고 감독하는 곳 │ ├─ mobile/ React Native (iOS/Android) │ ├─ cli/ Go — 'tutti' 바이너리(에이전트용 얇은 클라이언트) │ └─ ui-storyboard/ 디자인 시스템 스토리북 ├─ packages/ (45개, domain/name 2단계) │ ├─ agent/ host*·daemon·runtimeprep·store-sqlite·claude-sdk-sidecar·gui … │ │ (*host = 세션/턴 수명의 단일 코어, Go) │ ├─ workspace/ app-center·file-reference(@/+ 피커)·issues·files·terminal … │ ├─ workbench/ host(창/패널 커널)·surface·launchpad·snapshot │ ├─ events/ protocol(이벤트 스키마)·stream-core(TS)·stream-go(Go) │ ├─ appcli/core 워크스페이스-앱 CLI 호출 코어(manifest·invoke·scope) │ ├─ device-link/ ICE+QUIC P2P(다기기, Tutti·VM용, Go) │ ├─ clients/tuttid-ts 데몬 클라이언트(코드젠+수기) │ ├─ auth/·commerce/·browser/·ui/·configs/ … ├─ services/tuttid/ ★ Go 데몬 = 유일한 비즈니스 계층 │ ├─ main.go 부팅·PID 리스·부모 감시·서버 기동 │ ├─ api/ HTTP 핸들러 + 코드젠 OpenAPI + 이벤트 계약 │ ├─ service/ 30개 애플리케이션 서비스(agent·claudecode·workspace…) │ ├─ biz/ 20개 내구 도메인 코어(workspace·agentactivity…) │ ├─ data/ SQLite 영속화 │ └─ wiring*.go 의존성 조립(composition root) ├─ docs/ 30개 아키텍처 + 22개 컨벤션 + 9개 ADR └─ go.work · pnpm-workspace.yaml · Makefile · skills-lock.json
읽는 순서 팁

이 레포를 처음 연다면 docs/architecture/business-event-stream.md(실시간 모델) → services/tuttid/main.go(부팅·트랜스포트) → packages/agent/daemon/providerregistry/(에이전트 3종 배선) → docs/architecture/agent-reference-*.md(@-멘션 해석) 순서를 추천한다. "실시간이 어떻게 도는가 → 데몬이 어떻게 뜨는가 → 에이전트가 어떻게 붙는가 → 참조가 어떻게 풀리는가"로 내려가면 큰 그림이 자연스럽게 잡힌다. AGENTS.md·CONTEXT.md는 이 레포를 에이전트가 개발하도록 쓴 지침이라, 설계 의도가 사람 말로 정리돼 있어 먼저 읽어도 좋다.

6학습 포인트

AI를 걷어내도 남는 '대형 앱 아키텍처'의 교재

① 헥사고날 아키텍처를 'CI로 강제'하는 법

말로만 "비즈니스 로직은 코어에, 나머지는 어댑터에"가 아니라, check:agent-host-boundary경계 위반 시 빌드를 깨는 실전 예다. 대형 코드베이스가 시간이 지나며 무너지는 이유는 대개 "편의상 어댑터에 로직 한 줄"이 쌓여서다. Tutti는 그걸 기계가 막는다. 새 수명 로직은 conformance 시나리오부터 — 테스트가 설계를 이끄는 규율을 배울 수 있다.

② CRDT 없이 실시간 협업을 만드는 법

"실시간 = CRDT"라는 통념을 깬다. signal-and-reconcile(신호는 이벤트, 진실은 HTTP read-model, 버전 커서로 최신만 적용)은 구현·디버깅이 훨씬 쉽고, 백프레셔(1013)로 느린 소비자를 격리한다. "언제 CRDT가 과하고, 언제 신호+재조회로 충분한가"는 실무에서 계속 부딪히는 판단이다. 그 답의 한 축을 코드로 보여 준다.

③ 전략 패턴으로 이질적 외부 시스템을 통일

Claude(SDK)·Codex(app-server)·Cursor(ACP)는 프로토콜이 전부 다르다. Tutti는 이들을 3개 런타임 종류 + 선언적 서술자(descriptor)로 감싸, 공용 로직에 if provider == ... 분기가 없게 만든다. "제각각인 외부 API들을 하나의 인터페이스로 흡수하는" 어댑터/전략 패턴의 대규모 실전 사례다.

④ 계약 우선(codegen) — 손으로 안 맞추고 생성한다

OpenAPI → 클라이언트/서버, 이벤트 JSON-Schema → TS+Go 타입, Codex Rust exportcodexproto. 계약(스키마)이 단일 진실이고, TS와 Go 양쪽 타입이 거기서 생성돼 드리프트가 원천 차단된다. check:*-generated CI가 "생성물과 소스가 어긋났는지"까지 감시한다. 폴리글랏(다언어) 시스템에서 타입 안정성을 지키는 정석.

⑤ 견고한 프로세스 감독(supervision)

데몬은 PID 파일 리스로 중복 실행을 막고, TUTTI_DESKTOP_PARENT_PID500ms마다 getppid()로 감시해 Electron이 죽으면 스스로 종료한다(유령 데몬 방지). 리스너 정보는 임시파일+rename으로 원자적으로 쓰고, 부팅 시 낡은 락도 복구한다. "GUI가 백그라운드 데몬을 안전하게 거느리는" 데스크탑 앱의 정석 패턴이 main.go에 응축돼 있다.

⑥ Electron 3계층 경계를 엄격히 지키는 법

renderer(React)는 데몬에 HTTP/WS로만 접근하고, preload는 타입이 정해진 host 능력만 노출한다. 이 경계도 check:electron-runtime-boundaries로 검사한다. Electron 보안의 핵심(렌더러에 Node 권한을 주지 않기)을 규칙이 아니라 강제로 지키는 구성이다.

한 줄 요약
"AI 협업 툴"을 걷어내도 값진 레포

주제(에이전트 오케스트레이션)를 지워도 남는 게 많다. 헥사고날 경계의 CI 강제 · 2-plane 트랜스포트 · signal-and-reconcile 실시간 · 계약 우선 코드젠 · 프로세스 감독 · 대형 TS/Go 이중 모노레포 운영. "Electron + Go 데몬으로 진지한 데스크탑 제품을 어떻게 구조화하는가"의 종합 교재로 읽을 수 있다.

7하드웨어 / 시스템 요구사항

GPU 불필요 — 에이전트는 '당신의 구독'으로 로컬에서 돈다

Tutti 자체는 모델을 돌리지 않는다. 에이전트가 로컬에서 당신의 Claude·Codex 구독으로 실행되므로, GPU도, 별도 추론 서버도 필요 없다. 필요한 건 에이전트 구독(없으면 Early Access 동안 무료인 Tutti Agent로 시작 가능)과, 데스크탑 앱을 돌릴 평범한 PC다. 다운로드판(Tutti · Local)은 "coming soon" 단계이고, 지금은 소스에서 빌드하는 경로가 열려 있다.

소스에서 빌드(개발자)

필요버전 / 비고
Node.js24 이상 (.node-version이 baseline 고정)
pnpm10.11.0 (corepack 권장)
Go1.24 이상 (toolchain 1.24.5)
C 컴파일러불필요 — 순수 Go SQLite(cgo 없음)
에이전트Claude Code · Codex 구독(또는 무료 Tutti Agent)
# 소스 빌드 & GUI 개발 실행
pnpm install
pnpm setup:dev        # golangci-lint 등 개발 도구 설치
make dev-gui          # = bash ./tools/scripts/dev-gui.sh

# 데이터 위치: ~/.tutti/tuttid.db  (개발: ~/.tutti-dev/)
# 리스너 정보: <state-dir>/run/tuttid.listener.json (0600)
함정 주의
tuttid는 사용자가 직접 실행하는 데몬이 아니다

services/tuttidElectron 앱의 자식으로 떠서 부모(데스크탑)가 죽으면 함께 종료되도록 설계됐다(getppid() 감시). 그래서 데몬만 따로 실행하면 --help만 보이거나 곧 종료된다. 데몬을 단독 기동해 디버깅하려면 부모 PID 감시 로직(TUTTI_DESKTOP_PARENT_PID)과 리스너 파일 발행 경로를 먼저 이해해야 한다.

용어 풀이
Tutti · Local vs Tutti · VM
Local(오픈소스) = 에이전트가 로컬에서 돌고 작업 상태도 로컬에 남는 1인·다(多)에이전트용. VM(coming soon) = 다층 가상화로 로컬 에이전트를 실시간 공유 클라우드 워크스페이스(Room)로 확장 — 다기기·다인원 협업용. 에이전트는 VM에서도 여전히 로컬 실행이고, "작업 상태"만 클라우드 Room에 실시간으로 산다. 코드 곳곳의 TSH가 이 VM 제품의 코드명이다.

8직접 해볼 수 있는 실습 과제

난이도별 5개 — 빌드 체감부터 @-멘션 해석 추적까지
과제 1 · 난이도 ★☆☆☆☆

빌드해서 띄우고, 3채널 트랜스포트를 눈으로 보기

Node 24·pnpm 10.11·Go 1.24를 준비하고 pnpm install → pnpm setup:dev → make dev-gui로 앱을 띄운다. 그다음 상태 디렉토리의 run/tuttid.listener.json을 열어 바인딩 주소(127.0.0.1:임의포트)와 Bearer 토큰이 어떻게 발행되는지 확인하라. HTTP /v1/..., 이벤트 WS /v1/events/ws, 터미널 WS가 왜 따로인지 services/tuttid/api/routes.go에서 대조한다.

과제 2 · 난이도 ★★☆☆☆

'에이전트 호스트 경계'를 CI로 확인하기

AGENTS.md의 Agent Host Boundary 절을 읽고, services/tuttid/service/agent가 어떻게 로직을 직접 갖지 않고 ApplicationHost()위임하는지 추적하라. 그다음 pnpm check:agent-host-boundary를 돌려 보고, 어댑터에 일부러 fooCoordinator 같은 이름을 넣으면 빌드가 깨지는지 실험한다. "규칙을 기계가 지키게 하는" 감각을 체험하는 게 목표.

과제 3 · 난이도 ★★★☆☆

이벤트 하나가 TS+Go 타입으로 생성되는 길 따라가기

docs/architecture/business-event-stream.md를 읽고, packages/events/protocol/definitions/에서 workspace.issue.updated 같은 이벤트 정의 JSON 하나를 고른다. 그 스키마가 코드젠으로 packages/events/protocol(TS 타입/검증기)과 services/tuttid/api/events/generated(Go 타입)로 양쪽에 생성되는 걸 확인하라. "스키마가 단일 진실"의 실감.

과제 4 · 난이도 ★★★★☆

런타임 3종 서술자 읽고, 가상의 에이전트 추가 설계

packages/agent/daemon/providerregistry/types.goclaude_code.go·codex.go를 읽어 RuntimeDescriptor.Kind 3종(claude_sdk·codex_app_server·standard_acp)의 차이를 정리하라. 그다음 "새 ACP 에이전트를 붙인다면 ProviderDescriptor에 무엇을 채워야 하나"(command·auth·skill root·composer 프로파일)를 종이 설계로 써 본다. 분기문 없이 선언만으로 확장되는 구조를 이해하는 게 핵심.

과제 5 · 난이도 ★★★★★

@-멘션이 '쓰는 시점에' 풀리는 경로 끝까지 추적

docs/architecture/agent-reference-*.md를 읽고, mention://workspace-reference/<id>가 파일·경로·자격증명을 담지 않은 채 주입된 reference 스킬 + tutti CLI사용 시점에 해석되는 흐름을 정리하라. 함께: 워크스페이스 앱의 tutti.app.cli.v1 매니페스트(packages/appcli/core/manifest.go)가 어떻게 "에이전트가 앱을 호출"로 이어지는지 대조한다. 지연 해석이 왜 "적은 뒤 파일이 바뀌어도 최신"인지 설명할 수 있으면 성공.

9관련 기술 심화 학습 로드맵

4주 커리큘럼 — 이 저장소를 발판으로
주차주제할 것
1주차실시간 & 트랜스포트business-event-stream.md·desktop-transport.md 정독 → signal-and-reconcile·버전 커서·백프레셔(1013) 이해 → HTTP read-model과 이벤트 WS의 역할 분리 정리
2주차에이전트 런타임·프로토콜providerregistry의 3종 런타임 읽기 → Claude SDK 사이드카(stdio JSON)·Codex app-server(codexproto)·ACP 비교 → ADR 0001(코드젠 접근) 읽기
3주차도메인 설계·코드젠tuttid의 api→service→biz→data 계층 추적 → 이벤트/OpenAPI 코드젠 파이프라인 → Issue(실행)와 Session(대화) 분리, Tutti Mode 흐름
4주차데스크탑·감독·경계main.go의 PID 리스·부모 감시·리스너 발행 → Electron 3계층 경계와 check:*-boundary CI → 헥사고날 경계를 강제하는 방법 재현
학습 순서 팁

이 저장소는 "AI 협업"이라는 주제를 잠시 접어 두고 봐도 값지다. 대형 TS/Go 이중 모노레포 운영 · 헥사고날 경계의 CI 강제 · 계약 우선 코드젠 · 실시간을 신호+재조회로 푸는 절제 · GUI가 데몬을 거느리는 프로세스 감독 — 진지한 데스크탑 제품을 구조화하는 정석이 한 레포에 다 있다. Electron/Go로 견고한 앱을 짜고 싶은 사람에게 특히 참고가 된다.

10핵심 키워드 사전

이 문서에 나온 용어 빠른 정리
용어
Tutti"전원 합주"를 뜻하는 음악 용어. 여러 AI 에이전트가 한 워크스페이스에서 함께 일하게 하는 로컬 우선 협업 앱
에이전트 오케스트레이션여러 에이전트를 조율해 각자 잘하는 일을 맡기고 결과를 잇는 것. 핵심 난제는 맥락 전달
공유 워크스페이스맥락·파일·앱·작업이 실시간으로 연결된 공간. 요약을 넘기는 대신 '상태'를 공유
tuttidGo로 짠 로컬 데몬. "로컬 제품의 유일한 비즈니스 계층". Electron의 자식으로 실행
agent host boundary세션/턴 수명 로직은 packages/agent/host에만, tuttid는 어댑터. CI가 위반을 빌드 실패로 강제
signal-and-reconcile이벤트로 "무엇이 바뀌었다" 신호만 쏘고, 클라이언트가 HTTP로 최신 상태를 재조회해 맞추는 실시간 방식(CRDT 아님)
2-plane 트랜스포트권위 상태=HTTP, 라이브 신호=이벤트 WS, 터미널=별도 WS로 채널을 엄격히 분리
버전 커서afterVersion·stateRevision 등으로 "내가 가진 것보다 새 것"일 때만 상태를 적용
런타임 종류(3종)claude_sdk(Claude Code)·codex_app_server(Codex)·standard_acp(Cursor·OpenCode 등)
사이드카본체 옆에 붙는 보조 프로세스. Claude SDK 호출을 Node 사이드카가 대신 처리(환경 호환 목적)
ACPAgent Client Protocol — 여러 코딩 에이전트를 표준 방식으로 붙이는 프로토콜
app-server / codexprotoCodex가 노출하는 JSON-RPC 서버. Rust export에서 코드젠한 Go 바인딩이 codexproto
@-멘션 / +-참조파일·경로를 담지 않는 짧은 URI를 '쓰는 시점에' 스킬+CLI로 펼쳐 최신 상태를 해석
tutti.app.cli.v1워크스페이스 앱이 "에이전트가 호출 가능한 명령"을 선언하는 CLI 매니페스트 스키마
Tutti Mode목표(goal)를 검토 가능한 Markdown 계획으로 바꾼 뒤 Issue로 materialize하는 기능
Issue / Session 분리실행 상태(Workspace Issue: Issue·Task·Run)와 대화 상태(Agent Host: Session·Turn)를 다른 도메인으로 분리
계약 우선 코드젠OpenAPI·JSON-Schema·Codex export를 단일 진실로 두고 TS+Go 타입을 생성해 드리프트 차단
go.work여러 Go 모듈을 한 워크스페이스로 묶는 상위 설정. Tutti는 16개 Go 모듈을 묶음
oxc 스택Rust로 짠 차세대 JS 도구(oxlint·oxfmt)와 네이티브 TS 컴파일러 tsgo
device-linkICE 후보 협상 + QUIC + 상호 인증서 고정으로 다기기 P2P를 잇는 패키지(Tutti·VM용)
Tutti · VM (TSH)로컬 에이전트를 실시간 공유 클라우드 Room으로 확장하는 비공개 클라우드판. 코드명 TSH

11참고 링크

원문으로 더 파고들기