전체 글

[agents-baton]0.6.0 stable 버전 배포

2026. 9. 18. 16:39
반응형

변경사항이 하도 많아서 초안 작성을 에이전트에게 시켜서 작성하고 히우 후 수정을 하다보니 글이 조금 다를 수 있다. 워낙 단기간에 패치가 진행이 되어 글 쓰는 것이 따라가지 못하는 것이 많다.

agents-baton

에이전트에게 일을 넘기는 CLI에서 협업 상태를 보존하는 시스템

Baton v0.6.0rc2부터 v0.6.0 Stable까지의 (9월10일에 배포)

개요

Baton은 Codex App에서 여러 role agent가 하나의 프로젝트를 이어서 작업하도록 만들기 시작한 CLI다. CLI 로 만든 이유는 이전 글에도 언급했지만, 초기 사용시 서브에이전트의 과도한 토큰 사용과 불안정성 그렇다고 중간 프로그램 없이 파일 단위로 두고 작업을 핑퐁시켰을 때, 불편함, 등을 해결하기 위해서 만들었다.

그렇게 대단한 도구라기 보다는 간단한 보조 기능에서 출발한 것이다. sqlite 를 도입한 이유도 DBMS 계열에는 레이스컨디션을 관리해주는 도구들이 발달되어 있기 때문에 이를 그대로 사용할 수 있지 않을까 싶은 발상으로 사용된 것이다.

아래는 GPT-5.6 sol 이 RC2 에서 Stable 까지 변화를 판단해서 적은 내용이다.

  • 전달한 메시지를 상대가 실제로 처리했는가?
  • 완료된 작업은 성공한 것인가, 검증을 끝냈지만 결함을 발견한 것인가?
  • 실패 후 재시도와 취소 후 대체 작업은 같은 이력으로 이어지는가?
  • DB schema가 바뀌어도 기존 프로젝트 기록이 보존되는가?
  • 여러 Git worktree가 하나의 프로젝트 상태를 안전하게 공유할 수 있는가?
  • agent가 기다려야 할 때 CPU를 낭비하거나 같은 메시지를 반복하지 않는가?

GPT-5.6 sol 감성으로는 'RC2부터 Stable까지의 과정은 기능을 많이 추가하는 작업이라기보다, 이런 질문에 모호하지 않은 답을
하나씩 추가하는 과정이었다. 가장 중요하게 지킨 원칙은 기존 기록을 덮어쓰지 않고, 추측을 줄이고,
복구 행위까지 감사 가능한 상태로 남기는 것이었다.' 라고 한다.

RC2 ~ Stable 이력 수치

아래 값은 로컬 Git 태그와 Stable 소스 트리를 기준으로 산출했다.

항목
범위 v0.6.0rc2v0.6.0
태그 생성 기간 2026-09-08 → 2026-09-10
릴리스 체크포인트 RC2~RC9 8개 + Stable 1개
DB schema 12 → 15
RC2 대비 Stable 변경량 40 files, +5,000 / -446 lines
Stable 태그의 shell test scripts 34개
주요 운영 피드백 문서 8개

변경량에는 구현뿐 아니라 테스트와 문서가 포함된다. 실제 분포도 테스트 16개, 문서 12개, source 9개,
루트 문서 3개였다. Baton에서는 상태 전이 코드만큼 회귀 테스트와 agent 가이드가 제품 동작의 일부였기
때문에 의도된 비율이다.

변화의 흐름

flowchart LR
    A["RC2<br/>재시도와 대체 관계"] --> B["RC3-RC4<br/>업그레이드와 운영 진단"]
    B --> C["RC5<br/>구조화된 완료 결과"]
    C --> D["RC6-RC7<br/>명시적 복구와 상태 해석"]
    D --> E["RC8-RC9<br/>알림 복구와 감사 조회"]
    E --> F["Stable 0.6.0<br/>복수 CR과 실행 결과 관찰"]

큰 흐름으로 보면 Baton은 다음 순서로 발전했다.

  1. 작업의 재시도와 대체 관계를 보존했다.
  2. 운영 중 멈춘 이유를 설명할 수 있게 했다.
  3. finishedsuccessful을 분리했다.
  4. 잘못되거나 누락된 연결을 원본 수정 없이 복구했다.
  5. host 메시지와 실제 agent 처리를 구분했다.
  6. 하나의 결과에 여러 blocker가 존재하는 현실을 표현했다.

RC2: 재시도 로그

RC2의 핵심은 handoff attempt와 replacement chain이었다.

실패한 작업을 재시도할 때 새 작업처럼 취급하면 알림 중복 억제와 감사 이력이 섞인다. 반대로 모든
재시도를 같은 상태로 덮어쓰면 어느 시도에서 무엇이 전달됐는지 알 수 없다. RC2는 재시도할 때
attempt를 증가시키고 notification 중복 범위를 attempt 단위로 제한했다.

취소된 CR 구현 작업도 단순히 사라지지 않게 했다. 취소된 implementation handoff에는 명시적인
replacement를 연결하고, 모든 replacement chain이 정상적으로 끝나야 CR을 implemented로 종료할 수
있게 했다. 이때부터 Baton의 복구 철학이 분명해졌다.

잘못된 과거를 수정하지 않는다. 대신 무엇이 과거를 대체했는지 새 기록으로 남긴다.

중복 dependency는 DB 쓰기 전에 거부했고, 이미 끝난 선행 작업만 가진 handoff는 바로 open되도록
했다. 실패한 handoff를 scheduling dependency로 잘못 연결하는 경우에는 경고를 추가했다.

RC3와 RC4: 정상 동작보다 정지 사유 기록

RC3는 기능보다 업그레이드 안내에 집중했다. pipx로 실행 파일을 업데이트해도 각 프로젝트의
.baton/baton.sqlite3는 자동으로 바뀌지 않는다. 실행 파일과 프로젝트 DB의 lifecycle을 분리하고,
schema migration 전 stop, backup, compatibility 확인 절차를 문서화했다.

RC3 운영 피드백에서는 다음 공백이 드러났다.

  • 활성 작업이 남은 상태에서 executable을 교체할 위험
  • host가 메시지를 수락한 것과 수신 agent가 처리한 것 사이의 공백
  • 요청 role과 agent profile이 어긋날 때의 진단 부족
  • 잘못 기록한 완료 증거를 정정할 수 없는 문제
  • planner가 제출된 CR을 발견하기 어려운 문제

RC4는 이 문제를 운영 명령으로 바꿨다. upgrade preflight는 maintenance stop, waiter, 진행 중 handoff,
취소 확인 대기, claim된 CR review를 읽기 전용으로 검사했다. blocker가 있으면 단순 실패가 아니라 관련
ID와 필요한 조치를 출력했다.

notify status도 저장된 sent를 실제 완료로 해석하지 않았다. host가 메시지를 받았지만 아직 claim되지
않은 상태를 host_accepted_unclaimed, 일정 시간이 지난 경우를 stale_unclaimed로 보여줬다.
next --explain, wait/watch timeout 설명, role mismatch 경고와 CR review queue 필터도 이 시기에 추가됐다.

여기서 배운 점은 명확했다. 협업 시스템에서는 “아무 일도 일어나지 않는 상태”도 설명할 수 있어야 한다.

RC5: finished 의미 세분화

RC4 운영에서 가장 큰 의미적 문제가 발견됐다. 검증 agent가 테스트를 끝내고 결함을 발견한 경우,
작업 lifecycle은 정상적으로 완료됐지만 결과는 실패다. 기존 모델에서는 둘 다 finished로만 보이거나,
작업 실행 자체가 실패한 failed와 혼동될 수 있었다.

RC5는 완료 결과를 다음과 같이 구조화했다.

pass
fail
conditional
inconclusive
unspecified

여기에 후속 작업을 막아야 하는지 나타내는 completion_blocking과 관련 CR을 연결했다. 이 구분으로
“검증 수행은 완료됐지만 발견된 문제는 해결되지 않음”을 한 상태에서 표현할 수 있게 됐다.

Git commit 증거도 문자열에서 검증 대상으로 바뀌었다. finish --commit은 로컬에서 실제 commit
object로 해석되는지 확인하고 canonical ID를 저장한다. 외부 저장소나 아직 fetch되지 않은 reference처럼
확인할 수 없는 경우에는 명시적인 override와 사유가 있어야 한다.

잘못된 완료 commit은 기존 row를 수정하지 않고 append-only correction으로 보강했다. schema 호환성과
package version도 분리했다. Baton이 작업을 실행할 수 있는지는 package 문자열이 아니라 schema와
migration compatibility로 판단하게 했다.

RC6와 RC7: 자동 추론보다 명시적 복구

RC4 피드백에서 실제 구현은 끝났지만 CR implementation link가 누락된 사례가 있었다. 내용이 비슷하다는
이유로 migration이 기존 handoff를 자동 연결하면 잘못된 작업을 공식 구현으로 채택할 위험이 있다.

RC6는 reviewer가 cr link-handoff로 기존 finished handoff를 명시적으로 채택하는 경로를 만들었다.
대상은 정확한 cr:<CR-ID> source reference, 검증 가능한 commit 증거, non-blocking 결과를 모두 만족해야
했다. 채택은 권한과 감사 event를 요구했고 반복 실행에 안전했다.

RC6의 2시간 운영 피드백에서는 또 다른 표현 문제가 나왔다. 누적된 blocking 결과 수가 현재 해결되지
않은 blocker 수처럼 보였다. 이미 CR이 구현된 과거 결함까지 현재 장애처럼 읽힐 수 있었다.

RC7은 blocking 결과를 다음 context로 분리했다.

  • open CR이 연결된 결과
  • 모든 CR이 구현된 결과
  • rejected, cancelled, superseded처럼 구현되지 않은 terminal CR이 연결된 결과
  • outcome CR이 없는 결과

기존 집계 값은 하위 호환을 위해 유지하되 누적 감사 값이라는 의미를 문서화했다. CR implementation 채택
후보와 단순 관련 handoff도 분리하고, 현재 CR review claimant와 과거 claimant projection도 나눴다.

이 단계에서 Baton은 데이터를 더 많이 저장하는 것보다 같은 숫자가 무엇을 의미하는지를 더 중요하게
다루기 시작했다.

RC8과 RC9: 메시지 전달은 작업 소유권이 아니다

Codex task 간 메시지를 보낼 수 있어도 host acceptance는 수신 agent의 관찰, claim 또는 실행 성공을
보장하지 않는다. RC7 운영에서는 메시지가 전달된 뒤 active turn이 정체되거나, CR implementation
handoff가 기존 dependency graph만으로는 알림 후보에 나타나지 않는 사례가 확인됐다.

RC8은 notification에 delivery_attempt를 추가하고, 실제 scheduling predecessor가 없는 ready handoff도
notify candidates로 조회할 수 있게 했다. 알림을 위해 가짜 dependency를 만들 필요가 없어졌다.

최초 알림이 stale 상태가 된 경우에는 같은 recipient session으로 단 한 번만 recovery delivery를 허용했다.
수신 session이 활성 상태인지, 이미 다른 작업을 소유하고 있지 않은지, target shift가 활성 상태인지,
stale 기준을 넘겼는지를 검사했다. 실패한 recovery도 한도를 소비하게 해 반복 broadcast를 막았다.

RC8 피드백에서는 notification 기록이 쌓이자 최근 이력을 보기 어렵다는 문제가 나왔다. RC9은 조회
기능을 다듬었다.

  • 최신 ID 우선 기본 출력
  • 명시적인 --limit
  • 배타적 --after-id, --before-id cursor
  • --recovery-only 필터
  • 필요할 때 사용하는 --order oldest
  • text와 JSON에 동일하게 적용되는 필터 조합

RC9은 schema를 바꾸지 않았다. 저장 구조가 아니라 사람이 감사 기록을 읽는 방법을 개선한 릴리스였다.

Stable 0.6.0: 하나의 실패에는 하나의 CR만 존재하지 않는다

RC9의 2시간 운영에서 한 validation completion이 서로 독립적인 blocker 두 건을 발견하는 사례가 나왔다.
단일 outcome_cr_id만 저장하면 나머지 CR은 본문이나 사람의 기억에 의존해야 했다. 첫 CR이 구현됐다는
이유로 전체 blocker가 해결된 것처럼 보일 수도 있었다.

Stable의 schema 15는 하나의 completion에 ordered CR 관계를 여러 개 저장한다. finish --outcome-cr
반복해 알려진 blocker를 모두 연결하고, 완료 후 다른 blocker가 발견되면 권한 있는 reviewer가
handoff outcome-cr-link로 관계를 추가한다. 첫 CR은 기존 소비자를 위해 compatibility projection으로
남긴다.

blocking 결과는 연결된 모든 CR이 implemented일 때만 해결된 것으로 해석한다. 일부 CR만 구현됐거나
하나라도 terminal-unimplemented 상태라면 blocker는 유지된다.

RC9에서 발견된 두 번째 문제는 host가 메시지를 수락한 뒤 receiver execution이 실패하거나 취소된 사실을
Baton에 남길 수 없다는 점이었다. Stable은 notify observe를 추가했다. 이 기록은 고정된 result와 reason
class만 받으며 raw host error, prompt 또는 secret을 저장하지 않는다. 또한 delivery, recovery allowance,
claim ownership, handoff lifecycle을 바꾸지 않는다. 관찰은 관찰로만 남긴다는 경계다.

끝까지 신경 쓴 다섯 가지

1. Append-only 감사 이력

완료 증거 정정, 취소 후 replacement, CR 구현 채택, notification recovery, 완료 후 outcome CR 연결은
모두 과거 row를 다시 쓰지 않고 새 관계나 event를 추가한다. 운영자가 “현재 값”뿐 아니라 “왜 이 값이
됐는지”를 따라갈 수 있어야 했다.

2. 보수적인 migration

새 schema는 additive migration으로 적용하고 기존 row를 보존했다. historical intent를 추측해서 role이나
CR 관계를 자동 변경하지 않았다. migration 전에는 프로젝트를 멈추고 preflight를 수행하며 validated
backup을 만든 뒤 transaction과 integrity check를 통과해야 했다.

3. 프로젝트별 독립성

pipx로 설치된 실행 파일은 공유하지만 DB, shift, waiter, agent identity와 CR body는 프로젝트의
.baton/에 남는다. Git 저장소의 존재나 폴더 이름에 의존하지 않고 marker를 기준으로 프로젝트 경계를
찾는다. 여러 worktree가 하나의 논리 프로젝트라면 명시적으로 같은 control DB를 사용한다.

4. 권한과 agent identity의 분리

권한은 role에 있고 claim ownership은 concrete agent identity에 있다. 취소 결정은 제한된 role이 내리되,
진행 중 작업의 정상 취소는 원 claimant가 확인한다. 메시지를 받은 agent도 Baton claim에 성공하기 전에는
작업 소유자가 아니다.

5. 문서도 릴리스 산출물

Agent가 CLI help와 guide를 읽고 행동하기 때문에 문서 차이는 곧 실행 차이다. RC5부터 구현, CLI help,
README, schema, upgrade guide와 bundled guide를 릴리스 직전에 다시 대조하는 절차를 필수 gate로 두었다.
코드 테스트가 모두 통과해도 문서가 실제 동작과 다르면 배포를 중단하도록 했다.

RC 번호가 9까지 올라간 이유

RC 번호가 늘어난 이유는 동일한 결함을 반복해서 고친 것이 아니라, 실제 프로젝트에서 한 문제를 해결한
뒤 다음 경계가 드러났기 때문이다.

retry 추적
  -> upgrade 중단점
  -> 완료 결과 의미
  -> 누락된 관계 복구
  -> 현재 blocker 해석
  -> 메시지 recovery
  -> 감사 이력 조회
  -> 복수 blocker와 receiver-side 관찰

각 단계는 운영 피드백 문서로 먼저 기록하고, 재현 가능한 문제인지, 기존 정책으로 해결 가능한지,
schema 변경이 필요한지를 구분했다. 즉시 고쳐야 할 데이터 안전 문제와 다음 유지보수로 미룰 수 있는
표현 문제도 분리했다.

Stable 승격 기준은 기능 개수가 아니었다. 대표 프로젝트에서 다음 문제가 더 이상 관찰되지 않는지가
기준이었다.

  • 데이터 유실과 migration 호환성 실패
  • dependency 순서 역전과 중복 claim
  • CR body 무결성 손상
  • blocking 결과의 잘못된 해결 판정
  • notification 중복 recovery와 감사 기록 충돌
  • 구현, help, 문서와 agent guide의 불일치

Stable 이후 남은 과제

Stable은 완성의 의미가 아니라 호환성 계약을 지키기 시작하는 지점이다. 실제 운영 피드백에서는 당장
workflow를 막지는 않지만 다음 유지보수에서 개선할 항목도 확인됐다.

  • custom planner role이 새 권한을 빠르게 비교할 수 있는 read-only 진단
  • blocking handoff 수, 연결된 unresolved CR link 수, unique CR 수의 명확한 구분
  • 복수 CR 환경에서 첫 CR만 보여주는 compatibility projection의 의미 명시
  • queue empty와 프로젝트 단계 완료를 구분하는 milestone 정책
  • milestone을 사용하더라도 무기한 동작하지 않게 하는 유한 shift 경계

특히 milestone은 시간 제한을 대체하지 않는다. 앞으로의 방향은 milestone 완료, shift deadline, 명시적
stop 중 먼저 발생한 사건을 실행 경계로 삼는 것이다. queue가 비었다고 완료를 추측하지 않고, 남는
시간을 채우기 위해 등록되지 않은 테스트를 시작하지 않는 것이 핵심이다.

이거 외에 baton은 role 당 하나의 에이전트로 구분을 하면 자신의 경계에 맞게 작업을 하지만, role 1개에 2개 이상의 에이전트로 구성을 하면 스스로를 구분할 수단이 흐려지는 문제가 있다. 이러한 부분을 해결하기 위한 부분을 고민중에 있다. GPT-5.6 sol 의 머리로는 아직 모호한 해결을 제시중이다. 애초에 바톤과 같은 기능을 구상할때도 GPT 는 해결책을 제시하지 못하였고 그냥 OpenAI에 공시한 대로 하자고만 했었으니...

근데, role 당 1개의 에이전트로 구성을 해도 상관은 없다. 처음 정의한 롤을 기준으로 작엽 경계(하지말아야 할 것과 해도 되는 것)이 구분이 되어 있기 때문에 role 내에 여러 에이전트로 나누려면 다시 세부 role 나누는 것이 맞기 때문이다. 이게 귀찮다면 결국 서브 에이전트를 쓰는 것이 가장 적합한데, 이 경우는 굳이 바톤을 쓸 필요가 없다.

마치며

Baton을 만들면서 가장 크게 바뀐 관점은 agent 협업을 메시지 전달 문제로만 보면 안 된다는 점이었다.
메시지는 작업을 알려줄 뿐이고, 실제 계약은 DB에 기록된 scope, dependency, claim, outcome, CR, Gate와
감사 event다.

RC2에서 Stable까지 Baton은 “다음 agent에게 일을 넘기는 도구”에서 “여러 agent가 서로 다른 시점에
접속해도 같은 프로젝트 상태를 읽고 안전하게 이어갈 수 있는 도구”로 이동했다. 앞으로 기능을 추가할
때도 같은 기준을 유지해야 한다. 자동화가 더 많은 결정을 하게 만드는 것보다, 결정의 주체와 근거를
명확히 남기고 잘못된 추론을 거부하는 것이 Baton에 더 잘 맞는다.

Recent posts