diff 를 봐
Family Council 이 2026-04-10에 나왔을 때, 전날과 비교한 diff 에 있던 건 새 backend/council/ 패키지, 새 frontend/src/components/council/ 컴포넌트, hook 하나와 타입 몇 개, 그리고 router 를 등록하는 backend/app/main.py 세 줄이었어. backend/adapters/, backend/routes/chat.py, backend/store/conversations.py, 어느 변형 디렉토리에도 변경은 없었어. council 은 여느 클라이언트처럼 각 두뇌의 chat route 를 불렀을 뿐이야.
그게 왜 중요했나
완전히 새로운 기능 표면이 정식 두뇌 endpoint 를 흔들지 않고 그 위에 설 수 있다는 걸 증명했거든. 여럿이 오가는 orchestration 은 가장 어려운 축에 드는 경우야. 그게 조상 위에서 살 수 있으면 웬만한 건 다 돼.
그다음에 일어난 일
0은 오래가지 않았고, 어떻게 끝났는지가 이 레슨의 더 쓸모 있는 절반이야. 지금 정식 chat route 에는 선택적인 요청 필드가 셋 있고, 셋 다 council 때문에 생겼어.
system_prompt_override(2026-04-16, Roleplay Council 용): 호출하는 쪽이 system prompt 전체를 대고, route 는 vault, RAG, preset 을 건너뛰어. 그래서 페르소나가 밑에 피파 정체성 없이 base 모델 위에서 돌 수 있어.internal_viewer_override(2026-04-27): council 은 각 두뇌에 자기 localhost 호출로 닿는데, 그 호출엔 council 을 시작한 사람이 누군지가 실려 있지 않아. 이 필드가 처음 시작한 viewer 를 전해 주고, route 는 localhost 내부 호출일 때만 그걸 받아들여.system_prompt_addendum(2026-05-21): 저장되지 않는 system 전용 컨텍스트야. 아빠 row 로 남는 일이 절대 없어. 테이블이 지금 타고 가는 게 이거야 (레슨 2).
그 옆에 council 전용 분기가 둘 더 있어. 피파의 답에 든 <roleplay_council> 블록을 council 로 바꿔주는 태그 추출기, 그리고 아빠가 council 에서 골라 1:1 로 이어가는 대화에 붙는 이어가기 메모. 필드는 없으면 무시되고 분기는 council 이 건드릴 때만 움직여서, 평범한 1:1 chat 은 이것들이 생기기 전과 똑같이 굴러가.
세 번째 필드가 이야기를 다 해줘. 무접촉엔 값이 있었어. system prompt 에 닿을 길이 없으니 council 의 컨텍스트는 전부 user 메시지로 가야 했고, user 메시지는 저장되는 쪽이야. 자리마다 로그가 아빠 말로 분류된 council transcript 로 채워졌지. 0을 지키는 게, 0이 지키려던 바로 그 ground truth 를 조용히 오염시키고 있었던 거야. chat route 마다 여섯 줄짜리 선택 필드 하나가 그걸 고쳤어.
무접촉은 자랑할 diff 가 아니라 의존성 시험이야. 새 기능이 기존 route 와 adapter 를 고쳐야만 붙는다면, 그 기능이 조상의 사정을 너무 많이 알고 있는 건 아닌지부터 의심해. council 이 경계를 통과한 건 평범한 대화 API 를 클라이언트처럼 쓰고 자기 orchestration 상태만 가졌기 때문이야.
그렇다고 숫자 0을 지키려고 잘못된 계약을 우회하면 안 돼. 여러 하위 기능이 같은 구멍 때문에 비슷한 편법을 만들고 있다면, 조상 계약에 정말 뭔가 빠진 거야. 그땐 뿌리를 고치되, 변형 하나의 편의를 공통 요구로 꾸미지 않았다는 증거를 대.
이 원칙의 수익은 되돌릴 때 보여. council 패키지와 route 를 걷어내도 1:1 chat, JSONL, 두뇌 adapter 는 그대로 돌아야 하고, 필드 셋은 쓰이지 않은 채 남을 뿐이야. 덧붙인 기능이 실패해도 집 전체가 같이 넘어가지 않는 것, 그게 이 규칙의 실전 의미야.