@mastra/memory 0.11.3-alpha.0 → 0.11.3
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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +37 -0
- package/dist/_tsup-dts-rollup.d.cts +22 -1
- package/dist/_tsup-dts-rollup.d.ts +22 -1
- package/dist/index.cjs +183 -30
- package/dist/index.js +184 -31
- package/package.json +4 -3
- package/src/index.ts +184 -43
- package/src/tools/working-memory.ts +83 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/memory@0.11.3-alpha.
|
|
2
|
+
> @mastra/memory@0.11.3-alpha.1 build /home/runner/work/mastra/mastra/packages/memory
|
|
3
3
|
> pnpm run check && tsup --silent src/index.ts src/processors/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @mastra/memory@0.11.3-alpha.
|
|
6
|
+
> @mastra/memory@0.11.3-alpha.1 check /home/runner/work/mastra/mastra/packages/memory
|
|
7
7
|
> tsc --noEmit
|
|
8
8
|
|
|
9
9
|
Analysis will use the bundled TypeScript version 5.8.3
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @mastra/memory
|
|
2
2
|
|
|
3
|
+
## 0.11.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4b20131: Fixed an issue where per-resource semantic recall wouldn't always be enabled properly in agent tool calls
|
|
8
|
+
- 2ba5b76: Allow passing jsonSchema into workingMemory schema
|
|
9
|
+
- c3a30de: added new experimental vnext working memory
|
|
10
|
+
- 626b0f4: [Cloud-126] Working Memory Playground - Added working memory to playground to allow users to view/edit working memory
|
|
11
|
+
- Updated dependencies [0b56518]
|
|
12
|
+
- Updated dependencies [db5cc15]
|
|
13
|
+
- Updated dependencies [2ba5b76]
|
|
14
|
+
- Updated dependencies [5237998]
|
|
15
|
+
- Updated dependencies [c3a30de]
|
|
16
|
+
- Updated dependencies [37c1acd]
|
|
17
|
+
- Updated dependencies [1aa60b1]
|
|
18
|
+
- Updated dependencies [89ec9d4]
|
|
19
|
+
- Updated dependencies [cf3a184]
|
|
20
|
+
- Updated dependencies [d6bfd60]
|
|
21
|
+
- Updated dependencies [626b0f4]
|
|
22
|
+
- Updated dependencies [c22a91f]
|
|
23
|
+
- Updated dependencies [f7403ab]
|
|
24
|
+
- Updated dependencies [6c89d7f]
|
|
25
|
+
- @mastra/core@0.10.15
|
|
26
|
+
|
|
27
|
+
## 0.11.3-alpha.1
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 2ba5b76: Allow passing jsonSchema into workingMemory schema
|
|
32
|
+
- c3a30de: added new experimental vnext working memory
|
|
33
|
+
- Updated dependencies [0b56518]
|
|
34
|
+
- Updated dependencies [2ba5b76]
|
|
35
|
+
- Updated dependencies [c3a30de]
|
|
36
|
+
- Updated dependencies [cf3a184]
|
|
37
|
+
- Updated dependencies [d6bfd60]
|
|
38
|
+
- @mastra/core@0.10.15-alpha.1
|
|
39
|
+
|
|
3
40
|
## 0.11.3-alpha.0
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
|
@@ -17,6 +17,8 @@ import type { TiktokenBPE } from 'js-tiktoken/lite';
|
|
|
17
17
|
import type { UIMessage } from 'ai';
|
|
18
18
|
import type { WorkingMemoryTemplate } from '@mastra/core/memory';
|
|
19
19
|
|
|
20
|
+
export declare const __experimental_updateWorkingMemoryToolVNext: (config: MemoryConfig_2) => CoreTool;
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* Concrete implementation of MastraMemory that adds support for thread configuration
|
|
22
24
|
* and message injection.
|
|
@@ -47,7 +49,7 @@ export declare class Memory extends MastraMemory {
|
|
|
47
49
|
getThreadsByResourceId({ resourceId }: {
|
|
48
50
|
resourceId: string;
|
|
49
51
|
}): Promise<StorageThreadType[]>;
|
|
50
|
-
saveThread({ thread
|
|
52
|
+
saveThread({ thread }: {
|
|
51
53
|
thread: StorageThreadType;
|
|
52
54
|
memoryConfig?: MemoryConfig;
|
|
53
55
|
}): Promise<StorageThreadType>;
|
|
@@ -63,6 +65,20 @@ export declare class Memory extends MastraMemory {
|
|
|
63
65
|
workingMemory: string;
|
|
64
66
|
memoryConfig?: MemoryConfig;
|
|
65
67
|
}): Promise<void>;
|
|
68
|
+
private updateWorkingMemoryMutexes;
|
|
69
|
+
/**
|
|
70
|
+
* @warning experimental! can be removed or changed at any time
|
|
71
|
+
*/
|
|
72
|
+
__experimental_updateWorkingMemoryVNext({ threadId, resourceId, workingMemory, searchString, memoryConfig, }: {
|
|
73
|
+
threadId: string;
|
|
74
|
+
resourceId?: string;
|
|
75
|
+
workingMemory: string;
|
|
76
|
+
searchString?: string;
|
|
77
|
+
memoryConfig?: MemoryConfig;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
success: boolean;
|
|
80
|
+
reason: string;
|
|
81
|
+
}>;
|
|
66
82
|
protected chunkText(text: string, tokenSize?: number): string[];
|
|
67
83
|
private hasher;
|
|
68
84
|
private embeddingCache;
|
|
@@ -110,6 +126,11 @@ export declare class Memory extends MastraMemory {
|
|
|
110
126
|
template: WorkingMemoryTemplate;
|
|
111
127
|
data: string | null;
|
|
112
128
|
}): string;
|
|
129
|
+
protected __experimental_getWorkingMemoryToolInstructionVNext({ template, data, }: {
|
|
130
|
+
template: WorkingMemoryTemplate;
|
|
131
|
+
data: string | null;
|
|
132
|
+
}): string;
|
|
133
|
+
private isVNextWorkingMemoryConfig;
|
|
113
134
|
getTools(config?: MemoryConfig): Record<string, CoreTool>;
|
|
114
135
|
/**
|
|
115
136
|
* Updates the metadata of a list of messages
|
|
@@ -17,6 +17,8 @@ import type { TiktokenBPE } from 'js-tiktoken/lite';
|
|
|
17
17
|
import type { UIMessage } from 'ai';
|
|
18
18
|
import type { WorkingMemoryTemplate } from '@mastra/core/memory';
|
|
19
19
|
|
|
20
|
+
export declare const __experimental_updateWorkingMemoryToolVNext: (config: MemoryConfig_2) => CoreTool;
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* Concrete implementation of MastraMemory that adds support for thread configuration
|
|
22
24
|
* and message injection.
|
|
@@ -47,7 +49,7 @@ export declare class Memory extends MastraMemory {
|
|
|
47
49
|
getThreadsByResourceId({ resourceId }: {
|
|
48
50
|
resourceId: string;
|
|
49
51
|
}): Promise<StorageThreadType[]>;
|
|
50
|
-
saveThread({ thread
|
|
52
|
+
saveThread({ thread }: {
|
|
51
53
|
thread: StorageThreadType;
|
|
52
54
|
memoryConfig?: MemoryConfig;
|
|
53
55
|
}): Promise<StorageThreadType>;
|
|
@@ -63,6 +65,20 @@ export declare class Memory extends MastraMemory {
|
|
|
63
65
|
workingMemory: string;
|
|
64
66
|
memoryConfig?: MemoryConfig;
|
|
65
67
|
}): Promise<void>;
|
|
68
|
+
private updateWorkingMemoryMutexes;
|
|
69
|
+
/**
|
|
70
|
+
* @warning experimental! can be removed or changed at any time
|
|
71
|
+
*/
|
|
72
|
+
__experimental_updateWorkingMemoryVNext({ threadId, resourceId, workingMemory, searchString, memoryConfig, }: {
|
|
73
|
+
threadId: string;
|
|
74
|
+
resourceId?: string;
|
|
75
|
+
workingMemory: string;
|
|
76
|
+
searchString?: string;
|
|
77
|
+
memoryConfig?: MemoryConfig;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
success: boolean;
|
|
80
|
+
reason: string;
|
|
81
|
+
}>;
|
|
66
82
|
protected chunkText(text: string, tokenSize?: number): string[];
|
|
67
83
|
private hasher;
|
|
68
84
|
private embeddingCache;
|
|
@@ -110,6 +126,11 @@ export declare class Memory extends MastraMemory {
|
|
|
110
126
|
template: WorkingMemoryTemplate;
|
|
111
127
|
data: string | null;
|
|
112
128
|
}): string;
|
|
129
|
+
protected __experimental_getWorkingMemoryToolInstructionVNext({ template, data, }: {
|
|
130
|
+
template: WorkingMemoryTemplate;
|
|
131
|
+
data: string | null;
|
|
132
|
+
}): string;
|
|
133
|
+
private isVNextWorkingMemoryConfig;
|
|
113
134
|
getTools(config?: MemoryConfig): Record<string, CoreTool>;
|
|
114
135
|
/**
|
|
115
136
|
* Updates the metadata of a list of messages
|
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ var core = require('@mastra/core');
|
|
|
4
4
|
var agent = require('@mastra/core/agent');
|
|
5
5
|
var memory = require('@mastra/core/memory');
|
|
6
6
|
var ai = require('ai');
|
|
7
|
+
var asyncMutex = require('async-mutex');
|
|
7
8
|
var xxhash = require('xxhash-wasm');
|
|
8
9
|
var zod = require('zod');
|
|
9
10
|
var zodToJsonSchema = require('zod-to-json-schema');
|
|
@@ -43,6 +44,56 @@ var updateWorkingMemoryTool = (memoryConfig) => ({
|
|
|
43
44
|
return { success: true };
|
|
44
45
|
}
|
|
45
46
|
});
|
|
47
|
+
var __experimental_updateWorkingMemoryToolVNext = (config) => ({
|
|
48
|
+
description: "Update the working memory with new information.",
|
|
49
|
+
parameters: zod.z.object({
|
|
50
|
+
newMemory: zod.z.string().optional().nullable().describe(`The ${config.workingMemory?.schema ? "JSON" : "Markdown"} formatted working memory content to store`),
|
|
51
|
+
searchString: zod.z.string().optional().nullable().describe(
|
|
52
|
+
"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."
|
|
53
|
+
),
|
|
54
|
+
updateReason: zod.z.enum(["append-new-memory", "clarify-existing-memory", "replace-irrelevant-memory"]).optional().nullable().describe(
|
|
55
|
+
"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"
|
|
56
|
+
)
|
|
57
|
+
}),
|
|
58
|
+
execute: async (params) => {
|
|
59
|
+
const { context, threadId, memory, resourceId } = params;
|
|
60
|
+
if (!threadId || !memory) {
|
|
61
|
+
throw new Error("Thread ID and Memory instance are required for working memory updates");
|
|
62
|
+
}
|
|
63
|
+
const thread = await memory.getThreadById({ threadId });
|
|
64
|
+
if (!thread) {
|
|
65
|
+
throw new Error(`Thread ${threadId} not found`);
|
|
66
|
+
}
|
|
67
|
+
if (thread.resourceId && thread.resourceId !== resourceId) {
|
|
68
|
+
throw new Error(`Thread with id ${threadId} resourceId does not match the current resourceId ${resourceId}`);
|
|
69
|
+
}
|
|
70
|
+
const workingMemory = context.newMemory || "";
|
|
71
|
+
if (!context.updateReason) context.updateReason = `append-new-memory`;
|
|
72
|
+
if (context.searchString && config.workingMemory?.scope === `resource` && context.updateReason === `replace-irrelevant-memory`) {
|
|
73
|
+
context.searchString = void 0;
|
|
74
|
+
}
|
|
75
|
+
if (context.updateReason === `append-new-memory` && context.searchString) {
|
|
76
|
+
context.searchString = void 0;
|
|
77
|
+
}
|
|
78
|
+
if (context.updateReason !== `append-new-memory` && !context.searchString) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
reason: `updateReason was ${context.updateReason} but no searchString was provided. Unable to replace undefined with "${context.newMemory}"`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const result = await memory.__experimental_updateWorkingMemoryVNext({
|
|
85
|
+
threadId,
|
|
86
|
+
resourceId: resourceId || thread.resourceId,
|
|
87
|
+
workingMemory,
|
|
88
|
+
searchString: context.searchString,
|
|
89
|
+
memoryConfig: config
|
|
90
|
+
});
|
|
91
|
+
if (result) {
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
return { success: true };
|
|
95
|
+
}
|
|
96
|
+
});
|
|
46
97
|
|
|
47
98
|
// src/index.ts
|
|
48
99
|
var CHARS_PER_TOKEN = 4;
|
|
@@ -205,32 +256,7 @@ var Memory = class extends memory.MastraMemory {
|
|
|
205
256
|
async getThreadsByResourceId({ resourceId }) {
|
|
206
257
|
return this.storage.getThreadsByResourceId({ resourceId });
|
|
207
258
|
}
|
|
208
|
-
async saveThread({
|
|
209
|
-
thread,
|
|
210
|
-
memoryConfig
|
|
211
|
-
}) {
|
|
212
|
-
const config = this.getMergedThreadConfig(memoryConfig || {});
|
|
213
|
-
if (config.workingMemory?.enabled) {
|
|
214
|
-
const scope = config.workingMemory.scope || "thread";
|
|
215
|
-
const workingMemory = await this.getWorkingMemoryTemplate({ memoryConfig: config });
|
|
216
|
-
if (scope === "resource" && thread.resourceId) {
|
|
217
|
-
const existingResource = await this.storage.getResourceById({ resourceId: thread.resourceId });
|
|
218
|
-
if (!existingResource?.workingMemory) {
|
|
219
|
-
await this.storage.updateResource({
|
|
220
|
-
resourceId: thread.resourceId,
|
|
221
|
-
workingMemory: workingMemory?.content
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
} else if (scope === "thread" && !thread?.metadata?.workingMemory) {
|
|
225
|
-
return this.storage.saveThread({
|
|
226
|
-
thread: core.deepMerge(thread, {
|
|
227
|
-
metadata: {
|
|
228
|
-
workingMemory: workingMemory?.content
|
|
229
|
-
}
|
|
230
|
-
})
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
}
|
|
259
|
+
async saveThread({ thread }) {
|
|
234
260
|
return this.storage.saveThread({ thread });
|
|
235
261
|
}
|
|
236
262
|
async updateThread({
|
|
@@ -278,6 +304,87 @@ var Memory = class extends memory.MastraMemory {
|
|
|
278
304
|
});
|
|
279
305
|
}
|
|
280
306
|
}
|
|
307
|
+
updateWorkingMemoryMutexes = /* @__PURE__ */ new Map();
|
|
308
|
+
/**
|
|
309
|
+
* @warning experimental! can be removed or changed at any time
|
|
310
|
+
*/
|
|
311
|
+
async __experimental_updateWorkingMemoryVNext({
|
|
312
|
+
threadId,
|
|
313
|
+
resourceId,
|
|
314
|
+
workingMemory,
|
|
315
|
+
searchString,
|
|
316
|
+
memoryConfig
|
|
317
|
+
}) {
|
|
318
|
+
const config = this.getMergedThreadConfig(memoryConfig || {});
|
|
319
|
+
if (!config.workingMemory?.enabled) {
|
|
320
|
+
throw new Error("Working memory is not enabled for this memory instance");
|
|
321
|
+
}
|
|
322
|
+
const mutexKey = memoryConfig?.workingMemory?.scope === `resource` ? `resource-${resourceId}` : `thread-${threadId}`;
|
|
323
|
+
const mutex = this.updateWorkingMemoryMutexes.has(mutexKey) ? this.updateWorkingMemoryMutexes.get(mutexKey) : new asyncMutex.Mutex();
|
|
324
|
+
this.updateWorkingMemoryMutexes.set(mutexKey, mutex);
|
|
325
|
+
const release = await mutex.acquire();
|
|
326
|
+
try {
|
|
327
|
+
const existingWorkingMemory = await this.getWorkingMemory({ threadId, resourceId, memoryConfig }) || "";
|
|
328
|
+
const template = await this.getWorkingMemoryTemplate({ memoryConfig });
|
|
329
|
+
let reason = "";
|
|
330
|
+
if (existingWorkingMemory) {
|
|
331
|
+
if (searchString && existingWorkingMemory?.includes(searchString)) {
|
|
332
|
+
workingMemory = existingWorkingMemory.replace(searchString, workingMemory);
|
|
333
|
+
reason = `found and replaced searchString with newMemory`;
|
|
334
|
+
} else if (existingWorkingMemory.includes(workingMemory) || template?.content?.trim() === workingMemory.trim()) {
|
|
335
|
+
return {
|
|
336
|
+
success: false,
|
|
337
|
+
reason: `attempted to insert duplicate data into working memory. this entry was skipped`
|
|
338
|
+
};
|
|
339
|
+
} else {
|
|
340
|
+
if (searchString) {
|
|
341
|
+
reason = `attempted to replace working memory string that doesn't exist. Appending to working memory instead.`;
|
|
342
|
+
} else {
|
|
343
|
+
reason = `appended newMemory to end of working memory`;
|
|
344
|
+
}
|
|
345
|
+
workingMemory = existingWorkingMemory + `
|
|
346
|
+
${workingMemory}`;
|
|
347
|
+
}
|
|
348
|
+
} else if (workingMemory === template?.content) {
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
reason: `try again when you have data to add. newMemory was equal to the working memory template`
|
|
352
|
+
};
|
|
353
|
+
} else {
|
|
354
|
+
reason = `started new working memory`;
|
|
355
|
+
}
|
|
356
|
+
workingMemory = template?.content ? workingMemory.replaceAll(template?.content, "") : workingMemory;
|
|
357
|
+
const scope = config.workingMemory.scope || "thread";
|
|
358
|
+
if (scope === "resource" && resourceId) {
|
|
359
|
+
await this.storage.updateResource({
|
|
360
|
+
resourceId,
|
|
361
|
+
workingMemory
|
|
362
|
+
});
|
|
363
|
+
if (reason) {
|
|
364
|
+
return { success: true, reason };
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
const thread = await this.storage.getThreadById({ threadId });
|
|
368
|
+
if (!thread) {
|
|
369
|
+
throw new Error(`Thread ${threadId} not found`);
|
|
370
|
+
}
|
|
371
|
+
await this.storage.updateThread({
|
|
372
|
+
id: threadId,
|
|
373
|
+
title: thread.title || "Untitled Thread",
|
|
374
|
+
metadata: {
|
|
375
|
+
...thread.metadata,
|
|
376
|
+
workingMemory
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return { success: true, reason };
|
|
381
|
+
} catch (e) {
|
|
382
|
+
this.logger.error(e instanceof Error ? e.stack || e.message : JSON.stringify(e));
|
|
383
|
+
return { success: false, reason: "Tool error." };
|
|
384
|
+
} finally {
|
|
385
|
+
release();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
281
388
|
chunkText(text, tokenSize = 4096) {
|
|
282
389
|
const charSize = tokenSize * CHARS_PER_TOKEN;
|
|
283
390
|
const chunks = [];
|
|
@@ -524,7 +631,10 @@ var Memory = class extends memory.MastraMemory {
|
|
|
524
631
|
if (!workingMemoryTemplate) {
|
|
525
632
|
return null;
|
|
526
633
|
}
|
|
527
|
-
return this.
|
|
634
|
+
return this.isVNextWorkingMemoryConfig(memoryConfig) ? this.__experimental_getWorkingMemoryToolInstructionVNext({
|
|
635
|
+
template: workingMemoryTemplate,
|
|
636
|
+
data: workingMemoryData
|
|
637
|
+
}) : this.getWorkingMemoryToolInstruction({
|
|
528
638
|
template: workingMemoryTemplate,
|
|
529
639
|
data: workingMemoryData
|
|
530
640
|
});
|
|
@@ -559,14 +669,16 @@ Guidelines:
|
|
|
559
669
|
6. IMPORTANT: ALWAYS pass the data you want to store in the memory field as a string. DO NOT pass an object.
|
|
560
670
|
7. IMPORTANT: Data must only be sent as a string no matter which format is used.
|
|
561
671
|
|
|
562
|
-
|
|
672
|
+
<working_memory_template>
|
|
563
673
|
${template.content}
|
|
674
|
+
</working_memory_template>
|
|
564
675
|
|
|
565
676
|
${hasEmptyWorkingMemoryTemplateObject ? "When working with json data, the object format below represents the template:" : ""}
|
|
566
677
|
${hasEmptyWorkingMemoryTemplateObject ? JSON.stringify(emptyWorkingMemoryTemplateObject) : ""}
|
|
567
678
|
|
|
568
|
-
|
|
679
|
+
<working_memory_data>
|
|
569
680
|
${data}
|
|
681
|
+
</working_memory_data>
|
|
570
682
|
|
|
571
683
|
Notes:
|
|
572
684
|
- Update memory whenever referenced information changes
|
|
@@ -577,11 +689,52 @@ Notes:
|
|
|
577
689
|
- IMPORTANT: You MUST call updateWorkingMemory in every response to a prompt where you received relevant information.
|
|
578
690
|
- IMPORTANT: Preserve the ${template.format === "json" ? "JSON" : "Markdown"} formatting structure above while updating the content.`;
|
|
579
691
|
}
|
|
692
|
+
__experimental_getWorkingMemoryToolInstructionVNext({
|
|
693
|
+
template,
|
|
694
|
+
data
|
|
695
|
+
}) {
|
|
696
|
+
return `WORKING_MEMORY_SYSTEM_INSTRUCTION:
|
|
697
|
+
Store and update any conversation-relevant information by calling the updateWorkingMemory tool.
|
|
698
|
+
|
|
699
|
+
Guidelines:
|
|
700
|
+
1. Store anything that could be useful later in the conversation
|
|
701
|
+
2. Update proactively when information changes, no matter how small
|
|
702
|
+
3. Use ${template.format === "json" ? "JSON" : "Markdown"} format for all data
|
|
703
|
+
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"
|
|
704
|
+
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
|
|
705
|
+
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.
|
|
706
|
+
|
|
707
|
+
<working_memory_template>
|
|
708
|
+
${template.content}
|
|
709
|
+
</working_memory_template>
|
|
710
|
+
|
|
711
|
+
<working_memory_data>
|
|
712
|
+
${data}
|
|
713
|
+
</working_memory_data>
|
|
714
|
+
|
|
715
|
+
Notes:
|
|
716
|
+
- Update memory whenever referenced information changes
|
|
717
|
+
${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)
|
|
718
|
+
`}
|
|
719
|
+
- 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
|
|
720
|
+
- 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.
|
|
721
|
+
- IMPORTANT: You MUST call updateWorkingMemory in every response to a prompt where you received relevant information if that information is not already stored.
|
|
722
|
+
- IMPORTANT: Preserve the ${template.format === "json" ? "JSON" : "Markdown"} formatting structure above while updating the content.
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
isVNextWorkingMemoryConfig(config) {
|
|
726
|
+
if (!config?.workingMemory) return false;
|
|
727
|
+
const isMDWorkingMemory = !(`schema` in config.workingMemory) && (typeof config.workingMemory.template === `string` || config.workingMemory.template) && config.workingMemory;
|
|
728
|
+
return Boolean(isMDWorkingMemory && isMDWorkingMemory.version === `vnext`);
|
|
729
|
+
}
|
|
580
730
|
getTools(config) {
|
|
581
731
|
const mergedConfig = this.getMergedThreadConfig(config);
|
|
582
732
|
if (mergedConfig.workingMemory?.enabled) {
|
|
583
733
|
return {
|
|
584
|
-
updateWorkingMemory:
|
|
734
|
+
updateWorkingMemory: this.isVNextWorkingMemoryConfig(mergedConfig) ? (
|
|
735
|
+
// use the new experimental tool
|
|
736
|
+
__experimental_updateWorkingMemoryToolVNext(mergedConfig)
|
|
737
|
+
) : updateWorkingMemoryTool(mergedConfig)
|
|
585
738
|
};
|
|
586
739
|
}
|
|
587
740
|
return {};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
6
7
|
import { z, ZodObject } from 'zod';
|
|
7
8
|
import zodToJsonSchema from 'zod-to-json-schema';
|
|
@@ -36,6 +37,56 @@ var updateWorkingMemoryTool = (memoryConfig) => ({
|
|
|
36
37
|
return { success: true };
|
|
37
38
|
}
|
|
38
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
|
+
});
|
|
39
90
|
|
|
40
91
|
// src/index.ts
|
|
41
92
|
var CHARS_PER_TOKEN = 4;
|
|
@@ -198,32 +249,7 @@ var Memory = class extends MastraMemory {
|
|
|
198
249
|
async getThreadsByResourceId({ resourceId }) {
|
|
199
250
|
return this.storage.getThreadsByResourceId({ resourceId });
|
|
200
251
|
}
|
|
201
|
-
async saveThread({
|
|
202
|
-
thread,
|
|
203
|
-
memoryConfig
|
|
204
|
-
}) {
|
|
205
|
-
const config = this.getMergedThreadConfig(memoryConfig || {});
|
|
206
|
-
if (config.workingMemory?.enabled) {
|
|
207
|
-
const scope = config.workingMemory.scope || "thread";
|
|
208
|
-
const workingMemory = await this.getWorkingMemoryTemplate({ memoryConfig: config });
|
|
209
|
-
if (scope === "resource" && thread.resourceId) {
|
|
210
|
-
const existingResource = await this.storage.getResourceById({ resourceId: thread.resourceId });
|
|
211
|
-
if (!existingResource?.workingMemory) {
|
|
212
|
-
await this.storage.updateResource({
|
|
213
|
-
resourceId: thread.resourceId,
|
|
214
|
-
workingMemory: workingMemory?.content
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
} else if (scope === "thread" && !thread?.metadata?.workingMemory) {
|
|
218
|
-
return this.storage.saveThread({
|
|
219
|
-
thread: deepMerge(thread, {
|
|
220
|
-
metadata: {
|
|
221
|
-
workingMemory: workingMemory?.content
|
|
222
|
-
}
|
|
223
|
-
})
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
252
|
+
async saveThread({ thread }) {
|
|
227
253
|
return this.storage.saveThread({ thread });
|
|
228
254
|
}
|
|
229
255
|
async updateThread({
|
|
@@ -271,6 +297,87 @@ var Memory = class extends MastraMemory {
|
|
|
271
297
|
});
|
|
272
298
|
}
|
|
273
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
|
+
}
|
|
274
381
|
chunkText(text, tokenSize = 4096) {
|
|
275
382
|
const charSize = tokenSize * CHARS_PER_TOKEN;
|
|
276
383
|
const chunks = [];
|
|
@@ -517,7 +624,10 @@ var Memory = class extends MastraMemory {
|
|
|
517
624
|
if (!workingMemoryTemplate) {
|
|
518
625
|
return null;
|
|
519
626
|
}
|
|
520
|
-
return this.
|
|
627
|
+
return this.isVNextWorkingMemoryConfig(memoryConfig) ? this.__experimental_getWorkingMemoryToolInstructionVNext({
|
|
628
|
+
template: workingMemoryTemplate,
|
|
629
|
+
data: workingMemoryData
|
|
630
|
+
}) : this.getWorkingMemoryToolInstruction({
|
|
521
631
|
template: workingMemoryTemplate,
|
|
522
632
|
data: workingMemoryData
|
|
523
633
|
});
|
|
@@ -552,14 +662,16 @@ Guidelines:
|
|
|
552
662
|
6. IMPORTANT: ALWAYS pass the data you want to store in the memory field as a string. DO NOT pass an object.
|
|
553
663
|
7. IMPORTANT: Data must only be sent as a string no matter which format is used.
|
|
554
664
|
|
|
555
|
-
|
|
665
|
+
<working_memory_template>
|
|
556
666
|
${template.content}
|
|
667
|
+
</working_memory_template>
|
|
557
668
|
|
|
558
669
|
${hasEmptyWorkingMemoryTemplateObject ? "When working with json data, the object format below represents the template:" : ""}
|
|
559
670
|
${hasEmptyWorkingMemoryTemplateObject ? JSON.stringify(emptyWorkingMemoryTemplateObject) : ""}
|
|
560
671
|
|
|
561
|
-
|
|
672
|
+
<working_memory_data>
|
|
562
673
|
${data}
|
|
674
|
+
</working_memory_data>
|
|
563
675
|
|
|
564
676
|
Notes:
|
|
565
677
|
- Update memory whenever referenced information changes
|
|
@@ -570,11 +682,52 @@ Notes:
|
|
|
570
682
|
- IMPORTANT: You MUST call updateWorkingMemory in every response to a prompt where you received relevant information.
|
|
571
683
|
- IMPORTANT: Preserve the ${template.format === "json" ? "JSON" : "Markdown"} formatting structure above while updating the content.`;
|
|
572
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
|
+
}
|
|
573
723
|
getTools(config) {
|
|
574
724
|
const mergedConfig = this.getMergedThreadConfig(config);
|
|
575
725
|
if (mergedConfig.workingMemory?.enabled) {
|
|
576
726
|
return {
|
|
577
|
-
updateWorkingMemory:
|
|
727
|
+
updateWorkingMemory: this.isVNextWorkingMemoryConfig(mergedConfig) ? (
|
|
728
|
+
// use the new experimental tool
|
|
729
|
+
__experimental_updateWorkingMemoryToolVNext(mergedConfig)
|
|
730
|
+
) : updateWorkingMemoryTool(mergedConfig)
|
|
578
731
|
};
|
|
579
732
|
}
|
|
580
733
|
return {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/memory",
|
|
3
|
-
"version": "0.11.3
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"pg-pool": "^3.10.1",
|
|
41
41
|
"postgres": "^3.4.7",
|
|
42
42
|
"redis": "^4.7.1",
|
|
43
|
+
"async-mutex": "^0.5.0",
|
|
43
44
|
"xxhash-wasm": "^1.1.0",
|
|
44
45
|
"zod": "^3.25.67",
|
|
45
46
|
"zod-to-json-schema": "^3.24.5"
|
|
@@ -55,8 +56,8 @@
|
|
|
55
56
|
"typescript": "^5.8.3",
|
|
56
57
|
"typescript-eslint": "^8.34.0",
|
|
57
58
|
"vitest": "^3.2.4",
|
|
58
|
-
"@
|
|
59
|
-
"@
|
|
59
|
+
"@internal/lint": "0.0.20",
|
|
60
|
+
"@mastra/core": "0.10.15"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
62
63
|
"@mastra/core": ">=0.10.9-0 <0.11.0-0"
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateEmptyFromSchema } from '@mastra/core';
|
|
2
2
|
import type { CoreTool, MastraMessageV1 } from '@mastra/core';
|
|
3
3
|
import { MessageList } from '@mastra/core/agent';
|
|
4
4
|
import type { MastraMessageV2 } from '@mastra/core/agent';
|
|
@@ -7,13 +7,14 @@ import type { MemoryConfig, SharedMemoryConfig, StorageThreadType, WorkingMemory
|
|
|
7
7
|
import type { StorageGetMessagesArg } from '@mastra/core/storage';
|
|
8
8
|
import { embedMany } from 'ai';
|
|
9
9
|
import type { CoreMessage, TextPart, UIMessage } from 'ai';
|
|
10
|
+
import { Mutex } from 'async-mutex';
|
|
10
11
|
import type { JSONSchema7 } from 'json-schema';
|
|
11
12
|
|
|
12
13
|
import xxhash from 'xxhash-wasm';
|
|
13
14
|
import { ZodObject } from 'zod';
|
|
14
15
|
import type { ZodTypeAny } from 'zod';
|
|
15
16
|
import zodToJsonSchema from 'zod-to-json-schema';
|
|
16
|
-
import { updateWorkingMemoryTool } from './tools/working-memory';
|
|
17
|
+
import { updateWorkingMemoryTool, __experimental_updateWorkingMemoryToolVNext } from './tools/working-memory';
|
|
17
18
|
|
|
18
19
|
// Average characters per token based on OpenAI's tokenization
|
|
19
20
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -252,39 +253,7 @@ export class Memory extends MastraMemory {
|
|
|
252
253
|
return this.storage.getThreadsByResourceId({ resourceId });
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
async saveThread({
|
|
256
|
-
thread,
|
|
257
|
-
memoryConfig,
|
|
258
|
-
}: {
|
|
259
|
-
thread: StorageThreadType;
|
|
260
|
-
memoryConfig?: MemoryConfig;
|
|
261
|
-
}): Promise<StorageThreadType> {
|
|
262
|
-
const config = this.getMergedThreadConfig(memoryConfig || {});
|
|
263
|
-
|
|
264
|
-
if (config.workingMemory?.enabled) {
|
|
265
|
-
const scope = config.workingMemory.scope || 'thread';
|
|
266
|
-
const workingMemory = await this.getWorkingMemoryTemplate({ memoryConfig: config });
|
|
267
|
-
if (scope === 'resource' && thread.resourceId) {
|
|
268
|
-
// For resource scope, initialize working memory in resource table
|
|
269
|
-
const existingResource = await this.storage.getResourceById({ resourceId: thread.resourceId });
|
|
270
|
-
if (!existingResource?.workingMemory) {
|
|
271
|
-
await this.storage.updateResource({
|
|
272
|
-
resourceId: thread.resourceId,
|
|
273
|
-
workingMemory: workingMemory?.content,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
} else if (scope === 'thread' && !thread?.metadata?.workingMemory) {
|
|
277
|
-
// For thread scope, initialize working memory in thread metadata (existing behavior)
|
|
278
|
-
return this.storage.saveThread({
|
|
279
|
-
thread: deepMerge(thread, {
|
|
280
|
-
metadata: {
|
|
281
|
-
workingMemory: workingMemory?.content,
|
|
282
|
-
},
|
|
283
|
-
}),
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
256
|
+
async saveThread({ thread }: { thread: StorageThreadType; memoryConfig?: MemoryConfig }): Promise<StorageThreadType> {
|
|
288
257
|
return this.storage.saveThread({ thread });
|
|
289
258
|
}
|
|
290
259
|
|
|
@@ -351,6 +320,116 @@ export class Memory extends MastraMemory {
|
|
|
351
320
|
}
|
|
352
321
|
}
|
|
353
322
|
|
|
323
|
+
private updateWorkingMemoryMutexes = new Map<string, Mutex>();
|
|
324
|
+
/**
|
|
325
|
+
* @warning experimental! can be removed or changed at any time
|
|
326
|
+
*/
|
|
327
|
+
async __experimental_updateWorkingMemoryVNext({
|
|
328
|
+
threadId,
|
|
329
|
+
resourceId,
|
|
330
|
+
workingMemory,
|
|
331
|
+
searchString,
|
|
332
|
+
memoryConfig,
|
|
333
|
+
}: {
|
|
334
|
+
threadId: string;
|
|
335
|
+
resourceId?: string;
|
|
336
|
+
workingMemory: string;
|
|
337
|
+
searchString?: string;
|
|
338
|
+
memoryConfig?: MemoryConfig;
|
|
339
|
+
}): Promise<{ success: boolean; reason: string }> {
|
|
340
|
+
const config = this.getMergedThreadConfig(memoryConfig || {});
|
|
341
|
+
|
|
342
|
+
if (!config.workingMemory?.enabled) {
|
|
343
|
+
throw new Error('Working memory is not enabled for this memory instance');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// If the agent calls the update working memory tool multiple times simultaneously
|
|
347
|
+
// each call could overwrite the other call
|
|
348
|
+
// so get an in memory mutex to make sure this.getWorkingMemory() returns up to date data each time
|
|
349
|
+
const mutexKey =
|
|
350
|
+
memoryConfig?.workingMemory?.scope === `resource` ? `resource-${resourceId}` : `thread-${threadId}`;
|
|
351
|
+
const mutex = this.updateWorkingMemoryMutexes.has(mutexKey)
|
|
352
|
+
? this.updateWorkingMemoryMutexes.get(mutexKey)!
|
|
353
|
+
: new Mutex();
|
|
354
|
+
this.updateWorkingMemoryMutexes.set(mutexKey, mutex);
|
|
355
|
+
const release = await mutex.acquire();
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const existingWorkingMemory = (await this.getWorkingMemory({ threadId, resourceId, memoryConfig })) || '';
|
|
359
|
+
const template = await this.getWorkingMemoryTemplate({ memoryConfig });
|
|
360
|
+
|
|
361
|
+
let reason = '';
|
|
362
|
+
if (existingWorkingMemory) {
|
|
363
|
+
if (searchString && existingWorkingMemory?.includes(searchString)) {
|
|
364
|
+
workingMemory = existingWorkingMemory.replace(searchString, workingMemory);
|
|
365
|
+
reason = `found and replaced searchString with newMemory`;
|
|
366
|
+
} else if (
|
|
367
|
+
existingWorkingMemory.includes(workingMemory) ||
|
|
368
|
+
template?.content?.trim() === workingMemory.trim()
|
|
369
|
+
) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
reason: `attempted to insert duplicate data into working memory. this entry was skipped`,
|
|
373
|
+
};
|
|
374
|
+
} else {
|
|
375
|
+
if (searchString) {
|
|
376
|
+
reason = `attempted to replace working memory string that doesn't exist. Appending to working memory instead.`;
|
|
377
|
+
} else {
|
|
378
|
+
reason = `appended newMemory to end of working memory`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
workingMemory = existingWorkingMemory + `\n${workingMemory}`;
|
|
382
|
+
}
|
|
383
|
+
} else if (workingMemory === template?.content) {
|
|
384
|
+
return {
|
|
385
|
+
success: false,
|
|
386
|
+
reason: `try again when you have data to add. newMemory was equal to the working memory template`,
|
|
387
|
+
};
|
|
388
|
+
} else {
|
|
389
|
+
reason = `started new working memory`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// remove empty template insertions which models sometimes duplicate
|
|
393
|
+
workingMemory = template?.content ? workingMemory.replaceAll(template?.content, '') : workingMemory;
|
|
394
|
+
|
|
395
|
+
const scope = config.workingMemory.scope || 'thread';
|
|
396
|
+
|
|
397
|
+
if (scope === 'resource' && resourceId) {
|
|
398
|
+
// Update working memory in resource table
|
|
399
|
+
await this.storage.updateResource({
|
|
400
|
+
resourceId,
|
|
401
|
+
workingMemory,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (reason) {
|
|
405
|
+
return { success: true, reason };
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
// Update working memory in thread metadata (existing behavior)
|
|
409
|
+
const thread = await this.storage.getThreadById({ threadId });
|
|
410
|
+
if (!thread) {
|
|
411
|
+
throw new Error(`Thread ${threadId} not found`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await this.storage.updateThread({
|
|
415
|
+
id: threadId,
|
|
416
|
+
title: thread.title || 'Untitled Thread',
|
|
417
|
+
metadata: {
|
|
418
|
+
...thread.metadata,
|
|
419
|
+
workingMemory,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { success: true, reason };
|
|
425
|
+
} catch (e) {
|
|
426
|
+
this.logger.error(e instanceof Error ? e.stack || e.message : JSON.stringify(e));
|
|
427
|
+
return { success: false, reason: 'Tool error.' };
|
|
428
|
+
} finally {
|
|
429
|
+
release();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
354
433
|
protected chunkText(text: string, tokenSize = 4096) {
|
|
355
434
|
// Convert token size to character size with some buffer
|
|
356
435
|
const charSize = tokenSize * CHARS_PER_TOKEN;
|
|
@@ -677,7 +756,7 @@ export class Memory extends MastraMemory {
|
|
|
677
756
|
}) as JSONSchema7;
|
|
678
757
|
} else {
|
|
679
758
|
// Already a JSON Schema
|
|
680
|
-
convertedSchema = schema as JSONSchema7;
|
|
759
|
+
convertedSchema = schema as any as JSONSchema7;
|
|
681
760
|
}
|
|
682
761
|
|
|
683
762
|
return { format: 'json', content: JSON.stringify(convertedSchema) };
|
|
@@ -713,10 +792,15 @@ export class Memory extends MastraMemory {
|
|
|
713
792
|
return null;
|
|
714
793
|
}
|
|
715
794
|
|
|
716
|
-
return this.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
795
|
+
return this.isVNextWorkingMemoryConfig(memoryConfig)
|
|
796
|
+
? this.__experimental_getWorkingMemoryToolInstructionVNext({
|
|
797
|
+
template: workingMemoryTemplate,
|
|
798
|
+
data: workingMemoryData,
|
|
799
|
+
})
|
|
800
|
+
: this.getWorkingMemoryToolInstruction({
|
|
801
|
+
template: workingMemoryTemplate,
|
|
802
|
+
data: workingMemoryData,
|
|
803
|
+
});
|
|
720
804
|
}
|
|
721
805
|
|
|
722
806
|
public defaultWorkingMemoryTemplate = `
|
|
@@ -756,14 +840,16 @@ Guidelines:
|
|
|
756
840
|
6. IMPORTANT: ALWAYS pass the data you want to store in the memory field as a string. DO NOT pass an object.
|
|
757
841
|
7. IMPORTANT: Data must only be sent as a string no matter which format is used.
|
|
758
842
|
|
|
759
|
-
|
|
843
|
+
<working_memory_template>
|
|
760
844
|
${template.content}
|
|
845
|
+
</working_memory_template>
|
|
761
846
|
|
|
762
847
|
${hasEmptyWorkingMemoryTemplateObject ? 'When working with json data, the object format below represents the template:' : ''}
|
|
763
848
|
${hasEmptyWorkingMemoryTemplateObject ? JSON.stringify(emptyWorkingMemoryTemplateObject) : ''}
|
|
764
849
|
|
|
765
|
-
|
|
850
|
+
<working_memory_data>
|
|
766
851
|
${data}
|
|
852
|
+
</working_memory_data>
|
|
767
853
|
|
|
768
854
|
Notes:
|
|
769
855
|
- Update memory whenever referenced information changes
|
|
@@ -775,11 +861,66 @@ Notes:
|
|
|
775
861
|
- IMPORTANT: Preserve the ${template.format === 'json' ? 'JSON' : 'Markdown'} formatting structure above while updating the content.`;
|
|
776
862
|
}
|
|
777
863
|
|
|
864
|
+
protected __experimental_getWorkingMemoryToolInstructionVNext({
|
|
865
|
+
template,
|
|
866
|
+
data,
|
|
867
|
+
}: {
|
|
868
|
+
template: WorkingMemoryTemplate;
|
|
869
|
+
data: string | null;
|
|
870
|
+
}) {
|
|
871
|
+
return `WORKING_MEMORY_SYSTEM_INSTRUCTION:
|
|
872
|
+
Store and update any conversation-relevant information by calling the updateWorkingMemory tool.
|
|
873
|
+
|
|
874
|
+
Guidelines:
|
|
875
|
+
1. Store anything that could be useful later in the conversation
|
|
876
|
+
2. Update proactively when information changes, no matter how small
|
|
877
|
+
3. Use ${template.format === 'json' ? 'JSON' : 'Markdown'} format for all data
|
|
878
|
+
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"
|
|
879
|
+
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
|
|
880
|
+
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.
|
|
881
|
+
|
|
882
|
+
<working_memory_template>
|
|
883
|
+
${template.content}
|
|
884
|
+
</working_memory_template>
|
|
885
|
+
|
|
886
|
+
<working_memory_data>
|
|
887
|
+
${data}
|
|
888
|
+
</working_memory_data>
|
|
889
|
+
|
|
890
|
+
Notes:
|
|
891
|
+
- Update memory whenever referenced information changes
|
|
892
|
+
${
|
|
893
|
+
template.content !== this.defaultWorkingMemoryTemplate
|
|
894
|
+
? `- 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`
|
|
895
|
+
: `- 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)
|
|
896
|
+
`
|
|
897
|
+
}
|
|
898
|
+
- 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
|
|
899
|
+
- 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.
|
|
900
|
+
- IMPORTANT: You MUST call updateWorkingMemory in every response to a prompt where you received relevant information if that information is not already stored.
|
|
901
|
+
- IMPORTANT: Preserve the ${template.format === 'json' ? 'JSON' : 'Markdown'} formatting structure above while updating the content.
|
|
902
|
+
`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
private isVNextWorkingMemoryConfig(config?: MemoryConfig): boolean {
|
|
906
|
+
if (!config?.workingMemory) return false;
|
|
907
|
+
|
|
908
|
+
const isMDWorkingMemory =
|
|
909
|
+
!(`schema` in config.workingMemory) &&
|
|
910
|
+
(typeof config.workingMemory.template === `string` || config.workingMemory.template) &&
|
|
911
|
+
config.workingMemory;
|
|
912
|
+
|
|
913
|
+
return Boolean(isMDWorkingMemory && isMDWorkingMemory.version === `vnext`);
|
|
914
|
+
}
|
|
915
|
+
|
|
778
916
|
public getTools(config?: MemoryConfig): Record<string, CoreTool> {
|
|
779
917
|
const mergedConfig = this.getMergedThreadConfig(config);
|
|
780
918
|
if (mergedConfig.workingMemory?.enabled) {
|
|
781
919
|
return {
|
|
782
|
-
updateWorkingMemory:
|
|
920
|
+
updateWorkingMemory: this.isVNextWorkingMemoryConfig(mergedConfig)
|
|
921
|
+
? // use the new experimental tool
|
|
922
|
+
__experimental_updateWorkingMemoryToolVNext(mergedConfig)
|
|
923
|
+
: updateWorkingMemoryTool(mergedConfig),
|
|
783
924
|
};
|
|
784
925
|
}
|
|
785
926
|
return {};
|
|
@@ -40,3 +40,86 @@ export const updateWorkingMemoryTool = (memoryConfig?: MemoryConfig): CoreTool =
|
|
|
40
40
|
return { success: true };
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
export const __experimental_updateWorkingMemoryToolVNext = (config: MemoryConfig): CoreTool => ({
|
|
45
|
+
description: 'Update the working memory with new information.',
|
|
46
|
+
parameters: z.object({
|
|
47
|
+
newMemory: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.nullable()
|
|
51
|
+
.describe(`The ${config.workingMemory?.schema ? 'JSON' : 'Markdown'} formatted working memory content to store`),
|
|
52
|
+
searchString: z
|
|
53
|
+
.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.nullable()
|
|
56
|
+
.describe(
|
|
57
|
+
"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.",
|
|
58
|
+
),
|
|
59
|
+
updateReason: z
|
|
60
|
+
.enum(['append-new-memory', 'clarify-existing-memory', 'replace-irrelevant-memory'])
|
|
61
|
+
.optional()
|
|
62
|
+
.nullable()
|
|
63
|
+
.describe(
|
|
64
|
+
"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",
|
|
65
|
+
),
|
|
66
|
+
}),
|
|
67
|
+
execute: async (params: any) => {
|
|
68
|
+
const { context, threadId, memory, resourceId } = params;
|
|
69
|
+
if (!threadId || !memory) {
|
|
70
|
+
throw new Error('Thread ID and Memory instance are required for working memory updates');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const thread = await memory.getThreadById({ threadId });
|
|
74
|
+
|
|
75
|
+
if (!thread) {
|
|
76
|
+
throw new Error(`Thread ${threadId} not found`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (thread.resourceId && thread.resourceId !== resourceId) {
|
|
80
|
+
throw new Error(`Thread with id ${threadId} resourceId does not match the current resourceId ${resourceId}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const workingMemory = context.newMemory || '';
|
|
84
|
+
if (!context.updateReason) context.updateReason = `append-new-memory`;
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
context.searchString &&
|
|
88
|
+
config.workingMemory?.scope === `resource` &&
|
|
89
|
+
context.updateReason === `replace-irrelevant-memory`
|
|
90
|
+
) {
|
|
91
|
+
// don't allow replacements due to something not being relevant to the current conversation
|
|
92
|
+
// if there's no searchString, then we will append.
|
|
93
|
+
context.searchString = undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (context.updateReason === `append-new-memory` && context.searchString) {
|
|
97
|
+
// do not find/replace when append-new-memory is selected
|
|
98
|
+
// some models get confused and pass a search string even when they don't want to replace it.
|
|
99
|
+
// TODO: maybe they're trying to add new info after the search string?
|
|
100
|
+
context.searchString = undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (context.updateReason !== `append-new-memory` && !context.searchString) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
reason: `updateReason was ${context.updateReason} but no searchString was provided. Unable to replace undefined with "${context.newMemory}"`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Use the new updateWorkingMemory method which handles both thread and resource scope
|
|
111
|
+
const result = await memory.__experimental_updateWorkingMemoryVNext({
|
|
112
|
+
threadId,
|
|
113
|
+
resourceId: resourceId || thread.resourceId,
|
|
114
|
+
workingMemory: workingMemory,
|
|
115
|
+
searchString: context.searchString,
|
|
116
|
+
memoryConfig: config,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (result) {
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { success: true };
|
|
124
|
+
},
|
|
125
|
+
});
|