pipeai 0.8.4 → 0.9.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/README.md +26 -0
- package/dist/index.cjs +412 -514
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +230 -127
- package/dist/index.d.ts +230 -127
- package/dist/index.js +412 -514
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -24,7 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
Agent: () => Agent,
|
|
25
25
|
CHECKPOINT_STEP_ID: () => CHECKPOINT_STEP_ID,
|
|
26
26
|
GATE_RESUME_STEP_ID: () => GATE_RESUME_STEP_ID,
|
|
27
|
-
SKIP: () =>
|
|
27
|
+
SKIP: () => SKIP2,
|
|
28
28
|
TOOL_PROVIDER_BRAND: () => TOOL_PROVIDER_BRAND,
|
|
29
29
|
ToolProvider: () => ToolProvider,
|
|
30
30
|
Workflow: () => Workflow,
|
|
@@ -52,6 +52,7 @@ function runWithWriter(writer, fn) {
|
|
|
52
52
|
function getActiveWriter() {
|
|
53
53
|
return writerStorage.getStore();
|
|
54
54
|
}
|
|
55
|
+
var SKIP = /* @__PURE__ */ Symbol("pipeai.foreach.skip");
|
|
55
56
|
function resolveValue(value, ctx, input) {
|
|
56
57
|
if (typeof value === "function") {
|
|
57
58
|
return value(ctx, input);
|
|
@@ -291,7 +292,15 @@ var Agent = class {
|
|
|
291
292
|
const resolved = await this.resolveConfig(ctx, input);
|
|
292
293
|
const options = this.buildCallOptions(resolved, ctx, input);
|
|
293
294
|
try {
|
|
294
|
-
const onErrorOption = this.config.onError ? {
|
|
295
|
+
const onErrorOption = this.config.onError ? {
|
|
296
|
+
onError: async ({ error }) => {
|
|
297
|
+
try {
|
|
298
|
+
await this.invokeOnError(error, ctx, input);
|
|
299
|
+
} catch (handlerError) {
|
|
300
|
+
console.error(`Agent "${this.id}": onError handler threw on the stream path:`, handlerError);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} : {};
|
|
295
304
|
return (0, import_ai2.streamText)({
|
|
296
305
|
...options,
|
|
297
306
|
...extra,
|
|
@@ -390,12 +399,19 @@ var import_ai3 = require("ai");
|
|
|
390
399
|
|
|
391
400
|
// src/steps/step.ts
|
|
392
401
|
var Step = class {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Observability event subtype for `type: "step"` nodes (agent / transform =
|
|
404
|
+
* `"step"`; nested / branch / foreach / repeat / parallel override).
|
|
405
|
+
* `undefined` on gate / catch / finally nodes, whose `type` IS the event type.
|
|
406
|
+
*/
|
|
407
|
+
category;
|
|
408
|
+
/**
|
|
409
|
+
* The sealed sub-workflow attached to this node, when it has one (`nested`,
|
|
410
|
+
* and workflow-target `foreach` / `repeat`). Consumed by the recursive
|
|
411
|
+
* `stepShapeHash` walk and the resume path-walk in `loadState`.
|
|
412
|
+
*/
|
|
413
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
414
|
+
nestedWorkflow;
|
|
399
415
|
/**
|
|
400
416
|
* Precedence source tag a kind writes to `state.pendingError` when it
|
|
401
417
|
* captures a thrown body error. Defaults to `"step"`; kinds with a distinct
|
|
@@ -403,19 +419,20 @@ var Step = class {
|
|
|
403
419
|
*/
|
|
404
420
|
errorSource = "step";
|
|
405
421
|
/**
|
|
406
|
-
* The step's body
|
|
407
|
-
* it to do its work
|
|
408
|
-
* `state.output` is the input on entry and becomes the output on
|
|
409
|
-
* `state.writer` is present in stream mode. The base implementation is
|
|
410
|
-
* no-op so kinds that carry no body of their own need not override it.
|
|
422
|
+
* The step's body, invoked by the run loop only after {@link shouldSkip}
|
|
423
|
+
* returned `false`. Each kind overrides it to do its work and capture errors
|
|
424
|
+
* onto state. `state.output` is the input on entry and becomes the output on
|
|
425
|
+
* exit; `state.writer` is present in stream mode. The base implementation is
|
|
426
|
+
* a no-op so kinds that carry no body of their own need not override it.
|
|
411
427
|
*/
|
|
412
428
|
async execute(_state) {
|
|
413
429
|
}
|
|
414
430
|
/**
|
|
415
|
-
* Run-policy gate
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
431
|
+
* Run-policy gate, called by the run loop before {@link execute}: return
|
|
432
|
+
* `true` when this step should be skipped silently (no hooks, no output
|
|
433
|
+
* change). The default is the "normal" policy — skip while the flow is
|
|
434
|
+
* suspended or already in error. Overridden by kinds with inverted policies:
|
|
435
|
+
* `catch` runs only when there's an error, `finally` always runs.
|
|
419
436
|
*/
|
|
420
437
|
shouldSkip(state) {
|
|
421
438
|
return !!state.suspension || !!state.pendingError;
|
|
@@ -451,7 +468,6 @@ var TransformStep = class extends Step {
|
|
|
451
468
|
this.options = options;
|
|
452
469
|
}
|
|
453
470
|
async execute(state) {
|
|
454
|
-
if (this.shouldSkip(state)) return;
|
|
455
471
|
try {
|
|
456
472
|
if (await this.applyConditionalSkip(state, this.options)) return;
|
|
457
473
|
state.output = await this.fn({
|
|
@@ -482,7 +498,6 @@ var AgentStep = class _AgentStep extends Step {
|
|
|
482
498
|
this.options = options;
|
|
483
499
|
}
|
|
484
500
|
async execute(state) {
|
|
485
|
-
if (this.shouldSkip(state)) return;
|
|
486
501
|
try {
|
|
487
502
|
if (await this.applyConditionalSkip(state, this.options)) return;
|
|
488
503
|
await _AgentStep.runAgent(state, this.agent, state.ctx, this.options);
|
|
@@ -553,6 +568,26 @@ var AgentStep = class _AgentStep extends Step {
|
|
|
553
568
|
}
|
|
554
569
|
};
|
|
555
570
|
|
|
571
|
+
// src/errors.ts
|
|
572
|
+
var WorkflowBranchError = class extends Error {
|
|
573
|
+
constructor(branchType, message) {
|
|
574
|
+
super(message);
|
|
575
|
+
this.branchType = branchType;
|
|
576
|
+
this.name = "WorkflowBranchError";
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
var WorkflowLoopError = class extends Error {
|
|
580
|
+
constructor(iterations, maxIterations) {
|
|
581
|
+
super(`Loop exceeded maximum iterations (${maxIterations})`);
|
|
582
|
+
this.iterations = iterations;
|
|
583
|
+
this.maxIterations = maxIterations;
|
|
584
|
+
this.name = "WorkflowLoopError";
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
var CHECKPOINT_STEP_ID = "::pipeai::onCheckpoint";
|
|
588
|
+
var ABORT_STEP_ID = "::pipeai::abort";
|
|
589
|
+
var GATE_RESUME_STEP_ID = "::pipeai::gate:resume";
|
|
590
|
+
|
|
556
591
|
// src/steps/branch-step.ts
|
|
557
592
|
var PredicateBranchStep = class extends Step {
|
|
558
593
|
type = "step";
|
|
@@ -567,7 +602,6 @@ var PredicateBranchStep = class extends Step {
|
|
|
567
602
|
this.cases = cases;
|
|
568
603
|
}
|
|
569
604
|
async execute(state) {
|
|
570
|
-
if (this.shouldSkip(state)) return;
|
|
571
605
|
try {
|
|
572
606
|
const ctx = state.ctx;
|
|
573
607
|
const input = state.output;
|
|
@@ -606,7 +640,6 @@ var SelectBranchStep = class extends Step {
|
|
|
606
640
|
this.config = config;
|
|
607
641
|
}
|
|
608
642
|
async execute(state) {
|
|
609
|
-
if (this.shouldSkip(state)) return;
|
|
610
643
|
try {
|
|
611
644
|
const ctx = state.ctx;
|
|
612
645
|
const input = state.output;
|
|
@@ -641,6 +674,95 @@ var SelectBranchStep = class extends Step {
|
|
|
641
674
|
}
|
|
642
675
|
};
|
|
643
676
|
|
|
677
|
+
// src/runtime.ts
|
|
678
|
+
function makeAbortError(signal) {
|
|
679
|
+
return {
|
|
680
|
+
error: signal.reason ?? new Error("Workflow aborted"),
|
|
681
|
+
stepId: ABORT_STEP_ID,
|
|
682
|
+
source: "step"
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function prependNestedPath(snapshot, index, state) {
|
|
686
|
+
const next = { ...snapshot, nestedPath: [index, ...snapshot.nestedPath ?? []] };
|
|
687
|
+
if (resolveFreezeSnapshots(state)) deepFreeze(next);
|
|
688
|
+
return next;
|
|
689
|
+
}
|
|
690
|
+
function resolveFreezeSnapshots(state) {
|
|
691
|
+
return state.runOptions?.freezeSnapshots ? true : false;
|
|
692
|
+
}
|
|
693
|
+
function pendingErrorSourceToStepType(source) {
|
|
694
|
+
switch (source) {
|
|
695
|
+
case "step":
|
|
696
|
+
return "step";
|
|
697
|
+
case "gate":
|
|
698
|
+
return "gate";
|
|
699
|
+
case "finally":
|
|
700
|
+
return "finally";
|
|
701
|
+
case "catch":
|
|
702
|
+
return "catch";
|
|
703
|
+
case "onCheckpoint":
|
|
704
|
+
return "step";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async function emitCheckpoint(state, opts, resumeFromIndex, stepShapeHash) {
|
|
708
|
+
if (!opts.onCheckpoint) return;
|
|
709
|
+
const willFreeze = resolveFreezeSnapshots(state);
|
|
710
|
+
const snap = {
|
|
711
|
+
version: 2,
|
|
712
|
+
kind: "checkpoint",
|
|
713
|
+
resumeFromIndex,
|
|
714
|
+
output: willFreeze ? structuredClone(state.output) : state.output,
|
|
715
|
+
stepShapeHash
|
|
716
|
+
};
|
|
717
|
+
if (willFreeze) deepFreeze(snap);
|
|
718
|
+
await opts.onCheckpoint(snap, { signal: state.abortSignal });
|
|
719
|
+
}
|
|
720
|
+
var warnedStreamOnErrorOnSuspend = false;
|
|
721
|
+
function pushWarning(state, source, stepId, error) {
|
|
722
|
+
(state.warnings ??= []).push({ source, stepId, error });
|
|
723
|
+
}
|
|
724
|
+
function fireHook(observability, state, name, event) {
|
|
725
|
+
const hook = observability?.[name];
|
|
726
|
+
if (!hook) return void 0;
|
|
727
|
+
return fireHookSlow(state, name, event, hook);
|
|
728
|
+
}
|
|
729
|
+
async function fireHookSlow(state, name, event, hook) {
|
|
730
|
+
try {
|
|
731
|
+
await hook(event);
|
|
732
|
+
return void 0;
|
|
733
|
+
} catch (e) {
|
|
734
|
+
if (name !== "onStepError") {
|
|
735
|
+
const stepId = event.stepId;
|
|
736
|
+
pushWarning(state, name, stepId, e);
|
|
737
|
+
console.error(`pipeai: ${name} hook threw for stepId "${stepId}":`, e);
|
|
738
|
+
}
|
|
739
|
+
return e;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function hasItemHooks(observability) {
|
|
743
|
+
return !!observability && !!(observability.onItemStart || observability.onItemFinish || observability.onItemError);
|
|
744
|
+
}
|
|
745
|
+
function demotePendingError(state, pe) {
|
|
746
|
+
pushWarning(state, pe.source, pe.stepId, pe.error);
|
|
747
|
+
}
|
|
748
|
+
function maybeWarnStreamOnErrorOnSuspend(result, options) {
|
|
749
|
+
if (result.status !== "suspended" || !options?.onError || warnedStreamOnErrorOnSuspend) return;
|
|
750
|
+
warnedStreamOnErrorOnSuspend = true;
|
|
751
|
+
console.warn(
|
|
752
|
+
"pipeai: stream() with options.onError suspended at a gate \u2014 onError will NOT be invoked for suspension. Discriminate via the resolved output Promise."
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
function makeRuntimeState(ctx, output, mode, opts, writer) {
|
|
756
|
+
return {
|
|
757
|
+
ctx,
|
|
758
|
+
output,
|
|
759
|
+
mode,
|
|
760
|
+
...writer ? { writer } : {},
|
|
761
|
+
runOptions: opts,
|
|
762
|
+
abortSignal: opts?.abortSignal
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
644
766
|
// src/steps/semaphore.ts
|
|
645
767
|
var Semaphore = class {
|
|
646
768
|
available;
|
|
@@ -663,17 +785,103 @@ var Semaphore = class {
|
|
|
663
785
|
this.available++;
|
|
664
786
|
}
|
|
665
787
|
}
|
|
666
|
-
async run(fn) {
|
|
667
|
-
await this.acquire();
|
|
668
|
-
try {
|
|
669
|
-
return await fn();
|
|
670
|
-
} finally {
|
|
671
|
-
this.release();
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
788
|
};
|
|
675
789
|
|
|
676
790
|
// src/steps/concurrent.ts
|
|
791
|
+
function validateConcurrency(kind, value) {
|
|
792
|
+
if (value !== void 0 && !(Number.isInteger(value) && value >= 1 || value === Infinity)) {
|
|
793
|
+
throw new Error(`${kind}: concurrency must be a positive integer or Infinity, got ${value}`);
|
|
794
|
+
}
|
|
795
|
+
return value ?? Infinity;
|
|
796
|
+
}
|
|
797
|
+
async function dispatchUnits(params) {
|
|
798
|
+
const { state, stepId, kind, units, observability, handleStream, onUnitSuccess } = params;
|
|
799
|
+
const unitStates = new Array(units.length);
|
|
800
|
+
const wantItemHooks = hasItemHooks(observability);
|
|
801
|
+
const executeUnit = async (unit, index) => {
|
|
802
|
+
const inheritStreaming = unit.isWorkflow || handleStream !== void 0;
|
|
803
|
+
const unitState = {
|
|
804
|
+
ctx: state.ctx,
|
|
805
|
+
output: unit.input,
|
|
806
|
+
mode: inheritStreaming ? state.mode : "generate",
|
|
807
|
+
writer: inheritStreaming ? state.writer : void 0,
|
|
808
|
+
abortSignal: state.abortSignal
|
|
809
|
+
};
|
|
810
|
+
unitStates[index] = unitState;
|
|
811
|
+
const unitStart = wantItemHooks ? performance.now() : 0;
|
|
812
|
+
if (wantItemHooks) {
|
|
813
|
+
await fireHook(observability, state, "onItemStart", {
|
|
814
|
+
stepId,
|
|
815
|
+
type: kind,
|
|
816
|
+
itemIndex: unit.key,
|
|
817
|
+
ctx: state.ctx,
|
|
818
|
+
input: unit.input
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
if (unit.isWorkflow) {
|
|
823
|
+
await unit.target.executeAsNested(unitState);
|
|
824
|
+
} else {
|
|
825
|
+
await AgentStep.runAgent(
|
|
826
|
+
unitState,
|
|
827
|
+
unit.target,
|
|
828
|
+
state.ctx,
|
|
829
|
+
handleStream ? { handleStream } : void 0,
|
|
830
|
+
unit.key
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
onUnitSuccess(index, unitState.output);
|
|
834
|
+
if (wantItemHooks) {
|
|
835
|
+
await fireHook(observability, state, "onItemFinish", {
|
|
836
|
+
stepId,
|
|
837
|
+
type: kind,
|
|
838
|
+
itemIndex: unit.key,
|
|
839
|
+
ctx: state.ctx,
|
|
840
|
+
output: unitState.output,
|
|
841
|
+
durationMs: performance.now() - unitStart
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
} catch (error) {
|
|
845
|
+
if (wantItemHooks) {
|
|
846
|
+
await fireHook(observability, state, "onItemError", {
|
|
847
|
+
stepId,
|
|
848
|
+
type: kind,
|
|
849
|
+
itemIndex: unit.key,
|
|
850
|
+
ctx: state.ctx,
|
|
851
|
+
error,
|
|
852
|
+
durationMs: performance.now() - unitStart
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
throw error;
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
const sem = new Semaphore(params.concurrency);
|
|
859
|
+
const failures = [];
|
|
860
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
861
|
+
for (let i = 0; i < units.length; i++) {
|
|
862
|
+
if (state.abortSignal?.aborted) break;
|
|
863
|
+
await sem.acquire();
|
|
864
|
+
if (state.abortSignal?.aborted) {
|
|
865
|
+
sem.release();
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
const index = i;
|
|
869
|
+
const unit = (async () => {
|
|
870
|
+
try {
|
|
871
|
+
await executeUnit(units[index], index);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
failures.push({ key: units[index].key, index, error });
|
|
874
|
+
} finally {
|
|
875
|
+
sem.release();
|
|
876
|
+
}
|
|
877
|
+
})();
|
|
878
|
+
inflight.add(unit);
|
|
879
|
+
void unit.finally(() => inflight.delete(unit));
|
|
880
|
+
}
|
|
881
|
+
await Promise.all(inflight);
|
|
882
|
+
failures.sort((a, b) => a.index - b.index);
|
|
883
|
+
return reconcileUnits(state, stepId, failures, units.length, (i) => units[i].key, unitStates, state.abortSignal);
|
|
884
|
+
}
|
|
677
885
|
function reconcileUnits(state, id, failures, count, keyAt, unitStates, signal) {
|
|
678
886
|
for (let i = 0; i < count; i++) {
|
|
679
887
|
const us = unitStates[i];
|
|
@@ -701,128 +909,50 @@ function reconcileUnits(state, id, failures, count, keyAt, unitStates, signal) {
|
|
|
701
909
|
var ForeachStep = class extends Step {
|
|
702
910
|
type = "step";
|
|
703
911
|
category = "foreach";
|
|
704
|
-
id;
|
|
705
912
|
nestedWorkflow;
|
|
913
|
+
id;
|
|
706
914
|
target;
|
|
707
915
|
concurrency;
|
|
708
916
|
onError;
|
|
709
917
|
handleStream;
|
|
710
918
|
isWorkflow;
|
|
711
|
-
inheritStreaming;
|
|
712
919
|
observability;
|
|
713
920
|
constructor(target, options, observability) {
|
|
714
921
|
super();
|
|
715
|
-
if (options?.concurrency !== void 0 && !(Number.isInteger(options.concurrency) && options.concurrency >= 1 || options.concurrency === Infinity)) {
|
|
716
|
-
throw new Error(`foreach: concurrency must be a positive integer or Infinity, got ${options.concurrency}`);
|
|
717
|
-
}
|
|
718
922
|
this.target = target;
|
|
719
|
-
this.concurrency = options?.concurrency
|
|
923
|
+
this.concurrency = validateConcurrency("foreach", options?.concurrency);
|
|
720
924
|
this.onError = options?.onError;
|
|
721
925
|
this.handleStream = options?.handleStream;
|
|
722
926
|
this.observability = observability;
|
|
723
927
|
this.isWorkflow = target instanceof SealedWorkflow;
|
|
724
|
-
this.inheritStreaming = this.isWorkflow || this.handleStream !== void 0;
|
|
725
928
|
const defaultId = this.isWorkflow ? target.id ?? "foreach" : `foreach:${target.id}`;
|
|
726
929
|
this.id = options?.id ?? defaultId;
|
|
727
930
|
this.nestedWorkflow = this.isWorkflow ? target : void 0;
|
|
728
931
|
}
|
|
729
932
|
async execute(state) {
|
|
730
|
-
if (this.shouldSkip(state)) return;
|
|
731
933
|
try {
|
|
732
934
|
const items = state.output;
|
|
733
935
|
if (!Array.isArray(items)) {
|
|
734
936
|
throw new Error(`foreach "${this.id}": expected array input, got ${typeof items}`);
|
|
735
937
|
}
|
|
736
938
|
const results = new Array(items.length);
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
};
|
|
748
|
-
itemStates[index] = itemState;
|
|
749
|
-
const itemStart = wantItemHooks ? performance.now() : 0;
|
|
750
|
-
if (wantItemHooks) {
|
|
751
|
-
await fireHook(this.observability, state, "onItemStart", {
|
|
752
|
-
stepId: this.id,
|
|
753
|
-
type: "foreach",
|
|
754
|
-
itemIndex: index,
|
|
755
|
-
ctx: state.ctx,
|
|
756
|
-
input: item
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
try {
|
|
760
|
-
if (this.isWorkflow) {
|
|
761
|
-
await this.target.executeAsNested(itemState);
|
|
762
|
-
} else {
|
|
763
|
-
await AgentStep.runAgent(
|
|
764
|
-
itemState,
|
|
765
|
-
this.target,
|
|
766
|
-
state.ctx,
|
|
767
|
-
this.handleStream ? { handleStream: this.handleStream } : void 0,
|
|
768
|
-
index
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
results[index] = itemState.output;
|
|
772
|
-
if (wantItemHooks) {
|
|
773
|
-
await fireHook(this.observability, state, "onItemFinish", {
|
|
774
|
-
stepId: this.id,
|
|
775
|
-
type: "foreach",
|
|
776
|
-
itemIndex: index,
|
|
777
|
-
ctx: state.ctx,
|
|
778
|
-
output: itemState.output,
|
|
779
|
-
durationMs: performance.now() - itemStart
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
} catch (error) {
|
|
783
|
-
if (wantItemHooks) {
|
|
784
|
-
await fireHook(this.observability, state, "onItemError", {
|
|
785
|
-
stepId: this.id,
|
|
786
|
-
type: "foreach",
|
|
787
|
-
itemIndex: index,
|
|
788
|
-
ctx: state.ctx,
|
|
789
|
-
error,
|
|
790
|
-
durationMs: performance.now() - itemStart
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
throw error;
|
|
794
|
-
}
|
|
795
|
-
};
|
|
796
|
-
const sem = new Semaphore(this.concurrency);
|
|
797
|
-
const failures = [];
|
|
798
|
-
const inflight = /* @__PURE__ */ new Set();
|
|
799
|
-
for (let i = 0; i < items.length; i++) {
|
|
800
|
-
if (state.abortSignal?.aborted) break;
|
|
801
|
-
await sem.acquire();
|
|
802
|
-
if (state.abortSignal?.aborted) {
|
|
803
|
-
sem.release();
|
|
804
|
-
break;
|
|
939
|
+
const failures = await dispatchUnits({
|
|
940
|
+
state,
|
|
941
|
+
stepId: this.id,
|
|
942
|
+
kind: "foreach",
|
|
943
|
+
units: items.map((item, i) => ({ key: i, input: item, target: this.target, isWorkflow: this.isWorkflow })),
|
|
944
|
+
concurrency: this.concurrency,
|
|
945
|
+
observability: this.observability,
|
|
946
|
+
handleStream: this.handleStream,
|
|
947
|
+
onUnitSuccess: (index, output) => {
|
|
948
|
+
results[index] = output;
|
|
805
949
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
await executeItem(items[index], index);
|
|
810
|
-
} catch (error) {
|
|
811
|
-
failures.push({ key: index, index, error });
|
|
812
|
-
} finally {
|
|
813
|
-
sem.release();
|
|
814
|
-
}
|
|
815
|
-
})();
|
|
816
|
-
inflight.add(unit);
|
|
817
|
-
void unit.finally(() => inflight.delete(unit));
|
|
818
|
-
}
|
|
819
|
-
await Promise.all(inflight);
|
|
820
|
-
failures.sort((a, b) => a.index - b.index);
|
|
821
|
-
const nonGateFailures = reconcileUnits(state, this.id, failures, items.length, (i) => i, itemStates, state.abortSignal);
|
|
822
|
-
for (const { index, error } of nonGateFailures) {
|
|
950
|
+
});
|
|
951
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
952
|
+
for (const { index, error } of failures) {
|
|
823
953
|
if (!this.onError) throw error;
|
|
824
954
|
const recovered = await this.onError({ error, item: items[index], index, ctx: state.ctx });
|
|
825
|
-
if (recovered ===
|
|
955
|
+
if (recovered === SKIP) {
|
|
826
956
|
skipped.add(index);
|
|
827
957
|
} else {
|
|
828
958
|
results[index] = recovered;
|
|
@@ -842,7 +972,6 @@ var ParallelStep = class extends Step {
|
|
|
842
972
|
id;
|
|
843
973
|
entries;
|
|
844
974
|
isTuple;
|
|
845
|
-
branchCount;
|
|
846
975
|
concurrency;
|
|
847
976
|
onError;
|
|
848
977
|
handleStream;
|
|
@@ -850,112 +979,30 @@ var ParallelStep = class extends Step {
|
|
|
850
979
|
constructor(branches, options, observability) {
|
|
851
980
|
super();
|
|
852
981
|
this.isTuple = Array.isArray(branches);
|
|
853
|
-
this.entries = this.isTuple ? branches.map((target, i) => ({ key: i, index: i, target })) : Object.entries(branches).map(([k, t], i) => ({ key: k, index: i, target: t }));
|
|
854
|
-
this.
|
|
855
|
-
const requestedConcurrency = options?.concurrency;
|
|
856
|
-
if (requestedConcurrency !== void 0 && !(Number.isInteger(requestedConcurrency) && requestedConcurrency >= 1 || requestedConcurrency === Infinity)) {
|
|
857
|
-
throw new Error(`parallel: concurrency must be a positive integer or Infinity, got ${requestedConcurrency}`);
|
|
858
|
-
}
|
|
859
|
-
this.concurrency = requestedConcurrency ?? Infinity;
|
|
982
|
+
this.entries = this.isTuple ? branches.map((target, i) => ({ key: i, index: i, target, isWorkflow: target instanceof SealedWorkflow })) : Object.entries(branches).map(([k, t], i) => ({ key: k, index: i, target: t, isWorkflow: t instanceof SealedWorkflow }));
|
|
983
|
+
this.concurrency = validateConcurrency("parallel", options?.concurrency);
|
|
860
984
|
this.onError = options?.onError;
|
|
861
985
|
this.handleStream = options?.handleStream;
|
|
862
986
|
this.observability = observability;
|
|
863
987
|
this.id = options?.id ?? (this.isTuple ? "parallel:tuple" : "parallel:record");
|
|
864
988
|
}
|
|
865
989
|
async execute(state) {
|
|
866
|
-
if (this.shouldSkip(state)) return;
|
|
867
990
|
try {
|
|
868
991
|
const input = state.output;
|
|
869
|
-
const results = this.isTuple ? new Array(this.
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
abortSignal: state.abortSignal
|
|
881
|
-
};
|
|
882
|
-
branchStates[index] = branchState;
|
|
883
|
-
const branchStart = wantItemHooks ? performance.now() : 0;
|
|
884
|
-
const itemIndex = this.isTuple ? index : key;
|
|
885
|
-
if (wantItemHooks) {
|
|
886
|
-
await fireHook(this.observability, state, "onItemStart", {
|
|
887
|
-
stepId: this.id,
|
|
888
|
-
type: "parallel",
|
|
889
|
-
itemIndex,
|
|
890
|
-
ctx: state.ctx,
|
|
891
|
-
input
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
try {
|
|
895
|
-
if (isWorkflowBranch) {
|
|
896
|
-
await target.executeAsNested(branchState);
|
|
897
|
-
} else {
|
|
898
|
-
await AgentStep.runAgent(
|
|
899
|
-
branchState,
|
|
900
|
-
target,
|
|
901
|
-
state.ctx,
|
|
902
|
-
this.handleStream ? { handleStream: this.handleStream } : void 0,
|
|
903
|
-
itemIndex
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
results[key] = branchState.output;
|
|
907
|
-
if (wantItemHooks) {
|
|
908
|
-
await fireHook(this.observability, state, "onItemFinish", {
|
|
909
|
-
stepId: this.id,
|
|
910
|
-
type: "parallel",
|
|
911
|
-
itemIndex,
|
|
912
|
-
ctx: state.ctx,
|
|
913
|
-
output: branchState.output,
|
|
914
|
-
durationMs: performance.now() - branchStart
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
} catch (error) {
|
|
918
|
-
if (wantItemHooks) {
|
|
919
|
-
await fireHook(this.observability, state, "onItemError", {
|
|
920
|
-
stepId: this.id,
|
|
921
|
-
type: "parallel",
|
|
922
|
-
itemIndex,
|
|
923
|
-
ctx: state.ctx,
|
|
924
|
-
error,
|
|
925
|
-
durationMs: performance.now() - branchStart
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
throw error;
|
|
929
|
-
}
|
|
930
|
-
};
|
|
931
|
-
const keyAt = (i) => this.entries[i].key;
|
|
932
|
-
const sem = new Semaphore(this.concurrency);
|
|
933
|
-
const failures = [];
|
|
934
|
-
const inflight = /* @__PURE__ */ new Set();
|
|
935
|
-
for (let i = 0; i < this.branchCount; i++) {
|
|
936
|
-
if (state.abortSignal?.aborted) break;
|
|
937
|
-
await sem.acquire();
|
|
938
|
-
if (state.abortSignal?.aborted) {
|
|
939
|
-
sem.release();
|
|
940
|
-
break;
|
|
992
|
+
const results = this.isTuple ? new Array(this.entries.length) : {};
|
|
993
|
+
const failures = await dispatchUnits({
|
|
994
|
+
state,
|
|
995
|
+
stepId: this.id,
|
|
996
|
+
kind: "parallel",
|
|
997
|
+
units: this.entries.map((e) => ({ key: e.key, input, target: e.target, isWorkflow: e.isWorkflow })),
|
|
998
|
+
concurrency: this.concurrency,
|
|
999
|
+
observability: this.observability,
|
|
1000
|
+
handleStream: this.handleStream,
|
|
1001
|
+
onUnitSuccess: (index, output) => {
|
|
1002
|
+
results[this.entries[index].key] = output;
|
|
941
1003
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
try {
|
|
945
|
-
await executeBranch(this.entries[index]);
|
|
946
|
-
} catch (error) {
|
|
947
|
-
failures.push({ key: keyAt(index), index, error });
|
|
948
|
-
} finally {
|
|
949
|
-
sem.release();
|
|
950
|
-
}
|
|
951
|
-
})();
|
|
952
|
-
inflight.add(unit);
|
|
953
|
-
void unit.finally(() => inflight.delete(unit));
|
|
954
|
-
}
|
|
955
|
-
await Promise.all(inflight);
|
|
956
|
-
failures.sort((a, b) => a.index - b.index);
|
|
957
|
-
const nonGateFailures = reconcileUnits(state, this.id, failures, this.branchCount, keyAt, branchStates, state.abortSignal);
|
|
958
|
-
for (const { key, index, error } of nonGateFailures) {
|
|
1004
|
+
});
|
|
1005
|
+
for (const { key, index, error } of failures) {
|
|
959
1006
|
if (!this.onError) throw error;
|
|
960
1007
|
const recovered = await this.onError({
|
|
961
1008
|
error,
|
|
@@ -963,11 +1010,7 @@ var ParallelStep = class extends Step {
|
|
|
963
1010
|
index: this.isTuple ? index : void 0,
|
|
964
1011
|
ctx: state.ctx
|
|
965
1012
|
});
|
|
966
|
-
|
|
967
|
-
results[key] = void 0;
|
|
968
|
-
} else {
|
|
969
|
-
results[key] = recovered;
|
|
970
|
-
}
|
|
1013
|
+
results[key] = recovered === SKIP ? void 0 : recovered;
|
|
971
1014
|
}
|
|
972
1015
|
state.output = results;
|
|
973
1016
|
} catch (error) {
|
|
@@ -987,25 +1030,25 @@ var GateStep = class extends Step {
|
|
|
987
1030
|
merge;
|
|
988
1031
|
payload;
|
|
989
1032
|
condition;
|
|
990
|
-
constructor(id,
|
|
1033
|
+
constructor(id, options) {
|
|
991
1034
|
super();
|
|
992
1035
|
this.id = id;
|
|
993
|
-
this.payload = payload;
|
|
994
|
-
this.schema = schema;
|
|
995
|
-
this.condition = condition;
|
|
996
|
-
this.merge = merge;
|
|
1036
|
+
this.payload = options?.payload;
|
|
1037
|
+
this.schema = options?.schema;
|
|
1038
|
+
this.condition = options?.condition;
|
|
1039
|
+
this.merge = options?.merge;
|
|
997
1040
|
}
|
|
998
1041
|
async execute(state) {
|
|
999
|
-
if (this.shouldSkip(state)) return;
|
|
1000
1042
|
try {
|
|
1001
|
-
|
|
1043
|
+
const params = { ctx: state.ctx, input: state.output };
|
|
1044
|
+
if (this.condition && !await this.condition(params)) return;
|
|
1002
1045
|
const snapshot = {
|
|
1003
1046
|
version: 2,
|
|
1004
1047
|
kind: "gate",
|
|
1005
1048
|
resumeFromIndex: state.stepIndex ?? -1,
|
|
1006
1049
|
output: state.output,
|
|
1007
1050
|
gateId: this.id,
|
|
1008
|
-
gatePayload: await this.payload(state
|
|
1051
|
+
gatePayload: this.payload ? await this.payload(params) : state.output
|
|
1009
1052
|
};
|
|
1010
1053
|
state.suspension = snapshot;
|
|
1011
1054
|
if (resolveFreezeSnapshots(state)) deepFreeze(snapshot);
|
|
@@ -1031,7 +1074,6 @@ var CatchStep = class extends Step {
|
|
|
1031
1074
|
return !!state.suspension || !state.pendingError || !!state.checkpointFailed;
|
|
1032
1075
|
}
|
|
1033
1076
|
async execute(state) {
|
|
1034
|
-
if (this.shouldSkip(state)) return;
|
|
1035
1077
|
const handled = state.pendingError;
|
|
1036
1078
|
state.output = await this.catchFn({
|
|
1037
1079
|
error: handled.error,
|
|
@@ -1059,7 +1101,6 @@ var FinallyStep = class extends Step {
|
|
|
1059
1101
|
return false;
|
|
1060
1102
|
}
|
|
1061
1103
|
async execute(state) {
|
|
1062
|
-
if (this.shouldSkip(state)) return;
|
|
1063
1104
|
await this.fn({ ctx: state.ctx });
|
|
1064
1105
|
}
|
|
1065
1106
|
};
|
|
@@ -1068,8 +1109,8 @@ var FinallyStep = class extends Step {
|
|
|
1068
1109
|
var NestedWorkflowStep = class extends Step {
|
|
1069
1110
|
type = "step";
|
|
1070
1111
|
category = "nested";
|
|
1071
|
-
id;
|
|
1072
1112
|
nestedWorkflow;
|
|
1113
|
+
id;
|
|
1073
1114
|
options;
|
|
1074
1115
|
constructor(id, workflow, options) {
|
|
1075
1116
|
super();
|
|
@@ -1096,7 +1137,6 @@ var NestedWorkflowStep = class extends Step {
|
|
|
1096
1137
|
}
|
|
1097
1138
|
return;
|
|
1098
1139
|
}
|
|
1099
|
-
if (this.shouldSkip(state)) return;
|
|
1100
1140
|
const myIndex = state.stepIndex ?? -1;
|
|
1101
1141
|
try {
|
|
1102
1142
|
if (await this.applyConditionalSkip(state, this.options)) return;
|
|
@@ -1112,8 +1152,8 @@ var NestedWorkflowStep = class extends Step {
|
|
|
1112
1152
|
var RepeatStep = class extends Step {
|
|
1113
1153
|
type = "step";
|
|
1114
1154
|
category = "repeat";
|
|
1115
|
-
id;
|
|
1116
1155
|
nestedWorkflow;
|
|
1156
|
+
id;
|
|
1117
1157
|
target;
|
|
1118
1158
|
predicate;
|
|
1119
1159
|
maxIterations;
|
|
@@ -1128,7 +1168,6 @@ var RepeatStep = class extends Step {
|
|
|
1128
1168
|
this.nestedWorkflow = isWorkflow ? target : void 0;
|
|
1129
1169
|
}
|
|
1130
1170
|
async execute(state) {
|
|
1131
|
-
if (this.shouldSkip(state)) return;
|
|
1132
1171
|
try {
|
|
1133
1172
|
const ctx = state.ctx;
|
|
1134
1173
|
for (let i = 1; i <= this.maxIterations; i++) {
|
|
@@ -1150,107 +1189,7 @@ var RepeatStep = class extends Step {
|
|
|
1150
1189
|
}
|
|
1151
1190
|
};
|
|
1152
1191
|
|
|
1153
|
-
// src/runtime.ts
|
|
1154
|
-
function resolveFreezeSnapshots(state) {
|
|
1155
|
-
return state.runOptions?.freezeSnapshots ? true : false;
|
|
1156
|
-
}
|
|
1157
|
-
function pendingErrorSourceToStepType(source) {
|
|
1158
|
-
switch (source) {
|
|
1159
|
-
case "step":
|
|
1160
|
-
return "step";
|
|
1161
|
-
case "gate":
|
|
1162
|
-
return "gate";
|
|
1163
|
-
case "finally":
|
|
1164
|
-
return "finally";
|
|
1165
|
-
case "catch":
|
|
1166
|
-
return "catch";
|
|
1167
|
-
case "onCheckpoint":
|
|
1168
|
-
return "step";
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
async function emitCheckpoint(state, opts, resumeFromIndex, stepShapeHash) {
|
|
1172
|
-
if (!opts.onCheckpoint) return;
|
|
1173
|
-
const willFreeze = resolveFreezeSnapshots(state);
|
|
1174
|
-
const snap = {
|
|
1175
|
-
version: 2,
|
|
1176
|
-
kind: "checkpoint",
|
|
1177
|
-
resumeFromIndex,
|
|
1178
|
-
output: willFreeze ? structuredClone(state.output) : state.output,
|
|
1179
|
-
stepShapeHash
|
|
1180
|
-
};
|
|
1181
|
-
if (willFreeze) deepFreeze(snap);
|
|
1182
|
-
await opts.onCheckpoint(snap, { signal: state.abortSignal });
|
|
1183
|
-
}
|
|
1184
|
-
var warnedStreamOnErrorOnSuspend = false;
|
|
1185
|
-
function pushWarning(state, source, stepId, error) {
|
|
1186
|
-
(state.warnings ??= []).push({ source, stepId, error });
|
|
1187
|
-
}
|
|
1188
|
-
function fireHook(observability, state, name, event) {
|
|
1189
|
-
const hook = observability?.[name];
|
|
1190
|
-
if (!hook) return void 0;
|
|
1191
|
-
return fireHookSlow(state, name, event, hook);
|
|
1192
|
-
}
|
|
1193
|
-
async function fireHookSlow(state, name, event, hook) {
|
|
1194
|
-
try {
|
|
1195
|
-
await hook(event);
|
|
1196
|
-
return void 0;
|
|
1197
|
-
} catch (e) {
|
|
1198
|
-
if (name !== "onStepError") {
|
|
1199
|
-
const stepId = event.stepId;
|
|
1200
|
-
pushWarning(state, name, stepId, e);
|
|
1201
|
-
console.error(`pipeai: ${name} hook threw for stepId "${stepId}":`, e);
|
|
1202
|
-
}
|
|
1203
|
-
return e;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
function hasItemHooks(observability) {
|
|
1207
|
-
return !!observability && !!(observability.onItemStart || observability.onItemFinish || observability.onItemError);
|
|
1208
|
-
}
|
|
1209
|
-
function demotePendingError(state, pe) {
|
|
1210
|
-
pushWarning(state, pe.source, pe.stepId, pe.error);
|
|
1211
|
-
}
|
|
1212
|
-
function maybeWarnStreamOnErrorOnSuspend(result, options) {
|
|
1213
|
-
if (result.status !== "suspended" || !options?.onError || warnedStreamOnErrorOnSuspend) return;
|
|
1214
|
-
warnedStreamOnErrorOnSuspend = true;
|
|
1215
|
-
console.warn(
|
|
1216
|
-
"pipeai: stream() with options.onError suspended at a gate \u2014 onError will NOT be invoked for suspension. Discriminate via the resolved output Promise."
|
|
1217
|
-
);
|
|
1218
|
-
}
|
|
1219
|
-
function makeRuntimeState(ctx, output, mode, opts, writer) {
|
|
1220
|
-
return {
|
|
1221
|
-
ctx,
|
|
1222
|
-
output,
|
|
1223
|
-
mode,
|
|
1224
|
-
...writer ? { writer } : {},
|
|
1225
|
-
runOptions: opts,
|
|
1226
|
-
abortSignal: opts?.abortSignal
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
1192
|
// src/workflow.ts
|
|
1231
|
-
var WorkflowBranchError = class extends Error {
|
|
1232
|
-
constructor(branchType, message) {
|
|
1233
|
-
super(message);
|
|
1234
|
-
this.branchType = branchType;
|
|
1235
|
-
this.name = "WorkflowBranchError";
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
var WorkflowLoopError = class extends Error {
|
|
1239
|
-
constructor(iterations, maxIterations) {
|
|
1240
|
-
super(`Loop exceeded maximum iterations (${maxIterations})`);
|
|
1241
|
-
this.iterations = iterations;
|
|
1242
|
-
this.maxIterations = maxIterations;
|
|
1243
|
-
this.name = "WorkflowLoopError";
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
var CHECKPOINT_STEP_ID = "::pipeai::onCheckpoint";
|
|
1247
|
-
var ABORT_STEP_ID = "::pipeai::abort";
|
|
1248
|
-
var GATE_RESUME_STEP_ID = "::pipeai::gate:resume";
|
|
1249
|
-
function prependNestedPath(snapshot, index, state) {
|
|
1250
|
-
const next = { ...snapshot, nestedPath: [index, ...snapshot.nestedPath ?? []] };
|
|
1251
|
-
if (resolveFreezeSnapshots(state)) deepFreeze(next);
|
|
1252
|
-
return next;
|
|
1253
|
-
}
|
|
1254
1193
|
function migrateSnapshot(legacy) {
|
|
1255
1194
|
if (legacy.version !== 1) {
|
|
1256
1195
|
throw new Error(`migrateSnapshot: expected v1 snapshot, got version ${legacy.version}`);
|
|
@@ -1264,30 +1203,15 @@ function migrateSnapshot(legacy) {
|
|
|
1264
1203
|
gatePayload: legacy.gatePayload
|
|
1265
1204
|
};
|
|
1266
1205
|
}
|
|
1267
|
-
function getObservabilityType(node) {
|
|
1268
|
-
if (node.type !== "step") return node.type;
|
|
1269
|
-
return node.category ?? "step";
|
|
1270
|
-
}
|
|
1271
|
-
function getNestedWorkflows(node) {
|
|
1272
|
-
switch (node.type) {
|
|
1273
|
-
case "step":
|
|
1274
|
-
return node.nestedWorkflow ? [node.nestedWorkflow] : [];
|
|
1275
|
-
case "gate":
|
|
1276
|
-
case "catch":
|
|
1277
|
-
case "finally":
|
|
1278
|
-
return [];
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
1206
|
var SealedWorkflow = class _SealedWorkflow {
|
|
1282
1207
|
id;
|
|
1283
1208
|
steps;
|
|
1284
1209
|
observability;
|
|
1285
1210
|
// Memoized — see ensureDuplicateCheck().
|
|
1286
1211
|
duplicateCheckPassed = false;
|
|
1287
|
-
// Memoized lazily per terminal instance
|
|
1288
|
-
//
|
|
1289
|
-
|
|
1290
|
-
_cachedCheckpointableStepCount;
|
|
1212
|
+
// Memoized lazily per terminal instance: the executable / checkpointable step
|
|
1213
|
+
// counts (one walk) and the recursive shape hash (separate — it's expensive).
|
|
1214
|
+
_stepCounts;
|
|
1291
1215
|
_cachedStepShapeHash;
|
|
1292
1216
|
constructor(steps, id, observability) {
|
|
1293
1217
|
this.steps = steps;
|
|
@@ -1322,39 +1246,26 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1322
1246
|
}
|
|
1323
1247
|
this.duplicateCheckPassed = true;
|
|
1324
1248
|
}
|
|
1325
|
-
// ── shape-hash
|
|
1249
|
+
// ── step counts + shape-hash (memoized) ────────────────────────────
|
|
1326
1250
|
/**
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
* `type
|
|
1251
|
+
* Two cadence inputs from a single walk:
|
|
1252
|
+
* - `executable` — nodes that aren't `catch` / `finally`. A graph-size
|
|
1253
|
+
* proxy for the catastrophe threshold in {@link validateRunOptions}.
|
|
1254
|
+
* - `checkpointable` — `type === "step"` nodes only (this includes
|
|
1255
|
+
* branch / foreach / repeat / parallel / nested). Drives the checkpoint
|
|
1256
|
+
* auto-cadence denominator: gates suspend/skip and never reach the
|
|
1257
|
+
* checkpoint block, so counting them would dilute the "~4 checkpoints
|
|
1258
|
+
* across the run" target.
|
|
1331
1259
|
*/
|
|
1332
|
-
get
|
|
1333
|
-
if (this.
|
|
1334
|
-
let
|
|
1260
|
+
get stepCounts() {
|
|
1261
|
+
if (this._stepCounts) return this._stepCounts;
|
|
1262
|
+
let executable = 0;
|
|
1263
|
+
let checkpointable = 0;
|
|
1335
1264
|
for (const s of this.steps) {
|
|
1336
|
-
if (s.type !== "catch" && s.type !== "finally")
|
|
1265
|
+
if (s.type !== "catch" && s.type !== "finally") executable++;
|
|
1266
|
+
if (s.type === "step") checkpointable++;
|
|
1337
1267
|
}
|
|
1338
|
-
this.
|
|
1339
|
-
return n;
|
|
1340
|
-
}
|
|
1341
|
-
/**
|
|
1342
|
-
* Count of *checkpointable* nodes — `type === "step"` only (this includes
|
|
1343
|
-
* `branch`/`foreach`/`repeat`/`parallel`/`nested`, all internally `step`).
|
|
1344
|
-
* Drives the checkpoint auto-cadence denominator. Distinct from
|
|
1345
|
-
* {@link cachedExecutableStepCount}, which also counts `gate` nodes: gates
|
|
1346
|
-
* suspend/skip and never reach the checkpoint block, so the runtime
|
|
1347
|
-
* `executableStepsSeen` counter never advances on them. Counting gates in
|
|
1348
|
-
* the denominator would dilute the "~4 checkpoints across the run" target.
|
|
1349
|
-
*/
|
|
1350
|
-
get cachedCheckpointableStepCount() {
|
|
1351
|
-
if (this._cachedCheckpointableStepCount !== void 0) return this._cachedCheckpointableStepCount;
|
|
1352
|
-
let n = 0;
|
|
1353
|
-
for (const s of this.steps) {
|
|
1354
|
-
if (s.type === "step") n++;
|
|
1355
|
-
}
|
|
1356
|
-
this._cachedCheckpointableStepCount = n;
|
|
1357
|
-
return n;
|
|
1268
|
+
return this._stepCounts = { executable, checkpointable };
|
|
1358
1269
|
}
|
|
1359
1270
|
/** @internal — used by `computeStepShapeHash` to descend nested workflows. */
|
|
1360
1271
|
getStepsForShapeHash() {
|
|
@@ -1362,7 +1273,7 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1362
1273
|
}
|
|
1363
1274
|
get cachedStepShapeHash() {
|
|
1364
1275
|
if (this._cachedStepShapeHash !== void 0) return this._cachedStepShapeHash;
|
|
1365
|
-
const getNested = (node) =>
|
|
1276
|
+
const getNested = (node) => node.nestedWorkflow ? [node.nestedWorkflow] : [];
|
|
1366
1277
|
this._cachedStepShapeHash = computeStepShapeHash(
|
|
1367
1278
|
this.steps,
|
|
1368
1279
|
getNested
|
|
@@ -1373,21 +1284,29 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1373
1284
|
* Validate user-provided RunOptions before a run begins. Throws on
|
|
1374
1285
|
* outright errors and on the loud-disaster combo (`freezeSnapshots: true
|
|
1375
1286
|
* + checkpointEvery: 1` on a workflow of 8+ steps). Warns once on the
|
|
1376
|
-
* merely-suspicious combo (`freezeSnapshots: true + cadence <= 2`)
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1287
|
+
* merely-suspicious combo (`freezeSnapshots: true + cadence <= 2`), and on
|
|
1288
|
+
* checkpoint-cadence options set without an `onCheckpoint` sink (a no-op
|
|
1289
|
+
* that usually signals a forgotten sink).
|
|
1379
1290
|
*/
|
|
1380
1291
|
validateRunOptions(opts) {
|
|
1381
1292
|
if (!opts) return;
|
|
1382
|
-
if (!opts.onCheckpoint) return;
|
|
1383
1293
|
if (opts.checkpointEvery !== void 0 && opts.checkpointWhen !== void 0) {
|
|
1384
1294
|
throw new Error("RunOptions: checkpointEvery and checkpointWhen are mutually exclusive");
|
|
1385
1295
|
}
|
|
1386
1296
|
if (opts.checkpointEvery !== void 0 && (!Number.isInteger(opts.checkpointEvery) || opts.checkpointEvery < 1)) {
|
|
1387
1297
|
throw new Error(`RunOptions: checkpointEvery must be a positive integer, got ${opts.checkpointEvery}`);
|
|
1388
1298
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1299
|
+
if (!opts.onCheckpoint) {
|
|
1300
|
+
if (opts.checkpointEvery !== void 0 || opts.checkpointWhen !== void 0) {
|
|
1301
|
+
warnOnce(
|
|
1302
|
+
"pipeai:checkpoint-without-sink",
|
|
1303
|
+
"pipeai: checkpointEvery/checkpointWhen set without onCheckpoint \u2014 no checkpoints will fire. Did you forget the onCheckpoint sink?"
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const length = this.stepCounts.executable;
|
|
1309
|
+
const cadence = opts.checkpointEvery ?? Math.max(1, Math.ceil(this.stepCounts.checkpointable / 4));
|
|
1391
1310
|
if (opts.freezeSnapshots && opts.freezeSnapshots !== "iAcceptThePerformanceCost" && cadence === 1 && length >= 8) {
|
|
1392
1311
|
throw new Error(
|
|
1393
1312
|
`freezeSnapshots+checkpointEvery:1 on a ${length}-step workflow is reliably catastrophic. Set checkpointEvery >= 5, freezeSnapshots: false, or pass "iAcceptThePerformanceCost".`
|
|
@@ -1401,6 +1320,13 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1401
1320
|
}
|
|
1402
1321
|
}
|
|
1403
1322
|
// ── Observability helpers ─────────────────────────────────────
|
|
1323
|
+
/** Observability event `type` for a node: a `type: "step"` node reports its
|
|
1324
|
+
* `category` (agent / transform default to `"step"`); every other node's
|
|
1325
|
+
* `type` IS the event type. */
|
|
1326
|
+
obsEventType(node) {
|
|
1327
|
+
if (node.type !== "step") return node.type;
|
|
1328
|
+
return node.category ?? "step";
|
|
1329
|
+
}
|
|
1404
1330
|
/**
|
|
1405
1331
|
* Fire an observability hook safely. Returns `undefined` synchronously when
|
|
1406
1332
|
* no hook is registered — avoiding the promise wrapper + microtask that an
|
|
@@ -1414,17 +1340,14 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1414
1340
|
* Returns the hook's thrown error if any; undefined otherwise. Callers
|
|
1415
1341
|
* `await` the result — `await undefined` is sync, so the no-hook path
|
|
1416
1342
|
* stays allocation-free.
|
|
1343
|
+
*
|
|
1344
|
+
* Thin delegate to the free `fireHook` (which takes an explicit
|
|
1345
|
+
* observability), kept as a method so the loop's many `this.fireHook` call
|
|
1346
|
+
* sites stay unchanged.
|
|
1417
1347
|
*/
|
|
1418
|
-
// Thin delegates to the free `fireHook` / `hasItemHooks` (which take an
|
|
1419
|
-
// explicit observability). Kept as methods so the loop's many `this.fireHook`
|
|
1420
|
-
// call sites stay unchanged; bare `fireHook` / `hasItemHooks` below resolve to
|
|
1421
|
-
// the module-level functions, not these members.
|
|
1422
1348
|
fireHook(state, name, event) {
|
|
1423
1349
|
return fireHook(this.observability, state, name, event);
|
|
1424
1350
|
}
|
|
1425
|
-
hasItemHooks() {
|
|
1426
|
-
return hasItemHooks(this.observability);
|
|
1427
|
-
}
|
|
1428
1351
|
/**
|
|
1429
1352
|
* Fire `onStepError` for a step-body failure and honor the documented
|
|
1430
1353
|
* cause-attachment contract uniformly across every firing path (step, gate,
|
|
@@ -1529,30 +1452,15 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1529
1452
|
if (opts !== void 0 && state.runOptions === void 0) {
|
|
1530
1453
|
state.runOptions = opts;
|
|
1531
1454
|
}
|
|
1532
|
-
const ckptCadence = opts?.onCheckpoint && opts.checkpointWhen === void 0 ? opts.checkpointEvery ?? Math.max(1, Math.ceil(this.
|
|
1533
|
-
|
|
1455
|
+
const ckptCadence = opts?.onCheckpoint && opts.checkpointWhen === void 0 ? opts.checkpointEvery ?? Math.max(1, Math.ceil(this.stepCounts.checkpointable / 4)) : 0;
|
|
1456
|
+
const ckptCounter = { seen: 0 };
|
|
1534
1457
|
state.pendingError = initialError ?? void 0;
|
|
1535
|
-
|
|
1536
|
-
const makeAbortError = (signal) => ({
|
|
1537
|
-
error: signal.reason ?? new Error("Workflow aborted"),
|
|
1538
|
-
stepId: ABORT_STEP_ID,
|
|
1539
|
-
source: "step"
|
|
1540
|
-
});
|
|
1458
|
+
const abortState = { promoted: false };
|
|
1541
1459
|
for (let i = startIndex; i < this.steps.length; i++) {
|
|
1542
|
-
|
|
1543
|
-
if (!abortPromoted) {
|
|
1544
|
-
abortPromoted = true;
|
|
1545
|
-
state.suspension = void 0;
|
|
1546
|
-
if (state.pendingError) demotePendingError(state, state.pendingError);
|
|
1547
|
-
state.pendingError = makeAbortError(state.abortSignal);
|
|
1548
|
-
} else if (!state.pendingError) {
|
|
1549
|
-
state.pendingError = makeAbortError(state.abortSignal);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1460
|
+
this.promoteAbort(state, abortState);
|
|
1552
1461
|
const node = this.steps[i];
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
const obsType = getObservabilityType(node);
|
|
1462
|
+
if (node.shouldSkip(state)) continue;
|
|
1463
|
+
const obsType = this.obsEventType(node);
|
|
1556
1464
|
const stepId = node.id;
|
|
1557
1465
|
const sStart = performance.now();
|
|
1558
1466
|
const errBefore = state.pendingError;
|
|
@@ -1572,7 +1480,9 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1572
1480
|
throw e;
|
|
1573
1481
|
}
|
|
1574
1482
|
const newError = state.pendingError && state.pendingError !== errBefore ? state.pendingError : null;
|
|
1575
|
-
|
|
1483
|
+
const isAbort = !!newError && state.abortSignal?.aborted === true && newError.error === state.abortSignal.reason;
|
|
1484
|
+
if (isAbort) {
|
|
1485
|
+
} else if (newError) {
|
|
1576
1486
|
await this.fireStepErrorAndAttachCause(state, {
|
|
1577
1487
|
stepId,
|
|
1578
1488
|
type: obsType,
|
|
@@ -1595,32 +1505,73 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1595
1505
|
state.suspension = void 0;
|
|
1596
1506
|
throw new Error(`internal: suspension bubbled from non-gate step "${node.id}" (gate "${leaked.gateId}").`);
|
|
1597
1507
|
}
|
|
1598
|
-
|
|
1599
|
-
executableStepsSeen++;
|
|
1600
|
-
const shouldCheckpoint = opts.checkpointWhen ? opts.checkpointWhen({ stepIndex: i, stepId: node.id, ctx: state.ctx }) : executableStepsSeen % ckptCadence === 0;
|
|
1601
|
-
if (shouldCheckpoint) {
|
|
1602
|
-
const ckptStart = performance.now();
|
|
1603
|
-
try {
|
|
1604
|
-
await emitCheckpoint(
|
|
1605
|
-
state,
|
|
1606
|
-
opts,
|
|
1607
|
-
i + 1,
|
|
1608
|
-
this.cachedStepShapeHash
|
|
1609
|
-
);
|
|
1610
|
-
} catch (e) {
|
|
1611
|
-
state.pendingError = { error: e, stepId: CHECKPOINT_STEP_ID, source: "onCheckpoint" };
|
|
1612
|
-
state.checkpointFailed = true;
|
|
1613
|
-
await this.fireStepErrorAndAttachCause(state, {
|
|
1614
|
-
stepId: CHECKPOINT_STEP_ID,
|
|
1615
|
-
type: "step",
|
|
1616
|
-
ctx: state.ctx,
|
|
1617
|
-
error: e,
|
|
1618
|
-
durationMs: performance.now() - ckptStart
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1508
|
+
await this.maybeCheckpoint(state, opts, node, i, ckptCadence, ckptCounter);
|
|
1623
1509
|
}
|
|
1510
|
+
await this.settleRun(state, abortState.promoted);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Promote a fired abort signal into `state.pendingError` at an iteration
|
|
1514
|
+
* boundary. First observation discards any in-progress suspension (the caller
|
|
1515
|
+
* asked to stop) and preserves a genuinely-different prior step error as a
|
|
1516
|
+
* warning — but NOT one that is itself the abort reason (a nested workflow /
|
|
1517
|
+
* concurrent unit that already rethrew it), which would surface a phantom
|
|
1518
|
+
* step-failure warning. Subsequent iterations only re-promote if a downstream
|
|
1519
|
+
* catch cleared pendingError — `AbortSignal.aborted` is sticky, so the
|
|
1520
|
+
* workflow must not resume mid-pipeline just because a catch swallowed one
|
|
1521
|
+
* observation.
|
|
1522
|
+
*/
|
|
1523
|
+
promoteAbort(state, abortState) {
|
|
1524
|
+
const signal = state.abortSignal;
|
|
1525
|
+
if (!signal?.aborted) return;
|
|
1526
|
+
if (!abortState.promoted) {
|
|
1527
|
+
abortState.promoted = true;
|
|
1528
|
+
state.suspension = void 0;
|
|
1529
|
+
const prior = state.pendingError;
|
|
1530
|
+
if (prior && prior.error !== signal.reason) demotePendingError(state, prior);
|
|
1531
|
+
state.pendingError = makeAbortError(signal);
|
|
1532
|
+
} else if (!state.pendingError) {
|
|
1533
|
+
state.pendingError = makeAbortError(signal);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Emit a checkpoint after a successful `type:"step"` body. Skipped on
|
|
1538
|
+
* pendingError (no clean state to snapshot), on suspension (gate already
|
|
1539
|
+
* won), and for catch/finally/gate nodes (not checkpointable). Numeric
|
|
1540
|
+
* `checkpointEvery` (default: `max(1, ceil(count/4))`) uses the loop-hoisted
|
|
1541
|
+
* `ckptCadence`; the predicate form runs per step. A `when:false`-skipped
|
|
1542
|
+
* `type:"step"` node returns normally (its body never ran) and still reaches
|
|
1543
|
+
* here — it advances the counter and can itself be a checkpoint boundary,
|
|
1544
|
+
* keeping the cadence denominator (`stepCounts.checkpointable`) consistent
|
|
1545
|
+
* with the runtime counter.
|
|
1546
|
+
*/
|
|
1547
|
+
async maybeCheckpoint(state, opts, node, index, ckptCadence, counter) {
|
|
1548
|
+
if (node.type !== "step" || state.pendingError || state.suspension || !opts?.onCheckpoint) return;
|
|
1549
|
+
counter.seen++;
|
|
1550
|
+
const shouldCheckpoint = opts.checkpointWhen ? opts.checkpointWhen({ stepIndex: index, stepId: node.id, ctx: state.ctx }) : counter.seen % ckptCadence === 0;
|
|
1551
|
+
if (!shouldCheckpoint) return;
|
|
1552
|
+
const ckptStart = performance.now();
|
|
1553
|
+
try {
|
|
1554
|
+
await emitCheckpoint(state, opts, index + 1, this.cachedStepShapeHash);
|
|
1555
|
+
} catch (e) {
|
|
1556
|
+
state.pendingError = { error: e, stepId: CHECKPOINT_STEP_ID, source: "onCheckpoint" };
|
|
1557
|
+
state.checkpointFailed = true;
|
|
1558
|
+
await this.fireStepErrorAndAttachCause(state, {
|
|
1559
|
+
stepId: CHECKPOINT_STEP_ID,
|
|
1560
|
+
type: "step",
|
|
1561
|
+
ctx: state.ctx,
|
|
1562
|
+
error: e,
|
|
1563
|
+
durationMs: performance.now() - ckptStart
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Terminal reconciliation after the loop. Re-promotes a swallowed abort
|
|
1569
|
+
* (recoverability must not depend on catch position), then resolves the
|
|
1570
|
+
* mutually-exclusive precedence tail: checkpointFailed > original-step error
|
|
1571
|
+
* > suspension. (A throwing catch/finally never reaches here — it bubbles
|
|
1572
|
+
* straight out of the loop, so there is no finally-aggregation branch.)
|
|
1573
|
+
*/
|
|
1574
|
+
async settleRun(state, abortPromoted) {
|
|
1624
1575
|
if (abortPromoted && !state.pendingError && !state.suspension && state.abortSignal?.aborted) {
|
|
1625
1576
|
state.pendingError = makeAbortError(state.abortSignal);
|
|
1626
1577
|
}
|
|
@@ -1696,15 +1647,14 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1696
1647
|
if (nestedPath && nestedPath.length > 0) {
|
|
1697
1648
|
let steps = this.steps;
|
|
1698
1649
|
for (const idx of nestedPath) {
|
|
1699
|
-
const
|
|
1700
|
-
const child = node?.type === "step" ? node.nestedWorkflow : void 0;
|
|
1650
|
+
const child = steps[idx]?.nestedWorkflow;
|
|
1701
1651
|
if (!child) {
|
|
1702
1652
|
throw new Error(`loadState: nested gate "${gateId}" path is stale \u2014 step ${idx} is not a nested workflow.`);
|
|
1703
1653
|
}
|
|
1704
1654
|
steps = child.getStepsForShapeHash();
|
|
1705
1655
|
}
|
|
1706
1656
|
const innerGate = steps[gateLike.resumeFromIndex];
|
|
1707
|
-
if (innerGate
|
|
1657
|
+
if (!(innerGate instanceof GateStep) || innerGate.id !== gateId) {
|
|
1708
1658
|
throw new Error(`loadState: nested gate "${gateId}" not found at the recorded path.`);
|
|
1709
1659
|
}
|
|
1710
1660
|
const remaining = [...nestedPath.slice(1), gateLike.resumeFromIndex + 1];
|
|
@@ -1768,9 +1718,7 @@ var SealedWorkflow = class _SealedWorkflow {
|
|
|
1768
1718
|
this.ensureDuplicateCheck();
|
|
1769
1719
|
}
|
|
1770
1720
|
return new CheckpointResumedWorkflow(this.steps, idx, {
|
|
1771
|
-
mode: "checkpoint",
|
|
1772
1721
|
priorOutput: ckpt.output,
|
|
1773
|
-
snapshot: ckpt,
|
|
1774
1722
|
observability: this.observability
|
|
1775
1723
|
});
|
|
1776
1724
|
}
|
|
@@ -1886,11 +1834,12 @@ var CheckpointResumedWorkflow = class extends SealedWorkflow {
|
|
|
1886
1834
|
};
|
|
1887
1835
|
var Workflow = class _Workflow extends SealedWorkflow {
|
|
1888
1836
|
/**
|
|
1889
|
-
* Sentinel value for `foreach`'s `onError` handler. Returning
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1837
|
+
* Sentinel value for `foreach`/`parallel`'s `onError` handler. Returning
|
|
1838
|
+
* `Workflow.SKIP` omits the failed item (foreach: shortens the output array;
|
|
1839
|
+
* parallel: leaves the slot `undefined`). Aliases the leaf-module `SKIP` so
|
|
1840
|
+
* the step subclasses can compare against it without importing this class.
|
|
1892
1841
|
*/
|
|
1893
|
-
static SKIP =
|
|
1842
|
+
static SKIP = SKIP;
|
|
1894
1843
|
constructor(steps = [], id, observability) {
|
|
1895
1844
|
super(steps, id, observability);
|
|
1896
1845
|
}
|
|
@@ -1901,8 +1850,6 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
1901
1850
|
return new _Workflow([]).step(agent, options);
|
|
1902
1851
|
}
|
|
1903
1852
|
// Builder helper — append a step and return a re-typed Workflow.
|
|
1904
|
-
// Centralizes the `[...steps, node] as any` + new Workflow + observability/id
|
|
1905
|
-
// forwarding pattern used by every combinator method.
|
|
1906
1853
|
appendStep(node) {
|
|
1907
1854
|
return new _Workflow([...this.steps, node], this.id, this.observability);
|
|
1908
1855
|
}
|
|
@@ -1947,67 +1894,18 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
1947
1894
|
if (this.steps.some((s) => s.type === "gate" && s.id === id)) {
|
|
1948
1895
|
throw new Error(`Workflow: duplicate gate ID "${id}". Each gate must have a unique identifier.`);
|
|
1949
1896
|
}
|
|
1950
|
-
const node = new GateStep(
|
|
1951
|
-
id,
|
|
1952
|
-
async (state) => {
|
|
1953
|
-
if (options?.payload) {
|
|
1954
|
-
return options.payload({
|
|
1955
|
-
ctx: state.ctx,
|
|
1956
|
-
input: state.output
|
|
1957
|
-
});
|
|
1958
|
-
}
|
|
1959
|
-
return state.output;
|
|
1960
|
-
},
|
|
1961
|
-
options?.schema,
|
|
1962
|
-
options?.condition ? async (state) => options.condition({
|
|
1963
|
-
ctx: state.ctx,
|
|
1964
|
-
input: state.output
|
|
1965
|
-
}) : void 0,
|
|
1966
|
-
options?.merge ? (params) => options.merge(params) : void 0
|
|
1967
|
-
);
|
|
1897
|
+
const node = new GateStep(id, options);
|
|
1968
1898
|
return this.appendStep(node);
|
|
1969
1899
|
}
|
|
1970
1900
|
// ── branch: implementation ────────────────────────────────────
|
|
1971
1901
|
branch(casesOrConfig, options) {
|
|
1972
|
-
|
|
1973
|
-
return this.branchPredicate(casesOrConfig, options?.id);
|
|
1974
|
-
}
|
|
1975
|
-
return this.branchSelect(casesOrConfig, options?.id);
|
|
1976
|
-
}
|
|
1977
|
-
branchPredicate(cases, explicitId) {
|
|
1978
|
-
const node = new PredicateBranchStep(explicitId ?? "branch:predicate", cases);
|
|
1979
|
-
return this.appendStep(node);
|
|
1980
|
-
}
|
|
1981
|
-
branchSelect(config, explicitId) {
|
|
1982
|
-
const node = new SelectBranchStep(explicitId ?? "branch:select", config);
|
|
1902
|
+
const node = Array.isArray(casesOrConfig) ? new PredicateBranchStep(options?.id ?? "branch:predicate", casesOrConfig) : new SelectBranchStep(options?.id ?? "branch:select", casesOrConfig);
|
|
1983
1903
|
return this.appendStep(node);
|
|
1984
1904
|
}
|
|
1985
|
-
//
|
|
1986
|
-
/**
|
|
1987
|
-
* Map each item of an array through an agent or sub-workflow.
|
|
1988
|
-
*
|
|
1989
|
-
* @param target Agent or `SealedWorkflow` invoked once per item.
|
|
1990
|
-
* @param options.id Override the default step id (`foreach:<agentId>` or
|
|
1991
|
-
* the workflow's id). Required when chaining multiple foreach over the same
|
|
1992
|
-
* target — the construction-time `(type, id)` walk rejects duplicates.
|
|
1993
|
-
* @param options.concurrency Max items in flight at any moment. **Default:
|
|
1994
|
-
* unbounded** (`Infinity` — every item runs concurrently, clamped only by
|
|
1995
|
-
* item count). Pass an integer to throttle against provider rate limits.
|
|
1996
|
-
* Backed by a worker pool: as soon as one item completes, the next launches —
|
|
1997
|
-
* no lockstep batching.
|
|
1998
|
-
* @param options.onError Per-iteration error handler. **Bypassed entirely on
|
|
1999
|
-
* the suspension path** (when any item hits a nested gate) **and on the
|
|
2000
|
-
* cancellation path** (the run was aborted — pre-abort failures become
|
|
2001
|
-
* `foreach-sibling` warnings and the abort reason rethrows) — see the
|
|
2002
|
-
* foreach concurrency hazards in the README. Otherwise: return a
|
|
2003
|
-
* `TNextOutput` value to substitute, return `Workflow.SKIP` to omit, throw
|
|
2004
|
-
* to abort. Invoked sequentially in index order after all items settle.
|
|
2005
|
-
* A throw (or rethrow) from `onError` aborts the foreach immediately:
|
|
2006
|
-
* failures at indices AFTER the throwing one are neither recovered nor
|
|
2007
|
-
* surfaced as warnings.
|
|
2008
|
-
*/
|
|
1905
|
+
// Implementation
|
|
2009
1906
|
foreach(target, options) {
|
|
2010
|
-
const
|
|
1907
|
+
const body = typeof target === "function" ? target(_Workflow.create({ observability: this.observability })) : target;
|
|
1908
|
+
const node = new ForeachStep(body, options, this.observability);
|
|
2011
1909
|
return this.appendStep(node);
|
|
2012
1910
|
}
|
|
2013
1911
|
// Implementation
|
|
@@ -2054,7 +1952,7 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
2054
1952
|
};
|
|
2055
1953
|
|
|
2056
1954
|
// src/index.ts
|
|
2057
|
-
var
|
|
1955
|
+
var SKIP2 = Workflow.SKIP;
|
|
2058
1956
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2059
1957
|
0 && (module.exports = {
|
|
2060
1958
|
ABORT_STEP_ID,
|