@scenario-labs/sdk 2.5.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.
@@ -57,6 +57,7 @@ export type WorkflowEditorNode =
57
57
  count?: number | string;
58
58
  isRandomize?: boolean;
59
59
  randomSeed?: string;
60
+ isSeedLocked?: boolean;
60
61
  value?: string;
61
62
  };
62
63
  }
@@ -500,7 +501,8 @@ function getSourceRef(
500
501
  }
501
502
 
502
503
  if (targetNode.type === 'ifElse') {
503
- 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');
504
506
  const handleIndex = handles.findIndex((h) => h.id === edge.targetHandle);
505
507
  if (handleIndex !== -1) {
506
508
  const targetFlowId = nodeIdToFlowIdMap.get(targetNodeId);
@@ -571,6 +573,38 @@ function getIsSliceAssetsItemsSourceHandle(nodeId: string, sourceHandle: string
571
573
  return sourceHandle === `${nodeId}-source-items` || sourceHandle === `${nodeId}-source-assets`;
572
574
  }
573
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
+
574
608
  /**
575
609
  * Builds the concat variables and flow inputs for a list-concatenating node
576
610
  * (groupItems, sliceAssets in randomize mode) from its incoming edges.
@@ -594,18 +628,14 @@ function collectConcatVarsAndInputs(
594
628
  const outHandle = targetNode.data.outputHandles?.find((h) => h.id === edge.targetHandle);
595
629
  const outHandleName = outHandle?.name ?? 'output';
596
630
  const varName = getVarName(targetNode.id, outHandleName);
597
- const isArr =
598
- (targetNode.type === 'asset' && !!(targetNode.data as { isMultiple?: boolean }).isMultiple) ||
599
- targetNode.type === 'groupItems' ||
600
- targetNode.type === 'sliceAssets' ||
601
- outHandleName === 'allOutputs';
631
+ const { isArray, inputType } = getConcatListSourceDescriptor(targetNode, outHandle, outHandleName);
602
632
  const isRequired =
603
633
  inputNodeIds.includes(targetNode.id) ?
604
634
  (targetNode.type === 'asset' && !!(targetNode.data as { isRequired?: boolean }).isRequired) ||
605
635
  ['firstFrame', 'lastFrame'].includes(outHandleName)
606
636
  : true;
607
- variables.push({ name: varName, isArray: isArr, isRequired });
608
- 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 });
609
639
  }
610
640
 
611
641
  return { variables, flowInputs };
@@ -841,6 +871,7 @@ function getForEachBodyEditorNodeIds(
841
871
  forEachId: string,
842
872
  forEachEndId: string,
843
873
  edges: WorkflowEditorEdge[],
874
+ nodes: WorkflowEditorNode[],
844
875
  ): Set<string> {
845
876
  const bodyNodeIds = new Set<string>();
846
877
  const queue: string[] = [];
@@ -860,9 +891,77 @@ function getForEachBodyEditorNodeIds(
860
891
  }
861
892
  }
862
893
  }
894
+
895
+ addRandomizeClosureToBody(bodyNodeIds, forEachEndId, edges, nodes);
896
+
863
897
  return bodyNodeIds;
864
898
  }
865
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
+
866
965
  function sortNodesByInputKeys(nodes: WorkflowEditorNode[], inputKeys: string[]): WorkflowEditorNode[] {
867
966
  return [...nodes].sort((a, b) => {
868
967
  const ai = inputKeys.indexOf(a.id);
@@ -1144,7 +1243,7 @@ export function convertWorkflowEditorToFlow(
1144
1243
  (n) => n.type === 'forEachEnd' && (n.data as { parentNodeId?: string }).parentNodeId === node.id,
1145
1244
  );
1146
1245
  if (!endNode) continue;
1147
- forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges));
1246
+ forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges, nodes));
1148
1247
  forEachLoopFlowIds.set(node.id, []);
1149
1248
  }
1150
1249
 
@@ -1330,21 +1429,16 @@ export function convertWorkflowEditorToFlow(
1330
1429
  targetInputType = outHandle?.type === 'text' ? 'string' : 'file';
1331
1430
  } else {
1332
1431
  targetNodeKey = getOutputHandleVarName(edge.target, targetOutputHandleName);
1333
- targetInputType =
1334
- (
1335
- (targetNode?.type === 'asset' &&
1336
- !!(targetNode.data as { isMultiple?: boolean }).isMultiple) ||
1337
- targetNode?.type === 'groupItems' ||
1338
- targetNode?.type === 'sliceAssets' ||
1339
- targetOutputHandleName === 'allOutputs'
1340
- ) ?
1341
- 'file_array'
1342
- : targetNode?.type === 'llm' && targetOutputHandleName === 'outputs' ? 'string_array'
1343
- : targetNode?.type === 'asset' || targetNode?.type === 'model' ? 'file'
1344
- : 'string';
1432
+ if (!targetNode) continue;
1433
+ const descriptor = getConcatListSourceDescriptor(
1434
+ targetNode,
1435
+ targetOutputHandle,
1436
+ targetOutputHandleName,
1437
+ );
1438
+ targetInputType = descriptor.inputType;
1345
1439
  }
1346
1440
 
1347
- const isArr = targetInputType === 'file_array';
1441
+ const isArr = targetInputType === 'file_array' || targetInputType === 'string_array';
1348
1442
  const isRequired =
1349
1443
  (targetNode?.type === 'asset' &&
1350
1444
  !!(targetNode.data as { isRequired?: boolean }).isRequired) ||
@@ -1599,6 +1693,7 @@ export function convertWorkflowEditorToFlow(
1599
1693
  count?: number | string;
1600
1694
  isRandomize?: boolean;
1601
1695
  randomSeed?: string;
1696
+ isSeedLocked?: boolean;
1602
1697
  };
1603
1698
  const conditionalHandleId = getConditionalInputHandleId(node.id);
1604
1699
  const itemsEdges = incomingEdges.filter(
@@ -1612,8 +1707,11 @@ export function convertWorkflowEditorToFlow(
1612
1707
  flowInputs.push(...collectConditionalConnectionInputs(incomingEdges, node.id, ctx));
1613
1708
 
1614
1709
  const count = Math.max(1, parseInt(String(sliceData.count ?? ''), 10) || 1);
1615
- const seed = parseInt(String(sliceData.randomSeed ?? ''), 10) || 0;
1616
- const transform = `randomSample(${buildConcatArrayTransform(variables)}, ${count}, ${seed})`;
1710
+ const listExpr = buildConcatArrayTransform(variables);
1711
+ const transform =
1712
+ sliceData.isSeedLocked === true ?
1713
+ `randomSample(${listExpr}, ${count}, ${parseInt(String(sliceData.randomSeed ?? ''), 10) || 0})`
1714
+ : `randomSample(${listExpr}, ${count})`;
1617
1715
 
1618
1716
  flow.push({
1619
1717
  id: flowId,
@@ -1630,7 +1728,9 @@ export function convertWorkflowEditorToFlow(
1630
1728
  const ref = targetNode ? getSourceRef(assetsEdge, ctx) : undefined;
1631
1729
  if (targetNode && ref) {
1632
1730
  const outHandle = targetNode.data.outputHandles?.find((h) => h.id === assetsEdge.targetHandle);
1633
- 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);
1634
1734
  const from = parseInt(String(sliceData.from ?? ''), 10) || 0;
1635
1735
  const count = parseInt(String(sliceData.count ?? ''), 10) || 0;
1636
1736
  const transform =
@@ -1638,7 +1738,7 @@ export function convertWorkflowEditorToFlow(
1638
1738
  `slice(${varName}, (${varName}).size() + ${from}, (${varName}).size() + ${from + count})`
1639
1739
  : `slice(${varName}, ${from}, ${from + count})`;
1640
1740
  const flowInputs: WorkflowEditorFlowInput[] = [
1641
- { name: varName, type: 'file_array', ref },
1741
+ { name: varName, type: inputType, ref },
1642
1742
  ...collectConditionalConnectionInputs(incomingEdges, node.id, ctx),
1643
1743
  ];
1644
1744
  flow.push({
@@ -11354,6 +11354,8 @@ export interface GeneratePromptParams {
11354
11354
  * - in `contextual` mode, images condition prompt generation by using their actual
11355
11355
  * descriptions as context
11356
11356
  * - in all other modes, it supersedes the `modelId` parameter when provided.
11357
+ * - `contextual-v2` (and modes aliased to it) accepts up to 15 references; the
11358
+ * other modes accept up to 5.
11357
11359
  */
11358
11360
  images?: Array<string>;
11359
11361
 
@@ -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.5.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.5.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.5.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.5.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.5.0'; // x-release-please-version
1
+ export const VERSION = '2.7.0'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map