kernl 0.6.2 → 0.6.3

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 (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-check-types.log +1 -1
  3. package/CHANGELOG.md +30 -0
  4. package/dist/agent/__tests__/concurrency.test.js +1 -1
  5. package/dist/agent/__tests__/run.test.js +1 -1
  6. package/dist/{types/agent.d.ts → agent/types.d.ts} +2 -2
  7. package/dist/agent/types.d.ts.map +1 -0
  8. package/dist/agent.d.ts +36 -4
  9. package/dist/agent.d.ts.map +1 -1
  10. package/dist/agent.js +58 -0
  11. package/dist/api/models/thread.d.ts +1 -1
  12. package/dist/api/resources/threads/threads.d.ts +1 -1
  13. package/dist/api/resources/threads/threads.d.ts.map +1 -1
  14. package/dist/api/resources/threads/threads.js +1 -1
  15. package/dist/api/resources/threads/types.d.ts +2 -2
  16. package/dist/api/resources/threads/types.d.ts.map +1 -1
  17. package/dist/context.d.ts +4 -4
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/guardrail.d.ts +2 -2
  20. package/dist/index.d.ts +5 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -3
  23. package/dist/internal.d.ts +2 -2
  24. package/dist/internal.js +1 -1
  25. package/dist/kernl/index.d.ts +1 -1
  26. package/dist/kernl/index.d.ts.map +1 -1
  27. package/dist/kernl/index.js +0 -1
  28. package/dist/kernl/kernl.d.ts +7 -18
  29. package/dist/kernl/kernl.d.ts.map +1 -1
  30. package/dist/kernl/kernl.js +29 -29
  31. package/dist/kernl/types.d.ts +91 -0
  32. package/dist/kernl/types.d.ts.map +1 -0
  33. package/dist/lib/error.d.ts +2 -2
  34. package/dist/lifecycle.d.ts +2 -2
  35. package/dist/memory/codec.d.ts +32 -0
  36. package/dist/memory/codec.d.ts.map +1 -0
  37. package/dist/memory/codec.js +97 -0
  38. package/dist/memory/codecs/domain.d.ts +34 -0
  39. package/dist/memory/codecs/domain.d.ts.map +1 -0
  40. package/dist/memory/codecs/domain.js +99 -0
  41. package/dist/memory/codecs/identity.d.ts +12 -0
  42. package/dist/memory/codecs/identity.d.ts.map +1 -0
  43. package/dist/memory/codecs/identity.js +17 -0
  44. package/dist/memory/codecs/index.d.ts +31 -0
  45. package/dist/memory/codecs/index.d.ts.map +1 -0
  46. package/dist/memory/codecs/index.js +39 -0
  47. package/dist/memory/codecs/tpuf.d.ts +38 -0
  48. package/dist/memory/codecs/tpuf.d.ts.map +1 -0
  49. package/dist/memory/codecs/tpuf.js +90 -0
  50. package/dist/memory/encoder.d.ts +29 -0
  51. package/dist/memory/encoder.d.ts.map +1 -0
  52. package/dist/memory/encoder.js +45 -0
  53. package/dist/memory/handle.d.ts +89 -0
  54. package/dist/memory/handle.d.ts.map +1 -0
  55. package/dist/memory/handle.js +128 -0
  56. package/dist/memory/index.d.ts +12 -0
  57. package/dist/memory/index.d.ts.map +1 -0
  58. package/dist/memory/index.js +7 -0
  59. package/dist/memory/indexes.d.ts +91 -0
  60. package/dist/memory/indexes.d.ts.map +1 -0
  61. package/dist/memory/indexes.js +7 -0
  62. package/dist/memory/memory.d.ts +51 -0
  63. package/dist/memory/memory.d.ts.map +1 -0
  64. package/dist/memory/memory.js +107 -0
  65. package/dist/memory/schema.d.ts +41 -0
  66. package/dist/memory/schema.d.ts.map +1 -0
  67. package/dist/memory/schema.js +112 -0
  68. package/dist/memory/store.d.ts +36 -0
  69. package/dist/memory/store.d.ts.map +1 -0
  70. package/dist/memory/store.js +4 -0
  71. package/dist/memory/types.d.ts +250 -0
  72. package/dist/memory/types.d.ts.map +1 -0
  73. package/dist/memory/types.js +4 -0
  74. package/dist/storage/base.d.ts +6 -1
  75. package/dist/storage/base.d.ts.map +1 -1
  76. package/dist/storage/in-memory.d.ts +24 -2
  77. package/dist/storage/in-memory.d.ts.map +1 -1
  78. package/dist/storage/in-memory.js +131 -0
  79. package/dist/storage/thread.d.ts +1 -1
  80. package/dist/thread/__tests__/integration.test.js +1 -1
  81. package/dist/thread/__tests__/mock.d.ts +1 -1
  82. package/dist/thread/__tests__/namespace.test.js +1 -1
  83. package/dist/thread/__tests__/thread.test.js +1 -1
  84. package/dist/thread/thread.d.ts +2 -2
  85. package/dist/thread/thread.d.ts.map +1 -1
  86. package/dist/{types/thread.d.ts → thread/types.d.ts} +2 -2
  87. package/dist/thread/types.d.ts.map +1 -0
  88. package/dist/thread/utils.d.ts +2 -2
  89. package/dist/thread/utils.d.ts.map +1 -1
  90. package/package.json +4 -2
  91. package/src/{types/agent.ts → agent/types.ts} +1 -1
  92. package/src/agent.ts +78 -2
  93. package/src/api/__tests__/threads.test.ts +2 -2
  94. package/src/api/models/thread.ts +1 -1
  95. package/src/api/resources/threads/events.ts +1 -1
  96. package/src/api/resources/threads/threads.ts +2 -2
  97. package/src/api/resources/threads/types.ts +2 -2
  98. package/src/context.ts +6 -136
  99. package/src/guardrail.ts +2 -2
  100. package/src/index.ts +35 -6
  101. package/src/internal.ts +2 -2
  102. package/src/kernl/index.ts +8 -0
  103. package/src/{kernl.ts → kernl/kernl.ts} +40 -3
  104. package/src/kernl/types.ts +106 -0
  105. package/src/lib/error.ts +2 -2
  106. package/src/lifecycle.ts +2 -2
  107. package/src/memory/codecs/domain.ts +115 -0
  108. package/src/memory/codecs/identity.ts +28 -0
  109. package/src/memory/codecs/index.ts +61 -0
  110. package/src/memory/codecs/tpuf.ts +115 -0
  111. package/src/memory/encoder.ts +56 -0
  112. package/src/memory/handle.ts +189 -0
  113. package/src/memory/index.ts +49 -0
  114. package/src/memory/indexes.ts +108 -0
  115. package/src/memory/memory.ts +143 -0
  116. package/src/memory/schema.ts +142 -0
  117. package/src/memory/store.ts +47 -0
  118. package/src/memory/types.ts +282 -0
  119. package/src/storage/__tests__/in-memory.test.ts +1 -1
  120. package/src/storage/base.ts +7 -1
  121. package/src/storage/in-memory.ts +170 -2
  122. package/src/storage/thread.ts +1 -1
  123. package/src/thread/__tests__/integration.test.ts +1 -1
  124. package/src/thread/__tests__/mock.ts +1 -1
  125. package/src/thread/__tests__/thread.test.ts +1 -1
  126. package/src/thread/thread.ts +2 -2
  127. package/src/{types/thread.ts → thread/types.ts} +1 -1
  128. package/src/thread/utils.ts +2 -2
  129. package/tsconfig.tsbuildinfo +1 -0
  130. package/dist/api/__tests__/cursor-page.test.d.ts +0 -2
  131. package/dist/api/__tests__/cursor-page.test.d.ts.map +0 -1
  132. package/dist/api/__tests__/cursor-page.test.js +0 -414
  133. package/dist/api/__tests__/offset-page.test.d.ts +0 -2
  134. package/dist/api/__tests__/offset-page.test.d.ts.map +0 -1
  135. package/dist/api/__tests__/offset-page.test.js +0 -510
  136. package/dist/api/pagination/base.d.ts +0 -48
  137. package/dist/api/pagination/base.d.ts.map +0 -1
  138. package/dist/api/pagination/base.js +0 -45
  139. package/dist/api/pagination/cursor.d.ts +0 -44
  140. package/dist/api/pagination/cursor.d.ts.map +0 -1
  141. package/dist/api/pagination/cursor.js +0 -52
  142. package/dist/api/pagination/offset.d.ts +0 -42
  143. package/dist/api/pagination/offset.d.ts.map +0 -1
  144. package/dist/api/pagination/offset.js +0 -55
  145. package/dist/kernl/threads.d.ts +0 -110
  146. package/dist/kernl/threads.d.ts.map +0 -1
  147. package/dist/kernl/threads.js +0 -126
  148. package/dist/kernl.d.ts +0 -51
  149. package/dist/kernl.d.ts.map +0 -1
  150. package/dist/kernl.js +0 -91
  151. package/dist/types/agent.d.ts.map +0 -1
  152. package/dist/types/kernl.d.ts +0 -42
  153. package/dist/types/kernl.d.ts.map +0 -1
  154. package/dist/types/thread.d.ts.map +0 -1
  155. package/src/api/__tests__/cursor-page.test.ts +0 -512
  156. package/src/api/__tests__/offset-page.test.ts +0 -624
  157. package/src/api/pagination/base.ts +0 -79
  158. package/src/api/pagination/cursor.ts +0 -86
  159. package/src/api/pagination/offset.ts +0 -89
  160. package/src/types/kernl.ts +0 -51
  161. /package/dist/{types/agent.js → agent/types.js} +0 -0
  162. /package/dist/{types/kernl.js → kernl/types.js} +0 -0
  163. /package/dist/{types/thread.js → thread/types.js} +0 -0
@@ -0,0 +1,106 @@
1
+ import { LanguageModel, EmbeddingModel } from "@kernl-sdk/protocol";
2
+ import { SearchIndex } from "@kernl-sdk/retrieval";
3
+
4
+ import { Agent } from "@/agent";
5
+ import { KernlStorage } from "@/storage";
6
+
7
+ /**
8
+ * Storage configuration for Kernl.
9
+ */
10
+ export interface StorageOptions {
11
+ /**
12
+ * Relational database storage (threads, tasks, traces).
13
+ */
14
+ db?: KernlStorage;
15
+
16
+ /**
17
+ * Vector search index for semantic memory search.
18
+ * Supports pgvector, Turbopuffer, etc.
19
+ */
20
+ vector?: SearchIndex;
21
+
22
+ // Future storage layers (deferred):
23
+ // blob?: BlobStore;
24
+ // lake?: DataLake;
25
+ }
26
+
27
+ /**
28
+ * Memory system configuration.
29
+ */
30
+ export interface MemoryOptions {
31
+ /**
32
+ * Embedding model for memory encoding.
33
+ *
34
+ * Can be:
35
+ * - A string like "openai/text-embedding-3-small" (resolved via provider registry)
36
+ * - An EmbeddingModel instance
37
+ *
38
+ * @default "openai/text-embedding-3-small"
39
+ */
40
+ embeddingModel?: string | EmbeddingModel<string>;
41
+
42
+ /**
43
+ * Logical index ID used by the search backend.
44
+ * - For pgvector: becomes the table name (with schema from indexProviderOptions)
45
+ * - For Turbopuffer: becomes the namespace
46
+ * @default "kernl_memories_index"
47
+ */
48
+ indexId?: string;
49
+
50
+ /**
51
+ * Backend-specific options passed to SearchIndex.createIndex().
52
+ * - For pgvector: { schema?: string } controls schema (default: "kernl")
53
+ * - For Turbopuffer: not used
54
+ */
55
+ indexProviderOptions?: Record<string, unknown>;
56
+
57
+ /**
58
+ * Vector dimensions for embeddings.
59
+ * Only needed if embedding model doesn't provide this automatically.
60
+ * @default 1536
61
+ */
62
+ dimensions?: number;
63
+
64
+ /**
65
+ * Similarity metric for vector search.
66
+ * @default "cosine"
67
+ */
68
+ similarity?: "cosine" | "euclidean" | "dot_product";
69
+ }
70
+
71
+ /**
72
+ * Configuration options for creating a Kernl instance.
73
+ */
74
+ export interface KernlOptions {
75
+ /**
76
+ * Storage configuration for persisting threads, tasks, and traces.
77
+ */
78
+ storage?: StorageOptions;
79
+
80
+ /**
81
+ * Memory system configuration.
82
+ */
83
+ memory?: MemoryOptions;
84
+ }
85
+
86
+ /**
87
+ * Agent registry interface.
88
+ *
89
+ * Satisfied by Map<string, Agent>.
90
+ */
91
+ export interface AgentRegistry {
92
+ get(id: string): Agent<any> | undefined;
93
+ }
94
+
95
+ /**
96
+ * Model registry interface.
97
+ *
98
+ * Satisfied by Map<string, LanguageModel>.
99
+ * Key format: "provider/modelId"
100
+ *
101
+ * TODO: Create an exhaustive model registry in the protocol package
102
+ * with all supported models and their metadata.
103
+ */
104
+ export interface ModelRegistry {
105
+ get(key: string): LanguageModel | undefined;
106
+ }
package/src/lib/error.ts CHANGED
@@ -9,8 +9,8 @@ import { randomID } from "@kernl-sdk/shared/lib";
9
9
  // import { SerializedThread } from "@/lib/serde/thread";
10
10
  type SerializedThread = any;
11
11
 
12
- import { AgentResponseType } from "@/types/agent";
13
- import { TextResponse } from "@/types/thread";
12
+ import { AgentResponseType } from "@/agent/types";
13
+ import { TextResponse } from "@/thread/types";
14
14
 
15
15
  /**
16
16
  * Abstract base class for all `kernl` errors
package/src/lifecycle.ts CHANGED
@@ -5,8 +5,8 @@ import { Context, UnknownContext } from "./context";
5
5
  import { Tool } from "./tool";
6
6
  import type { ToolCall } from "@kernl-sdk/protocol";
7
7
 
8
- import { AgentResponseType } from "./types/agent";
9
- import { TextResponse } from "./types/thread";
8
+ import { AgentResponseType } from "@/agent/types";
9
+ import { TextResponse } from "@/thread/types";
10
10
 
11
11
  export type EventEmitterEvents = Record<string, any[]>;
12
12
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Domain-level memory codecs.
3
+ *
4
+ * Codecs for transforming between memory domain types and search/index formats.
5
+ */
6
+
7
+ import type { Codec, AsyncCodec } from "@kernl-sdk/shared/lib";
8
+ import type { Filter as SearchFilter } from "@kernl-sdk/retrieval";
9
+
10
+ import type {
11
+ MemoryFilter,
12
+ MemoryRecord,
13
+ MemoryRecordUpdate,
14
+ IndexMemoryRecord,
15
+ IndexMemoryRecordPatch,
16
+ MemoryByteCodec,
17
+ } from "../types";
18
+
19
+ /**
20
+ * Codec for converting MemoryFilter to retrieval Filter.
21
+ *
22
+ * - scope.namespace → namespace
23
+ * - scope.entityId → entityId
24
+ * - scope.agentId → agentId
25
+ * - collections → collection: { $in: [...] }
26
+ * - after/before → timestamp: { $gt/$lt }
27
+ *
28
+ * Content field filtering (text, metadata, kind) is not currently supported.
29
+ * Text relevance is handled via vector similarity in the query, not filters.
30
+ */
31
+ export const MEMORY_FILTER: Codec<MemoryFilter, SearchFilter> = {
32
+ encode(mf: MemoryFilter): SearchFilter {
33
+ const sf: SearchFilter = {};
34
+
35
+ // scope
36
+ if (mf.scope?.namespace !== undefined) sf.namespace = mf.scope.namespace;
37
+ if (mf.scope?.entityId !== undefined) sf.entityId = mf.scope.entityId;
38
+ if (mf.scope?.agentId !== undefined) sf.agentId = mf.scope.agentId;
39
+
40
+ if (mf.collections && mf.collections.length > 0) {
41
+ sf.collection = { $in: mf.collections };
42
+ }
43
+
44
+ if (mf.after !== undefined || mf.before !== undefined) {
45
+ const ts: { $gt?: number; $lt?: number } = {};
46
+ if (mf.after !== undefined) ts.$gt = mf.after;
47
+ if (mf.before !== undefined) ts.$lt = mf.before;
48
+ sf.timestamp = ts;
49
+ }
50
+
51
+ return sf;
52
+ },
53
+
54
+ decode(_filter: SearchFilter): MemoryFilter {
55
+ throw new Error("MEMORY_FILTER.decode not implemented");
56
+ },
57
+ };
58
+
59
+ /**
60
+ * Create a codec for MemoryRecord -> IndexMemoryRecord.
61
+ */
62
+ export function recordCodec(
63
+ bytecodec: MemoryByteCodec,
64
+ ): AsyncCodec<MemoryRecord, IndexMemoryRecord> {
65
+ return {
66
+ async encode(record: MemoryRecord): Promise<IndexMemoryRecord> {
67
+ const indexable = await bytecodec.encode(record.content);
68
+ return {
69
+ id: record.id,
70
+ namespace: record.scope.namespace ?? null,
71
+ entityId: record.scope.entityId ?? null,
72
+ agentId: record.scope.agentId ?? null,
73
+ kind: record.kind,
74
+ collection: record.collection,
75
+ timestamp: record.timestamp,
76
+ createdAt: record.createdAt,
77
+ updatedAt: record.updatedAt,
78
+ ...indexable,
79
+ };
80
+ },
81
+ async decode(): Promise<MemoryRecord> {
82
+ throw new Error("recordCodec.decode not implemented");
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Codec for converting MemoryRecordUpdate to IndexMemoryRecordPatch.
89
+ *
90
+ * Maps patchable fields from domain update to index patch format.
91
+ * wmem/smem are store-only fields and are not included.
92
+ * content changes require full re-index, not a patch.
93
+ */
94
+ export const PATCH_CODEC: Codec<MemoryRecordUpdate, IndexMemoryRecordPatch> = {
95
+ encode(update: MemoryRecordUpdate): IndexMemoryRecordPatch {
96
+ const patch: IndexMemoryRecordPatch = { id: update.id };
97
+
98
+ if (update.scope?.namespace !== undefined)
99
+ patch.namespace = update.scope.namespace;
100
+ if (update.scope?.entityId !== undefined)
101
+ patch.entityId = update.scope.entityId;
102
+ if (update.scope?.agentId !== undefined)
103
+ patch.agentId = update.scope.agentId;
104
+ if (update.collection !== undefined) patch.collection = update.collection;
105
+ if (update.timestamp !== undefined) patch.timestamp = update.timestamp;
106
+ if (update.updatedAt !== undefined) patch.updatedAt = update.updatedAt;
107
+ if (update.metadata !== undefined) patch.metadata = update.metadata;
108
+
109
+ return patch;
110
+ },
111
+
112
+ decode(_patch: IndexMemoryRecordPatch): MemoryRecordUpdate {
113
+ throw new Error("PATCH_CODEC.decode not implemented");
114
+ },
115
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Identity codecs - pass through unchanged.
3
+ *
4
+ * Used for backends that support the full IndexMemoryRecord schema natively.
5
+ */
6
+
7
+ import type { Codec } from "@kernl-sdk/shared/lib";
8
+ import type { FieldSchema, SearchQuery, UnknownDocument } from "@kernl-sdk/retrieval";
9
+
10
+ import type { IndexMemoryRecord } from "../types";
11
+
12
+ export const IDENTITY_DOC: Codec<IndexMemoryRecord, UnknownDocument> = {
13
+ encode: (doc) => doc as unknown as UnknownDocument,
14
+ decode: (row) => row as unknown as IndexMemoryRecord,
15
+ };
16
+
17
+ export const IDENTITY_SCHEMA: Codec<
18
+ Record<string, FieldSchema>,
19
+ Record<string, FieldSchema>
20
+ > = {
21
+ encode: (schema) => schema,
22
+ decode: (schema) => schema,
23
+ };
24
+
25
+ export const IDENTITY_QUERY: Codec<SearchQuery, SearchQuery> = {
26
+ encode: (q) => q,
27
+ decode: (q) => q,
28
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Memory codecs.
3
+ *
4
+ * Re-exports all memory codecs:
5
+ * - Domain codecs (MEMORY_FILTER, PATCH_CODEC, recordCodec)
6
+ * - Backend codecs (TPUF_*, IDENTITY_*)
7
+ * - Backend codec registry (getBackendCodecs)
8
+ */
9
+
10
+ import type { Codec } from "@kernl-sdk/shared/lib";
11
+ import type {
12
+ FieldSchema,
13
+ SearchQuery,
14
+ UnknownDocument,
15
+ } from "@kernl-sdk/retrieval";
16
+
17
+ import type { IndexMemoryRecord } from "../types";
18
+ import { IDENTITY_DOC, IDENTITY_SCHEMA, IDENTITY_QUERY } from "./identity";
19
+ import { TPUF_DOC, TPUF_SCHEMA, TPUF_QUERY } from "./tpuf";
20
+
21
+ // re-exports
22
+ export { MEMORY_FILTER, PATCH_CODEC, recordCodec } from "./domain";
23
+ export { IDENTITY_DOC, IDENTITY_SCHEMA, IDENTITY_QUERY } from "./identity";
24
+ export { TPUF_DOC, TPUF_SCHEMA, TPUF_QUERY } from "./tpuf";
25
+
26
+ /**
27
+ * Backend codec set.
28
+ */
29
+ export interface AdapterCodecs {
30
+ doc: Codec<IndexMemoryRecord, UnknownDocument>;
31
+ schema: Codec<Record<string, FieldSchema>, Record<string, FieldSchema>>;
32
+ query: Codec<SearchQuery, SearchQuery>;
33
+ }
34
+
35
+ /**
36
+ * Registry of backend codecs.
37
+ */
38
+ export const ADAPTER_CODECS: Record<string, AdapterCodecs> = {
39
+ turbopuffer: { doc: TPUF_DOC, schema: TPUF_SCHEMA, query: TPUF_QUERY },
40
+ pgvector: {
41
+ doc: IDENTITY_DOC,
42
+ schema: IDENTITY_SCHEMA,
43
+ query: IDENTITY_QUERY,
44
+ },
45
+ };
46
+
47
+ /**
48
+ * Default codecs (identity) for unknown backends.
49
+ */
50
+ const DEFAULT_CODECS: AdapterCodecs = {
51
+ doc: IDENTITY_DOC,
52
+ schema: IDENTITY_SCHEMA,
53
+ query: IDENTITY_QUERY,
54
+ };
55
+
56
+ /**
57
+ * Get codecs for a backend, falling back to identity.
58
+ */
59
+ export function getAdapterCodecs(adapterId: string): AdapterCodecs {
60
+ return ADAPTER_CODECS[adapterId] ?? DEFAULT_CODECS;
61
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Turbopuffer backend codecs.
3
+ *
4
+ * Turbopuffer constraints:
5
+ * - Exactly one ANN vector field named "vector" per namespace.
6
+ *
7
+ * Memory model:
8
+ * - IndexMemoryRecord has modality-specific vectors: tvec, ivec, avec, vvec.
9
+ *
10
+ * Mapping:
11
+ * - tvec (text embedding) → vector
12
+ * - ivec/avec/vvec are dropped (not indexed in Turbopuffer)
13
+ */
14
+
15
+ import type { Codec } from "@kernl-sdk/shared/lib";
16
+ import type {
17
+ FieldSchema,
18
+ SearchQuery,
19
+ RankingSignal,
20
+ UnknownDocument,
21
+ } from "@kernl-sdk/retrieval";
22
+
23
+ import type { IndexMemoryRecord } from "../types";
24
+
25
+ /**
26
+ * Turbopuffer document codec.
27
+ *
28
+ * Maps tvec → vector, drops ivec/avec/vvec.
29
+ */
30
+ export const TPUF_DOC: Codec<IndexMemoryRecord, UnknownDocument> = {
31
+ encode(doc: IndexMemoryRecord): UnknownDocument {
32
+ const { tvec, ivec, avec, vvec, metadata, ...rest } = doc;
33
+ const row: UnknownDocument = {
34
+ ...rest,
35
+ metadata: metadata as UnknownDocument["metadata"], // metadata is JSONObject | null, cast to FieldValue for UnknownDocument
36
+ };
37
+ if (tvec) row.vector = tvec;
38
+ return row;
39
+ },
40
+
41
+ decode(row: UnknownDocument): IndexMemoryRecord {
42
+ const { vector, ...rest } = row;
43
+ return {
44
+ ...(rest as unknown as IndexMemoryRecord),
45
+ tvec: Array.isArray(vector) ? (vector as number[]) : undefined,
46
+ };
47
+ },
48
+ };
49
+
50
+ /**
51
+ * Turbopuffer schema codec.
52
+ *
53
+ * Maps tvec → vector, drops ivec/avec/vvec fields.
54
+ */
55
+ export const TPUF_SCHEMA: Codec<
56
+ Record<string, FieldSchema>,
57
+ Record<string, FieldSchema>
58
+ > = {
59
+ encode(schema: Record<string, FieldSchema>): Record<string, FieldSchema> {
60
+ const result: Record<string, FieldSchema> = {};
61
+ for (const [name, field] of Object.entries(schema)) {
62
+ if (name === "tvec") {
63
+ result.vector = field;
64
+ } else if (name === "ivec" || name === "avec" || name === "vvec") {
65
+ continue;
66
+ } else {
67
+ result[name] = field;
68
+ }
69
+ }
70
+ return result;
71
+ },
72
+
73
+ decode(): Record<string, FieldSchema> {
74
+ throw new Error("TPUF_SCHEMA.decode not implemented");
75
+ },
76
+ };
77
+
78
+ /**
79
+ * Turbopuffer query codec.
80
+ *
81
+ * Maps tvec → vector in query signals, drops ivec/avec/vvec.
82
+ * Operates on normalized SearchQuery (after planQuery).
83
+ *
84
+ * Defaults include to true so memory searches return all document attributes.
85
+ */
86
+ export const TPUF_QUERY: Codec<SearchQuery, SearchQuery> = {
87
+ encode(input: SearchQuery): SearchQuery {
88
+ // default include to true for memory search (tpuf only returns id + score without it)
89
+ const include = input.include ?? true;
90
+
91
+ if (!input.query) {
92
+ return { ...input, include };
93
+ }
94
+
95
+ const signals = input.query.map((signal: RankingSignal) => {
96
+ const { tvec, ivec, avec, vvec, ...rest } = signal as RankingSignal & {
97
+ tvec?: number[];
98
+ ivec?: number[];
99
+ avec?: number[];
100
+ vvec?: number[];
101
+ };
102
+
103
+ if (tvec) {
104
+ return { ...rest, vector: tvec };
105
+ }
106
+ return rest;
107
+ });
108
+
109
+ return { ...input, query: signals, include };
110
+ },
111
+
112
+ decode(): SearchQuery {
113
+ throw new Error("TPUF_QUERY.decode not implemented");
114
+ },
115
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * MemoryByte encoder - converts MemoryByte to IndexableByte with embeddings.
3
+ */
4
+
5
+ import type { EmbeddingModel } from "@kernl-sdk/protocol";
6
+
7
+ import type { MemoryByte, IndexableByte, MemoryByteCodec } from "./types";
8
+
9
+ /**
10
+ * Encoder that converts MemoryByte to IndexableByte.
11
+ *
12
+ * Extracts canonical text from content and computes embeddings.
13
+ */
14
+ export class MemoryByteEncoder implements MemoryByteCodec {
15
+ private readonly embedder: EmbeddingModel<string>;
16
+
17
+ constructor(embedder: EmbeddingModel<string>) {
18
+ this.embedder = embedder;
19
+ }
20
+
21
+ /**
22
+ * Encode a MemoryByte to IndexableByte.
23
+ * Extracts text and computes embeddings for each modality.
24
+ */
25
+ async encode(byte: MemoryByte): Promise<IndexableByte> {
26
+ const text = byte.text;
27
+ const tvec = text ? await this.embed(text) : undefined;
28
+
29
+ // TODO: embed other modalities (image, audio, video)
30
+ // const ivec = byte.image ? await this.embedImage(byte.image) : undefined;
31
+ // const avec = byte.audio ? await this.embedAudio(byte.audio) : undefined;
32
+ // const vvec = byte.video ? await this.embedVideo(byte.video) : undefined;
33
+
34
+ return {
35
+ text,
36
+ tvec,
37
+ metadata: byte.object ?? null,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Decode is not implemented - IndexableByte cannot be converted back to MemoryByte.
43
+ */
44
+ async decode(_indexable: IndexableByte): Promise<MemoryByte> {
45
+ throw new Error("MemoryByteEncoder.decode not implemented");
46
+ }
47
+
48
+ /**
49
+ * Embed a text string.
50
+ * Exposed for query embedding.
51
+ */
52
+ async embed(text: string): Promise<number[]> {
53
+ const result = await this.embedder.embed({ values: [text] });
54
+ return result.embeddings[0] ?? [];
55
+ }
56
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Memory index handle with lazy initialization.
3
+ */
4
+
5
+ import {
6
+ planQuery,
7
+ type SearchIndex,
8
+ type IndexHandle,
9
+ type QueryInput,
10
+ type SearchHit,
11
+ type DocumentPatch,
12
+ type UpsertResult,
13
+ type PatchResult,
14
+ type DeleteResult,
15
+ type FieldSchema,
16
+ type SearchCapabilities,
17
+ type UnknownDocument,
18
+ } from "@kernl-sdk/retrieval";
19
+
20
+ import { getAdapterCodecs, type AdapterCodecs } from "./codecs";
21
+ import type { IndexMemoryRecord } from "./types";
22
+
23
+ /**
24
+ * Configuration for MemoryIndexHandle.
25
+ */
26
+ export interface MemoryIndexHandleConfig {
27
+ /**
28
+ * The underlying search index backend.
29
+ */
30
+ index: SearchIndex;
31
+
32
+ /**
33
+ * Index identifier (e.g., "kernl_memories_index").
34
+ */
35
+ indexId: string;
36
+
37
+ /**
38
+ * Field schema for the memory index.
39
+ */
40
+ schema: Record<string, FieldSchema>;
41
+
42
+ /**
43
+ * Backend-specific options passed to SearchIndex.createIndex().
44
+ */
45
+ providerOptions?: Record<string, unknown>;
46
+ }
47
+
48
+ /**
49
+ * Domain-aware index handle for memory records with lazy initialization.
50
+ *
51
+ * - Wraps a SearchIndex and ensures the memory index is created before any operation.
52
+ * - Normalizes the idiosyncrasies of search adapters (capabilities, weird rules, ... - e.g. tpuf requires vector fields named
53
+ * literally 'vector' - dumb shit like this..)
54
+ */
55
+ export class MemoryIndexHandle implements IndexHandle<IndexMemoryRecord> {
56
+ readonly id: string;
57
+
58
+ private readonly index: SearchIndex;
59
+ private readonly schema: Record<string, FieldSchema>;
60
+ private readonly caps: SearchCapabilities;
61
+ private readonly codecs: AdapterCodecs;
62
+ private readonly providerOptions?: Record<string, unknown>;
63
+
64
+ private initPromise: Promise<void> | null = null;
65
+
66
+ constructor(config: MemoryIndexHandleConfig) {
67
+ this.index = config.index;
68
+ this.id = config.indexId;
69
+ this.schema = config.schema;
70
+ this.caps = this.index.capabilities();
71
+ this.codecs = getAdapterCodecs(this.index.id);
72
+ this.providerOptions = config.providerOptions;
73
+ }
74
+
75
+ /**
76
+ * Ensure memory index exists (lazy initialization).
77
+ *
78
+ * Safe to call multiple times - initialization only runs once.
79
+ */
80
+ private async ensureInit(): Promise<void> {
81
+ if (!this.initPromise) {
82
+ this.initPromise = this.createIndex().catch((err) => {
83
+ this.initPromise = null;
84
+ throw err;
85
+ });
86
+ }
87
+ await this.initPromise;
88
+ }
89
+
90
+ /**
91
+ * Create the memory index if it doesn't exist.
92
+ */
93
+ private async createIndex(): Promise<void> {
94
+ try {
95
+ await this.index.createIndex({
96
+ id: this.id,
97
+ schema: this.codecs.schema.encode(this.schema),
98
+ providerOptions: this.providerOptions,
99
+ });
100
+ } catch (err: any) {
101
+ // (TODO): we should probably enforce a stricter contract w/ tighter error types
102
+ //
103
+ // Ignore "already exists" errors
104
+ if (
105
+ err.message?.includes("already exists") ||
106
+ err.message?.includes("AlreadyExists")
107
+ ) {
108
+ return;
109
+ }
110
+ throw err;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Search for memory records matching the query.
116
+ *
117
+ * Adapts the query to backend capabilities, degrading gracefully
118
+ * when hybrid or multi-signal queries aren't supported.
119
+ */
120
+ async query(input: QueryInput): Promise<SearchHit<IndexMemoryRecord>[]> {
121
+ const { input: planned } = planQuery(input, this.caps);
122
+ const q = this.codecs.query.encode(planned);
123
+ const handle = await this.handle();
124
+ const hits = await handle.query(q);
125
+
126
+ // decode hits back to IndexMemoryRecord format
127
+ return hits.map((hit) => ({
128
+ ...hit,
129
+ document: hit.document
130
+ ? this.codecs.doc.decode(hit.document as UnknownDocument)
131
+ : undefined,
132
+ }));
133
+ }
134
+
135
+ /**
136
+ * Insert or update memory records in the index.
137
+ */
138
+ async upsert(
139
+ docs: IndexMemoryRecord | IndexMemoryRecord[],
140
+ ): Promise<UpsertResult> {
141
+ const arr = Array.isArray(docs) ? docs : [docs];
142
+ const encoded = arr.map((doc) => this.codecs.doc.encode(doc));
143
+ const handle = await this.handle();
144
+ return handle.upsert(encoded);
145
+ }
146
+
147
+ /**
148
+ * Partially update memory records without re-indexing vectors.
149
+ *
150
+ * Note: Patches don't include vector fields so we cast directly.
151
+ * Metadata field type mismatch (JSONObject vs FieldValue) is handled at runtime.
152
+ */
153
+ async patch(
154
+ patches:
155
+ | DocumentPatch<IndexMemoryRecord>
156
+ | DocumentPatch<IndexMemoryRecord>[],
157
+ ): Promise<PatchResult> {
158
+ const handle = await this.handle();
159
+ return handle.patch(patches as DocumentPatch<UnknownDocument>[]);
160
+ }
161
+
162
+ /**
163
+ * Remove memory records from the index.
164
+ */
165
+ async delete(ids: string | string[]): Promise<DeleteResult> {
166
+ const handle = await this.handle();
167
+ return handle.delete(ids);
168
+ }
169
+
170
+ /**
171
+ * Add a new field to the index schema.
172
+ *
173
+ * @throws Always throws - dynamic schema modification not supported
174
+ */
175
+ async addField(_field: string, _schema: FieldSchema): Promise<void> {
176
+ throw new Error("addField not supported for MemoryIndexHandle");
177
+ }
178
+
179
+ /**
180
+ * Get an initialized underlying index handle.
181
+ *
182
+ * Returns a handle typed for UnknownDocument since we encode/decode
183
+ * through the adapter codecs for backend-specific field mapping.
184
+ */
185
+ private async handle(): Promise<IndexHandle<UnknownDocument>> {
186
+ await this.ensureInit();
187
+ return this.index.index<UnknownDocument>(this.id);
188
+ }
189
+ }