neo4j-agent-memory 0.4.1 → 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,7 +52,13 @@ 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
64
  const feedback = await mem.feedback({
@@ -69,7 +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.
72
- - `feedback()` returns updated RECALLS edge posteriors for the provided memory ids.
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.
73
81
 
74
82
  Auto-relate config (defaults):
75
83
  - `enabled: true`
@@ -90,6 +98,21 @@ Auto-relate behavior:
90
98
  Performance note:
91
99
  - Auto-relate scans candidate memories with tag filtering; for large graphs, keep tags selective and consider tightening `maxCandidates` and `minSharedTags`.
92
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
+
93
116
  ## Tool adapter (createMemoryTools)
94
117
 
95
118
  Use the tool factory to preserve the existing tool surface used by the demo:
@@ -143,6 +166,24 @@ const graph = await mem.getMemoryGraph({
143
166
  agentId: "auggie",
144
167
  memoryIds: ["mem-1", "mem-2"],
145
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,
146
187
  });
147
188
  ```
148
189
 
@@ -170,6 +211,38 @@ await mem.captureStepEpisode({
170
211
  });
171
212
  ```
172
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
+
173
246
  ## Event hooks
174
247
 
175
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;
@@ -2,10 +2,12 @@
2
2
  // - $agentId: Agent id for RECALLS edges
3
3
  // - $memoryIds: Array of memory ids
4
4
  // - $includeNodes: Boolean to include node payloads
5
+ // - $includeRelatedTo: Boolean to include RELATED_TO edges
5
6
  WITH
6
7
  coalesce($agentId, "") AS agentId,
7
8
  [id IN coalesce($memoryIds, []) WHERE id IS NOT NULL AND id <> ""] AS ids,
8
- coalesce($includeNodes, true) AS includeNodes
9
+ coalesce($includeNodes, true) AS includeNodes,
10
+ coalesce($includeRelatedTo, false) AS includeRelatedTo
9
11
 
10
12
  CALL (ids, includeNodes) {
11
13
  WITH ids, includeNodes
@@ -82,4 +84,25 @@ CALL (ids) {
82
84
  }) AS coUsedEdges
83
85
  }
84
86
 
85
- RETURN nodes, recallEdges + coUsedEdges AS edges;
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;
@@ -23,4 +23,6 @@ export const cypher = {
23
23
  autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher"),
24
24
  getMemoriesById: loadCypher("get_memories_by_id.cypher"),
25
25
  getMemoryGraph: loadCypher("get_memory_graph.cypher"),
26
+ fallbackRetrieveMemories: loadCypher("fallback_retrieve_memories.cypher"),
27
+ listMemoryEdges: loadCypher("list_memory_edges.cypher"),
26
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;
package/dist/index.cjs CHANGED
@@ -88,7 +88,9 @@ var cypher = {
88
88
  relateConcepts: loadCypher("relate_concepts.cypher"),
89
89
  autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher"),
90
90
  getMemoriesById: loadCypher("get_memories_by_id.cypher"),
91
- getMemoryGraph: loadCypher("get_memory_graph.cypher")
91
+ getMemoryGraph: loadCypher("get_memory_graph.cypher"),
92
+ fallbackRetrieveMemories: loadCypher("fallback_retrieve_memories.cypher"),
93
+ listMemoryEdges: loadCypher("list_memory_edges.cypher")
92
94
  };
93
95
 
94
96
  // src/neo4j/schema.ts
@@ -278,6 +280,8 @@ var MemoryService = class {
278
280
  cyAutoRelateByTags = cypher.autoRelateByTags;
279
281
  cyGetMemoriesById = cypher.getMemoriesById;
280
282
  cyGetMemoryGraph = cypher.getMemoryGraph;
283
+ cyFallbackRetrieve = cypher.fallbackRetrieveMemories;
284
+ cyListMemoryEdges = cypher.listMemoryEdges;
281
285
  cyGetRecallEdges = `
282
286
  UNWIND $ids AS id
283
287
  MATCH (m:Memory {id:id})
@@ -365,7 +369,7 @@ var MemoryService = class {
365
369
  contentHash,
366
370
  tags,
367
371
  confidence: clamp01(l.confidence),
368
- utility: 0.2,
372
+ utility: typeof l.utility === "number" ? clamp01(l.utility) : 0.2,
369
373
  // start modest; reinforce via feedback
370
374
  triage: l.triage ? JSON.stringify(l.triage) : null,
371
375
  antiPattern: l.antiPattern ? JSON.stringify(l.antiPattern) : null
@@ -442,6 +446,13 @@ var MemoryService = class {
442
446
  await session.close();
443
447
  }
444
448
  }
449
+ /**
450
+ * Create a new Case with an auto-generated id if none is provided.
451
+ */
452
+ async createCase(c) {
453
+ const id = c.id ?? newId("case");
454
+ return this.upsertCase({ ...c, id });
455
+ }
445
456
  /**
446
457
  * Retrieve a ContextBundle with separate Fix and Do-not-do sections, using case-based reasoning.
447
458
  * The key idea: match cases by symptoms + env similarity, then pull linked memories.
@@ -467,28 +478,49 @@ var MemoryService = class {
467
478
  halfLifeSeconds: this.halfLifeSeconds
468
479
  });
469
480
  const sections = r.records[0].get("sections");
470
- const fixes = (sections.fixes ?? []).map((m) => ({
471
- id: m.id,
472
- kind: m.kind,
473
- polarity: m.polarity ?? "positive",
474
- title: m.title,
475
- content: m.content,
476
- tags: m.tags ?? [],
477
- confidence: m.confidence ?? 0.7,
478
- utility: m.utility ?? 0.2,
479
- updatedAt: m.updatedAt?.toString?.() ?? null
480
- }));
481
- const doNot = (sections.doNot ?? []).map((m) => ({
481
+ const mapSummary = (m, fallbackPolarity) => ({
482
482
  id: m.id,
483
483
  kind: m.kind,
484
- polarity: m.polarity ?? "negative",
484
+ polarity: m.polarity ?? fallbackPolarity,
485
485
  title: m.title,
486
486
  content: m.content,
487
487
  tags: m.tags ?? [],
488
488
  confidence: m.confidence ?? 0.7,
489
489
  utility: m.utility ?? 0.2,
490
490
  updatedAt: m.updatedAt?.toString?.() ?? null
491
- }));
491
+ });
492
+ let fixes = (sections.fixes ?? []).map((m) => mapSummary(m, "positive"));
493
+ let doNot = (sections.doNot ?? []).map((m) => mapSummary(m, "negative"));
494
+ const fallback = args.fallback ?? {};
495
+ const shouldFallback = fallback.enabled === true && fixes.length === 0 && doNot.length === 0;
496
+ if (shouldFallback) {
497
+ const fallbackFixLimit = fallback.limit ?? fixLimit;
498
+ const fallbackDontLimit = fallback.limit ?? dontLimit;
499
+ try {
500
+ const fallbackRes = await session.run(this.cyFallbackRetrieve, {
501
+ prompt: args.prompt ?? "",
502
+ tags: args.tags ?? [],
503
+ kinds: args.kinds ?? [],
504
+ fulltextIndex: this.fulltextIndex,
505
+ vectorIndex: this.vectorIndex,
506
+ embedding: fallback.embedding ?? null,
507
+ useFulltext: fallback.useFulltext ?? true,
508
+ useVector: fallback.useVector ?? false,
509
+ useTags: fallback.useTags ?? true,
510
+ fixLimit: fallbackFixLimit,
511
+ dontLimit: fallbackDontLimit
512
+ });
513
+ const fbSections = fallbackRes.records[0]?.get("sections");
514
+ fixes = (fbSections?.fixes ?? []).map((m) => mapSummary(m, "positive"));
515
+ doNot = (fbSections?.doNot ?? []).map((m) => mapSummary(m, "negative"));
516
+ } catch (err) {
517
+ this.emit({
518
+ type: "read",
519
+ action: "retrieveContextBundle.fallbackError",
520
+ meta: { message: err instanceof Error ? err.message : String(err) }
521
+ });
522
+ }
523
+ }
492
524
  const allIds = [.../* @__PURE__ */ new Set([...fixes.map((x) => x.id), ...doNot.map((x) => x.id)])];
493
525
  const edgeAfter = /* @__PURE__ */ new Map();
494
526
  if (allIds.length > 0) {
@@ -583,7 +615,8 @@ ${m.content}`).join("");
583
615
  const res = await session.run(this.cyGetMemoryGraph, {
584
616
  agentId: args.agentId ?? null,
585
617
  memoryIds: ids,
586
- includeNodes: args.includeNodes ?? true
618
+ includeNodes: args.includeNodes ?? true,
619
+ includeRelatedTo: args.includeRelatedTo ?? false
587
620
  });
588
621
  const record = res.records[0];
589
622
  const nodesRaw = record?.get("nodes") ?? [];
@@ -596,6 +629,32 @@ ${m.content}`).join("");
596
629
  await session.close();
597
630
  }
598
631
  }
632
+ async listMemoryEdges(args = {}) {
633
+ const session = this.client.session("READ");
634
+ try {
635
+ const res = await session.run(this.cyListMemoryEdges, {
636
+ limit: args.limit ?? 200,
637
+ minStrength: args.minStrength ?? 0
638
+ });
639
+ return res.records[0]?.get("edges") ?? [];
640
+ } finally {
641
+ await session.close();
642
+ }
643
+ }
644
+ async retrieveContextBundleWithGraph(args) {
645
+ const bundle = await this.retrieveContextBundle(args);
646
+ const ids = [
647
+ ...bundle.sections.fix.map((m) => m.id),
648
+ ...bundle.sections.doNotDo.map((m) => m.id)
649
+ ];
650
+ const graph = await this.getMemoryGraph({
651
+ agentId: args.agentId,
652
+ memoryIds: ids,
653
+ includeNodes: args.includeNodes ?? false,
654
+ includeRelatedTo: args.includeRelatedTo ?? false
655
+ });
656
+ return { bundle, graph };
657
+ }
599
658
  async listEpisodes(args = {}) {
600
659
  return this.listMemories({ ...args, kind: "episodic" });
601
660
  }
@@ -626,6 +685,22 @@ ${m.content}`).join("");
626
685
  this.emit({ type: "write", action: "captureEpisode", meta: { runId: args.runId, title } });
627
686
  return result;
628
687
  }
688
+ async captureUsefulLearning(args) {
689
+ if (args.useful === false) {
690
+ return { saved: [], rejected: [{ title: args.learning.title, reason: "not marked useful" }] };
691
+ }
692
+ const result = await this.saveLearnings({
693
+ agentId: args.agentId,
694
+ sessionId: args.sessionId,
695
+ learnings: [args.learning]
696
+ });
697
+ this.emit({
698
+ type: "write",
699
+ action: "captureUsefulLearning",
700
+ meta: { title: args.learning.title, savedCount: result.saved.length }
701
+ });
702
+ return result;
703
+ }
629
704
  async captureStepEpisode(args) {
630
705
  const title = `Episode ${args.workflowName} - ${args.stepName}`;
631
706
  const base = {
@@ -655,18 +730,34 @@ ${m.content}`).join("");
655
730
  const used = new Set(fb.usedIds ?? []);
656
731
  const useful = new Set(fb.usefulIds ?? []);
657
732
  const notUseful = new Set(fb.notUsefulIds ?? []);
733
+ const neutral = new Set(fb.neutralIds ?? []);
658
734
  const prevented = new Set(fb.preventedErrorIds ?? []);
735
+ const updateUnratedUsed = fb.updateUnratedUsed ?? true;
659
736
  for (const id of prevented) useful.add(id);
660
737
  for (const id of useful) notUseful.delete(id);
738
+ for (const id of neutral) notUseful.delete(id);
661
739
  for (const id of useful) used.add(id);
662
740
  for (const id of notUseful) used.add(id);
741
+ for (const id of neutral) used.add(id);
663
742
  const quality = clamp01(fb.metrics?.quality ?? 0.7);
664
743
  const hallucRisk = clamp01(fb.metrics?.hallucinationRisk ?? 0.2);
665
744
  const baseY = clamp01(quality - 0.7 * hallucRisk);
666
745
  const w = 0.5 + 1.5 * quality;
667
746
  const yById = /* @__PURE__ */ new Map();
668
747
  for (const id of used) {
669
- yById.set(id, useful.has(id) ? baseY : 0);
748
+ if (useful.has(id)) {
749
+ yById.set(id, baseY);
750
+ continue;
751
+ }
752
+ if (notUseful.has(id)) {
753
+ yById.set(id, 0);
754
+ continue;
755
+ }
756
+ if (neutral.has(id) || !updateUnratedUsed) {
757
+ yById.set(id, 0.5);
758
+ continue;
759
+ }
760
+ yById.set(id, 0);
670
761
  }
671
762
  const items = [...used].map((memoryId) => ({
672
763
  memoryId,
package/dist/index.d.ts CHANGED
@@ -71,7 +71,7 @@ interface MemorySummary {
71
71
  interface MemoryGraphEdge {
72
72
  source: string;
73
73
  target: string;
74
- kind: "recalls" | "co_used_with";
74
+ kind: "recalls" | "co_used_with" | "related_to";
75
75
  strength: number;
76
76
  evidence: number;
77
77
  updatedAt?: string | null;
@@ -106,6 +106,14 @@ interface RetrieveContextArgs {
106
106
  fixLimit?: number;
107
107
  dontLimit?: number;
108
108
  nowIso?: string;
109
+ fallback?: {
110
+ enabled?: boolean;
111
+ limit?: number;
112
+ useFulltext?: boolean;
113
+ useVector?: boolean;
114
+ useTags?: boolean;
115
+ embedding?: number[];
116
+ };
109
117
  }
110
118
  interface ListMemoriesArgs {
111
119
  kind?: MemoryKind;
@@ -119,6 +127,7 @@ interface GetMemoryGraphArgs {
119
127
  agentId?: string;
120
128
  memoryIds: string[];
121
129
  includeNodes?: boolean;
130
+ includeRelatedTo?: boolean;
122
131
  }
123
132
  interface BetaEdge {
124
133
  a: number;
@@ -160,6 +169,8 @@ interface MemoryFeedback {
160
169
  usedIds: string[];
161
170
  usefulIds: string[];
162
171
  notUsefulIds: string[];
172
+ neutralIds?: string[];
173
+ updateUnratedUsed?: boolean;
163
174
  preventedErrorIds?: string[];
164
175
  metrics?: FeedbackMetrics;
165
176
  notes?: string;
@@ -170,6 +181,34 @@ interface MemoryFeedbackResult {
170
181
  edge: BetaEdge;
171
182
  }>;
172
183
  }
184
+ interface ListMemoryEdgesArgs {
185
+ limit?: number;
186
+ minStrength?: number;
187
+ }
188
+ interface MemoryEdgeExport {
189
+ source: string;
190
+ target: string;
191
+ kind: "co_used_with" | "related_to";
192
+ strength: number;
193
+ evidence: number;
194
+ updatedAt?: string | null;
195
+ }
196
+ interface RetrieveContextBundleWithGraphArgs extends RetrieveContextArgs {
197
+ includeNodes?: boolean;
198
+ includeRelatedTo?: boolean;
199
+ }
200
+ interface ContextBundleWithGraph {
201
+ bundle: ContextBundle;
202
+ graph: MemoryGraphResponse;
203
+ }
204
+ interface CaptureUsefulLearningArgs {
205
+ agentId: string;
206
+ sessionId?: string;
207
+ useful?: boolean;
208
+ learning: LearningCandidate & {
209
+ utility?: number;
210
+ };
211
+ }
173
212
  interface CaptureEpisodeArgs {
174
213
  agentId: string;
175
214
  runId: string;
@@ -195,6 +234,7 @@ interface LearningCandidate {
195
234
  content: string;
196
235
  tags: string[];
197
236
  confidence: number;
237
+ utility?: number;
198
238
  signals?: MemoryRecord["signals"];
199
239
  env?: EnvironmentFingerprint;
200
240
  triage?: MemoryTriage;
@@ -271,6 +311,8 @@ declare class MemoryService {
271
311
  private cyAutoRelateByTags;
272
312
  private cyGetMemoriesById;
273
313
  private cyGetMemoryGraph;
314
+ private cyFallbackRetrieve;
315
+ private cyListMemoryEdges;
274
316
  private cyGetRecallEdges;
275
317
  constructor(cfg: MemoryServiceConfig);
276
318
  init(): Promise<void>;
@@ -291,6 +333,12 @@ declare class MemoryService {
291
333
  * Upsert an episodic Case (case-based reasoning) that links symptoms + env + resolved_by + negative memories.
292
334
  */
293
335
  upsertCase(c: CaseRecord): Promise<string>;
336
+ /**
337
+ * Create a new Case with an auto-generated id if none is provided.
338
+ */
339
+ createCase(c: Omit<CaseRecord, "id"> & {
340
+ id?: string;
341
+ }): Promise<string>;
294
342
  /**
295
343
  * Retrieve a ContextBundle with separate Fix and Do-not-do sections, using case-based reasoning.
296
344
  * The key idea: match cases by symptoms + env similarity, then pull linked memories.
@@ -299,6 +347,8 @@ declare class MemoryService {
299
347
  listMemories(args?: ListMemoriesArgs): Promise<MemorySummary[]>;
300
348
  getMemoriesById(args: GetMemoriesByIdArgs): Promise<MemoryRecord[]>;
301
349
  getMemoryGraph(args: GetMemoryGraphArgs): Promise<MemoryGraphResponse>;
350
+ listMemoryEdges(args?: ListMemoryEdgesArgs): Promise<MemoryEdgeExport[]>;
351
+ retrieveContextBundleWithGraph(args: RetrieveContextBundleWithGraphArgs): Promise<ContextBundleWithGraph>;
302
352
  listEpisodes(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
303
353
  listSkills(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
304
354
  listConcepts(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
@@ -308,6 +358,7 @@ declare class MemoryService {
308
358
  weight?: number;
309
359
  }): Promise<void>;
310
360
  captureEpisode(args: CaptureEpisodeArgs): Promise<SaveLearningResult>;
361
+ captureUsefulLearning(args: CaptureUsefulLearningArgs): Promise<SaveLearningResult>;
311
362
  captureStepEpisode(args: CaptureStepEpisodeArgs): Promise<SaveLearningResult>;
312
363
  /**
313
364
  * Reinforce/degrade agent->memory association weights using a single batched Cypher query.
@@ -354,6 +405,8 @@ declare const cypher: {
354
405
  autoRelateByTags: string;
355
406
  getMemoriesById: string;
356
407
  getMemoryGraph: string;
408
+ fallbackRetrieveMemories: string;
409
+ listMemoryEdges: string;
357
410
  };
358
411
 
359
412
  declare function createMemoryTools(service: MemoryService): MemoryToolSet;
@@ -364,4 +417,4 @@ declare function newId(prefix: string): string;
364
417
  declare function normaliseSymptom(s: string): string;
365
418
  declare function envHash(env: EnvironmentFingerprint): string;
366
419
 
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 };
420
+ export { type AutoRelateConfig, type BetaEdge, type CaptureEpisodeArgs, type CaptureStepEpisodeArgs, type CaptureUsefulLearningArgs, type CaseRecord, type ContextBundle, type ContextBundleWithGraph, type ContextMemoryBase, type ContextMemorySummary, type DistilledInvariant, type EnvironmentFingerprint, type FeedbackMetrics, type GetMemoriesByIdArgs, type GetMemoryGraphArgs, type LearningCandidate, type ListMemoriesArgs, type ListMemoryEdgesArgs, type MemoryEdgeExport, 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 RetrieveContextBundleWithGraphArgs, type SaveLearningRequest, type SaveLearningResult, canonicaliseForHash, createMemoryService, createMemoryTools, cypher, ensureSchema, envHash, loadCypher, migrate, newId, normaliseSymptom, schemaVersion, sha256Hex };
package/dist/index.js CHANGED
@@ -38,7 +38,9 @@ var cypher = {
38
38
  relateConcepts: loadCypher("relate_concepts.cypher"),
39
39
  autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher"),
40
40
  getMemoriesById: loadCypher("get_memories_by_id.cypher"),
41
- getMemoryGraph: loadCypher("get_memory_graph.cypher")
41
+ getMemoryGraph: loadCypher("get_memory_graph.cypher"),
42
+ fallbackRetrieveMemories: loadCypher("fallback_retrieve_memories.cypher"),
43
+ listMemoryEdges: loadCypher("list_memory_edges.cypher")
42
44
  };
43
45
 
44
46
  // src/neo4j/schema.ts
@@ -228,6 +230,8 @@ var MemoryService = class {
228
230
  cyAutoRelateByTags = cypher.autoRelateByTags;
229
231
  cyGetMemoriesById = cypher.getMemoriesById;
230
232
  cyGetMemoryGraph = cypher.getMemoryGraph;
233
+ cyFallbackRetrieve = cypher.fallbackRetrieveMemories;
234
+ cyListMemoryEdges = cypher.listMemoryEdges;
231
235
  cyGetRecallEdges = `
232
236
  UNWIND $ids AS id
233
237
  MATCH (m:Memory {id:id})
@@ -315,7 +319,7 @@ var MemoryService = class {
315
319
  contentHash,
316
320
  tags,
317
321
  confidence: clamp01(l.confidence),
318
- utility: 0.2,
322
+ utility: typeof l.utility === "number" ? clamp01(l.utility) : 0.2,
319
323
  // start modest; reinforce via feedback
320
324
  triage: l.triage ? JSON.stringify(l.triage) : null,
321
325
  antiPattern: l.antiPattern ? JSON.stringify(l.antiPattern) : null
@@ -392,6 +396,13 @@ var MemoryService = class {
392
396
  await session.close();
393
397
  }
394
398
  }
399
+ /**
400
+ * Create a new Case with an auto-generated id if none is provided.
401
+ */
402
+ async createCase(c) {
403
+ const id = c.id ?? newId("case");
404
+ return this.upsertCase({ ...c, id });
405
+ }
395
406
  /**
396
407
  * Retrieve a ContextBundle with separate Fix and Do-not-do sections, using case-based reasoning.
397
408
  * The key idea: match cases by symptoms + env similarity, then pull linked memories.
@@ -417,28 +428,49 @@ var MemoryService = class {
417
428
  halfLifeSeconds: this.halfLifeSeconds
418
429
  });
419
430
  const sections = r.records[0].get("sections");
420
- const fixes = (sections.fixes ?? []).map((m) => ({
421
- id: m.id,
422
- kind: m.kind,
423
- polarity: m.polarity ?? "positive",
424
- title: m.title,
425
- content: m.content,
426
- tags: m.tags ?? [],
427
- confidence: m.confidence ?? 0.7,
428
- utility: m.utility ?? 0.2,
429
- updatedAt: m.updatedAt?.toString?.() ?? null
430
- }));
431
- const doNot = (sections.doNot ?? []).map((m) => ({
431
+ const mapSummary = (m, fallbackPolarity) => ({
432
432
  id: m.id,
433
433
  kind: m.kind,
434
- polarity: m.polarity ?? "negative",
434
+ polarity: m.polarity ?? fallbackPolarity,
435
435
  title: m.title,
436
436
  content: m.content,
437
437
  tags: m.tags ?? [],
438
438
  confidence: m.confidence ?? 0.7,
439
439
  utility: m.utility ?? 0.2,
440
440
  updatedAt: m.updatedAt?.toString?.() ?? null
441
- }));
441
+ });
442
+ let fixes = (sections.fixes ?? []).map((m) => mapSummary(m, "positive"));
443
+ let doNot = (sections.doNot ?? []).map((m) => mapSummary(m, "negative"));
444
+ const fallback = args.fallback ?? {};
445
+ const shouldFallback = fallback.enabled === true && fixes.length === 0 && doNot.length === 0;
446
+ if (shouldFallback) {
447
+ const fallbackFixLimit = fallback.limit ?? fixLimit;
448
+ const fallbackDontLimit = fallback.limit ?? dontLimit;
449
+ try {
450
+ const fallbackRes = await session.run(this.cyFallbackRetrieve, {
451
+ prompt: args.prompt ?? "",
452
+ tags: args.tags ?? [],
453
+ kinds: args.kinds ?? [],
454
+ fulltextIndex: this.fulltextIndex,
455
+ vectorIndex: this.vectorIndex,
456
+ embedding: fallback.embedding ?? null,
457
+ useFulltext: fallback.useFulltext ?? true,
458
+ useVector: fallback.useVector ?? false,
459
+ useTags: fallback.useTags ?? true,
460
+ fixLimit: fallbackFixLimit,
461
+ dontLimit: fallbackDontLimit
462
+ });
463
+ const fbSections = fallbackRes.records[0]?.get("sections");
464
+ fixes = (fbSections?.fixes ?? []).map((m) => mapSummary(m, "positive"));
465
+ doNot = (fbSections?.doNot ?? []).map((m) => mapSummary(m, "negative"));
466
+ } catch (err) {
467
+ this.emit({
468
+ type: "read",
469
+ action: "retrieveContextBundle.fallbackError",
470
+ meta: { message: err instanceof Error ? err.message : String(err) }
471
+ });
472
+ }
473
+ }
442
474
  const allIds = [.../* @__PURE__ */ new Set([...fixes.map((x) => x.id), ...doNot.map((x) => x.id)])];
443
475
  const edgeAfter = /* @__PURE__ */ new Map();
444
476
  if (allIds.length > 0) {
@@ -533,7 +565,8 @@ ${m.content}`).join("");
533
565
  const res = await session.run(this.cyGetMemoryGraph, {
534
566
  agentId: args.agentId ?? null,
535
567
  memoryIds: ids,
536
- includeNodes: args.includeNodes ?? true
568
+ includeNodes: args.includeNodes ?? true,
569
+ includeRelatedTo: args.includeRelatedTo ?? false
537
570
  });
538
571
  const record = res.records[0];
539
572
  const nodesRaw = record?.get("nodes") ?? [];
@@ -546,6 +579,32 @@ ${m.content}`).join("");
546
579
  await session.close();
547
580
  }
548
581
  }
582
+ async listMemoryEdges(args = {}) {
583
+ const session = this.client.session("READ");
584
+ try {
585
+ const res = await session.run(this.cyListMemoryEdges, {
586
+ limit: args.limit ?? 200,
587
+ minStrength: args.minStrength ?? 0
588
+ });
589
+ return res.records[0]?.get("edges") ?? [];
590
+ } finally {
591
+ await session.close();
592
+ }
593
+ }
594
+ async retrieveContextBundleWithGraph(args) {
595
+ const bundle = await this.retrieveContextBundle(args);
596
+ const ids = [
597
+ ...bundle.sections.fix.map((m) => m.id),
598
+ ...bundle.sections.doNotDo.map((m) => m.id)
599
+ ];
600
+ const graph = await this.getMemoryGraph({
601
+ agentId: args.agentId,
602
+ memoryIds: ids,
603
+ includeNodes: args.includeNodes ?? false,
604
+ includeRelatedTo: args.includeRelatedTo ?? false
605
+ });
606
+ return { bundle, graph };
607
+ }
549
608
  async listEpisodes(args = {}) {
550
609
  return this.listMemories({ ...args, kind: "episodic" });
551
610
  }
@@ -576,6 +635,22 @@ ${m.content}`).join("");
576
635
  this.emit({ type: "write", action: "captureEpisode", meta: { runId: args.runId, title } });
577
636
  return result;
578
637
  }
638
+ async captureUsefulLearning(args) {
639
+ if (args.useful === false) {
640
+ return { saved: [], rejected: [{ title: args.learning.title, reason: "not marked useful" }] };
641
+ }
642
+ const result = await this.saveLearnings({
643
+ agentId: args.agentId,
644
+ sessionId: args.sessionId,
645
+ learnings: [args.learning]
646
+ });
647
+ this.emit({
648
+ type: "write",
649
+ action: "captureUsefulLearning",
650
+ meta: { title: args.learning.title, savedCount: result.saved.length }
651
+ });
652
+ return result;
653
+ }
579
654
  async captureStepEpisode(args) {
580
655
  const title = `Episode ${args.workflowName} - ${args.stepName}`;
581
656
  const base = {
@@ -605,18 +680,34 @@ ${m.content}`).join("");
605
680
  const used = new Set(fb.usedIds ?? []);
606
681
  const useful = new Set(fb.usefulIds ?? []);
607
682
  const notUseful = new Set(fb.notUsefulIds ?? []);
683
+ const neutral = new Set(fb.neutralIds ?? []);
608
684
  const prevented = new Set(fb.preventedErrorIds ?? []);
685
+ const updateUnratedUsed = fb.updateUnratedUsed ?? true;
609
686
  for (const id of prevented) useful.add(id);
610
687
  for (const id of useful) notUseful.delete(id);
688
+ for (const id of neutral) notUseful.delete(id);
611
689
  for (const id of useful) used.add(id);
612
690
  for (const id of notUseful) used.add(id);
691
+ for (const id of neutral) used.add(id);
613
692
  const quality = clamp01(fb.metrics?.quality ?? 0.7);
614
693
  const hallucRisk = clamp01(fb.metrics?.hallucinationRisk ?? 0.2);
615
694
  const baseY = clamp01(quality - 0.7 * hallucRisk);
616
695
  const w = 0.5 + 1.5 * quality;
617
696
  const yById = /* @__PURE__ */ new Map();
618
697
  for (const id of used) {
619
- yById.set(id, useful.has(id) ? baseY : 0);
698
+ if (useful.has(id)) {
699
+ yById.set(id, baseY);
700
+ continue;
701
+ }
702
+ if (notUseful.has(id)) {
703
+ yById.set(id, 0);
704
+ continue;
705
+ }
706
+ if (neutral.has(id) || !updateUnratedUsed) {
707
+ yById.set(id, 0.5);
708
+ continue;
709
+ }
710
+ yById.set(id, 0);
620
711
  }
621
712
  const items = [...used].map((memoryId) => ({
622
713
  memoryId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo4j-agent-memory",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",