Buzz 워크플로우는 액션이 일곱 개인데 셋은 로그만 남기고 성공한다
리뷰 영상 네 편을 봤습니다. 네 편 다 설치부터 시연까지 보여주고, 네 편 다 된다고 말합니다. 그런데도 코드를 열었습니다.
굳이 연 이유는 네 편의 순서가 전부 똑같았기 때문입니다. "된다"를 보여주고 나서 "그런데 이건 아직"을 덧붙이는데, 그 "이건 아직"이 네 편 모두 같은 기능에 걸려 있었습니다. 한 편이 걸리면 그 사람 환경 문제입니다. 네 편이 같은 자리에서 걸리면 화면 문제가 아니라 그 아래 문제입니다.
리뷰 영상은 되는 걸 보여주려고 만듭니다. 안 되면 다시 찍거나 그 대목을 잘라냅니다. 그 편향을 뚫고 네 편에 다 남아 있다면, 다시 찍어도 안 되고 잘라내면 흐름이 끊기는 자리라는 뜻입니다.
crates/buzz-workflow/src/executor.rs 532번째 줄에 이렇게 적혀 있었습니다.
/// For MVP, most actions log their intent and return a success output.
/// Real event emission is wired in WF-07/08 (relay integration).
대부분의 액션이 무엇을 하려 했는지를 로그로 남기고 성공 출력을 반환한다고, 코드가 자기 입으로 적어놓았습니다.

한 줄로 설명이 안 되는 물건입니다
Buzz 는 Block 이 공개한 오픈소스입니다. Apache 2.0 이라 가져다 고쳐 써도 됩니다. 그런데 이게 뭘 하는 물건인지가 한 줄로 안 잡힙니다. 채팅앱이라고 하기엔 git 저장소를 품고 있고, 개발 도구라고 하기엔 채널과 DM 이 있습니다. 공식 설명은 사람과 에이전트가 같이 일하는 작업 공간이라고 되어 있습니다.
구조는 세 덩어리로 보면 정리가 됩니다. relay 와 agent 와 community 입니다.
relay 는 Rust 로 짠 서버인데, 메시지든 반응이든 git 이벤트든 전부 여기 하나의 로그로 쌓입니다. 사람도 에이전트도 각자 키를 가지고, 남기는 흔적마다 서명이 붙습니다. 그래서 누가 뭘 밀었는지가 서버 관리자도 못 고치는 형태로 남습니다. 이름은 Nostr 프로토콜을 쓰지만 서로 이야기를 주고받는 P2P 는 아닙니다. relay 하나가 읽기와 쓰기의 단독 권위입니다. 탈중앙 쪽을 기대하고 들어오면 여기서 어긋납니다.
기록을 못 고치게 만드는 데 공을 많이 들인 구조입니다. 사람과 에이전트가 섞여서 일하면 나중에 "누가 이걸 시켰나"를 따질 일이 생기는데, 서명과 단일 로그는 그 질문에 답하려고 있는 장치입니다. 이 대목은 뒤에서 한 번 더 나옵니다.
agent 는 ACP 라는 공용 규약으로 붙습니다. 특정 모델에 묶여 있지 않아서 Claude Code 도 Codex 도 Block 이 만든 Goose 도 같은 방식으로 들어옵니다.
제가 이걸 본 이유가 여기 있었습니다. 지금 Claude Code 와 Codex 를 따로 띄워놓고 쓰는데, 한쪽에서 한 이야기를 다른 쪽이 모릅니다. 결과를 복사해서 옮기고, 옮긴 조각만 가지고 다음 작업을 시킵니다.
옮기는 게 번거로워서가 아닙니다. 제가 옮길 조각을 고르는 순간 나머지가 사라진다는 게 문제입니다. 한쪽이 왜 그 결론에 도달했는지, 중간에 무엇을 버렸는지는 안 넘어갑니다. 그래서 다른 쪽이 이미 기각된 방향을 다시 제안하는 일이 생깁니다. 컨텍스트가 제 손을 거쳐서만 건너가고, 그 손이 병목이자 필터입니다. 이걸 한 채널에 모아준다는 말이라면 제 작업 흐름에 바로 걸리는 이야기라서 도입을 검토했습니다.
액션은 일곱 개입니다
자동화는 워크플로우 엔진이 맡습니다. 화면에서 클릭으로 만드는 게 아니라 YAML 로 씁니다. 메시지가 올라왔을 때, 누가 이모지를 달았을 때, 정해진 시각에, 외부에서 webhook 을 쐈을 때. 이렇게 네 가지로 발동합니다.
발동하면 할 수 있는 일이 일곱 가지입니다. schema.rs 의 ActionDef 에 정확히 일곱 개가 정의되어 있습니다.
SendMessage · SendDm · SetChannelTopic · AddReaction · CallWebhook · RequestApproval · Delay
일곱 개를 성격으로 갈라보면 넷과 셋으로 나뉩니다. SendMessage · SendDm · SetChannelTopic · AddReaction 은 사람이 볼 화면에 무언가를 남기는 액션이고, CallWebhook · RequestApproval · Delay 는 흐름을 바깥으로 잇거나 붙잡아두는 액션입니다. 앞의 넷은 결과를 알리는 쪽이고 뒤의 셋은 진행을 통제하는 쪽입니다.
변수도 씁니다. 발동시킨 메시지의 본문이나 작성자, 앞 단계의 출력을 그대로 끌어다 다음 단계에 넣을 수 있습니다. 단계가 이어진다는 게 이 엔진의 핵심입니다. 하나짜리 자동화라면 굳이 엔진이 필요 없습니다. 여기까지만 보면 웬만한 자동화는 짤 수 있게 생겼습니다.
방어 설계를 보고는 꽤 잘 짜놨다고 생각했습니다. 조건식을 평가할 때 EVAL_TIMEOUT 이 100밀리초로 걸려 있어서 이상한 식으로 relay 를 붙잡아둘 수 없습니다. 조건식 길이도 MAX_EXPR_LEN 4,096자로 잘라놨습니다. 외부 webhook 을 부르면 응답을 WEBHOOK_MAX_RESPONSE_BYTES 1메가바이트에서 끊습니다. Delay 는 MAX_DELAY_SECS 270초가 상한입니다. 워크플로우가 만든 메시지는 다른 워크플로우를 발동시키지 않게 빼놨는데, 무한 루프를 아예 구조로 막은 셈입니다.
숫자 단위가 밀리초와 바이트입니다. 이 정도로 촘촘하면 나머지도 비슷하겠거니 하고 넘어가게 됩니다. 저도 처음엔 넘어갔습니다.
리뷰 네 편이 같은 자리에서 걸렸습니다
영상들을 다시 보면서 걸린 지점만 따로 모았습니다.
한 편에서는 에이전트 셋을 순서대로 이어붙이는 지시를 내렸는데, 앞 에이전트가 다음 에이전트를 태그하지 않아서 중간에 멈췄습니다. 진행자가 직접 다시 멘션해서 이어붙였습니다. 같은 편에서 프로그래머 역할 에이전트가 리서치 결과를 기다리지 않고 뼈대를 먼저 만든 뒤 이미지만 교체하는 장면도 나옵니다.
다른 편에서는 Claude Code 가 권한 승인을 요청하는데 버튼으로 뜨지 않고 메시지로만 왔습니다. 생성한 파일이 채널에 첨부되지 않는 것도 같은 편에서 짚었습니다.
세 번째 편은 에이전트끼리 토론하는 시간이 그대로 비용이 되니 정지 조건을 걸라고 권고합니다. 네 번째 편은 한계를 다섯 가지로 정리해뒀습니다.
증상은 제각각인데 걸린 자리는 하나로 모입니다. 승인입니다. 그리고 승인 다음으로 자주 나온 게 "결과물이 안 온다"였습니다.
읽다가 한 편은 호스팅 업체 제휴 광고라는 걸 알게 됐습니다. 쿠폰 코드까지 안내하는 편인데, 그 편은 개인 컴퓨터로 relay 를 돌리는 건 의미가 없다면서 유료 서버 쪽으로 안내합니다. 다른 편은 공식에서 제공하는 호스팅을 쓰는 게 제일 쉽다고 합니다. 두 경로가 다 되는 건 맞으니 틀린 안내는 아닙니다. 다만 같은 도구인데 권장 경로가 갈리고, 그 갈림에 광고가 끼어 있습니다.
RequestApproval 은 갈 곳이 없습니다
승인부터 찾았습니다. executor.rs 726번째 줄입니다.
// TODO (WF-08): create approval record in DB, emit kind:46010.
// For now, return Suspended with the token so the caller can persist state.
승인은 이 목록에서 특별한 자리에 있습니다. 나머지 여섯 개는 자동화가 무엇을 하느냐를 정하는데, 승인만은 자동화를 어디서 멈출지를 정합니다. 에이전트에게 실제 권한을 주면서 사고를 막는 장치가 이것 하나뿐입니다. 리뷰 네 편이 모두 여기서 걸린 게 우연으로 보이지 않는 이유이기도 합니다. 에이전트를 진짜로 일 시키려는 사람은 반드시 이 자리에 옵니다.
RequestApproval 은 실행을 멈추고 재개용 토큰까지 만듭니다. 거기까지는 됩니다. 그런데 그 토큰을 받아줄 승인 레코드를 데이터베이스에 만드는 일과 이벤트를 내보내는 일이 아직 할 일 목록으로 남아 있습니다.
영상에서 버튼이 안 뜬 건 화면을 덜 만들어서가 아닙니다. 버튼이 눌렸을 때 갈 곳이 없어서입니다. 화면을 아무리 붙여도 받는 쪽이 비어 있으면 같은 결과가 나옵니다.
찾다 보니 옆에 두 개가 더 있었습니다. 644번째 줄과 650번째 줄입니다.
warn!(run_id = %run_id, step = step_id, "SendDm not yet implemented (to={to})");
// TODO (WF-07): emit DM event.
warn!(run_id = %run_id, step = step_id, "SetChannelTopic not yet implemented");
// TODO (WF-07): update channel topic via DB.
SendDm 과 SetChannelTopic 입니다. 둘 다 아직 안 만들었다는 경고를 로그에 찍고 그냥 넘어갑니다. 일곱 개 중 셋이 이 상태입니다.
안 한 일이 한 일로 기록됩니다
여기까지는 여섯 달 된 프로젝트라면 그러려니 할 수 있습니다. 미구현은 일정의 문제이고, 시간이 지나면 채워집니다.
문제는 그 셋이 무엇을 반환하느냐입니다. 앞에서 인용한 532번째 줄의 함수 주석이 그 답입니다. 로그를 남기고 성공 출력을 반환합니다.
그러니까 SendDm 이 든 워크플로우를 짜서 돌리면 실행은 성공으로 끝납니다. DM 은 가지 않았는데 실패 신호는 없습니다. SetChannelTopic 도 마찬가지입니다. 주제는 그대로인데 바꿨다고 기록됩니다.
이게 미구현보다 나쁜 이유는 고칠 기회가 사라지기 때문입니다. 실패로 떨어지면 로그를 보고 고칩니다. 성공으로 떨어지면 로그를 볼 이유가 없습니다. 워크플로우 실행 기록에 초록색이 줄줄이 찍혀 있는데 아무 일도 일어나지 않은 상태를, 그 화면만 보고는 알아낼 방법이 없습니다.
앞에서 단계가 이어진다고 적은 게 여기서 걸립니다. 다음 단계는 앞 단계의 출력을 끌어다 씁니다. SendDm 이 성공을 반환하면 그 뒤에 붙은 단계들은 DM 이 갔다는 전제로 계속 굴러갑니다. 알림 보내고, 반응 달고, 채널에 요약 남기는 흐름을 짜두면 알림만 빠진 채로 나머지가 다 성공합니다. 실패를 반환했다면 거기서 멈췄을 흐름이 성공을 반환하니까 끝까지 갑니다. 한 액션의 오보고가 그 워크플로우 전체의 기록을 오염시킵니다.
기능이 없는 것과 없다는 사실을 숨기는 것은 다른 종류의 문제입니다. 앞은 쓰는 사람이 우회할 수 있고, 뒤는 우회할 대상 자체가 안 보입니다.
앞에서 방어 설계가 촘촘하다고 했던 게 여기서 뒤집힙니다. 막는 계층은 밀리초와 바이트 단위로 박혀 있는데, 안 한 일을 안 했다고 말하는 계층은 로그 한 줄입니다. 그것도 같은 파일 안에 둘 다 있습니다. 조건식 평가에 100밀리초 제한을 건 사람과, 미구현 액션에 성공을 반환하게 둔 사람이 같은 파일을 만졌습니다.
CallWebhook 도 비슷한 모양이 하나 더 있었습니다. 700번째 줄 근처에 빌드 설정에 따라 reqwest not enabled 를 로그로 남기고 placeholder 를 반환하는 경로가 있습니다. 기본 빌드에서 그 기능이 켜져 있는지는 확인하지 않았으니 단정하지 않겠습니다. 다만 "못 하면 로그 남기고 성공"이라는 처리가 한 곳에 몰린 실수가 아니라 이 파일의 습관이라는 건 보입니다.
두 달 열흘 동안 일곱 번 손댔습니다
개발 중이라 그런 것 아니냐는 반론이 가능합니다. 커밋 이력을 봤습니다.
이 파일은 6월 10일에 지금 이름으로 자리를 잡았고, 그 뒤로 손댄 게 일곱 번입니다. 가장 최근이 8월 20일이고 SendMessage 액션에 스레드 답장 기능을 붙이는 작업이었습니다.
두 달 하고 열흘 동안 일곱 번 만졌는데 WF-07 로 표시된 할 일 두 줄은 그대로입니다. 새 기능이 붙는 동안 안 만들어진 액션은 계속 성공을 돌려주고 있었습니다.
이건 잊혀서 생긴 일로 보기 어렵습니다. 같은 파일을 일곱 번 열었으면 644번째 줄과 650번째 줄을 일곱 번 지나쳤습니다. 그때마다 우선순위에서 밀렸다고 보는 편이 자연스럽습니다. 그리고 밀릴 만합니다. 안 만든 액션을 만드는 일은 티가 나고, 안 만든 액션이 성공을 반환하지 않게 고치는 일은 티가 안 납니다. 후자는 고쳐놔도 화면에 아무 변화가 없습니다. 오히려 있던 초록색이 빨간색으로 바뀌니까, 밖에서 보면 뭔가 망가진 것처럼 보입니다.
이건 우선순위를 정하는 사람의 문제가 아니라 우선순위가 정해지는 방식의 문제입니다. 이슈 목록에 올릴 때 "SendDm 구현"은 한 줄로 적히는데 "미구현 액션이 성공을 반환하지 않게"는 무슨 말인지 설명부터 해야 합니다. 설명이 필요한 항목은 뒤로 갑니다.
리뷰 한 편은 반대로 봤습니다
네 번째 편이 승인 쪽을 설명하면서 그 지점에서 실행이 실패로 처리된다고 말했습니다. 코드는 반대입니다. 실패가 아니라 Suspended 를 반환하고 재개 토큰까지 만듭니다.
방향이 정확히 뒤집혔는데, 이 편은 코드와 문서를 실제로 읽고 만든 아키텍처 해설이었습니다. 안 되는 것을 정직하게 짚겠다고 선언한 쪽조차 반대로 봤습니다.
같은 편에 Nostr 을 회사라고 부르는 대목도 있습니다. Nostr 은 회사가 아니라 개방형 프로토콜이고, Buzz 를 공개한 곳은 Block 입니다. 다른 편은 별 개수를 27,000개라고 말하는데, 촬영 시점 값이라 지금과 다릅니다.
이걸 리뷰어들의 부주의로 읽으면 얻을 게 없습니다. 네 사람이 각자 시간을 들여 설치하고 돌려보고 정리했습니다. 그런데도 못 본 건, 화면만 봐서는 알 수 없는 층이기 때문입니다. 실행 기록에 초록색이 뜨면 그건 됐다는 뜻입니다. 그 초록색을 의심하려면 초록색을 만드는 코드를 열어야 하는데, 도구를 소개하는 자리에서 거기까지 가는 사람은 드뭅니다.
저도 코드를 안 열었으면 같은 자리에서 멈췄습니다. 다만 어디를 열어야 하는지는 알고 있었고, 그건 Buzz 를 잘 알아서가 아닙니다. 상품 페이지에는 1Gbps 라고 적혀 있고 칩셋은 100Mbps 였던 공유기를 산 적이 있는데, 그때도 스펙표를 아무리 봐도 안 나왔고 칩셋 번호를 찾아야 나왔습니다. 여섯 편이 나란히 된다고 말하는 걸 붙여놓고 세어본 적도 있는데, 세어보니 실패 사례는 한 건이었습니다. 표에 적힌 숫자와 안에 든 부품이 다른 말을 하는 모양을 몇 번 보고 나면, 다음에 비슷한 게 오면 표부터 안 믿게 됩니다.
사람이 보는 자리에는 지금도 쓸 만합니다
도입을 검토하던 입장에서 정리하면 이렇습니다.
에이전트 여럿을 한 채널에 모아놓고 사람이 지켜보면서 진행시키는 용도라면 지금도 됩니다. 컨텍스트가 한 스레드에 남는다는 게 실제 이득이고, 모델을 바꿔 끼워도 앞 대화가 그대로 있습니다. 제가 복사해서 옮기던 일을 relay 가 대신합니다. SendMessage 와 AddReaction 은 구현돼 있으니 사람이 보는 자리에서는 문제가 안 됩니다.
반대로 사람이 안 보는 자리에는 못 넣습니다. 야간 배치, 결과를 DM 으로 받아보는 알림, 승인 없이는 다음으로 못 넘어가게 막아두는 흐름. 하필 지금 비어 있는 셋이 정확히 그 용도의 액션들입니다. SendDm 은 사람이 화면을 안 볼 때 소식을 전하라고 있는 액션이고, RequestApproval 은 사람이 볼 때까지 멈춰 있으라고 있는 액션입니다. 무인 운용에 필요한 것부터 비어 있습니다.
운영 부담도 따로 봐야 합니다. 직접 띄우려면 Docker 가 필요하고 PostgreSQL 과 Redis 를 같이 올려야 합니다. 파일 저장은 MinIO 를 씁니다. 소스에서 빌드하려면 Rust 와 Node 까지 갖춰야 합니다. 컨테이너 여러 개를 계속 살려둬야 하는 구성이라, 혼자 쓰자고 세우기에는 손이 많이 갑니다. 공식 호스팅을 쓰면 그건 넘어가는데 대신 보관 기간에 제한이 걸립니다.
그래서 지금은 안 넣기로 했습니다. 비어 있어서가 아니라 비어 있다는 걸 실행 기록으로는 알 수 없어서입니다.
넣게 될 조건은 명확합니다. WF-07 과 WF-08 이 지워지면 됩니다. 그 전이라도, 미구현 액션이 성공 대신 실패나 최소한 경고 상태를 반환하게 바뀌면 그때는 넣습니다. 그렇게 되면 안 되는 걸 알면서 쓰는 게 되고, 아는 것은 우회할 수 있습니다. 지금 상태에서 넣으면 되는 줄 알고 쓰게 되고, 그건 우회할 방법이 없습니다. 규격은 문서에 적혀 있는데 그걸 세는 방법이 없어서 열여덟 건과 일곱 건이 갈렸던 때와 같은 모양입니다. 그때도 규칙은 있었고 검사가 없었습니다.
Developer Preview 가 덮어주지 않는 자리
공식 사이트 첫 화면에 Developer Preview 라고 적혀 있습니다. Block 도 이게 완성품이라고 말하지 않습니다.
그 표기가 덮어주는 범위는 넓습니다. 아직 안 된다는 뜻으로는 충분히 덮어줍니다. 여섯 달 된 프로젝트에 안 만든 액션이 셋이라는 건 오히려 적은 편입니다.
덮어주지 않는 건 다른 쪽입니다. 안 됐는데 됐다고 말하는 것은 완성도의 문제가 아니라 보고 방식의 문제입니다. 미구현은 시간이 해결하지만, 미구현을 성공으로 보고하는 설계는 시간이 지나도 그대로 남습니다. 오히려 액션이 늘어날수록 같은 처리를 따라가는 액션도 늘어납니다. dispatch_action 의 주석이 "대부분의 액션"이라고 적은 게 그 뜻입니다.
앞에서 relay 가 기록을 못 고치게 만드는 데 공을 들였다고 적었습니다. 그 대목이 여기서 돌아옵니다. 검증하는 쪽 코드는 따로 잘 짜여 있었습니다. 서명을 확인하고, 권한을 확인하고, 감사 로그를 해시 체인으로 묶어서 중간을 지우면 사슬이 끊어지게 해뒀습니다. 누가 무엇을 했는지는 이렇게까지 챙깁니다. 그런데 무엇이 실제로 일어났는지를 말하는 자리는 로그 한 줄입니다. 서명과 해시 체인으로 지킨 기록의 내용이 "이 액션은 성공했다"인데, 그 문장 자체가 사실이 아닙니다.
기획서에 확정된 결정이 열두 개나 있었는데 크기를 정한 건 하나도 없었던 때와 결이 같습니다. 예/아니오로 답할 수 있는 것부터 코드가 됐습니다. 서명이 맞나 틀리나는 예/아니오입니다. 100밀리초를 넘었나 안 넘었나도 예/아니오입니다. 그런데 "이 액션이 실제로 무엇을 했나"는 예/아니오가 아니라 서술이라서, 검사로 옮기기 어렵고 그래서 뒤로 밀립니다. 옮기기 쉬워서 먼저 된 것이지 더 중요해서 먼저 된 게 아닙니다.
지금 보고 있는 값들은 8월 28일에 main 브랜치에서 확인한 것입니다. 활발하게 움직이는 저장소라서 줄 번호는 곧 어긋납니다. 다만 WF-07 과 WF-08 이라는 표시는 지워지기 전까지 남아 있을 테니, 나중에 같은 파일을 다시 열면 그 두 줄이 아직 있는지로 확인할 수 있습니다.
댓글
댓글 쓰기