@kernl-sdk/pg 0.1.9 → 0.1.11
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 +2 -2
- package/CHANGELOG.md +19 -0
- package/dist/__tests__/integration.test.js +81 -1
- package/dist/__tests__/thread.test.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/postgres.js +1 -1
- package/dist/storage.d.ts +15 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +26 -4
- package/dist/thread/store.d.ts +5 -1
- package/dist/thread/store.d.ts.map +1 -1
- package/dist/thread/store.js +15 -4
- package/package.json +6 -6
- package/src/__tests__/integration.test.ts +101 -0
- package/src/storage.ts +27 -4
- package/src/thread/store.ts +22 -4
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @kernl/pg
|
|
2
2
|
|
|
3
|
+
## 0.1.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c5a5fcf: Storage now auto-initializes on first operation - no need to call init() manually
|
|
8
|
+
- Updated dependencies [c5a5fcf]
|
|
9
|
+
- kernl@0.6.2
|
|
10
|
+
- @kernl-sdk/storage@0.1.11
|
|
11
|
+
|
|
12
|
+
## 0.1.10
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Fix ESM compatibility by adding --resolve-full-paths to tsc-alias build
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- kernl@0.6.1
|
|
19
|
+
- @kernl-sdk/shared@0.1.6
|
|
20
|
+
- @kernl-sdk/storage@0.1.10
|
|
21
|
+
|
|
3
22
|
## 0.1.9
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -1,9 +1,89 @@
|
|
|
1
1
|
import { beforeAll, afterAll, describe, it, expect } from "vitest";
|
|
2
2
|
import { Pool } from "pg";
|
|
3
|
-
import { PGStorage } from "../storage";
|
|
3
|
+
import { PGStorage } from "../storage.js";
|
|
4
4
|
import { Agent, Context, } from "kernl";
|
|
5
5
|
import { Thread } from "kernl/internal";
|
|
6
6
|
const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
|
|
7
|
+
describe.sequential("PGStorage auto-initialization", () => {
|
|
8
|
+
if (!TEST_DB_URL) {
|
|
9
|
+
it.skip("requires KERNL_PG_TEST_URL to be set", () => { });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Verifies that ALL store methods auto-initialize without explicit init() call.
|
|
14
|
+
*
|
|
15
|
+
* This is critical for DX - users should not need to remember to call init().
|
|
16
|
+
* Each method must internally call ensureInit() before any DB operation.
|
|
17
|
+
*
|
|
18
|
+
* Methods covered: get, list, insert, update, delete, history, append
|
|
19
|
+
*/
|
|
20
|
+
it("auto-initializes on first store operation (no explicit init required)", async () => {
|
|
21
|
+
const pool = new Pool({ connectionString: TEST_DB_URL });
|
|
22
|
+
const storage = new PGStorage({ pool });
|
|
23
|
+
// Clean slate - drop schema to prove init runs automatically
|
|
24
|
+
await pool.query('DROP SCHEMA IF EXISTS "kernl" CASCADE');
|
|
25
|
+
// Bind minimal registries
|
|
26
|
+
const model = {
|
|
27
|
+
spec: "1.0",
|
|
28
|
+
provider: "test",
|
|
29
|
+
modelId: "auto-init-model",
|
|
30
|
+
};
|
|
31
|
+
const agent = new Agent({
|
|
32
|
+
id: "auto-init-agent",
|
|
33
|
+
name: "Auto Init Agent",
|
|
34
|
+
instructions: () => "test",
|
|
35
|
+
model,
|
|
36
|
+
});
|
|
37
|
+
const agents = new Map([["auto-init-agent", agent]]);
|
|
38
|
+
const models = new Map([
|
|
39
|
+
["test/auto-init-model", model],
|
|
40
|
+
]);
|
|
41
|
+
storage.bind({ agents, models });
|
|
42
|
+
const store = storage.threads;
|
|
43
|
+
const tid = "auto-init-thread";
|
|
44
|
+
// 1) list() - should auto-init
|
|
45
|
+
const threads = await store.list();
|
|
46
|
+
expect(threads).toEqual([]);
|
|
47
|
+
// 2) get() - should work (returns null for non-existent)
|
|
48
|
+
const got = await store.get(tid);
|
|
49
|
+
expect(got).toBeNull();
|
|
50
|
+
// 3) insert() - should work
|
|
51
|
+
const inserted = await store.insert({
|
|
52
|
+
id: tid,
|
|
53
|
+
namespace: "kernl",
|
|
54
|
+
agentId: "auto-init-agent",
|
|
55
|
+
model: "test/auto-init-model",
|
|
56
|
+
});
|
|
57
|
+
expect(inserted.tid).toBe(tid);
|
|
58
|
+
// 4) update() - should work
|
|
59
|
+
await store.update(tid, { tick: 1 });
|
|
60
|
+
const tickResult = await pool.query(`SELECT tick FROM "kernl"."threads" WHERE id = $1`, [tid]);
|
|
61
|
+
expect(tickResult.rows[0]?.tick).toBe(1);
|
|
62
|
+
// 5) history() - should work (empty)
|
|
63
|
+
const hist = await store.history(tid);
|
|
64
|
+
expect(hist).toEqual([]);
|
|
65
|
+
// 6) append() - should work
|
|
66
|
+
await store.append([
|
|
67
|
+
{
|
|
68
|
+
id: "evt-1",
|
|
69
|
+
tid,
|
|
70
|
+
seq: 0,
|
|
71
|
+
kind: "message",
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
data: { role: "user", text: "test" },
|
|
74
|
+
metadata: null,
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
// 7) delete() - should work
|
|
78
|
+
await store.delete(tid);
|
|
79
|
+
const afterDelete = await store.get(tid);
|
|
80
|
+
expect(afterDelete).toBeNull();
|
|
81
|
+
// Verify schema was created
|
|
82
|
+
const schemaResult = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'kernl'`);
|
|
83
|
+
expect(schemaResult.rows).toHaveLength(1);
|
|
84
|
+
await storage.close();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
7
87
|
describe.sequential("PGStorage integration", () => {
|
|
8
88
|
if (!TEST_DB_URL) {
|
|
9
89
|
it.skip("requires KERNL_PG_TEST_URL to be set", () => {
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
|
|
2
2
|
import { Pool } from "pg";
|
|
3
3
|
import { Agent, Kernl, tool, FunctionToolkit, } from "kernl";
|
|
4
4
|
import { STOPPED, message, IN_PROGRESS } from "@kernl-sdk/protocol";
|
|
5
|
-
import { postgres } from "../postgres";
|
|
5
|
+
import { postgres } from "../postgres.js";
|
|
6
6
|
// Use helper from test fixtures (handles streaming properly)
|
|
7
7
|
function createMockModel(generateFn) {
|
|
8
8
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @kernl/pg - PostgreSQL storage adapter for Kernl
|
|
3
3
|
*/
|
|
4
|
-
export { PGStorage, type PGStorageConfig } from "./storage";
|
|
5
|
-
export { postgres, type PostgresConfig } from "./postgres";
|
|
6
|
-
export { migrations, REQUIRED_SCHEMA_VERSION } from "./migrations";
|
|
4
|
+
export { PGStorage, type PGStorageConfig } from "./storage.js";
|
|
5
|
+
export { postgres, type PostgresConfig } from "./postgres.js";
|
|
6
|
+
export { migrations, REQUIRED_SCHEMA_VERSION } from "./migrations.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @kernl/pg - PostgreSQL storage adapter for Kernl
|
|
3
3
|
*/
|
|
4
|
-
export { PGStorage } from "./storage";
|
|
5
|
-
export { postgres } from "./postgres";
|
|
6
|
-
export { migrations, REQUIRED_SCHEMA_VERSION } from "./migrations";
|
|
4
|
+
export { PGStorage } from "./storage.js";
|
|
5
|
+
export { postgres } from "./postgres.js";
|
|
6
|
+
export { migrations, REQUIRED_SCHEMA_VERSION } from "./migrations.js";
|
package/dist/postgres.js
CHANGED
package/dist/storage.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Pool } from "pg";
|
|
2
2
|
import type { AgentRegistry, ModelRegistry, KernlStorage, Transaction } from "kernl";
|
|
3
|
-
import { PGThreadStore } from "./thread/store";
|
|
3
|
+
import { PGThreadStore } from "./thread/store.js";
|
|
4
4
|
/**
|
|
5
5
|
* PostgreSQL storage configuration.
|
|
6
6
|
*/
|
|
@@ -12,11 +12,25 @@ export interface PGStorageConfig {
|
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* PostgreSQL storage adapter.
|
|
15
|
+
*
|
|
16
|
+
* Storage is lazily initialized on first use via `ensureInit()`. This means
|
|
17
|
+
* callers don't need to explicitly call `init()` - it happens automatically.
|
|
18
|
+
*
|
|
19
|
+
* NOTE: If the number of store methods grows significantly, consider replacing
|
|
20
|
+
* the manual `ensureInit()` calls with a Proxy-based wrapper for foolproof
|
|
21
|
+
* auto-initialization.
|
|
15
22
|
*/
|
|
16
23
|
export declare class PGStorage implements KernlStorage {
|
|
17
24
|
private pool;
|
|
25
|
+
private initPromise;
|
|
18
26
|
threads: PGThreadStore;
|
|
19
27
|
constructor(config: PGStorageConfig);
|
|
28
|
+
/**
|
|
29
|
+
* Ensure storage is initialized before any operation.
|
|
30
|
+
*
|
|
31
|
+
* Safe to call multiple times - initialization only runs once.
|
|
32
|
+
*/
|
|
33
|
+
private ensureInit;
|
|
20
34
|
/**
|
|
21
35
|
* Bind runtime registries to storage.
|
|
22
36
|
*/
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,IAAI,CAAC;AAG3C,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,YAAY,EACZ,WAAW,EACZ,MAAM,OAAO,CAAC;AAMf,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAI/C;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;;;;;;GASG;AACH,qBAAa,SAAU,YAAW,YAAY;IAC5C,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,WAAW,CAA8B;IAEjD,OAAO,EAAE,aAAa,CAAC;gBAEX,MAAM,EAAE,eAAe;IAKnC;;;;OAIG;YACW,UAAU;IAUxB;;OAEG;IACH,IAAI,CAAC,UAAU,EAAE;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,GAAG,IAAI;IAIxE;;OAEG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIrE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAyC9B;;OAEG;YACW,WAAW;IAUzB;;OAEG;YACW,YAAY;IA6F1B;;OAEG;YACW,UAAU;IAMxB;;OAEG;YACW,UAAU;IAIxB;;OAEG;YACW,SAAS;IAIvB;;OAEG;YACW,WAAW;CAkB1B"}
|
package/dist/storage.js
CHANGED
|
@@ -2,18 +2,40 @@ import assert from "assert";
|
|
|
2
2
|
import { SCHEMA_NAME, TABLE_MIGRATIONS } from "@kernl-sdk/storage";
|
|
3
3
|
import { UnimplementedError } from "@kernl-sdk/shared/lib";
|
|
4
4
|
/* pg */
|
|
5
|
-
import { PGThreadStore } from "./thread/store";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { PGThreadStore } from "./thread/store.js";
|
|
6
|
+
import { migrations } from "./migrations.js";
|
|
7
|
+
import { SQL_IDENTIFIER_REGEX } from "./sql.js";
|
|
8
8
|
/**
|
|
9
9
|
* PostgreSQL storage adapter.
|
|
10
|
+
*
|
|
11
|
+
* Storage is lazily initialized on first use via `ensureInit()`. This means
|
|
12
|
+
* callers don't need to explicitly call `init()` - it happens automatically.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: If the number of store methods grows significantly, consider replacing
|
|
15
|
+
* the manual `ensureInit()` calls with a Proxy-based wrapper for foolproof
|
|
16
|
+
* auto-initialization.
|
|
10
17
|
*/
|
|
11
18
|
export class PGStorage {
|
|
12
19
|
pool;
|
|
20
|
+
initPromise = null;
|
|
13
21
|
threads;
|
|
14
22
|
constructor(config) {
|
|
15
23
|
this.pool = config.pool;
|
|
16
|
-
this.threads = new PGThreadStore(this.pool);
|
|
24
|
+
this.threads = new PGThreadStore(this.pool, () => this.ensureInit());
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Ensure storage is initialized before any operation.
|
|
28
|
+
*
|
|
29
|
+
* Safe to call multiple times - initialization only runs once.
|
|
30
|
+
*/
|
|
31
|
+
async ensureInit() {
|
|
32
|
+
if (!this.initPromise) {
|
|
33
|
+
this.initPromise = this.init().catch((err) => {
|
|
34
|
+
this.initPromise = null;
|
|
35
|
+
throw err;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return this.initPromise;
|
|
17
39
|
}
|
|
18
40
|
/**
|
|
19
41
|
* Bind runtime registries to storage.
|
package/dist/thread/store.d.ts
CHANGED
|
@@ -4,11 +4,15 @@ import { Thread, type ThreadEvent } from "kernl/internal";
|
|
|
4
4
|
import { type AgentRegistry, type ModelRegistry, type ThreadStore, type NewThread, type ThreadUpdate, type ThreadInclude, type ThreadListOptions, type ThreadHistoryOptions } from "kernl";
|
|
5
5
|
/**
|
|
6
6
|
* PostgreSQL Thread store implementation.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: All async methods must call `await this.ensureInit()` before
|
|
9
|
+
* any database operations. This ensures schema/tables exist.
|
|
7
10
|
*/
|
|
8
11
|
export declare class PGThreadStore implements ThreadStore {
|
|
9
12
|
private db;
|
|
10
13
|
private registries;
|
|
11
|
-
|
|
14
|
+
private ensureInit;
|
|
15
|
+
constructor(db: Pool | PoolClient, ensureInit: () => Promise<void>);
|
|
12
16
|
/**
|
|
13
17
|
* Bind runtime registries for hydrating Thread instances.
|
|
14
18
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/thread/store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,OAAO,CAAC;AAEf
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/thread/store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,OAAO,CAAC;AAEf;;;;;GAKG;AACH,qBAAa,aAAc,YAAW,WAAW;IAC/C,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,UAAU,CAA0D;IAC5E,OAAO,CAAC,UAAU,CAAsB;gBAE5B,EAAE,EAAE,IAAI,GAAG,UAAU,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC;IAMlE;;;;OAIG;IACH,IAAI,CAAC,UAAU,EAAE;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,GAAG,IAAI;IAIxE;;OAEG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAuGvE;;OAEG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmG1D;;OAEG;IACG,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IA4BhD;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA+C/D;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxC;;OAEG;IACG,OAAO,CACX,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,oBAAoB,GAC1B,OAAO,CAAC,WAAW,EAAE,CAAC;IAuCzB;;;;;;;;;OASG;IACG,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmClD;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,YAAY,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAA;KAAE,GAAG,MAAM;CAkC1E"}
|
package/dist/thread/store.js
CHANGED
|
@@ -4,12 +4,17 @@ import { Thread } from "kernl/internal";
|
|
|
4
4
|
import { Context, } from "kernl";
|
|
5
5
|
/**
|
|
6
6
|
* PostgreSQL Thread store implementation.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: All async methods must call `await this.ensureInit()` before
|
|
9
|
+
* any database operations. This ensures schema/tables exist.
|
|
7
10
|
*/
|
|
8
11
|
export class PGThreadStore {
|
|
9
12
|
db;
|
|
10
13
|
registries;
|
|
11
|
-
|
|
14
|
+
ensureInit;
|
|
15
|
+
constructor(db, ensureInit) {
|
|
12
16
|
this.db = db;
|
|
17
|
+
this.ensureInit = ensureInit;
|
|
13
18
|
this.registries = null;
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
@@ -24,6 +29,7 @@ export class PGThreadStore {
|
|
|
24
29
|
* Get a thread by id.
|
|
25
30
|
*/
|
|
26
31
|
async get(tid, include) {
|
|
32
|
+
await this.ensureInit();
|
|
27
33
|
// JOIN with thread_events if include.history
|
|
28
34
|
if (include?.history) {
|
|
29
35
|
const opts = typeof include.history === "object" ? include.history : undefined;
|
|
@@ -110,6 +116,7 @@ export class PGThreadStore {
|
|
|
110
116
|
* List threads matching the filter.
|
|
111
117
|
*/
|
|
112
118
|
async list(options) {
|
|
119
|
+
await this.ensureInit();
|
|
113
120
|
let query = `SELECT * FROM ${SCHEMA_NAME}.threads`;
|
|
114
121
|
const values = [];
|
|
115
122
|
let paramIndex = 1;
|
|
@@ -193,6 +200,7 @@ export class PGThreadStore {
|
|
|
193
200
|
* Insert a new thread into the store.
|
|
194
201
|
*/
|
|
195
202
|
async insert(thread) {
|
|
203
|
+
await this.ensureInit();
|
|
196
204
|
const record = NewThreadCodec.encode(thread);
|
|
197
205
|
const result = await this.db.query(`INSERT INTO ${SCHEMA_NAME}.threads
|
|
198
206
|
(id, namespace, agent_id, model, context, tick, state, parent_task_id, metadata, created_at, updated_at)
|
|
@@ -216,6 +224,7 @@ export class PGThreadStore {
|
|
|
216
224
|
* Update thread runtime state.
|
|
217
225
|
*/
|
|
218
226
|
async update(tid, patch) {
|
|
227
|
+
await this.ensureInit();
|
|
219
228
|
const updates = [];
|
|
220
229
|
const values = [];
|
|
221
230
|
let paramIndex = 1;
|
|
@@ -229,7 +238,7 @@ export class PGThreadStore {
|
|
|
229
238
|
}
|
|
230
239
|
if (patch.context !== undefined) {
|
|
231
240
|
updates.push(`context = $${paramIndex++}`);
|
|
232
|
-
// NOTE: Store the raw context value, not the Context wrapper.
|
|
241
|
+
// NOTE: Store the raw context value, not the Context wrapper.
|
|
233
242
|
//
|
|
234
243
|
// THis may change in the future depending on Context implementation.
|
|
235
244
|
values.push(JSON.stringify(patch.context.context));
|
|
@@ -252,6 +261,7 @@ export class PGThreadStore {
|
|
|
252
261
|
* Delete a thread and cascade to thread_events.
|
|
253
262
|
*/
|
|
254
263
|
async delete(tid) {
|
|
264
|
+
await this.ensureInit();
|
|
255
265
|
await this.db.query(`DELETE FROM ${SCHEMA_NAME}.threads WHERE id = $1`, [
|
|
256
266
|
tid,
|
|
257
267
|
]);
|
|
@@ -260,6 +270,7 @@ export class PGThreadStore {
|
|
|
260
270
|
* Get the event history for a thread.
|
|
261
271
|
*/
|
|
262
272
|
async history(tid, opts) {
|
|
273
|
+
await this.ensureInit();
|
|
263
274
|
let query = `SELECT * FROM ${SCHEMA_NAME}.thread_events WHERE tid = $1`;
|
|
264
275
|
const values = [tid];
|
|
265
276
|
let paramIndex = 2;
|
|
@@ -284,8 +295,7 @@ export class PGThreadStore {
|
|
|
284
295
|
const result = await this.db.query(query, values);
|
|
285
296
|
return result.rows.map((record) => ThreadEventRecordCodec.decode({
|
|
286
297
|
...record,
|
|
287
|
-
//
|
|
288
|
-
timestamp: Number(record.timestamp),
|
|
298
|
+
timestamp: Number(record.timestamp), // normalize BIGINT (string) to number for zod schema
|
|
289
299
|
}));
|
|
290
300
|
}
|
|
291
301
|
/**
|
|
@@ -301,6 +311,7 @@ export class PGThreadStore {
|
|
|
301
311
|
async append(events) {
|
|
302
312
|
if (events.length === 0)
|
|
303
313
|
return;
|
|
314
|
+
await this.ensureInit();
|
|
304
315
|
const records = events.map((e) => ThreadEventRecordCodec.encode(e));
|
|
305
316
|
const values = [];
|
|
306
317
|
const placeholders = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernl-sdk/pg",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "PostgreSQL storage adapter for kernl",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kernl",
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
"tsc-alias": "^1.8.10",
|
|
36
36
|
"typescript": "5.9.2",
|
|
37
37
|
"vitest": "^4.0.8",
|
|
38
|
-
"@kernl-sdk/protocol": "^0.2.
|
|
38
|
+
"@kernl-sdk/protocol": "^0.2.5"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"pg": "^8.16.3",
|
|
42
|
-
"kernl": "^0.6.
|
|
43
|
-
"@kernl-sdk/shared": "^0.1.
|
|
44
|
-
"@kernl-sdk/storage": "0.1.
|
|
42
|
+
"kernl": "^0.6.2",
|
|
43
|
+
"@kernl-sdk/shared": "^0.1.6",
|
|
44
|
+
"@kernl-sdk/storage": "0.1.11"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
|
-
"build": "tsc && tsc-alias",
|
|
47
|
+
"build": "tsc && tsc-alias --resolve-full-paths",
|
|
48
48
|
"dev": "tsc --watch",
|
|
49
49
|
"check-types": "tsc --noEmit",
|
|
50
50
|
"test": "vitest",
|
|
@@ -16,6 +16,107 @@ const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
|
|
|
16
16
|
type TestLanguageModel =
|
|
17
17
|
ModelRegistry extends { get(key: string): infer T | undefined } ? T : never;
|
|
18
18
|
|
|
19
|
+
describe.sequential("PGStorage auto-initialization", () => {
|
|
20
|
+
if (!TEST_DB_URL) {
|
|
21
|
+
it.skip("requires KERNL_PG_TEST_URL to be set", () => {});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Verifies that ALL store methods auto-initialize without explicit init() call.
|
|
27
|
+
*
|
|
28
|
+
* This is critical for DX - users should not need to remember to call init().
|
|
29
|
+
* Each method must internally call ensureInit() before any DB operation.
|
|
30
|
+
*
|
|
31
|
+
* Methods covered: get, list, insert, update, delete, history, append
|
|
32
|
+
*/
|
|
33
|
+
it("auto-initializes on first store operation (no explicit init required)", async () => {
|
|
34
|
+
const pool = new Pool({ connectionString: TEST_DB_URL });
|
|
35
|
+
const storage = new PGStorage({ pool });
|
|
36
|
+
|
|
37
|
+
// Clean slate - drop schema to prove init runs automatically
|
|
38
|
+
await pool.query('DROP SCHEMA IF EXISTS "kernl" CASCADE');
|
|
39
|
+
|
|
40
|
+
// Bind minimal registries
|
|
41
|
+
const model = {
|
|
42
|
+
spec: "1.0" as const,
|
|
43
|
+
provider: "test",
|
|
44
|
+
modelId: "auto-init-model",
|
|
45
|
+
} as unknown as TestLanguageModel;
|
|
46
|
+
|
|
47
|
+
const agent = new Agent({
|
|
48
|
+
id: "auto-init-agent",
|
|
49
|
+
name: "Auto Init Agent",
|
|
50
|
+
instructions: () => "test",
|
|
51
|
+
model,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const agents: AgentRegistry = new Map([["auto-init-agent", agent]]);
|
|
55
|
+
const models: ModelRegistry = new Map([
|
|
56
|
+
["test/auto-init-model", model],
|
|
57
|
+
]) as unknown as ModelRegistry;
|
|
58
|
+
|
|
59
|
+
storage.bind({ agents, models });
|
|
60
|
+
const store = storage.threads;
|
|
61
|
+
const tid = "auto-init-thread";
|
|
62
|
+
|
|
63
|
+
// 1) list() - should auto-init
|
|
64
|
+
const threads = await store.list();
|
|
65
|
+
expect(threads).toEqual([]);
|
|
66
|
+
|
|
67
|
+
// 2) get() - should work (returns null for non-existent)
|
|
68
|
+
const got = await store.get(tid);
|
|
69
|
+
expect(got).toBeNull();
|
|
70
|
+
|
|
71
|
+
// 3) insert() - should work
|
|
72
|
+
const inserted = await store.insert({
|
|
73
|
+
id: tid,
|
|
74
|
+
namespace: "kernl",
|
|
75
|
+
agentId: "auto-init-agent",
|
|
76
|
+
model: "test/auto-init-model",
|
|
77
|
+
});
|
|
78
|
+
expect(inserted.tid).toBe(tid);
|
|
79
|
+
|
|
80
|
+
// 4) update() - should work
|
|
81
|
+
await store.update(tid, { tick: 1 });
|
|
82
|
+
const tickResult = await pool.query<{ tick: number }>(
|
|
83
|
+
`SELECT tick FROM "kernl"."threads" WHERE id = $1`,
|
|
84
|
+
[tid],
|
|
85
|
+
);
|
|
86
|
+
expect(tickResult.rows[0]?.tick).toBe(1);
|
|
87
|
+
|
|
88
|
+
// 5) history() - should work (empty)
|
|
89
|
+
const hist = await store.history(tid);
|
|
90
|
+
expect(hist).toEqual([]);
|
|
91
|
+
|
|
92
|
+
// 6) append() - should work
|
|
93
|
+
await store.append([
|
|
94
|
+
{
|
|
95
|
+
id: "evt-1",
|
|
96
|
+
tid,
|
|
97
|
+
seq: 0,
|
|
98
|
+
kind: "message",
|
|
99
|
+
timestamp: new Date(),
|
|
100
|
+
data: { role: "user", text: "test" },
|
|
101
|
+
metadata: null,
|
|
102
|
+
} as any,
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
// 7) delete() - should work
|
|
106
|
+
await store.delete(tid);
|
|
107
|
+
const afterDelete = await store.get(tid);
|
|
108
|
+
expect(afterDelete).toBeNull();
|
|
109
|
+
|
|
110
|
+
// Verify schema was created
|
|
111
|
+
const schemaResult = await pool.query(
|
|
112
|
+
`SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'kernl'`,
|
|
113
|
+
);
|
|
114
|
+
expect(schemaResult.rows).toHaveLength(1);
|
|
115
|
+
|
|
116
|
+
await storage.close();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
19
120
|
describe.sequential("PGStorage integration", () => {
|
|
20
121
|
if (!TEST_DB_URL) {
|
|
21
122
|
it.skip("requires KERNL_PG_TEST_URL to be set", () => {
|
package/src/storage.ts
CHANGED
|
@@ -2,20 +2,20 @@ 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 { 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 { SQL_IDENTIFIER_REGEX } from "./sql";
|
|
18
17
|
import { migrations } from "./migrations";
|
|
18
|
+
import { SQL_IDENTIFIER_REGEX } from "./sql";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* PostgreSQL storage configuration.
|
|
@@ -29,15 +29,38 @@ export interface PGStorageConfig {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* PostgreSQL storage adapter.
|
|
32
|
+
*
|
|
33
|
+
* Storage is lazily initialized on first use via `ensureInit()`. This means
|
|
34
|
+
* callers don't need to explicitly call `init()` - it happens automatically.
|
|
35
|
+
*
|
|
36
|
+
* NOTE: If the number of store methods grows significantly, consider replacing
|
|
37
|
+
* the manual `ensureInit()` calls with a Proxy-based wrapper for foolproof
|
|
38
|
+
* auto-initialization.
|
|
32
39
|
*/
|
|
33
40
|
export class PGStorage implements KernlStorage {
|
|
34
41
|
private pool: Pool;
|
|
42
|
+
private initPromise: Promise<void> | null = null;
|
|
35
43
|
|
|
36
44
|
threads: PGThreadStore;
|
|
37
45
|
|
|
38
46
|
constructor(config: PGStorageConfig) {
|
|
39
47
|
this.pool = config.pool;
|
|
40
|
-
this.threads = new PGThreadStore(this.pool);
|
|
48
|
+
this.threads = new PGThreadStore(this.pool, () => this.ensureInit());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ensure storage is initialized before any operation.
|
|
53
|
+
*
|
|
54
|
+
* Safe to call multiple times - initialization only runs once.
|
|
55
|
+
*/
|
|
56
|
+
private async ensureInit(): Promise<void> {
|
|
57
|
+
if (!this.initPromise) {
|
|
58
|
+
this.initPromise = this.init().catch((err) => {
|
|
59
|
+
this.initPromise = null;
|
|
60
|
+
throw err;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return this.initPromise;
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
/**
|
package/src/thread/store.ts
CHANGED
|
@@ -23,13 +23,18 @@ import {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* PostgreSQL Thread store implementation.
|
|
26
|
+
*
|
|
27
|
+
* IMPORTANT: All async methods must call `await this.ensureInit()` before
|
|
28
|
+
* any database operations. This ensures schema/tables exist.
|
|
26
29
|
*/
|
|
27
30
|
export class PGThreadStore implements ThreadStore {
|
|
28
31
|
private db: Pool | PoolClient;
|
|
29
32
|
private registries: { agents: AgentRegistry; models: ModelRegistry } | null;
|
|
33
|
+
private ensureInit: () => Promise<void>;
|
|
30
34
|
|
|
31
|
-
constructor(db: Pool | PoolClient) {
|
|
35
|
+
constructor(db: Pool | PoolClient, ensureInit: () => Promise<void>) {
|
|
32
36
|
this.db = db;
|
|
37
|
+
this.ensureInit = ensureInit;
|
|
33
38
|
this.registries = null;
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -46,6 +51,8 @@ export class PGThreadStore implements ThreadStore {
|
|
|
46
51
|
* Get a thread by id.
|
|
47
52
|
*/
|
|
48
53
|
async get(tid: string, include?: ThreadInclude): Promise<Thread | null> {
|
|
54
|
+
await this.ensureInit();
|
|
55
|
+
|
|
49
56
|
// JOIN with thread_events if include.history
|
|
50
57
|
if (include?.history) {
|
|
51
58
|
const opts =
|
|
@@ -150,6 +157,8 @@ export class PGThreadStore implements ThreadStore {
|
|
|
150
157
|
* List threads matching the filter.
|
|
151
158
|
*/
|
|
152
159
|
async list(options?: ThreadListOptions): Promise<Thread[]> {
|
|
160
|
+
await this.ensureInit();
|
|
161
|
+
|
|
153
162
|
let query = `SELECT * FROM ${SCHEMA_NAME}.threads`;
|
|
154
163
|
const values: any[] = [];
|
|
155
164
|
let paramIndex = 1;
|
|
@@ -250,6 +259,8 @@ export class PGThreadStore implements ThreadStore {
|
|
|
250
259
|
* Insert a new thread into the store.
|
|
251
260
|
*/
|
|
252
261
|
async insert(thread: NewThread): Promise<Thread> {
|
|
262
|
+
await this.ensureInit();
|
|
263
|
+
|
|
253
264
|
const record = NewThreadCodec.encode(thread);
|
|
254
265
|
|
|
255
266
|
const result = await this.db.query<ThreadRecord>(
|
|
@@ -279,6 +290,8 @@ export class PGThreadStore implements ThreadStore {
|
|
|
279
290
|
* Update thread runtime state.
|
|
280
291
|
*/
|
|
281
292
|
async update(tid: string, patch: ThreadUpdate): Promise<Thread> {
|
|
293
|
+
await this.ensureInit();
|
|
294
|
+
|
|
282
295
|
const updates: string[] = [];
|
|
283
296
|
const values: any[] = [];
|
|
284
297
|
let paramIndex = 1;
|
|
@@ -295,7 +308,7 @@ export class PGThreadStore implements ThreadStore {
|
|
|
295
308
|
|
|
296
309
|
if (patch.context !== undefined) {
|
|
297
310
|
updates.push(`context = $${paramIndex++}`);
|
|
298
|
-
// NOTE: Store the raw context value, not the Context wrapper.
|
|
311
|
+
// NOTE: Store the raw context value, not the Context wrapper.
|
|
299
312
|
//
|
|
300
313
|
// THis may change in the future depending on Context implementation.
|
|
301
314
|
values.push(JSON.stringify(patch.context.context));
|
|
@@ -327,6 +340,8 @@ export class PGThreadStore implements ThreadStore {
|
|
|
327
340
|
* Delete a thread and cascade to thread_events.
|
|
328
341
|
*/
|
|
329
342
|
async delete(tid: string): Promise<void> {
|
|
343
|
+
await this.ensureInit();
|
|
344
|
+
|
|
330
345
|
await this.db.query(`DELETE FROM ${SCHEMA_NAME}.threads WHERE id = $1`, [
|
|
331
346
|
tid,
|
|
332
347
|
]);
|
|
@@ -339,6 +354,8 @@ export class PGThreadStore implements ThreadStore {
|
|
|
339
354
|
tid: string,
|
|
340
355
|
opts?: ThreadHistoryOptions,
|
|
341
356
|
): Promise<ThreadEvent[]> {
|
|
357
|
+
await this.ensureInit();
|
|
358
|
+
|
|
342
359
|
let query = `SELECT * FROM ${SCHEMA_NAME}.thread_events WHERE tid = $1`;
|
|
343
360
|
const values: any[] = [tid];
|
|
344
361
|
let paramIndex = 2;
|
|
@@ -370,8 +387,7 @@ export class PGThreadStore implements ThreadStore {
|
|
|
370
387
|
return result.rows.map((record) =>
|
|
371
388
|
ThreadEventRecordCodec.decode({
|
|
372
389
|
...record,
|
|
373
|
-
//
|
|
374
|
-
timestamp: Number(record.timestamp),
|
|
390
|
+
timestamp: Number(record.timestamp), // normalize BIGINT (string) to number for zod schema
|
|
375
391
|
} as ThreadEventRecord),
|
|
376
392
|
);
|
|
377
393
|
}
|
|
@@ -388,6 +404,7 @@ export class PGThreadStore implements ThreadStore {
|
|
|
388
404
|
*/
|
|
389
405
|
async append(events: ThreadEvent[]): Promise<void> {
|
|
390
406
|
if (events.length === 0) return;
|
|
407
|
+
await this.ensureInit();
|
|
391
408
|
|
|
392
409
|
const records = events.map((e) => ThreadEventRecordCodec.encode(e));
|
|
393
410
|
|
|
@@ -434,6 +451,7 @@ export class PGThreadStore implements ThreadStore {
|
|
|
434
451
|
const agent = this.registries.agents.get(record.agent_id);
|
|
435
452
|
const model = this.registries.models.get(record.model);
|
|
436
453
|
|
|
454
|
+
// (TODO): we might want to allow this in the future, unclear how it would look though..
|
|
437
455
|
if (!agent || !model) {
|
|
438
456
|
throw new Error(
|
|
439
457
|
`Thread ${record.id} references non-existent agent/model (agent: ${record.agent_id}, model: ${record.model})`,
|