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