@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.
- package/README.md +3 -0
- package/dist/postgres/claims.d.ts +26 -0
- package/dist/postgres/claims.js +95 -0
- package/dist/postgres/event-stream.d.ts +20 -0
- package/dist/postgres/event-stream.js +16 -0
- package/dist/postgres/index.d.ts +3 -0
- package/dist/postgres/index.js +3 -0
- package/dist/postgres/locks.d.ts +18 -0
- package/dist/postgres/locks.js +51 -0
- package/dist/postgres/pg-helpers.d.ts +91 -0
- package/dist/postgres/pg-helpers.js +359 -0
- package/dist/postgres/run-schema.d.ts +23 -0
- package/dist/postgres/run-schema.js +92 -0
- package/dist/postgres/runtime-layer.d.ts +15 -0
- package/dist/postgres/runtime-layer.js +28 -0
- package/dist/postgres/schema.d.ts +6 -0
- package/dist/postgres/schema.js +274 -0
- package/dist/postgres/session-cancellation.d.ts +11 -0
- package/dist/postgres/session-cancellation.js +15 -0
- package/dist/postgres/session-storage.d.ts +32 -0
- package/dist/postgres/session-storage.js +63 -0
- package/dist/postgres/session-store.d.ts +24 -0
- package/dist/postgres/session-store.js +388 -0
- package/dist/postgres/store-admit.d.ts +21 -0
- package/dist/postgres/store-admit.js +87 -0
- package/dist/postgres/store-cancel.d.ts +14 -0
- package/dist/postgres/store-cancel.js +73 -0
- package/dist/postgres/store-claims.d.ts +17 -0
- package/dist/postgres/store-claims.js +72 -0
- package/dist/postgres/store-fan-out.d.ts +21 -0
- package/dist/postgres/store-fan-out.js +17 -0
- package/dist/postgres/store-inspection.d.ts +12 -0
- package/dist/postgres/store-inspection.js +44 -0
- package/dist/postgres/store-messaging.d.ts +14 -0
- package/dist/postgres/store-messaging.js +12 -0
- package/dist/postgres/store-model-response.d.ts +18 -0
- package/dist/postgres/store-model-response.js +156 -0
- package/dist/postgres/store-ops.d.ts +21 -0
- package/dist/postgres/store-ops.js +328 -0
- package/dist/postgres/store-program.d.ts +18 -0
- package/dist/postgres/store-program.js +36 -0
- package/dist/postgres/store-suspend.d.ts +13 -0
- package/dist/postgres/store-suspend.js +87 -0
- package/dist/postgres/store.d.ts +7 -0
- package/dist/postgres/store.js +373 -0
- package/dist/postgres/transaction-events.d.ts +15 -0
- package/dist/postgres/transaction-events.js +23 -0
- 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;
|