From 6387f655217600f419258674f282cf1d9c9bc3ca Mon Sep 17 00:00:00 2001 From: "Keita A." Date: Sat, 14 Mar 2026 08:10:05 +0900 Subject: [PATCH 1/2] refactor: unify AbortError detection with isAbortError utility (#10551) * refactor: unify AbortError detection with isAbortError utility AbortError detection was scattered across 3+ patterns (string comparison, .name check, .code check). Consolidates into a single isAbortError() utility that handles all known patterns: "cancel" string, DOMException, Error.name variants, ABORT_ERR code, and plain objects. Updates call sites in core/llm/index.ts and core/llm/utils/retry.ts. * fix: use exact match for AbortError name, add null guard, improve tests - Changed .includes("AbortError") to === "AbortError" to preserve original exact-match semantics at L505 and retry.ts call sites - Added null/undefined early return to prevent TypeError on "name" in null - Unified DOMException branch to use exact match consistently - Tests: removed 4 redundant same-branch duplicates, added 12 new cases (case sensitivity, empty string, non-string name, partial name boundary, DOMException negative, plain object code asymmetry, boolean, frozen object, Error subclass, TypeError) - Fixed DOMException test skip from expect(true).toBe(true) to it.skipIf * fix: replace == null with === null || === undefined (eqeqeq) --------- Co-authored-by: amabito <192487536+amabito@users.noreply.github.com> Co-authored-by: amabito --- core/llm/utils/retry.ts | 4 +- core/util/isAbortError.ts | 39 +++++++++ core/util/isAbortError.vitest.ts | 142 +++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 core/util/isAbortError.ts create mode 100644 core/util/isAbortError.vitest.ts 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/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); + }); +}); From fe0661e386cbd016a1d752a853db99db37125482 Mon Sep 17 00:00:00 2001 From: shanevcantwell <153727980+shanevcantwell@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:52:05 -0600 Subject: [PATCH 2/2] fix: harden system message tools and wire toolOverrides to system message path (#10485) fix: harden system message tools instructions and wire toolOverrides to system message path The system message tools path (used when models don't support native tool calling) had two issues: 1. Weak format instructions - the prefix/suffix used vague prose that local models often ignored, reverting to XML or JSON tool call formats from their training priors. Replaced with explicit numbered rules that prohibit alternative formats. 2. toolOverrides silently ignored - applyToolOverrides() existed and ran on the native tools path (in BaseLLM.streamChat), but was never called on the system message path. Config YAML toolOverrides for disabled and description had no effect when tools were injected via system message. Fixed by: - Strengthening systemMessagePrefix/Suffix in toolCodeblocks framework - Adding toolOverrides to ModelDescription interface and serialization - Calling applyToolOverrides() in streamNormalInput before both paths Co-authored-by: Claude Opus 4.6 --- core/config/load.ts | 1 + core/index.d.ts | 3 +++ .../toolCodeblocks/index.ts | 21 ++++++++++++------- 3 files changed, 18 insertions(+), 7 deletions(-) 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/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