| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073 |
- import cloneDeep from "clone-deep"
- import { serializeError } from "serialize-error"
- import { Anthropic } from "@anthropic-ai/sdk"
- import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
- import { TelemetryService } from "@roo-code/telemetry"
- import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
- import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"
- import { Package } from "../../shared/package"
- import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
- import { listFilesTool } from "../tools/ListFilesTool"
- import { readFileTool } from "../tools/ReadFileTool"
- import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool"
- import { shouldUseSingleFileRead, TOOL_PROTOCOL } from "@roo-code/types"
- import { writeToFileTool } from "../tools/WriteToFileTool"
- import { applyDiffTool } from "../tools/MultiApplyDiffTool"
- import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
- import { applyPatchTool } from "../tools/ApplyPatchTool"
- import { listCodeDefinitionNamesTool } from "../tools/ListCodeDefinitionNamesTool"
- import { searchFilesTool } from "../tools/SearchFilesTool"
- import { browserActionTool } from "../tools/BrowserActionTool"
- import { executeCommandTool } from "../tools/ExecuteCommandTool"
- import { useMcpToolTool } from "../tools/UseMcpToolTool"
- import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
- import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
- import { switchModeTool } from "../tools/SwitchModeTool"
- import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
- import { newTaskTool } from "../tools/NewTaskTool"
- import { updateTodoListTool } from "../tools/UpdateTodoListTool"
- import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
- import { generateImageTool } from "../tools/GenerateImageTool"
- import { formatResponse } from "../prompts/responses"
- import { validateToolUse } from "../tools/validateToolUse"
- import { Task } from "../task/Task"
- import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
- import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
- import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
- import { isNativeProtocol } from "@roo-code/types"
- import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
- /**
- * Processes and presents assistant message content to the user interface.
- *
- * This function is the core message handling system that:
- * - Sequentially processes content blocks from the assistant's response.
- * - Displays text content to the user.
- * - Executes tool use requests with appropriate user approval.
- * - Manages the flow of conversation by determining when to proceed to the next content block.
- * - Coordinates file system checkpointing for modified files.
- * - Controls the conversation state to determine when to continue to the next request.
- *
- * The function uses a locking mechanism to prevent concurrent execution and handles
- * partial content blocks during streaming. It's designed to work with the streaming
- * API response pattern, where content arrives incrementally and needs to be processed
- * as it becomes available.
- */
- export async function presentAssistantMessage(cline: Task) {
- if (cline.abort) {
- throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`)
- }
- if (cline.presentAssistantMessageLocked) {
- cline.presentAssistantMessageHasPendingUpdates = true
- return
- }
- cline.presentAssistantMessageLocked = true
- cline.presentAssistantMessageHasPendingUpdates = false
- if (cline.currentStreamingContentIndex >= cline.assistantMessageContent.length) {
- // This may happen if the last content block was completed before
- // streaming could finish. If streaming is finished, and we're out of
- // bounds then this means we already presented/executed the last
- // content block and are ready to continue to next request.
- if (cline.didCompleteReadingStream) {
- cline.userMessageContentReady = true
- }
- cline.presentAssistantMessageLocked = false
- return
- }
- let block: any
- try {
- 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
- } catch (error) {
- console.error(`ERROR cloning block:`, error)
- console.error(
- `Block content:`,
- JSON.stringify(cline.assistantMessageContent[cline.currentStreamingContentIndex], null, 2),
- )
- cline.presentAssistantMessageLocked = false
- return
- }
- switch (block.type) {
- case "mcp_tool_use": {
- // Handle native MCP tool calls (from mcp_serverName_toolName dynamic tools)
- // These are converted to the same execution path as use_mcp_tool but preserve
- // their original name in API history
- const mcpBlock = block as McpToolUse
- if (cline.didRejectTool) {
- // For native protocol, we must send a tool_result for every tool_use to avoid API errors
- const toolCallId = mcpBlock.id
- const errorMessage = !mcpBlock.partial
- ? `Skipping MCP tool ${mcpBlock.name} due to user rejecting a previous tool.`
- : `MCP tool ${mcpBlock.name} was interrupted and not executed due to user rejecting a previous tool.`
- if (toolCallId) {
- cline.userMessageContent.push({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: errorMessage,
- is_error: true,
- } as Anthropic.ToolResultBlockParam)
- }
- break
- }
- if (cline.didAlreadyUseTool) {
- const toolCallId = mcpBlock.id
- 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.`
- if (toolCallId) {
- cline.userMessageContent.push({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: errorMessage,
- is_error: true,
- } as Anthropic.ToolResultBlockParam)
- }
- break
- }
- // Track if we've already pushed a tool result
- let hasToolResult = false
- const toolCallId = mcpBlock.id
- const toolProtocol = TOOL_PROTOCOL.NATIVE // MCP tools in native mode always use native protocol
- const pushToolResult = (content: ToolResponse) => {
- if (hasToolResult) {
- console.warn(
- `[presentAssistantMessage] Skipping duplicate tool_result for mcp_tool_use: ${toolCallId}`,
- )
- return
- }
- let resultContent: string
- let imageBlocks: Anthropic.ImageBlockParam[] = []
- if (typeof content === "string") {
- resultContent = content || "(tool did not return anything)"
- } else {
- const textBlocks = content.filter((item) => item.type === "text")
- imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
- resultContent =
- textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
- "(tool did not return anything)"
- }
- if (toolCallId) {
- cline.userMessageContent.push({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: resultContent,
- } as Anthropic.ToolResultBlockParam)
- if (imageBlocks.length > 0) {
- cline.userMessageContent.push(...imageBlocks)
- }
- }
- hasToolResult = true
- cline.didAlreadyUseTool = true
- }
- const toolDescription = () => `[mcp_tool: ${mcpBlock.serverName}/${mcpBlock.toolName}]`
- const askApproval = async (
- type: ClineAsk,
- partialMessage?: string,
- progressStatus?: ToolProgressStatus,
- isProtected?: boolean,
- ) => {
- const { response, text, images } = await cline.ask(
- type,
- partialMessage,
- false,
- progressStatus,
- isProtected || false,
- )
- if (response !== "yesButtonClicked") {
- if (text) {
- await cline.say("user_feedback", text, images)
- pushToolResult(
- formatResponse.toolResult(
- formatResponse.toolDeniedWithFeedback(text, toolProtocol),
- images,
- ),
- )
- } else {
- pushToolResult(formatResponse.toolDenied(toolProtocol))
- }
- cline.didRejectTool = true
- return false
- }
- if (text) {
- await cline.say("user_feedback", text, images)
- pushToolResult(
- formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text, toolProtocol), images),
- )
- }
- return true
- }
- const handleError = async (action: string, error: Error) => {
- const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
- await cline.say(
- "error",
- `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
- )
- pushToolResult(formatResponse.toolError(errorString, toolProtocol))
- }
- if (!mcpBlock.partial) {
- cline.recordToolUsage("use_mcp_tool") // Record as use_mcp_tool for analytics
- TelemetryService.instance.captureToolUsage(cline.taskId, "use_mcp_tool", toolProtocol)
- }
- // Execute the MCP tool using the same handler as use_mcp_tool
- // Create a synthetic ToolUse block that the useMcpToolTool can handle
- const syntheticToolUse: ToolUse<"use_mcp_tool"> = {
- type: "tool_use",
- id: mcpBlock.id,
- name: "use_mcp_tool",
- params: {
- server_name: mcpBlock.serverName,
- tool_name: mcpBlock.toolName,
- arguments: JSON.stringify(mcpBlock.arguments),
- },
- partial: mcpBlock.partial,
- nativeArgs: {
- server_name: mcpBlock.serverName,
- tool_name: mcpBlock.toolName,
- arguments: mcpBlock.arguments,
- },
- }
- await useMcpToolTool.handle(cline, syntheticToolUse, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag: (tag, text) => text || "",
- toolProtocol,
- })
- break
- }
- case "text": {
- if (cline.didRejectTool || cline.didAlreadyUseTool) {
- break
- }
- let content = block.content
- if (content) {
- // Have to do this for partial and complete since sending
- // content in thinking tags to markdown renderer will
- // automatically be removed.
- // Remove end substrings of <thinking or </thinking (below xml
- // parsing is only for opening tags).
- // Tthis is done with the xml parsing below now, but keeping
- // here for reference.
- // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "")
- //
- // Remove all instances of <thinking> (with optional line break
- // after) and </thinking> (with optional line break before).
- // - Needs to be separate since we dont want to remove the line
- // break before the first tag.
- // - Needs to happen before the xml parsing below.
- content = content.replace(/<thinking>\s?/g, "")
- content = content.replace(/\s?<\/thinking>/g, "")
- // Remove partial XML tag at the very end of the content (for
- // tool use and thinking tags), Prevents scrollview from
- // jumping when tags are automatically removed.
- const lastOpenBracketIndex = content.lastIndexOf("<")
- if (lastOpenBracketIndex !== -1) {
- const possibleTag = content.slice(lastOpenBracketIndex)
- // Check if there's a '>' after the last '<' (i.e., if the
- // tag is complete) (complete thinking and tool tags will
- // have been removed by now.)
- const hasCloseBracket = possibleTag.includes(">")
- if (!hasCloseBracket) {
- // Extract the potential tag name.
- let tagContent: string
- if (possibleTag.startsWith("</")) {
- tagContent = possibleTag.slice(2).trim()
- } else {
- tagContent = possibleTag.slice(1).trim()
- }
- // Check if tagContent is likely an incomplete tag name
- // (letters and underscores only).
- const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent)
- // Preemptively remove < or </ to keep from these
- // artifacts showing up in chat (also handles closing
- // thinking tags).
- const isOpeningOrClosing = possibleTag === "<" || possibleTag === "</"
- // If the tag is incomplete and at the end, remove it
- // from the content.
- if (isOpeningOrClosing || isLikelyTagName) {
- content = content.slice(0, lastOpenBracketIndex).trim()
- }
- }
- }
- }
- await cline.say("text", content, undefined, block.partial)
- break
- }
- case "tool_use":
- const toolDescription = (): string => {
- switch (block.name) {
- case "execute_command":
- return `[${block.name} for '${block.params.command}']`
- case "read_file":
- // Check if this model should use the simplified description
- const modelId = cline.api.getModel().id
- if (shouldUseSingleFileRead(modelId)) {
- return getSimpleReadFileToolDescription(block.name, block.params)
- } else {
- // Prefer native typed args when available; fall back to legacy params
- // Check if nativeArgs exists (native protocol)
- if (block.nativeArgs) {
- return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs)
- }
- return readFileTool.getReadFileToolDescription(block.name, block.params)
- }
- case "fetch_instructions":
- return `[${block.name} for '${block.params.task}']`
- case "write_to_file":
- return `[${block.name} for '${block.params.path}']`
- case "apply_diff":
- // Handle both legacy format and new multi-file format
- if (block.params.path) {
- return `[${block.name} for '${block.params.path}']`
- } else if (block.params.args) {
- // Try to extract first file path from args for display
- const match = block.params.args.match(/<file>.*?<path>([^<]+)<\/path>/s)
- if (match) {
- const firstPath = match[1]
- // Check if there are multiple files
- const fileCount = (block.params.args.match(/<file>/g) || []).length
- if (fileCount > 1) {
- return `[${block.name} for '${firstPath}' and ${fileCount - 1} more file${fileCount > 2 ? "s" : ""}]`
- } else {
- return `[${block.name} for '${firstPath}']`
- }
- }
- }
- return `[${block.name}]`
- case "search_files":
- return `[${block.name} for '${block.params.regex}'${
- block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
- }]`
- case "search_and_replace":
- return `[${block.name} for '${block.params.path}']`
- case "apply_patch":
- return `[${block.name}]`
- case "list_files":
- return `[${block.name} for '${block.params.path}']`
- case "list_code_definition_names":
- return `[${block.name} for '${block.params.path}']`
- case "browser_action":
- return `[${block.name} for '${block.params.action}']`
- case "use_mcp_tool":
- return `[${block.name} for '${block.params.server_name}']`
- case "access_mcp_resource":
- return `[${block.name} for '${block.params.server_name}']`
- case "ask_followup_question":
- return `[${block.name} for '${block.params.question}']`
- case "attempt_completion":
- return `[${block.name}]`
- case "switch_mode":
- return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
- case "codebase_search": // Add case for the new tool
- return `[${block.name} for '${block.params.query}']`
- case "update_todo_list":
- return `[${block.name}]`
- case "new_task": {
- const mode = block.params.mode ?? defaultModeSlug
- const message = block.params.message ?? "(no message)"
- const modeName = getModeBySlug(mode, customModes)?.name ?? mode
- return `[${block.name} in ${modeName} mode: '${message}']`
- }
- case "run_slash_command":
- return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
- case "generate_image":
- return `[${block.name} for '${block.params.path}']`
- default:
- return `[${block.name}]`
- }
- }
- if (cline.didRejectTool) {
- // Ignore any tool content after user has rejected tool once.
- // For native protocol, we must send a tool_result for every tool_use to avoid API errors
- const toolCallId = block.id
- const errorMessage = !block.partial
- ? `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`
- : `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`
- if (toolCallId) {
- // Native protocol: MUST send tool_result for every tool_use
- cline.userMessageContent.push({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: errorMessage,
- is_error: true,
- } as Anthropic.ToolResultBlockParam)
- } else {
- // XML protocol: send as text
- cline.userMessageContent.push({
- type: "text",
- text: errorMessage,
- })
- }
- break
- }
- if (cline.didAlreadyUseTool) {
- // Ignore any content after a tool has already been used.
- // For native protocol, we must send a tool_result for every tool_use to avoid API errors
- const toolCallId = block.id
- 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.`
- if (toolCallId) {
- // Native protocol: MUST send tool_result for every tool_use
- cline.userMessageContent.push({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: errorMessage,
- is_error: true,
- } as Anthropic.ToolResultBlockParam)
- } else {
- // XML protocol: send as text
- cline.userMessageContent.push({
- type: "text",
- text: errorMessage,
- })
- }
- break
- }
- // Track if we've already pushed a tool result for this tool call (native protocol only)
- let hasToolResult = false
- // Determine protocol by checking if this tool call has an ID.
- // Native protocol tool calls ALWAYS have an ID (set when parsed from tool_call chunks).
- // XML protocol tool calls NEVER have an ID (parsed from XML text).
- const toolCallId = (block as any).id
- const toolProtocol = toolCallId ? TOOL_PROTOCOL.NATIVE : TOOL_PROTOCOL.XML
- // Multiple native tool calls feature is on hold - always disabled
- // Previously resolved from experiments.isEnabled(..., EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS)
- const isMultipleNativeToolCallsEnabled = false
- const pushToolResult = (content: ToolResponse) => {
- if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
- // For native protocol, only allow ONE tool_result per tool call
- if (hasToolResult) {
- console.warn(
- `[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${toolCallId}`,
- )
- return
- }
- // For native protocol, tool_result content must be a string
- // Images are added as separate blocks in the user message
- let resultContent: string
- let imageBlocks: Anthropic.ImageBlockParam[] = []
- if (typeof content === "string") {
- resultContent = content || "(tool did not return anything)"
- } else {
- // Separate text and image blocks
- const textBlocks = content.filter((item) => item.type === "text")
- imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
- // Convert text blocks to string for tool_result
- resultContent =
- textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
- "(tool did not return anything)"
- }
- // Add tool_result with text content only
- cline.userMessageContent.push({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: resultContent,
- } as Anthropic.ToolResultBlockParam)
- // Add image blocks separately after tool_result
- if (imageBlocks.length > 0) {
- cline.userMessageContent.push(...imageBlocks)
- }
- hasToolResult = true
- } else {
- // For XML protocol, add as text blocks (legacy behavior)
- cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
- if (typeof content === "string") {
- cline.userMessageContent.push({
- type: "text",
- text: content || "(tool did not return anything)",
- })
- } else {
- cline.userMessageContent.push(...content)
- }
- }
- // For XML protocol: Only one tool per message is allowed
- // For native protocol with experimental flag enabled: Multiple tools can be executed in sequence
- // For native protocol with experimental flag disabled: Single tool per message (default safe behavior)
- if (toolProtocol === TOOL_PROTOCOL.XML) {
- // Once a tool result has been collected, ignore all other tool
- // uses since we should only ever present one tool result per
- // message (XML protocol only).
- cline.didAlreadyUseTool = true
- } else if (toolProtocol === TOOL_PROTOCOL.NATIVE && !isMultipleNativeToolCallsEnabled) {
- // For native protocol with experimental flag disabled, enforce single tool per message
- cline.didAlreadyUseTool = true
- }
- // If toolProtocol is NATIVE and isMultipleNativeToolCallsEnabled is true,
- // allow multiple tool calls in sequence (don't set didAlreadyUseTool)
- }
- const askApproval = async (
- type: ClineAsk,
- partialMessage?: string,
- progressStatus?: ToolProgressStatus,
- isProtected?: boolean,
- ) => {
- const { response, text, images } = await cline.ask(
- type,
- partialMessage,
- false,
- progressStatus,
- isProtected || false,
- )
- if (response !== "yesButtonClicked") {
- // Handle both messageResponse and noButtonClicked with text.
- if (text) {
- await cline.say("user_feedback", text, images)
- pushToolResult(
- formatResponse.toolResult(
- formatResponse.toolDeniedWithFeedback(text, toolProtocol),
- images,
- ),
- )
- } else {
- pushToolResult(formatResponse.toolDenied(toolProtocol))
- }
- cline.didRejectTool = true
- return false
- }
- // Handle yesButtonClicked with text.
- if (text) {
- await cline.say("user_feedback", text, images)
- pushToolResult(
- formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text, toolProtocol), images),
- )
- }
- return true
- }
- const askFinishSubTaskApproval = async () => {
- // Ask the user to approve this task has completed, and he has
- // reviewed it, and we can declare task is finished and return
- // control to the parent task to continue running the rest of
- // the sub-tasks.
- const toolMessage = JSON.stringify({ tool: "finishTask" })
- return await askApproval("tool", toolMessage)
- }
- const handleError = async (action: string, error: Error) => {
- const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
- await cline.say(
- "error",
- `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
- )
- pushToolResult(formatResponse.toolError(errorString, toolProtocol))
- }
- // If block is partial, remove partial closing tag so its not
- // presented to user.
- const removeClosingTag = (tag: ToolParamName, text?: string): string => {
- if (!block.partial) {
- return text || ""
- }
- if (!text) {
- return ""
- }
- // This regex dynamically constructs a pattern to match the
- // closing tag:
- // - Optionally matches whitespace before the tag.
- // - Matches '<' or '</' optionally followed by any subset of
- // characters from the tag name.
- const tagRegex = new RegExp(
- `\\s?<\/?${tag
- .split("")
- .map((char) => `(?:${char})?`)
- .join("")}$`,
- "g",
- )
- return text.replace(tagRegex, "")
- }
- // Keep browser open during an active session so other tools can run.
- // Session is active if we've seen any browser_action_result and the last browser_action is not "close".
- try {
- const messages = cline.clineMessages || []
- const hasStarted = messages.some((m: any) => m.say === "browser_action_result")
- let isClosed = false
- for (let i = messages.length - 1; i >= 0; i--) {
- const m = messages[i]
- if (m.say === "browser_action") {
- try {
- const act = JSON.parse(m.text || "{}")
- isClosed = act.action === "close"
- } catch {}
- break
- }
- }
- const sessionActive = hasStarted && !isClosed
- // Only auto-close when no active browser session is present, and this isn't a browser_action
- if (!sessionActive && block.name !== "browser_action") {
- await cline.browserSession.closeBrowser()
- }
- } catch {
- // On any unexpected error, fall back to conservative behavior
- if (block.name !== "browser_action") {
- await cline.browserSession.closeBrowser()
- }
- }
- if (!block.partial) {
- cline.recordToolUsage(block.name)
- TelemetryService.instance.captureToolUsage(cline.taskId, block.name, toolProtocol)
- }
- // Validate tool use before execution.
- const {
- mode,
- customModes,
- experiments: stateExperiments,
- apiConfiguration,
- } = (await cline.providerRef.deref()?.getState()) ?? {}
- const modelInfo = cline.api.getModel()
- const includedTools = modelInfo?.info?.includedTools
- try {
- validateToolUse(
- block.name as ToolName,
- mode ?? defaultModeSlug,
- customModes ?? [],
- { apply_diff: cline.diffEnabled },
- block.params,
- stateExperiments,
- includedTools,
- )
- } catch (error) {
- cline.consecutiveMistakeCount++
- pushToolResult(formatResponse.toolError(error.message, toolProtocol))
- break
- }
- // Check for identical consecutive tool calls.
- if (!block.partial) {
- // Use the detector to check for repetition, passing the ToolUse
- // block directly.
- const repetitionCheck = cline.toolRepetitionDetector.check(block)
- // If execution is not allowed, notify user and break.
- if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
- // Handle repetition similar to mistake_limit_reached pattern.
- const { response, text, images } = await cline.ask(
- repetitionCheck.askUser.messageKey as ClineAsk,
- repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
- )
- if (response === "messageResponse") {
- // Add user feedback to userContent.
- cline.userMessageContent.push(
- {
- type: "text" as const,
- text: `Tool repetition limit reached. User feedback: ${text}`,
- },
- ...formatResponse.imageBlocks(images),
- )
- // Add user feedback to chat.
- await cline.say("user_feedback", text, images)
- // Track tool repetition in telemetry.
- TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
- }
- // Return tool result message about the repetition
- pushToolResult(
- formatResponse.toolError(
- `Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
- toolProtocol,
- ),
- )
- break
- }
- }
- switch (block.name) {
- case "write_to_file":
- await checkpointSaveAndMark(cline)
- await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "update_todo_list":
- await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "apply_diff": {
- await checkpointSaveAndMark(cline)
- // Check if this tool call came from native protocol by checking for ID
- // Native calls always have IDs, XML calls never do
- if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
- await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- }
- // Get the provider and state to check experiment settings
- const provider = cline.providerRef.deref()
- let isMultiFileApplyDiffEnabled = false
- if (provider) {
- const state = await provider.getState()
- isMultiFileApplyDiffEnabled = experiments.isEnabled(
- state.experiments ?? {},
- EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,
- )
- }
- if (isMultiFileApplyDiffEnabled) {
- await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
- } else {
- await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- }
- break
- }
- case "search_and_replace":
- await checkpointSaveAndMark(cline)
- await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "apply_patch":
- await checkpointSaveAndMark(cline)
- await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "read_file":
- // Check if this model should use the simplified single-file read tool
- // Only use simplified tool for XML protocol - native protocol works with standard tool
- const modelId = cline.api.getModel().id
- if (shouldUseSingleFileRead(modelId) && toolProtocol !== TOOL_PROTOCOL.NATIVE) {
- await simpleReadFileTool(
- cline,
- block,
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- )
- } else {
- // Type assertion is safe here because we're in the "read_file" case
- await readFileTool.handle(cline, block as ToolUse<"read_file">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- }
- break
- case "fetch_instructions":
- await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "list_files":
- await listFilesTool.handle(cline, block as ToolUse<"list_files">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "codebase_search":
- await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "list_code_definition_names":
- await listCodeDefinitionNamesTool.handle(cline, block as ToolUse<"list_code_definition_names">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "search_files":
- await searchFilesTool.handle(cline, block as ToolUse<"search_files">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "browser_action":
- await browserActionTool(
- cline,
- block as ToolUse<"browser_action">,
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- )
- break
- case "execute_command":
- await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "use_mcp_tool":
- await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "access_mcp_resource":
- await accessMcpResourceTool.handle(cline, block as ToolUse<"access_mcp_resource">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "ask_followup_question":
- await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "switch_mode":
- await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "new_task":
- await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- toolCallId: block.id,
- })
- break
- case "attempt_completion": {
- const completionCallbacks: AttemptCompletionCallbacks = {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- askFinishSubTaskApproval,
- toolDescription,
- toolProtocol,
- }
- await attemptCompletionTool.handle(
- cline,
- block as ToolUse<"attempt_completion">,
- completionCallbacks,
- )
- break
- }
- case "run_slash_command":
- await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "generate_image":
- await checkpointSaveAndMark(cline)
- await generateImageTool.handle(cline, block as ToolUse<"generate_image">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- }
- break
- }
- // Seeing out of bounds is fine, it means that the next too call is being
- // built up and ready to add to assistantMessageContent to present.
- // When you see the UI inactive during this, it means that a tool is
- // breaking without presenting any UI. For example the write_to_file tool
- // was breaking when relpath was undefined, and for invalid relpath it never
- // presented UI.
- // This needs to be placed here, if not then calling
- // cline.presentAssistantMessage below would fail (sometimes) since it's
- // locked.
- cline.presentAssistantMessageLocked = false
- // NOTE: When tool is rejected, iterator stream is interrupted and it waits
- // for `userMessageContentReady` to be true. Future calls to present will
- // skip execution since `didRejectTool` and iterate until `contentIndex` is
- // set to message length and it sets userMessageContentReady to true itself
- // (instead of preemptively doing it in iterator).
- if (!block.partial || cline.didRejectTool || cline.didAlreadyUseTool) {
- // Block is finished streaming and executing.
- if (cline.currentStreamingContentIndex === cline.assistantMessageContent.length - 1) {
- // It's okay that we increment if !didCompleteReadingStream, it'll
- // just return because out of bounds and as streaming continues it
- // will call `presentAssitantMessage` if a new block is ready. If
- // streaming is finished then we set `userMessageContentReady` to
- // true when out of bounds. This gracefully allows the stream to
- // continue on and all potential content blocks be presented.
- // Last block is complete and it is finished executing
- cline.userMessageContentReady = true // Will allow `pWaitFor` to continue.
- }
- // Call next block if it exists (if not then read stream will call it
- // when it's ready).
- // Need to increment regardless, so when read stream calls this function
- // again it will be streaming the next block.
- cline.currentStreamingContentIndex++
- if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) {
- // There are already more content blocks to stream, so we'll call
- // this function ourselves.
- presentAssistantMessage(cline)
- return
- } else {
- // CRITICAL FIX: If we're out of bounds and the stream is complete, set userMessageContentReady
- // This handles the case where assistantMessageContent is empty or becomes empty after processing
- if (cline.didCompleteReadingStream) {
- cline.userMessageContentReady = true
- }
- }
- }
- // Block is partial, but the read stream may have finished.
- if (cline.presentAssistantMessageHasPendingUpdates) {
- presentAssistantMessage(cline)
- }
- }
- /**
- * save checkpoint and mark done in the current streaming task.
- * @param task The Task instance to checkpoint save and mark.
- * @returns
- */
- async function checkpointSaveAndMark(task: Task) {
- if (task.currentStreamingDidCheckpoint) {
- return
- }
- try {
- await task.checkpointSave(true)
- task.currentStreamingDidCheckpoint = true
- } catch (error) {
- console.error(`[Task#presentAssistantMessage] Error saving checkpoint: ${error.message}`, error)
- }
- }
|