Skip to content

Commit 8733ed4

Browse files
mrubensq1600822305
authored andcommitted
Revert "Enable parallel tool calling with new_task isolation safeguards" (RooCodeInc#11004)
1 parent 23609c6 commit 8733ed4

5 files changed

Lines changed: 80 additions & 546 deletions

File tree

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,7 @@ export async function presentAssistantMessage(cline: Task) {
127127
break
128128
}
129129

130-
// Get parallel tool calling state from experiments
131-
const mcpState = await cline.providerRef.deref()?.getState()
132-
const mcpParallelToolCallsEnabled = mcpState?.experiments?.multipleNativeToolCalls ?? false
133-
134-
if (!mcpParallelToolCallsEnabled && cline.didAlreadyUseTool) {
130+
if (cline.didAlreadyUseTool) {
135131
const toolCallId = mcpBlock.id
136132
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.`
137133

@@ -200,10 +196,7 @@ export async function presentAssistantMessage(cline: Task) {
200196
}
201197

202198
hasToolResult = true
203-
// Only set didAlreadyUseTool when parallel tool calling is disabled
204-
if (!mcpParallelToolCallsEnabled) {
205-
cline.didAlreadyUseTool = true
206-
}
199+
cline.didAlreadyUseTool = true
207200
}
208201

209202
const toolDescription = () => `[mcp_tool: ${mcpBlock.serverName}/${mcpBlock.toolName}]`
@@ -490,10 +483,7 @@ export async function presentAssistantMessage(cline: Task) {
490483
break
491484
}
492485

493-
// Get parallel tool calling state from experiments (stateExperiments already fetched above)
494-
const parallelToolCallsEnabled = stateExperiments?.multipleNativeToolCalls ?? false
495-
496-
if (!parallelToolCallsEnabled && cline.didAlreadyUseTool) {
486+
if (cline.didAlreadyUseTool) {
497487
// Ignore any content after a tool has already been used.
498488
// For native protocol, we must send a tool_result for every tool_use to avoid API errors
499489
const toolCallId = block.id

src/core/prompts/tools/native-tools/new_task.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type OpenAI from "openai"
22

3-
const NEW_TASK_DESCRIPTION = `Create a new task instance in the chosen mode using your provided message and initial todo list (if required).
4-
5-
CRITICAL: This tool MUST be called alone. Do NOT call this tool alongside other tools in the same message turn. If you need to gather information before delegating, use other tools in a separate turn first, then call new_task by itself in the next turn.`
3+
const NEW_TASK_DESCRIPTION = `This will let you create a new task instance in the chosen mode using your provided message and initial todo list (if required).`
64

75
const MODE_PARAMETER_DESCRIPTION = `Slug of the mode to begin the new task in (e.g., code, debug, architect)`
86

src/core/task/Task.ts

Lines changed: 74 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -3138,6 +3138,58 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
31383138
}
31393139
}
31403140

3141+
// Finalize any remaining streaming tool calls that weren't explicitly ended
3142+
// This is critical for MCP tools which need tool_call_end events to be properly
3143+
// converted from ToolUse to McpToolUse via finalizeStreamingToolCall()
3144+
const finalizeEvents = NativeToolCallParser.finalizeRawChunks()
3145+
for (const event of finalizeEvents) {
3146+
if (event.type === "tool_call_end") {
3147+
// Finalize the streaming tool call
3148+
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
3149+
3150+
// Get the index for this tool call
3151+
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
3152+
3153+
if (finalToolUse) {
3154+
// Store the tool call ID
3155+
;(finalToolUse as any).id = event.id
3156+
3157+
// Get the index and replace partial with final
3158+
if (toolUseIndex !== undefined) {
3159+
this.assistantMessageContent[toolUseIndex] = finalToolUse
3160+
}
3161+
3162+
// Clean up tracking
3163+
this.streamingToolCallIndices.delete(event.id)
3164+
3165+
// Mark that we have new content to process
3166+
this.userMessageContentReady = false
3167+
3168+
// Present the finalized tool call
3169+
presentAssistantMessage(this)
3170+
} else if (toolUseIndex !== undefined) {
3171+
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
3172+
// We still need to mark the tool as non-partial so it gets executed
3173+
// The tool's validation will catch any missing required parameters
3174+
const existingToolUse = this.assistantMessageContent[toolUseIndex]
3175+
if (existingToolUse && existingToolUse.type === "tool_use") {
3176+
existingToolUse.partial = false
3177+
// Ensure it has the ID for native protocol
3178+
;(existingToolUse as any).id = event.id
3179+
}
3180+
3181+
// Clean up tracking
3182+
this.streamingToolCallIndices.delete(event.id)
3183+
3184+
// Mark that we have new content to process
3185+
this.userMessageContentReady = false
3186+
3187+
// Present the tool call - validation will handle missing params
3188+
presentAssistantMessage(this)
3189+
}
3190+
}
3191+
}
3192+
31413193
// Create a copy of current token values to avoid race conditions
31423194
const currentTokens = {
31433195
input: inputTokens,
@@ -3385,61 +3437,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
33853437
// the case, `presentAssistantMessage` relies on these blocks either
33863438
// to be completed or the user to reject a block in order to proceed
33873439
// and eventually set userMessageContentReady to true.)
3388-
3389-
// Finalize any remaining streaming tool calls that weren't explicitly ended
3390-
// This is critical for MCP tools which need tool_call_end events to be properly
3391-
// converted from ToolUse to McpToolUse via finalizeStreamingToolCall()
3392-
const finalizeEvents = NativeToolCallParser.finalizeRawChunks()
3393-
for (const event of finalizeEvents) {
3394-
if (event.type === "tool_call_end") {
3395-
// Finalize the streaming tool call
3396-
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
3397-
3398-
// Get the index for this tool call
3399-
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
3400-
3401-
if (finalToolUse) {
3402-
// Store the tool call ID
3403-
;(finalToolUse as any).id = event.id
3404-
3405-
// Get the index and replace partial with final
3406-
if (toolUseIndex !== undefined) {
3407-
this.assistantMessageContent[toolUseIndex] = finalToolUse
3408-
}
3409-
3410-
// Clean up tracking
3411-
this.streamingToolCallIndices.delete(event.id)
3412-
3413-
// Mark that we have new content to process
3414-
this.userMessageContentReady = false
3415-
3416-
// Present the finalized tool call
3417-
presentAssistantMessage(this)
3418-
} else if (toolUseIndex !== undefined) {
3419-
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
3420-
// We still need to mark the tool as non-partial so it gets executed
3421-
// The tool's validation will catch any missing required parameters
3422-
const existingToolUse = this.assistantMessageContent[toolUseIndex]
3423-
if (existingToolUse && existingToolUse.type === "tool_use") {
3424-
existingToolUse.partial = false
3425-
// Ensure it has the ID for native protocol
3426-
;(existingToolUse as any).id = event.id
3427-
}
3428-
3429-
// Clean up tracking
3430-
this.streamingToolCallIndices.delete(event.id)
3431-
3432-
// Mark that we have new content to process
3433-
this.userMessageContentReady = false
3434-
3435-
// Present the tool call - validation will handle missing params
3436-
presentAssistantMessage(this)
3437-
}
3438-
}
3439-
}
3440-
3441-
// IMPORTANT: Capture partialBlocks AFTER finalizeRawChunks() to avoid double-presentation.
3442-
// Tools finalized above are already presented, so we only want blocks still partial after finalization.
34433440
const partialBlocks = this.assistantMessageContent.filter((block) => block.partial)
34443441
partialBlocks.forEach((block) => (block.partial = false))
34453442

@@ -3455,6 +3452,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
34553452
this.assistantMessageContent = parsedBlocks
34563453
}
34573454

3455+
// Present any partial blocks that were just completed.
3456+
// Tool calls are typically presented during streaming via tool_call_partial events,
3457+
// but we still present here if any partial blocks remain (e.g., malformed streams).
3458+
if (partialBlocks.length > 0) {
3459+
// If there is content to update then it will complete and
3460+
// update `this.userMessageContentReady` to true, which we
3461+
// `pWaitFor` before making the next request.
3462+
presentAssistantMessage(this)
3463+
}
3464+
34583465
// Note: updateApiReqMsg() is now called from within drainStreamInBackgroundToFindAllUsage
34593466
// to ensure usage data is captured even when the stream is interrupted. The background task
34603467
// uses local variables to accumulate usage data before atomically updating the shared state.
@@ -3480,10 +3487,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
34803487
// Reset parser after each complete conversation round (XML protocol only)
34813488
this.assistantMessageParser?.reset()
34823489

3483-
// CRITICAL: Save assistant message to API history BEFORE executing tools.
3484-
// This ensures that when new_task triggers delegation and calls flushPendingToolResultsToHistory(),
3485-
// the assistant message is already in history. Otherwise, tool_result blocks would appear
3486-
// BEFORE their corresponding tool_use blocks, causing API errors.
3490+
// Now add to apiConversationHistory.
3491+
// Need to save assistant responses to file before proceeding to
3492+
// tool use since user can exit at any moment and we wouldn't be
3493+
// able to save the assistant's response.
34873494

34883495
// Check if we have any content to process (text or tool uses)
34893496
const hasTextContent = assistantMessage.length > 0
@@ -3580,69 +3587,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
35803587
}
35813588
}
35823589

3583-
// Enforce new_task isolation: if new_task is called alongside other tools,
3584-
// truncate any tools that come after it and inject error tool_results.
3585-
// This prevents orphaned tools when delegation disposes the parent task.
3586-
const newTaskIndex = assistantContent.findIndex(
3587-
(block) => block.type === "tool_use" && block.name === "new_task",
3588-
)
3589-
3590-
if (newTaskIndex !== -1 && newTaskIndex < assistantContent.length - 1) {
3591-
// new_task found but not last - truncate subsequent tools
3592-
const truncatedTools = assistantContent.slice(newTaskIndex + 1)
3593-
assistantContent.length = newTaskIndex + 1 // Truncate API history array
3594-
3595-
// ALSO truncate the execution array (assistantMessageContent) to prevent
3596-
// tools after new_task from being executed by presentAssistantMessage().
3597-
// Find new_task index in assistantMessageContent (may differ from assistantContent
3598-
// due to text blocks being structured differently).
3599-
const executionNewTaskIndex = this.assistantMessageContent.findIndex(
3600-
(block) => block.type === "tool_use" && block.name === "new_task",
3601-
)
3602-
if (executionNewTaskIndex !== -1) {
3603-
this.assistantMessageContent.length = executionNewTaskIndex + 1
3604-
}
3605-
3606-
// Pre-inject error tool_results for truncated tools
3607-
for (const tool of truncatedTools) {
3608-
if (tool.type === "tool_use" && (tool as Anthropic.ToolUseBlockParam).id) {
3609-
this.pushToolResultToUserContent({
3610-
type: "tool_result",
3611-
tool_use_id: (tool as Anthropic.ToolUseBlockParam).id,
3612-
content:
3613-
"This tool was not executed because new_task was called in the same message turn. The new_task tool must be the last tool in a message.",
3614-
is_error: true,
3615-
})
3616-
}
3617-
}
3618-
}
3619-
3620-
// Save assistant message BEFORE executing tools
3621-
// This is critical for new_task: when it triggers delegation, flushPendingToolResultsToHistory()
3622-
// will save the user message with tool_results. The assistant message must already be in history
3623-
// so that tool_result blocks appear AFTER their corresponding tool_use blocks.
36243590
await this.addToApiConversationHistory(
36253591
{ role: "assistant", content: assistantContent },
36263592
reasoningMessage || undefined,
36273593
)
36283594

36293595
TelemetryService.instance.captureConversationMessage(this.taskId, "assistant")
3630-
}
36313596

3632-
// Present any partial blocks that were just completed.
3633-
// Tool calls are typically presented during streaming via tool_call_partial events,
3634-
// but we still present here if any partial blocks remain (e.g., malformed streams).
3635-
// NOTE: This MUST happen AFTER saving the assistant message to API history.
3636-
// When new_task is in the batch, it triggers delegation which calls flushPendingToolResultsToHistory().
3637-
// If the assistant message isn't saved yet, tool_results would appear before tool_use blocks.
3638-
if (partialBlocks.length > 0) {
3639-
// If there is content to update then it will complete and
3640-
// update `this.userMessageContentReady` to true, which we
3641-
// `pWaitFor` before making the next request.
3642-
presentAssistantMessage(this)
3643-
}
3644-
3645-
if (hasTextContent || hasToolUses) {
36463597
// NOTE: This comment is here for future reference - this was a
36473598
// workaround for `userMessageContent` not getting set to true.
36483599
// It was due to it not recursively calling for partial blocks
@@ -4359,7 +4310,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
43594310
allowedFunctionNames = toolsResult.allowedFunctionNames
43604311
}
43614312

4313+
<<<<<<< HEAD
43624314
const parallelToolCallsEnabled = state?.experiments?.multipleNativeToolCalls ?? false
4315+
=======
4316+
const shouldIncludeTools = allTools.length > 0
4317+
4318+
// Parallel tool calls are disabled - feature is on hold
4319+
// Previously resolved from experiments.isEnabled(..., EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS)
4320+
const parallelToolCallsEnabled = false
4321+
>>>>>>> 5b3626f1a (Revert "Enable parallel tool calling with new_task isolation safeguards" (#11004))
43634322

43644323
const metadata: ApiHandlerCreateMessageMetadata = {
43654324
mode: mode,

0 commit comments

Comments
 (0)