@pikku/core 0.12.133 → 0.12.135
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/CHANGELOG.md +13 -0
- package/dist/function/compensation-name.d.ts +4 -0
- package/dist/function/compensation-name.js +6 -0
- package/dist/function/function-meta.types.d.ts +2 -0
- package/dist/function/function-runner.js +17 -0
- package/dist/function/functions.types.d.ts +7 -0
- package/dist/testing/service-tests/queued-workflow-harness.d.ts +61 -0
- package/dist/testing/service-tests/queued-workflow-harness.js +151 -0
- package/dist/testing/service-tests/workflow-compensation-queued-tests.d.ts +11 -0
- package/dist/testing/service-tests/workflow-compensation-queued-tests.js +683 -0
- package/dist/testing/service-tests.d.ts +4 -0
- package/dist/testing/service-tests.js +4 -0
- package/dist/utils/hmac.d.ts +0 -33
- package/dist/utils/hmac.js +0 -61
- package/dist/wirings/rpc/rpc-runner.js +21 -1
- package/dist/wirings/trigger/webhook-source-runner.js +17 -10
- package/dist/wirings/trigger/webhook-source.types.d.ts +4 -3
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +38 -6
- package/dist/wirings/workflow/graph/graph-node.d.ts +2 -1
- package/dist/wirings/workflow/graph/graph-node.js +2 -1
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +104 -67
- package/dist/wirings/workflow/graph/graph-validation.js +2 -2
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +20 -1
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +13 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +113 -143
- package/dist/wirings/workflow/run-timeline.d.ts +2 -0
- package/dist/wirings/workflow/run-timeline.js +3 -0
- package/dist/wirings/workflow/workflow-child-step.d.ts +17 -0
- package/dist/wirings/workflow/workflow-child-step.js +31 -0
- package/dist/wirings/workflow/workflow-compensation.d.ts +68 -0
- package/dist/wirings/workflow/workflow-compensation.js +282 -0
- package/dist/wirings/workflow/workflow-constants.d.ts +5 -0
- package/dist/wirings/workflow/workflow-constants.js +9 -0
- package/dist/wirings/workflow/workflow-dsl-pass.d.ts +12 -0
- package/dist/wirings/workflow/workflow-dsl-pass.js +75 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +3 -3
- package/dist/wirings/workflow/workflow-run-status.js +13 -1
- package/dist/wirings/workflow/workflow-status-stream.js +2 -0
- package/dist/wirings/workflow/workflow-unwind-plan.d.ts +38 -0
- package/dist/wirings/workflow/workflow-unwind-plan.js +120 -0
- package/dist/wirings/workflow/workflow.types.d.ts +9 -2
- package/knowledge/decisions/internals/workflow-step-compensation-runs-as-its-own-durable-step.md +21 -18
- package/package.json +1 -1
- package/src/public-surface.json +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WorkflowAsyncException, WorkflowSuspendedException, } from '../workflow-errors.js';
|
|
2
|
-
import { DEFAULT_STEP_RETRIES } from '../workflow-constants.js';
|
|
2
|
+
import { DEFAULT_STEP_RETRIES, NO_COMPENSATE_STATE_PREFIX, } from '../workflow-constants.js';
|
|
3
|
+
import { childRunFailure } from '../workflow-compensation.js';
|
|
3
4
|
import { runningStepLease } from '../workflow-step-lease.js';
|
|
4
5
|
import { pikkuState, getSingletonServices } from '../../../pikku-state.js';
|
|
5
6
|
import { RPCNotFoundError } from '../../rpc/rpc-runner.js';
|
|
@@ -87,7 +88,7 @@ function closesCycle(source, target, nodes) {
|
|
|
87
88
|
}
|
|
88
89
|
return false;
|
|
89
90
|
}
|
|
90
|
-
function planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, graphName, fanoutWidths = {}) {
|
|
91
|
+
function planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, graphName, fanoutWidths = {}, recovered = new Set()) {
|
|
91
92
|
const toLogical = (name) => remapStepNamesToNodeIds([name], nodes, graphName)[0];
|
|
92
93
|
const countByLogical = {};
|
|
93
94
|
const instancesByLogical = {};
|
|
@@ -103,10 +104,12 @@ function planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, grap
|
|
|
103
104
|
const width = fanoutWidths[nodeId];
|
|
104
105
|
if (width === undefined)
|
|
105
106
|
return false;
|
|
106
|
-
const succeeded = (instancesByLogical[nodeId] ?? []).filter(
|
|
107
|
+
const succeeded = (instancesByLogical[nodeId] ?? []).filter(isDone).length;
|
|
107
108
|
return succeeded >= width;
|
|
108
109
|
};
|
|
109
|
-
const
|
|
110
|
+
const ignored = (name) => recovered.has(name) && nodes[toLogical(name)]?.recover === 'ignore';
|
|
111
|
+
const isDone = (i) => i.status === 'succeeded' || ignored(i.stepName);
|
|
112
|
+
const completed = instances.filter(isDone);
|
|
110
113
|
const completedLogical = new Set();
|
|
111
114
|
for (const inst of completed) {
|
|
112
115
|
const logical = toLogical(inst.stepName);
|
|
@@ -121,6 +124,20 @@ function planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, grap
|
|
|
121
124
|
for (const entryId of entryNodeIds) {
|
|
122
125
|
edges.push({ fromKey: ENTRY_FROM, target: entryId });
|
|
123
126
|
}
|
|
127
|
+
for (const inst of instances) {
|
|
128
|
+
if (!recovered.has(inst.stepName))
|
|
129
|
+
continue;
|
|
130
|
+
const fromLogical = toLogical(inst.stepName);
|
|
131
|
+
for (const target of recoverTargets(nodes[fromLogical])) {
|
|
132
|
+
edges.push({
|
|
133
|
+
from: inst.stepName,
|
|
134
|
+
fromKey: inst.stepName,
|
|
135
|
+
fromLogical,
|
|
136
|
+
target,
|
|
137
|
+
recoveringFrom: { nodeId: fromLogical, stepName: inst.stepName },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
124
141
|
const fannedEdgesEmitted = new Set();
|
|
125
142
|
for (const inst of completed) {
|
|
126
143
|
const fromLogical = toLogical(inst.stepName);
|
|
@@ -155,12 +172,23 @@ function planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, grap
|
|
|
155
172
|
edges.push({ from: nodeId, fromKey: nodeId, fromLogical: nodeId, target });
|
|
156
173
|
}
|
|
157
174
|
}
|
|
175
|
+
const deadLogical = new Set(instances
|
|
176
|
+
.filter((i) => recovered.has(i.stepName) && !isDone(i))
|
|
177
|
+
.map((i) => toLogical(i.stepName)));
|
|
178
|
+
const readsDeadNode = (nodeId) => nodeDependencies(nodes[nodeId] ?? {}).some((dep) => deadLogical.has(dep));
|
|
158
179
|
const toFire = [];
|
|
159
180
|
const plannedFanout = new Set();
|
|
160
181
|
let blockedWaiting = false;
|
|
161
182
|
for (const edge of edges) {
|
|
162
183
|
const target = edge.target;
|
|
163
184
|
const edgeKey = `${edge.fromKey}->${target}`;
|
|
185
|
+
// A node reading the output of a failure that was recovered without
|
|
186
|
+
// `'ignore'` has no output to read and never will; waiting on it would
|
|
187
|
+
// leave the run unable to finish.
|
|
188
|
+
if (readsDeadNode(target)) {
|
|
189
|
+
consumed.add(edgeKey);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
164
192
|
if (isFanned(target)) {
|
|
165
193
|
if (plannedFanout.has(target))
|
|
166
194
|
continue;
|
|
@@ -221,13 +249,14 @@ function planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, grap
|
|
|
221
249
|
logical: target,
|
|
222
250
|
instanceKey: visits === 0 ? target : `${target}#${visits}`,
|
|
223
251
|
fromStepName: edge.from,
|
|
252
|
+
recoveringFrom: edge.recoveringFrom,
|
|
224
253
|
});
|
|
225
254
|
countByLogical[target] = visits + 1;
|
|
226
255
|
consumed.add(edgeKey);
|
|
227
256
|
}
|
|
228
257
|
return {
|
|
229
258
|
toFire,
|
|
230
|
-
hasInFlight: instances.some((i) => i
|
|
259
|
+
hasInFlight: instances.some((i) => !isDone(i) && !recovered.has(i.stepName)),
|
|
231
260
|
blockedWaiting,
|
|
232
261
|
};
|
|
233
262
|
}
|
|
@@ -396,6 +425,8 @@ function extractReferencedNodeIds(input) {
|
|
|
396
425
|
}
|
|
397
426
|
const IGNORED_REFS = new Set(['trigger', '$item', 'unknown']);
|
|
398
427
|
const FOREACH_MODES = new Set(['parallel', 'sequential']);
|
|
428
|
+
const recoverTargets = (node) => node.recover === 'ignore' ? [] : normalizeNodeTargets(node.recover);
|
|
429
|
+
const recoversFailure = (node) => !!node?.recover;
|
|
399
430
|
function normalizeNodeTargets(value) {
|
|
400
431
|
if (!value)
|
|
401
432
|
return [];
|
|
@@ -447,10 +478,9 @@ function validateGraphReferences(graphName, nodes, entryNodes) {
|
|
|
447
478
|
throw new Error(`Workflow graph '${graphName}': node '${nodeId}' routes to unknown node '${nextId}'`);
|
|
448
479
|
}
|
|
449
480
|
}
|
|
450
|
-
const
|
|
451
|
-
for (const errorId of errorTargets) {
|
|
481
|
+
for (const errorId of recoverTargets(node)) {
|
|
452
482
|
if (!nodeIds.has(errorId)) {
|
|
453
|
-
throw new Error(`Workflow graph '${graphName}': node '${nodeId}'
|
|
483
|
+
throw new Error(`Workflow graph '${graphName}': node '${nodeId}' recover targets unknown node '${errorId}'`);
|
|
454
484
|
}
|
|
455
485
|
}
|
|
456
486
|
}
|
|
@@ -564,6 +594,51 @@ async function createGraphResultReader(workflowService, runId, graphName, nodes,
|
|
|
564
594
|
},
|
|
565
595
|
};
|
|
566
596
|
}
|
|
597
|
+
const splitFailures = (rawFailed, nodes, graphName) => {
|
|
598
|
+
const logical = remapStepNamesToNodeIds(rawFailed, nodes, graphName);
|
|
599
|
+
const recovered = new Set();
|
|
600
|
+
const unrecovered = [];
|
|
601
|
+
rawFailed.forEach((stepName, i) => {
|
|
602
|
+
if (recoversFailure(nodes[logical[i]]))
|
|
603
|
+
recovered.add(stepName);
|
|
604
|
+
else
|
|
605
|
+
unrecovered.push(logical[i]);
|
|
606
|
+
});
|
|
607
|
+
return { recovered, unrecovered };
|
|
608
|
+
};
|
|
609
|
+
async function failGraphRun(workflowService, runId, failedNode, rpcService) {
|
|
610
|
+
await failGraphRunWith(workflowService, runId, new Error(`Graph node '${failedNode}' failed after exhausting retries`), rpcService);
|
|
611
|
+
}
|
|
612
|
+
async function failGraphRunWith(workflowService, runId, cause, rpcService) {
|
|
613
|
+
const error = Object.assign(cause, { code: 'GRAPH_NODE_FAILED' });
|
|
614
|
+
const run = await workflowService.getRun(runId);
|
|
615
|
+
if (run && rpcService) {
|
|
616
|
+
await workflowService.failOrUnwind(run, error, rpcService);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
await workflowService.updateRunStatus(runId, 'failed', undefined, {
|
|
620
|
+
message: error.message,
|
|
621
|
+
stack: error.stack ?? '',
|
|
622
|
+
code: 'GRAPH_NODE_FAILED',
|
|
623
|
+
});
|
|
624
|
+
if (run)
|
|
625
|
+
await workflowService.notifyChildWorkflowFailed(run, error);
|
|
626
|
+
}
|
|
627
|
+
const RECOVERING_FROM_STATE_PREFIX = 'recoveringFrom:';
|
|
628
|
+
async function prepareFire(workflowService, runId, fire, node) {
|
|
629
|
+
if (node.compensate === false) {
|
|
630
|
+
await workflowService.updateRunState(runId, `${NO_COMPENSATE_STATE_PREFIX}${fire.instanceKey}`, true);
|
|
631
|
+
}
|
|
632
|
+
const from = fire.recoveringFrom;
|
|
633
|
+
if (!from) {
|
|
634
|
+
await workflowService.updateRunState(runId, `${RECOVERING_FROM_STATE_PREFIX}${fire.instanceKey}`, null);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const steps = await workflowService.getRunSteps(runId);
|
|
638
|
+
const failed = steps.find((step) => step.stepName === from.stepName);
|
|
639
|
+
await workflowService.updateRunState(runId, `${RECOVERING_FROM_STATE_PREFIX}${fire.instanceKey}`, { ...from, error: failed?.error ?? { message: 'step failed' } });
|
|
640
|
+
await workflowService.updateRunState(runId, `${NO_COMPENSATE_STATE_PREFIX}${from.stepName}`, true);
|
|
641
|
+
}
|
|
567
642
|
const graphStepOptions = (nodeConfig) => ({
|
|
568
643
|
retries: nodeConfig?.retries ?? DEFAULT_STEP_RETRIES,
|
|
569
644
|
retryDelay: nodeConfig?.retryDelay,
|
|
@@ -573,7 +648,7 @@ async function queueGraphNode(workflowService, runId, _graphName, nodeId, rpcNam
|
|
|
573
648
|
await workflowService.insertStepState(runId, nodeId, rpcName, input, stepOptions, fromStepName);
|
|
574
649
|
await workflowService.queueStepWorker(runId, nodeId, rpcName, input, stepOptions, fromStepName);
|
|
575
650
|
}
|
|
576
|
-
export async function continueGraph(workflowService, runId, graphName, overrideMeta) {
|
|
651
|
+
export async function continueGraph(workflowService, runId, graphName, overrideMeta, rpcService) {
|
|
577
652
|
const meta = overrideMeta ?? getWorkflowMeta(graphName);
|
|
578
653
|
if (!meta?.nodes) {
|
|
579
654
|
throw new Error(`Workflow graph meta '${graphName}' not found`);
|
|
@@ -583,14 +658,9 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
|
|
|
583
658
|
const { completedNodeIds: rawCompleted, failedNodeIds: rawFailed, branchKeys: branchByStep, } = await workflowService.getCompletedGraphState(runId);
|
|
584
659
|
remapStepNamesToNodeIds(rawCompleted, nodes, graphName);
|
|
585
660
|
remapBranchKeys(branchByStep, nodes, graphName);
|
|
586
|
-
const
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
await workflowService.updateRunStatus(runId, 'failed', undefined, {
|
|
590
|
-
message: `Graph node '${failedNode}' failed after exhausting retries`,
|
|
591
|
-
stack: '',
|
|
592
|
-
code: 'GRAPH_NODE_FAILED',
|
|
593
|
-
});
|
|
661
|
+
const { recovered, unrecovered } = splitFailures(rawFailed, nodes, graphName);
|
|
662
|
+
if (unrecovered.length > 0) {
|
|
663
|
+
await failGraphRun(workflowService, runId, unrecovered[0], rpcService);
|
|
594
664
|
return;
|
|
595
665
|
}
|
|
596
666
|
const currentRun = await workflowService.getRun(runId);
|
|
@@ -600,7 +670,7 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
|
|
|
600
670
|
const instances = await workflowService.getStepInstances(runId);
|
|
601
671
|
const triggerInput = currentRun?.input;
|
|
602
672
|
const reader = await createGraphResultReader(workflowService, runId, graphName, nodes, triggerInput, instances);
|
|
603
|
-
const plan = planGraphTransitions(nodes, instances, branchByStep, meta.entryNodeIds ?? [], graphName, reader.fanoutWidths);
|
|
673
|
+
const plan = planGraphTransitions(nodes, instances, branchByStep, meta.entryNodeIds ?? [], graphName, reader.fanoutWidths, recovered);
|
|
604
674
|
const resolveNodeInput = async (logical, itemIndex) => {
|
|
605
675
|
const node = nodes[logical];
|
|
606
676
|
const referencedNodeIds = extractReferencedNodeIds(node.input).filter((id) => !IGNORED_REFS.has(id));
|
|
@@ -638,6 +708,7 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
|
|
|
638
708
|
if (!node?.rpcName)
|
|
639
709
|
continue;
|
|
640
710
|
const resolvedInput = await resolveNodeInput(fire.logical, fire.itemIndex);
|
|
711
|
+
await prepareFire(workflowService, runId, fire, node);
|
|
641
712
|
await queueGraphNode(workflowService, runId, graphName, fire.instanceKey, node.rpcName, resolvedInput, node, fire.fromStepName);
|
|
642
713
|
}
|
|
643
714
|
}
|
|
@@ -652,6 +723,7 @@ async function invokeGraphNodeRpc(workflowService, rpcService, runId, stepId, no
|
|
|
652
723
|
},
|
|
653
724
|
setState: (name, value) => workflowService.updateRunState(runId, name, value),
|
|
654
725
|
getState: () => workflowService.getRunState(runId),
|
|
726
|
+
recoveringFrom: (await workflowService.getRunState(runId))[`${RECOVERING_FROM_STATE_PREFIX}${nodeId}`],
|
|
655
727
|
};
|
|
656
728
|
const result = await rpcService.rpcWithWire(rpcName, input, {
|
|
657
729
|
graph: graphWire,
|
|
@@ -681,12 +753,9 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
|
|
|
681
753
|
await workflowService.setStepChildRunId(stepId, childRunId);
|
|
682
754
|
if (shouldInline) {
|
|
683
755
|
const childRun = await workflowService.getRun(childRunId);
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (childRun?.status === 'cancelled') {
|
|
688
|
-
throw new Error('Sub-workflow was cancelled');
|
|
689
|
-
}
|
|
756
|
+
const failure = childRun && childRunFailure(childRun);
|
|
757
|
+
if (failure)
|
|
758
|
+
throw failure;
|
|
690
759
|
result = childRun?.output;
|
|
691
760
|
}
|
|
692
761
|
else {
|
|
@@ -717,27 +786,11 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
|
|
|
717
786
|
});
|
|
718
787
|
throw error;
|
|
719
788
|
}
|
|
720
|
-
const meta = getWorkflowMeta(graphName);
|
|
721
|
-
if (meta?.nodes) {
|
|
722
|
-
const node = meta.nodes[nodeId];
|
|
723
|
-
if (node?.onError) {
|
|
724
|
-
const errorNodes = Array.isArray(node.onError)
|
|
725
|
-
? node.onError
|
|
726
|
-
: [node.onError];
|
|
727
|
-
for (const errorNodeId of errorNodes) {
|
|
728
|
-
const errorNode = meta.nodes[errorNodeId];
|
|
729
|
-
if (errorNode) {
|
|
730
|
-
await queueGraphNode(workflowService, runId, graphName, errorNodeId, errorNode.rpcName, { error: { message: error.message } }, errorNode);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
throw error;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
789
|
throw error;
|
|
737
790
|
}
|
|
738
791
|
}
|
|
739
792
|
export async function runFromMeta(workflowService, runId, meta, _rpcService) {
|
|
740
|
-
await continueGraph(workflowService, runId, meta.name, meta);
|
|
793
|
+
await continueGraph(workflowService, runId, meta.name, meta, _rpcService);
|
|
741
794
|
}
|
|
742
795
|
async function executeGraphNodeInline(workflowService, rpcService, runId, graphName, nodeId, instanceKey, input, nodes, fromStepName) {
|
|
743
796
|
const node = nodes[nodeId];
|
|
@@ -762,12 +815,9 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
|
|
|
762
815
|
const { runId: childRunId } = await workflowService.startWorkflow(rpcName, input, childWire, rpcService, { inline: true });
|
|
763
816
|
await workflowService.setStepChildRunId(stepState.stepId, childRunId);
|
|
764
817
|
const childRun = await workflowService.getRun(childRunId);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (childRun?.status === 'cancelled') {
|
|
769
|
-
throw new Error('Sub-workflow was cancelled');
|
|
770
|
-
}
|
|
818
|
+
const failure = childRun && childRunFailure(childRun);
|
|
819
|
+
if (failure)
|
|
820
|
+
throw failure;
|
|
771
821
|
result = childRun?.output;
|
|
772
822
|
}
|
|
773
823
|
else if (agentMeta) {
|
|
@@ -793,13 +843,8 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
|
|
|
793
843
|
throw new WorkflowSuspendedException(runId, 'RPC_NOT_FOUND');
|
|
794
844
|
}
|
|
795
845
|
await workflowService.setStepError(stepState.stepId, error);
|
|
796
|
-
if (node
|
|
797
|
-
const errorNodes = Array.isArray(node.onError)
|
|
798
|
-
? node.onError
|
|
799
|
-
: [node.onError];
|
|
800
|
-
await Promise.all(errorNodes.map((errorNodeId) => executeGraphNodeInline(workflowService, rpcService, runId, graphName, errorNodeId, errorNodeId, { error: { message: error.message } }, nodes, nodeId)));
|
|
846
|
+
if (recoversFailure(node))
|
|
801
847
|
return;
|
|
802
|
-
}
|
|
803
848
|
throw error;
|
|
804
849
|
}
|
|
805
850
|
}
|
|
@@ -808,14 +853,9 @@ async function continueGraphInline(workflowService, rpcService, runId, graphName
|
|
|
808
853
|
const { failedNodeIds: rawFailed, branchKeys: branchByStep, completedNodeIds: rawCompleted, } = await workflowService.getCompletedGraphState(runId);
|
|
809
854
|
remapStepNamesToNodeIds(rawCompleted, nodes, graphName);
|
|
810
855
|
remapBranchKeys(branchByStep, nodes, graphName);
|
|
811
|
-
const
|
|
812
|
-
if (
|
|
813
|
-
|
|
814
|
-
await workflowService.updateRunStatus(runId, 'failed', undefined, {
|
|
815
|
-
message: `Graph node '${failedNode}' failed after exhausting retries`,
|
|
816
|
-
stack: '',
|
|
817
|
-
code: 'GRAPH_NODE_FAILED',
|
|
818
|
-
});
|
|
856
|
+
const { recovered, unrecovered } = splitFailures(rawFailed, nodes, graphName);
|
|
857
|
+
if (unrecovered.length > 0) {
|
|
858
|
+
await failGraphRun(workflowService, runId, unrecovered[0], rpcService);
|
|
819
859
|
return;
|
|
820
860
|
}
|
|
821
861
|
const run = await workflowService.getRun(runId);
|
|
@@ -824,7 +864,7 @@ async function continueGraphInline(workflowService, rpcService, runId, graphName
|
|
|
824
864
|
}
|
|
825
865
|
const instances = await workflowService.getStepInstances(runId);
|
|
826
866
|
const reader = await createGraphResultReader(workflowService, runId, graphName, nodes, triggerInput, instances);
|
|
827
|
-
const plan = planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, graphName, reader.fanoutWidths);
|
|
867
|
+
const plan = planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, graphName, reader.fanoutWidths, recovered);
|
|
828
868
|
if (plan.toFire.length === 0) {
|
|
829
869
|
if (!plan.hasInFlight && !plan.blockedWaiting) {
|
|
830
870
|
await workflowService.updateRunStatus(runId, 'completed');
|
|
@@ -848,6 +888,7 @@ async function continueGraphInline(workflowService, rpcService, runId, graphName
|
|
|
848
888
|
}
|
|
849
889
|
const resolvedInput = resolveSerializedInput(node.input, nodeResults);
|
|
850
890
|
executed++;
|
|
891
|
+
await prepareFire(workflowService, runId, fire, node);
|
|
851
892
|
await executeGraphNodeInline(workflowService, rpcService, runId, graphName, fire.logical, fire.instanceKey, resolvedInput, nodes, fire.fromStepName);
|
|
852
893
|
}));
|
|
853
894
|
if (executed === 0)
|
|
@@ -926,11 +967,7 @@ export async function runWorkflowGraph(workflowService, graphName, triggerInput,
|
|
|
926
967
|
error instanceof WorkflowSuspendedException) {
|
|
927
968
|
return;
|
|
928
969
|
}
|
|
929
|
-
await workflowService
|
|
930
|
-
message: error.message,
|
|
931
|
-
stack: error.stack || '',
|
|
932
|
-
code: 'GRAPH_NODE_FAILED',
|
|
933
|
-
});
|
|
970
|
+
await failGraphRunWith(workflowService, runId, error, rpcService);
|
|
934
971
|
}
|
|
935
972
|
finally {
|
|
936
973
|
workflowService.unregisterInlineRun(runId);
|
|
@@ -24,9 +24,9 @@ export function generateMermaidDiagram(workflowName, nodes, entryNodeIds) {
|
|
|
24
24
|
for (const target of nextTargets) {
|
|
25
25
|
lines.push(` ${nodeId} --> ${target}`);
|
|
26
26
|
}
|
|
27
|
-
const errorTargets = normalizeTargets(node.
|
|
27
|
+
const errorTargets = node.recover === 'ignore' ? [] : normalizeTargets(node.recover);
|
|
28
28
|
for (const target of errorTargets) {
|
|
29
|
-
lines.push(` ${nodeId} -.->|
|
|
29
|
+
lines.push(` ${nodeId} -.->|recover| ${target}`);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
return lines.join('\n');
|
|
@@ -26,7 +26,14 @@ export interface GraphNodeConfig<NodeIds extends string = string> {
|
|
|
26
26
|
mode?: ForEachMode;
|
|
27
27
|
input?: (ref: RefFn<NodeIds>, template: TemplateFn, $item: ItemFn) => Record<string, unknown>;
|
|
28
28
|
next?: NextConfig<NodeIds>;
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* What happens when this node fails after its retries: run these nodes
|
|
31
|
+
* instead (the error is on `graph.recoveringFrom`), or `'ignore'` to carry
|
|
32
|
+
* on to `next` with a null output. A recovered failure is not compensated.
|
|
33
|
+
*/
|
|
34
|
+
recover?: NodeIds | NodeIds[] | 'ignore';
|
|
35
|
+
/** Leave this node out of the saga unwind. */
|
|
36
|
+
compensate?: false;
|
|
30
37
|
retries?: number;
|
|
31
38
|
retryDelay?: string | number;
|
|
32
39
|
notes?: string;
|
|
@@ -38,6 +45,18 @@ export interface PikkuGraphWire {
|
|
|
38
45
|
branch: (key: string) => void;
|
|
39
46
|
setState: (name: string, value: unknown) => Promise<void>;
|
|
40
47
|
getState: () => Promise<Record<string, unknown>>;
|
|
48
|
+
/** Set on a node that runs because another node failed and named it in `recover`. */
|
|
49
|
+
recoveringFrom?: GraphRecovery;
|
|
50
|
+
}
|
|
51
|
+
export interface GraphRecovery {
|
|
52
|
+
nodeId: string;
|
|
53
|
+
stepName: string;
|
|
54
|
+
error: {
|
|
55
|
+
message: string;
|
|
56
|
+
stack?: string;
|
|
57
|
+
code?: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
};
|
|
41
60
|
}
|
|
42
61
|
export interface GraphWireState {
|
|
43
62
|
branchKey?: string;
|
|
@@ -16,6 +16,6 @@ export { template, type TemplateString } from './graph/template.js';
|
|
|
16
16
|
export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGraphResult, } from './graph/wire-workflow-graph.js';
|
|
17
17
|
export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
|
|
18
18
|
export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowsRuntimeMeta, } from './workflow.types.js';
|
|
19
|
-
export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, Condition, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
|
|
19
|
+
export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, Condition, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, MilestoneStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
|
|
20
20
|
export { createGraph } from './graph/graph-node.js';
|
|
21
21
|
export type { GraphNodeConfig, ForEachConfig, ForEachMode, ItemFn, TemplateFn, } from './graph/workflow-graph.types.js';
|
|
@@ -52,6 +52,11 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
52
52
|
abstract getRunHistory(runId: string): Promise<Array<StepState & {
|
|
53
53
|
stepName: string;
|
|
54
54
|
}>>;
|
|
55
|
+
abstract getRunSteps(runId: string): Promise<Array<StepState & {
|
|
56
|
+
stepName: string;
|
|
57
|
+
rpcName?: string;
|
|
58
|
+
data?: any;
|
|
59
|
+
}>>;
|
|
55
60
|
updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
|
|
56
61
|
protected abstract updateRunStatusImpl(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
|
|
57
62
|
insertStepState(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: WorkflowStepOptions, fromStepName?: string): Promise<StepState>;
|
|
@@ -212,6 +217,7 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
212
217
|
runWorkflowJob(runId: string, rpcService: PikkuRPC): Promise<void>;
|
|
213
218
|
private runWorkflowJobInner;
|
|
214
219
|
private onChildWorkflowCompleted;
|
|
220
|
+
notifyChildWorkflowFailed(childRun: WorkflowRun, error: Error): Promise<void>;
|
|
215
221
|
protected onChildWorkflowFailed(childRun: WorkflowRun, error: Error): Promise<void>;
|
|
216
222
|
executeWorkflowStep(runId: string, stepName: string, rpcName: string, data: any, rpcService: PikkuRPC): Promise<void>;
|
|
217
223
|
/**
|
|
@@ -245,6 +251,13 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
245
251
|
approveStep(runId: string, reason: string, decision: unknown, session?: CoreUserSession): Promise<void>;
|
|
246
252
|
private approvalStep;
|
|
247
253
|
createWorkflowWire(name: string, runId: string, rpcService: PikkuRPC, addonNamespace?: string | null): PikkuWorkflowWire;
|
|
254
|
+
private compensationApi;
|
|
255
|
+
failOrUnwind(run: WorkflowRun, error: Error & {
|
|
256
|
+
code?: string;
|
|
257
|
+
}, rpcService: PikkuRPC): Promise<'unwinding' | 'finished' | 'failed'>;
|
|
258
|
+
/** Stops an unfinished run, children first, and unwinds what it completed. */
|
|
259
|
+
cancelRun(runId: string, rpcService: PikkuRPC, reason?: string): Promise<WorkflowStatus | undefined>;
|
|
260
|
+
protected markFailedUnlessUnwound(runId: string, error: SerializedError): Promise<void>;
|
|
248
261
|
protected verifyStepName(stepName: string): void;
|
|
249
262
|
private getConfig;
|
|
250
263
|
protected getOrchestratorQueueName(workflowName?: string): string;
|