bosun 0.42.2 → 0.42.4
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/.env.example +9 -0
- package/agent/agent-event-bus.mjs +10 -0
- package/agent/agent-supervisor.mjs +20 -0
- package/bosun-tui.mjs +107 -105
- package/cli.mjs +10 -0
- package/config/config.mjs +25 -0
- package/config/executor-config.mjs +124 -1
- package/infra/container-runner.mjs +565 -1
- package/infra/monitor.mjs +18 -0
- package/infra/tracing.mjs +544 -240
- package/infra/tui-bridge.mjs +13 -1
- package/kanban/kanban-adapter.mjs +128 -4
- package/lib/repo-map.mjs +114 -3
- package/package.json +11 -4
- package/server/ui-server.mjs +3 -0
- package/task/task-archiver.mjs +18 -6
- package/task/task-attachments.mjs +14 -10
- package/task/task-cli.mjs +24 -4
- package/task/task-executor.mjs +19 -0
- package/task/task-store.mjs +194 -37
- package/telegram/telegram-bot.mjs +4 -1
- package/tui/app.mjs +131 -171
- package/tui/components/status-header.mjs +178 -75
- package/tui/lib/header-config.mjs +68 -0
- package/tui/lib/ws-bridge.mjs +61 -9
- package/tui/screens/agents.mjs +127 -0
- package/tui/screens/tasks.mjs +1 -48
- package/ui/app.js +8 -5
- package/ui/components/kanban-board.js +65 -3
- package/ui/components/session-list.js +18 -32
- package/ui/demo-defaults.js +52 -2
- package/ui/modules/session-api.js +100 -0
- package/ui/modules/state.js +71 -15
- package/ui/tabs/workflows.js +25 -1
- package/ui/tui/App.js +298 -0
- package/ui/tui/TasksScreen.js +564 -0
- package/ui/tui/constants.js +55 -0
- package/ui/tui/tasks-screen-helpers.js +301 -0
- package/ui/tui/useTasks.js +61 -0
- package/ui/tui/useWebSocket.js +166 -0
- package/ui/tui/useWorkflows.js +30 -0
- package/workflow/workflow-engine.mjs +412 -7
- package/workflow/workflow-nodes.mjs +616 -75
- package/workflow-templates/agents.mjs +3 -0
- package/workflow-templates/planning.mjs +7 -0
- package/workflow-templates/sub-workflows.mjs +5 -0
- package/workflow-templates/task-execution.mjs +3 -0
- package/workspace/command-diagnostics.mjs +1 -1
- package/workspace/context-cache.mjs +182 -9
|
@@ -408,6 +408,190 @@ function cleanObject(value = {}) {
|
|
|
408
408
|
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
409
409
|
);
|
|
410
410
|
}
|
|
411
|
+
|
|
412
|
+
function normalizeDagText(value, maxLength = 240) {
|
|
413
|
+
const text = String(value ?? "")
|
|
414
|
+
.replace(/\s+/g, " ")
|
|
415
|
+
.trim();
|
|
416
|
+
if (!text) return "";
|
|
417
|
+
if (!Number.isFinite(maxLength) || maxLength <= 0 || text.length <= maxLength) return text;
|
|
418
|
+
return `${text.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function cloneDagGraphNode(node = {}) {
|
|
422
|
+
return {
|
|
423
|
+
nodeId: String(node.nodeId || "").trim(),
|
|
424
|
+
label: node.label || null,
|
|
425
|
+
type: node.type || null,
|
|
426
|
+
status: node.status || NodeStatus.PENDING,
|
|
427
|
+
dependencies: Array.isArray(node.dependencies) ? [...node.dependencies] : [],
|
|
428
|
+
attempts: Number(node.attempts || 0) || 0,
|
|
429
|
+
lastError: node.lastError || null,
|
|
430
|
+
issueFindingCount: Array.isArray(node.issueFindings) ? node.issueFindings.length : 0,
|
|
431
|
+
completionEvidenceCount: Array.isArray(node.completionEvidence) ? node.completionEvidence.length : 0,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function buildDagGraphSnapshot(dagState = {}) {
|
|
436
|
+
const nodes = Object.values(dagState?.nodes || {})
|
|
437
|
+
.map((node) => cloneDagGraphNode(node))
|
|
438
|
+
.filter((node) => node.nodeId);
|
|
439
|
+
const edges = Array.isArray(dagState?.edges)
|
|
440
|
+
? dagState.edges
|
|
441
|
+
.map((edge) => ({
|
|
442
|
+
edgeId: String(edge?.edgeId || edge?.id || `${edge?.source || ""}->${edge?.target || ""}`).trim(),
|
|
443
|
+
source: String(edge?.source || "").trim(),
|
|
444
|
+
target: String(edge?.target || "").trim(),
|
|
445
|
+
label: edge?.label || null,
|
|
446
|
+
condition: edge?.condition || null,
|
|
447
|
+
}))
|
|
448
|
+
.filter((edge) => edge.source && edge.target)
|
|
449
|
+
: [];
|
|
450
|
+
return { nodes, edges };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function detectDagFindingSource(node = {}, result = {}, errorMessage = "") {
|
|
454
|
+
const nodeType = String(node?.type || "").trim().toLowerCase();
|
|
455
|
+
const reason = String(result?.reason || "").trim().toLowerCase();
|
|
456
|
+
const combined = `${errorMessage} ${result?.output || ""} ${result?.stderr || ""} ${result?.reviewOutput || ""}`.toLowerCase();
|
|
457
|
+
|
|
458
|
+
if (nodeType.includes("model_review") || reason.includes("manual_review") || combined.includes("changes requested")) {
|
|
459
|
+
return "review";
|
|
460
|
+
}
|
|
461
|
+
if (
|
|
462
|
+
nodeType.startsWith("validation.") ||
|
|
463
|
+
combined.includes("validation") ||
|
|
464
|
+
combined.includes("tests red") ||
|
|
465
|
+
combined.includes("test failed")
|
|
466
|
+
) {
|
|
467
|
+
return "validation";
|
|
468
|
+
}
|
|
469
|
+
if (
|
|
470
|
+
combined.includes("dependency") ||
|
|
471
|
+
combined.includes("module not found") ||
|
|
472
|
+
combined.includes("cannot find module") ||
|
|
473
|
+
combined.includes("api changed") ||
|
|
474
|
+
combined.includes("contract") ||
|
|
475
|
+
combined.includes("schema")
|
|
476
|
+
) {
|
|
477
|
+
return "workflow_diagnostics";
|
|
478
|
+
}
|
|
479
|
+
return "execution";
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function detectDagRecommendedAction(node = {}, result = {}, errorMessage = "") {
|
|
483
|
+
const reason = String(result?.reason || "").trim().toLowerCase();
|
|
484
|
+
const combined = `${errorMessage} ${result?.output || ""} ${result?.stderr || ""} ${result?.reviewOutput || ""}`.toLowerCase();
|
|
485
|
+
const source = detectDagFindingSource(node, result, errorMessage);
|
|
486
|
+
|
|
487
|
+
if (
|
|
488
|
+
combined.includes("timeout") ||
|
|
489
|
+
combined.includes("timed out") ||
|
|
490
|
+
combined.includes("temporar") ||
|
|
491
|
+
combined.includes("network") ||
|
|
492
|
+
combined.includes("econnreset") ||
|
|
493
|
+
combined.includes("service unavailable") ||
|
|
494
|
+
combined.includes("rate limit")
|
|
495
|
+
) {
|
|
496
|
+
return "rerun_same_step";
|
|
497
|
+
}
|
|
498
|
+
if (source === "review") {
|
|
499
|
+
return "spawn_fix_step";
|
|
500
|
+
}
|
|
501
|
+
if (
|
|
502
|
+
source === "workflow_diagnostics" ||
|
|
503
|
+
combined.includes("dependency") ||
|
|
504
|
+
combined.includes("module not found") ||
|
|
505
|
+
combined.includes("cannot find module") ||
|
|
506
|
+
combined.includes("api changed") ||
|
|
507
|
+
combined.includes("schema") ||
|
|
508
|
+
combined.includes("contract")
|
|
509
|
+
) {
|
|
510
|
+
return "replan_subgraph";
|
|
511
|
+
}
|
|
512
|
+
if (reason === "manual_review_required") {
|
|
513
|
+
return "spawn_fix_step";
|
|
514
|
+
}
|
|
515
|
+
if (source === "validation") {
|
|
516
|
+
return "replan_from_failed";
|
|
517
|
+
}
|
|
518
|
+
return "inspect_failure";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function collectDagIssueFindings(node = {}, result = undefined, errorMessage = "") {
|
|
522
|
+
const output = result && typeof result === "object" ? result : {};
|
|
523
|
+
const message = normalizeDagText(
|
|
524
|
+
errorMessage || output?.reviewOutput || output?.stderr || output?.output || output?.reason || "",
|
|
525
|
+
260,
|
|
526
|
+
);
|
|
527
|
+
const failed = Boolean(errorMessage) || output?.passed === false || output?._failed === true;
|
|
528
|
+
if (!failed || !message) return [];
|
|
529
|
+
|
|
530
|
+
const source = detectDagFindingSource(node, output, message);
|
|
531
|
+
const recommendedAction = detectDagRecommendedAction(node, output, message);
|
|
532
|
+
return [cleanObject({
|
|
533
|
+
nodeId: node?.id || node?.nodeId || null,
|
|
534
|
+
label: node?.label || null,
|
|
535
|
+
source,
|
|
536
|
+
severity: source === "review" ? "high" : (source === "workflow_diagnostics" ? "high" : "medium"),
|
|
537
|
+
summary: message,
|
|
538
|
+
reason: output?.reason || null,
|
|
539
|
+
exitCode: Number.isFinite(Number(output?.exitCode)) ? Number(output.exitCode) : null,
|
|
540
|
+
command: normalizeDagText(output?.command || "", 140) || null,
|
|
541
|
+
recommendedAction,
|
|
542
|
+
suggestedRerun: output?.outputSuggestedRerun || output?.outputDiagnostics?.suggestedRerun || null,
|
|
543
|
+
})];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function collectDagCompletionEvidence(node = {}, result = undefined) {
|
|
547
|
+
const output = result && typeof result === "object" ? result : {};
|
|
548
|
+
const passed = output?.passed !== false && output?._failed !== true;
|
|
549
|
+
if (!passed) return [];
|
|
550
|
+
|
|
551
|
+
const evidence = [];
|
|
552
|
+
const summary = normalizeDagText(
|
|
553
|
+
output?.summary || output?.outputHint || output?.narrative || output?.output || output?.reviewPath || "",
|
|
554
|
+
220,
|
|
555
|
+
);
|
|
556
|
+
if (summary) {
|
|
557
|
+
evidence.push({
|
|
558
|
+
nodeId: node?.id || node?.nodeId || null,
|
|
559
|
+
label: node?.label || null,
|
|
560
|
+
kind: "summary",
|
|
561
|
+
summary,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
const command = normalizeDagText(output?.command || "", 140);
|
|
565
|
+
if (command) {
|
|
566
|
+
evidence.push(cleanObject({
|
|
567
|
+
nodeId: node?.id || node?.nodeId || null,
|
|
568
|
+
label: node?.label || null,
|
|
569
|
+
kind: "command",
|
|
570
|
+
command,
|
|
571
|
+
exitCode: Number.isFinite(Number(output?.exitCode)) ? Number(output.exitCode) : 0,
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
574
|
+
const artifactPath = normalizeDagText(output?.reviewPath || output?.evidenceDir || output?.path || "", 180);
|
|
575
|
+
if (artifactPath) {
|
|
576
|
+
evidence.push({
|
|
577
|
+
nodeId: node?.id || node?.nodeId || null,
|
|
578
|
+
label: node?.label || null,
|
|
579
|
+
kind: "artifact",
|
|
580
|
+
path: artifactPath,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
return evidence;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function summarizeDagEvidence(evidence = [], limit = 3) {
|
|
587
|
+
if (!Array.isArray(evidence) || evidence.length === 0) return [];
|
|
588
|
+
return evidence.slice(0, Math.max(1, limit)).map((entry) => {
|
|
589
|
+
if (entry?.summary) return entry.summary;
|
|
590
|
+
if (entry?.command) return `Command: ${entry.command}`;
|
|
591
|
+
if (entry?.path) return `Artifact: ${entry.path}`;
|
|
592
|
+
return normalizeDagText(JSON.stringify(entry), 140);
|
|
593
|
+
}).filter(Boolean);
|
|
594
|
+
}
|
|
411
595
|
/**
|
|
412
596
|
* Register a node type handler.
|
|
413
597
|
* @param {string} type - Node type identifier (e.g., "trigger.task_low", "action.run_agent")
|
|
@@ -532,6 +716,43 @@ export function listNodeTypes() {
|
|
|
532
716
|
* Runtime context passed through workflow execution.
|
|
533
717
|
* Accumulates data from each node's output.
|
|
534
718
|
*/
|
|
719
|
+
function collectValidationFailures(detail = {}) {
|
|
720
|
+
const nodeOutputs = detail?.nodeOutputs && typeof detail.nodeOutputs === "object"
|
|
721
|
+
? detail.nodeOutputs
|
|
722
|
+
: {};
|
|
723
|
+
const dagNodes = detail?.dagState?.nodes && typeof detail.dagState.nodes === "object"
|
|
724
|
+
? detail.dagState.nodes
|
|
725
|
+
: {};
|
|
726
|
+
return Object.entries(nodeOutputs)
|
|
727
|
+
.map(([nodeId, output]) => {
|
|
728
|
+
if (!output || typeof output !== "object") return null;
|
|
729
|
+
const diagnostic = output.failureDiagnostic && typeof output.failureDiagnostic === "object"
|
|
730
|
+
? output.failureDiagnostic
|
|
731
|
+
: null;
|
|
732
|
+
const failureKind = String(output.failureKind || diagnostic?.category || "").trim();
|
|
733
|
+
const hasRetryability = typeof output.retryable === "boolean" || typeof diagnostic?.retryable === "boolean";
|
|
734
|
+
if (!failureKind && !diagnostic && !hasRetryability) return null;
|
|
735
|
+
return {
|
|
736
|
+
nodeId,
|
|
737
|
+
nodeType: dagNodes?.[nodeId]?.type || null,
|
|
738
|
+
nodeLabel: dagNodes?.[nodeId]?.label || null,
|
|
739
|
+
failureKind: failureKind || diagnostic?.category || null,
|
|
740
|
+
retryable: typeof output.retryable === "boolean"
|
|
741
|
+
? output.retryable
|
|
742
|
+
: diagnostic?.retryable === true,
|
|
743
|
+
blocked: output.blocked === true || diagnostic?.blocked === true,
|
|
744
|
+
exitCode: diagnostic?.exitCode ?? output.exitCode ?? null,
|
|
745
|
+
summary:
|
|
746
|
+
diagnostic?.summary ||
|
|
747
|
+
output.outputHint ||
|
|
748
|
+
output.outputDiagnostics?.summary ||
|
|
749
|
+
null,
|
|
750
|
+
detail: diagnostic?.detail || null,
|
|
751
|
+
};
|
|
752
|
+
})
|
|
753
|
+
.filter(Boolean);
|
|
754
|
+
}
|
|
755
|
+
|
|
535
756
|
export class WorkflowContext {
|
|
536
757
|
constructor(initialData = {}) {
|
|
537
758
|
this.id = randomUUID();
|
|
@@ -709,7 +930,7 @@ export class WorkflowContext {
|
|
|
709
930
|
/** Get a serializable summary of the execution */
|
|
710
931
|
toJSON(endedAt = Date.now()) {
|
|
711
932
|
const finishedAt = Number.isFinite(endedAt) ? endedAt : Date.now();
|
|
712
|
-
|
|
933
|
+
const detail = {
|
|
713
934
|
id: this.id,
|
|
714
935
|
startedAt: this.startedAt,
|
|
715
936
|
endedAt: finishedAt,
|
|
@@ -735,6 +956,12 @@ export class WorkflowContext {
|
|
|
735
956
|
}))
|
|
736
957
|
: [],
|
|
737
958
|
};
|
|
959
|
+
const validationFailures = collectValidationFailures(detail);
|
|
960
|
+
if (validationFailures.length > 0) {
|
|
961
|
+
detail.validationFailures = validationFailures;
|
|
962
|
+
detail.latestValidationFailure = validationFailures.at(-1) || null;
|
|
963
|
+
}
|
|
964
|
+
return detail;
|
|
738
965
|
}
|
|
739
966
|
}
|
|
740
967
|
|
|
@@ -803,14 +1030,22 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
803
1030
|
|
|
804
1031
|
_initializeDagState(def, ctx, extra = {}) {
|
|
805
1032
|
const dependencyMap = new Map();
|
|
1033
|
+
const edges = [];
|
|
806
1034
|
for (const node of def?.nodes || []) {
|
|
807
1035
|
dependencyMap.set(node.id, []);
|
|
808
1036
|
}
|
|
809
1037
|
for (const edge of def?.edges || []) {
|
|
810
|
-
if (!edge?.target || edge.backEdge === true) continue;
|
|
1038
|
+
if (!edge?.source || !edge?.target || edge.backEdge === true) continue;
|
|
811
1039
|
const deps = dependencyMap.get(edge.target) || [];
|
|
812
1040
|
deps.push(edge.source);
|
|
813
1041
|
dependencyMap.set(edge.target, deps);
|
|
1042
|
+
edges.push({
|
|
1043
|
+
edgeId: String(edge.id || `${edge.source}->${edge.target}`),
|
|
1044
|
+
source: edge.source,
|
|
1045
|
+
target: edge.target,
|
|
1046
|
+
label: edge.label || null,
|
|
1047
|
+
condition: edge.condition || null,
|
|
1048
|
+
});
|
|
814
1049
|
}
|
|
815
1050
|
|
|
816
1051
|
const nowIso = new Date(ctx.startedAt).toISOString();
|
|
@@ -823,9 +1058,11 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
823
1058
|
parentRunId: extra.parentRunId || ctx.data?._workflowParentRunId || null,
|
|
824
1059
|
retryOf: extra.retryOf || ctx.data?._retryOf || null,
|
|
825
1060
|
retryMode: extra.retryMode || null,
|
|
1061
|
+
revisionReason: extra.revisionReason || null,
|
|
826
1062
|
createdAt: nowIso,
|
|
827
1063
|
updatedAt: nowIso,
|
|
828
1064
|
status: WorkflowStatus.RUNNING,
|
|
1065
|
+
revisions: [],
|
|
829
1066
|
counts: {
|
|
830
1067
|
total: Array.isArray(def?.nodes) ? def.nodes.length : 0,
|
|
831
1068
|
pending: Array.isArray(def?.nodes) ? def.nodes.length : 0,
|
|
@@ -834,6 +1071,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
834
1071
|
failed: 0,
|
|
835
1072
|
skipped: 0,
|
|
836
1073
|
},
|
|
1074
|
+
edges,
|
|
837
1075
|
nodes: Object.fromEntries(
|
|
838
1076
|
(def?.nodes || []).map((node) => [
|
|
839
1077
|
node.id,
|
|
@@ -846,6 +1084,8 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
846
1084
|
attempts: 0,
|
|
847
1085
|
lastError: null,
|
|
848
1086
|
outputSummary: null,
|
|
1087
|
+
issueFindings: [],
|
|
1088
|
+
completionEvidence: [],
|
|
849
1089
|
startedAt: null,
|
|
850
1090
|
endedAt: null,
|
|
851
1091
|
updatedAt: nowIso,
|
|
@@ -860,6 +1100,63 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
860
1100
|
return dagState;
|
|
861
1101
|
}
|
|
862
1102
|
|
|
1103
|
+
_recordDagRevision(ctx, revision = {}) {
|
|
1104
|
+
const dagState = ctx?.data?._dagState;
|
|
1105
|
+
if (!dagState || typeof dagState !== "object" || !dagState.nodes || typeof dagState.nodes !== "object") {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
if (!Array.isArray(dagState.revisions)) dagState.revisions = [];
|
|
1109
|
+
const nodes = Object.values(dagState.nodes);
|
|
1110
|
+
const counts = {
|
|
1111
|
+
total: nodes.length,
|
|
1112
|
+
pending: nodes.filter((node) => node?.status === NodeStatus.PENDING).length,
|
|
1113
|
+
running: nodes.filter((node) => node?.status === NodeStatus.RUNNING || node?.status === NodeStatus.WAITING).length,
|
|
1114
|
+
completed: nodes.filter((node) => node?.status === NodeStatus.COMPLETED).length,
|
|
1115
|
+
failed: nodes.filter((node) => node?.status === NodeStatus.FAILED).length,
|
|
1116
|
+
skipped: nodes.filter((node) => node?.status === NodeStatus.SKIPPED).length,
|
|
1117
|
+
};
|
|
1118
|
+
const completedNodeIds = nodes.filter((node) => node?.status === NodeStatus.COMPLETED).map((node) => node.nodeId);
|
|
1119
|
+
const failedNodeIds = nodes.filter((node) => node?.status === NodeStatus.FAILED).map((node) => node.nodeId);
|
|
1120
|
+
const pendingNodeIds = nodes
|
|
1121
|
+
.filter((node) => node?.status === NodeStatus.PENDING || node?.status === NodeStatus.WAITING)
|
|
1122
|
+
.map((node) => node.nodeId);
|
|
1123
|
+
const graphBefore = revision.graphBefore && typeof revision.graphBefore === "object"
|
|
1124
|
+
? revision.graphBefore
|
|
1125
|
+
: (dagState.revisions.length > 0 ? dagState.revisions[dagState.revisions.length - 1]?.graphAfter || null : null);
|
|
1126
|
+
const graphAfter = revision.graphAfter && typeof revision.graphAfter === "object"
|
|
1127
|
+
? revision.graphAfter
|
|
1128
|
+
: buildDagGraphSnapshot(dagState);
|
|
1129
|
+
const issueFindings = nodes.flatMap((node) => Array.isArray(node?.issueFindings) ? node.issueFindings : []);
|
|
1130
|
+
const completionEvidence = nodes.flatMap((node) => Array.isArray(node?.completionEvidence) ? node.completionEvidence : []);
|
|
1131
|
+
const snapshot = {
|
|
1132
|
+
index: dagState.revisions.length,
|
|
1133
|
+
recordedAt: new Date().toISOString(),
|
|
1134
|
+
reason: revision.reason || "update",
|
|
1135
|
+
sourceRunId: revision.sourceRunId || null,
|
|
1136
|
+
retryMode: dagState.retryMode || null,
|
|
1137
|
+
status: dagState.status || WorkflowStatus.RUNNING,
|
|
1138
|
+
counts,
|
|
1139
|
+
preservedCompletedNodeIds: Array.isArray(revision.preservedCompletedNodeIds)
|
|
1140
|
+
? [...new Set(revision.preservedCompletedNodeIds.map((id) => String(id || "").trim()).filter(Boolean))]
|
|
1141
|
+
: completedNodeIds,
|
|
1142
|
+
focusNodeIds: Array.isArray(revision.focusNodeIds)
|
|
1143
|
+
? [...new Set(revision.focusNodeIds.map((id) => String(id || "").trim()).filter(Boolean))]
|
|
1144
|
+
: (failedNodeIds.length ? failedNodeIds : pendingNodeIds),
|
|
1145
|
+
failedNodeIds,
|
|
1146
|
+
pendingNodeIds,
|
|
1147
|
+
issueFindingCount: issueFindings.length,
|
|
1148
|
+
completionEvidenceCount: completionEvidence.length,
|
|
1149
|
+
issueFindingsPreview: issueFindings.slice(0, 6),
|
|
1150
|
+
completionEvidencePreview: completionEvidence.slice(0, 6),
|
|
1151
|
+
graphBefore,
|
|
1152
|
+
graphAfter,
|
|
1153
|
+
edgeCount: Array.isArray(dagState.edges) ? dagState.edges.length : 0,
|
|
1154
|
+
nodeCount: nodes.length,
|
|
1155
|
+
};
|
|
1156
|
+
dagState.revisions.push(snapshot);
|
|
1157
|
+
dagState.updatedAt = snapshot.recordedAt;
|
|
1158
|
+
return snapshot;
|
|
1159
|
+
}
|
|
863
1160
|
_refreshDagState(ctx, status = null) {
|
|
864
1161
|
const dagState = ctx?.data?._dagState;
|
|
865
1162
|
if (!dagState || typeof dagState !== "object" || !dagState.nodes || typeof dagState.nodes !== "object") {
|
|
@@ -882,19 +1179,48 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
882
1179
|
const pendingNodes = nodes.filter((node) => node?.status === NodeStatus.PENDING || node?.status === NodeStatus.WAITING);
|
|
883
1180
|
const firstFailed = failedNodes[0] || null;
|
|
884
1181
|
const firstPending = pendingNodes[0] || null;
|
|
1182
|
+
const issueFindings = nodes.flatMap((node) => Array.isArray(node?.issueFindings) ? node.issueFindings : []);
|
|
1183
|
+
const completionEvidence = nodes.flatMap((node) => Array.isArray(node?.completionEvidence) ? node.completionEvidence : []);
|
|
1184
|
+
const firstFinding = issueFindings[0] || null;
|
|
885
1185
|
|
|
886
1186
|
let recommendedAction = "continue";
|
|
887
1187
|
let summary = "Workflow is ready to continue.";
|
|
888
1188
|
if (failedNodes.length > 0) {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1189
|
+
const preferredAction = firstFinding?.recommendedAction || null;
|
|
1190
|
+
recommendedAction = preferredAction;
|
|
1191
|
+
if (!recommendedAction || (recommendedAction === "inspect_failure" && counts.completed > 0)) {
|
|
1192
|
+
recommendedAction = counts.completed > 0 ? "replan_from_failed" : "inspect_failure";
|
|
1193
|
+
}
|
|
1194
|
+
summary = firstFinding?.summary
|
|
1195
|
+
? `Failed at ${firstFailed?.label || firstFailed?.nodeId || "a workflow step"}: ${firstFinding.summary}`
|
|
1196
|
+
: (firstFailed?.lastError
|
|
1197
|
+
? `Failed at ${firstFailed.label || firstFailed.nodeId}: ${firstFailed.lastError}`
|
|
1198
|
+
: `Workflow has ${failedNodes.length} failed node(s).`);
|
|
893
1199
|
} else if (pendingNodes.length > 0 && counts.completed > 0) {
|
|
894
1200
|
recommendedAction = "resume_remaining";
|
|
895
1201
|
summary = `Resume from ${firstPending?.label || firstPending?.nodeId || "the next pending node"}.`;
|
|
896
1202
|
}
|
|
897
1203
|
|
|
1204
|
+
const nextStepGuidance = failedNodes.length > 0
|
|
1205
|
+
? [
|
|
1206
|
+
recommendedAction === "rerun_same_step"
|
|
1207
|
+
? "Preserve completed work and rerun the same failed step with the suggested command or fix."
|
|
1208
|
+
: (recommendedAction === "spawn_fix_step"
|
|
1209
|
+
? "Preserve completed work and insert a targeted fix step before resuming downstream execution."
|
|
1210
|
+
: (recommendedAction === "replan_subgraph"
|
|
1211
|
+
? "Preserve completed work and replan the impacted downstream subgraph before continuing."
|
|
1212
|
+
: "Preserve completed work and replan from the failed boundary.")),
|
|
1213
|
+
firstFailed?.label ? `Focus next on ${firstFailed.label}.` : null,
|
|
1214
|
+
firstFinding?.suggestedRerun ? `Suggested rerun: ${firstFinding.suggestedRerun}` : null,
|
|
1215
|
+
firstFailed?.lastError ? `Address failure: ${firstFailed.lastError}` : null,
|
|
1216
|
+
].filter(Boolean).join(" ")
|
|
1217
|
+
: (pendingNodes.length > 0 && counts.completed > 0
|
|
1218
|
+
? [
|
|
1219
|
+
"Preserve completed work and continue from the next pending node.",
|
|
1220
|
+
firstPending?.label ? `Next step: ${firstPending.label}.` : null,
|
|
1221
|
+
].filter(Boolean).join(" ")
|
|
1222
|
+
: "Plan is healthy; continue executing the remaining graph.");
|
|
1223
|
+
|
|
898
1224
|
const issueAdvisor = {
|
|
899
1225
|
status: dagState.status,
|
|
900
1226
|
summary,
|
|
@@ -902,14 +1228,28 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
902
1228
|
failedNodeCount: failedNodes.length,
|
|
903
1229
|
pendingNodeCount: pendingNodes.length,
|
|
904
1230
|
completedNodeCount: counts.completed,
|
|
905
|
-
suggestedRerun: firstFailed?.suggestedRerun || null,
|
|
1231
|
+
suggestedRerun: firstFailed?.suggestedRerun || firstFinding?.suggestedRerun || null,
|
|
1232
|
+
retryDecisionClass:
|
|
1233
|
+
recommendedAction === "rerun_same_step"
|
|
1234
|
+
? "rerun_same_step"
|
|
1235
|
+
: (recommendedAction === "spawn_fix_step"
|
|
1236
|
+
? "spawn_fix_step"
|
|
1237
|
+
: ((recommendedAction === "replan_subgraph" || recommendedAction === "replan_from_failed")
|
|
1238
|
+
? "replan_entire_subgraph"
|
|
1239
|
+
: "inspect_failure")),
|
|
906
1240
|
failedNodes: failedNodes.slice(0, 6).map((node) => ({
|
|
907
1241
|
nodeId: node.nodeId,
|
|
908
1242
|
label: node.label || null,
|
|
909
1243
|
error: node.lastError || null,
|
|
910
1244
|
attempts: node.attempts || 0,
|
|
1245
|
+
issueFindings: Array.isArray(node.issueFindings) ? node.issueFindings.slice(0, 3) : [],
|
|
1246
|
+
completionEvidence: Array.isArray(node.completionEvidence) ? node.completionEvidence.slice(0, 3) : [],
|
|
911
1247
|
})),
|
|
1248
|
+
issueFindings: issueFindings.slice(0, 8),
|
|
1249
|
+
completionEvidence: completionEvidence.slice(0, 8),
|
|
912
1250
|
updatedAt: dagState.updatedAt,
|
|
1251
|
+
nextStepGuidance,
|
|
1252
|
+
dagRevisionCount: Array.isArray(dagState.revisions) ? dagState.revisions.length : 0,
|
|
913
1253
|
};
|
|
914
1254
|
|
|
915
1255
|
ctx.data._issueAdvisor = issueAdvisor;
|
|
@@ -920,11 +1260,16 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
920
1260
|
ctx.data._plannerFeedback = {
|
|
921
1261
|
...existingFeedback,
|
|
922
1262
|
issueAdvisor,
|
|
1263
|
+
issueAdvisorSummary: [summary, nextStepGuidance].filter(Boolean).join("\n"),
|
|
923
1264
|
dagStateSummary: {
|
|
1265
|
+
revisionCount: Array.isArray(dagState.revisions) ? dagState.revisions.length : 0,
|
|
924
1266
|
runId: dagState.runId,
|
|
925
1267
|
workflowId: dagState.workflowId,
|
|
926
1268
|
status: dagState.status,
|
|
927
1269
|
counts,
|
|
1270
|
+
issueFindingCount: issueFindings.length,
|
|
1271
|
+
completionEvidenceCount: completionEvidence.length,
|
|
1272
|
+
graph: buildDagGraphSnapshot(dagState),
|
|
928
1273
|
},
|
|
929
1274
|
};
|
|
930
1275
|
return issueAdvisor;
|
|
@@ -989,11 +1334,18 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
989
1334
|
} = {}) {
|
|
990
1335
|
if (!ctx || !node?.id) return;
|
|
991
1336
|
const timing = ctx.getNodeTiming(node.id) || {};
|
|
1337
|
+
const issueFindings = collectDagIssueFindings(node, result, error ? String(error) : "");
|
|
1338
|
+
const completionEvidence = collectDagCompletionEvidence(node, result);
|
|
1339
|
+
const existingDagNode = ctx.data?._dagState?.nodes?.[node.id] || {};
|
|
992
1340
|
const nodePatch = cleanObject({
|
|
993
1341
|
status,
|
|
994
1342
|
attempts: Number.isFinite(Number(attempt)) ? Number(attempt) : ctx.getRetryCount(node.id),
|
|
995
1343
|
lastError: error ? String(error) : null,
|
|
996
1344
|
outputSummary: result !== undefined ? this._summarizeTaskTraceNodeResult(result) : undefined,
|
|
1345
|
+
issueFindings: issueFindings.length > 0 ? issueFindings : (status === NodeStatus.COMPLETED ? [] : (existingDagNode.issueFindings || undefined)),
|
|
1346
|
+
completionEvidence: completionEvidence.length > 0
|
|
1347
|
+
? [...(Array.isArray(existingDagNode.completionEvidence) ? existingDagNode.completionEvidence : []), ...completionEvidence]
|
|
1348
|
+
: (status === NodeStatus.FAILED ? (existingDagNode.completionEvidence || undefined) : undefined),
|
|
997
1349
|
startedAt: Number.isFinite(Number(timing.startedAt)) ? new Date(Number(timing.startedAt)).toISOString() : null,
|
|
998
1350
|
endedAt: Number.isFinite(Number(timing.endedAt)) ? new Date(Number(timing.endedAt)).toISOString() : null,
|
|
999
1351
|
suggestedRerun:
|
|
@@ -1812,6 +2164,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1812
2164
|
_parentRunId: runId,
|
|
1813
2165
|
_rootRunId: originalRootRunId,
|
|
1814
2166
|
_retryMode: mode,
|
|
2167
|
+
_dagRevisionReason: "retry_replan_subgraph",
|
|
1815
2168
|
force: true,
|
|
1816
2169
|
});
|
|
1817
2170
|
return { retryRunId: ctx.id, mode, originalRunId: runId, ctx };
|
|
@@ -1842,8 +2195,11 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1842
2195
|
});
|
|
1843
2196
|
|
|
1844
2197
|
// Pre-populate nodes that already succeeded.
|
|
2198
|
+
const preservedCompletedNodeIds = [];
|
|
2199
|
+
const focusNodeIds = [];
|
|
1845
2200
|
for (const [nodeId, status] of Object.entries(nodeStatuses)) {
|
|
1846
2201
|
if (status === NodeStatus.COMPLETED) {
|
|
2202
|
+
preservedCompletedNodeIds.push(nodeId);
|
|
1847
2203
|
ctx.setNodeStatus(nodeId, NodeStatus.COMPLETED);
|
|
1848
2204
|
if (nodeOutputs[nodeId] !== undefined) {
|
|
1849
2205
|
ctx.setNodeOutput(nodeId, nodeOutputs[nodeId]);
|
|
@@ -1852,10 +2208,25 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1852
2208
|
status: NodeStatus.COMPLETED,
|
|
1853
2209
|
result: nodeOutputs[nodeId],
|
|
1854
2210
|
});
|
|
2211
|
+
} else if (status) {
|
|
2212
|
+
focusNodeIds.push(nodeId);
|
|
1855
2213
|
}
|
|
1856
2214
|
// Reset failed / skipped nodes so the DAG will re-run them.
|
|
1857
2215
|
}
|
|
1858
2216
|
|
|
2217
|
+
this._recordDagRevision(ctx, {
|
|
2218
|
+
reason: "retry_resume",
|
|
2219
|
+
sourceRunId: runId,
|
|
2220
|
+
preservedCompletedNodeIds,
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
this._recordDagRevision(ctx, {
|
|
2224
|
+
reason: mode === "from_failed" ? "retry_replan_from_failed" : `retry_${mode}`,
|
|
2225
|
+
sourceRunId: runId,
|
|
2226
|
+
preservedCompletedNodeIds,
|
|
2227
|
+
focusNodeIds,
|
|
2228
|
+
});
|
|
2229
|
+
|
|
1859
2230
|
const retryRunId = ctx.id;
|
|
1860
2231
|
this._activeRuns.set(retryRunId, {
|
|
1861
2232
|
workflowId,
|
|
@@ -2062,6 +2433,15 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2062
2433
|
if (issueAdvisor?.recommendedAction === "resume_remaining") {
|
|
2063
2434
|
mode = "from_failed";
|
|
2064
2435
|
reason = "issue_advisor.resume_remaining";
|
|
2436
|
+
} else if (issueAdvisor?.recommendedAction === "rerun_same_step") {
|
|
2437
|
+
mode = "from_failed";
|
|
2438
|
+
reason = "issue_advisor.rerun_same_step";
|
|
2439
|
+
} else if (issueAdvisor?.recommendedAction === "spawn_fix_step") {
|
|
2440
|
+
mode = "from_failed";
|
|
2441
|
+
reason = "issue_advisor.spawn_fix_step";
|
|
2442
|
+
} else if (issueAdvisor?.recommendedAction === "replan_subgraph") {
|
|
2443
|
+
mode = "from_scratch";
|
|
2444
|
+
reason = "issue_advisor.replan_subgraph";
|
|
2065
2445
|
} else if (issueAdvisor?.recommendedAction === "replan_from_failed") {
|
|
2066
2446
|
mode = "from_scratch";
|
|
2067
2447
|
reason = "issue_advisor.replan_from_failed";
|
|
@@ -2392,6 +2772,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2392
2772
|
|
|
2393
2773
|
let runs = [...active, ...persisted.filter((run) => !activeRunIds.has(run.runId))];
|
|
2394
2774
|
if (workflowId) runs = runs.filter((r) => r.workflowId === workflowId);
|
|
2775
|
+
runs = runs.map((run) => this.getRunDetail(run.runId) || run);
|
|
2395
2776
|
runs.sort((a, b) => Number(b?.startedAt || 0) - Number(a?.startedAt || 0));
|
|
2396
2777
|
if (hasLimit) {
|
|
2397
2778
|
return runs.slice(0, Math.floor(normalizedLimit));
|
|
@@ -2854,8 +3235,11 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2854
3235
|
// Pre-seed completed nodes from snapshot
|
|
2855
3236
|
const nodeStatuses = snapshot.nodeStatuses || {};
|
|
2856
3237
|
const nodeOutputs = snapshot.nodeOutputs || {};
|
|
3238
|
+
const preservedCompletedNodeIds = [];
|
|
3239
|
+
const focusNodeIds = [];
|
|
2857
3240
|
for (const [nodeId, status] of Object.entries(nodeStatuses)) {
|
|
2858
3241
|
if (status === "completed") {
|
|
3242
|
+
preservedCompletedNodeIds.push(nodeId);
|
|
2859
3243
|
ctx.setNodeStatus(nodeId, NodeStatus.COMPLETED);
|
|
2860
3244
|
if (nodeOutputs[nodeId] !== undefined) {
|
|
2861
3245
|
ctx.setNodeOutput(nodeId, nodeOutputs[nodeId]);
|
|
@@ -2863,6 +3247,13 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2863
3247
|
}
|
|
2864
3248
|
}
|
|
2865
3249
|
|
|
3250
|
+
this._recordDagRevision(ctx, {
|
|
3251
|
+
reason: (opts.mode === "from_failed") ? "retry_replan_from_failed" : `retry_${opts.mode || "snapshot"}`,
|
|
3252
|
+
sourceRunId: snapshotId,
|
|
3253
|
+
preservedCompletedNodeIds,
|
|
3254
|
+
focusNodeIds,
|
|
3255
|
+
});
|
|
3256
|
+
|
|
2866
3257
|
const retryRunId = ctx.id;
|
|
2867
3258
|
this._activeRuns.set(retryRunId, {
|
|
2868
3259
|
workflowId,
|
|
@@ -3032,8 +3423,10 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3032
3423
|
// If nodes are already marked COMPLETED in the context (pre-seeded by
|
|
3033
3424
|
// retryRun), treat them as already executed so the DAG skips them and
|
|
3034
3425
|
// begins from the first un-completed node.
|
|
3426
|
+
const preservedCompletedNodeIds = [];
|
|
3035
3427
|
for (const [nodeId, status] of ctx.nodeStatuses) {
|
|
3036
3428
|
if (status === NodeStatus.COMPLETED) {
|
|
3429
|
+
preservedCompletedNodeIds.push(nodeId);
|
|
3037
3430
|
executed.add(nodeId);
|
|
3038
3431
|
}
|
|
3039
3432
|
}
|
|
@@ -3872,6 +4265,8 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3872
4265
|
|
|
3873
4266
|
_serializeRunContext(ctx, isRunning = false) {
|
|
3874
4267
|
const detail = ctx.toJSON(Date.now());
|
|
4268
|
+
if (ctx?.data?._dagState) detail.dagState = ctx.data._dagState;
|
|
4269
|
+
if (ctx?.data?._issueAdvisor) detail.issueAdvisor = ctx.data._issueAdvisor;
|
|
3875
4270
|
if (isRunning) {
|
|
3876
4271
|
detail.endedAt = null;
|
|
3877
4272
|
detail.duration = Math.max(0, Date.now() - Number(ctx?.startedAt || Date.now()));
|
|
@@ -3929,6 +4324,8 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3929
4324
|
const retryDecisionReason = detail?.data?._retryDecisionReason || null;
|
|
3930
4325
|
const issueAdvisorRecommendation = detail?.issueAdvisor?.recommendedAction || null;
|
|
3931
4326
|
const issueAdvisorSummary = detail?.issueAdvisor?.summary || null;
|
|
4327
|
+
const dagRevisionCount = Array.isArray(detail?.dagState?.revisions) ? detail.dagState.revisions.length : 0;
|
|
4328
|
+
const validationFailures = collectValidationFailures(detail);
|
|
3932
4329
|
|
|
3933
4330
|
return {
|
|
3934
4331
|
runId,
|
|
@@ -3962,6 +4359,13 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3962
4359
|
retryDecisionReason,
|
|
3963
4360
|
issueAdvisorRecommendation,
|
|
3964
4361
|
issueAdvisorSummary,
|
|
4362
|
+
dagRevisionCount,
|
|
4363
|
+
...(validationFailures.length > 0
|
|
4364
|
+
? {
|
|
4365
|
+
validationFailures,
|
|
4366
|
+
latestValidationFailure: validationFailures.at(-1) || null,
|
|
4367
|
+
}
|
|
4368
|
+
: {}),
|
|
3965
4369
|
};
|
|
3966
4370
|
}
|
|
3967
4371
|
|
|
@@ -4546,3 +4950,4 @@ export function getWorkflow(id, opts) { return getWorkflowEngine(opts).get(id);
|
|
|
4546
4950
|
export async function executeWorkflow(id, data, opts) { return getWorkflowEngine(opts).execute(id, data, opts); }
|
|
4547
4951
|
export async function retryWorkflowRun(runId, retryOpts, engineOpts) { return getWorkflowEngine(engineOpts).retryRun(runId, retryOpts); }
|
|
4548
4952
|
|
|
4953
|
+
|