@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.
- package/README.md +1 -0
- package/dist/annotation-generator.js +36 -0
- package/dist/api/generate-in-place.js +39 -0
- package/dist/api/generate.js +11 -1
- package/dist/api/manipulation/nodes.js +22 -0
- package/dist/ast/types.d.ts +27 -1
- package/dist/built-in-nodes/index.d.ts +1 -0
- package/dist/built-in-nodes/index.js +1 -0
- package/dist/built-in-nodes/invoke-workflow.js +12 -1
- package/dist/built-in-nodes/mock-types.d.ts +2 -0
- package/dist/built-in-nodes/wait-for-agent.d.ts +13 -0
- package/dist/built-in-nodes/wait-for-agent.js +26 -0
- package/dist/chevrotain-parser/fan-parser.d.ts +38 -0
- package/dist/chevrotain-parser/fan-parser.js +149 -0
- package/dist/chevrotain-parser/grammar-diagrams.d.ts +1 -0
- package/dist/chevrotain-parser/grammar-diagrams.js +3 -0
- package/dist/chevrotain-parser/index.d.ts +3 -1
- package/dist/chevrotain-parser/index.js +3 -1
- package/dist/chevrotain-parser/tokens.d.ts +2 -0
- package/dist/chevrotain-parser/tokens.js +10 -0
- package/dist/cli/commands/diagram.d.ts +2 -1
- package/dist/cli/commands/diagram.js +9 -6
- package/dist/cli/commands/run.js +59 -1
- package/dist/cli/flow-weaver.mjs +1396 -77
- package/dist/cli/index.js +23 -36
- package/dist/diagram/geometry.js +47 -5
- package/dist/diagram/html-viewer.d.ts +12 -0
- package/dist/diagram/html-viewer.js +399 -0
- package/dist/diagram/index.d.ts +12 -0
- package/dist/diagram/index.js +22 -0
- package/dist/diagram/types.d.ts +1 -0
- package/dist/doc-metadata/extractors/annotations.js +282 -1
- package/dist/doc-metadata/types.d.ts +6 -0
- package/dist/generator/control-flow.d.ts +13 -0
- package/dist/generator/control-flow.js +74 -0
- package/dist/generator/inngest.js +23 -0
- package/dist/generator/unified.js +122 -2
- package/dist/jsdoc-parser.d.ts +24 -0
- package/dist/jsdoc-parser.js +41 -1
- package/dist/mcp/agent-channel.d.ts +35 -0
- package/dist/mcp/agent-channel.js +61 -0
- package/dist/mcp/run-registry.d.ts +29 -0
- package/dist/mcp/run-registry.js +24 -0
- package/dist/mcp/tools-diagram.d.ts +1 -1
- package/dist/mcp/tools-diagram.js +15 -7
- package/dist/mcp/tools-editor.js +75 -3
- package/dist/mcp/workflow-executor.d.ts +28 -0
- package/dist/mcp/workflow-executor.js +62 -1
- package/dist/parser.d.ts +8 -0
- package/dist/parser.js +100 -0
- package/dist/runtime/ExecutionContext.d.ts +2 -0
- package/dist/runtime/ExecutionContext.js +2 -0
- package/dist/runtime/events.d.ts +1 -1
- package/dist/sugar-optimizer.js +28 -3
- package/dist/validator.d.ts +8 -0
- package/dist/validator.js +92 -0
- package/docs/reference/advanced-annotations.md +431 -0
- package/docs/reference/built-in-nodes.md +225 -0
- package/docs/reference/cli-reference.md +882 -0
- package/docs/reference/compilation.md +351 -0
- package/docs/reference/concepts.md +400 -0
- package/docs/reference/debugging.md +255 -0
- package/docs/reference/deployment.md +207 -0
- package/docs/reference/error-codes.md +686 -0
- package/docs/reference/export-interface.md +229 -0
- package/docs/reference/iterative-development.md +186 -0
- package/docs/reference/jsdoc-grammar.md +471 -0
- package/docs/reference/marketplace.md +205 -0
- package/docs/reference/node-conversion.md +308 -0
- package/docs/reference/patterns.md +161 -0
- package/docs/reference/scaffold.md +160 -0
- package/docs/reference/tutorial.md +519 -0
- 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
|
*
|
package/dist/jsdoc-parser.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/jsdoc-parser.js
CHANGED
|
@@ -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
|
|
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,
|
|
44
|
-
return makeToolResult({ written: outputResolved, size:
|
|
51
|
+
fs.writeFileSync(outputResolved, result, 'utf-8');
|
|
52
|
+
return makeToolResult({ written: outputResolved, size: result.length });
|
|
45
53
|
}
|
|
46
|
-
return makeToolResult(
|
|
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)}`);
|
package/dist/mcp/tools-editor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|