@intentface/latch-drizzle 0.9.1 → 0.10.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/adapter.shared.d.ts.map +1 -1
- package/dist/adapter.shared.js +10 -1
- package/dist/adapter.shared.js.map +1 -1
- package/dist/agent-store.d.ts +10 -0
- package/dist/agent-store.d.ts.map +1 -1
- package/dist/agent-store.js +513 -7
- package/dist/agent-store.js.map +1 -1
- package/dist/agent-versions.smoke.d.ts +2 -0
- package/dist/agent-versions.smoke.d.ts.map +1 -0
- package/dist/agent-versions.smoke.js +53 -0
- package/dist/agent-versions.smoke.js.map +1 -0
- package/dist/agent-versions.smoke.pg.d.ts +2 -0
- package/dist/agent-versions.smoke.pg.d.ts.map +1 -0
- package/dist/agent-versions.smoke.pg.js +53 -0
- package/dist/agent-versions.smoke.pg.js.map +1 -0
- package/dist/agent-versions.smoke.shared.d.ts +38 -0
- package/dist/agent-versions.smoke.shared.d.ts.map +1 -0
- package/dist/agent-versions.smoke.shared.js +299 -0
- package/dist/agent-versions.smoke.shared.js.map +1 -0
- package/dist/migrate.d.ts.map +1 -1
- package/dist/migrate.js +83 -0
- package/dist/migrate.js.map +1 -1
- package/dist/migrate.pg.d.ts.map +1 -1
- package/dist/migrate.pg.js +83 -0
- package/dist/migrate.pg.js.map +1 -1
- package/dist/schema.d.ts +506 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +97 -0
- package/dist/schema.js.map +1 -1
- package/dist/schema.pg.d.ts +478 -0
- package/dist/schema.pg.d.ts.map +1 -1
- package/dist/schema.pg.js +97 -0
- package/dist/schema.pg.js.map +1 -1
- package/package.json +7 -5
package/dist/agent-store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { desc, eq, inArray, and, like, sql } from "drizzle-orm";
|
|
2
2
|
import { parseAgentRef } from "@intentface/latch-agent-builder";
|
|
3
|
+
import { isUniqueViolation } from "./adapter.shared.js";
|
|
3
4
|
import * as sqliteSchema from "./schema.js";
|
|
4
5
|
import * as pgSchema from "./schema.pg.js";
|
|
5
6
|
/**
|
|
@@ -10,12 +11,101 @@ import * as pgSchema from "./schema.pg.js";
|
|
|
10
11
|
function newId() {
|
|
11
12
|
return `agt_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
|
|
12
13
|
}
|
|
14
|
+
function newVersionId() {
|
|
15
|
+
return `agv_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
|
|
16
|
+
}
|
|
17
|
+
function newActivationId() {
|
|
18
|
+
return `aga_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* How many times a publish retries when a concurrent one takes its number.
|
|
22
|
+
*
|
|
23
|
+
* Each contended round lets exactly one writer through, so a loser may need as
|
|
24
|
+
* many attempts as there are concurrent publishers. Real contention here is a
|
|
25
|
+
* couple of humans clicking Publish, so this is far above what can be reached —
|
|
26
|
+
* it exists only so the loop is bounded rather than as a tuned value.
|
|
27
|
+
*/
|
|
28
|
+
const PUBLISH_ATTEMPTS = 20;
|
|
29
|
+
/**
|
|
30
|
+
* Jittered pause between publish retries. Without it, losers re-read and re-try
|
|
31
|
+
* in lockstep and collide again; a few random milliseconds desynchronises them.
|
|
32
|
+
*/
|
|
33
|
+
const publishBackoff = () => new Promise((r) => setTimeout(r, Math.floor(Math.random() * 5) + 1));
|
|
34
|
+
/**
|
|
35
|
+
* Stable JSON for EQUALITY ONLY (never for storage): object keys sorted at every
|
|
36
|
+
* depth, and absent/null/empty-array treated alike.
|
|
37
|
+
*
|
|
38
|
+
* That last part is what makes "publishing an unchanged record appends nothing"
|
|
39
|
+
* actually hold. The same record reaches this function in three spellings — a
|
|
40
|
+
* caller's `AgentRecord` (absent keys are `undefined`), a row round-tripped
|
|
41
|
+
* through the live table (empty arrays are stored `null`), and the migration's
|
|
42
|
+
* backfilled v1 (built by `json_object`, so every absent column is an explicit
|
|
43
|
+
* `null`). Without flattening those, a no-op publish right after the backfill
|
|
44
|
+
* would append a duplicate version.
|
|
45
|
+
*/
|
|
46
|
+
function canonicalize(value) {
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.map(canonicalize);
|
|
49
|
+
if (value && typeof value === "object") {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const key of Object.keys(value).sort()) {
|
|
52
|
+
const v = value[key];
|
|
53
|
+
if (v === undefined || v === null)
|
|
54
|
+
continue;
|
|
55
|
+
if (Array.isArray(v) && v.length === 0)
|
|
56
|
+
continue;
|
|
57
|
+
out[key] = canonicalize(v);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
const canonical = (rec) => JSON.stringify(canonicalize(rec));
|
|
64
|
+
/**
|
|
65
|
+
* Which top-level record fields differ between two versions. Compared through
|
|
66
|
+
* `canonicalize`, so the same spelling rules that decide "did anything change at
|
|
67
|
+
* all" also decide "which field changed" — otherwise a record could publish
|
|
68
|
+
* (unchanged overall) yet report fields as changed, or vice versa.
|
|
69
|
+
*
|
|
70
|
+
* `undefined` for the first version: there is nothing to have changed from.
|
|
71
|
+
*/
|
|
72
|
+
function changedFieldsBetween(prev, next) {
|
|
73
|
+
if (!prev)
|
|
74
|
+
return undefined;
|
|
75
|
+
const a = canonicalize(prev);
|
|
76
|
+
const b = canonicalize(next);
|
|
77
|
+
const fields = [...new Set([...Object.keys(a), ...Object.keys(b)])]
|
|
78
|
+
.sort()
|
|
79
|
+
.filter((k) => JSON.stringify(a[k]) !== JSON.stringify(b[k]));
|
|
80
|
+
return fields.length ? fields : undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A stored version payload back into an `AgentRecord`. Nulls become absent
|
|
84
|
+
* (see `canonicalize`) so a backfilled row and a published one round-trip to
|
|
85
|
+
* the same record. JSON columns arrive parsed from both dialects; a string is
|
|
86
|
+
* tolerated for hosts whose driver hands back raw text.
|
|
87
|
+
*/
|
|
88
|
+
function recordFromJson(value) {
|
|
89
|
+
const raw = (typeof value === "string" ? JSON.parse(value) : value);
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [key, v] of Object.entries(raw ?? {})) {
|
|
92
|
+
if (v === null || v === undefined)
|
|
93
|
+
continue;
|
|
94
|
+
out[key] = v;
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(out.connections))
|
|
97
|
+
out.connections = [];
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
13
100
|
/** Column value (INTEGER) ↔ AgentRecord.maxSteps: NULL=default, 0=unlimited, N=cap. */
|
|
14
101
|
const maxStepsToCol = (m) => m === "unlimited" ? 0 : typeof m === "number" ? m : null;
|
|
15
102
|
const maxStepsFromCol = (v) => v == null ? undefined : v === 0 ? "unlimited" : v;
|
|
16
103
|
const ownerKeyOf = (segments) => segments.map(encodeURIComponent).join("/");
|
|
17
104
|
function createStore(queries, opts) {
|
|
18
105
|
const ownerKey = (p, scope) => ownerKeyOf(opts.owner(p, scope));
|
|
106
|
+
// The user-scope owner key is already per-person wherever the host's user
|
|
107
|
+
// scope carries a user id, so it is the right default editor identity.
|
|
108
|
+
const editorKey = (p) => opts.editorKey?.(p) ?? ownerKeyOf(opts.owner(p, "user"));
|
|
19
109
|
const recordOf = (row) => ({
|
|
20
110
|
name: row.name,
|
|
21
111
|
title: row.title ?? undefined,
|
|
@@ -30,6 +120,18 @@ function createStore(queries, opts) {
|
|
|
30
120
|
attachedEvals: (row.attachedEvals ?? undefined),
|
|
31
121
|
scope: row.scope,
|
|
32
122
|
});
|
|
123
|
+
/** The matched ROW, for callers that need more than the record (see `resolve`). */
|
|
124
|
+
async function findRow(p, name, scope) {
|
|
125
|
+
// Personal wins over org on a name clash — unless pinned to one scope.
|
|
126
|
+
const owners = scope === "org"
|
|
127
|
+
? [ownerKey(p, "org")]
|
|
128
|
+
: scope === "user"
|
|
129
|
+
? [ownerKey(p, "user")]
|
|
130
|
+
: [ownerKey(p, "user"), ownerKey(p, "org")];
|
|
131
|
+
const rows = await queries.byOwners(owners, name);
|
|
132
|
+
const personal = rows.find((r) => r.owner === ownerKey(p, "user"));
|
|
133
|
+
return personal ?? rows[0];
|
|
134
|
+
}
|
|
33
135
|
async function find(p, name, scope) {
|
|
34
136
|
// Personal wins over org on a name clash — unless pinned to one scope.
|
|
35
137
|
const owners = scope === "org"
|
|
@@ -49,9 +151,10 @@ function createStore(queries, opts) {
|
|
|
49
151
|
// it), so a same-named record in the OTHER scope can never shadow the
|
|
50
152
|
// agent the binding was created for.
|
|
51
153
|
const ref = parseAgentRef(name);
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
154
|
+
const row = await findRow(principal, ref.name, ref.scope);
|
|
155
|
+
if (!row)
|
|
54
156
|
return undefined;
|
|
157
|
+
const rec = recordOf(row);
|
|
55
158
|
const decorate = opts.decorateResolve ?? ((cfg) => cfg);
|
|
56
159
|
const sel = rec.tools;
|
|
57
160
|
const approvalEntries = Object.entries(sel?.approval ?? {});
|
|
@@ -80,6 +183,10 @@ function createStore(queries, opts) {
|
|
|
80
183
|
]
|
|
81
184
|
: undefined;
|
|
82
185
|
return decorate({
|
|
186
|
+
// Which stored version this config is a copy of — recorded on the run
|
|
187
|
+
// and emitted as a trace tag, so an answer can be traced back to the
|
|
188
|
+
// configuration that produced it.
|
|
189
|
+
configVersion: row.publishedVersion ?? undefined,
|
|
83
190
|
model: opts.modelFor(rec.modelId),
|
|
84
191
|
instructions: rec.instructions,
|
|
85
192
|
connections: rec.connections,
|
|
@@ -129,23 +236,209 @@ function createStore(queries, opts) {
|
|
|
129
236
|
async listMemoryEnabledNames() {
|
|
130
237
|
return queries.memoryEnabledNames();
|
|
131
238
|
},
|
|
132
|
-
async upsert(principal, rec) {
|
|
239
|
+
async upsert(principal, rec, opts) {
|
|
240
|
+
await publish(ownerKey(principal, rec.scope), rec, {
|
|
241
|
+
author: editorKey(principal),
|
|
242
|
+
...opts,
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
async remove(principal, scope, name) {
|
|
246
|
+
const owner = ownerKey(principal, scope);
|
|
247
|
+
// Live row first: an interrupted delete then leaves orphan version rows
|
|
248
|
+
// (harmless — nothing resolves them) rather than a live agent whose
|
|
249
|
+
// history has silently vanished.
|
|
250
|
+
await queries.remove(owner, name);
|
|
251
|
+
await queries.removeVersions(owner, name);
|
|
252
|
+
await queries.removeActivations(owner, name);
|
|
253
|
+
await queries.removeDrafts(owner, name);
|
|
254
|
+
},
|
|
255
|
+
async listVersions(principal, name, scope) {
|
|
256
|
+
const owner = ownerKey(principal, scope);
|
|
257
|
+
const [rows, live] = await Promise.all([
|
|
258
|
+
queries.versions(owner, name),
|
|
259
|
+
queries.byOwners([owner], name),
|
|
260
|
+
]);
|
|
261
|
+
const publishedVersion = live[0]?.publishedVersion ?? null;
|
|
262
|
+
return rows.map((r) => ({
|
|
263
|
+
version: r.version,
|
|
264
|
+
author: r.author ?? undefined,
|
|
265
|
+
source: (r.source ?? undefined),
|
|
266
|
+
message: r.changeMessage ?? undefined,
|
|
267
|
+
changedFields: (r.changedFields ?? undefined),
|
|
268
|
+
createdAt: r.createdAt,
|
|
269
|
+
published: r.version === publishedVersion,
|
|
270
|
+
}));
|
|
271
|
+
},
|
|
272
|
+
async getVersion(principal, name, scope, version) {
|
|
273
|
+
const row = await queries.versionAt(ownerKey(principal, scope), name, version);
|
|
274
|
+
return row ? recordFromJson(row.record) : undefined;
|
|
275
|
+
},
|
|
276
|
+
async activateVersion(principal, name, scope, version, opts) {
|
|
277
|
+
const owner = ownerKey(principal, scope);
|
|
278
|
+
const row = await queries.versionAt(owner, name, version);
|
|
279
|
+
if (!row)
|
|
280
|
+
throw new Error(`No version ${version} of agent "${name}".`);
|
|
281
|
+
// Pin the scope to the one asked for: it decides the owner key, and a
|
|
282
|
+
// record stored under a since-changed scope must not write elsewhere.
|
|
283
|
+
const rec = { ...recordFromJson(row.record), name, scope };
|
|
284
|
+
const now = Date.now();
|
|
285
|
+
// Pointer move only — no version is appended. Flipping between two
|
|
286
|
+
// configurations must not grow history by one duplicate row each time.
|
|
133
287
|
await queries.upsert({
|
|
134
288
|
id: newId(),
|
|
135
|
-
owner
|
|
289
|
+
owner,
|
|
136
290
|
rec,
|
|
137
291
|
maxSteps: maxStepsToCol(rec.maxSteps),
|
|
292
|
+
now,
|
|
293
|
+
publishedVersion: version,
|
|
294
|
+
});
|
|
295
|
+
await recordActivation(owner, name, version, opts?.actor ?? editorKey(principal), "activate", now);
|
|
296
|
+
},
|
|
297
|
+
async listActivations(principal, name, scope, limit) {
|
|
298
|
+
const rows = await queries.activations(ownerKey(principal, scope), name, limit);
|
|
299
|
+
return rows.map((r) => ({
|
|
300
|
+
version: r.version,
|
|
301
|
+
actor: r.actor ?? undefined,
|
|
302
|
+
source: (r.source ?? undefined),
|
|
303
|
+
createdAt: r.createdAt,
|
|
304
|
+
}));
|
|
305
|
+
},
|
|
306
|
+
async getDraft(principal, name, scope) {
|
|
307
|
+
const row = await queries.draft(ownerKey(principal, scope), name, editorKey(principal));
|
|
308
|
+
if (!row)
|
|
309
|
+
return undefined;
|
|
310
|
+
return {
|
|
311
|
+
record: recordFromJson(row.record),
|
|
312
|
+
baseVersion: row.baseVersion ?? undefined,
|
|
313
|
+
updatedAt: row.updatedAt,
|
|
314
|
+
};
|
|
315
|
+
},
|
|
316
|
+
async saveDraft(principal, name, scope, rec, baseVersion) {
|
|
317
|
+
await queries.upsertDraft({
|
|
318
|
+
owner: ownerKey(principal, scope),
|
|
319
|
+
name,
|
|
320
|
+
editor: editorKey(principal),
|
|
321
|
+
// Pin identity to the arguments: a draft keyed (owner, name, editor)
|
|
322
|
+
// whose record claimed another name would publish to the wrong agent.
|
|
323
|
+
rec: { ...rec, name, scope },
|
|
324
|
+
baseVersion: baseVersion ?? null,
|
|
138
325
|
now: Date.now(),
|
|
139
326
|
});
|
|
140
327
|
},
|
|
141
|
-
async
|
|
142
|
-
await queries.
|
|
328
|
+
async discardDraft(principal, name, scope) {
|
|
329
|
+
await queries.removeDraft(ownerKey(principal, scope), name, editorKey(principal));
|
|
330
|
+
},
|
|
331
|
+
async listDrafts(principal) {
|
|
332
|
+
const mine = editorKey(principal);
|
|
333
|
+
const owners = [ownerKey(principal, "user"), ownerKey(principal, "org")];
|
|
334
|
+
const rows = await queries.draftsByOwners(owners);
|
|
335
|
+
return rows.map((r) => ({
|
|
336
|
+
name: r.name,
|
|
337
|
+
scope: r.owner === owners[1] ? "org" : "user",
|
|
338
|
+
mine: r.editor === mine,
|
|
339
|
+
updatedAt: r.updatedAt,
|
|
340
|
+
}));
|
|
143
341
|
},
|
|
144
342
|
};
|
|
343
|
+
/** Append to the publish timeline. Never fails the write it follows. */
|
|
344
|
+
async function recordActivation(owner, name, version, actor, source, now) {
|
|
345
|
+
try {
|
|
346
|
+
await queries.insertActivation({
|
|
347
|
+
id: newActivationId(),
|
|
348
|
+
owner,
|
|
349
|
+
name,
|
|
350
|
+
version,
|
|
351
|
+
actor: actor ?? null,
|
|
352
|
+
source,
|
|
353
|
+
now,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
// The timeline is an audit trail, not a correctness invariant: the live
|
|
358
|
+
// row and the version are already written, and losing one entry must not
|
|
359
|
+
// turn a successful publish into a failed one.
|
|
360
|
+
console.warn(`Could not record agent activation for "${name}":`, error);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Write the live row and append a version — the one path by which a record
|
|
365
|
+
* becomes current.
|
|
366
|
+
*
|
|
367
|
+
* Two ordering rules, neither of which needs a transaction (this store also
|
|
368
|
+
* serves better-sqlite3, whose transaction API rejects an async callback, and
|
|
369
|
+
* libsql, which opens one on a NEW connection — see `createSqliteAdapter`):
|
|
370
|
+
*
|
|
371
|
+
* 1. The VERSION row is written before the live row. Interrupted that way,
|
|
372
|
+
* history holds a version nothing points at — visible, inert, superseded by
|
|
373
|
+
* the next publish. The reverse order would leave the live row claiming a
|
|
374
|
+
* `published_version` that does not exist, breaking history and rollback.
|
|
375
|
+
* 2. The version NUMBER is `max + 1` read outside any lock, so two concurrent
|
|
376
|
+
* publishes can choose the same one. The UNIQUE (owner, name, version)
|
|
377
|
+
* index rejects the loser, which re-reads the head and retries — the same
|
|
378
|
+
* "let the index arbitrate" discipline the rest of this package uses.
|
|
379
|
+
*/
|
|
380
|
+
async function publish(owner, rec, opts) {
|
|
381
|
+
const target = canonical(rec);
|
|
382
|
+
for (let attempt = 1; attempt <= PUBLISH_ATTEMPTS; attempt++) {
|
|
383
|
+
const head = (await queries.versions(owner, rec.name, 1))[0];
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
// Unchanged content appends nothing, so history stays a list of real
|
|
386
|
+
// changes. The live row is still written: it reconciles a row that drifted
|
|
387
|
+
// from its version (a direct DB edit, an interrupted earlier publish).
|
|
388
|
+
if (head && canonical(recordFromJson(head.record)) === target) {
|
|
389
|
+
await queries.upsert({
|
|
390
|
+
id: newId(),
|
|
391
|
+
owner,
|
|
392
|
+
rec,
|
|
393
|
+
maxSteps: maxStepsToCol(rec.maxSteps),
|
|
394
|
+
now,
|
|
395
|
+
publishedVersion: head.version,
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const version = (head?.version ?? 0) + 1;
|
|
400
|
+
const message = opts?.message?.trim();
|
|
401
|
+
try {
|
|
402
|
+
await queries.insertVersion({
|
|
403
|
+
id: newVersionId(),
|
|
404
|
+
owner,
|
|
405
|
+
name: rec.name,
|
|
406
|
+
version,
|
|
407
|
+
rec,
|
|
408
|
+
author: opts?.author ?? null,
|
|
409
|
+
source: opts?.source ?? "api",
|
|
410
|
+
changedFields: changedFieldsBetween(head ? recordFromJson(head.record) : undefined, rec) ?? null,
|
|
411
|
+
changeMessage: message || null,
|
|
412
|
+
now,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
// Someone else took this number: back off, re-read the head, try again.
|
|
417
|
+
if (isUniqueViolation(error) && attempt < PUBLISH_ATTEMPTS) {
|
|
418
|
+
await publishBackoff();
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
await queries.upsert({
|
|
424
|
+
id: newId(),
|
|
425
|
+
owner,
|
|
426
|
+
rec,
|
|
427
|
+
maxSteps: maxStepsToCol(rec.maxSteps),
|
|
428
|
+
now,
|
|
429
|
+
publishedVersion: version,
|
|
430
|
+
});
|
|
431
|
+
// A publish is also an activation: recording both here and in
|
|
432
|
+
// `activateVersion` is what makes the timeline complete.
|
|
433
|
+
await recordActivation(owner, rec.name, version, opts?.author, "publish", now);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`Could not publish agent "${rec.name}": ${PUBLISH_ATTEMPTS} version numbers were taken concurrently.`);
|
|
437
|
+
}
|
|
145
438
|
}
|
|
146
439
|
/** Shared upsert `set` payload (column shapes match across dialects). */
|
|
147
440
|
function upsertValues(row) {
|
|
148
|
-
const { id, owner, rec, maxSteps, now } = row;
|
|
441
|
+
const { id, owner, rec, maxSteps, now, publishedVersion } = row;
|
|
149
442
|
return {
|
|
150
443
|
insert: {
|
|
151
444
|
id,
|
|
@@ -162,6 +455,7 @@ function upsertValues(row) {
|
|
|
162
455
|
attachedEvals: rec.attachedEvals?.length ? rec.attachedEvals : null,
|
|
163
456
|
maxSteps,
|
|
164
457
|
scope: rec.scope,
|
|
458
|
+
publishedVersion,
|
|
165
459
|
createdAt: now,
|
|
166
460
|
updatedAt: now,
|
|
167
461
|
},
|
|
@@ -177,13 +471,32 @@ function upsertValues(row) {
|
|
|
177
471
|
attachedEvals: rec.attachedEvals?.length ? rec.attachedEvals : null,
|
|
178
472
|
maxSteps,
|
|
179
473
|
scope: rec.scope,
|
|
474
|
+
publishedVersion,
|
|
180
475
|
updatedAt: now,
|
|
181
476
|
},
|
|
182
477
|
};
|
|
183
478
|
}
|
|
479
|
+
/** Shared version-row insert payload (column shapes match across dialects). */
|
|
480
|
+
function versionValues(row) {
|
|
481
|
+
return {
|
|
482
|
+
id: row.id,
|
|
483
|
+
owner: row.owner,
|
|
484
|
+
name: row.name,
|
|
485
|
+
version: row.version,
|
|
486
|
+
record: row.rec,
|
|
487
|
+
author: row.author,
|
|
488
|
+
source: row.source,
|
|
489
|
+
changedFields: row.changedFields,
|
|
490
|
+
changeMessage: row.changeMessage,
|
|
491
|
+
createdAt: row.now,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
184
494
|
/** `AgentBuilderStore` on a Drizzle SQLite database (libsql / better-sqlite3 / Turso). */
|
|
185
495
|
export function createSqliteAgentBuilderStore(db, opts) {
|
|
186
496
|
const t = sqliteSchema.agents;
|
|
497
|
+
const v = sqliteSchema.agentVersions;
|
|
498
|
+
const a = sqliteSchema.agentActivations;
|
|
499
|
+
const d = sqliteSchema.agentDrafts;
|
|
187
500
|
return createStore({
|
|
188
501
|
async byOwners(owners, name) {
|
|
189
502
|
const where = name ? and(inArray(t.owner, owners), eq(t.name, name)) : inArray(t.owner, owners);
|
|
@@ -205,11 +518,109 @@ export function createSqliteAgentBuilderStore(db, opts) {
|
|
|
205
518
|
async remove(owner, name) {
|
|
206
519
|
await db.delete(t).where(and(eq(t.owner, owner), eq(t.name, name)));
|
|
207
520
|
},
|
|
521
|
+
async versions(owner, name, limit) {
|
|
522
|
+
const q = db
|
|
523
|
+
.select({
|
|
524
|
+
version: v.version,
|
|
525
|
+
record: v.record,
|
|
526
|
+
author: v.author,
|
|
527
|
+
source: v.source,
|
|
528
|
+
changedFields: v.changedFields,
|
|
529
|
+
changeMessage: v.changeMessage,
|
|
530
|
+
createdAt: v.createdAt,
|
|
531
|
+
})
|
|
532
|
+
.from(v)
|
|
533
|
+
.where(and(eq(v.owner, owner), eq(v.name, name)))
|
|
534
|
+
.orderBy(desc(v.version));
|
|
535
|
+
return (limit ? await q.limit(limit) : await q);
|
|
536
|
+
},
|
|
537
|
+
async versionAt(owner, name, version) {
|
|
538
|
+
const rows = (await db
|
|
539
|
+
.select({
|
|
540
|
+
version: v.version,
|
|
541
|
+
record: v.record,
|
|
542
|
+
author: v.author,
|
|
543
|
+
source: v.source,
|
|
544
|
+
changedFields: v.changedFields,
|
|
545
|
+
changeMessage: v.changeMessage,
|
|
546
|
+
createdAt: v.createdAt,
|
|
547
|
+
})
|
|
548
|
+
.from(v)
|
|
549
|
+
.where(and(eq(v.owner, owner), eq(v.name, name), eq(v.version, version)))
|
|
550
|
+
.limit(1));
|
|
551
|
+
return rows[0];
|
|
552
|
+
},
|
|
553
|
+
async insertVersion(row) {
|
|
554
|
+
await db.insert(v).values(versionValues(row));
|
|
555
|
+
},
|
|
556
|
+
async removeVersions(owner, name) {
|
|
557
|
+
await db.delete(v).where(and(eq(v.owner, owner), eq(v.name, name)));
|
|
558
|
+
},
|
|
559
|
+
async insertActivation(row) {
|
|
560
|
+
await db.insert(a).values({
|
|
561
|
+
id: row.id,
|
|
562
|
+
owner: row.owner,
|
|
563
|
+
name: row.name,
|
|
564
|
+
version: row.version,
|
|
565
|
+
actor: row.actor,
|
|
566
|
+
source: row.source,
|
|
567
|
+
createdAt: row.now,
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
async activations(owner, name, limit) {
|
|
571
|
+
const q = db
|
|
572
|
+
.select({ version: a.version, actor: a.actor, source: a.source, createdAt: a.createdAt })
|
|
573
|
+
.from(a)
|
|
574
|
+
.where(and(eq(a.owner, owner), eq(a.name, name)))
|
|
575
|
+
.orderBy(desc(a.createdAt));
|
|
576
|
+
return (limit ? await q.limit(limit) : await q);
|
|
577
|
+
},
|
|
578
|
+
async removeActivations(owner, name) {
|
|
579
|
+
await db.delete(a).where(and(eq(a.owner, owner), eq(a.name, name)));
|
|
580
|
+
},
|
|
581
|
+
async draft(owner, name, editor) {
|
|
582
|
+
const rows = (await db
|
|
583
|
+
.select()
|
|
584
|
+
.from(d)
|
|
585
|
+
.where(and(eq(d.owner, owner), eq(d.name, name), eq(d.editor, editor)))
|
|
586
|
+
.limit(1));
|
|
587
|
+
return rows[0];
|
|
588
|
+
},
|
|
589
|
+
async upsertDraft(row) {
|
|
590
|
+
await db
|
|
591
|
+
.insert(d)
|
|
592
|
+
.values({
|
|
593
|
+
owner: row.owner,
|
|
594
|
+
name: row.name,
|
|
595
|
+
editor: row.editor,
|
|
596
|
+
record: row.rec,
|
|
597
|
+
baseVersion: row.baseVersion,
|
|
598
|
+
updatedAt: row.now,
|
|
599
|
+
})
|
|
600
|
+
.onConflictDoUpdate({
|
|
601
|
+
target: [d.owner, d.name, d.editor],
|
|
602
|
+
set: { record: row.rec, baseVersion: row.baseVersion, updatedAt: row.now },
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
async removeDraft(owner, name, editor) {
|
|
606
|
+
await db
|
|
607
|
+
.delete(d)
|
|
608
|
+
.where(and(eq(d.owner, owner), eq(d.name, name), eq(d.editor, editor)));
|
|
609
|
+
},
|
|
610
|
+
async draftsByOwners(owners) {
|
|
611
|
+
return (await db.select().from(d).where(inArray(d.owner, owners)));
|
|
612
|
+
},
|
|
613
|
+
async removeDrafts(owner, name) {
|
|
614
|
+
await db.delete(d).where(and(eq(d.owner, owner), eq(d.name, name)));
|
|
615
|
+
},
|
|
208
616
|
}, opts);
|
|
209
617
|
}
|
|
210
618
|
/** `AgentBuilderStore` on a Drizzle Postgres database (node-postgres / postgres-js / PGlite). */
|
|
211
619
|
export function createPostgresAgentBuilderStore(db, opts) {
|
|
212
620
|
const t = pgSchema.agents;
|
|
621
|
+
const v = pgSchema.agentVersions;
|
|
622
|
+
const a = pgSchema.agentActivations;
|
|
623
|
+
const d = pgSchema.agentDrafts;
|
|
213
624
|
return createStore({
|
|
214
625
|
async byOwners(owners, name) {
|
|
215
626
|
const where = name ? and(inArray(t.owner, owners), eq(t.name, name)) : inArray(t.owner, owners);
|
|
@@ -231,6 +642,101 @@ export function createPostgresAgentBuilderStore(db, opts) {
|
|
|
231
642
|
async remove(owner, name) {
|
|
232
643
|
await db.delete(t).where(and(eq(t.owner, owner), eq(t.name, name)));
|
|
233
644
|
},
|
|
645
|
+
async versions(owner, name, limit) {
|
|
646
|
+
const q = db
|
|
647
|
+
.select({
|
|
648
|
+
version: v.version,
|
|
649
|
+
record: v.record,
|
|
650
|
+
author: v.author,
|
|
651
|
+
source: v.source,
|
|
652
|
+
changedFields: v.changedFields,
|
|
653
|
+
changeMessage: v.changeMessage,
|
|
654
|
+
createdAt: v.createdAt,
|
|
655
|
+
})
|
|
656
|
+
.from(v)
|
|
657
|
+
.where(and(eq(v.owner, owner), eq(v.name, name)))
|
|
658
|
+
.orderBy(desc(v.version));
|
|
659
|
+
return (limit ? await q.limit(limit) : await q);
|
|
660
|
+
},
|
|
661
|
+
async versionAt(owner, name, version) {
|
|
662
|
+
const rows = (await db
|
|
663
|
+
.select({
|
|
664
|
+
version: v.version,
|
|
665
|
+
record: v.record,
|
|
666
|
+
author: v.author,
|
|
667
|
+
source: v.source,
|
|
668
|
+
changedFields: v.changedFields,
|
|
669
|
+
changeMessage: v.changeMessage,
|
|
670
|
+
createdAt: v.createdAt,
|
|
671
|
+
})
|
|
672
|
+
.from(v)
|
|
673
|
+
.where(and(eq(v.owner, owner), eq(v.name, name), eq(v.version, version)))
|
|
674
|
+
.limit(1));
|
|
675
|
+
return rows[0];
|
|
676
|
+
},
|
|
677
|
+
async insertVersion(row) {
|
|
678
|
+
await db.insert(v).values(versionValues(row));
|
|
679
|
+
},
|
|
680
|
+
async removeVersions(owner, name) {
|
|
681
|
+
await db.delete(v).where(and(eq(v.owner, owner), eq(v.name, name)));
|
|
682
|
+
},
|
|
683
|
+
async insertActivation(row) {
|
|
684
|
+
await db.insert(a).values({
|
|
685
|
+
id: row.id,
|
|
686
|
+
owner: row.owner,
|
|
687
|
+
name: row.name,
|
|
688
|
+
version: row.version,
|
|
689
|
+
actor: row.actor,
|
|
690
|
+
source: row.source,
|
|
691
|
+
createdAt: row.now,
|
|
692
|
+
});
|
|
693
|
+
},
|
|
694
|
+
async activations(owner, name, limit) {
|
|
695
|
+
const q = db
|
|
696
|
+
.select({ version: a.version, actor: a.actor, source: a.source, createdAt: a.createdAt })
|
|
697
|
+
.from(a)
|
|
698
|
+
.where(and(eq(a.owner, owner), eq(a.name, name)))
|
|
699
|
+
.orderBy(desc(a.createdAt));
|
|
700
|
+
return (limit ? await q.limit(limit) : await q);
|
|
701
|
+
},
|
|
702
|
+
async removeActivations(owner, name) {
|
|
703
|
+
await db.delete(a).where(and(eq(a.owner, owner), eq(a.name, name)));
|
|
704
|
+
},
|
|
705
|
+
async draft(owner, name, editor) {
|
|
706
|
+
const rows = (await db
|
|
707
|
+
.select()
|
|
708
|
+
.from(d)
|
|
709
|
+
.where(and(eq(d.owner, owner), eq(d.name, name), eq(d.editor, editor)))
|
|
710
|
+
.limit(1));
|
|
711
|
+
return rows[0];
|
|
712
|
+
},
|
|
713
|
+
async upsertDraft(row) {
|
|
714
|
+
await db
|
|
715
|
+
.insert(d)
|
|
716
|
+
.values({
|
|
717
|
+
owner: row.owner,
|
|
718
|
+
name: row.name,
|
|
719
|
+
editor: row.editor,
|
|
720
|
+
record: row.rec,
|
|
721
|
+
baseVersion: row.baseVersion,
|
|
722
|
+
updatedAt: row.now,
|
|
723
|
+
})
|
|
724
|
+
.onConflictDoUpdate({
|
|
725
|
+
target: [d.owner, d.name, d.editor],
|
|
726
|
+
set: { record: row.rec, baseVersion: row.baseVersion, updatedAt: row.now },
|
|
727
|
+
});
|
|
728
|
+
},
|
|
729
|
+
async removeDraft(owner, name, editor) {
|
|
730
|
+
await db
|
|
731
|
+
.delete(d)
|
|
732
|
+
.where(and(eq(d.owner, owner), eq(d.name, name), eq(d.editor, editor)));
|
|
733
|
+
},
|
|
734
|
+
async draftsByOwners(owners) {
|
|
735
|
+
return (await db.select().from(d).where(inArray(d.owner, owners)));
|
|
736
|
+
},
|
|
737
|
+
async removeDrafts(owner, name) {
|
|
738
|
+
await db.delete(d).where(and(eq(d.owner, owner), eq(d.name, name)));
|
|
739
|
+
},
|
|
234
740
|
}, opts);
|
|
235
741
|
}
|
|
236
742
|
//# sourceMappingURL=agent-store.js.map
|