Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions core/config/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@
}
if (name === "llm") {
const llm = models.find((model) => model.title === params?.modelTitle);
if (!llm) {

Check warning on line 467 in core/config/load.ts

View workflow job for this annotation

GitHub Actions / core-checks

Unexpected negated condition
errors.push({
fatal: false,
message: `Unknown reranking model ${params?.modelTitle}`,
Expand Down Expand Up @@ -560,7 +560,7 @@
id: `continue-mcp-server-${index + 1}`,
name: `MCP Server`,
requestOptions: mergeConfigYamlRequestOptions(
server.transport.type !== "stdio"

Check warning on line 563 in core/config/load.ts

View workflow job for this annotation

GitHub Actions / core-checks

Unexpected negated condition
? server.transport.requestOptions
: undefined,
config.requestOptions,
Expand Down Expand Up @@ -651,6 +651,7 @@
envSecretLocations: llm.envSecretLocations,
sourceFile: llm.sourceFile,
isFromAutoDetect: llm.isFromAutoDetect,
toolOverrides: llm.toolOverrides,
};
}

Expand Down
3 changes: 3 additions & 0 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,9 @@ export interface ModelDescription {

sourceFile?: string;
isFromAutoDetect?: boolean;

/** Tool overrides for this model */
toolOverrides?: ToolOverride[];
}

export interface JSONEmbedOptions {
Expand Down
4 changes: 3 additions & 1 deletion core/llm/utils/retry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isAbortError } from "../../util/isAbortError.js";

/**
* Configuration options for the retry decorator
*/
Expand Down Expand Up @@ -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;
}

Expand Down
21 changes: 14 additions & 7 deletions core/tools/systemMessageTools/toolCodeblocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tool_call> or <function=...>.
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
Expand Down
39 changes: 39 additions & 0 deletions core/util/isAbortError.ts
Original file line number Diff line number Diff line change
@@ -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;
}
142 changes: 142 additions & 0 deletions core/util/isAbortError.vitest.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading