@pikku/core 0.12.37 → 0.12.39
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 +113 -0
- package/dist/errors/error-handler.d.ts +8 -0
- package/dist/errors/error-handler.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/in-memory-queue-service.d.ts +9 -0
- package/dist/services/in-memory-queue-service.js +42 -11
- package/dist/services/in-memory-workflow-service.d.ts +7 -2
- package/dist/services/in-memory-workflow-service.js +19 -1
- package/dist/services/workflow-service.d.ts +3 -0
- package/dist/testing/service-tests.js +35 -0
- package/dist/types/core.types.d.ts +1 -0
- package/dist/wirings/cli/cli-runner.js +12 -3
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +19 -1
- package/dist/wirings/workflow/graph/graph-runner.js +168 -106
- package/dist/wirings/workflow/index.d.ts +4 -1
- package/dist/wirings/workflow/index.js +4 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +95 -5
- package/dist/wirings/workflow/pikku-workflow-service.js +323 -175
- package/dist/wirings/workflow/run-timeline.d.ts +85 -0
- package/dist/wirings/workflow/run-timeline.js +153 -0
- package/dist/wirings/workflow/workflow-invocation-id.d.ts +20 -0
- package/dist/wirings/workflow/workflow-invocation-id.js +38 -0
- package/dist/wirings/workflow/workflow-queue-workers.d.ts +2 -0
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/package.json +2 -1
- package/src/dev/hot-reload.test.ts +5 -0
- package/src/errors/error-handler.ts +10 -0
- package/src/index.ts +1 -0
- package/src/services/in-memory-queue-service.test.ts +97 -0
- package/src/services/in-memory-queue-service.ts +51 -16
- package/src/services/in-memory-workflow-service.ts +27 -1
- package/src/services/workflow-service.ts +9 -0
- package/src/testing/service-tests.ts +65 -0
- package/src/types/core.types.ts +4 -0
- package/src/wirings/cli/cli-runner.ts +11 -3
- package/src/wirings/oauth2/oauth2-client.test.ts +25 -23
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +19 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +159 -3
- package/src/wirings/workflow/graph/graph-runner.ts +253 -142
- package/src/wirings/workflow/index.ts +17 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +447 -213
- package/src/wirings/workflow/run-timeline.test.ts +211 -0
- package/src/wirings/workflow/run-timeline.ts +241 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +117 -0
- package/src/wirings/workflow/workflow-invocation-id.test.ts +53 -0
- package/src/wirings/workflow/workflow-invocation-id.ts +48 -0
- package/src/wirings/workflow/workflow-queue-workers.ts +2 -0
- package/src/wirings/workflow/workflow-retry-policy.test.ts +111 -0
- package/src/wirings/workflow/workflow-step-ordinal.test.ts +79 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type PikkuWorkflowService,
|
|
3
3
|
WorkflowAsyncException,
|
|
4
4
|
WorkflowSuspendedException,
|
|
5
|
+
DEFAULT_STEP_RETRIES,
|
|
5
6
|
} from '../pikku-workflow-service.js'
|
|
6
7
|
import type { GraphWireState, PikkuGraphWire } from './workflow-graph.types.js'
|
|
7
8
|
import { pikkuState, getSingletonServices } from '../../../pikku-state.js'
|
|
@@ -28,6 +29,13 @@ function buildTemplateRegex(nodeId: string): RegExp | null {
|
|
|
28
29
|
return new RegExp(`^${escaped}$`)
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** Strip a trailing revisit ordinal (`node#2` → `node`); leaves other names as-is. */
|
|
33
|
+
function stripInstanceOrdinal(name: string): string {
|
|
34
|
+
const hash = name.lastIndexOf('#')
|
|
35
|
+
if (hash <= 0) return name
|
|
36
|
+
return /^\d+$/.test(name.slice(hash + 1)) ? name.slice(0, hash) : name
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
function remapStepNamesToNodeIds(
|
|
32
40
|
stepNames: string[],
|
|
33
41
|
nodes: Record<string, any>,
|
|
@@ -38,12 +46,14 @@ function remapStepNamesToNodeIds(
|
|
|
38
46
|
const regex = buildTemplateRegex(nodeId)
|
|
39
47
|
if (regex) templatePatterns.set(nodeId, regex)
|
|
40
48
|
}
|
|
41
|
-
if (templatePatterns.size === 0) return stepNames
|
|
42
49
|
return stepNames.map((name) => {
|
|
43
50
|
if (nodes[name]) return name
|
|
51
|
+
// Revisit instance (`node#N`) maps to its logical node.
|
|
52
|
+
const base = stripInstanceOrdinal(name)
|
|
53
|
+
if (base !== name && nodes[base]) return base
|
|
44
54
|
const matches: string[] = []
|
|
45
55
|
for (const [nodeId, regex] of templatePatterns) {
|
|
46
|
-
if (regex.test(
|
|
56
|
+
if (regex.test(base)) matches.push(nodeId)
|
|
47
57
|
}
|
|
48
58
|
if (matches.length > 1) {
|
|
49
59
|
throw new Error(
|
|
@@ -57,6 +67,122 @@ function remapStepNamesToNodeIds(
|
|
|
57
67
|
})
|
|
58
68
|
}
|
|
59
69
|
|
|
70
|
+
const ENTRY_FROM = '__entry__'
|
|
71
|
+
|
|
72
|
+
/** Whether `target` can reach `source` over `next` edges — i.e. an edge
|
|
73
|
+
* source→target closes a cycle (a back-edge), vs a plain forward edge. */
|
|
74
|
+
function closesCycle(
|
|
75
|
+
source: string,
|
|
76
|
+
target: string,
|
|
77
|
+
nodes: Record<string, any>
|
|
78
|
+
): boolean {
|
|
79
|
+
const seen = new Set<string>()
|
|
80
|
+
const stack = [target]
|
|
81
|
+
while (stack.length) {
|
|
82
|
+
const cur = stack.pop()!
|
|
83
|
+
if (cur === source) return true
|
|
84
|
+
if (seen.has(cur)) continue
|
|
85
|
+
seen.add(cur)
|
|
86
|
+
for (const next of normalizeNodeTargets(nodes[cur]?.next)) stack.push(next)
|
|
87
|
+
}
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Decide which next steps to fire this tick. Two kinds of edge:
|
|
93
|
+
* - forward edge → node-once: fire the target only if it has no instance yet,
|
|
94
|
+
* so converging edges (joins) collapse to a single run (unchanged behavior).
|
|
95
|
+
* - back-edge (target can reach the source, closing a cycle) → revisit: fire a
|
|
96
|
+
* fresh ordinal instance (`target#1`, …), edge-once on `from → target` so it
|
|
97
|
+
* doesn't re-fire every tick. Cycles terminate when branch routing stops
|
|
98
|
+
* looping back; a node always records the predecessor it was reached from.
|
|
99
|
+
*/
|
|
100
|
+
function planGraphTransitions(
|
|
101
|
+
nodes: Record<string, any>,
|
|
102
|
+
instances: Array<{ stepName: string; status: string; fromStepName?: string }>,
|
|
103
|
+
branchByStep: Record<string, string>,
|
|
104
|
+
entryNodeIds: string[],
|
|
105
|
+
graphName: string
|
|
106
|
+
): {
|
|
107
|
+
toFire: Array<{ logical: string; instanceKey: string; fromStepName?: string }>
|
|
108
|
+
hasInFlight: boolean
|
|
109
|
+
blockedWaiting: boolean
|
|
110
|
+
} {
|
|
111
|
+
const toLogical = (name: string) =>
|
|
112
|
+
remapStepNamesToNodeIds([name], nodes, graphName)[0]!
|
|
113
|
+
|
|
114
|
+
const countByLogical: Record<string, number> = {}
|
|
115
|
+
const consumed = new Set<string>()
|
|
116
|
+
for (const inst of instances) {
|
|
117
|
+
const logical = toLogical(inst.stepName)
|
|
118
|
+
countByLogical[logical] = (countByLogical[logical] ?? 0) + 1
|
|
119
|
+
consumed.add(`${inst.fromStepName ?? ENTRY_FROM}->${logical}`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const completed = instances.filter((i) => i.status === 'succeeded')
|
|
123
|
+
const completedLogical = new Set(completed.map((i) => toLogical(i.stepName)))
|
|
124
|
+
|
|
125
|
+
// Available edges: entry edges + each completed instance's resolved `next`.
|
|
126
|
+
const edges: Array<{
|
|
127
|
+
from?: string
|
|
128
|
+
fromKey: string
|
|
129
|
+
fromLogical?: string
|
|
130
|
+
target: string
|
|
131
|
+
}> = []
|
|
132
|
+
for (const entryId of entryNodeIds) {
|
|
133
|
+
edges.push({ fromKey: ENTRY_FROM, target: entryId })
|
|
134
|
+
}
|
|
135
|
+
for (const inst of completed) {
|
|
136
|
+
const fromLogical = toLogical(inst.stepName)
|
|
137
|
+
const node = nodes[fromLogical]
|
|
138
|
+
if (!node?.next) continue
|
|
139
|
+
for (const target of resolveNextFromConfig(
|
|
140
|
+
node.next,
|
|
141
|
+
branchByStep[inst.stepName]
|
|
142
|
+
)) {
|
|
143
|
+
edges.push({ from: inst.stepName, fromKey: inst.stepName, fromLogical, target })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const toFire: Array<{
|
|
148
|
+
logical: string
|
|
149
|
+
instanceKey: string
|
|
150
|
+
fromStepName?: string
|
|
151
|
+
}> = []
|
|
152
|
+
let blockedWaiting = false
|
|
153
|
+
for (const edge of edges) {
|
|
154
|
+
const target = edge.target
|
|
155
|
+
const edgeKey = `${edge.fromKey}->${target}`
|
|
156
|
+
if (consumed.has(edgeKey)) continue
|
|
157
|
+
const visits = countByLogical[target] ?? 0
|
|
158
|
+
const isBackEdge =
|
|
159
|
+
edge.fromLogical !== undefined &&
|
|
160
|
+
closesCycle(edge.fromLogical, target, nodes)
|
|
161
|
+
// Forward edge into an already-started node = a join; node-once.
|
|
162
|
+
if (!isBackEdge && visits > 0) {
|
|
163
|
+
consumed.add(edgeKey)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
if (!areDependenciesSatisfied(nodes[target] ?? {}, completedLogical)) {
|
|
167
|
+
blockedWaiting = true
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
toFire.push({
|
|
171
|
+
logical: target,
|
|
172
|
+
instanceKey: visits === 0 ? target : `${target}#${visits}`,
|
|
173
|
+
fromStepName: edge.from,
|
|
174
|
+
})
|
|
175
|
+
countByLogical[target] = visits + 1
|
|
176
|
+
consumed.add(edgeKey)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
toFire,
|
|
181
|
+
hasInFlight: instances.some((i) => i.status !== 'succeeded'),
|
|
182
|
+
blockedWaiting,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
60
186
|
function remapBranchKeys(
|
|
61
187
|
branchKeys: Record<string, string>,
|
|
62
188
|
nodes: Record<string, any>,
|
|
@@ -325,10 +451,13 @@ async function queueGraphNode(
|
|
|
325
451
|
nodeId: string,
|
|
326
452
|
rpcName: string,
|
|
327
453
|
input: any,
|
|
328
|
-
nodeConfig?: { retries?: number; retryDelay?: string | number }
|
|
454
|
+
nodeConfig?: { retries?: number; retryDelay?: string | number },
|
|
455
|
+
fromStepName?: string
|
|
329
456
|
): Promise<void> {
|
|
457
|
+
// Default to the workflow-wide retry policy when the node sets none, so the
|
|
458
|
+
// persisted step retries match the queue `attempts` (see resolveStepJobOptions).
|
|
330
459
|
const stepOptions = {
|
|
331
|
-
retries: nodeConfig?.retries ??
|
|
460
|
+
retries: nodeConfig?.retries ?? DEFAULT_STEP_RETRIES,
|
|
332
461
|
retryDelay: nodeConfig?.retryDelay,
|
|
333
462
|
}
|
|
334
463
|
await workflowService.insertStepState(
|
|
@@ -336,14 +465,16 @@ async function queueGraphNode(
|
|
|
336
465
|
nodeId,
|
|
337
466
|
rpcName,
|
|
338
467
|
input,
|
|
339
|
-
stepOptions
|
|
468
|
+
stepOptions,
|
|
469
|
+
fromStepName
|
|
340
470
|
)
|
|
341
471
|
await workflowService.queueStepWorker(
|
|
342
472
|
runId,
|
|
343
473
|
nodeId,
|
|
344
474
|
rpcName,
|
|
345
475
|
input,
|
|
346
|
-
stepOptions
|
|
476
|
+
stepOptions,
|
|
477
|
+
fromStepName
|
|
347
478
|
)
|
|
348
479
|
}
|
|
349
480
|
|
|
@@ -364,16 +495,13 @@ export async function continueGraph(
|
|
|
364
495
|
const {
|
|
365
496
|
completedNodeIds: rawCompleted,
|
|
366
497
|
failedNodeIds: rawFailed,
|
|
367
|
-
branchKeys:
|
|
498
|
+
branchKeys: branchByStep,
|
|
368
499
|
} = await workflowService.getCompletedGraphState(runId)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)
|
|
374
|
-
const completedNodeIdSet = new Set(completedNodeIds)
|
|
500
|
+
// Validate step/branch names map to unambiguous nodes (planning keys
|
|
501
|
+
// physically; these calls only surface ambiguous template configs).
|
|
502
|
+
remapStepNamesToNodeIds(rawCompleted, nodes, graphName)
|
|
503
|
+
remapBranchKeys(branchByStep, nodes, graphName)
|
|
375
504
|
const failedNodeIds = remapStepNamesToNodeIds(rawFailed, nodes, graphName)
|
|
376
|
-
const branchKeys = remapBranchKeys(rawBranch, nodes, graphName)
|
|
377
505
|
|
|
378
506
|
if (failedNodeIds.length > 0) {
|
|
379
507
|
const failedNode = failedNodeIds[0]!
|
|
@@ -390,44 +518,18 @@ export async function continueGraph(
|
|
|
390
518
|
return
|
|
391
519
|
}
|
|
392
520
|
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
candidateNodes.add(nextNode)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
for (const entryId of meta.entryNodeIds ?? []) {
|
|
406
|
-
candidateNodes.add(entryId)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (candidateNodes.size === 0 && completedNodeIds.length > 0) {
|
|
410
|
-
await workflowService.updateRunStatus(runId, 'completed')
|
|
411
|
-
return
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const unstartedNodes = await workflowService.getNodesWithoutSteps(runId, [
|
|
415
|
-
...candidateNodes,
|
|
416
|
-
])
|
|
417
|
-
|
|
418
|
-
const nodesToQueue = unstartedNodes.filter((nodeId) => {
|
|
419
|
-
const node = nodes[nodeId]
|
|
420
|
-
return node && areDependenciesSatisfied(node, completedNodeIdSet)
|
|
421
|
-
})
|
|
521
|
+
const instances = await workflowService.getStepInstances(runId)
|
|
522
|
+
const plan = planGraphTransitions(
|
|
523
|
+
nodes,
|
|
524
|
+
instances,
|
|
525
|
+
branchByStep,
|
|
526
|
+
meta.entryNodeIds ?? [],
|
|
527
|
+
graphName
|
|
528
|
+
)
|
|
422
529
|
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
.map(([id]) => id)
|
|
427
|
-
const allRpcCompleted = allRpcNodes.every((id) =>
|
|
428
|
-
completedNodeIdSet.has(id)
|
|
429
|
-
)
|
|
430
|
-
if (allRpcCompleted) {
|
|
530
|
+
if (plan.toFire.length === 0) {
|
|
531
|
+
// Nothing left to fire and nothing running/blocked → the run is done.
|
|
532
|
+
if (!plan.hasInFlight && !plan.blockedWaiting) {
|
|
431
533
|
await workflowService.updateRunStatus(runId, 'completed')
|
|
432
534
|
}
|
|
433
535
|
return
|
|
@@ -436,8 +538,8 @@ export async function continueGraph(
|
|
|
436
538
|
const run = await workflowService.getRun(runId)
|
|
437
539
|
const triggerInput = run?.input
|
|
438
540
|
|
|
439
|
-
for (const
|
|
440
|
-
const node = nodes[
|
|
541
|
+
for (const fire of plan.toFire) {
|
|
542
|
+
const node = nodes[fire.logical]
|
|
441
543
|
if (!node?.rpcName) continue
|
|
442
544
|
|
|
443
545
|
const referencedNodeIds = extractReferencedNodeIds(node.input).filter(
|
|
@@ -447,7 +549,6 @@ export async function continueGraph(
|
|
|
447
549
|
runId,
|
|
448
550
|
referencedNodeIds
|
|
449
551
|
)
|
|
450
|
-
|
|
451
552
|
const nodeResults = { trigger: triggerInput, ...fetchedResults }
|
|
452
553
|
const resolvedInput = resolveSerializedInput(node.input, nodeResults)
|
|
453
554
|
|
|
@@ -455,22 +556,30 @@ export async function continueGraph(
|
|
|
455
556
|
workflowService,
|
|
456
557
|
runId,
|
|
457
558
|
graphName,
|
|
458
|
-
|
|
559
|
+
fire.instanceKey,
|
|
459
560
|
node.rpcName,
|
|
460
561
|
resolvedInput,
|
|
461
|
-
node
|
|
562
|
+
node,
|
|
563
|
+
fire.fromStepName
|
|
462
564
|
)
|
|
463
565
|
}
|
|
464
566
|
}
|
|
465
567
|
|
|
466
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Invoke a graph node's RPC with the graph + workflow wires, capturing any
|
|
570
|
+
* branch the node selects and persisting it. Shared by the queued
|
|
571
|
+
* (executeGraphStep) and inline (executeGraphNodeInline) executors so both build
|
|
572
|
+
* the wire and record the branch identically — only their child-workflow and
|
|
573
|
+
* onError handling differs around this call.
|
|
574
|
+
*/
|
|
575
|
+
async function invokeGraphNodeRpc(
|
|
467
576
|
workflowService: PikkuWorkflowService,
|
|
468
577
|
rpcService: any,
|
|
469
578
|
runId: string,
|
|
470
579
|
stepId: string,
|
|
471
580
|
nodeId: string,
|
|
472
581
|
rpcName: string,
|
|
473
|
-
|
|
582
|
+
input: any,
|
|
474
583
|
graphName: string
|
|
475
584
|
): Promise<any> {
|
|
476
585
|
const wireState: GraphWireState = {}
|
|
@@ -486,6 +595,28 @@ export async function executeGraphStep(
|
|
|
486
595
|
getState: () => workflowService.getRunState(runId),
|
|
487
596
|
}
|
|
488
597
|
|
|
598
|
+
const result = await rpcService.rpcWithWire(rpcName, input, {
|
|
599
|
+
graph: graphWire,
|
|
600
|
+
workflow: workflowService.createWorkflowWire(graphName, runId, rpcService),
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
if (wireState.branchKey) {
|
|
604
|
+
await workflowService.setBranchTaken(stepId, wireState.branchKey)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return result
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export async function executeGraphStep(
|
|
611
|
+
workflowService: PikkuWorkflowService,
|
|
612
|
+
rpcService: any,
|
|
613
|
+
runId: string,
|
|
614
|
+
stepId: string,
|
|
615
|
+
nodeId: string,
|
|
616
|
+
rpcName: string,
|
|
617
|
+
data: any,
|
|
618
|
+
graphName: string
|
|
619
|
+
): Promise<any> {
|
|
489
620
|
try {
|
|
490
621
|
let result: any
|
|
491
622
|
|
|
@@ -520,18 +651,16 @@ export async function executeGraphStep(
|
|
|
520
651
|
throw new ChildWorkflowStartedException(runId, stepId, childRunId)
|
|
521
652
|
}
|
|
522
653
|
} else {
|
|
523
|
-
result = await
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (wireState.branchKey) {
|
|
534
|
-
await workflowService.setBranchTaken(stepId, wireState.branchKey)
|
|
654
|
+
result = await invokeGraphNodeRpc(
|
|
655
|
+
workflowService,
|
|
656
|
+
rpcService,
|
|
657
|
+
runId,
|
|
658
|
+
stepId,
|
|
659
|
+
nodeId,
|
|
660
|
+
rpcName,
|
|
661
|
+
data,
|
|
662
|
+
graphName
|
|
663
|
+
)
|
|
535
664
|
}
|
|
536
665
|
|
|
537
666
|
return result
|
|
@@ -603,37 +732,30 @@ async function executeGraphNodeInline(
|
|
|
603
732
|
runId: string,
|
|
604
733
|
graphName: string,
|
|
605
734
|
nodeId: string,
|
|
735
|
+
instanceKey: string,
|
|
606
736
|
input: any,
|
|
607
|
-
nodes: Record<string, any
|
|
737
|
+
nodes: Record<string, any>,
|
|
738
|
+
fromStepName?: string
|
|
608
739
|
): Promise<void> {
|
|
609
740
|
const node = nodes[nodeId]
|
|
610
741
|
if (!node) return
|
|
611
742
|
|
|
612
743
|
const rpcName = node.rpcName
|
|
613
744
|
|
|
745
|
+
// Persist under the physical instance key (node, node#1 … for revisits) and
|
|
746
|
+
// record the predecessor — same as the queued path (queueGraphNode), so an
|
|
747
|
+
// inline graph run stores identical step rows + provenance.
|
|
614
748
|
const stepState = await workflowService.insertStepState(
|
|
615
749
|
runId,
|
|
616
|
-
|
|
750
|
+
instanceKey,
|
|
617
751
|
rpcName,
|
|
618
752
|
input,
|
|
619
|
-
{ retries: node.retries ?? 0, retryDelay: node.retryDelay }
|
|
753
|
+
{ retries: node.retries ?? 0, retryDelay: node.retryDelay },
|
|
754
|
+
fromStepName
|
|
620
755
|
)
|
|
621
756
|
|
|
622
757
|
await workflowService.setStepRunning(stepState.stepId)
|
|
623
758
|
|
|
624
|
-
const wireState: GraphWireState = {}
|
|
625
|
-
const graphWire: PikkuGraphWire = {
|
|
626
|
-
runId,
|
|
627
|
-
graphName,
|
|
628
|
-
nodeId,
|
|
629
|
-
branch: (key: string) => {
|
|
630
|
-
wireState.branchKey = key
|
|
631
|
-
},
|
|
632
|
-
setState: (name: string, value: unknown) =>
|
|
633
|
-
workflowService.updateRunState(runId, name, value),
|
|
634
|
-
getState: () => workflowService.getRunState(runId),
|
|
635
|
-
}
|
|
636
|
-
|
|
637
759
|
try {
|
|
638
760
|
let result: any
|
|
639
761
|
|
|
@@ -662,20 +784,15 @@ async function executeGraphNodeInline(
|
|
|
662
784
|
}
|
|
663
785
|
result = childRun?.output
|
|
664
786
|
} else {
|
|
665
|
-
result = await
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
runId,
|
|
670
|
-
rpcService
|
|
671
|
-
),
|
|
672
|
-
})
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (wireState.branchKey) {
|
|
676
|
-
await workflowService.setBranchTaken(
|
|
787
|
+
result = await invokeGraphNodeRpc(
|
|
788
|
+
workflowService,
|
|
789
|
+
rpcService,
|
|
790
|
+
runId,
|
|
677
791
|
stepState.stepId,
|
|
678
|
-
|
|
792
|
+
nodeId,
|
|
793
|
+
rpcName,
|
|
794
|
+
input,
|
|
795
|
+
graphName
|
|
679
796
|
)
|
|
680
797
|
}
|
|
681
798
|
|
|
@@ -709,8 +826,10 @@ async function executeGraphNodeInline(
|
|
|
709
826
|
runId,
|
|
710
827
|
graphName,
|
|
711
828
|
errorNodeId,
|
|
829
|
+
errorNodeId,
|
|
712
830
|
{ error: { message: (error as Error).message } },
|
|
713
|
-
nodes
|
|
831
|
+
nodes,
|
|
832
|
+
nodeId
|
|
714
833
|
)
|
|
715
834
|
)
|
|
716
835
|
)
|
|
@@ -729,20 +848,22 @@ async function continueGraphInline(
|
|
|
729
848
|
triggerInput: any,
|
|
730
849
|
entryNodeIds: string[]
|
|
731
850
|
): Promise<void> {
|
|
851
|
+
// Drive the run to completion in-process using the SAME planner as the queued
|
|
852
|
+
// path (continueGraph): each loop plans the next wave of transitions, executes
|
|
853
|
+
// them inline (vs queueGraphNode dispatch), then re-plans. Sharing the planner
|
|
854
|
+
// gives the inline path joins, cycle revisits and fromStepName provenance
|
|
855
|
+
// identical to the queue — instead of a second, weaker traversal.
|
|
732
856
|
while (true) {
|
|
733
857
|
const {
|
|
734
|
-
completedNodeIds: rawCompleted,
|
|
735
858
|
failedNodeIds: rawFailed,
|
|
736
|
-
branchKeys:
|
|
859
|
+
branchKeys: branchByStep,
|
|
860
|
+
completedNodeIds: rawCompleted,
|
|
737
861
|
} = await workflowService.getCompletedGraphState(runId)
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
)
|
|
743
|
-
const completedNodeIdSet = new Set(completedNodeIds)
|
|
862
|
+
// Validate step/branch names map to unambiguous nodes (planning keys
|
|
863
|
+
// physically; these calls only surface ambiguous template configs).
|
|
864
|
+
remapStepNamesToNodeIds(rawCompleted, nodes, graphName)
|
|
865
|
+
remapBranchKeys(branchByStep, nodes, graphName)
|
|
744
866
|
const failedNodeIds = remapStepNamesToNodeIds(rawFailed, nodes, graphName)
|
|
745
|
-
const branchKeys = remapBranchKeys(rawBranch, nodes, graphName)
|
|
746
867
|
|
|
747
868
|
if (failedNodeIds.length > 0) {
|
|
748
869
|
const failedNode = failedNodeIds[0]!
|
|
@@ -754,46 +875,31 @@ async function continueGraphInline(
|
|
|
754
875
|
return
|
|
755
876
|
}
|
|
756
877
|
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
for (const nodeId of completedNodeIds) {
|
|
760
|
-
const node = nodes[nodeId]
|
|
761
|
-
if (!node?.next) continue
|
|
762
|
-
|
|
763
|
-
const nextNodes = resolveNextFromConfig(node.next, branchKeys[nodeId])
|
|
764
|
-
for (const nextNode of nextNodes) {
|
|
765
|
-
candidateNodes.add(nextNode)
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
for (const entryId of entryNodeIds) {
|
|
770
|
-
candidateNodes.add(entryId)
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (candidateNodes.size === 0 && completedNodeIds.length > 0) {
|
|
774
|
-
await workflowService.updateRunStatus(runId, 'completed')
|
|
878
|
+
const run = await workflowService.getRun(runId)
|
|
879
|
+
if (run?.status === 'suspended') {
|
|
775
880
|
return
|
|
776
881
|
}
|
|
777
882
|
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
883
|
+
const instances = await workflowService.getStepInstances(runId)
|
|
884
|
+
const plan = planGraphTransitions(
|
|
885
|
+
nodes,
|
|
886
|
+
instances,
|
|
887
|
+
branchByStep,
|
|
888
|
+
entryNodeIds,
|
|
889
|
+
graphName
|
|
890
|
+
)
|
|
786
891
|
|
|
787
|
-
if (
|
|
788
|
-
if (
|
|
892
|
+
if (plan.toFire.length === 0) {
|
|
893
|
+
if (!plan.hasInFlight && !plan.blockedWaiting) {
|
|
789
894
|
await workflowService.updateRunStatus(runId, 'completed')
|
|
790
895
|
}
|
|
791
896
|
return
|
|
792
897
|
}
|
|
793
898
|
|
|
899
|
+
let executed = 0
|
|
794
900
|
await Promise.all(
|
|
795
|
-
|
|
796
|
-
const node = nodes[
|
|
901
|
+
plan.toFire.map(async (fire) => {
|
|
902
|
+
const node = nodes[fire.logical]
|
|
797
903
|
if (!node?.rpcName) return
|
|
798
904
|
|
|
799
905
|
const referencedNodeIds = extractReferencedNodeIds(node.input).filter(
|
|
@@ -803,21 +909,25 @@ async function continueGraphInline(
|
|
|
803
909
|
runId,
|
|
804
910
|
referencedNodeIds
|
|
805
911
|
)
|
|
806
|
-
|
|
807
912
|
const nodeResults = { trigger: triggerInput, ...fetchedResults }
|
|
808
913
|
const resolvedInput = resolveSerializedInput(node.input, nodeResults)
|
|
809
914
|
|
|
915
|
+
executed++
|
|
810
916
|
await executeGraphNodeInline(
|
|
811
917
|
workflowService,
|
|
812
918
|
rpcService,
|
|
813
919
|
runId,
|
|
814
920
|
graphName,
|
|
815
|
-
|
|
921
|
+
fire.logical,
|
|
922
|
+
fire.instanceKey,
|
|
816
923
|
resolvedInput,
|
|
817
|
-
nodes
|
|
924
|
+
nodes,
|
|
925
|
+
fire.fromStepName
|
|
818
926
|
)
|
|
819
927
|
})
|
|
820
928
|
)
|
|
929
|
+
// Nothing executable fired (e.g. nodes without an rpcName) → can't progress.
|
|
930
|
+
if (executed === 0) return
|
|
821
931
|
}
|
|
822
932
|
}
|
|
823
933
|
|
|
@@ -900,6 +1010,7 @@ export async function runWorkflowGraph(
|
|
|
900
1010
|
runId,
|
|
901
1011
|
graphName,
|
|
902
1012
|
nodeId,
|
|
1013
|
+
nodeId,
|
|
903
1014
|
resolvedInput,
|
|
904
1015
|
nodes
|
|
905
1016
|
)
|
|
@@ -5,9 +5,26 @@ export {
|
|
|
5
5
|
PikkuWorkflowService,
|
|
6
6
|
WorkflowCancelledException,
|
|
7
7
|
WorkflowSuspendedException,
|
|
8
|
+
WorkflowDispatchException,
|
|
8
9
|
WorkflowNotFoundError,
|
|
9
10
|
WorkflowRunNotFoundError,
|
|
11
|
+
DEFAULT_STEP_RETRIES,
|
|
10
12
|
} from './pikku-workflow-service.js'
|
|
13
|
+
export { deriveInvocationId, uuidv5 } from './workflow-invocation-id.js'
|
|
14
|
+
|
|
15
|
+
// Time-travel: reconstruct run state at any point from durable history
|
|
16
|
+
export {
|
|
17
|
+
buildRunTimeline,
|
|
18
|
+
reconstructStateAt,
|
|
19
|
+
reconstructFinalState,
|
|
20
|
+
} from './run-timeline.js'
|
|
21
|
+
export type {
|
|
22
|
+
RunTimeline,
|
|
23
|
+
RunTimelineEvent,
|
|
24
|
+
ReconstructedRunState,
|
|
25
|
+
ReconstructedStep,
|
|
26
|
+
RunPhase,
|
|
27
|
+
} from './run-timeline.js'
|
|
11
28
|
|
|
12
29
|
// Internal registration functions (used by generated code)
|
|
13
30
|
export { addWorkflow } from './dsl/workflow-runner.js'
|