@illuma-ai/agents 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +5 -12
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/utils/errors.cjs +113 -0
- package/dist/cjs/utils/errors.cjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +5 -12
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/utils/errors.mjs +109 -0
- package/dist/esm/utils/errors.mjs.map +1 -0
- package/dist/types/types/stream.d.ts +10 -0
- package/dist/types/utils/errors.d.ts +37 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +5 -14
- package/src/types/stream.ts +10 -0
- package/src/utils/__tests__/errors.test.ts +138 -0
- package/src/utils/errors.ts +115 -0
- package/src/utils/index.ts +1 -0
package/dist/esm/main.mjs
CHANGED
|
@@ -60,6 +60,7 @@ export { buildMultiDocHintContent, buildPostPruneNote, detectDocuments, hasTaskT
|
|
|
60
60
|
export { ToolDiscoveryCache } from './utils/toolDiscoveryCache.mjs';
|
|
61
61
|
export { applyCalibration, createPruneCalibration, updatePruneCalibration } from './utils/pruneCalibration.mjs';
|
|
62
62
|
export { FILE_MANIFEST_PREFIX, buildFileManifestBlock } from './utils/fileManifest.mjs';
|
|
63
|
+
export { extractErrorMessage, isContextOverflowError, isLikelyContextOverflowError } from './utils/errors.mjs';
|
|
63
64
|
export { CustomOpenAIClient } from './llm/openai/index.mjs';
|
|
64
65
|
export { ChatOpenRouter } from './llm/openrouter/index.mjs';
|
|
65
66
|
export { getChatModelClass, llmProviders } from './llm/providers.mjs';
|
package/dist/esm/main.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"main.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-overflow error detection helpers.
|
|
3
|
+
*
|
|
4
|
+
* Provider error messages vary — Anthropic returns "prompt is too long",
|
|
5
|
+
* OpenAI returns "context_length_exceeded", Bedrock returns "Input is too
|
|
6
|
+
* long", Google returns a size-limit phrase. This module centralises the
|
|
7
|
+
* phrase list so the agent graph's emergency-prune retry can classify
|
|
8
|
+
* errors consistently instead of duplicating inline substring matches at
|
|
9
|
+
* each call site.
|
|
10
|
+
*
|
|
11
|
+
* The strict check (`isContextOverflowError`) matches only known phrases.
|
|
12
|
+
* The loose check (`isLikelyContextOverflowError`) also matches a heuristic
|
|
13
|
+
* regex for providers we haven't explicitly catalogued. Both filter out
|
|
14
|
+
* false positives (rate-limit, auth, quota, billing) that might otherwise
|
|
15
|
+
* trigger an unnecessary prune retry.
|
|
16
|
+
*/
|
|
17
|
+
const CONTEXT_OVERFLOW_PHRASES = [
|
|
18
|
+
'request_too_large',
|
|
19
|
+
'context length exceeded',
|
|
20
|
+
'maximum context length',
|
|
21
|
+
'prompt is too long',
|
|
22
|
+
'exceeds model context window',
|
|
23
|
+
'exceeds the model',
|
|
24
|
+
'too large for model',
|
|
25
|
+
'context_length_exceeded',
|
|
26
|
+
'max_tokens',
|
|
27
|
+
'token limit',
|
|
28
|
+
'input too long',
|
|
29
|
+
'input is too long',
|
|
30
|
+
'payload too large',
|
|
31
|
+
'content_too_large',
|
|
32
|
+
];
|
|
33
|
+
const CONTEXT_OVERFLOW_HINT_RE = /413|too large|too long|context.*exceed|exceed.*context|token.*limit|limit.*token|prompt.*size|size.*limit|maximum.*length|length.*maximum/i;
|
|
34
|
+
const FALSE_POSITIVE_RE = /rate.?limit|too many requests|quota|billing|auth|permission|forbidden/i;
|
|
35
|
+
/**
|
|
36
|
+
* Extracts a human-readable error message from an unknown error value.
|
|
37
|
+
* Walks common shapes: strings, Error instances, `{ message }`,
|
|
38
|
+
* `{ error: string }`, `{ error: { message } }`. Falls back to
|
|
39
|
+
* JSON.stringify or String() so callers never have to null-check.
|
|
40
|
+
*/
|
|
41
|
+
function extractErrorMessage(error) {
|
|
42
|
+
if (error == null) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
if (typeof error === 'string') {
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
return error.message;
|
|
50
|
+
}
|
|
51
|
+
if (typeof error === 'object') {
|
|
52
|
+
const record = error;
|
|
53
|
+
if (typeof record.message === 'string') {
|
|
54
|
+
return record.message;
|
|
55
|
+
}
|
|
56
|
+
if (typeof record.error === 'string') {
|
|
57
|
+
return record.error;
|
|
58
|
+
}
|
|
59
|
+
if (typeof record.error === 'object' &&
|
|
60
|
+
record.error != null &&
|
|
61
|
+
typeof record.error.message === 'string') {
|
|
62
|
+
return record.error.message;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(error);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return String(error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Strict check: returns true only for known, unambiguous context-overflow
|
|
74
|
+
* phrases. Use when the recovery action is expensive (full prune + retry)
|
|
75
|
+
* and false positives are undesirable.
|
|
76
|
+
*/
|
|
77
|
+
function isContextOverflowError(errorMessage) {
|
|
78
|
+
if (!errorMessage) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const lower = errorMessage.toLowerCase();
|
|
82
|
+
if (FALSE_POSITIVE_RE.test(lower)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return CONTEXT_OVERFLOW_PHRASES.some((phrase) => lower.includes(phrase));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Loose check: returns true for known phrases OR heuristic regex matches.
|
|
89
|
+
* Preferred by the graph's emergency-prune retry because the cost of a
|
|
90
|
+
* false positive is one extra retry with a smaller context, while the
|
|
91
|
+
* cost of a false negative is an opaque provider failure surfaced to
|
|
92
|
+
* the user.
|
|
93
|
+
*/
|
|
94
|
+
function isLikelyContextOverflowError(errorMessage) {
|
|
95
|
+
if (!errorMessage) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (isContextOverflowError(errorMessage)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
const lower = errorMessage.toLowerCase();
|
|
102
|
+
if (FALSE_POSITIVE_RE.test(lower)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return CONTEXT_OVERFLOW_HINT_RE.test(lower);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { extractErrorMessage, isContextOverflowError, isLikelyContextOverflowError };
|
|
109
|
+
//# sourceMappingURL=errors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.mjs","sources":["../../../src/utils/errors.ts"],"sourcesContent":["/**\n * Context-overflow error detection helpers.\n *\n * Provider error messages vary — Anthropic returns \"prompt is too long\",\n * OpenAI returns \"context_length_exceeded\", Bedrock returns \"Input is too\n * long\", Google returns a size-limit phrase. This module centralises the\n * phrase list so the agent graph's emergency-prune retry can classify\n * errors consistently instead of duplicating inline substring matches at\n * each call site.\n *\n * The strict check (`isContextOverflowError`) matches only known phrases.\n * The loose check (`isLikelyContextOverflowError`) also matches a heuristic\n * regex for providers we haven't explicitly catalogued. Both filter out\n * false positives (rate-limit, auth, quota, billing) that might otherwise\n * trigger an unnecessary prune retry.\n */\n\nconst CONTEXT_OVERFLOW_PHRASES = [\n 'request_too_large',\n 'context length exceeded',\n 'maximum context length',\n 'prompt is too long',\n 'exceeds model context window',\n 'exceeds the model',\n 'too large for model',\n 'context_length_exceeded',\n 'max_tokens',\n 'token limit',\n 'input too long',\n 'input is too long',\n 'payload too large',\n 'content_too_large',\n] as const;\n\nconst CONTEXT_OVERFLOW_HINT_RE =\n /413|too large|too long|context.*exceed|exceed.*context|token.*limit|limit.*token|prompt.*size|size.*limit|maximum.*length|length.*maximum/i;\n\nconst FALSE_POSITIVE_RE =\n /rate.?limit|too many requests|quota|billing|auth|permission|forbidden/i;\n\n/**\n * Extracts a human-readable error message from an unknown error value.\n * Walks common shapes: strings, Error instances, `{ message }`,\n * `{ error: string }`, `{ error: { message } }`. Falls back to\n * JSON.stringify or String() so callers never have to null-check.\n */\nexport function extractErrorMessage(error: unknown): string {\n if (error == null) {\n return '';\n }\n if (typeof error === 'string') {\n return error;\n }\n if (error instanceof Error) {\n return error.message;\n }\n if (typeof error === 'object') {\n const record = error as Record<string, unknown>;\n if (typeof record.message === 'string') {\n return record.message;\n }\n if (typeof record.error === 'string') {\n return record.error;\n }\n if (\n typeof record.error === 'object' &&\n record.error != null &&\n typeof (record.error as Record<string, unknown>).message === 'string'\n ) {\n return (record.error as Record<string, unknown>).message as string;\n }\n }\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n}\n\n/**\n * Strict check: returns true only for known, unambiguous context-overflow\n * phrases. Use when the recovery action is expensive (full prune + retry)\n * and false positives are undesirable.\n */\nexport function isContextOverflowError(errorMessage?: string): boolean {\n if (!errorMessage) {\n return false;\n }\n const lower = errorMessage.toLowerCase();\n if (FALSE_POSITIVE_RE.test(lower)) {\n return false;\n }\n return CONTEXT_OVERFLOW_PHRASES.some((phrase) => lower.includes(phrase));\n}\n\n/**\n * Loose check: returns true for known phrases OR heuristic regex matches.\n * Preferred by the graph's emergency-prune retry because the cost of a\n * false positive is one extra retry with a smaller context, while the\n * cost of a false negative is an opaque provider failure surfaced to\n * the user.\n */\nexport function isLikelyContextOverflowError(errorMessage?: string): boolean {\n if (!errorMessage) {\n return false;\n }\n if (isContextOverflowError(errorMessage)) {\n return true;\n }\n const lower = errorMessage.toLowerCase();\n if (FALSE_POSITIVE_RE.test(lower)) {\n return false;\n }\n return CONTEXT_OVERFLOW_HINT_RE.test(lower);\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;AAeG;AAEH,MAAM,wBAAwB,GAAG;IAC/B,mBAAmB;IACnB,yBAAyB;IACzB,wBAAwB;IACxB,oBAAoB;IACpB,8BAA8B;IAC9B,mBAAmB;IACnB,qBAAqB;IACrB,yBAAyB;IACzB,YAAY;IACZ,aAAa;IACb,gBAAgB;IAChB,mBAAmB;IACnB,mBAAmB;IACnB,mBAAmB;CACX;AAEV,MAAM,wBAAwB,GAC5B,4IAA4I;AAE9I,MAAM,iBAAiB,GACrB,wEAAwE;AAE1E;;;;;AAKG;AACG,SAAU,mBAAmB,CAAC,KAAc,EAAA;AAChD,IAAA,IAAI,KAAK,IAAI,IAAI,EAAE;AACjB,QAAA,OAAO,EAAE;IACX;AACA,IAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AAC7B,QAAA,OAAO,KAAK;IACd;AACA,IAAA,IAAI,KAAK,YAAY,KAAK,EAAE;QAC1B,OAAO,KAAK,CAAC,OAAO;IACtB;AACA,IAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QAC7B,MAAM,MAAM,GAAG,KAAgC;AAC/C,QAAA,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE;YACtC,OAAO,MAAM,CAAC,OAAO;QACvB;AACA,QAAA,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE;YACpC,OAAO,MAAM,CAAC,KAAK;QACrB;AACA,QAAA,IACE,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;YAChC,MAAM,CAAC,KAAK,IAAI,IAAI;YACpB,OAAQ,MAAM,CAAC,KAAiC,CAAC,OAAO,KAAK,QAAQ,EACrE;AACA,YAAA,OAAQ,MAAM,CAAC,KAAiC,CAAC,OAAiB;QACpE;IACF;AACA,IAAA,IAAI;AACF,QAAA,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;IAC9B;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,MAAM,CAAC,KAAK,CAAC;IACtB;AACF;AAEA;;;;AAIG;AACG,SAAU,sBAAsB,CAAC,YAAqB,EAAA;IAC1D,IAAI,CAAC,YAAY,EAAE;AACjB,QAAA,OAAO,KAAK;IACd;AACA,IAAA,MAAM,KAAK,GAAG,YAAY,CAAC,WAAW,EAAE;AACxC,IAAA,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;AACjC,QAAA,OAAO,KAAK;IACd;AACA,IAAA,OAAO,wBAAwB,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC1E;AAEA;;;;;;AAMG;AACG,SAAU,4BAA4B,CAAC,YAAqB,EAAA;IAChE,IAAI,CAAC,YAAY,EAAE;AACjB,QAAA,OAAO,KAAK;IACd;AACA,IAAA,IAAI,sBAAsB,CAAC,YAAY,CAAC,EAAE;AACxC,QAAA,OAAO,IAAI;IACb;AACA,IAAA,MAAM,KAAK,GAAG,YAAY,CAAC,WAAW,EAAE;AACxC,IAAA,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;AACjC,QAAA,OAAO,KAAK;IACd;AACA,IAAA,OAAO,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC;AAC7C;;;;"}
|
|
@@ -116,6 +116,16 @@ export type ToolCallsDetails = {
|
|
|
116
116
|
export type ToolCallDelta = {
|
|
117
117
|
type: StepTypes;
|
|
118
118
|
tool_calls?: ToolCallChunk[];
|
|
119
|
+
/**
|
|
120
|
+
* Auth URL for tool calls that require interactive authentication
|
|
121
|
+
* (typically OAuth-gated MCP tools). Hosts populate this on a delta
|
|
122
|
+
* dispatch when a tool invocation surfaces an auth challenge so the
|
|
123
|
+
* client can render an approval prompt without waiting for the call
|
|
124
|
+
* to complete.
|
|
125
|
+
*/
|
|
126
|
+
auth?: string;
|
|
127
|
+
/** Auth challenge expiration (UNIX seconds). Pairs with `auth`. */
|
|
128
|
+
expires_at?: number;
|
|
119
129
|
};
|
|
120
130
|
export type AgentToolCall = {
|
|
121
131
|
id: string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-overflow error detection helpers.
|
|
3
|
+
*
|
|
4
|
+
* Provider error messages vary — Anthropic returns "prompt is too long",
|
|
5
|
+
* OpenAI returns "context_length_exceeded", Bedrock returns "Input is too
|
|
6
|
+
* long", Google returns a size-limit phrase. This module centralises the
|
|
7
|
+
* phrase list so the agent graph's emergency-prune retry can classify
|
|
8
|
+
* errors consistently instead of duplicating inline substring matches at
|
|
9
|
+
* each call site.
|
|
10
|
+
*
|
|
11
|
+
* The strict check (`isContextOverflowError`) matches only known phrases.
|
|
12
|
+
* The loose check (`isLikelyContextOverflowError`) also matches a heuristic
|
|
13
|
+
* regex for providers we haven't explicitly catalogued. Both filter out
|
|
14
|
+
* false positives (rate-limit, auth, quota, billing) that might otherwise
|
|
15
|
+
* trigger an unnecessary prune retry.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Extracts a human-readable error message from an unknown error value.
|
|
19
|
+
* Walks common shapes: strings, Error instances, `{ message }`,
|
|
20
|
+
* `{ error: string }`, `{ error: { message } }`. Falls back to
|
|
21
|
+
* JSON.stringify or String() so callers never have to null-check.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractErrorMessage(error: unknown): string;
|
|
24
|
+
/**
|
|
25
|
+
* Strict check: returns true only for known, unambiguous context-overflow
|
|
26
|
+
* phrases. Use when the recovery action is expensive (full prune + retry)
|
|
27
|
+
* and false positives are undesirable.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isContextOverflowError(errorMessage?: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Loose check: returns true for known phrases OR heuristic regex matches.
|
|
32
|
+
* Preferred by the graph's emergency-prune retry because the cost of a
|
|
33
|
+
* false positive is one extra retry with a smaller context, while the
|
|
34
|
+
* cost of a false negative is an opaque provider failure surfaced to
|
|
35
|
+
* the user.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isLikelyContextOverflowError(errorMessage?: string): boolean;
|
package/package.json
CHANGED
package/src/graphs/Graph.ts
CHANGED
|
@@ -82,6 +82,7 @@ import { safeDispatchCustomEvent } from '@/utils/events';
|
|
|
82
82
|
import { mlog, mwarn } from '@/utils/logging';
|
|
83
83
|
import { normalizeMessageToolCalls } from '@/utils/toolCallNormalization';
|
|
84
84
|
import { isTruncationReason } from '@/utils/finishReasons';
|
|
85
|
+
import { isLikelyContextOverflowError } from '@/utils/errors';
|
|
85
86
|
import {
|
|
86
87
|
detectDocuments,
|
|
87
88
|
shouldInjectMultiDocHint,
|
|
@@ -2148,15 +2149,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2148
2149
|
config
|
|
2149
2150
|
);
|
|
2150
2151
|
} catch (primaryError) {
|
|
2151
|
-
|
|
2152
|
-
const
|
|
2153
|
-
const isInputTooLongError =
|
|
2154
|
-
errorMessage.includes('too long') ||
|
|
2155
|
-
errorMessage.includes('input is too long') ||
|
|
2156
|
-
errorMessage.includes('context length') ||
|
|
2157
|
-
errorMessage.includes('maximum context') ||
|
|
2158
|
-
errorMessage.includes('validationexception') ||
|
|
2159
|
-
errorMessage.includes('prompt is too long');
|
|
2152
|
+
const errorMessage = (primaryError as Error).message;
|
|
2153
|
+
const isInputTooLongError = isLikelyContextOverflowError(errorMessage);
|
|
2160
2154
|
|
|
2161
2155
|
// Log when we detect the error
|
|
2162
2156
|
if (isInputTooLongError) {
|
|
@@ -2298,11 +2292,8 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2298
2292
|
`[Graph] ✅ Retry successful at ${reductionFactor * 100}% with ${reducedMessages.length} messages (reduced from ${finalMessages.length})`
|
|
2299
2293
|
);
|
|
2300
2294
|
} catch (retryError) {
|
|
2301
|
-
const retryErrorMsg = (retryError as Error).message
|
|
2302
|
-
const stillTooLong =
|
|
2303
|
-
retryErrorMsg.includes('too long') ||
|
|
2304
|
-
retryErrorMsg.includes('context length') ||
|
|
2305
|
-
retryErrorMsg.includes('validationexception');
|
|
2295
|
+
const retryErrorMsg = (retryError as Error).message;
|
|
2296
|
+
const stillTooLong = isLikelyContextOverflowError(retryErrorMsg);
|
|
2306
2297
|
|
|
2307
2298
|
if (stillTooLong && reductionFactor > 0.1) {
|
|
2308
2299
|
mwarn(
|
package/src/types/stream.ts
CHANGED
|
@@ -164,6 +164,16 @@ export type ToolCallsDetails = {
|
|
|
164
164
|
export type ToolCallDelta = {
|
|
165
165
|
type: StepTypes;
|
|
166
166
|
tool_calls?: ToolCallChunk[]; // #new
|
|
167
|
+
/**
|
|
168
|
+
* Auth URL for tool calls that require interactive authentication
|
|
169
|
+
* (typically OAuth-gated MCP tools). Hosts populate this on a delta
|
|
170
|
+
* dispatch when a tool invocation surfaces an auth challenge so the
|
|
171
|
+
* client can render an approval prompt without waiting for the call
|
|
172
|
+
* to complete.
|
|
173
|
+
*/
|
|
174
|
+
auth?: string;
|
|
175
|
+
/** Auth challenge expiration (UNIX seconds). Pairs with `auth`. */
|
|
176
|
+
expires_at?: number;
|
|
167
177
|
};
|
|
168
178
|
|
|
169
179
|
export type AgentToolCall =
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for context-overflow error classification.
|
|
3
|
+
*
|
|
4
|
+
* The graph's emergency-prune retry relies on these helpers to decide
|
|
5
|
+
* whether a provider failure warrants a truncated retry. False positives
|
|
6
|
+
* cost one extra retry; false negatives surface an opaque failure to the
|
|
7
|
+
* user. Both are cheaper than the previous inline substring matching,
|
|
8
|
+
* which missed phrases like "request_too_large" (Anthropic 429-adjacent)
|
|
9
|
+
* and could falsely trigger on rate-limit errors mentioning "too many".
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
extractErrorMessage,
|
|
13
|
+
isContextOverflowError,
|
|
14
|
+
isLikelyContextOverflowError,
|
|
15
|
+
} from '../errors';
|
|
16
|
+
|
|
17
|
+
describe('extractErrorMessage', () => {
|
|
18
|
+
it('returns empty string for null/undefined', () => {
|
|
19
|
+
expect(extractErrorMessage(null)).toBe('');
|
|
20
|
+
expect(extractErrorMessage(undefined)).toBe('');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns the string directly', () => {
|
|
24
|
+
expect(extractErrorMessage('something broke')).toBe('something broke');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('reads Error.message', () => {
|
|
28
|
+
expect(extractErrorMessage(new Error('boom'))).toBe('boom');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('reads plain-object message/error fields', () => {
|
|
32
|
+
expect(extractErrorMessage({ message: 'm' })).toBe('m');
|
|
33
|
+
expect(extractErrorMessage({ error: 'e' })).toBe('e');
|
|
34
|
+
expect(extractErrorMessage({ error: { message: 'nested' } })).toBe(
|
|
35
|
+
'nested'
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('falls back to JSON stringify for unknown shapes', () => {
|
|
40
|
+
expect(extractErrorMessage({ status: 500 })).toBe('{"status":500}');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('isContextOverflowError (strict)', () => {
|
|
45
|
+
it('returns false for empty input', () => {
|
|
46
|
+
expect(isContextOverflowError()).toBe(false);
|
|
47
|
+
expect(isContextOverflowError('')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('matches Anthropic prompt-too-long', () => {
|
|
51
|
+
expect(
|
|
52
|
+
isContextOverflowError('prompt is too long: 250000 tokens > 200000')
|
|
53
|
+
).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('matches OpenAI context_length_exceeded', () => {
|
|
57
|
+
expect(
|
|
58
|
+
isContextOverflowError(
|
|
59
|
+
"This model's maximum context length is 128000 tokens. context_length_exceeded"
|
|
60
|
+
)
|
|
61
|
+
).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('matches Bedrock input-too-long', () => {
|
|
65
|
+
expect(
|
|
66
|
+
isContextOverflowError(
|
|
67
|
+
'ValidationException: Input is too long for requested model.'
|
|
68
|
+
)
|
|
69
|
+
).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('matches request_too_large', () => {
|
|
73
|
+
expect(isContextOverflowError('Error code 413: request_too_large')).toBe(
|
|
74
|
+
true
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('is case-insensitive', () => {
|
|
79
|
+
expect(isContextOverflowError('PROMPT IS TOO LONG')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects rate-limit errors even if they mention "too many"', () => {
|
|
83
|
+
expect(
|
|
84
|
+
isContextOverflowError('429 rate_limit_exceeded: too many requests')
|
|
85
|
+
).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('rejects auth / billing errors', () => {
|
|
89
|
+
expect(isContextOverflowError('insufficient quota on billing plan')).toBe(
|
|
90
|
+
false
|
|
91
|
+
);
|
|
92
|
+
expect(isContextOverflowError('forbidden: missing permission')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not match loose phrases like bare "too long"', () => {
|
|
96
|
+
// Strict check should NOT fire on just "too long" — that's for the
|
|
97
|
+
// loose variant. Keeps the retry budget tight.
|
|
98
|
+
expect(isContextOverflowError('the response was too long')).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('isLikelyContextOverflowError (loose)', () => {
|
|
103
|
+
it('matches everything the strict check matches', () => {
|
|
104
|
+
expect(isLikelyContextOverflowError('prompt is too long')).toBe(true);
|
|
105
|
+
expect(isLikelyContextOverflowError('context_length_exceeded')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('matches heuristic regex: bare "too long"', () => {
|
|
109
|
+
expect(isLikelyContextOverflowError('response was too long')).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('matches heuristic regex: 413 status code', () => {
|
|
113
|
+
expect(isLikelyContextOverflowError('HTTP 413 payload')).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('matches "context ... exceed" in either order', () => {
|
|
117
|
+
expect(isLikelyContextOverflowError('your context exceeds limits')).toBe(
|
|
118
|
+
true
|
|
119
|
+
);
|
|
120
|
+
expect(isLikelyContextOverflowError('exceeds context window')).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('still rejects rate-limit / auth even on loose match', () => {
|
|
124
|
+
expect(
|
|
125
|
+
isLikelyContextOverflowError('rate limit: too many requests queued')
|
|
126
|
+
).toBe(false);
|
|
127
|
+
expect(
|
|
128
|
+
isLikelyContextOverflowError('authorization exceeds allowed quota')
|
|
129
|
+
).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns false for unrelated errors', () => {
|
|
133
|
+
expect(isLikelyContextOverflowError('ECONNREFUSED')).toBe(false);
|
|
134
|
+
expect(isLikelyContextOverflowError('unexpected token in JSON')).toBe(
|
|
135
|
+
false
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-overflow error detection helpers.
|
|
3
|
+
*
|
|
4
|
+
* Provider error messages vary — Anthropic returns "prompt is too long",
|
|
5
|
+
* OpenAI returns "context_length_exceeded", Bedrock returns "Input is too
|
|
6
|
+
* long", Google returns a size-limit phrase. This module centralises the
|
|
7
|
+
* phrase list so the agent graph's emergency-prune retry can classify
|
|
8
|
+
* errors consistently instead of duplicating inline substring matches at
|
|
9
|
+
* each call site.
|
|
10
|
+
*
|
|
11
|
+
* The strict check (`isContextOverflowError`) matches only known phrases.
|
|
12
|
+
* The loose check (`isLikelyContextOverflowError`) also matches a heuristic
|
|
13
|
+
* regex for providers we haven't explicitly catalogued. Both filter out
|
|
14
|
+
* false positives (rate-limit, auth, quota, billing) that might otherwise
|
|
15
|
+
* trigger an unnecessary prune retry.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const CONTEXT_OVERFLOW_PHRASES = [
|
|
19
|
+
'request_too_large',
|
|
20
|
+
'context length exceeded',
|
|
21
|
+
'maximum context length',
|
|
22
|
+
'prompt is too long',
|
|
23
|
+
'exceeds model context window',
|
|
24
|
+
'exceeds the model',
|
|
25
|
+
'too large for model',
|
|
26
|
+
'context_length_exceeded',
|
|
27
|
+
'max_tokens',
|
|
28
|
+
'token limit',
|
|
29
|
+
'input too long',
|
|
30
|
+
'input is too long',
|
|
31
|
+
'payload too large',
|
|
32
|
+
'content_too_large',
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
const CONTEXT_OVERFLOW_HINT_RE =
|
|
36
|
+
/413|too large|too long|context.*exceed|exceed.*context|token.*limit|limit.*token|prompt.*size|size.*limit|maximum.*length|length.*maximum/i;
|
|
37
|
+
|
|
38
|
+
const FALSE_POSITIVE_RE =
|
|
39
|
+
/rate.?limit|too many requests|quota|billing|auth|permission|forbidden/i;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extracts a human-readable error message from an unknown error value.
|
|
43
|
+
* Walks common shapes: strings, Error instances, `{ message }`,
|
|
44
|
+
* `{ error: string }`, `{ error: { message } }`. Falls back to
|
|
45
|
+
* JSON.stringify or String() so callers never have to null-check.
|
|
46
|
+
*/
|
|
47
|
+
export function extractErrorMessage(error: unknown): string {
|
|
48
|
+
if (error == null) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
if (typeof error === 'string') {
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
return error.message;
|
|
56
|
+
}
|
|
57
|
+
if (typeof error === 'object') {
|
|
58
|
+
const record = error as Record<string, unknown>;
|
|
59
|
+
if (typeof record.message === 'string') {
|
|
60
|
+
return record.message;
|
|
61
|
+
}
|
|
62
|
+
if (typeof record.error === 'string') {
|
|
63
|
+
return record.error;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
typeof record.error === 'object' &&
|
|
67
|
+
record.error != null &&
|
|
68
|
+
typeof (record.error as Record<string, unknown>).message === 'string'
|
|
69
|
+
) {
|
|
70
|
+
return (record.error as Record<string, unknown>).message as string;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return JSON.stringify(error);
|
|
75
|
+
} catch {
|
|
76
|
+
return String(error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Strict check: returns true only for known, unambiguous context-overflow
|
|
82
|
+
* phrases. Use when the recovery action is expensive (full prune + retry)
|
|
83
|
+
* and false positives are undesirable.
|
|
84
|
+
*/
|
|
85
|
+
export function isContextOverflowError(errorMessage?: string): boolean {
|
|
86
|
+
if (!errorMessage) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const lower = errorMessage.toLowerCase();
|
|
90
|
+
if (FALSE_POSITIVE_RE.test(lower)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return CONTEXT_OVERFLOW_PHRASES.some((phrase) => lower.includes(phrase));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Loose check: returns true for known phrases OR heuristic regex matches.
|
|
98
|
+
* Preferred by the graph's emergency-prune retry because the cost of a
|
|
99
|
+
* false positive is one extra retry with a smaller context, while the
|
|
100
|
+
* cost of a false negative is an opaque provider failure surfaced to
|
|
101
|
+
* the user.
|
|
102
|
+
*/
|
|
103
|
+
export function isLikelyContextOverflowError(errorMessage?: string): boolean {
|
|
104
|
+
if (!errorMessage) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (isContextOverflowError(errorMessage)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
const lower = errorMessage.toLowerCase();
|
|
111
|
+
if (FALSE_POSITIVE_RE.test(lower)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return CONTEXT_OVERFLOW_HINT_RE.test(lower);
|
|
115
|
+
}
|
package/src/utils/index.ts
CHANGED