neo4j-agent-memory 0.3.17 → 0.4.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
@@ -35,7 +35,16 @@ const mem = await createMemoryService({
35
35
  neo4j: { uri, username, password },
36
36
  vectorIndex: "memoryEmbedding",
37
37
  fulltextIndex: "memoryText",
38
- halfLifeSeconds: 30 * 24 * 3600
38
+ halfLifeSeconds: 30 * 24 * 3600,
39
+ autoRelate: {
40
+ enabled: true,
41
+ minSharedTags: 2,
42
+ minWeight: 0.2,
43
+ maxCandidates: 12,
44
+ sameKind: true,
45
+ samePolarity: true,
46
+ allowedKinds: ["semantic", "procedural"]
47
+ }
39
48
  });
40
49
 
41
50
  const bundle = await mem.retrieveContextBundle({
@@ -61,6 +70,25 @@ Notes:
61
70
  - `createMemoryService` runs schema setup on init.
62
71
  - Cypher assets are bundled at `dist/cypher` in the published package.
63
72
 
73
+ Auto-relate config (defaults):
74
+ - `enabled: true`
75
+ - `minSharedTags: 2`
76
+ - `minWeight: 0.2`
77
+ - `maxCandidates: 12`
78
+ - `sameKind: true`
79
+ - `samePolarity: true`
80
+ - `allowedKinds: ["semantic", "procedural"]`
81
+
82
+ Auto-relate behavior:
83
+ - Uses tag overlap with Jaccard weight (`shared / (a + b - shared)`).
84
+ - Runs only for newly inserted memories (skips deduped).
85
+ - Applies filters for `sameKind`, `samePolarity`, and `allowedKinds`.
86
+ - Requires `minSharedTags` and `minWeight` to pass before linking.
87
+ - Limits to `maxCandidates` highest-weight neighbors.
88
+
89
+ Performance note:
90
+ - Auto-relate scans candidate memories with tag filtering; for large graphs, keep tags selective and consider tightening `maxCandidates` and `minSharedTags`.
91
+
64
92
  ## Tool adapter (createMemoryTools)
65
93
 
66
94
  Use the tool factory to preserve the existing tool surface used by the demo:
@@ -0,0 +1,45 @@
1
+ // Parameters:
2
+ // - $id: Source memory id
3
+ // - $nowIso: ISO timestamp
4
+ // - $minSharedTags: Minimum overlapping tags
5
+ // - $minWeight: Minimum Jaccard weight to relate
6
+ // - $maxCandidates: Max related memories to link
7
+ // - $sameKind: Only relate to same kind if true
8
+ // - $samePolarity: Only relate to same polarity if true
9
+ // - $allowedKinds: Optional list of kinds to consider (empty = all)
10
+ WITH datetime($nowIso) AS now
11
+ MATCH (src:Memory {id: $id})
12
+ WITH src, now, coalesce(src.tags, []) AS srcTags
13
+ MATCH (m:Memory)
14
+ WHERE m.id <> src.id
15
+ AND (NOT $sameKind OR m.kind = src.kind)
16
+ AND (NOT $samePolarity OR m.polarity = src.polarity)
17
+ AND (size($allowedKinds) = 0 OR m.kind IN $allowedKinds)
18
+ WITH
19
+ src,
20
+ now,
21
+ srcTags,
22
+ m,
23
+ size([t IN srcTags WHERE t IN coalesce(m.tags, [])]) AS shared,
24
+ size(srcTags) AS aSize,
25
+ size(coalesce(m.tags, [])) AS bSize
26
+ WHERE shared >= $minSharedTags
27
+ WITH
28
+ src,
29
+ now,
30
+ m,
31
+ shared,
32
+ CASE
33
+ WHEN (aSize + bSize - shared) = 0 THEN 0.0
34
+ ELSE toFloat(shared) / (aSize + bSize - shared)
35
+ END AS weight
36
+ WHERE weight >= $minWeight
37
+ ORDER BY weight DESC, shared DESC
38
+ LIMIT $maxCandidates
39
+ MERGE (src)-[r:RELATED_TO]->(m)
40
+ ON CREATE SET r.weight = weight, r.createdAt = now, r.updatedAt = now
41
+ ON MATCH SET r.weight = weight, r.updatedAt = now
42
+ MERGE (m)-[r2:RELATED_TO]->(src)
43
+ ON CREATE SET r2.weight = weight, r2.createdAt = now, r2.updatedAt = now
44
+ ON MATCH SET r2.weight = weight, r2.updatedAt = now
45
+ RETURN count(*) AS related;
@@ -1,7 +1,34 @@
1
+ // :param {
2
+ // nowIso: "2026-01-04T22:07:53.086Z",
3
+ // agentId: "agent-123",
4
+ // halfLifeSeconds: 86400,
5
+ // aMin: 1.0,
6
+ // bMin: 1.0,
7
+ // w: 1.0,
8
+ // y: 1.0,
9
+ // 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"
21
+ // ]
22
+ // }
1
23
  WITH datetime($nowIso) AS now
2
- UNWIND $items AS item
24
+
25
+ UNWIND $items AS memoryId
26
+ WITH now, memoryId
27
+ WHERE memoryId IS NOT NULL AND memoryId <> ""
28
+
3
29
  MATCH (a:Agent {id: $agentId})
4
- MATCH (m:Memory {id: item.memoryId})
30
+ MATCH (m:Memory {id: memoryId})
31
+
5
32
  MERGE (a)-[r:RECALLS]->(m)
6
33
  ON CREATE SET
7
34
  r.a = 1.0,
@@ -11,32 +38,33 @@ ON CREATE SET
11
38
  r.successes = 0,
12
39
  r.failures = 0
13
40
 
14
- WITH now, r, item,
15
- // elapsed seconds, safe
16
- CASE WHEN duration.inSeconds(coalesce(r.updatedAt, now), now).seconds > 0
17
- THEN duration.inSeconds(coalesce(r.updatedAt, now), now).seconds
18
- ELSE 0 END AS dt
41
+ WITH now, r,
42
+ CASE
43
+ WHEN duration.inSeconds(coalesce(r.updatedAt, now), now).seconds > 0
44
+ THEN duration.inSeconds(coalesce(r.updatedAt, now), now).seconds
45
+ ELSE 0
46
+ END AS dt
19
47
 
20
- WITH now, r, item,
48
+ WITH now, r,
21
49
  0.5 ^ (dt / $halfLifeSeconds) AS gamma,
22
- coalesce(r.a, CASE WHEN $aMin > coalesce(r.strength, 0.5) * 2.0 THEN $aMin ELSE coalesce(r.strength, 0.5) * 2.0 END) AS aPrev,
23
- coalesce(r.b, CASE WHEN $bMin > (1.0 - coalesce(r.strength, 0.5)) * 2.0 THEN $bMin ELSE (1.0 - coalesce(r.strength, 0.5)) * 2.0 END) AS bPrev
50
+ coalesce(r.a, $aMin) AS aPrev,
51
+ coalesce(r.b, $bMin) AS bPrev
24
52
 
25
- WITH now, r, item, gamma,
53
+ WITH now, r, gamma,
26
54
  ($aMin + gamma * (aPrev - $aMin)) AS a0,
27
55
  ($bMin + gamma * (bPrev - $bMin)) AS b0
28
56
 
29
- WITH now, r, item,
30
- (a0 + item.w * item.y) AS a1,
31
- (b0 + item.w * (1.0 - item.y)) AS b1
57
+ WITH now, r,
58
+ (a0 + $w * $y) AS a1,
59
+ (b0 + $w * (1.0 - $y)) AS b1
32
60
 
33
61
  SET r.a = a1,
34
62
  r.b = b1,
35
63
  r.strength = a1 / (a1 + b1),
36
64
  r.evidence = a1 + b1,
37
65
  r.updatedAt = now,
38
- r.uses = coalesce(r.uses,0) + 1,
39
- r.successes = coalesce(r.successes,0) + CASE WHEN item.y >= 0.5 THEN 1 ELSE 0 END,
40
- r.failures = coalesce(r.failures,0) + CASE WHEN item.y < 0.5 THEN 1 ELSE 0 END
66
+ 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
41
69
 
42
70
  RETURN count(*) AS updated;
@@ -20,4 +20,5 @@ export const cypher = {
20
20
  feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
21
21
  listMemories: loadCypher("list_memories.cypher"),
22
22
  relateConcepts: loadCypher("relate_concepts.cypher"),
23
+ autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher"),
23
24
  };
@@ -1,28 +1,36 @@
1
- // Parameters:
1
+ // Parameters (Neo4j Browser :param)
2
+ // :param { kind: null, limit: 50, agentId: "" }
3
+
2
4
  // - $kind: Optional memory kind filter ("semantic" | "procedural" | "episodic")
3
5
  // - $limit: Max number of memories to return
4
6
  // - $agentId: Optional agent id to filter by RECALLS edges
7
+
5
8
  WITH
6
- $kind AS kind,
9
+ coalesce($kind, null) AS kind,
7
10
  coalesce($limit, 50) AS limit,
8
- $agentId AS agentId
11
+ coalesce($agentId, "") AS agentId
9
12
 
10
- CALL {
11
- WITH kind, agentId
13
+ CALL (kind, agentId) {
14
+ // If agentId provided -> only recalled by that agent
12
15
  WITH kind, agentId
13
16
  WHERE agentId IS NOT NULL AND agentId <> ""
14
17
  MATCH (:Agent {id: agentId})-[:RECALLS]->(m:Memory)
15
18
  WHERE kind IS NULL OR m.kind = kind
16
19
  RETURN m
20
+
17
21
  UNION
22
+
23
+ // If agentId absent -> all memories
18
24
  WITH kind, agentId
19
25
  WHERE agentId IS NULL OR agentId = ""
20
26
  MATCH (m:Memory)
21
27
  WHERE kind IS NULL OR m.kind = kind
22
28
  RETURN m
23
29
  }
24
- WITH m
30
+
31
+ WITH m, limit
25
32
  ORDER BY m.updatedAt DESC
33
+
26
34
  WITH collect(
27
35
  m {
28
36
  .id,
@@ -36,4 +44,5 @@ WITH collect(
36
44
  .updatedAt
37
45
  }
38
46
  ) AS rows, limit
47
+
39
48
  RETURN rows[0..limit] AS memories;
@@ -16,5 +16,8 @@ FOR (m:Memory) ON (m.polarity);
16
16
  CREATE INDEX memory_kind IF NOT EXISTS
17
17
  FOR (m:Memory) ON (m.kind);
18
18
 
19
+ CREATE INDEX memory_tags IF NOT EXISTS
20
+ FOR (m:Memory) ON (m.tags);
21
+
19
22
  CREATE CONSTRAINT agent_id_unique IF NOT EXISTS
20
23
  FOR (a:Agent) REQUIRE a.id IS UNIQUE;
package/dist/index.cjs CHANGED
@@ -85,7 +85,8 @@ var cypher = {
85
85
  feedbackBatch: loadCypher("feedback_batch.cypher"),
86
86
  feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
87
87
  listMemories: loadCypher("list_memories.cypher"),
88
- relateConcepts: loadCypher("relate_concepts.cypher")
88
+ relateConcepts: loadCypher("relate_concepts.cypher"),
89
+ autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher")
89
90
  };
90
91
 
91
92
  // src/neo4j/schema.ts
@@ -145,6 +146,17 @@ function envHash(env) {
145
146
  function clamp01(x) {
146
147
  return Math.max(0, Math.min(1, x));
147
148
  }
149
+ var DEFAULT_AUTO_RELATE = {
150
+ enabled: true,
151
+ minSharedTags: 2,
152
+ minWeight: 0.2,
153
+ maxCandidates: 12,
154
+ sameKind: true,
155
+ samePolarity: true,
156
+ allowedKinds: ["semantic", "procedural"]
157
+ };
158
+ var AUTO_RELATE_MIN_SHARED_TAGS = 1;
159
+ var AUTO_RELATE_MIN_MAX_CANDIDATES = 1;
148
160
  function toBetaEdge(raw) {
149
161
  const aMin = 1e-3;
150
162
  const bMin = 1e-3;
@@ -222,6 +234,7 @@ var MemoryService = class {
222
234
  fulltextIndex;
223
235
  halfLifeSeconds;
224
236
  onMemoryEvent;
237
+ autoRelateConfig;
225
238
  cyUpsertMemory = cypher.upsertMemory;
226
239
  cyUpsertCase = cypher.upsertCase;
227
240
  cyRetrieveBundle = cypher.retrieveContextBundle;
@@ -229,6 +242,7 @@ var MemoryService = class {
229
242
  cyFeedbackCoUsed = cypher.feedbackCoUsed;
230
243
  cyListMemories = cypher.listMemories;
231
244
  cyRelateConcepts = cypher.relateConcepts;
245
+ cyAutoRelateByTags = cypher.autoRelateByTags;
232
246
  cyGetRecallEdges = `
233
247
  UNWIND $ids AS id
234
248
  MATCH (m:Memory {id:id})
@@ -246,6 +260,22 @@ var MemoryService = class {
246
260
  this.fulltextIndex = cfg.fulltextIndex ?? "memoryText";
247
261
  this.halfLifeSeconds = cfg.halfLifeSeconds ?? 30 * 24 * 3600;
248
262
  this.onMemoryEvent = cfg.onMemoryEvent;
263
+ const autoRelate = cfg.autoRelate ?? {};
264
+ this.autoRelateConfig = {
265
+ enabled: autoRelate.enabled ?? DEFAULT_AUTO_RELATE.enabled,
266
+ minSharedTags: Math.max(
267
+ AUTO_RELATE_MIN_SHARED_TAGS,
268
+ Math.floor(autoRelate.minSharedTags ?? DEFAULT_AUTO_RELATE.minSharedTags)
269
+ ),
270
+ minWeight: clamp01(autoRelate.minWeight ?? DEFAULT_AUTO_RELATE.minWeight),
271
+ maxCandidates: Math.max(
272
+ AUTO_RELATE_MIN_MAX_CANDIDATES,
273
+ Math.floor(autoRelate.maxCandidates ?? DEFAULT_AUTO_RELATE.maxCandidates)
274
+ ),
275
+ sameKind: autoRelate.sameKind ?? DEFAULT_AUTO_RELATE.sameKind,
276
+ samePolarity: autoRelate.samePolarity ?? DEFAULT_AUTO_RELATE.samePolarity,
277
+ allowedKinds: autoRelate.allowedKinds ?? [...DEFAULT_AUTO_RELATE.allowedKinds]
278
+ };
249
279
  }
250
280
  async init() {
251
281
  await ensureSchema(this.client);
@@ -330,6 +360,21 @@ var MemoryService = class {
330
360
  }
331
361
  );
332
362
  }
363
+ const autoRelate = this.autoRelateConfig;
364
+ const allowedKinds = autoRelate.allowedKinds ?? [];
365
+ const canAutoRelate = autoRelate.enabled && tags.length >= autoRelate.minSharedTags && (allowedKinds.length === 0 || allowedKinds.includes(l.kind));
366
+ if (canAutoRelate) {
367
+ await write.run(this.cyAutoRelateByTags, {
368
+ id,
369
+ nowIso: (/* @__PURE__ */ new Date()).toISOString(),
370
+ minSharedTags: autoRelate.minSharedTags,
371
+ minWeight: autoRelate.minWeight,
372
+ maxCandidates: autoRelate.maxCandidates,
373
+ sameKind: autoRelate.sameKind,
374
+ samePolarity: autoRelate.samePolarity,
375
+ allowedKinds
376
+ });
377
+ }
333
378
  this.emit({ type: "write", action: "upsertMemory", meta: { id } });
334
379
  return { id, deduped: false };
335
380
  } finally {
@@ -0,0 +1,246 @@
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 };
package/dist/index.d.ts CHANGED
@@ -195,6 +195,15 @@ interface SaveLearningResult {
195
195
  reason: string;
196
196
  }>;
197
197
  }
198
+ interface AutoRelateConfig {
199
+ enabled?: boolean;
200
+ minSharedTags?: number;
201
+ minWeight?: number;
202
+ maxCandidates?: number;
203
+ sameKind?: boolean;
204
+ samePolarity?: boolean;
205
+ allowedKinds?: MemoryKind[];
206
+ }
198
207
  interface MemoryServiceConfig {
199
208
  neo4j: {
200
209
  uri: string;
@@ -205,6 +214,7 @@ interface MemoryServiceConfig {
205
214
  vectorIndex?: string;
206
215
  fulltextIndex?: string;
207
216
  halfLifeSeconds?: number;
217
+ autoRelate?: AutoRelateConfig;
208
218
  onMemoryEvent?: (event: MemoryEvent) => void;
209
219
  }
210
220
  type MemoryToolName = "store_skill" | "store_pattern" | "store_concept" | "relate_concepts" | "recall_skills" | "recall_concepts" | "recall_patterns";
@@ -222,6 +232,7 @@ declare class MemoryService {
222
232
  private fulltextIndex;
223
233
  private halfLifeSeconds;
224
234
  private onMemoryEvent?;
235
+ private autoRelateConfig;
225
236
  private cyUpsertMemory;
226
237
  private cyUpsertCase;
227
238
  private cyRetrieveBundle;
@@ -229,6 +240,7 @@ declare class MemoryService {
229
240
  private cyFeedbackCoUsed;
230
241
  private cyListMemories;
231
242
  private cyRelateConcepts;
243
+ private cyAutoRelateByTags;
232
244
  private cyGetRecallEdges;
233
245
  constructor(cfg: MemoryServiceConfig);
234
246
  init(): Promise<void>;
@@ -307,6 +319,7 @@ declare const cypher: {
307
319
  feedbackCoUsed: string;
308
320
  listMemories: string;
309
321
  relateConcepts: string;
322
+ autoRelateByTags: string;
310
323
  };
311
324
 
312
325
  declare function createMemoryTools(service: MemoryService): MemoryToolSet;
@@ -317,4 +330,4 @@ declare function newId(prefix: string): string;
317
330
  declare function normaliseSymptom(s: string): string;
318
331
  declare function envHash(env: EnvironmentFingerprint): string;
319
332
 
320
- export { type BetaEdge, type CaptureEpisodeArgs, type CaptureStepEpisodeArgs, type CaseRecord, type ContextBundle, type ContextMemoryBase, type ContextMemorySummary, type DistilledInvariant, type EnvironmentFingerprint, type FeedbackMetrics, type LearningCandidate, type ListMemoriesArgs, type MemoryEvent, type MemoryFeedback, type MemoryKind, type MemoryPolarity, type MemoryRecord, MemoryService, type MemoryServiceConfig, type MemorySummary, type MemoryToolDefinition, type MemoryToolName, type MemoryToolSet, Neo4jClient, type Neo4jClientConfig, type RetrieveContextArgs, type SaveLearningRequest, type SaveLearningResult, canonicaliseForHash, createMemoryService, createMemoryTools, cypher, ensureSchema, envHash, loadCypher, migrate, newId, normaliseSymptom, schemaVersion, sha256Hex };
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 };
package/dist/index.js CHANGED
@@ -35,7 +35,8 @@ var cypher = {
35
35
  feedbackBatch: loadCypher("feedback_batch.cypher"),
36
36
  feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
37
37
  listMemories: loadCypher("list_memories.cypher"),
38
- relateConcepts: loadCypher("relate_concepts.cypher")
38
+ relateConcepts: loadCypher("relate_concepts.cypher"),
39
+ autoRelateByTags: loadCypher("auto_relate_memory_by_tags.cypher")
39
40
  };
40
41
 
41
42
  // src/neo4j/schema.ts
@@ -95,6 +96,17 @@ function envHash(env) {
95
96
  function clamp01(x) {
96
97
  return Math.max(0, Math.min(1, x));
97
98
  }
99
+ var DEFAULT_AUTO_RELATE = {
100
+ enabled: true,
101
+ minSharedTags: 2,
102
+ minWeight: 0.2,
103
+ maxCandidates: 12,
104
+ sameKind: true,
105
+ samePolarity: true,
106
+ allowedKinds: ["semantic", "procedural"]
107
+ };
108
+ var AUTO_RELATE_MIN_SHARED_TAGS = 1;
109
+ var AUTO_RELATE_MIN_MAX_CANDIDATES = 1;
98
110
  function toBetaEdge(raw) {
99
111
  const aMin = 1e-3;
100
112
  const bMin = 1e-3;
@@ -172,6 +184,7 @@ var MemoryService = class {
172
184
  fulltextIndex;
173
185
  halfLifeSeconds;
174
186
  onMemoryEvent;
187
+ autoRelateConfig;
175
188
  cyUpsertMemory = cypher.upsertMemory;
176
189
  cyUpsertCase = cypher.upsertCase;
177
190
  cyRetrieveBundle = cypher.retrieveContextBundle;
@@ -179,6 +192,7 @@ var MemoryService = class {
179
192
  cyFeedbackCoUsed = cypher.feedbackCoUsed;
180
193
  cyListMemories = cypher.listMemories;
181
194
  cyRelateConcepts = cypher.relateConcepts;
195
+ cyAutoRelateByTags = cypher.autoRelateByTags;
182
196
  cyGetRecallEdges = `
183
197
  UNWIND $ids AS id
184
198
  MATCH (m:Memory {id:id})
@@ -196,6 +210,22 @@ var MemoryService = class {
196
210
  this.fulltextIndex = cfg.fulltextIndex ?? "memoryText";
197
211
  this.halfLifeSeconds = cfg.halfLifeSeconds ?? 30 * 24 * 3600;
198
212
  this.onMemoryEvent = cfg.onMemoryEvent;
213
+ const autoRelate = cfg.autoRelate ?? {};
214
+ this.autoRelateConfig = {
215
+ enabled: autoRelate.enabled ?? DEFAULT_AUTO_RELATE.enabled,
216
+ minSharedTags: Math.max(
217
+ AUTO_RELATE_MIN_SHARED_TAGS,
218
+ Math.floor(autoRelate.minSharedTags ?? DEFAULT_AUTO_RELATE.minSharedTags)
219
+ ),
220
+ minWeight: clamp01(autoRelate.minWeight ?? DEFAULT_AUTO_RELATE.minWeight),
221
+ maxCandidates: Math.max(
222
+ AUTO_RELATE_MIN_MAX_CANDIDATES,
223
+ Math.floor(autoRelate.maxCandidates ?? DEFAULT_AUTO_RELATE.maxCandidates)
224
+ ),
225
+ sameKind: autoRelate.sameKind ?? DEFAULT_AUTO_RELATE.sameKind,
226
+ samePolarity: autoRelate.samePolarity ?? DEFAULT_AUTO_RELATE.samePolarity,
227
+ allowedKinds: autoRelate.allowedKinds ?? [...DEFAULT_AUTO_RELATE.allowedKinds]
228
+ };
199
229
  }
200
230
  async init() {
201
231
  await ensureSchema(this.client);
@@ -280,6 +310,21 @@ var MemoryService = class {
280
310
  }
281
311
  );
282
312
  }
313
+ const autoRelate = this.autoRelateConfig;
314
+ const allowedKinds = autoRelate.allowedKinds ?? [];
315
+ const canAutoRelate = autoRelate.enabled && tags.length >= autoRelate.minSharedTags && (allowedKinds.length === 0 || allowedKinds.includes(l.kind));
316
+ if (canAutoRelate) {
317
+ await write.run(this.cyAutoRelateByTags, {
318
+ id,
319
+ nowIso: (/* @__PURE__ */ new Date()).toISOString(),
320
+ minSharedTags: autoRelate.minSharedTags,
321
+ minWeight: autoRelate.minWeight,
322
+ maxCandidates: autoRelate.maxCandidates,
323
+ sameKind: autoRelate.sameKind,
324
+ samePolarity: autoRelate.samePolarity,
325
+ allowedKinds
326
+ });
327
+ }
283
328
  this.emit({ type: "write", action: "upsertMemory", meta: { id } });
284
329
  return { id, deduped: false };
285
330
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo4j-agent-memory",
3
- "version": "0.3.17",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",