pipeai 0.3.0 → 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 +455 -47
- package/dist/index.cjs +1207 -209
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +533 -59
- package/dist/index.d.ts +533 -59
- package/dist/index.js +1197 -207
- 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,35 +59,58 @@ function resolveValue(value, ctx, input) {
|
|
|
50
59
|
}
|
|
51
60
|
return value;
|
|
52
61
|
}
|
|
53
|
-
|
|
54
|
-
available;
|
|
55
|
-
waiters = [];
|
|
56
|
-
constructor(permits) {
|
|
57
|
-
if (!Number.isInteger(permits) || permits < 1) {
|
|
58
|
-
throw new Error(`Semaphore: permits must be a positive integer, got ${permits}`);
|
|
59
|
-
}
|
|
60
|
-
this.available = permits;
|
|
61
|
-
}
|
|
62
|
-
async acquire() {
|
|
63
|
-
if (this.available > 0) {
|
|
64
|
-
this.available--;
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
await new Promise((resolve) => this.waiters.push(resolve));
|
|
68
|
-
}
|
|
69
|
-
release() {
|
|
70
|
-
const next = this.waiters.shift();
|
|
71
|
-
if (next) next();
|
|
72
|
-
else this.available++;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
async function extractOutput(result, hasStructuredOutput) {
|
|
62
|
+
async function extractOutput(result, hasStructuredOutput, schema) {
|
|
76
63
|
if (hasStructuredOutput) {
|
|
77
64
|
const output = await result.output;
|
|
78
|
-
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;
|
|
79
74
|
}
|
|
80
75
|
return await result.text;
|
|
81
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
|
+
}
|
|
82
114
|
|
|
83
115
|
// src/tool-provider.ts
|
|
84
116
|
var TOOL_PROVIDER_BRAND = /* @__PURE__ */ Symbol.for("agent-workflow.ToolProvider");
|
|
@@ -109,10 +141,17 @@ var Agent = class {
|
|
|
109
141
|
id;
|
|
110
142
|
description;
|
|
111
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;
|
|
112
152
|
config;
|
|
113
153
|
_hasDynamicConfig;
|
|
114
154
|
_resolvedStaticTools = null;
|
|
115
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
155
|
_passthrough;
|
|
117
156
|
_onStepFinish;
|
|
118
157
|
_onFinish;
|
|
@@ -120,6 +159,7 @@ var Agent = class {
|
|
|
120
159
|
this.id = config.id;
|
|
121
160
|
this.description = config.description ?? "";
|
|
122
161
|
this.hasOutput = config.output !== void 0;
|
|
162
|
+
this.validateOutput = config.validateOutput;
|
|
123
163
|
this.config = config;
|
|
124
164
|
this._hasDynamicConfig = [
|
|
125
165
|
config.model,
|
|
@@ -128,8 +168,7 @@ var Agent = class {
|
|
|
128
168
|
config.messages,
|
|
129
169
|
config.tools,
|
|
130
170
|
config.activeTools,
|
|
131
|
-
config.toolChoice
|
|
132
|
-
config.stopWhen
|
|
171
|
+
config.toolChoice
|
|
133
172
|
].some((v) => typeof v === "function");
|
|
134
173
|
if (!this._hasDynamicConfig) {
|
|
135
174
|
const rawTools = config.tools ?? {};
|
|
@@ -143,6 +182,7 @@ var Agent = class {
|
|
|
143
182
|
description: _desc,
|
|
144
183
|
input: _inputSchema,
|
|
145
184
|
output: _output,
|
|
185
|
+
validateOutput: _validateOutput,
|
|
146
186
|
model: _m,
|
|
147
187
|
system: _s,
|
|
148
188
|
prompt: _p,
|
|
@@ -162,25 +202,13 @@ var Agent = class {
|
|
|
162
202
|
}
|
|
163
203
|
async generate(ctx, ...args) {
|
|
164
204
|
const input = args[0];
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
return await (0, import_ai2.generateText)(options);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
if (this.config.onError) {
|
|
171
|
-
await this.config.onError({ error, ctx, input, writer: getActiveWriter() });
|
|
172
|
-
}
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
205
|
+
const callOptions = args[1];
|
|
206
|
+
return this.generateWithOptions(ctx, input, callOptions ?? {});
|
|
175
207
|
}
|
|
176
208
|
async stream(ctx, ...args) {
|
|
177
209
|
const input = args[0];
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
return (0, import_ai2.streamText)({
|
|
181
|
-
...options,
|
|
182
|
-
onError: this.config.onError ? ({ error }) => this.config.onError({ error, ctx, input, writer: getActiveWriter() }) : void 0
|
|
183
|
-
});
|
|
210
|
+
const callOptions = args[1];
|
|
211
|
+
return this.streamWithOptions(ctx, input, callOptions ?? {});
|
|
184
212
|
}
|
|
185
213
|
asTool(ctx, options) {
|
|
186
214
|
return this.createToolInstance(ctx, options);
|
|
@@ -201,24 +229,86 @@ var Agent = class {
|
|
|
201
229
|
return (0, import_ai2.tool)({
|
|
202
230
|
description: this.description,
|
|
203
231
|
inputSchema: this.config.input,
|
|
204
|
-
|
|
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;
|
|
205
238
|
const writer = getActiveWriter();
|
|
206
239
|
if (writer) {
|
|
207
|
-
const result2 = await this.
|
|
240
|
+
const result2 = await this.streamWithOptions(ctx, toolInput, { abortSignal });
|
|
208
241
|
writer.merge(result2.toUIMessageStream());
|
|
209
|
-
if (options?.mapOutput)
|
|
210
|
-
|
|
242
|
+
if (options?.mapOutput) {
|
|
243
|
+
await result2.text;
|
|
244
|
+
return options.mapOutput(result2);
|
|
245
|
+
}
|
|
246
|
+
return extractOutput(result2, this.hasOutput, this.validateOutput);
|
|
211
247
|
}
|
|
212
|
-
const result = await this.
|
|
248
|
+
const result = await this.generateWithOptions(ctx, toolInput, { abortSignal });
|
|
213
249
|
if (options?.mapOutput) return options.mapOutput(result);
|
|
214
|
-
return extractOutput(result, this.hasOutput);
|
|
250
|
+
return extractOutput(result, this.hasOutput, this.validateOutput);
|
|
215
251
|
}
|
|
216
252
|
// TS cannot simplify the SDK's `NeverOptional<TOutput, ...>` conditional in a
|
|
217
253
|
// generic context, so we cast through `unknown` instead of `any`.
|
|
218
254
|
});
|
|
219
255
|
}
|
|
220
|
-
//
|
|
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
|
+
}
|
|
221
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
|
+
}
|
|
222
312
|
return {
|
|
223
313
|
...this._passthrough,
|
|
224
314
|
model: resolved.model,
|
|
@@ -226,11 +316,16 @@ var Agent = class {
|
|
|
226
316
|
activeTools: resolved.activeTools,
|
|
227
317
|
toolChoice: resolved.toolChoice,
|
|
228
318
|
stopWhen: resolved.stopWhen,
|
|
229
|
-
...resolved.messages ? { messages: resolved.messages } : { prompt: resolved.prompt
|
|
230
|
-
|
|
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 } : {},
|
|
231
323
|
...this.config.output ? { output: this.config.output } : {},
|
|
232
|
-
|
|
233
|
-
|
|
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() }) } : {}
|
|
234
329
|
};
|
|
235
330
|
}
|
|
236
331
|
resolveConfig(ctx, input) {
|
|
@@ -252,18 +347,27 @@ var Agent = class {
|
|
|
252
347
|
return this.resolveConfigAsync(ctx, input);
|
|
253
348
|
}
|
|
254
349
|
async resolveConfigAsync(ctx, input) {
|
|
255
|
-
const [model, prompt, system, messages, rawTools, activeTools, toolChoice
|
|
350
|
+
const [model, prompt, system, messages, rawTools, activeTools, toolChoice] = await Promise.all([
|
|
256
351
|
resolveValue(this.config.model, ctx, input),
|
|
257
352
|
resolveValue(this.config.prompt, ctx, input),
|
|
258
353
|
resolveValue(this.config.system, ctx, input),
|
|
259
354
|
resolveValue(this.config.messages, ctx, input),
|
|
260
355
|
resolveValue(this.config.tools, ctx, input),
|
|
261
356
|
resolveValue(this.config.activeTools, ctx, input),
|
|
262
|
-
resolveValue(this.config.toolChoice, ctx, input)
|
|
263
|
-
resolveValue(this.config.stopWhen, ctx, input)
|
|
357
|
+
resolveValue(this.config.toolChoice, ctx, input)
|
|
264
358
|
]);
|
|
265
359
|
const tools = this.resolveTools(rawTools ?? {}, ctx);
|
|
266
|
-
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
|
+
};
|
|
267
371
|
}
|
|
268
372
|
resolveTools(tools, ctx) {
|
|
269
373
|
const entries = Object.entries(tools);
|
|
@@ -299,37 +403,284 @@ var WorkflowLoopError = class extends Error {
|
|
|
299
403
|
this.name = "WorkflowLoopError";
|
|
300
404
|
}
|
|
301
405
|
};
|
|
302
|
-
var
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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;
|
|
308
420
|
}
|
|
309
421
|
};
|
|
310
|
-
var
|
|
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;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
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 {
|
|
311
523
|
id;
|
|
312
524
|
steps;
|
|
313
|
-
|
|
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) {
|
|
314
533
|
this.steps = steps;
|
|
315
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
|
+
}
|
|
316
660
|
}
|
|
317
661
|
// ── Execution ─────────────────────────────────────────────────
|
|
318
662
|
async generate(ctx, ...args) {
|
|
663
|
+
this.ensureDuplicateCheck();
|
|
319
664
|
const input = args[0];
|
|
665
|
+
const opts = args[1];
|
|
666
|
+
this.validateRunOptions(opts);
|
|
320
667
|
const state = {
|
|
321
668
|
ctx,
|
|
322
669
|
output: input,
|
|
323
|
-
mode: "generate"
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
output: state.output
|
|
670
|
+
mode: "generate",
|
|
671
|
+
runOptions: opts,
|
|
672
|
+
abortSignal: opts?.abortSignal
|
|
328
673
|
};
|
|
674
|
+
await this.execute(state, 0, opts);
|
|
675
|
+
return this.buildResult(state);
|
|
329
676
|
}
|
|
330
677
|
stream(ctx, ...args) {
|
|
678
|
+
this.ensureDuplicateCheck();
|
|
331
679
|
const input = args[0];
|
|
332
680
|
const options = args[1];
|
|
681
|
+
const opts = args[2];
|
|
682
|
+
this.validateRunOptions(opts);
|
|
683
|
+
const abortSignal = opts?.abortSignal;
|
|
333
684
|
let resolveOutput;
|
|
334
685
|
let rejectOutput;
|
|
335
686
|
const outputPromise = new Promise((res, rej) => {
|
|
@@ -344,11 +695,15 @@ var SealedWorkflow = class {
|
|
|
344
695
|
ctx,
|
|
345
696
|
output: input,
|
|
346
697
|
mode: "stream",
|
|
347
|
-
writer
|
|
698
|
+
writer,
|
|
699
|
+
runOptions: opts,
|
|
700
|
+
abortSignal
|
|
348
701
|
};
|
|
349
702
|
try {
|
|
350
|
-
await this.execute(state);
|
|
351
|
-
|
|
703
|
+
await this.execute(state, 0, opts);
|
|
704
|
+
const result = this.buildResult(state);
|
|
705
|
+
maybeWarnStreamOnErrorOnSuspend(result, options);
|
|
706
|
+
resolveOutput(result);
|
|
352
707
|
} catch (error) {
|
|
353
708
|
rejectOutput(error);
|
|
354
709
|
throw error;
|
|
@@ -362,20 +717,78 @@ var SealedWorkflow = class {
|
|
|
362
717
|
output: outputPromise
|
|
363
718
|
};
|
|
364
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
|
+
}
|
|
365
732
|
// ── Internal: execute pipeline ────────────────────────────────
|
|
366
|
-
async execute(state, startIndex = 0) {
|
|
733
|
+
async execute(state, startIndex = 0, opts, initialError = null) {
|
|
367
734
|
if (this.steps.length === 0) {
|
|
368
735
|
throw new Error("Workflow has no steps. Add at least one step before calling generate() or stream().");
|
|
369
736
|
}
|
|
370
|
-
|
|
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
|
+
});
|
|
371
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
|
+
}
|
|
372
759
|
const node = this.steps[i];
|
|
373
760
|
if (node.type === "finally") {
|
|
374
|
-
|
|
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
|
+
}
|
|
375
785
|
continue;
|
|
376
786
|
}
|
|
377
787
|
if (node.type === "catch") {
|
|
378
|
-
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 });
|
|
379
792
|
try {
|
|
380
793
|
state.output = await node.catchFn({
|
|
381
794
|
error: pendingError.error,
|
|
@@ -384,49 +797,183 @@ var SealedWorkflow = class {
|
|
|
384
797
|
stepId: pendingError.stepId
|
|
385
798
|
});
|
|
386
799
|
pendingError = null;
|
|
387
|
-
|
|
388
|
-
|
|
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" };
|
|
389
818
|
}
|
|
390
819
|
continue;
|
|
391
820
|
}
|
|
821
|
+
if (state.suspension || pendingError) continue;
|
|
392
822
|
if (node.type === "gate") {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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,
|
|
402
872
|
output: state.output,
|
|
403
|
-
|
|
404
|
-
|
|
873
|
+
durationMs: performance.now() - sStart,
|
|
874
|
+
suspended: false
|
|
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
|
|
405
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`);
|
|
406
940
|
}
|
|
407
|
-
|
|
941
|
+
throw pendingError.error;
|
|
942
|
+
} else if (pendingError && state.suspension) {
|
|
943
|
+
demotePendingError(state, pendingError);
|
|
408
944
|
try {
|
|
409
|
-
await
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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);
|
|
413
954
|
}
|
|
955
|
+
pendingError = null;
|
|
414
956
|
}
|
|
415
|
-
if (pendingError) throw pendingError.error;
|
|
416
957
|
}
|
|
417
958
|
// ── Internal: execute a nested workflow within a step/loop ─────
|
|
418
959
|
// Defined on SealedWorkflow (not Workflow) because TypeScript's protected
|
|
419
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).
|
|
420
965
|
async executeNestedWorkflow(state, workflow) {
|
|
966
|
+
const savedRunOptions = state.runOptions;
|
|
967
|
+
state.runOptions = void 0;
|
|
421
968
|
try {
|
|
422
969
|
await workflow.execute(state);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
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);
|
|
430
977
|
}
|
|
431
978
|
}
|
|
432
979
|
// ── Internal: execute an agent within a step/branch ───────────
|
|
@@ -436,59 +983,141 @@ var SealedWorkflow = class {
|
|
|
436
983
|
async executeAgent(state, agent, ctx, options) {
|
|
437
984
|
const input = state.output;
|
|
438
985
|
const hasStructuredOutput = agent.hasOutput;
|
|
986
|
+
const abortSignal = state.abortSignal;
|
|
987
|
+
const agentCallOpts = abortSignal ? { abortSignal } : void 0;
|
|
439
988
|
if (state.mode === "stream" && state.writer) {
|
|
440
989
|
const writer = state.writer;
|
|
441
990
|
await runWithWriter(writer, async () => {
|
|
442
|
-
const result = await agent.stream(ctx, state.output);
|
|
991
|
+
const result = await agent.stream(ctx, state.output, agentCallOpts);
|
|
443
992
|
if (options?.handleStream) {
|
|
444
993
|
await options.handleStream({ result, writer, ctx });
|
|
445
994
|
} else {
|
|
446
995
|
writer.merge(result.toUIMessageStream());
|
|
447
996
|
}
|
|
448
|
-
|
|
449
|
-
|
|
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);
|
|
450
1006
|
}
|
|
451
|
-
if (options?.
|
|
452
|
-
state.output = await options.
|
|
1007
|
+
if (options?.mapResult) {
|
|
1008
|
+
state.output = await options.mapResult(hookParams);
|
|
453
1009
|
} else {
|
|
454
|
-
state.output = await extractOutput(result, hasStructuredOutput);
|
|
1010
|
+
state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
|
|
455
1011
|
}
|
|
456
1012
|
});
|
|
457
1013
|
} else {
|
|
458
|
-
const result = await agent.generate(ctx, state.output);
|
|
459
|
-
|
|
460
|
-
|
|
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);
|
|
461
1024
|
}
|
|
462
|
-
if (options?.
|
|
463
|
-
state.output = await options.
|
|
1025
|
+
if (options?.mapResult) {
|
|
1026
|
+
state.output = await options.mapResult(hookParams);
|
|
464
1027
|
} else {
|
|
465
|
-
state.output = await extractOutput(result, hasStructuredOutput);
|
|
1028
|
+
state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
|
|
466
1029
|
}
|
|
467
1030
|
}
|
|
468
1031
|
}
|
|
469
1032
|
// ── Gate: load persisted state for resumption ──────────────────
|
|
470
1033
|
loadState(gateId, snapshot) {
|
|
471
|
-
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) {
|
|
472
1039
|
throw new Error(
|
|
473
|
-
`loadState: gate ID mismatch \u2014 expected "${gateId}" but snapshot has "${
|
|
1040
|
+
`loadState: gate ID mismatch \u2014 expected "${gateId}" but snapshot has "${gateLike.gateId}".`
|
|
474
1041
|
);
|
|
475
1042
|
}
|
|
476
|
-
|
|
1043
|
+
this.ensureDuplicateCheck();
|
|
1044
|
+
const gateIndex = this.findGateIndex(gateLike);
|
|
477
1045
|
const gateNode = this.steps[gateIndex];
|
|
478
|
-
return new ResumedWorkflow(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
gateNode.
|
|
482
|
-
|
|
483
|
-
snapshot
|
|
484
|
-
|
|
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);
|
|
485
1114
|
}
|
|
486
1115
|
findGateIndex(snapshot) {
|
|
487
|
-
if (snapshot.version !== 1) {
|
|
1116
|
+
if (snapshot.version !== 1 && snapshot.version !== 2) {
|
|
488
1117
|
throw new Error(`Unsupported snapshot version: ${snapshot.version}`);
|
|
489
1118
|
}
|
|
490
1119
|
const hint = snapshot.resumeFromIndex;
|
|
491
|
-
if (hint >= 0 && hint < this.steps.length) {
|
|
1120
|
+
if (Number.isInteger(hint) && hint >= 0 && hint < this.steps.length) {
|
|
492
1121
|
const node = this.steps[hint];
|
|
493
1122
|
if (node.type === "gate" && node.id === snapshot.gateId) {
|
|
494
1123
|
return hint;
|
|
@@ -511,12 +1140,12 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
511
1140
|
mergeFn;
|
|
512
1141
|
priorOutput;
|
|
513
1142
|
/** @internal */
|
|
514
|
-
constructor(steps, startIndex,
|
|
515
|
-
super(steps);
|
|
1143
|
+
constructor(steps, startIndex, config) {
|
|
1144
|
+
super(steps, void 0, config.observability);
|
|
516
1145
|
this.startIndex = startIndex;
|
|
517
|
-
this.schema = schema;
|
|
518
|
-
this.mergeFn = mergeFn;
|
|
519
|
-
this.priorOutput = priorOutput;
|
|
1146
|
+
this.schema = config.schema;
|
|
1147
|
+
this.mergeFn = config.mergeFn;
|
|
1148
|
+
this.priorOutput = config.priorOutput;
|
|
520
1149
|
}
|
|
521
1150
|
validateResponse(response) {
|
|
522
1151
|
if (this.schema) {
|
|
@@ -525,15 +1154,31 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
525
1154
|
return response;
|
|
526
1155
|
}
|
|
527
1156
|
async generate(ctx, ...args) {
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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);
|
|
533
1176
|
}
|
|
534
1177
|
stream(ctx, ...args) {
|
|
535
|
-
const
|
|
1178
|
+
const rawResponse = args[0];
|
|
536
1179
|
const options = args[1];
|
|
1180
|
+
const opts = args[2];
|
|
1181
|
+
const abortSignal = opts?.abortSignal;
|
|
537
1182
|
let resolveOutput;
|
|
538
1183
|
let rejectOutput;
|
|
539
1184
|
const outputPromise = new Promise((res, rej) => {
|
|
@@ -544,18 +1189,92 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
544
1189
|
});
|
|
545
1190
|
const mergeFn = this.mergeFn;
|
|
546
1191
|
const priorOutput = this.priorOutput;
|
|
1192
|
+
const startIndex = this.startIndex;
|
|
547
1193
|
const stream = (0, import_ai3.createUIMessageStream)({
|
|
548
1194
|
execute: async ({ writer }) => {
|
|
549
|
-
|
|
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
|
+
}
|
|
550
1203
|
const state = {
|
|
551
1204
|
ctx,
|
|
552
1205
|
output,
|
|
553
1206
|
mode: "stream",
|
|
554
|
-
writer
|
|
1207
|
+
writer,
|
|
1208
|
+
runOptions: opts,
|
|
1209
|
+
abortSignal
|
|
1210
|
+
};
|
|
1211
|
+
try {
|
|
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
|
|
555
1272
|
};
|
|
556
1273
|
try {
|
|
557
|
-
await this.execute(state,
|
|
558
|
-
|
|
1274
|
+
await this.execute(state, startIndex, opts);
|
|
1275
|
+
const result = this.buildResult(state);
|
|
1276
|
+
maybeWarnStreamOnErrorOnSuspend(result, options);
|
|
1277
|
+
resolveOutput(result);
|
|
559
1278
|
} catch (error) {
|
|
560
1279
|
rejectOutput(error);
|
|
561
1280
|
throw error;
|
|
@@ -574,15 +1293,21 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
574
1293
|
* shortening it relative to the input array.
|
|
575
1294
|
*/
|
|
576
1295
|
static SKIP = /* @__PURE__ */ Symbol("pipeai.foreach.skip");
|
|
577
|
-
constructor(steps = [], id) {
|
|
578
|
-
super(steps, id);
|
|
1296
|
+
constructor(steps = [], id, observability) {
|
|
1297
|
+
super(steps, id, observability);
|
|
579
1298
|
}
|
|
580
1299
|
static create(options) {
|
|
581
|
-
return new _Workflow([], options?.id);
|
|
1300
|
+
return new _Workflow([], options?.id, options?.observability);
|
|
582
1301
|
}
|
|
583
1302
|
static from(agent, options) {
|
|
584
1303
|
return new _Workflow([]).step(agent, options);
|
|
585
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
|
+
}
|
|
586
1311
|
// ── step: implementation ──────────────────────────────────────
|
|
587
1312
|
step(target, optionsOrFn) {
|
|
588
1313
|
if (target instanceof SealedWorkflow) {
|
|
@@ -590,11 +1315,15 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
590
1315
|
const node2 = {
|
|
591
1316
|
type: "step",
|
|
592
1317
|
id: workflow.id ?? "nested-workflow",
|
|
1318
|
+
nestedWorkflow: workflow,
|
|
1319
|
+
// Feeds the recursive stepShapeHash walk.
|
|
1320
|
+
category: "nested",
|
|
1321
|
+
// Observability event type.
|
|
593
1322
|
execute: async (state) => {
|
|
594
1323
|
await this.executeNestedWorkflow(state, workflow);
|
|
595
1324
|
}
|
|
596
1325
|
};
|
|
597
|
-
return
|
|
1326
|
+
return this.appendStep(node2);
|
|
598
1327
|
}
|
|
599
1328
|
if (typeof target === "string") {
|
|
600
1329
|
if (typeof optionsOrFn !== "function") {
|
|
@@ -611,19 +1340,19 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
611
1340
|
});
|
|
612
1341
|
}
|
|
613
1342
|
};
|
|
614
|
-
return
|
|
1343
|
+
return this.appendStep(node2);
|
|
615
1344
|
}
|
|
616
1345
|
const agent = target;
|
|
617
1346
|
const options = optionsOrFn;
|
|
618
1347
|
const node = {
|
|
619
1348
|
type: "step",
|
|
620
|
-
id: agent.id,
|
|
1349
|
+
id: options?.id ?? agent.id,
|
|
621
1350
|
execute: async (state) => {
|
|
622
1351
|
const ctx = state.ctx;
|
|
623
1352
|
await this.executeAgent(state, agent, ctx, options);
|
|
624
1353
|
}
|
|
625
1354
|
};
|
|
626
|
-
return
|
|
1355
|
+
return this.appendStep(node);
|
|
627
1356
|
}
|
|
628
1357
|
// ── gate: human-in-the-loop suspension point ────────────────
|
|
629
1358
|
gate(id, options) {
|
|
@@ -649,19 +1378,20 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
649
1378
|
return state.output;
|
|
650
1379
|
}
|
|
651
1380
|
};
|
|
652
|
-
return
|
|
1381
|
+
return this.appendStep(node);
|
|
653
1382
|
}
|
|
654
1383
|
// ── branch: implementation ────────────────────────────────────
|
|
655
|
-
branch(casesOrConfig) {
|
|
1384
|
+
branch(casesOrConfig, options) {
|
|
656
1385
|
if (Array.isArray(casesOrConfig)) {
|
|
657
|
-
return this.branchPredicate(casesOrConfig);
|
|
1386
|
+
return this.branchPredicate(casesOrConfig, options?.id);
|
|
658
1387
|
}
|
|
659
|
-
return this.branchSelect(casesOrConfig);
|
|
1388
|
+
return this.branchSelect(casesOrConfig, options?.id);
|
|
660
1389
|
}
|
|
661
|
-
branchPredicate(cases) {
|
|
1390
|
+
branchPredicate(cases, explicitId) {
|
|
662
1391
|
const node = {
|
|
663
1392
|
type: "step",
|
|
664
|
-
id: "branch:predicate",
|
|
1393
|
+
id: explicitId ?? "branch:predicate",
|
|
1394
|
+
category: "branch",
|
|
665
1395
|
execute: async (state) => {
|
|
666
1396
|
const ctx = state.ctx;
|
|
667
1397
|
const input = state.output;
|
|
@@ -673,21 +1403,43 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
673
1403
|
await this.executeAgent(state, branchCase.agent, ctx, branchCase);
|
|
674
1404
|
return;
|
|
675
1405
|
}
|
|
676
|
-
|
|
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}`);
|
|
677
1414
|
}
|
|
678
1415
|
};
|
|
679
|
-
return
|
|
1416
|
+
return this.appendStep(node);
|
|
680
1417
|
}
|
|
681
|
-
branchSelect(config) {
|
|
1418
|
+
branchSelect(config, explicitId) {
|
|
682
1419
|
const node = {
|
|
683
1420
|
type: "step",
|
|
684
|
-
id: "branch:select",
|
|
1421
|
+
id: explicitId ?? "branch:select",
|
|
1422
|
+
category: "branch",
|
|
685
1423
|
execute: async (state) => {
|
|
686
1424
|
const ctx = state.ctx;
|
|
687
1425
|
const input = state.output;
|
|
688
1426
|
const key = await config.select({ ctx, input });
|
|
689
|
-
|
|
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;
|
|
690
1435
|
if (!agent) {
|
|
1436
|
+
if (config.onUnknownKey) {
|
|
1437
|
+
config.onUnknownKey({
|
|
1438
|
+
key,
|
|
1439
|
+
availableKeys: Object.keys(config.agents),
|
|
1440
|
+
ctx
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
691
1443
|
if (config.fallback) {
|
|
692
1444
|
agent = config.fallback;
|
|
693
1445
|
} else {
|
|
@@ -697,32 +1449,37 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
697
1449
|
await this.executeAgent(state, agent, ctx, config);
|
|
698
1450
|
}
|
|
699
1451
|
};
|
|
700
|
-
return
|
|
1452
|
+
return this.appendStep(node);
|
|
701
1453
|
}
|
|
702
1454
|
// ── foreach: array iteration ─────────────────────────────────
|
|
703
1455
|
/**
|
|
704
1456
|
* Map each item of an array through an agent or sub-workflow.
|
|
705
1457
|
*
|
|
706
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.
|
|
707
1462
|
* @param options.concurrency Max items in flight at any moment (default 1).
|
|
708
1463
|
* Backed by a semaphore: as soon as one item completes, the next launches —
|
|
709
1464
|
* no lockstep batching.
|
|
710
|
-
* @param options.onError Per-iteration error handler.
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
* to abort
|
|
715
|
-
* `.catch()`). When omitted, the existing fail-fast behavior is preserved.
|
|
716
|
-
* `onError` is invoked sequentially in index order after all items settle.
|
|
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.
|
|
717
1470
|
*/
|
|
718
1471
|
foreach(target, options) {
|
|
719
1472
|
const concurrency = options?.concurrency ?? 1;
|
|
720
1473
|
const onError = options?.onError;
|
|
721
1474
|
const isWorkflow = target instanceof SealedWorkflow;
|
|
722
|
-
const
|
|
1475
|
+
const defaultId = isWorkflow ? target.id ?? "foreach" : `foreach:${target.id}`;
|
|
1476
|
+
const id = options?.id ?? defaultId;
|
|
723
1477
|
const node = {
|
|
724
1478
|
type: "step",
|
|
725
1479
|
id,
|
|
1480
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1481
|
+
nestedWorkflow: isWorkflow ? target : void 0,
|
|
1482
|
+
category: "foreach",
|
|
726
1483
|
execute: async (state) => {
|
|
727
1484
|
const items = state.output;
|
|
728
1485
|
if (!Array.isArray(items)) {
|
|
@@ -731,14 +1488,58 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
731
1488
|
const ctx = state.ctx;
|
|
732
1489
|
const results = new Array(items.length);
|
|
733
1490
|
const skipped = /* @__PURE__ */ new Set();
|
|
1491
|
+
const itemStates = new Array(items.length);
|
|
734
1492
|
const executeItem = async (item, index) => {
|
|
735
|
-
const itemState = {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
+
}
|
|
740
1542
|
}
|
|
741
|
-
results[index] = itemState.output;
|
|
742
1543
|
};
|
|
743
1544
|
const handleRejection = async (error, item, index) => {
|
|
744
1545
|
if (!onError) throw error;
|
|
@@ -754,49 +1555,244 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
754
1555
|
results[index] = recovered;
|
|
755
1556
|
}
|
|
756
1557
|
};
|
|
1558
|
+
const failures = [];
|
|
1559
|
+
const signal = state.abortSignal;
|
|
757
1560
|
if (concurrency <= 1) {
|
|
758
1561
|
for (let i = 0; i < items.length; i++) {
|
|
1562
|
+
if (signal?.aborted) {
|
|
1563
|
+
failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
759
1566
|
try {
|
|
760
1567
|
await executeItem(items[i], i);
|
|
761
1568
|
} catch (error) {
|
|
762
|
-
|
|
1569
|
+
failures.push({ index: i, error });
|
|
763
1570
|
}
|
|
764
1571
|
}
|
|
765
1572
|
} else {
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
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) {
|
|
770
1703
|
try {
|
|
771
|
-
await
|
|
1704
|
+
await executeBranch(e);
|
|
772
1705
|
} catch (error) {
|
|
773
|
-
failures.push({ index:
|
|
774
|
-
} finally {
|
|
775
|
-
sem.release();
|
|
1706
|
+
failures.push({ key: e.key, index: e.index, error });
|
|
776
1707
|
}
|
|
777
|
-
}));
|
|
778
|
-
failures.sort((a, b) => a.index - b.index);
|
|
779
|
-
for (const { index, error } of failures) {
|
|
780
|
-
await handleRejection(error, items[index], index);
|
|
781
1708
|
}
|
|
1709
|
+
} else {
|
|
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);
|
|
782
1728
|
}
|
|
783
|
-
|
|
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;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
state.output = results;
|
|
784
1773
|
}
|
|
785
1774
|
};
|
|
786
|
-
return
|
|
1775
|
+
return this.appendStep(node);
|
|
787
1776
|
}
|
|
788
1777
|
// ── repeat: conditional loop ─────────────────────────────────
|
|
789
1778
|
repeat(target, options) {
|
|
790
1779
|
const maxIterations = options.maxIterations ?? 10;
|
|
791
1780
|
const isWorkflow = target instanceof SealedWorkflow;
|
|
792
|
-
const
|
|
1781
|
+
const defaultId = isWorkflow ? target.id ?? "repeat" : `repeat:${target.id}`;
|
|
1782
|
+
const id = options.id ?? defaultId;
|
|
793
1783
|
const predicate = options.until ?? (async (p) => !await options.while(p));
|
|
794
1784
|
const node = {
|
|
795
1785
|
type: "step",
|
|
796
1786
|
id,
|
|
1787
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1788
|
+
nestedWorkflow: isWorkflow ? target : void 0,
|
|
1789
|
+
category: "repeat",
|
|
797
1790
|
execute: async (state) => {
|
|
798
1791
|
const ctx = state.ctx;
|
|
799
1792
|
for (let i = 1; i <= maxIterations; i++) {
|
|
1793
|
+
if (state.abortSignal?.aborted) {
|
|
1794
|
+
throw state.abortSignal.reason ?? new Error("Workflow aborted");
|
|
1795
|
+
}
|
|
800
1796
|
if (isWorkflow) {
|
|
801
1797
|
await this.executeNestedWorkflow(state, target);
|
|
802
1798
|
} else {
|
|
@@ -812,7 +1808,7 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
812
1808
|
throw new WorkflowLoopError(maxIterations, maxIterations);
|
|
813
1809
|
}
|
|
814
1810
|
};
|
|
815
|
-
return
|
|
1811
|
+
return this.appendStep(node);
|
|
816
1812
|
}
|
|
817
1813
|
// ── catch ─────────────────────────────────────────────────────
|
|
818
1814
|
catch(id, fn) {
|
|
@@ -824,27 +1820,29 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
824
1820
|
id,
|
|
825
1821
|
catchFn: fn
|
|
826
1822
|
};
|
|
827
|
-
return
|
|
828
|
-
}
|
|
829
|
-
// ── finally (terminal — returns sealed workflow) ──────────────
|
|
830
|
-
finally(id, fn) {
|
|
831
|
-
const node = {
|
|
832
|
-
type: "finally",
|
|
833
|
-
id,
|
|
834
|
-
execute: async (state) => {
|
|
835
|
-
await fn({ ctx: state.ctx });
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
return new SealedWorkflow([...this.steps, node], this.id);
|
|
1823
|
+
return this.appendStep(node);
|
|
839
1824
|
}
|
|
1825
|
+
// `.finally()` is inherited from SealedWorkflow now (it lives there so
|
|
1826
|
+
// multi-finally chains are possible — `.finally().finally()`).
|
|
840
1827
|
};
|
|
1828
|
+
|
|
1829
|
+
// src/index.ts
|
|
1830
|
+
var SKIP = Workflow.SKIP;
|
|
841
1831
|
// Annotate the CommonJS export names for ESM import in node:
|
|
842
1832
|
0 && (module.exports = {
|
|
843
1833
|
Agent,
|
|
1834
|
+
CHECKPOINT_STEP_ID,
|
|
1835
|
+
CheckpointTimeoutError,
|
|
1836
|
+
NestedGateUnsupportedError,
|
|
1837
|
+
SKIP,
|
|
1838
|
+
TOOL_PROVIDER_BRAND,
|
|
1839
|
+
ToolProvider,
|
|
844
1840
|
Workflow,
|
|
845
1841
|
WorkflowBranchError,
|
|
846
1842
|
WorkflowLoopError,
|
|
847
|
-
|
|
848
|
-
|
|
1843
|
+
defineTool,
|
|
1844
|
+
getActiveWriter,
|
|
1845
|
+
isToolProvider,
|
|
1846
|
+
migrateSnapshot
|
|
849
1847
|
});
|
|
850
1848
|
//# sourceMappingURL=index.cjs.map
|