@librechat/agents 3.3.3 → 3.3.5

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 (274) 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/common/constants.cjs +21 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +10 -1
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +793 -111
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/index.cjs +12 -0
  14. package/dist/cjs/hooks/index.cjs.map +1 -1
  15. package/dist/cjs/hooks/types.cjs +1 -0
  16. package/dist/cjs/hooks/types.cjs.map +1 -1
  17. package/dist/cjs/langfuse.cjs +8 -0
  18. package/dist/cjs/langfuse.cjs.map +1 -1
  19. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  20. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  21. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
  22. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  24. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  25. package/dist/cjs/llm/invoke.cjs +331 -5
  26. package/dist/cjs/llm/invoke.cjs.map +1 -1
  27. package/dist/cjs/llm/openai/index.cjs +41 -5
  28. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  29. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  30. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  31. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  32. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  33. package/dist/cjs/llm/preempt.cjs +132 -0
  34. package/dist/cjs/llm/preempt.cjs.map +1 -0
  35. package/dist/cjs/main.cjs +36 -5
  36. package/dist/cjs/messages/alternation.cjs +92 -0
  37. package/dist/cjs/messages/alternation.cjs.map +1 -0
  38. package/dist/cjs/messages/cache.cjs +54 -33
  39. package/dist/cjs/messages/cache.cjs.map +1 -1
  40. package/dist/cjs/messages/contextPruning.cjs +17 -43
  41. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  42. package/dist/cjs/messages/core.cjs +315 -23
  43. package/dist/cjs/messages/core.cjs.map +1 -1
  44. package/dist/cjs/messages/format.cjs +290 -60
  45. package/dist/cjs/messages/format.cjs.map +1 -1
  46. package/dist/cjs/messages/handoffCue.cjs +63 -0
  47. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  48. package/dist/cjs/messages/index.cjs +4 -1
  49. package/dist/cjs/messages/injected.cjs +60 -0
  50. package/dist/cjs/messages/injected.cjs.map +1 -0
  51. package/dist/cjs/messages/prune.cjs +503 -127
  52. package/dist/cjs/messages/prune.cjs.map +1 -1
  53. package/dist/cjs/run.cjs +80 -7
  54. package/dist/cjs/run.cjs.map +1 -1
  55. package/dist/cjs/session/handlers.cjs +18 -9
  56. package/dist/cjs/session/handlers.cjs.map +1 -1
  57. package/dist/cjs/stream.cjs +35 -10
  58. package/dist/cjs/stream.cjs.map +1 -1
  59. package/dist/cjs/summarization/node.cjs +28 -14
  60. package/dist/cjs/summarization/node.cjs.map +1 -1
  61. package/dist/cjs/tools/ToolNode.cjs +56 -63
  62. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  63. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  64. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  65. package/dist/cjs/tools/search/tool.cjs +9 -1
  66. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  67. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  68. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  69. package/dist/cjs/utils/index.cjs +1 -1
  70. package/dist/cjs/utils/llm.cjs +1 -1
  71. package/dist/cjs/utils/llm.cjs.map +1 -1
  72. package/dist/cjs/utils/tokens.cjs +307 -20
  73. package/dist/cjs/utils/tokens.cjs.map +1 -1
  74. package/dist/cjs/utils/toolContent.cjs +1514 -0
  75. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  76. package/dist/esm/agents/AgentContext.mjs +2 -1
  77. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  78. package/dist/esm/common/constants.mjs +19 -1
  79. package/dist/esm/common/constants.mjs.map +1 -1
  80. package/dist/esm/events.mjs +10 -1
  81. package/dist/esm/events.mjs.map +1 -1
  82. package/dist/esm/graphs/Graph.mjs +798 -116
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  85. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  86. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  87. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  88. package/dist/esm/hooks/index.mjs +12 -1
  89. package/dist/esm/hooks/index.mjs.map +1 -1
  90. package/dist/esm/hooks/types.mjs +1 -0
  91. package/dist/esm/hooks/types.mjs.map +1 -1
  92. package/dist/esm/langfuse.mjs +9 -1
  93. package/dist/esm/langfuse.mjs.map +1 -1
  94. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  95. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/google/utils/common.mjs +19 -7
  99. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  100. package/dist/esm/llm/invoke.mjs +332 -8
  101. package/dist/esm/llm/invoke.mjs.map +1 -1
  102. package/dist/esm/llm/openai/index.mjs +41 -5
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/openrouter/index.mjs +4 -1
  107. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  108. package/dist/esm/llm/preempt.mjs +131 -0
  109. package/dist/esm/llm/preempt.mjs.map +1 -0
  110. package/dist/esm/main.mjs +14 -11
  111. package/dist/esm/messages/alternation.mjs +91 -0
  112. package/dist/esm/messages/alternation.mjs.map +1 -0
  113. package/dist/esm/messages/cache.mjs +55 -34
  114. package/dist/esm/messages/cache.mjs.map +1 -1
  115. package/dist/esm/messages/contextPruning.mjs +17 -43
  116. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  117. package/dist/esm/messages/core.mjs +305 -24
  118. package/dist/esm/messages/core.mjs.map +1 -1
  119. package/dist/esm/messages/format.mjs +290 -61
  120. package/dist/esm/messages/format.mjs.map +1 -1
  121. package/dist/esm/messages/handoffCue.mjs +61 -0
  122. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  123. package/dist/esm/messages/index.mjs +4 -1
  124. package/dist/esm/messages/injected.mjs +60 -0
  125. package/dist/esm/messages/injected.mjs.map +1 -0
  126. package/dist/esm/messages/prune.mjs +500 -128
  127. package/dist/esm/messages/prune.mjs.map +1 -1
  128. package/dist/esm/run.mjs +80 -7
  129. package/dist/esm/run.mjs.map +1 -1
  130. package/dist/esm/session/handlers.mjs +19 -10
  131. package/dist/esm/session/handlers.mjs.map +1 -1
  132. package/dist/esm/stream.mjs +34 -11
  133. package/dist/esm/stream.mjs.map +1 -1
  134. package/dist/esm/summarization/node.mjs +28 -14
  135. package/dist/esm/summarization/node.mjs.map +1 -1
  136. package/dist/esm/tools/ToolNode.mjs +56 -63
  137. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  138. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  139. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  140. package/dist/esm/tools/search/tool.mjs +9 -1
  141. package/dist/esm/tools/search/tool.mjs.map +1 -1
  142. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  143. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  144. package/dist/esm/utils/index.mjs +1 -1
  145. package/dist/esm/utils/llm.mjs +1 -1
  146. package/dist/esm/utils/llm.mjs.map +1 -1
  147. package/dist/esm/utils/tokens.mjs +307 -21
  148. package/dist/esm/utils/tokens.mjs.map +1 -1
  149. package/dist/esm/utils/toolContent.mjs +1503 -0
  150. package/dist/esm/utils/toolContent.mjs.map +1 -0
  151. package/dist/types/common/constants.d.ts +18 -0
  152. package/dist/types/graphs/Graph.d.ts +139 -2
  153. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  154. package/dist/types/hooks/index.d.ts +12 -1
  155. package/dist/types/hooks/types.d.ts +45 -6
  156. package/dist/types/index.d.ts +1 -1
  157. package/dist/types/llm/invoke.d.ts +34 -1
  158. package/dist/types/llm/openai/index.d.ts +4 -0
  159. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  160. package/dist/types/llm/openrouter/index.d.ts +5 -0
  161. package/dist/types/llm/preempt.d.ts +40 -0
  162. package/dist/types/messages/alternation.d.ts +23 -0
  163. package/dist/types/messages/contextPruning.d.ts +2 -1
  164. package/dist/types/messages/core.d.ts +36 -0
  165. package/dist/types/messages/format.d.ts +6 -0
  166. package/dist/types/messages/handoffCue.d.ts +40 -0
  167. package/dist/types/messages/index.d.ts +3 -0
  168. package/dist/types/messages/injected.d.ts +3 -0
  169. package/dist/types/messages/prune.d.ts +16 -12
  170. package/dist/types/run.d.ts +7 -0
  171. package/dist/types/stream.d.ts +14 -0
  172. package/dist/types/tools/ToolNode.d.ts +0 -7
  173. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  174. package/dist/types/tools/search/types.d.ts +31 -2
  175. package/dist/types/types/graph.d.ts +8 -1
  176. package/dist/types/types/run.d.ts +65 -0
  177. package/dist/types/types/stream.d.ts +1 -26
  178. package/dist/types/utils/tokens.d.ts +7 -0
  179. package/dist/types/utils/toolContent.d.ts +107 -0
  180. package/package.json +1 -1
  181. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  182. package/src/agents/AgentContext.ts +1 -0
  183. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  184. package/src/common/constants.ts +21 -0
  185. package/src/events.ts +15 -1
  186. package/src/graphs/Graph.ts +1261 -198
  187. package/src/graphs/MultiAgentGraph.ts +6 -1
  188. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
  189. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  190. package/src/hooks/HookRegistry.ts +40 -0
  191. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  192. package/src/hooks/index.ts +16 -2
  193. package/src/hooks/types.ts +47 -3
  194. package/src/index.ts +1 -1
  195. package/src/langfuse.ts +26 -1
  196. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  197. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  198. package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
  199. package/src/llm/bedrock/utils/message_inputs.ts +100 -24
  200. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  201. package/src/llm/google/utils/common.test.ts +75 -9
  202. package/src/llm/google/utils/common.ts +34 -3
  203. package/src/llm/invoke.alternation.test.ts +87 -0
  204. package/src/llm/invoke.handoffCue.test.ts +112 -0
  205. package/src/llm/invoke.test.ts +488 -0
  206. package/src/llm/invoke.ts +611 -8
  207. package/src/llm/openai/index.ts +106 -6
  208. package/src/llm/openai/utils/index.ts +109 -53
  209. package/src/llm/openai/utils/messages.test.ts +330 -1
  210. package/src/llm/openrouter/index.ts +12 -2
  211. package/src/llm/openrouter/reasoning.test.ts +314 -0
  212. package/src/llm/preempt.test.ts +323 -0
  213. package/src/llm/preempt.ts +178 -0
  214. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  215. package/src/messages/alternation.test.ts +212 -0
  216. package/src/messages/alternation.ts +112 -0
  217. package/src/messages/cache.tail.test.ts +193 -0
  218. package/src/messages/cache.test.ts +113 -0
  219. package/src/messages/cache.ts +92 -60
  220. package/src/messages/contextPruning.test.ts +184 -0
  221. package/src/messages/contextPruning.ts +49 -42
  222. package/src/messages/core.ts +653 -40
  223. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  224. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  225. package/src/messages/format.ts +454 -91
  226. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  227. package/src/messages/formatAgentMessages.test.ts +604 -14
  228. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  229. package/src/messages/handoffCue.test.ts +96 -0
  230. package/src/messages/handoffCue.ts +78 -0
  231. package/src/messages/index.ts +3 -0
  232. package/src/messages/injected.test.ts +90 -0
  233. package/src/messages/injected.ts +74 -0
  234. package/src/messages/prune.ts +996 -183
  235. package/src/run.ts +91 -6
  236. package/src/scripts/preempt-probe.ts +330 -0
  237. package/src/scripts/preempt-scenarios.ts +388 -0
  238. package/src/session/handlers.ts +32 -12
  239. package/src/specs/handoffCue.test.ts +165 -0
  240. package/src/specs/langfuse-callbacks.test.ts +352 -2
  241. package/src/specs/preemptSeal.test.ts +309 -0
  242. package/src/specs/prune.test.ts +1083 -6
  243. package/src/specs/summarization-unit.test.ts +105 -0
  244. package/src/specs/tokens.test.ts +609 -32
  245. package/src/stream.dispatch.test.ts +63 -0
  246. package/src/stream.ts +59 -23
  247. package/src/summarization/__tests__/node.test.ts +77 -0
  248. package/src/summarization/node.ts +46 -18
  249. package/src/tools/ToolNode.ts +150 -99
  250. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  251. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  252. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  253. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  254. package/src/tools/search/keenable-scraper.test.ts +153 -0
  255. package/src/tools/search/keenable-scraper.ts +137 -0
  256. package/src/tools/search/tool.ts +13 -2
  257. package/src/tools/search/types.ts +50 -3
  258. package/src/tools/toolOutputReferences.ts +6 -0
  259. package/src/types/graph.ts +12 -1
  260. package/src/types/run.ts +67 -0
  261. package/src/types/stream.ts +1 -42
  262. package/src/utils/llm.test.ts +18 -0
  263. package/src/utils/llm.ts +4 -1
  264. package/src/utils/tokens.ts +586 -31
  265. package/src/utils/toolContent.test.ts +1432 -0
  266. package/src/utils/toolContent.ts +2707 -0
  267. package/dist/cjs/splitStream.cjs +0 -151
  268. package/dist/cjs/splitStream.cjs.map +0 -1
  269. package/dist/esm/splitStream.mjs +0 -150
  270. package/dist/esm/splitStream.mjs.map +0 -1
  271. package/dist/types/mockStream.d.ts +0 -32
  272. package/dist/types/splitStream.d.ts +0 -37
  273. package/src/mockStream.ts +0 -99
  274. package/src/splitStream.ts +0 -234
package/src/llm/invoke.ts CHANGED
@@ -1,19 +1,51 @@
1
1
  import { concat } from '@langchain/core/utils/stream';
2
2
  import { AIMessageChunk } from '@langchain/core/messages';
3
+ import {
4
+ CallbackManager,
5
+ CallbackManagerForLLMRun,
6
+ type Callbacks,
7
+ } from '@langchain/core/callbacks/manager';
8
+ import { getCallbackManagerForConfig } from '@langchain/core/runnables';
9
+ import type { Serialized } from '@langchain/core/load/serializable';
10
+ import type { ChatGeneration } from '@langchain/core/outputs';
3
11
  import type { RunnableConfig } from '@langchain/core/runnables';
4
12
  import type { ToolCall } from '@langchain/core/messages/tool';
5
13
  import type { BaseMessage } from '@langchain/core/messages';
6
14
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
7
15
  import type { ContextOverflowContext } from '@/utils/errors';
8
16
  import type * as t from '@/types';
17
+ import {
18
+ projectCacheControlledToolOutputsToText,
19
+ projectComputerCallOutputsToText,
20
+ projectOpenAIChatToolMessageContent,
21
+ projectOpenAIResponsesToolMessageContent,
22
+ projectOpenRouterToolMessageContent,
23
+ projectSingleTextToolOutputsToText,
24
+ projectStructuredToolOutputsToText,
25
+ projectToolStreamContentForProvider,
26
+ } from '@/messages/core';
27
+ import {
28
+ stripAnthropicCacheControl,
29
+ stripBedrockCacheControl,
30
+ } from '@/messages/cache';
9
31
  import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
10
32
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
11
- import { Constants, GraphEvents, Providers } from '@/common';
33
+ import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
12
34
  import { manualToolStreamProviders } from '@/llm/providers';
35
+ import { appendCallbacks } from '@/utils/callbacks';
36
+ import { safeDispatchCustomEvent } from '@/utils/events';
13
37
  import { getContextOverflowInfo } from '@/utils/errors';
14
- import { modifyDeltaProperties } from '@/messages';
15
- import { ChatModelStreamHandler } from '@/stream';
38
+ import {
39
+ modifyDeltaProperties,
40
+ coalesceAdjacentUserTurns,
41
+ strictAlternationProviders,
42
+ appendPredecessorHandoffCue,
43
+ removePredecessorHandoffCue,
44
+ } from '@/messages';
45
+ import { canSealPreempt } from '@/llm/preempt';
46
+ import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
16
47
  import { initializeModel } from '@/llm/init';
48
+ import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
17
49
 
18
50
  /**
19
51
  * Context passed to `attemptInvoke`. Matches the subset of Graph that
@@ -54,13 +86,169 @@ export type InvokeContext = NonNullable<
54
86
  */
55
87
  export type OnChunk = (chunk: AIMessageChunk) => void | Promise<void>;
56
88
 
89
+ export function usesNativeOpenAIResponses(
90
+ model: t.ChatModel,
91
+ provider: Providers,
92
+ callOptions?: unknown
93
+ ): boolean {
94
+ if (!isOpenAILike(provider)) {
95
+ return false;
96
+ }
97
+ let candidate: unknown = model;
98
+ let effectiveCallOptions = callOptions;
99
+ const seen = new Set<object>();
100
+ for (let depth = 0; depth < 20; depth++) {
101
+ if (candidate == null || typeof candidate !== 'object') {
102
+ return false;
103
+ }
104
+ if (seen.has(candidate)) {
105
+ return false;
106
+ }
107
+ seen.add(candidate);
108
+ const runnable = candidate as {
109
+ _useResponsesApi?: (options?: unknown) => boolean;
110
+ bound?: unknown;
111
+ defaultOptions?: unknown;
112
+ last?: unknown;
113
+ constructor?: { name?: unknown };
114
+ };
115
+ try {
116
+ if (
117
+ runnable.defaultOptions != null &&
118
+ typeof runnable.defaultOptions === 'object' &&
119
+ !Array.isArray(runnable.defaultOptions) &&
120
+ effectiveCallOptions != null &&
121
+ typeof effectiveCallOptions === 'object' &&
122
+ !Array.isArray(effectiveCallOptions)
123
+ ) {
124
+ effectiveCallOptions = {
125
+ ...(runnable.defaultOptions as Record<string, unknown>),
126
+ ...(effectiveCallOptions as Record<string, unknown>),
127
+ };
128
+ } else if (effectiveCallOptions == null) {
129
+ effectiveCallOptions = runnable.defaultOptions;
130
+ }
131
+ if (
132
+ runnable._useResponsesApi?.(effectiveCallOptions) === true ||
133
+ runnable._useResponsesApi?.(undefined) === true
134
+ ) {
135
+ return true;
136
+ }
137
+ } catch {
138
+ // Continue through RunnableSequence/RunnableBinding wrappers.
139
+ }
140
+ if (
141
+ typeof runnable.constructor?.name === 'string' &&
142
+ runnable.constructor.name.includes('Responses')
143
+ ) {
144
+ return true;
145
+ }
146
+ if (runnable.last != null && typeof runnable.last === 'object') {
147
+ candidate = runnable.last;
148
+ continue;
149
+ }
150
+ if (runnable.bound != null && typeof runnable.bound === 'object') {
151
+ candidate = runnable.bound;
152
+ continue;
153
+ }
154
+ return false;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Produces the exact provider-facing message representation before a model
161
+ * adapter serializes it. This is shared by invocation and Graph's final budget
162
+ * guard so structured tool output cannot grow after the payload was measured.
163
+ */
164
+ export function projectMessagesForProvider({
165
+ model,
166
+ messages,
167
+ provider,
168
+ maxToolResultChars,
169
+ callOptions,
170
+ }: {
171
+ model: t.ChatModel;
172
+ messages: BaseMessage[];
173
+ provider: Providers;
174
+ maxToolResultChars?: number;
175
+ callOptions?: unknown;
176
+ }): BaseMessage[] {
177
+ const providerInputMessages = projectToolStreamContentForProvider(messages);
178
+ if (usesNativeOpenAIResponses(model, provider, callOptions)) {
179
+ return projectOpenAIResponsesToolMessageContent(
180
+ stripAnthropicCacheControl(
181
+ stripBedrockCacheControl(providerInputMessages)
182
+ ),
183
+ maxToolResultChars
184
+ );
185
+ }
186
+ if (provider === Providers.OPENROUTER) {
187
+ return projectComputerCallOutputsToText(
188
+ projectOpenRouterToolMessageContent(
189
+ stripBedrockCacheControl(providerInputMessages),
190
+ maxToolResultChars
191
+ )
192
+ );
193
+ }
194
+ if (isOpenAILike(provider)) {
195
+ return projectComputerCallOutputsToText(
196
+ projectOpenAIChatToolMessageContent(
197
+ stripAnthropicCacheControl(
198
+ stripBedrockCacheControl(providerInputMessages)
199
+ ),
200
+ maxToolResultChars
201
+ )
202
+ );
203
+ }
204
+ if (provider === Providers.ANTHROPIC) {
205
+ return projectComputerCallOutputsToText(
206
+ projectSingleTextToolOutputsToText(
207
+ stripBedrockCacheControl(providerInputMessages),
208
+ maxToolResultChars
209
+ )
210
+ );
211
+ }
212
+ if (provider === Providers.BEDROCK) {
213
+ return stripAnthropicCacheControl(
214
+ projectComputerCallOutputsToText(
215
+ projectCacheControlledToolOutputsToText(
216
+ providerInputMessages,
217
+ maxToolResultChars
218
+ )
219
+ )
220
+ );
221
+ }
222
+ return projectComputerCallOutputsToText(
223
+ projectStructuredToolOutputsToText(
224
+ projectSingleTextToolOutputsToText(
225
+ stripAnthropicCacheControl(
226
+ stripBedrockCacheControl(providerInputMessages)
227
+ ),
228
+ maxToolResultChars
229
+ ),
230
+ maxToolResultChars
231
+ )
232
+ );
233
+ }
234
+
235
+ /**
236
+ * The registered handler that owns content-part dispatch, if any.
237
+ *
238
+ * Detected by brand rather than by `instanceof`: a host that registers
239
+ * `new ChatModelStreamHandler()` to opt out of sealing gets wrapped by
240
+ * `createRunHandlers` on every `AgentSession` run, and by
241
+ * `composeEventHandlers` on a key collision. Both wrappers forward to the same
242
+ * dispatcher while failing an identity check, so an identity test would
243
+ * silently revoke the opt-out documented on `StreamPreemption`.
244
+ */
57
245
  function getRegisteredDefaultChatStreamHandler(
58
246
  context?: InvokeContext
59
- ): ChatModelStreamHandler | undefined {
247
+ ): t.EventHandler | undefined {
60
248
  const handler = context?.handlerRegistry?.getHandler(
61
249
  GraphEvents.CHAT_MODEL_STREAM
62
250
  );
63
- return handler instanceof ChatModelStreamHandler ? handler : undefined;
251
+ return dispatchesChatModelStream(handler) ? handler : undefined;
64
252
  }
65
253
 
66
254
  function hasReasoningDetails(chunk: AIMessageChunk): boolean {
@@ -157,6 +345,280 @@ function getStreamHandlingChunk({
157
345
  );
158
346
  }
159
347
 
348
+ /**
349
+ * Best-effort output-token count for a sealed turn, used only when the
350
+ * provider never got to send its usage chunk.
351
+ */
352
+ function countSealedTokens(
353
+ context: InvokeContext | undefined,
354
+ metadata: Record<string, unknown> | undefined,
355
+ messages: BaseMessage[]
356
+ ): number | undefined {
357
+ try {
358
+ const counter = context?.getAgentContext(metadata).tokenCounter;
359
+ if (counter == null) {
360
+ return undefined;
361
+ }
362
+ let total = 0;
363
+ for (const message of messages) {
364
+ total += counter(message);
365
+ }
366
+ return total;
367
+ } catch {
368
+ return undefined;
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Instruction overhead the provider processed but that never appears in the
374
+ * message array: `createCallModel` pipes the model through
375
+ * `agentContext.systemRunnable` and binds tool schemas AFTER `messages` is
376
+ * formed, so the system prompt, dynamic instructions, summary and tool
377
+ * schemas are all billed yet invisible here.
378
+ *
379
+ * Read per-node via `getAgentContext(metadata)` rather than the graph-level
380
+ * accessor, which is hardcoded to `defaultAgentId` and would report the wrong
381
+ * agent's overhead in a `MultiAgentGraph`.
382
+ */
383
+ function sealedInstructionOverhead(
384
+ context: InvokeContext | undefined,
385
+ metadata: Record<string, unknown> | undefined
386
+ ): number {
387
+ try {
388
+ const agentContext = context?.getAgentContext(metadata);
389
+ return (
390
+ agentContext?.resolvedInstructionOverhead ??
391
+ agentContext?.instructionTokens ??
392
+ 0
393
+ );
394
+ } catch {
395
+ return 0;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Best-effort usage for a turn the provider never got to bill us for.
401
+ *
402
+ * The prompt matters as much as the completion: the provider processed the
403
+ * ENTIRE prompt — messages plus instruction overhead — before we sealed, and
404
+ * every resume re-sends it, so under-counting input hides the expensive half
405
+ * of a preempted run.
406
+ *
407
+ * ESTIMATE, NOT MEASUREMENT. Messages are counted with the host's tokenizer
408
+ * rather than the provider's, and `toolSchemaTokens` applies a heuristic
409
+ * multiplier. It is also an over-count on the fallback path, where
410
+ * `tryFallbackProviders` builds a bare model with no `systemRunnable` pipe so
411
+ * the system prompt genuinely is not sent. Accepted rather than threaded
412
+ * through a flag: only the fallback-plus-seal combination is affected, and an
413
+ * over-count is safer than the previous fabricated `input_tokens: 0`.
414
+ *
415
+ * Marked `estimated_usage` so calibration can refuse to learn from it — a
416
+ * ratio derived from the same counter that produced the estimate is
417
+ * self-consistent by construction and would drag a provider's real
418
+ * calibration toward 1.0.
419
+ */
420
+ function synthesizeSealedUsage(
421
+ context: InvokeContext | undefined,
422
+ chunk: AIMessageChunk,
423
+ prompt: BaseMessage[],
424
+ metadata: Record<string, unknown> | undefined
425
+ ): void {
426
+ if (chunk.usage_metadata != null) {
427
+ return;
428
+ }
429
+ const outputTokens = countSealedTokens(context, metadata, [chunk]);
430
+ if (outputTokens == null) {
431
+ return;
432
+ }
433
+ const inputTokens =
434
+ (countSealedTokens(context, metadata, prompt) ?? 0) +
435
+ sealedInstructionOverhead(context, metadata);
436
+ chunk.usage_metadata = {
437
+ input_tokens: inputTokens,
438
+ output_tokens: outputTokens,
439
+ total_tokens: inputTokens + outputTokens,
440
+ };
441
+ chunk.response_metadata = {
442
+ ...chunk.response_metadata,
443
+ estimated_usage: true,
444
+ };
445
+ }
446
+
447
+ function getMessageText(chunk: AIMessageChunk): string {
448
+ if (typeof chunk.content === 'string') {
449
+ return chunk.content;
450
+ }
451
+ let text = '';
452
+ for (const block of chunk.content) {
453
+ if (block.type === ContentTypes.TEXT) {
454
+ const value = block[ContentTypes.TEXT];
455
+ if (typeof value === 'string') {
456
+ text += value;
457
+ }
458
+ }
459
+ }
460
+ return text;
461
+ }
462
+
463
+ /**
464
+ * Ends the real model run for a turn that was sealed mid-stream.
465
+ *
466
+ * Mandatory, not cosmetic. `@langchain/core`'s `_streamIterator` calls
467
+ * `handleLLMEnd` after its try/catch with no `finally`, so breaking out of the
468
+ * consumer's `for await` produces a *return* completion that fires neither
469
+ * `handleLLMError` nor `handleLLMEnd`. The run would stay open in every
470
+ * callback handler: the host records no usage — and since each seal re-sends
471
+ * the whole prompt, N preemptions cost N unrecorded prompts — while LangSmith
472
+ * and Langfuse hold a span that never closes.
473
+ *
474
+ * `runId` cannot be dictated from here (the bound runnable consumes
475
+ * `config.runId` for its own run and hands the chat model a fresh one), but it
476
+ * can be OBSERVED: the capture handler installed at the `model.stream` call
477
+ * records it from `handleChatModelStart`, which fires before the first chunk.
478
+ * Rebuilding the manager against that id closes the real run, and the host's
479
+ * `on_chat_model_end` then arrives through the ordinary `streamEvents` path.
480
+ *
481
+ * Falls back to a custom-event dispatch if the id was never observed, so the
482
+ * host still records usage even when the native close is unavailable.
483
+ */
484
+ /**
485
+ * Every callbacks source the real model run would compose beyond the per-call
486
+ * config. `model` here is whatever `createCallModel` produced — with tools
487
+ * that is `bindTools(...)`'s `RunnableBinding`, and a system runnable pipes a
488
+ * `RunnableSequence` on top — while `clientOptions.callbacks` lives on the
489
+ * chat model at the BOTTOM of that stack. Walks `bound` (bindings) and
490
+ * `last`/`steps` (sequences), collecting each wrapper's own `callbacks` and
491
+ * any binding-config callbacks along the way, since the binding merges its
492
+ * config into the call before the chat model composes.
493
+ */
494
+ function collectModelCallbackSources(model: unknown): Callbacks[] {
495
+ const sources: Callbacks[] = [];
496
+ const seen = new Set<unknown>();
497
+ let current: unknown = model;
498
+ while (current != null && typeof current === 'object' && !seen.has(current)) {
499
+ seen.add(current);
500
+ const wrapper = current as {
501
+ callbacks?: Callbacks;
502
+ config?: { callbacks?: Callbacks };
503
+ bound?: unknown;
504
+ last?: unknown;
505
+ steps?: unknown[];
506
+ };
507
+ if (wrapper.callbacks != null) {
508
+ sources.push(wrapper.callbacks);
509
+ }
510
+ if (wrapper.config?.callbacks != null) {
511
+ sources.push(wrapper.config.callbacks);
512
+ }
513
+ current =
514
+ wrapper.bound ??
515
+ wrapper.last ??
516
+ (Array.isArray(wrapper.steps)
517
+ ? wrapper.steps[wrapper.steps.length - 1]
518
+ : undefined);
519
+ }
520
+ return sources;
521
+ }
522
+
523
+ /**
524
+ * The serving model's id, read through the same wrapper stack
525
+ * `collectModelCallbackSources` walks — `bindTools` returns a
526
+ * `RunnableBinding` and a system runnable pipes a `RunnableSequence`, and
527
+ * neither exposes the chat model's `model` at the top level.
528
+ */
529
+ export function resolveServingModelId(model: unknown): string | undefined {
530
+ const seen = new Set<unknown>();
531
+ let current: unknown = model;
532
+ while (current != null && typeof current === 'object' && !seen.has(current)) {
533
+ seen.add(current);
534
+ const wrapper = current as {
535
+ model?: unknown;
536
+ bound?: unknown;
537
+ last?: unknown;
538
+ steps?: unknown[];
539
+ };
540
+ if (typeof wrapper.model === 'string' && wrapper.model !== '') {
541
+ return wrapper.model;
542
+ }
543
+ current =
544
+ wrapper.bound ??
545
+ wrapper.last ??
546
+ (Array.isArray(wrapper.steps)
547
+ ? wrapper.steps[wrapper.steps.length - 1]
548
+ : undefined);
549
+ }
550
+ return undefined;
551
+ }
552
+
553
+ async function endSealedModelRun(
554
+ context: InvokeContext | undefined,
555
+ chunk: AIMessageChunk,
556
+ prompt: BaseMessage[],
557
+ llmRunId: string | undefined,
558
+ config?: RunnableConfig,
559
+ model?: t.ChatModel
560
+ ): Promise<void> {
561
+ const metadata = config?.metadata as Record<string, unknown> | undefined;
562
+ synthesizeSealedUsage(context, chunk, prompt, metadata);
563
+ if (llmRunId != null) {
564
+ try {
565
+ let callbackManager = await getCallbackManagerForConfig(config);
566
+ /**
567
+ * The real model run composes the per-call config's callbacks WITH the
568
+ * model's own (`CallbackManager.configure(config.callbacks,
569
+ * this.callbacks, …)` in `@langchain/core`'s base chat model), so a
570
+ * handler supplied via `clientOptions.callbacks` received
571
+ * `handleChatModelStart` for this run. Rebuilding from the config alone
572
+ * would close the run for every handler EXCEPT those — leaving their
573
+ * span open forever. Composed the same way the real run composes:
574
+ * model callbacks appended non-inheritable, parent run id preserved by
575
+ * `copy`, tracers deduped by `configure`.
576
+ */
577
+ for (const source of collectModelCallbackSources(model)) {
578
+ callbackManager =
579
+ CallbackManager.configure(callbackManager ?? undefined, source) ??
580
+ callbackManager;
581
+ }
582
+ if (callbackManager != null) {
583
+ const runManager = new CallbackManagerForLLMRun(
584
+ llmRunId,
585
+ callbackManager.handlers,
586
+ callbackManager.inheritableHandlers,
587
+ callbackManager.tags,
588
+ callbackManager.inheritableTags,
589
+ callbackManager.metadata,
590
+ callbackManager.inheritableMetadata,
591
+ callbackManager.getParentRunId()
592
+ );
593
+ const generation: ChatGeneration = {
594
+ text: getMessageText(chunk),
595
+ message: chunk,
596
+ };
597
+ await runManager.handleLLMEnd({
598
+ generations: [[generation]],
599
+ llmOutput: {},
600
+ });
601
+ return;
602
+ }
603
+ } catch (e) {
604
+ /**
605
+ * A sealed answer that reaches the user is worth more than a tidy
606
+ * trace. Fall through to the custom event rather than failing the run.
607
+ */
608
+ // eslint-disable-next-line no-console
609
+ console.warn(
610
+ '[attemptInvoke] Native close of the sealed model run failed; falling back to a custom event:',
611
+ e instanceof Error ? e.message : e
612
+ );
613
+ }
614
+ }
615
+ await safeDispatchCustomEvent(
616
+ GraphEvents.CHAT_MODEL_END,
617
+ { output: chunk },
618
+ config
619
+ );
620
+ }
621
+
160
622
  function appendStreamChunk({
161
623
  current,
162
624
  next,
@@ -207,9 +669,66 @@ export async function attemptInvoke(
207
669
  * untouched so the graph state never sees `[ref: …]` / `_ref`
208
670
  * payload.
209
671
  */
672
+ const invocationMessages = projectMessagesForProvider({
673
+ model,
674
+ messages,
675
+ provider,
676
+ callOptions: config,
677
+ });
210
678
  const registry = context?.getOrCreateToolOutputRegistry();
211
679
  const runId = config?.configurable?.run_id as string | undefined;
212
- const messagesForProvider = annotateMessagesForLLM(messages, registry, runId);
680
+ const annotated = annotateMessagesForLLM(
681
+ invocationMessages,
682
+ registry,
683
+ runId
684
+ );
685
+ /**
686
+ * Keyed on the provider ACTUALLY serving this call, not the agent's primary.
687
+ * `createCallModel` normalizes for the primary, but `tryFallbackProviders`
688
+ * re-sends the same array — so an OpenAI primary that fails after a boundary
689
+ * injected two human turns would hand a Bedrock or Mistral fallback the
690
+ * consecutive user turns those APIs reject, and the recovery request would
691
+ * fail for a reason unrelated to the original failure.
692
+ *
693
+ * `attemptInvoke` is the single funnel for primary, fallback and
694
+ * summarization calls, so applying it here covers all three. Idempotent, so
695
+ * the primary simply re-runs a no-op over already-coalesced messages.
696
+ */
697
+ /**
698
+ * Serving-provider re-keying for the predecessor handoff cue (#345). The
699
+ * PRIMARY's cue is baked in createCallModel's measured transform stage —
700
+ * appending after measurement could push a just-fits prompt over budget —
701
+ * so this funnel only corrects for fallbacks crossing provider families:
702
+ * a tolerant primary falling back to a Claude surface gains the cue here,
703
+ * and an Anthropic primary falling back to OpenAI/Mistral/Nova has the
704
+ * Claude-only synthetic turn stripped. Both helpers are identity on their
705
+ * no-op paths, so the primary's own pass re-runs for free.
706
+ *
707
+ * The serving model id is read through the wrapper stack (`bindTools`'
708
+ * binding, a system runnable's sequence) — a wrapper's top-level `.model`
709
+ * is undefined, and `isAnthropicLike` would otherwise default a wrapped
710
+ * Bedrock-Nova model to Claude. The context cast is widened deliberately:
711
+ * the type says every context is a full Graph, but summarization passes
712
+ * none and long-standing tests pass partial stubs.
713
+ */
714
+ const isRunProduced = (
715
+ context as
716
+ | { isRunProducedMessage?: (message: BaseMessage) => boolean }
717
+ | undefined
718
+ )?.isRunProducedMessage;
719
+ const cued = isAnthropicLike(provider, {
720
+ model: resolveServingModelId(model),
721
+ })
722
+ ? appendPredecessorHandoffCue(
723
+ annotated,
724
+ isRunProduced == null
725
+ ? undefined
726
+ : (message): boolean => isRunProduced.call(context, message)
727
+ )
728
+ : removePredecessorHandoffCue(annotated);
729
+ const messagesForProvider = strictAlternationProviders.has(provider)
730
+ ? coalesceAdjacentUserTurns(cued)
731
+ : cued;
213
732
 
214
733
  /**
215
734
  * Stamp the provider that is ACTUALLY serving this invocation onto the
@@ -229,8 +748,34 @@ export async function attemptInvoke(
229
748
  };
230
749
 
231
750
  if (model.stream) {
232
- const stream = await model.stream(messagesForProvider, config);
751
+ /**
752
+ * Observed, not dictated. `handleChatModelStart` fires with the chat
753
+ * model's real run id before the first chunk, which is the only way to
754
+ * name the run a seal has to close — pinning `config.runId` does not
755
+ * survive the bound runnable. Installed only when preemption is
756
+ * configured, so a run that cannot seal carries no extra handler.
757
+ */
758
+ let sealedRunId: string | undefined;
759
+ const streamConfig =
760
+ context?.preemption == null
761
+ ? config
762
+ : {
763
+ ...config,
764
+ callbacks: appendCallbacks(config.callbacks, [
765
+ {
766
+ handleChatModelStart: (
767
+ _llm: Serialized,
768
+ _messages: BaseMessage[][],
769
+ runId: string
770
+ ): void => {
771
+ sealedRunId ??= runId;
772
+ },
773
+ },
774
+ ]),
775
+ };
776
+ const stream = await model.stream(messagesForProvider, streamConfig);
233
777
  let finalChunk: AIMessageChunk | undefined;
778
+ let preempted = false;
234
779
  const registeredStreamHandler =
235
780
  getRegisteredDefaultChatStreamHandler(context);
236
781
 
@@ -265,6 +810,26 @@ export async function attemptInvoke(
265
810
  next: chunk,
266
811
  provider,
267
812
  });
813
+ /**
814
+ * Only this loop may seal. The registered-handler branch below
815
+ * dispatches through `run.ts`'s decoupled `streamEvents` consumer,
816
+ * which can lag the accumulated chunk — sealing there would let the
817
+ * host index a content part the user has not been shown yet.
818
+ */
819
+ /**
820
+ * Cheap poll first, shape check second, budget claim last. The claim
821
+ * is what makes this safe under a parallel `MultiAgentGraph`: several
822
+ * agents share one graph and can each see the poll as true, but only
823
+ * one can take the slot, and a chunk that cannot seal never spends it.
824
+ */
825
+ if (
826
+ context?.shouldPreemptStream() === true &&
827
+ canSealPreempt(finalChunk) &&
828
+ context.claimPreemptSeal()
829
+ ) {
830
+ preempted = true;
831
+ break;
832
+ }
268
833
  }
269
834
  } else {
270
835
  const metadata = config.metadata as Record<string, unknown> | undefined;
@@ -294,6 +859,21 @@ export async function attemptInvoke(
294
859
  finalChunk = modifyDeltaProperties(provider, finalChunk);
295
860
  }
296
861
 
862
+ if (preempted && finalChunk != null) {
863
+ finalChunk.response_metadata = {
864
+ ...finalChunk.response_metadata,
865
+ preempted: true,
866
+ };
867
+ await endSealedModelRun(
868
+ context,
869
+ finalChunk,
870
+ messagesForProvider,
871
+ sealedRunId,
872
+ config,
873
+ model
874
+ );
875
+ }
876
+
297
877
  if ((finalChunk?.tool_calls?.length ?? 0) > 0) {
298
878
  finalChunk!.tool_calls = finalChunk!.tool_calls?.filter(
299
879
  (tool_call: ToolCall) => !!tool_call.name
@@ -404,6 +984,7 @@ export async function tryFallbackProviders({
404
984
  context,
405
985
  onChunk,
406
986
  overflowContext,
987
+ prepareProviderMessages,
407
988
  }: {
408
989
  fallbacks: t.FallbackConfig[];
409
990
  tools?: t.GraphTools;
@@ -419,6 +1000,19 @@ export async function tryFallbackProviders({
419
1000
  * be dropped in favour of whichever failure came last.
420
1001
  */
421
1002
  overflowContext?: ContextOverflowContext;
1003
+ /**
1004
+ * Optional final payload guard used by Graph. It receives the initialized,
1005
+ * tool-bound fallback model so Responses-vs-Chat projection is exact before
1006
+ * the fallback request is measured and sent.
1007
+ */
1008
+ prepareProviderMessages?: (input: {
1009
+ model: t.ChatModel;
1010
+ messages: BaseMessage[];
1011
+ provider: Providers;
1012
+ clientOptions?: t.ClientOptions;
1013
+ maxContextTokens?: number;
1014
+ config?: RunnableConfig;
1015
+ }) => BaseMessage[] | Promise<BaseMessage[]>;
422
1016
  }): Promise<Partial<t.BaseGraphState> | undefined> {
423
1017
  const isOverflow = (
424
1018
  error: unknown,
@@ -460,10 +1054,19 @@ export async function tryFallbackProviders({
460
1054
  [Constants.INVOKED_MODEL]: fbModelName,
461
1055
  },
462
1056
  };
1057
+ const fallbackMessages =
1058
+ (await prepareProviderMessages?.({
1059
+ model: fbModel as t.ChatModel,
1060
+ messages,
1061
+ provider: fb.provider,
1062
+ clientOptions: fb.clientOptions,
1063
+ maxContextTokens: fb.maxContextTokens,
1064
+ config: fbConfig,
1065
+ })) ?? messages;
463
1066
  const result = await attemptInvoke(
464
1067
  {
465
1068
  model: fbModel as t.ChatModel,
466
- messages,
1069
+ messages: fallbackMessages,
467
1070
  provider: fb.provider,
468
1071
  context,
469
1072
  onChunk,