@librechat/agents 3.3.0 → 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.
- 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 +7 -8
- 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/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
package/src/graphs/Graph.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
MessageContent,
|
|
12
12
|
} from '@langchain/core/messages';
|
|
13
13
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
14
|
+
import type { OverflowRecoveryPlan } from '@/llm/contextOverflowRecovery';
|
|
15
|
+
import type { FallbackErrorContext } from '@/llm/invoke';
|
|
14
16
|
import type { HookRegistry } from '@/hooks';
|
|
15
17
|
import type * as t from '@/types';
|
|
16
18
|
import {
|
|
@@ -22,9 +24,9 @@ import {
|
|
|
22
24
|
extractToolDiscoveries,
|
|
23
25
|
addBedrockTailCacheControl,
|
|
24
26
|
formatArtifactPayload,
|
|
25
|
-
enforceOriginalContentCap,
|
|
26
27
|
formatContentStrings,
|
|
27
28
|
isLegacyConvertible,
|
|
29
|
+
CALIBRATION_RATIO_MAX,
|
|
28
30
|
createPruneMessages,
|
|
29
31
|
syncBudgetDerivedFields,
|
|
30
32
|
addTailCacheControl,
|
|
@@ -34,6 +36,8 @@ import {
|
|
|
34
36
|
getMessageId,
|
|
35
37
|
makeIsDeferred,
|
|
36
38
|
partitionAndMarkAnthropicToolCache,
|
|
39
|
+
DEFAULT_RETAIN_RECENT_TURNS,
|
|
40
|
+
splitAtRecencyBoundary,
|
|
37
41
|
} from '@/messages';
|
|
38
42
|
import {
|
|
39
43
|
createLangfuseHandler,
|
|
@@ -51,16 +55,28 @@ import {
|
|
|
51
55
|
sleep,
|
|
52
56
|
} from '@/utils';
|
|
53
57
|
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
getBlindRecoveryBudget,
|
|
59
|
+
planContextOverflowRecovery,
|
|
60
|
+
translateRecoveryBudget,
|
|
61
|
+
} from '@/llm/contextOverflowRecovery';
|
|
62
|
+
import {
|
|
63
|
+
attemptInvoke,
|
|
64
|
+
tryFallbackProviders,
|
|
65
|
+
getFallbackErrorContext,
|
|
66
|
+
getFallbackOverflowCandidates,
|
|
67
|
+
} from '@/llm/invoke';
|
|
57
68
|
import {
|
|
69
|
+
Constants,
|
|
58
70
|
GraphNodeKeys,
|
|
59
71
|
ContentTypes,
|
|
60
72
|
GraphEvents,
|
|
61
73
|
Providers,
|
|
62
74
|
StepTypes,
|
|
63
75
|
} from '@/common';
|
|
76
|
+
import {
|
|
77
|
+
resolveLangfuseRuntimeScope,
|
|
78
|
+
withLangfuseRuntimeScope,
|
|
79
|
+
} from '@/langfuseRuntimeScope';
|
|
64
80
|
import {
|
|
65
81
|
appendCallbacks,
|
|
66
82
|
findCallback,
|
|
@@ -75,7 +91,6 @@ import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
|
|
|
75
91
|
import { partitionAndMarkBedrockToolCache } from '@/llm/bedrock/toolCache';
|
|
76
92
|
import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
|
|
77
93
|
import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
|
|
78
|
-
import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
|
|
79
94
|
import { buildSubagentToolParams } from '@/tools/SubagentTool';
|
|
80
95
|
import { initializeLangfuseTracing } from '@/instrumentation';
|
|
81
96
|
import { shouldTriggerSummarization } from '@/summarization';
|
|
@@ -370,6 +385,61 @@ function clearCurrentDeltaStepMarkers({
|
|
|
370
385
|
}
|
|
371
386
|
}
|
|
372
387
|
|
|
388
|
+
/**
|
|
389
|
+
* The completion allowance the caller configured, under whichever key the
|
|
390
|
+
* provider's client uses. Providers count it against the same ceiling as the
|
|
391
|
+
* prompt, so overflow recovery has to reserve it when the error did not
|
|
392
|
+
* itemize the total.
|
|
393
|
+
*/
|
|
394
|
+
function getConfiguredCompletionTokens(
|
|
395
|
+
clientOptions: t.ClientOptions | undefined
|
|
396
|
+
): number | undefined {
|
|
397
|
+
const options = clientOptions as
|
|
398
|
+
| { maxTokens?: unknown; maxOutputTokens?: unknown }
|
|
399
|
+
| undefined;
|
|
400
|
+
for (const value of [options?.maxTokens, options?.maxOutputTokens]) {
|
|
401
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Our own estimate of the prompt that was actually sent, derived from the
|
|
410
|
+
* pre-invoke usage snapshot. Used to corroborate ambiguous provider errors
|
|
411
|
+
* and to measure how far our token accounting sits from the provider's.
|
|
412
|
+
*/
|
|
413
|
+
function getEstimatedPromptTokens(
|
|
414
|
+
contextUsage: t.ContextUsageEvent | null
|
|
415
|
+
): number | undefined {
|
|
416
|
+
const budget = contextUsage?.contextBudget;
|
|
417
|
+
const remaining = contextUsage?.remainingContextTokens;
|
|
418
|
+
if (
|
|
419
|
+
budget == null ||
|
|
420
|
+
remaining == null ||
|
|
421
|
+
!Number.isFinite(budget) ||
|
|
422
|
+
!Number.isFinite(remaining)
|
|
423
|
+
) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
const used = budget - remaining;
|
|
427
|
+
return used > 0 ? used : undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function minDefined(
|
|
431
|
+
left: number | undefined,
|
|
432
|
+
right: number | undefined
|
|
433
|
+
): number | undefined {
|
|
434
|
+
if (left == null) {
|
|
435
|
+
return right;
|
|
436
|
+
}
|
|
437
|
+
if (right == null) {
|
|
438
|
+
return left;
|
|
439
|
+
}
|
|
440
|
+
return Math.min(left, right);
|
|
441
|
+
}
|
|
442
|
+
|
|
373
443
|
async function dispatchMessageCreationStep({
|
|
374
444
|
graph,
|
|
375
445
|
stepKey,
|
|
@@ -520,7 +590,7 @@ export abstract class Graph<
|
|
|
520
590
|
T extends t.BaseGraphState = t.BaseGraphState,
|
|
521
591
|
_TNodeName extends string = string,
|
|
522
592
|
> {
|
|
523
|
-
abstract resetValues(): void;
|
|
593
|
+
abstract resetValues(keepContent?: boolean, checkpointScope?: string): void;
|
|
524
594
|
abstract initializeTools({
|
|
525
595
|
currentTools,
|
|
526
596
|
currentToolMap,
|
|
@@ -862,9 +932,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
862
932
|
overrideModel?: t.ChatModel;
|
|
863
933
|
/** Optional compile options passed into workflow.compile() */
|
|
864
934
|
compileOptions?: t.CompileOptions | undefined;
|
|
935
|
+
/** Whether the workflow was actually compiled with a checkpointer. */
|
|
936
|
+
hasCompiledCheckpointer: boolean = false;
|
|
865
937
|
messages: BaseMessage[] = [];
|
|
866
938
|
/** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
|
|
867
939
|
private cachedRunMessages?: BaseMessage[];
|
|
940
|
+
/** Checkpoint scope whose messages match index-keyed tool snapshots. */
|
|
941
|
+
private originalToolContentCheckpointScope?: string;
|
|
868
942
|
runId: string | undefined;
|
|
869
943
|
/**
|
|
870
944
|
* Boundary between historical messages (loaded from conversation state)
|
|
@@ -929,7 +1003,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
929
1003
|
|
|
930
1004
|
/* Init */
|
|
931
1005
|
|
|
932
|
-
resetValues(keepContent?: boolean): void {
|
|
1006
|
+
resetValues(keepContent?: boolean, checkpointScope?: string): void {
|
|
933
1007
|
this.messages = [];
|
|
934
1008
|
this.cachedRunMessages = undefined;
|
|
935
1009
|
this.config = resetIfNotEmpty(this.config, undefined);
|
|
@@ -977,9 +1051,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
977
1051
|
new Map()
|
|
978
1052
|
);
|
|
979
1053
|
this.invokedToolIds = resetIfNotEmpty(this.invokedToolIds, undefined);
|
|
1054
|
+
const hasScopedCheckpoint =
|
|
1055
|
+
this.hasCompiledCheckpointer &&
|
|
1056
|
+
checkpointScope != null &&
|
|
1057
|
+
checkpointScope !== '';
|
|
1058
|
+
const preserveOriginalToolContent =
|
|
1059
|
+
hasScopedCheckpoint &&
|
|
1060
|
+
this.originalToolContentCheckpointScope === checkpointScope;
|
|
980
1061
|
for (const context of this.agentContexts.values()) {
|
|
981
|
-
context.reset();
|
|
1062
|
+
context.reset({ preserveOriginalToolContent });
|
|
982
1063
|
}
|
|
1064
|
+
this.originalToolContentCheckpointScope = hasScopedCheckpoint
|
|
1065
|
+
? checkpointScope
|
|
1066
|
+
: undefined;
|
|
983
1067
|
}
|
|
984
1068
|
|
|
985
1069
|
override clearHeavyState(): void {
|
|
@@ -987,8 +1071,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
987
1071
|
super.clearHeavyState();
|
|
988
1072
|
this.messages = [];
|
|
989
1073
|
this.overrideModel = undefined;
|
|
1074
|
+
const preserveOriginalToolContent =
|
|
1075
|
+
this.hasCompiledCheckpointer &&
|
|
1076
|
+
this.originalToolContentCheckpointScope != null;
|
|
990
1077
|
for (const context of this.agentContexts.values()) {
|
|
991
|
-
context.reset();
|
|
1078
|
+
context.reset({ preserveOriginalToolContent });
|
|
992
1079
|
}
|
|
993
1080
|
}
|
|
994
1081
|
|
|
@@ -1445,6 +1532,82 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1445
1532
|
client.abortHandler = undefined;
|
|
1446
1533
|
}
|
|
1447
1534
|
|
|
1535
|
+
/**
|
|
1536
|
+
* Applies a context-overflow recovery plan and hands control to the
|
|
1537
|
+
* summarize node, which compacts and then routes straight back here for a
|
|
1538
|
+
* retry against the corrected budget.
|
|
1539
|
+
*
|
|
1540
|
+
* Returning the detour rather than rethrowing is the whole point: the
|
|
1541
|
+
* caller never sees the provider's rejection, only a slightly longer turn.
|
|
1542
|
+
*/
|
|
1543
|
+
private beginOverflowRecovery({
|
|
1544
|
+
recovery,
|
|
1545
|
+
agentContext,
|
|
1546
|
+
agentId,
|
|
1547
|
+
config,
|
|
1548
|
+
originalToolContent,
|
|
1549
|
+
estimatedPromptTokens,
|
|
1550
|
+
}: {
|
|
1551
|
+
recovery: OverflowRecoveryPlan;
|
|
1552
|
+
agentContext: AgentContext;
|
|
1553
|
+
agentId: string;
|
|
1554
|
+
config?: RunnableConfig;
|
|
1555
|
+
/** Masking record from the prune pass that built the rejected prompt. */
|
|
1556
|
+
originalToolContent?: Map<number, string>;
|
|
1557
|
+
/** Size of the rejected prompt, recorded to detect a correction that changed nothing. */
|
|
1558
|
+
estimatedPromptTokens?: number;
|
|
1559
|
+
}): Partial<t.AgentSubgraphState> {
|
|
1560
|
+
const previousBudget = agentContext.maxContextTokens;
|
|
1561
|
+
/**
|
|
1562
|
+
* Deterministic compaction first. Re-pruning against the corrected budget
|
|
1563
|
+
* raises context pressure, which is what drives the pruner's tool-output
|
|
1564
|
+
* truncation and observation masking — no model call, no cost, and no
|
|
1565
|
+
* message content lost. A summarization call is held back until that has
|
|
1566
|
+
* been tried and the provider rejected the prompt again.
|
|
1567
|
+
*/
|
|
1568
|
+
const allowSummarization = agentContext.shouldSummarizeOverflow();
|
|
1569
|
+
|
|
1570
|
+
agentContext.preserveOriginalToolContent(originalToolContent);
|
|
1571
|
+
agentContext.applyContextBudgetCorrection(
|
|
1572
|
+
recovery.budgetTokens,
|
|
1573
|
+
estimatedPromptTokens
|
|
1574
|
+
);
|
|
1575
|
+
agentContext.applyObservedOverflowCalibration(
|
|
1576
|
+
recovery.info.provider,
|
|
1577
|
+
recovery.observedCalibrationRatio
|
|
1578
|
+
);
|
|
1579
|
+
|
|
1580
|
+
emitAgentLog(
|
|
1581
|
+
config,
|
|
1582
|
+
'warn',
|
|
1583
|
+
'graph',
|
|
1584
|
+
'Provider rejected the prompt as too large — compacting and retrying',
|
|
1585
|
+
{
|
|
1586
|
+
kind: recovery.info.kind,
|
|
1587
|
+
previousBudget,
|
|
1588
|
+
recoveredBudget: recovery.budgetTokens,
|
|
1589
|
+
providerReportedLimit: recovery.info.limitTokens,
|
|
1590
|
+
providerReportedTokens: recovery.info.requestedTokens,
|
|
1591
|
+
providerReportedPromptTokens: recovery.info.promptTokens,
|
|
1592
|
+
observedCalibrationRatio: recovery.observedCalibrationRatio,
|
|
1593
|
+
detectedBy: recovery.info.source,
|
|
1594
|
+
attempt: agentContext.overflowRecoveryAttempts,
|
|
1595
|
+
compaction: allowSummarization ? 'summarize' : 'compress',
|
|
1596
|
+
},
|
|
1597
|
+
{ runId: this.runId, agentId },
|
|
1598
|
+
{ force: true }
|
|
1599
|
+
);
|
|
1600
|
+
|
|
1601
|
+
return {
|
|
1602
|
+
summarizationRequest: {
|
|
1603
|
+
remainingContextTokens: 0,
|
|
1604
|
+
agentId: agentId || agentContext.agentId,
|
|
1605
|
+
reason: 'overflow',
|
|
1606
|
+
allowSummarization,
|
|
1607
|
+
},
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1448
1611
|
createCallModel(agentId = 'default') {
|
|
1449
1612
|
return async (
|
|
1450
1613
|
state: t.AgentSubgraphState,
|
|
@@ -1567,6 +1730,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1567
1730
|
|
|
1568
1731
|
let messagesToUse = messages;
|
|
1569
1732
|
let contextUsage: t.ContextUsageEvent | null = null;
|
|
1733
|
+
/**
|
|
1734
|
+
* Held outside the prune block so overflow recovery — which detours to
|
|
1735
|
+
* the summarize node from the invoke catch below — can preserve the
|
|
1736
|
+
* same masking record the configured trigger preserves.
|
|
1737
|
+
*/
|
|
1738
|
+
let prunedOriginalToolContent: Map<number, string> | undefined;
|
|
1570
1739
|
if (
|
|
1571
1740
|
!agentContext.pruneMessages &&
|
|
1572
1741
|
agentContext.tokenCounter &&
|
|
@@ -1603,7 +1772,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1603
1772
|
messagesToRefine,
|
|
1604
1773
|
prePruneContextTokens,
|
|
1605
1774
|
remainingContextTokens,
|
|
1606
|
-
|
|
1775
|
+
newOriginalToolContent,
|
|
1607
1776
|
calibrationRatio,
|
|
1608
1777
|
resolvedInstructionOverhead,
|
|
1609
1778
|
contextBudget,
|
|
@@ -1614,6 +1783,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1614
1783
|
lastCallUsage: agentContext.lastCallUsage,
|
|
1615
1784
|
totalTokensFresh: agentContext.totalTokensFresh,
|
|
1616
1785
|
});
|
|
1786
|
+
prunedOriginalToolContent = newOriginalToolContent;
|
|
1787
|
+
/**
|
|
1788
|
+
* Masking rewrites tool content in `state.messages` in place, so this
|
|
1789
|
+
* map is the only surviving copy of the full output. Persist it on
|
|
1790
|
+
* every prune, not just when a summary is about to be written — the
|
|
1791
|
+
* pruner closure that produced it is discarded on the next reset, and
|
|
1792
|
+
* with it any chance of a later summary restoring the real content.
|
|
1793
|
+
* AgentContext bounds what accumulates.
|
|
1794
|
+
*/
|
|
1795
|
+
agentContext.preserveOriginalToolContent(newOriginalToolContent);
|
|
1617
1796
|
agentContext.indexTokenCountMap = indexTokenCountMap;
|
|
1618
1797
|
if (calibrationRatio != null && calibrationRatio > 0) {
|
|
1619
1798
|
agentContext.calibrationRatio = calibrationRatio;
|
|
@@ -1694,38 +1873,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1694
1873
|
});
|
|
1695
1874
|
|
|
1696
1875
|
if (triggerResult) {
|
|
1697
|
-
if (originalToolContent != null && originalToolContent.size > 0) {
|
|
1698
|
-
/**
|
|
1699
|
-
* Merge — never overwrite — the pruner's masking record
|
|
1700
|
-
* into pendingOriginalToolContent. Carry-over entries
|
|
1701
|
-
* from a prior summarize (preserved by the recency
|
|
1702
|
-
* window for masked tool messages still in the tail) and
|
|
1703
|
-
* the current pruner's new entries are both keyed by
|
|
1704
|
-
* indices in the current `state.messages`, so a key-wise
|
|
1705
|
-
* union is correct. Overwriting would discard the
|
|
1706
|
-
* carry-over and reduce summary fidelity when those
|
|
1707
|
-
* masked tail messages eventually move into the head.
|
|
1708
|
-
*/
|
|
1709
|
-
if (agentContext.pendingOriginalToolContent == null) {
|
|
1710
|
-
agentContext.pendingOriginalToolContent = originalToolContent;
|
|
1711
|
-
} else {
|
|
1712
|
-
for (const [idx, content] of originalToolContent) {
|
|
1713
|
-
agentContext.pendingOriginalToolContent.set(idx, content);
|
|
1714
|
-
}
|
|
1715
|
-
/**
|
|
1716
|
-
* Re-apply the per-store char cap after the union. The
|
|
1717
|
-
* pruner enforces ORIGINAL_CONTENT_MAX_CHARS inside its
|
|
1718
|
-
* own map via the onContentStored callback, but a
|
|
1719
|
-
* key-wise merge with recency carry-over bypasses that
|
|
1720
|
-
* accounting and could let the merged map grow without
|
|
1721
|
-
* bound across long sessions.
|
|
1722
|
-
*/
|
|
1723
|
-
enforceOriginalContentCap(
|
|
1724
|
-
agentContext.pendingOriginalToolContent
|
|
1725
|
-
);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
1876
|
emitAgentLog(
|
|
1730
1877
|
config,
|
|
1731
1878
|
'info',
|
|
@@ -2220,21 +2367,165 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2220
2367
|
graph: this,
|
|
2221
2368
|
metadata,
|
|
2222
2369
|
});
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2370
|
+
/**
|
|
2371
|
+
* A context overflow is a deterministic consequence of the payload,
|
|
2372
|
+
* not a provider being unavailable — so it is answered by compacting
|
|
2373
|
+
* and retrying rather than by re-sending the same oversized prompt
|
|
2374
|
+
* down the fallback chain. Fallbacks still run for every other
|
|
2375
|
+
* failure, and for an overflow whose recovery budget is spent.
|
|
2376
|
+
*/
|
|
2377
|
+
/**
|
|
2378
|
+
* Compaction has to have something to work with. Without a token
|
|
2379
|
+
* counter there is no pruner, and with summarization disabled the
|
|
2380
|
+
* summarize node deliberately no-ops — so in that combination the
|
|
2381
|
+
* retry would resend a byte-identical prompt. Skipping the detour
|
|
2382
|
+
* keeps the original error and one round trip instead of three.
|
|
2383
|
+
*/
|
|
2384
|
+
const estimatedPromptTokens = getEstimatedPromptTokens(contextUsage);
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* A previous correction that left the prompt no smaller proves this
|
|
2388
|
+
* state has nothing left to compact — an emptied message list whose
|
|
2389
|
+
* content rides along in an injected summary, for instance. Measuring
|
|
2390
|
+
* that beats trying to predict every such configuration.
|
|
2391
|
+
*/
|
|
2392
|
+
const recoveryStalled = agentContext.overflowRecoveryStalled(
|
|
2393
|
+
estimatedPromptTokens
|
|
2237
2394
|
);
|
|
2395
|
+
const canSummarizeOverflow =
|
|
2396
|
+
agentContext.summarizationEnabled === true &&
|
|
2397
|
+
splitAtRecencyBoundary(messages, {
|
|
2398
|
+
turns:
|
|
2399
|
+
agentContext.summarizationConfig?.retainRecent?.turns ??
|
|
2400
|
+
DEFAULT_RETAIN_RECENT_TURNS,
|
|
2401
|
+
tokens: agentContext.summarizationConfig?.retainRecent?.tokens,
|
|
2402
|
+
tokenCounter: agentContext.tokenCounter,
|
|
2403
|
+
}).head.length > 0;
|
|
2404
|
+
|
|
2405
|
+
const planRecovery = (
|
|
2406
|
+
error: unknown,
|
|
2407
|
+
attributedFallbackContext?: FallbackErrorContext
|
|
2408
|
+
): OverflowRecoveryPlan | null => {
|
|
2409
|
+
if (recoveryStalled) {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* When the rejection came from a fallback, plan against *that*
|
|
2414
|
+
* client: its window and output allowance are why it was configured
|
|
2415
|
+
* as an alternative in the first place.
|
|
2416
|
+
*/
|
|
2417
|
+
const fallbackContext =
|
|
2418
|
+
attributedFallbackContext ?? getFallbackErrorContext(error);
|
|
2419
|
+
const recovery = planContextOverflowRecovery({
|
|
2420
|
+
error,
|
|
2421
|
+
provider: fallbackContext?.provider ?? agentContext.provider,
|
|
2422
|
+
maxContextTokens:
|
|
2423
|
+
fallbackContext?.maxContextTokens ??
|
|
2424
|
+
agentContext.maxContextTokens,
|
|
2425
|
+
estimatedPromptTokens,
|
|
2426
|
+
calibrationRatio: agentContext.calibrationRatio,
|
|
2427
|
+
instructionTokens: agentContext.instructionTokens,
|
|
2428
|
+
canSummarize: agentContext.summarizationEnabled === true,
|
|
2429
|
+
configuredCompletionTokens: getConfiguredCompletionTokens(
|
|
2430
|
+
fallbackContext?.clientOptions ?? agentContext.clientOptions
|
|
2431
|
+
),
|
|
2432
|
+
attemptsSoFar: agentContext.overflowRecoveryAttempts,
|
|
2433
|
+
});
|
|
2434
|
+
if (recovery == null) {
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
const translatedRecovery =
|
|
2438
|
+
fallbackContext != null
|
|
2439
|
+
? {
|
|
2440
|
+
...recovery,
|
|
2441
|
+
budgetTokens: minDefined(
|
|
2442
|
+
getBlindRecoveryBudget(agentContext.maxContextTokens),
|
|
2443
|
+
translateRecoveryBudget(
|
|
2444
|
+
recovery.budgetTokens,
|
|
2445
|
+
recovery.observedCalibrationRatio ??
|
|
2446
|
+
CALIBRATION_RATIO_MAX,
|
|
2447
|
+
agentContext.calibrationRatio
|
|
2448
|
+
)
|
|
2449
|
+
),
|
|
2450
|
+
observedCalibrationRatio: undefined,
|
|
2451
|
+
}
|
|
2452
|
+
: recovery;
|
|
2453
|
+
const canReduceContext =
|
|
2454
|
+
canSummarizeOverflow ||
|
|
2455
|
+
(agentContext.tokenCounter != null &&
|
|
2456
|
+
translatedRecovery.budgetTokens != null);
|
|
2457
|
+
return canReduceContext ? translatedRecovery : null;
|
|
2458
|
+
};
|
|
2459
|
+
|
|
2460
|
+
const recovery = planRecovery(primaryError);
|
|
2461
|
+
if (recovery != null) {
|
|
2462
|
+
return this.beginOverflowRecovery({
|
|
2463
|
+
recovery,
|
|
2464
|
+
agentContext,
|
|
2465
|
+
agentId,
|
|
2466
|
+
config,
|
|
2467
|
+
originalToolContent: prunedOriginalToolContent,
|
|
2468
|
+
estimatedPromptTokens,
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* A fallback can reject the same prompt as too large even when the
|
|
2474
|
+
* primary failed for an unrelated reason — a fallback with a smaller
|
|
2475
|
+
* window is the obvious case. Planning against the exhausted-chain
|
|
2476
|
+
* error keeps that path recoverable instead of surfacing it.
|
|
2477
|
+
*/
|
|
2478
|
+
try {
|
|
2479
|
+
result = await withLangfuseRuntimeScope(
|
|
2480
|
+
resolveLangfuseRuntimeScope({
|
|
2481
|
+
runLangfuse: this.langfuse,
|
|
2482
|
+
langfuseOverlay: agentContext.langfuse,
|
|
2483
|
+
}),
|
|
2484
|
+
() =>
|
|
2485
|
+
tryFallbackProviders({
|
|
2486
|
+
fallbacks,
|
|
2487
|
+
tools: agentContext.tools,
|
|
2488
|
+
messages: finalMessages,
|
|
2489
|
+
config: invokeConfig,
|
|
2490
|
+
primaryError,
|
|
2491
|
+
context: this,
|
|
2492
|
+
/**
|
|
2493
|
+
* Lets the chain recognise a fallback overflow whose signature
|
|
2494
|
+
* carries no reason of its own (Vertex AI's bare 400) and
|
|
2495
|
+
* surface it rather than a later unrelated failure.
|
|
2496
|
+
*/
|
|
2497
|
+
overflowContext: {
|
|
2498
|
+
provider: agentContext.provider,
|
|
2499
|
+
estimatedPromptTokens: getEstimatedPromptTokens(contextUsage),
|
|
2500
|
+
maxContextTokens: agentContext.maxContextTokens,
|
|
2501
|
+
},
|
|
2502
|
+
})
|
|
2503
|
+
);
|
|
2504
|
+
} catch (fallbackError) {
|
|
2505
|
+
const overflowCandidates =
|
|
2506
|
+
getFallbackOverflowCandidates(fallbackError);
|
|
2507
|
+
let fallbackRecovery: OverflowRecoveryPlan | null = null;
|
|
2508
|
+
for (const candidate of overflowCandidates) {
|
|
2509
|
+
fallbackRecovery = planRecovery(candidate.error, candidate.context);
|
|
2510
|
+
if (fallbackRecovery != null) {
|
|
2511
|
+
break;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
if (overflowCandidates.length === 0) {
|
|
2515
|
+
fallbackRecovery = planRecovery(fallbackError);
|
|
2516
|
+
}
|
|
2517
|
+
if (fallbackRecovery == null) {
|
|
2518
|
+
throw fallbackError;
|
|
2519
|
+
}
|
|
2520
|
+
return this.beginOverflowRecovery({
|
|
2521
|
+
recovery: fallbackRecovery,
|
|
2522
|
+
agentContext,
|
|
2523
|
+
agentId,
|
|
2524
|
+
config,
|
|
2525
|
+
originalToolContent: prunedOriginalToolContent,
|
|
2526
|
+
estimatedPromptTokens,
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2238
2529
|
} finally {
|
|
2239
2530
|
await disposeLangfuseHandler(langfuseHandler);
|
|
2240
2531
|
}
|
|
@@ -2593,10 +2884,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2593
2884
|
isMultiAgent: this.isMultiAgentGraph(),
|
|
2594
2885
|
hookRegistry: this.hookRegistry,
|
|
2595
2886
|
dispatchRunStep: async (runStep, nodeConfig) => {
|
|
2887
|
+
const resolvedConfig = nodeConfig ?? this.config;
|
|
2888
|
+
if (runStep.agentId != null) {
|
|
2889
|
+
const groupId = this.resolveParallelGroupId(
|
|
2890
|
+
runStep.agentId,
|
|
2891
|
+
resolvedConfig?.metadata
|
|
2892
|
+
);
|
|
2893
|
+
if (groupId != null) {
|
|
2894
|
+
runStep.groupId = groupId;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2596
2897
|
this.contentData.push(runStep);
|
|
2597
2898
|
this.contentIndexMap.set(runStep.id, runStep.index);
|
|
2598
2899
|
|
|
2599
|
-
const resolvedConfig = nodeConfig ?? this.config;
|
|
2600
2900
|
const handler = this.handlerRegistry?.getHandler(
|
|
2601
2901
|
GraphEvents.ON_RUN_STEP
|
|
2602
2902
|
);
|
|
@@ -2666,6 +2966,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2666
2966
|
}
|
|
2667
2967
|
|
|
2668
2968
|
createWorkflow(): t.CompiledStateWorkflow {
|
|
2969
|
+
this.hasCompiledCheckpointer = this.compileOptions?.checkpointer != null;
|
|
2669
2970
|
const agentNode = this.createAgentNode(this.defaultAgentId);
|
|
2670
2971
|
const StateAnnotation = Annotation.Root({
|
|
2671
2972
|
messages: Annotation<BaseMessage[]>({
|
|
@@ -2716,6 +3017,33 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2716
3017
|
return undefined;
|
|
2717
3018
|
}
|
|
2718
3019
|
|
|
3020
|
+
protected resolveParallelGroupId(
|
|
3021
|
+
agentId: string,
|
|
3022
|
+
metadata?: Record<string, unknown>
|
|
3023
|
+
): number | undefined {
|
|
3024
|
+
if (
|
|
3025
|
+
metadata == null ||
|
|
3026
|
+
!Object.prototype.hasOwnProperty.call(
|
|
3027
|
+
metadata,
|
|
3028
|
+
Constants.HANDOFF_GROUP_ID
|
|
3029
|
+
)
|
|
3030
|
+
) {
|
|
3031
|
+
return this.getParallelGroupIdForAgent(agentId);
|
|
3032
|
+
}
|
|
3033
|
+
const runtimeGroupId = metadata[Constants.HANDOFF_GROUP_ID];
|
|
3034
|
+
if (runtimeGroupId === null) {
|
|
3035
|
+
return undefined;
|
|
3036
|
+
}
|
|
3037
|
+
if (
|
|
3038
|
+
typeof runtimeGroupId === 'number' &&
|
|
3039
|
+
Number.isSafeInteger(runtimeGroupId) &&
|
|
3040
|
+
runtimeGroupId > 0
|
|
3041
|
+
) {
|
|
3042
|
+
return runtimeGroupId;
|
|
3043
|
+
}
|
|
3044
|
+
return this.getParallelGroupIdForAgent(agentId);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2719
3047
|
/* Dispatchers */
|
|
2720
3048
|
|
|
2721
3049
|
/**
|
|
@@ -2760,7 +3088,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2760
3088
|
const agentContext = this.getAgentContext(metadata);
|
|
2761
3089
|
if (this.isMultiAgentGraph() && agentContext.agentId) {
|
|
2762
3090
|
runStep.agentId = agentContext.agentId;
|
|
2763
|
-
const groupId = this.
|
|
3091
|
+
const groupId = this.resolveParallelGroupId(
|
|
3092
|
+
agentContext.agentId,
|
|
3093
|
+
metadata
|
|
3094
|
+
);
|
|
2764
3095
|
if (groupId != null) {
|
|
2765
3096
|
runStep.groupId = groupId;
|
|
2766
3097
|
}
|