langchain_agentx_stream_ui 0.1.9 → 0.2.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.
package/dist/index.js CHANGED
@@ -1169,17 +1169,24 @@ var WorkflowContainerStateMachine = class {
1169
1169
  (node) => workflowPath === node.workflow_path || workflowPath.startsWith(`${node.workflow_path}>`)
1170
1170
  );
1171
1171
  if (prefixCandidates.length > 0) {
1172
- const parent = prefixCandidates.reduce(
1173
- (best, node) => this.active.compareContainerRank(node.container_id, best.container_id) > 0 ? node : best
1174
- );
1175
- if (parent.workflow_path === workflowPath && parent.scope === "workflow") {
1176
- return parent.container_id;
1177
- }
1178
- if (workflowPath.startsWith(`${parent.workflow_path}>`)) {
1179
- return parent.container_id;
1180
- }
1181
- if (parent.workflow_path === workflowPath && parent.scope !== scope) {
1182
- return parent.container_id;
1172
+ const parentCandidates = prefixCandidates.filter((node) => {
1173
+ if (scope === "item") return node.scope !== "item";
1174
+ if (scope === "aggregate") return node.scope !== "item" && node.scope !== "aggregate";
1175
+ return node.scope !== scope;
1176
+ });
1177
+ if (parentCandidates.length > 0) {
1178
+ const parent = parentCandidates.reduce(
1179
+ (best, node) => this.active.compareContainerRank(node.container_id, best.container_id) > 0 ? node : best
1180
+ );
1181
+ if (parent.workflow_path === workflowPath && parent.scope === "workflow") {
1182
+ return parent.container_id;
1183
+ }
1184
+ if (workflowPath.startsWith(`${parent.workflow_path}>`)) {
1185
+ return parent.container_id;
1186
+ }
1187
+ if (parent.workflow_path === workflowPath && parent.scope !== scope) {
1188
+ return parent.container_id;
1189
+ }
1183
1190
  }
1184
1191
  }
1185
1192
  const workflowCandidates = openNodes.filter((node) => node.scope === "workflow");
@@ -1277,6 +1284,167 @@ function loopTaskScopeForStructureOpen(eventType) {
1277
1284
  return LOOP_TASK_OPEN_SCOPES[eventType];
1278
1285
  }
1279
1286
 
1287
+ // src/core/workflowStructureEvents.ts
1288
+ var WORKFLOW_STRUCTURE_EVENT_PREFIXES = [
1289
+ "workflow-",
1290
+ "stage-",
1291
+ "parallel-",
1292
+ "route-",
1293
+ "subworkflow-"
1294
+ ];
1295
+ var WORKFLOW_STRUCTURE_EVENT_TYPES = /* @__PURE__ */ new Set([
1296
+ "workflow-start",
1297
+ "workflow-end",
1298
+ "workflow-failed",
1299
+ "subworkflow-start",
1300
+ "subworkflow-end",
1301
+ "stage-start",
1302
+ "stage-done",
1303
+ "stage-failed",
1304
+ "parallel-item-start",
1305
+ "parallel-item-done",
1306
+ "parallel-item-failed",
1307
+ "parallel-aggregate-start",
1308
+ "parallel-aggregate-done",
1309
+ "parallel-aggregate-failed",
1310
+ "route-branch-start",
1311
+ "route-branch-done",
1312
+ "route-branch-failed"
1313
+ ]);
1314
+ var AGENT_LOOP_REPLAY_EVENT_TYPES = new Set(
1315
+ Object.keys(DEFAULT_EVENT_TIERS)
1316
+ );
1317
+ function isWorkflowStructureEventType(eventType) {
1318
+ if (WORKFLOW_STRUCTURE_EVENT_TYPES.has(eventType)) return true;
1319
+ return WORKFLOW_STRUCTURE_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix));
1320
+ }
1321
+ function isAgentLoopReplayEventType(eventType) {
1322
+ return AGENT_LOOP_REPLAY_EVENT_TYPES.has(eventType);
1323
+ }
1324
+ function filterAgentLoopReplayEvents(events) {
1325
+ return events.filter(
1326
+ (event) => isAgentLoopReplayEventType(event.event_type) && !isWorkflowStructureEventType(event.event_type)
1327
+ );
1328
+ }
1329
+
1330
+ // src/core/workflow/workflowPathUtils.ts
1331
+ function splitWorkflowPathSegments(workflowPath) {
1332
+ return workflowPath.split(">").filter(Boolean);
1333
+ }
1334
+ function parentWorkflowPath(workflowPath) {
1335
+ const segments = splitWorkflowPathSegments(workflowPath);
1336
+ if (segments.length <= 1) return null;
1337
+ return segments.slice(0, -1).join(">");
1338
+ }
1339
+ function lastWorkflowPathSegment(workflowPath) {
1340
+ const segments = splitWorkflowPathSegments(workflowPath);
1341
+ return segments.length > 0 ? segments[segments.length - 1] : null;
1342
+ }
1343
+ function isWorkflowPathUnderAncestor(workflowPath, ancestorPath) {
1344
+ if (workflowPath === ancestorPath) return false;
1345
+ const prefix = `${ancestorPath}>`;
1346
+ return workflowPath.startsWith(prefix);
1347
+ }
1348
+ function isWorkflowPathEqualOrUnder(workflowPath, ancestorPath) {
1349
+ return workflowPath === ancestorPath || isWorkflowPathUnderAncestor(workflowPath, ancestorPath);
1350
+ }
1351
+
1352
+ // src/core/workflow/nativeNestedUtils.ts
1353
+ function readString3(data, key) {
1354
+ const value = data[key];
1355
+ return typeof value === "string" ? value : void 0;
1356
+ }
1357
+ function readNumber(data, key) {
1358
+ const value = data[key];
1359
+ return typeof value === "number" ? value : void 0;
1360
+ }
1361
+ function isNativeNestedStructureEvent(event) {
1362
+ if (!isWorkflowStructureEventType(event.event_type)) return false;
1363
+ const data = event.data ?? {};
1364
+ const path = readString3(data, "workflow_path") ?? "";
1365
+ const depth = readNumber(data, "workflow_depth") ?? 0;
1366
+ return depth >= 1 && path.includes(">");
1367
+ }
1368
+ function resolveHostStageContainerId(tree, workflowPath) {
1369
+ const parentPath = parentWorkflowPath(workflowPath);
1370
+ const childSegment = lastWorkflowPathSegment(workflowPath);
1371
+ if (!parentPath || !childSegment) return null;
1372
+ const stageId = buildContainerId(parentPath, "stage", childSegment);
1373
+ return tree.containersById[stageId] != null ? stageId : null;
1374
+ }
1375
+ function isHostStageNestedChildPath(tree, workflowPath) {
1376
+ return resolveHostStageContainerId(tree, workflowPath) != null;
1377
+ }
1378
+ function hasNativeNestedStructureInTree(tree) {
1379
+ for (const node of Object.values(tree.containersById)) {
1380
+ if (node.workflow_depth < 1) continue;
1381
+ if (!node.workflow_path.includes(">")) continue;
1382
+ if (isHostStageNestedChildPath(tree, node.workflow_path)) {
1383
+ return true;
1384
+ }
1385
+ }
1386
+ return false;
1387
+ }
1388
+ function clearHostStageLoopSession(tree, hostStageId) {
1389
+ if (!hostStageId) return tree;
1390
+ const hostStage = tree.containersById[hostStageId];
1391
+ if (!hostStage || hostStage.scope !== "stage" || !hostStage.loopSessionId) return tree;
1392
+ return {
1393
+ ...tree,
1394
+ containersById: {
1395
+ ...tree.containersById,
1396
+ [hostStageId]: { ...hostStage, loopSessionId: null }
1397
+ }
1398
+ };
1399
+ }
1400
+ function isNestedHostStageShell(tree, stageNode) {
1401
+ if (stageNode.scope !== "stage") return false;
1402
+ return Object.values(tree.containersById).some(
1403
+ (node) => node.scope === "subworkflow" && node.parent_container_id === stageNode.container_id
1404
+ );
1405
+ }
1406
+ function clearHostStageLoopOnNestedChildOpen(tree, event) {
1407
+ const eventType = event.event_type;
1408
+ if (eventType !== "subworkflow-start" && eventType !== "parallel-item-start") return tree;
1409
+ const data = event.data ?? {};
1410
+ const workflowPath = readString3(data, "workflow_path");
1411
+ if (!workflowPath) return tree;
1412
+ if (eventType === "subworkflow-start") {
1413
+ const childWorkflowId = readString3(data, "child_workflow_id");
1414
+ if (!childWorkflowId) return tree;
1415
+ const subId = buildContainerId(workflowPath, "subworkflow", childWorkflowId);
1416
+ const sub = tree.containersById[subId];
1417
+ const hostStageId = sub?.parent_container_id ?? resolveHostStageContainerId(tree, workflowPath);
1418
+ return clearHostStageLoopSession(tree, hostStageId);
1419
+ }
1420
+ return clearHostStageLoopSession(tree, resolveHostStageContainerId(tree, workflowPath));
1421
+ }
1422
+ function dropStaleEmbeddedPathsOnNativeSubworkflowStart(tree, event) {
1423
+ if (event.event_type !== "subworkflow-start" || !isNativeNestedStructureEvent(event)) {
1424
+ return tree;
1425
+ }
1426
+ const data = event.data ?? {};
1427
+ const nativeChildPath = readString3(data, "workflow_path");
1428
+ if (!nativeChildPath) return tree;
1429
+ const rootAnchor = splitWorkflowPathSegments(nativeChildPath)[0];
1430
+ if (!rootAnchor) return tree;
1431
+ let changed = false;
1432
+ const containersById = { ...tree.containersById };
1433
+ let activeContainerIds = [...tree.activeContainerIds];
1434
+ for (const [id, node] of Object.entries(tree.containersById)) {
1435
+ if (node.workflow_depth < 1) continue;
1436
+ if (!node.workflow_path.startsWith(`${rootAnchor}>`)) continue;
1437
+ if (node.workflow_path === nativeChildPath || isWorkflowPathEqualOrUnder(node.workflow_path, nativeChildPath)) {
1438
+ continue;
1439
+ }
1440
+ if (isHostStageNestedChildPath(tree, node.workflow_path)) continue;
1441
+ delete containersById[id];
1442
+ activeContainerIds = activeContainerIds.filter((cid) => cid !== id);
1443
+ changed = true;
1444
+ }
1445
+ return changed ? { ...tree, containersById, activeContainerIds } : tree;
1446
+ }
1447
+
1280
1448
  // src/core/workflow/embeddedSubworkflowUtils.ts
1281
1449
  function parseLoopSessionParts(sessionId) {
1282
1450
  const trimmed = sessionId.trim();
@@ -1378,7 +1546,20 @@ function completeOpenItemsUnderSubworkflow(tree, subworkflowId) {
1378
1546
  function resolveLeafScope(taskKey) {
1379
1547
  return taskKey === "aggregate" ? "aggregate" : "item";
1380
1548
  }
1549
+ function shouldUseEmbeddedSynthesis(treeOrState, pendingEvent) {
1550
+ const tree = "containerTree" in treeOrState ? treeOrState.containerTree : treeOrState;
1551
+ if (pendingEvent && isNativeNestedStructureEvent(pendingEvent)) {
1552
+ return false;
1553
+ }
1554
+ if (hasNativeNestedStructureInTree(tree)) {
1555
+ return false;
1556
+ }
1557
+ return true;
1558
+ }
1381
1559
  function ensureEmbeddedSubworkflowContainers(tree, loopSessionId) {
1560
+ if (!shouldUseEmbeddedSynthesis(tree)) {
1561
+ return tree;
1562
+ }
1382
1563
  const root = resolveRootWorkflow(tree);
1383
1564
  const parts = parseLoopSessionParts(loopSessionId);
1384
1565
  if (!root || !parts || !isEmbeddedChildLoopSession(loopSessionId, root.workflowId)) {
@@ -1461,11 +1642,39 @@ function findContainerIdByLoopSession(tree, loopSessionId) {
1461
1642
  }
1462
1643
  return null;
1463
1644
  }
1645
+ var PARALLEL_ITEM_STRUCTURE_EVENTS = /* @__PURE__ */ new Set([
1646
+ "parallel-item-start",
1647
+ "parallel-item-done",
1648
+ "parallel-item-failed"
1649
+ ]);
1650
+ function readItemKey(data) {
1651
+ const value = data.item_key;
1652
+ return typeof value === "string" ? value : void 0;
1653
+ }
1654
+ function resolveParallelItemLoopSessionId(eventType, eventSessionId, derivedLoopSessionId, data) {
1655
+ if (!PARALLEL_ITEM_STRUCTURE_EVENTS.has(eventType) || !eventSessionId) return null;
1656
+ if (eventSessionId === derivedLoopSessionId) return null;
1657
+ const itemKey = readItemKey(data);
1658
+ const parsed = parseLoopSessionParts(eventSessionId);
1659
+ if (itemKey && parsed?.taskKey === itemKey) {
1660
+ return eventSessionId;
1661
+ }
1662
+ return null;
1663
+ }
1464
1664
  function resolveStructureOpenLoopSessionId(eventType, eventSessionId, derivedLoopSessionId, data) {
1465
1665
  const rootWorkflowId = readWorkflowId(data) ?? "";
1466
1666
  if ((eventType === "parallel-aggregate-start" || eventType === "parallel-aggregate-done" || eventType === "parallel-aggregate-failed") && eventSessionId && isEmbeddedChildLoopSession(eventSessionId, rootWorkflowId)) {
1467
1667
  return eventSessionId;
1468
1668
  }
1669
+ const parallelItemSessionId = resolveParallelItemLoopSessionId(
1670
+ eventType,
1671
+ eventSessionId,
1672
+ derivedLoopSessionId,
1673
+ data
1674
+ );
1675
+ if (parallelItemSessionId) {
1676
+ return parallelItemSessionId;
1677
+ }
1469
1678
  return derivedLoopSessionId;
1470
1679
  }
1471
1680
  var EMBEDDED_AGGREGATE_STRUCTURE_EVENTS = /* @__PURE__ */ new Set([
@@ -1483,7 +1692,7 @@ function patchEmbeddedStructureEvent(event, rootWorkflowId) {
1483
1692
  const parts = parseLoopSessionParts(event.session_id);
1484
1693
  if (!parts) return event;
1485
1694
  const data = event.data ?? {};
1486
- const rootPath = readString3(data, "workflow_path") ?? rootWorkflowId;
1695
+ const rootPath = readString4(data, "workflow_path") ?? rootWorkflowId;
1487
1696
  const childPath = `${rootPath}>${parts.workflowId}`;
1488
1697
  const workflowDepth = typeof data.workflow_depth === "number" ? data.workflow_depth + 1 : 1;
1489
1698
  return {
@@ -1495,14 +1704,17 @@ function patchEmbeddedStructureEvent(event, rootWorkflowId) {
1495
1704
  }
1496
1705
  };
1497
1706
  }
1498
- function readString3(data, key) {
1707
+ function readString4(data, key) {
1499
1708
  const value = data[key];
1500
1709
  return typeof value === "string" ? value : void 0;
1501
1710
  }
1502
1711
  function isDescendantOf(containersById, ancestorId, nodeId) {
1712
+ const seen = /* @__PURE__ */ new Set();
1503
1713
  let current = containersById[nodeId]?.parent_container_id ?? null;
1504
1714
  while (current) {
1505
1715
  if (current === ancestorId) return true;
1716
+ if (seen.has(current)) return false;
1717
+ seen.add(current);
1506
1718
  current = containersById[current]?.parent_container_id ?? null;
1507
1719
  }
1508
1720
  return false;
@@ -1530,51 +1742,8 @@ function readWorkflowId(data) {
1530
1742
  return readWorkflowIdFromData(data);
1531
1743
  }
1532
1744
 
1533
- // src/core/workflowStructureEvents.ts
1534
- var WORKFLOW_STRUCTURE_EVENT_PREFIXES = [
1535
- "workflow-",
1536
- "stage-",
1537
- "parallel-",
1538
- "route-",
1539
- "subworkflow-"
1540
- ];
1541
- var WORKFLOW_STRUCTURE_EVENT_TYPES = /* @__PURE__ */ new Set([
1542
- "workflow-start",
1543
- "workflow-end",
1544
- "workflow-failed",
1545
- "subworkflow-start",
1546
- "subworkflow-end",
1547
- "stage-start",
1548
- "stage-done",
1549
- "stage-failed",
1550
- "parallel-item-start",
1551
- "parallel-item-done",
1552
- "parallel-item-failed",
1553
- "parallel-aggregate-start",
1554
- "parallel-aggregate-done",
1555
- "parallel-aggregate-failed",
1556
- "route-branch-start",
1557
- "route-branch-done",
1558
- "route-branch-failed"
1559
- ]);
1560
- var AGENT_LOOP_REPLAY_EVENT_TYPES = new Set(
1561
- Object.keys(DEFAULT_EVENT_TIERS)
1562
- );
1563
- function isWorkflowStructureEventType(eventType) {
1564
- if (WORKFLOW_STRUCTURE_EVENT_TYPES.has(eventType)) return true;
1565
- return WORKFLOW_STRUCTURE_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix));
1566
- }
1567
- function isAgentLoopReplayEventType(eventType) {
1568
- return AGENT_LOOP_REPLAY_EVENT_TYPES.has(eventType);
1569
- }
1570
- function filterAgentLoopReplayEvents(events) {
1571
- return events.filter(
1572
- (event) => isAgentLoopReplayEventType(event.event_type) && !isWorkflowStructureEventType(event.event_type)
1573
- );
1574
- }
1575
-
1576
1745
  // src/core/workflowReducer.ts
1577
- function readString4(data, key) {
1746
+ function readString5(data, key) {
1578
1747
  const value = data[key];
1579
1748
  return typeof value === "string" ? value : void 0;
1580
1749
  }
@@ -1673,7 +1842,7 @@ function prebindLoopSessionFromStructureOpen(tree, event) {
1673
1842
  loopSessionId = derivedLoopSessionId;
1674
1843
  }
1675
1844
  let nextTree = event.event_type === "parallel-aggregate-start" ? ensureEmbeddedSubworkflowContainers(tree, loopSessionId) : tree;
1676
- const workflowPath = readString4(data, "workflow_path") ?? "";
1845
+ const workflowPath = readString5(data, "workflow_path") ?? "";
1677
1846
  const scopeKey = resolveScopeKey(event.event_type, data);
1678
1847
  const containerId = buildContainerId(workflowPath, scope, scopeKey);
1679
1848
  let targetId = containerId;
@@ -1754,7 +1923,7 @@ function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
1754
1923
  let nextTree = ensureEmbeddedSubworkflowContainers(tree, sessionId);
1755
1924
  if (findContainerByLoopSession(nextTree, sessionId)) return nextTree;
1756
1925
  const data = event.data ?? {};
1757
- const workflowPath = readString4(data, "workflow_path") ?? null;
1926
+ const workflowPath = readString5(data, "workflow_path") ?? null;
1758
1927
  const scopeKey = resolveContentScopeKey(data);
1759
1928
  let targetId = findContainerByLoopSession(nextTree, sessionId);
1760
1929
  if (!targetId && scopeKey) {
@@ -1765,12 +1934,17 @@ function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
1765
1934
  );
1766
1935
  }
1767
1936
  if (!targetId) {
1768
- for (const containerId of nextTree.activeContainerIds) {
1769
- const node2 = nextTree.containersById[containerId];
1770
- if ((node2?.scope === "item" || node2?.scope === "stage" || node2?.scope === "aggregate") && !node2.loopSessionId) {
1937
+ const fallbackScopes = ["item", "aggregate", "stage"];
1938
+ for (const scope of fallbackScopes) {
1939
+ for (const containerId of nextTree.activeContainerIds) {
1940
+ const node2 = nextTree.containersById[containerId];
1941
+ if (!node2 || node2.loopSessionId) continue;
1942
+ if (node2.scope !== scope) continue;
1943
+ if (node2.scope === "stage" && isNestedHostStageShell(nextTree, node2)) continue;
1771
1944
  targetId = containerId;
1772
1945
  break;
1773
1946
  }
1947
+ if (targetId) break;
1774
1948
  }
1775
1949
  }
1776
1950
  if (!targetId) {
@@ -1783,6 +1957,9 @@ function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
1783
1957
  if (!targetId) return nextTree;
1784
1958
  const node = nextTree.containersById[targetId];
1785
1959
  if (!node || node.loopSessionId) return nextTree;
1960
+ if (node.scope === "stage" && isNestedHostStageShell(nextTree, node)) {
1961
+ return nextTree;
1962
+ }
1786
1963
  if (node.scope !== "stage" && node.scope !== "item" && node.scope !== "branch" && node.scope !== "aggregate") {
1787
1964
  return nextTree;
1788
1965
  }
@@ -1882,8 +2059,15 @@ function finalizeLoopTreeOnStructureClose(state, event) {
1882
2059
  }
1883
2060
  function applyStructureEvent(state, event) {
1884
2061
  const rootWorkflowId = resolveRootWorkflowId(state.containerTree);
1885
- const projectedEvent = rootWorkflowId != null ? patchEmbeddedStructureEvent(event, rootWorkflowId) : event;
2062
+ const useEmbeddedSynthesis = shouldUseEmbeddedSynthesis(state, event);
2063
+ const projectedEvent = rootWorkflowId != null && useEmbeddedSynthesis ? patchEmbeddedStructureEvent(event, rootWorkflowId) : event;
1886
2064
  let containerTree = applyStructureToContainerTree(state.containerTree, projectedEvent);
2065
+ if (projectedEvent.event_type === "subworkflow-start" || projectedEvent.event_type === "parallel-item-start") {
2066
+ containerTree = clearHostStageLoopOnNestedChildOpen(containerTree, projectedEvent);
2067
+ }
2068
+ if (projectedEvent.event_type === "subworkflow-start") {
2069
+ containerTree = dropStaleEmbeddedPathsOnNativeSubworkflowStart(containerTree, projectedEvent);
2070
+ }
1887
2071
  const derivedLoopSessionId = deriveLoopSessionIdFromStructureOpen(projectedEvent);
1888
2072
  const structureData = projectedEvent.data ?? {};
1889
2073
  const structureLoopSessionId = derivedLoopSessionId ? resolveStructureOpenLoopSessionId(
@@ -1892,13 +2076,13 @@ function applyStructureEvent(state, event) {
1892
2076
  derivedLoopSessionId,
1893
2077
  structureData
1894
2078
  ) : null;
1895
- if (structureLoopSessionId) {
2079
+ if (structureLoopSessionId && useEmbeddedSynthesis) {
1896
2080
  containerTree = ensureEmbeddedSubworkflowContainers(containerTree, structureLoopSessionId);
1897
2081
  }
1898
2082
  containerTree = prebindLoopSessionFromStructureOpen(containerTree, projectedEvent);
1899
2083
  if (projectedEvent.event_type === "stage-done" || projectedEvent.event_type === "stage-failed") {
1900
- const stageKey = readString4(structureData, "stage_key");
1901
- const workflowPath = readString4(structureData, "workflow_path") ?? "";
2084
+ const stageKey = readString5(structureData, "stage_key");
2085
+ const workflowPath = readString5(structureData, "workflow_path") ?? "";
1902
2086
  if (stageKey) {
1903
2087
  const stageId = buildContainerId(workflowPath, "stage", stageKey);
1904
2088
  if (containerTree.containersById[stageId]) {
@@ -2154,6 +2338,18 @@ function evictCompletedLoopOverflow(state, maxHydratedCompletedLoops) {
2154
2338
  }
2155
2339
 
2156
2340
  // src/core/workflowSessionStore.ts
2341
+ var DEFAULT_HYDRATE_CHUNK_SIZE = 50;
2342
+ function nextAnimationFrame() {
2343
+ return new Promise((resolve) => {
2344
+ requestAnimationFrame(() => resolve());
2345
+ });
2346
+ }
2347
+ function withoutHydrating(ids, loopSessionId) {
2348
+ if (!ids[loopSessionId]) return ids;
2349
+ const next = { ...ids };
2350
+ delete next[loopSessionId];
2351
+ return next;
2352
+ }
2157
2353
  function safeReduceWorkflow(state, event, eventIndex, options) {
2158
2354
  try {
2159
2355
  return { state: reduceWorkflowSession(state, event, eventIndex, options) };
@@ -2174,6 +2370,39 @@ function safeReduceWorkflow(state, event, eventIndex, options) {
2174
2370
  };
2175
2371
  }
2176
2372
  }
2373
+ function finalizeWorkflowSessionOnStreamError(state, message, eventIndex, stack) {
2374
+ const containersById = {
2375
+ ...state.containerTree.containersById
2376
+ };
2377
+ for (const [id, node] of Object.entries(containersById)) {
2378
+ if (node.status === "running" || node.status === "pending") {
2379
+ containersById[id] = { ...node, status: "failed", is_open: false };
2380
+ }
2381
+ }
2382
+ const loopTreesBySessionId = { ...state.loopTreesBySessionId };
2383
+ for (const [loopSessionId, tree] of Object.entries(loopTreesBySessionId)) {
2384
+ if (tree.status === "running" || tree.status === "connecting") {
2385
+ loopTreesBySessionId[loopSessionId] = finalizeWorkflowLoopTree(tree, "error");
2386
+ }
2387
+ }
2388
+ return {
2389
+ ...state,
2390
+ status: "error",
2391
+ containerTree: {
2392
+ ...state.containerTree,
2393
+ containersById,
2394
+ activeContainerIds: state.containerTree.activeContainerIds.filter(
2395
+ (id) => containersById[id]?.is_open
2396
+ )
2397
+ },
2398
+ loopTreesBySessionId,
2399
+ activeLoopSessionId: null,
2400
+ internalErrors: [
2401
+ ...state.internalErrors,
2402
+ { eventIndex, eventType: "stream-error", message, stack }
2403
+ ]
2404
+ };
2405
+ }
2177
2406
  function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionState(), storeOptions) {
2178
2407
  const reduceOpts = {
2179
2408
  tierOverrides: storeOptions?.tierOverrides
@@ -2190,6 +2419,7 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2190
2419
  return createStore2((set, get) => ({
2191
2420
  state: initialState,
2192
2421
  eventCount: initialEventCount,
2422
+ hydratingLoopSessionIds: {},
2193
2423
  applyEvent(event, ctx) {
2194
2424
  const sseEventId = ctx?.sseEventId;
2195
2425
  if (shouldSkipDuplicateEvent(seenEventIds, sseEventId)) return;
@@ -2216,26 +2446,20 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2216
2446
  seenEventIds.clear();
2217
2447
  set({
2218
2448
  state: createEmptyWorkflowSessionState(),
2219
- eventCount: 0
2449
+ eventCount: 0,
2450
+ hydratingLoopSessionIds: {}
2220
2451
  });
2221
2452
  },
2222
2453
  markAsError(error) {
2223
- const { state } = get();
2454
+ const { state, eventCount } = get();
2224
2455
  const message = error?.message ?? "Workflow stream error";
2225
2456
  set({
2226
- state: {
2227
- ...state,
2228
- status: "error",
2229
- internalErrors: [
2230
- ...state.internalErrors,
2231
- {
2232
- eventIndex: get().eventCount,
2233
- eventType: "stream-error",
2234
- message,
2235
- stack: error?.stack
2236
- }
2237
- ]
2238
- }
2457
+ state: finalizeWorkflowSessionOnStreamError(
2458
+ state,
2459
+ message,
2460
+ eventCount,
2461
+ error?.stack
2462
+ )
2239
2463
  });
2240
2464
  },
2241
2465
  /**
@@ -2275,6 +2499,71 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2275
2499
  next = finalizeState(next);
2276
2500
  return { state: next };
2277
2501
  });
2502
+ },
2503
+ async hydrateLoopSessionAsync(loopSessionId, events, options) {
2504
+ const { hydratingLoopSessionIds } = get();
2505
+ if (hydratingLoopSessionIds[loopSessionId]) return;
2506
+ const filtered = filterAgentLoopReplayEvents(events);
2507
+ if (filtered.length === 0) return;
2508
+ const chunkSize = options?.chunkSize ?? DEFAULT_HYDRATE_CHUNK_SIZE;
2509
+ const signal = options?.signal;
2510
+ set((current) => ({
2511
+ hydratingLoopSessionIds: {
2512
+ ...current.hydratingLoopSessionIds,
2513
+ [loopSessionId]: true
2514
+ }
2515
+ }));
2516
+ try {
2517
+ let tree = get().state.loopTreesBySessionId[loopSessionId] ?? createEmptyTree();
2518
+ let idx = get().eventCount;
2519
+ for (let offset = 0; offset < filtered.length; offset += chunkSize) {
2520
+ if (signal?.aborted) {
2521
+ throw new DOMException("Hydrate aborted", "AbortError");
2522
+ }
2523
+ const chunk = filtered.slice(offset, offset + chunkSize);
2524
+ for (const event of chunk) {
2525
+ tree = reduceTree(tree, event, idx, reduceOpts);
2526
+ idx += 1;
2527
+ }
2528
+ const isLastChunk = offset + chunkSize >= filtered.length;
2529
+ set((current) => {
2530
+ let next = {
2531
+ ...current.state,
2532
+ loopTreesBySessionId: {
2533
+ ...current.state.loopTreesBySessionId,
2534
+ [loopSessionId]: tree
2535
+ }
2536
+ };
2537
+ if (isLastChunk) {
2538
+ next = touchCompletedLoopSession(next, loopSessionId);
2539
+ next = evictCompletedLoopOverflow(next, maxHydratedCompletedLoops);
2540
+ }
2541
+ next = finalizeState(next);
2542
+ return { state: next };
2543
+ });
2544
+ if (!isLastChunk) {
2545
+ await nextAnimationFrame();
2546
+ }
2547
+ }
2548
+ } catch {
2549
+ set((current) => {
2550
+ const { [loopSessionId]: _removed, ...restLoops } = current.state.loopTreesBySessionId;
2551
+ return {
2552
+ state: { ...current.state, loopTreesBySessionId: restLoops },
2553
+ hydratingLoopSessionIds: withoutHydrating(
2554
+ current.hydratingLoopSessionIds,
2555
+ loopSessionId
2556
+ )
2557
+ };
2558
+ });
2559
+ return;
2560
+ }
2561
+ set((current) => ({
2562
+ hydratingLoopSessionIds: withoutHydrating(
2563
+ current.hydratingLoopSessionIds,
2564
+ loopSessionId
2565
+ )
2566
+ }));
2278
2567
  }
2279
2568
  }));
2280
2569
  }
@@ -2283,7 +2572,7 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2283
2572
  import { useMemo as useMemo7, useState as useState6 } from "react";
2284
2573
 
2285
2574
  // src/view/workflow/WorkflowAggregateContainer.tsx
2286
- import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState } from "react";
2575
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState, memo } from "react";
2287
2576
 
2288
2577
  // src/view/workflow/WorkflowContainerLine.tsx
2289
2578
  import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -2551,6 +2840,29 @@ function mapAggregateUiStatus(node) {
2551
2840
  return mapContainerUiStatus(node.status);
2552
2841
  }
2553
2842
 
2843
+ // src/view/workflow/workflowContainerMemo.ts
2844
+ function displayVisualEqual(a, b) {
2845
+ if (a === b) return true;
2846
+ if (a === null || b === null) return false;
2847
+ if (a.progress_current !== b.progress_current) return false;
2848
+ if (a.status !== b.status) return false;
2849
+ return true;
2850
+ }
2851
+ function workflowContainerNodeVisualEqual(a, b) {
2852
+ if (a.container_id !== b.container_id) return false;
2853
+ if (a.status !== b.status) return false;
2854
+ if (a.is_open !== b.is_open) return false;
2855
+ if (a.loopSessionId !== b.loopSessionId) return false;
2856
+ if (a.title !== b.title) return false;
2857
+ if (a.subtitle !== b.subtitle) return false;
2858
+ if (a.content_blocks.length !== b.content_blocks.length) return false;
2859
+ if (!displayVisualEqual(a.display, b.display)) return false;
2860
+ return true;
2861
+ }
2862
+ function workflowContainerViewPropsEqual(prev, next) {
2863
+ return prev.depth === next.depth && prev.virtualized === next.virtualized && prev.virtualizeThreshold === next.virtualizeThreshold && prev.groupParallelTools === next.groupParallelTools;
2864
+ }
2865
+
2554
2866
  // src/view/workflow/WorkflowAggregateContainer.tsx
2555
2867
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
2556
2868
  function mergeContentPreview(node) {
@@ -2558,7 +2870,7 @@ function mergeContentPreview(node) {
2558
2870
  if (blocks.length === 0) return "";
2559
2871
  return blocks.map((block) => block.preview?.trim() ?? "").filter(Boolean).join("\n\n");
2560
2872
  }
2561
- function WorkflowAggregateContainer({
2873
+ function WorkflowAggregateContainerInner({
2562
2874
  node,
2563
2875
  loopTree,
2564
2876
  siblingItems = [],
@@ -2678,9 +2990,24 @@ function WorkflowAggregateContainer({
2678
2990
  }
2679
2991
  );
2680
2992
  }
2993
+ function workflowAggregateContainerPropsEqual(prev, next) {
2994
+ if (prev.loopTree !== next.loopTree) return false;
2995
+ if (!workflowContainerViewPropsEqual(prev, next)) return false;
2996
+ if (prev.siblingItems.length !== next.siblingItems.length) return false;
2997
+ for (let i = 0; i < prev.siblingItems.length; i += 1) {
2998
+ if (!workflowContainerNodeVisualEqual(prev.siblingItems[i], next.siblingItems[i])) {
2999
+ return false;
3000
+ }
3001
+ }
3002
+ return workflowContainerNodeVisualEqual(prev.node, next.node);
3003
+ }
3004
+ var WorkflowAggregateContainer = memo(
3005
+ WorkflowAggregateContainerInner,
3006
+ workflowAggregateContainerPropsEqual
3007
+ );
2681
3008
 
2682
3009
  // src/view/workflow/WorkflowParallelGroup.tsx
2683
- import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useState as useState3 } from "react";
3010
+ import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useState as useState3, memo as memo3 } from "react";
2684
3011
  import { useStore as useStore3 } from "zustand";
2685
3012
 
2686
3013
  // src/core/workflow/workflowParallelGroupMetrics.ts
@@ -2747,7 +3074,7 @@ function formatParallelGroupSummary(metrics) {
2747
3074
  }
2748
3075
 
2749
3076
  // src/view/workflow/WorkflowStageContainer.tsx
2750
- import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useState as useState2 } from "react";
3077
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useState as useState2, memo as memo2 } from "react";
2751
3078
  import { useStore as useStore2 } from "zustand";
2752
3079
 
2753
3080
  // src/core/workflow/loopTreeUtils.ts
@@ -2822,6 +3149,9 @@ function partitionWorkflowStages(stages, maxVisibleDoneCount) {
2822
3149
  }
2823
3150
  function isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds) {
2824
3151
  if (activeContainerIds.includes(node.container_id)) return true;
3152
+ if (node.status === "completed" || node.status === "failed" || node.status === "skipped") {
3153
+ return false;
3154
+ }
2825
3155
  if (node.loopSessionId && node.loopSessionId === activeLoopSessionId) return true;
2826
3156
  return false;
2827
3157
  }
@@ -2837,13 +3167,14 @@ function bodyTestId(node) {
2837
3167
  if (node.scope === "item") return `lax-workflow-parallel-body-${node.scope_key}`;
2838
3168
  return `lax-workflow-stage-body-${node.scope_key}`;
2839
3169
  }
2840
- function WorkflowStageContainer({
3170
+ function WorkflowStageContainerInner({
2841
3171
  node,
2842
3172
  loopTree,
2843
3173
  depth = 0,
2844
3174
  virtualized,
2845
3175
  virtualizeThreshold,
2846
- groupParallelTools
3176
+ groupParallelTools,
3177
+ suppressHostScopedLoop = false
2847
3178
  }) {
2848
3179
  const { verbose } = useSessionViewOptions();
2849
3180
  const scale = useWorkflowScaleOptions();
@@ -2906,13 +3237,13 @@ function WorkflowStageContainer({
2906
3237
  return () => window.removeEventListener("keydown", onKeyDown);
2907
3238
  }, [requestLoopHydration]);
2908
3239
  const displayTitle = node.title;
2909
- const usesScopedLoop = Boolean(node.loopSessionId);
3240
+ const usesScopedLoop = Boolean(node.loopSessionId) && !suppressHostScopedLoop;
2910
3241
  const runningLoopTree = loopTree ?? (usesScopedLoop && isRunning && isActive ? createWorkflowRunningLoopTree() : void 0);
2911
3242
  const staticContentTeaser = !usesScopedLoop && !loopTree && node.content_blocks.length > 0 ? node.content_blocks[node.content_blocks.length - 1].preview : "";
2912
3243
  const teaserText = doneSummary?.teaser || staticContentTeaser;
2913
3244
  const expandHint = doneSummary ? formatTeaserExpandHint(doneSummary.extraLines, verbose) : null;
2914
3245
  const doneLabel = doneSummary?.label ?? "done";
2915
- const statusLabel = isDone ? doneLabel : mapContainerStatusLabel(node.status);
3246
+ const statusLabel = isDone ? doneLabel : suppressHostScopedLoop && isRunning ? "running" : mapContainerStatusLabel(node.status);
2916
3247
  const testId = containerTestId(node);
2917
3248
  const isItem = node.scope === "item";
2918
3249
  const canToggleShell = isRunning && !isSkipped || isDone;
@@ -3016,6 +3347,13 @@ function WorkflowStageContainer({
3016
3347
  }
3017
3348
  );
3018
3349
  }
3350
+ function workflowStageContainerPropsEqual(prev, next) {
3351
+ if (prev.suppressHostScopedLoop !== next.suppressHostScopedLoop) return false;
3352
+ if (prev.loopTree !== next.loopTree) return false;
3353
+ if (!workflowContainerViewPropsEqual(prev, next)) return false;
3354
+ return workflowContainerNodeVisualEqual(prev.node, next.node);
3355
+ }
3356
+ var WorkflowStageContainer = memo2(WorkflowStageContainerInner, workflowStageContainerPropsEqual);
3019
3357
 
3020
3358
  // src/view/workflow/WorkflowParallelGroup.tsx
3021
3359
  import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -3023,7 +3361,7 @@ function resolveLoopTree(node, loopTreesBySessionId) {
3023
3361
  if (!node.loopSessionId) return void 0;
3024
3362
  return loopTreesBySessionId[node.loopSessionId];
3025
3363
  }
3026
- function WorkflowParallelGroup({
3364
+ function WorkflowParallelGroupInner({
3027
3365
  groupKey,
3028
3366
  items,
3029
3367
  loopTreesBySessionId,
@@ -3039,12 +3377,13 @@ function WorkflowParallelGroup({
3039
3377
  const metrics = useMemo4(() => computeParallelGroupMetrics(items), [items]);
3040
3378
  const summary = formatParallelGroupSummary(metrics);
3041
3379
  const hasRunning = metrics.running > 0;
3380
+ const isNestedGroup = groupKey.includes(">");
3042
3381
  const hasActiveItem = useMemo4(
3043
3382
  () => items.some((item) => isActiveWorkflowContainer(item, activeLoopSessionId, activeContainerIds)),
3044
3383
  [items, activeLoopSessionId, activeContainerIds]
3045
3384
  );
3046
3385
  const allSettled = metrics.total > 0 && metrics.running === 0 && metrics.completed + metrics.failed >= metrics.total;
3047
- const shouldAutoExpandGroup = hasRunning && (!scale.onlyExpandActiveRunning || hasActiveItem);
3386
+ const shouldAutoExpandGroup = hasRunning && (isNestedGroup || !scale.onlyExpandActiveRunning || hasActiveItem);
3048
3387
  const [expanded, setExpanded] = useState3(shouldAutoExpandGroup);
3049
3388
  useEffect5(() => {
3050
3389
  if (shouldAutoExpandGroup) {
@@ -3095,7 +3434,7 @@ function WorkflowParallelGroup({
3095
3434
  WorkflowExpandTrigger,
3096
3435
  {
3097
3436
  summary,
3098
- expandHint: !expanded && !hasRunning ? "(ctrl+o to expand)" : null,
3437
+ expandHint: !expanded && hasRunning ? "(click to expand agents)" : !expanded && !hasRunning ? "(ctrl+o to expand)" : null,
3099
3438
  onToggle: toggleExpanded,
3100
3439
  expanded,
3101
3440
  testId: `lax-workflow-parallel-group-summary-${groupKey}`,
@@ -3107,6 +3446,24 @@ function WorkflowParallelGroup({
3107
3446
  }
3108
3447
  );
3109
3448
  }
3449
+ function workflowParallelGroupPropsEqual(prev, next) {
3450
+ if (prev.groupKey !== next.groupKey) return false;
3451
+ if (!workflowContainerViewPropsEqual(prev, next)) return false;
3452
+ if (prev.items.length !== next.items.length) return false;
3453
+ for (let i = 0; i < prev.items.length; i += 1) {
3454
+ const prevItem = prev.items[i];
3455
+ const nextItem = next.items[i];
3456
+ if (!workflowContainerNodeVisualEqual(prevItem, nextItem)) return false;
3457
+ const loopSessionId = prevItem.loopSessionId;
3458
+ if (loopSessionId) {
3459
+ if (prev.loopTreesBySessionId[loopSessionId] !== next.loopTreesBySessionId[loopSessionId]) {
3460
+ return false;
3461
+ }
3462
+ }
3463
+ }
3464
+ return true;
3465
+ }
3466
+ var WorkflowParallelGroup = memo3(WorkflowParallelGroupInner, workflowParallelGroupPropsEqual);
3110
3467
 
3111
3468
  // src/view/workflow/WorkflowRootContainer.tsx
3112
3469
  import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -3313,15 +3670,16 @@ function WorkflowSubworkflowContainer({
3313
3670
  children
3314
3671
  }
3315
3672
  ) : null,
3316
- isDone && !expanded ? /* @__PURE__ */ jsx14(
3673
+ !expanded && children ? /* @__PURE__ */ jsx14(
3317
3674
  WorkflowExpandTrigger,
3318
3675
  {
3319
- summary: `${node.title} \xB7 ${buildDoneSummary(node)}`,
3676
+ summary: `${node.title} \xB7 ${isDone ? buildDoneSummary(node) : "running"}`,
3320
3677
  summaryTestId: `${testId}-title`,
3321
3678
  teaser: teaser || void 0,
3679
+ expandHint: isRunning ? "(click to expand subworkflow)" : void 0,
3322
3680
  onToggle: () => setExpanded(true),
3323
3681
  expanded: false,
3324
- testId: `${testId}-done`,
3682
+ testId: isDone ? `${testId}-done` : `${testId}-running-collapsed`,
3325
3683
  teaserTestId: teaser ? `${testId}-teaser` : void 0
3326
3684
  }
3327
3685
  ) : null
@@ -3345,7 +3703,7 @@ function collectSiblingItems(node, containersById) {
3345
3703
  (child) => child.scope === "item"
3346
3704
  );
3347
3705
  }
3348
- function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps) {
3706
+ function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps, options) {
3349
3707
  const loopTree = resolveLoopTree2(node, loopTreesBySessionId);
3350
3708
  switch (node.scope) {
3351
3709
  case "aggregate":
@@ -3378,6 +3736,7 @@ function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, v
3378
3736
  node,
3379
3737
  loopTree,
3380
3738
  depth,
3739
+ suppressHostScopedLoop: options?.suppressHostScopedLoop,
3381
3740
  ...viewProps
3382
3741
  },
3383
3742
  node.container_id
@@ -3402,6 +3761,11 @@ function renderItemSiblings(items, groupKey, depth, containerTree, loopTreesBySe
3402
3761
  }
3403
3762
  return null;
3404
3763
  }
3764
+ function stageHasSubworkflowChild(stageId, containerTree) {
3765
+ return collectChildContainers(containerTree.containersById, stageId).some(
3766
+ (node) => node.scope === "subworkflow"
3767
+ );
3768
+ }
3405
3769
  function renderContainerChildren(parentId, containerTree, loopTreesBySessionId, viewProps, depth) {
3406
3770
  const children = collectChildContainers(containerTree.containersById, parentId);
3407
3771
  const items = children.filter((node) => node.scope === "item");
@@ -3432,13 +3796,21 @@ function renderContainerNode(node, containerTree, loopTreesBySessionId, viewProp
3432
3796
  );
3433
3797
  }
3434
3798
  if (node.scope === "stage") {
3799
+ const suppressHostScopedLoop = stageHasSubworkflowChild(node.container_id, containerTree);
3435
3800
  return /* @__PURE__ */ jsxs10(
3436
3801
  "div",
3437
3802
  {
3438
3803
  className: "lax-workflow-nested-block",
3439
3804
  "data-testid": `lax-workflow-nested-${node.scope}-${node.scope_key}`,
3440
3805
  children: [
3441
- renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps),
3806
+ renderLeafContainer(
3807
+ node,
3808
+ depth,
3809
+ containerTree,
3810
+ loopTreesBySessionId,
3811
+ viewProps,
3812
+ { suppressHostScopedLoop }
3813
+ ),
3442
3814
  hasNested ? /* @__PURE__ */ jsx15("div", { className: "lax-workflow-container-children", children: nested }) : null
3443
3815
  ]
3444
3816
  },
@@ -3788,6 +4160,14 @@ function WorkflowSession({
3788
4160
  storeRef.current,
3789
4161
  (s) => s.state.loopTreesBySessionId
3790
4162
  );
4163
+ const sessionStatus = useStore5(
4164
+ storeRef.current,
4165
+ (s) => s.state.status
4166
+ );
4167
+ const internalErrors = useStore5(
4168
+ storeRef.current,
4169
+ (s) => s.state.internalErrors
4170
+ );
3791
4171
  useEffect8(() => {
3792
4172
  const store = storeRef.current;
3793
4173
  const controller = new AbortController();
@@ -3795,10 +4175,10 @@ function WorkflowSession({
3795
4175
  store.getState().applyEvent(event, ctx);
3796
4176
  }, controller.signal).catch((err) => {
3797
4177
  if (err instanceof SseTransportTerminalError) {
3798
- const { state } = store.getState();
3799
- if (state.status === "running") {
3800
- store.setState({ state: { ...state, status: "error" } });
3801
- }
4178
+ store.getState().markAsError(err);
4179
+ onError?.(err);
4180
+ } else if (err instanceof Error) {
4181
+ onError?.(err);
3802
4182
  }
3803
4183
  });
3804
4184
  return () => {
@@ -3811,6 +4191,15 @@ function WorkflowSession({
3811
4191
  className: "lax-agent-session lax-workflow-session",
3812
4192
  "data-testid": "lax-workflow-session",
3813
4193
  children: [
4194
+ sessionStatus === "error" || internalErrors.length > 0 ? /* @__PURE__ */ jsx17(
4195
+ "div",
4196
+ {
4197
+ className: "lax-workflow-error",
4198
+ "data-testid": "lax-workflow-error",
4199
+ role: "alert",
4200
+ children: internalErrors[internalErrors.length - 1]?.message ?? "Workflow stream error"
4201
+ }
4202
+ ) : null,
3814
4203
  /* @__PURE__ */ jsx17(
3815
4204
  WorkflowChrome,
3816
4205
  {