@tenetkit/pg 0.28.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.
Files changed (48) hide show
  1. package/README.md +3 -0
  2. package/dist/postgres/claims.d.ts +26 -0
  3. package/dist/postgres/claims.js +95 -0
  4. package/dist/postgres/event-stream.d.ts +20 -0
  5. package/dist/postgres/event-stream.js +16 -0
  6. package/dist/postgres/index.d.ts +3 -0
  7. package/dist/postgres/index.js +3 -0
  8. package/dist/postgres/locks.d.ts +18 -0
  9. package/dist/postgres/locks.js +51 -0
  10. package/dist/postgres/pg-helpers.d.ts +91 -0
  11. package/dist/postgres/pg-helpers.js +359 -0
  12. package/dist/postgres/run-schema.d.ts +23 -0
  13. package/dist/postgres/run-schema.js +92 -0
  14. package/dist/postgres/runtime-layer.d.ts +15 -0
  15. package/dist/postgres/runtime-layer.js +28 -0
  16. package/dist/postgres/schema.d.ts +6 -0
  17. package/dist/postgres/schema.js +274 -0
  18. package/dist/postgres/session-cancellation.d.ts +11 -0
  19. package/dist/postgres/session-cancellation.js +15 -0
  20. package/dist/postgres/session-storage.d.ts +32 -0
  21. package/dist/postgres/session-storage.js +63 -0
  22. package/dist/postgres/session-store.d.ts +24 -0
  23. package/dist/postgres/session-store.js +388 -0
  24. package/dist/postgres/store-admit.d.ts +21 -0
  25. package/dist/postgres/store-admit.js +87 -0
  26. package/dist/postgres/store-cancel.d.ts +14 -0
  27. package/dist/postgres/store-cancel.js +73 -0
  28. package/dist/postgres/store-claims.d.ts +17 -0
  29. package/dist/postgres/store-claims.js +72 -0
  30. package/dist/postgres/store-fan-out.d.ts +21 -0
  31. package/dist/postgres/store-fan-out.js +17 -0
  32. package/dist/postgres/store-inspection.d.ts +12 -0
  33. package/dist/postgres/store-inspection.js +44 -0
  34. package/dist/postgres/store-messaging.d.ts +14 -0
  35. package/dist/postgres/store-messaging.js +12 -0
  36. package/dist/postgres/store-model-response.d.ts +18 -0
  37. package/dist/postgres/store-model-response.js +156 -0
  38. package/dist/postgres/store-ops.d.ts +21 -0
  39. package/dist/postgres/store-ops.js +328 -0
  40. package/dist/postgres/store-program.d.ts +18 -0
  41. package/dist/postgres/store-program.js +36 -0
  42. package/dist/postgres/store-suspend.d.ts +13 -0
  43. package/dist/postgres/store-suspend.js +87 -0
  44. package/dist/postgres/store.d.ts +7 -0
  45. package/dist/postgres/store.js +373 -0
  46. package/dist/postgres/transaction-events.d.ts +15 -0
  47. package/dist/postgres/transaction-events.js +23 -0
  48. package/package.json +54 -0
@@ -0,0 +1,359 @@
1
+ import { Effect, Function } from "effect";
2
+ import { SqlClient } from "effect/unstable/sql";
3
+ import { PgClient } from "@effect/sql-pg";
4
+ import { eventIdFor } from "tenetkit/runtime/driver/run-event";
5
+ import { isTerminal } from "tenetkit/runtime/driver/run";
6
+ import { StringArray, decodeMessage, decodeQueue, encodeExecutableManifest, encodeExecutableRef, encodeEvent, encodeJson, encodeMessage, encodeQueue, } from "tenetkit/runtime/driver/sql/codecs";
7
+ import { reconcileFanOutWith } from "tenetkit/runtime/driver/sql/store-fan-out";
8
+ import { decodePersistedEvents, decodeRunEffect, nowIso } from "tenetkit/runtime/driver/sql/store-helpers";
9
+ import { NOTIFY_CHANNEL } from "./schema.js";
10
+ import { PendingRunOutcome } from "tenetkit/runtime/driver/run-store";
11
+ import { RunNotFound, RunTerminal, RuntimeUnavailable } from "tenetkit/runtime/driver/errors";
12
+ import { StaleClaim } from "tenetkit/runtime/driver/sql/errors";
13
+ import { admitChildSettlementFromEventId } from "tenetkit/runtime/driver/sql/settlement-notifications";
14
+ import { discardPendingSteering } from "tenetkit/runtime/driver/sql/store-steering-disposition";
15
+ import { hasUnsettledChild, loadTerminalEvent, reconcileChildWaitWith, } from "tenetkit/runtime/driver/sql/store-child-settlement";
16
+ import { appendTerminalToolResultsForEvent } from "tenetkit/runtime/driver/sql/session-terminalization";
17
+ export const loadRun = (runId) => Effect.gen(function* () {
18
+ const sql = yield* SqlClient.SqlClient;
19
+ const rows = yield* sql `SELECT * FROM baton_runs WHERE run_id = ${runId}`;
20
+ const row = rows[0];
21
+ return row === undefined ? undefined : yield* decodeRunEffect(row);
22
+ });
23
+ export const requireRun = (runId) => loadRun(runId).pipe(Effect.flatMap((loaded) => loaded === undefined ? Effect.fail(RunNotFound.make({ runId })) : Effect.succeed(loaded)));
24
+ export const emitAgentEvent = Function.dual(2, (hub, input) => Effect.gen(function* () {
25
+ const sql = yield* SqlClient.SqlClient;
26
+ yield* sql `SELECT run_id FROM baton_runs WHERE run_id = ${input.runId} FOR UPDATE`;
27
+ const loaded = yield* loadRun(input.runId);
28
+ if (loaded === undefined)
29
+ return yield* RunNotFound.make({ runId: input.runId });
30
+ if (loaded.ownerWorkerId !== input.ownerId || loaded.attemptFence !== input.attemptFence) {
31
+ return yield* StaleClaim.make({
32
+ runId: input.runId,
33
+ workerId: input.ownerId,
34
+ attemptFence: input.attemptFence,
35
+ });
36
+ }
37
+ if (isTerminal(loaded.status))
38
+ return yield* RunTerminal.make({ runId: loaded.runId, status: loaded.status });
39
+ yield* appendEvent(hub, loaded, input.event);
40
+ if (input.event._tag === "TurnCompleted") {
41
+ yield* sql `UPDATE baton_runs SET continuation_json = NULL WHERE run_id = ${loaded.runId}`;
42
+ }
43
+ }));
44
+ export const loadEventsAfter = Function.dual(2, (runId, cursor) => Effect.gen(function* () {
45
+ const sql = yield* SqlClient.SqlClient;
46
+ const run = yield* loadRun(runId);
47
+ if (run === undefined)
48
+ return [];
49
+ const rows = yield* sql `
50
+ SELECT * FROM baton_run_events
51
+ WHERE run_id = ${runId} AND sequence > ${cursor}
52
+ ORDER BY sequence ASC
53
+ `;
54
+ return yield* decodePersistedEvents(rows, run.executableManifest);
55
+ }));
56
+ export const allocateSequence = (runId) => Effect.gen(function* () {
57
+ const sql = yield* SqlClient.SqlClient;
58
+ const rows = yield* sql `
59
+ UPDATE baton_runs
60
+ SET last_sequence = last_sequence + 1, updated_at = NOW()
61
+ WHERE run_id = ${runId}
62
+ RETURNING last_sequence
63
+ `;
64
+ return Number(rows[0].last_sequence);
65
+ });
66
+ export const appendEvent = Function.dual((args) => args.length >= 4 || (args.length === 3 && !("runId" in args[0])), (_hub, run, partial, nextStatus) => Effect.gen(function* () {
67
+ const sql = yield* SqlClient.SqlClient;
68
+ const pg = yield* PgClient.PgClient;
69
+ const discarded = yield* discardPendingSteering({ runId: run.runId, terminalTag: partial._tag });
70
+ if (discarded !== undefined) {
71
+ yield* appendEvent(_hub, run, discarded);
72
+ return yield* appendEvent(_hub, (yield* loadRun(run.runId)), partial, nextStatus);
73
+ }
74
+ yield* appendTerminalToolResultsForEvent({ run, event: partial });
75
+ const sequence = yield* allocateSequence(run.runId);
76
+ const occurredAt = yield* nowIso;
77
+ const event = {
78
+ specVersion: "1",
79
+ eventId: eventIdFor(run.runId, sequence),
80
+ runId: run.runId,
81
+ sequence,
82
+ executableRef: run.executableRef,
83
+ rootRunId: run.rootRunId,
84
+ depth: run.depth,
85
+ occurredAt,
86
+ ...(run.parentRunId === undefined ? {} : { parentRunId: run.parentRunId }),
87
+ ...(run.message.causationId === undefined ? {} : { causationId: run.message.causationId }),
88
+ ...(run.message.correlationId === undefined ? {} : { correlationId: run.message.correlationId }),
89
+ ...(run.attempt > 0 ? { attemptId: `${run.runId}:attempt:${run.attempt}` } : {}),
90
+ ...partial,
91
+ };
92
+ yield* sql `
93
+ INSERT INTO baton_run_events (run_id, sequence, event_id, event_json)
94
+ VALUES (${run.runId}, ${sequence}, ${event.eventId}, ${encodeEvent(event)})
95
+ `;
96
+ const treeRoot = (yield* sql `
97
+ UPDATE baton_tree_roots SET last_position = last_position + 1
98
+ WHERE root_run_id = ${run.rootRunId} RETURNING last_position
99
+ `)[0];
100
+ yield* sql `
101
+ INSERT INTO baton_tree_event_index (root_run_id, position, run_id, run_sequence, event_id)
102
+ VALUES (${run.rootRunId}, ${Number(treeRoot.last_position)}, ${run.runId}, ${sequence}, ${event.eventId})
103
+ `;
104
+ const status = nextStatus ?? run.status;
105
+ const activeWaitId = event._tag === "RunWaiting"
106
+ ? event.wait.waitId
107
+ : event._tag === "RunResumed"
108
+ ? null
109
+ : (run.activeWaitId ?? null);
110
+ const terminalEventId = event._tag === "RunCompleted" || event._tag === "RunFailed" || event._tag === "RunCancelled"
111
+ ? event.eventId
112
+ : (run.terminalEventId ?? null);
113
+ const cancellationRequested = event._tag === "RunCancellationRequested" || run.cancellationRequested;
114
+ const cancelReason = event._tag === "RunCancellationRequested" && "reason" in event && typeof event.reason === "string"
115
+ ? event.reason
116
+ : (run.cancelReason ?? null);
117
+ const attempt = event._tag === "RunAttemptStarted" ? event.attempt : run.attempt;
118
+ const terminalPartial = event._tag === "RunCompleted" || event._tag === "RunFailed" || event._tag === "RunCancelled";
119
+ if (terminalPartial) {
120
+ yield* sql `
121
+ UPDATE baton_runs SET
122
+ status = ${status},
123
+ active_wait_id = ${activeWaitId},
124
+ terminal_event_id = ${terminalEventId},
125
+ cancellation_requested = ${cancellationRequested},
126
+ cancel_reason = ${cancelReason},
127
+ attempt = ${attempt},
128
+ owner_worker_id = NULL,
129
+ lease_expires_at = NULL,
130
+ continuation_json = NULL,
131
+ pending_outcome_json = NULL,
132
+ suspension_json = NULL,
133
+ updated_at = NOW()
134
+ WHERE run_id = ${run.runId}
135
+ AND status NOT IN ('succeeded', 'failed', 'cancelled')
136
+ `;
137
+ }
138
+ else {
139
+ yield* sql `
140
+ UPDATE baton_runs SET
141
+ status = ${status},
142
+ active_wait_id = ${activeWaitId},
143
+ terminal_event_id = ${terminalEventId},
144
+ cancellation_requested = ${cancellationRequested},
145
+ cancel_reason = ${cancelReason},
146
+ attempt = ${attempt},
147
+ updated_at = NOW()
148
+ WHERE run_id = ${run.runId}
149
+ `;
150
+ }
151
+ yield* pg.notify(NOTIFY_CHANNEL, run.runId);
152
+ return event;
153
+ }));
154
+ export const promoteHead = Function.dual(3, (hub, address, sessionId) => Effect.gen(function* () {
155
+ const sql = yield* SqlClient.SqlClient;
156
+ const lanes = yield* sql `
157
+ SELECT head_run_id, queue_json FROM baton_lanes
158
+ WHERE address = ${address} AND session_id = ${sessionId}
159
+ FOR UPDATE
160
+ `;
161
+ const lane = lanes[0];
162
+ if (lane === undefined)
163
+ return;
164
+ const queue = decodeQueue(lane.queue_json);
165
+ const headId = lane.head_run_id ?? queue[0];
166
+ if (headId === undefined)
167
+ return;
168
+ if (lane.head_run_id !== headId) {
169
+ yield* sql `
170
+ UPDATE baton_lanes SET head_run_id = ${headId}
171
+ WHERE address = ${address} AND session_id = ${sessionId}
172
+ `;
173
+ }
174
+ const head = yield* loadRun(headId);
175
+ if (head === undefined || head.status !== "queued" || head.cancellationRequested)
176
+ return;
177
+ }));
178
+ export const removeFromLane = Function.dual(3, (address, sessionId, runId) => Effect.gen(function* () {
179
+ const sql = yield* SqlClient.SqlClient;
180
+ const lanes = yield* sql `
181
+ SELECT queue_json FROM baton_lanes
182
+ WHERE address = ${address} AND session_id = ${sessionId}
183
+ FOR UPDATE
184
+ `;
185
+ const lane = lanes[0];
186
+ if (lane === undefined)
187
+ return;
188
+ const queue = decodeQueue(lane.queue_json).filter((id) => id !== runId);
189
+ if (queue.length === 0) {
190
+ yield* sql `DELETE FROM baton_lanes WHERE address = ${address} AND session_id = ${sessionId}`;
191
+ }
192
+ else {
193
+ yield* sql `
194
+ UPDATE baton_lanes
195
+ SET queue_json = ${encodeQueue(queue)}, head_run_id = ${queue[0]}
196
+ WHERE address = ${address} AND session_id = ${sessionId}
197
+ `;
198
+ }
199
+ }));
200
+ export const afterTerminal = Function.dual(2, (hub, run) => Effect.gen(function* () {
201
+ yield* removeFromLane(run.address, run.sessionId, run.runId);
202
+ yield* promoteHead(hub, run.address, run.sessionId);
203
+ }));
204
+ export const completeRun = Function.dual(3, (hub, run, result) => Effect.gen(function* () {
205
+ const sql = yield* SqlClient.SqlClient;
206
+ if (isTerminal(run.status))
207
+ return yield* RunTerminal.make({ runId: run.runId, status: run.status });
208
+ if (run.cancellationRequested) {
209
+ const runningFanOut = yield* sql `
210
+ SELECT fan_out_id FROM baton_fan_outs WHERE parent_run_id = ${run.runId} AND status = 'running' LIMIT 1
211
+ `;
212
+ if (runningFanOut.length > 0 || (yield* hasUnsettledChild(run.runId))) {
213
+ yield* sql `
214
+ UPDATE baton_runs SET owner_worker_id = NULL, lease_expires_at = NULL WHERE run_id = ${run.runId}
215
+ `;
216
+ return;
217
+ }
218
+ const event = yield* appendEvent(hub, run, { _tag: "RunCancelled", ...(run.cancelReason === undefined ? {} : { reason: run.cancelReason }) }, "cancelled");
219
+ const settled = (yield* loadRun(run.runId));
220
+ yield* settleParent(hub, settled, event.eventId);
221
+ yield* afterTerminal(hub, settled);
222
+ return;
223
+ }
224
+ const runningFanOut = yield* sql `
225
+ SELECT fan_out_id FROM baton_fan_outs WHERE parent_run_id = ${run.runId} AND status = 'running' LIMIT 1
226
+ `;
227
+ if (runningFanOut.length > 0) {
228
+ yield* sql `
229
+ UPDATE baton_runs SET status = 'waiting', owner_worker_id = NULL, lease_expires_at = NULL,
230
+ suspension_json = NULL,
231
+ pending_outcome_json = ${encodeJson(PendingRunOutcome, { _tag: "Completed", result })}
232
+ WHERE run_id = ${run.runId}
233
+ `;
234
+ return;
235
+ }
236
+ const event = yield* appendEvent(hub, run, { _tag: "RunCompleted", result }, "succeeded");
237
+ const settled = (yield* loadRun(run.runId));
238
+ yield* settleParent(hub, settled, event.eventId);
239
+ yield* afterTerminal(hub, settled);
240
+ }));
241
+ export const settleParent = Function.dual(3, (hub, child, terminalEventId) => Effect.gen(function* () {
242
+ if (child.parentRunId === undefined)
243
+ return;
244
+ const sql = yield* SqlClient.SqlClient;
245
+ yield* sql `SELECT run_id FROM baton_runs WHERE run_id = ${child.parentRunId} FOR UPDATE`;
246
+ const parent = yield* loadRun(child.parentRunId);
247
+ if (parent === undefined)
248
+ return;
249
+ const existing = yield* sql `
250
+ SELECT child_run_id FROM baton_run_links
251
+ WHERE parent_run_id = ${parent.runId} AND child_run_id = ${child.runId} AND terminal_event_id IS NOT NULL
252
+ `;
253
+ if (existing.length > 0)
254
+ return;
255
+ yield* sql `
256
+ UPDATE baton_run_links
257
+ SET readiness = 'settled', terminal_event_id = ${terminalEventId}, settled_at = NOW()
258
+ WHERE parent_run_id = ${parent.runId} AND child_run_id = ${child.runId}
259
+ `;
260
+ yield* admitChildSettlementFromEventId({ parent, child, terminalEventId });
261
+ if (!isTerminal(parent.status)) {
262
+ yield* appendEvent(hub, parent, {
263
+ _tag: "ChildReadinessChanged",
264
+ childRunId: child.runId,
265
+ readiness: "settled",
266
+ });
267
+ yield* appendEvent(hub, (yield* loadRun(parent.runId)), {
268
+ _tag: "ChildSettled",
269
+ childRunId: child.runId,
270
+ terminalEventId,
271
+ });
272
+ }
273
+ yield* reconcileFanOutWith(hub, child.runId, terminalEventId, appendEvent, settleParent, afterTerminal);
274
+ let currentParent = yield* loadRun(parent.runId);
275
+ const terminalEvent = yield* loadTerminalEvent(terminalEventId);
276
+ if (currentParent !== undefined && terminalEvent !== undefined) {
277
+ yield* reconcileChildWaitWith({ hub, parent: currentParent, child, event: terminalEvent, append: appendEvent });
278
+ currentParent = yield* loadRun(parent.runId);
279
+ }
280
+ if (currentParent?.status === "queued") {
281
+ const unsettled = yield* sql `
282
+ SELECT l.child_run_id FROM baton_run_links l
283
+ JOIN baton_runs r ON r.run_id = l.child_run_id
284
+ WHERE l.parent_run_id = ${parent.runId}
285
+ AND r.status NOT IN ('succeeded', 'failed', 'cancelled')
286
+ LIMIT 1
287
+ `;
288
+ if (unsettled.length === 0) {
289
+ const attempt = currentParent.attempt + 1;
290
+ yield* sql `UPDATE baton_runs SET attempt_fence = ${attempt} WHERE run_id = ${parent.runId}`;
291
+ yield* appendEvent(hub, { ...currentParent, attempt }, { _tag: "RunAttemptStarted", attempt }, "running");
292
+ }
293
+ return;
294
+ }
295
+ if (currentParent?.status !== "cancelling" || currentParent.ownerWorkerId !== undefined)
296
+ return;
297
+ const running = yield* sql `
298
+ SELECT fan_out_id FROM baton_fan_outs WHERE parent_run_id = ${parent.runId} AND status = 'running' LIMIT 1
299
+ `;
300
+ if (running.length > 0)
301
+ return;
302
+ if (yield* hasUnsettledChild(parent.runId))
303
+ return;
304
+ const cancelled = yield* appendEvent(hub, currentParent, {
305
+ _tag: "RunCancelled",
306
+ ...(currentParent.cancelReason === undefined ? {} : { reason: currentParent.cancelReason }),
307
+ }, "cancelled");
308
+ const settledParent = (yield* loadRun(parent.runId));
309
+ yield* settleParent(hub, settledParent, cancelled.eventId);
310
+ yield* afterTerminal(hub, settledParent);
311
+ }));
312
+ export const insertRun = (input) => Effect.gen(function* () {
313
+ const sql = yield* SqlClient.SqlClient;
314
+ yield* sql `
315
+ INSERT INTO baton_runs (
316
+ run_id, status, address, session_id, message_id, message_json, message_digest, idempotency_key,
317
+ executable_ref_json, executable_manifest_json, root_run_id, depth, max_depth, max_subagents, parent_run_id, invocation_id, active_wait_id, attempt, attempt_fence,
318
+ last_sequence, cancellation_requested, cancel_reason, terminal_event_id, accepted_sequence,
319
+ responded_wait_ids_json, owner_worker_id, lease_expires_at, created_at, updated_at
320
+ ) VALUES (
321
+ ${input.runId}, ${input.status}, ${input.message.to}, ${input.message.sessionId}, ${input.message.id},
322
+ ${encodeMessage(input.message)}, ${input.digest}, ${input.message.idempotencyKey},
323
+ ${encodeExecutableRef(input.executableRef)}, ${encodeExecutableManifest(input.executableManifest)},
324
+ ${input.rootRunId}, ${input.depth}, ${input.treePolicy.maxDepth}, ${input.treePolicy.maxSubagents}, ${input.parentRunId ?? null}, ${input.invocationId ?? null},
325
+ NULL, ${input.attempt ?? 0}, ${input.attempt ?? 0}, -1, FALSE, NULL, NULL, ${input.acceptedSequence},
326
+ ${encodeJson(StringArray, [])}, NULL, NULL, NOW(), NOW()
327
+ )
328
+ `;
329
+ if (input.runId === input.rootRunId) {
330
+ yield* sql `INSERT INTO baton_tree_roots (root_run_id) VALUES (${input.runId})`;
331
+ }
332
+ });
333
+ export const enqueueLane = Function.dual(3, (address, sessionId, runId) => Effect.gen(function* () {
334
+ const sql = yield* SqlClient.SqlClient;
335
+ const lanes = yield* sql `
336
+ SELECT accepted_sequence, queue_json, head_run_id FROM baton_lanes
337
+ WHERE address = ${address} AND session_id = ${sessionId}
338
+ FOR UPDATE
339
+ `;
340
+ const lane = lanes[0];
341
+ if (lane === undefined) {
342
+ yield* sql `
343
+ INSERT INTO baton_lanes (address, session_id, accepted_sequence, queue_json, head_run_id)
344
+ VALUES (${address}, ${sessionId}, 0, ${encodeQueue([runId])}, ${runId})
345
+ `;
346
+ return { acceptedSequence: 0, isHead: true };
347
+ }
348
+ const acceptedSequence = Number(lane.accepted_sequence) + 1;
349
+ const queue = [...decodeQueue(lane.queue_json), runId];
350
+ const head = lane.head_run_id ?? queue[0];
351
+ yield* sql `
352
+ UPDATE baton_lanes
353
+ SET accepted_sequence = ${acceptedSequence}, queue_json = ${encodeQueue(queue)}, head_run_id = ${head}
354
+ WHERE address = ${address} AND session_id = ${sessionId}
355
+ `;
356
+ return { acceptedSequence, isHead: head === runId };
357
+ }));
358
+ export { toOperationRecord } from "tenetkit/runtime/driver/sql/operations";
359
+ export { decodeMessage, encodeQueue, decodeQueue };
@@ -0,0 +1,23 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { SqlClient } from "effect/unstable/sql";
3
+ import type { SqlError } from "effect/unstable/sql/SqlError";
4
+ import { PgClient } from "@effect/sql-pg";
5
+ import { SchemaChecksumMismatch, SchemaDirty, SchemaMigrationFailed, SchemaUpgradeRequired, SchemaVersionUnsupported } from "tenetkit/runtime/driver/sql/errors";
6
+ export interface SchemaPlan {
7
+ readonly current: number;
8
+ readonly required: number;
9
+ readonly checksum: string;
10
+ readonly statements: ReadonlyArray<string>;
11
+ readonly upgradeRequired: boolean;
12
+ }
13
+ export declare const plan: (source: string) => Effect.Effect<SchemaPlan, SchemaMigrationFailed, SqlClient.SqlClient>;
14
+ export declare const check: (source: string) => Effect.Effect<undefined, SchemaChecksumMismatch | SchemaDirty | SchemaMigrationFailed | SchemaUpgradeRequired | SchemaVersionUnsupported, SqlClient.SqlClient>;
15
+ export declare const apply: (source: string) => Effect.Effect<undefined, SchemaChecksumMismatch | SchemaDirty | SchemaMigrationFailed | SchemaUpgradeRequired | SchemaVersionUnsupported | SqlError, SqlClient.SqlClient>;
16
+ export declare const markDirty: (source: string) => Effect.Effect<void, SchemaMigrationFailed, SqlClient.SqlClient>;
17
+ export declare const RunSchema: {
18
+ readonly plan: typeof plan;
19
+ readonly check: typeof check;
20
+ readonly apply: typeof apply;
21
+ readonly markDirty: typeof markDirty;
22
+ };
23
+ export declare const layerClient: (url: string) => Layer.Layer<SqlClient.SqlClient | PgClient.PgClient, SqlError>;
@@ -0,0 +1,92 @@
1
+ import { DateTime, Effect, Layer, Redacted } from "effect";
2
+ import { Migrator, SqlClient } from "effect/unstable/sql";
3
+ import { PgClient } from "@effect/sql-pg";
4
+ import { SchemaChecksumMismatch, SchemaDirty, SchemaMigrationFailed, SchemaUpgradeRequired, SchemaVersionUnsupported, } from "tenetkit/runtime/driver/sql/errors";
5
+ import { mapSqlError } from "tenetkit/runtime/driver/sql/sql-effect";
6
+ import { MIGRATIONS_TABLE, SCHEMA_META_TABLE, SCHEMA_STATEMENTS, SCHEMA_VERSION, schemaChecksum } from "./schema.js";
7
+ const readMeta = (source) => mapSqlError(Effect.gen(function* () {
8
+ const sql = yield* SqlClient.SqlClient;
9
+ const exists = yield* sql `
10
+ SELECT EXISTS (
11
+ SELECT 1 FROM information_schema.tables
12
+ WHERE table_schema = current_schema() AND table_name = ${SCHEMA_META_TABLE}
13
+ ) AS exists
14
+ `;
15
+ if (exists[0]?.exists !== true)
16
+ return { version: 0, checksum: "", dirty: false, present: false };
17
+ const rows = yield* sql `
18
+ SELECT version, checksum, dirty FROM ${sql(SCHEMA_META_TABLE)} WHERE id = 1
19
+ `;
20
+ const row = rows[0];
21
+ if (row === undefined)
22
+ return { version: 0, checksum: "", dirty: false, present: false };
23
+ return { version: Number(row.version), checksum: row.checksum, dirty: row.dirty, present: true };
24
+ })).pipe(Effect.mapError((error) => SchemaMigrationFailed.make({
25
+ source,
26
+ message: "message" in error ? String(error.message) : "schema meta read failed",
27
+ })));
28
+ const migrationEffect = Effect.gen(function* () {
29
+ const sql = yield* SqlClient.SqlClient;
30
+ for (const statement of SCHEMA_STATEMENTS)
31
+ yield* sql.unsafe(statement);
32
+ const now = yield* DateTime.nowAsDate;
33
+ yield* sql `
34
+ INSERT INTO ${sql(SCHEMA_META_TABLE)} (id, version, checksum, dirty, applied_at)
35
+ VALUES (1, ${SCHEMA_VERSION}, ${schemaChecksum()}, FALSE, ${now})
36
+ `;
37
+ });
38
+ export const plan = (source) => Effect.map(readMeta(source), (meta) => ({
39
+ current: meta.version,
40
+ required: SCHEMA_VERSION,
41
+ checksum: schemaChecksum(),
42
+ statements: meta.present ? [] : SCHEMA_STATEMENTS,
43
+ upgradeRequired: !meta.present,
44
+ }));
45
+ export const check = (source) => Effect.gen(function* () {
46
+ const meta = yield* readMeta(source);
47
+ if (!meta.present)
48
+ return yield* SchemaUpgradeRequired.make({ source, current: 0, required: SCHEMA_VERSION });
49
+ if (meta.dirty)
50
+ return yield* SchemaDirty.make({ source, version: meta.version });
51
+ if (meta.version > SCHEMA_VERSION) {
52
+ return yield* SchemaVersionUnsupported.make({ source, version: meta.version, supported: SCHEMA_VERSION });
53
+ }
54
+ const expected = schemaChecksum();
55
+ if (meta.version !== SCHEMA_VERSION || meta.checksum !== expected) {
56
+ return yield* SchemaChecksumMismatch.make({ source, expected, actual: meta.checksum });
57
+ }
58
+ });
59
+ export const apply = (source) => Effect.gen(function* () {
60
+ const meta = yield* readMeta(source);
61
+ if (meta.present)
62
+ return yield* check(source);
63
+ const sql = yield* SqlClient.SqlClient;
64
+ const existing = yield* sql `
65
+ SELECT COUNT(*) AS present FROM information_schema.tables
66
+ WHERE table_schema = current_schema() AND table_name LIKE 'baton\_%' ESCAPE '\'
67
+ `;
68
+ if (Number(existing[0]?.present ?? 0) > 0) {
69
+ return yield* SchemaMigrationFailed.make({
70
+ source,
71
+ message: "cannot create the baseline over an existing TenetKit schema",
72
+ });
73
+ }
74
+ const runMigrations = Migrator.make({});
75
+ yield* runMigrations({
76
+ loader: Migrator.fromRecord({ "0001_baton_runtime": migrationEffect }),
77
+ table: MIGRATIONS_TABLE,
78
+ }).pipe(Effect.mapError((error) => SchemaMigrationFailed.make({
79
+ source,
80
+ message: "message" in error ? String(error.message) : "migration failed",
81
+ })));
82
+ yield* check(source).pipe(Effect.catchTag("tenetkit/runtime/SchemaUpgradeRequired", (error) => SchemaMigrationFailed.make({ source, message: `schema absent after apply: ${error.current}` })));
83
+ });
84
+ export const markDirty = (source) => mapSqlError(Effect.gen(function* () {
85
+ const sql = yield* SqlClient.SqlClient;
86
+ yield* sql `UPDATE ${sql(SCHEMA_META_TABLE)} SET dirty = TRUE WHERE id = 1`;
87
+ })).pipe(Effect.mapError((error) => SchemaMigrationFailed.make({
88
+ source,
89
+ message: "message" in error ? String(error.message) : "failed to mark schema dirty",
90
+ })));
91
+ export const RunSchema = { plan, check, apply, markDirty };
92
+ export const layerClient = (url) => PgClient.layer({ url: Redacted.make(url) });
@@ -0,0 +1,15 @@
1
+ import { Layer } from "effect";
2
+ import type { SqlError } from "effect/unstable/sql/SqlError";
3
+ import { Runtime } from "tenetkit/runtime/driver/runtime";
4
+ import { RunStore } from "tenetkit/runtime/driver/run-store";
5
+ import { RunClaims } from "tenetkit/runtime/driver/sql/run-claims";
6
+ import { ExecutionHost } from "tenetkit/runtime/driver/execution-host";
7
+ import type { LayerOptions } from "tenetkit/runtime/driver/runtime";
8
+ import { SchemaMigrationFailed, type SchemaChecksumMismatch, type SchemaDirty, type SchemaUpgradeRequired, type SchemaVersionUnsupported } from "tenetkit/runtime/driver/sql/errors";
9
+ export interface PostgresStoreOptions extends LayerOptions {
10
+ readonly url: string;
11
+ readonly source?: string;
12
+ readonly maxConnections?: number;
13
+ }
14
+ export type PostgresStoreError = SchemaDirty | SchemaChecksumMismatch | SchemaVersionUnsupported | SchemaUpgradeRequired | SchemaMigrationFailed;
15
+ export declare const layerPostgres: (options: PostgresStoreOptions) => Layer.Layer<Runtime | RunStore | RunClaims | ExecutionHost, PostgresStoreError | SqlError>;
@@ -0,0 +1,28 @@
1
+ import { Context, Effect, Layer, Redacted } from "effect";
2
+ import { PgClient } from "@effect/sql-pg";
3
+ import { makeRuntime } from "tenetkit/runtime/driver/memory/runtime-layer";
4
+ import { Runtime } from "tenetkit/runtime/driver/runtime";
5
+ import { RunStore } from "tenetkit/runtime/driver/run-store";
6
+ import { RunClaims } from "tenetkit/runtime/driver/sql/run-claims";
7
+ import { makePostgresServices } from "./store.js";
8
+ import { ExecutionHost, make as makeExecutionHost } from "tenetkit/runtime/driver/execution-host";
9
+ import { layer as activeExecutionsLayer } from "tenetkit/runtime/driver/active-executions";
10
+ import { layer as modelPreviewLayer } from "tenetkit/runtime/driver/model-preview";
11
+ import { SchemaMigrationFailed, } from "tenetkit/runtime/driver/sql/errors";
12
+ export const layerPostgres = (options) => {
13
+ const maxConnections = options.maxConnections ?? 10;
14
+ const client = Layer.unwrap(Effect.gen(function* () {
15
+ if (!Number.isSafeInteger(maxConnections) || maxConnections < 1) {
16
+ return yield* SchemaMigrationFailed.make({
17
+ source: options.source ?? "postgres",
18
+ message: "PostgreSQL maxConnections must be a positive integer",
19
+ });
20
+ }
21
+ return PgClient.layer({ url: Redacted.make(options.url), maxConnections });
22
+ }));
23
+ const services = Layer.effectContext(makePostgresServices(options).pipe(Effect.map(({ store, claims }) => Context.make(RunStore, store).pipe(Context.add(RunClaims, claims))))).pipe(Layer.provide(client));
24
+ const dependencies = Layer.mergeAll(services, activeExecutionsLayer, modelPreviewLayer);
25
+ const runtime = Layer.effect(Runtime, makeRuntime(options)).pipe(Layer.provide(dependencies));
26
+ const host = Layer.effect(ExecutionHost, makeExecutionHost({ workerId: "postgres", resolver: options.resolver })).pipe(Layer.provide(dependencies));
27
+ return Layer.mergeAll(runtime, host, services);
28
+ };
@@ -0,0 +1,6 @@
1
+ export declare const SCHEMA_VERSION = 7;
2
+ export declare const SCHEMA_META_TABLE = "baton_schema_meta";
3
+ export declare const MIGRATIONS_TABLE = "baton_sql_migrations";
4
+ export declare const NOTIFY_CHANNEL = "baton_run_events";
5
+ export declare const SCHEMA_STATEMENTS: ReadonlyArray<string>;
6
+ export declare const schemaChecksum: () => string;