@kernl-sdk/storage 0.1.9
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 -0
- package/CHANGELOG.md +113 -0
- package/LICENSE +201 -0
- package/dist/__tests__/table.test.d.ts +2 -0
- package/dist/__tests__/table.test.d.ts.map +1 -0
- package/dist/__tests__/table.test.js +112 -0
- package/dist/base.d.ts +6 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/serde/__tests__/thread.test.d.ts +2 -0
- package/dist/serde/__tests__/thread.test.d.ts.map +1 -0
- package/dist/serde/__tests__/thread.test.js +180 -0
- package/dist/serde/thread.d.ts +89 -0
- package/dist/serde/thread.d.ts.map +1 -0
- package/dist/serde/thread.js +139 -0
- package/dist/table.d.ts +87 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +80 -0
- package/dist/thread/index.d.ts +7 -0
- package/dist/thread/index.d.ts.map +1 -0
- package/dist/thread/index.js +6 -0
- package/dist/thread/schema.d.ts +122 -0
- package/dist/thread/schema.d.ts.map +1 -0
- package/dist/thread/schema.js +141 -0
- package/dist/thread/store.d.ts +6 -0
- package/dist/thread/store.d.ts.map +1 -0
- package/dist/thread/store.js +1 -0
- package/dist/thread/types.d.ts +6 -0
- package/dist/thread/types.d.ts.map +1 -0
- package/dist/thread/types.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/package.json +51 -0
- package/src/__tests__/table.test.ts +134 -0
- package/src/base.ts +5 -0
- package/src/index.ts +8 -0
- package/src/serde/__tests__/thread.test.ts +218 -0
- package/src/serde/thread.ts +178 -0
- package/src/table.ts +199 -0
- package/src/thread/index.ts +7 -0
- package/src/thread/schema.ts +170 -0
- package/src/thread/store.ts +5 -0
- package/src/thread/types.ts +13 -0
- package/src/types.ts +67 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread serialization - codecs for converting between domain types and database records.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type Codec, neapolitanCodec } from "@kernl-sdk/shared/lib";
|
|
6
|
+
import type { IThread, ThreadEvent } from "kernl/internal";
|
|
7
|
+
import { STOPPED } from "@kernl-sdk/protocol";
|
|
8
|
+
|
|
9
|
+
import type { ThreadRecord, ThreadEventRecord } from "@/thread/schema";
|
|
10
|
+
import { ThreadRecordSchema, ThreadEventRecordSchema } from "@/thread/schema";
|
|
11
|
+
import type { NewThread } from "@/thread/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Decoded thread record - validated but not hydrated.
|
|
15
|
+
*
|
|
16
|
+
* Has all IThread fields except:
|
|
17
|
+
* - agent/model are replaced with agentId/model (string references)
|
|
18
|
+
* - input/history/task are not included (loaded separately, task becomes parentTaskId)
|
|
19
|
+
* - context is raw JSONB data (needs reconstruction into Context instance)
|
|
20
|
+
*/
|
|
21
|
+
export type DecodedThread = Omit<
|
|
22
|
+
IThread,
|
|
23
|
+
"agent" | "model" | "input" | "history" | "task" | "context"
|
|
24
|
+
> & {
|
|
25
|
+
namespace: string;
|
|
26
|
+
agentId: string;
|
|
27
|
+
model: string; // composite: "provider/modelId"
|
|
28
|
+
parentTaskId: string | null; // stored task reference
|
|
29
|
+
context: unknown; // raw JSONB - reconstruct with new Context(data)
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/* ---- Codecs ---- */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Thread codec - converts between DecodedThread and ThreadRecord.
|
|
36
|
+
*/
|
|
37
|
+
const rawThreadCodec: Codec<DecodedThread, ThreadRecord> = {
|
|
38
|
+
encode(decoded: DecodedThread): ThreadRecord {
|
|
39
|
+
return {
|
|
40
|
+
id: decoded.tid,
|
|
41
|
+
namespace: decoded.namespace,
|
|
42
|
+
agent_id: decoded.agentId,
|
|
43
|
+
model: decoded.model,
|
|
44
|
+
parent_task_id: decoded.parentTaskId,
|
|
45
|
+
context: decoded.context,
|
|
46
|
+
tick: decoded.tick,
|
|
47
|
+
state: decoded.state,
|
|
48
|
+
created_at: decoded.createdAt.getTime(),
|
|
49
|
+
updated_at: decoded.updatedAt.getTime(),
|
|
50
|
+
metadata: decoded.metadata,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
decode(record: ThreadRecord): DecodedThread {
|
|
55
|
+
return {
|
|
56
|
+
tid: record.id,
|
|
57
|
+
namespace: record.namespace,
|
|
58
|
+
agentId: record.agent_id,
|
|
59
|
+
model: record.model,
|
|
60
|
+
parentTaskId: record.parent_task_id,
|
|
61
|
+
context: record.context,
|
|
62
|
+
tick: record.tick,
|
|
63
|
+
state: record.state,
|
|
64
|
+
createdAt: new Date(record.created_at),
|
|
65
|
+
updatedAt: new Date(record.updated_at),
|
|
66
|
+
metadata: record.metadata,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const ThreadCodec = neapolitanCodec({
|
|
72
|
+
codec: rawThreadCodec,
|
|
73
|
+
schema: ThreadRecordSchema,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* ThreadEvent codec - converts between flat ThreadEvent and ThreadEventRecord.
|
|
78
|
+
*
|
|
79
|
+
* App layer: {kind, ...dataFields, id, tid, seq, timestamp, metadata}
|
|
80
|
+
* DB layer: {kind, data: {...dataFields}, id, tid, seq, timestamp, metadata}
|
|
81
|
+
*/
|
|
82
|
+
const rawThreadEventCodec: Codec<ThreadEvent, ThreadEventRecord> = {
|
|
83
|
+
encode(event: ThreadEvent): ThreadEventRecord {
|
|
84
|
+
const base = {
|
|
85
|
+
id: event.id,
|
|
86
|
+
tid: event.tid,
|
|
87
|
+
seq: event.seq,
|
|
88
|
+
timestamp: event.timestamp.getTime(),
|
|
89
|
+
metadata: event.metadata ?? null,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// System events have no data
|
|
93
|
+
if (event.kind === "system") {
|
|
94
|
+
return { ...base, kind: "system", data: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Extract data (everything except base fields and kind)
|
|
98
|
+
const { id, tid, seq, timestamp, metadata, kind, ...data } = event as any;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...base,
|
|
102
|
+
kind: event.kind,
|
|
103
|
+
data,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
decode(record: ThreadEventRecord): ThreadEvent {
|
|
108
|
+
const base = {
|
|
109
|
+
id: record.id,
|
|
110
|
+
tid: record.tid,
|
|
111
|
+
seq: record.seq,
|
|
112
|
+
timestamp: new Date(record.timestamp),
|
|
113
|
+
metadata: record.metadata ?? {},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// System events have no data
|
|
117
|
+
if (record.kind === "system") {
|
|
118
|
+
return { ...base, kind: "system" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// For non-system events, data is always a record (enforced by schema)
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
kind: record.kind,
|
|
125
|
+
...record.data,
|
|
126
|
+
} as ThreadEvent;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const ThreadEventRecordCodec = neapolitanCodec({
|
|
131
|
+
codec: rawThreadEventCodec,
|
|
132
|
+
schema: ThreadEventRecordSchema,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* NewThread codec - converts NewThread input to ThreadRecord for insertion.
|
|
137
|
+
*
|
|
138
|
+
* Handles default values at encoding time (tick=0, state="stopped", timestamps=now).
|
|
139
|
+
*/
|
|
140
|
+
const rawNewThreadCodec: Codec<NewThread, ThreadRecord> = {
|
|
141
|
+
encode(thread: NewThread): ThreadRecord {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
return {
|
|
144
|
+
id: thread.id,
|
|
145
|
+
namespace: thread.namespace,
|
|
146
|
+
agent_id: thread.agentId,
|
|
147
|
+
model: thread.model,
|
|
148
|
+
context: thread.context ?? {},
|
|
149
|
+
tick: thread.tick ?? 0,
|
|
150
|
+
state: thread.state ?? STOPPED,
|
|
151
|
+
parent_task_id: thread.parentTaskId ?? null,
|
|
152
|
+
metadata: thread.metadata ?? null,
|
|
153
|
+
created_at: thread.createdAt?.getTime() ?? now,
|
|
154
|
+
updated_at: thread.updatedAt?.getTime() ?? now,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
decode(record: ThreadRecord): NewThread {
|
|
159
|
+
return {
|
|
160
|
+
id: record.id,
|
|
161
|
+
namespace: record.namespace,
|
|
162
|
+
agentId: record.agent_id,
|
|
163
|
+
model: record.model,
|
|
164
|
+
context: record.context,
|
|
165
|
+
tick: record.tick,
|
|
166
|
+
state: record.state,
|
|
167
|
+
parentTaskId: record.parent_task_id,
|
|
168
|
+
metadata: record.metadata,
|
|
169
|
+
createdAt: new Date(record.created_at),
|
|
170
|
+
updatedAt: new Date(record.updated_at),
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const NewThreadCodec = neapolitanCodec({
|
|
176
|
+
codec: rawNewThreadCodec,
|
|
177
|
+
schema: ThreadRecordSchema,
|
|
178
|
+
});
|
package/src/table.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema name for all kernl tables.
|
|
3
|
+
*/
|
|
4
|
+
export const SCHEMA_NAME = "kernl";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Table definition with name, columns, and optional constraints.
|
|
8
|
+
*/
|
|
9
|
+
export interface Table<
|
|
10
|
+
Name extends string,
|
|
11
|
+
Cols extends Record<string, Column>,
|
|
12
|
+
> {
|
|
13
|
+
name: Name;
|
|
14
|
+
columns: Cols;
|
|
15
|
+
constraints?: TableConstraint[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Define a table with name, columns, and optional constraints.
|
|
20
|
+
*/
|
|
21
|
+
export function defineTable<
|
|
22
|
+
const Name extends string,
|
|
23
|
+
const Cols extends Record<string, Column>,
|
|
24
|
+
>(
|
|
25
|
+
name: Name,
|
|
26
|
+
columns: Cols,
|
|
27
|
+
constraints?: TableConstraint[],
|
|
28
|
+
): Table<Name, Cols> {
|
|
29
|
+
// set table/column metadata on each column for FK resolution
|
|
30
|
+
for (const cname in columns) {
|
|
31
|
+
columns[cname]._table = name;
|
|
32
|
+
columns[cname]._column = cname;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { name, columns, constraints };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ---- Column Types ---- */
|
|
39
|
+
|
|
40
|
+
export type ColumnType = "text" | "integer" | "bigint" | "jsonb";
|
|
41
|
+
export type OnDeleteAction = "CASCADE" | "SET NULL" | "RESTRICT";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Column definition with type and constraints.
|
|
45
|
+
*/
|
|
46
|
+
export interface Column<T = unknown> {
|
|
47
|
+
type: ColumnType;
|
|
48
|
+
|
|
49
|
+
_table?: string;
|
|
50
|
+
_column?: string;
|
|
51
|
+
|
|
52
|
+
// Constraints
|
|
53
|
+
_pk?: boolean;
|
|
54
|
+
_nullable?: boolean;
|
|
55
|
+
_unique?: boolean;
|
|
56
|
+
_default?: T;
|
|
57
|
+
_fk?: {
|
|
58
|
+
table: string;
|
|
59
|
+
column: string;
|
|
60
|
+
};
|
|
61
|
+
_onDelete?: OnDeleteAction;
|
|
62
|
+
|
|
63
|
+
// -- builders --
|
|
64
|
+
primaryKey(): Column<T>;
|
|
65
|
+
nullable(): Column<T>;
|
|
66
|
+
default(val: T): Column<T>;
|
|
67
|
+
unique(): Column<T>;
|
|
68
|
+
references(
|
|
69
|
+
ref: () => Column,
|
|
70
|
+
opts?: { onDelete?: OnDeleteAction },
|
|
71
|
+
): Column<T>;
|
|
72
|
+
|
|
73
|
+
// -- codec --
|
|
74
|
+
encode(val: T): string;
|
|
75
|
+
decode(val: string): T;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const text = (): Column<string> => col("text");
|
|
79
|
+
export const integer = (): Column<number> => col("integer");
|
|
80
|
+
export const bigint = (): Column<number> => col("bigint");
|
|
81
|
+
export const jsonb = <T = unknown>(): Column<T> => col("jsonb");
|
|
82
|
+
|
|
83
|
+
export const timestamps = {
|
|
84
|
+
created_at: bigint(),
|
|
85
|
+
updated_at: bigint(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function col<T>(type: ColumnType): Column<T> {
|
|
89
|
+
const c: Column<T> = {
|
|
90
|
+
type,
|
|
91
|
+
|
|
92
|
+
primaryKey() {
|
|
93
|
+
this._pk = true;
|
|
94
|
+
return this;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
nullable() {
|
|
98
|
+
this._nullable = true;
|
|
99
|
+
return this;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
default(val) {
|
|
103
|
+
this._default = val;
|
|
104
|
+
return this;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
unique() {
|
|
108
|
+
this._unique = true;
|
|
109
|
+
return this;
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
references(ref, opts) {
|
|
113
|
+
const targetCol = ref();
|
|
114
|
+
if (targetCol._table && targetCol._column) {
|
|
115
|
+
this._fk = {
|
|
116
|
+
table: targetCol._table,
|
|
117
|
+
column: targetCol._column,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
this._onDelete = opts?.onDelete;
|
|
121
|
+
return this;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
encode(val: T): string {
|
|
125
|
+
if (val === null || val === undefined) return "NULL";
|
|
126
|
+
|
|
127
|
+
switch (this.type) {
|
|
128
|
+
case "text":
|
|
129
|
+
return `'${String(val).replace(/'/g, "''")}'`;
|
|
130
|
+
case "integer":
|
|
131
|
+
case "bigint":
|
|
132
|
+
return String(val);
|
|
133
|
+
case "jsonb":
|
|
134
|
+
return `'${JSON.stringify(val)}'`;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
decode(val: string): T {
|
|
139
|
+
switch (this.type) {
|
|
140
|
+
case "text":
|
|
141
|
+
return val as T;
|
|
142
|
+
case "integer":
|
|
143
|
+
case "bigint":
|
|
144
|
+
return Number.parseInt(val, 10) as T;
|
|
145
|
+
case "jsonb":
|
|
146
|
+
return JSON.parse(val) as T;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return c;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ---- Constraints ---- */
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Table-level constraint types.
|
|
158
|
+
*/
|
|
159
|
+
export type TableConstraint =
|
|
160
|
+
| UniqueConstraint
|
|
161
|
+
| PrimaryKeyConstraint
|
|
162
|
+
| ForeignKeyConstraint
|
|
163
|
+
| CheckConstraint
|
|
164
|
+
| IndexConstraint;
|
|
165
|
+
|
|
166
|
+
export interface UniqueConstraint {
|
|
167
|
+
readonly kind: "unique";
|
|
168
|
+
columns: string[];
|
|
169
|
+
name?: string; // optional constraint name
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface IndexConstraint {
|
|
173
|
+
readonly kind: "index";
|
|
174
|
+
columns: string[];
|
|
175
|
+
unique?: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface PrimaryKeyConstraint {
|
|
179
|
+
readonly kind: "pkey";
|
|
180
|
+
columns: string[];
|
|
181
|
+
name?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ForeignKeyConstraint {
|
|
185
|
+
readonly kind: "fkey";
|
|
186
|
+
columns: string[];
|
|
187
|
+
references: {
|
|
188
|
+
table: string;
|
|
189
|
+
columns: string[];
|
|
190
|
+
};
|
|
191
|
+
onDelete?: OnDeleteAction;
|
|
192
|
+
name?: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface CheckConstraint {
|
|
196
|
+
readonly kind: "check";
|
|
197
|
+
expression: string;
|
|
198
|
+
name?: string;
|
|
199
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread record schemas and table definitions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { THREAD_STATES } from "kernl";
|
|
7
|
+
import { text, jsonb, bigint, integer, timestamps, defineTable } from "@/table";
|
|
8
|
+
|
|
9
|
+
/* ---- Table Definitions ---- */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Threads table schema.
|
|
13
|
+
*/
|
|
14
|
+
export const TABLE_THREADS = defineTable(
|
|
15
|
+
"threads",
|
|
16
|
+
{
|
|
17
|
+
id: text().primaryKey(),
|
|
18
|
+
namespace: text().default("kernl"),
|
|
19
|
+
agent_id: text(),
|
|
20
|
+
model: text(),
|
|
21
|
+
context: jsonb(),
|
|
22
|
+
parent_task_id: text().nullable(),
|
|
23
|
+
tick: integer().default(0),
|
|
24
|
+
state: text(),
|
|
25
|
+
metadata: jsonb().nullable(),
|
|
26
|
+
...timestamps,
|
|
27
|
+
},
|
|
28
|
+
[
|
|
29
|
+
{ kind: "index", columns: ["state"] },
|
|
30
|
+
{ kind: "index", columns: ["namespace"] },
|
|
31
|
+
{ kind: "index", columns: ["agent_id"] },
|
|
32
|
+
{ kind: "index", columns: ["parent_task_id"] },
|
|
33
|
+
{ kind: "index", columns: ["created_at"] },
|
|
34
|
+
{ kind: "index", columns: ["updated_at"] },
|
|
35
|
+
],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Thread events table schema.
|
|
40
|
+
*/
|
|
41
|
+
export const TABLE_THREAD_EVENTS = defineTable(
|
|
42
|
+
"thread_events",
|
|
43
|
+
{
|
|
44
|
+
id: text(),
|
|
45
|
+
tid: text().references(() => TABLE_THREADS.columns.id, {
|
|
46
|
+
onDelete: "CASCADE",
|
|
47
|
+
}),
|
|
48
|
+
seq: integer(),
|
|
49
|
+
kind: text(),
|
|
50
|
+
timestamp: bigint(),
|
|
51
|
+
data: jsonb().nullable(),
|
|
52
|
+
metadata: jsonb().nullable(),
|
|
53
|
+
},
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
kind: "unique",
|
|
57
|
+
columns: ["tid", "id"],
|
|
58
|
+
},
|
|
59
|
+
{ kind: "index", columns: ["tid", "seq"] }, // for ordering events within a thread
|
|
60
|
+
{ kind: "index", columns: ["tid", "kind"] }, // for filtering by thread + kind
|
|
61
|
+
],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Migrations table.
|
|
66
|
+
*/
|
|
67
|
+
export const TABLE_MIGRATIONS = defineTable("migrations", {
|
|
68
|
+
id: text().primaryKey(),
|
|
69
|
+
applied_at: bigint(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/* ---- Record Schemas ---- */
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Thread record schema (zod-first).
|
|
76
|
+
*/
|
|
77
|
+
export const ThreadRecordSchema = z.object({
|
|
78
|
+
id: z.string(),
|
|
79
|
+
namespace: z.string(),
|
|
80
|
+
agent_id: z.string(),
|
|
81
|
+
model: z.string(), // composite: "provider/modelId"
|
|
82
|
+
context: z.unknown(), // JSONB - Context<TContext>
|
|
83
|
+
parent_task_id: z.string().nullable(),
|
|
84
|
+
tick: z.number().int().nonnegative(),
|
|
85
|
+
state: z.enum(THREAD_STATES),
|
|
86
|
+
created_at: z.number().int(),
|
|
87
|
+
updated_at: z.number().int(),
|
|
88
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export type ThreadRecord = z.infer<typeof ThreadRecordSchema>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Thread event inner data - stored in JSONB column.
|
|
95
|
+
*
|
|
96
|
+
* Always an object (record) for non-system events. Inner data is validated by protocol layer
|
|
97
|
+
* and is already JSON-serializable. The actual structure depends on the event kind:
|
|
98
|
+
* - message: {role, content, ...}
|
|
99
|
+
* - tool-call: {callId, toolId, state, arguments}
|
|
100
|
+
* - tool-result: {callId, toolId, state, result, error}
|
|
101
|
+
* - reasoning: {text}
|
|
102
|
+
* - system: null (handled separately)
|
|
103
|
+
*/
|
|
104
|
+
export const ThreadEventInnerSchema = z.record(z.string(), z.unknown());
|
|
105
|
+
|
|
106
|
+
export type ThreadEventInner = z.infer<typeof ThreadEventInnerSchema>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Thread event record base schema - common fields for all events.
|
|
110
|
+
*/
|
|
111
|
+
const ThreadEventRecordBaseSchema = z.object({
|
|
112
|
+
id: z.string(),
|
|
113
|
+
tid: z.string(),
|
|
114
|
+
seq: z.number().int().nonnegative(),
|
|
115
|
+
timestamp: z.number().int(), // epoch millis
|
|
116
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Message event record (user, assistant, system messages).
|
|
121
|
+
*/
|
|
122
|
+
const ThreadMessageEventRecordSchema = ThreadEventRecordBaseSchema.extend({
|
|
123
|
+
kind: z.literal("message"),
|
|
124
|
+
data: ThreadEventInnerSchema, // Message data: {role, content, ...}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reasoning event record.
|
|
129
|
+
*/
|
|
130
|
+
const ThreadReasoningEventRecordSchema = ThreadEventRecordBaseSchema.extend({
|
|
131
|
+
kind: z.literal("reasoning"),
|
|
132
|
+
data: ThreadEventInnerSchema, // Reasoning data: {text}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Tool call event record.
|
|
137
|
+
*/
|
|
138
|
+
const ThreadToolCallEventRecordSchema = ThreadEventRecordBaseSchema.extend({
|
|
139
|
+
kind: z.literal("tool-call"),
|
|
140
|
+
data: ThreadEventInnerSchema, // ToolCall data: {callId, toolId, state, arguments}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Tool result event record.
|
|
145
|
+
*/
|
|
146
|
+
const ThreadToolResultEventRecordSchema = ThreadEventRecordBaseSchema.extend({
|
|
147
|
+
kind: z.literal("tool-result"),
|
|
148
|
+
data: ThreadEventInnerSchema, // ToolResult data: {callId, toolId, state, result, error}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* System event record - runtime state changes (not sent to model).
|
|
153
|
+
*/
|
|
154
|
+
const ThreadSystemEventRecordSchema = ThreadEventRecordBaseSchema.extend({
|
|
155
|
+
kind: z.literal("system"),
|
|
156
|
+
data: z.null(), // System events have no data
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Thread event record schema (discriminated union by kind).
|
|
161
|
+
*/
|
|
162
|
+
export const ThreadEventRecordSchema = z.discriminatedUnion("kind", [
|
|
163
|
+
ThreadMessageEventRecordSchema,
|
|
164
|
+
ThreadReasoningEventRecordSchema,
|
|
165
|
+
ThreadToolCallEventRecordSchema,
|
|
166
|
+
ThreadToolResultEventRecordSchema,
|
|
167
|
+
ThreadSystemEventRecordSchema,
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
export type ThreadEventRecord = z.infer<typeof ThreadEventRecordSchema>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread storage DTOs are now defined in kernl.
|
|
3
|
+
* Re-export them here for convenience.
|
|
4
|
+
*/
|
|
5
|
+
export type {
|
|
6
|
+
NewThread,
|
|
7
|
+
ThreadUpdate,
|
|
8
|
+
ThreadFilter,
|
|
9
|
+
ThreadInclude,
|
|
10
|
+
SortOrder,
|
|
11
|
+
ThreadListOptions,
|
|
12
|
+
ThreadHistoryOptions,
|
|
13
|
+
} from "kernl";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core storage abstractions for Kernl.
|
|
3
|
+
*
|
|
4
|
+
* Defines the top-level storage interface and transaction primitives.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ThreadStore } from "./thread";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The main storage interface for Kernl.
|
|
11
|
+
*
|
|
12
|
+
* Provides access to system stores (threads, tasks, traces) and transaction support.
|
|
13
|
+
*/
|
|
14
|
+
export interface KernlStorage {
|
|
15
|
+
/**
|
|
16
|
+
* Thread store - manages thread execution records and event history.
|
|
17
|
+
*/
|
|
18
|
+
threads: ThreadStore;
|
|
19
|
+
|
|
20
|
+
// Future stores (deferred)
|
|
21
|
+
// tasks: TaskStore;
|
|
22
|
+
// traces: TraceStore;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the storage backend.
|
|
26
|
+
*
|
|
27
|
+
* Connects to the database and ensures all required schemas/tables exist.
|
|
28
|
+
*/
|
|
29
|
+
init(): Promise<void>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Close the storage backend and cleanup resources.
|
|
33
|
+
*/
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute a function within a transaction.
|
|
38
|
+
*
|
|
39
|
+
* All operations performed using the transaction-scoped stores will be
|
|
40
|
+
* committed atomically or rolled back on error.
|
|
41
|
+
*/
|
|
42
|
+
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Transaction context providing transactional access to stores.
|
|
47
|
+
*/
|
|
48
|
+
export interface Transaction {
|
|
49
|
+
/**
|
|
50
|
+
* Thread store within this transaction.
|
|
51
|
+
*/
|
|
52
|
+
threads: ThreadStore;
|
|
53
|
+
|
|
54
|
+
// Future stores (deferred)
|
|
55
|
+
// tasks: TaskStore;
|
|
56
|
+
// traces: TraceStore;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Commit the transaction.
|
|
60
|
+
*/
|
|
61
|
+
commit(): Promise<void>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rollback the transaction.
|
|
65
|
+
*/
|
|
66
|
+
rollback(): Promise<void>;
|
|
67
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"baseUrl": ".",
|
|
7
|
+
"paths": {
|
|
8
|
+
"@/*": ["./src/*"],
|
|
9
|
+
"kernl": ["../kernl/src/index.ts"],
|
|
10
|
+
"kernl/internal": ["../kernl/src/internal.ts"]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
globals: true,
|
|
7
|
+
environment: "node",
|
|
8
|
+
exclude: ["**/node_modules/**", "**/dist/**"],
|
|
9
|
+
},
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
"@": path.resolve(__dirname, "./src"),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|