# 카르밀라 인터랙티브 노벨 — MVP 구현 계획 > 승인일: 2026-07-03 · 상태: **M0~M6 전체 완료 = MVP 완성** (2026-07-03, feature/mvp-m1-m4) > 실행: `.venv/bin/uvicorn engine.router:app --port 8777` → http://localhost:8777 (웹 리더) > `.venv/bin/python scripts/play.py` (CLI) · `adk web .` → engine (포켓 단독) > LLM: 루트 `.env`의 OPENAI_API_KEY 자동 로딩. 실 LLM으로 회피사다리·심층해금·수렴 E2E 검증 완료 > follow-up: SequentialAgent → google.adk.workflow 마이그레이션 검토 · E06-09 유보(중립) 3안 > 선택지 미노출 · R-약점반응 구조화(현재 raw 주입) · 타 화자(아버지 등) 채팅 포켓 · 세션 영속화 > 원칙: CLAUDE.md 준수 (google-adk 공식 패턴, 서사 진행은 코드가·대사는 LLM이, 스포일러 필터 강제) ## MVP 정의 **E01~E06(챕터1)을 처음부터 끝까지 플레이 가능한 수직 슬라이스.** - 카드 단위 읽기(L0, 캐논 고정) → 선택지/자유채팅 개입 포켓(L1) → 숨은 상태 누적 → 엔딩 3종 수렴(L2) - 엔진: google-adk. 실행: CLI + `adk web`. 리딩 UI는 MVP 이후(M5) - GraphRAG/Neo4j는 MVP 제외 — `kb_query`는 pre_kb 유지, repository 인터페이스만 준비 - `reference/`는 개념 검증 프로토타입. **참고만 하고 코드는 ADK로 새로 구축** (유저 확정) ## 데이터 모델 (A안 아키텍처 — reference v10에서 검증된 개념) | 레이어 | 파일 | 내용 | |---|---|---| | 정체성 (불변) | `content/characters.yaml` | 인물 시트(ENOS), player=laura, 지식경계, kb_query 훅 | | 챕터 메타 | `content/chapters/*.yaml` | entry/exit beats, beat_range, 엔딩 3종 규칙, R카드 등록 | | 내러티브 | `content/beatcards/E0*/*.yaml` | 카드: narration 블록/choices/next/canon/복선 태그/연출 | - **콘텐츠 원천(권위)**: `카르밀라/0701_카르밀라/비트카드/` MD (대표님 소유) - 변환 YAML은 파생물: `scripts/beatcard_convert.py`(개발팀 소유)로 생성 - 숨은 상태 3값 (P0 4종 엔진 명세): **신뢰축**(시소) · **쇠약도**(단조 증가, 신뢰와 독립) · **앎**(유저, 도입 1회 확립 후 불변). 숫자/게이지 유저 비노출 - 엔딩 규칙: `doubt≥30→처단(캐논)` / `trust≥60∧doubt<20→영원` / `default→구원` ## 코드 vs LLM 경계 | 개념 | 구현 | 타입 | |---|---|---| | 카드 진행(next 링크, canon 읽기) | `services/story.py` 상태머신 | 결정적 코드 | | 선택지 → 신뢰축 반영 | 선택 매핑 + state 커밋 | 결정적 코드 | | 자유채팅 개입 포켓 | guard → character → director | ADK 파이프라인 | | R-회피사다리/약점반응 | guard 규칙 + character instruction 주입 | BaseAgent + instruction | | 숨은 상태 | session.state + FunctionTool → 포켓 종료 시 커밋 | ADK state | | 수렴 판정 | director 구조화 출력 + `services/pocket.py` 코드 재검증 | LlmAgent + 코드 | | 엔딩 분기 | ending_rules 조건식 평가 | 결정적 코드 | | 스포일러 방어 | `beat_id ≤ 현재` 필터 + before_model 콜백 | 콜백 | ## 개입 포켓 파이프라인 (escalate 격리 규칙 적용) ``` pocket_session (SequentialAgent) ├── pocket_context_loader ← BaseAgent: 카드 브리프 + 신뢰 레벨 결 로드 ├── turn_loop (LoopAgent max=카드별 상한) │ ├── input_guard ← BaseAgent: 세계관 이탈/정체직격/약점 감지 │ ├── character ← LlmAgent: 카르밀라 (시트+화법6규칙+R카드+신뢰별 결) │ └── director ← LlmAgent: 비트 달성/이탈/신뢰반영/수렴 → escalate └── pocket_closer (LoopAgent max=1) ← 강제 수렴 내레이션 + 상태 커밋 → 다음 카드 복귀 ``` ## 마일스톤 - **M0 — 스캐폴드** (0.5일): git init(로컬만, 원격 푸시는 추후) · engine/ 뼈대 · 의존성 · CLAUDE.md 데이터 모델 섹션 갱신 · 본 문서 - **M1 — 콘텐츠 계약** (1일): 비트카드/챕터/캐릭터 Pydantic 스키마 + `python -m engine.schemas.validate content/` · 변환 스크립트 · next 링크 무결성 검사(고아/끊긴 체인). 검증 기준: 기존 61카드 통과 - **M2 — 결정적 리딩 루프** (1일): entry_beat부터 canon 페이징 · 선택지 신뢰축 반영 · 쇠약도 단조 증가 · **LLM 없이** CLI로 E01~E06 통독 + 엔딩 3종 도달 확인 - **M3 — ADK 개입 포켓** (2~3일, 핵심): 턴 파이프라인 · 카르밀라 instruction · director 구조화 판정(FunctionTool→state) · guard · `adk web`으로 포켓 1개(E06-09 권장) 집중 검증 - **M4 — 전체 관통** (1~2일): 리딩 루프 ↔ 포켓 연결 · E01→엔딩 전체 플레이 · 턴 로깅 플러그인 · 시연(CLI + adk web) - **M5 — MVP 이후**: FastAPI router + 리딩 UI · GraphRAG(kb_query 활성화) · 평가 하네스 · style_critic 루프 ## 폴더 구조 ``` sorac/ ├── engine/ │ ├── agent.py / app.py / model.py / router.py │ ├── schemas/ beatcard.py · chapter.py · character.py · validate.py │ ├── services/ story.py · pocket.py · state.py · retrieval.py(2단계 스텁) │ ├── repositories/ content.py · playlog.py │ ├── agents/ input_guard.py · pocket_context_loader.py │ ├── sub_agents/ character/ · director/ │ ├── tools/ state/ (update_trust.py, mark_beat.py) · retrieval/(스텁) │ ├── callbacks/ · plugins/logging.py · instructions/ ├── content/ beatcards/ · chapters/ · characters.yaml (M1에서 적재) ├── scripts/ beatcard_convert.py ├── tests/ └── docs/plans/mvp.md (본 문서) ``` ## 결정 로그 - 데모 프론트: 엔진 우선, UI 최소 (유저 확정) - E01_구현계획.drawio 무시 (유저 지시) — 씬 설계 근거로 쓰지 않음 - 비트카드 MD가 콘텐츠 권위, reference 내 YAML은 파생물 (승인) - CLAUDE.md "YAML 4종" 섹션 → A안 3레이어로 갱신 (승인) - E07~E15: MD만 존재, MVP 범위 밖