presentAssistantMessage.ts 36 KB

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