@nt-ai-lab/deterministic-agent-workflow-event-store 0.2.0 → 0.3.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.
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RecordReflectionInput, StoredEvent, StoredReflection } from '@nt-ai-lab/deterministic-agent-workflow-engine';
|
|
2
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
3
|
/** @riviere-role value-object */
|
|
15
4
|
export type SqliteEventStore = {
|
|
16
|
-
readonly readEvents: (sessionId: string) => readonly
|
|
17
|
-
readonly appendEvents: (sessionId: string, events: readonly
|
|
5
|
+
readonly readEvents: (sessionId: string) => readonly StoredEvent[];
|
|
6
|
+
readonly appendEvents: (sessionId: string, events: readonly StoredEvent[]) => void;
|
|
18
7
|
readonly sessionExists: (sessionId: string) => boolean;
|
|
19
8
|
readonly hasSessionStarted: (sessionId: string) => boolean;
|
|
9
|
+
readonly recordReflection: (sessionId: string, createdAt: string, input: RecordReflectionInput) => StoredReflection;
|
|
10
|
+
readonly listReflections: (sessionId: string) => readonly StoredReflection[];
|
|
20
11
|
readonly listSessions: () => readonly string[];
|
|
21
12
|
readonly db: SqliteDatabase;
|
|
22
13
|
};
|
|
@@ -24,4 +15,3 @@ export type SqliteEventStore = {
|
|
|
24
15
|
export declare function createStore(dbPath: string): SqliteEventStore;
|
|
25
16
|
/** @riviere-role domain-service */
|
|
26
17
|
export declare function resolveSessionId(store: SqliteEventStore, input: string): string;
|
|
27
|
-
export {};
|
|
@@ -1,50 +1,74 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { WorkflowStateError } from '@nt-ai-lab/deterministic-agent-workflow-engine';
|
|
2
|
+
import { recordReflectionInputSchema, storedReflectionSchema, stripEnvelopeKeys, WorkflowStateError, } from '@nt-ai-lab/deterministic-agent-workflow-engine';
|
|
3
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
4
|
const createTableSql = `
|
|
9
5
|
CREATE TABLE IF NOT EXISTS events (
|
|
10
6
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
7
|
session_id TEXT NOT NULL,
|
|
12
8
|
type TEXT NOT NULL,
|
|
13
9
|
at TEXT NOT NULL,
|
|
10
|
+
state TEXT,
|
|
14
11
|
payload TEXT NOT NULL
|
|
15
12
|
)
|
|
16
13
|
`;
|
|
17
|
-
const
|
|
14
|
+
const createReflectionsTableSql = `
|
|
15
|
+
CREATE TABLE IF NOT EXISTS reflections (
|
|
16
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
+
session_id TEXT NOT NULL,
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
label TEXT,
|
|
20
|
+
agent_name TEXT,
|
|
21
|
+
source_state TEXT,
|
|
22
|
+
payload_json TEXT NOT NULL
|
|
23
|
+
)
|
|
24
|
+
`;
|
|
25
|
+
const createReflectionsIndexSql = `
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_reflections_session_created_at
|
|
27
|
+
ON reflections (session_id, created_at DESC, id DESC)
|
|
28
|
+
`;
|
|
29
|
+
const eventRowSchema = z.array(z.object({
|
|
30
|
+
type: z.string(),
|
|
31
|
+
at: z.string(),
|
|
32
|
+
state: z.string().nullable(),
|
|
33
|
+
payload: z.string(),
|
|
34
|
+
}));
|
|
18
35
|
const rowWithSessionIdSchema = z.array(z.object({ session_id: z.string() }));
|
|
19
36
|
const countFieldSchema = z.union([z.number(), z.bigint(), z.string()]);
|
|
20
37
|
const countRowSchema = z.object({ count: countFieldSchema });
|
|
38
|
+
const tableInfoRowSchema = z.array(z.object({ name: z.string() }));
|
|
39
|
+
const reflectionIdRowSchema = z.object({ id: countFieldSchema });
|
|
40
|
+
const reflectionRowsSchema = z.array(z.object({
|
|
41
|
+
id: z.number(),
|
|
42
|
+
session_id: z.string(),
|
|
43
|
+
created_at: z.string(),
|
|
44
|
+
label: z.string().nullable(),
|
|
45
|
+
agent_name: z.string().nullable(),
|
|
46
|
+
source_state: z.string().nullable(),
|
|
47
|
+
payload_json: z.string(),
|
|
48
|
+
}));
|
|
21
49
|
/** @riviere-role domain-service */
|
|
22
50
|
export function createStore(dbPath) {
|
|
23
51
|
const db = openSqliteDatabase(dbPath);
|
|
24
52
|
enableWalMode(db);
|
|
25
53
|
db.exec(createTableSql);
|
|
54
|
+
db.exec(createReflectionsTableSql);
|
|
55
|
+
db.exec(createReflectionsIndexSql);
|
|
56
|
+
ensureStateColumn(db);
|
|
26
57
|
return {
|
|
27
58
|
db,
|
|
28
59
|
readEvents(sessionId) {
|
|
29
|
-
const rawRows = db.prepare('SELECT payload FROM events WHERE session_id = ? ORDER BY seq').all(sessionId);
|
|
30
|
-
const rows =
|
|
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
|
-
});
|
|
60
|
+
const rawRows = db.prepare('SELECT type, at, state, payload FROM events WHERE session_id = ? ORDER BY seq').all(sessionId);
|
|
61
|
+
const rows = eventRowSchema.parse(rawRows);
|
|
62
|
+
return rows.map((row, index) => buildStoredEvent(row, sessionId, index));
|
|
39
63
|
},
|
|
40
64
|
appendEvents(sessionId, events) {
|
|
41
65
|
if (events.length === 0)
|
|
42
66
|
return;
|
|
43
|
-
const insert = db.prepare('INSERT INTO events (session_id, type, at, payload) VALUES (?, ?, ?, ?)');
|
|
67
|
+
const insert = db.prepare('INSERT INTO events (session_id, type, at, state, payload) VALUES (?, ?, ?, ?, ?)');
|
|
44
68
|
db.exec('BEGIN IMMEDIATE');
|
|
45
69
|
try {
|
|
46
70
|
for (const event of events) {
|
|
47
|
-
insert.run(sessionId, event.type, event.at, JSON.stringify(event));
|
|
71
|
+
insert.run(sessionId, event.envelope.type, event.envelope.at, event.envelope.state ?? null, JSON.stringify(event.payload));
|
|
48
72
|
}
|
|
49
73
|
db.exec('COMMIT');
|
|
50
74
|
}
|
|
@@ -59,6 +83,47 @@ export function createStore(dbPath) {
|
|
|
59
83
|
hasSessionStarted(sessionId) {
|
|
60
84
|
return readCount(db, "SELECT COUNT(1) AS count FROM events WHERE session_id = ? AND type = 'session-started'", sessionId) > 0;
|
|
61
85
|
},
|
|
86
|
+
recordReflection(sessionId, createdAt, input) {
|
|
87
|
+
const parsedInput = recordReflectionInputSchema.parse(input);
|
|
88
|
+
const insert = db.prepare('INSERT INTO reflections (session_id, created_at, label, agent_name, source_state, payload_json) VALUES (?, ?, ?, ?, ?, ?)');
|
|
89
|
+
db.exec('BEGIN IMMEDIATE');
|
|
90
|
+
try {
|
|
91
|
+
insert.run(sessionId, createdAt, parsedInput.label ?? null, parsedInput.agentName ?? null, parsedInput.sourceState ?? null, JSON.stringify(parsedInput.reflection));
|
|
92
|
+
const rawId = db.prepare('SELECT last_insert_rowid() AS id').get();
|
|
93
|
+
const parsedId = reflectionIdRowSchema.parse(rawId);
|
|
94
|
+
const id = Number(parsedId.id);
|
|
95
|
+
db.exec('COMMIT');
|
|
96
|
+
return storedReflectionSchema.parse({
|
|
97
|
+
id,
|
|
98
|
+
sessionId,
|
|
99
|
+
createdAt,
|
|
100
|
+
...(parsedInput.label === undefined ? {} : { label: parsedInput.label }),
|
|
101
|
+
...(parsedInput.agentName === undefined ? {} : { agentName: parsedInput.agentName }),
|
|
102
|
+
...(parsedInput.sourceState === undefined ? {} : { sourceState: parsedInput.sourceState }),
|
|
103
|
+
reflection: parsedInput.reflection,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
db.exec('ROLLBACK');
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
listReflections(sessionId) {
|
|
112
|
+
const rawRows = db.prepare('SELECT id, session_id, created_at, label, agent_name, source_state, payload_json FROM reflections WHERE session_id = ? ORDER BY created_at DESC, id DESC').all(sessionId);
|
|
113
|
+
const rows = reflectionRowsSchema.parse(rawRows);
|
|
114
|
+
return rows.map((row) => {
|
|
115
|
+
const reflectionPayload = JSON.parse(row.payload_json);
|
|
116
|
+
return storedReflectionSchema.parse({
|
|
117
|
+
id: row.id,
|
|
118
|
+
sessionId: row.session_id,
|
|
119
|
+
createdAt: row.created_at,
|
|
120
|
+
...(row.label === null ? {} : { label: row.label }),
|
|
121
|
+
...(row.agent_name === null ? {} : { agentName: row.agent_name }),
|
|
122
|
+
...(row.source_state === null ? {} : { sourceState: row.source_state }),
|
|
123
|
+
reflection: reflectionPayload,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
},
|
|
62
127
|
listSessions() {
|
|
63
128
|
const rawRows = db.prepare('SELECT session_id FROM events GROUP BY session_id ORDER BY MIN(seq)').all();
|
|
64
129
|
return rowWithSessionIdSchema.parse(rawRows).map((row) => row.session_id);
|
|
@@ -101,3 +166,33 @@ function tryParsePayload(payload, index) {
|
|
|
101
166
|
throw new WorkflowStateError(`Cannot parse event payload at index ${index}: ${String(cause)}`);
|
|
102
167
|
}
|
|
103
168
|
}
|
|
169
|
+
function ensureStateColumn(db) {
|
|
170
|
+
const rawColumns = db.prepare('PRAGMA table_info(events)').all();
|
|
171
|
+
const columns = tableInfoRowSchema.parse(rawColumns);
|
|
172
|
+
if (columns.some((column) => column.name === 'state'))
|
|
173
|
+
return;
|
|
174
|
+
db.exec('ALTER TABLE events ADD COLUMN state TEXT');
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Dual-read adapter. Modern rows carry only domain fields in `payload` JSON
|
|
178
|
+
* (envelope in columns). Legacy rows have `type`/`at` duplicated inside the
|
|
179
|
+
* payload JSON and no `state` column value. Either way, normalize to
|
|
180
|
+
* `StoredEvent` with undefined state for legacy rows.
|
|
181
|
+
*/
|
|
182
|
+
function buildStoredEvent(row, sessionId, index) {
|
|
183
|
+
const parsedPayload = tryParsePayload(row.payload, index);
|
|
184
|
+
if (!isRecord(parsedPayload)) {
|
|
185
|
+
throw new WorkflowStateError(`Invalid event payload at index ${index} for session ${sessionId}: expected object`);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
envelope: {
|
|
189
|
+
type: row.type,
|
|
190
|
+
at: row.at,
|
|
191
|
+
state: row.state ?? undefined,
|
|
192
|
+
},
|
|
193
|
+
payload: stripEnvelopeKeys(parsedPayload),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function isRecord(value) {
|
|
197
|
+
return typeof value === 'object' && value !== null;
|
|
198
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nt-ai-lab/deterministic-agent-workflow-event-store",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"dist"
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
"zod": "^3.25.76",
|
|
15
|
+
"@nt-ai-lab/deterministic-agent-workflow-engine": "0.3.0"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|