@synergenius/flow-weaver 0.3.0 → 0.4.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.
Files changed (73) hide show
  1. package/README.md +1 -0
  2. package/dist/annotation-generator.js +36 -0
  3. package/dist/api/generate-in-place.js +39 -0
  4. package/dist/api/generate.js +11 -1
  5. package/dist/api/manipulation/nodes.js +22 -0
  6. package/dist/ast/types.d.ts +27 -1
  7. package/dist/built-in-nodes/index.d.ts +1 -0
  8. package/dist/built-in-nodes/index.js +1 -0
  9. package/dist/built-in-nodes/invoke-workflow.js +12 -1
  10. package/dist/built-in-nodes/mock-types.d.ts +2 -0
  11. package/dist/built-in-nodes/wait-for-agent.d.ts +13 -0
  12. package/dist/built-in-nodes/wait-for-agent.js +26 -0
  13. package/dist/chevrotain-parser/fan-parser.d.ts +38 -0
  14. package/dist/chevrotain-parser/fan-parser.js +149 -0
  15. package/dist/chevrotain-parser/grammar-diagrams.d.ts +1 -0
  16. package/dist/chevrotain-parser/grammar-diagrams.js +3 -0
  17. package/dist/chevrotain-parser/index.d.ts +3 -1
  18. package/dist/chevrotain-parser/index.js +3 -1
  19. package/dist/chevrotain-parser/tokens.d.ts +2 -0
  20. package/dist/chevrotain-parser/tokens.js +10 -0
  21. package/dist/cli/commands/diagram.d.ts +2 -1
  22. package/dist/cli/commands/diagram.js +9 -6
  23. package/dist/cli/commands/run.js +59 -1
  24. package/dist/cli/flow-weaver.mjs +1396 -77
  25. package/dist/cli/index.js +23 -36
  26. package/dist/diagram/geometry.js +47 -5
  27. package/dist/diagram/html-viewer.d.ts +12 -0
  28. package/dist/diagram/html-viewer.js +399 -0
  29. package/dist/diagram/index.d.ts +12 -0
  30. package/dist/diagram/index.js +22 -0
  31. package/dist/diagram/types.d.ts +1 -0
  32. package/dist/doc-metadata/extractors/annotations.js +282 -1
  33. package/dist/doc-metadata/types.d.ts +6 -0
  34. package/dist/generator/control-flow.d.ts +13 -0
  35. package/dist/generator/control-flow.js +74 -0
  36. package/dist/generator/inngest.js +23 -0
  37. package/dist/generator/unified.js +122 -2
  38. package/dist/jsdoc-parser.d.ts +24 -0
  39. package/dist/jsdoc-parser.js +41 -1
  40. package/dist/mcp/agent-channel.d.ts +35 -0
  41. package/dist/mcp/agent-channel.js +61 -0
  42. package/dist/mcp/run-registry.d.ts +29 -0
  43. package/dist/mcp/run-registry.js +24 -0
  44. package/dist/mcp/tools-diagram.d.ts +1 -1
  45. package/dist/mcp/tools-diagram.js +15 -7
  46. package/dist/mcp/tools-editor.js +75 -3
  47. package/dist/mcp/workflow-executor.d.ts +28 -0
  48. package/dist/mcp/workflow-executor.js +62 -1
  49. package/dist/parser.d.ts +8 -0
  50. package/dist/parser.js +100 -0
  51. package/dist/runtime/ExecutionContext.d.ts +2 -0
  52. package/dist/runtime/ExecutionContext.js +2 -0
  53. package/dist/runtime/events.d.ts +1 -1
  54. package/dist/sugar-optimizer.js +28 -3
  55. package/dist/validator.d.ts +8 -0
  56. package/dist/validator.js +92 -0
  57. package/docs/reference/advanced-annotations.md +431 -0
  58. package/docs/reference/built-in-nodes.md +225 -0
  59. package/docs/reference/cli-reference.md +882 -0
  60. package/docs/reference/compilation.md +351 -0
  61. package/docs/reference/concepts.md +400 -0
  62. package/docs/reference/debugging.md +255 -0
  63. package/docs/reference/deployment.md +207 -0
  64. package/docs/reference/error-codes.md +686 -0
  65. package/docs/reference/export-interface.md +229 -0
  66. package/docs/reference/iterative-development.md +186 -0
  67. package/docs/reference/jsdoc-grammar.md +471 -0
  68. package/docs/reference/marketplace.md +205 -0
  69. package/docs/reference/node-conversion.md +308 -0
  70. package/docs/reference/patterns.md +161 -0
  71. package/docs/reference/scaffold.md +160 -0
  72. package/docs/reference/tutorial.md +519 -0
  73. package/package.json +1 -1
@@ -82,9 +82,21 @@ export async function executeWorkflowFromFile(filePath, params, options) {
82
82
  if (options?.mocks) {
83
83
  globalThis.__fw_mocks__ = options.mocks;
84
84
  }
85
+ // Set agent channel for waitForAgent pause/resume
86
+ if (options?.agentChannel) {
87
+ globalThis.__fw_agent_channel__ = options.agentChannel;
88
+ }
85
89
  // Dynamic import using file:// URL for cross-platform compatibility
86
90
  // (Windows paths like C:\... break with bare import() — "Received protocol 'c:'")
87
91
  const mod = await import(pathToFileURL(tmpFile).href);
92
+ // Register exported functions for local invokeWorkflow resolution
93
+ const workflowRegistry = {};
94
+ for (const [key, value] of Object.entries(mod)) {
95
+ if (typeof value === 'function' && key !== '__esModule') {
96
+ workflowRegistry[key] = value;
97
+ }
98
+ }
99
+ globalThis.__fw_workflow_registry__ = workflowRegistry;
88
100
  // Find the target exported function
89
101
  const exportedFn = findExportedFunction(mod, options?.workflowName);
90
102
  if (!exportedFn) {
@@ -99,13 +111,15 @@ export async function executeWorkflowFromFile(filePath, params, options) {
99
111
  result,
100
112
  functionName: exportedFn.name,
101
113
  executionTime,
102
- ...(includeTrace && { trace }),
114
+ ...(includeTrace && { trace, summary: computeTraceSummary(trace) }),
103
115
  };
104
116
  }
105
117
  finally {
106
118
  // Clean up globals
107
119
  delete globalThis.__fw_debugger__;
108
120
  delete globalThis.__fw_mocks__;
121
+ delete globalThis.__fw_workflow_registry__;
122
+ delete globalThis.__fw_agent_channel__;
109
123
  // Clean up temp files
110
124
  try {
111
125
  fs.unlinkSync(tmpFile);
@@ -117,6 +131,53 @@ export async function executeWorkflowFromFile(filePath, params, options) {
117
131
  catch { /* ignore */ }
118
132
  }
119
133
  }
134
+ /** Compute a concise summary from raw trace events. */
135
+ export function computeTraceSummary(trace) {
136
+ if (trace.length === 0) {
137
+ return { totalNodes: 0, succeeded: 0, failed: 0, cancelled: 0, nodeTimings: [], totalDurationMs: 0 };
138
+ }
139
+ const nodeStartTimes = new Map();
140
+ const nodeFinalStatus = new Map();
141
+ const nodeTimings = [];
142
+ for (const event of trace) {
143
+ if (event.type !== 'STATUS_CHANGED' || !event.data)
144
+ continue;
145
+ const id = event.data.id;
146
+ const status = event.data.status;
147
+ if (!id || !status)
148
+ continue;
149
+ if (status === 'RUNNING') {
150
+ nodeStartTimes.set(id, event.timestamp);
151
+ }
152
+ if (status === 'SUCCEEDED' || status === 'FAILED' || status === 'CANCELLED') {
153
+ nodeFinalStatus.set(id, status);
154
+ const startTime = nodeStartTimes.get(id);
155
+ if (startTime !== undefined) {
156
+ nodeTimings.push({ nodeId: id, durationMs: event.timestamp - startTime });
157
+ }
158
+ }
159
+ }
160
+ let succeeded = 0;
161
+ let failed = 0;
162
+ let cancelled = 0;
163
+ for (const status of nodeFinalStatus.values()) {
164
+ if (status === 'SUCCEEDED')
165
+ succeeded++;
166
+ else if (status === 'FAILED')
167
+ failed++;
168
+ else if (status === 'CANCELLED')
169
+ cancelled++;
170
+ }
171
+ const totalDurationMs = trace[trace.length - 1].timestamp - trace[0].timestamp;
172
+ return {
173
+ totalNodes: nodeFinalStatus.size,
174
+ succeeded,
175
+ failed,
176
+ cancelled,
177
+ nodeTimings,
178
+ totalDurationMs,
179
+ };
180
+ }
120
181
  function findExportedFunction(mod, preferredName) {
121
182
  // If a preferred name is specified, try it first
122
183
  if (preferredName && typeof mod[preferredName] === 'function') {
package/dist/parser.d.ts CHANGED
@@ -135,6 +135,14 @@ export declare class AnnotationParser {
135
135
  * Processes all paths together for shared deduplication.
136
136
  */
137
137
  private expandPathMacros;
138
+ /**
139
+ * Expand @fanOut macros into 1-to-N connections.
140
+ */
141
+ private expandFanOutMacros;
142
+ /**
143
+ * Expand @fanIn macros into N-to-1 connections.
144
+ */
145
+ private expandFanInMacros;
138
146
  /**
139
147
  * Generate automatic connections for @autoConnect workflows.
140
148
  * Wires nodes in declaration order as a linear pipeline:
package/dist/parser.js CHANGED
@@ -916,6 +916,14 @@ export class AnnotationParser {
916
916
  if (config.paths && config.paths.length > 0) {
917
917
  this.expandPathMacros(config.paths, instances, connections, allAvailableNodeTypes, startPorts, exitPorts, macros, errors, warnings);
918
918
  }
919
+ // Expand @fanOut macros into 1-to-N connections
920
+ if (config.fanOuts && config.fanOuts.length > 0) {
921
+ this.expandFanOutMacros(config.fanOuts, instances, connections, startPorts, exitPorts, macros, errors);
922
+ }
923
+ // Expand @fanIn macros into N-to-1 connections
924
+ if (config.fanIns && config.fanIns.length > 0) {
925
+ this.expandFanInMacros(config.fanIns, instances, connections, startPorts, exitPorts, macros, errors);
926
+ }
919
927
  // Include ALL available nodeTypes in the workflow AST, plus imported npm types.
920
928
  // Previously this filtered to only nodeTypes used by instances, but that caused
921
929
  // a bug: when creating a new nodeType and then adding its first instance,
@@ -1502,6 +1510,98 @@ export class AnnotationParser {
1502
1510
  });
1503
1511
  }
1504
1512
  }
1513
+ /**
1514
+ * Expand @fanOut macros into 1-to-N connections.
1515
+ */
1516
+ expandFanOutMacros(fanOutConfigs, instances, connections, startPorts, exitPorts, macros, errors) {
1517
+ const instanceIds = new Set(instances.map(i => i.id));
1518
+ instanceIds.add('Start');
1519
+ instanceIds.add('Exit');
1520
+ for (const config of fanOutConfigs) {
1521
+ const { source, targets } = config;
1522
+ // Validate source node exists
1523
+ if (!instanceIds.has(source.node)) {
1524
+ errors.push(`@fanOut: source node "${source.node}" does not exist`);
1525
+ continue;
1526
+ }
1527
+ let valid = true;
1528
+ for (const target of targets) {
1529
+ if (!instanceIds.has(target.node)) {
1530
+ errors.push(`@fanOut: target node "${target.node}" does not exist`);
1531
+ valid = false;
1532
+ }
1533
+ }
1534
+ if (!valid)
1535
+ continue;
1536
+ // Create connections
1537
+ for (const target of targets) {
1538
+ const targetPort = target.port ?? source.port;
1539
+ const conn = {
1540
+ type: 'Connection',
1541
+ from: { node: source.node, port: source.port },
1542
+ to: { node: target.node, port: targetPort },
1543
+ };
1544
+ // Deduplicate
1545
+ const exists = connections.some(c => c.from.node === conn.from.node && c.from.port === conn.from.port &&
1546
+ c.to.node === conn.to.node && c.to.port === conn.to.port);
1547
+ if (!exists) {
1548
+ connections.push(conn);
1549
+ }
1550
+ }
1551
+ // Store macro for round-trip preservation
1552
+ macros.push({
1553
+ type: 'fanOut',
1554
+ source: { node: source.node, port: source.port },
1555
+ targets: targets.map(t => t.port ? { node: t.node, port: t.port } : { node: t.node }),
1556
+ });
1557
+ }
1558
+ }
1559
+ /**
1560
+ * Expand @fanIn macros into N-to-1 connections.
1561
+ */
1562
+ expandFanInMacros(fanInConfigs, instances, connections, startPorts, exitPorts, macros, errors) {
1563
+ const instanceIds = new Set(instances.map(i => i.id));
1564
+ instanceIds.add('Start');
1565
+ instanceIds.add('Exit');
1566
+ for (const config of fanInConfigs) {
1567
+ const { sources, target } = config;
1568
+ // Validate target node exists
1569
+ if (!instanceIds.has(target.node)) {
1570
+ errors.push(`@fanIn: target node "${target.node}" does not exist`);
1571
+ continue;
1572
+ }
1573
+ let valid = true;
1574
+ for (const source of sources) {
1575
+ if (!instanceIds.has(source.node)) {
1576
+ errors.push(`@fanIn: source node "${source.node}" does not exist`);
1577
+ valid = false;
1578
+ }
1579
+ }
1580
+ if (!valid)
1581
+ continue;
1582
+ // Create connections
1583
+ for (const source of sources) {
1584
+ const sourcePort = source.port ?? target.port;
1585
+ const conn = {
1586
+ type: 'Connection',
1587
+ from: { node: source.node, port: sourcePort },
1588
+ to: { node: target.node, port: target.port },
1589
+ };
1590
+ // Deduplicate
1591
+ const exists = connections.some(c => c.from.node === conn.from.node && c.from.port === conn.from.port &&
1592
+ c.to.node === conn.to.node && c.to.port === conn.to.port);
1593
+ if (!exists) {
1594
+ connections.push(conn);
1595
+ }
1596
+ }
1597
+ // Store macro for round-trip preservation
1598
+ macros.push({
1599
+ type: 'fanIn',
1600
+ sources: sources.map(s => s.port ? { node: s.node, port: s.port } : { node: s.node }),
1601
+ target: { node: target.node, port: target.port },
1602
+ });
1603
+ }
1604
+ }
1505
1605
  /**
1506
1606
  * Generate automatic connections for @autoConnect workflows.
1507
1607
  * Wires nodes in declaration order as a linear pipeline:
@@ -21,6 +21,8 @@ export interface VariableAddress {
21
21
  portName: string;
22
22
  executionIndex: number;
23
23
  nodeTypeName?: string | undefined;
24
+ scope?: string | undefined;
25
+ side?: 'start' | 'exit' | undefined;
24
26
  }
25
27
  export interface ExecutionInfo {
26
28
  id: string;
@@ -65,6 +65,8 @@ export class GeneratedExecutionContext {
65
65
  portName: address.portName,
66
66
  executionIndex: address.executionIndex,
67
67
  key: 'default',
68
+ ...(address.scope !== undefined && { scope: address.scope }),
69
+ ...(address.side !== undefined && { side: address.side }),
68
70
  },
69
71
  value: actualValue,
70
72
  });
@@ -1,4 +1,4 @@
1
- export type TStatusType = "RUNNING" | "SCHEDULED" | "SUCCEEDED" | "FAILED" | "CANCELLED" | "PENDING";
1
+ export type TStatusType = "RUNNING" | "SCHEDULED" | "SUCCEEDED" | "FAILED" | "CANCELLED" | "PENDING" | "WAITING_FOR_AGENT";
2
2
  export type TVariableIdentification = {
3
3
  nodeTypeName: string;
4
4
  id: string;
@@ -69,10 +69,23 @@ export function validatePathMacro(path, connections, instances) {
69
69
  * Non-path macros are passed through unchanged.
70
70
  */
71
71
  export function filterStaleMacros(macros, connections, instances) {
72
+ const instanceIds = new Set(instances.map(i => i.id));
73
+ instanceIds.add('Start');
74
+ instanceIds.add('Exit');
72
75
  return macros.filter(macro => {
73
- if (macro.type !== 'path')
74
- return true;
75
- return validatePathMacro(macro, connections, instances);
76
+ if (macro.type === 'path')
77
+ return validatePathMacro(macro, connections, instances);
78
+ if (macro.type === 'fanOut') {
79
+ if (!instanceIds.has(macro.source.node))
80
+ return false;
81
+ return macro.targets.every(t => instanceIds.has(t.node));
82
+ }
83
+ if (macro.type === 'fanIn') {
84
+ if (!instanceIds.has(macro.target.node))
85
+ return false;
86
+ return macro.sources.every(s => instanceIds.has(s.node));
87
+ }
88
+ return true;
76
89
  });
77
90
  }
78
91
  /**
@@ -381,6 +394,18 @@ function buildExistingMacroCoverage(macros) {
381
394
  covered.add(step.node);
382
395
  }
383
396
  }
397
+ else if (macro.type === 'fanOut') {
398
+ covered.add(macro.source.node);
399
+ for (const t of macro.targets) {
400
+ covered.add(t.node);
401
+ }
402
+ }
403
+ else if (macro.type === 'fanIn') {
404
+ for (const s of macro.sources) {
405
+ covered.add(s.node);
406
+ }
407
+ covered.add(macro.target.node);
408
+ }
384
409
  }
385
410
  return covered;
386
411
  }
@@ -86,6 +86,14 @@ export declare class WorkflowValidator {
86
86
  * from opposite branches (onSuccess vs onFailure) of the same branching node.
87
87
  */
88
88
  private areMutuallyExclusive;
89
+ /**
90
+ * Validate inner graph topology for nodes that have scoped ports.
91
+ *
92
+ * For each scoped node instance, checks that:
93
+ * 1. Inner nodes (children) have their required inputs connected within the scope
94
+ * 2. Scoped input ports (callback returns) have connections from inner nodes
95
+ */
96
+ private validateScopeTopology;
89
97
  private normalizeTypeString;
90
98
  }
91
99
  export declare const validator: WorkflowValidator;
package/dist/validator.js CHANGED
@@ -118,6 +118,7 @@ export class WorkflowValidator {
118
118
  this.validateCycles(workflow);
119
119
  this.validateMultipleInputConnections(workflow, instanceMap);
120
120
  this.validateAnnotationSignatureConsistency(workflow);
121
+ this.validateScopeTopology(workflow, instanceMap);
121
122
  // Deduplicate cascading errors: if a node has UNKNOWN_NODE_TYPE,
122
123
  // suppress UNKNOWN_SOURCE_NODE, UNKNOWN_TARGET_NODE, and UNDEFINED_NODE
123
124
  // that reference the same node IDs (they're just noise).
@@ -953,6 +954,97 @@ export class WorkflowValidator {
953
954
  const branches = new Set(branchInfos.map((info) => info.branch));
954
955
  return branches.size > 1;
955
956
  }
957
+ /**
958
+ * Validate inner graph topology for nodes that have scoped ports.
959
+ *
960
+ * For each scoped node instance, checks that:
961
+ * 1. Inner nodes (children) have their required inputs connected within the scope
962
+ * 2. Scoped input ports (callback returns) have connections from inner nodes
963
+ */
964
+ validateScopeTopology(workflow, instanceMap) {
965
+ // Find all instances that have scoped ports
966
+ for (const instance of workflow.instances) {
967
+ const nodeType = instanceMap.get(instance.id);
968
+ if (!nodeType)
969
+ continue;
970
+ // Collect scoped port pairs per scope name
971
+ const scopeNames = new Set();
972
+ for (const portDef of Object.values(nodeType.outputs)) {
973
+ if (portDef.scope)
974
+ scopeNames.add(portDef.scope);
975
+ }
976
+ for (const portDef of Object.values(nodeType.inputs)) {
977
+ if (portDef.scope)
978
+ scopeNames.add(portDef.scope);
979
+ }
980
+ if (scopeNames.size === 0)
981
+ continue;
982
+ for (const scopeName of scopeNames) {
983
+ // Find children in this scope
984
+ const childIds = [];
985
+ for (const child of workflow.instances) {
986
+ if (child.parent &&
987
+ child.parent.id === instance.id &&
988
+ child.parent.scope === scopeName) {
989
+ childIds.push(child.id);
990
+ }
991
+ }
992
+ if (childIds.length === 0)
993
+ continue;
994
+ // Collect scoped connections (connections with scope tags)
995
+ const scopedConnections = workflow.connections.filter((conn) => (conn.from.scope === scopeName && conn.from.node === instance.id) ||
996
+ (conn.to.scope === scopeName && conn.to.node === instance.id) ||
997
+ (conn.from.scope === scopeName && childIds.includes(conn.from.node)) ||
998
+ (conn.to.scope === scopeName && childIds.includes(conn.to.node)));
999
+ // Check: each child's required inputs must be satisfied within the scope
1000
+ for (const childId of childIds) {
1001
+ const childType = instanceMap.get(childId);
1002
+ if (!childType)
1003
+ continue;
1004
+ for (const [portName, portConfig] of Object.entries(childType.inputs)) {
1005
+ if (isExecutePort(portName))
1006
+ continue;
1007
+ if (portConfig.scope)
1008
+ continue; // Skip scoped ports on children
1009
+ if (portConfig.optional || portConfig.default !== undefined)
1010
+ continue;
1011
+ // Check instance expression overrides
1012
+ const childInstance = workflow.instances.find((i) => i.id === childId);
1013
+ const instancePortConfig = childInstance?.config?.portConfigs?.find((pc) => pc.portName === portName && (pc.direction == null || pc.direction === 'INPUT'));
1014
+ if (portConfig.expression || instancePortConfig?.expression !== undefined)
1015
+ continue;
1016
+ // Check if connected by any connection (scoped, inter-child, or outer)
1017
+ const isConnected = workflow.connections.some((conn) => conn.to.node === childId && conn.to.port === portName);
1018
+ if (!isConnected) {
1019
+ this.errors.push({
1020
+ type: 'error',
1021
+ code: 'SCOPE_MISSING_REQUIRED_INPUT',
1022
+ message: `Scoped child "${childId}" has unconnected required input "${portName}" within scope "${scopeName}" of "${instance.id}".`,
1023
+ node: childId,
1024
+ location: childInstance?.sourceLocation,
1025
+ });
1026
+ }
1027
+ }
1028
+ }
1029
+ // Check: scoped INPUT ports should have connections from inner nodes
1030
+ const scopedInputPorts = Object.entries(nodeType.inputs).filter(([_, portDef]) => portDef.scope === scopeName);
1031
+ for (const [portName] of scopedInputPorts) {
1032
+ const hasConnection = scopedConnections.some((conn) => conn.to.node === instance.id &&
1033
+ conn.to.port === portName &&
1034
+ conn.to.scope === scopeName);
1035
+ if (!hasConnection) {
1036
+ this.warnings.push({
1037
+ type: 'warning',
1038
+ code: 'SCOPE_UNUSED_INPUT',
1039
+ message: `Scoped input port "${portName}" of "${instance.id}" (scope "${scopeName}") has no connection from inner nodes. Data will not flow back from the scope.`,
1040
+ node: instance.id,
1041
+ location: this.getInstanceLocation(workflow, instance.id),
1042
+ });
1043
+ }
1044
+ }
1045
+ }
1046
+ }
1047
+ }
956
1048
  normalizeTypeString(type) {
957
1049
  let n = type;
958
1050
  // Remove all whitespace