@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,715 @@
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 { Action } from '../action/action.types';
10
+ import type { BindingKindSchemas } from '../bindings/base-binding';
11
+ import { BaseWorkflowContext } from '../context';
12
+ import type { Settings, WorkflowDefinition } from '../dsl.types';
13
+ import { compileWorkflow } from '../workflow-compiler';
14
+ import { StepType, type EventEmitterLike } from '../workflow-event-emitter';
15
+
16
+ import { createTaskDefs, mergeTaskDefs } from './test-helpers';
17
+
18
+ class TestContext extends BaseWorkflowContext {
19
+ public eventEmitter: EventEmitterLike = { emit: jest.fn(), on: jest.fn() };
20
+ }
21
+
22
+ const baseRetries = {
23
+ enabled: true,
24
+ max_attempts: 3,
25
+ backoff_ms: 10,
26
+ max_delay_ms: 100,
27
+ jitter: 0,
28
+ multiplier: 2,
29
+ };
30
+ const createAction = (
31
+ overrides: Partial<
32
+ Action<unknown, unknown, BaseWorkflowContext, Settings>
33
+ > = {},
34
+ ) => {
35
+ const parseSettings = jest.fn(
36
+ (settings: unknown) =>
37
+ ({
38
+ timeout_ms: 0,
39
+ retries: baseRetries,
40
+ ...(settings as Record<string, unknown>),
41
+ parsed: true,
42
+ }) as unknown as Settings & { parsed: boolean },
43
+ ) as Action<unknown, unknown, BaseWorkflowContext, Settings>['parseSettings'];
44
+ const action: Action<unknown, unknown, BaseWorkflowContext, Settings> = {
45
+ name: 'worker_action',
46
+ description: 'test',
47
+ inputSchema: z.any(),
48
+ outputSchema: z.any(),
49
+ settingSchema: z.any(),
50
+ parseInput: jest.fn(),
51
+ parseOutput: jest.fn(),
52
+ parseSettings,
53
+ execute: jest.fn(),
54
+ run: jest.fn(),
55
+ ...overrides,
56
+ };
57
+
58
+ return { action, parseSettings };
59
+ };
60
+
61
+ describe('compileWorkflow', () => {
62
+ it('binds actions, merges settings, and builds flow metadata', () => {
63
+ const { action, parseSettings } = createAction();
64
+ const definition: WorkflowDefinition = {
65
+ defaults: {
66
+ settings: {
67
+ timeout_ms: 25,
68
+ retries: baseRetries,
69
+ },
70
+ },
71
+ defs: createTaskDefs({
72
+ worker_task: {
73
+ action: 'worker_action',
74
+ description: 'does work',
75
+ inputs: { source: '=$input.source', literal: 1 },
76
+ settings: {
77
+ timeout_ms: 50,
78
+ retries: {
79
+ enabled: true,
80
+ max_attempts: 1,
81
+ backoff_ms: 10,
82
+ max_delay_ms: 100,
83
+ jitter: 0,
84
+ multiplier: 2,
85
+ },
86
+ },
87
+ },
88
+ }),
89
+ flow: [
90
+ { do: 'worker_task' },
91
+ {
92
+ conditional: {
93
+ description: 'check',
94
+ when: [
95
+ {
96
+ condition: '=$output.worker_task.echoed',
97
+ steps: [{ do: 'worker_task' }],
98
+ },
99
+ {
100
+ else: true,
101
+ steps: [
102
+ {
103
+ parallel: {
104
+ steps: [{ do: 'worker_task' }],
105
+ strategy: 'wait_any',
106
+ },
107
+ },
108
+ ],
109
+ },
110
+ ],
111
+ },
112
+ },
113
+ {
114
+ loop: {
115
+ type: 'for_each',
116
+ name: 'looping',
117
+ description: 'loop desc',
118
+ for_each: { item: 'item', in: '=$input.items' },
119
+ accumulate: { as: 'total', initial: 0, merge: '=$accumulator + 1' },
120
+ until: '=$iteration.index > 1',
121
+ steps: [{ do: 'worker_task' }],
122
+ },
123
+ },
124
+ ],
125
+ outputs: { final: '=$output.worker_task.echoed' },
126
+ inputs: {
127
+ schema: {
128
+ source: { type: 'string' },
129
+ items: { type: 'array', items: { type: 'string' } },
130
+ },
131
+ },
132
+ };
133
+ const compiled = compileWorkflow(definition, {
134
+ actions: { worker_action: action },
135
+ });
136
+
137
+ expect(parseSettings).toHaveBeenCalledWith({
138
+ timeout_ms: 50,
139
+ retries: { ...baseRetries, max_attempts: 1 },
140
+ });
141
+ expect(compiled.tasks.worker_task.settings).toMatchObject({
142
+ parsed: true,
143
+ });
144
+
145
+ expect(compiled.flow[0]).toMatchObject({
146
+ type: StepType.Task,
147
+ id: '0:worker_task',
148
+ label: 'worker_task',
149
+ });
150
+
151
+ const conditional = compiled.flow[1];
152
+ if (conditional.type === 'conditional') {
153
+ expect(conditional.branches).toHaveLength(2);
154
+ expect(conditional.branches[0].condition).toMatchObject({
155
+ kind: 'expression',
156
+ source: '=$output.worker_task.echoed',
157
+ });
158
+ } else {
159
+ throw new Error('Expected conditional branch');
160
+ }
161
+
162
+ const loop = compiled.flow[2];
163
+ if (loop.type === 'loop') {
164
+ expect(loop.loopType).toBe('for_each');
165
+ if (loop.loopType !== 'for_each') {
166
+ throw new Error('Expected for_each loop step');
167
+ }
168
+ expect(loop.forEach.in).toMatchObject({
169
+ kind: 'expression',
170
+ source: '=$input.items',
171
+ });
172
+ expect(loop.accumulate?.merge).toMatchObject({
173
+ kind: 'expression',
174
+ source: '=$accumulator + 1',
175
+ });
176
+ } else {
177
+ throw new Error('Expected loop step');
178
+ }
179
+
180
+ expect(
181
+ compiled.inputParser.parse({ source: 'value', items: ['a'] }),
182
+ ).toEqual({ source: 'value', items: ['a'] });
183
+ expect(() =>
184
+ compiled.inputParser.parse({
185
+ source: 'value',
186
+ items: ['a'],
187
+ extra: true,
188
+ }),
189
+ ).toThrow();
190
+ });
191
+
192
+ it('throws when an action implementation is missing', () => {
193
+ const definition: WorkflowDefinition = {
194
+ defs: createTaskDefs({
195
+ missing: {
196
+ action: 'unknown_action',
197
+ inputs: { value: 1 },
198
+ },
199
+ }),
200
+ flow: [{ do: 'missing' }],
201
+ outputs: { out: '=$output.missing.value' },
202
+ };
203
+
204
+ expect(() =>
205
+ compileWorkflow(definition, {
206
+ actions: {} as Record<
207
+ string,
208
+ Action<unknown, unknown, TestContext, Settings>
209
+ >,
210
+ }),
211
+ ).toThrow(/No action implementation provided for "unknown_action"/);
212
+ });
213
+
214
+ it('compiles while loops with a pre-check condition', () => {
215
+ const { action } = createAction();
216
+ const definition: WorkflowDefinition = {
217
+ defs: createTaskDefs({
218
+ worker_task: {
219
+ action: 'worker_action',
220
+ },
221
+ }),
222
+ flow: [
223
+ {
224
+ loop: {
225
+ type: 'while',
226
+ name: 'until_valid',
227
+ while: '=$not($exists($output.worker_task.done))',
228
+ steps: [{ do: 'worker_task' }],
229
+ },
230
+ },
231
+ ],
232
+ outputs: { final: '=$output.worker_task' },
233
+ };
234
+ const compiled = compileWorkflow(definition, {
235
+ actions: { worker_action: action },
236
+ });
237
+ const loop = compiled.flow[0];
238
+
239
+ if (loop.type !== 'loop') {
240
+ throw new Error('Expected loop step');
241
+ }
242
+
243
+ expect(loop.loopType).toBe('while');
244
+ if (loop.loopType !== 'while') {
245
+ throw new Error('Expected while loop step');
246
+ }
247
+ expect(loop.while).toMatchObject({
248
+ kind: 'expression',
249
+ source: '=$not($exists($output.worker_task.done))',
250
+ });
251
+ expect('maxConcurrency' in loop).toBe(false);
252
+ });
253
+
254
+ it('mounts task bindings from defs using parsed binding payloads', () => {
255
+ const { action } = createAction({
256
+ supportedBindings: ['tools'],
257
+ });
258
+ const bindingKinds: BindingKindSchemas = {
259
+ tools: {
260
+ schema: z.strictObject({
261
+ multiplier: z.number(),
262
+ bias: z.number(),
263
+ }),
264
+ multiple: true,
265
+ actionPolicy: 'required',
266
+ },
267
+ };
268
+ const definition: WorkflowDefinition = {
269
+ defs: mergeTaskDefs(
270
+ {
271
+ worker_task: {
272
+ action: 'worker_action',
273
+ bindings: {
274
+ tools: ['calculate'],
275
+ },
276
+ },
277
+ },
278
+ {
279
+ calculate: {
280
+ kind: 'tools',
281
+ description: 'Compute a score',
282
+ action: 'calculate_score',
283
+ settings: {
284
+ multiplier: 2,
285
+ bias: 1,
286
+ },
287
+ },
288
+ },
289
+ ),
290
+ flow: [{ do: 'worker_task' }],
291
+ outputs: { out: '=$output.worker_task' },
292
+ };
293
+ const compiled = compileWorkflow(definition, {
294
+ actions: { worker_action: action, calculate_score: action },
295
+ bindingKinds,
296
+ });
297
+
298
+ expect(compiled.tasks.worker_task.bindings).toEqual({
299
+ tools: {
300
+ calculate: {
301
+ action: 'calculate_score',
302
+ settings: {
303
+ multiplier: 2,
304
+ bias: 1,
305
+ },
306
+ },
307
+ },
308
+ });
309
+ });
310
+
311
+ it('mounts nested bindings recursively', () => {
312
+ const { action } = createAction({
313
+ supportedBindings: ['tools', 'model'],
314
+ });
315
+ const bindingKinds: BindingKindSchemas = {
316
+ tools: {
317
+ schema: z.strictObject({
318
+ multiplier: z.number(),
319
+ }),
320
+ multiple: true,
321
+ actionPolicy: 'required',
322
+ supportedBindings: ['model'],
323
+ },
324
+ model: {
325
+ schema: z.strictObject({
326
+ provider: z.string(),
327
+ model: z.string(),
328
+ }),
329
+ multiple: false,
330
+ },
331
+ };
332
+ const definition: WorkflowDefinition = {
333
+ defs: mergeTaskDefs(
334
+ {
335
+ worker_task: {
336
+ action: 'worker_action',
337
+ bindings: {
338
+ tools: ['calculate'],
339
+ },
340
+ },
341
+ },
342
+ {
343
+ calculate: {
344
+ kind: 'tools',
345
+ action: 'calculate_score',
346
+ settings: {
347
+ multiplier: 2,
348
+ },
349
+ bindings: {
350
+ model: 'chat_model',
351
+ },
352
+ },
353
+ chat_model: {
354
+ kind: 'model',
355
+ settings: {
356
+ provider: 'openai',
357
+ model: 'gpt-4o-mini',
358
+ },
359
+ },
360
+ },
361
+ ),
362
+ flow: [{ do: 'worker_task' }],
363
+ outputs: { out: '=$output.worker_task' },
364
+ };
365
+ const compiled = compileWorkflow(definition, {
366
+ actions: {
367
+ worker_action: action,
368
+ calculate_score: {
369
+ ...action,
370
+ supportedBindings: ['model'],
371
+ },
372
+ },
373
+ bindingKinds,
374
+ });
375
+
376
+ expect(compiled.tasks.worker_task.bindings).toEqual({
377
+ tools: {
378
+ calculate: {
379
+ action: 'calculate_score',
380
+ settings: {
381
+ multiplier: 2,
382
+ },
383
+ bindings: {
384
+ model: {
385
+ settings: {
386
+ provider: 'openai',
387
+ model: 'gpt-4o-mini',
388
+ },
389
+ },
390
+ },
391
+ },
392
+ },
393
+ });
394
+ });
395
+
396
+ it('mounts single-ref bindings as direct payloads', () => {
397
+ const { action } = createAction({
398
+ supportedBindings: ['model'],
399
+ });
400
+ const bindingKinds: BindingKindSchemas = {
401
+ model: {
402
+ schema: z.strictObject({
403
+ provider: z.string(),
404
+ model: z.string(),
405
+ }),
406
+ multiple: false,
407
+ },
408
+ };
409
+ const definition: WorkflowDefinition = {
410
+ defs: mergeTaskDefs(
411
+ {
412
+ worker_task: {
413
+ action: 'worker_action',
414
+ bindings: {
415
+ model: 'openai_chatgpt',
416
+ },
417
+ },
418
+ },
419
+ {
420
+ openai_chatgpt: {
421
+ kind: 'model',
422
+ settings: {
423
+ provider: 'openai',
424
+ model: 'gpt-4o-mini',
425
+ },
426
+ },
427
+ },
428
+ ),
429
+ flow: [{ do: 'worker_task' }],
430
+ outputs: { out: '=$output.worker_task' },
431
+ };
432
+ const compiled = compileWorkflow(definition, {
433
+ actions: { worker_action: action },
434
+ bindingKinds,
435
+ });
436
+
437
+ expect(compiled.tasks.worker_task.bindings).toEqual({
438
+ model: {
439
+ settings: {
440
+ provider: 'openai',
441
+ model: 'gpt-4o-mini',
442
+ },
443
+ },
444
+ });
445
+ });
446
+
447
+ it('throws when single-ref binding kinds are provided as arrays', () => {
448
+ const { action } = createAction({
449
+ supportedBindings: ['model'],
450
+ });
451
+ const bindingKinds: BindingKindSchemas = {
452
+ model: {
453
+ schema: z.strictObject({
454
+ provider: z.string(),
455
+ model: z.string(),
456
+ }),
457
+ multiple: false,
458
+ },
459
+ };
460
+ const definition: WorkflowDefinition = {
461
+ defs: mergeTaskDefs(
462
+ {
463
+ worker_task: {
464
+ action: 'worker_action',
465
+ bindings: {
466
+ model: ['openai_chatgpt'],
467
+ },
468
+ },
469
+ },
470
+ {
471
+ openai_chatgpt: {
472
+ kind: 'model',
473
+ settings: {
474
+ provider: 'openai',
475
+ model: 'gpt-4o-mini',
476
+ },
477
+ },
478
+ },
479
+ ),
480
+ flow: [{ do: 'worker_task' }],
481
+ outputs: { out: '=$output.worker_task' },
482
+ };
483
+
484
+ expect(() =>
485
+ compileWorkflow(definition, {
486
+ actions: { worker_action: action },
487
+ bindingKinds,
488
+ }),
489
+ ).toThrow(/Expected a single def reference string/);
490
+ });
491
+
492
+ it('throws when multi-ref binding kinds are provided as strings', () => {
493
+ const { action } = createAction({
494
+ supportedBindings: ['tools'],
495
+ });
496
+ const bindingKinds: BindingKindSchemas = {
497
+ tools: {
498
+ schema: z.strictObject({
499
+ multiplier: z.number().optional(),
500
+ }),
501
+ multiple: true,
502
+ actionPolicy: 'required',
503
+ },
504
+ };
505
+ const definition: WorkflowDefinition = {
506
+ defs: mergeTaskDefs(
507
+ {
508
+ worker_task: {
509
+ action: 'worker_action',
510
+ bindings: {
511
+ tools: 'calculate',
512
+ },
513
+ },
514
+ },
515
+ {
516
+ calculate: {
517
+ kind: 'tools',
518
+ action: 'calculate_score',
519
+ settings: {},
520
+ },
521
+ },
522
+ ),
523
+ flow: [{ do: 'worker_task' }],
524
+ outputs: { out: '=$output.worker_task' },
525
+ };
526
+
527
+ expect(() =>
528
+ compileWorkflow(definition, {
529
+ actions: { worker_action: action, calculate_score: action },
530
+ bindingKinds,
531
+ }),
532
+ ).toThrow(/Expected an array of def references/);
533
+ });
534
+
535
+ it('throws when a task mounts bindings not supported by the action', () => {
536
+ const { action } = createAction();
537
+ const bindingKinds: BindingKindSchemas = {
538
+ tools: {
539
+ schema: z.strictObject({
540
+ multiplier: z.number().optional(),
541
+ }),
542
+ multiple: true,
543
+ actionPolicy: 'required',
544
+ },
545
+ };
546
+ const definition: WorkflowDefinition = {
547
+ defs: mergeTaskDefs(
548
+ {
549
+ worker_task: {
550
+ action: 'worker_action',
551
+ bindings: {
552
+ tools: ['calculate'],
553
+ },
554
+ },
555
+ },
556
+ {
557
+ calculate: {
558
+ kind: 'tools',
559
+ action: 'calculate_score',
560
+ settings: {},
561
+ },
562
+ },
563
+ ),
564
+ flow: [{ do: 'worker_task' }],
565
+ outputs: { out: '=$output.worker_task' },
566
+ };
567
+
568
+ expect(() =>
569
+ compileWorkflow(definition, {
570
+ actions: { worker_action: action, calculate_score: action },
571
+ bindingKinds,
572
+ }),
573
+ ).toThrow(/does not support binding kind "tools"/);
574
+ });
575
+
576
+ it('allows actions with no supported bindings when tasks do not mount bindings', () => {
577
+ const { action } = createAction();
578
+ const definition: WorkflowDefinition = {
579
+ defs: createTaskDefs({
580
+ worker_task: {
581
+ action: 'worker_action',
582
+ inputs: { value: 1 },
583
+ },
584
+ }),
585
+ flow: [{ do: 'worker_task' }],
586
+ outputs: { out: '=$output.worker_task' },
587
+ };
588
+ const compiled = compileWorkflow(definition, {
589
+ actions: { worker_action: action },
590
+ });
591
+
592
+ expect(compiled.tasks.worker_task.bindings).toEqual({});
593
+ });
594
+
595
+ it('throws when a def settings payload fails binding schema validation', () => {
596
+ const { action } = createAction({
597
+ supportedBindings: ['tools'],
598
+ });
599
+ const bindingKinds: BindingKindSchemas = {
600
+ tools: {
601
+ schema: z.strictObject({
602
+ multiplier: z.number(),
603
+ }),
604
+ multiple: true,
605
+ actionPolicy: 'required',
606
+ },
607
+ };
608
+ const definition: WorkflowDefinition = {
609
+ defs: mergeTaskDefs(
610
+ {
611
+ worker_task: {
612
+ action: 'worker_action',
613
+ bindings: {
614
+ tools: ['calculate'],
615
+ },
616
+ },
617
+ },
618
+ {
619
+ calculate: {
620
+ kind: 'tools',
621
+ action: 'calculate_score',
622
+ settings: {
623
+ multiplier: 'invalid',
624
+ } as any,
625
+ },
626
+ },
627
+ ),
628
+ flow: [{ do: 'worker_task' }],
629
+ outputs: { out: '=$output.worker_task' },
630
+ };
631
+
632
+ expect(() =>
633
+ compileWorkflow(definition, {
634
+ actions: { worker_action: action, calculate_score: action },
635
+ bindingKinds,
636
+ }),
637
+ ).toThrow(/defs\.calculate\.settings/);
638
+ });
639
+
640
+ it('throws when defs or bindings are provided without bindingKinds', () => {
641
+ const { action } = createAction({
642
+ supportedBindings: ['tools'],
643
+ });
644
+ const definition: WorkflowDefinition = {
645
+ defs: mergeTaskDefs(
646
+ {
647
+ worker_task: {
648
+ action: 'worker_action',
649
+ bindings: {
650
+ tools: ['calculate'],
651
+ },
652
+ },
653
+ },
654
+ {
655
+ calculate: {
656
+ kind: 'tools',
657
+ action: 'calculate_score',
658
+ settings: {},
659
+ },
660
+ },
661
+ ),
662
+ flow: [{ do: 'worker_task' }],
663
+ outputs: { out: '=$output.worker_task' },
664
+ };
665
+
666
+ expect(() =>
667
+ compileWorkflow(definition, {
668
+ actions: { worker_action: action, calculate_score: action },
669
+ }),
670
+ ).toThrow(/bindingKinds/);
671
+ });
672
+
673
+ it('throws when non-task defs with action do not resolve', () => {
674
+ const { action } = createAction({
675
+ supportedBindings: ['tools'],
676
+ });
677
+ const bindingKinds: BindingKindSchemas = {
678
+ tools: {
679
+ schema: z.strictObject({
680
+ multiplier: z.number().optional(),
681
+ }),
682
+ multiple: true,
683
+ actionPolicy: 'required',
684
+ },
685
+ };
686
+ const definition: WorkflowDefinition = {
687
+ defs: mergeTaskDefs(
688
+ {
689
+ worker_task: {
690
+ action: 'worker_action',
691
+ bindings: {
692
+ tools: ['calculate'],
693
+ },
694
+ },
695
+ },
696
+ {
697
+ calculate: {
698
+ kind: 'tools',
699
+ action: 'calculate_score',
700
+ settings: {},
701
+ },
702
+ },
703
+ ),
704
+ flow: [{ do: 'worker_task' }],
705
+ outputs: { out: '=$output.worker_task' },
706
+ };
707
+
708
+ expect(() =>
709
+ compileWorkflow(definition, {
710
+ actions: { worker_action: action },
711
+ bindingKinds,
712
+ }),
713
+ ).toThrow(/No action implementation provided for "calculate_score"/);
714
+ });
715
+ });