Skip to content

[Epic] 프로젝트 단위 AI 사용량 예산 상한 (budget cap) — 기간 원장 + 디스패치/턴 리스 게이트 #645

Description

@moncher-dev

개요

프로젝트 단위 AI 사용량 예산 상한(budget cap) 도입. 운영자가 "이 프로젝트는 월 N 토큰(또는 환산 금액)까지"를 설정하면, 오케스트레이터가 기간 누적 사용량을 추적하고 상한 도달 시 신규 디스패치와 진행 중인 런의 다음 턴을 멈춘다.

한 문장: 토큰은 지금도 세고 있지만, 아무도 멈추지 않는다. 세는 것에 기간 원장 + 게이트를 얹는다.

배경

사업화 시 AI 크레딧(모델 API 비용)은 고객 부담으로 설계됨. 고객이 통제할 수 없는 변동 청구서는 이탈의 1순위 원인이므로, 고객이 스스로 상한을 정하고 시스템이 그것을 강제하는 기능이 사업화의 전제 조건이다. 동시에 폭주하는 단일 런(무한 루프, 거대 컨텍스트)에 대한 안전장치 역할도 한다.

현재 상태 (2026-08-26 조사)

집계 경로는 이미 존재한다:

  • 런타임 → 워커: agent.tokenUsageUpdated 이벤트 (packages/core/src/runtime/events.ts), 워커가 절대값/델타 처리 (packages/worker/src/index.ts extractAbsoluteTokenUsage, 스펙 §13.5 준수)
  • 워커 → 오케스트레이터: 채널 이벤트의 tokenUsage + WORKSPACE_RUNTIME_DIR/token-usage.json 아티팩트 (packages/worker/src/token-usage.ts)
  • 오케스트레이터: OrchestratorRunRecord.tokenUsage (런 단위) + snapshot-builder.ts의 이슈별 누적·codexTotals (전체 합계)
  • 표면: repo status, 대시보드, 컨트롤플레인 API에 토큰 수치 표시

없는 것:

  1. 상한 설정 자체가 없음. WorkflowAgentConfigmaxConcurrentAgents/maxTurns/재시도만 있고 (packages/core/src/workflow/config.ts), OrchestratorProjectConfig에도 예산 필드 없음. SYMPHONY_MAX_TOKENSdocs/configuration.md에 "legacy token budget context"로만 남아 있고 워커 시작 시 클리어됨.
  2. 기간 개념이 없음. 집계는 전부 all-time 합계. "이번 달" 누적을 구할 수 없고, 재시작 후에도 이어지는 기간 원장이 없다.
  3. 게이트가 없음. 디스패치 루프(packages/orchestrator/src/service.ts ~L1455–1510)는 슬롯·상태별 동시성·재시도 유예·실패 억제만 검사한다. 런 안에서 세션을 멈추는 유일한 한도는 maxTurns뿐.
  4. Claude 런타임은 사용량을 보고하지 않음. agent.tokenUsageUpdated를 emit하는 곳은 runtime-codex뿐 (packages/runtime-codex/src/runtime.ts L353, L389). runtime-claudeusageagent.rateLimit 페이로드에 실어 보낼 뿐 토큰 이벤트를 내지 않는다 (packages/runtime-claude/src/events.ts). → Claude 런은 예산 집계에서 빠진다. 선행 과제.

설계 방향

  • 정책 위치: 프로젝트(workspace) 단위. 표현은 두 곳 — WORKFLOW.md front matter agent.budget.* (repo 정책) 과 OrchestratorProjectConfig.budget (운영자 오버라이드, standalone 프로젝트 모델과 정합). 트래커 비의존, core에 타입 정의.
  • 단위: 1차는 토큰. 금액은 cost_per_mtoken(입력/출력 분리) 환산값을 표시용으로만 제공 — 모델 가격표를 core가 소유하지 않는다.
  • 기간: period: monthly + anchor_day(청구 기준일), 확장 여지로 rolling_30d. 원장은 .runtime/orchestrator/workspaces/<id>/budget-ledger.json에 기간별 누적을 영속 (런 레코드 재집계 fallback 가능).
  • 게이트 지점 두 곳:
    1. 디스패치 루프 — 상태별 동시성 체크 옆에 예산 체크. 초과 시 후보 스킵 + 구조화 이벤트.
    2. 턴 리스 발급 — 워커는 매 턴 오케스트레이터에서 리스를 받는다([worker] acquired turn lease). 리스 발급을 거부하면 워커 코드 변경 최소로 진행 중 런도 다음 턴에서 멈춘다. 워커는 거부를 budget_exhausted 종료로 분류(비실패, 재시도 큐 진입 금지).
  • 동작 모드: action: pause | warn. 임계 이벤트 budget_threshold(80%), budget_exhausted(100%). 스냅샷 degraded 사유에 반영. 기간 경과 시 자동 해제, 운영자 수동 해제 명령 제공.
  • 런 단위 안전장치: max_tokens_per_run — 월 상한과 별개로 단일 런 폭주 차단.
  • 스펙 관계: 상위 스펙 §13.5는 집계 규칙만 정의하고 예산/게이트는 없다 → 명시적 확장으로 ADR 기록.

하위 이슈 후보

  • feat(runtime-claude): print-mode result.usageagent.tokenUsageUpdated emit (선행; 없으면 Claude 런은 예산 밖)
  • feat(core): BudgetPolicy 타입 + WORKFLOW.md agent.budget 파싱 + OrchestratorProjectConfig.budget 오버라이드
  • feat(orchestrator): 기간 원장(budget-ledger.json) — 런 tokenUsage 델타 누적, 기간 롤오버, 재시작 복원, 런 레코드 재집계 fallback
  • feat(orchestrator): 디스패치 게이트 + 턴 리스 발급 게이트, budget_threshold/budget_exhausted 구조화 이벤트, 스냅샷 degraded 사유
  • feat(worker): 리스 거부 → budget_exhausted 종료 분류(exit-classification), 재시도 미진입; max_tokens_per_run
  • feat(cli,dashboard,control-plane): 기간 사용량/상한/잔여율/환산금액 표시, repo config budget 설정·해제 명령
  • feat(extension-github-workflow): 예산 소진으로 일시정지 시 이슈/프로젝트 코멘트 통보 (GitHub 전용은 extension에 유지)
  • docs: ADR(스펙 확장 명시) + configuration.md + changeset + Docker E2E (상한 도달 → 디스패치 중단 → 기간 롤오버 후 재개 블랙박스)

순서

runtime-claude usage → core 타입 → 원장 → {디스패치 게이트, 리스 게이트} → 워커 종료 분류 → 표면(CLI/대시보드) → extension 통보 → docs/E2E

완료 기준

  • 프로젝트에 월 상한을 설정하면 Codex·Claude 런타임 모두의 사용량이 원장에 누적되고, 100% 도달 시 신규 디스패치와 진행 중 런의 다음 턴이 멈춘다 (Docker E2E 블랙박스 통과)
  • 오케스트레이터 재시작 후에도 기간 누적이 유지된다
  • 상한 미설정 프로젝트는 동작 변화 없음 (회귀 없음)
  • repo status/대시보드에서 "이번 기간 사용량 / 상한 / 잔여"를 확인할 수 있다
  • 스펙 확장 ADR 병합

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestepicTracking issue grouping multiple sub-issues

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions