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