
tunaFlow 기술 포스트 시리즈 3편입니다. 2편에서 워크플로우 파이프라인(Plan → Dev → Review)을 다뤘는데, 그 파이프라인이 동작하려면 "대화를 분기하는 구조"가 필요합니다. Implementation(현재는 Dev) Branch, Review Branch가 바로 그것입니다. 이번 편은 Branch 자체의 설계와 활용에 대한 이야기입니다.
왜 대화를 분기해야 하는가
에이전트와 대화하다 보면 이런 상황이 자주 생깁니다.
나: "인증을 OAuth2로 바꾸면 어떨까?"
Claude: "좋습니다. 이렇게 하면 됩니다..." (긴 설명)
나: "아... 근데 JWT를 유지하는 방향도 한번 볼래?"
Claude: (이미 OAuth2 맥락으로 쭉 온 상태)
메인 대화에서 다른 방향을 시도하면 맥락이 섞입니다. "아까 OAuth2 쪽이 나았는데"하고 돌아가려면 이전 메시지를 일일이 참조해야 합니다.
그런데 이건 코드 작업에서 더 심각합니다. 메인 대화에서 에이전트에게 코드를 수정시키면 그 결과가 워킹 디렉토리에 바로 반영됩니다. 실험적인 수정을 메인 대화에서 하면, 되돌리기가 곤란합니다.
git에서 branch를 만들어서 실험하고, 괜찮으면 merge하는 것처럼, 대화에도 같은 구조가 필요했습니다.
Branch의 기본 구조
tunaFlow에서 Branch는 메인 대화의 특정 시점에서 분기한 독립 대화입니다.
메인 대화:
msg1 → msg2 → msg3 → msg4 → msg5 → ...
↓
Branch A (msg3 시점에서 분기)
→ branchMsg1 → branchMsg2 → ...
Shadow Conversation
Branch의 메시지는 별도 conversation에 저장됩니다. 이걸 shadow conversation이라고 부릅니다(부르기로 했어요!)
conversations 테이블:
id: "conv_main" ← 메인 대화
id: "branch:{branchId}" ← Branch의 shadow conversation
Branch를 열면 shadow conversation의 메시지를 로드합니다. 메인 대화와 완전히 분리된 공간이어서, Branch 안에서 뭘 하든 메인 대화에 영향이 없습니다. Branch에서 브랜치를 만들 수도 있습니다 — parent_branch_id로 트리 구조를 추적합니다. 실사용에서 2단계를 넘어가는 경우는 거의 없었지만, 구조적으로 깊이 제한은 없습니다.
Checkpoint와 맥락 상속
Branch가 분기한 지점의 메시지를 checkpoint라고 합니다.
pub struct Branch {
pub id: String,
pub conversation_id: String, // 부모 대화
pub checkpoint_id: Option, // 분기 지점 메시지
pub parent_branch_id: Option,
pub mode: String, // "chat" 또는 "roundtable"
pub git_branch: Option,
// ...
}
중요한 것은 Branch가 부모 대화의 맥락을 어떻게 이어받는가입니다. 이 부분이 실행 모드에 따라 다릅니다.
PTY 모드 (기본): 부모 대화의 PTY 세션을 그대로 공유합니다. Claude CLI는 세션 내 대화 히스토리를 자체적으로 유지하기 때문에, Branch의 첫 메시지를 보내면 에이전트는 부모 대화의 전체 맥락을 이미 알고 있는 상태에서 응답합니다. 별도의 맥락 전달이 필요 없습니다.
// threadSlice.ts — Branch는 부모의 PTY 세션을 공유
const ptySession = usePtyStore.getState().getSession(engineKey);
await sendMessageViaPty(set, get, prompt, ptySession, convId, engineKey, {
forceNewJsonl: isFirstMessage, // 첫 메시지만 새 JSONL 경로
});
CLI 폴백 모드 (-p): PTY를 사용할 수 없는 경우 ContextPack에 thread inheritance 섹션이 포함됩니다. Thread Anchor(분기 지점 메시지)와 부모 대화의 최근 4턴을 넣어서 에이전트에게 맥락을 제공합니다.
// context_loading.rs
let thread_inheritance = if is_branch {
build_thread_inheritance_section(conn, conversation_id)
} else { None };
RT 모드: Roundtable 참가자는 항상 -p 모드로 실행됩니다. 각 참가자가 독립 서브프로세스이므로 세션 공유가 불가능하고, 대신 RtContextCache가 최소 맥락(~1-2k 토큰)과 라운드별 transcript를 조립해서 전달합니다. RT는 4편에서 상세히 다룹니다.
세 종류의 Branch
tunaFlow에서 Branch는 용도에 따라 세 가지로 나뉩니다. 코드상으로는 모두 같은 branches 테이블에 있고, 용도에 따라 다르게 사용됩니다.
1. 일반 Branch — 자유 실험
사용자가 대화 중에 수동으로 만드는 Branch입니다. "이 방향으로 한번 시도해보자"할 때 사용합니다.
메시지 우클릭 → "Branch 분기" → 새 Branch 생성 → 드로어에서 열림
2. Implementation(Dev) Branch — 워크플로우 자동 생성
2편에서 다룬 것처럼, Plan이 승인되면 자동으로 생성됩니다.
Plan 승인 → Implementation Branch 자동 생성
→ Developer에게 프롬프트 전송
→ 구현 완료 → impl-complete 마커 감지
3. Review Branch — 리뷰용 자동 생성
구현이 완료되면 Review를 위한 Branch가 자동 생성됩니다.
impl-complete 감지 → Review Branch 자동 생성
→ Reviewer 에이전트 실행 → verdict 출력
세 종류 모두 같은 Branch 구조를 사용합니다. 워크플로우 Branch(2, 3)는 Plan의 implementation_branch_id, review_branch_id와 연결되어 Workflow 탭에서 상태를 추적할 수 있습니다.
Branch 위에 워크플로우를 얹는 구조입니다. Branch 자체는 범용적인 대화 분기이고, 워크플로우는 그 위에 Phase(implementation/review/rework)와 자동 전환을 올린 것입니다. 그래서 워크플로우 없이도 Branch를 자유롭게 사용할 수 있고, 워크플로우 Branch와 사용자 Branch가 동일한 구조로 관리됩니다.
드로어 UX
Branch를 열면 **오른쪽 드로어(슬라이더)**로 열립니다. 전체 화면 전환이 아닙니다.
┌──────────┬──────────────────┬──────────────────┐
│ Sidebar │ CenterPanel │ Branch 드로어 │
│ │ (메인 채팅) │ (Branch 채팅) │
│ │ │ │
│ │ msg1 │ branchMsg1 │
│ │ msg2 │ branchMsg2 │
│ │ msg3 ← checkpoint│ │
│ │ msg4 │ │
└──────────┴──────────────────┴──────────────────┘
메인 대화와 나란히 볼 수 있다는 것이 핵심입니다. Branch에서 구현하면서 메인 대화의 설계 논의를 참고하는 경우가 많습니다. 전체 화면이면 왔다 갔다 해야 하지만, 드로어는 "옆에서 참고하는" 느낌입니다. 맥락 전환 비용이 낮습니다.
드로어의 📌(pin) 버튼을 클릭하면 오버레이 대신 CenterPanel과 50:50으로 분할되는 고정 패널 모드도 있습니다. 리뷰하면서 구현 Branch를 계속 옆에 두고 싶을 때 유용합니다.
Adopt: 결과를 메인 대화에 합류
Branch에서 좋은 결과가 나오면 메인 대화에 요약을 삽입합니다. 이걸 adopt라고 부릅니다.
Branch에서 "OAuth2 PKCE 방식이 적합합니다. 이유는..." 결론 도달
→ Adopt 클릭
→ 메인 대화에 요약 메시지 삽입
→ Branch 상태: active → adopted → Archive로 이동
adopt ≠ merge
git merge와 달리, adopt는 요약만 삽입합니다. Branch의 전체 메시지가 메인 대화로 복사되는 것이 아닙니다.
Branch 메시지를 전부 넣으면 메인 대화가 급격히 길어짐
요약만 있으면 "이런 결론이 나왔다"는 사실만 메인 대화에 남음
원본이 필요하면 Archive에서 해당 Branch를 열어볼 수 있음
요약 추출 방식
RT Branch와 일반 Branch의 요약 추출이 다릅니다.
RT: brief(토론 요약 문서)에서 Key Positions 섹션을 추출합니다. RT는 토론 구조가 정형화되어 있어서 비교적 깔끔한 요약이 나옵니다.
일반 Branch: 마지막 assistant 메시지에서 300자를 잘라서 넣습니다. 솔직히 이건 조잡합니다. 하남자스럽습니다. 그리고 LLM으로 요약을 생성하는 방법도 있지만 아직 자동으로 적용하지 않았습니다. 현재는 adopt 전에 에이전트에게 "지금까지 결론을 정리해줘"라고 요청하고 adopt하는 것이 실사용 패턴입니다.
Git Branch 연동
tunaFlow Branch에 실제 git branch를 연결할 수 있습니다.
Branch "impl-auth" → git branch "feature/oauth-migration"
연결하면 사이드바에 git branch 이름이 표시되고, Developer가 해당 Branch에서 작업할 때 git 맥락이 제공됩니다.
// branches.rs — create_branch
let git_branch: Option = if let Some(ref parent_id) = input.parent_branch_id {
// 부모 Branch의 git_branch를 상속
conn.query_row(
"SELECT git_branch FROM branches WHERE id = ?1",
[parent_id], |row| row.get(0),
).ok().flatten()
} else {
// 부모가 없으면 현재 git branch 자동 감지
detect_current_git_branch(project_path)
};
부모 Branch의 git branch를 상속하고, 없으면 프로젝트의 현재 git branch를 자동 감지합니다. Implementation Branch에서 에이전트가 코드를 수정하면 연결된 git branch에 반영되므로, 메인 브랜치를 오염시키지 않고 실험할 수 있습니다.
사이드바에서의 Branch 관리
사이드바는 Branch를 세 곳에 보여줍니다.
Sidebar:
▸ Branches (3) ← 활성 일반 Branch
🔀 impl-auth
🔀 review-auth
🔀 experiment-jwt
▸ Roundtables (1) ← 활성 RT (RT = Branch의 확장 모드)
👥 auth-discussion
▸ Archive ← adopted/archived Branch
📦 oauth-analysis
mode 필드와 status 필드로 구분합니다. RT가 Branch의 "모드"라는 것은 4편에서 상세히 다루겠습니다.
현재 한계
1. adopt 요약이 조잡함
RT에서는 Key Positions 추출이 비교적 잘 되지만, 일반 Branch에서는 마지막 응답 300자 절단이 전부입니다. 실사용에서는 adopt 전에 에이전트에게 "지금까지 결론을 정리해줘"라고 요청하고 adopt하는 식으로 우회하고 있습니다.
개선 방향: adopt 시점에 에이전트에게 Branch 전체 대화의 요약을 생성하는 것이 자연스럽습니다. Branch의 메시지 수가 보통 10개 이내라 토큰 비용이 크지 않고, 이미 compressed memory 파이프라인에서 LLM 요약 생성 인프라가 있어서 재활용할 수 있습니다. adopt 요약에 마커를 붙여두고 있어서, 향후 전용 UI 카드(BranchAdoptCard)로 렌더링하는 것도 준비되어 있습니다.
2. 격리와 공유 사이의 균형
Branch와 메인 대화는 대화 UI상으로는 격리되어 있지만, 완전한 격리는 아닙니다. ContextPack의 retrieval 파이프라인이 프로젝트 내 모든 대화(Branch 포함)를 FTS5 + 벡터 검색 대상으로 잡기 때문에, Branch에서 논의한 내용이 메인 대화의 ContextPack에 관련 청크로 끌려올 수 있습니다. 코드 수정 역시 같은 워킹 디렉토리를 공유하므로 git 레벨에서는 바로 보입니다.
다만 이건 검색 기반의 간접적 전달이라, "Branch에서 OAuth2 PKCE로 결론 났다"는 명시적 정보가 메인에 전달되는 것은 아닙니다. 그건 여전히 adopt의 역할입니다.
개선 방향: session_links 테이블로 대화 간 자동 연결이 이미 구현되어 있고, cross-session 섹션이 ContextPack에 포함됩니다. Branch adopt 시점에 session link를 자동 생성하면, adopt 이후 메인 대화의 에이전트가 Branch 내용을 cross-session으로 참조할 수 있습니다. adopt 전에도 Branch의 주요 결론을 메인에 힌트로 넣는 방법은 검토 중이지만, 격리의 장점을 해치지 않는 선에서 신중하게 접근하고 있습니다.
핵심 결정 정리
결정 | 이유 |
|---|
Shadow conversation으로 격리 | 메인 대화 오염 방지. Branch 삭제 시 메시지도 깔끔하게 제거 |
PTY 세션 공유 (일반 Branch) | 부모 대화 맥락을 자연스럽게 계승. 별도 맥락 전달 불필요 |
드로어 UX (전체 화면 아님) | 메인과 나란히 보기 + 낮은 맥락 전환 비용 |
adopt = 요약만 삽입 | 메인 대화 비대화 방지. 원본은 Archive에서 접근 |
워크플로우가 Branch를 빌려쓰는 구조 | Branch는 범용, 워크플로우는 그 위의 레이어 |
git branch 상속 | 부모 Branch의 git 맥락을 자동으로 계승 |
다음 편 예고
4편: "에이전트끼리 토론시키기" — Roundtable 설계와 한계
RT는 Branch의 확장 모드입니다. branches.mode = "roundtable"이면 여러 에이전트가 순차 또는 병렬로 토론합니다. Review RT가 왜 2-agent인지, Sequential과 Deliberative의 차이, RT가 잘 안 되는 경우를 다룹니다.
시리즈 목록
에이전트에게 프로세스를 줘라 — AOC를 만들면서 배운 것
Plan → Dev → Review — 워크플로우 파이프라인 구현기
대화를 분기한다 — Branch 설계와 활용 (이 글)
에이전트끼리 토론시키기 — Roundtable 설계와 한계
대화가 길어지면 — 에이전트 장기 메모리 구현기
Claude $20으로 워크플로우 돌리기 — 엔진 아키텍처
코드 구조를 에이전트에게 알려주기 — rawq + code-review-graph
246개 스킬 중 필요한 것만 — 스킬 자동 적용 구현기
에이전트가 같은 실수를 반복하면 — 품질 보증 설계
tunaFlow로 풀사이클 돌려보기 — 워크플로우 실전 테스트 회고
레퍼런스
tunaFlow 내부 문서
Branch 커맨드: src-tauri/src/commands/branches.rs
Thread store: src/stores/slices/threadSlice.ts
PTY 메시지 전송: src/stores/slices/ptyMessageSender.ts
드로어 패널: src/components/tunaflow/BranchThreadPanel.tsx
ContextPack 맥락 로딩: src-tauri/src/commands/agents_helpers/send_common/context_loading.rs
Plan Branch 연결: src-tauri/src/commands/plans.rs