@librechat/agents 3.3.3 → 3.3.4

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 (156) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +342 -109
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
  10. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  12. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  13. package/dist/cjs/llm/invoke.cjs +64 -3
  14. package/dist/cjs/llm/invoke.cjs.map +1 -1
  15. package/dist/cjs/llm/openai/index.cjs +41 -5
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  18. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  20. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  21. package/dist/cjs/main.cjs +19 -2
  22. package/dist/cjs/messages/cache.cjs +54 -33
  23. package/dist/cjs/messages/cache.cjs.map +1 -1
  24. package/dist/cjs/messages/contextPruning.cjs +17 -43
  25. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  26. package/dist/cjs/messages/core.cjs +315 -23
  27. package/dist/cjs/messages/core.cjs.map +1 -1
  28. package/dist/cjs/messages/format.cjs +218 -60
  29. package/dist/cjs/messages/format.cjs.map +1 -1
  30. package/dist/cjs/messages/index.cjs +1 -1
  31. package/dist/cjs/messages/prune.cjs +503 -127
  32. package/dist/cjs/messages/prune.cjs.map +1 -1
  33. package/dist/cjs/stream.cjs +7 -3
  34. package/dist/cjs/stream.cjs.map +1 -1
  35. package/dist/cjs/summarization/node.cjs +28 -14
  36. package/dist/cjs/summarization/node.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +54 -41
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  40. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  41. package/dist/cjs/utils/index.cjs +1 -1
  42. package/dist/cjs/utils/llm.cjs +1 -1
  43. package/dist/cjs/utils/llm.cjs.map +1 -1
  44. package/dist/cjs/utils/tokens.cjs +307 -20
  45. package/dist/cjs/utils/tokens.cjs.map +1 -1
  46. package/dist/cjs/utils/toolContent.cjs +1514 -0
  47. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  48. package/dist/esm/agents/AgentContext.mjs +2 -1
  49. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  50. package/dist/esm/graphs/Graph.mjs +349 -116
  51. package/dist/esm/graphs/Graph.mjs.map +1 -1
  52. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  54. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  55. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  56. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
  57. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  58. package/dist/esm/llm/google/utils/common.mjs +19 -7
  59. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  60. package/dist/esm/llm/invoke.mjs +65 -5
  61. package/dist/esm/llm/invoke.mjs.map +1 -1
  62. package/dist/esm/llm/openai/index.mjs +41 -5
  63. package/dist/esm/llm/openai/index.mjs.map +1 -1
  64. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  65. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  66. package/dist/esm/llm/openrouter/index.mjs +4 -1
  67. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  68. package/dist/esm/main.mjs +7 -7
  69. package/dist/esm/messages/cache.mjs +55 -34
  70. package/dist/esm/messages/cache.mjs.map +1 -1
  71. package/dist/esm/messages/contextPruning.mjs +17 -43
  72. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  73. package/dist/esm/messages/core.mjs +305 -24
  74. package/dist/esm/messages/core.mjs.map +1 -1
  75. package/dist/esm/messages/format.mjs +218 -61
  76. package/dist/esm/messages/format.mjs.map +1 -1
  77. package/dist/esm/messages/index.mjs +1 -1
  78. package/dist/esm/messages/prune.mjs +500 -128
  79. package/dist/esm/messages/prune.mjs.map +1 -1
  80. package/dist/esm/stream.mjs +7 -3
  81. package/dist/esm/stream.mjs.map +1 -1
  82. package/dist/esm/summarization/node.mjs +28 -14
  83. package/dist/esm/summarization/node.mjs.map +1 -1
  84. package/dist/esm/tools/ToolNode.mjs +54 -41
  85. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  86. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  87. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  88. package/dist/esm/utils/index.mjs +1 -1
  89. package/dist/esm/utils/llm.mjs +1 -1
  90. package/dist/esm/utils/llm.mjs.map +1 -1
  91. package/dist/esm/utils/tokens.mjs +307 -21
  92. package/dist/esm/utils/tokens.mjs.map +1 -1
  93. package/dist/esm/utils/toolContent.mjs +1503 -0
  94. package/dist/esm/utils/toolContent.mjs.map +1 -0
  95. package/dist/types/graphs/Graph.d.ts +1 -1
  96. package/dist/types/llm/invoke.d.ts +27 -1
  97. package/dist/types/llm/openai/index.d.ts +4 -0
  98. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  99. package/dist/types/llm/openrouter/index.d.ts +5 -0
  100. package/dist/types/messages/contextPruning.d.ts +2 -1
  101. package/dist/types/messages/core.d.ts +36 -0
  102. package/dist/types/messages/format.d.ts +6 -0
  103. package/dist/types/messages/prune.d.ts +16 -12
  104. package/dist/types/types/stream.d.ts +1 -1
  105. package/dist/types/utils/tokens.d.ts +7 -0
  106. package/dist/types/utils/toolContent.d.ts +107 -0
  107. package/package.json +1 -1
  108. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  109. package/src/agents/AgentContext.ts +1 -0
  110. package/src/graphs/Graph.ts +698 -200
  111. package/src/graphs/MultiAgentGraph.ts +6 -1
  112. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
  113. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  114. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  115. package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
  116. package/src/llm/bedrock/utils/message_inputs.ts +86 -13
  117. package/src/llm/google/utils/common.test.ts +75 -9
  118. package/src/llm/google/utils/common.ts +34 -3
  119. package/src/llm/invoke.test.ts +488 -0
  120. package/src/llm/invoke.ts +196 -2
  121. package/src/llm/openai/index.ts +106 -6
  122. package/src/llm/openai/utils/index.ts +109 -53
  123. package/src/llm/openai/utils/messages.test.ts +330 -1
  124. package/src/llm/openrouter/index.ts +12 -2
  125. package/src/llm/openrouter/reasoning.test.ts +314 -0
  126. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  127. package/src/messages/cache.tail.test.ts +193 -0
  128. package/src/messages/cache.test.ts +113 -0
  129. package/src/messages/cache.ts +92 -60
  130. package/src/messages/contextPruning.test.ts +184 -0
  131. package/src/messages/contextPruning.ts +49 -42
  132. package/src/messages/core.ts +653 -40
  133. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  134. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  135. package/src/messages/format.ts +363 -91
  136. package/src/messages/formatAgentMessages.test.ts +604 -14
  137. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  138. package/src/messages/prune.ts +996 -183
  139. package/src/specs/prune.test.ts +1083 -6
  140. package/src/specs/summarization-unit.test.ts +105 -0
  141. package/src/specs/tokens.test.ts +609 -32
  142. package/src/stream.ts +23 -15
  143. package/src/summarization/__tests__/node.test.ts +77 -0
  144. package/src/summarization/node.ts +46 -18
  145. package/src/tools/ToolNode.ts +147 -68
  146. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  147. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  148. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  149. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  150. package/src/tools/toolOutputReferences.ts +6 -0
  151. package/src/types/stream.ts +1 -1
  152. package/src/utils/llm.test.ts +18 -0
  153. package/src/utils/llm.ts +4 -1
  154. package/src/utils/tokens.ts +586 -31
  155. package/src/utils/toolContent.test.ts +1432 -0
  156. package/src/utils/toolContent.ts +2707 -0
@@ -2,9 +2,14 @@
2
2
  import { nanoid } from 'nanoid';
3
3
  import { tool } from '@langchain/core/tools';
4
4
  import { ToolNode } from '@langchain/langgraph/prebuilt';
5
+ import { ContextOverflowError } from '@langchain/core/errors';
5
6
  import { Runnable, RunnableConfig } from '@langchain/core/runnables';
6
- import { ToolMessage, AIMessageChunk } from '@langchain/core/messages';
7
7
  import { START, END, StateGraph, Annotation } from '@langchain/langgraph';
8
+ import {
9
+ ToolMessage,
10
+ HumanMessage,
11
+ AIMessageChunk,
12
+ } from '@langchain/core/messages';
8
13
  import type {
9
14
  UsageMetadata,
10
15
  BaseMessage,
@@ -16,55 +21,66 @@ import type { FallbackErrorContext } from '@/llm/invoke';
16
21
  import type { HookRegistry } from '@/hooks';
17
22
  import type * as t from '@/types';
18
23
  import {
19
- formatAnthropicArtifactContent,
24
+ projectAnthropicArtifactContent,
20
25
  ensureThinkingBlockInMessages,
21
26
  foldToolBlocksForToollessAgent,
22
27
  convertMessagesToContent,
23
28
  sanitizeOrphanToolBlocks,
24
29
  extractToolDiscoveries,
25
30
  addBedrockTailCacheControl,
26
- formatArtifactPayload,
31
+ projectArtifactPayload,
27
32
  formatContentStrings,
28
- isLegacyConvertible,
29
33
  CALIBRATION_RATIO_MAX,
34
+ REPLY_PRIMER_TOKENS,
30
35
  createPruneMessages,
36
+ projectToolCallInputs,
37
+ calculateMaxToolCallInputChars,
38
+ projectToolStreamContentForProvider,
31
39
  syncBudgetDerivedFields,
32
40
  addTailCacheControl,
33
41
  resolvePromptCacheTtl,
34
42
  resolveBedrockPromptCacheTtl,
35
43
  supportsBedrockToolCache,
44
+ isSyntheticProviderContextMessage,
36
45
  getMessageId,
37
46
  makeIsDeferred,
38
47
  partitionAndMarkAnthropicToolCache,
39
48
  DEFAULT_RETAIN_RECENT_TURNS,
40
49
  splitAtRecencyBoundary,
41
50
  } from '@/messages';
42
- import {
43
- createLangfuseHandler,
44
- createLangfuseTraceMetadata,
45
- disposeLangfuseHandler,
46
- isLangfuseCallbackHandler,
47
- } from '@/langfuse';
48
51
  import {
49
52
  resetIfNotEmpty,
50
53
  isAnthropicLike,
51
54
  isOpenAILike,
52
55
  isGoogleLike,
53
56
  apportionTokenCounts,
57
+ calculateMaxToolResultChars,
54
58
  joinKeys,
55
59
  sleep,
56
60
  } from '@/utils';
57
- import {
58
- getBlindRecoveryBudget,
59
- planContextOverflowRecovery,
60
- translateRecoveryBudget,
61
- } from '@/llm/contextOverflowRecovery';
62
61
  import {
63
62
  attemptInvoke,
64
63
  tryFallbackProviders,
65
64
  getFallbackErrorContext,
66
65
  getFallbackOverflowCandidates,
66
+ projectMessagesForProvider,
67
67
  } from '@/llm/invoke';
68
+ import {
69
+ createLangfuseHandler,
70
+ createLangfuseTraceMetadata,
71
+ disposeLangfuseHandler,
72
+ isLangfuseCallbackHandler,
73
+ } from '@/langfuse';
74
+ import {
75
+ getBlindRecoveryBudget,
76
+ planContextOverflowRecovery,
77
+ translateRecoveryBudget,
78
+ } from '@/llm/contextOverflowRecovery';
79
+ import {
80
+ compactToolContent,
81
+ getToolContentCharLength,
82
+ serializeToolContentBounded,
83
+ } from '@/utils/toolContent';
68
84
  import {
69
85
  Constants,
70
86
  GraphNodeKeys,
@@ -73,6 +89,10 @@ import {
73
89
  Providers,
74
90
  StepTypes,
75
91
  } from '@/common';
92
+ import {
93
+ annotateMessagesForLLM,
94
+ ToolOutputReferenceRegistry,
95
+ } from '@/tools/toolOutputReferences';
76
96
  import {
77
97
  resolveLangfuseRuntimeScope,
78
98
  withLangfuseRuntimeScope,
@@ -87,7 +107,6 @@ import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
87
107
  import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
88
108
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
89
109
  import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
90
- import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
91
110
  import { partitionAndMarkBedrockToolCache } from '@/llm/bedrock/toolCache';
92
111
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
93
112
  import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
@@ -124,26 +143,6 @@ function createToolHandlerRegistry(
124
143
  return registry;
125
144
  }
126
145
 
127
- /**
128
- * Start index of the span post-prune formatters can mutate in place: the
129
- * trailing tool batch plus its owning AI message (artifact formatting touches
130
- * every tool result after the last AI tool call; Bedrock rewrites the AI
131
- * message before a trailing tool result). Capped so the usage-snapshot
132
- * recount stays constant-cost.
133
- */
134
- function trailingMutationStart(messages: BaseMessage[]): number {
135
- const MAX_SPAN = 16;
136
- let index = messages.length - 1;
137
- while (
138
- index >= 0 &&
139
- messages[index]?.getType() === 'tool' &&
140
- messages.length - index < MAX_SPAN
141
- ) {
142
- index--;
143
- }
144
- return Math.max(0, Math.min(index, messages.length - 2));
145
- }
146
-
147
146
  type ReasoningKey = 'reasoning_content' | 'reasoning';
148
147
  type ReasoningSummary = { summary?: Array<{ text?: string }> };
149
148
  type ReasoningDetail = { type?: string; text?: string };
@@ -1747,6 +1746,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1747
1746
  provider: agentContext.provider,
1748
1747
  tokenCounter: agentContext.tokenCounter,
1749
1748
  maxTokens: agentContext.maxContextTokens,
1749
+ maxToolResultChars: agentContext.maxToolResultChars,
1750
1750
  thinkingEnabled: isThinkingEnabled(
1751
1751
  agentContext.provider,
1752
1752
  agentContext.clientOptions
@@ -1923,37 +1923,136 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1923
1923
  }
1924
1924
 
1925
1925
  let finalMessages = messagesToUse;
1926
- /** Tail snapshot for the dispatch-time usage delta: in-place
1927
- * formatters (artifact appends, Bedrock content rewrites, legacy
1928
- * string conversion) mutate without changing length or identity
1929
- * capture before they run. Legacy string conversion can also touch
1930
- * messages before the tail, so those convertible indices are
1931
- * tracked separately (none exist in the common case). */
1932
- const tailStart = trailingMutationStart(messagesToUse);
1933
- let preFormatTailTokens: number | null = null;
1934
- let legacyIndices: number[] | null = null;
1935
- let preFormatLegacyTokens = 0;
1926
+ /**
1927
+ * Keep the pruner's provider-grounded aggregate as the authoritative
1928
+ * baseline, then attribute it across retained messages. Provider
1929
+ * transforms can shrink one message while expanding or adding another;
1930
+ * per-origin accounting prevents that unrelated shrink from canceling
1931
+ * the expansion. Raw counts are frozen before in-place formatters run.
1932
+ */
1933
+ let providerMessageBaseline:
1934
+ | Array<{ rawTokens: number; accountingWeight: number }>
1935
+ | undefined;
1936
+ const providerMessageOrigins = new WeakMap<BaseMessage, number>();
1936
1937
  if (contextUsage != null && agentContext.tokenCounter != null) {
1937
- preFormatTailTokens = 0;
1938
- for (const message of messagesToUse.slice(tailStart)) {
1939
- preFormatTailTokens += agentContext.tokenCounter(message);
1938
+ const sourceIndices = new WeakMap<BaseMessage, number>();
1939
+ for (let i = 0; i < messages.length; i++) {
1940
+ sourceIndices.set(messages[i], i);
1940
1941
  }
1941
- if (agentContext.useLegacyContent) {
1942
- legacyIndices = [];
1943
- for (let i = 0; i < tailStart; i++) {
1944
- if (isLegacyConvertible(messagesToUse[i])) {
1945
- legacyIndices.push(i);
1946
- preFormatLegacyTokens += agentContext.tokenCounter(
1947
- messagesToUse[i]
1948
- );
1942
+ providerMessageBaseline = messagesToUse.map((message, index) => {
1943
+ const rawTokens = agentContext.tokenCounter!(message);
1944
+ const sourceIndex = sourceIndices.get(message);
1945
+ const indexedTokens =
1946
+ sourceIndex != null
1947
+ ? agentContext.indexTokenCountMap[sourceIndex]
1948
+ : undefined;
1949
+ const accountingWeight =
1950
+ indexedTokens != null &&
1951
+ Number.isFinite(indexedTokens) &&
1952
+ indexedTokens >= 0
1953
+ ? indexedTokens
1954
+ : rawTokens;
1955
+ if (!providerMessageOrigins.has(message)) {
1956
+ providerMessageOrigins.set(message, index);
1957
+ }
1958
+ return { rawTokens, accountingWeight };
1959
+ });
1960
+ }
1961
+
1962
+ const getProviderMessageOriginKey = (
1963
+ message: BaseMessage
1964
+ ): string | undefined => {
1965
+ const type = message.getType();
1966
+ if (
1967
+ message instanceof ToolMessage &&
1968
+ typeof message.tool_call_id === 'string' &&
1969
+ message.tool_call_id.length > 0
1970
+ ) {
1971
+ return `tool:call:${message.tool_call_id}`;
1972
+ }
1973
+ if (typeof message.id === 'string' && message.id.length > 0) {
1974
+ return `${type}:id:${message.id}`;
1975
+ }
1976
+ return undefined;
1977
+ };
1978
+
1979
+ /**
1980
+ * Provider projections clone messages. Preserve their baseline origin
1981
+ * without writing tracking metadata onto the wire. Synthetic fold
1982
+ * messages intentionally remain unattributed and are charged in full.
1983
+ */
1984
+ const trackProviderMessageOrigins = (
1985
+ before: BaseMessage[],
1986
+ after: BaseMessage[]
1987
+ ): BaseMessage[] => {
1988
+ if (providerMessageBaseline == null || before === after) {
1989
+ return after;
1990
+ }
1991
+ if (before.length === after.length) {
1992
+ for (let i = 0; i < after.length; i++) {
1993
+ const origin = providerMessageOrigins.get(before[i]);
1994
+ if (
1995
+ origin != null &&
1996
+ !providerMessageOrigins.has(after[i]) &&
1997
+ before[i].getType() === after[i].getType() &&
1998
+ !isSyntheticProviderContextMessage(after[i])
1999
+ ) {
2000
+ providerMessageOrigins.set(after[i], origin);
1949
2001
  }
1950
2002
  }
2003
+ return after;
1951
2004
  }
1952
- }
2005
+
2006
+ const keyedOrigins = new Map<string, number | null>();
2007
+ for (const message of before) {
2008
+ const origin = providerMessageOrigins.get(message);
2009
+ const key = getProviderMessageOriginKey(message);
2010
+ if (origin == null || key == null) {
2011
+ continue;
2012
+ }
2013
+ keyedOrigins.set(key, keyedOrigins.has(key) ? null : origin);
2014
+ }
2015
+ for (const message of after) {
2016
+ if (
2017
+ providerMessageOrigins.has(message) ||
2018
+ isSyntheticProviderContextMessage(message)
2019
+ ) {
2020
+ continue;
2021
+ }
2022
+ const key = getProviderMessageOriginKey(message);
2023
+ const origin = key != null ? keyedOrigins.get(key) : undefined;
2024
+ if (origin != null) {
2025
+ providerMessageOrigins.set(message, origin);
2026
+ }
2027
+ }
2028
+ return after;
2029
+ };
2030
+
1953
2031
  if (agentContext.useLegacyContent) {
1954
- finalMessages = formatContentStrings(finalMessages);
2032
+ const before = finalMessages;
2033
+ finalMessages = trackProviderMessageOrigins(
2034
+ before,
2035
+ formatContentStrings(before)
2036
+ );
1955
2037
  }
1956
2038
 
2039
+ const maxProviderToolResultChars =
2040
+ agentContext.maxToolResultChars ??
2041
+ calculateMaxToolResultChars(agentContext.maxContextTokens);
2042
+ const beforeToolStreamProjection = finalMessages;
2043
+ finalMessages = trackProviderMessageOrigins(
2044
+ beforeToolStreamProjection,
2045
+ projectToolStreamContentForProvider(beforeToolStreamProjection)
2046
+ );
2047
+ const beforeToolInputProjection = finalMessages;
2048
+ finalMessages = trackProviderMessageOrigins(
2049
+ beforeToolInputProjection,
2050
+ projectToolCallInputs(
2051
+ beforeToolInputProjection,
2052
+ calculateMaxToolCallInputChars(agentContext.maxContextTokens)
2053
+ )
2054
+ );
2055
+
1957
2056
  const lastMessageX =
1958
2057
  finalMessages.length >= 2
1959
2058
  ? finalMessages[finalMessages.length - 2]
@@ -1979,58 +2078,368 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1979
2078
  trimmed.length > 0 ? [{ type: 'text' as const, text: trimmed }] : '';
1980
2079
  }
1981
2080
 
2081
+ const localProviderOverflowMeasurements = new WeakMap<
2082
+ object,
2083
+ {
2084
+ contextBudget: number;
2085
+ estimatedPromptTokens: number;
2086
+ }
2087
+ >();
2088
+ const measureProviderPayload = (
2089
+ candidate: BaseMessage[],
2090
+ contextBudgetOverride?: number,
2091
+ forceRawRecount = false
2092
+ ): {
2093
+ fits: boolean;
2094
+ projectedMessageTokens?: number;
2095
+ availableMessageTokens?: number;
2096
+ contextBudget?: number;
2097
+ effectiveInstructionTokens?: number;
2098
+ } => {
2099
+ const contextBudget =
2100
+ contextBudgetOverride ?? contextUsage?.contextBudget;
2101
+ const effectiveInstructionTokens =
2102
+ contextUsage?.effectiveInstructionTokens ??
2103
+ (forceRawRecount ? agentContext.instructionTokens : undefined);
2104
+ if (
2105
+ agentContext.tokenCounter == null ||
2106
+ contextBudget == null ||
2107
+ effectiveInstructionTokens == null
2108
+ ) {
2109
+ return { fits: true };
2110
+ }
2111
+ const availableMessageTokens = Math.max(
2112
+ 0,
2113
+ contextBudget - effectiveInstructionTokens
2114
+ );
2115
+ let usageRatio =
2116
+ agentContext.calibrationRatio > 0 ? agentContext.calibrationRatio : 1;
2117
+ if (
2118
+ contextUsage?.calibrationRatio != null &&
2119
+ contextUsage.calibrationRatio > 0
2120
+ ) {
2121
+ usageRatio = contextUsage.calibrationRatio;
2122
+ }
2123
+ if (forceRawRecount) {
2124
+ usageRatio = Math.max(1, usageRatio);
2125
+ }
2126
+ const baselineRemaining = contextUsage?.remainingContextTokens;
2127
+ const accountedMessageTokens =
2128
+ !forceRawRecount &&
2129
+ providerMessageBaseline != null &&
2130
+ baselineRemaining != null &&
2131
+ Number.isFinite(baselineRemaining)
2132
+ ? availableMessageTokens -
2133
+ Math.min(availableMessageTokens, Math.max(0, baselineRemaining))
2134
+ : undefined;
2135
+
2136
+ let projectedMessageTokens: number;
2137
+ if (accountedMessageTokens != null && providerMessageBaseline != null) {
2138
+ const replyPrimerTokens = Math.round(
2139
+ REPLY_PRIMER_TOKENS * usageRatio
2140
+ );
2141
+ const rawWeights: Record<string, number> = {};
2142
+ let totalWeight = 0;
2143
+ for (let i = 0; i < providerMessageBaseline.length; i++) {
2144
+ const weight = providerMessageBaseline[i].accountingWeight;
2145
+ rawWeights[i] = weight;
2146
+ totalWeight += weight;
2147
+ }
2148
+ const attributableTokens =
2149
+ totalWeight > 0
2150
+ ? Math.min(
2151
+ Math.max(0, accountedMessageTokens - replyPrimerTokens),
2152
+ Math.round(totalWeight * usageRatio)
2153
+ )
2154
+ : 0;
2155
+ const apportionedTokens =
2156
+ totalWeight > 0
2157
+ ? apportionTokenCounts(
2158
+ rawWeights,
2159
+ attributableTokens / totalWeight,
2160
+ attributableTokens
2161
+ )
2162
+ : {};
2163
+ const attributedByOrigin = providerMessageBaseline.map(
2164
+ (_, origin) => apportionedTokens[origin] || 0
2165
+ );
2166
+ projectedMessageTokens = Math.max(
2167
+ replyPrimerTokens,
2168
+ accountedMessageTokens - attributableTokens
2169
+ );
2170
+ let newRawTokens = 0;
2171
+ const usedOrigins = new Set<number>();
2172
+ for (const message of candidate) {
2173
+ const rawTokens = agentContext.tokenCounter(message);
2174
+ const origin = providerMessageOrigins.get(message);
2175
+ if (origin == null || usedOrigins.has(origin)) {
2176
+ newRawTokens += rawTokens;
2177
+ continue;
2178
+ }
2179
+ usedOrigins.add(origin);
2180
+ projectedMessageTokens += Math.max(
2181
+ 0,
2182
+ attributedByOrigin[origin] +
2183
+ Math.round(
2184
+ (rawTokens - providerMessageBaseline[origin].rawTokens) *
2185
+ usageRatio
2186
+ )
2187
+ );
2188
+ }
2189
+ projectedMessageTokens += Math.round(newRawTokens * usageRatio);
2190
+ } else {
2191
+ let rawTokens = REPLY_PRIMER_TOKENS;
2192
+ for (const message of candidate) {
2193
+ rawTokens += agentContext.tokenCounter(message);
2194
+ }
2195
+ projectedMessageTokens = Math.round(rawTokens * usageRatio);
2196
+ }
2197
+ return {
2198
+ fits: projectedMessageTokens <= availableMessageTokens,
2199
+ projectedMessageTokens,
2200
+ availableMessageTokens,
2201
+ contextBudget,
2202
+ effectiveInstructionTokens,
2203
+ };
2204
+ };
2205
+
2206
+ const createProviderPayloadOverflowError = ({
2207
+ projection,
2208
+ provider,
2209
+ info,
2210
+ }: {
2211
+ projection: ReturnType<typeof measureProviderPayload>;
2212
+ provider?: Providers;
2213
+ info: string;
2214
+ }): ContextOverflowError => {
2215
+ const error = new ContextOverflowError(
2216
+ JSON.stringify({
2217
+ type: 'final_context_overflow',
2218
+ info,
2219
+ provider,
2220
+ projectedMessageTokens: projection.projectedMessageTokens,
2221
+ availableMessageTokens: projection.availableMessageTokens,
2222
+ })
2223
+ );
2224
+ if (
2225
+ projection.projectedMessageTokens != null &&
2226
+ projection.contextBudget != null &&
2227
+ projection.effectiveInstructionTokens != null
2228
+ ) {
2229
+ localProviderOverflowMeasurements.set(error, {
2230
+ contextBudget: projection.contextBudget,
2231
+ estimatedPromptTokens:
2232
+ projection.projectedMessageTokens +
2233
+ projection.effectiveInstructionTokens,
2234
+ });
2235
+ }
2236
+ return error;
2237
+ };
2238
+
2239
+ const applyProviderMessageTransforms = (
2240
+ candidate: BaseMessage[]
2241
+ ): BaseMessage[] => {
2242
+ let transformed = candidate;
2243
+ if (
2244
+ isThinkingEnabled(agentContext.provider, agentContext.clientOptions)
2245
+ ) {
2246
+ /**
2247
+ * Current-run AI messages may validly omit a thinking block. The
2248
+ * boundary prevents them from being mistaken for foreign history.
2249
+ */
2250
+ const before = transformed;
2251
+ transformed = trackProviderMessageOrigins(
2252
+ before,
2253
+ ensureThinkingBlockInMessages(
2254
+ before,
2255
+ agentContext.provider,
2256
+ config,
2257
+ this.startIndex
2258
+ )
2259
+ );
2260
+ }
2261
+
2262
+ /**
2263
+ * Tool-less destinations cannot send inherited tool blocks without a
2264
+ * tool schema, so fold those interactions into provider-valid content.
2265
+ */
2266
+ if (toolsForBinding == null || toolsForBinding.length === 0) {
2267
+ const before = transformed;
2268
+ transformed = trackProviderMessageOrigins(
2269
+ before,
2270
+ foldToolBlocksForToollessAgent(before, config)
2271
+ );
2272
+ if (agentContext.useLegacyContent) {
2273
+ const beforeLegacyFormat = transformed;
2274
+ transformed = trackProviderMessageOrigins(
2275
+ beforeLegacyFormat,
2276
+ formatContentStrings(beforeLegacyFormat)
2277
+ );
2278
+ }
2279
+ }
2280
+ return transformed;
2281
+ };
2282
+
2283
+ const toolOutputRegistry = this.getOrCreateToolOutputRegistry();
2284
+ const providerRunId = config.configurable?.run_id as string | undefined;
2285
+ const projectProviderReferences = (
2286
+ candidate: BaseMessage[]
2287
+ ): BaseMessage[] =>
2288
+ trackProviderMessageOrigins(
2289
+ candidate,
2290
+ annotateMessagesForLLM(candidate, toolOutputRegistry, providerRunId)
2291
+ );
2292
+
2293
+ const compactSyntheticProviderContext = (
2294
+ candidate: BaseMessage[]
2295
+ ): BaseMessage[] => {
2296
+ const synthetic: Array<{
2297
+ index: number;
2298
+ message: HumanMessage;
2299
+ chars: number;
2300
+ }> = [];
2301
+ for (let i = 0; i < candidate.length; i++) {
2302
+ const message = candidate[i];
2303
+ if (
2304
+ !(message instanceof HumanMessage) ||
2305
+ !isSyntheticProviderContextMessage(message)
2306
+ ) {
2307
+ continue;
2308
+ }
2309
+ const content = message.content;
2310
+ synthetic.push({
2311
+ index: i,
2312
+ message,
2313
+ chars: getToolContentCharLength(content),
2314
+ });
2315
+ }
2316
+ if (synthetic.length === 0) {
2317
+ return candidate;
2318
+ }
2319
+
2320
+ const buildCandidate = (scale: number): BaseMessage[] => {
2321
+ const compacted = [...candidate];
2322
+ for (const { index, message, chars } of synthetic) {
2323
+ const content = compactToolContent(
2324
+ message.content,
2325
+ Math.floor(chars * scale)
2326
+ ).content;
2327
+ compacted[index] = new HumanMessage({
2328
+ content,
2329
+ id: message.id,
2330
+ name: message.name,
2331
+ additional_kwargs: message.additional_kwargs,
2332
+ response_metadata: message.response_metadata,
2333
+ });
2334
+ }
2335
+ return compacted;
2336
+ };
2337
+
2338
+ let best = buildCandidate(0);
2339
+ if (!measureProviderPayload(best).fits) {
2340
+ return candidate;
2341
+ }
2342
+ let low = 0;
2343
+ let high = 1;
2344
+ for (let i = 0; i < 12; i++) {
2345
+ const scale = (low + high) / 2;
2346
+ const attempt = buildCandidate(scale);
2347
+ if (measureProviderPayload(attempt).fits) {
2348
+ best = attempt;
2349
+ low = scale;
2350
+ } else {
2351
+ high = scale;
2352
+ }
2353
+ }
2354
+ return best;
2355
+ };
2356
+
2357
+ let artifactBaseMessages: BaseMessage[] | undefined;
1982
2358
  if (lastMessageY instanceof ToolMessage) {
2359
+ let artifactCandidate = finalMessages;
1983
2360
  if (anthropicLike) {
1984
- formatAnthropicArtifactContent(finalMessages);
2361
+ artifactCandidate = trackProviderMessageOrigins(
2362
+ finalMessages,
2363
+ projectAnthropicArtifactContent(
2364
+ finalMessages,
2365
+ maxProviderToolResultChars
2366
+ )
2367
+ );
1985
2368
  } else if (
1986
2369
  (isOpenAILike(agentContext.provider) &&
1987
2370
  agentContext.provider !== Providers.DEEPSEEK) ||
1988
2371
  isGoogleLike(agentContext.provider)
1989
2372
  ) {
1990
- formatArtifactPayload(finalMessages);
2373
+ artifactCandidate = trackProviderMessageOrigins(
2374
+ finalMessages,
2375
+ projectArtifactPayload(finalMessages, maxProviderToolResultChars)
2376
+ );
1991
2377
  }
1992
- }
1993
2378
 
1994
- if (
1995
- isThinkingEnabled(agentContext.provider, agentContext.clientOptions)
1996
- ) {
1997
- /**
1998
- * Pass `this.startIndex` so the function can distinguish CURRENT-run
1999
- * AI messages (the agent's own iterations — possibly without a
2000
- * leading thinking block, which Claude is allowed to skip) from
2001
- * historical context that genuinely needs the
2002
- * `[Previous agent context]` placeholder. Without this signal the
2003
- * function would convert the agent's own in-run tool_use messages,
2004
- * polluting the next iteration's prompt with a placeholder the
2005
- * model treats as suspicious injected content.
2006
- */
2007
- finalMessages = ensureThinkingBlockInMessages(
2008
- finalMessages,
2009
- agentContext.provider,
2010
- config,
2011
- this.startIndex
2012
- );
2379
+ if (artifactCandidate !== finalMessages) {
2380
+ const projection = measureProviderPayload(artifactCandidate);
2381
+ if (projection.fits) {
2382
+ artifactBaseMessages = finalMessages;
2383
+ finalMessages = artifactCandidate;
2384
+ } else {
2385
+ emitAgentLog(
2386
+ config,
2387
+ 'warn',
2388
+ 'graph',
2389
+ 'Artifact payload omitted because it exceeds the remaining context budget',
2390
+ {
2391
+ projectedMessageTokens: projection.projectedMessageTokens,
2392
+ availableMessageTokens: projection.availableMessageTokens,
2393
+ },
2394
+ { runId: this.runId, agentId }
2395
+ );
2396
+ }
2397
+ }
2013
2398
  }
2014
2399
 
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);
2400
+ finalMessages = projectProviderReferences(
2401
+ applyProviderMessageTransforms(finalMessages)
2402
+ );
2403
+ let finalProjection = measureProviderPayload(finalMessages);
2404
+ if (artifactBaseMessages != null) {
2405
+ if (!finalProjection.fits) {
2406
+ finalMessages = projectProviderReferences(
2407
+ applyProviderMessageTransforms(artifactBaseMessages)
2408
+ );
2409
+ finalProjection = measureProviderPayload(finalMessages);
2410
+ emitAgentLog(
2411
+ config,
2412
+ 'warn',
2413
+ 'graph',
2414
+ 'Artifact payload omitted after final provider formatting exceeded the remaining context budget',
2415
+ {
2416
+ projectedMessageTokens: finalProjection.projectedMessageTokens,
2417
+ availableMessageTokens: finalProjection.availableMessageTokens,
2418
+ },
2419
+ { runId: this.runId, agentId }
2420
+ );
2421
+ }
2422
+ }
2423
+ if (!finalProjection.fits) {
2424
+ const compacted = compactSyntheticProviderContext(finalMessages);
2425
+ if (compacted !== finalMessages) {
2426
+ finalMessages = compacted;
2427
+ finalProjection = measureProviderPayload(finalMessages);
2428
+ emitAgentLog(
2429
+ config,
2430
+ finalProjection.fits ? 'warn' : 'error',
2431
+ 'graph',
2432
+ finalProjection.fits
2433
+ ? 'Synthetic provider context compacted to fit the final payload budget'
2434
+ : 'Final provider payload still exceeds budget after synthetic context compaction',
2435
+ {
2436
+ projectedMessageTokens: finalProjection.projectedMessageTokens,
2437
+ availableMessageTokens: finalProjection.availableMessageTokens,
2438
+ },
2439
+ { runId: this.runId, agentId }
2440
+ );
2031
2441
  }
2032
2442
  }
2033
-
2034
2443
  // Determine the prompt-cache strategy up front. Two distinct facts:
2035
2444
  //
2036
2445
  // `providerPromptCacheEnabled` — prompt caching is on for this provider
@@ -2083,7 +2492,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2083
2492
  providerPromptCacheEnabled);
2084
2493
  if (needsOrphanSanitize) {
2085
2494
  const beforeSanitize = finalMessages.length;
2086
- finalMessages = sanitizeOrphanToolBlocks(finalMessages);
2495
+ const beforeSanitizeMessages = finalMessages;
2496
+ finalMessages = trackProviderMessageOrigins(
2497
+ beforeSanitizeMessages,
2498
+ sanitizeOrphanToolBlocks(beforeSanitizeMessages, (source, clone) => {
2499
+ const origin = providerMessageOrigins.get(source);
2500
+ if (origin != null) {
2501
+ providerMessageOrigins.set(clone, origin);
2502
+ }
2503
+ })
2504
+ );
2087
2505
  if (finalMessages.length !== beforeSanitize) {
2088
2506
  emitAgentLog(
2089
2507
  config,
@@ -2113,20 +2531,24 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2113
2531
  (anthropicPromptCacheEnabled || openRouterPromptCacheEnabled) &&
2114
2532
  !agentContext.systemRunnable
2115
2533
  ) {
2116
- finalMessages = addTailCacheControl<BaseMessage>(
2117
- finalMessages,
2118
- resolvePromptCacheTtl(
2119
- anthropicPromptCacheEnabled
2120
- ? (
2121
- agentContext.clientOptions as
2122
- | t.AnthropicClientOptions
2123
- | undefined
2124
- )?.promptCacheTtl
2125
- : (
2126
- agentContext.clientOptions as
2127
- | t.ProviderOptionsMap[Providers.OPENROUTER]
2128
- | undefined
2129
- )?.promptCacheTtl
2534
+ const beforeCacheControl = finalMessages;
2535
+ finalMessages = trackProviderMessageOrigins(
2536
+ beforeCacheControl,
2537
+ addTailCacheControl<BaseMessage>(
2538
+ beforeCacheControl,
2539
+ resolvePromptCacheTtl(
2540
+ anthropicPromptCacheEnabled
2541
+ ? (
2542
+ agentContext.clientOptions as
2543
+ | t.AnthropicClientOptions
2544
+ | undefined
2545
+ )?.promptCacheTtl
2546
+ : (
2547
+ agentContext.clientOptions as
2548
+ | t.ProviderOptionsMap[Providers.OPENROUTER]
2549
+ | undefined
2550
+ )?.promptCacheTtl
2551
+ )
2130
2552
  )
2131
2553
  );
2132
2554
  } else if (bedrockPromptCacheEnabled) {
@@ -2135,15 +2557,46 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2135
2557
  | undefined;
2136
2558
  // Non-Claude models (Nova) reject the extended 1h TTL, so resolve it
2137
2559
  // against the model — message/system caching stays on, clamped to 5m.
2138
- finalMessages = addBedrockTailCacheControl<BaseMessage>(
2139
- finalMessages,
2140
- resolveBedrockPromptCacheTtl(
2141
- bedrockOptions?.promptCacheTtl,
2142
- (bedrockOptions as { model?: string } | undefined)?.model
2560
+ const beforeCacheControl = finalMessages;
2561
+ finalMessages = trackProviderMessageOrigins(
2562
+ beforeCacheControl,
2563
+ addBedrockTailCacheControl<BaseMessage>(
2564
+ beforeCacheControl,
2565
+ resolveBedrockPromptCacheTtl(
2566
+ bedrockOptions?.promptCacheTtl,
2567
+ (bedrockOptions as { model?: string } | undefined)?.model
2568
+ )
2143
2569
  )
2144
2570
  );
2145
2571
  }
2146
2572
 
2573
+ const fallbackBaseMessages = finalMessages;
2574
+ const beforeFinalProviderProjection = fallbackBaseMessages;
2575
+ finalMessages = trackProviderMessageOrigins(
2576
+ beforeFinalProviderProjection,
2577
+ projectMessagesForProvider({
2578
+ model: (this.overrideModel ?? model) as t.ChatModel,
2579
+ messages: beforeFinalProviderProjection,
2580
+ provider: agentContext.provider,
2581
+ maxToolResultChars: maxProviderToolResultChars,
2582
+ callOptions: config,
2583
+ })
2584
+ );
2585
+
2586
+ /**
2587
+ * Prompt-cache placement and orphan sanitization are provider-wire
2588
+ * transforms too. Re-measure after both so no content added after the
2589
+ * earlier artifact/synthetic compaction decision can bypass the guard.
2590
+ */
2591
+ finalProjection = measureProviderPayload(finalMessages);
2592
+ const preInvokeContextOverflowError = !finalProjection.fits
2593
+ ? createProviderPayloadOverflowError({
2594
+ projection: finalProjection,
2595
+ provider: agentContext.provider,
2596
+ info: 'Provider message formatting exceeded the context budget and no safe synthetic-context compaction could make it fit.',
2597
+ })
2598
+ : undefined;
2599
+
2147
2600
  if (
2148
2601
  agentContext.lastStreamCall != null &&
2149
2602
  agentContext.streamBuffer != null
@@ -2219,66 +2672,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2219
2672
 
2220
2673
  /** Past the empty-prompt guard — a model call is now guaranteed */
2221
2674
  if (contextUsage != null) {
2222
- const usageRatio =
2223
- contextUsage.calibrationRatio != null &&
2224
- contextUsage.calibrationRatio > 0
2225
- ? contextUsage.calibrationRatio
2226
- : 1;
2227
2675
  if (
2228
- agentContext.tokenCounter != null &&
2229
- finalMessages.length !== messagesToUse.length
2676
+ finalProjection.projectedMessageTokens != null &&
2677
+ finalProjection.availableMessageTokens != null
2230
2678
  ) {
2231
- /** Post-prune formatting restructured the payload (e.g. thinking
2232
- * placeholder collapse, orphan drops) — recount so the gauge
2233
- * reflects what is actually sent */
2234
- let rawTokens = 0;
2235
- for (const message of finalMessages) {
2236
- rawTokens += agentContext.tokenCounter(message);
2237
- }
2238
2679
  contextUsage.breakdown.messageCount = finalMessages.length;
2239
- if (
2240
- contextUsage.contextBudget != null &&
2241
- contextUsage.effectiveInstructionTokens != null
2242
- ) {
2243
- contextUsage.remainingContextTokens = Math.max(
2244
- 0,
2245
- contextUsage.contextBudget -
2246
- contextUsage.effectiveInstructionTokens -
2247
- Math.round(rawTokens * usageRatio)
2248
- );
2249
- }
2250
- } else if (
2251
- preFormatTailTokens != null &&
2252
- agentContext.tokenCounter != null &&
2253
- contextUsage.remainingContextTokens != null
2254
- ) {
2255
- /** Same-length formatting can still mutate in place — the trailing
2256
- * tool batch (artifacts, Bedrock rewrites) and any legacy-converted
2257
- * messages before it — adjust remaining by the calibrated delta */
2258
- let postFormatTailTokens = 0;
2259
- for (const message of finalMessages.slice(tailStart)) {
2260
- postFormatTailTokens += agentContext.tokenCounter(message);
2261
- }
2262
- let formatDelta = postFormatTailTokens - preFormatTailTokens;
2263
- if (legacyIndices != null && legacyIndices.length > 0) {
2264
- let postFormatLegacyTokens = 0;
2265
- for (const index of legacyIndices) {
2266
- postFormatLegacyTokens += agentContext.tokenCounter(
2267
- finalMessages[index]
2268
- );
2269
- }
2270
- formatDelta += postFormatLegacyTokens - preFormatLegacyTokens;
2271
- }
2272
- if (formatDelta !== 0) {
2273
- contextUsage.remainingContextTokens = Math.max(
2274
- 0,
2275
- Math.min(
2276
- contextUsage.contextBudget ?? Number.MAX_SAFE_INTEGER,
2277
- contextUsage.remainingContextTokens -
2278
- Math.round(formatDelta * usageRatio)
2279
- )
2280
- );
2281
- }
2680
+ contextUsage.remainingContextTokens = Math.max(
2681
+ 0,
2682
+ finalProjection.availableMessageTokens -
2683
+ finalProjection.projectedMessageTokens
2684
+ );
2282
2685
  }
2283
2686
  syncBudgetDerivedFields(contextUsage);
2284
2687
  /** Awaited so async host handlers receive the pre-invoke snapshot
@@ -2346,6 +2749,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2346
2749
  const metadata = config.metadata as Record<string, unknown>;
2347
2750
 
2348
2751
  try {
2752
+ if (preInvokeContextOverflowError != null) {
2753
+ throw preInvokeContextOverflowError;
2754
+ }
2349
2755
  result = await withLangfuseRuntimeScope(
2350
2756
  resolveLangfuseRuntimeScope({
2351
2757
  runLangfuse: this.langfuse,
@@ -2383,15 +2789,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2383
2789
  */
2384
2790
  const estimatedPromptTokens = getEstimatedPromptTokens(contextUsage);
2385
2791
 
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
2394
- );
2395
2792
  const canSummarizeOverflow =
2396
2793
  agentContext.summarizationEnabled === true &&
2397
2794
  splitAtRecencyBoundary(messages, {
@@ -2402,13 +2799,36 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2402
2799
  tokenCounter: agentContext.tokenCounter,
2403
2800
  }).head.length > 0;
2404
2801
 
2802
+ const getLocalProviderOverflowMeasurement = (
2803
+ error: unknown
2804
+ ):
2805
+ | {
2806
+ contextBudget: number;
2807
+ estimatedPromptTokens: number;
2808
+ }
2809
+ | undefined =>
2810
+ typeof error === 'object' && error !== null
2811
+ ? localProviderOverflowMeasurements.get(error)
2812
+ : undefined;
2813
+
2814
+ const getRecoveryPromptEstimate = (
2815
+ error: unknown,
2816
+ fallbackContext?: FallbackErrorContext
2817
+ ): number | undefined => {
2818
+ const resolvedFallbackContext =
2819
+ fallbackContext ?? getFallbackErrorContext(error);
2820
+ return (
2821
+ getLocalProviderOverflowMeasurement(error)?.estimatedPromptTokens ??
2822
+ (resolvedFallbackContext == null
2823
+ ? estimatedPromptTokens
2824
+ : undefined)
2825
+ );
2826
+ };
2827
+
2405
2828
  const planRecovery = (
2406
2829
  error: unknown,
2407
2830
  attributedFallbackContext?: FallbackErrorContext
2408
2831
  ): OverflowRecoveryPlan | null => {
2409
- if (recoveryStalled) {
2410
- return null;
2411
- }
2412
2832
  /**
2413
2833
  * When the rejection came from a fallback, plan against *that*
2414
2834
  * client: its window and output allowance are why it was configured
@@ -2416,13 +2836,28 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2416
2836
  */
2417
2837
  const fallbackContext =
2418
2838
  attributedFallbackContext ?? getFallbackErrorContext(error);
2839
+ const localMeasurement = getLocalProviderOverflowMeasurement(error);
2840
+ const recoveryPromptEstimate = getRecoveryPromptEstimate(
2841
+ error,
2842
+ fallbackContext
2843
+ );
2844
+ /**
2845
+ * A previous correction that left the rejected prompt no smaller
2846
+ * proves this state has nothing left to compact. Use the fallback
2847
+ * projection when one exists so unlike provider formats are never
2848
+ * compared through the primary's cheaper pre-projection estimate.
2849
+ */
2850
+ if (agentContext.overflowRecoveryStalled(recoveryPromptEstimate)) {
2851
+ return null;
2852
+ }
2419
2853
  const recovery = planContextOverflowRecovery({
2420
2854
  error,
2421
2855
  provider: fallbackContext?.provider ?? agentContext.provider,
2422
2856
  maxContextTokens:
2857
+ localMeasurement?.contextBudget ??
2423
2858
  fallbackContext?.maxContextTokens ??
2424
2859
  agentContext.maxContextTokens,
2425
- estimatedPromptTokens,
2860
+ estimatedPromptTokens: recoveryPromptEstimate,
2426
2861
  calibrationRatio: agentContext.calibrationRatio,
2427
2862
  instructionTokens: agentContext.instructionTokens,
2428
2863
  canSummarize: agentContext.summarizationEnabled === true,
@@ -2440,12 +2875,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2440
2875
  ...recovery,
2441
2876
  budgetTokens: minDefined(
2442
2877
  getBlindRecoveryBudget(agentContext.maxContextTokens),
2443
- translateRecoveryBudget(
2444
- recovery.budgetTokens,
2445
- recovery.observedCalibrationRatio ??
2446
- CALIBRATION_RATIO_MAX,
2447
- agentContext.calibrationRatio
2448
- )
2878
+ localMeasurement != null
2879
+ ? recovery.budgetTokens
2880
+ : translateRecoveryBudget(
2881
+ recovery.budgetTokens,
2882
+ recovery.observedCalibrationRatio ??
2883
+ CALIBRATION_RATIO_MAX,
2884
+ agentContext.calibrationRatio
2885
+ )
2449
2886
  ),
2450
2887
  observedCalibrationRatio: undefined,
2451
2888
  }
@@ -2459,16 +2896,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2459
2896
 
2460
2897
  const recovery = planRecovery(primaryError);
2461
2898
  if (recovery != null) {
2899
+ const recoveryPromptEstimate =
2900
+ getRecoveryPromptEstimate(primaryError);
2462
2901
  return this.beginOverflowRecovery({
2463
2902
  recovery,
2464
2903
  agentContext,
2465
2904
  agentId,
2466
2905
  config,
2467
2906
  originalToolContent: prunedOriginalToolContent,
2468
- estimatedPromptTokens,
2907
+ estimatedPromptTokens: recoveryPromptEstimate,
2469
2908
  });
2470
2909
  }
2471
-
2472
2910
  /**
2473
2911
  * A fallback can reject the same prompt as too large even when the
2474
2912
  * primary failed for an unrelated reason — a fallback with a smaller
@@ -2485,7 +2923,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2485
2923
  tryFallbackProviders({
2486
2924
  fallbacks,
2487
2925
  tools: agentContext.tools,
2488
- messages: finalMessages,
2926
+ messages: fallbackBaseMessages,
2489
2927
  config: invokeConfig,
2490
2928
  primaryError,
2491
2929
  context: this,
@@ -2499,20 +2937,71 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2499
2937
  estimatedPromptTokens: getEstimatedPromptTokens(contextUsage),
2500
2938
  maxContextTokens: agentContext.maxContextTokens,
2501
2939
  },
2940
+ prepareProviderMessages: ({
2941
+ model: fallbackModel,
2942
+ messages: fallbackMessages,
2943
+ provider: fallbackProvider,
2944
+ maxContextTokens: fallbackMaxContextTokens,
2945
+ config: fallbackConfig,
2946
+ }) => {
2947
+ const fallbackToolResultChars =
2948
+ agentContext.maxToolResultChars ??
2949
+ calculateMaxToolResultChars(
2950
+ fallbackMaxContextTokens ?? agentContext.maxContextTokens
2951
+ );
2952
+ const projectedFallbackMessages = trackProviderMessageOrigins(
2953
+ fallbackMessages,
2954
+ projectMessagesForProvider({
2955
+ model: fallbackModel,
2956
+ messages: fallbackMessages,
2957
+ provider: fallbackProvider,
2958
+ maxToolResultChars: fallbackToolResultChars,
2959
+ callOptions: fallbackConfig,
2960
+ })
2961
+ );
2962
+ const primaryContextBudget = contextUsage?.contextBudget;
2963
+ const fallbackContextBudget =
2964
+ fallbackMaxContextTokens == null
2965
+ ? primaryContextBudget
2966
+ : Math.min(
2967
+ primaryContextBudget ?? fallbackMaxContextTokens,
2968
+ fallbackMaxContextTokens
2969
+ );
2970
+ const projection = measureProviderPayload(
2971
+ projectedFallbackMessages,
2972
+ fallbackContextBudget,
2973
+ true
2974
+ );
2975
+ if (!projection.fits) {
2976
+ throw createProviderPayloadOverflowError({
2977
+ projection,
2978
+ provider: fallbackProvider,
2979
+ info: 'Fallback provider message formatting exceeded the context budget before invocation.',
2980
+ });
2981
+ }
2982
+ return projectedFallbackMessages;
2983
+ },
2502
2984
  })
2503
2985
  );
2504
2986
  } catch (fallbackError) {
2505
2987
  const overflowCandidates =
2506
2988
  getFallbackOverflowCandidates(fallbackError);
2507
2989
  let fallbackRecovery: OverflowRecoveryPlan | null = null;
2990
+ let fallbackRecoveryPromptEstimate: number | undefined;
2508
2991
  for (const candidate of overflowCandidates) {
2509
2992
  fallbackRecovery = planRecovery(candidate.error, candidate.context);
2510
2993
  if (fallbackRecovery != null) {
2994
+ fallbackRecoveryPromptEstimate = getRecoveryPromptEstimate(
2995
+ candidate.error,
2996
+ candidate.context
2997
+ );
2511
2998
  break;
2512
2999
  }
2513
3000
  }
2514
3001
  if (overflowCandidates.length === 0) {
2515
3002
  fallbackRecovery = planRecovery(fallbackError);
3003
+ fallbackRecoveryPromptEstimate =
3004
+ getRecoveryPromptEstimate(fallbackError);
2516
3005
  }
2517
3006
  if (fallbackRecovery == null) {
2518
3007
  throw fallbackError;
@@ -2523,7 +3012,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2523
3012
  agentId,
2524
3013
  config,
2525
3014
  originalToolContent: prunedOriginalToolContent,
2526
- estimatedPromptTokens,
3015
+ estimatedPromptTokens: fallbackRecoveryPromptEstimate,
2527
3016
  });
2528
3017
  }
2529
3018
  } finally {
@@ -3165,6 +3654,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3165
3654
  }
3166
3655
 
3167
3656
  const { name, input: args, error } = data;
3657
+ const eventValueLimit = calculateMaxToolResultChars();
3658
+ const errorOutputPrefix = 'Error processing tool';
3659
+ const errorDetail =
3660
+ error?.message != null
3661
+ ? `: ${serializeToolContentBounded(
3662
+ error.message,
3663
+ Math.max(0, eventValueLimit - errorOutputPrefix.length - 2)
3664
+ )}`
3665
+ : '';
3168
3666
 
3169
3667
  const runStep = graph.getRunStep(stepId);
3170
3668
  if (!runStep) {
@@ -3174,8 +3672,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3174
3672
  const tool_call: t.ProcessedToolCall = {
3175
3673
  id: data.id,
3176
3674
  name: name || '',
3177
- args: typeof args === 'string' ? args : JSON.stringify(args),
3178
- output: `Error processing tool${error?.message != null ? `: ${error.message}` : ''}`,
3675
+ args: serializeToolContentBounded(args, eventValueLimit),
3676
+ output: `${errorOutputPrefix}${errorDetail}`,
3179
3677
  progress: 1,
3180
3678
  };
3181
3679