@outputai/core 0.10.1-next.fc0a41f.0 → 0.11.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 (63) hide show
  1. package/package.json +12 -7
  2. package/src/consts.js +5 -2
  3. package/src/errors.d.ts +12 -2
  4. package/src/errors.js +17 -3
  5. package/src/errors.spec.js +19 -0
  6. package/src/helpers/activity_options.js +21 -0
  7. package/src/helpers/activity_options.spec.js +55 -0
  8. package/src/helpers/error_serializer.js +236 -0
  9. package/src/helpers/error_serializer.spec.js +413 -0
  10. package/src/helpers/errors.js +55 -13
  11. package/src/helpers/errors.spec.js +166 -66
  12. package/src/helpers/fetch.js +1 -38
  13. package/src/helpers/fetch.spec.js +1 -39
  14. package/src/helpers/function.js +13 -0
  15. package/src/helpers/function.spec.js +23 -1
  16. package/src/helpers/redact.js +51 -0
  17. package/src/helpers/redact.spec.js +66 -0
  18. package/src/helpers/string.js +10 -0
  19. package/src/helpers/string.spec.js +36 -1
  20. package/src/hooks/index.d.ts +35 -44
  21. package/src/hooks/index.js +2 -1
  22. package/src/hooks/index.spec.js +15 -7
  23. package/src/interface/evaluator.d.ts +10 -9
  24. package/src/interface/evaluator.js +1 -6
  25. package/src/interface/evaluator.spec.js +10 -10
  26. package/src/interface/step.d.ts +16 -15
  27. package/src/interface/step.js +1 -6
  28. package/src/interface/step.spec.js +10 -10
  29. package/src/interface/validations/index.js +33 -53
  30. package/src/interface/validations/index.spec.js +25 -17
  31. package/src/interface/webhook.js +1 -2
  32. package/src/interface/webhook.spec.js +25 -19
  33. package/src/interface/workflow.d.ts +25 -18
  34. package/src/interface/workflow.js +24 -52
  35. package/src/interface/workflow.spec.js +109 -108
  36. package/src/interface/workflow_activity_options.js +2 -5
  37. package/src/temporal/client.js +47 -0
  38. package/src/temporal/client.spec.js +124 -0
  39. package/src/temporal/index.d.ts +32 -0
  40. package/src/temporal/index.js +1 -0
  41. package/src/tracing/trace_engine.js +9 -11
  42. package/src/tracing/trace_engine.spec.js +67 -28
  43. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  44. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  45. package/src/worker/index.js +10 -1
  46. package/src/worker/index.spec.js +23 -8
  47. package/src/worker/interceptors/activity.js +32 -19
  48. package/src/worker/interceptors/activity.spec.js +71 -166
  49. package/src/worker/interceptors/workflow.js +53 -11
  50. package/src/worker/interceptors/workflow.spec.js +176 -49
  51. package/src/worker/loader/workflows.js +16 -3
  52. package/src/worker/loader/workflows.spec.js +37 -5
  53. package/src/worker/log_hooks.js +13 -10
  54. package/src/worker/log_hooks.spec.js +12 -4
  55. package/src/worker/sinks.js +3 -1
  56. package/src/worker/sinks.spec.js +21 -4
  57. package/src/worker/telemetry.js +2 -1
  58. package/src/worker/temporal_logger.js +33 -0
  59. package/src/worker/temporal_logger.spec.js +90 -0
  60. package/src/helpers/aggregations.js +0 -44
  61. package/src/helpers/aggregations.spec.js +0 -92
  62. package/src/tracing/tools/utils.js +0 -42
  63. package/src/tracing/tools/utils.spec.js +0 -83
@@ -79,25 +79,6 @@ export interface WorkflowDetails {
79
79
  attempt: number;
80
80
  }
81
81
 
82
- /**
83
- * Attribute totals collected while an activity executes.
84
- */
85
- export interface Aggregations {
86
- /** Cost totals collected from HTTP request cost and LLM usage attributes. */
87
- cost: {
88
- total: number;
89
- };
90
- /** Token totals collected from LLM usage attributes. */
91
- tokens: {
92
- [tokenType: string]: number | undefined;
93
- total: number;
94
- };
95
- /** HTTP request count totals. */
96
- httpRequests: {
97
- total: number;
98
- };
99
- }
100
-
101
82
  /**
102
83
  * Common hook payload fields
103
84
  */
@@ -108,24 +89,6 @@ export interface HookPayloadBase {
108
89
  eventDate: number;
109
90
  }
110
91
 
111
- /**
112
- * Payload passed to the onError() handler when a workflow, activity or runtime error occurs.
113
- */
114
- export interface ErrorHookPayload extends HookPayloadBase {
115
- /** Origin of the error: workflow execution, activity execution, or runtime. */
116
- source: 'workflow' | 'activity' | 'runtime';
117
- /** Information about the current workflow execution */
118
- workflowDetails?: WorkflowDetails;
119
- /** Temporal's activityInfo(). If source is activity */
120
- activityInfo?: Info;
121
- /** Output component kind for the activity, e.g. step, evaluator, or internal_step. */
122
- outputActivityKind: string;
123
- /** Attribute totals collected during the activity execution. */
124
- aggregations?: Aggregations | null;
125
- /** The error thrown. */
126
- error: Error;
127
- }
128
-
129
92
  /**
130
93
  * Common hook payload fields for events associated with an workflow.
131
94
  */
@@ -148,7 +111,13 @@ export type WorkflowEndHookPayload = WorkflowPayloadBase;
148
111
  * Payload passed to the onWorkflowError() handler when a workflow run fails.
149
112
  */
150
113
  export interface WorkflowErrorHookPayload extends WorkflowPayloadBase {
151
- /** The error thrown. */
114
+ /**
115
+ * Workflow error rehydrated after crossing the Temporal sandbox boundary.
116
+ * Preserves `name`, `message`, `cause`, and other enumerable properties from
117
+ * the serialized form; it is a reconstructed `Error`, not the original class.
118
+ * Extra fields are present at runtime but not on the `Error` type — narrow or
119
+ * cast in TypeScript when you need them.
120
+ */
152
121
  error: Error;
153
122
  }
154
123
 
@@ -172,21 +141,43 @@ export type ActivityStartHookPayload = ActivityPayloadBase;
172
141
  /**
173
142
  * Payload passed to the onActivityEnd() handler when an activity completes successfully.
174
143
  */
175
- export interface ActivityEndHookPayload extends ActivityPayloadBase {
176
- /** Attribute totals collected during the activity execution. */
177
- aggregations: Aggregations | null;
178
- }
144
+ export type ActivityEndHookPayload = ActivityPayloadBase;
179
145
 
180
146
  /**
181
147
  * Payload passed to the onActivityError() handler when an activity fails.
182
148
  */
183
149
  export interface ActivityErrorHookPayload extends ActivityPayloadBase {
184
- /** Attribute totals collected during the activity execution. */
185
- aggregations: Aggregations | null;
186
150
  /** The error thrown. */
187
151
  error: Error;
188
152
  }
189
153
 
154
+ /**
155
+ * Payload passed to the onError() handler when a workflow, activity or runtime error occurs.
156
+ */
157
+ export type ErrorHookPayload =
158
+ ( WorkflowPayloadBase & {
159
+ /** Workflow error origin. */
160
+ source: 'workflow';
161
+ /**
162
+ * Workflow error rehydrated after crossing the Temporal sandbox boundary.
163
+ * Preserves `name`, `message`, `cause`, and other enumerable properties from
164
+ * the serialized form; it is a reconstructed `Error`, not the original class.
165
+ * Extra fields are present at runtime but not on the `Error` type — narrow or
166
+ * cast in TypeScript when you need them.
167
+ */
168
+ error: Error;
169
+ } ) |
170
+ ( ActivityErrorHookPayload & {
171
+ /** Activity error origin. */
172
+ source: 'activity';
173
+ } ) |
174
+ ( HookPayloadBase & {
175
+ /** Worker runtime error origin. */
176
+ source: 'runtime';
177
+ /** The original error instance. */
178
+ error: Error;
179
+ } );
180
+
190
181
  /**
191
182
  * Register a handler to be invoked on workflow, activity or runtime errors.
192
183
  *
@@ -1,5 +1,6 @@
1
1
  import { mainEventBus, stepEventBus } from '#bus';
2
2
  import { BusEventType } from '#consts';
3
+ import { serializeError } from '#helpers/error_serializer';
3
4
  import { createChildLogger } from '#logger';
4
5
  import { pendingHooks } from './pending_hooks.js';
5
6
 
@@ -15,7 +16,7 @@ const log = createChildLogger( 'Hooks' );
15
16
  const callHookCb = ( fn, args, hookName ) => {
16
17
  const promise = Promise.resolve()
17
18
  .then( () => fn( args ) )
18
- .catch( e => log.error( `${hookName} hook error`, { message: e.message, stack: e.stack } ) )
19
+ .catch( e => log.error( `${hookName} hook error`, { error: serializeError( e ) } ) )
19
20
  .finally( () => pendingHooks.delete( promise ) );
20
21
  pendingHooks.add( promise );
21
22
  return promise;
@@ -5,6 +5,11 @@ const logErrorMock = vi.hoisted( () => vi.fn() );
5
5
  const createChildLoggerMock = vi.hoisted( () =>
6
6
  vi.fn( () => ( { error: logErrorMock } ) )
7
7
  );
8
+ const serializeErrorMock = vi.hoisted( () => vi.fn( error => ( {
9
+ name: error.name,
10
+ message: error.message,
11
+ stack: error.stack
12
+ } ) ) );
8
13
 
9
14
  const mainOnHandlers = vi.hoisted( () => ( {} ) );
10
15
  const stepOnHandlers = vi.hoisted( () => ( {} ) );
@@ -21,6 +26,7 @@ const stepEventBusMock = vi.hoisted( () => ( {
21
26
  } ) );
22
27
 
23
28
  vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
29
+ vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
24
30
  vi.mock( '#bus', () => ( {
25
31
  mainEventBus: mainEventBusMock,
26
32
  stepEventBus: stepEventBusMock
@@ -59,12 +65,6 @@ const activityInfo = {
59
65
 
60
66
  const eventDate = 1710000001234;
61
67
 
62
- const aggregations = {
63
- cost: { total: 0 },
64
- tokens: { total: 0 },
65
- httpRequests: { total: 1 }
66
- };
67
-
68
68
  describe( 'hooks/index', () => {
69
69
  beforeEach( () => {
70
70
  vi.clearAllMocks();
@@ -145,6 +145,14 @@ describe( 'hooks/index', () => {
145
145
  await mainOnHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', eventDate, error } );
146
146
 
147
147
  expect( handler ).toHaveBeenCalledWith( { eventId: 'evt-rt-1', eventDate, source: 'runtime', error } );
148
+ expect( logErrorMock ).toHaveBeenCalledWith( 'onError hook error', {
149
+ error: {
150
+ name: 'Error',
151
+ message: 'boom',
152
+ stack: expect.any( String )
153
+ }
154
+ } );
155
+ expect( serializeErrorMock ).toHaveBeenCalledWith( expect.objectContaining( { message: 'boom' } ) );
148
156
  } );
149
157
  } );
150
158
 
@@ -205,7 +213,7 @@ describe( 'hooks/index', () => {
205
213
  describe( 'activity lifecycle hooks', () => {
206
214
  const cases = [
207
215
  [ 'onActivityStart', onActivityStart, BusEventType.ACTIVITY_START, undefined ],
208
- [ 'onActivityEnd', onActivityEnd, BusEventType.ACTIVITY_END, { aggregations } ],
216
+ [ 'onActivityEnd', onActivityEnd, BusEventType.ACTIVITY_END ],
209
217
  [ 'onActivityError', onActivityError, BusEventType.ACTIVITY_ERROR, { error: new Error( 'activity failed' ) } ]
210
218
  ];
211
219
 
@@ -16,7 +16,7 @@ export type EvaluatorOptions = {
16
16
  /**
17
17
  * The handler function of an evaluator.
18
18
  *
19
- * @param input - The evaluator input; it matches the schema defined by `inputSchema`.
19
+ * @param input - Parsed evaluator input (`z.infer<inputSchema>`).
20
20
  *
21
21
  * @returns The result of the evaluation.
22
22
  */
@@ -30,14 +30,15 @@ export type EvaluatorFunction<
30
30
  /**
31
31
  * A wrapper around the user defined `fn` handler function.
32
32
  *
33
- * It has the same signature and returns the same value, calling the user function inside.
34
- *
35
- * It adds input validation based on the `inputSchema`.
33
+ * Callers pass values accepted by `inputSchema` (`z.input`). The wrapper parses input
34
+ * and invokes `fn`.
36
35
  */
37
- export type EvaluatorFunctionWrapper<EvaluatorFunction extends ( ...args: any ) => any> = // eslint-disable-line @typescript-eslint/no-explicit-any
38
- Parameters<EvaluatorFunction> extends [infer Input] ?
39
- ( input: Input ) => ReturnType<EvaluatorFunction> :
40
- () => ReturnType<EvaluatorFunction>;
36
+ export type EvaluatorFunctionWrapper<
37
+ InputSchema extends AnyZodSchema | undefined = undefined,
38
+ Result extends EvaluationResult = EvaluationResult
39
+ > = InputSchema extends AnyZodSchema ?
40
+ ( input: z.input<InputSchema> ) => Promise<Result> :
41
+ () => Promise<Result>;
41
42
 
42
43
  /**
43
44
  * Creates an evaluation function. It is similar to a step, but must return an EvaluationResult.
@@ -67,4 +68,4 @@ export declare function evaluator<
67
68
  inputSchema: InputSchema;
68
69
  fn: EvaluatorFunction<InputSchema, Result>;
69
70
  options?: EvaluatorOptions;
70
- } ): EvaluatorFunctionWrapper<EvaluatorFunction<InputSchema, Result>>;
71
+ } ): EvaluatorFunctionWrapper<InputSchema, Result>;
@@ -13,11 +13,6 @@ export function evaluator( { name, description, inputSchema, fn, options } ) {
13
13
  description,
14
14
  inputSchema,
15
15
  options,
16
- handler: async input => {
17
- validator.validateInput( input );
18
- const output = await fn( input );
19
- validator.validateOutput( output );
20
- return output;
21
- }
16
+ handler: async input => validator.parseOutput( await fn( validator.parseInput( input ) ) )
22
17
  } );
23
18
  }
@@ -9,8 +9,8 @@ import {
9
9
  import { ValidationError } from '#errors';
10
10
 
11
11
  const validateDefinitionMock = vi.hoisted( () => vi.fn() );
12
- const validateInputMock = vi.hoisted( () => vi.fn() );
13
- const validateOutputMock = vi.hoisted( () => vi.fn() );
12
+ const parseInputMock = vi.hoisted( () => vi.fn( input => input ) );
13
+ const parseOutputMock = vi.hoisted( () => vi.fn( output => output ) );
14
14
  const validatorConstructorMock = vi.hoisted( () => vi.fn() );
15
15
  const createEvaluatorMock = vi.hoisted( () => vi.fn( ( { handler } ) => handler ) );
16
16
 
@@ -22,8 +22,8 @@ vi.mock( './validations/index.js', () => {
22
22
 
23
23
  constructor( ...args ) {
24
24
  validatorConstructorMock( ...args );
25
- this.validateInput = validateInputMock;
26
- this.validateOutput = validateOutputMock;
25
+ this.parseInput = parseInputMock;
26
+ this.parseOutput = parseOutputMock;
27
27
  }
28
28
  }
29
29
 
@@ -87,15 +87,15 @@ describe( 'evaluator()', () => {
87
87
 
88
88
  await expect( wrapper( { value: 'input' } ) ).resolves.toBe( output );
89
89
 
90
- expect( validateInputMock ).toHaveBeenCalledWith( { value: 'input' } );
90
+ expect( parseInputMock ).toHaveBeenCalledWith( { value: 'input' } );
91
91
  expect( fn ).toHaveBeenCalledWith( { value: 'input' } );
92
- expect( validateOutputMock ).toHaveBeenCalledWith( output );
92
+ expect( parseOutputMock ).toHaveBeenCalledWith( output );
93
93
  } );
94
94
 
95
95
  it( 'does not call the evaluator function when input validation throws', async () => {
96
96
  const { evaluator } = await import( './evaluator.js' );
97
97
  const error = new ValidationError( 'invalid input' );
98
- validateInputMock.mockImplementationOnce( () => {
98
+ parseInputMock.mockImplementationOnce( () => {
99
99
  throw error;
100
100
  } );
101
101
  const fn = vi.fn();
@@ -106,13 +106,13 @@ describe( 'evaluator()', () => {
106
106
 
107
107
  await expect( wrapper( { value: 'bad' } ) ).rejects.toBe( error );
108
108
  expect( fn ).not.toHaveBeenCalled();
109
- expect( validateOutputMock ).not.toHaveBeenCalled();
109
+ expect( parseOutputMock ).not.toHaveBeenCalled();
110
110
  } );
111
111
 
112
112
  it( 'propagates output validation errors after the evaluator function runs', async () => {
113
113
  const { evaluator } = await import( './evaluator.js' );
114
114
  const error = new ValidationError( 'invalid output' );
115
- validateOutputMock.mockImplementationOnce( () => {
115
+ parseOutputMock.mockImplementationOnce( () => {
116
116
  throw error;
117
117
  } );
118
118
  const output = { value: 'not-an-evaluation-result' };
@@ -124,7 +124,7 @@ describe( 'evaluator()', () => {
124
124
 
125
125
  await expect( wrapper( { value: 'input' } ) ).rejects.toBe( error );
126
126
  expect( fn ).toHaveBeenCalledOnce();
127
- expect( validateOutputMock ).toHaveBeenCalledWith( output );
127
+ expect( parseOutputMock ).toHaveBeenCalledWith( output );
128
128
  } );
129
129
  } );
130
130
 
@@ -15,31 +15,32 @@ export type StepOptions = {
15
15
  /**
16
16
  * The handler function of a step.
17
17
  *
18
- * @param input - The step input; it matches the schema defined by `inputSchema`.
18
+ * @param input - Parsed step input (`z.infer<inputSchema>`).
19
19
  *
20
- * @returns A value matching the schema defined by `outputSchema`.
20
+ * @returns A value accepted by `outputSchema` before parse (`z.input<outputSchema>`).
21
21
  */
22
22
  export type StepFunction<
23
23
  InputSchema extends AnyZodSchema | undefined = undefined,
24
24
  OutputSchema extends AnyZodSchema | undefined = undefined
25
25
  > = InputSchema extends AnyZodSchema ?
26
- ( input: z.infer<InputSchema> ) => Promise<OutputSchema extends AnyZodSchema ? z.infer<OutputSchema> : void> :
27
- () => Promise<OutputSchema extends AnyZodSchema ? z.infer<OutputSchema> : void>;
26
+ ( input: z.infer<InputSchema> ) => Promise<OutputSchema extends AnyZodSchema ? z.input<OutputSchema> : void> :
27
+ () => Promise<OutputSchema extends AnyZodSchema ? z.input<OutputSchema> : void>;
28
28
 
29
29
  /**
30
30
  * A wrapper around the user defined `fn` handler function.
31
31
  *
32
- * It accepts the same input and returns the same value, calling the user function inside.
33
- *
34
- * It adds input and output validation based on the `inputSchema`, `outputSchema`.
32
+ * Callers pass values accepted by `inputSchema` (`z.input`). The wrapper parses input,
33
+ * invokes `fn`, parses output, and returns `z.infer<outputSchema>`.
35
34
  *
36
- * @param input - The Step input; it matches the schema defined by `inputSchema`.
37
- * @returns A value matching the schema defined by `outputSchema`.
35
+ * @param input - The step input before `inputSchema` parse.
36
+ * @returns The step output after `outputSchema` parse.
38
37
  */
39
- export type StepFunctionWrapper<StepFunction extends ( ...args: any ) => any> = // eslint-disable-line @typescript-eslint/no-explicit-any
40
- Parameters<StepFunction> extends [infer Input] ?
41
- ( input: Input ) => ReturnType<StepFunction> :
42
- () => ReturnType<StepFunction>;
38
+ export type StepFunctionWrapper<
39
+ InputSchema extends AnyZodSchema | undefined = undefined,
40
+ OutputSchema extends AnyZodSchema | undefined = undefined
41
+ > = InputSchema extends AnyZodSchema ?
42
+ ( input: z.input<InputSchema> ) => Promise<OutputSchema extends AnyZodSchema ? z.infer<OutputSchema> : void> :
43
+ () => Promise<OutputSchema extends AnyZodSchema ? z.infer<OutputSchema> : void>;
43
44
 
44
45
  /**
45
46
  * Creates a step.
@@ -123,7 +124,7 @@ export type StepFunctionWrapper<StepFunction extends ( ...args: any ) => any> =
123
124
  * @param params.outputSchema - Zod schema for the `fn` output
124
125
  * @param params.fn - A handler function containing the step code
125
126
  * @param params.options - Optional step options.
126
- * @returns The same handler function set at `fn`
127
+ * @returns A wrapper that parses input/output around `fn`
127
128
  */
128
129
  export declare function step<
129
130
  InputSchema extends AnyZodSchema | undefined = undefined,
@@ -135,4 +136,4 @@ export declare function step<
135
136
  outputSchema?: OutputSchema;
136
137
  fn: StepFunction<InputSchema, OutputSchema>;
137
138
  options?: StepOptions;
138
- } ): StepFunctionWrapper<StepFunction<InputSchema, OutputSchema>>;
139
+ } ): StepFunctionWrapper<InputSchema, OutputSchema>;
@@ -14,11 +14,6 @@ export function step( { name, description, inputSchema, outputSchema, fn, option
14
14
  inputSchema,
15
15
  outputSchema,
16
16
  options,
17
- handler: async input => {
18
- validator.validateInput( input );
19
- const output = await fn( input );
20
- validator.validateOutput( output );
21
- return output;
22
- }
17
+ handler: async input => validator.parseOutput( await fn( validator.parseInput( input ) ) )
23
18
  } );
24
19
  }
@@ -2,8 +2,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { ValidationError } from '#errors';
3
3
 
4
4
  const validateDefinitionMock = vi.hoisted( () => vi.fn() );
5
- const validateInputMock = vi.hoisted( () => vi.fn() );
6
- const validateOutputMock = vi.hoisted( () => vi.fn() );
5
+ const parseInputMock = vi.hoisted( () => vi.fn( input => input ) );
6
+ const parseOutputMock = vi.hoisted( () => vi.fn( output => output ) );
7
7
  const validatorConstructorMock = vi.hoisted( () => vi.fn() );
8
8
  const createStepMock = vi.hoisted( () => vi.fn( ( { handler } ) => handler ) );
9
9
 
@@ -15,8 +15,8 @@ vi.mock( './validations/index.js', () => {
15
15
 
16
16
  constructor( ...args ) {
17
17
  validatorConstructorMock( ...args );
18
- this.validateInput = validateInputMock;
19
- this.validateOutput = validateOutputMock;
18
+ this.parseInput = parseInputMock;
19
+ this.parseOutput = parseOutputMock;
20
20
  }
21
21
  }
22
22
 
@@ -86,15 +86,15 @@ describe( 'step()', () => {
86
86
 
87
87
  await expect( wrapper( { value: 'input' } ) ).resolves.toBe( output );
88
88
 
89
- expect( validateInputMock ).toHaveBeenCalledWith( { value: 'input' } );
89
+ expect( parseInputMock ).toHaveBeenCalledWith( { value: 'input' } );
90
90
  expect( fn ).toHaveBeenCalledWith( { value: 'input' } );
91
- expect( validateOutputMock ).toHaveBeenCalledWith( output );
91
+ expect( parseOutputMock ).toHaveBeenCalledWith( output );
92
92
  } );
93
93
 
94
94
  it( 'does not call the step function when input validation throws', async () => {
95
95
  const { step } = await import( './step.js' );
96
96
  const error = new ValidationError( 'invalid input' );
97
- validateInputMock.mockImplementationOnce( () => {
97
+ parseInputMock.mockImplementationOnce( () => {
98
98
  throw error;
99
99
  } );
100
100
  const fn = vi.fn();
@@ -105,13 +105,13 @@ describe( 'step()', () => {
105
105
 
106
106
  await expect( wrapper( { value: 'bad' } ) ).rejects.toBe( error );
107
107
  expect( fn ).not.toHaveBeenCalled();
108
- expect( validateOutputMock ).not.toHaveBeenCalled();
108
+ expect( parseOutputMock ).not.toHaveBeenCalled();
109
109
  } );
110
110
 
111
111
  it( 'propagates output validation errors after the step function runs', async () => {
112
112
  const { step } = await import( './step.js' );
113
113
  const error = new ValidationError( 'invalid output' );
114
- validateOutputMock.mockImplementationOnce( () => {
114
+ parseOutputMock.mockImplementationOnce( () => {
115
115
  throw error;
116
116
  } );
117
117
  const output = { ok: false };
@@ -123,6 +123,6 @@ describe( 'step()', () => {
123
123
 
124
124
  await expect( wrapper( { value: 'input' } ) ).rejects.toBe( error );
125
125
  expect( fn ).toHaveBeenCalledOnce();
126
- expect( validateOutputMock ).toHaveBeenCalledWith( output );
126
+ expect( parseOutputMock ).toHaveBeenCalledWith( output );
127
127
  } );
128
128
  } );
@@ -1,4 +1,5 @@
1
1
  import { ValidationError } from '#errors';
2
+ import { ComponentType } from '#consts';
2
3
  import { prettifyError } from 'zod';
3
4
  import {
4
5
  evaluatorOutputSchema,
@@ -10,84 +11,63 @@ import {
10
11
  workflowSchema
11
12
  } from './schemas.js';
12
13
 
13
- /**
14
- * Validates data using a Zod schema
15
- * @param {unknown} data
16
- * @param {ZodType} schema
17
- * @param {string} prefix - Validation error prefix
18
- * @throws {ValidationError} If validation fails
19
- * @returns {void}
20
- */
21
- const validate = ( data, schema, prefix = '' ) => {
14
+ const capitalize = word => word.at( 0 ).toUpperCase() + word.slice( 1 );
15
+
16
+ const validate = ( schema, data, prefix ) => {
22
17
  if ( !schema ) {
23
- return;
18
+ return data;
24
19
  }
25
-
26
20
  const result = schema.safeParse( data );
27
- if ( !result.success ) {
21
+ if ( result.success === false ) {
28
22
  throw new ValidationError( `${prefix} validation failed: ${prettifyError( result.error ) }` );
29
23
  }
24
+ return result.data;
30
25
  };
31
26
 
32
- export class WorkflowValidator {
27
+ export class Validator {
28
+ static label;
29
+ static definitionSchema;
30
+
33
31
  static validateDefinition( definition ) {
34
- validate( definition, workflowSchema, 'Workflow' );
32
+ validate( this.definitionSchema, definition, this.label );
35
33
  }
36
34
 
37
35
  constructor( { name, inputSchema, outputSchema } ) {
38
36
  this.name = name;
39
37
  this.inputSchema = inputSchema;
40
38
  this.outputSchema = outputSchema;
39
+ this.prefix = `${this.constructor.label} "${this.name}"`;
41
40
  }
42
41
 
43
- validateInput( input ) {
44
- validate( input, this.inputSchema, `Workflow "${this.name}" input` );
45
- }
46
-
47
- validateOutput( output ) {
48
- validate( output, this.outputSchema, `Workflow "${this.name}" output` );
42
+ parseInput( input ) {
43
+ return validate( this.inputSchema, input, `${this.prefix} input` );
49
44
  }
50
45
 
51
- validateInvocationOptions( options ) {
52
- validate( options, workflowInvocationOptionsSchema, `Workflow "${this.name}" invocation options` );
46
+ parseOutput( output ) {
47
+ return validate( this.outputSchema, output, `${this.prefix} output` );
53
48
  }
54
49
  }
55
50
 
56
- export class StepValidator {
57
- static validateDefinition( definition ) {
58
- validate( definition, stepSchema, 'Step' );
59
- }
60
-
61
- constructor( { name, inputSchema, outputSchema } ) {
62
- this.name = name;
63
- this.inputSchema = inputSchema;
64
- this.outputSchema = outputSchema;
65
- }
66
-
67
- validateInput( input ) {
68
- validate( input, this.inputSchema, `Step "${this.name}" input` );
69
- }
51
+ export class WorkflowValidator extends Validator {
52
+ static label = capitalize( ComponentType.WORKFLOW );
53
+ static definitionSchema = workflowSchema;
70
54
 
71
- validateOutput( output ) {
72
- validate( output, this.outputSchema, `Step "${this.name}" output` );
55
+ parseInvocationOptions( options ) {
56
+ return validate( workflowInvocationOptionsSchema, options, `${this.prefix} invocation options` );
73
57
  }
74
58
  }
75
59
 
76
- export class EvaluatorValidator {
77
- static validateDefinition( definition ) {
78
- validate( definition, evaluatorSchema, 'Evaluator' );
79
- }
80
- constructor( { name, inputSchema } ) {
81
- this.name = name;
82
- this.inputSchema = inputSchema;
83
- }
60
+ export class StepValidator extends Validator {
61
+ static label = capitalize( ComponentType.STEP );
62
+ static definitionSchema = stepSchema;
63
+ }
84
64
 
85
- validateInput( input ) {
86
- validate( input, this.inputSchema, `Evaluator "${this.name}" input` );
87
- }
65
+ export class EvaluatorValidator extends Validator {
66
+ static label = capitalize( ComponentType.EVALUATOR );
67
+ static definitionSchema = evaluatorSchema;
88
68
 
89
- validateOutput( output ) {
90
- validate( output, evaluatorOutputSchema, `Evaluator "${this.name}" output` );
69
+ constructor( { name, inputSchema } ) {
70
+ super( { name, inputSchema, outputSchema: evaluatorOutputSchema } );
91
71
  }
92
72
  }
93
73
 
@@ -96,7 +76,7 @@ export class EvaluatorValidator {
96
76
  * @param {object} args - The request arguments
97
77
  */
98
78
  export function validateRequestPayload( args ) {
99
- validate( args, httpRequestSchema, 'Request payload' );
79
+ validate( httpRequestSchema, args, 'Request payload' );
100
80
  };
101
81
 
102
82
  /**
@@ -104,5 +84,5 @@ export function validateRequestPayload( args ) {
104
84
  * @param {object} args - The request arguments
105
85
  */
106
86
  export function validateExecuteInParallel( args ) {
107
- validate( args, executeInParallelSchema, 'ExecuteInParallel' );
87
+ validate( executeInParallelSchema, args, 'ExecuteInParallel' );
108
88
  };