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
This commit is contained in:
Roo Code 2025-08-15 16:42:21 +00:00
parent 9a734d0cab
commit 34e074642c
3 changed files with 245 additions and 3 deletions

View file

@ -189,6 +189,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
isPaused: boolean = false
pausedModeSlug: string = defaultModeSlug
private pauseInterval: NodeJS.Timeout | undefined
subtaskContextByMode?: Map<string, string[]>
currentSubtaskMode?: string
// API
readonly apiConfiguration: ProviderSettings
@ -1034,6 +1036,25 @@ export class Task extends EventEmitter<TaskEvents> 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.

View file

@ -36,6 +36,8 @@ const mockCline = {
consecutiveMistakeCount: 0,
isPaused: false,
pausedModeSlug: "ask",
subtaskContextByMode: undefined as Map<string, string[]> | 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
})

View file

@ -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.