@mastra/memory 0.12.2 → 0.13.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,1064 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdtemp } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { openai } from '@ai-sdk/openai';
6
+ import type { MastraMessageV1 } from '@mastra/core';
7
+ import { Agent } from '@mastra/core/agent';
8
+ import { fastembed } from '@mastra/fastembed';
9
+ import { LibSQLVector, LibSQLStore } from '@mastra/libsql';
10
+ import { Memory } from '@mastra/memory';
11
+ import type { ToolCallPart } from 'ai';
12
+ import { config } from 'dotenv';
13
+ import type { JSONSchema7 } from 'json-schema';
14
+ import { describe, expect, it, beforeEach, afterEach } from 'vitest';
15
+ import { z } from 'zod';
16
+
17
+ const resourceId = 'test-resource';
18
+ let messageCounter = 0;
19
+
20
+ // Test helpers
21
+ const createTestThread = (title: string, metadata = {}) => ({
22
+ id: randomUUID(),
23
+ title,
24
+ resourceId,
25
+ metadata,
26
+ createdAt: new Date(),
27
+ updatedAt: new Date(),
28
+ });
29
+
30
+ const createTestMessage = (threadId: string, content: string, role: 'user' | 'assistant' = 'user'): MastraMessageV1 => {
31
+ messageCounter++;
32
+ return {
33
+ id: randomUUID(),
34
+ threadId,
35
+ content,
36
+ role,
37
+ type: 'text',
38
+ createdAt: new Date(Date.now() + messageCounter * 1000),
39
+ resourceId,
40
+ };
41
+ };
42
+
43
+ function extractUserData(obj: any) {
44
+ // Remove common schema keys
45
+ const { type, properties, required, additionalProperties, $schema, ...data } = obj;
46
+ return data;
47
+ }
48
+
49
+ config({ path: '.env.test' });
50
+
51
+ describe('Working Memory Tests', () => {
52
+ let memory: Memory;
53
+ let thread: any;
54
+ let storage: LibSQLStore;
55
+ let vector: LibSQLVector;
56
+
57
+ describe('Working Memory Test with Template', () => {
58
+ beforeEach(async () => {
59
+ // Create a new unique database file in the temp directory for each test
60
+ const dbPath = join(await mkdtemp(join(tmpdir(), `memory-working-test-${Date.now()}`)), 'test.db');
61
+ console.log('dbPath', dbPath);
62
+
63
+ storage = new LibSQLStore({
64
+ url: `file:${dbPath}`,
65
+ });
66
+ vector = new LibSQLVector({
67
+ connectionUrl: `file:${dbPath}`,
68
+ });
69
+
70
+ // Create memory instance with working memory enabled
71
+ memory = new Memory({
72
+ options: {
73
+ workingMemory: {
74
+ enabled: true,
75
+ template: `# User Information
76
+ - **First Name**:
77
+ - **Last Name**:
78
+ - **Location**:
79
+ - **Interests**:
80
+ `,
81
+ },
82
+ lastMessages: 10,
83
+ semanticRecall: {
84
+ topK: 3,
85
+ messageRange: 2,
86
+ },
87
+ threads: {
88
+ generateTitle: false,
89
+ },
90
+ },
91
+ storage,
92
+ vector,
93
+ embedder: fastembed,
94
+ });
95
+ // Reset message counter
96
+ messageCounter = 0;
97
+ // Create a new thread for each test
98
+ thread = await memory.saveThread({
99
+ thread: createTestThread('Working Memory Test Thread'),
100
+ });
101
+ });
102
+
103
+ afterEach(async () => {
104
+ //@ts-ignore
105
+ await storage.client.close();
106
+ //@ts-ignore
107
+ await vector.turso.close();
108
+ });
109
+
110
+ it('should handle LLM responses with working memory using OpenAI (test that the working memory prompt works)', async () => {
111
+ const agent = new Agent({
112
+ name: 'Memory Test Agent',
113
+ instructions: 'You are a helpful AI agent. Always add working memory tags to remember user information.',
114
+ model: openai('gpt-4o'),
115
+ memory,
116
+ });
117
+
118
+ await agent.generateVNext('Hi, my name is Tyler and I live in San Francisco', {
119
+ threadId: thread.id,
120
+ resourceId,
121
+ });
122
+
123
+ // Get working memory
124
+ const workingMemory = await memory.getWorkingMemory({ threadId: thread.id });
125
+ expect(workingMemory).not.toBeNull();
126
+ if (workingMemory) {
127
+ // Check for specific Markdown format
128
+ expect(workingMemory).toContain('# User Information');
129
+ expect(workingMemory).toContain('**First Name**: Tyler');
130
+ expect(workingMemory).toContain('**Location**: San Francisco');
131
+ }
132
+ });
133
+
134
+ it('should initialize with default working memory template', async () => {
135
+ const systemInstruction = await memory.getSystemMessage({ threadId: thread.id });
136
+ expect(systemInstruction).not.toBeNull();
137
+ if (systemInstruction) {
138
+ // Should match our Markdown template
139
+ expect(systemInstruction).toContain('# User Information');
140
+ expect(systemInstruction).toContain('First Name');
141
+ }
142
+ });
143
+
144
+ it('should hide working memory tags in remembered messages', async () => {
145
+ const messages = [
146
+ createTestMessage(thread.id, 'Hi, my name is John'),
147
+ createTestMessage(
148
+ thread.id,
149
+ `Hello John!
150
+ <working_memory>
151
+ # User Information
152
+ - **First Name**: John
153
+ - **Last Name**:
154
+ - **Location**:
155
+ - **Interests**:
156
+ </working_memory>`,
157
+ 'assistant',
158
+ ),
159
+ ];
160
+
161
+ await memory.saveMessages({ messages, format: 'v2' });
162
+
163
+ const remembered = await memory.rememberMessages({
164
+ threadId: thread.id,
165
+ config: { lastMessages: 10 },
166
+ });
167
+
168
+ // Working memory tags should be stripped from the messages
169
+ expect(remembered.messages[1].content).not.toContain('<working_memory>');
170
+ expect(remembered.messages[1].content).toContain('Hello John!');
171
+ });
172
+
173
+ it('should respect working memory enabled/disabled setting', async () => {
174
+ const dbPath = join(await mkdtemp(join(tmpdir(), `memory-working-test-${Date.now()}`)), 'test.db');
175
+
176
+ // Create memory instance with working memory disabled
177
+ const disabledMemory = new Memory({
178
+ storage: new LibSQLStore({
179
+ url: `file:${dbPath}`,
180
+ }),
181
+ vector: new LibSQLVector({
182
+ connectionUrl: `file:${dbPath}`,
183
+ }),
184
+ embedder: openai.embedding('text-embedding-3-small'),
185
+ options: {
186
+ workingMemory: {
187
+ enabled: false,
188
+ template: `# User Information
189
+ - **First Name**:
190
+ - **Last Name**:
191
+ `,
192
+ },
193
+ lastMessages: 10,
194
+ semanticRecall: {
195
+ topK: 3,
196
+ messageRange: 2,
197
+ },
198
+ threads: {
199
+ generateTitle: false,
200
+ },
201
+ },
202
+ });
203
+
204
+ const thread = await disabledMemory.saveThread({
205
+ thread: createTestThread('Disabled Working Memory Thread'),
206
+ });
207
+
208
+ const messages = [
209
+ createTestMessage(thread.id, 'Hi, my name is John'),
210
+ createTestMessage(
211
+ thread.id,
212
+ `Hello John!
213
+ <working_memory>
214
+ # User Information
215
+ - **First Name**: John
216
+ - **Last Name**:
217
+ </working_memory>`,
218
+ 'assistant',
219
+ ),
220
+ ];
221
+
222
+ await disabledMemory.saveMessages({ messages, format: 'v2' });
223
+
224
+ // Working memory should be null when disabled
225
+ const workingMemory = await disabledMemory.getWorkingMemory({ threadId: thread.id });
226
+ expect(workingMemory).toBeNull();
227
+
228
+ // Thread metadata should not contain working memory
229
+ const updatedThread = await disabledMemory.getThreadById({ threadId: thread.id });
230
+ expect(updatedThread?.metadata?.workingMemory).toBeUndefined();
231
+ });
232
+
233
+ it('should handle LLM responses with working memory using tool calls', async () => {
234
+ const agent = new Agent({
235
+ name: 'Memory Test Agent',
236
+ instructions: 'You are a helpful AI agent. Always add working memory tags to remember user information.',
237
+ model: openai('gpt-4o'),
238
+ memory,
239
+ });
240
+
241
+ const thread = await memory.createThread(createTestThread(`Tool call working memory test`));
242
+
243
+ await agent.generateVNext('Hi, my name is Tyler and I live in San Francisco', {
244
+ threadId: thread.id,
245
+ resourceId,
246
+ });
247
+
248
+ const workingMemory = await memory.getWorkingMemory({ threadId: thread.id });
249
+ expect(workingMemory).not.toBeNull();
250
+ if (workingMemory) {
251
+ // Check for specific Markdown format
252
+ expect(workingMemory).toContain('# User Information');
253
+ expect(workingMemory).toContain('**First Name**: Tyler');
254
+ expect(workingMemory).toContain('**Location**: San Francisco');
255
+ }
256
+ });
257
+
258
+ it("shouldn't pollute context with working memory tool call args, only the system instruction working memory should exist", async () => {
259
+ const agent = new Agent({
260
+ name: 'Memory Test Agent',
261
+ instructions: 'You are a helpful AI agent. Always add working memory tags to remember user information.',
262
+ model: openai('gpt-4o'),
263
+ memory,
264
+ });
265
+
266
+ const thread = await memory.createThread(createTestThread(`Tool call working memory context pollution test`));
267
+
268
+ await agent.generateVNext('Hi, my name is Tyler and I live in a submarine under the sea', {
269
+ threadId: thread.id,
270
+ resourceId,
271
+ });
272
+
273
+ let workingMemory = await memory.getWorkingMemory({ threadId: thread.id });
274
+ expect(workingMemory).not.toBeNull();
275
+ if (workingMemory) {
276
+ expect(workingMemory).toContain('# User Information');
277
+ expect(workingMemory).toContain('**First Name**: Tyler');
278
+ expect(workingMemory?.toLowerCase()).toContain('**location**:');
279
+ expect(workingMemory?.toLowerCase()).toContain('submarine under the sea');
280
+ }
281
+
282
+ await agent.generateVNext('I changed my name to Jim', {
283
+ threadId: thread.id,
284
+ resourceId,
285
+ });
286
+
287
+ workingMemory = await memory.getWorkingMemory({ threadId: thread.id });
288
+ expect(workingMemory).not.toBeNull();
289
+ if (workingMemory) {
290
+ expect(workingMemory).toContain('# User Information');
291
+ expect(workingMemory).toContain('**First Name**: Jim');
292
+ expect(workingMemory?.toLowerCase()).toContain('**location**:');
293
+ expect(workingMemory?.toLowerCase()).toContain('submarine under the sea');
294
+ }
295
+
296
+ await agent.generateVNext('I moved to Vancouver Island', {
297
+ threadId: thread.id,
298
+ resourceId,
299
+ });
300
+
301
+ workingMemory = await memory.getWorkingMemory({ threadId: thread.id });
302
+ expect(workingMemory).not.toBeNull();
303
+ if (workingMemory) {
304
+ expect(workingMemory).toContain('# User Information');
305
+ expect(workingMemory).toContain('**First Name**: Jim');
306
+ expect(workingMemory).toContain('**Location**: Vancouver Island');
307
+ }
308
+
309
+ const history = await memory.query({
310
+ threadId: thread.id,
311
+ resourceId,
312
+ selectBy: {
313
+ last: 20,
314
+ },
315
+ });
316
+
317
+ const memoryArgs: string[] = [];
318
+
319
+ for (const message of history.messages) {
320
+ if (message.role === `assistant`) {
321
+ for (const part of message.content) {
322
+ if (typeof part === `string`) continue;
323
+ if (part.type === `tool-call` && part.toolName === `updateWorkingMemory`) {
324
+ memoryArgs.push((part.args as any).memory);
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ expect(memoryArgs).not.toContain(`Tyler`);
331
+ expect(memoryArgs).not.toContain('submarine under the sea');
332
+ expect(memoryArgs).not.toContain('Jim');
333
+ expect(memoryArgs).not.toContain('Vancouver Island');
334
+ expect(memoryArgs).toEqual([]);
335
+
336
+ workingMemory = await memory.getWorkingMemory({ threadId: thread.id });
337
+ expect(workingMemory).not.toBeNull();
338
+ if (workingMemory) {
339
+ // Format-specific assertion that checks for Markdown format
340
+ expect(workingMemory).toContain('# User Information');
341
+ expect(workingMemory).toContain('**First Name**: Jim');
342
+ expect(workingMemory).toContain('**Location**: Vancouver Island');
343
+ }
344
+ });
345
+
346
+ it('should remove tool-call/tool-result messages with toolName "updateWorkingMemory"', async () => {
347
+ const threadId = thread.id;
348
+ const messages = [
349
+ createTestMessage(threadId, 'User says something'),
350
+ // Pure tool-call message (should be removed)
351
+ {
352
+ id: randomUUID(),
353
+ threadId,
354
+ role: 'assistant',
355
+ type: 'tool-call',
356
+ content: [
357
+ {
358
+ type: 'tool-call',
359
+ toolName: 'updateWorkingMemory',
360
+ // ...other fields as needed
361
+ },
362
+ ],
363
+ toolNames: ['updateWorkingMemory'],
364
+ createdAt: new Date(),
365
+ resourceId,
366
+ },
367
+ // Mixed content: tool-call + text (tool-call part should be filtered, text kept)
368
+ {
369
+ id: randomUUID(),
370
+ threadId,
371
+ role: 'assistant',
372
+ type: 'text',
373
+ content: [
374
+ {
375
+ type: 'tool-call',
376
+ toolName: 'updateWorkingMemory',
377
+ args: { memory: 'should not persist' },
378
+ },
379
+ {
380
+ type: 'text',
381
+ text: 'Normal message',
382
+ },
383
+ ],
384
+ createdAt: new Date(),
385
+ resourceId,
386
+ },
387
+ // Pure text message (should be kept)
388
+ {
389
+ id: randomUUID(),
390
+ threadId,
391
+ role: 'assistant',
392
+ type: 'text',
393
+ content: 'Another normal message',
394
+ createdAt: new Date(),
395
+ resourceId,
396
+ },
397
+ ];
398
+
399
+ // Save messages
400
+ const saved = await memory.saveMessages({ messages: messages as MastraMessageV1[], format: 'v2' });
401
+
402
+ // Should not include any updateWorkingMemory tool-call messages (pure or mixed)
403
+ expect(
404
+ saved.some(
405
+ m =>
406
+ (m.type === 'tool-call' || m.type === 'tool-result') &&
407
+ Array.isArray(m.content.parts) &&
408
+ m.content.parts.some(
409
+ c => c.type === 'tool-invocation' && c.toolInvocation.toolName === `updateWorkingMemory`,
410
+ ),
411
+ ),
412
+ ).toBe(false);
413
+
414
+ // Mixed content message: should only keep the text part
415
+ const assistantMessages = saved.filter(m => m.role === 'assistant');
416
+ expect(
417
+ assistantMessages.every(m => {
418
+ // TODO: seems like saveMessages says it returns MastraMessageV2 but it's returning V1
419
+ return JSON.stringify(m).includes(`updateWorkingMemory`);
420
+ }),
421
+ ).toBe(false);
422
+ // working memory should not be present
423
+ expect(
424
+ saved.some(
425
+ m =>
426
+ (m.type === 'tool-call' || m.type === 'tool-result') &&
427
+ Array.isArray(m.content) &&
428
+ m.content.some(c => (c as ToolCallPart).toolName === 'updateWorkingMemory'),
429
+ ),
430
+ ).toBe(false);
431
+
432
+ // TODO: again seems like we're getting V1 here but types say V2
433
+ // It actually should return V1 for now (CoreMessage compatible)
434
+
435
+ // Pure text message should be present
436
+ expect(saved.some(m => m.content.content === 'Another normal message')).toBe(true);
437
+ // User message should be present
438
+ expect(
439
+ saved.some(m => typeof m.content.content === 'string' && m.content.content.includes('User says something')),
440
+ ).toBe(true);
441
+ });
442
+ });
443
+
444
+ describe('Working Memory with agent memory', () => {
445
+ let agent: Agent;
446
+ let thread: any;
447
+ let memory: Memory;
448
+
449
+ beforeEach(async () => {
450
+ const dbPath = join(await mkdtemp(join(tmpdir(), `memory-working-test-${Date.now()}`)), 'test.db');
451
+ storage = new LibSQLStore({
452
+ url: `file:${dbPath}`,
453
+ });
454
+
455
+ memory = new Memory({
456
+ storage,
457
+ options: {
458
+ workingMemory: {
459
+ enabled: true,
460
+ schema: z.object({
461
+ favouriteAnimal: z.string(),
462
+ }),
463
+ },
464
+ lastMessages: 1,
465
+ threads: {
466
+ generateTitle: false,
467
+ },
468
+ },
469
+ });
470
+ // Reset message counter
471
+ messageCounter = 0;
472
+
473
+ // Create a new thread for each test
474
+ thread = await memory.saveThread({
475
+ thread: createTestThread('Working Memory Test Thread'),
476
+ });
477
+ expect(await memory.getWorkingMemory({ threadId: thread.id })).toBeNull();
478
+ agent = new Agent({
479
+ name: 'Memory Test Agent',
480
+ instructions: 'You are a helpful AI agent. Always add working memory tags to remember user information.',
481
+ model: openai('gpt-4o'),
482
+ memory,
483
+ });
484
+ });
485
+
486
+ it('should remember information from working memory in subsequent calls', async () => {
487
+ const thread = await memory.saveThread({
488
+ thread: createTestThread('Remembering Test'),
489
+ });
490
+
491
+ // First call to establish a fact in working memory
492
+ await agent.generateVNext('My favorite animal is the majestic wolf.', {
493
+ threadId: thread.id,
494
+ resourceId,
495
+ });
496
+
497
+ // Verify it's in the working memory
498
+ const workingMemoryAfterFirstCall = await memory.getWorkingMemory({ threadId: thread.id });
499
+ expect(workingMemoryAfterFirstCall).not.toBeNull();
500
+ if (workingMemoryAfterFirstCall) {
501
+ expect(workingMemoryAfterFirstCall.toLowerCase()).toContain('wolf');
502
+ }
503
+
504
+ // add messages to the thread
505
+ await agent.generateVNext('How are you doing?', {
506
+ threadId: thread.id,
507
+ resourceId,
508
+ });
509
+
510
+ // third call to see if the agent remembers the fact
511
+ const response = await agent.generateVNext('What is my favorite animal?', {
512
+ threadId: thread.id,
513
+ resourceId,
514
+ });
515
+
516
+ expect(response.text.toLowerCase()).toContain('wolf');
517
+ });
518
+
519
+ describe('Working Memory with Schema', () => {
520
+ let agent: Agent;
521
+ beforeEach(async () => {
522
+ const dbPath = join(await mkdtemp(join(tmpdir(), `memory-working-test-${Date.now()}`)), 'test.db');
523
+ storage = new LibSQLStore({
524
+ url: `file:${dbPath}`,
525
+ });
526
+ vector = new LibSQLVector({
527
+ connectionUrl: `file:${dbPath}`,
528
+ });
529
+
530
+ memory = new Memory({
531
+ storage,
532
+ vector,
533
+ embedder: fastembed,
534
+ options: {
535
+ workingMemory: {
536
+ enabled: true,
537
+ schema: z.object({
538
+ city: z.string(),
539
+ temperature: z.number().optional(),
540
+ }),
541
+ },
542
+ lastMessages: 10,
543
+ semanticRecall: {
544
+ topK: 3,
545
+ messageRange: 2,
546
+ },
547
+ threads: {
548
+ generateTitle: false,
549
+ },
550
+ },
551
+ });
552
+ // Reset message counter
553
+ messageCounter = 0;
554
+
555
+ // Create a new thread for each test
556
+ thread = await memory.saveThread({
557
+ thread: createTestThread('Working Memory Test Thread'),
558
+ });
559
+
560
+ expect(await memory.getWorkingMemory({ threadId: thread.id })).toBeNull();
561
+
562
+ agent = new Agent({
563
+ name: 'Memory Test Agent',
564
+ instructions: 'You are a helpful AI agent. Always add working memory tags to remember user information.',
565
+ model: openai('gpt-4o'),
566
+ memory,
567
+ });
568
+ });
569
+
570
+ afterEach(async () => {
571
+ //@ts-ignore
572
+ await storage.client.close();
573
+ //@ts-ignore
574
+ await vector.turso.close();
575
+ });
576
+
577
+ it('should accept valid working memory updates matching the schema', async () => {
578
+ const validMemory = { city: 'Austin', temperature: 85 };
579
+ await agent.generateVNext('I am in Austin and it is 85 degrees', {
580
+ threadId: thread.id,
581
+ resourceId,
582
+ });
583
+
584
+ const wmRaw = await memory.getWorkingMemory({ threadId: thread.id });
585
+ const wm = typeof wmRaw === 'string' ? JSON.parse(wmRaw) : wmRaw;
586
+ const wmObj = typeof wm === 'string' ? JSON.parse(wm) : wm;
587
+ expect(extractUserData(wmObj)).toMatchObject(validMemory);
588
+ });
589
+
590
+ it('should recall the most recent valid schema-based working memory', async () => {
591
+ const second = { city: 'Denver', temperature: 75 };
592
+ await agent.generateVNext('Now I am in Seattle and it is 60 degrees', {
593
+ threadId: thread.id,
594
+ resourceId,
595
+ });
596
+ await agent.generateVNext('Now I am in Denver and it is 75 degrees', {
597
+ threadId: thread.id,
598
+ resourceId,
599
+ });
600
+
601
+ const wmRaw = await memory.getWorkingMemory({ threadId: thread.id });
602
+ const wm = typeof wmRaw === 'string' ? JSON.parse(wmRaw) : wmRaw;
603
+ const wmObj = typeof wm === 'string' ? JSON.parse(wm) : wm;
604
+ expect(extractUserData(wmObj)).toMatchObject(second);
605
+ });
606
+
607
+ // Skip this for now it's an edge case where an agent updates the working memory based off of the
608
+ // message history.
609
+ it.skip('should not update working from message history', async () => {
610
+ const newThread = await memory.saveThread({
611
+ thread: createTestThread('Test111'),
612
+ });
613
+ const first = { city: 'Toronto', temperature: 80 };
614
+ const generateOptions = {
615
+ memory: {
616
+ resource: resourceId,
617
+ thread: newThread.id,
618
+ options: {
619
+ lastMessages: 0,
620
+ semanticRecall: undefined,
621
+ workingMemory: {
622
+ enabled: true,
623
+ schema: z.object({
624
+ city: z.string(),
625
+ temperature: z.number().optional(),
626
+ }),
627
+ },
628
+ threads: {
629
+ generateTitle: false,
630
+ },
631
+ },
632
+ },
633
+ };
634
+ await agent.generateVNext('Now I am in Toronto and it is 80 degrees', generateOptions);
635
+
636
+ await agent.generateVNext('how are you doing?', generateOptions);
637
+
638
+ const firstWorkingMemory = await memory.getWorkingMemory({ threadId: newThread.id });
639
+ const wm = typeof firstWorkingMemory === 'string' ? JSON.parse(firstWorkingMemory) : firstWorkingMemory;
640
+ const wmObj = typeof wm === 'string' ? JSON.parse(wm) : wm;
641
+
642
+ expect(wmObj).toMatchObject(first);
643
+
644
+ const updatedThread = await memory.getThreadById({ threadId: newThread.id });
645
+ if (!updatedThread) {
646
+ throw new Error('Thread not found');
647
+ }
648
+ // Update thread metadata with new working memory
649
+ await memory.saveThread({
650
+ thread: {
651
+ ...updatedThread,
652
+ metadata: {
653
+ ...(updatedThread.metadata || {}),
654
+ workingMemory: { city: 'Waterloo', temperature: 78 },
655
+ },
656
+ },
657
+ memoryConfig: generateOptions.memory.options,
658
+ });
659
+
660
+ // This should not update the working memory
661
+ await agent.generateVNext('how are you doing?', generateOptions);
662
+
663
+ const result = await agent.generateVNext('Can you tell me where I am?', generateOptions);
664
+
665
+ expect(result.text).toContain('Waterloo');
666
+ const secondWorkingMemory = await memory.getWorkingMemory({ threadId: newThread.id });
667
+ expect(secondWorkingMemory).toMatchObject({ city: 'Waterloo', temperature: 78 });
668
+ });
669
+ });
670
+ });
671
+
672
+ describe('Working Memory with JSONSchema7', () => {
673
+ let agent: Agent;
674
+ let thread: any;
675
+ let memory: Memory;
676
+
677
+ beforeEach(async () => {
678
+ const dbPath = join(await mkdtemp(join(tmpdir(), `memory-jsonschema-test-${Date.now()}`)), 'test.db');
679
+ storage = new LibSQLStore({
680
+ url: `file:${dbPath}`,
681
+ });
682
+ vector = new LibSQLVector({
683
+ connectionUrl: `file:${dbPath}`,
684
+ });
685
+
686
+ const jsonSchema: JSONSchema7 = {
687
+ type: 'object',
688
+ properties: {
689
+ name: { type: 'string' },
690
+ age: { type: 'number' },
691
+ city: { type: 'string' },
692
+ preferences: {
693
+ type: 'object',
694
+ properties: {
695
+ theme: { type: 'string' },
696
+ notifications: { type: 'boolean' },
697
+ },
698
+ },
699
+ },
700
+ required: ['name', 'city'],
701
+ };
702
+
703
+ memory = new Memory({
704
+ storage,
705
+ vector,
706
+ embedder: fastembed,
707
+ options: {
708
+ workingMemory: {
709
+ enabled: true,
710
+ schema: jsonSchema,
711
+ },
712
+ lastMessages: 10,
713
+ semanticRecall: {
714
+ topK: 3,
715
+ messageRange: 2,
716
+ },
717
+ threads: {
718
+ generateTitle: false,
719
+ },
720
+ },
721
+ });
722
+
723
+ // Reset message counter
724
+ messageCounter = 0;
725
+
726
+ // Create a new thread for each test
727
+ thread = await memory.saveThread({
728
+ thread: createTestThread('JSONSchema7 Working Memory Test Thread'),
729
+ });
730
+
731
+ // Verify initial working memory is empty
732
+ expect(await memory.getWorkingMemory({ threadId: thread.id })).toBeNull();
733
+
734
+ agent = new Agent({
735
+ name: 'JSONSchema Memory Test Agent',
736
+ instructions: 'You are a helpful AI agent. Always update working memory with user information.',
737
+ model: openai('gpt-4o'),
738
+ memory,
739
+ });
740
+ });
741
+
742
+ afterEach(async () => {
743
+ //@ts-ignore
744
+ await storage.client.close();
745
+ //@ts-ignore
746
+ await vector.turso.close();
747
+ });
748
+
749
+ it('should accept JSONSchema7 in working memory configuration', async () => {
750
+ // Test that we can create a Memory instance with JSONSchema7 schema
751
+ const jsonSchema: JSONSchema7 = {
752
+ type: 'object',
753
+ properties: {
754
+ testField: { type: 'string' },
755
+ },
756
+ required: ['testField'],
757
+ };
758
+
759
+ const testMemory = new Memory({
760
+ storage,
761
+ options: {
762
+ workingMemory: {
763
+ enabled: true,
764
+ schema: jsonSchema,
765
+ },
766
+ },
767
+ });
768
+
769
+ // Get the working memory template
770
+ const template = await testMemory.getWorkingMemoryTemplate({
771
+ memoryConfig: {
772
+ workingMemory: {
773
+ enabled: true,
774
+ schema: jsonSchema,
775
+ },
776
+ },
777
+ });
778
+
779
+ expect(template).not.toBeNull();
780
+ expect(template?.format).toBe('json');
781
+ expect(template?.content).toContain('testField');
782
+ expect(template?.content).toContain('string');
783
+ });
784
+
785
+ it('should accept valid working memory updates matching the JSONSchema7', async () => {
786
+ await agent.generateVNext(
787
+ 'Hi, my name is John Doe, I am 30 years old and I live in Boston. I prefer dark theme and want notifications enabled.',
788
+ {
789
+ threadId: thread.id,
790
+ resourceId,
791
+ },
792
+ );
793
+
794
+ const wmRaw = await memory.getWorkingMemory({ threadId: thread.id });
795
+ const wm = typeof wmRaw === 'string' ? JSON.parse(wmRaw) : wmRaw;
796
+ const wmObj = typeof wm === 'string' ? JSON.parse(wm) : wm;
797
+ const userData = extractUserData(wmObj);
798
+
799
+ expect(userData.name).toBe('John Doe');
800
+ expect(userData.age).toBe(30);
801
+ expect(userData.city).toBe('Boston');
802
+ });
803
+
804
+ it('should handle required and optional fields correctly with JSONSchema7', async () => {
805
+ // Test with only required fields
806
+ await agent.generateVNext('My name is Jane Smith and I live in Portland.', {
807
+ threadId: thread.id,
808
+ resourceId,
809
+ });
810
+
811
+ const wmRaw = await memory.getWorkingMemory({ threadId: thread.id });
812
+ const wm = typeof wmRaw === 'string' ? JSON.parse(wmRaw) : wmRaw;
813
+ const wmObj = typeof wm === 'string' ? JSON.parse(wm) : wm;
814
+ const userData = extractUserData(wmObj);
815
+
816
+ expect(userData.name).toBe('Jane Smith');
817
+ expect(userData.city).toBe('Portland');
818
+ // Age is not required, so it might not be set
819
+ });
820
+
821
+ it('should update working memory progressively with JSONSchema7', async () => {
822
+ // First message with partial info
823
+ await agent.generateVNext('Hi, I am Alex and I live in Miami.', {
824
+ threadId: thread.id,
825
+ resourceId,
826
+ });
827
+
828
+ let wmRaw = await memory.getWorkingMemory({ threadId: thread.id });
829
+ let wm = typeof wmRaw === 'string' ? JSON.parse(wmRaw) : wmRaw;
830
+ let wmObj = typeof wm === 'string' ? JSON.parse(wm) : wm;
831
+ let userData = extractUserData(wmObj);
832
+
833
+ expect(userData.name).toBe('Alex');
834
+ expect(userData.city).toBe('Miami');
835
+
836
+ // Second message adding more info
837
+ await agent.generateVNext('I am 25 years old.', {
838
+ threadId: thread.id,
839
+ resourceId,
840
+ });
841
+
842
+ wmRaw = await memory.getWorkingMemory({ threadId: thread.id });
843
+ wm = typeof wmRaw === 'string' ? JSON.parse(wmRaw) : wmRaw;
844
+ wmObj = typeof wm === 'string' ? JSON.parse(wm) : wm;
845
+ userData = extractUserData(wmObj);
846
+
847
+ expect(userData.name).toBe('Alex');
848
+ expect(userData.city).toBe('Miami');
849
+ expect(userData.age).toBe(25);
850
+ });
851
+
852
+ it('should persist working memory across multiple interactions with JSONSchema7', async () => {
853
+ // Set initial data
854
+ await agent.generateVNext('My name is Sarah Wilson, I am 28 and live in Seattle.', {
855
+ threadId: thread.id,
856
+ resourceId,
857
+ });
858
+
859
+ // Verify working memory is set
860
+ let wmRaw = await memory.getWorkingMemory({ threadId: thread.id });
861
+ let wm = typeof wmRaw === 'string' ? JSON.parse(wmRaw) : wmRaw;
862
+ let wmObj = typeof wm === 'string' ? JSON.parse(wm) : wm;
863
+ let userData = extractUserData(wmObj);
864
+ expect(userData.name).toBe('Sarah Wilson');
865
+
866
+ // Ask a question that should use the working memory
867
+ const response = await agent.generateVNext('What is my name and where do I live?', {
868
+ threadId: thread.id,
869
+ resourceId,
870
+ });
871
+
872
+ // The response should contain the information from working memory
873
+ expect(response.text.toLowerCase()).toContain('sarah');
874
+ expect(response.text.toLowerCase()).toContain('seattle');
875
+ });
876
+ });
877
+
878
+ describe('Resource-Scoped Working Memory Tests', () => {
879
+ beforeEach(async () => {
880
+ // Create a new unique database file in the temp directory for each test
881
+ const dbPath = join(await mkdtemp(join(tmpdir(), `memory-resource-working-test-`)), 'test.db');
882
+ console.log('dbPath', dbPath);
883
+
884
+ storage = new LibSQLStore({
885
+ url: `file:${dbPath}`,
886
+ });
887
+ vector = new LibSQLVector({
888
+ connectionUrl: `file:${dbPath}`,
889
+ });
890
+
891
+ // Create memory instance with resource-scoped working memory enabled
892
+ memory = new Memory({
893
+ options: {
894
+ workingMemory: {
895
+ enabled: true,
896
+ scope: 'resource',
897
+ template: `# User Information
898
+ - **First Name**:
899
+ - **Last Name**:
900
+ - **Location**:
901
+ - **Interests**:
902
+ `,
903
+ },
904
+ lastMessages: 10,
905
+ semanticRecall: {
906
+ topK: 3,
907
+ messageRange: 2,
908
+ },
909
+ threads: {
910
+ generateTitle: false,
911
+ },
912
+ },
913
+ storage,
914
+ vector,
915
+ embedder: fastembed,
916
+ });
917
+ // Reset message counter
918
+ messageCounter = 0;
919
+ // Create a new thread for each test
920
+ thread = await memory.saveThread({
921
+ thread: createTestThread('Resource Working Memory Test Thread'),
922
+ });
923
+ });
924
+
925
+ afterEach(async () => {
926
+ //@ts-ignore
927
+ await storage.client.close();
928
+ //@ts-ignore
929
+ await vector.turso.close();
930
+ });
931
+
932
+ it('should store working memory at resource level', async () => {
933
+ // Update working memory using the updateWorkingMemory method
934
+ const workingMemoryData = `# User Information
935
+ - **First Name**: John
936
+ - **Last Name**: Doe
937
+ - **Location**: New York
938
+ - **Interests**: AI, Machine Learning
939
+ `;
940
+
941
+ await memory.updateWorkingMemory({
942
+ threadId: thread.id,
943
+ resourceId,
944
+ workingMemory: workingMemoryData,
945
+ });
946
+
947
+ // Get working memory and verify it's stored at resource level
948
+ const retrievedWorkingMemory = await memory.getWorkingMemory({
949
+ threadId: thread.id,
950
+ resourceId,
951
+ });
952
+
953
+ expect(retrievedWorkingMemory).toBe(workingMemoryData);
954
+ });
955
+
956
+ it('should share working memory across multiple threads for the same resource', async () => {
957
+ // Create a second thread for the same resource
958
+ const thread2 = await memory.saveThread({
959
+ thread: createTestThread('Second Resource Working Memory Test Thread'),
960
+ });
961
+
962
+ // Update working memory from first thread
963
+ const workingMemoryData = `# User Information
964
+ - **First Name**: Alice
965
+ - **Last Name**: Smith
966
+ - **Location**: California
967
+ - **Interests**: Data Science, Python
968
+ `;
969
+
970
+ await memory.updateWorkingMemory({
971
+ threadId: thread.id,
972
+ resourceId,
973
+ workingMemory: workingMemoryData,
974
+ });
975
+
976
+ // Retrieve working memory from second thread
977
+ const retrievedFromThread2 = await memory.getWorkingMemory({
978
+ threadId: thread2.id,
979
+ resourceId,
980
+ });
981
+
982
+ expect(retrievedFromThread2).toBe(workingMemoryData);
983
+ });
984
+
985
+ it('should update working memory across all threads when updated from any thread', async () => {
986
+ // Create multiple threads for the same resource
987
+ const thread2 = await memory.saveThread({
988
+ thread: createTestThread('Second Thread'),
989
+ });
990
+ const thread3 = await memory.saveThread({
991
+ thread: createTestThread('Third Thread'),
992
+ });
993
+
994
+ // Set initial working memory from thread1
995
+ const initialWorkingMemory = `# User Information
996
+ - **First Name**: Bob
997
+ - **Last Name**: Johnson
998
+ - **Location**: Texas
999
+ - **Interests**: Software Development
1000
+ `;
1001
+
1002
+ await memory.updateWorkingMemory({
1003
+ threadId: thread.id,
1004
+ resourceId,
1005
+ workingMemory: initialWorkingMemory,
1006
+ });
1007
+
1008
+ // Update working memory from thread2
1009
+ const updatedWorkingMemory = `# User Information
1010
+ - **First Name**: Bob
1011
+ - **Last Name**: Johnson
1012
+ - **Location**: Florida
1013
+ - **Interests**: Software Development, Travel
1014
+ `;
1015
+
1016
+ await memory.updateWorkingMemory({
1017
+ threadId: thread2.id,
1018
+ resourceId,
1019
+ workingMemory: updatedWorkingMemory,
1020
+ });
1021
+
1022
+ // Verify all threads see the updated working memory
1023
+ const wmFromThread1 = await memory.getWorkingMemory({ threadId: thread.id, resourceId });
1024
+ const wmFromThread2 = await memory.getWorkingMemory({ threadId: thread2.id, resourceId });
1025
+ const wmFromThread3 = await memory.getWorkingMemory({ threadId: thread3.id, resourceId });
1026
+
1027
+ expect(wmFromThread1).toBe(updatedWorkingMemory);
1028
+ expect(wmFromThread2).toBe(updatedWorkingMemory);
1029
+ expect(wmFromThread3).toBe(updatedWorkingMemory);
1030
+ });
1031
+
1032
+ it('should handle JSON format correctly for resource-scoped working memory', async () => {
1033
+ const workingMemoryData = `{"name":"Charlie","age":30,"city":"Seattle"}`;
1034
+
1035
+ await memory.updateWorkingMemory({
1036
+ threadId: thread.id,
1037
+ resourceId,
1038
+ workingMemory: workingMemoryData,
1039
+ });
1040
+
1041
+ // Test JSON format retrieval
1042
+ const retrievedAsJson = await memory.getWorkingMemory({
1043
+ threadId: thread.id,
1044
+ resourceId,
1045
+ });
1046
+
1047
+ expect(retrievedAsJson).toBe(`{"name":"Charlie","age":30,"city":"Seattle"}`);
1048
+
1049
+ // Test default format retrieval
1050
+ const retrievedDefault = await memory.getWorkingMemory({
1051
+ threadId: thread.id,
1052
+ resourceId,
1053
+ });
1054
+
1055
+ expect(retrievedDefault).toBe(workingMemoryData);
1056
+ });
1057
+
1058
+ it('should verify storage adapter support for resource working memory', async () => {
1059
+ // This test would require a mock storage adapter that doesn't support resource working memory
1060
+ // For now, we'll just verify that LibSQL supports it
1061
+ expect(storage.supports.resourceWorkingMemory).toBe(true);
1062
+ });
1063
+ });
1064
+ });