TrendShift 딥다이브 · 2026-07-17 · Daily #13

deer-flow/llm-space 딥다이브
— 에이전트를 "한 걸음씩" 뜯어보는 실험실

LLM Space에이전트 아이디어를 만들고, 하니스가 밟는 매 스텝을 들여다보고, 실패한 실행을 다시 돌려 보고, 두 실행의 성능을 채점하는 로컬 퍼스트 데스크톱 앱이다. 여기서 가장 먼저 각인해야 할 사실 하나 — 이건 Electron도 Tauri도 아니라 Electrobun이라는 Bun 기반 데스크톱 껍데기로 만들어졌다. 만든 곳은 바이트댄스의 딥리서치 프레임워크 DeerFlow 팀으로, 자기네 에이전트를 디버깅하려고 만든 도구를 오픈소스로 공개한 것(=도그푸딩)이다. 2023년 3월 시작해 지금은 네 번째 대개편(v4). 진짜 핵심은 "에이전트 루프를 자동으로 끝까지 돌리지 않고, 도구 호출마다 멈춰서 사람이 들여다보게 만든다"는 발상이다. 이 문서는 소스를 직접 클론해 "여기서 무엇을 배울 수 있는가"를 파고든다. (저장소: deer-flow/llm-space · Electrobun 데스크톱 앱 · 순수 TypeScript/TSX · MIT © 2023 LLM Space · 앱 버전 4.1.1 · macOS 전용 · 최신 커밋 2026-07-17 cae5ace · TrendShift Daily #13 · #AI agent)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 · 모노레포 구조 해부
  6. 학습 포인트
  7. 시스템 · 실행 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"챗봇 앱"이 아니라 에이전트를 만들고·해부하고·채점하는 워크벤치(작업대)

deer-flow/llm-space(제품명 LLM Space)는 "에이전트를 프로토타이핑하는 로컬 데스크톱 IDE"다. 보통의 챗 앱은 당신이 메시지를 치면 AI가 알아서 도구를 다 돌리고 최종 답만 보여준다. LLM Space는 정반대다 — 모델이 "이 도구를 이런 인자로 부르겠다"고 말하는 순간 루프를 멈추고, 그 도구 호출을 화면에 카드로 띄워 사람이 인자를 확인·수정하거나, 결과를 손으로 채워 넣거나, 재생성하게 한다. 즉 "생각→도구→관찰"이라는 에이전트 루프의 매 한 걸음을 사람이 직접 밟는 디버거에 가깝다.

작업의 단위는 스레드(Thread)다. 스레드 하나가 곧 저장 가능한 실험 파일(JSON) 하나이고, 그 안에 제목·모델과 파라미터·시스템 프롬프트·변수·도구 목록·메시지들·도구 호출·실행 이력·평가가 통째로 담긴다. 그래서 "모델만 바꿔서 다시" "이 도구 인자를 이렇게 고쳐서 다시" "실패했던 그 실행을 그대로 다시" 같은 실험을 파일 하나로 반복할 수 있다. 이름은 "LLM Space(엘엘엠 공간)"지만, 실체는 프롬프트·도구·모델을 이리저리 조립하며 노는 에이전트 실험실이다.

핵심 용어 · 이 앱의 몸통
Electrobun (일렉트로번) — Electron도 Tauri도 아니다
데스크톱 앱을 웹 기술(HTML/JS)로 만들 때 보통 Electron(크로미움+Node를 통째로 끼움, 무겁다)이나 Tauri(Rust+OS 웹뷰)를 쓴다. LLM Space는 그 어느 쪽도 아닌 Electrobun을 쓴다 — Bun(빠른 JS 런타임)을 심장으로 삼는 가벼운 데스크톱 껍데기다. 증거는 명확하다: apps/desktop/package.jsonelectrobun 의존성만 있고 electron@tauri-apps/*src-tauri/ 폴더도 없다. 그래서 "메인 프로세스"는 Node가 아니라 Bun 프로세스이고, 화면(웹뷰)의 React와는 타입이 붙은 RPC 채널 하나로 대화한다. 이 사실 하나를 놓치면 코드가 안 읽힌다.
핵심 용어 · 작업의 단위
스레드(Thread) · 하니스(Harness) 스텝
스레드는 이 앱에서 저장·공유·리플레이되는 실험 한 판이다 = JSON 파일 한 개. 하니스(harness)는 LLM을 감싸 "도구를 부르며 일하게" 만드는 실행 틀을 말하고, 그 틀이 한 번 도는 것이 한 "스텝"이다. LLM Space의 핵심 가치는 이 스텝을 자동으로 넘기지 않고 하나씩 멈춰 세워(inspect) 도구 호출·인자·결과를 사람이 뜯어보게 하는 데 있다. 실패한 실행은 이력에 스냅샷으로 남아 다시 돌려 볼 수 있고(replay), 두 실행을 나란히 놓고 점수를 매길 수 있다(evaluate). 앱 설명 문구 "prototype agent ideas, inspect every harness step, replay failures, and evaluate performance"가 이 네 동작을 그대로 요약한다.
한 문장 비유

"AI 에이전트용 비디오 편집 타임라인 — 통째로 재생만 하지 말고, 프레임 하나하나를 멈춰 보고·잘라 보고·다시 돌려라"

보통의 챗봇은 영화를 '재생' 버튼 한 번으로 끝까지 틀어 결말만 보여준다. LLM Space는 편집 타임라인이다 — 에이전트가 도구를 부르는 매 순간(프레임)에서 정지하고, 그 프레임의 인자·결과를 들여다보고, 마음에 안 들면 그 지점부터 다시 찍는다(리플레이). 심지어 두 편집본(실행)을 나란히 놓고 "어느 쪽이 더 나은가"를 채점표(루브릭)로 매긴다.

그리고 이 편집실은 당신 컴퓨터 안에 있다. 모든 스레드·설정은 ~/.llm-space 폴더의 평범한 파일로 저장된다 — 클라우드도, 로그인도, 데이터베이스도 없는 로컬 퍼스트(local-first). 필름은 전부 내 서랍 안에 있다.

2왜 지금 주목받는가

TrendShift Daily #13 · #AI agent — "DeerFlow의 자매 도구 + 스텝 디버깅 + 로컬 퍼스트"의 세 겹 매력

2026년, 누구나 에이전트를 만들 수 있게 되면서 진짜 어려운 문제는 "내 에이전트가 왜 저기서 헛발질했는지"를 알아내는 것으로 옮겨 갔다. 프롬프트를 한 줄 바꾸면 결과가 통째로 달라지는데, 챗 UI로는 그 안에서 무슨 일이 벌어졌는지 보이지 않는다. LLM Space는 바로 이 지점을 판다 — 에이전트의 실행을 스텝 단위로 열어 보고, 실패를 리플레이하고, 프롬프트·모델·도구를 바꿔 가며 A/B로 채점하게 한다. 게다가 이걸 만든 팀이 바이트댄스의 유명 딥리서치 프레임워크 DeerFlow 팀이고, "우리가 DeerFlow를 디버깅하려고 실제로 쓰는 도구"라는 도그푸딩 서사가 신뢰를 얹는다.

포인트내용
정체에이전트 프로토타이핑 데스크톱 앱. 채팅이 아니라 "만들기(Build)·추적(Trace)·디버그(Debug)·평가(Evaluate)"용 워크벤치. macOS 앱으로 배포.
차별점스텝별 하니스 인스펙션 — 도구 호출마다 루프를 멈춰 사람이 인자·결과를 뜯어보고 수정. 자동으로 안 넘어간다(원할 때만 ReAct 자동 루프 켬).
혈통DeerFlow(bytedance/deer-flow)의 자매 프로젝트. 그 팀이 자기 에이전트를 디버깅하려 만들어 오픈소스화. 공식 사이트 deer-flow.github.io/llm-space.
로컬 퍼스트모든 데이터가 ~/.llm-space의 파일로만 저장 — 클라우드·계정·DB 없음. 스레드는 그냥 JSON 파일이라 Git으로 버전 관리·공유 가능.
호환성도구는 MCP 서버 연결, 스킬은 ~/.claude/skills·~/.codex/skills를 그대로 재사용. 스레드 임포트는 OpenAI·Anthropic 대화 포맷도 인식.

주목 포인트 1 — "자동으로 안 돌리는" 에이전트라는 역발상

대부분의 에이전트 도구는 "얼마나 자율적으로 끝까지 해내는가"를 자랑한다. LLM Space는 반대로 기본값이 '수동'이다. 모델이 도구를 부르겠다고 하면 거기서 멈추고, 사용자가 재생 버튼(하나 실행)·"Call tools"(대기 중인 것 전부 병렬 실행)·"Continue"(루프 한 칸 전진)를 눌러야 나아간다. 원한다면 "Enable ReAct loop"를 켜서 사람 개입이 필요한 지점 전까지 자동으로 굴릴 수도 있다. "자율성"과 "관찰 가능성"을 맞바꾼 이 설계가, 에이전트를 디버깅하려는 개발자에게 정확히 맞는다.

주목 포인트 2 — 실패를 다시 돌려 보고, 점수로 비교한다

실행 이력(Run History)은 각 실행을 불변 스냅샷으로 남긴다. 그래서 "어제 실패한 그 실행"을 골라 그대로 다시 밟아 볼 수 있다(replay). 더 나아가 두 실행을 왼쪽/오른쪽에 놓고 채점한다 — 간단하게는 "어느 쪽이 낫다(verdict)", 정교하게는 2~6개 기준의 루브릭(각 1~5점)으로. 점수는 실행별 평균과 B − A 차이로 계산된다. 흥미로운 함정: 이 평가는 100% 사람이 매긴다 — LLM이 심판(LLM-as-judge)으로 끼어들지 않는다. 채점표를 나중에 고쳐도 과거 기록은 불변으로 남는다.

주목 포인트 3 — 두 개의 몸, 하나의 데이터

같은 소스에서 두 에디션이 나온다. 기본 LLM Space는 OS의 시스템 웹뷰를 쓰는 가벼운 몸(다운로드 ~27MB, 메모리·배터리 절약), LLM Space Performance는 크로미움을 CEF로 임베드한 무거운 몸(~130MB, 무거운 화면도 매끈). 둘은 같은 ~/.llm-space 데이터를 공유하되 업데이트 피드는 따로 간다. "가벼움 vs 성능"을 사용자가 고르게 한 영리한 이원화다.

중요 · 오해 방지 — "deer-flow/llm-space"를 두 번 헷갈리지 말 것
bytedance/deer-flow(딥리서치)와 다른 저장소다 ② Electron/Tauri가 아니라 Electrobun이다

속기 쉬운 점 ①: deer-flow라는 이름 때문에 이미 유명한 bytedance/deer-flow(딥리서치 프레임워크)와 같은 것으로 착각하기 쉽다. 여기서 deer-flow는 GitHub 조직(org) 이름이고, 저장소는 llm-space — DeerFlow의 자매이지 DeerFlow 자체가 아니다. (이 문서 폴더에도 이미 deer-flow-deep-dive.html이 있지만 그건 bytedance/deer-flow를 다룬 별개 자료다.)

속기 쉬운 점 ②: "데스크톱 앱 + React"라고 하면 반사적으로 Electron을 떠올린다. 아니다 — Electrobun이라 메인 프로세스가 Bun이고, Node·npm을 아예 안 쓴다(패키지 매니저도 Bun). 이 전제를 깔고 봐야 apps/desktop/src/bun/(메인) ↔ 웹뷰(React)의 구조가 이해된다.

3기술 스택 전체 지도

주인공은 Cargo.tomlDockerfile도 아닌 Bun 워크스페이스 — 순수 TypeScript 342개 파일(.ts 199 + .tsx 143)

이 저장소엔 Rust도 Go도 Python도 없다(파이썬은 CI에서 YAML 검증용 한 줄이 전부). 거의 순수 TypeScript/TSX 프로젝트이고, 런타임·패키지 매니저·빌드가 전부 Bun으로 돈다. 스택을 이해하는 축은 셋이다 — "무엇이 앱을 실행하는가(껍데기)", "무엇이 화면을 그리는가(렌더러)", "무엇이 에이전트를 돌리는가(엔진)".

껍데기·도구 (런타임 · 빌드 레이어)

요소내용
데스크톱 셸electrobun ^1.18.1 — Bun 네이티브 데스크톱 껍데기. Electron·Tauri 아님. 메인=Bun 프로세스, 렌더러=웹뷰, 둘을 타입 RPC로 연결.
런타임·패키지Bun 1.3이 런타임 겸 패키지 매니저. Node·npm·pnpm·yarn 없음. 설치는 bun install.
태스크 러너mise(min 2026.2.5) — 모든 사용자 명령의 "정문". mise run dev가 내부 스크립트로 위임.
언어·빌드TypeScript(엄격, ESNext) + Vite ^6.0.3(@vitejs/plugin-react)로 렌더러 번들. 린트 eslint 10(flat) + prettier.

화면 (React 렌더러 레이어)

요소내용
UI 프레임워크React 19(^19.0.0) + react-dom. 상태는 스레드마다 하나씩 두는 Zustand 5(전역 스토어 없음), 서버 통신은 @tanstack/react-query 5.
디자인 시스템Tailwind CSS v4(OKLch 토큰, 다크 테마) + shadcn + radix-ui + @base-ui/react. 아이콘 lucide-react + @lobehub/icons(LLM 브랜드 로고).
에디터·표시CodeMirror 6(@uiw/react-codemirror, json/markdown, 여러 테마) — 스레드의 JSON·프롬프트 편집. 마크다운은 react-markdown+remark-gfm.
UX 소품크롬식 탭 @sinm/react-chrome-tabs, 드래그 @hello-pangea/dnd, 명령 팔레트 cmdk, 토스트 sonner, 리사이즈 패널 react-resizable-panels, 폰트 Geist.
검증·스키마zod 4(런타임 검증) + typebox(스레드·모델 타입 스키마). MCP는 @modelcontextprotocol/sdk 1.29.0.

두뇌 (에이전트 엔진 레이어)

요소내용
에이전트 프레임워크@earendil-works/pi-agent-core · pi-ai ^0.80.10 — "Pi"라는 경량 에이전트 프레임워크. 이게 없으면 이 앱은 못 돈다. pi-agent-core=에이전트 루프(agentLoopContinue), pi-ai=프로바이더/모델 추상화.
모델 프로바이더빌트인 24종 — openai·anthropic·google·deepseek·groq·xai·openrouter·moonshotai·minimax·amazon-bedrock, 그리고 스폰서 바이트댄스의 VolcEngine Ark 3종(ark·ark-agent-plan·ark-coding-plan) 등. API 방언 3종(anthropic-messages·openai-completions·openai-responses).
스트리밍 파싱parse-sse(SSE 소비) + best-effort-json-parser(스트리밍 중 반쪽 JSON도 최선 파싱) — 도구 인자가 조각조각 흘러올 때 쓴다.
인증·네트워크GitHub OAuth 디바이스 플로우(bun/auth/), 모든 요청을 사용자 프록시로 라우팅(NetworkSettingsManager). 텔레메트리는 posthog-node(EU, 옵트아웃).
핵심 용어 · 엔진의 정체
Pi (pi-agent-core · pi-ai)
LLM Space가 직접 에이전트 루프를 짠 게 아니라, 외부 오픈소스 @earendil-works/pi("가벼운 에이전트 프레임워크")를 심장으로 가져다 쓴다. pi-agent-core는 "생각→도구→관찰"을 한 스텝 전진시키는 agentLoopContinue()AgentEvent·AgentTool 타입을 제공하고, pi-ai는 수십 개 LLM 프로바이더를 하나의 인터페이스로 감싼다. 그래서 LLM Space의 몫은 "이 프레임워크를 데스크톱 UI·로컬 파일·스텝 디버깅에 어떻게 얹느냐"이지, 에이전트 루프 자체의 발명이 아니다. 배울 때 이 경계를 알면 코드가 훨씬 잘 읽힌다.
한눈에 — "세 층으로 쌓인 작업실"

스택이 많아 보여도 층은 셋이다. 바닥(껍데기)은 Electrobun+Bun이 앱을 띄우고, 가운데(화면)는 React 19+Tailwind+CodeMirror가 스레드를 그리고 편집하며, 천장(두뇌)은 Pi 프레임워크가 모델을 부르고 도구를 돌린다. 바닥과 천장은 각각 Bun 프로세스와 웹뷰로 분리돼 있고, 그 사이를 타입 RPC 한 가닥이 잇는다. 이 "메인↔웹뷰가 RPC로만 대화한다"는 구조가 다음 장(§4)의 모든 것을 좌우한다.

4아키텍처 심화 분석

데이터 흐름(Run→RPC→streamAgent) · 스텝별 정지(terminate:true) · HostServices 이음새 · Command 계층, 이 넷이 핵심

4-1. 전체 데이터 흐름 — "Run" 한 번이 거치는 길

LLM Space를 이해하는 가장 빠른 길은 사용자가 Run(실행)을 누른 순간부터 화면에 글자가 흐르기까지의 경로를 따라가는 것이다. 웹뷰(React)와 메인(Bun)이 분리돼 있으니, 그 사이를 어떻게 건너는지가 관건이다. 핵심 반전은 Electrobun의 RPC에는 '스트리밍'이 없다는 점 — 그래서 이 앱은 한 번 쏘고 잊는(fire-and-forget) 메시지들을 streamId로 엮어 스트림을 흉내 낸다.

┌────────────────────────────────────────────────────────────────────────┐ │ LLM Space 실행 흐름 — "Run" 버튼 → 화면에 토큰이 흐르기까지 │ │ │ │ [웹뷰 · React] [메인 · Bun 프로세스] │ │ │ │ 사용자가 Run 클릭 │ │ │ run() (ui/stores/thread-store.ts, Zustand) │ │ v │ │ streamThread() (core/client/api.ts) │ │ │ ← AgentTransport 를 주입받아 호출 │ │ v │ │ createRpcTransport() ────(RPC 메시지: sendStreamThreadRequest)───► │ │ ▲ 스트리밍 없는 RPC 위에서 StreamThreadController│ │ │ streamId(uuid)로 스트림을 .run() │ │ │ "시뮬레이션" │ │ │ │ v │ │ │ streamAgent() │ │ │ (core/server/agent/stream.ts)│ │ │ │ │ │ │ agentLoopContinue() │ │ │ (@earendil-works/pi-agent) │ │ │ │ AgentEvent │ │ ◄──(RPC 메시지: receiveStreamThreadResponse, streamId로 태깅)──────────┘│ │ │ │ │ v transport 가 메시지들을 다시 `for await` async iterator 로 재조립│ │ reduceMessages() (core/client/reducer.ts) — 이벤트를 Message 로 접기 │ │ │ │ │ v Zustand 갱신 → React 리렌더 → 화면에 토큰/도구카드 표시 │ └────────────────────────────────────────────────────────────────────────┘

여기서 계약서 역할을 하는 것이 src/shared/rpc.tsDesktopRPCType다. 요청/응답형(웹뷰→메인, 답을 기다림)에는 availableModels·프로바이더 CRUD·파일 조작(fsLs/fsRead/fsWrite/…)이 있고, 한 번 쏘고 잊는 메시지형에 스트리밍 3종(sendStreamThreadRequest·receiveStreamThreadResponse·abortStreamThread)이 있다. "스트리밍 없는 채널 위에 스트림을 얹는" 이 패턴은 그 자체로 좋은 교보재다.

핵심 통찰 · 스트리밍 없는 RPC 위의 스트림
streamId 로 엮는 가짜 스트림
Electrobun의 RPC는 "요청→응답" 또는 "쏘고 잊기"만 된다 — 서버가 조금씩 계속 밀어 주는 진짜 스트리밍이 없다. 그런데 LLM 응답은 토큰이 한 글자씩 흘러오는 스트림이다. 이 간극을 어떻게 메울까? LLM Space는 실행마다 streamId(uuid)를 하나 발급하고, 메인이 receiveStreamThreadResponse라는 쏘고 잊는 메시지를 그 id를 붙여 연달아 발사한다. 웹뷰 쪽 트랜스포트는 같은 id의 메시지들을 모아 for await로 순회 가능한 async iterator로 재조립한다. 결과적으로 위층 코드는 "진짜 스트림"을 쓰는 것처럼 보인다. 전송 계층의 한계를 상위 추상화로 감추는 전형적 기법.

4-2. 스텝별 정지 — 이 앱의 심장

이제 이 프로젝트를 "또 하나의 챗 앱"이 아니게 만드는 지점이다. 비밀은 놀랍도록 단순하다. core/server/agent/stream.ts에서 도구들을 에이전트 루프에 넘길 때 { stepByStep: true } 옵션을 준다. 그러면 모든 도구의 execute()가 실제로 도구를 실행하는 대신 { terminate: true }를 돌려주고 루프를 그 자리에서 멈춘다.

// core/server/agent/stream.ts — 스텝별 정지의 핵심 (요지)
const agentTools = _convertToAgentTools(tools, {
    stepByStep: true,     // ← 이 한 줄이 "수동 디버거"를 만든다
});
// stepByStep 이면 각 도구의 execute() 는 도구를 '실행'하지 않고
// { terminate: true, content: [...] } 를 반환 → 루프가 도구 호출 직후 멈춤
// → UI 가 그 도구 호출 카드를 띄우고 사람의 판단을 기다린다

멈춰 선 루프 앞에서 사용자는 세 가지를 할 수 있다. ① 도구 카드의 재생 버튼으로 그 도구 하나만 실행, ② "Call tools"로 대기 중인 도구를 전부 병렬 실행, ③ "Continue"로 ReAct 루프를 한 칸 전진. 그리고 인자를 고치거나 도구 결과를 가짜로 채워 넣어 특정 엣지 케이스를 재현할 수도 있다. "Enable ReAct loop"를 켜면 사람 개입이 필요한 스텝 전까지 자동으로 굴러간다. 자율성을 끄고 관찰 가능성을 켠 이 스위치가, 디버깅 도구로서의 정체성을 만든다.

4-3. 리플레이와 평가 — 불변 스냅샷의 힘

리플레이(replay)는 실행 이력에 남는 ThreadSnapshot으로 돌아간다. 되돌리기/다시하기(stores/thread-history.ts)는 복사-후-쓰기(copy-on-write) 참조를 써서 스냅샷 하나가 사실상 포인터 이동(O(1))이다 — 스레드를 통째로 복제하지 않는다(개수 + 보관 이미지 바이트 예산으로 상한). 평가(evaluate)는 두 실행(leftRunId/rightRunId)을 골라 비교한다.

평가(Evaluation) 방식 — 사람이 매긴다, LLM 심판 없음 ┌──────────────────────────────────────────────────────────────┐ │ 방법 A · 판정(verdict) │ │ 좌 실행 vs 우 실행 → leftBetter / rightBetter / tie │ │ │ │ 방법 B · 루브릭(rubric) — 2~6개 기준, 각 1~5점 │ │ 기준1 정확성 좌 4 · 우 5 │ │ 기준2 간결성 좌 3 · 우 4 │ │ 기준3 안전성 좌 5 · 우 4 │ │ ──────────────────────────── │ │ 평균 좌 4.0 · 우 4.33 │ │ 델타 (B − A) = +0.33 → 우측이 낫다 │ │ │ │ · 점수는 사람이 입력 (모델 호출 0회) │ │ · 저장된 평가 = 루브릭+점수의 불변 스냅샷 (안정적 run ID 키) │ │ · 나중에 루브릭을 고쳐도 과거 기록은 다시 안 쓰인다 │ └──────────────────────────────────────────────────────────────┘

점수는 실행별 가중치 없는 평균B − A 델타(evaluationScoreDelta)로 계산된다. 다시 강조할 함정 — 이 평가는 전부 사람 손이다. "성능 평가"라는 말에 자동 채점을 기대하면 안 된다. 대신 채점의 기록이 불변이라, 프롬프트를 바꿔 가며 실험한 결과를 신뢰 가능한 히스토리로 쌓을 수 있다.

4-4. HostServices 이음새 — 데스크톱과 웹이 같은 UI를 쓰는 비결

LLM Space에는 정적 웹사이트(공유된 스레드를 열어 보는 뷰어)도 딸려 있다. 그런데 데스크톱 앱과 웹사이트가 똑같은 React UI(packages/ui의 Thread Playground)를 공유한다. 어떻게? 파일 접근·모델 호출 같은 "환경에 의존하는 일"을 HostServicesProvider/useHostServices()라는 이음새(seam) 뒤로 숨겼기 때문이다.

// 같은 Thread Playground 컴포넌트, 주입되는 host 만 다르다
// 데스크톱 (apps/desktop/src/host/host-services.tsx)
<DesktopHostProvider>      // 진짜 Electrobun RPC 기반 구현 주입
   <ThreadPlayground />     //   fs 읽기/쓰기, 모델 호출이 실제로 동작
</DesktopHostProvider>

// 웹사이트 (web/)
<HostServicesProvider host={{ presentational: true }}>
   <ThreadPlayground />     // 표시 전용 no-op 스텁 — 클릭해도 실행은 안 됨
</HostServicesProvider>

덕분에 packages/uiElectrobun을 전혀 몰라도 되는(=의존하지 않는) 순수 UI가 되고, 같은 코드가 데스크톱에선 살아 움직이고 웹에선 읽기 전용으로 렌더된다. "환경 의존을 인터페이스로 뽑아 주입한다"는 의존성 역전의 교과서적 적용이다.

보조 개념 · 경계를 넘는 모든 행동은 Command
Command 계층 (src/shared/commands.ts)
웹뷰와 메인의 경계를 넘는 사용자 행동(파일 열기, 모델 목록 새로고침 등)은 전부 하나의 Command 객체로 표현된다 — type 구분자 + 타입이 붙은 args. 각 커맨드는 COMMAND_META에서 target: "webview" | "bun"으로 태깅되고, 양쪽의 단일 executeCommand 라우터가 알맞은 곳으로 보낸다. "행동을 데이터로 만들어 한 곳에서 라우팅"하는 이 패턴 덕에, 새 기능을 추가할 때 커맨드 하나만 등록하면 된다.
한눈 정리 · 아키텍처가 지키는 핵심 4가지
가짜 스트림 · 스텝별 정지 · 불변 스냅샷 · 주입되는 Host

가짜 스트림: 스트리밍 없는 RPC 위에 streamId로 async iterator를 재조립. ② 스텝별 정지: stepByStep+terminate:true로 도구 호출마다 루프를 멈춰 사람이 개입 — 이 앱만의 차별점. ③ 불변 스냅샷: 복사-후-쓰기로 O(1) 리플레이 + 사람이 매기는 불변 평가. ④ 주입되는 Host: HostServices 이음새로 데스크톱/웹이 같은 UI 공유. ①③④는 재사용 가능한 설계 패턴, ②는 LLM Space의 정체성이다.

5디렉토리 · 모노레포 구조 해부

순수 TypeScript 461개 파일 — Bun 워크스페이스 packages/* · apps/* · web의 3구획

저장소는 Bun 워크스페이스 모노레포다. 크게 재사용 라이브러리(packages/), 실제 앱(apps/desktop), 웹사이트(web)로 나뉜다. "무엇을 먼저 읽어야 하는가" 관점의 지도는 아래와 같다.

llm-space/ ├── package.json 루트(private 모노레포, 버전 없음) · Bun workspaces ├── mise.toml / mise.lock 툴체인 고정(Bun 1.3) · 모든 명령의 "정문" ├── packages/ │ ├── core/ @llm-space/core ★ 도메인 라이브러리(빌드 스텝 없음, TS 직접 소비) │ │ ├── client/ streamThread()·reduceMessages()·AgentTransport (브라우저 안전) │ │ ├── server/ ★ agent/stream.ts = streamAgent() · storage · paths (Bun 전용) │ │ ├── thread/ history.ts(리플레이)·usage.ts(토큰 집계) │ │ ├── types/ agent·mcp·skills·models·threads·tools (typebox 스키마) │ │ └── parsers/ 외부 포맷(OpenAI/Anthropic/Aurora) → 네이티브 스레드 │ ├── ui/ @llm-space/ui ★ 공유 React 디자인 시스템 + Thread Playground │ │ └── components/thread-playground/ ← 제품의 얼굴(Build·Trace·Debug·Evaluate) │ │ ├── stores/thread-store.ts run() = Zustand 실행 진입점 │ │ ├── stores/thread-history.ts 되돌리기/다시하기(copy-on-write) │ │ ├── run-trace-view.tsx 실행 추적(스텝 인스펙션) │ │ └── run-evaluation-*.tsx 루브릭 채점·스코어카드 │ └── (ui 는 Electrobun 을 전혀 모른다 = 웹에서도 그대로 렌더) ├── apps/ │ └── desktop/ @llm-space/desktop ★★ Electrobun 앱 (버전 4.1.1 = 단일 진실원) │ ├── electrobun.config.ts 두 에디션(system webview / CEF) 정의 │ └── src/ │ ├── bun/ ← 메인 프로세스(Bun): rpc·streaming·models·mcp·skills· │ │ tools·traces·auth(GitHub device flow)·storage·updates │ ├── mainview/ 렌더러 진입(index.html + main.tsx, Vite) │ ├── host/ DesktopHostProvider(진짜 구현 주입) │ └── shared/ rpc.ts(DesktopRPCType) · commands.ts(Command 계층) ├── web/ @llm-space/web 정적 랜딩 + 공유 스레드 뷰어(GitHub Pages) ├── docs/ 사용자 가이드(EN + zh-CN) — get-started·core-concepts·settings… └── .agents/skills/ 내부 개발 툴(electrobun-cdp-debug · kaizen-loop)

꼭 먼저 볼 곳

경로역할
packages/core/server/agent/stream.ts엔진의 심장. streamAgent()가 Pi의 agentLoopContinue()를 몰고, stepByStep으로 스텝별 정지를 건다. 여길 읽으면 실행이 어떻게 도는지 다 보인다.
packages/ui/…/thread-playground/제품의 얼굴. 스레드를 만들고·추적하고·채점하는 모든 UI. stores/thread-store.tsrun()이 실행 진입점.
packages/core/client/{api,reducer}.tsstreamThread()(트랜스포트 주입)와 reduceMessages()(이벤트→메시지 접기). §4-1 흐름의 웹뷰 쪽 절반.
apps/desktop/src/bun/메인 프로세스 전부 — RPC 서버, 스트리밍 컨트롤러, 모델·MCP·스킬 매니저, GitHub 인증. "데스크톱만의 진짜 구현"이 사는 곳.
apps/desktop/src/shared/{rpc,commands}.ts웹뷰↔메인 계약서. DesktopRPCType(RPC 스키마)와 Command 계층. 두 프로세스가 어떻게 대화하는지의 사전.
packages/core/types/스레드·모델·도구의 typebox 스키마. 데이터 모양을 알고 싶으면 여기부터.
한눈에 — "core는 두뇌, ui는 얼굴, desktop은 몸"

파일이 많아도 규칙만 알면 조직도처럼 읽힌다. packages/core두뇌(에이전트 로직·타입·저장), packages/ui얼굴(화면과 상호작용), apps/desktop몸(Electrobun 껍데기 + 메인 프로세스)이다. 핵심 규칙 하나: core와 ui는 "환경"을 몰라야 한다 — 그래야 데스크톱과 웹이 같은 두뇌·얼굴을 공유한다. 환경 의존(파일·RPC)은 전부 apps/desktop에 갇힌다. 그래서 "새 기능을 어디에 둘까?"의 답은 대개 "환경을 타면 desktop, 아니면 core/ui"다.

6학습 포인트

"LLM Space 사용법"이 아니라 "관찰 가능한 에이전트 도구를 어떻게 설계하는가"로 가져갈 것들

배울 것 1 — 스트리밍 없는 채널 위에 스트림을 얹기

가장 재사용성 높은 기법. Electrobun RPC엔 스트리밍이 없는데, LLM 응답은 스트림이다. LLM Space는 streamId로 태깅한 '쏘고 잊는' 메시지들을 웹뷰에서 async iterator로 재조립해 이 간극을 메운다. WebSocket이 없거나, IPC가 요청/응답만 되는 어떤 환경에서든 그대로 응용할 수 있는 패턴이다. "전송 계층의 제약을 상위 추상화로 감추는 법"의 실물 교보재.

배울 것 2 — 자율성을 끄면 관찰 가능성이 켜진다

{ stepByStep: true } 한 줄로 도구의 execute()terminate:true를 뱉게 만들어, 에이전트 루프를 도구 호출마다 멈추는 설계. "얼마나 자율적인가"만 좇던 흐름에서 벗어나, "얼마나 들여다볼 수 있는가"를 1급 기능으로 끌어올린 발상이다. 에이전트를 디버깅·교육·감사해야 하는 상황이라면 반드시 참고할 만하다.

배울 것 3 — 환경 의존을 인터페이스로 뽑아 주입하기

HostServicesProvider/useHostServices() 이음새로 파일·모델 호출 같은 환경 의존을 숨겨, 같은 React UI가 데스크톱(실제 구현)과 웹(표시 전용 스텁)에서 모두 돈다. 의존성 역전(DIP)을 "한 코드베이스, 두 실행 환경"으로 구현한 깔끔한 사례. 테스트용 mock 주입, Storybook, 멀티 플랫폼 앱 어디에나 통하는 사고법이다.

배울 것 4 — 파일이 곧 데이터베이스인 로컬 퍼스트

SQLite도 ORM도 없이 ~/.llm-space 아래 JSON·마크다운 파일로 모든 것을 저장한다. 스레드는 사람이 읽을 수 있는 JSON이라 Git으로 diff·버전 관리·공유가 된다. 쓰기는 임시 파일→rename 원자적 교체로 안전하게. "DB를 두지 않고도 견고한 영속성을 얻는 법", 그리고 "사용자가 자기 데이터를 소유한다"는 로컬 퍼스트 철학의 실전 구현이다.

배울 것 5 — 사람이 매기는 평가 루프(불변 스냅샷)

두 실행을 루브릭(2~6기준, 1~5점)으로 채점하고 B − A 델타로 비교하되, 평가는 안정적 run ID를 키로 하는 불변 스냅샷으로 저장된다. LLM 심판을 끼우지 않아 재현 가능하고 신뢰할 수 있는 실험 기록을 남긴다. "프롬프트 실험을 어떻게 과학적으로 관리하는가"의 미니멀한 답. LLM-as-judge의 편향이 부담스러운 상황에서 특히 참고할 만하다.

배울 것 6 — Bun · Electrobun 모노레포 운영

Node·npm 없이 Bun 하나로 런타임·패키지·테스트를 통일하고, mise로 툴체인을 못 박고, "환경을 아는 코드"를 apps/desktop에만 격리한 워크스페이스 설계. 렌더러 빌드가 --max-old-space-size=4096 없이는 14701개 모듈에서 OOM 나던 실전 교훈(build:view)까지 남아 있다. "가벼운 대안 스택으로 크로스 플랫폼 데스크톱 앱을 짜는 법"을 통째로 볼 수 있다.

한 줄 정리 · 재사용 가능한 교훈
"이 저장소의 진짜 교재 가치는 'LLM Space 쓰는 법'이 아니라 '관찰 가능하고 로컬 퍼스트인 에이전트 도구의 설계'다"

모델이 무엇이든, 이 저장소가 남기는 원칙은 보편적이다. 에이전트를 자동으로만 돌리지 말고 멈춰 세워 보게 하고, 환경 의존은 인터페이스로 주입하고, 데이터는 사용자 소유의 파일로 남기고, 실험은 불변 기록으로 채점하라. 에이전트 프레임워크(Pi)는 남의 것을 빌려 쓰되, 그 위에 "관찰·리플레이·평가"라는 개발자 경험을 얹은 제품 전략까지 — 에이전트를 직접 만들지 않더라도, 개발자 도구를 설계하는 사람이라면 뜯어볼 값어치가 크다.

7시스템 · 실행 요구사항

두 갈래 — "그냥 쓰기(macOS DMG 내려받기)"와 "소스에서 직접 빌드(Bun 툴체인)"

대부분의 사용자는 릴리스에서 macOS 앱을 내려받으면 된다. 반면 소스에서 빌드하려면 Bun 툴체인이 필요하다. 아래 표가 두 경로를 가른다.

항목요구/수치
OSmacOS 전용. 릴리스는 Apple Silicon(arm64) + Intel(x64) DMG. (electrobun.config.ts에 linux·win 항목은 있으나 CEF 미번들 상태로 배포되지 않음.)
에디션 선택LLM Space(시스템 웹뷰, ~27MB, 가벼움) vs LLM Space Performance(CEF 크로미움 임베드, ~130MB, 무거운 화면도 매끈). 둘 다 ~/.llm-space 공유, 업데이트 피드는 별도.
모델·인증빌트인 24종 프로바이더 중 택1의 API 키(openai·anthropic·deepseek·google·xai·openrouter·moonshotai·VolcEngine Ark 등). 온보딩 때 환경변수(OPENAI_API_KEY·ARK_API_KEY 등)에서 자동 감지. 도구용 GitHub은 OAuth 디바이스 플로우.
GPU불필요. 추론은 원격 모델 API에 위임 — 앱 자체는 순수 클라이언트. 키와 파일은 로컬에 머문다.
빌드(소스)Bun 1.3(런타임 겸 패키지 매니저) — Node.js 불필요. 설치 bun install(또는 mise run setup으로 고정 툴체인). 개발 mise run dev(Vite HMR :5173 + electrobun dev).
디버깅(선택)mise run dev:cef로 CEF/CDP 디버깅을 127.0.0.1:9333에 노출. 텔레메트리는 LLM_SPACE_ANALYTICS_DISABLED=1로 끔.
한눈 요약 — "완제품을 받을까, 부품으로 조립할까"

macOS DMG는 "완제품 구매"다 — 내려받아 모델 키만 넣으면 끝, Bun을 몰라도 된다. 반면 소스 빌드는 "부품 조립 키트"라 Bun 1.3 툴체인이 필요하다(단 Node는 안 쓴다). 대다수는 완제품으로 충분하고, "스텝별 정지가 진짜 어떻게 구현됐는지 뜯어보려는 사람"만 조립 경로로 간다. GPU가 없어도 되니 값싼 API 키 하나로 누구나 시작할 수 있다. 다만 지금은 macOS만이라는 문턱은 분명히 있다.

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

난이도별 — "설치·예제 체험 → 스텝 디버깅 → 두 실행 채점 → 소스 읽기 → 스텐 흐름 추적"

먼저 설치. macOS라면 릴리스에서 DMG를, 소스를 볼 거라면 클론 후 Bun으로.

# 소스에서 실행 (macOS + Bun 1.3)
git clone https://github.com/deer-flow/llm-space.git
cd llm-space
bun install          # 또는 mise run setup (고정 툴체인)
mise run dev         # Vite HMR + electrobun dev
실습 1 · 난이도 ★☆☆☆☆

"Start from Example"로 에이전트 한 판 돌려 보기 — 스레드 개념 체감

General Agent나 Deep Research 예제를 열어 스레드가 무엇으로 이뤄졌는지 본다 — 시스템 프롬프트·모델·도구·메시지가 한 파일에 담긴 구조를 §1과 대조한다. Run을 눌러 도구 호출 카드가 뜨는 순간을 관찰한다.

실습 2 · 난이도 ★★☆☆☆

스텝별로 멈춰 도구 인자 고쳐 보기 — 이 앱의 존재 이유

에이전트가 bashread 도구를 부르려 할 때 루프가 멈추는 것을 확인하고, 재생 버튼(하나 실행)·"Call tools"(전부)·"Continue"(전진)를 눌러 본다. 도구 인자를 손으로 수정하거나 결과를 가짜로 채워 특정 상황을 재현해 본다. §4-2의 terminate:true가 무슨 뜻인지 몸으로 안다.

실습 3 · 난이도 ★★★☆☆

두 실행을 루브릭으로 채점 — replay + evaluate

같은 스레드에서 모델이나 프롬프트만 바꿔 두 번 실행한 뒤, 실행 이력에서 둘을 골라 루브릭(2~3개 기준, 1~5점)으로 채점한다. B − A 델타가 어떻게 나오는지 보고, 나중에 루브릭을 고쳐도 과거 점수가 안 바뀌는(불변) 것을 확인한다.

실습 4 · 난이도 ★★★★☆

소스 클론해 실행 흐름 읽기 — streamAgent를 따라가기

packages/core/server/agent/stream.tsstreamAgent()client/api.tsstreamThread()를 연다. "Run → RPC → agentLoopContinue → 이벤트 → reduceMessages"의 흐름(§4-1)을 코드로 따라가고, stepByStep 옵션이 어디서 걸리는지 찾는다.

실습 5 · 난이도 ★★★★★

~/.llm-space 를 열어 스레드 JSON 해부 — 로컬 퍼스트의 실체

홈 폴더의 ~/.llm-space/workspace/에서 스레드 JSON을 직접 열어, docs/core-concepts.md의 스키마(title·model·context.{systemPrompt,tools,messages,variables})와 대조한다. 파일을 Git에 커밋해 diff를 보고, settings/models.json·mcp.json이 어떻게 생겼는지도 확인한다. "DB 없이 파일로 사는 앱"의 실체를 손으로 만진다.

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

LLM Space를 정복하며 곁들여 익히면 좋은 것들 — 주차별 5단계
주차주제 · 목표
1주차에이전트 루프의 기본기. "생각→도구 호출→관찰→반복"의 ReAct 루프, 도구 스키마, 이벤트를 메시지로 접는 리듀서 패턴. 목표: §4-1 흐름도를 스스로 그릴 수 있게.
2주차프로세스 간 통신(IPC/RPC). 메인↔웹뷰 분리, 요청/응답 vs 쏘고 잊기, 스트리밍 없는 채널 위에 스트림 얹기(streamId 재조립). Electrobun/Electron의 프로세스 모델.
3주차React 상태·의존성 주입. Zustand 스토어 설계, HostServicesProvider식 컨텍스트 주입으로 환경 의존 분리(DIP), copy-on-write 되돌리기/다시하기.
4주차로컬 퍼스트 영속성. 파일 기반 저장, 원자적 쓰기(temp→rename), 사람이 읽는 JSON을 Git으로 버전 관리. typebox/zod로 스키마를 런타임 검증하는 법.
5주차에이전트 상호운용. MCP(도구 연결)로 외부 도구 붙이기, SKILL.md 스킬~/.claude/skills와 공유, 프로바이더 추상화(pi-ai)로 여러 LLM API 방언 다루기.

10핵심 키워드 사전

이 문서와 저장소에서 반복되는 용어 빠른 참조
용어
LLM Spacedeer-flow/llm-space. 에이전트를 만들고·스텝별로 들여다보고·리플레이하고·채점하는 로컬 퍼스트 macOS 앱. DeerFlow의 자매 프로젝트, v4.
Electrobun이 앱의 데스크톱 껍데기. Bun 기반으로 Electron·Tauri가 아니다. 메인=Bun 프로세스, 렌더러=웹뷰, 둘을 타입 RPC로 연결.
스레드 (Thread)작업·저장·공유·리플레이의 단위 = JSON 파일 1개. title·model·context{systemPrompt,tools,messages,variables} + 실행 이력·평가.
스텝별 정지stepByStep:true로 도구의 execute()terminate:true를 반환 → 루프가 도구 호출마다 멈춰 사람이 인자·결과를 뜯어봄. 이 앱의 정체성.
Pi (pi-agent-core/pi-ai)외부 경량 에이전트 프레임워크(@earendil-works/pi). agentLoopContinue()=루프, pi-ai=프로바이더 추상화. LLM Space의 엔진.
streamThread / streamAgent각각 웹뷰 쪽 진입(core/client/api.ts)과 메인 쪽 구동(core/server/agent/stream.ts). 사이를 RPC 트랜스포트가 잇는다.
streamId 재조립스트리밍 없는 RPC 위에서, 같은 streamId를 붙인 '쏘고 잊는' 메시지들을 웹뷰가 async iterator로 되모아 가짜 스트림을 만드는 기법.
Run History · replay각 실행을 불변 ThreadSnapshot으로 저장(copy-on-write, O(1)). 과거 실행을 골라 다시 밟아 볼 수 있다.
Rubric 평가두 실행을 2~6기준·1~5점으로 채점, B − A 델타로 비교. 전부 사람이 매김(LLM 심판 없음), 기록은 불변.
HostServices파일·모델 호출 같은 환경 의존을 감추는 이음새. 데스크톱은 실제 구현, 웹은 표시 전용 스텁을 주입 → 같은 UI 공유.
로컬 퍼스트모든 데이터가 ~/.llm-space의 파일로만 저장(클라우드·계정·DB 없음). 스레드는 Git으로 관리 가능.
두 에디션LLM Space(시스템 웹뷰, ~27MB) vs LLM Space Performance(CEF 크로미움, ~130MB). 데이터 공유, 업데이트 피드는 별도.

11참고 링크

공식 소스 우선 — 버전·수치는 클론한 소스(앱 4.1.1, 커밋 cae5ace) 기준

공식
· GitHub 저장소: github.com/deer-flow/llm-space (MIT · Electrobun 데스크톱 앱)
· TrendShift 카드: trendshift.io/repositories/83147 (Daily #13 · #AI agent)
· 공식 사이트: deer-flow.github.io/llm-space (랜딩 + 공유 스레드 뷰어)
· 자매 프로젝트(별개): github.com/bytedance/deer-flow (DeerFlow 딥리서치 — 이 문서 폴더의 deer-flow-deep-dive.html)

저장소 안에서 꼭 읽을 것
· packages/core/server/agent/stream.tsstreamAgent() + 스텝별 정지(엔진의 심장)
· packages/core/client/{api,reducer}.tsstreamThread() · reduceMessages()
· packages/ui/src/components/thread-playground/ — 제품의 얼굴(Build·Trace·Debug·Evaluate)
· apps/desktop/src/shared/{rpc,commands}.ts — 웹뷰↔메인 계약서
· packages/core/types/ — 스레드·모델·도구 typebox 스키마
· docs/core-concepts.md — 스레드 JSON 스키마 공식 설명

곁들여 학습
· Bun · Electrobun — Node 없는 런타임 · Bun 기반 데스크톱 셸
· React 19 · Zustand · TanStack Query · Tailwind v4 · shadcn/radix
· @earendil-works/pi(pi-agent-core/pi-ai) — 경량 에이전트 프레임워크
· MCP(Model Context Protocol) · SKILL.md 스킬(Claude Code/Codex 호환)
· 이미 분석한 다른 에이전트 도구 딥다이브와 비교(§인덱스 cat-agents — DeepTutor·airi 등)

참고 · 소스 정독으로 확인한 주의점
"deer-flow인데 DeerFlow가 아니고, 데스크톱인데 Electron이 아니다"를 잊지 말 것

bytedance/deer-flow와 다른 저장소다deer-flow는 조직명, 저장소는 llm-space. DeerFlow의 자매 도구다. ② Electron/Tauri가 아니라 Electrobun — 메인이 Bun 프로세스이고 Node·npm을 안 쓴다. ③ 버전 진실원은 apps/desktop/package.json4.1.1 하나뿐 — core/ui/web의 4.0.1은 의도적으로 안 올린 값이지 버그가 아니다. ④ 현재 macOS 전용(arm64+x64 DMG). linux·win 설정은 있으나 배포 안 됨. ⑤ 평가는 100% 사람 손이다 — "성능 평가"라는 말에 LLM 자동 채점을 기대하면 안 된다. ⑥ VolcEngine Ark 카탈로그의 doubao-seed-2.x·deepseek-v4·glm-5.2·kimi-k2.7·minimax-m3 같은 미래형 모델명은 실존 여부 검증 필요(문서 시점 기준 미확인, 예시·전방참조 성격). ⑦ 한국어 README·문서는 없다(EN·zh-CN만) — 사실상 첫 한국어 정리. ⑧ 이 문서 수치는 분석 시점 최신 커밋(2026-07-17, cae5ace) 기준이며, 트렌딩 특성상 순위·지표는 계속 바뀐다.