Skip to content
Draft
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
5 changes: 5 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {

export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3

export const DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS = 4
export const MIN_MAX_FOLLOW_UP_SUGGESTIONS = 1
export const MAX_MAX_FOLLOW_UP_SUGGESTIONS = 10

/**
* DynamicProvider
*
Expand Down Expand Up @@ -178,6 +182,7 @@ const baseProviderSettingsSchema = z.object({
modelTemperature: z.number().nullish(),
rateLimitSeconds: z.number().optional(),
consecutiveMistakeLimit: z.number().min(0).optional(),
maxFollowUpSuggestions: z.number().min(1).max(10).optional(),

// Model reasoning.
enableReasoningEffort: z.boolean().optional(),
Expand Down
5 changes: 4 additions & 1 deletion src/core/prompts/sections/rules.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS } from "@roo-code/types"

import type { SystemPromptSettings } from "../types"

import { getShell } from "../../../utils/shell"
Expand Down Expand Up @@ -66,6 +68,7 @@ export function getRulesSection(cwd: string, settings?: SystemPromptSettings): s
// Get shell-appropriate command chaining operator
const chainOp = getCommandChainOperator()
const chainNote = getCommandChainNote()
const maxSuggestions = settings?.maxFollowUpSuggestions ?? DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS

return `====

Expand All @@ -81,7 +84,7 @@ RULES
* For example, in architect mode trying to edit app.js would be rejected because architect mode can only edit files matching "\\.md$"
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. When you ask a question, provide the user with 2-4 suggested answers based on your question so they don't need to do so much typing. The suggestions should be specific, actionable, and directly related to the completed task. They should be ordered by priority or logical sequence. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. When you ask a question, provide the user with 2-${maxSuggestions} suggested answers based on your question so they don't need to do so much typing. The suggestions should be specific, actionable, and directly related to the completed task. They should be ordered by priority or logical sequence. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you.
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type OpenAI from "openai"

import { createAskFollowupQuestionTool } from "../ask_followup_question"
import { getNativeTools } from "../index"

type FunctionTool = OpenAI.Chat.ChatCompletionFunctionTool

describe("createAskFollowupQuestionTool", () => {
it("should use default maxItems of 4 when no argument provided", () => {
const tool = createAskFollowupQuestionTool() as FunctionTool
const params = tool.function.parameters as any
expect(params.properties.follow_up.maxItems).toBe(4)
expect(params.properties.follow_up.minItems).toBe(1)
expect(tool.function.description).toContain("2-4")
})

it("should use custom maxItems when provided", () => {
const tool = createAskFollowupQuestionTool(7) as FunctionTool
const params = tool.function.parameters as any
expect(params.properties.follow_up.maxItems).toBe(7)
expect(params.properties.follow_up.minItems).toBe(1)
expect(tool.function.description).toContain("2-7")
expect(tool.function.description).not.toContain("2-4")
})

it("should use maxItems of 10 when max value provided", () => {
const tool = createAskFollowupQuestionTool(10) as FunctionTool
const params = tool.function.parameters as any
expect(params.properties.follow_up.maxItems).toBe(10)
expect(tool.function.description).toContain("2-10")
})

it("should use maxItems of 1 when min value provided", () => {
const tool = createAskFollowupQuestionTool(1) as FunctionTool
const params = tool.function.parameters as any
expect(params.properties.follow_up.maxItems).toBe(1)
})

it("should have the correct tool name", () => {
const tool = createAskFollowupQuestionTool(5) as FunctionTool
expect(tool.function.name).toBe("ask_followup_question")
expect(tool.type).toBe("function")
})

it("should update the follow_up parameter description", () => {
const tool = createAskFollowupQuestionTool(8) as FunctionTool
const params = tool.function.parameters as any
expect(params.properties.follow_up.description).toContain("2-8")
})
})

describe("getNativeTools with maxFollowUpSuggestions", () => {
it("should use default maxItems when maxFollowUpSuggestions is not provided", () => {
const tools = getNativeTools()
const askTool = tools.find(
(t) => (t as FunctionTool).function?.name === "ask_followup_question",
) as FunctionTool
expect(askTool).toBeDefined()
const params = askTool.function.parameters as any
expect(params.properties.follow_up.maxItems).toBe(4)
})

it("should use custom maxItems when maxFollowUpSuggestions is provided", () => {
const tools = getNativeTools({ maxFollowUpSuggestions: 7 })
const askTool = tools.find(
(t) => (t as FunctionTool).function?.name === "ask_followup_question",
) as FunctionTool
expect(askTool).toBeDefined()
const params = askTool.function.parameters as any
expect(params.properties.follow_up.maxItems).toBe(7)
})
})
87 changes: 50 additions & 37 deletions src/core/prompts/tools/native-tools/ask_followup_question.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,75 @@
import type OpenAI from "openai"

const ASK_FOLLOWUP_QUESTION_DESCRIPTION = `Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
import { DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS } from "@roo-code/types"

function getAskFollowupQuestionDescription(maxSuggestions: number): string {
return `Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.

Parameters:
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers. Suggestions must be complete, actionable answers without placeholders. Optionally include mode to switch modes (code/architect/etc.)
- follow_up: (required) A list of 2-${maxSuggestions} suggested answers. Suggestions must be complete, actionable answers without placeholders. Optionally include mode to switch modes (code/architect/etc.)

Example: Asking for file path
{ "question": "What is the path to the frontend-config.json file?", "follow_up": [{ "text": "./src/frontend-config.json", "mode": null }, { "text": "./config/frontend-config.json", "mode": null }, { "text": "./frontend-config.json", "mode": null }] }

Example: Asking with mode switch
{ "question": "Would you like me to implement this feature?", "follow_up": [{ "text": "Yes, implement it now", "mode": "code" }, { "text": "No, just plan it out", "mode": "architect" }] }`
}

const QUESTION_PARAMETER_DESCRIPTION = `Clear, specific question that captures the missing information you need`

const FOLLOW_UP_PARAMETER_DESCRIPTION = `Required list of 2-4 suggested responses; each suggestion must be a complete, actionable answer and may include a mode switch`
function getFollowUpParameterDescription(maxSuggestions: number): string {
return `Required list of 2-${maxSuggestions} suggested responses; each suggestion must be a complete, actionable answer and may include a mode switch`
}

const FOLLOW_UP_TEXT_DESCRIPTION = `Suggested answer the user can pick`

const FOLLOW_UP_MODE_DESCRIPTION = `Optional mode slug to switch to if this suggestion is chosen (e.g., code, architect)`

export default {
type: "function",
function: {
name: "ask_followup_question",
description: ASK_FOLLOWUP_QUESTION_DESCRIPTION,
strict: true,
parameters: {
type: "object",
properties: {
question: {
type: "string",
description: QUESTION_PARAMETER_DESCRIPTION,
},
follow_up: {
type: "array",
description: FOLLOW_UP_PARAMETER_DESCRIPTION,
items: {
type: "object",
properties: {
text: {
type: "string",
description: FOLLOW_UP_TEXT_DESCRIPTION,
},
mode: {
type: ["string", "null"],
description: FOLLOW_UP_MODE_DESCRIPTION,
export function createAskFollowupQuestionTool(
maxSuggestions: number = DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS,
): OpenAI.Chat.ChatCompletionTool {
return {
type: "function",
function: {
name: "ask_followup_question",
description: getAskFollowupQuestionDescription(maxSuggestions),
strict: true,
parameters: {
type: "object",
properties: {
question: {
type: "string",
description: QUESTION_PARAMETER_DESCRIPTION,
},
follow_up: {
type: "array",
description: getFollowUpParameterDescription(maxSuggestions),
items: {
type: "object",
properties: {
text: {
type: "string",
description: FOLLOW_UP_TEXT_DESCRIPTION,
},
mode: {
type: ["string", "null"],
description: FOLLOW_UP_MODE_DESCRIPTION,
},
},
required: ["text", "mode"],
additionalProperties: false,
},
required: ["text", "mode"],
additionalProperties: false,
minItems: 1,
maxItems: maxSuggestions,
},
minItems: 1,
maxItems: 4,
},
required: ["question", "follow_up"],
additionalProperties: false,
},
required: ["question", "follow_up"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
} satisfies OpenAI.Chat.ChatCompletionTool
}

// Backward compatibility: default export with default maxItems
export default createAskFollowupQuestionTool()
8 changes: 5 additions & 3 deletions src/core/prompts/tools/native-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type OpenAI from "openai"
import accessMcpResource from "./access_mcp_resource"
import { apply_diff } from "./apply_diff"
import applyPatch from "./apply_patch"
import askFollowupQuestion from "./ask_followup_question"
import askFollowupQuestion, { createAskFollowupQuestionTool } from "./ask_followup_question"
import attemptCompletion from "./attempt_completion"
import codebaseSearch from "./codebase_search"
import editTool from "./edit"
Expand Down Expand Up @@ -31,6 +31,8 @@ export type { ReadFileToolOptions } from "./read_file"
export interface NativeToolsOptions {
/** Whether the model supports image processing (default: false) */
supportsImages?: boolean
/** Maximum number of follow-up suggestions allowed in ask_followup_question (default: 4) */
maxFollowUpSuggestions?: number
}

/**
Expand All @@ -40,7 +42,7 @@ export interface NativeToolsOptions {
* @returns Array of native tool definitions
*/
export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] {
const { supportsImages = false } = options
const { supportsImages = false, maxFollowUpSuggestions } = options

const readFileOptions: ReadFileToolOptions = {
supportsImages,
Expand All @@ -50,7 +52,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
accessMcpResource,
apply_diff,
applyPatch,
askFollowupQuestion,
maxFollowUpSuggestions ? createAskFollowupQuestionTool(maxFollowUpSuggestions) : askFollowupQuestion,
attemptCompletion,
codebaseSearch,
executeCommand,
Expand Down
2 changes: 2 additions & 0 deletions src/core/prompts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export interface SystemPromptSettings {
newTaskRequireTodos: boolean
/** When true, model should hide vendor/company identity in responses */
isStealthModel?: boolean
/** Maximum number of follow-up suggestions the model can provide (default: 4, range: 1-10) */
maxFollowUpSuggestions?: number
}
1 change: 1 addition & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3810,6 +3810,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
.getConfiguration(Package.name)
.get<boolean>("newTaskRequireTodos", false),
isStealthModel: modelInfo?.isStealthModel,
maxFollowUpSuggestions: apiConfiguration?.maxFollowUpSuggestions,
},
undefined, // todoList
this.api.getModel().id,
Expand Down
1 change: 1 addition & 0 deletions src/core/task/build-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
// Build native tools with dynamic read_file tool based on settings.
const nativeTools = getNativeTools({
supportsImages,
maxFollowUpSuggestions: apiConfiguration?.maxFollowUpSuggestions,
})

// Filter native tools based on mode restrictions.
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/generateSystemPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
.getConfiguration(Package.name)
.get<boolean>("newTaskRequireTodos", false),
isStealthModel: modelInfo?.isStealthModel,
maxFollowUpSuggestions: apiConfiguration?.maxFollowUpSuggestions,
},
undefined, // todoList
undefined, // modelId
Expand Down
10 changes: 10 additions & 0 deletions webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type ProviderSettings,
isRetiredProvider,
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS,
openRouterDefaultModelId,
requestyDefaultModelId,
litellmDefaultModelId,
Expand Down Expand Up @@ -104,6 +105,7 @@ import { TodoListSettingsControl } from "./TodoListSettingsControl"
import { TemperatureControl } from "./TemperatureControl"
import { RateLimitSecondsControl } from "./RateLimitSecondsControl"
import { ConsecutiveMistakeLimitControl } from "./ConsecutiveMistakeLimitControl"
import { MaxFollowUpSuggestionsControl } from "./MaxFollowUpSuggestionsControl"
import { BedrockCustomArn } from "./providers/BedrockCustomArn"
import { RooBalanceDisplay } from "./providers/RooBalanceDisplay"
import { buildDocLink } from "@src/utils/docLinks"
Expand Down Expand Up @@ -800,6 +802,14 @@ const ApiOptions = ({
}
onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
/>
<MaxFollowUpSuggestionsControl
value={
apiConfiguration.maxFollowUpSuggestions !== undefined
? apiConfiguration.maxFollowUpSuggestions
: DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS
}
onChange={(value) => setApiConfigurationField("maxFollowUpSuggestions", value)}
/>
{selectedProvider === "openrouter" &&
openRouterModelProviders &&
Object.keys(openRouterModelProviders).length > 0 && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS } from "@roo-code/types"

import { useAppTranslation } from "@/i18n/TranslationContext"

import { Slider } from "@/components/ui"

interface MaxFollowUpSuggestionsControlProps {
value: number
onChange: (value: number) => void
}

export const MaxFollowUpSuggestionsControl = ({ value, onChange }: MaxFollowUpSuggestionsControlProps) => {
const { t } = useAppTranslation()

return (
<div className="flex flex-col gap-1">
<label className="block font-medium mb-1">{t("settings:providers.maxFollowUpSuggestions.label")}</label>
<div className="flex items-center gap-2">
<Slider
value={[value ?? DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS]}
min={1}
max={10}
step={1}
onValueChange={(newValue) => onChange(Math.max(1, newValue[0]))}
/>
<span className="w-10">{Math.max(1, value ?? DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS)}</span>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.maxFollowUpSuggestions.description", {
value: value ?? DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS,
})}
</div>
</div>
)
}
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,10 @@
"unlimitedDescription": "Unlimited retries enabled (auto-proceed). The dialog will never appear.",
"warning": "⚠️ Setting to 0 allows unlimited retries which may consume significant API usage"
},
"maxFollowUpSuggestions": {
"label": "Max Follow-Up Suggestions",
"description": "Maximum number of suggested answers the model can offer when asking a question ({{value}})."
},
"reasoningEffort": {
"label": "Model Reasoning Effort",
"none": "None",
Expand Down
Loading