@kernl-sdk/libsql 0.1.36 → 0.1.39
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 +5 -4
- package/CHANGELOG.md +20 -0
- package/README.md +225 -0
- package/dist/__tests__/constraints.test.d.ts +2 -0
- package/dist/__tests__/constraints.test.d.ts.map +1 -0
- package/dist/__tests__/constraints.test.js +97 -0
- package/dist/__tests__/helpers.d.ts +36 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/helpers.js +80 -0
- package/dist/__tests__/memory.create-get.test.d.ts +2 -0
- package/dist/__tests__/memory.create-get.test.d.ts.map +1 -0
- package/dist/__tests__/memory.create-get.test.js +8 -0
- package/dist/__tests__/memory.delete.test.d.ts +2 -0
- package/dist/__tests__/memory.delete.test.d.ts.map +1 -0
- package/dist/__tests__/memory.delete.test.js +6 -0
- package/dist/__tests__/memory.list.test.d.ts +2 -0
- package/dist/__tests__/memory.list.test.d.ts.map +1 -0
- package/dist/__tests__/memory.list.test.js +8 -0
- package/dist/__tests__/memory.update.test.d.ts +2 -0
- package/dist/__tests__/memory.update.test.d.ts.map +1 -0
- package/dist/__tests__/memory.update.test.js +8 -0
- package/dist/__tests__/migrations.test.d.ts +2 -0
- package/dist/__tests__/migrations.test.d.ts.map +1 -0
- package/dist/__tests__/migrations.test.js +68 -0
- package/dist/__tests__/row-codecs.test.d.ts +2 -0
- package/dist/__tests__/row-codecs.test.d.ts.map +1 -0
- package/dist/__tests__/row-codecs.test.js +175 -0
- package/dist/__tests__/sql-utils.test.d.ts +2 -0
- package/dist/__tests__/sql-utils.test.d.ts.map +1 -0
- package/dist/__tests__/sql-utils.test.js +45 -0
- package/dist/__tests__/storage.init.test.d.ts +2 -0
- package/dist/__tests__/storage.init.test.d.ts.map +1 -0
- package/dist/__tests__/storage.init.test.js +63 -0
- package/dist/__tests__/thread.lifecycle.test.d.ts +2 -0
- package/dist/__tests__/thread.lifecycle.test.d.ts.map +1 -0
- package/dist/__tests__/thread.lifecycle.test.js +172 -0
- package/dist/__tests__/transaction.test.d.ts +2 -0
- package/dist/__tests__/transaction.test.d.ts.map +1 -0
- package/dist/__tests__/transaction.test.js +16 -0
- package/dist/__tests__/utils.test.d.ts +2 -0
- package/dist/__tests__/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils.test.js +31 -0
- package/dist/client.d.ts +46 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +46 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/memory/__tests__/create-get.test.d.ts +2 -0
- package/dist/memory/__tests__/create-get.test.d.ts.map +1 -0
- package/dist/memory/__tests__/create-get.test.js +126 -0
- package/dist/memory/__tests__/delete.test.d.ts +2 -0
- package/dist/memory/__tests__/delete.test.d.ts.map +1 -0
- package/dist/memory/__tests__/delete.test.js +96 -0
- package/dist/memory/__tests__/list.test.d.ts +2 -0
- package/dist/memory/__tests__/list.test.d.ts.map +1 -0
- package/dist/memory/__tests__/list.test.js +168 -0
- package/dist/memory/__tests__/sql.test.d.ts +2 -0
- package/dist/memory/__tests__/sql.test.d.ts.map +1 -0
- package/dist/memory/__tests__/sql.test.js +159 -0
- package/dist/memory/__tests__/update.test.d.ts +2 -0
- package/dist/memory/__tests__/update.test.d.ts.map +1 -0
- package/dist/memory/__tests__/update.test.js +113 -0
- package/dist/memory/row.d.ts +11 -0
- package/dist/memory/row.d.ts.map +1 -0
- package/dist/memory/row.js +29 -0
- package/dist/memory/sql.d.ts +34 -0
- package/dist/memory/sql.d.ts.map +1 -0
- package/dist/memory/sql.js +109 -0
- package/dist/memory/store.d.ts +41 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +132 -0
- package/dist/migrations.d.ts +32 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +157 -0
- package/dist/sql.d.ts +28 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +22 -0
- package/dist/storage.d.ts +75 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +123 -0
- package/dist/thread/__tests__/append.test.d.ts +2 -0
- package/dist/thread/__tests__/append.test.d.ts.map +1 -0
- package/dist/thread/__tests__/append.test.js +141 -0
- package/dist/thread/__tests__/history.test.d.ts +2 -0
- package/dist/thread/__tests__/history.test.d.ts.map +1 -0
- package/dist/thread/__tests__/history.test.js +146 -0
- package/dist/thread/__tests__/sql.test.d.ts +2 -0
- package/dist/thread/__tests__/sql.test.d.ts.map +1 -0
- package/dist/thread/__tests__/sql.test.js +129 -0
- package/dist/thread/__tests__/store.test.d.ts +2 -0
- package/dist/thread/__tests__/store.test.d.ts.map +1 -0
- package/dist/thread/__tests__/store.test.js +170 -0
- package/dist/thread/row.d.ts +19 -0
- package/dist/thread/row.d.ts.map +1 -0
- package/dist/thread/row.js +65 -0
- package/dist/thread/sql.d.ts +33 -0
- package/dist/thread/sql.d.ts.map +1 -0
- package/dist/thread/sql.js +112 -0
- package/dist/thread/store.d.ts +67 -0
- package/dist/thread/store.d.ts.map +1 -0
- package/dist/thread/store.js +282 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +21 -0
- package/package.json +15 -11
- package/src/__tests__/constraints.test.ts +123 -0
- package/src/__tests__/helpers.ts +98 -0
- package/src/__tests__/migrations.test.ts +114 -0
- package/src/__tests__/row-codecs.test.ts +201 -0
- package/src/__tests__/sql-utils.test.ts +52 -0
- package/src/__tests__/storage.init.test.ts +92 -0
- package/src/__tests__/thread.lifecycle.test.ts +234 -0
- package/src/__tests__/transaction.test.ts +25 -0
- package/src/__tests__/utils.test.ts +38 -0
- package/src/client.ts +71 -0
- package/src/index.ts +10 -0
- package/src/memory/__tests__/create-get.test.ts +161 -0
- package/src/memory/__tests__/delete.test.ts +124 -0
- package/src/memory/__tests__/list.test.ts +198 -0
- package/src/memory/__tests__/sql.test.ts +186 -0
- package/src/memory/__tests__/update.test.ts +148 -0
- package/src/memory/row.ts +36 -0
- package/src/memory/sql.ts +142 -0
- package/src/memory/store.ts +173 -0
- package/src/migrations.ts +206 -0
- package/src/sql.ts +35 -0
- package/src/storage.ts +170 -0
- package/src/thread/__tests__/append.test.ts +201 -0
- package/src/thread/__tests__/history.test.ts +198 -0
- package/src/thread/__tests__/sql.test.ts +154 -0
- package/src/thread/__tests__/store.test.ts +219 -0
- package/src/thread/row.ts +77 -0
- package/src/thread/sql.ts +153 -0
- package/src/thread/store.ts +381 -0
- package/src/utils.ts +20 -0
- package/LICENSE +0 -201
package/dist/sql.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL utilities for safe query construction.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* SQL identifier regex - alphanumeric + underscore, must start with letter/underscore.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SQL_IDENTIFIER_REGEX: RegExp;
|
|
8
|
+
/**
|
|
9
|
+
* SQL clause with placeholder parameters.
|
|
10
|
+
*/
|
|
11
|
+
export interface SQLClause {
|
|
12
|
+
sql: string;
|
|
13
|
+
params: unknown[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Expand an array into individual ? placeholders for IN clause.
|
|
17
|
+
*
|
|
18
|
+
* LibSQL/SQLite doesn't support array parameters like PostgreSQL's ANY($1).
|
|
19
|
+
* This helper expands arrays into individual placeholders.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* expandArray(['a', 'b', 'c']) => { placeholders: '?, ?, ?', params: ['a', 'b', 'c'] }
|
|
23
|
+
*/
|
|
24
|
+
export declare function expandarray(arr: unknown[]): {
|
|
25
|
+
placeholders: string;
|
|
26
|
+
params: unknown[];
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAE/D;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB,CAKA"}
|
package/dist/sql.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL utilities for safe query construction.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* SQL identifier regex - alphanumeric + underscore, must start with letter/underscore.
|
|
6
|
+
*/
|
|
7
|
+
export const SQL_IDENTIFIER_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
8
|
+
/**
|
|
9
|
+
* Expand an array into individual ? placeholders for IN clause.
|
|
10
|
+
*
|
|
11
|
+
* LibSQL/SQLite doesn't support array parameters like PostgreSQL's ANY($1).
|
|
12
|
+
* This helper expands arrays into individual placeholders.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* expandArray(['a', 'b', 'c']) => { placeholders: '?, ?, ?', params: ['a', 'b', 'c'] }
|
|
16
|
+
*/
|
|
17
|
+
export function expandarray(arr) {
|
|
18
|
+
return {
|
|
19
|
+
placeholders: arr.map(() => "?").join(", "),
|
|
20
|
+
params: arr,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LibSQL storage adapter.
|
|
3
|
+
*/
|
|
4
|
+
import type { Client } from "@libsql/client";
|
|
5
|
+
import type { IAgentRegistry, IModelRegistry, KernlStorage, Transaction } from "kernl";
|
|
6
|
+
import { LibSQLThreadStore } from "./thread/store.js";
|
|
7
|
+
import { LibSQLMemoryStore } from "./memory/store.js";
|
|
8
|
+
/**
|
|
9
|
+
* LibSQL storage configuration.
|
|
10
|
+
*/
|
|
11
|
+
export interface LibSQLStorageConfig {
|
|
12
|
+
/**
|
|
13
|
+
* LibSQL client instance.
|
|
14
|
+
*/
|
|
15
|
+
client: Client;
|
|
16
|
+
/**
|
|
17
|
+
* Original connection URL (optional).
|
|
18
|
+
*
|
|
19
|
+
* Used to detect local file databases for setting SQLite PRAGMAs.
|
|
20
|
+
* Not needed if using a pre-configured client.
|
|
21
|
+
*/
|
|
22
|
+
url?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* LibSQL storage adapter.
|
|
26
|
+
*
|
|
27
|
+
* Storage is lazily initialized on first use via `ensureInit()`. This means
|
|
28
|
+
* callers don't need to explicitly call `init()` - it happens automatically.
|
|
29
|
+
*/
|
|
30
|
+
export declare class LibSQLStorage implements KernlStorage {
|
|
31
|
+
private client;
|
|
32
|
+
private url?;
|
|
33
|
+
private initPromise;
|
|
34
|
+
threads: LibSQLThreadStore;
|
|
35
|
+
memories: LibSQLMemoryStore;
|
|
36
|
+
constructor(config: LibSQLStorageConfig);
|
|
37
|
+
/**
|
|
38
|
+
* Ensure storage is initialized before any operation.
|
|
39
|
+
*
|
|
40
|
+
* Safe to call multiple times - initialization only runs once.
|
|
41
|
+
*/
|
|
42
|
+
private ensureInit;
|
|
43
|
+
/**
|
|
44
|
+
* Bind runtime registries to storage.
|
|
45
|
+
*/
|
|
46
|
+
bind(registries: {
|
|
47
|
+
agents: IAgentRegistry;
|
|
48
|
+
models: IModelRegistry;
|
|
49
|
+
}): void;
|
|
50
|
+
/**
|
|
51
|
+
* Execute a function within a transaction.
|
|
52
|
+
*/
|
|
53
|
+
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
|
|
54
|
+
/**
|
|
55
|
+
* Initialize the storage backend.
|
|
56
|
+
*/
|
|
57
|
+
init(): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Check if this is a local file or in-memory database.
|
|
60
|
+
*/
|
|
61
|
+
private isLocal;
|
|
62
|
+
/**
|
|
63
|
+
* Close the storage backend and cleanup resources.
|
|
64
|
+
*/
|
|
65
|
+
close(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Run migrations to ensure all required tables exist.
|
|
68
|
+
*/
|
|
69
|
+
migrate(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Create a table from its definition.
|
|
72
|
+
*/
|
|
73
|
+
private createTable;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,YAAY,EACZ,WAAW,EACZ,MAAM,OAAO,CAAC;AAKf,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAGnD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,qBAAa,aAAc,YAAW,YAAY;IAChD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,GAAG,CAAC,CAAS;IACrB,OAAO,CAAC,WAAW,CAA8B;IAEjD,OAAO,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,EAAE,iBAAiB,CAAC;gBAEhB,MAAM,EAAE,mBAAmB;IAOvC;;;;OAIG;YACW,UAAU;IAUxB;;OAEG;IACH,IAAI,CAAC,UAAU,EAAE;QAAE,MAAM,EAAE,cAAc,CAAC;QAAC,MAAM,EAAE,cAAc,CAAA;KAAE,GAAG,IAAI;IAI1E;;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;IAa3B;;OAEG;IACH,OAAO,CAAC,OAAO;IAKf;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuC9B;;OAEG;YACW,WAAW;CAK1B"}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LibSQL storage adapter.
|
|
3
|
+
*/
|
|
4
|
+
import { KERNL_SCHEMA_NAME, TABLE_MIGRATIONS } from "@kernl-sdk/storage";
|
|
5
|
+
import { UnimplementedError } from "@kernl-sdk/shared/lib";
|
|
6
|
+
import { LibSQLThreadStore } from "./thread/store.js";
|
|
7
|
+
import { LibSQLMemoryStore } from "./memory/store.js";
|
|
8
|
+
import { MIGRATIONS, createTable } from "./migrations.js";
|
|
9
|
+
/**
|
|
10
|
+
* LibSQL storage adapter.
|
|
11
|
+
*
|
|
12
|
+
* Storage is lazily initialized on first use via `ensureInit()`. This means
|
|
13
|
+
* callers don't need to explicitly call `init()` - it happens automatically.
|
|
14
|
+
*/
|
|
15
|
+
export class LibSQLStorage {
|
|
16
|
+
client;
|
|
17
|
+
url;
|
|
18
|
+
initPromise = null;
|
|
19
|
+
threads;
|
|
20
|
+
memories;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.client = config.client;
|
|
23
|
+
this.url = config.url;
|
|
24
|
+
this.threads = new LibSQLThreadStore(this.client, () => this.ensureInit());
|
|
25
|
+
this.memories = new LibSQLMemoryStore(this.client, () => this.ensureInit());
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Ensure storage is initialized before any operation.
|
|
29
|
+
*
|
|
30
|
+
* Safe to call multiple times - initialization only runs once.
|
|
31
|
+
*/
|
|
32
|
+
async ensureInit() {
|
|
33
|
+
if (!this.initPromise) {
|
|
34
|
+
this.initPromise = this.init().catch((err) => {
|
|
35
|
+
this.initPromise = null;
|
|
36
|
+
throw err;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return this.initPromise;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Bind runtime registries to storage.
|
|
43
|
+
*/
|
|
44
|
+
bind(registries) {
|
|
45
|
+
this.threads.bind(registries);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Execute a function within a transaction.
|
|
49
|
+
*/
|
|
50
|
+
async transaction(fn) {
|
|
51
|
+
throw new UnimplementedError();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Initialize the storage backend.
|
|
55
|
+
*/
|
|
56
|
+
async init() {
|
|
57
|
+
// Set SQLite PRAGMAs for local file databases
|
|
58
|
+
// (Turso handles these automatically for remote connections)
|
|
59
|
+
if (this.isLocal()) {
|
|
60
|
+
await this.client.execute("PRAGMA journal_mode = WAL");
|
|
61
|
+
await this.client.execute("PRAGMA busy_timeout = 5000");
|
|
62
|
+
}
|
|
63
|
+
// Create migrations table first
|
|
64
|
+
await this.createTable(TABLE_MIGRATIONS);
|
|
65
|
+
await this.migrate();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if this is a local file or in-memory database.
|
|
69
|
+
*/
|
|
70
|
+
isLocal() {
|
|
71
|
+
if (!this.url)
|
|
72
|
+
return false;
|
|
73
|
+
return this.url.startsWith("file:") || this.url === ":memory:";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Close the storage backend and cleanup resources.
|
|
77
|
+
*/
|
|
78
|
+
async close() {
|
|
79
|
+
this.client.close();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run migrations to ensure all required tables exist.
|
|
83
|
+
*/
|
|
84
|
+
async migrate() {
|
|
85
|
+
const tx = await this.client.transaction("write");
|
|
86
|
+
try {
|
|
87
|
+
// read applied migration IDs
|
|
88
|
+
const migrationsTable = `${KERNL_SCHEMA_NAME}_migrations`;
|
|
89
|
+
const result = await tx.execute(`SELECT id FROM "${migrationsTable}" ORDER BY applied_at ASC`);
|
|
90
|
+
const applied = new Set(result.rows.map((row) => row.id));
|
|
91
|
+
// filter pending migrations
|
|
92
|
+
const pending = MIGRATIONS.filter((m) => !applied.has(m.id));
|
|
93
|
+
if (pending.length === 0) {
|
|
94
|
+
await tx.commit();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// run pending migrations + insert into migrations table
|
|
98
|
+
for (const migration of pending) {
|
|
99
|
+
await migration.up({
|
|
100
|
+
client: tx,
|
|
101
|
+
createTable: async (table) => {
|
|
102
|
+
await createTable(tx, table);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
await tx.execute({
|
|
106
|
+
sql: `INSERT INTO "${migrationsTable}" (id, applied_at) VALUES (?, ?)`,
|
|
107
|
+
args: [migration.id, Date.now()],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
await tx.commit();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
await tx.rollback();
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create a table from its definition.
|
|
119
|
+
*/
|
|
120
|
+
async createTable(table) {
|
|
121
|
+
await createTable(this.client, table);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"append.test.d.ts","sourceRoot":"","sources":["../../../src/thread/__tests__/append.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { message, IN_PROGRESS, COMPLETED } from "@kernl-sdk/protocol";
|
|
3
|
+
import { create_client, create_storage, create_mock_registries, testid, } from "../../__tests__/helpers.js";
|
|
4
|
+
/** Create a ThreadEvent from a message */
|
|
5
|
+
function evt(id, tid, seq, timestamp, role = "user", text = `msg-${seq}`, metadata = {}) {
|
|
6
|
+
return {
|
|
7
|
+
...message({ role, text }),
|
|
8
|
+
id,
|
|
9
|
+
tid,
|
|
10
|
+
seq,
|
|
11
|
+
timestamp,
|
|
12
|
+
metadata,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Create a tool.call ThreadEvent */
|
|
16
|
+
function toolCallEvt(id, tid, seq, timestamp) {
|
|
17
|
+
return {
|
|
18
|
+
kind: "tool.call",
|
|
19
|
+
id,
|
|
20
|
+
tid,
|
|
21
|
+
seq,
|
|
22
|
+
timestamp,
|
|
23
|
+
callId: `call-${seq}`,
|
|
24
|
+
toolId: "test-tool",
|
|
25
|
+
state: IN_PROGRESS,
|
|
26
|
+
arguments: "{}",
|
|
27
|
+
metadata: {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Create a tool.result ThreadEvent */
|
|
31
|
+
function toolResultEvt(id, tid, seq, timestamp) {
|
|
32
|
+
return {
|
|
33
|
+
kind: "tool.result",
|
|
34
|
+
id,
|
|
35
|
+
tid,
|
|
36
|
+
seq,
|
|
37
|
+
timestamp,
|
|
38
|
+
callId: `call-${seq - 1}`,
|
|
39
|
+
toolId: "test-tool",
|
|
40
|
+
state: COMPLETED,
|
|
41
|
+
result: null,
|
|
42
|
+
error: null,
|
|
43
|
+
metadata: {},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
describe("LibSQLThreadStore append", () => {
|
|
47
|
+
let client;
|
|
48
|
+
let storage;
|
|
49
|
+
let tid;
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
client = create_client();
|
|
52
|
+
storage = create_storage(client);
|
|
53
|
+
storage.bind(create_mock_registries());
|
|
54
|
+
await storage.memories.list(); // init
|
|
55
|
+
tid = testid("thread");
|
|
56
|
+
await storage.threads.insert({
|
|
57
|
+
id: tid,
|
|
58
|
+
namespace: "default",
|
|
59
|
+
agentId: "test-agent",
|
|
60
|
+
model: "test/model",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
client.close();
|
|
65
|
+
});
|
|
66
|
+
it("appends events in order", async () => {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
await storage.threads.append([
|
|
69
|
+
evt("e1", tid, 1, new Date(now), "user"),
|
|
70
|
+
evt("e2", tid, 2, new Date(now + 1), "assistant"),
|
|
71
|
+
]);
|
|
72
|
+
const history = await storage.threads.history(tid);
|
|
73
|
+
expect(history.length).toBe(2);
|
|
74
|
+
expect(history[0].id).toBe("e1");
|
|
75
|
+
expect(history[0].seq).toBe(1);
|
|
76
|
+
expect(history[1].id).toBe("e2");
|
|
77
|
+
expect(history[1].seq).toBe(2);
|
|
78
|
+
});
|
|
79
|
+
it("preserves event data and metadata", async () => {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const metadata = { source: "api", version: 2 };
|
|
82
|
+
await storage.threads.append([
|
|
83
|
+
evt("e1", tid, 1, new Date(now), "user", "Hello, world!", metadata),
|
|
84
|
+
]);
|
|
85
|
+
const history = await storage.threads.history(tid);
|
|
86
|
+
const event = history[0];
|
|
87
|
+
expect(event.kind).toBe("message");
|
|
88
|
+
if (event.kind === "message") {
|
|
89
|
+
expect(event.role).toBe("user");
|
|
90
|
+
expect(event.content).toEqual([{ kind: "text", text: "Hello, world!" }]);
|
|
91
|
+
}
|
|
92
|
+
expect(event.metadata).toEqual(metadata);
|
|
93
|
+
expect(event.timestamp.getTime()).toBe(now);
|
|
94
|
+
});
|
|
95
|
+
it("is idempotent on duplicate ids", async () => {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
// First append
|
|
98
|
+
await storage.threads.append([evt("e1", tid, 1, new Date(now))]);
|
|
99
|
+
// Duplicate append (same id)
|
|
100
|
+
await storage.threads.append([evt("e1", tid, 1, new Date(now + 100))]);
|
|
101
|
+
const history = await storage.threads.history(tid);
|
|
102
|
+
// Should only have one event
|
|
103
|
+
expect(history.length).toBe(1);
|
|
104
|
+
// Original timestamp should be preserved
|
|
105
|
+
expect(history[0].timestamp.getTime()).toBe(now);
|
|
106
|
+
});
|
|
107
|
+
it("handles empty event list", async () => {
|
|
108
|
+
await storage.threads.append([]);
|
|
109
|
+
const history = await storage.threads.history(tid);
|
|
110
|
+
expect(history.length).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
it("handles multiple appends", async () => {
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
await storage.threads.append([evt("e1", tid, 1, new Date(now))]);
|
|
115
|
+
await storage.threads.append([
|
|
116
|
+
toolCallEvt("e2", tid, 2, new Date(now + 1)),
|
|
117
|
+
toolResultEvt("e3", tid, 3, new Date(now + 2)),
|
|
118
|
+
]);
|
|
119
|
+
await storage.threads.append([evt("e4", tid, 4, new Date(now + 3))]);
|
|
120
|
+
const history = await storage.threads.history(tid);
|
|
121
|
+
expect(history.length).toBe(4);
|
|
122
|
+
expect(history.map((e) => e.id)).toEqual(["e1", "e2", "e3", "e4"]);
|
|
123
|
+
});
|
|
124
|
+
it("handles mixed duplicate and new events", async () => {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
await storage.threads.append([
|
|
127
|
+
evt("e1", tid, 1, new Date(now)),
|
|
128
|
+
evt("e2", tid, 2, new Date(now + 1)),
|
|
129
|
+
]);
|
|
130
|
+
// Append with one duplicate and one new
|
|
131
|
+
await storage.threads.append([
|
|
132
|
+
evt("e2", tid, 2, new Date(now + 100)), // duplicate
|
|
133
|
+
evt("e3", tid, 3, new Date(now + 2)), // new
|
|
134
|
+
]);
|
|
135
|
+
const history = await storage.threads.history(tid);
|
|
136
|
+
expect(history.length).toBe(3);
|
|
137
|
+
expect(history.map((e) => e.id)).toEqual(["e1", "e2", "e3"]);
|
|
138
|
+
// e2 should have original timestamp
|
|
139
|
+
expect(history[1].timestamp.getTime()).toBe(now + 1);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.test.d.ts","sourceRoot":"","sources":["../../../src/thread/__tests__/history.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { message, IN_PROGRESS, COMPLETED } from "@kernl-sdk/protocol";
|
|
3
|
+
import { create_client, create_storage, create_mock_registries, testid, } from "../../__tests__/helpers.js";
|
|
4
|
+
/** Create a message ThreadEvent */
|
|
5
|
+
function evt(id, tid, seq, timestamp, role = "user") {
|
|
6
|
+
return {
|
|
7
|
+
...message({ role, text: `msg-${seq}` }),
|
|
8
|
+
id,
|
|
9
|
+
tid,
|
|
10
|
+
seq,
|
|
11
|
+
timestamp,
|
|
12
|
+
metadata: {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Create a tool.call ThreadEvent */
|
|
16
|
+
function toolCallEvt(id, tid, seq, timestamp) {
|
|
17
|
+
return {
|
|
18
|
+
kind: "tool.call",
|
|
19
|
+
id,
|
|
20
|
+
tid,
|
|
21
|
+
seq,
|
|
22
|
+
timestamp,
|
|
23
|
+
callId: `call-${seq}`,
|
|
24
|
+
toolId: "test-tool",
|
|
25
|
+
state: IN_PROGRESS,
|
|
26
|
+
arguments: "{}",
|
|
27
|
+
metadata: {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Create a tool.result ThreadEvent */
|
|
31
|
+
function toolResultEvt(id, tid, seq, timestamp) {
|
|
32
|
+
return {
|
|
33
|
+
kind: "tool.result",
|
|
34
|
+
id,
|
|
35
|
+
tid,
|
|
36
|
+
seq,
|
|
37
|
+
timestamp,
|
|
38
|
+
callId: `call-${seq - 1}`,
|
|
39
|
+
toolId: "test-tool",
|
|
40
|
+
state: COMPLETED,
|
|
41
|
+
result: null,
|
|
42
|
+
error: null,
|
|
43
|
+
metadata: {},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
describe("LibSQLThreadStore history", () => {
|
|
47
|
+
let client;
|
|
48
|
+
let storage;
|
|
49
|
+
let tid;
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
client = create_client();
|
|
52
|
+
storage = create_storage(client);
|
|
53
|
+
storage.bind(create_mock_registries());
|
|
54
|
+
await storage.memories.list(); // init
|
|
55
|
+
tid = testid("thread");
|
|
56
|
+
await storage.threads.insert({
|
|
57
|
+
id: tid,
|
|
58
|
+
namespace: "default",
|
|
59
|
+
agentId: "test-agent",
|
|
60
|
+
model: "test/model",
|
|
61
|
+
});
|
|
62
|
+
// Add test events
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
await storage.threads.append([
|
|
65
|
+
evt("e1", tid, 1, new Date(now)),
|
|
66
|
+
toolCallEvt("e2", tid, 2, new Date(now + 1)),
|
|
67
|
+
toolResultEvt("e3", tid, 3, new Date(now + 2)),
|
|
68
|
+
evt("e4", tid, 4, new Date(now + 3)),
|
|
69
|
+
evt("e5", tid, 5, new Date(now + 4)),
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
client.close();
|
|
74
|
+
});
|
|
75
|
+
it("returns events in asc order by default", async () => {
|
|
76
|
+
const history = await storage.threads.history(tid);
|
|
77
|
+
expect(history.length).toBe(5);
|
|
78
|
+
expect(history[0].seq).toBe(1);
|
|
79
|
+
expect(history[4].seq).toBe(5);
|
|
80
|
+
});
|
|
81
|
+
it("filters by after seq", async () => {
|
|
82
|
+
const history = await storage.threads.history(tid, { after: 2 });
|
|
83
|
+
expect(history.length).toBe(3);
|
|
84
|
+
expect(history[0].seq).toBe(3);
|
|
85
|
+
expect(history[2].seq).toBe(5);
|
|
86
|
+
});
|
|
87
|
+
it("filters by kinds", async () => {
|
|
88
|
+
const history = await storage.threads.history(tid, {
|
|
89
|
+
kinds: ["message"],
|
|
90
|
+
});
|
|
91
|
+
expect(history.length).toBe(3);
|
|
92
|
+
expect(history.every((e) => e.kind === "message")).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it("filters by multiple kinds", async () => {
|
|
95
|
+
const history = await storage.threads.history(tid, {
|
|
96
|
+
kinds: ["tool.call", "tool.result"],
|
|
97
|
+
});
|
|
98
|
+
expect(history.length).toBe(2);
|
|
99
|
+
expect(history.map((e) => e.kind)).toEqual(["tool.call", "tool.result"]);
|
|
100
|
+
});
|
|
101
|
+
it("supports desc order and limit", async () => {
|
|
102
|
+
const history = await storage.threads.history(tid, {
|
|
103
|
+
order: "desc",
|
|
104
|
+
limit: 2,
|
|
105
|
+
});
|
|
106
|
+
expect(history.length).toBe(2);
|
|
107
|
+
expect(history[0].seq).toBe(5);
|
|
108
|
+
expect(history[1].seq).toBe(4);
|
|
109
|
+
});
|
|
110
|
+
it("combines after, kinds, and limit", async () => {
|
|
111
|
+
const history = await storage.threads.history(tid, {
|
|
112
|
+
after: 1,
|
|
113
|
+
kinds: ["message"],
|
|
114
|
+
limit: 1,
|
|
115
|
+
order: "asc",
|
|
116
|
+
});
|
|
117
|
+
expect(history.length).toBe(1);
|
|
118
|
+
expect(history[0].seq).toBe(4); // First message after seq 1
|
|
119
|
+
});
|
|
120
|
+
it("returns empty array for non-existent thread", async () => {
|
|
121
|
+
const history = await storage.threads.history("nonexistent");
|
|
122
|
+
expect(history).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
it("includes history when getting thread with include option", async () => {
|
|
125
|
+
const thread = await storage.threads.get(tid, { history: true });
|
|
126
|
+
// Thread is returned with history loaded (history is private,
|
|
127
|
+
// so we verify via get returning non-null and separate history() call)
|
|
128
|
+
expect(thread).not.toBeNull();
|
|
129
|
+
expect(thread?.tid).toBe(tid);
|
|
130
|
+
// Verify events exist via direct history query
|
|
131
|
+
const history = await storage.threads.history(tid);
|
|
132
|
+
expect(history.length).toBe(5);
|
|
133
|
+
});
|
|
134
|
+
it("respects history options when getting thread", async () => {
|
|
135
|
+
const thread = await storage.threads.get(tid, {
|
|
136
|
+
history: { after: 3, limit: 2 },
|
|
137
|
+
});
|
|
138
|
+
// Thread is returned with filtered history loaded
|
|
139
|
+
expect(thread).not.toBeNull();
|
|
140
|
+
expect(thread?.tid).toBe(tid);
|
|
141
|
+
// Verify filtered history via direct query with same options
|
|
142
|
+
const history = await storage.threads.history(tid, { after: 3, limit: 2 });
|
|
143
|
+
expect(history.length).toBe(2);
|
|
144
|
+
expect(history[0].seq).toBe(4);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.test.d.ts","sourceRoot":"","sources":["../../../src/thread/__tests__/sql.test.ts"],"names":[],"mappings":""}
|