@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
@@ -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)) {
@@ -0,0 +1,209 @@
1
+ import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import type {
3
+ ToolApprovalInterruptPayload,
4
+ ToolApprovalRequest,
5
+ ToolApprovalReviewConfig,
6
+ } from '@/types/hitl';
7
+ import { stableStringify } from '@/tools/eagerEventExecution';
8
+ import { isToolApprovalInterrupt } from '@/types/hitl';
9
+
10
+ /**
11
+ * Private resume-only config entry populated from the checkpointed interrupt.
12
+ * Hosts must never need to construct or inspect this value.
13
+ */
14
+ export const TOOL_APPROVAL_REVIEW_CONFIG_KEY =
15
+ '__librechat_tool_approval_review';
16
+
17
+ export interface ToolApprovalReviewEvidence {
18
+ interruptId: string;
19
+ payload: ToolApprovalInterruptPayload;
20
+ owner?: string;
21
+ }
22
+
23
+ export interface ReviewedToolApproval {
24
+ request: ToolApprovalRequest;
25
+ reviewConfig: ToolApprovalReviewConfig;
26
+ }
27
+
28
+ const APPROVAL_DECISIONS = new Set(['approve', 'reject', 'edit', 'respond']);
29
+
30
+ /** Detach approval payloads from host- or transport-owned object graphs. */
31
+ export function cloneToolApprovalInterruptPayload<T>(payload: T): T {
32
+ return isToolApprovalInterrupt(payload) ? structuredClone(payload) : payload;
33
+ }
34
+
35
+ function isRecord(value: unknown): value is Record<string, unknown> {
36
+ return value != null && typeof value === 'object' && !Array.isArray(value);
37
+ }
38
+
39
+ function hasValidApprovalShape(payload: ToolApprovalInterruptPayload): boolean {
40
+ if (
41
+ !Array.isArray(payload.action_requests) ||
42
+ !Array.isArray(payload.review_configs) ||
43
+ payload.action_requests.length !== payload.review_configs.length
44
+ ) {
45
+ return false;
46
+ }
47
+
48
+ const toolCallIds = new Set<string>();
49
+ return payload.action_requests.every((request, index) => {
50
+ const reviewConfig = payload.review_configs[index];
51
+ if (!isRecord(request) || !isRecord(reviewConfig)) {
52
+ return false;
53
+ }
54
+ const toolCallId = request.tool_call_id;
55
+ const toolName = request.name;
56
+ const allowedDecisions = reviewConfig.allowed_decisions;
57
+ if (
58
+ typeof toolCallId !== 'string' ||
59
+ toolCallId.length === 0 ||
60
+ toolCallIds.has(toolCallId) ||
61
+ typeof toolName !== 'string' ||
62
+ toolName.length === 0 ||
63
+ !isRecord(request.arguments) ||
64
+ (request.description != null &&
65
+ typeof request.description !== 'string') ||
66
+ reviewConfig.tool_call_id !== toolCallId ||
67
+ reviewConfig.action_name !== toolName ||
68
+ !Array.isArray(allowedDecisions) ||
69
+ !allowedDecisions.every(
70
+ (decision) =>
71
+ typeof decision === 'string' && APPROVAL_DECISIONS.has(decision)
72
+ )
73
+ ) {
74
+ return false;
75
+ }
76
+ toolCallIds.add(toolCallId);
77
+ return true;
78
+ });
79
+ }
80
+
81
+ /** Build trusted review evidence from the interrupt restored by `Run`. */
82
+ export function createToolApprovalReviewEvidence(
83
+ interruptId: string | undefined,
84
+ payload: unknown,
85
+ owner?: string
86
+ ): ToolApprovalReviewEvidence | undefined {
87
+ if (
88
+ typeof interruptId !== 'string' ||
89
+ interruptId.length === 0 ||
90
+ !isToolApprovalInterrupt(payload) ||
91
+ !hasValidApprovalShape(payload)
92
+ ) {
93
+ return undefined;
94
+ }
95
+ return {
96
+ interruptId,
97
+ payload: cloneToolApprovalInterruptPayload(payload),
98
+ ...(owner == null ? {} : { owner }),
99
+ };
100
+ }
101
+
102
+ /** Read only well-shaped evidence from a ToolNode's runnable config. */
103
+ export function getToolApprovalReviewEvidence(
104
+ config: RunnableConfig,
105
+ owner?: string
106
+ ): ToolApprovalReviewEvidence | undefined {
107
+ const candidate = config.configurable?.[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
108
+ if (candidate == null || typeof candidate !== 'object') {
109
+ return undefined;
110
+ }
111
+ const {
112
+ interruptId,
113
+ payload,
114
+ owner: evidenceOwner,
115
+ } = candidate as {
116
+ interruptId?: unknown;
117
+ payload?: unknown;
118
+ owner?: unknown;
119
+ };
120
+ if (
121
+ evidenceOwner != null &&
122
+ (typeof evidenceOwner !== 'string' ||
123
+ (owner != null && owner !== evidenceOwner))
124
+ ) {
125
+ return undefined;
126
+ }
127
+ return createToolApprovalReviewEvidence(
128
+ typeof interruptId === 'string' ? interruptId : undefined,
129
+ payload,
130
+ typeof evidenceOwner === 'string' ? evidenceOwner : undefined
131
+ );
132
+ }
133
+
134
+ export function getReviewedToolApproval(
135
+ payload: ToolApprovalInterruptPayload | undefined,
136
+ toolCallId: string | undefined
137
+ ): ReviewedToolApproval | undefined {
138
+ if (payload == null || toolCallId == null || toolCallId === '') {
139
+ return undefined;
140
+ }
141
+ const request = payload.action_requests.find(
142
+ (candidate) => candidate.tool_call_id === toolCallId
143
+ );
144
+ const reviewConfig = payload.review_configs.find(
145
+ (candidate) => candidate.tool_call_id === toolCallId
146
+ );
147
+ if (request == null || reviewConfig == null) {
148
+ return undefined;
149
+ }
150
+ return { request, reviewConfig };
151
+ }
152
+
153
+ function decisionsEqual(
154
+ left: ReadonlyArray<string>,
155
+ right: ReadonlyArray<string>
156
+ ): boolean {
157
+ if (left.length !== right.length) {
158
+ return false;
159
+ }
160
+ const sortedLeft = [...left].sort();
161
+ const sortedRight = [...right].sort();
162
+ return sortedLeft.every((value, index) => value === sortedRight[index]);
163
+ }
164
+
165
+ /**
166
+ * Bind a resume decision to the exact proposal the reviewer saw. Description
167
+ * text is deliberately excluded: it explains policy but cannot change the
168
+ * side effect. Tool identity, normalized arguments and available decisions
169
+ * are execution-authoritative.
170
+ */
171
+ export function toolApprovalProposalMatches(
172
+ current: ReviewedToolApproval,
173
+ reviewed: ReviewedToolApproval
174
+ ): boolean {
175
+ return (
176
+ current.request.tool_call_id === reviewed.request.tool_call_id &&
177
+ current.request.name === reviewed.request.name &&
178
+ current.reviewConfig.tool_call_id === reviewed.reviewConfig.tool_call_id &&
179
+ current.reviewConfig.action_name === reviewed.reviewConfig.action_name &&
180
+ stableStringify(current.request.arguments) ===
181
+ stableStringify(reviewed.request.arguments) &&
182
+ decisionsEqual(
183
+ current.reviewConfig.allowed_decisions,
184
+ reviewed.reviewConfig.allowed_decisions
185
+ )
186
+ );
187
+ }
188
+
189
+ /** Preserve batch order as part of the decision-to-request binding. */
190
+ export function toolApprovalPayloadMatches(
191
+ current: ToolApprovalInterruptPayload,
192
+ reviewed: ToolApprovalInterruptPayload
193
+ ): boolean {
194
+ if (
195
+ current.action_requests.length !== reviewed.action_requests.length ||
196
+ current.review_configs.length !== reviewed.review_configs.length
197
+ ) {
198
+ return false;
199
+ }
200
+ return current.action_requests.every((request, index) => {
201
+ const currentReviewConfig = current.review_configs[index];
202
+ const reviewedRequest = reviewed.action_requests[index];
203
+ const reviewedReviewConfig = reviewed.review_configs[index];
204
+ return toolApprovalProposalMatches(
205
+ { request, reviewConfig: currentReviewConfig },
206
+ { request: reviewedRequest, reviewConfig: reviewedReviewConfig }
207
+ );
208
+ });
209
+ }
@@ -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);
@@ -1023,6 +1026,61 @@ export function isLangfuseCallbackHandler(value: unknown): boolean {
1023
1026
  return value instanceof CallbackHandler;
1024
1027
  }
1025
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
+
1026
1084
  export async function disposeLangfuseHandler(value: unknown): Promise<void> {
1027
1085
  if (value instanceof ScopedLangfuseCallbackHandler) {
1028
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