@nt-ai-lab/deterministic-agent-workflow-event-store 0.1.0
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/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/platform/domain/sqlite-event-store.d.ts +27 -0
- package/dist/platform/domain/sqlite-event-store.js +103 -0
- package/dist/platform/infra/external-clients/sqlite/sqlite-runtime.d.ts +20 -0
- package/dist/platform/infra/external-clients/sqlite/sqlite-runtime.js +64 -0
- package/package.json +20 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { SqliteEventStore } from './platform/domain/sqlite-event-store';
|
|
2
|
+
export { createStore, resolveSessionId, } from './platform/domain/sqlite-event-store';
|
|
3
|
+
export type { SqliteDatabase, SqliteStatement, } from './platform/infra/external-clients/sqlite/sqlite-runtime';
|
|
4
|
+
export { enableWalMode, openSqliteDatabase, } from './platform/infra/external-clients/sqlite/sqlite-runtime';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type SqliteDatabase } from '../infra/external-clients/sqlite/sqlite-runtime';
|
|
3
|
+
declare const passthroughEventSchema: z.ZodObject<{
|
|
4
|
+
type: z.ZodString;
|
|
5
|
+
at: z.ZodString;
|
|
6
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
7
|
+
type: z.ZodString;
|
|
8
|
+
at: z.ZodString;
|
|
9
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
10
|
+
type: z.ZodString;
|
|
11
|
+
at: z.ZodString;
|
|
12
|
+
}, z.ZodTypeAny, "passthrough">>;
|
|
13
|
+
type BaseEvent = z.infer<typeof passthroughEventSchema>;
|
|
14
|
+
/** @riviere-role value-object */
|
|
15
|
+
export type SqliteEventStore = {
|
|
16
|
+
readonly readEvents: (sessionId: string) => readonly BaseEvent[];
|
|
17
|
+
readonly appendEvents: (sessionId: string, events: readonly BaseEvent[]) => void;
|
|
18
|
+
readonly sessionExists: (sessionId: string) => boolean;
|
|
19
|
+
readonly hasSessionStarted: (sessionId: string) => boolean;
|
|
20
|
+
readonly listSessions: () => readonly string[];
|
|
21
|
+
readonly db: SqliteDatabase;
|
|
22
|
+
};
|
|
23
|
+
/** @riviere-role domain-service */
|
|
24
|
+
export declare function createStore(dbPath: string): SqliteEventStore;
|
|
25
|
+
/** @riviere-role domain-service */
|
|
26
|
+
export declare function resolveSessionId(store: SqliteEventStore, input: string): string;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { WorkflowStateError } from '@nt-ai-lab/deterministic-agent-workflow-engine';
|
|
3
|
+
import { enableWalMode, openSqliteDatabase, } from '../infra/external-clients/sqlite/sqlite-runtime.js';
|
|
4
|
+
const passthroughEventSchema = z.object({
|
|
5
|
+
type: z.string(),
|
|
6
|
+
at: z.string(),
|
|
7
|
+
}).passthrough();
|
|
8
|
+
const createTableSql = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
10
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
session_id TEXT NOT NULL,
|
|
12
|
+
type TEXT NOT NULL,
|
|
13
|
+
at TEXT NOT NULL,
|
|
14
|
+
payload TEXT NOT NULL
|
|
15
|
+
)
|
|
16
|
+
`;
|
|
17
|
+
const rowWithPayloadSchema = z.array(z.object({ payload: z.string() }));
|
|
18
|
+
const rowWithSessionIdSchema = z.array(z.object({ session_id: z.string() }));
|
|
19
|
+
const countFieldSchema = z.union([z.number(), z.bigint(), z.string()]);
|
|
20
|
+
const countRowSchema = z.object({ count: countFieldSchema });
|
|
21
|
+
/** @riviere-role domain-service */
|
|
22
|
+
export function createStore(dbPath) {
|
|
23
|
+
const db = openSqliteDatabase(dbPath);
|
|
24
|
+
enableWalMode(db);
|
|
25
|
+
db.exec(createTableSql);
|
|
26
|
+
return {
|
|
27
|
+
db,
|
|
28
|
+
readEvents(sessionId) {
|
|
29
|
+
const rawRows = db.prepare('SELECT payload FROM events WHERE session_id = ? ORDER BY seq').all(sessionId);
|
|
30
|
+
const rows = rowWithPayloadSchema.parse(rawRows);
|
|
31
|
+
return rows.map((row, index) => {
|
|
32
|
+
const parsedPayload = tryParsePayload(row.payload, index);
|
|
33
|
+
const parsedEvent = passthroughEventSchema.safeParse(parsedPayload);
|
|
34
|
+
if (!parsedEvent.success) {
|
|
35
|
+
throw new WorkflowStateError(`Invalid event at index ${index} for session ${sessionId}: ${parsedEvent.error.message}`);
|
|
36
|
+
}
|
|
37
|
+
return parsedEvent.data;
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
appendEvents(sessionId, events) {
|
|
41
|
+
if (events.length === 0)
|
|
42
|
+
return;
|
|
43
|
+
const insert = db.prepare('INSERT INTO events (session_id, type, at, payload) VALUES (?, ?, ?, ?)');
|
|
44
|
+
db.exec('BEGIN IMMEDIATE');
|
|
45
|
+
try {
|
|
46
|
+
for (const event of events) {
|
|
47
|
+
insert.run(sessionId, event.type, event.at, JSON.stringify(event));
|
|
48
|
+
}
|
|
49
|
+
db.exec('COMMIT');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
db.exec('ROLLBACK');
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
sessionExists(sessionId) {
|
|
57
|
+
return readCount(db, 'SELECT COUNT(1) AS count FROM events WHERE session_id = ?', sessionId) > 0;
|
|
58
|
+
},
|
|
59
|
+
hasSessionStarted(sessionId) {
|
|
60
|
+
return readCount(db, "SELECT COUNT(1) AS count FROM events WHERE session_id = ? AND type = 'session-started'", sessionId) > 0;
|
|
61
|
+
},
|
|
62
|
+
listSessions() {
|
|
63
|
+
const rawRows = db.prepare('SELECT session_id FROM events GROUP BY session_id ORDER BY MIN(seq)').all();
|
|
64
|
+
return rowWithSessionIdSchema.parse(rawRows).map((row) => row.session_id);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function readCount(db, query, sessionId) {
|
|
69
|
+
const rawRow = db.prepare(query).get(sessionId);
|
|
70
|
+
if (rawRow === undefined || rawRow === null)
|
|
71
|
+
return 0;
|
|
72
|
+
const parsed = countRowSchema.safeParse(rawRow);
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
throw new WorkflowStateError(`Invalid count query row for session ${sessionId}: ${parsed.error.message}`);
|
|
75
|
+
}
|
|
76
|
+
const normalized = Number(parsed.data.count);
|
|
77
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
78
|
+
throw new WorkflowStateError(`Invalid count value for session ${sessionId}: ${String(parsed.data.count)}`);
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
/** @riviere-role domain-service */
|
|
83
|
+
export function resolveSessionId(store, input) {
|
|
84
|
+
if (store.sessionExists(input))
|
|
85
|
+
return input;
|
|
86
|
+
const prefixMatches = store.listSessions().filter((session) => session.startsWith(input));
|
|
87
|
+
const singleMatch = prefixMatches.length === 1 ? prefixMatches[0] : undefined;
|
|
88
|
+
if (singleMatch !== undefined)
|
|
89
|
+
return singleMatch;
|
|
90
|
+
if (prefixMatches.length > 1) {
|
|
91
|
+
const matches = prefixMatches.map((session) => ` ${session}`).join('\n');
|
|
92
|
+
throw new WorkflowStateError(`Ambiguous session prefix "${input}". Matches:\n${matches}`);
|
|
93
|
+
}
|
|
94
|
+
throw new WorkflowStateError(`No events found for session "${input}". Run "analyze --all" to list available sessions.`);
|
|
95
|
+
}
|
|
96
|
+
function tryParsePayload(payload, index) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(payload);
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
throw new WorkflowStateError(`Cannot parse event payload at index ${index}: ${String(cause)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @riviere-role external-client-model */
|
|
2
|
+
export type SqliteStatement = {
|
|
3
|
+
readonly all: (...params: readonly unknown[]) => readonly unknown[];
|
|
4
|
+
readonly get: (...params: readonly unknown[]) => unknown | undefined;
|
|
5
|
+
readonly run: (...params: readonly unknown[]) => unknown;
|
|
6
|
+
};
|
|
7
|
+
/** @riviere-role external-client-model */
|
|
8
|
+
export type SqliteDatabase = {
|
|
9
|
+
readonly prepare: (sql: string) => SqliteStatement;
|
|
10
|
+
readonly exec: (sql: string) => void;
|
|
11
|
+
readonly close: () => void;
|
|
12
|
+
};
|
|
13
|
+
type OpenOptions = {
|
|
14
|
+
readonly readonly?: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** @riviere-role external-client-service */
|
|
17
|
+
export declare function openSqliteDatabase(path: string, options?: OpenOptions): SqliteDatabase;
|
|
18
|
+
/** @riviere-role external-client-service */
|
|
19
|
+
export declare function enableWalMode(database: SqliteDatabase): void;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const sqliteFactory = loadSqliteFactory();
|
|
4
|
+
/** @riviere-role external-client-service */
|
|
5
|
+
export function openSqliteDatabase(path, options = {}) {
|
|
6
|
+
return wrapSqliteDatabase(sqliteFactory.open(path, options));
|
|
7
|
+
}
|
|
8
|
+
/** @riviere-role external-client-service */
|
|
9
|
+
export function enableWalMode(database) {
|
|
10
|
+
database.exec('PRAGMA journal_mode = WAL');
|
|
11
|
+
}
|
|
12
|
+
function loadSqliteFactory() {
|
|
13
|
+
if (process.versions['bun'] !== undefined) {
|
|
14
|
+
return loadBunSqliteFactory();
|
|
15
|
+
}
|
|
16
|
+
return loadNodeSqliteFactory();
|
|
17
|
+
}
|
|
18
|
+
function loadBunSqliteFactory() {
|
|
19
|
+
const requiredModule = require('bun:sqlite');
|
|
20
|
+
if (!isBunSqliteModule(requiredModule)) {
|
|
21
|
+
throw new TypeError('bun:sqlite did not expose Database.');
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
open(path, options) {
|
|
25
|
+
return options.readonly === true
|
|
26
|
+
? new requiredModule.Database(path, { readonly: true })
|
|
27
|
+
: new requiredModule.Database(path);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function loadNodeSqliteFactory() {
|
|
32
|
+
const requiredModule = require('node:sqlite');
|
|
33
|
+
if (!isNodeSqliteModule(requiredModule)) {
|
|
34
|
+
throw new TypeError('node:sqlite did not expose DatabaseSync.');
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
open(path, options) {
|
|
38
|
+
return new requiredModule.DatabaseSync(path, { readOnly: options.readonly === true });
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function wrapSqliteDatabase(db) {
|
|
43
|
+
return {
|
|
44
|
+
prepare: (sql) => wrapSqliteStatement(db.prepare(sql)),
|
|
45
|
+
exec: (sql) => db.exec(sql),
|
|
46
|
+
close: () => db.close(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function wrapSqliteStatement(statement) {
|
|
50
|
+
return {
|
|
51
|
+
all: (...params) => statement.all(...params),
|
|
52
|
+
get: (...params) => normalizeGetResult(statement.get(...params)),
|
|
53
|
+
run: (...params) => statement.run(...params),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function normalizeGetResult(row) {
|
|
57
|
+
return row === null ? undefined : row;
|
|
58
|
+
}
|
|
59
|
+
function isBunSqliteModule(value) {
|
|
60
|
+
return typeof value === 'object' && value !== null && 'Database' in value;
|
|
61
|
+
}
|
|
62
|
+
function isNodeSqliteModule(value) {
|
|
63
|
+
return typeof value === 'object' && value !== null && 'DatabaseSync' in value;
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nt-ai-lab/deterministic-agent-workflow-event-store",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@nt-ai-lab/deterministic-agent-workflow-engine": "workspace:*",
|
|
15
|
+
"zod": "^3.25.76"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
}
|
|
20
|
+
}
|