@kernl-sdk/pg 0.1.10 → 0.1.12

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 (153) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/.turbo/turbo-check-types.log +36 -0
  3. package/CHANGELOG.md +41 -0
  4. package/README.md +124 -0
  5. package/dist/__tests__/integration.test.js +81 -1
  6. package/dist/__tests__/memory-integration.test.d.ts +2 -0
  7. package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
  8. package/dist/__tests__/memory-integration.test.js +287 -0
  9. package/dist/__tests__/memory.test.d.ts +2 -0
  10. package/dist/__tests__/memory.test.d.ts.map +1 -0
  11. package/dist/__tests__/memory.test.js +357 -0
  12. package/dist/index.d.ts +5 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -3
  15. package/dist/memory/sql.d.ts +30 -0
  16. package/dist/memory/sql.d.ts.map +1 -0
  17. package/dist/memory/sql.js +100 -0
  18. package/dist/memory/store.d.ts +41 -0
  19. package/dist/memory/store.d.ts.map +1 -0
  20. package/dist/memory/store.js +114 -0
  21. package/dist/migrations.d.ts +1 -1
  22. package/dist/migrations.d.ts.map +1 -1
  23. package/dist/migrations.js +9 -3
  24. package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
  25. package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
  26. package/dist/pgvector/__tests__/handle.test.js +277 -0
  27. package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
  28. package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
  29. package/dist/pgvector/__tests__/hit.test.js +134 -0
  30. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
  31. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
  32. package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
  33. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
  34. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
  35. package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
  36. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
  37. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  38. package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
  39. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  40. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  41. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
  42. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
  43. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
  44. package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
  45. package/dist/pgvector/__tests__/search.test.d.ts +2 -0
  46. package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
  47. package/dist/pgvector/__tests__/search.test.js +279 -0
  48. package/dist/pgvector/handle.d.ts +60 -0
  49. package/dist/pgvector/handle.d.ts.map +1 -0
  50. package/dist/pgvector/handle.js +213 -0
  51. package/dist/pgvector/hit.d.ts +10 -0
  52. package/dist/pgvector/hit.d.ts.map +1 -0
  53. package/dist/pgvector/hit.js +44 -0
  54. package/dist/pgvector/index.d.ts +7 -0
  55. package/dist/pgvector/index.d.ts.map +1 -0
  56. package/dist/pgvector/index.js +5 -0
  57. package/dist/pgvector/search.d.ts +60 -0
  58. package/dist/pgvector/search.d.ts.map +1 -0
  59. package/dist/pgvector/search.js +227 -0
  60. package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
  61. package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
  62. package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
  63. package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
  64. package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
  65. package/dist/pgvector/sql/__tests__/order.test.js +218 -0
  66. package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
  67. package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
  68. package/dist/pgvector/sql/__tests__/query.test.js +392 -0
  69. package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
  70. package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
  71. package/dist/pgvector/sql/__tests__/select.test.js +293 -0
  72. package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
  73. package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
  74. package/dist/pgvector/sql/__tests__/where.test.js +488 -0
  75. package/dist/pgvector/sql/index.d.ts +7 -0
  76. package/dist/pgvector/sql/index.d.ts.map +1 -0
  77. package/dist/pgvector/sql/index.js +6 -0
  78. package/dist/pgvector/sql/limit.d.ts +8 -0
  79. package/dist/pgvector/sql/limit.d.ts.map +1 -0
  80. package/dist/pgvector/sql/limit.js +20 -0
  81. package/dist/pgvector/sql/order.d.ts +9 -0
  82. package/dist/pgvector/sql/order.d.ts.map +1 -0
  83. package/dist/pgvector/sql/order.js +47 -0
  84. package/dist/pgvector/sql/query.d.ts +46 -0
  85. package/dist/pgvector/sql/query.d.ts.map +1 -0
  86. package/dist/pgvector/sql/query.js +54 -0
  87. package/dist/pgvector/sql/schema.d.ts +16 -0
  88. package/dist/pgvector/sql/schema.d.ts.map +1 -0
  89. package/dist/pgvector/sql/schema.js +47 -0
  90. package/dist/pgvector/sql/select.d.ts +11 -0
  91. package/dist/pgvector/sql/select.d.ts.map +1 -0
  92. package/dist/pgvector/sql/select.js +87 -0
  93. package/dist/pgvector/sql/where.d.ts +8 -0
  94. package/dist/pgvector/sql/where.d.ts.map +1 -0
  95. package/dist/pgvector/sql/where.js +137 -0
  96. package/dist/pgvector/types.d.ts +20 -0
  97. package/dist/pgvector/types.d.ts.map +1 -0
  98. package/dist/pgvector/types.js +1 -0
  99. package/dist/pgvector/utils.d.ts +18 -0
  100. package/dist/pgvector/utils.d.ts.map +1 -0
  101. package/dist/pgvector/utils.js +22 -0
  102. package/dist/postgres.d.ts +19 -26
  103. package/dist/postgres.d.ts.map +1 -1
  104. package/dist/postgres.js +15 -27
  105. package/dist/storage.d.ts +62 -0
  106. package/dist/storage.d.ts.map +1 -1
  107. package/dist/storage.js +55 -10
  108. package/dist/thread/sql.d.ts +38 -0
  109. package/dist/thread/sql.d.ts.map +1 -0
  110. package/dist/thread/sql.js +112 -0
  111. package/dist/thread/store.d.ts +7 -3
  112. package/dist/thread/store.d.ts.map +1 -1
  113. package/dist/thread/store.js +46 -105
  114. package/package.json +8 -5
  115. package/src/__tests__/integration.test.ts +114 -15
  116. package/src/__tests__/memory-integration.test.ts +355 -0
  117. package/src/__tests__/memory.test.ts +428 -0
  118. package/src/index.ts +19 -3
  119. package/src/memory/sql.ts +141 -0
  120. package/src/memory/store.ts +166 -0
  121. package/src/migrations.ts +13 -3
  122. package/src/pgvector/README.md +50 -0
  123. package/src/pgvector/__tests__/handle.test.ts +335 -0
  124. package/src/pgvector/__tests__/hit.test.ts +165 -0
  125. package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
  126. package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
  127. package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
  128. package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
  129. package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
  130. package/src/pgvector/__tests__/search.test.ts +366 -0
  131. package/src/pgvector/handle.ts +285 -0
  132. package/src/pgvector/hit.ts +56 -0
  133. package/src/pgvector/index.ts +7 -0
  134. package/src/pgvector/search.ts +330 -0
  135. package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
  136. package/src/pgvector/sql/__tests__/order.test.ts +248 -0
  137. package/src/pgvector/sql/__tests__/query.test.ts +548 -0
  138. package/src/pgvector/sql/__tests__/select.test.ts +367 -0
  139. package/src/pgvector/sql/__tests__/where.test.ts +554 -0
  140. package/src/pgvector/sql/index.ts +14 -0
  141. package/src/pgvector/sql/limit.ts +29 -0
  142. package/src/pgvector/sql/order.ts +55 -0
  143. package/src/pgvector/sql/query.ts +112 -0
  144. package/src/pgvector/sql/schema.ts +61 -0
  145. package/src/pgvector/sql/select.ts +100 -0
  146. package/src/pgvector/sql/where.ts +152 -0
  147. package/src/pgvector/types.ts +21 -0
  148. package/src/pgvector/utils.ts +24 -0
  149. package/src/postgres.ts +31 -33
  150. package/src/storage.ts +102 -11
  151. package/src/thread/sql.ts +159 -0
  152. package/src/thread/store.ts +58 -127
  153. package/tsconfig.tsbuildinfo +1 -0
package/src/storage.ts CHANGED
@@ -2,20 +2,73 @@ import assert from "assert";
2
2
  import type { Pool, PoolClient } from "pg";
3
3
 
4
4
  /* workspace */
5
- import type { Table, Column, IndexConstraint } from "@kernl-sdk/storage";
6
- import { SCHEMA_NAME, TABLE_MIGRATIONS } from "@kernl-sdk/storage";
7
5
  import type {
8
6
  AgentRegistry,
9
7
  ModelRegistry,
10
8
  KernlStorage,
11
9
  Transaction,
12
10
  } from "kernl";
11
+ import type { Table, Column, IndexConstraint } from "@kernl-sdk/storage";
12
+ import { KERNL_SCHEMA_NAME, TABLE_MIGRATIONS } from "@kernl-sdk/storage";
13
13
  import { UnimplementedError } from "@kernl-sdk/shared/lib";
14
14
 
15
15
  /* pg */
16
16
  import { PGThreadStore } from "./thread/store";
17
+ import { PGMemoryStore } from "./memory/store";
18
+ import { MIGRATIONS } from "./migrations";
17
19
  import { SQL_IDENTIFIER_REGEX } from "./sql";
18
- import { migrations } from "./migrations";
20
+
21
+ /**
22
+ * Vector similarity metric for pgvector.
23
+ */
24
+ export type VectorSimilarity = "cosine" | "euclidean" | "dot_product";
25
+
26
+ /**
27
+ * pgvector configuration options.
28
+ */
29
+ export interface PGVectorConfig {
30
+ /**
31
+ * Vector dimensions.
32
+ * @default 1536 (OpenAI text-embedding-3-small)
33
+ */
34
+ dimensions?: number;
35
+
36
+ /**
37
+ * Distance metric for similarity search.
38
+ * @default "cosine"
39
+ */
40
+ similarity?: VectorSimilarity;
41
+ }
42
+
43
+ /**
44
+ * Resolved vector configuration with defaults applied.
45
+ */
46
+ export interface ResolvedVectorConfig {
47
+ dimensions: number;
48
+ similarity: VectorSimilarity;
49
+ }
50
+
51
+ /**
52
+ * Default vector configuration.
53
+ */
54
+ export const DEFAULT_VECTOR_CONFIG: ResolvedVectorConfig = {
55
+ dimensions: 1536,
56
+ similarity: "cosine",
57
+ };
58
+
59
+ /**
60
+ * Resolve vector config, applying defaults.
61
+ */
62
+ export function resolveVectorConfig(
63
+ config: boolean | PGVectorConfig | undefined,
64
+ ): ResolvedVectorConfig | undefined {
65
+ if (!config) return undefined;
66
+ if (config === true) return DEFAULT_VECTOR_CONFIG;
67
+ return {
68
+ dimensions: config.dimensions ?? DEFAULT_VECTOR_CONFIG.dimensions,
69
+ similarity: config.similarity ?? DEFAULT_VECTOR_CONFIG.similarity,
70
+ };
71
+ }
19
72
 
20
73
  /**
21
74
  * PostgreSQL storage configuration.
@@ -25,19 +78,57 @@ export interface PGStorageConfig {
25
78
  * Pool instance for database connections.
26
79
  */
27
80
  pool: Pool;
81
+
82
+ /**
83
+ * Enable pgvector support for semantic search.
84
+ *
85
+ * - `true`: Use default config (1536 dimensions, cosine similarity)
86
+ * - `PGVectorConfig`: Custom dimensions and similarity metric
87
+ *
88
+ * Requires pgvector extension to be installed by superuser:
89
+ * ```sql
90
+ * CREATE EXTENSION IF NOT EXISTS vector;
91
+ * ```
92
+ */
93
+ vector?: boolean | PGVectorConfig;
28
94
  }
29
95
 
30
96
  /**
31
97
  * PostgreSQL storage adapter.
98
+ *
99
+ * Storage is lazily initialized on first use via `ensureInit()`. This means
100
+ * callers don't need to explicitly call `init()` - it happens automatically.
101
+ *
102
+ * NOTE: If the number of store methods grows significantly, consider replacing
103
+ * the manual `ensureInit()` calls with a Proxy-based wrapper for foolproof
104
+ * auto-initialization.
32
105
  */
33
106
  export class PGStorage implements KernlStorage {
34
107
  private pool: Pool;
108
+ private initPromise: Promise<void> | null = null;
35
109
 
36
110
  threads: PGThreadStore;
111
+ memories: PGMemoryStore;
37
112
 
38
113
  constructor(config: PGStorageConfig) {
39
114
  this.pool = config.pool;
40
- this.threads = new PGThreadStore(this.pool);
115
+ this.threads = new PGThreadStore(this.pool, () => this.ensureInit());
116
+ this.memories = new PGMemoryStore(this.pool, () => this.ensureInit());
117
+ }
118
+
119
+ /**
120
+ * Ensure storage is initialized before any operation.
121
+ *
122
+ * Safe to call multiple times - initialization only runs once.
123
+ */
124
+ private async ensureInit(): Promise<void> {
125
+ if (!this.initPromise) {
126
+ this.initPromise = this.init().catch((err) => {
127
+ this.initPromise = null;
128
+ throw err;
129
+ });
130
+ }
131
+ return this.initPromise;
41
132
  }
42
133
 
43
134
  /**
@@ -58,7 +149,7 @@ export class PGStorage implements KernlStorage {
58
149
  * Initialize the storage backend.
59
150
  */
60
151
  async init(): Promise<void> {
61
- await this.pool.query(`CREATE SCHEMA IF NOT EXISTS "${SCHEMA_NAME}"`);
152
+ await this.pool.query(`CREATE SCHEMA IF NOT EXISTS "${KERNL_SCHEMA_NAME}"`);
62
153
  await this.createTable(TABLE_MIGRATIONS);
63
154
  await this.migrate();
64
155
  }
@@ -80,12 +171,12 @@ export class PGStorage implements KernlStorage {
80
171
 
81
172
  // read applied migration IDs
82
173
  const result = await client.query<{ id: string }>(
83
- `SELECT id FROM "${SCHEMA_NAME}".migrations ORDER BY applied_at ASC`,
174
+ `SELECT id FROM "${KERNL_SCHEMA_NAME}".migrations ORDER BY applied_at ASC`,
84
175
  );
85
176
  const applied = new Set(result.rows.map((row) => row.id));
86
177
 
87
178
  // filter pending migrations
88
- const pending = migrations.filter((m) => !applied.has(m.id));
179
+ const pending = MIGRATIONS.filter((m) => !applied.has(m.id));
89
180
  if (pending.length === 0) {
90
181
  await client.query("COMMIT");
91
182
  return;
@@ -100,7 +191,7 @@ export class PGStorage implements KernlStorage {
100
191
  },
101
192
  });
102
193
  await client.query(
103
- `INSERT INTO "${SCHEMA_NAME}".migrations (id, applied_at) VALUES ($1, $2)`,
194
+ `INSERT INTO "${KERNL_SCHEMA_NAME}".migrations (id, applied_at) VALUES ($1, $2)`,
104
195
  [migration.id, Date.now()],
105
196
  );
106
197
  }
@@ -151,7 +242,7 @@ export class PGStorage implements KernlStorage {
151
242
 
152
243
  // foreign key reference
153
244
  if (col._fk) {
154
- let ref = `REFERENCES "${SCHEMA_NAME}"."${col._fk.table}" ("${col._fk.column}")`;
245
+ let ref = `REFERENCES "${KERNL_SCHEMA_NAME}"."${col._fk.table}" ("${col._fk.column}")`;
155
246
  if (col._onDelete) {
156
247
  ref += ` ON DELETE ${col._onDelete}`;
157
248
  }
@@ -210,7 +301,7 @@ export class PGStorage implements KernlStorage {
210
301
  const constraints = [...columns, ...tableConstraints];
211
302
 
212
303
  const sql = `
213
- CREATE TABLE IF NOT EXISTS "${SCHEMA_NAME}"."${table.name}" (
304
+ CREATE TABLE IF NOT EXISTS "${KERNL_SCHEMA_NAME}"."${table.name}" (
214
305
  ${constraints.join(",\n ")}
215
306
  )
216
307
  `.trim();
@@ -262,7 +353,7 @@ export class PGStorage implements KernlStorage {
262
353
 
263
354
  const sql = `
264
355
  CREATE ${uniqueKeyword} INDEX IF NOT EXISTS "${indexName}"
265
- ON "${SCHEMA_NAME}"."${tableName}" (${columns})
356
+ ON "${KERNL_SCHEMA_NAME}"."${tableName}" (${columns})
266
357
  `.trim();
267
358
 
268
359
  await client.query(sql);
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Thread SQL conversion codecs.
3
+ *
4
+ * TODO: generalize object -> SQL conversion into a shared utility
5
+ */
6
+
7
+ import type { Codec } from "@kernl-sdk/shared/lib";
8
+ import type { ThreadFilter, ThreadUpdate, SortOrder } from "kernl";
9
+
10
+ export interface SQLClause {
11
+ sql: string;
12
+ params: unknown[];
13
+ }
14
+
15
+ export interface WhereInput {
16
+ filter?: ThreadFilter;
17
+ startIdx: number;
18
+ }
19
+
20
+ /**
21
+ * Encode ThreadFilter to SQL WHERE clause.
22
+ */
23
+ export const SQL_WHERE: Codec<WhereInput, SQLClause> = {
24
+ encode({ filter, startIdx }) {
25
+ if (!filter) {
26
+ return { sql: "", params: [] };
27
+ }
28
+
29
+ const conditions: string[] = [];
30
+ const params: unknown[] = [];
31
+ let idx = startIdx;
32
+
33
+ if (filter.namespace !== undefined) {
34
+ conditions.push(`namespace = $${idx++}`);
35
+ params.push(filter.namespace);
36
+ }
37
+
38
+ if (filter.state !== undefined) {
39
+ if (Array.isArray(filter.state)) {
40
+ conditions.push(`state = ANY($${idx++})`);
41
+ params.push(filter.state);
42
+ } else {
43
+ conditions.push(`state = $${idx++}`);
44
+ params.push(filter.state);
45
+ }
46
+ }
47
+
48
+ if (filter.agentId !== undefined) {
49
+ conditions.push(`agent_id = $${idx++}`);
50
+ params.push(filter.agentId);
51
+ }
52
+
53
+ if (filter.parentTaskId !== undefined) {
54
+ conditions.push(`parent_task_id = $${idx++}`);
55
+ params.push(filter.parentTaskId);
56
+ }
57
+
58
+ if (filter.createdAfter !== undefined) {
59
+ conditions.push(`created_at > $${idx++}`);
60
+ params.push(filter.createdAfter.getTime());
61
+ }
62
+
63
+ if (filter.createdBefore !== undefined) {
64
+ conditions.push(`created_at < $${idx++}`);
65
+ params.push(filter.createdBefore.getTime());
66
+ }
67
+
68
+ return {
69
+ sql: conditions.length > 0 ? conditions.join(" AND ") : "",
70
+ params,
71
+ };
72
+ },
73
+
74
+ decode() {
75
+ throw new Error("SQL_WHERE.decode not implemented");
76
+ },
77
+ };
78
+
79
+ export interface OrderInput {
80
+ order?: {
81
+ createdAt?: SortOrder;
82
+ updatedAt?: SortOrder;
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Encode order options to SQL ORDER BY clause.
88
+ */
89
+ export const SQL_ORDER: Codec<OrderInput, string> = {
90
+ encode({ order }) {
91
+ const clauses: string[] = [];
92
+
93
+ if (order?.createdAt) {
94
+ clauses.push(`created_at ${order.createdAt.toUpperCase()}`);
95
+ }
96
+ if (order?.updatedAt) {
97
+ clauses.push(`updated_at ${order.updatedAt.toUpperCase()}`);
98
+ }
99
+
100
+ if (clauses.length === 0) {
101
+ return "created_at DESC";
102
+ }
103
+
104
+ return clauses.join(", ");
105
+ },
106
+
107
+ decode() {
108
+ throw new Error("SQL_ORDER.decode not implemented");
109
+ },
110
+ };
111
+
112
+ export interface UpdateInput {
113
+ patch: ThreadUpdate;
114
+ startIdx: number;
115
+ }
116
+
117
+ /**
118
+ * Encode ThreadUpdate to SQL SET clause.
119
+ */
120
+ export const SQL_UPDATE: Codec<UpdateInput, SQLClause> = {
121
+ encode({ patch, startIdx }) {
122
+ const sets: string[] = [];
123
+ const params: unknown[] = [];
124
+ let idx = startIdx;
125
+
126
+ if (patch.tick !== undefined) {
127
+ sets.push(`tick = $${idx++}`);
128
+ params.push(patch.tick);
129
+ }
130
+
131
+ if (patch.state !== undefined) {
132
+ sets.push(`state = $${idx++}`);
133
+ params.push(patch.state);
134
+ }
135
+
136
+ if (patch.context !== undefined) {
137
+ sets.push(`context = $${idx++}`);
138
+ params.push(JSON.stringify(patch.context.context));
139
+ }
140
+
141
+ if (patch.metadata !== undefined) {
142
+ sets.push(`metadata = $${idx++}`);
143
+ params.push(patch.metadata ? JSON.stringify(patch.metadata) : null);
144
+ }
145
+
146
+ // always update updated_at
147
+ sets.push(`updated_at = $${idx++}`);
148
+ params.push(Date.now());
149
+
150
+ return {
151
+ sql: sets.join(", "),
152
+ params,
153
+ };
154
+ },
155
+
156
+ decode() {
157
+ throw new Error("SQL_UPDATE.decode not implemented");
158
+ },
159
+ };
@@ -1,14 +1,6 @@
1
1
  import assert from "assert";
2
2
  import type { Pool, PoolClient } from "pg";
3
3
 
4
- import {
5
- SCHEMA_NAME,
6
- NewThreadCodec,
7
- ThreadEventRecordCodec,
8
- type ThreadRecord,
9
- type ThreadEventRecord,
10
- } from "@kernl-sdk/storage";
11
- import { Thread, type ThreadEvent } from "kernl/internal";
12
4
  import {
13
5
  Context,
14
6
  type AgentRegistry,
@@ -20,16 +12,31 @@ import {
20
12
  type ThreadListOptions,
21
13
  type ThreadHistoryOptions,
22
14
  } from "kernl";
15
+ import { Thread, type ThreadEvent } from "kernl/internal";
16
+ import {
17
+ KERNL_SCHEMA_NAME,
18
+ NewThreadCodec,
19
+ ThreadEventRecordCodec,
20
+ type ThreadRecord,
21
+ type ThreadEventRecord,
22
+ } from "@kernl-sdk/storage";
23
+
24
+ import { SQL_WHERE, SQL_ORDER, SQL_UPDATE } from "./sql";
23
25
 
24
26
  /**
25
27
  * PostgreSQL Thread store implementation.
28
+ *
29
+ * IMPORTANT: All async methods must call `await this.ensureInit()` before
30
+ * any database operations. This ensures schema/tables exist.
26
31
  */
27
32
  export class PGThreadStore implements ThreadStore {
28
33
  private db: Pool | PoolClient;
29
34
  private registries: { agents: AgentRegistry; models: ModelRegistry } | null;
35
+ private ensureInit: () => Promise<void>;
30
36
 
31
- constructor(db: Pool | PoolClient) {
37
+ constructor(db: Pool | PoolClient, ensureInit: () => Promise<void>) {
32
38
  this.db = db;
39
+ this.ensureInit = ensureInit;
33
40
  this.registries = null;
34
41
  }
35
42
 
@@ -46,6 +53,8 @@ export class PGThreadStore implements ThreadStore {
46
53
  * Get a thread by id.
47
54
  */
48
55
  async get(tid: string, include?: ThreadInclude): Promise<Thread | null> {
56
+ await this.ensureInit();
57
+
49
58
  // JOIN with thread_events if include.history
50
59
  if (include?.history) {
51
60
  const opts =
@@ -78,8 +87,8 @@ export class PGThreadStore implements ThreadStore {
78
87
  e.timestamp,
79
88
  e.data,
80
89
  e.metadata as event_metadata
81
- FROM ${SCHEMA_NAME}.threads t
82
- LEFT JOIN ${SCHEMA_NAME}.thread_events e ON t.id = e.tid${eventFilter}
90
+ FROM ${KERNL_SCHEMA_NAME}.threads t
91
+ LEFT JOIN ${KERNL_SCHEMA_NAME}.thread_events e ON t.id = e.tid${eventFilter}
83
92
  WHERE t.id = $1
84
93
  ORDER BY e.seq ${order.toUpperCase()}
85
94
  ${limit}
@@ -131,7 +140,7 @@ export class PGThreadStore implements ThreadStore {
131
140
 
132
141
  // simple query without events
133
142
  const result = await this.db.query<ThreadRecord>(
134
- `SELECT * FROM ${SCHEMA_NAME}.threads WHERE id = $1`,
143
+ `SELECT * FROM ${KERNL_SCHEMA_NAME}.threads WHERE id = $1`,
135
144
  [tid],
136
145
  );
137
146
 
@@ -150,88 +159,30 @@ export class PGThreadStore implements ThreadStore {
150
159
  * List threads matching the filter.
151
160
  */
152
161
  async list(options?: ThreadListOptions): Promise<Thread[]> {
153
- let query = `SELECT * FROM ${SCHEMA_NAME}.threads`;
154
- const values: any[] = [];
155
- let paramIndex = 1;
156
-
157
- // build WHERE clause
158
- const conditions: string[] = [];
159
- if (options?.filter) {
160
- const {
161
- state,
162
- agentId,
163
- parentTaskId,
164
- createdAfter,
165
- createdBefore,
166
- namespace,
167
- } = options.filter;
168
-
169
- if (namespace) {
170
- conditions.push(`namespace = $${paramIndex++}`);
171
- values.push(namespace);
172
- }
162
+ await this.ensureInit();
173
163
 
174
- if (state) {
175
- if (Array.isArray(state)) {
176
- conditions.push(`state = ANY($${paramIndex++})`);
177
- values.push(state);
178
- } else {
179
- conditions.push(`state = $${paramIndex++}`);
180
- values.push(state);
181
- }
182
- }
183
-
184
- if (agentId) {
185
- conditions.push(`agent_id = $${paramIndex++}`);
186
- values.push(agentId);
187
- }
188
-
189
- if (parentTaskId) {
190
- conditions.push(`parent_task_id = $${paramIndex++}`);
191
- values.push(parentTaskId);
192
- }
193
-
194
- if (createdAfter) {
195
- conditions.push(`created_at > $${paramIndex++}`);
196
- values.push(createdAfter.getTime());
197
- }
164
+ const { sql: where, params } = SQL_WHERE.encode({
165
+ filter: options?.filter,
166
+ startIdx: 1,
167
+ });
198
168
 
199
- if (createdBefore) {
200
- conditions.push(`created_at < $${paramIndex++}`);
201
- values.push(createdBefore.getTime());
202
- }
203
- }
169
+ let idx = params.length + 1;
170
+ let query = `SELECT * FROM ${KERNL_SCHEMA_NAME}.threads`;
204
171
 
205
- if (conditions.length > 0) {
206
- query += ` WHERE ${conditions.join(" AND ")}`;
207
- }
208
-
209
- // build ORDER BY clause
210
- const orderClauses: string[] = [];
211
- if (options?.order?.createdAt) {
212
- orderClauses.push(`created_at ${options.order.createdAt.toUpperCase()}`);
213
- }
214
- if (options?.order?.updatedAt) {
215
- orderClauses.push(`updated_at ${options.order.updatedAt.toUpperCase()}`);
216
- }
217
- if (orderClauses.length > 0) {
218
- query += ` ORDER BY ${orderClauses.join(", ")}`;
219
- } else {
220
- // default: most recent first
221
- query += ` ORDER BY created_at DESC`;
222
- }
172
+ if (where) query += ` WHERE ${where}`;
173
+ query += ` ORDER BY ${SQL_ORDER.encode({ order: options?.order })}`;
223
174
 
224
175
  if (options?.limit) {
225
- query += ` LIMIT $${paramIndex++}`;
226
- values.push(options.limit);
176
+ query += ` LIMIT $${idx++}`;
177
+ params.push(options.limit);
227
178
  }
228
179
 
229
180
  if (options?.offset) {
230
- query += ` OFFSET $${paramIndex++}`;
231
- values.push(options.offset);
181
+ query += ` OFFSET $${idx++}`;
182
+ params.push(options.offset);
232
183
  }
233
184
 
234
- const result = await this.db.query<ThreadRecord>(query, values);
185
+ const result = await this.db.query<ThreadRecord>(query, params);
235
186
  return result.rows
236
187
  .map((record) => {
237
188
  try {
@@ -250,10 +201,12 @@ export class PGThreadStore implements ThreadStore {
250
201
  * Insert a new thread into the store.
251
202
  */
252
203
  async insert(thread: NewThread): Promise<Thread> {
204
+ await this.ensureInit();
205
+
253
206
  const record = NewThreadCodec.encode(thread);
254
207
 
255
208
  const result = await this.db.query<ThreadRecord>(
256
- `INSERT INTO ${SCHEMA_NAME}.threads
209
+ `INSERT INTO ${KERNL_SCHEMA_NAME}.threads
257
210
  (id, namespace, agent_id, model, context, tick, state, parent_task_id, metadata, created_at, updated_at)
258
211
  VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9::jsonb, $10, $11)
259
212
  RETURNING *`,
@@ -279,45 +232,18 @@ export class PGThreadStore implements ThreadStore {
279
232
  * Update thread runtime state.
280
233
  */
281
234
  async update(tid: string, patch: ThreadUpdate): Promise<Thread> {
282
- const updates: string[] = [];
283
- const values: any[] = [];
284
- let paramIndex = 1;
235
+ await this.ensureInit();
285
236
 
286
- if (patch.tick !== undefined) {
287
- updates.push(`tick = $${paramIndex++}`);
288
- values.push(patch.tick);
289
- }
290
-
291
- if (patch.state !== undefined) {
292
- updates.push(`state = $${paramIndex++}`);
293
- values.push(patch.state);
294
- }
295
-
296
- if (patch.context !== undefined) {
297
- updates.push(`context = $${paramIndex++}`);
298
- // NOTE: Store the raw context value, not the Context wrapper.
299
- //
300
- // THis may change in the future depending on Context implementation.
301
- values.push(JSON.stringify(patch.context.context));
302
- }
303
-
304
- if (patch.metadata !== undefined) {
305
- updates.push(`metadata = $${paramIndex++}`);
306
- values.push(patch.metadata ? JSON.stringify(patch.metadata) : null);
307
- }
308
-
309
- // always update `updated_at`
310
- updates.push(`updated_at = $${paramIndex++}`);
311
- values.push(Date.now());
312
-
313
- values.push(tid); // WHERE id = $N
237
+ const { sql: updates, params } = SQL_UPDATE.encode({ patch, startIdx: 1 });
238
+ const idx = params.length + 1;
239
+ params.push(tid);
314
240
 
315
241
  const result = await this.db.query<ThreadRecord>(
316
- `UPDATE ${SCHEMA_NAME}.threads
317
- SET ${updates.join(", ")}
318
- WHERE id = $${paramIndex}
242
+ `UPDATE ${KERNL_SCHEMA_NAME}.threads
243
+ SET ${updates}
244
+ WHERE id = $${idx}
319
245
  RETURNING *`,
320
- values,
246
+ params,
321
247
  );
322
248
 
323
249
  return this.hydrate({ record: result.rows[0] });
@@ -327,9 +253,11 @@ export class PGThreadStore implements ThreadStore {
327
253
  * Delete a thread and cascade to thread_events.
328
254
  */
329
255
  async delete(tid: string): Promise<void> {
330
- await this.db.query(`DELETE FROM ${SCHEMA_NAME}.threads WHERE id = $1`, [
331
- tid,
332
- ]);
256
+ await this.ensureInit();
257
+ await this.db.query(
258
+ `DELETE FROM ${KERNL_SCHEMA_NAME}.threads WHERE id = $1`,
259
+ [tid],
260
+ );
333
261
  }
334
262
 
335
263
  /**
@@ -339,7 +267,9 @@ export class PGThreadStore implements ThreadStore {
339
267
  tid: string,
340
268
  opts?: ThreadHistoryOptions,
341
269
  ): Promise<ThreadEvent[]> {
342
- let query = `SELECT * FROM ${SCHEMA_NAME}.thread_events WHERE tid = $1`;
270
+ await this.ensureInit();
271
+
272
+ let query = `SELECT * FROM ${KERNL_SCHEMA_NAME}.thread_events WHERE tid = $1`;
343
273
  const values: any[] = [tid];
344
274
  let paramIndex = 2;
345
275
 
@@ -370,8 +300,7 @@ export class PGThreadStore implements ThreadStore {
370
300
  return result.rows.map((record) =>
371
301
  ThreadEventRecordCodec.decode({
372
302
  ...record,
373
- // Normalize BIGINT (string) to number for zod schema
374
- timestamp: Number(record.timestamp),
303
+ timestamp: Number(record.timestamp), // normalize BIGINT (string) to number for zod schema
375
304
  } as ThreadEventRecord),
376
305
  );
377
306
  }
@@ -388,6 +317,7 @@ export class PGThreadStore implements ThreadStore {
388
317
  */
389
318
  async append(events: ThreadEvent[]): Promise<void> {
390
319
  if (events.length === 0) return;
320
+ await this.ensureInit();
391
321
 
392
322
  const records = events.map((e) => ThreadEventRecordCodec.encode(e));
393
323
 
@@ -412,7 +342,7 @@ export class PGThreadStore implements ThreadStore {
412
342
 
413
343
  // insert with ON CONFLICT DO NOTHING for idempotency
414
344
  await this.db.query(
415
- `INSERT INTO ${SCHEMA_NAME}.thread_events
345
+ `INSERT INTO ${KERNL_SCHEMA_NAME}.thread_events
416
346
  (id, tid, seq, kind, timestamp, data, metadata)
417
347
  VALUES ${placeholders.join(", ")}
418
348
  ON CONFLICT (tid, id) DO NOTHING`,
@@ -434,6 +364,7 @@ export class PGThreadStore implements ThreadStore {
434
364
  const agent = this.registries.agents.get(record.agent_id);
435
365
  const model = this.registries.models.get(record.model);
436
366
 
367
+ // (TODO): we might want to allow this in the future, unclear how it would look though..
437
368
  if (!agent || !model) {
438
369
  throw new Error(
439
370
  `Thread ${record.id} references non-existent agent/model (agent: ${record.agent_id}, model: ${record.model})`,
@@ -0,0 +1 @@
1
+ {"root":["./src/index.ts","./src/migrations.ts","./src/postgres.ts","./src/sql.ts","./src/storage.ts","./src/__tests__/integration.test.ts","./src/__tests__/memory.test.ts","./src/__tests__/thread.test.ts","./src/memory/sql.ts","./src/memory/store.ts","./src/thread/sql.ts","./src/thread/store.ts"],"version":"5.9.2"}