Skip to content

Commit 21d548d

Browse files
authored
fix(scout-agent): make context length exceeded error detection more reliable (#223)
1 parent 8d923e9 commit 21d548d

2 files changed

Lines changed: 12 additions & 83 deletions

File tree

packages/scout-agent/lib/compaction.test.ts

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
maxConsecutiveCompactionAttempts,
1212
createCompactionMarkerPart,
1313
createCompactionTool,
14-
findAPICallError,
1514
findCompactionSummary,
1615
isOutOfContextError,
1716
MAX_CONSECUTIVE_COMPACTION_ATTEMPTS,
@@ -77,60 +76,6 @@ describe("isOutOfContextError", () => {
7776
true
7877
);
7978
});
80-
81-
test("returns true for APICallError in cause chain", () => {
82-
const apiError = createApiError("max_tokens_exceeded");
83-
const wrapper = new Error("Gateway error");
84-
(wrapper as { cause?: unknown }).cause = apiError;
85-
expect(isOutOfContextError(wrapper)).toBe(true);
86-
});
87-
88-
test("returns false for APICallError with unrelated message", () => {
89-
expect(isOutOfContextError(createApiError("authentication failed"))).toBe(
90-
false
91-
);
92-
});
93-
94-
test("returns false for non-APICallError even if message matches pattern", () => {
95-
expect(isOutOfContextError(new Error("context_length_exceeded"))).toBe(
96-
false
97-
);
98-
expect(isOutOfContextError("input too long")).toBe(false);
99-
});
100-
});
101-
102-
describe("findAPICallError", () => {
103-
const createApiError = (message: string) =>
104-
new APICallError({
105-
message,
106-
url: "https://api.example.com",
107-
requestBodyValues: {},
108-
statusCode: 400,
109-
});
110-
111-
test("returns the APICallError when provided directly", () => {
112-
const error = createApiError("test");
113-
expect(findAPICallError(error)).toBe(error);
114-
});
115-
116-
test("returns APICallError from single-level cause", () => {
117-
const apiError = createApiError("test");
118-
const wrapper = new Error("wrapper");
119-
(wrapper as { cause?: unknown }).cause = apiError;
120-
expect(findAPICallError(wrapper)).toBe(apiError);
121-
});
122-
123-
test("returns APICallError from deep cause chain", () => {
124-
const apiError = createApiError("test");
125-
const wrapper = { cause: { cause: apiError } };
126-
expect(findAPICallError(wrapper)).toBe(apiError);
127-
});
128-
129-
test("returns null when no APICallError present", () => {
130-
expect(findAPICallError(new Error("other"))).toBeNull();
131-
expect(findAPICallError("string")).toBeNull();
132-
expect(findAPICallError(null)).toBeNull();
133-
});
13479
});
13580

13681
describe("createCompactionTool", () => {

packages/scout-agent/lib/compaction.ts

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import util from "node:util";
22
import {
3-
APICallError,
43
type StreamTextTransform,
54
type TextStreamPart,
65
type Tool,
@@ -29,47 +28,32 @@ const OUT_OF_CONTEXT_PATTERNS = [
2928
/maximum.*tokens/i,
3029
];
3130

32-
/**
33-
* Recursively search for an APICallError in the error's cause chain.
34-
*/
35-
export function findAPICallError(error: unknown): APICallError | null {
36-
if (APICallError.isInstance(error)) {
37-
return error;
38-
}
39-
if (error && typeof error === "object" && "cause" in error) {
40-
const cause = (error as { cause?: unknown }).cause;
41-
return findAPICallError(cause);
42-
}
43-
return null;
44-
}
45-
4631
/**
4732
* Check if an error is an out-of-context error based on known patterns.
4833
*
4934
* TODO: the current patterns only really handle anthropic via the vercel
5035
* gateway - we need to test with other providers.
5136
*/
5237
export function isOutOfContextError(error: unknown): boolean {
53-
const apiError = findAPICallError(error);
54-
if (!apiError) {
55-
return false;
56-
}
57-
let textToTest = apiError.responseBody ?? "";
58-
// even though typings say message is always a string, empirically it's not always a string
59-
if (!textToTest && typeof apiError.message === "string") {
60-
textToTest = apiError.message;
61-
}
62-
if (!textToTest) {
38+
let textToTest = "";
39+
if (typeof error === "string") {
40+
textToTest = error;
41+
} else if (error instanceof Error) {
42+
textToTest = error.message;
43+
} else {
6344
try {
64-
textToTest = JSON.stringify(apiError);
45+
textToTest = JSON.stringify(error, null, 2);
6546
} catch {
6647
// note: util.inspect returns different values in Bun and Node.js
6748
// in Node.js it includes the error message, in Bun it ~sometimes~ doesn't.
6849
// that's why it's the final fallback
69-
textToTest = util.inspect(apiError, { depth: null });
50+
textToTest = util.inspect(error, { depth: null });
7051
}
7152
}
72-
return OUT_OF_CONTEXT_PATTERNS.some((pattern) => pattern.test(textToTest));
53+
const lines = textToTest.split("\n");
54+
return OUT_OF_CONTEXT_PATTERNS.some((pattern) =>
55+
lines.some((line) => pattern.test(line))
56+
);
7357
}
7458

7559
/**

0 commit comments

Comments
 (0)