@synergenius/flow-weaver 0.2.1 → 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 (83) hide show
  1. package/README.md +261 -200
  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/docs.d.ts +11 -0
  24. package/dist/cli/commands/docs.js +77 -0
  25. package/dist/cli/commands/run.js +59 -1
  26. package/dist/cli/flow-weaver.mjs +2447 -594
  27. package/dist/cli/index.js +40 -2
  28. package/dist/diagram/geometry.d.ts +9 -4
  29. package/dist/diagram/geometry.js +262 -31
  30. package/dist/diagram/html-viewer.d.ts +12 -0
  31. package/dist/diagram/html-viewer.js +399 -0
  32. package/dist/diagram/index.d.ts +12 -0
  33. package/dist/diagram/index.js +22 -0
  34. package/dist/diagram/renderer.js +137 -116
  35. package/dist/diagram/types.d.ts +1 -0
  36. package/dist/doc-metadata/extractors/annotations.js +282 -1
  37. package/dist/doc-metadata/types.d.ts +6 -0
  38. package/dist/docs/index.d.ts +54 -0
  39. package/dist/docs/index.js +256 -0
  40. package/dist/generator/control-flow.d.ts +13 -0
  41. package/dist/generator/control-flow.js +74 -0
  42. package/dist/generator/inngest.js +23 -0
  43. package/dist/generator/unified.js +122 -2
  44. package/dist/jsdoc-parser.d.ts +24 -0
  45. package/dist/jsdoc-parser.js +41 -1
  46. package/dist/mcp/agent-channel.d.ts +35 -0
  47. package/dist/mcp/agent-channel.js +61 -0
  48. package/dist/mcp/run-registry.d.ts +29 -0
  49. package/dist/mcp/run-registry.js +24 -0
  50. package/dist/mcp/server.js +2 -0
  51. package/dist/mcp/tools-diagram.d.ts +1 -1
  52. package/dist/mcp/tools-diagram.js +15 -7
  53. package/dist/mcp/tools-docs.d.ts +3 -0
  54. package/dist/mcp/tools-docs.js +62 -0
  55. package/dist/mcp/tools-editor.js +77 -3
  56. package/dist/mcp/tools-query.js +3 -1
  57. package/dist/mcp/workflow-executor.d.ts +28 -0
  58. package/dist/mcp/workflow-executor.js +66 -3
  59. package/dist/parser.d.ts +8 -0
  60. package/dist/parser.js +100 -0
  61. package/dist/runtime/ExecutionContext.d.ts +2 -0
  62. package/dist/runtime/ExecutionContext.js +2 -0
  63. package/dist/runtime/events.d.ts +1 -1
  64. package/dist/sugar-optimizer.js +28 -3
  65. package/dist/validator.d.ts +8 -0
  66. package/dist/validator.js +92 -0
  67. package/docs/reference/advanced-annotations.md +431 -0
  68. package/docs/reference/built-in-nodes.md +225 -0
  69. package/docs/reference/cli-reference.md +882 -0
  70. package/docs/reference/compilation.md +351 -0
  71. package/docs/reference/concepts.md +400 -0
  72. package/docs/reference/debugging.md +255 -0
  73. package/docs/reference/deployment.md +207 -0
  74. package/docs/reference/error-codes.md +686 -0
  75. package/docs/reference/export-interface.md +229 -0
  76. package/docs/reference/iterative-development.md +186 -0
  77. package/docs/reference/jsdoc-grammar.md +471 -0
  78. package/docs/reference/marketplace.md +205 -0
  79. package/docs/reference/node-conversion.md +308 -0
  80. package/docs/reference/patterns.md +161 -0
  81. package/docs/reference/scaffold.md +160 -0
  82. package/docs/reference/tutorial.md +519 -0
  83. package/package.json +10 -4
@@ -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
@@ -11,6 +11,7 @@ import { registerPatternTools } from './tools-pattern.js';
11
11
  import { registerExportTools } from './tools-export.js';
12
12
  import { registerMarketplaceTools } from './tools-marketplace.js';
13
13
  import { registerDiagramTools } from './tools-diagram.js';
14
+ import { registerDocsTools } from './tools-docs.js';
14
15
  import { registerResources } from './resources.js';
15
16
  function parseEventFilterFromEnv() {
16
17
  const filter = {};
@@ -70,6 +71,7 @@ export async function startMcpServer(options) {
70
71
  registerExportTools(mcp);
71
72
  registerMarketplaceTools(mcp);
72
73
  registerDiagramTools(mcp);
74
+ registerDocsTools(mcp);
73
75
  registerResources(mcp, connection, buffer);
74
76
  // Connect transport (only in stdio MCP mode)
75
77
  if (!options._testDeps && options.stdio) {
@@ -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)}`);
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerDocsTools(mcp: McpServer): void;
3
+ //# sourceMappingURL=tools-docs.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod';
2
+ import { listTopics, readTopic, searchDocs } from '../docs/index.js';
3
+ import { makeToolResult, makeErrorResult } from './response-utils.js';
4
+ export function registerDocsTools(mcp) {
5
+ mcp.tool('fw_docs', 'Browse Flow Weaver documentation and reference guides. Use action="list" to see topics, action="read" to read a topic, action="search" to search across all docs.', {
6
+ action: z.enum(['list', 'read', 'search']).describe('What to do: list topics, read a topic, or search'),
7
+ topic: z.string().optional().describe('Topic slug to read (for action="read")'),
8
+ query: z.string().optional().describe('Search query (for action="search")'),
9
+ compact: z.boolean().optional().describe('Return compact LLM-friendly version (default: false)'),
10
+ }, async (args) => {
11
+ try {
12
+ switch (args.action) {
13
+ case 'list': {
14
+ const topics = listTopics();
15
+ return makeToolResult({
16
+ topics: topics.map((t) => ({
17
+ slug: t.slug,
18
+ name: t.name,
19
+ description: t.description,
20
+ keywords: t.keywords,
21
+ })),
22
+ });
23
+ }
24
+ case 'read': {
25
+ if (!args.topic) {
26
+ return makeErrorResult('MISSING_PARAM', 'The "topic" parameter is required for action="read"');
27
+ }
28
+ const doc = readTopic(args.topic, args.compact ?? false);
29
+ if (!doc) {
30
+ const available = listTopics().map((t) => t.slug);
31
+ return makeErrorResult('TOPIC_NOT_FOUND', `Unknown topic "${args.topic}". Available topics: ${available.join(', ')}`);
32
+ }
33
+ return makeToolResult({
34
+ name: doc.name,
35
+ description: doc.description,
36
+ content: doc.content,
37
+ });
38
+ }
39
+ case 'search': {
40
+ if (!args.query) {
41
+ return makeErrorResult('MISSING_PARAM', 'The "query" parameter is required for action="search"');
42
+ }
43
+ const results = searchDocs(args.query);
44
+ return makeToolResult({
45
+ query: args.query,
46
+ results: results.slice(0, 20).map((r) => ({
47
+ topic: r.topic,
48
+ slug: r.slug,
49
+ heading: r.heading,
50
+ excerpt: r.excerpt,
51
+ relevance: r.relevance,
52
+ })),
53
+ });
54
+ }
55
+ }
56
+ }
57
+ catch (err) {
58
+ return makeErrorResult('DOCS_ERROR', `fw_docs failed: ${err instanceof Error ? err.message : String(err)}`);
59
+ }
60
+ });
61
+ }
62
+ //# sourceMappingURL=tools-docs.js.map
@@ -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 } } —
@@ -114,7 +116,10 @@ export function registerEditorTools(mcp, connection, buffer) {
114
116
  const result = await connection.sendCommand(args.action, {});
115
117
  return makeToolResult(unwrapAckResult(result));
116
118
  });
117
- mcp.tool('fw_execute_workflow', 'Run the current workflow with optional parameters and return the result.', {
119
+ mcp.tool('fw_execute_workflow', 'Run the current workflow with optional parameters and return the result. ' +
120
+ 'Includes per-node execution trace by default (STATUS_CHANGED, VARIABLE_SET events) — ' +
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.', {
118
123
  filePath: z
119
124
  .string()
120
125
  .optional()
@@ -132,11 +137,39 @@ export function registerEditorTools(mcp, connection, buffer) {
132
137
  // When filePath is provided, compile and execute directly (no editor needed)
133
138
  if (args.filePath) {
134
139
  try {
135
- 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, {
136
143
  workflowName: args.workflowName,
137
144
  includeTrace: args.includeTrace,
145
+ agentChannel: channel,
138
146
  });
139
- 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);
140
173
  }
141
174
  catch (err) {
142
175
  const message = err instanceof Error ? err.message : String(err);
@@ -152,6 +185,47 @@ export function registerEditorTools(mcp, connection, buffer) {
152
185
  const result = await connection.sendCommand('execute-workflow', args.params ?? {});
153
186
  return makeToolResult(unwrapAckResult(result));
154
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
+ });
155
229
  mcp.tool('fw_get_workflow_details', 'Get full workflow structure including nodes, connections, types, and positions.', {}, async () => {
156
230
  if (!connection.isConnected) {
157
231
  return makeErrorResult('EDITOR_NOT_CONNECTED', 'Not connected to the editor. Is the editor running?');
@@ -128,7 +128,9 @@ export function registerQueryTools(mcp) {
128
128
  return makeErrorResult('VALIDATE_ERROR', `fw_validate failed: ${err instanceof Error ? err.message : String(err)}`);
129
129
  }
130
130
  });
131
- mcp.tool('fw_compile', 'Compile a workflow to executable code. Use target=inngest for per-node step.run() durability.', {
131
+ mcp.tool('fw_compile', 'Compile a workflow to executable code. Only regenerates code inside @flow-weaver-runtime ' +
132
+ 'and @flow-weaver-body marker sections — user code outside markers is preserved. ' +
133
+ 'Set production: true to strip debug instrumentation. Use target=inngest for per-node step.run() durability.', {
132
134
  filePath: z.string().describe('Path to the workflow file'),
133
135
  write: z.boolean().optional().describe('Whether to write the output file (default: true)'),
134
136
  production: z
@@ -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
@@ -5,6 +5,7 @@
5
5
  import * as path from 'path';
6
6
  import * as fs from 'fs';
7
7
  import * as os from 'os';
8
+ import { pathToFileURL } from 'url';
8
9
  import ts from 'typescript';
9
10
  import { compileWorkflow } from '../api/index.js';
10
11
  import { getAvailableWorkflows } from '../api/workflow-file-operations.js';
@@ -81,8 +82,21 @@ export async function executeWorkflowFromFile(filePath, params, options) {
81
82
  if (options?.mocks) {
82
83
  globalThis.__fw_mocks__ = options.mocks;
83
84
  }
84
- // Dynamic import (tsx runtime supports .ts imports)
85
- const mod = await import(tmpFile);
85
+ // Set agent channel for waitForAgent pause/resume
86
+ if (options?.agentChannel) {
87
+ globalThis.__fw_agent_channel__ = options.agentChannel;
88
+ }
89
+ // Dynamic import using file:// URL for cross-platform compatibility
90
+ // (Windows paths like C:\... break with bare import() — "Received protocol 'c:'")
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;
86
100
  // Find the target exported function
87
101
  const exportedFn = findExportedFunction(mod, options?.workflowName);
88
102
  if (!exportedFn) {
@@ -97,13 +111,15 @@ export async function executeWorkflowFromFile(filePath, params, options) {
97
111
  result,
98
112
  functionName: exportedFn.name,
99
113
  executionTime,
100
- ...(includeTrace && { trace }),
114
+ ...(includeTrace && { trace, summary: computeTraceSummary(trace) }),
101
115
  };
102
116
  }
103
117
  finally {
104
118
  // Clean up globals
105
119
  delete globalThis.__fw_debugger__;
106
120
  delete globalThis.__fw_mocks__;
121
+ delete globalThis.__fw_workflow_registry__;
122
+ delete globalThis.__fw_agent_channel__;
107
123
  // Clean up temp files
108
124
  try {
109
125
  fs.unlinkSync(tmpFile);
@@ -115,6 +131,53 @@ export async function executeWorkflowFromFile(filePath, params, options) {
115
131
  catch { /* ignore */ }
116
132
  }
117
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
+ }
118
181
  function findExportedFunction(mod, preferredName) {
119
182
  // If a preferred name is specified, try it first
120
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: