@pikku/core 0.12.38 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/errors/error-handler.d.ts +8 -0
  3. package/dist/errors/error-handler.js +8 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/services/in-memory-queue-service.d.ts +9 -0
  7. package/dist/services/in-memory-queue-service.js +42 -11
  8. package/dist/services/in-memory-workflow-service.js +2 -0
  9. package/dist/services/workflow-service.d.ts +3 -0
  10. package/dist/testing/service-tests.js +35 -0
  11. package/dist/types/core.types.d.ts +1 -0
  12. package/dist/wirings/cli/cli-runner.js +12 -3
  13. package/dist/wirings/workflow/graph/graph-runner.js +51 -61
  14. package/dist/wirings/workflow/index.d.ts +2 -0
  15. package/dist/wirings/workflow/index.js +2 -0
  16. package/dist/wirings/workflow/pikku-workflow-service.d.ts +32 -0
  17. package/dist/wirings/workflow/pikku-workflow-service.js +136 -119
  18. package/dist/wirings/workflow/run-timeline.d.ts +85 -0
  19. package/dist/wirings/workflow/run-timeline.js +153 -0
  20. package/dist/wirings/workflow/workflow.types.d.ts +2 -0
  21. package/package.json +1 -1
  22. package/src/errors/error-handler.ts +10 -0
  23. package/src/index.ts +1 -0
  24. package/src/services/in-memory-queue-service.test.ts +97 -0
  25. package/src/services/in-memory-queue-service.ts +51 -16
  26. package/src/services/in-memory-workflow-service.ts +2 -0
  27. package/src/services/workflow-service.ts +9 -0
  28. package/src/testing/service-tests.ts +65 -0
  29. package/src/types/core.types.ts +4 -0
  30. package/src/wirings/cli/cli-runner.ts +11 -3
  31. package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
  32. package/src/wirings/workflow/graph/graph-runner.ts +95 -86
  33. package/src/wirings/workflow/index.ts +14 -0
  34. package/src/wirings/workflow/pikku-workflow-service.ts +193 -137
  35. package/src/wirings/workflow/run-timeline.test.ts +211 -0
  36. package/src/wirings/workflow/run-timeline.ts +241 -0
  37. package/src/wirings/workflow/workflow.types.ts +2 -0
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -565,14 +565,21 @@ export async function continueGraph(
565
565
  }
566
566
  }
567
567
 
568
- export async function executeGraphStep(
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(
569
576
  workflowService: PikkuWorkflowService,
570
577
  rpcService: any,
571
578
  runId: string,
572
579
  stepId: string,
573
580
  nodeId: string,
574
581
  rpcName: string,
575
- data: any,
582
+ input: any,
576
583
  graphName: string
577
584
  ): Promise<any> {
578
585
  const wireState: GraphWireState = {}
@@ -588,6 +595,28 @@ export async function executeGraphStep(
588
595
  getState: () => workflowService.getRunState(runId),
589
596
  }
590
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> {
591
620
  try {
592
621
  let result: any
593
622
 
@@ -622,18 +651,16 @@ export async function executeGraphStep(
622
651
  throw new ChildWorkflowStartedException(runId, stepId, childRunId)
623
652
  }
624
653
  } else {
625
- result = await rpcService.rpcWithWire(rpcName, data, {
626
- graph: graphWire,
627
- workflow: workflowService.createWorkflowWire(
628
- graphName,
629
- runId,
630
- rpcService
631
- ),
632
- })
633
- }
634
-
635
- if (wireState.branchKey) {
636
- 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
+ )
637
664
  }
638
665
 
639
666
  return result
@@ -705,37 +732,30 @@ async function executeGraphNodeInline(
705
732
  runId: string,
706
733
  graphName: string,
707
734
  nodeId: string,
735
+ instanceKey: string,
708
736
  input: any,
709
- nodes: Record<string, any>
737
+ nodes: Record<string, any>,
738
+ fromStepName?: string
710
739
  ): Promise<void> {
711
740
  const node = nodes[nodeId]
712
741
  if (!node) return
713
742
 
714
743
  const rpcName = node.rpcName
715
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.
716
748
  const stepState = await workflowService.insertStepState(
717
749
  runId,
718
- nodeId,
750
+ instanceKey,
719
751
  rpcName,
720
752
  input,
721
- { retries: node.retries ?? 0, retryDelay: node.retryDelay }
753
+ { retries: node.retries ?? 0, retryDelay: node.retryDelay },
754
+ fromStepName
722
755
  )
723
756
 
724
757
  await workflowService.setStepRunning(stepState.stepId)
725
758
 
726
- const wireState: GraphWireState = {}
727
- const graphWire: PikkuGraphWire = {
728
- runId,
729
- graphName,
730
- nodeId,
731
- branch: (key: string) => {
732
- wireState.branchKey = key
733
- },
734
- setState: (name: string, value: unknown) =>
735
- workflowService.updateRunState(runId, name, value),
736
- getState: () => workflowService.getRunState(runId),
737
- }
738
-
739
759
  try {
740
760
  let result: any
741
761
 
@@ -764,20 +784,15 @@ async function executeGraphNodeInline(
764
784
  }
765
785
  result = childRun?.output
766
786
  } else {
767
- result = await rpcService.rpcWithWire(rpcName, input, {
768
- graph: graphWire,
769
- workflow: workflowService.createWorkflowWire(
770
- graphName,
771
- runId,
772
- rpcService
773
- ),
774
- })
775
- }
776
-
777
- if (wireState.branchKey) {
778
- await workflowService.setBranchTaken(
787
+ result = await invokeGraphNodeRpc(
788
+ workflowService,
789
+ rpcService,
790
+ runId,
779
791
  stepState.stepId,
780
- wireState.branchKey
792
+ nodeId,
793
+ rpcName,
794
+ input,
795
+ graphName
781
796
  )
782
797
  }
783
798
 
@@ -811,8 +826,10 @@ async function executeGraphNodeInline(
811
826
  runId,
812
827
  graphName,
813
828
  errorNodeId,
829
+ errorNodeId,
814
830
  { error: { message: (error as Error).message } },
815
- nodes
831
+ nodes,
832
+ nodeId
816
833
  )
817
834
  )
818
835
  )
@@ -831,20 +848,22 @@ async function continueGraphInline(
831
848
  triggerInput: any,
832
849
  entryNodeIds: string[]
833
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.
834
856
  while (true) {
835
857
  const {
836
- completedNodeIds: rawCompleted,
837
858
  failedNodeIds: rawFailed,
838
- branchKeys: rawBranch,
859
+ branchKeys: branchByStep,
860
+ completedNodeIds: rawCompleted,
839
861
  } = await workflowService.getCompletedGraphState(runId)
840
- const completedNodeIds = remapStepNamesToNodeIds(
841
- rawCompleted,
842
- nodes,
843
- graphName
844
- )
845
- 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)
846
866
  const failedNodeIds = remapStepNamesToNodeIds(rawFailed, nodes, graphName)
847
- const branchKeys = remapBranchKeys(rawBranch, nodes, graphName)
848
867
 
849
868
  if (failedNodeIds.length > 0) {
850
869
  const failedNode = failedNodeIds[0]!
@@ -856,46 +875,31 @@ async function continueGraphInline(
856
875
  return
857
876
  }
858
877
 
859
- const candidateNodes = new Set<string>()
860
-
861
- for (const nodeId of completedNodeIds) {
862
- const node = nodes[nodeId]
863
- if (!node?.next) continue
864
-
865
- const nextNodes = resolveNextFromConfig(node.next, branchKeys[nodeId])
866
- for (const nextNode of nextNodes) {
867
- candidateNodes.add(nextNode)
868
- }
869
- }
870
-
871
- for (const entryId of entryNodeIds) {
872
- candidateNodes.add(entryId)
873
- }
874
-
875
- if (candidateNodes.size === 0 && completedNodeIds.length > 0) {
876
- await workflowService.updateRunStatus(runId, 'completed')
878
+ const run = await workflowService.getRun(runId)
879
+ if (run?.status === 'suspended') {
877
880
  return
878
881
  }
879
882
 
880
- const unstartedNodes = await workflowService.getNodesWithoutSteps(runId, [
881
- ...candidateNodes,
882
- ])
883
-
884
- const nodesToExecute = unstartedNodes.filter((nodeId) => {
885
- const node = nodes[nodeId]
886
- return node && areDependenciesSatisfied(node, completedNodeIdSet)
887
- })
883
+ const instances = await workflowService.getStepInstances(runId)
884
+ const plan = planGraphTransitions(
885
+ nodes,
886
+ instances,
887
+ branchByStep,
888
+ entryNodeIds,
889
+ graphName
890
+ )
888
891
 
889
- if (nodesToExecute.length === 0) {
890
- if (completedNodeIds.length > 0 && unstartedNodes.length === 0) {
892
+ if (plan.toFire.length === 0) {
893
+ if (!plan.hasInFlight && !plan.blockedWaiting) {
891
894
  await workflowService.updateRunStatus(runId, 'completed')
892
895
  }
893
896
  return
894
897
  }
895
898
 
899
+ let executed = 0
896
900
  await Promise.all(
897
- nodesToExecute.map(async (nodeId) => {
898
- const node = nodes[nodeId]
901
+ plan.toFire.map(async (fire) => {
902
+ const node = nodes[fire.logical]
899
903
  if (!node?.rpcName) return
900
904
 
901
905
  const referencedNodeIds = extractReferencedNodeIds(node.input).filter(
@@ -905,21 +909,25 @@ async function continueGraphInline(
905
909
  runId,
906
910
  referencedNodeIds
907
911
  )
908
-
909
912
  const nodeResults = { trigger: triggerInput, ...fetchedResults }
910
913
  const resolvedInput = resolveSerializedInput(node.input, nodeResults)
911
914
 
915
+ executed++
912
916
  await executeGraphNodeInline(
913
917
  workflowService,
914
918
  rpcService,
915
919
  runId,
916
920
  graphName,
917
- nodeId,
921
+ fire.logical,
922
+ fire.instanceKey,
918
923
  resolvedInput,
919
- nodes
924
+ nodes,
925
+ fire.fromStepName
920
926
  )
921
927
  })
922
928
  )
929
+ // Nothing executable fired (e.g. nodes without an rpcName) → can't progress.
930
+ if (executed === 0) return
923
931
  }
924
932
  }
925
933
 
@@ -1002,6 +1010,7 @@ export async function runWorkflowGraph(
1002
1010
  runId,
1003
1011
  graphName,
1004
1012
  nodeId,
1013
+ nodeId,
1005
1014
  resolvedInput,
1006
1015
  nodes
1007
1016
  )
@@ -12,6 +12,20 @@ export {
12
12
  } from './pikku-workflow-service.js'
13
13
  export { deriveInvocationId, uuidv5 } from './workflow-invocation-id.js'
14
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'
28
+
15
29
  // Internal registration functions (used by generated code)
16
30
  export { addWorkflow } from './dsl/workflow-runner.js'
17
31