@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
@@ -1,9 +1,8 @@
1
1
  // Converts a workflow editor state (nodes + edges) to a flow array suitable for the Scenario API.
2
2
  // Ported from fawkes useConvertWorkflowToApp — pure function, no React dependencies.
3
+ import { USER_SELECTION_MAX_CANDIDATES, USER_SELECTION_MIN_SELECT } from "./user_selection_limits.mjs";
3
4
  const RUNNABLE_TYPES = new Set(['model', 'llm', 'forEach', 'forEachEnd']);
4
5
  const MODEL_CUSTOM_INPUT_HANDLE_SUFFIX = '-source-custom';
5
- const USER_SELECTION_MIN_SELECT = 1;
6
- const USER_SELECTION_MAX_SELECT = 100;
7
6
  const DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE = 'Select the items to continue';
8
7
  export const ASPECT_RATIO_PRESETS = [
9
8
  '21:9',
@@ -27,7 +26,12 @@ function toCelIdentifier(name) {
27
26
  return name.replaceAll('-', '_');
28
27
  }
29
28
  function escapeCELString(value) {
30
- return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
29
+ return value
30
+ .replace(/\\/g, '\\\\')
31
+ .replace(/'/g, "\\'")
32
+ .replace(/\n/g, '\\n')
33
+ .replace(/\r/g, '\\r')
34
+ .replace(/\t/g, '\\t');
31
35
  }
32
36
  function formatTextAsCELString(text) {
33
37
  return `'${escapeCELString(text)}'`;
@@ -152,6 +156,7 @@ function getForEachFlowListInputDescriptor(args) {
152
156
  listProvider?.type === 'text' ||
153
157
  listProvider?.type === 'llm' ||
154
158
  listProvider?.type === 'transformText' ||
159
+ listProvider?.type === 'joinTexts' ||
155
160
  (outputHandle?.type === 'text' && !!outputHandle.isArray);
156
161
  return isText ?
157
162
  { name: `text${inputIndex}`, type: 'string_array' }
@@ -325,17 +330,41 @@ function getIsSliceAssetsItemsSourceHandle(nodeId, sourceHandle) {
325
330
  return sourceHandle === `${nodeId}-source-items` || sourceHandle === `${nodeId}-source-assets`;
326
331
  }
327
332
  /**
328
- * Prompt Builder (transformText) binds upstream vars for CEL. Reuse the same
329
- * list-producer rules as concat/slice (`groupItems`, `sliceAssets`, `allOutputs`,
330
- * split-text, LLM "All Results", `isArray` handles): those nodes always emit CEL
331
- * lists (via concat/slice/randomSample), so Prompt Builder must bind them as
332
- * `string_array` for indexing. Scalar text (plain text nodes, forEach outputs)
333
- * stays `string`.
333
+ * True when `expression` treats `varName` as a CEL list via indexing, `size(x)`,
334
+ * or list macros (`.map` / `.filter` / …). This is a converter tolerance for
335
+ * flows that index a list producer (editor First/Last/Nth stores `x[0]` on
336
+ * `isArray` handles; agent/MCP/import flows may index without that flag). It is
337
+ * not Fawkes's preview rule: the webapp binds a list only when the handle has
338
+ * `isArray`. Intentionally narrow so Prompt Builder cannot be steered into
339
+ * list semantics through other CEL list idioms.
340
+ */
341
+ function expressionUsesListVariable(expression, varName) {
342
+ const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
343
+ return (new RegExp(`${escaped}\\s*\\[`).test(expression) ||
344
+ new RegExp(`\\bsize\\s*\\(\\s*${escaped}\\s*\\)`).test(expression) ||
345
+ new RegExp(`${escaped}\\s*\\.\\s*(map|filter|all|exists|exists_one)\\s*\\(`).test(expression));
346
+ }
347
+ /**
348
+ * Prompt Builder (transformText) binds upstream vars for CEL. List producers
349
+ * bind as `string_array` when the source handle is `isArray`, or when the
350
+ * expression indexes/uses the variable as a list (see
351
+ * `expressionUsesListVariable`). Otherwise bare refs stay `string` so the API
352
+ * collapses the list to its first item.
353
+ *
354
+ * Fawkes today omits `isArray` on Slice/Group Items on purpose: only `isArray`
355
+ * handles get an indexed insert (`x[0]`); every other list insert is bare `x`
356
+ * and must collapse to the first item. Adding `isArray` to those handles later
357
+ * requires migrating stored expressions (`x` → `x[0]`) in the same Fawkes
358
+ * release, or both preview and published flows break on `'…' + x`.
334
359
  */
335
- function getPromptBuilderInputType(targetNode, outHandle, outHandleName) {
336
- return getConcatListSourceDescriptor(targetNode, outHandle, outHandleName).inputType === 'string_array' ?
337
- 'string_array'
338
- : 'string';
360
+ function getPromptBuilderInputType(targetNode, outHandle, outHandleName, expression, varName) {
361
+ if (getConcatListSourceDescriptor(targetNode, outHandle, outHandleName).inputType !== 'string_array') {
362
+ return 'string';
363
+ }
364
+ if (outHandle?.isArray || expressionUsesListVariable(expression, varName)) {
365
+ return 'string_array';
366
+ }
367
+ return 'string';
339
368
  }
340
369
  function getConcatListSourceDescriptor(targetNode, outHandle, outHandleName) {
341
370
  const isArray = (targetNode.type === 'asset' && !!targetNode.data.isMultiple) ||
@@ -552,12 +581,43 @@ function applyAspectRatioInputs(args) {
552
581
  });
553
582
  }
554
583
  }
555
- function getForEachBodyEditorNodeIds(forEachId, forEachEndId, edges, nodes) {
584
+ function buildLoopGraph(nodes, edges) {
585
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
586
+ const isInternalForEachEdge = (e) => {
587
+ const target = nodeById.get(e.target);
588
+ return (nodeById.get(e.source)?.type === 'forEach' &&
589
+ target?.type === 'forEachEnd' &&
590
+ target.data.parentNodeId === e.source);
591
+ };
592
+ const push = (map, key, value) => {
593
+ const list = map.get(key);
594
+ if (list)
595
+ list.push(value);
596
+ else
597
+ map.set(key, [value]);
598
+ };
599
+ const graph = {
600
+ nodeById,
601
+ consumers: new Map(),
602
+ dataProviders: new Map(),
603
+ dataConsumers: new Map(),
604
+ };
605
+ for (const e of edges) {
606
+ push(graph.consumers, e.target, e.source);
607
+ if (isInternalForEachEdge(e) || nodeById.get(e.source)?.type === 'approval')
608
+ continue;
609
+ push(graph.dataProviders, e.source, e.target);
610
+ push(graph.dataConsumers, e.target, e.source);
611
+ }
612
+ return graph;
613
+ }
614
+ function getForEachBodyEditorNodeIds(forEachId, forEachEndId, edges, graph) {
556
615
  const bodyNodeIds = new Set();
557
616
  const queue = [];
558
617
  const escaped = forEachId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
618
+ const itemHandle = new RegExp(`^${escaped}-output-\\d+$`);
559
619
  for (const edge of edges) {
560
- if (edge.target === forEachId && edge.targetHandle?.match(new RegExp(`^${escaped}-output-\\d+$`))) {
620
+ if (edge.target === forEachId && edge.targetHandle && itemHandle.test(edge.targetHandle)) {
561
621
  queue.push(edge.source);
562
622
  }
563
623
  }
@@ -566,13 +626,12 @@ function getForEachBodyEditorNodeIds(forEachId, forEachEndId, edges, nodes) {
566
626
  if (nodeId === forEachEndId || bodyNodeIds.has(nodeId))
567
627
  continue;
568
628
  bodyNodeIds.add(nodeId);
569
- for (const edge of edges) {
570
- if (edge.target === nodeId && !bodyNodeIds.has(edge.source) && edge.source !== forEachEndId) {
571
- queue.push(edge.source);
572
- }
629
+ for (const consumer of graph.consumers.get(nodeId) ?? []) {
630
+ if (!bodyNodeIds.has(consumer) && consumer !== forEachEndId)
631
+ queue.push(consumer);
573
632
  }
574
633
  }
575
- addRandomizeClosureToBody(bodyNodeIds, forEachEndId, edges, nodes);
634
+ addRandomizeClosureToBody(bodyNodeIds, forEachEndId, graph);
576
635
  return bodyNodeIds;
577
636
  }
578
637
  /**
@@ -586,28 +645,34 @@ function getForEachBodyEditorNodeIds(forEachId, forEachEndId, edges, nodes) {
586
645
  * for an UNLOCKED randomize node we mutate `bodyNodeIds` in place to also include
587
646
  * that node plus the deterministic chain down to the body, so the runtime clones
588
647
  * it per iteration and each iteration re-draws. We only pull it in when *every*
589
- * path out of it lands in the body — if it also feeds a node outside the loop,
590
- * cloning it would break that external consumer, so we leave it shared.
648
+ * path out of it lands in the body or in this loop's results — if it also feeds a
649
+ * node outside the loop, cloning it would break that external consumer, so we
650
+ * leave it shared.
651
+ *
652
+ * Loop nodes (any for-each or loop end outside the body) stop both walks: a
653
+ * loop's list is computed once, before the loop runs, so neither a loop nor what
654
+ * feeds its list may be cloned with another loop's body. A for-each in its own
655
+ * body would even expand again on every iteration. A node that reaches one stays
656
+ * shared.
591
657
  */
592
- function addRandomizeClosureToBody(bodyNodeIds, forEachEndId, edges, nodes) {
593
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
658
+ function addRandomizeClosureToBody(bodyNodeIds, forEachEndId, graph) {
659
+ const isLoopNode = (id) => {
660
+ const type = graph.nodeById.get(id)?.type;
661
+ return type === 'forEach' || type === 'forEachEnd';
662
+ };
594
663
  const isUnlockedRandomize = (id) => {
595
- const data = nodeById.get(id)?.data;
596
- return data?.isRandomize === true && data.isSeedLocked !== true;
664
+ const data = graph.nodeById.get(id)?.data;
665
+ return !!data && getSliceAssetsMode(data) === 'randomize' && data.isSeedLocked !== true;
597
666
  };
598
- // edge convention: `{ source: consumer, target: provider }` — an input handle
599
- // on `source` reads an output handle on `target`.
600
- const providersOf = (id) => edges.filter((e) => e.source === id).map((e) => e.target);
601
- const consumersOf = (id) => edges.filter((e) => e.target === id).map((e) => e.source);
602
- // Every node strictly upstream of the body (its transitive input providers).
667
+ // The nodes that feed the body without going through a loop node.
603
668
  const upstream = new Set();
604
669
  const upstreamQueue = [...bodyNodeIds];
605
670
  while (upstreamQueue.length > 0) {
606
- for (const provider of providersOf(upstreamQueue.pop())) {
607
- if (provider !== forEachEndId && !bodyNodeIds.has(provider) && !upstream.has(provider)) {
608
- upstream.add(provider);
609
- upstreamQueue.push(provider);
610
- }
671
+ for (const provider of graph.dataProviders.get(upstreamQueue.pop()) ?? []) {
672
+ if (bodyNodeIds.has(provider) || upstream.has(provider) || isLoopNode(provider))
673
+ continue;
674
+ upstream.add(provider);
675
+ upstreamQueue.push(provider);
611
676
  }
612
677
  }
613
678
  for (const randomId of upstream) {
@@ -619,18 +684,17 @@ function addRandomizeClosureToBody(bodyNodeIds, forEachEndId, edges, nodes) {
619
684
  let escapesLoop = false;
620
685
  while (chainQueue.length > 0) {
621
686
  const id = chainQueue.pop();
622
- if (bodyNodeIds.has(id))
623
- continue; // reached the loop body — a valid sink
624
- chain.add(id);
625
- const consumers = consumersOf(id);
626
- if (consumers.length === 0) {
627
- escapesLoop = true; // dead-end outside the loop → do not internalize
687
+ // The body and this loop's results are where a per-iteration draw belongs.
688
+ if (bodyNodeIds.has(id) || id === forEachEndId || chain.has(id))
689
+ continue;
690
+ const consumers = graph.dataConsumers.get(id) ?? [];
691
+ // Another loop, or a dead end outside the loop: the draw must stay shared.
692
+ if (isLoopNode(id) || consumers.length === 0) {
693
+ escapesLoop = true;
628
694
  break;
629
695
  }
630
- for (const consumer of consumers) {
631
- if (!bodyNodeIds.has(consumer))
632
- chainQueue.push(consumer);
633
- }
696
+ chain.add(id);
697
+ chainQueue.push(...consumers);
634
698
  }
635
699
  if (!escapesLoop)
636
700
  for (const id of chain)
@@ -842,13 +906,14 @@ export function convertWorkflowEditorToFlow(params) {
842
906
  const forEachBodyEditorNodeIds = new Map();
843
907
  const forEachLoopFlowIds = new Map();
844
908
  const forEachFlowItemRefs = new Map();
909
+ const loopGraph = buildLoopGraph(nodes, edges);
845
910
  for (const node of sortedNodes) {
846
911
  if (node.type !== 'forEach')
847
912
  continue;
848
913
  const endNode = nodes.find((n) => n.type === 'forEachEnd' && n.data.parentNodeId === node.id);
849
914
  if (!endNode)
850
915
  continue;
851
- forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges, nodes));
916
+ forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges, loopGraph));
852
917
  forEachLoopFlowIds.set(node.id, []);
853
918
  }
854
919
  // Pre-populate video firstFrame/lastFrame transform IDs for input nodes
@@ -1200,9 +1265,10 @@ export function convertWorkflowEditorToFlow(params) {
1200
1265
  const outHandle = targetNode.data.outputHandles?.find((h) => h.id === edge.targetHandle);
1201
1266
  const outHandleName = outHandle?.name ?? 'output';
1202
1267
  const targetFlowId = nodeIdToFlowIdMap.get(targetNode.id) ?? targetNode.id;
1268
+ const varName = `${targetFlowId}_${outHandleName}`;
1203
1269
  flowInputs.push({
1204
- name: `${targetFlowId}_${outHandleName}`,
1205
- type: getPromptBuilderInputType(targetNode, outHandle, outHandleName),
1270
+ name: varName,
1271
+ type: getPromptBuilderInputType(targetNode, outHandle, outHandleName, value, varName),
1206
1272
  ref,
1207
1273
  });
1208
1274
  }
@@ -1245,6 +1311,37 @@ export function convertWorkflowEditorToFlow(params) {
1245
1311
  inputs: flowInputs.length > 0 ? flowInputs : undefined,
1246
1312
  });
1247
1313
  // ------------------------------------
1314
+ // JoinTexts -> join string list into a single string
1315
+ // ------------------------------------
1316
+ }
1317
+ else if (node.type === 'joinTexts') {
1318
+ const flowInputs = [];
1319
+ const conditionalHandleId = getConditionalInputHandleId(node.id);
1320
+ let listSourceVarName;
1321
+ for (const edge of incomingEdges) {
1322
+ if (edge.sourceHandle === conditionalHandleId)
1323
+ continue;
1324
+ const targetNode = nodes.find((n) => n.id === edge.target);
1325
+ if (!targetNode)
1326
+ continue;
1327
+ const ref = getSourceRef(edge, ctx);
1328
+ if (!ref)
1329
+ continue;
1330
+ const varName = toCelIdentifier(targetNode.id);
1331
+ flowInputs.push({ name: varName, type: 'string_array', ref });
1332
+ listSourceVarName = varName;
1333
+ }
1334
+ flowInputs.push(...collectConditionalConnectionInputs(incomingEdges, node.id, ctx));
1335
+ const delim = node.data.joinDelimiter ?? ', ';
1336
+ flow.push({
1337
+ id: flowId,
1338
+ type: 'transform',
1339
+ logic: {
1340
+ transform: `${listSourceVarName ?? '[]'}.join(${formatTextAsCELString(delim)})`,
1341
+ },
1342
+ inputs: flowInputs.length > 0 ? flowInputs : undefined,
1343
+ });
1344
+ // ------------------------------------
1248
1345
  // GroupItems -> array concatenation
1249
1346
  // ------------------------------------
1250
1347
  }
@@ -1285,7 +1382,9 @@ export function convertWorkflowEditorToFlow(params) {
1285
1382
  else {
1286
1383
  const { variables, flowInputs } = collectConcatVarsAndInputs(itemsEdges, ctx, getOutputHandleVarName);
1287
1384
  const count = clampUserSelectionCount(parseInt(String(sliceData.count ?? ''), 10) || 1);
1288
- const message = sliceData.message?.trim() || DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE;
1385
+ const message = typeof sliceData.message === 'string' ?
1386
+ sliceData.message.trim() || DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE
1387
+ : DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE;
1289
1388
  const arrayType = getUserSelectionArrayType(flowInputs);
1290
1389
  // Single array-typed source can feed user-selection directly; scalars must be wrapped.
1291
1390
  if (variables.length === 1 && variables[0]?.isArray && flowInputs[0]?.ref) {
@@ -1417,6 +1516,9 @@ export function convertWorkflowEditorToFlow(params) {
1417
1516
  fieldsUsed.add(condition.field);
1418
1517
  }
1419
1518
  }
1519
+ // CEL conditions need hyphen→underscore identifiers, but the shared input-name
1520
+ // map must keep the real workflow input keys (`text-1`, not `text_1`).
1521
+ const celFieldNameMap = new Map();
1420
1522
  const edgesIntoIfElse = edges.filter((e) => e.target === node.id);
1421
1523
  for (const fieldKey of fieldsUsed) {
1422
1524
  const resolved = resolveIfElseConditionField(fieldKey, nodes);
@@ -1429,7 +1531,7 @@ export function convertWorkflowEditorToFlow(params) {
1429
1531
  ?.data.outputHandles?.find((h) => h.id === resolved.targetHandleId)?.name ?? 'output'
1430
1532
  : 'output';
1431
1533
  const celVarName = getOutputHandleVarName(fieldNodeId, outputHandleName);
1432
- nodeIdToInputNameMap.set(fieldKey, celVarName);
1534
+ celFieldNameMap.set(fieldKey, celVarName);
1433
1535
  const edgeToIfElse = edgesIntoIfElse
1434
1536
  .filter((e) => e.source === fieldNodeId)
1435
1537
  .find((e) => {
@@ -1463,7 +1565,7 @@ export function convertWorkflowEditorToFlow(params) {
1463
1565
  }
1464
1566
  const cases = [];
1465
1567
  for (let i = 0; i < conditionBlocks.length; i++) {
1466
- const condition = conditionBlockToCEL(conditionBlocks[i], nodeIdToInputNameMap);
1568
+ const condition = conditionBlockToCEL(conditionBlocks[i], celFieldNameMap);
1467
1569
  if (condition)
1468
1570
  cases.push({ condition, value: String(i + 2) });
1469
1571
  }
@@ -1570,7 +1672,7 @@ function getSliceAssetsMode(data) {
1570
1672
  return data.isRandomize ? 'randomize' : 'default';
1571
1673
  }
1572
1674
  function clampUserSelectionCount(count) {
1573
- return Math.min(USER_SELECTION_MAX_SELECT, Math.max(USER_SELECTION_MIN_SELECT, count));
1675
+ return Math.min(USER_SELECTION_MAX_CANDIDATES, Math.max(USER_SELECTION_MIN_SELECT, count));
1574
1676
  }
1575
1677
  function getUserSelectionArrayType(flowInputs) {
1576
1678
  const hasFile = flowInputs.some((input) => input.type === 'file' || input.type === 'file_array');