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