@librechat/agents 3.3.2 → 3.3.4

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 (162) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +342 -109
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +6 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  14. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  15. package/dist/cjs/llm/invoke.cjs +64 -3
  16. package/dist/cjs/llm/invoke.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/index.cjs +41 -5
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  20. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  21. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  22. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  23. package/dist/cjs/main.cjs +19 -2
  24. package/dist/cjs/messages/cache.cjs +54 -33
  25. package/dist/cjs/messages/cache.cjs.map +1 -1
  26. package/dist/cjs/messages/contextPruning.cjs +17 -43
  27. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  28. package/dist/cjs/messages/core.cjs +315 -23
  29. package/dist/cjs/messages/core.cjs.map +1 -1
  30. package/dist/cjs/messages/format.cjs +218 -60
  31. package/dist/cjs/messages/format.cjs.map +1 -1
  32. package/dist/cjs/messages/index.cjs +1 -1
  33. package/dist/cjs/messages/prune.cjs +503 -127
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/stream.cjs +7 -3
  36. package/dist/cjs/stream.cjs.map +1 -1
  37. package/dist/cjs/summarization/node.cjs +28 -14
  38. package/dist/cjs/summarization/node.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +54 -41
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  42. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  43. package/dist/cjs/utils/index.cjs +1 -1
  44. package/dist/cjs/utils/llm.cjs +1 -1
  45. package/dist/cjs/utils/llm.cjs.map +1 -1
  46. package/dist/cjs/utils/tokens.cjs +307 -20
  47. package/dist/cjs/utils/tokens.cjs.map +1 -1
  48. package/dist/cjs/utils/toolContent.cjs +1514 -0
  49. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  50. package/dist/esm/agents/AgentContext.mjs +2 -1
  51. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  52. package/dist/esm/graphs/Graph.mjs +349 -116
  53. package/dist/esm/graphs/Graph.mjs.map +1 -1
  54. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  55. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  56. package/dist/esm/langfuse.mjs +6 -0
  57. package/dist/esm/langfuse.mjs.map +1 -1
  58. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  59. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  60. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
  61. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  62. package/dist/esm/llm/google/utils/common.mjs +19 -7
  63. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  64. package/dist/esm/llm/invoke.mjs +65 -5
  65. package/dist/esm/llm/invoke.mjs.map +1 -1
  66. package/dist/esm/llm/openai/index.mjs +41 -5
  67. package/dist/esm/llm/openai/index.mjs.map +1 -1
  68. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  69. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  70. package/dist/esm/llm/openrouter/index.mjs +4 -1
  71. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  72. package/dist/esm/main.mjs +7 -7
  73. package/dist/esm/messages/cache.mjs +55 -34
  74. package/dist/esm/messages/cache.mjs.map +1 -1
  75. package/dist/esm/messages/contextPruning.mjs +17 -43
  76. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  77. package/dist/esm/messages/core.mjs +305 -24
  78. package/dist/esm/messages/core.mjs.map +1 -1
  79. package/dist/esm/messages/format.mjs +218 -61
  80. package/dist/esm/messages/format.mjs.map +1 -1
  81. package/dist/esm/messages/index.mjs +1 -1
  82. package/dist/esm/messages/prune.mjs +500 -128
  83. package/dist/esm/messages/prune.mjs.map +1 -1
  84. package/dist/esm/stream.mjs +7 -3
  85. package/dist/esm/stream.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +28 -14
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +54 -41
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  91. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  92. package/dist/esm/utils/index.mjs +1 -1
  93. package/dist/esm/utils/llm.mjs +1 -1
  94. package/dist/esm/utils/llm.mjs.map +1 -1
  95. package/dist/esm/utils/tokens.mjs +307 -21
  96. package/dist/esm/utils/tokens.mjs.map +1 -1
  97. package/dist/esm/utils/toolContent.mjs +1503 -0
  98. package/dist/esm/utils/toolContent.mjs.map +1 -0
  99. package/dist/types/graphs/Graph.d.ts +1 -1
  100. package/dist/types/llm/invoke.d.ts +27 -1
  101. package/dist/types/llm/openai/index.d.ts +4 -0
  102. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  103. package/dist/types/llm/openrouter/index.d.ts +5 -0
  104. package/dist/types/messages/contextPruning.d.ts +2 -1
  105. package/dist/types/messages/core.d.ts +36 -0
  106. package/dist/types/messages/format.d.ts +6 -0
  107. package/dist/types/messages/prune.d.ts +16 -12
  108. package/dist/types/types/stream.d.ts +1 -1
  109. package/dist/types/utils/tokens.d.ts +7 -0
  110. package/dist/types/utils/toolContent.d.ts +107 -0
  111. package/package.json +13 -9
  112. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  113. package/src/agents/AgentContext.ts +1 -0
  114. package/src/graphs/Graph.ts +698 -200
  115. package/src/graphs/MultiAgentGraph.ts +6 -1
  116. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
  117. package/src/langfuse.ts +15 -0
  118. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  119. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  120. package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
  121. package/src/llm/bedrock/utils/message_inputs.ts +86 -13
  122. package/src/llm/google/utils/common.test.ts +75 -9
  123. package/src/llm/google/utils/common.ts +34 -3
  124. package/src/llm/invoke.test.ts +488 -0
  125. package/src/llm/invoke.ts +196 -2
  126. package/src/llm/openai/index.ts +106 -6
  127. package/src/llm/openai/utils/index.ts +109 -53
  128. package/src/llm/openai/utils/messages.test.ts +330 -1
  129. package/src/llm/openrouter/index.ts +12 -2
  130. package/src/llm/openrouter/reasoning.test.ts +314 -0
  131. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  132. package/src/messages/cache.tail.test.ts +193 -0
  133. package/src/messages/cache.test.ts +113 -0
  134. package/src/messages/cache.ts +92 -60
  135. package/src/messages/contextPruning.test.ts +184 -0
  136. package/src/messages/contextPruning.ts +49 -42
  137. package/src/messages/core.ts +653 -40
  138. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  139. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  140. package/src/messages/format.ts +363 -91
  141. package/src/messages/formatAgentMessages.test.ts +604 -14
  142. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  143. package/src/messages/prune.ts +996 -183
  144. package/src/specs/langfuse-callbacks.test.ts +203 -1
  145. package/src/specs/prune.test.ts +1083 -6
  146. package/src/specs/summarization-unit.test.ts +105 -0
  147. package/src/specs/tokens.test.ts +609 -32
  148. package/src/stream.ts +23 -15
  149. package/src/summarization/__tests__/node.test.ts +77 -0
  150. package/src/summarization/node.ts +46 -18
  151. package/src/tools/ToolNode.ts +147 -68
  152. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  153. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  154. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  155. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  156. package/src/tools/toolOutputReferences.ts +6 -0
  157. package/src/types/stream.ts +1 -1
  158. package/src/utils/llm.test.ts +18 -0
  159. package/src/utils/llm.ts +4 -1
  160. package/src/utils/tokens.ts +586 -31
  161. package/src/utils/toolContent.test.ts +1432 -0
  162. package/src/utils/toolContent.ts +2707 -0
@@ -755,6 +755,54 @@ describe('ensureThinkingBlockInMessages', () => {
755
755
  expect(result[0]).toBeInstanceOf(HumanMessage);
756
756
  expect(result[1]).toBeInstanceOf(ToolMessage);
757
757
  });
758
+
759
+ test('should not emit an empty user message after the shared fold budget is exhausted', () => {
760
+ const messages = [
761
+ new HumanMessage({ content: 'Run both tools' }),
762
+ new AIMessage({
763
+ content: '',
764
+ tool_calls: [
765
+ { id: 'first', name: 'query', args: {}, type: 'tool_call' },
766
+ ],
767
+ }),
768
+ new ToolMessage({
769
+ content: Array.from({ length: 60 }, () => ({
770
+ type: 'text',
771
+ text: 'x'.repeat(8_000),
772
+ })),
773
+ tool_call_id: 'first',
774
+ }),
775
+ new AIMessage({
776
+ content: '',
777
+ tool_calls: [
778
+ { id: 'second', name: 'query', args: {}, type: 'tool_call' },
779
+ ],
780
+ }),
781
+ new ToolMessage({
782
+ content: 'second result',
783
+ tool_call_id: 'second',
784
+ }),
785
+ ];
786
+
787
+ const result = ensureThinkingBlockInMessages(
788
+ messages,
789
+ Providers.ANTHROPIC
790
+ );
791
+
792
+ expect(result).toHaveLength(2);
793
+ expect(getTextContent(result[1])).toContain(
794
+ 'additional folded context omitted'
795
+ );
796
+ expect(
797
+ result
798
+ .filter((message) => message instanceof HumanMessage)
799
+ .every((message) =>
800
+ typeof message.content === 'string'
801
+ ? message.content.length > 0
802
+ : message.content.length > 0
803
+ )
804
+ ).toBe(true);
805
+ });
758
806
  });
759
807
 
760
808
  describe('image content preservation (token amplification fix)', () => {
@@ -1132,7 +1180,7 @@ describe('ensureThinkingBlockInMessages', () => {
1132
1180
 
1133
1181
  expect(result).toHaveLength(2);
1134
1182
  const allText = getTextContent(result[1]);
1135
- // The resource block should be serialized as text, not silently dropped
1183
+ // The resource block should be serialized as text, not silently dropped.
1136
1184
  expect(allText).toContain('[resource]');
1137
1185
  expect(allText).toContain('data.csv');
1138
1186
  });
@@ -6,7 +6,11 @@ import {
6
6
  ToolMessage,
7
7
  } from '@langchain/core/messages';
8
8
  import type { ExtendedMessageContent } from '@/types';
9
- import { foldToolBlocksForToollessAgent } from './format';
9
+ import {
10
+ foldToolBlocksForToollessAgent,
11
+ isSyntheticProviderContextMessage,
12
+ } from './format';
13
+ import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
10
14
 
11
15
  /** Concatenated text across a message's content (string or structured array). */
12
16
  function getTextContent(msg: {
@@ -96,6 +100,12 @@ describe('foldToolBlocksForToollessAgent', () => {
96
100
  expect(folded).toContain('file_search');
97
101
  expect(folded).toContain('roadmap');
98
102
  expect(folded).toContain('Found roadmap.md');
103
+ expect(isSyntheticProviderContextMessage(result[1])).toBe(true);
104
+ expect(
105
+ isSyntheticProviderContextMessage(
106
+ new HumanMessage('[Previous tool interaction] user-authored text')
107
+ )
108
+ ).toBe(false);
99
109
  });
100
110
 
101
111
  test('folds historical tool content that precedes the last human turn (the reported bug)', () => {
@@ -338,7 +348,11 @@ describe('foldToolBlocksForToollessAgent', () => {
338
348
  }),
339
349
  new HumanMessage({
340
350
  content: [
341
- { type: 'tool_result', tool_use_id: 'c1', content: 'Found roadmap.md' },
351
+ {
352
+ type: 'tool_result',
353
+ tool_use_id: 'c1',
354
+ content: 'Found roadmap.md',
355
+ },
342
356
  ],
343
357
  }),
344
358
  new HumanMessage('thanks'),
@@ -393,9 +407,7 @@ describe('foldToolBlocksForToollessAgent', () => {
393
407
  new HumanMessage('Render a chart'),
394
408
  new AIMessage({
395
409
  content: '',
396
- tool_calls: [
397
- { id: 'c', name: 'chart', args: {}, type: 'tool_call' },
398
- ],
410
+ tool_calls: [{ id: 'c', name: 'chart', args: {}, type: 'tool_call' }],
399
411
  }),
400
412
  new ToolMessage({
401
413
  content: [
@@ -422,6 +434,152 @@ describe('foldToolBlocksForToollessAgent', () => {
422
434
  ).toBe(true);
423
435
  });
424
436
 
437
+ test('bounds non-portable media blocks instead of preserving or JSON-expanding them', () => {
438
+ const blob = 'A'.repeat(20_000);
439
+ const resource = {
440
+ type: 'resource',
441
+ resource: { uri: 'file:///report.bin', blob },
442
+ };
443
+ const messages = [
444
+ new HumanMessage('Read the report'),
445
+ new AIMessage({
446
+ content: '',
447
+ tool_calls: [
448
+ { id: 'doc', name: 'read_document', args: {}, type: 'tool_call' },
449
+ ],
450
+ }),
451
+ new ToolMessage({
452
+ content: [{ type: 'text', text: 'report:' }, resource],
453
+ tool_call_id: 'doc',
454
+ name: 'read_document',
455
+ }),
456
+ ];
457
+
458
+ const result = foldToolBlocksForToollessAgent(messages);
459
+ const foldedContent = result[result.length - 1].content;
460
+ const foldedText = getTextContent(result[result.length - 1]);
461
+
462
+ expect(
463
+ Array.isArray(foldedContent) &&
464
+ foldedContent.some(
465
+ (block) => typeof block === 'object' && block.type === 'resource'
466
+ )
467
+ ).toBe(false);
468
+ expect(foldedText).toContain('[resource]');
469
+ expect(foldedText).toContain('report.bin');
470
+ expect(foldedText.length).toBeLessThan(9_000);
471
+ expect(foldedText).not.toContain(blob);
472
+ });
473
+
474
+ test('does not invoke toJSON while folding a small non-portable media block', () => {
475
+ let toJSONCalls = 0;
476
+ const resource = {
477
+ type: 'resource',
478
+ resource: { uri: 'file:///report.bin', blob: 'AAAA' },
479
+ toJSON() {
480
+ toJSONCalls++;
481
+ return { expanded: 'x'.repeat(100_000) };
482
+ },
483
+ };
484
+ const messages = [
485
+ new HumanMessage('Read the report'),
486
+ new AIMessage({
487
+ content: '',
488
+ tool_calls: [
489
+ { id: 'doc', name: 'read_document', args: {}, type: 'tool_call' },
490
+ ],
491
+ }),
492
+ new ToolMessage({
493
+ content: [resource],
494
+ tool_call_id: 'doc',
495
+ name: 'read_document',
496
+ }),
497
+ ];
498
+
499
+ const result = foldToolBlocksForToollessAgent(messages);
500
+ const foldedText = getTextContent(result[result.length - 1]);
501
+
502
+ expect(toJSONCalls).toBe(0);
503
+ expect(foldedText).toContain('report.bin');
504
+ expect(foldedText.length).toBeLessThan(9_000);
505
+ expect(foldedText).not.toContain('x'.repeat(1_000));
506
+ });
507
+
508
+ test('bounds the aggregate folded context across many large text blocks', () => {
509
+ const messages = [
510
+ new HumanMessage('Summarize the query'),
511
+ new AIMessage({
512
+ content: '',
513
+ tool_calls: [
514
+ { id: 'query', name: 'run_query', args: {}, type: 'tool_call' },
515
+ ],
516
+ }),
517
+ new ToolMessage({
518
+ content: Array.from({ length: 100 }, (_, index) => ({
519
+ type: 'text',
520
+ text: `${index}:${'x'.repeat(8_000)}`,
521
+ })),
522
+ tool_call_id: 'query',
523
+ name: 'run_query',
524
+ }),
525
+ ];
526
+
527
+ const result = foldToolBlocksForToollessAgent(messages);
528
+ const foldedText = getTextContent(result[result.length - 1]);
529
+
530
+ expect(foldedText.length).toBeLessThanOrEqual(HARD_MAX_TOOL_RESULT_CHARS);
531
+ expect(foldedText).toContain('additional folded context omitted');
532
+ });
533
+
534
+ test('does not emit an empty user message when an earlier fold exhausts the shared budget', () => {
535
+ const messages = [
536
+ new HumanMessage('Run both queries'),
537
+ new AIMessage({
538
+ content: '',
539
+ tool_calls: [
540
+ { id: 'first', name: 'run_query', args: {}, type: 'tool_call' },
541
+ ],
542
+ }),
543
+ new ToolMessage({
544
+ content: Array.from({ length: 60 }, () => ({
545
+ type: 'text',
546
+ text: 'x'.repeat(8_000),
547
+ })),
548
+ tool_call_id: 'first',
549
+ name: 'run_query',
550
+ }),
551
+ new AIMessage({
552
+ content: '',
553
+ tool_calls: [
554
+ { id: 'second', name: 'run_query', args: {}, type: 'tool_call' },
555
+ ],
556
+ }),
557
+ new ToolMessage({
558
+ content: 'second result',
559
+ tool_call_id: 'second',
560
+ name: 'run_query',
561
+ }),
562
+ ];
563
+
564
+ const result = foldToolBlocksForToollessAgent(messages);
565
+ const syntheticMessages = result.filter(isSyntheticProviderContextMessage);
566
+
567
+ expect(hasResidualToolContent(result)).toBe(false);
568
+ expect(syntheticMessages).toHaveLength(1);
569
+ expect(getTextContent(syntheticMessages[0])).toContain(
570
+ 'additional folded context omitted'
571
+ );
572
+ expect(
573
+ result
574
+ .filter((message) => message instanceof HumanMessage)
575
+ .every((message) =>
576
+ typeof message.content === 'string'
577
+ ? message.content.length > 0
578
+ : message.content.length > 0
579
+ )
580
+ ).toBe(true);
581
+ });
582
+
425
583
  test('leaves non-tool conversations untouched', () => {
426
584
  const messages = [
427
585
  new SystemMessage('sys'),