langchain_agentx_stream_ui 0.1.9 → 0.2.2
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/{chunk-DP7V33X7.js → chunk-6Z4ZF36Z.js} +1 -1
- package/dist/chunk-6Z4ZF36Z.js.map +1 -0
- package/dist/{chunk-2T26L3VT.js → chunk-DGC43A5L.js} +5 -5
- package/dist/chunk-DGC43A5L.js.map +1 -0
- package/dist/{chunk-4RIOBLGB.js → chunk-GBY5DJ7L.js} +1 -1
- package/dist/chunk-GBY5DJ7L.js.map +1 -0
- package/dist/{chunk-ZNXAQ4ZZ.js → chunk-OFXC2GZI.js} +144 -64
- package/dist/chunk-OFXC2GZI.js.map +1 -0
- package/dist/{chunk-6DYISADG.js → chunk-T626YBJX.js} +4 -4
- package/dist/chunk-T626YBJX.js.map +1 -0
- package/dist/{chunk-7CLAU74Y.js → chunk-YUZXTGJJ.js} +2 -2
- package/dist/chunk-YUZXTGJJ.js.map +1 -0
- package/dist/index.d.ts +27 -11
- package/dist/index.js +999 -373
- package/dist/index.js.map +1 -1
- package/dist/multi-session.js +6 -6
- package/dist/tools-presentation.js +2 -2
- package/dist/tools.js +4 -4
- package/dist/virtual.js +3 -3
- package/package.json +8 -8
- package/dist/chunk-2T26L3VT.js.map +0 -1
- package/dist/chunk-4RIOBLGB.js.map +0 -1
- package/dist/chunk-6DYISADG.js.map +0 -1
- package/dist/chunk-7CLAU74Y.js.map +0 -1
- package/dist/chunk-DP7V33X7.js.map +0 -1
- package/dist/chunk-ZNXAQ4ZZ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
fixDiagramSvg,
|
|
24
24
|
markEventIdSeen,
|
|
25
25
|
shouldSkipDuplicateEvent
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-DGC43A5L.js";
|
|
27
27
|
import {
|
|
28
28
|
DefaultToolBody,
|
|
29
29
|
InteractionBusContext,
|
|
@@ -32,8 +32,8 @@ import {
|
|
|
32
32
|
getNoopInteractionBus,
|
|
33
33
|
resolveToolBody,
|
|
34
34
|
useInteractionBus
|
|
35
|
-
} from "./chunk-
|
|
36
|
-
import "./chunk-
|
|
35
|
+
} from "./chunk-T626YBJX.js";
|
|
36
|
+
import "./chunk-6Z4ZF36Z.js";
|
|
37
37
|
import {
|
|
38
38
|
CollapseKind,
|
|
39
39
|
CollapsedExploreNode,
|
|
@@ -111,7 +111,7 @@ import {
|
|
|
111
111
|
useSessionStatus,
|
|
112
112
|
useSessionViewOptions,
|
|
113
113
|
useTimeline
|
|
114
|
-
} from "./chunk-
|
|
114
|
+
} from "./chunk-OFXC2GZI.js";
|
|
115
115
|
import {
|
|
116
116
|
Agent,
|
|
117
117
|
AgentToolBody,
|
|
@@ -138,7 +138,7 @@ import {
|
|
|
138
138
|
WriteToolBody,
|
|
139
139
|
createDefaultToolRegistry,
|
|
140
140
|
formatAgentTitle
|
|
141
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-YUZXTGJJ.js";
|
|
142
142
|
import {
|
|
143
143
|
BodyBlockList,
|
|
144
144
|
DiffView,
|
|
@@ -154,7 +154,7 @@ import {
|
|
|
154
154
|
formatTimeoutFooter,
|
|
155
155
|
parseDiffText,
|
|
156
156
|
truncateCommand
|
|
157
|
-
} from "./chunk-
|
|
157
|
+
} from "./chunk-GBY5DJ7L.js";
|
|
158
158
|
|
|
159
159
|
// src/view/AgentSession.tsx
|
|
160
160
|
import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "react";
|
|
@@ -275,6 +275,14 @@ function transportLog(message, level = "warn") {
|
|
|
275
275
|
console.warn(line);
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
+
function isSseStreamTerminalEvent(agentEvent, expectedApplicationKind) {
|
|
279
|
+
const eventType = agentEvent.event_type;
|
|
280
|
+
if (eventType === "error") return true;
|
|
281
|
+
if (expectedApplicationKind === "workflow") {
|
|
282
|
+
return eventType === "workflow-end" || eventType === "workflow-failed";
|
|
283
|
+
}
|
|
284
|
+
return eventType === "finish";
|
|
285
|
+
}
|
|
278
286
|
function buildResumeStreamUrl(baseUrl, lastEventId, queryParam = "last_event_id") {
|
|
279
287
|
if (!lastEventId) return baseUrl;
|
|
280
288
|
const sep = baseUrl.includes("?") ? "&" : "?";
|
|
@@ -420,7 +428,7 @@ function createAgentxSseSource(options) {
|
|
|
420
428
|
} else {
|
|
421
429
|
flushBufferedDelta();
|
|
422
430
|
handler(agentEvent, ctx);
|
|
423
|
-
if (agentEvent
|
|
431
|
+
if (isSseStreamTerminalEvent(agentEvent, expectedApplicationKind)) {
|
|
424
432
|
streamTerminal = true;
|
|
425
433
|
}
|
|
426
434
|
}
|
|
@@ -626,6 +634,8 @@ function AgentSession({
|
|
|
626
634
|
workspaceRoot
|
|
627
635
|
]
|
|
628
636
|
);
|
|
637
|
+
const onErrorRef = useRef2(onError);
|
|
638
|
+
onErrorRef.current = onError;
|
|
629
639
|
useEffect2(() => {
|
|
630
640
|
const store = storeRef.current;
|
|
631
641
|
const controller = new AbortController();
|
|
@@ -637,12 +647,15 @@ function AgentSession({
|
|
|
637
647
|
if (tree.status === "running" || tree.status === "connecting") {
|
|
638
648
|
store.setState({ tree: { ...tree, status: "error" } });
|
|
639
649
|
}
|
|
650
|
+
onErrorRef.current?.(err);
|
|
651
|
+
} else if (err instanceof Error) {
|
|
652
|
+
onErrorRef.current?.(err);
|
|
640
653
|
}
|
|
641
654
|
});
|
|
642
655
|
return () => {
|
|
643
656
|
controller.abort();
|
|
644
657
|
};
|
|
645
|
-
}, [source
|
|
658
|
+
}, [source]);
|
|
646
659
|
return /* @__PURE__ */ jsx3(SessionStoreContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx3(InteractionBusContext.Provider, { value: busRef, children: /* @__PURE__ */ jsx3(NodeRegistryContext.Provider, { value: registryRef, children: /* @__PURE__ */ jsx3(MarkdownRendererContext.Provider, { value: markdownRenderer, children: /* @__PURE__ */ jsx3(ToolDisplayOptionsContext.Provider, { value: toolDisplayRef, children: /* @__PURE__ */ jsx3(SessionViewOptionsContext.Provider, { value: sessionViewRef, children: /* @__PURE__ */ jsxs("div", { className: "lax-agent-session", "data-testid": "lax-agent-session", children: [
|
|
647
660
|
children ?? /* @__PURE__ */ jsx3(
|
|
648
661
|
AgentLoopView,
|
|
@@ -762,6 +775,9 @@ function createEmptyWorkflowSessionState() {
|
|
|
762
775
|
}
|
|
763
776
|
|
|
764
777
|
// src/core/workflow/workflowLoopTreeUtils.ts
|
|
778
|
+
function isWorkflowLoopTreeActive(tree) {
|
|
779
|
+
return tree?.status === "running" || tree?.status === "connecting";
|
|
780
|
+
}
|
|
765
781
|
function createWorkflowRunningLoopTree(seedMs = Date.now()) {
|
|
766
782
|
const tree = createEmptyTree();
|
|
767
783
|
tree.status = "running";
|
|
@@ -793,6 +809,168 @@ function finalizeWorkflowLoopTree(tree, terminalStatus = "done") {
|
|
|
793
809
|
};
|
|
794
810
|
}
|
|
795
811
|
|
|
812
|
+
// src/core/workflow/stageDoneSummary.ts
|
|
813
|
+
function countToolUses(tree) {
|
|
814
|
+
return Object.values(tree.byId).filter((node) => node.kind === "tool_call").length;
|
|
815
|
+
}
|
|
816
|
+
function computeDurationSeconds(tree) {
|
|
817
|
+
const started = tree.meta.startedAt;
|
|
818
|
+
if (started == null) return null;
|
|
819
|
+
let maxTs = started;
|
|
820
|
+
for (const node of Object.values(tree.byId)) {
|
|
821
|
+
if ("endedAt" in node && typeof node.endedAt === "number") {
|
|
822
|
+
maxTs = Math.max(maxTs, node.endedAt);
|
|
823
|
+
}
|
|
824
|
+
if ("startedAt" in node && typeof node.startedAt === "number") {
|
|
825
|
+
maxTs = Math.max(maxTs, node.startedAt);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const seconds = Math.round((maxTs - started) / 1e3);
|
|
829
|
+
return seconds > 0 ? seconds : null;
|
|
830
|
+
}
|
|
831
|
+
function lastTextContent(tree) {
|
|
832
|
+
const textNodes = Object.values(tree.byId).filter((node) => node.kind === "text");
|
|
833
|
+
if (textNodes.length === 0) return "";
|
|
834
|
+
return textNodes[textNodes.length - 1].accumulated.trim();
|
|
835
|
+
}
|
|
836
|
+
function buildStageDoneSummary(tree) {
|
|
837
|
+
if (tree.status !== "done" && tree.status !== "error") return null;
|
|
838
|
+
const toolUses = countToolUses(tree);
|
|
839
|
+
const duration = computeDurationSeconds(tree);
|
|
840
|
+
const durationSuffix = duration != null ? ` \xB7 ${duration}s` : "";
|
|
841
|
+
const label = toolUses > 0 ? `Done (${toolUses} tool use${toolUses === 1 ? "" : "s"}${durationSuffix})` : `Done (1 agent turn${durationSuffix})`;
|
|
842
|
+
const fullText = lastTextContent(tree);
|
|
843
|
+
const lines = fullText.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
844
|
+
const teaser = lines[0] ?? "";
|
|
845
|
+
const extraLines = Math.max(0, lines.length - 1);
|
|
846
|
+
return { label, teaser, extraLines };
|
|
847
|
+
}
|
|
848
|
+
function formatTeaserExpandHint(extraLines, verbose) {
|
|
849
|
+
if (extraLines <= 0) return null;
|
|
850
|
+
const shown = verbose ? Math.min(extraLines, 2) : 0;
|
|
851
|
+
const hidden = extraLines - shown;
|
|
852
|
+
if (hidden <= 0) return null;
|
|
853
|
+
return `\u2026 +${hidden} lines (ctrl+o to expand)`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/core/workflow/workflowLoopEviction.ts
|
|
857
|
+
function containersByLoopSessionId(tree) {
|
|
858
|
+
const map = /* @__PURE__ */ new Map();
|
|
859
|
+
for (const node of Object.values(tree.containersById)) {
|
|
860
|
+
if (node.loopSessionId) map.set(node.loopSessionId, node);
|
|
861
|
+
}
|
|
862
|
+
return map;
|
|
863
|
+
}
|
|
864
|
+
function hasRunningParallelSibling(node, tree) {
|
|
865
|
+
if (node.scope !== "item" || !node.parent_container_id) return false;
|
|
866
|
+
for (const sibling of Object.values(tree.containersById)) {
|
|
867
|
+
if (sibling.container_id === node.container_id) continue;
|
|
868
|
+
if (sibling.scope !== "item") continue;
|
|
869
|
+
if (sibling.parent_container_id !== node.parent_container_id) continue;
|
|
870
|
+
if (sibling.status === "running" || sibling.status === "retrying") return true;
|
|
871
|
+
}
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
function collectProtectedLoopSessionIds(state, pinnedLoopSessionIds) {
|
|
875
|
+
const protectedIds = new Set(Object.keys(pinnedLoopSessionIds));
|
|
876
|
+
const byLoop = containersByLoopSessionId(state.containerTree);
|
|
877
|
+
if (state.activeLoopSessionId) {
|
|
878
|
+
const activeContainer = byLoop.get(state.activeLoopSessionId);
|
|
879
|
+
if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
|
|
880
|
+
protectedIds.add(state.activeLoopSessionId);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
for (const containerId of state.containerTree.activeContainerIds) {
|
|
884
|
+
const node = state.containerTree.containersById[containerId];
|
|
885
|
+
if (!node?.loopSessionId) continue;
|
|
886
|
+
if (node.status === "running" || node.status === "retrying") {
|
|
887
|
+
protectedIds.add(node.loopSessionId);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
for (const node of Object.values(state.containerTree.containersById)) {
|
|
891
|
+
if (!node.loopSessionId) continue;
|
|
892
|
+
if (node.status === "running" || node.status === "retrying") {
|
|
893
|
+
protectedIds.add(node.loopSessionId);
|
|
894
|
+
}
|
|
895
|
+
if ((node.status === "completed" || node.status === "failed") && hasRunningParallelSibling(node, state.containerTree)) {
|
|
896
|
+
protectedIds.add(node.loopSessionId);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return protectedIds;
|
|
900
|
+
}
|
|
901
|
+
function evictInactiveLoopTrees(state, pinnedLoopSessionIds) {
|
|
902
|
+
const protectedIds = collectProtectedLoopSessionIds(state, pinnedLoopSessionIds);
|
|
903
|
+
for (const id of state.hotCompletedLoopSessionIds) {
|
|
904
|
+
protectedIds.add(id);
|
|
905
|
+
}
|
|
906
|
+
const byLoop = containersByLoopSessionId(state.containerTree);
|
|
907
|
+
const nextLoops = { ...state.loopTreesBySessionId };
|
|
908
|
+
const nextSummaries = {
|
|
909
|
+
...state.stageDoneSummariesByContainerId
|
|
910
|
+
};
|
|
911
|
+
for (const [loopSessionId, tree] of Object.entries(state.loopTreesBySessionId)) {
|
|
912
|
+
if (protectedIds.has(loopSessionId)) continue;
|
|
913
|
+
const container = byLoop.get(loopSessionId);
|
|
914
|
+
if (!container) continue;
|
|
915
|
+
if (container.status !== "completed" && container.status !== "failed") continue;
|
|
916
|
+
const summary = buildStageDoneSummary(tree);
|
|
917
|
+
if (summary) {
|
|
918
|
+
nextSummaries[container.container_id] = summary;
|
|
919
|
+
}
|
|
920
|
+
delete nextLoops[loopSessionId];
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
...state,
|
|
924
|
+
loopTreesBySessionId: nextLoops,
|
|
925
|
+
stageDoneSummariesByContainerId: nextSummaries
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function touchCompletedLoopSession(state, loopSessionId) {
|
|
929
|
+
const ids = state.hotCompletedLoopSessionIds.filter((id) => id !== loopSessionId);
|
|
930
|
+
ids.push(loopSessionId);
|
|
931
|
+
return {
|
|
932
|
+
...state,
|
|
933
|
+
hotCompletedLoopSessionIds: ids
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function evictCompletedLoopOverflow(state, maxHydratedCompletedLoops) {
|
|
937
|
+
if (state.hotCompletedLoopSessionIds.length <= maxHydratedCompletedLoops) {
|
|
938
|
+
return state;
|
|
939
|
+
}
|
|
940
|
+
const overflow = state.hotCompletedLoopSessionIds.length - maxHydratedCompletedLoops;
|
|
941
|
+
const candidateEvictIds = state.hotCompletedLoopSessionIds.slice(0, overflow);
|
|
942
|
+
let trimmedIds = state.hotCompletedLoopSessionIds.slice(overflow);
|
|
943
|
+
const byLoop = containersByLoopSessionId(state.containerTree);
|
|
944
|
+
const nextLoops = { ...state.loopTreesBySessionId };
|
|
945
|
+
const nextSummaries = {
|
|
946
|
+
...state.stageDoneSummariesByContainerId
|
|
947
|
+
};
|
|
948
|
+
for (const id of candidateEvictIds) {
|
|
949
|
+
const tree = nextLoops[id];
|
|
950
|
+
if (!tree) continue;
|
|
951
|
+
const container = byLoop.get(id);
|
|
952
|
+
if (container && hasRunningParallelSibling(container, state.containerTree)) {
|
|
953
|
+
if (!trimmedIds.includes(id)) {
|
|
954
|
+
trimmedIds = [id, ...trimmedIds];
|
|
955
|
+
}
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (container) {
|
|
959
|
+
const summary = buildStageDoneSummary(tree);
|
|
960
|
+
if (summary) {
|
|
961
|
+
nextSummaries[container.container_id] = summary;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
delete nextLoops[id];
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
...state,
|
|
968
|
+
loopTreesBySessionId: nextLoops,
|
|
969
|
+
stageDoneSummariesByContainerId: nextSummaries,
|
|
970
|
+
hotCompletedLoopSessionIds: trimmedIds
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
796
974
|
// src/types/workflowDisplay.ts
|
|
797
975
|
function readDisplayPayload(data) {
|
|
798
976
|
const display = data.display;
|
|
@@ -1169,17 +1347,24 @@ var WorkflowContainerStateMachine = class {
|
|
|
1169
1347
|
(node) => workflowPath === node.workflow_path || workflowPath.startsWith(`${node.workflow_path}>`)
|
|
1170
1348
|
);
|
|
1171
1349
|
if (prefixCandidates.length > 0) {
|
|
1172
|
-
const
|
|
1173
|
-
(
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1350
|
+
const parentCandidates = prefixCandidates.filter((node) => {
|
|
1351
|
+
if (scope === "item") return node.scope !== "item";
|
|
1352
|
+
if (scope === "aggregate") return node.scope !== "item" && node.scope !== "aggregate";
|
|
1353
|
+
return node.scope !== scope;
|
|
1354
|
+
});
|
|
1355
|
+
if (parentCandidates.length > 0) {
|
|
1356
|
+
const parent = parentCandidates.reduce(
|
|
1357
|
+
(best, node) => this.active.compareContainerRank(node.container_id, best.container_id) > 0 ? node : best
|
|
1358
|
+
);
|
|
1359
|
+
if (parent.workflow_path === workflowPath && parent.scope === "workflow") {
|
|
1360
|
+
return parent.container_id;
|
|
1361
|
+
}
|
|
1362
|
+
if (workflowPath.startsWith(`${parent.workflow_path}>`)) {
|
|
1363
|
+
return parent.container_id;
|
|
1364
|
+
}
|
|
1365
|
+
if (parent.workflow_path === workflowPath && parent.scope !== scope) {
|
|
1366
|
+
return parent.container_id;
|
|
1367
|
+
}
|
|
1183
1368
|
}
|
|
1184
1369
|
}
|
|
1185
1370
|
const workflowCandidates = openNodes.filter((node) => node.scope === "workflow");
|
|
@@ -1277,6 +1462,167 @@ function loopTaskScopeForStructureOpen(eventType) {
|
|
|
1277
1462
|
return LOOP_TASK_OPEN_SCOPES[eventType];
|
|
1278
1463
|
}
|
|
1279
1464
|
|
|
1465
|
+
// src/core/workflowStructureEvents.ts
|
|
1466
|
+
var WORKFLOW_STRUCTURE_EVENT_PREFIXES = [
|
|
1467
|
+
"workflow-",
|
|
1468
|
+
"stage-",
|
|
1469
|
+
"parallel-",
|
|
1470
|
+
"route-",
|
|
1471
|
+
"subworkflow-"
|
|
1472
|
+
];
|
|
1473
|
+
var WORKFLOW_STRUCTURE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1474
|
+
"workflow-start",
|
|
1475
|
+
"workflow-end",
|
|
1476
|
+
"workflow-failed",
|
|
1477
|
+
"subworkflow-start",
|
|
1478
|
+
"subworkflow-end",
|
|
1479
|
+
"stage-start",
|
|
1480
|
+
"stage-done",
|
|
1481
|
+
"stage-failed",
|
|
1482
|
+
"parallel-item-start",
|
|
1483
|
+
"parallel-item-done",
|
|
1484
|
+
"parallel-item-failed",
|
|
1485
|
+
"parallel-aggregate-start",
|
|
1486
|
+
"parallel-aggregate-done",
|
|
1487
|
+
"parallel-aggregate-failed",
|
|
1488
|
+
"route-branch-start",
|
|
1489
|
+
"route-branch-done",
|
|
1490
|
+
"route-branch-failed"
|
|
1491
|
+
]);
|
|
1492
|
+
var AGENT_LOOP_REPLAY_EVENT_TYPES = new Set(
|
|
1493
|
+
Object.keys(DEFAULT_EVENT_TIERS)
|
|
1494
|
+
);
|
|
1495
|
+
function isWorkflowStructureEventType(eventType) {
|
|
1496
|
+
if (WORKFLOW_STRUCTURE_EVENT_TYPES.has(eventType)) return true;
|
|
1497
|
+
return WORKFLOW_STRUCTURE_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix));
|
|
1498
|
+
}
|
|
1499
|
+
function isAgentLoopReplayEventType(eventType) {
|
|
1500
|
+
return AGENT_LOOP_REPLAY_EVENT_TYPES.has(eventType);
|
|
1501
|
+
}
|
|
1502
|
+
function filterAgentLoopReplayEvents(events) {
|
|
1503
|
+
return events.filter(
|
|
1504
|
+
(event) => isAgentLoopReplayEventType(event.event_type) && !isWorkflowStructureEventType(event.event_type)
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/core/workflow/workflowPathUtils.ts
|
|
1509
|
+
function splitWorkflowPathSegments(workflowPath) {
|
|
1510
|
+
return workflowPath.split(">").filter(Boolean);
|
|
1511
|
+
}
|
|
1512
|
+
function parentWorkflowPath(workflowPath) {
|
|
1513
|
+
const segments = splitWorkflowPathSegments(workflowPath);
|
|
1514
|
+
if (segments.length <= 1) return null;
|
|
1515
|
+
return segments.slice(0, -1).join(">");
|
|
1516
|
+
}
|
|
1517
|
+
function lastWorkflowPathSegment(workflowPath) {
|
|
1518
|
+
const segments = splitWorkflowPathSegments(workflowPath);
|
|
1519
|
+
return segments.length > 0 ? segments[segments.length - 1] : null;
|
|
1520
|
+
}
|
|
1521
|
+
function isWorkflowPathUnderAncestor(workflowPath, ancestorPath) {
|
|
1522
|
+
if (workflowPath === ancestorPath) return false;
|
|
1523
|
+
const prefix = `${ancestorPath}>`;
|
|
1524
|
+
return workflowPath.startsWith(prefix);
|
|
1525
|
+
}
|
|
1526
|
+
function isWorkflowPathEqualOrUnder(workflowPath, ancestorPath) {
|
|
1527
|
+
return workflowPath === ancestorPath || isWorkflowPathUnderAncestor(workflowPath, ancestorPath);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/core/workflow/nativeNestedUtils.ts
|
|
1531
|
+
function readString3(data, key) {
|
|
1532
|
+
const value = data[key];
|
|
1533
|
+
return typeof value === "string" ? value : void 0;
|
|
1534
|
+
}
|
|
1535
|
+
function readNumber(data, key) {
|
|
1536
|
+
const value = data[key];
|
|
1537
|
+
return typeof value === "number" ? value : void 0;
|
|
1538
|
+
}
|
|
1539
|
+
function isNativeNestedStructureEvent(event) {
|
|
1540
|
+
if (!isWorkflowStructureEventType(event.event_type)) return false;
|
|
1541
|
+
const data = event.data ?? {};
|
|
1542
|
+
const path = readString3(data, "workflow_path") ?? "";
|
|
1543
|
+
const depth = readNumber(data, "workflow_depth") ?? 0;
|
|
1544
|
+
return depth >= 1 && path.includes(">");
|
|
1545
|
+
}
|
|
1546
|
+
function resolveHostStageContainerId(tree, workflowPath) {
|
|
1547
|
+
const parentPath = parentWorkflowPath(workflowPath);
|
|
1548
|
+
const childSegment = lastWorkflowPathSegment(workflowPath);
|
|
1549
|
+
if (!parentPath || !childSegment) return null;
|
|
1550
|
+
const stageId = buildContainerId(parentPath, "stage", childSegment);
|
|
1551
|
+
return tree.containersById[stageId] != null ? stageId : null;
|
|
1552
|
+
}
|
|
1553
|
+
function isHostStageNestedChildPath(tree, workflowPath) {
|
|
1554
|
+
return resolveHostStageContainerId(tree, workflowPath) != null;
|
|
1555
|
+
}
|
|
1556
|
+
function hasNativeNestedStructureInTree(tree) {
|
|
1557
|
+
for (const node of Object.values(tree.containersById)) {
|
|
1558
|
+
if (node.workflow_depth < 1) continue;
|
|
1559
|
+
if (!node.workflow_path.includes(">")) continue;
|
|
1560
|
+
if (isHostStageNestedChildPath(tree, node.workflow_path)) {
|
|
1561
|
+
return true;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
function clearHostStageLoopSession(tree, hostStageId) {
|
|
1567
|
+
if (!hostStageId) return tree;
|
|
1568
|
+
const hostStage = tree.containersById[hostStageId];
|
|
1569
|
+
if (!hostStage || hostStage.scope !== "stage" || !hostStage.loopSessionId) return tree;
|
|
1570
|
+
return {
|
|
1571
|
+
...tree,
|
|
1572
|
+
containersById: {
|
|
1573
|
+
...tree.containersById,
|
|
1574
|
+
[hostStageId]: { ...hostStage, loopSessionId: null }
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function isNestedHostStageShell(tree, stageNode) {
|
|
1579
|
+
if (stageNode.scope !== "stage") return false;
|
|
1580
|
+
return Object.values(tree.containersById).some(
|
|
1581
|
+
(node) => node.scope === "subworkflow" && node.parent_container_id === stageNode.container_id
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
function clearHostStageLoopOnNestedChildOpen(tree, event) {
|
|
1585
|
+
const eventType = event.event_type;
|
|
1586
|
+
if (eventType !== "subworkflow-start" && eventType !== "parallel-item-start") return tree;
|
|
1587
|
+
const data = event.data ?? {};
|
|
1588
|
+
const workflowPath = readString3(data, "workflow_path");
|
|
1589
|
+
if (!workflowPath) return tree;
|
|
1590
|
+
if (eventType === "subworkflow-start") {
|
|
1591
|
+
const childWorkflowId = readString3(data, "child_workflow_id");
|
|
1592
|
+
if (!childWorkflowId) return tree;
|
|
1593
|
+
const subId = buildContainerId(workflowPath, "subworkflow", childWorkflowId);
|
|
1594
|
+
const sub = tree.containersById[subId];
|
|
1595
|
+
const hostStageId = sub?.parent_container_id ?? resolveHostStageContainerId(tree, workflowPath);
|
|
1596
|
+
return clearHostStageLoopSession(tree, hostStageId);
|
|
1597
|
+
}
|
|
1598
|
+
return clearHostStageLoopSession(tree, resolveHostStageContainerId(tree, workflowPath));
|
|
1599
|
+
}
|
|
1600
|
+
function dropStaleEmbeddedPathsOnNativeSubworkflowStart(tree, event) {
|
|
1601
|
+
if (event.event_type !== "subworkflow-start" || !isNativeNestedStructureEvent(event)) {
|
|
1602
|
+
return tree;
|
|
1603
|
+
}
|
|
1604
|
+
const data = event.data ?? {};
|
|
1605
|
+
const nativeChildPath = readString3(data, "workflow_path");
|
|
1606
|
+
if (!nativeChildPath) return tree;
|
|
1607
|
+
const rootAnchor = splitWorkflowPathSegments(nativeChildPath)[0];
|
|
1608
|
+
if (!rootAnchor) return tree;
|
|
1609
|
+
let changed = false;
|
|
1610
|
+
const containersById = { ...tree.containersById };
|
|
1611
|
+
let activeContainerIds = [...tree.activeContainerIds];
|
|
1612
|
+
for (const [id, node] of Object.entries(tree.containersById)) {
|
|
1613
|
+
if (node.workflow_depth < 1) continue;
|
|
1614
|
+
if (!node.workflow_path.startsWith(`${rootAnchor}>`)) continue;
|
|
1615
|
+
if (node.workflow_path === nativeChildPath || isWorkflowPathEqualOrUnder(node.workflow_path, nativeChildPath)) {
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (isHostStageNestedChildPath(tree, node.workflow_path)) continue;
|
|
1619
|
+
delete containersById[id];
|
|
1620
|
+
activeContainerIds = activeContainerIds.filter((cid) => cid !== id);
|
|
1621
|
+
changed = true;
|
|
1622
|
+
}
|
|
1623
|
+
return changed ? { ...tree, containersById, activeContainerIds } : tree;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1280
1626
|
// src/core/workflow/embeddedSubworkflowUtils.ts
|
|
1281
1627
|
function parseLoopSessionParts(sessionId) {
|
|
1282
1628
|
const trimmed = sessionId.trim();
|
|
@@ -1378,7 +1724,20 @@ function completeOpenItemsUnderSubworkflow(tree, subworkflowId) {
|
|
|
1378
1724
|
function resolveLeafScope(taskKey) {
|
|
1379
1725
|
return taskKey === "aggregate" ? "aggregate" : "item";
|
|
1380
1726
|
}
|
|
1727
|
+
function shouldUseEmbeddedSynthesis(treeOrState, pendingEvent) {
|
|
1728
|
+
const tree = "containerTree" in treeOrState ? treeOrState.containerTree : treeOrState;
|
|
1729
|
+
if (pendingEvent && isNativeNestedStructureEvent(pendingEvent)) {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
if (hasNativeNestedStructureInTree(tree)) {
|
|
1733
|
+
return false;
|
|
1734
|
+
}
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1381
1737
|
function ensureEmbeddedSubworkflowContainers(tree, loopSessionId) {
|
|
1738
|
+
if (!shouldUseEmbeddedSynthesis(tree)) {
|
|
1739
|
+
return tree;
|
|
1740
|
+
}
|
|
1382
1741
|
const root = resolveRootWorkflow(tree);
|
|
1383
1742
|
const parts = parseLoopSessionParts(loopSessionId);
|
|
1384
1743
|
if (!root || !parts || !isEmbeddedChildLoopSession(loopSessionId, root.workflowId)) {
|
|
@@ -1461,11 +1820,39 @@ function findContainerIdByLoopSession(tree, loopSessionId) {
|
|
|
1461
1820
|
}
|
|
1462
1821
|
return null;
|
|
1463
1822
|
}
|
|
1823
|
+
var PARALLEL_ITEM_STRUCTURE_EVENTS = /* @__PURE__ */ new Set([
|
|
1824
|
+
"parallel-item-start",
|
|
1825
|
+
"parallel-item-done",
|
|
1826
|
+
"parallel-item-failed"
|
|
1827
|
+
]);
|
|
1828
|
+
function readItemKey(data) {
|
|
1829
|
+
const value = data.item_key;
|
|
1830
|
+
return typeof value === "string" ? value : void 0;
|
|
1831
|
+
}
|
|
1832
|
+
function resolveParallelItemLoopSessionId(eventType, eventSessionId, derivedLoopSessionId, data) {
|
|
1833
|
+
if (!PARALLEL_ITEM_STRUCTURE_EVENTS.has(eventType) || !eventSessionId) return null;
|
|
1834
|
+
if (eventSessionId === derivedLoopSessionId) return null;
|
|
1835
|
+
const itemKey = readItemKey(data);
|
|
1836
|
+
const parsed = parseLoopSessionParts(eventSessionId);
|
|
1837
|
+
if (itemKey && parsed?.taskKey === itemKey) {
|
|
1838
|
+
return eventSessionId;
|
|
1839
|
+
}
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1464
1842
|
function resolveStructureOpenLoopSessionId(eventType, eventSessionId, derivedLoopSessionId, data) {
|
|
1465
1843
|
const rootWorkflowId = readWorkflowId(data) ?? "";
|
|
1466
1844
|
if ((eventType === "parallel-aggregate-start" || eventType === "parallel-aggregate-done" || eventType === "parallel-aggregate-failed") && eventSessionId && isEmbeddedChildLoopSession(eventSessionId, rootWorkflowId)) {
|
|
1467
1845
|
return eventSessionId;
|
|
1468
1846
|
}
|
|
1847
|
+
const parallelItemSessionId = resolveParallelItemLoopSessionId(
|
|
1848
|
+
eventType,
|
|
1849
|
+
eventSessionId,
|
|
1850
|
+
derivedLoopSessionId,
|
|
1851
|
+
data
|
|
1852
|
+
);
|
|
1853
|
+
if (parallelItemSessionId) {
|
|
1854
|
+
return parallelItemSessionId;
|
|
1855
|
+
}
|
|
1469
1856
|
return derivedLoopSessionId;
|
|
1470
1857
|
}
|
|
1471
1858
|
var EMBEDDED_AGGREGATE_STRUCTURE_EVENTS = /* @__PURE__ */ new Set([
|
|
@@ -1483,7 +1870,7 @@ function patchEmbeddedStructureEvent(event, rootWorkflowId) {
|
|
|
1483
1870
|
const parts = parseLoopSessionParts(event.session_id);
|
|
1484
1871
|
if (!parts) return event;
|
|
1485
1872
|
const data = event.data ?? {};
|
|
1486
|
-
const rootPath =
|
|
1873
|
+
const rootPath = readString4(data, "workflow_path") ?? rootWorkflowId;
|
|
1487
1874
|
const childPath = `${rootPath}>${parts.workflowId}`;
|
|
1488
1875
|
const workflowDepth = typeof data.workflow_depth === "number" ? data.workflow_depth + 1 : 1;
|
|
1489
1876
|
return {
|
|
@@ -1495,14 +1882,17 @@ function patchEmbeddedStructureEvent(event, rootWorkflowId) {
|
|
|
1495
1882
|
}
|
|
1496
1883
|
};
|
|
1497
1884
|
}
|
|
1498
|
-
function
|
|
1885
|
+
function readString4(data, key) {
|
|
1499
1886
|
const value = data[key];
|
|
1500
1887
|
return typeof value === "string" ? value : void 0;
|
|
1501
1888
|
}
|
|
1502
1889
|
function isDescendantOf(containersById, ancestorId, nodeId) {
|
|
1890
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1503
1891
|
let current = containersById[nodeId]?.parent_container_id ?? null;
|
|
1504
1892
|
while (current) {
|
|
1505
1893
|
if (current === ancestorId) return true;
|
|
1894
|
+
if (seen.has(current)) return false;
|
|
1895
|
+
seen.add(current);
|
|
1506
1896
|
current = containersById[current]?.parent_container_id ?? null;
|
|
1507
1897
|
}
|
|
1508
1898
|
return false;
|
|
@@ -1515,6 +1905,9 @@ function settleCompletedStageSubtree(tree, stageContainerId) {
|
|
|
1515
1905
|
}
|
|
1516
1906
|
if (node.container_id === stageContainerId) continue;
|
|
1517
1907
|
if (node.status === "completed" || node.status === "failed") continue;
|
|
1908
|
+
if (node.status === "running" || node.status === "retrying" || node.status === "pending" || node.status === "blocked") {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1518
1911
|
next = upsertRuntimeNode(next, {
|
|
1519
1912
|
...node,
|
|
1520
1913
|
status: "completed",
|
|
@@ -1530,51 +1923,8 @@ function readWorkflowId(data) {
|
|
|
1530
1923
|
return readWorkflowIdFromData(data);
|
|
1531
1924
|
}
|
|
1532
1925
|
|
|
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
1926
|
// src/core/workflowReducer.ts
|
|
1577
|
-
function
|
|
1927
|
+
function readString5(data, key) {
|
|
1578
1928
|
const value = data[key];
|
|
1579
1929
|
return typeof value === "string" ? value : void 0;
|
|
1580
1930
|
}
|
|
@@ -1673,7 +2023,7 @@ function prebindLoopSessionFromStructureOpen(tree, event) {
|
|
|
1673
2023
|
loopSessionId = derivedLoopSessionId;
|
|
1674
2024
|
}
|
|
1675
2025
|
let nextTree = event.event_type === "parallel-aggregate-start" ? ensureEmbeddedSubworkflowContainers(tree, loopSessionId) : tree;
|
|
1676
|
-
const workflowPath =
|
|
2026
|
+
const workflowPath = readString5(data, "workflow_path") ?? "";
|
|
1677
2027
|
const scopeKey = resolveScopeKey(event.event_type, data);
|
|
1678
2028
|
const containerId = buildContainerId(workflowPath, scope, scopeKey);
|
|
1679
2029
|
let targetId = containerId;
|
|
@@ -1739,22 +2089,31 @@ function findContainerByLoopSession(tree, loopSessionId) {
|
|
|
1739
2089
|
return findContainerIdByLoopSession(tree, loopSessionId);
|
|
1740
2090
|
}
|
|
1741
2091
|
var WORKFLOW_DEFAULT_LOOP_SESSION_ID = "workflow-loop-default";
|
|
1742
|
-
function resolveAgentLoopSessionId(event, activeLoopSessionId) {
|
|
2092
|
+
function resolveAgentLoopSessionId(event, activeLoopSessionId, containerTree) {
|
|
1743
2093
|
if (event.session_id) return event.session_id;
|
|
1744
2094
|
if (isWorkflowStructureEventType(event.event_type)) return null;
|
|
1745
2095
|
if (event.event_type === "task-list-snapshot") return null;
|
|
1746
2096
|
const derived = deriveLoopSessionIdFromAgentEvent(event);
|
|
1747
2097
|
if (derived) return derived;
|
|
2098
|
+
const byLoop = containersByLoopSessionId(containerTree);
|
|
2099
|
+
if (activeLoopSessionId) {
|
|
2100
|
+
const activeContainer = byLoop.get(activeLoopSessionId);
|
|
2101
|
+
if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
|
|
2102
|
+
return activeLoopSessionId;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
const runningLoopIds = [...byLoop.entries()].filter(([, node]) => node.status === "running" || node.status === "retrying").map(([loopSessionId]) => loopSessionId);
|
|
2106
|
+
if (runningLoopIds.length === 1) return runningLoopIds[0];
|
|
1748
2107
|
if (activeLoopSessionId) return activeLoopSessionId;
|
|
1749
2108
|
return WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
1750
2109
|
}
|
|
1751
2110
|
function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
|
|
1752
|
-
const sessionId = resolveAgentLoopSessionId(event, activeLoopSessionId);
|
|
2111
|
+
const sessionId = resolveAgentLoopSessionId(event, activeLoopSessionId, tree);
|
|
1753
2112
|
if (!sessionId) return tree;
|
|
1754
2113
|
let nextTree = ensureEmbeddedSubworkflowContainers(tree, sessionId);
|
|
1755
2114
|
if (findContainerByLoopSession(nextTree, sessionId)) return nextTree;
|
|
1756
2115
|
const data = event.data ?? {};
|
|
1757
|
-
const workflowPath =
|
|
2116
|
+
const workflowPath = readString5(data, "workflow_path") ?? null;
|
|
1758
2117
|
const scopeKey = resolveContentScopeKey(data);
|
|
1759
2118
|
let targetId = findContainerByLoopSession(nextTree, sessionId);
|
|
1760
2119
|
if (!targetId && scopeKey) {
|
|
@@ -1765,12 +2124,17 @@ function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
|
|
|
1765
2124
|
);
|
|
1766
2125
|
}
|
|
1767
2126
|
if (!targetId) {
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
2127
|
+
const fallbackScopes = ["item", "aggregate", "stage"];
|
|
2128
|
+
for (const scope of fallbackScopes) {
|
|
2129
|
+
for (const containerId of nextTree.activeContainerIds) {
|
|
2130
|
+
const node2 = nextTree.containersById[containerId];
|
|
2131
|
+
if (!node2 || node2.loopSessionId) continue;
|
|
2132
|
+
if (node2.scope !== scope) continue;
|
|
2133
|
+
if (node2.scope === "stage" && isNestedHostStageShell(nextTree, node2)) continue;
|
|
1771
2134
|
targetId = containerId;
|
|
1772
2135
|
break;
|
|
1773
2136
|
}
|
|
2137
|
+
if (targetId) break;
|
|
1774
2138
|
}
|
|
1775
2139
|
}
|
|
1776
2140
|
if (!targetId) {
|
|
@@ -1783,6 +2147,9 @@ function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
|
|
|
1783
2147
|
if (!targetId) return nextTree;
|
|
1784
2148
|
const node = nextTree.containersById[targetId];
|
|
1785
2149
|
if (!node || node.loopSessionId) return nextTree;
|
|
2150
|
+
if (node.scope === "stage" && isNestedHostStageShell(nextTree, node)) {
|
|
2151
|
+
return nextTree;
|
|
2152
|
+
}
|
|
1786
2153
|
if (node.scope !== "stage" && node.scope !== "item" && node.scope !== "branch" && node.scope !== "aggregate") {
|
|
1787
2154
|
return nextTree;
|
|
1788
2155
|
}
|
|
@@ -1882,8 +2249,15 @@ function finalizeLoopTreeOnStructureClose(state, event) {
|
|
|
1882
2249
|
}
|
|
1883
2250
|
function applyStructureEvent(state, event) {
|
|
1884
2251
|
const rootWorkflowId = resolveRootWorkflowId(state.containerTree);
|
|
1885
|
-
const
|
|
2252
|
+
const useEmbeddedSynthesis = shouldUseEmbeddedSynthesis(state, event);
|
|
2253
|
+
const projectedEvent = rootWorkflowId != null && useEmbeddedSynthesis ? patchEmbeddedStructureEvent(event, rootWorkflowId) : event;
|
|
1886
2254
|
let containerTree = applyStructureToContainerTree(state.containerTree, projectedEvent);
|
|
2255
|
+
if (projectedEvent.event_type === "subworkflow-start" || projectedEvent.event_type === "parallel-item-start") {
|
|
2256
|
+
containerTree = clearHostStageLoopOnNestedChildOpen(containerTree, projectedEvent);
|
|
2257
|
+
}
|
|
2258
|
+
if (projectedEvent.event_type === "subworkflow-start") {
|
|
2259
|
+
containerTree = dropStaleEmbeddedPathsOnNativeSubworkflowStart(containerTree, projectedEvent);
|
|
2260
|
+
}
|
|
1887
2261
|
const derivedLoopSessionId = deriveLoopSessionIdFromStructureOpen(projectedEvent);
|
|
1888
2262
|
const structureData = projectedEvent.data ?? {};
|
|
1889
2263
|
const structureLoopSessionId = derivedLoopSessionId ? resolveStructureOpenLoopSessionId(
|
|
@@ -1892,13 +2266,13 @@ function applyStructureEvent(state, event) {
|
|
|
1892
2266
|
derivedLoopSessionId,
|
|
1893
2267
|
structureData
|
|
1894
2268
|
) : null;
|
|
1895
|
-
if (structureLoopSessionId) {
|
|
2269
|
+
if (structureLoopSessionId && useEmbeddedSynthesis) {
|
|
1896
2270
|
containerTree = ensureEmbeddedSubworkflowContainers(containerTree, structureLoopSessionId);
|
|
1897
2271
|
}
|
|
1898
2272
|
containerTree = prebindLoopSessionFromStructureOpen(containerTree, projectedEvent);
|
|
1899
2273
|
if (projectedEvent.event_type === "stage-done" || projectedEvent.event_type === "stage-failed") {
|
|
1900
|
-
const stageKey =
|
|
1901
|
-
const workflowPath =
|
|
2274
|
+
const stageKey = readString5(structureData, "stage_key");
|
|
2275
|
+
const workflowPath = readString5(structureData, "workflow_path") ?? "";
|
|
1902
2276
|
if (stageKey) {
|
|
1903
2277
|
const stageId = buildContainerId(workflowPath, "stage", stageKey);
|
|
1904
2278
|
if (containerTree.containersById[stageId]) {
|
|
@@ -1973,7 +2347,7 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
1973
2347
|
if (isWorkflowStructureEventType(event.event_type)) {
|
|
1974
2348
|
return applyStructureEvent(state, event);
|
|
1975
2349
|
}
|
|
1976
|
-
const loopSessionId = resolveAgentLoopSessionId(event, state.activeLoopSessionId) ?? WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
2350
|
+
const loopSessionId = resolveAgentLoopSessionId(event, state.activeLoopSessionId, state.containerTree) ?? WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
1977
2351
|
let containerTree = bindLoopSessionToContainerTree(
|
|
1978
2352
|
state.containerTree,
|
|
1979
2353
|
event,
|
|
@@ -1992,6 +2366,11 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
1992
2366
|
next.containerTree,
|
|
1993
2367
|
next.workflowProgress
|
|
1994
2368
|
);
|
|
2369
|
+
let status = next.status;
|
|
2370
|
+
if (workflowProgress.status === "done") status = "done";
|
|
2371
|
+
else if (workflowProgress.status === "error") status = "error";
|
|
2372
|
+
else if (reducedLoop.status === "error") status = "error";
|
|
2373
|
+
else if (status === "connecting") status = "running";
|
|
1995
2374
|
return {
|
|
1996
2375
|
...next,
|
|
1997
2376
|
workflowProgress,
|
|
@@ -2000,160 +2379,107 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
2000
2379
|
[loopSessionId]: reducedLoop
|
|
2001
2380
|
},
|
|
2002
2381
|
activeLoopSessionId: loopSessionId,
|
|
2003
|
-
status
|
|
2382
|
+
status
|
|
2004
2383
|
};
|
|
2005
2384
|
}
|
|
2006
|
-
function
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
createEmptyWorkflowSessionState()
|
|
2010
|
-
);
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
// src/core/workflow/stageDoneSummary.ts
|
|
2014
|
-
function countToolUses(tree) {
|
|
2015
|
-
return Object.values(tree.byId).filter((node) => node.kind === "tool_call").length;
|
|
2016
|
-
}
|
|
2017
|
-
function computeDurationSeconds(tree) {
|
|
2018
|
-
const started = tree.meta.startedAt;
|
|
2019
|
-
if (started == null) return null;
|
|
2020
|
-
let maxTs = started;
|
|
2021
|
-
for (const node of Object.values(tree.byId)) {
|
|
2022
|
-
if ("endedAt" in node && typeof node.endedAt === "number") {
|
|
2023
|
-
maxTs = Math.max(maxTs, node.endedAt);
|
|
2024
|
-
}
|
|
2025
|
-
if ("startedAt" in node && typeof node.startedAt === "number") {
|
|
2026
|
-
maxTs = Math.max(maxTs, node.startedAt);
|
|
2027
|
-
}
|
|
2385
|
+
function finalizeWorkflowSessionOnStreamComplete(state) {
|
|
2386
|
+
if (state.status === "done" || state.status === "error") {
|
|
2387
|
+
return { ...state, activeLoopSessionId: null };
|
|
2028
2388
|
}
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
const textNodes = Object.values(tree.byId).filter((node) => node.kind === "text");
|
|
2034
|
-
if (textNodes.length === 0) return "";
|
|
2035
|
-
return textNodes[textNodes.length - 1].accumulated.trim();
|
|
2036
|
-
}
|
|
2037
|
-
function buildStageDoneSummary(tree) {
|
|
2038
|
-
if (tree.status !== "done" && tree.status !== "error") return null;
|
|
2039
|
-
const toolUses = countToolUses(tree);
|
|
2040
|
-
const duration = computeDurationSeconds(tree);
|
|
2041
|
-
const durationSuffix = duration != null ? ` \xB7 ${duration}s` : "";
|
|
2042
|
-
const label = toolUses > 0 ? `Done (${toolUses} tool use${toolUses === 1 ? "" : "s"}${durationSuffix})` : `Done (1 agent turn${durationSuffix})`;
|
|
2043
|
-
const fullText = lastTextContent(tree);
|
|
2044
|
-
const lines = fullText.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2045
|
-
const teaser = lines[0] ?? "";
|
|
2046
|
-
const extraLines = Math.max(0, lines.length - 1);
|
|
2047
|
-
return { label, teaser, extraLines };
|
|
2048
|
-
}
|
|
2049
|
-
function formatTeaserExpandHint(extraLines, verbose) {
|
|
2050
|
-
if (extraLines <= 0) return null;
|
|
2051
|
-
const shown = verbose ? Math.min(extraLines, 2) : 0;
|
|
2052
|
-
const hidden = extraLines - shown;
|
|
2053
|
-
if (hidden <= 0) return null;
|
|
2054
|
-
return `\u2026 +${hidden} lines (ctrl+o to expand)`;
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
// src/core/workflow/workflowLoopEviction.ts
|
|
2058
|
-
function containersByLoopSessionId(tree) {
|
|
2059
|
-
const map = /* @__PURE__ */ new Map();
|
|
2060
|
-
for (const node of Object.values(tree.containersById)) {
|
|
2061
|
-
if (node.loopSessionId) map.set(node.loopSessionId, node);
|
|
2389
|
+
const tree = state.containerTree;
|
|
2390
|
+
const rootId = tree.rootContainerIds[0];
|
|
2391
|
+
if (!rootId) {
|
|
2392
|
+
return { ...state, status: "done", activeLoopSessionId: null };
|
|
2062
2393
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
const protectedIds = new Set(Object.keys(pinnedLoopSessionIds));
|
|
2067
|
-
const byLoop = containersByLoopSessionId(state.containerTree);
|
|
2068
|
-
if (state.activeLoopSessionId) {
|
|
2069
|
-
const activeContainer = byLoop.get(state.activeLoopSessionId);
|
|
2070
|
-
if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
|
|
2071
|
-
protectedIds.add(state.activeLoopSessionId);
|
|
2072
|
-
}
|
|
2394
|
+
const root = tree.containersById[rootId];
|
|
2395
|
+
if (!root) {
|
|
2396
|
+
return { ...state, status: "done", activeLoopSessionId: null };
|
|
2073
2397
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2398
|
+
const topLevelStages = Object.values(tree.containersById).filter(
|
|
2399
|
+
(node) => node.scope === "stage" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
|
|
2400
|
+
);
|
|
2401
|
+
const progressTotal = root.display?.progress_total ?? (topLevelStages.reduce(
|
|
2402
|
+
(max, s) => Math.max(max, s.display?.progress_total ?? 0),
|
|
2403
|
+
0
|
|
2404
|
+
) || topLevelStages.length);
|
|
2405
|
+
const settledStageCount = topLevelStages.filter(
|
|
2406
|
+
(s) => s.status === "completed" || s.status === "failed" || s.status === "skipped"
|
|
2407
|
+
).length;
|
|
2408
|
+
const progressCurrent = Math.max(
|
|
2409
|
+
root.display?.progress_current ?? 0,
|
|
2410
|
+
settledStageCount
|
|
2411
|
+
);
|
|
2412
|
+
const rootParallelItems = Object.values(tree.containersById).filter(
|
|
2413
|
+
(node) => node.scope === "item" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
|
|
2414
|
+
);
|
|
2415
|
+
const rootParallelSettled = topLevelStages.length === 0 && rootParallelItems.length > 0 && rootParallelItems.every(
|
|
2416
|
+
(item) => item.status === "completed" || item.status === "failed" || item.status === "skipped"
|
|
2417
|
+
) && !Object.values(tree.containersById).some(
|
|
2418
|
+
(node) => node.container_id !== rootId && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth && (node.status === "running" || node.status === "retrying" || node.status === "pending")
|
|
2419
|
+
);
|
|
2420
|
+
const workflowVisuallyComplete = root.status === "completed" || root.status === "failed" || progressTotal > 0 && progressCurrent >= progressTotal || rootParallelSettled;
|
|
2421
|
+
if (!workflowVisuallyComplete) {
|
|
2422
|
+
return state;
|
|
2080
2423
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
if (node.status === "running" || node.status === "
|
|
2084
|
-
|
|
2424
|
+
let containersById = { ...tree.containersById };
|
|
2425
|
+
for (const [id, node] of Object.entries(containersById)) {
|
|
2426
|
+
if (node.status === "running" || node.status === "pending") {
|
|
2427
|
+
containersById[id] = { ...node, status: "completed", is_open: false };
|
|
2085
2428
|
}
|
|
2086
2429
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2430
|
+
const settledRoot = containersById[rootId] ?? root;
|
|
2431
|
+
if (settledRoot.status !== "completed" && settledRoot.status !== "failed") {
|
|
2432
|
+
containersById = {
|
|
2433
|
+
...containersById,
|
|
2434
|
+
[rootId]: { ...settledRoot, status: "completed", is_open: false }
|
|
2435
|
+
};
|
|
2093
2436
|
}
|
|
2094
|
-
const
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
};
|
|
2099
|
-
for (const [loopSessionId, tree] of Object.entries(state.loopTreesBySessionId)) {
|
|
2100
|
-
if (protectedIds.has(loopSessionId)) continue;
|
|
2101
|
-
const container = byLoop.get(loopSessionId);
|
|
2102
|
-
if (!container) continue;
|
|
2103
|
-
if (container.status !== "completed" && container.status !== "failed") continue;
|
|
2104
|
-
const summary = buildStageDoneSummary(tree);
|
|
2105
|
-
if (summary) {
|
|
2106
|
-
nextSummaries[container.container_id] = summary;
|
|
2437
|
+
const loopTreesBySessionId = { ...state.loopTreesBySessionId };
|
|
2438
|
+
for (const [loopSessionId, loopTree] of Object.entries(loopTreesBySessionId)) {
|
|
2439
|
+
if (loopTree.status === "running" || loopTree.status === "connecting") {
|
|
2440
|
+
loopTreesBySessionId[loopSessionId] = finalizeWorkflowLoopTree(loopTree, "done");
|
|
2107
2441
|
}
|
|
2108
|
-
delete nextLoops[loopSessionId];
|
|
2109
2442
|
}
|
|
2110
|
-
|
|
2111
|
-
...
|
|
2112
|
-
|
|
2113
|
-
|
|
2443
|
+
const containerTree = {
|
|
2444
|
+
...tree,
|
|
2445
|
+
containersById,
|
|
2446
|
+
activeContainerIds: tree.activeContainerIds.filter(
|
|
2447
|
+
(id) => containersById[id]?.is_open
|
|
2448
|
+
)
|
|
2114
2449
|
};
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2450
|
+
const workflowProgress = syncWorkflowProgressFromContainerTree(
|
|
2451
|
+
containerTree,
|
|
2452
|
+
state.workflowProgress
|
|
2453
|
+
);
|
|
2119
2454
|
return {
|
|
2120
2455
|
...state,
|
|
2121
|
-
|
|
2456
|
+
status: workflowProgress.status === "error" ? "error" : "done",
|
|
2457
|
+
containerTree,
|
|
2458
|
+
workflowProgress,
|
|
2459
|
+
loopTreesBySessionId,
|
|
2460
|
+
activeLoopSessionId: null
|
|
2122
2461
|
};
|
|
2123
2462
|
}
|
|
2124
|
-
function
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
const evictedIds = new Set(state.hotCompletedLoopSessionIds.slice(0, overflow));
|
|
2130
|
-
const trimmedIds = state.hotCompletedLoopSessionIds.slice(overflow);
|
|
2131
|
-
const byLoop = containersByLoopSessionId(state.containerTree);
|
|
2132
|
-
const nextLoops = { ...state.loopTreesBySessionId };
|
|
2133
|
-
const nextSummaries = {
|
|
2134
|
-
...state.stageDoneSummariesByContainerId
|
|
2135
|
-
};
|
|
2136
|
-
for (const id of evictedIds) {
|
|
2137
|
-
const tree = nextLoops[id];
|
|
2138
|
-
if (!tree) continue;
|
|
2139
|
-
const container = byLoop.get(id);
|
|
2140
|
-
if (container) {
|
|
2141
|
-
const summary = buildStageDoneSummary(tree);
|
|
2142
|
-
if (summary) {
|
|
2143
|
-
nextSummaries[container.container_id] = summary;
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
delete nextLoops[id];
|
|
2147
|
-
}
|
|
2148
|
-
return {
|
|
2149
|
-
...state,
|
|
2150
|
-
loopTreesBySessionId: nextLoops,
|
|
2151
|
-
stageDoneSummariesByContainerId: nextSummaries,
|
|
2152
|
-
hotCompletedLoopSessionIds: trimmedIds
|
|
2153
|
-
};
|
|
2463
|
+
function reduceWorkflowEvents(events, options) {
|
|
2464
|
+
return events.reduce(
|
|
2465
|
+
(state, event, index) => reduceWorkflowSession(state, event, index, options),
|
|
2466
|
+
createEmptyWorkflowSessionState()
|
|
2467
|
+
);
|
|
2154
2468
|
}
|
|
2155
2469
|
|
|
2156
2470
|
// src/core/workflowSessionStore.ts
|
|
2471
|
+
var DEFAULT_HYDRATE_CHUNK_SIZE = 50;
|
|
2472
|
+
function nextAnimationFrame() {
|
|
2473
|
+
return new Promise((resolve) => {
|
|
2474
|
+
requestAnimationFrame(() => resolve());
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
function withoutHydrating(ids, loopSessionId) {
|
|
2478
|
+
if (!ids[loopSessionId]) return ids;
|
|
2479
|
+
const next = { ...ids };
|
|
2480
|
+
delete next[loopSessionId];
|
|
2481
|
+
return next;
|
|
2482
|
+
}
|
|
2157
2483
|
function safeReduceWorkflow(state, event, eventIndex, options) {
|
|
2158
2484
|
try {
|
|
2159
2485
|
return { state: reduceWorkflowSession(state, event, eventIndex, options) };
|
|
@@ -2174,6 +2500,39 @@ function safeReduceWorkflow(state, event, eventIndex, options) {
|
|
|
2174
2500
|
};
|
|
2175
2501
|
}
|
|
2176
2502
|
}
|
|
2503
|
+
function finalizeWorkflowSessionOnStreamError(state, message, eventIndex, stack) {
|
|
2504
|
+
const containersById = {
|
|
2505
|
+
...state.containerTree.containersById
|
|
2506
|
+
};
|
|
2507
|
+
for (const [id, node] of Object.entries(containersById)) {
|
|
2508
|
+
if (node.status === "running" || node.status === "pending") {
|
|
2509
|
+
containersById[id] = { ...node, status: "failed", is_open: false };
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const loopTreesBySessionId = { ...state.loopTreesBySessionId };
|
|
2513
|
+
for (const [loopSessionId, tree] of Object.entries(loopTreesBySessionId)) {
|
|
2514
|
+
if (tree.status === "running" || tree.status === "connecting") {
|
|
2515
|
+
loopTreesBySessionId[loopSessionId] = finalizeWorkflowLoopTree(tree, "error");
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
return {
|
|
2519
|
+
...state,
|
|
2520
|
+
status: "error",
|
|
2521
|
+
containerTree: {
|
|
2522
|
+
...state.containerTree,
|
|
2523
|
+
containersById,
|
|
2524
|
+
activeContainerIds: state.containerTree.activeContainerIds.filter(
|
|
2525
|
+
(id) => containersById[id]?.is_open
|
|
2526
|
+
)
|
|
2527
|
+
},
|
|
2528
|
+
loopTreesBySessionId,
|
|
2529
|
+
activeLoopSessionId: null,
|
|
2530
|
+
internalErrors: [
|
|
2531
|
+
...state.internalErrors,
|
|
2532
|
+
{ eventIndex, eventType: "stream-error", message, stack }
|
|
2533
|
+
]
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2177
2536
|
function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionState(), storeOptions) {
|
|
2178
2537
|
const reduceOpts = {
|
|
2179
2538
|
tierOverrides: storeOptions?.tierOverrides
|
|
@@ -2190,6 +2549,7 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
|
|
|
2190
2549
|
return createStore2((set, get) => ({
|
|
2191
2550
|
state: initialState,
|
|
2192
2551
|
eventCount: initialEventCount,
|
|
2552
|
+
hydratingLoopSessionIds: {},
|
|
2193
2553
|
applyEvent(event, ctx) {
|
|
2194
2554
|
const sseEventId = ctx?.sseEventId;
|
|
2195
2555
|
if (shouldSkipDuplicateEvent(seenEventIds, sseEventId)) return;
|
|
@@ -2216,28 +2576,29 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
|
|
|
2216
2576
|
seenEventIds.clear();
|
|
2217
2577
|
set({
|
|
2218
2578
|
state: createEmptyWorkflowSessionState(),
|
|
2219
|
-
eventCount: 0
|
|
2579
|
+
eventCount: 0,
|
|
2580
|
+
hydratingLoopSessionIds: {}
|
|
2220
2581
|
});
|
|
2221
2582
|
},
|
|
2222
2583
|
markAsError(error) {
|
|
2223
|
-
const { state } = get();
|
|
2584
|
+
const { state, eventCount } = get();
|
|
2224
2585
|
const message = error?.message ?? "Workflow stream error";
|
|
2225
2586
|
set({
|
|
2226
|
-
state:
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
eventIndex: get().eventCount,
|
|
2233
|
-
eventType: "stream-error",
|
|
2234
|
-
message,
|
|
2235
|
-
stack: error?.stack
|
|
2236
|
-
}
|
|
2237
|
-
]
|
|
2238
|
-
}
|
|
2587
|
+
state: finalizeWorkflowSessionOnStreamError(
|
|
2588
|
+
state,
|
|
2589
|
+
message,
|
|
2590
|
+
eventCount,
|
|
2591
|
+
error?.stack
|
|
2592
|
+
)
|
|
2239
2593
|
});
|
|
2240
2594
|
},
|
|
2595
|
+
markStreamComplete() {
|
|
2596
|
+
set((current) => ({
|
|
2597
|
+
state: finalizeState(
|
|
2598
|
+
finalizeWorkflowSessionOnStreamComplete(current.state)
|
|
2599
|
+
)
|
|
2600
|
+
}));
|
|
2601
|
+
},
|
|
2241
2602
|
/**
|
|
2242
2603
|
* P2:将 completed loop 加入 LRU 热缓存并淘汰溢出。
|
|
2243
2604
|
* running 保护由 collectProtectedLoopSessionIds 按状态自动覆盖,不再依赖永久 pin。
|
|
@@ -2275,6 +2636,71 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
|
|
|
2275
2636
|
next = finalizeState(next);
|
|
2276
2637
|
return { state: next };
|
|
2277
2638
|
});
|
|
2639
|
+
},
|
|
2640
|
+
async hydrateLoopSessionAsync(loopSessionId, events, options) {
|
|
2641
|
+
const { hydratingLoopSessionIds } = get();
|
|
2642
|
+
if (hydratingLoopSessionIds[loopSessionId]) return;
|
|
2643
|
+
const filtered = filterAgentLoopReplayEvents(events);
|
|
2644
|
+
if (filtered.length === 0) return;
|
|
2645
|
+
const chunkSize = options?.chunkSize ?? DEFAULT_HYDRATE_CHUNK_SIZE;
|
|
2646
|
+
const signal = options?.signal;
|
|
2647
|
+
set((current) => ({
|
|
2648
|
+
hydratingLoopSessionIds: {
|
|
2649
|
+
...current.hydratingLoopSessionIds,
|
|
2650
|
+
[loopSessionId]: true
|
|
2651
|
+
}
|
|
2652
|
+
}));
|
|
2653
|
+
try {
|
|
2654
|
+
let tree = get().state.loopTreesBySessionId[loopSessionId] ?? createEmptyTree();
|
|
2655
|
+
let idx = get().eventCount;
|
|
2656
|
+
for (let offset = 0; offset < filtered.length; offset += chunkSize) {
|
|
2657
|
+
if (signal?.aborted) {
|
|
2658
|
+
throw new DOMException("Hydrate aborted", "AbortError");
|
|
2659
|
+
}
|
|
2660
|
+
const chunk = filtered.slice(offset, offset + chunkSize);
|
|
2661
|
+
for (const event of chunk) {
|
|
2662
|
+
tree = reduceTree(tree, event, idx, reduceOpts);
|
|
2663
|
+
idx += 1;
|
|
2664
|
+
}
|
|
2665
|
+
const isLastChunk = offset + chunkSize >= filtered.length;
|
|
2666
|
+
set((current) => {
|
|
2667
|
+
let next = {
|
|
2668
|
+
...current.state,
|
|
2669
|
+
loopTreesBySessionId: {
|
|
2670
|
+
...current.state.loopTreesBySessionId,
|
|
2671
|
+
[loopSessionId]: tree
|
|
2672
|
+
}
|
|
2673
|
+
};
|
|
2674
|
+
if (isLastChunk) {
|
|
2675
|
+
next = touchCompletedLoopSession(next, loopSessionId);
|
|
2676
|
+
next = evictCompletedLoopOverflow(next, maxHydratedCompletedLoops);
|
|
2677
|
+
}
|
|
2678
|
+
next = finalizeState(next);
|
|
2679
|
+
return { state: next };
|
|
2680
|
+
});
|
|
2681
|
+
if (!isLastChunk) {
|
|
2682
|
+
await nextAnimationFrame();
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
} catch {
|
|
2686
|
+
set((current) => {
|
|
2687
|
+
const { [loopSessionId]: _removed, ...restLoops } = current.state.loopTreesBySessionId;
|
|
2688
|
+
return {
|
|
2689
|
+
state: { ...current.state, loopTreesBySessionId: restLoops },
|
|
2690
|
+
hydratingLoopSessionIds: withoutHydrating(
|
|
2691
|
+
current.hydratingLoopSessionIds,
|
|
2692
|
+
loopSessionId
|
|
2693
|
+
)
|
|
2694
|
+
};
|
|
2695
|
+
});
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
set((current) => ({
|
|
2699
|
+
hydratingLoopSessionIds: withoutHydrating(
|
|
2700
|
+
current.hydratingLoopSessionIds,
|
|
2701
|
+
loopSessionId
|
|
2702
|
+
)
|
|
2703
|
+
}));
|
|
2278
2704
|
}
|
|
2279
2705
|
}));
|
|
2280
2706
|
}
|
|
@@ -2283,7 +2709,7 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
|
|
|
2283
2709
|
import { useMemo as useMemo7, useState as useState6 } from "react";
|
|
2284
2710
|
|
|
2285
2711
|
// src/view/workflow/WorkflowAggregateContainer.tsx
|
|
2286
|
-
import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState } from "react";
|
|
2712
|
+
import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState, memo } from "react";
|
|
2287
2713
|
|
|
2288
2714
|
// src/view/workflow/WorkflowContainerLine.tsx
|
|
2289
2715
|
import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -2427,6 +2853,76 @@ function WorkflowMarkdownPreview({
|
|
|
2427
2853
|
return /* @__PURE__ */ jsx7("div", { className, "data-testid": testId, children: /* @__PURE__ */ jsx7(RichMarkdown, { content, deferDiagrams: true }) });
|
|
2428
2854
|
}
|
|
2429
2855
|
|
|
2856
|
+
// src/view/workflow/workflowStageListUtils.ts
|
|
2857
|
+
var ACTIVE_CONTAINER_STATUSES = /* @__PURE__ */ new Set([
|
|
2858
|
+
"running",
|
|
2859
|
+
"retrying",
|
|
2860
|
+
"pending",
|
|
2861
|
+
"blocked"
|
|
2862
|
+
]);
|
|
2863
|
+
function isDescendantContainer(containersById, ancestorId, nodeId) {
|
|
2864
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2865
|
+
let current = containersById[nodeId]?.parent_container_id ?? null;
|
|
2866
|
+
while (current) {
|
|
2867
|
+
if (current === ancestorId) return true;
|
|
2868
|
+
if (seen.has(current)) return false;
|
|
2869
|
+
seen.add(current);
|
|
2870
|
+
current = containersById[current]?.parent_container_id ?? null;
|
|
2871
|
+
}
|
|
2872
|
+
return false;
|
|
2873
|
+
}
|
|
2874
|
+
function subtreeHasActiveWork(containersById, rootContainerId) {
|
|
2875
|
+
for (const node of Object.values(containersById)) {
|
|
2876
|
+
if (node.container_id === rootContainerId) continue;
|
|
2877
|
+
if (!isDescendantContainer(containersById, rootContainerId, node.container_id)) continue;
|
|
2878
|
+
if (ACTIVE_CONTAINER_STATUSES.has(node.status)) return true;
|
|
2879
|
+
}
|
|
2880
|
+
return false;
|
|
2881
|
+
}
|
|
2882
|
+
function stageSortKey(node) {
|
|
2883
|
+
return node.display?.progress_current ?? 0;
|
|
2884
|
+
}
|
|
2885
|
+
function partitionWorkflowStages(stages, maxVisibleDoneCount, containersById) {
|
|
2886
|
+
const sorted = [...stages].sort(
|
|
2887
|
+
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
2888
|
+
);
|
|
2889
|
+
const always = [];
|
|
2890
|
+
const done = [];
|
|
2891
|
+
for (const stage of sorted) {
|
|
2892
|
+
if (containersById && subtreeHasActiveWork(containersById, stage.container_id)) {
|
|
2893
|
+
always.push(stage);
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
if (stage.status === "running" || stage.status === "retrying" || stage.status === "failed" || stage.status === "pending" || stage.status === "blocked") {
|
|
2897
|
+
always.push(stage);
|
|
2898
|
+
} else if (stage.status === "completed" || stage.status === "skipped") {
|
|
2899
|
+
done.push(stage);
|
|
2900
|
+
} else {
|
|
2901
|
+
always.push(stage);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
const visibleDone = done.slice(Math.max(0, done.length - maxVisibleDoneCount));
|
|
2905
|
+
const visible = [...always, ...visibleDone].sort(
|
|
2906
|
+
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
2907
|
+
);
|
|
2908
|
+
const hiddenCount = stages.length - visible.length;
|
|
2909
|
+
return {
|
|
2910
|
+
visible,
|
|
2911
|
+
hiddenCount,
|
|
2912
|
+
total: stages.length,
|
|
2913
|
+
visibleDoneCount: visibleDone.length,
|
|
2914
|
+
totalDoneCount: done.length
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
function isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds) {
|
|
2918
|
+
if (activeContainerIds.includes(node.container_id)) return true;
|
|
2919
|
+
if (node.status === "completed" || node.status === "failed" || node.status === "skipped") {
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
if (node.loopSessionId && node.loopSessionId === activeLoopSessionId) return true;
|
|
2923
|
+
return false;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2430
2926
|
// src/view/workflow/workflowContainerLineUtils.ts
|
|
2431
2927
|
function formatContainerScopeMetaLabel(scope) {
|
|
2432
2928
|
return scope;
|
|
@@ -2496,6 +2992,27 @@ function buildWorkflowRootMeta(node) {
|
|
|
2496
2992
|
}
|
|
2497
2993
|
return withScopeMeta(node, meta);
|
|
2498
2994
|
}
|
|
2995
|
+
function buildEffectiveWorkflowRootMeta(node, containersById) {
|
|
2996
|
+
const meta = [];
|
|
2997
|
+
if (node.pattern) meta.push(node.pattern);
|
|
2998
|
+
const topStages = Object.values(containersById).filter(
|
|
2999
|
+
(n) => n.scope === "stage" && n.parent_container_id === node.container_id
|
|
3000
|
+
);
|
|
3001
|
+
const total = node.display?.progress_total ?? Math.max(topStages.length, 1);
|
|
3002
|
+
let settled = 0;
|
|
3003
|
+
for (const stage of topStages) {
|
|
3004
|
+
if (subtreeHasActiveWork(containersById, stage.container_id)) continue;
|
|
3005
|
+
if (stage.status === "completed" || stage.status === "failed" || stage.status === "skipped") {
|
|
3006
|
+
settled += 1;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
if (total > 0) meta.push(`${settled}/${total}`);
|
|
3010
|
+
return withScopeMeta(node, meta);
|
|
3011
|
+
}
|
|
3012
|
+
function isWorkflowRootVisuallyActive(node, containersById) {
|
|
3013
|
+
if (node.status === "running" || node.status === "retrying") return true;
|
|
3014
|
+
return subtreeHasActiveWork(containersById, node.container_id);
|
|
3015
|
+
}
|
|
2499
3016
|
function buildStageLineMeta(node) {
|
|
2500
3017
|
const progress = buildStageProgressMeta(node);
|
|
2501
3018
|
return withScopeMeta(node, progress ? [progress] : []);
|
|
@@ -2551,6 +3068,29 @@ function mapAggregateUiStatus(node) {
|
|
|
2551
3068
|
return mapContainerUiStatus(node.status);
|
|
2552
3069
|
}
|
|
2553
3070
|
|
|
3071
|
+
// src/view/workflow/workflowContainerMemo.ts
|
|
3072
|
+
function displayVisualEqual(a, b) {
|
|
3073
|
+
if (a === b) return true;
|
|
3074
|
+
if (a === null || b === null) return false;
|
|
3075
|
+
if (a.progress_current !== b.progress_current) return false;
|
|
3076
|
+
if (a.status !== b.status) return false;
|
|
3077
|
+
return true;
|
|
3078
|
+
}
|
|
3079
|
+
function workflowContainerNodeVisualEqual(a, b) {
|
|
3080
|
+
if (a.container_id !== b.container_id) return false;
|
|
3081
|
+
if (a.status !== b.status) return false;
|
|
3082
|
+
if (a.is_open !== b.is_open) return false;
|
|
3083
|
+
if (a.loopSessionId !== b.loopSessionId) return false;
|
|
3084
|
+
if (a.title !== b.title) return false;
|
|
3085
|
+
if (a.subtitle !== b.subtitle) return false;
|
|
3086
|
+
if (a.content_blocks.length !== b.content_blocks.length) return false;
|
|
3087
|
+
if (!displayVisualEqual(a.display, b.display)) return false;
|
|
3088
|
+
return true;
|
|
3089
|
+
}
|
|
3090
|
+
function workflowContainerViewPropsEqual(prev, next) {
|
|
3091
|
+
return prev.depth === next.depth && prev.virtualized === next.virtualized && prev.virtualizeThreshold === next.virtualizeThreshold && prev.groupParallelTools === next.groupParallelTools;
|
|
3092
|
+
}
|
|
3093
|
+
|
|
2554
3094
|
// src/view/workflow/WorkflowAggregateContainer.tsx
|
|
2555
3095
|
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2556
3096
|
function mergeContentPreview(node) {
|
|
@@ -2558,7 +3098,7 @@ function mergeContentPreview(node) {
|
|
|
2558
3098
|
if (blocks.length === 0) return "";
|
|
2559
3099
|
return blocks.map((block) => block.preview?.trim() ?? "").filter(Boolean).join("\n\n");
|
|
2560
3100
|
}
|
|
2561
|
-
function
|
|
3101
|
+
function WorkflowAggregateContainerInner({
|
|
2562
3102
|
node,
|
|
2563
3103
|
loopTree,
|
|
2564
3104
|
siblingItems = [],
|
|
@@ -2678,12 +3218,59 @@ function WorkflowAggregateContainer({
|
|
|
2678
3218
|
}
|
|
2679
3219
|
);
|
|
2680
3220
|
}
|
|
3221
|
+
function workflowAggregateContainerPropsEqual(prev, next) {
|
|
3222
|
+
if (prev.loopTree !== next.loopTree) return false;
|
|
3223
|
+
if (!workflowContainerViewPropsEqual(prev, next)) return false;
|
|
3224
|
+
if (prev.siblingItems.length !== next.siblingItems.length) return false;
|
|
3225
|
+
for (let i = 0; i < prev.siblingItems.length; i += 1) {
|
|
3226
|
+
if (!workflowContainerNodeVisualEqual(prev.siblingItems[i], next.siblingItems[i])) {
|
|
3227
|
+
return false;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
return workflowContainerNodeVisualEqual(prev.node, next.node);
|
|
3231
|
+
}
|
|
3232
|
+
var WorkflowAggregateContainer = memo(
|
|
3233
|
+
WorkflowAggregateContainerInner,
|
|
3234
|
+
workflowAggregateContainerPropsEqual
|
|
3235
|
+
);
|
|
2681
3236
|
|
|
2682
3237
|
// src/view/workflow/WorkflowParallelGroup.tsx
|
|
2683
|
-
import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useState as useState3 } from "react";
|
|
2684
|
-
import { useStore as useStore3 } from "zustand";
|
|
3238
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useState as useState3, memo as memo3 } from "react";
|
|
2685
3239
|
|
|
2686
3240
|
// src/core/workflow/workflowParallelGroupMetrics.ts
|
|
3241
|
+
var ACTIVE_ITEM_STATUSES = /* @__PURE__ */ new Set([
|
|
3242
|
+
"running",
|
|
3243
|
+
"retrying",
|
|
3244
|
+
"pending",
|
|
3245
|
+
"blocked"
|
|
3246
|
+
]);
|
|
3247
|
+
function countMetricsFromContainerStatus(items) {
|
|
3248
|
+
let running = 0;
|
|
3249
|
+
let completed = 0;
|
|
3250
|
+
let failed = 0;
|
|
3251
|
+
for (const item of items) {
|
|
3252
|
+
if (ACTIVE_ITEM_STATUSES.has(item.status)) {
|
|
3253
|
+
running += 1;
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
switch (item.status) {
|
|
3257
|
+
case "completed":
|
|
3258
|
+
completed += 1;
|
|
3259
|
+
break;
|
|
3260
|
+
case "failed":
|
|
3261
|
+
failed += 1;
|
|
3262
|
+
break;
|
|
3263
|
+
default:
|
|
3264
|
+
break;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
return {
|
|
3268
|
+
total: items.length,
|
|
3269
|
+
running,
|
|
3270
|
+
completed,
|
|
3271
|
+
failed
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
2687
3274
|
function readDisplayMetrics(items) {
|
|
2688
3275
|
let hasMetrics = false;
|
|
2689
3276
|
let running = 0;
|
|
@@ -2704,34 +3291,29 @@ function readDisplayMetrics(items) {
|
|
|
2704
3291
|
if (total <= 0) total = items.length;
|
|
2705
3292
|
return { total, running, completed, failed };
|
|
2706
3293
|
}
|
|
3294
|
+
function mergeParallelGroupMetrics(fromDisplay, fromStatus, itemCount) {
|
|
3295
|
+
return {
|
|
3296
|
+
total: Math.max(fromDisplay.total, fromStatus.total, itemCount),
|
|
3297
|
+
running: Math.max(fromDisplay.running, fromStatus.running),
|
|
3298
|
+
completed: Math.max(fromDisplay.completed, fromStatus.completed),
|
|
3299
|
+
failed: Math.max(fromDisplay.failed, fromStatus.failed)
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
2707
3302
|
function computeParallelGroupMetrics(items) {
|
|
3303
|
+
const fromStatus = countMetricsFromContainerStatus(items);
|
|
2708
3304
|
const fromDisplay = readDisplayMetrics(items);
|
|
2709
|
-
if (fromDisplay) return
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
3305
|
+
if (!fromDisplay) return fromStatus;
|
|
3306
|
+
return mergeParallelGroupMetrics(fromDisplay, fromStatus, items.length);
|
|
3307
|
+
}
|
|
3308
|
+
function parallelGroupHasActiveWork(items, loopTreesBySessionId = {}) {
|
|
3309
|
+
const metrics = computeParallelGroupMetrics(items);
|
|
3310
|
+
if (metrics.running > 0) return true;
|
|
2713
3311
|
for (const item of items) {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
running += 1;
|
|
2718
|
-
break;
|
|
2719
|
-
case "completed":
|
|
2720
|
-
completed += 1;
|
|
2721
|
-
break;
|
|
2722
|
-
case "failed":
|
|
2723
|
-
failed += 1;
|
|
2724
|
-
break;
|
|
2725
|
-
default:
|
|
2726
|
-
break;
|
|
2727
|
-
}
|
|
3312
|
+
const loopSessionId = item.loopSessionId;
|
|
3313
|
+
if (!loopSessionId) continue;
|
|
3314
|
+
if (isWorkflowLoopTreeActive(loopTreesBySessionId[loopSessionId])) return true;
|
|
2728
3315
|
}
|
|
2729
|
-
return
|
|
2730
|
-
total: items.length,
|
|
2731
|
-
running,
|
|
2732
|
-
completed,
|
|
2733
|
-
failed
|
|
2734
|
-
};
|
|
3316
|
+
return false;
|
|
2735
3317
|
}
|
|
2736
3318
|
function formatParallelGroupSummary(metrics) {
|
|
2737
3319
|
if (metrics.running > 0) {
|
|
@@ -2747,7 +3329,7 @@ function formatParallelGroupSummary(metrics) {
|
|
|
2747
3329
|
}
|
|
2748
3330
|
|
|
2749
3331
|
// src/view/workflow/WorkflowStageContainer.tsx
|
|
2750
|
-
import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useState as useState2 } from "react";
|
|
3332
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useState as useState2, memo as memo2 } from "react";
|
|
2751
3333
|
import { useStore as useStore2 } from "zustand";
|
|
2752
3334
|
|
|
2753
3335
|
// src/core/workflow/loopTreeUtils.ts
|
|
@@ -2788,44 +3370,6 @@ function WorkflowStageDoneBody({
|
|
|
2788
3370
|
);
|
|
2789
3371
|
}
|
|
2790
3372
|
|
|
2791
|
-
// src/view/workflow/workflowStageListUtils.ts
|
|
2792
|
-
function stageSortKey(node) {
|
|
2793
|
-
return node.display?.progress_current ?? 0;
|
|
2794
|
-
}
|
|
2795
|
-
function partitionWorkflowStages(stages, maxVisibleDoneCount) {
|
|
2796
|
-
const sorted = [...stages].sort(
|
|
2797
|
-
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
2798
|
-
);
|
|
2799
|
-
const always = [];
|
|
2800
|
-
const done = [];
|
|
2801
|
-
for (const stage of sorted) {
|
|
2802
|
-
if (stage.status === "running" || stage.status === "retrying" || stage.status === "failed" || stage.status === "pending" || stage.status === "blocked") {
|
|
2803
|
-
always.push(stage);
|
|
2804
|
-
} else if (stage.status === "completed" || stage.status === "skipped") {
|
|
2805
|
-
done.push(stage);
|
|
2806
|
-
} else {
|
|
2807
|
-
always.push(stage);
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
const visibleDone = done.slice(Math.max(0, done.length - maxVisibleDoneCount));
|
|
2811
|
-
const visible = [...always, ...visibleDone].sort(
|
|
2812
|
-
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
2813
|
-
);
|
|
2814
|
-
const hiddenCount = stages.length - visible.length;
|
|
2815
|
-
return {
|
|
2816
|
-
visible,
|
|
2817
|
-
hiddenCount,
|
|
2818
|
-
total: stages.length,
|
|
2819
|
-
visibleDoneCount: visibleDone.length,
|
|
2820
|
-
totalDoneCount: done.length
|
|
2821
|
-
};
|
|
2822
|
-
}
|
|
2823
|
-
function isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds) {
|
|
2824
|
-
if (activeContainerIds.includes(node.container_id)) return true;
|
|
2825
|
-
if (node.loopSessionId && node.loopSessionId === activeLoopSessionId) return true;
|
|
2826
|
-
return false;
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
3373
|
// src/view/workflow/WorkflowStageContainer.tsx
|
|
2830
3374
|
import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2831
3375
|
function containerTestId(node) {
|
|
@@ -2837,16 +3381,17 @@ function bodyTestId(node) {
|
|
|
2837
3381
|
if (node.scope === "item") return `lax-workflow-parallel-body-${node.scope_key}`;
|
|
2838
3382
|
return `lax-workflow-stage-body-${node.scope_key}`;
|
|
2839
3383
|
}
|
|
2840
|
-
function
|
|
3384
|
+
function WorkflowStageContainerInner({
|
|
2841
3385
|
node,
|
|
2842
3386
|
loopTree,
|
|
2843
3387
|
depth = 0,
|
|
2844
3388
|
virtualized,
|
|
2845
3389
|
virtualizeThreshold,
|
|
2846
|
-
groupParallelTools
|
|
3390
|
+
groupParallelTools,
|
|
3391
|
+
suppressHostScopedLoop = false,
|
|
3392
|
+
subtreeHasActiveWork: subtreeHasActiveWork2 = false
|
|
2847
3393
|
}) {
|
|
2848
3394
|
const { verbose } = useSessionViewOptions();
|
|
2849
|
-
const scale = useWorkflowScaleOptions();
|
|
2850
3395
|
const store = useWorkflowSessionStoreApi();
|
|
2851
3396
|
const replayHandler = useWorkflowLoopReplayHandler();
|
|
2852
3397
|
const activeLoopSessionId = useStore2(store, (s) => s.state.activeLoopSessionId);
|
|
@@ -2856,31 +3401,30 @@ function WorkflowStageContainer({
|
|
|
2856
3401
|
(s) => s.state.stageDoneSummariesByContainerId[node.container_id]
|
|
2857
3402
|
);
|
|
2858
3403
|
const pinLoopSession = useStore2(store, (s) => s.pinLoopSession);
|
|
2859
|
-
const uiStatus = mapContainerUiStatus(node.status);
|
|
2860
3404
|
const isRunning = node.status === "running" || node.status === "retrying";
|
|
2861
3405
|
const isDone = node.status === "completed" || node.status === "failed";
|
|
2862
3406
|
const isSkipped = node.status === "skipped";
|
|
3407
|
+
const loopStillActive = isWorkflowLoopTreeActive(loopTree);
|
|
3408
|
+
const effectivelyRunning = isRunning || loopStillActive || subtreeHasActiveWork2 && isDone;
|
|
3409
|
+
const effectivelyDone = isDone && !subtreeHasActiveWork2 && !loopStillActive;
|
|
3410
|
+
const uiStatus = mapContainerUiStatus(effectivelyRunning ? "running" : node.status);
|
|
2863
3411
|
const isActive = isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds);
|
|
2864
3412
|
const doneSummaryFromTree = useMemo3(
|
|
2865
3413
|
() => loopTree && isDone ? buildStageDoneSummary(loopTree) : null,
|
|
2866
3414
|
[loopTree, isDone]
|
|
2867
3415
|
);
|
|
2868
3416
|
const doneSummary = doneSummaryFromTree ?? (isDone ? cachedSummary : null);
|
|
2869
|
-
const shouldAutoExpandRunning =
|
|
3417
|
+
const shouldAutoExpandRunning = effectivelyRunning && !isSkipped;
|
|
2870
3418
|
const [expanded, setExpanded] = useState2(shouldAutoExpandRunning);
|
|
2871
3419
|
useEffect4(() => {
|
|
2872
3420
|
if (shouldAutoExpandRunning) {
|
|
2873
3421
|
setExpanded(true);
|
|
2874
3422
|
return;
|
|
2875
3423
|
}
|
|
2876
|
-
if (
|
|
2877
|
-
setExpanded(false);
|
|
2878
|
-
return;
|
|
2879
|
-
}
|
|
2880
|
-
if (isDone || isSkipped) {
|
|
3424
|
+
if (effectivelyDone || isSkipped) {
|
|
2881
3425
|
setExpanded(false);
|
|
2882
3426
|
}
|
|
2883
|
-
}, [shouldAutoExpandRunning,
|
|
3427
|
+
}, [shouldAutoExpandRunning, effectivelyDone, isSkipped]);
|
|
2884
3428
|
const requestLoopHydration = useCallback2(() => {
|
|
2885
3429
|
const loopSessionId = node.loopSessionId;
|
|
2886
3430
|
if (!loopSessionId || loopTree) return;
|
|
@@ -2905,17 +3449,18 @@ function WorkflowStageContainer({
|
|
|
2905
3449
|
window.addEventListener("keydown", onKeyDown);
|
|
2906
3450
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
2907
3451
|
}, [requestLoopHydration]);
|
|
3452
|
+
const testId = containerTestId(node);
|
|
3453
|
+
const isItem = node.scope === "item";
|
|
2908
3454
|
const displayTitle = node.title;
|
|
2909
|
-
const usesScopedLoop = Boolean(node.loopSessionId);
|
|
2910
|
-
const runningLoopTree = loopTree ?? (usesScopedLoop &&
|
|
3455
|
+
const usesScopedLoop = Boolean(node.loopSessionId) && !suppressHostScopedLoop;
|
|
3456
|
+
const runningLoopTree = loopTree ?? (usesScopedLoop && effectivelyRunning ? createWorkflowRunningLoopTree() : void 0);
|
|
3457
|
+
const itemLoopVirtualized = isItem ? false : virtualized;
|
|
2911
3458
|
const staticContentTeaser = !usesScopedLoop && !loopTree && node.content_blocks.length > 0 ? node.content_blocks[node.content_blocks.length - 1].preview : "";
|
|
2912
3459
|
const teaserText = doneSummary?.teaser || staticContentTeaser;
|
|
2913
3460
|
const expandHint = doneSummary ? formatTeaserExpandHint(doneSummary.extraLines, verbose) : null;
|
|
2914
3461
|
const doneLabel = doneSummary?.label ?? "done";
|
|
2915
|
-
const statusLabel =
|
|
2916
|
-
const
|
|
2917
|
-
const isItem = node.scope === "item";
|
|
2918
|
-
const canToggleShell = isRunning && !isSkipped || isDone;
|
|
3462
|
+
const statusLabel = effectivelyDone ? doneLabel : suppressHostScopedLoop && effectivelyRunning ? "running" : mapContainerStatusLabel(effectivelyRunning ? "running" : node.status);
|
|
3463
|
+
const canToggleShell = effectivelyRunning && !isSkipped || effectivelyDone;
|
|
2919
3464
|
const shellToggle = canToggleShell ? toggleExpanded : void 0;
|
|
2920
3465
|
return /* @__PURE__ */ jsxs5(
|
|
2921
3466
|
"div",
|
|
@@ -2941,13 +3486,13 @@ function WorkflowStageContainer({
|
|
|
2941
3486
|
onToggle: shellToggle,
|
|
2942
3487
|
interactive: canToggleShell,
|
|
2943
3488
|
expanded,
|
|
2944
|
-
headerTestId:
|
|
3489
|
+
headerTestId: effectivelyDone ? `${testId}-done` : void 0,
|
|
2945
3490
|
titleTestId: `${testId}-title`,
|
|
2946
3491
|
descriptionTestId: `${testId}-desc`,
|
|
2947
3492
|
className: isItem ? "lax-workflow-parallel-item__header" : void 0
|
|
2948
3493
|
}
|
|
2949
3494
|
),
|
|
2950
|
-
|
|
3495
|
+
effectivelyDone && !expanded && (teaserText || expandHint) ? /* @__PURE__ */ jsxs5("div", { className: "lax-workflow-stage-container__done-teaser", children: [
|
|
2951
3496
|
teaserText ? /* @__PURE__ */ jsx10(
|
|
2952
3497
|
"span",
|
|
2953
3498
|
{
|
|
@@ -2973,7 +3518,7 @@ function WorkflowStageContainer({
|
|
|
2973
3518
|
))
|
|
2974
3519
|
}
|
|
2975
3520
|
) : null,
|
|
2976
|
-
expanded &&
|
|
3521
|
+
expanded && effectivelyRunning && runningLoopTree ? /* @__PURE__ */ jsx10(
|
|
2977
3522
|
"div",
|
|
2978
3523
|
{
|
|
2979
3524
|
className: isItem ? "lax-workflow-parallel-item__body lax-workflow-stage-container__body" : "lax-workflow-stage-container__body",
|
|
@@ -2983,20 +3528,20 @@ function WorkflowStageContainer({
|
|
|
2983
3528
|
{
|
|
2984
3529
|
tree: runningLoopTree,
|
|
2985
3530
|
showTaskListFooter: false,
|
|
2986
|
-
virtualized,
|
|
3531
|
+
virtualized: itemLoopVirtualized,
|
|
2987
3532
|
virtualizeThreshold,
|
|
2988
3533
|
groupParallelTools
|
|
2989
3534
|
}
|
|
2990
3535
|
)
|
|
2991
3536
|
}
|
|
2992
3537
|
) : null,
|
|
2993
|
-
|
|
3538
|
+
effectivelyDone && expanded ? loopTree ? /* @__PURE__ */ jsx10(
|
|
2994
3539
|
WorkflowStageDoneBody,
|
|
2995
3540
|
{
|
|
2996
3541
|
node,
|
|
2997
3542
|
loopTree,
|
|
2998
3543
|
testId: bodyTestId(node),
|
|
2999
|
-
virtualized,
|
|
3544
|
+
virtualized: itemLoopVirtualized,
|
|
3000
3545
|
virtualizeThreshold,
|
|
3001
3546
|
groupParallelTools,
|
|
3002
3547
|
bodyClassName: isItem ? "lax-workflow-parallel-item__body lax-workflow-stage-container__body" : "lax-workflow-stage-container__body"
|
|
@@ -3016,6 +3561,14 @@ function WorkflowStageContainer({
|
|
|
3016
3561
|
}
|
|
3017
3562
|
);
|
|
3018
3563
|
}
|
|
3564
|
+
function workflowStageContainerPropsEqual(prev, next) {
|
|
3565
|
+
if (prev.suppressHostScopedLoop !== next.suppressHostScopedLoop) return false;
|
|
3566
|
+
if (prev.subtreeHasActiveWork !== next.subtreeHasActiveWork) return false;
|
|
3567
|
+
if (prev.loopTree !== next.loopTree) return false;
|
|
3568
|
+
if (!workflowContainerViewPropsEqual(prev, next)) return false;
|
|
3569
|
+
return workflowContainerNodeVisualEqual(prev.node, next.node);
|
|
3570
|
+
}
|
|
3571
|
+
var WorkflowStageContainer = memo2(WorkflowStageContainerInner, workflowStageContainerPropsEqual);
|
|
3019
3572
|
|
|
3020
3573
|
// src/view/workflow/WorkflowParallelGroup.tsx
|
|
3021
3574
|
import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
@@ -3023,7 +3576,7 @@ function resolveLoopTree(node, loopTreesBySessionId) {
|
|
|
3023
3576
|
if (!node.loopSessionId) return void 0;
|
|
3024
3577
|
return loopTreesBySessionId[node.loopSessionId];
|
|
3025
3578
|
}
|
|
3026
|
-
function
|
|
3579
|
+
function WorkflowParallelGroupInner({
|
|
3027
3580
|
groupKey,
|
|
3028
3581
|
items,
|
|
3029
3582
|
loopTreesBySessionId,
|
|
@@ -3032,33 +3585,23 @@ function WorkflowParallelGroup({
|
|
|
3032
3585
|
virtualizeThreshold,
|
|
3033
3586
|
groupParallelTools
|
|
3034
3587
|
}) {
|
|
3035
|
-
const scale = useWorkflowScaleOptions();
|
|
3036
|
-
const store = useWorkflowSessionStoreApi();
|
|
3037
|
-
const activeLoopSessionId = useStore3(store, (s) => s.state.activeLoopSessionId);
|
|
3038
|
-
const activeContainerIds = useStore3(store, (s) => s.state.containerTree.activeContainerIds);
|
|
3039
3588
|
const metrics = useMemo4(() => computeParallelGroupMetrics(items), [items]);
|
|
3040
3589
|
const summary = formatParallelGroupSummary(metrics);
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
[items, activeLoopSessionId, activeContainerIds]
|
|
3590
|
+
const hasActiveWork = useMemo4(
|
|
3591
|
+
() => parallelGroupHasActiveWork(items, loopTreesBySessionId),
|
|
3592
|
+
[items, loopTreesBySessionId]
|
|
3045
3593
|
);
|
|
3046
3594
|
const allSettled = metrics.total > 0 && metrics.running === 0 && metrics.completed + metrics.failed >= metrics.total;
|
|
3047
|
-
const
|
|
3048
|
-
const [expanded, setExpanded] = useState3(shouldAutoExpandGroup);
|
|
3595
|
+
const [expanded, setExpanded] = useState3(hasActiveWork);
|
|
3049
3596
|
useEffect5(() => {
|
|
3050
|
-
if (
|
|
3597
|
+
if (hasActiveWork) {
|
|
3051
3598
|
setExpanded(true);
|
|
3052
3599
|
return;
|
|
3053
3600
|
}
|
|
3054
|
-
if (hasRunning && scale.onlyExpandActiveRunning) {
|
|
3055
|
-
setExpanded(false);
|
|
3056
|
-
return;
|
|
3057
|
-
}
|
|
3058
3601
|
if (allSettled) {
|
|
3059
3602
|
setExpanded(false);
|
|
3060
3603
|
}
|
|
3061
|
-
}, [
|
|
3604
|
+
}, [hasActiveWork, allSettled]);
|
|
3062
3605
|
const toggleExpanded = useCallback3(() => {
|
|
3063
3606
|
setExpanded((prev) => !prev);
|
|
3064
3607
|
}, []);
|
|
@@ -3095,7 +3638,7 @@ function WorkflowParallelGroup({
|
|
|
3095
3638
|
WorkflowExpandTrigger,
|
|
3096
3639
|
{
|
|
3097
3640
|
summary,
|
|
3098
|
-
expandHint: !expanded && !
|
|
3641
|
+
expandHint: !expanded && hasActiveWork ? "(click to expand agents)" : !expanded && !hasActiveWork ? "(ctrl+o to expand)" : null,
|
|
3099
3642
|
onToggle: toggleExpanded,
|
|
3100
3643
|
expanded,
|
|
3101
3644
|
testId: `lax-workflow-parallel-group-summary-${groupKey}`,
|
|
@@ -3107,16 +3650,37 @@ function WorkflowParallelGroup({
|
|
|
3107
3650
|
}
|
|
3108
3651
|
);
|
|
3109
3652
|
}
|
|
3653
|
+
function workflowParallelGroupPropsEqual(prev, next) {
|
|
3654
|
+
if (prev.groupKey !== next.groupKey) return false;
|
|
3655
|
+
if (!workflowContainerViewPropsEqual(prev, next)) return false;
|
|
3656
|
+
if (prev.items.length !== next.items.length) return false;
|
|
3657
|
+
for (let i = 0; i < prev.items.length; i += 1) {
|
|
3658
|
+
const prevItem = prev.items[i];
|
|
3659
|
+
const nextItem = next.items[i];
|
|
3660
|
+
if (!workflowContainerNodeVisualEqual(prevItem, nextItem)) return false;
|
|
3661
|
+
const loopSessionId = prevItem.loopSessionId;
|
|
3662
|
+
if (loopSessionId) {
|
|
3663
|
+
if (prev.loopTreesBySessionId[loopSessionId] !== next.loopTreesBySessionId[loopSessionId]) {
|
|
3664
|
+
return false;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
return true;
|
|
3669
|
+
}
|
|
3670
|
+
var WorkflowParallelGroup = memo3(WorkflowParallelGroupInner, workflowParallelGroupPropsEqual);
|
|
3110
3671
|
|
|
3111
3672
|
// src/view/workflow/WorkflowRootContainer.tsx
|
|
3112
3673
|
import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3113
3674
|
function WorkflowRootContainer({
|
|
3114
3675
|
node,
|
|
3115
|
-
children
|
|
3676
|
+
children,
|
|
3677
|
+
containersById
|
|
3116
3678
|
}) {
|
|
3117
3679
|
const { verbose } = useSessionViewOptions();
|
|
3118
|
-
const
|
|
3119
|
-
const
|
|
3680
|
+
const visuallyActive = containersById ? isWorkflowRootVisuallyActive(node, containersById) : node.status === "running" || node.status === "retrying";
|
|
3681
|
+
const uiStatus = mapContainerUiStatus(visuallyActive ? "running" : node.status);
|
|
3682
|
+
const statusLabel = mapWorkflowRootStatusLabel(visuallyActive ? "running" : node.status);
|
|
3683
|
+
const meta = containersById ? buildEffectiveWorkflowRootMeta(node, containersById) : buildWorkflowRootMeta(node);
|
|
3120
3684
|
return /* @__PURE__ */ jsxs7("div", { className: "lax-workflow-root", "data-testid": "lax-workflow-root", children: [
|
|
3121
3685
|
/* @__PURE__ */ jsx12(
|
|
3122
3686
|
WorkflowContainerLine,
|
|
@@ -3125,7 +3689,7 @@ function WorkflowRootContainer({
|
|
|
3125
3689
|
title: node.title,
|
|
3126
3690
|
description: node.subtitle || void 0,
|
|
3127
3691
|
uiStatus,
|
|
3128
|
-
meta
|
|
3692
|
+
meta,
|
|
3129
3693
|
statusLabel,
|
|
3130
3694
|
titleTestId: "lax-workflow-root-title",
|
|
3131
3695
|
descriptionTestId: "lax-workflow-root-desc",
|
|
@@ -3253,31 +3817,36 @@ function buildDoneSummary(node) {
|
|
|
3253
3817
|
function WorkflowSubworkflowContainer({
|
|
3254
3818
|
node,
|
|
3255
3819
|
depth = 1,
|
|
3256
|
-
children
|
|
3820
|
+
children,
|
|
3821
|
+
subtreeHasActiveWork: subtreeHasActiveWork2 = false
|
|
3257
3822
|
}) {
|
|
3258
3823
|
const { verbose } = useSessionViewOptions();
|
|
3259
|
-
const uiStatus = mapContainerUiStatus(node.status);
|
|
3260
3824
|
const isRunning = node.status === "running" || node.status === "retrying";
|
|
3261
3825
|
const isDone = node.status === "completed" || node.status === "failed";
|
|
3826
|
+
const effectivelyRunning = isRunning || subtreeHasActiveWork2;
|
|
3827
|
+
const effectivelyDone = isDone && !subtreeHasActiveWork2;
|
|
3828
|
+
const uiStatus = mapContainerUiStatus(
|
|
3829
|
+
effectivelyRunning ? "running" : node.status
|
|
3830
|
+
);
|
|
3262
3831
|
const testId = `lax-workflow-subworkflow-${node.scope_key}`;
|
|
3263
3832
|
const teaser = useMemo6(() => {
|
|
3264
3833
|
if (node.content_blocks.length === 0) return "";
|
|
3265
3834
|
return node.content_blocks[node.content_blocks.length - 1]?.preview ?? "";
|
|
3266
3835
|
}, [node.content_blocks]);
|
|
3267
|
-
const [expanded, setExpanded] = useState5(
|
|
3836
|
+
const [expanded, setExpanded] = useState5(effectivelyRunning);
|
|
3268
3837
|
useEffect7(() => {
|
|
3269
|
-
if (
|
|
3270
|
-
if (
|
|
3271
|
-
}, [
|
|
3838
|
+
if (effectivelyRunning) setExpanded(true);
|
|
3839
|
+
else if (effectivelyDone) setExpanded(false);
|
|
3840
|
+
}, [effectivelyRunning, effectivelyDone]);
|
|
3272
3841
|
const toggleExpanded = useCallback5(() => {
|
|
3273
3842
|
setExpanded((prev) => !prev);
|
|
3274
3843
|
}, []);
|
|
3275
|
-
const shellToggle =
|
|
3844
|
+
const shellToggle = effectivelyRunning ? toggleExpanded : effectivelyDone && expanded ? toggleExpanded : void 0;
|
|
3276
3845
|
const depthStyle = {
|
|
3277
3846
|
"--lax-workflow-depth": depth,
|
|
3278
3847
|
"--lax-workflow-indent-unit": workflowIndentUnit(depth)
|
|
3279
3848
|
};
|
|
3280
|
-
const showShellLine = !(
|
|
3849
|
+
const showShellLine = !(effectivelyDone && !expanded);
|
|
3281
3850
|
return /* @__PURE__ */ jsxs9(
|
|
3282
3851
|
"div",
|
|
3283
3852
|
{
|
|
@@ -3294,9 +3863,11 @@ function WorkflowSubworkflowContainer({
|
|
|
3294
3863
|
description: node.subtitle || void 0,
|
|
3295
3864
|
uiStatus,
|
|
3296
3865
|
meta: buildWorkflowRootMeta(node),
|
|
3297
|
-
statusLabel: mapWorkflowRootStatusLabel(
|
|
3866
|
+
statusLabel: mapWorkflowRootStatusLabel(
|
|
3867
|
+
effectivelyRunning ? "running" : node.status
|
|
3868
|
+
),
|
|
3298
3869
|
onToggle: shellToggle,
|
|
3299
|
-
interactive:
|
|
3870
|
+
interactive: effectivelyRunning,
|
|
3300
3871
|
expanded,
|
|
3301
3872
|
titleTestId: `${testId}-title`,
|
|
3302
3873
|
descriptionTestId: `${testId}-desc`,
|
|
@@ -3313,15 +3884,16 @@ function WorkflowSubworkflowContainer({
|
|
|
3313
3884
|
children
|
|
3314
3885
|
}
|
|
3315
3886
|
) : null,
|
|
3316
|
-
|
|
3887
|
+
!expanded && children ? /* @__PURE__ */ jsx14(
|
|
3317
3888
|
WorkflowExpandTrigger,
|
|
3318
3889
|
{
|
|
3319
|
-
summary: `${node.title} \xB7 ${buildDoneSummary(node)}`,
|
|
3890
|
+
summary: `${node.title} \xB7 ${effectivelyDone ? buildDoneSummary(node) : "running"}`,
|
|
3320
3891
|
summaryTestId: `${testId}-title`,
|
|
3321
3892
|
teaser: teaser || void 0,
|
|
3893
|
+
expandHint: effectivelyRunning ? "(click to expand subworkflow)" : void 0,
|
|
3322
3894
|
onToggle: () => setExpanded(true),
|
|
3323
3895
|
expanded: false,
|
|
3324
|
-
testId: `${testId}-done`,
|
|
3896
|
+
testId: effectivelyDone ? `${testId}-done` : `${testId}-running-collapsed`,
|
|
3325
3897
|
teaserTestId: teaser ? `${testId}-teaser` : void 0
|
|
3326
3898
|
}
|
|
3327
3899
|
) : null
|
|
@@ -3345,8 +3917,9 @@ function collectSiblingItems(node, containersById) {
|
|
|
3345
3917
|
(child) => child.scope === "item"
|
|
3346
3918
|
);
|
|
3347
3919
|
}
|
|
3348
|
-
function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps) {
|
|
3920
|
+
function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps, options) {
|
|
3349
3921
|
const loopTree = resolveLoopTree2(node, loopTreesBySessionId);
|
|
3922
|
+
const subtreeActive = options?.subtreeHasActiveWork ?? subtreeHasActiveWork(containerTree.containersById, node.container_id);
|
|
3350
3923
|
switch (node.scope) {
|
|
3351
3924
|
case "aggregate":
|
|
3352
3925
|
return /* @__PURE__ */ jsx15(
|
|
@@ -3378,6 +3951,8 @@ function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, v
|
|
|
3378
3951
|
node,
|
|
3379
3952
|
loopTree,
|
|
3380
3953
|
depth,
|
|
3954
|
+
suppressHostScopedLoop: options?.suppressHostScopedLoop,
|
|
3955
|
+
subtreeHasActiveWork: subtreeActive,
|
|
3381
3956
|
...viewProps
|
|
3382
3957
|
},
|
|
3383
3958
|
node.container_id
|
|
@@ -3402,6 +3977,11 @@ function renderItemSiblings(items, groupKey, depth, containerTree, loopTreesBySe
|
|
|
3402
3977
|
}
|
|
3403
3978
|
return null;
|
|
3404
3979
|
}
|
|
3980
|
+
function stageHasSubworkflowChild(stageId, containerTree) {
|
|
3981
|
+
return collectChildContainers(containerTree.containersById, stageId).some(
|
|
3982
|
+
(node) => node.scope === "subworkflow"
|
|
3983
|
+
);
|
|
3984
|
+
}
|
|
3405
3985
|
function renderContainerChildren(parentId, containerTree, loopTreesBySessionId, viewProps, depth) {
|
|
3406
3986
|
const children = collectChildContainers(containerTree.containersById, parentId);
|
|
3407
3987
|
const items = children.filter((node) => node.scope === "item");
|
|
@@ -3426,19 +4006,31 @@ function renderContainerNode(node, containerTree, loopTreesBySessionId, viewProp
|
|
|
3426
4006
|
{
|
|
3427
4007
|
node,
|
|
3428
4008
|
depth,
|
|
4009
|
+
subtreeHasActiveWork: subtreeHasActiveWork(
|
|
4010
|
+
containerTree.containersById,
|
|
4011
|
+
node.container_id
|
|
4012
|
+
),
|
|
3429
4013
|
children: hasNested ? nested : null
|
|
3430
4014
|
},
|
|
3431
4015
|
node.container_id
|
|
3432
4016
|
);
|
|
3433
4017
|
}
|
|
3434
4018
|
if (node.scope === "stage") {
|
|
4019
|
+
const suppressHostScopedLoop = stageHasSubworkflowChild(node.container_id, containerTree);
|
|
3435
4020
|
return /* @__PURE__ */ jsxs10(
|
|
3436
4021
|
"div",
|
|
3437
4022
|
{
|
|
3438
4023
|
className: "lax-workflow-nested-block",
|
|
3439
4024
|
"data-testid": `lax-workflow-nested-${node.scope}-${node.scope_key}`,
|
|
3440
4025
|
children: [
|
|
3441
|
-
renderLeafContainer(
|
|
4026
|
+
renderLeafContainer(
|
|
4027
|
+
node,
|
|
4028
|
+
depth,
|
|
4029
|
+
containerTree,
|
|
4030
|
+
loopTreesBySessionId,
|
|
4031
|
+
viewProps,
|
|
4032
|
+
{ suppressHostScopedLoop }
|
|
4033
|
+
),
|
|
3442
4034
|
hasNested ? /* @__PURE__ */ jsx15("div", { className: "lax-workflow-container-children", children: nested }) : null
|
|
3443
4035
|
]
|
|
3444
4036
|
},
|
|
@@ -3479,8 +4071,8 @@ function WorkflowStageListPanel({
|
|
|
3479
4071
|
const [extraCompletedPages, setExtraCompletedPages] = useState6(0);
|
|
3480
4072
|
const maxVisibleDoneCount = scale.completedStagePageSize + extraCompletedPages * scale.completedStagePageSize;
|
|
3481
4073
|
const partition = useMemo7(
|
|
3482
|
-
() => partitionWorkflowStages(stages, maxVisibleDoneCount),
|
|
3483
|
-
[stages, maxVisibleDoneCount]
|
|
4074
|
+
() => partitionWorkflowStages(stages, maxVisibleDoneCount, containerTree.containersById),
|
|
4075
|
+
[stages, maxVisibleDoneCount, containerTree.containersById]
|
|
3484
4076
|
);
|
|
3485
4077
|
if (partition.visible.length === 0) return null;
|
|
3486
4078
|
const showMoreCount = Math.min(
|
|
@@ -3578,17 +4170,25 @@ function WorkflowChrome({
|
|
|
3578
4170
|
}
|
|
3579
4171
|
) });
|
|
3580
4172
|
}
|
|
3581
|
-
return /* @__PURE__ */ jsx15("div", { className: "lax-workflow-chrome", "data-testid": "lax-workflow-chrome", children: rootNodes.map((workflowNode) => /* @__PURE__ */ jsx15(
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
4173
|
+
return /* @__PURE__ */ jsx15("div", { className: "lax-workflow-chrome", "data-testid": "lax-workflow-chrome", children: rootNodes.map((workflowNode) => /* @__PURE__ */ jsx15(
|
|
4174
|
+
WorkflowRootContainer,
|
|
4175
|
+
{
|
|
4176
|
+
node: workflowNode,
|
|
4177
|
+
containersById: containerTree.containersById,
|
|
4178
|
+
children: renderWorkflowChildren(
|
|
4179
|
+
workflowNode,
|
|
4180
|
+
containerTree,
|
|
4181
|
+
loopTreesBySessionId,
|
|
4182
|
+
viewProps
|
|
4183
|
+
)
|
|
4184
|
+
},
|
|
4185
|
+
workflowNode.container_id
|
|
4186
|
+
)) });
|
|
3587
4187
|
}
|
|
3588
4188
|
|
|
3589
4189
|
// src/view/workflow/WorkflowTaskListFooter.tsx
|
|
3590
4190
|
import { useMemo as useMemo8, useState as useState7 } from "react";
|
|
3591
|
-
import { useStore as
|
|
4191
|
+
import { useStore as useStore3 } from "zustand";
|
|
3592
4192
|
|
|
3593
4193
|
// src/view/workflow/workflowTaskListMerge.ts
|
|
3594
4194
|
var STATUS_ORDER = {
|
|
@@ -3668,7 +4268,7 @@ function MessageResponse({ children }) {
|
|
|
3668
4268
|
}
|
|
3669
4269
|
function WorkflowTaskListFooter() {
|
|
3670
4270
|
const store = useWorkflowSessionStoreApi();
|
|
3671
|
-
const state =
|
|
4271
|
+
const state = useStore3(store, (s) => s.state);
|
|
3672
4272
|
const [expanded, setExpanded] = useState7(false);
|
|
3673
4273
|
const footerState = useMemo8(() => deriveWorkflowTaskFooterState(state), [state]);
|
|
3674
4274
|
const visible = shouldShowWorkflowTaskListFooter(state);
|
|
@@ -3708,7 +4308,7 @@ function WorkflowTaskListFooter() {
|
|
|
3708
4308
|
}
|
|
3709
4309
|
|
|
3710
4310
|
// src/view/workflow/WorkflowSession.tsx
|
|
3711
|
-
import { useStore as
|
|
4311
|
+
import { useStore as useStore4 } from "zustand";
|
|
3712
4312
|
import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3713
4313
|
function WorkflowSession({
|
|
3714
4314
|
source,
|
|
@@ -3731,6 +4331,7 @@ function WorkflowSession({
|
|
|
3731
4331
|
groupParallelTools = false,
|
|
3732
4332
|
workflowScale,
|
|
3733
4333
|
onRequestLoopReplay,
|
|
4334
|
+
onStreamComplete,
|
|
3734
4335
|
children
|
|
3735
4336
|
}) {
|
|
3736
4337
|
const storeRef = useRef3(null);
|
|
@@ -3780,37 +4381,61 @@ function WorkflowSession({
|
|
|
3780
4381
|
workspaceRoot
|
|
3781
4382
|
]
|
|
3782
4383
|
);
|
|
3783
|
-
const containerTree =
|
|
4384
|
+
const containerTree = useStore4(
|
|
3784
4385
|
storeRef.current,
|
|
3785
4386
|
(s) => s.state.containerTree
|
|
3786
4387
|
);
|
|
3787
|
-
const loopTreesBySessionId =
|
|
4388
|
+
const loopTreesBySessionId = useStore4(
|
|
3788
4389
|
storeRef.current,
|
|
3789
4390
|
(s) => s.state.loopTreesBySessionId
|
|
3790
4391
|
);
|
|
4392
|
+
const sessionStatus = useStore4(
|
|
4393
|
+
storeRef.current,
|
|
4394
|
+
(s) => s.state.status
|
|
4395
|
+
);
|
|
4396
|
+
const internalErrors = useStore4(
|
|
4397
|
+
storeRef.current,
|
|
4398
|
+
(s) => s.state.internalErrors
|
|
4399
|
+
);
|
|
4400
|
+
const onErrorRef = useRef3(onError);
|
|
4401
|
+
const onStreamCompleteRef = useRef3(onStreamComplete);
|
|
4402
|
+
onErrorRef.current = onError;
|
|
4403
|
+
onStreamCompleteRef.current = onStreamComplete;
|
|
3791
4404
|
useEffect8(() => {
|
|
3792
4405
|
const store = storeRef.current;
|
|
3793
4406
|
const controller = new AbortController();
|
|
3794
4407
|
void source.start((event, ctx) => {
|
|
3795
4408
|
store.getState().applyEvent(event, ctx);
|
|
3796
|
-
}, controller.signal).
|
|
4409
|
+
}, controller.signal).then(() => {
|
|
4410
|
+
store.getState().markStreamComplete();
|
|
4411
|
+
onStreamCompleteRef.current?.();
|
|
4412
|
+
}).catch((err) => {
|
|
3797
4413
|
if (err instanceof SseTransportTerminalError) {
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
4414
|
+
store.getState().markAsError(err);
|
|
4415
|
+
onErrorRef.current?.(err);
|
|
4416
|
+
} else if (err instanceof Error) {
|
|
4417
|
+
onErrorRef.current?.(err);
|
|
3802
4418
|
}
|
|
3803
4419
|
});
|
|
3804
4420
|
return () => {
|
|
3805
4421
|
controller.abort();
|
|
3806
4422
|
};
|
|
3807
|
-
}, [source
|
|
4423
|
+
}, [source]);
|
|
3808
4424
|
return /* @__PURE__ */ jsx17(WorkflowSessionStoreContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx17(WorkflowScaleContext.Provider, { value: scaleOptions, children: /* @__PURE__ */ jsx17(WorkflowLoopReplayProvider, { onRequestLoopReplay, children: /* @__PURE__ */ jsx17(InteractionBusContext.Provider, { value: busRef, children: /* @__PURE__ */ jsx17(NodeRegistryContext.Provider, { value: registryRef, children: /* @__PURE__ */ jsx17(MarkdownRendererContext.Provider, { value: markdownRenderer, children: /* @__PURE__ */ jsx17(ToolDisplayOptionsContext.Provider, { value: toolDisplayRef, children: /* @__PURE__ */ jsx17(SessionViewOptionsContext.Provider, { value: sessionViewRef, children: /* @__PURE__ */ jsxs12(
|
|
3809
4425
|
"div",
|
|
3810
4426
|
{
|
|
3811
4427
|
className: "lax-agent-session lax-workflow-session",
|
|
3812
4428
|
"data-testid": "lax-workflow-session",
|
|
3813
4429
|
children: [
|
|
4430
|
+
sessionStatus === "error" || internalErrors.length > 0 ? /* @__PURE__ */ jsx17(
|
|
4431
|
+
"div",
|
|
4432
|
+
{
|
|
4433
|
+
className: "lax-workflow-error",
|
|
4434
|
+
"data-testid": "lax-workflow-error",
|
|
4435
|
+
role: "alert",
|
|
4436
|
+
children: internalErrors[internalErrors.length - 1]?.message ?? "Workflow stream error"
|
|
4437
|
+
}
|
|
4438
|
+
) : null,
|
|
3814
4439
|
/* @__PURE__ */ jsx17(
|
|
3815
4440
|
WorkflowChrome,
|
|
3816
4441
|
{
|
|
@@ -4018,6 +4643,7 @@ export {
|
|
|
4018
4643
|
createReplaySource,
|
|
4019
4644
|
createSessionStore,
|
|
4020
4645
|
createWorkflowSessionStore,
|
|
4646
|
+
deriveLoopSessionIdFromAgentEvent,
|
|
4021
4647
|
displayPath,
|
|
4022
4648
|
entryKey,
|
|
4023
4649
|
filterAgentLoopReplayEvents,
|