@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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +50 -0
  3. package/src/AlertHumanRequestOptions.ts +8 -0
  4. package/src/AlertRuntimeServices.ts +10 -0
  5. package/src/ChildWorkflowDefinition.ts +5 -0
  6. package/src/ChildWorkflowExecuteOptions.ts +14 -0
  7. package/src/ContinuationRequest.ts +3 -0
  8. package/src/HijackState.ts +19 -0
  9. package/src/HumanRequestKind.ts +1 -0
  10. package/src/HumanRequestStatus.ts +1 -0
  11. package/src/PlanNode.ts +29 -0
  12. package/src/RalphMeta.ts +7 -0
  13. package/src/RalphState.ts +4 -0
  14. package/src/RalphStateMap.ts +3 -0
  15. package/src/ScheduleResult.ts +15 -0
  16. package/src/SignalRunOptions.ts +5 -0
  17. package/src/alert-runtime.js +22 -0
  18. package/src/approvals.js +220 -0
  19. package/src/child-workflow.js +163 -0
  20. package/src/effect/ApprovalDeferredResolution.ts +13 -0
  21. package/src/effect/ApprovalDurableDeferredResolution.ts +11 -0
  22. package/src/effect/ApprovalPayload.ts +7 -0
  23. package/src/effect/ApprovalResult.ts +6 -0
  24. package/src/effect/BuilderNode.ts +52 -0
  25. package/src/effect/BuilderStepHandle.ts +47 -0
  26. package/src/effect/CancelPayload.ts +3 -0
  27. package/src/effect/CancelResult.ts +4 -0
  28. package/src/effect/DeferredResolution.ts +7 -0
  29. package/src/effect/DiffBundle.ts +7 -0
  30. package/src/effect/ExecuteTaskActivityOptions.ts +7 -0
  31. package/src/effect/FilePatch.ts +6 -0
  32. package/src/effect/GetRunPayload.ts +3 -0
  33. package/src/effect/GetRunResult.ts +3 -0
  34. package/src/effect/LegacyExecuteTaskFn.ts +24 -0
  35. package/src/effect/ListRunsPayload.ts +6 -0
  36. package/src/effect/RunStatusSchema.ts +9 -0
  37. package/src/effect/RunSummary.ts +23 -0
  38. package/src/effect/SignalPayload.ts +7 -0
  39. package/src/effect/SignalResult.ts +6 -0
  40. package/src/effect/SmithersSqliteOptions.ts +3 -0
  41. package/src/effect/SqlMessageStorageEventHistoryQuery.ts +7 -0
  42. package/src/effect/TaggedWorkerError.ts +46 -0
  43. package/src/effect/TaskActivityContext.ts +4 -0
  44. package/src/effect/TaskActivityRetryOptions.ts +4 -0
  45. package/src/effect/TaskBridgeToolConfig.ts +6 -0
  46. package/src/effect/TaskFailure.ts +3 -0
  47. package/src/effect/TaskResult.ts +5 -0
  48. package/src/effect/UnknownWorkerError.ts +5 -0
  49. package/src/effect/WaitForEventDurableDeferredResolution.ts +11 -0
  50. package/src/effect/WorkerDispatchKind.ts +1 -0
  51. package/src/effect/WorkerTask.ts +14 -0
  52. package/src/effect/WorkerTaskError.ts +4 -0
  53. package/src/effect/WorkerTaskKind.ts +1 -0
  54. package/src/effect/WorkflowPatchDecisionRecord.ts +4 -0
  55. package/src/effect/WorkflowPatchDecisions.ts +1 -0
  56. package/src/effect/WorkflowVersioningRuntime.ts +7 -0
  57. package/src/effect/activity-bridge.js +131 -0
  58. package/src/effect/bridge-utils.js +45 -0
  59. package/src/effect/builder.js +837 -0
  60. package/src/effect/compute-task-bridge.js +734 -0
  61. package/src/effect/deferred-bridge.js +63 -0
  62. package/src/effect/deferred-state-bridge.js +1343 -0
  63. package/src/effect/diff-bundle.js +352 -0
  64. package/src/effect/durable-deferred-bridge.js +282 -0
  65. package/src/effect/entity-worker.js +154 -0
  66. package/src/effect/http-runner.js +86 -0
  67. package/src/effect/rpc-schema.js +101 -0
  68. package/src/effect/single-runner.js +189 -0
  69. package/src/effect/sql-message-storage.js +817 -0
  70. package/src/effect/static-task-bridge.js +308 -0
  71. package/src/effect/versioning.js +123 -0
  72. package/src/effect/workflow-bridge.js +260 -0
  73. package/src/effect/workflow-make-bridge.js +233 -0
  74. package/src/engine.js +6933 -0
  75. package/src/events.js +237 -0
  76. package/src/external/json-schema-to-zod.js +214 -0
  77. package/src/getDefinedToolMetadata.js +10 -0
  78. package/src/hot/HotReloadEvent.ts +21 -0
  79. package/src/hot/HotWorkflowController.js +220 -0
  80. package/src/hot/OverlayOptions.ts +4 -0
  81. package/src/hot/WatchTreeOptions.ts +6 -0
  82. package/src/hot/index.js +9 -0
  83. package/src/hot/overlay.js +177 -0
  84. package/src/hot/watch.js +174 -0
  85. package/src/human-requests.js +120 -0
  86. package/src/index.d.ts +1597 -0
  87. package/src/index.js +41 -0
  88. package/src/runtime-owner.js +36 -0
  89. package/src/scheduler.js +31 -0
  90. 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
+ };