@mastra/memory 0.12.2 → 0.12.3-alpha.0

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.
@@ -0,0 +1,13 @@
1
+ import { Mastra } from '@mastra/core';
2
+ import { LibSQLStore } from '@mastra/libsql';
3
+ import { memoryProcessorAgent, weatherAgent } from './agents/weather';
4
+
5
+ export const mastra = new Mastra({
6
+ agents: {
7
+ test: weatherAgent,
8
+ testProcessor: memoryProcessorAgent,
9
+ },
10
+ storage: new LibSQLStore({
11
+ url: 'file:mastra.db',
12
+ }),
13
+ });
@@ -0,0 +1,24 @@
1
+ import { createTool } from '@mastra/core/tools';
2
+ import { z } from 'zod';
3
+
4
+ export const weatherTool = createTool({
5
+ id: 'get_weather',
6
+ description: 'Get the weather for a given location',
7
+ inputSchema: z.object({
8
+ postalCode: z.string().describe('The location to get the weather for'),
9
+ }),
10
+ execute: async ({ context: { postalCode } }) => {
11
+ return `The weather in ${postalCode} is sunny. It is currently 70 degrees and feels like 65 degrees.`;
12
+ },
13
+ });
14
+
15
+ export const weatherToolCity = createTool({
16
+ id: 'get_weather_city',
17
+ description: 'Get the weather for a given location',
18
+ inputSchema: z.object({
19
+ city: z.string().describe('The location to get the weather for'),
20
+ }),
21
+ execute: async ({ context: { city } }) => {
22
+ return `The weather in ${city} is sunny. It is currently 70 degrees and feels like 65 degrees.`;
23
+ },
24
+ });
@@ -0,0 +1,600 @@
1
+ import { mkdtemp } from 'fs/promises';
2
+ import { afterEach } from 'node:test';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { openai } from '@ai-sdk/openai';
6
+ import type { CoreMessage, MemoryProcessorOpts } from '@mastra/core';
7
+ import { MemoryProcessor } from '@mastra/core';
8
+ import type { MastraMessageV2 } from '@mastra/core/agent';
9
+ import { Agent, MessageList } from '@mastra/core/agent';
10
+ import { createTool } from '@mastra/core/tools';
11
+ import { fastembed } from '@mastra/fastembed';
12
+ import { LibSQLVector, LibSQLStore } from '@mastra/libsql';
13
+ import { Memory } from '@mastra/memory';
14
+ import { TokenLimiter, ToolCallFilter } from '@mastra/memory/processors';
15
+ import type { UIMessage } from 'ai';
16
+ import { describe, it, expect, beforeEach } from 'vitest';
17
+ import { z } from 'zod';
18
+ import { filterToolCallsByName, filterToolResultsByName, generateConversationHistory } from './test-utils';
19
+
20
+ function v2ToCoreMessages(messages: MastraMessageV2[] | UIMessage[]): CoreMessage[] {
21
+ return new MessageList().add(messages, 'memory').get.all.core();
22
+ }
23
+
24
+ let memory: Memory;
25
+ let storage: LibSQLStore;
26
+ let vector: LibSQLVector;
27
+ const resourceId = 'processor-test';
28
+
29
+ beforeEach(async () => {
30
+ // Create a new unique database file in the temp directory for each test
31
+ const dbPath = join(await mkdtemp(join(tmpdir(), `memory-processor-test-`)), 'test.db');
32
+
33
+ storage = new LibSQLStore({
34
+ url: `file:${dbPath}`,
35
+ });
36
+ vector = new LibSQLVector({
37
+ connectionUrl: `file:${dbPath}`,
38
+ });
39
+
40
+ // Initialize memory with the in-memory database
41
+ memory = new Memory({
42
+ storage,
43
+ options: {
44
+ lastMessages: 10,
45
+ semanticRecall: false,
46
+ threads: {
47
+ generateTitle: false,
48
+ },
49
+ },
50
+ });
51
+ });
52
+
53
+ afterEach(async () => {
54
+ //@ts-ignore
55
+ await storage.client.close();
56
+ //@ts-ignore
57
+ await vector.turso.close();
58
+ });
59
+
60
+ describe('Memory with Processors', () => {
61
+ it('should apply TokenLimiter when retrieving messages', async () => {
62
+ // Create a thread
63
+ const thread = await memory.createThread({
64
+ title: 'TokenLimiter Test Thread',
65
+ resourceId,
66
+ });
67
+
68
+ // Generate conversation with 10 turn pairs (20 messages total)
69
+ const { messagesV2 } = generateConversationHistory({
70
+ threadId: thread.id,
71
+ resourceId,
72
+ messageCount: 10,
73
+ toolFrequency: 3,
74
+ });
75
+
76
+ // Save messages
77
+ await memory.saveMessages({ messages: messagesV2, format: 'v2' });
78
+
79
+ // Get messages with a token limit of 250 (should get ~2.5 messages)
80
+ const queryResult = await memory.query({
81
+ threadId: thread.id,
82
+ selectBy: { last: 20 },
83
+ });
84
+ const result = memory.processMessages({
85
+ messages: new MessageList({ threadId: thread.id, resourceId })
86
+ .add(queryResult.uiMessages, 'memory')
87
+ .get.all.core(),
88
+ processors: [new TokenLimiter(250)], // Limit to 250 tokens
89
+ });
90
+
91
+ // We should have messages limited by token count
92
+ expect(result.length).toBeGreaterThan(0);
93
+ expect(result.length).toBeLessThanOrEqual(4); // Should get a small subset of messages
94
+
95
+ expect(result.at(-1)).toEqual({
96
+ role: 'tool',
97
+ content: [
98
+ {
99
+ type: 'tool-result',
100
+ toolCallId: 'tool-9',
101
+ toolName: 'weather',
102
+ result: 'Pretty hot',
103
+ },
104
+ ],
105
+ });
106
+
107
+ // Now query with a very high token limit that should return all messages
108
+ const allMessagesQuery = await memory.query({
109
+ threadId: thread.id,
110
+ selectBy: { last: 20 },
111
+ });
112
+ expect(allMessagesQuery.messages.length).toBe(20);
113
+
114
+ const allMessagesResult = memory.processMessages({
115
+ messages: new MessageList({ threadId: thread.id, resourceId })
116
+ .add(allMessagesQuery.uiMessages, 'memory')
117
+ .get.all.core(),
118
+ processors: [new TokenLimiter(3000)], // High limit that should exceed total tokens
119
+ });
120
+
121
+ // create response message list to add to memory
122
+ const messages = new MessageList({ threadId: thread.id, resourceId })
123
+ .add(allMessagesResult, 'response')
124
+ .get.all.v2();
125
+
126
+ const listed = new MessageList({ threadId: thread.id, resourceId }).add(messages, 'memory').get.all.v2();
127
+
128
+ // We should get all 20 messages
129
+ expect(listed.length).toBe(20);
130
+ // core messages store tool call/result as separate messages, so +3
131
+ expect(allMessagesResult.length).toBe(23);
132
+ });
133
+
134
+ it('should apply ToolCallFilter when retrieving messages', async () => {
135
+ // Create a thread
136
+ const thread = await memory.createThread({
137
+ title: 'ToolFilter Test Thread',
138
+ resourceId,
139
+ });
140
+
141
+ // Generate conversation with tool calls
142
+ const { messagesV2 } = generateConversationHistory({
143
+ threadId: thread.id,
144
+ resourceId,
145
+ messageCount: 5,
146
+ toolFrequency: 2, // Every other assistant response is a tool call
147
+ toolNames: ['weather', 'calculator'],
148
+ });
149
+
150
+ // Save messages
151
+ await memory.saveMessages({ messages: messagesV2, format: 'v2' });
152
+
153
+ // filter weather tool calls
154
+ const queryResult = await memory.query({
155
+ threadId: thread.id,
156
+ selectBy: { last: 20 },
157
+ });
158
+ const result = memory.processMessages({
159
+ messages: v2ToCoreMessages(queryResult.uiMessages),
160
+ processors: [new ToolCallFilter({ exclude: ['weather'] })],
161
+ });
162
+ const messages = new MessageList({ threadId: thread.id, resourceId }).add(result, 'response').get.all.v2();
163
+ expect(new MessageList().add(messages, 'memory').get.all.v2().length).toBeLessThan(messagesV2.length);
164
+ expect(filterToolCallsByName(result, 'weather')).toHaveLength(0);
165
+ expect(filterToolResultsByName(result, 'weather')).toHaveLength(0);
166
+ expect(filterToolCallsByName(result, 'calculator')).toHaveLength(1);
167
+ expect(filterToolResultsByName(result, 'calculator')).toHaveLength(1);
168
+
169
+ // make another query with no processors to make sure memory messages in DB were not altered and were only filtered from results
170
+ const queryResult2 = await memory.query({
171
+ threadId: thread.id,
172
+ selectBy: { last: 20 },
173
+ });
174
+ const result2 = memory.processMessages({ messages: v2ToCoreMessages(queryResult2.uiMessages), processors: [] });
175
+ const messages2 = new MessageList({ threadId: thread.id, resourceId }).add(result2, 'response').get.all.v2();
176
+ expect(new MessageList().add(messages2, 'memory').get.all.v2()).toHaveLength(messagesV2.length);
177
+ expect(filterToolCallsByName(result2, 'weather')).toHaveLength(1);
178
+ expect(filterToolResultsByName(result2, 'weather')).toHaveLength(1);
179
+ expect(filterToolCallsByName(result2, 'calculator')).toHaveLength(1);
180
+ expect(filterToolResultsByName(result2, 'calculator')).toHaveLength(1);
181
+
182
+ // filter all by name
183
+ const queryResult3 = await memory.query({
184
+ threadId: thread.id,
185
+ selectBy: { last: 20 },
186
+ });
187
+ const result3 = memory.processMessages({
188
+ messages: v2ToCoreMessages(queryResult3.uiMessages),
189
+ processors: [new ToolCallFilter({ exclude: ['weather', 'calculator'] })],
190
+ });
191
+ expect(result3.length).toBeLessThan(messagesV2.length);
192
+ expect(filterToolCallsByName(result3, 'weather')).toHaveLength(0);
193
+ expect(filterToolResultsByName(result3, 'weather')).toHaveLength(0);
194
+ expect(filterToolCallsByName(result3, 'calculator')).toHaveLength(0);
195
+ expect(filterToolResultsByName(result3, 'calculator')).toHaveLength(0);
196
+
197
+ // filter all by default
198
+ const queryResult4 = await memory.query({
199
+ threadId: thread.id,
200
+ selectBy: { last: 20 },
201
+ });
202
+ const result4 = memory.processMessages({
203
+ messages: v2ToCoreMessages(queryResult4.uiMessages),
204
+ processors: [new ToolCallFilter()],
205
+ });
206
+ expect(result4.length).toBeLessThan(messagesV2.length);
207
+ expect(filterToolCallsByName(result4, 'weather')).toHaveLength(0);
208
+ expect(filterToolResultsByName(result4, 'weather')).toHaveLength(0);
209
+ expect(filterToolCallsByName(result4, 'calculator')).toHaveLength(0);
210
+ expect(filterToolResultsByName(result4, 'calculator')).toHaveLength(0);
211
+ });
212
+
213
+ it('should apply multiple processors in order', async () => {
214
+ // Create a thread
215
+ const thread = await memory.createThread({
216
+ title: 'Multiple Processors Test Thread',
217
+ resourceId,
218
+ });
219
+
220
+ // Generate conversation with tool calls
221
+ const { messages } = generateConversationHistory({
222
+ threadId: thread.id,
223
+ resourceId,
224
+ messageCount: 8,
225
+ toolFrequency: 2, // Every other assistant response is a tool call
226
+ toolNames: ['weather', 'calculator', 'search'],
227
+ });
228
+
229
+ // Save messages
230
+ await memory.saveMessages({ messages });
231
+
232
+ // Apply multiple processors: first remove weather tool calls, then limit to 250 tokens
233
+ const queryResult = await memory.query({
234
+ threadId: thread.id,
235
+ selectBy: { last: 20 },
236
+ });
237
+ const result = memory.processMessages({
238
+ messages: v2ToCoreMessages(queryResult.uiMessages),
239
+ processors: [new ToolCallFilter({ exclude: ['weather'] }), new TokenLimiter(250)],
240
+ });
241
+
242
+ // We should have fewer messages after filtering and token limiting
243
+ expect(result.length).toBeGreaterThan(0);
244
+ expect(result.length).toBeLessThan(messages.length);
245
+ // And they should exclude weather tool messages
246
+ expect(filterToolResultsByName(result, `weather`)).toHaveLength(0);
247
+ expect(filterToolCallsByName(result, `weather`)).toHaveLength(0);
248
+ });
249
+
250
+ it('should apply multiple processors without duplicating messages', async () => {
251
+ class ConversationOnlyFilter extends MemoryProcessor {
252
+ constructor() {
253
+ super({ name: 'ConversationOnlyFilter' });
254
+ }
255
+
256
+ process(messages: CoreMessage[], _opts: MemoryProcessorOpts = {}): CoreMessage[] {
257
+ return messages.filter(msg => msg.role === 'user' || msg.role === 'assistant');
258
+ }
259
+ }
260
+ const memory = new Memory({
261
+ storage,
262
+ vector,
263
+ embedder: fastembed,
264
+ processors: [new ToolCallFilter(), new ConversationOnlyFilter(), new TokenLimiter(127000)],
265
+ options: {
266
+ lastMessages: 10,
267
+ semanticRecall: true,
268
+ workingMemory: {
269
+ enabled: true,
270
+ },
271
+ },
272
+ });
273
+ const thread = await memory.createThread({
274
+ title: 'Multiple Processors Test Thread 2',
275
+ resourceId,
276
+ });
277
+ const instructions = 'You are a helpful assistant';
278
+ const agent = new Agent({
279
+ name: 'processor-test-agent',
280
+ instructions,
281
+ model: openai('gpt-4o'),
282
+ memory,
283
+ });
284
+
285
+ const userMessage = 'Tell me something interesting about space';
286
+
287
+ const res = await agent.generateVNext(
288
+ [
289
+ {
290
+ role: 'user',
291
+ content: userMessage,
292
+ },
293
+ ],
294
+ {
295
+ threadId: thread.id,
296
+ resourceId,
297
+ },
298
+ );
299
+
300
+ const requestInputMessages = res.request.body?.input;
301
+ if (!Array.isArray(requestInputMessages)) {
302
+ throw new Error(`responseMessages should be an array`);
303
+ }
304
+
305
+ const userMessagesByContent = requestInputMessages.filter(m => m.content?.[0]?.text === userMessage);
306
+ expect(userMessagesByContent).toEqual([
307
+ expect.objectContaining({ role: 'user', content: [expect.objectContaining({ text: userMessage })] }),
308
+ ]); // should only be one
309
+ expect(userMessagesByContent.length).toBe(1); // if there's more than one we have duplicate messages
310
+
311
+ const userMessage2 = 'Tell me something else interesting about space';
312
+
313
+ const res2 = await agent.generateVNext(
314
+ [
315
+ {
316
+ role: 'user',
317
+ content: userMessage2,
318
+ },
319
+ ],
320
+ {
321
+ threadId: thread.id,
322
+ resourceId,
323
+ },
324
+ );
325
+
326
+ const requestInputMessages2 = res2.request.body?.input;
327
+
328
+ if (!Array.isArray(requestInputMessages2)) {
329
+ throw new Error(`responseMessages should be an array`);
330
+ }
331
+
332
+ const userMessagesByContent2 = requestInputMessages2.filter((m: any) => m.content?.[0]?.text === userMessage2);
333
+ expect(userMessagesByContent2).toEqual([
334
+ expect.objectContaining({ role: 'user', content: [expect.objectContaining({ text: userMessage2 })] }),
335
+ ]); // should only be one
336
+ expect(userMessagesByContent2.length).toBe(1); // if there's more than one we have duplicate messages
337
+
338
+ // make sure all user messages are there
339
+ const allUserMessages = requestInputMessages2.filter((m: CoreMessage) => m.role === 'user');
340
+ expect(allUserMessages.length).toBe(2);
341
+
342
+ const remembered = await memory.query({
343
+ threadId: thread.id,
344
+ resourceId,
345
+ selectBy: {
346
+ last: 20,
347
+ },
348
+ });
349
+ expect(remembered.messages.filter(m => m.role === 'user').length).toBe(2);
350
+ expect(remembered.messages.length).toBe(4); // 2 user, 2 assistant. These wont be filtered because they come from memory.query() directly
351
+ });
352
+
353
+ it('should apply processors with a real Mastra agent', async () => {
354
+ // Create a thread
355
+ const thread = await memory.createThread({
356
+ title: 'Real Agent Processor Test Thread',
357
+ resourceId,
358
+ });
359
+
360
+ const threadId = thread.id;
361
+
362
+ // Create test tools
363
+ const weatherTool = createTool({
364
+ id: 'get_weather',
365
+ description: 'Get the weather for a given location',
366
+ inputSchema: z.object({
367
+ location: z.string().describe('The location to get the weather for'),
368
+ }),
369
+ execute: async ({ context: { location } }) => {
370
+ return `The weather in ${location} is sunny. It is currently 70 degrees and feels like 65 degrees.`;
371
+ },
372
+ });
373
+
374
+ const calculatorTool = createTool({
375
+ id: 'calculator',
376
+ description: 'Perform a simple calculation',
377
+ inputSchema: z.object({
378
+ expression: z.string().describe('The mathematical expression to calculate'),
379
+ }),
380
+ execute: async ({ context: { expression } }) => {
381
+ return `The result of ${expression} is ${eval(expression)}`;
382
+ },
383
+ });
384
+
385
+ const instructions =
386
+ 'You are a helpful assistant with access to weather and calculator tools. Use them when appropriate.';
387
+ // Create agent with memory and tools
388
+ const agent = new Agent({
389
+ name: 'processor-test-agent',
390
+ instructions,
391
+ model: openai('gpt-4o'),
392
+ memory,
393
+ tools: {
394
+ get_weather: weatherTool,
395
+ calculator: calculatorTool,
396
+ },
397
+ });
398
+
399
+ // First message - use weather tool
400
+ await agent.generateVNext('What is the weather in Seattle?', {
401
+ threadId,
402
+ resourceId,
403
+ });
404
+ // Second message - use calculator tool
405
+ await agent.generateVNext('Calculate 123 * 456', {
406
+ threadId,
407
+ resourceId,
408
+ });
409
+ // Third message - simple text response
410
+ await agent.generateVNext('Tell me something interesting about space', {
411
+ threadId,
412
+ resourceId,
413
+ });
414
+
415
+ // Query with no processors to verify baseline message count
416
+ const queryResult = await memory.query({
417
+ threadId,
418
+ selectBy: { last: 20 },
419
+ });
420
+
421
+ const list = new MessageList({ threadId }).add(queryResult.messagesV2, 'memory');
422
+
423
+ const baselineResult = memory.processMessages({
424
+ messages: list.get.remembered.core(),
425
+ newMessages: list.get.input.core(),
426
+ processors: [],
427
+ });
428
+
429
+ // There should be at least 6 messages (3 user + 3 assistant responses)
430
+ expect(baselineResult.length).toBeGreaterThanOrEqual(6);
431
+
432
+ // Verify we have tool calls in the baseline
433
+ const weatherToolCalls = filterToolCallsByName(baselineResult, 'get_weather');
434
+ const calculatorToolCalls = filterToolCallsByName(baselineResult, 'calculator');
435
+ expect(weatherToolCalls.length).toBeGreaterThan(0);
436
+ expect(calculatorToolCalls.length).toBeGreaterThan(0);
437
+
438
+ // Test filtering weather tool calls
439
+ const weatherQueryResult = await memory.query({
440
+ threadId,
441
+ selectBy: { last: 20 },
442
+ });
443
+ const list2 = new MessageList({ threadId }).add(weatherQueryResult.messagesV2, 'memory');
444
+ const weatherFilteredResult = memory.processMessages({
445
+ messages: list2.get.all.core(),
446
+ processors: [new ToolCallFilter({ exclude: ['get_weather'] })],
447
+ });
448
+
449
+ // Should have fewer messages after filtering
450
+ expect(weatherFilteredResult.length).toBeLessThan(baselineResult.length);
451
+
452
+ // No weather tool calls should remain
453
+ expect(filterToolCallsByName(weatherFilteredResult, 'get_weather').length).toBe(0);
454
+ expect(filterToolResultsByName(weatherFilteredResult, 'get_weather').length).toBe(0);
455
+
456
+ // Calculator tool calls should still be present
457
+ expect(filterToolCallsByName(weatherFilteredResult, 'calculator').length).toBeGreaterThan(0);
458
+
459
+ // Test token limiting
460
+ const tokenLimitQuery = await memory.query({
461
+ threadId,
462
+ selectBy: { last: 20 },
463
+ });
464
+ const list3 = new MessageList({ threadId }).add(tokenLimitQuery.messages, 'memory');
465
+ const tokenLimitedResult = memory.processMessages({
466
+ messages: list3.get.all.core(),
467
+ processors: [new TokenLimiter(100)], // Small limit to only get a subset
468
+ });
469
+
470
+ // Should have fewer messages after token limiting
471
+ expect(tokenLimitedResult.length).toBeLessThan(baselineResult.length);
472
+
473
+ // Test combining processors
474
+ const combinedQuery = await memory.query({
475
+ threadId,
476
+ selectBy: { last: 20 },
477
+ });
478
+ const list4 = new MessageList({ threadId }).add(combinedQuery.messages, 'memory');
479
+ const combinedResult = memory.processMessages({
480
+ messages: list4.get.all.core(),
481
+ processors: [new ToolCallFilter({ exclude: ['get_weather', 'calculator'] }), new TokenLimiter(500)],
482
+ });
483
+
484
+ // No tool calls should remain
485
+ expect(filterToolCallsByName(combinedResult, 'get_weather').length).toBe(0);
486
+ expect(filterToolCallsByName(combinedResult, 'calculator').length).toBe(0);
487
+ expect(filterToolResultsByName(combinedResult, 'get_weather').length).toBe(0);
488
+ expect(filterToolResultsByName(combinedResult, 'calculator').length).toBe(0);
489
+
490
+ // The result should still contain some messages
491
+ expect(combinedResult.length).toBeGreaterThan(0);
492
+ });
493
+
494
+ it('should chunk long text by character count', async () => {
495
+ // Create a thread
496
+ const thread = await memory.createThread({
497
+ title: 'Text Chunking Test Thread',
498
+ resourceId,
499
+ });
500
+
501
+ // Create a long text with known word boundaries
502
+ const words = [];
503
+ for (let i = 0; i < 1000; i++) {
504
+ words.push(`word${i}`);
505
+ }
506
+ const longText = words.join(' ');
507
+
508
+ // Save a message with the long text
509
+ await memory.saveMessages({
510
+ messages: [
511
+ {
512
+ id: 'chunking-test',
513
+ threadId: thread.id,
514
+ role: 'user',
515
+ content: {
516
+ format: 2,
517
+ parts: [{ type: 'text', text: longText }],
518
+ },
519
+ createdAt: new Date(),
520
+ resourceId,
521
+ },
522
+ ],
523
+ });
524
+
525
+ // Query the message back
526
+ const queryResult = await memory.query({
527
+ threadId: thread.id,
528
+ selectBy: { last: 1 },
529
+ });
530
+
531
+ // Retrieve the message (no TokenLimiter, just get the message back)
532
+ const result = memory.processMessages({
533
+ messages: v2ToCoreMessages(queryResult.uiMessages),
534
+ });
535
+
536
+ // Should have retrieved the message
537
+ expect(result.length).toBe(1);
538
+
539
+ // Each chunk should respect word boundaries
540
+ for (const msg of result) {
541
+ // No words should be cut off
542
+ const content = typeof msg.content === 'string' ? msg.content : (msg.content[0] as { text: string }).text;
543
+ const words = content.split(/\s+/);
544
+ for (const word of words) {
545
+ expect(word).toMatch(/^word\d+$/); // Each word should be complete
546
+ }
547
+ }
548
+
549
+ // Chunks should maintain original order
550
+ let prevNum = -1;
551
+ for (const msg of result) {
552
+ const content = typeof msg.content === 'string' ? msg.content : (msg.content[0] as { text: string }).text;
553
+ const firstWord = content.split(/\s+/)[0];
554
+ const num = parseInt(firstWord.replace('word', ''));
555
+ expect(num).toBeGreaterThan(prevNum);
556
+ prevNum = num;
557
+ }
558
+ });
559
+ });
560
+
561
+ // Direct unit test for chunkText
562
+
563
+ describe('Memory.chunkText', () => {
564
+ it('should split long text into chunks at word boundaries', () => {
565
+ const memory = new Memory({
566
+ storage,
567
+ vector,
568
+ embedder: fastembed,
569
+ options: {
570
+ semanticRecall: true,
571
+ lastMessages: 10,
572
+ },
573
+ });
574
+ const words = [];
575
+ for (let i = 0; i < 1000; i++) {
576
+ words.push(`word${i}`);
577
+ }
578
+ const longText = words.join(' ');
579
+ // Use a small token size to force chunking
580
+ const chunks = (memory as any).chunkText(longText, 50);
581
+ expect(chunks.length).toBeGreaterThan(1);
582
+ // Each chunk should respect word boundaries
583
+ for (const chunk of chunks) {
584
+ const chunkWords = chunk.split(/\s+/);
585
+ for (const word of chunkWords) {
586
+ if (word.length === 0) continue;
587
+ expect(word).toMatch(/^word\d+$/);
588
+ }
589
+ }
590
+ // Chunks should maintain original order
591
+ let prevNum = -1;
592
+ for (const chunk of chunks) {
593
+ const firstWord = chunk.split(/\s+/)[0];
594
+ if (!firstWord) continue; // skip empty
595
+ const num = parseInt(firstWord.replace('word', ''));
596
+ expect(num).toBeGreaterThan(prevNum);
597
+ prevNum = num;
598
+ }
599
+ });
600
+ });