@hexabot-ai/agentic 3.0.1-alpha.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 (176) hide show
  1. package/LICENSE.md +120 -0
  2. package/README.md +231 -0
  3. package/dist/cjs/action/abstract-action.js +141 -0
  4. package/dist/cjs/action/action.js +47 -0
  5. package/dist/cjs/action/action.types.js +7 -0
  6. package/dist/cjs/bindings/base-binding.js +278 -0
  7. package/dist/cjs/context.js +79 -0
  8. package/dist/cjs/dsl.types.js +318 -0
  9. package/dist/cjs/index.js +52 -0
  10. package/dist/cjs/runner-runtime-control.js +343 -0
  11. package/dist/cjs/step-executors/conditional-executor.js +63 -0
  12. package/dist/cjs/step-executors/loop-executor.js +187 -0
  13. package/dist/cjs/step-executors/parallel-executor.js +51 -0
  14. package/dist/cjs/step-executors/skip-helpers.js +29 -0
  15. package/dist/cjs/step-executors/task-executor.js +155 -0
  16. package/dist/cjs/step-executors/types.js +7 -0
  17. package/dist/cjs/suspension-rebuilder.js +301 -0
  18. package/dist/cjs/utils/deferred.js +17 -0
  19. package/dist/cjs/utils/naming.js +48 -0
  20. package/dist/cjs/utils/timeout.js +42 -0
  21. package/dist/cjs/utils/workflow-definition.js +102 -0
  22. package/dist/cjs/workflow-compiler.js +212 -0
  23. package/dist/cjs/workflow-event-emitter.js +40 -0
  24. package/dist/cjs/workflow-runner.js +448 -0
  25. package/dist/cjs/workflow-types.js +7 -0
  26. package/dist/cjs/workflow-values.js +99 -0
  27. package/dist/cjs/workflow.js +243 -0
  28. package/dist/esm/action/abstract-action.js +137 -0
  29. package/dist/esm/action/action.js +44 -0
  30. package/dist/esm/action/action.types.js +6 -0
  31. package/dist/esm/bindings/base-binding.js +273 -0
  32. package/dist/esm/context.js +75 -0
  33. package/dist/esm/dsl.types.js +309 -0
  34. package/dist/esm/index.js +16 -0
  35. package/dist/esm/runner-runtime-control.js +338 -0
  36. package/dist/esm/step-executors/conditional-executor.js +60 -0
  37. package/dist/esm/step-executors/loop-executor.js +182 -0
  38. package/dist/esm/step-executors/parallel-executor.js +48 -0
  39. package/dist/esm/step-executors/skip-helpers.js +25 -0
  40. package/dist/esm/step-executors/task-executor.js +152 -0
  41. package/dist/esm/step-executors/types.js +6 -0
  42. package/dist/esm/suspension-rebuilder.js +296 -0
  43. package/dist/esm/utils/deferred.js +14 -0
  44. package/dist/esm/utils/naming.js +42 -0
  45. package/dist/esm/utils/timeout.js +37 -0
  46. package/dist/esm/utils/workflow-definition.js +98 -0
  47. package/dist/esm/workflow-compiler.js +208 -0
  48. package/dist/esm/workflow-event-emitter.js +36 -0
  49. package/dist/esm/workflow-runner.js +444 -0
  50. package/dist/esm/workflow-types.js +6 -0
  51. package/dist/esm/workflow-values.js +89 -0
  52. package/dist/esm/workflow.js +236 -0
  53. package/dist/types/action/abstract-action.d.ts +54 -0
  54. package/dist/types/action/abstract-action.d.ts.map +1 -0
  55. package/dist/types/action/action.d.ts +23 -0
  56. package/dist/types/action/action.d.ts.map +1 -0
  57. package/dist/types/action/action.types.d.ts +55 -0
  58. package/dist/types/action/action.types.d.ts.map +1 -0
  59. package/dist/types/bindings/base-binding.d.ts +52 -0
  60. package/dist/types/bindings/base-binding.d.ts.map +1 -0
  61. package/dist/types/context.d.ts +105 -0
  62. package/dist/types/context.d.ts.map +1 -0
  63. package/dist/types/dsl.types.d.ts +220 -0
  64. package/dist/types/dsl.types.d.ts.map +1 -0
  65. package/dist/types/index.d.ts +18 -0
  66. package/dist/types/index.d.ts.map +1 -0
  67. package/dist/types/runner-runtime-control.d.ts +64 -0
  68. package/dist/types/runner-runtime-control.d.ts.map +1 -0
  69. package/dist/types/step-executors/conditional-executor.d.ts +13 -0
  70. package/dist/types/step-executors/conditional-executor.d.ts.map +1 -0
  71. package/dist/types/step-executors/loop-executor.d.ts +33 -0
  72. package/dist/types/step-executors/loop-executor.d.ts.map +1 -0
  73. package/dist/types/step-executors/parallel-executor.d.ts +14 -0
  74. package/dist/types/step-executors/parallel-executor.d.ts.map +1 -0
  75. package/dist/types/step-executors/skip-helpers.d.ts +12 -0
  76. package/dist/types/step-executors/skip-helpers.d.ts.map +1 -0
  77. package/dist/types/step-executors/task-executor.d.ts +13 -0
  78. package/dist/types/step-executors/task-executor.d.ts.map +1 -0
  79. package/dist/types/step-executors/types.d.ts +24 -0
  80. package/dist/types/step-executors/types.d.ts.map +1 -0
  81. package/dist/types/suspension-rebuilder.d.ts +67 -0
  82. package/dist/types/suspension-rebuilder.d.ts.map +1 -0
  83. package/dist/types/utils/deferred.d.ts +7 -0
  84. package/dist/types/utils/deferred.d.ts.map +1 -0
  85. package/dist/types/utils/naming.d.ts +24 -0
  86. package/dist/types/utils/naming.d.ts.map +1 -0
  87. package/dist/types/utils/timeout.d.ts +17 -0
  88. package/dist/types/utils/timeout.d.ts.map +1 -0
  89. package/dist/types/utils/workflow-definition.d.ts +3 -0
  90. package/dist/types/utils/workflow-definition.d.ts.map +1 -0
  91. package/dist/types/workflow-compiler.d.ts +12 -0
  92. package/dist/types/workflow-compiler.d.ts.map +1 -0
  93. package/dist/types/workflow-event-emitter.d.ts +70 -0
  94. package/dist/types/workflow-event-emitter.d.ts.map +1 -0
  95. package/dist/types/workflow-runner.d.ts +179 -0
  96. package/dist/types/workflow-runner.d.ts.map +1 -0
  97. package/dist/types/workflow-types.d.ts +195 -0
  98. package/dist/types/workflow-types.d.ts.map +1 -0
  99. package/dist/types/workflow-values.d.ts +34 -0
  100. package/dist/types/workflow-values.d.ts.map +1 -0
  101. package/dist/types/workflow.d.ts +75 -0
  102. package/dist/types/workflow.d.ts.map +1 -0
  103. package/examples/defs-bindings-agent/actions/ai-agent.ts +110 -0
  104. package/examples/defs-bindings-agent/actions/caculate-score.ts +49 -0
  105. package/examples/defs-bindings-agent/actions/index.ts +15 -0
  106. package/examples/defs-bindings-agent/bindings.ts +20 -0
  107. package/examples/defs-bindings-agent/context.ts +32 -0
  108. package/examples/defs-bindings-agent/workflow.ts +67 -0
  109. package/examples/defs-bindings-agent/workflow.yml +28 -0
  110. package/examples/full/actions/await-user-input.ts +46 -0
  111. package/examples/full/actions/call-llm.ts +133 -0
  112. package/examples/full/actions/create-ticket.ts +46 -0
  113. package/examples/full/actions/decision-router.ts +83 -0
  114. package/examples/full/actions/get-calendar-events.ts +55 -0
  115. package/examples/full/actions/get-user-profile.ts +72 -0
  116. package/examples/full/actions/index.ts +33 -0
  117. package/examples/full/actions/query-memory.ts +45 -0
  118. package/examples/full/actions/search-web.ts +53 -0
  119. package/examples/full/actions/send-email.ts +50 -0
  120. package/examples/full/context.ts +41 -0
  121. package/examples/full/workflow.ts +75 -0
  122. package/examples/full/workflow.yml +359 -0
  123. package/examples/loop/actions/await-reply.ts +47 -0
  124. package/examples/loop/actions/index.ts +19 -0
  125. package/examples/loop/actions/send-text-message.ts +40 -0
  126. package/examples/loop/context.ts +32 -0
  127. package/examples/loop/workflow.ts +66 -0
  128. package/examples/loop/workflow.yml +66 -0
  129. package/examples/suspend-resume/actions/format-reply.ts +43 -0
  130. package/examples/suspend-resume/actions/index.ts +13 -0
  131. package/examples/suspend-resume/actions/wait-for-user.ts +51 -0
  132. package/examples/suspend-resume/context.ts +32 -0
  133. package/examples/suspend-resume/workflow.ts +79 -0
  134. package/examples/suspend-resume/workflow.yml +29 -0
  135. package/package.json +60 -0
  136. package/src/__tests__/parser.test.ts +95 -0
  137. package/src/__tests__/suspension-rebuilder.test.ts +361 -0
  138. package/src/__tests__/test-helpers.ts +36 -0
  139. package/src/__tests__/validation.test.ts +526 -0
  140. package/src/__tests__/workflow-compiler.test.ts +715 -0
  141. package/src/__tests__/workflow-definition-path.test.ts +381 -0
  142. package/src/__tests__/workflow-event-emitter.test.ts +50 -0
  143. package/src/__tests__/workflow-runner.test.ts +1397 -0
  144. package/src/__tests__/workflow-values.test.ts +132 -0
  145. package/src/__tests__/workflow.test.ts +320 -0
  146. package/src/action/__tests__/abstract-action-timing.test.ts +183 -0
  147. package/src/action/__tests__/action.test.ts +473 -0
  148. package/src/action/abstract-action.ts +215 -0
  149. package/src/action/action.ts +83 -0
  150. package/src/action/action.types.ts +93 -0
  151. package/src/bindings/base-binding.ts +508 -0
  152. package/src/context.ts +179 -0
  153. package/src/dsl.types.ts +473 -0
  154. package/src/index.ts +114 -0
  155. package/src/runner-runtime-control.ts +544 -0
  156. package/src/step-executors/conditional-executor.test.ts +192 -0
  157. package/src/step-executors/conditional-executor.ts +83 -0
  158. package/src/step-executors/loop-executor.test.ts +303 -0
  159. package/src/step-executors/loop-executor.ts +299 -0
  160. package/src/step-executors/parallel-executor.test.ts +182 -0
  161. package/src/step-executors/parallel-executor.ts +77 -0
  162. package/src/step-executors/skip-helpers.ts +68 -0
  163. package/src/step-executors/task-executor.test.ts +257 -0
  164. package/src/step-executors/task-executor.ts +248 -0
  165. package/src/step-executors/types.ts +70 -0
  166. package/src/suspension-rebuilder.ts +504 -0
  167. package/src/utils/deferred.ts +23 -0
  168. package/src/utils/naming.ts +55 -0
  169. package/src/utils/timeout.ts +48 -0
  170. package/src/utils/workflow-definition.ts +173 -0
  171. package/src/workflow-compiler.ts +317 -0
  172. package/src/workflow-event-emitter.ts +86 -0
  173. package/src/workflow-runner.ts +603 -0
  174. package/src/workflow-types.ts +207 -0
  175. package/src/workflow-values.ts +159 -0
  176. package/src/workflow.ts +387 -0
@@ -0,0 +1,473 @@
1
+ /*
2
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
3
+ * Copyright (c) 2025 Hexastack.
4
+ * Full terms: see LICENSE.md.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+
9
+ import type {
10
+ BindingKindSchemas,
11
+ InferWorkflowBindings,
12
+ MountedBindingPayload,
13
+ } from '../../bindings/base-binding';
14
+ import { BaseWorkflowContext } from '../../context';
15
+ import { Settings, SettingsSchema } from '../../dsl.types';
16
+ import { EventEmitterLike } from '../../workflow-event-emitter';
17
+ import { AbstractAction } from '../abstract-action';
18
+ import {
19
+ InferActionBindings,
20
+ InferActionContext,
21
+ InferActionInput,
22
+ InferActionOutput,
23
+ InferActionSettings,
24
+ type Action,
25
+ type ActionExecutionArgs,
26
+ type ActionMetadata,
27
+ } from '../action.types';
28
+
29
+ type Equal<A, B> =
30
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
31
+ ? true
32
+ : false;
33
+ const expectType = <T extends true>(): void => {
34
+ void (0 as unknown as T);
35
+ };
36
+
37
+ class DoubleContext extends BaseWorkflowContext<{ factor: number }> {
38
+ public eventEmitter: EventEmitterLike = { emit: jest.fn(), on: jest.fn() };
39
+ }
40
+
41
+ const InputSchema = z.object({ value: z.number() });
42
+ const OutputSchema = z.object({ result: z.number() });
43
+ const NoSettingsSchema = z.any();
44
+ const _bindingKindSchemas = {
45
+ tools: {
46
+ schema: z.record(z.string(), z.unknown()),
47
+ multiple: true,
48
+ actionPolicy: 'required',
49
+ },
50
+ model: {
51
+ schema: z.strictObject({
52
+ provider: z.string(),
53
+ model_id: z.string(),
54
+ }),
55
+ multiple: false,
56
+ },
57
+ } as const satisfies BindingKindSchemas;
58
+ type TestBindings = InferWorkflowBindings<typeof _bindingKindSchemas>;
59
+
60
+ class DoubleAction extends AbstractAction<
61
+ z.infer<typeof InputSchema>,
62
+ z.infer<typeof OutputSchema>,
63
+ DoubleContext,
64
+ unknown
65
+ > {
66
+ constructor() {
67
+ const metadata: ActionMetadata<
68
+ z.infer<typeof InputSchema>,
69
+ z.infer<typeof OutputSchema>,
70
+ unknown
71
+ > = {
72
+ name: 'double_step',
73
+ description: 'Doubles the incoming value by a context-defined factor.',
74
+ inputSchema: InputSchema,
75
+ outputSchema: OutputSchema,
76
+ settingsSchema: NoSettingsSchema,
77
+ };
78
+
79
+ super(metadata);
80
+ }
81
+
82
+ async execute({
83
+ input,
84
+ context,
85
+ }: ActionExecutionArgs<
86
+ z.infer<typeof InputSchema>,
87
+ DoubleContext,
88
+ unknown
89
+ >): Promise<z.infer<typeof OutputSchema>> {
90
+ return {
91
+ result: input.value * context.state.factor * 2,
92
+ };
93
+ }
94
+ }
95
+
96
+ const DoubleSettingsSchema = z.strictObject({
97
+ multiplier: z.int().min(1),
98
+ });
99
+
100
+ class ConfigurableDoubleAction extends AbstractAction<
101
+ z.infer<typeof InputSchema>,
102
+ z.infer<typeof OutputSchema>,
103
+ DoubleContext,
104
+ z.infer<typeof DoubleSettingsSchema>
105
+ > {
106
+ constructor() {
107
+ const metadata: ActionMetadata<
108
+ z.infer<typeof InputSchema>,
109
+ z.infer<typeof OutputSchema>,
110
+ z.infer<typeof DoubleSettingsSchema>
111
+ > = {
112
+ name: 'configurable_double_step',
113
+ description: 'Doubles the incoming value by configurable multiplier.',
114
+ inputSchema: InputSchema,
115
+ outputSchema: OutputSchema,
116
+ settingsSchema: DoubleSettingsSchema,
117
+ };
118
+
119
+ super(metadata);
120
+ }
121
+
122
+ async execute({
123
+ input,
124
+ context,
125
+ settings,
126
+ }: ActionExecutionArgs<
127
+ z.infer<typeof InputSchema>,
128
+ DoubleContext,
129
+ z.infer<typeof DoubleSettingsSchema>
130
+ >): Promise<z.infer<typeof OutputSchema>> {
131
+ const base = input.value * context.state.factor * 2;
132
+ const multiplier = settings?.multiplier ?? 1;
133
+
134
+ return {
135
+ result: base * multiplier,
136
+ };
137
+ }
138
+ }
139
+
140
+ const InvalidSettingsSchema = z.strictObject({
141
+ timeout_ms: z.int().positive().max(5_000),
142
+ });
143
+
144
+ class InvalidSettingsAction extends AbstractAction<
145
+ z.infer<typeof InputSchema>,
146
+ z.infer<typeof OutputSchema>,
147
+ DoubleContext,
148
+ z.infer<typeof InvalidSettingsSchema>
149
+ > {
150
+ constructor() {
151
+ const metadata: ActionMetadata<
152
+ z.infer<typeof InputSchema>,
153
+ z.infer<typeof OutputSchema>,
154
+ z.infer<typeof InvalidSettingsSchema>
155
+ > = {
156
+ name: 'invalid_settings_step',
157
+ description: 'Invalid action that redefines base settings keys.',
158
+ inputSchema: InputSchema,
159
+ outputSchema: OutputSchema,
160
+ settingsSchema: InvalidSettingsSchema,
161
+ };
162
+
163
+ super(metadata);
164
+ }
165
+
166
+ async execute({
167
+ input,
168
+ context,
169
+ }: ActionExecutionArgs<
170
+ z.infer<typeof InputSchema>,
171
+ DoubleContext,
172
+ z.infer<typeof InvalidSettingsSchema>
173
+ >): Promise<z.infer<typeof OutputSchema>> {
174
+ return {
175
+ result: input.value * context.state.factor * 2,
176
+ };
177
+ }
178
+ }
179
+
180
+ class FlakyDoubleAction extends DoubleAction {
181
+ public attemptCount = 0;
182
+
183
+ constructor() {
184
+ super();
185
+ }
186
+
187
+ override async execute({
188
+ input,
189
+ context,
190
+ settings,
191
+ bindings,
192
+ }: ActionExecutionArgs<
193
+ z.infer<typeof InputSchema>,
194
+ DoubleContext,
195
+ unknown
196
+ >): Promise<z.infer<typeof OutputSchema>> {
197
+ this.attemptCount += 1;
198
+ if (this.attemptCount < 3) {
199
+ throw new Error('Intermittent failure');
200
+ }
201
+
202
+ return super.execute({ input, context, settings, bindings });
203
+ }
204
+ }
205
+
206
+ class BindingsAwareAction extends AbstractAction<
207
+ z.infer<typeof InputSchema>,
208
+ z.infer<typeof OutputSchema>,
209
+ DoubleContext,
210
+ unknown,
211
+ TestBindings
212
+ > {
213
+ constructor() {
214
+ const metadata: ActionMetadata<
215
+ z.infer<typeof InputSchema>,
216
+ z.infer<typeof OutputSchema>,
217
+ unknown
218
+ > = {
219
+ name: 'bindings_aware_step',
220
+ description: 'Reads workflow bindings.',
221
+ inputSchema: InputSchema,
222
+ outputSchema: OutputSchema,
223
+ settingsSchema: NoSettingsSchema,
224
+ supportedBindings: ['tools'],
225
+ };
226
+
227
+ super(metadata);
228
+ }
229
+
230
+ async execute({
231
+ input,
232
+ }: ActionExecutionArgs<
233
+ z.infer<typeof InputSchema>,
234
+ DoubleContext,
235
+ unknown,
236
+ TestBindings
237
+ >): Promise<z.infer<typeof OutputSchema>> {
238
+ return {
239
+ result: input.value,
240
+ };
241
+ }
242
+ }
243
+
244
+ describe('workflow step primitives', () => {
245
+ it('rejects invalid retry configuration via schema validation', () => {
246
+ expect(() =>
247
+ SettingsSchema.parse({ retries: { max_attempts: 0, backoff_ms: 10 } }),
248
+ ).toThrow();
249
+ });
250
+
251
+ it('parses input and output while executing run()', async () => {
252
+ const step = new DoubleAction();
253
+ const context = new DoubleContext({ factor: 3 });
254
+ const output = await step.run({ value: 2 }, context, {});
255
+
256
+ expect(output).toEqual({ result: 12 });
257
+ });
258
+
259
+ it('parses settings using the configured schema before execution', async () => {
260
+ const step = new ConfigurableDoubleAction();
261
+ const context = new DoubleContext({ factor: 3 });
262
+
263
+ await expect(
264
+ step.run({ value: 2 }, context, { multiplier: 2 }),
265
+ ).resolves.toEqual({ result: 24 });
266
+ await expect(
267
+ step.run({ value: 2 }, context, { multiplier: 1 }),
268
+ ).resolves.toEqual({ result: 12 });
269
+ await expect(
270
+ step.run({ value: 2 }, context, { multiplier: 0 }),
271
+ ).rejects.toThrow();
272
+ });
273
+
274
+ it('rejects run() bindings when the action does not support their kind', async () => {
275
+ const step = new DoubleAction();
276
+ const context = new DoubleContext({ factor: 3 });
277
+
278
+ await expect(
279
+ step.run({ value: 2 }, context, {}, {
280
+ tools: {
281
+ calculate: {
282
+ action: 'calculate_score',
283
+ settings: {},
284
+ },
285
+ },
286
+ } as any),
287
+ ).rejects.toThrow('does not support binding kind(s): tools');
288
+ });
289
+
290
+ it('accepts run() bindings when the action declares the binding kind', async () => {
291
+ const step = new BindingsAwareAction();
292
+ const context = new DoubleContext({ factor: 3 });
293
+
294
+ await expect(
295
+ step.run(
296
+ { value: 2 },
297
+ context,
298
+ {},
299
+ {
300
+ tools: {
301
+ calculate: {
302
+ action: 'calculate_score',
303
+ settings: {},
304
+ },
305
+ },
306
+ },
307
+ ),
308
+ ).resolves.toEqual({ result: 2 });
309
+ });
310
+
311
+ it('accepts base settings without requiring action schemas to extend them', async () => {
312
+ const step = new ConfigurableDoubleAction();
313
+ const context = new DoubleContext({ factor: 3 });
314
+
315
+ await expect(
316
+ step.run({ value: 2 }, context, {
317
+ multiplier: 2,
318
+ timeout_ms: 10,
319
+ retries: {
320
+ enabled: false,
321
+ max_attempts: 3,
322
+ backoff_ms: 25,
323
+ max_delay_ms: 1_000,
324
+ jitter: 0,
325
+ multiplier: 1,
326
+ },
327
+ }),
328
+ ).resolves.toEqual({ result: 24 });
329
+
330
+ expect(() => step.settingSchema.parse({ multiplier: 2 })).not.toThrow();
331
+ expect(() =>
332
+ step.settingSchema.parse({
333
+ multiplier: 2,
334
+ retries: {
335
+ enabled: false,
336
+ max_attempts: 3,
337
+ backoff_ms: 25,
338
+ max_delay_ms: 1_000,
339
+ jitter: 0,
340
+ multiplier: 1,
341
+ },
342
+ }),
343
+ ).toThrow();
344
+ });
345
+
346
+ it('rejects action schemas that redefine base setting keys', () => {
347
+ expect(() => new InvalidSettingsAction()).toThrow(
348
+ 'settingsSchema cannot redefine base settings keys: timeout_ms',
349
+ );
350
+ });
351
+
352
+ it('exposes accurate compile-time types', () => {
353
+ type ActionType = Action<
354
+ InferActionInput<DoubleAction>,
355
+ InferActionOutput<DoubleAction>,
356
+ InferActionContext<DoubleAction>
357
+ >;
358
+
359
+ expectType<Equal<InferActionInput<DoubleAction>, { value: number }>>();
360
+ expectType<Equal<InferActionOutput<DoubleAction>, { result: number }>>();
361
+ expectType<Equal<InferActionContext<DoubleAction>, DoubleContext>>();
362
+ expectType<
363
+ Equal<
364
+ InferActionSettings<ConfigurableDoubleAction>,
365
+ Settings & z.infer<typeof DoubleSettingsSchema>
366
+ >
367
+ >();
368
+ expectType<Equal<InferActionBindings<BindingsAwareAction>, TestBindings>>();
369
+ expectType<
370
+ Equal<
371
+ NonNullable<TestBindings['tools']>,
372
+ Record<
373
+ string,
374
+ MountedBindingPayload<
375
+ typeof _bindingKindSchemas.tools,
376
+ typeof _bindingKindSchemas
377
+ >
378
+ >
379
+ >
380
+ >();
381
+ expectType<
382
+ Equal<
383
+ NonNullable<TestBindings['model']>,
384
+ MountedBindingPayload<
385
+ typeof _bindingKindSchemas.model,
386
+ typeof _bindingKindSchemas
387
+ >
388
+ >
389
+ >();
390
+ type NestedToolBindings = NonNullable<
391
+ NonNullable<TestBindings['tools']>[string]['bindings']
392
+ >;
393
+ expectType<
394
+ Equal<
395
+ NonNullable<NestedToolBindings['model']>,
396
+ MountedBindingPayload<
397
+ typeof _bindingKindSchemas.model,
398
+ typeof _bindingKindSchemas
399
+ >
400
+ >
401
+ >();
402
+ expectType<
403
+ Equal<
404
+ NonNullable<NestedToolBindings['tools']>,
405
+ Record<
406
+ string,
407
+ MountedBindingPayload<
408
+ typeof _bindingKindSchemas.tools,
409
+ typeof _bindingKindSchemas
410
+ >
411
+ >
412
+ >
413
+ >();
414
+
415
+ const step: ActionType = new DoubleAction();
416
+ expect(step.name).toBe('double_step');
417
+ });
418
+
419
+ it('retries failing steps using exponential backoff', async () => {
420
+ jest.useFakeTimers();
421
+ try {
422
+ const step = new FlakyDoubleAction();
423
+ const context = new DoubleContext({ factor: 2 });
424
+ const runPromise = step.run({ value: 1 }, context, {
425
+ retries: {
426
+ enabled: true,
427
+ max_attempts: 3,
428
+ backoff_ms: 25,
429
+ max_delay_ms: 10_000,
430
+ jitter: 0,
431
+ multiplier: 1,
432
+ },
433
+ });
434
+
435
+ // First attempt happens immediately and fails
436
+ await Promise.resolve();
437
+ expect(step.attemptCount).toBe(1);
438
+
439
+ // Advance to trigger second attempt
440
+ await jest.advanceTimersByTimeAsync(25);
441
+ await Promise.resolve();
442
+ expect(step.attemptCount).toBe(2);
443
+
444
+ // Advance to trigger third attempt which should succeed
445
+ await jest.advanceTimersByTimeAsync(25);
446
+ await Promise.resolve();
447
+
448
+ await expect(runPromise).resolves.toEqual({ result: 4 });
449
+ expect(step.attemptCount).toBe(3);
450
+ } finally {
451
+ jest.useRealTimers();
452
+ }
453
+ });
454
+
455
+ it('does not retry when retries are disabled', async () => {
456
+ const step = new FlakyDoubleAction();
457
+ const context = new DoubleContext({ factor: 2 });
458
+
459
+ await expect(
460
+ step.run({ value: 1 }, context, {
461
+ retries: {
462
+ enabled: false,
463
+ max_attempts: 5,
464
+ backoff_ms: 25,
465
+ max_delay_ms: 1_000,
466
+ jitter: 0,
467
+ multiplier: 2,
468
+ },
469
+ }),
470
+ ).rejects.toThrow('Intermittent failure');
471
+ expect(step.attemptCount).toBe(1);
472
+ });
473
+ });
@@ -0,0 +1,215 @@
1
+ /*
2
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
3
+ * Copyright (c) 2025 Hexastack.
4
+ * Full terms: see LICENSE.md.
5
+ */
6
+
7
+ import { z, ZodType } from 'zod';
8
+
9
+ import { BaseWorkflowContext } from '../context';
10
+ import { BaseSettingsSchema } from '../dsl.types';
11
+ import { assertSnakeCaseName } from '../utils/naming';
12
+ import { sleep, withTimeout } from '../utils/timeout';
13
+
14
+ import {
15
+ Action,
16
+ ActionExecutionArgs,
17
+ ActionMetadata,
18
+ AnyRuntimeBindings,
19
+ RuntimeSettings,
20
+ } from './action.types';
21
+
22
+ const BASE_SETTINGS_KEYS = Object.keys(BaseSettingsSchema.shape);
23
+
24
+ /**
25
+ * Base implementation that enforces schema-validated input/output.
26
+ */
27
+ export abstract class AbstractAction<
28
+ I,
29
+ O,
30
+ C extends BaseWorkflowContext,
31
+ S,
32
+ B extends AnyRuntimeBindings = AnyRuntimeBindings,
33
+ > implements Action<I, O, C, S, B>
34
+ {
35
+ public readonly name: string;
36
+
37
+ public readonly description: string;
38
+
39
+ public readonly inputSchema: ZodType<I>;
40
+
41
+ public readonly outputSchema: ZodType<O>;
42
+
43
+ public readonly settingSchema: ZodType<S>;
44
+
45
+ public readonly supportedBindings: readonly string[];
46
+
47
+ /**
48
+ * Sets up core metadata and schemas for the action.
49
+ *
50
+ * @param metadata - Describes the action name, description, and schemas.
51
+ * @param options - Optional configuration or definition override.
52
+ */
53
+ protected constructor(metadata: ActionMetadata<I, O, S>) {
54
+ if (metadata.settingsSchema instanceof z.ZodObject) {
55
+ const actionSettingsKeys = Object.keys(metadata.settingsSchema.shape);
56
+ const overlappingKeys = actionSettingsKeys.filter((key) =>
57
+ BASE_SETTINGS_KEYS.includes(key),
58
+ );
59
+
60
+ if (overlappingKeys.length > 0) {
61
+ throw new Error(
62
+ `settingsSchema cannot redefine base settings keys: ${overlappingKeys.join(', ')}`,
63
+ );
64
+ }
65
+ }
66
+
67
+ assertSnakeCaseName(metadata.name, 'action');
68
+ this.name = metadata.name;
69
+ this.description = metadata.description;
70
+ this.inputSchema = metadata.inputSchema;
71
+ this.outputSchema = metadata.outputSchema;
72
+ this.settingSchema = metadata.settingsSchema;
73
+ this.supportedBindings = metadata.supportedBindings ?? [];
74
+ }
75
+
76
+ /**
77
+ * Parses incoming payloads using the input schema.
78
+ *
79
+ * @param payload - Raw input received by the action.
80
+ * @returns Validated input typed as `I`.
81
+ */
82
+ parseInput(payload: unknown): I {
83
+ return this.inputSchema.parse(payload);
84
+ }
85
+
86
+ /**
87
+ * Validates and returns the raw action output.
88
+ *
89
+ * @param payload - Raw output produced by {@link execute}.
90
+ * @returns Output typed as `O` after schema validation.
91
+ */
92
+ parseOutput(payload: unknown): O {
93
+ return this.outputSchema.parse(payload);
94
+ }
95
+
96
+ /**
97
+ * Executes the action with retry, timeout, and schema safety.
98
+ *
99
+ * @param payload - Raw input being provided to the action.
100
+ * @param context - Workflow context used during execution.
101
+ * @returns Validated output produced by the action.
102
+ * @throws Error when retries are exhausted or validation fails.
103
+ */
104
+ parseSettings(payload: unknown): RuntimeSettings<S> {
105
+ const settings = z
106
+ .intersection(BaseSettingsSchema, this.settingSchema)
107
+ .parse(payload ?? {});
108
+
109
+ return (settings ?? {}) as RuntimeSettings<S>;
110
+ }
111
+
112
+ async run(
113
+ payload: unknown,
114
+ context: C,
115
+ settings?: Partial<RuntimeSettings<S>>,
116
+ bindings?: B,
117
+ ): Promise<O> {
118
+ const input = this.parseInput(payload);
119
+ const parsedSettings = this.parseSettings(settings);
120
+ const parsedBindings = (bindings ?? {}) as B;
121
+ this.assertSupportedBindings(parsedBindings);
122
+ const timeoutMs = parsedSettings.timeout_ms ?? 0;
123
+ const retrySettings = parsedSettings.retries ?? {
124
+ enabled: false,
125
+ max_attempts: 1,
126
+ backoff_ms: 0,
127
+ max_delay_ms: 0,
128
+ jitter: 0,
129
+ multiplier: 1,
130
+ };
131
+ const retriesEnabled = retrySettings.enabled ?? true;
132
+ const maxAttempts = retriesEnabled ? retrySettings.max_attempts : 1;
133
+
134
+ let attempt = 0;
135
+ let currentDelay = retrySettings.backoff_ms ?? 0;
136
+ const maxDelayMs = retrySettings.max_delay_ms;
137
+ const jitter = retrySettings.jitter;
138
+ const multiplier = retrySettings.multiplier;
139
+
140
+ while (attempt < maxAttempts) {
141
+ try {
142
+ const result = await withTimeout(
143
+ this.execute({
144
+ input,
145
+ context,
146
+ settings: parsedSettings,
147
+ bindings: parsedBindings,
148
+ }),
149
+ timeoutMs,
150
+ );
151
+
152
+ return this.parseOutput(result);
153
+ } catch (error) {
154
+ attempt += 1;
155
+
156
+ if (attempt >= maxAttempts) {
157
+ throw error;
158
+ }
159
+
160
+ let delay = currentDelay;
161
+ if (maxDelayMs > 0) {
162
+ delay = Math.min(delay, maxDelayMs);
163
+ }
164
+
165
+ if (delay > 0) {
166
+ const jitterFactor =
167
+ jitter > 0 ? 1 + (Math.random() * 2 - 1) * jitter : 1;
168
+ const jitteredDelay = Math.max(0, Math.round(delay * jitterFactor));
169
+
170
+ if (jitteredDelay > 0) {
171
+ await sleep(jitteredDelay);
172
+ }
173
+ }
174
+
175
+ const nextDelay = currentDelay * multiplier;
176
+ currentDelay =
177
+ maxDelayMs > 0 ? Math.min(nextDelay, maxDelayMs) : nextDelay;
178
+ }
179
+ }
180
+
181
+ throw new Error('Action failed after exhausting retry attempts.');
182
+ }
183
+
184
+ private assertSupportedBindings(bindings: B): void {
185
+ const bindingKinds = Object.keys(
186
+ (bindings ?? {}) as Record<string, unknown>,
187
+ );
188
+ if (bindingKinds.length === 0) {
189
+ return;
190
+ }
191
+
192
+ const unsupportedKinds = bindingKinds.filter(
193
+ (bindingKind) => !this.supportedBindings.includes(bindingKind),
194
+ );
195
+ if (unsupportedKinds.length === 0) {
196
+ return;
197
+ }
198
+
199
+ const supported =
200
+ this.supportedBindings.length > 0
201
+ ? this.supportedBindings.join(', ')
202
+ : '<none>';
203
+ throw new Error(
204
+ `Action "${this.name}" does not support binding kind(s): ${unsupportedKinds.join(', ')}. Supported binding kinds: ${supported}.`,
205
+ );
206
+ }
207
+
208
+ /**
209
+ * Runs the core business logic for the action.
210
+ *
211
+ * @param args - Strongly typed input and context for the action.
212
+ * @returns Action output wrapped in a promise.
213
+ */
214
+ abstract execute(args: ActionExecutionArgs<I, C, S, B>): Promise<O>;
215
+ }