Filter tools

This commit is contained in:
Matt Rubens 2025-10-29 22:43:48 -04:00
parent a840ec4979
commit 2749c6e9bd
3 changed files with 278 additions and 26 deletions

View file

@ -0,0 +1,218 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { SYSTEM_PROMPT } from "../system"
import { CodeIndexManager } from "../../../services/code-index/manager"
import type { SystemPromptSettings } from "../types"
vi.mock("../../../services/code-index/manager")
vi.mock("../../../utils/storage", () => ({
getSettingsDirectoryPath: vi.fn().mockResolvedValue("/test/settings"),
}))
vi.mock("../../../utils/globalContext", () => ({
ensureSettingsDirectoryExists: vi.fn().mockResolvedValue("/test/settings"),
}))
describe("SYSTEM_PROMPT with native tools", () => {
const mockContext = {
extensionUri: { fsPath: "/test/path" },
globalStorageUri: { fsPath: "/test/global-storage" },
globalState: {
get: vi.fn(),
update: vi.fn(),
},
workspaceState: {
get: vi.fn(),
update: vi.fn(),
},
} as any
const defaultSettings: SystemPromptSettings = {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: true,
}
beforeEach(() => {
vi.clearAllMocks()
})
it("should filter out update_todo_list when todoListEnabled is false", async () => {
const mockCodeIndexManager = {
isFeatureEnabled: true,
isFeatureConfigured: true,
isInitialized: true,
}
vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any)
const result = await SYSTEM_PROMPT(
mockContext,
"/test/cwd",
false,
undefined,
undefined,
undefined,
"code",
undefined,
undefined,
undefined,
true,
undefined,
false,
undefined,
undefined,
false,
{ ...defaultSettings, todoListEnabled: false },
undefined,
undefined,
true, // useNativeTools
)
expect(result.tools).toBeDefined()
const toolNames = result.tools?.map((t) => t.name) || []
expect(toolNames).not.toContain("update_todo_list")
})
it("should include update_todo_list when todoListEnabled is true", async () => {
const mockCodeIndexManager = {
isFeatureEnabled: true,
isFeatureConfigured: true,
isInitialized: true,
}
vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any)
const result = await SYSTEM_PROMPT(
mockContext,
"/test/cwd",
false,
undefined,
undefined,
undefined,
"code",
undefined,
undefined,
undefined,
true,
undefined,
false,
undefined,
undefined,
false,
{ ...defaultSettings, todoListEnabled: true },
undefined,
undefined,
true, // useNativeTools
)
expect(result.tools).toBeDefined()
const toolNames = result.tools?.map((t) => t.name) || []
expect(toolNames).toContain("update_todo_list")
})
it("should filter out codebase_search when feature is not configured", async () => {
const mockCodeIndexManager = {
isFeatureEnabled: false,
isFeatureConfigured: false,
isInitialized: false,
}
vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any)
const result = await SYSTEM_PROMPT(
mockContext,
"/test/cwd",
false,
undefined,
undefined,
undefined,
"code",
undefined,
undefined,
undefined,
true,
undefined,
false,
undefined,
undefined,
false,
undefined,
undefined,
undefined,
true, // useNativeTools
)
expect(result.tools).toBeDefined()
const toolNames = result.tools?.map((t) => t.name) || []
expect(toolNames).not.toContain("codebase_search")
})
it("should filter out generate_image when experiment is disabled", async () => {
const mockCodeIndexManager = {
isFeatureEnabled: true,
isFeatureConfigured: true,
isInitialized: true,
}
vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any)
const result = await SYSTEM_PROMPT(
mockContext,
"/test/cwd",
false,
undefined,
undefined,
undefined,
"code",
undefined,
undefined,
undefined,
true,
{ imageGeneration: false },
false,
undefined,
undefined,
false,
undefined,
undefined,
undefined,
true, // useNativeTools
)
expect(result.tools).toBeDefined()
const toolNames = result.tools?.map((t) => t.name) || []
expect(toolNames).not.toContain("generate_image")
})
it("should filter out run_slash_command when experiment is disabled", async () => {
const mockCodeIndexManager = {
isFeatureEnabled: true,
isFeatureConfigured: true,
isInitialized: true,
}
vi.mocked(CodeIndexManager.getInstance).mockReturnValue(mockCodeIndexManager as any)
const result = await SYSTEM_PROMPT(
mockContext,
"/test/cwd",
false,
undefined,
undefined,
undefined,
"code",
undefined,
undefined,
undefined,
true,
{ runSlashCommand: false },
false,
undefined,
undefined,
false,
undefined,
undefined,
undefined,
true, // useNativeTools
)
expect(result.tools).toBeDefined()
const toolNames = result.tools?.map((t) => t.name) || []
expect(toolNames).not.toContain("run_slash_command")
})
})

View file

@ -17,7 +17,7 @@ import { getToolSpecs } from "./tool-specs"
import { PromptVariables, loadSystemPromptFile } from "./sections/custom-system-prompt"
import { getToolDescriptionsForMode } from "./tools"
import { getToolDescriptionsForMode, filterToolsByAvailability } from "./tools"
import {
getRulesSection,
getSystemInfoSection,
@ -252,8 +252,12 @@ ${customInstructions}`,
// Remove duplicates
const uniqueToolNames = [...new Set(toolNames)]
// Apply centralized filtering to ensure consistency with XML tool mode
const codeIndexManager = CodeIndexManager.getInstance(context, cwd)
const filteredTools = filterToolsByAvailability(uniqueToolNames, codeIndexManager, settings, experiments)
// Get tool specifications
const tools = getToolSpecs(uniqueToolNames)
const tools = getToolSpecs(filteredTools)
return {
systemPrompt: basePrompt,

View file

@ -3,6 +3,7 @@ import type { ToolName, ModeConfig } from "@roo-code/types"
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy } from "../../../shared/tools"
import { McpHub } from "../../../services/mcp/McpHub"
import { Mode, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"
import { CodeIndexManager } from "../../../services/code-index/manager"
import { ToolArgs } from "./types"
import { getExecuteCommandDescription } from "./execute-command"
@ -27,7 +28,50 @@ import { getCodebaseSearchDescription } from "./codebase-search"
import { getUpdateTodoListDescription } from "./update-todo-list"
import { getRunSlashCommandDescription } from "./run-slash-command"
import { getGenerateImageDescription } from "./generate-image"
import { CodeIndexManager } from "../../../services/code-index/manager"
/**
* Filters a list of tool names based on feature flags, settings, and experiments.
* This ensures consistent tool availability across XML and native tool modes.
*/
export function filterToolsByAvailability(
tools: ToolName[],
codeIndexManager: CodeIndexManager | undefined,
settings?: Record<string, any>,
experiments?: Record<string, boolean>,
): ToolName[] {
return tools.filter((tool) => {
// Conditionally exclude codebase_search if feature is disabled or not configured
if (tool === "codebase_search") {
if (
!codeIndexManager ||
!(
codeIndexManager.isFeatureEnabled &&
codeIndexManager.isFeatureConfigured &&
codeIndexManager.isInitialized
)
) {
return false
}
}
// Conditionally exclude update_todo_list if disabled in settings
if (tool === "update_todo_list" && settings?.todoListEnabled === false) {
return false
}
// Conditionally exclude generate_image if experiment is not enabled
if (tool === "generate_image" && !experiments?.imageGeneration) {
return false
}
// Conditionally exclude run_slash_command if experiment is not enabled
if (tool === "run_slash_command" && !experiments?.runSlashCommand) {
return false
}
return true
})
}
// Map of tool names to their description functions
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
@ -120,31 +164,17 @@ export function getToolDescriptionsForMode(
// Add always available tools
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
// Conditionally exclude codebase_search if feature is disabled or not configured
if (
!codeIndexManager ||
!(codeIndexManager.isFeatureEnabled && codeIndexManager.isFeatureConfigured && codeIndexManager.isInitialized)
) {
tools.delete("codebase_search")
}
// Conditionally exclude update_todo_list if disabled in settings
if (settings?.todoListEnabled === false) {
tools.delete("update_todo_list")
}
// Conditionally exclude generate_image if experiment is not enabled
if (!experiments?.imageGeneration) {
tools.delete("generate_image")
}
// Conditionally exclude run_slash_command if experiment is not enabled
if (!experiments?.runSlashCommand) {
tools.delete("run_slash_command")
}
// Apply consistent filtering across all tool modes
const filteredTools = filterToolsByAvailability(
Array.from(tools) as ToolName[],
codeIndexManager,
settings,
experiments,
)
const filteredToolsSet = new Set(filteredTools)
// Map tool descriptions for allowed tools
const descriptions = Array.from(tools).map((toolName) => {
const descriptions = Array.from(filteredToolsSet).map((toolName) => {
const descriptionFn = toolDescriptionMap[toolName]
if (!descriptionFn) {
return undefined