에이전트의 예산 확인은 Race Condition에 취약합니다. TypeScript로 예약 원장(Reservation Ledger)을 구축해
요약
에이전트가 예산을 확인할 때 발생할 수 있는 Race Condition 문제를 지적하며, 이를 해결하기 위해 '예약 원장(Reservation Ledger)' 구축 방법을 제시합니다. 이 패턴은 작업 지시 전에 필요한 용량을 확보하고 나중에 정산하는 방식으로 재정적 안정성을 높입니다.
핵심 포인트
- 에이전트의 예산 확인 과정은 Race Condition에 취약할 수 있습니다.
- 해결책은 '작업 전 예약' 후 '신뢰성 있는 영수증을 통한 정산'입니다.
- 예약 원장은 사용 가능 용량(available)과 활성 예약(active holds)을 추적합니다.
- 이 패턴 적용 시, 예상 비용 외에 상한선 설정 및 다른 과금 작업을 고려해야 합니다.
두 에이전트 호출이 예산 확인 절차를 거칩니다. 둘 다 동일한 금액을 사용할 수 있는 권한을 얻습니다.
남은 단위는 100개입니다. Call A가 60개를 원하고, Call B도 60개를 원합니다. 각 호출은 완료되기 전에 잔액을 확인합니다. 둘 다 통과합니다. 나중에 대시보드를 보니 120이 100보다 크다는 것을 알게 됩니다. 관찰 가능성은 훌륭하지만, 회계 처리가 약간 늦었습니다.
유용한 경계는 작업 지시 전에 예약 용량을 확보한 다음, 신뢰할 수 있는 영수증을 통해 정산하는 것입니다.
이 작은 TypeScript 코드는 API 키나 모델, 실제 청구서 없이도 Race Condition을 재현할 수 있게 해줍니다. 단위는 합성된 값이며, 달러나 제공업체 토큰 가격이 아닙니다.
확인 절차가 이미 오래되었습니다 (Stale)
여기에 테스트 환경(fixture)에서 가져온 의도적으로 잘못된 버전이 있습니다:
let naiveSpent = 0;
async function naive(cap: number) {
if (naiveSpent + cap > 100) return false;
...
두 호출 모두 계속 실행되기 전에 0을 읽습니다. 최종 청구액은 120이 됩니다.
이를 재현하기 위해 두 개의 CPU 스레드가 필요하지 않습니다. 비동기적인 간격만으로 충분합니다. 확인(check)과 예약(reservation)을 동기적으로 유지하면 단일 원장 인스턴스 내의 이 특정 간격을 막을 수 있습니다. 이는 별도의 워커를 조정하는 것은 아닙니다.
잔액을 예약 원장으로 전환하기
세 가지 수량(quantity)을 추적합니다:
available = limit - spent - active holds
새로운 호출은 비동기 작업이 시작되기 전에 자신의 용량을 예약합니다. 정산(Settling)은 예약을 실제 지출로 전환합니다. 사용되지 않은 용량은 다시 사용 가능해집니다.
테스트 환경에서 A가 100 중 60을 예약합니다. B의 추가 60 요청은 거부됩니다. A의 영수증이 35를 보고하면, 원장에는 35가 지출되었고, 활성 예약(active hold)은 없으며, 65가 사용 가능하게 됩니다.
캡(cap)은 강제하거나 안전하게 의존할 수 있는 경계여야 합니다. 모델이 예상하는 최종 청구액은 경계가 아닙니다. 이 패턴을 돈에 적용하기 전에, 알려진 입력 비용을 계산하고, 출력의 상한선을 설정하며, 다른 과금 대상 작업을 고려해야 합니다. 만약 제공업체가 캡보다 더 많은 금액을 청구할 수 있다면, 이 장난감(toy)은 확실한 재정적 한계를 보장할 수 없습니다.
reserve의 중요한 부분은 작습니다:
if (cap > this.snapshot().available) return false;
this.#holds.set(id, { cap, state: 'held' });
return true;
용량 확인과 홀드 사이에 await가 없습니다. 완성된 클래스는 또한 유효하지 않은 정수, 빈 키, 0의 캡, 그리고 다른 캡을 가진 키의 재사용을 거부합니다.
동일한 활성 키를 다시 시도한다고 해서 두 번 예약되는 것은 아닙니다. 이것이 회계적 Idempotency(멱등성)입니다. 이것은 두 번 디스패치할 권한이 아닙니다. 별도의 디스패치 소유자와 도구 자체의 Idempotency 계약이 여전히 중요합니다.
Unknown은 상태이지, 환불 정책이 아닙니다
호출이 시간 초과됩니다. 제공업체가 작업을 수행했나요? 아직 알 수 없습니다.
원장은 예약을 unknown으로 표시하고 모든 60 단위를 홀드된 상태로 유지합니다. 사용 가능한 용량은 40으로 유지됩니다. 타이머가 작동했다고 해서 홀드를 해제하지 않습니다.
신뢰할 수 있는 영수증(receipt)이 unknown 예약을 정산할 수 있습니다. 동일한 정산을 반복하는 것은 무해합니다. 금액을 변경하는 것은 거부됩니다. 해제되거나 정산된 키는 새로운 작업을 승인할 수 없습니다.
releaseBeforeDispatch는 의도적으로 이름이 붙여졌습니다. 호출자는 디스패치(dispatch)가 시작된 적이 없음을 증명해야 합니다. 이 원장(ledger)은 그러한 증거를 제공하지 않습니다. 따라서 일반적인 타임아웃 핸들러나 요청을 보낸 후의 finally 블록에서 이를 호출해서는 안 됩니다.
영수증(receipt)이 한도를 초과하면, 장난감(toy)은 정산(settlement)을 거부하고 보유액(hold)을 유지합니다. 이것은 규모가 큰 청구서에 대한 올바른 회계 처리라기보다는 에스컬레이션 신호입니다. 프로덕션 시스템은 실제 초과분을 기록하고, 추가 입장을 차단하거나 검토하며, 예산을 조정해야 합니다. 한도가 청구액이었다고 조용히 가정하는 것은 대시보드를 은행 계좌보다 더 좋아 보이게 만들 것입니다.
빌드 실행하기
공개 소스: bobbyhalljr/agent-budget-reservations.
Node.js 22.20.0 이상을 사용하세요. 의존성이나 API 키가 필요하지 않습니다.
git clone https://github.com/bobbyhalljr/agent-budget-reservations.git
cd agent-budget-reservations
npm test
이 스크립트는 Node의 타입 스트리핑(type stripping)을 사용하여 TypeScript로 실행됩니다. Node 문서는 지원되는 구문과 그 한계를 설명합니다; 타입 스트리핑은 파일을 실행할 뿐, 타입 검사(type-check)를 수행하지는 않습니다.
실행 결과는 20개의 이름이 지정된 체크 항목을 출력한 후, 다음의 정확한 요약 라인으로 끝납니다:
20/20 checks passed
naive: admitted=2, spent=120, limit=100
reserved: admitted=1, held=60, available=40
...
이 테스트 환경(fixture)은 입금 결과와 최종 잔액을 모두 단언합니다. 테스트는 변경된 키, 반복되는 영수증, 취소, 터미널 키, 안전하지 않은 금액, 누락된 예약, 알려지지 않은 결과, 그리고 한도 초과 영수증 등을 다룹니다. 이는 제공업체의 청구 행동에 대한 증거가 아니라 결정론적인 로컬 체크입니다.
이 장난감이 멈추는 지점
이것은 신뢰할 수 있는 호출자(trusted callers)를 가진 단일 프로세스를 위한 인메모리 원장입니다. 재시작하면 보유액이 사라집니다. 별도의 인스턴스는 별도의 예산을 가집니다. 인증된 테넌트 경계, 영수증 검증기, 영구 디스패치 소유권, 가격 책정 엔진, 데이터베이스 트랜잭션, 또는 조정 워커가 없습니다.
여러 워커를 위해 예약 전환(reservation transition)을 영속적이고 원자적으로 만드세요. 하나의 데이터베이스 설계는 계정 행(account row)을 잠그고, 사용 가능한 용량을 확인한 후, 고유 키가 지정된 예약을 삽입하고, 보유 용량(held capacity)을 같은 트랜잭션 내에서 업데이트하는 것입니다. PostgreSQL의 행 잠금 문서는 충돌하는 행 작업이 트랜잭션이 끝날 때까지 기다리는 방법을 설명합니다. 이 예제(toy)는 해당 데이터베이스 설계를 구현하거나 테스트하지 않습니다. 외부 모델 호출 중에는 데이터베이스 잠금을 유지하지 마십시오.
알 수 없는 보유량(Unknown holds) 또한 운영상의 거점(operational home)이 필요합니다: 나이, 소유자, 영수증 조회, 그리고 에스컬레이션 경로가 필요합니다. 만료 시간은 조정(reconciliation)을 트리거할 수 있습니다. 하지만 이는 제공자가 아무런 작업을 수행하지 않았음을 증명할 수는 없습니다.
저는 실제 작업을 수행하는 AI 직원들을 중심으로 Roster를 구축하고 있습니다. 실제 작업은 공유 자원을 소비합니다. 프롬프트에 인쇄된 예산은 유용한 컨텍스트입니다. 배포(dispatch) 전의 예약이 애플리케이션 경계(application boundary)입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

