@output.ai/core 0.1.1 → 0.1.2

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
@@ -4,24 +4,30 @@ Provides tools to develop and run a workflow, which is a well defined logical un
4
4
 
5
5
  ## Structure
6
6
 
7
- Workflows are defined using core functions "workflow" and "step", separate files:
7
+ Workflows are defined using core functions ("workflow", "step", "evaluator"), these are defined in separate files, and must be placed within the same folder:
8
8
 
9
9
  ```
10
10
  └ workflows
11
11
  └ example
12
12
  ├ workflow.ts|js <- workflow entry point
13
- ├ steps.ts|js <- file with each step of this workflow
13
+ ├ steps.ts|js <- file containing steps used by the workflow
14
+ ├ evaluators.ts|js <- file containing evaluating functions
14
15
  └ prompt.prompt <- a prompt file
15
16
  └ other-example
16
17
 
17
18
  ```
18
19
 
19
- Think that workflows is the orchestrator and steps are executors. So the workflow only call the steps and the steps call the IO operations, like APIs, DBs, LLMs, etc.
20
+ Workflows are the orchestrator and steps are executors. So the workflow only call the steps and the steps call the IO operations, like APIs, DBs, LLMs, etc. Evaluators are just another different flavor for steps, they work the same, but must return an `EvaluationResult` object.
20
21
 
21
- ## Workflow code
22
+ ## Components
22
23
 
23
- ### workflow.js
24
+ ### Workflow
24
25
 
26
+ The main code, must contain only deterministic orchestration code.
27
+
28
+ File: `workflow.js`
29
+
30
+ Example:
25
31
  ```js
26
32
  import { workflow, z } from '@output.ai/workflow';
27
33
  import { guessByName } from './steps.js';
@@ -42,8 +48,13 @@ export default workflow( {
42
48
  })
43
49
  ```
44
50
 
45
- ### steps.js
51
+ ### Step
52
+
53
+ Re-usable units of work that can contain IO, used by the workflow.
46
54
 
55
+ File: `steps.js`
56
+
57
+ Example:
47
58
  ```js
48
59
  import { api } from './api.js'
49
60
 
@@ -58,9 +69,73 @@ export const guessByName = step( {
58
69
  } )
59
70
  ```
60
71
 
61
- ## webhooks
72
+ ### Shared Steps
73
+
74
+ By default, steps are exclusive to the workflow, so it is not passible to use these steps from elsewhere. In order to have shared steps and make them accessible in different workflows, create a shared steps file. This file can be relatively imported anywhere.
75
+
76
+ File: `shared_steps.js`
77
+
78
+ Example:
79
+ ```js
80
+ export const mySharedStep = step( {
81
+ name: 'mySharedStep',
82
+ ...
83
+ } )
84
+ ```
85
+
86
+ And the usage is the same as any step:
87
+ `workflow.js`
88
+ ```js
89
+ import { mySharedStep } from '../../tools/shared_steps.js'
90
+ ```
91
+
92
+ ### Evaluators
93
+
94
+ Steps that analyze LLM response, or take other measurements are contained in evaluators.
95
+
96
+ File: `evaluators.js`
97
+
98
+ Example:
99
+ ```js
100
+ import { evaluator, EvaluationStringResult } from './api.js'
101
+
102
+ export const judgeResult = evaluator( {
103
+ name: 'judgeResult',
104
+ inputSchema: z.string(),
105
+ fn: async name => {
106
+ ...
107
+ return new EvaluationStringResult({
108
+ value: 'good',
109
+ confidence: .95
110
+ });
111
+ }
112
+ } )
113
+ ```
114
+
115
+ Its usage is the same as steps:
116
+ `workflow.js`
117
+ ```js
118
+ import { workflow, z } from '@output.ai/workflow';
119
+ import { judgeResult } from './evaluators.js';
62
120
 
63
- workflows can call webhooks that will stop their execution until an answer is given back.
121
+ export default workflow( {
122
+ name: 'guessMyProfession',
123
+ inputSchema: z.object( {
124
+ name: z.string()
125
+ } ),
126
+ outputSchema: z.object( {
127
+ result: z.string()
128
+ } ),
129
+ fn: async input => {
130
+ const judgment = await judgeResult( input.name );
131
+ return { result: judgement.value };
132
+ }
133
+ })
134
+ ```
135
+
136
+ ## Webhooks
137
+
138
+ Workflows can call webhooks that will stop their execution until an answer is given back.
64
139
 
65
140
  ```js
66
141
  import { workflow, createWebhook } from '@output.ai/workflow';
@@ -107,6 +182,21 @@ POST http://locahost:3001/workflow/feedback
107
182
  }
108
183
  ```
109
184
 
185
+ ## Options
186
+
187
+ All core interface functions: workflow, step, evaluator have similar signature, with the following options:
188
+ - name: The function name, used to call it internally and identify it in the trace files, must be a code friendly string;
189
+ - description: Human description of the workflow/step, used for the catalog;
190
+ - inputSchema: a zod object indicating the type of the argument received by the `fn` function. It is validated. Omit if it doesn't have input arguments;
191
+ - outputSchema: a zod object indicating the type of that the `fn` function returns. It is validated. Omit if it is void. Evaluators do not have this option, since they must always return an EvaluationResult object;
192
+ - fn: The actual implementation of the workflow/step, including all its logic.
193
+ - options: Advanced options that will overwrite Temporal's ActivityOptions when calling activities.
194
+
195
+ If used on `workflow()` it will apply for all activities. If used on `step()` or `evaluator()` it will apply only to that underlying activity. If changed in both places, the end value will be a merge between the initial values, workflow values and the step values.
196
+
197
+ Order of precedence
198
+ `step options > workflow options > default options`
199
+
110
200
  ## Developing
111
201
 
112
202
  To develop workflows you need the code, which will be called the worker, the API and the engine (Temporal).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -42,6 +42,7 @@
42
42
  "#utils": "./src/utils.js",
43
43
  "#tracing": "./src/tracing/internal_interface.js",
44
44
  "#async_storage": "./src/async_storage.js",
45
+ "#temporal_options": "./src/temporal_options.js",
45
46
  "#internal_activities": "./src/internal_activities/index.js"
46
47
  }
47
48
  }
package/src/consts.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export const ACTIVITY_SEND_WEBHOOK = '__internal#sendWebhook';
2
2
  export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
3
+ export const SHARED_STEP_PREFIX = '__shared#';
3
4
  export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
5
+ export const ACTIVITY_OPTIONS_FILENAME = '__activity_options.js';
4
6
  export const WORKFLOW_CATALOG = '$catalog';
5
7
  export const ComponentType = {
6
8
  EVALUATOR: 'evaluator',
package/src/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Import Zod types for dual schema support
2
2
  import type { z } from 'zod';
3
+ import type { ActivityOptions } from '@temporalio/workflow';
3
4
 
4
5
  // Re-export Zod for consumers to use
5
6
  export { z } from 'zod';
@@ -19,6 +20,17 @@ export { z } from 'zod';
19
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
21
  type AnyZodSchema = z.ZodType<any, any, any>;
21
22
 
23
+ /**
24
+ * Activity retry options accepted by step/evaluator/workflow definitions.
25
+ * Uses Temporal's ActivityOptions['retry'] type.
26
+ */
27
+ export type Options = { retry?: ActivityOptions['retry'] };
28
+
29
+ /**
30
+ * @typedef {object} Options
31
+ * @property {import('@temporalio/workflow').ActivityOptions['retry']} [retry]
32
+ */
33
+
22
34
  /*
23
35
  ╭─────────╮
24
36
  │ S T E P │╮
@@ -43,6 +55,7 @@ type AnyZodSchema = z.ZodType<any, any, any>;
43
55
  * @param {z.ZodType} options.inputSchema - Zod schema for the fn input
44
56
  * @param {z.ZodType} options.outputSchema - Zod schema for the fn output
45
57
  * @param {function} options.fn - The function logic: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
58
+ * @param {Options} [options.options] - Activity retry options
46
59
  * @returns {function} Function with signature: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
47
60
  */
48
61
  export async function step<
@@ -54,6 +67,7 @@ export async function step<
54
67
  inputSchema: InputSchema;
55
68
  outputSchema: OutputSchema;
56
69
  fn: ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>;
70
+ options?: Options;
57
71
  } ): ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>;
58
72
 
59
73
  /**
@@ -64,6 +78,7 @@ export async function step<
64
78
  * @param {string} [options.description] - Description of the step
65
79
  * @param {z.ZodType} options.inputSchema - Zod schema for the fn input
66
80
  * @param {function} options.fn - The function logic: `(input: z.infer<InputSchema>) => Promise<void>`
81
+ * @param {Options} [options.options] - Activity retry options
67
82
  * @returns {function} Function with signature: `(input: z.infer<InputSchema>) => Promise<void>`
68
83
  */
69
84
  export async function step<
@@ -73,6 +88,7 @@ export async function step<
73
88
  description?: string;
74
89
  inputSchema: InputSchema;
75
90
  fn: ( input: z.infer<InputSchema> ) => Promise<void>;
91
+ options?: Options;
76
92
  } ): ( input: z.infer<InputSchema> ) => Promise<void>;
77
93
 
78
94
  /**
@@ -83,6 +99,7 @@ export async function step<
83
99
  * @param {string} [options.description] - Description of the step
84
100
  * @param {z.ZodType} options.outputSchema - Zod schema for the fn output
85
101
  * @param {function} options.fn - The function logic: `() => Promise<z.infer<OutputSchema>>`
102
+ * @param {Options} [options.options] - Activity retry options
86
103
  * @returns {function} Function with signature: `() => Promise<z.infer<OutputSchema>>`
87
104
  */
88
105
  export async function step<
@@ -92,6 +109,7 @@ export async function step<
92
109
  description?: string;
93
110
  outputSchema: OutputSchema;
94
111
  fn: () => Promise<z.infer<OutputSchema>>;
112
+ options?: Options;
95
113
  } ): () => Promise<z.infer<OutputSchema>>;
96
114
 
97
115
  /**
@@ -101,12 +119,14 @@ export async function step<
101
119
  * @param {string} options.name - Human-readable step name (only letters, numbers and "_")
102
120
  * @param {string} [options.description] - Description of the step
103
121
  * @param {function} options.fn - The function logic: `() => Promise<void>`
122
+ * @param {Options} [options.options] - Activity retry options
104
123
  * @returns {function} Function with signature: `() => Promise<void>`
105
124
  */
106
125
  export async function step( options: {
107
126
  name: string;
108
127
  description?: string;
109
128
  fn: () => Promise<void>;
129
+ options?: Options;
110
130
  } ): () => Promise<void>;
111
131
 
112
132
  /*
@@ -132,6 +152,7 @@ export async function step( options: {
132
152
  * @param {z.ZodType} options.inputSchema - Zod schema for workflow input
133
153
  * @param {z.ZodType} options.outputSchema - Zod schema for workflow output
134
154
  * @param {function} options.fn - Workflow logic: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
155
+ * @param {Options} [options.options] - Activity retry options
135
156
  * @returns {function} Callable workflow function: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
136
157
  */
137
158
  export function workflow<
@@ -142,7 +163,8 @@ export function workflow<
142
163
  description?: string,
143
164
  inputSchema: InputSchema,
144
165
  outputSchema: OutputSchema,
145
- fn: ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>
166
+ fn: ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>,
167
+ options?: Options
146
168
  } ): ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>;
147
169
 
148
170
  /**
@@ -153,6 +175,7 @@ export function workflow<
153
175
  * @param {string} [options.description] - Description of the workflow
154
176
  * @param {z.ZodType} options.inputSchema - Zod schema for workflow input
155
177
  * @param {function} options.fn - Workflow logic: `(input: z.infer<InputSchema>) => Promise<void>`
178
+ * @param {Options} [options.options] - Activity retry options
156
179
  * @returns {function} Callable workflow function: `(input: z.infer<InputSchema>) => Promise<void>`
157
180
  */
158
181
  export function workflow<
@@ -161,7 +184,8 @@ export function workflow<
161
184
  name: string,
162
185
  description?: string,
163
186
  inputSchema: InputSchema,
164
- fn: ( input: z.infer<InputSchema> ) => Promise<void>
187
+ fn: ( input: z.infer<InputSchema> ) => Promise<void>,
188
+ options?: Options
165
189
  } ): ( input: z.infer<InputSchema> ) => Promise<void>;
166
190
 
167
191
  /**
@@ -172,6 +196,7 @@ export function workflow<
172
196
  * @param {string} [options.description] - Description of the workflow
173
197
  * @param {z.ZodType} options.outputSchema - Zod schema for workflow output
174
198
  * @param {function} options.fn - Workflow logic: `() => Promise<z.infer<OutputSchema>>`
199
+ * @param {Options} [options.options] - Activity retry options
175
200
  * @returns {function} Callable workflow function: `() => Promise<z.infer<OutputSchema>>`
176
201
  */
177
202
  export function workflow<
@@ -180,7 +205,8 @@ export function workflow<
180
205
  name: string,
181
206
  description?: string,
182
207
  outputSchema: OutputSchema,
183
- fn: () => Promise<z.infer<OutputSchema>>
208
+ fn: () => Promise<z.infer<OutputSchema>>,
209
+ options?: Options
184
210
  } ): () => Promise<z.infer<OutputSchema>>;
185
211
 
186
212
  /**
@@ -190,12 +216,14 @@ export function workflow<
190
216
  * @param {string} options.name - Unique workflow name
191
217
  * @param {string} [options.description] - Description of the workflow
192
218
  * @param {function} options.fn - Workflow logic: `() => Promise<void>`
219
+ * @param {Options} [options.options] - Activity retry options
193
220
  * @returns {function} Callable workflow function: `() => Promise<void>`
194
221
  */
195
222
  export function workflow( options : {
196
223
  name: string,
197
224
  description?: string,
198
- fn: () => Promise<void>
225
+ fn: () => Promise<void>,
226
+ options?: Options
199
227
  } ): () => Promise<void>;
200
228
 
201
229
  /*
@@ -309,6 +337,7 @@ export class EvaluationBooleanResult extends EvaluationResult {
309
337
  * @param {string} [options.description] - Description of the evaluator
310
338
  * @param {z.ZodType} options.inputSchema - Zod schema for the fn input
311
339
  * @param {function} options.fn - The function logic: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
340
+ * @param {Options} [options.options] - Activity retry options
312
341
  * @returns {function} Function with signature: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
313
342
  */
314
343
  export async function evaluator<
@@ -319,6 +348,7 @@ export async function evaluator<
319
348
  description?: string;
320
349
  inputSchema: InputSchema;
321
350
  fn: ( input: z.infer<InputSchema> ) => Promise<Result>;
351
+ options?: Options;
322
352
  } ): ( input: z.infer<InputSchema> ) => Promise<Result>;
323
353
 
324
354
  /**
@@ -329,6 +359,7 @@ export async function evaluator<
329
359
  * @param {string} [options.description] - Description of the evaluator
330
360
  * @param {z.ZodType} options.inputSchema - Zod schema for the fn input
331
361
  * @param {function} options.fn - The function logic: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
362
+ * @param {Options} [options.options] - Activity retry options
332
363
  * @returns {function} Function with signature: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
333
364
  */
334
365
  export async function evaluator<
@@ -338,6 +369,7 @@ export async function evaluator<
338
369
  name: string;
339
370
  description?: string;
340
371
  fn: ( input: z.infer<InputSchema> ) => Promise<Result>;
372
+ options?: Options;
341
373
  } ): ( input: z.infer<InputSchema> ) => Promise<Result>;
342
374
 
343
375
  /*
@@ -1,6 +1,6 @@
1
- import { setMetadata } from './metadata.js';
2
1
  import { validateEvaluator } from './validations/static.js';
3
2
  import { validateWithSchema } from './validations/runtime.js';
3
+ import { setMetadata } from '#utils';
4
4
  import { ValidationError } from '#errors';
5
5
  import { ComponentType } from '#consts';
6
6
  import * as z from 'zod';
@@ -130,8 +130,8 @@ export class EvaluationNumberResult extends EvaluationResult {
130
130
  }
131
131
  };
132
132
 
133
- export function evaluator( { name, description, inputSchema, fn } ) {
134
- validateEvaluator( { name, description, inputSchema, fn } );
133
+ export function evaluator( { name, description, inputSchema, fn, options } ) {
134
+ validateEvaluator( { name, description, inputSchema, fn, options } );
135
135
 
136
136
  const wrapper = async input => {
137
137
  validateWithSchema( inputSchema, input, `Evaluator ${name} input` );
@@ -145,6 +145,6 @@ export function evaluator( { name, description, inputSchema, fn } ) {
145
145
  return output;
146
146
  };
147
147
 
148
- setMetadata( wrapper, { name, description, inputSchema, type: ComponentType.EVALUATOR } );
148
+ setMetadata( wrapper, { name, description, inputSchema, type: ComponentType.EVALUATOR, options } );
149
149
  return wrapper;
150
150
  };
@@ -1,10 +1,10 @@
1
- import { setMetadata } from './metadata.js';
1
+ import { setMetadata } from '#utils';
2
2
  import { validateStep } from './validations/static.js';
3
3
  import { validateWithSchema } from './validations/runtime.js';
4
4
  import { ComponentType } from '#consts';
5
5
 
6
- export function step( { name, description, inputSchema, outputSchema, fn } ) {
7
- validateStep( { name, description, inputSchema, outputSchema, fn } );
6
+ export function step( { name, description, inputSchema, outputSchema, fn, options } ) {
7
+ validateStep( { name, description, inputSchema, outputSchema, fn, options } );
8
8
 
9
9
  const wrapper = async input => {
10
10
  validateWithSchema( inputSchema, input, `Step ${name} input` );
@@ -16,6 +16,6 @@ export function step( { name, description, inputSchema, outputSchema, fn } ) {
16
16
  return output;
17
17
  };
18
18
 
19
- setMetadata( wrapper, { name, description, inputSchema, outputSchema, type: ComponentType.STEP } );
19
+ setMetadata( wrapper, { name, description, inputSchema, outputSchema, type: ComponentType.STEP, options } );
20
20
  return wrapper;
21
21
  };
@@ -17,12 +17,26 @@ const refineSchema = ( value, ctx ) => {
17
17
  } );
18
18
  };
19
19
 
20
- const stepAndWorkflowSchema = z.object( {
20
+ export const durationStringSchema = z.string().regex(
21
+ /^(\d+)(ms|s|m|h|d)$/,
22
+ 'Expected duration like "500ms", "10s", "5m", "2h", or "1d"'
23
+ );
24
+
25
+ const stepAndWorkflowSchema = z.strictObject( {
21
26
  name: z.string().regex( /^[a-z_][a-z0-9_]*$/i ),
22
27
  description: z.string().optional(),
23
28
  inputSchema: z.any().optional().superRefine( refineSchema ),
24
29
  outputSchema: z.any().optional().superRefine( refineSchema ),
25
- fn: z.function()
30
+ fn: z.function(),
31
+ options: z.strictObject( {
32
+ retry: z.strictObject( {
33
+ initialInterval: durationStringSchema.optional(),
34
+ backoffCoefficient: z.number().gte( 1 ).optional(),
35
+ maximumInterval: durationStringSchema.optional(),
36
+ maximumAttempts: z.number().gte( 1 ).int().optional(),
37
+ nonRetryableErrorTypes: z.array( z.string() ).optional()
38
+ } ).optional()
39
+ } ).optional()
26
40
  } );
27
41
 
28
42
  const evaluatorSchema = stepAndWorkflowSchema.omit( { outputSchema: true } );
@@ -65,6 +65,26 @@ describe( 'interface/validator', () => {
65
65
  const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at fn' );
66
66
  expect( () => validateStep( { ...validArgs, fn: 'not-fn' } ) ).toThrow( error );
67
67
  } );
68
+
69
+ it( 'passes with options.retry (second-level options)', () => {
70
+ const args = {
71
+ ...validArgs,
72
+ options: {
73
+ retry: {
74
+ initialInterval: '1s',
75
+ backoffCoefficient: 2,
76
+ maximumInterval: '10s',
77
+ maximumAttempts: 3,
78
+ nonRetryableErrorTypes: [ 'SomeError' ]
79
+ }
80
+ }
81
+ };
82
+ expect( () => validateStep( args ) ).not.toThrow();
83
+ } );
84
+
85
+ it( 'rejects unknown top-level keys due to strictObject', () => {
86
+ expect( () => validateStep( { ...validArgs, extra: 123 } ) ).toThrow( StaticValidationError );
87
+ } );
68
88
  } );
69
89
 
70
90
  describe( 'validateWorkflow', () => {
@@ -1,27 +1,29 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
2
  import { proxyActivities, inWorkflowContext, executeChild, workflowInfo } from '@temporalio/workflow';
3
3
  import { getInvocationDir } from './utils.js';
4
- import { setMetadata } from './metadata.js';
5
- import { FatalError, ValidationError } from '#errors';
6
4
  import { validateWorkflow } from './validations/static.js';
7
5
  import { validateWithSchema } from './validations/runtime.js';
6
+ import { SHARED_STEP_PREFIX } from '#consts';
7
+ import { mergeActivityOptions, setMetadata } from '#utils';
8
+ import { FatalError, ValidationError } from '#errors';
8
9
 
9
- const temporalActivityConfigs = {
10
- startToCloseTimeout: '20 minute',
10
+ const defaultActivityOptions = {
11
+ startToCloseTimeout: '20m',
11
12
  retry: {
12
13
  initialInterval: '10s',
13
14
  backoffCoefficient: 2.0,
14
- maximumInterval: '2 minutes',
15
+ maximumInterval: '2m',
15
16
  maximumAttempts: 3,
16
17
  nonRetryableErrorTypes: [ ValidationError.name, FatalError.name ]
17
18
  }
18
19
  };
19
20
 
20
- export function workflow( { name, description, inputSchema, outputSchema, fn } ) {
21
- validateWorkflow( { name, description, inputSchema, outputSchema, fn } );
21
+ export function workflow( { name, description, inputSchema, outputSchema, fn, options } ) {
22
+ validateWorkflow( { name, description, inputSchema, outputSchema, fn, options } );
22
23
  const workflowPath = getInvocationDir();
23
24
 
24
- const steps = proxyActivities( temporalActivityConfigs );
25
+ const activityOptions = mergeActivityOptions( defaultActivityOptions, options );
26
+ const steps = proxyActivities( activityOptions );
25
27
 
26
28
  const wrapper = async input => {
27
29
  validateWithSchema( inputSchema, input, `Workflow ${name} input` );
@@ -44,12 +46,16 @@ export function workflow( { name, description, inputSchema, outputSchema, fn } )
44
46
  startTime: startTime.getTime()
45
47
  };
46
48
 
47
- Object.assign( memo, { executionContext } );
49
+ Object.assign( memo, {
50
+ executionContext,
51
+ activityOptions: memo.activityOptions ?? activityOptions // Also preserve the original activity options
52
+ } );
48
53
 
49
- // binds the methods called in the code that Webpack loader will add, they will exposed via "this"
54
+ // binds the methods called in the code that Webpack loader will add, they will exposed via "this"
50
55
  const output = await fn.call( {
51
- invokeStep: async ( stepName, input ) => steps[`${workflowPath}#${stepName}`]( input ),
52
- invokeEvaluator: async ( evaluatorName, input ) => steps[`${workflowPath}#${evaluatorName}`]( input ),
56
+ invokeStep: async ( stepName, input, options ) => steps[`${workflowPath}#${stepName}`]( input, options ),
57
+ invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
58
+ invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${workflowPath}#${evaluatorName}`]( input, options ),
53
59
 
54
60
  startWorkflow: async ( childName, input ) => {
55
61
  return executeChild( childName, { args: input ? [ input ] : [], memo: { executionContext, parentId: workflowId } } );
@@ -410,12 +410,12 @@ describe( 'Zod Schema Integration Tests', () => {
410
410
  inputSchema,
411
411
  fn: async input => {
412
412
  switch ( input.action ) {
413
- case 'create':
414
- return `Creating ${input.type}: ${input.name}`;
415
- case 'delete':
416
- return `Deleting item ${input.id}`;
417
- default:
418
- throw new Error( 'Unknown action' );
413
+ case 'create':
414
+ return `Creating ${input.type}: ${input.name}`;
415
+ case 'delete':
416
+ return `Deleting item ${input.id}`;
417
+ default:
418
+ throw new Error( 'Unknown action' );
419
419
  }
420
420
  }
421
421
  } );
@@ -1,5 +1,5 @@
1
1
  import { FatalError } from '#errors';
2
- import { setMetadata } from '../interface/metadata.js';
2
+ import { setMetadata } from '#utils';
3
3
  import { ComponentType } from '#consts';
4
4
 
5
5
  /**
package/src/utils.js CHANGED
@@ -1,3 +1,12 @@
1
+ import { METADATA_ACCESS_SYMBOL } from '#consts';
2
+
3
+ /**
4
+ * Node safe clone implementation that doesn't use global structuredClone()
5
+ * @param {object} v
6
+ * @returns {object}
7
+ */
8
+ export const clone = v => JSON.parse( JSON.stringify( v ) );
9
+
1
10
  /**
2
11
  * Throw given error
3
12
  * @param {Error} e
@@ -6,3 +15,23 @@
6
15
  export const throws = e => {
7
16
  throw e;
8
17
  };
18
+
19
+ /**
20
+ * Add metadata "values" property to a given object
21
+ * @param {object} target
22
+ * @param {object} values
23
+ * @returns
24
+ */
25
+ export const setMetadata = ( target, values ) =>
26
+ Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
27
+
28
+ /**
29
+ * Merge two temporal activity options
30
+ * @param {import('@temporalio/workflow').ActivityOptions} base
31
+ * @param {import('@temporalio/workflow').ActivityOptions} ext
32
+ * @returns {import('@temporalio/workflow').ActivityOptions}
33
+ */
34
+ export const mergeActivityOptions = ( base = {}, ext = {} ) =>
35
+ Object.entries( ext ).reduce( ( options, [ k, v ] ) =>
36
+ Object.assign( options, { [k]: typeof v === 'object' ? mergeActivityOptions( options[k], v ) : v } )
37
+ , clone( base ) );
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { clone, mergeActivityOptions } from './utils.js';
3
+
4
+ describe( 'clone', () => {
5
+ it( 'produces a deep copy without shared references', () => {
6
+ const original = { a: 1, nested: { b: 2 } };
7
+ const copied = clone( original );
8
+
9
+ copied.nested.b = 3;
10
+
11
+ expect( original.nested.b ).toBe( 2 );
12
+ expect( copied.nested.b ).toBe( 3 );
13
+ expect( copied ).not.toBe( original );
14
+ } );
15
+ } );
16
+
17
+ describe( 'mergeActivityOptions', () => {
18
+ it( 'recursively merges nested objects', () => {
19
+ const base = {
20
+ taskQueue: 'q1',
21
+ retry: { maximumAttempts: 3, backoffCoefficient: 2 }
22
+ };
23
+ const ext = {
24
+ retry: { maximumAttempts: 5, initialInterval: '1s' }
25
+ };
26
+
27
+ const result = mergeActivityOptions( base, ext );
28
+
29
+ expect( result ).toEqual( {
30
+ taskQueue: 'q1',
31
+ retry: { maximumAttempts: 5, backoffCoefficient: 2, initialInterval: '1s' }
32
+ } );
33
+ } );
34
+
35
+ it( 'omitted properties in second do not overwrite first', () => {
36
+ const base = {
37
+ taskQueue: 'q2',
38
+ retry: { initialInterval: '2s', backoffCoefficient: 2 }
39
+ };
40
+ const ext = {
41
+ retry: { backoffCoefficient: 3 }
42
+ };
43
+
44
+ const result = mergeActivityOptions( base, ext );
45
+
46
+ expect( result.retry.initialInterval ).toBe( '2s' );
47
+ expect( result.retry.backoffCoefficient ).toBe( 3 );
48
+ expect( result.taskQueue ).toBe( 'q2' );
49
+ } );
50
+
51
+ it( 'handles omitted second argument by returning a clone', () => {
52
+ const base = { taskQueue: 'q3', retry: { maximumAttempts: 2 } };
53
+
54
+ const result = mergeActivityOptions( base );
55
+
56
+ expect( result ).toEqual( base );
57
+ expect( result ).not.toBe( base );
58
+ } );
59
+ } );
60
+
@@ -1,6 +1,9 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
2
  import { workflowInfo, proxySinks, ApplicationFailure } from '@temporalio/workflow';
3
3
  import { memoToHeaders } from '../sandboxed_utils.js';
4
+ import { mergeActivityOptions } from '#utils';
5
+ // this is a dynamic generated file with activity configs overwrites
6
+ import stepOptions from '../temp/__activity_options.js';
4
7
 
5
8
  /*
6
9
  This is not an AI comment!
@@ -13,7 +16,13 @@ import { memoToHeaders } from '../sandboxed_utils.js';
13
16
  */
14
17
  class HeadersInjectionInterceptor {
15
18
  async scheduleActivity( input, next ) {
16
- Object.assign( input.headers, memoToHeaders( workflowInfo().memo ) );
19
+ const memo = workflowInfo().memo ?? {};
20
+ Object.assign( input.headers, memoToHeaders( memo ) );
21
+ // apply per-invocation options passed as second argument by rewritten calls
22
+ const options = stepOptions[input.activityType];
23
+ if ( options ) {
24
+ input.options = mergeActivityOptions( memo.activityOptions, options );
25
+ }
17
26
  return next( input );
18
27
  }
19
28
  };
@@ -1,13 +1,30 @@
1
- import { dirname, join } from 'node:path';
1
+ import { basename, dirname, join } from 'node:path';
2
2
  import { mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { EOL } from 'node:os';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { sendWebhook } from '#internal_activities';
6
- import { ACTIVITY_SEND_WEBHOOK, WORKFLOWS_INDEX_FILENAME, WORKFLOW_CATALOG } from '#consts';
7
6
  import { importComponents } from './loader_tools.js';
7
+ import {
8
+ ACTIVITY_SEND_WEBHOOK,
9
+ ACTIVITY_OPTIONS_FILENAME,
10
+ SHARED_STEP_PREFIX,
11
+ WORKFLOWS_INDEX_FILENAME,
12
+ WORKFLOW_CATALOG
13
+ } from '#consts';
8
14
 
9
15
  const __dirname = dirname( fileURLToPath( import.meta.url ) );
10
16
 
17
+ /**
18
+ * Writes to file the activity options
19
+ *
20
+ * @param {object} optionsMap
21
+ */
22
+ const writeActivityOptionsFile = map => {
23
+ const path = join( __dirname, 'temp', ACTIVITY_OPTIONS_FILENAME );
24
+ mkdirSync( dirname( path ), { recursive: true } );
25
+ writeFileSync( path, `export default ${JSON.stringify( map, undefined, 2 )};`, 'utf-8' );
26
+ };
27
+
11
28
  /**
12
29
  * Builds a map of activities, where the key is their path and name and the value is the function
13
30
  *
@@ -16,11 +33,21 @@ const __dirname = dirname( fileURLToPath( import.meta.url ) );
16
33
  */
17
34
  export async function loadActivities( target ) {
18
35
  const activities = {};
19
- for await ( const { fn, metadata, path } of importComponents( target, [ 'steps.js', 'evaluators.js' ] ) ) {
36
+ const activityOptionsMap = {};
37
+ for await ( const { fn, metadata, path } of importComponents( target, [ 'steps.js', 'evaluators.js', 'shared_steps.js' ] ) ) {
38
+ const isShared = basename( path ) === 'shared_steps.js';
39
+ const prefix = isShared ? SHARED_STEP_PREFIX : dirname( path );
40
+
20
41
  console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
21
- activities[`${dirname( path )}#${metadata.name}`] = fn;
42
+ activities[`${prefix}#${metadata.name}`] = fn;
43
+ if ( metadata.options ) {
44
+ activityOptionsMap[`${prefix}#${metadata.name}`] = metadata.options;
45
+ }
22
46
  }
23
47
 
48
+ // writes down the activity option overrides
49
+ writeActivityOptionsFile( activityOptionsMap );
50
+
24
51
  // system activities
25
52
  activities[ACTIVITY_SEND_WEBHOOK] = sendWebhook;
26
53
  return activities;
@@ -3,7 +3,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
3
  vi.mock( '#consts', () => ( {
4
4
  ACTIVITY_SEND_WEBHOOK: '__internal#sendWebhook',
5
5
  WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
6
- WORKFLOW_CATALOG: 'catalog'
6
+ WORKFLOW_CATALOG: 'catalog',
7
+ ACTIVITY_OPTIONS_FILENAME: '__activity_options.js'
7
8
  } ) );
8
9
 
9
10
  const sendWebhookMock = vi.fn();
@@ -26,16 +27,26 @@ describe( 'worker/loader', () => {
26
27
  vi.clearAllMocks();
27
28
  } );
28
29
 
29
- it( 'loadActivities returns map including system activity', async () => {
30
+ it( 'loadActivities returns map including system activity and writes options file', async () => {
30
31
  const { loadActivities } = await import( './loader.js' );
31
32
 
32
33
  importComponentsMock.mockImplementationOnce( async function *() {
33
- yield { fn: () => {}, metadata: { name: 'Act1' }, path: '/a/steps.js' };
34
+ yield { fn: () => {}, metadata: { name: 'Act1', options: { retry: { maximumAttempts: 3 } } }, path: '/a/steps.js' };
34
35
  } );
35
36
 
36
37
  const activities = await loadActivities( '/root' );
37
38
  expect( activities['/a#Act1'] ).toBeTypeOf( 'function' );
38
39
  expect( activities['__internal#sendWebhook'] ).toBe( sendWebhookMock );
40
+
41
+ // options file written with the collected map
42
+ expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
43
+ const [ writtenPath, contents ] = writeFileSyncMock.mock.calls[0];
44
+ expect( writtenPath ).toMatch( /temp\/__activity_options\.js$/ );
45
+ expect( contents ).toContain( 'export default' );
46
+ expect( JSON.parse( contents.replace( /^export default\s*/, '' ).replace( /;\s*$/, '' ) ) ).toEqual( {
47
+ '/a#Act1': { retry: { maximumAttempts: 3 } }
48
+ } );
49
+ expect( mkdirSyncMock ).toHaveBeenCalled();
39
50
  } );
40
51
 
41
52
  it( 'loadWorkflows returns array of workflows with metadata', async () => {
@@ -3,9 +3,11 @@ import {
3
3
  buildWorkflowNameMap,
4
4
  getLocalNameFromDestructuredProperty,
5
5
  isEvaluatorsPath,
6
+ isSharedStepsPath,
6
7
  isStepsPath,
7
8
  isWorkflowPath,
8
9
  buildStepsNameMap,
10
+ buildSharedStepsNameMap,
9
11
  buildEvaluatorsNameMap,
10
12
  toAbsolutePath
11
13
  } from './tools.js';
@@ -36,8 +38,9 @@ const traverse = traverseModule.default ?? traverseModule;
36
38
  * @returns {{ stepImports: Array<{localName:string,stepName:string}>,
37
39
  * flowImports: Array<{localName:string,workflowName:string}> }} Collected info mappings.
38
40
  */
39
- export default function collectTargetImports( ast, fileDir, { stepsNameCache, workflowNameCache, evaluatorsNameCache } ) {
41
+ export default function collectTargetImports( ast, fileDir, { stepsNameCache, workflowNameCache, evaluatorsNameCache, sharedStepsNameCache } ) {
40
42
  const stepImports = [];
43
+ const sharedStepImports = [];
41
44
  const flowImports = [];
42
45
  const evaluatorImports = [];
43
46
 
@@ -45,33 +48,31 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
45
48
  ImportDeclaration: path => {
46
49
  const src = path.node.source.value;
47
50
  // Ignore other imports
48
- if ( !isStepsPath( src ) && !isWorkflowPath( src ) && !isEvaluatorsPath( src ) ) {
51
+ if ( !isStepsPath( src ) && !isSharedStepsPath( src ) && !isWorkflowPath( src ) && !isEvaluatorsPath( src ) ) {
49
52
  return;
50
53
  }
51
54
 
52
55
  const absolutePath = toAbsolutePath( fileDir, src );
53
- if ( isStepsPath( src ) ) {
54
- const nameMap = buildStepsNameMap( absolutePath, stepsNameCache );
55
- for ( const s of path.node.specifiers.filter( s => isImportSpecifier( s ) ) ) {
56
- const importedName = s.imported.name;
57
- const localName = s.local.name;
58
- const stepName = nameMap.get( importedName );
59
- if ( stepName ) {
60
- stepImports.push( { localName, stepName } );
61
- }
56
+ const collectNamedImports = ( match, buildMapFn, cache, targetArr, valueKey ) => {
57
+ if ( !match ) {
58
+ return;
62
59
  }
63
- }
64
- if ( isEvaluatorsPath( src ) ) {
65
- const nameMap = buildEvaluatorsNameMap( absolutePath, evaluatorsNameCache );
60
+ const nameMap = buildMapFn( absolutePath, cache );
66
61
  for ( const s of path.node.specifiers.filter( s => isImportSpecifier( s ) ) ) {
67
62
  const importedName = s.imported.name;
68
63
  const localName = s.local.name;
69
- const evaluatorName = nameMap.get( importedName );
70
- if ( evaluatorName ) {
71
- evaluatorImports.push( { localName, evaluatorName } );
64
+ const value = nameMap.get( importedName );
65
+ if ( value ) {
66
+ const entry = { localName };
67
+ entry[valueKey] = value;
68
+ targetArr.push( entry );
72
69
  }
73
70
  }
74
- }
71
+ };
72
+
73
+ collectNamedImports( isStepsPath( src ), buildStepsNameMap, stepsNameCache, stepImports, 'stepName' );
74
+ collectNamedImports( isSharedStepsPath( src ), buildSharedStepsNameMap, sharedStepsNameCache, sharedStepImports, 'stepName' );
75
+ collectNamedImports( isEvaluatorsPath( src ), buildEvaluatorsNameMap, evaluatorsNameCache, evaluatorImports, 'evaluatorName' );
75
76
  if ( isWorkflowPath( src ) ) {
76
77
  const { named, default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
77
78
  for ( const s of path.node.specifiers ) {
@@ -108,7 +109,7 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
108
109
 
109
110
  const req = firstArgument.value;
110
111
  // Must be steps/workflows module
111
- if ( !isStepsPath( req ) && !isWorkflowPath( req ) && !isEvaluatorsPath( req ) ) {
112
+ if ( !isStepsPath( req ) && !isSharedStepsPath( req ) && !isWorkflowPath( req ) && !isEvaluatorsPath( req ) ) {
112
113
  return;
113
114
  }
114
115
 
@@ -130,6 +131,23 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
130
131
  } else {
131
132
  path.remove();
132
133
  }
134
+ } else if ( isSharedStepsPath( req ) && isObjectPattern( path.node.id ) ) {
135
+ const nameMap = buildSharedStepsNameMap( absolutePath, sharedStepsNameCache ?? stepsNameCache );
136
+ for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
137
+ const importedName = prop.key.name;
138
+ const localName = getLocalNameFromDestructuredProperty( prop );
139
+ if ( localName ) {
140
+ const stepName = nameMap.get( importedName );
141
+ if ( stepName ) {
142
+ sharedStepImports.push( { localName, stepName } );
143
+ }
144
+ }
145
+ }
146
+ if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
147
+ path.parentPath.remove();
148
+ } else {
149
+ path.remove();
150
+ }
133
151
  } else if ( isEvaluatorsPath( req ) && isObjectPattern( path.node.id ) ) {
134
152
  const nameMap = buildEvaluatorsNameMap( absolutePath, evaluatorsNameCache );
135
153
  for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
@@ -160,5 +178,5 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
160
178
  }
161
179
  } );
162
180
 
163
- return { stepImports, evaluatorImports, flowImports };
181
+ return { stepImports, sharedStepImports, evaluatorImports, flowImports };
164
182
  };
@@ -10,6 +10,7 @@ const generate = generatorModule.default ?? generatorModule;
10
10
 
11
11
  // Caches to avoid re-reading files during a build
12
12
  const stepsNameCache = new Map(); // path -> Map<exported, stepName>
13
+ const sharedStepsNameCache = new Map(); // path -> Map<exported, stepName> (shared)
13
14
  const evaluatorsNameCache = new Map(); // path -> Map<exported, evaluatorName>
14
15
  const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exported, flowName> }
15
16
 
@@ -26,20 +27,20 @@ const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exp
26
27
  export default function stepImportRewriterAstLoader( source, inputMap ) {
27
28
  this.cacheable?.( true );
28
29
  const callback = this.async?.() ?? this.callback;
29
- const cache = { stepsNameCache, evaluatorsNameCache, workflowNameCache };
30
+ const cache = { stepsNameCache, sharedStepsNameCache, evaluatorsNameCache, workflowNameCache };
30
31
 
31
32
  try {
32
33
  const filename = this.resourcePath;
33
34
  const ast = parse( String( source ), filename );
34
35
  const fileDir = dirname( filename );
35
- const { stepImports, evaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
36
+ const { stepImports, sharedStepImports, evaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
36
37
 
37
38
  // No imports
38
- if ( [].concat( stepImports, evaluatorImports, flowImports ).length === 0 ) {
39
+ if ( [].concat( stepImports, sharedStepImports, evaluatorImports, flowImports ).length === 0 ) {
39
40
  return callback( null, source, inputMap );
40
41
  }
41
42
 
42
- const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports, flowImports } );
43
+ const rewrote = rewriteFnBodies( { ast, stepImports, sharedStepImports, evaluatorImports, flowImports } );
43
44
  // No edits performed
44
45
  if ( !rewrote ) {
45
46
  return callback( null, source, inputMap );
@@ -51,6 +51,54 @@ describe( 'workflows_rewriter Webpack loader spec', () => {
51
51
  rmSync( dir, { recursive: true, force: true } );
52
52
  } );
53
53
 
54
+ it( 'rewrites ESM shared_steps imports to invokeSharedStep', async () => {
55
+ const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-' ) );
56
+ writeFileSync( join( dir, 'shared_steps.js' ), 'export const SharedA = step({ name: \'shared.a\' })\n' );
57
+
58
+ const source = [
59
+ 'import { SharedA } from \'./shared_steps.js\';',
60
+ '',
61
+ 'const obj = {',
62
+ ' fn: async (x) => {',
63
+ ' SharedA(1);',
64
+ ' }',
65
+ '}',
66
+ ''
67
+ ].join( '\n' );
68
+
69
+ const { code } = await runLoader( source, join( dir, 'file.js' ) );
70
+
71
+ expect( code ).not.toMatch( /from '\.\/shared_steps\.js'/ );
72
+ expect( code ).toMatch( /fn:\s*async function \(x\)/ );
73
+ expect( code ).toMatch( /this\.invokeSharedStep\('shared\.a',\s*1\)/ );
74
+
75
+ rmSync( dir, { recursive: true, force: true } );
76
+ } );
77
+
78
+ it( 'rewrites CJS shared_steps requires to invokeSharedStep', async () => {
79
+ const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-' ) );
80
+ writeFileSync( join( dir, 'shared_steps.js' ), 'export const SharedB = step({ name: \'shared.b\' })\n' );
81
+
82
+ const source = [
83
+ 'const { SharedB } = require(\'./shared_steps.js\');',
84
+ '',
85
+ 'const obj = {',
86
+ ' fn: async (y) => {',
87
+ ' SharedB();',
88
+ ' }',
89
+ '}',
90
+ ''
91
+ ].join( '\n' );
92
+
93
+ const { code } = await runLoader( source, join( dir, 'file.js' ) );
94
+
95
+ expect( code ).not.toMatch( /require\('\.\/shared_steps\.js'\)/ );
96
+ expect( code ).toMatch( /fn:\s*async function \(y\)/ );
97
+ expect( code ).toMatch( /this\.invokeSharedStep\('shared\.b'\)/ );
98
+
99
+ rmSync( dir, { recursive: true, force: true } );
100
+ } );
101
+
54
102
  it( 'rewrites CJS requires and converts fn arrow to function', async () => {
55
103
  const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-' ) );
56
104
  writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: \'step.b\' })\n' );
@@ -18,11 +18,12 @@ const traverse = traverseModule.default ?? traverseModule;
18
18
  * @param {object} params
19
19
  * @param {import('@babel/types').File} params.ast - Parsed file AST.
20
20
  * @param {Array<{localName:string,stepName:string}>} params.stepImports - Step imports.
21
+ * @param {Array<{localName:string,stepName:string}>} params.sharedStepImports - Shared step imports.
21
22
  * @param {Array<{localName:string,evaluatorName:string}>} params.evaluatorImports - Evaluator imports.
22
23
  * @param {Array<{localName:string,workflowName:string}>} params.flowImports - Workflow imports.
23
24
  * @returns {boolean} True if the AST was modified; false otherwise.
24
25
  */
25
- export default function rewriteFnBodies( { ast, stepImports, evaluatorImports, flowImports } ) {
26
+ export default function rewriteFnBodies( { ast, stepImports, sharedStepImports = [], evaluatorImports, flowImports } ) {
26
27
  const state = { rewrote: false };
27
28
  traverse( ast, {
28
29
  ObjectProperty: path => {
@@ -51,25 +52,20 @@ export default function rewriteFnBodies( { ast, stepImports, evaluatorImports, f
51
52
  if ( !isIdentifier( callee ) ) {
52
53
  return;
53
54
  } // Skip: complex callee not supported
54
- const step = stepImports.find( x => x.localName === callee.name );
55
- if ( step ) {
56
- const args = cPath.node.arguments;
57
- cPath.replaceWith( createThisMethodCall( 'invokeStep', step.stepName, args ) );
58
- state.rewrote = true;
59
- return; // Stop after rewriting as step call
60
- }
61
- const evaluator = evaluatorImports.find( x => x.localName === callee.name );
62
- if ( evaluator ) {
63
- const args = cPath.node.arguments;
64
- cPath.replaceWith( createThisMethodCall( 'invokeEvaluator', evaluator.evaluatorName, args ) );
65
- state.rewrote = true;
66
- return; // Stop after rewriting as evaluator call
67
- }
68
- const flow = flowImports.find( x => x.localName === callee.name );
69
- if ( flow ) {
70
- const args = cPath.node.arguments;
71
- cPath.replaceWith( createThisMethodCall( 'startWorkflow', flow.workflowName, args ) );
72
- state.rewrote = true;
55
+ const descriptors = [
56
+ { list: stepImports, method: 'invokeStep', key: 'stepName' },
57
+ { list: sharedStepImports, method: 'invokeSharedStep', key: 'stepName' },
58
+ { list: evaluatorImports, method: 'invokeEvaluator', key: 'evaluatorName' },
59
+ { list: flowImports, method: 'startWorkflow', key: 'workflowName' }
60
+ ];
61
+ for ( const { list, method, key } of descriptors ) {
62
+ const found = list.find( x => x.localName === callee.name );
63
+ if ( found ) {
64
+ const args = cPath.node.arguments;
65
+ cPath.replaceWith( createThisMethodCall( method, found[key], args ) );
66
+ state.rewrote = true;
67
+ return;
68
+ }
73
69
  }
74
70
  }
75
71
  } );
@@ -106,6 +106,13 @@ export const toFunctionExpression = arrow => {
106
106
  */
107
107
  export const isStepsPath = value => /(^|\/)steps\.js$/.test( value );
108
108
 
109
+ /**
110
+ * Check if a module specifier or request string points to shared_steps.js.
111
+ * @param {string} value - Module path or request string.
112
+ * @returns {boolean} True if it matches shared_steps.js.
113
+ */
114
+ export const isSharedStepsPath = value => /(^|\/)shared_steps\.js$/.test( value );
115
+
109
116
  /**
110
117
  * Check if a module specifier or request string points to evaluators.js.
111
118
  * @param {string} value - Module path or request string.
@@ -215,6 +222,22 @@ export const buildStepsNameMap = ( path, cache ) => buildComponentNameMap( {
215
222
  invalidMessagePrefix: 'Invalid step name in'
216
223
  } );
217
224
 
225
+ /**
226
+ * Build a map from exported shared step identifier to declared step name.
227
+ * Parses `shared_steps.js` for `export const X = step({ name: '...' })`.
228
+ * Uses the same factory as regular steps.
229
+ *
230
+ * @param {string} path - Absolute path to the shared steps module file.
231
+ * @param {Map<string, Map<string,string>>} cache - Cache of computed name maps.
232
+ * @returns {Map<string,string>} Exported identifier -> step name.
233
+ */
234
+ export const buildSharedStepsNameMap = ( path, cache ) => buildComponentNameMap( {
235
+ path,
236
+ cache,
237
+ calleeName: 'step',
238
+ invalidMessagePrefix: 'Invalid shared step name in'
239
+ } );
240
+
218
241
  /**
219
242
  * Build a map from exported evaluator identifier to declared evaluator name.
220
243
  * Parses `evaluators.js` for `export const X = evaluator({ name: '...' })`.
@@ -1,4 +0,0 @@
1
- import { METADATA_ACCESS_SYMBOL } from '#consts';
2
-
3
- export const setMetadata = ( target, values ) =>
4
- Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );