From f44909afee8e8db8b6d4443cbf3ca7314f755608 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sun, 8 Mar 2026 22:44:04 +0000 Subject: [PATCH] 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 --- packages/types/src/provider-settings.ts | 5 ++ src/core/prompts/sections/rules.ts | 5 +- .../__tests__/ask_followup_question.spec.ts | 72 +++++++++++++++ .../native-tools/ask_followup_question.ts | 87 +++++++++++-------- src/core/prompts/tools/native-tools/index.ts | 8 +- src/core/prompts/types.ts | 2 + src/core/task/Task.ts | 1 + src/core/task/build-tools.ts | 1 + src/core/webview/generateSystemPrompt.ts | 1 + .../src/components/settings/ApiOptions.tsx | 10 +++ .../MaxFollowUpSuggestionsControl.tsx | 35 ++++++++ webview-ui/src/i18n/locales/en/settings.json | 4 + 12 files changed, 190 insertions(+), 41 deletions(-) create mode 100644 src/core/prompts/tools/native-tools/__tests__/ask_followup_question.spec.ts create mode 100644 webview-ui/src/components/settings/MaxFollowUpSuggestionsControl.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 859792d7c36..095e8957cc6 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -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(), diff --git a/src/core/prompts/sections/rules.ts b/src/core/prompts/sections/rules.ts index 4f6e573fa73..f120e119ecf 100644 --- a/src/core/prompts/sections/rules.ts +++ b/src/core/prompts/sections/rules.ts @@ -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. diff --git a/src/core/prompts/tools/native-tools/__tests__/ask_followup_question.spec.ts b/src/core/prompts/tools/native-tools/__tests__/ask_followup_question.spec.ts new file mode 100644 index 00000000000..ccaefcc0aab --- /dev/null +++ b/src/core/prompts/tools/native-tools/__tests__/ask_followup_question.spec.ts @@ -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) + }) +}) diff --git a/src/core/prompts/tools/native-tools/ask_followup_question.ts b/src/core/prompts/tools/native-tools/ask_followup_question.ts index b0591206ade..d353b7ac69c 100644 --- a/src/core/prompts/tools/native-tools/ask_followup_question.ts +++ b/src/core/prompts/tools/native-tools/ask_followup_question.ts @@ -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() diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 758914d2d65..d2de108488a 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -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, diff --git a/src/core/prompts/types.ts b/src/core/prompts/types.ts index a4c17c3a6e6..d68c3490afb 100644 --- a/src/core/prompts/types.ts +++ b/src/core/prompts/types.ts @@ -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 } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 005bb0f292b..bc385d727c1 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3810,6 +3810,7 @@ export class Task extends EventEmitter implements TaskLike { .getConfiguration(Package.name) .get("newTaskRequireTodos", false), isStealthModel: modelInfo?.isStealthModel, + maxFollowUpSuggestions: apiConfiguration?.maxFollowUpSuggestions, }, undefined, // todoList this.api.getModel().id, diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index c32d8f6f9b2..8f9089eec40 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -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. diff --git a/src/core/webview/generateSystemPrompt.ts b/src/core/webview/generateSystemPrompt.ts index 8af2f5ff5d5..4c46d92b77c 100644 --- a/src/core/webview/generateSystemPrompt.ts +++ b/src/core/webview/generateSystemPrompt.ts @@ -60,6 +60,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web .getConfiguration(Package.name) .get("newTaskRequireTodos", false), isStealthModel: modelInfo?.isStealthModel, + maxFollowUpSuggestions: apiConfiguration?.maxFollowUpSuggestions, }, undefined, // todoList undefined, // modelId diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 4d914a4833a..61d3135e723 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -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)} /> + setApiConfigurationField("maxFollowUpSuggestions", value)} + /> {selectedProvider === "openrouter" && openRouterModelProviders && Object.keys(openRouterModelProviders).length > 0 && ( diff --git a/webview-ui/src/components/settings/MaxFollowUpSuggestionsControl.tsx b/webview-ui/src/components/settings/MaxFollowUpSuggestionsControl.tsx new file mode 100644 index 00000000000..18d360495e0 --- /dev/null +++ b/webview-ui/src/components/settings/MaxFollowUpSuggestionsControl.tsx @@ -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 ( +
+ +
+ onChange(Math.max(1, newValue[0]))} + /> + {Math.max(1, value ?? DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS)} +
+
+ {t("settings:providers.maxFollowUpSuggestions.description", { + value: value ?? DEFAULT_MAX_FOLLOW_UP_SUGGESTIONS, + })} +
+
+ ) +} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 3b2497aaee7..3c225138b48 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -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",