diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts
index 21c973ab50..eed42aefdd 100644
--- a/src/core/assistant-message/presentAssistantMessage.ts
+++ b/src/core/assistant-message/presentAssistantMessage.ts
@@ -24,6 +24,13 @@ import { askFollowupQuestionTool } from "../tools/askFollowupQuestionTool"
import { switchModeTool } from "../tools/switchModeTool"
import { attemptCompletionTool } from "../tools/attemptCompletionTool"
import { newTaskTool } from "../tools/newTaskTool"
+import { dispatchTaskTool } from "../tools/dispatchTaskTool"
+import { getTaskStatusTool } from "../tools/getTaskStatusTool"
+import { consolidateResultsTool } from "../tools/consolidateResultsTool"
+import { cancelTaskTool } from "../tools/cancelTaskTool"
+import { releaseTasksTool } from "../tools/releaseTasksTool"
+import { resumeParentTaskTool } from "../tools/resumeParentTaskTool"
+import { startConversationTool } from "../tools/startConversationTool" // Added startConversationTool
import { checkpointSave } from "../checkpoints"
@@ -211,6 +218,24 @@ export async function presentAssistantMessage(cline: Task) {
const modeName = getModeBySlug(mode, customModes)?.name ?? mode
return `[${block.name} in ${modeName} mode: '${message}']`
}
+ case "dispatch_task": { // Added for dispatch_task
+ const mode = block.params.mode ?? defaultModeSlug
+ const message = block.params.message ?? "(no message)"
+ const modeName = getModeBySlug(mode, customModes)?.name ?? mode
+ return `[${block.name} in ${modeName} mode: '${message}']`
+ }
+ case "get_task_status":
+ return `[${block.name} for '${block.params.task_instance_ids}']`
+ case "consolidate_results":
+ return `[${block.name} for '${block.params.task_instance_ids}']`
+ case "cancel_task":
+ return `[${block.name} for task '${block.params.task_instance_id}']`
+ case "release_tasks":
+ return `[${block.name} for tasks '${block.params.task_instance_ids}']`
+ case "resume_parent_task":
+ return `[${block.name} for parent '${block.params.original_parent_id}']`
+ case "start_conversation": // Added for start_conversation
+ return `[${block.name} with initial prompt: '${block.params.initial_prompt}']`
}
}
@@ -504,6 +529,27 @@ export async function presentAssistantMessage(cline: Task) {
case "new_task":
await newTaskTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
break
+ case "dispatch_task":
+ await dispatchTaskTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag, toolDescription)
+ break
+ case "get_task_status":
+ await getTaskStatusTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
+ break
+ case "consolidate_results":
+ await consolidateResultsTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
+ break
+ case "cancel_task":
+ await cancelTaskTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
+ break
+ case "release_tasks":
+ await releaseTasksTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
+ break
+ case "resume_parent_task":
+ await resumeParentTaskTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
+ break
+ case "start_conversation": // Added for start_conversation
+ await startConversationTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
+ break
case "attempt_completion":
await attemptCompletionTool(
cline,
diff --git a/src/core/prompts/tools/cancelTask.ts b/src/core/prompts/tools/cancelTask.ts
new file mode 100644
index 0000000000..1c0b54ed12
--- /dev/null
+++ b/src/core/prompts/tools/cancelTask.ts
@@ -0,0 +1,15 @@
+import { ToolDefinition } from "./types"
+
+export const cancelTaskToolDefinition: ToolDefinition = {
+ name: "cancel_task",
+ description:
+ "Requests the cancellation of an actively running sub-task previously dispatched by `dispatch_task`. The target task will be moved to an 'aborted' state. This does not immediately remove the task record; use `release_tasks` for cleanup after confirming cancellation if needed.",
+ parameters: [
+ {
+ name: "task_instance_id",
+ description: "The instance ID of the task to be cancelled.",
+ type: "string",
+ required: true,
+ },
+ ],
+}
diff --git a/src/core/prompts/tools/consolidateResults.ts b/src/core/prompts/tools/consolidateResults.ts
new file mode 100644
index 0000000000..fab3508ed0
--- /dev/null
+++ b/src/core/prompts/tools/consolidateResults.ts
@@ -0,0 +1,16 @@
+import { ToolDefinition } from "./types"
+
+export const consolidateResultsToolDefinition: ToolDefinition = {
+ name: "consolidate_results",
+ description:
+ "Waits for one or more dispatched sub-tasks (identified by their instance IDs) to complete, fail, or be aborted. This is a blocking tool; the current task will pause until all specified sub-tasks have terminated. It then returns an aggregated list of their final statuses and results. After using this, consider calling `release_tasks` to clean up terminated task records.",
+ parameters: [
+ {
+ name: "task_instance_ids",
+ description:
+ "A comma-separated string of task instance IDs whose results are to be consolidated (e.g., 'id1,id2,id3').",
+ type: "string",
+ required: true,
+ },
+ ],
+}
diff --git a/src/core/prompts/tools/dispatchTask.ts b/src/core/prompts/tools/dispatchTask.ts
new file mode 100644
index 0000000000..ad5ba219a8
--- /dev/null
+++ b/src/core/prompts/tools/dispatchTask.ts
@@ -0,0 +1,22 @@
+import { ToolDefinition } from "./types"
+
+export const dispatchTaskToolDefinition: ToolDefinition = {
+ name: "dispatch_task",
+ description:
+ "Dispatches a new sub-task to be executed in parallel. This tool is non-blocking; the current task will continue to execute immediately after dispatching the sub-task. The sub-task will run independently. Use `get_task_status` to check its progress and `consolidate_results` to retrieve its output once completed.",
+ parameters: [
+ {
+ name: "mode",
+ description:
+ "The mode (persona or capability set) in which the new sub-task should operate (e.g., 'code', 'debug', 'architect', or a custom mode slug).",
+ type: "string",
+ required: true,
+ },
+ {
+ name: "message",
+ description: "The initial message or instruction for the new sub-task.",
+ type: "string",
+ required: true,
+ },
+ ],
+}
diff --git a/src/core/prompts/tools/getTaskStatus.ts b/src/core/prompts/tools/getTaskStatus.ts
new file mode 100644
index 0000000000..e80716190d
--- /dev/null
+++ b/src/core/prompts/tools/getTaskStatus.ts
@@ -0,0 +1,16 @@
+import { ToolDefinition } from "./types"
+
+export const getTaskStatusToolDefinition: ToolDefinition = {
+ name: "get_task_status",
+ description:
+ "Checks the current status of one or more actively running sub-tasks that were previously dispatched using `dispatch_task`. Tasks are identified by their instance IDs. If a task ID is not found among active tasks, its status will be reported as 'unknown'. To get final results of completed/failed tasks, use `consolidate_results`.",
+ parameters: [
+ {
+ name: "task_instance_ids",
+ description:
+ "A comma-separated string of task instance IDs for which to retrieve the status (e.g., 'id1,id2,id3').",
+ type: "string",
+ required: true,
+ },
+ ],
+}
diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts
index 736c716a27..37e97aab6c 100644
--- a/src/core/prompts/tools/index.ts
+++ b/src/core/prompts/tools/index.ts
@@ -21,9 +21,128 @@ import { getUseMcpToolDescription } from "./use-mcp-tool"
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
import { getSwitchModeDescription } from "./switch-mode"
import { getNewTaskDescription } from "./new-task"
+import { dispatchTaskToolDefinition } from "./dispatchTask"
+import { getTaskStatusToolDefinition } from "./getTaskStatus"
+import { consolidateResultsToolDefinition } from "./consolidateResults"
+import { cancelTaskToolDefinition } from "./cancelTask"
+import { releaseTasksToolDefinition } from "./releaseTasks"
+import { resumeParentTaskToolDefinition } from "./resumeParentTask"
+import { startConversationToolDefinition } from "./startConversation" // Import new definition
import { getCodebaseSearchDescription } from "./codebase-search"
import { CodeIndexManager } from "../../../services/code-index/manager"
+// Function for the new tool's description
+function getDispatchTaskDescription(): string {
+ return `
+ ${dispatchTaskToolDefinition.name}
+ ${dispatchTaskToolDefinition.description}
+
+ ${dispatchTaskToolDefinition.parameters
+ .map(
+ (param) =>
+ `\n${param.name}\n${param.type}\n${param.description}\n`,
+ )
+ .join("\n\t\t")}
+
+`
+}
+
+// Function for get_task_status tool's description
+function getGetTaskStatusDescription(): string {
+ return `
+ ${getTaskStatusToolDefinition.name}
+ ${getTaskStatusToolDefinition.description}
+
+ ${getTaskStatusToolDefinition.parameters
+ .map(
+ (param) =>
+ `\n${param.name}\n${param.type}\n${param.description}\n`,
+ )
+ .join("\n\t\t")}
+
+`
+}
+
+// Function for consolidate_results tool's description
+function getConsolidateResultsDescription(): string {
+ return `
+ ${consolidateResultsToolDefinition.name}
+ ${consolidateResultsToolDefinition.description}
+
+ ${consolidateResultsToolDefinition.parameters
+ .map(
+ (param) =>
+ `\n${param.name}\n${param.type}\n${param.description}\n`,
+ )
+ .join("\n\t\t")}
+
+`
+}
+
+// Function for cancel_task tool's description
+function getCancelTaskDescription(): string {
+ return `
+ ${cancelTaskToolDefinition.name}
+ ${cancelTaskToolDefinition.description}
+
+ ${cancelTaskToolDefinition.parameters
+ .map(
+ (param) =>
+ `\n${param.name}\n${param.type}\n${param.description}\n`,
+ )
+ .join("\n\t\t")}
+
+`
+}
+
+// Function for release_tasks tool's description
+function getReleaseTasksDescription(): string {
+ return `
+ ${releaseTasksToolDefinition.name}
+ ${releaseTasksToolDefinition.description}
+
+ ${releaseTasksToolDefinition.parameters
+ .map(
+ (param) =>
+ `\n${param.name}\n${param.type}\n${param.description}\n`,
+ )
+ .join("\n\t\t")}
+
+`
+}
+
+// Function for resume_parent_task tool's description
+function getResumeParentTaskDescription(): string {
+ return `
+ ${resumeParentTaskToolDefinition.name}
+ ${resumeParentTaskToolDefinition.description}
+
+ ${resumeParentTaskToolDefinition.parameters
+ .map(
+ (param) =>
+ `\n${param.name}\n${param.type}\n${param.description}\n`,
+ )
+ .join("\n\t\t")}
+
+`
+}
+
+// Function for start_conversation tool's description
+function getStartConversationDescription(): string {
+ return `
+ ${startConversationToolDefinition.name}
+ ${startConversationToolDefinition.description}
+
+ ${startConversationToolDefinition.parameters
+ .map(
+ (param) =>
+ `\n${param.name}\n${param.type}\n${param.description}\n`,
+ )
+ .join("\n\t\t")}
+
+`
+}
+
// Map of tool names to their description functions
const toolDescriptionMap: Record string | undefined> = {
execute_command: (args) => getExecuteCommandDescription(args),
@@ -41,6 +160,13 @@ const toolDescriptionMap: Record string | undefined>
codebase_search: () => getCodebaseSearchDescription(),
switch_mode: () => getSwitchModeDescription(),
new_task: (args) => getNewTaskDescription(args),
+ dispatch_task: () => getDispatchTaskDescription(),
+ get_task_status: () => getGetTaskStatusDescription(),
+ consolidate_results: () => getConsolidateResultsDescription(),
+ cancel_task: () => getCancelTaskDescription(),
+ release_tasks: () => getReleaseTasksDescription(),
+ resume_parent_task: () => getResumeParentTaskDescription(),
+ start_conversation: () => getStartConversationDescription(), // Added start_conversation
insert_content: (args) => getInsertContentDescription(args),
search_and_replace: (args) => getSearchAndReplaceDescription(args),
apply_diff: (args) =>
diff --git a/src/core/prompts/tools/releaseTasks.ts b/src/core/prompts/tools/releaseTasks.ts
new file mode 100644
index 0000000000..f2480e8120
--- /dev/null
+++ b/src/core/prompts/tools/releaseTasks.ts
@@ -0,0 +1,16 @@
+import { ToolDefinition } from "./types"
+
+export const releaseTasksToolDefinition: ToolDefinition = {
+ name: "release_tasks",
+ description:
+ "Releases the records of one or more terminated (completed, failed, or aborted) sub-tasks from active memory. This should be called after results have been consolidated and the task records are no longer needed. This tool only affects tasks that are already in a terminal state.",
+ parameters: [
+ {
+ name: "task_instance_ids",
+ description:
+ "A comma-separated string of task instance IDs whose records are to be released (e.g., 'id1,id2,id3').",
+ type: "string",
+ required: true,
+ },
+ ],
+}
diff --git a/src/core/prompts/tools/resumeParentTask.ts b/src/core/prompts/tools/resumeParentTask.ts
new file mode 100644
index 0000000000..a3679dff9d
--- /dev/null
+++ b/src/core/prompts/tools/resumeParentTask.ts
@@ -0,0 +1,22 @@
+import { ToolDefinition } from "./types"
+
+export const resumeParentTaskToolDefinition: ToolDefinition = {
+ name: "resume_parent_task",
+ description:
+ "Called by a mediator agent after it has processed a task's original result. This tool signals that the original parent task (if one exists) should now be resumed using the (potentially modified) result provided by the mediator. If the original task was a root task, this tool indicates the mediation is complete.",
+ parameters: [
+ {
+ name: "original_parent_id",
+ description:
+ "The instance ID of the original parent task that was awaiting mediation. Should be 'null' (as a string) if the original task was a root task.",
+ type: "string", // Will be parsed, 'null' string for actual null
+ required: true,
+ },
+ {
+ name: "mediated_result",
+ description: "The final result (potentially modified by the mediator) to be passed to the original parent task or considered the final output if the original was a root task.",
+ type: "string",
+ required: true,
+ },
+ ],
+}
diff --git a/src/core/prompts/tools/startConversation.ts b/src/core/prompts/tools/startConversation.ts
new file mode 100644
index 0000000000..50c71b52b6
--- /dev/null
+++ b/src/core/prompts/tools/startConversation.ts
@@ -0,0 +1,34 @@
+import { ToolDefinition } from "./types"
+
+export const startConversationToolDefinition: ToolDefinition = {
+ name: "start_conversation",
+ description:
+ "Initiates and manages a structured, turn-by-turn conversation (debate) between two dynamically defined AI agents to collaboratively refine ideas, critique plans, or create artifacts. The conversation continues until a specified termination condition is met, as judged by a referee LLM. Returns the full conversation transcript.",
+ parameters: [
+ {
+ name: "participants",
+ description:
+ "A JSON string representing an array of two participant agent definitions. Each definition object should have: `base_mode` (optional string, e.g., 'code', 'architect', defaults to general agent mode) and `dynamic_persona_instructions` (required string, specific instructions for this agent in this conversation). Example: '[{\"base_mode\": \"code\", \"dynamic_persona_instructions\": \"You are a senior Python developer. Focus on code clarity and efficiency.\"}, {\"dynamic_persona_instructions\": \"You are a QA engineer. Focus on edge cases and potential bugs.\"}]'",
+ type: "string", // JSON string
+ required: true,
+ },
+ {
+ name: "shared_context",
+ description: "The initial data, document, code snippet, or problem statement that the conversation should be based on. This will be provided to both agents.",
+ type: "string",
+ required: true,
+ },
+ {
+ name: "initial_prompt",
+ description: "The first message to start the conversation, which will be delivered to the first participant.",
+ type: "string",
+ required: true,
+ },
+ {
+ name: "termination_condition",
+ description: "A clear, objective question that a referee LLM will use to determine if the debate is over after each round (e.g., 'Have the participants produced a final, agreed-upon list of changes?'). The referee will answer 'yes' or 'no'.",
+ type: "string",
+ required: true,
+ },
+ ],
+}
diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts
index 46da7485ed..355e2a13c3 100644
--- a/src/core/task/Task.ts
+++ b/src/core/task/Task.ts
@@ -116,9 +116,12 @@ export type TaskOptions = {
parentTask?: Task
taskNumber?: number
onCreated?: (cline: Task) => void
+ systemPromptOverride?: string // Added for custom system prompts
}
export class Task extends EventEmitter {
+ static activeTasks: Map = new Map() // Added activeTasks map
+
readonly taskId: string
readonly instanceId: string
@@ -183,6 +186,7 @@ export class Task extends EventEmitter {
consecutiveMistakeLimit: number
consecutiveMistakeCountForApplyDiff: Map = new Map()
toolUsage: ToolUsage = {}
+ dispatchedTaskIds: Set = new Set()
// Checkpoints
enableCheckpoints: boolean
@@ -192,6 +196,12 @@ export class Task extends EventEmitter {
// Streaming
isWaitingForFirstChunk = false
isStreaming = false
+ status: "pending" | "running" | "completed" | "failed" | "aborted" | "completed_pending_mediation" = "pending"
+ finalResult: string | null = null
+ isAwaitingMediation: boolean = false // Added for mediator pattern
+ mediatedResultForResumption: string | null = null // Added for mediator result passing
+ private systemPromptOverride?: string // Store the override
+
currentStreamingContentIndex = 0
assistantMessageContent: AssistantMessageContent[] = []
presentAssistantMessageLocked = false
@@ -217,9 +227,12 @@ export class Task extends EventEmitter {
parentTask,
taskNumber = -1,
onCreated,
+ systemPromptOverride, // Destructure new option
}: TaskOptions) {
super()
+ this.systemPromptOverride = systemPromptOverride // Store it
+
if (startTask && !task && !images && !historyItem) {
throw new Error("Either historyItem or task/images must be provided")
}
@@ -285,6 +298,23 @@ export class Task extends EventEmitter {
onCreated?.(this)
+ Task.activeTasks.set(this.taskId, this) // Add to activeTasks
+ this.status = "pending" // Set initial status
+
+ this.on("taskCompleted", (taskId, tokenUsage, toolUsage) => { // Event params might be useful later
+ this.status = "completed"
+ // Task remains in activeTasks until explicitly cleaned up or consolidated.
+ // finalResult will be set by attemptCompletionTool.
+ })
+
+ this.on("taskToolFailed", (taskId, toolName, error) => { // Event params might be useful later
+ this.status = "failed"
+ this.finalResult = `Tool '${toolName}' failed: ${error}`
+ // Task remains in activeTasks.
+ })
+
+ // The 'aborted' status is handled in abortTask method, which will also set a finalResult.
+
if (startTask) {
if (task || images) {
this.startTask(task, images)
@@ -1072,6 +1102,8 @@ export class Task extends EventEmitter {
}
this.abort = true
+ this.status = "aborted" // Set status
+ this.finalResult = "Task was aborted." // Set finalResult for aborted tasks
this.emit("taskAborted")
try {
@@ -1087,6 +1119,7 @@ export class Task extends EventEmitter {
} catch (error) {
console.error(`Error saving messages during abort for task ${this.taskId}.${this.instanceId}:`, error)
}
+ // Task.activeTasks.delete(this.taskId) // Task remains in activeTasks
}
// Used when a sub-task is launched and the parent task is waiting for it to
@@ -1114,6 +1147,7 @@ export class Task extends EventEmitter {
let nextUserContent = userContent
let includeFileDetails = true
+ this.status = "running" // Set status
this.emit("taskStarted")
while (!this.abort) {
@@ -1562,6 +1596,10 @@ export class Task extends EventEmitter {
}
private async getSystemPrompt(): Promise {
+ if (this.systemPromptOverride) {
+ return this.systemPromptOverride
+ }
+
const { mcpEnabled } = (await this.providerRef.deref()?.getState()) ?? {}
let mcpHub: McpHub | undefined
if (mcpEnabled ?? true) {
diff --git a/src/core/tools/attemptCompletionTool.ts b/src/core/tools/attemptCompletionTool.ts
index 57f5870022..1e8464d528 100644
--- a/src/core/tools/attemptCompletionTool.ts
+++ b/src/core/tools/attemptCompletionTool.ts
@@ -68,9 +68,102 @@ export async function attemptCompletionTool(
// Command execution is permanently disabled in attempt_completion
// Users must use execute_command tool separately before attempt_completion
await cline.say("completion_result", result, undefined, false)
+
+ cline.finalResult = result // Set the final result on the task instance
+
TelemetryService.instance.captureTaskCompleted(cline.taskId)
+ // Emitting taskCompleted will trigger the handler in Task.ts to set status = "completed"
+ // Note: The task remains in Task.activeTasks
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage)
+ const provider = cline.providerRef.deref()
+ if (!provider) {
+ // Should not happen, but good to check
+ await handleError("attempt_completion", new Error("Provider reference lost"))
+ return
+ }
+
+ const { taskCompletionMediatorModeEnabled, taskCompletionMediatorAgentMode } = await provider.getState()
+
+ if (taskCompletionMediatorModeEnabled) {
+ const mediatorInput = {
+ originalTaskId: cline.taskId,
+ originalParentId: cline.parentTask?.taskId || null,
+ originalResult: result,
+ }
+ const mediatorMessage = JSON.stringify(mediatorInput)
+
+ const originalParentTaskInstance = cline.parentTask;
+
+ if (originalParentTaskInstance) {
+ originalParentTaskInstance.isAwaitingMediation = true
+ provider.log(`[Mediator] Task ${cline.taskId} completed. Parent ${originalParentTaskInstance.taskId} now awaiting mediation.`)
+ } else {
+ provider.log(`[Mediator] Root task ${cline.taskId} completed. Initiating mediator task (as new root).`)
+ }
+
+ const originalModeOfProvider = (await provider.getState()).mode ?? defaultModeSlug;
+ await provider.handleModeSwitch(taskCompletionMediatorAgentMode);
+ await new Promise(resolve => setTimeout(resolve, 100)); // Ensure mode switch is processed
+
+ // Launch mediator:
+ // - If originalParentTaskInstance exists, mediator is its child.
+ // - If not (cline was a root task), mediator is a new root task.
+ const mediatorTaskParent = originalParentTaskInstance || undefined;
+ const mediatorTask = await provider.initClineWithTask(mediatorMessage, undefined, mediatorTaskParent, {});
+
+ if (!mediatorTask) {
+ await handleError("attempt_completion", new Error("Failed to create mediator task."));
+ if (originalParentTaskInstance) originalParentTaskInstance.isAwaitingMediation = false;
+ await provider.handleModeSwitch(originalModeOfProvider); // Switch back mode
+ return;
+ }
+ provider.log(`[Mediator] Mediator task ${mediatorTask.taskId} (parent: ${mediatorTaskParent?.taskId || 'none'}) created in mode '${taskCompletionMediatorAgentMode}'.`);
+
+ // Restore provider's mode if it was changed for the mediator.
+ // This is important if the completingTask's own completion processing by the provider might be affected by the mode.
+ // However, the completingTask (`cline`) will be terminated shortly.
+ // The active mode should remain `taskCompletionMediatorAgentMode` for the mediator.
+ // If `originalParentTaskInstance` was null, this means the mediator is now the primary task.
+ // If `originalParentTaskInstance` exists, it will resume in its own original mode when the mediator calls `resume_parent_task`.
+ // So, no need to switch back `originalModeOfProvider` here. The provider is now set for the mediator.
+
+ // Terminate the current task (cline) cleanly.
+ // Its result is passed to the mediator. It should not proceed to call finishSubTask.
+ // We can set a special status and prevent further actions.
+ cline.status = "completed_pending_mediation" as any; // Add this to status types if persisted
+ cline.finalResult = `Handed off to mediator. Original result: ${result}`;
+ // No call to provider.finishSubTask(result) for cline.
+ // The task `cline` is now effectively done. Its parent (if any, which is originalParentTaskInstance)
+ // will not be resumed by `cline`'s completion, but by the mediator.
+ // If `cline` itself was the root, the mediator effectively becomes the new primary flow.
+
+ // We need to ensure that `cline` is popped from the stack correctly if it's a sub-task
+ // without triggering the standard parent resumption.
+ // `attemptCompletionTool` is called from `presentAssistantMessage`.
+ // After this tool returns, `presentAssistantMessage` might try to do more.
+ // The `return;` here ensures this tool's execution path stops.
+ // The `cline` (completing task) itself will be on the `clineStack`.
+ // If `mediatorTaskParent` is `originalParentTaskInstance`, then `mediatorTask` is pushed onto the stack.
+ // When `mediatorTask` finishes, it pops, and `originalParentTaskInstance` is at top.
+ // If `mediatorTaskParent` is `undefined`, `mediatorTask` becomes a new root stack.
+ // What happens to `cline` on the stack?
+ // If `cline` was `taskA` (child of `root`), and `originalParentTaskInstance` is `root`.
+ // Mediator becomes child of `root`. `taskA` is still on stack below mediator.
+ // This needs careful handling of `clineStack` in `ClineProvider`.
+
+ // Revised thought: `cline` (completing task) should be removed from stack by its `provider.finishSubTask`
+ // but `finishSubTask` needs to know NOT to resume `cline.parentTask` if mediation is involved.
+ // The `isAwaitingMediation` flag on `cline.parentTask` already handles this.
+ // So, `cline` can complete "normally" after this point, its `finishSubTask` will be called.
+ // `finishSubTask` will see `cline.parentTask.isAwaitingMediation` is true and not resume it.
+
+ // So, the `return;` is correct. `cline` has done its part.
+ // The system state (`isAwaitingMediation`, `mediatedResultForResumption`) will handle the rest.
+ return;
+ }
+
+ // --- Original logic if mediator mode is NOT enabled ---
if (cline.parentTask) {
const didApprove = await askFinishSubTaskApproval()
@@ -79,7 +172,7 @@ export async function attemptCompletionTool(
}
// tell the provider to remove the current subtask and resume the previous task in the stack
- await cline.providerRef.deref()?.finishSubTask(result)
+ await provider.finishSubTask(result)
return
}
diff --git a/src/core/tools/cancelTaskTool.ts b/src/core/tools/cancelTaskTool.ts
new file mode 100644
index 0000000000..5bffcbd61c
--- /dev/null
+++ b/src/core/tools/cancelTaskTool.ts
@@ -0,0 +1,74 @@
+import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools"
+import { Task } from "../task/Task"
+import { formatResponse } from "../prompts/responses"
+import { t } from "../../i18n"
+
+export async function cancelTaskTool(
+ cline: Task, // The calling Task instance
+ block: ToolUse, // Parsed tool use from LLM
+ askApproval: AskApproval,
+ handleError: HandleError,
+ pushToolResult: PushToolResult,
+ removeClosingTag: RemoveClosingTag,
+) {
+ const taskIdToCancel: string | undefined = block.params.task_instance_id
+
+ try {
+ if (block.partial) {
+ const partialData = {
+ tool: "cancel_task",
+ task_instance_id: removeClosingTag("task_instance_id", taskIdToCancel),
+ }
+ await cline.say("tool_in_progress", JSON.stringify(partialData), undefined, true)
+ return
+ }
+
+ if (!taskIdToCancel) {
+ cline.consecutiveMistakeCount++
+ cline.recordToolError("cancel_task")
+ pushToolResult(await cline.sayAndCreateMissingParamError("cancel_task", "task_instance_id"))
+ return
+ }
+
+ cline.consecutiveMistakeCount = 0
+
+ const toolMessage = JSON.stringify({
+ tool: "cancel_task",
+ task_instance_id: taskIdToCancel,
+ })
+
+ // Approval might be important for cancelling tasks
+ const didApprove = await askApproval("tool", toolMessage)
+ if (!didApprove) {
+ pushToolResult(formatResponse.toolError(t("common:errors.user_rejected_tool_use", { toolName: "cancel_task" })))
+ return
+ }
+
+ const taskInstance = Task.activeTasks.get(taskIdToCancel)
+
+ if (!taskInstance) {
+ pushToolResult(formatResponse.toolError(`Task with ID '${taskIdToCancel}' not found or already terminated.`))
+ cline.recordToolError("cancel_task", `Task ID ${taskIdToCancel} not found`)
+ return
+ }
+
+ if (taskInstance.status === "aborted" || taskInstance.status === "completed" || taskInstance.status === "failed") {
+ pushToolResult(formatResponse.toolSuccess(`Task with ID '${taskIdToCancel}' is already in a terminal state: ${taskInstance.status}.`))
+ return
+ }
+
+ await taskInstance.abortTask(true) // true for isAbandoned, or consider if this should be false
+
+ // abortTask now sets the status to "aborted" and finalResult.
+ // It also emits "taskAborted" event.
+ // The task remains in Task.activeTasks.
+
+ pushToolResult(formatResponse.toolSuccess(`Task with ID '${taskIdToCancel}' has been requested to cancel. Its status is now '${taskInstance.status}'.`))
+
+ } catch (error) {
+ await handleError(t("tools:cancelTask.errors.generic", { error: error.message }), error)
+ cline.recordToolError("cancel_task", error.message)
+ pushToolResult(formatResponse.toolError(t("tools:cancelTask.errors.generic", { error: error.message })))
+ return
+ }
+}
diff --git a/src/core/tools/consolidateResultsTool.ts b/src/core/tools/consolidateResultsTool.ts
new file mode 100644
index 0000000000..9577060142
--- /dev/null
+++ b/src/core/tools/consolidateResultsTool.ts
@@ -0,0 +1,103 @@
+import pWaitFor from "p-wait-for"
+import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools"
+import { Task } from "../task/Task"
+import { formatResponse } from "../prompts/responses"
+import { t } from "../../i18n"
+
+export async function consolidateResultsTool(
+ cline: Task, // The calling Task instance
+ block: ToolUse, // Parsed tool use from LLM
+ askApproval: AskApproval, // May not be needed if consolidation is an automatic internal step
+ handleError: HandleError,
+ pushToolResult: PushToolResult,
+ removeClosingTag: RemoveClosingTag,
+) {
+ const taskIdsParam: string | undefined = block.params.task_instance_ids
+
+ try {
+ if (block.partial) {
+ const partialData = {
+ tool: "consolidate_results",
+ task_instance_ids: removeClosingTag("task_instance_ids", taskIdsParam),
+ }
+ await cline.say("tool_in_progress", JSON.stringify(partialData), undefined, true)
+ return
+ }
+
+ if (!taskIdsParam) {
+ cline.consecutiveMistakeCount++
+ cline.recordToolError("consolidate_results")
+ pushToolResult(await cline.sayAndCreateMissingParamError("consolidate_results", "task_instance_ids"))
+ return
+ }
+
+ cline.consecutiveMistakeCount = 0
+ const taskIdsToConsolidate = taskIdsParam.split(",").map(id => id.trim()).filter(id => id)
+
+ if (taskIdsToConsolidate.length === 0) {
+ pushToolResult(formatResponse.toolError("No task instance IDs provided for consolidation."))
+ cline.recordToolError("consolidate_results", "No task instance IDs provided")
+ return
+ }
+
+ // This tool is blocking. Inform the user.
+ await cline.say("text", `Waiting for tasks to complete: ${taskIdsToConsolidate.join(", ")}...`, undefined, false, undefined, "in_progress")
+
+ const results: Record = {}
+ const consolidationTimeoutMs = 300_000 // 5 minutes timeout for all tasks to complete, adjust as needed
+ const individualTaskCheckIntervalMs = 500 // How often to check each task's status
+
+ await Promise.all(
+ taskIdsToConsolidate.map(async (taskId) => {
+ try {
+ await pWaitFor(
+ () => {
+ const taskInstance = Task.activeTasks.get(taskId)
+ return taskInstance?.status === "completed" || taskInstance?.status === "failed" || taskInstance?.status === "aborted"
+ },
+ {
+ interval: individualTaskCheckIntervalMs,
+ timeout: consolidationTimeoutMs / taskIdsToConsolidate.length, // Crude per-task timeout
+ message: `Timeout waiting for task ${taskId} to complete.`
+ }
+ )
+ const taskInstance = Task.activeTasks.get(taskId)
+ if (taskInstance) {
+ results[taskId] = {
+ status: taskInstance.status,
+ result: taskInstance.finalResult,
+ }
+ // Optional: Clean up task from activeTasks after consolidation
+ // Task.activeTasks.delete(taskId)
+ // cline.dispatchedTaskIds.delete(taskId) // Also remove from parent's tracking set
+ } else {
+ // Should not happen if pWaitFor resolved based on status, unless task was removed by another process.
+ results[taskId] = {
+ status: "unknown",
+ result: "Task instance not found after waiting.",
+ }
+ }
+ } catch (error) {
+ results[taskId] = {
+ status: "timeout_or_error",
+ result: error.message || "Error during consolidation wait.",
+ }
+ }
+ })
+ )
+
+ // Update progress status to complete
+ await cline.say("text", `Consolidation complete for tasks: ${taskIdsToConsolidate.join(", ")}.`, undefined, false, undefined, "complete")
+
+
+ pushToolResult(formatResponse.toolSuccess(JSON.stringify(results)))
+
+ } catch (error) {
+ await handleError(t("tools:consolidateResults.errors.generic", { error: error.message }), error)
+ cline.recordToolError("consolidate_results", error.message)
+ // Ensure progress is marked as error if the tool itself fails
+ await cline.say("text", `Error during task consolidation: ${error.message}`, undefined, false, undefined, "error")
+ pushToolResult(formatResponse.toolError(t("tools:consolidateResults.errors.generic", { error: error.message })))
+ return
+ }
+}
diff --git a/src/core/tools/dispatchTaskTool.ts b/src/core/tools/dispatchTaskTool.ts
new file mode 100644
index 0000000000..8b358275f4
--- /dev/null
+++ b/src/core/tools/dispatchTaskTool.ts
@@ -0,0 +1,119 @@
+import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolDescription } from "../../shared/tools"
+import { Task } from "../task/Task"
+import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
+import { formatResponse } from "../prompts/responses"
+import { t } from "../../i18n"
+
+export async function dispatchTaskTool(
+ cline: Task, // The calling Task instance (orchestrator)
+ block: ToolUse, // Parsed tool use from LLM
+ askApproval: AskApproval,
+ handleError: HandleError,
+ pushToolResult: PushToolResult,
+ removeClosingTag: RemoveClosingTag,
+ toolDescription: ToolDescription,
+) {
+ const mode: string | undefined = block.params.mode
+ const message: string | undefined = block.params.message
+
+ try {
+ if (block.partial) {
+ // Handle partial streaming if necessary (similar to other tools)
+ const partialData = {
+ tool: "dispatch_task",
+ mode: removeClosingTag("mode", mode),
+ message: removeClosingTag("message", message),
+ }
+ // For dispatch, we might not need to ask for approval on partial,
+ // but good to have a placeholder for consistency.
+ // Let's assume it just updates UI for now, doesn't block.
+ await cline.say("tool_in_progress", JSON.stringify(partialData), undefined, true)
+ return
+ }
+
+ // Validate parameters
+ if (!mode) {
+ cline.consecutiveMistakeCount++
+ cline.recordToolError("dispatch_task")
+ pushToolResult(await cline.sayAndCreateMissingParamError("dispatch_task", "mode"))
+ return
+ }
+
+ if (!message) {
+ cline.consecutiveMistakeCount++
+ cline.recordToolError("dispatch_task")
+ pushToolResult(await cline.sayAndCreateMissingParamError("dispatch_task", "message"))
+ return
+ }
+
+ cline.consecutiveMistakeCount = 0
+ const unescapedMessage = message.replace(/\\\\@/g, "\\@") // Consistent with newTaskTool
+
+ const provider = cline.providerRef.deref()
+ if (!provider) {
+ throw new Error("ClineProvider reference lost")
+ }
+
+ const targetMode = getModeBySlug(mode, (await provider.getState()).customModes)
+ if (!targetMode) {
+ pushToolResult(formatResponse.toolError(`Invalid mode: ${mode}`))
+ cline.recordToolError("dispatch_task", `Invalid mode: ${mode}`)
+ return
+ }
+
+ const toolMessage = JSON.stringify({
+ tool: "dispatch_task",
+ mode: targetMode.name,
+ content: unescapedMessage,
+ })
+
+ const didApprove = await askApproval("tool", toolMessage)
+ if (!didApprove) {
+ // User rejected the tool use
+ pushToolResult(formatResponse.toolError(t("common:errors.user_rejected_tool_use", { toolName: "dispatch_task" })))
+ // No need to increment consecutiveMistakeCount here as it's a user decision
+ return
+ }
+
+ // Save checkpoint if enabled
+ if (cline.enableCheckpoints) {
+ await cline.checkpointSave(true)
+ }
+
+ // IMPORTANT: Unlike newTaskTool, we do NOT set cline.isPaused = true here.
+ // The parent task (cline) continues execution.
+
+ // Create the new task. It will be a child of the current task (cline).
+ // The provider.initClineWithTask will handle adding it to the clineStack.
+ const dispatchedTask = await provider.initClineWithTask(unescapedMessage, undefined, cline, {
+ // Pass relevant options from parent if needed, or use defaults
+ enableDiff: cline.diffEnabled,
+ enableCheckpoints: cline.enableCheckpoints,
+ fuzzyMatchThreshold: cline.fuzzyMatchThreshold,
+ consecutiveMistakeLimit: cline.consecutiveMistakeLimit,
+ experiments: (await provider.getState()).experiments,
+ })
+
+ if (!dispatchedTask) {
+ pushToolResult(formatResponse.toolError(t("tools:newTask.errors.policy_restriction"))) // Reusing existing translation
+ cline.recordToolError("dispatch_task", "Policy restriction or task creation failed")
+ return
+ }
+
+ // Store the dispatched task's ID in the parent task
+ cline.dispatchedTaskIds.add(dispatchedTask.taskId)
+
+ // Emit an event indicating a task was dispatched (optional, but good for observability)
+ cline.emit("taskSpawned" as any, dispatchedTask.taskId) // Reusing taskSpawned for now
+
+ // Return the taskId to the LLM
+ pushToolResult(formatResponse.toolSuccess(`Task dispatched with ID: ${dispatchedTask.taskId}. Mode: ${targetMode.name}. Message: "${unescapedMessage}"`))
+
+ } catch (error) {
+ await handleError(t("tools:dispatchTask.errors.generic", { error: error.message }), error)
+ cline.recordToolError("dispatch_task", error.message)
+ // Ensure a result is pushed even in case of unexpected errors
+ pushToolResult(formatResponse.toolError(t("tools:dispatchTask.errors.generic", { error: error.message })))
+ return
+ }
+}
diff --git a/src/core/tools/getTaskStatusTool.ts b/src/core/tools/getTaskStatusTool.ts
new file mode 100644
index 0000000000..b5dd0e2831
--- /dev/null
+++ b/src/core/tools/getTaskStatusTool.ts
@@ -0,0 +1,110 @@
+import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools"
+import { Task } from "../task/Task"
+import { formatResponse } from "../prompts/responses"
+import { t } from "../../i18n"
+
+export async function getTaskStatusTool(
+ cline: Task, // The calling Task instance
+ block: ToolUse, // Parsed tool use from LLM
+ askApproval: AskApproval, // Not typically needed for a read-only status check
+ handleError: HandleError,
+ pushToolResult: PushToolResult,
+ removeClosingTag: RemoveClosingTag,
+) {
+ const taskIdsParam: string | undefined = block.params.task_instance_ids
+
+ try {
+ if (block.partial) {
+ // For a status check, partial streaming might not be very useful,
+ // but handle it consistently.
+ const partialData = {
+ tool: "get_task_status",
+ task_instance_ids: removeClosingTag("task_instance_ids", taskIdsParam),
+ }
+ await cline.say("tool_in_progress", JSON.stringify(partialData), undefined, true)
+ return
+ }
+
+ if (!taskIdsParam) {
+ cline.consecutiveMistakeCount++
+ cline.recordToolError("get_task_status")
+ pushToolResult(await cline.sayAndCreateMissingParamError("get_task_status", "task_instance_ids"))
+ return
+ }
+
+ cline.consecutiveMistakeCount = 0
+
+ // Assuming task_instance_ids is a comma-separated string of IDs
+ const taskIds = taskIdsParam.split(",").map(id => id.trim()).filter(id => id)
+
+ if (taskIds.length === 0) {
+ pushToolResult(formatResponse.toolError("No task instance IDs provided."))
+ cline.recordToolError("get_task_status", "No task instance IDs provided")
+ return
+ }
+
+ const statuses: Record = {}
+ let allFound = true
+
+ for (const taskId of taskIds) {
+ const taskInstance = Task.activeTasks.get(taskId)
+ if (taskInstance) {
+ statuses[taskId] = taskInstance.status
+ } else {
+ // If not in activeTasks, it might have completed/failed already and been removed.
+ // Or it's an invalid ID. For now, mark as 'unknown' or try to find in history.
+ // This part might need refinement if we need to query historical/non-active tasks.
+ // For this iteration, "unknown" if not active is a starting point.
+ // A more robust solution would be to check a persistent store or the history if the task is not active.
+ // However, the task plan mentioned `Task.activeTasks.delete(this.taskId)` upon completion/failure.
+ // This implies that if a task is not in `activeTasks`, it's considered terminal.
+ // We need a way to get the *final* status if it was removed.
+ // For now, if it's not in activeTasks, we'll assume it's "completed" or "failed" if we can't find its final state.
+ // This part is tricky because the task removes itself from activeTasks upon completion/failure.
+ // The `consolidate_results` tool will need a more robust way to get final results.
+ // For `get_task_status`, if it's not active, its status is effectively its terminal state.
+ // Let's assume for now that if it's not in activeTasks, we can't get a live status,
+ // and it's up to `consolidate_results` to get final outcomes.
+ // So, if not found in activeTasks, its current "live" status is effectively 'unknown' or 'terminated'.
+ // The prompt for the tool should clarify it checks active tasks.
+ // A better approach: The Task events for completion/failure should also update a persistent status if needed.
+ // For now, let's keep it simple: if not in activeTasks, its status is what it was when it left.
+ // This is a simplification. A truly robust status system might need tasks to log their final state somewhere
+ // if they are removed from `activeTasks`.
+ // Given the current setup where tasks remove themselves from `activeTasks` upon termination:
+ // We can't reliably get a 'completed' or 'failed' status here for tasks already terminated *unless*
+ // we check another source (e.g. task history, or a new service).
+ // The plan states: "Task status is 'completed'" or "'failed'".
+ // This implies the `task.status` property IS the source of truth.
+ // So, if a task is NOT in `activeTasks` it means it already finished (completed, failed, aborted).
+ // We need a way to query that final status.
+ // This is a design flaw in removing from activeTasks immediately.
+ // Alternative: activeTasks stores ALL tasks ever created in this session, and status is the source of truth.
+ // Or, Task.status is updated, and it remains in activeTasks until consolidated or explicitly cleaned up.
+ // For now, let's assume `Task.activeTasks` holds tasks that are 'pending' or 'running'.
+ // If a task ID is not in `Task.activeTasks`, we cannot determine its status via this mechanism.
+ // This means `get_task_status` is for *currently active* tasks.
+ // The description of the tool for the LLM should reflect this.
+
+ // Let's adjust the logic: if not in activeTasks, we cannot provide a live status.
+ // The LLM should be guided to use this for tasks it expects to be still running.
+ // `consolidate_results` will be the one to get final outcomes.
+ statuses[taskId] = "unknown (not actively running or ID invalid)"
+ // allFound = false; // Decided against this, let it return status for found ones.
+ }
+ }
+
+ // if (!allFound && taskIds.length === 1) {
+ // pushToolResult(formatResponse.toolError(`Task with ID ${taskIds[0]} not found or not active.`))
+ // return
+ // }
+
+ pushToolResult(formatResponse.toolSuccess(JSON.stringify(statuses)))
+
+ } catch (error) {
+ await handleError(t("tools:getTaskStatus.errors.generic", { error: error.message }), error)
+ cline.recordToolError("get_task_status", error.message)
+ pushToolResult(formatResponse.toolError(t("tools:getTaskStatus.errors.generic", { error: error.message })))
+ return
+ }
+}
diff --git a/src/core/tools/releaseTasksTool.ts b/src/core/tools/releaseTasksTool.ts
new file mode 100644
index 0000000000..5b071fec32
--- /dev/null
+++ b/src/core/tools/releaseTasksTool.ts
@@ -0,0 +1,98 @@
+import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools"
+import { Task } from "../task/Task"
+import { formatResponse } from "../prompts/responses"
+import { t } from "../../i18n"
+
+export async function releaseTasksTool(
+ cline: Task, // The calling Task instance
+ block: ToolUse, // Parsed tool use from LLM
+ askApproval: AskApproval, // Approval might be good for a "destructive" action like releasing
+ handleError: HandleError,
+ pushToolResult: PushToolResult,
+ removeClosingTag: RemoveClosingTag,
+) {
+ const taskIdsParam: string | undefined = block.params.task_instance_ids
+
+ try {
+ if (block.partial) {
+ const partialData = {
+ tool: "release_tasks",
+ task_instance_ids: removeClosingTag("task_instance_ids", taskIdsParam),
+ }
+ await cline.say("tool_in_progress", JSON.stringify(partialData), undefined, true)
+ return
+ }
+
+ if (!taskIdsParam) {
+ cline.consecutiveMistakeCount++
+ cline.recordToolError("release_tasks")
+ pushToolResult(await cline.sayAndCreateMissingParamError("release_tasks", "task_instance_ids"))
+ return
+ }
+
+ cline.consecutiveMistakeCount = 0
+ const taskIdsToRelease = taskIdsParam.split(",").map(id => id.trim()).filter(id => id)
+
+ if (taskIdsToRelease.length === 0) {
+ pushToolResult(formatResponse.toolError("No task instance IDs provided for release."))
+ cline.recordToolError("release_tasks", "No task instance IDs provided")
+ return
+ }
+
+ const toolMessage = JSON.stringify({
+ tool: "release_tasks",
+ task_instance_ids: taskIdsToRelease.join(", "),
+ })
+
+ // It's good practice to ask for approval before removing task records,
+ // even if they are terminated.
+ const didApprove = await askApproval("tool", toolMessage)
+ if (!didApprove) {
+ pushToolResult(formatResponse.toolError(t("common:errors.user_rejected_tool_use", { toolName: "release_tasks" })))
+ return
+ }
+
+ const releasedIds: string[] = []
+ const notFoundIds: string[] = []
+ const stillActiveIds: string[] = []
+
+ for (const taskId of taskIdsToRelease) {
+ const taskInstance = Task.activeTasks.get(taskId)
+ if (taskInstance) {
+ // Only release tasks that are in a terminal state
+ if (taskInstance.status === "completed" || taskInstance.status === "failed" || taskInstance.status === "aborted") {
+ Task.activeTasks.delete(taskId)
+ releasedIds.push(taskId)
+ // The calling/orchestrator task should manage its own dispatchedTaskIds list
+ } else {
+ stillActiveIds.push(taskId)
+ }
+ } else {
+ notFoundIds.push(taskId)
+ }
+ }
+
+ let resultMessage = ""
+ if (releasedIds.length > 0) {
+ resultMessage += `Successfully released task records for IDs: ${releasedIds.join(", ")}. `
+ }
+ if (notFoundIds.length > 0) {
+ resultMessage += `Task IDs not found (possibly already released or invalid): ${notFoundIds.join(", ")}. `
+ }
+ if (stillActiveIds.length > 0) {
+ resultMessage += `Task IDs not released because they are still active (status not completed, failed, or aborted): ${stillActiveIds.join(", ")}. `
+ }
+
+ if (resultMessage === "") {
+ resultMessage = "No tasks were eligible for release or found with the provided IDs."
+ }
+
+ pushToolResult(formatResponse.toolSuccess(resultMessage.trim()))
+
+ } catch (error) {
+ await handleError(t("tools:releaseTasks.errors.generic", { error: error.message }), error)
+ cline.recordToolError("release_tasks", error.message)
+ pushToolResult(formatResponse.toolError(t("tools:releaseTasks.errors.generic", { error: error.message })))
+ return
+ }
+}
diff --git a/src/core/tools/resumeParentTaskTool.ts b/src/core/tools/resumeParentTaskTool.ts
new file mode 100644
index 0000000000..e43808eda4
--- /dev/null
+++ b/src/core/tools/resumeParentTaskTool.ts
@@ -0,0 +1,138 @@
+import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
+import { Task } from "../task/Task"
+import { ClineProvider } from "../webview/ClineProvider" // May need provider access
+import { formatResponse } from "../prompts/responses"
+import { t } from "../../i18n"
+
+export async function resumeParentTaskTool(
+ cline: Task, // This is the Mediator Task instance
+ block: ToolUse,
+ askApproval: AskApproval,
+ handleError: HandleError,
+ pushToolResult: PushToolResult,
+ removeClosingTag: RemoveClosingTag,
+) {
+ const originalParentId: string | null = block.params.original_parent_id === "null" ? null : block.params.original_parent_id
+ const mediatedResult: string | undefined = block.params.mediated_result
+
+ try {
+ if (block.partial) {
+ const partialData = {
+ tool: "resume_parent_task",
+ original_parent_id: removeClosingTag("original_parent_id", originalParentId),
+ mediated_result: removeClosingTag("mediated_result", mediatedResult),
+ }
+ await cline.say("tool_in_progress", JSON.stringify(partialData), undefined, true)
+ return
+ }
+
+ if (mediatedResult === undefined) { // original_parent_id can be null, but result is essential
+ cline.consecutiveMistakeCount++
+ cline.recordToolError("resume_parent_task")
+ pushToolResult(await cline.sayAndCreateMissingParamError("resume_parent_task", "mediated_result"))
+ return
+ }
+
+ cline.consecutiveMistakeCount = 0
+
+ const toolMessage = JSON.stringify({
+ tool: "resume_parent_task",
+ original_parent_id: originalParentId,
+ mediated_result: mediatedResult,
+ })
+
+ const didApprove = await askApproval("tool", toolMessage)
+ if (!didApprove) {
+ pushToolResult(formatResponse.toolError(t("common:errors.user_rejected_tool_use", { toolName: "resume_parent_task" })))
+ return
+ }
+
+ const provider = cline.providerRef.deref()
+ if (!provider) {
+ throw new Error("ClineProvider reference lost from mediator task")
+ }
+
+ if (originalParentId) {
+ const originalParentTask = Task.activeTasks.get(originalParentId)
+
+ if (!originalParentTask) {
+ pushToolResult(formatResponse.toolError(`Original parent task with ID '${originalParentId}' not found.`))
+ cline.recordToolError("resume_parent_task", `Original parent task ${originalParentId} not found.`)
+ return
+ }
+
+ if (!originalParentTask.isAwaitingMediation) {
+ pushToolResult(formatResponse.toolError(`Original parent task '${originalParentId}' was not awaiting mediation.`))
+ cline.recordToolError("resume_parent_task", `Original parent task ${originalParentId} not awaiting mediation.`)
+ return
+ }
+
+ originalParentTask.isAwaitingMediation = false
+
+ // To correctly resume, the originalParentTask needs to become the current task on the stack
+ // and then be unpaused.
+ // This might be complex if the mediator task is a child of the *completing* task,
+ // and the originalParentTask is further down the stack.
+ // `finishSubTask` on the mediator task (`cline`) should pop it.
+ // Then, if `originalParentTask` is now at the top of the stack, it can be resumed.
+
+ // For now, let's assume `originalParentTask.resumePausedTask` will make it active if it's the current one.
+ // The crucial part is that `finishSubTask` for the *mediator* task needs to happen AFTER this tool logic.
+ // This tool's success means the mediator's job is done.
+
+ // The `resumePausedTask` method handles unsetting `isPaused` and processing the message.
+ // We need to ensure the task stack in ClineProvider is managed correctly.
+ // When the mediator task (current `cline`) calls `attempt_completion` after this tool,
+ // its `finishSubTask` will be called. If its parent was the `completingTask`, that one would resume.
+ // This is not what we want.
+
+ // We need a way for the provider to switch active context back to originalParentTask
+ // and then feed it the result.
+
+ provider.log(`[Mediator] Resuming original parent task ${originalParentId} with mediated result.`)
+
+ // This is a conceptual step. The actual mechanism might need provider involvement
+ // to manage the task stack correctly and make originalParentTask the active one to resume.
+ // A simple direct call might not be enough if originalParentTask is not clineStack.getCurrentCline().
+
+ // Let's try a more direct approach assuming the `originalParentTask` can be directly "woken up".
+ // This assumes `resumePausedTask` can handle being called on a non-top-of-stack task
+ // if the provider's current task logic is adjusted, or if `finishSubTask` of the mediator
+ // correctly pops to the original parent.
+
+ // The mediator task (`cline`) will complete after this. Its parent is the `completingTask`.
+ // When `cline` (mediator) completes, `finishSubTask` will be called for it.
+ // `provider.finishSubTask` will pop `cline`, then attempt to resume `cline.parentTask` (the `completingTask`).
+ // This is fine. The `completingTask` will then also complete (as its `attempt_completion` was the trigger).
+ // When `completingTask` finishes, `provider.finishSubTask` is called for it.
+ // This will pop `completingTask`. Its parent is `originalParentTask`.
+ // Now `originalParentTask` is at the top. `provider.finishSubTask` for `completingTask` will call
+ // `originalParentTask.resumePausedTask(mediatedResult)` because `isAwaitingMediation` is now false.
+ // This seems like a plausible flow. The key is that `mediatedResult` must be passed along.
+
+ // So, this tool's job is primarily to:
+ // 1. Mark `originalParentTask.isAwaitingMediation = false`. (Done earlier)
+ // 2. Store `mediatedResult` on the `originalParentTask` so `ClineProvider.finishSubTask` can use it.
+
+ originalParentTask.mediatedResultForResumption = mediatedResult;
+
+ pushToolResult(formatResponse.toolSuccess(`Original parent task '${originalParentId}' is now set to resume with the new result. Mediator task should now complete.`))
+
+ // The mediator task should now call attempt_completion with a message indicating its success.
+ // Example: "Mediator processing complete. Original parent task ${originalParentId} will now resume."
+
+ } else {
+ // Original task was a root task. Mediator is effectively the new "main" result.
+ provider.log(`[Mediator] Original task was a root task. Mediator result: ${mediatedResult}`)
+ // The mediator can just say this result.
+ await cline.say("text", `Mediator result (original was root task): ${mediatedResult}`)
+ pushToolResult(formatResponse.toolSuccess("Mediator processed result for original root task."))
+ }
+
+ } catch (error) {
+ await handleError(t("tools:resumeParentTask.errors.generic", { error: error.message }), error)
+ cline.recordToolError("resume_parent_task", error.message)
+ pushToolResult(formatResponse.toolError(t("tools:resumeParentTask.errors.generic", { error: error.message })))
+ return
+ }
+}
diff --git a/src/core/tools/startConversationTool.ts b/src/core/tools/startConversationTool.ts
new file mode 100644
index 0000000000..860fb90de8
--- /dev/null
+++ b/src/core/tools/startConversationTool.ts
@@ -0,0 +1,270 @@
+import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools"
+import { Task, TaskOptions } from "../task/Task" // Assuming TaskOptions might be useful
+import { ClineProvider } from "../webview/ClineProvider"
+import { formatResponse } from "../prompts/responses"
+import { t } from "../../i18n"
+import { defaultModeSlug } from "../../shared/modes" // For default base_mode
+import { SYSTEM_PROMPT } from "../prompts/system" // To potentially build dynamic system prompts
+import { Anthropic } from "@anthropic-ai/sdk" // For message types
+
+interface ParticipantDefinition {
+ base_mode?: string // Slug for a base mode
+ dynamic_persona_instructions: string
+}
+
+interface StartConversationParams {
+ participants: ParticipantDefinition[]
+ shared_context: string
+ initial_prompt: string
+ termination_condition: string // A question for the referee LLM
+}
+
+export async function startConversationTool(
+ cline: Task, // The orchestrator Task instance
+ block: ToolUse,
+ askApproval: AskApproval,
+ handleError: HandleError,
+ pushToolResult: PushToolResult,
+ removeClosingTag: RemoveClosingTag,
+) {
+ // Extract and parse parameters
+ // The 'participants' parameter will be a JSON string from the LLM.
+ let participantsString = block.params.participants
+ let sharedContext = block.params.shared_context
+ let initialPrompt = block.params.initial_prompt
+ let terminationCondition = block.params.termination_condition
+
+ // Partial handling
+ if (block.partial) {
+ const partialData = {
+ tool: "start_conversation",
+ participants: removeClosingTag("participants", participantsString),
+ shared_context: removeClosingTag("shared_context", sharedContext),
+ initial_prompt: removeClosingTag("initial_prompt", initialPrompt),
+ termination_condition: removeClosingTag("termination_condition", terminationCondition),
+ }
+ await cline.say("tool_in_progress", JSON.stringify(partialData), undefined, true)
+ return
+ }
+
+ // Validate required parameters
+ if (!participantsString) {
+ return await handleErrorMissingParam("participants")
+ }
+ if (!sharedContext) {
+ // Allow empty shared_context, but it must be explicitly provided if intended.
+ // For now, let's make it required for simplicity in the first pass.
+ return await handleErrorMissingParam("shared_context")
+ }
+ if (!initialPrompt) {
+ return await handleErrorMissingParam("initial_prompt")
+ }
+ if (!terminationCondition) {
+ return await handleErrorMissingParam("termination_condition")
+ }
+
+ async function handleErrorMissingParam(paramName: string) {
+ cline.consecutiveMistakeCount++
+ cline.recordToolError("start_conversation")
+ pushToolResult(await cline.sayAndCreateMissingParamError("start_conversation", paramName))
+ }
+
+ let parsedParticipants: ParticipantDefinition[]
+ try {
+ parsedParticipants = JSON.parse(participantsString)
+ if (!Array.isArray(parsedParticipants) || parsedParticipants.length !== 2) {
+ throw new Error("Participants parameter must be an array of two agent definitions.")
+ }
+ for (const p of parsedParticipants) {
+ if (typeof p.dynamic_persona_instructions !== 'string') {
+ throw new Error("Each participant must have 'dynamic_persona_instructions' as a string.")
+ }
+ if (p.base_mode && typeof p.base_mode !== 'string') {
+ throw new Error("Participant 'base_mode', if provided, must be a string.")
+ }
+ }
+ } catch (error) {
+ await handleError(t("tools:startConversation.errors.paramParseError", { param: "participants", error: error.message }), error)
+ cline.recordToolError("start_conversation", `Failed to parse participants: ${error.message}`)
+ pushToolResult(formatResponse.toolError(`Error parsing 'participants' parameter: ${error.message}`))
+ return
+ }
+
+ cline.consecutiveMistakeCount = 0
+
+ const toolMessage = JSON.stringify({
+ tool: "start_conversation",
+ participants: parsedParticipants.map(p => ({ base_mode: p.base_mode || defaultModeSlug, persona: p.dynamic_persona_instructions })),
+ shared_context: sharedContext,
+ initial_prompt: initialPrompt,
+ termination_condition: terminationCondition,
+ }, null, 2)
+
+ const didApprove = await askApproval("tool", toolMessage)
+ if (!didApprove) {
+ pushToolResult(formatResponse.toolError(t("common:errors.user_rejected_tool_use", { toolName: "start_conversation" })))
+ return
+ }
+
+ const provider = cline.providerRef.deref()
+ if (!provider) {
+ await handleError("start_conversation", new Error("ClineProvider reference lost"))
+ pushToolResult(formatResponse.toolError("Internal error: Provider reference lost."))
+ return
+ }
+
+ let agentA_Task: Task | undefined = undefined
+ let agentB_Task: Task | undefined = undefined
+ const conversationTranscript: { speaker: string, utterance: string }[] = []
+
+ try {
+ await cline.say("text", "Setting up debate agents...", undefined, false, undefined, "in_progress")
+
+ // Agent Setup
+ const setupAgent = async (participantDef: ParticipantDefinition, agentName: string): Promise => {
+ const agentMode = participantDef.base_mode || defaultModeSlug;
+ const providerState = await provider.getState();
+
+ // Generate the base system prompt for the agent's mode
+ // Note: SYSTEM_PROMPT function needs various parameters from the provider's state.
+ const baseSystemPrompt = await SYSTEM_PROMPT(
+ provider.context,
+ provider.cwd, // Assuming debate agents operate in the same CWD
+ providerState.browserToolEnabled ?? false,
+ undefined, // mcpHub - debate agents likely won't use MCP tools directly in this simple setup
+ undefined, // diffStrategy
+ providerState.browserViewportSize,
+ agentMode, // Use the agent's specific mode here
+ providerState.customModePrompts,
+ providerState.customModes,
+ "", // customInstructions for this agent are from dynamic_persona_instructions
+ providerState.diffEnabled ?? false,
+ providerState.experiments,
+ providerState.enableMcpServerCreation,
+ providerState.language,
+ undefined, // rooIgnoreInstructions
+ (providerState.maxReadFileLine ?? -1) !== -1,
+ { maxConcurrentFileReads: providerState.maxConcurrentFileReads }
+ );
+
+ const dynamicSystemPrompt = `${baseSystemPrompt}\n\nYour specific instructions for this conversation:\n${participantDef.dynamic_persona_instructions}`;
+
+ // Create the temporary task with the systemPromptOverride
+ // These tasks are not added to the main clineStack by initClineWithTask,
+ // because we are calling new Task() directly.
+ const agentTask = new Task({
+ provider: provider,
+ apiConfiguration: cline.apiConfiguration, // Use orchestrator's API config
+ task: `Internal debate agent: ${agentName}`,
+ startTask: false,
+ systemPromptOverride: dynamicSystemPrompt, // Pass the fully constructed prompt
+ // Other minimal TaskOptions if needed
+ enableDiff: providerState.diffEnabled,
+ enableCheckpoints: false, // No checkpoints for transient debate agents
+ });
+ await agentTask.overwriteApiConversationHistory([]); // Start with a fresh history
+ return agentTask;
+ }
+
+ agentA_Task = await setupAgent(parsedParticipants[0], "AgentA");
+ agentB_Task = await setupAgent(parsedParticipants[1], "AgentB");
+
+ // Conversation Loop
+ let currentSpeakerTask = agentA_Task
+ let currentSpeakerName = "AgentA"
+ let nextSpeakerTask = agentB_Task
+ let nextSpeakerName = "AgentB"
+ let lastUtterance = initialPrompt
+
+ conversationTranscript.push({ speaker: "Context", utterance: sharedContext });
+ conversationTranscript.push({ speaker: "Orchestrator", utterance: initialPrompt });
+ await cline.say("text", `Debate started. Context: "${sharedContext}". Initial prompt to AgentA: "${initialPrompt}"`, undefined, false, undefined, "in_progress")
+
+
+ const MAX_TURNS = 10 // Safeguard against infinite loops
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
+ // Prepare input for current_speaker_task
+ const currentTurnInput: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: `${sharedContext}\n\nPrevious utterances:\n${conversationTranscript.map(t => `${t.speaker}: ${t.utterance}`).join("\n\n")}\n\nYour turn, ${currentSpeakerName}. Respond to: ${lastUtterance}`
+ }
+
+ await currentSpeakerTask.addToApiConversationHistory(currentTurnInput);
+
+ // Execute a "turn" for current_speaker_task
+ // This requires a simplified way to get a single response from a Task
+ // without its full UI interaction loop.
+ let assistantResponseText = "";
+ try {
+ const stream = currentSpeakerTask.attemptApiRequest(); // Assuming this returns the stream directly
+ for await (const chunk of stream) {
+ if (chunk.type === "text") {
+ assistantResponseText += chunk.text;
+ } else if (chunk.type === "usage") {
+ // log usage if necessary
+ }
+ }
+ if (!assistantResponseText) throw new Error("Agent did not provide a text response.");
+
+ // Add assistant's response to its own history for context in *its* next turn (if any)
+ await currentSpeakerTask.addToApiConversationHistory({role: "assistant", content: [{type: "text", text: assistantResponseText}]});
+
+ } catch (e) {
+ assistantResponseText = `Error during ${currentSpeakerName}'s turn: ${e.message}`;
+ conversationTranscript.push({ speaker: currentSpeakerName, utterance: assistantResponseText });
+ await cline.say("text", `Error in ${currentSpeakerName}'s turn: ${e.message}. Ending debate.`, undefined, false, undefined, "error")
+ break; // End debate on error
+ }
+
+ conversationTranscript.push({ speaker: currentSpeakerName, utterance: assistantResponseText });
+ lastUtterance = assistantResponseText;
+ await cline.say("text", `${currentSpeakerName} says: "${assistantResponseText}"`, undefined, false, undefined, "in_progress")
+
+
+ // Referee Check
+ const refereePrompt = `Conversation History:\n${conversationTranscript.map(t => `${t.speaker}: ${t.utterance}`).join("\n\n")}\n\nTermination Condition: "${terminationCondition}"\n\nBased on the conversation, has the termination condition been met? Answer strictly with "yes" or "no".`
+ const refereeResponse = await cline.api.createMessage(
+ await cline.getSystemPrompt(), // Orchestrator's system prompt for referee
+ [{role: "user", content: refereePrompt}],
+ {taskId: cline.taskId, mode: (await provider.getState()).mode ?? defaultModeSlug} // Orchestrator's metadata
+ )
+
+ let refereeDecision = "";
+ // Assuming createMessage returns a stream similar to Task's attemptApiRequest
+ for await (const chunk of refereeResponse) {
+ if (chunk.type === "text") refereeDecision += chunk.text;
+ }
+ refereeDecision = refereeDecision.trim().toLowerCase();
+ await cline.say("text", `Referee decision: "${refereeDecision}" (Condition: "${terminationCondition}")`, undefined, false, undefined, "in_progress")
+
+
+ if (refereeDecision.includes("yes")) {
+ await cline.say("text", "Termination condition met. Ending debate.", undefined, false, undefined, "complete")
+ break
+ }
+
+ if (turn === MAX_TURNS - 1) {
+ await cline.say("text", "Max turns reached. Ending debate.", undefined, false, undefined, "complete")
+ break
+ }
+
+ // Swap speakers
+ [currentSpeakerTask, nextSpeakerTask] = [nextSpeakerTask, currentSpeakerTask];
+ [currentSpeakerName, nextSpeakerName] = [nextSpeakerName, currentSpeakerName];
+ }
+
+ // Return Result
+ const finalTranscript = conversationTranscript.map(t => `${t.speaker}: ${t.utterance}`).join("\n\n")
+ pushToolResult(formatResponse.toolSuccess(`Debate finished. Transcript:\n${finalTranscript}`))
+
+ } catch (error) {
+ await handleError(t("tools:startConversation.errors.generic", { error: error.message }), error)
+ cline.recordToolError("start_conversation", error.message)
+ pushToolResult(formatResponse.toolError(`Error in start_conversation: ${error.message}`))
+ } finally {
+ // Cleanup
+ agentA_Task?.dispose(); // Task.dispose() needs to be robust
+ agentB_Task?.dispose();
+ await cline.say("text", "Debate agents cleaned up.", undefined, false, undefined, "complete")
+ }
+}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index dff2263a06..18bc345ac9 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -229,8 +229,24 @@ export class ClineProvider
console.log(`[subtasks] finishing subtask ${lastMessage}`)
// remove the last cline instance from the stack (this is the finished sub task)
await this.removeClineFromStack()
- // resume the last cline instance in the stack (if it exists - this is the 'parent' calling task)
- await this.getCurrentCline()?.resumePausedTask(lastMessage)
+
+ const parentTask = this.getCurrentCline()
+ if (parentTask) {
+ // Check if this parent task was awaiting mediation and now has a result from the mediator.
+ if (parentTask.mediatedResultForResumption !== null) {
+ const resultToUse = parentTask.mediatedResultForResumption;
+ parentTask.mediatedResultForResumption = null; // Clear it after use
+ parentTask.isAwaitingMediation = false; // Ensure this is also cleared
+ this.log(`[Mediator] Parent task ${parentTask.taskId} resuming with mediated result.`);
+ await parentTask.resumePausedTask(resultToUse);
+ } else if (parentTask.isAwaitingMediation) {
+ // Still awaiting mediation, a mediator hasn't called resume_parent_task with a result yet.
+ this.log(`[Mediator] Parent task ${parentTask.taskId} is awaiting mediation. Not resuming immediately as mediated result is not yet available.`);
+ } else {
+ // Standard resumption logic
+ await parentTask.resumePausedTask(lastMessage);
+ }
+ }
}
// Clear the current task without treating it as a subtask
@@ -529,37 +545,44 @@ export class ClineProvider
options: Partial<
Pick<
TaskOptions,
- "enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments"
+ // Added systemPromptOverride to the pick
+ "enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments" | "systemPromptOverride"
>
> = {},
) {
const {
apiConfiguration,
organizationAllowList,
- diffEnabled: enableDiff,
- enableCheckpoints,
- fuzzyMatchThreshold,
- experiments,
+ // Defaulting these from provider.getState() if not in options
+ diffEnabled: optionDiffEnabled,
+ enableCheckpoints: optionEnableCheckpoints,
+ fuzzyMatchThreshold: optionFuzzyMatchThreshold,
+ experiments: optionExperiments,
} = await this.getState()
if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) {
throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
}
+ const taskOptionsFromState = await this.getState();
+
+
const cline = new Task({
provider: this,
apiConfiguration,
- enableDiff,
- enableCheckpoints,
- fuzzyMatchThreshold,
+ enableDiff: options.enableDiff ?? taskOptionsFromState.diffEnabled,
+ enableCheckpoints: options.enableCheckpoints ?? taskOptionsFromState.enableCheckpoints,
+ fuzzyMatchThreshold: options.fuzzyMatchThreshold ?? taskOptionsFromState.fuzzyMatchThreshold,
task,
images,
- experiments,
+ experiments: options.experiments ?? taskOptionsFromState.experiments,
rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
parentTask,
taskNumber: this.clineStack.length + 1,
- onCreated: (cline) => this.emit("clineCreated", cline),
- ...options,
+ onCreated: (cl) => this.emit("clineCreated", cl), // Renamed cline to cl to avoid shadow
+ systemPromptOverride: options.systemPromptOverride, // Pass it through
+ // Pass other options from the partial pick
+ consecutiveMistakeLimit: options.consecutiveMistakeLimit, // This was missing from options destructuring but present in TaskOptions
})
await this.addClineToStack(cline)
@@ -1549,9 +1572,16 @@ export class ClineProvider
)
}
+ // Read workspace specific configurations
+ const workspaceConfig = vscode.workspace.getConfiguration(Package.name)
+ const taskCompletionMediatorModeEnabled = workspaceConfig.get("taskCompletionMediatorModeEnabled", false)
+ const taskCompletionMediatorAgentMode = workspaceConfig.get("taskCompletionMediatorAgentMode", "mediator-agent")
+
// Return the same structure as before
return {
apiConfiguration: providerSettings,
+ taskCompletionMediatorModeEnabled, // Added new setting
+ taskCompletionMediatorAgentMode, // Added new setting
lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
customInstructions: stateValues.customInstructions,
apiModelId: stateValues.apiModelId,
diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json
index 0265a84398..7e48c66498 100644
--- a/src/i18n/locales/en/tools.json
+++ b/src/i18n/locales/en/tools.json
@@ -12,5 +12,41 @@
"errors": {
"policy_restriction": "Failed to create new task due to policy restrictions."
}
+ },
+ "dispatchTask": {
+ "errors": {
+ "generic": "An error occurred while dispatching the task: {{error}}"
+ }
+ },
+ "getTaskStatus": {
+ "errors": {
+ "generic": "An error occurred while getting task status: {{error}}"
+ }
+ },
+ "consolidateResults": {
+ "errors": {
+ "generic": "An error occurred while consolidating task results: {{error}}"
+ }
+ },
+ "cancelTask": {
+ "errors": {
+ "generic": "An error occurred while cancelling the task: {{error}}"
+ }
+ },
+ "releaseTasks": {
+ "errors": {
+ "generic": "An error occurred while releasing tasks: {{error}}"
+ }
+ },
+ "resumeParentTask": {
+ "errors": {
+ "generic": "An error occurred while resuming the parent task: {{error}}"
+ }
+ },
+ "startConversation": {
+ "errors": {
+ "paramParseError": "Error parsing '{{param}}' parameter for start_conversation: {{error}}",
+ "generic": "An error occurred during the conversation: {{error}}"
+ }
}
}
diff --git a/src/package.json b/src/package.json
index bb4a784173..de311b5574 100644
--- a/src/package.json
+++ b/src/package.json
@@ -344,6 +344,18 @@
"type": "boolean",
"default": false,
"description": "%settings.rooCodeCloudEnabled.description%"
+ },
+ "roo-cline.taskCompletionMediatorModeEnabled": {
+ "type": "boolean",
+ "default": false,
+ "scope": "resource",
+ "description": "%settings.taskCompletionMediatorModeEnabled.description%"
+ },
+ "roo-cline.taskCompletionMediatorAgentMode": {
+ "type": "string",
+ "default": "mediator-agent",
+ "scope": "resource",
+ "description": "%settings.taskCompletionMediatorAgentMode.description%"
}
}
}
diff --git a/src/shared/tools.ts b/src/shared/tools.ts
index 0725e2e4d6..65270634d4 100644
--- a/src/shared/tools.ts
+++ b/src/shared/tools.ts
@@ -157,6 +157,41 @@ export interface NewTaskToolUse extends ToolUse {
params: Partial, "mode" | "message">>
}
+export interface DispatchTaskToolUse extends ToolUse { // Added DispatchTaskToolUse
+ name: "dispatch_task"
+ params: Partial, "mode" | "message">>
+}
+
+export interface GetTaskStatusToolUse extends ToolUse { // Added GetTaskStatusToolUse
+ name: "get_task_status"
+ params: Partial, "task_instance_ids">>
+}
+
+export interface ConsolidateResultsToolUse extends ToolUse { // Added ConsolidateResultsToolUse
+ name: "consolidate_results"
+ params: Partial, "task_instance_ids">>
+}
+
+export interface CancelTaskToolUse extends ToolUse { // Added CancelTaskToolUse
+ name: "cancel_task"
+ params: Partial, "task_instance_id">>
+}
+
+export interface ReleaseTasksToolUse extends ToolUse { // Added ReleaseTasksToolUse
+ name: "release_tasks"
+ params: Partial, "task_instance_ids">>
+}
+
+export interface ResumeParentTaskToolUse extends ToolUse { // Added ResumeParentTaskToolUse
+ name: "resume_parent_task"
+ params: Partial, "original_parent_id" | "mediated_result">>
+}
+
+export interface StartConversationToolUse extends ToolUse { // Added StartConversationToolUse
+ name: "start_conversation"
+ params: Partial, "participants" | "shared_context" | "initial_prompt" | "termination_condition">>
+}
+
export interface SearchAndReplaceToolUse extends ToolUse {
name: "search_and_replace"
params: Required, "path" | "search" | "replace">> &
@@ -185,6 +220,13 @@ export const TOOL_DISPLAY_NAMES: Record = {
attempt_completion: "complete tasks",
switch_mode: "switch modes",
new_task: "create new task",
+ dispatch_task: "dispatch new task",
+ get_task_status: "get task status",
+ consolidate_results: "consolidate task results",
+ cancel_task: "cancel task",
+ release_tasks: "release task records",
+ resume_parent_task: "resume parent task",
+ start_conversation: "start conversation", // Added start_conversation
insert_content: "insert content",
search_and_replace: "search and replace",
codebase_search: "codebase search",
@@ -226,6 +268,13 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"attempt_completion",
"switch_mode",
"new_task",
+ "dispatch_task",
+ "get_task_status",
+ "consolidate_results",
+ "cancel_task",
+ "release_tasks",
+ "resume_parent_task",
+ "start_conversation", // Added start_conversation
] as const
export type DiffResult =