@scenario-labs/sdk 2.6.0 → 2.7.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.
@@ -501,7 +501,8 @@ function getSourceRef(
501
501
  }
502
502
 
503
503
  if (targetNode.type === 'ifElse') {
504
- const handles = targetNode.data.outputHandles ?? [];
504
+ // Approval handles are UI-only and must not shift case/else indices.
505
+ const handles = (targetNode.data.outputHandles ?? []).filter((h) => h.type !== 'approval');
505
506
  const handleIndex = handles.findIndex((h) => h.id === edge.targetHandle);
506
507
  if (handleIndex !== -1) {
507
508
  const targetFlowId = nodeIdToFlowIdMap.get(targetNodeId);
@@ -572,6 +573,38 @@ function getIsSliceAssetsItemsSourceHandle(nodeId: string, sourceHandle: string
572
573
  return sourceHandle === `${nodeId}-source-items` || sourceHandle === `${nodeId}-source-assets`;
573
574
  }
574
575
 
576
+ function getConcatListSourceDescriptor(
577
+ targetNode: WorkflowEditorNode,
578
+ outHandle: WorkflowEditorHandleOutput | undefined,
579
+ outHandleName: string,
580
+ ): { isArray: boolean; inputType: string } {
581
+ const isArray =
582
+ (targetNode.type === 'asset' && !!(targetNode.data as { isMultiple?: boolean }).isMultiple) ||
583
+ targetNode.type === 'groupItems' ||
584
+ targetNode.type === 'sliceAssets' ||
585
+ targetNode.type === 'splitText' ||
586
+ (targetNode.type === 'llm' && outHandleName === 'outputs') ||
587
+ !!outHandle?.isArray ||
588
+ outHandleName === 'allOutputs';
589
+
590
+ if (!isArray) {
591
+ return {
592
+ isArray: false,
593
+ inputType: outHandle?.type === 'text' ? 'string' : 'file',
594
+ };
595
+ }
596
+
597
+ const isTextList =
598
+ targetNode.type === 'splitText' ||
599
+ (targetNode.type === 'llm' && outHandleName === 'outputs') ||
600
+ outHandle?.type === 'text';
601
+
602
+ return {
603
+ isArray: true,
604
+ inputType: isTextList ? 'string_array' : 'file_array',
605
+ };
606
+ }
607
+
575
608
  /**
576
609
  * Builds the concat variables and flow inputs for a list-concatenating node
577
610
  * (groupItems, sliceAssets in randomize mode) from its incoming edges.
@@ -595,18 +628,14 @@ function collectConcatVarsAndInputs(
595
628
  const outHandle = targetNode.data.outputHandles?.find((h) => h.id === edge.targetHandle);
596
629
  const outHandleName = outHandle?.name ?? 'output';
597
630
  const varName = getVarName(targetNode.id, outHandleName);
598
- const isArr =
599
- (targetNode.type === 'asset' && !!(targetNode.data as { isMultiple?: boolean }).isMultiple) ||
600
- targetNode.type === 'groupItems' ||
601
- targetNode.type === 'sliceAssets' ||
602
- outHandleName === 'allOutputs';
631
+ const { isArray, inputType } = getConcatListSourceDescriptor(targetNode, outHandle, outHandleName);
603
632
  const isRequired =
604
633
  inputNodeIds.includes(targetNode.id) ?
605
634
  (targetNode.type === 'asset' && !!(targetNode.data as { isRequired?: boolean }).isRequired) ||
606
635
  ['firstFrame', 'lastFrame'].includes(outHandleName)
607
636
  : true;
608
- variables.push({ name: varName, isArray: isArr, isRequired });
609
- flowInputs.push({ name: varName, type: isArr ? 'file_array' : 'file', ref });
637
+ variables.push({ name: varName, isArray, isRequired });
638
+ flowInputs.push({ name: varName, type: inputType, ref });
610
639
  }
611
640
 
612
641
  return { variables, flowInputs };
@@ -842,6 +871,7 @@ function getForEachBodyEditorNodeIds(
842
871
  forEachId: string,
843
872
  forEachEndId: string,
844
873
  edges: WorkflowEditorEdge[],
874
+ nodes: WorkflowEditorNode[],
845
875
  ): Set<string> {
846
876
  const bodyNodeIds = new Set<string>();
847
877
  const queue: string[] = [];
@@ -861,9 +891,77 @@ function getForEachBodyEditorNodeIds(
861
891
  }
862
892
  }
863
893
  }
894
+
895
+ addRandomizeClosureToBody(bodyNodeIds, forEachEndId, edges, nodes);
896
+
864
897
  return bodyNodeIds;
865
898
  }
866
899
 
900
+ /**
901
+ * The body traversal above only walks *downstream* from the loop's per-item
902
+ * outputs, so an unlocked randomize node sitting on a side branch that merely
903
+ * *feeds* the body (e.g. `randomSample(...) -> promptBuilder -> generator`) is
904
+ * left out — computed once and shared, so every iteration reuses the same draw
905
+ * (scenario-labs/phoenix#8498).
906
+ *
907
+ * A locked seed means the user wants reproducibility, so those stay shared. But
908
+ * for an UNLOCKED randomize node we mutate `bodyNodeIds` in place to also include
909
+ * that node plus the deterministic chain down to the body, so the runtime clones
910
+ * it per iteration and each iteration re-draws. We only pull it in when *every*
911
+ * path out of it lands in the body — if it also feeds a node outside the loop,
912
+ * cloning it would break that external consumer, so we leave it shared.
913
+ */
914
+ function addRandomizeClosureToBody(
915
+ bodyNodeIds: Set<string>,
916
+ forEachEndId: string,
917
+ edges: WorkflowEditorEdge[],
918
+ nodes: WorkflowEditorNode[],
919
+ ): void {
920
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
921
+ const isUnlockedRandomize = (id: string): boolean => {
922
+ const data = nodeById.get(id)?.data as { isRandomize?: boolean; isSeedLocked?: boolean } | undefined;
923
+ return data?.isRandomize === true && data.isSeedLocked !== true;
924
+ };
925
+ // edge convention: `{ source: consumer, target: provider }` — an input handle
926
+ // on `source` reads an output handle on `target`.
927
+ const providersOf = (id: string): string[] => edges.filter((e) => e.source === id).map((e) => e.target);
928
+ const consumersOf = (id: string): string[] => edges.filter((e) => e.target === id).map((e) => e.source);
929
+
930
+ // Every node strictly upstream of the body (its transitive input providers).
931
+ const upstream = new Set<string>();
932
+ const upstreamQueue = [...bodyNodeIds];
933
+ while (upstreamQueue.length > 0) {
934
+ for (const provider of providersOf(upstreamQueue.pop()!)) {
935
+ if (provider !== forEachEndId && !bodyNodeIds.has(provider) && !upstream.has(provider)) {
936
+ upstream.add(provider);
937
+ upstreamQueue.push(provider);
938
+ }
939
+ }
940
+ }
941
+
942
+ for (const randomId of upstream) {
943
+ if (!isUnlockedRandomize(randomId)) continue;
944
+ // Walk downstream from the randomize node, stopping at the body frontier.
945
+ const chain = new Set<string>();
946
+ const chainQueue = [randomId];
947
+ let escapesLoop = false;
948
+ while (chainQueue.length > 0) {
949
+ const id = chainQueue.pop()!;
950
+ if (bodyNodeIds.has(id)) continue; // reached the loop body — a valid sink
951
+ chain.add(id);
952
+ const consumers = consumersOf(id);
953
+ if (consumers.length === 0) {
954
+ escapesLoop = true; // dead-end outside the loop → do not internalize
955
+ break;
956
+ }
957
+ for (const consumer of consumers) {
958
+ if (!bodyNodeIds.has(consumer)) chainQueue.push(consumer);
959
+ }
960
+ }
961
+ if (!escapesLoop) for (const id of chain) bodyNodeIds.add(id);
962
+ }
963
+ }
964
+
867
965
  function sortNodesByInputKeys(nodes: WorkflowEditorNode[], inputKeys: string[]): WorkflowEditorNode[] {
868
966
  return [...nodes].sort((a, b) => {
869
967
  const ai = inputKeys.indexOf(a.id);
@@ -1145,7 +1243,7 @@ export function convertWorkflowEditorToFlow(
1145
1243
  (n) => n.type === 'forEachEnd' && (n.data as { parentNodeId?: string }).parentNodeId === node.id,
1146
1244
  );
1147
1245
  if (!endNode) continue;
1148
- forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges));
1246
+ forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges, nodes));
1149
1247
  forEachLoopFlowIds.set(node.id, []);
1150
1248
  }
1151
1249
 
@@ -1331,21 +1429,16 @@ export function convertWorkflowEditorToFlow(
1331
1429
  targetInputType = outHandle?.type === 'text' ? 'string' : 'file';
1332
1430
  } else {
1333
1431
  targetNodeKey = getOutputHandleVarName(edge.target, targetOutputHandleName);
1334
- targetInputType =
1335
- (
1336
- (targetNode?.type === 'asset' &&
1337
- !!(targetNode.data as { isMultiple?: boolean }).isMultiple) ||
1338
- targetNode?.type === 'groupItems' ||
1339
- targetNode?.type === 'sliceAssets' ||
1340
- targetOutputHandleName === 'allOutputs'
1341
- ) ?
1342
- 'file_array'
1343
- : targetNode?.type === 'llm' && targetOutputHandleName === 'outputs' ? 'string_array'
1344
- : targetNode?.type === 'asset' || targetNode?.type === 'model' ? 'file'
1345
- : 'string';
1432
+ if (!targetNode) continue;
1433
+ const descriptor = getConcatListSourceDescriptor(
1434
+ targetNode,
1435
+ targetOutputHandle,
1436
+ targetOutputHandleName,
1437
+ );
1438
+ targetInputType = descriptor.inputType;
1346
1439
  }
1347
1440
 
1348
- const isArr = targetInputType === 'file_array';
1441
+ const isArr = targetInputType === 'file_array' || targetInputType === 'string_array';
1349
1442
  const isRequired =
1350
1443
  (targetNode?.type === 'asset' &&
1351
1444
  !!(targetNode.data as { isRequired?: boolean }).isRequired) ||
@@ -1635,7 +1728,9 @@ export function convertWorkflowEditorToFlow(
1635
1728
  const ref = targetNode ? getSourceRef(assetsEdge, ctx) : undefined;
1636
1729
  if (targetNode && ref) {
1637
1730
  const outHandle = targetNode.data.outputHandles?.find((h) => h.id === assetsEdge.targetHandle);
1638
- const varName = getOutputHandleVarName(targetNode.id, outHandle?.name ?? 'output');
1731
+ const outHandleName = outHandle?.name ?? 'output';
1732
+ const varName = getOutputHandleVarName(targetNode.id, outHandleName);
1733
+ const { inputType } = getConcatListSourceDescriptor(targetNode, outHandle, outHandleName);
1639
1734
  const from = parseInt(String(sliceData.from ?? ''), 10) || 0;
1640
1735
  const count = parseInt(String(sliceData.count ?? ''), 10) || 0;
1641
1736
  const transform =
@@ -1643,7 +1738,7 @@ export function convertWorkflowEditorToFlow(
1643
1738
  `slice(${varName}, (${varName}).size() + ${from}, (${varName}).size() + ${from + count})`
1644
1739
  : `slice(${varName}, ${from}, ${from + count})`;
1645
1740
  const flowInputs: WorkflowEditorFlowInput[] = [
1646
- { name: varName, type: 'file_array', ref },
1741
+ { name: varName, type: inputType, ref },
1647
1742
  ...collectConditionalConnectionInputs(incomingEdges, node.id, ctx),
1648
1743
  ];
1649
1744
  flow.push({
@@ -5058,7 +5058,8 @@ export namespace ExampleListResponse {
5058
5058
 
5059
5059
  export interface ExampleUpdateParams {
5060
5060
  /**
5061
- * Body param: The list of asset ids to use as examples of the model
5061
+ * Body param: The list of asset ids to use as examples of the model. Maximum of
5062
+ * 150 asset ids.
5062
5063
  */
5063
5064
  assetIds: Array<string>;
5064
5065
 
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '2.6.0'; // x-release-please-version
1
+ export const VERSION = '2.7.0'; // x-release-please-version
package/version.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "2.6.0";
1
+ export declare const VERSION = "2.7.0";
2
2
  //# sourceMappingURL=version.d.mts.map
package/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "2.6.0";
1
+ export declare const VERSION = "2.7.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '2.6.0'; // x-release-please-version
4
+ exports.VERSION = '2.7.0'; // x-release-please-version
5
5
  //# sourceMappingURL=version.js.map
package/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = '2.6.0'; // x-release-please-version
1
+ export const VERSION = '2.7.0'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map