From 4f5549391e641be9505437cc0a138005acf59fb9 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Mon, 21 Jul 2025 08:26:50 -0500 Subject: [PATCH 1/3] fix: allow text content after update_todo_list tool (#5847) The update_todo_list tool is a special case that doesn't produce actionable output or require user interaction beyond confirmation. Unlike other tools that perform file operations or system commands, update_todo_list simply updates the UI's todo list display. This change exempts update_todo_list from setting the didAlreadyUseTool flag, which allows subsequent text content to be rendered. This enables the assistant to provide context or explanations after updating the todo list, improving the user experience. The one-tool-per-message restriction remains in effect for all other tools to prevent multiple potentially conflicting operations in a single message. --- src/core/assistant-message/presentAssistantMessage.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index ee3fa148b4..344af8bfcc 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -256,8 +256,11 @@ export async function presentAssistantMessage(cline: Task) { // Once a tool result has been collected, ignore all other tool // uses since we should only ever present one tool result per - // message. - cline.didAlreadyUseTool = true + // message. Exception: update_todo_list is allowed to be followed + // by text content. + if (block.name !== "update_todo_list") { + cline.didAlreadyUseTool = true + } } const askApproval = async ( From d2c1a2655cd16992c4f8bff1632db6085a37677a Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Mon, 21 Jul 2025 09:39:52 -0500 Subject: [PATCH 2/3] fix: allow text after update_todo_list and support multiple calls - Modified presentAssistantMessage.ts to skip tool blocking for update_todo_list - Prevented setting didAlreadyUseTool flag when update_todo_list is used - Updated Task.ts to check for update_todo_list before interrupting stream - Allows update_todo_list to be called multiple times in a single message - Text can now appear after update_todo_list tool calls Fixes #5847 --- .../assistant-message/presentAssistantMessage.ts | 15 ++++++++++----- src/core/task/Task.ts | 8 +++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 344af8bfcc..ca922df543 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -82,7 +82,7 @@ export async function presentAssistantMessage(cline: Task) { switch (block.type) { case "text": { - if (cline.didRejectTool || cline.didAlreadyUseTool) { + if (cline.didRejectTool) { break } @@ -235,8 +235,9 @@ export async function presentAssistantMessage(cline: Task) { break } - if (cline.didAlreadyUseTool) { + if (cline.didAlreadyUseTool && block.name !== "update_todo_list") { // Ignore any content after a tool has already been used. + // Exception: update_todo_list can be used multiple times cline.userMessageContent.push({ type: "text", text: `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.`, @@ -256,8 +257,7 @@ export async function presentAssistantMessage(cline: Task) { // Once a tool result has been collected, ignore all other tool // uses since we should only ever present one tool result per - // message. Exception: update_todo_list is allowed to be followed - // by text content. + // message. Exception: update_todo_list can be used multiple times. if (block.name !== "update_todo_list") { cline.didAlreadyUseTool = true } @@ -554,7 +554,12 @@ export async function presentAssistantMessage(cline: Task) { // 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) { + if ( + !block.partial || + cline.didRejectTool || + (cline.didAlreadyUseTool && block.type !== "tool_use") || + (cline.didAlreadyUseTool && block.type === "tool_use" && block.name !== "update_todo_list") + ) { // Block is finished streaming and executing. if (cline.currentStreamingContentIndex === cline.assistantMessageContent.length - 1) { // It's okay that we increment if !didCompleteReadingStream, it'll diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 53b8ef5b87..112b711629 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1427,7 +1427,13 @@ export class Task extends EventEmitter { // get generation details. // UPDATE: It's better UX to interrupt the request at the // cost of the API cost not being retrieved. - if (this.didAlreadyUseTool) { + // Exception: update_todo_list can be used multiple times + // Check if any update_todo_list is being processed (partial or complete) + const hasUpdateTodoList = this.assistantMessageContent.some( + (block) => block.type === "tool_use" && block.name === "update_todo_list", + ) + + if (this.didAlreadyUseTool && !hasUpdateTodoList) { assistantMessage += "\n\n[Response interrupted by a tool use result. Only one tool may be used at a time and should be placed at the end of the message.]" break From 3bce917af2f7ad133c0d4e00e359b94fe0b8fa32 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 22 Jul 2025 13:21:55 +0000 Subject: [PATCH 3/3] fix: remove special condition for update_todo_list tool - Remove exception that allowed update_todo_list to be called alongside other tools - Now all tools are limited to one per message without exceptions - Keeps the change that allows text after any tool use --- src/core/assistant-message/presentAssistantMessage.ts | 11 ++++------- src/core/task/Task.ts | 8 +------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index ca922df543..bd7729d001 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -235,9 +235,8 @@ export async function presentAssistantMessage(cline: Task) { break } - if (cline.didAlreadyUseTool && block.name !== "update_todo_list") { + if (cline.didAlreadyUseTool) { // Ignore any content after a tool has already been used. - // Exception: update_todo_list can be used multiple times cline.userMessageContent.push({ type: "text", text: `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.`, @@ -257,10 +256,8 @@ export async function presentAssistantMessage(cline: Task) { // Once a tool result has been collected, ignore all other tool // uses since we should only ever present one tool result per - // message. Exception: update_todo_list can be used multiple times. - if (block.name !== "update_todo_list") { - cline.didAlreadyUseTool = true - } + // message. + cline.didAlreadyUseTool = true } const askApproval = async ( @@ -558,7 +555,7 @@ export async function presentAssistantMessage(cline: Task) { !block.partial || cline.didRejectTool || (cline.didAlreadyUseTool && block.type !== "tool_use") || - (cline.didAlreadyUseTool && block.type === "tool_use" && block.name !== "update_todo_list") + (cline.didAlreadyUseTool && block.type === "tool_use") ) { // Block is finished streaming and executing. if (cline.currentStreamingContentIndex === cline.assistantMessageContent.length - 1) { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 112b711629..53b8ef5b87 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1427,13 +1427,7 @@ export class Task extends EventEmitter { // get generation details. // UPDATE: It's better UX to interrupt the request at the // cost of the API cost not being retrieved. - // Exception: update_todo_list can be used multiple times - // Check if any update_todo_list is being processed (partial or complete) - const hasUpdateTodoList = this.assistantMessageContent.some( - (block) => block.type === "tool_use" && block.name === "update_todo_list", - ) - - if (this.didAlreadyUseTool && !hasUpdateTodoList) { + if (this.didAlreadyUseTool) { assistantMessage += "\n\n[Response interrupted by a tool use result. Only one tool may be used at a time and should be placed at the end of the message.]" break