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/dist/index.js CHANGED
@@ -36,7 +36,11 @@ 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"),
42
+ fallbackRetrieveMemories: loadCypher("fallback_retrieve_memories.cypher"),
43
+ listMemoryEdges: loadCypher("list_memory_edges.cypher")
40
44
  };
41
45
 
42
46
  // src/neo4j/schema.ts
@@ -96,6 +100,20 @@ function envHash(env) {
96
100
  function clamp01(x) {
97
101
  return Math.max(0, Math.min(1, x));
98
102
  }
103
+ function parseJsonField(value) {
104
+ if (value === null || value === void 0) return void 0;
105
+ if (typeof value !== "string") return value;
106
+ try {
107
+ return JSON.parse(value);
108
+ } catch {
109
+ return void 0;
110
+ }
111
+ }
112
+ function toDateString(value) {
113
+ if (value === null || value === void 0) return void 0;
114
+ if (typeof value?.toString === "function") return value.toString();
115
+ return String(value);
116
+ }
99
117
  var DEFAULT_AUTO_RELATE = {
100
118
  enabled: true,
101
119
  minSharedTags: 2,
@@ -122,6 +140,23 @@ function toBetaEdge(raw) {
122
140
  updatedAt: raw?.updatedAt ?? null
123
141
  };
124
142
  }
143
+ function toMemoryRecord(raw) {
144
+ return {
145
+ id: raw.id,
146
+ kind: raw.kind,
147
+ polarity: raw.polarity ?? "positive",
148
+ title: raw.title,
149
+ content: raw.content,
150
+ tags: raw.tags ?? [],
151
+ confidence: raw.confidence ?? 0.7,
152
+ utility: raw.utility ?? 0.2,
153
+ createdAt: toDateString(raw.createdAt),
154
+ updatedAt: toDateString(raw.updatedAt),
155
+ triage: parseJsonField(raw.triage),
156
+ antiPattern: parseJsonField(raw.antiPattern),
157
+ env: raw.env ?? void 0
158
+ };
159
+ }
125
160
  function defaultPolicy(req) {
126
161
  return {
127
162
  minConfidence: req?.minConfidence ?? 0.65,
@@ -193,6 +228,10 @@ var MemoryService = class {
193
228
  cyListMemories = cypher.listMemories;
194
229
  cyRelateConcepts = cypher.relateConcepts;
195
230
  cyAutoRelateByTags = cypher.autoRelateByTags;
231
+ cyGetMemoriesById = cypher.getMemoriesById;
232
+ cyGetMemoryGraph = cypher.getMemoryGraph;
233
+ cyFallbackRetrieve = cypher.fallbackRetrieveMemories;
234
+ cyListMemoryEdges = cypher.listMemoryEdges;
196
235
  cyGetRecallEdges = `
197
236
  UNWIND $ids AS id
198
237
  MATCH (m:Memory {id:id})
@@ -280,7 +319,7 @@ var MemoryService = class {
280
319
  contentHash,
281
320
  tags,
282
321
  confidence: clamp01(l.confidence),
283
- utility: 0.2,
322
+ utility: typeof l.utility === "number" ? clamp01(l.utility) : 0.2,
284
323
  // start modest; reinforce via feedback
285
324
  triage: l.triage ? JSON.stringify(l.triage) : null,
286
325
  antiPattern: l.antiPattern ? JSON.stringify(l.antiPattern) : null
@@ -357,6 +396,13 @@ var MemoryService = class {
357
396
  await session.close();
358
397
  }
359
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
+ }
360
406
  /**
361
407
  * Retrieve a ContextBundle with separate Fix and Do-not-do sections, using case-based reasoning.
362
408
  * The key idea: match cases by symptoms + env similarity, then pull linked memories.
@@ -382,28 +428,49 @@ var MemoryService = class {
382
428
  halfLifeSeconds: this.halfLifeSeconds
383
429
  });
384
430
  const sections = r.records[0].get("sections");
385
- const fixes = (sections.fixes ?? []).map((m) => ({
386
- id: m.id,
387
- kind: m.kind,
388
- polarity: m.polarity ?? "positive",
389
- title: m.title,
390
- content: m.content,
391
- tags: m.tags ?? [],
392
- confidence: m.confidence ?? 0.7,
393
- utility: m.utility ?? 0.2,
394
- updatedAt: m.updatedAt?.toString?.() ?? null
395
- }));
396
- const doNot = (sections.doNot ?? []).map((m) => ({
431
+ const mapSummary = (m, fallbackPolarity) => ({
397
432
  id: m.id,
398
433
  kind: m.kind,
399
- polarity: m.polarity ?? "negative",
434
+ polarity: m.polarity ?? fallbackPolarity,
400
435
  title: m.title,
401
436
  content: m.content,
402
437
  tags: m.tags ?? [],
403
438
  confidence: m.confidence ?? 0.7,
404
439
  utility: m.utility ?? 0.2,
405
440
  updatedAt: m.updatedAt?.toString?.() ?? null
406
- }));
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
+ }
407
474
  const allIds = [.../* @__PURE__ */ new Set([...fixes.map((x) => x.id), ...doNot.map((x) => x.id)])];
408
475
  const edgeAfter = /* @__PURE__ */ new Map();
409
476
  if (allIds.length > 0) {
@@ -478,6 +545,66 @@ ${m.content}`).join("");
478
545
  await session.close();
479
546
  }
480
547
  }
548
+ async getMemoriesById(args) {
549
+ const ids = [...new Set((args.ids ?? []).filter(Boolean))];
550
+ if (ids.length === 0) return [];
551
+ const session = this.client.session("READ");
552
+ try {
553
+ const res = await session.run(this.cyGetMemoriesById, { ids });
554
+ const memories = res.records[0]?.get("memories") ?? [];
555
+ return memories.map(toMemoryRecord);
556
+ } finally {
557
+ await session.close();
558
+ }
559
+ }
560
+ async getMemoryGraph(args) {
561
+ const ids = [...new Set((args.memoryIds ?? []).filter(Boolean))];
562
+ if (ids.length === 0) return { nodes: [], edges: [] };
563
+ const session = this.client.session("READ");
564
+ try {
565
+ const res = await session.run(this.cyGetMemoryGraph, {
566
+ agentId: args.agentId ?? null,
567
+ memoryIds: ids,
568
+ includeNodes: args.includeNodes ?? true,
569
+ includeRelatedTo: args.includeRelatedTo ?? false
570
+ });
571
+ const record = res.records[0];
572
+ const nodesRaw = record?.get("nodes") ?? [];
573
+ const edges = record?.get("edges") ?? [];
574
+ return {
575
+ nodes: nodesRaw.map(toMemoryRecord),
576
+ edges
577
+ };
578
+ } finally {
579
+ await session.close();
580
+ }
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
+ }
481
608
  async listEpisodes(args = {}) {
482
609
  return this.listMemories({ ...args, kind: "episodic" });
483
610
  }
@@ -508,6 +635,22 @@ ${m.content}`).join("");
508
635
  this.emit({ type: "write", action: "captureEpisode", meta: { runId: args.runId, title } });
509
636
  return result;
510
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
+ }
511
654
  async captureStepEpisode(args) {
512
655
  const title = `Episode ${args.workflowName} - ${args.stepName}`;
513
656
  const base = {
@@ -537,29 +680,45 @@ ${m.content}`).join("");
537
680
  const used = new Set(fb.usedIds ?? []);
538
681
  const useful = new Set(fb.usefulIds ?? []);
539
682
  const notUseful = new Set(fb.notUsefulIds ?? []);
683
+ const neutral = new Set(fb.neutralIds ?? []);
540
684
  const prevented = new Set(fb.preventedErrorIds ?? []);
685
+ const updateUnratedUsed = fb.updateUnratedUsed ?? true;
541
686
  for (const id of prevented) useful.add(id);
542
687
  for (const id of useful) notUseful.delete(id);
688
+ for (const id of neutral) notUseful.delete(id);
543
689
  for (const id of useful) used.add(id);
544
690
  for (const id of notUseful) used.add(id);
691
+ for (const id of neutral) used.add(id);
545
692
  const quality = clamp01(fb.metrics?.quality ?? 0.7);
546
693
  const hallucRisk = clamp01(fb.metrics?.hallucinationRisk ?? 0.2);
547
694
  const baseY = clamp01(quality - 0.7 * hallucRisk);
548
695
  const w = 0.5 + 1.5 * quality;
549
696
  const yById = /* @__PURE__ */ new Map();
550
697
  for (const id of used) {
551
- 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);
552
711
  }
553
712
  const items = [...used].map((memoryId) => ({
554
713
  memoryId,
555
714
  y: yById.get(memoryId) ?? 0,
556
715
  w
557
716
  }));
558
- if (items.length === 0) return;
717
+ if (items.length === 0) return { updated: [] };
559
718
  const session = this.client.session("WRITE");
560
719
  try {
561
720
  await session.run("MERGE (a:Agent {id:$id}) RETURN a", { id: fb.agentId });
562
- await session.run(this.cyFeedbackBatch, {
721
+ const feedbackRes = await session.run(this.cyFeedbackBatch, {
563
722
  agentId: fb.agentId,
564
723
  nowIso,
565
724
  items,
@@ -567,6 +726,16 @@ ${m.content}`).join("");
567
726
  aMin: 1e-3,
568
727
  bMin: 1e-3
569
728
  });
729
+ const updated = feedbackRes.records.map((rec) => {
730
+ const raw = {
731
+ a: rec.get("a"),
732
+ b: rec.get("b"),
733
+ strength: rec.get("strength"),
734
+ evidence: rec.get("evidence"),
735
+ updatedAt: rec.get("updatedAt")
736
+ };
737
+ return { id: rec.get("id"), edge: toBetaEdge(raw) };
738
+ });
570
739
  const ids = [...used];
571
740
  const pairs = [];
572
741
  for (let i = 0; i < ids.length; i++) {
@@ -588,6 +757,7 @@ ${m.content}`).join("");
588
757
  });
589
758
  }
590
759
  this.emit({ type: "write", action: "feedback", meta: { agentId: fb.agentId, usedCount: used.size } });
760
+ return { updated };
591
761
  } finally {
592
762
  await session.close();
593
763
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo4j-agent-memory",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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 };