@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
@@ -1267,6 +1267,11 @@ export class MultiAgentGraph extends StandardGraph {
1267
1267
  reducer: (_current, update) => update,
1268
1268
  default: () => undefined,
1269
1269
  }),
1270
+ /** Surfaced from the summarizing agent's subgraph on a summarize-only run. */
1271
+ manualSummary: Annotation<string | undefined>({
1272
+ reducer: (_current, update) => update,
1273
+ default: () => undefined,
1274
+ }),
1270
1275
  runStepState: this.createRunStepStateAnnotation(),
1271
1276
  });
1272
1277
 
@@ -1281,14 +1286,29 @@ export class MultiAgentGraph extends StandardGraph {
1281
1286
  builder.addEdge(source, destination);
1282
1287
  };
1283
1288
 
1289
+ /**
1290
+ * A summarize-only run compiles to the opted-in agent alone: no other
1291
+ * node, no handoff or direct edge, START → agent → END. Anything else
1292
+ * would run the summarizer twice (once from START, once through its
1293
+ * predecessor) or append routing prompts to the checkpoint it just
1294
+ * produced.
1295
+ */
1296
+ const summarizeOnlyAgentId = this.summarizeOnlyAgentId;
1297
+ const agentIds =
1298
+ summarizeOnlyAgentId != null
1299
+ ? [summarizeOnlyAgentId]
1300
+ : [...this.agentContexts.keys()];
1301
+ const handoffEdges = summarizeOnlyAgentId != null ? [] : this.handoffEdges;
1302
+ const directEdges = summarizeOnlyAgentId != null ? [] : this.directEdges;
1303
+
1284
1304
  // Add all agents as complete subgraphs
1285
- for (const [agentId] of this.agentContexts) {
1305
+ for (const agentId of agentIds) {
1286
1306
  // Get all possible destinations for this agent
1287
1307
  const handoffDestinations = new Set<string>();
1288
1308
  const directDestinations = new Set<string>();
1289
1309
 
1290
1310
  // Check handoff edges for destinations
1291
- for (const edge of this.handoffEdges) {
1311
+ for (const edge of handoffEdges) {
1292
1312
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1293
1313
  if (sources.includes(agentId) === true) {
1294
1314
  const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
@@ -1297,7 +1317,7 @@ export class MultiAgentGraph extends StandardGraph {
1297
1317
  }
1298
1318
 
1299
1319
  // Check direct edges for destinations
1300
- for (const edge of this.directEdges) {
1320
+ for (const edge of directEdges) {
1301
1321
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1302
1322
  if (sources.includes(agentId) === true) {
1303
1323
  const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
@@ -1500,6 +1520,7 @@ export class MultiAgentGraph extends StandardGraph {
1500
1520
  } else {
1501
1521
  result = await agentSubgraph.invoke(state, memberConfig);
1502
1522
  }
1523
+ result = this.propagateManualCompaction(result);
1503
1524
 
1504
1525
  if (this.resultAgentId === agentId) {
1505
1526
  result = {
@@ -1560,8 +1581,9 @@ export class MultiAgentGraph extends StandardGraph {
1560
1581
  });
1561
1582
  }
1562
1583
 
1563
- // Add starting edges for all starting nodes
1564
- for (const startNode of this.startingNodes) {
1584
+ const startingNodes =
1585
+ summarizeOnlyAgentId != null ? [summarizeOnlyAgentId] : this.startingNodes;
1586
+ for (const startNode of startingNodes) {
1565
1587
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1566
1588
  /** @ts-ignore */
1567
1589
  builder.addEdge(START, startNode);
@@ -1573,7 +1595,7 @@ export class MultiAgentGraph extends StandardGraph {
1573
1595
  */
1574
1596
  const edgesByDestination = new Map<string, t.GraphEdge[]>();
1575
1597
 
1576
- for (const edge of this.directEdges) {
1598
+ for (const edge of directEdges) {
1577
1599
  const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
1578
1600
  for (const destination of destinations) {
1579
1601
  if (!edgesByDestination.has(destination)) {
@@ -252,13 +252,16 @@ export interface PreCompactHookInput extends BaseHookInput {
252
252
  /**
253
253
  * What triggered compaction. Matches `SummarizationTrigger.type` from the
254
254
  * agent's summarization config. `'default'` means no trigger was
255
- * configured and compaction fired because messages were pruned.
255
+ * configured and compaction fired because messages were pruned;
256
+ * `'manual'` means the host asked for the summary outright and no trigger
257
+ * was evaluated.
256
258
  */
257
259
  trigger:
258
260
  | 'token_ratio'
259
261
  | 'remaining_tokens'
260
262
  | 'messages_to_refine'
261
263
  | 'default'
264
+ | 'manual'
262
265
  | (string & {});
263
266
  }
264
267
 
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from './run';
3
3
  export * from './stream';
4
4
  export * from './events';
5
5
  export * from './messages';
6
+ export { LANGFUSE_OBSERVATION_METADATA_ARTIFACT_KEY } from './langfuseToolOutputTracing';
6
7
 
7
8
  /* Graphs */
8
9
  export * from './graphs';
package/src/langfuse.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { tool } from '@langchain/core/tools';
1
2
  import { CallbackHandler } from '@langfuse/langchain';
2
3
  import { LangfuseOtelContextKeys } from '@langfuse/core';
3
4
  import { AIMessage, AIMessageChunk } from '@langchain/core/messages';
@@ -18,6 +19,7 @@ import type {
18
19
  LLMResult,
19
20
  } from '@langchain/core/outputs';
20
21
  import type { PropagateAttributesParams } from '@langfuse/tracing';
22
+ import type { RunnableConfig } from '@langchain/core/runnables';
21
23
  import type { Context, SpanContext } from '@opentelemetry/api';
22
24
  import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
23
25
  import type * as t from '@/types';
@@ -39,10 +41,15 @@ import {
39
41
  registerLangfuseManagedSpan,
40
42
  resolveLangfuseDestinationKey,
41
43
  } from '@/langfuseSpanRegistry';
44
+ import {
45
+ getToolObservationMetadata,
46
+ LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
47
+ } from '@/langfuseToolOutputTracing';
42
48
  import {
43
49
  readPreemptRestartedRun,
44
50
  PREEMPT_RESTART_CONTROL_FLOW,
45
51
  } from '@/llm/preempt';
52
+ import { filterCallbacks, findCallback } from '@/utils/callbacks';
46
53
  import { isPresent, parseBooleanEnv } from '@/utils/misc';
47
54
 
48
55
  export {
@@ -306,10 +313,10 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
306
313
  private deferredRootStarted = false;
307
314
  private deferredRootOutcome:
308
315
  | {
309
- type: 'end';
310
- output: Parameters<CallbackHandler['handleChainEnd']>[0];
311
- parentRunId?: string;
312
- }
316
+ type: 'end';
317
+ output: Parameters<CallbackHandler['handleChainEnd']>[0];
318
+ parentRunId?: string;
319
+ }
313
320
  | { type: 'error'; error: Error; parentRunId?: string }
314
321
  | undefined;
315
322
 
@@ -615,11 +622,7 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
615
622
  this.deferredRootStarted = false;
616
623
  this.deferredRootOutcome = undefined;
617
624
  if (outcome.type === 'error') {
618
- await super.handleChainError(
619
- outcome.error,
620
- runId,
621
- outcome.parentRunId
622
- );
625
+ await super.handleChainError(outcome.error, runId, outcome.parentRunId);
623
626
  return;
624
627
  }
625
628
  await super.handleChainEnd(outcome.output, runId, outcome.parentRunId);
@@ -895,6 +898,18 @@ function mergeLangfuseTags(
895
898
  return merged.length > 0 ? [...new Set(merged)] : undefined;
896
899
  }
897
900
 
901
+ /**
902
+ * The trace's user identity: the host-configured `langfuse.userId` when
903
+ * present, else the caller's (normally `configurable.user_id`).
904
+ */
905
+ export function resolveLangfuseTraceUserId(
906
+ langfuse: t.LangfuseConfig | undefined,
907
+ userId: string | undefined
908
+ ): string | undefined {
909
+ const configured = langfuse?.userId?.trim();
910
+ return isPresent(configured) ? configured : userId;
911
+ }
912
+
898
913
  export function getLangfuseTraceName(
899
914
  traceMetadata?: LangfuseTraceMetadata,
900
915
  fallback: string = 'LibreChat Agent'
@@ -942,7 +957,7 @@ export function createLangfuseHandler({
942
957
  return undefined;
943
958
  }
944
959
  return new ScopedLangfuseCallbackHandler({
945
- userId,
960
+ userId: resolveLangfuseTraceUserId(langfuse, userId),
946
961
  sessionId,
947
962
  traceMetadata:
948
963
  inheritTraceIdentity === true
@@ -972,7 +987,10 @@ function createPropagateAttributeParams({
972
987
  inheritTraceIdentity,
973
988
  }: LangfuseAttributeParams): PropagateAttributesParams {
974
989
  return {
975
- userId: inheritTraceIdentity === true ? undefined : userId,
990
+ userId:
991
+ inheritTraceIdentity === true
992
+ ? undefined
993
+ : resolveLangfuseTraceUserId(langfuse, userId),
976
994
  sessionId: inheritTraceIdentity === true ? undefined : sessionId,
977
995
  traceName: inheritTraceIdentity === true ? undefined : traceName,
978
996
  tags: mergeLangfuseTags(tags, langfuse?.tags),
@@ -1008,6 +1026,61 @@ export function isLangfuseCallbackHandler(value: unknown): boolean {
1008
1026
  return value instanceof CallbackHandler;
1009
1027
  }
1010
1028
 
1029
+ /** Host results have passed host output filters. Record only reserved metadata,
1030
+ * not arguments, rows, errors, or attachments. These completion observations
1031
+ * do not measure tool latency; execution timing belongs in the metadata. */
1032
+ export async function traceHostToolResults(
1033
+ request: t.ToolExecuteBatchRequest,
1034
+ results: t.ToolExecuteResult[],
1035
+ config?: RunnableConfig
1036
+ ): Promise<void> {
1037
+ if (findCallback(config?.callbacks, isLangfuseCallbackHandler) == null) {
1038
+ return;
1039
+ }
1040
+ const callbacks = filterCallbacks(
1041
+ config?.callbacks,
1042
+ isLangfuseCallbackHandler
1043
+ );
1044
+ const callsById = new Map(request.toolCalls.map((call) => [call.id, call]));
1045
+ for (const result of results) {
1046
+ const metadata = getToolObservationMetadata(result.artifact);
1047
+ const call = callsById.get(result.toolCallId);
1048
+ if (call == null || Object.keys(metadata).length === 0) {
1049
+ continue;
1050
+ }
1051
+ const failure =
1052
+ result.status === 'error'
1053
+ ? new Error('Host tool execution failed')
1054
+ : undefined;
1055
+ const observation = tool(
1056
+ async () => {
1057
+ if (failure != null) {
1058
+ throw failure;
1059
+ }
1060
+ return LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT;
1061
+ },
1062
+ {
1063
+ name: call.name,
1064
+ description: 'Host tool execution metadata',
1065
+ schema: { type: 'object', properties: {} },
1066
+ }
1067
+ );
1068
+ try {
1069
+ await observation.invoke(
1070
+ { type: 'tool_call', id: call.id, name: call.name, args: {} },
1071
+ {
1072
+ callbacks,
1073
+ metadata: { ...metadata, ...request.metadata },
1074
+ }
1075
+ );
1076
+ } catch (error) {
1077
+ if (failure == null || error !== failure) {
1078
+ throw error;
1079
+ }
1080
+ }
1081
+ }
1082
+ }
1083
+
1011
1084
  export async function disposeLangfuseHandler(value: unknown): Promise<void> {
1012
1085
  if (value instanceof ScopedLangfuseCallbackHandler) {
1013
1086
  await value.finishDeferredRoot();
@@ -24,6 +24,9 @@ import { resolveToolOutputTracingConfigForSpan } from '@/langfuseRuntimeScope';
24
24
 
25
25
  export { LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT, resolveLangfuseConfig };
26
26
 
27
+ export const LANGFUSE_OBSERVATION_METADATA_ARTIFACT_KEY =
28
+ 'librechatLangfuseObservationMetadata';
29
+
27
30
  const LANGGRAPH_TOOL_NODE_PREFIX = 'tools=';
28
31
  const SERVER_TOOL_RESULT_PREFIX = '{"serverToolResult":';
29
32
  const SERVER_TOOL_RESULT_REPLAY_MARKER_KEY = 'librechatResponsesReplay';
@@ -52,6 +55,9 @@ type RedactionContext = {
52
55
  };
53
56
 
54
57
  const TOOL_OUTPUT_FIELD_KEYS = ['content', 'artifact'];
58
+ const MAX_OBSERVATION_METADATA_FIELDS = 32;
59
+ const MAX_OBSERVATION_METADATA_STRING_LENGTH = 200;
60
+ const OBSERVATION_METADATA_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
55
61
 
56
62
  type ResponsesReplayOutputDescriptor = {
57
63
  nestedOutputFields?: Readonly<Record<string, readonly string[]>>;
@@ -744,6 +750,90 @@ export function classifyLangfuseToolNodeSpan(span: ReadableSpan): void {
744
750
  classifyLangGraphToolNodeSpan((span as SpanWithAttributes).attributes);
745
751
  }
746
752
 
753
+ function parseSerializedValue(value: unknown): unknown {
754
+ if (typeof value !== 'string') {
755
+ return value;
756
+ }
757
+ try {
758
+ return JSON.parse(value) as unknown;
759
+ } catch {
760
+ return undefined;
761
+ }
762
+ }
763
+
764
+ function getToolMessageArtifact(
765
+ value: unknown
766
+ ): Record<string, unknown> | undefined {
767
+ const parsed = parseSerializedValue(value);
768
+ if (!isRecord(parsed)) {
769
+ return undefined;
770
+ }
771
+ if (isRecord(parsed.artifact)) {
772
+ return parsed.artifact;
773
+ }
774
+ if (isRecord(parsed.kwargs) && isRecord(parsed.kwargs.artifact)) {
775
+ return parsed.kwargs.artifact;
776
+ }
777
+ return undefined;
778
+ }
779
+
780
+ export function getToolObservationMetadata(
781
+ artifact: unknown
782
+ ): Record<string, string | number | boolean> {
783
+ // This reserved artifact field is for trusted host adapters. Never copy
784
+ // arbitrary tool or model output into it without an explicit allowlist.
785
+ const metadata = isRecord(artifact)
786
+ ? artifact[LANGFUSE_OBSERVATION_METADATA_ARTIFACT_KEY]
787
+ : undefined;
788
+ const result: Record<string, string | number | boolean> = {};
789
+ if (!isRecord(metadata)) {
790
+ return result;
791
+ }
792
+
793
+ let promoted = 0;
794
+ for (const [key, value] of Object.entries(metadata)) {
795
+ if (
796
+ promoted >= MAX_OBSERVATION_METADATA_FIELDS ||
797
+ !OBSERVATION_METADATA_KEY_PATTERN.test(key) ||
798
+ !(
799
+ typeof value === 'number' ||
800
+ typeof value === 'boolean' ||
801
+ (typeof value === 'string' &&
802
+ value.length <= MAX_OBSERVATION_METADATA_STRING_LENGTH)
803
+ )
804
+ ) {
805
+ continue;
806
+ }
807
+ if (typeof value === 'number' && !Number.isFinite(value)) {
808
+ continue;
809
+ }
810
+ result[key] = value;
811
+ promoted += 1;
812
+ }
813
+ return result;
814
+ }
815
+
816
+ function promoteToolObservationMetadata(
817
+ span: ReadableSpan,
818
+ attributes: Record<string, unknown>,
819
+ config: ResolvedLangfuseToolOutputTracingConfig
820
+ ): void {
821
+ if (!isToolObservation(attributes) || !shouldRedactTool(span.name, config)) {
822
+ return;
823
+ }
824
+ const metadata = getToolObservationMetadata(
825
+ getToolMessageArtifact(
826
+ attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]
827
+ )
828
+ );
829
+ for (const [key, value] of Object.entries(metadata)) {
830
+ const attributeKey = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.${key}`;
831
+ if (!Object.prototype.hasOwnProperty.call(attributes, attributeKey)) {
832
+ attributes[attributeKey] = value;
833
+ }
834
+ }
835
+ }
836
+
747
837
  function redactToolObservationOutput(
748
838
  span: ReadableSpan,
749
839
  attributes: Record<string, unknown>,
@@ -774,6 +864,7 @@ export function redactLangfuseSpanToolOutputs(
774
864
  return;
775
865
  }
776
866
 
867
+ promoteToolObservationMetadata(span, attributes, config);
777
868
  redactToolObservationOutput(span, attributes, config);
778
869
 
779
870
  for (const key of [
@@ -246,6 +246,18 @@ function findLastMessageText(value: unknown, role: string): string | undefined {
246
246
  return undefined;
247
247
  }
248
248
 
249
+ /**
250
+ * A summarize-only run answers with its summary, carried in state as
251
+ * `manualSummary`, rather than with an assistant message.
252
+ */
253
+ function getManualSummary(value: unknown): string | undefined {
254
+ if (!isRecord(value) || typeof value.manualSummary !== 'string') {
255
+ return undefined;
256
+ }
257
+ const text = value.manualSummary.trim();
258
+ return text === '' ? undefined : text;
259
+ }
260
+
249
261
  function normalizeToolCall(value: unknown): SerializedToolCall | undefined {
250
262
  if (!isRecord(value)) {
251
263
  return undefined;
@@ -534,10 +546,11 @@ function shapeConversationPayload(span: MutableSpan): void {
534
546
  parseAttributeValue(span.attributes[inputKey]),
535
547
  'user'
536
548
  );
537
- const answer = findLastMessageText(
538
- parseAttributeValue(span.attributes[outputKey]),
539
- 'assistant'
540
- );
549
+ const output = parseAttributeValue(span.attributes[outputKey]);
550
+ /** A summarize-only run's summary is its answer; any assistant message
551
+ * still in state is an older reply the run retained, not its output. */
552
+ const answer =
553
+ getManualSummary(output) ?? findLastMessageText(output, 'assistant');
541
554
  /** A generation that IS the trace root — a bare `model.invoke` with no
542
555
  * wrapping chain, i.e. the activity-label path — is also the only
543
556
  * observation carrying its own prompt: reducing its observation input
@@ -39,6 +39,7 @@ import {
39
39
  serializeStructuredValueBounded,
40
40
  } from '@/utils/toolContent';
41
41
  import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
42
+ import { isReasoningContentBlock } from '@/messages/reasoningTypes';
42
43
  import { Constants } from '@/common';
43
44
 
44
45
  type StandardTextBlock = Data.StandardTextBlock;
@@ -656,7 +657,6 @@ function _formatContent(message: BaseMessage) {
656
657
  * cross-provider handoff (e.g. Bedrock → Anthropic) we drop them rather than
657
658
  * forwarding an unusable block. The receiving model produces its own thinking.
658
659
  */
659
- const foreignReasoningTypes = ['reasoning_content', 'reasoning', 'think'];
660
660
  /**
661
661
  * Google server-side tool blocks (`toolCall`/`toolResponse` parts from e.g.
662
662
  * URL context or Google Search). Only Google can execute these and validate
@@ -1049,7 +1049,7 @@ function _formatContent(message: BaseMessage) {
1049
1049
  };
1050
1050
  } else if (
1051
1051
  isAIMessage(message) &&
1052
- foreignReasoningTypes.some((t) => t === contentPart.type)
1052
+ isReasoningContentBlock(contentPart)
1053
1053
  ) {
1054
1054
  // Foreign reasoning on an ASSISTANT turn (Bedrock `reasoning_content`,
1055
1055
  // Google `reasoning`, LibreChat `think`) carries provider-specific
@@ -1240,12 +1240,6 @@ function messagesHaveCacheControl(
1240
1240
  );
1241
1241
  }
1242
1242
 
1243
- /** Anthropic rejects cache_control on these reasoning blocks. */
1244
- const NON_CACHEABLE_PAYLOAD_BLOCK_TYPES = new Set([
1245
- 'thinking',
1246
- 'redacted_thinking',
1247
- ]);
1248
-
1249
1243
  /**
1250
1244
  * Place one ephemeral `cache_control` on the last cacheable block of the final
1251
1245
  * message of an already-converted Anthropic payload. Used to re-anchor the tail
@@ -1287,8 +1281,9 @@ function reanchorTailCacheControl(
1287
1281
 
1288
1282
  let anchor = -1;
1289
1283
  for (let i = 0; i < content.length; i++) {
1290
- const type = (content[i] as { type?: string }).type;
1291
- if (type == null || NON_CACHEABLE_PAYLOAD_BLOCK_TYPES.has(type)) {
1284
+ const block = content[i] as { type?: string };
1285
+ const type = block.type;
1286
+ if (type == null || isReasoningContentBlock(block)) {
1292
1287
  continue;
1293
1288
  }
1294
1289
  if (
@@ -30,6 +30,7 @@ import type {
30
30
  } from '../types';
31
31
  import { serializeStructuredValueBounded } from '@/utils/toolContent';
32
32
  import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
33
+ import { isReasoningContentBlock } from '@/messages/reasoningTypes';
33
34
 
34
35
  /**
35
36
  * Reasoning blocks from other providers, relative to Bedrock. Bedrock's native
@@ -37,13 +38,6 @@ import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
37
38
  * signatures Bedrock cannot validate, so they are dropped on a cross-provider
38
39
  * handoff (e.g. Anthropic → Bedrock) rather than crashing the conversion.
39
40
  */
40
- const FOREIGN_REASONING_TYPES = [
41
- 'thinking',
42
- 'redacted_thinking',
43
- 'reasoning',
44
- 'think',
45
- ];
46
-
47
41
  /**
48
42
  * Google server-side tool blocks (`toolCall`/`toolResponse` parts from e.g.
49
43
  * URL context or Google Search). Only Google can execute these and validate
@@ -797,7 +791,7 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
797
791
  contentBlocks.push({
798
792
  cachePoint,
799
793
  } as BedrockContentBlock);
800
- } else if (FOREIGN_REASONING_TYPES.some((t) => t === block.type)) {
794
+ } else if (isReasoningContentBlock(block)) {
801
795
  // Reasoning from another provider (Anthropic `thinking`/
802
796
  // `redacted_thinking`, Google `reasoning`, LibreChat `think`).
803
797
  // Bedrock's native reasoning is `reasoning_content` (handled above); a
@@ -9,7 +9,8 @@ import {
9
9
  REPLY_PRIMER_TOKENS,
10
10
  isSyntheticProviderContextMessage,
11
11
  } from '@/messages';
12
- import { apportionTokenCounts } from '@/utils';
12
+ import { createToolMessageUsageAccumulator } from '@/messages/budget';
13
+ import { readTokenMetadataProperty, apportionTokenCounts } from '@/utils';
13
14
 
14
15
  interface ContextPressureUsage {
15
16
  contextBudget?: number;
@@ -19,6 +20,7 @@ interface ContextPressureUsage {
19
20
  }
20
21
 
21
22
  interface ContextPressureMeterParams {
23
+ provider?: t.ProviderName;
22
24
  tokenCounter?: t.TokenCounter;
23
25
  tokenCountCache?: ExactTokenCountCache;
24
26
  sourceMessages: BaseMessage[];
@@ -89,6 +91,21 @@ function readDataProperty(
89
91
  : { own: true, safe: false };
90
92
  }
91
93
 
94
+ function readCacheMetadataProperty(
95
+ owner: object,
96
+ property: PropertyKey,
97
+ path: string
98
+ ): { safe: boolean; value?: unknown } {
99
+ try {
100
+ return {
101
+ safe: true,
102
+ value: readTokenMetadataProperty(owner, property, path),
103
+ };
104
+ } catch {
105
+ return { safe: false };
106
+ }
107
+ }
108
+
92
109
  function getStableTokenSurface(
93
110
  message: BaseMessage
94
111
  ): StableTokenSurface | undefined {
@@ -105,10 +122,13 @@ function getStableTokenSurface(
105
122
  }
106
123
  const messageType = message.getType();
107
124
  const roleProperty = readDataProperty(message, 'role');
108
- if (!roleProperty.safe || (!roleProperty.own && 'role' in message)) {
125
+ const resolvedRoleProperty = roleProperty.own
126
+ ? roleProperty
127
+ : readCacheMetadataProperty(message, 'role', 'message.role');
128
+ if (!resolvedRoleProperty.safe) {
109
129
  return undefined;
110
130
  }
111
- const role = roleProperty.value;
131
+ const role = resolvedRoleProperty.value;
112
132
  if (role != null && typeof role !== 'string') {
113
133
  return undefined;
114
134
  }
@@ -129,6 +149,14 @@ function getStableTokenSurface(
129
149
  return undefined;
130
150
  }
131
151
  const additionalKwargs = rawAdditionalKwargs;
152
+ const rawToolCalls = readCacheMetadataProperty(
153
+ additionalKwargs,
154
+ 'tool_calls',
155
+ 'additional_kwargs.tool_calls'
156
+ );
157
+ if (!rawToolCalls.safe || rawToolCalls.value != null) {
158
+ return undefined;
159
+ }
132
160
  const typeProperty = readDataProperty(additionalKwargs, 'type');
133
161
  if (!typeProperty.safe) {
134
162
  return undefined;
@@ -138,16 +166,16 @@ function getStableTokenSurface(
138
166
  message as AIMessage,
139
167
  'tool_calls'
140
168
  );
141
- if (
142
- !toolCallsProperty.safe ||
143
- (!toolCallsProperty.own && 'tool_calls' in message)
144
- ) {
169
+ const resolvedToolCallsProperty = toolCallsProperty.own
170
+ ? toolCallsProperty
171
+ : readCacheMetadataProperty(message, 'tool_calls', 'message.tool_calls');
172
+ if (!resolvedToolCallsProperty.safe) {
145
173
  return undefined;
146
174
  }
147
- const toolCalls = toolCallsProperty.value;
175
+ const toolCalls = resolvedToolCallsProperty.value;
148
176
  if (
149
177
  toolCalls != null &&
150
- (!Array.isArray(toolCalls) || isProxy(toolCalls) || toolCalls.length > 0)
178
+ (isProxy(toolCalls) || !Array.isArray(toolCalls) || toolCalls.length > 0)
151
179
  ) {
152
180
  return undefined;
153
181
  }
@@ -230,6 +258,7 @@ function getProviderMessageOriginKey(message: BaseMessage): string | undefined {
230
258
 
231
259
  /** Measures repeated provider projections while tokenizing each message object once. */
232
260
  export function createContextPressureMeter({
261
+ provider,
233
262
  tokenCounter,
234
263
  tokenCountCache,
235
264
  sourceMessages,
@@ -382,6 +411,22 @@ export function createContextPressureMeter({
382
411
  ? availableMessageTokens -
383
412
  Math.min(availableMessageTokens, Math.max(0, baselineRemaining))
384
413
  : undefined;
414
+ const toolUsage = createToolMessageUsageAccumulator(provider);
415
+ const toolUsageState: { error?: Error } = {};
416
+ const addToolUsage = (
417
+ message: BaseMessage,
418
+ getRawTokens: () => number
419
+ ): void => {
420
+ if (toolUsageState.error != null) {
421
+ return;
422
+ }
423
+ try {
424
+ toolUsage.add(message, getRawTokens);
425
+ } catch (error) {
426
+ toolUsageState.error =
427
+ error instanceof Error ? error : new Error(String(error));
428
+ }
429
+ };
385
430
 
386
431
  let projectedMessageTokens: number;
387
432
  if (
@@ -423,15 +468,18 @@ export function createContextPressureMeter({
423
468
  const usedOrigins = new Set<number>();
424
469
  for (const message of messages) {
425
470
  const origin = origins.get(message);
471
+ let rawTokens: number | undefined;
472
+ const getRawTokens = (): number => (rawTokens ??= count(message));
473
+ addToolUsage(message, getRawTokens);
426
474
  if (origin == null || usedOrigins.has(origin)) {
427
- newRawTokens += count(message);
475
+ newRawTokens += getRawTokens();
428
476
  continue;
429
477
  }
430
478
  usedOrigins.add(origin);
431
479
  const projectionDelta =
432
480
  message === baseline[origin].message
433
481
  ? 0
434
- : count(message) - baseline[origin].rawTokens;
482
+ : getRawTokens() - baseline[origin].rawTokens;
435
483
  projectedMessageTokens += Math.max(
436
484
  0,
437
485
  attribution.attributedByOrigin[origin] +
@@ -442,16 +490,36 @@ export function createContextPressureMeter({
442
490
  } else {
443
491
  let rawTokens = REPLY_PRIMER_TOKENS;
444
492
  for (const message of messages) {
445
- rawTokens += count(message);
493
+ const messageTokens = count(message);
494
+ addToolUsage(message, () => messageTokens);
495
+ rawTokens += messageTokens;
446
496
  }
447
497
  projectedMessageTokens = Math.round(rawTokens * usageRatio);
448
498
  }
499
+ let measuredToolUsage:
500
+ | Pick<
501
+ t.TokenBudgetBreakdown,
502
+ 'toolMessageTokens' | 'toolMessageTokenCounts'
503
+ >
504
+ | undefined;
505
+ try {
506
+ measuredToolUsage =
507
+ toolUsageState.error == null ? toolUsage.finish(usageRatio) : undefined;
508
+ } catch (error) {
509
+ toolUsageState.error =
510
+ error instanceof Error ? error : new Error(String(error));
511
+ measuredToolUsage = undefined;
512
+ }
449
513
  return {
450
514
  fits: projectedMessageTokens <= availableMessageTokens,
451
515
  projectedMessageTokens,
452
516
  availableMessageTokens,
453
517
  contextBudget,
454
518
  effectiveInstructionTokens,
519
+ ...measuredToolUsage,
520
+ ...(toolUsageState.error == null
521
+ ? {}
522
+ : { toolMessageUsageError: toolUsageState.error }),
455
523
  };
456
524
  };
457
525