@waynesutton/agent-memory 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 (201) hide show
  1. package/.claude/settings.json +9 -0
  2. package/.claude/settings.local.json +7 -0
  3. package/AGENTS.md +113 -0
  4. package/CLAUDE.md +79 -0
  5. package/README.md +1 -0
  6. package/dist/cli/index.d.ts +3 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +192 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/parsers/claude-code.d.ts +3 -0
  11. package/dist/cli/parsers/claude-code.d.ts.map +1 -0
  12. package/dist/cli/parsers/claude-code.js +75 -0
  13. package/dist/cli/parsers/claude-code.js.map +1 -0
  14. package/dist/cli/parsers/codex.d.ts +3 -0
  15. package/dist/cli/parsers/codex.d.ts.map +1 -0
  16. package/dist/cli/parsers/codex.js +42 -0
  17. package/dist/cli/parsers/codex.js.map +1 -0
  18. package/dist/cli/parsers/conductor.d.ts +3 -0
  19. package/dist/cli/parsers/conductor.d.ts.map +1 -0
  20. package/dist/cli/parsers/conductor.js +43 -0
  21. package/dist/cli/parsers/conductor.js.map +1 -0
  22. package/dist/cli/parsers/cursor.d.ts +3 -0
  23. package/dist/cli/parsers/cursor.d.ts.map +1 -0
  24. package/dist/cli/parsers/cursor.js +50 -0
  25. package/dist/cli/parsers/cursor.js.map +1 -0
  26. package/dist/cli/parsers/index.d.ts +12 -0
  27. package/dist/cli/parsers/index.d.ts.map +1 -0
  28. package/dist/cli/parsers/index.js +27 -0
  29. package/dist/cli/parsers/index.js.map +1 -0
  30. package/dist/cli/parsers/opencode.d.ts +3 -0
  31. package/dist/cli/parsers/opencode.d.ts.map +1 -0
  32. package/dist/cli/parsers/opencode.js +72 -0
  33. package/dist/cli/parsers/opencode.js.map +1 -0
  34. package/dist/cli/parsers/parsers.test.d.ts +2 -0
  35. package/dist/cli/parsers/parsers.test.d.ts.map +1 -0
  36. package/dist/cli/parsers/parsers.test.js +151 -0
  37. package/dist/cli/parsers/parsers.test.js.map +1 -0
  38. package/dist/cli/parsers/pi.d.ts +3 -0
  39. package/dist/cli/parsers/pi.d.ts.map +1 -0
  40. package/dist/cli/parsers/pi.js +43 -0
  41. package/dist/cli/parsers/pi.js.map +1 -0
  42. package/dist/cli/parsers/types.d.ts +25 -0
  43. package/dist/cli/parsers/types.d.ts.map +1 -0
  44. package/dist/cli/parsers/types.js +2 -0
  45. package/dist/cli/parsers/types.js.map +1 -0
  46. package/dist/cli/parsers/vscode-copilot.d.ts +3 -0
  47. package/dist/cli/parsers/vscode-copilot.d.ts.map +1 -0
  48. package/dist/cli/parsers/vscode-copilot.js +69 -0
  49. package/dist/cli/parsers/vscode-copilot.js.map +1 -0
  50. package/dist/cli/parsers/zed.d.ts +3 -0
  51. package/dist/cli/parsers/zed.d.ts.map +1 -0
  52. package/dist/cli/parsers/zed.js +43 -0
  53. package/dist/cli/parsers/zed.js.map +1 -0
  54. package/dist/cli/sync.d.ts +21 -0
  55. package/dist/cli/sync.d.ts.map +1 -0
  56. package/dist/cli/sync.js +78 -0
  57. package/dist/cli/sync.js.map +1 -0
  58. package/dist/cli/type-extractor.d.ts +25 -0
  59. package/dist/cli/type-extractor.d.ts.map +1 -0
  60. package/dist/cli/type-extractor.js +254 -0
  61. package/dist/cli/type-extractor.js.map +1 -0
  62. package/dist/cli/type-extractor.test.d.ts +2 -0
  63. package/dist/cli/type-extractor.test.d.ts.map +1 -0
  64. package/dist/cli/type-extractor.test.js +173 -0
  65. package/dist/cli/type-extractor.test.js.map +1 -0
  66. package/dist/client/http.d.ts +44 -0
  67. package/dist/client/http.d.ts.map +1 -0
  68. package/dist/client/http.js +311 -0
  69. package/dist/client/http.js.map +1 -0
  70. package/dist/client/index.d.ts +158 -0
  71. package/dist/client/index.d.ts.map +1 -0
  72. package/dist/client/index.js +256 -0
  73. package/dist/client/index.js.map +1 -0
  74. package/dist/component/_generated/api.d.ts +12 -0
  75. package/dist/component/_generated/api.d.ts.map +1 -0
  76. package/dist/component/_generated/api.js +13 -0
  77. package/dist/component/_generated/api.js.map +1 -0
  78. package/dist/component/_generated/dataModel.d.ts +18 -0
  79. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  80. package/dist/component/_generated/dataModel.js +11 -0
  81. package/dist/component/_generated/dataModel.js.map +1 -0
  82. package/dist/component/_generated/server.d.ts +42 -0
  83. package/dist/component/_generated/server.d.ts.map +1 -0
  84. package/dist/component/_generated/server.js +39 -0
  85. package/dist/component/_generated/server.js.map +1 -0
  86. package/dist/component/actions.d.ts +42 -0
  87. package/dist/component/actions.d.ts.map +1 -0
  88. package/dist/component/actions.js +405 -0
  89. package/dist/component/actions.js.map +1 -0
  90. package/dist/component/apiKeyMutations.d.ts +29 -0
  91. package/dist/component/apiKeyMutations.d.ts.map +1 -0
  92. package/dist/component/apiKeyMutations.js +149 -0
  93. package/dist/component/apiKeyMutations.js.map +1 -0
  94. package/dist/component/apiKeyQueries.d.ts +37 -0
  95. package/dist/component/apiKeyQueries.d.ts.map +1 -0
  96. package/dist/component/apiKeyQueries.js +127 -0
  97. package/dist/component/apiKeyQueries.js.map +1 -0
  98. package/dist/component/checksum.d.ts +6 -0
  99. package/dist/component/checksum.d.ts.map +1 -0
  100. package/dist/component/checksum.js +14 -0
  101. package/dist/component/checksum.js.map +1 -0
  102. package/dist/component/checksum.test.d.ts +2 -0
  103. package/dist/component/checksum.test.d.ts.map +1 -0
  104. package/dist/component/checksum.test.js +27 -0
  105. package/dist/component/checksum.test.js.map +1 -0
  106. package/dist/component/convex.config.d.ts +3 -0
  107. package/dist/component/convex.config.d.ts.map +1 -0
  108. package/dist/component/convex.config.js +4 -0
  109. package/dist/component/convex.config.js.map +1 -0
  110. package/dist/component/cronActions.d.ts +3 -0
  111. package/dist/component/cronActions.d.ts.map +1 -0
  112. package/dist/component/cronActions.js +38 -0
  113. package/dist/component/cronActions.js.map +1 -0
  114. package/dist/component/cronQueries.d.ts +6 -0
  115. package/dist/component/cronQueries.d.ts.map +1 -0
  116. package/dist/component/cronQueries.js +38 -0
  117. package/dist/component/cronQueries.js.map +1 -0
  118. package/dist/component/crons.d.ts +3 -0
  119. package/dist/component/crons.d.ts.map +1 -0
  120. package/dist/component/crons.js +18 -0
  121. package/dist/component/crons.js.map +1 -0
  122. package/dist/component/format.d.ts +11 -0
  123. package/dist/component/format.d.ts.map +1 -0
  124. package/dist/component/format.js +175 -0
  125. package/dist/component/format.js.map +1 -0
  126. package/dist/component/format.test.d.ts +2 -0
  127. package/dist/component/format.test.d.ts.map +1 -0
  128. package/dist/component/format.test.js +118 -0
  129. package/dist/component/format.test.js.map +1 -0
  130. package/dist/component/mutations.d.ts +158 -0
  131. package/dist/component/mutations.d.ts.map +1 -0
  132. package/dist/component/mutations.js +745 -0
  133. package/dist/component/mutations.js.map +1 -0
  134. package/dist/component/queries.d.ts +94 -0
  135. package/dist/component/queries.d.ts.map +1 -0
  136. package/dist/component/queries.js +574 -0
  137. package/dist/component/queries.js.map +1 -0
  138. package/dist/component/schema.d.ts +278 -0
  139. package/dist/component/schema.d.ts.map +1 -0
  140. package/dist/component/schema.js +161 -0
  141. package/dist/component/schema.js.map +1 -0
  142. package/dist/mcp/server.d.ts +11 -0
  143. package/dist/mcp/server.d.ts.map +1 -0
  144. package/dist/mcp/server.js +571 -0
  145. package/dist/mcp/server.js.map +1 -0
  146. package/dist/shared.d.ts +126 -0
  147. package/dist/shared.d.ts.map +1 -0
  148. package/dist/shared.js +67 -0
  149. package/dist/shared.js.map +1 -0
  150. package/dist/test.d.ts +23 -0
  151. package/dist/test.d.ts.map +1 -0
  152. package/dist/test.js +21 -0
  153. package/dist/test.js.map +1 -0
  154. package/eslint.config.js +15 -0
  155. package/example/convex/convex.config.ts +7 -0
  156. package/example/convex/memory.ts +129 -0
  157. package/llms.md +175 -0
  158. package/llms.txt +126 -0
  159. package/package.json +72 -0
  160. package/prds/API-REFERENCE.md +935 -0
  161. package/prds/README.md +988 -0
  162. package/prds/SETUP.md +682 -0
  163. package/src/cli/index.ts +254 -0
  164. package/src/cli/parsers/claude-code.ts +80 -0
  165. package/src/cli/parsers/codex.ts +45 -0
  166. package/src/cli/parsers/conductor.ts +47 -0
  167. package/src/cli/parsers/cursor.ts +55 -0
  168. package/src/cli/parsers/index.ts +30 -0
  169. package/src/cli/parsers/opencode.ts +84 -0
  170. package/src/cli/parsers/parsers.test.ts +201 -0
  171. package/src/cli/parsers/pi.ts +47 -0
  172. package/src/cli/parsers/types.ts +26 -0
  173. package/src/cli/parsers/vscode-copilot.ts +78 -0
  174. package/src/cli/parsers/zed.ts +47 -0
  175. package/src/cli/sync.ts +110 -0
  176. package/src/cli/type-extractor.test.ts +241 -0
  177. package/src/cli/type-extractor.ts +331 -0
  178. package/src/client/http.ts +415 -0
  179. package/src/client/index.ts +519 -0
  180. package/src/component/_generated/api.ts +14 -0
  181. package/src/component/_generated/dataModel.ts +20 -0
  182. package/src/component/_generated/server.ts +64 -0
  183. package/src/component/actions.ts +558 -0
  184. package/src/component/apiKeyMutations.ts +175 -0
  185. package/src/component/apiKeyQueries.ts +156 -0
  186. package/src/component/checksum.test.ts +31 -0
  187. package/src/component/checksum.ts +13 -0
  188. package/src/component/convex.config.ts +5 -0
  189. package/src/component/cronActions.ts +52 -0
  190. package/src/component/cronQueries.ts +42 -0
  191. package/src/component/crons.ts +34 -0
  192. package/src/component/format.test.ts +133 -0
  193. package/src/component/format.ts +232 -0
  194. package/src/component/mutations.ts +824 -0
  195. package/src/component/queries.ts +684 -0
  196. package/src/component/schema.ts +207 -0
  197. package/src/mcp/server.ts +695 -0
  198. package/src/shared.ts +251 -0
  199. package/src/test.ts +32 -0
  200. package/tsconfig.json +21 -0
  201. package/vitest.config.ts +8 -0
@@ -0,0 +1,558 @@
1
+ import { action } from "./_generated/server.js";
2
+ import { internal } from "./_generated/api.js";
3
+ import { v } from "convex/values";
4
+ import {
5
+ memoryTypeValidator,
6
+ scopeValidator,
7
+ } from "./schema.js";
8
+
9
+ // ── Return validators ───────────────────────────────────────────────
10
+
11
+ const memoryDocValidator = v.object({
12
+ _id: v.string(),
13
+ _creationTime: v.float64(),
14
+ projectId: v.string(),
15
+ scope: scopeValidator,
16
+ userId: v.optional(v.string()),
17
+ agentId: v.optional(v.string()),
18
+ sessionId: v.optional(v.string()),
19
+ title: v.string(),
20
+ content: v.string(),
21
+ memoryType: memoryTypeValidator,
22
+ tags: v.array(v.string()),
23
+ paths: v.optional(v.array(v.string())),
24
+ priority: v.optional(v.float64()),
25
+ source: v.optional(v.string()),
26
+ lastSyncedAt: v.optional(v.float64()),
27
+ checksum: v.string(),
28
+ archived: v.boolean(),
29
+ embeddingId: v.optional(v.string()),
30
+ accessCount: v.optional(v.float64()),
31
+ lastAccessedAt: v.optional(v.float64()),
32
+ positiveCount: v.optional(v.float64()),
33
+ negativeCount: v.optional(v.float64()),
34
+ });
35
+
36
+ const ingestEventValidator = v.union(
37
+ v.literal("added"),
38
+ v.literal("updated"),
39
+ v.literal("deleted"),
40
+ v.literal("skipped"),
41
+ );
42
+
43
+ // ── generateEmbedding ───────────────────────────────────────────────
44
+
45
+ export const generateEmbedding = action({
46
+ args: {
47
+ memoryId: v.string(),
48
+ embeddingApiKey: v.string(),
49
+ model: v.optional(v.string()),
50
+ },
51
+ returns: v.null(),
52
+ handler: async (ctx, args) => {
53
+ const model = args.model ?? "text-embedding-3-small";
54
+
55
+ const memory = await ctx.runQuery(internal.queries.get, {
56
+ memoryId: args.memoryId,
57
+ });
58
+ if (!memory) throw new Error(`Memory not found: ${args.memoryId}`);
59
+
60
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
61
+ method: "POST",
62
+ headers: {
63
+ Authorization: `Bearer ${args.embeddingApiKey}`,
64
+ "Content-Type": "application/json",
65
+ },
66
+ body: JSON.stringify({
67
+ input: `${memory.title}\n\n${memory.content}`,
68
+ model,
69
+ }),
70
+ });
71
+
72
+ if (!response.ok) {
73
+ const error = await response.text();
74
+ throw new Error(`Embedding API error: ${response.status} ${error}`);
75
+ }
76
+
77
+ const data = (await response.json()) as {
78
+ data: Array<{ embedding: number[] }>;
79
+ };
80
+ const embedding = data.data[0].embedding;
81
+
82
+ await ctx.runMutation(internal.mutations.storeEmbedding, {
83
+ memoryId: args.memoryId,
84
+ embedding,
85
+ model,
86
+ dimensions: embedding.length,
87
+ });
88
+
89
+ return null;
90
+ },
91
+ });
92
+
93
+ // ── semanticSearch ──────────────────────────────────────────────────
94
+
95
+ export const semanticSearch = action({
96
+ args: {
97
+ projectId: v.string(),
98
+ query: v.string(),
99
+ embeddingApiKey: v.optional(v.string()),
100
+ limit: v.optional(v.float64()),
101
+ },
102
+ returns: v.array(memoryDocValidator),
103
+ handler: async (ctx, args) => {
104
+ const limit = args.limit ?? 10;
105
+
106
+ if (!args.embeddingApiKey) {
107
+ return await ctx.runQuery(internal.queries.search, {
108
+ projectId: args.projectId,
109
+ query: args.query,
110
+ limit,
111
+ });
112
+ }
113
+
114
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
115
+ method: "POST",
116
+ headers: {
117
+ Authorization: `Bearer ${args.embeddingApiKey}`,
118
+ "Content-Type": "application/json",
119
+ },
120
+ body: JSON.stringify({
121
+ input: args.query,
122
+ model: "text-embedding-3-small",
123
+ }),
124
+ });
125
+
126
+ if (!response.ok) {
127
+ return await ctx.runQuery(internal.queries.search, {
128
+ projectId: args.projectId,
129
+ query: args.query,
130
+ limit,
131
+ });
132
+ }
133
+
134
+ const data = (await response.json()) as {
135
+ data: Array<{ embedding: number[] }>;
136
+ };
137
+ const queryEmbedding = data.data[0].embedding;
138
+
139
+ const vectorResults = await ctx.vectorSearch("embeddings", "by_embedding", {
140
+ vector: queryEmbedding,
141
+ limit: limit * 2,
142
+ });
143
+
144
+ const memories = [];
145
+ for (const result of vectorResults) {
146
+ const embedding = await ctx.runQuery(internal.queries.getEmbeddingMemory, {
147
+ embeddingId: result._id as unknown as string,
148
+ });
149
+ if (
150
+ embedding &&
151
+ embedding.projectId === args.projectId &&
152
+ !embedding.archived
153
+ ) {
154
+ memories.push(embedding);
155
+ if (memories.length >= limit) break;
156
+ }
157
+ }
158
+
159
+ return memories;
160
+ },
161
+ });
162
+
163
+ // ── embedAll ────────────────────────────────────────────────────────
164
+
165
+ export const embedAll = action({
166
+ args: {
167
+ projectId: v.string(),
168
+ embeddingApiKey: v.string(),
169
+ model: v.optional(v.string()),
170
+ },
171
+ returns: v.object({
172
+ embedded: v.float64(),
173
+ skipped: v.float64(),
174
+ }),
175
+ handler: async (ctx, args) => {
176
+ const memories = await ctx.runQuery(internal.queries.listUnembedded, {
177
+ projectId: args.projectId,
178
+ });
179
+
180
+ let embedded = 0;
181
+ let skipped = 0;
182
+
183
+ for (const memory of memories) {
184
+ try {
185
+ await ctx.runAction(internal.actions.generateEmbedding, {
186
+ memoryId: memory._id,
187
+ embeddingApiKey: args.embeddingApiKey,
188
+ model: args.model,
189
+ });
190
+ embedded++;
191
+ } catch {
192
+ skipped++;
193
+ }
194
+ }
195
+
196
+ return { embedded, skipped };
197
+ },
198
+ });
199
+
200
+ // ── ingest (intelligent memory pipeline) ────────────────────────────
201
+ //
202
+ // This is the core "smart memory" feature inspired by mem0.
203
+ // It takes raw text (conversations, notes, etc.) and:
204
+ // 1. Extracts discrete facts/learnings via LLM
205
+ // 2. Searches existing memories for overlap
206
+ // 3. Decides per-fact: ADD, UPDATE, DELETE, or SKIP
207
+ // 4. Returns a structured changelog
208
+
209
+ export const ingest = action({
210
+ args: {
211
+ projectId: v.string(),
212
+ content: v.string(), // raw text to extract memories from
213
+ scope: v.optional(scopeValidator),
214
+ userId: v.optional(v.string()),
215
+ agentId: v.optional(v.string()),
216
+ sessionId: v.optional(v.string()),
217
+ llmApiKey: v.string(), // OpenAI-compatible API key
218
+ llmModel: v.optional(v.string()),
219
+ llmBaseUrl: v.optional(v.string()),
220
+ embeddingApiKey: v.optional(v.string()),
221
+ customExtractionPrompt: v.optional(v.string()),
222
+ customUpdatePrompt: v.optional(v.string()),
223
+ },
224
+ returns: v.object({
225
+ results: v.array(
226
+ v.object({
227
+ memoryId: v.string(),
228
+ content: v.string(),
229
+ event: ingestEventValidator,
230
+ previousContent: v.optional(v.string()),
231
+ }),
232
+ ),
233
+ totalProcessed: v.float64(),
234
+ }),
235
+ handler: async (ctx, args) => {
236
+ const scope = args.scope ?? "project";
237
+ const llmModel = args.llmModel ?? "gpt-4.1-nano";
238
+ const llmBaseUrl = args.llmBaseUrl ?? "https://api.openai.com/v1";
239
+
240
+ // Load project settings for custom prompts
241
+ const projectSettings = await ctx.runQuery(
242
+ internal.queries.getProjectSettings,
243
+ { projectId: args.projectId },
244
+ );
245
+
246
+ const extractionPrompt =
247
+ args.customExtractionPrompt ??
248
+ projectSettings?.settings.factExtractionPrompt ??
249
+ DEFAULT_EXTRACTION_PROMPT;
250
+
251
+ const updatePrompt =
252
+ args.customUpdatePrompt ??
253
+ projectSettings?.settings.updateDecisionPrompt ??
254
+ DEFAULT_UPDATE_PROMPT;
255
+
256
+ // Step 1: Extract facts from input via LLM
257
+ const facts = await extractFacts(
258
+ args.content,
259
+ extractionPrompt,
260
+ args.llmApiKey,
261
+ llmModel,
262
+ llmBaseUrl,
263
+ );
264
+
265
+ if (facts.length === 0) {
266
+ return { results: [], totalProcessed: 0 };
267
+ }
268
+
269
+ // Step 2: Load existing memories for dedup comparison
270
+ const existingMemories = await ctx.runQuery(
271
+ internal.queries.listForIngest,
272
+ { projectId: args.projectId, limit: 200 },
273
+ );
274
+
275
+ // Step 3: For each fact, decide action via LLM
276
+ const decisions = await decideActions(
277
+ facts,
278
+ existingMemories,
279
+ updatePrompt,
280
+ args.llmApiKey,
281
+ llmModel,
282
+ llmBaseUrl,
283
+ );
284
+
285
+ // Step 4: Execute decisions
286
+ const results: Array<{
287
+ memoryId: string;
288
+ content: string;
289
+ event: "added" | "updated" | "deleted" | "skipped";
290
+ previousContent?: string;
291
+ }> = [];
292
+
293
+ for (const decision of decisions) {
294
+ switch (decision.action) {
295
+ case "ADD": {
296
+ const memoryId = await ctx.runMutation(
297
+ internal.mutations.ingestCreateMemory,
298
+ {
299
+ projectId: args.projectId,
300
+ scope,
301
+ userId: args.userId,
302
+ agentId: args.agentId,
303
+ sessionId: args.sessionId,
304
+ title: decision.title,
305
+ content: decision.content,
306
+ memoryType: decision.memoryType ?? "learning",
307
+ tags: decision.tags ?? [],
308
+ source: "ingest",
309
+ },
310
+ );
311
+
312
+ // Generate embedding if API key available
313
+ if (args.embeddingApiKey) {
314
+ try {
315
+ await ctx.runAction(internal.actions.generateEmbedding, {
316
+ memoryId,
317
+ embeddingApiKey: args.embeddingApiKey,
318
+ });
319
+ } catch {
320
+ // Non-fatal: embedding generation failure shouldn't block ingest
321
+ }
322
+ }
323
+
324
+ results.push({
325
+ memoryId,
326
+ content: decision.content,
327
+ event: "added",
328
+ });
329
+ break;
330
+ }
331
+
332
+ case "UPDATE": {
333
+ if (!decision.existingMemoryId) break;
334
+ const existing = existingMemories.find(
335
+ (m: any) => m._id === decision.existingMemoryId,
336
+ );
337
+
338
+ await ctx.runMutation(internal.mutations.ingestUpdateMemory, {
339
+ memoryId: decision.existingMemoryId,
340
+ content: decision.content,
341
+ });
342
+
343
+ results.push({
344
+ memoryId: decision.existingMemoryId,
345
+ content: decision.content,
346
+ event: "updated",
347
+ previousContent: existing?.content,
348
+ });
349
+ break;
350
+ }
351
+
352
+ case "DELETE": {
353
+ if (!decision.existingMemoryId) break;
354
+ const deletedMemory = existingMemories.find(
355
+ (m: any) => m._id === decision.existingMemoryId,
356
+ );
357
+
358
+ await ctx.runMutation(internal.mutations.ingestDeleteMemory, {
359
+ memoryId: decision.existingMemoryId,
360
+ });
361
+
362
+ results.push({
363
+ memoryId: decision.existingMemoryId,
364
+ content: deletedMemory?.content ?? "",
365
+ event: "deleted",
366
+ previousContent: deletedMemory?.content,
367
+ });
368
+ break;
369
+ }
370
+
371
+ default:
372
+ results.push({
373
+ memoryId: "",
374
+ content: decision.content,
375
+ event: "skipped",
376
+ });
377
+ }
378
+ }
379
+
380
+ return { results, totalProcessed: facts.length };
381
+ },
382
+ });
383
+
384
+ // ── LLM helpers for ingest pipeline ─────────────────────────────────
385
+
386
+ const DEFAULT_EXTRACTION_PROMPT = `You are a memory extraction system. Extract discrete, actionable facts from the following text.
387
+
388
+ Rules:
389
+ - Each fact should be a single, self-contained piece of information
390
+ - Include preferences, decisions, corrections, patterns, and learnings
391
+ - Exclude trivial or ephemeral information (greetings, acknowledgments)
392
+ - Return facts as a JSON array of strings
393
+ - Each fact should be 1-3 sentences maximum
394
+
395
+ Return ONLY a JSON array of strings, no other text.`;
396
+
397
+ const DEFAULT_UPDATE_PROMPT = `You are a memory management system. Given new facts and existing memories, decide what to do with each new fact.
398
+
399
+ For each new fact, return one of:
400
+ - ADD: Create a new memory (no existing memory covers this)
401
+ - UPDATE: Merge with an existing memory (specify which one by ID and provide merged content)
402
+ - DELETE: Remove an existing memory because the new fact contradicts/invalidates it
403
+ - NONE: Skip this fact (already covered by existing memories)
404
+
405
+ Also assign each ADD/UPDATE a:
406
+ - title: short descriptive title (2-6 words)
407
+ - memoryType: one of "instruction", "learning", "reference", "feedback", "journal"
408
+ - tags: relevant tags as array of strings
409
+
410
+ Return a JSON array of decision objects.`;
411
+
412
+ interface IngestDecision {
413
+ action: "ADD" | "UPDATE" | "DELETE" | "NONE";
414
+ content: string;
415
+ title: string;
416
+ existingMemoryId?: string;
417
+ memoryType?: string;
418
+ tags?: string[];
419
+ }
420
+
421
+ async function callLLM(
422
+ messages: Array<{ role: string; content: string }>,
423
+ apiKey: string,
424
+ model: string,
425
+ baseUrl: string,
426
+ ): Promise<string> {
427
+ const response = await fetch(`${baseUrl}/chat/completions`, {
428
+ method: "POST",
429
+ headers: {
430
+ Authorization: `Bearer ${apiKey}`,
431
+ "Content-Type": "application/json",
432
+ },
433
+ body: JSON.stringify({
434
+ model,
435
+ messages,
436
+ temperature: 0.1,
437
+ max_tokens: 4096,
438
+ }),
439
+ });
440
+
441
+ if (!response.ok) {
442
+ const error = await response.text();
443
+ throw new Error(`LLM API error: ${response.status} ${error}`);
444
+ }
445
+
446
+ const data = (await response.json()) as {
447
+ choices: Array<{ message: { content: string } }>;
448
+ };
449
+ return data.choices[0].message.content;
450
+ }
451
+
452
+ function extractJson(text: string): string {
453
+ // Strip markdown code fences if present
454
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
455
+ if (fenced) return fenced[1].trim();
456
+
457
+ // Strip <think> tags
458
+ const cleaned = text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
459
+
460
+ // Try to find JSON array or object
461
+ const jsonMatch = cleaned.match(/(\[[\s\S]*\]|\{[\s\S]*\})/);
462
+ return jsonMatch ? jsonMatch[1] : cleaned;
463
+ }
464
+
465
+ async function extractFacts(
466
+ content: string,
467
+ prompt: string,
468
+ apiKey: string,
469
+ model: string,
470
+ baseUrl: string,
471
+ ): Promise<string[]> {
472
+ const response = await callLLM(
473
+ [
474
+ { role: "system", content: prompt },
475
+ { role: "user", content },
476
+ ],
477
+ apiKey,
478
+ model,
479
+ baseUrl,
480
+ );
481
+
482
+ try {
483
+ const parsed = JSON.parse(extractJson(response));
484
+ if (Array.isArray(parsed)) {
485
+ return parsed.map((item: unknown) =>
486
+ typeof item === "string" ? item : JSON.stringify(item),
487
+ );
488
+ }
489
+ return [];
490
+ } catch {
491
+ return [];
492
+ }
493
+ }
494
+
495
+ async function decideActions(
496
+ facts: string[],
497
+ existingMemories: Array<{
498
+ _id: string;
499
+ title: string;
500
+ content: string;
501
+ memoryType: string;
502
+ }>,
503
+ prompt: string,
504
+ apiKey: string,
505
+ model: string,
506
+ baseUrl: string,
507
+ ): Promise<IngestDecision[]> {
508
+ const existingStr = existingMemories
509
+ .map(
510
+ (m) =>
511
+ `[ID: ${m._id}] [${m.memoryType}] ${m.title}: ${m.content.slice(0, 200)}`,
512
+ )
513
+ .join("\n");
514
+
515
+ const factsStr = facts.map((f, i) => `${i + 1}. ${f}`).join("\n");
516
+
517
+ const userMessage = `## Existing Memories\n${existingStr || "(none)"}\n\n## New Facts\n${factsStr}`;
518
+
519
+ const response = await callLLM(
520
+ [
521
+ { role: "system", content: prompt },
522
+ { role: "user", content: userMessage },
523
+ ],
524
+ apiKey,
525
+ model,
526
+ baseUrl,
527
+ );
528
+
529
+ try {
530
+ const parsed = JSON.parse(extractJson(response));
531
+ if (!Array.isArray(parsed)) return [];
532
+
533
+ return parsed
534
+ .filter(
535
+ (d: any) =>
536
+ d && typeof d === "object" && d.action && d.content,
537
+ )
538
+ .map((d: any) => ({
539
+ action: d.action as "ADD" | "UPDATE" | "DELETE" | "NONE",
540
+ content: String(d.content),
541
+ title: String(d.title ?? "untitled"),
542
+ existingMemoryId: d.existingMemoryId
543
+ ? String(d.existingMemoryId)
544
+ : undefined,
545
+ memoryType: d.memoryType ? String(d.memoryType) : undefined,
546
+ tags: Array.isArray(d.tags) ? d.tags.map(String) : undefined,
547
+ }));
548
+ } catch {
549
+ // If LLM returns unparseable response, treat all facts as ADDs
550
+ return facts.map((f, i) => ({
551
+ action: "ADD" as const,
552
+ content: f,
553
+ title: `fact-${i + 1}`,
554
+ memoryType: "learning",
555
+ tags: [],
556
+ }));
557
+ }
558
+ }