@illuma-ai/agents 1.3.0 → 1.3.1

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/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';
@@ -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;;;;"}
@@ -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;
@@ -12,3 +12,4 @@ export * from './contextPressure';
12
12
  export * from './toolDiscoveryCache';
13
13
  export * from './pruneCalibration';
14
14
  export * from './fileManifest';
15
+ export * from './errors';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illuma-ai/agents",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -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
- // Check if this is a "input too long" error from Bedrock/Anthropic
2152
- const errorMessage = (primaryError as Error).message.toLowerCase();
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.toLowerCase();
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(
@@ -0,0 +1,136 @@
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(
74
+ isContextOverflowError('Error code 413: request_too_large')
75
+ ).toBe(true);
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(false);
135
+ });
136
+ });
@@ -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
+ }
@@ -12,3 +12,4 @@ export * from './contextPressure';
12
12
  export * from './toolDiscoveryCache';
13
13
  export * from './pruneCalibration';
14
14
  export * from './fileManifest';
15
+ export * from './errors';