제출, 라우팅, 입력 분류, 메시지 구성, API 정규화 — 4단계 파이프라인.
사용자가 Enter를 누르면 텍스트가 Claude API에 도달하기까지 4단계 파이프라인을 통과합니다. 각 단계에서 입력이 분류, 확장, 변환되며, 최종적으로 API가 이해할 수 있는 정규화된 메시지 배열로 변환됩니다.
messages/handlePromptSubmit.ts → messages/processUserInput.ts → messages/processTextPrompt.ts → messages/normalizeMessagesForAPI.ts
4단계 파이프라인:
handlePromptSubmit은 사용자 입력을 처리하는 최초 진입점입니다. 큐 가드로 동시 제출을 방지하고, 붙여넣기된 참조를 확장합니다.
// messages/handlePromptSubmit.ts — 큐 가드 & 참조 확장
let isProcessing = false
async function handlePromptSubmit(rawInput: string): Promise<void> {
// 큐 가드: 이전 요청이 처리 중이면 무시
if (isProcessing) {
showWarning('이전 요청을 처리 중입니다...')
return
}
isProcessing = true
try {
// 붙여넣기 참조 확장: @파일경로 → 파일 내용 인라인
const expanded = expandPasteReferences(rawInput)
// 입력 분류로 전달
await processUserInput(expanded)
} finally {
isProcessing = false
}
}
사용자가 @src/main.ts 형태로 파일을 참조하면, 해당 파일의 내용이 인라인으로 확장됩니다. 이는 Claude에게 파일 전체를 보여주고 싶을 때 수동으로 복사-붙여넣기하는 수고를 덜어줍니다.
큐 가드는 단순 boolean 플래그입니다. finally 블록에서 반드시 해제해야 하며, 예외 발생 시 해제되지 않으면 이후 모든 입력이 차단됩니다.
processUserInput은 확장된 입력을 분석하여 3방향 라우팅을 수행합니다. 이미지 전처리, ultraplan 키워드 감지도 이 단계에서 처리됩니다.
// messages/processUserInput.ts — 3방향 라우팅
async function processUserInput(input: ExpandedInput): Promise<void> {
// 이미지 전처리: 붙여넣기된 이미지를 base64로 변환
const processed = await preprocessImages(input)
// 라우팅 1: 슬래시 커맨드
if (processed.text.startsWith('/')) {
return handleSlashCommand(processed)
}
// 라우팅 2: ULTRAPLAN 키워드 감지
if (shouldTriggerUltraplan(processed.text)) {
return launchUltraplan(processed)
}
// 라우팅 3: 일반 메시지 → 메시지 구성으로
await processTextPrompt(processed)
}
사용자가 이미지를 터미널에 붙여넣으면 자동으로 감지되어 base64로 인코딩됩니다. 지원 형식(PNG, JPEG, GIF, WebP)이 확인되고, 크기 제한(20MB)이 적용됩니다.
입력은 세 가지 경로 중 하나로 분기됩니다:
/로 시작하는 입력은 커맨드 핸들러로 전달됩니다메시지 구성 단계에서는 사용자 입력을 Claude API가 이해하는 메시지 구조로 변환합니다. processTextPrompt이 텍스트를 처리하고, createUserMessage가 최종 메시지 객체를 생성합니다.
// messages/processTextPrompt.ts — 메시지 구성
async function processTextPrompt(input: ProcessedInput): Promise<void> {
// 사용자 메시지 생성
const userMsg = createUserMessage({
text: input.text,
images: input.images, // base64 인코딩된 이미지
files: input.fileContents, // @참조로 확장된 파일 내용
})
// 합성 메시지: 컨텍스트 강화를 위한 추가 메시지
const syntheticMsgs = buildSyntheticMessages({
systemMemory: getSessionMemory(),
recentFiles: getRecentlyEditedFiles(),
gitStatus: getGitStatus(),
})
// 메시지 배열 구성
const messages = [...syntheticMsgs, userMsg]
// API 정규화로 전달
await sendToAPI(normalizeMessagesForAPI(messages))
}
합성(synthetic) 메시지는 사용자가 직접 입력하지 않았지만, 컨텍스트를 강화하기 위해 자동으로 생성되는 메시지입니다. 세션 메모리, 최근 편집 파일, git 상태 등이 합성 메시지로 주입됩니다.
Claude API의 메시지 형식은 하나의 메시지 내에 여러 콘텐츠 블록(텍스트, 이미지, 파일)을 포함할 수 있습니다. createUserMessage는 사용자의 텍스트, 붙여넣기 이미지, @ 참조 파일을 모두 하나의 메시지 내 별도 콘텐츠 블록으로 구성합니다.
normalizeMessagesForAPI는 내부 메시지 배열을 Claude API가 요구하는 형식으로 정규화합니다. 다중 패스로 처리됩니다.
// messages/normalizeMessagesForAPI.ts — 다중 패스 정규화
function normalizeMessagesForAPI(messages: InternalMessage[]): APIMessage[] {
let result = messages
// 패스 1: 연속된 같은 역할의 메시지를 병합
// API 제약: user/assistant가 교대로 와야 함
result = mergeConsecutiveSameRole(result)
// 패스 2: 빈 텍스트 블록 제거
result = removeEmptyTextBlocks(result)
// 패스 3: 도구 사용 결과를 올바른 위치에 배치
result = reorderToolResults(result)
// 패스 4: 컨텍스트 윈도우 초과 시 오래된 메시지 압축
result = compressIfOverLimit(result, contextWindowSize)
// 패스 5: 최종 API 형식 변환
return result.map(toAPIFormat)
}
단일 패스로 모든 정규화를 처리하면 복잡한 상호 의존성이 발생합니다. 예를 들어, 합성 메시지 삽입(패스 3)이 연속 동일 역할 제약(패스 1)을 위반할 수 있습니다. 다중 패스는 각 변환을 독립적으로 적용하여 순서에 대한 추론을 단순화합니다.
내부적으로 메시지는 6가지 타입으로 분류됩니다. 각 타입은 파이프라인에서 다르게 처리됩니다.
// 6가지 메시지 타입
type MessageType =
| 'user_text' // 사용자가 직접 입력한 텍스트
| 'user_image' // 사용자가 붙여넣기한 이미지
| 'assistant_text' // Claude의 텍스트 응답
| 'tool_use' // Claude의 도구 사용 요청
| 'tool_result' // 도구 실행 결과
| 'synthetic' // 시스템이 자동 생성한 컨텍스트 메시지
각 타입의 정규화 처리:
메시지 처리 파이프라인은 각 쿼리의 특성을 프로파일링하여 최적의 처리 전략을 결정합니다.
// 쿼리 프로파일링 — 입력 특성 분석
interface QueryProfile {
tokenCount: number // 입력 토큰 수 추정
hasImages: boolean // 이미지 포함 여부
hasFileRefs: boolean // @파일 참조 포함 여부
estimatedComplexity: 'simple' | 'moderate' | 'complex'
suggestedEffort: EffortLevel // 권장 노력 수준
}
function profileQuery(messages: InternalMessage[]): QueryProfile {
const totalTokens = estimateTokens(messages)
const hasImages = messages.some(m => m.type === 'user_image')
return {
tokenCount: totalTokens,
hasImages,
hasFileRefs: messages.some(m => m.fileRefs?.length > 0),
estimatedComplexity: totalTokens > 50_000 ? 'complex'
: totalTokens > 10_000 ? 'moderate' : 'simple',
suggestedEffort: inferEffort(totalTokens, hasImages),
}
}
토큰 수 추정은 실제 토큰화 없이 휴리스틱으로 계산됩니다(예: 영어 4자 = 1토큰). 정확한 토큰 수는 API 응답의 usage 필드에서만 알 수 있습니다. 추정치는 컨텍스트 윈도우 초과 여부를 판단하는 데만 사용됩니다.
handlePromptSubmit의 큐 가드는 동시 제출을 방지하고, 붙여넣기 참조 확장이 @파일경로를 파일 내용으로 인라인합니다processUserInput은 3방향 라우팅(슬래시 커맨드, ULTRAPLAN, 일반 메시지)을 수행합니다normalizeMessagesForAPI는 다중 패스로 정규화합니다: 역할 병합 → 빈 블록 제거 → 도구 결과 정렬 → 컨텍스트 압축 → API 형식 변환Q1. handlePromptSubmit의 큐 가드가 필요한 이유는?
Q2. processUserInput의 3방향 라우팅에서 슬래시 커맨드가 가장 먼저 체크되는 이유는?
/ 접두사로 명확하게 식별 가능하며, 대부분 로컬에서 즉시 처리됩니다(API 호출 불필요). 가장 먼저 체크하면 불필요한 ULTRAPLAN 키워드 스캔이나 메시지 구성을 건너뛸 수 있습니다.Q3. 합성(synthetic) 메시지의 역할은?
Q4. normalizeMessagesForAPI가 다중 패스로 정규화하는 이유는?
Q5. 컨텍스트 윈도우 초과 시 가장 먼저 압축되는 메시지 타입은?