@magic-marker/nurt 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,996 @@
1
+ // src/errors.ts
2
+ var CycleDetectedError = class extends Error {
3
+ constructor(cycle) {
4
+ super(`Cycle detected in flow graph: ${cycle.join(" \u2192 ")}`);
5
+ this.name = "CycleDetectedError";
6
+ }
7
+ };
8
+ var DuplicateStepError = class extends Error {
9
+ constructor(name) {
10
+ super(`Step "${name}" is already registered`);
11
+ this.name = "DuplicateStepError";
12
+ }
13
+ };
14
+ var UnknownParentError = class extends Error {
15
+ constructor(stepName, parentName) {
16
+ super(`Step "${stepName}" depends on unknown parent "${parentName}"`);
17
+ this.name = "UnknownParentError";
18
+ }
19
+ };
20
+ var UnsealedGroupError = class extends Error {
21
+ constructor(groupName) {
22
+ super(`Group "${groupName}" was never sealed`);
23
+ this.name = "UnsealedGroupError";
24
+ }
25
+ };
26
+ var UnfilledSlotError = class extends Error {
27
+ constructor(slotPath) {
28
+ super(`Slot "${slotPath}" was not provided an implementation`);
29
+ this.name = "UnfilledSlotError";
30
+ }
31
+ };
32
+ var StepExecutionError = class extends Error {
33
+ stepName;
34
+ cause;
35
+ constructor(stepName, cause) {
36
+ const message = cause instanceof Error ? cause.message : String(cause);
37
+ super(`Step "${stepName}" failed: ${message}`);
38
+ this.name = "StepExecutionError";
39
+ this.stepName = stepName;
40
+ this.cause = cause;
41
+ }
42
+ };
43
+
44
+ // src/graph.ts
45
+ function validateAcyclic(nodes) {
46
+ const inDegree = /* @__PURE__ */ new Map();
47
+ const children = /* @__PURE__ */ new Map();
48
+ for (const node of nodes) {
49
+ if (!inDegree.has(node.name)) {
50
+ inDegree.set(node.name, 0);
51
+ }
52
+ if (!children.has(node.name)) {
53
+ children.set(node.name, []);
54
+ }
55
+ for (const parent of node.parentNames) {
56
+ if (!children.has(parent)) {
57
+ children.set(parent, []);
58
+ }
59
+ children.get(parent).push(node.name);
60
+ inDegree.set(node.name, (inDegree.get(node.name) ?? 0) + 1);
61
+ }
62
+ }
63
+ const queue = [];
64
+ for (const [name, degree] of inDegree) {
65
+ if (degree === 0) {
66
+ queue.push(name);
67
+ }
68
+ }
69
+ let visited = 0;
70
+ while (queue.length > 0) {
71
+ const current = queue.shift();
72
+ visited++;
73
+ for (const child of children.get(current) ?? []) {
74
+ const newDegree = inDegree.get(child) - 1;
75
+ inDegree.set(child, newDegree);
76
+ if (newDegree === 0) {
77
+ queue.push(child);
78
+ }
79
+ }
80
+ }
81
+ if (visited < nodes.length) {
82
+ const remaining = nodes.filter((n) => (inDegree.get(n.name) ?? 0) > 0).map((n) => n.name);
83
+ throw new CycleDetectedError(remaining);
84
+ }
85
+ }
86
+ function topologicalSort(nodes) {
87
+ const inDegree = /* @__PURE__ */ new Map();
88
+ const children = /* @__PURE__ */ new Map();
89
+ for (const node of nodes) {
90
+ if (!inDegree.has(node.name)) {
91
+ inDegree.set(node.name, 0);
92
+ }
93
+ if (!children.has(node.name)) {
94
+ children.set(node.name, []);
95
+ }
96
+ for (const parent of node.parentNames) {
97
+ if (!children.has(parent)) {
98
+ children.set(parent, []);
99
+ }
100
+ children.get(parent).push(node.name);
101
+ inDegree.set(node.name, (inDegree.get(node.name) ?? 0) + 1);
102
+ }
103
+ }
104
+ const queue = [];
105
+ for (const [name, degree] of inDegree) {
106
+ if (degree === 0) {
107
+ queue.push(name);
108
+ }
109
+ }
110
+ const sorted = [];
111
+ while (queue.length > 0) {
112
+ const current = queue.shift();
113
+ sorted.push(current);
114
+ for (const child of children.get(current) ?? []) {
115
+ const newDegree = inDegree.get(child) - 1;
116
+ inDegree.set(child, newDegree);
117
+ if (newDegree === 0) {
118
+ queue.push(child);
119
+ }
120
+ }
121
+ }
122
+ return sorted;
123
+ }
124
+ function getReadySteps(steps, statuses) {
125
+ const ready = [];
126
+ for (const step of steps) {
127
+ const status = statuses.get(step.name);
128
+ if (status !== "pending") continue;
129
+ const allParentsDone = step.parentNames.every((parent) => {
130
+ const parentStatus = statuses.get(parent);
131
+ return parentStatus === "success";
132
+ });
133
+ if (allParentsDone) {
134
+ ready.push(step.name);
135
+ }
136
+ }
137
+ return ready;
138
+ }
139
+ function getSkippableSteps(steps, statuses) {
140
+ const skippable = [];
141
+ for (const step of steps) {
142
+ const status = statuses.get(step.name);
143
+ if (status !== "pending") continue;
144
+ if (step.allowFailures) continue;
145
+ const hasFailedParent = step.parentNames.some((parent) => {
146
+ const parentStatus = statuses.get(parent);
147
+ return parentStatus === "error" || parentStatus === "skipped";
148
+ });
149
+ if (hasFailedParent) {
150
+ skippable.push(step.name);
151
+ }
152
+ }
153
+ return skippable;
154
+ }
155
+ function getReadyWithFailures(steps, statuses) {
156
+ const ready = [];
157
+ for (const step of steps) {
158
+ const status = statuses.get(step.name);
159
+ if (status !== "pending") continue;
160
+ if (!step.allowFailures) continue;
161
+ const allParentsSettled = step.parentNames.every((parent) => {
162
+ const parentStatus = statuses.get(parent);
163
+ return parentStatus === "success" || parentStatus === "error" || parentStatus === "skipped";
164
+ });
165
+ if (allParentsSettled) {
166
+ ready.push(step.name);
167
+ }
168
+ }
169
+ return ready;
170
+ }
171
+
172
+ // src/types.ts
173
+ var GROUP_REF_BRAND = /* @__PURE__ */ Symbol("GroupRef");
174
+ function group(name) {
175
+ return { [GROUP_REF_BRAND]: true, groupName: name };
176
+ }
177
+ function isGroupRef(value) {
178
+ return typeof value === "object" && value !== null && GROUP_REF_BRAND in value;
179
+ }
180
+ function isSubgraphMember(member) {
181
+ return "flow" in member;
182
+ }
183
+ function isExecutable(value) {
184
+ return typeof value === "object" && value !== null && "execute" in value && typeof value.execute === "function";
185
+ }
186
+ function invokeHandler(handler, input, ctx) {
187
+ if (isExecutable(handler)) {
188
+ return handler.execute(input, ctx);
189
+ }
190
+ return handler(
191
+ input,
192
+ ctx
193
+ );
194
+ }
195
+
196
+ // src/snapshot.ts
197
+ function createSnapshot(input) {
198
+ const steps = input.flow.steps.map((def) => {
199
+ const record = input.stepRecords.get(def.name);
200
+ return {
201
+ allowFailures: def.allowFailures,
202
+ completedAt: record?.completedAt,
203
+ durationMs: record?.durationMs,
204
+ error: record?.error,
205
+ name: def.name,
206
+ output: record?.output,
207
+ parentNames: [...def.parentNames],
208
+ startedAt: record?.startedAt,
209
+ status: record?.status ?? "pending",
210
+ terminal: def.terminal
211
+ };
212
+ });
213
+ const groups = input.flow.groups.map((groupDef) => {
214
+ const runtime = input.groupRuntimes.get(groupDef.name);
215
+ return {
216
+ dependsOn: [...groupDef.dependsOn],
217
+ members: runtime?.members.map(serializeGroupMember) ?? [],
218
+ name: groupDef.name,
219
+ sealed: runtime?.sealed ?? false
220
+ };
221
+ });
222
+ return {
223
+ flow: {
224
+ groups,
225
+ name: input.flow.name,
226
+ steps
227
+ },
228
+ run: {
229
+ completedAt: input.run.completedAt,
230
+ runId: input.run.runId,
231
+ startedAt: input.run.startedAt,
232
+ status: input.run.status
233
+ }
234
+ };
235
+ }
236
+ function serializeGroupMember(member) {
237
+ const isSubgraph = isSubgraphMember(member.member);
238
+ const durationMs = member.startedAt && member.completedAt ? member.completedAt - member.startedAt : void 0;
239
+ const result = {
240
+ completedAt: member.completedAt,
241
+ durationMs,
242
+ error: member.error,
243
+ name: member.name,
244
+ output: member.output,
245
+ startedAt: member.startedAt,
246
+ status: member.status,
247
+ type: isSubgraph ? "subgraph" : "single"
248
+ };
249
+ if (isSubgraph) {
250
+ const sub = member.member;
251
+ if (sub.externalDeps) {
252
+ result.externalDeps = { ...sub.externalDeps };
253
+ }
254
+ if (member.childRun) {
255
+ result.subgraph = member.childRun.snapshot();
256
+ }
257
+ }
258
+ return result;
259
+ }
260
+
261
+ // src/group.ts
262
+ function createGroupRuntime(name, dependsOn) {
263
+ return {
264
+ dependsOn,
265
+ members: [],
266
+ name,
267
+ sealed: false
268
+ };
269
+ }
270
+ function addGroupMembers(group2, members, seal) {
271
+ for (const member of members) {
272
+ group2.members.push({
273
+ member,
274
+ name: member.name,
275
+ status: "pending"
276
+ });
277
+ }
278
+ if (seal) {
279
+ group2.sealed = true;
280
+ }
281
+ }
282
+ function isGroupComplete(group2) {
283
+ if (!group2.sealed) return false;
284
+ return group2.members.every(
285
+ (m) => m.status === "success" || m.status === "error"
286
+ );
287
+ }
288
+ function collectGroupOutputs(group2) {
289
+ return group2.members.map((m) => {
290
+ if (m.status === "success") {
291
+ return { status: "success", value: m.output };
292
+ }
293
+ return {
294
+ error: m.error ?? "Unknown error",
295
+ status: "error"
296
+ };
297
+ });
298
+ }
299
+ function collectSuccessOutputs(group2) {
300
+ return group2.members.filter((m) => m.status === "success").map((m) => m.output);
301
+ }
302
+ async function executeGroupMember(memberRuntime, parentInput, ctx, parentStepOutputs, onStateChange) {
303
+ memberRuntime.status = "running";
304
+ memberRuntime.startedAt = Date.now();
305
+ onStateChange?.();
306
+ try {
307
+ if (isSubgraphMember(memberRuntime.member)) {
308
+ const subgraph = memberRuntime.member;
309
+ let injectedSteps;
310
+ if (subgraph.externalDeps && parentStepOutputs) {
311
+ const deps = Object.entries(subgraph.externalDeps);
312
+ while (!ctx.signal.aborted && deps.some(([, parentName]) => !parentStepOutputs.has(parentName))) {
313
+ await new Promise((r) => setTimeout(r, 10));
314
+ }
315
+ injectedSteps = /* @__PURE__ */ new Map();
316
+ for (const [childStepName, parentStepName] of deps) {
317
+ injectedSteps.set(
318
+ childStepName,
319
+ parentStepOutputs.get(parentStepName)
320
+ );
321
+ }
322
+ }
323
+ const childHooks = onStateChange ? {
324
+ onStepComplete: onStateChange,
325
+ onStepError: onStateChange,
326
+ onStepStart: onStateChange
327
+ } : void 0;
328
+ const childRun = subgraph.flow.run({
329
+ hooks: childHooks,
330
+ injectedSteps
331
+ });
332
+ memberRuntime.childRun = childRun;
333
+ const onAbort = () => childRun.abort();
334
+ ctx.signal.addEventListener("abort", onAbort);
335
+ const result = await childRun.result;
336
+ ctx.signal.removeEventListener("abort", onAbort);
337
+ if (result.status === "error") {
338
+ const errorStep = result.steps.find((s) => s.status === "error");
339
+ throw new Error(errorStep?.error ?? "Subgraph failed");
340
+ }
341
+ const terminalStep = result.steps.find(
342
+ (s) => s.status === "success" && subgraph.flow.steps.some(
343
+ (d) => d.name === s.name && d.terminal
344
+ )
345
+ );
346
+ memberRuntime.output = terminalStep?.output ?? result.steps.findLast((s) => s.status === "success")?.output;
347
+ } else {
348
+ const single = memberRuntime.member;
349
+ memberRuntime.output = await invokeHandler(
350
+ single.execute,
351
+ parentInput,
352
+ ctx
353
+ );
354
+ }
355
+ memberRuntime.status = "success";
356
+ memberRuntime.completedAt = Date.now();
357
+ onStateChange?.();
358
+ } catch (error) {
359
+ memberRuntime.status = "error";
360
+ memberRuntime.completedAt = Date.now();
361
+ onStateChange?.();
362
+ memberRuntime.error = error instanceof Error ? error.message : String(error);
363
+ throw error;
364
+ }
365
+ }
366
+
367
+ // src/history.ts
368
+ var History = class {
369
+ store = /* @__PURE__ */ new Map();
370
+ set(key, value) {
371
+ this.store.set(key, value);
372
+ }
373
+ get(key) {
374
+ return this.store.get(key);
375
+ }
376
+ has(key) {
377
+ return this.store.has(key);
378
+ }
379
+ snapshot() {
380
+ return new Map(this.store);
381
+ }
382
+ };
383
+
384
+ // src/flow-run.ts
385
+ var generateRunId = () => crypto.randomUUID().slice(0, 8);
386
+ var FlowRun = class {
387
+ runId;
388
+ flow;
389
+ hooks;
390
+ failFast;
391
+ abortController;
392
+ history;
393
+ stepDefs;
394
+ stepRecords;
395
+ stepOutputs;
396
+ groupRuntimes;
397
+ providedSlots;
398
+ providedData;
399
+ status = "idle";
400
+ startedAt = 0;
401
+ resolveResult;
402
+ result;
403
+ loopRunning = false;
404
+ pendingGroupStarts = [];
405
+ dynamicStepsAdded = false;
406
+ constructor(flow2, options) {
407
+ this.runId = generateRunId();
408
+ this.flow = flow2;
409
+ this.hooks = options?.hooks ?? {};
410
+ this.failFast = options?.failFast ?? false;
411
+ this.abortController = new AbortController();
412
+ this.history = new History();
413
+ this.stepDefs = /* @__PURE__ */ new Map();
414
+ this.stepRecords = /* @__PURE__ */ new Map();
415
+ this.stepOutputs = /* @__PURE__ */ new Map();
416
+ this.groupRuntimes = /* @__PURE__ */ new Map();
417
+ this.providedSlots = /* @__PURE__ */ new Map();
418
+ this.providedData = /* @__PURE__ */ new Map();
419
+ for (const step of flow2.steps) {
420
+ this.stepDefs.set(step.name, step);
421
+ this.stepRecords.set(step.name, {
422
+ name: step.name,
423
+ parentNames: step.parentNames,
424
+ status: "pending"
425
+ });
426
+ }
427
+ for (const group2 of flow2.groups) {
428
+ this.groupRuntimes.set(
429
+ group2.name,
430
+ createGroupRuntime(group2.name, group2.dependsOn)
431
+ );
432
+ }
433
+ if (options?.injectedSteps) {
434
+ for (const [name, output] of options.injectedSteps) {
435
+ if (!this.stepDefs.has(name)) {
436
+ this.stepDefs.set(name, {
437
+ allowFailures: false,
438
+ handler: async () => output,
439
+ name,
440
+ parentNames: [],
441
+ terminal: false
442
+ });
443
+ }
444
+ this.stepRecords.set(name, {
445
+ completedAt: Date.now(),
446
+ durationMs: 0,
447
+ name,
448
+ output,
449
+ parentNames: [],
450
+ startedAt: Date.now(),
451
+ status: "success"
452
+ });
453
+ this.stepOutputs.set(name, output);
454
+ }
455
+ }
456
+ this.result = new Promise((resolve) => {
457
+ this.resolveResult = resolve;
458
+ });
459
+ this.startExecution();
460
+ }
461
+ /**
462
+ * Read-only snapshot of current step records.
463
+ */
464
+ get steps() {
465
+ return Array.from(this.stepRecords.values());
466
+ }
467
+ /**
468
+ * JSON-serializable snapshot of the current flow + run state.
469
+ */
470
+ snapshot() {
471
+ return createSnapshot({
472
+ flow: this.flow,
473
+ groupRuntimes: this.groupRuntimes,
474
+ run: {
475
+ completedAt: void 0,
476
+ runId: this.runId,
477
+ startedAt: this.startedAt,
478
+ status: this.status
479
+ },
480
+ stepRecords: this.stepRecords
481
+ });
482
+ }
483
+ /**
484
+ * Abort the run — signals all in-progress steps.
485
+ */
486
+ abort() {
487
+ this.abortController.abort();
488
+ }
489
+ /**
490
+ * Add a step dynamically during execution.
491
+ */
492
+ addStep(def) {
493
+ this.stepDefs.set(def.name, def);
494
+ const record = {
495
+ name: def.name,
496
+ parentNames: def.parentNames,
497
+ status: "pending"
498
+ };
499
+ this.stepRecords.set(def.name, record);
500
+ this.fireHook("onStepAdded", record);
501
+ this.dynamicStepsAdded = true;
502
+ if (!this.loopRunning && this.status === "running") {
503
+ this.executionLoop();
504
+ }
505
+ }
506
+ /**
507
+ * Spawn members into a group and auto-seal it.
508
+ */
509
+ spawnGroup(groupName, members) {
510
+ const grp = this.groupRuntimes.get(groupName);
511
+ if (!grp) {
512
+ throw new Error(`Unknown group "${groupName}"`);
513
+ }
514
+ addGroupMembers(grp, members, true);
515
+ this.startOrDeferGroup(grp);
516
+ }
517
+ /**
518
+ * Add a single member to a group (without sealing).
519
+ */
520
+ addGroupMember(groupName, member) {
521
+ const grp = this.groupRuntimes.get(groupName);
522
+ if (!grp) {
523
+ throw new Error(`Unknown group "${groupName}"`);
524
+ }
525
+ addGroupMembers(grp, [member], false);
526
+ this.startOrDeferGroup(grp);
527
+ }
528
+ /**
529
+ * Seal a group — no more members can be added.
530
+ */
531
+ sealGroup(groupName) {
532
+ const group2 = this.groupRuntimes.get(groupName);
533
+ if (!group2) {
534
+ throw new Error(`Unknown group "${groupName}"`);
535
+ }
536
+ group2.sealed = true;
537
+ this.checkGroupCompletion(group2);
538
+ }
539
+ /**
540
+ * Provide a step implementation (slot fill) or external data.
541
+ */
542
+ provide(path, value) {
543
+ if (typeof value === "function" || isExecutableValue(value)) {
544
+ this.providedSlots.set(path, value);
545
+ } else {
546
+ this.providedData.set(path, value);
547
+ }
548
+ }
549
+ // --- Private helpers ---
550
+ createContext() {
551
+ const runHandle = {
552
+ addGroupMember: (groupName, member) => this.addGroupMember(groupName, member),
553
+ provide: (path, value) => this.provide(path, value),
554
+ sealGroup: (groupName) => this.sealGroup(groupName),
555
+ spawnGroup: (groupName, members) => this.spawnGroup(groupName, members)
556
+ };
557
+ return {
558
+ history: this.history,
559
+ provided() {
560
+ return void 0;
561
+ },
562
+ run: runHandle,
563
+ runId: this.runId,
564
+ signal: this.abortController.signal
565
+ };
566
+ }
567
+ // --- Private execution logic ---
568
+ async startExecution() {
569
+ this.status = "running";
570
+ this.startedAt = Date.now();
571
+ await this.executionLoop();
572
+ }
573
+ async executionLoop() {
574
+ if (this.loopRunning) return;
575
+ this.loopRunning = true;
576
+ try {
577
+ while (!this.abortController.signal.aborted) {
578
+ this.dynamicStepsAdded = false;
579
+ this.markSkippableSteps();
580
+ const allDefs = Array.from(this.stepDefs.values());
581
+ const statuses = this.getStatusMap();
582
+ const readyNormal = getReadySteps(allDefs, statuses);
583
+ const readyWithFailures = getReadyWithFailures(allDefs, statuses);
584
+ const ready = [.../* @__PURE__ */ new Set([...readyNormal, ...readyWithFailures])];
585
+ if (ready.length === 0) {
586
+ const hasRunning = Array.from(this.stepRecords.values()).some(
587
+ (r) => r.status === "running"
588
+ );
589
+ const hasPendingGroups = Array.from(this.groupRuntimes.values()).some(
590
+ (g) => !isGroupComplete(g) && g.members.length > 0
591
+ );
592
+ if (!hasRunning && !hasPendingGroups && !this.dynamicStepsAdded) {
593
+ break;
594
+ }
595
+ if (!this.dynamicStepsAdded) {
596
+ await new Promise((resolve) => setTimeout(resolve, 1));
597
+ continue;
598
+ }
599
+ continue;
600
+ }
601
+ await new Promise((resolveWave) => {
602
+ let pending = ready.length;
603
+ for (const name of ready) {
604
+ this.executeStep(name).finally(() => {
605
+ pending--;
606
+ if (this.failFast && this.hasAnyError()) {
607
+ this.abortController.abort();
608
+ }
609
+ this.scheduleReadySteps();
610
+ if (pending === 0) resolveWave();
611
+ });
612
+ }
613
+ });
614
+ }
615
+ } finally {
616
+ this.loopRunning = false;
617
+ }
618
+ this.completeRun();
619
+ }
620
+ /**
621
+ * Check for and launch any newly ready steps without waiting for the full wave.
622
+ * Called after each individual step completion.
623
+ */
624
+ scheduleReadySteps() {
625
+ this.markSkippableSteps();
626
+ this.flushPendingGroups();
627
+ const allDefs = Array.from(this.stepDefs.values());
628
+ const statuses = this.getStatusMap();
629
+ const readyNormal = getReadySteps(allDefs, statuses);
630
+ const readyWithFailures = getReadyWithFailures(allDefs, statuses);
631
+ const ready = [.../* @__PURE__ */ new Set([...readyNormal, ...readyWithFailures])];
632
+ for (const name of ready) {
633
+ this.executeStep(name).finally(() => {
634
+ this.scheduleReadySteps();
635
+ });
636
+ }
637
+ }
638
+ async executeStep(name) {
639
+ const def = this.stepDefs.get(name);
640
+ const record = this.stepRecords.get(name);
641
+ record.status = "running";
642
+ record.startedAt = Date.now();
643
+ this.fireHook("onStepStart", record);
644
+ try {
645
+ const input = this.buildInput(def);
646
+ const ctx = this.createContext();
647
+ const output = await invokeHandler(def.handler, input, ctx);
648
+ record.status = "success";
649
+ record.completedAt = Date.now();
650
+ record.durationMs = record.completedAt - record.startedAt;
651
+ record.output = output;
652
+ this.stepOutputs.set(name, output);
653
+ this.fireHook("onStepComplete", record);
654
+ this.flushPendingGroups();
655
+ } catch (error) {
656
+ record.status = "error";
657
+ record.completedAt = Date.now();
658
+ record.durationMs = record.completedAt - record.startedAt;
659
+ record.error = error instanceof Error ? error.message : String(error);
660
+ this.fireHook("onStepError", record);
661
+ }
662
+ }
663
+ buildInput(def) {
664
+ const input = {};
665
+ for (const parentName of def.parentNames) {
666
+ const groupRuntime = this.groupRuntimes.get(parentName);
667
+ if (groupRuntime) {
668
+ input[parentName] = def.allowFailures ? collectGroupOutputs(groupRuntime) : collectSuccessOutputs(groupRuntime);
669
+ continue;
670
+ }
671
+ if (def.allowFailures) {
672
+ const parentRecord = this.stepRecords.get(parentName);
673
+ if (parentRecord?.status === "success") {
674
+ input[parentName] = {
675
+ status: "success",
676
+ value: this.stepOutputs.get(parentName)
677
+ };
678
+ } else if (parentRecord?.status === "error") {
679
+ input[parentName] = {
680
+ error: parentRecord.error ?? "Unknown error",
681
+ status: "error"
682
+ };
683
+ } else {
684
+ input[parentName] = {
685
+ status: "skipped"
686
+ };
687
+ }
688
+ } else {
689
+ input[parentName] = this.stepOutputs.get(parentName);
690
+ }
691
+ }
692
+ if (def.transform) {
693
+ return def.transform(input);
694
+ }
695
+ return input;
696
+ }
697
+ markSkippableSteps() {
698
+ const allDefs = Array.from(this.stepDefs.values());
699
+ const statuses = this.getStatusMap();
700
+ let changed = true;
701
+ while (changed) {
702
+ changed = false;
703
+ const skippable = getSkippableSteps(allDefs, statuses);
704
+ for (const name of skippable) {
705
+ const record = this.stepRecords.get(name);
706
+ record.status = "skipped";
707
+ statuses.set(name, "skipped");
708
+ changed = true;
709
+ }
710
+ }
711
+ }
712
+ getStatusMap() {
713
+ const map = /* @__PURE__ */ new Map();
714
+ for (const [name, record] of this.stepRecords) {
715
+ map.set(name, record.status);
716
+ }
717
+ for (const [name, group2] of this.groupRuntimes) {
718
+ if (isGroupComplete(group2)) {
719
+ const allSuccess = group2.members.every((m) => m.status === "success");
720
+ map.set(name, allSuccess ? "success" : "error");
721
+ } else if (group2.members.some((m) => m.status === "running")) {
722
+ map.set(name, "running");
723
+ } else if (group2.members.length > 0) {
724
+ map.set(name, "running");
725
+ } else {
726
+ map.set(name, "pending");
727
+ }
728
+ }
729
+ return map;
730
+ }
731
+ hasAnyError() {
732
+ for (const record of this.stepRecords.values()) {
733
+ if (record.status === "error") return true;
734
+ }
735
+ return false;
736
+ }
737
+ areGroupDependenciesMet(group2) {
738
+ for (const dep of group2.dependsOn) {
739
+ const record = this.stepRecords.get(dep);
740
+ if (!record || record.status !== "success") return false;
741
+ }
742
+ return true;
743
+ }
744
+ startOrDeferGroup(grp) {
745
+ if (this.areGroupDependenciesMet(grp)) {
746
+ this.startGroupMembers(grp);
747
+ } else {
748
+ if (!this.pendingGroupStarts.includes(grp)) {
749
+ this.pendingGroupStarts.push(grp);
750
+ }
751
+ }
752
+ }
753
+ flushPendingGroups() {
754
+ const toFlush = [...this.pendingGroupStarts];
755
+ this.pendingGroupStarts.length = 0;
756
+ for (const grp of toFlush) {
757
+ if (this.areGroupDependenciesMet(grp)) {
758
+ this.startGroupMembers(grp);
759
+ } else {
760
+ this.pendingGroupStarts.push(grp);
761
+ }
762
+ }
763
+ }
764
+ startGroupMembers(group2) {
765
+ if (!this.areGroupDependenciesMet(group2)) return;
766
+ const parentInput = {};
767
+ for (const dep of group2.dependsOn) {
768
+ parentInput[dep] = this.stepOutputs.get(dep);
769
+ }
770
+ const ctx = this.createContext();
771
+ for (const member of group2.members) {
772
+ if (member.status !== "pending") continue;
773
+ executeGroupMember(
774
+ member,
775
+ parentInput,
776
+ ctx,
777
+ this.stepOutputs,
778
+ () => this.fireHook("onChange", void 0)
779
+ ).then(() => {
780
+ this.checkGroupCompletion(group2);
781
+ }).catch(() => {
782
+ this.checkGroupCompletion(group2);
783
+ });
784
+ }
785
+ }
786
+ checkGroupCompletion(group2) {
787
+ if (isGroupComplete(group2)) {
788
+ this.dynamicStepsAdded = true;
789
+ this.fireHook("onChange", void 0);
790
+ }
791
+ }
792
+ completeRun() {
793
+ const terminalSteps = Array.from(this.stepDefs.values()).filter(
794
+ (d) => d.terminal
795
+ );
796
+ const stepsToCheck = terminalSteps.length > 0 ? terminalSteps.map((d) => this.stepRecords.get(d.name)) : Array.from(this.stepRecords.values());
797
+ const hasError = stepsToCheck.some((r) => r.status === "error");
798
+ this.status = hasError ? "error" : "success";
799
+ const result = {
800
+ completedAt: Date.now(),
801
+ history: this.history.snapshot(),
802
+ runId: this.runId,
803
+ startedAt: this.startedAt,
804
+ status: this.status,
805
+ steps: Array.from(this.stepRecords.values())
806
+ };
807
+ this.fireHook("onRunComplete", result);
808
+ this.resolveResult(result);
809
+ }
810
+ fireHook(hookName, arg) {
811
+ try {
812
+ const hook = this.hooks[hookName];
813
+ if (hook) {
814
+ hook(arg);
815
+ }
816
+ if (hookName !== "onChange" && this.hooks.onChange) {
817
+ this.hooks.onChange();
818
+ }
819
+ } catch {
820
+ }
821
+ }
822
+ };
823
+ function isExecutableValue(value) {
824
+ return typeof value === "object" && value !== null && "execute" in value && typeof value.execute === "function";
825
+ }
826
+
827
+ // src/flow.ts
828
+ var Flow = class {
829
+ name;
830
+ steps;
831
+ groups;
832
+ constructor(name, steps, groups) {
833
+ const groupNodes = groups.map((g) => ({
834
+ name: g.name,
835
+ parentNames: g.dependsOn
836
+ }));
837
+ validateAcyclic([...steps, ...groupNodes]);
838
+ this.name = name;
839
+ this.steps = Object.freeze([...steps]);
840
+ this.groups = Object.freeze([...groups]);
841
+ }
842
+ /**
843
+ * Creates a new FlowRun instance.
844
+ * Multiple runs can execute concurrently from the same Flow.
845
+ */
846
+ run(options) {
847
+ return new FlowRun(this, options);
848
+ }
849
+ };
850
+
851
+ // src/flow-builder.ts
852
+ var FlowBuilder = class _FlowBuilder {
853
+ flowName;
854
+ steps;
855
+ groups;
856
+ knownNames;
857
+ constructor(flowName, steps = [], groups = [], knownNames) {
858
+ this.flowName = flowName;
859
+ this.steps = steps;
860
+ this.groups = groups;
861
+ this.knownNames = knownNames ?? new Set(steps.map((s) => s.name));
862
+ }
863
+ // Implementation
864
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
865
+ step(name, parentsOrHandler, handlerOrOptions) {
866
+ if (this.knownNames.has(name)) {
867
+ throw new DuplicateStepError(name);
868
+ }
869
+ let parentNames = [];
870
+ let handler;
871
+ let allowFailures = false;
872
+ let terminal = false;
873
+ let transform;
874
+ if (Array.isArray(parentsOrHandler)) {
875
+ parentNames = parentsOrHandler.map((p) => {
876
+ if (isGroupRef(p)) return p.groupName;
877
+ return p;
878
+ });
879
+ if (typeof handlerOrOptions === "object" && handlerOrOptions !== null && "execute" in handlerOrOptions) {
880
+ const opts = handlerOrOptions;
881
+ handler = opts.execute;
882
+ allowFailures = opts.allowFailures ?? false;
883
+ terminal = opts.terminal ?? false;
884
+ transform = opts.transform;
885
+ } else {
886
+ handler = handlerOrOptions;
887
+ }
888
+ } else {
889
+ handler = parentsOrHandler;
890
+ }
891
+ for (const parent of parentNames) {
892
+ if (!this.knownNames.has(parent)) {
893
+ throw new UnknownParentError(name, parent);
894
+ }
895
+ }
896
+ const newSteps = [
897
+ ...this.steps,
898
+ {
899
+ allowFailures,
900
+ handler,
901
+ name,
902
+ parentNames,
903
+ terminal,
904
+ transform
905
+ }
906
+ ];
907
+ const newKnown = new Set(this.knownNames);
908
+ newKnown.add(name);
909
+ return new _FlowBuilder(this.flowName, newSteps, [...this.groups], newKnown);
910
+ }
911
+ /**
912
+ * Declare a typed group for dynamic fan-in.
913
+ * .group<TOut>("reviews", { dependsOn: ["arbiter"] })
914
+ */
915
+ group(name, options = {}) {
916
+ if (this.knownNames.has(name)) {
917
+ throw new DuplicateStepError(name);
918
+ }
919
+ const dependsOn = options.dependsOn ?? [];
920
+ for (const dep of dependsOn) {
921
+ if (!this.knownNames.has(dep)) {
922
+ throw new UnknownParentError(name, dep);
923
+ }
924
+ }
925
+ const newGroups = [...this.groups, { dependsOn, name }];
926
+ const newKnown = new Set(this.knownNames);
927
+ newKnown.add(name);
928
+ return new _FlowBuilder(
929
+ this.flowName,
930
+ [...this.steps],
931
+ newGroups,
932
+ newKnown
933
+ );
934
+ }
935
+ /**
936
+ * Validates the DAG and returns an immutable Flow.
937
+ */
938
+ build() {
939
+ return new Flow(this.flowName, this.steps, this.groups);
940
+ }
941
+ };
942
+ function flow(name) {
943
+ return new FlowBuilder(name);
944
+ }
945
+
946
+ // src/pipeline.ts
947
+ function pipeline(name, steps) {
948
+ if (steps.length === 0) {
949
+ throw new Error(`Pipeline "${name}" must have at least one step`);
950
+ }
951
+ let builder = flow(`${name}`);
952
+ for (let i = 0; i < steps.length; i++) {
953
+ const step = steps[i];
954
+ const handler = step.execute ?? (async (input) => input);
955
+ const isLast = i === steps.length - 1;
956
+ if (i === 0) {
957
+ builder = isLast ? builder.step(step.name, {
958
+ execute: handler,
959
+ terminal: true
960
+ }) : builder.step(step.name, handler);
961
+ } else {
962
+ const parents = [steps[i - 1].name];
963
+ builder = isLast ? builder.step(step.name, parents, {
964
+ execute: handler,
965
+ terminal: true
966
+ }) : builder.step(step.name, parents, handler);
967
+ }
968
+ }
969
+ return { flow: builder.build(), name };
970
+ }
971
+ export {
972
+ CycleDetectedError,
973
+ DuplicateStepError,
974
+ Flow,
975
+ FlowBuilder,
976
+ FlowRun,
977
+ History,
978
+ StepExecutionError,
979
+ UnfilledSlotError,
980
+ UnknownParentError,
981
+ UnsealedGroupError,
982
+ createSnapshot,
983
+ flow,
984
+ getReadySteps,
985
+ getReadyWithFailures,
986
+ getSkippableSteps,
987
+ group,
988
+ invokeHandler,
989
+ isExecutable,
990
+ isGroupRef,
991
+ isSubgraphMember,
992
+ pipeline,
993
+ topologicalSort,
994
+ validateAcyclic
995
+ };
996
+ //# sourceMappingURL=index.js.map