pipeai 0.2.1 → 0.8.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 +482 -50
- package/dist/index.cjs +1242 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +553 -52
- package/dist/index.d.ts +553 -52
- package/dist/index.js +1232 -167
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -21,11 +21,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Agent: () => Agent,
|
|
24
|
+
CHECKPOINT_STEP_ID: () => CHECKPOINT_STEP_ID,
|
|
25
|
+
CheckpointTimeoutError: () => CheckpointTimeoutError,
|
|
26
|
+
NestedGateUnsupportedError: () => NestedGateUnsupportedError,
|
|
27
|
+
SKIP: () => SKIP,
|
|
28
|
+
TOOL_PROVIDER_BRAND: () => TOOL_PROVIDER_BRAND,
|
|
29
|
+
ToolProvider: () => ToolProvider,
|
|
24
30
|
Workflow: () => Workflow,
|
|
25
31
|
WorkflowBranchError: () => WorkflowBranchError,
|
|
26
32
|
WorkflowLoopError: () => WorkflowLoopError,
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
defineTool: () => defineTool,
|
|
34
|
+
getActiveWriter: () => getActiveWriter,
|
|
35
|
+
isToolProvider: () => isToolProvider,
|
|
36
|
+
migrateSnapshot: () => migrateSnapshot
|
|
29
37
|
});
|
|
30
38
|
module.exports = __toCommonJS(index_exports);
|
|
31
39
|
|
|
@@ -37,6 +45,7 @@ var import_ai = require("ai");
|
|
|
37
45
|
|
|
38
46
|
// src/utils.ts
|
|
39
47
|
var import_node_async_hooks = require("async_hooks");
|
|
48
|
+
var import_node_crypto = require("crypto");
|
|
40
49
|
var writerStorage = new import_node_async_hooks.AsyncLocalStorage();
|
|
41
50
|
function runWithWriter(writer, fn) {
|
|
42
51
|
return writerStorage.run(writer, fn);
|
|
@@ -50,13 +59,58 @@ function resolveValue(value, ctx, input) {
|
|
|
50
59
|
}
|
|
51
60
|
return value;
|
|
52
61
|
}
|
|
53
|
-
async function extractOutput(result, hasStructuredOutput) {
|
|
62
|
+
async function extractOutput(result, hasStructuredOutput, schema) {
|
|
54
63
|
if (hasStructuredOutput) {
|
|
55
64
|
const output = await result.output;
|
|
56
|
-
if (output
|
|
65
|
+
if (output === void 0) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Agent: structured output was declared but the model returned none. This usually means the model produced text that did not match the declared schema, or the underlying SDK did not parse the structured output."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (schema) {
|
|
71
|
+
return schema.parse(output);
|
|
72
|
+
}
|
|
73
|
+
return output;
|
|
57
74
|
}
|
|
58
75
|
return await result.text;
|
|
59
76
|
}
|
|
77
|
+
function deepFreeze(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
78
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return value;
|
|
79
|
+
if (Object.isFrozen(value)) return value;
|
|
80
|
+
seen.add(value);
|
|
81
|
+
Object.freeze(value);
|
|
82
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
83
|
+
deepFreeze(value[key], seen);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function computeStepShapeHash(steps, getNested) {
|
|
88
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonicalDescriptor(steps, getNested, /* @__PURE__ */ new WeakSet())).digest("hex");
|
|
89
|
+
}
|
|
90
|
+
function canonicalDescriptor(steps, getNested, path) {
|
|
91
|
+
return JSON.stringify(steps.map((s, i) => {
|
|
92
|
+
const triple = [i, s.type, s.id];
|
|
93
|
+
for (const inner of getNested(s)) {
|
|
94
|
+
if (path.has(inner)) {
|
|
95
|
+
triple.push(`<cycle:${inner.id ?? "anon"}>`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
path.add(inner);
|
|
99
|
+
try {
|
|
100
|
+
triple.push(canonicalDescriptor(inner.getStepsForShapeHash(), getNested, path));
|
|
101
|
+
} finally {
|
|
102
|
+
path.delete(inner);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return triple;
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
var warnedOnceKeys = /* @__PURE__ */ new Set();
|
|
109
|
+
function warnOnce(key, message) {
|
|
110
|
+
if (warnedOnceKeys.has(key)) return;
|
|
111
|
+
warnedOnceKeys.add(key);
|
|
112
|
+
console.warn(message ?? key);
|
|
113
|
+
}
|
|
60
114
|
|
|
61
115
|
// src/tool-provider.ts
|
|
62
116
|
var TOOL_PROVIDER_BRAND = /* @__PURE__ */ Symbol.for("agent-workflow.ToolProvider");
|
|
@@ -87,10 +141,17 @@ var Agent = class {
|
|
|
87
141
|
id;
|
|
88
142
|
description;
|
|
89
143
|
hasOutput;
|
|
144
|
+
/**
|
|
145
|
+
* Zod schema used to validate the agent's structured `output` after the AI
|
|
146
|
+
* SDK returns. Distinct from `tool.outputSchema` (which validates tool
|
|
147
|
+
* execution output). Exposed (readonly) so external runners — notably the
|
|
148
|
+
* workflow runtime — can pass it through to `extractOutput` without
|
|
149
|
+
* re-plumbing it.
|
|
150
|
+
*/
|
|
151
|
+
validateOutput;
|
|
90
152
|
config;
|
|
91
153
|
_hasDynamicConfig;
|
|
92
154
|
_resolvedStaticTools = null;
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
155
|
_passthrough;
|
|
95
156
|
_onStepFinish;
|
|
96
157
|
_onFinish;
|
|
@@ -98,6 +159,7 @@ var Agent = class {
|
|
|
98
159
|
this.id = config.id;
|
|
99
160
|
this.description = config.description ?? "";
|
|
100
161
|
this.hasOutput = config.output !== void 0;
|
|
162
|
+
this.validateOutput = config.validateOutput;
|
|
101
163
|
this.config = config;
|
|
102
164
|
this._hasDynamicConfig = [
|
|
103
165
|
config.model,
|
|
@@ -106,8 +168,7 @@ var Agent = class {
|
|
|
106
168
|
config.messages,
|
|
107
169
|
config.tools,
|
|
108
170
|
config.activeTools,
|
|
109
|
-
config.toolChoice
|
|
110
|
-
config.stopWhen
|
|
171
|
+
config.toolChoice
|
|
111
172
|
].some((v) => typeof v === "function");
|
|
112
173
|
if (!this._hasDynamicConfig) {
|
|
113
174
|
const rawTools = config.tools ?? {};
|
|
@@ -121,6 +182,7 @@ var Agent = class {
|
|
|
121
182
|
description: _desc,
|
|
122
183
|
input: _inputSchema,
|
|
123
184
|
output: _output,
|
|
185
|
+
validateOutput: _validateOutput,
|
|
124
186
|
model: _m,
|
|
125
187
|
system: _s,
|
|
126
188
|
prompt: _p,
|
|
@@ -140,25 +202,13 @@ var Agent = class {
|
|
|
140
202
|
}
|
|
141
203
|
async generate(ctx, ...args) {
|
|
142
204
|
const input = args[0];
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
return await (0, import_ai2.generateText)(options);
|
|
147
|
-
} catch (error) {
|
|
148
|
-
if (this.config.onError) {
|
|
149
|
-
await this.config.onError({ error, ctx, input, writer: getActiveWriter() });
|
|
150
|
-
}
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
205
|
+
const callOptions = args[1];
|
|
206
|
+
return this.generateWithOptions(ctx, input, callOptions ?? {});
|
|
153
207
|
}
|
|
154
208
|
async stream(ctx, ...args) {
|
|
155
209
|
const input = args[0];
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
return (0, import_ai2.streamText)({
|
|
159
|
-
...options,
|
|
160
|
-
onError: this.config.onError ? ({ error }) => this.config.onError({ error, ctx, input, writer: getActiveWriter() }) : void 0
|
|
161
|
-
});
|
|
210
|
+
const callOptions = args[1];
|
|
211
|
+
return this.streamWithOptions(ctx, input, callOptions ?? {});
|
|
162
212
|
}
|
|
163
213
|
asTool(ctx, options) {
|
|
164
214
|
return this.createToolInstance(ctx, options);
|
|
@@ -179,24 +229,86 @@ var Agent = class {
|
|
|
179
229
|
return (0, import_ai2.tool)({
|
|
180
230
|
description: this.description,
|
|
181
231
|
inputSchema: this.config.input,
|
|
182
|
-
|
|
232
|
+
// The AI SDK passes a `ToolExecutionOptions` argument that carries
|
|
233
|
+
// `abortSignal`, `toolCallId`, `messages`, etc. Forward `abortSignal` so
|
|
234
|
+
// a parent agent's abort cancels in-flight sub-agent calls instead of
|
|
235
|
+
// leaving them running and producing detached output.
|
|
236
|
+
execute: async (toolInput, execOptions) => {
|
|
237
|
+
const abortSignal = execOptions?.abortSignal;
|
|
183
238
|
const writer = getActiveWriter();
|
|
184
239
|
if (writer) {
|
|
185
|
-
const result2 = await this.
|
|
240
|
+
const result2 = await this.streamWithOptions(ctx, toolInput, { abortSignal });
|
|
186
241
|
writer.merge(result2.toUIMessageStream());
|
|
187
|
-
if (options?.mapOutput)
|
|
188
|
-
|
|
242
|
+
if (options?.mapOutput) {
|
|
243
|
+
await result2.text;
|
|
244
|
+
return options.mapOutput(result2);
|
|
245
|
+
}
|
|
246
|
+
return extractOutput(result2, this.hasOutput, this.validateOutput);
|
|
189
247
|
}
|
|
190
|
-
const result = await this.
|
|
248
|
+
const result = await this.generateWithOptions(ctx, toolInput, { abortSignal });
|
|
191
249
|
if (options?.mapOutput) return options.mapOutput(result);
|
|
192
|
-
return extractOutput(result, this.hasOutput);
|
|
250
|
+
return extractOutput(result, this.hasOutput, this.validateOutput);
|
|
193
251
|
}
|
|
194
252
|
// TS cannot simplify the SDK's `NeverOptional<TOutput, ...>` conditional in a
|
|
195
253
|
// generic context, so we cast through `unknown` instead of `any`.
|
|
196
254
|
});
|
|
197
255
|
}
|
|
198
|
-
//
|
|
256
|
+
// ── Internal: shared call helpers ─────────────────────────────
|
|
257
|
+
// `generate()` / `stream()` and the `asTool()` wrapper all funnel through
|
|
258
|
+
// these so the abortSignal-forwarding and onError-wrapping logic stays in
|
|
259
|
+
// one place. `extra` carries per-call overrides (currently `abortSignal`).
|
|
260
|
+
// If the user-supplied onError callback itself throws, attach the original
|
|
261
|
+
// model error as `.cause` on the new error and rethrow the wrapper. Without
|
|
262
|
+
// this, the original error is silently shadowed.
|
|
263
|
+
async invokeOnError(error, ctx, input) {
|
|
264
|
+
if (!this.config.onError) return;
|
|
265
|
+
try {
|
|
266
|
+
await this.config.onError({ error, ctx, input, writer: getActiveWriter() });
|
|
267
|
+
} catch (handlerError) {
|
|
268
|
+
if (handlerError instanceof Error) {
|
|
269
|
+
if (handlerError.cause === void 0) {
|
|
270
|
+
handlerError.cause = error;
|
|
271
|
+
}
|
|
272
|
+
throw handlerError;
|
|
273
|
+
}
|
|
274
|
+
const wrapped = new Error(
|
|
275
|
+
`Agent "${this.id}": onError handler threw a non-Error value (${typeof handlerError}): ${String(handlerError)}`
|
|
276
|
+
);
|
|
277
|
+
wrapped.cause = error;
|
|
278
|
+
throw wrapped;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async generateWithOptions(ctx, input, extra) {
|
|
282
|
+
const resolved = await this.resolveConfig(ctx, input);
|
|
283
|
+
const options = this.buildCallOptions(resolved, ctx, input);
|
|
284
|
+
try {
|
|
285
|
+
return await (0, import_ai2.generateText)({ ...options, ...extra });
|
|
286
|
+
} catch (error) {
|
|
287
|
+
await this.invokeOnError(error, ctx, input);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async streamWithOptions(ctx, input, extra) {
|
|
292
|
+
const resolved = await this.resolveConfig(ctx, input);
|
|
293
|
+
const options = this.buildCallOptions(resolved, ctx, input);
|
|
294
|
+
try {
|
|
295
|
+
const onErrorOption = this.config.onError ? { onError: ({ error }) => this.invokeOnError(error, ctx, input) } : {};
|
|
296
|
+
return (0, import_ai2.streamText)({
|
|
297
|
+
...options,
|
|
298
|
+
...extra,
|
|
299
|
+
...onErrorOption
|
|
300
|
+
});
|
|
301
|
+
} catch (error) {
|
|
302
|
+
await this.invokeOnError(error, ctx, input);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
199
306
|
buildCallOptions(resolved, ctx, input) {
|
|
307
|
+
if (resolved.messages === void 0 && resolved.prompt === void 0) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Agent "${this.id}": neither \`prompt\` nor \`messages\` was provided. Configure one of them, or supply a Resolvable that returns one based on input.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
200
312
|
return {
|
|
201
313
|
...this._passthrough,
|
|
202
314
|
model: resolved.model,
|
|
@@ -204,11 +316,16 @@ var Agent = class {
|
|
|
204
316
|
activeTools: resolved.activeTools,
|
|
205
317
|
toolChoice: resolved.toolChoice,
|
|
206
318
|
stopWhen: resolved.stopWhen,
|
|
207
|
-
...resolved.messages ? { messages: resolved.messages } : { prompt: resolved.prompt
|
|
208
|
-
|
|
319
|
+
...resolved.messages !== void 0 ? { messages: resolved.messages } : { prompt: resolved.prompt },
|
|
320
|
+
// Use `!== undefined` rather than truthy so an intentional empty
|
|
321
|
+
// `system: ""` survives instead of being silently dropped.
|
|
322
|
+
...resolved.system !== void 0 ? { system: resolved.system } : {},
|
|
209
323
|
...this.config.output ? { output: this.config.output } : {},
|
|
210
|
-
|
|
211
|
-
|
|
324
|
+
// Only attach the callback when the user supplied one. Passing
|
|
325
|
+
// `undefined` explicitly can suppress default SDK rethrow behavior in
|
|
326
|
+
// some versions.
|
|
327
|
+
...this._onStepFinish ? { onStepFinish: (event) => this._onStepFinish({ result: event, ctx, input, writer: getActiveWriter() }) } : {},
|
|
328
|
+
...this._onFinish ? { onFinish: (event) => this._onFinish({ result: event, ctx, input, writer: getActiveWriter() }) } : {}
|
|
212
329
|
};
|
|
213
330
|
}
|
|
214
331
|
resolveConfig(ctx, input) {
|
|
@@ -230,18 +347,27 @@ var Agent = class {
|
|
|
230
347
|
return this.resolveConfigAsync(ctx, input);
|
|
231
348
|
}
|
|
232
349
|
async resolveConfigAsync(ctx, input) {
|
|
233
|
-
const [model, prompt, system, messages, rawTools, activeTools, toolChoice
|
|
350
|
+
const [model, prompt, system, messages, rawTools, activeTools, toolChoice] = await Promise.all([
|
|
234
351
|
resolveValue(this.config.model, ctx, input),
|
|
235
352
|
resolveValue(this.config.prompt, ctx, input),
|
|
236
353
|
resolveValue(this.config.system, ctx, input),
|
|
237
354
|
resolveValue(this.config.messages, ctx, input),
|
|
238
355
|
resolveValue(this.config.tools, ctx, input),
|
|
239
356
|
resolveValue(this.config.activeTools, ctx, input),
|
|
240
|
-
resolveValue(this.config.toolChoice, ctx, input)
|
|
241
|
-
resolveValue(this.config.stopWhen, ctx, input)
|
|
357
|
+
resolveValue(this.config.toolChoice, ctx, input)
|
|
242
358
|
]);
|
|
243
359
|
const tools = this.resolveTools(rawTools ?? {}, ctx);
|
|
244
|
-
return {
|
|
360
|
+
return {
|
|
361
|
+
model,
|
|
362
|
+
prompt,
|
|
363
|
+
system,
|
|
364
|
+
messages,
|
|
365
|
+
tools,
|
|
366
|
+
activeTools,
|
|
367
|
+
toolChoice,
|
|
368
|
+
// `stopWhen` is always static — see field declaration for why.
|
|
369
|
+
stopWhen: this.config.stopWhen
|
|
370
|
+
};
|
|
245
371
|
}
|
|
246
372
|
resolveTools(tools, ctx) {
|
|
247
373
|
const entries = Object.entries(tools);
|
|
@@ -277,37 +403,284 @@ var WorkflowLoopError = class extends Error {
|
|
|
277
403
|
this.name = "WorkflowLoopError";
|
|
278
404
|
}
|
|
279
405
|
};
|
|
280
|
-
var
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
406
|
+
var NestedGateUnsupportedError = class extends Error {
|
|
407
|
+
gateId;
|
|
408
|
+
workflowId;
|
|
409
|
+
// Always present; non-gate rejections from concurrent foreach.
|
|
410
|
+
siblingErrors;
|
|
411
|
+
// Always present; OTHER suspending items in concurrent foreach.
|
|
412
|
+
siblingSuspensions;
|
|
413
|
+
constructor(gateId, workflowId, siblingErrors = [], siblingSuspensions = []) {
|
|
414
|
+
super(`Gate "${gateId}" hit inside nested workflow "${workflowId ?? "(anonymous)"}". Nested gates are not yet supported.`);
|
|
415
|
+
this.name = "NestedGateUnsupportedError";
|
|
416
|
+
this.gateId = gateId;
|
|
417
|
+
this.workflowId = workflowId;
|
|
418
|
+
this.siblingErrors = siblingErrors;
|
|
419
|
+
this.siblingSuspensions = siblingSuspensions;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
var CHECKPOINT_STEP_ID = "::pipeai::onCheckpoint";
|
|
423
|
+
var CheckpointTimeoutError = class extends Error {
|
|
424
|
+
timeoutMs;
|
|
425
|
+
constructor(timeoutMs) {
|
|
426
|
+
super(`onCheckpoint exceeded ${timeoutMs}ms timeout`);
|
|
427
|
+
this.name = "CheckpointTimeoutError";
|
|
428
|
+
this.timeoutMs = timeoutMs;
|
|
286
429
|
}
|
|
287
430
|
};
|
|
288
|
-
|
|
431
|
+
function resolveFreezeSnapshots(state) {
|
|
432
|
+
return state.runOptions?.freezeSnapshots ? true : false;
|
|
433
|
+
}
|
|
434
|
+
function migrateSnapshot(legacy) {
|
|
435
|
+
if (legacy.version !== 1) {
|
|
436
|
+
throw new Error(`migrateSnapshot: expected v1 snapshot, got version ${legacy.version}`);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
version: 2,
|
|
440
|
+
kind: "gate",
|
|
441
|
+
resumeFromIndex: legacy.resumeFromIndex,
|
|
442
|
+
output: legacy.output,
|
|
443
|
+
gateId: legacy.gateId,
|
|
444
|
+
gatePayload: legacy.gatePayload
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function getObservabilityType(node) {
|
|
448
|
+
if (node.type !== "step") return node.type;
|
|
449
|
+
return node.category ?? "step";
|
|
450
|
+
}
|
|
451
|
+
function getNestedWorkflows(node) {
|
|
452
|
+
switch (node.type) {
|
|
453
|
+
case "step":
|
|
454
|
+
return node.nestedWorkflow ? [node.nestedWorkflow] : [];
|
|
455
|
+
case "gate":
|
|
456
|
+
case "catch":
|
|
457
|
+
case "finally":
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function pendingErrorSourceToStepType(source) {
|
|
462
|
+
switch (source) {
|
|
463
|
+
case "step":
|
|
464
|
+
return "step";
|
|
465
|
+
case "finally":
|
|
466
|
+
return "finally";
|
|
467
|
+
case "catch":
|
|
468
|
+
return "catch";
|
|
469
|
+
case "onCheckpoint":
|
|
470
|
+
return "step";
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function emitCheckpoint(state, opts, resumeFromIndex, stepShapeHash) {
|
|
474
|
+
if (!opts.onCheckpoint) return;
|
|
475
|
+
const snap = {
|
|
476
|
+
version: 2,
|
|
477
|
+
kind: "checkpoint",
|
|
478
|
+
resumeFromIndex,
|
|
479
|
+
output: state.output,
|
|
480
|
+
stepShapeHash
|
|
481
|
+
};
|
|
482
|
+
if (resolveFreezeSnapshots(state)) deepFreeze(snap);
|
|
483
|
+
const controller = new AbortController();
|
|
484
|
+
if (opts.checkpointTimeout !== void 0) {
|
|
485
|
+
const timeoutMs = opts.checkpointTimeout;
|
|
486
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
487
|
+
try {
|
|
488
|
+
const callPromise = Promise.resolve(opts.onCheckpoint(snap, { signal: controller.signal }));
|
|
489
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
490
|
+
controller.signal.addEventListener(
|
|
491
|
+
"abort",
|
|
492
|
+
() => reject(new CheckpointTimeoutError(timeoutMs)),
|
|
493
|
+
{ once: true }
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
callPromise.catch(() => {
|
|
497
|
+
});
|
|
498
|
+
timeoutPromise.catch(() => {
|
|
499
|
+
});
|
|
500
|
+
await Promise.race([callPromise, timeoutPromise]);
|
|
501
|
+
} finally {
|
|
502
|
+
clearTimeout(timeoutId);
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
await opts.onCheckpoint(snap, { signal: controller.signal });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
var warnedStreamOnErrorOnSuspend = false;
|
|
509
|
+
function pushWarning(state, source, stepId, error) {
|
|
510
|
+
(state.warnings ??= []).push({ source, stepId, error });
|
|
511
|
+
}
|
|
512
|
+
function demotePendingError(state, pe) {
|
|
513
|
+
pushWarning(state, pe.source, pe.stepId, pe.error);
|
|
514
|
+
}
|
|
515
|
+
function maybeWarnStreamOnErrorOnSuspend(result, options) {
|
|
516
|
+
if (result.status !== "suspended" || !options?.onError || warnedStreamOnErrorOnSuspend) return;
|
|
517
|
+
warnedStreamOnErrorOnSuspend = true;
|
|
518
|
+
console.warn(
|
|
519
|
+
"pipeai: stream() with options.onError suspended at a gate \u2014 onError will NOT be invoked for suspension. Discriminate via the resolved output Promise."
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
var SealedWorkflow = class _SealedWorkflow {
|
|
289
523
|
id;
|
|
290
524
|
steps;
|
|
291
|
-
|
|
525
|
+
observability;
|
|
526
|
+
// Memoized — see ensureDuplicateCheck().
|
|
527
|
+
duplicateCheckPassed = false;
|
|
528
|
+
// Memoized lazily per terminal instance — build pipelines once at module
|
|
529
|
+
// load and re-run via generate() to amortize.
|
|
530
|
+
_cachedExecutableStepCount;
|
|
531
|
+
_cachedStepShapeHash;
|
|
532
|
+
constructor(steps, id, observability) {
|
|
292
533
|
this.steps = steps;
|
|
293
534
|
this.id = id;
|
|
535
|
+
this.observability = observability;
|
|
536
|
+
}
|
|
537
|
+
// ── Construction-time validation (memoized per terminal instance) ────
|
|
538
|
+
/**
|
|
539
|
+
* Walk the step list once per terminal instance. Rejects:
|
|
540
|
+
* - Duplicate `(type, id)` pairs.
|
|
541
|
+
* - User step ids containing the reserved `::pipeai::` namespace
|
|
542
|
+
* (CHECKPOINT_STEP_ID lives there).
|
|
543
|
+
*/
|
|
544
|
+
ensureDuplicateCheck() {
|
|
545
|
+
if (this.duplicateCheckPassed) return;
|
|
546
|
+
const seen = /* @__PURE__ */ new Map();
|
|
547
|
+
for (let i = 0; i < this.steps.length; i++) {
|
|
548
|
+
const node = this.steps[i];
|
|
549
|
+
if (node.id.includes("::pipeai::")) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`Workflow: step id "${node.id}" uses the reserved "::pipeai::" namespace at index ${i}.`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const key = `${node.type}:${node.id}`;
|
|
555
|
+
const prior = seen.get(key);
|
|
556
|
+
if (prior !== void 0) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
`Workflow: duplicate (${node.type}, "${node.id}") at indices ${prior} and ${i}. Pass an explicit \`{ id }\` (e.g. for back-to-back \`branch(...)\` or \`foreach(agentX).foreach(agentX)\`) to disambiguate.`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
seen.set(key, i);
|
|
562
|
+
}
|
|
563
|
+
this.duplicateCheckPassed = true;
|
|
564
|
+
}
|
|
565
|
+
// ── shape-hash + RunOptions validation ────────────────────────
|
|
566
|
+
/**
|
|
567
|
+
* Count of executable nodes — i.e. NOT `catch` or `finally`. Drives
|
|
568
|
+
* checkpoint auto-cadence so adding cleanup steps doesn't surprise users
|
|
569
|
+
* with extra fires. `branch`/`foreach`/`repeat`/`parallel`/`nested` are all
|
|
570
|
+
* `type: "step"` internally and count as executable.
|
|
571
|
+
*/
|
|
572
|
+
get cachedExecutableStepCount() {
|
|
573
|
+
if (this._cachedExecutableStepCount !== void 0) return this._cachedExecutableStepCount;
|
|
574
|
+
let n = 0;
|
|
575
|
+
for (const s of this.steps) {
|
|
576
|
+
if (s.type !== "catch" && s.type !== "finally") n++;
|
|
577
|
+
}
|
|
578
|
+
this._cachedExecutableStepCount = n;
|
|
579
|
+
return n;
|
|
580
|
+
}
|
|
581
|
+
/** @internal — used by `computeStepShapeHash` to descend nested workflows. */
|
|
582
|
+
getStepsForShapeHash() {
|
|
583
|
+
return this.steps;
|
|
584
|
+
}
|
|
585
|
+
get cachedStepShapeHash() {
|
|
586
|
+
if (this._cachedStepShapeHash !== void 0) return this._cachedStepShapeHash;
|
|
587
|
+
const getNested = (node) => getNestedWorkflows(node);
|
|
588
|
+
this._cachedStepShapeHash = computeStepShapeHash(
|
|
589
|
+
this.steps,
|
|
590
|
+
getNested
|
|
591
|
+
);
|
|
592
|
+
return this._cachedStepShapeHash;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Validate user-provided RunOptions before a run begins. Throws on
|
|
596
|
+
* outright errors and on the loud-disaster combo (`freezeSnapshots: true
|
|
597
|
+
* + checkpointEvery: 1` on a workflow of 8+ steps). Warns once on the
|
|
598
|
+
* merely-suspicious combo (`freezeSnapshots: true + cadence <= 2`).
|
|
599
|
+
* Plan-of-record: catastrophic combo escape via the
|
|
600
|
+
* `"iAcceptThePerformanceCost"` literal.
|
|
601
|
+
*/
|
|
602
|
+
validateRunOptions(opts) {
|
|
603
|
+
if (!opts) return;
|
|
604
|
+
if (!opts.onCheckpoint) return;
|
|
605
|
+
if (opts.checkpointEvery !== void 0 && opts.checkpointWhen !== void 0) {
|
|
606
|
+
throw new Error("RunOptions: checkpointEvery and checkpointWhen are mutually exclusive");
|
|
607
|
+
}
|
|
608
|
+
if (opts.checkpointEvery !== void 0 && (!Number.isInteger(opts.checkpointEvery) || opts.checkpointEvery < 1)) {
|
|
609
|
+
throw new Error(`RunOptions: checkpointEvery must be a positive integer, got ${opts.checkpointEvery}`);
|
|
610
|
+
}
|
|
611
|
+
if (opts.checkpointTimeout !== void 0 && (!Number.isFinite(opts.checkpointTimeout) || opts.checkpointTimeout < 1)) {
|
|
612
|
+
throw new Error(`RunOptions: checkpointTimeout must be a finite positive number (ms), got ${opts.checkpointTimeout}`);
|
|
613
|
+
}
|
|
614
|
+
const length = this.cachedExecutableStepCount;
|
|
615
|
+
const cadence = opts.checkpointEvery ?? Math.max(1, Math.ceil(length / 4));
|
|
616
|
+
if (opts.freezeSnapshots && opts.freezeSnapshots !== "iAcceptThePerformanceCost" && cadence === 1 && length >= 8) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`freezeSnapshots+checkpointEvery:1 on a ${length}-step workflow is reliably catastrophic. Set checkpointEvery >= 5, freezeSnapshots: false, or pass "iAcceptThePerformanceCost".`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
if (opts.freezeSnapshots && cadence <= 2) {
|
|
622
|
+
warnOnce(
|
|
623
|
+
"pipeai:freezeSnapshots-low-cadence",
|
|
624
|
+
"pipeai: freezeSnapshots+checkpointEvery<=2 compounds graph-walk cost."
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// ── Observability helpers ─────────────────────────────────────
|
|
629
|
+
/**
|
|
630
|
+
* Fire an observability hook safely. Returns `undefined` synchronously when
|
|
631
|
+
* no hook is registered — avoiding the promise wrapper + microtask that an
|
|
632
|
+
* async function would unconditionally allocate on every step boundary.
|
|
633
|
+
*
|
|
634
|
+
* On hook throw:
|
|
635
|
+
* - non-`onStepError` hooks: warning pushed + console.error.
|
|
636
|
+
* - `onStepError`: throw is propagated as a return value; the run loop
|
|
637
|
+
* attaches it as `cause` on the original step error.
|
|
638
|
+
*
|
|
639
|
+
* Returns the hook's thrown error if any; undefined otherwise. Callers
|
|
640
|
+
* `await` the result — `await undefined` is sync, so the no-hook path
|
|
641
|
+
* stays allocation-free.
|
|
642
|
+
*/
|
|
643
|
+
fireHook(state, name, event) {
|
|
644
|
+
const hook = this.observability?.[name];
|
|
645
|
+
if (!hook) return void 0;
|
|
646
|
+
return this.fireHookSlow(state, name, event, hook);
|
|
647
|
+
}
|
|
648
|
+
async fireHookSlow(state, name, event, hook) {
|
|
649
|
+
try {
|
|
650
|
+
await hook(event);
|
|
651
|
+
return void 0;
|
|
652
|
+
} catch (e) {
|
|
653
|
+
if (name !== "onStepError") {
|
|
654
|
+
const stepId = event.stepId;
|
|
655
|
+
pushWarning(state, name, stepId, e);
|
|
656
|
+
console.error(`pipeai: ${name} hook threw for stepId "${stepId}":`, e);
|
|
657
|
+
}
|
|
658
|
+
return e;
|
|
659
|
+
}
|
|
294
660
|
}
|
|
295
661
|
// ── Execution ─────────────────────────────────────────────────
|
|
296
662
|
async generate(ctx, ...args) {
|
|
663
|
+
this.ensureDuplicateCheck();
|
|
297
664
|
const input = args[0];
|
|
665
|
+
const opts = args[1];
|
|
666
|
+
this.validateRunOptions(opts);
|
|
298
667
|
const state = {
|
|
299
668
|
ctx,
|
|
300
669
|
output: input,
|
|
301
|
-
mode: "generate"
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
output: state.output
|
|
670
|
+
mode: "generate",
|
|
671
|
+
runOptions: opts,
|
|
672
|
+
abortSignal: opts?.abortSignal
|
|
306
673
|
};
|
|
674
|
+
await this.execute(state, 0, opts);
|
|
675
|
+
return this.buildResult(state);
|
|
307
676
|
}
|
|
308
677
|
stream(ctx, ...args) {
|
|
678
|
+
this.ensureDuplicateCheck();
|
|
309
679
|
const input = args[0];
|
|
310
680
|
const options = args[1];
|
|
681
|
+
const opts = args[2];
|
|
682
|
+
this.validateRunOptions(opts);
|
|
683
|
+
const abortSignal = opts?.abortSignal;
|
|
311
684
|
let resolveOutput;
|
|
312
685
|
let rejectOutput;
|
|
313
686
|
const outputPromise = new Promise((res, rej) => {
|
|
@@ -322,11 +695,15 @@ var SealedWorkflow = class {
|
|
|
322
695
|
ctx,
|
|
323
696
|
output: input,
|
|
324
697
|
mode: "stream",
|
|
325
|
-
writer
|
|
698
|
+
writer,
|
|
699
|
+
runOptions: opts,
|
|
700
|
+
abortSignal
|
|
326
701
|
};
|
|
327
702
|
try {
|
|
328
|
-
await this.execute(state);
|
|
329
|
-
|
|
703
|
+
await this.execute(state, 0, opts);
|
|
704
|
+
const result = this.buildResult(state);
|
|
705
|
+
maybeWarnStreamOnErrorOnSuspend(result, options);
|
|
706
|
+
resolveOutput(result);
|
|
330
707
|
} catch (error) {
|
|
331
708
|
rejectOutput(error);
|
|
332
709
|
throw error;
|
|
@@ -340,20 +717,78 @@ var SealedWorkflow = class {
|
|
|
340
717
|
output: outputPromise
|
|
341
718
|
};
|
|
342
719
|
}
|
|
720
|
+
// Helper — converts terminal RuntimeState into a WorkflowResult; freezes
|
|
721
|
+
// snapshot + warnings if requested via runOptions.
|
|
722
|
+
buildResult(state) {
|
|
723
|
+
const warnings = state.warnings ?? [];
|
|
724
|
+
if (state.suspension && resolveFreezeSnapshots(state)) {
|
|
725
|
+
deepFreeze(warnings);
|
|
726
|
+
}
|
|
727
|
+
if (state.suspension) {
|
|
728
|
+
return { status: "suspended", snapshot: state.suspension, warnings };
|
|
729
|
+
}
|
|
730
|
+
return { status: "complete", output: state.output, warnings };
|
|
731
|
+
}
|
|
343
732
|
// ── Internal: execute pipeline ────────────────────────────────
|
|
344
|
-
async execute(state, startIndex = 0) {
|
|
733
|
+
async execute(state, startIndex = 0, opts, initialError = null) {
|
|
345
734
|
if (this.steps.length === 0) {
|
|
346
735
|
throw new Error("Workflow has no steps. Add at least one step before calling generate() or stream().");
|
|
347
736
|
}
|
|
348
|
-
|
|
737
|
+
if (opts !== void 0 && state.runOptions === void 0) {
|
|
738
|
+
state.runOptions = opts;
|
|
739
|
+
}
|
|
740
|
+
const ckptCadence = opts?.onCheckpoint && opts.checkpointWhen === void 0 ? opts.checkpointEvery ?? Math.max(1, Math.ceil(this.cachedExecutableStepCount / 4)) : 0;
|
|
741
|
+
let pendingError = initialError;
|
|
742
|
+
let abortPromoted = false;
|
|
743
|
+
const makeAbortError = (signal) => ({
|
|
744
|
+
error: signal.reason ?? new Error("Workflow aborted"),
|
|
745
|
+
stepId: "abort",
|
|
746
|
+
source: "step"
|
|
747
|
+
});
|
|
349
748
|
for (let i = startIndex; i < this.steps.length; i++) {
|
|
749
|
+
if (state.abortSignal?.aborted) {
|
|
750
|
+
if (!abortPromoted) {
|
|
751
|
+
abortPromoted = true;
|
|
752
|
+
state.suspension = void 0;
|
|
753
|
+
if (pendingError) demotePendingError(state, pendingError);
|
|
754
|
+
pendingError = makeAbortError(state.abortSignal);
|
|
755
|
+
} else if (!pendingError) {
|
|
756
|
+
pendingError = makeAbortError(state.abortSignal);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
350
759
|
const node = this.steps[i];
|
|
351
760
|
if (node.type === "finally") {
|
|
352
|
-
|
|
761
|
+
const stepId2 = node.id;
|
|
762
|
+
const finStart = performance.now();
|
|
763
|
+
await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "finally", ctx: state.ctx, input: state.output });
|
|
764
|
+
try {
|
|
765
|
+
await node.execute(state);
|
|
766
|
+
await this.fireHook(state, "onStepFinish", {
|
|
767
|
+
stepId: stepId2,
|
|
768
|
+
type: "finally",
|
|
769
|
+
ctx: state.ctx,
|
|
770
|
+
output: state.output,
|
|
771
|
+
durationMs: performance.now() - finStart,
|
|
772
|
+
suspended: false
|
|
773
|
+
});
|
|
774
|
+
} catch (e) {
|
|
775
|
+
await this.fireHook(state, "onStepError", {
|
|
776
|
+
stepId: stepId2,
|
|
777
|
+
type: "finally",
|
|
778
|
+
ctx: state.ctx,
|
|
779
|
+
error: e,
|
|
780
|
+
durationMs: performance.now() - finStart
|
|
781
|
+
});
|
|
782
|
+
if (pendingError) demotePendingError(state, pendingError);
|
|
783
|
+
pendingError = { error: e, stepId: stepId2, source: "finally" };
|
|
784
|
+
}
|
|
353
785
|
continue;
|
|
354
786
|
}
|
|
355
787
|
if (node.type === "catch") {
|
|
356
|
-
if (!pendingError) continue;
|
|
788
|
+
if (state.suspension || !pendingError || state.checkpointFailed) continue;
|
|
789
|
+
const stepId2 = node.id;
|
|
790
|
+
const cStart = performance.now();
|
|
791
|
+
await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "catch", ctx: state.ctx, input: state.output });
|
|
357
792
|
try {
|
|
358
793
|
state.output = await node.catchFn({
|
|
359
794
|
error: pendingError.error,
|
|
@@ -362,49 +797,183 @@ var SealedWorkflow = class {
|
|
|
362
797
|
stepId: pendingError.stepId
|
|
363
798
|
});
|
|
364
799
|
pendingError = null;
|
|
365
|
-
|
|
366
|
-
|
|
800
|
+
await this.fireHook(state, "onStepFinish", {
|
|
801
|
+
stepId: stepId2,
|
|
802
|
+
type: "catch",
|
|
803
|
+
ctx: state.ctx,
|
|
804
|
+
output: state.output,
|
|
805
|
+
durationMs: performance.now() - cStart,
|
|
806
|
+
suspended: false
|
|
807
|
+
});
|
|
808
|
+
} catch (e) {
|
|
809
|
+
await this.fireHook(state, "onStepError", {
|
|
810
|
+
stepId: stepId2,
|
|
811
|
+
type: "catch",
|
|
812
|
+
ctx: state.ctx,
|
|
813
|
+
error: e,
|
|
814
|
+
durationMs: performance.now() - cStart
|
|
815
|
+
});
|
|
816
|
+
if (pendingError) demotePendingError(state, pendingError);
|
|
817
|
+
pendingError = { error: e, stepId: stepId2, source: "catch" };
|
|
367
818
|
}
|
|
368
819
|
continue;
|
|
369
820
|
}
|
|
821
|
+
if (state.suspension || pendingError) continue;
|
|
370
822
|
if (node.type === "gate") {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
823
|
+
const stepId2 = node.id;
|
|
824
|
+
const gStart = performance.now();
|
|
825
|
+
await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "gate", ctx: state.ctx, input: state.output });
|
|
826
|
+
try {
|
|
827
|
+
if (node.condition && !await node.condition(state)) {
|
|
828
|
+
await this.fireHook(state, "onStepFinish", {
|
|
829
|
+
stepId: stepId2,
|
|
830
|
+
type: "gate",
|
|
831
|
+
ctx: state.ctx,
|
|
832
|
+
output: state.output,
|
|
833
|
+
durationMs: performance.now() - gStart,
|
|
834
|
+
suspended: false
|
|
835
|
+
});
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const snapshot = {
|
|
839
|
+
version: 2,
|
|
840
|
+
kind: "gate",
|
|
841
|
+
resumeFromIndex: i,
|
|
842
|
+
output: state.output,
|
|
843
|
+
gateId: node.id,
|
|
844
|
+
gatePayload: await node.payload(state)
|
|
845
|
+
};
|
|
846
|
+
state.suspension = snapshot;
|
|
847
|
+
if (resolveFreezeSnapshots(state)) deepFreeze(snapshot);
|
|
848
|
+
await this.fireHook(state, "onStepFinish", {
|
|
849
|
+
stepId: stepId2,
|
|
850
|
+
type: "gate",
|
|
851
|
+
ctx: state.ctx,
|
|
852
|
+
output: state.output,
|
|
853
|
+
durationMs: performance.now() - gStart,
|
|
854
|
+
suspended: true
|
|
855
|
+
});
|
|
856
|
+
} catch (e) {
|
|
857
|
+
pendingError = { error: e, stepId: node.id, source: "step" };
|
|
858
|
+
}
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const obsType = getObservabilityType(node);
|
|
862
|
+
const stepId = node.id;
|
|
863
|
+
const sStart = performance.now();
|
|
864
|
+
const stepInput = state.output;
|
|
865
|
+
await this.fireHook(state, "onStepStart", { stepId, type: obsType, ctx: state.ctx, input: stepInput });
|
|
866
|
+
try {
|
|
867
|
+
await node.execute(state);
|
|
868
|
+
await this.fireHook(state, "onStepFinish", {
|
|
869
|
+
stepId,
|
|
870
|
+
type: obsType,
|
|
871
|
+
ctx: state.ctx,
|
|
380
872
|
output: state.output,
|
|
381
|
-
|
|
382
|
-
|
|
873
|
+
durationMs: performance.now() - sStart,
|
|
874
|
+
suspended: false
|
|
383
875
|
});
|
|
876
|
+
} catch (e) {
|
|
877
|
+
pendingError = { error: e, stepId: node.id, source: "step" };
|
|
878
|
+
const obsError = await this.fireHook(state, "onStepError", {
|
|
879
|
+
stepId,
|
|
880
|
+
type: obsType,
|
|
881
|
+
ctx: state.ctx,
|
|
882
|
+
error: e,
|
|
883
|
+
durationMs: performance.now() - sStart
|
|
884
|
+
});
|
|
885
|
+
if (obsError !== void 0 && typeof e === "object" && e !== null) {
|
|
886
|
+
try {
|
|
887
|
+
e.cause = obsError;
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const leaked = state.suspension;
|
|
893
|
+
if (leaked) {
|
|
894
|
+
state.suspension = void 0;
|
|
895
|
+
throw new Error(`internal: suspension bubbled from non-gate step "${node.id}" (gate "${leaked.gateId}").`);
|
|
896
|
+
}
|
|
897
|
+
if (!pendingError && !state.suspension && opts?.onCheckpoint) {
|
|
898
|
+
const shouldCheckpoint = opts.checkpointWhen ? opts.checkpointWhen({ stepIndex: i, stepId: node.id, ctx: state.ctx }) : (i + 1) % ckptCadence === 0;
|
|
899
|
+
if (shouldCheckpoint) {
|
|
900
|
+
const ckptStart = performance.now();
|
|
901
|
+
try {
|
|
902
|
+
await emitCheckpoint(
|
|
903
|
+
state,
|
|
904
|
+
opts,
|
|
905
|
+
i + 1,
|
|
906
|
+
this.cachedStepShapeHash
|
|
907
|
+
);
|
|
908
|
+
} catch (e) {
|
|
909
|
+
pendingError = { error: e, stepId: CHECKPOINT_STEP_ID, source: "onCheckpoint" };
|
|
910
|
+
state.checkpointFailed = true;
|
|
911
|
+
await this.fireHook(state, "onStepError", {
|
|
912
|
+
stepId: CHECKPOINT_STEP_ID,
|
|
913
|
+
type: "step",
|
|
914
|
+
ctx: state.ctx,
|
|
915
|
+
error: e,
|
|
916
|
+
durationMs: performance.now() - ckptStart
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (pendingError && !state.suspension) {
|
|
923
|
+
if (state.checkpointFailed) {
|
|
924
|
+
const warningsArr = state.warnings ?? [];
|
|
925
|
+
const checkpointError = pendingError.source === "onCheckpoint" ? pendingError.error : warningsArr.find((w) => w.source === "onCheckpoint")?.error;
|
|
926
|
+
const finallyErrors = warningsArr.filter((w) => w.source === "finally").map((w) => w.error);
|
|
927
|
+
const all = pendingError.source === "finally" ? [...finallyErrors, pendingError.error] : finallyErrors;
|
|
928
|
+
if (all.length > 0) {
|
|
929
|
+
console.warn(
|
|
930
|
+
`pipeai: ${all.length} .finally() error(s) suppressed by checkpoint-failure precedence:`,
|
|
931
|
+
all
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
throw checkpointError ?? pendingError.error;
|
|
935
|
+
}
|
|
936
|
+
const isFinallyPath = pendingError.source === "finally" || (state.warnings?.some((w) => w.source === "finally") ?? false);
|
|
937
|
+
if (isFinallyPath) {
|
|
938
|
+
const all = [...(state.warnings ?? []).map((w) => w.error), pendingError.error];
|
|
939
|
+
throw new AggregateError(all, `Workflow failed with ${all.length} error(s) from .finally() bodies`);
|
|
384
940
|
}
|
|
385
|
-
|
|
941
|
+
throw pendingError.error;
|
|
942
|
+
} else if (pendingError && state.suspension) {
|
|
943
|
+
demotePendingError(state, pendingError);
|
|
386
944
|
try {
|
|
387
|
-
await
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
945
|
+
await this.observability?.onStepError?.({
|
|
946
|
+
stepId: pendingError.stepId,
|
|
947
|
+
type: pendingErrorSourceToStepType(pendingError.source),
|
|
948
|
+
ctx: state.ctx,
|
|
949
|
+
error: pendingError.error,
|
|
950
|
+
durationMs: 0
|
|
951
|
+
});
|
|
952
|
+
} catch (obsError) {
|
|
953
|
+
pushWarning(state, "onStepError", pendingError.stepId, obsError);
|
|
391
954
|
}
|
|
955
|
+
pendingError = null;
|
|
392
956
|
}
|
|
393
|
-
if (pendingError) throw pendingError.error;
|
|
394
957
|
}
|
|
395
958
|
// ── Internal: execute a nested workflow within a step/loop ─────
|
|
396
959
|
// Defined on SealedWorkflow (not Workflow) because TypeScript's protected
|
|
397
960
|
// access rules only allow calling workflow.execute() from the same class.
|
|
961
|
+
//
|
|
962
|
+
// Contract: clears any inner suspension before re-throwing as
|
|
963
|
+
// NestedGateUnsupportedError. The outer execute() therefore never observes
|
|
964
|
+
// a leaked `state.suspension` from non-gate nodes (defensive invariant).
|
|
398
965
|
async executeNestedWorkflow(state, workflow) {
|
|
966
|
+
const savedRunOptions = state.runOptions;
|
|
967
|
+
state.runOptions = void 0;
|
|
399
968
|
try {
|
|
400
969
|
await workflow.execute(state);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
throw
|
|
970
|
+
} finally {
|
|
971
|
+
state.runOptions = savedRunOptions;
|
|
972
|
+
}
|
|
973
|
+
if (state.suspension) {
|
|
974
|
+
const gateId = state.suspension.gateId;
|
|
975
|
+
state.suspension = void 0;
|
|
976
|
+
throw new NestedGateUnsupportedError(gateId, workflow.id);
|
|
408
977
|
}
|
|
409
978
|
}
|
|
410
979
|
// ── Internal: execute an agent within a step/branch ───────────
|
|
@@ -414,59 +983,141 @@ var SealedWorkflow = class {
|
|
|
414
983
|
async executeAgent(state, agent, ctx, options) {
|
|
415
984
|
const input = state.output;
|
|
416
985
|
const hasStructuredOutput = agent.hasOutput;
|
|
986
|
+
const abortSignal = state.abortSignal;
|
|
987
|
+
const agentCallOpts = abortSignal ? { abortSignal } : void 0;
|
|
417
988
|
if (state.mode === "stream" && state.writer) {
|
|
418
989
|
const writer = state.writer;
|
|
419
990
|
await runWithWriter(writer, async () => {
|
|
420
|
-
const result = await agent.stream(ctx, state.output);
|
|
991
|
+
const result = await agent.stream(ctx, state.output, agentCallOpts);
|
|
421
992
|
if (options?.handleStream) {
|
|
422
993
|
await options.handleStream({ result, writer, ctx });
|
|
423
994
|
} else {
|
|
424
995
|
writer.merge(result.toUIMessageStream());
|
|
425
996
|
}
|
|
426
|
-
|
|
427
|
-
|
|
997
|
+
const hookParams = {
|
|
998
|
+
mode: "stream",
|
|
999
|
+
result,
|
|
1000
|
+
ctx,
|
|
1001
|
+
input
|
|
1002
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1003
|
+
};
|
|
1004
|
+
if (options?.onResult) {
|
|
1005
|
+
await options.onResult(hookParams);
|
|
428
1006
|
}
|
|
429
|
-
if (options?.
|
|
430
|
-
state.output = await options.
|
|
1007
|
+
if (options?.mapResult) {
|
|
1008
|
+
state.output = await options.mapResult(hookParams);
|
|
431
1009
|
} else {
|
|
432
|
-
state.output = await extractOutput(result, hasStructuredOutput);
|
|
1010
|
+
state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
|
|
433
1011
|
}
|
|
434
1012
|
});
|
|
435
1013
|
} else {
|
|
436
|
-
const result = await agent.generate(ctx, state.output);
|
|
437
|
-
|
|
438
|
-
|
|
1014
|
+
const result = await agent.generate(ctx, state.output, agentCallOpts);
|
|
1015
|
+
const hookParams = {
|
|
1016
|
+
mode: "generate",
|
|
1017
|
+
result,
|
|
1018
|
+
ctx,
|
|
1019
|
+
input
|
|
1020
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1021
|
+
};
|
|
1022
|
+
if (options?.onResult) {
|
|
1023
|
+
await options.onResult(hookParams);
|
|
439
1024
|
}
|
|
440
|
-
if (options?.
|
|
441
|
-
state.output = await options.
|
|
1025
|
+
if (options?.mapResult) {
|
|
1026
|
+
state.output = await options.mapResult(hookParams);
|
|
442
1027
|
} else {
|
|
443
|
-
state.output = await extractOutput(result, hasStructuredOutput);
|
|
1028
|
+
state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
|
|
444
1029
|
}
|
|
445
1030
|
}
|
|
446
1031
|
}
|
|
447
1032
|
// ── Gate: load persisted state for resumption ──────────────────
|
|
448
1033
|
loadState(gateId, snapshot) {
|
|
449
|
-
if (snapshot.
|
|
1034
|
+
if (snapshot.version === 2 && snapshot.kind === "checkpoint") {
|
|
1035
|
+
throw new Error(`loadState: received a checkpoint snapshot. Use resumeFrom() for checkpoint resume; loadState() is for gates.`);
|
|
1036
|
+
}
|
|
1037
|
+
const gateLike = snapshot;
|
|
1038
|
+
if (gateLike.gateId !== gateId) {
|
|
450
1039
|
throw new Error(
|
|
451
|
-
`loadState: gate ID mismatch \u2014 expected "${gateId}" but snapshot has "${
|
|
1040
|
+
`loadState: gate ID mismatch \u2014 expected "${gateId}" but snapshot has "${gateLike.gateId}".`
|
|
452
1041
|
);
|
|
453
1042
|
}
|
|
454
|
-
|
|
1043
|
+
this.ensureDuplicateCheck();
|
|
1044
|
+
const gateIndex = this.findGateIndex(gateLike);
|
|
455
1045
|
const gateNode = this.steps[gateIndex];
|
|
456
|
-
return new ResumedWorkflow(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
gateNode.
|
|
460
|
-
|
|
461
|
-
snapshot
|
|
462
|
-
|
|
1046
|
+
return new ResumedWorkflow(this.steps, gateIndex + 1, {
|
|
1047
|
+
mode: "gate",
|
|
1048
|
+
schema: gateNode.schema,
|
|
1049
|
+
mergeFn: gateNode.merge,
|
|
1050
|
+
priorOutput: gateLike.output,
|
|
1051
|
+
snapshot: gateLike,
|
|
1052
|
+
observability: this.observability
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
// ── Checkpoint resume ──────────────────────────────────────────
|
|
1056
|
+
/**
|
|
1057
|
+
* Resume from a checkpoint snapshot. Validates the step-shape hash unless
|
|
1058
|
+
* `{ skipShapeCheck: true }` is passed. Throws on:
|
|
1059
|
+
* - gate snapshots (use `loadState` instead)
|
|
1060
|
+
* - missing/corrupted `stepShapeHash`
|
|
1061
|
+
* - shape mismatch (unless skipped)
|
|
1062
|
+
* - out-of-bounds `resumeFromIndex`
|
|
1063
|
+
* - 0-step workflow (structural invariant)
|
|
1064
|
+
*
|
|
1065
|
+
* Returns a `CheckpointResumedWorkflow` whose `generate(ctx, opts?)` takes
|
|
1066
|
+
* NO response arg — the state is seeded from the snapshot's output. The
|
|
1067
|
+
* matching gate-resume path (`loadState`) keeps the `response` arg.
|
|
1068
|
+
*/
|
|
1069
|
+
resumeFrom(snapshot, options) {
|
|
1070
|
+
const isGate = snapshot.version === 2 && snapshot.kind === "gate" || snapshot.version === 1 && snapshot.gateId !== void 0;
|
|
1071
|
+
if (isGate) {
|
|
1072
|
+
throw new Error(`resumeFrom: received a gate snapshot. Use loadState() for gate resume; resumeFrom() is for checkpoints.`);
|
|
1073
|
+
}
|
|
1074
|
+
if (this.steps.length === 0) {
|
|
1075
|
+
throw new Error("resumeFrom: workflow has no steps; snapshot is structurally invalid.");
|
|
1076
|
+
}
|
|
1077
|
+
const ckpt = snapshot;
|
|
1078
|
+
const idx = ckpt.resumeFromIndex;
|
|
1079
|
+
if (!Number.isInteger(idx) || idx < 0 || idx > this.steps.length) {
|
|
1080
|
+
throw new Error(`resumeFrom: resumeFromIndex (${idx}) out of bounds for ${this.steps.length}-step workflow.`);
|
|
1081
|
+
}
|
|
1082
|
+
if (!options?.skipShapeCheck) {
|
|
1083
|
+
if (!ckpt.stepShapeHash) {
|
|
1084
|
+
throw new Error("resumeFrom: snapshot missing stepShapeHash; corrupted or hand-crafted.");
|
|
1085
|
+
}
|
|
1086
|
+
this.ensureDuplicateCheck();
|
|
1087
|
+
if (this.cachedStepShapeHash !== ckpt.stepShapeHash) {
|
|
1088
|
+
throw new Error("resumeFrom: workflow shape mismatch; cannot safely resume. Pass { skipShapeCheck: true } to override.");
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
this.ensureDuplicateCheck();
|
|
1092
|
+
}
|
|
1093
|
+
return new CheckpointResumedWorkflow(this.steps, idx, {
|
|
1094
|
+
mode: "checkpoint",
|
|
1095
|
+
priorOutput: ckpt.output,
|
|
1096
|
+
snapshot: ckpt,
|
|
1097
|
+
observability: this.observability
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Append a `.finally()` body to a sealed workflow, returning another sealed
|
|
1102
|
+
* workflow. Allows multi-finally chains (`.finally().finally()`). A throwing
|
|
1103
|
+
* `.finally` body does NOT abort subsequent ones — they all run.
|
|
1104
|
+
*/
|
|
1105
|
+
finally(id, fn) {
|
|
1106
|
+
const node = {
|
|
1107
|
+
type: "finally",
|
|
1108
|
+
id,
|
|
1109
|
+
execute: async (state) => {
|
|
1110
|
+
await fn({ ctx: state.ctx });
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
return new _SealedWorkflow([...this.steps, node], this.id, this.observability);
|
|
463
1114
|
}
|
|
464
1115
|
findGateIndex(snapshot) {
|
|
465
|
-
if (snapshot.version !== 1) {
|
|
1116
|
+
if (snapshot.version !== 1 && snapshot.version !== 2) {
|
|
466
1117
|
throw new Error(`Unsupported snapshot version: ${snapshot.version}`);
|
|
467
1118
|
}
|
|
468
1119
|
const hint = snapshot.resumeFromIndex;
|
|
469
|
-
if (hint >= 0 && hint < this.steps.length) {
|
|
1120
|
+
if (Number.isInteger(hint) && hint >= 0 && hint < this.steps.length) {
|
|
470
1121
|
const node = this.steps[hint];
|
|
471
1122
|
if (node.type === "gate" && node.id === snapshot.gateId) {
|
|
472
1123
|
return hint;
|
|
@@ -489,12 +1140,12 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
489
1140
|
mergeFn;
|
|
490
1141
|
priorOutput;
|
|
491
1142
|
/** @internal */
|
|
492
|
-
constructor(steps, startIndex,
|
|
493
|
-
super(steps);
|
|
1143
|
+
constructor(steps, startIndex, config) {
|
|
1144
|
+
super(steps, void 0, config.observability);
|
|
494
1145
|
this.startIndex = startIndex;
|
|
495
|
-
this.schema = schema;
|
|
496
|
-
this.mergeFn = mergeFn;
|
|
497
|
-
this.priorOutput = priorOutput;
|
|
1146
|
+
this.schema = config.schema;
|
|
1147
|
+
this.mergeFn = config.mergeFn;
|
|
1148
|
+
this.priorOutput = config.priorOutput;
|
|
498
1149
|
}
|
|
499
1150
|
validateResponse(response) {
|
|
500
1151
|
if (this.schema) {
|
|
@@ -503,15 +1154,31 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
503
1154
|
return response;
|
|
504
1155
|
}
|
|
505
1156
|
async generate(ctx, ...args) {
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1157
|
+
const rawResponse = args[0];
|
|
1158
|
+
const opts = args[1];
|
|
1159
|
+
let output = this.priorOutput;
|
|
1160
|
+
let initialError = null;
|
|
1161
|
+
try {
|
|
1162
|
+
const response = this.validateResponse(rawResponse);
|
|
1163
|
+
output = this.mergeFn ? await this.mergeFn({ priorOutput: this.priorOutput, response }) : response;
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
initialError = { error, stepId: "gate:resume", source: "step" };
|
|
1166
|
+
}
|
|
1167
|
+
const state = {
|
|
1168
|
+
ctx,
|
|
1169
|
+
output,
|
|
1170
|
+
mode: "generate",
|
|
1171
|
+
runOptions: opts,
|
|
1172
|
+
abortSignal: opts?.abortSignal
|
|
1173
|
+
};
|
|
1174
|
+
await this.execute(state, this.startIndex, opts, initialError);
|
|
1175
|
+
return this.buildResult(state);
|
|
511
1176
|
}
|
|
512
1177
|
stream(ctx, ...args) {
|
|
513
|
-
const
|
|
1178
|
+
const rawResponse = args[0];
|
|
514
1179
|
const options = args[1];
|
|
1180
|
+
const opts = args[2];
|
|
1181
|
+
const abortSignal = opts?.abortSignal;
|
|
515
1182
|
let resolveOutput;
|
|
516
1183
|
let rejectOutput;
|
|
517
1184
|
const outputPromise = new Promise((res, rej) => {
|
|
@@ -522,18 +1189,92 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
522
1189
|
});
|
|
523
1190
|
const mergeFn = this.mergeFn;
|
|
524
1191
|
const priorOutput = this.priorOutput;
|
|
1192
|
+
const startIndex = this.startIndex;
|
|
525
1193
|
const stream = (0, import_ai3.createUIMessageStream)({
|
|
526
1194
|
execute: async ({ writer }) => {
|
|
527
|
-
|
|
1195
|
+
let output = priorOutput;
|
|
1196
|
+
let initialError = null;
|
|
1197
|
+
try {
|
|
1198
|
+
const response = this.validateResponse(rawResponse);
|
|
1199
|
+
output = mergeFn ? await mergeFn({ priorOutput, response }) : response;
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
initialError = { error, stepId: "gate:resume", source: "step" };
|
|
1202
|
+
}
|
|
528
1203
|
const state = {
|
|
529
1204
|
ctx,
|
|
530
1205
|
output,
|
|
531
1206
|
mode: "stream",
|
|
532
|
-
writer
|
|
1207
|
+
writer,
|
|
1208
|
+
runOptions: opts,
|
|
1209
|
+
abortSignal
|
|
533
1210
|
};
|
|
534
1211
|
try {
|
|
535
|
-
await this.execute(state,
|
|
536
|
-
|
|
1212
|
+
await this.execute(state, startIndex, opts, initialError);
|
|
1213
|
+
const result = this.buildResult(state);
|
|
1214
|
+
maybeWarnStreamOnErrorOnSuspend(result, options);
|
|
1215
|
+
resolveOutput(result);
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
rejectOutput(error);
|
|
1218
|
+
throw error;
|
|
1219
|
+
}
|
|
1220
|
+
},
|
|
1221
|
+
...options?.onError ? { onError: options.onError } : {},
|
|
1222
|
+
...options?.onFinish ? { onFinish: options.onFinish } : {}
|
|
1223
|
+
});
|
|
1224
|
+
return { stream, output: outputPromise };
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
var CheckpointResumedWorkflow = class extends SealedWorkflow {
|
|
1228
|
+
startIndex;
|
|
1229
|
+
priorOutput;
|
|
1230
|
+
/** @internal */
|
|
1231
|
+
constructor(steps, startIndex, config) {
|
|
1232
|
+
super(steps, void 0, config.observability);
|
|
1233
|
+
this.startIndex = startIndex;
|
|
1234
|
+
this.priorOutput = config.priorOutput;
|
|
1235
|
+
}
|
|
1236
|
+
// Override with widened arg list compatible with parent's `[input?, opts?]`.
|
|
1237
|
+
// Inputs are ignored — state is seeded from the snapshot's `output` field.
|
|
1238
|
+
async generate(ctx, ...args) {
|
|
1239
|
+
const opts = args[1];
|
|
1240
|
+
this.validateRunOptions(opts);
|
|
1241
|
+
const state = {
|
|
1242
|
+
ctx,
|
|
1243
|
+
output: this.priorOutput,
|
|
1244
|
+
mode: "generate",
|
|
1245
|
+
runOptions: opts
|
|
1246
|
+
};
|
|
1247
|
+
await this.execute(state, this.startIndex, opts);
|
|
1248
|
+
return this.buildResult(state);
|
|
1249
|
+
}
|
|
1250
|
+
stream(ctx, ...args) {
|
|
1251
|
+
const options = args[1];
|
|
1252
|
+
const opts = args[2];
|
|
1253
|
+
this.validateRunOptions(opts);
|
|
1254
|
+
let resolveOutput;
|
|
1255
|
+
let rejectOutput;
|
|
1256
|
+
const outputPromise = new Promise((res, rej) => {
|
|
1257
|
+
resolveOutput = res;
|
|
1258
|
+
rejectOutput = rej;
|
|
1259
|
+
});
|
|
1260
|
+
outputPromise.catch(() => {
|
|
1261
|
+
});
|
|
1262
|
+
const priorOutput = this.priorOutput;
|
|
1263
|
+
const startIndex = this.startIndex;
|
|
1264
|
+
const stream = (0, import_ai3.createUIMessageStream)({
|
|
1265
|
+
execute: async ({ writer }) => {
|
|
1266
|
+
const state = {
|
|
1267
|
+
ctx,
|
|
1268
|
+
output: priorOutput,
|
|
1269
|
+
mode: "stream",
|
|
1270
|
+
writer,
|
|
1271
|
+
runOptions: opts
|
|
1272
|
+
};
|
|
1273
|
+
try {
|
|
1274
|
+
await this.execute(state, startIndex, opts);
|
|
1275
|
+
const result = this.buildResult(state);
|
|
1276
|
+
maybeWarnStreamOnErrorOnSuspend(result, options);
|
|
1277
|
+
resolveOutput(result);
|
|
537
1278
|
} catch (error) {
|
|
538
1279
|
rejectOutput(error);
|
|
539
1280
|
throw error;
|
|
@@ -546,15 +1287,27 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
546
1287
|
}
|
|
547
1288
|
};
|
|
548
1289
|
var Workflow = class _Workflow extends SealedWorkflow {
|
|
549
|
-
|
|
550
|
-
|
|
1290
|
+
/**
|
|
1291
|
+
* Sentinel value for `foreach`'s `onError` handler. Returning `Workflow.SKIP`
|
|
1292
|
+
* from `onError` omits the failed item's index from the output array,
|
|
1293
|
+
* shortening it relative to the input array.
|
|
1294
|
+
*/
|
|
1295
|
+
static SKIP = /* @__PURE__ */ Symbol("pipeai.foreach.skip");
|
|
1296
|
+
constructor(steps = [], id, observability) {
|
|
1297
|
+
super(steps, id, observability);
|
|
551
1298
|
}
|
|
552
1299
|
static create(options) {
|
|
553
|
-
return new _Workflow([], options?.id);
|
|
1300
|
+
return new _Workflow([], options?.id, options?.observability);
|
|
554
1301
|
}
|
|
555
1302
|
static from(agent, options) {
|
|
556
1303
|
return new _Workflow([]).step(agent, options);
|
|
557
1304
|
}
|
|
1305
|
+
// Builder helper — append a step and return a re-typed Workflow.
|
|
1306
|
+
// Centralizes the `[...steps, node] as any` + new Workflow + observability/id
|
|
1307
|
+
// forwarding pattern used by every combinator method.
|
|
1308
|
+
appendStep(node) {
|
|
1309
|
+
return new _Workflow([...this.steps, node], this.id, this.observability);
|
|
1310
|
+
}
|
|
558
1311
|
// ── step: implementation ──────────────────────────────────────
|
|
559
1312
|
step(target, optionsOrFn) {
|
|
560
1313
|
if (target instanceof SealedWorkflow) {
|
|
@@ -562,11 +1315,15 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
562
1315
|
const node2 = {
|
|
563
1316
|
type: "step",
|
|
564
1317
|
id: workflow.id ?? "nested-workflow",
|
|
1318
|
+
nestedWorkflow: workflow,
|
|
1319
|
+
// Feeds the recursive stepShapeHash walk.
|
|
1320
|
+
category: "nested",
|
|
1321
|
+
// Observability event type.
|
|
565
1322
|
execute: async (state) => {
|
|
566
1323
|
await this.executeNestedWorkflow(state, workflow);
|
|
567
1324
|
}
|
|
568
1325
|
};
|
|
569
|
-
return
|
|
1326
|
+
return this.appendStep(node2);
|
|
570
1327
|
}
|
|
571
1328
|
if (typeof target === "string") {
|
|
572
1329
|
if (typeof optionsOrFn !== "function") {
|
|
@@ -583,19 +1340,19 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
583
1340
|
});
|
|
584
1341
|
}
|
|
585
1342
|
};
|
|
586
|
-
return
|
|
1343
|
+
return this.appendStep(node2);
|
|
587
1344
|
}
|
|
588
1345
|
const agent = target;
|
|
589
1346
|
const options = optionsOrFn;
|
|
590
1347
|
const node = {
|
|
591
1348
|
type: "step",
|
|
592
|
-
id: agent.id,
|
|
1349
|
+
id: options?.id ?? agent.id,
|
|
593
1350
|
execute: async (state) => {
|
|
594
1351
|
const ctx = state.ctx;
|
|
595
1352
|
await this.executeAgent(state, agent, ctx, options);
|
|
596
1353
|
}
|
|
597
1354
|
};
|
|
598
|
-
return
|
|
1355
|
+
return this.appendStep(node);
|
|
599
1356
|
}
|
|
600
1357
|
// ── gate: human-in-the-loop suspension point ────────────────
|
|
601
1358
|
gate(id, options) {
|
|
@@ -621,19 +1378,20 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
621
1378
|
return state.output;
|
|
622
1379
|
}
|
|
623
1380
|
};
|
|
624
|
-
return
|
|
1381
|
+
return this.appendStep(node);
|
|
625
1382
|
}
|
|
626
1383
|
// ── branch: implementation ────────────────────────────────────
|
|
627
|
-
branch(casesOrConfig) {
|
|
1384
|
+
branch(casesOrConfig, options) {
|
|
628
1385
|
if (Array.isArray(casesOrConfig)) {
|
|
629
|
-
return this.branchPredicate(casesOrConfig);
|
|
1386
|
+
return this.branchPredicate(casesOrConfig, options?.id);
|
|
630
1387
|
}
|
|
631
|
-
return this.branchSelect(casesOrConfig);
|
|
1388
|
+
return this.branchSelect(casesOrConfig, options?.id);
|
|
632
1389
|
}
|
|
633
|
-
branchPredicate(cases) {
|
|
1390
|
+
branchPredicate(cases, explicitId) {
|
|
634
1391
|
const node = {
|
|
635
1392
|
type: "step",
|
|
636
|
-
id: "branch:predicate",
|
|
1393
|
+
id: explicitId ?? "branch:predicate",
|
|
1394
|
+
category: "branch",
|
|
637
1395
|
execute: async (state) => {
|
|
638
1396
|
const ctx = state.ctx;
|
|
639
1397
|
const input = state.output;
|
|
@@ -645,21 +1403,43 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
645
1403
|
await this.executeAgent(state, branchCase.agent, ctx, branchCase);
|
|
646
1404
|
return;
|
|
647
1405
|
}
|
|
648
|
-
|
|
1406
|
+
let inputRepr;
|
|
1407
|
+
try {
|
|
1408
|
+
inputRepr = JSON.stringify(input);
|
|
1409
|
+
if (inputRepr === void 0) inputRepr = String(input);
|
|
1410
|
+
} catch {
|
|
1411
|
+
inputRepr = `[unserializable ${typeof input}]`;
|
|
1412
|
+
}
|
|
1413
|
+
throw new WorkflowBranchError("predicate", `No branch matched and no default branch (a case without \`when\`) was provided. Input: ${inputRepr}`);
|
|
649
1414
|
}
|
|
650
1415
|
};
|
|
651
|
-
return
|
|
1416
|
+
return this.appendStep(node);
|
|
652
1417
|
}
|
|
653
|
-
branchSelect(config) {
|
|
1418
|
+
branchSelect(config, explicitId) {
|
|
654
1419
|
const node = {
|
|
655
1420
|
type: "step",
|
|
656
|
-
id: "branch:select",
|
|
1421
|
+
id: explicitId ?? "branch:select",
|
|
1422
|
+
category: "branch",
|
|
657
1423
|
execute: async (state) => {
|
|
658
1424
|
const ctx = state.ctx;
|
|
659
1425
|
const input = state.output;
|
|
660
1426
|
const key = await config.select({ ctx, input });
|
|
661
|
-
|
|
1427
|
+
const keyDeclared = Object.prototype.hasOwnProperty.call(config.agents, key);
|
|
1428
|
+
if (keyDeclared && config.agents[key] === void 0) {
|
|
1429
|
+
throw new WorkflowBranchError(
|
|
1430
|
+
"select",
|
|
1431
|
+
`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(", ")}`
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
let agent = keyDeclared ? config.agents[key] : void 0;
|
|
662
1435
|
if (!agent) {
|
|
1436
|
+
if (config.onUnknownKey) {
|
|
1437
|
+
config.onUnknownKey({
|
|
1438
|
+
key,
|
|
1439
|
+
availableKeys: Object.keys(config.agents),
|
|
1440
|
+
ctx
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
663
1443
|
if (config.fallback) {
|
|
664
1444
|
agent = config.fallback;
|
|
665
1445
|
} else {
|
|
@@ -669,16 +1449,37 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
669
1449
|
await this.executeAgent(state, agent, ctx, config);
|
|
670
1450
|
}
|
|
671
1451
|
};
|
|
672
|
-
return
|
|
1452
|
+
return this.appendStep(node);
|
|
673
1453
|
}
|
|
674
1454
|
// ── foreach: array iteration ─────────────────────────────────
|
|
1455
|
+
/**
|
|
1456
|
+
* Map each item of an array through an agent or sub-workflow.
|
|
1457
|
+
*
|
|
1458
|
+
* @param target Agent or `SealedWorkflow` invoked once per item.
|
|
1459
|
+
* @param options.id Override the default step id (`foreach:<agentId>` or
|
|
1460
|
+
* the workflow's id). Required when chaining multiple foreach over the same
|
|
1461
|
+
* target — the construction-time `(type, id)` walk rejects duplicates.
|
|
1462
|
+
* @param options.concurrency Max items in flight at any moment (default 1).
|
|
1463
|
+
* Backed by a semaphore: as soon as one item completes, the next launches —
|
|
1464
|
+
* no lockstep batching.
|
|
1465
|
+
* @param options.onError Per-iteration error handler. **Bypassed entirely on
|
|
1466
|
+
* the suspension path** (when any item hits a nested gate) — see the
|
|
1467
|
+
* foreach concurrency hazards in the README. Otherwise: return a
|
|
1468
|
+
* `TNextOutput` value to substitute, return `Workflow.SKIP` to omit, throw
|
|
1469
|
+
* to abort. Invoked sequentially in index order after all items settle.
|
|
1470
|
+
*/
|
|
675
1471
|
foreach(target, options) {
|
|
676
1472
|
const concurrency = options?.concurrency ?? 1;
|
|
1473
|
+
const onError = options?.onError;
|
|
677
1474
|
const isWorkflow = target instanceof SealedWorkflow;
|
|
678
|
-
const
|
|
1475
|
+
const defaultId = isWorkflow ? target.id ?? "foreach" : `foreach:${target.id}`;
|
|
1476
|
+
const id = options?.id ?? defaultId;
|
|
679
1477
|
const node = {
|
|
680
1478
|
type: "step",
|
|
681
1479
|
id,
|
|
1480
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1481
|
+
nestedWorkflow: isWorkflow ? target : void 0,
|
|
1482
|
+
category: "foreach",
|
|
682
1483
|
execute: async (state) => {
|
|
683
1484
|
const items = state.output;
|
|
684
1485
|
if (!Array.isArray(items)) {
|
|
@@ -686,42 +1487,312 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
686
1487
|
}
|
|
687
1488
|
const ctx = state.ctx;
|
|
688
1489
|
const results = new Array(items.length);
|
|
1490
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
1491
|
+
const itemStates = new Array(items.length);
|
|
689
1492
|
const executeItem = async (item, index) => {
|
|
690
|
-
const itemState = {
|
|
691
|
-
|
|
692
|
-
|
|
1493
|
+
const itemState = {
|
|
1494
|
+
ctx: state.ctx,
|
|
1495
|
+
output: item,
|
|
1496
|
+
mode: "generate",
|
|
1497
|
+
abortSignal: state.abortSignal
|
|
1498
|
+
};
|
|
1499
|
+
itemStates[index] = itemState;
|
|
1500
|
+
const itemStart = performance.now();
|
|
1501
|
+
await this.fireHook(state, "onItemStart", {
|
|
1502
|
+
stepId: id,
|
|
1503
|
+
type: "foreach",
|
|
1504
|
+
itemIndex: index,
|
|
1505
|
+
ctx: state.ctx,
|
|
1506
|
+
input: item
|
|
1507
|
+
});
|
|
1508
|
+
try {
|
|
1509
|
+
if (isWorkflow) {
|
|
1510
|
+
await this.executeNestedWorkflow(itemState, target);
|
|
1511
|
+
} else {
|
|
1512
|
+
await this.executeAgent(itemState, target, ctx);
|
|
1513
|
+
}
|
|
1514
|
+
results[index] = itemState.output;
|
|
1515
|
+
await this.fireHook(state, "onItemFinish", {
|
|
1516
|
+
stepId: id,
|
|
1517
|
+
type: "foreach",
|
|
1518
|
+
itemIndex: index,
|
|
1519
|
+
ctx: state.ctx,
|
|
1520
|
+
output: itemState.output,
|
|
1521
|
+
durationMs: performance.now() - itemStart
|
|
1522
|
+
});
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
await this.fireHook(state, "onItemError", {
|
|
1525
|
+
stepId: id,
|
|
1526
|
+
type: "foreach",
|
|
1527
|
+
itemIndex: index,
|
|
1528
|
+
ctx: state.ctx,
|
|
1529
|
+
error,
|
|
1530
|
+
durationMs: performance.now() - itemStart
|
|
1531
|
+
});
|
|
1532
|
+
throw error;
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
const mergeItemWarnings = () => {
|
|
1536
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
1537
|
+
const its = itemStates[idx];
|
|
1538
|
+
if (!its?.warnings) continue;
|
|
1539
|
+
for (const w of its.warnings) {
|
|
1540
|
+
pushWarning(state, w.source, `${id}[${idx}]:${w.stepId}`, w.error);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
const handleRejection = async (error, item, index) => {
|
|
1545
|
+
if (!onError) throw error;
|
|
1546
|
+
const recovered = await onError({
|
|
1547
|
+
error,
|
|
1548
|
+
item,
|
|
1549
|
+
index,
|
|
1550
|
+
ctx: state.ctx
|
|
1551
|
+
});
|
|
1552
|
+
if (recovered === _Workflow.SKIP) {
|
|
1553
|
+
skipped.add(index);
|
|
693
1554
|
} else {
|
|
694
|
-
|
|
1555
|
+
results[index] = recovered;
|
|
695
1556
|
}
|
|
696
|
-
results[index] = itemState.output;
|
|
697
1557
|
};
|
|
1558
|
+
const failures = [];
|
|
1559
|
+
const signal = state.abortSignal;
|
|
698
1560
|
if (concurrency <= 1) {
|
|
699
1561
|
for (let i = 0; i < items.length; i++) {
|
|
700
|
-
|
|
1562
|
+
if (signal?.aborted) {
|
|
1563
|
+
failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
try {
|
|
1567
|
+
await executeItem(items[i], i);
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
failures.push({ index: i, error });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
} else {
|
|
1573
|
+
let nextIndex = 0;
|
|
1574
|
+
const worker = async () => {
|
|
1575
|
+
while (true) {
|
|
1576
|
+
const i = nextIndex++;
|
|
1577
|
+
if (i >= items.length) return;
|
|
1578
|
+
if (signal?.aborted) {
|
|
1579
|
+
failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
await executeItem(items[i], i);
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
failures.push({ index: i, error });
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
const workers = Array.from(
|
|
1590
|
+
{ length: Math.min(concurrency, items.length) },
|
|
1591
|
+
() => worker()
|
|
1592
|
+
);
|
|
1593
|
+
await Promise.all(workers);
|
|
1594
|
+
}
|
|
1595
|
+
failures.sort((a, b) => a.index - b.index);
|
|
1596
|
+
const gateFailures = [];
|
|
1597
|
+
const nonGateFailures = [];
|
|
1598
|
+
for (const f of failures) {
|
|
1599
|
+
if (f.error instanceof NestedGateUnsupportedError) {
|
|
1600
|
+
gateFailures.push({ index: f.index, error: f.error });
|
|
1601
|
+
} else {
|
|
1602
|
+
nonGateFailures.push(f);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
mergeItemWarnings();
|
|
1606
|
+
if (gateFailures.length > 0) {
|
|
1607
|
+
for (const nr of nonGateFailures) {
|
|
1608
|
+
pushWarning(state, "foreach-sibling", `${id}[${nr.index}]`, nr.error);
|
|
1609
|
+
}
|
|
1610
|
+
const lowest = gateFailures[0];
|
|
1611
|
+
const otherSuspensions = gateFailures.slice(1).map((g) => ({
|
|
1612
|
+
index: g.index,
|
|
1613
|
+
gateId: g.error.gateId
|
|
1614
|
+
}));
|
|
1615
|
+
const siblingErrors = nonGateFailures.map((nr) => nr.error);
|
|
1616
|
+
throw new NestedGateUnsupportedError(
|
|
1617
|
+
lowest.error.gateId,
|
|
1618
|
+
lowest.error.workflowId,
|
|
1619
|
+
siblingErrors,
|
|
1620
|
+
otherSuspensions
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
for (const { index, error } of nonGateFailures) {
|
|
1624
|
+
await handleRejection(error, items[index], index);
|
|
1625
|
+
}
|
|
1626
|
+
state.output = skipped.size === 0 ? results : results.filter((_, i) => !skipped.has(i));
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
return this.appendStep(node);
|
|
1630
|
+
}
|
|
1631
|
+
// Implementation
|
|
1632
|
+
parallel(branches, options) {
|
|
1633
|
+
const isTuple = Array.isArray(branches);
|
|
1634
|
+
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 }));
|
|
1635
|
+
const branchCount = entries.length;
|
|
1636
|
+
const requestedConcurrency = options?.concurrency;
|
|
1637
|
+
let effectiveConcurrency;
|
|
1638
|
+
if (requestedConcurrency === void 0) {
|
|
1639
|
+
effectiveConcurrency = Math.min(branchCount, 5);
|
|
1640
|
+
} else {
|
|
1641
|
+
effectiveConcurrency = requestedConcurrency;
|
|
1642
|
+
}
|
|
1643
|
+
if (requestedConcurrency === void 0 && branchCount > 5) {
|
|
1644
|
+
warnOnce(
|
|
1645
|
+
"pipeai:parallel-cap",
|
|
1646
|
+
`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.`
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
const onError = options?.onError;
|
|
1650
|
+
const id = options?.id ?? (isTuple ? "parallel:tuple" : "parallel:record");
|
|
1651
|
+
const node = {
|
|
1652
|
+
type: "step",
|
|
1653
|
+
id,
|
|
1654
|
+
category: "parallel",
|
|
1655
|
+
execute: async (state) => {
|
|
1656
|
+
const ctx = state.ctx;
|
|
1657
|
+
const input = state.output;
|
|
1658
|
+
const results = isTuple ? new Array(branchCount) : {};
|
|
1659
|
+
const branchStates = new Array(branchCount);
|
|
1660
|
+
const executeBranch = async ({ key, index, target }) => {
|
|
1661
|
+
const branchState = { ctx: state.ctx, output: input, mode: "generate" };
|
|
1662
|
+
branchStates[index] = branchState;
|
|
1663
|
+
const branchStart = performance.now();
|
|
1664
|
+
const itemIndex = isTuple ? index : key;
|
|
1665
|
+
await this.fireHook(state, "onItemStart", {
|
|
1666
|
+
stepId: id,
|
|
1667
|
+
type: "parallel",
|
|
1668
|
+
itemIndex,
|
|
1669
|
+
ctx: state.ctx,
|
|
1670
|
+
input
|
|
1671
|
+
});
|
|
1672
|
+
try {
|
|
1673
|
+
if (target instanceof SealedWorkflow) {
|
|
1674
|
+
await this.executeNestedWorkflow(branchState, target);
|
|
1675
|
+
} else {
|
|
1676
|
+
await this.executeAgent(branchState, target, ctx);
|
|
1677
|
+
}
|
|
1678
|
+
results[key] = branchState.output;
|
|
1679
|
+
await this.fireHook(state, "onItemFinish", {
|
|
1680
|
+
stepId: id,
|
|
1681
|
+
type: "parallel",
|
|
1682
|
+
itemIndex,
|
|
1683
|
+
ctx: state.ctx,
|
|
1684
|
+
output: branchState.output,
|
|
1685
|
+
durationMs: performance.now() - branchStart
|
|
1686
|
+
});
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
await this.fireHook(state, "onItemError", {
|
|
1689
|
+
stepId: id,
|
|
1690
|
+
type: "parallel",
|
|
1691
|
+
itemIndex,
|
|
1692
|
+
ctx: state.ctx,
|
|
1693
|
+
error,
|
|
1694
|
+
durationMs: performance.now() - branchStart
|
|
1695
|
+
});
|
|
1696
|
+
throw error;
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
const failures = [];
|
|
1700
|
+
const eff = Number.isFinite(effectiveConcurrency) ? Math.max(1, effectiveConcurrency) : branchCount;
|
|
1701
|
+
if (eff <= 1) {
|
|
1702
|
+
for (const e of entries) {
|
|
1703
|
+
try {
|
|
1704
|
+
await executeBranch(e);
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
failures.push({ key: e.key, index: e.index, error });
|
|
1707
|
+
}
|
|
701
1708
|
}
|
|
702
1709
|
} else {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1710
|
+
let nextIndex = 0;
|
|
1711
|
+
const worker = async () => {
|
|
1712
|
+
while (true) {
|
|
1713
|
+
const i = nextIndex++;
|
|
1714
|
+
if (i >= branchCount) return;
|
|
1715
|
+
const e = entries[i];
|
|
1716
|
+
try {
|
|
1717
|
+
await executeBranch(e);
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
failures.push({ key: e.key, index: e.index, error });
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
const workers = Array.from(
|
|
1724
|
+
{ length: Math.min(eff, branchCount) },
|
|
1725
|
+
() => worker()
|
|
1726
|
+
);
|
|
1727
|
+
await Promise.all(workers);
|
|
1728
|
+
}
|
|
1729
|
+
for (let idx = 0; idx < branchCount; idx++) {
|
|
1730
|
+
const bs = branchStates[idx];
|
|
1731
|
+
if (!bs?.warnings) continue;
|
|
1732
|
+
for (const w of bs.warnings) {
|
|
1733
|
+
pushWarning(state, w.source, `${id}[${entries[idx].key}]:${w.stepId}`, w.error);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const gateFailures = [];
|
|
1737
|
+
const nonGateFailures = [];
|
|
1738
|
+
for (const f of failures) {
|
|
1739
|
+
if (f.error instanceof NestedGateUnsupportedError) gateFailures.push({ key: f.key, index: f.index, error: f.error });
|
|
1740
|
+
else nonGateFailures.push(f);
|
|
1741
|
+
}
|
|
1742
|
+
gateFailures.sort((a, b) => a.index - b.index);
|
|
1743
|
+
nonGateFailures.sort((a, b) => a.index - b.index);
|
|
1744
|
+
if (gateFailures.length > 0) {
|
|
1745
|
+
for (const nr of nonGateFailures) {
|
|
1746
|
+
pushWarning(state, "foreach-sibling", `${id}[${nr.key}]`, nr.error);
|
|
1747
|
+
}
|
|
1748
|
+
const lowest = gateFailures[0];
|
|
1749
|
+
const otherSuspensions = gateFailures.slice(1).map((g) => ({ index: g.index, gateId: g.error.gateId }));
|
|
1750
|
+
const siblingErrors = nonGateFailures.map((nr) => nr.error);
|
|
1751
|
+
throw new NestedGateUnsupportedError(
|
|
1752
|
+
lowest.error.gateId,
|
|
1753
|
+
lowest.error.workflowId,
|
|
1754
|
+
siblingErrors,
|
|
1755
|
+
otherSuspensions
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
for (const { key, index, error } of nonGateFailures) {
|
|
1759
|
+
if (!onError) throw error;
|
|
1760
|
+
const recovered = await onError({
|
|
1761
|
+
error,
|
|
1762
|
+
key: isTuple ? void 0 : key,
|
|
1763
|
+
index: isTuple ? index : void 0,
|
|
1764
|
+
ctx: state.ctx
|
|
1765
|
+
});
|
|
1766
|
+
if (recovered === _Workflow.SKIP) {
|
|
1767
|
+
results[key] = void 0;
|
|
1768
|
+
} else {
|
|
1769
|
+
results[key] = recovered;
|
|
706
1770
|
}
|
|
707
1771
|
}
|
|
708
1772
|
state.output = results;
|
|
709
1773
|
}
|
|
710
1774
|
};
|
|
711
|
-
return
|
|
1775
|
+
return this.appendStep(node);
|
|
712
1776
|
}
|
|
713
1777
|
// ── repeat: conditional loop ─────────────────────────────────
|
|
714
1778
|
repeat(target, options) {
|
|
715
1779
|
const maxIterations = options.maxIterations ?? 10;
|
|
716
1780
|
const isWorkflow = target instanceof SealedWorkflow;
|
|
717
|
-
const
|
|
1781
|
+
const defaultId = isWorkflow ? target.id ?? "repeat" : `repeat:${target.id}`;
|
|
1782
|
+
const id = options.id ?? defaultId;
|
|
718
1783
|
const predicate = options.until ?? (async (p) => !await options.while(p));
|
|
719
1784
|
const node = {
|
|
720
1785
|
type: "step",
|
|
721
1786
|
id,
|
|
1787
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1788
|
+
nestedWorkflow: isWorkflow ? target : void 0,
|
|
1789
|
+
category: "repeat",
|
|
722
1790
|
execute: async (state) => {
|
|
723
1791
|
const ctx = state.ctx;
|
|
724
1792
|
for (let i = 1; i <= maxIterations; i++) {
|
|
1793
|
+
if (state.abortSignal?.aborted) {
|
|
1794
|
+
throw state.abortSignal.reason ?? new Error("Workflow aborted");
|
|
1795
|
+
}
|
|
725
1796
|
if (isWorkflow) {
|
|
726
1797
|
await this.executeNestedWorkflow(state, target);
|
|
727
1798
|
} else {
|
|
@@ -737,7 +1808,7 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
737
1808
|
throw new WorkflowLoopError(maxIterations, maxIterations);
|
|
738
1809
|
}
|
|
739
1810
|
};
|
|
740
|
-
return
|
|
1811
|
+
return this.appendStep(node);
|
|
741
1812
|
}
|
|
742
1813
|
// ── catch ─────────────────────────────────────────────────────
|
|
743
1814
|
catch(id, fn) {
|
|
@@ -749,27 +1820,29 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
749
1820
|
id,
|
|
750
1821
|
catchFn: fn
|
|
751
1822
|
};
|
|
752
|
-
return
|
|
753
|
-
}
|
|
754
|
-
// ── finally (terminal — returns sealed workflow) ──────────────
|
|
755
|
-
finally(id, fn) {
|
|
756
|
-
const node = {
|
|
757
|
-
type: "finally",
|
|
758
|
-
id,
|
|
759
|
-
execute: async (state) => {
|
|
760
|
-
await fn({ ctx: state.ctx });
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
return new SealedWorkflow([...this.steps, node], this.id);
|
|
1823
|
+
return this.appendStep(node);
|
|
764
1824
|
}
|
|
1825
|
+
// `.finally()` is inherited from SealedWorkflow now (it lives there so
|
|
1826
|
+
// multi-finally chains are possible — `.finally().finally()`).
|
|
765
1827
|
};
|
|
1828
|
+
|
|
1829
|
+
// src/index.ts
|
|
1830
|
+
var SKIP = Workflow.SKIP;
|
|
766
1831
|
// Annotate the CommonJS export names for ESM import in node:
|
|
767
1832
|
0 && (module.exports = {
|
|
768
1833
|
Agent,
|
|
1834
|
+
CHECKPOINT_STEP_ID,
|
|
1835
|
+
CheckpointTimeoutError,
|
|
1836
|
+
NestedGateUnsupportedError,
|
|
1837
|
+
SKIP,
|
|
1838
|
+
TOOL_PROVIDER_BRAND,
|
|
1839
|
+
ToolProvider,
|
|
769
1840
|
Workflow,
|
|
770
1841
|
WorkflowBranchError,
|
|
771
1842
|
WorkflowLoopError,
|
|
772
|
-
|
|
773
|
-
|
|
1843
|
+
defineTool,
|
|
1844
|
+
getActiveWriter,
|
|
1845
|
+
isToolProvider,
|
|
1846
|
+
migrateSnapshot
|
|
774
1847
|
});
|
|
775
1848
|
//# sourceMappingURL=index.cjs.map
|