Git Refs와 CRDT를 사용하여 두 개의 코딩 에이전트 조정하기
요약
Git Refs와 CRDT(Conflict-free Replicated Data Types)를 활용하여 두 개의 독립적인 코딩 에이전트가 충돌 없이 변경 사항을 조정하는 기술적 방법을 다룹니다. 브랜치 병합 없이도 에이전트의 의도를 결정론적으로 수렴시켜 최종 상태를 생성하는 실험적 모델을 제시합니다.
핵심 포인트
- Git Refs와 CRDT를 결합하여 에이전트 간의 독립적인 작업 보존
- 브랜치 병합(Merge) 없이도 결정론적인 상태 수렴 가능
- 에이전트 간 대화 기록이나 체크아웃 공유 없이도 동기화 구현
- 작업 순서에 관계없이 동일한 최종 상태를 생성하는 리듀서 모델
Coordinating Two Coding Agents with Git Refs and a CRDT — Agent Lab Journal
Agent Lab Journal
Guides
...
실습 · 에이전트 엔지니어링 (Agent Engineering) · Git
Git Refs와 CRDT를 사용하여 두 개의 코딩 에이전트 조정하기
레벨: advanced
읽기 및 실험 시간: 60분
결과: 작업 브랜치(working branch)로 브랜치를 병합하지 않고도 두 개의 독립적인 에이전트 변경 사항을 보존함
...
목차
-
증명할 내용
-
브랜치만으로는 불충분한 이유
-
조정 모델 (Coordination model)
-
구체적인 사례
-
전제 조건 및 안전 경계
-
1단계: 저장소 생성
-
2단계: CRDT 리듀서 (reducer) 구현
-
3단계: 두 개의 독립적인 변경 사항 게시
-
4단계: Git refs를 통한 동기화
-
5단계: 수락된 상태 구체화 (materialize)
-
6단계: 세션 복구 증명
-
검증 프로토콜
-
실제 코딩 에이전트 연결
-
실패 사례
-
한계점
-
운영 환경 강화 (Production hardening)
증명할 내용
이 실험은 독립적으로 편집 가능한 두 개의 필드가 있는 작업을 사용합니다. 에이전트 Alpha는 작업 상태를 open에서 in_progress로 변경합니다. 에이전트 Beta는 검증 요구 사항을 추가합니다. 각 에이전트는 자신의 변경 사항을 별도의 불변 작업(immutable operation)으로 기록하고 자신의 ref만 진행시킵니다.
결정론적 리듀서 (deterministic reducer)는 Git 객체 데이터베이스에서 두 ref를 직접 읽어 작업 내용을 검증하며, ref가 제공되는 순서와 관계없이 동일한 구체화된 작업을 생성합니다. 수락된 브랜치는 에이전트 작업 공간의 병합이 아닌, 생성된 하나의 파일을 받습니다.
실험이 끝나면, 실험실은 다음의 모든 속성을 확립해야 합니다:
-
에이전트들이 동일한 베이스 커밋 (base commit)에서 시작해야 합니다;
-
어느 에이전트도 상대 에이전트의 체크아웃 (checkout)이나 대화 기록을 필요로 하지 않아야 합니다;
-
각 에이전트는 개별적인 프라이빗 퍼블리케이션 레프 (private publication ref)를 가집니다;
-
작업 식별자 (operation identifiers)가 충돌하지 않아야 합니다;
-
Alpha를 적용한 후 Beta를 적용하는 것이 Beta를 적용한 후 Alpha를 적용하는 것과 동일한 상태를 주어야 합니다;
-
동일한 작업을 다시 재생 (replaying)하더라도 두 번 적용되지 않아야 합니다;
-
최종 작업에는 두 개의 독립적인 변경 사항이 모두 포함되어야 합니다;
-
워킹 브랜치 (working branch)에는 어느 에이전트 브랜치로부터의 머지 커밋 (merge commit)도 없어야 합니다;
-
새로운 세션이 레프 (refs)와 커밋된 객체 (committed objects)만으로 결과를 재구성할 수 있어야 합니다.
여기서 "충돌 없이"라는 의미
이는 임의의 동시 소스 코드 편집이 의미론적으로 호환된다는 것을 의미하지 않습니다. 이는 에이전트들이 허용된 워킹 트리 (working tree) 외부에서 구조화된 의도 (structured intent)를 게시하며, 정의된 작업 집합이 결정론적인 수렴 규칙 (deterministic convergence rules)을 가진다는 것을 의미합니다. 생성된 상태가 수용된 프로젝트 상태가 되기 전에는 여전히 인간 또는 자동화된 검토가 필요합니다.
브랜치만으로는 불충분한 이유
전통적인 브랜치는 파일 시스템 스냅샷 (filesystem snapshots)의 연속을 기록합니다. 이는 소스 코드에는 유용하지만, 브랜치는 두 스냅샷이 중복된 작업인지, 독립적인 추가 사항인지, 아니면 상충하는 결정인지를 설명하지 못합니다. 만약 두 에이전트가 동일한 JSON 작업 파일을 편집한다면, Git은 단순히 줄 (lines) 단위로 인식합니다. 코디네이터 (coordinator)에게는 식별자, 작성자, 논리적 순서, 그리고 명시적인 의미론 (semantics)을 갖춘 작업 (operations)이 필요합니다.
익숙한 "에이전트당 하나의 브랜치" 설계 또한 조정의 공백 (coordination gap)을 남깁니다. 에이전트가 올바르게 커밋을 했더라도 풀 리퀘스트 (pull request)를 열기 전에 사라질 수 있습니다. 다른 세션은 브랜치 이름은 알 수 있지만 작업에 대한 가정 (task assumptions)은 알지 못할 수 있습니다. 세 번째 프로세스는 두 브랜치를 모두 머지하면서 텍스트 충돌 (textual conflict)을 해결하는 과정에서 실수로 한 에이전트의 의도를 버릴 수도 있습니다.
...
-
비공개 실행 상태 (Private execution state). 각 에이전트는 자신만의 디렉토리, 임시 파일, 프롬프트(prompts), 그리고 커밋되지 않은 실험들을 사용할 수 있습니다.
-
공개된 조정 상태 (Published coordination state). 불변 작업(Immutable operations)은
refs/agents/하위의 참조(refs)를 통해 접근할 수 있습니다. -
수락된 프로젝트 상태 (Accepted project state). 조정자(coordinator)는 에이전트 브랜치를 머지(merge)하지 않고도 검증된 작업들을
main에 구체화(materialize)합니다.Git 커밋은 불변의 전송 봉투(immutable transport envelope)가 됩니다. 커밋의 해시(hash)는 콘텐츠 주소 지정(content addressing)을 제공하고, 부모(parent)는 출처(provenance)를 제공하며, 참조(ref)는 에이전트에 의해 게시된 최신 봉투를 가리키는 이동 가능한 포인터(movable pointer)를 제공합니다. Git은 전송 및 보존 계층(transport and retention layer)이며, CRDT는 동시적인 의도(concurrent intent)가 어떻게 수렴(converge)하는지를 정의합니다.
조정 모델 (Coordination model)
비공개 참조 (Private refs)
실험실(laboratory)은 다음 이름들을 예약합니다:
refs/heads/main
refs/agents/alpha/task-42
refs/agents/beta/task-42
refs/agents/ 하위의 참조(refs)는 일반적인 Git 참조이지만, 일반적인 `git branch` 출력에서는 로컬 브랜치로 나타나지 않습니다. 에이전트들은 `git update-ref`를 사용하여 이를 업데이트합니다. 조정자는 `git show`를 통해 이들의 커밋 트리(commit trees)를 읽으며, 작업 브랜치(working branch) 위로 이를 체크아웃(checkout)하지 않습니다.
작업 집합 CRDT (Operation-set CRDT)
공유 데이터 타입은 추가 전용 작업 집합(add-only set of operations)입니다. 모든 작업은 전역적으로 고유한 식별자(globally unique identifier)를 가집니다. 집합 합집합(Set union)은 교환 법칙(commutative), 결합 법칙(associative), 그리고 멱등성(idempotent)을 가지므로, 중복 전달되거나 발견 순서가 다르더라도 구성원(membership)은 변하지 않습니다.
태스크(task) 자체에는 두 가지 필드 정책이 있습니다:
-
status는 논리적 튜플(logical tuple)에 의해 정렬되는 마지막 작성자 승리(last-writer-wins) 레지스터입니다. -
requirements는 안정적인 요구사항 식별자(stable requirement identifiers)를 키로 사용하는 추가 전용 집합(add-only set)입니다.마지막 작성자 승리(last-writer-wins) 레지스터는 Lamport 시계(Lamport clock)와 액터 식별자(actor identifier)를 결정론적인 타이 브레이커(tie-breaker)로 사용합니다. 시계는 재현 가능한 순서(reproducible order)를 확립하지만, 나중에 내려진 결정이 더 정확하다는 것을 의미하지는 않습니다.
작업 스키마 (Operation schema)
{
"op_id": "alpha:task-42:1",
"actor": "alpha",
...
Operation 파일은 ops/<actor>/<op-id>.json에 저장됩니다. 파일당 하나의 연산(operation)을 사용하면 동시 추가(concurrent append)로 인한 손상을 방지할 수 있으며 중복된 식별자를 관찰할 수 있습니다. 에이전트는 발행(publication) 후에는 절대로 연산을 수정해서는 안 됩니다. 수정 사항은 새로운 연산으로 처리해야 합니다.
리듀서(reducer)가 입력을 정렬하는 이유
집합 합집합(Set union)은 어떤 연산들이 존재하는지 결정하지만, 구체화 도구(materializer)는 여전히 안정적인 출력 바이트(stable output bytes)가 필요합니다. 따라서 리듀서는 op_id를 통해 중복을 제거하고, 동일한 식별자를 가진 서로 다른 페이로드(divergent payloads)를 거부하며, 리덕션(reduction) 전에 연산들을 정렬합니다. JSON 키와 요구 사항 목록(requirement lists) 또한 고정된 순서로 직렬화(serialized)됩니다.
구체적인 사례: 두 에이전트가 하나의 태스크를 변경하는 경우
태스크 task-42는 다음과 같이 시작됩니다:
{
"id": "task-42",
"title": "Prevent duplicate payment retries",
...
에이전트들은 동일한 베이스 리비전(base revision)을 받지만 서로 다른 책임을 가집니다:
Actor
...
전제 조건 및 안전 경계
다음이 필요합니다:
- Git 2.30 이상;
- Python 3.9 이상 (표준 라이브러리만 사용);
- POSIX 호환 셸(shell);
- 비어 있는 일회용 디렉토리.
도구를 확인하세요:
git --version
python3 --version
기존 저장소 내부가 아닌, 새로운 일회용 디렉토리에서 다음 실험(laboratory)을 실행하세요. 명령어들은 저장소(repository), 커밋(commits), 참조(refs), 그리고 임시 작업 영역(temporary work areas)을 생성합니다. 환경에 이미 작성자 정보가 설정되어 있다면 예시의 작성자 식별자를 교체하세요.
운영 환경(production system)에서 에이전트들은 모든 참조(ref)에 대해 제한 없는 접근 권한을 가져서는 안 됩니다. 각 액터(actor)에게는 자신의 네임스페이스(namespace)만 업데이트할 수 있는 권한을 부여하고, 신뢰할 수 있는 코디네이터(coordinator)가 승인된 브랜치(branch)를 소유하도록 하세요.
1단계: 저장소 생성
비어 있는 디렉토리를 생성하고 진입합니다:
mkdir agent-ref-crdt-lab
cd agent-ref-crdt-lab
git init -b main
...
base/task-42.json을 생성합니다:
{
"id": "task-42",
"title": "Prevent duplicate payment retries",
...
README.md를 생성합니다:
# Agent ref and CRDT laboratory
Immutable agent operations are published under refs/agents/.
...
공통 베이스를 커밋합니다:
git add README.md base/task-42.json
git commit -m "Initialize task coordination laboratory"
BASE_COMMIT=$(git rev-parse HEAD)
...
터미널 세션에 출력된 커밋 식별자(commit identifier)를 보존하세요. 두 에이전트의 게시물(publications)은 모두 정확히 이 커밋을 부모(parent)로 사용하게 되며, 이는 어느 쪽도 다른 쪽에 기반하지 않았음을 증명합니다.
Step 2: CRDT 리듀서 (reducer) 구현
다음 코드로 tools/reduce.py를 생성합니다:
#!/usr/bin/env python3
import argparse
import hashlib
...
에이전트 게시물을 생성하기 전에 리듀서를 커밋합니다:
chmod +x tools/reduce.py
git add tools/reduce.py
git commit -m "Add deterministic task operation reducer"
...
여기서 BASE_COMMIT을 업데이트하는 것이 중요합니다. 에이전트들의 트리(trees)에는 각자의 작업(operation)만 추가되겠지만, 두 에이전트의 커밋 모두 리듀서의 부모 리비전(parent revision)을 포함해야 합니다.
Step 3: 두 개의 독립적인 변경 사항 게시
Git 워크트리 (worktree)를 사용하면 하나의 오브젝트 데이터베이스 (object database)를 공유하면서 각 에이전트에게 별도의 파일 시스템을 제공할 수 있습니다. 워크트리는 실험실(laboratory) 환경에서 편리하지만, 수렴 알고리즘 (convergence algorithm)의 일부는 아닙니다. 별도의 클론 (clones)들도 공유 베어 리포지토리 (bare repository)에 동일한 refs를 게시할 수 있습니다.
Agent Alpha
공통 베이스로부터 분리된(detached) 워크트리를 생성합니다:
git worktree add --detach .agent-alpha "$BASE_COMMIT"
mkdir -p .agent-alpha/ops/alpha
.agent-alpha/ops/alpha/alpha-task-42-1.json을 생성합니다:
{
"op_id": "alpha:task-42:1",
"actor": "alpha",
...
작업을 커밋하고 프라이빗 레프 (private ref)를 게시합니다:
git -C .agent-alpha add ops/alpha/alpha-task-42-1.json
git -C .agent-alpha commit -m "Alpha claims task-42"
ALPHA_COMMIT=$(git -C .agent-alpha rev-parse HEAD)
...
모든 값이 0인 이전 값 (old value)은 "존재하지 않을 때만 생성"함을 의미합니다. 이는 비교 및 교체 (compare-and-swap) 가드 역할을 합니다. 즉, 예상치 못한 기존 레프 (ref)가 있을 경우 다른 세션의 포인터를 조용히 덮어쓰는 대신 게시를 실패하게 만듭니다.
Agent Beta
동일한 베이스로부터 또 다른 분리된 (detached) 워크트리를 생성합니다:
Agent Beta
동일한 베이스로부터 또 다른 분리된 (detached) 워크트리를 생성합니다:
git worktree add --detach .agent-beta "$BASE_COMMIT"
mkdir -p .agent-beta/ops/beta
Create .agent-beta/ops/beta/beta-task-42-1.json:
{
"op_id": "beta:task-42:1",
"actor": "beta",
...
Commit and publish Beta's ref:
git -C .agent-beta add ops/beta/beta-task-42-1.json
git -C .agent-beta commit -m "Beta adds retry verification requirement"
BETA_COMMIT=$(git -C .agent-beta rev-parse HEAD)
...
독립성 확인 (Confirm independence)
test "$(git rev-parse refs/agents/alpha/task-42^)" = "$BASE_COMMIT"
test "$(git rev-parse refs/agents/beta/task-42^)" = "$BASE_COMMIT"
...
앞의 두 검증은 두 게시물이 동일한 부모를 가짐을 증명합니다. 다음 두 개는 어떤 에이전트의 커밋도 다른 쪽의 조상(ancestor)이 아님을 요구합니다.
Step 4: Git refs를 통한 동기화 (synchronize through Git refs)
브랜치를 전환하지 않고 조정 네임스페이스 목록 보기:
git for-each-ref \
--format='%(refname) %(objectname)' \
refs/agents/
커밋 트리에서 게시된 작업을 직접 검사하기:
git show \
refs/agents/alpha/task-42:ops/alpha/alpha-task-42-1.json
...
이 시점에서 main은 이동하지 않았습니다. 수락된 워킹 트리에 에이전트 작업이 포함되어 있지 않음을 확인합니다:
test ! -e ops/alpha/alpha-task-42-1.json
test ! -e ops/beta/beta-task-42-1.json
git status --short
에이전트가 별도의 클론을 사용하는 경우 (When agents use separate clones)
공유 리포지토리는 사용자 정의 네임스페이스를 명시적으로 전송해야 합니다. 기본 fetch는 보통 refs/heads/*.만 따르기 때문입니다. 에이전트는 다음을 사용하여 자신의 비공개 ref를 푸시할 수 있습니다:
git push origin
HEAD:refs/agents/alpha/task-42
조정자(coordinator)는 다음을 사용하여 모든 에이전트 ref를 가져올 수 있습니다:
git fetch origin
'+refs/agents/:refs/agents/'
선행하는 플러스(+) 기호는 non-fast-forward 대체(replacement)를 허용하므로, 강화된 배포(hardened deployment)에는 너무 관대합니다. fast-forward 게시를 요구하거나 불변의, 생성별 특정 ref 이름을 요구하는 서버 측 정책을 선호하십시오.
## 단계 5: 수락된 상태 구체화 (materialize accepted state)
### Alpha를 먼저 Reduce한 후 Beta를 Reduce하기
python3 tools/reduce.py
--ref refs/agents/alpha/task-42
--ref refs/agents/beta/task-42
...
### Beta를 먼저 Reduce한 후 Alpha를 Reduce하기
python3 tools/reduce.py
--ref refs/agents/beta/task-42
--ref refs/agents/alpha/task-42
...
cmp 명령은 반드시 성공적으로 종료되어야 합니다. 이는 단순히 의미론적 유사성 (semantic similarity)이 아니라, 바이트 단위의 일치 (byte-for-byte equality)를 확인합니다.
### 구체화된 태스크(task) 검사하기
python3 -m json.tool /tmp/task-ab.json
생성된 문서는 다음과 같은 구조를 가져야 합니다:
{
"id": "task-42",
"requirements": [
...
이것은 주장된 벤치마크 결과가 아닙니다. 이는 두 가지 작업 피스처 (operation fixtures)에 의해 암시되는 결정론적인 기대 상태 (deterministic expected state)입니다. 즉, Alpha는 승리 상태 작업 (winning status operation)을 제공하고, Beta는 유일한 요구 사항 (requirement)을 기여합니다.
### 생성된 상태만 커밋하기
mkdir -p ta
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기