neo4j-agent-memory 0.3.19 → 0.4.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/README.md CHANGED
@@ -35,7 +35,16 @@ const mem = await createMemoryService({
35
35
  neo4j: { uri, username, password },
36
36
  vectorIndex: "memoryEmbedding",
37
37
  fulltextIndex: "memoryText",
38
- halfLifeSeconds: 30 * 24 * 3600
38
+ halfLifeSeconds: 30 * 24 * 3600,
39
+ autoRelate: {
40
+ enabled: true,
41
+ minSharedTags: 2,
42
+ minWeight: 0.2,
43
+ maxCandidates: 12,
44
+ sameKind: true,
45
+ samePolarity: true,
46
+ allowedKinds: ["semantic", "procedural"]
47
+ }
39
48
  });
40
49
 
41
50
  const bundle = await mem.retrieveContextBundle({
@@ -46,7 +55,7 @@ const bundle = await mem.retrieveContextBundle({
46
55
  env: { os: "macos", packageManager: "npm", container: false }
47
56
  });
48
57
 
49
- await mem.feedback({
58
+ const feedback = await mem.feedback({
50
59
  agentId: "auggie",
51
60
  sessionId: bundle.sessionId,
52
61
  usedIds: bundle.sections.fix.map((m) => m.id),
@@ -60,6 +69,26 @@ await mem.close();
60
69
  Notes:
61
70
  - `createMemoryService` runs schema setup on init.
62
71
  - Cypher assets are bundled at `dist/cypher` in the published package.
72
+ - `feedback()` returns updated RECALLS edge posteriors for the provided memory ids.
73
+
74
+ Auto-relate config (defaults):
75
+ - `enabled: true`
76
+ - `minSharedTags: 2`
77
+ - `minWeight: 0.2`
78
+ - `maxCandidates: 12`
79
+ - `sameKind: true`
80
+ - `samePolarity: true`
81
+ - `allowedKinds: ["semantic", "procedural"]`
82
+
83
+ Auto-relate behavior:
84
+ - Uses tag overlap with Jaccard weight (`shared / (a + b - shared)`).
85
+ - Runs only for newly inserted memories (skips deduped).
86
+ - Applies filters for `sameKind`, `samePolarity`, and `allowedKinds`.
87
+ - Requires `minSharedTags` and `minWeight` to pass before linking.
88
+ - Limits to `maxCandidates` highest-weight neighbors.
89
+
90
+ Performance note:
91
+ - Auto-relate scans candidate memories with tag filtering; for large graphs, keep tags selective and consider tightening `maxCandidates` and `minSharedTags`.
63
92
 
64
93
  ## Tool adapter (createMemoryTools)
65
94
 
@@ -99,6 +128,24 @@ const skills = await mem.listSkills({ agentId: "auggie" });
99
128
  const concepts = await mem.listConcepts({ agentId: "auggie" });
100
129
  ```
101
130
 
131
+ ## Graph APIs
132
+
133
+ Fetch full memory records by id:
134
+
135
+ ```ts
136
+ const records = await mem.getMemoriesById({ ids: ["mem-1", "mem-2"] });
137
+ ```
138
+
139
+ Retrieve a weighted subgraph for UI maps:
140
+
141
+ ```ts
142
+ const graph = await mem.getMemoryGraph({
143
+ agentId: "auggie",
144
+ memoryIds: ["mem-1", "mem-2"],
145
+ includeNodes: true,
146
+ });
147
+ ```
148
+
102
149
  ## Episodic capture helpers
103
150
 
104
151
  ```ts
@@ -0,0 +1,45 @@
1
+ // Parameters:
2
+ // - $id: Source memory id
3
+ // - $nowIso: ISO timestamp
4
+ // - $minSharedTags: Minimum overlapping tags
5
+ // - $minWeight: Minimum Jaccard weight to relate
6
+ // - $maxCandidates: Max related memories to link
7
+ // - $sameKind: Only relate to same kind if true
8
+ // - $samePolarity: Only relate to same polarity if true
9
+ // - $allowedKinds: Optional list of kinds to consider (empty = all)
10
+ WITH datetime($nowIso) AS now
11
+ MATCH (src:Memory {id: $id})
12
+ WITH src, now, coalesce(src.tags, []) AS srcTags
13
+ MATCH (m:Memory)
14
+ WHERE m.id <> src.id
15
+ AND (NOT $sameKind OR m.kind = src.kind)
16
+ AND (NOT $samePolarity OR m.polarity = src.polarity)
17
+ AND (size($allowedKinds) = 0 OR m.kind IN $allowedKinds)
18
+ WITH
19
+ src,
20
+ now,
21
+ srcTags,
22
+ m,
23
+ size([t IN srcTags WHERE t IN coalesce(m.tags, [])]) AS shared,
24
+ size(srcTags) AS aSize,
25
+ size(coalesce(m.tags, [])) AS bSize
26
+ WHERE shared >= $minSharedTags
27
+ WITH
28
+ src,
29
+ now,
30
+ m,
31
+ shared,
32
+ CASE
33
+ WHEN (aSize + bSize - shared) = 0 THEN 0.0
34
+ ELSE toFloat(shared) / (aSize + bSize - shared)
35
+ END AS weight
36
+ WHERE weight >= $minWeight
37
+ ORDER BY weight DESC, shared DESC
38
+ LIMIT $maxCandidates
39
+ MERGE (src)-[r:RELATED_TO]->(m)
40
+ ON CREATE SET r.weight = weight, r.createdAt = now, r.updatedAt = now
41
+ ON MATCH SET r.weight = weight, r.updatedAt = now
42
+ MERGE (m)-[r2:RELATED_TO]->(src)
43
+ ON CREATE SET r2.weight = weight, r2.createdAt = now, r2.updatedAt = now
44
+ ON MATCH SET r2.weight = weight, r2.updatedAt = now
45
+ RETURN count(*) AS related;
@@ -4,30 +4,19 @@
4
4
  // halfLifeSeconds: 86400,
5
5
  // aMin: 1.0,
6
6
  // bMin: 1.0,
7
- // w: 1.0,
8
- // y: 1.0,
9
7
  // items: [
10
- // "mem_8cd773c2-208c-45ad-97ea-1b2337dca751",
11
- // "mem_64fbcc73-0b5c-4041-8b6b-66514ffaf1d0",
12
- // "mem_e55b80e2-6156-47bc-a964-9aef596f74a6",
13
- // "mem_13189119-e3ad-4769-9f2b-2d3e3b8bc07f",
14
- // "mem_137c3ff7-a81d-453d-8048-5c1b736db6ca",
15
- // "mem_c0ea5643-3c61-4607-ba9f-67f4c2bb01ee",
16
- // "mem_aa15e7b6-7f94-445a-9c52-3eb24a315215",
17
- // "mem_e93e7bb8-b43c-44f5-a5d3-87545d67be59",
18
- // "mem_b81e3709-35ae-4cab-931d-612dcc2cd43d",
19
- // "mem_ce8a5b62-7ddb-4618-8a6c-93ed3b425f27",
20
- // "mem_737df25b-7944-4dee-a7aa-86af93567663"
8
+ // { memoryId: "mem_8cd773c2-208c-45ad-97ea-1b2337dca751", w: 1.0, y: 1.0 },
9
+ // { memoryId: "mem_64fbcc73-0b5c-4041-8b6b-66514ffaf1d0", w: 1.0, y: 0.0 }
21
10
  // ]
22
11
  // }
23
12
  WITH datetime($nowIso) AS now
24
13
 
25
- UNWIND $items AS memoryId
26
- WITH now, memoryId
27
- WHERE memoryId IS NOT NULL AND memoryId <> ""
14
+ UNWIND $items AS item
15
+ WITH now, item
16
+ WHERE item.memoryId IS NOT NULL AND item.memoryId <> ""
28
17
 
29
18
  MATCH (a:Agent {id: $agentId})
30
- MATCH (m:Memory {id: memoryId})
19
+ MATCH (m:Memory {id: item.memoryId})
31
20
 
32
21
  MERGE (a)-[r:RECALLS]->(m)
33
22
  ON CREATE SET
@@ -38,25 +27,25 @@ ON CREATE SET
38
27
  r.successes = 0,
39
28
  r.failures = 0
40
29
 
41
- WITH now, r,
30
+ WITH now, r, item,
42
31
  CASE
43
32
  WHEN duration.inSeconds(coalesce(r.updatedAt, now), now).seconds > 0
44
33
  THEN duration.inSeconds(coalesce(r.updatedAt, now), now).seconds
45
34
  ELSE 0
46
35
  END AS dt
47
36
 
48
- WITH now, r,
37
+ WITH now, r, item,
49
38
  0.5 ^ (dt / $halfLifeSeconds) AS gamma,
50
39
  coalesce(r.a, $aMin) AS aPrev,
51
40
  coalesce(r.b, $bMin) AS bPrev
52
41
 
53
- WITH now, r, gamma,
42
+ WITH now, r, item, gamma,
54
43
  ($aMin + gamma * (aPrev - $aMin)) AS a0,
55
44
  ($bMin + gamma * (bPrev - $bMin)) AS b0
56
45
 
57
- WITH now, r,
58
- (a0 + $w * $y) AS a1,
59
- (b0 + $w * (1.0 - $y)) AS b1
46
+ WITH now, r, item,
47
+ (a0 + item.w * item.y) AS a1,
48
+ (b0 + item.w * (1.0 - item.y)) AS b1
60
49
 
61
50
  SET r.a = a1,
62
51
  r.b = b1,
@@ -64,7 +53,12 @@ SET r.a = a1,
64
53
  r.evidence = a1 + b1,
65
54
  r.updatedAt = now,
66
55
  r.uses = coalesce(r.uses, 0) + 1,
67
- r.successes = coalesce(r.successes, 0) + CASE WHEN $y >= 0.5 THEN 1 ELSE 0 END,
68
- r.failures = coalesce(r.failures, 0) + CASE WHEN $y < 0.5 THEN 1 ELSE 0 END
56
+ r.successes = coalesce(r.successes, 0) + CASE WHEN item.y >= 0.5 THEN 1 ELSE 0 END,
57
+ r.failures = coalesce(r.failures, 0) + CASE WHEN item.y < 0.5 THEN 1 ELSE 0 END
69
58
 
70
- RETURN count(*) AS updated;
59
+ RETURN item.memoryId AS id,
60
+ r.a AS a,
61
+ r.b AS b,
62
+ r.strength AS strength,
63
+ r.evidence AS evidence,
64
+ toString(r.updatedAt) AS updatedAt;
@@ -0,0 +1,41 @@
1
+ // Parameters:
2
+ // - $ids: Array of memory ids
3
+ WITH [id IN coalesce($ids, []) WHERE id IS NOT NULL AND id <> ""] AS ids
4
+ UNWIND range(0, size(ids) - 1) AS idx
5
+ WITH idx, ids[idx] AS id
6
+ MATCH (m:Memory {id: id})
7
+ OPTIONAL MATCH (m)-[:APPLIES_IN]->(e:EnvironmentFingerprint)
8
+ WITH idx, m, collect(e {
9
+ .hash,
10
+ .os,
11
+ .distro,
12
+ .ci,
13
+ .container,
14
+ .filesystem,
15
+ .workspaceMount,
16
+ .nodeVersion,
17
+ .packageManager,
18
+ .pmVersion
19
+ }) AS envs
20
+ WITH collect({
21
+ idx: idx,
22
+ memory: m {
23
+ .id,
24
+ .kind,
25
+ .polarity,
26
+ .title,
27
+ .content,
28
+ .tags,
29
+ .confidence,
30
+ .utility,
31
+ .triage,
32
+ .antiPattern,
33
+ .createdAt,
34
+ .updatedAt,
35
+ env: envs[0]
36
+ }
37
+ }) AS rows
38
+ UNWIND rows AS row
39
+ WITH row
40
+ ORDER BY row.idx
41
+ RETURN collect(row.memory) AS memories;
@@ -0,0 +1,85 @@
1
+ // Parameters:
2
+ // - $agentId: Agent id for RECALLS edges
3
+ // - $memoryIds: Array of memory ids
4
+ // - $includeNodes: Boolean to include node payloads
5
+ WITH
6
+ coalesce($agentId, "") AS agentId,
7
+ [id IN coalesce($memoryIds, []) WHERE id IS NOT NULL AND id <> ""] AS ids,
8
+ coalesce($includeNodes, true) AS includeNodes
9
+
10
+ CALL (ids, includeNodes) {
11
+ WITH ids, includeNodes
12
+ MATCH (m:Memory)
13
+ WHERE includeNodes = true AND m.id IN ids
14
+ OPTIONAL MATCH (m)-[:APPLIES_IN]->(e:EnvironmentFingerprint)
15
+ WITH m, collect(e {
16
+ .hash,
17
+ .os,
18
+ .distro,
19
+ .ci,
20
+ .container,
21
+ .filesystem,
22
+ .workspaceMount,
23
+ .nodeVersion,
24
+ .packageManager,
25
+ .pmVersion
26
+ }) AS envs
27
+ RETURN collect(m {
28
+ .id,
29
+ .kind,
30
+ .polarity,
31
+ .title,
32
+ .content,
33
+ .tags,
34
+ .confidence,
35
+ .utility,
36
+ .triage,
37
+ .antiPattern,
38
+ .createdAt,
39
+ .updatedAt,
40
+ env: envs[0]
41
+ }) AS nodes
42
+
43
+ UNION
44
+
45
+ WITH ids, includeNodes
46
+ WHERE includeNodes = false
47
+ RETURN [] AS nodes
48
+ }
49
+
50
+ CALL (ids, agentId) {
51
+ WITH ids, agentId
52
+ WHERE agentId IS NOT NULL AND agentId <> ""
53
+ MATCH (a:Agent {id: agentId})-[r:RECALLS]->(m:Memory)
54
+ WHERE m.id IN ids
55
+ RETURN collect({
56
+ source: a.id,
57
+ target: m.id,
58
+ kind: "recalls",
59
+ strength: r.strength,
60
+ evidence: r.evidence,
61
+ updatedAt: toString(r.updatedAt)
62
+ }) AS recallEdges
63
+
64
+ UNION
65
+
66
+ WITH ids, agentId
67
+ WHERE agentId IS NULL OR agentId = ""
68
+ RETURN [] AS recallEdges
69
+ }
70
+
71
+ CALL (ids) {
72
+ WITH ids
73
+ MATCH (m1:Memory)-[c:CO_USED_WITH]->(m2:Memory)
74
+ WHERE m1.id IN ids AND m2.id IN ids
75
+ RETURN collect({
76
+ source: m1.id,
77
+ target: m2.id,
78
+ kind: "co_used_with",
79
+ strength: c.strength,
80
+ evidence: c.evidence,
81
+ updatedAt: toString(c.updatedAt)
82
+ }) AS coUsedEdges
83
+ }
84
+
85
+ RETURN nodes, recallEdges + coUsedEdges AS edges;
@@ -20,4 +20,7 @@ export const cypher = {
20
20
  feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
21
21
  listMemories: loadCypher("list_memories.cypher"),
22
22
  relateConcepts: loadCypher("relate_concepts.cypher"),
23
+ autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher"),
24
+ getMemoriesById: loadCypher("get_memories_by_id.cypher"),
25
+ getMemoryGraph: loadCypher("get_memory_graph.cypher"),
23
26
  };
@@ -16,5 +16,8 @@ FOR (m:Memory) ON (m.polarity);
16
16
  CREATE INDEX memory_kind IF NOT EXISTS
17
17
  FOR (m:Memory) ON (m.kind);
18
18
 
19
+ CREATE INDEX memory_tags IF NOT EXISTS
20
+ FOR (m:Memory) ON (m.tags);
21
+
19
22
  CREATE CONSTRAINT agent_id_unique IF NOT EXISTS
20
23
  FOR (a:Agent) REQUIRE a.id IS UNIQUE;
package/dist/index.cjs CHANGED
@@ -85,7 +85,10 @@ var cypher = {
85
85
  feedbackBatch: loadCypher("feedback_batch.cypher"),
86
86
  feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
87
87
  listMemories: loadCypher("list_memories.cypher"),
88
- relateConcepts: loadCypher("relate_concepts.cypher")
88
+ relateConcepts: loadCypher("relate_concepts.cypher"),
89
+ autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher"),
90
+ getMemoriesById: loadCypher("get_memories_by_id.cypher"),
91
+ getMemoryGraph: loadCypher("get_memory_graph.cypher")
89
92
  };
90
93
 
91
94
  // src/neo4j/schema.ts
@@ -145,6 +148,31 @@ function envHash(env) {
145
148
  function clamp01(x) {
146
149
  return Math.max(0, Math.min(1, x));
147
150
  }
151
+ function parseJsonField(value) {
152
+ if (value === null || value === void 0) return void 0;
153
+ if (typeof value !== "string") return value;
154
+ try {
155
+ return JSON.parse(value);
156
+ } catch {
157
+ return void 0;
158
+ }
159
+ }
160
+ function toDateString(value) {
161
+ if (value === null || value === void 0) return void 0;
162
+ if (typeof value?.toString === "function") return value.toString();
163
+ return String(value);
164
+ }
165
+ var DEFAULT_AUTO_RELATE = {
166
+ enabled: true,
167
+ minSharedTags: 2,
168
+ minWeight: 0.2,
169
+ maxCandidates: 12,
170
+ sameKind: true,
171
+ samePolarity: true,
172
+ allowedKinds: ["semantic", "procedural"]
173
+ };
174
+ var AUTO_RELATE_MIN_SHARED_TAGS = 1;
175
+ var AUTO_RELATE_MIN_MAX_CANDIDATES = 1;
148
176
  function toBetaEdge(raw) {
149
177
  const aMin = 1e-3;
150
178
  const bMin = 1e-3;
@@ -160,6 +188,23 @@ function toBetaEdge(raw) {
160
188
  updatedAt: raw?.updatedAt ?? null
161
189
  };
162
190
  }
191
+ function toMemoryRecord(raw) {
192
+ return {
193
+ id: raw.id,
194
+ kind: raw.kind,
195
+ polarity: raw.polarity ?? "positive",
196
+ title: raw.title,
197
+ content: raw.content,
198
+ tags: raw.tags ?? [],
199
+ confidence: raw.confidence ?? 0.7,
200
+ utility: raw.utility ?? 0.2,
201
+ createdAt: toDateString(raw.createdAt),
202
+ updatedAt: toDateString(raw.updatedAt),
203
+ triage: parseJsonField(raw.triage),
204
+ antiPattern: parseJsonField(raw.antiPattern),
205
+ env: raw.env ?? void 0
206
+ };
207
+ }
163
208
  function defaultPolicy(req) {
164
209
  return {
165
210
  minConfidence: req?.minConfidence ?? 0.65,
@@ -222,6 +267,7 @@ var MemoryService = class {
222
267
  fulltextIndex;
223
268
  halfLifeSeconds;
224
269
  onMemoryEvent;
270
+ autoRelateConfig;
225
271
  cyUpsertMemory = cypher.upsertMemory;
226
272
  cyUpsertCase = cypher.upsertCase;
227
273
  cyRetrieveBundle = cypher.retrieveContextBundle;
@@ -229,6 +275,9 @@ var MemoryService = class {
229
275
  cyFeedbackCoUsed = cypher.feedbackCoUsed;
230
276
  cyListMemories = cypher.listMemories;
231
277
  cyRelateConcepts = cypher.relateConcepts;
278
+ cyAutoRelateByTags = cypher.autoRelateByTags;
279
+ cyGetMemoriesById = cypher.getMemoriesById;
280
+ cyGetMemoryGraph = cypher.getMemoryGraph;
232
281
  cyGetRecallEdges = `
233
282
  UNWIND $ids AS id
234
283
  MATCH (m:Memory {id:id})
@@ -246,6 +295,22 @@ var MemoryService = class {
246
295
  this.fulltextIndex = cfg.fulltextIndex ?? "memoryText";
247
296
  this.halfLifeSeconds = cfg.halfLifeSeconds ?? 30 * 24 * 3600;
248
297
  this.onMemoryEvent = cfg.onMemoryEvent;
298
+ const autoRelate = cfg.autoRelate ?? {};
299
+ this.autoRelateConfig = {
300
+ enabled: autoRelate.enabled ?? DEFAULT_AUTO_RELATE.enabled,
301
+ minSharedTags: Math.max(
302
+ AUTO_RELATE_MIN_SHARED_TAGS,
303
+ Math.floor(autoRelate.minSharedTags ?? DEFAULT_AUTO_RELATE.minSharedTags)
304
+ ),
305
+ minWeight: clamp01(autoRelate.minWeight ?? DEFAULT_AUTO_RELATE.minWeight),
306
+ maxCandidates: Math.max(
307
+ AUTO_RELATE_MIN_MAX_CANDIDATES,
308
+ Math.floor(autoRelate.maxCandidates ?? DEFAULT_AUTO_RELATE.maxCandidates)
309
+ ),
310
+ sameKind: autoRelate.sameKind ?? DEFAULT_AUTO_RELATE.sameKind,
311
+ samePolarity: autoRelate.samePolarity ?? DEFAULT_AUTO_RELATE.samePolarity,
312
+ allowedKinds: autoRelate.allowedKinds ?? [...DEFAULT_AUTO_RELATE.allowedKinds]
313
+ };
249
314
  }
250
315
  async init() {
251
316
  await ensureSchema(this.client);
@@ -330,6 +395,21 @@ var MemoryService = class {
330
395
  }
331
396
  );
332
397
  }
398
+ const autoRelate = this.autoRelateConfig;
399
+ const allowedKinds = autoRelate.allowedKinds ?? [];
400
+ const canAutoRelate = autoRelate.enabled && tags.length >= autoRelate.minSharedTags && (allowedKinds.length === 0 || allowedKinds.includes(l.kind));
401
+ if (canAutoRelate) {
402
+ await write.run(this.cyAutoRelateByTags, {
403
+ id,
404
+ nowIso: (/* @__PURE__ */ new Date()).toISOString(),
405
+ minSharedTags: autoRelate.minSharedTags,
406
+ minWeight: autoRelate.minWeight,
407
+ maxCandidates: autoRelate.maxCandidates,
408
+ sameKind: autoRelate.sameKind,
409
+ samePolarity: autoRelate.samePolarity,
410
+ allowedKinds
411
+ });
412
+ }
333
413
  this.emit({ type: "write", action: "upsertMemory", meta: { id } });
334
414
  return { id, deduped: false };
335
415
  } finally {
@@ -483,6 +563,39 @@ ${m.content}`).join("");
483
563
  await session.close();
484
564
  }
485
565
  }
566
+ async getMemoriesById(args) {
567
+ const ids = [...new Set((args.ids ?? []).filter(Boolean))];
568
+ if (ids.length === 0) return [];
569
+ const session = this.client.session("READ");
570
+ try {
571
+ const res = await session.run(this.cyGetMemoriesById, { ids });
572
+ const memories = res.records[0]?.get("memories") ?? [];
573
+ return memories.map(toMemoryRecord);
574
+ } finally {
575
+ await session.close();
576
+ }
577
+ }
578
+ async getMemoryGraph(args) {
579
+ const ids = [...new Set((args.memoryIds ?? []).filter(Boolean))];
580
+ if (ids.length === 0) return { nodes: [], edges: [] };
581
+ const session = this.client.session("READ");
582
+ try {
583
+ const res = await session.run(this.cyGetMemoryGraph, {
584
+ agentId: args.agentId ?? null,
585
+ memoryIds: ids,
586
+ includeNodes: args.includeNodes ?? true
587
+ });
588
+ const record = res.records[0];
589
+ const nodesRaw = record?.get("nodes") ?? [];
590
+ const edges = record?.get("edges") ?? [];
591
+ return {
592
+ nodes: nodesRaw.map(toMemoryRecord),
593
+ edges
594
+ };
595
+ } finally {
596
+ await session.close();
597
+ }
598
+ }
486
599
  async listEpisodes(args = {}) {
487
600
  return this.listMemories({ ...args, kind: "episodic" });
488
601
  }
@@ -560,11 +673,11 @@ ${m.content}`).join("");
560
673
  y: yById.get(memoryId) ?? 0,
561
674
  w
562
675
  }));
563
- if (items.length === 0) return;
676
+ if (items.length === 0) return { updated: [] };
564
677
  const session = this.client.session("WRITE");
565
678
  try {
566
679
  await session.run("MERGE (a:Agent {id:$id}) RETURN a", { id: fb.agentId });
567
- await session.run(this.cyFeedbackBatch, {
680
+ const feedbackRes = await session.run(this.cyFeedbackBatch, {
568
681
  agentId: fb.agentId,
569
682
  nowIso,
570
683
  items,
@@ -572,6 +685,16 @@ ${m.content}`).join("");
572
685
  aMin: 1e-3,
573
686
  bMin: 1e-3
574
687
  });
688
+ const updated = feedbackRes.records.map((rec) => {
689
+ const raw = {
690
+ a: rec.get("a"),
691
+ b: rec.get("b"),
692
+ strength: rec.get("strength"),
693
+ evidence: rec.get("evidence"),
694
+ updatedAt: rec.get("updatedAt")
695
+ };
696
+ return { id: rec.get("id"), edge: toBetaEdge(raw) };
697
+ });
575
698
  const ids = [...used];
576
699
  const pairs = [];
577
700
  for (let i = 0; i < ids.length; i++) {
@@ -593,6 +716,7 @@ ${m.content}`).join("");
593
716
  });
594
717
  }
595
718
  this.emit({ type: "write", action: "feedback", meta: { agentId: fb.agentId, usedCount: used.size } });
719
+ return { updated };
596
720
  } finally {
597
721
  await session.close();
598
722
  }
package/dist/index.d.ts CHANGED
@@ -21,6 +21,13 @@ interface DistilledInvariant {
21
21
  applicability?: string[];
22
22
  risks?: string[];
23
23
  }
24
+ interface MemoryTriage {
25
+ symptoms: string[];
26
+ likelyCauses: string[];
27
+ verificationSteps?: string[];
28
+ fixSteps?: string[];
29
+ gotchas?: string[];
30
+ }
24
31
  interface MemoryRecord {
25
32
  id: string;
26
33
  kind: MemoryKind;
@@ -32,6 +39,7 @@ interface MemoryRecord {
32
39
  utility: number;
33
40
  createdAt?: string;
34
41
  updatedAt?: string;
42
+ triage?: MemoryTriage;
35
43
  signals?: {
36
44
  symptoms?: string[];
37
45
  environment?: string[];
@@ -60,6 +68,18 @@ interface MemorySummary {
60
68
  createdAt?: string | null;
61
69
  updatedAt?: string | null;
62
70
  }
71
+ interface MemoryGraphEdge {
72
+ source: string;
73
+ target: string;
74
+ kind: "recalls" | "co_used_with";
75
+ strength: number;
76
+ evidence: number;
77
+ updatedAt?: string | null;
78
+ }
79
+ interface MemoryGraphResponse {
80
+ nodes: MemoryRecord[];
81
+ edges: MemoryGraphEdge[];
82
+ }
63
83
  interface CaseRecord {
64
84
  id: string;
65
85
  title: string;
@@ -92,6 +112,14 @@ interface ListMemoriesArgs {
92
112
  limit?: number;
93
113
  agentId?: string;
94
114
  }
115
+ interface GetMemoriesByIdArgs {
116
+ ids: string[];
117
+ }
118
+ interface GetMemoryGraphArgs {
119
+ agentId?: string;
120
+ memoryIds: string[];
121
+ includeNodes?: boolean;
122
+ }
95
123
  interface BetaEdge {
96
124
  a: number;
97
125
  b: number;
@@ -136,6 +164,12 @@ interface MemoryFeedback {
136
164
  metrics?: FeedbackMetrics;
137
165
  notes?: string;
138
166
  }
167
+ interface MemoryFeedbackResult {
168
+ updated: Array<{
169
+ id: string;
170
+ edge: BetaEdge;
171
+ }>;
172
+ }
139
173
  interface CaptureEpisodeArgs {
140
174
  agentId: string;
141
175
  runId: string;
@@ -163,13 +197,7 @@ interface LearningCandidate {
163
197
  confidence: number;
164
198
  signals?: MemoryRecord["signals"];
165
199
  env?: EnvironmentFingerprint;
166
- triage?: {
167
- symptoms: string[];
168
- likelyCauses: string[];
169
- verificationSteps?: string[];
170
- fixSteps?: string[];
171
- gotchas?: string[];
172
- };
200
+ triage?: MemoryTriage;
173
201
  antiPattern?: MemoryRecord["antiPattern"];
174
202
  }
175
203
  interface SaveLearningRequest {
@@ -195,6 +223,15 @@ interface SaveLearningResult {
195
223
  reason: string;
196
224
  }>;
197
225
  }
226
+ interface AutoRelateConfig {
227
+ enabled?: boolean;
228
+ minSharedTags?: number;
229
+ minWeight?: number;
230
+ maxCandidates?: number;
231
+ sameKind?: boolean;
232
+ samePolarity?: boolean;
233
+ allowedKinds?: MemoryKind[];
234
+ }
198
235
  interface MemoryServiceConfig {
199
236
  neo4j: {
200
237
  uri: string;
@@ -205,6 +242,7 @@ interface MemoryServiceConfig {
205
242
  vectorIndex?: string;
206
243
  fulltextIndex?: string;
207
244
  halfLifeSeconds?: number;
245
+ autoRelate?: AutoRelateConfig;
208
246
  onMemoryEvent?: (event: MemoryEvent) => void;
209
247
  }
210
248
  type MemoryToolName = "store_skill" | "store_pattern" | "store_concept" | "relate_concepts" | "recall_skills" | "recall_concepts" | "recall_patterns";
@@ -222,6 +260,7 @@ declare class MemoryService {
222
260
  private fulltextIndex;
223
261
  private halfLifeSeconds;
224
262
  private onMemoryEvent?;
263
+ private autoRelateConfig;
225
264
  private cyUpsertMemory;
226
265
  private cyUpsertCase;
227
266
  private cyRetrieveBundle;
@@ -229,6 +268,9 @@ declare class MemoryService {
229
268
  private cyFeedbackCoUsed;
230
269
  private cyListMemories;
231
270
  private cyRelateConcepts;
271
+ private cyAutoRelateByTags;
272
+ private cyGetMemoriesById;
273
+ private cyGetMemoryGraph;
232
274
  private cyGetRecallEdges;
233
275
  constructor(cfg: MemoryServiceConfig);
234
276
  init(): Promise<void>;
@@ -255,6 +297,8 @@ declare class MemoryService {
255
297
  */
256
298
  retrieveContextBundle(args: RetrieveContextArgs): Promise<ContextBundle>;
257
299
  listMemories(args?: ListMemoriesArgs): Promise<MemorySummary[]>;
300
+ getMemoriesById(args: GetMemoriesByIdArgs): Promise<MemoryRecord[]>;
301
+ getMemoryGraph(args: GetMemoryGraphArgs): Promise<MemoryGraphResponse>;
258
302
  listEpisodes(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
259
303
  listSkills(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
260
304
  listConcepts(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
@@ -269,7 +313,7 @@ declare class MemoryService {
269
313
  * Reinforce/degrade agent->memory association weights using a single batched Cypher query.
270
314
  * This supports mid-run retrieval by making feedback cheap and frequent.
271
315
  */
272
- feedback(fb: MemoryFeedback): Promise<void>;
316
+ feedback(fb: MemoryFeedback): Promise<MemoryFeedbackResult>;
273
317
  /**
274
318
  * Save distilled learnings discovered during a task.
275
319
  * Enforces quality gates and stores negative memories explicitly.
@@ -307,6 +351,9 @@ declare const cypher: {
307
351
  feedbackCoUsed: string;
308
352
  listMemories: string;
309
353
  relateConcepts: string;
354
+ autoRelateByTags: string;
355
+ getMemoriesById: string;
356
+ getMemoryGraph: string;
310
357
  };
311
358
 
312
359
  declare function createMemoryTools(service: MemoryService): MemoryToolSet;
@@ -317,4 +364,4 @@ declare function newId(prefix: string): string;
317
364
  declare function normaliseSymptom(s: string): string;
318
365
  declare function envHash(env: EnvironmentFingerprint): string;
319
366
 
320
- export { type BetaEdge, type CaptureEpisodeArgs, type CaptureStepEpisodeArgs, type CaseRecord, type ContextBundle, type ContextMemoryBase, type ContextMemorySummary, type DistilledInvariant, type EnvironmentFingerprint, type FeedbackMetrics, type LearningCandidate, type ListMemoriesArgs, type MemoryEvent, type MemoryFeedback, type MemoryKind, type MemoryPolarity, type MemoryRecord, MemoryService, type MemoryServiceConfig, type MemorySummary, type MemoryToolDefinition, type MemoryToolName, type MemoryToolSet, Neo4jClient, type Neo4jClientConfig, type RetrieveContextArgs, type SaveLearningRequest, type SaveLearningResult, canonicaliseForHash, createMemoryService, createMemoryTools, cypher, ensureSchema, envHash, loadCypher, migrate, newId, normaliseSymptom, schemaVersion, sha256Hex };
367
+ export { type AutoRelateConfig, type BetaEdge, type CaptureEpisodeArgs, type CaptureStepEpisodeArgs, type CaseRecord, type ContextBundle, type ContextMemoryBase, type ContextMemorySummary, type DistilledInvariant, type EnvironmentFingerprint, type FeedbackMetrics, type GetMemoriesByIdArgs, type GetMemoryGraphArgs, type LearningCandidate, type ListMemoriesArgs, type MemoryEvent, type MemoryFeedback, type MemoryFeedbackResult, type MemoryGraphEdge, type MemoryGraphResponse, type MemoryKind, type MemoryPolarity, type MemoryRecord, MemoryService, type MemoryServiceConfig, type MemorySummary, type MemoryToolDefinition, type MemoryToolName, type MemoryToolSet, type MemoryTriage, Neo4jClient, type Neo4jClientConfig, type RetrieveContextArgs, type SaveLearningRequest, type SaveLearningResult, canonicaliseForHash, createMemoryService, createMemoryTools, cypher, ensureSchema, envHash, loadCypher, migrate, newId, normaliseSymptom, schemaVersion, sha256Hex };
package/dist/index.js CHANGED
@@ -35,7 +35,10 @@ var cypher = {
35
35
  feedbackBatch: loadCypher("feedback_batch.cypher"),
36
36
  feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
37
37
  listMemories: loadCypher("list_memories.cypher"),
38
- relateConcepts: loadCypher("relate_concepts.cypher")
38
+ relateConcepts: loadCypher("relate_concepts.cypher"),
39
+ autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher"),
40
+ getMemoriesById: loadCypher("get_memories_by_id.cypher"),
41
+ getMemoryGraph: loadCypher("get_memory_graph.cypher")
39
42
  };
40
43
 
41
44
  // src/neo4j/schema.ts
@@ -95,6 +98,31 @@ function envHash(env) {
95
98
  function clamp01(x) {
96
99
  return Math.max(0, Math.min(1, x));
97
100
  }
101
+ function parseJsonField(value) {
102
+ if (value === null || value === void 0) return void 0;
103
+ if (typeof value !== "string") return value;
104
+ try {
105
+ return JSON.parse(value);
106
+ } catch {
107
+ return void 0;
108
+ }
109
+ }
110
+ function toDateString(value) {
111
+ if (value === null || value === void 0) return void 0;
112
+ if (typeof value?.toString === "function") return value.toString();
113
+ return String(value);
114
+ }
115
+ var DEFAULT_AUTO_RELATE = {
116
+ enabled: true,
117
+ minSharedTags: 2,
118
+ minWeight: 0.2,
119
+ maxCandidates: 12,
120
+ sameKind: true,
121
+ samePolarity: true,
122
+ allowedKinds: ["semantic", "procedural"]
123
+ };
124
+ var AUTO_RELATE_MIN_SHARED_TAGS = 1;
125
+ var AUTO_RELATE_MIN_MAX_CANDIDATES = 1;
98
126
  function toBetaEdge(raw) {
99
127
  const aMin = 1e-3;
100
128
  const bMin = 1e-3;
@@ -110,6 +138,23 @@ function toBetaEdge(raw) {
110
138
  updatedAt: raw?.updatedAt ?? null
111
139
  };
112
140
  }
141
+ function toMemoryRecord(raw) {
142
+ return {
143
+ id: raw.id,
144
+ kind: raw.kind,
145
+ polarity: raw.polarity ?? "positive",
146
+ title: raw.title,
147
+ content: raw.content,
148
+ tags: raw.tags ?? [],
149
+ confidence: raw.confidence ?? 0.7,
150
+ utility: raw.utility ?? 0.2,
151
+ createdAt: toDateString(raw.createdAt),
152
+ updatedAt: toDateString(raw.updatedAt),
153
+ triage: parseJsonField(raw.triage),
154
+ antiPattern: parseJsonField(raw.antiPattern),
155
+ env: raw.env ?? void 0
156
+ };
157
+ }
113
158
  function defaultPolicy(req) {
114
159
  return {
115
160
  minConfidence: req?.minConfidence ?? 0.65,
@@ -172,6 +217,7 @@ var MemoryService = class {
172
217
  fulltextIndex;
173
218
  halfLifeSeconds;
174
219
  onMemoryEvent;
220
+ autoRelateConfig;
175
221
  cyUpsertMemory = cypher.upsertMemory;
176
222
  cyUpsertCase = cypher.upsertCase;
177
223
  cyRetrieveBundle = cypher.retrieveContextBundle;
@@ -179,6 +225,9 @@ var MemoryService = class {
179
225
  cyFeedbackCoUsed = cypher.feedbackCoUsed;
180
226
  cyListMemories = cypher.listMemories;
181
227
  cyRelateConcepts = cypher.relateConcepts;
228
+ cyAutoRelateByTags = cypher.autoRelateByTags;
229
+ cyGetMemoriesById = cypher.getMemoriesById;
230
+ cyGetMemoryGraph = cypher.getMemoryGraph;
182
231
  cyGetRecallEdges = `
183
232
  UNWIND $ids AS id
184
233
  MATCH (m:Memory {id:id})
@@ -196,6 +245,22 @@ var MemoryService = class {
196
245
  this.fulltextIndex = cfg.fulltextIndex ?? "memoryText";
197
246
  this.halfLifeSeconds = cfg.halfLifeSeconds ?? 30 * 24 * 3600;
198
247
  this.onMemoryEvent = cfg.onMemoryEvent;
248
+ const autoRelate = cfg.autoRelate ?? {};
249
+ this.autoRelateConfig = {
250
+ enabled: autoRelate.enabled ?? DEFAULT_AUTO_RELATE.enabled,
251
+ minSharedTags: Math.max(
252
+ AUTO_RELATE_MIN_SHARED_TAGS,
253
+ Math.floor(autoRelate.minSharedTags ?? DEFAULT_AUTO_RELATE.minSharedTags)
254
+ ),
255
+ minWeight: clamp01(autoRelate.minWeight ?? DEFAULT_AUTO_RELATE.minWeight),
256
+ maxCandidates: Math.max(
257
+ AUTO_RELATE_MIN_MAX_CANDIDATES,
258
+ Math.floor(autoRelate.maxCandidates ?? DEFAULT_AUTO_RELATE.maxCandidates)
259
+ ),
260
+ sameKind: autoRelate.sameKind ?? DEFAULT_AUTO_RELATE.sameKind,
261
+ samePolarity: autoRelate.samePolarity ?? DEFAULT_AUTO_RELATE.samePolarity,
262
+ allowedKinds: autoRelate.allowedKinds ?? [...DEFAULT_AUTO_RELATE.allowedKinds]
263
+ };
199
264
  }
200
265
  async init() {
201
266
  await ensureSchema(this.client);
@@ -280,6 +345,21 @@ var MemoryService = class {
280
345
  }
281
346
  );
282
347
  }
348
+ const autoRelate = this.autoRelateConfig;
349
+ const allowedKinds = autoRelate.allowedKinds ?? [];
350
+ const canAutoRelate = autoRelate.enabled && tags.length >= autoRelate.minSharedTags && (allowedKinds.length === 0 || allowedKinds.includes(l.kind));
351
+ if (canAutoRelate) {
352
+ await write.run(this.cyAutoRelateByTags, {
353
+ id,
354
+ nowIso: (/* @__PURE__ */ new Date()).toISOString(),
355
+ minSharedTags: autoRelate.minSharedTags,
356
+ minWeight: autoRelate.minWeight,
357
+ maxCandidates: autoRelate.maxCandidates,
358
+ sameKind: autoRelate.sameKind,
359
+ samePolarity: autoRelate.samePolarity,
360
+ allowedKinds
361
+ });
362
+ }
283
363
  this.emit({ type: "write", action: "upsertMemory", meta: { id } });
284
364
  return { id, deduped: false };
285
365
  } finally {
@@ -433,6 +513,39 @@ ${m.content}`).join("");
433
513
  await session.close();
434
514
  }
435
515
  }
516
+ async getMemoriesById(args) {
517
+ const ids = [...new Set((args.ids ?? []).filter(Boolean))];
518
+ if (ids.length === 0) return [];
519
+ const session = this.client.session("READ");
520
+ try {
521
+ const res = await session.run(this.cyGetMemoriesById, { ids });
522
+ const memories = res.records[0]?.get("memories") ?? [];
523
+ return memories.map(toMemoryRecord);
524
+ } finally {
525
+ await session.close();
526
+ }
527
+ }
528
+ async getMemoryGraph(args) {
529
+ const ids = [...new Set((args.memoryIds ?? []).filter(Boolean))];
530
+ if (ids.length === 0) return { nodes: [], edges: [] };
531
+ const session = this.client.session("READ");
532
+ try {
533
+ const res = await session.run(this.cyGetMemoryGraph, {
534
+ agentId: args.agentId ?? null,
535
+ memoryIds: ids,
536
+ includeNodes: args.includeNodes ?? true
537
+ });
538
+ const record = res.records[0];
539
+ const nodesRaw = record?.get("nodes") ?? [];
540
+ const edges = record?.get("edges") ?? [];
541
+ return {
542
+ nodes: nodesRaw.map(toMemoryRecord),
543
+ edges
544
+ };
545
+ } finally {
546
+ await session.close();
547
+ }
548
+ }
436
549
  async listEpisodes(args = {}) {
437
550
  return this.listMemories({ ...args, kind: "episodic" });
438
551
  }
@@ -510,11 +623,11 @@ ${m.content}`).join("");
510
623
  y: yById.get(memoryId) ?? 0,
511
624
  w
512
625
  }));
513
- if (items.length === 0) return;
626
+ if (items.length === 0) return { updated: [] };
514
627
  const session = this.client.session("WRITE");
515
628
  try {
516
629
  await session.run("MERGE (a:Agent {id:$id}) RETURN a", { id: fb.agentId });
517
- await session.run(this.cyFeedbackBatch, {
630
+ const feedbackRes = await session.run(this.cyFeedbackBatch, {
518
631
  agentId: fb.agentId,
519
632
  nowIso,
520
633
  items,
@@ -522,6 +635,16 @@ ${m.content}`).join("");
522
635
  aMin: 1e-3,
523
636
  bMin: 1e-3
524
637
  });
638
+ const updated = feedbackRes.records.map((rec) => {
639
+ const raw = {
640
+ a: rec.get("a"),
641
+ b: rec.get("b"),
642
+ strength: rec.get("strength"),
643
+ evidence: rec.get("evidence"),
644
+ updatedAt: rec.get("updatedAt")
645
+ };
646
+ return { id: rec.get("id"), edge: toBetaEdge(raw) };
647
+ });
525
648
  const ids = [...used];
526
649
  const pairs = [];
527
650
  for (let i = 0; i < ids.length; i++) {
@@ -543,6 +666,7 @@ ${m.content}`).join("");
543
666
  });
544
667
  }
545
668
  this.emit({ type: "write", action: "feedback", meta: { agentId: fb.agentId, usedCount: used.size } });
669
+ return { updated };
546
670
  } finally {
547
671
  await session.close();
548
672
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo4j-agent-memory",
3
- "version": "0.3.19",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",