convex-agent-knowledge 0.0.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.
Files changed (97) hide show
  1. package/LICENSE +73 -0
  2. package/README.md +156 -0
  3. package/dist/client/chunking.d.ts +7 -0
  4. package/dist/client/chunking.d.ts.map +1 -0
  5. package/dist/client/chunking.js +36 -0
  6. package/dist/client/chunking.js.map +1 -0
  7. package/dist/client/extraction.d.ts +9 -0
  8. package/dist/client/extraction.d.ts.map +1 -0
  9. package/dist/client/extraction.js +88 -0
  10. package/dist/client/extraction.js.map +1 -0
  11. package/dist/client/hash.d.ts +3 -0
  12. package/dist/client/hash.d.ts.map +1 -0
  13. package/dist/client/hash.js +17 -0
  14. package/dist/client/hash.js.map +1 -0
  15. package/dist/client/index.d.ts +99 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +194 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/client/types.d.ts +126 -0
  20. package/dist/client/types.d.ts.map +1 -0
  21. package/dist/client/types.js +2 -0
  22. package/dist/client/types.js.map +1 -0
  23. package/dist/component/_generated/api.d.ts +40 -0
  24. package/dist/component/_generated/api.d.ts.map +1 -0
  25. package/dist/component/_generated/api.js +31 -0
  26. package/dist/component/_generated/api.js.map +1 -0
  27. package/dist/component/_generated/component.d.ts +277 -0
  28. package/dist/component/_generated/component.d.ts.map +1 -0
  29. package/dist/component/_generated/component.js +11 -0
  30. package/dist/component/_generated/component.js.map +1 -0
  31. package/dist/component/_generated/dataModel.d.ts +46 -0
  32. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  33. package/dist/component/_generated/dataModel.js +11 -0
  34. package/dist/component/_generated/dataModel.js.map +1 -0
  35. package/dist/component/_generated/server.d.ts +121 -0
  36. package/dist/component/_generated/server.d.ts.map +1 -0
  37. package/dist/component/_generated/server.js +78 -0
  38. package/dist/component/_generated/server.js.map +1 -0
  39. package/dist/component/actions.d.ts +12 -0
  40. package/dist/component/actions.d.ts.map +1 -0
  41. package/dist/component/actions.js +42 -0
  42. package/dist/component/actions.js.map +1 -0
  43. package/dist/component/convex.config.d.ts +3 -0
  44. package/dist/component/convex.config.d.ts.map +1 -0
  45. package/dist/component/convex.config.js +4 -0
  46. package/dist/component/convex.config.js.map +1 -0
  47. package/dist/component/mutations.d.ts +85 -0
  48. package/dist/component/mutations.d.ts.map +1 -0
  49. package/dist/component/mutations.js +407 -0
  50. package/dist/component/mutations.js.map +1 -0
  51. package/dist/component/queries.d.ts +95 -0
  52. package/dist/component/queries.d.ts.map +1 -0
  53. package/dist/component/queries.js +181 -0
  54. package/dist/component/queries.js.map +1 -0
  55. package/dist/component/schema.d.ts +442 -0
  56. package/dist/component/schema.d.ts.map +1 -0
  57. package/dist/component/schema.js +140 -0
  58. package/dist/component/schema.js.map +1 -0
  59. package/dist/component/validators.d.ts +158 -0
  60. package/dist/component/validators.d.ts.map +1 -0
  61. package/dist/component/validators.js +90 -0
  62. package/dist/component/validators.js.map +1 -0
  63. package/dist/node/index.d.ts +3 -0
  64. package/dist/node/index.d.ts.map +1 -0
  65. package/dist/node/index.js +2 -0
  66. package/dist/node/index.js.map +1 -0
  67. package/dist/node/neo4j.d.ts +5 -0
  68. package/dist/node/neo4j.d.ts.map +1 -0
  69. package/dist/node/neo4j.js +147 -0
  70. package/dist/node/neo4j.js.map +1 -0
  71. package/dist/shared/ranking.d.ts +17 -0
  72. package/dist/shared/ranking.d.ts.map +1 -0
  73. package/dist/shared/ranking.js +44 -0
  74. package/dist/shared/ranking.js.map +1 -0
  75. package/package.json +84 -0
  76. package/src/client/chunking.test.ts +25 -0
  77. package/src/client/chunking.ts +45 -0
  78. package/src/client/extraction.test.ts +15 -0
  79. package/src/client/extraction.ts +108 -0
  80. package/src/client/hash.test.ts +17 -0
  81. package/src/client/hash.ts +17 -0
  82. package/src/client/index.ts +307 -0
  83. package/src/client/types.ts +141 -0
  84. package/src/component/_generated/api.ts +56 -0
  85. package/src/component/_generated/component.ts +310 -0
  86. package/src/component/_generated/dataModel.ts +60 -0
  87. package/src/component/_generated/server.ts +156 -0
  88. package/src/component/actions.ts +47 -0
  89. package/src/component/convex.config.ts +5 -0
  90. package/src/component/mutations.ts +445 -0
  91. package/src/component/queries.ts +202 -0
  92. package/src/component/schema.ts +161 -0
  93. package/src/component/validators.ts +104 -0
  94. package/src/node/index.ts +2 -0
  95. package/src/node/neo4j.ts +210 -0
  96. package/src/shared/ranking.test.ts +30 -0
  97. package/src/shared/ranking.ts +64 -0
@@ -0,0 +1,202 @@
1
+ // @ts-nocheck
2
+ import { v } from "convex/values";
3
+ import { internalQuery, query } from "./_generated/server.js";
4
+ import { memoryCardValidator, vectorTableForDimension } from "./validators.js";
5
+
6
+ type QueryCtx = any;
7
+
8
+ async function getActiveMemory(ctx: QueryCtx, memoryId: string) {
9
+ const id = ctx.db.normalizeId("memories", memoryId);
10
+ if (!id) {
11
+ return null;
12
+ }
13
+ const memory = await ctx.db.get(id);
14
+ if (!memory || memory.status !== "active") {
15
+ return null;
16
+ }
17
+ return memory;
18
+ }
19
+
20
+ async function buildMemoryCard(
21
+ ctx: QueryCtx,
22
+ memoryId: string,
23
+ score: number,
24
+ scores?: { semanticScore?: number; graphScore?: number },
25
+ ) {
26
+ const memory = await getActiveMemory(ctx, memoryId);
27
+ if (!memory) {
28
+ return null;
29
+ }
30
+ const [entities, relationships] = await Promise.all([
31
+ ctx.db
32
+ .query("entities")
33
+ .withIndex("by_memory", (q) => q.eq("memoryId", memory._id))
34
+ .collect(),
35
+ ctx.db
36
+ .query("relationships")
37
+ .withIndex("by_memory", (q) => q.eq("memoryId", memory._id))
38
+ .collect(),
39
+ ]);
40
+
41
+ return {
42
+ memoryId: memory._id,
43
+ namespace: memory.namespace,
44
+ ...(memory.key === undefined ? {} : { key: memory.key }),
45
+ ...(memory.agentId === undefined ? {} : { agentId: memory.agentId }),
46
+ text: memory.text,
47
+ score,
48
+ ...(scores?.semanticScore === undefined ? {} : { semanticScore: scores.semanticScore }),
49
+ ...(scores?.graphScore === undefined ? {} : { graphScore: scores.graphScore }),
50
+ importance: memory.importance,
51
+ ...(memory.source === undefined ? {} : { source: memory.source }),
52
+ ...(memory.metadata === undefined ? {} : { metadata: memory.metadata }),
53
+ entities: entities.map((entity) => ({
54
+ externalId: entity.externalId,
55
+ name: entity.name,
56
+ type: entity.type,
57
+ ...(entity.description === undefined ? {} : { description: entity.description }),
58
+ confidence: entity.confidence,
59
+ })),
60
+ relationships: relationships.map((relationship) => ({
61
+ fromEntityExternalId: relationship.fromEntityExternalId,
62
+ toEntityExternalId: relationship.toEntityExternalId,
63
+ type: relationship.type,
64
+ ...(relationship.description === undefined ? {} : { description: relationship.description }),
65
+ confidence: relationship.confidence,
66
+ weight: relationship.weight,
67
+ })),
68
+ };
69
+ }
70
+
71
+ export const getMemory = query({
72
+ args: {
73
+ memoryId: v.string(),
74
+ },
75
+ returns: v.nullable(memoryCardValidator),
76
+ handler: async (ctx, args) => {
77
+ return await buildMemoryCard(ctx, args.memoryId, 1);
78
+ },
79
+ });
80
+
81
+ export const listMemories = query({
82
+ args: {
83
+ namespace: v.string(),
84
+ limit: v.optional(v.number()),
85
+ cursor: v.optional(v.string()),
86
+ },
87
+ returns: v.object({
88
+ page: v.array(memoryCardValidator),
89
+ continueCursor: v.union(v.string(), v.null()),
90
+ isDone: v.boolean(),
91
+ }),
92
+ handler: async (ctx, args) => {
93
+ const page = await ctx.db
94
+ .query("memories")
95
+ .withIndex("by_namespace", (q) => q.eq("namespace", args.namespace))
96
+ .filter((q) => q.eq(q.field("status"), "active"))
97
+ .order("desc")
98
+ .paginate({
99
+ cursor: args.cursor ?? null,
100
+ numItems: args.limit ?? 25,
101
+ });
102
+
103
+ const cards = [];
104
+ for (const memory of page.page) {
105
+ const card = await buildMemoryCard(ctx, memory._id, memory.importance);
106
+ if (card) {
107
+ cards.push(card);
108
+ }
109
+ }
110
+ return { ...page, page: cards };
111
+ },
112
+ });
113
+
114
+ export const fetchMemoryCards = query({
115
+ args: {
116
+ matches: v.array(
117
+ v.object({
118
+ memoryId: v.string(),
119
+ score: v.number(),
120
+ semanticScore: v.optional(v.number()),
121
+ graphScore: v.optional(v.number()),
122
+ }),
123
+ ),
124
+ },
125
+ returns: v.array(memoryCardValidator),
126
+ handler: async (ctx, args) => {
127
+ const cards = [];
128
+ for (const match of args.matches) {
129
+ const card = await buildMemoryCard(ctx, match.memoryId, match.score, {
130
+ semanticScore: match.semanticScore,
131
+ graphScore: match.graphScore,
132
+ });
133
+ if (card) {
134
+ cards.push(card);
135
+ }
136
+ }
137
+ return cards;
138
+ },
139
+ });
140
+
141
+ export const fetchMemoryCardsByVectorMatches = internalQuery({
142
+ args: {
143
+ embeddingDimension: v.number(),
144
+ matches: v.array(
145
+ v.object({
146
+ vectorId: v.string(),
147
+ score: v.number(),
148
+ }),
149
+ ),
150
+ },
151
+ returns: v.array(memoryCardValidator),
152
+ handler: async (ctx, args) => {
153
+ const table = vectorTableForDimension(args.embeddingDimension);
154
+ const cards = [];
155
+ for (const match of args.matches) {
156
+ const vectorId = ctx.db.normalizeId(table, match.vectorId);
157
+ if (!vectorId) {
158
+ continue;
159
+ }
160
+ const vectorRow = await ctx.db.get(vectorId);
161
+ if (!vectorRow) {
162
+ continue;
163
+ }
164
+ const card = await buildMemoryCard(ctx, vectorRow.memoryId, match.score, {
165
+ semanticScore: match.score,
166
+ });
167
+ if (card) {
168
+ cards.push(card);
169
+ }
170
+ }
171
+ return cards;
172
+ },
173
+ });
174
+
175
+ export const listPendingGraphSyncJobs = query({
176
+ args: {
177
+ limit: v.number(),
178
+ },
179
+ returns: v.array(
180
+ v.object({
181
+ jobId: v.string(),
182
+ namespace: v.string(),
183
+ operation: v.string(),
184
+ attempts: v.number(),
185
+ payload: v.any(),
186
+ }),
187
+ ),
188
+ handler: async (ctx, args) => {
189
+ const jobs = await ctx.db
190
+ .query("graphSyncJobs")
191
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
192
+ .order("asc")
193
+ .take(args.limit);
194
+ return jobs.map((job) => ({
195
+ jobId: job._id,
196
+ namespace: job.namespace,
197
+ operation: job.operation,
198
+ attempts: job.attempts,
199
+ payload: job.payload,
200
+ }));
201
+ },
202
+ });
@@ -0,0 +1,161 @@
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+
4
+ const sourceValidator = v.object({
5
+ type: v.string(),
6
+ id: v.optional(v.string()),
7
+ url: v.optional(v.string()),
8
+ title: v.optional(v.string()),
9
+ });
10
+
11
+ const memoryStatus = v.union(v.literal("active"), v.literal("deleted"), v.literal("pending"));
12
+
13
+ const syncStatus = v.union(
14
+ v.literal("pending"),
15
+ v.literal("running"),
16
+ v.literal("succeeded"),
17
+ v.literal("failed"),
18
+ );
19
+
20
+ const vectorRowFields = {
21
+ namespace: v.string(),
22
+ memoryId: v.id("memories"),
23
+ chunkId: v.id("chunks"),
24
+ agentId: v.string(),
25
+ kind: v.string(),
26
+ embedding: v.array(v.float64()),
27
+ };
28
+
29
+ function vectorTable(dimensions: number) {
30
+ return defineTable(vectorRowFields)
31
+ .index("by_memory", ["memoryId"])
32
+ .index("by_chunk", ["chunkId"])
33
+ .vectorIndex("by_embedding", {
34
+ vectorField: "embedding",
35
+ dimensions,
36
+ filterFields: ["namespace", "agentId", "kind"],
37
+ });
38
+ }
39
+
40
+ export default defineSchema({
41
+ namespaces: defineTable({
42
+ namespace: v.string(),
43
+ metadata: v.optional(v.any()),
44
+ createdAt: v.number(),
45
+ updatedAt: v.number(),
46
+ }).index("by_namespace", ["namespace"]),
47
+
48
+ memories: defineTable({
49
+ namespace: v.string(),
50
+ key: v.optional(v.string()),
51
+ agentId: v.optional(v.string()),
52
+ text: v.string(),
53
+ contentHash: v.string(),
54
+ source: v.optional(sourceValidator),
55
+ metadata: v.optional(v.any()),
56
+ status: memoryStatus,
57
+ importance: v.number(),
58
+ observationScore: v.number(),
59
+ embeddingDimension: v.number(),
60
+ chunkCount: v.number(),
61
+ entityCount: v.number(),
62
+ relationshipCount: v.number(),
63
+ createdAt: v.number(),
64
+ updatedAt: v.number(),
65
+ deletedAt: v.optional(v.number()),
66
+ })
67
+ .index("by_namespace", ["namespace"])
68
+ .index("by_namespace_key", ["namespace", "key"])
69
+ .index("by_namespace_hash", ["namespace", "contentHash"])
70
+ .index("by_agent", ["namespace", "agentId"]),
71
+
72
+ chunks: defineTable({
73
+ namespace: v.string(),
74
+ memoryId: v.id("memories"),
75
+ agentId: v.optional(v.string()),
76
+ order: v.number(),
77
+ text: v.string(),
78
+ summary: v.optional(v.string()),
79
+ tokenCount: v.optional(v.number()),
80
+ metadata: v.optional(v.any()),
81
+ createdAt: v.number(),
82
+ })
83
+ .index("by_memory", ["memoryId", "order"])
84
+ .index("by_namespace", ["namespace"]),
85
+
86
+ entities: defineTable({
87
+ namespace: v.string(),
88
+ memoryId: v.id("memories"),
89
+ chunkId: v.optional(v.id("chunks")),
90
+ externalId: v.string(),
91
+ name: v.string(),
92
+ type: v.string(),
93
+ description: v.optional(v.string()),
94
+ aliases: v.optional(v.array(v.string())),
95
+ confidence: v.number(),
96
+ metadata: v.optional(v.any()),
97
+ createdAt: v.number(),
98
+ updatedAt: v.number(),
99
+ })
100
+ .index("by_memory", ["memoryId"])
101
+ .index("by_namespace_name", ["namespace", "name"])
102
+ .index("by_external_id", ["namespace", "externalId"]),
103
+
104
+ relationships: defineTable({
105
+ namespace: v.string(),
106
+ memoryId: v.id("memories"),
107
+ fromEntityExternalId: v.string(),
108
+ toEntityExternalId: v.string(),
109
+ type: v.string(),
110
+ description: v.optional(v.string()),
111
+ confidence: v.number(),
112
+ weight: v.number(),
113
+ metadata: v.optional(v.any()),
114
+ createdAt: v.number(),
115
+ updatedAt: v.number(),
116
+ })
117
+ .index("by_memory", ["memoryId"])
118
+ .index("by_from", ["namespace", "fromEntityExternalId"])
119
+ .index("by_to", ["namespace", "toEntityExternalId"]),
120
+
121
+ observations: defineTable({
122
+ namespace: v.string(),
123
+ memoryId: v.id("memories"),
124
+ query: v.string(),
125
+ outcome: v.union(v.literal("helpful"), v.literal("not_helpful"), v.literal("neutral")),
126
+ feedback: v.optional(v.string()),
127
+ metadata: v.optional(v.any()),
128
+ createdAt: v.number(),
129
+ })
130
+ .index("by_memory", ["memoryId"])
131
+ .index("by_namespace", ["namespace"]),
132
+
133
+ graphSyncJobs: defineTable({
134
+ namespace: v.string(),
135
+ memoryId: v.optional(v.id("memories")),
136
+ operation: v.union(
137
+ v.literal("upsert_memory"),
138
+ v.literal("delete_memory"),
139
+ v.literal("promote_memory"),
140
+ ),
141
+ status: syncStatus,
142
+ attempts: v.number(),
143
+ payload: v.any(),
144
+ lastError: v.optional(v.string()),
145
+ createdAt: v.number(),
146
+ updatedAt: v.number(),
147
+ })
148
+ .index("by_status", ["status", "createdAt"])
149
+ .index("by_memory", ["memoryId"]),
150
+
151
+ vectors_128: vectorTable(128),
152
+ vectors_256: vectorTable(256),
153
+ vectors_384: vectorTable(384),
154
+ vectors_512: vectorTable(512),
155
+ vectors_768: vectorTable(768),
156
+ vectors_1024: vectorTable(1024),
157
+ vectors_1536: vectorTable(1536),
158
+ vectors_2048: vectorTable(2048),
159
+ vectors_3072: vectorTable(3072),
160
+ vectors_4096: vectorTable(4096),
161
+ });
@@ -0,0 +1,104 @@
1
+ import { v } from "convex/values";
2
+
3
+ export const supportedEmbeddingDimensions = [
4
+ 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096,
5
+ ] as const;
6
+
7
+ export type SupportedEmbeddingDimension = (typeof supportedEmbeddingDimensions)[number];
8
+
9
+ export const sourceValidator = v.object({
10
+ type: v.string(),
11
+ id: v.optional(v.string()),
12
+ url: v.optional(v.string()),
13
+ title: v.optional(v.string()),
14
+ });
15
+
16
+ export const entityInputValidator = v.object({
17
+ externalId: v.string(),
18
+ name: v.string(),
19
+ type: v.string(),
20
+ description: v.optional(v.string()),
21
+ aliases: v.optional(v.array(v.string())),
22
+ confidence: v.optional(v.number()),
23
+ metadata: v.optional(v.any()),
24
+ });
25
+
26
+ export const relationshipInputValidator = v.object({
27
+ fromEntityExternalId: v.string(),
28
+ toEntityExternalId: v.string(),
29
+ type: v.string(),
30
+ description: v.optional(v.string()),
31
+ confidence: v.optional(v.number()),
32
+ weight: v.optional(v.number()),
33
+ metadata: v.optional(v.any()),
34
+ });
35
+
36
+ export const chunkInputValidator = v.object({
37
+ text: v.string(),
38
+ embedding: v.array(v.float64()),
39
+ summary: v.optional(v.string()),
40
+ tokenCount: v.optional(v.number()),
41
+ metadata: v.optional(v.any()),
42
+ });
43
+
44
+ export const memoryCardValidator = v.object({
45
+ memoryId: v.string(),
46
+ namespace: v.string(),
47
+ key: v.optional(v.string()),
48
+ agentId: v.optional(v.string()),
49
+ text: v.string(),
50
+ score: v.number(),
51
+ semanticScore: v.optional(v.number()),
52
+ graphScore: v.optional(v.number()),
53
+ importance: v.number(),
54
+ source: v.optional(sourceValidator),
55
+ metadata: v.optional(v.any()),
56
+ entities: v.array(
57
+ v.object({
58
+ externalId: v.string(),
59
+ name: v.string(),
60
+ type: v.string(),
61
+ description: v.optional(v.string()),
62
+ confidence: v.number(),
63
+ }),
64
+ ),
65
+ relationships: v.array(
66
+ v.object({
67
+ fromEntityExternalId: v.string(),
68
+ toEntityExternalId: v.string(),
69
+ type: v.string(),
70
+ description: v.optional(v.string()),
71
+ confidence: v.number(),
72
+ weight: v.number(),
73
+ }),
74
+ ),
75
+ });
76
+
77
+ export function vectorTableForDimension(dimension: number) {
78
+ switch (dimension) {
79
+ case 128:
80
+ return "vectors_128";
81
+ case 256:
82
+ return "vectors_256";
83
+ case 384:
84
+ return "vectors_384";
85
+ case 512:
86
+ return "vectors_512";
87
+ case 768:
88
+ return "vectors_768";
89
+ case 1024:
90
+ return "vectors_1024";
91
+ case 1536:
92
+ return "vectors_1536";
93
+ case 2048:
94
+ return "vectors_2048";
95
+ case 3072:
96
+ return "vectors_3072";
97
+ case 4096:
98
+ return "vectors_4096";
99
+ default:
100
+ throw new Error(
101
+ `Unsupported embedding dimension ${dimension}. Supported dimensions: ${supportedEmbeddingDimensions.join(", ")}`,
102
+ );
103
+ }
104
+ }
@@ -0,0 +1,2 @@
1
+ export { createNeo4jGraphStore } from "./neo4j.js";
2
+ export type { Neo4jGraphStoreOptions } from "./neo4j.js";
@@ -0,0 +1,210 @@
1
+ import neo4j from "neo4j-driver";
2
+ import type {
3
+ GraphExpandInput,
4
+ GraphMemoryScore,
5
+ GraphStore,
6
+ GraphSyncJob,
7
+ Neo4jConfig,
8
+ } from "../client/types.js";
9
+
10
+ export type Neo4jGraphStoreOptions = Neo4jConfig;
11
+
12
+ export function createNeo4jGraphStore(options: Neo4jGraphStoreOptions): GraphStore {
13
+ return {
14
+ async syncJob(job) {
15
+ await withNeo4j(options, async (session) => {
16
+ if (job.operation === "upsert_memory") {
17
+ await upsertMemoryGraph(session, job.payload);
18
+ } else if (job.operation === "delete_memory") {
19
+ await deleteMemoryGraph(session, job.payload);
20
+ } else if (job.operation === "promote_memory") {
21
+ await promoteMemoryGraph(session, job.payload);
22
+ } else {
23
+ throw new Error(`Unsupported graph sync operation ${job.operation}`);
24
+ }
25
+ });
26
+ },
27
+ async expand(input) {
28
+ return await expandGraph(options, input);
29
+ },
30
+ };
31
+ }
32
+
33
+ async function withNeo4j<T>(config: Neo4jConfig, fn: (session: neo4j.Session) => Promise<T>) {
34
+ const driver = neo4j.driver(config.uri, neo4j.auth.basic(config.user, config.password));
35
+ const session = driver.session(config.database ? { database: config.database } : undefined);
36
+ try {
37
+ return await fn(session);
38
+ } finally {
39
+ await session.close();
40
+ await driver.close();
41
+ }
42
+ }
43
+
44
+ async function upsertMemoryGraph(session: neo4j.Session, payload: unknown) {
45
+ const graphPayload = payload as {
46
+ memory: Record<string, unknown> & {
47
+ id: string;
48
+ namespace: string;
49
+ importance?: number;
50
+ };
51
+ entities?: Array<Record<string, unknown>>;
52
+ relationships?: Array<Record<string, unknown>>;
53
+ };
54
+ await session.executeWrite((tx) =>
55
+ tx.run(
56
+ `
57
+ MERGE (m:Memory {id: $memory.id})
58
+ SET m.namespace = $memory.namespace,
59
+ m.key = $memory.key,
60
+ m.agentId = $memory.agentId,
61
+ m.text = $memory.text,
62
+ m.importance = $memory.importance,
63
+ m.updatedAt = timestamp()
64
+ WITH m
65
+ UNWIND $entities AS entity
66
+ MERGE (e:Entity {id: entity.externalId})
67
+ SET e.namespace = $memory.namespace,
68
+ e.name = entity.name,
69
+ e.type = entity.type,
70
+ e.description = entity.description,
71
+ e.confidence = coalesce(entity.confidence, 0.75),
72
+ e.updatedAt = timestamp()
73
+ MERGE (m)-[mention:MENTIONS]->(e)
74
+ SET mention.namespace = $memory.namespace,
75
+ mention.updatedAt = timestamp()
76
+ `,
77
+ {
78
+ memory: graphPayload.memory,
79
+ entities: graphPayload.entities ?? [],
80
+ },
81
+ ),
82
+ );
83
+
84
+ await session.executeWrite((tx) =>
85
+ tx.run(
86
+ `
87
+ UNWIND $relationships AS relationship
88
+ MATCH (from:Entity {id: relationship.fromEntityExternalId})
89
+ MATCH (to:Entity {id: relationship.toEntityExternalId})
90
+ MERGE (from)-[r:RELATED {namespace: $namespace, type: relationship.type}]->(to)
91
+ SET r.description = relationship.description,
92
+ r.confidence = coalesce(relationship.confidence, 0.75),
93
+ r.weight = coalesce(relationship.weight, 0.5),
94
+ r.memoryId = $memoryId,
95
+ r.updatedAt = timestamp()
96
+ `,
97
+ {
98
+ namespace: graphPayload.memory.namespace,
99
+ memoryId: graphPayload.memory.id,
100
+ relationships: graphPayload.relationships ?? [],
101
+ },
102
+ ),
103
+ );
104
+ }
105
+
106
+ async function deleteMemoryGraph(session: neo4j.Session, payload: unknown) {
107
+ const graphPayload = payload as { memoryId: string; namespace: string };
108
+ await session.executeWrite((tx) =>
109
+ tx.run(
110
+ `
111
+ MATCH (m:Memory {id: $memoryId, namespace: $namespace})
112
+ DETACH DELETE m
113
+ `,
114
+ graphPayload,
115
+ ),
116
+ );
117
+ }
118
+
119
+ async function promoteMemoryGraph(session: neo4j.Session, payload: unknown) {
120
+ const graphPayload = payload as {
121
+ memoryId: string;
122
+ namespace: string;
123
+ importance: number;
124
+ observationScore: number;
125
+ };
126
+ await session.executeWrite((tx) =>
127
+ tx.run(
128
+ `
129
+ MATCH (m:Memory {id: $memoryId, namespace: $namespace})
130
+ SET m.importance = $importance,
131
+ m.observationScore = $observationScore,
132
+ m.updatedAt = timestamp()
133
+ `,
134
+ graphPayload,
135
+ ),
136
+ );
137
+ }
138
+
139
+ async function expandGraph(
140
+ config: Neo4jConfig,
141
+ input: GraphExpandInput,
142
+ ): Promise<GraphMemoryScore[]> {
143
+ const seedMemoryIds = input.seedMemoryIds;
144
+ const entityHints = input.entityHints ?? [];
145
+ if (seedMemoryIds.length === 0 && entityHints.length === 0) {
146
+ return [];
147
+ }
148
+ const safeHops = Math.min(Math.max(Math.trunc(input.hops ?? 2), 1), 4);
149
+ const limit = Math.min(Math.max(input.limit ?? 32, 1), 128);
150
+ return await withNeo4j(config, async (session) => {
151
+ const result =
152
+ seedMemoryIds.length > 0
153
+ ? await session.executeRead((tx) =>
154
+ tx.run(
155
+ `
156
+ MATCH (seed:Memory {namespace: $namespace})
157
+ WHERE seed.id IN $seedMemoryIds
158
+ MATCH path = (seed)-[:MENTIONS]->(:Entity)-[:RELATED*1..${safeHops}]-(:Entity)<-[:MENTIONS]-(related:Memory {namespace: $namespace})
159
+ WHERE NOT related.id IN $seedMemoryIds
160
+ RETURN related.id AS memoryId, count(path) AS graphScore
161
+ ORDER BY graphScore DESC
162
+ LIMIT $limit
163
+ `,
164
+ {
165
+ namespace: input.namespace,
166
+ seedMemoryIds,
167
+ limit: neo4j.int(limit),
168
+ },
169
+ ),
170
+ )
171
+ : await session.executeRead((tx) =>
172
+ tx.run(
173
+ `
174
+ MATCH (seed:Entity {namespace: $namespace})
175
+ WHERE toLower(seed.name) IN $entityHints
176
+ MATCH path = (seed)-[:RELATED*0..${safeHops}]-(:Entity)<-[:MENTIONS]-(related:Memory {namespace: $namespace})
177
+ RETURN related.id AS memoryId, count(path) AS graphScore
178
+ ORDER BY graphScore DESC
179
+ LIMIT $limit
180
+ `,
181
+ {
182
+ namespace: input.namespace,
183
+ entityHints: entityHints.map((hint) => hint.toLowerCase()),
184
+ limit: neo4j.int(limit),
185
+ },
186
+ ),
187
+ );
188
+ return result.records.map((record) => ({
189
+ memoryId: record.get("memoryId") as string,
190
+ graphScore: toNumber(record.get("graphScore")),
191
+ }));
192
+ });
193
+ }
194
+
195
+ function toNumber(value: unknown) {
196
+ if (typeof value === "number") {
197
+ return value;
198
+ }
199
+ if (
200
+ value &&
201
+ typeof value === "object" &&
202
+ "toNumber" in value &&
203
+ typeof value.toNumber === "function"
204
+ ) {
205
+ return value.toNumber();
206
+ }
207
+ return Number(value);
208
+ }
209
+
210
+ export type { GraphStore, GraphSyncJob };
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { clamp, fuseMemoryScores } from "./ranking.js";
3
+
4
+ describe("clamp", () => {
5
+ it("limits values to the target range", () => {
6
+ expect(clamp(-1)).toBe(0);
7
+ expect(clamp(0.5)).toBe(0.5);
8
+ expect(clamp(2)).toBe(1);
9
+ });
10
+ });
11
+
12
+ describe("fuseMemoryScores", () => {
13
+ it("merges semantic and graph scores by memory id", () => {
14
+ const results = fuseMemoryScores(
15
+ [{ memoryId: "a", score: 0.8, semanticScore: 0.8, importance: 0.2 }],
16
+ [
17
+ { memoryId: "a", score: 2, graphScore: 2, importance: 0.2 },
18
+ { memoryId: "b", score: 3, graphScore: 3, importance: 0.1 },
19
+ ],
20
+ { limit: 2 },
21
+ );
22
+
23
+ expect(results).toHaveLength(2);
24
+ expect(results[0]!.memoryId).toBe("a");
25
+ expect(results.find((result) => result.memoryId === "a")).toMatchObject({
26
+ semanticScore: 0.8,
27
+ graphScore: 2,
28
+ });
29
+ });
30
+ });