neo4j-agent-memory 0.4.0 → 0.5.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 CHANGED
@@ -52,10 +52,16 @@ const bundle = await mem.retrieveContextBundle({
52
52
  prompt: "EACCES cannot create node_modules",
53
53
  symptoms: ["eacces", "permission denied", "node_modules"],
54
54
  tags: ["npm", "node_modules"],
55
- env: { os: "macos", packageManager: "npm", container: false }
55
+ env: { os: "macos", packageManager: "npm", container: false },
56
+ fallback: {
57
+ enabled: true,
58
+ useFulltext: true,
59
+ useTags: true,
60
+ useVector: false,
61
+ },
56
62
  });
57
63
 
58
- await mem.feedback({
64
+ const feedback = await mem.feedback({
59
65
  agentId: "auggie",
60
66
  sessionId: bundle.sessionId,
61
67
  usedIds: bundle.sections.fix.map((m) => m.id),
@@ -69,6 +75,9 @@ await mem.close();
69
75
  Notes:
70
76
  - `createMemoryService` runs schema setup on init.
71
77
  - Cypher assets are bundled at `dist/cypher` in the published package.
78
+ - `feedback()` returns updated RECALLS edge posteriors for the provided memory ids.
79
+ - Neutral usage: pass `neutralIds` or `updateUnratedUsed: false` to avoid penalizing retrieved-but-unrated memories.
80
+ - Fallback retrieval uses fulltext/tag (and optional vector) search; provide `fallback.embedding` when using vector indexes.
72
81
 
73
82
  Auto-relate config (defaults):
74
83
  - `enabled: true`
@@ -89,6 +98,21 @@ Auto-relate behavior:
89
98
  Performance note:
90
99
  - Auto-relate scans candidate memories with tag filtering; for large graphs, keep tags selective and consider tightening `maxCandidates` and `minSharedTags`.
91
100
 
101
+ Neo4j Browser params (auto-relate by tags):
102
+
103
+ ```cypher
104
+ :param nowIso => "2026-01-04T22:07:53.086Z";
105
+ :param id => "mem_8cd773c2-208c-45ad-97ea-1b2337dca751";
106
+ :param minSharedTags => 2;
107
+ :param minWeight => 0.3;
108
+ :param maxCandidates => 10;
109
+ :param sameKind => false;
110
+ :param samePolarity => false;
111
+ :param allowedKinds => [];
112
+ ```
113
+
114
+ Note: `:param` lines are only supported in Neo4j Browser; other runners should pass parameters via the driver.
115
+
92
116
  ## Tool adapter (createMemoryTools)
93
117
 
94
118
  Use the tool factory to preserve the existing tool surface used by the demo:
@@ -127,6 +151,42 @@ const skills = await mem.listSkills({ agentId: "auggie" });
127
151
  const concepts = await mem.listConcepts({ agentId: "auggie" });
128
152
  ```
129
153
 
154
+ ## Graph APIs
155
+
156
+ Fetch full memory records by id:
157
+
158
+ ```ts
159
+ const records = await mem.getMemoriesById({ ids: ["mem-1", "mem-2"] });
160
+ ```
161
+
162
+ Retrieve a weighted subgraph for UI maps:
163
+
164
+ ```ts
165
+ const graph = await mem.getMemoryGraph({
166
+ agentId: "auggie",
167
+ memoryIds: ["mem-1", "mem-2"],
168
+ includeNodes: true,
169
+ includeRelatedTo: true,
170
+ });
171
+ ```
172
+
173
+ List edges for analytics/audit:
174
+
175
+ ```ts
176
+ const edges = await mem.listMemoryEdges({ limit: 500, minStrength: 0.2 });
177
+ ```
178
+
179
+ Retrieve a bundle with graph edges in one call:
180
+
181
+ ```ts
182
+ const bundleWithGraph = await mem.retrieveContextBundleWithGraph({
183
+ agentId: "auggie",
184
+ prompt: "EACCES cannot create node_modules",
185
+ tags: ["npm", "node_modules"],
186
+ includeRelatedTo: true,
187
+ });
188
+ ```
189
+
130
190
  ## Episodic capture helpers
131
191
 
132
192
  ```ts
@@ -151,6 +211,38 @@ await mem.captureStepEpisode({
151
211
  });
152
212
  ```
153
213
 
214
+ ## Useful learning capture
215
+
216
+ ```ts
217
+ await mem.captureUsefulLearning({
218
+ agentId: "auggie",
219
+ sessionId: "run-123",
220
+ useful: true,
221
+ learning: {
222
+ kind: "semantic",
223
+ title: "Avoid chmod 777 on node_modules",
224
+ content: "Use npm cache ownership fixes instead of chmod 777.",
225
+ tags: ["npm", "permissions"],
226
+ confidence: 0.8,
227
+ utility: 0.3,
228
+ },
229
+ });
230
+ ```
231
+
232
+ ## Case helpers
233
+
234
+ ```ts
235
+ await mem.createCase({
236
+ title: "npm EACCES",
237
+ summary: "Permission denied on cache directory.",
238
+ outcome: "resolved",
239
+ symptoms: ["eacces", "permission denied"],
240
+ env: { os: "macos", packageManager: "npm" },
241
+ resolvedByMemoryIds: ["mem-1"],
242
+ negativeMemoryIds: [],
243
+ });
244
+ ```
245
+
154
246
  ## Event hooks
155
247
 
156
248
  Provide an `onMemoryEvent` callback to observe reads/writes:
@@ -7,6 +7,15 @@
7
7
  // - $sameKind: Only relate to same kind if true
8
8
  // - $samePolarity: Only relate to same polarity if true
9
9
  // - $allowedKinds: Optional list of kinds to consider (empty = all)
10
+ // Neo4j Browser params (example only):
11
+ // :param nowIso => "2026-01-04T22:07:53.086Z";
12
+ // :param id => "mem_8cd773c2-208c-45ad-97ea-1b2337dca751";
13
+ // :param minSharedTags => 2;
14
+ // :param minWeight => 0.3;
15
+ // :param maxCandidates => 10;
16
+ // :param sameKind => false;
17
+ // :param samePolarity => false;
18
+ // :param allowedKinds => [];
10
19
  WITH datetime($nowIso) AS now
11
20
  MATCH (src:Memory {id: $id})
12
21
  WITH src, now, coalesce(src.tags, []) AS srcTags
@@ -0,0 +1,68 @@
1
+ // Parameters:
2
+ // - $prompt: Query text
3
+ // - $tags: Array of tags
4
+ // - $kinds: Optional kinds filter
5
+ // - $fulltextIndex: Fulltext index name
6
+ // - $vectorIndex: Vector index name
7
+ // - $embedding: Optional embedding vector
8
+ // - $useFulltext: boolean
9
+ // - $useVector: boolean
10
+ // - $useTags: boolean
11
+ // - $fixLimit: number
12
+ // - $dontLimit: number
13
+ WITH
14
+ coalesce($prompt, "") AS prompt,
15
+ coalesce($tags, []) AS tags,
16
+ coalesce($kinds, []) AS kinds,
17
+ coalesce($fulltextIndex, "") AS fulltextIndex,
18
+ coalesce($vectorIndex, "") AS vectorIndex,
19
+ $embedding AS embedding,
20
+ coalesce($useFulltext, true) AS useFulltext,
21
+ coalesce($useVector, false) AS useVector,
22
+ coalesce($useTags, true) AS useTags,
23
+ coalesce($fixLimit, 8) AS fixLimit,
24
+ coalesce($dontLimit, 6) AS dontLimit
25
+
26
+ CALL {
27
+ WITH useFulltext, fulltextIndex, prompt
28
+ WHERE useFulltext = true AND fulltextIndex <> "" AND prompt <> ""
29
+ CALL db.index.fulltext.queryNodes(fulltextIndex, prompt) YIELD node, score
30
+ RETURN node AS m, score AS score
31
+
32
+ UNION
33
+
34
+ WITH useTags, tags
35
+ WHERE useTags = true AND size(tags) > 0
36
+ MATCH (m:Memory)
37
+ WHERE any(t IN tags WHERE t IN coalesce(m.tags, []))
38
+ RETURN m, 0.1 AS score
39
+
40
+ UNION
41
+
42
+ WITH useVector, vectorIndex, embedding
43
+ WHERE useVector = true AND vectorIndex <> "" AND embedding IS NOT NULL
44
+ CALL db.index.vector.queryNodes(vectorIndex, embedding, 20) YIELD node, score
45
+ RETURN node AS m, score AS score
46
+ }
47
+ WITH m, max(score) AS score, kinds, fixLimit, dontLimit
48
+ WHERE m IS NOT NULL AND (size(kinds) = 0 OR m.kind IN kinds)
49
+ WITH m, score, fixLimit, dontLimit
50
+ ORDER BY score DESC, m.updatedAt DESC
51
+
52
+ WITH collect(m {
53
+ .id,
54
+ .kind,
55
+ .polarity,
56
+ .title,
57
+ .content,
58
+ .tags,
59
+ .confidence,
60
+ .utility,
61
+ .updatedAt
62
+ }) AS rows, fixLimit, dontLimit
63
+
64
+ WITH
65
+ [m IN rows WHERE m.polarity <> "negative"][0..fixLimit] AS fixes,
66
+ [m IN rows WHERE m.polarity = "negative"][0..dontLimit] AS doNot
67
+
68
+ RETURN { fixes: fixes, doNot: doNot } AS sections;
@@ -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,108 @@
1
+ // Parameters:
2
+ // - $agentId: Agent id for RECALLS edges
3
+ // - $memoryIds: Array of memory ids
4
+ // - $includeNodes: Boolean to include node payloads
5
+ // - $includeRelatedTo: Boolean to include RELATED_TO edges
6
+ WITH
7
+ coalesce($agentId, "") AS agentId,
8
+ [id IN coalesce($memoryIds, []) WHERE id IS NOT NULL AND id <> ""] AS ids,
9
+ coalesce($includeNodes, true) AS includeNodes,
10
+ coalesce($includeRelatedTo, false) AS includeRelatedTo
11
+
12
+ CALL (ids, includeNodes) {
13
+ WITH ids, includeNodes
14
+ MATCH (m:Memory)
15
+ WHERE includeNodes = true AND m.id IN ids
16
+ OPTIONAL MATCH (m)-[:APPLIES_IN]->(e:EnvironmentFingerprint)
17
+ WITH m, collect(e {
18
+ .hash,
19
+ .os,
20
+ .distro,
21
+ .ci,
22
+ .container,
23
+ .filesystem,
24
+ .workspaceMount,
25
+ .nodeVersion,
26
+ .packageManager,
27
+ .pmVersion
28
+ }) AS envs
29
+ RETURN collect(m {
30
+ .id,
31
+ .kind,
32
+ .polarity,
33
+ .title,
34
+ .content,
35
+ .tags,
36
+ .confidence,
37
+ .utility,
38
+ .triage,
39
+ .antiPattern,
40
+ .createdAt,
41
+ .updatedAt,
42
+ env: envs[0]
43
+ }) AS nodes
44
+
45
+ UNION
46
+
47
+ WITH ids, includeNodes
48
+ WHERE includeNodes = false
49
+ RETURN [] AS nodes
50
+ }
51
+
52
+ CALL (ids, agentId) {
53
+ WITH ids, agentId
54
+ WHERE agentId IS NOT NULL AND agentId <> ""
55
+ MATCH (a:Agent {id: agentId})-[r:RECALLS]->(m:Memory)
56
+ WHERE m.id IN ids
57
+ RETURN collect({
58
+ source: a.id,
59
+ target: m.id,
60
+ kind: "recalls",
61
+ strength: r.strength,
62
+ evidence: r.evidence,
63
+ updatedAt: toString(r.updatedAt)
64
+ }) AS recallEdges
65
+
66
+ UNION
67
+
68
+ WITH ids, agentId
69
+ WHERE agentId IS NULL OR agentId = ""
70
+ RETURN [] AS recallEdges
71
+ }
72
+
73
+ CALL (ids) {
74
+ WITH ids
75
+ MATCH (m1:Memory)-[c:CO_USED_WITH]->(m2:Memory)
76
+ WHERE m1.id IN ids AND m2.id IN ids
77
+ RETURN collect({
78
+ source: m1.id,
79
+ target: m2.id,
80
+ kind: "co_used_with",
81
+ strength: c.strength,
82
+ evidence: c.evidence,
83
+ updatedAt: toString(c.updatedAt)
84
+ }) AS coUsedEdges
85
+ }
86
+
87
+ CALL (ids, includeRelatedTo) {
88
+ WITH ids, includeRelatedTo
89
+ WHERE includeRelatedTo = true
90
+ MATCH (m1:Memory)-[r:RELATED_TO]->(m2:Memory)
91
+ WHERE m1.id IN ids AND m2.id IN ids
92
+ RETURN collect({
93
+ source: m1.id,
94
+ target: m2.id,
95
+ kind: "related_to",
96
+ strength: r.weight,
97
+ evidence: 0.0,
98
+ updatedAt: toString(r.updatedAt)
99
+ }) AS relatedEdges
100
+
101
+ UNION
102
+
103
+ WITH ids, includeRelatedTo
104
+ WHERE includeRelatedTo = false
105
+ RETURN [] AS relatedEdges
106
+ }
107
+
108
+ RETURN nodes, recallEdges + coUsedEdges + relatedEdges AS edges;
@@ -21,4 +21,8 @@ export const cypher = {
21
21
  listMemories: loadCypher("list_memories.cypher"),
22
22
  relateConcepts: loadCypher("relate_concepts.cypher"),
23
23
  autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher"),
24
+ getMemoriesById: loadCypher("get_memories_by_id.cypher"),
25
+ getMemoryGraph: loadCypher("get_memory_graph.cypher"),
26
+ fallbackRetrieveMemories: loadCypher("fallback_retrieve_memories.cypher"),
27
+ listMemoryEdges: loadCypher("list_memory_edges.cypher"),
24
28
  };
@@ -0,0 +1,37 @@
1
+ // Parameters:
2
+ // - $limit: Max edges to return
3
+ // - $minStrength: Minimum strength threshold
4
+ WITH
5
+ coalesce($limit, 200) AS limit,
6
+ coalesce($minStrength, 0.0) AS minStrength
7
+
8
+ CALL {
9
+ WITH minStrength
10
+ MATCH (m1:Memory)-[c:CO_USED_WITH]->(m2:Memory)
11
+ WHERE coalesce(c.strength, 0.0) >= minStrength
12
+ RETURN {
13
+ source: m1.id,
14
+ target: m2.id,
15
+ kind: "co_used_with",
16
+ strength: c.strength,
17
+ evidence: c.evidence,
18
+ updatedAt: toString(c.updatedAt)
19
+ } AS edge
20
+
21
+ UNION
22
+
23
+ WITH minStrength
24
+ MATCH (m1:Memory)-[r:RELATED_TO]->(m2:Memory)
25
+ WHERE coalesce(r.weight, 0.0) >= minStrength
26
+ RETURN {
27
+ source: m1.id,
28
+ target: m2.id,
29
+ kind: "related_to",
30
+ strength: r.weight,
31
+ evidence: 0.0,
32
+ updatedAt: toString(r.updatedAt)
33
+ } AS edge
34
+ }
35
+ WITH edge, limit
36
+ ORDER BY edge.strength DESC
37
+ RETURN collect(edge)[0..limit] AS edges;