diff --git a/core/protocol/core.ts b/core/protocol/core.ts index 28e617f798f..f23666165bf 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -307,7 +307,11 @@ export type ToCoreFromIdeOrWebviewProtocol = { "auth/getAuthUrl": [{ useOnboarding: boolean }, { url: string }]; "tools/call": [ { toolCall: ToolCall }, - { contextItems: ContextItem[]; errorMessage?: string }, + { + contextItems: ContextItem[]; + errorMessage?: string; + errorReason?: ContinueErrorReason; + }, ]; "tools/evaluatePolicy": [ { toolName: string; basePolicy: ToolPolicy; args: Record }, diff --git a/core/tools/callTool.ts b/core/tools/callTool.ts index faec8a94b8c..8d695e1e192 100644 --- a/core/tools/callTool.ts +++ b/core/tools/callTool.ts @@ -1,6 +1,7 @@ import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js"; import { ContextItem, Tool, ToolCall, ToolExtras } from ".."; import { MCPManagerSingleton } from "../context/mcp/MCPManagerSingleton"; +import { ContinueError, ContinueErrorReason } from "../util/errors"; import { canParseUrl } from "../util/url"; import { BuiltInToolNames } from "./builtIn"; @@ -197,6 +198,7 @@ export async function callTool( ): Promise<{ contextItems: ContextItem[]; errorMessage: string | undefined; + errorReason?: ContinueErrorReason; }> { try { const args = safeParseToolCallArgs(toolCall); @@ -214,12 +216,19 @@ export async function callTool( }; } catch (e) { let errorMessage = `${e}`; - if (e instanceof Error) { + let errorReason: ContinueErrorReason | undefined; + + if (e instanceof ContinueError) { + errorMessage = e.message; + errorReason = e.reason; + } else if (e instanceof Error) { errorMessage = e.message; } + return { contextItems: [], errorMessage, + errorReason, }; } } diff --git a/core/tools/implementations/createNewFile.ts b/core/tools/implementations/createNewFile.ts index 0677e7dcd0f..dca307d99d6 100644 --- a/core/tools/implementations/createNewFile.ts +++ b/core/tools/implementations/createNewFile.ts @@ -3,6 +3,7 @@ import { inferResolvedUriFromRelativePath } from "../../util/ideUtils"; import { ToolImpl } from "."; import { getCleanUriPath, getUriPathBasename } from "../../util/uri"; import { getStringArg } from "../parseArgs"; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; export const createNewFileImpl: ToolImpl = async (args, extras) => { const filepath = getStringArg(args, "filepath"); @@ -15,7 +16,8 @@ export const createNewFileImpl: ToolImpl = async (args, extras) => { if (resolvedFileUri) { const exists = await extras.ide.fileExists(resolvedFileUri); if (exists) { - throw new Error( + throw new ContinueError( + ContinueErrorReason.FileAlreadyExists, `File ${filepath} already exists. Use the edit tool to edit this file`, ); } @@ -37,6 +39,9 @@ export const createNewFileImpl: ToolImpl = async (args, extras) => { }, ]; } else { - throw new Error("Failed to resolve path"); + throw new ContinueError( + ContinueErrorReason.PathResolutionFailed, + "Failed to resolve path", + ); } }; diff --git a/core/tools/implementations/grepSearch.ts b/core/tools/implementations/grepSearch.ts index 727b5c560bf..69ccf6aacc3 100644 --- a/core/tools/implementations/grepSearch.ts +++ b/core/tools/implementations/grepSearch.ts @@ -1,5 +1,6 @@ import { ToolImpl } from "."; import { ContextItem } from "../.."; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; import { formatGrepSearchResults } from "../../util/grepSearch"; import { prepareQueryForRipgrep } from "../../util/regexValidator"; import { getStringArg } from "../parseArgs"; @@ -63,7 +64,10 @@ export const grepSearchImpl: ToolImpl = async (args, extras) => { ]; } - throw error; + throw new ContinueError( + ContinueErrorReason.SearchExecutionFailed, + errorMessage, + ); } const { formatted, numResults, truncated } = formatGrepSearchResults( diff --git a/core/tools/implementations/lsTool.ts b/core/tools/implementations/lsTool.ts index 67122f082be..67e0bb1718d 100644 --- a/core/tools/implementations/lsTool.ts +++ b/core/tools/implementations/lsTool.ts @@ -3,6 +3,7 @@ import ignore from "ignore"; import { ToolImpl } from "."; import { walkDir } from "../../indexing/walkDir"; import { resolveRelativePathInDir } from "../../util/ideUtils"; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; export function resolveLsToolDirPath(dirPath: string | undefined) { if (!dirPath || dirPath === ".") { @@ -20,7 +21,8 @@ export const lsToolImpl: ToolImpl = async (args, extras) => { const dirPath = resolveLsToolDirPath(args?.dirPath); const uri = await resolveRelativePathInDir(dirPath, extras.ide); if (!uri) { - throw new Error( + throw new ContinueError( + ContinueErrorReason.DirectoryNotFound, `Directory ${args.dirPath} not found. Make sure to use forward-slash paths`, ); } diff --git a/core/tools/implementations/readFile.ts b/core/tools/implementations/readFile.ts index afa6495ad08..8daee746212 100644 --- a/core/tools/implementations/readFile.ts +++ b/core/tools/implementations/readFile.ts @@ -5,6 +5,7 @@ import { ToolImpl } from "."; import { throwIfFileIsSecurityConcern } from "../../indexing/ignore"; import { getStringArg } from "../parseArgs"; import { throwIfFileExceedsHalfOfContext } from "./readFileLimit"; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; export const readFileImpl: ToolImpl = async (args, extras) => { const filepath = getStringArg(args, "filepath"); @@ -12,7 +13,8 @@ export const readFileImpl: ToolImpl = async (args, extras) => { const firstUriMatch = await resolveRelativePathInDir(filepath, extras.ide); if (!firstUriMatch) { - throw new Error( + throw new ContinueError( + ContinueErrorReason.FileNotFound, `File "${filepath}" does not exist. You might want to check the path and try again.`, ); } diff --git a/core/tools/implementations/readFileLimit.ts b/core/tools/implementations/readFileLimit.ts index e2af14d5665..48f4b9f860b 100644 --- a/core/tools/implementations/readFileLimit.ts +++ b/core/tools/implementations/readFileLimit.ts @@ -1,5 +1,6 @@ import { ILLM } from "../.."; import { countTokensAsync } from "../../llm/countTokens"; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; export async function throwIfFileExceedsHalfOfContext( filepath: string, @@ -10,7 +11,8 @@ export async function throwIfFileExceedsHalfOfContext( const tokens = await countTokensAsync(content, model.title); const tokenLimit = model.contextLength / 2; if (tokens > tokenLimit) { - throw new Error( + throw new ContinueError( + ContinueErrorReason.FileTooLarge, `File ${filepath} is too large (${tokens} tokens vs ${tokenLimit} token limit). Try another approach`, ); } diff --git a/core/tools/implementations/readFileRange.ts b/core/tools/implementations/readFileRange.ts index c313f4bff14..0ff97d18991 100644 --- a/core/tools/implementations/readFileRange.ts +++ b/core/tools/implementations/readFileRange.ts @@ -4,6 +4,7 @@ import { getUriPathBasename } from "../../util/uri"; import { ToolImpl } from "."; import { getNumberArg, getStringArg } from "../parseArgs"; import { throwIfFileExceedsHalfOfContext } from "./readFileLimit"; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; export const readFileRangeImpl: ToolImpl = async (args, extras) => { const filepath = getStringArg(args, "filepath"); @@ -12,24 +13,28 @@ export const readFileRangeImpl: ToolImpl = async (args, extras) => { // Validate that line numbers are positive integers if (startLine < 1) { - throw new Error( + throw new ContinueError( + ContinueErrorReason.InvalidLineNumber, "startLine must be 1 or greater. Negative line numbers are not supported - use the terminal tool with 'tail' command for reading from file end.", ); } if (endLine < 1) { - throw new Error( + throw new ContinueError( + ContinueErrorReason.InvalidLineNumber, "endLine must be 1 or greater. Negative line numbers are not supported - use the terminal tool with 'tail' command for reading from file end.", ); } if (endLine < startLine) { - throw new Error( + throw new ContinueError( + ContinueErrorReason.InvalidLineNumber, `endLine (${endLine}) must be greater than or equal to startLine (${startLine})`, ); } const firstUriMatch = await resolveRelativePathInDir(filepath, extras.ide); if (!firstUriMatch) { - throw new Error( + throw new ContinueError( + ContinueErrorReason.FileNotFound, `File "${filepath}" does not exist. You might want to check the path and try again.`, ); } diff --git a/core/tools/implementations/requestRule.ts b/core/tools/implementations/requestRule.ts index c5c312e7087..21ee86af852 100644 --- a/core/tools/implementations/requestRule.ts +++ b/core/tools/implementations/requestRule.ts @@ -1,4 +1,5 @@ import { ToolImpl } from "."; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; import { getStringArg } from "../parseArgs"; export const requestRuleImpl: ToolImpl = async (args, extras) => { @@ -8,7 +9,10 @@ export const requestRuleImpl: ToolImpl = async (args, extras) => { const rule = extras.config.rules.find((r) => r.name === name); if (!rule || !rule.sourceFile) { - throw new Error(`Rule with name "${name}" not found or has no file path`); + throw new ContinueError( + ContinueErrorReason.RuleNotFound, + `Rule with name "${name}" not found or has no file path`, + ); } return [ diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index cccf3353b18..15b1dc1bffb 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -2,6 +2,7 @@ import iconv from "iconv-lite"; import childProcess from "node:child_process"; import os from "node:os"; import util from "node:util"; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; // Automatically decode the buffer according to the platform to avoid garbled Chinese function getDecodedOutput(data: Buffer): string { if (process.platform === "win32") { @@ -337,7 +338,8 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { if (code === 0) { resolve({ stdout, stderr }); } else { - const error = new Error( + const error = new ContinueError( + ContinueErrorReason.CommandExecutionFailed, `Command failed with exit code ${code}`, ); (error as any).stderr = stderr; diff --git a/core/tools/implementations/viewSubdirectory.ts b/core/tools/implementations/viewSubdirectory.ts index b7897aae8a4..de546833852 100644 --- a/core/tools/implementations/viewSubdirectory.ts +++ b/core/tools/implementations/viewSubdirectory.ts @@ -2,6 +2,7 @@ import generateRepoMap from "../../util/generateRepoMap"; import { resolveRelativePathInDir } from "../../util/ideUtils"; import { ToolImpl } from "."; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; import { getStringArg } from "../parseArgs"; export const viewSubdirectoryImpl: ToolImpl = async (args: any, extras) => { @@ -10,7 +11,10 @@ export const viewSubdirectoryImpl: ToolImpl = async (args: any, extras) => { const uri = await resolveRelativePathInDir(directory_path, extras.ide); if (!uri) { - throw new Error(`Directory path "${directory_path}" does not exist.`); + throw new ContinueError( + ContinueErrorReason.DirectoryNotFound, + `Directory path "${directory_path}" does not exist.`, + ); } const repoMap = await generateRepoMap(extras.llm, extras.ide, { diff --git a/core/util/errors.ts b/core/util/errors.ts index 4c6cfb16d32..69df7c81423 100644 --- a/core/util/errors.ts +++ b/core/util/errors.ts @@ -47,6 +47,20 @@ export enum ContinueErrorReason { FileWriteError = "file_write_error", FileIsSecurityConcern = "file_is_security_concern", ParentDirectoryNotFound = "parent_directory_not_found", + FileTooLarge = "file_too_large", + PathResolutionFailed = "path_resolution_failed", + InvalidLineNumber = "invalid_line_number", + DirectoryNotFound = "directory_not_found", + + // Terminal/Command execution + CommandExecutionFailed = "command_execution_failed", + CommandNotAvailableInRemote = "command_not_available_in_remote", + + // Search + SearchExecutionFailed = "search_execution_failed", + + // Rules + RuleNotFound = "rule_not_found", // Other Unspecified = "unspecified", // I.e. a known error but no specific code for it diff --git a/extensions/cli/src/telemetry/telemetryService.ts b/extensions/cli/src/telemetry/telemetryService.ts index f8121129850..910c82f59ed 100644 --- a/extensions/cli/src/telemetry/telemetryService.ts +++ b/extensions/cli/src/telemetry/telemetryService.ts @@ -17,6 +17,7 @@ import { } from "@opentelemetry/semantic-conventions"; import { v4 as uuidv4 } from "uuid"; +import { ContinueErrorReason } from "../../../../core/util/errors.js"; import { isHeadlessMode } from "../util/cli.js"; import { isContinueRemoteAgent, isGitHubActions } from "../util/git.js"; import { logger } from "../util/logger.js"; @@ -498,7 +499,7 @@ class TelemetryService { success: boolean; durationMs: number; error?: string; - errorReason?: string; + errorReason?: ContinueErrorReason; decision?: "accept" | "reject"; source?: string; toolParameters?: string; diff --git a/gui/src/redux/thunks/callToolById.ts b/gui/src/redux/thunks/callToolById.ts index 9c9689b87aa..d0da3465fca 100644 --- a/gui/src/redux/thunks/callToolById.ts +++ b/gui/src/redux/thunks/callToolById.ts @@ -94,7 +94,7 @@ export const callToolById = createAsyncThunk< output = result.content.contextItems; error = result.content.errorMessage ? new ContinueError( - ContinueErrorReason.Unspecified, + result.content.errorReason || ContinueErrorReason.Unspecified, result.content.errorMessage, ) : undefined;