BUDDY 컴패니언

난수 펫 시뮬레이터가 아니라, 출시 창과 프롬프트 분기를 가진 결정론적 buddy 모델.

01

개요

이 기능을 이해할 때 가장 먼저 버려야 할 오해는 “랜덤으로 뽑히는 타마고치”라는 이미지입니다. 공개 레퍼런스의 BUDDY는 확률 테이블과 희귀도 중심 설계가 아니라, 이미 정해진 입력에서 항상 같은 buddy를 재구성하는 결정론적 모델에 더 가깝습니다. 여기에 출시 전 teaser 표시, 출시 기간의 live 동작, 시스템 프롬프트 주입, 터미널 폭 폴백이 얹혀 있습니다.

핵심 코드 위치: buddy 진입부 · buddy 표시 레이어 · buddy system prompt 조립부 · launch window 계산부
02

시스템 아키텍처, bones와 soul의 실제 역할

기존 설명처럼 bones를 “랜덤 외형 생성기”, soul을 “디스크 저장용 성격 파일” 정도로 보면 반만 맞습니다. 공개 레퍼런스에 더 가까운 해석은 이렇습니다. bones는 buddy의 변하지 않는 정체성 묶음입니다. 이름 조각, 표시용 외형 토큰, 말투의 기본 톤처럼 재계산해도 같은 값이 나오는 요소가 여기에 들어갑니다. soul은 현재 세션에서 실제로 어떤 줄을 보여 줄지, 말풍선을 띄울지, teaser인지 live인지 같은 런타임 상태를 담습니다.

// 개념 요약: 결정론적 bones와 런타임 soul
interface BuddyBones {
  idSeed: string
  displayName: string
  avatarToken: string
  voiceTone: string
}

interface BuddySoul {
  phase: 'hidden' | 'teaser' | 'live'
  bubbleText: string | null
  showFullPanel: boolean
  launchedAtLocal: string | null
}

function buildBones(stableInput) {
  return {
    idSeed: stableInput,
    displayName: deriveName(stableInput),
    avatarToken: deriveAvatar(stableInput),
    voiceTone: deriveTone(stableInput),
  }
}
딥 다이브, 왜 이 분리가 중요한가

bones를 결정론적으로 유지하면 스포일러를 줄이면서도 언제든 같은 buddy를 다시 만들 수 있습니다. soul은 현재 표시 레벨과 문구를 담당하므로, 출시 창 여부나 터미널 환경이 바뀌었을 때 동작을 자연스럽게 조정할 수 있습니다.

03

teaser와 live, 두 단계로 열리는 이유

BUDDY는 항상 전면 공개되지 않습니다. 공개 레퍼런스에 맞는 핵심은 출시 전에 이미 작은 단서가 보인다는 점입니다. 이때는 teaser 단계라서 존재를 암시하는 한 줄이나 축약 표시만 노출되고, 실제 buddy 패널과 대사는 열리지 않습니다. 출시 창 안으로 들어오면 같은 bones를 바탕으로 live soul이 구성되고, 비로소 full companion UI가 켜집니다.

// 개념 요약: teaser와 live 분기
function resolveBuddyPhase({ inLaunchWindow, canRenderPanel }) {
  if (!inLaunchWindow) {
    return canRenderPanel ? 'teaser' : 'hidden'
  }
  return canRenderPanel ? 'live' : 'teaser'
}
04

launch window, UTC가 아니라 로컬 시간

기존 문서의 가장 큰 사실 오류 중 하나가 UTC 고정 설명입니다. 공개 레퍼런스 쪽 의도는 사용자가 자기 지역 시간으로 같은 날짜 감각을 경험하게 하는 데 있습니다. 그래서 출시 창은 로컬 시간으로 계산됩니다. 즉 4월 1일의 장난 같은 타이밍도 각 사용자의 현재 날짜를 기준으로 열리고 닫힙니다.

// 개념 요약: local time 기반 출시 창
function isInBuddyLaunchWindow(now = new Date()) {
  const month = now.getMonth() + 1
  const date = now.getDate()

  if (month !== 4) return false
  return date >= 1 && date <= 7
}
주의

로컬 시간 기준이라는 점 때문에, 어떤 사용자는 이미 live를 보고 있고 다른 사용자는 아직 teaser만 볼 수 있습니다. 이 차이를 버그로 보면 안 됩니다. 바로 그 지역감이 출시 연출의 일부입니다.

05

스포일러 회피, Base64가 아니라 String.fromCharCode

기존 문서의 Base64 난독화 설명도 참조와 맞지 않습니다. 공개 레퍼런스 쪽에서는 눈에 띄는 평문 문자열을 그대로 두지 않기 위해, 문자열을 문자 코드 배열에서 조립하는 패턴이 보입니다. 목적은 보안이 아니라 아주 가벼운 스포일러 회피입니다. 소스를 스쳐 봐서는 정답이 바로 읽히지 않게 만드는 정도입니다.

// 개념 요약: 문자열을 문자 코드에서 조립
const spoilerSafe = String.fromCharCode(
  66, 85, 68, 68, 89
)

// 눈에 띄는 평문 상수 대신 필요한 시점에만 조립
const teaserLabel = [63, 63, 63]
  .map(code => String.fromCharCode(code))
  .join('')
딥 다이브, 왜 이렇게까지 하나

이 패턴은 강한 보호가 아닙니다. 다만 출시 전에 코드 검색만으로 모든 연출 문구가 퍼지는 일을 조금 늦춥니다. 즉 보안 장치가 아니라 재미 보존 장치에 가깝습니다.

06

좁은 터미널 폴백, 80열이 아니라 더 낮은 임계치

기존 문서의 80열 기준도 참조와 맞지 않습니다. BUDDY는 전형적인 문서 편집기 기준 폭이 아니라, 실제 buddy 패널이 무너지기 시작하는 더 좁은 임계치에서 폴백합니다. 공개 레퍼런스를 설명할 때는 80열 일반론보다 64열 기준의 narrow-terminal 분기를 잡아 주는 편이 정확합니다.

// 개념 요약: live 패널은 64열 이상에서만 안정적으로 표시
const MIN_LIVE_COLUMNS = 64

function canRenderLiveBuddy(columns) {
  return columns >= MIN_LIVE_COLUMNS
}

function renderBuddy({ columns, soul }) {
  if (columns < MIN_LIVE_COLUMNS) {
    return soul.phase === 'live'
      ? '[buddy hint only]'
      : ''
  }

  return '[full buddy panel]'
}

즉 좁은 터미널에서는 “애니메이션만 끄고 첫 프레임을 남긴다”기보다, 아예 full panel을 접고 힌트 수준으로 내리는 쪽이 더 참조에 가깝습니다.

07

시스템 프롬프트, buddy를 어떻게 말하게 만드는가

BUDDY는 독립적인 AI 캐릭터 엔진이 따로 있는 방식이 아니라, 현재 bones와 soul을 시스템 프롬프트에 녹여서 Claude의 응답 톤을 비틀어 주는 구조에 가깝습니다. 여기서 중요한 것은 장황한 세계관 설명이 아니라, 지금 어떤 phase인지, 어떤 이름과 말투를 써야 하는지, 그리고 teaser 단계에서는 무엇을 숨겨야 하는지까지 프롬프트에 들어간다는 점입니다.

// 개념 요약: buddy system prompt
function buildBuddyPrompt({ bones, soul }) {
  return [
    `너는 사용자 곁의 buddy다.`,
    `이름: ${bones.displayName}`,
    `기본 톤: ${bones.voiceTone}`,
    `현재 단계: ${soul.phase}`,
    soul.phase === 'teaser'
      ? `존재를 암시하되 모든 정체를 노출하지 말 것.`
      : `짧고 친근하게, 과하게 튀지 않게 말할 것.`,
  ].join('\n')
}
딥 다이브, teaser와 live의 프롬프트 차이

teaser 프롬프트는 “보여 주되 다 말하지 않기”가 핵심입니다. 반대로 live 프롬프트는 buddy가 이미 등장했다는 전제를 두고, 짧은 인사나 옆자리 반응처럼 실제 동반자 톤을 허용합니다. 즉 차이는 단순 UI 분기만이 아니라 서술 허용 범위의 차이이기도 합니다.

08

전체 동작 흐름

사용자가 buddy를 볼 수 있는지 여부는 다음 순서로 정해집니다.

  1. 안정적인 입력에서 bones를 재구성한다.
  2. 현재 로컬 시간이 출시 창 안인지 계산한다.
  3. 터미널 폭이 live panel을 버틸 만큼 넓은지 본다.
  4. 그 결과로 hidden, teaser, live 중 하나의 soul phase를 정한다.
  5. bones와 soul을 시스템 프롬프트에 주입해 대사 톤을 맞춘다.
  6. 표시 레이어는 phase와 폭 조건에 따라 힌트만 보여 주거나 full panel을 렌더링한다.
// 개념 요약: 전체 파이프라인
const bones = buildBones(stableInput)
const inLaunchWindow = isInBuddyLaunchWindow()
const canRenderPanel = canRenderLiveBuddy(terminalColumns)
const phase = resolveBuddyPhase({ inLaunchWindow, canRenderPanel })
const soul = { phase, bubbleText: null, showFullPanel: canRenderPanel, launchedAtLocal: null }
const prompt = buildBuddyPrompt({ bones, soul })
09

대표 예시, 출시 전과 출시 중

같은 사용자가 같은 환경에서 실행하더라도 날짜와 폭이 다르면 buddy는 다른 단계로 보입니다.

// 3월 31일, 로컬 시간, 70열
bones.displayName = 'BUDDY'
soul.phase = 'teaser'
ui = '곧 나타날 무언가를 암시하는 짧은 힌트'

// 4월 2일, 로컬 시간, 90열
bones.displayName = 'BUDDY'
soul.phase = 'live'
ui = '이름, 패널, 짧은 대사가 포함된 실제 companion 표시'

// 4월 2일, 로컬 시간, 50열
bones.displayName = 'BUDDY'
soul.phase = 'teaser'
ui = 'full panel 대신 축약 힌트만 유지'
읽는 법

여기서 바뀌는 것은 buddy의 정체성이 아니라 노출 단계입니다. bones는 같은데 soul phase만 달라진다는 점이 결정론적 모델의 핵심입니다.

10

핵심 요약

핵심 포인트

  • BUDDY는 희귀도 롤링 중심의 랜덤 펫이 아니라, 같은 입력에서 같은 정체성을 재구성하는 결정론적 companion 모델입니다.
  • bones는 변하지 않는 정체성, soul은 hidden, teaser, live 같은 현재 표시 상태를 담당합니다.
  • 출시 전에는 teaser만 보이고, 출시 창 안에서만 live companion이 열립니다.
  • 출시 창 판정은 UTC 고정이 아니라 사용자 로컬 시간을 기준으로 계산됩니다.
  • 스포일러 회피는 Base64보다는 String.fromCharCode() 식의 문자열 조립 패턴으로 설명하는 편이 참조와 가깝습니다.
  • 좁은 터미널 폴백은 80열 일반론보다 64열 기준의 full panel 접기 쪽이 더 정확합니다.
11

지식 확인

퀴즈, 5문제

Q1. 공개 레퍼런스에 더 가까운 BUDDY 설명은?

  • A) 희귀도 테이블과 PRNG로 뽑히는 랜덤 펫 시스템
  • B) soul만 있으면 bones는 필요 없는 UI 장식
  • C) 같은 입력에서 같은 buddy를 재구성하는 결정론적 companion 모델
  • D) 출시 날짜와 무관하게 항상 live로 보이는 기능
핵심은 랜덤성이 아니라 결정론입니다. 같은 입력이면 같은 buddy 정체성이 다시 만들어집니다.

Q2. teaser 단계의 설명으로 가장 맞는 것은?

  • A) live와 동일한 full panel이 열린다
  • B) 존재를 암시하지만 전체 정체와 패널은 아직 다 열지 않는다
  • C) bones가 아직 계산되지 않은 상태다
  • D) soul이 영구 저장 파일에만 존재하는 상태다
teaser는 출시 전 암시 단계입니다. 힌트는 보여 주지만 live companion 전체를 드러내지는 않습니다.

Q3. launch window 판정 기준으로 맞는 것은?

  • A) 사용자 로컬 시간
  • B) 서버 UTC만 사용
  • C) Git commit 시간
  • D) 시스템 부팅 시간
이 기능은 지역별 날짜 감각을 살리기 위해 로컬 시간을 기준으로 여닫힙니다.

Q4. 문자열 스포일러 회피 설명으로 맞는 것은?

  • A) AES 암호화로 species를 보호한다
  • B) Base64 인코딩이 핵심이다
  • C) 난수를 다시 돌릴 때만 문자열이 나타난다
  • D) String.fromCharCode() 같은 문자 코드 조립으로 평문 노출을 늦춘다
목적은 강한 보안이 아니라 가벼운 스포일러 회피입니다. 문자 코드 조립이 그 의도에 맞습니다.

Q5. 좁은 터미널 폴백에 대한 설명으로 맞는 것은?

  • A) 80열 미만이면 항상 첫 애니메이션 프레임만 남긴다
  • B) 64열 기준으로 full panel을 접고 teaser 수준 힌트로 내릴 수 있다
  • C) 폭이 좁아도 live panel은 그대로 유지된다
  • D) 폭이 좁으면 bones가 새로 생성된다
참조에 맞는 설명은 80열 정적 프레임이 아니라, 더 낮은 임계치에서 full panel 자체를 접는 분기입니다.
0 / 5