Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 22 additions & 6 deletions src/core/assistant-message/NativeToolCallParser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { parseJSON } from "partial-json"

import { type ToolName, toolNames, type FileEntry } from "@roo-code/types"
import { customToolRegistry } from "@roo-code/core"

import {
type ToolUse,
type McpToolUse,
type ToolParamName,
toolParamNames,
type NativeToolArgs,
toolParamNames,
} from "../../shared/tools"
import { resolveToolAlias } from "../prompts/tools/filter-tools-for-mode"
import { parseJSON } from "partial-json"
import type {
ApiStreamToolCallStartChunk,
ApiStreamToolCallDeltaChunk,
Expand Down Expand Up @@ -556,25 +559,28 @@ export class NativeToolCallParser {
name: TName
arguments: string
}): ToolUse<TName> | McpToolUse | null {
// console.log(`parseToolCall -> ${JSON.stringify(toolCall, null, 2)}`)

// Check if this is a dynamic MCP tool (mcp--serverName--toolName)
const mcpPrefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR

if (typeof toolCall.name === "string" && toolCall.name.startsWith(mcpPrefix)) {
return this.parseDynamicMcpTool(toolCall)
}

// Resolve tool alias to canonical name (e.g., "edit_file" -> "apply_diff", "temp_edit_file" -> "search_and_replace")
const resolvedName = resolveToolAlias(toolCall.name as string) as TName

// Validate tool name (after alias resolution)
if (!toolNames.includes(resolvedName as ToolName)) {
// Validate tool name (after alias resolution).
if (!toolNames.includes(resolvedName as ToolName) && !customToolRegistry.has(resolvedName)) {
console.error(`Invalid tool name: ${toolCall.name} (resolved: ${resolvedName})`)
console.error(`Valid tool names:`, toolNames)
return null
}

try {
// Parse the arguments JSON string
const args = JSON.parse(toolCall.arguments)
const args = toolCall.arguments === "" ? {} : JSON.parse(toolCall.arguments)

// Build legacy params object for backward compatibility with XML protocol and UI.
// Native execution path uses nativeArgs instead, which has proper typing.
Expand All @@ -589,7 +595,7 @@ export class NativeToolCallParser {
}

// Validate parameter name
if (!toolParamNames.includes(key as ToolParamName)) {
if (!toolParamNames.includes(key as ToolParamName) && !customToolRegistry.has(resolvedName)) {
console.warn(`Unknown parameter '${key}' for tool '${resolvedName}'`)
console.warn(`Valid param names:`, toolParamNames)
continue
Expand Down Expand Up @@ -786,6 +792,14 @@ export class NativeToolCallParser {
break

default:
if (customToolRegistry.has(resolvedName)) {
nativeArgs = args as NativeArgsFor<TName>
} else {
console.error(`Unhandled tool: ${resolvedName}`)
console.error(`Valid tool names:`, toolNames)
console.error(`Custom tool names:`, customToolRegistry.list())
}

break
}

Expand All @@ -802,6 +816,8 @@ export class NativeToolCallParser {
result.originalName = toolCall.name
}

console.log(`parseToolCall -> result: ${JSON.stringify(result, null, 2)}`)
Comment thread
cte marked this conversation as resolved.
Outdated

return result
} catch (error) {
console.error(
Expand Down
64 changes: 53 additions & 11 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import { Anthropic } from "@anthropic-ai/sdk"

import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { customToolRegistry } from "@roo-code/core"

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

import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"
import { Package } from "../../shared/package"
import { t } from "../../i18n"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"

import { AskIgnoredError } from "../task/AskIgnoredError"
import { Task } from "../task/Task"

import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
import { listFilesTool } from "../tools/ListFilesTool"
Expand All @@ -30,17 +34,14 @@ import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
import { switchModeTool } from "../tools/SwitchModeTool"
import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
import { newTaskTool } from "../tools/NewTaskTool"

import { updateTodoListTool } from "../tools/UpdateTodoListTool"
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
import { generateImageTool } from "../tools/GenerateImageTool"

import { formatResponse } from "../prompts/responses"
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
import { validateToolUse } from "../tools/validateToolUse"
import { Task } from "../task/Task"
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"

import { formatResponse } from "../prompts/responses"

/**
* Processes and presents assistant message content to the user interface.
Expand Down Expand Up @@ -353,7 +354,7 @@ export async function presentAssistantMessage(cline: Task) {
case "tool_use": {
// Fetch state early so it's available for toolDescription and validation
const state = await cline.providerRef.deref()?.getState()
const { mode, customModes, experiments: stateExperiments, apiConfiguration } = state ?? {}
const { mode, customModes, experiments: stateExperiments } = state ?? {}

const toolDescription = (): string => {
switch (block.name) {
Expand Down Expand Up @@ -731,6 +732,8 @@ export async function presentAssistantMessage(cline: Task) {
// This prevents the stream from being interrupted with "Response interrupted by tool use result"
// which would cause the extension to appear to hang
const errorContent = formatResponse.toolError(error.message, toolProtocol)
console.error(`[presentAssistantMessage] errorContent: ${errorContent}`)

if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
// For native protocol, push tool_result directly without setting didAlreadyUseTool
cline.userMessageContent.push({
Expand All @@ -743,6 +746,7 @@ export async function presentAssistantMessage(cline: Task) {
// For XML protocol, use the standard pushToolResult
pushToolResult(errorContent)
}

break
}
}
Expand Down Expand Up @@ -1043,9 +1047,8 @@ export async function presentAssistantMessage(cline: Task) {
})
break
default: {
// Handle unknown/invalid tool names
// Handle unknown/invalid tool names OR custom tools
// This is critical for native protocol where every tool_use MUST have a tool_result
// Note: This case should rarely be reached since validateToolUse now checks for unknown tools

// CRITICAL: Don't process partial blocks for unknown tools - just let them stream in.
// If we try to show errors for partial blocks, we'd show the error on every streaming chunk,
Expand All @@ -1054,6 +1057,45 @@ export async function presentAssistantMessage(cline: Task) {
break
}

const customTool = customToolRegistry.get(block.name)

if (customTool) {
try {
console.log(`executing customTool -> ${JSON.stringify(customTool, null, 2)}`)
let customToolArgs

if (customTool.parameters) {
try {
customToolArgs = customTool.parameters.parse(block.nativeArgs || block.params || {})
console.log(`customToolArgs -> ${JSON.stringify(customToolArgs, null, 2)}`)
} catch (parseParamsError) {
const message = `Custom tool "${block.name}" argument validation failed: ${parseParamsError.message}`
console.error(message)
cline.consecutiveMistakeCount++
await cline.say("error", message)
pushToolResult(formatResponse.toolError(message, toolProtocol))
break
}
}

console.log(`${customTool.name}.execute() -> ${JSON.stringify(customToolArgs, null, 2)}`)

const result = await customTool.execute(customToolArgs, {
mode: mode ?? defaultModeSlug,
task: cline,
})

pushToolResult(result)
cline.consecutiveMistakeCount = 0
} catch (executionError: any) {
cline.consecutiveMistakeCount++
await handleError(`executing custom tool "${block.name}"`, executionError)
}

break
}

// Not a custom tool - handle as unknown tool error
const errorMessage = `Unknown tool "${block.name}". This tool does not exist. Please use one of the available tools.`
cline.consecutiveMistakeCount++
cline.recordToolError(block.name as ToolName, errorMessage)
Expand Down
4 changes: 3 additions & 1 deletion src/core/environment/__tests__/getEnvironmentDetails.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { Mock } from "vitest"

import { getEnvironmentDetails } from "../getEnvironmentDetails"
import { EXPERIMENT_IDS, experiments } from "../../../shared/experiments"
import { defaultModeSlug, getFullModeDetails, getModeBySlug, isToolAllowedForMode } from "../../../shared/modes"
import { getFullModeDetails } from "../../../shared/modes"
import { isToolAllowedForMode } from "../../tools/validateToolUse"
import { getApiMetrics } from "../../../shared/getApiMetrics"
import { listFiles } from "../../../services/glob/list-files"
import { TerminalRegistry } from "../../../integrations/terminal/TerminalRegistry"
Expand Down Expand Up @@ -51,6 +52,7 @@ vi.mock("../../../integrations/terminal/Terminal")
vi.mock("../../../utils/path")
vi.mock("../../../utils/git")
vi.mock("../../prompts/responses")
vi.mock("../../tools/validateToolUse")

describe("getEnvironmentDetails", () => {
const mockCwd = "/test/path"
Expand Down
2 changes: 1 addition & 1 deletion src/core/environment/getEnvironmentDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT } from "@roo-code/types"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
import { EXPERIMENT_IDS, experiments as Experiments } from "../../shared/experiments"
import { formatLanguage } from "../../shared/language"
import { defaultModeSlug, getFullModeDetails, getModeBySlug, isToolAllowedForMode } from "../../shared/modes"
import { defaultModeSlug, getFullModeDetails } from "../../shared/modes"
import { getApiMetrics } from "../../shared/getApiMetrics"
import { listFiles } from "../../services/glob/list-files"
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
Expand Down
28 changes: 23 additions & 5 deletions src/core/prompts/system.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import * as vscode from "vscode"
import * as os from "os"

import type { ModeConfig, PromptComponent, CustomModePrompts, TodoItem } from "@roo-code/types"

import type { SystemPromptSettings } from "./types"
import {
type ModeConfig,
type PromptComponent,
type CustomModePrompts,
type TodoItem,
getEffectiveProtocol,
isNativeProtocol,
} from "@roo-code/types"
import { customToolRegistry, formatXml } from "@roo-code/core"

import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes"
import { DiffStrategy } from "../../shared/tools"
Expand All @@ -15,8 +21,8 @@ import { CodeIndexManager } from "../../services/code-index/manager"

import { PromptVariables, loadSystemPromptFile } from "./sections/custom-system-prompt"

import type { SystemPromptSettings } from "./types"
import { getToolDescriptionsForMode } from "./tools"
import { getEffectiveProtocol, isNativeProtocol } from "@roo-code/types"
import {
getRulesSection,
getSystemInfoSection,
Expand Down Expand Up @@ -98,7 +104,7 @@ async function generatePrompt(
])

// Build tools catalog section only for XML protocol
const toolsCatalog = isNativeProtocol(effectiveProtocol)
const builtInToolsCatalog = isNativeProtocol(effectiveProtocol)
? ""
: `\n\n${getToolDescriptionsForMode(
mode,
Expand All @@ -116,6 +122,18 @@ async function generatePrompt(
modelId,
)}`

let customToolsSection = ""

if (!isNativeProtocol(effectiveProtocol)) {
const customTools = customToolRegistry.getAllSerialized()

if (customTools.length > 0) {
customToolsSection = `\n\n${formatXml(customTools)}`
}
}

const toolsCatalog = builtInToolsCatalog + customToolsSection

const basePrompt = `${roleDefinition}

${markdownFormattingSection()}
Expand Down
3 changes: 2 additions & 1 deletion src/core/prompts/tools/filter-tools-for-mode.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type OpenAI from "openai"
import type { ModeConfig, ToolName, ToolGroup, ModelInfo } from "@roo-code/types"
import { getModeBySlug, getToolsForMode, isToolAllowedForMode } from "../../../shared/modes"
import { getModeBySlug, getToolsForMode } from "../../../shared/modes"
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, TOOL_ALIASES } from "../../../shared/tools"
import { defaultModeSlug } from "../../../shared/modes"
import type { CodeIndexManager } from "../../../services/code-index/manager"
import type { McpHub } from "../../../services/mcp/McpHub"
import { isToolAllowedForMode } from "../../../core/tools/validateToolUse"

/**
* Reverse lookup map - maps alias name to canonical tool name.
Expand Down
9 changes: 6 additions & 3 deletions src/core/prompts/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type { ToolName, ModeConfig } from "@roo-code/types"
import { shouldUseSingleFileRead } from "@roo-code/types"

import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy } from "../../../shared/tools"
import { Mode, getModeConfig, getGroupName } from "../../../shared/modes"

import { isToolAllowedForMode } from "../../tools/validateToolUse"

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"
import { getReadFileDescription } from "./read-file"
import { getSimpleReadFileDescription } from "./simple-read-file"
import { getFetchInstructionsDescription } from "./fetch-instructions"
import { shouldUseSingleFileRead } from "@roo-code/types"
import { getWriteToFileDescription } from "./write-to-file"
import { getSearchFilesDescription } from "./search-files"
import { getListFilesDescription } from "./list-files"
Expand All @@ -24,7 +28,6 @@ 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"

// Map of tool names to their description functions
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
Expand Down
8 changes: 8 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2568,6 +2568,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

// Get the index and replace partial with final
if (toolUseIndex !== undefined) {
console.log(
`[tool_call_partial] setting tool use at index ${toolUseIndex} with finalToolUse: ${JSON.stringify(finalToolUse, null, 2)}`,
)

this.assistantMessageContent[toolUseIndex] = finalToolUse
}

Expand Down Expand Up @@ -2727,6 +2731,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

// Get the index and replace partial with final
if (toolUseIndex !== undefined) {
console.log(
`[tool_call_end] setting tool use at index ${toolUseIndex} with finalToolUse: ${JSON.stringify(finalToolUse, null, 2)}`,
)

this.assistantMessageContent[toolUseIndex] = finalToolUse
}

Expand Down
Loading