@synergenius/flow-weaver 0.3.0 → 0.4.1

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 +37 -1
@@ -1,7 +1,7 @@
1
1
  import { extractStartPorts } from '../ast/workflow-utils.js';
2
2
  import { mapToTypeScript } from '../type-mappings.js';
3
3
  import { buildNodeArgumentsWithContext, toValidIdentifier } from './code-utils.js';
4
- import { buildControlFlowGraph, detectBranchingChains, findAllBranchingNodes, findNodesInBranch, performKahnsTopologicalSort, isPerPortScopedChild, } from './control-flow.js';
4
+ import { buildControlFlowGraph, computeParallelLevels, detectBranchingChains, findAllBranchingNodes, findNodesInBranch, performKahnsTopologicalSort, isPerPortScopedChild, } from './control-flow.js';
5
5
  import { RESERVED_NODE_NAMES, RESERVED_PORT_NAMES, EXECUTION_STRATEGIES, isStartNode, isExitNode, isExecutePort, isSuccessPort, isFailurePort, } from '../constants.js';
6
6
  /**
7
7
  * Helper: Determine if an instance has pull execution enabled
@@ -246,6 +246,59 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
246
246
  chainMembers.add(chain[i]);
247
247
  }
248
248
  });
249
+ // Compute parallel levels for async workflows
250
+ const perPortScopedChildrenSet = new Set();
251
+ workflow.instances.forEach((instance) => {
252
+ if (isPerPortScopedChild(instance, workflow, nodeTypes)) {
253
+ perPortScopedChildrenSet.add(instance.id);
254
+ }
255
+ });
256
+ const parallelGroupOf = new Map();
257
+ if (isAsync) {
258
+ const parallelLevels = computeParallelLevels(cfg, branchingNodes, perPortScopedChildrenSet);
259
+ for (const group of parallelLevels) {
260
+ if (group.length < 2)
261
+ continue;
262
+ // Filter out nodes that can't be parallelized
263
+ const eligible = group.filter((id) => {
264
+ if (nodesInBranches.has(id))
265
+ return false;
266
+ if (pullExecutionNodes.has(id))
267
+ return false;
268
+ if (nodeLevelScopedChildren.has(id))
269
+ return false;
270
+ if (nodesPromotedFromBranches.has(id))
271
+ return false;
272
+ if (chainMembers.has(id))
273
+ return false;
274
+ if (branchingNodes.has(id))
275
+ return false;
276
+ return true;
277
+ });
278
+ if (eligible.length >= 2) {
279
+ for (const nodeId of eligible) {
280
+ parallelGroupOf.set(nodeId, eligible);
281
+ }
282
+ }
283
+ }
284
+ }
285
+ // Pre-declare execution indices for parallel group nodes
286
+ if (parallelGroupOf.size > 0) {
287
+ const declared = new Set();
288
+ parallelGroupOf.forEach((_, instanceId) => {
289
+ if (declared.has(instanceId))
290
+ return;
291
+ declared.add(instanceId);
292
+ // Only declare if not already declared by earlier let declarations
293
+ if (!nodesInBranches.has(instanceId) &&
294
+ !branchingNodes.has(instanceId) &&
295
+ !pullExecutionNodes.has(instanceId) &&
296
+ !nodeLevelScopedChildren.has(instanceId)) {
297
+ lines.push(` let ${toValidIdentifier(instanceId)}Idx: number | undefined;`);
298
+ }
299
+ });
300
+ lines.push('');
301
+ }
249
302
  const generatedNodes = new Set();
250
303
  const availableVars = new Map();
251
304
  Object.keys(extractStartPorts(workflow)).forEach((portName) => {
@@ -272,6 +325,26 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
272
325
  lines.push(` // Node '${instance.id}' skipped: type '${instance.nodeType}' not found`);
273
326
  return;
274
327
  }
328
+ // Handle parallel groups: emit Promise.all when hitting first node of a group
329
+ if (parallelGroupOf.has(instanceId)) {
330
+ const group = parallelGroupOf.get(instanceId);
331
+ const ungeneratedGroup = group.filter((id) => !generatedNodes.has(id));
332
+ if (ungeneratedGroup.length >= 2) {
333
+ generateParallelGroupWithContext(ungeneratedGroup, workflow, nodeTypes, availableVars, lines, generatedNodes, ' ', isAsync, 'ctx', bundleMode, branchingNodes);
334
+ // Generate scoped children for each parallel node
335
+ for (const parallelNodeId of ungeneratedGroup) {
336
+ const inst = workflow.instances.find((i) => i.id === parallelNodeId);
337
+ if (!inst)
338
+ continue;
339
+ const nt = nodeTypes.find((n) => n.name === inst.nodeType || n.functionName === inst.nodeType);
340
+ if (!nt)
341
+ continue;
342
+ generateScopedChildrenExecution(inst, nt, workflow, nodeTypes, generatedNodes, availableVars, lines, ' ', branchingNodes, branchRegions, isAsync, bundleMode);
343
+ }
344
+ return;
345
+ }
346
+ // else: degenerated to 1 or 0, fall through to sequential handling
347
+ }
275
348
  if (branchingNodes.has(instanceId)) {
276
349
  // Chain members are generated by their chain head — skip
277
350
  if (chainMembers.has(instanceId)) {
@@ -342,7 +415,8 @@ export function generateControlFlowWithExecutionContext(workflow, nodeTypes, isA
342
415
  const nodeUseConst = !nodesInBranches.has(instanceId) &&
343
416
  !branchingNodes.has(instanceId) &&
344
417
  !pullExecutionNodes.has(instanceId) &&
345
- !nodeLevelScopedChildren.has(instanceId);
418
+ !nodeLevelScopedChildren.has(instanceId) &&
419
+ !parallelGroupOf.has(instanceId);
346
420
  generateNodeCallWithContext(instance, nodeType, workflow, availableVars, lines, nodeTypes, ' ', isAsync, nodeUseConst, undefined, // instanceParent
347
421
  'ctx', // ctxVar
348
422
  bundleMode, false, // skipExecuteGuard
@@ -566,6 +640,52 @@ function generateScopedChildrenExecution(parentInstance, parentNodeType, workflo
566
640
  lines.push(`${indent}ctx.mergeScope(${parentInstance.id}_scopedCtx);`);
567
641
  lines.push(``);
568
642
  }
643
+ /**
644
+ * Generate a Promise.all block for 2+ parallel nodes in the unified generator.
645
+ *
646
+ * Each node's execution code is wrapped in an async IIFE inside Promise.all.
647
+ * The outer `let` variables for execution indices are assigned inside the IIFEs.
648
+ */
649
+ function generateParallelGroupWithContext(nodeIds, workflow, nodeTypes, availableVars, lines, generatedNodes, indent, isAsync, ctxVar, bundleMode, branchingNodes) {
650
+ // Collect code buffers for each node
651
+ const nodeBuffers = [];
652
+ for (const nodeId of nodeIds) {
653
+ const instance = workflow.instances.find((i) => i.id === nodeId);
654
+ if (!instance)
655
+ continue;
656
+ const nodeType = nodeTypes.find((nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType);
657
+ if (!nodeType)
658
+ continue;
659
+ const nodeLines = [];
660
+ generateNodeCallWithContext(instance, nodeType, workflow, availableVars, nodeLines, nodeTypes, `${indent} `, // indent for inside the async IIFE
661
+ isAsync, false, // useConst = false — outer let declarations
662
+ undefined, ctxVar, bundleMode, false, branchingNodes);
663
+ nodeBuffers.push({ id: nodeId, lines: nodeLines });
664
+ }
665
+ // Fallback: if only 0-1 nodes remain, emit directly without Promise.all
666
+ if (nodeBuffers.length < 2) {
667
+ for (const buf of nodeBuffers) {
668
+ for (const line of buf.lines) {
669
+ lines.push(line);
670
+ }
671
+ generatedNodes.add(buf.id);
672
+ }
673
+ return;
674
+ }
675
+ lines.push(`${indent}await Promise.all([`);
676
+ for (let i = 0; i < nodeBuffers.length; i++) {
677
+ const buf = nodeBuffers[i];
678
+ const comma = i < nodeBuffers.length - 1 ? ',' : '';
679
+ lines.push(`${indent} (async () => {`);
680
+ for (const line of buf.lines) {
681
+ lines.push(line);
682
+ }
683
+ lines.push(`${indent} })()${comma}`);
684
+ generatedNodes.add(buf.id);
685
+ }
686
+ lines.push(`${indent}]);`);
687
+ lines.push('');
688
+ }
569
689
  /**
570
690
  * Sort branch nodes topologically based on their dependencies
571
691
  *
@@ -135,6 +135,28 @@ export interface JSDocWorkflowConfig {
135
135
  route?: 'ok' | 'fail';
136
136
  }>;
137
137
  }>;
138
+ /** @fanOut macros that expand to 1-to-N connections */
139
+ fanOuts?: Array<{
140
+ source: {
141
+ node: string;
142
+ port: string;
143
+ };
144
+ targets: Array<{
145
+ node: string;
146
+ port?: string;
147
+ }>;
148
+ }>;
149
+ /** @fanIn macros that expand to N-to-1 connections */
150
+ fanIns?: Array<{
151
+ sources: Array<{
152
+ node: string;
153
+ port?: string;
154
+ }>;
155
+ target: {
156
+ node: string;
157
+ port: string;
158
+ };
159
+ }>;
138
160
  /** @trigger annotation — event name and/or cron schedule */
139
161
  trigger?: {
140
162
  event?: string;
@@ -287,6 +309,8 @@ export declare class JSDocParser {
287
309
  * Format: @path Start -> validator:ok -> classifier -> urgencyRouter:fail -> escalate -> Exit
288
310
  */
289
311
  private parsePathTag;
312
+ private parseFanOutTag;
313
+ private parseFanInTag;
290
314
  /**
291
315
  * Parse @trigger tag using Chevrotain parser.
292
316
  */
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { isExecutePort, isSuccessPort, isFailurePort, isScopedMandatoryPort } from './constants.js';
7
7
  import { inferDataTypeFromTS } from './type-mappings.js';
8
- import { parsePortLine, parseNodeLine, parseConnectLine, parsePositionLine, parseScopeLine, parseMapLine, parsePathLine, parseTriggerLine, parseCancelOnLine, parseThrottleLine, } from './chevrotain-parser/index.js';
8
+ import { parsePortLine, parseNodeLine, parseConnectLine, parsePositionLine, parseScopeLine, parseMapLine, parsePathLine, parseFanOutLine, parseFanInLine, parseTriggerLine, parseCancelOnLine, parseThrottleLine, } from './chevrotain-parser/index.js';
9
9
  /**
10
10
  * Extract the type of a field from a callback's return type using ts-morph Type API.
11
11
  *
@@ -260,6 +260,12 @@ export class JSDocParser {
260
260
  case 'path':
261
261
  this.parsePathTag(tag, config, warnings);
262
262
  break;
263
+ case 'fanOut':
264
+ this.parseFanOutTag(tag, config, warnings);
265
+ break;
266
+ case 'fanIn':
267
+ this.parseFanInTag(tag, config, warnings);
268
+ break;
263
269
  case 'trigger':
264
270
  this.parseTriggerTag(tag, config, warnings);
265
271
  break;
@@ -864,6 +870,40 @@ export class JSDocParser {
864
870
  steps: result.steps,
865
871
  });
866
872
  }
873
+ parseFanOutTag(tag, config, warnings) {
874
+ const comment = tag.getCommentText() || '';
875
+ const result = parseFanOutLine(`@fanOut ${comment}`, warnings);
876
+ if (!result) {
877
+ warnings.push(`Invalid @fanOut tag format: ${comment}`);
878
+ return;
879
+ }
880
+ if (!result.source.port) {
881
+ warnings.push(`@fanOut source must specify a port: ${comment}`);
882
+ return;
883
+ }
884
+ config.fanOuts = config.fanOuts || [];
885
+ config.fanOuts.push({
886
+ source: { node: result.source.node, port: result.source.port },
887
+ targets: result.targets,
888
+ });
889
+ }
890
+ parseFanInTag(tag, config, warnings) {
891
+ const comment = tag.getCommentText() || '';
892
+ const result = parseFanInLine(`@fanIn ${comment}`, warnings);
893
+ if (!result) {
894
+ warnings.push(`Invalid @fanIn tag format: ${comment}`);
895
+ return;
896
+ }
897
+ if (!result.target.port) {
898
+ warnings.push(`@fanIn target must specify a port: ${comment}`);
899
+ return;
900
+ }
901
+ config.fanIns = config.fanIns || [];
902
+ config.fanIns.push({
903
+ sources: result.sources,
904
+ target: { node: result.target.node, port: result.target.port },
905
+ });
906
+ }
867
907
  /**
868
908
  * Parse @trigger tag using Chevrotain parser.
869
909
  */
@@ -0,0 +1,35 @@
1
+ /**
2
+ * AgentChannel provides Promise-based pause/resume for workflow execution.
3
+ *
4
+ * When a workflow hits a waitForAgent node, the node calls `request()` which
5
+ * suspends execution on an unresolved Promise. The executor detects the pause
6
+ * via `onPause()`, and later calls `resume()` to resolve the Promise and
7
+ * continue execution from exactly where it paused.
8
+ */
9
+ export declare class AgentChannel {
10
+ private _resolve;
11
+ private _reject;
12
+ private _pauseResolve;
13
+ private _pausePromise;
14
+ constructor();
15
+ /**
16
+ * Called by the waitForAgent node to suspend execution.
17
+ * Returns a Promise that resolves when `resume()` is called.
18
+ */
19
+ request(agentRequest: object): Promise<object>;
20
+ /**
21
+ * Awaited by the executor to detect when the workflow pauses.
22
+ * Resolves with the agent request data from `request()`.
23
+ */
24
+ onPause(): Promise<object>;
25
+ /**
26
+ * Called by fw_resume_workflow to continue execution with the agent's result.
27
+ */
28
+ resume(result: object): void;
29
+ /**
30
+ * Called to fail a pending wait with an error.
31
+ */
32
+ fail(reason: string): void;
33
+ private _createPausePromise;
34
+ }
35
+ //# sourceMappingURL=agent-channel.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * AgentChannel provides Promise-based pause/resume for workflow execution.
3
+ *
4
+ * When a workflow hits a waitForAgent node, the node calls `request()` which
5
+ * suspends execution on an unresolved Promise. The executor detects the pause
6
+ * via `onPause()`, and later calls `resume()` to resolve the Promise and
7
+ * continue execution from exactly where it paused.
8
+ */
9
+ export class AgentChannel {
10
+ _resolve = null;
11
+ _reject = null;
12
+ _pauseResolve = null;
13
+ _pausePromise;
14
+ constructor() {
15
+ this._pausePromise = this._createPausePromise();
16
+ }
17
+ /**
18
+ * Called by the waitForAgent node to suspend execution.
19
+ * Returns a Promise that resolves when `resume()` is called.
20
+ */
21
+ async request(agentRequest) {
22
+ // Signal the executor that we're pausing
23
+ this._pauseResolve?.(agentRequest);
24
+ // Suspend on a new Promise until resume() or fail() is called
25
+ return new Promise((resolve, reject) => {
26
+ this._resolve = resolve;
27
+ this._reject = reject;
28
+ });
29
+ }
30
+ /**
31
+ * Awaited by the executor to detect when the workflow pauses.
32
+ * Resolves with the agent request data from `request()`.
33
+ */
34
+ onPause() {
35
+ return this._pausePromise;
36
+ }
37
+ /**
38
+ * Called by fw_resume_workflow to continue execution with the agent's result.
39
+ */
40
+ resume(result) {
41
+ this._resolve?.(result);
42
+ this._resolve = null;
43
+ this._reject = null;
44
+ this._pausePromise = this._createPausePromise();
45
+ }
46
+ /**
47
+ * Called to fail a pending wait with an error.
48
+ */
49
+ fail(reason) {
50
+ this._reject?.(new Error(reason));
51
+ this._resolve = null;
52
+ this._reject = null;
53
+ this._pausePromise = this._createPausePromise();
54
+ }
55
+ _createPausePromise() {
56
+ return new Promise((resolve) => {
57
+ this._pauseResolve = resolve;
58
+ });
59
+ }
60
+ }
61
+ //# sourceMappingURL=agent-channel.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * In-memory registry for pending workflow runs that are waiting for agent input.
3
+ * Used by fw_execute_workflow (to store paused runs) and fw_resume_workflow (to resume them).
4
+ */
5
+ import type { AgentChannel } from './agent-channel.js';
6
+ export interface PendingRun {
7
+ runId: string;
8
+ filePath: string;
9
+ workflowName?: string;
10
+ /** The still-pending execution promise. Resolves when workflow completes. */
11
+ executionPromise: Promise<unknown>;
12
+ agentChannel: AgentChannel;
13
+ /** The agent request data that triggered the pause. */
14
+ request?: object;
15
+ createdAt: number;
16
+ /** Temp files to clean up when the run completes or is cancelled. */
17
+ tmpFiles: string[];
18
+ }
19
+ export declare function storePendingRun(run: PendingRun): void;
20
+ export declare function getPendingRun(runId: string): PendingRun | undefined;
21
+ export declare function removePendingRun(runId: string): void;
22
+ export declare function listPendingRuns(): Array<{
23
+ runId: string;
24
+ filePath: string;
25
+ workflowName?: string;
26
+ request?: object;
27
+ createdAt: number;
28
+ }>;
29
+ //# sourceMappingURL=run-registry.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * In-memory registry for pending workflow runs that are waiting for agent input.
3
+ * Used by fw_execute_workflow (to store paused runs) and fw_resume_workflow (to resume them).
4
+ */
5
+ const pendingRuns = new Map();
6
+ export function storePendingRun(run) {
7
+ pendingRuns.set(run.runId, run);
8
+ }
9
+ export function getPendingRun(runId) {
10
+ return pendingRuns.get(runId);
11
+ }
12
+ export function removePendingRun(runId) {
13
+ pendingRuns.delete(runId);
14
+ }
15
+ export function listPendingRuns() {
16
+ return Array.from(pendingRuns.values()).map((run) => ({
17
+ runId: run.runId,
18
+ filePath: run.filePath,
19
+ workflowName: run.workflowName,
20
+ request: run.request,
21
+ createdAt: run.createdAt,
22
+ }));
23
+ }
24
+ //# sourceMappingURL=run-registry.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * MCP Diagram Tool - fw_diagram
3
3
  *
4
- * Generates SVG diagrams from workflow files.
4
+ * Generates SVG or interactive HTML diagrams from workflow files.
5
5
  */
6
6
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
7
  export declare function registerDiagramTools(mcp: McpServer): void;
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * MCP Diagram Tool - fw_diagram
3
3
  *
4
- * Generates SVG diagrams from workflow files.
4
+ * Generates SVG or interactive HTML diagrams from workflow files.
5
5
  */
6
6
  import { z } from 'zod';
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
- import { fileToSVG } from '../diagram/index.js';
9
+ import { fileToSVG, fileToHTML } from '../diagram/index.js';
10
10
  import { makeToolResult, makeErrorResult } from './response-utils.js';
11
11
  export function registerDiagramTools(mcp) {
12
12
  mcp.tool('fw_diagram', 'Generate an SVG diagram of a workflow. Returns SVG string or writes to a file.', {
@@ -27,23 +27,31 @@ export function registerDiagramTools(mcp) {
27
27
  .boolean()
28
28
  .optional()
29
29
  .describe('Show port labels on diagram (default: true)'),
30
+ format: z
31
+ .enum(['svg', 'html'])
32
+ .optional()
33
+ .describe('Output format: svg (default) or html (interactive viewer)'),
30
34
  }, async (args) => {
31
35
  try {
32
36
  const resolvedPath = path.resolve(args.filePath);
33
37
  if (!fs.existsSync(resolvedPath)) {
34
38
  return makeErrorResult('FILE_NOT_FOUND', `File not found: ${resolvedPath}`);
35
39
  }
36
- const svg = fileToSVG(resolvedPath, {
40
+ const diagramOptions = {
37
41
  workflowName: args.workflowName,
38
42
  theme: args.theme,
39
43
  showPortLabels: args.showPortLabels,
40
- });
44
+ };
45
+ const isHtml = args.format === 'html';
46
+ const result = isHtml
47
+ ? fileToHTML(resolvedPath, diagramOptions)
48
+ : fileToSVG(resolvedPath, diagramOptions);
41
49
  if (args.outputPath) {
42
50
  const outputResolved = path.resolve(args.outputPath);
43
- fs.writeFileSync(outputResolved, svg, 'utf-8');
44
- return makeToolResult({ written: outputResolved, size: svg.length });
51
+ fs.writeFileSync(outputResolved, result, 'utf-8');
52
+ return makeToolResult({ written: outputResolved, size: result.length });
45
53
  }
46
- return makeToolResult(svg);
54
+ return makeToolResult(result);
47
55
  }
48
56
  catch (error) {
49
57
  return makeErrorResult('DIAGRAM_ERROR', `Diagram generation failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { makeToolResult, makeErrorResult } from './response-utils.js';
3
3
  import { executeWorkflowFromFile } from './workflow-executor.js';
4
+ import { AgentChannel } from './agent-channel.js';
5
+ import { storePendingRun, getPendingRun, removePendingRun, listPendingRuns } from './run-registry.js';
4
6
  /**
5
7
  * Unwrap editor ack responses to flatten double-nested results.
6
8
  * Editor returns { requestId, success, result: { actualData } } —
@@ -116,7 +118,8 @@ export function registerEditorTools(mcp, connection, buffer) {
116
118
  });
117
119
  mcp.tool('fw_execute_workflow', 'Run the current workflow with optional parameters and return the result. ' +
118
120
  'Includes per-node execution trace by default (STATUS_CHANGED, VARIABLE_SET events) — ' +
119
- 'use includeTrace: false to disable.', {
121
+ 'use includeTrace: false to disable. If the workflow pauses at a waitForAgent node, ' +
122
+ 'returns immediately with status "waiting" and a runId — use fw_resume_workflow to continue.', {
120
123
  filePath: z
121
124
  .string()
122
125
  .optional()
@@ -134,11 +137,39 @@ export function registerEditorTools(mcp, connection, buffer) {
134
137
  // When filePath is provided, compile and execute directly (no editor needed)
135
138
  if (args.filePath) {
136
139
  try {
137
- const execResult = await executeWorkflowFromFile(args.filePath, args.params, {
140
+ const channel = new AgentChannel();
141
+ const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2)}`;
142
+ const execPromise = executeWorkflowFromFile(args.filePath, args.params, {
138
143
  workflowName: args.workflowName,
139
144
  includeTrace: args.includeTrace,
145
+ agentChannel: channel,
140
146
  });
141
- return makeToolResult(execResult);
147
+ // Race between workflow completing and workflow pausing
148
+ const raceResult = await Promise.race([
149
+ execPromise.then((r) => ({ type: 'completed', result: r })),
150
+ channel.onPause().then((req) => ({ type: 'paused', request: req })),
151
+ ]);
152
+ if (raceResult.type === 'paused') {
153
+ // Store the pending run for later resumption
154
+ storePendingRun({
155
+ runId,
156
+ filePath: args.filePath,
157
+ workflowName: args.workflowName,
158
+ executionPromise: execPromise,
159
+ agentChannel: channel,
160
+ request: raceResult.request,
161
+ createdAt: Date.now(),
162
+ tmpFiles: [], // executor manages its own cleanup
163
+ });
164
+ return makeToolResult({
165
+ status: 'waiting',
166
+ runId,
167
+ request: raceResult.request,
168
+ message: 'Workflow paused at waitForAgent node. Use fw_resume_workflow to continue.',
169
+ });
170
+ }
171
+ // Completed without pausing — return flat result for backward compatibility
172
+ return makeToolResult(raceResult.result);
142
173
  }
143
174
  catch (err) {
144
175
  const message = err instanceof Error ? err.message : String(err);
@@ -154,6 +185,47 @@ export function registerEditorTools(mcp, connection, buffer) {
154
185
  const result = await connection.sendCommand('execute-workflow', args.params ?? {});
155
186
  return makeToolResult(unwrapAckResult(result));
156
187
  });
188
+ mcp.tool('fw_resume_workflow', 'Resume a paused workflow that is waiting for agent input. ' +
189
+ 'Use this after fw_execute_workflow returns status "waiting".', {
190
+ runId: z.string().describe('The runId from the waiting execution result'),
191
+ result: z.record(z.unknown()).describe('The agent result to send back to the workflow'),
192
+ }, async (args) => {
193
+ const run = getPendingRun(args.runId);
194
+ if (!run) {
195
+ return makeErrorResult('RUN_NOT_FOUND', `No pending run found with ID "${args.runId}". It may have already completed or been cancelled.`);
196
+ }
197
+ try {
198
+ // Resume the workflow by resolving the agent channel's Promise
199
+ run.agentChannel.resume(args.result);
200
+ // Wait for the workflow to either complete or pause again
201
+ const raceResult = await Promise.race([
202
+ run.executionPromise.then((r) => ({ type: 'completed', result: r })),
203
+ run.agentChannel.onPause().then((req) => ({ type: 'paused', request: req })),
204
+ ]);
205
+ if (raceResult.type === 'paused') {
206
+ // Workflow paused again at another waitForAgent node
207
+ run.request = raceResult.request;
208
+ return makeToolResult({
209
+ status: 'waiting',
210
+ runId: args.runId,
211
+ request: raceResult.request,
212
+ message: 'Workflow paused again at another waitForAgent node.',
213
+ });
214
+ }
215
+ // Workflow completed
216
+ removePendingRun(args.runId);
217
+ return makeToolResult({ status: 'completed', result: raceResult.result });
218
+ }
219
+ catch (err) {
220
+ removePendingRun(args.runId);
221
+ const message = err instanceof Error ? err.message : String(err);
222
+ return makeErrorResult('EXECUTION_ERROR', message);
223
+ }
224
+ });
225
+ mcp.tool('fw_list_pending_runs', 'List workflows that are currently paused waiting for agent input.', {}, async () => {
226
+ const runs = listPendingRuns();
227
+ return makeToolResult(runs);
228
+ });
157
229
  mcp.tool('fw_get_workflow_details', 'Get full workflow structure including nodes, connections, types, and positions.', {}, async () => {
158
230
  if (!connection.isConnected) {
159
231
  return makeErrorResult('EDITOR_NOT_CONNECTED', 'Not connected to the editor. Is the editor running?');
@@ -3,6 +3,7 @@
3
3
  * Copies source to a temp file, compiles all workflows in-place, then dynamically imports and executes.
4
4
  */
5
5
  import type { FwMockConfig } from '../built-in-nodes/mock-types.js';
6
+ import type { AgentChannel } from './agent-channel.js';
6
7
  /** A single trace event captured during workflow execution. */
7
8
  export interface ExecutionTraceEvent {
8
9
  /** The event type (e.g. "NODE_STARTED", "NODE_COMPLETED"). */
@@ -12,6 +13,28 @@ export interface ExecutionTraceEvent {
12
13
  /** Additional event data. */
13
14
  data?: Record<string, unknown>;
14
15
  }
16
+ /** Per-node timing from a trace summary. */
17
+ export interface NodeTiming {
18
+ /** The node instance ID. */
19
+ nodeId: string;
20
+ /** Duration from RUNNING to terminal status, in milliseconds. */
21
+ durationMs: number;
22
+ }
23
+ /** Summary of workflow execution derived from trace events. */
24
+ export interface TraceSummary {
25
+ /** Number of unique nodes that emitted STATUS_CHANGED events. */
26
+ totalNodes: number;
27
+ /** Nodes that reached SUCCEEDED status. */
28
+ succeeded: number;
29
+ /** Nodes that reached FAILED status. */
30
+ failed: number;
31
+ /** Nodes that reached CANCELLED status. */
32
+ cancelled: number;
33
+ /** Per-node timings (RUNNING → terminal status). */
34
+ nodeTimings: NodeTiming[];
35
+ /** Wall-clock duration from first to last trace event, in milliseconds. */
36
+ totalDurationMs: number;
37
+ }
15
38
  /** Result returned after executing a workflow from a file. */
16
39
  export interface ExecuteWorkflowResult {
17
40
  /** The return value of the executed workflow function. */
@@ -22,6 +45,8 @@ export interface ExecuteWorkflowResult {
22
45
  executionTime: number;
23
46
  /** Execution trace events, included when `includeTrace` is enabled. */
24
47
  trace?: ExecutionTraceEvent[];
48
+ /** Summary of trace events, included when `includeTrace` is enabled. */
49
+ summary?: TraceSummary;
25
50
  }
26
51
  /**
27
52
  * Compiles and executes a workflow from a TypeScript source file.
@@ -43,5 +68,8 @@ export declare function executeWorkflowFromFile(filePath: string, params?: Recor
43
68
  production?: boolean;
44
69
  includeTrace?: boolean;
45
70
  mocks?: FwMockConfig;
71
+ agentChannel?: AgentChannel;
46
72
  }): Promise<ExecuteWorkflowResult>;
73
+ /** Compute a concise summary from raw trace events. */
74
+ export declare function computeTraceSummary(trace: ExecutionTraceEvent[]): TraceSummary;
47
75
  //# sourceMappingURL=workflow-executor.d.ts.map