@mastra/memory 0.11.2 → 0.11.3-alpha.1

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.
package/dist/index.js CHANGED
@@ -1,16 +1,19 @@
1
- import { deepMerge } from '@mastra/core';
1
+ import { generateEmptyFromSchema } from '@mastra/core';
2
2
  import { MessageList } from '@mastra/core/agent';
3
3
  import { MastraMemory } from '@mastra/core/memory';
4
4
  import { embedMany } from 'ai';
5
+ import { Mutex } from 'async-mutex';
5
6
  import xxhash from 'xxhash-wasm';
7
+ import { z, ZodObject } from 'zod';
6
8
  import zodToJsonSchema from 'zod-to-json-schema';
7
- import { z } from 'zod';
8
9
 
9
10
  // src/index.ts
10
- var updateWorkingMemoryTool = ({ format }) => ({
11
- description: "Update the working memory with new information",
11
+ var updateWorkingMemoryTool = (memoryConfig) => ({
12
+ description: "Update the working memory with new information. Always pass data as string to the memory field. Never pass an object.",
12
13
  parameters: z.object({
13
- memory: z.string().describe(`The ${format === "json" ? "JSON" : "Markdown"} formatted working memory content to store`)
14
+ memory: z.string().describe(
15
+ `The ${!!memoryConfig?.workingMemory?.schema ? "JSON" : "Markdown"} formatted working memory content to store. This MUST be a string. Never pass an object.`
16
+ )
14
17
  }),
15
18
  execute: async (params) => {
16
19
  const { context, threadId, memory, resourceId } = params;
@@ -28,16 +31,68 @@ var updateWorkingMemoryTool = ({ format }) => ({
28
31
  await memory.updateWorkingMemory({
29
32
  threadId,
30
33
  resourceId: resourceId || thread.resourceId,
31
- workingMemory
34
+ workingMemory,
35
+ memoryConfig
32
36
  });
33
37
  return { success: true };
34
38
  }
35
39
  });
40
+ var __experimental_updateWorkingMemoryToolVNext = (config) => ({
41
+ description: "Update the working memory with new information.",
42
+ parameters: z.object({
43
+ newMemory: z.string().optional().nullable().describe(`The ${config.workingMemory?.schema ? "JSON" : "Markdown"} formatted working memory content to store`),
44
+ searchString: z.string().optional().nullable().describe(
45
+ "The working memory string to find. Will be replaced with the newMemory string. If this is omitted or doesn't exist, the newMemory string will be appended to the end of your working memory. Replacing single lines at a time is encouraged for greater accuracy. If updateReason is not 'append-new-memory', this search string must be provided or the tool call will be rejected."
46
+ ),
47
+ updateReason: z.enum(["append-new-memory", "clarify-existing-memory", "replace-irrelevant-memory"]).optional().nullable().describe(
48
+ "The reason you're updating working memory. Passing any value other than 'append-new-memory' requires a searchString to be provided. Defaults to append-new-memory"
49
+ )
50
+ }),
51
+ execute: async (params) => {
52
+ const { context, threadId, memory, resourceId } = params;
53
+ if (!threadId || !memory) {
54
+ throw new Error("Thread ID and Memory instance are required for working memory updates");
55
+ }
56
+ const thread = await memory.getThreadById({ threadId });
57
+ if (!thread) {
58
+ throw new Error(`Thread ${threadId} not found`);
59
+ }
60
+ if (thread.resourceId && thread.resourceId !== resourceId) {
61
+ throw new Error(`Thread with id ${threadId} resourceId does not match the current resourceId ${resourceId}`);
62
+ }
63
+ const workingMemory = context.newMemory || "";
64
+ if (!context.updateReason) context.updateReason = `append-new-memory`;
65
+ if (context.searchString && config.workingMemory?.scope === `resource` && context.updateReason === `replace-irrelevant-memory`) {
66
+ context.searchString = void 0;
67
+ }
68
+ if (context.updateReason === `append-new-memory` && context.searchString) {
69
+ context.searchString = void 0;
70
+ }
71
+ if (context.updateReason !== `append-new-memory` && !context.searchString) {
72
+ return {
73
+ success: false,
74
+ reason: `updateReason was ${context.updateReason} but no searchString was provided. Unable to replace undefined with "${context.newMemory}"`
75
+ };
76
+ }
77
+ const result = await memory.__experimental_updateWorkingMemoryVNext({
78
+ threadId,
79
+ resourceId: resourceId || thread.resourceId,
80
+ workingMemory,
81
+ searchString: context.searchString,
82
+ memoryConfig: config
83
+ });
84
+ if (result) {
85
+ return result;
86
+ }
87
+ return { success: true };
88
+ }
89
+ });
36
90
 
37
91
  // src/index.ts
38
92
  var CHARS_PER_TOKEN = 4;
39
93
  var DEFAULT_MESSAGE_RANGE = { before: 2, after: 2 };
40
94
  var DEFAULT_TOP_K = 2;
95
+ var isZodObject = (v) => v instanceof ZodObject;
41
96
  var Memory = class extends MastraMemory {
42
97
  constructor(config = {}) {
43
98
  super({ name: "Memory", ...config });
@@ -194,39 +249,7 @@ var Memory = class extends MastraMemory {
194
249
  async getThreadsByResourceId({ resourceId }) {
195
250
  return this.storage.getThreadsByResourceId({ resourceId });
196
251
  }
197
- async saveThread({
198
- thread,
199
- memoryConfig
200
- }) {
201
- const config = this.getMergedThreadConfig(memoryConfig || {});
202
- if (config.workingMemory?.enabled) {
203
- const scope = config.workingMemory.scope || "thread";
204
- if (scope === "resource" && thread.resourceId) {
205
- const existingResource = await this.storage.getResourceById({ resourceId: thread.resourceId });
206
- if (!existingResource?.workingMemory) {
207
- let workingMemory = config.workingMemory.template || this.defaultWorkingMemoryTemplate;
208
- if (config.workingMemory.schema) {
209
- workingMemory = JSON.stringify(zodToJsonSchema(config.workingMemory.schema));
210
- }
211
- await this.storage.updateResource({
212
- resourceId: thread.resourceId,
213
- workingMemory
214
- });
215
- }
216
- } else if (scope === "thread" && !thread?.metadata?.workingMemory) {
217
- let workingMemory = config.workingMemory.template || this.defaultWorkingMemoryTemplate;
218
- if (config.workingMemory.schema) {
219
- workingMemory = JSON.stringify(zodToJsonSchema(config.workingMemory.schema));
220
- }
221
- return this.storage.saveThread({
222
- thread: deepMerge(thread, {
223
- metadata: {
224
- workingMemory
225
- }
226
- })
227
- });
228
- }
229
- }
252
+ async saveThread({ thread }) {
230
253
  return this.storage.saveThread({ thread });
231
254
  }
232
255
  async updateThread({
@@ -274,6 +297,87 @@ var Memory = class extends MastraMemory {
274
297
  });
275
298
  }
276
299
  }
300
+ updateWorkingMemoryMutexes = /* @__PURE__ */ new Map();
301
+ /**
302
+ * @warning experimental! can be removed or changed at any time
303
+ */
304
+ async __experimental_updateWorkingMemoryVNext({
305
+ threadId,
306
+ resourceId,
307
+ workingMemory,
308
+ searchString,
309
+ memoryConfig
310
+ }) {
311
+ const config = this.getMergedThreadConfig(memoryConfig || {});
312
+ if (!config.workingMemory?.enabled) {
313
+ throw new Error("Working memory is not enabled for this memory instance");
314
+ }
315
+ const mutexKey = memoryConfig?.workingMemory?.scope === `resource` ? `resource-${resourceId}` : `thread-${threadId}`;
316
+ const mutex = this.updateWorkingMemoryMutexes.has(mutexKey) ? this.updateWorkingMemoryMutexes.get(mutexKey) : new Mutex();
317
+ this.updateWorkingMemoryMutexes.set(mutexKey, mutex);
318
+ const release = await mutex.acquire();
319
+ try {
320
+ const existingWorkingMemory = await this.getWorkingMemory({ threadId, resourceId, memoryConfig }) || "";
321
+ const template = await this.getWorkingMemoryTemplate({ memoryConfig });
322
+ let reason = "";
323
+ if (existingWorkingMemory) {
324
+ if (searchString && existingWorkingMemory?.includes(searchString)) {
325
+ workingMemory = existingWorkingMemory.replace(searchString, workingMemory);
326
+ reason = `found and replaced searchString with newMemory`;
327
+ } else if (existingWorkingMemory.includes(workingMemory) || template?.content?.trim() === workingMemory.trim()) {
328
+ return {
329
+ success: false,
330
+ reason: `attempted to insert duplicate data into working memory. this entry was skipped`
331
+ };
332
+ } else {
333
+ if (searchString) {
334
+ reason = `attempted to replace working memory string that doesn't exist. Appending to working memory instead.`;
335
+ } else {
336
+ reason = `appended newMemory to end of working memory`;
337
+ }
338
+ workingMemory = existingWorkingMemory + `
339
+ ${workingMemory}`;
340
+ }
341
+ } else if (workingMemory === template?.content) {
342
+ return {
343
+ success: false,
344
+ reason: `try again when you have data to add. newMemory was equal to the working memory template`
345
+ };
346
+ } else {
347
+ reason = `started new working memory`;
348
+ }
349
+ workingMemory = template?.content ? workingMemory.replaceAll(template?.content, "") : workingMemory;
350
+ const scope = config.workingMemory.scope || "thread";
351
+ if (scope === "resource" && resourceId) {
352
+ await this.storage.updateResource({
353
+ resourceId,
354
+ workingMemory
355
+ });
356
+ if (reason) {
357
+ return { success: true, reason };
358
+ }
359
+ } else {
360
+ const thread = await this.storage.getThreadById({ threadId });
361
+ if (!thread) {
362
+ throw new Error(`Thread ${threadId} not found`);
363
+ }
364
+ await this.storage.updateThread({
365
+ id: threadId,
366
+ title: thread.title || "Untitled Thread",
367
+ metadata: {
368
+ ...thread.metadata,
369
+ workingMemory
370
+ }
371
+ });
372
+ }
373
+ return { success: true, reason };
374
+ } catch (e) {
375
+ this.logger.error(e instanceof Error ? e.stack || e.message : JSON.stringify(e));
376
+ return { success: false, reason: "Tool error." };
377
+ } finally {
378
+ release();
379
+ }
380
+ }
277
381
  chunkText(text, tokenSize = 4096) {
278
382
  const charSize = tokenSize * CHARS_PER_TOKEN;
279
383
  const chunks = [];
@@ -472,23 +576,38 @@ var Memory = class extends MastraMemory {
472
576
  }
473
577
  return workingMemoryData;
474
578
  }
475
- async getWorkingMemoryTemplate() {
476
- if (!this.threadConfig.workingMemory?.enabled) {
579
+ /**
580
+ * Gets the working memory template for the current memory configuration.
581
+ * Supports both ZodObject and JSONSchema7 schemas.
582
+ *
583
+ * @param memoryConfig - The memory configuration containing the working memory settings
584
+ * @returns The working memory template with format and content, or null if working memory is disabled
585
+ */
586
+ async getWorkingMemoryTemplate({
587
+ memoryConfig
588
+ }) {
589
+ const config = this.getMergedThreadConfig(memoryConfig || {});
590
+ if (!config.workingMemory?.enabled) {
477
591
  return null;
478
592
  }
479
- if (this.threadConfig?.workingMemory?.schema) {
593
+ if (config.workingMemory?.schema) {
480
594
  try {
481
- const schema = this.threadConfig.workingMemory.schema;
482
- const convertedSchema = zodToJsonSchema(schema, {
483
- $refStrategy: "none"
484
- });
595
+ const schema = config.workingMemory.schema;
596
+ let convertedSchema;
597
+ if (isZodObject(schema)) {
598
+ convertedSchema = zodToJsonSchema(schema, {
599
+ $refStrategy: "none"
600
+ });
601
+ } else {
602
+ convertedSchema = schema;
603
+ }
485
604
  return { format: "json", content: JSON.stringify(convertedSchema) };
486
605
  } catch (error) {
487
606
  this.logger.error("Error converting schema", error);
488
607
  throw error;
489
608
  }
490
609
  }
491
- const memory = this.threadConfig.workingMemory.template || this.defaultWorkingMemoryTemplate;
610
+ const memory = config.workingMemory.template || this.defaultWorkingMemoryTemplate;
492
611
  return { format: "markdown", content: memory.trim() };
493
612
  }
494
613
  async getSystemMessage({
@@ -500,12 +619,15 @@ var Memory = class extends MastraMemory {
500
619
  if (!config.workingMemory?.enabled) {
501
620
  return null;
502
621
  }
503
- const workingMemoryTemplate = await this.getWorkingMemoryTemplate();
622
+ const workingMemoryTemplate = await this.getWorkingMemoryTemplate({ memoryConfig: config });
504
623
  const workingMemoryData = await this.getWorkingMemory({ threadId, resourceId, memoryConfig: config });
505
624
  if (!workingMemoryTemplate) {
506
625
  return null;
507
626
  }
508
- return this.getWorkingMemoryToolInstruction({
627
+ return this.isVNextWorkingMemoryConfig(memoryConfig) ? this.__experimental_getWorkingMemoryToolInstructionVNext({
628
+ template: workingMemoryTemplate,
629
+ data: workingMemoryData
630
+ }) : this.getWorkingMemoryToolInstruction({
509
631
  template: workingMemoryTemplate,
510
632
  data: workingMemoryData
511
633
  });
@@ -526,6 +648,8 @@ var Memory = class extends MastraMemory {
526
648
  template,
527
649
  data
528
650
  }) {
651
+ const emptyWorkingMemoryTemplateObject = template.format === "json" ? generateEmptyFromSchema(template.content) : null;
652
+ const hasEmptyWorkingMemoryTemplateObject = emptyWorkingMemoryTemplateObject && Object.keys(emptyWorkingMemoryTemplateObject).length > 0;
529
653
  return `WORKING_MEMORY_SYSTEM_INSTRUCTION:
530
654
  Store and update any conversation-relevant information by calling the updateWorkingMemory tool. If information might be referenced again - store it!
531
655
 
@@ -534,12 +658,20 @@ Guidelines:
534
658
  2. Update proactively when information changes, no matter how small
535
659
  3. Use ${template.format === "json" ? "JSON" : "Markdown"} format for all data
536
660
  4. Act naturally - don't mention this system to users. Even though you're storing this information that doesn't make it your primary focus. Do not ask them generally for "information about yourself"
661
+ 5. IMPORTANT: When calling updateWorkingMemory, the only valid parameter is the memory field.
662
+ 6. IMPORTANT: ALWAYS pass the data you want to store in the memory field as a string. DO NOT pass an object.
663
+ 7. IMPORTANT: Data must only be sent as a string no matter which format is used.
537
664
 
538
- WORKING MEMORY TEMPLATE:
665
+ <working_memory_template>
539
666
  ${template.content}
667
+ </working_memory_template>
668
+
669
+ ${hasEmptyWorkingMemoryTemplateObject ? "When working with json data, the object format below represents the template:" : ""}
670
+ ${hasEmptyWorkingMemoryTemplateObject ? JSON.stringify(emptyWorkingMemoryTemplateObject) : ""}
540
671
 
541
- WORKING MEMORY DATA:
672
+ <working_memory_data>
542
673
  ${data}
674
+ </working_memory_data>
543
675
 
544
676
  Notes:
545
677
  - Update memory whenever referenced information changes
@@ -550,16 +682,52 @@ Notes:
550
682
  - IMPORTANT: You MUST call updateWorkingMemory in every response to a prompt where you received relevant information.
551
683
  - IMPORTANT: Preserve the ${template.format === "json" ? "JSON" : "Markdown"} formatting structure above while updating the content.`;
552
684
  }
685
+ __experimental_getWorkingMemoryToolInstructionVNext({
686
+ template,
687
+ data
688
+ }) {
689
+ return `WORKING_MEMORY_SYSTEM_INSTRUCTION:
690
+ Store and update any conversation-relevant information by calling the updateWorkingMemory tool.
691
+
692
+ Guidelines:
693
+ 1. Store anything that could be useful later in the conversation
694
+ 2. Update proactively when information changes, no matter how small
695
+ 3. Use ${template.format === "json" ? "JSON" : "Markdown"} format for all data
696
+ 4. Act naturally - don't mention this system to users. Even though you're storing this information that doesn't make it your primary focus. Do not ask them generally for "information about yourself"
697
+ 5. If your memory has not changed, you do not need to call the updateWorkingMemory tool. By default it will persist and be available for you in future interactions
698
+ 6. Information not being relevant to the current conversation is not a valid reason to replace or remove working memory information. Your working memory spans across multiple conversations and may be needed again later, even if it's not currently relevant.
699
+
700
+ <working_memory_template>
701
+ ${template.content}
702
+ </working_memory_template>
703
+
704
+ <working_memory_data>
705
+ ${data}
706
+ </working_memory_data>
707
+
708
+ Notes:
709
+ - Update memory whenever referenced information changes
710
+ ${template.content !== this.defaultWorkingMemoryTemplate ? `- Only store information if it's in the working memory template, do not store other information unless the user asks you to remember it, as that non-template information may be irrelevant` : `- If you're unsure whether to store something, store it (eg if the user tells you information about themselves, call updateWorkingMemory immediately to update it)
711
+ `}
712
+ - This system is here so that you can maintain the conversation when your context window is very short. Update your working memory because you may need it to maintain the conversation without the full conversation history
713
+ - REMEMBER: the way you update your working memory is by calling the updateWorkingMemory tool with the ${template.format === "json" ? "JSON" : "Markdown"} content. The system will store it for you. The user will not see it.
714
+ - IMPORTANT: You MUST call updateWorkingMemory in every response to a prompt where you received relevant information if that information is not already stored.
715
+ - IMPORTANT: Preserve the ${template.format === "json" ? "JSON" : "Markdown"} formatting structure above while updating the content.
716
+ `;
717
+ }
718
+ isVNextWorkingMemoryConfig(config) {
719
+ if (!config?.workingMemory) return false;
720
+ const isMDWorkingMemory = !(`schema` in config.workingMemory) && (typeof config.workingMemory.template === `string` || config.workingMemory.template) && config.workingMemory;
721
+ return Boolean(isMDWorkingMemory && isMDWorkingMemory.version === `vnext`);
722
+ }
553
723
  getTools(config) {
554
724
  const mergedConfig = this.getMergedThreadConfig(config);
555
725
  if (mergedConfig.workingMemory?.enabled) {
556
- if (mergedConfig.workingMemory.schema) {
557
- return {
558
- updateWorkingMemory: updateWorkingMemoryTool({ format: "json" })
559
- };
560
- }
561
726
  return {
562
- updateWorkingMemory: updateWorkingMemoryTool({ format: "markdown" })
727
+ updateWorkingMemory: this.isVNextWorkingMemoryConfig(mergedConfig) ? (
728
+ // use the new experimental tool
729
+ __experimental_updateWorkingMemoryToolVNext(mergedConfig)
730
+ ) : updateWorkingMemoryTool(mergedConfig)
563
731
  };
564
732
  }
565
733
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/memory",
3
- "version": "0.11.2",
3
+ "version": "0.11.3-alpha.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,10 +35,12 @@
35
35
  "@upstash/redis": "^1.35.0",
36
36
  "ai": "^4.3.16",
37
37
  "js-tiktoken": "^1.0.20",
38
+ "json-schema": "^0.4.0",
38
39
  "pg": "^8.16.3",
39
40
  "pg-pool": "^3.10.1",
40
41
  "postgres": "^3.4.7",
41
42
  "redis": "^4.7.1",
43
+ "async-mutex": "^0.5.0",
42
44
  "xxhash-wasm": "^1.1.0",
43
45
  "zod": "^3.25.67",
44
46
  "zod-to-json-schema": "^3.24.5"
@@ -46,15 +48,16 @@
46
48
  "devDependencies": {
47
49
  "@ai-sdk/openai": "^1.3.22",
48
50
  "@microsoft/api-extractor": "^7.52.8",
51
+ "@types/json-schema": "^7.0.15",
49
52
  "@types/node": "^20.19.0",
50
53
  "@types/pg": "^8.15.4",
51
- "eslint": "^9.29.0",
54
+ "eslint": "^9.30.1",
52
55
  "tsup": "^8.5.0",
53
56
  "typescript": "^5.8.3",
54
57
  "typescript-eslint": "^8.34.0",
55
58
  "vitest": "^3.2.4",
56
- "@mastra/core": "0.10.11",
57
- "@internal/lint": "0.0.18"
59
+ "@internal/lint": "0.0.19",
60
+ "@mastra/core": "0.10.15-alpha.1"
58
61
  },
59
62
  "peerDependencies": {
60
63
  "@mastra/core": ">=0.10.9-0 <0.11.0-0"