@kernl-sdk/turbopuffer 0.1.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.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-check-types.log +60 -0
  3. package/CHANGELOG.md +33 -0
  4. package/LICENSE +201 -0
  5. package/README.md +60 -0
  6. package/dist/__tests__/convert.test.d.ts +2 -0
  7. package/dist/__tests__/convert.test.d.ts.map +1 -0
  8. package/dist/__tests__/convert.test.js +346 -0
  9. package/dist/__tests__/filter.test.d.ts +8 -0
  10. package/dist/__tests__/filter.test.d.ts.map +1 -0
  11. package/dist/__tests__/filter.test.js +649 -0
  12. package/dist/__tests__/filters.integration.test.d.ts +8 -0
  13. package/dist/__tests__/filters.integration.test.d.ts.map +1 -0
  14. package/dist/__tests__/filters.integration.test.js +502 -0
  15. package/dist/__tests__/integration/filters.integration.test.d.ts +8 -0
  16. package/dist/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/integration/filters.integration.test.js +475 -0
  18. package/dist/__tests__/integration/integration.test.d.ts +2 -0
  19. package/dist/__tests__/integration/integration.test.d.ts.map +1 -0
  20. package/dist/__tests__/integration/integration.test.js +329 -0
  21. package/dist/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  22. package/dist/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  23. package/dist/__tests__/integration/lifecycle.integration.test.js +370 -0
  24. package/dist/__tests__/integration/memory.integration.test.d.ts +2 -0
  25. package/dist/__tests__/integration/memory.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/integration/memory.integration.test.js +287 -0
  27. package/dist/__tests__/integration/query.integration.test.d.ts +8 -0
  28. package/dist/__tests__/integration/query.integration.test.d.ts.map +1 -0
  29. package/dist/__tests__/integration/query.integration.test.js +385 -0
  30. package/dist/__tests__/integration.test.d.ts +2 -0
  31. package/dist/__tests__/integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/integration.test.js +343 -0
  33. package/dist/__tests__/lifecycle.integration.test.d.ts +8 -0
  34. package/dist/__tests__/lifecycle.integration.test.d.ts.map +1 -0
  35. package/dist/__tests__/lifecycle.integration.test.js +385 -0
  36. package/dist/__tests__/query.integration.test.d.ts +8 -0
  37. package/dist/__tests__/query.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/query.integration.test.js +423 -0
  39. package/dist/__tests__/query.test.d.ts +8 -0
  40. package/dist/__tests__/query.test.d.ts.map +1 -0
  41. package/dist/__tests__/query.test.js +472 -0
  42. package/dist/convert/document.d.ts +20 -0
  43. package/dist/convert/document.d.ts.map +1 -0
  44. package/dist/convert/document.js +72 -0
  45. package/dist/convert/filter.d.ts +15 -0
  46. package/dist/convert/filter.d.ts.map +1 -0
  47. package/dist/convert/filter.js +109 -0
  48. package/dist/convert/index.d.ts +8 -0
  49. package/dist/convert/index.d.ts.map +1 -0
  50. package/dist/convert/index.js +7 -0
  51. package/dist/convert/query.d.ts +22 -0
  52. package/dist/convert/query.d.ts.map +1 -0
  53. package/dist/convert/query.js +111 -0
  54. package/dist/convert/schema.d.ts +39 -0
  55. package/dist/convert/schema.d.ts.map +1 -0
  56. package/dist/convert/schema.js +124 -0
  57. package/dist/convert.d.ts +68 -0
  58. package/dist/convert.d.ts.map +1 -0
  59. package/dist/convert.js +333 -0
  60. package/dist/handle.d.ts +34 -0
  61. package/dist/handle.d.ts.map +1 -0
  62. package/dist/handle.js +72 -0
  63. package/dist/index.d.ts +27 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +26 -0
  66. package/dist/search.d.ts +85 -0
  67. package/dist/search.d.ts.map +1 -0
  68. package/dist/search.js +167 -0
  69. package/dist/types.d.ts +14 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +1 -0
  72. package/package.json +57 -0
  73. package/src/__tests__/convert.test.ts +425 -0
  74. package/src/__tests__/filter.test.ts +730 -0
  75. package/src/__tests__/integration/filters.integration.test.ts +558 -0
  76. package/src/__tests__/integration/integration.test.ts +399 -0
  77. package/src/__tests__/integration/lifecycle.integration.test.ts +464 -0
  78. package/src/__tests__/integration/memory.integration.test.ts +353 -0
  79. package/src/__tests__/integration/query.integration.test.ts +471 -0
  80. package/src/__tests__/query.test.ts +636 -0
  81. package/src/convert/document.ts +95 -0
  82. package/src/convert/filter.ts +123 -0
  83. package/src/convert/index.ts +8 -0
  84. package/src/convert/query.ts +151 -0
  85. package/src/convert/schema.ts +163 -0
  86. package/src/handle.ts +104 -0
  87. package/src/index.ts +31 -0
  88. package/src/search.ts +207 -0
  89. package/src/types.ts +14 -0
  90. package/tsconfig.json +13 -0
  91. package/vitest.config.ts +15 -0
@@ -0,0 +1,353 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
2
+ import { Pool } from "pg";
3
+ import { Kernl, Agent } from "kernl";
4
+ import type { LanguageModel } from "@kernl-sdk/protocol";
5
+ import "@kernl-sdk/ai/openai"; // Register OpenAI embedding provider
6
+
7
+ import { postgres } from "@kernl-sdk/pg";
8
+ import { turbopuffer } from "../../index";
9
+
10
+ const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
11
+ const TURBOPUFFER_API_KEY = process.env.TURBOPUFFER_API_KEY;
12
+ const TURBOPUFFER_REGION = process.env.TURBOPUFFER_REGION ?? "api";
13
+
14
+ describe.sequential(
15
+ "Memory Integration with Turbopuffer",
16
+ { timeout: 60000 },
17
+ () => {
18
+ if (!TEST_DB_URL) {
19
+ it.skip("requires KERNL_PG_TEST_URL environment variable", () => {});
20
+ return;
21
+ }
22
+
23
+ if (!TURBOPUFFER_API_KEY) {
24
+ it.skip("requires TURBOPUFFER_API_KEY environment variable", () => {});
25
+ return;
26
+ }
27
+
28
+ if (!process.env.OPENAI_API_KEY) {
29
+ it.skip("requires OPENAI_API_KEY environment variable", () => {});
30
+ return;
31
+ }
32
+
33
+ let pool: Pool;
34
+ let kernl: Kernl;
35
+ let agent: Agent;
36
+ const vectorIndexId = `kernl-test-memories-${Date.now()}`;
37
+
38
+ // Unique ID generator to avoid stale data between tests
39
+ let testCounter = 0;
40
+ const uid = (base: string) => `${base}-${Date.now()}-${++testCounter}`;
41
+
42
+ beforeAll(async () => {
43
+ pool = new Pool({ connectionString: TEST_DB_URL });
44
+
45
+ // Clean slate for postgres
46
+ await pool.query('DROP SCHEMA IF EXISTS "kernl" CASCADE');
47
+
48
+ // Create Kernl with PG for relational + Turbopuffer for vector
49
+ kernl = new Kernl({
50
+ storage: {
51
+ db: postgres({ pool }),
52
+ vector: turbopuffer({
53
+ apiKey: TURBOPUFFER_API_KEY,
54
+ region: TURBOPUFFER_REGION,
55
+ }),
56
+ },
57
+ memory: {
58
+ embeddingModel: "openai/text-embedding-3-small",
59
+ dimensions: 1536,
60
+ indexId: vectorIndexId,
61
+ },
62
+ });
63
+
64
+ // Register a dummy agent for test scope
65
+ const model = {
66
+ spec: "1.0" as const,
67
+ provider: "test",
68
+ modelId: "test-model",
69
+ } as unknown as LanguageModel;
70
+
71
+ agent = new Agent({
72
+ id: "test-agent",
73
+ name: "Test Agent",
74
+ instructions: () => "test instructions",
75
+ model,
76
+ });
77
+
78
+ kernl.register(agent);
79
+
80
+ // Initialize storage (creates "kernl" schema and tables)
81
+ await kernl.storage.init();
82
+ });
83
+
84
+ afterAll(async () => {
85
+ // Clean up Turbopuffer namespace
86
+ try {
87
+ const tpuf = turbopuffer({
88
+ apiKey: TURBOPUFFER_API_KEY!,
89
+ region: TURBOPUFFER_REGION,
90
+ });
91
+ await tpuf.deleteIndex(vectorIndexId);
92
+ } catch {
93
+ // Ignore if already deleted
94
+ }
95
+
96
+ if (kernl) {
97
+ await kernl.storage.close();
98
+ }
99
+ });
100
+
101
+ beforeEach(async () => {
102
+ // Clean memories in postgres
103
+ await pool.query('DELETE FROM "kernl"."memories"');
104
+
105
+ // Note: We don't delete the Turbopuffer namespace between tests because
106
+ // MemoryIndexHandle caches its init state. The namespace is cleaned up
107
+ // in afterAll. Tests use unique memory IDs so they don't conflict.
108
+ });
109
+
110
+ it("creates memory and indexes it in Turbopuffer", async () => {
111
+ const id = uid("m");
112
+ const memory = await agent.memories.create({
113
+ id,
114
+ namespace: "test",
115
+ collection: "facts",
116
+ content: { text: "The user loves TypeScript programming" },
117
+ });
118
+
119
+ expect(memory.id).toBe(id);
120
+ expect(memory.content.text).toBe("The user loves TypeScript programming");
121
+
122
+ // Verify memory exists in postgres
123
+ const dbResult = await pool.query(
124
+ 'SELECT * FROM "kernl"."memories" WHERE id = $1',
125
+ [id],
126
+ );
127
+ expect(dbResult.rows).toHaveLength(1);
128
+
129
+ // Verify memory is searchable in Turbopuffer
130
+ const results = await agent.memories.search({
131
+ query: "TypeScript",
132
+ limit: 10,
133
+ });
134
+
135
+ expect(results.length).toBeGreaterThan(0);
136
+ expect(results[0].document?.id).toBe(id);
137
+ });
138
+
139
+ it("searches memories using vector search", async () => {
140
+ // Create several memories with unique IDs
141
+ const id1 = uid("m");
142
+ const id2 = uid("m");
143
+ const id3 = uid("m");
144
+
145
+ await agent.memories.create({
146
+ id: id1,
147
+ namespace: "test",
148
+ collection: "facts",
149
+ content: { text: "The user loves TypeScript programming" },
150
+ });
151
+
152
+ await agent.memories.create({
153
+ id: id2,
154
+ namespace: "test",
155
+ collection: "facts",
156
+ content: { text: "The user enjoys cooking Italian food" },
157
+ });
158
+
159
+ await agent.memories.create({
160
+ id: id3,
161
+ namespace: "test",
162
+ collection: "facts",
163
+ content: { text: "TypeScript has excellent type safety" },
164
+ });
165
+
166
+ // Search for TypeScript-related memories (vector-only since hybrid not supported)
167
+ const results = await agent.memories.search({
168
+ query: "programming languages",
169
+ limit: 10,
170
+ });
171
+
172
+ expect(results.length).toBeGreaterThan(0);
173
+
174
+ // Should find TypeScript-related memories with higher scores
175
+ const ids = results.map((r) => r.document?.id);
176
+ expect(ids).toContain(id1); // Direct match
177
+ expect(ids).toContain(id3); // Related to TypeScript
178
+ });
179
+
180
+ it("returns no results when filters exclude all matches", async () => {
181
+ const id = uid("m");
182
+ const ns = uid("ns");
183
+
184
+ await agent.memories.create({
185
+ id,
186
+ namespace: ns,
187
+ collection: "facts",
188
+ content: { text: "User likes hiking" },
189
+ });
190
+
191
+ // Filter for a different namespace that has no memories
192
+ const results = await agent.memories.search({
193
+ query: "hiking",
194
+ filter: { scope: { namespace: "nonexistent-ns" } },
195
+ limit: 10,
196
+ });
197
+
198
+ expect(results.length).toBe(0);
199
+ });
200
+
201
+ it("filters search results by scope", async () => {
202
+ const id1 = uid("m");
203
+ const id2 = uid("m");
204
+ const ns1 = uid("user");
205
+ const ns2 = uid("user");
206
+
207
+ await agent.memories.create({
208
+ id: id1,
209
+ namespace: ns1,
210
+ collection: "facts",
211
+ content: { text: "User 1 likes cats" },
212
+ });
213
+
214
+ await agent.memories.create({
215
+ id: id2,
216
+ namespace: ns2,
217
+ collection: "facts",
218
+ content: { text: "User 2 likes cats" },
219
+ });
220
+
221
+ // Search only in ns1 namespace
222
+ const results = await agent.memories.search({
223
+ query: "cats",
224
+ filter: { scope: { namespace: ns1 } },
225
+ limit: 10,
226
+ });
227
+
228
+ expect(results.length).toBe(1);
229
+ expect(results[0].document?.id).toBe(id1);
230
+ });
231
+
232
+ it("respects topK limit", async () => {
233
+ const ns = uid("ns");
234
+
235
+ await agent.memories.create({
236
+ namespace: ns,
237
+ collection: "facts",
238
+ content: { text: "The user likes TypeScript" },
239
+ });
240
+
241
+ await agent.memories.create({
242
+ namespace: ns,
243
+ collection: "facts",
244
+ content: { text: "The user likes JavaScript" },
245
+ });
246
+
247
+ await agent.memories.create({
248
+ namespace: ns,
249
+ collection: "facts",
250
+ content: { text: "The user likes Rust" },
251
+ });
252
+
253
+ // Filter by namespace to only get our test's memories
254
+ const results = await agent.memories.search({
255
+ query: "programming languages",
256
+ filter: { scope: { namespace: ns } },
257
+ limit: 1,
258
+ });
259
+
260
+ expect(results.length).toBe(1);
261
+ });
262
+
263
+ it("updates memory content and re-indexes", async () => {
264
+ const id = uid("m");
265
+ const ns = uid("ns");
266
+
267
+ await agent.memories.create({
268
+ id,
269
+ namespace: ns,
270
+ collection: "facts",
271
+ content: { text: "Original content about dogs" },
272
+ });
273
+
274
+ // Update content (still use kernl.memories for update - not yet on agent API)
275
+ await kernl.memories.update({
276
+ id,
277
+ content: { text: "Updated content about cats" },
278
+ });
279
+
280
+ // Search should find updated content
281
+ const results = await agent.memories.search({
282
+ query: "cats",
283
+ filter: { scope: { namespace: ns } },
284
+ limit: 10,
285
+ });
286
+
287
+ expect(results.length).toBeGreaterThan(0);
288
+ const match = results.find((r) => r.document?.id === id);
289
+ expect(match).toBeDefined();
290
+ expect(match?.document?.text).toBe("Updated content about cats");
291
+ });
292
+
293
+ it("creates memories with multimodal content", async () => {
294
+ const id = uid("m");
295
+ const ns = uid("ns");
296
+
297
+ await agent.memories.create({
298
+ id,
299
+ namespace: ns,
300
+ collection: "media",
301
+ content: {
302
+ text: "A beautiful sunset",
303
+ image: {
304
+ data: "base64encodedimage",
305
+ mime: "image/png",
306
+ alt: "Sunset over the ocean",
307
+ },
308
+ },
309
+ });
310
+
311
+ // Should be searchable by text
312
+ const results = await agent.memories.search({
313
+ query: "sunset",
314
+ filter: { scope: { namespace: ns } },
315
+ limit: 10,
316
+ });
317
+
318
+ expect(results.length).toBeGreaterThan(0);
319
+ const match = results.find((r) => r.document?.id === id);
320
+ expect(match).toBeDefined();
321
+ });
322
+
323
+ it("filters by collection", async () => {
324
+ const id1 = uid("m");
325
+ const id2 = uid("m");
326
+ const ns = uid("ns");
327
+
328
+ await agent.memories.create({
329
+ id: id1,
330
+ namespace: ns,
331
+ collection: "facts",
332
+ content: { text: "This is a fact about programming" },
333
+ });
334
+
335
+ await agent.memories.create({
336
+ id: id2,
337
+ namespace: ns,
338
+ collection: "preferences",
339
+ content: { text: "User prefers programming in TypeScript" },
340
+ });
341
+
342
+ // Filter by collection and namespace
343
+ const results = await agent.memories.search({
344
+ query: "programming",
345
+ filter: { scope: { namespace: ns }, collections: ["facts"] },
346
+ limit: 10,
347
+ });
348
+
349
+ expect(results.length).toBe(1);
350
+ expect(results[0].document?.id).toBe(id1);
351
+ });
352
+ },
353
+ );