From 34e074642cf7b6664e4f68821a6ce2106778052b Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 15 Aug 2025 16:42:21 +0000 Subject: [PATCH] fix: preserve context when Orchestrator calls same mode multiple times - Add subtaskContextByMode Map to track completion results by mode - Enhance new task messages with context from previous subtasks of same mode - Store subtask results in resumePausedTask for future reference - Add comprehensive tests for context preservation functionality Fixes #7131 --- src/core/task/Task.ts | 21 ++ src/core/tools/__tests__/newTaskTool.spec.ts | 199 +++++++++++++++++++ src/core/tools/newTaskTool.ts | 28 ++- 3 files changed, 245 insertions(+), 3 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 204348d028..a6bfc27a8a 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -189,6 +189,8 @@ export class Task extends EventEmitter implements TaskLike { isPaused: boolean = false pausedModeSlug: string = defaultModeSlug private pauseInterval: NodeJS.Timeout | undefined + subtaskContextByMode?: Map + currentSubtaskMode?: string // API readonly apiConfiguration: ProviderSettings @@ -1034,6 +1036,25 @@ export class Task extends EventEmitter implements TaskLike { this.isPaused = false this.emit(RooCodeEventName.TaskUnpaused) + // Store the subtask result as context for future calls to the same mode + if (this.currentSubtaskMode && lastMessage) { + if (!this.subtaskContextByMode) { + this.subtaskContextByMode = new Map() + } + if (!this.subtaskContextByMode.has(this.currentSubtaskMode)) { + this.subtaskContextByMode.set(this.currentSubtaskMode, []) + } + const contexts = this.subtaskContextByMode.get(this.currentSubtaskMode)! + // Keep only the last 3 contexts to avoid overwhelming the model + if (contexts.length >= 3) { + contexts.shift() + } + // Extract a concise summary from the result message + const summary = lastMessage.length > 200 ? lastMessage.substring(0, 200) + "..." : lastMessage + contexts.push(summary) + this.currentSubtaskMode = undefined // Reset for next subtask + } + // Fake an answer from the subtask that it has completed running and // this is the result of what it has done add the message to the chat // history and to the webview ui. diff --git a/src/core/tools/__tests__/newTaskTool.spec.ts b/src/core/tools/__tests__/newTaskTool.spec.ts index 8680a60035..2fd71c755f 100644 --- a/src/core/tools/__tests__/newTaskTool.spec.ts +++ b/src/core/tools/__tests__/newTaskTool.spec.ts @@ -36,6 +36,8 @@ const mockCline = { consecutiveMistakeCount: 0, isPaused: false, pausedModeSlug: "ask", + subtaskContextByMode: undefined as Map | undefined, + currentSubtaskMode: undefined as string | undefined, providerRef: { deref: vi.fn(() => ({ getState: vi.fn(() => ({ customModes: [], mode: "ask" })), @@ -63,6 +65,8 @@ describe("newTaskTool", () => { }) // Default valid mode mockCline.consecutiveMistakeCount = 0 mockCline.isPaused = false + mockCline.subtaskContextByMode = undefined + mockCline.currentSubtaskMode = undefined }) it("should correctly un-escape \\\\@ to \\@ in the message passed to the new task", async () => { @@ -183,5 +187,200 @@ describe("newTaskTool", () => { ) }) + describe("context preservation", () => { + it("should initialize subtaskContextByMode map if not exists", async () => { + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "architect", + message: "Design a system", + }, + partial: false, + } + + vi.mocked(getModeBySlug).mockReturnValue({ + slug: "architect", + name: "🏗️ Architect", + roleDefinition: "Test role definition", + groups: ["read", "edit"], + }) + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockCline.subtaskContextByMode).toBeInstanceOf(Map) + expect(mockCline.currentSubtaskMode).toBe("architect") + }) + + it("should include previous context when calling same mode multiple times", async () => { + // Set up previous context + mockCline.subtaskContextByMode = new Map([ + ["architect", ["Created initial system design with 3 microservices"]], + ]) + + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "architect", + message: "Add authentication to the design", + }, + partial: false, + } + + vi.mocked(getModeBySlug).mockReturnValue({ + slug: "architect", + name: "🏗️ Architect", + roleDefinition: "Test role definition", + groups: ["read", "edit"], + }) + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Verify the enhanced message includes context + expect(mockCreateTask).toHaveBeenCalledWith( + expect.stringContaining("[Context from previous 🏗️ Architect subtasks]"), + undefined, + mockCline, + ) + expect(mockCreateTask).toHaveBeenCalledWith( + expect.stringContaining( + "Previous 🏗️ Architect subtask 1 result: Created initial system design with 3 microservices", + ), + undefined, + mockCline, + ) + expect(mockCreateTask).toHaveBeenCalledWith( + expect.stringContaining("Add authentication to the design"), + undefined, + mockCline, + ) + }) + + it("should handle multiple previous contexts", async () => { + // Set up multiple previous contexts + mockCline.subtaskContextByMode = new Map([ + ["architect", ["Created initial system design", "Added database schema", "Defined API endpoints"]], + ]) + + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "architect", + message: "Add caching layer", + }, + partial: false, + } + + vi.mocked(getModeBySlug).mockReturnValue({ + slug: "architect", + name: "🏗️ Architect", + roleDefinition: "Test role definition", + groups: ["read", "edit"], + }) + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockCreateTask).toHaveBeenCalled() + const calls = mockCreateTask.mock.calls as any[] + expect(calls.length).toBeGreaterThan(0) + const callArgs = calls[0][0] as string + expect(callArgs).toContain("Previous 🏗️ Architect subtask 1 result: Created initial system design") + expect(callArgs).toContain("Previous 🏗️ Architect subtask 2 result: Added database schema") + expect(callArgs).toContain("Previous 🏗️ Architect subtask 3 result: Defined API endpoints") + }) + + it("should not include context for different modes", async () => { + // Set up context for architect mode + mockCline.subtaskContextByMode = new Map([["architect", ["Created system design"]]]) + + // Call code mode instead + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "code", + message: "Implement the feature", + }, + partial: false, + } + + vi.mocked(getModeBySlug).mockReturnValue({ + slug: "code", + name: "💻 Code", + roleDefinition: "Test role definition", + groups: ["command", "read", "edit"], + }) + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Should not include architect context + expect(mockCreateTask).toHaveBeenCalledWith( + "Implement the feature", // No context prepended + undefined, + mockCline, + ) + }) + + it("should set currentSubtaskMode for tracking", async () => { + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "debug", + message: "Find the bug", + }, + partial: false, + } + + vi.mocked(getModeBySlug).mockReturnValue({ + slug: "debug", + name: "🪲 Debug", + roleDefinition: "Test role definition", + groups: ["command", "read", "edit"], + }) + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockCline.currentSubtaskMode).toBe("debug") + }) + }) + // Add more tests for error handling (missing params, invalid mode, approval denied) if needed }) diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index dbe08cbae8..38082ae8fc 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -57,6 +57,25 @@ export async function newTaskTool( return } + // Track subtask context for the same mode + // This helps when Orchestrator calls the same mode multiple times + if (!cline.subtaskContextByMode) { + cline.subtaskContextByMode = new Map() + } + + // Get previous context for this mode if it exists + const previousContext = cline.subtaskContextByMode.get(mode) + let enhancedMessage = unescapedMessage + + // If there's previous context for this mode, prepend it to the message + if (previousContext && previousContext.length > 0) { + const contextSummary = previousContext + .map((ctx, index) => `Previous ${targetMode.name} subtask ${index + 1} result: ${ctx}`) + .join("\n") + + enhancedMessage = `[Context from previous ${targetMode.name} subtasks]\n${contextSummary}\n\n[Current task]\n${unescapedMessage}` + } + const toolMessage = JSON.stringify({ tool: "newTask", mode: targetMode.name, @@ -82,14 +101,17 @@ export async function newTaskTool( // Preserve the current mode so we can resume with it later. cline.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug - // Create new task instance first (this preserves parent's current mode in its history) - const newCline = await provider.createTask(unescapedMessage, undefined, cline) + // Create new task instance with enhanced message that includes context + const newCline = await provider.createTask(enhancedMessage, undefined, cline) if (!newCline) { pushToolResult(t("tools:newTask.errors.policy_restriction")) return } + // Store the new task reference so we can track its result later + cline.currentSubtaskMode = mode + // Now switch the newly created task to the desired mode await provider.handleModeSwitch(mode) @@ -98,7 +120,7 @@ export async function newTaskTool( cline.emit(RooCodeEventName.TaskSpawned, newCline.taskId) - pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`) + pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${enhancedMessage}`) // Set the isPaused flag to true so the parent // task can wait for the sub-task to finish.