@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,36 @@
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 type {
8
+ DefDefinition,
9
+ TaskDefinition,
10
+ WorkflowDefinition,
11
+ } from '../dsl.types';
12
+
13
+ export const createTaskDef = (
14
+ definition: Omit<TaskDefinition, 'kind'>,
15
+ ): TaskDefinition => ({
16
+ kind: 'task',
17
+ ...definition,
18
+ });
19
+
20
+ export const createTaskDefs = (
21
+ tasks: Record<string, Omit<TaskDefinition, 'kind'>>,
22
+ ): WorkflowDefinition['defs'] =>
23
+ Object.fromEntries(
24
+ Object.entries(tasks).map(([taskName, taskDefinition]) => [
25
+ taskName,
26
+ createTaskDef(taskDefinition),
27
+ ]),
28
+ );
29
+
30
+ export const mergeTaskDefs = (
31
+ tasks: Record<string, Omit<TaskDefinition, 'kind'>>,
32
+ defs: Record<string, DefDefinition> = {},
33
+ ): WorkflowDefinition['defs'] => ({
34
+ ...defs,
35
+ ...createTaskDefs(tasks),
36
+ });
@@ -0,0 +1,526 @@
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 fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ import { parse as parseYaml } from 'yaml';
11
+ import { z } from 'zod';
12
+
13
+ import { validateWorkflow } from '../dsl.types';
14
+
15
+ import { mergeTaskDefs } from './test-helpers';
16
+
17
+ const fixturePath = path.join(
18
+ __dirname,
19
+ '..',
20
+ '..',
21
+ 'examples',
22
+ 'full',
23
+ 'workflow.yml',
24
+ );
25
+ const fixtureYaml = fs.readFileSync(fixturePath, 'utf8');
26
+ const bindingKinds = {
27
+ tools: {
28
+ schema: z.record(z.string(), z.unknown()),
29
+ multiple: true,
30
+ actionPolicy: 'required' as const,
31
+ },
32
+ toolset: {
33
+ schema: z.strictObject({
34
+ name: z.string(),
35
+ }),
36
+ multiple: true,
37
+ },
38
+ model: {
39
+ schema: z.strictObject({
40
+ provider: z.string(),
41
+ model: z.string(),
42
+ }),
43
+ multiple: false,
44
+ },
45
+ };
46
+
47
+ describe('validateWorkflow', () => {
48
+ it('accepts the reference workflow example', () => {
49
+ const result = validateWorkflow(fixtureYaml, { bindingKinds });
50
+
51
+ expect(result.success).toBe(true);
52
+
53
+ if (result.success) {
54
+ expect(Object.keys(result.data.defs)).toContain('understand_request');
55
+ expect(result.data.flow.length).toBeGreaterThan(0);
56
+ expect(result.data.outputs).toHaveProperty('delivered');
57
+ }
58
+ });
59
+
60
+ it('accepts loop steps with an empty body', () => {
61
+ const parsed = parseYaml(fixtureYaml) as Record<string, unknown>;
62
+ parsed.flow = [
63
+ {
64
+ loop: {
65
+ type: 'for_each',
66
+ for_each: {
67
+ item: 'item',
68
+ in: '=[]',
69
+ },
70
+ steps: [],
71
+ },
72
+ },
73
+ ];
74
+
75
+ const result = validateWorkflow(parsed, { bindingKinds });
76
+
77
+ expect(result.success).toBe(true);
78
+ });
79
+
80
+ it('accepts while loop steps with an empty body', () => {
81
+ const parsed = parseYaml(fixtureYaml) as Record<string, unknown>;
82
+ parsed.flow = [
83
+ {
84
+ loop: {
85
+ type: 'while',
86
+ while: '=true',
87
+ steps: [],
88
+ },
89
+ },
90
+ ];
91
+
92
+ const result = validateWorkflow(parsed, { bindingKinds });
93
+
94
+ expect(result.success).toBe(true);
95
+ });
96
+
97
+ it('fails while loop steps when max_concurrency is provided', () => {
98
+ const parsed = parseYaml(fixtureYaml) as Record<string, unknown>;
99
+ parsed.flow = [
100
+ {
101
+ loop: {
102
+ type: 'while',
103
+ while: '=true',
104
+ max_concurrency: 4,
105
+ steps: [],
106
+ },
107
+ },
108
+ ];
109
+
110
+ const result = validateWorkflow(parsed, { bindingKinds });
111
+
112
+ expect(result.success).toBe(false);
113
+ if (!result.success) {
114
+ expect(result.errors.length).toBeGreaterThan(0);
115
+ }
116
+ });
117
+
118
+ it('fails loop schema validation when loop.type is missing', () => {
119
+ const parsed = parseYaml(fixtureYaml) as Record<string, unknown>;
120
+ parsed.flow = [
121
+ {
122
+ loop: {
123
+ for_each: {
124
+ item: 'item',
125
+ in: '=[]',
126
+ },
127
+ steps: [],
128
+ },
129
+ },
130
+ ];
131
+ const result = validateWorkflow(parsed, { bindingKinds });
132
+
133
+ expect(result.success).toBe(false);
134
+ if (!result.success) {
135
+ expect(result.errors.length).toBeGreaterThan(0);
136
+ }
137
+ });
138
+
139
+ it('fails while loops when the while condition is missing', () => {
140
+ const parsed = parseYaml(fixtureYaml) as Record<string, unknown>;
141
+ parsed.flow = [
142
+ {
143
+ loop: {
144
+ type: 'while',
145
+ steps: [],
146
+ },
147
+ },
148
+ ];
149
+
150
+ const result = validateWorkflow(parsed, { bindingKinds });
151
+
152
+ expect(result.success).toBe(false);
153
+ if (!result.success) {
154
+ expect(result.errors.length).toBeGreaterThan(0);
155
+ }
156
+ });
157
+
158
+ it('accepts parallel steps with an empty body', () => {
159
+ const parsed = parseYaml(fixtureYaml) as Record<string, unknown>;
160
+ parsed.flow = [
161
+ {
162
+ parallel: {
163
+ steps: [],
164
+ },
165
+ },
166
+ ];
167
+
168
+ const result = validateWorkflow(parsed, { bindingKinds });
169
+
170
+ expect(result.success).toBe(true);
171
+ });
172
+
173
+ it('fails when flow references unknown tasks', () => {
174
+ const parsed = parseYaml(fixtureYaml) as Record<string, unknown>;
175
+ parsed.flow = [{ do: 'non_existent_task' }];
176
+
177
+ const result = validateWorkflow(parsed, { bindingKinds });
178
+
179
+ expect(result.success).toBe(false);
180
+ if (!result.success) {
181
+ expect(result.errors[0]).toMatch(/non_existent_task/);
182
+ }
183
+ });
184
+
185
+ it('fails schema validation when expressions are malformed', () => {
186
+ const parsed = parseYaml(fixtureYaml) as Record<string, unknown>;
187
+ parsed.flow = [
188
+ {
189
+ conditional: {
190
+ when: [
191
+ {
192
+ condition: 'missing-equals-prefix',
193
+ steps: [{ do: 'understand_request' }],
194
+ },
195
+ ],
196
+ },
197
+ },
198
+ ];
199
+
200
+ const result = validateWorkflow(parsed, { bindingKinds });
201
+
202
+ expect(result.success).toBe(false);
203
+ if (!result.success) {
204
+ expect(
205
+ result.errors.some((err) => err.includes('Expression strings')),
206
+ ).toBe(true);
207
+ }
208
+ });
209
+
210
+ it('accepts defs and task bindings when all refs and kinds are valid', () => {
211
+ const workflow = {
212
+ defs: mergeTaskDefs(
213
+ {
214
+ agent_step: {
215
+ action: 'understand_request_action',
216
+ bindings: {
217
+ tools: ['calculate'],
218
+ },
219
+ },
220
+ },
221
+ {
222
+ calculate: {
223
+ kind: 'tools',
224
+ action: 'calculate_score',
225
+ settings: { multiplier: 2 },
226
+ },
227
+ },
228
+ ),
229
+ flow: [{ do: 'agent_step' }],
230
+ outputs: { result: '=$output.agent_step' },
231
+ };
232
+ const result = validateWorkflow(workflow, { bindingKinds });
233
+
234
+ expect(result.success).toBe(true);
235
+ });
236
+
237
+ it('accepts single-ref task bindings for kinds with multiple=false', () => {
238
+ const workflow = {
239
+ defs: mergeTaskDefs(
240
+ {
241
+ agent_step: {
242
+ action: 'understand_request_action',
243
+ bindings: {
244
+ model: 'chat_model',
245
+ },
246
+ },
247
+ },
248
+ {
249
+ chat_model: {
250
+ kind: 'model',
251
+ settings: {
252
+ provider: 'openai',
253
+ model: 'gpt-4o-mini',
254
+ },
255
+ },
256
+ },
257
+ ),
258
+ flow: [{ do: 'agent_step' }],
259
+ outputs: { result: '=$output.agent_step' },
260
+ };
261
+ const result = validateWorkflow(workflow, { bindingKinds });
262
+
263
+ expect(result.success).toBe(true);
264
+ });
265
+
266
+ it('fails when single-ref binding kinds are provided as arrays', () => {
267
+ const workflow = {
268
+ defs: mergeTaskDefs(
269
+ {
270
+ agent_step: {
271
+ action: 'understand_request_action',
272
+ bindings: {
273
+ model: ['chat_model'],
274
+ },
275
+ },
276
+ },
277
+ {
278
+ chat_model: {
279
+ kind: 'model',
280
+ settings: {
281
+ provider: 'openai',
282
+ model: 'gpt-4o-mini',
283
+ },
284
+ },
285
+ },
286
+ ),
287
+ flow: [{ do: 'agent_step' }],
288
+ outputs: { result: '=$output.agent_step' },
289
+ };
290
+ const result = validateWorkflow(workflow, { bindingKinds });
291
+
292
+ expect(result.success).toBe(false);
293
+ if (!result.success) {
294
+ expect(
295
+ result.errors.some((error) =>
296
+ error.includes(
297
+ 'Expected a single def reference string for binding kind "model"',
298
+ ),
299
+ ),
300
+ ).toBe(true);
301
+ }
302
+ });
303
+
304
+ it('fails when multi-ref binding kinds are provided as strings', () => {
305
+ const workflow = {
306
+ defs: mergeTaskDefs(
307
+ {
308
+ agent_step: {
309
+ action: 'understand_request_action',
310
+ bindings: {
311
+ tools: 'calculate',
312
+ },
313
+ },
314
+ },
315
+ {
316
+ calculate: {
317
+ kind: 'tools',
318
+ action: 'calculate_score',
319
+ settings: {},
320
+ },
321
+ },
322
+ ),
323
+ flow: [{ do: 'agent_step' }],
324
+ outputs: { result: '=$output.agent_step' },
325
+ };
326
+ const result = validateWorkflow(workflow, { bindingKinds });
327
+
328
+ expect(result.success).toBe(false);
329
+ if (!result.success) {
330
+ expect(
331
+ result.errors.some((error) =>
332
+ error.includes(
333
+ 'Expected an array of def references for binding kind "tools"',
334
+ ),
335
+ ),
336
+ ).toBe(true);
337
+ }
338
+ });
339
+
340
+ it('fails when task bindings reference unknown defs', () => {
341
+ const workflow = {
342
+ defs: mergeTaskDefs(
343
+ {
344
+ agent_step: {
345
+ action: 'understand_request_action',
346
+ bindings: {
347
+ tools: ['missing_tool'],
348
+ },
349
+ },
350
+ },
351
+ {},
352
+ ),
353
+ flow: [{ do: 'agent_step' }],
354
+ outputs: { result: '=$output.agent_step' },
355
+ };
356
+ const result = validateWorkflow(workflow, { bindingKinds });
357
+
358
+ expect(result.success).toBe(false);
359
+ if (!result.success) {
360
+ expect(
361
+ result.errors.some((error) => error.includes('missing_tool')),
362
+ ).toBe(true);
363
+ }
364
+ });
365
+
366
+ it('fails when a task binding references a def with a different kind', () => {
367
+ const workflow = {
368
+ defs: mergeTaskDefs(
369
+ {
370
+ agent_step: {
371
+ action: 'understand_request_action',
372
+ bindings: {
373
+ tools: ['calculator_pack'],
374
+ },
375
+ },
376
+ },
377
+ {
378
+ calculator_pack: {
379
+ kind: 'toolset',
380
+ settings: { name: 'calc' },
381
+ },
382
+ },
383
+ ),
384
+ flow: [{ do: 'agent_step' }],
385
+ outputs: { result: '=$output.agent_step' },
386
+ };
387
+ const result = validateWorkflow(workflow, { bindingKinds });
388
+
389
+ expect(result.success).toBe(false);
390
+ if (!result.success) {
391
+ expect(
392
+ result.errors.some((error) => error.includes('cannot be mounted as')),
393
+ ).toBe(true);
394
+ }
395
+ });
396
+
397
+ it('fails when defs declare undeclared kinds', () => {
398
+ const workflow = {
399
+ defs: mergeTaskDefs(
400
+ {
401
+ agent_step: {
402
+ action: 'understand_request_action',
403
+ },
404
+ },
405
+ {
406
+ remote_server: {
407
+ kind: 'mcp_server',
408
+ settings: {
409
+ endpoint: 'http://localhost:3000',
410
+ },
411
+ },
412
+ },
413
+ ),
414
+ flow: [{ do: 'agent_step' }],
415
+ outputs: { result: '=$output.agent_step' },
416
+ };
417
+ const result = validateWorkflow(workflow, { bindingKinds });
418
+
419
+ expect(result.success).toBe(false);
420
+ if (!result.success) {
421
+ expect(
422
+ result.errors.some((error) => error.includes('Unknown binding kind')),
423
+ ).toBe(true);
424
+ }
425
+ });
426
+
427
+ it('fails when task bindings use undeclared kinds', () => {
428
+ const workflow = {
429
+ defs: mergeTaskDefs(
430
+ {
431
+ agent_step: {
432
+ action: 'understand_request_action',
433
+ bindings: {
434
+ mcp_server: ['calculate'],
435
+ },
436
+ },
437
+ },
438
+ {
439
+ calculate: {
440
+ kind: 'tools',
441
+ action: 'calculate_score',
442
+ settings: {},
443
+ },
444
+ },
445
+ ),
446
+ flow: [{ do: 'agent_step' }],
447
+ outputs: { result: '=$output.agent_step' },
448
+ };
449
+ const result = validateWorkflow(workflow, { bindingKinds });
450
+
451
+ expect(result.success).toBe(false);
452
+ if (!result.success) {
453
+ expect(
454
+ result.errors.some((error) =>
455
+ error.includes('defs.agent_step.bindings.mcp_server'),
456
+ ),
457
+ ).toBe(true);
458
+ }
459
+ });
460
+
461
+ it('fails when defs or bindings are present without bindingKinds', () => {
462
+ const workflow = {
463
+ defs: mergeTaskDefs(
464
+ {
465
+ agent_step: {
466
+ action: 'understand_request_action',
467
+ bindings: {
468
+ tools: ['calculate'],
469
+ },
470
+ },
471
+ },
472
+ {
473
+ calculate: {
474
+ kind: 'tools',
475
+ action: 'calculate_score',
476
+ settings: {},
477
+ },
478
+ },
479
+ ),
480
+ flow: [{ do: 'agent_step' }],
481
+ outputs: { result: '=$output.agent_step' },
482
+ };
483
+ const result = validateWorkflow(workflow);
484
+
485
+ expect(result.success).toBe(false);
486
+ if (!result.success) {
487
+ expect(
488
+ result.errors.some((error) => error.includes('bindingKinds')),
489
+ ).toBe(true);
490
+ }
491
+ });
492
+
493
+ it('fails when task bindings include duplicate refs', () => {
494
+ const workflow = {
495
+ defs: mergeTaskDefs(
496
+ {
497
+ agent_step: {
498
+ action: 'understand_request_action',
499
+ bindings: {
500
+ tools: ['calculate', 'calculate'],
501
+ },
502
+ },
503
+ },
504
+ {
505
+ calculate: {
506
+ kind: 'tools',
507
+ action: 'calculate_score',
508
+ settings: {},
509
+ },
510
+ },
511
+ ),
512
+ flow: [{ do: 'agent_step' }],
513
+ outputs: { result: '=$output.agent_step' },
514
+ };
515
+ const result = validateWorkflow(workflow, { bindingKinds });
516
+
517
+ expect(result.success).toBe(false);
518
+ if (!result.success) {
519
+ expect(
520
+ result.errors.some((error) =>
521
+ error.includes('Duplicate def reference'),
522
+ ),
523
+ ).toBe(true);
524
+ }
525
+ });
526
+ });