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