@synergenius/flow-weaver 0.5.1 → 0.6.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.
package/README.md CHANGED
@@ -218,7 +218,7 @@ flow-weaver export workflow.ts --target cloudflare --output deploy/
218
218
  flow-weaver serve ./workflows --port 3000 --swagger
219
219
  ```
220
220
 
221
- Inngest compilation wraps each node in `step.run()` for individual durability, parallelizes independent nodes with `Promise.all()`, and generates typed Zod event schemas.
221
+ Both the default TypeScript target and Inngest target parallelize independent nodes with `Promise.all()`. Inngest additionally wraps each node in `step.run()` for individual durability and generates typed Zod event schemas.
222
222
 
223
223
  ## Visual Human-in-the-Loop
224
224
 
@@ -319,6 +319,7 @@ flow-weaver listen # Stream editor events
319
319
  |------|---------|
320
320
  | `delay` | Sleep for a duration (ms, s, m, h, d). Mockable for fast testing. |
321
321
  | `waitForEvent` | Wait for an external event with optional field matching and timeout. Maps to Inngest `step.waitForEvent()` for zero-cost durable pauses. |
322
+ | `waitForAgent` | Pause execution and wait for an external agent to provide a result. Supports multi-step human-in-the-loop and agent delegation patterns. |
322
323
  | `invokeWorkflow` | Invoke another workflow by ID with payload and timeout. Maps to Inngest `step.invoke()`. |
323
324
 
324
325
  ## STEP Port Architecture
@@ -20,6 +20,10 @@ export class AnnotationGenerator {
20
20
  if (nodeType.variant === 'MAP_ITERATOR') {
21
21
  return;
22
22
  }
23
+ // Skip synthetic COERCION node types — they're generated from @coerce macros
24
+ if (nodeType.variant === 'COERCION') {
25
+ return;
26
+ }
23
27
  lines.push(...this.generateNodeTypeAnnotation(nodeType, indent, includeComments, includeMetadata));
24
28
  lines.push("");
25
29
  });
@@ -136,10 +140,11 @@ export class AnnotationGenerator {
136
140
  }
137
141
  generateWorkflowAnnotation(workflow, indent, includeComments, skipParamReturns = false) {
138
142
  const lines = [];
139
- // Build macro coverage sets for filtering (@map-specific)
143
+ // Build macro coverage sets for filtering (@map and @coerce)
140
144
  const macroInstanceIds = new Set();
141
145
  const macroChildIds = new Set();
142
146
  const macroScopeNames = new Set();
147
+ const coerceInstanceIds = new Set();
143
148
  if (workflow.macros && workflow.macros.length > 0) {
144
149
  for (const macro of workflow.macros) {
145
150
  if (macro.type === 'map') {
@@ -147,6 +152,9 @@ export class AnnotationGenerator {
147
152
  macroChildIds.add(macro.childId);
148
153
  macroScopeNames.add(`${macro.instanceId}.iterate`);
149
154
  }
155
+ else if (macro.type === 'coerce') {
156
+ coerceInstanceIds.add(macro.instanceId);
157
+ }
150
158
  }
151
159
  }
152
160
  // Generate JSDoc comment block
@@ -211,10 +219,12 @@ export class AnnotationGenerator {
211
219
  if (workflow.description && includeComments) {
212
220
  lines.push(` * @description ${workflow.description}`);
213
221
  }
214
- // Add node instances — skip synthetic MAP_ITERATOR instances, strip parent from macro children
222
+ // Add node instances — skip synthetic MAP_ITERATOR/COERCION instances, strip parent from macro children
215
223
  workflow.instances.forEach((instance) => {
216
224
  if (macroInstanceIds.has(instance.id))
217
225
  return;
226
+ if (coerceInstanceIds.has(instance.id))
227
+ return;
218
228
  if (macroChildIds.has(instance.id) && instance.parent) {
219
229
  const stripped = { ...instance, parent: undefined };
220
230
  lines.push(generateNodeInstanceTag(stripped));
@@ -257,6 +267,11 @@ export class AnnotationGenerator {
257
267
  const tgt = `${macro.target.node}.${macro.target.port}`;
258
268
  lines.push(` * @fanIn ${srcs} -> ${tgt}`);
259
269
  }
270
+ else if (macro.type === 'coerce') {
271
+ const src = `${macro.source.node}.${macro.source.port}`;
272
+ const tgt = `${macro.target.node}.${macro.target.port}`;
273
+ lines.push(` * @coerce ${macro.instanceId} ${src} -> ${tgt} as ${macro.targetType}`);
274
+ }
260
275
  }
261
276
  }
262
277
  // Add node positions (if they exist)
@@ -492,6 +507,14 @@ function isConnectionCoveredByMacroStatic(conn, macros) {
492
507
  }
493
508
  }
494
509
  }
510
+ else if (macro.type === 'coerce') {
511
+ if (conn.from.scope || conn.to.scope)
512
+ continue;
513
+ // Connections to/from the synthetic coercion instance are covered
514
+ if (conn.to.node === macro.instanceId || conn.from.node === macro.instanceId) {
515
+ return true;
516
+ }
517
+ }
495
518
  }
496
519
  return false;
497
520
  }
@@ -111,7 +111,7 @@ export type TWorkflowAST = {
111
111
  * A sugar macro that expands to a full scope pattern or connection set.
112
112
  * Stored on the workflow AST for round-trip preservation in generateInPlace.
113
113
  */
114
- export type TWorkflowMacro = TMapMacro | TPathMacro | TFanOutMacro | TFanInMacro;
114
+ export type TWorkflowMacro = TMapMacro | TPathMacro | TFanOutMacro | TFanInMacro | TCoerceMacro;
115
115
  export type TMapMacro = {
116
116
  type: 'map';
117
117
  /** Instance ID for the synthetic iterator node (e.g., "loop") */
@@ -159,6 +159,24 @@ export type TFanInMacro = {
159
159
  port: string;
160
160
  };
161
161
  };
162
+ export type TCoerceTargetType = 'string' | 'number' | 'boolean' | 'json' | 'object';
163
+ export type TCoerceMacro = {
164
+ type: 'coerce';
165
+ /** Instance ID for the synthetic coercion node */
166
+ instanceId: string;
167
+ /** Source port in "node.port" format */
168
+ source: {
169
+ node: string;
170
+ port: string;
171
+ };
172
+ /** Target port in "node.port" format */
173
+ target: {
174
+ node: string;
175
+ port: string;
176
+ };
177
+ /** Coercion target type */
178
+ targetType: TCoerceTargetType;
179
+ };
162
180
  /**
163
181
  * Default configuration for a node type that can be overridden per instance.
164
182
  */
@@ -222,7 +240,7 @@ export type TNodeTypeAST = {
222
240
  /** Multiple scopes this node creates */
223
241
  scopes?: string[];
224
242
  /** Variant identifier (set by app layer) */
225
- variant?: 'FUNCTION' | 'WORKFLOW' | 'IMPORTED_WORKFLOW' | 'MAP_ITERATOR';
243
+ variant?: 'FUNCTION' | 'WORKFLOW' | 'IMPORTED_WORKFLOW' | 'MAP_ITERATOR' | 'COERCION';
226
244
  /** File path for external node types */
227
245
  path?: string;
228
246
  /** Function reference for function-based node types */
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Synthetic node type definitions for explicit type coercion.
3
+ *
4
+ * These are compiler-internal node types created by @coerce macros.
5
+ * They generate inline JavaScript coercion expressions instead of function calls.
6
+ */
7
+ import type { TNodeTypeAST, TCoerceTargetType } from '../ast/types.js';
8
+ /** Maps coercion target type keyword to the synthetic node type name */
9
+ export declare const COERCE_TYPE_MAP: Record<TCoerceTargetType, string>;
10
+ /** Maps synthetic node type name to the inline JS expression */
11
+ export declare const COERCE_EXPRESSIONS: Record<string, string>;
12
+ export declare const COERCION_NODE_TYPES: Record<string, TNodeTypeAST>;
13
+ /** Check if a function name is a coercion node type */
14
+ export declare function isCoercionNode(functionName: string): boolean;
15
+ //# sourceMappingURL=coercion-types.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Synthetic node type definitions for explicit type coercion.
3
+ *
4
+ * These are compiler-internal node types created by @coerce macros.
5
+ * They generate inline JavaScript coercion expressions instead of function calls.
6
+ */
7
+ /** Maps coercion target type keyword to the synthetic node type name */
8
+ export const COERCE_TYPE_MAP = {
9
+ string: '__fw_toString',
10
+ number: '__fw_toNumber',
11
+ boolean: '__fw_toBoolean',
12
+ json: '__fw_toJSON',
13
+ object: '__fw_parseJSON',
14
+ };
15
+ /** Maps synthetic node type name to the inline JS expression */
16
+ export const COERCE_EXPRESSIONS = {
17
+ __fw_toString: 'String',
18
+ __fw_toNumber: 'Number',
19
+ __fw_toBoolean: 'Boolean',
20
+ __fw_toJSON: 'JSON.stringify',
21
+ __fw_parseJSON: 'JSON.parse',
22
+ };
23
+ function makeCoercionNodeType(name, outputType, outputTsType) {
24
+ return {
25
+ type: 'NodeType',
26
+ name,
27
+ functionName: name,
28
+ expression: true,
29
+ isAsync: false,
30
+ hasSuccessPort: true,
31
+ hasFailurePort: true,
32
+ executeWhen: 'CONJUNCTION',
33
+ variant: 'COERCION',
34
+ inputs: {
35
+ execute: { dataType: 'STEP', label: 'Execute' },
36
+ value: { dataType: 'ANY', label: 'Value', tsType: 'unknown' },
37
+ },
38
+ outputs: {
39
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
40
+ onFailure: { dataType: 'STEP', label: 'On Failure', failure: true, isControlFlow: true },
41
+ result: { dataType: outputType, label: 'Result', tsType: outputTsType },
42
+ },
43
+ visuals: {
44
+ color: '#0d9488',
45
+ icon: 'transform',
46
+ tags: [{ label: 'coerce' }],
47
+ },
48
+ };
49
+ }
50
+ export const COERCION_NODE_TYPES = {
51
+ __fw_toString: makeCoercionNodeType('__fw_toString', 'STRING', 'string'),
52
+ __fw_toNumber: makeCoercionNodeType('__fw_toNumber', 'NUMBER', 'number'),
53
+ __fw_toBoolean: makeCoercionNodeType('__fw_toBoolean', 'BOOLEAN', 'boolean'),
54
+ __fw_toJSON: makeCoercionNodeType('__fw_toJSON', 'STRING', 'string'),
55
+ __fw_parseJSON: makeCoercionNodeType('__fw_parseJSON', 'OBJECT', 'object'),
56
+ };
57
+ /** Check if a function name is a coercion node type */
58
+ export function isCoercionNode(functionName) {
59
+ return functionName in COERCION_NODE_TYPES;
60
+ }
61
+ //# sourceMappingURL=coercion-types.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @module chevrotain-parser/coerce-parser
3
+ *
4
+ * Parser for @coerce sugar annotation using Chevrotain.
5
+ *
6
+ * Syntax:
7
+ * @coerce instanceId sourceNode.port -> targetNode.port as targetType
8
+ *
9
+ * Where targetType is one of: string, number, boolean, json, object
10
+ */
11
+ import type { TCoerceTargetType } from '../ast/types.js';
12
+ export interface CoerceParseResult {
13
+ instanceId: string;
14
+ source: {
15
+ node: string;
16
+ port: string;
17
+ };
18
+ target: {
19
+ node: string;
20
+ port: string;
21
+ };
22
+ targetType: TCoerceTargetType;
23
+ }
24
+ /**
25
+ * Parse a @coerce line and return structured result.
26
+ * Returns null if the line is not a valid @coerce declaration.
27
+ */
28
+ export declare function parseCoerceLine(input: string, warnings: string[]): CoerceParseResult | null;
29
+ /**
30
+ * Get serialized grammar for documentation/diagram generation.
31
+ */
32
+ export declare function getCoerceGrammar(): import("chevrotain").ISerializedGast[];
33
+ //# sourceMappingURL=coerce-parser.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @module chevrotain-parser/coerce-parser
3
+ *
4
+ * Parser for @coerce sugar annotation using Chevrotain.
5
+ *
6
+ * Syntax:
7
+ * @coerce instanceId sourceNode.port -> targetNode.port as targetType
8
+ *
9
+ * Where targetType is one of: string, number, boolean, json, object
10
+ */
11
+ import { CstParser } from 'chevrotain';
12
+ import { JSDocLexer, CoerceTag, Identifier, Arrow, Dot, AsKeyword, allTokens, } from './tokens.js';
13
+ const VALID_COERCE_TYPES = new Set(['string', 'number', 'boolean', 'json', 'object']);
14
+ // =============================================================================
15
+ // Parser Definition
16
+ // =============================================================================
17
+ class CoerceParser extends CstParser {
18
+ constructor() {
19
+ super(allTokens);
20
+ this.performSelfAnalysis();
21
+ }
22
+ // @coerce Identifier portRef Arrow portRef AsKeyword Identifier
23
+ coerceLine = this.RULE('coerceLine', () => {
24
+ this.CONSUME(CoerceTag);
25
+ this.CONSUME(Identifier, { LABEL: 'instanceId' });
26
+ this.SUBRULE(this.portRef, { LABEL: 'source' });
27
+ this.CONSUME(Arrow);
28
+ this.SUBRULE2(this.portRef, { LABEL: 'target' });
29
+ this.CONSUME(AsKeyword);
30
+ this.CONSUME2(Identifier, { LABEL: 'typeName' });
31
+ });
32
+ // portRef: Identifier Dot Identifier
33
+ portRef = this.RULE('portRef', () => {
34
+ this.CONSUME(Identifier, { LABEL: 'nodeName' });
35
+ this.CONSUME(Dot);
36
+ this.CONSUME2(Identifier, { LABEL: 'portName' });
37
+ });
38
+ }
39
+ // =============================================================================
40
+ // Parser Instance (singleton)
41
+ // =============================================================================
42
+ const parserInstance = new CoerceParser();
43
+ // =============================================================================
44
+ // CST Visitor
45
+ // =============================================================================
46
+ const BaseVisitor = parserInstance.getBaseCstVisitorConstructor();
47
+ class CoerceVisitor extends BaseVisitor {
48
+ constructor() {
49
+ super();
50
+ this.validateVisitor();
51
+ }
52
+ coerceLine(ctx) {
53
+ const instanceId = ctx.instanceId[0].image;
54
+ const source = this.portRef(ctx.source[0].children);
55
+ const target = this.portRef(ctx.target[0].children);
56
+ const targetType = ctx.typeName[0].image;
57
+ return { instanceId, source, target, targetType };
58
+ }
59
+ portRef(ctx) {
60
+ return {
61
+ node: ctx.nodeName[0].image,
62
+ port: ctx.portName[0].image,
63
+ };
64
+ }
65
+ }
66
+ const visitorInstance = new CoerceVisitor();
67
+ // =============================================================================
68
+ // Public API
69
+ // =============================================================================
70
+ /**
71
+ * Parse a @coerce line and return structured result.
72
+ * Returns null if the line is not a valid @coerce declaration.
73
+ */
74
+ export function parseCoerceLine(input, warnings) {
75
+ const lexResult = JSDocLexer.tokenize(input);
76
+ if (lexResult.errors.length > 0 || lexResult.tokens.length === 0)
77
+ return null;
78
+ if (lexResult.tokens[0].tokenType !== CoerceTag)
79
+ return null;
80
+ parserInstance.input = lexResult.tokens;
81
+ const cst = parserInstance.coerceLine();
82
+ if (parserInstance.errors.length > 0) {
83
+ const truncatedInput = input.length > 80 ? input.substring(0, 80) + '...' : input;
84
+ warnings.push(`Failed to parse @coerce line: "${truncatedInput}"\n` +
85
+ ` Error: ${parserInstance.errors[0].message}\n` +
86
+ ` Expected format: @coerce instanceId source.port -> target.port as type`);
87
+ return null;
88
+ }
89
+ const result = visitorInstance.visit(cst);
90
+ // Validate the target type
91
+ if (!VALID_COERCE_TYPES.has(result.targetType)) {
92
+ warnings.push(`@coerce: invalid target type "${result.targetType}". Valid types: ${[...VALID_COERCE_TYPES].join(', ')}`);
93
+ return null;
94
+ }
95
+ return result;
96
+ }
97
+ /**
98
+ * Get serialized grammar for documentation/diagram generation.
99
+ */
100
+ export function getCoerceGrammar() {
101
+ return parserInstance.getSerializedGastProductions();
102
+ }
103
+ //# sourceMappingURL=coerce-parser.js.map
@@ -4,7 +4,7 @@
4
4
  * Chevrotain-based parsers for JSDoc annotations.
5
5
  * Provides structured parsing for @input, @output, @node, @connect, and @position.
6
6
  */
7
- export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
7
+ export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, CoerceTag, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
8
8
  export { parsePortLine, getPortGrammar } from './port-parser.js';
9
9
  export type { PortParseResult } from './port-parser.js';
10
10
  export { parseNodeLine, getNodeGrammar } from './node-parser.js';
@@ -21,6 +21,8 @@ export { parsePathLine, getPathGrammar } from './path-parser.js';
21
21
  export type { PathParseResult, PathStep } from './path-parser.js';
22
22
  export { parseFanOutLine, parseFanInLine } from './fan-parser.js';
23
23
  export type { FanOutParseResult, FanInParseResult, PortRef } from './fan-parser.js';
24
+ export { parseCoerceLine } from './coerce-parser.js';
25
+ export type { CoerceParseResult } from './coerce-parser.js';
24
26
  export { parseTriggerLine, parseCancelOnLine, parseRetriesLine, parseTimeoutLine, parseThrottleLine } from './trigger-cancel-parser.js';
25
27
  export type { TriggerParseResult, CancelOnParseResult, ThrottleParseResult } from './trigger-cancel-parser.js';
26
28
  export { generateGrammarDiagrams, getAllGrammars, serializedToEBNF } from './grammar-diagrams.js';
@@ -5,7 +5,7 @@
5
5
  * Provides structured parsing for @input, @output, @node, @connect, and @position.
6
6
  */
7
7
  // Token exports (for advanced use cases)
8
- export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
8
+ export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, CoerceTag, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
9
9
  // Port parser
10
10
  export { parsePortLine, getPortGrammar } from './port-parser.js';
11
11
  // Node parser
@@ -22,6 +22,8 @@ export { parseMapLine, getMapGrammar } from './map-parser.js';
22
22
  export { parsePathLine, getPathGrammar } from './path-parser.js';
23
23
  // Fan-out / Fan-in parser
24
24
  export { parseFanOutLine, parseFanInLine } from './fan-parser.js';
25
+ // Coerce parser
26
+ export { parseCoerceLine } from './coerce-parser.js';
25
27
  // Trigger/cancel/retries/timeout/throttle parser
26
28
  export { parseTriggerLine, parseCancelOnLine, parseRetriesLine, parseTimeoutLine, parseThrottleLine } from './trigger-cancel-parser.js';
27
29
  // Grammar diagram generation
@@ -4,7 +4,7 @@
4
4
  * Parser for @input/@output/@step port declarations using Chevrotain.
5
5
  */
6
6
  import { CstParser } from 'chevrotain';
7
- import { JSDocLexer, InputTag, OutputTag, StepTag, Identifier, ScopePrefix, OrderPrefix, PlacementPrefix, TypePrefix, MergeStrategyPrefix, TopKeyword, BottomKeyword, TrueKeyword, FalseKeyword, OverKeyword, MinimizedKeyword, Integer, StringLiteral, DescriptionText, LBracket, RBracket, LParen, RParen, Comma, Colon, Dot, Dash, Equals, GreaterThan, LessThan, Pipe, Ampersand, LBrace, RBrace, Asterisk, allTokens, } from './tokens.js';
7
+ import { JSDocLexer, InputTag, OutputTag, StepTag, Identifier, ScopePrefix, OrderPrefix, PlacementPrefix, TypePrefix, MergeStrategyPrefix, TopKeyword, BottomKeyword, TrueKeyword, FalseKeyword, OverKeyword, AsKeyword, MinimizedKeyword, Integer, StringLiteral, DescriptionText, LBracket, RBracket, LParen, RParen, Comma, Colon, Dot, Dash, Equals, GreaterThan, LessThan, Pipe, Ampersand, LBrace, RBrace, Asterisk, allTokens, } from './tokens.js';
8
8
  // =============================================================================
9
9
  // Parser Definition
10
10
  // =============================================================================
@@ -160,6 +160,7 @@ class PortParser extends CstParser {
160
160
  { ALT: () => this.CONSUME(Asterisk) },
161
161
  // Keywords that can appear in description text
162
162
  { ALT: () => this.CONSUME(OverKeyword) },
163
+ { ALT: () => this.CONSUME(AsKeyword) },
163
164
  { ALT: () => this.CONSUME(TopKeyword) },
164
165
  { ALT: () => this.CONSUME(BottomKeyword) },
165
166
  { ALT: () => this.CONSUME(TrueKeyword) },
@@ -19,12 +19,14 @@ export declare const MapTag: import("chevrotain").TokenType;
19
19
  export declare const PathTag: import("chevrotain").TokenType;
20
20
  export declare const FanOutTag: import("chevrotain").TokenType;
21
21
  export declare const FanInTag: import("chevrotain").TokenType;
22
+ export declare const CoerceTag: import("chevrotain").TokenType;
22
23
  export declare const TriggerTag: import("chevrotain").TokenType;
23
24
  export declare const CancelOnTag: import("chevrotain").TokenType;
24
25
  export declare const RetriesTag: import("chevrotain").TokenType;
25
26
  export declare const TimeoutTag: import("chevrotain").TokenType;
26
27
  export declare const ThrottleTag: import("chevrotain").TokenType;
27
28
  export declare const OverKeyword: import("chevrotain").TokenType;
29
+ export declare const AsKeyword: import("chevrotain").TokenType;
28
30
  export declare const ParamTag: import("chevrotain").TokenType;
29
31
  export declare const ReturnsTag: import("chevrotain").TokenType;
30
32
  export declare const FlowWeaverTag: import("chevrotain").TokenType;
@@ -69,6 +69,10 @@ export const FanInTag = createToken({
69
69
  name: 'FanInTag',
70
70
  pattern: /@fanIn\b/,
71
71
  });
72
+ export const CoerceTag = createToken({
73
+ name: 'CoerceTag',
74
+ pattern: /@coerce\b/,
75
+ });
72
76
  export const TriggerTag = createToken({
73
77
  name: 'TriggerTag',
74
78
  pattern: /@trigger\b/,
@@ -93,6 +97,10 @@ export const OverKeyword = createToken({
93
97
  name: 'OverKeyword',
94
98
  pattern: /over\b/,
95
99
  });
100
+ export const AsKeyword = createToken({
101
+ name: 'AsKeyword',
102
+ pattern: /as\b/,
103
+ });
96
104
  export const ParamTag = createToken({
97
105
  name: 'ParamTag',
98
106
  pattern: /@param\b/,
@@ -325,6 +333,7 @@ export const allTokens = [
325
333
  PathTag,
326
334
  FanOutTag,
327
335
  FanInTag,
336
+ CoerceTag,
328
337
  TriggerTag,
329
338
  CancelOnTag,
330
339
  RetriesTag,
@@ -358,6 +367,7 @@ export const allTokens = [
358
367
  PeriodEq,
359
368
  // Keywords (before Identifier)
360
369
  OverKeyword,
370
+ AsKeyword,
361
371
  MinimizedKeyword,
362
372
  TopKeyword,
363
373
  BottomKeyword,
@@ -12,6 +12,8 @@ export interface RunOptions {
12
12
  production?: boolean;
13
13
  /** Include execution trace events */
14
14
  trace?: boolean;
15
+ /** Stream trace events in real-time */
16
+ stream?: boolean;
15
17
  /** Output result as JSON (for scripting) */
16
18
  json?: boolean;
17
19
  /** Execution timeout in milliseconds */
@@ -102,10 +102,38 @@ export async function runCommand(input, options) {
102
102
  // - If --production is set, no trace (unless --trace explicitly set)
103
103
  // - If --trace is set, include trace
104
104
  // - Default: include trace in dev mode
105
- const includeTrace = options.trace ?? !options.production;
105
+ const includeTrace = options.stream || options.trace || !options.production;
106
106
  if (!options.json && mocks) {
107
107
  logger.info('Running with mock data');
108
108
  }
109
+ // Build onEvent callback for real-time streaming
110
+ const nodeStartTimes = new Map();
111
+ const onEvent = options.stream && !options.json
112
+ ? (event) => {
113
+ if (event.type === 'STATUS_CHANGED' && event.data) {
114
+ const nodeId = event.data.id;
115
+ const status = event.data.status;
116
+ if (!nodeId || !status)
117
+ return;
118
+ if (status === 'RUNNING') {
119
+ nodeStartTimes.set(nodeId, event.timestamp);
120
+ logger.log(` [STATUS_CHANGED] ${nodeId}: → RUNNING`);
121
+ }
122
+ else {
123
+ const startTime = nodeStartTimes.get(nodeId);
124
+ const duration = startTime ? ` (${event.timestamp - startTime}ms)` : '';
125
+ logger.log(` [STATUS_CHANGED] ${nodeId}: → ${status}${duration}`);
126
+ }
127
+ }
128
+ else if (event.type === 'VARIABLE_SET' && event.data) {
129
+ const nodeId = event.data.nodeId;
130
+ const varName = event.data.name;
131
+ if (nodeId && varName) {
132
+ logger.log(` [VARIABLE_SET] ${nodeId}.${varName}`);
133
+ }
134
+ }
135
+ }
136
+ : undefined;
109
137
  const channel = new AgentChannel();
110
138
  const execPromise = executeWorkflowFromFile(filePath, params, {
111
139
  workflowName: options.workflow,
@@ -113,6 +141,7 @@ export async function runCommand(input, options) {
113
141
  includeTrace,
114
142
  mocks,
115
143
  agentChannel: channel,
144
+ onEvent,
116
145
  });
117
146
  let result;
118
147
  let execDone = false;