pipeai 0.3.0 → 0.8.1
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 +1216 -212
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +578 -62
- package/dist/index.d.ts +578 -62
- package/dist/index.js +1206 -210
- 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;
|
|
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;
|
|
308
429
|
}
|
|
309
430
|
};
|
|
310
|
-
|
|
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,38 +695,102 @@ 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;
|
|
355
710
|
}
|
|
356
711
|
},
|
|
357
712
|
...options?.onError ? { onError: options.onError } : {},
|
|
358
|
-
...options?.onFinish ? { onFinish: options.onFinish } : {}
|
|
713
|
+
...options?.onFinish ? { onFinish: options.onFinish } : {},
|
|
714
|
+
...options?.originalMessages ? { originalMessages: options.originalMessages } : {},
|
|
715
|
+
...options?.generateId ? { generateId: options.generateId } : {}
|
|
359
716
|
});
|
|
360
717
|
return {
|
|
361
718
|
stream,
|
|
362
719
|
output: outputPromise
|
|
363
720
|
};
|
|
364
721
|
}
|
|
722
|
+
// Helper — converts terminal RuntimeState into a WorkflowResult; freezes
|
|
723
|
+
// snapshot + warnings if requested via runOptions.
|
|
724
|
+
buildResult(state) {
|
|
725
|
+
const warnings = state.warnings ?? [];
|
|
726
|
+
if (state.suspension && resolveFreezeSnapshots(state)) {
|
|
727
|
+
deepFreeze(warnings);
|
|
728
|
+
}
|
|
729
|
+
if (state.suspension) {
|
|
730
|
+
return { status: "suspended", snapshot: state.suspension, warnings };
|
|
731
|
+
}
|
|
732
|
+
return { status: "complete", output: state.output, warnings };
|
|
733
|
+
}
|
|
365
734
|
// ── Internal: execute pipeline ────────────────────────────────
|
|
366
|
-
async execute(state, startIndex = 0) {
|
|
735
|
+
async execute(state, startIndex = 0, opts, initialError = null) {
|
|
367
736
|
if (this.steps.length === 0) {
|
|
368
737
|
throw new Error("Workflow has no steps. Add at least one step before calling generate() or stream().");
|
|
369
738
|
}
|
|
370
|
-
|
|
739
|
+
if (opts !== void 0 && state.runOptions === void 0) {
|
|
740
|
+
state.runOptions = opts;
|
|
741
|
+
}
|
|
742
|
+
const ckptCadence = opts?.onCheckpoint && opts.checkpointWhen === void 0 ? opts.checkpointEvery ?? Math.max(1, Math.ceil(this.cachedExecutableStepCount / 4)) : 0;
|
|
743
|
+
let pendingError = initialError;
|
|
744
|
+
let abortPromoted = false;
|
|
745
|
+
const makeAbortError = (signal) => ({
|
|
746
|
+
error: signal.reason ?? new Error("Workflow aborted"),
|
|
747
|
+
stepId: "abort",
|
|
748
|
+
source: "step"
|
|
749
|
+
});
|
|
371
750
|
for (let i = startIndex; i < this.steps.length; i++) {
|
|
751
|
+
if (state.abortSignal?.aborted) {
|
|
752
|
+
if (!abortPromoted) {
|
|
753
|
+
abortPromoted = true;
|
|
754
|
+
state.suspension = void 0;
|
|
755
|
+
if (pendingError) demotePendingError(state, pendingError);
|
|
756
|
+
pendingError = makeAbortError(state.abortSignal);
|
|
757
|
+
} else if (!pendingError) {
|
|
758
|
+
pendingError = makeAbortError(state.abortSignal);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
372
761
|
const node = this.steps[i];
|
|
373
762
|
if (node.type === "finally") {
|
|
374
|
-
|
|
763
|
+
const stepId2 = node.id;
|
|
764
|
+
const finStart = performance.now();
|
|
765
|
+
await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "finally", ctx: state.ctx, input: state.output });
|
|
766
|
+
try {
|
|
767
|
+
await node.execute(state);
|
|
768
|
+
await this.fireHook(state, "onStepFinish", {
|
|
769
|
+
stepId: stepId2,
|
|
770
|
+
type: "finally",
|
|
771
|
+
ctx: state.ctx,
|
|
772
|
+
output: state.output,
|
|
773
|
+
durationMs: performance.now() - finStart,
|
|
774
|
+
suspended: false
|
|
775
|
+
});
|
|
776
|
+
} catch (e) {
|
|
777
|
+
await this.fireHook(state, "onStepError", {
|
|
778
|
+
stepId: stepId2,
|
|
779
|
+
type: "finally",
|
|
780
|
+
ctx: state.ctx,
|
|
781
|
+
error: e,
|
|
782
|
+
durationMs: performance.now() - finStart
|
|
783
|
+
});
|
|
784
|
+
if (pendingError) demotePendingError(state, pendingError);
|
|
785
|
+
pendingError = { error: e, stepId: stepId2, source: "finally" };
|
|
786
|
+
}
|
|
375
787
|
continue;
|
|
376
788
|
}
|
|
377
789
|
if (node.type === "catch") {
|
|
378
|
-
if (!pendingError) continue;
|
|
790
|
+
if (state.suspension || !pendingError || state.checkpointFailed) continue;
|
|
791
|
+
const stepId2 = node.id;
|
|
792
|
+
const cStart = performance.now();
|
|
793
|
+
await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "catch", ctx: state.ctx, input: state.output });
|
|
379
794
|
try {
|
|
380
795
|
state.output = await node.catchFn({
|
|
381
796
|
error: pendingError.error,
|
|
@@ -384,49 +799,183 @@ var SealedWorkflow = class {
|
|
|
384
799
|
stepId: pendingError.stepId
|
|
385
800
|
});
|
|
386
801
|
pendingError = null;
|
|
387
|
-
|
|
388
|
-
|
|
802
|
+
await this.fireHook(state, "onStepFinish", {
|
|
803
|
+
stepId: stepId2,
|
|
804
|
+
type: "catch",
|
|
805
|
+
ctx: state.ctx,
|
|
806
|
+
output: state.output,
|
|
807
|
+
durationMs: performance.now() - cStart,
|
|
808
|
+
suspended: false
|
|
809
|
+
});
|
|
810
|
+
} catch (e) {
|
|
811
|
+
await this.fireHook(state, "onStepError", {
|
|
812
|
+
stepId: stepId2,
|
|
813
|
+
type: "catch",
|
|
814
|
+
ctx: state.ctx,
|
|
815
|
+
error: e,
|
|
816
|
+
durationMs: performance.now() - cStart
|
|
817
|
+
});
|
|
818
|
+
if (pendingError) demotePendingError(state, pendingError);
|
|
819
|
+
pendingError = { error: e, stepId: stepId2, source: "catch" };
|
|
389
820
|
}
|
|
390
821
|
continue;
|
|
391
822
|
}
|
|
823
|
+
if (state.suspension || pendingError) continue;
|
|
392
824
|
if (node.type === "gate") {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
825
|
+
const stepId2 = node.id;
|
|
826
|
+
const gStart = performance.now();
|
|
827
|
+
await this.fireHook(state, "onStepStart", { stepId: stepId2, type: "gate", ctx: state.ctx, input: state.output });
|
|
828
|
+
try {
|
|
829
|
+
if (node.condition && !await node.condition(state)) {
|
|
830
|
+
await this.fireHook(state, "onStepFinish", {
|
|
831
|
+
stepId: stepId2,
|
|
832
|
+
type: "gate",
|
|
833
|
+
ctx: state.ctx,
|
|
834
|
+
output: state.output,
|
|
835
|
+
durationMs: performance.now() - gStart,
|
|
836
|
+
suspended: false
|
|
837
|
+
});
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
const snapshot = {
|
|
841
|
+
version: 2,
|
|
842
|
+
kind: "gate",
|
|
843
|
+
resumeFromIndex: i,
|
|
844
|
+
output: state.output,
|
|
845
|
+
gateId: node.id,
|
|
846
|
+
gatePayload: await node.payload(state)
|
|
847
|
+
};
|
|
848
|
+
state.suspension = snapshot;
|
|
849
|
+
if (resolveFreezeSnapshots(state)) deepFreeze(snapshot);
|
|
850
|
+
await this.fireHook(state, "onStepFinish", {
|
|
851
|
+
stepId: stepId2,
|
|
852
|
+
type: "gate",
|
|
853
|
+
ctx: state.ctx,
|
|
854
|
+
output: state.output,
|
|
855
|
+
durationMs: performance.now() - gStart,
|
|
856
|
+
suspended: true
|
|
857
|
+
});
|
|
858
|
+
} catch (e) {
|
|
859
|
+
pendingError = { error: e, stepId: node.id, source: "step" };
|
|
860
|
+
}
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const obsType = getObservabilityType(node);
|
|
864
|
+
const stepId = node.id;
|
|
865
|
+
const sStart = performance.now();
|
|
866
|
+
const stepInput = state.output;
|
|
867
|
+
await this.fireHook(state, "onStepStart", { stepId, type: obsType, ctx: state.ctx, input: stepInput });
|
|
868
|
+
try {
|
|
869
|
+
await node.execute(state);
|
|
870
|
+
await this.fireHook(state, "onStepFinish", {
|
|
871
|
+
stepId,
|
|
872
|
+
type: obsType,
|
|
873
|
+
ctx: state.ctx,
|
|
402
874
|
output: state.output,
|
|
403
|
-
|
|
404
|
-
|
|
875
|
+
durationMs: performance.now() - sStart,
|
|
876
|
+
suspended: false
|
|
405
877
|
});
|
|
878
|
+
} catch (e) {
|
|
879
|
+
pendingError = { error: e, stepId: node.id, source: "step" };
|
|
880
|
+
const obsError = await this.fireHook(state, "onStepError", {
|
|
881
|
+
stepId,
|
|
882
|
+
type: obsType,
|
|
883
|
+
ctx: state.ctx,
|
|
884
|
+
error: e,
|
|
885
|
+
durationMs: performance.now() - sStart
|
|
886
|
+
});
|
|
887
|
+
if (obsError !== void 0 && typeof e === "object" && e !== null) {
|
|
888
|
+
try {
|
|
889
|
+
e.cause = obsError;
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const leaked = state.suspension;
|
|
895
|
+
if (leaked) {
|
|
896
|
+
state.suspension = void 0;
|
|
897
|
+
throw new Error(`internal: suspension bubbled from non-gate step "${node.id}" (gate "${leaked.gateId}").`);
|
|
898
|
+
}
|
|
899
|
+
if (!pendingError && !state.suspension && opts?.onCheckpoint) {
|
|
900
|
+
const shouldCheckpoint = opts.checkpointWhen ? opts.checkpointWhen({ stepIndex: i, stepId: node.id, ctx: state.ctx }) : (i + 1) % ckptCadence === 0;
|
|
901
|
+
if (shouldCheckpoint) {
|
|
902
|
+
const ckptStart = performance.now();
|
|
903
|
+
try {
|
|
904
|
+
await emitCheckpoint(
|
|
905
|
+
state,
|
|
906
|
+
opts,
|
|
907
|
+
i + 1,
|
|
908
|
+
this.cachedStepShapeHash
|
|
909
|
+
);
|
|
910
|
+
} catch (e) {
|
|
911
|
+
pendingError = { error: e, stepId: CHECKPOINT_STEP_ID, source: "onCheckpoint" };
|
|
912
|
+
state.checkpointFailed = true;
|
|
913
|
+
await this.fireHook(state, "onStepError", {
|
|
914
|
+
stepId: CHECKPOINT_STEP_ID,
|
|
915
|
+
type: "step",
|
|
916
|
+
ctx: state.ctx,
|
|
917
|
+
error: e,
|
|
918
|
+
durationMs: performance.now() - ckptStart
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (pendingError && !state.suspension) {
|
|
925
|
+
if (state.checkpointFailed) {
|
|
926
|
+
const warningsArr = state.warnings ?? [];
|
|
927
|
+
const checkpointError = pendingError.source === "onCheckpoint" ? pendingError.error : warningsArr.find((w) => w.source === "onCheckpoint")?.error;
|
|
928
|
+
const finallyErrors = warningsArr.filter((w) => w.source === "finally").map((w) => w.error);
|
|
929
|
+
const all = pendingError.source === "finally" ? [...finallyErrors, pendingError.error] : finallyErrors;
|
|
930
|
+
if (all.length > 0) {
|
|
931
|
+
console.warn(
|
|
932
|
+
`pipeai: ${all.length} .finally() error(s) suppressed by checkpoint-failure precedence:`,
|
|
933
|
+
all
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
throw checkpointError ?? pendingError.error;
|
|
406
937
|
}
|
|
407
|
-
|
|
938
|
+
const isFinallyPath = pendingError.source === "finally" || (state.warnings?.some((w) => w.source === "finally") ?? false);
|
|
939
|
+
if (isFinallyPath) {
|
|
940
|
+
const all = [...(state.warnings ?? []).map((w) => w.error), pendingError.error];
|
|
941
|
+
throw new AggregateError(all, `Workflow failed with ${all.length} error(s) from .finally() bodies`);
|
|
942
|
+
}
|
|
943
|
+
throw pendingError.error;
|
|
944
|
+
} else if (pendingError && state.suspension) {
|
|
945
|
+
demotePendingError(state, pendingError);
|
|
408
946
|
try {
|
|
409
|
-
await
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
947
|
+
await this.observability?.onStepError?.({
|
|
948
|
+
stepId: pendingError.stepId,
|
|
949
|
+
type: pendingErrorSourceToStepType(pendingError.source),
|
|
950
|
+
ctx: state.ctx,
|
|
951
|
+
error: pendingError.error,
|
|
952
|
+
durationMs: 0
|
|
953
|
+
});
|
|
954
|
+
} catch (obsError) {
|
|
955
|
+
pushWarning(state, "onStepError", pendingError.stepId, obsError);
|
|
413
956
|
}
|
|
957
|
+
pendingError = null;
|
|
414
958
|
}
|
|
415
|
-
if (pendingError) throw pendingError.error;
|
|
416
959
|
}
|
|
417
960
|
// ── Internal: execute a nested workflow within a step/loop ─────
|
|
418
961
|
// Defined on SealedWorkflow (not Workflow) because TypeScript's protected
|
|
419
962
|
// access rules only allow calling workflow.execute() from the same class.
|
|
963
|
+
//
|
|
964
|
+
// Contract: clears any inner suspension before re-throwing as
|
|
965
|
+
// NestedGateUnsupportedError. The outer execute() therefore never observes
|
|
966
|
+
// a leaked `state.suspension` from non-gate nodes (defensive invariant).
|
|
420
967
|
async executeNestedWorkflow(state, workflow) {
|
|
968
|
+
const savedRunOptions = state.runOptions;
|
|
969
|
+
state.runOptions = void 0;
|
|
421
970
|
try {
|
|
422
971
|
await workflow.execute(state);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
throw
|
|
972
|
+
} finally {
|
|
973
|
+
state.runOptions = savedRunOptions;
|
|
974
|
+
}
|
|
975
|
+
if (state.suspension) {
|
|
976
|
+
const gateId = state.suspension.gateId;
|
|
977
|
+
state.suspension = void 0;
|
|
978
|
+
throw new NestedGateUnsupportedError(gateId, workflow.id);
|
|
430
979
|
}
|
|
431
980
|
}
|
|
432
981
|
// ── Internal: execute an agent within a step/branch ───────────
|
|
@@ -436,59 +985,141 @@ var SealedWorkflow = class {
|
|
|
436
985
|
async executeAgent(state, agent, ctx, options) {
|
|
437
986
|
const input = state.output;
|
|
438
987
|
const hasStructuredOutput = agent.hasOutput;
|
|
988
|
+
const abortSignal = state.abortSignal;
|
|
989
|
+
const agentCallOpts = abortSignal ? { abortSignal } : void 0;
|
|
439
990
|
if (state.mode === "stream" && state.writer) {
|
|
440
991
|
const writer = state.writer;
|
|
441
992
|
await runWithWriter(writer, async () => {
|
|
442
|
-
const result = await agent.stream(ctx, state.output);
|
|
993
|
+
const result = await agent.stream(ctx, state.output, agentCallOpts);
|
|
443
994
|
if (options?.handleStream) {
|
|
444
|
-
await options.handleStream({ result, writer, ctx });
|
|
995
|
+
await options.handleStream({ result, writer, ctx, input });
|
|
445
996
|
} else {
|
|
446
997
|
writer.merge(result.toUIMessageStream());
|
|
447
998
|
}
|
|
448
|
-
|
|
449
|
-
|
|
999
|
+
const hookParams = {
|
|
1000
|
+
mode: "stream",
|
|
1001
|
+
result,
|
|
1002
|
+
ctx,
|
|
1003
|
+
input
|
|
1004
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1005
|
+
};
|
|
1006
|
+
if (options?.onResult) {
|
|
1007
|
+
await options.onResult(hookParams);
|
|
450
1008
|
}
|
|
451
|
-
if (options?.
|
|
452
|
-
state.output = await options.
|
|
1009
|
+
if (options?.mapResult) {
|
|
1010
|
+
state.output = await options.mapResult(hookParams);
|
|
453
1011
|
} else {
|
|
454
|
-
state.output = await extractOutput(result, hasStructuredOutput);
|
|
1012
|
+
state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
|
|
455
1013
|
}
|
|
456
1014
|
});
|
|
457
1015
|
} else {
|
|
458
|
-
const result = await agent.generate(ctx, state.output);
|
|
459
|
-
|
|
460
|
-
|
|
1016
|
+
const result = await agent.generate(ctx, state.output, agentCallOpts);
|
|
1017
|
+
const hookParams = {
|
|
1018
|
+
mode: "generate",
|
|
1019
|
+
result,
|
|
1020
|
+
ctx,
|
|
1021
|
+
input
|
|
1022
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1023
|
+
};
|
|
1024
|
+
if (options?.onResult) {
|
|
1025
|
+
await options.onResult(hookParams);
|
|
461
1026
|
}
|
|
462
|
-
if (options?.
|
|
463
|
-
state.output = await options.
|
|
1027
|
+
if (options?.mapResult) {
|
|
1028
|
+
state.output = await options.mapResult(hookParams);
|
|
464
1029
|
} else {
|
|
465
|
-
state.output = await extractOutput(result, hasStructuredOutput);
|
|
1030
|
+
state.output = await extractOutput(result, hasStructuredOutput, agent.validateOutput);
|
|
466
1031
|
}
|
|
467
1032
|
}
|
|
468
1033
|
}
|
|
469
1034
|
// ── Gate: load persisted state for resumption ──────────────────
|
|
470
1035
|
loadState(gateId, snapshot) {
|
|
471
|
-
if (snapshot.
|
|
1036
|
+
if (snapshot.version === 2 && snapshot.kind === "checkpoint") {
|
|
1037
|
+
throw new Error(`loadState: received a checkpoint snapshot. Use resumeFrom() for checkpoint resume; loadState() is for gates.`);
|
|
1038
|
+
}
|
|
1039
|
+
const gateLike = snapshot;
|
|
1040
|
+
if (gateLike.gateId !== gateId) {
|
|
472
1041
|
throw new Error(
|
|
473
|
-
`loadState: gate ID mismatch \u2014 expected "${gateId}" but snapshot has "${
|
|
1042
|
+
`loadState: gate ID mismatch \u2014 expected "${gateId}" but snapshot has "${gateLike.gateId}".`
|
|
474
1043
|
);
|
|
475
1044
|
}
|
|
476
|
-
|
|
1045
|
+
this.ensureDuplicateCheck();
|
|
1046
|
+
const gateIndex = this.findGateIndex(gateLike);
|
|
477
1047
|
const gateNode = this.steps[gateIndex];
|
|
478
|
-
return new ResumedWorkflow(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
gateNode.
|
|
482
|
-
|
|
483
|
-
snapshot
|
|
484
|
-
|
|
1048
|
+
return new ResumedWorkflow(this.steps, gateIndex + 1, {
|
|
1049
|
+
mode: "gate",
|
|
1050
|
+
schema: gateNode.schema,
|
|
1051
|
+
mergeFn: gateNode.merge,
|
|
1052
|
+
priorOutput: gateLike.output,
|
|
1053
|
+
snapshot: gateLike,
|
|
1054
|
+
observability: this.observability
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
// ── Checkpoint resume ──────────────────────────────────────────
|
|
1058
|
+
/**
|
|
1059
|
+
* Resume from a checkpoint snapshot. Validates the step-shape hash unless
|
|
1060
|
+
* `{ skipShapeCheck: true }` is passed. Throws on:
|
|
1061
|
+
* - gate snapshots (use `loadState` instead)
|
|
1062
|
+
* - missing/corrupted `stepShapeHash`
|
|
1063
|
+
* - shape mismatch (unless skipped)
|
|
1064
|
+
* - out-of-bounds `resumeFromIndex`
|
|
1065
|
+
* - 0-step workflow (structural invariant)
|
|
1066
|
+
*
|
|
1067
|
+
* Returns a `CheckpointResumedWorkflow` whose `generate(ctx, opts?)` takes
|
|
1068
|
+
* NO response arg — the state is seeded from the snapshot's output. The
|
|
1069
|
+
* matching gate-resume path (`loadState`) keeps the `response` arg.
|
|
1070
|
+
*/
|
|
1071
|
+
resumeFrom(snapshot, options) {
|
|
1072
|
+
const isGate = snapshot.version === 2 && snapshot.kind === "gate" || snapshot.version === 1 && snapshot.gateId !== void 0;
|
|
1073
|
+
if (isGate) {
|
|
1074
|
+
throw new Error(`resumeFrom: received a gate snapshot. Use loadState() for gate resume; resumeFrom() is for checkpoints.`);
|
|
1075
|
+
}
|
|
1076
|
+
if (this.steps.length === 0) {
|
|
1077
|
+
throw new Error("resumeFrom: workflow has no steps; snapshot is structurally invalid.");
|
|
1078
|
+
}
|
|
1079
|
+
const ckpt = snapshot;
|
|
1080
|
+
const idx = ckpt.resumeFromIndex;
|
|
1081
|
+
if (!Number.isInteger(idx) || idx < 0 || idx > this.steps.length) {
|
|
1082
|
+
throw new Error(`resumeFrom: resumeFromIndex (${idx}) out of bounds for ${this.steps.length}-step workflow.`);
|
|
1083
|
+
}
|
|
1084
|
+
if (!options?.skipShapeCheck) {
|
|
1085
|
+
if (!ckpt.stepShapeHash) {
|
|
1086
|
+
throw new Error("resumeFrom: snapshot missing stepShapeHash; corrupted or hand-crafted.");
|
|
1087
|
+
}
|
|
1088
|
+
this.ensureDuplicateCheck();
|
|
1089
|
+
if (this.cachedStepShapeHash !== ckpt.stepShapeHash) {
|
|
1090
|
+
throw new Error("resumeFrom: workflow shape mismatch; cannot safely resume. Pass { skipShapeCheck: true } to override.");
|
|
1091
|
+
}
|
|
1092
|
+
} else {
|
|
1093
|
+
this.ensureDuplicateCheck();
|
|
1094
|
+
}
|
|
1095
|
+
return new CheckpointResumedWorkflow(this.steps, idx, {
|
|
1096
|
+
mode: "checkpoint",
|
|
1097
|
+
priorOutput: ckpt.output,
|
|
1098
|
+
snapshot: ckpt,
|
|
1099
|
+
observability: this.observability
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Append a `.finally()` body to a sealed workflow, returning another sealed
|
|
1104
|
+
* workflow. Allows multi-finally chains (`.finally().finally()`). A throwing
|
|
1105
|
+
* `.finally` body does NOT abort subsequent ones — they all run.
|
|
1106
|
+
*/
|
|
1107
|
+
finally(id, fn) {
|
|
1108
|
+
const node = {
|
|
1109
|
+
type: "finally",
|
|
1110
|
+
id,
|
|
1111
|
+
execute: async (state) => {
|
|
1112
|
+
await fn({ ctx: state.ctx });
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
return new _SealedWorkflow([...this.steps, node], this.id, this.observability);
|
|
485
1116
|
}
|
|
486
1117
|
findGateIndex(snapshot) {
|
|
487
|
-
if (snapshot.version !== 1) {
|
|
1118
|
+
if (snapshot.version !== 1 && snapshot.version !== 2) {
|
|
488
1119
|
throw new Error(`Unsupported snapshot version: ${snapshot.version}`);
|
|
489
1120
|
}
|
|
490
1121
|
const hint = snapshot.resumeFromIndex;
|
|
491
|
-
if (hint >= 0 && hint < this.steps.length) {
|
|
1122
|
+
if (Number.isInteger(hint) && hint >= 0 && hint < this.steps.length) {
|
|
492
1123
|
const node = this.steps[hint];
|
|
493
1124
|
if (node.type === "gate" && node.id === snapshot.gateId) {
|
|
494
1125
|
return hint;
|
|
@@ -511,12 +1142,12 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
511
1142
|
mergeFn;
|
|
512
1143
|
priorOutput;
|
|
513
1144
|
/** @internal */
|
|
514
|
-
constructor(steps, startIndex,
|
|
515
|
-
super(steps);
|
|
1145
|
+
constructor(steps, startIndex, config) {
|
|
1146
|
+
super(steps, void 0, config.observability);
|
|
516
1147
|
this.startIndex = startIndex;
|
|
517
|
-
this.schema = schema;
|
|
518
|
-
this.mergeFn = mergeFn;
|
|
519
|
-
this.priorOutput = priorOutput;
|
|
1148
|
+
this.schema = config.schema;
|
|
1149
|
+
this.mergeFn = config.mergeFn;
|
|
1150
|
+
this.priorOutput = config.priorOutput;
|
|
520
1151
|
}
|
|
521
1152
|
validateResponse(response) {
|
|
522
1153
|
if (this.schema) {
|
|
@@ -525,15 +1156,31 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
525
1156
|
return response;
|
|
526
1157
|
}
|
|
527
1158
|
async generate(ctx, ...args) {
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1159
|
+
const rawResponse = args[0];
|
|
1160
|
+
const opts = args[1];
|
|
1161
|
+
let output = this.priorOutput;
|
|
1162
|
+
let initialError = null;
|
|
1163
|
+
try {
|
|
1164
|
+
const response = this.validateResponse(rawResponse);
|
|
1165
|
+
output = this.mergeFn ? await this.mergeFn({ priorOutput: this.priorOutput, response }) : response;
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
initialError = { error, stepId: "gate:resume", source: "step" };
|
|
1168
|
+
}
|
|
1169
|
+
const state = {
|
|
1170
|
+
ctx,
|
|
1171
|
+
output,
|
|
1172
|
+
mode: "generate",
|
|
1173
|
+
runOptions: opts,
|
|
1174
|
+
abortSignal: opts?.abortSignal
|
|
1175
|
+
};
|
|
1176
|
+
await this.execute(state, this.startIndex, opts, initialError);
|
|
1177
|
+
return this.buildResult(state);
|
|
533
1178
|
}
|
|
534
1179
|
stream(ctx, ...args) {
|
|
535
|
-
const
|
|
1180
|
+
const rawResponse = args[0];
|
|
536
1181
|
const options = args[1];
|
|
1182
|
+
const opts = args[2];
|
|
1183
|
+
const abortSignal = opts?.abortSignal;
|
|
537
1184
|
let resolveOutput;
|
|
538
1185
|
let rejectOutput;
|
|
539
1186
|
const outputPromise = new Promise((res, rej) => {
|
|
@@ -544,25 +1191,103 @@ var ResumedWorkflow = class extends SealedWorkflow {
|
|
|
544
1191
|
});
|
|
545
1192
|
const mergeFn = this.mergeFn;
|
|
546
1193
|
const priorOutput = this.priorOutput;
|
|
1194
|
+
const startIndex = this.startIndex;
|
|
547
1195
|
const stream = (0, import_ai3.createUIMessageStream)({
|
|
548
1196
|
execute: async ({ writer }) => {
|
|
549
|
-
|
|
1197
|
+
let output = priorOutput;
|
|
1198
|
+
let initialError = null;
|
|
1199
|
+
try {
|
|
1200
|
+
const response = this.validateResponse(rawResponse);
|
|
1201
|
+
output = mergeFn ? await mergeFn({ priorOutput, response }) : response;
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
initialError = { error, stepId: "gate:resume", source: "step" };
|
|
1204
|
+
}
|
|
550
1205
|
const state = {
|
|
551
1206
|
ctx,
|
|
552
1207
|
output,
|
|
553
1208
|
mode: "stream",
|
|
554
|
-
writer
|
|
1209
|
+
writer,
|
|
1210
|
+
runOptions: opts,
|
|
1211
|
+
abortSignal
|
|
1212
|
+
};
|
|
1213
|
+
try {
|
|
1214
|
+
await this.execute(state, startIndex, opts, initialError);
|
|
1215
|
+
const result = this.buildResult(state);
|
|
1216
|
+
maybeWarnStreamOnErrorOnSuspend(result, options);
|
|
1217
|
+
resolveOutput(result);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
rejectOutput(error);
|
|
1220
|
+
throw error;
|
|
1221
|
+
}
|
|
1222
|
+
},
|
|
1223
|
+
...options?.onError ? { onError: options.onError } : {},
|
|
1224
|
+
...options?.onFinish ? { onFinish: options.onFinish } : {},
|
|
1225
|
+
...options?.originalMessages ? { originalMessages: options.originalMessages } : {},
|
|
1226
|
+
...options?.generateId ? { generateId: options.generateId } : {}
|
|
1227
|
+
});
|
|
1228
|
+
return { stream, output: outputPromise };
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
var CheckpointResumedWorkflow = class extends SealedWorkflow {
|
|
1232
|
+
startIndex;
|
|
1233
|
+
priorOutput;
|
|
1234
|
+
/** @internal */
|
|
1235
|
+
constructor(steps, startIndex, config) {
|
|
1236
|
+
super(steps, void 0, config.observability);
|
|
1237
|
+
this.startIndex = startIndex;
|
|
1238
|
+
this.priorOutput = config.priorOutput;
|
|
1239
|
+
}
|
|
1240
|
+
// Override with widened arg list compatible with parent's `[input?, opts?]`.
|
|
1241
|
+
// Inputs are ignored — state is seeded from the snapshot's `output` field.
|
|
1242
|
+
async generate(ctx, ...args) {
|
|
1243
|
+
const opts = args[1];
|
|
1244
|
+
this.validateRunOptions(opts);
|
|
1245
|
+
const state = {
|
|
1246
|
+
ctx,
|
|
1247
|
+
output: this.priorOutput,
|
|
1248
|
+
mode: "generate",
|
|
1249
|
+
runOptions: opts
|
|
1250
|
+
};
|
|
1251
|
+
await this.execute(state, this.startIndex, opts);
|
|
1252
|
+
return this.buildResult(state);
|
|
1253
|
+
}
|
|
1254
|
+
stream(ctx, ...args) {
|
|
1255
|
+
const options = args[1];
|
|
1256
|
+
const opts = args[2];
|
|
1257
|
+
this.validateRunOptions(opts);
|
|
1258
|
+
let resolveOutput;
|
|
1259
|
+
let rejectOutput;
|
|
1260
|
+
const outputPromise = new Promise((res, rej) => {
|
|
1261
|
+
resolveOutput = res;
|
|
1262
|
+
rejectOutput = rej;
|
|
1263
|
+
});
|
|
1264
|
+
outputPromise.catch(() => {
|
|
1265
|
+
});
|
|
1266
|
+
const priorOutput = this.priorOutput;
|
|
1267
|
+
const startIndex = this.startIndex;
|
|
1268
|
+
const stream = (0, import_ai3.createUIMessageStream)({
|
|
1269
|
+
execute: async ({ writer }) => {
|
|
1270
|
+
const state = {
|
|
1271
|
+
ctx,
|
|
1272
|
+
output: priorOutput,
|
|
1273
|
+
mode: "stream",
|
|
1274
|
+
writer,
|
|
1275
|
+
runOptions: opts
|
|
555
1276
|
};
|
|
556
1277
|
try {
|
|
557
|
-
await this.execute(state,
|
|
558
|
-
|
|
1278
|
+
await this.execute(state, startIndex, opts);
|
|
1279
|
+
const result = this.buildResult(state);
|
|
1280
|
+
maybeWarnStreamOnErrorOnSuspend(result, options);
|
|
1281
|
+
resolveOutput(result);
|
|
559
1282
|
} catch (error) {
|
|
560
1283
|
rejectOutput(error);
|
|
561
1284
|
throw error;
|
|
562
1285
|
}
|
|
563
1286
|
},
|
|
564
1287
|
...options?.onError ? { onError: options.onError } : {},
|
|
565
|
-
...options?.onFinish ? { onFinish: options.onFinish } : {}
|
|
1288
|
+
...options?.onFinish ? { onFinish: options.onFinish } : {},
|
|
1289
|
+
...options?.originalMessages ? { originalMessages: options.originalMessages } : {},
|
|
1290
|
+
...options?.generateId ? { generateId: options.generateId } : {}
|
|
566
1291
|
});
|
|
567
1292
|
return { stream, output: outputPromise };
|
|
568
1293
|
}
|
|
@@ -574,15 +1299,21 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
574
1299
|
* shortening it relative to the input array.
|
|
575
1300
|
*/
|
|
576
1301
|
static SKIP = /* @__PURE__ */ Symbol("pipeai.foreach.skip");
|
|
577
|
-
constructor(steps = [], id) {
|
|
578
|
-
super(steps, id);
|
|
1302
|
+
constructor(steps = [], id, observability) {
|
|
1303
|
+
super(steps, id, observability);
|
|
579
1304
|
}
|
|
580
1305
|
static create(options) {
|
|
581
|
-
return new _Workflow([], options?.id);
|
|
1306
|
+
return new _Workflow([], options?.id, options?.observability);
|
|
582
1307
|
}
|
|
583
1308
|
static from(agent, options) {
|
|
584
1309
|
return new _Workflow([]).step(agent, options);
|
|
585
1310
|
}
|
|
1311
|
+
// Builder helper — append a step and return a re-typed Workflow.
|
|
1312
|
+
// Centralizes the `[...steps, node] as any` + new Workflow + observability/id
|
|
1313
|
+
// forwarding pattern used by every combinator method.
|
|
1314
|
+
appendStep(node) {
|
|
1315
|
+
return new _Workflow([...this.steps, node], this.id, this.observability);
|
|
1316
|
+
}
|
|
586
1317
|
// ── step: implementation ──────────────────────────────────────
|
|
587
1318
|
step(target, optionsOrFn) {
|
|
588
1319
|
if (target instanceof SealedWorkflow) {
|
|
@@ -590,11 +1321,15 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
590
1321
|
const node2 = {
|
|
591
1322
|
type: "step",
|
|
592
1323
|
id: workflow.id ?? "nested-workflow",
|
|
1324
|
+
nestedWorkflow: workflow,
|
|
1325
|
+
// Feeds the recursive stepShapeHash walk.
|
|
1326
|
+
category: "nested",
|
|
1327
|
+
// Observability event type.
|
|
593
1328
|
execute: async (state) => {
|
|
594
1329
|
await this.executeNestedWorkflow(state, workflow);
|
|
595
1330
|
}
|
|
596
1331
|
};
|
|
597
|
-
return
|
|
1332
|
+
return this.appendStep(node2);
|
|
598
1333
|
}
|
|
599
1334
|
if (typeof target === "string") {
|
|
600
1335
|
if (typeof optionsOrFn !== "function") {
|
|
@@ -611,19 +1346,19 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
611
1346
|
});
|
|
612
1347
|
}
|
|
613
1348
|
};
|
|
614
|
-
return
|
|
1349
|
+
return this.appendStep(node2);
|
|
615
1350
|
}
|
|
616
1351
|
const agent = target;
|
|
617
1352
|
const options = optionsOrFn;
|
|
618
1353
|
const node = {
|
|
619
1354
|
type: "step",
|
|
620
|
-
id: agent.id,
|
|
1355
|
+
id: options?.id ?? agent.id,
|
|
621
1356
|
execute: async (state) => {
|
|
622
1357
|
const ctx = state.ctx;
|
|
623
1358
|
await this.executeAgent(state, agent, ctx, options);
|
|
624
1359
|
}
|
|
625
1360
|
};
|
|
626
|
-
return
|
|
1361
|
+
return this.appendStep(node);
|
|
627
1362
|
}
|
|
628
1363
|
// ── gate: human-in-the-loop suspension point ────────────────
|
|
629
1364
|
gate(id, options) {
|
|
@@ -649,19 +1384,20 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
649
1384
|
return state.output;
|
|
650
1385
|
}
|
|
651
1386
|
};
|
|
652
|
-
return
|
|
1387
|
+
return this.appendStep(node);
|
|
653
1388
|
}
|
|
654
1389
|
// ── branch: implementation ────────────────────────────────────
|
|
655
|
-
branch(casesOrConfig) {
|
|
1390
|
+
branch(casesOrConfig, options) {
|
|
656
1391
|
if (Array.isArray(casesOrConfig)) {
|
|
657
|
-
return this.branchPredicate(casesOrConfig);
|
|
1392
|
+
return this.branchPredicate(casesOrConfig, options?.id);
|
|
658
1393
|
}
|
|
659
|
-
return this.branchSelect(casesOrConfig);
|
|
1394
|
+
return this.branchSelect(casesOrConfig, options?.id);
|
|
660
1395
|
}
|
|
661
|
-
branchPredicate(cases) {
|
|
1396
|
+
branchPredicate(cases, explicitId) {
|
|
662
1397
|
const node = {
|
|
663
1398
|
type: "step",
|
|
664
|
-
id: "branch:predicate",
|
|
1399
|
+
id: explicitId ?? "branch:predicate",
|
|
1400
|
+
category: "branch",
|
|
665
1401
|
execute: async (state) => {
|
|
666
1402
|
const ctx = state.ctx;
|
|
667
1403
|
const input = state.output;
|
|
@@ -673,21 +1409,43 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
673
1409
|
await this.executeAgent(state, branchCase.agent, ctx, branchCase);
|
|
674
1410
|
return;
|
|
675
1411
|
}
|
|
676
|
-
|
|
1412
|
+
let inputRepr;
|
|
1413
|
+
try {
|
|
1414
|
+
inputRepr = JSON.stringify(input);
|
|
1415
|
+
if (inputRepr === void 0) inputRepr = String(input);
|
|
1416
|
+
} catch {
|
|
1417
|
+
inputRepr = `[unserializable ${typeof input}]`;
|
|
1418
|
+
}
|
|
1419
|
+
throw new WorkflowBranchError("predicate", `No branch matched and no default branch (a case without \`when\`) was provided. Input: ${inputRepr}`);
|
|
677
1420
|
}
|
|
678
1421
|
};
|
|
679
|
-
return
|
|
1422
|
+
return this.appendStep(node);
|
|
680
1423
|
}
|
|
681
|
-
branchSelect(config) {
|
|
1424
|
+
branchSelect(config, explicitId) {
|
|
682
1425
|
const node = {
|
|
683
1426
|
type: "step",
|
|
684
|
-
id: "branch:select",
|
|
1427
|
+
id: explicitId ?? "branch:select",
|
|
1428
|
+
category: "branch",
|
|
685
1429
|
execute: async (state) => {
|
|
686
1430
|
const ctx = state.ctx;
|
|
687
1431
|
const input = state.output;
|
|
688
1432
|
const key = await config.select({ ctx, input });
|
|
689
|
-
|
|
1433
|
+
const keyDeclared = Object.prototype.hasOwnProperty.call(config.agents, key);
|
|
1434
|
+
if (keyDeclared && config.agents[key] === void 0) {
|
|
1435
|
+
throw new WorkflowBranchError(
|
|
1436
|
+
"select",
|
|
1437
|
+
`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(", ")}`
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
let agent = keyDeclared ? config.agents[key] : void 0;
|
|
690
1441
|
if (!agent) {
|
|
1442
|
+
if (config.onUnknownKey) {
|
|
1443
|
+
config.onUnknownKey({
|
|
1444
|
+
key,
|
|
1445
|
+
availableKeys: Object.keys(config.agents),
|
|
1446
|
+
ctx
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
691
1449
|
if (config.fallback) {
|
|
692
1450
|
agent = config.fallback;
|
|
693
1451
|
} else {
|
|
@@ -697,32 +1455,37 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
697
1455
|
await this.executeAgent(state, agent, ctx, config);
|
|
698
1456
|
}
|
|
699
1457
|
};
|
|
700
|
-
return
|
|
1458
|
+
return this.appendStep(node);
|
|
701
1459
|
}
|
|
702
1460
|
// ── foreach: array iteration ─────────────────────────────────
|
|
703
1461
|
/**
|
|
704
1462
|
* Map each item of an array through an agent or sub-workflow.
|
|
705
1463
|
*
|
|
706
1464
|
* @param target Agent or `SealedWorkflow` invoked once per item.
|
|
1465
|
+
* @param options.id Override the default step id (`foreach:<agentId>` or
|
|
1466
|
+
* the workflow's id). Required when chaining multiple foreach over the same
|
|
1467
|
+
* target — the construction-time `(type, id)` walk rejects duplicates.
|
|
707
1468
|
* @param options.concurrency Max items in flight at any moment (default 1).
|
|
708
1469
|
* Backed by a semaphore: as soon as one item completes, the next launches —
|
|
709
1470
|
* 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.
|
|
1471
|
+
* @param options.onError Per-iteration error handler. **Bypassed entirely on
|
|
1472
|
+
* the suspension path** (when any item hits a nested gate) — see the
|
|
1473
|
+
* foreach concurrency hazards in the README. Otherwise: return a
|
|
1474
|
+
* `TNextOutput` value to substitute, return `Workflow.SKIP` to omit, throw
|
|
1475
|
+
* to abort. Invoked sequentially in index order after all items settle.
|
|
717
1476
|
*/
|
|
718
1477
|
foreach(target, options) {
|
|
719
1478
|
const concurrency = options?.concurrency ?? 1;
|
|
720
1479
|
const onError = options?.onError;
|
|
721
1480
|
const isWorkflow = target instanceof SealedWorkflow;
|
|
722
|
-
const
|
|
1481
|
+
const defaultId = isWorkflow ? target.id ?? "foreach" : `foreach:${target.id}`;
|
|
1482
|
+
const id = options?.id ?? defaultId;
|
|
723
1483
|
const node = {
|
|
724
1484
|
type: "step",
|
|
725
1485
|
id,
|
|
1486
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1487
|
+
nestedWorkflow: isWorkflow ? target : void 0,
|
|
1488
|
+
category: "foreach",
|
|
726
1489
|
execute: async (state) => {
|
|
727
1490
|
const items = state.output;
|
|
728
1491
|
if (!Array.isArray(items)) {
|
|
@@ -731,14 +1494,58 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
731
1494
|
const ctx = state.ctx;
|
|
732
1495
|
const results = new Array(items.length);
|
|
733
1496
|
const skipped = /* @__PURE__ */ new Set();
|
|
1497
|
+
const itemStates = new Array(items.length);
|
|
734
1498
|
const executeItem = async (item, index) => {
|
|
735
|
-
const itemState = {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1499
|
+
const itemState = {
|
|
1500
|
+
ctx: state.ctx,
|
|
1501
|
+
output: item,
|
|
1502
|
+
mode: "generate",
|
|
1503
|
+
abortSignal: state.abortSignal
|
|
1504
|
+
};
|
|
1505
|
+
itemStates[index] = itemState;
|
|
1506
|
+
const itemStart = performance.now();
|
|
1507
|
+
await this.fireHook(state, "onItemStart", {
|
|
1508
|
+
stepId: id,
|
|
1509
|
+
type: "foreach",
|
|
1510
|
+
itemIndex: index,
|
|
1511
|
+
ctx: state.ctx,
|
|
1512
|
+
input: item
|
|
1513
|
+
});
|
|
1514
|
+
try {
|
|
1515
|
+
if (isWorkflow) {
|
|
1516
|
+
await this.executeNestedWorkflow(itemState, target);
|
|
1517
|
+
} else {
|
|
1518
|
+
await this.executeAgent(itemState, target, ctx);
|
|
1519
|
+
}
|
|
1520
|
+
results[index] = itemState.output;
|
|
1521
|
+
await this.fireHook(state, "onItemFinish", {
|
|
1522
|
+
stepId: id,
|
|
1523
|
+
type: "foreach",
|
|
1524
|
+
itemIndex: index,
|
|
1525
|
+
ctx: state.ctx,
|
|
1526
|
+
output: itemState.output,
|
|
1527
|
+
durationMs: performance.now() - itemStart
|
|
1528
|
+
});
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
await this.fireHook(state, "onItemError", {
|
|
1531
|
+
stepId: id,
|
|
1532
|
+
type: "foreach",
|
|
1533
|
+
itemIndex: index,
|
|
1534
|
+
ctx: state.ctx,
|
|
1535
|
+
error,
|
|
1536
|
+
durationMs: performance.now() - itemStart
|
|
1537
|
+
});
|
|
1538
|
+
throw error;
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
const mergeItemWarnings = () => {
|
|
1542
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
1543
|
+
const its = itemStates[idx];
|
|
1544
|
+
if (!its?.warnings) continue;
|
|
1545
|
+
for (const w of its.warnings) {
|
|
1546
|
+
pushWarning(state, w.source, `${id}[${idx}]:${w.stepId}`, w.error);
|
|
1547
|
+
}
|
|
740
1548
|
}
|
|
741
|
-
results[index] = itemState.output;
|
|
742
1549
|
};
|
|
743
1550
|
const handleRejection = async (error, item, index) => {
|
|
744
1551
|
if (!onError) throw error;
|
|
@@ -754,49 +1561,244 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
754
1561
|
results[index] = recovered;
|
|
755
1562
|
}
|
|
756
1563
|
};
|
|
1564
|
+
const failures = [];
|
|
1565
|
+
const signal = state.abortSignal;
|
|
757
1566
|
if (concurrency <= 1) {
|
|
758
1567
|
for (let i = 0; i < items.length; i++) {
|
|
1568
|
+
if (signal?.aborted) {
|
|
1569
|
+
failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
759
1572
|
try {
|
|
760
1573
|
await executeItem(items[i], i);
|
|
761
1574
|
} catch (error) {
|
|
762
|
-
|
|
1575
|
+
failures.push({ index: i, error });
|
|
763
1576
|
}
|
|
764
1577
|
}
|
|
765
1578
|
} else {
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
1579
|
+
let nextIndex = 0;
|
|
1580
|
+
const worker = async () => {
|
|
1581
|
+
while (true) {
|
|
1582
|
+
const i = nextIndex++;
|
|
1583
|
+
if (i >= items.length) return;
|
|
1584
|
+
if (signal?.aborted) {
|
|
1585
|
+
failures.push({ index: i, error: signal.reason ?? new Error("Workflow aborted") });
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
try {
|
|
1589
|
+
await executeItem(items[i], i);
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
failures.push({ index: i, error });
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
const workers = Array.from(
|
|
1596
|
+
{ length: Math.min(concurrency, items.length) },
|
|
1597
|
+
() => worker()
|
|
1598
|
+
);
|
|
1599
|
+
await Promise.all(workers);
|
|
1600
|
+
}
|
|
1601
|
+
failures.sort((a, b) => a.index - b.index);
|
|
1602
|
+
const gateFailures = [];
|
|
1603
|
+
const nonGateFailures = [];
|
|
1604
|
+
for (const f of failures) {
|
|
1605
|
+
if (f.error instanceof NestedGateUnsupportedError) {
|
|
1606
|
+
gateFailures.push({ index: f.index, error: f.error });
|
|
1607
|
+
} else {
|
|
1608
|
+
nonGateFailures.push(f);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
mergeItemWarnings();
|
|
1612
|
+
if (gateFailures.length > 0) {
|
|
1613
|
+
for (const nr of nonGateFailures) {
|
|
1614
|
+
pushWarning(state, "foreach-sibling", `${id}[${nr.index}]`, nr.error);
|
|
1615
|
+
}
|
|
1616
|
+
const lowest = gateFailures[0];
|
|
1617
|
+
const otherSuspensions = gateFailures.slice(1).map((g) => ({
|
|
1618
|
+
index: g.index,
|
|
1619
|
+
gateId: g.error.gateId
|
|
1620
|
+
}));
|
|
1621
|
+
const siblingErrors = nonGateFailures.map((nr) => nr.error);
|
|
1622
|
+
throw new NestedGateUnsupportedError(
|
|
1623
|
+
lowest.error.gateId,
|
|
1624
|
+
lowest.error.workflowId,
|
|
1625
|
+
siblingErrors,
|
|
1626
|
+
otherSuspensions
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
for (const { index, error } of nonGateFailures) {
|
|
1630
|
+
await handleRejection(error, items[index], index);
|
|
1631
|
+
}
|
|
1632
|
+
state.output = skipped.size === 0 ? results : results.filter((_, i) => !skipped.has(i));
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
return this.appendStep(node);
|
|
1636
|
+
}
|
|
1637
|
+
// Implementation
|
|
1638
|
+
parallel(branches, options) {
|
|
1639
|
+
const isTuple = Array.isArray(branches);
|
|
1640
|
+
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 }));
|
|
1641
|
+
const branchCount = entries.length;
|
|
1642
|
+
const requestedConcurrency = options?.concurrency;
|
|
1643
|
+
let effectiveConcurrency;
|
|
1644
|
+
if (requestedConcurrency === void 0) {
|
|
1645
|
+
effectiveConcurrency = Math.min(branchCount, 5);
|
|
1646
|
+
} else {
|
|
1647
|
+
effectiveConcurrency = requestedConcurrency;
|
|
1648
|
+
}
|
|
1649
|
+
if (requestedConcurrency === void 0 && branchCount > 5) {
|
|
1650
|
+
warnOnce(
|
|
1651
|
+
"pipeai:parallel-cap",
|
|
1652
|
+
`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.`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
const onError = options?.onError;
|
|
1656
|
+
const id = options?.id ?? (isTuple ? "parallel:tuple" : "parallel:record");
|
|
1657
|
+
const node = {
|
|
1658
|
+
type: "step",
|
|
1659
|
+
id,
|
|
1660
|
+
category: "parallel",
|
|
1661
|
+
execute: async (state) => {
|
|
1662
|
+
const ctx = state.ctx;
|
|
1663
|
+
const input = state.output;
|
|
1664
|
+
const results = isTuple ? new Array(branchCount) : {};
|
|
1665
|
+
const branchStates = new Array(branchCount);
|
|
1666
|
+
const executeBranch = async ({ key, index, target }) => {
|
|
1667
|
+
const branchState = { ctx: state.ctx, output: input, mode: "generate" };
|
|
1668
|
+
branchStates[index] = branchState;
|
|
1669
|
+
const branchStart = performance.now();
|
|
1670
|
+
const itemIndex = isTuple ? index : key;
|
|
1671
|
+
await this.fireHook(state, "onItemStart", {
|
|
1672
|
+
stepId: id,
|
|
1673
|
+
type: "parallel",
|
|
1674
|
+
itemIndex,
|
|
1675
|
+
ctx: state.ctx,
|
|
1676
|
+
input
|
|
1677
|
+
});
|
|
1678
|
+
try {
|
|
1679
|
+
if (target instanceof SealedWorkflow) {
|
|
1680
|
+
await this.executeNestedWorkflow(branchState, target);
|
|
1681
|
+
} else {
|
|
1682
|
+
await this.executeAgent(branchState, target, ctx);
|
|
1683
|
+
}
|
|
1684
|
+
results[key] = branchState.output;
|
|
1685
|
+
await this.fireHook(state, "onItemFinish", {
|
|
1686
|
+
stepId: id,
|
|
1687
|
+
type: "parallel",
|
|
1688
|
+
itemIndex,
|
|
1689
|
+
ctx: state.ctx,
|
|
1690
|
+
output: branchState.output,
|
|
1691
|
+
durationMs: performance.now() - branchStart
|
|
1692
|
+
});
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
await this.fireHook(state, "onItemError", {
|
|
1695
|
+
stepId: id,
|
|
1696
|
+
type: "parallel",
|
|
1697
|
+
itemIndex,
|
|
1698
|
+
ctx: state.ctx,
|
|
1699
|
+
error,
|
|
1700
|
+
durationMs: performance.now() - branchStart
|
|
1701
|
+
});
|
|
1702
|
+
throw error;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
const failures = [];
|
|
1706
|
+
const eff = Number.isFinite(effectiveConcurrency) ? Math.max(1, effectiveConcurrency) : branchCount;
|
|
1707
|
+
if (eff <= 1) {
|
|
1708
|
+
for (const e of entries) {
|
|
770
1709
|
try {
|
|
771
|
-
await
|
|
1710
|
+
await executeBranch(e);
|
|
772
1711
|
} catch (error) {
|
|
773
|
-
failures.push({ index:
|
|
774
|
-
} finally {
|
|
775
|
-
sem.release();
|
|
1712
|
+
failures.push({ key: e.key, index: e.index, error });
|
|
776
1713
|
}
|
|
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
1714
|
}
|
|
1715
|
+
} else {
|
|
1716
|
+
let nextIndex = 0;
|
|
1717
|
+
const worker = async () => {
|
|
1718
|
+
while (true) {
|
|
1719
|
+
const i = nextIndex++;
|
|
1720
|
+
if (i >= branchCount) return;
|
|
1721
|
+
const e = entries[i];
|
|
1722
|
+
try {
|
|
1723
|
+
await executeBranch(e);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
failures.push({ key: e.key, index: e.index, error });
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
const workers = Array.from(
|
|
1730
|
+
{ length: Math.min(eff, branchCount) },
|
|
1731
|
+
() => worker()
|
|
1732
|
+
);
|
|
1733
|
+
await Promise.all(workers);
|
|
782
1734
|
}
|
|
783
|
-
|
|
1735
|
+
for (let idx = 0; idx < branchCount; idx++) {
|
|
1736
|
+
const bs = branchStates[idx];
|
|
1737
|
+
if (!bs?.warnings) continue;
|
|
1738
|
+
for (const w of bs.warnings) {
|
|
1739
|
+
pushWarning(state, w.source, `${id}[${entries[idx].key}]:${w.stepId}`, w.error);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const gateFailures = [];
|
|
1743
|
+
const nonGateFailures = [];
|
|
1744
|
+
for (const f of failures) {
|
|
1745
|
+
if (f.error instanceof NestedGateUnsupportedError) gateFailures.push({ key: f.key, index: f.index, error: f.error });
|
|
1746
|
+
else nonGateFailures.push(f);
|
|
1747
|
+
}
|
|
1748
|
+
gateFailures.sort((a, b) => a.index - b.index);
|
|
1749
|
+
nonGateFailures.sort((a, b) => a.index - b.index);
|
|
1750
|
+
if (gateFailures.length > 0) {
|
|
1751
|
+
for (const nr of nonGateFailures) {
|
|
1752
|
+
pushWarning(state, "foreach-sibling", `${id}[${nr.key}]`, nr.error);
|
|
1753
|
+
}
|
|
1754
|
+
const lowest = gateFailures[0];
|
|
1755
|
+
const otherSuspensions = gateFailures.slice(1).map((g) => ({ index: g.index, gateId: g.error.gateId }));
|
|
1756
|
+
const siblingErrors = nonGateFailures.map((nr) => nr.error);
|
|
1757
|
+
throw new NestedGateUnsupportedError(
|
|
1758
|
+
lowest.error.gateId,
|
|
1759
|
+
lowest.error.workflowId,
|
|
1760
|
+
siblingErrors,
|
|
1761
|
+
otherSuspensions
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
for (const { key, index, error } of nonGateFailures) {
|
|
1765
|
+
if (!onError) throw error;
|
|
1766
|
+
const recovered = await onError({
|
|
1767
|
+
error,
|
|
1768
|
+
key: isTuple ? void 0 : key,
|
|
1769
|
+
index: isTuple ? index : void 0,
|
|
1770
|
+
ctx: state.ctx
|
|
1771
|
+
});
|
|
1772
|
+
if (recovered === _Workflow.SKIP) {
|
|
1773
|
+
results[key] = void 0;
|
|
1774
|
+
} else {
|
|
1775
|
+
results[key] = recovered;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
state.output = results;
|
|
784
1779
|
}
|
|
785
1780
|
};
|
|
786
|
-
return
|
|
1781
|
+
return this.appendStep(node);
|
|
787
1782
|
}
|
|
788
1783
|
// ── repeat: conditional loop ─────────────────────────────────
|
|
789
1784
|
repeat(target, options) {
|
|
790
1785
|
const maxIterations = options.maxIterations ?? 10;
|
|
791
1786
|
const isWorkflow = target instanceof SealedWorkflow;
|
|
792
|
-
const
|
|
1787
|
+
const defaultId = isWorkflow ? target.id ?? "repeat" : `repeat:${target.id}`;
|
|
1788
|
+
const id = options.id ?? defaultId;
|
|
793
1789
|
const predicate = options.until ?? (async (p) => !await options.while(p));
|
|
794
1790
|
const node = {
|
|
795
1791
|
type: "step",
|
|
796
1792
|
id,
|
|
1793
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1794
|
+
nestedWorkflow: isWorkflow ? target : void 0,
|
|
1795
|
+
category: "repeat",
|
|
797
1796
|
execute: async (state) => {
|
|
798
1797
|
const ctx = state.ctx;
|
|
799
1798
|
for (let i = 1; i <= maxIterations; i++) {
|
|
1799
|
+
if (state.abortSignal?.aborted) {
|
|
1800
|
+
throw state.abortSignal.reason ?? new Error("Workflow aborted");
|
|
1801
|
+
}
|
|
800
1802
|
if (isWorkflow) {
|
|
801
1803
|
await this.executeNestedWorkflow(state, target);
|
|
802
1804
|
} else {
|
|
@@ -812,7 +1814,7 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
812
1814
|
throw new WorkflowLoopError(maxIterations, maxIterations);
|
|
813
1815
|
}
|
|
814
1816
|
};
|
|
815
|
-
return
|
|
1817
|
+
return this.appendStep(node);
|
|
816
1818
|
}
|
|
817
1819
|
// ── catch ─────────────────────────────────────────────────────
|
|
818
1820
|
catch(id, fn) {
|
|
@@ -824,27 +1826,29 @@ var Workflow = class _Workflow extends SealedWorkflow {
|
|
|
824
1826
|
id,
|
|
825
1827
|
catchFn: fn
|
|
826
1828
|
};
|
|
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);
|
|
1829
|
+
return this.appendStep(node);
|
|
839
1830
|
}
|
|
1831
|
+
// `.finally()` is inherited from SealedWorkflow now (it lives there so
|
|
1832
|
+
// multi-finally chains are possible — `.finally().finally()`).
|
|
840
1833
|
};
|
|
1834
|
+
|
|
1835
|
+
// src/index.ts
|
|
1836
|
+
var SKIP = Workflow.SKIP;
|
|
841
1837
|
// Annotate the CommonJS export names for ESM import in node:
|
|
842
1838
|
0 && (module.exports = {
|
|
843
1839
|
Agent,
|
|
1840
|
+
CHECKPOINT_STEP_ID,
|
|
1841
|
+
CheckpointTimeoutError,
|
|
1842
|
+
NestedGateUnsupportedError,
|
|
1843
|
+
SKIP,
|
|
1844
|
+
TOOL_PROVIDER_BRAND,
|
|
1845
|
+
ToolProvider,
|
|
844
1846
|
Workflow,
|
|
845
1847
|
WorkflowBranchError,
|
|
846
1848
|
WorkflowLoopError,
|
|
847
|
-
|
|
848
|
-
|
|
1849
|
+
defineTool,
|
|
1850
|
+
getActiveWriter,
|
|
1851
|
+
isToolProvider,
|
|
1852
|
+
migrateSnapshot
|
|
849
1853
|
});
|
|
850
1854
|
//# sourceMappingURL=index.cjs.map
|