@smithers-orchestrator/db 0.16.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/LICENSE +21 -0
- package/package.json +43 -0
- package/src/JsonBounds.ts +6 -0
- package/src/SchemaRegistryEntry.ts +6 -0
- package/src/SqlMessageStorage.js +818 -0
- package/src/SqlMessageStorageEventHistoryQuery.ts +7 -0
- package/src/SqliteWriteRetryOptions.ts +7 -0
- package/src/adapter/AlertRow.ts +29 -0
- package/src/adapter/AlertSeverity.ts +2 -0
- package/src/adapter/AlertStatus.ts +2 -0
- package/src/adapter/ApprovalRow.ts +13 -0
- package/src/adapter/AttemptRow.ts +17 -0
- package/src/adapter/CacheRow.ts +12 -0
- package/src/adapter/DB_ALERT_ALLOWED_SEVERITIES.js +5 -0
- package/src/adapter/DB_ALERT_ALLOWED_STATUSES.js +6 -0
- package/src/adapter/DB_ALERT_ID_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_ALERT_MESSAGE_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_ALERT_POLICY_NAME_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +10 -0
- package/src/adapter/DB_RUN_ID_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js +1 -0
- package/src/adapter/EventHistoryQuery.ts +7 -0
- package/src/adapter/HumanRequestRow.ts +19 -0
- package/src/adapter/NodeDiffCacheRow.ts +9 -0
- package/src/adapter/NodeRow.ts +10 -0
- package/src/adapter/PendingHumanRequestRow.ts +7 -0
- package/src/adapter/RunAncestryRow.ts +5 -0
- package/src/adapter/RunRow.ts +21 -0
- package/src/adapter/SignalQuery.ts +6 -0
- package/src/adapter/SignalRow.ts +9 -0
- package/src/adapter/SmithersDb.js +2236 -0
- package/src/adapter/StaleRunRecord.ts +7 -0
- package/src/adapter/index.js +27 -0
- package/src/adapter.js +2359 -0
- package/src/assertJsonPayloadWithinBounds.js +94 -0
- package/src/assertMaxBytes.js +23 -0
- package/src/assertMaxJsonDepth.js +40 -0
- package/src/assertMaxStringLength.js +16 -0
- package/src/assertOptionalArrayMaxLength.js +16 -0
- package/src/assertOptionalStringMaxLength.js +11 -0
- package/src/assertPositiveFiniteInteger.js +14 -0
- package/src/assertPositiveFiniteNumber.js +12 -0
- package/src/buildHumanRequestId.js +9 -0
- package/src/cache/nodeDiffCache.js +124 -0
- package/src/ensure.js +18 -0
- package/src/ensureSqlMessageStorage.js +11 -0
- package/src/ensureSqlMessageStorageEffect.js +12 -0
- package/src/frame-codec/FRAME_KEYFRAME_INTERVAL.js +1 -0
- package/src/frame-codec/FrameDelta.ts +6 -0
- package/src/frame-codec/FrameDeltaOp.ts +20 -0
- package/src/frame-codec/FrameEncoding.ts +1 -0
- package/src/frame-codec/JsonPath.ts +3 -0
- package/src/frame-codec/JsonPathSegment.ts +1 -0
- package/src/frame-codec/applyFrameDelta.js +143 -0
- package/src/frame-codec/applyFrameDeltaJson.js +10 -0
- package/src/frame-codec/encodeFrameDelta.js +247 -0
- package/src/frame-codec/index.js +15 -0
- package/src/frame-codec/normalizeFrameEncoding.js +13 -0
- package/src/frame-codec/parseFrameDelta.js +27 -0
- package/src/frame-codec/serializeFrameDelta.js +9 -0
- package/src/frame-codec.js +409 -0
- package/src/getSqlMessageStorage.js +11 -0
- package/src/index.d.ts +5203 -0
- package/src/index.js +20 -0
- package/src/input-bounds.js +12 -0
- package/src/input.js +17 -0
- package/src/internal-schema/index.js +19 -0
- package/src/internal-schema/smithersAlerts.js +27 -0
- package/src/internal-schema/smithersApprovals.js +18 -0
- package/src/internal-schema/smithersAttempts.js +20 -0
- package/src/internal-schema/smithersCache.js +13 -0
- package/src/internal-schema/smithersCron.js +11 -0
- package/src/internal-schema/smithersEvents.js +10 -0
- package/src/internal-schema/smithersFrames.js +14 -0
- package/src/internal-schema/smithersHumanRequests.js +17 -0
- package/src/internal-schema/smithersNodeDiffs.js +12 -0
- package/src/internal-schema/smithersNodes.js +13 -0
- package/src/internal-schema/smithersRalph.js +10 -0
- package/src/internal-schema/smithersRuns.js +22 -0
- package/src/internal-schema/smithersSandboxes.js +16 -0
- package/src/internal-schema/smithersSignals.js +12 -0
- package/src/internal-schema/smithersTimeTravelAudit.js +12 -0
- package/src/internal-schema/smithersToolCalls.js +19 -0
- package/src/internal-schema/smithersVectors.js +12 -0
- package/src/internal-schema.js +245 -0
- package/src/isRetryableSqliteWriteError.js +53 -0
- package/src/loadInputEffect.js +28 -0
- package/src/loadOutputsEffect.js +87 -0
- package/src/output/OutputKey.ts +1 -0
- package/src/output/buildKeyWhere.js +17 -0
- package/src/output/buildOutputRow.js +34 -0
- package/src/output/describeSchemaShape.js +70 -0
- package/src/output/getAgentOutputSchema.js +13 -0
- package/src/output/getKeyColumns.js +19 -0
- package/src/output/index.js +14 -0
- package/src/output/selectOutputRowEffect.js +30 -0
- package/src/output/stripAutoColumns.js +10 -0
- package/src/output/upsertOutputRowEffect.js +38 -0
- package/src/output/validateExistingOutput.js +17 -0
- package/src/output/validateOutput.js +17 -0
- package/src/output-schema-descriptor.js +163 -0
- package/src/output.js +240 -0
- package/src/react-output.js +10 -0
- package/src/runState/ComputeRunStateOptions.ts +4 -0
- package/src/runState/DeriveRunStateInput.ts +10 -0
- package/src/runState/RUN_STATE_HEARTBEAT_STALE_MS.js +1 -0
- package/src/runState/ReasonBlocked.ts +10 -0
- package/src/runState/ReasonUnhealthy.ts +6 -0
- package/src/runState/RunState.ts +12 -0
- package/src/runState/RunStateView.ts +11 -0
- package/src/runState/computeRunState.js +22 -0
- package/src/runState/computeRunStateFromRow.js +102 -0
- package/src/runState/deriveRunState.js +109 -0
- package/src/runState/parseEventMeta.js +18 -0
- package/src/runState/parseTimerMeta.js +16 -0
- package/src/runState-types.ts +23 -0
- package/src/runState.js +7 -0
- package/src/schema-signature.js +22 -0
- package/src/snapshot.js +125 -0
- package/src/sql-message-storage.js +839 -0
- package/src/storage/InMemoryStorage.js +484 -0
- package/src/storage/StorageService.js +7 -0
- package/src/storage/StorageServiceShape.ts +122 -0
- package/src/storage/StorageServiceTypes.ts +150 -0
- package/src/unwrapZodType.js +17 -0
- package/src/utils/camelToSnake.js +6 -0
- package/src/withSqliteWriteRetryEffect.js +110 -0
- package/src/write-retry.js +49 -0
- package/src/zodToCreateTableSQL.js +41 -0
- package/src/zodToTable.js +60 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { getTableColumns } from "drizzle-orm/utils";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
5
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
6
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
7
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {BunSQLiteDatabase<Record<string, never>>} db
|
|
11
|
+
* @param {Table} inputTable
|
|
12
|
+
* @param {string} runId
|
|
13
|
+
* @returns {Effect.Effect<Record<string, unknown> | undefined, SmithersError>}
|
|
14
|
+
*/
|
|
15
|
+
export function loadInput(db, inputTable, runId) {
|
|
16
|
+
const cols = getTableColumns(inputTable);
|
|
17
|
+
const runIdCol = cols.runId;
|
|
18
|
+
if (!runIdCol) {
|
|
19
|
+
throw new SmithersError("DB_MISSING_COLUMNS", "schema.input must include runId column");
|
|
20
|
+
}
|
|
21
|
+
return Effect.tryPromise({
|
|
22
|
+
try: () => db.select().from(inputTable).where(eq(runIdCol, runId)).limit(1),
|
|
23
|
+
catch: (cause) => toSmithersError(cause, "load input", {
|
|
24
|
+
code: "DB_QUERY_FAILED",
|
|
25
|
+
details: { runId },
|
|
26
|
+
}),
|
|
27
|
+
}).pipe(Effect.map((rows) => rows[0]), Effect.annotateLogs({ runId }), Effect.withLogSpan("db:load-input"));
|
|
28
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { eq, getTableName } from "drizzle-orm";
|
|
2
|
+
import { getTableColumns } from "drizzle-orm/utils";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
4
|
+
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
5
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
6
|
+
/** @typedef {import("@smithers-orchestrator/driver/OutputSnapshot").OutputSnapshot} OutputSnapshot */
|
|
7
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
8
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Table} table
|
|
12
|
+
* @returns {string[]}
|
|
13
|
+
*/
|
|
14
|
+
function getBooleanColumnKeys(table) {
|
|
15
|
+
try {
|
|
16
|
+
const cols = getTableColumns(table);
|
|
17
|
+
const keys = [];
|
|
18
|
+
for (const [key, col] of Object.entries(cols)) {
|
|
19
|
+
const c = /** @type {Record<string, unknown> & { config?: { mode?: string }; mapFromDriverValue?: unknown }} */ (/** @type {unknown} */ (col));
|
|
20
|
+
const mapFn = /** @type {{ toString?: () => string } | undefined} */ (c?.mapFromDriverValue);
|
|
21
|
+
if (c?.columnType === "SQLiteBoolean" || c?.config?.mode === "boolean" || c?.mode === "boolean" || mapFn?.toString?.().includes("Boolean") || (c?.dataType === "boolean")) {
|
|
22
|
+
keys.push(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return keys;
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @param {ReadonlyArray<Record<string, unknown>>} rows
|
|
32
|
+
* @param {readonly string[]} boolKeys
|
|
33
|
+
* @returns {Array<Record<string, unknown>>}
|
|
34
|
+
*/
|
|
35
|
+
function coerceBooleanColumns(rows, boolKeys) {
|
|
36
|
+
if (boolKeys.length === 0) return rows.slice();
|
|
37
|
+
return rows.map((row) => {
|
|
38
|
+
if (!row) return row;
|
|
39
|
+
/** @type {Record<string, unknown>} */
|
|
40
|
+
const patched = { ...row };
|
|
41
|
+
for (const key of boolKeys) {
|
|
42
|
+
if (key in patched && typeof patched[key] !== "boolean") {
|
|
43
|
+
patched[key] = Boolean(patched[key]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return patched;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
51
|
+
* @param {Record<string, Table | unknown>} schema
|
|
52
|
+
* @param {string} runId
|
|
53
|
+
* @returns {Effect.Effect<OutputSnapshot, SmithersError>}
|
|
54
|
+
*/
|
|
55
|
+
export function loadOutputs(db, schema, runId) {
|
|
56
|
+
return Effect.gen(function* () {
|
|
57
|
+
/** @type {Record<string, ReadonlyArray<Record<string, unknown>>>} */
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const [key, table] of Object.entries(schema)) {
|
|
60
|
+
if (!table || typeof table !== "object") continue;
|
|
61
|
+
if (key === "input") continue;
|
|
62
|
+
const colsOpt = yield* Effect.try({
|
|
63
|
+
try: () => getTableColumns(/** @type {Table} */ (table)),
|
|
64
|
+
catch: (cause) => toSmithersError(cause, "get table columns", { code: "DB_QUERY_FAILED", details: { runId, schemaKey: key } }),
|
|
65
|
+
}).pipe(Effect.option);
|
|
66
|
+
if (Option.isNone(colsOpt)) continue;
|
|
67
|
+
const cols = colsOpt.value;
|
|
68
|
+
const runIdCol = cols.runId;
|
|
69
|
+
if (!runIdCol) continue;
|
|
70
|
+
const tableNameOpt = yield* Effect.try({
|
|
71
|
+
try: () => getTableName(/** @type {Table} */ (table)),
|
|
72
|
+
catch: (cause) => toSmithersError(cause, "get table name", { code: "DB_QUERY_FAILED", details: { runId, schemaKey: key } }),
|
|
73
|
+
}).pipe(Effect.option);
|
|
74
|
+
if (Option.isNone(tableNameOpt)) continue;
|
|
75
|
+
const tableName = tableNameOpt.value;
|
|
76
|
+
const rawRows = yield* Effect.tryPromise({
|
|
77
|
+
try: () => db.select().from(/** @type {Table} */ (table)).where(eq(runIdCol, runId)),
|
|
78
|
+
catch: (cause) => toSmithersError(cause, `load outputs ${tableName}`, { code: "DB_QUERY_FAILED", details: { runId, tableName } }),
|
|
79
|
+
});
|
|
80
|
+
const boolKeys = getBooleanColumnKeys(/** @type {Table} */ (table));
|
|
81
|
+
const rows = coerceBooleanColumns(rawRows, boolKeys);
|
|
82
|
+
out[tableName] = rows;
|
|
83
|
+
out[key] = rows;
|
|
84
|
+
}
|
|
85
|
+
return /** @type {OutputSnapshot} */ (/** @type {unknown} */ (out));
|
|
86
|
+
}).pipe(Effect.annotateLogs({ runId }), Effect.withLogSpan("db:load-outputs"));
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type OutputKey = { runId: string; nodeId: string; iteration?: number };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { getKeyColumns } from "./getKeyColumns.js";
|
|
3
|
+
/** @typedef {import("./OutputKey.ts").OutputKey} OutputKey */
|
|
4
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Table} table
|
|
8
|
+
* @param {OutputKey} key
|
|
9
|
+
*/
|
|
10
|
+
export function buildKeyWhere(table, key) {
|
|
11
|
+
const cols = getKeyColumns(table);
|
|
12
|
+
const clauses = [eq(cols.runId, key.runId), eq(cols.nodeId, key.nodeId)];
|
|
13
|
+
if (cols.iteration) {
|
|
14
|
+
clauses.push(eq(cols.iteration, key.iteration ?? 0));
|
|
15
|
+
}
|
|
16
|
+
return and(...clauses);
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getTableColumns } from "drizzle-orm/utils";
|
|
2
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {Table} table
|
|
6
|
+
* @param {string} runId
|
|
7
|
+
* @param {string} nodeId
|
|
8
|
+
* @param {number} iteration
|
|
9
|
+
* @param {unknown} payload
|
|
10
|
+
*/
|
|
11
|
+
export function buildOutputRow(table, runId, nodeId, iteration, payload) {
|
|
12
|
+
const cols = getTableColumns(table);
|
|
13
|
+
const keys = Object.keys(cols);
|
|
14
|
+
const hasPayload = keys.includes("payload");
|
|
15
|
+
const payloadOnly = hasPayload &&
|
|
16
|
+
keys.every((key) => key === "runId" ||
|
|
17
|
+
key === "nodeId" ||
|
|
18
|
+
key === "iteration" ||
|
|
19
|
+
key === "payload");
|
|
20
|
+
if (payloadOnly) {
|
|
21
|
+
return {
|
|
22
|
+
runId,
|
|
23
|
+
nodeId,
|
|
24
|
+
iteration,
|
|
25
|
+
payload: (payload ?? null),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...(payload ?? {}),
|
|
30
|
+
runId,
|
|
31
|
+
nodeId,
|
|
32
|
+
iteration,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getAgentOutputSchema } from "./getAgentOutputSchema.js";
|
|
3
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {Table | z.ZodObject} tableOrSchema
|
|
7
|
+
* @param {z.ZodObject} [zodSchema]
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function describeSchemaShape(tableOrSchema, zodSchema) {
|
|
11
|
+
const schema = zodSchema ?? (isZodSchema(tableOrSchema) ? tableOrSchema : null);
|
|
12
|
+
if (schema && typeof schema.toJSONSchema === "function") {
|
|
13
|
+
const jsonSchema = schema.toJSONSchema();
|
|
14
|
+
return JSON.stringify(jsonSchema, null, 2);
|
|
15
|
+
}
|
|
16
|
+
if (!isZodSchema(tableOrSchema)) {
|
|
17
|
+
const agentSchema = getAgentOutputSchema(tableOrSchema);
|
|
18
|
+
if (typeof agentSchema.toJSONSchema === "function") {
|
|
19
|
+
const jsonSchema = agentSchema.toJSONSchema();
|
|
20
|
+
return JSON.stringify(jsonSchema, null, 2);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const target = schema ?? (isZodSchema(tableOrSchema) ? tableOrSchema : getAgentOutputSchema(tableOrSchema));
|
|
24
|
+
const shape = target.shape;
|
|
25
|
+
/** @type {Record<string, string>} */
|
|
26
|
+
const fields = {};
|
|
27
|
+
for (const [key, zodType] of Object.entries(shape)) {
|
|
28
|
+
fields[key] = describeZodType(/** @type {z.ZodType} */ (zodType));
|
|
29
|
+
}
|
|
30
|
+
return JSON.stringify(fields, null, 2);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @param {unknown} val
|
|
34
|
+
* @returns {val is z.ZodObject}
|
|
35
|
+
*/
|
|
36
|
+
function isZodSchema(val) {
|
|
37
|
+
return (!!val && typeof val === "object" && "shape" in val && typeof (/** @type {{ shape: unknown }} */ (val)).shape === "object");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @param {z.ZodType} schema
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function describeZodType(schema) {
|
|
44
|
+
const internal = /** @type {{ _zod?: { def?: Record<string, unknown> } }} */ (/** @type {unknown} */ (schema));
|
|
45
|
+
if (internal._zod?.def) {
|
|
46
|
+
const def = internal._zod.def;
|
|
47
|
+
const typeName = /** @type {string} */ (def.type);
|
|
48
|
+
if (typeName === "optional" || typeName === "default" || typeName === "nullable") {
|
|
49
|
+
const inner = def.innerType ? describeZodType(/** @type {z.ZodType} */ (def.innerType)) : "unknown";
|
|
50
|
+
if (typeName === "optional") return `${inner} (optional)`;
|
|
51
|
+
if (typeName === "nullable") return `${inner} | null`;
|
|
52
|
+
return inner;
|
|
53
|
+
}
|
|
54
|
+
if (typeName === "string") return "string";
|
|
55
|
+
if (typeName === "number" || typeName === "int" || typeName === "float") return "number";
|
|
56
|
+
if (typeName === "boolean") return "boolean";
|
|
57
|
+
if (typeName === "array") {
|
|
58
|
+
const itemType = def.element ? describeZodType(/** @type {z.ZodType} */ (def.element)) : "unknown";
|
|
59
|
+
return `${itemType}[]`;
|
|
60
|
+
}
|
|
61
|
+
if (typeName === "object") return "object";
|
|
62
|
+
if (typeName === "enum") return `enum(${(/** @type {unknown[]} */ (def.values ?? [])).join(" | ")})`;
|
|
63
|
+
if (typeName === "literal") return `literal(${JSON.stringify(def.value)})`;
|
|
64
|
+
if (typeName === "union") {
|
|
65
|
+
const options = (/** @type {z.ZodType[]} */ (def.options ?? [])).map((o) => describeZodType(o));
|
|
66
|
+
return options.join(" | ");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return "unknown";
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createInsertSchema } from "drizzle-zod";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Zod schema for agent output by removing runId, nodeId, iteration
|
|
5
|
+
* (which are auto-populated by smithers)
|
|
6
|
+
*/
|
|
7
|
+
export function getAgentOutputSchema(table) {
|
|
8
|
+
const baseSchema = createInsertSchema(table);
|
|
9
|
+
// Remove the key columns that smithers populates automatically
|
|
10
|
+
const shape = baseSchema.shape;
|
|
11
|
+
const { runId, nodeId, iteration, ...rest } = shape;
|
|
12
|
+
return z.object(rest);
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getTableColumns } from "drizzle-orm/utils";
|
|
2
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
3
|
+
/** @typedef {import("drizzle-orm").AnyColumn} AnyColumn */
|
|
4
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Table} table
|
|
8
|
+
* @returns {{ runId: AnyColumn; nodeId: AnyColumn; iteration?: AnyColumn; }}
|
|
9
|
+
*/
|
|
10
|
+
export function getKeyColumns(table) {
|
|
11
|
+
const cols = getTableColumns(table);
|
|
12
|
+
const runId = cols.runId;
|
|
13
|
+
const nodeId = cols.nodeId;
|
|
14
|
+
const iteration = cols.iteration;
|
|
15
|
+
if (!runId || !nodeId) {
|
|
16
|
+
throw new SmithersError("DB_MISSING_COLUMNS", `Output table ${table["_"]?.name ?? ""} must include runId and nodeId columns.`);
|
|
17
|
+
}
|
|
18
|
+
return { runId, nodeId, iteration };
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./OutputKey.ts").OutputKey} OutputKey */
|
|
3
|
+
// @smithers-type-exports-end
|
|
4
|
+
|
|
5
|
+
export { buildOutputRow } from "./buildOutputRow.js";
|
|
6
|
+
export { stripAutoColumns } from "./stripAutoColumns.js";
|
|
7
|
+
export { getKeyColumns } from "./getKeyColumns.js";
|
|
8
|
+
export { buildKeyWhere } from "./buildKeyWhere.js";
|
|
9
|
+
export { selectOutputRow } from "./selectOutputRowEffect.js";
|
|
10
|
+
export { upsertOutputRow } from "./upsertOutputRowEffect.js";
|
|
11
|
+
export { validateOutput } from "./validateOutput.js";
|
|
12
|
+
export { validateExistingOutput } from "./validateExistingOutput.js";
|
|
13
|
+
export { getAgentOutputSchema } from "./getAgentOutputSchema.js";
|
|
14
|
+
export { describeSchemaShape } from "./describeSchemaShape.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
3
|
+
import { buildKeyWhere } from "./buildKeyWhere.js";
|
|
4
|
+
/** @typedef {import("./OutputKey.ts").OutputKey} OutputKey */
|
|
5
|
+
/** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
|
|
6
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
7
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @template T
|
|
11
|
+
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
12
|
+
* @param {Table} table
|
|
13
|
+
* @param {OutputKey} key
|
|
14
|
+
* @returns {Effect.Effect<T | undefined, SmithersError>}
|
|
15
|
+
*/
|
|
16
|
+
export function selectOutputRow(db, table, key) {
|
|
17
|
+
const where = buildKeyWhere(table, key);
|
|
18
|
+
return Effect.tryPromise({
|
|
19
|
+
try: () => db.select().from(table).where(where).limit(1),
|
|
20
|
+
catch: (cause) => toSmithersError(cause, `select output ${table["_"]?.name ?? "output"}`, {
|
|
21
|
+
code: "DB_QUERY_FAILED",
|
|
22
|
+
details: { outputTable: table["_"]?.name ?? "output" },
|
|
23
|
+
}),
|
|
24
|
+
}).pipe(Effect.map((rows) => rows[0]), Effect.annotateLogs({
|
|
25
|
+
outputTable: table["_"]?.name ?? "output",
|
|
26
|
+
runId: key.runId,
|
|
27
|
+
nodeId: key.nodeId,
|
|
28
|
+
iteration: key.iteration ?? 0,
|
|
29
|
+
}), Effect.withLogSpan("db:select-output-row"));
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {unknown} payload
|
|
3
|
+
*/
|
|
4
|
+
export function stripAutoColumns(payload) {
|
|
5
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
6
|
+
return payload;
|
|
7
|
+
}
|
|
8
|
+
const { runId: _runId, nodeId: _nodeId, iteration: _iteration, ...rest } = payload;
|
|
9
|
+
return rest;
|
|
10
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
3
|
+
import { withSqliteWriteRetryEffect } from "../write-retry.js";
|
|
4
|
+
import { getKeyColumns } from "./getKeyColumns.js";
|
|
5
|
+
/** @typedef {import("./OutputKey.ts").OutputKey} OutputKey */
|
|
6
|
+
/** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
|
|
7
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
8
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
12
|
+
* @param {Table} table
|
|
13
|
+
* @param {OutputKey} key
|
|
14
|
+
* @param {Record<string, unknown>} payload
|
|
15
|
+
* @returns {Effect.Effect<void, SmithersError>}
|
|
16
|
+
*/
|
|
17
|
+
export function upsertOutputRow(db, table, key, payload) {
|
|
18
|
+
const cols = getKeyColumns(table);
|
|
19
|
+
const values = { ...payload };
|
|
20
|
+
values.runId = key.runId;
|
|
21
|
+
values.nodeId = key.nodeId;
|
|
22
|
+
if (cols.iteration) {
|
|
23
|
+
values.iteration = key.iteration ?? 0;
|
|
24
|
+
}
|
|
25
|
+
const target = cols.iteration ? [cols.runId, cols.nodeId, cols.iteration] : [cols.runId, cols.nodeId];
|
|
26
|
+
return withSqliteWriteRetryEffect(() => Effect.tryPromise({
|
|
27
|
+
try: () => db.insert(table).values(values).onConflictDoUpdate({ target, set: values }),
|
|
28
|
+
catch: (cause) => toSmithersError(cause, `upsert output ${table["_"]?.name ?? "output"}`, {
|
|
29
|
+
code: "DB_WRITE_FAILED",
|
|
30
|
+
details: { outputTable: table["_"]?.name ?? "output" },
|
|
31
|
+
}),
|
|
32
|
+
}), { label: `upsert output ${table["_"]?.name ?? "output"}` }).pipe(Effect.asVoid, Effect.annotateLogs({
|
|
33
|
+
outputTable: table["_"]?.name ?? "output",
|
|
34
|
+
runId: key.runId,
|
|
35
|
+
nodeId: key.nodeId,
|
|
36
|
+
iteration: key.iteration ?? 0,
|
|
37
|
+
}), Effect.withLogSpan("db:upsert-output-row"));
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createSelectSchema } from "drizzle-zod";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {Table} table
|
|
7
|
+
* @param {unknown} payload
|
|
8
|
+
* @returns {{ ok: boolean; data?: any; error?: z.ZodError; }}
|
|
9
|
+
*/
|
|
10
|
+
export function validateExistingOutput(table, payload) {
|
|
11
|
+
const schema = createSelectSchema(table);
|
|
12
|
+
const result = schema.safeParse(payload);
|
|
13
|
+
if (result.success) {
|
|
14
|
+
return { ok: true, data: result.data };
|
|
15
|
+
}
|
|
16
|
+
return { ok: false, error: result.error };
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createInsertSchema } from "drizzle-zod";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** @typedef {import("drizzle-orm").Table} Table */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {Table} table
|
|
7
|
+
* @param {unknown} payload
|
|
8
|
+
* @returns {{ ok: boolean; data?: any; error?: z.ZodError; }}
|
|
9
|
+
*/
|
|
10
|
+
export function validateOutput(table, payload) {
|
|
11
|
+
const schema = createInsertSchema(table);
|
|
12
|
+
const result = schema.safeParse(payload);
|
|
13
|
+
if (result.success) {
|
|
14
|
+
return { ok: true, data: result.data };
|
|
15
|
+
}
|
|
16
|
+
return { ok: false, error: result.error };
|
|
17
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/** @typedef {"string" | "number" | "boolean" | "object" | "array" | "null" | "unknown"} OutputSchemaFieldType */
|
|
2
|
+
/** @typedef {{ name: string; type: OutputSchemaFieldType; optional: boolean; nullable: boolean; description?: string; enum?: readonly unknown[]; }} OutputSchemaFieldDescriptor */
|
|
3
|
+
/** @typedef {{ fields: OutputSchemaFieldDescriptor[]; }} OutputSchemaDescriptor */
|
|
4
|
+
/** @typedef {{ code: "SchemaConversionError"; field: string; construct: string; message: string; }} OutputSchemaDescriptorWarning */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{ shape?: Record<string, unknown> } & Record<string, unknown>} schema
|
|
8
|
+
* @param {{ onWarning?: (warning: OutputSchemaDescriptorWarning) => void }} [options]
|
|
9
|
+
* @returns {OutputSchemaDescriptor}
|
|
10
|
+
*/
|
|
11
|
+
export function buildOutputSchemaDescriptor(schema, options = {}) {
|
|
12
|
+
const shape = schema?.shape;
|
|
13
|
+
if (!shape || typeof shape !== "object" || Array.isArray(shape)) {
|
|
14
|
+
throw new Error("Output schema descriptor requires a Zod object schema.");
|
|
15
|
+
}
|
|
16
|
+
/** @type {OutputSchemaFieldDescriptor[]} */
|
|
17
|
+
const fields = [];
|
|
18
|
+
for (const [name, fieldSchema] of Object.entries(shape)) {
|
|
19
|
+
fields.push(describeField(name, fieldSchema, options));
|
|
20
|
+
}
|
|
21
|
+
return { fields };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} name
|
|
25
|
+
* @param {unknown} schema
|
|
26
|
+
* @param {{ onWarning?: (warning: OutputSchemaDescriptorWarning) => void }} options
|
|
27
|
+
* @returns {OutputSchemaFieldDescriptor}
|
|
28
|
+
*/
|
|
29
|
+
function describeField(name, schema, options) {
|
|
30
|
+
const unwrapped = unwrapSchema(schema);
|
|
31
|
+
const schemaRecord = /** @type {{ description?: unknown } | null | undefined} */ (schema && typeof schema === "object" ? schema : null);
|
|
32
|
+
const innerRecord = /** @type {{ description?: unknown } | null | undefined} */ (unwrapped.schema && typeof unwrapped.schema === "object" ? unwrapped.schema : null);
|
|
33
|
+
const description = asOptionalString(schemaRecord?.description) ?? asOptionalString(innerRecord?.description);
|
|
34
|
+
const base = describeUnwrappedType(name, unwrapped.schema, options);
|
|
35
|
+
/** @type {OutputSchemaFieldDescriptor} */
|
|
36
|
+
const field = {
|
|
37
|
+
name,
|
|
38
|
+
type: base.type,
|
|
39
|
+
optional: unwrapped.optional,
|
|
40
|
+
nullable: unwrapped.nullable,
|
|
41
|
+
};
|
|
42
|
+
if (description) field.description = description;
|
|
43
|
+
if (base.description) field.description = field.description ?? base.description;
|
|
44
|
+
if (base.enumValues) field.enum = base.enumValues;
|
|
45
|
+
return field;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @param {unknown} schema
|
|
49
|
+
* @returns {{ schema: unknown; optional: boolean; nullable: boolean }}
|
|
50
|
+
*/
|
|
51
|
+
function unwrapSchema(schema) {
|
|
52
|
+
/** @type {unknown} */
|
|
53
|
+
let current = schema;
|
|
54
|
+
let optional = false;
|
|
55
|
+
let nullable = false;
|
|
56
|
+
let guard = 0;
|
|
57
|
+
/**
|
|
58
|
+
* @param {unknown} value
|
|
59
|
+
* @returns {Record<string, unknown> | undefined}
|
|
60
|
+
*/
|
|
61
|
+
const getDef = (value) => {
|
|
62
|
+
if (!value || typeof value !== "object") return undefined;
|
|
63
|
+
const inner = /** @type {{ _zod?: { def?: Record<string, unknown> } }} */ (value);
|
|
64
|
+
return inner._zod?.def;
|
|
65
|
+
};
|
|
66
|
+
let def = getDef(current);
|
|
67
|
+
while (def && guard < 32) {
|
|
68
|
+
guard += 1;
|
|
69
|
+
switch (def?.type) {
|
|
70
|
+
case "optional":
|
|
71
|
+
optional = true;
|
|
72
|
+
current = def.innerType;
|
|
73
|
+
break;
|
|
74
|
+
case "default":
|
|
75
|
+
optional = true;
|
|
76
|
+
current = def.innerType;
|
|
77
|
+
break;
|
|
78
|
+
case "nullable":
|
|
79
|
+
nullable = true;
|
|
80
|
+
current = def.innerType;
|
|
81
|
+
break;
|
|
82
|
+
case "readonly":
|
|
83
|
+
case "nonoptional":
|
|
84
|
+
case "catch":
|
|
85
|
+
current = def.innerType;
|
|
86
|
+
break;
|
|
87
|
+
case "pipe":
|
|
88
|
+
current = def.out ?? def.in ?? current;
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
return { schema: current, optional, nullable };
|
|
92
|
+
}
|
|
93
|
+
def = getDef(current);
|
|
94
|
+
}
|
|
95
|
+
return { schema: current, optional, nullable };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} field
|
|
99
|
+
* @param {unknown} schema
|
|
100
|
+
* @param {{ onWarning?: (warning: OutputSchemaDescriptorWarning) => void }} options
|
|
101
|
+
* @returns {{ type: OutputSchemaFieldType; enumValues?: readonly unknown[]; description?: string; }}
|
|
102
|
+
*/
|
|
103
|
+
function describeUnwrappedType(field, schema, options) {
|
|
104
|
+
const def = /** @type {Record<string, unknown> | undefined} */ (schema && typeof schema === "object" ? /** @type {{ _zod?: { def?: Record<string, unknown> } }} */ (schema)._zod?.def : undefined);
|
|
105
|
+
const typeName = asOptionalString(def?.type) ?? "unknown";
|
|
106
|
+
switch (typeName) {
|
|
107
|
+
case "string": return { type: "string" };
|
|
108
|
+
case "number":
|
|
109
|
+
case "int":
|
|
110
|
+
case "float": return { type: "number" };
|
|
111
|
+
case "boolean": return { type: "boolean" };
|
|
112
|
+
case "null": return { type: "null" };
|
|
113
|
+
case "array":
|
|
114
|
+
case "tuple":
|
|
115
|
+
case "set": return { type: "array" };
|
|
116
|
+
case "object": return { type: "object" };
|
|
117
|
+
case "record": return { type: "object", description: "Record value shape is not expanded in v1." };
|
|
118
|
+
case "map": return { type: "object", description: "Map entries are not expanded in v1." };
|
|
119
|
+
case "enum": {
|
|
120
|
+
const entries = def?.entries;
|
|
121
|
+
const enumValues = entries && typeof entries === "object" ? Object.values(entries) : [];
|
|
122
|
+
return { type: "string", ...(enumValues.length > 0 ? { enumValues } : {}) };
|
|
123
|
+
}
|
|
124
|
+
case "literal": {
|
|
125
|
+
const values = Array.isArray(def?.values) ? def.values : [];
|
|
126
|
+
if (values.length === 1 && values[0] === null) return { type: "null" };
|
|
127
|
+
if (values.length === 1 && typeof values[0] === "string") return { type: "string", enumValues: values };
|
|
128
|
+
if (values.length === 1 && typeof values[0] === "number") return { type: "number", enumValues: values };
|
|
129
|
+
if (values.length === 1 && typeof values[0] === "boolean") return { type: "boolean", enumValues: values };
|
|
130
|
+
reportUnsupported(field, typeName, options);
|
|
131
|
+
return { type: "unknown" };
|
|
132
|
+
}
|
|
133
|
+
case "unknown":
|
|
134
|
+
case "any":
|
|
135
|
+
case "void":
|
|
136
|
+
case "undefined":
|
|
137
|
+
case "never": return { type: "unknown" };
|
|
138
|
+
default:
|
|
139
|
+
reportUnsupported(field, typeName, options);
|
|
140
|
+
return { type: "unknown" };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} field
|
|
145
|
+
* @param {string} construct
|
|
146
|
+
* @param {{ onWarning?: (warning: OutputSchemaDescriptorWarning) => void }} options
|
|
147
|
+
* @returns {void}
|
|
148
|
+
*/
|
|
149
|
+
function reportUnsupported(field, construct, options) {
|
|
150
|
+
options.onWarning?.({
|
|
151
|
+
code: "SchemaConversionError",
|
|
152
|
+
field,
|
|
153
|
+
construct,
|
|
154
|
+
message: `Unsupported schema construct "${construct}" in output field "${field}".`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* @param {unknown} value
|
|
159
|
+
* @returns {string | undefined}
|
|
160
|
+
*/
|
|
161
|
+
function asOptionalString(value) {
|
|
162
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
163
|
+
}
|