MODULO 1.2

⚙️ Query Engine & LLM API

As quatro camadas entre a mensagem do usuario e a resposta no terminal: QueryEngine, queryLoop, callModel e stop hooks. Cada superficie publica do Claude Code -- REPL, SDK, remote -- passa pelo mesmo query() generator.

8
Topicos
~120
Minutos
Deep
Nivel
Source
Tipo
1

📨 The Big Picture - Four Layers

Quando uma mensagem e enviada ao Claude Code, ela passa por quatro camadas distintas antes de chegar ao terminal do usuario:

1. QueryEngine.submitMessage()

Valida o prompt, constroi o system prompt, resolve o modelo, grava o transcript e passa para query().

2. query() / queryLoop()

Um async function* que faz loop ate o modelo parar de chamar tools. Cada iteracao representa uma chamada ao modelo.

3. queryModel / callModel

Chama a API Anthropic via interface streaming do SDK, envelopada em withRetry().

4. Stop hooks & token budget

Apos cada turno, hooks externos rodam; o token budget determina se injeta um nudge e continua o loop.

💡 Insight-chave

"Every public surface of Claude Code -- the REPL, the SDK, remote Claude Code -- funnels through the same query() generator." Isso significa que entender o query loop e entender o core de todo o sistema.

2

💬 QueryEngine - One Engine Per Conversation

QueryEngine e uma classe stateful instanciada uma vez por conversa. Ela mantem historico de mensagens mutavel, uso total de tokens, denials de permissao e o abort controller.

export class QueryEngine {
  private mutableMessages: Message[]
  private abortController: AbortController
  private totalUsage: NonNullableUsage
  private permissionDenials: SDKPermissionDenial[]

  // Turn-scoped: cleared at start of each submitMessage() call
  private discoveredSkillNames = new Set<string>()

  async *submitMessage(
    prompt: string | ContentBlockParam[],
    options?: { uuid?: string; isMeta?: boolean },
  ): AsyncGenerator<SDKMessage> {
    // 1. Build system prompt (fetchSystemPromptParts)
    // 2. processUserInput -- handles slash commands
    // 3. recordTranscript -- persists BEFORE the API call
    // 4. yield* query({ messages, ... })
    // 5. yield final result SDKMessage
  }
}

📊 Por que o Transcript e Escrito Antes da API Call

A mensagem do usuario e persistida em disco antes de query() ser chamado. Isso torna a sessao resumivel mesmo se o processo for killed antes da resposta do modelo. "Writing now makes the transcript resumable from the point the user message was accepted, even if no API response ever arrives."

3

🔄 queryLoop() - The While(true) Core

queryLoop() em query.ts e um while(true) carregando um objeto State tipado entre iteracoes:

type State = {
  messages: Message[]
  toolUseContext: ToolUseContext
  autoCompactTracking: AutoCompactTrackingState | undefined
  maxOutputTokensRecoveryCount: number
  hasAttemptedReactiveCompact: boolean
  maxOutputTokensOverride: number | undefined
  pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
  stopHookActive: boolean | undefined
  turnCount: number
  transition: Continue | undefined   // WHY we looped again
}

Continue Transitions -- Sete Razoes para Continuar o Loop

transition.reason Significado
max_output_tokens_escalatePrimeiro hit do cap de 8k; retry com 64k max_tokens
max_output_tokens_recoveryModelo atingiu limite de output; injeta recovery nudge (ate 3x)
reactive_compact_retryPrompt-too-long -> compacted history -> retry
collapse_drain_retryPrompt-too-long -> drained context-collapse stages -> retry
stop_hook_blockingStop hook retornou erro blocking; re-query com erro como user message
token_budget_continuationToken budget diz que trabalho nao esta feito; injeta nudge
(needs follow-up)Normal: modelo retornou tool_use blocks -> run tools -> loop

⚠️ Condicoes de Terminacao

O loop sai via um Terminal em: completed, blocking_limit, model_error, prompt_too_long, aborted_streaming, stop_hook_prevented, image_error.

4

🌐 Streaming & the API Layer

queryModel em claude.ts e um async function* que chama o endpoint de mensagens beta da Anthropic e re-yields cada evento de stream como um AssistantMessage ou StreamEvent interno.

for await (const message of deps.callModel({
  messages: prependUserContext(messagesForQuery, userContext),
  systemPrompt: fullSystemPrompt,
  thinkingConfig: toolUseContext.options.thinkingConfig,
  tools: toolUseContext.options.tools,
  signal: toolUseContext.abortController.signal,
  options: { model: currentModel, fallbackModel, ... },
})) {
  if (message.type === 'assistant') {
    assistantMessages.push(message)
    const toolBlocks = message.message.content
      .filter(b => b.type === 'tool_use')
    if (toolBlocks.length > 0) needsFollowUp = true
  }
  yield yieldMessage // surfaces to SDK caller / REPL
}

Streaming Tool Execution

Quando config.gates.streamingToolExecution esta habilitado, um StreamingToolExecutor inicia ferramentas enquanto o stream ainda esta aberto. Tools com inputs disponiveis comecam a executar em paralelo com o modelo ainda gerando texto, reduzindo latencia em turns com multiplas tools.

💡 Tombstone Messages

Se um streaming fallback e acionado mid-stream, objetos AssistantMessage parcialmente recebidos sao "tombstoned" -- yielded como { type: 'tombstone', message } para que a UI e o transcript possam remove-los. Isso previne erros de "thinking blocks cannot be modified" na API ao fazer retry.

5

🛡️ withRetry() - Smart Exponential Backoff

Toda API call passa por withRetry() em services/api/withRetry.ts. A funcao e um async function* com ate DEFAULT_MAX_RETRIES = 10 tentativas, yielding um SystemAPIErrorMessage antes de cada sleep para que usuarios vejam status updates em tempo real.

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
}

Regras de Decisao de Retry

  • 529 (overloaded): Apenas sources foreground fazem retry (usuario esta esperando). Sources background abortam imediatamente para evitar amplificar cascatas de capacidade.
  • Opus fallback: Apos 3 erros 529 consecutivos em modelo Opus nao-custom, lanca FallbackTriggeredError que queryLoop captura e muda para fallbackModel.
  • OAuth 401: Forca token refresh via handleOAuth401Error() antes da proxima tentativa.
  • Context overflow 400: Parseia contagens de tokens da mensagem de erro e calcula novo maxTokensOverride.
  • Persistent mode (UNATTENDED_RETRY): Retenta indefinidamente com cap de 30 minutos, yielding heartbeat messages a cada 30s.
  • ECONNRESET/EPIPE: Socket keep-alive stale detectado; disableKeepAlive() chamado antes do retry.
6

💾 Context Management & Autocompact

Antes de cada API call, queryLoop executa um pipeline de estrategias de reducao de contexto em ordem fixa de prioridade:

1.
applyToolResultBudget() - Limita tamanho em bytes de resultados individuais de tools. Resultados grandes sao armazenados externamente e substituidos por um stub de referencia.
2.
snipCompact (HISTORY_SNIP) - Remove mensagens antigas do meio do historico quando provadamente desnecessarias, liberando tokens sem sumarizacao completa.
3.
microcompact / cached microcompact - Merge pares consecutivos de tool-result/user message em sumarizacoes condensadas.
4.
contextCollapse (CONTEXT_COLLAPSE) - Projecao read-time sobre o historico completo do REPL. Collapses estagiados sao committed em cada entrada.
5.
autoCompact - Quando contexto se aproxima do blocking limit, aciona sumarizacao completa via agente forked.
// task_budget carryover across compaction (query.ts ~508)
if (params.taskBudget) {
  const preCompactContext =
    finalContextTokensFromLastResponse(messagesForQuery)
  taskBudgetRemaining = Math.max(
    0,
    (taskBudgetRemaining ?? params.taskBudget.total) - preCompactContext,
  )
}
7

🔔 Stop Hooks - Post-Turn Lifecycle

Apos o modelo terminar (sem tool calls, sem recovery), o engine chama handleStopHooks() que executa tres categorias em ordem:

1. Stop Hooks (sempre)

Registrados via hooks em settings.json. Rodam em paralelo; podem produzir blocking errors, prevent continuation ou success.

2. TaskCompleted Hooks (teammate mode)

Disparam para cada task in_progress owned por este agente.

3. TeammateIdle Hooks (teammate mode)

Disparam quando este teammate transiciona para idle.

Fire-and-Forget Background Tasks

Ignorados em bare mode (-p). Disparados sem await em modo interativo:

if (!isBareMode()) {
  void executePromptSuggestion(stopHookContext)
  if (feature('EXTRACT_MEMORIES') && isExtractModeActive()) {
    void extractMemoriesModule!.executeExtractMemories(...)
  }
  if (!toolUseContext.agentId) {
    void executeAutoDream(...)
  }
}
8

💰 Token Budget - Auto-Continue

query/tokenBudget.ts implementa auto-continue para o SDK path. Quando um budget por turno e configurado, o engine verifica apos cada stop limpo se o modelo "usou" budget suficiente.

const COMPLETION_THRESHOLD = 0.9   // 90% usado = feito
const DIMINISHING_THRESHOLD = 500  // <500 novos tokens = sem progresso

export function checkTokenBudget(
  tracker: BudgetTracker,
  agentId: string | undefined,
  budget: number | null,
  globalTurnTokens: number,
): TokenBudgetDecision {
  if (agentId || budget === null || budget <= 0) {
    return { action: 'stop', completionEvent: null }
  }
  const pct = Math.round((globalTurnTokens / budget) * 100)
  const isDiminishing =
    tracker.continuationCount >= 3 &&
    deltaSinceLastCheck < DIMINISHING_THRESHOLD &&
    tracker.lastDeltaTokens < DIMINISHING_THRESHOLD
  if (!isDiminishing && turnTokens < budget * COMPLETION_THRESHOLD) {
    return { action: 'continue', nudgeMessage: ... }
  }
  return { action: 'stop', ... }
}

✅ Continua quando

  • Tokens usados < 90% do budget
  • Nao esta em diminishing returns

❌ Para quando

  • Budget exaurido (>= 90%)
  • Apos 3+ continuacoes, ambos deltas < 500 tokens

🗺️ Diagrama de Sequencia - Fluxo Completo

sequenceDiagram
    participant User
    participant QE as QueryEngine
    participant Q as queryLoop()
    participant QM as queryModel
    participant API as Anthropic API
    participant Tools as Tool Executor
    participant SH as stopHooks

    User->>QE: submitMessage(prompt)
    QE->>QE: fetchSystemPromptParts()
    QE->>Q: query({ messages, systemPrompt })
    Q->>Q: applyToolResultBudget / microcompact / snip
    loop queryLoop iteration
        Q->>QM: callModel({ messages, tools })
        QM->>API: POST /v1/messages (streaming)
        API-->>QM: content_block_delta events
        QM-->>Q: yield AssistantMessage
        alt tool_use blocks present
            Q->>Tools: runTools(toolUseBlocks)
            Tools-->>Q: tool_result messages
            Note over Q: loop continues
        else no tool calls
            Q->>SH: handleStopHooks()
            alt hook blocking error
                Q->>Q: append error, loop again
            else clean stop
                Q->>Q: checkTokenBudget()
                Q-->>QE: Terminal { reason }
            end
        end
    end
    QE-->>User: yield SDKMessage stream
                    

📋 Resumo do Modulo

O while(true) em queryLoop sai via um Terminal tipado -- toda condicao de parada tem uma razao nomeada
Generators all the way down: submitMessage, query, queryLoop, queryModel, withRetry -- todos sao async function*
Transcript-first reliability: mensagem persistida em disco antes da API call
Retry e mais inteligente que exponential backoff: routing foreground/background, OAuth refresh, Opus fallback
Pipeline de compacao em 5 estagios garante que conversas longas nao estourem o contexto
Background effects (memory, suggestions, auto-dream) sao fire-and-forget e nao rodam em bare mode
Modulo Anterior Proximo Modulo