mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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:
parent
9a734d0cab
commit
34e074642c
3 changed files with 245 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue