feat: make ask_followup_question maxItems configurable via settings

Add maxFollowUpSuggestions setting (default: 4, range: 1-10) to
ProviderSettings. This allows users to configure how many follow-up
suggestions the model can offer, addressing the limitation where
skills needing more than 4 options were constrained.

Changes:
- Add maxFollowUpSuggestions to baseProviderSettingsSchema
- Convert ask_followup_question tool from static to dynamic (createAskFollowupQuestionTool)
- Thread setting through NativeToolsOptions, build-tools, SystemPromptSettings
- Update rules section "2-4" text to be dynamic
- Add MaxFollowUpSuggestionsControl slider UI component
- Add i18n strings for the new control
- Add tests for the dynamic tool generation

Closes #11895
This commit is contained in:
Roo Code 2026-03-08 22:44:04 +00:00
parent 44fd975b17
commit f44909afee
12 changed files with 192 additions and 43 deletions

View file

@ -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
*
@ -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(),

View file

@ -1,3 +1,5 @@
import { DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS } from "@roo-code/types"
import type { SystemPromptSettings } from "../types"
import { getShell } from "../../../utils/shell"
@ -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 `====
@ -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.

View file

@ -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)
})
})

View file

@ -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,
},
},
required: ["text", "mode"],
additionalProperties: false,
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,
},
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()

View file

@ -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"
@ -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
}
/**
@ -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,
@ -50,7 +52,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
accessMcpResource,
apply_diff,
applyPatch,
askFollowupQuestion,
maxFollowUpSuggestions ? createAskFollowupQuestionTool(maxFollowUpSuggestions) : askFollowupQuestion,
attemptCompletion,
codebaseSearch,
executeCommand,

View file

@ -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
}

View file

@ -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,

View file

@ -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.

View file

@ -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

View file

@ -9,6 +9,7 @@ import {
type ProviderSettings,
isRetiredProvider,
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS,
openRouterDefaultModelId,
requestyDefaultModelId,
litellmDefaultModelId,
@ -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"
@ -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 && (

View file

@ -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>
)
}

View file

@ -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",