Query Loop
ServiceThe execution center. Manages mutable `State` (messages, turns, toolUseID, compact, transition) as an AsyncGenerator. Handles seven named recovery conditions: fallback-model retry, compaction circuit breaker, max_output_tokens continuation, prompt-too-long reactive compact, stop hook resume, tool-use-only continuation, and post-compact continuation. Runs a five-step preprocessing pipeline before every model call: budget → snip → microcompact → context collapse → autocompact (ordering is load-bearing; each step can short-circuit the next). Hands tool batches to services/tools rather than executing inline.
Why This Component
A single turn often requires multiple model sampling rounds. query.ts models the full turn lifecycle as a recoverable state machine, not a single API call, with `State.transition` recording exactly which recovery condition fired — making recovery paths testable without inspecting message content.
Source anchor
`src/query.ts`. `QueryDeps` has exactly four method signatures — `callModel`, `compact`, `uuid`, `now` — making it a minimal test seam for the loop. The five-step preprocessing pipeline ordering is not arbitrary: budget must run before snip, snip before microcompact, and so on. `query/tokenBudget.ts` manages budget. `query/stopHooks.ts` runs PostQuery hooks. autoCompact circuit breaker: when `State.compactFailed = true` (previous compaction failed), the preprocessing pipeline skips autoCompact on the current turn to prevent retry storms when the summarization model fails.
Trade-offs
A stateful loop is harder to trace but handles streaming, compaction, fallback, and tool-use continuation without outside help. The State.transition field is essential for debugging which recovery condition fired.
Alternatives
A stateless per-turn API call is simpler to host but cannot handle multi-round tool use, context compaction, or streaming fallback without moving that logic outside the product.