@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.
- package/LICENSE.md +120 -0
- package/README.md +231 -0
- package/dist/cjs/action/abstract-action.js +141 -0
- package/dist/cjs/action/action.js +47 -0
- package/dist/cjs/action/action.types.js +7 -0
- package/dist/cjs/bindings/base-binding.js +278 -0
- package/dist/cjs/context.js +79 -0
- package/dist/cjs/dsl.types.js +318 -0
- package/dist/cjs/index.js +52 -0
- package/dist/cjs/runner-runtime-control.js +343 -0
- package/dist/cjs/step-executors/conditional-executor.js +63 -0
- package/dist/cjs/step-executors/loop-executor.js +187 -0
- package/dist/cjs/step-executors/parallel-executor.js +51 -0
- package/dist/cjs/step-executors/skip-helpers.js +29 -0
- package/dist/cjs/step-executors/task-executor.js +155 -0
- package/dist/cjs/step-executors/types.js +7 -0
- package/dist/cjs/suspension-rebuilder.js +301 -0
- package/dist/cjs/utils/deferred.js +17 -0
- package/dist/cjs/utils/naming.js +48 -0
- package/dist/cjs/utils/timeout.js +42 -0
- package/dist/cjs/utils/workflow-definition.js +102 -0
- package/dist/cjs/workflow-compiler.js +212 -0
- package/dist/cjs/workflow-event-emitter.js +40 -0
- package/dist/cjs/workflow-runner.js +448 -0
- package/dist/cjs/workflow-types.js +7 -0
- package/dist/cjs/workflow-values.js +99 -0
- package/dist/cjs/workflow.js +243 -0
- package/dist/esm/action/abstract-action.js +137 -0
- package/dist/esm/action/action.js +44 -0
- package/dist/esm/action/action.types.js +6 -0
- package/dist/esm/bindings/base-binding.js +273 -0
- package/dist/esm/context.js +75 -0
- package/dist/esm/dsl.types.js +309 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/runner-runtime-control.js +338 -0
- package/dist/esm/step-executors/conditional-executor.js +60 -0
- package/dist/esm/step-executors/loop-executor.js +182 -0
- package/dist/esm/step-executors/parallel-executor.js +48 -0
- package/dist/esm/step-executors/skip-helpers.js +25 -0
- package/dist/esm/step-executors/task-executor.js +152 -0
- package/dist/esm/step-executors/types.js +6 -0
- package/dist/esm/suspension-rebuilder.js +296 -0
- package/dist/esm/utils/deferred.js +14 -0
- package/dist/esm/utils/naming.js +42 -0
- package/dist/esm/utils/timeout.js +37 -0
- package/dist/esm/utils/workflow-definition.js +98 -0
- package/dist/esm/workflow-compiler.js +208 -0
- package/dist/esm/workflow-event-emitter.js +36 -0
- package/dist/esm/workflow-runner.js +444 -0
- package/dist/esm/workflow-types.js +6 -0
- package/dist/esm/workflow-values.js +89 -0
- package/dist/esm/workflow.js +236 -0
- package/dist/types/action/abstract-action.d.ts +54 -0
- package/dist/types/action/abstract-action.d.ts.map +1 -0
- package/dist/types/action/action.d.ts +23 -0
- package/dist/types/action/action.d.ts.map +1 -0
- package/dist/types/action/action.types.d.ts +55 -0
- package/dist/types/action/action.types.d.ts.map +1 -0
- package/dist/types/bindings/base-binding.d.ts +52 -0
- package/dist/types/bindings/base-binding.d.ts.map +1 -0
- package/dist/types/context.d.ts +105 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/dsl.types.d.ts +220 -0
- package/dist/types/dsl.types.d.ts.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/runner-runtime-control.d.ts +64 -0
- package/dist/types/runner-runtime-control.d.ts.map +1 -0
- package/dist/types/step-executors/conditional-executor.d.ts +13 -0
- package/dist/types/step-executors/conditional-executor.d.ts.map +1 -0
- package/dist/types/step-executors/loop-executor.d.ts +33 -0
- package/dist/types/step-executors/loop-executor.d.ts.map +1 -0
- package/dist/types/step-executors/parallel-executor.d.ts +14 -0
- package/dist/types/step-executors/parallel-executor.d.ts.map +1 -0
- package/dist/types/step-executors/skip-helpers.d.ts +12 -0
- package/dist/types/step-executors/skip-helpers.d.ts.map +1 -0
- package/dist/types/step-executors/task-executor.d.ts +13 -0
- package/dist/types/step-executors/task-executor.d.ts.map +1 -0
- package/dist/types/step-executors/types.d.ts +24 -0
- package/dist/types/step-executors/types.d.ts.map +1 -0
- package/dist/types/suspension-rebuilder.d.ts +67 -0
- package/dist/types/suspension-rebuilder.d.ts.map +1 -0
- package/dist/types/utils/deferred.d.ts +7 -0
- package/dist/types/utils/deferred.d.ts.map +1 -0
- package/dist/types/utils/naming.d.ts +24 -0
- package/dist/types/utils/naming.d.ts.map +1 -0
- package/dist/types/utils/timeout.d.ts +17 -0
- package/dist/types/utils/timeout.d.ts.map +1 -0
- package/dist/types/utils/workflow-definition.d.ts +3 -0
- package/dist/types/utils/workflow-definition.d.ts.map +1 -0
- package/dist/types/workflow-compiler.d.ts +12 -0
- package/dist/types/workflow-compiler.d.ts.map +1 -0
- package/dist/types/workflow-event-emitter.d.ts +70 -0
- package/dist/types/workflow-event-emitter.d.ts.map +1 -0
- package/dist/types/workflow-runner.d.ts +179 -0
- package/dist/types/workflow-runner.d.ts.map +1 -0
- package/dist/types/workflow-types.d.ts +195 -0
- package/dist/types/workflow-types.d.ts.map +1 -0
- package/dist/types/workflow-values.d.ts +34 -0
- package/dist/types/workflow-values.d.ts.map +1 -0
- package/dist/types/workflow.d.ts +75 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/examples/defs-bindings-agent/actions/ai-agent.ts +110 -0
- package/examples/defs-bindings-agent/actions/caculate-score.ts +49 -0
- package/examples/defs-bindings-agent/actions/index.ts +15 -0
- package/examples/defs-bindings-agent/bindings.ts +20 -0
- package/examples/defs-bindings-agent/context.ts +32 -0
- package/examples/defs-bindings-agent/workflow.ts +67 -0
- package/examples/defs-bindings-agent/workflow.yml +28 -0
- package/examples/full/actions/await-user-input.ts +46 -0
- package/examples/full/actions/call-llm.ts +133 -0
- package/examples/full/actions/create-ticket.ts +46 -0
- package/examples/full/actions/decision-router.ts +83 -0
- package/examples/full/actions/get-calendar-events.ts +55 -0
- package/examples/full/actions/get-user-profile.ts +72 -0
- package/examples/full/actions/index.ts +33 -0
- package/examples/full/actions/query-memory.ts +45 -0
- package/examples/full/actions/search-web.ts +53 -0
- package/examples/full/actions/send-email.ts +50 -0
- package/examples/full/context.ts +41 -0
- package/examples/full/workflow.ts +75 -0
- package/examples/full/workflow.yml +359 -0
- package/examples/loop/actions/await-reply.ts +47 -0
- package/examples/loop/actions/index.ts +19 -0
- package/examples/loop/actions/send-text-message.ts +40 -0
- package/examples/loop/context.ts +32 -0
- package/examples/loop/workflow.ts +66 -0
- package/examples/loop/workflow.yml +66 -0
- package/examples/suspend-resume/actions/format-reply.ts +43 -0
- package/examples/suspend-resume/actions/index.ts +13 -0
- package/examples/suspend-resume/actions/wait-for-user.ts +51 -0
- package/examples/suspend-resume/context.ts +32 -0
- package/examples/suspend-resume/workflow.ts +79 -0
- package/examples/suspend-resume/workflow.yml +29 -0
- package/package.json +60 -0
- package/src/__tests__/parser.test.ts +95 -0
- package/src/__tests__/suspension-rebuilder.test.ts +361 -0
- package/src/__tests__/test-helpers.ts +36 -0
- package/src/__tests__/validation.test.ts +526 -0
- package/src/__tests__/workflow-compiler.test.ts +715 -0
- package/src/__tests__/workflow-definition-path.test.ts +381 -0
- package/src/__tests__/workflow-event-emitter.test.ts +50 -0
- package/src/__tests__/workflow-runner.test.ts +1397 -0
- package/src/__tests__/workflow-values.test.ts +132 -0
- package/src/__tests__/workflow.test.ts +320 -0
- package/src/action/__tests__/abstract-action-timing.test.ts +183 -0
- package/src/action/__tests__/action.test.ts +473 -0
- package/src/action/abstract-action.ts +215 -0
- package/src/action/action.ts +83 -0
- package/src/action/action.types.ts +93 -0
- package/src/bindings/base-binding.ts +508 -0
- package/src/context.ts +179 -0
- package/src/dsl.types.ts +473 -0
- package/src/index.ts +114 -0
- package/src/runner-runtime-control.ts +544 -0
- package/src/step-executors/conditional-executor.test.ts +192 -0
- package/src/step-executors/conditional-executor.ts +83 -0
- package/src/step-executors/loop-executor.test.ts +303 -0
- package/src/step-executors/loop-executor.ts +299 -0
- package/src/step-executors/parallel-executor.test.ts +182 -0
- package/src/step-executors/parallel-executor.ts +77 -0
- package/src/step-executors/skip-helpers.ts +68 -0
- package/src/step-executors/task-executor.test.ts +257 -0
- package/src/step-executors/task-executor.ts +248 -0
- package/src/step-executors/types.ts +70 -0
- package/src/suspension-rebuilder.ts +504 -0
- package/src/utils/deferred.ts +23 -0
- package/src/utils/naming.ts +55 -0
- package/src/utils/timeout.ts +48 -0
- package/src/utils/workflow-definition.ts +173 -0
- package/src/workflow-compiler.ts +317 -0
- package/src/workflow-event-emitter.ts +86 -0
- package/src/workflow-runner.ts +603 -0
- package/src/workflow-types.ts +207 -0
- package/src/workflow-values.ts +159 -0
- 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
|
+
}
|