Add structured memory system for persistent agent context

Implements `StructuredMemoryManager` to track conversation summaries,
user preferences, and learned facts across sessions. Includes:
- Configurable storage limits with automatic enforcement
- Heuristic extraction of preferences and facts from messages
- Memory context injection into system prompts
- Full test coverage for all manager operations
This commit is contained in:
2026-05-20 22:18:33 +02:00
parent 9d4eb9a62a
commit fbb744ba6b
7 changed files with 1201 additions and 39 deletions
+35 -9
View File
@@ -59,16 +59,22 @@ const mockSettings: PluginSettings = {
minNoteLength: 50,
maxNoteLength: 8000,
tagPromptTemplate: 'Tags: {{content}}',
dryRun: false,
targetFolder: '',
normalizeTags: true,
dryRun: false,
targetFolder: '',
normalizeTags: true,
},
autoLinkConfig: {
enabled: false,
maxLinksPerNote: 3,
similarityThreshold: 0.6,
targetFolder: '',
dryRun: false,
targetFolder: '',
dryRun: false,
},
structuredMemoryConfig: {
enabled: true,
maxSummaries: 10,
maxPreferences: 20,
maxFacts: 50,
},
};
@@ -354,7 +360,17 @@ describe('ChatView', () => {
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'search query';
const searchSpy = jest.spyOn(view["noteContextBuilder"], "buildContext").mockResolvedValue({ explicitMentions: [], openNote: undefined, selectedText: undefined, backlinks: [], outlinks: [], relatedNotes: [], searchResults: [] });
const searchSpy = jest
.spyOn(view['noteContextBuilder'], 'buildContext')
.mockResolvedValue({
explicitMentions: [],
openNote: undefined,
selectedText: undefined,
backlinks: [],
outlinks: [],
relatedNotes: [],
searchResults: [],
});
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
@@ -363,7 +379,7 @@ describe('ChatView', () => {
await (view as any).handleUserInput('search query');
expect(searchSpy).toHaveBeenCalledWith("search query", 3, expect.any(Object)); // Should use DEFAULT_VAULT_SEARCH_LIMIT
expect(searchSpy).toHaveBeenCalledWith('search query', 3, expect.any(Object)); // Should use DEFAULT_VAULT_SEARCH_LIMIT
expect(chatSpy).toHaveBeenCalled();
});
@@ -799,7 +815,17 @@ describe('ChatView', () => {
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'search query';
const searchSpy = jest.spyOn(view["noteContextBuilder"], "buildContext").mockResolvedValue({ explicitMentions: [], openNote: undefined, selectedText: undefined, backlinks: [], outlinks: [], relatedNotes: [], searchResults: [] });
const searchSpy = jest
.spyOn(view['noteContextBuilder'], 'buildContext')
.mockResolvedValue({
explicitMentions: [],
openNote: undefined,
selectedText: undefined,
backlinks: [],
outlinks: [],
relatedNotes: [],
searchResults: [],
});
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
@@ -808,7 +834,7 @@ describe('ChatView', () => {
await (view as any).handleUserInput('search query');
expect(searchSpy).toHaveBeenCalledWith("search query", 3, expect.any(Object)); // Should use DEFAULT_VAULT_SEARCH_LIMIT
expect(searchSpy).toHaveBeenCalledWith('search query', 3, expect.any(Object)); // Should use DEFAULT_VAULT_SEARCH_LIMIT
expect(chatSpy).toHaveBeenCalled();
});