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