@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
@@ -26,12 +26,14 @@ import { AIMessageChunk } from '@langchain/core/messages';
26
26
  import { ChatGenerationChunk, ChatResult } from '@langchain/core/outputs';
27
27
  import {
28
28
  ConverseStreamCommand,
29
+ type ConverseStreamOutput,
29
30
  type GuardrailConfiguration,
30
31
  type GuardrailStreamConfiguration,
31
32
  } from '@aws-sdk/client-bedrock-runtime';
32
33
  import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
33
34
  import type { BaseMessage, ResponseMetadata } from '@langchain/core/messages';
34
35
  import type { ChatBedrockConverseInput } from '@langchain/aws';
36
+ import type { ContentBlockDeltaEvent } from './types';
35
37
  import {
36
38
  convertToConverseMessages,
37
39
  createConverseToolUseStopChunk,
@@ -39,7 +41,6 @@ import {
39
41
  handleConverseStreamContentBlockDelta,
40
42
  handleConverseStreamMetadata,
41
43
  } from './utils';
42
- import type { ContentBlockDeltaEvent } from './types';
43
44
  import {
44
45
  resolveBedrockPromptCacheTtl,
45
46
  supportsBedrockToolCache,
@@ -102,6 +103,22 @@ function splitStreamToken(text: string): string[] {
102
103
  return chunks;
103
104
  }
104
105
 
106
+ /**
107
+ * Resolves the text a delta contributes to the smoothing cadence, preferring a
108
+ * text delta over a reasoning delta and ignoring non-string payloads.
109
+ */
110
+ function resolveVisibleText(text?: string, reasoningText?: string): string {
111
+ if (typeof text === 'string') {
112
+ return text;
113
+ }
114
+
115
+ if (typeof reasoningText === 'string') {
116
+ return reasoningText;
117
+ }
118
+
119
+ return '';
120
+ }
121
+
105
122
  function getCadencedStreamDelay({
106
123
  targetDelay,
107
124
  lastVisibleContentAt,
@@ -282,10 +299,10 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
282
299
  const toolConfig =
283
300
  this.promptCache === true && supportsBedrockToolCache(this.cacheModelId)
284
301
  ? insertBedrockToolCachePoint(
285
- baseParams.toolConfig,
286
- true,
287
- resolveBedrockPromptCacheTtl(this.promptCacheTtl, this.cacheModelId)
288
- )
302
+ baseParams.toolConfig,
303
+ true,
304
+ resolveBedrockPromptCacheTtl(this.promptCacheTtl, this.cacheModelId)
305
+ )
289
306
  : baseParams.toolConfig;
290
307
 
291
308
  /** Service tier from options or fall back to class-level setting */
@@ -372,7 +389,8 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
372
389
  abortSignal: streamAbortController.signal,
373
390
  });
374
391
 
375
- const stream = response.stream;
392
+ const stream: AsyncIterable<ConverseStreamOutput> | undefined =
393
+ response.stream;
376
394
  if (!stream) {
377
395
  return;
378
396
  }
@@ -525,12 +543,7 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
525
543
  const text = delta.text;
526
544
  const reasoningContent = delta.reasoningContent;
527
545
  const reasoningText = reasoningContent?.text;
528
- const visibleText =
529
- typeof text === 'string'
530
- ? text
531
- : typeof reasoningText === 'string'
532
- ? reasoningText
533
- : '';
546
+ const visibleText = resolveVisibleText(text, reasoningText);
534
547
  const smooth = this._lc_stream_delay > 0 && visibleText !== '';
535
548
  const tokenChunks = smooth
536
549
  ? splitStreamToken(visibleText)
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Recovery policy for provider context-overflow rejections.
3
+ *
4
+ * Detection (`@/utils/errors`) answers "was this an overflow, and what did
5
+ * the provider disclose?". This module answers the follow-up: "what budget
6
+ * should the retry target?" — deliberately kept as pure functions so the
7
+ * policy can be reasoned about and tested without a graph.
8
+ *
9
+ * On units: `maxContextTokens` is a **provider-space** budget. The pruner
10
+ * converts it into its own raw estimate space by dividing by the
11
+ * `calibrationRatio` it learns from reported usage. A provider-reported
12
+ * ceiling is therefore applied verbatim — converting it here as well would
13
+ * apply the same correction twice and prune toward roughly `limit / ratio²`,
14
+ * silently discarding far more history than the overflow called for.
15
+ *
16
+ * `observedCalibrationRatio` is returned separately so the caller can seed
17
+ * the pruner's conversion without folding the same correction into the
18
+ * provider-space budget.
19
+ */
20
+ import type { ContextOverflowInfo } from '@/utils/errors';
21
+ import type { Providers } from '@/common';
22
+ import { getContextOverflowInfo } from '@/utils/errors';
23
+
24
+ /** Fraction of the previous budget used when the provider named no ceiling. */
25
+ const BLIND_SHRINK_RATIO = 0.7;
26
+
27
+ /** Slack left below a known ceiling so the retry is not sized to the edge. */
28
+ const CEILING_HEADROOM_RATIO = 0.95;
29
+
30
+ /**
31
+ * Fallback floor, used only when the instruction size is unknown. When it is
32
+ * known the floor is derived from it instead, so a genuinely small model — a
33
+ * 4k window, where a 95%-of-ceiling budget lands below this constant — can
34
+ * still recover.
35
+ */
36
+ const MIN_RECOVERY_BUDGET_TOKENS = 4_000;
37
+
38
+ /**
39
+ * Room a corrected budget must leave above the instructions for the messages
40
+ * themselves. Without it, a budget that merely clears the system prompt and
41
+ * tool schemas is not a budget anything can be compacted into.
42
+ */
43
+ const MIN_MESSAGE_HEADROOM_TOKENS = 2_000;
44
+
45
+ /** Bound on forced-compaction retries per agent, per run. */
46
+ export const DEFAULT_MAX_OVERFLOW_RECOVERIES = 2;
47
+
48
+ export interface OverflowRecoveryPlan {
49
+ /** Budget the retry should target, in provider token units when known. */
50
+ budgetTokens?: number;
51
+ /** What the provider disclosed. Carried through for logging. */
52
+ info: ContextOverflowInfo;
53
+ /**
54
+ * Provider-reported message tokens divided by our own message estimate,
55
+ * when both are known. Greater than 1 means we under-count relative to this
56
+ * provider.
57
+ *
58
+ * Returned separately so the graph can seed the pruner's calibration;
59
+ * applying it to this plan's budget as well would double-count. Fixed
60
+ * instruction overhead is removed before deriving it from
61
+ * `info.promptTokens`, never `info.requestedTokens`, since several providers
62
+ * fold the completion allowance into the latter.
63
+ */
64
+ observedCalibrationRatio?: number;
65
+ }
66
+
67
+ export interface OverflowRecoveryParams {
68
+ error: unknown;
69
+ provider: Providers;
70
+ /** Budget in force when the rejected prompt was built. */
71
+ maxContextTokens?: number;
72
+ /** Our own estimate of the prompt we actually sent. */
73
+ estimatedPromptTokens?: number;
74
+ /** Provider/local calibration already applied to the prompt estimate. */
75
+ calibrationRatio?: number;
76
+ /**
77
+ * System prompt plus tool schemas — the part of the budget compaction
78
+ * cannot touch. A corrected budget at or below this leaves no room for
79
+ * messages, and the summarize node refuses to run, so recovery is declined
80
+ * rather than entered.
81
+ */
82
+ instructionTokens?: number;
83
+ /** Whether a model-backed summary can compact messages without a pruner. */
84
+ canSummarize?: boolean;
85
+ /**
86
+ * Completion allowance the caller configured. Providers count it against
87
+ * the same ceiling, so it has to come off the top when the error itself did
88
+ * not break the total down.
89
+ */
90
+ configuredCompletionTokens?: number;
91
+ /** Recoveries already attempted for this agent in this run. */
92
+ attemptsSoFar: number;
93
+ maxAttempts?: number;
94
+ }
95
+
96
+ function isUsable(value: number | undefined): value is number {
97
+ return value != null && Number.isFinite(value) && value > 0;
98
+ }
99
+
100
+ /**
101
+ * Converts a provider-space retry budget into the units consumed by a pruner
102
+ * calibrated for another provider while preserving the same raw-token limit.
103
+ */
104
+ export function translateRecoveryBudget(
105
+ budgetTokens: number | undefined,
106
+ sourceCalibrationRatio: number | undefined,
107
+ targetCalibrationRatio: number | undefined
108
+ ): number | undefined {
109
+ if (
110
+ !isUsable(budgetTokens) ||
111
+ !isUsable(sourceCalibrationRatio) ||
112
+ !isUsable(targetCalibrationRatio)
113
+ ) {
114
+ return budgetTokens;
115
+ }
116
+ return Math.floor(
117
+ (budgetTokens * targetCalibrationRatio) / sourceCalibrationRatio
118
+ );
119
+ }
120
+
121
+ /** Applies the conservative shrink used when no provider-space ceiling is usable. */
122
+ export function getBlindRecoveryBudget(
123
+ maxContextTokens: number | undefined
124
+ ): number | undefined {
125
+ return isUsable(maxContextTokens)
126
+ ? Math.floor(maxContextTokens * BLIND_SHRINK_RATIO)
127
+ : undefined;
128
+ }
129
+
130
+ /**
131
+ * The completion allowance the provider counted against the same ceiling,
132
+ * when it reported both the total and the prompt portion. The retry budget
133
+ * governs the prompt only, so this has to come off the ceiling first —
134
+ * otherwise a large `maxTokens` keeps the request over the limit no matter
135
+ * how far the prompt is compacted.
136
+ */
137
+ function reservedForCompletion(
138
+ info: ContextOverflowInfo,
139
+ configuredCompletionTokens: number | undefined
140
+ ): number {
141
+ if (isUsable(info.requestedTokens) && isUsable(info.promptTokens)) {
142
+ const difference = info.requestedTokens - info.promptTokens;
143
+ if (difference > 0) {
144
+ return difference;
145
+ }
146
+ }
147
+ /**
148
+ * No breakdown on offer. Fall back to what the caller configured, because
149
+ * the provider still counts it: targeting the whole ceiling would leave the
150
+ * retry at `prompt + maxTokens` and over the limit however far the prompt
151
+ * is compacted.
152
+ */
153
+ return isUsable(configuredCompletionTokens) ? configuredCompletionTokens : 0;
154
+ }
155
+
156
+ function resolveTargetBudget(
157
+ info: ContextOverflowInfo,
158
+ maxContextTokens: number | undefined,
159
+ estimatedPromptTokens: number | undefined,
160
+ configuredCompletionTokens: number | undefined
161
+ ): number | null {
162
+ if (isUsable(info.limitTokens)) {
163
+ const promptCeiling =
164
+ info.limitTokens -
165
+ reservedForCompletion(info, configuredCompletionTokens);
166
+ /**
167
+ * A completion allowance at or above the ceiling leaves nothing for the
168
+ * prompt: even an empty one plus the requested output overruns the limit.
169
+ * Compaction cannot fix that, so declining surfaces the real problem
170
+ * instead of burning the recovery budget on retries that must fail.
171
+ */
172
+ if (promptCeiling <= 0) {
173
+ return null;
174
+ }
175
+
176
+ return promptCeiling * CEILING_HEADROOM_RATIO;
177
+ }
178
+ if (isUsable(estimatedPromptTokens)) {
179
+ return estimatedPromptTokens * BLIND_SHRINK_RATIO;
180
+ }
181
+ if (isUsable(maxContextTokens)) {
182
+ return maxContextTokens * BLIND_SHRINK_RATIO;
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Decides whether a failed model call is a recoverable context overflow and,
189
+ * if so, what budget the retry should be re-pruned against.
190
+ *
191
+ * Returns `null` when the error is something compaction cannot fix, or when
192
+ * the per-run recovery budget is spent — in both cases the caller should let
193
+ * its normal failure handling proceed.
194
+ */
195
+ export function planContextOverflowRecovery({
196
+ error,
197
+ provider,
198
+ maxContextTokens,
199
+ estimatedPromptTokens,
200
+ calibrationRatio,
201
+ instructionTokens,
202
+ canSummarize = false,
203
+ configuredCompletionTokens,
204
+ attemptsSoFar,
205
+ maxAttempts = DEFAULT_MAX_OVERFLOW_RECOVERIES,
206
+ }: OverflowRecoveryParams): OverflowRecoveryPlan | null {
207
+ if (attemptsSoFar >= maxAttempts) {
208
+ return null;
209
+ }
210
+
211
+ const info = getContextOverflowInfo(error, {
212
+ provider,
213
+ estimatedPromptTokens,
214
+ maxContextTokens,
215
+ });
216
+ if (info == null) {
217
+ return null;
218
+ }
219
+
220
+ const currentCalibrationRatio = isUsable(calibrationRatio)
221
+ ? calibrationRatio
222
+ : 1;
223
+ const estimatedMessageTokens =
224
+ isUsable(estimatedPromptTokens) && isUsable(instructionTokens)
225
+ ? estimatedPromptTokens - instructionTokens
226
+ : estimatedPromptTokens;
227
+ const observedMessageTokens =
228
+ isUsable(info.promptTokens) && isUsable(instructionTokens)
229
+ ? info.promptTokens - instructionTokens
230
+ : info.promptTokens;
231
+ const observedCalibrationRatio =
232
+ isUsable(observedMessageTokens) && isUsable(estimatedMessageTokens)
233
+ ? (observedMessageTokens / estimatedMessageTokens) *
234
+ currentCalibrationRatio
235
+ : undefined;
236
+
237
+ const target = resolveTargetBudget(
238
+ info,
239
+ maxContextTokens,
240
+ estimatedPromptTokens,
241
+ configuredCompletionTokens
242
+ );
243
+ if (target == null) {
244
+ const hasNumericBasis =
245
+ isUsable(info.limitTokens) ||
246
+ isUsable(maxContextTokens) ||
247
+ isUsable(estimatedPromptTokens);
248
+ return hasNumericBasis
249
+ ? null
250
+ : { info, observedCalibrationRatio, budgetTokens: undefined };
251
+ }
252
+
253
+ /**
254
+ * A retry that does not actually shrink the prompt would just reproduce the
255
+ * same rejection, so a ceiling that lands at or above the budget we already
256
+ * had is replaced by a blind shrink.
257
+ */
258
+ const bounded =
259
+ isUsable(maxContextTokens) && target >= maxContextTokens
260
+ ? (getBlindRecoveryBudget(maxContextTokens) ?? target)
261
+ : target;
262
+
263
+ const budgetTokens = Math.floor(bounded);
264
+
265
+ /**
266
+ * Below the floor there is no usable budget left: either nothing survives
267
+ * pruning, or the instructions alone fill the window, in which case the
268
+ * summarize node refuses to run and the detour would bounce between the
269
+ * agent and summarize nodes without ever shrinking the prompt. Declining
270
+ * lets the existing "instructions exceed context budget" guidance surface
271
+ * instead.
272
+ */
273
+ let floorTokens = MIN_RECOVERY_BUDGET_TOKENS;
274
+ if (isUsable(instructionTokens)) {
275
+ floorTokens = instructionTokens + MIN_MESSAGE_HEADROOM_TOKENS;
276
+ } else if (canSummarize) {
277
+ floorTokens = 1;
278
+ }
279
+ if (budgetTokens < floorTokens) {
280
+ return null;
281
+ }
282
+
283
+ /**
284
+ * Refuse to report a "recovery" that changes nothing: when the budget in
285
+ * force is already at or below the target, re-pruning cannot free space.
286
+ */
287
+ if (isUsable(maxContextTokens) && budgetTokens >= maxContextTokens) {
288
+ return null;
289
+ }
290
+
291
+ return { budgetTokens, info, observedCalibrationRatio };
292
+ }
package/src/llm/invoke.ts CHANGED
@@ -4,11 +4,13 @@ import type { RunnableConfig } from '@langchain/core/runnables';
4
4
  import type { ToolCall } from '@langchain/core/messages/tool';
5
5
  import type { BaseMessage } from '@langchain/core/messages';
6
6
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
7
+ import type { ContextOverflowContext } from '@/utils/errors';
7
8
  import type * as t from '@/types';
8
9
  import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
9
10
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
10
11
  import { Constants, GraphEvents, Providers } from '@/common';
11
12
  import { manualToolStreamProviders } from '@/llm/providers';
13
+ import { getContextOverflowInfo } from '@/utils/errors';
12
14
  import { modifyDeltaProperties } from '@/messages';
13
15
  import { ChatModelStreamHandler } from '@/stream';
14
16
  import { initializeModel } from '@/llm/init';
@@ -312,6 +314,59 @@ export async function attemptInvoke(
312
314
  return { messages: [finalMessage] };
313
315
  }
314
316
 
317
+ /**
318
+ * Identifies which fallback produced an error, so a caller planning a
319
+ * recovery can reason about the client that actually failed rather than the
320
+ * primary's configuration — their context windows and output allowances
321
+ * differ, which is the whole reason a fallback exists.
322
+ */
323
+ export interface FallbackErrorContext {
324
+ provider: Providers;
325
+ clientOptions?: t.ClientOptions;
326
+ maxContextTokens?: number;
327
+ }
328
+
329
+ export interface FallbackOverflowCandidate {
330
+ error: unknown;
331
+ context: FallbackErrorContext;
332
+ }
333
+
334
+ const fallbackErrorContexts = new WeakMap<object, FallbackErrorContext>();
335
+ const fallbackOverflowCandidates = new WeakMap<
336
+ object,
337
+ FallbackOverflowCandidate[]
338
+ >();
339
+
340
+ function attachFallbackErrorContext(
341
+ error: unknown,
342
+ fallbackContext: FallbackErrorContext
343
+ ): void {
344
+ if (typeof error !== 'object' || error === null) {
345
+ return;
346
+ }
347
+ fallbackErrorContexts.set(error, fallbackContext);
348
+ }
349
+
350
+ /** Reads back the fallback attribution attached by `tryFallbackProviders`. */
351
+ export function getFallbackErrorContext(
352
+ error: unknown
353
+ ): FallbackErrorContext | undefined {
354
+ if (typeof error !== 'object' || error === null) {
355
+ return undefined;
356
+ }
357
+ return fallbackErrorContexts.get(error);
358
+ }
359
+
360
+ /** Returns every fallback overflow retained from an exhausted provider chain. */
361
+ export function getFallbackOverflowCandidates(
362
+ error: unknown
363
+ ): FallbackOverflowCandidate[] {
364
+ if (typeof error !== 'object' || error === null) {
365
+ return [];
366
+ }
367
+ return [...(fallbackOverflowCandidates.get(error) ?? [])];
368
+ }
369
+
315
370
  /**
316
371
  * Best-effort read of the configured model name from client options.
317
372
  * Providers disagree on the key (`model` vs `modelName`).
@@ -333,7 +388,12 @@ function extractClientOptionsModel(
333
388
 
334
389
  /**
335
390
  * Attempts each fallback provider in order until one succeeds.
336
- * Throws the last error if all fallbacks fail.
391
+ *
392
+ * When every fallback fails, a context overflow among them is thrown in
393
+ * preference to whichever failure happened to come last. An overflow is the
394
+ * one failure the caller can act on — it compacts and retries — and losing it
395
+ * behind a later unrelated error would surface a dead end instead. Ordinary
396
+ * failures still throw last-error-wins.
337
397
  */
338
398
  export async function tryFallbackProviders({
339
399
  fallbacks,
@@ -343,16 +403,38 @@ export async function tryFallbackProviders({
343
403
  primaryError,
344
404
  context,
345
405
  onChunk,
406
+ overflowContext,
346
407
  }: {
347
- fallbacks: Array<{ provider: Providers; clientOptions?: t.ClientOptions }>;
408
+ fallbacks: t.FallbackConfig[];
348
409
  tools?: t.GraphTools;
349
410
  messages: BaseMessage[];
350
411
  config?: RunnableConfig;
351
412
  primaryError: unknown;
352
413
  context?: InvokeContext;
353
414
  onChunk?: OnChunk;
415
+ /**
416
+ * Prompt-size corroboration for signatures that are not self-describing.
417
+ * Vertex AI's overflow is a bare `400` with no reason, so without this a
418
+ * fallback that overflows is indistinguishable from any other 400 and would
419
+ * be dropped in favour of whichever failure came last.
420
+ */
421
+ overflowContext?: ContextOverflowContext;
354
422
  }): Promise<Partial<t.BaseGraphState> | undefined> {
423
+ const isOverflow = (
424
+ error: unknown,
425
+ contextOverride = overflowContext
426
+ ): boolean => getContextOverflowInfo(error, contextOverride) != null;
355
427
  let lastError: unknown = primaryError;
428
+ /**
429
+ * Tracked apart from the primary's overflow. A caller reaching this
430
+ * function with an overflowing primary has already failed to recover from
431
+ * it, so a fallback overflow — which may sit against a different window and
432
+ * output allowance — is the more useful of the two to surface.
433
+ */
434
+ const overflowCandidates: FallbackOverflowCandidate[] = [];
435
+ const primaryOverflowError: unknown = isOverflow(primaryError)
436
+ ? primaryError
437
+ : undefined;
356
438
  for (const fb of fallbacks) {
357
439
  try {
358
440
  const fbModel = initializeModel({
@@ -391,11 +473,44 @@ export async function tryFallbackProviders({
391
473
  return result;
392
474
  } catch (e) {
393
475
  lastError = e;
476
+ const fallbackOverflowContext: ContextOverflowContext = {
477
+ provider: fb.provider,
478
+ maxContextTokens: fb.maxContextTokens,
479
+ ...(overflowContext?.provider === fb.provider
480
+ ? {
481
+ estimatedPromptTokens: overflowContext.estimatedPromptTokens,
482
+ }
483
+ : {}),
484
+ };
485
+ if (isOverflow(e, fallbackOverflowContext)) {
486
+ const errorContext: FallbackErrorContext = {
487
+ provider: fb.provider,
488
+ clientOptions: fb.clientOptions,
489
+ maxContextTokens: fb.maxContextTokens,
490
+ };
491
+ attachFallbackErrorContext(e, errorContext);
492
+ overflowCandidates.push({ error: e, context: errorContext });
493
+ }
394
494
  continue;
395
495
  }
396
496
  }
397
- if (lastError !== undefined) {
398
- throw lastError;
497
+ /**
498
+ * Preference order: a fallback overflow, then the primary's overflow, then
499
+ * whichever failure came last. An overflow is the only one of the three a
500
+ * caller can act on, and the fallback's carries the client attribution that
501
+ * makes a correct retry budget possible.
502
+ */
503
+ const preferred =
504
+ overflowCandidates[0]?.error ?? primaryOverflowError ?? lastError;
505
+ if (
506
+ overflowCandidates.length > 0 &&
507
+ typeof preferred === 'object' &&
508
+ preferred !== null
509
+ ) {
510
+ fallbackOverflowCandidates.set(preferred, overflowCandidates);
511
+ }
512
+ if (preferred !== undefined) {
513
+ throw preferred;
399
514
  }
400
515
  return undefined;
401
516
  }