jev-devtools 0.3.0 → 0.4.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 +11 -12
- package/lib/flow/index.d.ts +64 -2
- package/lib/flow/index.js +466 -25
- package/lib/flow/runtime.d.ts +5 -0
- package/lib/flow/runtime.js +81 -0
- package/lib/server/flows.d.ts +3 -0
- package/lib/server/flows.js +89 -17
- package/lib/server/index.d.ts +6 -1
- package/lib/server/index.js +15 -0
- package/lib/server/recorder.js +7 -2
- package/lib/src/FlowDetailsPanel.d.ts +8 -0
- package/lib/src/FlowDetailsPanel.js +79 -0
- package/lib/src/FlowOverview.d.ts +2 -1
- package/lib/src/FlowOverview.js +292 -80
- package/lib/src/JevDrawer.js +2 -4
- package/lib/src/condition-inputs.js +12 -3
- package/lib/src/flow-condition.js +3 -2
- package/lib/src/flow-protocol.d.ts +27 -2
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +371 -260
- package/lib/src/useDiagramViewport.d.ts +1 -1
- package/lib/src/useDiagramViewport.js +11 -6
- package/package.json +6 -3
- package/skills/setup-jev-devtools/SKILL.md +7 -7
- package/skills/setup-jev-devtools/references/flows.md +86 -9
- package/skills/setup-jev-devtools/references/nextjs.md +3 -1
- package/skills/setup-jev-devtools/references/react-node.md +3 -4
- package/skills/setup-jev-devtools/references/verify.md +1 -1
package/lib/flow/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { jsonSnapshot, runCallback } from "./runtime.js";
|
|
2
3
|
const owners = new WeakMap();
|
|
4
|
+
const builderIds = new WeakMap();
|
|
3
5
|
function freezeAst(ast, depth = 0) {
|
|
4
6
|
if (depth > 80)
|
|
5
7
|
throw new Error("Expression nesting exceeds 80");
|
|
@@ -35,6 +37,9 @@ export class FlowExpression {
|
|
|
35
37
|
lt(value) {
|
|
36
38
|
return operation("lt", [this, value]);
|
|
37
39
|
}
|
|
40
|
+
gt(value) {
|
|
41
|
+
return operation("gt", [this, value]);
|
|
42
|
+
}
|
|
38
43
|
gte(value) {
|
|
39
44
|
return operation("gte", [this, value]);
|
|
40
45
|
}
|
|
@@ -57,6 +62,23 @@ function operation(op, args) {
|
|
|
57
62
|
export const all = (...conditions) => operation("all", conditions);
|
|
58
63
|
export const any = (...conditions) => operation("any", conditions);
|
|
59
64
|
export const not = (condition) => operation("not", [condition]);
|
|
65
|
+
// Nested programs share a reservation budget without exposing execution internals in the API.
|
|
66
|
+
const executionBudgets = new WeakMap();
|
|
67
|
+
function withBudget(options, budget) {
|
|
68
|
+
executionBudgets.set(options, budget);
|
|
69
|
+
return options;
|
|
70
|
+
}
|
|
71
|
+
function definitionSize(definition) {
|
|
72
|
+
return definition.nodes.reduce((count, node) => count +
|
|
73
|
+
1 +
|
|
74
|
+
(node.itemDefinition ? definitionSize(node.itemDefinition) : 0), 0);
|
|
75
|
+
}
|
|
76
|
+
export class FlowLoopLimitError extends Error {
|
|
77
|
+
name = "FlowLoopLimitError";
|
|
78
|
+
constructor() {
|
|
79
|
+
super("Loop condition is still true at maxIterations");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
60
82
|
const plain = (v) => !!v &&
|
|
61
83
|
typeof v === "object" &&
|
|
62
84
|
(Object.getPrototypeOf(v) === Object.prototype ||
|
|
@@ -101,7 +123,7 @@ function describe(ast) {
|
|
|
101
123
|
return "[literal]";
|
|
102
124
|
return `${ast.op}(${ast.args.map(describe).join(", ")})`;
|
|
103
125
|
}
|
|
104
|
-
function conditionRule(ast, requests) {
|
|
126
|
+
function conditionRule(ast, requests, tasks) {
|
|
105
127
|
let remaining = 128;
|
|
106
128
|
const visit = (node) => {
|
|
107
129
|
if (--remaining < 0)
|
|
@@ -139,10 +161,12 @@ function conditionRule(ast, requests) {
|
|
|
139
161
|
? path.length === 3 && ["noul", "choice", "score"].includes(path[2])
|
|
140
162
|
? path[1]
|
|
141
163
|
: path.slice(1).join(" ")
|
|
142
|
-
:
|
|
164
|
+
: path.at(-1) ?? "Input",
|
|
143
165
|
...(base.op === "ref" && requests.has(base.id)
|
|
144
166
|
? { stepId: base.id, path }
|
|
145
|
-
:
|
|
167
|
+
: base.op === "ref" && tasks.has(base.id)
|
|
168
|
+
? { stepId: base.id, path, source: "task" }
|
|
169
|
+
: {}),
|
|
146
170
|
};
|
|
147
171
|
}
|
|
148
172
|
if (node.op === "literal")
|
|
@@ -211,14 +235,15 @@ function evaluate(ast, input, values) {
|
|
|
211
235
|
!Number.isFinite(a) ||
|
|
212
236
|
!Number.isFinite(b))
|
|
213
237
|
throw new Error("Numeric comparison requires finite numbers");
|
|
214
|
-
return ast.op === "lt" ? a < b : a >= b;
|
|
238
|
+
return ast.op === "lt" ? a < b : ast.op === "gt" ? a > b : a >= b;
|
|
215
239
|
}
|
|
216
240
|
export function defineJevFlow(name, build) {
|
|
217
241
|
const owner = {}, ids = new Set(), nodes = [];
|
|
218
242
|
const input = new FlowExpression({ op: "input" }, owner);
|
|
219
243
|
let building = true;
|
|
244
|
+
let templateSteps = 0;
|
|
220
245
|
let activeBlock;
|
|
221
|
-
function block(callback, available, path, previous = [], depth = 0, requireOutcome = false) {
|
|
246
|
+
function block(callback, available, path, previous = [], depth = 0, requireOutcome = false, requireValue = false) {
|
|
222
247
|
if (depth > 16)
|
|
223
248
|
throw new Error("Flow nesting exceeds 16");
|
|
224
249
|
const token = {};
|
|
@@ -242,7 +267,7 @@ export function defineJevFlow(name, build) {
|
|
|
242
267
|
throw new Error("No steps allowed after a terminal output or branch");
|
|
243
268
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$/.test(id) || ids.has(id))
|
|
244
269
|
throw new Error(`Invalid or duplicate step ID: ${id}`);
|
|
245
|
-
if (ids.size >= 256)
|
|
270
|
+
if (ids.size + templateSteps >= 256)
|
|
246
271
|
throw new Error("Flows support at most 256 steps");
|
|
247
272
|
const copy = {};
|
|
248
273
|
if (display !== undefined) {
|
|
@@ -283,21 +308,45 @@ export function defineJevFlow(name, build) {
|
|
|
283
308
|
const builder = {
|
|
284
309
|
input,
|
|
285
310
|
jev(id, request, display) {
|
|
286
|
-
const
|
|
311
|
+
const dynamic = "request" in request;
|
|
312
|
+
if (dynamic && typeof request.request !== "function")
|
|
313
|
+
throw new Error("Request factory must be a function");
|
|
314
|
+
const frozen = mapValue(dynamic ? request.input : request, (expr) => expr);
|
|
287
315
|
const step = add(id, "jev", frozen, display);
|
|
288
|
-
|
|
316
|
+
if (dynamic) {
|
|
317
|
+
step.input = frozen;
|
|
318
|
+
step.factory = request.request;
|
|
319
|
+
step.answerNames = new Set();
|
|
320
|
+
}
|
|
321
|
+
else
|
|
322
|
+
step.request = frozen;
|
|
289
323
|
return {
|
|
324
|
+
answers: new FlowExpression({ op: "get", from: { op: "ref", id }, key: "answers" }, owner),
|
|
290
325
|
answer(question) {
|
|
291
|
-
if (!Object.hasOwn(
|
|
326
|
+
if (!dynamic && !Object.hasOwn(step.request.questions, question))
|
|
292
327
|
throw new Error(`Unknown question: ${question}`);
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
?
|
|
296
|
-
: type
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
328
|
+
step.answerNames?.add(question);
|
|
329
|
+
const type = dynamic
|
|
330
|
+
? undefined
|
|
331
|
+
: step.request.questions[question].type;
|
|
332
|
+
const fields = dynamic
|
|
333
|
+
? [
|
|
334
|
+
"type",
|
|
335
|
+
"noul",
|
|
336
|
+
"choice",
|
|
337
|
+
"score",
|
|
338
|
+
"confidence",
|
|
339
|
+
"probabilities",
|
|
340
|
+
"legend",
|
|
341
|
+
"box",
|
|
342
|
+
]
|
|
343
|
+
: type === "noul"
|
|
344
|
+
? ["type", "noul"]
|
|
345
|
+
: type === "choice"
|
|
346
|
+
? ["type", "choice", "confidence", "probabilities"]
|
|
347
|
+
: type === "score"
|
|
348
|
+
? ["type", "score", "confidence", "probabilities", "legend"]
|
|
349
|
+
: ["type", "box", "confidence"];
|
|
301
350
|
return Object.fromEntries(fields.map((field) => [
|
|
302
351
|
field,
|
|
303
352
|
new FlowExpression({
|
|
@@ -317,6 +366,157 @@ export function defineJevFlow(name, build) {
|
|
|
317
366
|
},
|
|
318
367
|
};
|
|
319
368
|
},
|
|
369
|
+
task(id, task, display) {
|
|
370
|
+
if (typeof task.run !== "function")
|
|
371
|
+
throw new Error("Task run must be a function");
|
|
372
|
+
if (task.timeoutMs !== undefined &&
|
|
373
|
+
(!Number.isInteger(task.timeoutMs) ||
|
|
374
|
+
task.timeoutMs < 1 ||
|
|
375
|
+
task.timeoutMs > 2_147_483_647))
|
|
376
|
+
throw new Error("Task timeoutMs must be a positive 32-bit integer");
|
|
377
|
+
const frozen = mapValue(task.input, (expr) => expr);
|
|
378
|
+
const step = add(id, "task", frozen, display);
|
|
379
|
+
step.input = frozen;
|
|
380
|
+
step.task = task.run;
|
|
381
|
+
step.timeoutMs = task.timeoutMs;
|
|
382
|
+
return new FlowExpression({ op: "ref", id }, owner);
|
|
383
|
+
},
|
|
384
|
+
map(id, config, buildItem, display) {
|
|
385
|
+
const concurrency = config.concurrency ?? 4;
|
|
386
|
+
if (!Number.isInteger(concurrency) ||
|
|
387
|
+
concurrency < 1 ||
|
|
388
|
+
concurrency > 16)
|
|
389
|
+
throw new Error("Map concurrency must be an integer from 1 to 16");
|
|
390
|
+
if (!Number.isInteger(config.maxItems) ||
|
|
391
|
+
config.maxItems < 1 ||
|
|
392
|
+
config.maxItems > 64)
|
|
393
|
+
throw new Error("Map maxItems must be an integer from 1 to 64");
|
|
394
|
+
const frozen = mapValue({ items: config.items, context: config.input }, (e) => e);
|
|
395
|
+
const step = add(id, "map", frozen, display);
|
|
396
|
+
const parentScope = activeBlock;
|
|
397
|
+
activeBlock = undefined;
|
|
398
|
+
let program;
|
|
399
|
+
try {
|
|
400
|
+
program = defineJevFlow(`Item · ${display?.title ?? id}`.slice(0, 100), (item) => {
|
|
401
|
+
const result = buildItem(item);
|
|
402
|
+
let outputId = "item-result";
|
|
403
|
+
for (let suffix = 1; builderIds.get(item).has(outputId); suffix++)
|
|
404
|
+
outputId = `item-result-${suffix}`;
|
|
405
|
+
return item.output(outputId, result, {
|
|
406
|
+
title: "Item result",
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
activeBlock = parentScope;
|
|
412
|
+
}
|
|
413
|
+
if (program.definition.nodes.some((n) => n.kind === "map"))
|
|
414
|
+
throw new Error("Nested maps are not supported");
|
|
415
|
+
if (program.definition.nodes.some((n) => n.kind === "loop"))
|
|
416
|
+
throw new Error("Loops inside maps are not supported");
|
|
417
|
+
templateSteps += definitionSize(program.definition);
|
|
418
|
+
if (ids.size + templateSteps > 256)
|
|
419
|
+
throw new Error("Flows support at most 256 steps including map templates");
|
|
420
|
+
step.collection = {
|
|
421
|
+
...frozen,
|
|
422
|
+
concurrency,
|
|
423
|
+
maxItems: config.maxItems,
|
|
424
|
+
program: program,
|
|
425
|
+
};
|
|
426
|
+
nodes.find((node) => node.id === id).itemDefinition =
|
|
427
|
+
program.definition;
|
|
428
|
+
return new FlowExpression({ op: "ref", id }, owner);
|
|
429
|
+
},
|
|
430
|
+
loop(id, config, buildIteration, display) {
|
|
431
|
+
if (!Number.isInteger(config.maxIterations) ||
|
|
432
|
+
config.maxIterations < 1 ||
|
|
433
|
+
config.maxIterations > 64)
|
|
434
|
+
throw new Error("Loop maxIterations must be an integer from 1 to 64");
|
|
435
|
+
const gateOwner = {};
|
|
436
|
+
const when = config.while(new FlowExpression({ op: "input" }, gateOwner));
|
|
437
|
+
if (!(when instanceof FlowExpression) || owners.get(when) !== gateOwner)
|
|
438
|
+
throw new Error("Loop while must be an expression of its state");
|
|
439
|
+
inspectAst(when.ast, () => {
|
|
440
|
+
throw new Error("Loop while cannot reference outer steps");
|
|
441
|
+
});
|
|
442
|
+
const frozen = mapValue({ initial: config.initial, context: config.input }, (e) => e);
|
|
443
|
+
const step = add(id, "loop", frozen, display);
|
|
444
|
+
const parentScope = activeBlock;
|
|
445
|
+
activeBlock = undefined;
|
|
446
|
+
let program;
|
|
447
|
+
try {
|
|
448
|
+
program = defineJevFlow(`Iteration · ${display?.title ?? id}`.slice(0, 100), (iteration) => {
|
|
449
|
+
const result = buildIteration(iteration);
|
|
450
|
+
let outputId = "iteration-result";
|
|
451
|
+
for (let suffix = 1; builderIds.get(iteration).has(outputId); suffix++)
|
|
452
|
+
outputId = `iteration-result-${suffix}`;
|
|
453
|
+
return iteration.output(outputId, result, {
|
|
454
|
+
title: "Next iteration state",
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
activeBlock = parentScope;
|
|
460
|
+
}
|
|
461
|
+
if (program.definition.nodes.some((n) => n.kind === "loop"))
|
|
462
|
+
throw new Error("Nested loops are not supported");
|
|
463
|
+
templateSteps += definitionSize(program.definition);
|
|
464
|
+
if (ids.size + templateSteps > 256)
|
|
465
|
+
throw new Error("Flows support at most 256 steps including iteration templates");
|
|
466
|
+
step.loop = {
|
|
467
|
+
...frozen,
|
|
468
|
+
when,
|
|
469
|
+
maxIterations: config.maxIterations,
|
|
470
|
+
program,
|
|
471
|
+
};
|
|
472
|
+
const node = nodes.find((n) => n.id === id);
|
|
473
|
+
node.itemDefinition = program.definition;
|
|
474
|
+
// Point displayed gate inputs at the loop's captured latest state.
|
|
475
|
+
// Execution still evaluates the original isolated state expression.
|
|
476
|
+
const referenceState = (ast) => {
|
|
477
|
+
if (ast.op === "input")
|
|
478
|
+
return { op: "ref", id };
|
|
479
|
+
if (ast.op === "get")
|
|
480
|
+
return { ...ast, from: referenceState(ast.from) };
|
|
481
|
+
if ("args" in ast)
|
|
482
|
+
return { ...ast, args: ast.args.map(referenceState) };
|
|
483
|
+
return ast;
|
|
484
|
+
};
|
|
485
|
+
node.conditionRule = conditionRule(referenceState(when.ast), new Set(), new Set([id]));
|
|
486
|
+
return new FlowExpression({ op: "ref", id }, owner);
|
|
487
|
+
},
|
|
488
|
+
if(id, paths, display) {
|
|
489
|
+
if (!(paths.when instanceof FlowExpression))
|
|
490
|
+
throw new Error("if requires a condition expression");
|
|
491
|
+
const step = add(id, "if", paths.when, display);
|
|
492
|
+
step.when = paths.when;
|
|
493
|
+
const node = nodes.find((n) => n.id === id);
|
|
494
|
+
node.condition = describe(paths.when.ast);
|
|
495
|
+
node.conditionRule = conditionRule(paths.when.ast, new Set(nodes.filter((n) => n.kind === "jev").map((n) => n.id)), new Set(nodes
|
|
496
|
+
.filter((n) => n.kind === "task" || n.kind === "merge" || n.kind === "loop")
|
|
497
|
+
.map((n) => n.id)));
|
|
498
|
+
step.then = block(paths.then, new Set(available), [...path, `${id}:then`], [id], depth + 1, false, true);
|
|
499
|
+
step.else = block(paths.else, new Set(available), [...path, `${id}:else`], [id], depth + 1, false, true);
|
|
500
|
+
// A separate merge keeps the diagram acyclic and gives downstream refs a
|
|
501
|
+
// scope-safe identity, including when an arm returns an existing value.
|
|
502
|
+
const base = `${id.slice(0, 65)}-result`;
|
|
503
|
+
let mergeId = base;
|
|
504
|
+
for (let suffix = 2; ids.has(mergeId); suffix++)
|
|
505
|
+
mergeId = `${base}-${suffix}`;
|
|
506
|
+
const merge = add(mergeId, "merge", undefined, {
|
|
507
|
+
title: "Selected result",
|
|
508
|
+
description: "Continue with the value returned by the selected path.",
|
|
509
|
+
});
|
|
510
|
+
merge.mergeOf = id;
|
|
511
|
+
const mergeNode = nodes.find((n) => n.id === mergeId);
|
|
512
|
+
const ends = {
|
|
513
|
+
then: step.then.nodes.at(-1)?.id ?? id,
|
|
514
|
+
else: step.else.nodes.at(-1)?.id ?? id,
|
|
515
|
+
};
|
|
516
|
+
mergeNode.dependencies = [...new Set(Object.values(ends))];
|
|
517
|
+
mergeNode.merge = { condition: id, ...ends };
|
|
518
|
+
return new FlowExpression({ op: "ref", id: mergeId }, owner);
|
|
519
|
+
},
|
|
320
520
|
branch(id, paths, display) {
|
|
321
521
|
const step = add(id, "branch", paths.when, display);
|
|
322
522
|
step.when = paths.when;
|
|
@@ -324,7 +524,9 @@ export function defineJevFlow(name, build) {
|
|
|
324
524
|
throw new Error("branch requires a condition expression");
|
|
325
525
|
const node = nodes.find((n) => n.id === id);
|
|
326
526
|
node.condition = describe(paths.when.ast);
|
|
327
|
-
node.conditionRule = conditionRule(paths.when.ast, new Set(nodes.filter((n) => n.kind === "jev").map((n) => n.id))
|
|
527
|
+
node.conditionRule = conditionRule(paths.when.ast, new Set(nodes.filter((n) => n.kind === "jev").map((n) => n.id)), new Set(nodes
|
|
528
|
+
.filter((n) => n.kind === "task" || n.kind === "merge" || n.kind === "loop")
|
|
529
|
+
.map((n) => n.id)));
|
|
328
530
|
step.then = block(paths.then, new Set(available), [...path, `${id}:then`], [id], depth + 1, true);
|
|
329
531
|
step.else = block(paths.else, new Set(available), [...path, `${id}:else`], [id], depth + 1, true);
|
|
330
532
|
terminal = true;
|
|
@@ -358,6 +560,7 @@ export function defineJevFlow(name, build) {
|
|
|
358
560
|
return new FlowExpression({ op: "ref", id: outcome }, owner);
|
|
359
561
|
},
|
|
360
562
|
};
|
|
563
|
+
builderIds.set(builder, ids);
|
|
361
564
|
const parentBlock = activeBlock;
|
|
362
565
|
activeBlock = token;
|
|
363
566
|
let returned;
|
|
@@ -369,6 +572,8 @@ export function defineJevFlow(name, build) {
|
|
|
369
572
|
}
|
|
370
573
|
if (!(returned instanceof FlowExpression))
|
|
371
574
|
throw new Error("Flow callbacks must return an expression, output, or branch");
|
|
575
|
+
if (requireValue && terminal)
|
|
576
|
+
throw new Error("if paths must return a value, not a terminal output or branch");
|
|
372
577
|
if (requireOutcome && !terminal)
|
|
373
578
|
throw new Error("Branch paths must end with output or branch");
|
|
374
579
|
if (terminal &&
|
|
@@ -409,6 +614,7 @@ export function defineJevFlow(name, build) {
|
|
|
409
614
|
if (node.lanes)
|
|
410
615
|
Object.freeze(node.lanes);
|
|
411
616
|
freezeRule(node.conditionRule);
|
|
617
|
+
freezeRule(node.itemDefinition);
|
|
412
618
|
Object.freeze(node);
|
|
413
619
|
}
|
|
414
620
|
Object.freeze(definitionView.nodes);
|
|
@@ -417,6 +623,14 @@ export function defineJevFlow(name, build) {
|
|
|
417
623
|
definition: definitionView,
|
|
418
624
|
async run(data, options) {
|
|
419
625
|
const controller = new AbortController();
|
|
626
|
+
// Preserve the initiating failure before aborting siblings: their cancellation
|
|
627
|
+
// can reach a surrounding parallel join before the failing map settles.
|
|
628
|
+
let siblingFailure;
|
|
629
|
+
const stopSiblings = (error) => {
|
|
630
|
+
if (!controller.signal.aborted)
|
|
631
|
+
siblingFailure = { error };
|
|
632
|
+
controller.abort(error);
|
|
633
|
+
};
|
|
420
634
|
const cancel = () => controller.abort(options.signal?.reason);
|
|
421
635
|
if (options.signal?.aborted)
|
|
422
636
|
cancel();
|
|
@@ -435,6 +649,12 @@ export function defineJevFlow(name, build) {
|
|
|
435
649
|
])),
|
|
436
650
|
};
|
|
437
651
|
const started = performance.now(), values = new Map();
|
|
652
|
+
const budget = executionBudgets.get(options) ?? { used: nodes.length };
|
|
653
|
+
const reserve = (count) => {
|
|
654
|
+
if (budget.used + count > 1024)
|
|
655
|
+
throw new RangeError("Flow exceeds 1024 expanded steps");
|
|
656
|
+
budget.used += count;
|
|
657
|
+
};
|
|
438
658
|
let observer;
|
|
439
659
|
try {
|
|
440
660
|
observer = options.devtools?.startFlow(structuredClone(definition), structuredClone(run));
|
|
@@ -445,9 +665,12 @@ export function defineJevFlow(name, build) {
|
|
|
445
665
|
const emit = () => {
|
|
446
666
|
try {
|
|
447
667
|
if (observer) {
|
|
448
|
-
const { value, ...metadata } = run;
|
|
668
|
+
const { value, steps, ...metadata } = run;
|
|
669
|
+
// Stable data snapshots are captured once by the recorder, not
|
|
670
|
+
// cloned/redacted again on every unrelated step update.
|
|
449
671
|
observer.update({
|
|
450
672
|
...structuredClone(metadata),
|
|
673
|
+
steps: Object.fromEntries(Object.entries(steps).map(([id, step]) => [id, { ...step }])),
|
|
451
674
|
...(value !== undefined ? { value } : {}),
|
|
452
675
|
});
|
|
453
676
|
}
|
|
@@ -482,7 +705,29 @@ export function defineJevFlow(name, build) {
|
|
|
482
705
|
try {
|
|
483
706
|
let value;
|
|
484
707
|
if (step.kind === "jev") {
|
|
485
|
-
const
|
|
708
|
+
const generated = step.factory
|
|
709
|
+
? step.factory(jsonSnapshot(mapValue(step.input, (e) => evaluate(e.ast, data, values))))
|
|
710
|
+
: step.request;
|
|
711
|
+
if (step.factory) {
|
|
712
|
+
if (generated &&
|
|
713
|
+
typeof generated.then ===
|
|
714
|
+
"function") {
|
|
715
|
+
// Invalid async factories must not leave an unhandled rejection.
|
|
716
|
+
void Promise.resolve(generated).catch(() => { });
|
|
717
|
+
throw new TypeError("Request factory must be synchronous");
|
|
718
|
+
}
|
|
719
|
+
if (!plain(generated) || !plain(generated.questions))
|
|
720
|
+
throw new Error("Request factory must return a synchronous SDK request");
|
|
721
|
+
for (const name of step.answerNames)
|
|
722
|
+
if (!Object.hasOwn(generated.questions, name))
|
|
723
|
+
throw new Error(`Unknown generated question: ${name}`);
|
|
724
|
+
}
|
|
725
|
+
const request = mapValue(generated, (e) => {
|
|
726
|
+
if (step.factory)
|
|
727
|
+
throw new Error("Request factories must return resolved values");
|
|
728
|
+
return evaluate(e.ast, data, values);
|
|
729
|
+
});
|
|
730
|
+
abortCheck();
|
|
486
731
|
const call = () => options.client.systemOne(request, {
|
|
487
732
|
signal: controller.signal,
|
|
488
733
|
});
|
|
@@ -491,15 +736,211 @@ export function defineJevFlow(name, build) {
|
|
|
491
736
|
stepId: step.id,
|
|
492
737
|
executionId: state.executionId,
|
|
493
738
|
};
|
|
494
|
-
value = await (observer
|
|
739
|
+
value = await runCallback(() => observer
|
|
495
740
|
? observer.request(link, (id) => {
|
|
741
|
+
if (controller.signal.aborted ||
|
|
742
|
+
state.status !== "running")
|
|
743
|
+
return;
|
|
496
744
|
state.traceId = id;
|
|
497
745
|
emit();
|
|
498
746
|
}, call)
|
|
499
|
-
: call());
|
|
747
|
+
: call(), controller.signal);
|
|
500
748
|
abortCheck();
|
|
501
749
|
}
|
|
502
|
-
else if (step.kind === "
|
|
750
|
+
else if (step.kind === "task") {
|
|
751
|
+
const input = jsonSnapshot(mapValue(step.input, (e) => evaluate(e.ast, data, values)));
|
|
752
|
+
state.data = { input };
|
|
753
|
+
emit();
|
|
754
|
+
value = jsonSnapshot(await runCallback((signal) => step.task(jsonSnapshot(input), {
|
|
755
|
+
signal,
|
|
756
|
+
runId: run.id,
|
|
757
|
+
stepId: step.id,
|
|
758
|
+
executionId: state.executionId,
|
|
759
|
+
}), controller.signal, step.timeoutMs));
|
|
760
|
+
abortCheck();
|
|
761
|
+
state.data = { input, output: jsonSnapshot(value) };
|
|
762
|
+
}
|
|
763
|
+
else if (step.kind === "loop") {
|
|
764
|
+
const config = step.loop;
|
|
765
|
+
const initial = jsonSnapshot(mapValue(config.initial, (e) => evaluate(e.ast, data, values)));
|
|
766
|
+
const context = jsonSnapshot(mapValue(config.context, (e) => evaluate(e.ast, data, values)));
|
|
767
|
+
let current = jsonSnapshot(initial);
|
|
768
|
+
state.collection = { total: 0, concurrency: 1, items: [] };
|
|
769
|
+
state.loop = { maxIterations: config.maxIterations };
|
|
770
|
+
state.data = { input: initial, output: current };
|
|
771
|
+
for (let index = 0;; index++) {
|
|
772
|
+
abortCheck();
|
|
773
|
+
const decision = evaluate(config.when.ast, current, new Map());
|
|
774
|
+
if (typeof decision !== "boolean")
|
|
775
|
+
throw new TypeError("Loop condition did not return a boolean");
|
|
776
|
+
state.decision = decision;
|
|
777
|
+
if (!decision) {
|
|
778
|
+
state.loop = { ...state.loop, stopReason: "condition" };
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
if (index === config.maxIterations) {
|
|
782
|
+
state.loop = { ...state.loop, stopReason: "limit" };
|
|
783
|
+
throw new FlowLoopLimitError();
|
|
784
|
+
}
|
|
785
|
+
reserve(config.program.definition.nodes.length);
|
|
786
|
+
state.collection = {
|
|
787
|
+
...state.collection,
|
|
788
|
+
total: index + 1,
|
|
789
|
+
items: [
|
|
790
|
+
...state.collection.items,
|
|
791
|
+
{ index, status: "waiting" },
|
|
792
|
+
],
|
|
793
|
+
};
|
|
794
|
+
const updateIteration = (child) => {
|
|
795
|
+
state.collection = {
|
|
796
|
+
...state.collection,
|
|
797
|
+
items: state.collection.items.map((item) => item.index === index
|
|
798
|
+
? {
|
|
799
|
+
index,
|
|
800
|
+
status: child.status === "pending"
|
|
801
|
+
? "running"
|
|
802
|
+
: child.status === "success"
|
|
803
|
+
? "completed"
|
|
804
|
+
: child.status,
|
|
805
|
+
run: child,
|
|
806
|
+
}
|
|
807
|
+
: item),
|
|
808
|
+
};
|
|
809
|
+
emit();
|
|
810
|
+
};
|
|
811
|
+
const result = await config.program.run({ state: current, index, context }, withBudget({
|
|
812
|
+
client: options.client,
|
|
813
|
+
signal: controller.signal,
|
|
814
|
+
devtools: {
|
|
815
|
+
startFlow(_definition, child) {
|
|
816
|
+
updateIteration(child);
|
|
817
|
+
return {
|
|
818
|
+
update: updateIteration,
|
|
819
|
+
request(link, onTrace, callback) {
|
|
820
|
+
return observer
|
|
821
|
+
? observer.request({
|
|
822
|
+
...link,
|
|
823
|
+
runId: run.id,
|
|
824
|
+
stepId: `${step.id}/${index}/${link.stepId}`,
|
|
825
|
+
}, onTrace, callback)
|
|
826
|
+
: callback();
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
}, budget));
|
|
832
|
+
abortCheck();
|
|
833
|
+
current = jsonSnapshot(result.value);
|
|
834
|
+
state.data = { input: initial, output: current };
|
|
835
|
+
}
|
|
836
|
+
value = current;
|
|
837
|
+
}
|
|
838
|
+
else if (step.kind === "map") {
|
|
839
|
+
const config = step.collection;
|
|
840
|
+
const items = jsonSnapshot(mapValue(config.items, (e) => evaluate(e.ast, data, values)));
|
|
841
|
+
const context = jsonSnapshot(mapValue(config.context, (e) => evaluate(e.ast, data, values)));
|
|
842
|
+
if (!Array.isArray(items))
|
|
843
|
+
throw new TypeError("Map items must be an array");
|
|
844
|
+
if (items.length > config.maxItems)
|
|
845
|
+
throw new RangeError("Map exceeds maxItems");
|
|
846
|
+
reserve(items.length * config.program.definition.nodes.length);
|
|
847
|
+
state.collection = {
|
|
848
|
+
total: items.length,
|
|
849
|
+
concurrency: config.concurrency,
|
|
850
|
+
items: items.map((_, index) => ({ index, status: "waiting" })),
|
|
851
|
+
};
|
|
852
|
+
emit();
|
|
853
|
+
const results = new Array(items.length);
|
|
854
|
+
let next = 0, failed = false, firstError;
|
|
855
|
+
const updateItem = (index, child) => {
|
|
856
|
+
state.collection = {
|
|
857
|
+
...state.collection,
|
|
858
|
+
items: state.collection.items.map((item) => item.index === index
|
|
859
|
+
? {
|
|
860
|
+
index,
|
|
861
|
+
status: child.status === "pending"
|
|
862
|
+
? "running"
|
|
863
|
+
: child.status === "success"
|
|
864
|
+
? "completed"
|
|
865
|
+
: child.status,
|
|
866
|
+
run: child,
|
|
867
|
+
}
|
|
868
|
+
: item),
|
|
869
|
+
};
|
|
870
|
+
emit();
|
|
871
|
+
};
|
|
872
|
+
const worker = async () => {
|
|
873
|
+
while (!controller.signal.aborted &&
|
|
874
|
+
!failed &&
|
|
875
|
+
next < items.length) {
|
|
876
|
+
const index = next++;
|
|
877
|
+
try {
|
|
878
|
+
const result = await config.program.run({ item: items[index], index, context }, withBudget({
|
|
879
|
+
client: options.client,
|
|
880
|
+
signal: controller.signal,
|
|
881
|
+
devtools: {
|
|
882
|
+
startFlow(_definition, child) {
|
|
883
|
+
updateItem(index, child);
|
|
884
|
+
return {
|
|
885
|
+
update(child) {
|
|
886
|
+
updateItem(index, child);
|
|
887
|
+
},
|
|
888
|
+
request(link, onTrace, callback) {
|
|
889
|
+
return observer
|
|
890
|
+
? observer.request({
|
|
891
|
+
...link,
|
|
892
|
+
runId: run.id,
|
|
893
|
+
stepId: `${step.id}/${index}/${link.stepId}`,
|
|
894
|
+
}, onTrace, callback)
|
|
895
|
+
: callback();
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
}, budget));
|
|
901
|
+
results[index] = result.value;
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
if (!failed) {
|
|
905
|
+
failed = true;
|
|
906
|
+
firstError = error;
|
|
907
|
+
}
|
|
908
|
+
stopSiblings(error);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
await Promise.all(Array.from({ length: Math.min(config.concurrency, items.length) }, worker));
|
|
913
|
+
if (failed || controller.signal.aborted) {
|
|
914
|
+
state.collection = {
|
|
915
|
+
...state.collection,
|
|
916
|
+
items: state.collection.items.map((item) => item.status === "waiting"
|
|
917
|
+
? {
|
|
918
|
+
...item,
|
|
919
|
+
status: options.signal?.aborted
|
|
920
|
+
? "cancelled"
|
|
921
|
+
: "blocked",
|
|
922
|
+
}
|
|
923
|
+
: item),
|
|
924
|
+
};
|
|
925
|
+
if (failed)
|
|
926
|
+
throw firstError;
|
|
927
|
+
abortCheck();
|
|
928
|
+
}
|
|
929
|
+
value = results;
|
|
930
|
+
}
|
|
931
|
+
else if (step.kind === "merge") {
|
|
932
|
+
value = values.get(step.mergeOf);
|
|
933
|
+
state.decision = run.steps[step.mergeOf].decision;
|
|
934
|
+
// Capture is observational; a direct input/reference can contain
|
|
935
|
+
// a non-JSON value even though task boundaries require JSON.
|
|
936
|
+
try {
|
|
937
|
+
state.data = { output: jsonSnapshot(value) };
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
state.data = "Selected value could not be captured as JSON.";
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
else if (step.kind === "branch" || step.kind === "if") {
|
|
503
944
|
const decision = evaluate(step.when.ast, data, values);
|
|
504
945
|
if (typeof decision !== "boolean")
|
|
505
946
|
throw new Error("Branch condition did not return a boolean");
|
|
@@ -518,10 +959,10 @@ export function defineJevFlow(name, build) {
|
|
|
518
959
|
}
|
|
519
960
|
catch (e) {
|
|
520
961
|
if (!failed) {
|
|
521
|
-
firstError = e;
|
|
962
|
+
firstError = siblingFailure ? siblingFailure.error : e;
|
|
522
963
|
failed = true;
|
|
523
964
|
}
|
|
524
|
-
|
|
965
|
+
stopSiblings(e);
|
|
525
966
|
throw e;
|
|
526
967
|
}
|
|
527
968
|
}));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Json } from "../src/protocol.js";
|
|
2
|
+
/** Detached JSON snapshot, without capture truncation or lossy JSON.stringify. */
|
|
3
|
+
export declare function jsonSnapshot(value: unknown, seen?: Set<object>, depth?: number): Json;
|
|
4
|
+
/** Cancellation ends awaiting, even when user code ignores the cooperative signal. */
|
|
5
|
+
export declare function runCallback(callback: (signal: AbortSignal) => unknown, parent: AbortSignal, timeoutMs?: number): Promise<unknown>;
|