@librechat/agents 3.8.0 → 3.8.2

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 (248) 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 +176 -55
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +16 -6
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/hitl/approvalReview.cjs +89 -0
  8. package/dist/cjs/hitl/approvalReview.cjs.map +1 -0
  9. package/dist/cjs/langfuse.cjs +42 -0
  10. package/dist/cjs/langfuse.cjs.map +1 -1
  11. package/dist/cjs/langfuseToolOutputTracing.cjs +42 -0
  12. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  13. package/dist/cjs/langfuseTraceShaping.cjs +7 -1
  14. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  15. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +6 -10
  16. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  17. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +2 -7
  18. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  19. package/dist/cjs/llm/contextPressureMeter.cjs +51 -10
  20. package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -1
  21. package/dist/cjs/llm/init.cjs +1 -1
  22. package/dist/cjs/llm/invoke.cjs +2 -2
  23. package/dist/cjs/llm/openai/index.cjs +4 -3
  24. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  25. package/dist/cjs/llm/openai/utils/index.cjs +5 -4
  26. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  27. package/dist/cjs/llm/preempt.cjs +3 -2
  28. package/dist/cjs/llm/preempt.cjs.map +1 -1
  29. package/dist/cjs/llm/prepareProviderRequest.cjs +9 -7
  30. package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -1
  31. package/dist/cjs/llm/providers.cjs +1 -1
  32. package/dist/cjs/main.cjs +14 -8
  33. package/dist/cjs/messages/alternation.cjs +1 -5
  34. package/dist/cjs/messages/alternation.cjs.map +1 -1
  35. package/dist/cjs/messages/budget.cjs +206 -7
  36. package/dist/cjs/messages/budget.cjs.map +1 -1
  37. package/dist/cjs/messages/cache.cjs +3 -9
  38. package/dist/cjs/messages/cache.cjs.map +1 -1
  39. package/dist/cjs/messages/core.cjs +26 -38
  40. package/dist/cjs/messages/core.cjs.map +1 -1
  41. package/dist/cjs/messages/format.cjs +132 -43
  42. package/dist/cjs/messages/format.cjs.map +1 -1
  43. package/dist/cjs/messages/index.cjs +2 -0
  44. package/dist/cjs/messages/prune.cjs +14 -9
  45. package/dist/cjs/messages/prune.cjs.map +1 -1
  46. package/dist/cjs/messages/reasoningTypes.cjs +21 -0
  47. package/dist/cjs/messages/reasoningTypes.cjs.map +1 -0
  48. package/dist/cjs/messages/recency.cjs +15 -94
  49. package/dist/cjs/messages/recency.cjs.map +1 -1
  50. package/dist/cjs/messages/toolHistoryProjection.cjs +243 -0
  51. package/dist/cjs/messages/toolHistoryProjection.cjs.map +1 -0
  52. package/dist/cjs/messages/toolResultTypes.cjs +145 -4
  53. package/dist/cjs/messages/toolResultTypes.cjs.map +1 -1
  54. package/dist/cjs/run.cjs +22 -10
  55. package/dist/cjs/run.cjs.map +1 -1
  56. package/dist/cjs/session/AgentSession.cjs +7 -5
  57. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  58. package/dist/cjs/session/JsonlSessionStore.cjs +3 -0
  59. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  60. package/dist/cjs/session/index.cjs +1 -1
  61. package/dist/cjs/session/sessionProjection.cjs +75 -0
  62. package/dist/cjs/session/sessionProjection.cjs.map +1 -0
  63. package/dist/cjs/stream.cjs +20 -19
  64. package/dist/cjs/stream.cjs.map +1 -1
  65. package/dist/cjs/summarization/index.cjs.map +1 -1
  66. package/dist/cjs/summarization/node.cjs +19 -9
  67. package/dist/cjs/summarization/node.cjs.map +1 -1
  68. package/dist/cjs/summarization/shared.cjs +9 -0
  69. package/dist/cjs/summarization/shared.cjs.map +1 -1
  70. package/dist/cjs/tools/ToolNode.cjs +256 -84
  71. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  72. package/dist/cjs/tools/local/CompileCheckTool.cjs +1 -1
  73. package/dist/cjs/tools/local/LocalCodingTools.cjs +1 -1
  74. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +24 -9
  75. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  76. package/dist/cjs/tools/subagent/SubagentReplay.cjs +2 -0
  77. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  78. package/dist/cjs/tools/toolBatchReplay.cjs +187 -0
  79. package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -0
  80. package/dist/cjs/tools/toolOutputReferences.cjs +12 -0
  81. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  82. package/dist/cjs/types/hitl.cjs +4 -0
  83. package/dist/cjs/types/hitl.cjs.map +1 -1
  84. package/dist/cjs/utils/events.cjs +13 -0
  85. package/dist/cjs/utils/events.cjs.map +1 -1
  86. package/dist/cjs/utils/tokens.cjs +105 -0
  87. package/dist/cjs/utils/tokens.cjs.map +1 -1
  88. package/dist/esm/agents/AgentContext.mjs +13 -3
  89. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  90. package/dist/esm/graphs/Graph.mjs +178 -57
  91. package/dist/esm/graphs/Graph.mjs.map +1 -1
  92. package/dist/esm/graphs/MultiAgentGraph.mjs +16 -6
  93. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  94. package/dist/esm/hitl/approvalReview.mjs +83 -0
  95. package/dist/esm/hitl/approvalReview.mjs.map +1 -0
  96. package/dist/esm/langfuse.mjs +43 -2
  97. package/dist/esm/langfuse.mjs.map +1 -1
  98. package/dist/esm/langfuseToolOutputTracing.mjs +41 -1
  99. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  100. package/dist/esm/langfuseTraceShaping.mjs +7 -1
  101. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  102. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +6 -10
  103. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  104. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +2 -7
  105. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  106. package/dist/esm/llm/contextPressureMeter.mjs +52 -11
  107. package/dist/esm/llm/contextPressureMeter.mjs.map +1 -1
  108. package/dist/esm/llm/init.mjs +1 -1
  109. package/dist/esm/llm/invoke.mjs +2 -2
  110. package/dist/esm/llm/openai/index.mjs +2 -1
  111. package/dist/esm/llm/openai/index.mjs.map +1 -1
  112. package/dist/esm/llm/openai/utils/index.mjs +5 -4
  113. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  114. package/dist/esm/llm/preempt.mjs +2 -1
  115. package/dist/esm/llm/preempt.mjs.map +1 -1
  116. package/dist/esm/llm/prepareProviderRequest.mjs +9 -7
  117. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -1
  118. package/dist/esm/llm/providers.mjs +1 -1
  119. package/dist/esm/main.mjs +13 -10
  120. package/dist/esm/messages/alternation.mjs +2 -6
  121. package/dist/esm/messages/alternation.mjs.map +1 -1
  122. package/dist/esm/messages/budget.mjs +206 -8
  123. package/dist/esm/messages/budget.mjs.map +1 -1
  124. package/dist/esm/messages/cache.mjs +3 -9
  125. package/dist/esm/messages/cache.mjs.map +1 -1
  126. package/dist/esm/messages/core.mjs +24 -34
  127. package/dist/esm/messages/core.mjs.map +1 -1
  128. package/dist/esm/messages/format.mjs +133 -44
  129. package/dist/esm/messages/format.mjs.map +1 -1
  130. package/dist/esm/messages/index.mjs +2 -0
  131. package/dist/esm/messages/prune.mjs +14 -9
  132. package/dist/esm/messages/prune.mjs.map +1 -1
  133. package/dist/esm/messages/reasoningTypes.mjs +20 -0
  134. package/dist/esm/messages/reasoningTypes.mjs.map +1 -0
  135. package/dist/esm/messages/recency.mjs +16 -95
  136. package/dist/esm/messages/recency.mjs.map +1 -1
  137. package/dist/esm/messages/toolHistoryProjection.mjs +237 -0
  138. package/dist/esm/messages/toolHistoryProjection.mjs.map +1 -0
  139. package/dist/esm/messages/toolResultTypes.mjs +141 -4
  140. package/dist/esm/messages/toolResultTypes.mjs.map +1 -1
  141. package/dist/esm/run.mjs +23 -11
  142. package/dist/esm/run.mjs.map +1 -1
  143. package/dist/esm/session/AgentSession.mjs +7 -5
  144. package/dist/esm/session/AgentSession.mjs.map +1 -1
  145. package/dist/esm/session/JsonlSessionStore.mjs +3 -0
  146. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  147. package/dist/esm/session/index.mjs +1 -1
  148. package/dist/esm/session/sessionProjection.mjs +72 -0
  149. package/dist/esm/session/sessionProjection.mjs.map +1 -0
  150. package/dist/esm/stream.mjs +17 -16
  151. package/dist/esm/stream.mjs.map +1 -1
  152. package/dist/esm/summarization/index.mjs.map +1 -1
  153. package/dist/esm/summarization/node.mjs +20 -10
  154. package/dist/esm/summarization/node.mjs.map +1 -1
  155. package/dist/esm/summarization/shared.mjs +9 -1
  156. package/dist/esm/summarization/shared.mjs.map +1 -1
  157. package/dist/esm/tools/ToolNode.mjs +256 -84
  158. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  159. package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
  160. package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
  161. package/dist/esm/tools/subagent/SubagentExecutor.mjs +24 -9
  162. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  163. package/dist/esm/tools/subagent/SubagentReplay.mjs +1 -1
  164. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  165. package/dist/esm/tools/toolBatchReplay.mjs +176 -0
  166. package/dist/esm/tools/toolBatchReplay.mjs.map +1 -0
  167. package/dist/esm/tools/toolOutputReferences.mjs +12 -0
  168. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  169. package/dist/esm/types/hitl.mjs +4 -1
  170. package/dist/esm/types/hitl.mjs.map +1 -1
  171. package/dist/esm/utils/events.mjs +13 -0
  172. package/dist/esm/utils/events.mjs.map +1 -1
  173. package/dist/esm/utils/tokens.mjs +105 -1
  174. package/dist/esm/utils/tokens.mjs.map +1 -1
  175. package/dist/types/agents/AgentContext.d.ts +13 -1
  176. package/dist/types/graphs/Graph.d.ts +27 -0
  177. package/dist/types/hitl/approvalReview.d.ts +32 -0
  178. package/dist/types/hooks/types.d.ts +4 -2
  179. package/dist/types/index.d.ts +1 -0
  180. package/dist/types/langfuse.d.ts +5 -0
  181. package/dist/types/langfuseToolOutputTracing.d.ts +2 -0
  182. package/dist/types/llm/contextPressureMeter.d.ts +2 -1
  183. package/dist/types/llm/prepareProviderRequest.d.ts +7 -1
  184. package/dist/types/messages/budget.d.ts +15 -10
  185. package/dist/types/messages/core.d.ts +8 -17
  186. package/dist/types/messages/format.d.ts +3 -2
  187. package/dist/types/messages/index.d.ts +1 -1
  188. package/dist/types/messages/prune.d.ts +1 -1
  189. package/dist/types/messages/reasoningTypes.d.ts +7 -0
  190. package/dist/types/messages/recency.d.ts +3 -0
  191. package/dist/types/messages/toolHistoryProjection.d.ts +65 -0
  192. package/dist/types/messages/toolResultTypes.d.ts +31 -1
  193. package/dist/types/session/sessionProjection.d.ts +10 -0
  194. package/dist/types/summarization/index.d.ts +2 -1
  195. package/dist/types/summarization/node.d.ts +1 -0
  196. package/dist/types/summarization/shared.d.ts +12 -0
  197. package/dist/types/tools/ToolNode.d.ts +12 -6
  198. package/dist/types/tools/subagent/SubagentReplay.d.ts +2 -0
  199. package/dist/types/tools/toolBatchReplay.d.ts +54 -0
  200. package/dist/types/tools/toolOutputReferences.d.ts +2 -0
  201. package/dist/types/types/graph.d.ts +20 -0
  202. package/dist/types/types/run.d.ts +4 -0
  203. package/dist/types/types/summarize.d.ts +4 -1
  204. package/dist/types/utils/tokens.d.ts +2 -1
  205. package/package.json +1 -1
  206. package/src/agents/AgentContext.ts +34 -3
  207. package/src/graphs/Graph.ts +465 -147
  208. package/src/graphs/MultiAgentGraph.ts +28 -6
  209. package/src/hitl/approvalReview.ts +209 -0
  210. package/src/hooks/types.ts +4 -1
  211. package/src/index.ts +1 -0
  212. package/src/langfuse.ts +67 -9
  213. package/src/langfuseToolOutputTracing.ts +91 -0
  214. package/src/langfuseTraceShaping.ts +17 -4
  215. package/src/llm/anthropic/utils/message_inputs.ts +5 -10
  216. package/src/llm/bedrock/utils/message_inputs.ts +2 -8
  217. package/src/llm/contextPressureMeter.ts +80 -12
  218. package/src/llm/openai/utils/index.ts +5 -4
  219. package/src/llm/prepareProviderRequest.ts +21 -16
  220. package/src/messages/alternation.ts +2 -15
  221. package/src/messages/budget.ts +439 -23
  222. package/src/messages/cache.ts +8 -9
  223. package/src/messages/core.ts +65 -95
  224. package/src/messages/format.ts +262 -78
  225. package/src/messages/index.ts +1 -1
  226. package/src/messages/prune.ts +31 -12
  227. package/src/messages/reasoningTypes.ts +23 -0
  228. package/src/messages/recency.ts +35 -179
  229. package/src/messages/toolHistoryProjection.ts +460 -0
  230. package/src/messages/toolResultTypes.ts +331 -100
  231. package/src/run.ts +44 -22
  232. package/src/session/AgentSession.ts +33 -16
  233. package/src/session/JsonlSessionStore.ts +6 -0
  234. package/src/session/sessionProjection.ts +126 -0
  235. package/src/stream.ts +14 -19
  236. package/src/summarization/index.ts +2 -0
  237. package/src/summarization/node.ts +62 -13
  238. package/src/summarization/shared.ts +23 -0
  239. package/src/tools/ToolNode.ts +518 -184
  240. package/src/tools/subagent/SubagentExecutor.ts +35 -6
  241. package/src/tools/subagent/SubagentReplay.ts +2 -2
  242. package/src/tools/toolBatchReplay.ts +395 -0
  243. package/src/tools/toolOutputReferences.ts +16 -0
  244. package/src/types/graph.ts +20 -0
  245. package/src/types/run.ts +4 -0
  246. package/src/types/summarize.ts +4 -1
  247. package/src/utils/events.ts +19 -0
  248. package/src/utils/tokens.ts +183 -0
@@ -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
  }
@@ -4,6 +4,7 @@ import type Anthropic from '@anthropic-ai/sdk';
4
4
  import type { AnthropicMessage } from '@/types/messages';
5
5
  import { hasComputerCallOutputMarker } from '@/utils/toolContent';
6
6
  import { toLangChainContent } from './langchain';
7
+ import { isReasoningContentBlock } from './reasoningTypes';
7
8
  import { ContentTypes } from '@/common/enum';
8
9
 
9
10
  type MessageWithContent = {
@@ -412,27 +413,25 @@ function isCachePoint(block: unknown): boolean {
412
413
  * loses tail caching, so all of these are excluded.
413
414
  */
414
415
  const NON_ANCHORABLE_BLOCK_TYPES = new Set([
415
- 'thinking',
416
- 'redacted_thinking',
417
- 'reasoning_content',
418
- 'reasoning',
419
- 'think',
420
416
  'input_json_delta',
421
417
  ]);
422
418
 
423
419
  /**
424
420
  * A block can anchor the tail cache breakpoint when it is a real content block
425
421
  * that the Anthropic API accepts `cache_control` on and that survives provider
426
- * conversion. Reasoning / dropped-delta blocks are excluded (see
427
- * {@link NON_ANCHORABLE_BLOCK_TYPES}), and empty text blocks are not cacheable,
428
- * so both are skipped.
422
+ * conversion. Reasoning and dropped-delta blocks are excluded, and empty text
423
+ * blocks are not cacheable, so all are skipped.
429
424
  */
430
425
  function isTailCacheableBlock(block: MessageContentComplex): boolean {
431
426
  if (isCachePoint(block)) {
432
427
  return false;
433
428
  }
434
429
  const type = (block as { type?: string }).type;
435
- if (type == null || NON_ANCHORABLE_BLOCK_TYPES.has(type)) {
430
+ if (
431
+ type == null ||
432
+ isReasoningContentBlock(block) ||
433
+ NON_ANCHORABLE_BLOCK_TYPES.has(type)
434
+ ) {
436
435
  return false;
437
436
  }
438
437
  if (type === 'text') {