`QueryEngine`은 Claude Code의 핵심 실행 엔진으로, LLM(Large Language Model, 대형 언어 모델) API 호출, 에이전트 루프(agent loop) 관리, 스트리밍(streaming) 응답 처리를 단일 클래스에서 조율한다. SDK(Software Development Kit) 헤드리스 경로와 REPL(Read-Eval-Print Loop) 인터랙티브 경로 양쪽에서 하나의 대화 세션을 대표하는 객체로 인스턴스화된다.
- **역할**: LLM API 호출, 에이전트 루프 관리, 스트리밍 응답 처리의 중앙 엔진
- **위치**: `src/QueryEngine.ts` (1,295 lines)
- **설계 원칙**: 대화 하나 당 인스턴스 하나. `submitMessage()` 호출마다 새 턴(turn)이 시작되며, 메시지 히스토리·파일 캐시·사용량 집계는 턴 간에 유지된다.
setSDKStatus?: (s: SDKStatus) => void // SDK 상태 콜백
abortController?: AbortController // 외부에서 주입 가능한 취소 컨트롤러
orphanedPermission?: OrphanedPermission // 고아(orphaned) 권한 처리
snipReplay?: (msg, store) => ... // HISTORY_SNIP 기능 콜백 (주입식)
}
```
**주요 설계 결정**: `customSystemPrompt`가 존재하면 기본 시스템 프롬프트를 완전히 대체하고, `appendSystemPrompt`는 기본 프롬프트 뒤에 추가된다. SDK 호출자가 커스텀 프롬프트와 `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE`를 함께 설정했을 때는 메모리 메카닉스 프롬프트가 자동 주입된다.
### `QueryEngine` 클래스 내부 상태
```typescript
class QueryEngine {
private config: QueryEngineConfig
private mutableMessages: Message[] // 턴 간 누적 메시지 히스토리
private abortController: AbortController // 인터럽트 신호
private discoveredSkillNames: Set<string> // 턴-스코프 스킬 발견 추적
private loadedNestedMemoryPaths: Set<string>
private readFileState: FileStateCache // 언두 가능한 파일 상태
}
```
---
## 4. 실행 흐름
### 4.1 설정 조립 (Config Assembly)
`submitMessage()` 진입 시 `config`에서 구조 분해로 파라미터를 추출한다. `discoveredSkillNames`는 각 턴 시작 시 초기화되어 탐색된 스킬이 다음 턴으로 누적되지 않도록 한다. `setCwd(cwd)`를 호출해 작업 디렉터리를 전역 셸 상태와 동기화한다.
`fetchSystemPromptParts()`는 코디네이터 모드(`COORDINATOR_MODE`) 기능 플래그가 활성화된 경우 `getCoordinatorUserContext()`를 통해 `userContext`를 보강한다. 이 함수는 dead-code elimination(데드코드 제거)을 위해 `feature()` 조건부 `require()`로 지연 임포트된다.
### 4.3 메시지 정규화
`processUserInput()`이 원시 프롬프트(문자열 또는 `ContentBlockParam[]`)를 처리해 내부 `Message` 타입으로 변환한다. 슬래시 커맨드가 포함된 경우 이 단계에서 실행된다. 처리 후 `messagesFromUserInput`이 `this.mutableMessages`에 추가된다.
처리 결과인 `shouldQuery`가 `false`이면(로컬 커맨드만 있는 경우) API 호출 없이 즉시 결과를 반환한다.
### 4.4 사전 트랜스크립트 기록
```typescript
// API 응답 전에 사용자 메시지를 즉시 저장
if (persistSession && messagesFromUserInput.length > 0) {
프로세스가 API 응답 전에 종료되더라도 `--resume`이 가능하도록 사용자 메시지를 먼저 기록한다. 베어 모드(bare mode, `--bare` 플래그)에서는 약 4~30ms의 지연을 피하기 위해 fire-and-forget으로 처리한다.
### 4.5 API 호출 (`query()` 함수)
```typescript
for await (const message of query({
messages,
systemPrompt,
userContext,
systemContext,
canUseTool: wrappedCanUseTool,
toolUseContext: processUserInputContext,
fallbackModel,
querySource: 'sdk',
maxTurns,
taskBudget,
})) { ... }
```
`query()`는 `src/query.ts`에 정의된 AsyncGenerator 함수로, 내부 `queryLoop()`를 호출한다. 루프 내에서 `deps.callModel()`(실제로는 `claude.ts`의 Anthropic 베타 API 클라이언트)을 통해 스트리밍 응답을 수신한다.
### 4.6 스트리밍 응답 처리
`query()`가 yield하는 메시지 타입별 처리:
| 메시지 타입 | 처리 방식 |
|---|---|
| `assistant` | `mutableMessages`에 추가, `normalizeMessage()` 후 yield |
`cost-tracker.ts`의 `addToTotalSessionCost()`는 모델별로 토큰(입력/출력/캐시 읽기/캐시 생성)과 USD 비용을 누적 집계한다. 어드바이저(advisor) 모델이 있는 경우 재귀 호출로 어드바이저 사용량도 합산한다. 세션 비용은 `saveCurrentSessionCosts()`를 통해 프로젝트 설정에 저장되며 재개 시 `restoreCostStateForSession()`으로 복원된다.
사용자 메시지마다 파일 히스토리 스냅샷을 생성해 언두(undo) 기능을 지원한다. `readFileState` (`FileStateCache`)는 엔진 인스턴스 수명 동안 유지되며, `ask()` 래퍼가 완료 후 `setReadFileCache(engine.getReadFileState())`를 호출해 상위 컨텍스트로 전파한다.
### 4.11 압축/컴팩션 트리거
`query.ts`의 `queryLoop()` 내에서 매 API 호출 전에 압축 조건을 확인한다:
- **자동 압축 (autocompact)**: 토큰이 임계값을 초과하면 `deps.autocompact()`를 호출해 대화를 요약본으로 압축
- **스닙 (HISTORY_SNIP)**: 특정 메시지 패턴을 감지해 히스토리 일부를 잘라냄
- **반응형 압축 (REACTIVE_COMPACT)**: API에서 `prompt_too_long` 오류 수신 시 사후 압축 시도
- **컨텍스트 콜랩스 (CONTEXT_COLLAPSE)**: 컨텍스트 창 초과 전에 단계적 축소
압축 완료 후 `compact_boundary` 시스템 메시지가 yield되고, `QueryEngine`은 이를 받아 `mutableMessages`에서 압축 전 메시지를 GC(garbage collection, 가비지 컬렉션) 해제한다.
`fetchSystemPromptParts()`는 `getSystemContext()`와 `getUserContext()`를 내부적으로 사용한다. 이 함수들은 비용이 높은 파일 시스템 접근(CLAUDE.md, 플러그인, 메모리 디렉터리 등)을 한 번만 수행하고 결과를 메모이즈(memoize)한다. `submitMessage()` 내에서 매 턴마다 호출하더라도 성능에 영향을 주지 않는다.
### 5.2 Dead Code Elimination (`bun:bundle` 기능 플래그)
`feature()` 함수는 `bun:bundle`의 트리 쉐이킹(tree-shaking) 경계 역할을 한다. 플래그가 비활성화된 빌드에서는 해당 모듈 전체가 번들에서 제외된다. `QueryEngine.ts`와 `query.ts` 모두 이 패턴을 사용해 `HISTORY_SNIP`, `COORDINATOR_MODE`, `REACTIVE_COMPACT`, `CONTEXT_COLLAPSE` 등의 실험적 기능을 격리한다.
`MessageSelector.tsx`는 React/ink를 의존하므로 최상위 임포트 시 테스트 셔드(shard)의 모듈 초기화 순서를 깨뜨릴 수 있다. `require()` 지연 임포트로 실제 사용 시점까지 로딩을 미룬다. `snipReplay` 콜백을 설정으로 주입하는 것도 같은 이유다: 기능 플래그 문자열이 `QueryEngine.ts`에 직접 포함되면 제외 문자열 검사를 통과하지 못한다.
### 5.4 `FileStateCache`와 언두 기능
`FileStateCache`는 파일 경로를 키로 파일 내용의 과거 상태를 저장한다. `ask()` 편의 래퍼는 엔진 생성 시 `cloneFileStateCache(getReadFileCache())`로 독립적인 캐시 복사본을 만들고, 완료 후 `setReadFileCache(engine.getReadFileState())`로 변경 사항을 상위에 전파한다. 이 구조가 도구 실행이 변경한 파일을 다음 턴에서 정확히 읽을 수 있게 하고, 언두 기능의 기반이 된다.
### 5.5 도구 결과 예산 및 콘텐츠 교체
`applyToolResultBudget()`은 누적된 도구 결과의 총 크기가 한 메시지당 예산을 초과하면 오래된 결과를 플레이스홀더로 교체한다. 교체 기록은 `recordContentReplacement()`를 통해 세션 스토리지에 저장되어 재개 시 복원이 가능하다. 마이크로컴팩트(cached microcompact)는 `tool_use_id` 기준으로만 동작하므로 콘텐츠 교체와 독립적으로 합성된다.
### 5.6 고아 권한 처리 (Orphaned Permission)
SDK 호출에서 이전 세션의 미결 권한 요청이 있을 경우, `submitMessage()` 첫 호출 시 단 한 번 `handleOrphanedPermission()`이 실행된다. `hasHandledOrphanedPermission` 플래그로 중복 처리를 방지한다.
### 5.7 베어 모드와 트랜스크립트 전략
`isBareMode()`가 true이면 사용자 메시지 트랜스크립트 기록을 fire-and-forget으로 처리한다. 스크립트 호출은 `--resume` 후 재개가 필요 없으므로 약 4~30ms의 디스크 I/O 지연을 절약한다. 비대화형(non-interactive)이지만 코워크(cowork) 환경에서는 `CLAUDE_CODE_EAGER_FLUSH` 또는 `CLAUDE_CODE_IS_COWORK` 환경 변수로 `flushSessionStorage()`를 강제로 동기 호출한다.
`ask()`는 단발성(one-shot) 사용을 위한 편의 래퍼다. 내부적으로 `QueryEngine`을 생성하고 `submitMessage()`를 위임한다. `finally` 블록에서 파일 상태 캐시를 항상 상위로 전파한다. `HISTORY_SNIP` 기능이 활성화된 경우 `snipReplay` 콜백을 주입해 기능 플래그 문자열이 `QueryEngine.ts`에 포함되지 않도록 한다.
---
## 7. 결과 유형 (SDKResultMessage 서브타입)
| 서브타입 | 원인 |
|---|---|
| `success` | 정상 완료 (`end_turn` 또는 tool_result 후 응답) |
| `error_max_turns` | `maxTurns` 초과 |
| `error_max_budget_usd` | `maxBudgetUsd` 초과 |
| `error_max_structured_output_retries` | 구조화 출력 재시도 한도 초과 |
| `error_during_execution` | 비정상 종료 (API 오류, 예기치 않은 stop_reason 등) |
`error_during_execution`에는 진단 접두사(`[ede_diagnostic]`)와 해당 턴 내에서 발생한 인메모리(in-memory) 에러 로그가 `errors[]`에 포함된다. 워터마크(watermark) 기반으로 이전 턴의 에러가 포함되지 않도록 한다.
---
## 8. 관련 문서
- **상위 개요**: [요청 생명주기](../level-1-overview/request-lifecycle.md) — `ask()`가 어떻게 호출되는지