@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.
Files changed (129) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +116 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +2 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +229 -48
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +102 -27
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/index.cjs +11 -2
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
  12. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
  13. package/dist/cjs/llm/google/index.cjs +1 -1
  14. package/dist/cjs/llm/invoke.cjs +60 -3
  15. package/dist/cjs/llm/invoke.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +20 -7
  17. package/dist/cjs/messages/prune.cjs +16 -5
  18. package/dist/cjs/messages/prune.cjs.map +1 -1
  19. package/dist/cjs/messages/recency.cjs +2 -0
  20. package/dist/cjs/messages/recency.cjs.map +1 -1
  21. package/dist/cjs/run.cjs +15 -2
  22. package/dist/cjs/run.cjs.map +1 -1
  23. package/dist/cjs/stream.cjs +205 -49
  24. package/dist/cjs/stream.cjs.map +1 -1
  25. package/dist/cjs/summarization/node.cjs +55 -7
  26. package/dist/cjs/summarization/node.cjs.map +1 -1
  27. package/dist/cjs/tools/BashExecutor.cjs +2 -2
  28. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  29. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
  30. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  31. package/dist/cjs/tools/CodeExecutor.cjs +69 -8
  32. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  33. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  35. package/dist/cjs/tools/ToolNode.cjs +32 -3
  36. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  37. package/dist/cjs/utils/errors.cjs +317 -52
  38. package/dist/cjs/utils/errors.cjs.map +1 -1
  39. package/dist/esm/agents/AgentContext.mjs +117 -3
  40. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  41. package/dist/esm/common/enum.mjs +2 -0
  42. package/dist/esm/common/enum.mjs.map +1 -1
  43. package/dist/esm/graphs/Graph.mjs +227 -46
  44. package/dist/esm/graphs/Graph.mjs.map +1 -1
  45. package/dist/esm/graphs/MultiAgentGraph.mjs +102 -27
  46. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  47. package/dist/esm/llm/bedrock/index.mjs +11 -2
  48. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  49. package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
  50. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
  51. package/dist/esm/llm/google/index.mjs +1 -1
  52. package/dist/esm/llm/invoke.mjs +59 -4
  53. package/dist/esm/llm/invoke.mjs.map +1 -1
  54. package/dist/esm/main.mjs +9 -9
  55. package/dist/esm/messages/prune.mjs +14 -6
  56. package/dist/esm/messages/prune.mjs.map +1 -1
  57. package/dist/esm/messages/recency.mjs +2 -1
  58. package/dist/esm/messages/recency.mjs.map +1 -1
  59. package/dist/esm/run.mjs +15 -2
  60. package/dist/esm/run.mjs.map +1 -1
  61. package/dist/esm/stream.mjs +205 -49
  62. package/dist/esm/stream.mjs.map +1 -1
  63. package/dist/esm/summarization/node.mjs +55 -7
  64. package/dist/esm/summarization/node.mjs.map +1 -1
  65. package/dist/esm/tools/BashExecutor.mjs +3 -3
  66. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  67. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
  68. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  69. package/dist/esm/tools/CodeExecutor.mjs +62 -9
  70. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  71. package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
  72. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  73. package/dist/esm/tools/ToolNode.mjs +32 -3
  74. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  75. package/dist/esm/utils/errors.mjs +317 -53
  76. package/dist/esm/utils/errors.mjs.map +1 -1
  77. package/dist/types/agents/AgentContext.d.ts +62 -3
  78. package/dist/types/common/enum.d.ts +2 -0
  79. package/dist/types/graphs/Graph.d.ts +16 -2
  80. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  81. package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
  82. package/dist/types/llm/invoke.d.ts +35 -6
  83. package/dist/types/messages/prune.d.ts +10 -2
  84. package/dist/types/messages/recency.d.ts +1 -0
  85. package/dist/types/run.d.ts +2 -0
  86. package/dist/types/tools/CodeExecutor.d.ts +14 -1
  87. package/dist/types/types/llm.d.ts +7 -4
  88. package/dist/types/types/stream.d.ts +5 -4
  89. package/dist/types/types/summarize.d.ts +22 -0
  90. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
  91. package/dist/types/utils/errors.d.ts +65 -16
  92. package/dist/types/utils/redactSecrets.d.ts +3 -0
  93. package/package.json +7 -8
  94. package/src/agents/AgentContext.ts +188 -7
  95. package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
  96. package/src/common/enum.ts +2 -0
  97. package/src/graphs/Graph.ts +389 -58
  98. package/src/graphs/MultiAgentGraph.ts +184 -46
  99. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
  100. package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
  101. package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
  102. package/src/llm/bedrock/index.ts +25 -12
  103. package/src/llm/contextOverflowRecovery.ts +292 -0
  104. package/src/llm/invoke.ts +119 -4
  105. package/src/messages/prune.ts +24 -11
  106. package/src/messages/recency.ts +3 -1
  107. package/src/run.ts +24 -1
  108. package/src/scripts/context-overflow-probe.ts +997 -0
  109. package/src/specs/agent-handoffs.test.ts +903 -1
  110. package/src/specs/context-overflow-recovery.live.test.ts +213 -0
  111. package/src/splitStream.test.ts +882 -0
  112. package/src/stream.ts +315 -51
  113. package/src/summarization/__tests__/aggregator.test.ts +83 -0
  114. package/src/summarization/__tests__/node.test.ts +139 -0
  115. package/src/summarization/node.ts +99 -14
  116. package/src/tools/BashExecutor.ts +4 -2
  117. package/src/tools/BashProgrammaticToolCalling.ts +4 -7
  118. package/src/tools/CodeExecutor.ts +119 -8
  119. package/src/tools/ProgrammaticToolCalling.ts +29 -27
  120. package/src/tools/ToolNode.ts +50 -8
  121. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
  122. package/src/types/llm.ts +8 -1
  123. package/src/types/stream.ts +5 -4
  124. package/src/types/summarize.ts +22 -0
  125. package/src/utils/__tests__/errors.test.ts +270 -0
  126. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
  127. package/src/utils/__tests__/redactSecrets.test.ts +56 -0
  128. package/src/utils/errors.ts +484 -66
  129. package/src/utils/redactSecrets.ts +61 -0
@@ -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
- resolveLangfuseRuntimeScope,
55
- withLangfuseRuntimeScope,
56
- } from '@/langfuseRuntimeScope';
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
- originalToolContent,
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
- result = await withLangfuseRuntimeScope(
2224
- resolveLangfuseRuntimeScope({
2225
- runLangfuse: this.langfuse,
2226
- langfuseOverlay: agentContext.langfuse,
2227
- }),
2228
- () =>
2229
- tryFallbackProviders({
2230
- fallbacks,
2231
- tools: agentContext.tools,
2232
- messages: finalMessages,
2233
- config: invokeConfig,
2234
- primaryError,
2235
- context: this,
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.getParallelGroupIdForAgent(agentContext.agentId);
3091
+ const groupId = this.resolveParallelGroupId(
3092
+ agentContext.agentId,
3093
+ metadata
3094
+ );
2764
3095
  if (groupId != null) {
2765
3096
  runStep.groupId = groupId;
2766
3097
  }