@librechat/agents 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/agents/AgentContext.cjs +116 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +229 -48
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +102 -27
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +11 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
- package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
- package/dist/cjs/llm/google/index.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +60 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/main.cjs +20 -7
- package/dist/cjs/messages/prune.cjs +16 -5
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +2 -0
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/run.cjs +15 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +205 -49
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +55 -7
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +2 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +69 -8
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +32 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/utils/errors.cjs +317 -52
- package/dist/cjs/utils/errors.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +117 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +227 -46
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +102 -27
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +11 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
- package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
- package/dist/esm/llm/google/index.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +59 -4
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/main.mjs +9 -9
- package/dist/esm/messages/prune.mjs +14 -6
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +2 -1
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/run.mjs +15 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +205 -49
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +55 -7
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +62 -9
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +32 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/utils/errors.mjs +317 -53
- package/dist/esm/utils/errors.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +62 -3
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +16 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
- package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
- package/dist/types/llm/invoke.d.ts +35 -6
- package/dist/types/messages/prune.d.ts +10 -2
- package/dist/types/messages/recency.d.ts +1 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -1
- package/dist/types/types/llm.d.ts +7 -4
- package/dist/types/types/stream.d.ts +5 -4
- package/dist/types/types/summarize.d.ts +22 -0
- package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
- package/dist/types/utils/errors.d.ts +65 -16
- package/dist/types/utils/redactSecrets.d.ts +3 -0
- package/package.json +10 -11
- package/src/agents/AgentContext.ts +188 -7
- package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +389 -58
- package/src/graphs/MultiAgentGraph.ts +184 -46
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
- package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
- package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
- package/src/llm/anthropic/llm.spec.ts +8 -0
- package/src/llm/bedrock/index.ts +25 -12
- package/src/llm/contextOverflowRecovery.ts +292 -0
- package/src/llm/invoke.ts +119 -4
- package/src/messages/prune.ts +24 -11
- package/src/messages/recency.ts +3 -1
- package/src/run.ts +24 -1
- package/src/scripts/context-overflow-probe.ts +997 -0
- package/src/specs/agent-handoffs.test.ts +903 -1
- package/src/specs/context-overflow-recovery.live.test.ts +213 -0
- package/src/splitStream.test.ts +882 -0
- package/src/stream.ts +315 -51
- package/src/summarization/__tests__/aggregator.test.ts +83 -0
- package/src/summarization/__tests__/node.test.ts +139 -0
- package/src/summarization/node.ts +99 -14
- package/src/tools/BashExecutor.ts +4 -2
- package/src/tools/BashProgrammaticToolCalling.ts +4 -7
- package/src/tools/CodeExecutor.ts +119 -8
- package/src/tools/ProgrammaticToolCalling.ts +29 -27
- package/src/tools/ToolNode.ts +50 -8
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
- package/src/types/llm.ts +8 -1
- package/src/types/stream.ts +5 -4
- package/src/types/summarize.ts +22 -0
- package/src/utils/__tests__/errors.test.ts +270 -0
- package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
- package/src/utils/__tests__/redactSecrets.test.ts +56 -0
- package/src/utils/errors.ts +484 -66
- package/src/utils/redactSecrets.ts +61 -0
|
@@ -976,6 +976,7 @@ test('Anthropic message_delta usage preserves cumulative input totals', () => {
|
|
|
976
976
|
output_tokens: 375,
|
|
977
977
|
cache_creation_input_tokens: 11,
|
|
978
978
|
cache_read_input_tokens: 13,
|
|
979
|
+
fallback_credit: null,
|
|
979
980
|
server_tool_use: null,
|
|
980
981
|
iterations: null,
|
|
981
982
|
output_tokens_details: null,
|
|
@@ -1018,6 +1019,7 @@ test('Anthropic stream usage does not double-count cumulative input tokens', asy
|
|
|
1018
1019
|
cache_creation: null,
|
|
1019
1020
|
cache_creation_input_tokens: 20,
|
|
1020
1021
|
cache_read_input_tokens: 30,
|
|
1022
|
+
fallback_credit: null,
|
|
1021
1023
|
inference_geo: null,
|
|
1022
1024
|
input_tokens: 243,
|
|
1023
1025
|
iterations: null,
|
|
@@ -1043,6 +1045,7 @@ test('Anthropic stream usage does not double-count cumulative input tokens', asy
|
|
|
1043
1045
|
output_tokens: 375,
|
|
1044
1046
|
cache_creation_input_tokens: 20,
|
|
1045
1047
|
cache_read_input_tokens: 30,
|
|
1048
|
+
fallback_credit: null,
|
|
1046
1049
|
server_tool_use: null,
|
|
1047
1050
|
iterations: null,
|
|
1048
1051
|
output_tokens_details: null,
|
|
@@ -1089,6 +1092,7 @@ test('Anthropic stream usage accepts input first reported at message_delta', asy
|
|
|
1089
1092
|
cache_creation: null,
|
|
1090
1093
|
cache_creation_input_tokens: 0,
|
|
1091
1094
|
cache_read_input_tokens: 0,
|
|
1095
|
+
fallback_credit: null,
|
|
1092
1096
|
inference_geo: null,
|
|
1093
1097
|
input_tokens: 0,
|
|
1094
1098
|
iterations: null,
|
|
@@ -1114,6 +1118,7 @@ test('Anthropic stream usage accepts input first reported at message_delta', asy
|
|
|
1114
1118
|
output_tokens: 42,
|
|
1115
1119
|
cache_creation_input_tokens: 500,
|
|
1116
1120
|
cache_read_input_tokens: 200,
|
|
1121
|
+
fallback_credit: null,
|
|
1117
1122
|
server_tool_use: null,
|
|
1118
1123
|
iterations: null,
|
|
1119
1124
|
output_tokens_details: null,
|
|
@@ -1160,6 +1165,7 @@ test('Anthropic stream usage handles multiple cumulative message_delta events',
|
|
|
1160
1165
|
cache_creation: null,
|
|
1161
1166
|
cache_creation_input_tokens: 0,
|
|
1162
1167
|
cache_read_input_tokens: 0,
|
|
1168
|
+
fallback_credit: null,
|
|
1163
1169
|
inference_geo: null,
|
|
1164
1170
|
input_tokens: 243,
|
|
1165
1171
|
iterations: null,
|
|
@@ -1185,6 +1191,7 @@ test('Anthropic stream usage handles multiple cumulative message_delta events',
|
|
|
1185
1191
|
output_tokens: 100,
|
|
1186
1192
|
cache_creation_input_tokens: 0,
|
|
1187
1193
|
cache_read_input_tokens: 0,
|
|
1194
|
+
fallback_credit: null,
|
|
1188
1195
|
server_tool_use: null,
|
|
1189
1196
|
iterations: null,
|
|
1190
1197
|
output_tokens_details: null,
|
|
@@ -1204,6 +1211,7 @@ test('Anthropic stream usage handles multiple cumulative message_delta events',
|
|
|
1204
1211
|
output_tokens: 375,
|
|
1205
1212
|
cache_creation_input_tokens: 0,
|
|
1206
1213
|
cache_read_input_tokens: 0,
|
|
1214
|
+
fallback_credit: null,
|
|
1207
1215
|
server_tool_use: null,
|
|
1208
1216
|
iterations: null,
|
|
1209
1217
|
output_tokens_details: null,
|
package/src/llm/bedrock/index.ts
CHANGED
|
@@ -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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 =
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
|
|
398
|
-
|
|
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
|
}
|