@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.
- package/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-check-types.log +36 -0
- package/CHANGELOG.md +41 -0
- package/README.md +124 -0
- package/dist/__tests__/integration.test.js +81 -1
- package/dist/__tests__/memory-integration.test.d.ts +2 -0
- package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
- package/dist/__tests__/memory-integration.test.js +287 -0
- package/dist/__tests__/memory.test.d.ts +2 -0
- package/dist/__tests__/memory.test.d.ts.map +1 -0
- package/dist/__tests__/memory.test.js +357 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/memory/sql.d.ts +30 -0
- package/dist/memory/sql.d.ts.map +1 -0
- package/dist/memory/sql.js +100 -0
- package/dist/memory/store.d.ts +41 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +114 -0
- package/dist/migrations.d.ts +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +9 -3
- package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
- package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/handle.test.js +277 -0
- package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
- package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/hit.test.js +134 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
- package/dist/pgvector/__tests__/search.test.d.ts +2 -0
- package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/search.test.js +279 -0
- package/dist/pgvector/handle.d.ts +60 -0
- package/dist/pgvector/handle.d.ts.map +1 -0
- package/dist/pgvector/handle.js +213 -0
- package/dist/pgvector/hit.d.ts +10 -0
- package/dist/pgvector/hit.d.ts.map +1 -0
- package/dist/pgvector/hit.js +44 -0
- package/dist/pgvector/index.d.ts +7 -0
- package/dist/pgvector/index.d.ts.map +1 -0
- package/dist/pgvector/index.js +5 -0
- package/dist/pgvector/search.d.ts +60 -0
- package/dist/pgvector/search.d.ts.map +1 -0
- package/dist/pgvector/search.js +227 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/order.test.js +218 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/query.test.js +392 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/select.test.js +293 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/where.test.js +488 -0
- package/dist/pgvector/sql/index.d.ts +7 -0
- package/dist/pgvector/sql/index.d.ts.map +1 -0
- package/dist/pgvector/sql/index.js +6 -0
- package/dist/pgvector/sql/limit.d.ts +8 -0
- package/dist/pgvector/sql/limit.d.ts.map +1 -0
- package/dist/pgvector/sql/limit.js +20 -0
- package/dist/pgvector/sql/order.d.ts +9 -0
- package/dist/pgvector/sql/order.d.ts.map +1 -0
- package/dist/pgvector/sql/order.js +47 -0
- package/dist/pgvector/sql/query.d.ts +46 -0
- package/dist/pgvector/sql/query.d.ts.map +1 -0
- package/dist/pgvector/sql/query.js +54 -0
- package/dist/pgvector/sql/schema.d.ts +16 -0
- package/dist/pgvector/sql/schema.d.ts.map +1 -0
- package/dist/pgvector/sql/schema.js +47 -0
- package/dist/pgvector/sql/select.d.ts +11 -0
- package/dist/pgvector/sql/select.d.ts.map +1 -0
- package/dist/pgvector/sql/select.js +87 -0
- package/dist/pgvector/sql/where.d.ts +8 -0
- package/dist/pgvector/sql/where.d.ts.map +1 -0
- package/dist/pgvector/sql/where.js +137 -0
- package/dist/pgvector/types.d.ts +20 -0
- package/dist/pgvector/types.d.ts.map +1 -0
- package/dist/pgvector/types.js +1 -0
- package/dist/pgvector/utils.d.ts +18 -0
- package/dist/pgvector/utils.d.ts.map +1 -0
- package/dist/pgvector/utils.js +22 -0
- package/dist/postgres.d.ts +19 -26
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +15 -27
- package/dist/storage.d.ts +62 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +55 -10
- package/dist/thread/sql.d.ts +38 -0
- package/dist/thread/sql.d.ts.map +1 -0
- package/dist/thread/sql.js +112 -0
- package/dist/thread/store.d.ts +7 -3
- package/dist/thread/store.d.ts.map +1 -1
- package/dist/thread/store.js +46 -105
- package/package.json +8 -5
- package/src/__tests__/integration.test.ts +114 -15
- package/src/__tests__/memory-integration.test.ts +355 -0
- package/src/__tests__/memory.test.ts +428 -0
- package/src/index.ts +19 -3
- package/src/memory/sql.ts +141 -0
- package/src/memory/store.ts +166 -0
- package/src/migrations.ts +13 -3
- package/src/pgvector/README.md +50 -0
- package/src/pgvector/__tests__/handle.test.ts +335 -0
- package/src/pgvector/__tests__/hit.test.ts +165 -0
- package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
- package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
- package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
- package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
- package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
- package/src/pgvector/__tests__/search.test.ts +366 -0
- package/src/pgvector/handle.ts +285 -0
- package/src/pgvector/hit.ts +56 -0
- package/src/pgvector/index.ts +7 -0
- package/src/pgvector/search.ts +330 -0
- package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
- package/src/pgvector/sql/__tests__/order.test.ts +248 -0
- package/src/pgvector/sql/__tests__/query.test.ts +548 -0
- package/src/pgvector/sql/__tests__/select.test.ts +367 -0
- package/src/pgvector/sql/__tests__/where.test.ts +554 -0
- package/src/pgvector/sql/index.ts +14 -0
- package/src/pgvector/sql/limit.ts +29 -0
- package/src/pgvector/sql/order.ts +55 -0
- package/src/pgvector/sql/query.ts +112 -0
- package/src/pgvector/sql/schema.ts +61 -0
- package/src/pgvector/sql/select.ts +100 -0
- package/src/pgvector/sql/where.ts +152 -0
- package/src/pgvector/types.ts +21 -0
- package/src/pgvector/utils.ts +24 -0
- package/src/postgres.ts +31 -33
- package/src/storage.ts +102 -11
- package/src/thread/sql.ts +159 -0
- package/src/thread/store.ts +58 -127
- 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
|
-
|
|
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 "${
|
|
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 "${
|
|
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 =
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
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
|
+
};
|
package/src/thread/store.ts
CHANGED
|
@@ -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 ${
|
|
82
|
-
LEFT JOIN ${
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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 (
|
|
206
|
-
|
|
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 $${
|
|
226
|
-
|
|
176
|
+
query += ` LIMIT $${idx++}`;
|
|
177
|
+
params.push(options.limit);
|
|
227
178
|
}
|
|
228
179
|
|
|
229
180
|
if (options?.offset) {
|
|
230
|
-
query += ` OFFSET $${
|
|
231
|
-
|
|
181
|
+
query += ` OFFSET $${idx++}`;
|
|
182
|
+
params.push(options.offset);
|
|
232
183
|
}
|
|
233
184
|
|
|
234
|
-
const result = await this.db.query<ThreadRecord>(query,
|
|
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 ${
|
|
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
|
-
|
|
283
|
-
const values: any[] = [];
|
|
284
|
-
let paramIndex = 1;
|
|
235
|
+
await this.ensureInit();
|
|
285
236
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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 ${
|
|
317
|
-
SET ${updates
|
|
318
|
-
WHERE id = $${
|
|
242
|
+
`UPDATE ${KERNL_SCHEMA_NAME}.threads
|
|
243
|
+
SET ${updates}
|
|
244
|
+
WHERE id = $${idx}
|
|
319
245
|
RETURNING *`,
|
|
320
|
-
|
|
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.
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 ${
|
|
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"}
|