pipeai 0.8.1 → 0.8.4

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.cjs CHANGED
@@ -20,10 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ABORT_STEP_ID: () => ABORT_STEP_ID,
23
24
  Agent: () => Agent,
24
25
  CHECKPOINT_STEP_ID: () => CHECKPOINT_STEP_ID,
25
- CheckpointTimeoutError: () => CheckpointTimeoutError,
26
- NestedGateUnsupportedError: () => NestedGateUnsupportedError,
26
+ GATE_RESUME_STEP_ID: () => GATE_RESUME_STEP_ID,
27
27
  SKIP: () => SKIP,
28
28
  TOOL_PROVIDER_BRAND: () => TOOL_PROVIDER_BRAND,
29
29
  ToolProvider: () => ToolProvider,
@@ -31,7 +31,6 @@ __export(index_exports, {
31
31
  WorkflowBranchError: () => WorkflowBranchError,
32
32
  WorkflowLoopError: () => WorkflowLoopError,
33
33
  defineTool: () => defineTool,
34
- getActiveWriter: () => getActiveWriter,
35
34
  isToolProvider: () => isToolProvider,
36
35
  migrateSnapshot: () => migrateSnapshot
37
36
  });
@@ -388,6 +387,847 @@ var Agent = class {
388
387
 
389
388
  // src/workflow.ts
390
389
  var import_ai3 = require("ai");
390
+
391
+ // src/steps/step.ts
392
+ var Step = class {
393
+ // Note: `type: "step"` subclasses (agent / transform / nested / branch /
394
+ // foreach / repeat / parallel) also carry a `readonly category` that the run
395
+ // loop reads (`getObservabilityType`) to type observability events. It is not
396
+ // declared on this base — the `StepNode` union in `workflow.ts` redeclares the
397
+ // node shape, so `category` is part of that structural contract rather than
398
+ // this class. Subclasses add it directly.
399
+ /**
400
+ * Precedence source tag a kind writes to `state.pendingError` when it
401
+ * captures a thrown body error. Defaults to `"step"`; kinds with a distinct
402
+ * error-precedence bucket (e.g. `finally`, `catch`, `gate`) override it.
403
+ */
404
+ errorSource = "step";
405
+ /**
406
+ * The step's body — the only method the run loop invokes. Each kind overrides
407
+ * it to do its work, run its own skip checks, and capture errors onto state.
408
+ * `state.output` is the input on entry and becomes the output on exit;
409
+ * `state.writer` is present in stream mode. The base implementation is a
410
+ * no-op so kinds that carry no body of their own need not override it.
411
+ */
412
+ async execute(_state) {
413
+ }
414
+ /**
415
+ * Run-policy gate: return `true` when this step should be skipped silently
416
+ * (no output change). The default is the "normal" policy — skip while the
417
+ * flow is suspended or already in error. Overridden by kinds with inverted
418
+ * policies: `catch` runs only when there's an error, `finally` always runs.
419
+ */
420
+ shouldSkip(state) {
421
+ return !!state.suspension || !!state.pendingError;
422
+ }
423
+ /**
424
+ * Apply `when` / `otherwise` conditional-skip options. Returns `true` when
425
+ * the body should be skipped — i.e. `when` returned false. On skip,
426
+ * `otherwise` (if present) produces the output; without it the input passes
427
+ * through unchanged. Distinct from {@link shouldSkip}: this is the body-level
428
+ * `when` / `otherwise` decision a kind applies after the policy gate passes.
429
+ */
430
+ async applyConditionalSkip(state, options) {
431
+ if (!options?.when) return false;
432
+ const params = { ctx: state.ctx, input: state.output };
433
+ if (await options.when(params)) return false;
434
+ if (options.otherwise) {
435
+ state.output = await options.otherwise(params);
436
+ }
437
+ return true;
438
+ }
439
+ };
440
+
441
+ // src/steps/transform-step.ts
442
+ var TransformStep = class extends Step {
443
+ type = "step";
444
+ id;
445
+ fn;
446
+ options;
447
+ constructor(id, fn, options) {
448
+ super();
449
+ this.id = id;
450
+ this.fn = fn;
451
+ this.options = options;
452
+ }
453
+ async execute(state) {
454
+ if (this.shouldSkip(state)) return;
455
+ try {
456
+ if (await this.applyConditionalSkip(state, this.options)) return;
457
+ state.output = await this.fn({
458
+ ctx: state.ctx,
459
+ input: state.output,
460
+ // Present in stream mode (undefined in generate mode), letting the
461
+ // inline step emit UIMessageChunk parts onto the workflow's stream.
462
+ writer: state.writer
463
+ });
464
+ } catch (error) {
465
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
466
+ }
467
+ }
468
+ };
469
+
470
+ // src/steps/agent-step.ts
471
+ var AgentStep = class _AgentStep extends Step {
472
+ type = "step";
473
+ id;
474
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
475
+ agent;
476
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
477
+ options;
478
+ constructor(id, agent, options) {
479
+ super();
480
+ this.id = id;
481
+ this.agent = agent;
482
+ this.options = options;
483
+ }
484
+ async execute(state) {
485
+ if (this.shouldSkip(state)) return;
486
+ try {
487
+ if (await this.applyConditionalSkip(state, this.options)) return;
488
+ await _AgentStep.runAgent(state, this.agent, state.ctx, this.options);
489
+ } catch (error) {
490
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
491
+ }
492
+ }
493
+ /**
494
+ * Run an agent against the current state, writing its result to
495
+ * `state.output`. In stream mode, output extraction awaits the full stream
496
+ * before returning — streaming benefits the client (incremental output), not
497
+ * pipeline throughput, since each step still runs sequentially.
498
+ *
499
+ * Static (does not touch instance state) so the still-literal foreach /
500
+ * parallel / branch combinators can share it. `itemIndex` identifies the
501
+ * execution to `handleStream` inside a multi-execution combinator (numeric
502
+ * index, record key, or matched case); `undefined` for a plain single
503
+ * `.step(agent)`.
504
+ */
505
+ static async runAgent(state, agent, ctx, options, itemIndex) {
506
+ const input = state.output;
507
+ const hasStructuredOutput = agent.hasOutput;
508
+ const abortSignal = state.abortSignal;
509
+ const agentCallOpts = abortSignal ? { abortSignal } : void 0;
510
+ if (state.mode === "stream" && state.writer) {
511
+ const writer = state.writer;
512
+ await runWithWriter(writer, async () => {
513
+ const result = await agent.stream(ctx, state.output, agentCallOpts);
514
+ if (options?.handleStream) {
515
+ await options.handleStream({ result, writer, ctx, input, itemIndex });
516
+ } else {
517
+ writer.merge(result.toUIMessageStream());
518
+ }
519
+ const hookParams = {
520
+ mode: "stream",
521
+ result,
522
+ ctx,
523
+ input
524
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
525
+ };
526
+ if (options?.onResult) {
527
+ await options.onResult(hookParams);
528
+ }
529
+ if (options?.mapResult) {
530
+ state.output = await options.mapResult(hookParams);
531
+ } else {
532
+ state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
533
+ }
534
+ });
535
+ } else {
536
+ const result = await agent.generate(ctx, state.output, agentCallOpts);
537
+ const hookParams = {
538
+ mode: "generate",
539
+ result,
540
+ ctx,
541
+ input
542
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
543
+ };
544
+ if (options?.onResult) {
545
+ await options.onResult(hookParams);
546
+ }
547
+ if (options?.mapResult) {
548
+ state.output = await options.mapResult(hookParams);
549
+ } else {
550
+ state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
551
+ }
552
+ }
553
+ }
554
+ };
555
+
556
+ // src/steps/branch-step.ts
557
+ var PredicateBranchStep = class extends Step {
558
+ type = "step";
559
+ category = "branch";
560
+ id;
561
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
562
+ cases;
563
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
564
+ constructor(id, cases) {
565
+ super();
566
+ this.id = id;
567
+ this.cases = cases;
568
+ }
569
+ async execute(state) {
570
+ if (this.shouldSkip(state)) return;
571
+ try {
572
+ const ctx = state.ctx;
573
+ const input = state.output;
574
+ for (let caseIndex = 0; caseIndex < this.cases.length; caseIndex++) {
575
+ const branchCase = this.cases[caseIndex];
576
+ if (branchCase.when) {
577
+ const match = await branchCase.when({ ctx, input });
578
+ if (!match) continue;
579
+ }
580
+ await AgentStep.runAgent(state, branchCase.agent, ctx, branchCase, caseIndex);
581
+ return;
582
+ }
583
+ let inputRepr;
584
+ try {
585
+ inputRepr = JSON.stringify(input);
586
+ if (inputRepr === void 0) inputRepr = String(input);
587
+ } catch {
588
+ inputRepr = `[unserializable ${typeof input}]`;
589
+ }
590
+ throw new WorkflowBranchError("predicate", `No branch matched and no default branch (a case without \`when\`) was provided. Input: ${inputRepr}`);
591
+ } catch (error) {
592
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
593
+ }
594
+ }
595
+ };
596
+ var SelectBranchStep = class extends Step {
597
+ type = "step";
598
+ category = "branch";
599
+ id;
600
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
601
+ config;
602
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
603
+ constructor(id, config) {
604
+ super();
605
+ this.id = id;
606
+ this.config = config;
607
+ }
608
+ async execute(state) {
609
+ if (this.shouldSkip(state)) return;
610
+ try {
611
+ const ctx = state.ctx;
612
+ const input = state.output;
613
+ const config = this.config;
614
+ const key = await config.select({ ctx, input });
615
+ const keyDeclared = Object.prototype.hasOwnProperty.call(config.agents, key);
616
+ if (keyDeclared && config.agents[key] === void 0) {
617
+ throw new WorkflowBranchError(
618
+ "select",
619
+ `Agent for key "${key}" was declared but the value is undefined. This usually means a conditional spread set the value to undefined. Available keys: ${Object.keys(config.agents).join(", ")}`
620
+ );
621
+ }
622
+ let agent = keyDeclared ? config.agents[key] : void 0;
623
+ if (!agent) {
624
+ if (config.onUnknownKey) {
625
+ config.onUnknownKey({
626
+ key,
627
+ availableKeys: Object.keys(config.agents),
628
+ ctx
629
+ });
630
+ }
631
+ if (config.fallback) {
632
+ agent = config.fallback;
633
+ } else {
634
+ throw new WorkflowBranchError("select", `No agent found for key "${key}" and no fallback provided. Available keys: ${Object.keys(config.agents).join(", ")}`);
635
+ }
636
+ }
637
+ await AgentStep.runAgent(state, agent, ctx, config, key);
638
+ } catch (error) {
639
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
640
+ }
641
+ }
642
+ };
643
+
644
+ // src/steps/semaphore.ts
645
+ var Semaphore = class {
646
+ available;
647
+ waiters = [];
648
+ constructor(permits) {
649
+ this.available = permits;
650
+ }
651
+ acquire() {
652
+ if (this.available > 0) {
653
+ this.available--;
654
+ return Promise.resolve();
655
+ }
656
+ return new Promise((resolve) => this.waiters.push(resolve));
657
+ }
658
+ release() {
659
+ const next = this.waiters.shift();
660
+ if (next) {
661
+ next();
662
+ } else {
663
+ this.available++;
664
+ }
665
+ }
666
+ async run(fn) {
667
+ await this.acquire();
668
+ try {
669
+ return await fn();
670
+ } finally {
671
+ this.release();
672
+ }
673
+ }
674
+ };
675
+
676
+ // src/steps/concurrent.ts
677
+ function reconcileUnits(state, id, failures, count, keyAt, unitStates, signal) {
678
+ for (let i = 0; i < count; i++) {
679
+ const us = unitStates[i];
680
+ if (!us) continue;
681
+ if (us.suspension) {
682
+ throw new Error(
683
+ `internal: gate "${us.suspension.gateId}" suspended inside concurrent unit ${id}[${keyAt(i)}]. Gates are forbidden in foreach / parallel targets \u2014 a cast must have bypassed the build-time guard.`
684
+ );
685
+ }
686
+ if (!us.warnings) continue;
687
+ for (const w of us.warnings) {
688
+ pushWarning(state, w.source, `${id}[${keyAt(i)}]:${w.stepId}`, w.error);
689
+ }
690
+ }
691
+ if (signal?.aborted) {
692
+ for (const f of failures) {
693
+ pushWarning(state, "foreach-sibling", `${id}[${f.key}]`, f.error);
694
+ }
695
+ throw signal.reason ?? new Error("Workflow aborted");
696
+ }
697
+ return failures;
698
+ }
699
+
700
+ // src/steps/foreach-step.ts
701
+ var ForeachStep = class extends Step {
702
+ type = "step";
703
+ category = "foreach";
704
+ id;
705
+ nestedWorkflow;
706
+ target;
707
+ concurrency;
708
+ onError;
709
+ handleStream;
710
+ isWorkflow;
711
+ inheritStreaming;
712
+ observability;
713
+ constructor(target, options, observability) {
714
+ 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
+ this.target = target;
719
+ this.concurrency = options?.concurrency ?? Infinity;
720
+ this.onError = options?.onError;
721
+ this.handleStream = options?.handleStream;
722
+ this.observability = observability;
723
+ this.isWorkflow = target instanceof SealedWorkflow;
724
+ this.inheritStreaming = this.isWorkflow || this.handleStream !== void 0;
725
+ const defaultId = this.isWorkflow ? target.id ?? "foreach" : `foreach:${target.id}`;
726
+ this.id = options?.id ?? defaultId;
727
+ this.nestedWorkflow = this.isWorkflow ? target : void 0;
728
+ }
729
+ async execute(state) {
730
+ if (this.shouldSkip(state)) return;
731
+ try {
732
+ const items = state.output;
733
+ if (!Array.isArray(items)) {
734
+ throw new Error(`foreach "${this.id}": expected array input, got ${typeof items}`);
735
+ }
736
+ const results = new Array(items.length);
737
+ const skipped = /* @__PURE__ */ new Set();
738
+ const itemStates = new Array(items.length);
739
+ const wantItemHooks = hasItemHooks(this.observability);
740
+ const executeItem = async (item, index) => {
741
+ const itemState = {
742
+ ctx: state.ctx,
743
+ output: item,
744
+ mode: this.inheritStreaming ? state.mode : "generate",
745
+ writer: this.inheritStreaming ? state.writer : void 0,
746
+ abortSignal: state.abortSignal
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;
805
+ }
806
+ const index = i;
807
+ const unit = (async () => {
808
+ try {
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) {
823
+ if (!this.onError) throw error;
824
+ const recovered = await this.onError({ error, item: items[index], index, ctx: state.ctx });
825
+ if (recovered === Workflow.SKIP) {
826
+ skipped.add(index);
827
+ } else {
828
+ results[index] = recovered;
829
+ }
830
+ }
831
+ state.output = skipped.size === 0 ? results : results.filter((_, i) => !skipped.has(i));
832
+ } catch (error) {
833
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
834
+ }
835
+ }
836
+ };
837
+
838
+ // src/steps/parallel-step.ts
839
+ var ParallelStep = class extends Step {
840
+ type = "step";
841
+ category = "parallel";
842
+ id;
843
+ entries;
844
+ isTuple;
845
+ branchCount;
846
+ concurrency;
847
+ onError;
848
+ handleStream;
849
+ observability;
850
+ constructor(branches, options, observability) {
851
+ super();
852
+ 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.branchCount = this.entries.length;
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;
860
+ this.onError = options?.onError;
861
+ this.handleStream = options?.handleStream;
862
+ this.observability = observability;
863
+ this.id = options?.id ?? (this.isTuple ? "parallel:tuple" : "parallel:record");
864
+ }
865
+ async execute(state) {
866
+ if (this.shouldSkip(state)) return;
867
+ try {
868
+ const input = state.output;
869
+ const results = this.isTuple ? new Array(this.branchCount) : {};
870
+ const branchStates = new Array(this.branchCount);
871
+ const wantItemHooks = hasItemHooks(this.observability);
872
+ const executeBranch = async ({ key, index, target }) => {
873
+ const isWorkflowBranch = target instanceof SealedWorkflow;
874
+ const inheritStreaming = isWorkflowBranch || this.handleStream !== void 0;
875
+ const branchState = {
876
+ ctx: state.ctx,
877
+ output: input,
878
+ mode: inheritStreaming ? state.mode : "generate",
879
+ writer: inheritStreaming ? state.writer : void 0,
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;
941
+ }
942
+ const index = i;
943
+ const unit = (async () => {
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) {
959
+ if (!this.onError) throw error;
960
+ const recovered = await this.onError({
961
+ error,
962
+ key: this.isTuple ? void 0 : key,
963
+ index: this.isTuple ? index : void 0,
964
+ ctx: state.ctx
965
+ });
966
+ if (recovered === Workflow.SKIP) {
967
+ results[key] = void 0;
968
+ } else {
969
+ results[key] = recovered;
970
+ }
971
+ }
972
+ state.output = results;
973
+ } catch (error) {
974
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
975
+ }
976
+ }
977
+ };
978
+
979
+ // src/steps/gate-step.ts
980
+ var GateStep = class extends Step {
981
+ type = "gate";
982
+ id;
983
+ errorSource = "gate";
984
+ /** Read by `loadState` to validate the resumed gate response. */
985
+ schema;
986
+ /** Read by `loadState` to merge the response with the suspended output. */
987
+ merge;
988
+ payload;
989
+ condition;
990
+ constructor(id, payload, schema, condition, merge) {
991
+ super();
992
+ this.id = id;
993
+ this.payload = payload;
994
+ this.schema = schema;
995
+ this.condition = condition;
996
+ this.merge = merge;
997
+ }
998
+ async execute(state) {
999
+ if (this.shouldSkip(state)) return;
1000
+ try {
1001
+ if (this.condition && !await this.condition(state)) return;
1002
+ const snapshot = {
1003
+ version: 2,
1004
+ kind: "gate",
1005
+ resumeFromIndex: state.stepIndex ?? -1,
1006
+ output: state.output,
1007
+ gateId: this.id,
1008
+ gatePayload: await this.payload(state)
1009
+ };
1010
+ state.suspension = snapshot;
1011
+ if (resolveFreezeSnapshots(state)) deepFreeze(snapshot);
1012
+ } catch (error) {
1013
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
1014
+ }
1015
+ }
1016
+ };
1017
+
1018
+ // src/steps/catch-step.ts
1019
+ var CatchStep = class extends Step {
1020
+ type = "catch";
1021
+ id;
1022
+ errorSource = "catch";
1023
+ catchFn;
1024
+ constructor(id, catchFn) {
1025
+ super();
1026
+ this.id = id;
1027
+ this.catchFn = catchFn;
1028
+ }
1029
+ // Runs only on a pending error; skipped on suspension and checkpoint failure.
1030
+ shouldSkip(state) {
1031
+ return !!state.suspension || !state.pendingError || !!state.checkpointFailed;
1032
+ }
1033
+ async execute(state) {
1034
+ if (this.shouldSkip(state)) return;
1035
+ const handled = state.pendingError;
1036
+ state.output = await this.catchFn({
1037
+ error: handled.error,
1038
+ ctx: state.ctx,
1039
+ lastOutput: state.output,
1040
+ stepId: handled.stepId
1041
+ });
1042
+ state.pendingError = void 0;
1043
+ }
1044
+ };
1045
+
1046
+ // src/steps/finally-step.ts
1047
+ var FinallyStep = class extends Step {
1048
+ type = "finally";
1049
+ id;
1050
+ errorSource = "finally";
1051
+ fn;
1052
+ constructor(id, fn) {
1053
+ super();
1054
+ this.id = id;
1055
+ this.fn = fn;
1056
+ }
1057
+ // Always runs — cleanup must fire regardless of suspension / error state.
1058
+ shouldSkip(_state) {
1059
+ return false;
1060
+ }
1061
+ async execute(state) {
1062
+ if (this.shouldSkip(state)) return;
1063
+ await this.fn({ ctx: state.ctx });
1064
+ }
1065
+ };
1066
+
1067
+ // src/steps/nested-workflow-step.ts
1068
+ var NestedWorkflowStep = class extends Step {
1069
+ type = "step";
1070
+ category = "nested";
1071
+ id;
1072
+ nestedWorkflow;
1073
+ options;
1074
+ constructor(id, workflow, options) {
1075
+ super();
1076
+ this.id = id;
1077
+ this.nestedWorkflow = workflow;
1078
+ this.options = options;
1079
+ }
1080
+ async execute(state) {
1081
+ const descent = state.resumeDescent;
1082
+ if (descent) {
1083
+ state.resumeDescent = void 0;
1084
+ const [childStart, ...rest] = descent.remaining;
1085
+ const myIndex2 = state.stepIndex ?? -1;
1086
+ try {
1087
+ if (rest.length === 0) {
1088
+ state.output = descent.seedOutput;
1089
+ } else {
1090
+ state.resumeDescent = { remaining: rest, seedOutput: descent.seedOutput };
1091
+ }
1092
+ await this.nestedWorkflow.executeAsNested(state, childStart);
1093
+ if (state.suspension) state.suspension = prependNestedPath(state.suspension, myIndex2, state);
1094
+ } catch (error) {
1095
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
1096
+ }
1097
+ return;
1098
+ }
1099
+ if (this.shouldSkip(state)) return;
1100
+ const myIndex = state.stepIndex ?? -1;
1101
+ try {
1102
+ if (await this.applyConditionalSkip(state, this.options)) return;
1103
+ await this.nestedWorkflow.executeAsNested(state);
1104
+ if (state.suspension) state.suspension = prependNestedPath(state.suspension, myIndex, state);
1105
+ } catch (error) {
1106
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
1107
+ }
1108
+ }
1109
+ };
1110
+
1111
+ // src/steps/repeat-step.ts
1112
+ var RepeatStep = class extends Step {
1113
+ type = "step";
1114
+ category = "repeat";
1115
+ id;
1116
+ nestedWorkflow;
1117
+ target;
1118
+ predicate;
1119
+ maxIterations;
1120
+ isWorkflow;
1121
+ constructor(id, target, predicate, maxIterations, isWorkflow) {
1122
+ super();
1123
+ this.id = id;
1124
+ this.target = target;
1125
+ this.predicate = predicate;
1126
+ this.maxIterations = maxIterations;
1127
+ this.isWorkflow = isWorkflow;
1128
+ this.nestedWorkflow = isWorkflow ? target : void 0;
1129
+ }
1130
+ async execute(state) {
1131
+ if (this.shouldSkip(state)) return;
1132
+ try {
1133
+ const ctx = state.ctx;
1134
+ for (let i = 1; i <= this.maxIterations; i++) {
1135
+ if (state.abortSignal?.aborted) {
1136
+ throw state.abortSignal.reason ?? new Error("Workflow aborted");
1137
+ }
1138
+ if (this.isWorkflow) {
1139
+ await this.target.executeAsNested(state);
1140
+ } else {
1141
+ await AgentStep.runAgent(state, this.target, ctx);
1142
+ }
1143
+ const done = await this.predicate({ output: state.output, ctx, iterations: i });
1144
+ if (done) return;
1145
+ }
1146
+ throw new WorkflowLoopError(this.maxIterations, this.maxIterations);
1147
+ } catch (error) {
1148
+ state.pendingError = { error, stepId: this.id, source: this.errorSource };
1149
+ }
1150
+ }
1151
+ };
1152
+
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
+ // src/workflow.ts
391
1231
  var WorkflowBranchError = class extends Error {
392
1232
  constructor(branchType, message) {
393
1233
  super(message);
@@ -403,33 +1243,13 @@ var WorkflowLoopError = class extends Error {
403
1243
  this.name = "WorkflowLoopError";
404
1244
  }
405
1245
  };
406
- var NestedGateUnsupportedError = class extends Error {
407
- gateId;
408
- workflowId;
409
- // Always present; non-gate rejections from concurrent foreach.
410
- siblingErrors;
411
- // Always present; OTHER suspending items in concurrent foreach.
412
- siblingSuspensions;
413
- constructor(gateId, workflowId, siblingErrors = [], siblingSuspensions = []) {
414
- super(`Gate "${gateId}" hit inside nested workflow "${workflowId ?? "(anonymous)"}". Nested gates are not yet supported.`);
415
- this.name = "NestedGateUnsupportedError";
416
- this.gateId = gateId;
417
- this.workflowId = workflowId;
418
- this.siblingErrors = siblingErrors;
419
- this.siblingSuspensions = siblingSuspensions;
420
- }
421
- };
422
1246
  var CHECKPOINT_STEP_ID = "::pipeai::onCheckpoint";
423
- var CheckpointTimeoutError = class extends Error {
424
- timeoutMs;
425
- constructor(timeoutMs) {
426
- super(`onCheckpoint exceeded ${timeoutMs}ms timeout`);
427
- this.name = "CheckpointTimeoutError";
428
- this.timeoutMs = timeoutMs;
429
- }
430
- };
431
- function resolveFreezeSnapshots(state) {
432
- return state.runOptions?.freezeSnapshots ? true : false;
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;
433
1253
  }
434
1254
  function migrateSnapshot(legacy) {
435
1255
  if (legacy.version !== 1) {
@@ -458,67 +1278,6 @@ function getNestedWorkflows(node) {
458
1278
  return [];
459
1279
  }
460
1280
  }
461
- function pendingErrorSourceToStepType(source) {
462
- switch (source) {
463
- case "step":
464
- return "step";
465
- case "finally":
466
- return "finally";
467
- case "catch":
468
- return "catch";
469
- case "onCheckpoint":
470
- return "step";
471
- }
472
- }
473
- async function emitCheckpoint(state, opts, resumeFromIndex, stepShapeHash) {
474
- if (!opts.onCheckpoint) return;
475
- const snap = {
476
- version: 2,
477
- kind: "checkpoint",
478
- resumeFromIndex,
479
- output: state.output,
480
- stepShapeHash
481
- };
482
- if (resolveFreezeSnapshots(state)) deepFreeze(snap);
483
- const controller = new AbortController();
484
- if (opts.checkpointTimeout !== void 0) {
485
- const timeoutMs = opts.checkpointTimeout;
486
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
487
- try {
488
- const callPromise = Promise.resolve(opts.onCheckpoint(snap, { signal: controller.signal }));
489
- const timeoutPromise = new Promise((_, reject) => {
490
- controller.signal.addEventListener(
491
- "abort",
492
- () => reject(new CheckpointTimeoutError(timeoutMs)),
493
- { once: true }
494
- );
495
- });
496
- callPromise.catch(() => {
497
- });
498
- timeoutPromise.catch(() => {
499
- });
500
- await Promise.race([callPromise, timeoutPromise]);
501
- } finally {
502
- clearTimeout(timeoutId);
503
- }
504
- } else {
505
- await opts.onCheckpoint(snap, { signal: controller.signal });
506
- }
507
- }
508
- var warnedStreamOnErrorOnSuspend = false;
509
- function pushWarning(state, source, stepId, error) {
510
- (state.warnings ??= []).push({ source, stepId, error });
511
- }
512
- function demotePendingError(state, pe) {
513
- pushWarning(state, pe.source, pe.stepId, pe.error);
514
- }
515
- function maybeWarnStreamOnErrorOnSuspend(result, options) {
516
- if (result.status !== "suspended" || !options?.onError || warnedStreamOnErrorOnSuspend) return;
517
- warnedStreamOnErrorOnSuspend = true;
518
- console.warn(
519
- "pipeai: stream() with options.onError suspended at a gate \u2014 onError will NOT be invoked for suspension. Discriminate via the resolved output Promise."
520
- );
521
- }
522
1281
  var SealedWorkflow = class _SealedWorkflow {
523
1282
  id;
524
1283
  steps;
@@ -528,6 +1287,7 @@ var SealedWorkflow = class _SealedWorkflow {
528
1287
  // Memoized lazily per terminal instance — build pipelines once at module
529
1288
  // load and re-run via generate() to amortize.
530
1289
  _cachedExecutableStepCount;
1290
+ _cachedCheckpointableStepCount;
531
1291
  _cachedStepShapeHash;
532
1292
  constructor(steps, id, observability) {
533
1293
  this.steps = steps;
@@ -578,6 +1338,24 @@ var SealedWorkflow = class _SealedWorkflow {
578
1338
  this._cachedExecutableStepCount = n;
579
1339
  return n;
580
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;
1358
+ }
581
1359
  /** @internal — used by `computeStepShapeHash` to descend nested workflows. */
582
1360
  getStepsForShapeHash() {
583
1361
  return this.steps;
@@ -608,11 +1386,8 @@ var SealedWorkflow = class _SealedWorkflow {
608
1386
  if (opts.checkpointEvery !== void 0 && (!Number.isInteger(opts.checkpointEvery) || opts.checkpointEvery < 1)) {
609
1387
  throw new Error(`RunOptions: checkpointEvery must be a positive integer, got ${opts.checkpointEvery}`);
610
1388
  }
611
- if (opts.checkpointTimeout !== void 0 && (!Number.isFinite(opts.checkpointTimeout) || opts.checkpointTimeout < 1)) {
612
- throw new Error(`RunOptions: checkpointTimeout must be a finite positive number (ms), got ${opts.checkpointTimeout}`);
613
- }
614
1389
  const length = this.cachedExecutableStepCount;
615
- const cadence = opts.checkpointEvery ?? Math.max(1, Math.ceil(length / 4));
1390
+ const cadence = opts.checkpointEvery ?? Math.max(1, Math.ceil(this.cachedCheckpointableStepCount / 4));
616
1391
  if (opts.freezeSnapshots && opts.freezeSnapshots !== "iAcceptThePerformanceCost" && cadence === 1 && length >= 8) {
617
1392
  throw new Error(
618
1393
  `freezeSnapshots+checkpointEvery:1 on a ${length}-step workflow is reliably catastrophic. Set checkpointEvery >= 5, freezeSnapshots: false, or pass "iAcceptThePerformanceCost".`
@@ -640,47 +1415,82 @@ var SealedWorkflow = class _SealedWorkflow {
640
1415
  * `await` the result — `await undefined` is sync, so the no-hook path
641
1416
  * stays allocation-free.
642
1417
  */
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.
643
1422
  fireHook(state, name, event) {
644
- const hook = this.observability?.[name];
645
- if (!hook) return void 0;
646
- return this.fireHookSlow(state, name, event, hook);
1423
+ return fireHook(this.observability, state, name, event);
647
1424
  }
648
- async fireHookSlow(state, name, event, hook) {
649
- try {
650
- await hook(event);
651
- return void 0;
652
- } catch (e) {
653
- if (name !== "onStepError") {
654
- const stepId = event.stepId;
655
- pushWarning(state, name, stepId, e);
656
- console.error(`pipeai: ${name} hook threw for stepId "${stepId}":`, e);
1425
+ hasItemHooks() {
1426
+ return hasItemHooks(this.observability);
1427
+ }
1428
+ /**
1429
+ * Fire `onStepError` for a step-body failure and honor the documented
1430
+ * cause-attachment contract uniformly across every firing path (step, gate,
1431
+ * catch, finally, checkpoint). When the hook itself throws, its error is
1432
+ * attached as `cause` on the ORIGINAL error so the original still reaches the
1433
+ * caller with the failure trail attached. If the original error is frozen /
1434
+ * non-extensible (cause assignment throws) or is not an object, the hook
1435
+ * error is recorded as a warning instead — so an `onStepError` throw is never
1436
+ * silently lost. (The suspension-wins tail fires `onStepError` separately, on
1437
+ * its own demotion path.)
1438
+ */
1439
+ async fireStepErrorAndAttachCause(state, event) {
1440
+ const obsError = await this.fireHook(state, "onStepError", event);
1441
+ if (obsError === void 0) return;
1442
+ const e = event.error;
1443
+ if (typeof e === "object" && e !== null) {
1444
+ try {
1445
+ e.cause = obsError;
1446
+ return;
1447
+ } catch {
657
1448
  }
658
- return e;
659
1449
  }
1450
+ pushWarning(state, "onStepError", event.stepId, obsError);
660
1451
  }
661
1452
  // ── Execution ─────────────────────────────────────────────────
662
1453
  async generate(ctx, ...args) {
663
1454
  this.ensureDuplicateCheck();
664
1455
  const input = args[0];
665
1456
  const opts = args[1];
666
- this.validateRunOptions(opts);
667
- const state = {
668
- ctx,
669
- output: input,
670
- mode: "generate",
671
- runOptions: opts,
672
- abortSignal: opts?.abortSignal
673
- };
674
- await this.execute(state, 0, opts);
675
- return this.buildResult(state);
1457
+ return this.runGenerate(ctx, 0, opts, () => ({ output: input, initialError: null }));
676
1458
  }
677
1459
  stream(ctx, ...args) {
678
1460
  this.ensureDuplicateCheck();
679
1461
  const input = args[0];
680
1462
  const options = args[1];
681
1463
  const opts = args[2];
1464
+ return this.runStream(ctx, 0, opts, options, () => ({ output: input, initialError: null }));
1465
+ }
1466
+ // Helper — converts terminal RuntimeState into a WorkflowResult; freezes
1467
+ // snapshot + warnings if requested via runOptions.
1468
+ buildResult(state) {
1469
+ const warnings = state.warnings ?? [];
1470
+ if (resolveFreezeSnapshots(state)) {
1471
+ deepFreeze(warnings);
1472
+ }
1473
+ if (state.suspension) {
1474
+ return { status: "suspended", snapshot: state.suspension, warnings };
1475
+ }
1476
+ return { status: "complete", output: state.output, warnings };
1477
+ }
1478
+ // ── Shared run drivers (generate / stream) ────────────────────
1479
+ // Every public entry point — base generate/stream plus gate- and
1480
+ // checkpoint-resume — differs only in (a) how it seeds the initial output /
1481
+ // pre-execute error and (b) the start index. Both drivers take a `seed`
1482
+ // thunk for (a) and a `startIndex` for (b); the rest (validation, state
1483
+ // construction, execute, result building, stream plumbing) is identical.
1484
+ async runGenerate(ctx, startIndex, opts, seed) {
1485
+ this.validateRunOptions(opts);
1486
+ const seeded = await seed();
1487
+ const state = makeRuntimeState(ctx, seeded.output, "generate", opts);
1488
+ if (seeded.resumeDescent) state.resumeDescent = seeded.resumeDescent;
1489
+ await this.execute(state, startIndex, opts, seeded.initialError);
1490
+ return this.buildResult(state);
1491
+ }
1492
+ runStream(ctx, startIndex, opts, options, seed) {
682
1493
  this.validateRunOptions(opts);
683
- const abortSignal = opts?.abortSignal;
684
1494
  let resolveOutput;
685
1495
  let rejectOutput;
686
1496
  const outputPromise = new Promise((res, rej) => {
@@ -691,16 +1501,11 @@ var SealedWorkflow = class _SealedWorkflow {
691
1501
  });
692
1502
  const stream = (0, import_ai3.createUIMessageStream)({
693
1503
  execute: async ({ writer }) => {
694
- const state = {
695
- ctx,
696
- output: input,
697
- mode: "stream",
698
- writer,
699
- runOptions: opts,
700
- abortSignal
701
- };
702
1504
  try {
703
- await this.execute(state, 0, opts);
1505
+ const seeded = await seed();
1506
+ const state = makeRuntimeState(ctx, seeded.output, "stream", opts, writer);
1507
+ if (seeded.resumeDescent) state.resumeDescent = seeded.resumeDescent;
1508
+ await this.execute(state, startIndex, opts, seeded.initialError);
704
1509
  const result = this.buildResult(state);
705
1510
  maybeWarnStreamOnErrorOnSuspend(result, options);
706
1511
  resolveOutput(result);
@@ -714,22 +1519,7 @@ var SealedWorkflow = class _SealedWorkflow {
714
1519
  ...options?.originalMessages ? { originalMessages: options.originalMessages } : {},
715
1520
  ...options?.generateId ? { generateId: options.generateId } : {}
716
1521
  });
717
- return {
718
- stream,
719
- output: outputPromise
720
- };
721
- }
722
- // Helper — converts terminal RuntimeState into a WorkflowResult; freezes
723
- // snapshot + warnings if requested via runOptions.
724
- buildResult(state) {
725
- const warnings = state.warnings ?? [];
726
- if (state.suspension && resolveFreezeSnapshots(state)) {
727
- deepFreeze(warnings);
728
- }
729
- if (state.suspension) {
730
- return { status: "suspended", snapshot: state.suspension, warnings };
731
- }
732
- return { status: "complete", output: state.output, warnings };
1522
+ return { stream, output: outputPromise };
733
1523
  }
734
1524
  // ── Internal: execute pipeline ────────────────────────────────
735
1525
  async execute(state, startIndex = 0, opts, initialError = null) {
@@ -739,12 +1529,13 @@ var SealedWorkflow = class _SealedWorkflow {
739
1529
  if (opts !== void 0 && state.runOptions === void 0) {
740
1530
  state.runOptions = opts;
741
1531
  }
742
- const ckptCadence = opts?.onCheckpoint && opts.checkpointWhen === void 0 ? opts.checkpointEvery ?? Math.max(1, Math.ceil(this.cachedExecutableStepCount / 4)) : 0;
743
- let pendingError = initialError;
1532
+ const ckptCadence = opts?.onCheckpoint && opts.checkpointWhen === void 0 ? opts.checkpointEvery ?? Math.max(1, Math.ceil(this.cachedCheckpointableStepCount / 4)) : 0;
1533
+ let executableStepsSeen = 0;
1534
+ state.pendingError = initialError ?? void 0;
744
1535
  let abortPromoted = false;
745
1536
  const makeAbortError = (signal) => ({
746
1537
  error: signal.reason ?? new Error("Workflow aborted"),
747
- stepId: "abort",
1538
+ stepId: ABORT_STEP_ID,
748
1539
  source: "step"
749
1540
  });
750
1541
  for (let i = startIndex; i < this.steps.length; i++) {
@@ -752,152 +1543,61 @@ var SealedWorkflow = class _SealedWorkflow {
752
1543
  if (!abortPromoted) {
753
1544
  abortPromoted = true;
754
1545
  state.suspension = void 0;
755
- if (pendingError) demotePendingError(state, pendingError);
756
- pendingError = makeAbortError(state.abortSignal);
757
- } else if (!pendingError) {
758
- pendingError = makeAbortError(state.abortSignal);
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);
759
1550
  }
760
1551
  }
761
1552
  const node = this.steps[i];
762
- if (node.type === "finally") {
763
- const stepId2 = node.id;
764
- const finStart = performance.now();
765
- await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "finally", ctx: state.ctx, input: state.output });
766
- try {
767
- await node.execute(state);
768
- await this.fireHook(state, "onStepFinish", {
769
- stepId: stepId2,
770
- type: "finally",
771
- ctx: state.ctx,
772
- output: state.output,
773
- durationMs: performance.now() - finStart,
774
- suspended: false
775
- });
776
- } catch (e) {
777
- await this.fireHook(state, "onStepError", {
778
- stepId: stepId2,
779
- type: "finally",
780
- ctx: state.ctx,
781
- error: e,
782
- durationMs: performance.now() - finStart
783
- });
784
- if (pendingError) demotePendingError(state, pendingError);
785
- pendingError = { error: e, stepId: stepId2, source: "finally" };
786
- }
787
- continue;
788
- }
789
- if (node.type === "catch") {
790
- if (state.suspension || !pendingError || state.checkpointFailed) continue;
791
- const stepId2 = node.id;
792
- const cStart = performance.now();
793
- await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "catch", ctx: state.ctx, input: state.output });
794
- try {
795
- state.output = await node.catchFn({
796
- error: pendingError.error,
797
- ctx: state.ctx,
798
- lastOutput: state.output,
799
- stepId: pendingError.stepId
800
- });
801
- pendingError = null;
802
- await this.fireHook(state, "onStepFinish", {
803
- stepId: stepId2,
804
- type: "catch",
805
- ctx: state.ctx,
806
- output: state.output,
807
- durationMs: performance.now() - cStart,
808
- suspended: false
809
- });
810
- } catch (e) {
811
- await this.fireHook(state, "onStepError", {
812
- stepId: stepId2,
813
- type: "catch",
814
- ctx: state.ctx,
815
- error: e,
816
- durationMs: performance.now() - cStart
817
- });
818
- if (pendingError) demotePendingError(state, pendingError);
819
- pendingError = { error: e, stepId: stepId2, source: "catch" };
820
- }
821
- continue;
822
- }
823
- if (state.suspension || pendingError) continue;
824
- if (node.type === "gate") {
825
- const stepId2 = node.id;
826
- const gStart = performance.now();
827
- await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "gate", ctx: state.ctx, input: state.output });
828
- try {
829
- if (node.condition && !await node.condition(state)) {
830
- await this.fireHook(state, "onStepFinish", {
831
- stepId: stepId2,
832
- type: "gate",
833
- ctx: state.ctx,
834
- output: state.output,
835
- durationMs: performance.now() - gStart,
836
- suspended: false
837
- });
838
- continue;
839
- }
840
- const snapshot = {
841
- version: 2,
842
- kind: "gate",
843
- resumeFromIndex: i,
844
- output: state.output,
845
- gateId: node.id,
846
- gatePayload: await node.payload(state)
847
- };
848
- state.suspension = snapshot;
849
- if (resolveFreezeSnapshots(state)) deepFreeze(snapshot);
850
- await this.fireHook(state, "onStepFinish", {
851
- stepId: stepId2,
852
- type: "gate",
853
- ctx: state.ctx,
854
- output: state.output,
855
- durationMs: performance.now() - gStart,
856
- suspended: true
857
- });
858
- } catch (e) {
859
- pendingError = { error: e, stepId: node.id, source: "step" };
860
- }
861
- continue;
862
- }
1553
+ const skip = node.type === "finally" ? false : node.type === "catch" ? !!state.suspension || !state.pendingError || !!state.checkpointFailed : !!state.suspension || !!state.pendingError;
1554
+ if (skip) continue;
863
1555
  const obsType = getObservabilityType(node);
864
1556
  const stepId = node.id;
865
1557
  const sStart = performance.now();
866
- const stepInput = state.output;
867
- await this.fireHook(state, "onStepStart", { stepId, type: obsType, ctx: state.ctx, input: stepInput });
1558
+ const errBefore = state.pendingError;
1559
+ const suspendedBefore = !!state.suspension;
1560
+ state.stepIndex = i;
1561
+ await this.fireHook(state, "onStepStart", { stepId, type: obsType, ctx: state.ctx, input: state.output });
868
1562
  try {
869
1563
  await node.execute(state);
870
- await this.fireHook(state, "onStepFinish", {
1564
+ } catch (e) {
1565
+ await this.fireStepErrorAndAttachCause(state, {
871
1566
  stepId,
872
1567
  type: obsType,
873
1568
  ctx: state.ctx,
874
- output: state.output,
875
- durationMs: performance.now() - sStart,
876
- suspended: false
1569
+ error: e,
1570
+ durationMs: performance.now() - sStart
877
1571
  });
878
- } catch (e) {
879
- pendingError = { error: e, stepId: node.id, source: "step" };
880
- const obsError = await this.fireHook(state, "onStepError", {
1572
+ throw e;
1573
+ }
1574
+ const newError = state.pendingError && state.pendingError !== errBefore ? state.pendingError : null;
1575
+ if (newError) {
1576
+ await this.fireStepErrorAndAttachCause(state, {
881
1577
  stepId,
882
1578
  type: obsType,
883
1579
  ctx: state.ctx,
884
- error: e,
1580
+ error: newError.error,
885
1581
  durationMs: performance.now() - sStart
886
1582
  });
887
- if (obsError !== void 0 && typeof e === "object" && e !== null) {
888
- try {
889
- e.cause = obsError;
890
- } catch {
891
- }
892
- }
1583
+ } else {
1584
+ await this.fireHook(state, "onStepFinish", {
1585
+ stepId,
1586
+ type: obsType,
1587
+ ctx: state.ctx,
1588
+ output: state.output,
1589
+ durationMs: performance.now() - sStart,
1590
+ suspended: !suspendedBefore && !!state.suspension
1591
+ });
893
1592
  }
894
- const leaked = state.suspension;
895
- if (leaked) {
1593
+ if (node.type === "step" && node.category !== "nested" && state.suspension) {
1594
+ const leaked = state.suspension;
896
1595
  state.suspension = void 0;
897
1596
  throw new Error(`internal: suspension bubbled from non-gate step "${node.id}" (gate "${leaked.gateId}").`);
898
1597
  }
899
- if (!pendingError && !state.suspension && opts?.onCheckpoint) {
900
- const shouldCheckpoint = opts.checkpointWhen ? opts.checkpointWhen({ stepIndex: i, stepId: node.id, ctx: state.ctx }) : (i + 1) % ckptCadence === 0;
1598
+ if (node.type === "step" && !state.pendingError && !state.suspension && opts?.onCheckpoint) {
1599
+ executableStepsSeen++;
1600
+ const shouldCheckpoint = opts.checkpointWhen ? opts.checkpointWhen({ stepIndex: i, stepId: node.id, ctx: state.ctx }) : executableStepsSeen % ckptCadence === 0;
901
1601
  if (shouldCheckpoint) {
902
1602
  const ckptStart = performance.now();
903
1603
  try {
@@ -908,9 +1608,9 @@ var SealedWorkflow = class _SealedWorkflow {
908
1608
  this.cachedStepShapeHash
909
1609
  );
910
1610
  } catch (e) {
911
- pendingError = { error: e, stepId: CHECKPOINT_STEP_ID, source: "onCheckpoint" };
1611
+ state.pendingError = { error: e, stepId: CHECKPOINT_STEP_ID, source: "onCheckpoint" };
912
1612
  state.checkpointFailed = true;
913
- await this.fireHook(state, "onStepError", {
1613
+ await this.fireStepErrorAndAttachCause(state, {
914
1614
  stepId: CHECKPOINT_STEP_ID,
915
1615
  type: "step",
916
1616
  ctx: state.ctx,
@@ -918,117 +1618,66 @@ var SealedWorkflow = class _SealedWorkflow {
918
1618
  durationMs: performance.now() - ckptStart
919
1619
  });
920
1620
  }
921
- }
922
- }
923
- }
924
- if (pendingError && !state.suspension) {
925
- if (state.checkpointFailed) {
926
- const warningsArr = state.warnings ?? [];
927
- const checkpointError = pendingError.source === "onCheckpoint" ? pendingError.error : warningsArr.find((w) => w.source === "onCheckpoint")?.error;
928
- const finallyErrors = warningsArr.filter((w) => w.source === "finally").map((w) => w.error);
929
- const all = pendingError.source === "finally" ? [...finallyErrors, pendingError.error] : finallyErrors;
930
- if (all.length > 0) {
931
- console.warn(
932
- `pipeai: ${all.length} .finally() error(s) suppressed by checkpoint-failure precedence:`,
933
- all
934
- );
935
- }
936
- throw checkpointError ?? pendingError.error;
937
- }
938
- const isFinallyPath = pendingError.source === "finally" || (state.warnings?.some((w) => w.source === "finally") ?? false);
939
- if (isFinallyPath) {
940
- const all = [...(state.warnings ?? []).map((w) => w.error), pendingError.error];
941
- throw new AggregateError(all, `Workflow failed with ${all.length} error(s) from .finally() bodies`);
942
- }
943
- throw pendingError.error;
944
- } else if (pendingError && state.suspension) {
945
- demotePendingError(state, pendingError);
946
- try {
947
- await this.observability?.onStepError?.({
948
- stepId: pendingError.stepId,
949
- type: pendingErrorSourceToStepType(pendingError.source),
950
- ctx: state.ctx,
951
- error: pendingError.error,
952
- durationMs: 0
953
- });
954
- } catch (obsError) {
955
- pushWarning(state, "onStepError", pendingError.stepId, obsError);
956
- }
957
- pendingError = null;
958
- }
959
- }
960
- // ── Internal: execute a nested workflow within a step/loop ─────
961
- // Defined on SealedWorkflow (not Workflow) because TypeScript's protected
962
- // access rules only allow calling workflow.execute() from the same class.
963
- //
964
- // Contract: clears any inner suspension before re-throwing as
965
- // NestedGateUnsupportedError. The outer execute() therefore never observes
966
- // a leaked `state.suspension` from non-gate nodes (defensive invariant).
967
- async executeNestedWorkflow(state, workflow) {
968
- const savedRunOptions = state.runOptions;
969
- state.runOptions = void 0;
970
- try {
971
- await workflow.execute(state);
972
- } finally {
973
- state.runOptions = savedRunOptions;
974
- }
975
- if (state.suspension) {
976
- const gateId = state.suspension.gateId;
977
- state.suspension = void 0;
978
- throw new NestedGateUnsupportedError(gateId, workflow.id);
979
- }
980
- }
981
- // ── Internal: execute an agent within a step/branch ───────────
982
- // In stream mode, output extraction awaits the full stream before returning.
983
- // Streaming benefits the client (incremental output), not pipeline throughput —
984
- // each step still runs sequentially.
985
- async executeAgent(state, agent, ctx, options) {
986
- const input = state.output;
987
- const hasStructuredOutput = agent.hasOutput;
988
- const abortSignal = state.abortSignal;
989
- const agentCallOpts = abortSignal ? { abortSignal } : void 0;
990
- if (state.mode === "stream" && state.writer) {
991
- const writer = state.writer;
992
- await runWithWriter(writer, async () => {
993
- const result = await agent.stream(ctx, state.output, agentCallOpts);
994
- if (options?.handleStream) {
995
- await options.handleStream({ result, writer, ctx, input });
996
- } else {
997
- writer.merge(result.toUIMessageStream());
998
- }
999
- const hookParams = {
1000
- mode: "stream",
1001
- result,
1002
- ctx,
1003
- input
1004
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1005
- };
1006
- if (options?.onResult) {
1007
- await options.onResult(hookParams);
1008
- }
1009
- if (options?.mapResult) {
1010
- state.output = await options.mapResult(hookParams);
1011
- } else {
1012
- state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
1013
- }
1014
- });
1015
- } else {
1016
- const result = await agent.generate(ctx, state.output, agentCallOpts);
1017
- const hookParams = {
1018
- mode: "generate",
1019
- result,
1020
- ctx,
1021
- input
1022
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1023
- };
1024
- if (options?.onResult) {
1025
- await options.onResult(hookParams);
1621
+ }
1026
1622
  }
1027
- if (options?.mapResult) {
1028
- state.output = await options.mapResult(hookParams);
1029
- } else {
1030
- state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
1623
+ }
1624
+ if (abortPromoted && !state.pendingError && !state.suspension && state.abortSignal?.aborted) {
1625
+ state.pendingError = makeAbortError(state.abortSignal);
1626
+ }
1627
+ if (state.pendingError && !state.suspension) {
1628
+ const pe = state.pendingError;
1629
+ if (state.checkpointFailed) {
1630
+ const warningsArr = state.warnings ?? [];
1631
+ const checkpointError = pe.source === "onCheckpoint" ? pe.error : warningsArr.find((w) => w.source === "onCheckpoint")?.error;
1632
+ const suppressed = warningsArr.filter((w) => w.error !== checkpointError).map((w) => w.error);
1633
+ if (pe.source !== "onCheckpoint" && pe.error !== checkpointError) {
1634
+ suppressed.push(pe.error);
1635
+ }
1636
+ if (suppressed.length > 0) {
1637
+ console.warn(
1638
+ `pipeai: ${suppressed.length} error(s) suppressed by checkpoint-failure precedence:`,
1639
+ suppressed
1640
+ );
1641
+ }
1642
+ throw checkpointError ?? pe.error;
1643
+ }
1644
+ throw pe.error;
1645
+ } else if (state.pendingError && state.suspension) {
1646
+ const pe = state.pendingError;
1647
+ demotePendingError(state, pe);
1648
+ try {
1649
+ await this.observability?.onStepError?.({
1650
+ stepId: pe.stepId,
1651
+ type: pendingErrorSourceToStepType(pe.source),
1652
+ ctx: state.ctx,
1653
+ error: pe.error,
1654
+ durationMs: 0
1655
+ });
1656
+ } catch (obsError) {
1657
+ pushWarning(state, "onStepError", pe.stepId, obsError);
1031
1658
  }
1659
+ state.pendingError = void 0;
1660
+ }
1661
+ }
1662
+ /**
1663
+ * Run THIS sealed workflow as a nested step on the caller's run `state`.
1664
+ * Public (internal; not re-exported from index) so `Step` subclasses —
1665
+ * `nested` / `repeat` / `foreach` / `parallel` with `SealedWorkflow` targets
1666
+ * — can run a sub-workflow without reaching the protected `execute`.
1667
+ *
1668
+ * Contract: RunOptions is run-scoped, so the child never inherits the
1669
+ * parent's (`state.warnings` IS propagated — telemetry > config). A gate
1670
+ * inside the child leaves `state.suspension` set so it propagates up (only a
1671
+ * `.step(workflow)` ever does this — concurrent/looped combinators forbid
1672
+ * gated targets at build time).
1673
+ */
1674
+ async executeAsNested(state, startIndex = 0) {
1675
+ const savedRunOptions = state.runOptions;
1676
+ state.runOptions = void 0;
1677
+ try {
1678
+ await this.execute(state, startIndex);
1679
+ } finally {
1680
+ state.runOptions = savedRunOptions;
1032
1681
  }
1033
1682
  }
1034
1683
  // ── Gate: load persisted state for resumption ──────────────────
@@ -1043,6 +1692,32 @@ var SealedWorkflow = class _SealedWorkflow {
1043
1692
  );
1044
1693
  }
1045
1694
  this.ensureDuplicateCheck();
1695
+ const nestedPath = gateLike.nestedPath;
1696
+ if (nestedPath && nestedPath.length > 0) {
1697
+ let steps = this.steps;
1698
+ for (const idx of nestedPath) {
1699
+ const node = steps[idx];
1700
+ const child = node?.type === "step" ? node.nestedWorkflow : void 0;
1701
+ if (!child) {
1702
+ throw new Error(`loadState: nested gate "${gateId}" path is stale \u2014 step ${idx} is not a nested workflow.`);
1703
+ }
1704
+ steps = child.getStepsForShapeHash();
1705
+ }
1706
+ const innerGate = steps[gateLike.resumeFromIndex];
1707
+ if (innerGate?.type !== "gate" || innerGate.id !== gateId) {
1708
+ throw new Error(`loadState: nested gate "${gateId}" not found at the recorded path.`);
1709
+ }
1710
+ const remaining = [...nestedPath.slice(1), gateLike.resumeFromIndex + 1];
1711
+ return new ResumedWorkflow(this.steps, nestedPath[0], {
1712
+ mode: "gate",
1713
+ schema: innerGate.schema,
1714
+ mergeFn: innerGate.merge,
1715
+ priorOutput: gateLike.output,
1716
+ snapshot: gateLike,
1717
+ observability: this.observability,
1718
+ nestedRemaining: remaining
1719
+ });
1720
+ }
1046
1721
  const gateIndex = this.findGateIndex(gateLike);
1047
1722
  const gateNode = this.steps[gateIndex];
1048
1723
  return new ResumedWorkflow(this.steps, gateIndex + 1, {
@@ -1102,16 +1777,12 @@ var SealedWorkflow = class _SealedWorkflow {
1102
1777
  /**
1103
1778
  * Append a `.finally()` body to a sealed workflow, returning another sealed
1104
1779
  * workflow. Allows multi-finally chains (`.finally().finally()`). A throwing
1105
- * `.finally` body does NOT abort subsequent ones they all run.
1780
+ * `.finally` body bubbles straight out of the run: it is non-recoverable, does
1781
+ * NOT aggregate with a prior error, and subsequent `.finally()` bodies do not
1782
+ * run. (See {@link FinallyStep} for the full contract.)
1106
1783
  */
1107
1784
  finally(id, fn) {
1108
- const node = {
1109
- type: "finally",
1110
- id,
1111
- execute: async (state) => {
1112
- await fn({ ctx: state.ctx });
1113
- }
1114
- };
1785
+ const node = new FinallyStep(id, fn);
1115
1786
  return new _SealedWorkflow([...this.steps, node], this.id, this.observability);
1116
1787
  }
1117
1788
  findGateIndex(snapshot) {
@@ -1141,6 +1812,7 @@ var ResumedWorkflow = class extends SealedWorkflow {
1141
1812
  schema;
1142
1813
  mergeFn;
1143
1814
  priorOutput;
1815
+ nestedRemaining;
1144
1816
  /** @internal */
1145
1817
  constructor(steps, startIndex, config) {
1146
1818
  super(steps, void 0, config.observability);
@@ -1148,6 +1820,7 @@ var ResumedWorkflow = class extends SealedWorkflow {
1148
1820
  this.schema = config.schema;
1149
1821
  this.mergeFn = config.mergeFn;
1150
1822
  this.priorOutput = config.priorOutput;
1823
+ this.nestedRemaining = config.nestedRemaining;
1151
1824
  }
1152
1825
  validateResponse(response) {
1153
1826
  if (this.schema) {
@@ -1155,77 +1828,39 @@ var ResumedWorkflow = class extends SealedWorkflow {
1155
1828
  }
1156
1829
  return response;
1157
1830
  }
1158
- async generate(ctx, ...args) {
1159
- const rawResponse = args[0];
1160
- const opts = args[1];
1161
- let output = this.priorOutput;
1162
- let initialError = null;
1831
+ /**
1832
+ * Seed the run by validating the gate response and merging it with the
1833
+ * suspended output. Runs schema.parse + mergeFn inside a try so a failure
1834
+ * becomes a pre-execute `initialError` (routed through `.catch()`) rather
1835
+ * than escaping the run synchronously. On error the output falls back to the
1836
+ * prior (pre-gate) output.
1837
+ */
1838
+ async seedFromResponse(rawResponse) {
1163
1839
  try {
1164
1840
  const response = this.validateResponse(rawResponse);
1165
- output = this.mergeFn ? await this.mergeFn({ priorOutput: this.priorOutput, response }) : response;
1841
+ const merged = this.mergeFn ? await this.mergeFn({ priorOutput: this.priorOutput, response }) : response;
1842
+ if (this.nestedRemaining) {
1843
+ return {
1844
+ output: this.priorOutput,
1845
+ initialError: null,
1846
+ resumeDescent: { remaining: this.nestedRemaining, seedOutput: merged }
1847
+ };
1848
+ }
1849
+ return { output: merged, initialError: null };
1166
1850
  } catch (error) {
1167
- initialError = { error, stepId: "gate:resume", source: "step" };
1168
- }
1169
- const state = {
1170
- ctx,
1171
- output,
1172
- mode: "generate",
1173
- runOptions: opts,
1174
- abortSignal: opts?.abortSignal
1175
- };
1176
- await this.execute(state, this.startIndex, opts, initialError);
1177
- return this.buildResult(state);
1851
+ return { output: this.priorOutput, initialError: { error, stepId: GATE_RESUME_STEP_ID, source: "step" } };
1852
+ }
1853
+ }
1854
+ async generate(ctx, ...args) {
1855
+ const rawResponse = args[0];
1856
+ const opts = args[1];
1857
+ return this.runGenerate(ctx, this.startIndex, opts, () => this.seedFromResponse(rawResponse));
1178
1858
  }
1179
1859
  stream(ctx, ...args) {
1180
1860
  const rawResponse = args[0];
1181
1861
  const options = args[1];
1182
1862
  const opts = args[2];
1183
- const abortSignal = opts?.abortSignal;
1184
- let resolveOutput;
1185
- let rejectOutput;
1186
- const outputPromise = new Promise((res, rej) => {
1187
- resolveOutput = res;
1188
- rejectOutput = rej;
1189
- });
1190
- outputPromise.catch(() => {
1191
- });
1192
- const mergeFn = this.mergeFn;
1193
- const priorOutput = this.priorOutput;
1194
- const startIndex = this.startIndex;
1195
- const stream = (0, import_ai3.createUIMessageStream)({
1196
- execute: async ({ writer }) => {
1197
- let output = priorOutput;
1198
- let initialError = null;
1199
- try {
1200
- const response = this.validateResponse(rawResponse);
1201
- output = mergeFn ? await mergeFn({ priorOutput, response }) : response;
1202
- } catch (error) {
1203
- initialError = { error, stepId: "gate:resume", source: "step" };
1204
- }
1205
- const state = {
1206
- ctx,
1207
- output,
1208
- mode: "stream",
1209
- writer,
1210
- runOptions: opts,
1211
- abortSignal
1212
- };
1213
- try {
1214
- await this.execute(state, startIndex, opts, initialError);
1215
- const result = this.buildResult(state);
1216
- maybeWarnStreamOnErrorOnSuspend(result, options);
1217
- resolveOutput(result);
1218
- } catch (error) {
1219
- rejectOutput(error);
1220
- throw error;
1221
- }
1222
- },
1223
- ...options?.onError ? { onError: options.onError } : {},
1224
- ...options?.onFinish ? { onFinish: options.onFinish } : {},
1225
- ...options?.originalMessages ? { originalMessages: options.originalMessages } : {},
1226
- ...options?.generateId ? { generateId: options.generateId } : {}
1227
- });
1228
- return { stream, output: outputPromise };
1863
+ return this.runStream(ctx, this.startIndex, opts, options, () => this.seedFromResponse(rawResponse));
1229
1864
  }
1230
1865
  };
1231
1866
  var CheckpointResumedWorkflow = class extends SealedWorkflow {
@@ -1241,55 +1876,12 @@ var CheckpointResumedWorkflow = class extends SealedWorkflow {
1241
1876
  // Inputs are ignored — state is seeded from the snapshot's `output` field.
1242
1877
  async generate(ctx, ...args) {
1243
1878
  const opts = args[1];
1244
- this.validateRunOptions(opts);
1245
- const state = {
1246
- ctx,
1247
- output: this.priorOutput,
1248
- mode: "generate",
1249
- runOptions: opts
1250
- };
1251
- await this.execute(state, this.startIndex, opts);
1252
- return this.buildResult(state);
1879
+ return this.runGenerate(ctx, this.startIndex, opts, () => ({ output: this.priorOutput, initialError: null }));
1253
1880
  }
1254
1881
  stream(ctx, ...args) {
1255
1882
  const options = args[1];
1256
1883
  const opts = args[2];
1257
- this.validateRunOptions(opts);
1258
- let resolveOutput;
1259
- let rejectOutput;
1260
- const outputPromise = new Promise((res, rej) => {
1261
- resolveOutput = res;
1262
- rejectOutput = rej;
1263
- });
1264
- outputPromise.catch(() => {
1265
- });
1266
- const priorOutput = this.priorOutput;
1267
- const startIndex = this.startIndex;
1268
- const stream = (0, import_ai3.createUIMessageStream)({
1269
- execute: async ({ writer }) => {
1270
- const state = {
1271
- ctx,
1272
- output: priorOutput,
1273
- mode: "stream",
1274
- writer,
1275
- runOptions: opts
1276
- };
1277
- try {
1278
- await this.execute(state, startIndex, opts);
1279
- const result = this.buildResult(state);
1280
- maybeWarnStreamOnErrorOnSuspend(result, options);
1281
- resolveOutput(result);
1282
- } catch (error) {
1283
- rejectOutput(error);
1284
- throw error;
1285
- }
1286
- },
1287
- ...options?.onError ? { onError: options.onError } : {},
1288
- ...options?.onFinish ? { onFinish: options.onFinish } : {},
1289
- ...options?.originalMessages ? { originalMessages: options.originalMessages } : {},
1290
- ...options?.generateId ? { generateId: options.generateId } : {}
1291
- });
1292
- return { stream, output: outputPromise };
1884
+ return this.runStream(ctx, this.startIndex, opts, options, () => ({ output: this.priorOutput, initialError: null }));
1293
1885
  }
1294
1886
  };
1295
1887
  var Workflow = class _Workflow extends SealedWorkflow {
@@ -1315,20 +1907,16 @@ var Workflow = class _Workflow extends SealedWorkflow {
1315
1907
  return new _Workflow([...this.steps, node], this.id, this.observability);
1316
1908
  }
1317
1909
  // ── step: implementation ──────────────────────────────────────
1318
- step(target, optionsOrFn) {
1910
+ step(target, optionsOrFn, inlineOptions) {
1319
1911
  if (target instanceof SealedWorkflow) {
1320
1912
  const workflow = target;
1321
- const node2 = {
1322
- type: "step",
1323
- id: workflow.id ?? "nested-workflow",
1324
- nestedWorkflow: workflow,
1325
- // Feeds the recursive stepShapeHash walk.
1326
- category: "nested",
1327
- // Observability event type.
1328
- execute: async (state) => {
1329
- await this.executeNestedWorkflow(state, workflow);
1330
- }
1331
- };
1913
+ const options2 = optionsOrFn;
1914
+ const node2 = new NestedWorkflowStep(
1915
+ options2?.id ?? workflow.id ?? "nested-workflow",
1916
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1917
+ workflow,
1918
+ options2
1919
+ );
1332
1920
  return this.appendStep(node2);
1333
1921
  }
1334
1922
  if (typeof target === "string") {
@@ -1336,28 +1924,22 @@ var Workflow = class _Workflow extends SealedWorkflow {
1336
1924
  throw new Error(`Workflow step("${target}"): second argument must be a function`);
1337
1925
  }
1338
1926
  const fn = optionsOrFn;
1339
- const node2 = {
1340
- type: "step",
1341
- id: target,
1342
- execute: async (state) => {
1343
- state.output = await fn({
1344
- ctx: state.ctx,
1345
- input: state.output
1346
- });
1347
- }
1348
- };
1927
+ const node2 = new TransformStep(
1928
+ target,
1929
+ fn,
1930
+ inlineOptions
1931
+ );
1349
1932
  return this.appendStep(node2);
1350
1933
  }
1351
1934
  const agent = target;
1352
1935
  const options = optionsOrFn;
1353
- const node = {
1354
- type: "step",
1355
- id: options?.id ?? agent.id,
1356
- execute: async (state) => {
1357
- const ctx = state.ctx;
1358
- await this.executeAgent(state, agent, ctx, options);
1359
- }
1360
- };
1936
+ const node = new AgentStep(
1937
+ options?.id ?? agent.id,
1938
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1939
+ agent,
1940
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1941
+ options
1942
+ );
1361
1943
  return this.appendStep(node);
1362
1944
  }
1363
1945
  // ── gate: human-in-the-loop suspension point ────────────────
@@ -1365,16 +1947,9 @@ var Workflow = class _Workflow extends SealedWorkflow {
1365
1947
  if (this.steps.some((s) => s.type === "gate" && s.id === id)) {
1366
1948
  throw new Error(`Workflow: duplicate gate ID "${id}". Each gate must have a unique identifier.`);
1367
1949
  }
1368
- const node = {
1369
- type: "gate",
1950
+ const node = new GateStep(
1370
1951
  id,
1371
- schema: options?.schema,
1372
- condition: options?.condition ? async (state) => options.condition({
1373
- ctx: state.ctx,
1374
- input: state.output
1375
- }) : void 0,
1376
- merge: options?.merge ? (params) => options.merge(params) : void 0,
1377
- payload: async (state) => {
1952
+ async (state) => {
1378
1953
  if (options?.payload) {
1379
1954
  return options.payload({
1380
1955
  ctx: state.ctx,
@@ -1382,8 +1957,14 @@ var Workflow = class _Workflow extends SealedWorkflow {
1382
1957
  });
1383
1958
  }
1384
1959
  return state.output;
1385
- }
1386
- };
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
+ );
1387
1968
  return this.appendStep(node);
1388
1969
  }
1389
1970
  // ── branch: implementation ────────────────────────────────────
@@ -1394,67 +1975,11 @@ var Workflow = class _Workflow extends SealedWorkflow {
1394
1975
  return this.branchSelect(casesOrConfig, options?.id);
1395
1976
  }
1396
1977
  branchPredicate(cases, explicitId) {
1397
- const node = {
1398
- type: "step",
1399
- id: explicitId ?? "branch:predicate",
1400
- category: "branch",
1401
- execute: async (state) => {
1402
- const ctx = state.ctx;
1403
- const input = state.output;
1404
- for (const branchCase of cases) {
1405
- if (branchCase.when) {
1406
- const match = await branchCase.when({ ctx, input });
1407
- if (!match) continue;
1408
- }
1409
- await this.executeAgent(state, branchCase.agent, ctx, branchCase);
1410
- return;
1411
- }
1412
- let inputRepr;
1413
- try {
1414
- inputRepr = JSON.stringify(input);
1415
- if (inputRepr === void 0) inputRepr = String(input);
1416
- } catch {
1417
- inputRepr = `[unserializable ${typeof input}]`;
1418
- }
1419
- throw new WorkflowBranchError("predicate", `No branch matched and no default branch (a case without \`when\`) was provided. Input: ${inputRepr}`);
1420
- }
1421
- };
1978
+ const node = new PredicateBranchStep(explicitId ?? "branch:predicate", cases);
1422
1979
  return this.appendStep(node);
1423
1980
  }
1424
1981
  branchSelect(config, explicitId) {
1425
- const node = {
1426
- type: "step",
1427
- id: explicitId ?? "branch:select",
1428
- category: "branch",
1429
- execute: async (state) => {
1430
- const ctx = state.ctx;
1431
- const input = state.output;
1432
- const key = await config.select({ ctx, input });
1433
- const keyDeclared = Object.prototype.hasOwnProperty.call(config.agents, key);
1434
- if (keyDeclared && config.agents[key] === void 0) {
1435
- throw new WorkflowBranchError(
1436
- "select",
1437
- `Agent for key "${key}" was declared but the value is undefined. This usually means a conditional spread set the value to undefined. Available keys: ${Object.keys(config.agents).join(", ")}`
1438
- );
1439
- }
1440
- let agent = keyDeclared ? config.agents[key] : void 0;
1441
- if (!agent) {
1442
- if (config.onUnknownKey) {
1443
- config.onUnknownKey({
1444
- key,
1445
- availableKeys: Object.keys(config.agents),
1446
- ctx
1447
- });
1448
- }
1449
- if (config.fallback) {
1450
- agent = config.fallback;
1451
- } else {
1452
- throw new WorkflowBranchError("select", `No agent found for key "${key}" and no fallback provided. Available keys: ${Object.keys(config.agents).join(", ")}`);
1453
- }
1454
- }
1455
- await this.executeAgent(state, agent, ctx, config);
1456
- }
1457
- };
1982
+ const node = new SelectBranchStep(explicitId ?? "branch:select", config);
1458
1983
  return this.appendStep(node);
1459
1984
  }
1460
1985
  // ── foreach: array iteration ─────────────────────────────────
@@ -1465,367 +1990,63 @@ var Workflow = class _Workflow extends SealedWorkflow {
1465
1990
  * @param options.id Override the default step id (`foreach:<agentId>` or
1466
1991
  * the workflow's id). Required when chaining multiple foreach over the same
1467
1992
  * target — the construction-time `(type, id)` walk rejects duplicates.
1468
- * @param options.concurrency Max items in flight at any moment (default 1).
1469
- * Backed by a semaphore: as soon as one item completes, the next launches —
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 —
1470
1997
  * no lockstep batching.
1471
1998
  * @param options.onError Per-iteration error handler. **Bypassed entirely on
1472
- * the suspension path** (when any item hits a nested gate) see the
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
1473
2002
  * foreach concurrency hazards in the README. Otherwise: return a
1474
2003
  * `TNextOutput` value to substitute, return `Workflow.SKIP` to omit, throw
1475
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.
1476
2008
  */
1477
2009
  foreach(target, options) {
1478
- const concurrency = options?.concurrency ?? 1;
1479
- const onError = options?.onError;
1480
- const isWorkflow = target instanceof SealedWorkflow;
1481
- const defaultId = isWorkflow ? target.id ?? "foreach" : `foreach:${target.id}`;
1482
- const id = options?.id ?? defaultId;
1483
- const node = {
1484
- type: "step",
1485
- id,
1486
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1487
- nestedWorkflow: isWorkflow ? target : void 0,
1488
- category: "foreach",
1489
- execute: async (state) => {
1490
- const items = state.output;
1491
- if (!Array.isArray(items)) {
1492
- throw new Error(`foreach "${id}": expected array input, got ${typeof items}`);
1493
- }
1494
- const ctx = state.ctx;
1495
- const results = new Array(items.length);
1496
- const skipped = /* @__PURE__ */ new Set();
1497
- const itemStates = new Array(items.length);
1498
- const executeItem = async (item, index) => {
1499
- const itemState = {
1500
- ctx: state.ctx,
1501
- output: item,
1502
- mode: "generate",
1503
- abortSignal: state.abortSignal
1504
- };
1505
- itemStates[index] = itemState;
1506
- const itemStart = performance.now();
1507
- await this.fireHook(state, "onItemStart", {
1508
- stepId: id,
1509
- type: "foreach",
1510
- itemIndex: index,
1511
- ctx: state.ctx,
1512
- input: item
1513
- });
1514
- try {
1515
- if (isWorkflow) {
1516
- await this.executeNestedWorkflow(itemState, target);
1517
- } else {
1518
- await this.executeAgent(itemState, target, ctx);
1519
- }
1520
- results[index] = itemState.output;
1521
- await this.fireHook(state, "onItemFinish", {
1522
- stepId: id,
1523
- type: "foreach",
1524
- itemIndex: index,
1525
- ctx: state.ctx,
1526
- output: itemState.output,
1527
- durationMs: performance.now() - itemStart
1528
- });
1529
- } catch (error) {
1530
- await this.fireHook(state, "onItemError", {
1531
- stepId: id,
1532
- type: "foreach",
1533
- itemIndex: index,
1534
- ctx: state.ctx,
1535
- error,
1536
- durationMs: performance.now() - itemStart
1537
- });
1538
- throw error;
1539
- }
1540
- };
1541
- const mergeItemWarnings = () => {
1542
- for (let idx = 0; idx < items.length; idx++) {
1543
- const its = itemStates[idx];
1544
- if (!its?.warnings) continue;
1545
- for (const w of its.warnings) {
1546
- pushWarning(state, w.source, `${id}[${idx}]:${w.stepId}`, w.error);
1547
- }
1548
- }
1549
- };
1550
- const handleRejection = async (error, item, index) => {
1551
- if (!onError) throw error;
1552
- const recovered = await onError({
1553
- error,
1554
- item,
1555
- index,
1556
- ctx: state.ctx
1557
- });
1558
- if (recovered === _Workflow.SKIP) {
1559
- skipped.add(index);
1560
- } else {
1561
- results[index] = recovered;
1562
- }
1563
- };
1564
- const failures = [];
1565
- const signal = state.abortSignal;
1566
- if (concurrency <= 1) {
1567
- for (let i = 0; i < items.length; i++) {
1568
- if (signal?.aborted) {
1569
- failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
1570
- continue;
1571
- }
1572
- try {
1573
- await executeItem(items[i], i);
1574
- } catch (error) {
1575
- failures.push({ index: i, error });
1576
- }
1577
- }
1578
- } else {
1579
- let nextIndex = 0;
1580
- const worker = async () => {
1581
- while (true) {
1582
- const i = nextIndex++;
1583
- if (i >= items.length) return;
1584
- if (signal?.aborted) {
1585
- failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
1586
- continue;
1587
- }
1588
- try {
1589
- await executeItem(items[i], i);
1590
- } catch (error) {
1591
- failures.push({ index: i, error });
1592
- }
1593
- }
1594
- };
1595
- const workers = Array.from(
1596
- { length: Math.min(concurrency, items.length) },
1597
- () => worker()
1598
- );
1599
- await Promise.all(workers);
1600
- }
1601
- failures.sort((a, b) => a.index - b.index);
1602
- const gateFailures = [];
1603
- const nonGateFailures = [];
1604
- for (const f of failures) {
1605
- if (f.error instanceof NestedGateUnsupportedError) {
1606
- gateFailures.push({ index: f.index, error: f.error });
1607
- } else {
1608
- nonGateFailures.push(f);
1609
- }
1610
- }
1611
- mergeItemWarnings();
1612
- if (gateFailures.length > 0) {
1613
- for (const nr of nonGateFailures) {
1614
- pushWarning(state, "foreach-sibling", `${id}[${nr.index}]`, nr.error);
1615
- }
1616
- const lowest = gateFailures[0];
1617
- const otherSuspensions = gateFailures.slice(1).map((g) => ({
1618
- index: g.index,
1619
- gateId: g.error.gateId
1620
- }));
1621
- const siblingErrors = nonGateFailures.map((nr) => nr.error);
1622
- throw new NestedGateUnsupportedError(
1623
- lowest.error.gateId,
1624
- lowest.error.workflowId,
1625
- siblingErrors,
1626
- otherSuspensions
1627
- );
1628
- }
1629
- for (const { index, error } of nonGateFailures) {
1630
- await handleRejection(error, items[index], index);
1631
- }
1632
- state.output = skipped.size === 0 ? results : results.filter((_, i) => !skipped.has(i));
1633
- }
1634
- };
2010
+ const node = new ForeachStep(target, options, this.observability);
1635
2011
  return this.appendStep(node);
1636
2012
  }
1637
2013
  // Implementation
1638
2014
  parallel(branches, options) {
1639
- const isTuple = Array.isArray(branches);
1640
- const entries = isTuple ? branches.map((target, i) => ({ key: i, index: i, target })) : Object.entries(branches).map(([k, t], i) => ({ key: k, index: i, target: t }));
1641
- const branchCount = entries.length;
1642
- const requestedConcurrency = options?.concurrency;
1643
- let effectiveConcurrency;
1644
- if (requestedConcurrency === void 0) {
1645
- effectiveConcurrency = Math.min(branchCount, 5);
1646
- } else {
1647
- effectiveConcurrency = requestedConcurrency;
1648
- }
1649
- if (requestedConcurrency === void 0 && branchCount > 5) {
1650
- warnOnce(
1651
- "pipeai:parallel-cap",
1652
- `pipeai: parallel() with ${branchCount} branches capped at concurrency 5 by default. Pass { concurrency: ${branchCount} } (or Infinity) to opt in, or set { concurrency: N } if you want fewer.`
1653
- );
1654
- }
1655
- const onError = options?.onError;
1656
- const id = options?.id ?? (isTuple ? "parallel:tuple" : "parallel:record");
1657
- const node = {
1658
- type: "step",
1659
- id,
1660
- category: "parallel",
1661
- execute: async (state) => {
1662
- const ctx = state.ctx;
1663
- const input = state.output;
1664
- const results = isTuple ? new Array(branchCount) : {};
1665
- const branchStates = new Array(branchCount);
1666
- const executeBranch = async ({ key, index, target }) => {
1667
- const branchState = { ctx: state.ctx, output: input, mode: "generate" };
1668
- branchStates[index] = branchState;
1669
- const branchStart = performance.now();
1670
- const itemIndex = isTuple ? index : key;
1671
- await this.fireHook(state, "onItemStart", {
1672
- stepId: id,
1673
- type: "parallel",
1674
- itemIndex,
1675
- ctx: state.ctx,
1676
- input
1677
- });
1678
- try {
1679
- if (target instanceof SealedWorkflow) {
1680
- await this.executeNestedWorkflow(branchState, target);
1681
- } else {
1682
- await this.executeAgent(branchState, target, ctx);
1683
- }
1684
- results[key] = branchState.output;
1685
- await this.fireHook(state, "onItemFinish", {
1686
- stepId: id,
1687
- type: "parallel",
1688
- itemIndex,
1689
- ctx: state.ctx,
1690
- output: branchState.output,
1691
- durationMs: performance.now() - branchStart
1692
- });
1693
- } catch (error) {
1694
- await this.fireHook(state, "onItemError", {
1695
- stepId: id,
1696
- type: "parallel",
1697
- itemIndex,
1698
- ctx: state.ctx,
1699
- error,
1700
- durationMs: performance.now() - branchStart
1701
- });
1702
- throw error;
1703
- }
1704
- };
1705
- const failures = [];
1706
- const eff = Number.isFinite(effectiveConcurrency) ? Math.max(1, effectiveConcurrency) : branchCount;
1707
- if (eff <= 1) {
1708
- for (const e of entries) {
1709
- try {
1710
- await executeBranch(e);
1711
- } catch (error) {
1712
- failures.push({ key: e.key, index: e.index, error });
1713
- }
1714
- }
1715
- } else {
1716
- let nextIndex = 0;
1717
- const worker = async () => {
1718
- while (true) {
1719
- const i = nextIndex++;
1720
- if (i >= branchCount) return;
1721
- const e = entries[i];
1722
- try {
1723
- await executeBranch(e);
1724
- } catch (error) {
1725
- failures.push({ key: e.key, index: e.index, error });
1726
- }
1727
- }
1728
- };
1729
- const workers = Array.from(
1730
- { length: Math.min(eff, branchCount) },
1731
- () => worker()
1732
- );
1733
- await Promise.all(workers);
1734
- }
1735
- for (let idx = 0; idx < branchCount; idx++) {
1736
- const bs = branchStates[idx];
1737
- if (!bs?.warnings) continue;
1738
- for (const w of bs.warnings) {
1739
- pushWarning(state, w.source, `${id}[${entries[idx].key}]:${w.stepId}`, w.error);
1740
- }
1741
- }
1742
- const gateFailures = [];
1743
- const nonGateFailures = [];
1744
- for (const f of failures) {
1745
- if (f.error instanceof NestedGateUnsupportedError) gateFailures.push({ key: f.key, index: f.index, error: f.error });
1746
- else nonGateFailures.push(f);
1747
- }
1748
- gateFailures.sort((a, b) => a.index - b.index);
1749
- nonGateFailures.sort((a, b) => a.index - b.index);
1750
- if (gateFailures.length > 0) {
1751
- for (const nr of nonGateFailures) {
1752
- pushWarning(state, "foreach-sibling", `${id}[${nr.key}]`, nr.error);
1753
- }
1754
- const lowest = gateFailures[0];
1755
- const otherSuspensions = gateFailures.slice(1).map((g) => ({ index: g.index, gateId: g.error.gateId }));
1756
- const siblingErrors = nonGateFailures.map((nr) => nr.error);
1757
- throw new NestedGateUnsupportedError(
1758
- lowest.error.gateId,
1759
- lowest.error.workflowId,
1760
- siblingErrors,
1761
- otherSuspensions
1762
- );
1763
- }
1764
- for (const { key, index, error } of nonGateFailures) {
1765
- if (!onError) throw error;
1766
- const recovered = await onError({
1767
- error,
1768
- key: isTuple ? void 0 : key,
1769
- index: isTuple ? index : void 0,
1770
- ctx: state.ctx
1771
- });
1772
- if (recovered === _Workflow.SKIP) {
1773
- results[key] = void 0;
1774
- } else {
1775
- results[key] = recovered;
1776
- }
1777
- }
1778
- state.output = results;
1779
- }
1780
- };
2015
+ const node = new ParallelStep(branches, options, this.observability);
1781
2016
  return this.appendStep(node);
1782
2017
  }
1783
2018
  // ── repeat: conditional loop ─────────────────────────────────
1784
2019
  repeat(target, options) {
2020
+ if (options.maxIterations !== void 0 && (!Number.isInteger(options.maxIterations) || options.maxIterations < 1)) {
2021
+ throw new Error(`repeat: maxIterations must be a positive integer, got ${options.maxIterations}`);
2022
+ }
2023
+ if (options.until === void 0 === (options.while === void 0)) {
2024
+ throw new Error("repeat: requires exactly one of `until` or `while`");
2025
+ }
1785
2026
  const maxIterations = options.maxIterations ?? 10;
1786
2027
  const isWorkflow = target instanceof SealedWorkflow;
1787
2028
  const defaultId = isWorkflow ? target.id ?? "repeat" : `repeat:${target.id}`;
1788
2029
  const id = options.id ?? defaultId;
1789
2030
  const predicate = options.until ?? (async (p) => !await options.while(p));
1790
- const node = {
1791
- type: "step",
2031
+ const node = new RepeatStep(
1792
2032
  id,
1793
2033
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1794
- nestedWorkflow: isWorkflow ? target : void 0,
1795
- category: "repeat",
1796
- execute: async (state) => {
1797
- const ctx = state.ctx;
1798
- for (let i = 1; i <= maxIterations; i++) {
1799
- if (state.abortSignal?.aborted) {
1800
- throw state.abortSignal.reason ?? new Error("Workflow aborted");
1801
- }
1802
- if (isWorkflow) {
1803
- await this.executeNestedWorkflow(state, target);
1804
- } else {
1805
- await this.executeAgent(state, target, ctx);
1806
- }
1807
- const done = await predicate({
1808
- output: state.output,
1809
- ctx,
1810
- iterations: i
1811
- });
1812
- if (done) return;
1813
- }
1814
- throw new WorkflowLoopError(maxIterations, maxIterations);
1815
- }
1816
- };
2034
+ target,
2035
+ predicate,
2036
+ maxIterations,
2037
+ isWorkflow
2038
+ );
1817
2039
  return this.appendStep(node);
1818
2040
  }
1819
2041
  // ── catch ─────────────────────────────────────────────────────
1820
2042
  catch(id, fn) {
1821
- if (!this.steps.some((s) => s.type === "step")) {
1822
- throw new Error(`Workflow: catch("${id}") requires at least one preceding step.`);
2043
+ if (!this.steps.some((s) => s.type === "step" || s.type === "gate")) {
2044
+ throw new Error(`Workflow: catch("${id}") requires at least one preceding step or gate.`);
1823
2045
  }
1824
- const node = {
1825
- type: "catch",
2046
+ const node = new CatchStep(
1826
2047
  id,
1827
- catchFn: fn
1828
- };
2048
+ fn
2049
+ );
1829
2050
  return this.appendStep(node);
1830
2051
  }
1831
2052
  // `.finally()` is inherited from SealedWorkflow now (it lives there so
@@ -1836,10 +2057,10 @@ var Workflow = class _Workflow extends SealedWorkflow {
1836
2057
  var SKIP = Workflow.SKIP;
1837
2058
  // Annotate the CommonJS export names for ESM import in node:
1838
2059
  0 && (module.exports = {
2060
+ ABORT_STEP_ID,
1839
2061
  Agent,
1840
2062
  CHECKPOINT_STEP_ID,
1841
- CheckpointTimeoutError,
1842
- NestedGateUnsupportedError,
2063
+ GATE_RESUME_STEP_ID,
1843
2064
  SKIP,
1844
2065
  TOOL_PROVIDER_BRAND,
1845
2066
  ToolProvider,
@@ -1847,7 +2068,6 @@ var SKIP = Workflow.SKIP;
1847
2068
  WorkflowBranchError,
1848
2069
  WorkflowLoopError,
1849
2070
  defineTool,
1850
- getActiveWriter,
1851
2071
  isToolProvider,
1852
2072
  migrateSnapshot
1853
2073
  });