QueryEngine.ts 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295
  1. import { feature } from 'bun:bundle'
  2. import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
  3. import { randomUUID } from 'crypto'
  4. import last from 'lodash-es/last.js'
  5. import {
  6. getSessionId,
  7. isSessionPersistenceDisabled,
  8. } from 'src/bootstrap/state.js'
  9. import type {
  10. PermissionMode,
  11. SDKCompactBoundaryMessage,
  12. SDKMessage,
  13. SDKPermissionDenial,
  14. SDKStatus,
  15. SDKUserMessageReplay,
  16. } from 'src/entrypoints/agentSdkTypes.js'
  17. import { accumulateUsage, updateUsage } from 'src/services/api/claude.js'
  18. import type { NonNullableUsage } from 'src/services/api/logging.js'
  19. import { EMPTY_USAGE } from 'src/services/api/logging.js'
  20. import stripAnsi from 'strip-ansi'
  21. import type { Command } from './commands.js'
  22. import { getSlashCommandToolSkills } from './commands.js'
  23. import {
  24. LOCAL_COMMAND_STDERR_TAG,
  25. LOCAL_COMMAND_STDOUT_TAG,
  26. } from './constants/xml.js'
  27. import {
  28. getModelUsage,
  29. getTotalAPIDuration,
  30. getTotalCost,
  31. } from './cost-tracker.js'
  32. import type { CanUseToolFn } from './hooks/useCanUseTool.js'
  33. import { loadMemoryPrompt } from './memdir/memdir.js'
  34. import { hasAutoMemPathOverride } from './memdir/paths.js'
  35. import { query } from './query.js'
  36. import { categorizeRetryableAPIError } from './services/api/errors.js'
  37. import type { MCPServerConnection } from './services/mcp/types.js'
  38. import type { AppState } from './state/AppState.js'
  39. import { type Tools, type ToolUseContext, toolMatchesName } from './Tool.js'
  40. import type { AgentDefinition } from './tools/AgentTool/loadAgentsDir.js'
  41. import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
  42. import type { Message } from './types/message.js'
  43. import type { OrphanedPermission } from './types/textInputTypes.js'
  44. import { createAbortController } from './utils/abortController.js'
  45. import type { AttributionState } from './utils/commitAttribution.js'
  46. import { getGlobalConfig } from './utils/config.js'
  47. import { getCwd } from './utils/cwd.js'
  48. import { isBareMode, isEnvTruthy } from './utils/envUtils.js'
  49. import { getFastModeState } from './utils/fastMode.js'
  50. import {
  51. type FileHistoryState,
  52. fileHistoryEnabled,
  53. fileHistoryMakeSnapshot,
  54. } from './utils/fileHistory.js'
  55. import {
  56. cloneFileStateCache,
  57. type FileStateCache,
  58. } from './utils/fileStateCache.js'
  59. import { headlessProfilerCheckpoint } from './utils/headlessProfiler.js'
  60. import { registerStructuredOutputEnforcement } from './utils/hooks/hookHelpers.js'
  61. import { getInMemoryErrors } from './utils/log.js'
  62. import { countToolCalls, SYNTHETIC_MESSAGES } from './utils/messages.js'
  63. import {
  64. getMainLoopModel,
  65. parseUserSpecifiedModel,
  66. } from './utils/model/model.js'
  67. import { loadAllPluginsCacheOnly } from './utils/plugins/pluginLoader.js'
  68. import {
  69. type ProcessUserInputContext,
  70. processUserInput,
  71. } from './utils/processUserInput/processUserInput.js'
  72. import { fetchSystemPromptParts } from './utils/queryContext.js'
  73. import { setCwd } from './utils/Shell.js'
  74. import {
  75. flushSessionStorage,
  76. recordTranscript,
  77. } from './utils/sessionStorage.js'
  78. import { asSystemPrompt } from './utils/systemPromptType.js'
  79. import { resolveThemeSetting } from './utils/systemTheme.js'
  80. import {
  81. shouldEnableThinkingByDefault,
  82. type ThinkingConfig,
  83. } from './utils/thinking.js'
  84. // Lazy: MessageSelector.tsx pulls React/ink; only needed for message filtering at query time
  85. /* eslint-disable @typescript-eslint/no-require-imports */
  86. const messageSelector =
  87. (): typeof import('src/components/MessageSelector.js') =>
  88. require('src/components/MessageSelector.js')
  89. import {
  90. localCommandOutputToSDKAssistantMessage,
  91. toSDKCompactMetadata,
  92. } from './utils/messages/mappers.js'
  93. import {
  94. buildSystemInitMessage,
  95. sdkCompatToolName,
  96. } from './utils/messages/systemInit.js'
  97. import {
  98. getScratchpadDir,
  99. isScratchpadEnabled,
  100. } from './utils/permissions/filesystem.js'
  101. /* eslint-enable @typescript-eslint/no-require-imports */
  102. import {
  103. handleOrphanedPermission,
  104. isResultSuccessful,
  105. normalizeMessage,
  106. } from './utils/queryHelpers.js'
  107. // Dead code elimination: conditional import for coordinator mode
  108. /* eslint-disable @typescript-eslint/no-require-imports */
  109. const getCoordinatorUserContext: (
  110. mcpClients: ReadonlyArray<{ name: string }>,
  111. scratchpadDir?: string,
  112. ) => { [k: string]: string } = feature('COORDINATOR_MODE')
  113. ? require('./coordinator/coordinatorMode.js').getCoordinatorUserContext
  114. : () => ({})
  115. /* eslint-enable @typescript-eslint/no-require-imports */
  116. // Dead code elimination: conditional import for snip compaction
  117. /* eslint-disable @typescript-eslint/no-require-imports */
  118. const snipModule = feature('HISTORY_SNIP')
  119. ? (require('./services/compact/snipCompact.js') as typeof import('./services/compact/snipCompact.js'))
  120. : null
  121. const snipProjection = feature('HISTORY_SNIP')
  122. ? (require('./services/compact/snipProjection.js') as typeof import('./services/compact/snipProjection.js'))
  123. : null
  124. /* eslint-enable @typescript-eslint/no-require-imports */
  125. export type QueryEngineConfig = {
  126. cwd: string
  127. tools: Tools
  128. commands: Command[]
  129. mcpClients: MCPServerConnection[]
  130. agents: AgentDefinition[]
  131. canUseTool: CanUseToolFn
  132. getAppState: () => AppState
  133. setAppState: (f: (prev: AppState) => AppState) => void
  134. initialMessages?: Message[]
  135. readFileCache: FileStateCache
  136. customSystemPrompt?: string
  137. appendSystemPrompt?: string
  138. userSpecifiedModel?: string
  139. fallbackModel?: string
  140. thinkingConfig?: ThinkingConfig
  141. maxTurns?: number
  142. maxBudgetUsd?: number
  143. taskBudget?: { total: number }
  144. jsonSchema?: Record<string, unknown>
  145. verbose?: boolean
  146. replayUserMessages?: boolean
  147. /** Handler for URL elicitations triggered by MCP tool -32042 errors. */
  148. handleElicitation?: ToolUseContext['handleElicitation']
  149. includePartialMessages?: boolean
  150. setSDKStatus?: (status: SDKStatus) => void
  151. abortController?: AbortController
  152. orphanedPermission?: OrphanedPermission
  153. /**
  154. * Snip-boundary handler: receives each yielded system message plus the
  155. * current mutableMessages store. Returns undefined if the message is not a
  156. * snip boundary; otherwise returns the replayed snip result. Injected by
  157. * ask() when HISTORY_SNIP is enabled so feature-gated strings stay inside
  158. * the gated module (keeps QueryEngine free of excluded strings and testable
  159. * despite feature() returning false under bun test). SDK-only: the REPL
  160. * keeps full history for UI scrollback and projects on demand via
  161. * projectSnippedView; QueryEngine truncates here to bound memory in long
  162. * headless sessions (no UI to preserve).
  163. */
  164. snipReplay?: (
  165. yieldedSystemMsg: Message,
  166. store: Message[],
  167. ) => { messages: Message[]; executed: boolean } | undefined
  168. }
  169. /**
  170. * QueryEngine owns the query lifecycle and session state for a conversation.
  171. * It extracts the core logic from ask() into a standalone class that can be
  172. * used by both the headless/SDK path and (in a future phase) the REPL.
  173. *
  174. * One QueryEngine per conversation. Each submitMessage() call starts a new
  175. * turn within the same conversation. State (messages, file cache, usage, etc.)
  176. * persists across turns.
  177. */
  178. export class QueryEngine {
  179. private config: QueryEngineConfig
  180. private mutableMessages: Message[]
  181. private abortController: AbortController
  182. private permissionDenials: SDKPermissionDenial[]
  183. private totalUsage: NonNullableUsage
  184. private hasHandledOrphanedPermission = false
  185. private readFileState: FileStateCache
  186. // Turn-scoped skill discovery tracking (feeds was_discovered on
  187. // tengu_skill_tool_invocation). Must persist across the two
  188. // processUserInputContext rebuilds inside submitMessage, but is cleared
  189. // at the start of each submitMessage to avoid unbounded growth across
  190. // many turns in SDK mode.
  191. private discoveredSkillNames = new Set<string>()
  192. private loadedNestedMemoryPaths = new Set<string>()
  193. constructor(config: QueryEngineConfig) {
  194. this.config = config
  195. this.mutableMessages = config.initialMessages ?? []
  196. this.abortController = config.abortController ?? createAbortController()
  197. this.permissionDenials = []
  198. this.readFileState = config.readFileCache
  199. this.totalUsage = EMPTY_USAGE
  200. }
  201. async *submitMessage(
  202. prompt: string | ContentBlockParam[],
  203. options?: { uuid?: string; isMeta?: boolean },
  204. ): AsyncGenerator<SDKMessage, void, unknown> {
  205. const {
  206. cwd,
  207. commands,
  208. tools,
  209. mcpClients,
  210. verbose = false,
  211. thinkingConfig,
  212. maxTurns,
  213. maxBudgetUsd,
  214. taskBudget,
  215. canUseTool,
  216. customSystemPrompt,
  217. appendSystemPrompt,
  218. userSpecifiedModel,
  219. fallbackModel,
  220. jsonSchema,
  221. getAppState,
  222. setAppState,
  223. replayUserMessages = false,
  224. includePartialMessages = false,
  225. agents = [],
  226. setSDKStatus,
  227. orphanedPermission,
  228. } = this.config
  229. this.discoveredSkillNames.clear()
  230. setCwd(cwd)
  231. const persistSession = !isSessionPersistenceDisabled()
  232. const startTime = Date.now()
  233. // Wrap canUseTool to track permission denials
  234. const wrappedCanUseTool: CanUseToolFn = async (
  235. tool,
  236. input,
  237. toolUseContext,
  238. assistantMessage,
  239. toolUseID,
  240. forceDecision,
  241. ) => {
  242. const result = await canUseTool(
  243. tool,
  244. input,
  245. toolUseContext,
  246. assistantMessage,
  247. toolUseID,
  248. forceDecision,
  249. )
  250. // Track denials for SDK reporting
  251. if (result.behavior !== 'allow') {
  252. this.permissionDenials.push({
  253. tool_name: sdkCompatToolName(tool.name),
  254. tool_use_id: toolUseID,
  255. tool_input: input,
  256. })
  257. }
  258. return result
  259. }
  260. const initialAppState = getAppState()
  261. const initialMainLoopModel = userSpecifiedModel
  262. ? parseUserSpecifiedModel(userSpecifiedModel)
  263. : getMainLoopModel()
  264. const initialThinkingConfig: ThinkingConfig = thinkingConfig
  265. ? thinkingConfig
  266. : shouldEnableThinkingByDefault() !== false
  267. ? { type: 'adaptive' }
  268. : { type: 'disabled' }
  269. headlessProfilerCheckpoint('before_getSystemPrompt')
  270. // Narrow once so TS tracks the type through the conditionals below.
  271. const customPrompt =
  272. typeof customSystemPrompt === 'string' ? customSystemPrompt : undefined
  273. const {
  274. defaultSystemPrompt,
  275. userContext: baseUserContext,
  276. systemContext,
  277. } = await fetchSystemPromptParts({
  278. tools,
  279. mainLoopModel: initialMainLoopModel,
  280. additionalWorkingDirectories: Array.from(
  281. initialAppState.toolPermissionContext.additionalWorkingDirectories.keys(),
  282. ),
  283. mcpClients,
  284. customSystemPrompt: customPrompt,
  285. })
  286. headlessProfilerCheckpoint('after_getSystemPrompt')
  287. const userContext = {
  288. ...baseUserContext,
  289. ...getCoordinatorUserContext(
  290. mcpClients,
  291. isScratchpadEnabled() ? getScratchpadDir() : undefined,
  292. ),
  293. }
  294. // When an SDK caller provides a custom system prompt AND has set
  295. // CLAUDE_COWORK_MEMORY_PATH_OVERRIDE, inject the memory-mechanics prompt.
  296. // The env var is an explicit opt-in signal — the caller has wired up
  297. // a memory directory and needs Claude to know how to use it (which
  298. // Write/Edit tools to call, MEMORY.md filename, loading semantics).
  299. // The caller can layer their own policy text via appendSystemPrompt.
  300. const memoryMechanicsPrompt =
  301. customPrompt !== undefined && hasAutoMemPathOverride()
  302. ? await loadMemoryPrompt()
  303. : null
  304. const systemPrompt = asSystemPrompt([
  305. ...(customPrompt !== undefined ? [customPrompt] : defaultSystemPrompt),
  306. ...(memoryMechanicsPrompt ? [memoryMechanicsPrompt] : []),
  307. ...(appendSystemPrompt ? [appendSystemPrompt] : []),
  308. ])
  309. // Register function hook for structured output enforcement
  310. const hasStructuredOutputTool = tools.some(t =>
  311. toolMatchesName(t, SYNTHETIC_OUTPUT_TOOL_NAME),
  312. )
  313. if (jsonSchema && hasStructuredOutputTool) {
  314. registerStructuredOutputEnforcement(setAppState, getSessionId())
  315. }
  316. let processUserInputContext: ProcessUserInputContext = {
  317. messages: this.mutableMessages,
  318. // Slash commands that mutate the message array (e.g. /force-snip)
  319. // call setMessages(fn). In interactive mode this writes back to
  320. // AppState; in print mode we write back to mutableMessages so the
  321. // rest of the query loop (push at :389, snapshot at :392) sees
  322. // the result. The second processUserInputContext below (after
  323. // slash-command processing) keeps the no-op — nothing else calls
  324. // setMessages past that point.
  325. setMessages: fn => {
  326. this.mutableMessages = fn(this.mutableMessages)
  327. },
  328. onChangeAPIKey: () => {},
  329. handleElicitation: this.config.handleElicitation,
  330. options: {
  331. commands,
  332. debug: false, // we use stdout, so don't want to clobber it
  333. tools,
  334. verbose,
  335. mainLoopModel: initialMainLoopModel,
  336. thinkingConfig: initialThinkingConfig,
  337. mcpClients,
  338. mcpResources: {},
  339. ideInstallationStatus: null,
  340. isNonInteractiveSession: true,
  341. customSystemPrompt,
  342. appendSystemPrompt,
  343. agentDefinitions: { activeAgents: agents, allAgents: [] },
  344. theme: resolveThemeSetting(getGlobalConfig().theme),
  345. maxBudgetUsd,
  346. },
  347. getAppState,
  348. setAppState,
  349. abortController: this.abortController,
  350. readFileState: this.readFileState,
  351. nestedMemoryAttachmentTriggers: new Set<string>(),
  352. loadedNestedMemoryPaths: this.loadedNestedMemoryPaths,
  353. dynamicSkillDirTriggers: new Set<string>(),
  354. discoveredSkillNames: this.discoveredSkillNames,
  355. setInProgressToolUseIDs: () => {},
  356. setResponseLength: () => {},
  357. updateFileHistoryState: (
  358. updater: (prev: FileHistoryState) => FileHistoryState,
  359. ) => {
  360. setAppState(prev => {
  361. const updated = updater(prev.fileHistory)
  362. if (updated === prev.fileHistory) return prev
  363. return { ...prev, fileHistory: updated }
  364. })
  365. },
  366. updateAttributionState: (
  367. updater: (prev: AttributionState) => AttributionState,
  368. ) => {
  369. setAppState(prev => {
  370. const updated = updater(prev.attribution)
  371. if (updated === prev.attribution) return prev
  372. return { ...prev, attribution: updated }
  373. })
  374. },
  375. setSDKStatus,
  376. }
  377. // Handle orphaned permission (only once per engine lifetime)
  378. if (orphanedPermission && !this.hasHandledOrphanedPermission) {
  379. this.hasHandledOrphanedPermission = true
  380. for await (const message of handleOrphanedPermission(
  381. orphanedPermission,
  382. tools,
  383. this.mutableMessages,
  384. processUserInputContext,
  385. )) {
  386. yield message
  387. }
  388. }
  389. const {
  390. messages: messagesFromUserInput,
  391. shouldQuery,
  392. allowedTools,
  393. model: modelFromUserInput,
  394. resultText,
  395. } = await processUserInput({
  396. input: prompt,
  397. mode: 'prompt',
  398. setToolJSX: () => {},
  399. context: {
  400. ...processUserInputContext,
  401. messages: this.mutableMessages,
  402. },
  403. messages: this.mutableMessages,
  404. uuid: options?.uuid,
  405. isMeta: options?.isMeta,
  406. querySource: 'sdk',
  407. })
  408. // Push new messages, including user input and any attachments
  409. this.mutableMessages.push(...messagesFromUserInput)
  410. // Update params to reflect updates from processing /slash commands
  411. const messages = [...this.mutableMessages]
  412. // Persist the user's message(s) to transcript BEFORE entering the query
  413. // loop. The for-await below only calls recordTranscript when ask() yields
  414. // an assistant/user/compact_boundary message — which doesn't happen until
  415. // the API responds. If the process is killed before that (e.g. user clicks
  416. // Stop in cowork seconds after send), the transcript is left with only
  417. // queue-operation entries; getLastSessionLog filters those out, returns
  418. // null, and --resume fails with "No conversation found". Writing now makes
  419. // the transcript resumable from the point the user message was accepted,
  420. // even if no API response ever arrives.
  421. //
  422. // --bare / SIMPLE: fire-and-forget. Scripted calls don't --resume after
  423. // kill-mid-request. The await is ~4ms on SSD, ~30ms under disk contention
  424. // — the single largest controllable critical-path cost after module eval.
  425. // Transcript is still written (for post-hoc debugging); just not blocking.
  426. if (persistSession && messagesFromUserInput.length > 0) {
  427. const transcriptPromise = recordTranscript(messages)
  428. if (isBareMode()) {
  429. void transcriptPromise
  430. } else {
  431. await transcriptPromise
  432. if (
  433. isEnvTruthy(process.env.CLAUDE_CODE_EAGER_FLUSH) ||
  434. isEnvTruthy(process.env.CLAUDE_CODE_IS_COWORK)
  435. ) {
  436. await flushSessionStorage()
  437. }
  438. }
  439. }
  440. // Filter messages that should be acknowledged after transcript
  441. const replayableMessages = messagesFromUserInput.filter(
  442. msg =>
  443. (msg.type === 'user' &&
  444. !msg.isMeta && // Skip synthetic caveat messages
  445. !msg.toolUseResult && // Skip tool results (they'll be acked from query)
  446. messageSelector().selectableUserMessagesFilter(msg)) || // Skip non-user-authored messages (task notifications, etc.)
  447. (msg.type === 'system' && msg.subtype === 'compact_boundary'), // Always ack compact boundaries
  448. )
  449. const messagesToAck = replayUserMessages ? replayableMessages : []
  450. // Update the ToolPermissionContext based on user input processing (as necessary)
  451. setAppState(prev => ({
  452. ...prev,
  453. toolPermissionContext: {
  454. ...prev.toolPermissionContext,
  455. alwaysAllowRules: {
  456. ...prev.toolPermissionContext.alwaysAllowRules,
  457. command: allowedTools,
  458. },
  459. },
  460. }))
  461. const mainLoopModel = modelFromUserInput ?? initialMainLoopModel
  462. // Recreate after processing the prompt to pick up updated messages and
  463. // model (from slash commands).
  464. processUserInputContext = {
  465. messages,
  466. setMessages: () => {},
  467. onChangeAPIKey: () => {},
  468. handleElicitation: this.config.handleElicitation,
  469. options: {
  470. commands,
  471. debug: false,
  472. tools,
  473. verbose,
  474. mainLoopModel,
  475. thinkingConfig: initialThinkingConfig,
  476. mcpClients,
  477. mcpResources: {},
  478. ideInstallationStatus: null,
  479. isNonInteractiveSession: true,
  480. customSystemPrompt,
  481. appendSystemPrompt,
  482. theme: resolveThemeSetting(getGlobalConfig().theme),
  483. agentDefinitions: { activeAgents: agents, allAgents: [] },
  484. maxBudgetUsd,
  485. },
  486. getAppState,
  487. setAppState,
  488. abortController: this.abortController,
  489. readFileState: this.readFileState,
  490. nestedMemoryAttachmentTriggers: new Set<string>(),
  491. loadedNestedMemoryPaths: this.loadedNestedMemoryPaths,
  492. dynamicSkillDirTriggers: new Set<string>(),
  493. discoveredSkillNames: this.discoveredSkillNames,
  494. setInProgressToolUseIDs: () => {},
  495. setResponseLength: () => {},
  496. updateFileHistoryState: processUserInputContext.updateFileHistoryState,
  497. updateAttributionState: processUserInputContext.updateAttributionState,
  498. setSDKStatus,
  499. }
  500. headlessProfilerCheckpoint('before_skills_plugins')
  501. // Cache-only: headless/SDK/CCR startup must not block on network for
  502. // ref-tracked plugins. CCR populates the cache via CLAUDE_CODE_SYNC_PLUGIN_INSTALL
  503. // (headlessPluginInstall) or CLAUDE_CODE_PLUGIN_SEED_DIR before this runs;
  504. // SDK callers that need fresh source can call /reload-plugins.
  505. const [skills, { enabled: enabledPlugins }] = await Promise.all([
  506. getSlashCommandToolSkills(getCwd()),
  507. loadAllPluginsCacheOnly(),
  508. ])
  509. headlessProfilerCheckpoint('after_skills_plugins')
  510. yield buildSystemInitMessage({
  511. tools,
  512. mcpClients,
  513. model: mainLoopModel,
  514. permissionMode: initialAppState.toolPermissionContext
  515. .mode as PermissionMode, // TODO: avoid the cast
  516. commands,
  517. agents,
  518. skills,
  519. plugins: enabledPlugins,
  520. fastMode: initialAppState.fastMode,
  521. })
  522. // Record when system message is yielded for headless latency tracking
  523. headlessProfilerCheckpoint('system_message_yielded')
  524. if (!shouldQuery) {
  525. // Return the results of local slash commands.
  526. // Use messagesFromUserInput (not replayableMessages) for command output
  527. // because selectableUserMessagesFilter excludes local-command-stdout tags.
  528. for (const msg of messagesFromUserInput) {
  529. if (
  530. msg.type === 'user' &&
  531. typeof msg.message.content === 'string' &&
  532. (msg.message.content.includes(`<${LOCAL_COMMAND_STDOUT_TAG}>`) ||
  533. msg.message.content.includes(`<${LOCAL_COMMAND_STDERR_TAG}>`) ||
  534. msg.isCompactSummary)
  535. ) {
  536. yield {
  537. type: 'user',
  538. message: {
  539. ...msg.message,
  540. content: stripAnsi(msg.message.content),
  541. },
  542. session_id: getSessionId(),
  543. parent_tool_use_id: null,
  544. uuid: msg.uuid,
  545. timestamp: msg.timestamp,
  546. isReplay: !msg.isCompactSummary,
  547. isSynthetic: msg.isMeta || msg.isVisibleInTranscriptOnly,
  548. } as SDKUserMessageReplay
  549. }
  550. // Local command output — yield as a synthetic assistant message so
  551. // RC renders it as assistant-style text rather than a user bubble.
  552. // Emitted as assistant (not the dedicated SDKLocalCommandOutputMessage
  553. // system subtype) so mobile clients + session-ingress can parse it.
  554. if (
  555. msg.type === 'system' &&
  556. msg.subtype === 'local_command' &&
  557. typeof msg.content === 'string' &&
  558. (msg.content.includes(`<${LOCAL_COMMAND_STDOUT_TAG}>`) ||
  559. msg.content.includes(`<${LOCAL_COMMAND_STDERR_TAG}>`))
  560. ) {
  561. yield localCommandOutputToSDKAssistantMessage(msg.content, msg.uuid)
  562. }
  563. if (msg.type === 'system' && msg.subtype === 'compact_boundary') {
  564. yield {
  565. type: 'system',
  566. subtype: 'compact_boundary' as const,
  567. session_id: getSessionId(),
  568. uuid: msg.uuid,
  569. compact_metadata: toSDKCompactMetadata(msg.compactMetadata),
  570. } as SDKCompactBoundaryMessage
  571. }
  572. }
  573. if (persistSession) {
  574. await recordTranscript(messages)
  575. if (
  576. isEnvTruthy(process.env.CLAUDE_CODE_EAGER_FLUSH) ||
  577. isEnvTruthy(process.env.CLAUDE_CODE_IS_COWORK)
  578. ) {
  579. await flushSessionStorage()
  580. }
  581. }
  582. yield {
  583. type: 'result',
  584. subtype: 'success',
  585. is_error: false,
  586. duration_ms: Date.now() - startTime,
  587. duration_api_ms: getTotalAPIDuration(),
  588. num_turns: messages.length - 1,
  589. result: resultText ?? '',
  590. stop_reason: null,
  591. session_id: getSessionId(),
  592. total_cost_usd: getTotalCost(),
  593. usage: this.totalUsage,
  594. modelUsage: getModelUsage(),
  595. permission_denials: this.permissionDenials,
  596. fast_mode_state: getFastModeState(
  597. mainLoopModel,
  598. initialAppState.fastMode,
  599. ),
  600. uuid: randomUUID(),
  601. }
  602. return
  603. }
  604. if (fileHistoryEnabled() && persistSession) {
  605. messagesFromUserInput
  606. .filter(messageSelector().selectableUserMessagesFilter)
  607. .forEach(message => {
  608. void fileHistoryMakeSnapshot(
  609. (updater: (prev: FileHistoryState) => FileHistoryState) => {
  610. setAppState(prev => ({
  611. ...prev,
  612. fileHistory: updater(prev.fileHistory),
  613. }))
  614. },
  615. message.uuid,
  616. )
  617. })
  618. }
  619. // Track current message usage (reset on each message_start)
  620. let currentMessageUsage: NonNullableUsage = EMPTY_USAGE
  621. let turnCount = 1
  622. let hasAcknowledgedInitialMessages = false
  623. // Track structured output from StructuredOutput tool calls
  624. let structuredOutputFromTool: unknown
  625. // Track the last stop_reason from assistant messages
  626. let lastStopReason: string | null = null
  627. // Reference-based watermark so error_during_execution's errors[] is
  628. // turn-scoped. A length-based index breaks when the 100-entry ring buffer
  629. // shift()s during the turn — the index slides. If this entry is rotated
  630. // out, lastIndexOf returns -1 and we include everything (safe fallback).
  631. const errorLogWatermark = getInMemoryErrors().at(-1)
  632. // Snapshot count before this query for delta-based retry limiting
  633. const initialStructuredOutputCalls = jsonSchema
  634. ? countToolCalls(this.mutableMessages, SYNTHETIC_OUTPUT_TOOL_NAME)
  635. : 0
  636. for await (const message of query({
  637. messages,
  638. systemPrompt,
  639. userContext,
  640. systemContext,
  641. canUseTool: wrappedCanUseTool,
  642. toolUseContext: processUserInputContext,
  643. fallbackModel,
  644. querySource: 'sdk',
  645. maxTurns,
  646. taskBudget,
  647. })) {
  648. // Record assistant, user, and compact boundary messages
  649. if (
  650. message.type === 'assistant' ||
  651. message.type === 'user' ||
  652. (message.type === 'system' && message.subtype === 'compact_boundary')
  653. ) {
  654. // Before writing a compact boundary, flush any in-memory-only
  655. // messages up through the preservedSegment tail. Attachments and
  656. // progress are now recorded inline (their switch cases below), but
  657. // this flush still matters for the preservedSegment tail walk.
  658. // If the SDK subprocess restarts before then (claude-desktop kills
  659. // between turns), tailUuid points to a never-written message →
  660. // applyPreservedSegmentRelinks fails its tail→head walk → returns
  661. // without pruning → resume loads full pre-compact history.
  662. if (
  663. persistSession &&
  664. message.type === 'system' &&
  665. message.subtype === 'compact_boundary'
  666. ) {
  667. const tailUuid = message.compactMetadata?.preservedSegment?.tailUuid
  668. if (tailUuid) {
  669. const tailIdx = this.mutableMessages.findLastIndex(
  670. m => m.uuid === tailUuid,
  671. )
  672. if (tailIdx !== -1) {
  673. await recordTranscript(this.mutableMessages.slice(0, tailIdx + 1))
  674. }
  675. }
  676. }
  677. messages.push(message)
  678. if (persistSession) {
  679. // Fire-and-forget for assistant messages. claude.ts yields one
  680. // assistant message per content block, then mutates the last
  681. // one's message.usage/stop_reason on message_delta — relying on
  682. // the write queue's 100ms lazy jsonStringify. Awaiting here
  683. // blocks ask()'s generator, so message_delta can't run until
  684. // every block is consumed; the drain timer (started at block 1)
  685. // elapses first. Interactive CC doesn't hit this because
  686. // useLogMessages.ts fire-and-forgets. enqueueWrite is
  687. // order-preserving so fire-and-forget here is safe.
  688. if (message.type === 'assistant') {
  689. void recordTranscript(messages)
  690. } else {
  691. await recordTranscript(messages)
  692. }
  693. }
  694. // Acknowledge initial user messages after first transcript recording
  695. if (!hasAcknowledgedInitialMessages && messagesToAck.length > 0) {
  696. hasAcknowledgedInitialMessages = true
  697. for (const msgToAck of messagesToAck) {
  698. if (msgToAck.type === 'user') {
  699. yield {
  700. type: 'user',
  701. message: msgToAck.message,
  702. session_id: getSessionId(),
  703. parent_tool_use_id: null,
  704. uuid: msgToAck.uuid,
  705. timestamp: msgToAck.timestamp,
  706. isReplay: true,
  707. } as SDKUserMessageReplay
  708. }
  709. }
  710. }
  711. }
  712. if (message.type === 'user') {
  713. turnCount++
  714. }
  715. switch (message.type) {
  716. case 'tombstone':
  717. // Tombstone messages are control signals for removing messages, skip them
  718. break
  719. case 'assistant':
  720. // Capture stop_reason if already set (synthetic messages). For
  721. // streamed responses, this is null at content_block_stop time;
  722. // the real value arrives via message_delta (handled below).
  723. if (message.message.stop_reason != null) {
  724. lastStopReason = message.message.stop_reason
  725. }
  726. this.mutableMessages.push(message)
  727. yield* normalizeMessage(message)
  728. break
  729. case 'progress':
  730. this.mutableMessages.push(message)
  731. // Record inline so the dedup loop in the next ask() call sees it
  732. // as already-recorded. Without this, deferred progress interleaves
  733. // with already-recorded tool_results in mutableMessages, and the
  734. // dedup walk freezes startingParentUuid at the wrong message —
  735. // forking the chain and orphaning the conversation on resume.
  736. if (persistSession) {
  737. messages.push(message)
  738. void recordTranscript(messages)
  739. }
  740. yield* normalizeMessage(message)
  741. break
  742. case 'user':
  743. this.mutableMessages.push(message)
  744. yield* normalizeMessage(message)
  745. break
  746. case 'stream_event':
  747. if (message.event.type === 'message_start') {
  748. // Reset current message usage for new message
  749. currentMessageUsage = EMPTY_USAGE
  750. currentMessageUsage = updateUsage(
  751. currentMessageUsage,
  752. message.event.message.usage,
  753. )
  754. }
  755. if (message.event.type === 'message_delta') {
  756. currentMessageUsage = updateUsage(
  757. currentMessageUsage,
  758. message.event.usage,
  759. )
  760. // Capture stop_reason from message_delta. The assistant message
  761. // is yielded at content_block_stop with stop_reason=null; the
  762. // real value only arrives here (see claude.ts message_delta
  763. // handler). Without this, result.stop_reason is always null.
  764. if (message.event.delta.stop_reason != null) {
  765. lastStopReason = message.event.delta.stop_reason
  766. }
  767. }
  768. if (message.event.type === 'message_stop') {
  769. // Accumulate current message usage into total
  770. this.totalUsage = accumulateUsage(
  771. this.totalUsage,
  772. currentMessageUsage,
  773. )
  774. }
  775. if (includePartialMessages) {
  776. yield {
  777. type: 'stream_event' as const,
  778. event: message.event,
  779. session_id: getSessionId(),
  780. parent_tool_use_id: null,
  781. uuid: randomUUID(),
  782. }
  783. }
  784. break
  785. case 'attachment':
  786. this.mutableMessages.push(message)
  787. // Record inline (same reason as progress above).
  788. if (persistSession) {
  789. messages.push(message)
  790. void recordTranscript(messages)
  791. }
  792. // Extract structured output from StructuredOutput tool calls
  793. if (message.attachment.type === 'structured_output') {
  794. structuredOutputFromTool = message.attachment.data
  795. }
  796. // Handle max turns reached signal from query.ts
  797. else if (message.attachment.type === 'max_turns_reached') {
  798. if (persistSession) {
  799. if (
  800. isEnvTruthy(process.env.CLAUDE_CODE_EAGER_FLUSH) ||
  801. isEnvTruthy(process.env.CLAUDE_CODE_IS_COWORK)
  802. ) {
  803. await flushSessionStorage()
  804. }
  805. }
  806. yield {
  807. type: 'result',
  808. subtype: 'error_max_turns',
  809. duration_ms: Date.now() - startTime,
  810. duration_api_ms: getTotalAPIDuration(),
  811. is_error: true,
  812. num_turns: message.attachment.turnCount,
  813. stop_reason: lastStopReason,
  814. session_id: getSessionId(),
  815. total_cost_usd: getTotalCost(),
  816. usage: this.totalUsage,
  817. modelUsage: getModelUsage(),
  818. permission_denials: this.permissionDenials,
  819. fast_mode_state: getFastModeState(
  820. mainLoopModel,
  821. initialAppState.fastMode,
  822. ),
  823. uuid: randomUUID(),
  824. errors: [
  825. `Reached maximum number of turns (${message.attachment.maxTurns})`,
  826. ],
  827. }
  828. return
  829. }
  830. // Yield queued_command attachments as SDK user message replays
  831. else if (
  832. replayUserMessages &&
  833. message.attachment.type === 'queued_command'
  834. ) {
  835. yield {
  836. type: 'user',
  837. message: {
  838. role: 'user' as const,
  839. content: message.attachment.prompt,
  840. },
  841. session_id: getSessionId(),
  842. parent_tool_use_id: null,
  843. uuid: message.attachment.source_uuid || message.uuid,
  844. timestamp: message.timestamp,
  845. isReplay: true,
  846. } as SDKUserMessageReplay
  847. }
  848. break
  849. case 'stream_request_start':
  850. // Don't yield stream request start messages
  851. break
  852. case 'system': {
  853. // Snip boundary: replay on our store to remove zombie messages and
  854. // stale markers. The yielded boundary is a signal, not data to push —
  855. // the replay produces its own equivalent boundary. Without this,
  856. // markers persist and re-trigger on every turn, and mutableMessages
  857. // never shrinks (memory leak in long SDK sessions). The subtype
  858. // check lives inside the injected callback so feature-gated strings
  859. // stay out of this file (excluded-strings check).
  860. const snipResult = this.config.snipReplay?.(
  861. message,
  862. this.mutableMessages,
  863. )
  864. if (snipResult !== undefined) {
  865. if (snipResult.executed) {
  866. this.mutableMessages.length = 0
  867. this.mutableMessages.push(...snipResult.messages)
  868. }
  869. break
  870. }
  871. this.mutableMessages.push(message)
  872. // Yield compact boundary messages to SDK
  873. if (
  874. message.subtype === 'compact_boundary' &&
  875. message.compactMetadata
  876. ) {
  877. // Release pre-compaction messages for GC. The boundary was just
  878. // pushed so it's the last element. query.ts already uses
  879. // getMessagesAfterCompactBoundary() internally, so only
  880. // post-boundary messages are needed going forward.
  881. const mutableBoundaryIdx = this.mutableMessages.length - 1
  882. if (mutableBoundaryIdx > 0) {
  883. this.mutableMessages.splice(0, mutableBoundaryIdx)
  884. }
  885. const localBoundaryIdx = messages.length - 1
  886. if (localBoundaryIdx > 0) {
  887. messages.splice(0, localBoundaryIdx)
  888. }
  889. yield {
  890. type: 'system',
  891. subtype: 'compact_boundary' as const,
  892. session_id: getSessionId(),
  893. uuid: message.uuid,
  894. compact_metadata: toSDKCompactMetadata(message.compactMetadata),
  895. }
  896. }
  897. if (message.subtype === 'api_error') {
  898. yield {
  899. type: 'system',
  900. subtype: 'api_retry' as const,
  901. attempt: message.retryAttempt,
  902. max_retries: message.maxRetries,
  903. retry_delay_ms: message.retryInMs,
  904. error_status: message.error.status ?? null,
  905. error: categorizeRetryableAPIError(message.error),
  906. session_id: getSessionId(),
  907. uuid: message.uuid,
  908. }
  909. }
  910. // Don't yield other system messages in headless mode
  911. break
  912. }
  913. case 'tool_use_summary':
  914. // Yield tool use summary messages to SDK
  915. yield {
  916. type: 'tool_use_summary' as const,
  917. summary: message.summary,
  918. preceding_tool_use_ids: message.precedingToolUseIds,
  919. session_id: getSessionId(),
  920. uuid: message.uuid,
  921. }
  922. break
  923. }
  924. // Check if USD budget has been exceeded
  925. if (maxBudgetUsd !== undefined && getTotalCost() >= maxBudgetUsd) {
  926. if (persistSession) {
  927. if (
  928. isEnvTruthy(process.env.CLAUDE_CODE_EAGER_FLUSH) ||
  929. isEnvTruthy(process.env.CLAUDE_CODE_IS_COWORK)
  930. ) {
  931. await flushSessionStorage()
  932. }
  933. }
  934. yield {
  935. type: 'result',
  936. subtype: 'error_max_budget_usd',
  937. duration_ms: Date.now() - startTime,
  938. duration_api_ms: getTotalAPIDuration(),
  939. is_error: true,
  940. num_turns: turnCount,
  941. stop_reason: lastStopReason,
  942. session_id: getSessionId(),
  943. total_cost_usd: getTotalCost(),
  944. usage: this.totalUsage,
  945. modelUsage: getModelUsage(),
  946. permission_denials: this.permissionDenials,
  947. fast_mode_state: getFastModeState(
  948. mainLoopModel,
  949. initialAppState.fastMode,
  950. ),
  951. uuid: randomUUID(),
  952. errors: [`Reached maximum budget ($${maxBudgetUsd})`],
  953. }
  954. return
  955. }
  956. // Check if structured output retry limit exceeded (only on user messages)
  957. if (message.type === 'user' && jsonSchema) {
  958. const currentCalls = countToolCalls(
  959. this.mutableMessages,
  960. SYNTHETIC_OUTPUT_TOOL_NAME,
  961. )
  962. const callsThisQuery = currentCalls - initialStructuredOutputCalls
  963. const maxRetries = parseInt(
  964. process.env.MAX_STRUCTURED_OUTPUT_RETRIES || '5',
  965. 10,
  966. )
  967. if (callsThisQuery >= maxRetries) {
  968. if (persistSession) {
  969. if (
  970. isEnvTruthy(process.env.CLAUDE_CODE_EAGER_FLUSH) ||
  971. isEnvTruthy(process.env.CLAUDE_CODE_IS_COWORK)
  972. ) {
  973. await flushSessionStorage()
  974. }
  975. }
  976. yield {
  977. type: 'result',
  978. subtype: 'error_max_structured_output_retries',
  979. duration_ms: Date.now() - startTime,
  980. duration_api_ms: getTotalAPIDuration(),
  981. is_error: true,
  982. num_turns: turnCount,
  983. stop_reason: lastStopReason,
  984. session_id: getSessionId(),
  985. total_cost_usd: getTotalCost(),
  986. usage: this.totalUsage,
  987. modelUsage: getModelUsage(),
  988. permission_denials: this.permissionDenials,
  989. fast_mode_state: getFastModeState(
  990. mainLoopModel,
  991. initialAppState.fastMode,
  992. ),
  993. uuid: randomUUID(),
  994. errors: [
  995. `Failed to provide valid structured output after ${maxRetries} attempts`,
  996. ],
  997. }
  998. return
  999. }
  1000. }
  1001. }
  1002. // Stop hooks yield progress/attachment messages AFTER the assistant
  1003. // response (via yield* handleStopHooks in query.ts). Since #23537 pushes
  1004. // those to `messages` inline, last(messages) can be a progress/attachment
  1005. // instead of the assistant — which makes textResult extraction below
  1006. // return '' and -p mode emit a blank line. Allowlist to assistant|user:
  1007. // isResultSuccessful handles both (user with all tool_result blocks is a
  1008. // valid successful terminal state).
  1009. const result = messages.findLast(
  1010. m => m.type === 'assistant' || m.type === 'user',
  1011. )
  1012. // Capture for the error_during_execution diagnostic — isResultSuccessful
  1013. // is a type predicate (message is Message), so inside the false branch
  1014. // `result` narrows to never and these accesses don't typecheck.
  1015. const edeResultType = result?.type ?? 'undefined'
  1016. const edeLastContentType =
  1017. result?.type === 'assistant'
  1018. ? (last(result.message.content)?.type ?? 'none')
  1019. : 'n/a'
  1020. // Flush buffered transcript writes before yielding result.
  1021. // The desktop app kills the CLI process immediately after receiving the
  1022. // result message, so any unflushed writes would be lost.
  1023. if (persistSession) {
  1024. if (
  1025. isEnvTruthy(process.env.CLAUDE_CODE_EAGER_FLUSH) ||
  1026. isEnvTruthy(process.env.CLAUDE_CODE_IS_COWORK)
  1027. ) {
  1028. await flushSessionStorage()
  1029. }
  1030. }
  1031. if (!isResultSuccessful(result, lastStopReason)) {
  1032. yield {
  1033. type: 'result',
  1034. subtype: 'error_during_execution',
  1035. duration_ms: Date.now() - startTime,
  1036. duration_api_ms: getTotalAPIDuration(),
  1037. is_error: true,
  1038. num_turns: turnCount,
  1039. stop_reason: lastStopReason,
  1040. session_id: getSessionId(),
  1041. total_cost_usd: getTotalCost(),
  1042. usage: this.totalUsage,
  1043. modelUsage: getModelUsage(),
  1044. permission_denials: this.permissionDenials,
  1045. fast_mode_state: getFastModeState(
  1046. mainLoopModel,
  1047. initialAppState.fastMode,
  1048. ),
  1049. uuid: randomUUID(),
  1050. // Diagnostic prefix: these are what isResultSuccessful() checks — if
  1051. // the result type isn't assistant-with-text/thinking or user-with-
  1052. // tool_result, and stop_reason isn't end_turn, that's why this fired.
  1053. // errors[] is turn-scoped via the watermark; previously it dumped the
  1054. // entire process's logError buffer (ripgrep timeouts, ENOENT, etc).
  1055. errors: (() => {
  1056. const all = getInMemoryErrors()
  1057. const start = errorLogWatermark
  1058. ? all.lastIndexOf(errorLogWatermark) + 1
  1059. : 0
  1060. return [
  1061. `[ede_diagnostic] result_type=${edeResultType} last_content_type=${edeLastContentType} stop_reason=${lastStopReason}`,
  1062. ...all.slice(start).map(_ => _.error),
  1063. ]
  1064. })(),
  1065. }
  1066. return
  1067. }
  1068. // Extract the text result based on message type
  1069. let textResult = ''
  1070. let isApiError = false
  1071. if (result.type === 'assistant') {
  1072. const lastContent = last(result.message.content)
  1073. if (
  1074. lastContent?.type === 'text' &&
  1075. !SYNTHETIC_MESSAGES.has(lastContent.text)
  1076. ) {
  1077. textResult = lastContent.text
  1078. }
  1079. isApiError = Boolean(result.isApiErrorMessage)
  1080. }
  1081. yield {
  1082. type: 'result',
  1083. subtype: 'success',
  1084. is_error: isApiError,
  1085. duration_ms: Date.now() - startTime,
  1086. duration_api_ms: getTotalAPIDuration(),
  1087. num_turns: turnCount,
  1088. result: textResult,
  1089. stop_reason: lastStopReason,
  1090. session_id: getSessionId(),
  1091. total_cost_usd: getTotalCost(),
  1092. usage: this.totalUsage,
  1093. modelUsage: getModelUsage(),
  1094. permission_denials: this.permissionDenials,
  1095. structured_output: structuredOutputFromTool,
  1096. fast_mode_state: getFastModeState(
  1097. mainLoopModel,
  1098. initialAppState.fastMode,
  1099. ),
  1100. uuid: randomUUID(),
  1101. }
  1102. }
  1103. interrupt(): void {
  1104. this.abortController.abort()
  1105. }
  1106. getMessages(): readonly Message[] {
  1107. return this.mutableMessages
  1108. }
  1109. getReadFileState(): FileStateCache {
  1110. return this.readFileState
  1111. }
  1112. getSessionId(): string {
  1113. return getSessionId()
  1114. }
  1115. setModel(model: string): void {
  1116. this.config.userSpecifiedModel = model
  1117. }
  1118. }
  1119. /**
  1120. * Sends a single prompt to the Claude API and returns the response.
  1121. * Assumes that claude is being used non-interactively -- will not
  1122. * ask the user for permissions or further input.
  1123. *
  1124. * Convenience wrapper around QueryEngine for one-shot usage.
  1125. */
  1126. export async function* ask({
  1127. commands,
  1128. prompt,
  1129. promptUuid,
  1130. isMeta,
  1131. cwd,
  1132. tools,
  1133. mcpClients,
  1134. verbose = false,
  1135. thinkingConfig,
  1136. maxTurns,
  1137. maxBudgetUsd,
  1138. taskBudget,
  1139. canUseTool,
  1140. mutableMessages = [],
  1141. getReadFileCache,
  1142. setReadFileCache,
  1143. customSystemPrompt,
  1144. appendSystemPrompt,
  1145. userSpecifiedModel,
  1146. fallbackModel,
  1147. jsonSchema,
  1148. getAppState,
  1149. setAppState,
  1150. abortController,
  1151. replayUserMessages = false,
  1152. includePartialMessages = false,
  1153. handleElicitation,
  1154. agents = [],
  1155. setSDKStatus,
  1156. orphanedPermission,
  1157. }: {
  1158. commands: Command[]
  1159. prompt: string | Array<ContentBlockParam>
  1160. promptUuid?: string
  1161. isMeta?: boolean
  1162. cwd: string
  1163. tools: Tools
  1164. verbose?: boolean
  1165. mcpClients: MCPServerConnection[]
  1166. thinkingConfig?: ThinkingConfig
  1167. maxTurns?: number
  1168. maxBudgetUsd?: number
  1169. taskBudget?: { total: number }
  1170. canUseTool: CanUseToolFn
  1171. mutableMessages?: Message[]
  1172. customSystemPrompt?: string
  1173. appendSystemPrompt?: string
  1174. userSpecifiedModel?: string
  1175. fallbackModel?: string
  1176. jsonSchema?: Record<string, unknown>
  1177. getAppState: () => AppState
  1178. setAppState: (f: (prev: AppState) => AppState) => void
  1179. getReadFileCache: () => FileStateCache
  1180. setReadFileCache: (cache: FileStateCache) => void
  1181. abortController?: AbortController
  1182. replayUserMessages?: boolean
  1183. includePartialMessages?: boolean
  1184. handleElicitation?: ToolUseContext['handleElicitation']
  1185. agents?: AgentDefinition[]
  1186. setSDKStatus?: (status: SDKStatus) => void
  1187. orphanedPermission?: OrphanedPermission
  1188. }): AsyncGenerator<SDKMessage, void, unknown> {
  1189. const engine = new QueryEngine({
  1190. cwd,
  1191. tools,
  1192. commands,
  1193. mcpClients,
  1194. agents,
  1195. canUseTool,
  1196. getAppState,
  1197. setAppState,
  1198. initialMessages: mutableMessages,
  1199. readFileCache: cloneFileStateCache(getReadFileCache()),
  1200. customSystemPrompt,
  1201. appendSystemPrompt,
  1202. userSpecifiedModel,
  1203. fallbackModel,
  1204. thinkingConfig,
  1205. maxTurns,
  1206. maxBudgetUsd,
  1207. taskBudget,
  1208. jsonSchema,
  1209. verbose,
  1210. handleElicitation,
  1211. replayUserMessages,
  1212. includePartialMessages,
  1213. setSDKStatus,
  1214. abortController,
  1215. orphanedPermission,
  1216. ...(feature('HISTORY_SNIP')
  1217. ? {
  1218. snipReplay: (yielded: Message, store: Message[]) => {
  1219. if (!snipProjection!.isSnipBoundaryMessage(yielded))
  1220. return undefined
  1221. return snipModule!.snipCompactIfNeeded(store, { force: true })
  1222. },
  1223. }
  1224. : {}),
  1225. })
  1226. try {
  1227. yield* engine.submitMessage(prompt, {
  1228. uuid: promptUuid,
  1229. isMeta,
  1230. })
  1231. } finally {
  1232. setReadFileCache(engine.getReadFileState())
  1233. }
  1234. }