pipeai 0.8.2 → 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/README.md +75 -12
- package/dist/index.cjs +1154 -937
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -131
- package/dist/index.d.ts +337 -131
- package/dist/index.js +1152 -934
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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.
|
|
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
|
|
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.
|
|
743
|
-
let
|
|
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:
|
|
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
|
-
|
|
763
|
-
|
|
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
|
|
867
|
-
|
|
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
|
-
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
await this.fireStepErrorAndAttachCause(state, {
|
|
871
1566
|
stepId,
|
|
872
1567
|
type: obsType,
|
|
873
1568
|
ctx: state.ctx,
|
|
874
|
-
|
|
875
|
-
durationMs: performance.now() - sStart
|
|
876
|
-
suspended: false
|
|
1569
|
+
error: e,
|
|
1570
|
+
durationMs: performance.now() - sStart
|
|
877
1571
|
});
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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:
|
|
1580
|
+
error: newError.error,
|
|
885
1581
|
durationMs: performance.now() - sStart
|
|
886
1582
|
});
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
895
|
-
|
|
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
|
-
|
|
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,127 +1608,76 @@ 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.
|
|
1613
|
+
await this.fireStepErrorAndAttachCause(state, {
|
|
914
1614
|
stepId: CHECKPOINT_STEP_ID,
|
|
915
1615
|
type: "step",
|
|
916
1616
|
ctx: state.ctx,
|
|
917
1617
|
error: e,
|
|
918
1618
|
durationMs: performance.now() - ckptStart
|
|
919
1619
|
});
|
|
920
|
-
}
|
|
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);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1026
1622
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
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
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1322
|
-
|
|
1323
|
-
id
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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,31 +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
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
ctx: state.ctx,
|
|
1345
|
-
input: state.output,
|
|
1346
|
-
// Present in stream mode (undefined in generate mode), letting the
|
|
1347
|
-
// inline step emit UIMessageChunk parts onto the workflow's stream.
|
|
1348
|
-
writer: state.writer
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
};
|
|
1927
|
+
const node2 = new TransformStep(
|
|
1928
|
+
target,
|
|
1929
|
+
fn,
|
|
1930
|
+
inlineOptions
|
|
1931
|
+
);
|
|
1352
1932
|
return this.appendStep(node2);
|
|
1353
1933
|
}
|
|
1354
1934
|
const agent = target;
|
|
1355
1935
|
const options = optionsOrFn;
|
|
1356
|
-
const node =
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
};
|
|
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
|
+
);
|
|
1364
1943
|
return this.appendStep(node);
|
|
1365
1944
|
}
|
|
1366
1945
|
// ── gate: human-in-the-loop suspension point ────────────────
|
|
@@ -1368,16 +1947,9 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
1368
1947
|
if (this.steps.some((s) => s.type === "gate" && s.id === id)) {
|
|
1369
1948
|
throw new Error(`Workflow: duplicate gate ID "${id}". Each gate must have a unique identifier.`);
|
|
1370
1949
|
}
|
|
1371
|
-
const node =
|
|
1372
|
-
type: "gate",
|
|
1950
|
+
const node = new GateStep(
|
|
1373
1951
|
id,
|
|
1374
|
-
|
|
1375
|
-
condition: options?.condition ? async (state) => options.condition({
|
|
1376
|
-
ctx: state.ctx,
|
|
1377
|
-
input: state.output
|
|
1378
|
-
}) : void 0,
|
|
1379
|
-
merge: options?.merge ? (params) => options.merge(params) : void 0,
|
|
1380
|
-
payload: async (state) => {
|
|
1952
|
+
async (state) => {
|
|
1381
1953
|
if (options?.payload) {
|
|
1382
1954
|
return options.payload({
|
|
1383
1955
|
ctx: state.ctx,
|
|
@@ -1385,8 +1957,14 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
1385
1957
|
});
|
|
1386
1958
|
}
|
|
1387
1959
|
return state.output;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
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
|
+
);
|
|
1390
1968
|
return this.appendStep(node);
|
|
1391
1969
|
}
|
|
1392
1970
|
// ── branch: implementation ────────────────────────────────────
|
|
@@ -1397,67 +1975,11 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
1397
1975
|
return this.branchSelect(casesOrConfig, options?.id);
|
|
1398
1976
|
}
|
|
1399
1977
|
branchPredicate(cases, explicitId) {
|
|
1400
|
-
const node =
|
|
1401
|
-
type: "step",
|
|
1402
|
-
id: explicitId ?? "branch:predicate",
|
|
1403
|
-
category: "branch",
|
|
1404
|
-
execute: async (state) => {
|
|
1405
|
-
const ctx = state.ctx;
|
|
1406
|
-
const input = state.output;
|
|
1407
|
-
for (const branchCase of cases) {
|
|
1408
|
-
if (branchCase.when) {
|
|
1409
|
-
const match = await branchCase.when({ ctx, input });
|
|
1410
|
-
if (!match) continue;
|
|
1411
|
-
}
|
|
1412
|
-
await this.executeAgent(state, branchCase.agent, ctx, branchCase);
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
let inputRepr;
|
|
1416
|
-
try {
|
|
1417
|
-
inputRepr = JSON.stringify(input);
|
|
1418
|
-
if (inputRepr === void 0) inputRepr = String(input);
|
|
1419
|
-
} catch {
|
|
1420
|
-
inputRepr = `[unserializable ${typeof input}]`;
|
|
1421
|
-
}
|
|
1422
|
-
throw new WorkflowBranchError("predicate", `No branch matched and no default branch (a case without \`when\`) was provided. Input: ${inputRepr}`);
|
|
1423
|
-
}
|
|
1424
|
-
};
|
|
1978
|
+
const node = new PredicateBranchStep(explicitId ?? "branch:predicate", cases);
|
|
1425
1979
|
return this.appendStep(node);
|
|
1426
1980
|
}
|
|
1427
1981
|
branchSelect(config, explicitId) {
|
|
1428
|
-
const node =
|
|
1429
|
-
type: "step",
|
|
1430
|
-
id: explicitId ?? "branch:select",
|
|
1431
|
-
category: "branch",
|
|
1432
|
-
execute: async (state) => {
|
|
1433
|
-
const ctx = state.ctx;
|
|
1434
|
-
const input = state.output;
|
|
1435
|
-
const key = await config.select({ ctx, input });
|
|
1436
|
-
const keyDeclared = Object.prototype.hasOwnProperty.call(config.agents, key);
|
|
1437
|
-
if (keyDeclared && config.agents[key] === void 0) {
|
|
1438
|
-
throw new WorkflowBranchError(
|
|
1439
|
-
"select",
|
|
1440
|
-
`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(", ")}`
|
|
1441
|
-
);
|
|
1442
|
-
}
|
|
1443
|
-
let agent = keyDeclared ? config.agents[key] : void 0;
|
|
1444
|
-
if (!agent) {
|
|
1445
|
-
if (config.onUnknownKey) {
|
|
1446
|
-
config.onUnknownKey({
|
|
1447
|
-
key,
|
|
1448
|
-
availableKeys: Object.keys(config.agents),
|
|
1449
|
-
ctx
|
|
1450
|
-
});
|
|
1451
|
-
}
|
|
1452
|
-
if (config.fallback) {
|
|
1453
|
-
agent = config.fallback;
|
|
1454
|
-
} else {
|
|
1455
|
-
throw new WorkflowBranchError("select", `No agent found for key "${key}" and no fallback provided. Available keys: ${Object.keys(config.agents).join(", ")}`);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
await this.executeAgent(state, agent, ctx, config);
|
|
1459
|
-
}
|
|
1460
|
-
};
|
|
1982
|
+
const node = new SelectBranchStep(explicitId ?? "branch:select", config);
|
|
1461
1983
|
return this.appendStep(node);
|
|
1462
1984
|
}
|
|
1463
1985
|
// ── foreach: array iteration ─────────────────────────────────
|
|
@@ -1468,367 +1990,63 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
1468
1990
|
* @param options.id Override the default step id (`foreach:<agentId>` or
|
|
1469
1991
|
* the workflow's id). Required when chaining multiple foreach over the same
|
|
1470
1992
|
* target — the construction-time `(type, id)` walk rejects duplicates.
|
|
1471
|
-
* @param options.concurrency Max items in flight at any moment
|
|
1472
|
-
*
|
|
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 —
|
|
1473
1997
|
* no lockstep batching.
|
|
1474
1998
|
* @param options.onError Per-iteration error handler. **Bypassed entirely on
|
|
1475
|
-
* the suspension path** (when any item hits a nested gate)
|
|
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
|
|
1476
2002
|
* foreach concurrency hazards in the README. Otherwise: return a
|
|
1477
2003
|
* `TNextOutput` value to substitute, return `Workflow.SKIP` to omit, throw
|
|
1478
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.
|
|
1479
2008
|
*/
|
|
1480
2009
|
foreach(target, options) {
|
|
1481
|
-
const
|
|
1482
|
-
const onError = options?.onError;
|
|
1483
|
-
const isWorkflow = target instanceof SealedWorkflow;
|
|
1484
|
-
const defaultId = isWorkflow ? target.id ?? "foreach" : `foreach:${target.id}`;
|
|
1485
|
-
const id = options?.id ?? defaultId;
|
|
1486
|
-
const node = {
|
|
1487
|
-
type: "step",
|
|
1488
|
-
id,
|
|
1489
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1490
|
-
nestedWorkflow: isWorkflow ? target : void 0,
|
|
1491
|
-
category: "foreach",
|
|
1492
|
-
execute: async (state) => {
|
|
1493
|
-
const items = state.output;
|
|
1494
|
-
if (!Array.isArray(items)) {
|
|
1495
|
-
throw new Error(`foreach "${id}": expected array input, got ${typeof items}`);
|
|
1496
|
-
}
|
|
1497
|
-
const ctx = state.ctx;
|
|
1498
|
-
const results = new Array(items.length);
|
|
1499
|
-
const skipped = /* @__PURE__ */ new Set();
|
|
1500
|
-
const itemStates = new Array(items.length);
|
|
1501
|
-
const executeItem = async (item, index) => {
|
|
1502
|
-
const itemState = {
|
|
1503
|
-
ctx: state.ctx,
|
|
1504
|
-
output: item,
|
|
1505
|
-
mode: "generate",
|
|
1506
|
-
abortSignal: state.abortSignal
|
|
1507
|
-
};
|
|
1508
|
-
itemStates[index] = itemState;
|
|
1509
|
-
const itemStart = performance.now();
|
|
1510
|
-
await this.fireHook(state, "onItemStart", {
|
|
1511
|
-
stepId: id,
|
|
1512
|
-
type: "foreach",
|
|
1513
|
-
itemIndex: index,
|
|
1514
|
-
ctx: state.ctx,
|
|
1515
|
-
input: item
|
|
1516
|
-
});
|
|
1517
|
-
try {
|
|
1518
|
-
if (isWorkflow) {
|
|
1519
|
-
await this.executeNestedWorkflow(itemState, target);
|
|
1520
|
-
} else {
|
|
1521
|
-
await this.executeAgent(itemState, target, ctx);
|
|
1522
|
-
}
|
|
1523
|
-
results[index] = itemState.output;
|
|
1524
|
-
await this.fireHook(state, "onItemFinish", {
|
|
1525
|
-
stepId: id,
|
|
1526
|
-
type: "foreach",
|
|
1527
|
-
itemIndex: index,
|
|
1528
|
-
ctx: state.ctx,
|
|
1529
|
-
output: itemState.output,
|
|
1530
|
-
durationMs: performance.now() - itemStart
|
|
1531
|
-
});
|
|
1532
|
-
} catch (error) {
|
|
1533
|
-
await this.fireHook(state, "onItemError", {
|
|
1534
|
-
stepId: id,
|
|
1535
|
-
type: "foreach",
|
|
1536
|
-
itemIndex: index,
|
|
1537
|
-
ctx: state.ctx,
|
|
1538
|
-
error,
|
|
1539
|
-
durationMs: performance.now() - itemStart
|
|
1540
|
-
});
|
|
1541
|
-
throw error;
|
|
1542
|
-
}
|
|
1543
|
-
};
|
|
1544
|
-
const mergeItemWarnings = () => {
|
|
1545
|
-
for (let idx = 0; idx < items.length; idx++) {
|
|
1546
|
-
const its = itemStates[idx];
|
|
1547
|
-
if (!its?.warnings) continue;
|
|
1548
|
-
for (const w of its.warnings) {
|
|
1549
|
-
pushWarning(state, w.source, `${id}[${idx}]:${w.stepId}`, w.error);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
};
|
|
1553
|
-
const handleRejection = async (error, item, index) => {
|
|
1554
|
-
if (!onError) throw error;
|
|
1555
|
-
const recovered = await onError({
|
|
1556
|
-
error,
|
|
1557
|
-
item,
|
|
1558
|
-
index,
|
|
1559
|
-
ctx: state.ctx
|
|
1560
|
-
});
|
|
1561
|
-
if (recovered === _Workflow.SKIP) {
|
|
1562
|
-
skipped.add(index);
|
|
1563
|
-
} else {
|
|
1564
|
-
results[index] = recovered;
|
|
1565
|
-
}
|
|
1566
|
-
};
|
|
1567
|
-
const failures = [];
|
|
1568
|
-
const signal = state.abortSignal;
|
|
1569
|
-
if (concurrency <= 1) {
|
|
1570
|
-
for (let i = 0; i < items.length; i++) {
|
|
1571
|
-
if (signal?.aborted) {
|
|
1572
|
-
failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
|
|
1573
|
-
continue;
|
|
1574
|
-
}
|
|
1575
|
-
try {
|
|
1576
|
-
await executeItem(items[i], i);
|
|
1577
|
-
} catch (error) {
|
|
1578
|
-
failures.push({ index: i, error });
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
} else {
|
|
1582
|
-
let nextIndex = 0;
|
|
1583
|
-
const worker = async () => {
|
|
1584
|
-
while (true) {
|
|
1585
|
-
const i = nextIndex++;
|
|
1586
|
-
if (i >= items.length) return;
|
|
1587
|
-
if (signal?.aborted) {
|
|
1588
|
-
failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
|
|
1589
|
-
continue;
|
|
1590
|
-
}
|
|
1591
|
-
try {
|
|
1592
|
-
await executeItem(items[i], i);
|
|
1593
|
-
} catch (error) {
|
|
1594
|
-
failures.push({ index: i, error });
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
};
|
|
1598
|
-
const workers = Array.from(
|
|
1599
|
-
{ length: Math.min(concurrency, items.length) },
|
|
1600
|
-
() => worker()
|
|
1601
|
-
);
|
|
1602
|
-
await Promise.all(workers);
|
|
1603
|
-
}
|
|
1604
|
-
failures.sort((a, b) => a.index - b.index);
|
|
1605
|
-
const gateFailures = [];
|
|
1606
|
-
const nonGateFailures = [];
|
|
1607
|
-
for (const f of failures) {
|
|
1608
|
-
if (f.error instanceof NestedGateUnsupportedError) {
|
|
1609
|
-
gateFailures.push({ index: f.index, error: f.error });
|
|
1610
|
-
} else {
|
|
1611
|
-
nonGateFailures.push(f);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
mergeItemWarnings();
|
|
1615
|
-
if (gateFailures.length > 0) {
|
|
1616
|
-
for (const nr of nonGateFailures) {
|
|
1617
|
-
pushWarning(state, "foreach-sibling", `${id}[${nr.index}]`, nr.error);
|
|
1618
|
-
}
|
|
1619
|
-
const lowest = gateFailures[0];
|
|
1620
|
-
const otherSuspensions = gateFailures.slice(1).map((g) => ({
|
|
1621
|
-
index: g.index,
|
|
1622
|
-
gateId: g.error.gateId
|
|
1623
|
-
}));
|
|
1624
|
-
const siblingErrors = nonGateFailures.map((nr) => nr.error);
|
|
1625
|
-
throw new NestedGateUnsupportedError(
|
|
1626
|
-
lowest.error.gateId,
|
|
1627
|
-
lowest.error.workflowId,
|
|
1628
|
-
siblingErrors,
|
|
1629
|
-
otherSuspensions
|
|
1630
|
-
);
|
|
1631
|
-
}
|
|
1632
|
-
for (const { index, error } of nonGateFailures) {
|
|
1633
|
-
await handleRejection(error, items[index], index);
|
|
1634
|
-
}
|
|
1635
|
-
state.output = skipped.size === 0 ? results : results.filter((_, i) => !skipped.has(i));
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
2010
|
+
const node = new ForeachStep(target, options, this.observability);
|
|
1638
2011
|
return this.appendStep(node);
|
|
1639
2012
|
}
|
|
1640
2013
|
// Implementation
|
|
1641
2014
|
parallel(branches, options) {
|
|
1642
|
-
const
|
|
1643
|
-
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 }));
|
|
1644
|
-
const branchCount = entries.length;
|
|
1645
|
-
const requestedConcurrency = options?.concurrency;
|
|
1646
|
-
let effectiveConcurrency;
|
|
1647
|
-
if (requestedConcurrency === void 0) {
|
|
1648
|
-
effectiveConcurrency = Math.min(branchCount, 5);
|
|
1649
|
-
} else {
|
|
1650
|
-
effectiveConcurrency = requestedConcurrency;
|
|
1651
|
-
}
|
|
1652
|
-
if (requestedConcurrency === void 0 && branchCount > 5) {
|
|
1653
|
-
warnOnce(
|
|
1654
|
-
"pipeai:parallel-cap",
|
|
1655
|
-
`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.`
|
|
1656
|
-
);
|
|
1657
|
-
}
|
|
1658
|
-
const onError = options?.onError;
|
|
1659
|
-
const id = options?.id ?? (isTuple ? "parallel:tuple" : "parallel:record");
|
|
1660
|
-
const node = {
|
|
1661
|
-
type: "step",
|
|
1662
|
-
id,
|
|
1663
|
-
category: "parallel",
|
|
1664
|
-
execute: async (state) => {
|
|
1665
|
-
const ctx = state.ctx;
|
|
1666
|
-
const input = state.output;
|
|
1667
|
-
const results = isTuple ? new Array(branchCount) : {};
|
|
1668
|
-
const branchStates = new Array(branchCount);
|
|
1669
|
-
const executeBranch = async ({ key, index, target }) => {
|
|
1670
|
-
const branchState = { ctx: state.ctx, output: input, mode: "generate" };
|
|
1671
|
-
branchStates[index] = branchState;
|
|
1672
|
-
const branchStart = performance.now();
|
|
1673
|
-
const itemIndex = isTuple ? index : key;
|
|
1674
|
-
await this.fireHook(state, "onItemStart", {
|
|
1675
|
-
stepId: id,
|
|
1676
|
-
type: "parallel",
|
|
1677
|
-
itemIndex,
|
|
1678
|
-
ctx: state.ctx,
|
|
1679
|
-
input
|
|
1680
|
-
});
|
|
1681
|
-
try {
|
|
1682
|
-
if (target instanceof SealedWorkflow) {
|
|
1683
|
-
await this.executeNestedWorkflow(branchState, target);
|
|
1684
|
-
} else {
|
|
1685
|
-
await this.executeAgent(branchState, target, ctx);
|
|
1686
|
-
}
|
|
1687
|
-
results[key] = branchState.output;
|
|
1688
|
-
await this.fireHook(state, "onItemFinish", {
|
|
1689
|
-
stepId: id,
|
|
1690
|
-
type: "parallel",
|
|
1691
|
-
itemIndex,
|
|
1692
|
-
ctx: state.ctx,
|
|
1693
|
-
output: branchState.output,
|
|
1694
|
-
durationMs: performance.now() - branchStart
|
|
1695
|
-
});
|
|
1696
|
-
} catch (error) {
|
|
1697
|
-
await this.fireHook(state, "onItemError", {
|
|
1698
|
-
stepId: id,
|
|
1699
|
-
type: "parallel",
|
|
1700
|
-
itemIndex,
|
|
1701
|
-
ctx: state.ctx,
|
|
1702
|
-
error,
|
|
1703
|
-
durationMs: performance.now() - branchStart
|
|
1704
|
-
});
|
|
1705
|
-
throw error;
|
|
1706
|
-
}
|
|
1707
|
-
};
|
|
1708
|
-
const failures = [];
|
|
1709
|
-
const eff = Number.isFinite(effectiveConcurrency) ? Math.max(1, effectiveConcurrency) : branchCount;
|
|
1710
|
-
if (eff <= 1) {
|
|
1711
|
-
for (const e of entries) {
|
|
1712
|
-
try {
|
|
1713
|
-
await executeBranch(e);
|
|
1714
|
-
} catch (error) {
|
|
1715
|
-
failures.push({ key: e.key, index: e.index, error });
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
} else {
|
|
1719
|
-
let nextIndex = 0;
|
|
1720
|
-
const worker = async () => {
|
|
1721
|
-
while (true) {
|
|
1722
|
-
const i = nextIndex++;
|
|
1723
|
-
if (i >= branchCount) return;
|
|
1724
|
-
const e = entries[i];
|
|
1725
|
-
try {
|
|
1726
|
-
await executeBranch(e);
|
|
1727
|
-
} catch (error) {
|
|
1728
|
-
failures.push({ key: e.key, index: e.index, error });
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
};
|
|
1732
|
-
const workers = Array.from(
|
|
1733
|
-
{ length: Math.min(eff, branchCount) },
|
|
1734
|
-
() => worker()
|
|
1735
|
-
);
|
|
1736
|
-
await Promise.all(workers);
|
|
1737
|
-
}
|
|
1738
|
-
for (let idx = 0; idx < branchCount; idx++) {
|
|
1739
|
-
const bs = branchStates[idx];
|
|
1740
|
-
if (!bs?.warnings) continue;
|
|
1741
|
-
for (const w of bs.warnings) {
|
|
1742
|
-
pushWarning(state, w.source, `${id}[${entries[idx].key}]:${w.stepId}`, w.error);
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
const gateFailures = [];
|
|
1746
|
-
const nonGateFailures = [];
|
|
1747
|
-
for (const f of failures) {
|
|
1748
|
-
if (f.error instanceof NestedGateUnsupportedError) gateFailures.push({ key: f.key, index: f.index, error: f.error });
|
|
1749
|
-
else nonGateFailures.push(f);
|
|
1750
|
-
}
|
|
1751
|
-
gateFailures.sort((a, b) => a.index - b.index);
|
|
1752
|
-
nonGateFailures.sort((a, b) => a.index - b.index);
|
|
1753
|
-
if (gateFailures.length > 0) {
|
|
1754
|
-
for (const nr of nonGateFailures) {
|
|
1755
|
-
pushWarning(state, "foreach-sibling", `${id}[${nr.key}]`, nr.error);
|
|
1756
|
-
}
|
|
1757
|
-
const lowest = gateFailures[0];
|
|
1758
|
-
const otherSuspensions = gateFailures.slice(1).map((g) => ({ index: g.index, gateId: g.error.gateId }));
|
|
1759
|
-
const siblingErrors = nonGateFailures.map((nr) => nr.error);
|
|
1760
|
-
throw new NestedGateUnsupportedError(
|
|
1761
|
-
lowest.error.gateId,
|
|
1762
|
-
lowest.error.workflowId,
|
|
1763
|
-
siblingErrors,
|
|
1764
|
-
otherSuspensions
|
|
1765
|
-
);
|
|
1766
|
-
}
|
|
1767
|
-
for (const { key, index, error } of nonGateFailures) {
|
|
1768
|
-
if (!onError) throw error;
|
|
1769
|
-
const recovered = await onError({
|
|
1770
|
-
error,
|
|
1771
|
-
key: isTuple ? void 0 : key,
|
|
1772
|
-
index: isTuple ? index : void 0,
|
|
1773
|
-
ctx: state.ctx
|
|
1774
|
-
});
|
|
1775
|
-
if (recovered === _Workflow.SKIP) {
|
|
1776
|
-
results[key] = void 0;
|
|
1777
|
-
} else {
|
|
1778
|
-
results[key] = recovered;
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
state.output = results;
|
|
1782
|
-
}
|
|
1783
|
-
};
|
|
2015
|
+
const node = new ParallelStep(branches, options, this.observability);
|
|
1784
2016
|
return this.appendStep(node);
|
|
1785
2017
|
}
|
|
1786
2018
|
// ── repeat: conditional loop ─────────────────────────────────
|
|
1787
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
|
+
}
|
|
1788
2026
|
const maxIterations = options.maxIterations ?? 10;
|
|
1789
2027
|
const isWorkflow = target instanceof SealedWorkflow;
|
|
1790
2028
|
const defaultId = isWorkflow ? target.id ?? "repeat" : `repeat:${target.id}`;
|
|
1791
2029
|
const id = options.id ?? defaultId;
|
|
1792
2030
|
const predicate = options.until ?? (async (p) => !await options.while(p));
|
|
1793
|
-
const node =
|
|
1794
|
-
type: "step",
|
|
2031
|
+
const node = new RepeatStep(
|
|
1795
2032
|
id,
|
|
1796
2033
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
if (state.abortSignal?.aborted) {
|
|
1803
|
-
throw state.abortSignal.reason ?? new Error("Workflow aborted");
|
|
1804
|
-
}
|
|
1805
|
-
if (isWorkflow) {
|
|
1806
|
-
await this.executeNestedWorkflow(state, target);
|
|
1807
|
-
} else {
|
|
1808
|
-
await this.executeAgent(state, target, ctx);
|
|
1809
|
-
}
|
|
1810
|
-
const done = await predicate({
|
|
1811
|
-
output: state.output,
|
|
1812
|
-
ctx,
|
|
1813
|
-
iterations: i
|
|
1814
|
-
});
|
|
1815
|
-
if (done) return;
|
|
1816
|
-
}
|
|
1817
|
-
throw new WorkflowLoopError(maxIterations, maxIterations);
|
|
1818
|
-
}
|
|
1819
|
-
};
|
|
2034
|
+
target,
|
|
2035
|
+
predicate,
|
|
2036
|
+
maxIterations,
|
|
2037
|
+
isWorkflow
|
|
2038
|
+
);
|
|
1820
2039
|
return this.appendStep(node);
|
|
1821
2040
|
}
|
|
1822
2041
|
// ── catch ─────────────────────────────────────────────────────
|
|
1823
2042
|
catch(id, fn) {
|
|
1824
|
-
if (!this.steps.some((s) => s.type === "step")) {
|
|
1825
|
-
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.`);
|
|
1826
2045
|
}
|
|
1827
|
-
const node =
|
|
1828
|
-
type: "catch",
|
|
2046
|
+
const node = new CatchStep(
|
|
1829
2047
|
id,
|
|
1830
|
-
|
|
1831
|
-
|
|
2048
|
+
fn
|
|
2049
|
+
);
|
|
1832
2050
|
return this.appendStep(node);
|
|
1833
2051
|
}
|
|
1834
2052
|
// `.finally()` is inherited from SealedWorkflow now (it lives there so
|
|
@@ -1839,10 +2057,10 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
1839
2057
|
var SKIP = Workflow.SKIP;
|
|
1840
2058
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1841
2059
|
0 && (module.exports = {
|
|
2060
|
+
ABORT_STEP_ID,
|
|
1842
2061
|
Agent,
|
|
1843
2062
|
CHECKPOINT_STEP_ID,
|
|
1844
|
-
|
|
1845
|
-
NestedGateUnsupportedError,
|
|
2063
|
+
GATE_RESUME_STEP_ID,
|
|
1846
2064
|
SKIP,
|
|
1847
2065
|
TOOL_PROVIDER_BRAND,
|
|
1848
2066
|
ToolProvider,
|
|
@@ -1850,7 +2068,6 @@ var SKIP = Workflow.SKIP;
|
|
|
1850
2068
|
WorkflowBranchError,
|
|
1851
2069
|
WorkflowLoopError,
|
|
1852
2070
|
defineTool,
|
|
1853
|
-
getActiveWriter,
|
|
1854
2071
|
isToolProvider,
|
|
1855
2072
|
migrateSnapshot
|
|
1856
2073
|
});
|