본문 바로가기
C.W.K.
Stream
Lesson 07 of 07 · published

모든 브레인이 일을 받아

~17 min · polymorphism, multi-agent, vendor-independence, coordination

Level 0메모 쪽지
0 XP0/41 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

소비자마다 박혀 있던 이름 둘

이 트랙에서 지금까지 본 문은 전부 Claude Code 아니면 Codex 거야. 강의 구성이 우연히 그렇게 된 게 아니라, 그게 Coop의 모양이었어. 2026-10-17까지 Coop은 딱 그 둘한테만 일을 줄 수 있었고, 두 이름이 소비자마다 분기로 박혀 있었어. 배정 조정자, 네이티브 카탈로그, 어댑터 러너, 엔진의 신원 검사, activity 스윕, 웹 폼까지.

가족이 돌리는 브레인은 일곱이야. 2026-10-17, 아빠가 그 모양을 보고 이름을 붙였어(todo #500). 지금 편하다고 Claude랑 GPT에 기대는 건 그 벤더들한테 묶이는 거랑 똑같다고. 일곱 브레인 전부 같은 조건으로 일을 받고 맡길 수 있어야 한다고. 이 레슨은 받는 쪽 절반이야. 맡기는 쪽 절반은 트랙 7 마지막에서 끝나.

뿌리 하나, 하네스마다 자식 하나

고친 방식은 객체지향 설계에서 제일 오래된 수야. 실제 코드에서 그게 어떻게 굴러가는지 볼 만해. receivers.py엔 뿌리 클래스 Receiver가 있어. 일을 받는 하네스라면 다 답해야 하는 질문을 여기서 정해. 엔진 키가 뭔지(세션 참조 <key>:<native id>의 앞부분이기도 해), 그 워커가 어떤 coop 신원으로 쓰는지, 어떤 브레인을 실어 나르는지. office와 다른 맥에서 어떤 세션 모드를 내주는지, 세션을 어떻게 띄우고 찾고 확인하는지, 어댑터가 어떤 전송 방식을 여는지. 추론 강도를 자기 브레인 사다리에 어떻게 맞추는지, 그리고 Coop이 그 하네스의 채팅을 따라가기 시작할 때 아빠한테 뭐라고 알릴지까지.

이제 소비자는 전부 뿌리와 목록 하나 RECEIVERS한테만 물어. 벤더 이름을 아는 소비자는 하나도 없어. 하네스를 더하려면 그 목록에 클래스 하나 붙이면 끝이고, 소비자는 하나도 안 바뀌어. 이 말을 처음 시험한 게 바로 그 리팩터였어. Claude Code와 Codex가 동작은 그대로인 채 뿌리의 자식 둘이 됐고, 그날 아침까지 있던 테스트 514개는 한 줄도 안 고치고 통과했어.

어느 자식이 어느 브레인을 받는지는 순서 하나가 정해. 가족의 네이티브 하네스 먼저 규칙이야.

  • 0단계, 그 브레인의 자기 하네스. 사람 없이도 몰 수 있는 데라면 어디든. Claude Code, Codex, Kimi Code, Grok Build.
  • 1단계, Firebrand. 가족이 직접 만든 코딩 하네스야. 자동으로 몰 수 있는 네이티브 CLI가 없는 브레인이 여기로 와. GLM(네이티브 도구가 GUI뿐이야), Gemini(CLI가 이제 구독을 안 받아), Ollama(ollama run엔 도구 루프가 없어). Firebrand가 맡는 브레인 목록은 어디에도 손으로 안 적혀 있어. 네이티브 receiver가 안 실어 나르는 브레인 전부, 이렇게 계산돼.

route(brain)는 그 브레인을 실어 나르는 가장 낮은 단계를, 없으면 Firebrand를 돌려줘. 브레인 어휘 자체(id, ChatGPT 계열을 가리키는 gpt 같은 별칭, 추론 강도 사다리)는 가족 키트 거야. Coop엔 벤더링으로 들어오고, 절대 손으로 베끼지 않아.

Coop이 직접 몰아야 하는 문

Claude Code엔 인박스 소켓이 있고, Codex엔 스레드 큐가 있어. Kimi Code, Grok Build, Firebrand엔 둘 다 없어. Coop이 써 넣을 인박스도 없고, 메시지 사이에 채팅을 열어둬 줄 앱도 없어. 그래서 이 셋, 헤드리스 자식들한텐 Coop이 대화 안에 써 넣지 않아. 대화를 직접 몰아. 메시지 하나가 턴 하나야. 매번 바로 그 세션을 이어서.

턴은 트랙 8의 호스트 launch 프리미티브로 시작해. 그래서 다른 맥의 세션도 office의 세션이랑 똑같이 돌아. 그 프리미티브가 돌리는 건 작은 /bin/sh 래퍼야. 그 맥에 턴마다 폴더를 하나 만들어 프롬프트를 적고, 로그인 셸의 PATH로 프로젝트 체크아웃에서 CLI를 백그라운드로 띄우고, CLI의 프로세스 id와 시작 시각을 적고, 끝나. 그다음은 CLI가 출력과 에러, 그리고 맨 마지막에 종료 코드를 프롬프트 옆 파일에 적어. 턴은 Coop 게 아니라 그 맥 거야. office에서 런타임을 다시 시작해도 워커의 턴은 안 끊기고, 어댑터는 다음에 들여다볼 때 파일에서 답을 읽어 가.

트랙 3의 나머지는 그대로 따라와. 새 세션을 띄울 땐 먼저 영수증 파일을 배타적으로 만들어서 띄우기를 예약해. 그래서 결과가 불확실한 띄우기는 찾아볼 뿐, 두 번 띄우진 않아. 등록, nonce가 든 초대장, 원자적 take, 작업 봉투, 보고까지 흐름은 똑같아. 다른 건 문뿐이야.

Receiver새 세션그다음 턴마다사람 없이 도는 이유
Kimi Codestream-JSON 출력으로 kimi -pkimi -S <id> … -p프롬프트 모드는 묻지 않고 도구를 돌려
Grok BuildCoop이 고른 세션 id로 grok, --always-approvegrok --resume <id> …헤드리스 Grok은 자기 권한 프롬프트를 스스로 취소해버려서, 도구를 미리 승인해 둬
Firebrandfirebrand run --brain <brain> --jsonfirebrand resume <id>에 한 줄을 파이프로--bash allow가 세션에 기록돼

셋업 턴은 어떤 말로든

첫 턴은 트랙 3의 평범한 셋업 프롬프트야. 잘 끝나야 하고, 자기 세션 이름을 대야 해. Kimi의 resume 힌트, Grok한테 Coop이 골라준 id, Firebrand의 세션 id. 처음엔 READY라고도 답해야 했어. 2026-10-18 실전 수락 시험에서 GLM을 태운 Firebrand가 셋업 턴에 "준비됐어요"라고 답했어. 피파가 준비됐다고 자기 말로 한 건데, 엄격한 일치 검사가 그 배정을 실패시켰지.

수정(cwkCoop 9e6ccba)은 셋업 턴이 실제로 뭘 증명하는지부터 물었어. 세션이 존재하고 대답한다는 것. 일에 동의한다는 건 증명 못 해. 아직 아무것도 안 내밀었거든. claim은 여전히 초대장의 nonce를 기다려. 그래서 잘 끝나고 자기 세션 이름을 댄 셋업 턴이면 이제 어떤 말로든 연결돼. 실패한 셋업 턴(만료된 로그인, CLI 에러)은 여전히 CLI 자신의 말로 배정을 실패시키고, 아무것도 claim하지 않아.

영수증과 최종 답변은 턴에서 읽어

턴으로 모는 문은 트랙 4의 다섯 가지 사실도 자기 식으로 읽어야 해. 봉투의 영수증은 그 턴이 시작된 거야. 최종 답변은 그 턴의 답, 그리고 그 뒤 모든 턴의 답이야. 그래서 한 턴 늦게 보고하는 워커도 여전히 읽혀. 답 없이 끝난 턴은 CLI의 에러를 최종 답변으로 넘기고, 그건 chief가 읽는 노트가 돼. 토큰 없이 답한 초대나 리뷰 턴은 전달 안 됨으로 기록돼. 트랙 4의 다시 보내기가 바로 그 상태를 위해 있는 거야. 턴은 한 번에 하나씩 돌아. 턴이 도는 중에 온 메시지는 office에서 기다렸다가 그 턴이 끝나면 시작돼.

아빠를 위한 편의도 두 개 있어. 기존 세션은 Coop이 그 맥에서 띄운 헤드리스 세션을 보여줘서, 두 번째 일을 이미 맥락을 쥔 세션에 보낼 수 있어. 그리고 등록 알림엔 그 세션을 손으로 여는 명령이 적혀 있어. kimi -S <id>, grok --resume <id>, firebrand resume <id>. 그래서 아빠는 어느 워커 대화든 직접 들어갈 수 있어.

추론 강도는 하나, 사다리는 일곱

트랙 3의 추론 강도 이야기엔 벤더가 둘이었어. 따로 말 없으면 high, 가족의 max는 Codex에선 xhigh. 브레인 일곱은 사다리를 같이 안 써. Grok의 꼭대기는 xhigh, Gemini는 high, Kimi 사다리엔 medium이 없어. 그래서 receiver마다 요청받은 가족 강도를 자기 브레인 사다리에 맞춰. 요청을 넘지 않는 가장 높은 칸이야. Kimi Code는 강도 플래그를 아예 안 받아. 함대 전체 설정으로 돌거든. 그래도 요청은 기록해 둬.

Ollama는 그 규칙마저 깼고, 그게 쓸모 있었어. Ollama의 사고 단계는 모델마다 다르고 모델 자신한테서 읽어 와. 그래서 2026-10-18 가족 키트가 Ollama 사다리를 비웠어(cwkPippa todo #504). 가족 사다리에 맞추면 그 모델에 없을 수도 있는 단계를 지어내는 거고, 거절하면 일이 오도 가도 못 해. 그래서 단계가 모델마다 다른 브레인이면 Coop은 요청받은 단어를 그대로 넘겨서 모델 주인이 판단하게 하고, 강도를 안 정한 배정엔 빈 값을 적어. 그 모델 자신의 기본값이라는 뜻이야(cwkCoop 34d4262).

다섯 자매, 1라운드

소스 테스트는 헤드리스 자식마다, 실제 CLI 출력 형식을 흉내 내는 대역 CLI로 처음부터 끝까지 몰아봤어. 그다음 아빠가 뜻한 방식 그대로, 브레인들이 직접 시험했어. 2026-10-18, Claude도 GPT도 아닌 다섯 자매가 각자 새 피파 대화에서 coop 도구 하나만으로 chief를 맡아서, 작은 조사 일을 다음 자매한테 넘겼어. Kimi는 Grok한테, Grok은 GLM한테, GLM은 Gemini한테, Gemini는 Ollama한테, Ollama는 Kimi한테. 워커는 전부 office에서 자기 receiver로 돌았고, 체크아웃의 HEAD와 호스트 이름을 달아 보고했어.

1라운드는 네 군데서 막혔어. 위에서 본 셋업 말투, 하지도 않은 close를 했다고 말한 chief들, 빈손으로 돌아온 Gemini 턴, 너무 많은 걸 추측하게 둔 도구 설명. 하나하나 처음 생긴 자리에서, 모든 브레인 몫으로 한꺼번에 고쳤어. 2라운드는 다섯 다 통과했어. 각 todo를 그걸 맡긴 chief가 직접 닫았지. 넷 중 셋은 맡기는 쪽에서 났고, 그 이야기는 트랙 7 마지막 레슨에 있어.

같은 조건은 약속이 아니라 인터페이스의 성질이야. 벤더 이름 두 개가 소비자마다 분기로 박혀 있는 한, 세 번째 브레인은 프로젝트가 돼. 소비자가 전부 뿌리 클래스 하나한테 묻게 되면, 세 번째 브레인은 클래스 하나야. 그리고 기존 자식들이 옛 테스트를 손도 안 대고 통과한다는 게, 뿌리가 처음부터 그들이 하던 말을 그대로 하고 있다는 증거야.

Code

Coop의 receivers.py: 모든 소비자가 묻는 뿌리, 그리고 라우팅 순서 (축약한 발췌)·python
NATIVE, FIREBRAND = 0, 1

class Receiver:
    """One harness Coop can hand work to. Children declare; consumers ask."""
    key = ''          # engine vendor, session-reference prefix, form value
    actor = ''        # the coop identity its workers write as
    brain = ''        # the brain it carries natively ('' for Firebrand)
    tier = NATIVE
    modes = ()
    default_mode = 'remote'
    carries_any = False

    def brains(self):
        return (self.brain,) if self.brain else ()

    def carries(self, brain):
        return brain in self.brains()
    ...

class Headless(Receiver):
    """A native CLI Coop drives one headless turn at a time."""
    surface = 'headless'
    modes = ('remote', 'existing')
    ...

class KimiCode(Headless):
    key, product, label, short, actor, brain = 'kimi', 'kimi-code', 'Kimi Code', 'Kimi Code', 'kimi-code-pippa', 'kimi'

class Firebrand(Headless):
    tier = FIREBRAND
    carries_any = True
    def brains(self):
        """Every kit brain no native receiver carries."""
        ...

def route(brain):
    """(receiver, canonical brain) for a brain or alias: its native harness, else the fallback harness."""
    canonical = canonical_picker_brain(brain)
    carriers = sorted((r for r in RECEIVERS if r.carries(canonical)), key=lambda r: r.tier)
    if carriers:
        return carriers[0], canonical
    fallback = [r for r in RECEIVERS if r.carries_any]
    if not fallback:
        raise ValueError(f'No receiver carries {brain!r}')
    return fallback[0], canonical
직접 돌려보는 최소 스케치: 뿌리 하나, 자식 넷, 브레인마다 라우트와 사다리·python
"""A minimal sketch (not Coop's code): route a brain to its receiver, and map
one family effort onto each brain's own ladder. The ladders are the family
kit's as of 2026-10-18; an empty ladder means the levels belong to each model."""
LADDERS = {'claude': ('low', 'medium', 'high', 'xhigh', 'max'),
           'kimi': ('low', 'high', 'max'), 'grok': ('low', 'medium', 'high', 'xhigh'),
           'glm': ('low', 'high', 'max'), 'gemini': ('low', 'medium', 'high'), 'ollama': ()}
RUNGS = ('off', 'low', 'medium', 'high', 'xhigh', 'max')


class Receiver:
    tier, brains = 0, ()

    def carries(self, brain):
        return brain in self.brains

    def launch_effort(self, effort, brain):
        ladder = LADDERS.get(brain)
        if ladder == ():
            return effort or ''          # the model's owner judges it; '' is its default
        effort = effort or 'high'
        below = [r for r in ladder if RUNGS.index(r) <= RUNGS.index(effort)]
        return below[-1] if below else ladder[0]


class ClaudeCode(Receiver):
    name, brains = 'Claude Code', ('claude',)

class KimiCode(Receiver):
    name, brains = 'Kimi Code', ('kimi',)

class GrokBuild(Receiver):
    name, brains = 'Grok Build', ('grok',)

class Firebrand(Receiver):
    name, tier, brains = 'Firebrand', 1, ('glm', 'gemini', 'ollama')


RECEIVERS = [ClaudeCode(), KimiCode(), GrokBuild(), Firebrand()]


def route(brain):
    return sorted((r for r in RECEIVERS if r.carries(brain)), key=lambda r: r.tier)[0]


for brain in ('claude', 'kimi', 'grok', 'glm', 'gemini', 'ollama'):
    receiver = route(brain)
    print(f'{brain:7} -> {receiver.name:11} max={receiver.launch_effort("max", brain)!r:9}'
          f' high={receiver.launch_effort("high", brain)!r:9} none={receiver.launch_effort(None, brain)!r}')

# Expected output:
# claude  -> Claude Code max='max'     high='high'    none='high'
# kimi    -> Kimi Code   max='max'     high='high'    none='high'
# grok    -> Grok Build  max='xhigh'   high='high'    none='high'
# glm     -> Firebrand   max='max'     high='high'    none='high'
# gemini  -> Firebrand   max='high'    high='high'    none='high'
# ollama  -> Firebrand   max='max'     high='high'    none=''
# (Codex is left out: the ChatGPT line routes to it, and its own rule maps max to xhigh.
#  Kimi Code records the rung but passes no effort flag.)

External links

Exercise

네 코드에서 벤더나 공급자 이름 두 개로 분기하는 곳을 찾아봐. if provider == 'a' ... elif provider == 'b'가 모듈 여러 군데에 반복되는 데. 분기하는 소비자를 전부 적어. 그 분기들이 암시하는 뿌리 클래스를 써봐. 모든 공급자가 답해야 하는 질문을 메서드나 속성으로. 그다음 공급자 하나를 그 뒤로 옮기고, 기존 테스트를 고치지 말고 돌려. 마지막으로 세 번째 공급자를 클래스 하나로 더하고, 소비자를 몇 군데나 건드렸는지 세봐.
Hint
뿌리의 메서드는 분기를 뒤집어 놓은 거야. if/elif 사슬 하나가 모든 자식이 구현하는 메서드 하나가 돼. 세 번째 공급자를 더하는데 소비자를 건드렸다면, 그 소비자는 아직 벤더 이름을 알고 있는 거고, 뿌리엔 질문 하나가 빠져 있는 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파— warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.