openclaudeimproved
§ · Nested detail page

Query lifecycle 深度解析

这一页拆 QueryEngine 如何把 prompt、tools、commands、MCP、memory 和 provider profile 组合成可复用 SDK 运行时。

§ 00 · IMPORT CHAIN

导入链与契约

▸QueryEngine 配置

QueryEngine 的构造参数在 src/QueryEngine.ts:132 到 src/QueryEngine.ts:176 展开,显示它不是 UI 组件,而是把工具、命令、MCP、agents、budget 和 schema 都收敛进 headless runtime。

src/QueryEngine.ts · L132-211TypeScript
132export type QueryEngineConfig = {
133  cwd: string
134  tools: Tools
135  commands: Command[]
136  mcpClients: MCPServerConnection[]
137  agents: AgentDefinition[]
138  canUseTool: CanUseToolFn
139  getAppState: () => AppState
140  setAppState: (f: (prev: AppState) => AppState) => void
141  initialMessages?: Message[]
142  readFileCache: FileStateCache
143  customSystemPrompt?: string
144  appendSystemPrompt?: string
145  userSpecifiedModel?: string
146  fallbackModel?: string
147  thinkingConfig?: ThinkingConfig
148  maxTurns?: number
149  maxBudgetUsd?: number
150  taskBudget?: { total: number }
151  jsonSchema?: Record<string, unknown>
152  verbose?: boolean
153  replayUserMessages?: boolean
154  /** Handler for URL elicitations triggered by MCP tool -32042 errors. */
155  handleElicitation?: ToolUseContext['handleElicitation']
156  includePartialMessages?: boolean
157  setSDKStatus?: (status: SDKStatus) => void
158  abortController?: AbortController
159  orphanedPermission?: OrphanedPermission
160  /**
161   * Snip-boundary handler: receives each yielded system message plus the
162   * current mutableMessages store. Returns undefined if the message is not a
163   * snip boundary; otherwise returns the replayed snip result. Injected by
164   * ask() when HISTORY_SNIP is enabled so feature-gated strings stay inside
165   * the gated module (keeps QueryEngine free of excluded strings and testable
166   * despite feature() returning false under bun test). SDK-only: the REPL
167   * keeps full history for UI scrollback and projects on demand via
168   * projectSnippedView; QueryEngine truncates here to bound memory in long
169   * headless sessions (no UI to preserve).
170   */
171  snipReplay?: (
172    yieldedSystemMsg: Message,
173    store: Message[],
174  ) => { messages: Message[]; executed: boolean } | undefined
175  query?: typeof defaultQuery
176}
177
178/**
179 * QueryEngine owns the query lifecycle and session state for a conversation.
180 * It extracts the core logic from ask() into a standalone class that can be
181 * used by both the headless/SDK path and (in a future phase) the REPL.
182 *
183 * One QueryEngine per conversation. Each submitMessage() call starts a new
184 * turn within the same conversation. State (messages, file cache, usage, etc.)
185 * persists across turns.
186 */
187export class QueryEngine {
188  private config: QueryEngineConfig
189  private mutableMessages: Message[]
190  private abortController: AbortController
191  private permissionDenials: SDKPermissionDenial[]
192  private totalUsage: NonNullableUsage
193  private hasHandledOrphanedPermission = false
194  private readFileState: FileStateCache
195  private autoCompactTracking: AutoCompactTrackingState | undefined
196  // Turn-scoped skill discovery tracking (feeds was_discovered on
197  // tengu_skill_tool_invocation). Must persist across the two
198  // processUserInputContext rebuilds inside submitMessage, but is cleared
199  // at the start of each submitMessage to avoid unbounded growth across
200  // many turns in SDK mode.
201  private discoveredSkillNames = new Set<string>()
202  private loadedNestedMemoryPaths = new Set<string>()
203
204  constructor(config: QueryEngineConfig) {
205    this.config = config
206    this.mutableMessages = config.initialMessages ?? []
207    this.abortController = config.abortController ?? createAbortController()
208    this.permissionDenials = []
209    this.readFileState = config.readFileCache
210    this.totalUsage = EMPTY_USAGE
211  }

▸一轮消息如何开始

每次 submitMessage 从 src/QueryEngine.ts:213 进入,先读取配置,再在 src/QueryEngine.ts:242 清掉本轮 skill discovery;这让长会话保留历史消息,但不会把本轮发现的 skill 无限增长到后续轮次。

src/QueryEngine.ts · L213-337TypeScript
213  async *submitMessage(
214    prompt: string | ContentBlockParam[],
215    options?: { uuid?: string; isMeta?: boolean },
216  ): AsyncGenerator<SDKMessage, void, unknown> {
217    const {
218      cwd,
219      commands,
220      tools,
221      mcpClients,
222      verbose = false,
223      thinkingConfig,
224      maxTurns,
225      maxBudgetUsd,
226      taskBudget,
227      canUseTool,
228      customSystemPrompt,
229      appendSystemPrompt,
230      userSpecifiedModel,
231      fallbackModel,
232      jsonSchema,
233      getAppState,
234      setAppState,
235      replayUserMessages = false,
236      includePartialMessages = false,
237      agents = [],
238      setSDKStatus,
239      orphanedPermission,
240    } = this.config
241
242    this.discoveredSkillNames.clear()
243    setCwd(cwd)
244    const persistSession = !isSessionPersistenceDisabled()
245    const startTime = Date.now()
246
247    // Wrap canUseTool to track permission denials
248    const wrappedCanUseTool: CanUseToolFn = async (
249      tool,
250      input,
251      toolUseContext,
252      assistantMessage,
253      toolUseID,
254      forceDecision,
255    ) => {
256      const result = await canUseTool(
257        tool,
258        input,
259        toolUseContext,
260        assistantMessage,
261        toolUseID,
262        forceDecision,
263      )
264
265      // Track denials for SDK reporting
266      if (result.behavior !== 'allow') {
267        this.permissionDenials.push({
268          tool_name: sdkCompatToolName(tool.name),
269          tool_use_id: toolUseID,
270          tool_input: input,
271        })
272      }
273
274      return result
275    }
276
277    const initialAppState = getAppState()
278    const initialMainLoopModel = userSpecifiedModel
279      ? parseUserSpecifiedModel(userSpecifiedModel)
280      : getMainLoopModel()
281
282    const initialThinkingConfig: ThinkingConfig = thinkingConfig
283      ? thinkingConfig
284      : shouldEnableThinkingByDefault() !== false
285        ? { type: 'adaptive' }
286        : { type: 'disabled' }
287
288    headlessProfilerCheckpoint('before_getSystemPrompt')
289    // Narrow once so TS tracks the type through the conditionals below.
290    const customPrompt =
291      typeof customSystemPrompt === 'string' ? customSystemPrompt : undefined
292    const {
293      defaultSystemPrompt,
294      userContext: baseUserContext,
295      systemContext,
296    } = await fetchSystemPromptParts({
297      tools,
298      mainLoopModel: initialMainLoopModel,
299      additionalWorkingDirectories: Array.from(
300        initialAppState.toolPermissionContext.additionalWorkingDirectories.keys(),
301      ),
302      mcpClients,
303      customSystemPrompt: customPrompt,
304    })
305    headlessProfilerCheckpoint('after_getSystemPrompt')
306    const userContext = {
307      ...baseUserContext,
308      ...getCoordinatorUserContext(
309        mcpClients,
310        isScratchpadEnabled() ? getScratchpadDir() : undefined,
311      ),
312    }
313
314    // When an SDK caller provides a custom system prompt AND has set
315    // CLAUDE_COWORK_MEMORY_PATH_OVERRIDE, inject the memory-mechanics prompt.
316    // The env var is an explicit opt-in signal — the caller has wired up
317    // a memory directory and needs Claude to know how to use it (which
318    // Write/Edit tools to call, MEMORY.md filename, loading semantics).
319    // The caller can layer their own policy text via appendSystemPrompt.
320    const memoryMechanicsPrompt =
321      customPrompt !== undefined && hasAutoMemPathOverride()
322        ? await loadMemoryPrompt()
323        : null
324
325    const systemPrompt = asSystemPrompt([
326      ...(customPrompt !== undefined ? [customPrompt] : defaultSystemPrompt),
327      ...(memoryMechanicsPrompt ? [memoryMechanicsPrompt] : []),
328      ...(appendSystemPrompt ? [appendSystemPrompt] : []),
329    ])
330
331    // Register function hook for structured output enforcement
332    const hasStructuredOutputTool = tools.some(t =>
333      toolMatchesName(t, SYNTHETIC_OUTPUT_TOOL_NAME),
334    )
335    if (jsonSchema && hasStructuredOutputTool) {
336      registerStructuredOutputEnforcement(setAppState, getSessionId())
337    }

▸processUserInput 上下文

processUserInputContext 是核心胶水。src/QueryEngine.ts:339 绑定 mutableMessages,src/QueryEngine.ts:373 带入 readFileState,src/QueryEngine.ts:374 到 src/QueryEngine.ts:377 把 nested memory 与动态 skill triggers 一并暴露。

src/QueryEngine.ts · L339-430TypeScript
339    let processUserInputContext: ProcessUserInputContext = {
340      messages: this.mutableMessages,
341      // Slash commands that mutate the message array (e.g. /clear)
342      // call setMessages(fn).  In interactive mode this writes back to
343      // AppState; in print mode we write back to mutableMessages so the
344      // rest of the query loop (push at :389, snapshot at :392) sees
345      // the result.  The second processUserInputContext below (after
346      // slash-command processing) keeps the no-op — nothing else calls
347      // setMessages past that point.
348      setMessages: fn => {
349        this.mutableMessages = fn(this.mutableMessages)
350      },
351      onChangeAPIKey: () => {},
352      handleElicitation: this.config.handleElicitation,
353      options: {
354        commands,
355        debug: false, // we use stdout, so don't want to clobber it
356        tools,
357        verbose,
358        mainLoopModel: initialMainLoopModel,
359        thinkingConfig: initialThinkingConfig,
360        mcpClients,
361        mcpResources: {},
362        ideInstallationStatus: null,
363        isNonInteractiveSession: true,
364        customSystemPrompt,
365        appendSystemPrompt,
366        agentDefinitions: { activeAgents: agents, allAgents: agents },
367        theme: resolveThemeSetting(getGlobalConfig().theme),
368        maxBudgetUsd,
369      },
370      getAppState,
371      setAppState,
372      abortController: this.abortController,
373      readFileState: this.readFileState,
374      nestedMemoryAttachmentTriggers: new Set<string>(),
375      loadedNestedMemoryPaths: this.loadedNestedMemoryPaths,
376      dynamicSkillDirTriggers: new Set<string>(),
377      discoveredSkillNames: this.discoveredSkillNames,
378      setInProgressToolUseIDs: () => {},
379      setResponseLength: () => {},
380      updateFileHistoryState: (
381        updater: (prev: FileHistoryState) => FileHistoryState,
382      ) => {
383        setAppState(prev => {
384          const updated = updater(prev.fileHistory)
385          if (updated === prev.fileHistory) return prev
386          return { ...prev, fileHistory: updated }
387        })
388      },
389      updateAttributionState: (
390        updater: (prev: AttributionState) => AttributionState,
391      ) => {
392        setAppState(prev => {
393          const updated = updater(prev.attribution)
394          if (updated === prev.attribution) return prev
395          return { ...prev, attribution: updated }
396        })
397      },
398      setSDKStatus,
399    }
400
401    // Handle orphaned permission (only once per engine lifetime)
402    if (orphanedPermission && !this.hasHandledOrphanedPermission) {
403      this.hasHandledOrphanedPermission = true
404      for await (const message of handleOrphanedPermission(
405        orphanedPermission,
406        tools,
407        this.mutableMessages,
408        processUserInputContext,
409      )) {
410        yield message
411      }
412    }
413
414    const {
415      messages: messagesFromUserInput,
416      shouldQuery,
417      allowedTools,
418      model: modelFromUserInput,
419      resultText,
420    } = await processUserInput({
421      input: prompt,
422      mode: 'prompt',
423      setToolJSX: () => {},
424      context: {
425        ...processUserInputContext,
426        messages: this.mutableMessages,
427      },
428      messages: this.mutableMessages,
429      uuid: options?.uuid,
430      isMeta: options?.isMeta,

▸Provider profile 与 secret 面

多模型能力落到 provider profile。src/utils/providerProfile.ts:124 定义兼容 profile,src/utils/providerProfile.ts:152 定义各 provider env,src/utils/providerProfile.ts:215 则让新增 credential 也能参与脱敏。

src/utils/providerProfile.ts · L124-218TypeScript
124export type CompatibilityProfileMode =
125  | 'anthropic'
126  | 'openai'
127  | 'gemini'
128  | 'mistral'
129  | 'github'
130  | 'github-enterprise'
131  | 'bedrock'
132  | 'vertex'
133
134export type ProviderProfile =
135  | 'anthropic'
136  | 'openai'
137  | 'ollama'
138  | 'codex'
139  | 'gemini'
140  | 'atomic-chat'
141  | 'nvidia-nim'
142  | 'minimax'
143  | 'mistral'
144  | 'github'
145  | 'github-enterprise'
146  | 'bedrock'
147  | 'vertex'
148  | 'xai'
149  | 'opencode'
150  | 'clinepass'
151
152export type ProfileEnv = {
153  ANTHROPIC_BASE_URL?: string
154  ANTHROPIC_MODEL?: string
155  ANTHROPIC_API_KEY?: string
156  ANTHROPIC_AUTH_TOKEN?: string
157  ANTHROPIC_CUSTOM_HEADERS?: string
158  ANTHROPIC_BEDROCK_BASE_URL?: string
159  ANTHROPIC_VERTEX_BASE_URL?: string
160  OPENAI_BASE_URL?: string
161  OPENAI_API_BASE?: string
162  OPENAI_MODEL?: string
163  OPENAI_API_FORMAT?: 'chat_completions' | 'responses' | 'responses_compat'
164  OPENAI_AZURE_STYLE?: string
165  OPENAI_AUTH_HEADER?: string
166  OPENAI_AUTH_SCHEME?: 'bearer' | 'raw'
167  OPENAI_AUTH_HEADER_VALUE?: string
168  OPENAI_API_KEYS?: string
169  OPENAI_API_KEY?: string
170  GITHUB_COPILOT_KEY?: string
171  GITHUB_ENTERPRISE_URL?: string
172  CODEX_API_KEY?: string
173  CODEX_CREDENTIAL_SOURCE?: 'oauth' | 'existing'
174  CHATGPT_ACCOUNT_ID?: string
175  CODEX_ACCOUNT_ID?: string
176  GEMINI_API_KEY?: string
177  GEMINI_AUTH_MODE?: 'api-key' | 'access-token' | 'adc'
178  GEMINI_ACCESS_TOKEN?: string
179  GEMINI_MODEL?: string
180  GEMINI_BASE_URL?: string
181  GOOGLE_API_KEY?: string
182  NVIDIA_NIM?: string
183  NVIDIA_API_KEY?: string
184  MINIMAX_API_KEY?: string
185  MINIMAX_BASE_URL?: string
186  MINIMAX_MODEL?: string
187  MISTRAL_BASE_URL?: string
188  MISTRAL_API_KEY?: string
189  MISTRAL_MODEL?: string
190  BANKR_BASE_URL?: string
191  BNKR_API_KEY?: string
192  BANKR_MODEL?: string
193  XAI_API_KEY?: string
194  XAI_CREDENTIAL_SOURCE?: 'oauth'
195  AIMLAPI_API_KEY?: string
196  VENICE_API_KEY?: string
197  MIMO_API_KEY?: string
198  ATLAS_CLOUD_API_KEY?: string
199  CLINE_API_KEY?: string
200  NEARAI_API_KEY?: string
201  FIREWORKS_API_KEY?: string
202  LONGCAT_API_KEY?: string
203  OPENCODE_API_KEY?: string
204  CLOUDFLARE_API_TOKEN?: string
205  CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS?: string
206  CLAUDE_CODE_PROVIDER_ROUTE_ID?: string
207}
208
209export type ProfileFile = {
210  profile: ProviderProfile
211  env: ProfileEnv
212  createdAt: string
213}
214
215// SecretValueSource is intentionally open (Partial<Record<string, ...>>) so
216// that newly declared provider credential env vars are redactable without a
217// matching type update. See providerSecrets.ts for the canonical definition.
218export type { SecretValueSource } from './providerSecrets.js'

▸逐行证据

Query 子页面 的证据点 src/QueryEngine.ts:132 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:187 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:213 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:242 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:248 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:292 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:320 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:339 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:401 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/QueryEngine.ts:414 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/utils/providerProfile.ts:49 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/utils/providerProfile.ts:58 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/utils/providerProfile.ts:124 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/utils/providerProfile.ts:209 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/utils/providerProfile.ts:215 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/commands/install.tsx:91 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/commands/install.tsx:107 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/commands/install.tsx:131 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/commands/install.tsx:149 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/commands/install.tsx:177 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/workSecret.ts:5 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/workSecret.ts:44 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/workSecret.ts:64 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/workSecret.ts:81 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/workSecret.ts:101 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/workSecret.ts:136 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/sessionRunner.test.ts:13 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/sessionRunner.test.ts:34 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Query 子页面 的证据点 src/bridge/sessionRunner.test.ts:78 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Generated · 2026-07-29Source · 0xwilliamortiz/openclaude-improved