@librechat/agents 3.2.68 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +117 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +4 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +243 -49
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +103 -28
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  10. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/index.cjs +11 -2
  12. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  13. package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
  14. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
  15. package/dist/cjs/llm/google/index.cjs +1 -1
  16. package/dist/cjs/llm/invoke.cjs +60 -3
  17. package/dist/cjs/llm/invoke.cjs.map +1 -1
  18. package/dist/cjs/llm/openai/index.cjs +1 -1
  19. package/dist/cjs/main.cjs +21 -7
  20. package/dist/cjs/messages/format.cjs +136 -4
  21. package/dist/cjs/messages/format.cjs.map +1 -1
  22. package/dist/cjs/messages/prune.cjs +16 -5
  23. package/dist/cjs/messages/prune.cjs.map +1 -1
  24. package/dist/cjs/messages/recency.cjs +2 -0
  25. package/dist/cjs/messages/recency.cjs.map +1 -1
  26. package/dist/cjs/prompts/activityLabel.cjs +101 -0
  27. package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
  28. package/dist/cjs/run.cjs +177 -3
  29. package/dist/cjs/run.cjs.map +1 -1
  30. package/dist/cjs/stream.cjs +205 -49
  31. package/dist/cjs/stream.cjs.map +1 -1
  32. package/dist/cjs/summarization/node.cjs +55 -7
  33. package/dist/cjs/summarization/node.cjs.map +1 -1
  34. package/dist/cjs/tools/BashExecutor.cjs +2 -2
  35. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  36. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
  37. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  38. package/dist/cjs/tools/CodeExecutor.cjs +69 -8
  39. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  40. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
  41. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  42. package/dist/cjs/tools/ToolNode.cjs +32 -3
  43. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  44. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  45. package/dist/cjs/utils/errors.cjs +317 -52
  46. package/dist/cjs/utils/errors.cjs.map +1 -1
  47. package/dist/esm/agents/AgentContext.mjs +118 -4
  48. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  49. package/dist/esm/common/enum.mjs +4 -0
  50. package/dist/esm/common/enum.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +242 -48
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +103 -28
  54. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  55. package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
  56. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  57. package/dist/esm/llm/bedrock/index.mjs +11 -2
  58. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  59. package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
  60. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
  61. package/dist/esm/llm/google/index.mjs +1 -1
  62. package/dist/esm/llm/invoke.mjs +59 -4
  63. package/dist/esm/llm/invoke.mjs.map +1 -1
  64. package/dist/esm/llm/openai/index.mjs +1 -1
  65. package/dist/esm/main.mjs +10 -10
  66. package/dist/esm/messages/format.mjs +136 -5
  67. package/dist/esm/messages/format.mjs.map +1 -1
  68. package/dist/esm/messages/prune.mjs +14 -6
  69. package/dist/esm/messages/prune.mjs.map +1 -1
  70. package/dist/esm/messages/recency.mjs +2 -1
  71. package/dist/esm/messages/recency.mjs.map +1 -1
  72. package/dist/esm/prompts/activityLabel.mjs +100 -0
  73. package/dist/esm/prompts/activityLabel.mjs.map +1 -0
  74. package/dist/esm/run.mjs +178 -4
  75. package/dist/esm/run.mjs.map +1 -1
  76. package/dist/esm/stream.mjs +205 -49
  77. package/dist/esm/stream.mjs.map +1 -1
  78. package/dist/esm/summarization/node.mjs +55 -7
  79. package/dist/esm/summarization/node.mjs.map +1 -1
  80. package/dist/esm/tools/BashExecutor.mjs +3 -3
  81. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  82. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
  83. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  84. package/dist/esm/tools/CodeExecutor.mjs +62 -9
  85. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  86. package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
  87. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +32 -3
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  91. package/dist/esm/utils/errors.mjs +317 -53
  92. package/dist/esm/utils/errors.mjs.map +1 -1
  93. package/dist/types/agents/AgentContext.d.ts +62 -3
  94. package/dist/types/common/enum.d.ts +5 -1
  95. package/dist/types/graphs/Graph.d.ts +16 -2
  96. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  97. package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
  98. package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
  99. package/dist/types/llm/invoke.d.ts +35 -6
  100. package/dist/types/messages/format.d.ts +22 -0
  101. package/dist/types/messages/prune.d.ts +10 -2
  102. package/dist/types/messages/recency.d.ts +1 -0
  103. package/dist/types/prompts/activityLabel.d.ts +31 -0
  104. package/dist/types/run.d.ts +16 -0
  105. package/dist/types/tools/CodeExecutor.d.ts +14 -1
  106. package/dist/types/types/activityLabel.d.ts +53 -0
  107. package/dist/types/types/index.d.ts +1 -0
  108. package/dist/types/types/llm.d.ts +7 -4
  109. package/dist/types/types/stream.d.ts +7 -4
  110. package/dist/types/types/summarize.d.ts +22 -0
  111. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
  112. package/dist/types/utils/errors.d.ts +65 -16
  113. package/dist/types/utils/redactSecrets.d.ts +3 -0
  114. package/package.json +7 -8
  115. package/src/agents/AgentContext.ts +188 -7
  116. package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
  117. package/src/common/enum.ts +4 -0
  118. package/src/graphs/Graph.ts +409 -58
  119. package/src/graphs/MultiAgentGraph.ts +184 -46
  120. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
  121. package/src/langfuseToolOutputTracing.ts +4 -1
  122. package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
  123. package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
  124. package/src/llm/bedrock/index.ts +25 -12
  125. package/src/llm/contextOverflowRecovery.ts +292 -0
  126. package/src/llm/invoke.ts +119 -4
  127. package/src/messages/foldToollessToolBlocks.test.ts +438 -0
  128. package/src/messages/format.ts +233 -5
  129. package/src/messages/prune.ts +24 -11
  130. package/src/messages/recency.ts +3 -1
  131. package/src/prompts/activityLabel.ts +177 -0
  132. package/src/run.ts +322 -3
  133. package/src/scripts/context-overflow-probe.ts +997 -0
  134. package/src/specs/activity-label-prompt.test.ts +128 -0
  135. package/src/specs/activity-label-trace-seed.test.ts +47 -0
  136. package/src/specs/agent-handoffs.test.ts +903 -1
  137. package/src/specs/bedrock-toolless.live.test.ts +123 -0
  138. package/src/specs/context-overflow-recovery.live.test.ts +213 -0
  139. package/src/splitStream.test.ts +882 -0
  140. package/src/stream.ts +315 -51
  141. package/src/summarization/__tests__/aggregator.test.ts +83 -0
  142. package/src/summarization/__tests__/node.test.ts +139 -0
  143. package/src/summarization/node.ts +99 -14
  144. package/src/tools/BashExecutor.ts +4 -2
  145. package/src/tools/BashProgrammaticToolCalling.ts +4 -7
  146. package/src/tools/CodeExecutor.ts +119 -8
  147. package/src/tools/ProgrammaticToolCalling.ts +29 -27
  148. package/src/tools/ToolNode.ts +50 -8
  149. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
  150. package/src/types/activityLabel.ts +55 -0
  151. package/src/types/index.ts +1 -0
  152. package/src/types/llm.ts +8 -1
  153. package/src/types/stream.ts +7 -4
  154. package/src/types/summarize.ts +22 -0
  155. package/src/utils/__tests__/errors.test.ts +270 -0
  156. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
  157. package/src/utils/__tests__/redactSecrets.test.ts +56 -0
  158. package/src/utils/errors.ts +484 -66
  159. package/src/utils/redactSecrets.ts +61 -0
@@ -11,19 +11,22 @@ 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 {
17
19
  formatAnthropicArtifactContent,
18
20
  ensureThinkingBlockInMessages,
21
+ foldToolBlocksForToollessAgent,
19
22
  convertMessagesToContent,
20
23
  sanitizeOrphanToolBlocks,
21
24
  extractToolDiscoveries,
22
25
  addBedrockTailCacheControl,
23
26
  formatArtifactPayload,
24
- enforceOriginalContentCap,
25
27
  formatContentStrings,
26
28
  isLegacyConvertible,
29
+ CALIBRATION_RATIO_MAX,
27
30
  createPruneMessages,
28
31
  syncBudgetDerivedFields,
29
32
  addTailCacheControl,
@@ -33,6 +36,8 @@ import {
33
36
  getMessageId,
34
37
  makeIsDeferred,
35
38
  partitionAndMarkAnthropicToolCache,
39
+ DEFAULT_RETAIN_RECENT_TURNS,
40
+ splitAtRecencyBoundary,
36
41
  } from '@/messages';
37
42
  import {
38
43
  createLangfuseHandler,
@@ -50,16 +55,28 @@ import {
50
55
  sleep,
51
56
  } from '@/utils';
52
57
  import {
53
- resolveLangfuseRuntimeScope,
54
- withLangfuseRuntimeScope,
55
- } 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';
56
68
  import {
69
+ Constants,
57
70
  GraphNodeKeys,
58
71
  ContentTypes,
59
72
  GraphEvents,
60
73
  Providers,
61
74
  StepTypes,
62
75
  } from '@/common';
76
+ import {
77
+ resolveLangfuseRuntimeScope,
78
+ withLangfuseRuntimeScope,
79
+ } from '@/langfuseRuntimeScope';
63
80
  import {
64
81
  appendCallbacks,
65
82
  findCallback,
@@ -74,7 +91,6 @@ import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
74
91
  import { partitionAndMarkBedrockToolCache } from '@/llm/bedrock/toolCache';
75
92
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
76
93
  import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
77
- import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
78
94
  import { buildSubagentToolParams } from '@/tools/SubagentTool';
79
95
  import { initializeLangfuseTracing } from '@/instrumentation';
80
96
  import { shouldTriggerSummarization } from '@/summarization';
@@ -369,6 +385,61 @@ function clearCurrentDeltaStepMarkers({
369
385
  }
370
386
  }
371
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
+
372
443
  async function dispatchMessageCreationStep({
373
444
  graph,
374
445
  stepKey,
@@ -519,7 +590,7 @@ export abstract class Graph<
519
590
  T extends t.BaseGraphState = t.BaseGraphState,
520
591
  _TNodeName extends string = string,
521
592
  > {
522
- abstract resetValues(): void;
593
+ abstract resetValues(keepContent?: boolean, checkpointScope?: string): void;
523
594
  abstract initializeTools({
524
595
  currentTools,
525
596
  currentToolMap,
@@ -861,9 +932,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
861
932
  overrideModel?: t.ChatModel;
862
933
  /** Optional compile options passed into workflow.compile() */
863
934
  compileOptions?: t.CompileOptions | undefined;
935
+ /** Whether the workflow was actually compiled with a checkpointer. */
936
+ hasCompiledCheckpointer: boolean = false;
864
937
  messages: BaseMessage[] = [];
865
938
  /** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
866
939
  private cachedRunMessages?: BaseMessage[];
940
+ /** Checkpoint scope whose messages match index-keyed tool snapshots. */
941
+ private originalToolContentCheckpointScope?: string;
867
942
  runId: string | undefined;
868
943
  /**
869
944
  * Boundary between historical messages (loaded from conversation state)
@@ -928,7 +1003,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
928
1003
 
929
1004
  /* Init */
930
1005
 
931
- resetValues(keepContent?: boolean): void {
1006
+ resetValues(keepContent?: boolean, checkpointScope?: string): void {
932
1007
  this.messages = [];
933
1008
  this.cachedRunMessages = undefined;
934
1009
  this.config = resetIfNotEmpty(this.config, undefined);
@@ -976,9 +1051,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
976
1051
  new Map()
977
1052
  );
978
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;
979
1061
  for (const context of this.agentContexts.values()) {
980
- context.reset();
1062
+ context.reset({ preserveOriginalToolContent });
981
1063
  }
1064
+ this.originalToolContentCheckpointScope = hasScopedCheckpoint
1065
+ ? checkpointScope
1066
+ : undefined;
982
1067
  }
983
1068
 
984
1069
  override clearHeavyState(): void {
@@ -986,8 +1071,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
986
1071
  super.clearHeavyState();
987
1072
  this.messages = [];
988
1073
  this.overrideModel = undefined;
1074
+ const preserveOriginalToolContent =
1075
+ this.hasCompiledCheckpointer &&
1076
+ this.originalToolContentCheckpointScope != null;
989
1077
  for (const context of this.agentContexts.values()) {
990
- context.reset();
1078
+ context.reset({ preserveOriginalToolContent });
991
1079
  }
992
1080
  }
993
1081
 
@@ -1444,6 +1532,82 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1444
1532
  client.abortHandler = undefined;
1445
1533
  }
1446
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
+
1447
1611
  createCallModel(agentId = 'default') {
1448
1612
  return async (
1449
1613
  state: t.AgentSubgraphState,
@@ -1566,6 +1730,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1566
1730
 
1567
1731
  let messagesToUse = messages;
1568
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;
1569
1739
  if (
1570
1740
  !agentContext.pruneMessages &&
1571
1741
  agentContext.tokenCounter &&
@@ -1602,7 +1772,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1602
1772
  messagesToRefine,
1603
1773
  prePruneContextTokens,
1604
1774
  remainingContextTokens,
1605
- originalToolContent,
1775
+ newOriginalToolContent,
1606
1776
  calibrationRatio,
1607
1777
  resolvedInstructionOverhead,
1608
1778
  contextBudget,
@@ -1613,6 +1783,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1613
1783
  lastCallUsage: agentContext.lastCallUsage,
1614
1784
  totalTokensFresh: agentContext.totalTokensFresh,
1615
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);
1616
1796
  agentContext.indexTokenCountMap = indexTokenCountMap;
1617
1797
  if (calibrationRatio != null && calibrationRatio > 0) {
1618
1798
  agentContext.calibrationRatio = calibrationRatio;
@@ -1693,38 +1873,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1693
1873
  });
1694
1874
 
1695
1875
  if (triggerResult) {
1696
- if (originalToolContent != null && originalToolContent.size > 0) {
1697
- /**
1698
- * Merge — never overwrite — the pruner's masking record
1699
- * into pendingOriginalToolContent. Carry-over entries
1700
- * from a prior summarize (preserved by the recency
1701
- * window for masked tool messages still in the tail) and
1702
- * the current pruner's new entries are both keyed by
1703
- * indices in the current `state.messages`, so a key-wise
1704
- * union is correct. Overwriting would discard the
1705
- * carry-over and reduce summary fidelity when those
1706
- * masked tail messages eventually move into the head.
1707
- */
1708
- if (agentContext.pendingOriginalToolContent == null) {
1709
- agentContext.pendingOriginalToolContent = originalToolContent;
1710
- } else {
1711
- for (const [idx, content] of originalToolContent) {
1712
- agentContext.pendingOriginalToolContent.set(idx, content);
1713
- }
1714
- /**
1715
- * Re-apply the per-store char cap after the union. The
1716
- * pruner enforces ORIGINAL_CONTENT_MAX_CHARS inside its
1717
- * own map via the onContentStored callback, but a
1718
- * key-wise merge with recency carry-over bypasses that
1719
- * accounting and could let the merged map grow without
1720
- * bound across long sessions.
1721
- */
1722
- enforceOriginalContentCap(
1723
- agentContext.pendingOriginalToolContent
1724
- );
1725
- }
1726
- }
1727
-
1728
1876
  emitAgentLog(
1729
1877
  config,
1730
1878
  'info',
@@ -1864,6 +2012,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1864
2012
  );
1865
2013
  }
1866
2014
 
2015
+ /**
2016
+ * A destination that binds no tools is invoked without a tool schema, but
2017
+ * in a multi-agent graph it can still inherit a prior agent's toolUse/
2018
+ * toolResult history. Bedrock's Converse API (and other tool-schema-strict
2019
+ * providers) reject such a request when no top-level toolConfig is sent.
2020
+ * Fold that historical tool content into plain text so the tool-less agent
2021
+ * receives valid, context-preserving messages. Handoff tools count as
2022
+ * bound tools, so a tool-less router mid-handoff is not affected.
2023
+ */
2024
+ if (toolsForBinding == null || toolsForBinding.length === 0) {
2025
+ finalMessages = foldToolBlocksForToollessAgent(finalMessages, config);
2026
+ // The fold emits structured (array) content; re-flatten for agents that
2027
+ // opted into string-only messages (`useLegacyContent`, run earlier at
2028
+ // the top of this block) so the folded turn isn't the lone exception.
2029
+ if (agentContext.useLegacyContent) {
2030
+ finalMessages = formatContentStrings(finalMessages);
2031
+ }
2032
+ }
2033
+
1867
2034
  // Determine the prompt-cache strategy up front. Two distinct facts:
1868
2035
  //
1869
2036
  // `providerPromptCacheEnabled` — prompt caching is on for this provider
@@ -2200,21 +2367,165 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2200
2367
  graph: this,
2201
2368
  metadata,
2202
2369
  });
2203
- result = await withLangfuseRuntimeScope(
2204
- resolveLangfuseRuntimeScope({
2205
- runLangfuse: this.langfuse,
2206
- langfuseOverlay: agentContext.langfuse,
2207
- }),
2208
- () =>
2209
- tryFallbackProviders({
2210
- fallbacks,
2211
- tools: agentContext.tools,
2212
- messages: finalMessages,
2213
- config: invokeConfig,
2214
- primaryError,
2215
- context: this,
2216
- })
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
2217
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
+ }
2218
2529
  } finally {
2219
2530
  await disposeLangfuseHandler(langfuseHandler);
2220
2531
  }
@@ -2573,10 +2884,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2573
2884
  isMultiAgent: this.isMultiAgentGraph(),
2574
2885
  hookRegistry: this.hookRegistry,
2575
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
+ }
2576
2897
  this.contentData.push(runStep);
2577
2898
  this.contentIndexMap.set(runStep.id, runStep.index);
2578
2899
 
2579
- const resolvedConfig = nodeConfig ?? this.config;
2580
2900
  const handler = this.handlerRegistry?.getHandler(
2581
2901
  GraphEvents.ON_RUN_STEP
2582
2902
  );
@@ -2646,6 +2966,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2646
2966
  }
2647
2967
 
2648
2968
  createWorkflow(): t.CompiledStateWorkflow {
2969
+ this.hasCompiledCheckpointer = this.compileOptions?.checkpointer != null;
2649
2970
  const agentNode = this.createAgentNode(this.defaultAgentId);
2650
2971
  const StateAnnotation = Annotation.Root({
2651
2972
  messages: Annotation<BaseMessage[]>({
@@ -2696,6 +3017,33 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2696
3017
  return undefined;
2697
3018
  }
2698
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
+
2699
3047
  /* Dispatchers */
2700
3048
 
2701
3049
  /**
@@ -2740,7 +3088,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2740
3088
  const agentContext = this.getAgentContext(metadata);
2741
3089
  if (this.isMultiAgentGraph() && agentContext.agentId) {
2742
3090
  runStep.agentId = agentContext.agentId;
2743
- const groupId = this.getParallelGroupIdForAgent(agentContext.agentId);
3091
+ const groupId = this.resolveParallelGroupId(
3092
+ agentContext.agentId,
3093
+ metadata
3094
+ );
2744
3095
  if (groupId != null) {
2745
3096
  runStep.groupId = groupId;
2746
3097
  }