@synergenius/flow-weaver 0.10.12 → 0.12.0

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.
@@ -109,6 +109,18 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
109
109
  lines.push(` const ctx = new GeneratedExecutionContext(${asyncArg}, __effectiveDebugger__, __abortSignal__);`);
110
110
  }
111
111
  lines.push('');
112
+ // Debug controller: step-through debugging and checkpoint/resume
113
+ // In production mode, no controller is emitted. In dev mode, the controller
114
+ // resolves from globalThis (injected by executor) or falls back to a no-op.
115
+ if (!production) {
116
+ lines.push(` // Debug controller for step-through debugging and checkpoint/resume`);
117
+ lines.push(` const __ctrl__: TDebugController = (`);
118
+ lines.push(` typeof globalThis !== 'undefined' && (globalThis as any).__fw_debug_controller__`);
119
+ lines.push(` ? (globalThis as any).__fw_debug_controller__`);
120
+ lines.push(` : { beforeNode: () => true, afterNode: () => {} }`);
121
+ lines.push(` );`);
122
+ lines.push('');
123
+ }
112
124
  lines.push(` const startIdx = ctx.addExecution('${RESERVED_NODE_NAMES.START}');`);
113
125
  Object.keys(extractStartPorts(workflow)).forEach((portName) => {
114
126
  const setCall = isAsync ? `await ctx.setVariable` : `ctx.setVariable`;
@@ -335,7 +347,7 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
335
347
  const group = parallelGroupOf.get(instanceId);
336
348
  const ungeneratedGroup = group.filter((id) => !generatedNodes.has(id));
337
349
  if (ungeneratedGroup.length >= 2) {
338
- generateParallelGroupWithContext(ungeneratedGroup, workflow, nodeTypes, availableVars, lines, generatedNodes, ' ', isAsync, 'ctx', bundleMode, branchingNodes);
350
+ generateParallelGroupWithContext(ungeneratedGroup, workflow, nodeTypes, availableVars, lines, generatedNodes, ' ', isAsync, 'ctx', bundleMode, branchingNodes, production);
339
351
  // Generate scoped children for each parallel node
340
352
  for (const parallelNodeId of ungeneratedGroup) {
341
353
  const inst = workflow.instances.find((i) => i.id === parallelNodeId);
@@ -344,7 +356,7 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
344
356
  const nt = nodeTypes.find((n) => n.name === inst.nodeType || n.functionName === inst.nodeType);
345
357
  if (!nt)
346
358
  continue;
347
- generateScopedChildrenExecution(inst, nt, workflow, nodeTypes, generatedNodes, availableVars, lines, ' ', branchingNodes, branchRegions, isAsync, bundleMode);
359
+ generateScopedChildrenExecution(inst, nt, workflow, nodeTypes, generatedNodes, availableVars, lines, ' ', branchingNodes, branchRegions, isAsync, bundleMode, production);
348
360
  }
349
361
  return;
350
362
  }
@@ -377,7 +389,7 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
377
389
  chainNeedsClose = true;
378
390
  }
379
391
  }
380
- generateBranchingChainCode(branchingChains.get(instanceId), workflow, nodeTypes, branchingNodes, branchRegions, availableVars, generatedNodes, lines, chainIndent, isAsync, 'ctx', bundleMode, branchingNodesNeedingSuccessFlag);
392
+ generateBranchingChainCode(branchingChains.get(instanceId), workflow, nodeTypes, branchingNodes, branchRegions, availableVars, generatedNodes, lines, chainIndent, isAsync, 'ctx', bundleMode, branchingNodesNeedingSuccessFlag, production);
381
393
  if (chainNeedsClose) {
382
394
  lines.push(` }`);
383
395
  }
@@ -404,7 +416,7 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
404
416
  branchNeedsClose = true;
405
417
  }
406
418
  }
407
- generateBranchingNodeCode(instance, nodeType, workflow, nodeTypes, branchRegions.get(instanceId), availableVars, generatedNodes, lines, branchIndent, false, branchingNodes, branchRegions, isAsync, 'ctx', bundleMode, new Set(), branchingNodesNeedingSuccessFlag.has(instanceId));
419
+ generateBranchingNodeCode(instance, nodeType, workflow, nodeTypes, branchRegions.get(instanceId), availableVars, generatedNodes, lines, branchIndent, false, branchingNodes, branchRegions, isAsync, 'ctx', bundleMode, new Set(), branchingNodesNeedingSuccessFlag.has(instanceId), production);
408
420
  if (branchNeedsClose) {
409
421
  lines.push(` }`);
410
422
  }
@@ -412,7 +424,7 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
412
424
  region.successNodes.forEach((n) => generatedNodes.add(n));
413
425
  region.failureNodes.forEach((n) => generatedNodes.add(n));
414
426
  // Check if this node creates a scope and generate scoped children
415
- generateScopedChildrenExecution(instance, nodeType, workflow, nodeTypes, generatedNodes, availableVars, lines, ' ', branchingNodes, branchRegions, isAsync, bundleMode);
427
+ generateScopedChildrenExecution(instance, nodeType, workflow, nodeTypes, generatedNodes, availableVars, lines, ' ', branchingNodes, branchRegions, isAsync, bundleMode, production);
416
428
  }
417
429
  else {
418
430
  const belongsToBranch = Array.from(branchRegions.values()).some((region) => region.successNodes.has(instanceId) || region.failureNodes.has(instanceId));
@@ -425,11 +437,11 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
425
437
  generateNodeCallWithContext(instance, nodeType, workflow, availableVars, lines, nodeTypes, ' ', isAsync, nodeUseConst, undefined, // instanceParent
426
438
  'ctx', // ctxVar
427
439
  bundleMode, false, // skipExecuteGuard
428
- branchingNodes // for port-aware STEP guards
429
- );
440
+ branchingNodes, // for port-aware STEP guards
441
+ production);
430
442
  generatedNodes.add(instanceId);
431
443
  // Check if this node creates a scope and generate scoped children
432
- generateScopedChildrenExecution(instance, nodeType, workflow, nodeTypes, generatedNodes, availableVars, lines, ' ', branchingNodes, branchRegions, isAsync, bundleMode);
444
+ generateScopedChildrenExecution(instance, nodeType, workflow, nodeTypes, generatedNodes, availableVars, lines, ' ', branchingNodes, branchRegions, isAsync, bundleMode, production);
433
445
  }
434
446
  }
435
447
  });
@@ -578,7 +590,7 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
578
590
  /**
579
591
  * Helper function to generate scoped children execution for nodes that create scopes
580
592
  */
581
- function generateScopedChildrenExecution(parentInstance, parentNodeType, workflow, allNodeTypes, generatedNodes, availableVars, lines, indent, branchingNodes, branchRegions, isAsync, bundleMode = false) {
593
+ function generateScopedChildrenExecution(parentInstance, parentNodeType, workflow, allNodeTypes, generatedNodes, availableVars, lines, indent, branchingNodes, branchRegions, isAsync, bundleMode = false, production = false) {
582
594
  // Check if this node creates a scope
583
595
  if (!parentNodeType.scope)
584
596
  return;
@@ -631,7 +643,7 @@ function generateScopedChildrenExecution(parentInstance, parentNodeType, workflo
631
643
  // Check if this child is a branching node
632
644
  if (branchingNodes.has(childInstanceId)) {
633
645
  generateBranchingNodeCode(childInstance, childNodeType, workflow, allNodeTypes, branchRegions.get(childInstanceId), availableVars, generatedNodes, lines, indent, false, branchingNodes, branchRegions, isAsync, scopedCtxVar, // Pass scoped context name
634
- bundleMode);
646
+ bundleMode, new Set(), false, production);
635
647
  const region = branchRegions.get(childInstanceId);
636
648
  region.successNodes.forEach((n) => generatedNodes.add(n));
637
649
  region.failureNodes.forEach((n) => generatedNodes.add(n));
@@ -640,7 +652,9 @@ function generateScopedChildrenExecution(parentInstance, parentNodeType, workflo
640
652
  generateNodeCallWithContext(childInstance, childNodeType, workflow, availableVars, lines, allNodeTypes, indent, isAsync, false, // useConst = false - scoped children need let (referenced outside scope block)
641
653
  parentInstance.id, // instanceParent - parent node is const, no ! needed when referencing it
642
654
  scopedCtxVar, // Pass scoped context name
643
- bundleMode);
655
+ bundleMode, false, // skipExecuteGuard
656
+ new Set(), // branchingNodes
657
+ production);
644
658
  }
645
659
  generatedNodes.add(childInstanceId);
646
660
  });
@@ -655,7 +669,7 @@ function generateScopedChildrenExecution(parentInstance, parentNodeType, workflo
655
669
  * Each node's execution code is wrapped in an async IIFE inside Promise.all.
656
670
  * The outer `let` variables for execution indices are assigned inside the IIFEs.
657
671
  */
658
- function generateParallelGroupWithContext(nodeIds, workflow, nodeTypes, availableVars, lines, generatedNodes, indent, isAsync, ctxVar, bundleMode, branchingNodes) {
672
+ function generateParallelGroupWithContext(nodeIds, workflow, nodeTypes, availableVars, lines, generatedNodes, indent, isAsync, ctxVar, bundleMode, branchingNodes, production = false) {
659
673
  // Collect code buffers for each node
660
674
  const nodeBuffers = [];
661
675
  for (const nodeId of nodeIds) {
@@ -668,7 +682,7 @@ function generateParallelGroupWithContext(nodeIds, workflow, nodeTypes, availabl
668
682
  const nodeLines = [];
669
683
  generateNodeCallWithContext(instance, nodeType, workflow, availableVars, nodeLines, nodeTypes, `${indent} `, // indent for inside the async IIFE
670
684
  isAsync, false, // useConst = false — outer let declarations
671
- undefined, ctxVar, bundleMode, false, branchingNodes);
685
+ undefined, ctxVar, bundleMode, false, branchingNodes, production);
672
686
  nodeBuffers.push({ id: nodeId, lines: nodeLines });
673
687
  }
674
688
  // Fallback: if only 0-1 nodes remain, emit directly without Promise.all
@@ -816,7 +830,7 @@ function buildStepSourceCondition(sourceNode, sourcePort, branchingNodes) {
816
830
  * if (A_success) { B code } else { CANCELLED for B,C and regions }
817
831
  * if (A_success && B_success) { C code } else { CANCELLED for C and regions }
818
832
  */
819
- function generateBranchingChainCode(chain, workflow, nodeTypes, branchingNodes, branchRegions, availableVars, generatedNodes, lines, indent, isAsync, ctxVar, bundleMode, forceTrackSuccessNodes = new Set()) {
833
+ function generateBranchingChainCode(chain, workflow, nodeTypes, branchingNodes, branchRegions, availableVars, generatedNodes, lines, indent, isAsync, ctxVar, bundleMode, forceTrackSuccessNodes = new Set(), production = false) {
820
834
  // Pre-declare success flags for all non-last chain nodes so they're
821
835
  // accessible across guard blocks (avoiding let-in-block scoping issues).
822
836
  // Also pre-declare for the last node if promoted nodes depend on its _success flag.
@@ -860,10 +874,10 @@ function generateBranchingChainCode(chain, workflow, nodeTypes, branchingNodes,
860
874
  lines.push(`${indent}if (${guardCondition}) {`);
861
875
  }
862
876
  const nodeIndent = hasGuard ? indent + ' ' : indent;
863
- generateBranchingNodeCode(instance, nodeType, workflow, nodeTypes, effectiveRegion, availableVars, generatedNodes, lines, nodeIndent, false, branchingNodes, branchRegions, isAsync, ctxVar, bundleMode, preDeclaredFlags, !isLast || forceTrackSuccessNodes.has(chain[i]) // forceTrackSuccess for non-last chain nodes or nodes with promoted dependents
864
- );
877
+ generateBranchingNodeCode(instance, nodeType, workflow, nodeTypes, effectiveRegion, availableVars, generatedNodes, lines, nodeIndent, false, branchingNodes, branchRegions, isAsync, ctxVar, bundleMode, preDeclaredFlags, !isLast || forceTrackSuccessNodes.has(chain[i]), // forceTrackSuccess for non-last chain nodes or nodes with promoted dependents
878
+ production);
865
879
  // Generate scoped children for this chain node
866
- generateScopedChildrenExecution(instance, nodeType, workflow, nodeTypes, generatedNodes, availableVars, lines, nodeIndent, branchingNodes, branchRegions, isAsync, bundleMode);
880
+ generateScopedChildrenExecution(instance, nodeType, workflow, nodeTypes, generatedNodes, availableVars, lines, nodeIndent, branchingNodes, branchRegions, isAsync, bundleMode, production);
867
881
  if (hasGuard) {
868
882
  lines.push(`${indent}} else {`);
869
883
  // Emit CANCELLED for this node and all remaining chain nodes + their regions
@@ -890,10 +904,29 @@ function generateBranchingChainCode(chain, workflow, nodeTypes, branchingNodes,
890
904
  }
891
905
  function generateBranchingNodeCode(instance, branchNode, workflow, allNodeTypes, region, availableVars, generatedNodes, lines, indent, _generateReturns = true, // DEPRECATED: always false, kept for signature compat
892
906
  branchingNodes, branchRegions, isAsync, ctxVar = 'ctx', // Context variable name (for scoped contexts)
893
- bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = false) {
907
+ bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = false, production = false) {
894
908
  const instanceId = instance.id;
895
909
  const safeId = toValidIdentifier(instanceId);
896
910
  const functionName = branchNode.functionName;
911
+ // Debug controller: beforeNode hook for branching nodes
912
+ const emitDebugHooks = !production;
913
+ const outerIndent = indent;
914
+ // Only declare success flag if there are downstream nodes
915
+ const hasSuccessDownstream = region.successNodes.size > 0;
916
+ const hasFailureDownstream = region.failureNodes.size > 0;
917
+ const hasDownstream = hasSuccessDownstream || hasFailureDownstream;
918
+ // Track success flag when there are downstream nodes OR when chain code needs it
919
+ const trackSuccess = hasDownstream || forceTrackSuccess;
920
+ if (emitDebugHooks) {
921
+ // Hoist success flag declaration before the if block so it remains in scope
922
+ // for downstream branching code that runs after the if/else.
923
+ if (trackSuccess && !preDeclaredSuccessFlags.has(safeId)) {
924
+ lines.push(`${indent}let ${safeId}_success = false;`);
925
+ }
926
+ const awaitHook = isAsync ? 'await ' : '';
927
+ lines.push(`${indent}if (${awaitHook}__ctrl__.beforeNode('${instanceId}', ${ctxVar})) {`);
928
+ indent = `${indent} `;
929
+ }
897
930
  lines.push(`${indent}${ctxVar}.checkAborted('${instanceId}');`);
898
931
  lines.push(`${indent}${safeId}Idx = ${ctxVar}.addExecution('${instanceId}');`);
899
932
  lines.push(`${indent}if (typeof globalThis !== 'undefined') (globalThis as any).__fw_current_node_id__ = '${instanceId}';`);
@@ -904,15 +937,9 @@ bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = fal
904
937
  lines.push(`${indent} status: 'RUNNING',`);
905
938
  lines.push(`${indent}});`);
906
939
  lines.push('');
907
- // Only declare success flag if there are downstream nodes
908
- const hasSuccessDownstream = region.successNodes.size > 0;
909
- const hasFailureDownstream = region.failureNodes.size > 0;
910
- const hasDownstream = hasSuccessDownstream || hasFailureDownstream;
911
- // Track success flag when there are downstream nodes OR when chain code needs it
912
- const trackSuccess = hasDownstream || forceTrackSuccess;
913
940
  if (trackSuccess) {
914
- if (preDeclaredSuccessFlags.has(safeId)) {
915
- // Flag was pre-declared by chain code — use assignment, not declaration
941
+ if (preDeclaredSuccessFlags.has(safeId) || emitDebugHooks) {
942
+ // Flag was pre-declared (by chain code or hoisted for debug hooks) — assignment only
916
943
  lines.push(`${indent}${safeId}_success = false;`);
917
944
  }
918
945
  else {
@@ -1051,6 +1078,11 @@ bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = fal
1051
1078
  lines.push(`${indent} executionIndex: ${safeId}Idx,`);
1052
1079
  lines.push(`${indent} status: 'SUCCEEDED',`);
1053
1080
  lines.push(`${indent} });`);
1081
+ // Debug controller: afterNode hook for branching nodes
1082
+ if (emitDebugHooks) {
1083
+ const awaitHook = isAsync ? 'await ' : '';
1084
+ lines.push(`${indent} ${awaitHook}__ctrl__.afterNode('${instanceId}', ${ctxVar});`);
1085
+ }
1054
1086
  // Use onSuccess from result to determine control flow
1055
1087
  // For expression nodes, onSuccess is always true here (catch handles failure)
1056
1088
  if (trackSuccess) {
@@ -1087,6 +1119,20 @@ bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = fal
1087
1119
  // Re-throw the error to propagate it up (important for recursive workflows)
1088
1120
  lines.push(`${indent} throw error;`);
1089
1121
  lines.push(`${indent}}`);
1122
+ // Close the debug controller beforeNode if block for the node's own execution.
1123
+ // The downstream branching must be outside the if block so it runs even when
1124
+ // the node is skipped (checkpoint resume).
1125
+ if (emitDebugHooks) {
1126
+ lines.push(`${outerIndent}} else {`);
1127
+ // Node was skipped (checkpoint resume). Controller already restored outputs
1128
+ // into ctx, but we need local vars for downstream branching.
1129
+ lines.push(`${outerIndent} ${safeId}Idx = ${ctxVar}.addExecution('${instanceId}');`);
1130
+ if (trackSuccess) {
1131
+ lines.push(`${outerIndent} ${safeId}_success = true;`);
1132
+ }
1133
+ lines.push(`${outerIndent}}`);
1134
+ indent = outerIndent; // Restore indent level for downstream code
1135
+ }
1090
1136
  lines.push('');
1091
1137
  // Only generate if/else if there are downstream nodes
1092
1138
  if (hasDownstream) {
@@ -1112,7 +1158,7 @@ bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = fal
1112
1158
  return;
1113
1159
  if (branchingNodes.has(instanceId)) {
1114
1160
  const nestedRegion = branchRegions.get(instanceId);
1115
- generateBranchingNodeCode(inst, nodeType, workflow, allNodeTypes, nestedRegion, successVars, generatedNodes, lines, `${indent} `, false, branchingNodes, branchRegions, isAsync, ctxVar, bundleMode);
1161
+ generateBranchingNodeCode(inst, nodeType, workflow, allNodeTypes, nestedRegion, successVars, generatedNodes, lines, `${indent} `, false, branchingNodes, branchRegions, isAsync, ctxVar, bundleMode, new Set(), false, production);
1116
1162
  successExecutedNodes.push(instanceId);
1117
1163
  nestedRegion.successNodes.forEach((n) => successExecutedNodes.push(n));
1118
1164
  nestedRegion.failureNodes.forEach((n) => successExecutedNodes.push(n));
@@ -1120,8 +1166,8 @@ bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = fal
1120
1166
  else {
1121
1167
  generateNodeCallWithContext(inst, nodeType, workflow, successVars, lines, allNodeTypes, `${indent} `, isAsync, false, // useConst
1122
1168
  undefined, // instanceParent
1123
- ctxVar, bundleMode, true // skipExecuteGuard — inside branch, execute is guaranteed by if/else
1124
- );
1169
+ ctxVar, bundleMode, true, // skipExecuteGuard — inside branch, execute is guaranteed by if/else
1170
+ branchingNodes, production);
1125
1171
  Object.keys(nodeType.outputs).forEach((portName) => {
1126
1172
  successVars.set(`${instanceId}.${portName}`, `${toValidIdentifier(instanceId)}Result.${portName}`);
1127
1173
  });
@@ -1153,7 +1199,7 @@ bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = fal
1153
1199
  return;
1154
1200
  if (branchingNodes.has(instanceId)) {
1155
1201
  const nestedRegion = branchRegions.get(instanceId);
1156
- generateBranchingNodeCode(inst, nodeType, workflow, allNodeTypes, nestedRegion, failureVars, generatedNodes, lines, `${indent} `, false, branchingNodes, branchRegions, isAsync, ctxVar, bundleMode);
1202
+ generateBranchingNodeCode(inst, nodeType, workflow, allNodeTypes, nestedRegion, failureVars, generatedNodes, lines, `${indent} `, false, branchingNodes, branchRegions, isAsync, ctxVar, bundleMode, new Set(), false, production);
1157
1203
  failureExecutedNodes.push(instanceId);
1158
1204
  nestedRegion.successNodes.forEach((n) => failureExecutedNodes.push(n));
1159
1205
  nestedRegion.failureNodes.forEach((n) => failureExecutedNodes.push(n));
@@ -1161,8 +1207,8 @@ bundleMode = false, preDeclaredSuccessFlags = new Set(), forceTrackSuccess = fal
1161
1207
  else {
1162
1208
  generateNodeCallWithContext(inst, nodeType, workflow, failureVars, lines, allNodeTypes, `${indent} `, isAsync, false, // useConst
1163
1209
  undefined, // instanceParent
1164
- ctxVar, bundleMode, true // skipExecuteGuard — inside branch, execute is guaranteed by if/else
1165
- );
1210
+ ctxVar, bundleMode, true, // skipExecuteGuard — inside branch, execute is guaranteed by if/else
1211
+ branchingNodes, production);
1166
1212
  Object.keys(nodeType.outputs).forEach((portName) => {
1167
1213
  failureVars.set(`${instanceId}.${portName}`, `${toValidIdentifier(instanceId)}Result.${portName}`);
1168
1214
  });
@@ -1318,7 +1364,8 @@ instanceParent, // Parent node ID for scope children (parent is const, no ! need
1318
1364
  ctxVar = 'ctx', // Context variable name (for scoped contexts)
1319
1365
  bundleMode = false, // Bundle mode uses params object pattern for wrapper functions
1320
1366
  skipExecuteGuard = false, // Skip execute port STEP guard (for nodes inside branch blocks)
1321
- branchingNodes = new Set() // Branching nodes set for port-aware STEP guards
1367
+ branchingNodes = new Set(), // Branching nodes set for port-aware STEP guards
1368
+ production = false // When false, emit debug controller hooks (beforeNode/afterNode)
1322
1369
  ) {
1323
1370
  const instanceId = instance.id;
1324
1371
  const safeId = toValidIdentifier(instanceId);
@@ -1458,7 +1505,24 @@ branchingNodes = new Set() // Branching nodes set for port-aware STEP guards
1458
1505
  }
1459
1506
  }
1460
1507
  }
1461
- const varDecl = useConst ? 'const ' : '';
1508
+ // Debug controller: beforeNode hook (step-through / checkpoint resume)
1509
+ // When enabled, wraps the entire node execution in if(await __ctrl__.beforeNode(...))
1510
+ // so checkpoint resume can skip already-completed nodes.
1511
+ const emitDebugHooks = !production;
1512
+ const outerIndent = indent; // preserve for closing brace
1513
+ if (emitDebugHooks) {
1514
+ // When useConst=true, the Idx variable would normally be declared with const
1515
+ // inside the node execution block. Since debug hooks wrap that in if/else,
1516
+ // we must hoist the declaration before the if block to keep it in scope.
1517
+ if (useConst) {
1518
+ lines.push(`${indent}let ${safeId}Idx: number;`);
1519
+ }
1520
+ const awaitHook = isAsync ? 'await ' : '';
1521
+ lines.push(`${indent}if (${awaitHook}__ctrl__.beforeNode('${instanceId}', ${ctxVar})) {`);
1522
+ indent = `${indent} `;
1523
+ }
1524
+ // When debug hooks hoist the declaration, we use assignment only inside the block.
1525
+ const varDecl = (useConst && !emitDebugHooks) ? 'const ' : '';
1462
1526
  lines.push(`${indent}${ctxVar}.checkAborted('${instanceId}');`);
1463
1527
  lines.push(`${indent}${varDecl}${safeId}Idx = ${ctxVar}.addExecution('${instanceId}');`);
1464
1528
  lines.push(`${indent}if (typeof globalThis !== 'undefined') (globalThis as any).__fw_current_node_id__ = '${instanceId}';`);
@@ -1628,6 +1692,11 @@ branchingNodes = new Set() // Branching nodes set for port-aware STEP guards
1628
1692
  lines.push(`${indent} executionIndex: ${safeId}Idx,`);
1629
1693
  lines.push(`${indent} status: 'SUCCEEDED',`);
1630
1694
  lines.push(`${indent} });`);
1695
+ // Debug controller: afterNode hook (checkpoint write, step pause)
1696
+ if (emitDebugHooks) {
1697
+ const awaitHook = isAsync ? 'await ' : '';
1698
+ lines.push(`${indent} ${awaitHook}__ctrl__.afterNode('${instanceId}', ${ctxVar});`);
1699
+ }
1631
1700
  lines.push(`${indent}} catch (error: unknown) {`);
1632
1701
  lines.push(`${indent} const isCancellation = CancellationError.isCancellationError(error);`);
1633
1702
  lines.push(`${indent} ${ctxVar}.sendStatusChangedEvent({`);
@@ -1651,8 +1720,16 @@ branchingNodes = new Set() // Branching nodes set for port-aware STEP guards
1651
1720
  lines.push(`${indent} }`);
1652
1721
  lines.push(`${indent} throw error;`);
1653
1722
  lines.push(`${indent}}`);
1723
+ // Close the debug controller beforeNode if block
1724
+ if (emitDebugHooks) {
1725
+ // Else block: node was skipped (checkpoint resume).
1726
+ // Register execution so downstream nodes can reference {safeId}Idx.
1727
+ lines.push(`${outerIndent}} else {`);
1728
+ lines.push(`${outerIndent} ${varDecl}${safeId}Idx = ${ctxVar}.addExecution('${instanceId}');`);
1729
+ lines.push(`${outerIndent}}`);
1730
+ }
1654
1731
  if (shouldIndent) {
1655
- const originalIndent = indent.slice(0, -2);
1732
+ const originalIndent = outerIndent.slice(0, -2);
1656
1733
  lines.push(`${originalIndent}}`);
1657
1734
  }
1658
1735
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * In-memory registry for active debug sessions.
3
+ * Mirrors the pattern of run-registry.ts but tracks DebugController state
4
+ * instead of AgentChannel state.
5
+ */
6
+ import type { DebugController, DebugPauseState } from '../runtime/debug-controller.js';
7
+ export interface DebugSession {
8
+ debugId: string;
9
+ filePath: string;
10
+ workflowName?: string;
11
+ controller: DebugController;
12
+ /** The still-pending execution promise. Resolves when workflow completes. */
13
+ executionPromise: Promise<unknown>;
14
+ createdAt: number;
15
+ /** Temp files to clean up when the session ends. */
16
+ tmpFiles: string[];
17
+ /** Most recent pause state (updated on each pause) */
18
+ lastPauseState?: DebugPauseState;
19
+ }
20
+ export declare function storeDebugSession(session: DebugSession): void;
21
+ export declare function getDebugSession(debugId: string): DebugSession | undefined;
22
+ export declare function removeDebugSession(debugId: string): void;
23
+ export declare function listDebugSessions(): Array<{
24
+ debugId: string;
25
+ filePath: string;
26
+ workflowName?: string;
27
+ createdAt: number;
28
+ lastPauseState?: DebugPauseState;
29
+ }>;
30
+ //# sourceMappingURL=debug-session.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * In-memory registry for active debug sessions.
3
+ * Mirrors the pattern of run-registry.ts but tracks DebugController state
4
+ * instead of AgentChannel state.
5
+ */
6
+ const debugSessions = new Map();
7
+ export function storeDebugSession(session) {
8
+ debugSessions.set(session.debugId, session);
9
+ }
10
+ export function getDebugSession(debugId) {
11
+ return debugSessions.get(debugId);
12
+ }
13
+ export function removeDebugSession(debugId) {
14
+ debugSessions.delete(debugId);
15
+ }
16
+ export function listDebugSessions() {
17
+ return Array.from(debugSessions.values()).map((session) => ({
18
+ debugId: session.debugId,
19
+ filePath: session.filePath,
20
+ workflowName: session.workflowName,
21
+ createdAt: session.createdAt,
22
+ lastPauseState: session.lastPauseState,
23
+ }));
24
+ }
25
+ //# sourceMappingURL=debug-session.js.map
@@ -7,6 +7,7 @@ export { registerEditorTools } from './tools-editor.js';
7
7
  export { registerQueryTools } from './tools-query.js';
8
8
  export { registerTemplateTools } from './tools-template.js';
9
9
  export { registerPatternTools } from './tools-pattern.js';
10
+ export { registerDebugTools } from './tools-debug.js';
10
11
  export { registerResources } from './resources.js';
11
12
  export { registerPrompts } from './prompts.js';
12
13
  export { startMcpServer, mcpServerCommand } from './server.js';
package/dist/mcp/index.js CHANGED
@@ -6,6 +6,7 @@ export { registerEditorTools } from './tools-editor.js';
6
6
  export { registerQueryTools } from './tools-query.js';
7
7
  export { registerTemplateTools } from './tools-template.js';
8
8
  export { registerPatternTools } from './tools-pattern.js';
9
+ export { registerDebugTools } from './tools-debug.js';
9
10
  export { registerResources } from './resources.js';
10
11
  export { registerPrompts } from './prompts.js';
11
12
  export { startMcpServer, mcpServerCommand } from './server.js';
@@ -12,6 +12,8 @@ import { registerMarketplaceTools } from './tools-marketplace.js';
12
12
  import { registerDiagramTools } from './tools-diagram.js';
13
13
  import { registerDocsTools } from './tools-docs.js';
14
14
  import { registerModelTools } from './tools-model.js';
15
+ import { registerDebugTools } from './tools-debug.js';
16
+ import { registerContextTools } from './tools-context.js';
15
17
  import { registerResources } from './resources.js';
16
18
  import { registerPrompts } from './prompts.js';
17
19
  function parseEventFilterFromEnv() {
@@ -74,6 +76,8 @@ export async function startMcpServer(options) {
74
76
  registerDiagramTools(mcp);
75
77
  registerDocsTools(mcp);
76
78
  registerModelTools(mcp);
79
+ registerDebugTools(mcp);
80
+ registerContextTools(mcp);
77
81
  registerResources(mcp, connection, buffer);
78
82
  registerPrompts(mcp);
79
83
  // Connect transport (only in stdio MCP mode)
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerContextTools(mcp: McpServer): void;
3
+ //# sourceMappingURL=tools-context.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ import { buildContext } from '../context/index.js';
3
+ import { makeToolResult, makeErrorResult } from './response-utils.js';
4
+ export function registerContextTools(mcp) {
5
+ mcp.tool('fw_context', 'Generate a self-contained LLM context bundle with Flow Weaver documentation, grammar, and conventions. Use preset="core" for basics, "authoring" for writing workflows, "full" for everything, "ops" for CLI/deployment reference.', {
6
+ preset: z
7
+ .enum(['core', 'authoring', 'full', 'ops'])
8
+ .optional()
9
+ .default('core')
10
+ .describe('Topic preset'),
11
+ profile: z
12
+ .enum(['standalone', 'assistant'])
13
+ .optional()
14
+ .default('assistant')
15
+ .describe('standalone = full self-contained dump, assistant = assumes MCP tools available'),
16
+ topics: z
17
+ .string()
18
+ .optional()
19
+ .describe('Comma-separated topic slugs (overrides preset)'),
20
+ addTopics: z
21
+ .string()
22
+ .optional()
23
+ .describe('Comma-separated slugs to add to preset'),
24
+ includeGrammar: z
25
+ .boolean()
26
+ .optional()
27
+ .default(true)
28
+ .describe('Include EBNF annotation grammar section'),
29
+ }, async (args) => {
30
+ try {
31
+ const result = buildContext({
32
+ preset: args.preset,
33
+ profile: args.profile,
34
+ topics: args.topics ? args.topics.split(',').map((s) => s.trim()) : undefined,
35
+ addTopics: args.addTopics ? args.addTopics.split(',').map((s) => s.trim()) : undefined,
36
+ includeGrammar: args.includeGrammar,
37
+ });
38
+ return makeToolResult({
39
+ profile: result.profile,
40
+ topicCount: result.topicCount,
41
+ lineCount: result.lineCount,
42
+ topicSlugs: result.topicSlugs,
43
+ content: result.content,
44
+ });
45
+ }
46
+ catch (err) {
47
+ return makeErrorResult('CONTEXT_ERROR', `fw_context failed: ${err instanceof Error ? err.message : String(err)}`);
48
+ }
49
+ });
50
+ }
51
+ //# sourceMappingURL=tools-context.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerDebugTools(mcp: McpServer): void;
3
+ //# sourceMappingURL=tools-debug.d.ts.map