presentAssistantMessage.ts 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217
  1. import cloneDeep from "clone-deep"
  2. import { serializeError } from "serialize-error"
  3. import { Anthropic } from "@anthropic-ai/sdk"
  4. import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
  5. import { ConsecutiveMistakeError } from "@roo-code/types"
  6. import { TelemetryService } from "@roo-code/telemetry"
  7. import { customToolRegistry } from "@roo-code/core"
  8. import { t } from "../../i18n"
  9. import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
  10. import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"
  11. import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
  12. import { AskIgnoredError } from "../task/AskIgnoredError"
  13. import { Task } from "../task/Task"
  14. import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
  15. import { listFilesTool } from "../tools/ListFilesTool"
  16. import { readFileTool } from "../tools/ReadFileTool"
  17. import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool"
  18. import { shouldUseSingleFileRead, TOOL_PROTOCOL } from "@roo-code/types"
  19. import { writeToFileTool } from "../tools/WriteToFileTool"
  20. import { applyDiffTool } from "../tools/MultiApplyDiffTool"
  21. import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
  22. import { searchReplaceTool } from "../tools/SearchReplaceTool"
  23. import { editFileTool } from "../tools/EditFileTool"
  24. import { applyPatchTool } from "../tools/ApplyPatchTool"
  25. import { searchFilesTool } from "../tools/SearchFilesTool"
  26. import { browserActionTool } from "../tools/BrowserActionTool"
  27. import { executeCommandTool } from "../tools/ExecuteCommandTool"
  28. import { useMcpToolTool } from "../tools/UseMcpToolTool"
  29. import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
  30. import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
  31. import { switchModeTool } from "../tools/SwitchModeTool"
  32. import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
  33. import { newTaskTool } from "../tools/NewTaskTool"
  34. import { updateTodoListTool } from "../tools/UpdateTodoListTool"
  35. import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
  36. import { generateImageTool } from "../tools/GenerateImageTool"
  37. import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
  38. import { validateToolUse } from "../tools/validateToolUse"
  39. import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
  40. import { formatResponse } from "../prompts/responses"
  41. /**
  42. * Processes and presents assistant message content to the user interface.
  43. *
  44. * This function is the core message handling system that:
  45. * - Sequentially processes content blocks from the assistant's response.
  46. * - Displays text content to the user.
  47. * - Executes tool use requests with appropriate user approval.
  48. * - Manages the flow of conversation by determining when to proceed to the next content block.
  49. * - Coordinates file system checkpointing for modified files.
  50. * - Controls the conversation state to determine when to continue to the next request.
  51. *
  52. * The function uses a locking mechanism to prevent concurrent execution and handles
  53. * partial content blocks during streaming. It's designed to work with the streaming
  54. * API response pattern, where content arrives incrementally and needs to be processed
  55. * as it becomes available.
  56. */
  57. export async function presentAssistantMessage(cline: Task) {
  58. if (cline.abort) {
  59. throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`)
  60. }
  61. if (cline.presentAssistantMessageLocked) {
  62. cline.presentAssistantMessageHasPendingUpdates = true
  63. return
  64. }
  65. cline.presentAssistantMessageLocked = true
  66. cline.presentAssistantMessageHasPendingUpdates = false
  67. if (cline.currentStreamingContentIndex >= cline.assistantMessageContent.length) {
  68. // This may happen if the last content block was completed before
  69. // streaming could finish. If streaming is finished, and we're out of
  70. // bounds then this means we already presented/executed the last
  71. // content block and are ready to continue to next request.
  72. if (cline.didCompleteReadingStream) {
  73. cline.userMessageContentReady = true
  74. }
  75. cline.presentAssistantMessageLocked = false
  76. return
  77. }
  78. let block: any
  79. try {
  80. block = cloneDeep(cline.assistantMessageContent[cline.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
  81. } catch (error) {
  82. console.error(`ERROR cloning block:`, error)
  83. console.error(
  84. `Block content:`,
  85. JSON.stringify(cline.assistantMessageContent[cline.currentStreamingContentIndex], null, 2),
  86. )
  87. cline.presentAssistantMessageLocked = false
  88. return
  89. }
  90. switch (block.type) {
  91. case "mcp_tool_use": {
  92. // Handle native MCP tool calls (from mcp_serverName_toolName dynamic tools)
  93. // These are converted to the same execution path as use_mcp_tool but preserve
  94. // their original name in API history
  95. const mcpBlock = block as McpToolUse
  96. if (cline.didRejectTool) {
  97. // For native protocol, we must send a tool_result for every tool_use to avoid API errors
  98. const toolCallId = mcpBlock.id
  99. const errorMessage = !mcpBlock.partial
  100. ? `Skipping MCP tool ${mcpBlock.name} due to user rejecting a previous tool.`
  101. : `MCP tool ${mcpBlock.name} was interrupted and not executed due to user rejecting a previous tool.`
  102. if (toolCallId) {
  103. cline.userMessageContent.push({
  104. type: "tool_result",
  105. tool_use_id: toolCallId,
  106. content: errorMessage,
  107. is_error: true,
  108. } as Anthropic.ToolResultBlockParam)
  109. }
  110. break
  111. }
  112. if (cline.didAlreadyUseTool) {
  113. const toolCallId = mcpBlock.id
  114. const errorMessage = `MCP tool [${mcpBlock.name}] was not executed because a tool has already been used in this message. Only one tool may be used per message.`
  115. if (toolCallId) {
  116. cline.userMessageContent.push({
  117. type: "tool_result",
  118. tool_use_id: toolCallId,
  119. content: errorMessage,
  120. is_error: true,
  121. } as Anthropic.ToolResultBlockParam)
  122. }
  123. break
  124. }
  125. // Track if we've already pushed a tool result
  126. let hasToolResult = false
  127. const toolCallId = mcpBlock.id
  128. const toolProtocol = TOOL_PROTOCOL.NATIVE // MCP tools in native mode always use native protocol
  129. const pushToolResult = (content: ToolResponse) => {
  130. if (hasToolResult) {
  131. console.warn(
  132. `[presentAssistantMessage] Skipping duplicate tool_result for mcp_tool_use: ${toolCallId}`,
  133. )
  134. return
  135. }
  136. let resultContent: string
  137. let imageBlocks: Anthropic.ImageBlockParam[] = []
  138. if (typeof content === "string") {
  139. resultContent = content || "(tool did not return anything)"
  140. } else {
  141. const textBlocks = content.filter((item) => item.type === "text")
  142. imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
  143. resultContent =
  144. textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
  145. "(tool did not return anything)"
  146. }
  147. if (toolCallId) {
  148. cline.userMessageContent.push({
  149. type: "tool_result",
  150. tool_use_id: toolCallId,
  151. content: resultContent,
  152. } as Anthropic.ToolResultBlockParam)
  153. if (imageBlocks.length > 0) {
  154. cline.userMessageContent.push(...imageBlocks)
  155. }
  156. }
  157. hasToolResult = true
  158. cline.didAlreadyUseTool = true
  159. }
  160. const toolDescription = () => `[mcp_tool: ${mcpBlock.serverName}/${mcpBlock.toolName}]`
  161. const askApproval = async (
  162. type: ClineAsk,
  163. partialMessage?: string,
  164. progressStatus?: ToolProgressStatus,
  165. isProtected?: boolean,
  166. ) => {
  167. const { response, text, images } = await cline.ask(
  168. type,
  169. partialMessage,
  170. false,
  171. progressStatus,
  172. isProtected || false,
  173. )
  174. if (response !== "yesButtonClicked") {
  175. if (text) {
  176. await cline.say("user_feedback", text, images)
  177. pushToolResult(
  178. formatResponse.toolResult(
  179. formatResponse.toolDeniedWithFeedback(text, toolProtocol),
  180. images,
  181. ),
  182. )
  183. } else {
  184. pushToolResult(formatResponse.toolDenied(toolProtocol))
  185. }
  186. cline.didRejectTool = true
  187. return false
  188. }
  189. if (text) {
  190. await cline.say("user_feedback", text, images)
  191. pushToolResult(
  192. formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text, toolProtocol), images),
  193. )
  194. }
  195. return true
  196. }
  197. const handleError = async (action: string, error: Error) => {
  198. // Silently ignore AskIgnoredError - this is an internal control flow
  199. // signal, not an actual error. It occurs when a newer ask supersedes an older one.
  200. if (error instanceof AskIgnoredError) {
  201. return
  202. }
  203. const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
  204. await cline.say(
  205. "error",
  206. `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
  207. )
  208. pushToolResult(formatResponse.toolError(errorString, toolProtocol))
  209. }
  210. if (!mcpBlock.partial) {
  211. cline.recordToolUsage("use_mcp_tool") // Record as use_mcp_tool for analytics
  212. TelemetryService.instance.captureToolUsage(cline.taskId, "use_mcp_tool", toolProtocol)
  213. }
  214. // Resolve sanitized server name back to original server name
  215. // The serverName from parsing is sanitized (e.g., "my_server" from "my server")
  216. // We need the original name to find the actual MCP connection
  217. const mcpHub = cline.providerRef.deref()?.getMcpHub()
  218. let resolvedServerName = mcpBlock.serverName
  219. if (mcpHub) {
  220. const originalName = mcpHub.findServerNameBySanitizedName(mcpBlock.serverName)
  221. if (originalName) {
  222. resolvedServerName = originalName
  223. }
  224. }
  225. // Execute the MCP tool using the same handler as use_mcp_tool
  226. // Create a synthetic ToolUse block that the useMcpToolTool can handle
  227. const syntheticToolUse: ToolUse<"use_mcp_tool"> = {
  228. type: "tool_use",
  229. id: mcpBlock.id,
  230. name: "use_mcp_tool",
  231. params: {
  232. server_name: resolvedServerName,
  233. tool_name: mcpBlock.toolName,
  234. arguments: JSON.stringify(mcpBlock.arguments),
  235. },
  236. partial: mcpBlock.partial,
  237. nativeArgs: {
  238. server_name: resolvedServerName,
  239. tool_name: mcpBlock.toolName,
  240. arguments: mcpBlock.arguments,
  241. },
  242. }
  243. await useMcpToolTool.handle(cline, syntheticToolUse, {
  244. askApproval,
  245. handleError,
  246. pushToolResult,
  247. removeClosingTag: (tag, text) => text || "",
  248. toolProtocol,
  249. })
  250. break
  251. }
  252. case "text": {
  253. if (cline.didRejectTool || cline.didAlreadyUseTool) {
  254. break
  255. }
  256. let content = block.content
  257. if (content) {
  258. // Have to do this for partial and complete since sending
  259. // content in thinking tags to markdown renderer will
  260. // automatically be removed.
  261. // Remove end substrings of <thinking or </thinking (below xml
  262. // parsing is only for opening tags).
  263. // Tthis is done with the xml parsing below now, but keeping
  264. // here for reference.
  265. // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "")
  266. //
  267. // Remove all instances of <thinking> (with optional line break
  268. // after) and </thinking> (with optional line break before).
  269. // - Needs to be separate since we dont want to remove the line
  270. // break before the first tag.
  271. // - Needs to happen before the xml parsing below.
  272. content = content.replace(/<thinking>\s?/g, "")
  273. content = content.replace(/\s?<\/thinking>/g, "")
  274. // Remove partial XML tag at the very end of the content (for
  275. // tool use and thinking tags), Prevents scrollview from
  276. // jumping when tags are automatically removed.
  277. const lastOpenBracketIndex = content.lastIndexOf("<")
  278. if (lastOpenBracketIndex !== -1) {
  279. const possibleTag = content.slice(lastOpenBracketIndex)
  280. // Check if there's a '>' after the last '<' (i.e., if the
  281. // tag is complete) (complete thinking and tool tags will
  282. // have been removed by now.)
  283. const hasCloseBracket = possibleTag.includes(">")
  284. if (!hasCloseBracket) {
  285. // Extract the potential tag name.
  286. let tagContent: string
  287. if (possibleTag.startsWith("</")) {
  288. tagContent = possibleTag.slice(2).trim()
  289. } else {
  290. tagContent = possibleTag.slice(1).trim()
  291. }
  292. // Check if tagContent is likely an incomplete tag name
  293. // (letters and underscores only).
  294. const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent)
  295. // Preemptively remove < or </ to keep from these
  296. // artifacts showing up in chat (also handles closing
  297. // thinking tags).
  298. const isOpeningOrClosing = possibleTag === "<" || possibleTag === "</"
  299. // If the tag is incomplete and at the end, remove it
  300. // from the content.
  301. if (isOpeningOrClosing || isLikelyTagName) {
  302. content = content.slice(0, lastOpenBracketIndex).trim()
  303. }
  304. }
  305. }
  306. }
  307. await cline.say("text", content, undefined, block.partial)
  308. break
  309. }
  310. case "tool_use": {
  311. // Fetch state early so it's available for toolDescription and validation
  312. const state = await cline.providerRef.deref()?.getState()
  313. const { mode, customModes, experiments: stateExperiments } = state ?? {}
  314. const toolDescription = (): string => {
  315. switch (block.name) {
  316. case "execute_command":
  317. return `[${block.name} for '${block.params.command}']`
  318. case "read_file":
  319. // Check if this model should use the simplified description
  320. const modelId = cline.api.getModel().id
  321. if (shouldUseSingleFileRead(modelId)) {
  322. return getSimpleReadFileToolDescription(block.name, block.params)
  323. } else {
  324. // Prefer native typed args when available; fall back to legacy params
  325. // Check if nativeArgs exists (native protocol)
  326. if (block.nativeArgs) {
  327. return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs)
  328. }
  329. return readFileTool.getReadFileToolDescription(block.name, block.params)
  330. }
  331. case "fetch_instructions":
  332. return `[${block.name} for '${block.params.task}']`
  333. case "write_to_file":
  334. return `[${block.name} for '${block.params.path}']`
  335. case "apply_diff":
  336. // Handle both legacy format and new multi-file format
  337. if (block.params.path) {
  338. return `[${block.name} for '${block.params.path}']`
  339. } else if (block.params.args) {
  340. // Try to extract first file path from args for display
  341. const match = block.params.args.match(/<file>.*?<path>([^<]+)<\/path>/s)
  342. if (match) {
  343. const firstPath = match[1]
  344. // Check if there are multiple files
  345. const fileCount = (block.params.args.match(/<file>/g) || []).length
  346. if (fileCount > 1) {
  347. return `[${block.name} for '${firstPath}' and ${fileCount - 1} more file${fileCount > 2 ? "s" : ""}]`
  348. } else {
  349. return `[${block.name} for '${firstPath}']`
  350. }
  351. }
  352. }
  353. return `[${block.name}]`
  354. case "search_files":
  355. return `[${block.name} for '${block.params.regex}'${
  356. block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
  357. }]`
  358. case "search_and_replace":
  359. return `[${block.name} for '${block.params.path}']`
  360. case "search_replace":
  361. return `[${block.name} for '${block.params.file_path}']`
  362. case "edit_file":
  363. return `[${block.name} for '${block.params.file_path}']`
  364. case "apply_patch":
  365. return `[${block.name}]`
  366. case "list_files":
  367. return `[${block.name} for '${block.params.path}']`
  368. case "browser_action":
  369. return `[${block.name} for '${block.params.action}']`
  370. case "use_mcp_tool":
  371. return `[${block.name} for '${block.params.server_name}']`
  372. case "access_mcp_resource":
  373. return `[${block.name} for '${block.params.server_name}']`
  374. case "ask_followup_question":
  375. return `[${block.name} for '${block.params.question}']`
  376. case "attempt_completion":
  377. return `[${block.name}]`
  378. case "switch_mode":
  379. return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
  380. case "codebase_search": // Add case for the new tool
  381. return `[${block.name} for '${block.params.query}']`
  382. case "update_todo_list":
  383. return `[${block.name}]`
  384. case "new_task": {
  385. const mode = block.params.mode ?? defaultModeSlug
  386. const message = block.params.message ?? "(no message)"
  387. const modeName = getModeBySlug(mode, customModes)?.name ?? mode
  388. return `[${block.name} in ${modeName} mode: '${message}']`
  389. }
  390. case "run_slash_command":
  391. return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
  392. case "generate_image":
  393. return `[${block.name} for '${block.params.path}']`
  394. default:
  395. return `[${block.name}]`
  396. }
  397. }
  398. if (cline.didRejectTool) {
  399. // Ignore any tool content after user has rejected tool once.
  400. // For native protocol, we must send a tool_result for every tool_use to avoid API errors
  401. const toolCallId = block.id
  402. const errorMessage = !block.partial
  403. ? `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`
  404. : `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`
  405. if (toolCallId) {
  406. // Native protocol: MUST send tool_result for every tool_use
  407. cline.userMessageContent.push({
  408. type: "tool_result",
  409. tool_use_id: toolCallId,
  410. content: errorMessage,
  411. is_error: true,
  412. } as Anthropic.ToolResultBlockParam)
  413. } else {
  414. // XML protocol: send as text
  415. cline.userMessageContent.push({
  416. type: "text",
  417. text: errorMessage,
  418. })
  419. }
  420. break
  421. }
  422. if (cline.didAlreadyUseTool) {
  423. // Ignore any content after a tool has already been used.
  424. // For native protocol, we must send a tool_result for every tool_use to avoid API errors
  425. const toolCallId = block.id
  426. const errorMessage = `Tool [${block.name}] was not executed because a tool has already been used in this message. Only one tool may be used per message. You must assess the first tool's result before proceeding to use the next tool.`
  427. if (toolCallId) {
  428. // Native protocol: MUST send tool_result for every tool_use
  429. cline.userMessageContent.push({
  430. type: "tool_result",
  431. tool_use_id: toolCallId,
  432. content: errorMessage,
  433. is_error: true,
  434. } as Anthropic.ToolResultBlockParam)
  435. } else {
  436. // XML protocol: send as text
  437. cline.userMessageContent.push({
  438. type: "text",
  439. text: errorMessage,
  440. })
  441. }
  442. break
  443. }
  444. // Track if we've already pushed a tool result for this tool call (native protocol only)
  445. let hasToolResult = false
  446. // Determine protocol by checking if this tool call has an ID.
  447. // Native protocol tool calls ALWAYS have an ID (set when parsed from tool_call chunks).
  448. // XML protocol tool calls NEVER have an ID (parsed from XML text).
  449. const toolCallId = (block as any).id
  450. const toolProtocol = toolCallId ? TOOL_PROTOCOL.NATIVE : TOOL_PROTOCOL.XML
  451. // Multiple native tool calls feature is on hold - always disabled
  452. // Previously resolved from experiments.isEnabled(..., EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS)
  453. const isMultipleNativeToolCallsEnabled = false
  454. const pushToolResult = (content: ToolResponse) => {
  455. if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
  456. // For native protocol, only allow ONE tool_result per tool call
  457. if (hasToolResult) {
  458. console.warn(
  459. `[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${toolCallId}`,
  460. )
  461. return
  462. }
  463. // For native protocol, tool_result content must be a string
  464. // Images are added as separate blocks in the user message
  465. let resultContent: string
  466. let imageBlocks: Anthropic.ImageBlockParam[] = []
  467. if (typeof content === "string") {
  468. resultContent = content || "(tool did not return anything)"
  469. } else {
  470. // Separate text and image blocks
  471. const textBlocks = content.filter((item) => item.type === "text")
  472. imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
  473. // Convert text blocks to string for tool_result
  474. resultContent =
  475. textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
  476. "(tool did not return anything)"
  477. }
  478. // Add tool_result with text content only
  479. cline.userMessageContent.push({
  480. type: "tool_result",
  481. tool_use_id: toolCallId,
  482. content: resultContent,
  483. } as Anthropic.ToolResultBlockParam)
  484. // Add image blocks separately after tool_result
  485. if (imageBlocks.length > 0) {
  486. cline.userMessageContent.push(...imageBlocks)
  487. }
  488. hasToolResult = true
  489. } else {
  490. // For XML protocol, add as text blocks (legacy behavior)
  491. cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
  492. if (typeof content === "string") {
  493. cline.userMessageContent.push({
  494. type: "text",
  495. text: content || "(tool did not return anything)",
  496. })
  497. } else {
  498. cline.userMessageContent.push(...content)
  499. }
  500. }
  501. // For XML protocol: Only one tool per message is allowed
  502. // For native protocol with experimental flag enabled: Multiple tools can be executed in sequence
  503. // For native protocol with experimental flag disabled: Single tool per message (default safe behavior)
  504. if (toolProtocol === TOOL_PROTOCOL.XML) {
  505. // Once a tool result has been collected, ignore all other tool
  506. // uses since we should only ever present one tool result per
  507. // message (XML protocol only).
  508. cline.didAlreadyUseTool = true
  509. } else if (toolProtocol === TOOL_PROTOCOL.NATIVE && !isMultipleNativeToolCallsEnabled) {
  510. // For native protocol with experimental flag disabled, enforce single tool per message
  511. cline.didAlreadyUseTool = true
  512. }
  513. // If toolProtocol is NATIVE and isMultipleNativeToolCallsEnabled is true,
  514. // allow multiple tool calls in sequence (don't set didAlreadyUseTool)
  515. }
  516. const askApproval = async (
  517. type: ClineAsk,
  518. partialMessage?: string,
  519. progressStatus?: ToolProgressStatus,
  520. isProtected?: boolean,
  521. ) => {
  522. const { response, text, images } = await cline.ask(
  523. type,
  524. partialMessage,
  525. false,
  526. progressStatus,
  527. isProtected || false,
  528. )
  529. if (response !== "yesButtonClicked") {
  530. // Handle both messageResponse and noButtonClicked with text.
  531. if (text) {
  532. await cline.say("user_feedback", text, images)
  533. pushToolResult(
  534. formatResponse.toolResult(
  535. formatResponse.toolDeniedWithFeedback(text, toolProtocol),
  536. images,
  537. ),
  538. )
  539. } else {
  540. pushToolResult(formatResponse.toolDenied(toolProtocol))
  541. }
  542. cline.didRejectTool = true
  543. return false
  544. }
  545. // Handle yesButtonClicked with text.
  546. if (text) {
  547. await cline.say("user_feedback", text, images)
  548. pushToolResult(
  549. formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text, toolProtocol), images),
  550. )
  551. }
  552. return true
  553. }
  554. const askFinishSubTaskApproval = async () => {
  555. // Ask the user to approve this task has completed, and he has
  556. // reviewed it, and we can declare task is finished and return
  557. // control to the parent task to continue running the rest of
  558. // the sub-tasks.
  559. const toolMessage = JSON.stringify({ tool: "finishTask" })
  560. return await askApproval("tool", toolMessage)
  561. }
  562. const handleError = async (action: string, error: Error) => {
  563. // Silently ignore AskIgnoredError - this is an internal control flow
  564. // signal, not an actual error. It occurs when a newer ask supersedes an older one.
  565. if (error instanceof AskIgnoredError) {
  566. return
  567. }
  568. const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
  569. await cline.say(
  570. "error",
  571. `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
  572. )
  573. pushToolResult(formatResponse.toolError(errorString, toolProtocol))
  574. }
  575. // If block is partial, remove partial closing tag so its not
  576. // presented to user.
  577. const removeClosingTag = (tag: ToolParamName, text?: string): string => {
  578. if (!block.partial) {
  579. return text || ""
  580. }
  581. if (!text) {
  582. return ""
  583. }
  584. // This regex dynamically constructs a pattern to match the
  585. // closing tag:
  586. // - Optionally matches whitespace before the tag.
  587. // - Matches '<' or '</' optionally followed by any subset of
  588. // characters from the tag name.
  589. const tagRegex = new RegExp(
  590. `\\s?<\/?${tag
  591. .split("")
  592. .map((char) => `(?:${char})?`)
  593. .join("")}$`,
  594. "g",
  595. )
  596. return text.replace(tagRegex, "")
  597. }
  598. // Keep browser open during an active session so other tools can run.
  599. // Session is active if we've seen any browser_action_result and the last browser_action is not "close".
  600. try {
  601. const messages = cline.clineMessages || []
  602. const hasStarted = messages.some((m: any) => m.say === "browser_action_result")
  603. let isClosed = false
  604. for (let i = messages.length - 1; i >= 0; i--) {
  605. const m = messages[i]
  606. if (m.say === "browser_action") {
  607. try {
  608. const act = JSON.parse(m.text || "{}")
  609. isClosed = act.action === "close"
  610. } catch {}
  611. break
  612. }
  613. }
  614. const sessionActive = hasStarted && !isClosed
  615. // Only auto-close when no active browser session is present, and this isn't a browser_action
  616. if (!sessionActive && block.name !== "browser_action") {
  617. await cline.browserSession.closeBrowser()
  618. }
  619. } catch {
  620. // On any unexpected error, fall back to conservative behavior
  621. if (block.name !== "browser_action") {
  622. await cline.browserSession.closeBrowser()
  623. }
  624. }
  625. if (!block.partial) {
  626. cline.recordToolUsage(block.name)
  627. TelemetryService.instance.captureToolUsage(cline.taskId, block.name, toolProtocol)
  628. }
  629. // Validate tool use before execution - ONLY for complete (non-partial) blocks.
  630. // Validating partial blocks would cause validation errors to be thrown repeatedly
  631. // during streaming, pushing multiple tool_results for the same tool_use_id and
  632. // potentially causing the stream to appear frozen.
  633. if (!block.partial) {
  634. const modelInfo = cline.api.getModel()
  635. // Resolve aliases in includedTools before validation
  636. // e.g., "edit_file" should resolve to "apply_diff"
  637. const rawIncludedTools = modelInfo?.info?.includedTools
  638. const { resolveToolAlias } = await import("../prompts/tools/filter-tools-for-mode")
  639. const includedTools = rawIncludedTools?.map((tool) => resolveToolAlias(tool))
  640. try {
  641. validateToolUse(
  642. block.name as ToolName,
  643. mode ?? defaultModeSlug,
  644. customModes ?? [],
  645. { apply_diff: cline.diffEnabled },
  646. block.params,
  647. stateExperiments,
  648. includedTools,
  649. )
  650. } catch (error) {
  651. cline.consecutiveMistakeCount++
  652. // For validation errors (unknown tool, tool not allowed for mode), we need to:
  653. // 1. Send a tool_result with the error (required for native protocol)
  654. // 2. NOT set didAlreadyUseTool = true (the tool was never executed, just failed validation)
  655. // This prevents the stream from being interrupted with "Response interrupted by tool use result"
  656. // which would cause the extension to appear to hang
  657. const errorContent = formatResponse.toolError(error.message, toolProtocol)
  658. if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
  659. // For native protocol, push tool_result directly without setting didAlreadyUseTool
  660. cline.userMessageContent.push({
  661. type: "tool_result",
  662. tool_use_id: toolCallId,
  663. content: typeof errorContent === "string" ? errorContent : "(validation error)",
  664. is_error: true,
  665. } as Anthropic.ToolResultBlockParam)
  666. } else {
  667. // For XML protocol, use the standard pushToolResult
  668. pushToolResult(errorContent)
  669. }
  670. break
  671. }
  672. }
  673. // Check for identical consecutive tool calls.
  674. if (!block.partial) {
  675. // Use the detector to check for repetition, passing the ToolUse
  676. // block directly.
  677. const repetitionCheck = cline.toolRepetitionDetector.check(block)
  678. // If execution is not allowed, notify user and break.
  679. if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
  680. // Handle repetition similar to mistake_limit_reached pattern.
  681. const { response, text, images } = await cline.ask(
  682. repetitionCheck.askUser.messageKey as ClineAsk,
  683. repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
  684. )
  685. if (response === "messageResponse") {
  686. // Add user feedback to userContent.
  687. cline.userMessageContent.push(
  688. {
  689. type: "text" as const,
  690. text: `Tool repetition limit reached. User feedback: ${text}`,
  691. },
  692. ...formatResponse.imageBlocks(images),
  693. )
  694. // Add user feedback to chat.
  695. await cline.say("user_feedback", text, images)
  696. }
  697. // Track tool repetition in telemetry via PostHog exception tracking and event.
  698. TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
  699. TelemetryService.instance.captureException(
  700. new ConsecutiveMistakeError(
  701. `Tool repetition limit reached for ${block.name}`,
  702. cline.taskId,
  703. cline.consecutiveMistakeCount,
  704. cline.consecutiveMistakeLimit,
  705. "tool_repetition",
  706. cline.apiConfiguration.apiProvider,
  707. cline.api.getModel().id,
  708. ),
  709. )
  710. // Return tool result message about the repetition
  711. pushToolResult(
  712. formatResponse.toolError(
  713. `Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
  714. toolProtocol,
  715. ),
  716. )
  717. break
  718. }
  719. }
  720. switch (block.name) {
  721. case "write_to_file":
  722. await checkpointSaveAndMark(cline)
  723. await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
  724. askApproval,
  725. handleError,
  726. pushToolResult,
  727. removeClosingTag,
  728. toolProtocol,
  729. })
  730. break
  731. case "update_todo_list":
  732. await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
  733. askApproval,
  734. handleError,
  735. pushToolResult,
  736. removeClosingTag,
  737. toolProtocol,
  738. })
  739. break
  740. case "apply_diff": {
  741. await checkpointSaveAndMark(cline)
  742. // Check if this tool call came from native protocol by checking for ID
  743. // Native calls always have IDs, XML calls never do
  744. if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
  745. await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
  746. askApproval,
  747. handleError,
  748. pushToolResult,
  749. removeClosingTag,
  750. toolProtocol,
  751. })
  752. break
  753. }
  754. // Get the provider and state to check experiment settings
  755. const provider = cline.providerRef.deref()
  756. let isMultiFileApplyDiffEnabled = false
  757. if (provider) {
  758. const state = await provider.getState()
  759. isMultiFileApplyDiffEnabled = experiments.isEnabled(
  760. state.experiments ?? {},
  761. EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,
  762. )
  763. }
  764. if (isMultiFileApplyDiffEnabled) {
  765. await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
  766. } else {
  767. await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
  768. askApproval,
  769. handleError,
  770. pushToolResult,
  771. removeClosingTag,
  772. toolProtocol,
  773. })
  774. }
  775. break
  776. }
  777. case "search_and_replace":
  778. await checkpointSaveAndMark(cline)
  779. await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
  780. askApproval,
  781. handleError,
  782. pushToolResult,
  783. removeClosingTag,
  784. toolProtocol,
  785. })
  786. break
  787. case "search_replace":
  788. await checkpointSaveAndMark(cline)
  789. await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, {
  790. askApproval,
  791. handleError,
  792. pushToolResult,
  793. removeClosingTag,
  794. toolProtocol,
  795. })
  796. break
  797. case "edit_file":
  798. await checkpointSaveAndMark(cline)
  799. await editFileTool.handle(cline, block as ToolUse<"edit_file">, {
  800. askApproval,
  801. handleError,
  802. pushToolResult,
  803. removeClosingTag,
  804. toolProtocol,
  805. })
  806. break
  807. case "apply_patch":
  808. await checkpointSaveAndMark(cline)
  809. await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, {
  810. askApproval,
  811. handleError,
  812. pushToolResult,
  813. removeClosingTag,
  814. toolProtocol,
  815. })
  816. break
  817. case "read_file":
  818. // Check if this model should use the simplified single-file read tool
  819. // Only use simplified tool for XML protocol - native protocol works with standard tool
  820. const modelId = cline.api.getModel().id
  821. if (shouldUseSingleFileRead(modelId) && toolProtocol !== TOOL_PROTOCOL.NATIVE) {
  822. await simpleReadFileTool(
  823. cline,
  824. block,
  825. askApproval,
  826. handleError,
  827. pushToolResult,
  828. removeClosingTag,
  829. toolProtocol,
  830. )
  831. } else {
  832. // Type assertion is safe here because we're in the "read_file" case
  833. await readFileTool.handle(cline, block as ToolUse<"read_file">, {
  834. askApproval,
  835. handleError,
  836. pushToolResult,
  837. removeClosingTag,
  838. toolProtocol,
  839. })
  840. }
  841. break
  842. case "fetch_instructions":
  843. await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, {
  844. askApproval,
  845. handleError,
  846. pushToolResult,
  847. removeClosingTag,
  848. toolProtocol,
  849. })
  850. break
  851. case "list_files":
  852. await listFilesTool.handle(cline, block as ToolUse<"list_files">, {
  853. askApproval,
  854. handleError,
  855. pushToolResult,
  856. removeClosingTag,
  857. toolProtocol,
  858. })
  859. break
  860. case "codebase_search":
  861. await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
  862. askApproval,
  863. handleError,
  864. pushToolResult,
  865. removeClosingTag,
  866. toolProtocol,
  867. })
  868. break
  869. case "search_files":
  870. await searchFilesTool.handle(cline, block as ToolUse<"search_files">, {
  871. askApproval,
  872. handleError,
  873. pushToolResult,
  874. removeClosingTag,
  875. toolProtocol,
  876. })
  877. break
  878. case "browser_action":
  879. await browserActionTool(
  880. cline,
  881. block as ToolUse<"browser_action">,
  882. askApproval,
  883. handleError,
  884. pushToolResult,
  885. removeClosingTag,
  886. )
  887. break
  888. case "execute_command":
  889. await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, {
  890. askApproval,
  891. handleError,
  892. pushToolResult,
  893. removeClosingTag,
  894. toolProtocol,
  895. })
  896. break
  897. case "use_mcp_tool":
  898. await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
  899. askApproval,
  900. handleError,
  901. pushToolResult,
  902. removeClosingTag,
  903. toolProtocol,
  904. })
  905. break
  906. case "access_mcp_resource":
  907. await accessMcpResourceTool.handle(cline, block as ToolUse<"access_mcp_resource">, {
  908. askApproval,
  909. handleError,
  910. pushToolResult,
  911. removeClosingTag,
  912. toolProtocol,
  913. })
  914. break
  915. case "ask_followup_question":
  916. await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, {
  917. askApproval,
  918. handleError,
  919. pushToolResult,
  920. removeClosingTag,
  921. toolProtocol,
  922. })
  923. break
  924. case "switch_mode":
  925. await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, {
  926. askApproval,
  927. handleError,
  928. pushToolResult,
  929. removeClosingTag,
  930. toolProtocol,
  931. })
  932. break
  933. case "new_task":
  934. await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
  935. askApproval,
  936. handleError,
  937. pushToolResult,
  938. removeClosingTag,
  939. toolProtocol,
  940. toolCallId: block.id,
  941. })
  942. break
  943. case "attempt_completion": {
  944. const completionCallbacks: AttemptCompletionCallbacks = {
  945. askApproval,
  946. handleError,
  947. pushToolResult,
  948. removeClosingTag,
  949. askFinishSubTaskApproval,
  950. toolDescription,
  951. toolProtocol,
  952. }
  953. await attemptCompletionTool.handle(
  954. cline,
  955. block as ToolUse<"attempt_completion">,
  956. completionCallbacks,
  957. )
  958. break
  959. }
  960. case "run_slash_command":
  961. await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, {
  962. askApproval,
  963. handleError,
  964. pushToolResult,
  965. removeClosingTag,
  966. toolProtocol,
  967. })
  968. break
  969. case "generate_image":
  970. await checkpointSaveAndMark(cline)
  971. await generateImageTool.handle(cline, block as ToolUse<"generate_image">, {
  972. askApproval,
  973. handleError,
  974. pushToolResult,
  975. removeClosingTag,
  976. toolProtocol,
  977. })
  978. break
  979. default: {
  980. // Handle unknown/invalid tool names OR custom tools
  981. // This is critical for native protocol where every tool_use MUST have a tool_result
  982. // CRITICAL: Don't process partial blocks for unknown tools - just let them stream in.
  983. // If we try to show errors for partial blocks, we'd show the error on every streaming chunk,
  984. // creating a loop that appears to freeze the extension. Only handle complete blocks.
  985. if (block.partial) {
  986. break
  987. }
  988. const customTool = stateExperiments?.customTools ? customToolRegistry.get(block.name) : undefined
  989. if (customTool) {
  990. try {
  991. let customToolArgs
  992. if (customTool.parameters) {
  993. try {
  994. customToolArgs = customTool.parameters.parse(block.nativeArgs || block.params || {})
  995. } catch (parseParamsError) {
  996. const message = `Custom tool "${block.name}" argument validation failed: ${parseParamsError.message}`
  997. console.error(message)
  998. cline.consecutiveMistakeCount++
  999. await cline.say("error", message)
  1000. pushToolResult(formatResponse.toolError(message, toolProtocol))
  1001. break
  1002. }
  1003. }
  1004. const result = await customTool.execute(customToolArgs, {
  1005. mode: mode ?? defaultModeSlug,
  1006. task: cline,
  1007. })
  1008. console.log(
  1009. `${customTool.name}.execute(): ${JSON.stringify(customToolArgs)} -> ${JSON.stringify(result)}`,
  1010. )
  1011. pushToolResult(result)
  1012. cline.consecutiveMistakeCount = 0
  1013. } catch (executionError: any) {
  1014. cline.consecutiveMistakeCount++
  1015. await handleError(`executing custom tool "${block.name}"`, executionError)
  1016. }
  1017. break
  1018. }
  1019. // Not a custom tool - handle as unknown tool error
  1020. const errorMessage = `Unknown tool "${block.name}". This tool does not exist. Please use one of the available tools.`
  1021. cline.consecutiveMistakeCount++
  1022. cline.recordToolError(block.name as ToolName, errorMessage)
  1023. await cline.say("error", t("tools:unknownToolError", { toolName: block.name }))
  1024. // Push tool_result directly for native protocol WITHOUT setting didAlreadyUseTool
  1025. // This prevents the stream from being interrupted with "Response interrupted by tool use result"
  1026. if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
  1027. cline.userMessageContent.push({
  1028. type: "tool_result",
  1029. tool_use_id: toolCallId,
  1030. content: formatResponse.toolError(errorMessage, toolProtocol),
  1031. is_error: true,
  1032. } as Anthropic.ToolResultBlockParam)
  1033. } else {
  1034. pushToolResult(formatResponse.toolError(errorMessage, toolProtocol))
  1035. }
  1036. break
  1037. }
  1038. }
  1039. break
  1040. }
  1041. }
  1042. // Seeing out of bounds is fine, it means that the next too call is being
  1043. // built up and ready to add to assistantMessageContent to present.
  1044. // When you see the UI inactive during this, it means that a tool is
  1045. // breaking without presenting any UI. For example the write_to_file tool
  1046. // was breaking when relpath was undefined, and for invalid relpath it never
  1047. // presented UI.
  1048. // This needs to be placed here, if not then calling
  1049. // cline.presentAssistantMessage below would fail (sometimes) since it's
  1050. // locked.
  1051. cline.presentAssistantMessageLocked = false
  1052. // NOTE: When tool is rejected, iterator stream is interrupted and it waits
  1053. // for `userMessageContentReady` to be true. Future calls to present will
  1054. // skip execution since `didRejectTool` and iterate until `contentIndex` is
  1055. // set to message length and it sets userMessageContentReady to true itself
  1056. // (instead of preemptively doing it in iterator).
  1057. if (!block.partial || cline.didRejectTool || cline.didAlreadyUseTool) {
  1058. // Block is finished streaming and executing.
  1059. if (cline.currentStreamingContentIndex === cline.assistantMessageContent.length - 1) {
  1060. // It's okay that we increment if !didCompleteReadingStream, it'll
  1061. // just return because out of bounds and as streaming continues it
  1062. // will call `presentAssitantMessage` if a new block is ready. If
  1063. // streaming is finished then we set `userMessageContentReady` to
  1064. // true when out of bounds. This gracefully allows the stream to
  1065. // continue on and all potential content blocks be presented.
  1066. // Last block is complete and it is finished executing
  1067. cline.userMessageContentReady = true // Will allow `pWaitFor` to continue.
  1068. }
  1069. // Call next block if it exists (if not then read stream will call it
  1070. // when it's ready).
  1071. // Need to increment regardless, so when read stream calls this function
  1072. // again it will be streaming the next block.
  1073. cline.currentStreamingContentIndex++
  1074. if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) {
  1075. // There are already more content blocks to stream, so we'll call
  1076. // this function ourselves.
  1077. presentAssistantMessage(cline)
  1078. return
  1079. } else {
  1080. // CRITICAL FIX: If we're out of bounds and the stream is complete, set userMessageContentReady
  1081. // This handles the case where assistantMessageContent is empty or becomes empty after processing
  1082. if (cline.didCompleteReadingStream) {
  1083. cline.userMessageContentReady = true
  1084. }
  1085. }
  1086. }
  1087. // Block is partial, but the read stream may have finished.
  1088. if (cline.presentAssistantMessageHasPendingUpdates) {
  1089. presentAssistantMessage(cline)
  1090. }
  1091. }
  1092. /**
  1093. * save checkpoint and mark done in the current streaming task.
  1094. * @param task The Task instance to checkpoint save and mark.
  1095. * @returns
  1096. */
  1097. async function checkpointSaveAndMark(task: Task) {
  1098. if (task.currentStreamingDidCheckpoint) {
  1099. return
  1100. }
  1101. try {
  1102. await task.checkpointSave(true)
  1103. task.currentStreamingDidCheckpoint = true
  1104. } catch (error) {
  1105. console.error(`[Task#presentAssistantMessage] Error saving checkpoint: ${error.message}`, error)
  1106. }
  1107. }