@acpus/runtime 0.6.1 → 0.6.3

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.
@@ -247,9 +247,10 @@ export class WorkflowInterpreter {
247
247
  }
248
248
  break;
249
249
  case "parallel":
250
- (node.children ?? []).forEach((child, index) => {
251
- const branchDynamic = nestedParallelBranchDynamic(dynamic, index);
252
- this.replayNode(child, ctx, runId, branchDynamic, keyPrefix, recorded, reached, evaluator);
250
+ (node.branches ?? []).forEach((branch) => {
251
+ const branchDynamic = nestedParallelBranchDynamic(dynamic, branch.id);
252
+ const branchCtx = { ...ctx, steps: { ...ctx.steps } };
253
+ this.replayNode(branch.child, branchCtx, runId, branchDynamic, keyPrefix, recorded, reached, evaluator);
253
254
  });
254
255
  break;
255
256
  case "fanout": {
@@ -258,7 +259,7 @@ export class WorkflowInterpreter {
258
259
  items.forEach((item, index) => {
259
260
  const keyCtx = { ...ctx, item, item_index: index };
260
261
  const itemId = this.extractItemId(item, node.metadata.key, index, keyCtx, evaluator);
261
- const itemDynamic = { ...dynamic, fanoutItemId: itemId, laneId: String(index) };
262
+ const itemDynamic = appendDynamicFrame(dynamic, { fanoutItemId: itemId, laneId: String(index) });
262
263
  const itemCtx = { ...keyCtx, steps: { ...ctx.steps }, item_id: itemId };
263
264
  for (const child of node.children ?? []) {
264
265
  this.replayNode(child, itemCtx, runId, itemDynamic, keyPrefix, recorded, reached, evaluator);
@@ -270,9 +271,7 @@ export class WorkflowInterpreter {
270
271
  for (const branch of node.branches ?? []) {
271
272
  const taken = !branch.when || Boolean(evaluator.evaluateExpression(branch.when, ctx));
272
273
  if (taken) {
273
- for (const child of branch.children) {
274
- this.replayNode(child, ctx, runId, dynamic, keyPrefix, recorded, reached, evaluator);
275
- }
274
+ this.replayNode(branch.child, ctx, runId, dynamic, keyPrefix, recorded, reached, evaluator);
276
275
  break;
277
276
  }
278
277
  }
@@ -283,7 +282,7 @@ export class WorkflowInterpreter {
283
282
  let lastOutput;
284
283
  for (let iter = 0; iter < maxIterations; iter++) {
285
284
  const loopCtx = { ...ctx, loop: { iter, last: lastOutput } };
286
- const loopDynamic = { ...dynamic, loopRound: iter };
285
+ const loopDynamic = appendDynamicFrame(dynamic, { loopRound: iter });
287
286
  if (untilExpr && iter > 0 && evaluator.evaluateExpression(untilExpr, loopCtx))
288
287
  break;
289
288
  // Only descend while this round's children were actually recorded.
@@ -294,7 +293,8 @@ export class WorkflowInterpreter {
294
293
  if (reached.size > before)
295
294
  anyChildReached = true;
296
295
  const childKey = withNodeKeyPrefix(keyPrefix, resolveNodeKey(child.keyTemplate, loopDynamic));
297
- lastOutput = recorded.get(childKey)?.output ?? lastOutput;
296
+ const recordedOutput = recorded.get(childKey)?.output;
297
+ lastOutput = recordedOutput !== undefined ? primaryOutput(recordedOutput) : lastOutput;
298
298
  }
299
299
  if (!anyChildReached)
300
300
  break;
@@ -335,13 +335,14 @@ export class WorkflowInterpreter {
335
335
  // transition. `attempt` is incremented by executeNode.
336
336
  this.runControl.resetNodeForRetry(runId, state);
337
337
  const ctx = this.buildContext(ir, input, runId);
338
- this.populateStepOutputs(runId, ctx);
338
+ const retryTarget = this.hydrateContextForNodeRetry(ir.root, ctx, runId, nodeKey, {});
339
339
  // Restore the parent dynamic value-context captured at first execution.
340
- this.restoreDynamicContext(ctx, state.dynamicContext);
340
+ const retryCtx = retryTarget?.ctx ?? ctx;
341
+ this.restoreDynamicContext(retryCtx, state.dynamicContext);
341
342
  // Retry re-runs the executable as a continuation: agents keep the same acpx
342
343
  // session name and receive the fixed continuation prompt. The original full
343
344
  // node key is preserved for stable identity.
344
- await this.executeNode(node, ctx, runId, {}, undefined, true, nodeKey);
345
+ await this.executeNode(node, retryCtx, runId, retryTarget?.dynamic ?? {}, undefined, true, nodeKey);
345
346
  }
346
347
  // ─── Run-level controls ─────────────────────────────────────────
347
348
  /**
@@ -565,18 +566,35 @@ export class WorkflowInterpreter {
565
566
  // ─── Kind-specific execution ───────────────────────────────────
566
567
  async executePipeline(node, ctx, runId, dynamic, keyPrefix) {
567
568
  const children = node.children ?? [];
569
+ if (node.metadata.implicit === true) {
570
+ for (const child of children) {
571
+ try {
572
+ await this.executeNode(child, ctx, runId, dynamic, keyPrefix);
573
+ }
574
+ catch (error) {
575
+ if (error instanceof ScopeCompleted)
576
+ return { output: { ...ctx.steps } };
577
+ throw error;
578
+ }
579
+ }
580
+ return { output: { ...ctx.steps } };
581
+ }
582
+ const frame = { ...ctx, steps: { ...ctx.steps } };
583
+ let lastOutput;
568
584
  for (const child of children) {
569
585
  try {
570
- await this.executeNode(child, ctx, runId, dynamic, keyPrefix);
586
+ lastOutput = await this.executeNode(child, frame, runId, dynamic, keyPrefix);
571
587
  }
572
588
  catch (error) {
573
589
  if (error instanceof ScopeCompleted)
574
- return { output: error.output };
590
+ return { output: primaryOutput(error.output) };
575
591
  throw error;
576
592
  }
577
593
  }
578
- // Pipeline output: map of step outputs
579
- return { output: { ...ctx.steps } };
594
+ if (isRecord(node.metadata.outputs)) {
595
+ return { output: evaluateWorkflowOutputs({ outputs: node.metadata.outputs }, frame, this.evaluator) };
596
+ }
597
+ return { output: primaryOutput(lastOutput) };
580
598
  }
581
599
  async executeAgent(node, ctx, runId, signal, nodeKey, continuation) {
582
600
  const retry = node.metadata.retry;
@@ -809,21 +827,20 @@ export class WorkflowInterpreter {
809
827
  state.renderedPrompt = persisted?.renderedPrompt ?? state.renderedPrompt;
810
828
  }
811
829
  async executeParallel(node, ctx, runId, dynamic, nodeKey, keyPrefix) {
812
- const children = node.children ?? [];
830
+ const branches = node.branches ?? [];
813
831
  const maxConcurrency = node.metadata.max_concurrency ?? this.maxConcurrency;
814
832
  const join = node.metadata.join ?? "all";
815
833
  if (join === "all") {
816
- children.forEach((child, index) => {
817
- this.materializePendingNode(runId, child, ctx, nestedParallelBranchDynamic(dynamic, index), keyPrefix);
834
+ branches.forEach((branch) => {
835
+ this.materializePendingNode(runId, branch.child, ctx, nestedParallelBranchDynamic(dynamic, branch.id), keyPrefix);
818
836
  });
819
837
  }
820
838
  const limit = pLimit(maxConcurrency);
821
- // Each branch resolves to { child, output } so race can identify the winner.
822
- const branchPromises = children.map((child, index) => limit(async () => {
823
- const branchDynamic = nestedParallelBranchDynamic(dynamic, index);
839
+ const branchPromises = branches.map((branch) => limit(async () => {
840
+ const branchDynamic = nestedParallelBranchDynamic(dynamic, branch.id);
824
841
  let output;
825
842
  try {
826
- output = await this.executeNode(child, { ...ctx, steps: { ...ctx.steps } }, runId, branchDynamic, keyPrefix);
843
+ output = await this.executeNode(branch.child, { ...ctx, steps: { ...ctx.steps } }, runId, branchDynamic, keyPrefix);
827
844
  }
828
845
  catch (error) {
829
846
  if (error instanceof ScopeCompleted) {
@@ -833,7 +850,7 @@ export class WorkflowInterpreter {
833
850
  throw error;
834
851
  }
835
852
  }
836
- return { child, output };
853
+ return { branch, output };
837
854
  }));
838
855
  if (join === "race") {
839
856
  try {
@@ -841,8 +858,7 @@ export class WorkflowInterpreter {
841
858
  // consumed so their later rejection doesn't surface as unhandled.
842
859
  const winner = await Promise.race(branchPromises);
843
860
  branchPromises.forEach((p) => void p.catch(() => undefined));
844
- const mapOutput = { [winner.child.id]: winner.output };
845
- ctx.steps[winner.child.id] = winner.output;
861
+ const mapOutput = { [winner.branch.id]: primaryOutput(winner.output) };
846
862
  return { output: mapOutput };
847
863
  }
848
864
  catch (error) {
@@ -871,9 +887,8 @@ export class WorkflowInterpreter {
871
887
  throw error;
872
888
  }
873
889
  const mapOutput = {};
874
- for (const { child, output } of results) {
875
- mapOutput[child.id] = output;
876
- ctx.steps[child.id] = output;
890
+ for (const { branch, output } of results) {
891
+ mapOutput[branch.id] = primaryOutput(output);
877
892
  }
878
893
  return { output: mapOutput };
879
894
  }
@@ -887,7 +902,10 @@ export class WorkflowInterpreter {
887
902
  const join = node.metadata.join ?? "all";
888
903
  const quorum = node.metadata.quorum;
889
904
  const successCriteria = node.metadata.success_criteria;
890
- const children = node.children ?? [];
905
+ const body = node.children?.[0];
906
+ if (!body) {
907
+ throw new Error(`fanout node ${node.id} missing body pipeline`);
908
+ }
891
909
  const limit = pLimit(maxConcurrency);
892
910
  // Success target (how many lanes must succeed). Default follows join.
893
911
  const defaultMinSuccess = join === "race" ? 1 : join === "quorum" ? (quorum ?? 1) : items.length;
@@ -895,7 +913,7 @@ export class WorkflowInterpreter {
895
913
  const lanePlan = items.map((item, index) => {
896
914
  const keyCtx = { ...ctx, item, item_index: index };
897
915
  const itemId = this.extractItemId(item, node.metadata.key, index, keyCtx, this.evaluator);
898
- const laneDynamic = { fanoutItemId: itemId, laneId: String(index) };
916
+ const laneDynamic = appendDynamicFrame({}, { fanoutItemId: itemId, laneId: String(index) });
899
917
  return {
900
918
  itemId,
901
919
  keyCtx,
@@ -904,10 +922,8 @@ export class WorkflowInterpreter {
904
922
  });
905
923
  if (join === "all") {
906
924
  for (const lane of lanePlan) {
907
- const itemDynamic = { ...dynamic, ...lane.itemDynamic };
908
- for (const child of children) {
909
- this.materializePendingNode(runId, child, lane.keyCtx, itemDynamic, keyPrefix);
910
- }
925
+ const itemDynamic = appendDynamicFrames(dynamic, lane.itemDynamic);
926
+ this.materializePendingNode(runId, body, lane.keyCtx, itemDynamic, keyPrefix);
911
927
  }
912
928
  }
913
929
  // Fail-fast: once enough lanes have failed that the success target is
@@ -922,27 +938,19 @@ export class WorkflowInterpreter {
922
938
  // not reject) so the join/min_success logic can run. A NodeAbortedError
923
939
  // (operator pause/cancel) re-throws so it propagates to the parent.
924
940
  const lanePromises = lanePlan.map((lane) => limit(async () => {
925
- const itemDynamic = { ...dynamic, ...lane.itemDynamic };
941
+ const itemDynamic = appendDynamicFrames(dynamic, lane.itemDynamic);
926
942
  const itemCtx = {
927
943
  ...lane.keyCtx,
928
944
  steps: { ...ctx.steps },
929
945
  item_id: lane.itemId
930
946
  };
931
947
  try {
932
- let laneOutput;
933
- for (const child of children) {
934
- try {
935
- laneOutput = await this.executeNode(child, itemCtx, runId, itemDynamic, keyPrefix);
936
- }
937
- catch (error) {
938
- if (error instanceof ScopeCompleted)
939
- return { ok: true, output: error.output };
940
- throw error;
941
- }
942
- }
943
- return { ok: true, output: laneOutput };
948
+ const laneOutput = await this.executeNode(body, itemCtx, runId, itemDynamic, keyPrefix);
949
+ return { ok: true, output: primaryOutput(laneOutput) };
944
950
  }
945
951
  catch (error) {
952
+ if (error instanceof ScopeCompleted)
953
+ return { ok: true, output: primaryOutput(error.output) };
946
954
  if (error instanceof NodeAbortedError) {
947
955
  throw error;
948
956
  }
@@ -1008,37 +1016,16 @@ export class WorkflowInterpreter {
1008
1016
  async executeSwitch(node, ctx, runId, dynamic, keyPrefix) {
1009
1017
  const branches = node.branches ?? [];
1010
1018
  for (const branch of branches) {
1011
- if (branch.when) {
1012
- const matches = this.evaluator.evaluateExpression(branch.when, ctx);
1013
- if (matches) {
1014
- let lastOutput;
1015
- for (const child of branch.children) {
1016
- try {
1017
- lastOutput = await this.executeNode(child, ctx, runId, dynamic, keyPrefix);
1018
- }
1019
- catch (error) {
1020
- if (error instanceof ScopeCompleted)
1021
- return { output: error.output };
1022
- throw error;
1023
- }
1024
- }
1025
- return { output: lastOutput };
1026
- }
1019
+ if (branch.when && !this.evaluator.evaluateExpression(branch.when, ctx))
1020
+ continue;
1021
+ // Default branch has no condition and is reached only after earlier cases fail.
1022
+ try {
1023
+ return { output: primaryOutput(await this.executeNode(branch.child, ctx, runId, dynamic, keyPrefix)) };
1027
1024
  }
1028
- else {
1029
- // Default branch (no when condition)
1030
- let lastOutput;
1031
- for (const child of branch.children) {
1032
- try {
1033
- lastOutput = await this.executeNode(child, ctx, runId, dynamic, keyPrefix);
1034
- }
1035
- catch (error) {
1036
- if (error instanceof ScopeCompleted)
1037
- return { output: error.output };
1038
- throw error;
1039
- }
1040
- }
1041
- return { output: lastOutput };
1025
+ catch (error) {
1026
+ if (error instanceof ScopeCompleted)
1027
+ return { output: primaryOutput(error.output) };
1028
+ throw error;
1042
1029
  }
1043
1030
  }
1044
1031
  throw new Error(`Switch node ${node.id}: no branch matched and no default`);
@@ -1046,35 +1033,33 @@ export class WorkflowInterpreter {
1046
1033
  async executeLoop(node, ctx, runId, dynamic, keyPrefix) {
1047
1034
  const untilExpr = node.metadata.until;
1048
1035
  const maxIterations = node.metadata.max_iterations ?? 100;
1049
- const children = node.children ?? [];
1036
+ const body = node.children?.[0];
1037
+ if (!body) {
1038
+ throw new Error(`loop node ${node.id} missing body pipeline`);
1039
+ }
1050
1040
  let lastOutput;
1051
1041
  for (let iter = 0; iter < maxIterations; iter++) {
1052
1042
  const loopCtx = {
1053
1043
  ...ctx,
1054
1044
  loop: { iter, last: lastOutput }
1055
1045
  };
1056
- const loopDynamic = { ...dynamic, loopRound: iter };
1046
+ const loopDynamic = appendDynamicFrame(dynamic, { loopRound: iter });
1057
1047
  // Check until condition (skip on first iteration)
1058
1048
  if (untilExpr && iter > 0) {
1059
1049
  const done = this.evaluator.evaluateExpression(untilExpr, loopCtx);
1060
1050
  if (done)
1061
1051
  break;
1062
1052
  }
1063
- // Execute loop body
1064
- for (const child of children) {
1065
- try {
1066
- lastOutput = await this.executeNode(child, loopCtx, runId, loopDynamic, keyPrefix);
1067
- }
1068
- catch (error) {
1069
- if (error instanceof ScopeCompleted)
1070
- return { output: error.output };
1071
- throw error;
1072
- }
1053
+ try {
1054
+ lastOutput = primaryOutput(await this.executeNode(body, loopCtx, runId, loopDynamic, keyPrefix));
1055
+ }
1056
+ catch (error) {
1057
+ if (error instanceof ScopeCompleted)
1058
+ return { output: primaryOutput(error.output) };
1059
+ throw error;
1073
1060
  }
1074
1061
  }
1075
- // outputMerge: "last" — lastOutput is already an { output: ... } envelope
1076
- // from executeNode, so return it directly (no extra wrapping).
1077
- return lastOutput ?? { output: {} };
1062
+ return { output: lastOutput ?? {} };
1078
1063
  }
1079
1064
  executeGuard(node, ctx) {
1080
1065
  const when = node.metadata.when;
@@ -1245,12 +1230,45 @@ export class WorkflowInterpreter {
1245
1230
  buildContext(ir, input, runId) {
1246
1231
  return { input, steps: {}, workflow: buildWorkflowExpressionContext(ir), run_id: runId };
1247
1232
  }
1248
- populateStepOutputs(runId, ctx) {
1249
- for (const nodeState of this.store.listNodeStates(runId)) {
1250
- if (nodeState.state === "completed" && nodeState.output !== undefined) {
1251
- ctx.steps[nodeState.nodeId] = nodeState.output;
1233
+ hydrateContextForNodeRetry(node, ctx, runId, targetNodeKey, dynamic, keyPrefix) {
1234
+ const nodeKey = withNodeKeyPrefix(keyPrefix, resolveNodeKey(node.keyTemplate, dynamic));
1235
+ if (nodeKey === targetNodeKey)
1236
+ return { ctx, dynamic };
1237
+ const state = this.store.readNodeState(runId, nodeKey);
1238
+ if (state?.state === "completed" && state.output !== undefined && node.metadata.implicit !== true) {
1239
+ ctx.steps[node.id] = state.output;
1240
+ return undefined;
1241
+ }
1242
+ if (node.kind === "pipeline") {
1243
+ const frame = node.metadata.implicit === true ? ctx : { ...ctx, steps: { ...ctx.steps } };
1244
+ for (const child of node.children ?? []) {
1245
+ const found = this.hydrateContextForNodeRetry(child, frame, runId, targetNodeKey, dynamic, keyPrefix);
1246
+ if (found)
1247
+ return found;
1252
1248
  }
1249
+ return undefined;
1253
1250
  }
1251
+ if (node.kind === "parallel") {
1252
+ for (const branch of node.branches ?? []) {
1253
+ const branchCtx = { ...ctx, steps: { ...ctx.steps } };
1254
+ const branchDynamic = nestedParallelBranchDynamic(dynamic, branch.id);
1255
+ const found = this.hydrateContextForNodeRetry(branch.child, branchCtx, runId, targetNodeKey, branchDynamic, keyPrefix);
1256
+ if (found)
1257
+ return found;
1258
+ }
1259
+ return undefined;
1260
+ }
1261
+ for (const child of node.children ?? []) {
1262
+ const found = this.hydrateContextForNodeRetry(child, ctx, runId, targetNodeKey, dynamic, keyPrefix);
1263
+ if (found)
1264
+ return found;
1265
+ }
1266
+ for (const branch of node.branches ?? []) {
1267
+ const found = this.hydrateContextForNodeRetry(branch.child, ctx, runId, targetNodeKey, dynamic, keyPrefix);
1268
+ if (found)
1269
+ return found;
1270
+ }
1271
+ return undefined;
1254
1272
  }
1255
1273
  /** Extract the parent dynamic value-context (fanout item / loop round) from a context, if any. */
1256
1274
  captureDynamicContext(ctx) {
@@ -1288,33 +1306,21 @@ export class WorkflowInterpreter {
1288
1306
  ctx.loop = snapshot.loop;
1289
1307
  }
1290
1308
  findNodeByKey(root, nodeKey) {
1291
- // Node keys contain dynamic dimensions (e.g. "workflow/mapped/item:0/lane:0")
1292
- // but IR nodes have template keys without dynamics. Match by extracting
1293
- // the nodePath from the resolved key and comparing to the template.
1294
- // The nodePath is always the prefix before any dynamic segments.
1295
- // We match by the IR node's id since that's unique and stable.
1296
- const nodeId = this.extractNodeIdFromKey(nodeKey);
1297
- return this.findNodeById(root, nodeId);
1298
- }
1299
- /** Extract the step ID from a resolved node key. The ID is the last path segment before dynamic dims. */
1300
- extractNodeIdFromKey(nodeKey) {
1301
1309
  const staticPath = staticNodePathFromKey(nodeKey);
1302
- return staticPath.split("/").at(-1) ?? nodeKey;
1310
+ return this.findNodeByPath(root, staticPath);
1303
1311
  }
1304
- findNodeById(root, nodeId) {
1305
- if (root.id === nodeId)
1312
+ findNodeByPath(root, staticPath) {
1313
+ if (root.nodePath.join("/") === staticPath)
1306
1314
  return root;
1307
1315
  for (const child of root.children ?? []) {
1308
- const found = this.findNodeById(child, nodeId);
1316
+ const found = this.findNodeByPath(child, staticPath);
1309
1317
  if (found)
1310
1318
  return found;
1311
1319
  }
1312
1320
  for (const branch of root.branches ?? []) {
1313
- for (const child of branch.children) {
1314
- const found = this.findNodeById(child, nodeId);
1315
- if (found)
1316
- return found;
1317
- }
1321
+ const found = this.findNodeByPath(branch.child, staticPath);
1322
+ if (found)
1323
+ return found;
1318
1324
  }
1319
1325
  return undefined;
1320
1326
  }
@@ -1551,6 +1557,12 @@ function hookAgentTelemetry(state) {
1551
1557
  }
1552
1558
  return result;
1553
1559
  }
1560
+ function primaryOutput(value) {
1561
+ return isRecord(value) && "output" in value ? value.output : value;
1562
+ }
1563
+ function isRecord(value) {
1564
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1565
+ }
1554
1566
  /** Generate a locally sortable run ID: yyyyMMddHHmmss + 20 uppercase hex chars. */
1555
1567
  export function generateRunId(now = new Date()) {
1556
1568
  const pad = (n) => String(n).padStart(2, "0");
@@ -1564,12 +1576,52 @@ export function generateRunId(now = new Date()) {
1564
1576
  ].join("");
1565
1577
  return `${timestamp}${randomBytes(10).toString("hex").toUpperCase()}`;
1566
1578
  }
1567
- function nestedParallelBranchDynamic(dynamic, branchIndex) {
1568
- const branchId = String(branchIndex);
1569
- return {
1570
- ...dynamic,
1571
- parallelBranchId: dynamic.parallelBranchId === undefined ? branchId : `${dynamic.parallelBranchId}.${branchId}`
1572
- };
1579
+ function nestedParallelBranchDynamic(dynamic, branchId) {
1580
+ const current = currentDynamicFrame(dynamic);
1581
+ const nextBranchId = current.parallelBranchId === undefined ? branchId : `${current.parallelBranchId}.${branchId}`;
1582
+ return replaceCurrentDynamicFrame(dynamic, { ...current, parallelBranchId: nextBranchId });
1583
+ }
1584
+ function appendDynamicFrame(dynamic, frame) {
1585
+ const frames = [...dynamicFrames(dynamic), frame];
1586
+ return withCollapsedDynamic(frames);
1587
+ }
1588
+ function appendDynamicFrames(dynamic, child) {
1589
+ return withCollapsedDynamic([...dynamicFrames(dynamic), ...dynamicFrames(child)]);
1590
+ }
1591
+ function replaceCurrentDynamicFrame(dynamic, frame) {
1592
+ const frames = dynamicFrames(dynamic);
1593
+ if (frames.length === 0)
1594
+ return withCollapsedDynamic([frame]);
1595
+ return withCollapsedDynamic([...frames.slice(0, -1), frame]);
1596
+ }
1597
+ function currentDynamicFrame(dynamic) {
1598
+ return dynamicFrames(dynamic).at(-1) ?? {};
1599
+ }
1600
+ function dynamicFrames(dynamic) {
1601
+ if (dynamic.frames && dynamic.frames.length > 0)
1602
+ return dynamic.frames;
1603
+ const { frames: _frames, ...frame } = dynamic;
1604
+ return isEmptyDynamicFrame(frame) ? [] : [frame];
1605
+ }
1606
+ function withCollapsedDynamic(frames) {
1607
+ const collapsed = { frames };
1608
+ for (const frame of frames) {
1609
+ if (frame.loopRound !== undefined)
1610
+ collapsed.loopRound = frame.loopRound;
1611
+ if (frame.fanoutItemId !== undefined)
1612
+ collapsed.fanoutItemId = frame.fanoutItemId;
1613
+ if (frame.laneId !== undefined)
1614
+ collapsed.laneId = frame.laneId;
1615
+ if (frame.parallelBranchId !== undefined)
1616
+ collapsed.parallelBranchId = frame.parallelBranchId;
1617
+ }
1618
+ return collapsed;
1619
+ }
1620
+ function isEmptyDynamicFrame(frame) {
1621
+ return frame.loopRound === undefined &&
1622
+ frame.fanoutItemId === undefined &&
1623
+ frame.laneId === undefined &&
1624
+ frame.parallelBranchId === undefined;
1573
1625
  }
1574
1626
  /** Find the immediate parent IR node of `childId`, or undefined for the root. */
1575
1627
  function findParentNode(node, childId) {
@@ -1581,13 +1633,11 @@ function findParentNode(node, childId) {
1581
1633
  return found;
1582
1634
  }
1583
1635
  for (const branch of node.branches ?? []) {
1584
- for (const child of branch.children) {
1585
- if (child.id === childId)
1586
- return node;
1587
- const found = findParentNode(child, childId);
1588
- if (found)
1589
- return found;
1590
- }
1636
+ if (branch.child.id === childId)
1637
+ return node;
1638
+ const found = findParentNode(branch.child, childId);
1639
+ if (found)
1640
+ return found;
1591
1641
  }
1592
1642
  return undefined;
1593
1643
  }