@librechat/agents 3.7.22 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +13 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +174 -53
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +15 -5
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +48 -2
  8. package/dist/cjs/langfuse.cjs.map +1 -1
  9. package/dist/cjs/langfuseToolOutputTracing.cjs +42 -0
  10. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  11. package/dist/cjs/langfuseTraceShaping.cjs +7 -1
  12. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  13. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +5 -9
  14. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  15. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +2 -7
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  17. package/dist/cjs/llm/contextPressureMeter.cjs +51 -10
  18. package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -1
  19. package/dist/cjs/llm/init.cjs +1 -1
  20. package/dist/cjs/llm/invoke.cjs +2 -2
  21. package/dist/cjs/llm/openai/index.cjs +4 -3
  22. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  23. package/dist/cjs/llm/openai/utils/index.cjs +5 -4
  24. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  25. package/dist/cjs/llm/preempt.cjs +3 -2
  26. package/dist/cjs/llm/preempt.cjs.map +1 -1
  27. package/dist/cjs/llm/prepareProviderRequest.cjs +9 -7
  28. package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -1
  29. package/dist/cjs/llm/providers.cjs +1 -1
  30. package/dist/cjs/main.cjs +11 -5
  31. package/dist/cjs/messages/alternation.cjs +1 -5
  32. package/dist/cjs/messages/alternation.cjs.map +1 -1
  33. package/dist/cjs/messages/budget.cjs +206 -7
  34. package/dist/cjs/messages/budget.cjs.map +1 -1
  35. package/dist/cjs/messages/cache.cjs +3 -9
  36. package/dist/cjs/messages/cache.cjs.map +1 -1
  37. package/dist/cjs/messages/core.cjs +25 -37
  38. package/dist/cjs/messages/core.cjs.map +1 -1
  39. package/dist/cjs/messages/format.cjs +131 -42
  40. package/dist/cjs/messages/format.cjs.map +1 -1
  41. package/dist/cjs/messages/index.cjs +2 -0
  42. package/dist/cjs/messages/prune.cjs +12 -7
  43. package/dist/cjs/messages/prune.cjs.map +1 -1
  44. package/dist/cjs/messages/reasoningTypes.cjs +21 -0
  45. package/dist/cjs/messages/reasoningTypes.cjs.map +1 -0
  46. package/dist/cjs/messages/recency.cjs +15 -94
  47. package/dist/cjs/messages/recency.cjs.map +1 -1
  48. package/dist/cjs/messages/toolHistoryProjection.cjs +243 -0
  49. package/dist/cjs/messages/toolHistoryProjection.cjs.map +1 -0
  50. package/dist/cjs/messages/toolResultTypes.cjs +145 -4
  51. package/dist/cjs/messages/toolResultTypes.cjs.map +1 -1
  52. package/dist/cjs/run.cjs +5 -4
  53. package/dist/cjs/run.cjs.map +1 -1
  54. package/dist/cjs/session/AgentSession.cjs +6 -4
  55. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  56. package/dist/cjs/session/JsonlSessionStore.cjs +3 -0
  57. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  58. package/dist/cjs/session/index.cjs +1 -1
  59. package/dist/cjs/session/sessionProjection.cjs +75 -0
  60. package/dist/cjs/session/sessionProjection.cjs.map +1 -0
  61. package/dist/cjs/stream.cjs +18 -17
  62. package/dist/cjs/stream.cjs.map +1 -1
  63. package/dist/cjs/summarization/index.cjs.map +1 -1
  64. package/dist/cjs/summarization/node.cjs +18 -8
  65. package/dist/cjs/summarization/node.cjs.map +1 -1
  66. package/dist/cjs/summarization/shared.cjs +9 -0
  67. package/dist/cjs/summarization/shared.cjs.map +1 -1
  68. package/dist/cjs/tools/ArtifactDelivery.cjs +27 -0
  69. package/dist/cjs/tools/ArtifactDelivery.cjs.map +1 -0
  70. package/dist/cjs/tools/BashExecutor.cjs +6 -1
  71. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  72. package/dist/cjs/tools/CodeExecutor.cjs +6 -1
  73. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  74. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -1
  75. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  76. package/dist/cjs/tools/local/CompileCheckTool.cjs +1 -1
  77. package/dist/cjs/tools/local/LocalCodingTools.cjs +1 -1
  78. package/dist/cjs/utils/events.cjs +13 -0
  79. package/dist/cjs/utils/events.cjs.map +1 -1
  80. package/dist/cjs/utils/tokens.cjs +105 -0
  81. package/dist/cjs/utils/tokens.cjs.map +1 -1
  82. package/dist/esm/agents/AgentContext.mjs +13 -3
  83. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  84. package/dist/esm/graphs/Graph.mjs +176 -55
  85. package/dist/esm/graphs/Graph.mjs.map +1 -1
  86. package/dist/esm/graphs/MultiAgentGraph.mjs +15 -5
  87. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  88. package/dist/esm/langfuse.mjs +49 -4
  89. package/dist/esm/langfuse.mjs.map +1 -1
  90. package/dist/esm/langfuseToolOutputTracing.mjs +41 -1
  91. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  92. package/dist/esm/langfuseTraceShaping.mjs +7 -1
  93. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  94. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +5 -9
  95. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +2 -7
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/contextPressureMeter.mjs +52 -11
  99. package/dist/esm/llm/contextPressureMeter.mjs.map +1 -1
  100. package/dist/esm/llm/init.mjs +1 -1
  101. package/dist/esm/llm/invoke.mjs +2 -2
  102. package/dist/esm/llm/openai/index.mjs +2 -1
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +5 -4
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/preempt.mjs +2 -1
  107. package/dist/esm/llm/preempt.mjs.map +1 -1
  108. package/dist/esm/llm/prepareProviderRequest.mjs +9 -7
  109. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -1
  110. package/dist/esm/llm/providers.mjs +1 -1
  111. package/dist/esm/main.mjs +10 -7
  112. package/dist/esm/messages/alternation.mjs +2 -6
  113. package/dist/esm/messages/alternation.mjs.map +1 -1
  114. package/dist/esm/messages/budget.mjs +206 -8
  115. package/dist/esm/messages/budget.mjs.map +1 -1
  116. package/dist/esm/messages/cache.mjs +3 -9
  117. package/dist/esm/messages/cache.mjs.map +1 -1
  118. package/dist/esm/messages/core.mjs +23 -33
  119. package/dist/esm/messages/core.mjs.map +1 -1
  120. package/dist/esm/messages/format.mjs +132 -43
  121. package/dist/esm/messages/format.mjs.map +1 -1
  122. package/dist/esm/messages/index.mjs +2 -0
  123. package/dist/esm/messages/prune.mjs +12 -7
  124. package/dist/esm/messages/prune.mjs.map +1 -1
  125. package/dist/esm/messages/reasoningTypes.mjs +20 -0
  126. package/dist/esm/messages/reasoningTypes.mjs.map +1 -0
  127. package/dist/esm/messages/recency.mjs +16 -95
  128. package/dist/esm/messages/recency.mjs.map +1 -1
  129. package/dist/esm/messages/toolHistoryProjection.mjs +237 -0
  130. package/dist/esm/messages/toolHistoryProjection.mjs.map +1 -0
  131. package/dist/esm/messages/toolResultTypes.mjs +141 -4
  132. package/dist/esm/messages/toolResultTypes.mjs.map +1 -1
  133. package/dist/esm/run.mjs +5 -4
  134. package/dist/esm/run.mjs.map +1 -1
  135. package/dist/esm/session/AgentSession.mjs +6 -4
  136. package/dist/esm/session/AgentSession.mjs.map +1 -1
  137. package/dist/esm/session/JsonlSessionStore.mjs +3 -0
  138. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  139. package/dist/esm/session/index.mjs +1 -1
  140. package/dist/esm/session/sessionProjection.mjs +72 -0
  141. package/dist/esm/session/sessionProjection.mjs.map +1 -0
  142. package/dist/esm/stream.mjs +15 -14
  143. package/dist/esm/stream.mjs.map +1 -1
  144. package/dist/esm/summarization/index.mjs.map +1 -1
  145. package/dist/esm/summarization/node.mjs +19 -9
  146. package/dist/esm/summarization/node.mjs.map +1 -1
  147. package/dist/esm/summarization/shared.mjs +9 -1
  148. package/dist/esm/summarization/shared.mjs.map +1 -1
  149. package/dist/esm/tools/ArtifactDelivery.mjs +26 -0
  150. package/dist/esm/tools/ArtifactDelivery.mjs.map +1 -0
  151. package/dist/esm/tools/BashExecutor.mjs +6 -1
  152. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  153. package/dist/esm/tools/CodeExecutor.mjs +6 -1
  154. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  155. package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -1
  156. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  157. package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
  158. package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
  159. package/dist/esm/utils/events.mjs +13 -0
  160. package/dist/esm/utils/events.mjs.map +1 -1
  161. package/dist/esm/utils/tokens.mjs +105 -1
  162. package/dist/esm/utils/tokens.mjs.map +1 -1
  163. package/dist/types/agents/AgentContext.d.ts +13 -1
  164. package/dist/types/graphs/Graph.d.ts +27 -0
  165. package/dist/types/hooks/types.d.ts +4 -2
  166. package/dist/types/index.d.ts +1 -0
  167. package/dist/types/langfuse.d.ts +10 -0
  168. package/dist/types/langfuseToolOutputTracing.d.ts +2 -0
  169. package/dist/types/llm/contextPressureMeter.d.ts +2 -1
  170. package/dist/types/llm/prepareProviderRequest.d.ts +7 -1
  171. package/dist/types/messages/budget.d.ts +15 -10
  172. package/dist/types/messages/core.d.ts +8 -17
  173. package/dist/types/messages/format.d.ts +3 -2
  174. package/dist/types/messages/index.d.ts +1 -1
  175. package/dist/types/messages/prune.d.ts +1 -1
  176. package/dist/types/messages/reasoningTypes.d.ts +7 -0
  177. package/dist/types/messages/recency.d.ts +3 -0
  178. package/dist/types/messages/toolHistoryProjection.d.ts +65 -0
  179. package/dist/types/messages/toolResultTypes.d.ts +31 -1
  180. package/dist/types/session/sessionProjection.d.ts +10 -0
  181. package/dist/types/summarization/index.d.ts +2 -1
  182. package/dist/types/summarization/node.d.ts +1 -0
  183. package/dist/types/summarization/shared.d.ts +12 -0
  184. package/dist/types/tools/ArtifactDelivery.d.ts +4 -0
  185. package/dist/types/types/graph.d.ts +28 -0
  186. package/dist/types/types/run.d.ts +4 -0
  187. package/dist/types/types/summarize.d.ts +4 -1
  188. package/dist/types/types/tools.d.ts +11 -0
  189. package/dist/types/utils/tokens.d.ts +2 -1
  190. package/package.json +1 -1
  191. package/src/agents/AgentContext.ts +34 -3
  192. package/src/graphs/Graph.ts +465 -147
  193. package/src/graphs/MultiAgentGraph.ts +28 -6
  194. package/src/hooks/types.ts +4 -1
  195. package/src/index.ts +1 -0
  196. package/src/langfuse.ts +84 -11
  197. package/src/langfuseToolOutputTracing.ts +91 -0
  198. package/src/langfuseTraceShaping.ts +17 -4
  199. package/src/llm/anthropic/utils/message_inputs.ts +5 -10
  200. package/src/llm/bedrock/utils/message_inputs.ts +2 -8
  201. package/src/llm/contextPressureMeter.ts +80 -12
  202. package/src/llm/openai/utils/index.ts +5 -4
  203. package/src/llm/prepareProviderRequest.ts +21 -16
  204. package/src/messages/alternation.ts +2 -15
  205. package/src/messages/budget.ts +439 -23
  206. package/src/messages/cache.ts +8 -9
  207. package/src/messages/core.ts +65 -95
  208. package/src/messages/format.ts +262 -78
  209. package/src/messages/index.ts +1 -1
  210. package/src/messages/prune.ts +31 -12
  211. package/src/messages/reasoningTypes.ts +23 -0
  212. package/src/messages/recency.ts +35 -179
  213. package/src/messages/toolHistoryProjection.ts +460 -0
  214. package/src/messages/toolResultTypes.ts +331 -100
  215. package/src/run.ts +10 -2
  216. package/src/session/AgentSession.ts +33 -16
  217. package/src/session/JsonlSessionStore.ts +6 -0
  218. package/src/session/sessionProjection.ts +126 -0
  219. package/src/stream.ts +14 -19
  220. package/src/summarization/index.ts +2 -0
  221. package/src/summarization/node.ts +62 -13
  222. package/src/summarization/shared.ts +23 -0
  223. package/src/tools/ArtifactDelivery.ts +50 -0
  224. package/src/tools/BashExecutor.ts +18 -1
  225. package/src/tools/CodeExecutor.ts +18 -1
  226. package/src/tools/ProgrammaticToolCalling.ts +15 -1
  227. package/src/types/graph.ts +28 -0
  228. package/src/types/run.ts +4 -0
  229. package/src/types/summarize.ts +4 -1
  230. package/src/types/tools.ts +12 -0
  231. package/src/utils/events.ts +19 -0
  232. package/src/utils/tokens.ts +183 -0
@@ -56,6 +56,7 @@ import {
56
56
  serializeToolCallInput,
57
57
  } from '@/messages/prune';
58
58
  import { toLangChainContent } from '@/messages/langchain';
59
+ import { isAnthropicThinkingContentBlock } from '@/messages/reasoningTypes';
59
60
 
60
61
  export type { OpenAICallOptions, OpenAIChatInput };
61
62
 
@@ -361,7 +362,7 @@ export function _convertMessagesToOpenAIParams(
361
362
  role = 'developer';
362
363
  }
363
364
 
364
- let hasAnthropicThinkingBlock: boolean = false;
365
+ let hasReasoningBlock = false;
365
366
 
366
367
  let content: unknown;
367
368
  if (
@@ -384,8 +385,8 @@ export function _convertMessagesToOpenAIParams(
384
385
  content = message.content;
385
386
  } else {
386
387
  content = message.content.map((m) => {
387
- if ('type' in m && m.type === 'thinking') {
388
- hasAnthropicThinkingBlock = true;
388
+ if (isAnthropicThinkingContentBlock(m)) {
389
+ hasReasoningBlock = true;
389
390
  return m;
390
391
  }
391
392
  if (isDataContentBlock(m)) {
@@ -416,7 +417,7 @@ export function _convertMessagesToOpenAIParams(
416
417
  completionParam.tool_calls = message.tool_calls.map(
417
418
  convertLangChainToolCallToBoundedOpenAI
418
419
  );
419
- completionParam.content = hasAnthropicThinkingBlock ? content : '';
420
+ completionParam.content = hasReasoningBlock ? content : '';
420
421
  if (
421
422
  options?.includeReasoningDetails === true &&
422
423
  message.additional_kwargs.reasoning_details != null
@@ -7,6 +7,7 @@ import type {
7
7
  import type { RunnableConfig } from '@langchain/core/runnables';
8
8
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
9
9
  import type * as t from '@/types';
10
+ import type { ToolHistoryPreparation } from '@/messages/toolHistoryProjection';
10
11
  import {
11
12
  projectCacheControlledToolOutputsToText,
12
13
  projectComputerCallOutputsToText,
@@ -27,15 +28,12 @@ import {
27
28
  stripBedrockCacheControl,
28
29
  cloneMessage,
29
30
  } from '@/messages/cache';
30
- import {
31
- isAnthropicLike,
32
- isGoogleLike,
33
- isOpenAILike,
34
- } from '@/utils/llm';
31
+ import { isAnthropicLike, isGoogleLike, isOpenAILike } from '@/utils/llm';
35
32
  import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
36
33
  import { providerRequiresStrictAlternation } from '@/llm/providers';
37
34
  import { getProviderFamily } from '@/llm/providerRegistry';
38
35
  import { Providers } from '@/common';
36
+ import { createToolHistoryPreparation } from '@/messages/toolHistoryProjection';
39
37
 
40
38
  const preparedProviderRequestBrand = Symbol('PreparedProviderRequest');
41
39
  const OMITTED_ATTACHMENT_TEXT =
@@ -90,6 +88,9 @@ export interface ProviderPayloadMeasurement {
90
88
  readonly availableMessageTokens?: number;
91
89
  readonly contextBudget?: number;
92
90
  readonly effectiveInstructionTokens?: number;
91
+ readonly toolMessageTokens?: number;
92
+ readonly toolMessageTokenCounts?: Record<string, number>;
93
+ readonly toolMessageUsageError?: Error;
93
94
  }
94
95
 
95
96
  export interface PreparedProviderRequest {
@@ -120,6 +121,7 @@ export interface PrepareProviderRequestParams {
120
121
  config?: RunnableConfig;
121
122
  maxToolResultChars?: number;
122
123
  measure?: (messages: BaseMessage[]) => ProviderPayloadMeasurement;
124
+ toolHistory?: ToolHistoryPreparation;
123
125
  }
124
126
 
125
127
  export function usesNativeOpenAIResponses(
@@ -164,11 +166,8 @@ export function usesNativeOpenAIResponses(
164
166
  } else if (effectiveCallOptions == null) {
165
167
  effectiveCallOptions = runnable.defaultOptions;
166
168
  }
167
- if (
168
- runnable._useResponsesApi?.(effectiveCallOptions) === true ||
169
- runnable._useResponsesApi?.(undefined) === true
170
- ) {
171
- return true;
169
+ if (typeof runnable._useResponsesApi === 'function') {
170
+ return runnable._useResponsesApi(effectiveCallOptions) === true;
172
171
  }
173
172
  } catch {
174
173
  // Continue through RunnableSequence/RunnableBinding wrappers.
@@ -208,6 +207,7 @@ interface ProjectMessagesForProviderParams {
208
207
  provider: t.ProviderName;
209
208
  maxToolResultChars?: number;
210
209
  callOptions?: unknown;
210
+ toolHistory?: ToolHistoryPreparation;
211
211
  }
212
212
 
213
213
  function isSerializedBuffer(value: object): value is SerializedBuffer {
@@ -380,10 +380,7 @@ function projectAttachmentsForProvider(
380
380
  }
381
381
  content ??= copyUsableContentPrefix(sourceContent, blockIndex);
382
382
  const mimeType = BEDROCK_DOCUMENT_MIME_TYPES[block.document.format];
383
- if (
384
- mimeType == null ||
385
- !canProjectBedrockDocument(provider, mimeType)
386
- ) {
383
+ if (mimeType == null || !canProjectBedrockDocument(provider, mimeType)) {
387
384
  continue;
388
385
  }
389
386
  const standardFile = toStandardFileBlock(block);
@@ -408,7 +405,12 @@ function projectAttachmentsForProvider(
408
405
  }
409
406
 
410
407
  function projectMessagesForProviderMode(
411
- { messages, provider, maxToolResultChars }: ProjectMessagesForProviderParams,
408
+ {
409
+ messages,
410
+ provider,
411
+ maxToolResultChars,
412
+ toolHistory,
413
+ }: ProjectMessagesForProviderParams,
412
414
  projectionMode: ProviderMessageProjectionMode
413
415
  ): BaseMessage[] {
414
416
  const providerFamily = getProviderFamily(provider);
@@ -417,7 +419,8 @@ function projectMessagesForProviderMode(
417
419
  projectToolStreamContentForProvider(
418
420
  messages,
419
421
  nativeOpenAIResponses ? 'native' : 'fallback',
420
- maxToolResultChars
422
+ maxToolResultChars,
423
+ toolHistory
421
424
  ),
422
425
  provider
423
426
  );
@@ -529,6 +532,7 @@ export function prepareProviderRequest({
529
532
  config,
530
533
  maxToolResultChars,
531
534
  measure,
535
+ toolHistory = createToolHistoryPreparation(),
532
536
  }: PrepareProviderRequestParams): PreparedProviderRequest {
533
537
  const projectionMode = resolveProviderMessageProjectionMode(
534
538
  model,
@@ -542,6 +546,7 @@ export function prepareProviderRequest({
542
546
  provider,
543
547
  maxToolResultChars,
544
548
  callOptions: config,
549
+ toolHistory,
545
550
  },
546
551
  projectionMode
547
552
  );
@@ -4,10 +4,10 @@ import type { BaseMessage, MessageContent } from '@langchain/core/messages';
4
4
  import type { ProviderMessageProvenancePart } from './provenance';
5
5
  import type { ProviderToolCallIndex } from './toolResultTypes';
6
6
  import {
7
+ appendProviderMessageToolCalls,
7
8
  appendProviderToolCallDescriptor,
8
9
  consumeProviderToolResultPair,
9
10
  getBoundedProviderPairingArrayProperty,
10
- getProviderAIMessageToolCallDescriptor,
11
11
  getProviderToolCallPartDescriptor,
12
12
  getProviderToolResultPartDescriptor,
13
13
  } from './toolResultTypes';
@@ -42,6 +42,7 @@ export const strictAlternationProviders: ReadonlySet<Providers> = new Set([
42
42
  */
43
43
  function collectProviderToolCalls(message: BaseMessage): ProviderToolCallIndex {
44
44
  const calls: ProviderToolCallIndex = new Map();
45
+ appendProviderMessageToolCalls(message, calls);
45
46
  const content = getBoundedProviderPairingArrayProperty(message, 'content');
46
47
  if (content != null) {
47
48
  for (let index = 0; index < content.length; index++) {
@@ -51,20 +52,6 @@ function collectProviderToolCalls(message: BaseMessage): ProviderToolCallIndex {
51
52
  }
52
53
  }
53
54
  }
54
- const toolCalls = getBoundedProviderPairingArrayProperty(
55
- message,
56
- 'tool_calls'
57
- );
58
- if (toolCalls != null) {
59
- for (let index = 0; index < toolCalls.length; index++) {
60
- const descriptor = getProviderAIMessageToolCallDescriptor(
61
- toolCalls[index]
62
- );
63
- if (descriptor != null) {
64
- appendProviderToolCallDescriptor(calls, descriptor);
65
- }
66
- }
67
- }
68
55
  return calls;
69
56
  }
70
57
 
@@ -1,32 +1,448 @@
1
+ import type {
2
+ BaseMessage,
3
+ MessageContentComplex,
4
+ } from '@langchain/core/messages';
5
+ import type { RunnableConfig } from '@langchain/core/runnables';
6
+ import type {
7
+ ProviderToolCallIndex,
8
+ ProviderToolResultPartDescriptor,
9
+ } from './toolResultTypes';
1
10
  import type * as t from '@/types';
11
+ import {
12
+ appendProviderMessageToolCalls,
13
+ appendProviderToolCallDescriptor,
14
+ consumeProviderToolResultPair,
15
+ getProviderMessageRole,
16
+ getProviderToolCallPartDescriptor,
17
+ getProviderToolMessageResultDescriptor,
18
+ getProviderToolResultPartDescriptor,
19
+ isExecutableCodePart,
20
+ } from './toolResultTypes';
21
+ import { getProviderMessageProvenance } from './provenance';
22
+ import { apportionTokenCounts } from '@/utils/tokens';
23
+ import { isReasoningContentBlock } from './reasoningTypes';
24
+ import { emitAgentLog } from '@/utils/events';
25
+ import { ContentTypes } from '@/common';
2
26
 
3
- /**
4
- * Reconciles a context-usage breakdown's instruction/available/message fields
5
- * from the pruner's budget metrics. `messageTokens` and `availableForMessages`
6
- * are DERIVED from `contextBudget` / `effectiveInstructionTokens` /
7
- * `remainingContextTokens` rather than summed from the index map — that map is
8
- * keyed by pre-prune indices, so summing it over the kept context would missum.
9
- * Shared by the live snapshot path (`Graph.createCallModel`) and the pre-send
10
- * projection (`AgentContext.projectContextUsage`) so both yield identical numbers.
11
- */
12
- export function syncBudgetDerivedFields(usage: t.ContextUsageEvent): void {
13
- const { breakdown, contextBudget, effectiveInstructionTokens } = usage;
14
- if (effectiveInstructionTokens == null) {
15
- return;
27
+ const UNKNOWN_TOOL = 'unknown_tool';
28
+ const BLANK_TEXT = /^\s*$/u;
29
+
30
+ /** Guards an aggregate: sums of rounded counts must stay safe integers.
31
+ * Callers must degrade rather than propagate: this runs on the live pre-invoke path. */
32
+ function safeCount(value: number): number {
33
+ if (!Number.isSafeInteger(value) || value < 0) {
34
+ throw new RangeError('Invalid tool context token count');
16
35
  }
17
- breakdown.instructionTokens = effectiveInstructionTokens;
18
- if (contextBudget == null) {
36
+ return value;
37
+ }
38
+
39
+ /** Accepts what the `TokenCounter` contract allows, an approximate `number`
40
+ * such as `length / 4`, by rounding it, and rejects only what no subset claim
41
+ * can be derived from: NaN, infinities, negatives and values past the safe range. */
42
+ function toCount(value: number): number {
43
+ if (!Number.isFinite(value) || value < 0) {
44
+ throw new RangeError('Invalid tool context token count');
45
+ }
46
+ return safeCount(Math.round(value));
47
+ }
48
+
49
+ function safeRawCount(value: number): number {
50
+ if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {
51
+ throw new RangeError('Invalid tool context token count');
52
+ }
53
+ return value;
54
+ }
55
+
56
+ let warnedUnavailableToolShare = false;
57
+
58
+ /** Warns once per process: an unusable counter is a permanent host-integration
59
+ * fault, while the share is dropped on every affected call regardless. The
60
+ * latch is spent only when a config can carry the event, so a config-less
61
+ * caller (the pre-send projection) cannot swallow the live path's one warning. */
62
+ function warnUnavailableToolShare(
63
+ config: RunnableConfig | undefined,
64
+ error: unknown
65
+ ): void {
66
+ if (warnedUnavailableToolShare || config == null) {
19
67
  return;
20
68
  }
21
- breakdown.availableForMessages = Math.max(
22
- 0,
23
- contextBudget - effectiveInstructionTokens
69
+ warnedUnavailableToolShare = true;
70
+ emitAgentLog(
71
+ config,
72
+ 'warn',
73
+ 'budget',
74
+ 'Tool-message context share unavailable: the token counter must return finite, non-negative counts within the safe range',
75
+ { error: error instanceof Error ? error.message : String(error) }
24
76
  );
25
- if (usage.remainingContextTokens == null) {
26
- return;
77
+ }
78
+
79
+ function isBlankTextPart(part: string | MessageContentComplex): boolean {
80
+ if (typeof part === 'string') {
81
+ return BLANK_TEXT.test(part);
27
82
  }
28
- breakdown.messageTokens = Math.max(
29
- 0,
30
- contextBudget - effectiveInstructionTokens - usage.remainingContextTokens
83
+ return (
84
+ part.type === ContentTypes.TEXT &&
85
+ typeof part.text === 'string' &&
86
+ BLANK_TEXT.test(part.text)
87
+ );
88
+ }
89
+
90
+ function readLegacyFunctionName(message: BaseMessage): string | undefined {
91
+ const name = message.additional_kwargs.function_call?.name;
92
+ return typeof name === 'string' && name.length > 0 ? name : undefined;
93
+ }
94
+
95
+ interface ResultPairing {
96
+ readonly paired: boolean;
97
+ readonly name?: string;
98
+ }
99
+
100
+ /** Pairs a result with its call the way the wire walkers do: the call is
101
+ * validated for kind and name and then consumed, so a provider that reuses a
102
+ * call id in a later turn is attributed to the current call rather than
103
+ * poisoning the id, and the bounded index never fills with answered calls. */
104
+ function pairResult(
105
+ descriptor: ProviderToolResultPartDescriptor,
106
+ calls: ProviderToolCallIndex,
107
+ previousPart?: unknown
108
+ ): ResultPairing {
109
+ const name =
110
+ descriptor.toolCallId == null
111
+ ? undefined
112
+ : calls.get(descriptor.toolCallId)?.descriptor.name;
113
+ const paired = consumeProviderToolResultPair(descriptor, calls, previousPart);
114
+ return paired ? { paired, name } : { paired };
115
+ }
116
+
117
+ /**
118
+ * A user turn a provider transform built from tool history: a tool-less
119
+ * destination inheriting tool turns folds each call and its results into one
120
+ * synthetic `HumanMessage`, and compaction of that fold keeps the lineage. The
121
+ * role is user on the wire, the bytes are retained tool output, and the fold
122
+ * is what the provenance stamp records. The counter measures whole messages,
123
+ * so per-tool attribution is lost with the fold, and any visible model text the
124
+ * fold carried alongside its calls (a "let me search" preamble, the fold's own
125
+ * scaffolding) is counted with it. That is a bounded over-count on a turn that
126
+ * exists only because of tool content; the alternative is reporting zero.
127
+ */
128
+ function isFoldedToolHistory(message: BaseMessage): boolean {
129
+ const parts = getProviderMessageProvenance(message)?.parts;
130
+ return parts != null && parts.some((part) => part.attribution === 'tool');
131
+ }
132
+
133
+ interface InvocationScan {
134
+ readonly recognizedCalls: number;
135
+ readonly toolOnly: boolean;
136
+ }
137
+
138
+ /**
139
+ * Registers an assistant turn's calls and decides whether the turn is
140
+ * tool-only. Tool-only means every part is blank text, a tool call, a provider
141
+ * tool result returned inline (Anthropic server tools), or reasoning attached
142
+ * to the turn. Visible text, media and unrecognized blocks keep the whole turn
143
+ * in the conversation share. Shape recognition is the taxonomy's, so a call or
144
+ * result representation this repo's converters accept is accepted here.
145
+ */
146
+ function scanInvocation(
147
+ message: BaseMessage,
148
+ calls: ProviderToolCallIndex,
149
+ provider?: t.ProviderName
150
+ ): InvocationScan {
151
+ let recognizedCalls = appendProviderMessageToolCalls(
152
+ message,
153
+ calls,
154
+ provider
31
155
  );
156
+ if (typeof message.content === 'string') {
157
+ return { recognizedCalls, toolOnly: BLANK_TEXT.test(message.content) };
158
+ }
159
+ const parts: ReadonlyArray<string | MessageContentComplex> = message.content;
160
+ let toolOnly = true;
161
+ let previousPart: string | MessageContentComplex | undefined;
162
+ for (const part of parts) {
163
+ if (typeof part === 'string') {
164
+ toolOnly &&= BLANK_TEXT.test(part);
165
+ previousPart = part;
166
+ continue;
167
+ }
168
+ const call = getProviderToolCallPartDescriptor(part);
169
+ if (call != null) {
170
+ appendProviderToolCallDescriptor(calls, call);
171
+ recognizedCalls += 1;
172
+ previousPart = part;
173
+ continue;
174
+ }
175
+ if (isExecutableCodePart(part)) {
176
+ recognizedCalls += 1;
177
+ previousPart = part;
178
+ continue;
179
+ }
180
+ const result = getProviderToolResultPartDescriptor(part);
181
+ if (result != null) {
182
+ pairResult(result, calls, previousPart);
183
+ previousPart = part;
184
+ continue;
185
+ }
186
+ previousPart = part;
187
+ if (isReasoningContentBlock(part)) {
188
+ continue;
189
+ }
190
+ toolOnly &&= isBlankTextPart(part);
191
+ }
192
+ return { recognizedCalls, toolOnly };
193
+ }
194
+
195
+ /** Result attribution: the paired call's name, then the message's own name,
196
+ * then the legacy call still pending for a nameless `FunctionMessage`. */
197
+ function getToolMessageResultName(
198
+ message: BaseMessage,
199
+ calls: ProviderToolCallIndex,
200
+ pendingLegacyName: string | undefined,
201
+ provider?: t.ProviderName
202
+ ): string {
203
+ const descriptor = getProviderToolMessageResultDescriptor(message, provider);
204
+ const pairing =
205
+ descriptor == null ? undefined : pairResult(descriptor, calls);
206
+ if (pairing?.name != null) {
207
+ return pairing.name;
208
+ }
209
+ if (message.name != null && message.name.length > 0) {
210
+ return message.name;
211
+ }
212
+ return pendingLegacyName ?? UNKNOWN_TOOL;
213
+ }
214
+
215
+ /**
216
+ * A user turn made only of `tool_result` parts that each pair with a pending
217
+ * call, the split `AIMessage(tool_call)` + `HumanMessage(tool_result)` history
218
+ * the converters accept. Pairing runs against a candidate copy and commits only
219
+ * when every part pairs, as the recency walker does; anything else is an
220
+ * ordinary user turn. The counter measures whole messages, so the turn is
221
+ * attributed to its one paired tool, or to `unknown_tool` when parts name
222
+ * several. Returns undefined for any other user turn.
223
+ */
224
+ function takeUserToolResultName(
225
+ message: BaseMessage,
226
+ calls: ProviderToolCallIndex
227
+ ): string | undefined {
228
+ if (typeof message.content === 'string' || message.content.length === 0) {
229
+ return undefined;
230
+ }
231
+ const candidate: ProviderToolCallIndex = new Map(calls);
232
+ let name: string | undefined;
233
+ let mixed = false;
234
+ let previousPart: unknown;
235
+ for (const part of message.content) {
236
+ const descriptor = getProviderToolResultPartDescriptor(part);
237
+ if (descriptor?.allowHumanMessagePairing !== true) {
238
+ return undefined;
239
+ }
240
+ const pairing = pairResult(descriptor, candidate, previousPart);
241
+ if (!pairing.paired) {
242
+ return undefined;
243
+ }
244
+ mixed ||= name != null && pairing.name !== name;
245
+ name = pairing.name;
246
+ previousPart = part;
247
+ }
248
+ calls.clear();
249
+ for (const [callId, entry] of candidate) {
250
+ calls.set(callId, entry);
251
+ }
252
+ return mixed || name == null ? UNKNOWN_TOOL : name;
253
+ }
254
+
255
+ /** Counts retained tool exchanges without serializing arguments or result content. */
256
+ export interface ToolMessageUsageAccumulator {
257
+ add(message: BaseMessage, getRawTokens: () => number): void;
258
+ finish(
259
+ calibrationRatio?: number
260
+ ): Pick<
261
+ t.TokenBudgetBreakdown,
262
+ 'toolMessageTokens' | 'toolMessageTokenCounts'
263
+ >;
264
+ }
265
+
266
+ /** Accumulates tool-share attribution while its caller walks a provider payload. */
267
+ export function createToolMessageUsageAccumulator(
268
+ provider?: t.ProviderName
269
+ ): ToolMessageUsageAccumulator {
270
+ const calls: ProviderToolCallIndex = new Map();
271
+ const counts: Record<string, number> = Object.create(null);
272
+ let pendingLegacyName: string | undefined;
273
+ let total = 0;
274
+ let resultTotal = 0;
275
+ const addRaw = (current: number, increment: number): number =>
276
+ safeRawCount(current + safeRawCount(increment));
277
+ const countResult = (tokens: number, name: string): void => {
278
+ total = addRaw(total, tokens);
279
+ if (tokens === 0) {
280
+ return;
281
+ }
282
+ counts[name] = addRaw(counts[name] ?? 0, tokens);
283
+ resultTotal = addRaw(resultTotal, tokens);
284
+ };
285
+
286
+ const add = (message: BaseMessage, getRawTokens: () => number): void => {
287
+ const readTokens = (): number => safeRawCount(getRawTokens());
288
+ const role = getProviderMessageRole(message, provider);
289
+ if (role === 'assistant') {
290
+ const scan = scanInvocation(message, calls, provider);
291
+ pendingLegacyName = readLegacyFunctionName(message);
292
+ if (scan.recognizedCalls > 0 && scan.toolOnly) {
293
+ total = addRaw(total, readTokens());
294
+ }
295
+ return;
296
+ }
297
+ if (role === 'tool' || role === 'function') {
298
+ const legacyName = role === 'function' ? pendingLegacyName : undefined;
299
+ countResult(
300
+ readTokens(),
301
+ getToolMessageResultName(message, calls, legacyName, provider)
302
+ );
303
+ if (role === 'function') {
304
+ pendingLegacyName = undefined;
305
+ }
306
+ return;
307
+ }
308
+ if (role === 'user' && isFoldedToolHistory(message)) {
309
+ total = addRaw(total, readTokens());
310
+ return;
311
+ }
312
+ const userResultName =
313
+ role === 'user' ? takeUserToolResultName(message, calls) : undefined;
314
+ if (userResultName != null) {
315
+ countResult(readTokens(), userResultName);
316
+ return;
317
+ }
318
+ if (role === 'user') {
319
+ calls.clear();
320
+ }
321
+ pendingLegacyName = undefined;
322
+ };
323
+
324
+ const finish = (
325
+ calibrationRatio = 1
326
+ ): Pick<
327
+ t.TokenBudgetBreakdown,
328
+ 'toolMessageTokens' | 'toolMessageTokenCounts'
329
+ > => {
330
+ const ratio =
331
+ Number.isFinite(calibrationRatio) && calibrationRatio > 0
332
+ ? calibrationRatio
333
+ : 1;
334
+ const toolMessageTokens = safeCount(Math.round(total * ratio));
335
+ const resultTokens = Math.min(
336
+ toolMessageTokens,
337
+ safeCount(Math.round(resultTotal * ratio))
338
+ );
339
+ return {
340
+ toolMessageTokens,
341
+ toolMessageTokenCounts:
342
+ resultTotal > 0 && resultTokens > 0
343
+ ? apportionTokenCounts(counts, ratio, resultTokens)
344
+ : undefined,
345
+ };
346
+ };
347
+
348
+ return { add, finish };
349
+ }
350
+
351
+ function computeToolMessageUsage(
352
+ context: readonly BaseMessage[],
353
+ tokenCounter: t.TokenCounter,
354
+ calibrationRatio = 1,
355
+ provider?: t.ProviderName
356
+ ): Pick<
357
+ t.TokenBudgetBreakdown,
358
+ 'toolMessageTokens' | 'toolMessageTokenCounts'
359
+ > {
360
+ const accumulator = createToolMessageUsageAccumulator(provider);
361
+ for (const message of context) {
362
+ accumulator.add(message, () => tokenCounter(message));
363
+ }
364
+ return accumulator.finish(calibrationRatio);
365
+ }
366
+
367
+ /** Applies the retained tool share and clamps it to the conversation total it
368
+ * is a subset of. Throws when a count cannot support that claim. */
369
+ function syncToolMessageShare(
370
+ usage: t.ContextUsageEvent,
371
+ context?: readonly BaseMessage[],
372
+ tokenCounter?: t.TokenCounter,
373
+ provider?: t.ProviderName
374
+ ): void {
375
+ const { breakdown } = usage;
376
+ if (context != null && tokenCounter != null) {
377
+ Object.assign(
378
+ breakdown,
379
+ computeToolMessageUsage(
380
+ context,
381
+ tokenCounter,
382
+ usage.calibrationRatio,
383
+ provider
384
+ )
385
+ );
386
+ }
387
+ if (breakdown.toolMessageTokens == null) {
388
+ return;
389
+ }
390
+ const total = toCount(breakdown.toolMessageTokens);
391
+ const clamped = Math.min(total, toCount(breakdown.messageTokens));
392
+ if (clamped !== total && breakdown.toolMessageTokenCounts != null) {
393
+ let resultTotal = 0;
394
+ for (const count of Object.values(breakdown.toolMessageTokenCounts)) {
395
+ resultTotal = safeCount(resultTotal + toCount(count));
396
+ }
397
+ const factor = total > 0 ? clamped / total : 0;
398
+ const target = Math.min(clamped, Math.round(resultTotal * factor));
399
+ breakdown.toolMessageTokenCounts =
400
+ target > 0
401
+ ? apportionTokenCounts(breakdown.toolMessageTokenCounts, factor, target)
402
+ : undefined;
403
+ }
404
+ breakdown.toolMessageTokens = clamped;
405
+ }
406
+
407
+ /** Reconciles derived budget fields and, when supplied, the retained tool share.
408
+ * The index map is keyed before pruning, so it cannot index the retained context.
409
+ * Callers may pass the existing exact token cache's counter, never a stale index lookup.
410
+ * Runs on the live pre-invoke path, so an unusable count drops the tool share
411
+ * (warned once) instead of failing the model call it only measures. */
412
+ export function syncBudgetDerivedFields(
413
+ usage: t.ContextUsageEvent,
414
+ context?: readonly BaseMessage[],
415
+ tokenCounter?: t.TokenCounter,
416
+ config?: RunnableConfig,
417
+ provider?: t.ProviderName,
418
+ toolMessageUsageError?: Error
419
+ ): void {
420
+ const { breakdown, contextBudget, effectiveInstructionTokens } = usage;
421
+ if (effectiveInstructionTokens != null) {
422
+ breakdown.instructionTokens = effectiveInstructionTokens;
423
+ if (contextBudget != null) {
424
+ breakdown.availableForMessages = Math.max(
425
+ 0,
426
+ contextBudget - effectiveInstructionTokens
427
+ );
428
+ if (usage.remainingContextTokens != null) {
429
+ breakdown.messageTokens = Math.max(
430
+ 0,
431
+ contextBudget -
432
+ effectiveInstructionTokens -
433
+ usage.remainingContextTokens
434
+ );
435
+ }
436
+ }
437
+ }
438
+ try {
439
+ if (toolMessageUsageError != null) {
440
+ throw toolMessageUsageError;
441
+ }
442
+ syncToolMessageShare(usage, context, tokenCounter, provider);
443
+ } catch (error) {
444
+ warnUnavailableToolShare(config, error);
445
+ delete breakdown.toolMessageTokens;
446
+ delete breakdown.toolMessageTokenCounts;
447
+ }
32
448
  }