neo4j-agent-memory 0.4.0 → 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
@@ -55,7 +55,7 @@ const bundle = await mem.retrieveContextBundle({
55
55
  env: { os: "macos", packageManager: "npm", container: false }
56
56
  });
57
57
 
58
- await mem.feedback({
58
+ const feedback = await mem.feedback({
59
59
  agentId: "auggie",
60
60
  sessionId: bundle.sessionId,
61
61
  usedIds: bundle.sections.fix.map((m) => m.id),
@@ -69,6 +69,7 @@ await mem.close();
69
69
  Notes:
70
70
  - `createMemoryService` runs schema setup on init.
71
71
  - Cypher assets are bundled at `dist/cypher` in the published package.
72
+ - `feedback()` returns updated RECALLS edge posteriors for the provided memory ids.
72
73
 
73
74
  Auto-relate config (defaults):
74
75
  - `enabled: true`
@@ -127,6 +128,24 @@ const skills = await mem.listSkills({ agentId: "auggie" });
127
128
  const concepts = await mem.listConcepts({ agentId: "auggie" });
128
129
  ```
129
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
+
130
149
  ## Episodic capture helpers
131
150
 
132
151
  ```ts
@@ -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;
@@ -21,4 +21,6 @@ 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"),
24
26
  };
package/dist/index.cjs CHANGED
@@ -86,7 +86,9 @@ var cypher = {
86
86
  feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
87
87
  listMemories: loadCypher("list_memories.cypher"),
88
88
  relateConcepts: loadCypher("relate_concepts.cypher"),
89
- autoRelateByTags: loadCypher("auto_relate_memory_by_tags.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")
90
92
  };
91
93
 
92
94
  // src/neo4j/schema.ts
@@ -146,6 +148,20 @@ function envHash(env) {
146
148
  function clamp01(x) {
147
149
  return Math.max(0, Math.min(1, x));
148
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
+ }
149
165
  var DEFAULT_AUTO_RELATE = {
150
166
  enabled: true,
151
167
  minSharedTags: 2,
@@ -172,6 +188,23 @@ function toBetaEdge(raw) {
172
188
  updatedAt: raw?.updatedAt ?? null
173
189
  };
174
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
+ }
175
208
  function defaultPolicy(req) {
176
209
  return {
177
210
  minConfidence: req?.minConfidence ?? 0.65,
@@ -243,6 +276,8 @@ var MemoryService = class {
243
276
  cyListMemories = cypher.listMemories;
244
277
  cyRelateConcepts = cypher.relateConcepts;
245
278
  cyAutoRelateByTags = cypher.autoRelateByTags;
279
+ cyGetMemoriesById = cypher.getMemoriesById;
280
+ cyGetMemoryGraph = cypher.getMemoryGraph;
246
281
  cyGetRecallEdges = `
247
282
  UNWIND $ids AS id
248
283
  MATCH (m:Memory {id:id})
@@ -528,6 +563,39 @@ ${m.content}`).join("");
528
563
  await session.close();
529
564
  }
530
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
+ }
531
599
  async listEpisodes(args = {}) {
532
600
  return this.listMemories({ ...args, kind: "episodic" });
533
601
  }
@@ -605,11 +673,11 @@ ${m.content}`).join("");
605
673
  y: yById.get(memoryId) ?? 0,
606
674
  w
607
675
  }));
608
- if (items.length === 0) return;
676
+ if (items.length === 0) return { updated: [] };
609
677
  const session = this.client.session("WRITE");
610
678
  try {
611
679
  await session.run("MERGE (a:Agent {id:$id}) RETURN a", { id: fb.agentId });
612
- await session.run(this.cyFeedbackBatch, {
680
+ const feedbackRes = await session.run(this.cyFeedbackBatch, {
613
681
  agentId: fb.agentId,
614
682
  nowIso,
615
683
  items,
@@ -617,6 +685,16 @@ ${m.content}`).join("");
617
685
  aMin: 1e-3,
618
686
  bMin: 1e-3
619
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
+ });
620
698
  const ids = [...used];
621
699
  const pairs = [];
622
700
  for (let i = 0; i < ids.length; i++) {
@@ -638,6 +716,7 @@ ${m.content}`).join("");
638
716
  });
639
717
  }
640
718
  this.emit({ type: "write", action: "feedback", meta: { agentId: fb.agentId, usedCount: used.size } });
719
+ return { updated };
641
720
  } finally {
642
721
  await session.close();
643
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 {
@@ -241,6 +269,8 @@ declare class MemoryService {
241
269
  private cyListMemories;
242
270
  private cyRelateConcepts;
243
271
  private cyAutoRelateByTags;
272
+ private cyGetMemoriesById;
273
+ private cyGetMemoryGraph;
244
274
  private cyGetRecallEdges;
245
275
  constructor(cfg: MemoryServiceConfig);
246
276
  init(): Promise<void>;
@@ -267,6 +297,8 @@ declare class MemoryService {
267
297
  */
268
298
  retrieveContextBundle(args: RetrieveContextArgs): Promise<ContextBundle>;
269
299
  listMemories(args?: ListMemoriesArgs): Promise<MemorySummary[]>;
300
+ getMemoriesById(args: GetMemoriesByIdArgs): Promise<MemoryRecord[]>;
301
+ getMemoryGraph(args: GetMemoryGraphArgs): Promise<MemoryGraphResponse>;
270
302
  listEpisodes(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
271
303
  listSkills(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
272
304
  listConcepts(args?: Omit<ListMemoriesArgs, "kind">): Promise<MemorySummary[]>;
@@ -281,7 +313,7 @@ declare class MemoryService {
281
313
  * Reinforce/degrade agent->memory association weights using a single batched Cypher query.
282
314
  * This supports mid-run retrieval by making feedback cheap and frequent.
283
315
  */
284
- feedback(fb: MemoryFeedback): Promise<void>;
316
+ feedback(fb: MemoryFeedback): Promise<MemoryFeedbackResult>;
285
317
  /**
286
318
  * Save distilled learnings discovered during a task.
287
319
  * Enforces quality gates and stores negative memories explicitly.
@@ -320,6 +352,8 @@ declare const cypher: {
320
352
  listMemories: string;
321
353
  relateConcepts: string;
322
354
  autoRelateByTags: string;
355
+ getMemoriesById: string;
356
+ getMemoryGraph: string;
323
357
  };
324
358
 
325
359
  declare function createMemoryTools(service: MemoryService): MemoryToolSet;
@@ -330,4 +364,4 @@ declare function newId(prefix: string): string;
330
364
  declare function normaliseSymptom(s: string): string;
331
365
  declare function envHash(env: EnvironmentFingerprint): string;
332
366
 
333
- export { type AutoRelateConfig, 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
@@ -36,7 +36,9 @@ var cypher = {
36
36
  feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
37
37
  listMemories: loadCypher("list_memories.cypher"),
38
38
  relateConcepts: loadCypher("relate_concepts.cypher"),
39
- autoRelateByTags: loadCypher("auto_relate_memory_by_tags.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")
40
42
  };
41
43
 
42
44
  // src/neo4j/schema.ts
@@ -96,6 +98,20 @@ function envHash(env) {
96
98
  function clamp01(x) {
97
99
  return Math.max(0, Math.min(1, x));
98
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
+ }
99
115
  var DEFAULT_AUTO_RELATE = {
100
116
  enabled: true,
101
117
  minSharedTags: 2,
@@ -122,6 +138,23 @@ function toBetaEdge(raw) {
122
138
  updatedAt: raw?.updatedAt ?? null
123
139
  };
124
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
+ }
125
158
  function defaultPolicy(req) {
126
159
  return {
127
160
  minConfidence: req?.minConfidence ?? 0.65,
@@ -193,6 +226,8 @@ var MemoryService = class {
193
226
  cyListMemories = cypher.listMemories;
194
227
  cyRelateConcepts = cypher.relateConcepts;
195
228
  cyAutoRelateByTags = cypher.autoRelateByTags;
229
+ cyGetMemoriesById = cypher.getMemoriesById;
230
+ cyGetMemoryGraph = cypher.getMemoryGraph;
196
231
  cyGetRecallEdges = `
197
232
  UNWIND $ids AS id
198
233
  MATCH (m:Memory {id:id})
@@ -478,6 +513,39 @@ ${m.content}`).join("");
478
513
  await session.close();
479
514
  }
480
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
+ }
481
549
  async listEpisodes(args = {}) {
482
550
  return this.listMemories({ ...args, kind: "episodic" });
483
551
  }
@@ -555,11 +623,11 @@ ${m.content}`).join("");
555
623
  y: yById.get(memoryId) ?? 0,
556
624
  w
557
625
  }));
558
- if (items.length === 0) return;
626
+ if (items.length === 0) return { updated: [] };
559
627
  const session = this.client.session("WRITE");
560
628
  try {
561
629
  await session.run("MERGE (a:Agent {id:$id}) RETURN a", { id: fb.agentId });
562
- await session.run(this.cyFeedbackBatch, {
630
+ const feedbackRes = await session.run(this.cyFeedbackBatch, {
563
631
  agentId: fb.agentId,
564
632
  nowIso,
565
633
  items,
@@ -567,6 +635,16 @@ ${m.content}`).join("");
567
635
  aMin: 1e-3,
568
636
  bMin: 1e-3
569
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
+ });
570
648
  const ids = [...used];
571
649
  const pairs = [];
572
650
  for (let i = 0; i < ids.length; i++) {
@@ -588,6 +666,7 @@ ${m.content}`).join("");
588
666
  });
589
667
  }
590
668
  this.emit({ type: "write", action: "feedback", meta: { agentId: fb.agentId, usedCount: used.size } });
669
+ return { updated };
591
670
  } finally {
592
671
  await session.close();
593
672
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo4j-agent-memory",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
package/dist/index.d.cts DELETED
@@ -1,246 +0,0 @@
1
- import { Session } from 'neo4j-driver';
2
-
3
- type MemoryKind = "semantic" | "procedural" | "episodic";
4
- type MemoryPolarity = "positive" | "negative";
5
- interface EnvironmentFingerprint {
6
- hash?: string;
7
- os?: "macos" | "linux" | "windows";
8
- distro?: string;
9
- ci?: string;
10
- container?: boolean;
11
- filesystem?: string;
12
- workspaceMount?: "local" | "network" | "bind" | "readonly";
13
- nodeVersion?: string;
14
- packageManager?: "npm" | "pnpm" | "yarn";
15
- pmVersion?: string;
16
- }
17
- interface DistilledInvariant {
18
- invariant: string;
19
- justification?: string;
20
- verification?: string[];
21
- applicability?: string[];
22
- risks?: string[];
23
- }
24
- interface MemoryRecord {
25
- id: string;
26
- kind: MemoryKind;
27
- polarity: MemoryPolarity;
28
- title: string;
29
- content: string;
30
- tags: string[];
31
- confidence: number;
32
- utility: number;
33
- createdAt?: string;
34
- updatedAt?: string;
35
- signals?: {
36
- symptoms?: string[];
37
- environment?: string[];
38
- };
39
- distilled?: {
40
- invariants?: DistilledInvariant[];
41
- steps?: string[];
42
- verificationSteps?: string[];
43
- gotchas?: string[];
44
- };
45
- antiPattern?: {
46
- action: string;
47
- whyBad: string;
48
- saferAlternative?: string;
49
- };
50
- env?: EnvironmentFingerprint;
51
- }
52
- interface CaseRecord {
53
- id: string;
54
- title: string;
55
- summary: string;
56
- outcome: "resolved" | "unresolved" | "workaround";
57
- symptoms: string[];
58
- env: EnvironmentFingerprint;
59
- resolvedByMemoryIds: string[];
60
- negativeMemoryIds: string[];
61
- resolvedAtIso?: string | null;
62
- }
63
- interface RetrieveContextArgs {
64
- agentId: string;
65
- prompt: string;
66
- symptoms?: string[];
67
- tags?: string[];
68
- kinds?: MemoryKind[];
69
- env?: EnvironmentFingerprint;
70
- baseline?: Record<string, {
71
- a: number;
72
- b: number;
73
- }>;
74
- caseLimit?: number;
75
- fixLimit?: number;
76
- dontLimit?: number;
77
- nowIso?: string;
78
- }
79
- interface BetaEdge {
80
- a: number;
81
- b: number;
82
- /** Posterior mean a/(a+b), cached for query speed */
83
- strength: number;
84
- /** Evidence mass a+b, cached for query speed */
85
- evidence: number;
86
- updatedAt: string | null;
87
- }
88
- type ContextMemoryBase = Pick<MemoryRecord, "id" | "kind" | "polarity" | "title" | "content" | "tags" | "confidence" | "utility" | "updatedAt">;
89
- type ContextMemorySummary = ContextMemoryBase & {
90
- /** Posterior snapshot before the current run (baseline) */
91
- edgeBefore?: BetaEdge;
92
- /** Posterior snapshot after the current run (undefined until feedback arrives) */
93
- edgeAfter?: BetaEdge;
94
- };
95
- interface ContextBundle {
96
- sessionId: string;
97
- sections: {
98
- fix: ContextMemorySummary[];
99
- doNotDo: ContextMemorySummary[];
100
- };
101
- injection: {
102
- fixBlock: string;
103
- doNotDoBlock: string;
104
- };
105
- }
106
- interface FeedbackMetrics {
107
- durationMs?: number;
108
- quality?: number;
109
- hallucinationRisk?: number;
110
- toolCalls?: number;
111
- verificationPassed?: boolean;
112
- }
113
- interface MemoryFeedback {
114
- agentId: string;
115
- sessionId: string;
116
- usedIds: string[];
117
- usefulIds: string[];
118
- notUsefulIds: string[];
119
- preventedErrorIds?: string[];
120
- metrics?: FeedbackMetrics;
121
- notes?: string;
122
- }
123
- interface LearningCandidate {
124
- kind: MemoryKind;
125
- polarity?: MemoryPolarity;
126
- title: string;
127
- content: string;
128
- tags: string[];
129
- confidence: number;
130
- signals?: MemoryRecord["signals"];
131
- env?: EnvironmentFingerprint;
132
- triage?: {
133
- symptoms: string[];
134
- likelyCauses: string[];
135
- verificationSteps?: string[];
136
- fixSteps?: string[];
137
- gotchas?: string[];
138
- };
139
- antiPattern?: MemoryRecord["antiPattern"];
140
- }
141
- interface SaveLearningRequest {
142
- agentId: string;
143
- sessionId?: string;
144
- taskId?: string;
145
- learnings: LearningCandidate[];
146
- policy?: {
147
- minConfidence?: number;
148
- requireVerificationSteps?: boolean;
149
- maxItems?: number;
150
- };
151
- }
152
- interface SaveLearningResult {
153
- saved: Array<{
154
- id: string;
155
- kind: MemoryKind;
156
- title: string;
157
- deduped: boolean;
158
- }>;
159
- rejected: Array<{
160
- title: string;
161
- reason: string;
162
- }>;
163
- }
164
- interface MemoryServiceConfig {
165
- neo4j: {
166
- uri: string;
167
- username: string;
168
- password: string;
169
- database?: string;
170
- };
171
- vectorIndex?: string;
172
- fulltextIndex?: string;
173
- halfLifeSeconds?: number;
174
- }
175
-
176
- declare class MemoryService {
177
- private client;
178
- private vectorIndex;
179
- private fulltextIndex;
180
- private halfLifeSeconds;
181
- private cyUpsertMemory;
182
- private cyUpsertCase;
183
- private cyRetrieveBundle;
184
- private cyFeedbackBatch;
185
- private cyFeedbackCoUsed;
186
- private cyGetRecallEdges;
187
- constructor(cfg: MemoryServiceConfig);
188
- init(): Promise<void>;
189
- close(): Promise<void>;
190
- private ensureEnvHash;
191
- /**
192
- * Save a distilled memory (semantic/procedural/episodic) with exact dedupe by contentHash.
193
- * NOTE: This package intentionally does not store "full answers" as semantic/procedural.
194
- */
195
- upsertMemory(l: LearningCandidate & {
196
- id?: string;
197
- }): Promise<{
198
- id: string;
199
- deduped: boolean;
200
- }>;
201
- /**
202
- * Upsert an episodic Case (case-based reasoning) that links symptoms + env + resolved_by + negative memories.
203
- */
204
- upsertCase(c: CaseRecord): Promise<string>;
205
- /**
206
- * Retrieve a ContextBundle with separate Fix and Do-not-do sections, using case-based reasoning.
207
- * The key idea: match cases by symptoms + env similarity, then pull linked memories.
208
- */
209
- retrieveContextBundle(args: RetrieveContextArgs): Promise<ContextBundle>;
210
- /**
211
- * Reinforce/degrade agent->memory association weights using a single batched Cypher query.
212
- * This supports mid-run retrieval by making feedback cheap and frequent.
213
- */
214
- feedback(fb: MemoryFeedback): Promise<void>;
215
- /**
216
- * Save distilled learnings discovered during a task.
217
- * Enforces quality gates and stores negative memories explicitly.
218
- * Automatically creates a Case if learnings have triage.symptoms.
219
- */
220
- saveLearnings(req: SaveLearningRequest): Promise<SaveLearningResult>;
221
- }
222
- declare function createMemoryService(cfg: MemoryServiceConfig): Promise<MemoryService>;
223
-
224
- interface Neo4jClientConfig {
225
- uri: string;
226
- username: string;
227
- password: string;
228
- database?: string;
229
- }
230
- declare class Neo4jClient {
231
- private driver;
232
- private database?;
233
- constructor(cfg: Neo4jClientConfig);
234
- session(mode?: "READ" | "WRITE"): Session;
235
- close(): Promise<void>;
236
- }
237
-
238
- declare function ensureSchema(client: Neo4jClient): Promise<void>;
239
-
240
- declare function sha256Hex(s: string): string;
241
- declare function canonicaliseForHash(title: string, content: string, tags: string[]): string;
242
- declare function newId(prefix: string): string;
243
- declare function normaliseSymptom(s: string): string;
244
- declare function envHash(env: EnvironmentFingerprint): string;
245
-
246
- export { type BetaEdge, type CaseRecord, type ContextBundle, type ContextMemoryBase, type ContextMemorySummary, type DistilledInvariant, type EnvironmentFingerprint, type FeedbackMetrics, type LearningCandidate, type MemoryFeedback, type MemoryKind, type MemoryPolarity, type MemoryRecord, MemoryService, type MemoryServiceConfig, Neo4jClient, type Neo4jClientConfig, type RetrieveContextArgs, type SaveLearningRequest, type SaveLearningResult, canonicaliseForHash, createMemoryService, ensureSchema, envHash, newId, normaliseSymptom, sha256Hex };