3.3 QueryEngine:会话状态管理器

章节目标:理解 QueryEngine 类如何管理多轮对话的状态,以及它与 query.ts 的分工。


QueryEngine 的职责

query.ts 里的 queryLoop 管理的是单次 Turn 内部的状态(工具调用循环、上下文压缩)。

QueryEngine 管理的是多个 Turn 之间的状态(对话历史、用量统计、权限记录)。

QueryEngine(会话级别)
    │
    ├── submitMessage() → Turn 1 → queryLoop → 返回
    ├── submitMessage() → Turn 2 → queryLoop → 返回
    └── submitMessage() → Turn 3 → queryLoop → 返回
    
    共享状态:mutableMessages, totalUsage, permissionDenials

类定义与核心字段

  private config: QueryEngineConfig
  
  // 跨 Turn 共享的消息历史
  private mutableMessages: Message[]
  
  // Abort Controller(支持外部中断)
  private abortController: AbortController
  
  // 权限拒绝记录(供 SDK 报告)
  private permissionDenials: SDKPermissionDenial[]
  
  // 累计 token 用量
  private totalUsage: NonNullableUsage
  
  // 已发现的技能名(避免重复上报遥测)
  private discoveredSkillNames = new Set<string>()
  
  // 已加载的嵌套记忆文件路径(避免重复注入)
  private loadedNestedMemoryPaths = new Set<string>()
}

submitMessage():每次对话的入口

async *submitMessage(
  prompt: string | ContentBlockParam[],
  options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage, void, unknown> {
  // 1. 重置 Turn 级别的追踪状态
  this.discoveredSkillNames.clear()
  setCwd(cwd)
  
  // 2. 构建系统提示
  const { defaultSystemPrompt, userContext, systemContext } = 
    await fetchSystemPromptParts({ tools, ... })
  
  // 3. 处理用户输入(斜杠命令、附件等)
  const processed = await processUserInput(prompt, ...)
  
  // 4. 推送到消息历史
  this.mutableMessages = [...this.mutableMessages, processed.userMessage]
  
  // 5. 调用 query() 主循环
  for await (const message of query({
    messages: this.mutableMessages,
    systemPrompt,
    canUseTool: wrappedCanUseTool,
    ...
  })) {
    // 6. 更新会话状态
    if (isAssistantMessage(message)) {
      this.mutableMessages = [...this.mutableMessages, message]
      this.totalUsage = accumulateUsage(this.totalUsage, message.usage)
    }
    
    // 7. yield 给上层调用者
    yield toSDKMessage(message)
  }
  
  // 8. 持久化会话
  if (persistSession) {
    await flushSessionStorage(this.mutableMessages, ...)
  }
}

权限拒绝追踪

QueryEngine 包装了 canUseTool 函数,在不影响原有逻辑的情况下记录每次拒绝:

const wrappedCanUseTool: CanUseToolFn = async (...args) => {
  const result = await canUseTool(...args)
  
  if (result.behavior !== 'allow') {
    // 记录被拒绝的工具调用,供 SDK 调用者查询
    this.permissionDenials.push({
      tool_name: sdkCompatToolName(tool.name),
      tool_use_id: toolUseID,
      tool_input: input,
    })
  }
  
  return result
}

这是装饰器(Decorator)模式的典型应用:在不修改被装饰函数的情况下,增加额外行为(记录拒绝)。


QueryEngine vs ask()

Claude Code 有两个代码路径:

路径使用场景入口
QueryEngineSDK 调用、headless 模式new QueryEngine(config).submitMessage(prompt)
ask()REPL 交互模式src/QueryEngine.ts 中的 ask() 函数

两者都最终调用 query(),区别在于:

注释中明确写道:

"One QueryEngine per conversation. Each submitMessage() call starts a new turn within the same conversation."


下一步