@librechat/agents 3.2.68 → 3.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.
Files changed (159) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +117 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +4 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +243 -49
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +103 -28
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  10. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/index.cjs +11 -2
  12. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  13. package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
  14. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
  15. package/dist/cjs/llm/google/index.cjs +1 -1
  16. package/dist/cjs/llm/invoke.cjs +60 -3
  17. package/dist/cjs/llm/invoke.cjs.map +1 -1
  18. package/dist/cjs/llm/openai/index.cjs +1 -1
  19. package/dist/cjs/main.cjs +21 -7
  20. package/dist/cjs/messages/format.cjs +136 -4
  21. package/dist/cjs/messages/format.cjs.map +1 -1
  22. package/dist/cjs/messages/prune.cjs +16 -5
  23. package/dist/cjs/messages/prune.cjs.map +1 -1
  24. package/dist/cjs/messages/recency.cjs +2 -0
  25. package/dist/cjs/messages/recency.cjs.map +1 -1
  26. package/dist/cjs/prompts/activityLabel.cjs +101 -0
  27. package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
  28. package/dist/cjs/run.cjs +177 -3
  29. package/dist/cjs/run.cjs.map +1 -1
  30. package/dist/cjs/stream.cjs +205 -49
  31. package/dist/cjs/stream.cjs.map +1 -1
  32. package/dist/cjs/summarization/node.cjs +55 -7
  33. package/dist/cjs/summarization/node.cjs.map +1 -1
  34. package/dist/cjs/tools/BashExecutor.cjs +2 -2
  35. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  36. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
  37. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  38. package/dist/cjs/tools/CodeExecutor.cjs +69 -8
  39. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  40. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
  41. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  42. package/dist/cjs/tools/ToolNode.cjs +32 -3
  43. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  44. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  45. package/dist/cjs/utils/errors.cjs +317 -52
  46. package/dist/cjs/utils/errors.cjs.map +1 -1
  47. package/dist/esm/agents/AgentContext.mjs +118 -4
  48. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  49. package/dist/esm/common/enum.mjs +4 -0
  50. package/dist/esm/common/enum.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +242 -48
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +103 -28
  54. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  55. package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
  56. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  57. package/dist/esm/llm/bedrock/index.mjs +11 -2
  58. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  59. package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
  60. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
  61. package/dist/esm/llm/google/index.mjs +1 -1
  62. package/dist/esm/llm/invoke.mjs +59 -4
  63. package/dist/esm/llm/invoke.mjs.map +1 -1
  64. package/dist/esm/llm/openai/index.mjs +1 -1
  65. package/dist/esm/main.mjs +10 -10
  66. package/dist/esm/messages/format.mjs +136 -5
  67. package/dist/esm/messages/format.mjs.map +1 -1
  68. package/dist/esm/messages/prune.mjs +14 -6
  69. package/dist/esm/messages/prune.mjs.map +1 -1
  70. package/dist/esm/messages/recency.mjs +2 -1
  71. package/dist/esm/messages/recency.mjs.map +1 -1
  72. package/dist/esm/prompts/activityLabel.mjs +100 -0
  73. package/dist/esm/prompts/activityLabel.mjs.map +1 -0
  74. package/dist/esm/run.mjs +178 -4
  75. package/dist/esm/run.mjs.map +1 -1
  76. package/dist/esm/stream.mjs +205 -49
  77. package/dist/esm/stream.mjs.map +1 -1
  78. package/dist/esm/summarization/node.mjs +55 -7
  79. package/dist/esm/summarization/node.mjs.map +1 -1
  80. package/dist/esm/tools/BashExecutor.mjs +3 -3
  81. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  82. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
  83. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  84. package/dist/esm/tools/CodeExecutor.mjs +62 -9
  85. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  86. package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
  87. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +32 -3
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  91. package/dist/esm/utils/errors.mjs +317 -53
  92. package/dist/esm/utils/errors.mjs.map +1 -1
  93. package/dist/types/agents/AgentContext.d.ts +62 -3
  94. package/dist/types/common/enum.d.ts +5 -1
  95. package/dist/types/graphs/Graph.d.ts +16 -2
  96. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  97. package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
  98. package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
  99. package/dist/types/llm/invoke.d.ts +35 -6
  100. package/dist/types/messages/format.d.ts +22 -0
  101. package/dist/types/messages/prune.d.ts +10 -2
  102. package/dist/types/messages/recency.d.ts +1 -0
  103. package/dist/types/prompts/activityLabel.d.ts +31 -0
  104. package/dist/types/run.d.ts +16 -0
  105. package/dist/types/tools/CodeExecutor.d.ts +14 -1
  106. package/dist/types/types/activityLabel.d.ts +53 -0
  107. package/dist/types/types/index.d.ts +1 -0
  108. package/dist/types/types/llm.d.ts +7 -4
  109. package/dist/types/types/stream.d.ts +7 -4
  110. package/dist/types/types/summarize.d.ts +22 -0
  111. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
  112. package/dist/types/utils/errors.d.ts +65 -16
  113. package/dist/types/utils/redactSecrets.d.ts +3 -0
  114. package/package.json +7 -8
  115. package/src/agents/AgentContext.ts +188 -7
  116. package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
  117. package/src/common/enum.ts +4 -0
  118. package/src/graphs/Graph.ts +409 -58
  119. package/src/graphs/MultiAgentGraph.ts +184 -46
  120. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
  121. package/src/langfuseToolOutputTracing.ts +4 -1
  122. package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
  123. package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
  124. package/src/llm/bedrock/index.ts +25 -12
  125. package/src/llm/contextOverflowRecovery.ts +292 -0
  126. package/src/llm/invoke.ts +119 -4
  127. package/src/messages/foldToollessToolBlocks.test.ts +438 -0
  128. package/src/messages/format.ts +233 -5
  129. package/src/messages/prune.ts +24 -11
  130. package/src/messages/recency.ts +3 -1
  131. package/src/prompts/activityLabel.ts +177 -0
  132. package/src/run.ts +322 -3
  133. package/src/scripts/context-overflow-probe.ts +997 -0
  134. package/src/specs/activity-label-prompt.test.ts +128 -0
  135. package/src/specs/activity-label-trace-seed.test.ts +47 -0
  136. package/src/specs/agent-handoffs.test.ts +903 -1
  137. package/src/specs/bedrock-toolless.live.test.ts +123 -0
  138. package/src/specs/context-overflow-recovery.live.test.ts +213 -0
  139. package/src/splitStream.test.ts +882 -0
  140. package/src/stream.ts +315 -51
  141. package/src/summarization/__tests__/aggregator.test.ts +83 -0
  142. package/src/summarization/__tests__/node.test.ts +139 -0
  143. package/src/summarization/node.ts +99 -14
  144. package/src/tools/BashExecutor.ts +4 -2
  145. package/src/tools/BashProgrammaticToolCalling.ts +4 -7
  146. package/src/tools/CodeExecutor.ts +119 -8
  147. package/src/tools/ProgrammaticToolCalling.ts +29 -27
  148. package/src/tools/ToolNode.ts +50 -8
  149. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
  150. package/src/types/activityLabel.ts +55 -0
  151. package/src/types/index.ts +1 -0
  152. package/src/types/llm.ts +8 -1
  153. package/src/types/stream.ts +7 -4
  154. package/src/types/summarize.ts +22 -0
  155. package/src/utils/__tests__/errors.test.ts +270 -0
  156. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
  157. package/src/utils/__tests__/redactSecrets.test.ts +56 -0
  158. package/src/utils/errors.ts +484 -66
  159. package/src/utils/redactSecrets.ts +61 -0
@@ -2,8 +2,8 @@ require("../../common/enum.cjs");
2
2
  require("../../common/index.cjs");
3
3
  const require_executeHooks = require("../../hooks/executeHooks.cjs");
4
4
  require("../../hooks/index.cjs");
5
- let _langchain_core_messages = require("@langchain/core/messages");
6
5
  let _langchain_core_callbacks_base = require("@langchain/core/callbacks/base");
6
+ let _langchain_core_messages = require("@langchain/core/messages");
7
7
  let nanoid = require("nanoid");
8
8
  //#region src/tools/subagent/SubagentExecutor.ts
9
9
  const DEFAULT_MAX_TURNS = 25;
@@ -1,41 +1,256 @@
1
+ let _langchain_core_errors = require("@langchain/core/errors");
1
2
  //#region src/utils/errors.ts
2
3
  /**
3
- * Context overflow error detection utilities.
4
+ * Context overflow error detection.
4
5
  *
5
- * Identifies provider-specific error messages that indicate the request
6
- * exceeded the model's context window. Used by the overflow recovery loop
7
- * to decide whether to retry with truncation/compaction vs. propagating
8
- * the error.
6
+ * Providers disagree on how they report "your input is bigger than I can
7
+ * take" the class thrown, the HTTP status, whether numbers are reported,
8
+ * and even whether it arrives as an HTTP error at all. Every pattern below
9
+ * was captured from a live over-limit request (see
10
+ * `src/scripts/context-overflow-probe.ts` and
11
+ * `docs/context-overflow-signatures.md`); nothing here is guessed.
12
+ *
13
+ * Consumed by the graph's overflow recovery loop, which converts a detection
14
+ * into a forced summarization pass instead of surfacing the error.
15
+ */
16
+ /**
17
+ * Fraction of the believed budget the prompt must reach before an ambiguous
18
+ * provider error is read as overflow. Well above normal traffic, low enough to
19
+ * catch the case the budget itself was miscalibrated.
9
20
  */
21
+ const CONTEXT_PRESSURE_RATIO = .8;
10
22
  /**
11
- * Exact phrases that definitively indicate a context overflow error.
12
- * These are returned by various LLM providers when the prompt is too large.
23
+ * Ordered most-specific first; the first match wins. Patterns that capture
24
+ * both numbers come before the bare-phrase fallbacks for the same provider so
25
+ * a recoverable error still yields the numbers when the provider reported
26
+ * them.
13
27
  */
14
- const CONTEXT_OVERFLOW_PHRASES = [
15
- "request_too_large",
16
- "context length exceeded",
17
- "maximum context length",
18
- "prompt is too long",
19
- "exceeds model context window",
20
- "exceeds the model",
21
- "too large for model",
22
- "context_length_exceeded",
23
- "max_tokens",
24
- "token limit",
25
- "input too long",
26
- "payload too large",
27
- "content_too_large"
28
+ const OVERFLOW_PATTERNS = [
29
+ (
30
+ /** Anthropic, and Bedrock's passthrough of the same upstream. */
31
+ {
32
+ kind: "context_window",
33
+ re: /prompt is too long:\s*(\d+)\s*tokens\s*>\s*(\d+)\s*maximum/i,
34
+ requestedGroup: 1,
35
+ limitGroup: 2,
36
+ requestedIsPromptOnly: true
37
+ }),
38
+ (
39
+ /**
40
+ * OpenAI's own wording, which measures the messages and nothing else.
41
+ * Ordered ahead of the shared sentence below so the prompt-only reading is
42
+ * preferred when OpenAI is the one answering.
43
+ */
44
+ {
45
+ kind: "context_window",
46
+ re: /maximum context length is\s*(\d+)\s*tokens\.\s*however,\s*your messages resulted in\s*(\d+)/i,
47
+ limitGroup: 1,
48
+ requestedGroup: 2,
49
+ requestedIsPromptOnly: true
50
+ }),
51
+ (
52
+ /**
53
+ * OpenRouter (`you requested about`) and DeepSeek (`you requested`). Their
54
+ * total folds in the completion allowance — both then break it down in
55
+ * parentheses, which `PROMPT_ONLY_BREAKDOWN_RE` recovers.
56
+ */
57
+ {
58
+ kind: "context_window",
59
+ re: /maximum context length is\s*(\d+)\s*tokens\.\s*however,\s*you requested(?:\s*about)?\s*(\d+)/i,
60
+ limitGroup: 1,
61
+ requestedGroup: 2
62
+ }),
63
+ (
64
+ /**
65
+ * xAI. Says "prompt length" rather than "context length", and does not say
66
+ * whether the count it quotes includes the completion allowance — so it is
67
+ * not trusted as a prompt measurement.
68
+ */
69
+ {
70
+ kind: "context_window",
71
+ re: /maximum prompt length is\s*(\d+)\s*(?:tokens\s*)?but the request contains\s*(\d+)\s*tokens/i,
72
+ limitGroup: 1,
73
+ requestedGroup: 2
74
+ }),
75
+ (
76
+ /** Mistral. */
77
+ {
78
+ kind: "context_window",
79
+ re: /prompt contains\s*(\d+)\s*tokens[^.]*?too large for model with\s*(\d+)\s*maximum context length/i,
80
+ requestedGroup: 1,
81
+ limitGroup: 2,
82
+ requestedIsPromptOnly: true
83
+ }),
84
+ (
85
+ /** Google Gemini / Vertex. Reports the ceiling only. */
86
+ {
87
+ kind: "context_window",
88
+ re: /input token count exceeds the maximum number of tokens allowed\s*\((\d+)\)/i,
89
+ limitGroup: 1
90
+ }),
91
+ (
92
+ /**
93
+ * OpenAI's token-bucket rejection. Numbers are validated by the caller:
94
+ * `Requested >= Limit` means no retry can ever succeed.
95
+ */
96
+ {
97
+ kind: "request_too_large",
98
+ re: /request too large[\s\S]*?limit\s*(\d+),\s*requested\s*(\d+)/i,
99
+ limitGroup: 1,
100
+ requestedGroup: 2
101
+ }),
102
+ (
103
+ /** Bedrock (Llama upstream) and any provider naming the ceiling alone. */
104
+ {
105
+ kind: "context_window",
106
+ re: /(?:maximum context length|maximum prompt length) is\s*(\d+)\s*tokens/i,
107
+ limitGroup: 1
108
+ }),
109
+ (
110
+ /** Bedrock (Nova upstream). */
111
+ {
112
+ kind: "context_window",
113
+ re: /number of input tokens exceeds maximum length|input tokens exceeded/i
114
+ }),
115
+ (
116
+ /** Bedrock (Claude Sonnet upstream). */
117
+ {
118
+ kind: "context_window",
119
+ re: /\binput (?:is )?too long(?: for requested model)?\b/i
120
+ }),
121
+ (
122
+ /** OpenAI-compatible error code, and the phrases LangChain itself keys on. */
123
+ {
124
+ kind: "context_window",
125
+ re: /context[_ ]length[_ ]exceeded|input tokens exceed the configured limit|exceeds the context window|exceeds model context window/i
126
+ }),
127
+ (
128
+ /** Generic long-tail phrasings observed across OpenAI-compatible gateways. */
129
+ {
130
+ kind: "context_window",
131
+ re: /prompt is too long|too large for model|reduce the length of (?:the |your )?(?:messages|prompt)/i
132
+ }),
133
+ (
134
+ /**
135
+ * Vertex AI. Its gaxios path discards the API error document, so an
136
+ * over-limit prompt arrives as a bare status line with no reason —
137
+ * identical to every other 400 from the same endpoint. Only a corroborated
138
+ * near-budget prompt makes this readable as overflow.
139
+ */
140
+ {
141
+ kind: "context_window",
142
+ re: /google request failed with status code 400(?!\s*:)/i,
143
+ requiresContextPressure: true
144
+ })
28
145
  ];
29
146
  /**
30
- * HTTP status codes and broader hints that suggest context overflow.
31
- * Used by the less-strict `isLikelyContextOverflowError`.
147
+ * Errors that mention size or limits but are NOT recoverable by compaction.
148
+ * Checked before the positive patterns.
149
+ *
150
+ * Note the deliberate precision: OpenAI's *recoverable* "Request too large"
151
+ * body links to `platform.openai.com/account/rate-limits`, so a loose
152
+ * /rate.?limit/ test would discard the very case this module exists to catch.
153
+ * URLs are stripped from the haystack before matching, and genuine throttling
154
+ * is matched on its own distinct phrasing.
155
+ */
156
+ const NON_RECOVERABLE_RE = /rate limit reached|requests per (?:min|day)|\brpm\b|too many requests|insufficient[_ ]quota|quota exceeded|billing|payment required|invalid[_ ]api[_ ]key|authentication|unauthorized|permission denied|forbidden/i;
157
+ /**
158
+ * Output-cap complaints. `max_tokens` appears in both families, so these are
159
+ * matched on the surrounding grammar rather than the bare parameter name.
160
+ */
161
+ const OUTPUT_LIMIT_RE = /max_?(?:completion_?)?tokens\s*(?:must be|is too|cannot|exceeds|too large|greater than)|maximum number of output tokens|max_tokens.*less than or equal/i;
162
+ /**
163
+ * Recovers the input-only figure from providers that quote a combined total
164
+ * and then break it down — OpenRouter's "(56811 of text input, 16 in the
165
+ * output)" and DeepSeek's "(1179652 in the messages, 16 in the completion)".
166
+ */
167
+ const PROMPT_ONLY_BREAKDOWN_RE = /\(\s*(\d+)\s*(?:of\s+text\s+input|in\s+the\s+messages|of\s+input|input\s+tokens)\b/i;
168
+ /** Broader hints for the deliberately fuzzy `isLikelyContextOverflowError`. */
169
+ const CONTEXT_OVERFLOW_HINT_RE = /413|payload too large|content_too_large|request entity too large|too many tokens|token count.*exceed|exceed.*token count/i;
170
+ const MAX_CAUSE_DEPTH = 4;
171
+ function asRecord(value) {
172
+ return typeof value === "object" && value !== null ? value : void 0;
173
+ }
174
+ /**
175
+ * Flattens an error into a single searchable string.
176
+ *
177
+ * Necessary because providers bury the useful sentence at different depths:
178
+ * Anthropic puts a JSON document in `message`, Mistral puts one in `body`,
179
+ * LangChain's `ContextOverflowError` keeps the original API error under
180
+ * `cause`, and the OpenAI SDK nests the body under `error`.
181
+ */
182
+ function collectErrorText(error, depth = 0) {
183
+ if (error == null || depth > MAX_CAUSE_DEPTH) return "";
184
+ if (typeof error === "string") return error;
185
+ const record = asRecord(error);
186
+ if (record == null) return String(error);
187
+ const parts = [];
188
+ if (typeof record.message === "string") parts.push(record.message);
189
+ for (const reason of [
190
+ record.code,
191
+ record.type,
192
+ record.status,
193
+ record.reason
194
+ ]) if (typeof reason === "string") parts.push(reason);
195
+ for (const nested of [
196
+ record.error,
197
+ record.cause,
198
+ record.body,
199
+ record.data,
200
+ record.response
201
+ ]) {
202
+ if (nested == null) continue;
203
+ parts.push(typeof nested === "string" ? nested : collectErrorText(nested, depth + 1));
204
+ }
205
+ if (parts.length === 0) try {
206
+ /** Non-objects returned above, so this always yields a string. */
207
+ return JSON.stringify(error);
208
+ } catch {
209
+ return String(error);
210
+ }
211
+ return parts.join(" ");
212
+ }
213
+ /** Strips URLs so their path segments cannot trip the negative matchers. */
214
+ function stripUrls(text) {
215
+ return text.replace(/https?:\/\/\S+/gi, " ");
216
+ }
217
+ function readNumber(match, group) {
218
+ if (group == null) return;
219
+ const parsed = Number(match[group]);
220
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
221
+ }
222
+ /**
223
+ * The provider's count of the prompt alone: its own breakdown when it gave
224
+ * one, otherwise the quoted total but only for providers that quote the
225
+ * prompt rather than the whole request. Returns undefined when the number on
226
+ * offer includes the completion allowance, since treating that as a prompt
227
+ * measurement would overstate how much the prompt has to shrink.
32
228
  */
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;
229
+ function resolvePromptTokens(haystack, pattern, requestedTokens) {
230
+ const breakdown = haystack.match(PROMPT_ONLY_BREAKDOWN_RE);
231
+ if (breakdown != null) {
232
+ const parsed = Number(breakdown[1]);
233
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
234
+ }
235
+ return pattern.requestedIsPromptOnly === true ? requestedTokens : void 0;
236
+ }
34
237
  /**
35
- * Patterns that should NOT be treated as context overflow even if they
36
- * contain words like "limit" or "too large".
238
+ * True when the caller's own accounting says the failed prompt was close
239
+ * enough to the budget that an otherwise ambiguous provider error is best
240
+ * explained by overflow.
37
241
  */
38
- const FALSE_POSITIVE_RE = /rate.?limit|too many requests|quota|billing|auth|permission|forbidden/i;
242
+ function hasContextPressure(context) {
243
+ const estimated = context?.estimatedPromptTokens;
244
+ const budget = context?.maxContextTokens;
245
+ if (estimated == null || budget == null || !Number.isFinite(estimated) || !Number.isFinite(budget) || budget <= 0) return false;
246
+ return estimated / budget >= CONTEXT_PRESSURE_RATIO;
247
+ }
248
+ function isLangChainOverflowError(error) {
249
+ if (_langchain_core_errors.ContextOverflowError.isInstance(error)) return true;
250
+ /** Duplicate `@langchain/core` copies break branding; the name survives. */
251
+ const record = asRecord(error);
252
+ return record?.name === "ContextOverflowError" || record?.lc_error_code === "CONTEXT_OVERFLOW";
253
+ }
39
254
  /**
40
255
  * Extracts a human-readable error message from an unknown error value.
41
256
  */
@@ -43,12 +258,14 @@ function extractErrorMessage(error) {
43
258
  if (error == null) return "";
44
259
  if (typeof error === "string") return error;
45
260
  if (error instanceof Error) return error.message;
46
- if (typeof error === "object") {
47
- const record = error;
48
- if (typeof record.message === "string") return record.message;
49
- if (typeof record.error === "string") return record.error;
50
- if (typeof record.error === "object" && record.error != null && typeof record.error.message === "string") return record.error.message;
51
- }
261
+ const record = asRecord(error);
262
+ if (record == null)
263
+ /** Functions and symbols serialize to nothing; describe them instead. */
264
+ return String(error);
265
+ if (typeof record.message === "string") return record.message;
266
+ if (typeof record.error === "string") return record.error;
267
+ const nested = asRecord(record.error);
268
+ if (typeof nested?.message === "string") return nested.message;
52
269
  try {
53
270
  return JSON.stringify(error);
54
271
  } catch {
@@ -56,34 +273,82 @@ function extractErrorMessage(error) {
56
273
  }
57
274
  }
58
275
  /**
59
- * Returns true if the error message definitively indicates a context
60
- * overflow / prompt-too-large error from the provider.
276
+ * Classifies a provider error as a recoverable context overflow, returning
277
+ * whatever the provider disclosed about the limit and the request size.
278
+ *
279
+ * Returns `null` for anything that compaction cannot fix — genuine
280
+ * throttling, auth, quota, and output-token-cap errors all mention limits
281
+ * and must not be mistaken for overflow.
282
+ *
283
+ * The reported numbers are the point of the return value: they let the
284
+ * caller retarget the token budget to the provider's real ceiling instead of
285
+ * retrying blindly against a configured value that was evidently wrong.
286
+ */
287
+ function getContextOverflowInfo(error, context) {
288
+ const provider = context?.provider;
289
+ const haystack = stripUrls(collectErrorText(error));
290
+ if (haystack === "") return null;
291
+ if (OUTPUT_LIMIT_RE.test(haystack)) return null;
292
+ const langChainFlagged = isLangChainOverflowError(error);
293
+ if (!langChainFlagged && NON_RECOVERABLE_RE.test(haystack)) return null;
294
+ const underContextPressure = hasContextPressure(context);
295
+ for (const pattern of OVERFLOW_PATTERNS) {
296
+ const match = haystack.match(pattern.re);
297
+ if (match == null) continue;
298
+ if (pattern.requiresContextPressure === true && !underContextPressure) continue;
299
+ const limitTokens = readNumber(match, pattern.limitGroup);
300
+ const requestedTokens = readNumber(match, pattern.requestedGroup);
301
+ /**
302
+ * A token-bucket rejection is only unrecoverable-by-waiting when the
303
+ * request alone overruns the bucket. When it merely fills it, the account
304
+ * was busy and the request will fit once the window drains — so equality
305
+ * belongs on the retry side, not the compaction side. Losing conversation
306
+ * history to a temporarily busy account is the worse error.
307
+ */
308
+ if (pattern.kind === "request_too_large" && limitTokens != null && requestedTokens != null && requestedTokens <= limitTokens) return null;
309
+ return {
310
+ kind: pattern.kind,
311
+ limitTokens,
312
+ requestedTokens,
313
+ promptTokens: resolvePromptTokens(haystack, pattern, requestedTokens),
314
+ source: "pattern",
315
+ provider
316
+ };
317
+ }
318
+ if (langChainFlagged) return {
319
+ kind: "context_window",
320
+ source: "langchain",
321
+ provider
322
+ };
323
+ return null;
324
+ }
325
+ /**
326
+ * Returns true if the error definitively indicates a context overflow.
61
327
  *
62
- * This is the strict check: only matches known, unambiguous phrases.
63
- * Use this when you want high confidence before taking recovery action.
328
+ * Accepts either a raw error or a pre-extracted message; passing the error
329
+ * itself is preferred, since several providers report the decisive detail in
330
+ * a nested body rather than in `message`.
64
331
  */
65
- function isContextOverflowError(errorMessage) {
66
- if (!errorMessage) return false;
67
- const lower = errorMessage.toLowerCase();
68
- if (FALSE_POSITIVE_RE.test(lower)) return false;
69
- return CONTEXT_OVERFLOW_PHRASES.some((phrase) => lower.includes(phrase));
332
+ function isContextOverflowError(error, context) {
333
+ return getContextOverflowInfo(error, context) != null;
70
334
  }
71
335
  /**
72
- * Returns true if the error message likely indicates a context overflow.
73
- * Uses broader heuristic matching (regex) in addition to exact phrases.
336
+ * Returns true if the error likely indicates a context overflow, adding
337
+ * body-size and token-count heuristics on top of the definitive patterns.
74
338
  *
75
- * May produce false positives for unusual error messages. Use this when
76
- * the cost of a false positive (one extra retry) is acceptable.
339
+ * May produce false positives on unusual messages. Use when the cost of
340
+ * being wrong is one extra compaction pass.
77
341
  */
78
- function isLikelyContextOverflowError(errorMessage) {
79
- if (!errorMessage) return false;
80
- if (isContextOverflowError(errorMessage)) return true;
81
- const lower = errorMessage.toLowerCase();
82
- if (FALSE_POSITIVE_RE.test(lower)) return false;
83
- return CONTEXT_OVERFLOW_HINT_RE.test(lower);
342
+ function isLikelyContextOverflowError(error, context) {
343
+ if (isContextOverflowError(error, context)) return true;
344
+ const haystack = stripUrls(collectErrorText(error));
345
+ if (haystack === "" || OUTPUT_LIMIT_RE.test(haystack)) return false;
346
+ if (NON_RECOVERABLE_RE.test(haystack)) return false;
347
+ return CONTEXT_OVERFLOW_HINT_RE.test(haystack);
84
348
  }
85
349
  //#endregion
86
350
  exports.extractErrorMessage = extractErrorMessage;
351
+ exports.getContextOverflowInfo = getContextOverflowInfo;
87
352
  exports.isContextOverflowError = isContextOverflowError;
88
353
  exports.isLikelyContextOverflowError = isLikelyContextOverflowError;
89
354
 
@@ -1 +1 @@
1
- {"version":3,"file":"errors.cjs","names":[],"sources":["../../../src/utils/errors.ts"],"sourcesContent":["/**\n * Context overflow error detection utilities.\n *\n * Identifies provider-specific error messages that indicate the request\n * exceeded the model's context window. Used by the overflow recovery loop\n * to decide whether to retry with truncation/compaction vs. propagating\n * the error.\n */\n\n/**\n * Exact phrases that definitively indicate a context overflow error.\n * These are returned by various LLM providers when the prompt is too large.\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 'payload too large',\n 'content_too_large',\n] as const;\n\n/**\n * HTTP status codes and broader hints that suggest context overflow.\n * Used by the less-strict `isLikelyContextOverflowError`.\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\n/**\n * Patterns that should NOT be treated as context overflow even if they\n * contain words like \"limit\" or \"too large\".\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 */\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 * Returns true if the error message definitively indicates a context\n * overflow / prompt-too-large error from the provider.\n *\n * This is the strict check: only matches known, unambiguous phrases.\n * Use this when you want high confidence before taking recovery action.\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 * Returns true if the error message likely indicates a context overflow.\n * Uses broader heuristic matching (regex) in addition to exact phrases.\n *\n * May produce false positives for unusual error messages. Use this when\n * the cost of a false positive (one extra retry) is acceptable.\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"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,2BAA2B;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,MAAM,2BACJ;;;;;AAMF,MAAM,oBACJ;;;;AAKF,SAAgB,oBAAoB,OAAwB;CAC1D,IAAI,SAAS,MACX,OAAO;CAET,IAAI,OAAO,UAAU,UACnB,OAAO;CAET,IAAI,iBAAiB,OACnB,OAAO,MAAM;CAEf,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,SAAS;EACf,IAAI,OAAO,OAAO,YAAY,UAC5B,OAAO,OAAO;EAEhB,IAAI,OAAO,OAAO,UAAU,UAC1B,OAAO,OAAO;EAEhB,IACE,OAAO,OAAO,UAAU,YACxB,OAAO,SAAS,QAChB,OAAQ,OAAO,MAAkC,YAAY,UAE7D,OAAQ,OAAO,MAAkC;CAErD;CACA,IAAI;EACF,OAAO,KAAK,UAAU,KAAK;CAC7B,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;;;;;;;;AASA,SAAgB,uBAAuB,cAAgC;CACrE,IAAI,CAAC,cACH,OAAO;CAET,MAAM,QAAQ,aAAa,YAAY;CACvC,IAAI,kBAAkB,KAAK,KAAK,GAC9B,OAAO;CAET,OAAO,yBAAyB,MAAM,WAAW,MAAM,SAAS,MAAM,CAAC;AACzE;;;;;;;;AASA,SAAgB,6BAA6B,cAAgC;CAC3E,IAAI,CAAC,cACH,OAAO;CAET,IAAI,uBAAuB,YAAY,GACrC,OAAO;CAET,MAAM,QAAQ,aAAa,YAAY;CACvC,IAAI,kBAAkB,KAAK,KAAK,GAC9B,OAAO;CAET,OAAO,yBAAyB,KAAK,KAAK;AAC5C"}
1
+ {"version":3,"file":"errors.cjs","names":["ContextOverflowError"],"sources":["../../../src/utils/errors.ts"],"sourcesContent":["/**\n * Context overflow error detection.\n *\n * Providers disagree on how they report \"your input is bigger than I can\n * take\" — the class thrown, the HTTP status, whether numbers are reported,\n * and even whether it arrives as an HTTP error at all. Every pattern below\n * was captured from a live over-limit request (see\n * `src/scripts/context-overflow-probe.ts` and\n * `docs/context-overflow-signatures.md`); nothing here is guessed.\n *\n * Consumed by the graph's overflow recovery loop, which converts a detection\n * into a forced summarization pass instead of surfacing the error.\n */\nimport { ContextOverflowError } from '@langchain/core/errors';\nimport type { Providers } from '@/common';\n\n/**\n * Why the request was rejected. Both kinds are fixed by shrinking the\n * prompt, which is what makes them recoverable; they are distinguished\n * because only `context_window` tells us the model's true window.\n */\nexport type ContextOverflowKind =\n /** Input exceeded the model's context window. */\n | 'context_window'\n /**\n * A single request exceeded a per-minute token allowance. Waiting cannot\n * help — the request can never fit the bucket — so this is a payload\n * problem wearing a 429, not throttling.\n */\n | 'request_too_large';\n\nexport interface ContextOverflowInfo {\n kind: ContextOverflowKind;\n /** Ceiling the provider reported, when it named one. */\n limitTokens?: number;\n /**\n * Token count the provider attributed to the whole request. Several\n * providers fold the requested completion allowance into this number, so it\n * is not interchangeable with the prompt size.\n */\n requestedTokens?: number;\n /**\n * The prompt alone, counted by the provider — set only when the provider\n * distinguished input from output, either by reporting an input-only figure\n * or by breaking the total down. Callers comparing provider counts against\n * their own prompt estimate must use this and not `requestedTokens`, whose\n * completion component would inflate the comparison.\n */\n promptTokens?: number;\n /** Which layer produced the verdict. Surfaced in logs and asserted in tests. */\n source: 'langchain' | 'pattern';\n provider?: Providers;\n}\n\ninterface OverflowPattern {\n readonly kind: ContextOverflowKind;\n readonly re: RegExp;\n readonly limitGroup?: number;\n readonly requestedGroup?: number;\n /**\n * Whether `requestedGroup` counts the prompt alone. Providers that report a\n * combined input+completion total leave this false, and their number is\n * never used as a prompt measurement.\n */\n readonly requestedIsPromptOnly?: boolean;\n /**\n * Marks a signature that is consistent with overflow but not exclusive to\n * it, so it only counts when the caller can corroborate that the prompt was\n * actually near the budget. Without corroboration the error propagates\n * untouched rather than triggering a needless compaction.\n */\n readonly requiresContextPressure?: boolean;\n}\n\n/**\n * Fraction of the believed budget the prompt must reach before an ambiguous\n * provider error is read as overflow. Well above normal traffic, low enough to\n * catch the case the budget itself was miscalibrated.\n */\nconst CONTEXT_PRESSURE_RATIO = 0.8;\n\nexport interface ContextOverflowContext {\n provider?: Providers;\n /** Our own estimate of the prompt size for the call that failed. */\n estimatedPromptTokens?: number;\n /** The budget we believed applied when we built that prompt. */\n maxContextTokens?: number;\n}\n\n/**\n * Ordered most-specific first; the first match wins. Patterns that capture\n * both numbers come before the bare-phrase fallbacks for the same provider so\n * a recoverable error still yields the numbers when the provider reported\n * them.\n */\nconst OVERFLOW_PATTERNS: readonly OverflowPattern[] = [\n /** Anthropic, and Bedrock's passthrough of the same upstream. */\n {\n kind: 'context_window',\n re: /prompt is too long:\\s*(\\d+)\\s*tokens\\s*>\\s*(\\d+)\\s*maximum/i,\n requestedGroup: 1,\n limitGroup: 2,\n requestedIsPromptOnly: true,\n },\n /**\n * OpenAI's own wording, which measures the messages and nothing else.\n * Ordered ahead of the shared sentence below so the prompt-only reading is\n * preferred when OpenAI is the one answering.\n */\n {\n kind: 'context_window',\n re: /maximum context length is\\s*(\\d+)\\s*tokens\\.\\s*however,\\s*your messages resulted in\\s*(\\d+)/i,\n limitGroup: 1,\n requestedGroup: 2,\n requestedIsPromptOnly: true,\n },\n /**\n * OpenRouter (`you requested about`) and DeepSeek (`you requested`). Their\n * total folds in the completion allowance — both then break it down in\n * parentheses, which `PROMPT_ONLY_BREAKDOWN_RE` recovers.\n */\n {\n kind: 'context_window',\n re: /maximum context length is\\s*(\\d+)\\s*tokens\\.\\s*however,\\s*you requested(?:\\s*about)?\\s*(\\d+)/i,\n limitGroup: 1,\n requestedGroup: 2,\n },\n /**\n * xAI. Says \"prompt length\" rather than \"context length\", and does not say\n * whether the count it quotes includes the completion allowance — so it is\n * not trusted as a prompt measurement.\n */\n {\n kind: 'context_window',\n re: /maximum prompt length is\\s*(\\d+)\\s*(?:tokens\\s*)?but the request contains\\s*(\\d+)\\s*tokens/i,\n limitGroup: 1,\n requestedGroup: 2,\n },\n /** Mistral. */\n {\n kind: 'context_window',\n re: /prompt contains\\s*(\\d+)\\s*tokens[^.]*?too large for model with\\s*(\\d+)\\s*maximum context length/i,\n requestedGroup: 1,\n limitGroup: 2,\n requestedIsPromptOnly: true,\n },\n /** Google Gemini / Vertex. Reports the ceiling only. */\n {\n kind: 'context_window',\n re: /input token count exceeds the maximum number of tokens allowed\\s*\\((\\d+)\\)/i,\n limitGroup: 1,\n },\n /**\n * OpenAI's token-bucket rejection. Numbers are validated by the caller:\n * `Requested >= Limit` means no retry can ever succeed.\n */\n {\n kind: 'request_too_large',\n re: /request too large[\\s\\S]*?limit\\s*(\\d+),\\s*requested\\s*(\\d+)/i,\n limitGroup: 1,\n requestedGroup: 2,\n },\n /** Bedrock (Llama upstream) and any provider naming the ceiling alone. */\n {\n kind: 'context_window',\n re: /(?:maximum context length|maximum prompt length) is\\s*(\\d+)\\s*tokens/i,\n limitGroup: 1,\n },\n /** Bedrock (Nova upstream). */\n {\n kind: 'context_window',\n re: /number of input tokens exceeds maximum length|input tokens exceeded/i,\n },\n /** Bedrock (Claude Sonnet upstream). */\n {\n kind: 'context_window',\n re: /\\binput (?:is )?too long(?: for requested model)?\\b/i,\n },\n /** OpenAI-compatible error code, and the phrases LangChain itself keys on. */\n {\n kind: 'context_window',\n re: /context[_ ]length[_ ]exceeded|input tokens exceed the configured limit|exceeds the context window|exceeds model context window/i,\n },\n /** Generic long-tail phrasings observed across OpenAI-compatible gateways. */\n {\n kind: 'context_window',\n re: /prompt is too long|too large for model|reduce the length of (?:the |your )?(?:messages|prompt)/i,\n },\n /**\n * Vertex AI. Its gaxios path discards the API error document, so an\n * over-limit prompt arrives as a bare status line with no reason —\n * identical to every other 400 from the same endpoint. Only a corroborated\n * near-budget prompt makes this readable as overflow.\n */\n {\n kind: 'context_window',\n re: /google request failed with status code 400(?!\\s*:)/i,\n requiresContextPressure: true,\n },\n] as const;\n\n/**\n * Errors that mention size or limits but are NOT recoverable by compaction.\n * Checked before the positive patterns.\n *\n * Note the deliberate precision: OpenAI's *recoverable* \"Request too large\"\n * body links to `platform.openai.com/account/rate-limits`, so a loose\n * /rate.?limit/ test would discard the very case this module exists to catch.\n * URLs are stripped from the haystack before matching, and genuine throttling\n * is matched on its own distinct phrasing.\n */\nconst NON_RECOVERABLE_RE =\n /rate limit reached|requests per (?:min|day)|\\brpm\\b|too many requests|insufficient[_ ]quota|quota exceeded|billing|payment required|invalid[_ ]api[_ ]key|authentication|unauthorized|permission denied|forbidden/i;\n\n/**\n * Output-cap complaints. `max_tokens` appears in both families, so these are\n * matched on the surrounding grammar rather than the bare parameter name.\n */\nconst OUTPUT_LIMIT_RE =\n /max_?(?:completion_?)?tokens\\s*(?:must be|is too|cannot|exceeds|too large|greater than)|maximum number of output tokens|max_tokens.*less than or equal/i;\n\n/**\n * Recovers the input-only figure from providers that quote a combined total\n * and then break it down — OpenRouter's \"(56811 of text input, 16 in the\n * output)\" and DeepSeek's \"(1179652 in the messages, 16 in the completion)\".\n */\nconst PROMPT_ONLY_BREAKDOWN_RE =\n /\\(\\s*(\\d+)\\s*(?:of\\s+text\\s+input|in\\s+the\\s+messages|of\\s+input|input\\s+tokens)\\b/i;\n\n/** Broader hints for the deliberately fuzzy `isLikelyContextOverflowError`. */\nconst CONTEXT_OVERFLOW_HINT_RE =\n /413|payload too large|content_too_large|request entity too large|too many tokens|token count.*exceed|exceed.*token count/i;\n\nconst MAX_CAUSE_DEPTH = 4;\n\ninterface NestedErrorShape {\n message?: unknown;\n code?: unknown;\n type?: unknown;\n status?: unknown;\n reason?: unknown;\n error?: unknown;\n cause?: unknown;\n body?: unknown;\n response?: unknown;\n /** gaxios-style clients (Vertex AI) put the API error document here. */\n data?: unknown;\n}\n\nfunction asRecord(value: unknown): NestedErrorShape | undefined {\n return typeof value === 'object' && value !== null\n ? (value as NestedErrorShape)\n : undefined;\n}\n\n/**\n * Flattens an error into a single searchable string.\n *\n * Necessary because providers bury the useful sentence at different depths:\n * Anthropic puts a JSON document in `message`, Mistral puts one in `body`,\n * LangChain's `ContextOverflowError` keeps the original API error under\n * `cause`, and the OpenAI SDK nests the body under `error`.\n */\nfunction collectErrorText(error: unknown, depth = 0): string {\n if (error == null || depth > MAX_CAUSE_DEPTH) {\n return '';\n }\n if (typeof error === 'string') {\n return error;\n }\n const record = asRecord(error);\n if (record == null) {\n return String(error);\n }\n\n const parts: string[] = [];\n if (typeof record.message === 'string') {\n parts.push(record.message);\n }\n for (const reason of [\n record.code,\n record.type,\n record.status,\n record.reason,\n ]) {\n if (typeof reason === 'string') {\n parts.push(reason);\n }\n }\n for (const nested of [\n record.error,\n record.cause,\n record.body,\n record.data,\n record.response,\n ]) {\n if (nested == null) {\n continue;\n }\n parts.push(\n typeof nested === 'string' ? nested : collectErrorText(nested, depth + 1)\n );\n }\n if (parts.length === 0) {\n try {\n /** Non-objects returned above, so this always yields a string. */\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n }\n return parts.join(' ');\n}\n\n/** Strips URLs so their path segments cannot trip the negative matchers. */\nfunction stripUrls(text: string): string {\n return text.replace(/https?:\\/\\/\\S+/gi, ' ');\n}\n\nfunction readNumber(\n match: RegExpMatchArray,\n group?: number\n): number | undefined {\n if (group == null) {\n return undefined;\n }\n const parsed = Number(match[group]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/**\n * The provider's count of the prompt alone: its own breakdown when it gave\n * one, otherwise the quoted total but only for providers that quote the\n * prompt rather than the whole request. Returns undefined when the number on\n * offer includes the completion allowance, since treating that as a prompt\n * measurement would overstate how much the prompt has to shrink.\n */\nfunction resolvePromptTokens(\n haystack: string,\n pattern: OverflowPattern,\n requestedTokens: number | undefined\n): number | undefined {\n const breakdown = haystack.match(PROMPT_ONLY_BREAKDOWN_RE);\n if (breakdown != null) {\n const parsed = Number(breakdown[1]);\n if (Number.isFinite(parsed) && parsed > 0) {\n return parsed;\n }\n }\n return pattern.requestedIsPromptOnly === true ? requestedTokens : undefined;\n}\n\n/**\n * True when the caller's own accounting says the failed prompt was close\n * enough to the budget that an otherwise ambiguous provider error is best\n * explained by overflow.\n */\nfunction hasContextPressure(context?: ContextOverflowContext): boolean {\n const estimated = context?.estimatedPromptTokens;\n const budget = context?.maxContextTokens;\n if (\n estimated == null ||\n budget == null ||\n !Number.isFinite(estimated) ||\n !Number.isFinite(budget) ||\n budget <= 0\n ) {\n return false;\n }\n return estimated / budget >= CONTEXT_PRESSURE_RATIO;\n}\n\nfunction isLangChainOverflowError(error: unknown): boolean {\n if (ContextOverflowError.isInstance(error)) {\n return true;\n }\n /** Duplicate `@langchain/core` copies break branding; the name survives. */\n const record = asRecord(error) as\n | { name?: unknown; lc_error_code?: unknown }\n | undefined;\n return (\n record?.name === 'ContextOverflowError' ||\n record?.lc_error_code === 'CONTEXT_OVERFLOW'\n );\n}\n\n/**\n * Extracts a human-readable error message from an unknown error value.\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 const record = asRecord(error);\n if (record == null) {\n /** Functions and symbols serialize to nothing; describe them instead. */\n return String(error);\n }\n if (typeof record.message === 'string') {\n return record.message;\n }\n if (typeof record.error === 'string') {\n return record.error;\n }\n const nested = asRecord(record.error);\n if (typeof nested?.message === 'string') {\n return nested.message;\n }\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n}\n\n/**\n * Classifies a provider error as a recoverable context overflow, returning\n * whatever the provider disclosed about the limit and the request size.\n *\n * Returns `null` for anything that compaction cannot fix — genuine\n * throttling, auth, quota, and output-token-cap errors all mention limits\n * and must not be mistaken for overflow.\n *\n * The reported numbers are the point of the return value: they let the\n * caller retarget the token budget to the provider's real ceiling instead of\n * retrying blindly against a configured value that was evidently wrong.\n */\nexport function getContextOverflowInfo(\n error: unknown,\n context?: ContextOverflowContext\n): ContextOverflowInfo | null {\n const provider = context?.provider;\n const haystack = stripUrls(collectErrorText(error));\n if (haystack === '') {\n return null;\n }\n\n if (OUTPUT_LIMIT_RE.test(haystack)) {\n return null;\n }\n\n const langChainFlagged = isLangChainOverflowError(error);\n if (!langChainFlagged && NON_RECOVERABLE_RE.test(haystack)) {\n return null;\n }\n\n const underContextPressure = hasContextPressure(context);\n\n for (const pattern of OVERFLOW_PATTERNS) {\n const match = haystack.match(pattern.re);\n if (match == null) {\n continue;\n }\n if (pattern.requiresContextPressure === true && !underContextPressure) {\n continue;\n }\n const limitTokens = readNumber(match, pattern.limitGroup);\n const requestedTokens = readNumber(match, pattern.requestedGroup);\n\n /**\n * A token-bucket rejection is only unrecoverable-by-waiting when the\n * request alone overruns the bucket. When it merely fills it, the account\n * was busy and the request will fit once the window drains — so equality\n * belongs on the retry side, not the compaction side. Losing conversation\n * history to a temporarily busy account is the worse error.\n */\n if (\n pattern.kind === 'request_too_large' &&\n limitTokens != null &&\n requestedTokens != null &&\n requestedTokens <= limitTokens\n ) {\n return null;\n }\n\n return {\n kind: pattern.kind,\n limitTokens,\n requestedTokens,\n promptTokens: resolvePromptTokens(haystack, pattern, requestedTokens),\n source: 'pattern',\n provider,\n };\n }\n\n if (langChainFlagged) {\n return { kind: 'context_window', source: 'langchain', provider };\n }\n\n return null;\n}\n\n/**\n * Returns true if the error definitively indicates a context overflow.\n *\n * Accepts either a raw error or a pre-extracted message; passing the error\n * itself is preferred, since several providers report the decisive detail in\n * a nested body rather than in `message`.\n */\nexport function isContextOverflowError(\n error?: unknown,\n context?: ContextOverflowContext\n): boolean {\n return getContextOverflowInfo(error, context) != null;\n}\n\n/**\n * Returns true if the error likely indicates a context overflow, adding\n * body-size and token-count heuristics on top of the definitive patterns.\n *\n * May produce false positives on unusual messages. Use when the cost of\n * being wrong is one extra compaction pass.\n */\nexport function isLikelyContextOverflowError(\n error?: unknown,\n context?: ContextOverflowContext\n): boolean {\n if (isContextOverflowError(error, context)) {\n return true;\n }\n const haystack = stripUrls(collectErrorText(error));\n if (haystack === '' || OUTPUT_LIMIT_RE.test(haystack)) {\n return false;\n }\n if (NON_RECOVERABLE_RE.test(haystack)) {\n return false;\n }\n return CONTEXT_OVERFLOW_HINT_RE.test(haystack);\n}\n"],"mappings":"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"}
@@ -2,15 +2,15 @@ import { ANTHROPIC_TOOL_TOKEN_MULTIPLIER, DEFAULT_TOOL_TOKEN_MULTIPLIER } from "
2
2
  import "../common/enum.mjs";
3
3
  import "../common/index.mjs";
4
4
  import { apportionTokenCounts } from "../utils/tokens.mjs";
5
- import { createPruneMessages } from "../messages/prune.mjs";
5
+ import { clampCalibrationRatio, createPruneMessages } from "../messages/prune.mjs";
6
6
  import { syncBudgetDerivedFields } from "../messages/budget.mjs";
7
7
  import { addCacheControlToStablePrefixMessages, addTailCacheControl, buildAnthropicCacheControl, buildBedrockCachePoint, cloneMessage, resolveBedrockPromptCacheTtl, resolvePromptCacheTtl } from "../messages/cache.mjs";
8
8
  import "../messages/index.mjs";
9
9
  import { toJsonSchema } from "../utils/schema.mjs";
10
10
  import { isThinkingEnabled } from "../llm/request.mjs";
11
11
  import { createSchemaOnlyTools } from "../tools/schema.mjs";
12
- import { HumanMessage, SystemMessage } from "@langchain/core/messages";
13
12
  import { RunnableLambda } from "@langchain/core/runnables";
13
+ import { HumanMessage, SystemMessage } from "@langchain/core/messages";
14
14
  //#region src/agents/AgentContext.ts
15
15
  /**
16
16
  * Encapsulates agent-specific state that can vary between agents in a multi-agent system
@@ -128,8 +128,20 @@ var AgentContext = class AgentContext {
128
128
  calibrationRatio = 1;
129
129
  /** Provider-observed instruction overhead from the pruner's best-variance turn. */
130
130
  resolvedInstructionOverhead;
131
+ _pendingOriginalToolContent;
132
+ pendingOriginalToolContentChars = 0;
131
133
  /** Pre-masking tool content keyed by message index, consumed by the summarize node. */
132
- pendingOriginalToolContent;
134
+ get pendingOriginalToolContent() {
135
+ return this._pendingOriginalToolContent;
136
+ }
137
+ set pendingOriginalToolContent(value) {
138
+ this._pendingOriginalToolContent = value;
139
+ this.pendingOriginalToolContentChars = 0;
140
+ if (value != null) {
141
+ for (const content of value.values()) this.pendingOriginalToolContentChars += content.length;
142
+ this.enforcePendingOriginalContentCap();
143
+ }
144
+ }
133
145
  /** Total instruction overhead: system message + tool schemas + pending summary. */
134
146
  get instructionTokens() {
135
147
  const summaryOverhead = this._summaryLocation === "user_message" ? this.summaryTokenCount : 0;
@@ -219,6 +231,25 @@ var AgentContext = class AgentContext {
219
231
  */
220
232
  _lastSummarizationMsgCount = 0;
221
233
  /**
234
+ * Forced compactions performed after a provider rejected a prompt as too
235
+ * large. Bounds the recovery loop so a model that keeps refusing cannot
236
+ * make the run compact indefinitely.
237
+ */
238
+ _overflowRecoveryAttempts = 0;
239
+ /**
240
+ * Budget in force before the first overflow correction of the current run.
241
+ * Recorded so `reset()` can undo the correction for the next run without
242
+ * disturbing a `maxContextTokens` that no correction ever touched.
243
+ */
244
+ _preOverflowMaxContextTokens;
245
+ /**
246
+ * Prompt size, normalized into the local counter's uncalibrated units, at
247
+ * the last overflow correction. Keeping both measurements in the same units
248
+ * lets a later overflow prove whether compaction changed anything even when
249
+ * the provider observation updated calibration between attempts.
250
+ */
251
+ _lastOverflowPromptTokens;
252
+ /**
222
253
  * Handoff context when this agent receives control via handoff.
223
254
  * Contains source and parallel execution info for system message context.
224
255
  */
@@ -510,7 +541,7 @@ The following tools are available exclusively through the \`${programmaticTool.n
510
541
  /**
511
542
  * Reset context for a new run
512
543
  */
513
- reset() {
544
+ reset(options) {
514
545
  this.systemMessageTokens = 0;
515
546
  this.dynamicInstructionTokens = 0;
516
547
  this.toolSchemaTokens = 0;
@@ -528,11 +559,13 @@ The following tools are available exclusively through the \`${programmaticTool.n
528
559
  this.currentTokenType = "text";
529
560
  this.discoveredToolNames.clear();
530
561
  this.handoffContext = void 0;
562
+ if (options?.preserveOriginalToolContent !== true) this.pendingOriginalToolContent = void 0;
531
563
  this.summaryText = this._durableSummaryText;
532
564
  this.summaryTokenCount = this._durableSummaryTokenCount;
533
565
  this._lastSummarizationMsgCount = 0;
534
566
  this.lastCallUsage = void 0;
535
567
  this.totalTokensFresh = false;
568
+ this.restoreContextBudgetAfterOverflow();
536
569
  if (this.tokenCounter) {
537
570
  this.initializeSystemRunnable();
538
571
  const baseTokenMap = { ...this.baseIndexTokenCountMap };
@@ -757,6 +790,87 @@ The following tools are available exclusively through the \`${programmaticTool.n
757
790
  markSummarizationTriggered(msgCount) {
758
791
  this._lastSummarizationMsgCount = msgCount;
759
792
  }
793
+ get overflowRecoveryAttempts() {
794
+ return this._overflowRecoveryAttempts;
795
+ }
796
+ shouldSummarizeOverflow() {
797
+ return this.summarizationEnabled === true && (this.tokenCounter == null || this.maxContextTokens == null || this._overflowRecoveryAttempts > 0);
798
+ }
799
+ /** Preserves the earliest full tool output recorded for each message index. */
800
+ preserveOriginalToolContent(originalToolContent) {
801
+ if (originalToolContent == null || originalToolContent.size === 0) return;
802
+ if (this.pendingOriginalToolContent == null) this.pendingOriginalToolContent = /* @__PURE__ */ new Map();
803
+ for (const [index, content] of originalToolContent) if (!this.pendingOriginalToolContent.has(index)) {
804
+ this.pendingOriginalToolContent.set(index, content);
805
+ this.pendingOriginalToolContentChars += content.length;
806
+ }
807
+ this.enforcePendingOriginalContentCap();
808
+ }
809
+ enforcePendingOriginalContentCap() {
810
+ const pending = this._pendingOriginalToolContent;
811
+ if (pending == null) return;
812
+ while (this.pendingOriginalToolContentChars > 2e6 && pending.size > 0) {
813
+ const oldest = pending.keys().next();
814
+ if (oldest.done === true) break;
815
+ const removed = pending.get(oldest.value);
816
+ if (removed != null) this.pendingOriginalToolContentChars -= removed.length;
817
+ pending.delete(oldest.value);
818
+ }
819
+ }
820
+ /**
821
+ * Retargets the context budget after a provider rejected the prompt as too
822
+ * large, and clears the memoized pruner so the next call is planned against
823
+ * the corrected budget rather than the one that was evidently wrong.
824
+ *
825
+ * Also clears the "already summarized at this message count" guard: that
826
+ * guard exists to stop redundant summarization of an unchanged history, but
827
+ * here the history has not changed and compaction is exactly what is
828
+ * needed.
829
+ */
830
+ applyContextBudgetCorrection(budgetTokens, promptTokens) {
831
+ if (this._overflowRecoveryAttempts === 0) this._preOverflowMaxContextTokens = this.maxContextTokens;
832
+ if (budgetTokens != null) this.maxContextTokens = budgetTokens;
833
+ this.pruneMessages = void 0;
834
+ this._lastSummarizationMsgCount = 0;
835
+ this._lastOverflowPromptTokens = promptTokens != null ? this.normalizePromptTokens(promptTokens) : promptTokens;
836
+ this._overflowRecoveryAttempts += 1;
837
+ }
838
+ /** Applies token calibration only when the observation came from this provider. */
839
+ applyObservedOverflowCalibration(provider, observedCalibrationRatio) {
840
+ if (provider !== this.provider || observedCalibrationRatio == null || observedCalibrationRatio <= 0) return;
841
+ this.calibrationRatio = clampCalibrationRatio(observedCalibrationRatio);
842
+ }
843
+ /**
844
+ * True when a previous correction failed to make the prompt any smaller —
845
+ * the signature of a state nothing can compact further (an emptied message
846
+ * list carrying its content in an injected summary, for example). Retrying
847
+ * from there resends a byte-identical prompt, so the caller should stop.
848
+ */
849
+ overflowRecoveryStalled(currentPromptTokens) {
850
+ const previous = this._lastOverflowPromptTokens;
851
+ if (previous == null || currentPromptTokens == null || !Number.isFinite(currentPromptTokens)) return false;
852
+ return this.normalizePromptTokens(currentPromptTokens) >= previous;
853
+ }
854
+ normalizePromptTokens(promptTokens) {
855
+ if (this.calibrationRatio <= 0) return promptTokens;
856
+ const messageTokens = Math.max(0, promptTokens - this.instructionTokens);
857
+ return this.instructionTokens + messageTokens / this.calibrationRatio;
858
+ }
859
+ /**
860
+ * Undoes overflow corrections so a reused context starts the next run with
861
+ * the budget it was configured with and a fresh recovery allowance.
862
+ *
863
+ * Without this, a single overflow would permanently shrink the budget for
864
+ * every later turn, and two would exhaust the per-run allowance for the
865
+ * lifetime of the context.
866
+ */
867
+ restoreContextBudgetAfterOverflow() {
868
+ if (this._overflowRecoveryAttempts === 0) return;
869
+ this.maxContextTokens = this._preOverflowMaxContextTokens;
870
+ this._preOverflowMaxContextTokens = void 0;
871
+ this._lastOverflowPromptTokens = void 0;
872
+ this._overflowRecoveryAttempts = 0;
873
+ }
760
874
  clearSummary() {
761
875
  if (this.summaryText != null) {
762
876
  this.summaryText = void 0;