queryContext.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /**
  2. * Shared helpers for building the API cache-key prefix (systemPrompt,
  3. * userContext, systemContext) for query() calls.
  4. *
  5. * Lives in its own file because it imports from context.ts and
  6. * constants/prompts.ts, which are high in the dependency graph. Putting
  7. * these imports in systemPrompt.ts or sideQuestion.ts (both reachable
  8. * from commands.ts) would create cycles. Only entrypoint-layer files
  9. * import from here (QueryEngine.ts, cli/print.ts).
  10. */
  11. import type { Command } from '../commands.js'
  12. import { getSystemPrompt } from '../constants/prompts.js'
  13. import { getSystemContext, getUserContext } from '../context.js'
  14. import type { MCPServerConnection } from '../services/mcp/types.js'
  15. import type { AppState } from '../state/AppStateStore.js'
  16. import type { Tools, ToolUseContext } from '../Tool.js'
  17. import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js'
  18. import type { Message } from '../types/message.js'
  19. import { createAbortController } from './abortController.js'
  20. import type { FileStateCache } from './fileStateCache.js'
  21. import type { CacheSafeParams } from './forkedAgent.js'
  22. import { getMainLoopModel } from './model/model.js'
  23. import { asSystemPrompt } from './systemPromptType.js'
  24. import {
  25. shouldEnableThinkingByDefault,
  26. type ThinkingConfig,
  27. } from './thinking.js'
  28. /**
  29. * Fetch the three context pieces that form the API cache-key prefix:
  30. * systemPrompt parts, userContext, systemContext.
  31. *
  32. * When customSystemPrompt is set, the default getSystemPrompt build and
  33. * getSystemContext are skipped — the custom prompt replaces the default
  34. * entirely, and systemContext would be appended to a default that isn't
  35. * being used.
  36. *
  37. * Callers assemble the final systemPrompt from defaultSystemPrompt (or
  38. * customSystemPrompt) + optional extras + appendSystemPrompt. QueryEngine
  39. * injects coordinator userContext and memory-mechanics prompt on top;
  40. * sideQuestion's fallback uses the base result directly.
  41. */
  42. export async function fetchSystemPromptParts({
  43. tools,
  44. mainLoopModel,
  45. additionalWorkingDirectories,
  46. mcpClients,
  47. customSystemPrompt,
  48. }: {
  49. tools: Tools
  50. mainLoopModel: string
  51. additionalWorkingDirectories: string[]
  52. mcpClients: MCPServerConnection[]
  53. customSystemPrompt: string | undefined
  54. }): Promise<{
  55. defaultSystemPrompt: string[]
  56. userContext: { [k: string]: string }
  57. systemContext: { [k: string]: string }
  58. }> {
  59. const [defaultSystemPrompt, userContext, systemContext] = await Promise.all([
  60. customSystemPrompt !== undefined
  61. ? Promise.resolve([])
  62. : getSystemPrompt(
  63. tools,
  64. mainLoopModel,
  65. additionalWorkingDirectories,
  66. mcpClients,
  67. ),
  68. getUserContext(),
  69. customSystemPrompt !== undefined ? Promise.resolve({}) : getSystemContext(),
  70. ])
  71. return { defaultSystemPrompt, userContext, systemContext }
  72. }
  73. /**
  74. * Build CacheSafeParams from raw inputs when getLastCacheSafeParams() is null.
  75. *
  76. * Used by the SDK side_question handler (print.ts) on resume before a turn
  77. * completes — there's no stopHooks snapshot yet. Mirrors the system prompt
  78. * assembly in QueryEngine.ts:ask() so the rebuilt prefix matches what the
  79. * main loop will send, preserving the cache hit in the common case.
  80. *
  81. * May still miss the cache if the main loop applies extras this path doesn't
  82. * know about (coordinator mode, memory-mechanics prompt). That's acceptable —
  83. * the alternative is returning null and failing the side question entirely.
  84. */
  85. export async function buildSideQuestionFallbackParams({
  86. tools,
  87. commands,
  88. mcpClients,
  89. messages,
  90. readFileState,
  91. getAppState,
  92. setAppState,
  93. customSystemPrompt,
  94. appendSystemPrompt,
  95. thinkingConfig,
  96. agents,
  97. }: {
  98. tools: Tools
  99. commands: Command[]
  100. mcpClients: MCPServerConnection[]
  101. messages: Message[]
  102. readFileState: FileStateCache
  103. getAppState: () => AppState
  104. setAppState: (f: (prev: AppState) => AppState) => void
  105. customSystemPrompt: string | undefined
  106. appendSystemPrompt: string | undefined
  107. thinkingConfig: ThinkingConfig | undefined
  108. agents: AgentDefinition[]
  109. }): Promise<CacheSafeParams> {
  110. const mainLoopModel = getMainLoopModel()
  111. const appState = getAppState()
  112. const { defaultSystemPrompt, userContext, systemContext } =
  113. await fetchSystemPromptParts({
  114. tools,
  115. mainLoopModel,
  116. additionalWorkingDirectories: Array.from(
  117. appState.toolPermissionContext.additionalWorkingDirectories.keys(),
  118. ),
  119. mcpClients,
  120. customSystemPrompt,
  121. })
  122. const systemPrompt = asSystemPrompt([
  123. ...(customSystemPrompt !== undefined
  124. ? [customSystemPrompt]
  125. : defaultSystemPrompt),
  126. ...(appendSystemPrompt ? [appendSystemPrompt] : []),
  127. ])
  128. // Strip in-progress assistant message (stop_reason === null) — same guard
  129. // as btw.tsx. The SDK can fire side_question mid-turn.
  130. const last = messages.at(-1)
  131. const forkContextMessages =
  132. last?.type === 'assistant' && last.message.stop_reason === null
  133. ? messages.slice(0, -1)
  134. : messages
  135. const toolUseContext: ToolUseContext = {
  136. options: {
  137. commands,
  138. debug: false,
  139. mainLoopModel,
  140. tools,
  141. verbose: false,
  142. thinkingConfig:
  143. thinkingConfig ??
  144. (shouldEnableThinkingByDefault() !== false
  145. ? { type: 'adaptive' }
  146. : { type: 'disabled' }),
  147. mcpClients,
  148. mcpResources: {},
  149. isNonInteractiveSession: true,
  150. agentDefinitions: { activeAgents: agents, allAgents: [] },
  151. customSystemPrompt,
  152. appendSystemPrompt,
  153. },
  154. abortController: createAbortController(),
  155. readFileState,
  156. getAppState,
  157. setAppState,
  158. messages: forkContextMessages,
  159. setInProgressToolUseIDs: () => {},
  160. setResponseLength: () => {},
  161. updateFileHistoryState: () => {},
  162. updateAttributionState: () => {},
  163. }
  164. return {
  165. systemPrompt,
  166. userContext,
  167. systemContext,
  168. toolUseContext,
  169. forkContextMessages,
  170. }
  171. }