QueryEngine.submitMessage()부터 while(true) 쿼리 루프, 스트리밍, 재시도, 컨텍스트 관리까지.
쿼리 엔진은 사용자 메시지를 받아 LLM 응답을 생성하기까지의 모든 과정을 관장합니다. 네 개의 레이어로 구성됩니다:
QueryEngine.submitMessage(): 프롬프트 검증, 시스템 프롬프트 구성, 모델 해석, 트랜스크립트 기록query() / queryLoop(): 도구 호출 루프 — 모델이 end_turn을 반환하거나 터미널 조건에 도달할 때까지 반복queryModel / callModel: Anthropic API 호출, withRetry()를 통한 재시도 및 폴백사용자 메시지부터 최종 응답까지의 전체 흐름을 시퀀스 다이어그램으로 추적합니다.
QueryEngine은 하나의 대화 세션에 대한 모든 상태를 보유합니다. 핵심 상태로 mutableMessages(대화 히스토리), abortController(취소 제어), totalUsage(누적 토큰 사용량)를 관리합니다.
submitMessage()의 흐름:
query() 호출로 실제 API 통신 시작// QueryEngine — submitMessage 흐름 (간소화)
async submitMessage(userInput: UserInput) {
const systemPrompt = await buildSystemPrompt()
this.mutableMessages.push(userMessage)
// 프로세스 종료 시에도 세션 재개 가능하도록 미리 기록
await writeTranscript(this.mutableMessages)
return await this.query(systemPrompt)
}
트랜스크립트를 API 호출 전에 기록하는 것은 의도적인 설계입니다. 프로세스가 API 응답 대기 중에 종료되더라도 사용자 입력이 보존되어 --resume으로 세션을 이어갈 수 있습니다.
queryLoop()는 쿼리 엔진의 심장부입니다. while(true) 루프로 동작하며, 각 반복의 결과를 State 타입으로 표현합니다.
State는 두 가지 범주로 분류됩니다:
Continue 전환 — 루프가 계속되는 7가지 이유:
max_output_tokens_escalate — 출력 토큰 한도 도달 시 더 높은 한도로 재시도max_output_tokens_recovery — 에스컬레이션 후 복구reactive_compact_retry — 컨텍스트 초과 시 압축 후 재시도collapse_drain_retry — 컨텍스트 붕괴 후 드레인 재시도stop_hook_blocking — 스톱 훅이 계속 진행을 요청token_budget_continuation — 토큰 예산 내에서 자동 계속needs_follow_up — 도구 호출 결과에 대한 후속 처리 필요Terminal 종료 조건: end_turn, 예산 소진, 치명적 오류, 사용자 중단 등으로 루프가 종료됩니다.
// query.ts — queryLoop 상태 타입
type State = {
messages: Message[]
toolUseContext: ToolUseContext
autoCompactTracking: AutoCompactTrackingState | undefined
maxOutputTokensRecoveryCount: number
hasAttemptedReactiveCompact: boolean
pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
stopHookActive: boolean | undefined
turnCount: number
transition: Continue | undefined // 다시 루프한 이유
}
// queryLoop — State 타입 (간소화)
type State =
| { kind: 'continue'; transition: { reason: ContinueReason } }
| { kind: 'terminal'; result: QueryResult }
// reason 필드 = 이전 반복이 종료 대신 계속을 결정한 이유
Anthropic API와의 통신은 SSE(Server-Sent Events) 스트리밍으로 이루어집니다. 스트림 청크가 도착하는 대로 재구성되어 사용자에게 실시간으로 표시됩니다.
주요 메커니즘:
대화가 길어지면 컨텍스트 윈도우를 초과할 수 있습니다. 쿼리 엔진은 5단계 파이프라인으로 컨텍스트를 자동 관리합니다:
applyToolResultBudget — 도구 결과의 크기를 예산 내로 잘라냄snipCompact — 오래된 메시지의 도구 결과를 요약으로 대체microcompact — 미세 압축: 공백 제거, 반복 패턴 축소contextCollapse — 컨텍스트 붕괴: 대화의 큰 블록을 요약으로 대체autoCompact — 자동 압축: LLM을 사용하여 전체 대화를 요약이 단계들은 점진적으로 적용됩니다. 가벼운 방법부터 시도하고, 그래도 부족하면 더 공격적인 압축을 수행합니다.
withRetry()는 API 호출 실패 시 지수 백오프(exponential backoff)를 적용하여 재시도합니다. 단순한 재시도를 넘어 여러 특수 상황을 처리합니다:
// services/api/withRetry.ts — 백오프 공식
export function getRetryDelay(
attempt: number,
retryAfterHeader?: string | null,
maxDelayMs = 32000,
): number {
if (retryAfterHeader) {
const seconds = parseInt(retryAfterHeader, 10)
if (!isNaN(seconds)) return seconds * 1000
}
const baseDelay = Math.min(
BASE_DELAY_MS * Math.pow(2, attempt - 1),
maxDelayMs,
)
const jitter = Math.random() * 0.25 * baseDelay
return baseDelay + jitter
}
handleStopHooks()는 모델이 end_turn을 반환했을 때 실행되며, 세 가지 카테고리의 훅을 처리합니다:
stop_hook_blocking 전환으로 루프 지속Fire-and-Forget 백그라운드 작업: 스톱 훅 처리 후 다음 작업들이 백그라운드에서 실행됩니다:
마지막 어시스턴트 메시지가 API 오류인 경우 스톱 훅을 건너뜁니다. 이유: 차단 훅이 재시도를 요청하면 재오류가 발생하고, 다시 스톱 훅이 실행되어 무한 루프에 빠질 수 있기 때문입니다.
SDK 경로에서는 토큰 예산이 설정되어 에이전트의 자동 계속을 관리합니다.
checkTokenBudget()이 3회 이상 연속으로 양쪽(입력+출력) 델타가 500토큰 미만이면 진전 없음으로 판단하고 조기 중단을 트리거이 메커니즘은 모델이 동일한 패턴을 반복하며 토큰을 낭비하는 것을 방지합니다.
submitMessage() → queryLoop() → queryModel/callModel → 스톱 훅 & 토큰 예산submitMessage()는 API 호출 전에 트랜스크립트를 디스크에 기록하여 프로세스 종료 시에도 세션 재개가 가능합니다queryLoop()의 State.transition.reason 필드는 이전 반복이 종료 대신 계속을 결정한 이유를 기록합니다withRetry()는 지수 백오프, 529 포그라운드 전용 재시도, 3회 연속 529 후 Opus 폴백, OAuth 갱신, 30분 캡 영구 모드를 지원합니다Q1. submitMessage()가 API 호출 전 트랜스크립트를 디스크에 쓰는 주된 이유는?
--resume으로 세션을 이어갈 수 있습니다.Q2. 3회 연속 529 응답 후 withRetry()가 발생시키는 동작은?
withRetry()는 현재 모델 대신 더 가벼운 모델로 폴백을 트리거합니다. 이는 서버 과부하 상황에서 사용자 경험을 유지하기 위한 전략입니다.Q3. State의 transition.reason 필드가 나타내는 것은?
transition.reason은 이전 반복이 왜 terminal이 아닌 continue를 선택했는지를 기록합니다. 7가지 Continue 이유(max_output_tokens_escalate, reactive_compact_retry 등) 중 하나가 됩니다.Q4. checkTokenBudget()이 체감 감소 조기 중단을 트리거하는 조건은?
Q5. 마지막 어시스턴트 메시지가 API 오류일 때 스톱 훅을 건너뛰는 이유는?