@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
package/src/output.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { getTableColumns } from "drizzle-orm/utils";
|
|
3
|
+
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
7
|
+
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
8
|
+
import { withSqliteWriteRetryEffect } from "./write-retry.js";
|
|
9
|
+
/** @typedef {import("drizzle-orm").AnyColumn} AnyColumn */
|
|
10
|
+
/** @typedef {import("./output/OutputKey.ts").OutputKey} _OutputKey */
|
|
11
|
+
/** @typedef {import("drizzle-orm").Table} _Table */
|
|
12
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {_Table} table
|
|
16
|
+
* @param {string} runId
|
|
17
|
+
* @param {string} nodeId
|
|
18
|
+
* @param {number} iteration
|
|
19
|
+
* @param {unknown} payload
|
|
20
|
+
* @returns {Record<string, unknown>}
|
|
21
|
+
*/
|
|
22
|
+
export function buildOutputRow(table, runId, nodeId, iteration, payload) {
|
|
23
|
+
const cols = getTableColumns(table);
|
|
24
|
+
const keys = Object.keys(cols);
|
|
25
|
+
const hasPayload = keys.includes("payload");
|
|
26
|
+
const payloadOnly = hasPayload && keys.every((key) => key === "runId" || key === "nodeId" || key === "iteration" || key === "payload");
|
|
27
|
+
if (payloadOnly) {
|
|
28
|
+
return { runId, nodeId, iteration, payload: (payload ?? null) };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...(/** @type {Record<string, unknown>} */ (payload ?? {})),
|
|
32
|
+
runId, nodeId, iteration,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @param {unknown} payload
|
|
37
|
+
* @returns {unknown}
|
|
38
|
+
*/
|
|
39
|
+
export function stripAutoColumns(payload) {
|
|
40
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
41
|
+
return payload;
|
|
42
|
+
}
|
|
43
|
+
const { runId: _runId, nodeId: _nodeId, iteration: _iteration, ...rest } = /** @type {Record<string, unknown>} */ (payload);
|
|
44
|
+
return rest;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @param {_Table} table
|
|
48
|
+
* @returns {{ runId: AnyColumn; nodeId: AnyColumn; iteration?: AnyColumn; }}
|
|
49
|
+
*/
|
|
50
|
+
export function getKeyColumns(table) {
|
|
51
|
+
const cols = getTableColumns(table);
|
|
52
|
+
const runId = cols.runId;
|
|
53
|
+
const nodeId = cols.nodeId;
|
|
54
|
+
const iteration = cols.iteration;
|
|
55
|
+
if (!runId || !nodeId) {
|
|
56
|
+
throw new SmithersError("DB_MISSING_COLUMNS", `Output table ${table["_"]?.name ?? ""} must include runId and nodeId columns.`);
|
|
57
|
+
}
|
|
58
|
+
return { runId, nodeId, iteration };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @param {_Table} table
|
|
62
|
+
* @param {_OutputKey} key
|
|
63
|
+
* @returns {ReturnType<typeof and>}
|
|
64
|
+
*/
|
|
65
|
+
export function buildKeyWhere(table, key) {
|
|
66
|
+
const cols = getKeyColumns(table);
|
|
67
|
+
const clauses = [eq(cols.runId, key.runId), eq(cols.nodeId, key.nodeId)];
|
|
68
|
+
if (cols.iteration) clauses.push(eq(cols.iteration, key.iteration ?? 0));
|
|
69
|
+
return and(...clauses);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* @template T
|
|
73
|
+
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
74
|
+
* @param {_Table} table
|
|
75
|
+
* @param {_OutputKey} key
|
|
76
|
+
* @returns {Effect.Effect<T | undefined, SmithersError>}
|
|
77
|
+
*/
|
|
78
|
+
export function selectOutputRowEffect(db, table, key) {
|
|
79
|
+
const where = buildKeyWhere(table, key);
|
|
80
|
+
return Effect.tryPromise({
|
|
81
|
+
try: () => db.select().from(table).where(where).limit(1),
|
|
82
|
+
catch: (cause) => toSmithersError(cause, `select output ${table["_"]?.name ?? "output"}`, {
|
|
83
|
+
code: "DB_QUERY_FAILED",
|
|
84
|
+
details: { outputTable: table["_"]?.name ?? "output" },
|
|
85
|
+
}),
|
|
86
|
+
}).pipe(Effect.map((rows) => rows[0]), Effect.annotateLogs({
|
|
87
|
+
outputTable: table["_"]?.name ?? "output",
|
|
88
|
+
runId: key.runId,
|
|
89
|
+
nodeId: key.nodeId,
|
|
90
|
+
iteration: key.iteration ?? 0,
|
|
91
|
+
}), Effect.withLogSpan("db:select-output-row"));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* @template T
|
|
95
|
+
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
96
|
+
* @param {_Table} table
|
|
97
|
+
* @param {_OutputKey} key
|
|
98
|
+
* @returns {Promise<T | undefined>}
|
|
99
|
+
*/
|
|
100
|
+
export function selectOutputRow(db, table, key) {
|
|
101
|
+
return Effect.runPromise(selectOutputRowEffect(db, table, key));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
105
|
+
* @param {_Table} table
|
|
106
|
+
* @param {_OutputKey} key
|
|
107
|
+
* @param {Record<string, unknown>} payload
|
|
108
|
+
* @returns {Effect.Effect<void, SmithersError>}
|
|
109
|
+
*/
|
|
110
|
+
export function upsertOutputRowEffect(db, table, key, payload) {
|
|
111
|
+
const cols = getKeyColumns(table);
|
|
112
|
+
/** @type {Record<string, unknown>} */
|
|
113
|
+
const values = { ...payload };
|
|
114
|
+
values.runId = key.runId;
|
|
115
|
+
values.nodeId = key.nodeId;
|
|
116
|
+
if (cols.iteration) values.iteration = key.iteration ?? 0;
|
|
117
|
+
const target = cols.iteration ? [cols.runId, cols.nodeId, cols.iteration] : [cols.runId, cols.nodeId];
|
|
118
|
+
return withSqliteWriteRetryEffect(() => Effect.tryPromise({
|
|
119
|
+
try: () => db.insert(table).values(values).onConflictDoUpdate({ target, set: values }),
|
|
120
|
+
catch: (cause) => toSmithersError(cause, `upsert output ${table["_"]?.name ?? "output"}`, {
|
|
121
|
+
code: "DB_WRITE_FAILED",
|
|
122
|
+
details: { outputTable: table["_"]?.name ?? "output" },
|
|
123
|
+
}),
|
|
124
|
+
}), { label: `upsert output ${table["_"]?.name ?? "output"}` }).pipe(Effect.asVoid, Effect.annotateLogs({
|
|
125
|
+
outputTable: table["_"]?.name ?? "output",
|
|
126
|
+
runId: key.runId,
|
|
127
|
+
nodeId: key.nodeId,
|
|
128
|
+
iteration: key.iteration ?? 0,
|
|
129
|
+
}), Effect.withLogSpan("db:upsert-output-row"));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
133
|
+
* @param {_Table} table
|
|
134
|
+
* @param {_OutputKey} key
|
|
135
|
+
* @param {Record<string, unknown>} payload
|
|
136
|
+
* @returns {Promise<void>}
|
|
137
|
+
*/
|
|
138
|
+
export function upsertOutputRow(db, table, key, payload) {
|
|
139
|
+
return Effect.runPromise(upsertOutputRowEffect(db, table, key, payload));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* @param {_Table} table
|
|
143
|
+
* @param {unknown} payload
|
|
144
|
+
* @returns {{ ok: boolean; data?: unknown; error?: z.ZodError; }}
|
|
145
|
+
*/
|
|
146
|
+
export function validateOutput(table, payload) {
|
|
147
|
+
const schema = createInsertSchema(table);
|
|
148
|
+
const result = schema.safeParse(payload);
|
|
149
|
+
if (result.success) return { ok: true, data: result.data };
|
|
150
|
+
return { ok: false, error: result.error };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* @param {_Table} table
|
|
154
|
+
* @param {unknown} payload
|
|
155
|
+
* @returns {{ ok: boolean; data?: unknown; error?: z.ZodError; }}
|
|
156
|
+
*/
|
|
157
|
+
export function validateExistingOutput(table, payload) {
|
|
158
|
+
const schema = createSelectSchema(table);
|
|
159
|
+
const result = schema.safeParse(payload);
|
|
160
|
+
if (result.success) return { ok: true, data: result.data };
|
|
161
|
+
return { ok: false, error: result.error };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* @param {_Table} table
|
|
165
|
+
* @returns {z.ZodObject}
|
|
166
|
+
*/
|
|
167
|
+
export function getAgentOutputSchema(table) {
|
|
168
|
+
const baseSchema = createInsertSchema(table);
|
|
169
|
+
const rest = { ...baseSchema.shape };
|
|
170
|
+
delete rest.runId;
|
|
171
|
+
delete rest.nodeId;
|
|
172
|
+
delete rest.iteration;
|
|
173
|
+
return z.object(rest);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* @param {_Table | z.ZodObject} tableOrSchema
|
|
177
|
+
* @param {z.ZodObject} [zodSchema]
|
|
178
|
+
* @returns {string}
|
|
179
|
+
*/
|
|
180
|
+
export function describeSchemaShape(tableOrSchema, zodSchema) {
|
|
181
|
+
const schema = zodSchema ?? (isZodSchema(tableOrSchema) ? tableOrSchema : null);
|
|
182
|
+
if (schema && typeof schema.toJSONSchema === "function") {
|
|
183
|
+
const jsonSchema = schema.toJSONSchema();
|
|
184
|
+
return JSON.stringify(jsonSchema, null, 2);
|
|
185
|
+
}
|
|
186
|
+
if (!isZodSchema(tableOrSchema)) {
|
|
187
|
+
const agentSchema = getAgentOutputSchema(tableOrSchema);
|
|
188
|
+
if (typeof agentSchema.toJSONSchema === "function") {
|
|
189
|
+
const jsonSchema = agentSchema.toJSONSchema();
|
|
190
|
+
return JSON.stringify(jsonSchema, null, 2);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const target = schema ?? (isZodSchema(tableOrSchema) ? tableOrSchema : getAgentOutputSchema(tableOrSchema));
|
|
194
|
+
const shape = target.shape;
|
|
195
|
+
/** @type {Record<string, string>} */
|
|
196
|
+
const fields = {};
|
|
197
|
+
for (const [key, zodType] of Object.entries(shape)) {
|
|
198
|
+
fields[key] = describeZodType(/** @type {z.ZodType} */ (zodType));
|
|
199
|
+
}
|
|
200
|
+
return JSON.stringify(fields, null, 2);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* @param {unknown} val
|
|
204
|
+
* @returns {val is z.ZodObject}
|
|
205
|
+
*/
|
|
206
|
+
function isZodSchema(val) {
|
|
207
|
+
return (!!val && typeof val === "object" && "shape" in val && typeof (/** @type {{ shape: unknown }} */ (val)).shape === "object");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* @param {z.ZodType} schema
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
213
|
+
function describeZodType(schema) {
|
|
214
|
+
const internal = /** @type {{ _zod?: { def?: Record<string, unknown> } }} */ (/** @type {unknown} */ (schema));
|
|
215
|
+
if (internal._zod?.def) {
|
|
216
|
+
const def = internal._zod.def;
|
|
217
|
+
const typeName = /** @type {string} */ (def.type);
|
|
218
|
+
if (typeName === "optional" || typeName === "default" || typeName === "nullable") {
|
|
219
|
+
const inner = def.innerType ? describeZodType(/** @type {z.ZodType} */ (def.innerType)) : "unknown";
|
|
220
|
+
if (typeName === "optional") return `${inner} (optional)`;
|
|
221
|
+
if (typeName === "nullable") return `${inner} | null`;
|
|
222
|
+
return inner;
|
|
223
|
+
}
|
|
224
|
+
if (typeName === "string") return "string";
|
|
225
|
+
if (typeName === "number" || typeName === "int" || typeName === "float") return "number";
|
|
226
|
+
if (typeName === "boolean") return "boolean";
|
|
227
|
+
if (typeName === "array") {
|
|
228
|
+
const itemType = def.element ? describeZodType(/** @type {z.ZodType} */ (def.element)) : "unknown";
|
|
229
|
+
return `${itemType}[]`;
|
|
230
|
+
}
|
|
231
|
+
if (typeName === "object") return "object";
|
|
232
|
+
if (typeName === "enum") return `enum(${(/** @type {unknown[]} */ (def.values ?? [])).join(" | ")})`;
|
|
233
|
+
if (typeName === "literal") return `literal(${JSON.stringify(def.value)})`;
|
|
234
|
+
if (typeName === "union") {
|
|
235
|
+
const options = (/** @type {z.ZodType[]} */ (def.options ?? [])).map((o) => describeZodType(o));
|
|
236
|
+
return options.join(" | ");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return "unknown";
|
|
240
|
+
}
|
|
@@ -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,10 @@
|
|
|
1
|
+
import type { RunRow } from "../adapter/RunRow.ts";
|
|
2
|
+
|
|
3
|
+
export type DeriveRunStateInput = {
|
|
4
|
+
run: RunRow;
|
|
5
|
+
pendingApproval?: { nodeId: string; requestedAtMs: number } | null;
|
|
6
|
+
pendingTimer?: { nodeId: string; firesAtMs: number } | null;
|
|
7
|
+
pendingEvent?: { nodeId: string; correlationKey: string } | null;
|
|
8
|
+
now?: number;
|
|
9
|
+
staleThresholdMs?: number;
|
|
10
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RUN_STATE_HEARTBEAT_STALE_MS = 30_000;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ReasonBlocked =
|
|
2
|
+
| { kind: "approval"; nodeId: string; requestedAt: string }
|
|
3
|
+
| { kind: "event"; nodeId: string; correlationKey: string }
|
|
4
|
+
| { kind: "timer"; nodeId: string; wakeAt: string }
|
|
5
|
+
| {
|
|
6
|
+
kind: "provider";
|
|
7
|
+
nodeId: string;
|
|
8
|
+
code: "rate-limit" | "auth" | "timeout";
|
|
9
|
+
}
|
|
10
|
+
| { kind: "tool"; nodeId: string; toolName: string; code: string };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ReasonUnhealthy =
|
|
2
|
+
| { kind: "engine-heartbeat-stale"; lastHeartbeatAt: string }
|
|
3
|
+
| { kind: "ui-heartbeat-stale"; lastSeenAt: string }
|
|
4
|
+
| { kind: "db-lock" }
|
|
5
|
+
| { kind: "sandbox-unreachable" }
|
|
6
|
+
| { kind: "supervisor-backoff"; attempt: number; nextAt: string };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReasonBlocked } from "./ReasonBlocked.ts";
|
|
2
|
+
import type { ReasonUnhealthy } from "./ReasonUnhealthy.ts";
|
|
3
|
+
import type { RunState } from "./RunState.ts";
|
|
4
|
+
|
|
5
|
+
export type RunStateView = {
|
|
6
|
+
runId: string;
|
|
7
|
+
state: RunState;
|
|
8
|
+
blocked?: ReasonBlocked;
|
|
9
|
+
unhealthy?: ReasonUnhealthy;
|
|
10
|
+
computedAt: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
import { computeRunStateFromRow } from "./computeRunStateFromRow.js";
|
|
3
|
+
|
|
4
|
+
/** @typedef {import("../adapter/SmithersDb.js").SmithersDb} SmithersDb */
|
|
5
|
+
/** @typedef {import("./RunStateView.ts").RunStateView} RunStateView */
|
|
6
|
+
/** @typedef {import("./ComputeRunStateOptions.ts").ComputeRunStateOptions} ComputeRunStateOptions */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {SmithersDb} adapter
|
|
10
|
+
* @param {string} runId
|
|
11
|
+
* @param {ComputeRunStateOptions} [options]
|
|
12
|
+
* @returns {Promise<RunStateView>}
|
|
13
|
+
*/
|
|
14
|
+
export async function computeRunState(adapter, runId, options = {}) {
|
|
15
|
+
const run = await adapter.getRun(runId);
|
|
16
|
+
if (!run) {
|
|
17
|
+
throw new SmithersError("RUN_NOT_FOUND", `Run not found: ${runId}`, {
|
|
18
|
+
runId,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return computeRunStateFromRow(adapter, run, options);
|
|
22
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { deriveRunState } from "./deriveRunState.js";
|
|
2
|
+
import { parseEventMeta } from "./parseEventMeta.js";
|
|
3
|
+
import { parseTimerMeta } from "./parseTimerMeta.js";
|
|
4
|
+
|
|
5
|
+
/** @typedef {import("../adapter/RunRow.ts").RunRow} RunRow */
|
|
6
|
+
/** @typedef {import("../adapter/SmithersDb.js").SmithersDb} SmithersDb */
|
|
7
|
+
/** @typedef {import("./RunStateView.ts").RunStateView} RunStateView */
|
|
8
|
+
/** @typedef {import("./ComputeRunStateOptions.ts").ComputeRunStateOptions} ComputeRunStateOptions */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {SmithersDb} adapter
|
|
12
|
+
* @param {RunRow} run
|
|
13
|
+
* @param {ComputeRunStateOptions} [options]
|
|
14
|
+
* @returns {Promise<RunStateView>}
|
|
15
|
+
*/
|
|
16
|
+
export async function computeRunStateFromRow(adapter, run, options = {}) {
|
|
17
|
+
let pendingApproval = null;
|
|
18
|
+
let pendingTimer = null;
|
|
19
|
+
let pendingEvent = null;
|
|
20
|
+
|
|
21
|
+
if (run.status === "waiting-approval") {
|
|
22
|
+
pendingApproval = await loadPendingApproval(adapter, run.runId);
|
|
23
|
+
} else if (run.status === "waiting-timer") {
|
|
24
|
+
pendingTimer = await loadPendingTimer(adapter, run.runId);
|
|
25
|
+
} else if (run.status === "waiting-event") {
|
|
26
|
+
pendingEvent = await loadPendingEvent(adapter, run.runId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return deriveRunState({
|
|
30
|
+
run,
|
|
31
|
+
pendingApproval,
|
|
32
|
+
pendingTimer,
|
|
33
|
+
pendingEvent,
|
|
34
|
+
now: options.now,
|
|
35
|
+
staleThresholdMs: options.staleThresholdMs,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {SmithersDb} adapter
|
|
41
|
+
* @param {string} runId
|
|
42
|
+
*/
|
|
43
|
+
async function loadPendingApproval(adapter, runId) {
|
|
44
|
+
const approvals = await adapter.listPendingApprovals(runId);
|
|
45
|
+
let earliest = null;
|
|
46
|
+
for (const a of approvals) {
|
|
47
|
+
if (typeof a.requestedAtMs !== "number") continue;
|
|
48
|
+
if (earliest == null || a.requestedAtMs < earliest.requestedAtMs) {
|
|
49
|
+
earliest = { nodeId: a.nodeId, requestedAtMs: a.requestedAtMs };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return earliest;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {SmithersDb} adapter
|
|
57
|
+
* @param {string} runId
|
|
58
|
+
*/
|
|
59
|
+
async function loadPendingTimer(adapter, runId) {
|
|
60
|
+
const nodes = await adapter.listNodes(runId);
|
|
61
|
+
let earliest = null;
|
|
62
|
+
for (const node of nodes) {
|
|
63
|
+
if (node.state !== "waiting-timer") continue;
|
|
64
|
+
const attempts = await adapter.listAttempts(
|
|
65
|
+
runId,
|
|
66
|
+
node.nodeId,
|
|
67
|
+
node.iteration ?? 0,
|
|
68
|
+
);
|
|
69
|
+
const waiting =
|
|
70
|
+
attempts.find((a) => a.state === "waiting-timer") ?? attempts[0];
|
|
71
|
+
const parsed = parseTimerMeta(waiting?.metaJson);
|
|
72
|
+
if (parsed == null) continue;
|
|
73
|
+
if (earliest == null || parsed.firesAtMs < earliest.firesAtMs) {
|
|
74
|
+
earliest = { nodeId: node.nodeId, firesAtMs: parsed.firesAtMs };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return earliest;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {SmithersDb} adapter
|
|
82
|
+
* @param {string} runId
|
|
83
|
+
*/
|
|
84
|
+
async function loadPendingEvent(adapter, runId) {
|
|
85
|
+
const nodes = await adapter.listNodes(runId);
|
|
86
|
+
for (const node of nodes) {
|
|
87
|
+
if (node.state !== "waiting-event") continue;
|
|
88
|
+
const attempts = await adapter.listAttempts(
|
|
89
|
+
runId,
|
|
90
|
+
node.nodeId,
|
|
91
|
+
node.iteration ?? 0,
|
|
92
|
+
);
|
|
93
|
+
const waiting =
|
|
94
|
+
attempts.find((a) => a.state === "waiting-event") ?? attempts[0];
|
|
95
|
+
const parsed = parseEventMeta(waiting?.metaJson);
|
|
96
|
+
return {
|
|
97
|
+
nodeId: node.nodeId,
|
|
98
|
+
correlationKey: parsed?.correlationKey ?? "",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { RUN_STATE_HEARTBEAT_STALE_MS } from "./RUN_STATE_HEARTBEAT_STALE_MS.js";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./DeriveRunStateInput.ts").DeriveRunStateInput} DeriveRunStateInput */
|
|
4
|
+
/** @typedef {import("./RunStateView.ts").RunStateView} RunStateView */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {DeriveRunStateInput} input
|
|
8
|
+
* @returns {RunStateView}
|
|
9
|
+
*/
|
|
10
|
+
export function deriveRunState(input) {
|
|
11
|
+
const {
|
|
12
|
+
run,
|
|
13
|
+
pendingApproval = null,
|
|
14
|
+
pendingTimer = null,
|
|
15
|
+
pendingEvent = null,
|
|
16
|
+
now = Date.now(),
|
|
17
|
+
staleThresholdMs = RUN_STATE_HEARTBEAT_STALE_MS,
|
|
18
|
+
} = input;
|
|
19
|
+
|
|
20
|
+
const computedAt = new Date(now).toISOString();
|
|
21
|
+
const base = { runId: run.runId, computedAt };
|
|
22
|
+
|
|
23
|
+
switch (run.status) {
|
|
24
|
+
case "finished":
|
|
25
|
+
case "continued":
|
|
26
|
+
return { ...base, state: "succeeded" };
|
|
27
|
+
case "failed":
|
|
28
|
+
return { ...base, state: "failed" };
|
|
29
|
+
case "cancelled":
|
|
30
|
+
return { ...base, state: "cancelled" };
|
|
31
|
+
case "waiting-approval":
|
|
32
|
+
return pendingApproval
|
|
33
|
+
? {
|
|
34
|
+
...base,
|
|
35
|
+
state: "waiting-approval",
|
|
36
|
+
blocked: {
|
|
37
|
+
kind: "approval",
|
|
38
|
+
nodeId: pendingApproval.nodeId,
|
|
39
|
+
requestedAt: new Date(
|
|
40
|
+
pendingApproval.requestedAtMs,
|
|
41
|
+
).toISOString(),
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
: { ...base, state: "waiting-approval" };
|
|
45
|
+
case "waiting-timer":
|
|
46
|
+
return pendingTimer
|
|
47
|
+
? {
|
|
48
|
+
...base,
|
|
49
|
+
state: "waiting-timer",
|
|
50
|
+
blocked: {
|
|
51
|
+
kind: "timer",
|
|
52
|
+
nodeId: pendingTimer.nodeId,
|
|
53
|
+
wakeAt: new Date(pendingTimer.firesAtMs).toISOString(),
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
: { ...base, state: "waiting-timer" };
|
|
57
|
+
case "waiting-event":
|
|
58
|
+
return pendingEvent
|
|
59
|
+
? {
|
|
60
|
+
...base,
|
|
61
|
+
state: "waiting-event",
|
|
62
|
+
blocked: {
|
|
63
|
+
kind: "event",
|
|
64
|
+
nodeId: pendingEvent.nodeId,
|
|
65
|
+
correlationKey: pendingEvent.correlationKey,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
: { ...base, state: "waiting-event" };
|
|
69
|
+
case "running":
|
|
70
|
+
return classifyRunning(run, now, staleThresholdMs, base);
|
|
71
|
+
default:
|
|
72
|
+
return { ...base, state: "unknown" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {import("../adapter/RunRow.ts").RunRow} run
|
|
78
|
+
* @param {number} now
|
|
79
|
+
* @param {number} staleThresholdMs
|
|
80
|
+
* @param {{ runId: string; computedAt: string }} base
|
|
81
|
+
* @returns {RunStateView}
|
|
82
|
+
*/
|
|
83
|
+
function classifyRunning(run, now, staleThresholdMs, base) {
|
|
84
|
+
const heartbeat =
|
|
85
|
+
typeof run.heartbeatAtMs === "number" ? run.heartbeatAtMs : null;
|
|
86
|
+
const startedAt =
|
|
87
|
+
typeof run.startedAtMs === "number" ? run.startedAtMs : null;
|
|
88
|
+
// Fall back to startedAt so a brand-new run with no heartbeat yet
|
|
89
|
+
// isn't reported as stale.
|
|
90
|
+
const lastAlive = Math.max(heartbeat ?? 0, startedAt ?? 0);
|
|
91
|
+
|
|
92
|
+
if (lastAlive === 0) {
|
|
93
|
+
return { ...base, state: "unknown" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (now - lastAlive <= staleThresholdMs) {
|
|
97
|
+
return { ...base, state: "running" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const lastHeartbeatAt = new Date(lastAlive).toISOString();
|
|
101
|
+
// Without a registered owner, supervisor has nothing to take over.
|
|
102
|
+
const orphaned =
|
|
103
|
+
run.runtimeOwnerId == null || run.runtimeOwnerId.length === 0;
|
|
104
|
+
return {
|
|
105
|
+
...base,
|
|
106
|
+
state: orphaned ? "orphaned" : "stale",
|
|
107
|
+
unhealthy: { kind: "engine-heartbeat-stale", lastHeartbeatAt },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string | null | undefined} metaJson
|
|
3
|
+
* @returns {{ correlationKey: string } | null}
|
|
4
|
+
*/
|
|
5
|
+
export function parseEventMeta(metaJson) {
|
|
6
|
+
if (!metaJson) return null;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(metaJson);
|
|
9
|
+
const key =
|
|
10
|
+
parsed?.event?.correlationKey ??
|
|
11
|
+
parsed?.correlationKey ??
|
|
12
|
+
parsed?.event?.eventName ??
|
|
13
|
+
null;
|
|
14
|
+
return typeof key === "string" ? { correlationKey: key } : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string | null | undefined} metaJson
|
|
3
|
+
* @returns {{ firesAtMs: number } | null}
|
|
4
|
+
*/
|
|
5
|
+
export function parseTimerMeta(metaJson) {
|
|
6
|
+
if (!metaJson) return null;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(metaJson);
|
|
9
|
+
const candidate = Number(parsed?.timer?.firesAtMs);
|
|
10
|
+
return Number.isFinite(candidate)
|
|
11
|
+
? { firesAtMs: Math.floor(candidate) }
|
|
12
|
+
: null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type { RunState } from "./runState/RunState.ts";
|
|
2
|
+
export type { RunStateView } from "./runState/RunStateView.ts";
|
|
3
|
+
|
|
4
|
+
import type { DeriveRunStateInput } from "./runState/DeriveRunStateInput.ts";
|
|
5
|
+
import type { RunStateView } from "./runState/RunStateView.ts";
|
|
6
|
+
|
|
7
|
+
export type ComputeRunStateOptions = {
|
|
8
|
+
now?: number;
|
|
9
|
+
staleThresholdMs?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export declare const RUN_STATE_HEARTBEAT_STALE_MS: number;
|
|
13
|
+
export declare function deriveRunState(input: DeriveRunStateInput): RunStateView;
|
|
14
|
+
export declare function computeRunState(
|
|
15
|
+
adapter: unknown,
|
|
16
|
+
runId: string,
|
|
17
|
+
options?: ComputeRunStateOptions,
|
|
18
|
+
): Promise<RunStateView>;
|
|
19
|
+
export declare function computeRunStateFromRow(
|
|
20
|
+
adapter: unknown,
|
|
21
|
+
run: unknown,
|
|
22
|
+
options?: ComputeRunStateOptions,
|
|
23
|
+
): Promise<RunStateView>;
|
package/src/runState.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** @typedef {import("./runState/RunState.ts").RunState} RunState */
|
|
2
|
+
/** @typedef {import("./runState/RunStateView.ts").RunStateView} RunStateView */
|
|
3
|
+
|
|
4
|
+
export { computeRunState } from "./runState/computeRunState.js";
|
|
5
|
+
export { computeRunStateFromRow } from "./runState/computeRunStateFromRow.js";
|
|
6
|
+
export { deriveRunState } from "./runState/deriveRunState.js";
|
|
7
|
+
export { RUN_STATE_HEARTBEAT_STALE_MS } from "./runState/RUN_STATE_HEARTBEAT_STALE_MS.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getTableName } from "drizzle-orm";
|
|
2
|
+
import { getTableColumns } from "drizzle-orm/utils";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
/** @typedef {import("drizzle-orm").Table} _Table */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {_Table} table
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function schemaSignature(table) {
|
|
11
|
+
const cols = getTableColumns(table);
|
|
12
|
+
const keys = Object.keys(cols).sort();
|
|
13
|
+
const parts = [getTableName(table)];
|
|
14
|
+
for (const key of keys) {
|
|
15
|
+
const col = cols[key];
|
|
16
|
+
const type = col?.columnType ?? col?.dataType ?? col?.getSQLType?.() ?? "unknown";
|
|
17
|
+
const notNull = col?.notNull ? "1" : "0";
|
|
18
|
+
const primary = col?.primary ? "1" : "0";
|
|
19
|
+
parts.push(`${key}:${type}:${notNull}:${primary}`);
|
|
20
|
+
}
|
|
21
|
+
return createHash("sha256").update(parts.join("|")).digest("hex");
|
|
22
|
+
}
|