Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/types/src/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
* ExperimentId
*/

export const experimentIds = ["preventFocusDisruption", "imageGeneration", "runSlashCommand", "customTools"] as const
export const experimentIds = [
"preventFocusDisruption",
"imageGeneration",
"runSlashCommand",
"customTools",
"subagent",
] as const

export const experimentIdsSchema = z.enum(experimentIds)

Expand All @@ -21,6 +27,7 @@ export const experimentsSchema = z.object({
imageGeneration: z.boolean().optional(),
runSlashCommand: z.boolean().optional(),
customTools: z.boolean().optional(),
subagent: z.boolean().optional(),
})

export type Experiments = z.infer<typeof experimentsSchema>
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export type ClineSay = z.infer<typeof clineSaySchema>
export const toolProgressStatusSchema = z.object({
icon: z.string().optional(),
text: z.string().optional(),
spin: z.boolean().optional(),
})

export type ToolProgressStatus = z.infer<typeof toolProgressStatusSchema>
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const toolNames = [
"skill",
"generate_image",
"custom_tool",
"subagent",
] as const

export const toolNamesSchema = z.enum(toolNames)
Expand Down
9 changes: 9 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,8 @@ export interface ClineSayTool {
| "runSlashCommand"
| "updateTodoList"
| "skill"
| "subagentRunning"
| "subagentCompleted"
path?: string
// For readCommandOutput
readStart?: number
Expand Down Expand Up @@ -837,6 +839,13 @@ export interface ClineSayTool {
description?: string
// Properties for skill tool
skill?: string
// Properties for subagent tool (subagentRunning / subagentCompleted)
currentTask?: string
result?: string
error?: string
/** When set (e.g. CANCELLED), webview shows t(messageKey) instead of result/error. */
resultCode?: string
messageKey?: string
}

export interface ClineAskUseMcpServer {
Expand Down
29 changes: 29 additions & 0 deletions src/core/assistant-message/NativeToolCallParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,20 @@ export class NativeToolCallParser {
}
break

case "subagent":
if (
partialArgs.description !== undefined ||
partialArgs.prompt !== undefined ||
partialArgs.subagent_type !== undefined
) {
nativeArgs = {
description: partialArgs.description,
prompt: partialArgs.prompt,
subagent_type: partialArgs.subagent_type,
}
}
break

default:
break
}
Expand Down Expand Up @@ -911,6 +925,21 @@ export class NativeToolCallParser {
}
break

case "subagent":
if (
args.description !== undefined &&
args.prompt !== undefined &&
args.subagent_type !== undefined &&
(args.subagent_type === "general" || args.subagent_type === "explore")
) {
nativeArgs = {
description: args.description,
prompt: args.prompt,
subagent_type: args.subagent_type,
} as NativeArgsFor<TName>
}
break

case "use_mcp_tool":
if (args.server_name !== undefined && args.tool_name !== undefined) {
nativeArgs = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,56 @@ describe("NativeToolCallParser", () => {
})
})
})

describe("subagent tool", () => {
it("should parse description, prompt, and subagent_type into nativeArgs", () => {
const toolCall = {
id: "toolu_subagent_1",
name: "subagent" as const,
arguments: JSON.stringify({
description: "Explore codebase",
prompt: "List all exports from src/index.ts",
subagent_type: "explore",
}),
}

const result = NativeToolCallParser.parseToolCall(toolCall)

expect(result).not.toBeNull()
expect(result?.type).toBe("tool_use")
if (result?.type === "tool_use") {
expect(result.nativeArgs).toBeDefined()
const nativeArgs = result.nativeArgs as {
description: string
prompt: string
subagent_type: "general" | "explore"
}
expect(nativeArgs.description).toBe("Explore codebase")
expect(nativeArgs.prompt).toBe("List all exports from src/index.ts")
expect(nativeArgs.subagent_type).toBe("explore")
}
})

it("should parse general subagent_type", () => {
const toolCall = {
id: "toolu_subagent_2",
name: "subagent" as const,
arguments: JSON.stringify({
description: "Fix bug",
prompt: "Fix the null check in utils.ts",
subagent_type: "general",
}),
}

const result = NativeToolCallParser.parseToolCall(toolCall)

expect(result).not.toBeNull()
if (result?.type === "tool_use" && result.nativeArgs) {
const nativeArgs = result.nativeArgs as { subagent_type: string }
expect(nativeArgs.subagent_type).toBe("general")
}
})
})
})

describe("processStreamingChunk", () => {
Expand Down
14 changes: 14 additions & 0 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { customToolRegistry } from "@roo-code/core"

import { t } from "../../i18n"

import { SUBAGENT_STATUS_THINKING } from "../../shared/subagent"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"

Expand All @@ -30,6 +31,7 @@ import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
import { switchModeTool } from "../tools/SwitchModeTool"
import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
import { newTaskTool } from "../tools/NewTaskTool"
import { subagentTool } from "../tools/SubagentTool"
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
import { skillTool } from "../tools/SkillTool"
Expand Down Expand Up @@ -292,6 +294,7 @@ export async function presentAssistantMessage(cline: Task) {
content = content.replace(/\s?<\/thinking>/g, "")
}

cline.subagentProgressCallback?.(SUBAGENT_STATUS_THINKING)
await cline.say("text", content, undefined, block.partial)
break
}
Expand Down Expand Up @@ -383,6 +386,8 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
case "generate_image":
return `[${block.name} for '${block.params.path}']`
case "subagent":
return `[${block.name}: ${block.params.description ?? "(no description)"}]`
default:
return `[${block.name}]`
}
Expand Down Expand Up @@ -675,6 +680,7 @@ export async function presentAssistantMessage(cline: Task) {
}
}

cline.subagentProgressCallback?.(toolDescription())
switch (block.name) {
case "write_to_file":
await checkpointSaveAndMark(cline)
Expand Down Expand Up @@ -812,6 +818,14 @@ export async function presentAssistantMessage(cline: Task) {
toolCallId: block.id,
})
break
case "subagent":
await subagentTool.handle(cline, block as ToolUse<"subagent">, {
askApproval,
handleError,
pushToolResult,
toolCallId: block.id,
})
break
case "attempt_completion": {
const completionCallbacks: AttemptCompletionCallbacks = {
askApproval,
Expand Down
52 changes: 52 additions & 0 deletions src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,55 @@ describe("filterNativeToolsForMode - disabledTools", () => {
expect(resultNames).not.toContain("edit")
})
})

describe("filterNativeToolsForMode - subagent experiment", () => {
const codeMode = {
slug: "code",
name: "Code",
roleDefinition: "Test",
groups: ["read", "edit", "command", "mcp"] as ("read" | "edit" | "command" | "mcp")[],
}

const mockSubagentTool: OpenAI.Chat.ChatCompletionTool = {
type: "function",
function: {
name: "subagent",
description: "Run subagent",
parameters: {},
},
}

const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
makeTool("read_file"),
makeTool("write_to_file"),
mockSubagentTool,
]

it("should exclude subagent when experiment is not enabled", () => {
const filtered = filterNativeToolsForMode(
mockNativeTools,
"code",
[codeMode],
{ subagent: false },
undefined,
{},
undefined,
)
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
expect(toolNames).not.toContain("subagent")
})

it("should include subagent when experiment is enabled", () => {
const filtered = filterNativeToolsForMode(
mockNativeTools,
"code",
[codeMode],
{ subagent: true },
undefined,
{},
undefined,
)
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
expect(toolNames).toContain("subagent")
})
})
7 changes: 7 additions & 0 deletions src/core/prompts/tools/filter-tools-for-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ export function filterNativeToolsForMode(
allowedToolNames.delete("run_slash_command")
}

if (!experiments?.subagent) {
allowedToolNames.delete("subagent")
}

// Remove tools that are explicitly disabled via the disabledTools setting
if (settings?.disabledTools?.length) {
for (const toolName of settings.disabledTools) {
Expand Down Expand Up @@ -379,6 +383,9 @@ export function isToolAllowedInMode(
if (toolName === "run_slash_command") {
return experiments?.runSlashCommand === true
}
if (toolName === "subagent") {
return experiments?.subagent === true
}
return true
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/prompts/tools/native-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import newTask from "./new_task"
import readCommandOutput from "./read_command_output"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command"
import subagent from "./subagent"
import skill from "./skill"
import searchReplace from "./search_replace"
import edit_file from "./edit_file"
Expand Down Expand Up @@ -60,6 +61,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
readCommandOutput,
createReadFileTool(readFileOptions),
runSlashCommand,
subagent,
skill,
searchReplace,
edit_file,
Expand Down
36 changes: 36 additions & 0 deletions src/core/prompts/tools/native-tools/subagent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type OpenAI from "openai"

const SUBAGENT_DESCRIPTION = `Run a subagent in the background to do a focused sub-task. When the subagent finishes, its result or summary is returned as this tool's result and you can use it in your next step. Use when: (1) you need to offload research, exploration, or a multi-step sub-task without switching the user's view, or (2) you want read-only exploration (subagent_type "explore") with no file edits or commands. Do not use for creating a new user-visible task—use new_task instead.`

const DESCRIPTION_PARAM = `Short label for this subagent, shown in the chat (e.g. "List exports in src", "Check README")`
const PROMPT_PARAM = `Full instructions for the subagent. Its result or summary will be returned as this tool's result.`
const SUBAGENT_TYPE_PARAM = `"explore": read-only—subagent can only use read/search/list tools (no file edits or commands). Use for research or gathering information. "general": full tools—subagent can read, edit, and run commands. Use when the sub-task may need to change files or run commands.`

export default {
type: "function",
function: {
name: "subagent",
description: SUBAGENT_DESCRIPTION,
strict: true,
parameters: {
type: "object",
properties: {
description: {
type: "string",
description: DESCRIPTION_PARAM,
},
prompt: {
type: "string",
description: PROMPT_PARAM,
},
subagent_type: {
type: "string",
description: SUBAGENT_TYPE_PARAM,
enum: ["general", "explore"],
},
},
required: ["description", "prompt", "subagent_type"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
Loading
Loading