@smithers-orchestrator/engine 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 +50 -0
- package/src/AlertHumanRequestOptions.ts +8 -0
- package/src/AlertRuntimeServices.ts +10 -0
- package/src/ChildWorkflowDefinition.ts +5 -0
- package/src/ChildWorkflowExecuteOptions.ts +14 -0
- package/src/ContinuationRequest.ts +3 -0
- package/src/HijackState.ts +19 -0
- package/src/HumanRequestKind.ts +1 -0
- package/src/HumanRequestStatus.ts +1 -0
- package/src/PlanNode.ts +29 -0
- package/src/RalphMeta.ts +7 -0
- package/src/RalphState.ts +4 -0
- package/src/RalphStateMap.ts +3 -0
- package/src/ScheduleResult.ts +15 -0
- package/src/SignalRunOptions.ts +5 -0
- package/src/alert-runtime.js +22 -0
- package/src/approvals.js +220 -0
- package/src/child-workflow.js +163 -0
- package/src/effect/ApprovalDeferredResolution.ts +13 -0
- package/src/effect/ApprovalDurableDeferredResolution.ts +11 -0
- package/src/effect/ApprovalPayload.ts +7 -0
- package/src/effect/ApprovalResult.ts +6 -0
- package/src/effect/BuilderNode.ts +52 -0
- package/src/effect/BuilderStepHandle.ts +47 -0
- package/src/effect/CancelPayload.ts +3 -0
- package/src/effect/CancelResult.ts +4 -0
- package/src/effect/DeferredResolution.ts +7 -0
- package/src/effect/DiffBundle.ts +7 -0
- package/src/effect/ExecuteTaskActivityOptions.ts +7 -0
- package/src/effect/FilePatch.ts +6 -0
- package/src/effect/GetRunPayload.ts +3 -0
- package/src/effect/GetRunResult.ts +3 -0
- package/src/effect/LegacyExecuteTaskFn.ts +24 -0
- package/src/effect/ListRunsPayload.ts +6 -0
- package/src/effect/RunStatusSchema.ts +9 -0
- package/src/effect/RunSummary.ts +23 -0
- package/src/effect/SignalPayload.ts +7 -0
- package/src/effect/SignalResult.ts +6 -0
- package/src/effect/SmithersSqliteOptions.ts +3 -0
- package/src/effect/SqlMessageStorageEventHistoryQuery.ts +7 -0
- package/src/effect/TaggedWorkerError.ts +46 -0
- package/src/effect/TaskActivityContext.ts +4 -0
- package/src/effect/TaskActivityRetryOptions.ts +4 -0
- package/src/effect/TaskBridgeToolConfig.ts +6 -0
- package/src/effect/TaskFailure.ts +3 -0
- package/src/effect/TaskResult.ts +5 -0
- package/src/effect/UnknownWorkerError.ts +5 -0
- package/src/effect/WaitForEventDurableDeferredResolution.ts +11 -0
- package/src/effect/WorkerDispatchKind.ts +1 -0
- package/src/effect/WorkerTask.ts +14 -0
- package/src/effect/WorkerTaskError.ts +4 -0
- package/src/effect/WorkerTaskKind.ts +1 -0
- package/src/effect/WorkflowPatchDecisionRecord.ts +4 -0
- package/src/effect/WorkflowPatchDecisions.ts +1 -0
- package/src/effect/WorkflowVersioningRuntime.ts +7 -0
- package/src/effect/activity-bridge.js +131 -0
- package/src/effect/bridge-utils.js +45 -0
- package/src/effect/builder.js +837 -0
- package/src/effect/compute-task-bridge.js +734 -0
- package/src/effect/deferred-bridge.js +63 -0
- package/src/effect/deferred-state-bridge.js +1343 -0
- package/src/effect/diff-bundle.js +352 -0
- package/src/effect/durable-deferred-bridge.js +282 -0
- package/src/effect/entity-worker.js +154 -0
- package/src/effect/http-runner.js +86 -0
- package/src/effect/rpc-schema.js +101 -0
- package/src/effect/single-runner.js +189 -0
- package/src/effect/sql-message-storage.js +817 -0
- package/src/effect/static-task-bridge.js +308 -0
- package/src/effect/versioning.js +123 -0
- package/src/effect/workflow-bridge.js +260 -0
- package/src/effect/workflow-make-bridge.js +233 -0
- package/src/engine.js +6933 -0
- package/src/events.js +237 -0
- package/src/external/json-schema-to-zod.js +214 -0
- package/src/getDefinedToolMetadata.js +10 -0
- package/src/hot/HotReloadEvent.ts +21 -0
- package/src/hot/HotWorkflowController.js +220 -0
- package/src/hot/OverlayOptions.ts +4 -0
- package/src/hot/WatchTreeOptions.ts +6 -0
- package/src/hot/index.js +9 -0
- package/src/hot/overlay.js +177 -0
- package/src/hot/watch.js +174 -0
- package/src/human-requests.js +120 -0
- package/src/index.d.ts +1597 -0
- package/src/index.js +41 -0
- package/src/runtime-owner.js +36 -0
- package/src/scheduler.js +31 -0
- package/src/signals.js +82 -0
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
3
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
4
|
+
import { Context, Duration, Effect, Exit, Layer, Schedule, Schema, } from "effect";
|
|
5
|
+
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
8
|
+
import { runWorkflow } from "../engine.js";
|
|
9
|
+
import { ignoreSyncError } from "@smithers-orchestrator/driver/interop";
|
|
10
|
+
import { requireTaskRuntime } from "@smithers-orchestrator/driver/task-runtime";
|
|
11
|
+
import { Branch, Loop, Parallel, Sequence, Task, Worktree, Workflow, } from "@smithers-orchestrator/components/components/index";
|
|
12
|
+
import { camelToSnake } from "@smithers-orchestrator/db/utils/camelToSnake";
|
|
13
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {import("effect").Schema.Schema<unknown, unknown, never>} AnySchema
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {{ needs?: Record<string, BuilderStepHandle>; request: (ctx: Record<string, unknown>) => { title: string; summary?: string | null; }; onDeny?: "fail" | "continue" | "skip"; }} ApprovalOptions
|
|
19
|
+
*/
|
|
20
|
+
/** @typedef {import("./BuilderNode.ts").BuilderNode} BuilderNode */
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Record<string, unknown> & { input: unknown; executionId: string; stepId: string; attempt: number; signal: AbortSignal; iteration: number; heartbeat: (data?: unknown) => void; lastHeartbeat: unknown | null; }} BuilderStepContext
|
|
23
|
+
*/
|
|
24
|
+
/** @typedef {import("./BuilderStepHandle.ts").BuilderStepHandle} BuilderStepHandle */
|
|
25
|
+
/** @typedef {import("@smithers-orchestrator/scheduler/RetryPolicy").RetryPolicy} RetryPolicy */
|
|
26
|
+
/** @typedef {import("./SmithersSqliteOptions.ts").SmithersSqliteOptions} SmithersSqliteOptions */
|
|
27
|
+
|
|
28
|
+
const SmithersSqlite = Context.GenericTag("smithers/effect/sqlite");
|
|
29
|
+
class ApprovalDecision extends Schema.Class("ApprovalDecision")({
|
|
30
|
+
approved: Schema.Boolean,
|
|
31
|
+
note: Schema.NullOr(Schema.String),
|
|
32
|
+
decidedBy: Schema.NullOr(Schema.String),
|
|
33
|
+
decidedAt: Schema.NullOr(Schema.String),
|
|
34
|
+
}) {
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} name
|
|
38
|
+
*/
|
|
39
|
+
function createPayloadTable(name) {
|
|
40
|
+
return sqliteTable(name, {
|
|
41
|
+
runId: text("run_id").notNull(),
|
|
42
|
+
nodeId: text("node_id").notNull(),
|
|
43
|
+
iteration: integer("iteration").notNull().default(0),
|
|
44
|
+
payload: text("payload", { mode: "json" }).$type(),
|
|
45
|
+
}, (t) => ({
|
|
46
|
+
pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} value
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function sanitizeIdentifier(value) {
|
|
54
|
+
const snake = camelToSnake(value)
|
|
55
|
+
.replace(/[^a-zA-Z0-9_]+/g, "_")
|
|
56
|
+
.replace(/_+/g, "_")
|
|
57
|
+
.replace(/^_+|_+$/g, "")
|
|
58
|
+
.toLowerCase();
|
|
59
|
+
return snake || "node";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} id
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function makeTableName(id) {
|
|
66
|
+
return `smithers_${sanitizeIdentifier(id)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* @returns {BuilderApi}
|
|
70
|
+
*/
|
|
71
|
+
function createBuilder(prefix = "") {
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} id
|
|
74
|
+
*/
|
|
75
|
+
const applyPrefix = (id) => (prefix ? `${prefix}.${id}` : id);
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} id
|
|
78
|
+
* @param {StepOptions} options
|
|
79
|
+
* @returns {BuilderStepHandle}
|
|
80
|
+
*/
|
|
81
|
+
const step = (id, options) => {
|
|
82
|
+
const fullId = applyPrefix(id);
|
|
83
|
+
const tableName = makeTableName(fullId);
|
|
84
|
+
return {
|
|
85
|
+
kind: "step",
|
|
86
|
+
id: fullId,
|
|
87
|
+
localId: id,
|
|
88
|
+
tableKey: sanitizeIdentifier(fullId),
|
|
89
|
+
tableName,
|
|
90
|
+
table: createPayloadTable(tableName),
|
|
91
|
+
output: options.output,
|
|
92
|
+
needs: options.needs ?? {},
|
|
93
|
+
run: options.run,
|
|
94
|
+
retries: deriveRetryCount(options.retry),
|
|
95
|
+
retryPolicy: options.retryPolicy ?? deriveRetryPolicy(options.retry),
|
|
96
|
+
timeoutMs: durationToMs(options.timeout),
|
|
97
|
+
skipIf: options.skipIf,
|
|
98
|
+
cache: options.cache,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} id
|
|
103
|
+
* @param {ApprovalOptions} options
|
|
104
|
+
* @returns {BuilderStepHandle}
|
|
105
|
+
*/
|
|
106
|
+
const approval = (id, options) => {
|
|
107
|
+
const fullId = applyPrefix(id);
|
|
108
|
+
const tableName = makeTableName(fullId);
|
|
109
|
+
return {
|
|
110
|
+
kind: "approval",
|
|
111
|
+
id: fullId,
|
|
112
|
+
localId: id,
|
|
113
|
+
tableKey: sanitizeIdentifier(fullId),
|
|
114
|
+
tableName,
|
|
115
|
+
table: createPayloadTable(tableName),
|
|
116
|
+
output: ApprovalDecision,
|
|
117
|
+
needs: options.needs ?? {},
|
|
118
|
+
request: options.request,
|
|
119
|
+
onDeny: options.onDeny ?? "fail",
|
|
120
|
+
retries: 0,
|
|
121
|
+
timeoutMs: null,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
step,
|
|
126
|
+
approval,
|
|
127
|
+
sequence: (...nodes) => ({ kind: "sequence", children: nodes }),
|
|
128
|
+
parallel: (...args) => {
|
|
129
|
+
let maxConcurrency;
|
|
130
|
+
const items = [...args];
|
|
131
|
+
const last = items[items.length - 1];
|
|
132
|
+
if (last &&
|
|
133
|
+
typeof last === "object" &&
|
|
134
|
+
!Array.isArray(last) &&
|
|
135
|
+
!isBuilderNode(last) &&
|
|
136
|
+
"maxConcurrency" in last) {
|
|
137
|
+
maxConcurrency = Number(last.maxConcurrency);
|
|
138
|
+
items.pop();
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
kind: "parallel",
|
|
142
|
+
children: items,
|
|
143
|
+
maxConcurrency,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
loop: (options) => ({
|
|
147
|
+
kind: "loop",
|
|
148
|
+
id: options.id ? applyPrefix(options.id) : undefined,
|
|
149
|
+
children: options.children,
|
|
150
|
+
until: options.until,
|
|
151
|
+
maxIterations: options.maxIterations,
|
|
152
|
+
onMaxReached: options.onMaxReached,
|
|
153
|
+
}),
|
|
154
|
+
match: (source, options) => ({
|
|
155
|
+
kind: "match",
|
|
156
|
+
source,
|
|
157
|
+
when: options.when,
|
|
158
|
+
then: options.then(),
|
|
159
|
+
else: options.else?.(),
|
|
160
|
+
}),
|
|
161
|
+
component: (instanceId, definition, params) => definition.buildWithPrefix(applyPrefix(instanceId), params),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* @param {unknown} value
|
|
166
|
+
* @returns {value is BuilderNode}
|
|
167
|
+
*/
|
|
168
|
+
function isBuilderNode(value) {
|
|
169
|
+
if (!value || typeof value !== "object")
|
|
170
|
+
return false;
|
|
171
|
+
const kind = value.kind;
|
|
172
|
+
return kind === "step" ||
|
|
173
|
+
kind === "approval" ||
|
|
174
|
+
kind === "sequence" ||
|
|
175
|
+
kind === "parallel" ||
|
|
176
|
+
kind === "loop" ||
|
|
177
|
+
kind === "match" ||
|
|
178
|
+
kind === "branch" ||
|
|
179
|
+
kind === "worktree";
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* @param {unknown} input
|
|
183
|
+
* @returns {number | null}
|
|
184
|
+
*/
|
|
185
|
+
function durationToMs(input) {
|
|
186
|
+
if (input == null)
|
|
187
|
+
return null;
|
|
188
|
+
if (typeof input === "string") {
|
|
189
|
+
const trimmed = input.trim();
|
|
190
|
+
const match = trimmed.match(/^(-?\d+(?:\.\d+)?)(ms|s|m|h)$/i);
|
|
191
|
+
if (match) {
|
|
192
|
+
const value = Number(match[1]);
|
|
193
|
+
if (Number.isFinite(value)) {
|
|
194
|
+
const unit = match[2].toLowerCase();
|
|
195
|
+
const factor = unit === "ms"
|
|
196
|
+
? 1
|
|
197
|
+
: unit === "s"
|
|
198
|
+
? 1000
|
|
199
|
+
: unit === "m"
|
|
200
|
+
? 60_000
|
|
201
|
+
: 3_600_000;
|
|
202
|
+
return Math.max(0, Math.floor(value * factor));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (typeof input === "number" && Number.isFinite(input)) {
|
|
207
|
+
return Math.max(0, Math.floor(input));
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
return Math.max(0, Math.floor(Duration.toMillis(Duration.decode(input))));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* @param {unknown} retry
|
|
218
|
+
* @returns {RetryPolicy | undefined}
|
|
219
|
+
*/
|
|
220
|
+
function deriveRetryPolicy(retry) {
|
|
221
|
+
if (!retry || typeof retry !== "object")
|
|
222
|
+
return undefined;
|
|
223
|
+
const backoff = retry.backoff;
|
|
224
|
+
const initialDelayMs = durationToMs(retry.initialDelay);
|
|
225
|
+
if (backoff !== "fixed" &&
|
|
226
|
+
backoff !== "linear" &&
|
|
227
|
+
backoff !== "exponential" &&
|
|
228
|
+
initialDelayMs == null) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
backoff: backoff === "fixed" || backoff === "linear" || backoff === "exponential"
|
|
233
|
+
? backoff
|
|
234
|
+
: undefined,
|
|
235
|
+
initialDelayMs: initialDelayMs ?? undefined,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* @param {unknown} retry
|
|
240
|
+
* @returns {number}
|
|
241
|
+
*/
|
|
242
|
+
function deriveRetryCount(retry) {
|
|
243
|
+
if (retry == null)
|
|
244
|
+
return 0;
|
|
245
|
+
if (typeof retry === "number" && Number.isFinite(retry)) {
|
|
246
|
+
return Math.max(0, Math.floor(retry));
|
|
247
|
+
}
|
|
248
|
+
if (typeof retry === "object" && retry !== null) {
|
|
249
|
+
const maxAttempts = retry.maxAttempts;
|
|
250
|
+
if (typeof maxAttempts === "number" && Number.isFinite(maxAttempts)) {
|
|
251
|
+
return Math.max(0, Math.floor(maxAttempts - 1));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const driver = Effect.runSync(Schedule.driver(retry));
|
|
256
|
+
let count = 0;
|
|
257
|
+
while (count < 100) {
|
|
258
|
+
const exit = Effect.runSyncExit(driver.next(undefined));
|
|
259
|
+
if (Exit.isFailure(exit)) {
|
|
260
|
+
return count;
|
|
261
|
+
}
|
|
262
|
+
count += 1;
|
|
263
|
+
}
|
|
264
|
+
return count;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* @template T
|
|
272
|
+
* @param {AnySchema} schema
|
|
273
|
+
* @param {unknown} value
|
|
274
|
+
* @returns {T}
|
|
275
|
+
*/
|
|
276
|
+
function decodeSchema(schema, value) {
|
|
277
|
+
return Schema.decodeUnknownSync(schema)(value);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* @param {AnySchema} schema
|
|
281
|
+
* @param {unknown} value
|
|
282
|
+
*/
|
|
283
|
+
function encodeSchema(schema, value) {
|
|
284
|
+
return Schema.encodeSync(schema)(value);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* @param {BuilderStepHandle} handle
|
|
288
|
+
* @param {{ iteration?: number; iterations?: Record<string, number>; }} ctx
|
|
289
|
+
* @returns {number}
|
|
290
|
+
*/
|
|
291
|
+
function resolveHandleIteration(handle, ctx) {
|
|
292
|
+
if (handle.loopId) {
|
|
293
|
+
return ctx.iterations?.[handle.loopId] ?? 0;
|
|
294
|
+
}
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* @param {Record<string, unknown>} row
|
|
299
|
+
*/
|
|
300
|
+
function stripPersistedKeys(row) {
|
|
301
|
+
const { runId, nodeId, iteration, payload, ...rest } = row;
|
|
302
|
+
if (payload !== undefined)
|
|
303
|
+
return payload;
|
|
304
|
+
return rest;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* @param {BuilderStepHandle} handle
|
|
308
|
+
* @param {any} ctx
|
|
309
|
+
* @returns {unknown}
|
|
310
|
+
*/
|
|
311
|
+
function readHandleMaybe(handle, ctx) {
|
|
312
|
+
const iteration = resolveHandleIteration(handle, ctx);
|
|
313
|
+
const row = ctx.outputMaybe(handle.tableName, {
|
|
314
|
+
nodeId: handle.id,
|
|
315
|
+
iteration,
|
|
316
|
+
});
|
|
317
|
+
if (!row)
|
|
318
|
+
return undefined;
|
|
319
|
+
return decodeSchema(handle.output, stripPersistedKeys(row));
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* @param {BuilderStepHandle} handle
|
|
323
|
+
* @param {any} ctx
|
|
324
|
+
* @returns {unknown}
|
|
325
|
+
*/
|
|
326
|
+
function readHandle(handle, ctx) {
|
|
327
|
+
const value = readHandleMaybe(handle, ctx);
|
|
328
|
+
if (value === undefined) {
|
|
329
|
+
throw new SmithersError("MISSING_OUTPUT", `Missing output for step "${handle.id}"`, {
|
|
330
|
+
nodeId: handle.id,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* @param {BuilderStepHandle} handle
|
|
337
|
+
* @param {any} ctx
|
|
338
|
+
* @param {unknown} decodedInput
|
|
339
|
+
* @param {ReturnType<typeof requireTaskRuntime>} [runtime]
|
|
340
|
+
* @returns {BuilderStepContext}
|
|
341
|
+
*/
|
|
342
|
+
function buildUserContext(handle, ctx, decodedInput, runtime) {
|
|
343
|
+
const data = {};
|
|
344
|
+
for (const [key, dependency] of Object.entries(handle.needs)) {
|
|
345
|
+
data[key] = readHandle(dependency, ctx);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
...data,
|
|
349
|
+
input: decodedInput,
|
|
350
|
+
executionId: runtime?.runId ?? ctx.runId,
|
|
351
|
+
stepId: handle.id,
|
|
352
|
+
attempt: runtime?.attempt ?? 1,
|
|
353
|
+
signal: runtime?.signal ?? new AbortController().signal,
|
|
354
|
+
iteration: runtime?.iteration ?? resolveHandleIteration(handle, ctx),
|
|
355
|
+
heartbeat: runtime?.heartbeat ?? (() => { }),
|
|
356
|
+
lastHeartbeat: runtime?.lastHeartbeat ?? null,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* @param {Record<string, BuilderStepHandle> | undefined} needs
|
|
361
|
+
* @param {any} ctx
|
|
362
|
+
* @param {unknown} decodedInput
|
|
363
|
+
* @param {ReturnType<typeof requireTaskRuntime>} [runtime]
|
|
364
|
+
*/
|
|
365
|
+
function buildNeedsContext(needs, ctx, decodedInput, runtime) {
|
|
366
|
+
const data = {};
|
|
367
|
+
if (needs) {
|
|
368
|
+
for (const [key, dependency] of Object.entries(needs)) {
|
|
369
|
+
data[key] = readHandleMaybe(dependency, ctx);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const iteration = runtime?.iteration ??
|
|
373
|
+
(typeof ctx?.iteration === "number" ? ctx.iteration : 0);
|
|
374
|
+
return {
|
|
375
|
+
...data,
|
|
376
|
+
input: decodedInput,
|
|
377
|
+
executionId: runtime?.runId ?? ctx.runId,
|
|
378
|
+
stepId: runtime?.stepId ?? "",
|
|
379
|
+
attempt: runtime?.attempt ?? 1,
|
|
380
|
+
signal: runtime?.signal ?? new AbortController().signal,
|
|
381
|
+
iteration,
|
|
382
|
+
heartbeat: runtime?.heartbeat ?? (() => { }),
|
|
383
|
+
lastHeartbeat: runtime?.lastHeartbeat ?? null,
|
|
384
|
+
loop: { iteration: iteration + 1 },
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* @param {unknown} value
|
|
389
|
+
* @param {any} env
|
|
390
|
+
* @param {AbortSignal} signal
|
|
391
|
+
*/
|
|
392
|
+
async function resolveEffectResult(value, env, signal) {
|
|
393
|
+
if (Effect.isEffect?.(value)) {
|
|
394
|
+
return await Effect.runPromise(value.pipe(Effect.provide(env)), { signal });
|
|
395
|
+
}
|
|
396
|
+
if (value && typeof value.then === "function") {
|
|
397
|
+
const resolved = await value;
|
|
398
|
+
if (Effect.isEffect?.(resolved)) {
|
|
399
|
+
return await Effect.runPromise(resolved.pipe(Effect.provide(env)), { signal });
|
|
400
|
+
}
|
|
401
|
+
return resolved;
|
|
402
|
+
}
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* @param {BuilderStepHandle} handle
|
|
407
|
+
* @param {any} ctx
|
|
408
|
+
* @param {unknown} decodedInput
|
|
409
|
+
* @param {any} env
|
|
410
|
+
*/
|
|
411
|
+
async function executeStepHandle(handle, ctx, decodedInput, env) {
|
|
412
|
+
const runtime = requireTaskRuntime();
|
|
413
|
+
if (handle.kind === "approval") {
|
|
414
|
+
const adapter = new SmithersDb(runtime.db);
|
|
415
|
+
const approval = await adapter.getApproval(runtime.runId, handle.id, runtime.iteration);
|
|
416
|
+
return encodeSchema(ApprovalDecision, {
|
|
417
|
+
approved: approval?.status === "approved",
|
|
418
|
+
note: approval?.note ?? null,
|
|
419
|
+
decidedBy: approval?.decidedBy ?? null,
|
|
420
|
+
decidedAt: null,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const userCtx = buildUserContext(handle, ctx, decodedInput, runtime);
|
|
424
|
+
const output = await resolveEffectResult(handle.run?.(userCtx), env, runtime.signal);
|
|
425
|
+
const decoded = decodeSchema(handle.output, output);
|
|
426
|
+
return encodeSchema(handle.output, decoded);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* @param {BuilderStepHandle} handle
|
|
430
|
+
* @param {any} ctx
|
|
431
|
+
* @param {unknown} decodedInput
|
|
432
|
+
* @returns {boolean}
|
|
433
|
+
*/
|
|
434
|
+
function evaluateSkip(handle, ctx, decodedInput) {
|
|
435
|
+
if (!handle.skipIf)
|
|
436
|
+
return false;
|
|
437
|
+
try {
|
|
438
|
+
return Boolean(handle.skipIf(buildUserContext(handle, ctx, decodedInput)));
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* @param {BuilderNode} node
|
|
446
|
+
* @param {any} ctx
|
|
447
|
+
* @param {unknown} decodedInput
|
|
448
|
+
* @param {any} env
|
|
449
|
+
* @returns {React.ReactNode}
|
|
450
|
+
*/
|
|
451
|
+
function renderNode(node, ctx, decodedInput, env) {
|
|
452
|
+
if (node.kind === "step" || node.kind === "approval") {
|
|
453
|
+
const requestInfo = node.kind === "approval"
|
|
454
|
+
? (() => {
|
|
455
|
+
if (!node.request)
|
|
456
|
+
return null;
|
|
457
|
+
const entries = Object.entries(node.needs).map(([key, dep]) => [
|
|
458
|
+
key,
|
|
459
|
+
readHandleMaybe(dep, ctx),
|
|
460
|
+
]);
|
|
461
|
+
if (entries.some(([, value]) => value === undefined)) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
return node.request(Object.fromEntries(entries));
|
|
465
|
+
})()
|
|
466
|
+
: null;
|
|
467
|
+
const compute = () => executeStepHandle(node, ctx, decodedInput, env);
|
|
468
|
+
const needsMap = Object.keys(node.needs).length > 0
|
|
469
|
+
? Object.fromEntries(Object.entries(node.needs).map(([key, dep]) => [key, dep.id]))
|
|
470
|
+
: undefined;
|
|
471
|
+
return (React.createElement(Task, {
|
|
472
|
+
id: node.id,
|
|
473
|
+
output: node.table,
|
|
474
|
+
retries: node.retries,
|
|
475
|
+
retryPolicy: node.retryPolicy,
|
|
476
|
+
timeoutMs: node.timeoutMs,
|
|
477
|
+
cache: node.cache,
|
|
478
|
+
skipIf: evaluateSkip(node, ctx, decodedInput),
|
|
479
|
+
needsApproval: node.kind === "approval",
|
|
480
|
+
approvalMode: node.kind === "approval" ? "decision" : undefined,
|
|
481
|
+
approvalOnDeny: node.kind === "approval" ? node.onDeny : undefined,
|
|
482
|
+
needs: needsMap,
|
|
483
|
+
dependsOn: Object.values(node.needs).map((dep) => dep.id),
|
|
484
|
+
label: requestInfo?.title,
|
|
485
|
+
meta: requestInfo?.summary
|
|
486
|
+
? { requestSummary: requestInfo.summary }
|
|
487
|
+
: undefined,
|
|
488
|
+
children: compute,
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
if (node.kind === "sequence") {
|
|
492
|
+
return React.createElement(Sequence, null, node.children.map((child, index) => React.createElement(React.Fragment, { key: `sequence-${index}` }, renderNode(child, ctx, decodedInput, env))));
|
|
493
|
+
}
|
|
494
|
+
if (node.kind === "parallel") {
|
|
495
|
+
return React.createElement(Parallel, { maxConcurrency: node.maxConcurrency }, node.children.map((child, index) => React.createElement(React.Fragment, { key: `parallel-${index}` }, renderNode(child, ctx, decodedInput, env))));
|
|
496
|
+
}
|
|
497
|
+
if (node.kind === "loop") {
|
|
498
|
+
const outputs = {};
|
|
499
|
+
for (const handle of node.handles ?? []) {
|
|
500
|
+
outputs[handle.localId] = readHandleMaybe(handle, ctx);
|
|
501
|
+
}
|
|
502
|
+
const iteration = (node.id && ctx?.iterations && typeof ctx.iterations[node.id] === "number")
|
|
503
|
+
? ctx.iterations[node.id]
|
|
504
|
+
: (typeof ctx?.iteration === "number" ? ctx.iteration : 0);
|
|
505
|
+
const evalCtx = {
|
|
506
|
+
...outputs,
|
|
507
|
+
input: decodedInput,
|
|
508
|
+
iteration,
|
|
509
|
+
loop: { iteration: iteration + 1 },
|
|
510
|
+
};
|
|
511
|
+
return React.createElement(Loop, {
|
|
512
|
+
id: node.id,
|
|
513
|
+
until: Boolean(node.until(evalCtx)),
|
|
514
|
+
maxIterations: node.maxIterations,
|
|
515
|
+
onMaxReached: node.onMaxReached,
|
|
516
|
+
}, renderNode(node.children, ctx, decodedInput, env));
|
|
517
|
+
}
|
|
518
|
+
if (node.kind === "branch") {
|
|
519
|
+
const baseCtx = buildNeedsContext(node.needs, ctx, decodedInput);
|
|
520
|
+
const chooseThen = Boolean(node.condition(baseCtx));
|
|
521
|
+
return React.createElement(Branch, {
|
|
522
|
+
if: chooseThen,
|
|
523
|
+
then: React.createElement(React.Fragment, null, renderNode(node.then, ctx, decodedInput, env)),
|
|
524
|
+
else: node.else
|
|
525
|
+
? React.createElement(React.Fragment, null, renderNode(node.else, ctx, decodedInput, env))
|
|
526
|
+
: undefined,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (node.kind === "worktree") {
|
|
530
|
+
const baseCtx = buildNeedsContext(node.needs, ctx, decodedInput);
|
|
531
|
+
const skip = node.skipIf ? Boolean(node.skipIf(baseCtx)) : false;
|
|
532
|
+
return React.createElement(Worktree, { id: node.id, path: node.path, branch: node.branch, skipIf: skip }, renderNode(node.children, ctx, decodedInput, env));
|
|
533
|
+
}
|
|
534
|
+
if (node.kind === "match") {
|
|
535
|
+
const sourceValue = readHandleMaybe(node.source, ctx);
|
|
536
|
+
const chooseThen = sourceValue !== undefined && node.when(sourceValue);
|
|
537
|
+
return React.createElement(Branch, {
|
|
538
|
+
if: chooseThen,
|
|
539
|
+
then: React.createElement(React.Fragment, null, renderNode(node.then, ctx, decodedInput, env)),
|
|
540
|
+
else: node.else
|
|
541
|
+
? React.createElement(React.Fragment, null, renderNode(node.else, ctx, decodedInput, env))
|
|
542
|
+
: undefined,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* @param {BuilderNode} node
|
|
549
|
+
* @param {BuilderStepHandle[]} [out]
|
|
550
|
+
*/
|
|
551
|
+
function collectHandles(node, out = []) {
|
|
552
|
+
switch (node.kind) {
|
|
553
|
+
case "step":
|
|
554
|
+
case "approval":
|
|
555
|
+
out.push(node);
|
|
556
|
+
return out;
|
|
557
|
+
case "sequence":
|
|
558
|
+
case "parallel":
|
|
559
|
+
for (const child of node.children)
|
|
560
|
+
collectHandles(child, out);
|
|
561
|
+
return out;
|
|
562
|
+
case "loop":
|
|
563
|
+
collectHandles(node.children, out);
|
|
564
|
+
return out;
|
|
565
|
+
case "match":
|
|
566
|
+
collectHandles(node.then, out);
|
|
567
|
+
if (node.else)
|
|
568
|
+
collectHandles(node.else, out);
|
|
569
|
+
return out;
|
|
570
|
+
case "branch":
|
|
571
|
+
collectHandles(node.then, out);
|
|
572
|
+
if (node.else)
|
|
573
|
+
collectHandles(node.else, out);
|
|
574
|
+
return out;
|
|
575
|
+
case "worktree":
|
|
576
|
+
collectHandles(node.children, out);
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* @param {BuilderStepHandle[]} handles
|
|
582
|
+
*/
|
|
583
|
+
function assertUniqueHandleIds(handles) {
|
|
584
|
+
const seen = new Set();
|
|
585
|
+
for (const handle of handles) {
|
|
586
|
+
if (seen.has(handle.id)) {
|
|
587
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate step id "${handle.id}"`, {
|
|
588
|
+
kind: handle.kind,
|
|
589
|
+
id: handle.id,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
seen.add(handle.id);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* @param {BuilderNode} node
|
|
597
|
+
* @param {string} [activeLoopId]
|
|
598
|
+
* @returns {BuilderStepHandle[]}
|
|
599
|
+
*/
|
|
600
|
+
function annotateLoops(node, activeLoopId) {
|
|
601
|
+
switch (node.kind) {
|
|
602
|
+
case "step":
|
|
603
|
+
case "approval":
|
|
604
|
+
node.loopId = activeLoopId;
|
|
605
|
+
return [node];
|
|
606
|
+
case "sequence":
|
|
607
|
+
case "parallel":
|
|
608
|
+
return node.children.flatMap((child) => annotateLoops(child, activeLoopId));
|
|
609
|
+
case "loop": {
|
|
610
|
+
if (activeLoopId) {
|
|
611
|
+
throw new SmithersError("NESTED_LOOP", "Nested builder loops are not supported.");
|
|
612
|
+
}
|
|
613
|
+
const handles = annotateLoops(node.children, node.id ?? "__loop__");
|
|
614
|
+
node.handles = handles;
|
|
615
|
+
return handles;
|
|
616
|
+
}
|
|
617
|
+
case "match":
|
|
618
|
+
return [
|
|
619
|
+
...annotateLoops(node.then, activeLoopId),
|
|
620
|
+
...(node.else ? annotateLoops(node.else, activeLoopId) : []),
|
|
621
|
+
];
|
|
622
|
+
case "branch":
|
|
623
|
+
return [
|
|
624
|
+
...annotateLoops(node.then, activeLoopId),
|
|
625
|
+
...(node.else ? annotateLoops(node.else, activeLoopId) : []),
|
|
626
|
+
];
|
|
627
|
+
case "worktree":
|
|
628
|
+
return annotateLoops(node.children, activeLoopId);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function createInputTable() {
|
|
632
|
+
return sqliteTable("input", {
|
|
633
|
+
runId: text("run_id").primaryKey(),
|
|
634
|
+
payload: text("payload", { mode: "json" }).$type(),
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* @param {string} filename
|
|
639
|
+
* @param {BuilderStepHandle[]} handles
|
|
640
|
+
*/
|
|
641
|
+
function createBuilderDb(filename, handles) {
|
|
642
|
+
const sqlite = new Database(filename);
|
|
643
|
+
sqlite.run("PRAGMA journal_mode = WAL");
|
|
644
|
+
// 30s timeout: concurrent worktrees each spawn agent processes that all write
|
|
645
|
+
// to smithers.db simultaneously. 5s is too short and causes SQLITE_IOERR_VNODE
|
|
646
|
+
// on macOS when the VFS can't acquire the WAL shared-memory lock in time.
|
|
647
|
+
sqlite.run("PRAGMA busy_timeout = 30000");
|
|
648
|
+
// NORMAL is safe in WAL mode (no data loss on crash) and reduces fsync
|
|
649
|
+
// stalls that contribute to WAL checkpoint contention across processes.
|
|
650
|
+
sqlite.run("PRAGMA synchronous = NORMAL");
|
|
651
|
+
// Ensure no exclusive lock is held, allowing multiple readers/writers.
|
|
652
|
+
sqlite.run("PRAGMA locking_mode = NORMAL");
|
|
653
|
+
sqlite.run("PRAGMA foreign_keys = ON");
|
|
654
|
+
sqlite.run(`CREATE TABLE IF NOT EXISTS "input" (run_id TEXT PRIMARY KEY, payload TEXT)`);
|
|
655
|
+
for (const handle of handles) {
|
|
656
|
+
sqlite.run(`CREATE TABLE IF NOT EXISTS "${handle.tableName}" (` +
|
|
657
|
+
`run_id TEXT NOT NULL, ` +
|
|
658
|
+
`node_id TEXT NOT NULL, ` +
|
|
659
|
+
`iteration INTEGER NOT NULL DEFAULT 0, ` +
|
|
660
|
+
`payload TEXT, ` +
|
|
661
|
+
`PRIMARY KEY (run_id, node_id, iteration)` +
|
|
662
|
+
`)`);
|
|
663
|
+
}
|
|
664
|
+
const inputTable = createInputTable();
|
|
665
|
+
const schema = { input: inputTable };
|
|
666
|
+
for (const handle of handles) {
|
|
667
|
+
schema[handle.tableKey] = handle.table;
|
|
668
|
+
}
|
|
669
|
+
const db = drizzle(sqlite, { schema });
|
|
670
|
+
return {
|
|
671
|
+
sqlite,
|
|
672
|
+
db,
|
|
673
|
+
inputTable,
|
|
674
|
+
schema,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* @param {any} db
|
|
679
|
+
* @param {string} runId
|
|
680
|
+
* @param {BuilderStepHandle} handle
|
|
681
|
+
*/
|
|
682
|
+
async function readLatestHandleResult(db, runId, handle) {
|
|
683
|
+
const rows = await db
|
|
684
|
+
.select()
|
|
685
|
+
.from(handle.table)
|
|
686
|
+
.where(and(eq(handle.table.runId, runId), eq(handle.table.nodeId, handle.id)))
|
|
687
|
+
.orderBy(desc(handle.table.iteration))
|
|
688
|
+
.limit(1);
|
|
689
|
+
const row = rows[0];
|
|
690
|
+
if (!row)
|
|
691
|
+
return undefined;
|
|
692
|
+
return decodeSchema(handle.output, stripPersistedKeys(row));
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* @param {BuilderNode} node
|
|
696
|
+
* @param {any} db
|
|
697
|
+
* @param {string} runId
|
|
698
|
+
* @param {unknown} [decodedInput]
|
|
699
|
+
* @returns {Promise<unknown>}
|
|
700
|
+
*/
|
|
701
|
+
async function extractResult(node, db, runId, decodedInput) {
|
|
702
|
+
switch (node.kind) {
|
|
703
|
+
case "step":
|
|
704
|
+
case "approval":
|
|
705
|
+
return readLatestHandleResult(db, runId, node);
|
|
706
|
+
case "sequence": {
|
|
707
|
+
const last = node.children[node.children.length - 1];
|
|
708
|
+
return last ? extractResult(last, db, runId, decodedInput) : undefined;
|
|
709
|
+
}
|
|
710
|
+
case "parallel":
|
|
711
|
+
return Promise.all(node.children.map((child) => extractResult(child, db, runId, decodedInput)));
|
|
712
|
+
case "loop":
|
|
713
|
+
return extractResult(node.children, db, runId, decodedInput);
|
|
714
|
+
case "match": {
|
|
715
|
+
const source = await readLatestHandleResult(db, runId, node.source);
|
|
716
|
+
if (source !== undefined && node.when(source)) {
|
|
717
|
+
return extractResult(node.then, db, runId, decodedInput);
|
|
718
|
+
}
|
|
719
|
+
return node.else ? extractResult(node.else, db, runId, decodedInput) : undefined;
|
|
720
|
+
}
|
|
721
|
+
case "branch": {
|
|
722
|
+
const ctx = {
|
|
723
|
+
input: decodedInput ?? {},
|
|
724
|
+
iteration: 0,
|
|
725
|
+
loop: { iteration: 1 },
|
|
726
|
+
};
|
|
727
|
+
if (node.needs) {
|
|
728
|
+
for (const [key, handle] of Object.entries(node.needs)) {
|
|
729
|
+
ctx[key] = await readLatestHandleResult(db, runId, handle);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (node.condition(ctx)) {
|
|
733
|
+
return extractResult(node.then, db, runId, decodedInput);
|
|
734
|
+
}
|
|
735
|
+
return node.else ? extractResult(node.else, db, runId, decodedInput) : undefined;
|
|
736
|
+
}
|
|
737
|
+
case "worktree":
|
|
738
|
+
return extractResult(node.children, db, runId, decodedInput);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* @param {{ status: string; error?: unknown }} result
|
|
743
|
+
*/
|
|
744
|
+
function normalizeExecutionError(result) {
|
|
745
|
+
if (result.error instanceof Error)
|
|
746
|
+
return result.error;
|
|
747
|
+
if (typeof result.error === "string" && result.error.length > 0) {
|
|
748
|
+
return new SmithersError("WORKFLOW_EXECUTION_FAILED", result.error, {
|
|
749
|
+
status: result.status,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return new SmithersError("WORKFLOW_EXECUTION_FAILED", `Workflow execution ended with status "${result.status}"`, { status: result.status });
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* @param {{ name: string; input: AnySchema }} options
|
|
756
|
+
*/
|
|
757
|
+
function createWorkflow(options) {
|
|
758
|
+
return {
|
|
759
|
+
/**
|
|
760
|
+
* @param {($: BuilderApi) => BuilderNode} buildGraph
|
|
761
|
+
* @returns {BuiltSmithersWorkflow}
|
|
762
|
+
*/
|
|
763
|
+
build(buildGraph) {
|
|
764
|
+
const root = buildGraph(createBuilder());
|
|
765
|
+
annotateLoops(root);
|
|
766
|
+
const handles = collectHandles(root);
|
|
767
|
+
assertUniqueHandleIds(handles);
|
|
768
|
+
return {
|
|
769
|
+
/**
|
|
770
|
+
* @param {unknown} input
|
|
771
|
+
* @param {Omit<Parameters<typeof runWorkflow>[1], "input">} [opts]
|
|
772
|
+
*/
|
|
773
|
+
execute(input, opts) {
|
|
774
|
+
return Effect.gen(function* () {
|
|
775
|
+
const env = yield* Effect.context();
|
|
776
|
+
const sqliteConfig = yield* SmithersSqlite;
|
|
777
|
+
const decodedInput = decodeSchema(options.input, input);
|
|
778
|
+
const encodedInput = JSON.parse(JSON.stringify(encodeSchema(options.input, decodedInput) ?? {}));
|
|
779
|
+
return yield* Effect.acquireUseRelease(Effect.sync(() => createBuilderDb(sqliteConfig.filename, handles)), (runtime) => Effect.promise(async () => {
|
|
780
|
+
const workflow = {
|
|
781
|
+
db: runtime.db,
|
|
782
|
+
build: (ctx) => React.createElement(Workflow, { name: options.name }, renderNode(ctx && root ? root : root, ctx, decodedInput, env)),
|
|
783
|
+
opts: {},
|
|
784
|
+
};
|
|
785
|
+
const result = await Effect.runPromise(runWorkflow(workflow, {
|
|
786
|
+
...opts,
|
|
787
|
+
input: encodedInput,
|
|
788
|
+
}));
|
|
789
|
+
if (result.status === "finished") {
|
|
790
|
+
return await extractResult(root, runtime.db, result.runId, decodedInput);
|
|
791
|
+
}
|
|
792
|
+
if (result.status === "waiting-approval" ||
|
|
793
|
+
result.status === "waiting-timer") {
|
|
794
|
+
return result;
|
|
795
|
+
}
|
|
796
|
+
throw normalizeExecutionError(result);
|
|
797
|
+
}), (runtime) => ignoreSyncError("close builder sqlite", () => runtime.sqlite.close()));
|
|
798
|
+
});
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* @param {{ name: string; params?: Record<string, unknown> }} options
|
|
806
|
+
*/
|
|
807
|
+
function createComponent(options) {
|
|
808
|
+
return {
|
|
809
|
+
/**
|
|
810
|
+
* @param {($: BuilderApi, params: Record<string, unknown>) => BuilderNode} buildGraph
|
|
811
|
+
* @returns {ComponentDefinition}
|
|
812
|
+
*/
|
|
813
|
+
build(buildGraph) {
|
|
814
|
+
return {
|
|
815
|
+
kind: "component-definition",
|
|
816
|
+
name: options.name,
|
|
817
|
+
/**
|
|
818
|
+
* @param {string} prefix
|
|
819
|
+
* @param {Record<string, unknown>} params
|
|
820
|
+
*/
|
|
821
|
+
buildWithPrefix(prefix, params) {
|
|
822
|
+
return buildGraph(createBuilder(prefix), params);
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* @param {SmithersSqliteOptions} options
|
|
830
|
+
*/
|
|
831
|
+
function sqlite(options) {
|
|
832
|
+
return Layer.succeed(SmithersSqlite, options);
|
|
833
|
+
}
|
|
834
|
+
/** @type {{ sqlite: typeof sqlite }} */
|
|
835
|
+
export const Smithers = {
|
|
836
|
+
sqlite,
|
|
837
|
+
};
|