QueryEngine.ts 47 KB

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