diff --git a/core/config/load.ts b/core/config/load.ts index 4a88fbcce30..e4c7e13c4f0 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -651,6 +651,7 @@ function llmToSerializedModelDescription(llm: ILLM): ModelDescription { envSecretLocations: llm.envSecretLocations, sourceFile: llm.sourceFile, isFromAutoDetect: llm.isFromAutoDetect, + toolOverrides: llm.toolOverrides, }; } diff --git a/core/index.d.ts b/core/index.d.ts index 82941d189e7..068c51abcec 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1255,6 +1255,9 @@ export interface ModelDescription { sourceFile?: string; isFromAutoDetect?: boolean; + + /** Tool overrides for this model */ + toolOverrides?: ToolOverride[]; } export interface JSONEmbedOptions { diff --git a/core/llm/utils/retry.ts b/core/llm/utils/retry.ts index 1ef12c79dcf..ba502c354f8 100644 --- a/core/llm/utils/retry.ts +++ b/core/llm/utils/retry.ts @@ -1,3 +1,5 @@ +import { isAbortError } from "../../util/isAbortError.js"; + /** * Configuration options for the retry decorator */ @@ -103,7 +105,7 @@ function defaultShouldRetry(error: any, attempt: number): boolean { } // Abort signal errors should not be retried - if (error.name === "AbortError" || error.code === "ABORT_ERR") { + if (isAbortError(error)) { return false; } diff --git a/core/tools/systemMessageTools/toolCodeblocks/index.ts b/core/tools/systemMessageTools/toolCodeblocks/index.ts index 97baa19fa06..a2cdc393d66 100644 --- a/core/tools/systemMessageTools/toolCodeblocks/index.ts +++ b/core/tools/systemMessageTools/toolCodeblocks/index.ts @@ -65,13 +65,20 @@ export class SystemMessageToolCodeblocksFramework return toolDefinition.trim(); } - systemMessagePrefix = `You have access to several "tools" that you can use at any time to retrieve information and/or perform tasks for the User. -To use a tool, respond with a tool code block (\`\`\`tool) using the syntax shown in the examples below:`; - - systemMessageSuffix = `If it seems like the User's request could be solved with one of the tools, choose the BEST one for the job based on the user's request and the tool descriptions -Then send the \`\`\`tool codeblock (YOU call the tool, not the user). Always start the codeblock on a new line. -Do not perform actions with/for hypothetical files. Ask the user or use tools to deduce which files are relevant. -You can only call ONE tool at at time. The tool codeblock should be the last thing you say; stop your response after the tool codeblock.`; + systemMessagePrefix = `You have access to tools. To call a tool, you MUST respond with EXACTLY this format — a tool code block (\`\`\`tool) using the syntax shown below. + +CRITICAL: Follow the exact syntax. Do not use XML tags, JSON objects, or any other format for tool calls.`; + + systemMessageSuffix = `RULES FOR TOOL USE: +1. To call a tool, output a \`\`\`tool code block using EXACTLY the format shown above. +2. Always start the code block on a new line. +3. You can only call ONE tool at a time. +4. The \`\`\`tool code block MUST be the last thing in your response. Stop immediately after the closing \`\`\`. +5. Do NOT wrap tool calls in XML tags like or . +6. Do NOT use JSON format for tool calls. +7. Do NOT invent tools that are not listed above. +8. If the user's request can be addressed with a listed tool, use it rather than guessing. +9. Do not perform actions with hypothetical files. Use tools to find relevant files.`; exampleDynamicToolDefinition = ` \`\`\`tool_definition diff --git a/core/util/isAbortError.ts b/core/util/isAbortError.ts new file mode 100644 index 00000000000..583d656aebd --- /dev/null +++ b/core/util/isAbortError.ts @@ -0,0 +1,39 @@ +/** + * Unified abort error detection. + * Covers all known abort patterns in the Continue codebase: + * - String literal "cancel" (streaming cancellation) + * - Error with name "AbortError" (node-fetch, DOM) + * - Error with code "ABORT_ERR" (Node.js AbortSignal) + * - DOMException with name "AbortError" (browser/Node.js 18+) + * - Plain objects with name "AbortError" (serialized errors) + */ +export function isAbortError(error: unknown): boolean { + if (error === null || error === undefined) return false; + + // String-based "cancel" (used in Continue streaming path) + if (error === "cancel") return true; + + // Standard Error objects + if (error instanceof Error) { + if (error.name === "AbortError") return true; + if ("code" in error && (error as any).code === "ABORT_ERR") return true; + } + + // DOMException (browser/Node.js 18+) + // In some runtimes DOMException does not extend Error, so check separately. + if ( + typeof DOMException !== "undefined" && + error instanceof DOMException && + error.name === "AbortError" + ) { + return true; + } + + // Plain objects with name property (e.g. serialized errors across boundaries) + if (typeof error === "object" && "name" in error) { + const name = (error as any).name; + if (typeof name === "string" && name === "AbortError") return true; + } + + return false; +} diff --git a/core/util/isAbortError.vitest.ts b/core/util/isAbortError.vitest.ts new file mode 100644 index 00000000000..ccd6441415f --- /dev/null +++ b/core/util/isAbortError.vitest.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from "vitest"; +import { isAbortError } from "./isAbortError"; + +describe("isAbortError", () => { + // -- Positive cases -- + + it('returns true for "cancel" string', () => { + expect(isAbortError("cancel")).toBe(true); + }); + + it("returns true for Error with name AbortError", () => { + const error = Object.assign(new Error("aborted"), { name: "AbortError" }); + expect(isAbortError(error)).toBe(true); + }); + + it("returns true for Error with ABORT_ERR code", () => { + const error = Object.assign(new Error("aborted"), { code: "ABORT_ERR" }); + expect(isAbortError(error)).toBe(true); + }); + + it("returns true for Error with both AbortError name and ABORT_ERR code", () => { + const error = Object.assign(new Error("aborted"), { + name: "AbortError", + code: "ABORT_ERR", + }); + expect(isAbortError(error)).toBe(true); + }); + + it("returns true for custom Error subclass with AbortError name", () => { + class CustomAbortError extends Error { + constructor() { + super("aborted"); + this.name = "AbortError"; + } + } + expect(isAbortError(new CustomAbortError())).toBe(true); + }); + + const hasDOMException = typeof DOMException !== "undefined"; + + it.skipIf(!hasDOMException)( + "returns true for DOMException with AbortError name", + () => { + const error = new DOMException("Operation aborted", "AbortError"); + expect(isAbortError(error)).toBe(true); + }, + ); + + it("returns true for plain object with name AbortError", () => { + expect(isAbortError({ name: "AbortError" })).toBe(true); + }); + + it("returns true for frozen object with name AbortError", () => { + expect(isAbortError(Object.freeze({ name: "AbortError" }))).toBe(true); + }); + + // -- Negative cases: exact match, not substring -- + + it("returns false for Error with name containing AbortError as substring", () => { + const error = Object.assign(new Error("x"), { name: "NetworkAbortError" }); + expect(isAbortError(error)).toBe(false); + }); + + it("returns false for Error with partial name Abort", () => { + const error = Object.assign(new Error("x"), { name: "Abort" }); + expect(isAbortError(error)).toBe(false); + }); + + it("returns false for plain object with AbortError substring in name", () => { + expect(isAbortError({ name: "AbortErrorWrapper" })).toBe(false); + }); + + // -- Negative cases: cancel is case-sensitive -- + + it('returns false for "Cancel" (capital C)', () => { + expect(isAbortError("Cancel")).toBe(false); + }); + + it('returns false for "CANCEL" (all caps)', () => { + expect(isAbortError("CANCEL")).toBe(false); + }); + + // -- Negative cases: plain object edge cases -- + + it("returns false for plain object with ABORT_ERR code but no AbortError name", () => { + // code check only applies to instanceof Error, not plain objects + expect(isAbortError({ code: "ABORT_ERR" })).toBe(false); + }); + + it("returns false for plain object with numeric name", () => { + expect(isAbortError({ name: 123 })).toBe(false); + }); + + it("returns false for plain object with null name", () => { + expect(isAbortError({ name: null })).toBe(false); + }); + + it.skipIf(!hasDOMException)( + "returns false for DOMException with non-AbortError name", + () => { + const error = new DOMException("fail", "NetworkError"); + expect(isAbortError(error)).toBe(false); + }, + ); + + // -- Negative cases: primitives and nullish -- + + it("returns false for null", () => { + expect(isAbortError(null)).toBe(false); + }); + + it("returns false for undefined", () => { + expect(isAbortError(undefined)).toBe(false); + }); + + it("returns false for empty string", () => { + expect(isAbortError("")).toBe(false); + }); + + it("returns false for number", () => { + expect(isAbortError(42)).toBe(false); + }); + + it("returns false for boolean", () => { + expect(isAbortError(false)).toBe(false); + expect(isAbortError(true)).toBe(false); + }); + + // -- Negative cases: regular errors -- + + it("returns false for regular Error", () => { + expect(isAbortError(new Error("network error"))).toBe(false); + }); + + it("returns false for TypeError", () => { + expect(isAbortError(new TypeError("bad type"))).toBe(false); + }); + + it("returns false for plain object without name", () => { + expect(isAbortError({ message: "error" })).toBe(false); + }); +});