@cogmem/engram 0.0.0 → 0.2.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.
- package/README.md +1 -3
- package/package.json +1 -1
- package/src/config/defaults.ts +6 -0
- package/src/core/associations.ts +131 -10
- package/src/core/consolidation.ts +10 -3
- package/src/core/encoder.ts +4 -0
- package/src/core/memory.ts +2 -0
- package/src/core/recall.ts +39 -9
- package/src/core/working-memory.ts +7 -0
- package/src/index.ts +3 -0
- package/src/mcp/server.ts +36 -20
- package/src/mcp/tools.ts +149 -17
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# engram
|
|
2
2
|
|
|
3
|
-
**Human memory for artificial minds.**
|
|
4
|
-
|
|
5
3
|
> **Note:** This is an experiment in cognitive memory architecture. It's a research prototype, not production software.
|
|
6
4
|
|
|
7
5
|
Every AI agent today has amnesia. They process, respond, and forget. engram fixes this — not with a smarter key-value store, but with a cognitive memory system modeled on how the human brain actually forms, stores, recalls, and forgets information.
|
|
@@ -167,7 +165,7 @@ Add to your MCP client configuration (e.g., Claude Code `settings.json`):
|
|
|
167
165
|
"mcpServers": {
|
|
168
166
|
"engram": {
|
|
169
167
|
"command": "bunx",
|
|
170
|
-
"args": ["@cogmem/engram-mcp"]
|
|
168
|
+
"args": ["-p", "@cogmem/engram", "engram-mcp"]
|
|
171
169
|
}
|
|
172
170
|
}
|
|
173
171
|
}
|
package/package.json
CHANGED
package/src/config/defaults.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface CognitiveConfig {
|
|
|
14
14
|
reconsolidationBlendRate: number;
|
|
15
15
|
chunkingSimilarityThreshold: number;
|
|
16
16
|
semanticExtractionThreshold: number;
|
|
17
|
+
temporalContextWindow: number;
|
|
18
|
+
recallSpreadingDepth: number;
|
|
19
|
+
workingMemoryPrimingWeight: number;
|
|
17
20
|
dbPath: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -33,6 +36,9 @@ export const DEFAULT_CONFIG: CognitiveConfig = {
|
|
|
33
36
|
reconsolidationBlendRate: 0.1,
|
|
34
37
|
chunkingSimilarityThreshold: 0.6,
|
|
35
38
|
semanticExtractionThreshold: 3,
|
|
39
|
+
temporalContextWindow: 10,
|
|
40
|
+
recallSpreadingDepth: 3,
|
|
41
|
+
workingMemoryPrimingWeight: 0.5,
|
|
36
42
|
dbPath: "~/.engram/memory.db",
|
|
37
43
|
};
|
|
38
44
|
|
package/src/core/associations.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { CognitiveConfig } from "../config/defaults.ts";
|
|
2
2
|
import type { EngramStorage } from "../storage/sqlite.ts";
|
|
3
|
-
import type { Association, AssociationType, Memory } from "./memory.ts";
|
|
3
|
+
import type { Association, AssociationType, Emotion, Memory } from "./memory.ts";
|
|
4
4
|
import { generateId } from "./memory.ts";
|
|
5
5
|
import { extractKeywords } from "./search.ts";
|
|
6
6
|
|
|
7
|
+
type ArousalTier = "high" | "medium" | "low";
|
|
8
|
+
|
|
9
|
+
const AROUSAL_TIERS: Record<Emotion, ArousalTier> = {
|
|
10
|
+
anxiety: "high",
|
|
11
|
+
surprise: "high",
|
|
12
|
+
joy: "medium",
|
|
13
|
+
frustration: "medium",
|
|
14
|
+
satisfaction: "low",
|
|
15
|
+
curiosity: "low",
|
|
16
|
+
neutral: "low",
|
|
17
|
+
};
|
|
18
|
+
|
|
7
19
|
export function formAssociation(
|
|
8
20
|
storage: EngramStorage,
|
|
9
21
|
sourceId: string,
|
|
@@ -36,33 +48,62 @@ export function strengthenAssociation(
|
|
|
36
48
|
export function formTemporalAssociations(
|
|
37
49
|
storage: EngramStorage,
|
|
38
50
|
memory: Memory,
|
|
39
|
-
|
|
51
|
+
config: CognitiveConfig,
|
|
40
52
|
now?: number,
|
|
41
53
|
): Association[] {
|
|
42
54
|
const currentTime = now ?? Date.now();
|
|
43
|
-
const allMemories = storage.getAllMemories();
|
|
44
55
|
const formed: Association[] = [];
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
if (memory.context) {
|
|
58
|
+
const contextMemories = storage
|
|
59
|
+
.getMemoriesByContext(memory.context, undefined, config.temporalContextWindow)
|
|
60
|
+
.filter((m) => m.id !== memory.id)
|
|
61
|
+
.sort((a, b) => b.encodedAt - a.encodedAt);
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < contextMemories.length; i++) {
|
|
64
|
+
const other = contextMemories[i]!;
|
|
65
|
+
const positionGap = i + 1;
|
|
66
|
+
const strength = 1 / (1 + positionGap);
|
|
48
67
|
|
|
49
|
-
const timeDiff = Math.abs(memory.encodedAt - other.encodedAt);
|
|
50
|
-
if (timeDiff <= windowMs) {
|
|
51
68
|
const existing = storage.getAssociationsFrom(memory.id);
|
|
52
69
|
const alreadyLinked = existing.some((a) => a.targetId === other.id);
|
|
53
70
|
if (alreadyLinked) continue;
|
|
54
71
|
|
|
55
|
-
const strength = 1 - timeDiff / windowMs;
|
|
56
72
|
const assoc = formAssociation(
|
|
57
73
|
storage,
|
|
58
74
|
memory.id,
|
|
59
75
|
other.id,
|
|
60
76
|
"temporal",
|
|
61
|
-
Math.max(0.1, strength
|
|
77
|
+
Math.max(0.1, strength),
|
|
62
78
|
currentTime,
|
|
63
79
|
);
|
|
64
80
|
formed.push(assoc);
|
|
65
81
|
}
|
|
82
|
+
} else {
|
|
83
|
+
const windowMs = 300000;
|
|
84
|
+
const allMemories = storage.getAllMemories();
|
|
85
|
+
|
|
86
|
+
for (const other of allMemories) {
|
|
87
|
+
if (other.id === memory.id) continue;
|
|
88
|
+
|
|
89
|
+
const timeDiff = Math.abs(memory.encodedAt - other.encodedAt);
|
|
90
|
+
if (timeDiff <= windowMs) {
|
|
91
|
+
const existing = storage.getAssociationsFrom(memory.id);
|
|
92
|
+
const alreadyLinked = existing.some((a) => a.targetId === other.id);
|
|
93
|
+
if (alreadyLinked) continue;
|
|
94
|
+
|
|
95
|
+
const strength = 1 - timeDiff / windowMs;
|
|
96
|
+
const assoc = formAssociation(
|
|
97
|
+
storage,
|
|
98
|
+
memory.id,
|
|
99
|
+
other.id,
|
|
100
|
+
"temporal",
|
|
101
|
+
Math.max(0.1, strength * 0.8),
|
|
102
|
+
currentTime,
|
|
103
|
+
);
|
|
104
|
+
formed.push(assoc);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
66
107
|
}
|
|
67
108
|
|
|
68
109
|
return formed;
|
|
@@ -111,6 +152,86 @@ export function formSemanticAssociations(
|
|
|
111
152
|
return formed;
|
|
112
153
|
}
|
|
113
154
|
|
|
155
|
+
export function formEmotionalAssociations(
|
|
156
|
+
storage: EngramStorage,
|
|
157
|
+
memory: Memory,
|
|
158
|
+
now?: number,
|
|
159
|
+
): Association[] {
|
|
160
|
+
if (memory.emotion === "neutral" || memory.emotionWeight <= 0.3) return [];
|
|
161
|
+
|
|
162
|
+
const currentTime = now ?? Date.now();
|
|
163
|
+
const allMemories = storage.getAllMemories();
|
|
164
|
+
const formed: Association[] = [];
|
|
165
|
+
const memoryTier = AROUSAL_TIERS[memory.emotion];
|
|
166
|
+
|
|
167
|
+
for (const other of allMemories) {
|
|
168
|
+
if (other.id === memory.id) continue;
|
|
169
|
+
if (other.emotion === "neutral" || other.emotionWeight <= 0.3) continue;
|
|
170
|
+
|
|
171
|
+
const existing = storage.getAssociations(memory.id);
|
|
172
|
+
const alreadyLinked = existing.some(
|
|
173
|
+
(a) =>
|
|
174
|
+
a.type === "emotional" &&
|
|
175
|
+
((a.sourceId === memory.id && a.targetId === other.id) ||
|
|
176
|
+
(a.sourceId === other.id && a.targetId === memory.id)),
|
|
177
|
+
);
|
|
178
|
+
if (alreadyLinked) continue;
|
|
179
|
+
|
|
180
|
+
let strength: number;
|
|
181
|
+
if (memory.emotion === other.emotion) {
|
|
182
|
+
strength = 1 - Math.abs(memory.emotionWeight - other.emotionWeight);
|
|
183
|
+
} else if (AROUSAL_TIERS[other.emotion] === memoryTier) {
|
|
184
|
+
strength = 0.3 * (1 - Math.abs(memory.emotionWeight - other.emotionWeight));
|
|
185
|
+
} else {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (strength < 0.1) continue;
|
|
190
|
+
|
|
191
|
+
const assoc = formAssociation(storage, memory.id, other.id, "emotional", strength, currentTime);
|
|
192
|
+
formed.push(assoc);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return formed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function formCausalAssociations(
|
|
199
|
+
storage: EngramStorage,
|
|
200
|
+
memory: Memory,
|
|
201
|
+
config: CognitiveConfig,
|
|
202
|
+
now?: number,
|
|
203
|
+
): Association[] {
|
|
204
|
+
if (!memory.context) return [];
|
|
205
|
+
|
|
206
|
+
const currentTime = now ?? Date.now();
|
|
207
|
+
const contextMemories = storage
|
|
208
|
+
.getMemoriesByContext(memory.context, undefined, config.temporalContextWindow)
|
|
209
|
+
.filter((m) => m.id !== memory.id && m.encodedAt < memory.encodedAt)
|
|
210
|
+
.sort((a, b) => b.encodedAt - a.encodedAt);
|
|
211
|
+
|
|
212
|
+
const formed: Association[] = [];
|
|
213
|
+
|
|
214
|
+
for (let i = 0; i < contextMemories.length; i++) {
|
|
215
|
+
const source = contextMemories[i]!;
|
|
216
|
+
const sequenceGap = i + 1;
|
|
217
|
+
const strength = 1 / (1 + sequenceGap);
|
|
218
|
+
|
|
219
|
+
const existing = storage.getAssociations(memory.id);
|
|
220
|
+
const alreadyLinked = existing.some(
|
|
221
|
+
(a) =>
|
|
222
|
+
a.type === "causal" &&
|
|
223
|
+
((a.sourceId === source.id && a.targetId === memory.id) ||
|
|
224
|
+
(a.sourceId === memory.id && a.targetId === source.id)),
|
|
225
|
+
);
|
|
226
|
+
if (alreadyLinked) continue;
|
|
227
|
+
|
|
228
|
+
const assoc = formAssociation(storage, source.id, memory.id, "causal", strength, currentTime);
|
|
229
|
+
formed.push(assoc);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return formed;
|
|
233
|
+
}
|
|
234
|
+
|
|
114
235
|
export function recordCoRecall(
|
|
115
236
|
storage: EngramStorage,
|
|
116
237
|
memoryIds: string[],
|
|
@@ -147,7 +268,7 @@ export function getSpreadingActivationTargets(
|
|
|
147
268
|
storage: EngramStorage,
|
|
148
269
|
sourceId: string,
|
|
149
270
|
config: CognitiveConfig,
|
|
150
|
-
maxDepth: number =
|
|
271
|
+
maxDepth: number = config.recallSpreadingDepth,
|
|
151
272
|
): { memoryId: string; activationBoost: number; depth: number }[] {
|
|
152
273
|
const visited = new Set<string>([sourceId]);
|
|
153
274
|
const results: { memoryId: string; activationBoost: number; depth: number }[] = [];
|
|
@@ -3,7 +3,12 @@ import type { EngramStorage } from "../storage/sqlite.ts";
|
|
|
3
3
|
import type { Memory, ConsolidationLog } from "./memory.ts";
|
|
4
4
|
import { generateId } from "./memory.ts";
|
|
5
5
|
import { refreshActivations } from "./forgetting.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
formSemanticAssociations,
|
|
8
|
+
formTemporalAssociations,
|
|
9
|
+
formEmotionalAssociations,
|
|
10
|
+
formCausalAssociations,
|
|
11
|
+
} from "./associations.ts";
|
|
7
12
|
import { encode } from "./encoder.ts";
|
|
8
13
|
import { extractKeywords, tokenize } from "./search.ts";
|
|
9
14
|
|
|
@@ -63,10 +68,12 @@ export function consolidate(
|
|
|
63
68
|
|
|
64
69
|
const remainingMemories = storage.getAllMemories();
|
|
65
70
|
for (const memory of remainingMemories) {
|
|
66
|
-
const temporalAssocs = formTemporalAssociations(storage, memory,
|
|
71
|
+
const temporalAssocs = formTemporalAssociations(storage, memory, config, currentTime);
|
|
67
72
|
const semanticAssocs = formSemanticAssociations(storage, memory, currentTime);
|
|
73
|
+
const emotionalAssocs = formEmotionalAssociations(storage, memory, currentTime);
|
|
74
|
+
const causalAssocs = formCausalAssociations(storage, memory, config, currentTime);
|
|
68
75
|
|
|
69
|
-
for (const assoc of [...temporalAssocs, ...semanticAssocs]) {
|
|
76
|
+
for (const assoc of [...temporalAssocs, ...semanticAssocs, ...emotionalAssocs, ...causalAssocs]) {
|
|
70
77
|
result.associationsDiscovered++;
|
|
71
78
|
result.discoveredAssociationPairs.push([assoc.sourceId, assoc.targetId]);
|
|
72
79
|
}
|
package/src/core/encoder.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Memory, EncodeInput } from "./memory.ts";
|
|
|
4
4
|
import { generateMemoryId } from "./memory.ts";
|
|
5
5
|
import { defaultEmotionWeight } from "./emotional-tag.ts";
|
|
6
6
|
import { baseLevelActivation } from "./activation.ts";
|
|
7
|
+
import { formEmotionalAssociations, formCausalAssociations } from "./associations.ts";
|
|
7
8
|
|
|
8
9
|
export function encode(
|
|
9
10
|
storage: EngramStorage,
|
|
@@ -43,5 +44,8 @@ export function encode(
|
|
|
43
44
|
storage.logAccess(id, "encode", currentTime);
|
|
44
45
|
});
|
|
45
46
|
|
|
47
|
+
formEmotionalAssociations(storage, memory, currentTime);
|
|
48
|
+
formCausalAssociations(storage, memory, config, currentTime);
|
|
49
|
+
|
|
46
50
|
return memory;
|
|
47
51
|
}
|
package/src/core/memory.ts
CHANGED
|
@@ -29,6 +29,8 @@ export const AssociationType = {
|
|
|
29
29
|
Temporal: "temporal",
|
|
30
30
|
Semantic: "semantic",
|
|
31
31
|
CoRecall: "co-recall",
|
|
32
|
+
Emotional: "emotional",
|
|
33
|
+
Causal: "causal",
|
|
32
34
|
} as const;
|
|
33
35
|
export type AssociationType = (typeof AssociationType)[keyof typeof AssociationType];
|
|
34
36
|
|
package/src/core/recall.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { CognitiveConfig } from "../config/defaults.ts";
|
|
|
2
2
|
import type { EngramStorage } from "../storage/sqlite.ts";
|
|
3
3
|
import type { Memory, RecallResult } from "./memory.ts";
|
|
4
4
|
import { computeActivation, spreadingActivationStrength } from "./activation.ts";
|
|
5
|
+
import { getSpreadingActivationTargets } from "./associations.ts";
|
|
6
|
+
import { getWorkingMemoryIds } from "./working-memory.ts";
|
|
5
7
|
|
|
6
8
|
export function recall(
|
|
7
9
|
storage: EngramStorage,
|
|
@@ -20,15 +22,17 @@ export function recall(
|
|
|
20
22
|
const limit = options?.limit ?? 10;
|
|
21
23
|
const associative = options?.associative ?? true;
|
|
22
24
|
|
|
23
|
-
const
|
|
25
|
+
const seedIds = new Set<string>();
|
|
26
|
+
|
|
27
|
+
const wmIds = getWorkingMemoryIds(storage);
|
|
28
|
+
for (const id of wmIds) seedIds.add(id);
|
|
24
29
|
|
|
25
30
|
const ftsIds = storage.searchFTS(cue, limit * 2);
|
|
26
|
-
for (const id of ftsIds)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
candidateMap.set(m.id, m);
|
|
31
|
+
for (const id of ftsIds) seedIds.add(id);
|
|
32
|
+
|
|
33
|
+
if (options?.context) {
|
|
34
|
+
const contextMatches = storage.getMemoriesByContext(options.context, options?.type, limit * 2);
|
|
35
|
+
for (const m of contextMatches) seedIds.add(m.id);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const allCandidates = options?.type
|
|
@@ -42,6 +46,32 @@ export function recall(
|
|
|
42
46
|
|
|
43
47
|
const sorted = filtered.sort((a, b) => b.activation - a.activation);
|
|
44
48
|
for (const m of sorted.slice(0, limit)) {
|
|
49
|
+
seedIds.add(m.id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const candidateIds = new Set<string>(seedIds);
|
|
53
|
+
const graphBoosts = new Map<string, number>();
|
|
54
|
+
|
|
55
|
+
if (associative) {
|
|
56
|
+
for (const seedId of seedIds) {
|
|
57
|
+
const targets = getSpreadingActivationTargets(storage, seedId, config);
|
|
58
|
+
const isWmSeed = wmIds.includes(seedId);
|
|
59
|
+
const primingWeight = isWmSeed ? config.workingMemoryPrimingWeight : 1.0;
|
|
60
|
+
|
|
61
|
+
for (const t of targets) {
|
|
62
|
+
candidateIds.add(t.memoryId);
|
|
63
|
+
const existing = graphBoosts.get(t.memoryId) ?? 0;
|
|
64
|
+
graphBoosts.set(t.memoryId, existing + t.activationBoost * primingWeight);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const candidateMap = new Map<string, Memory>();
|
|
70
|
+
for (const id of candidateIds) {
|
|
71
|
+
const m = storage.getMemory(id);
|
|
72
|
+
if (!m) continue;
|
|
73
|
+
if (options?.type && m.type !== options.type) continue;
|
|
74
|
+
if (options?.context && (!m.context || !m.context.startsWith(options.context))) continue;
|
|
45
75
|
candidateMap.set(m.id, m);
|
|
46
76
|
}
|
|
47
77
|
|
|
@@ -52,8 +82,8 @@ export function recall(
|
|
|
52
82
|
for (const memory of candidateMap.values()) {
|
|
53
83
|
const timestamps = storage.getAccessTimestamps(memory.id);
|
|
54
84
|
|
|
55
|
-
let spreadingSum = 0;
|
|
56
|
-
if (associative) {
|
|
85
|
+
let spreadingSum = graphBoosts.get(memory.id) ?? 0;
|
|
86
|
+
if (associative && spreadingSum === 0) {
|
|
57
87
|
const assocFrom = storage.getAssociationsFrom(memory.id);
|
|
58
88
|
const assocTo = storage.getAssociationsTo(memory.id);
|
|
59
89
|
const allAssocs = [...assocFrom, ...assocTo];
|
|
@@ -54,6 +54,13 @@ export function clearFocus(storage: EngramStorage): number {
|
|
|
54
54
|
return count;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export function getWorkingMemoryIds(storage: EngramStorage): string[] {
|
|
58
|
+
return storage
|
|
59
|
+
.getWorkingMemory()
|
|
60
|
+
.filter((s) => s.memoryRef !== null)
|
|
61
|
+
.map((s) => s.memoryRef!);
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
export function focusUtilization(
|
|
58
65
|
storage: EngramStorage,
|
|
59
66
|
config: CognitiveConfig,
|
package/src/index.ts
CHANGED
|
@@ -10,11 +10,14 @@ export {
|
|
|
10
10
|
getFocus,
|
|
11
11
|
clearFocus,
|
|
12
12
|
focusUtilization,
|
|
13
|
+
getWorkingMemoryIds,
|
|
13
14
|
} from "./core/working-memory.ts";
|
|
14
15
|
export {
|
|
15
16
|
formAssociation,
|
|
16
17
|
formTemporalAssociations,
|
|
17
18
|
formSemanticAssociations,
|
|
19
|
+
formEmotionalAssociations,
|
|
20
|
+
formCausalAssociations,
|
|
18
21
|
recordCoRecall,
|
|
19
22
|
getSpreadingActivationTargets,
|
|
20
23
|
} from "./core/associations.ts";
|
package/src/mcp/server.ts
CHANGED
|
@@ -17,11 +17,7 @@ server.registerTool(
|
|
|
17
17
|
"memory_store",
|
|
18
18
|
{
|
|
19
19
|
title: "Store Memory",
|
|
20
|
-
description: `
|
|
21
|
-
|
|
22
|
-
Actions:
|
|
23
|
-
- action:"encode" — Create a new memory. Params: content (required), type? (semantic|episodic|procedural, default semantic), emotion? (neutral|curiosity|surprise|anxiety|satisfaction|frustration|confusion|excitement|calm|urgency), emotionWeight? (0-1), context? (e.g. "project:acme")
|
|
24
|
-
- action:"reconsolidate" — Update an existing memory during recall. Params: id (required), newContext?, currentEmotion?, currentEmotionWeight?`,
|
|
20
|
+
description: `Actions: encode(content) — store memory | encode_batch(memories[]) — store multiple | reconsolidate(id) — update during recall. Optional: type, emotion, emotionWeight, context.`,
|
|
25
21
|
inputSchema: z.discriminatedUnion("action", [
|
|
26
22
|
z.object({
|
|
27
23
|
action: z.literal("encode"),
|
|
@@ -31,6 +27,22 @@ Actions:
|
|
|
31
27
|
emotionWeight: z.number().min(0).max(1).optional().describe("Emotion intensity 0-1"),
|
|
32
28
|
context: z.string().optional().describe("Context tag (e.g. project:acme)"),
|
|
33
29
|
}),
|
|
30
|
+
z.object({
|
|
31
|
+
action: z.literal("encode_batch"),
|
|
32
|
+
memories: z
|
|
33
|
+
.array(
|
|
34
|
+
z.object({
|
|
35
|
+
content: z.string(),
|
|
36
|
+
type: z.nativeEnum(MemoryType).optional(),
|
|
37
|
+
emotion: z.nativeEnum(Emotion).optional(),
|
|
38
|
+
emotionWeight: z.number().min(0).max(1).optional(),
|
|
39
|
+
context: z.string().optional(),
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
.min(1)
|
|
43
|
+
.max(50)
|
|
44
|
+
.describe("Array of memories to encode"),
|
|
45
|
+
}),
|
|
34
46
|
z.object({
|
|
35
47
|
action: z.literal("reconsolidate"),
|
|
36
48
|
id: z.string().describe("Memory ID to update"),
|
|
@@ -47,26 +59,30 @@ server.registerTool(
|
|
|
47
59
|
"memory_recall",
|
|
48
60
|
{
|
|
49
61
|
title: "Recall Memories",
|
|
50
|
-
description: `
|
|
51
|
-
|
|
52
|
-
Actions:
|
|
53
|
-
- action:"recall" — Cue-based retrieval with spreading activation. Params: cue (required), limit? (default 5), type? (semantic|episodic|procedural), context?, associative? (default true), verbose?
|
|
54
|
-
- action:"inspect" — Examine a specific memory's full lifecycle. Params: id (required, full ID or prefix)
|
|
55
|
-
- action:"stats" — Memory system overview (counts, working memory usage, last consolidation). No extra params.`,
|
|
62
|
+
description: `Actions: recall(cue) — cue-based retrieval | list — browse without activation effects | inspect(id) — full lifecycle | stats — system overview. Optional: limit, type, context, format, verbose.`,
|
|
56
63
|
inputSchema: z.discriminatedUnion("action", [
|
|
57
64
|
z.object({
|
|
58
|
-
action: z.literal("recall")
|
|
65
|
+
action: z.literal("recall"),
|
|
59
66
|
cue: z.string().describe("Recall cue"),
|
|
60
67
|
limit: z.number().optional().describe("Max results (default: 5)"),
|
|
61
68
|
type: z.nativeEnum(MemoryType).optional().describe("Filter by type"),
|
|
62
69
|
context: z.string().optional().describe("Filter by context"),
|
|
63
70
|
associative: z.boolean().optional().describe("Spreading activation (default: true)"),
|
|
64
71
|
verbose: z.boolean().optional().describe("Full fields"),
|
|
72
|
+
format: z.enum(["full", "content", "ids"]).optional().describe("Response format (default: full)"),
|
|
65
73
|
}),
|
|
66
74
|
z.object({
|
|
67
75
|
action: z.literal("inspect"),
|
|
68
76
|
id: z.string().describe("Memory ID or prefix"),
|
|
69
77
|
}),
|
|
78
|
+
z.object({
|
|
79
|
+
action: z.literal("list"),
|
|
80
|
+
type: z.nativeEnum(MemoryType).optional().describe("Filter by type"),
|
|
81
|
+
context: z.string().optional().describe("Filter by context prefix"),
|
|
82
|
+
limit: z.number().optional().describe("Max results (default: 20)"),
|
|
83
|
+
offset: z.number().optional().describe("Skip first N results (default: 0)"),
|
|
84
|
+
format: z.enum(["full", "content", "ids"]).optional().describe("Response format (default: full)"),
|
|
85
|
+
}),
|
|
70
86
|
z.object({
|
|
71
87
|
action: z.literal("stats"),
|
|
72
88
|
}),
|
|
@@ -79,14 +95,7 @@ server.registerTool(
|
|
|
79
95
|
"memory_manage",
|
|
80
96
|
{
|
|
81
97
|
title: "Manage Memory",
|
|
82
|
-
description: `
|
|
83
|
-
|
|
84
|
-
Actions:
|
|
85
|
-
- action:"consolidate" — Run sleep cycle (strengthen, prune, extract patterns, discover links). No extra params.
|
|
86
|
-
- action:"focus_push" — Push to working memory buffer. Params: content (required), memoryRef?
|
|
87
|
-
- action:"focus_pop" — Remove most recent item from working memory. No extra params.
|
|
88
|
-
- action:"focus_get" — View current working memory contents. No extra params.
|
|
89
|
-
- action:"focus_clear" — Clear all working memory. No extra params.`,
|
|
98
|
+
description: `Actions: consolidate — run sleep cycle | recall_to_focus(cue) — recall and load to working memory | focus_push(content) — push to buffer | focus_pop — pop newest | focus_get — view buffer | focus_clear — empty buffer.`,
|
|
90
99
|
inputSchema: z.discriminatedUnion("action", [
|
|
91
100
|
z.object({
|
|
92
101
|
action: z.literal("consolidate"),
|
|
@@ -105,6 +114,13 @@ Actions:
|
|
|
105
114
|
z.object({
|
|
106
115
|
action: z.literal("focus_clear"),
|
|
107
116
|
}),
|
|
117
|
+
z.object({
|
|
118
|
+
action: z.literal("recall_to_focus"),
|
|
119
|
+
cue: z.string().describe("Recall cue"),
|
|
120
|
+
limit: z.number().optional().describe("Max memories to load (default: 3)"),
|
|
121
|
+
type: z.nativeEnum(MemoryType).optional().describe("Filter by type"),
|
|
122
|
+
context: z.string().optional().describe("Filter by context"),
|
|
123
|
+
}),
|
|
108
124
|
]),
|
|
109
125
|
},
|
|
110
126
|
async (args) => handleManage(engine, args),
|
package/src/mcp/tools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CognitiveConfig } from "../config/defaults.ts";
|
|
2
2
|
import type { EngramStorage } from "../storage/sqlite.ts";
|
|
3
3
|
import type { EngramEngine } from "../core/engine.ts";
|
|
4
|
-
import { isValidMemoryType } from "../core/memory.ts";
|
|
4
|
+
import { isValidMemoryType, MemoryType, Emotion } from "../core/memory.ts";
|
|
5
5
|
import { encode } from "../core/encoder.ts";
|
|
6
6
|
import { recall } from "../core/recall.ts";
|
|
7
7
|
import {
|
|
@@ -38,6 +38,8 @@ export function handleStore(
|
|
|
38
38
|
switch (args.action) {
|
|
39
39
|
case "encode":
|
|
40
40
|
return handleEncode(engine.storage, engine.config, args as any, engine.projectContext);
|
|
41
|
+
case "encode_batch":
|
|
42
|
+
return handleEncodeBatch(engine.storage, engine.config, args as any, engine.projectContext);
|
|
41
43
|
case "reconsolidate":
|
|
42
44
|
return handleReconsolidate(engine.storage, engine.config, args as any);
|
|
43
45
|
default:
|
|
@@ -55,6 +57,8 @@ export function handleRecall(
|
|
|
55
57
|
return handleRecallQuery(engine.storage, engine.config, args as any, engine.projectContext);
|
|
56
58
|
case "inspect":
|
|
57
59
|
return handleInspect(engine.storage, args as any);
|
|
60
|
+
case "list":
|
|
61
|
+
return handleList(engine.storage, args as any, engine.projectContext);
|
|
58
62
|
case "stats":
|
|
59
63
|
return handleStats(engine.storage, engine.config);
|
|
60
64
|
default:
|
|
@@ -77,6 +81,8 @@ export function handleManage(
|
|
|
77
81
|
return handleFocusGet(engine.storage, engine.config);
|
|
78
82
|
case "focus_clear":
|
|
79
83
|
return handleFocusClear(engine.storage);
|
|
84
|
+
case "recall_to_focus":
|
|
85
|
+
return handleRecallToFocus(engine.storage, engine.config, args as any, engine.projectContext);
|
|
80
86
|
default:
|
|
81
87
|
return errorResult(`Unknown manage action: ${args.action}`);
|
|
82
88
|
}
|
|
@@ -96,12 +102,12 @@ function handleEncode(
|
|
|
96
102
|
): ToolResult {
|
|
97
103
|
const typeStr = args.type ?? "semantic";
|
|
98
104
|
if (!isValidMemoryType(typeStr)) {
|
|
99
|
-
return errorResult(`Invalid type
|
|
105
|
+
return errorResult(`Invalid type '${typeStr}'. Valid: ${Object.values(MemoryType).join(", ")}`);
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
const emotionStr = args.emotion ?? "neutral";
|
|
103
109
|
if (!isValidEmotion(emotionStr)) {
|
|
104
|
-
return errorResult(`Invalid emotion
|
|
110
|
+
return errorResult(`Invalid emotion '${emotionStr}'. Valid: ${Object.values(Emotion).join(", ")}`);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
const memory = encode(
|
|
@@ -116,17 +122,58 @@ function handleEncode(
|
|
|
116
122
|
config,
|
|
117
123
|
);
|
|
118
124
|
|
|
119
|
-
return textResult(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
return textResult(JSON.stringify({ id: memory.id }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handleEncodeBatch(
|
|
129
|
+
storage: EngramStorage,
|
|
130
|
+
config: CognitiveConfig,
|
|
131
|
+
args: {
|
|
132
|
+
memories: Array<{
|
|
133
|
+
content: string;
|
|
134
|
+
type?: string;
|
|
135
|
+
emotion?: string;
|
|
136
|
+
emotionWeight?: number;
|
|
137
|
+
context?: string;
|
|
138
|
+
}>;
|
|
139
|
+
},
|
|
140
|
+
defaultContext?: string | null,
|
|
141
|
+
): ToolResult {
|
|
142
|
+
const ids: string[] = [];
|
|
143
|
+
const errors: string[] = [];
|
|
144
|
+
|
|
145
|
+
storage.transaction(() => {
|
|
146
|
+
for (let i = 0; i < args.memories.length; i++) {
|
|
147
|
+
const m = args.memories[i]!;
|
|
148
|
+
const typeStr = m.type ?? "semantic";
|
|
149
|
+
if (!isValidMemoryType(typeStr)) {
|
|
150
|
+
errors.push(`[${i}] Invalid type '${typeStr}'`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const emotionStr = m.emotion ?? "neutral";
|
|
154
|
+
if (!isValidEmotion(emotionStr)) {
|
|
155
|
+
errors.push(`[${i}] Invalid emotion '${emotionStr}'`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const memory = encode(
|
|
159
|
+
storage,
|
|
160
|
+
{
|
|
161
|
+
content: m.content,
|
|
162
|
+
type: typeStr,
|
|
163
|
+
emotion: emotionStr,
|
|
164
|
+
emotionWeight: m.emotionWeight,
|
|
165
|
+
context: m.context ?? defaultContext ?? undefined,
|
|
166
|
+
},
|
|
167
|
+
config,
|
|
168
|
+
);
|
|
169
|
+
ids.push(memory.id);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
return textResult(JSON.stringify({ stored: ids, errors }));
|
|
175
|
+
}
|
|
176
|
+
return textResult(JSON.stringify({ stored: ids }));
|
|
130
177
|
}
|
|
131
178
|
|
|
132
179
|
function handleRecallQuery(
|
|
@@ -139,6 +186,7 @@ function handleRecallQuery(
|
|
|
139
186
|
context?: string;
|
|
140
187
|
associative?: boolean;
|
|
141
188
|
verbose?: boolean;
|
|
189
|
+
format?: "full" | "content" | "ids";
|
|
142
190
|
},
|
|
143
191
|
defaultContext?: string | null,
|
|
144
192
|
): ToolResult {
|
|
@@ -155,6 +203,15 @@ function handleRecallQuery(
|
|
|
155
203
|
return textResult("No memories found above retrieval threshold.");
|
|
156
204
|
}
|
|
157
205
|
|
|
206
|
+
const format = args.format ?? "full";
|
|
207
|
+
|
|
208
|
+
if (format === "ids") {
|
|
209
|
+
return textResult(JSON.stringify(results.map((r) => r.memory.id)));
|
|
210
|
+
}
|
|
211
|
+
if (format === "content") {
|
|
212
|
+
return textResult(JSON.stringify(results.map((r) => r.memory.content)));
|
|
213
|
+
}
|
|
214
|
+
|
|
158
215
|
const formatted = args.verbose
|
|
159
216
|
? results.map((r) => ({
|
|
160
217
|
id: r.memory.id,
|
|
@@ -178,6 +235,56 @@ function handleRecallQuery(
|
|
|
178
235
|
return textResult(JSON.stringify(formatted));
|
|
179
236
|
}
|
|
180
237
|
|
|
238
|
+
function handleList(
|
|
239
|
+
storage: EngramStorage,
|
|
240
|
+
args: {
|
|
241
|
+
type?: string;
|
|
242
|
+
context?: string;
|
|
243
|
+
limit?: number;
|
|
244
|
+
offset?: number;
|
|
245
|
+
format?: "full" | "content" | "ids";
|
|
246
|
+
},
|
|
247
|
+
defaultContext?: string | null,
|
|
248
|
+
): ToolResult {
|
|
249
|
+
const limit = args.limit ?? 20;
|
|
250
|
+
const offset = args.offset ?? 0;
|
|
251
|
+
const typeFilter = args.type && isValidMemoryType(args.type) ? args.type : undefined;
|
|
252
|
+
const context = args.context ?? defaultContext ?? undefined;
|
|
253
|
+
|
|
254
|
+
let results;
|
|
255
|
+
if (context) {
|
|
256
|
+
results = storage.getMemoriesByContext(context, typeFilter, limit + offset);
|
|
257
|
+
} else {
|
|
258
|
+
results = storage.getAllMemories(typeFilter).slice(0, limit + offset);
|
|
259
|
+
}
|
|
260
|
+
results = results.slice(offset, offset + limit);
|
|
261
|
+
|
|
262
|
+
if (results.length === 0) {
|
|
263
|
+
return textResult("No memories found.");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const format = args.format ?? "full";
|
|
267
|
+
|
|
268
|
+
if (format === "ids") {
|
|
269
|
+
return textResult(JSON.stringify(results.map((m) => m.id)));
|
|
270
|
+
}
|
|
271
|
+
if (format === "content") {
|
|
272
|
+
return textResult(JSON.stringify(results.map((m) => m.content)));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return textResult(
|
|
276
|
+
JSON.stringify(
|
|
277
|
+
results.map((m) => ({
|
|
278
|
+
id: m.id,
|
|
279
|
+
content: m.content,
|
|
280
|
+
type: m.type,
|
|
281
|
+
context: m.context,
|
|
282
|
+
activation: m.activation,
|
|
283
|
+
})),
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
181
288
|
function handleFocusPush(
|
|
182
289
|
storage: EngramStorage,
|
|
183
290
|
config: CognitiveConfig,
|
|
@@ -222,6 +329,34 @@ function handleFocusClear(storage: EngramStorage): ToolResult {
|
|
|
222
329
|
return textResult(`Cleared ${count} items from working memory.`);
|
|
223
330
|
}
|
|
224
331
|
|
|
332
|
+
function handleRecallToFocus(
|
|
333
|
+
storage: EngramStorage,
|
|
334
|
+
config: CognitiveConfig,
|
|
335
|
+
args: { cue: string; limit?: number; type?: string; context?: string },
|
|
336
|
+
defaultContext?: string | null,
|
|
337
|
+
): ToolResult {
|
|
338
|
+
const typeFilter = args.type && isValidMemoryType(args.type) ? args.type : undefined;
|
|
339
|
+
const limit = args.limit ?? 3;
|
|
340
|
+
|
|
341
|
+
const results = recall(storage, args.cue, config, {
|
|
342
|
+
limit,
|
|
343
|
+
type: typeFilter,
|
|
344
|
+
context: args.context ?? defaultContext ?? undefined,
|
|
345
|
+
associative: true,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const loaded: string[] = [];
|
|
349
|
+
for (const r of results) {
|
|
350
|
+
pushFocus(storage, r.memory.content, config, {
|
|
351
|
+
memoryRef: r.memory.id,
|
|
352
|
+
});
|
|
353
|
+
loaded.push(r.memory.id);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const { used, capacity } = focusUtilization(storage, config);
|
|
357
|
+
return textResult(JSON.stringify({ loaded, focus: { used, capacity } }));
|
|
358
|
+
}
|
|
359
|
+
|
|
225
360
|
function handleConsolidate(storage: EngramStorage, config: CognitiveConfig): ToolResult {
|
|
226
361
|
const result = consolidate(storage, config);
|
|
227
362
|
const chunks = discoverChunks(storage, config);
|
|
@@ -324,9 +459,6 @@ function handleReconsolidate(
|
|
|
324
459
|
return textResult(
|
|
325
460
|
JSON.stringify({
|
|
326
461
|
id: updated.id,
|
|
327
|
-
content: updated.content,
|
|
328
|
-
emotion: updated.emotion,
|
|
329
|
-
emotionWeight: updated.emotionWeight,
|
|
330
462
|
context: updated.context,
|
|
331
463
|
reconsolidationCount: updated.reconsolidationCount,
|
|
332
464
|
}),
|