feat: implement sequential fan-out / fan-in for orchestrator (Phase 2 of #12330)

Adds subtask queue support to the new_task tool, allowing the orchestrator
to define multiple subtasks that execute automatically in sequence without
returning to the parent between each one. This saves LLM API calls and
enables more efficient multi-agent workflows.

Key changes:
- SubtaskQueueItem, SubtaskResult types in packages/types/src/history.ts
- task_queue parameter on new_task tool (optional JSON array)
- NewTaskTool parses and validates queued subtasks, stores on parent
- delegateParentAndOpenChild persists queue in parent HistoryItem
- reopenParentFromDelegation auto-advances queue via advanceSubtaskQueue
- formatAggregatedQueueResults aggregates all results when queue completes
- 9 new tests covering queue advance, exhaustion, and result formatting
- All 56 existing delegation tests continue to pass
This commit is contained in:
Roo Code 2026-05-12 03:28:52 +00:00
parent 6139871b94
commit 4b9c7ac155
7 changed files with 498 additions and 11 deletions

View file

@ -1,5 +1,27 @@
import { z } from "zod"
/**
* SubtaskQueueItem a single queued subtask definition for sequential fan-out.
* Used by the orchestrator to define a pipeline of subtasks that execute one after another.
*/
export const subtaskQueueItemSchema = z.object({
mode: z.string(),
message: z.string(),
})
export type SubtaskQueueItem = z.infer<typeof subtaskQueueItemSchema>
/**
* SubtaskResult the result of a completed subtask in a queue.
*/
export const subtaskResultSchema = z.object({
taskId: z.string(),
mode: z.string(),
summary: z.string(),
})
export type SubtaskResult = z.infer<typeof subtaskResultSchema>
/**
* HistoryItem
*/
@ -26,6 +48,10 @@ export const historyItemSchema = z.object({
awaitingChildId: z.string().optional(), // Child currently awaited (set when delegated)
completedByChildId: z.string().optional(), // Child that completed and resumed this parent
completionResultSummary: z.string().optional(), // Summary from completed child
// Sequential fan-out queue (Phase 2)
subtaskQueue: z.array(subtaskQueueItemSchema).optional(), // Remaining subtasks to execute
subtaskQueueIndex: z.number().optional(), // Current position in the original queue (0-based)
subtaskResults: z.array(subtaskResultSchema).optional(), // Results from completed queue subtasks
})
export type HistoryItem = z.infer<typeof historyItemSchema>

View file

@ -0,0 +1,242 @@
/**
* Tests for Phase 2: Sequential fan-out / fan-in.
*
* Tests the subtask queue mechanism where an orchestrator can define
* multiple subtasks that execute one after another with automatic transitions.
*/
import { describe, it, expect, vi } from "vitest"
import { RooCodeEventName } from "@roo-code/types"
import type { HistoryItem, SubtaskQueueItem } from "@roo-code/types"
import { ClineProvider } from "../core/webview/ClineProvider"
describe("Sequential fan-out queue types", () => {
it("SubtaskQueueItem has required mode and message fields", () => {
const item: SubtaskQueueItem = { mode: "code", message: "Implement feature X" }
expect(item.mode).toBe("code")
expect(item.message).toBe("Implement feature X")
})
it("HistoryItem can include subtask queue fields", () => {
const historyItem: Partial<HistoryItem> = {
id: "test-1",
subtaskQueue: [
{ mode: "code", message: "Step 1" },
{ mode: "debug", message: "Step 2" },
],
subtaskQueueIndex: 0,
subtaskResults: [{ taskId: "child-1", mode: "code", summary: "Done" }],
}
expect(historyItem.subtaskQueue).toHaveLength(2)
expect(historyItem.subtaskQueueIndex).toBe(0)
expect(historyItem.subtaskResults).toHaveLength(1)
})
it("HistoryItem subtask queue fields are optional", () => {
const historyItem: Partial<HistoryItem> = {
id: "test-2",
status: "active",
}
expect(historyItem.subtaskQueue).toBeUndefined()
expect(historyItem.subtaskQueueIndex).toBeUndefined()
expect(historyItem.subtaskResults).toBeUndefined()
})
})
describe("advanceSubtaskQueue", () => {
const makeHistoryItem = (overrides: Partial<HistoryItem> = {}): HistoryItem => ({
id: "parent-1",
number: 1,
ts: Date.now(),
task: "Test task",
tokensIn: 0,
tokensOut: 0,
totalCost: 0,
status: "delegated",
...overrides,
})
it("returns handled=true when there are more subtasks in the queue", async () => {
const emitSpy = vi.fn()
const mockChild = { taskId: "child-2", start: vi.fn() }
const provider = {
getCurrentTask: vi.fn().mockReturnValue({ taskId: "child-1" }),
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
getTaskWithId: vi.fn().mockResolvedValue({
historyItem: makeHistoryItem({ id: "child-1", status: "active" }),
}),
updateTaskHistory: vi.fn().mockResolvedValue(undefined),
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
createTask: vi.fn().mockResolvedValue(mockChild),
emit: emitSpy,
log: vi.fn(),
}
const subtaskQueue: SubtaskQueueItem[] = [
{ mode: "code", message: "Step 1" },
{ mode: "debug", message: "Step 2" },
]
const historyItem = makeHistoryItem({
subtaskQueue,
subtaskQueueIndex: 0,
subtaskResults: [],
childIds: ["child-1"],
})
const result = await (ClineProvider.prototype as any).advanceSubtaskQueue.call(provider, {
parentTaskId: "parent-1",
childTaskId: "child-1",
completionResultSummary: "Step 1 done",
historyItem,
})
expect(result.handled).toBe(true)
// Should have closed the current child
expect(provider.removeClineFromStack).toHaveBeenCalled()
// Should have marked child as completed
expect(provider.updateTaskHistory).toHaveBeenCalledWith(
expect.objectContaining({ id: "child-1", status: "completed" }),
)
// Should have switched mode to next subtask's mode
expect(provider.handleModeSwitch).toHaveBeenCalledWith("debug")
// Should have created the next child with the queued message
expect(provider.createTask).toHaveBeenCalledWith("Step 2", undefined, undefined, {
initialTodos: [],
initialStatus: "active",
startTask: false,
})
// Should have started the next child
expect(mockChild.start).toHaveBeenCalled()
// Should have updated parent with advanced queue index
expect(provider.updateTaskHistory).toHaveBeenCalledWith(
expect.objectContaining({
id: "parent-1",
subtaskQueueIndex: 1,
subtaskResults: [{ taskId: "child-1", mode: "unknown", summary: "Step 1 done" }],
awaitingChildId: "child-2",
delegatedToId: "child-2",
}),
)
// Should have emitted delegation events
expect(emitSpy).toHaveBeenCalledWith(
RooCodeEventName.TaskDelegationCompleted,
"parent-1",
"child-1",
"Step 1 done",
)
expect(emitSpy).toHaveBeenCalledWith(RooCodeEventName.TaskDelegated, "parent-1", "child-2")
})
it("returns handled=false with aggregated summary when queue is exhausted", async () => {
const provider = {
getCurrentTask: vi.fn().mockReturnValue({ taskId: "child-2" }),
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
getTaskWithId: vi.fn().mockResolvedValue({
historyItem: makeHistoryItem({ id: "child-2", status: "active" }),
}),
updateTaskHistory: vi.fn().mockResolvedValue(undefined),
handleModeSwitch: vi.fn(),
createTask: vi.fn(),
emit: vi.fn(),
log: vi.fn(),
formatAggregatedQueueResults: (ClineProvider.prototype as any).formatAggregatedQueueResults,
}
const subtaskQueue: SubtaskQueueItem[] = [{ mode: "code", message: "Step 1" }]
const historyItem = makeHistoryItem({
subtaskQueue,
subtaskQueueIndex: 0,
subtaskResults: [{ taskId: "child-1", mode: "code", summary: "Step 1 done" }],
childIds: ["child-1", "child-2"],
})
const result = await (ClineProvider.prototype as any).advanceSubtaskQueue.call(provider, {
parentTaskId: "parent-1",
childTaskId: "child-2",
completionResultSummary: "Step 2 done",
historyItem,
})
expect(result.handled).toBe(false)
expect(result.aggregatedSummary).toContain("Sequential Fan-Out Complete")
expect(result.aggregatedSummary).toContain("Step 1 done")
expect(result.aggregatedSummary).toContain("Step 2 done")
// Should NOT have created a new child
expect(provider.createTask).not.toHaveBeenCalled()
// Should have cleared queue from parent metadata
expect(provider.updateTaskHistory).toHaveBeenCalledWith(
expect.objectContaining({
subtaskQueue: undefined,
subtaskQueueIndex: undefined,
}),
)
})
it("returns handled=false immediately when queue is empty", async () => {
const provider = {
getCurrentTask: vi.fn(),
removeClineFromStack: vi.fn(),
getTaskWithId: vi.fn(),
updateTaskHistory: vi.fn(),
emit: vi.fn(),
log: vi.fn(),
formatAggregatedQueueResults: (ClineProvider.prototype as any).formatAggregatedQueueResults,
}
const historyItem = makeHistoryItem({
subtaskQueue: [],
subtaskQueueIndex: 0,
})
const result = await (ClineProvider.prototype as any).advanceSubtaskQueue.call(provider, {
parentTaskId: "parent-1",
childTaskId: "child-1",
completionResultSummary: "Done",
historyItem,
})
expect(result.handled).toBe(false)
expect(result.aggregatedSummary).toBe("Done")
})
})
describe("formatAggregatedQueueResults", () => {
it("formats multiple results into a structured summary", () => {
const results = [
{ taskId: "child-1", mode: "code", summary: "Implemented feature X" },
{ taskId: "child-2", mode: "debug", summary: "Fixed bugs in feature X" },
]
const formatted = (ClineProvider.prototype as any).formatAggregatedQueueResults(results, "Final result")
expect(formatted).toContain("Sequential Fan-Out Complete (2 subtasks)")
expect(formatted).toContain("Subtask 1 (code)")
expect(formatted).toContain("Implemented feature X")
expect(formatted).toContain("Subtask 2 (debug)")
expect(formatted).toContain("Fixed bugs in feature X")
})
it("returns last summary when results array is empty", () => {
const formatted = (ClineProvider.prototype as any).formatAggregatedQueueResults([], "Just a summary")
expect(formatted).toBe("Just a summary")
})
it("handles single result", () => {
const results = [{ taskId: "child-1", mode: "code", summary: "Done" }]
const formatted = (ClineProvider.prototype as any).formatAggregatedQueueResults(results, "Done")
expect(formatted).toContain("Sequential Fan-Out Complete (1 subtask)")
expect(formatted).toContain("Subtask 1 (code)")
})
})

View file

@ -633,6 +633,7 @@ export class NativeToolCallParser {
mode: partialArgs.mode,
message: partialArgs.message,
todos: partialArgs.todos,
task_queue: partialArgs.task_queue,
}
}
break
@ -982,6 +983,7 @@ export class NativeToolCallParser {
mode: args.mode,
message: args.message,
todos: args.todos,
task_queue: args.task_queue,
} as NativeArgsFor<TName>
}
break

View file

@ -2,7 +2,9 @@ import type OpenAI from "openai"
const NEW_TASK_DESCRIPTION = `Create a new task instance in the chosen mode using your provided message and initial todo list (if required).
CRITICAL: This tool MUST be called alone. Do NOT call this tool alongside other tools in the same message turn. If you need to gather information before delegating, use other tools in a separate turn first, then call new_task by itself in the next turn.`
CRITICAL: This tool MUST be called alone. Do NOT call this tool alongside other tools in the same message turn. If you need to gather information before delegating, use other tools in a separate turn first, then call new_task by itself in the next turn.
SEQUENTIAL FAN-OUT: You can optionally provide a task_queue parameter to define additional subtasks that will execute automatically in sequence after the first subtask completes. Each queued subtask runs one after another without returning to the parent in between, saving time and API calls. Use this when you have planned multiple independent subtasks upfront. The first subtask is defined by the mode and message parameters; subsequent subtasks are defined in the task_queue array.`
const MODE_PARAMETER_DESCRIPTION = `Slug of the mode to begin the new task in (e.g., code, debug, architect)`
@ -10,6 +12,8 @@ const MESSAGE_PARAMETER_DESCRIPTION = `Initial user instructions or context for
const TODOS_PARAMETER_DESCRIPTION = `Optional initial todo list written as a markdown checklist; required when the workspace mandates todos`
const TASK_QUEUE_PARAMETER_DESCRIPTION = `Optional JSON array of additional subtasks to execute sequentially after the first subtask completes. Each element is an object with "mode" (string) and "message" (string). Example: [{"mode":"code","message":"Implement feature X"},{"mode":"debug","message":"Test feature X"}]. When provided, the system automatically transitions between subtasks without returning to the parent, collecting all results. The parent receives aggregated results when the entire queue completes.`
export default {
type: "function",
function: {
@ -31,6 +35,10 @@ export default {
type: ["string", "null"],
description: TODOS_PARAMETER_DESCRIPTION,
},
task_queue: {
type: ["string", "null"],
description: TASK_QUEUE_PARAMETER_DESCRIPTION,
},
},
required: ["mode", "message", "todos"],
additionalProperties: false,

View file

@ -1,6 +1,7 @@
import * as vscode from "vscode"
import { TodoItem } from "@roo-code/types"
import type { SubtaskQueueItem } from "@roo-code/types"
import { Task } from "../task/Task"
import { getModeBySlug } from "../../shared/modes"
@ -15,13 +16,14 @@ interface NewTaskParams {
mode: string
message: string
todos?: string
task_queue?: string
}
export class NewTaskTool extends BaseTool<"new_task"> {
readonly name = "new_task" as const
async execute(params: NewTaskParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
const { mode, message, todos } = params
const { mode, message, todos, task_queue } = params
const { askApproval, handleError, pushToolResult } = callbacks
try {
@ -96,11 +98,47 @@ export class NewTaskTool extends BaseTool<"new_task"> {
return
}
// Parse task_queue if provided (sequential fan-out)
let queueItems: SubtaskQueueItem[] = []
if (task_queue) {
try {
const parsed = JSON.parse(task_queue)
if (Array.isArray(parsed)) {
for (const item of parsed) {
if (typeof item.mode === "string" && typeof item.message === "string") {
// Validate each queued mode exists
const queuedMode = getModeBySlug(item.mode, state?.customModes)
if (!queuedMode) {
pushToolResult(
formatResponse.toolError(
`Invalid mode in task_queue: "${item.mode}". All queued subtasks must use valid modes.`,
),
)
return
}
queueItems.push({ mode: item.mode, message: item.message })
}
}
}
} catch {
task.consecutiveMistakeCount++
task.recordToolError("new_task")
task.didToolFailInCurrentTurn = true
pushToolResult(
formatResponse.toolError(
"Invalid task_queue format: must be a JSON array of objects with 'mode' and 'message' properties.",
),
)
return
}
}
const toolMessage = JSON.stringify({
tool: "newTask",
mode: targetMode.name,
content: message,
todos: todoItems,
taskQueue: queueItems.length > 0 ? queueItems : undefined,
})
const didApprove = await askApproval("tool", toolMessage)
@ -115,10 +153,15 @@ export class NewTaskTool extends BaseTool<"new_task"> {
message: unescapedMessage,
initialTodos: todoItems,
mode,
subtaskQueue: queueItems.length > 0 ? queueItems : undefined,
})
// Reflect delegation in tool result (no pause/unpause, no wait)
pushToolResult(`Delegated to child task ${child.taskId}`)
const queueMsg =
queueItems.length > 0
? ` (${queueItems.length} additional subtask${queueItems.length > 1 ? "s" : ""} queued)`
: ""
pushToolResult(`Delegated to child task ${child.taskId}${queueMsg}`)
return
} catch (error) {
await handleError("creating new task", error)
@ -130,12 +173,14 @@ export class NewTaskTool extends BaseTool<"new_task"> {
const mode: string | undefined = block.params.mode
const message: string | undefined = block.params.message
const todos: string | undefined = block.params.todos
const taskQueue: string | undefined = block.params.task_queue
const partialMessage = JSON.stringify({
tool: "newTask",
mode: mode ?? "",
content: message ?? "",
todos: todos,
taskQueue: taskQueue,
})
await task.ask("tool", partialMessage, block.partial).catch(() => {})

View file

@ -85,7 +85,7 @@ import { CustomModesManager } from "../config/CustomModesManager"
import { Task } from "../task/Task"
import { webviewMessageHandler } from "./webviewMessageHandler"
import type { ClineMessage, TodoItem } from "@roo-code/types"
import type { ClineMessage, TodoItem, SubtaskQueueItem } from "@roo-code/types"
import { readApiMessages, saveApiMessages, saveTaskMessages, TaskHistoryStore } from "../task-persistence"
import { readTaskMessages } from "../task-persistence/taskMessages"
import { getNonce } from "./getNonce"
@ -3017,8 +3017,9 @@ export class ClineProvider
message: string
initialTodos: TodoItem[]
mode: string
subtaskQueue?: SubtaskQueueItem[]
}): Promise<Task> {
const { parentTaskId, message, initialTodos, mode } = params
const { parentTaskId, message, initialTodos, mode, subtaskQueue } = params
// Metadata-driven delegation is always enabled
@ -3121,6 +3122,9 @@ export class ClineProvider
delegatedToId: child.taskId,
awaitingChildId: child.taskId,
childIds,
...(subtaskQueue && subtaskQueue.length > 0
? { subtaskQueue, subtaskQueueIndex: 0, subtaskResults: [] }
: {}),
}
await this.updateTaskHistory(updatedHistory)
} catch (err) {
@ -3152,12 +3156,28 @@ export class ClineProvider
childTaskId: string
completionResultSummary: string
}): Promise<void> {
const { parentTaskId, childTaskId, completionResultSummary } = params
const { parentTaskId, childTaskId } = params
let effectiveSummary = params.completionResultSummary
const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
// 1) Load parent from history and current persisted messages
const { historyItem } = await this.getTaskWithId(parentTaskId)
// PHASE 2: Sequential fan-out — check if parent has queued subtasks
if (historyItem.subtaskQueue && historyItem.subtaskQueue.length > 0) {
const queueAdvanceResult = await this.advanceSubtaskQueue({
parentTaskId,
childTaskId,
completionResultSummary: effectiveSummary,
historyItem,
})
if (queueAdvanceResult.handled) {
return // Queue advanced to next subtask; do NOT reopen parent
}
// Queue exhausted — use aggregated summary and continue with normal reopen
effectiveSummary = queueAdvanceResult.aggregatedSummary
}
let parentClineMessages: ClineMessage[] = []
try {
parentClineMessages = await readTaskMessages({
@ -3203,7 +3223,7 @@ export class ClineProvider
const subtaskUiMessage: ClineMessage = {
type: "say",
say: "subtask_result",
text: completionResultSummary,
text: effectiveSummary,
ts,
}
parentClineMessages.push(subtaskUiMessage)
@ -3316,7 +3336,7 @@ export class ClineProvider
...historyItem,
status: "active",
completedByChildId: childTaskId,
completionResultSummary,
completionResultSummary: effectiveSummary,
awaitingChildId: undefined,
childIds,
}
@ -3324,7 +3344,7 @@ export class ClineProvider
// 6) Emit TaskDelegationCompleted (provider-level)
try {
this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, completionResultSummary)
this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, effectiveSummary)
} catch {
// non-fatal
}
@ -3358,6 +3378,149 @@ export class ClineProvider
}
}
/**
* Advance the sequential fan-out subtask queue.
* Called when a child completes and the parent has a subtaskQueue.
*
* Returns { handled: true } if the next subtask was started (caller should return).
* Returns { handled: false, aggregatedSummary } if queue is exhausted (caller should continue with normal reopen).
*/
private async advanceSubtaskQueue(params: {
parentTaskId: string
childTaskId: string
completionResultSummary: string
historyItem: HistoryItem
}): Promise<{ handled: true } | { handled: false; aggregatedSummary: string }> {
const { parentTaskId, childTaskId, completionResultSummary, historyItem } = params
const { subtaskQueue, subtaskQueueIndex, subtaskResults } = historyItem
if (!subtaskQueue || subtaskQueue.length === 0) {
return { handled: false, aggregatedSummary: completionResultSummary }
}
const currentIndex = subtaskQueueIndex ?? 0
const nextIndex = currentIndex + 1
// Record this child's result
const completedMode = historyItem.mode ?? "unknown"
const updatedResults = [
...(subtaskResults ?? []),
{ taskId: childTaskId, mode: completedMode, summary: completionResultSummary },
]
// Close current child if still open
const current = this.getCurrentTask()
if (current?.taskId === childTaskId) {
await this.removeClineFromStack()
}
// Mark child as completed
try {
const { historyItem: childHistory } = await this.getTaskWithId(childTaskId)
await this.updateTaskHistory({ ...childHistory, status: "completed" })
} catch (err) {
this.log(
`[advanceSubtaskQueue] Failed to persist child completed status for ${childTaskId}: ${
(err as Error)?.message ?? String(err)
}`,
)
}
// Emit completion event for the finished child
try {
this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, completionResultSummary)
} catch {
// non-fatal
}
if (nextIndex <= subtaskQueue.length - 1) {
// More subtasks in queue — start the next one
const nextSubtask = subtaskQueue[nextIndex]
this.log(
`[advanceSubtaskQueue] Auto-advancing queue: subtask ${nextIndex + 1}/${subtaskQueue.length} (mode: ${nextSubtask.mode})`,
)
// Switch mode
try {
await this.handleModeSwitch(nextSubtask.mode as any)
} catch (e) {
this.log(
`[advanceSubtaskQueue] handleModeSwitch failed for queued mode '${nextSubtask.mode}': ${
(e as Error)?.message ?? String(e)
}`,
)
}
// Create next child
const nextChild = await this.createTask(nextSubtask.message, undefined, undefined, {
initialTodos: [],
initialStatus: "active",
startTask: false,
})
// Update parent metadata
const childIds = Array.from(new Set([...(historyItem.childIds ?? []), childTaskId, nextChild.taskId]))
await this.updateTaskHistory({
...historyItem,
status: "delegated",
delegatedToId: nextChild.taskId,
awaitingChildId: nextChild.taskId,
childIds,
subtaskQueue,
subtaskQueueIndex: nextIndex,
subtaskResults: updatedResults,
})
// Start the child
nextChild.start()
try {
this.emit(RooCodeEventName.TaskDelegated, parentTaskId, nextChild.taskId)
} catch {
// non-fatal
}
return { handled: true }
}
// Queue exhausted — aggregate results and let normal reopen proceed
const aggregatedSummary = this.formatAggregatedQueueResults(updatedResults, completionResultSummary)
// Clear queue from parent metadata (will be fully updated by caller)
const childIds = Array.from(new Set([...(historyItem.childIds ?? []), childTaskId]))
await this.updateTaskHistory({
...historyItem,
subtaskQueue: undefined,
subtaskQueueIndex: undefined,
subtaskResults: updatedResults,
childIds,
})
return { handled: false, aggregatedSummary }
}
/**
* Format aggregated results from all completed subtasks in a queue.
*/
private formatAggregatedQueueResults(
results: Array<{ taskId: string; mode: string; summary: string }>,
lastSummary: string,
): string {
if (results.length === 0) {
return lastSummary
}
const lines = [`## Sequential Fan-Out Complete (${results.length} subtask${results.length > 1 ? "s" : ""})`, ""]
for (let i = 0; i < results.length; i++) {
const r = results[i]
lines.push(`### Subtask ${i + 1} (${r.mode}) — ${r.taskId}`)
lines.push(r.summary)
lines.push("")
}
return lines.join("\n")
}
/**
* Convert a file path to a webview-accessible URI
* This method safely converts file paths to URIs that can be loaded in the webview

View file

@ -56,6 +56,7 @@ export const toolParamNames = [
"start_line",
"end_line",
"todos",
"task_queue",
"prompt",
"image",
// read_file parameters (native protocol)
@ -102,7 +103,7 @@ export type NativeToolArgs = {
edit_file: { file_path: string; old_string: string; new_string: string; expected_replacements?: number }
apply_patch: { patch: string }
list_files: { path: string; recursive?: boolean }
new_task: { mode: string; message: string; todos?: string }
new_task: { mode: string; message: string; todos?: string; task_queue?: string }
ask_followup_question: {
question: string
follow_up: Array<{ text: string; mode?: string }>
@ -240,7 +241,7 @@ export interface SwitchModeToolUse extends ToolUse<"switch_mode"> {
export interface NewTaskToolUse extends ToolUse<"new_task"> {
name: "new_task"
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message" | "todos">>
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message" | "todos" | "task_queue">>
}
export interface RunSlashCommandToolUse extends ToolUse<"run_slash_command"> {