@librechat/agents 3.3.4 → 3.3.6

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 (263) hide show
  1. package/dist/cjs/common/constants.cjs +21 -0
  2. package/dist/cjs/common/constants.cjs.map +1 -1
  3. package/dist/cjs/events.cjs +10 -1
  4. package/dist/cjs/events.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +456 -7
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  9. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
  10. package/dist/cjs/hooks/index.cjs +12 -0
  11. package/dist/cjs/hooks/index.cjs.map +1 -1
  12. package/dist/cjs/hooks/types.cjs +1 -0
  13. package/dist/cjs/hooks/types.cjs.map +1 -1
  14. package/dist/cjs/langfuse.cjs +8 -0
  15. package/dist/cjs/langfuse.cjs.map +1 -1
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +1 -3
  17. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  18. package/dist/cjs/llm/invoke.cjs +268 -3
  19. package/dist/cjs/llm/invoke.cjs.map +1 -1
  20. package/dist/cjs/llm/openai/index.cjs +51 -6
  21. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  22. package/dist/cjs/llm/preempt.cjs +132 -0
  23. package/dist/cjs/llm/preempt.cjs.map +1 -0
  24. package/dist/cjs/main.cjs +31 -3
  25. package/dist/cjs/messages/alternation.cjs +92 -0
  26. package/dist/cjs/messages/alternation.cjs.map +1 -0
  27. package/dist/cjs/messages/format.cjs +72 -0
  28. package/dist/cjs/messages/format.cjs.map +1 -1
  29. package/dist/cjs/messages/handoffCue.cjs +63 -0
  30. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  31. package/dist/cjs/messages/index.cjs +3 -0
  32. package/dist/cjs/messages/injected.cjs +60 -0
  33. package/dist/cjs/messages/injected.cjs.map +1 -0
  34. package/dist/cjs/run.cjs +80 -7
  35. package/dist/cjs/run.cjs.map +1 -1
  36. package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
  37. package/dist/cjs/session/handlers.cjs +18 -9
  38. package/dist/cjs/session/handlers.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +34 -8
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/tools/BashExecutor.cjs +3 -1
  42. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  43. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -0
  44. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  45. package/dist/cjs/tools/CodeExecutor.cjs +19 -16
  46. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  47. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +2 -0
  48. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  49. package/dist/cjs/tools/ReadFile.cjs +8 -4
  50. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  51. package/dist/cjs/tools/SkillTool.cjs +2 -0
  52. package/dist/cjs/tools/SkillTool.cjs.map +1 -1
  53. package/dist/cjs/tools/SubagentTool.cjs +3 -0
  54. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  55. package/dist/cjs/tools/ToolNode.cjs +91 -50
  56. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  57. package/dist/cjs/tools/ToolSearch.cjs +5 -1
  58. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  59. package/dist/cjs/tools/intentArg.cjs +230 -0
  60. package/dist/cjs/tools/intentArg.cjs.map +1 -0
  61. package/dist/cjs/tools/local/CompileCheckTool.cjs +3 -2
  62. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
  63. package/dist/cjs/tools/local/LocalCodingTools.cjs +13 -12
  64. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  65. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +2 -2
  66. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  67. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  68. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  69. package/dist/cjs/tools/search/schema.cjs +2 -0
  70. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  71. package/dist/cjs/tools/search/tool.cjs +40 -2
  72. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  73. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  74. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  75. package/dist/cjs/tools/toolOutputReferences.cjs +41 -7
  76. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  77. package/dist/esm/common/constants.mjs +19 -1
  78. package/dist/esm/common/constants.mjs.map +1 -1
  79. package/dist/esm/events.mjs +10 -1
  80. package/dist/esm/events.mjs.map +1 -1
  81. package/dist/esm/graphs/Graph.mjs +455 -6
  82. package/dist/esm/graphs/Graph.mjs.map +1 -1
  83. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  84. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  85. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  86. package/dist/esm/hooks/index.mjs +12 -1
  87. package/dist/esm/hooks/index.mjs.map +1 -1
  88. package/dist/esm/hooks/types.mjs +1 -0
  89. package/dist/esm/hooks/types.mjs.map +1 -1
  90. package/dist/esm/langfuse.mjs +9 -1
  91. package/dist/esm/langfuse.mjs.map +1 -1
  92. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +1 -3
  93. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  94. package/dist/esm/llm/invoke.mjs +270 -6
  95. package/dist/esm/llm/invoke.mjs.map +1 -1
  96. package/dist/esm/llm/openai/index.mjs +51 -6
  97. package/dist/esm/llm/openai/index.mjs.map +1 -1
  98. package/dist/esm/llm/preempt.mjs +131 -0
  99. package/dist/esm/llm/preempt.mjs.map +1 -0
  100. package/dist/esm/main.mjs +11 -7
  101. package/dist/esm/messages/alternation.mjs +91 -0
  102. package/dist/esm/messages/alternation.mjs.map +1 -0
  103. package/dist/esm/messages/format.mjs +72 -0
  104. package/dist/esm/messages/format.mjs.map +1 -1
  105. package/dist/esm/messages/handoffCue.mjs +61 -0
  106. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  107. package/dist/esm/messages/index.mjs +3 -0
  108. package/dist/esm/messages/injected.mjs +60 -0
  109. package/dist/esm/messages/injected.mjs.map +1 -0
  110. package/dist/esm/run.mjs +80 -7
  111. package/dist/esm/run.mjs.map +1 -1
  112. package/dist/esm/session/JsonlSessionStore.mjs +1 -1
  113. package/dist/esm/session/handlers.mjs +19 -10
  114. package/dist/esm/session/handlers.mjs.map +1 -1
  115. package/dist/esm/stream.mjs +33 -9
  116. package/dist/esm/stream.mjs.map +1 -1
  117. package/dist/esm/tools/BashExecutor.mjs +3 -1
  118. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  119. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -0
  120. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  121. package/dist/esm/tools/CodeExecutor.mjs +19 -16
  122. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  123. package/dist/esm/tools/ProgrammaticToolCalling.mjs +2 -0
  124. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  125. package/dist/esm/tools/ReadFile.mjs +8 -4
  126. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  127. package/dist/esm/tools/SkillTool.mjs +2 -0
  128. package/dist/esm/tools/SkillTool.mjs.map +1 -1
  129. package/dist/esm/tools/SubagentTool.mjs +3 -0
  130. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  131. package/dist/esm/tools/ToolNode.mjs +91 -51
  132. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  133. package/dist/esm/tools/ToolSearch.mjs +5 -1
  134. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  135. package/dist/esm/tools/intentArg.mjs +220 -0
  136. package/dist/esm/tools/intentArg.mjs.map +1 -0
  137. package/dist/esm/tools/local/CompileCheckTool.mjs +3 -2
  138. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
  139. package/dist/esm/tools/local/LocalCodingTools.mjs +13 -12
  140. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  141. package/dist/esm/tools/local/LocalExecutionEngine.mjs +2 -2
  142. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  143. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  144. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  145. package/dist/esm/tools/search/schema.mjs +2 -0
  146. package/dist/esm/tools/search/schema.mjs.map +1 -1
  147. package/dist/esm/tools/search/tool.mjs +40 -3
  148. package/dist/esm/tools/search/tool.mjs.map +1 -1
  149. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  150. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  151. package/dist/esm/tools/toolOutputReferences.mjs +41 -7
  152. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  153. package/dist/types/common/constants.d.ts +18 -0
  154. package/dist/types/graphs/Graph.d.ts +138 -1
  155. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  156. package/dist/types/hooks/index.d.ts +12 -1
  157. package/dist/types/hooks/types.d.ts +45 -6
  158. package/dist/types/index.d.ts +2 -1
  159. package/dist/types/llm/invoke.d.ts +7 -0
  160. package/dist/types/llm/openai/index.d.ts +0 -11
  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/handoffCue.d.ts +40 -0
  164. package/dist/types/messages/index.d.ts +3 -0
  165. package/dist/types/messages/injected.d.ts +3 -0
  166. package/dist/types/run.d.ts +7 -0
  167. package/dist/types/stream.d.ts +14 -0
  168. package/dist/types/tools/BashExecutor.d.ts +18 -0
  169. package/dist/types/tools/CodeExecutor.d.ts +18 -0
  170. package/dist/types/tools/ReadFile.d.ts +18 -0
  171. package/dist/types/tools/SkillTool.d.ts +18 -0
  172. package/dist/types/tools/SubagentTool.d.ts +9 -0
  173. package/dist/types/tools/ToolNode.d.ts +37 -12
  174. package/dist/types/tools/ToolSearch.d.ts +18 -0
  175. package/dist/types/tools/intentArg.d.ts +119 -0
  176. package/dist/types/tools/ptcTimeout.d.ts +2 -0
  177. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  178. package/dist/types/tools/search/schema.d.ts +18 -0
  179. package/dist/types/tools/search/tool.d.ts +18 -0
  180. package/dist/types/tools/search/types.d.ts +31 -2
  181. package/dist/types/tools/toolOutputReferences.d.ts +12 -2
  182. package/dist/types/types/graph.d.ts +8 -1
  183. package/dist/types/types/run.d.ts +65 -0
  184. package/dist/types/types/stream.d.ts +12 -25
  185. package/dist/types/types/tools.d.ts +27 -2
  186. package/package.json +1 -1
  187. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  188. package/src/common/constants.ts +21 -0
  189. package/src/events.ts +15 -1
  190. package/src/graphs/Graph.ts +568 -3
  191. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +18 -7
  192. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  193. package/src/hooks/HookRegistry.ts +40 -0
  194. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  195. package/src/hooks/index.ts +16 -2
  196. package/src/hooks/types.ts +47 -3
  197. package/src/index.ts +2 -1
  198. package/src/langfuse.ts +26 -1
  199. package/src/llm/bedrock/utils/message_inputs.test.ts +82 -0
  200. package/src/llm/bedrock/utils/message_inputs.ts +14 -11
  201. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  202. package/src/llm/invoke.alternation.test.ts +87 -0
  203. package/src/llm/invoke.handoffCue.test.ts +112 -0
  204. package/src/llm/invoke.ts +417 -8
  205. package/src/llm/openai/index.ts +74 -10
  206. package/src/llm/openai/llm.spec.ts +105 -0
  207. package/src/llm/preempt.test.ts +323 -0
  208. package/src/llm/preempt.ts +178 -0
  209. package/src/messages/alternation.test.ts +212 -0
  210. package/src/messages/alternation.ts +112 -0
  211. package/src/messages/format.ts +91 -0
  212. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  213. package/src/messages/handoffCue.test.ts +96 -0
  214. package/src/messages/handoffCue.ts +78 -0
  215. package/src/messages/index.ts +3 -0
  216. package/src/messages/injected.test.ts +90 -0
  217. package/src/messages/injected.ts +74 -0
  218. package/src/run.ts +91 -6
  219. package/src/scripts/preempt-probe.ts +330 -0
  220. package/src/scripts/preempt-scenarios.ts +388 -0
  221. package/src/session/handlers.ts +32 -12
  222. package/src/specs/handoffCue.test.ts +165 -0
  223. package/src/specs/langfuse-callbacks.test.ts +352 -2
  224. package/src/specs/preemptSeal.test.ts +309 -0
  225. package/src/stream.dispatch.test.ts +63 -0
  226. package/src/stream.ts +48 -9
  227. package/src/tools/BashExecutor.ts +6 -0
  228. package/src/tools/BashProgrammaticToolCalling.ts +2 -0
  229. package/src/tools/CodeExecutor.ts +6 -0
  230. package/src/tools/ProgrammaticToolCalling.ts +2 -0
  231. package/src/tools/ReadFile.ts +2 -0
  232. package/src/tools/SkillTool.ts +2 -0
  233. package/src/tools/SubagentTool.ts +3 -0
  234. package/src/tools/ToolNode.ts +193 -74
  235. package/src/tools/ToolSearch.ts +3 -0
  236. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +174 -0
  237. package/src/tools/__tests__/intentArg.test.ts +281 -0
  238. package/src/tools/__tests__/intentCoverage.test.ts +139 -0
  239. package/src/tools/__tests__/toolOutputReferences.test.ts +83 -0
  240. package/src/tools/intentArg.ts +323 -0
  241. package/src/tools/local/CompileCheckTool.ts +4 -3
  242. package/src/tools/local/LocalCodingTools.ts +14 -13
  243. package/src/tools/ptcTimeout.ts +2 -0
  244. package/src/tools/search/keenable-scraper.test.ts +153 -0
  245. package/src/tools/search/keenable-scraper.ts +137 -0
  246. package/src/tools/search/outcome.test.ts +90 -0
  247. package/src/tools/search/schema.ts +3 -0
  248. package/src/tools/search/tool.ts +60 -3
  249. package/src/tools/search/types.ts +50 -3
  250. package/src/tools/subagent/SubagentExecutor.ts +2 -1
  251. package/src/tools/toolOutputReferences.ts +77 -9
  252. package/src/types/graph.ts +12 -1
  253. package/src/types/run.ts +67 -0
  254. package/src/types/stream.ts +12 -41
  255. package/src/types/tools.ts +24 -2
  256. package/dist/cjs/splitStream.cjs +0 -151
  257. package/dist/cjs/splitStream.cjs.map +0 -1
  258. package/dist/esm/splitStream.mjs +0 -150
  259. package/dist/esm/splitStream.mjs.map +0 -1
  260. package/dist/types/mockStream.d.ts +0 -32
  261. package/dist/types/splitStream.d.ts +0 -37
  262. package/src/mockStream.ts +0 -99
  263. package/src/splitStream.ts +0 -234
package/src/llm/invoke.ts CHANGED
@@ -1,5 +1,13 @@
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';
@@ -22,13 +30,22 @@ import {
22
30
  } from '@/messages/cache';
23
31
  import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
24
32
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
25
- import { Constants, GraphEvents, Providers } from '@/common';
33
+ import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
26
34
  import { manualToolStreamProviders } from '@/llm/providers';
35
+ import { appendCallbacks } from '@/utils/callbacks';
36
+ import { safeDispatchCustomEvent } from '@/utils/events';
27
37
  import { getContextOverflowInfo } from '@/utils/errors';
28
- import { modifyDeltaProperties } from '@/messages';
29
- 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';
30
47
  import { initializeModel } from '@/llm/init';
31
- import { isOpenAILike } from '@/utils/llm';
48
+ import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
32
49
 
33
50
  /**
34
51
  * Context passed to `attemptInvoke`. Matches the subset of Graph that
@@ -215,13 +232,23 @@ export function projectMessagesForProvider({
215
232
  );
216
233
  }
217
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
+ */
218
245
  function getRegisteredDefaultChatStreamHandler(
219
246
  context?: InvokeContext
220
- ): ChatModelStreamHandler | undefined {
247
+ ): t.EventHandler | undefined {
221
248
  const handler = context?.handlerRegistry?.getHandler(
222
249
  GraphEvents.CHAT_MODEL_STREAM
223
250
  );
224
- return handler instanceof ChatModelStreamHandler ? handler : undefined;
251
+ return dispatchesChatModelStream(handler) ? handler : undefined;
225
252
  }
226
253
 
227
254
  function hasReasoningDetails(chunk: AIMessageChunk): boolean {
@@ -318,6 +345,280 @@ function getStreamHandlingChunk({
318
345
  );
319
346
  }
320
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
+
321
622
  function appendStreamChunk({
322
623
  current,
323
624
  next,
@@ -376,11 +677,58 @@ export async function attemptInvoke(
376
677
  });
377
678
  const registry = context?.getOrCreateToolOutputRegistry();
378
679
  const runId = config?.configurable?.run_id as string | undefined;
379
- const messagesForProvider = annotateMessagesForLLM(
680
+ const annotated = annotateMessagesForLLM(
380
681
  invocationMessages,
381
682
  registry,
382
683
  runId
383
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;
384
732
 
385
733
  /**
386
734
  * Stamp the provider that is ACTUALLY serving this invocation onto the
@@ -400,8 +748,34 @@ export async function attemptInvoke(
400
748
  };
401
749
 
402
750
  if (model.stream) {
403
- 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);
404
777
  let finalChunk: AIMessageChunk | undefined;
778
+ let preempted = false;
405
779
  const registeredStreamHandler =
406
780
  getRegisteredDefaultChatStreamHandler(context);
407
781
 
@@ -436,6 +810,26 @@ export async function attemptInvoke(
436
810
  next: chunk,
437
811
  provider,
438
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
+ }
439
833
  }
440
834
  } else {
441
835
  const metadata = config.metadata as Record<string, unknown> | undefined;
@@ -465,6 +859,21 @@ export async function attemptInvoke(
465
859
  finalChunk = modifyDeltaProperties(provider, finalChunk);
466
860
  }
467
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
+
468
877
  if ((finalChunk?.tool_calls?.length ?? 0) > 0) {
469
878
  finalChunk!.tool_calls = finalChunk!.tool_calls?.filter(
470
879
  (tool_call: ToolCall) => !!tool_call.name
@@ -51,6 +51,7 @@ import {
51
51
  projectOpenAIResponsesToolMessageContent,
52
52
  projectToolStreamContentForProvider,
53
53
  } from '@/messages/core';
54
+ import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
54
55
  import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
55
56
  import { dropRepeatedScalarMetadata } from './streamMetadata';
56
57
 
@@ -1003,6 +1004,65 @@ function createAbortHandler(controller: AbortController): () => void {
1003
1004
  * @param {Object} [fields] Additional fields to add to the OpenAI tool.
1004
1005
  * @returns {ToolDefinition} The inputted tool in OpenAI tool format.
1005
1006
  */
1007
+ /**
1008
+ * OpenAI strict function schemas require every property to appear in
1009
+ * `required`. The optional `intent` label (see `tools/intentArg.ts`) is
1010
+ * deliberately NOT required — the same schema is callable from programmatic
1011
+ * tool calling — so a tool auto-marked `strict: true` (the non-streaming
1012
+ * `json_schema` structured-output path) would be rejected as invalid before
1013
+ * execution. That path never streams a live label anyway, so the
1014
+ * marker-identified property is dropped there; every other path keeps it.
1015
+ */
1016
+ function stripIntentFromStrictTools<T extends object>(params: T): T {
1017
+ const record = params as { tools?: unknown[] };
1018
+ const tools = record.tools;
1019
+ if (!Array.isArray(tools) || tools.length === 0) {
1020
+ return params;
1021
+ }
1022
+ const nextTools = tools.map((tool) => {
1023
+ const candidate = tool as {
1024
+ strict?: boolean;
1025
+ parameters?: { properties?: Record<string, unknown>; required?: unknown };
1026
+ function?: {
1027
+ strict?: boolean;
1028
+ parameters?: {
1029
+ properties?: Record<string, unknown>;
1030
+ required?: unknown;
1031
+ };
1032
+ };
1033
+ };
1034
+ /** Chat-completions tools nest under `function`; responses-API tools are flat. */
1035
+ const holder = candidate.function ?? candidate;
1036
+ if (holder.strict !== true) {
1037
+ return tool;
1038
+ }
1039
+ const parameters = holder.parameters;
1040
+ const properties = parameters?.properties;
1041
+ if (properties == null || !isIntentLabelProperty(properties[INTENT_ARG])) {
1042
+ return tool;
1043
+ }
1044
+ const required = Array.isArray(parameters?.required)
1045
+ ? (parameters.required as unknown[])
1046
+ : [];
1047
+ if (required.includes(INTENT_ARG)) {
1048
+ return tool;
1049
+ }
1050
+ const { [INTENT_ARG]: _omit, ...restProps } = properties;
1051
+ const nextParams = { ...parameters, properties: restProps };
1052
+ if (candidate.function != null) {
1053
+ return {
1054
+ ...candidate,
1055
+ function: { ...candidate.function, parameters: nextParams },
1056
+ };
1057
+ }
1058
+ return { ...candidate, parameters: nextParams };
1059
+ });
1060
+ if (nextTools.every((tool, index) => tool === tools[index])) {
1061
+ return params;
1062
+ }
1063
+ return { ...params, tools: nextTools } as T;
1064
+ }
1065
+
1006
1066
  export function _convertToOpenAITool(
1007
1067
  tool: BindToolsInput,
1008
1068
  fields?: {
@@ -1184,10 +1244,12 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
1184
1244
  options?: this['ParsedCallOptions'],
1185
1245
  extra?: { streaming?: boolean }
1186
1246
  ): ReturnType<OriginalChatOpenAICompletions['invocationParams']> {
1187
- return applyManagedRequestParams(super.invocationParams(options, extra), {
1188
- promptCacheExplicit: this.promptCacheExplicit,
1189
- safetyIdentifier: this.safetyIdentifier,
1190
- });
1247
+ return stripIntentFromStrictTools(
1248
+ applyManagedRequestParams(super.invocationParams(options, extra), {
1249
+ promptCacheExplicit: this.promptCacheExplicit,
1250
+ safetyIdentifier: this.safetyIdentifier,
1251
+ })
1252
+ );
1191
1253
  }
1192
1254
 
1193
1255
  protected _getReasoningParams(
@@ -1640,7 +1702,7 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
1640
1702
  ]),
1641
1703
  ];
1642
1704
  }
1643
- return params;
1705
+ return stripIntentFromStrictTools(params);
1644
1706
  }
1645
1707
 
1646
1708
  async completionWithRetry(
@@ -1741,10 +1803,12 @@ class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions
1741
1803
  options?: this['ParsedCallOptions'],
1742
1804
  extra?: { streaming?: boolean }
1743
1805
  ): ReturnType<OriginalAzureChatOpenAICompletions['invocationParams']> {
1744
- return applyManagedRequestParams(super.invocationParams(options, extra), {
1745
- promptCacheExplicit: this.promptCacheExplicit,
1746
- safetyIdentifier: this.safetyIdentifier,
1747
- });
1806
+ return stripIntentFromStrictTools(
1807
+ applyManagedRequestParams(super.invocationParams(options, extra), {
1808
+ promptCacheExplicit: this.promptCacheExplicit,
1809
+ safetyIdentifier: this.safetyIdentifier,
1810
+ })
1811
+ );
1748
1812
  }
1749
1813
 
1750
1814
  protected _getReasoningParams(
@@ -1888,7 +1952,7 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
1888
1952
  ]),
1889
1953
  ];
1890
1954
  }
1891
- return params;
1955
+ return stripIntentFromStrictTools(params);
1892
1956
  }
1893
1957
 
1894
1958
  async completionWithRetry(