@scenario-labs/sdk 3.3.0 → 3.4.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/client.d.mts +4 -4
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +4 -4
  5. package/client.d.ts.map +1 -1
  6. package/client.js.map +1 -1
  7. package/client.mjs.map +1 -1
  8. package/index.d.mts +1 -1
  9. package/index.d.mts.map +1 -1
  10. package/index.d.ts +1 -1
  11. package/index.d.ts.map +1 -1
  12. package/index.js +2 -1
  13. package/index.js.map +1 -1
  14. package/index.mjs +1 -1
  15. package/index.mjs.map +1 -1
  16. package/lib/asset.d.mts +1 -1
  17. package/lib/asset.d.ts +1 -1
  18. package/lib/asset.js +1 -1
  19. package/lib/asset.mjs +1 -1
  20. package/lib/upload.d.mts +10 -5
  21. package/lib/upload.d.mts.map +1 -1
  22. package/lib/upload.d.ts +10 -5
  23. package/lib/upload.d.ts.map +1 -1
  24. package/lib/upload.js.map +1 -1
  25. package/lib/upload.mjs.map +1 -1
  26. package/lib/user_selection_limits.d.mts +13 -0
  27. package/lib/user_selection_limits.d.mts.map +1 -0
  28. package/lib/user_selection_limits.d.ts +13 -0
  29. package/lib/user_selection_limits.d.ts.map +1 -0
  30. package/lib/user_selection_limits.js +16 -0
  31. package/lib/user_selection_limits.js.map +1 -0
  32. package/lib/user_selection_limits.mjs +13 -0
  33. package/lib/user_selection_limits.mjs.map +1 -0
  34. package/lib/workflow.d.mts +4 -2
  35. package/lib/workflow.d.mts.map +1 -1
  36. package/lib/workflow.d.ts +4 -2
  37. package/lib/workflow.d.ts.map +1 -1
  38. package/lib/workflow.js +5 -1
  39. package/lib/workflow.js.map +1 -1
  40. package/lib/workflow.mjs +5 -1
  41. package/lib/workflow.mjs.map +1 -1
  42. package/lib/workflow_converter.d.mts +6 -0
  43. package/lib/workflow_converter.d.mts.map +1 -1
  44. package/lib/workflow_converter.d.ts +6 -0
  45. package/lib/workflow_converter.d.ts.map +1 -1
  46. package/lib/workflow_converter.js +155 -53
  47. package/lib/workflow_converter.js.map +1 -1
  48. package/lib/workflow_converter.mjs +155 -53
  49. package/lib/workflow_converter.mjs.map +1 -1
  50. package/lib/workflow_import_validator.d.mts +17 -2
  51. package/lib/workflow_import_validator.d.mts.map +1 -1
  52. package/lib/workflow_import_validator.d.ts +17 -2
  53. package/lib/workflow_import_validator.d.ts.map +1 -1
  54. package/lib/workflow_import_validator.js +100 -1
  55. package/lib/workflow_import_validator.js.map +1 -1
  56. package/lib/workflow_import_validator.mjs +99 -1
  57. package/lib/workflow_import_validator.mjs.map +1 -1
  58. package/lib/workflow_validator.d.mts +1 -1
  59. package/lib/workflow_validator.d.mts.map +1 -1
  60. package/lib/workflow_validator.d.ts +1 -1
  61. package/lib/workflow_validator.d.ts.map +1 -1
  62. package/lib/workflow_validator.js +10 -11
  63. package/lib/workflow_validator.js.map +1 -1
  64. package/lib/workflow_validator.mjs +7 -8
  65. package/lib/workflow_validator.mjs.map +1 -1
  66. package/package.json +1 -1
  67. package/resources/assets/assets.d.mts +126 -30
  68. package/resources/assets/assets.d.mts.map +1 -1
  69. package/resources/assets/assets.d.ts +126 -30
  70. package/resources/assets/assets.d.ts.map +1 -1
  71. package/resources/assets/assets.js +12 -0
  72. package/resources/assets/assets.js.map +1 -1
  73. package/resources/assets/assets.mjs +12 -0
  74. package/resources/assets/assets.mjs.map +1 -1
  75. package/resources/assets/index.d.mts +1 -1
  76. package/resources/assets/index.d.mts.map +1 -1
  77. package/resources/assets/index.d.ts +1 -1
  78. package/resources/assets/index.d.ts.map +1 -1
  79. package/resources/assets/index.js.map +1 -1
  80. package/resources/assets/index.mjs.map +1 -1
  81. package/resources/generate.d.mts +120 -9
  82. package/resources/generate.d.mts.map +1 -1
  83. package/resources/generate.d.ts +120 -9
  84. package/resources/generate.d.ts.map +1 -1
  85. package/resources/index.d.mts +2 -2
  86. package/resources/index.d.mts.map +1 -1
  87. package/resources/index.d.ts +2 -2
  88. package/resources/index.d.ts.map +1 -1
  89. package/resources/index.js.map +1 -1
  90. package/resources/index.mjs.map +1 -1
  91. package/resources/jobs.d.mts +36 -0
  92. package/resources/jobs.d.mts.map +1 -1
  93. package/resources/jobs.d.ts +36 -0
  94. package/resources/jobs.d.ts.map +1 -1
  95. package/resources/models/description.d.mts +4 -4
  96. package/resources/models/description.d.mts.map +1 -1
  97. package/resources/models/description.d.ts +4 -4
  98. package/resources/models/description.d.ts.map +1 -1
  99. package/resources/models/examples.d.mts +40 -6
  100. package/resources/models/examples.d.mts.map +1 -1
  101. package/resources/models/examples.d.ts +40 -6
  102. package/resources/models/examples.d.ts.map +1 -1
  103. package/resources/models/index.d.mts +1 -1
  104. package/resources/models/index.d.mts.map +1 -1
  105. package/resources/models/index.d.ts +1 -1
  106. package/resources/models/index.d.ts.map +1 -1
  107. package/resources/models/index.js.map +1 -1
  108. package/resources/models/index.mjs.map +1 -1
  109. package/resources/models/models.d.mts +1353 -199
  110. package/resources/models/models.d.mts.map +1 -1
  111. package/resources/models/models.d.ts +1353 -199
  112. package/resources/models/models.d.ts.map +1 -1
  113. package/resources/models/models.js +13 -0
  114. package/resources/models/models.js.map +1 -1
  115. package/resources/models/models.mjs +13 -0
  116. package/resources/models/models.mjs.map +1 -1
  117. package/resources/models/train.d.mts +36 -2
  118. package/resources/models/train.d.mts.map +1 -1
  119. package/resources/models/train.d.ts +36 -2
  120. package/resources/models/train.d.ts.map +1 -1
  121. package/resources/recommendations.d.mts +16 -5
  122. package/resources/recommendations.d.mts.map +1 -1
  123. package/resources/recommendations.d.ts +16 -5
  124. package/resources/recommendations.d.ts.map +1 -1
  125. package/resources/usages.d.mts +14 -2
  126. package/resources/usages.d.mts.map +1 -1
  127. package/resources/usages.d.ts +14 -2
  128. package/resources/usages.d.ts.map +1 -1
  129. package/resources/workflows.d.mts +152 -9
  130. package/resources/workflows.d.mts.map +1 -1
  131. package/resources/workflows.d.ts +152 -9
  132. package/resources/workflows.d.ts.map +1 -1
  133. package/resources/workflows.js +2 -3
  134. package/resources/workflows.js.map +1 -1
  135. package/resources/workflows.mjs +2 -3
  136. package/resources/workflows.mjs.map +1 -1
  137. package/src/client.ts +8 -0
  138. package/src/index.ts +1 -0
  139. package/src/lib/asset.ts +1 -1
  140. package/src/lib/upload.ts +10 -5
  141. package/src/lib/user_selection_limits.ts +14 -0
  142. package/src/lib/workflow.ts +12 -1
  143. package/src/lib/workflow_converter.ts +176 -55
  144. package/src/lib/workflow_import_validator.ts +116 -1
  145. package/src/lib/workflow_validator.ts +12 -8
  146. package/src/resources/assets/assets.ts +164 -4
  147. package/src/resources/assets/index.ts +2 -0
  148. package/src/resources/generate.ts +142 -0
  149. package/src/resources/index.ts +4 -0
  150. package/src/resources/jobs.ts +42 -0
  151. package/src/resources/models/description.ts +6 -0
  152. package/src/resources/models/examples.ts +48 -0
  153. package/src/resources/models/index.ts +2 -0
  154. package/src/resources/models/models.ts +1610 -114
  155. package/src/resources/models/train.ts +42 -0
  156. package/src/resources/recommendations.ts +18 -3
  157. package/src/resources/usages.ts +16 -0
  158. package/src/resources/workflows.ts +176 -9
  159. package/src/version.ts +1 -1
  160. package/version.d.mts +1 -1
  161. package/version.d.ts +1 -1
  162. package/version.js +1 -1
  163. package/version.mjs +1 -1
@@ -8,10 +8,9 @@ exports.getAspectRatioModelInputNames = getAspectRatioModelInputNames;
8
8
  exports.getAspectRatioBounds = getAspectRatioBounds;
9
9
  exports.getDimensionsFromAspectRatio = getDimensionsFromAspectRatio;
10
10
  exports.convertWorkflowEditorToFlow = convertWorkflowEditorToFlow;
11
+ const user_selection_limits_1 = require("./user_selection_limits.js");
11
12
  const RUNNABLE_TYPES = new Set(['model', 'llm', 'forEach', 'forEachEnd']);
12
13
  const MODEL_CUSTOM_INPUT_HANDLE_SUFFIX = '-source-custom';
13
- const USER_SELECTION_MIN_SELECT = 1;
14
- const USER_SELECTION_MAX_SELECT = 100;
15
14
  const DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE = 'Select the items to continue';
16
15
  exports.ASPECT_RATIO_PRESETS = [
17
16
  '21:9',
@@ -35,7 +34,12 @@ function toCelIdentifier(name) {
35
34
  return name.replaceAll('-', '_');
36
35
  }
37
36
  function escapeCELString(value) {
38
- return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
37
+ return value
38
+ .replace(/\\/g, '\\\\')
39
+ .replace(/'/g, "\\'")
40
+ .replace(/\n/g, '\\n')
41
+ .replace(/\r/g, '\\r')
42
+ .replace(/\t/g, '\\t');
39
43
  }
40
44
  function formatTextAsCELString(text) {
41
45
  return `'${escapeCELString(text)}'`;
@@ -160,6 +164,7 @@ function getForEachFlowListInputDescriptor(args) {
160
164
  listProvider?.type === 'text' ||
161
165
  listProvider?.type === 'llm' ||
162
166
  listProvider?.type === 'transformText' ||
167
+ listProvider?.type === 'joinTexts' ||
163
168
  (outputHandle?.type === 'text' && !!outputHandle.isArray);
164
169
  return isText ?
165
170
  { name: `text${inputIndex}`, type: 'string_array' }
@@ -333,17 +338,41 @@ function getIsSliceAssetsItemsSourceHandle(nodeId, sourceHandle) {
333
338
  return sourceHandle === `${nodeId}-source-items` || sourceHandle === `${nodeId}-source-assets`;
334
339
  }
335
340
  /**
336
- * Prompt Builder (transformText) binds upstream vars for CEL. Reuse the same
337
- * list-producer rules as concat/slice (`groupItems`, `sliceAssets`, `allOutputs`,
338
- * split-text, LLM "All Results", `isArray` handles): those nodes always emit CEL
339
- * lists (via concat/slice/randomSample), so Prompt Builder must bind them as
340
- * `string_array` for indexing. Scalar text (plain text nodes, forEach outputs)
341
- * stays `string`.
341
+ * True when `expression` treats `varName` as a CEL list via indexing, `size(x)`,
342
+ * or list macros (`.map` / `.filter` / …). This is a converter tolerance for
343
+ * flows that index a list producer (editor First/Last/Nth stores `x[0]` on
344
+ * `isArray` handles; agent/MCP/import flows may index without that flag). It is
345
+ * not Fawkes's preview rule: the webapp binds a list only when the handle has
346
+ * `isArray`. Intentionally narrow so Prompt Builder cannot be steered into
347
+ * list semantics through other CEL list idioms.
348
+ */
349
+ function expressionUsesListVariable(expression, varName) {
350
+ const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
351
+ return (new RegExp(`${escaped}\\s*\\[`).test(expression) ||
352
+ new RegExp(`\\bsize\\s*\\(\\s*${escaped}\\s*\\)`).test(expression) ||
353
+ new RegExp(`${escaped}\\s*\\.\\s*(map|filter|all|exists|exists_one)\\s*\\(`).test(expression));
354
+ }
355
+ /**
356
+ * Prompt Builder (transformText) binds upstream vars for CEL. List producers
357
+ * bind as `string_array` when the source handle is `isArray`, or when the
358
+ * expression indexes/uses the variable as a list (see
359
+ * `expressionUsesListVariable`). Otherwise bare refs stay `string` so the API
360
+ * collapses the list to its first item.
361
+ *
362
+ * Fawkes today omits `isArray` on Slice/Group Items on purpose: only `isArray`
363
+ * handles get an indexed insert (`x[0]`); every other list insert is bare `x`
364
+ * and must collapse to the first item. Adding `isArray` to those handles later
365
+ * requires migrating stored expressions (`x` → `x[0]`) in the same Fawkes
366
+ * release, or both preview and published flows break on `'…' + x`.
342
367
  */
343
- function getPromptBuilderInputType(targetNode, outHandle, outHandleName) {
344
- return getConcatListSourceDescriptor(targetNode, outHandle, outHandleName).inputType === 'string_array' ?
345
- 'string_array'
346
- : 'string';
368
+ function getPromptBuilderInputType(targetNode, outHandle, outHandleName, expression, varName) {
369
+ if (getConcatListSourceDescriptor(targetNode, outHandle, outHandleName).inputType !== 'string_array') {
370
+ return 'string';
371
+ }
372
+ if (outHandle?.isArray || expressionUsesListVariable(expression, varName)) {
373
+ return 'string_array';
374
+ }
375
+ return 'string';
347
376
  }
348
377
  function getConcatListSourceDescriptor(targetNode, outHandle, outHandleName) {
349
378
  const isArray = (targetNode.type === 'asset' && !!targetNode.data.isMultiple) ||
@@ -560,12 +589,43 @@ function applyAspectRatioInputs(args) {
560
589
  });
561
590
  }
562
591
  }
563
- function getForEachBodyEditorNodeIds(forEachId, forEachEndId, edges, nodes) {
592
+ function buildLoopGraph(nodes, edges) {
593
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
594
+ const isInternalForEachEdge = (e) => {
595
+ const target = nodeById.get(e.target);
596
+ return (nodeById.get(e.source)?.type === 'forEach' &&
597
+ target?.type === 'forEachEnd' &&
598
+ target.data.parentNodeId === e.source);
599
+ };
600
+ const push = (map, key, value) => {
601
+ const list = map.get(key);
602
+ if (list)
603
+ list.push(value);
604
+ else
605
+ map.set(key, [value]);
606
+ };
607
+ const graph = {
608
+ nodeById,
609
+ consumers: new Map(),
610
+ dataProviders: new Map(),
611
+ dataConsumers: new Map(),
612
+ };
613
+ for (const e of edges) {
614
+ push(graph.consumers, e.target, e.source);
615
+ if (isInternalForEachEdge(e) || nodeById.get(e.source)?.type === 'approval')
616
+ continue;
617
+ push(graph.dataProviders, e.source, e.target);
618
+ push(graph.dataConsumers, e.target, e.source);
619
+ }
620
+ return graph;
621
+ }
622
+ function getForEachBodyEditorNodeIds(forEachId, forEachEndId, edges, graph) {
564
623
  const bodyNodeIds = new Set();
565
624
  const queue = [];
566
625
  const escaped = forEachId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
626
+ const itemHandle = new RegExp(`^${escaped}-output-\\d+$`);
567
627
  for (const edge of edges) {
568
- if (edge.target === forEachId && edge.targetHandle?.match(new RegExp(`^${escaped}-output-\\d+$`))) {
628
+ if (edge.target === forEachId && edge.targetHandle && itemHandle.test(edge.targetHandle)) {
569
629
  queue.push(edge.source);
570
630
  }
571
631
  }
@@ -574,13 +634,12 @@ function getForEachBodyEditorNodeIds(forEachId, forEachEndId, edges, nodes) {
574
634
  if (nodeId === forEachEndId || bodyNodeIds.has(nodeId))
575
635
  continue;
576
636
  bodyNodeIds.add(nodeId);
577
- for (const edge of edges) {
578
- if (edge.target === nodeId && !bodyNodeIds.has(edge.source) && edge.source !== forEachEndId) {
579
- queue.push(edge.source);
580
- }
637
+ for (const consumer of graph.consumers.get(nodeId) ?? []) {
638
+ if (!bodyNodeIds.has(consumer) && consumer !== forEachEndId)
639
+ queue.push(consumer);
581
640
  }
582
641
  }
583
- addRandomizeClosureToBody(bodyNodeIds, forEachEndId, edges, nodes);
642
+ addRandomizeClosureToBody(bodyNodeIds, forEachEndId, graph);
584
643
  return bodyNodeIds;
585
644
  }
586
645
  /**
@@ -594,28 +653,34 @@ function getForEachBodyEditorNodeIds(forEachId, forEachEndId, edges, nodes) {
594
653
  * for an UNLOCKED randomize node we mutate `bodyNodeIds` in place to also include
595
654
  * that node plus the deterministic chain down to the body, so the runtime clones
596
655
  * it per iteration and each iteration re-draws. We only pull it in when *every*
597
- * path out of it lands in the body — if it also feeds a node outside the loop,
598
- * cloning it would break that external consumer, so we leave it shared.
656
+ * path out of it lands in the body or in this loop's results — if it also feeds a
657
+ * node outside the loop, cloning it would break that external consumer, so we
658
+ * leave it shared.
659
+ *
660
+ * Loop nodes (any for-each or loop end outside the body) stop both walks: a
661
+ * loop's list is computed once, before the loop runs, so neither a loop nor what
662
+ * feeds its list may be cloned with another loop's body. A for-each in its own
663
+ * body would even expand again on every iteration. A node that reaches one stays
664
+ * shared.
599
665
  */
600
- function addRandomizeClosureToBody(bodyNodeIds, forEachEndId, edges, nodes) {
601
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
666
+ function addRandomizeClosureToBody(bodyNodeIds, forEachEndId, graph) {
667
+ const isLoopNode = (id) => {
668
+ const type = graph.nodeById.get(id)?.type;
669
+ return type === 'forEach' || type === 'forEachEnd';
670
+ };
602
671
  const isUnlockedRandomize = (id) => {
603
- const data = nodeById.get(id)?.data;
604
- return data?.isRandomize === true && data.isSeedLocked !== true;
672
+ const data = graph.nodeById.get(id)?.data;
673
+ return !!data && getSliceAssetsMode(data) === 'randomize' && data.isSeedLocked !== true;
605
674
  };
606
- // edge convention: `{ source: consumer, target: provider }` — an input handle
607
- // on `source` reads an output handle on `target`.
608
- const providersOf = (id) => edges.filter((e) => e.source === id).map((e) => e.target);
609
- const consumersOf = (id) => edges.filter((e) => e.target === id).map((e) => e.source);
610
- // Every node strictly upstream of the body (its transitive input providers).
675
+ // The nodes that feed the body without going through a loop node.
611
676
  const upstream = new Set();
612
677
  const upstreamQueue = [...bodyNodeIds];
613
678
  while (upstreamQueue.length > 0) {
614
- for (const provider of providersOf(upstreamQueue.pop())) {
615
- if (provider !== forEachEndId && !bodyNodeIds.has(provider) && !upstream.has(provider)) {
616
- upstream.add(provider);
617
- upstreamQueue.push(provider);
618
- }
679
+ for (const provider of graph.dataProviders.get(upstreamQueue.pop()) ?? []) {
680
+ if (bodyNodeIds.has(provider) || upstream.has(provider) || isLoopNode(provider))
681
+ continue;
682
+ upstream.add(provider);
683
+ upstreamQueue.push(provider);
619
684
  }
620
685
  }
621
686
  for (const randomId of upstream) {
@@ -627,18 +692,17 @@ function addRandomizeClosureToBody(bodyNodeIds, forEachEndId, edges, nodes) {
627
692
  let escapesLoop = false;
628
693
  while (chainQueue.length > 0) {
629
694
  const id = chainQueue.pop();
630
- if (bodyNodeIds.has(id))
631
- continue; // reached the loop body — a valid sink
632
- chain.add(id);
633
- const consumers = consumersOf(id);
634
- if (consumers.length === 0) {
635
- escapesLoop = true; // dead-end outside the loop → do not internalize
695
+ // The body and this loop's results are where a per-iteration draw belongs.
696
+ if (bodyNodeIds.has(id) || id === forEachEndId || chain.has(id))
697
+ continue;
698
+ const consumers = graph.dataConsumers.get(id) ?? [];
699
+ // Another loop, or a dead end outside the loop: the draw must stay shared.
700
+ if (isLoopNode(id) || consumers.length === 0) {
701
+ escapesLoop = true;
636
702
  break;
637
703
  }
638
- for (const consumer of consumers) {
639
- if (!bodyNodeIds.has(consumer))
640
- chainQueue.push(consumer);
641
- }
704
+ chain.add(id);
705
+ chainQueue.push(...consumers);
642
706
  }
643
707
  if (!escapesLoop)
644
708
  for (const id of chain)
@@ -850,13 +914,14 @@ function convertWorkflowEditorToFlow(params) {
850
914
  const forEachBodyEditorNodeIds = new Map();
851
915
  const forEachLoopFlowIds = new Map();
852
916
  const forEachFlowItemRefs = new Map();
917
+ const loopGraph = buildLoopGraph(nodes, edges);
853
918
  for (const node of sortedNodes) {
854
919
  if (node.type !== 'forEach')
855
920
  continue;
856
921
  const endNode = nodes.find((n) => n.type === 'forEachEnd' && n.data.parentNodeId === node.id);
857
922
  if (!endNode)
858
923
  continue;
859
- forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges, nodes));
924
+ forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges, loopGraph));
860
925
  forEachLoopFlowIds.set(node.id, []);
861
926
  }
862
927
  // Pre-populate video firstFrame/lastFrame transform IDs for input nodes
@@ -1208,9 +1273,10 @@ function convertWorkflowEditorToFlow(params) {
1208
1273
  const outHandle = targetNode.data.outputHandles?.find((h) => h.id === edge.targetHandle);
1209
1274
  const outHandleName = outHandle?.name ?? 'output';
1210
1275
  const targetFlowId = nodeIdToFlowIdMap.get(targetNode.id) ?? targetNode.id;
1276
+ const varName = `${targetFlowId}_${outHandleName}`;
1211
1277
  flowInputs.push({
1212
- name: `${targetFlowId}_${outHandleName}`,
1213
- type: getPromptBuilderInputType(targetNode, outHandle, outHandleName),
1278
+ name: varName,
1279
+ type: getPromptBuilderInputType(targetNode, outHandle, outHandleName, value, varName),
1214
1280
  ref,
1215
1281
  });
1216
1282
  }
@@ -1253,6 +1319,37 @@ function convertWorkflowEditorToFlow(params) {
1253
1319
  inputs: flowInputs.length > 0 ? flowInputs : undefined,
1254
1320
  });
1255
1321
  // ------------------------------------
1322
+ // JoinTexts -> join string list into a single string
1323
+ // ------------------------------------
1324
+ }
1325
+ else if (node.type === 'joinTexts') {
1326
+ const flowInputs = [];
1327
+ const conditionalHandleId = getConditionalInputHandleId(node.id);
1328
+ let listSourceVarName;
1329
+ for (const edge of incomingEdges) {
1330
+ if (edge.sourceHandle === conditionalHandleId)
1331
+ continue;
1332
+ const targetNode = nodes.find((n) => n.id === edge.target);
1333
+ if (!targetNode)
1334
+ continue;
1335
+ const ref = getSourceRef(edge, ctx);
1336
+ if (!ref)
1337
+ continue;
1338
+ const varName = toCelIdentifier(targetNode.id);
1339
+ flowInputs.push({ name: varName, type: 'string_array', ref });
1340
+ listSourceVarName = varName;
1341
+ }
1342
+ flowInputs.push(...collectConditionalConnectionInputs(incomingEdges, node.id, ctx));
1343
+ const delim = node.data.joinDelimiter ?? ', ';
1344
+ flow.push({
1345
+ id: flowId,
1346
+ type: 'transform',
1347
+ logic: {
1348
+ transform: `${listSourceVarName ?? '[]'}.join(${formatTextAsCELString(delim)})`,
1349
+ },
1350
+ inputs: flowInputs.length > 0 ? flowInputs : undefined,
1351
+ });
1352
+ // ------------------------------------
1256
1353
  // GroupItems -> array concatenation
1257
1354
  // ------------------------------------
1258
1355
  }
@@ -1293,7 +1390,9 @@ function convertWorkflowEditorToFlow(params) {
1293
1390
  else {
1294
1391
  const { variables, flowInputs } = collectConcatVarsAndInputs(itemsEdges, ctx, getOutputHandleVarName);
1295
1392
  const count = clampUserSelectionCount(parseInt(String(sliceData.count ?? ''), 10) || 1);
1296
- const message = sliceData.message?.trim() || DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE;
1393
+ const message = typeof sliceData.message === 'string' ?
1394
+ sliceData.message.trim() || DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE
1395
+ : DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE;
1297
1396
  const arrayType = getUserSelectionArrayType(flowInputs);
1298
1397
  // Single array-typed source can feed user-selection directly; scalars must be wrapped.
1299
1398
  if (variables.length === 1 && variables[0]?.isArray && flowInputs[0]?.ref) {
@@ -1425,6 +1524,9 @@ function convertWorkflowEditorToFlow(params) {
1425
1524
  fieldsUsed.add(condition.field);
1426
1525
  }
1427
1526
  }
1527
+ // CEL conditions need hyphen→underscore identifiers, but the shared input-name
1528
+ // map must keep the real workflow input keys (`text-1`, not `text_1`).
1529
+ const celFieldNameMap = new Map();
1428
1530
  const edgesIntoIfElse = edges.filter((e) => e.target === node.id);
1429
1531
  for (const fieldKey of fieldsUsed) {
1430
1532
  const resolved = resolveIfElseConditionField(fieldKey, nodes);
@@ -1437,7 +1539,7 @@ function convertWorkflowEditorToFlow(params) {
1437
1539
  ?.data.outputHandles?.find((h) => h.id === resolved.targetHandleId)?.name ?? 'output'
1438
1540
  : 'output';
1439
1541
  const celVarName = getOutputHandleVarName(fieldNodeId, outputHandleName);
1440
- nodeIdToInputNameMap.set(fieldKey, celVarName);
1542
+ celFieldNameMap.set(fieldKey, celVarName);
1441
1543
  const edgeToIfElse = edgesIntoIfElse
1442
1544
  .filter((e) => e.source === fieldNodeId)
1443
1545
  .find((e) => {
@@ -1471,7 +1573,7 @@ function convertWorkflowEditorToFlow(params) {
1471
1573
  }
1472
1574
  const cases = [];
1473
1575
  for (let i = 0; i < conditionBlocks.length; i++) {
1474
- const condition = conditionBlockToCEL(conditionBlocks[i], nodeIdToInputNameMap);
1576
+ const condition = conditionBlockToCEL(conditionBlocks[i], celFieldNameMap);
1475
1577
  if (condition)
1476
1578
  cases.push({ condition, value: String(i + 2) });
1477
1579
  }
@@ -1578,7 +1680,7 @@ function getSliceAssetsMode(data) {
1578
1680
  return data.isRandomize ? 'randomize' : 'default';
1579
1681
  }
1580
1682
  function clampUserSelectionCount(count) {
1581
- return Math.min(USER_SELECTION_MAX_SELECT, Math.max(USER_SELECTION_MIN_SELECT, count));
1683
+ return Math.min(user_selection_limits_1.USER_SELECTION_MAX_CANDIDATES, Math.max(user_selection_limits_1.USER_SELECTION_MIN_SELECT, count));
1582
1684
  }
1583
1685
  function getUserSelectionArrayType(flowInputs) {
1584
1686
  const hasFile = flowInputs.some((input) => input.type === 'file' || input.type === 'file_array');