@librechat/agents 3.1.57 → 3.1.61

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 (214) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +326 -62
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +13 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +7 -27
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +303 -222
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -4
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +6 -2
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/init.cjs +60 -0
  14. package/dist/cjs/llm/init.cjs.map +1 -0
  15. package/dist/cjs/llm/invoke.cjs +90 -0
  16. package/dist/cjs/llm/invoke.cjs.map +1 -0
  17. package/dist/cjs/llm/openai/index.cjs +2 -0
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/request.cjs +41 -0
  20. package/dist/cjs/llm/request.cjs.map +1 -0
  21. package/dist/cjs/main.cjs +40 -0
  22. package/dist/cjs/main.cjs.map +1 -1
  23. package/dist/cjs/messages/cache.cjs +76 -89
  24. package/dist/cjs/messages/cache.cjs.map +1 -1
  25. package/dist/cjs/messages/contextPruning.cjs +156 -0
  26. package/dist/cjs/messages/contextPruning.cjs.map +1 -0
  27. package/dist/cjs/messages/contextPruningSettings.cjs +53 -0
  28. package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -0
  29. package/dist/cjs/messages/core.cjs +23 -37
  30. package/dist/cjs/messages/core.cjs.map +1 -1
  31. package/dist/cjs/messages/format.cjs +156 -11
  32. package/dist/cjs/messages/format.cjs.map +1 -1
  33. package/dist/cjs/messages/prune.cjs +1161 -49
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/messages/reducer.cjs +87 -0
  36. package/dist/cjs/messages/reducer.cjs.map +1 -0
  37. package/dist/cjs/run.cjs +81 -42
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +54 -7
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/summarization/index.cjs +75 -0
  42. package/dist/cjs/summarization/index.cjs.map +1 -0
  43. package/dist/cjs/summarization/node.cjs +663 -0
  44. package/dist/cjs/summarization/node.cjs.map +1 -0
  45. package/dist/cjs/tools/ToolNode.cjs +16 -8
  46. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  47. package/dist/cjs/tools/handlers.cjs +2 -0
  48. package/dist/cjs/tools/handlers.cjs.map +1 -1
  49. package/dist/cjs/utils/errors.cjs +115 -0
  50. package/dist/cjs/utils/errors.cjs.map +1 -0
  51. package/dist/cjs/utils/events.cjs +17 -0
  52. package/dist/cjs/utils/events.cjs.map +1 -1
  53. package/dist/cjs/utils/handlers.cjs +16 -0
  54. package/dist/cjs/utils/handlers.cjs.map +1 -1
  55. package/dist/cjs/utils/llm.cjs +10 -0
  56. package/dist/cjs/utils/llm.cjs.map +1 -1
  57. package/dist/cjs/utils/tokens.cjs +247 -14
  58. package/dist/cjs/utils/tokens.cjs.map +1 -1
  59. package/dist/cjs/utils/truncation.cjs +107 -0
  60. package/dist/cjs/utils/truncation.cjs.map +1 -0
  61. package/dist/esm/agents/AgentContext.mjs +325 -61
  62. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  63. package/dist/esm/common/enum.mjs +13 -0
  64. package/dist/esm/common/enum.mjs.map +1 -1
  65. package/dist/esm/events.mjs +8 -28
  66. package/dist/esm/events.mjs.map +1 -1
  67. package/dist/esm/graphs/Graph.mjs +307 -226
  68. package/dist/esm/graphs/Graph.mjs.map +1 -1
  69. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -4
  70. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  71. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +6 -2
  72. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  73. package/dist/esm/llm/init.mjs +58 -0
  74. package/dist/esm/llm/init.mjs.map +1 -0
  75. package/dist/esm/llm/invoke.mjs +87 -0
  76. package/dist/esm/llm/invoke.mjs.map +1 -0
  77. package/dist/esm/llm/openai/index.mjs +2 -0
  78. package/dist/esm/llm/openai/index.mjs.map +1 -1
  79. package/dist/esm/llm/request.mjs +38 -0
  80. package/dist/esm/llm/request.mjs.map +1 -0
  81. package/dist/esm/main.mjs +13 -3
  82. package/dist/esm/main.mjs.map +1 -1
  83. package/dist/esm/messages/cache.mjs +76 -89
  84. package/dist/esm/messages/cache.mjs.map +1 -1
  85. package/dist/esm/messages/contextPruning.mjs +154 -0
  86. package/dist/esm/messages/contextPruning.mjs.map +1 -0
  87. package/dist/esm/messages/contextPruningSettings.mjs +50 -0
  88. package/dist/esm/messages/contextPruningSettings.mjs.map +1 -0
  89. package/dist/esm/messages/core.mjs +23 -37
  90. package/dist/esm/messages/core.mjs.map +1 -1
  91. package/dist/esm/messages/format.mjs +156 -11
  92. package/dist/esm/messages/format.mjs.map +1 -1
  93. package/dist/esm/messages/prune.mjs +1158 -52
  94. package/dist/esm/messages/prune.mjs.map +1 -1
  95. package/dist/esm/messages/reducer.mjs +83 -0
  96. package/dist/esm/messages/reducer.mjs.map +1 -0
  97. package/dist/esm/run.mjs +82 -43
  98. package/dist/esm/run.mjs.map +1 -1
  99. package/dist/esm/stream.mjs +54 -7
  100. package/dist/esm/stream.mjs.map +1 -1
  101. package/dist/esm/summarization/index.mjs +73 -0
  102. package/dist/esm/summarization/index.mjs.map +1 -0
  103. package/dist/esm/summarization/node.mjs +659 -0
  104. package/dist/esm/summarization/node.mjs.map +1 -0
  105. package/dist/esm/tools/ToolNode.mjs +16 -8
  106. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  107. package/dist/esm/tools/handlers.mjs +2 -0
  108. package/dist/esm/tools/handlers.mjs.map +1 -1
  109. package/dist/esm/utils/errors.mjs +111 -0
  110. package/dist/esm/utils/errors.mjs.map +1 -0
  111. package/dist/esm/utils/events.mjs +17 -1
  112. package/dist/esm/utils/events.mjs.map +1 -1
  113. package/dist/esm/utils/handlers.mjs +16 -0
  114. package/dist/esm/utils/handlers.mjs.map +1 -1
  115. package/dist/esm/utils/llm.mjs +10 -1
  116. package/dist/esm/utils/llm.mjs.map +1 -1
  117. package/dist/esm/utils/tokens.mjs +245 -15
  118. package/dist/esm/utils/tokens.mjs.map +1 -1
  119. package/dist/esm/utils/truncation.mjs +102 -0
  120. package/dist/esm/utils/truncation.mjs.map +1 -0
  121. package/dist/types/agents/AgentContext.d.ts +124 -6
  122. package/dist/types/common/enum.d.ts +14 -1
  123. package/dist/types/graphs/Graph.d.ts +22 -27
  124. package/dist/types/index.d.ts +5 -0
  125. package/dist/types/llm/init.d.ts +18 -0
  126. package/dist/types/llm/invoke.d.ts +48 -0
  127. package/dist/types/llm/request.d.ts +14 -0
  128. package/dist/types/messages/contextPruning.d.ts +42 -0
  129. package/dist/types/messages/contextPruningSettings.d.ts +44 -0
  130. package/dist/types/messages/core.d.ts +1 -1
  131. package/dist/types/messages/format.d.ts +17 -1
  132. package/dist/types/messages/index.d.ts +3 -0
  133. package/dist/types/messages/prune.d.ts +162 -1
  134. package/dist/types/messages/reducer.d.ts +18 -0
  135. package/dist/types/run.d.ts +12 -1
  136. package/dist/types/summarization/index.d.ts +20 -0
  137. package/dist/types/summarization/node.d.ts +29 -0
  138. package/dist/types/tools/ToolNode.d.ts +3 -1
  139. package/dist/types/types/graph.d.ts +44 -6
  140. package/dist/types/types/index.d.ts +1 -0
  141. package/dist/types/types/run.d.ts +30 -0
  142. package/dist/types/types/stream.d.ts +31 -4
  143. package/dist/types/types/summarize.d.ts +47 -0
  144. package/dist/types/types/tools.d.ts +7 -0
  145. package/dist/types/utils/errors.d.ts +28 -0
  146. package/dist/types/utils/events.d.ts +13 -0
  147. package/dist/types/utils/index.d.ts +2 -0
  148. package/dist/types/utils/llm.d.ts +4 -0
  149. package/dist/types/utils/tokens.d.ts +14 -1
  150. package/dist/types/utils/truncation.d.ts +49 -0
  151. package/package.json +3 -3
  152. package/src/agents/AgentContext.ts +388 -58
  153. package/src/agents/__tests__/AgentContext.test.ts +265 -5
  154. package/src/common/enum.ts +13 -0
  155. package/src/events.ts +9 -39
  156. package/src/graphs/Graph.ts +468 -331
  157. package/src/index.ts +7 -0
  158. package/src/llm/anthropic/llm.spec.ts +3 -3
  159. package/src/llm/anthropic/utils/message_inputs.ts +6 -4
  160. package/src/llm/bedrock/llm.spec.ts +1 -1
  161. package/src/llm/bedrock/utils/message_inputs.ts +6 -2
  162. package/src/llm/init.ts +63 -0
  163. package/src/llm/invoke.ts +144 -0
  164. package/src/llm/request.ts +55 -0
  165. package/src/messages/__tests__/observationMasking.test.ts +221 -0
  166. package/src/messages/cache.ts +77 -102
  167. package/src/messages/contextPruning.ts +191 -0
  168. package/src/messages/contextPruningSettings.ts +90 -0
  169. package/src/messages/core.ts +32 -53
  170. package/src/messages/ensureThinkingBlock.test.ts +39 -39
  171. package/src/messages/format.ts +227 -15
  172. package/src/messages/formatAgentMessages.test.ts +511 -1
  173. package/src/messages/index.ts +3 -0
  174. package/src/messages/prune.ts +1548 -62
  175. package/src/messages/reducer.ts +22 -0
  176. package/src/run.ts +104 -51
  177. package/src/scripts/bedrock-merge-test.ts +1 -1
  178. package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
  179. package/src/scripts/test-thinking-handoff.ts +1 -1
  180. package/src/scripts/thinking-bedrock.ts +1 -1
  181. package/src/scripts/thinking.ts +1 -1
  182. package/src/specs/anthropic.simple.test.ts +1 -1
  183. package/src/specs/multi-agent-summarization.test.ts +396 -0
  184. package/src/specs/prune.test.ts +1196 -23
  185. package/src/specs/summarization-unit.test.ts +868 -0
  186. package/src/specs/summarization.test.ts +3827 -0
  187. package/src/specs/summarize-prune.test.ts +376 -0
  188. package/src/specs/thinking-handoff.test.ts +10 -10
  189. package/src/specs/thinking-prune.test.ts +7 -4
  190. package/src/specs/token-accounting-e2e.test.ts +1034 -0
  191. package/src/specs/token-accounting-pipeline.test.ts +882 -0
  192. package/src/specs/token-distribution-edge-case.test.ts +25 -26
  193. package/src/splitStream.test.ts +42 -33
  194. package/src/stream.ts +64 -11
  195. package/src/summarization/__tests__/aggregator.test.ts +153 -0
  196. package/src/summarization/__tests__/node.test.ts +708 -0
  197. package/src/summarization/__tests__/trigger.test.ts +50 -0
  198. package/src/summarization/index.ts +102 -0
  199. package/src/summarization/node.ts +982 -0
  200. package/src/tools/ToolNode.ts +25 -3
  201. package/src/types/graph.ts +62 -7
  202. package/src/types/index.ts +1 -0
  203. package/src/types/run.ts +32 -0
  204. package/src/types/stream.ts +45 -5
  205. package/src/types/summarize.ts +58 -0
  206. package/src/types/tools.ts +7 -0
  207. package/src/utils/errors.ts +117 -0
  208. package/src/utils/events.ts +31 -0
  209. package/src/utils/handlers.ts +18 -0
  210. package/src/utils/index.ts +2 -0
  211. package/src/utils/llm.ts +12 -0
  212. package/src/utils/tokens.ts +336 -18
  213. package/src/utils/truncation.ts +124 -0
  214. package/src/scripts/image.ts +0 -180
@@ -4,7 +4,7 @@ import {
4
4
  SystemMessage,
5
5
  ToolMessage,
6
6
  } from '@langchain/core/messages';
7
- import type { TPayload } from '@/types';
7
+ import type { MessageContentComplex, TPayload } from '@/types';
8
8
  import { formatAgentMessages } from './format';
9
9
  import { ContentTypes } from '@/common';
10
10
 
@@ -20,6 +20,41 @@ describe('formatAgentMessages', () => {
20
20
  expect(result.messages[1]).toBeInstanceOf(AIMessage);
21
21
  });
22
22
 
23
+ it('preserves source messageId on formatted messages', () => {
24
+ const payload: TPayload = [
25
+ {
26
+ role: 'assistant',
27
+ messageId: 'msg_assistant_1',
28
+ content: [
29
+ {
30
+ type: ContentTypes.TEXT,
31
+ [ContentTypes.TEXT]: 'Running tool',
32
+ tool_call_ids: ['tool_1'],
33
+ },
34
+ {
35
+ type: ContentTypes.TOOL_CALL,
36
+ tool_call: {
37
+ id: 'tool_1',
38
+ name: 'search',
39
+ args: '{"query":"hello"}',
40
+ output: 'world',
41
+ },
42
+ },
43
+ ],
44
+ },
45
+ { role: 'user', messageId: 'msg_user_1', content: 'thanks' },
46
+ ];
47
+
48
+ const result = formatAgentMessages(payload);
49
+ expect(result.messages).toHaveLength(3);
50
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
51
+ expect(result.messages[1]).toBeInstanceOf(ToolMessage);
52
+ expect(result.messages[2]).toBeInstanceOf(HumanMessage);
53
+ expect(result.messages[0].id).toBe('msg_assistant_1');
54
+ expect(result.messages[1].id).toBe('msg_assistant_1');
55
+ expect(result.messages[2].id).toBe('msg_user_1');
56
+ });
57
+
23
58
  it('should handle system messages', () => {
24
59
  const payload = [
25
60
  { role: 'system', content: 'You are a helpful assistant.' },
@@ -29,6 +64,83 @@ describe('formatAgentMessages', () => {
29
64
  expect(result.messages[0]).toBeInstanceOf(SystemMessage);
30
65
  });
31
66
 
67
+ it('should prepend the latest summary and trim context before its boundary', () => {
68
+ const payload: TPayload = [
69
+ { role: 'user', content: 'Old user message' },
70
+ { role: 'assistant', content: 'Old assistant message' },
71
+ {
72
+ role: 'assistant',
73
+ content: [
74
+ { type: ContentTypes.TEXT, text: 'Covered by summary' },
75
+ {
76
+ type: ContentTypes.SUMMARY,
77
+ text: 'Conversation summary',
78
+ tokenCount: 12,
79
+ },
80
+ { type: ContentTypes.TEXT, text: 'Preserved tail' },
81
+ ],
82
+ },
83
+ { role: 'user', content: 'Latest user message' },
84
+ ];
85
+
86
+ const result = formatAgentMessages(payload, {
87
+ 0: 5,
88
+ 1: 6,
89
+ 2: 18,
90
+ 3: 4,
91
+ });
92
+
93
+ expect(result.messages).toHaveLength(2);
94
+ expect(result.summary).toBeDefined();
95
+ expect(result.summary!.text).toBe('Conversation summary');
96
+ expect(result.summary!.tokenCount).toBe(12);
97
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
98
+ expect(result.messages[1]).toBeInstanceOf(HumanMessage);
99
+ expect(
100
+ (result.messages[0].content as MessageContentComplex[])[0]
101
+ ).toMatchObject({
102
+ type: ContentTypes.TEXT,
103
+ text: 'Preserved tail',
104
+ });
105
+ expect(result.indexTokenCountMap?.[0]).toBeLessThan(18);
106
+ expect(result.indexTokenCountMap?.[0]).toBeGreaterThan(0);
107
+ expect(result.indexTokenCountMap?.[1]).toBe(4);
108
+ });
109
+
110
+ it('should apply last-summary-wins when multiple summary blocks exist', () => {
111
+ const payload: TPayload = [
112
+ {
113
+ role: 'assistant',
114
+ content: [
115
+ { type: ContentTypes.SUMMARY, text: 'Old summary', tokenCount: 3 },
116
+ { type: ContentTypes.TEXT, text: 'Old tail' },
117
+ ],
118
+ },
119
+ {
120
+ role: 'assistant',
121
+ content: [
122
+ { type: ContentTypes.TEXT, text: 'Drop this part' },
123
+ { type: ContentTypes.SUMMARY, text: 'Newest summary', tokenCount: 9 },
124
+ { type: ContentTypes.TEXT, text: 'Keep this part' },
125
+ ],
126
+ },
127
+ ];
128
+
129
+ const result = formatAgentMessages(payload);
130
+
131
+ expect(result.messages).toHaveLength(1);
132
+ expect(result.summary).toBeDefined();
133
+ expect(result.summary!.text).toBe('Newest summary');
134
+ expect(result.summary!.tokenCount).toBe(9);
135
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
136
+ expect(
137
+ (result.messages[0].content as MessageContentComplex[])[0]
138
+ ).toMatchObject({
139
+ type: ContentTypes.TEXT,
140
+ text: 'Keep this part',
141
+ });
142
+ });
143
+
32
144
  it('should format messages with content arrays', () => {
33
145
  const payload = [
34
146
  {
@@ -2683,4 +2795,402 @@ describe('formatAgentMessages', () => {
2683
2795
  }).not.toThrow();
2684
2796
  });
2685
2797
  });
2798
+
2799
+ describe('summary boundary token count adjustment', () => {
2800
+ it('should proportion token count when thinking block is sliced off by boundary', () => {
2801
+ const thinkingText = 'x'.repeat(1000);
2802
+ const payload: TPayload = [
2803
+ { role: 'user', content: 'Old question' },
2804
+ {
2805
+ role: 'assistant',
2806
+ content: [
2807
+ { type: ContentTypes.THINKING, thinking: thinkingText },
2808
+ {
2809
+ type: ContentTypes.SUMMARY,
2810
+ text: 'Summary of conversation',
2811
+ tokenCount: 15,
2812
+ },
2813
+ { type: ContentTypes.TEXT, text: 'Brief response after summary' },
2814
+ ],
2815
+ },
2816
+ { role: 'user', content: 'Follow-up question' },
2817
+ ];
2818
+
2819
+ const indexTokenCountMap = { 0: 5, 1: 1590, 2: 8 };
2820
+ const result = formatAgentMessages(payload, indexTokenCountMap);
2821
+
2822
+ expect(result.summary).toBeDefined();
2823
+ expect(result.summary!.text).toBe('Summary of conversation');
2824
+
2825
+ const boundaryMsgTokens = result.indexTokenCountMap?.[0];
2826
+ expect(boundaryMsgTokens).toBeDefined();
2827
+ expect(boundaryMsgTokens!).toBeLessThan(200);
2828
+ expect(boundaryMsgTokens!).toBeGreaterThan(0);
2829
+
2830
+ expect(result.indexTokenCountMap?.[1]).toBe(8);
2831
+ });
2832
+
2833
+ it('should proportion token count when thinking + tool_use are sliced off', () => {
2834
+ const thinkingText = 'a'.repeat(800);
2835
+ const toolInput = JSON.stringify({ data: 'b'.repeat(400) });
2836
+ const payload: TPayload = [
2837
+ {
2838
+ role: 'assistant',
2839
+ content: [
2840
+ { type: ContentTypes.THINKING, thinking: thinkingText },
2841
+ {
2842
+ type: ContentTypes.TOOL_CALL,
2843
+ tool_call: {
2844
+ id: 'tc1',
2845
+ name: 'search',
2846
+ args: toolInput,
2847
+ output: 'result',
2848
+ },
2849
+ },
2850
+ {
2851
+ type: ContentTypes.SUMMARY,
2852
+ text: 'Conversation summary after tool use',
2853
+ tokenCount: 20,
2854
+ },
2855
+ { type: ContentTypes.TEXT, text: 'Short tail' },
2856
+ ],
2857
+ },
2858
+ ];
2859
+
2860
+ const indexTokenCountMap = { 0: 2000 };
2861
+ const result = formatAgentMessages(payload, indexTokenCountMap);
2862
+
2863
+ expect(result.summary).toBeDefined();
2864
+
2865
+ const totalOutputTokens = Object.values(
2866
+ result.indexTokenCountMap || {}
2867
+ ).reduce((sum, v) => sum + v, 0);
2868
+
2869
+ expect(totalOutputTokens).toBeLessThan(200);
2870
+ expect(totalOutputTokens).toBeGreaterThan(0);
2871
+ });
2872
+
2873
+ it('should roughly halve token count when content is evenly split around boundary', () => {
2874
+ const payload: TPayload = [
2875
+ {
2876
+ role: 'assistant',
2877
+ content: [
2878
+ { type: ContentTypes.TEXT, text: 'a'.repeat(100) },
2879
+ {
2880
+ type: ContentTypes.SUMMARY,
2881
+ text: 'Mid-conversation summary',
2882
+ tokenCount: 10,
2883
+ },
2884
+ { type: ContentTypes.TEXT, text: 'b'.repeat(100) },
2885
+ ],
2886
+ },
2887
+ ];
2888
+
2889
+ const indexTokenCountMap = { 0: 500 };
2890
+ const result = formatAgentMessages(payload, indexTokenCountMap);
2891
+
2892
+ expect(result.summary).toBeDefined();
2893
+
2894
+ const adjustedTokens = result.indexTokenCountMap?.[0] ?? 0;
2895
+ expect(adjustedTokens).toBeGreaterThan(150);
2896
+ expect(adjustedTokens).toBeLessThan(350);
2897
+ });
2898
+
2899
+ it('should still adjust when summary is the first content part (its own text is sliced off)', () => {
2900
+ const payload: TPayload = [
2901
+ {
2902
+ role: 'assistant',
2903
+ content: [
2904
+ {
2905
+ type: ContentTypes.SUMMARY,
2906
+ text: 'Summary at start',
2907
+ tokenCount: 10,
2908
+ },
2909
+ { type: ContentTypes.TEXT, text: 'Everything after the summary' },
2910
+ ],
2911
+ },
2912
+ { role: 'user', content: 'Next question' },
2913
+ ];
2914
+
2915
+ const indexTokenCountMap = { 0: 300, 1: 10 };
2916
+ const result = formatAgentMessages(payload, indexTokenCountMap);
2917
+
2918
+ expect(result.summary).toBeDefined();
2919
+
2920
+ const adjustedTokens = result.indexTokenCountMap?.[0] ?? 0;
2921
+ expect(adjustedTokens).toBeLessThan(300);
2922
+ expect(adjustedTokens).toBeGreaterThan(100);
2923
+ expect(result.indexTokenCountMap?.[1]).toBe(10);
2924
+ });
2925
+
2926
+ it('should account for tool_use input size in the char-length ratio', () => {
2927
+ const hugeInput = JSON.stringify({ payload: 'z'.repeat(5000) });
2928
+ const payload: TPayload = [
2929
+ {
2930
+ role: 'assistant',
2931
+ content: [
2932
+ {
2933
+ type: 'tool_use' as ContentTypes,
2934
+ input: hugeInput,
2935
+ } as unknown as MessageContentComplex,
2936
+ {
2937
+ type: ContentTypes.SUMMARY,
2938
+ text: 'After heavy tool use',
2939
+ tokenCount: 12,
2940
+ },
2941
+ { type: ContentTypes.TEXT, text: 'Tiny tail' },
2942
+ ],
2943
+ },
2944
+ ];
2945
+
2946
+ const indexTokenCountMap = { 0: 3000 };
2947
+ const result = formatAgentMessages(payload, indexTokenCountMap);
2948
+
2949
+ expect(result.summary).toBeDefined();
2950
+
2951
+ const adjustedTokens = result.indexTokenCountMap?.[0] ?? 0;
2952
+ expect(adjustedTokens).toBeLessThan(100);
2953
+ expect(adjustedTokens).toBeGreaterThan(0);
2954
+ });
2955
+
2956
+ it('should handle multiple content parts after the boundary', () => {
2957
+ const thinkingText = 'x'.repeat(2000);
2958
+ const payload: TPayload = [
2959
+ {
2960
+ role: 'assistant',
2961
+ content: [
2962
+ { type: ContentTypes.THINKING, thinking: thinkingText },
2963
+ {
2964
+ type: ContentTypes.SUMMARY,
2965
+ text: 'Conversation checkpoint',
2966
+ tokenCount: 14,
2967
+ },
2968
+ { type: ContentTypes.TEXT, text: 'Part A of the tail' },
2969
+ {
2970
+ type: ContentTypes.TEXT,
2971
+ text: 'Part B of the tail with more text',
2972
+ },
2973
+ ],
2974
+ },
2975
+ { role: 'user', content: 'Next message' },
2976
+ ];
2977
+
2978
+ const indexTokenCountMap = { 0: 4000, 1: 6 };
2979
+ const result = formatAgentMessages(payload, indexTokenCountMap);
2980
+
2981
+ expect(result.summary).toBeDefined();
2982
+
2983
+ const adjustedTokens = result.indexTokenCountMap?.[0] ?? 0;
2984
+ expect(adjustedTokens).toBeLessThan(200);
2985
+ expect(adjustedTokens).toBeGreaterThan(0);
2986
+
2987
+ expect(result.indexTokenCountMap?.[1]).toBe(6);
2988
+ });
2989
+
2990
+ it('should produce integer token counts after proportional adjustment', () => {
2991
+ const payload: TPayload = [
2992
+ {
2993
+ role: 'assistant',
2994
+ content: [
2995
+ { type: ContentTypes.THINKING, thinking: 'x'.repeat(333) },
2996
+ {
2997
+ type: ContentTypes.SUMMARY,
2998
+ text: 'Summary',
2999
+ tokenCount: 5,
3000
+ },
3001
+ { type: ContentTypes.TEXT, text: 'y'.repeat(77) },
3002
+ ],
3003
+ },
3004
+ ];
3005
+
3006
+ const indexTokenCountMap = { 0: 997 };
3007
+ const result = formatAgentMessages(payload, indexTokenCountMap);
3008
+
3009
+ const adjustedTokens = result.indexTokenCountMap?.[0];
3010
+ expect(adjustedTokens).toBeDefined();
3011
+ expect(Number.isInteger(adjustedTokens)).toBe(true);
3012
+ });
3013
+ });
3014
+
3015
+ describe('cross-run summary token accounting', () => {
3016
+ it('should conserve tokens: summary boundary excludes pre-boundary messages from the map', () => {
3017
+ const payload: TPayload = [
3018
+ { role: 'user', content: 'Old question' },
3019
+ { role: 'assistant', content: 'Old answer' },
3020
+ {
3021
+ role: 'assistant',
3022
+ content: [
3023
+ { type: ContentTypes.TEXT, text: 'Text before summary' },
3024
+ {
3025
+ type: ContentTypes.SUMMARY,
3026
+ text: 'This is a conversation summary capturing prior context.',
3027
+ tokenCount: 25,
3028
+ },
3029
+ { type: ContentTypes.TEXT, text: 'Text after summary' },
3030
+ ],
3031
+ },
3032
+ { role: 'user', content: 'New question after summary' },
3033
+ { role: 'assistant', content: 'New answer after summary' },
3034
+ ];
3035
+
3036
+ const indexTokenCountMap = {
3037
+ 0: 8,
3038
+ 1: 12,
3039
+ 2: 60,
3040
+ 3: 10,
3041
+ 4: 15,
3042
+ };
3043
+
3044
+ const result = formatAgentMessages(payload, indexTokenCountMap);
3045
+
3046
+ expect(result.summary).toBeDefined();
3047
+ expect(result.summary!.text).toBe(
3048
+ 'This is a conversation summary capturing prior context.'
3049
+ );
3050
+ expect(result.summary!.tokenCount).toBe(25);
3051
+
3052
+ const outputKeys = Object.keys(result.indexTokenCountMap || {}).map(
3053
+ Number
3054
+ );
3055
+ expect(outputKeys).toHaveLength(3);
3056
+
3057
+ const boundaryMsgTokens = result.indexTokenCountMap?.[0] ?? 0;
3058
+ expect(boundaryMsgTokens).toBeLessThan(60);
3059
+ expect(boundaryMsgTokens).toBeGreaterThan(0);
3060
+ expect(result.indexTokenCountMap?.[1]).toBe(10);
3061
+ expect(result.indexTokenCountMap?.[2]).toBe(15);
3062
+ });
3063
+
3064
+ it('should preserve summary token at index 0 when tool calls expand post-boundary messages', () => {
3065
+ const payload: TPayload = [
3066
+ { role: 'user', content: 'Summarized away' },
3067
+ {
3068
+ role: 'assistant',
3069
+ content: [
3070
+ {
3071
+ type: ContentTypes.SUMMARY,
3072
+ text: 'Summary of the conversation so far.',
3073
+ tokenCount: 20,
3074
+ },
3075
+ ],
3076
+ },
3077
+ {
3078
+ role: 'assistant',
3079
+ content: [
3080
+ {
3081
+ type: ContentTypes.TEXT,
3082
+ [ContentTypes.TEXT]: 'Let me compute that.',
3083
+ tool_call_ids: ['calc_1'],
3084
+ },
3085
+ {
3086
+ type: ContentTypes.TOOL_CALL,
3087
+ tool_call: {
3088
+ id: 'calc_1',
3089
+ name: 'calculator',
3090
+ args: '{"expr":"2+2"}',
3091
+ output: '4',
3092
+ },
3093
+ },
3094
+ {
3095
+ type: ContentTypes.TEXT,
3096
+ [ContentTypes.TEXT]: 'The answer is 4.',
3097
+ },
3098
+ ],
3099
+ },
3100
+ { role: 'user', content: 'Thanks!' },
3101
+ ];
3102
+
3103
+ const indexTokenCountMap = {
3104
+ 0: 5,
3105
+ 1: 30,
3106
+ 2: 80,
3107
+ 3: 6,
3108
+ };
3109
+
3110
+ const result = formatAgentMessages(payload, indexTokenCountMap);
3111
+
3112
+ expect(result.summary).toBeDefined();
3113
+ expect(result.summary!.text).toBe('Summary of the conversation so far.');
3114
+ expect(result.summary!.tokenCount).toBe(20);
3115
+
3116
+ const totalTokens = Object.values(result.indexTokenCountMap || {}).reduce(
3117
+ (sum, count) => sum + count,
3118
+ 0
3119
+ );
3120
+ expect(totalTokens).toBe(80 + 6);
3121
+ });
3122
+
3123
+ it('should produce correct maps across a simulated multi-run lifecycle', () => {
3124
+ const run1Payload: TPayload = [
3125
+ { role: 'user', content: 'What is 2+2?' },
3126
+ { role: 'assistant', content: 'The answer is 4.' },
3127
+ ];
3128
+ const run1Map = { 0: 10, 1: 12 };
3129
+
3130
+ const run1Result = formatAgentMessages(run1Payload, run1Map);
3131
+ expect(run1Result.messages).toHaveLength(2);
3132
+ expect(run1Result.indexTokenCountMap?.[0]).toBe(10);
3133
+ expect(run1Result.indexTokenCountMap?.[1]).toBe(12);
3134
+
3135
+ const run2Payload: TPayload = [
3136
+ ...run1Payload,
3137
+ { role: 'user', content: 'Now multiply 4 by 10.' },
3138
+ {
3139
+ role: 'assistant',
3140
+ content: [
3141
+ { type: ContentTypes.TEXT, text: 'Sure, the answer is 40.' },
3142
+ {
3143
+ type: ContentTypes.SUMMARY,
3144
+ text: 'User asked basic arithmetic: 2+2=4, then 4*10=40.',
3145
+ tokenCount: 18,
3146
+ },
3147
+ ],
3148
+ },
3149
+ ];
3150
+ const run2Map = { 0: 10, 1: 12, 2: 14, 3: 50 };
3151
+
3152
+ const run2Result = formatAgentMessages(run2Payload, run2Map);
3153
+ expect(run2Result.summary).toBeDefined();
3154
+ expect(run2Result.summary!.text).toBe(
3155
+ 'User asked basic arithmetic: 2+2=4, then 4*10=40.'
3156
+ );
3157
+ expect(run2Result.summary!.tokenCount).toBe(18);
3158
+
3159
+ const run2TotalPostBoundary = Object.values(
3160
+ run2Result.indexTokenCountMap || {}
3161
+ ).reduce((sum, v) => sum + v, 0);
3162
+ expect(run2TotalPostBoundary).toBe(0);
3163
+
3164
+ const run3Payload: TPayload = [
3165
+ {
3166
+ role: 'assistant',
3167
+ content: [
3168
+ {
3169
+ type: ContentTypes.SUMMARY,
3170
+ text: 'User asked basic arithmetic: 2+2=4, then 4*10=40.',
3171
+ tokenCount: 18,
3172
+ },
3173
+ ],
3174
+ },
3175
+ { role: 'user', content: 'What is the square root of 40?' },
3176
+ {
3177
+ role: 'assistant',
3178
+ content: 'The square root of 40 is approximately 6.32.',
3179
+ },
3180
+ ];
3181
+ const run3Map = { 0: 18, 1: 15, 2: 20 };
3182
+
3183
+ const run3Result = formatAgentMessages(run3Payload, run3Map);
3184
+ expect(run3Result.summary).toBeDefined();
3185
+ expect(run3Result.summary!.text).toBe(
3186
+ 'User asked basic arithmetic: 2+2=4, then 4*10=40.'
3187
+ );
3188
+ expect(run3Result.summary!.tokenCount).toBe(18);
3189
+
3190
+ const run3Total = Object.values(
3191
+ run3Result.indexTokenCountMap || {}
3192
+ ).reduce((sum, count) => sum + count, 0);
3193
+ expect(run3Total).toBe(15 + 20);
3194
+ });
3195
+ });
2686
3196
  });
@@ -5,3 +5,6 @@ export * from './format';
5
5
  export * from './cache';
6
6
  export * from './content';
7
7
  export * from './tools';
8
+ export * from './contextPruning';
9
+ export * from './contextPruningSettings';
10
+ export * from './reducer';