@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,273 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
const TASK_KIND = 'task';
|
|
7
|
+
const resolveValidationOptions = (options) => {
|
|
8
|
+
if (!options) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
if ('bindingKinds' in options || 'actions' in options) {
|
|
12
|
+
return options;
|
|
13
|
+
}
|
|
14
|
+
return { bindingKinds: options };
|
|
15
|
+
};
|
|
16
|
+
const hasBindingsConfigured = (workflow) => {
|
|
17
|
+
const defs = workflow.defs ?? {};
|
|
18
|
+
const hasNonTaskDefs = Object.values(defs).some((definition) => definition.kind !== TASK_KIND);
|
|
19
|
+
const hasNestedBindings = Object.values(defs).some((definition) => definition.bindings && Object.keys(definition.bindings).length > 0);
|
|
20
|
+
return hasNonTaskDefs || hasNestedBindings;
|
|
21
|
+
};
|
|
22
|
+
const formatZodIssues = (issues, prefix) => issues.map((issue) => {
|
|
23
|
+
const path = issue.path.join('.') || '<root>';
|
|
24
|
+
return `${prefix}.${path}: ${issue.message}`;
|
|
25
|
+
});
|
|
26
|
+
const collectDuplicateReferences = (refs) => {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const duplicates = new Set();
|
|
29
|
+
for (const ref of refs) {
|
|
30
|
+
if (seen.has(ref)) {
|
|
31
|
+
duplicates.add(ref);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
seen.add(ref);
|
|
35
|
+
}
|
|
36
|
+
return Array.from(duplicates).sort();
|
|
37
|
+
};
|
|
38
|
+
const toBindingRefs = (refs) => Array.isArray(refs) ? refs : [refs];
|
|
39
|
+
const collectBindingRefs = (bindings) => {
|
|
40
|
+
if (!bindings) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const refs = [];
|
|
44
|
+
for (const [bindingKind, bindingRefs] of Object.entries(bindings)) {
|
|
45
|
+
for (const ref of toBindingRefs(bindingRefs)) {
|
|
46
|
+
refs.push({ kind: bindingKind, ref });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return refs;
|
|
50
|
+
};
|
|
51
|
+
const detectBindingCycles = (defs) => {
|
|
52
|
+
const errors = [];
|
|
53
|
+
const reportedCycles = new Set();
|
|
54
|
+
const states = new Map();
|
|
55
|
+
const stack = [];
|
|
56
|
+
const defNames = Object.keys(defs);
|
|
57
|
+
const visit = (defName) => {
|
|
58
|
+
const state = states.get(defName);
|
|
59
|
+
if (state === 'visited') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (state === 'visiting') {
|
|
63
|
+
const cycleStart = stack.indexOf(defName);
|
|
64
|
+
if (cycleStart === -1) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const cyclePath = [...stack.slice(cycleStart), defName];
|
|
68
|
+
const cycleKey = cyclePath.join('->');
|
|
69
|
+
if (!reportedCycles.has(cycleKey)) {
|
|
70
|
+
reportedCycles.add(cycleKey);
|
|
71
|
+
errors.push(`defs.${defName}.bindings: Circular binding reference detected (${cyclePath.join(' -> ')}).`);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
states.set(defName, 'visiting');
|
|
76
|
+
stack.push(defName);
|
|
77
|
+
const definition = defs[defName];
|
|
78
|
+
for (const { ref } of collectBindingRefs(definition?.bindings)) {
|
|
79
|
+
if (!Object.prototype.hasOwnProperty.call(defs, ref)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
visit(ref);
|
|
83
|
+
}
|
|
84
|
+
stack.pop();
|
|
85
|
+
states.set(defName, 'visited');
|
|
86
|
+
};
|
|
87
|
+
for (const defName of defNames) {
|
|
88
|
+
visit(defName);
|
|
89
|
+
}
|
|
90
|
+
return errors;
|
|
91
|
+
};
|
|
92
|
+
const resolveSupportedBindingKinds = (definition, defName, kinds, actions, errors) => {
|
|
93
|
+
if (definition.action) {
|
|
94
|
+
if (!actions) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const action = actions[definition.action];
|
|
98
|
+
if (!action) {
|
|
99
|
+
errors.push(`defs.${defName}.action: No action implementation provided for "${definition.action}".`);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return action.supportedBindings ?? [];
|
|
103
|
+
}
|
|
104
|
+
if (definition.kind === TASK_KIND) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return kinds[definition.kind]?.supportedBindings ?? [];
|
|
108
|
+
};
|
|
109
|
+
export const validateAndResolveBindings = (workflow, options) => {
|
|
110
|
+
const errors = [];
|
|
111
|
+
const resolvedDefs = {};
|
|
112
|
+
const defs = workflow.defs ?? {};
|
|
113
|
+
const resolvedOptions = resolveValidationOptions(options);
|
|
114
|
+
const kinds = resolvedOptions.bindingKinds ?? {};
|
|
115
|
+
const actions = resolvedOptions.actions;
|
|
116
|
+
const kindNames = Object.keys(kinds);
|
|
117
|
+
const hasBindingUsage = hasBindingsConfigured(workflow);
|
|
118
|
+
const parsedSettingsByDefName = new Map();
|
|
119
|
+
if (hasBindingUsage && kindNames.length === 0) {
|
|
120
|
+
errors.push('Workflows that declare non-task defs or nested bindings require a non-empty "bindingKinds" registry.');
|
|
121
|
+
}
|
|
122
|
+
for (const [defName, defDefinition] of Object.entries(defs)) {
|
|
123
|
+
if (defDefinition.kind === TASK_KIND) {
|
|
124
|
+
if (defDefinition.action && actions && !actions[defDefinition.action]) {
|
|
125
|
+
errors.push(`defs.${defName}.action: No action implementation provided for "${defDefinition.action}".`);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const kindDefinition = kinds[defDefinition.kind];
|
|
130
|
+
if (!kindDefinition) {
|
|
131
|
+
errors.push(`defs.${defName}.kind: Unknown binding kind "${defDefinition.kind}".`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const actionPolicy = kindDefinition.actionPolicy ?? 'optional';
|
|
135
|
+
if (actionPolicy === 'required' && !defDefinition.action) {
|
|
136
|
+
errors.push(`defs.${defName}.action: Binding kind "${defDefinition.kind}" requires an action.`);
|
|
137
|
+
}
|
|
138
|
+
if (actionPolicy === 'forbidden' && defDefinition.action) {
|
|
139
|
+
errors.push(`defs.${defName}.action: Binding kind "${defDefinition.kind}" does not allow action declarations.`);
|
|
140
|
+
}
|
|
141
|
+
if (defDefinition.action && actions && !actions[defDefinition.action]) {
|
|
142
|
+
errors.push(`defs.${defName}.action: No action implementation provided for "${defDefinition.action}".`);
|
|
143
|
+
}
|
|
144
|
+
const { schema } = kindDefinition;
|
|
145
|
+
const parsedPayload = schema.safeParse(defDefinition.settings);
|
|
146
|
+
if (!parsedPayload.success) {
|
|
147
|
+
errors.push(...formatZodIssues(parsedPayload.error.issues, `defs.${defName}.settings`));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
parsedSettingsByDefName.set(defName, parsedPayload.data);
|
|
151
|
+
}
|
|
152
|
+
for (const [defName, defDefinition] of Object.entries(defs)) {
|
|
153
|
+
const defBindings = defDefinition.bindings;
|
|
154
|
+
if (!defBindings) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const supportedKinds = resolveSupportedBindingKinds(defDefinition, defName, kinds, actions, errors);
|
|
158
|
+
const shouldValidateSupportedKinds = Array.isArray(supportedKinds);
|
|
159
|
+
for (const [bindingKind, bindingRefs] of Object.entries(defBindings)) {
|
|
160
|
+
const kindDefinition = kinds[bindingKind];
|
|
161
|
+
if (!kindDefinition) {
|
|
162
|
+
errors.push(`defs.${defName}.bindings.${bindingKind}: Unknown binding kind "${bindingKind}".`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const { multiple } = kindDefinition;
|
|
166
|
+
if (multiple && !Array.isArray(bindingRefs)) {
|
|
167
|
+
errors.push(`defs.${defName}.bindings.${bindingKind}: Expected an array of def references for binding kind "${bindingKind}".`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!multiple && typeof bindingRefs !== 'string') {
|
|
171
|
+
errors.push(`defs.${defName}.bindings.${bindingKind}: Expected a single def reference string for binding kind "${bindingKind}".`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (shouldValidateSupportedKinds &&
|
|
175
|
+
supportedKinds &&
|
|
176
|
+
!supportedKinds.includes(bindingKind)) {
|
|
177
|
+
const supportedKindsLabel = supportedKinds.length > 0 ? supportedKinds.join(', ') : '<none>';
|
|
178
|
+
errors.push(`defs.${defName}.bindings.${bindingKind}: "${defDefinition.action ?? defDefinition.kind}" does not support binding kind "${bindingKind}". Supported binding kinds: ${supportedKindsLabel}.`);
|
|
179
|
+
}
|
|
180
|
+
const refs = toBindingRefs(bindingRefs);
|
|
181
|
+
const duplicateRefs = collectDuplicateReferences(refs);
|
|
182
|
+
if (duplicateRefs.length > 0) {
|
|
183
|
+
errors.push(`defs.${defName}.bindings.${bindingKind}: Duplicate def reference(s): ${duplicateRefs.join(', ')}`);
|
|
184
|
+
}
|
|
185
|
+
for (const ref of refs) {
|
|
186
|
+
const definition = defs[ref];
|
|
187
|
+
if (!definition) {
|
|
188
|
+
errors.push(`defs.${defName}.bindings.${bindingKind}: Unknown def reference "${ref}".`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (definition.kind !== bindingKind) {
|
|
192
|
+
errors.push(`defs.${defName}.bindings.${bindingKind}: Def "${ref}" has kind "${definition.kind}" and cannot be mounted as "${bindingKind}".`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
errors.push(...detectBindingCycles(defs));
|
|
198
|
+
const inProgress = new Set();
|
|
199
|
+
const mountResolvedDef = (defName) => {
|
|
200
|
+
const existing = resolvedDefs[defName];
|
|
201
|
+
if (existing) {
|
|
202
|
+
return existing.payload;
|
|
203
|
+
}
|
|
204
|
+
const definition = defs[defName];
|
|
205
|
+
if (!definition || definition.kind === TASK_KIND) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const parsedSettings = parsedSettingsByDefName.get(defName);
|
|
209
|
+
if (parsedSettings === undefined) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (inProgress.has(defName)) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
inProgress.add(defName);
|
|
216
|
+
const nestedBindings = mountTaskBindings(definition.bindings, resolvedDefs, kinds, mountResolvedDef);
|
|
217
|
+
const payload = {
|
|
218
|
+
settings: parsedSettings,
|
|
219
|
+
...(definition.action ? { action: definition.action } : {}),
|
|
220
|
+
...(Object.keys(nestedBindings).length > 0
|
|
221
|
+
? { bindings: nestedBindings }
|
|
222
|
+
: {}),
|
|
223
|
+
};
|
|
224
|
+
resolvedDefs[defName] = {
|
|
225
|
+
kind: definition.kind,
|
|
226
|
+
payload,
|
|
227
|
+
};
|
|
228
|
+
inProgress.delete(defName);
|
|
229
|
+
return payload;
|
|
230
|
+
};
|
|
231
|
+
for (const [defName, definition] of Object.entries(defs)) {
|
|
232
|
+
if (definition.kind === TASK_KIND) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
mountResolvedDef(defName);
|
|
236
|
+
}
|
|
237
|
+
return { errors, resolvedDefs };
|
|
238
|
+
};
|
|
239
|
+
export const mountTaskBindings = (taskBindings, resolvedDefs, bindingKinds, resolveDef) => {
|
|
240
|
+
if (!taskBindings) {
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
const mounted = {};
|
|
244
|
+
const kinds = bindingKinds ?? {};
|
|
245
|
+
for (const [bindingKind, bindingRefs] of Object.entries(taskBindings)) {
|
|
246
|
+
const refs = toBindingRefs(bindingRefs);
|
|
247
|
+
const isMultiple = kinds[bindingKind]?.multiple ?? Array.isArray(bindingRefs);
|
|
248
|
+
if (!isMultiple) {
|
|
249
|
+
const ref = refs[0];
|
|
250
|
+
if (!ref) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const resolvedDefPayload = resolvedDefs[ref]?.payload ?? resolveDef?.(ref) ?? undefined;
|
|
254
|
+
if (!resolvedDefPayload) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
mounted[bindingKind] = resolvedDefPayload;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const mountedKindDefs = {};
|
|
261
|
+
for (const ref of refs) {
|
|
262
|
+
const resolvedDefPayload = resolvedDefs[ref]?.payload ?? resolveDef?.(ref) ?? undefined;
|
|
263
|
+
if (!resolvedDefPayload) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
mountedKindDefs[ref] = resolvedDefPayload;
|
|
267
|
+
}
|
|
268
|
+
if (Object.keys(mountedKindDefs).length > 0) {
|
|
269
|
+
mounted[bindingKind] = mountedKindDefs;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return mounted;
|
|
273
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Base context that is threaded through every workflow execution.
|
|
8
|
+
*
|
|
9
|
+
* Extend this class in user-land to expose shared services such as loggers,
|
|
10
|
+
* event emitters, database connections, or feature flags.
|
|
11
|
+
*/
|
|
12
|
+
export class BaseWorkflowContext {
|
|
13
|
+
/**
|
|
14
|
+
* Sets up the context and copies any initial state onto the instance.
|
|
15
|
+
*
|
|
16
|
+
* @param initialState - Optional properties to assign to the context instance.
|
|
17
|
+
* @param eventEmitter - Optional workflow event emitter to expose to actions.
|
|
18
|
+
*/
|
|
19
|
+
constructor(initialState) {
|
|
20
|
+
this.state = (initialState ?? {});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Snapshot the context state for telemetry or UI rendering.
|
|
24
|
+
* Override to filter or reshape the exposed state.
|
|
25
|
+
*/
|
|
26
|
+
snapshot() {
|
|
27
|
+
return safeCloneRecord(this.state);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Provides access to the workflow runtime API for the current execution.
|
|
31
|
+
*
|
|
32
|
+
* @returns Runtime control methods that allow suspension and inspection.
|
|
33
|
+
* @throws Error when the context is not currently attached to a runner.
|
|
34
|
+
*/
|
|
35
|
+
get workflow() {
|
|
36
|
+
if (!this._workflowControl) {
|
|
37
|
+
throw new Error('Workflow runtime is not attached to this context.');
|
|
38
|
+
}
|
|
39
|
+
return this._workflowControl;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Attaches or detaches the runtime control object.
|
|
43
|
+
*
|
|
44
|
+
* @param control - Runtime control instance or `undefined` to detach.
|
|
45
|
+
* @returns Nothing; the method mutates internal state.
|
|
46
|
+
*/
|
|
47
|
+
attachWorkflowRuntime(control) {
|
|
48
|
+
this._workflowControl = control;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const safeCloneRecord = (value) => {
|
|
52
|
+
if (!value) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
if (typeof structuredClone === 'function') {
|
|
56
|
+
try {
|
|
57
|
+
return structuredClone(value);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Fall through to JSON clone below.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(JSON.stringify(value));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
const shallow = {};
|
|
68
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
69
|
+
if (typeof entry !== 'function') {
|
|
70
|
+
shallow[key] = entry;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return shallow;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import jsonata from 'jsonata';
|
|
7
|
+
import { parse as parseYaml } from 'yaml';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { validateAndResolveBindings } from './bindings/base-binding';
|
|
10
|
+
export const ExpressionStringSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.regex(/^=/, 'Expression strings must start with "="')
|
|
13
|
+
.superRefine((value, ctx) => {
|
|
14
|
+
try {
|
|
15
|
+
jsonata(value.slice(1));
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const message = error instanceof Error ? error.message : 'Unknown JSONata parse error';
|
|
19
|
+
ctx.addIssue({
|
|
20
|
+
code: 'custom',
|
|
21
|
+
message: `Invalid JSONata expression: ${message}`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
// Per-action timeout in milliseconds; 0 disables the timeout wrapper.
|
|
26
|
+
export const DEFAULT_TIMEOUT_MS = 0;
|
|
27
|
+
// Retry defaults: disabled with 3 attempts and exponential backoff starting at 25ms, capped at 10s, no jitter.
|
|
28
|
+
export const DEFAULT_RETRY_SETTINGS = {
|
|
29
|
+
enabled: false,
|
|
30
|
+
max_attempts: 3,
|
|
31
|
+
backoff_ms: 25,
|
|
32
|
+
max_delay_ms: 10000,
|
|
33
|
+
jitter: 0,
|
|
34
|
+
multiplier: 1,
|
|
35
|
+
};
|
|
36
|
+
export const JsonValueSchema = z.lazy(() => z.union([
|
|
37
|
+
z.string(),
|
|
38
|
+
z.number(),
|
|
39
|
+
z.boolean(),
|
|
40
|
+
z.null(),
|
|
41
|
+
z.array(JsonValueSchema),
|
|
42
|
+
z.record(z.string(), JsonValueSchema),
|
|
43
|
+
]));
|
|
44
|
+
const InputFieldSchema = z.lazy(() => z
|
|
45
|
+
.strictObject({
|
|
46
|
+
type: z.enum([
|
|
47
|
+
'string',
|
|
48
|
+
'number',
|
|
49
|
+
'integer',
|
|
50
|
+
'boolean',
|
|
51
|
+
'array',
|
|
52
|
+
'object',
|
|
53
|
+
]),
|
|
54
|
+
description: z.string().optional(),
|
|
55
|
+
enum: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
56
|
+
items: InputFieldSchema.optional(),
|
|
57
|
+
properties: z.record(z.string(), InputFieldSchema).optional(),
|
|
58
|
+
})
|
|
59
|
+
.superRefine((value, ctx) => {
|
|
60
|
+
if (value.type === 'array' && !value.items) {
|
|
61
|
+
ctx.addIssue({
|
|
62
|
+
code: 'custom',
|
|
63
|
+
message: 'Array inputs must declare "items"',
|
|
64
|
+
path: ['items'],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (value.type !== 'array' && value.items) {
|
|
68
|
+
ctx.addIssue({
|
|
69
|
+
code: 'custom',
|
|
70
|
+
message: '"items" is only valid when type is "array"',
|
|
71
|
+
path: ['items'],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (value.type !== 'object' && value.properties) {
|
|
75
|
+
ctx.addIssue({
|
|
76
|
+
code: 'custom',
|
|
77
|
+
message: '"properties" is only valid when type is "object"',
|
|
78
|
+
path: ['properties'],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
82
|
+
// Retry policy: exponential backoff per attempt, capped by max_delay_ms, with optional jitter multiplier.
|
|
83
|
+
const RetriesSchema = z.strictObject({
|
|
84
|
+
enabled: z.boolean().optional().meta({
|
|
85
|
+
title: 'Enabled',
|
|
86
|
+
description: 'Enable retry attempts when an action fails (disabled = no retries).',
|
|
87
|
+
}),
|
|
88
|
+
max_attempts: z
|
|
89
|
+
.int()
|
|
90
|
+
.min(1)
|
|
91
|
+
.default(DEFAULT_RETRY_SETTINGS.max_attempts)
|
|
92
|
+
.meta({
|
|
93
|
+
title: 'Max Attempts',
|
|
94
|
+
description: 'Total number of attempts before giving up.',
|
|
95
|
+
}),
|
|
96
|
+
backoff_ms: z.int().min(0).default(DEFAULT_RETRY_SETTINGS.backoff_ms).meta({
|
|
97
|
+
title: 'Initial Backoff (ms)',
|
|
98
|
+
description: 'Delay in milliseconds before the first retry.',
|
|
99
|
+
}),
|
|
100
|
+
max_delay_ms: z
|
|
101
|
+
.int()
|
|
102
|
+
.min(0)
|
|
103
|
+
.default(DEFAULT_RETRY_SETTINGS.max_delay_ms)
|
|
104
|
+
.meta({
|
|
105
|
+
title: 'Max Delay (ms)',
|
|
106
|
+
description: 'Maximum delay cap in milliseconds between retries.',
|
|
107
|
+
}),
|
|
108
|
+
jitter: z.number().min(0).default(DEFAULT_RETRY_SETTINGS.jitter).meta({
|
|
109
|
+
title: 'Jitter',
|
|
110
|
+
description: 'Randomization factor applied to retry delays (0 = none).',
|
|
111
|
+
}),
|
|
112
|
+
multiplier: z
|
|
113
|
+
.number()
|
|
114
|
+
.min(1)
|
|
115
|
+
.default(DEFAULT_RETRY_SETTINGS.multiplier)
|
|
116
|
+
.meta({
|
|
117
|
+
title: 'Backoff Multiplier',
|
|
118
|
+
description: 'Multiplier applied to the delay after each retry.',
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
// Shared execution settings applied to every action invocation.
|
|
122
|
+
export const BaseSettingsSchema = z.strictObject({
|
|
123
|
+
timeout_ms: z.int().nonnegative().optional().meta({
|
|
124
|
+
title: 'Timeout (ms)',
|
|
125
|
+
description: 'Maximum runtime in milliseconds for a single action invocation (0 = disabled).',
|
|
126
|
+
}),
|
|
127
|
+
retries: RetriesSchema.optional().meta({
|
|
128
|
+
title: 'Retries',
|
|
129
|
+
description: 'Retry policy applied when an action fails.',
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
export const SettingsSchema = BaseSettingsSchema.catchall(JsonValueSchema);
|
|
133
|
+
const TaskBindingReferenceSchema = z.union([z.string(), z.array(z.string())]);
|
|
134
|
+
const TaskBindingsSchema = z.record(z.string(), TaskBindingReferenceSchema);
|
|
135
|
+
export const TASK_KIND = 'task';
|
|
136
|
+
export const TaskDefinitionSchema = z.strictObject({
|
|
137
|
+
kind: z.literal(TASK_KIND),
|
|
138
|
+
description: z.string().optional(),
|
|
139
|
+
action: z.string(),
|
|
140
|
+
inputs: z.record(z.string(), JsonValueSchema).optional(),
|
|
141
|
+
bindings: TaskBindingsSchema.optional(),
|
|
142
|
+
settings: SettingsSchema.optional(),
|
|
143
|
+
});
|
|
144
|
+
const NonTaskDefinitionSchema = z.strictObject({
|
|
145
|
+
kind: z
|
|
146
|
+
.string()
|
|
147
|
+
.refine((kind) => kind !== TASK_KIND, `kind "${TASK_KIND}" must follow the task definition shape.`),
|
|
148
|
+
description: z.string().optional(),
|
|
149
|
+
action: z.string().optional(),
|
|
150
|
+
settings: z.record(z.string(), z.unknown()),
|
|
151
|
+
bindings: TaskBindingsSchema.optional(),
|
|
152
|
+
});
|
|
153
|
+
export const DefDefinitionSchema = z.union([
|
|
154
|
+
TaskDefinitionSchema,
|
|
155
|
+
NonTaskDefinitionSchema,
|
|
156
|
+
]);
|
|
157
|
+
const ConditionalWhenSchema = z.lazy(() => z.strictObject({
|
|
158
|
+
condition: ExpressionStringSchema,
|
|
159
|
+
steps: z.array(FlowStepSchema),
|
|
160
|
+
}));
|
|
161
|
+
const ConditionalElseSchema = z.lazy(() => z.strictObject({
|
|
162
|
+
else: z.unknown().optional(),
|
|
163
|
+
steps: z.array(FlowStepSchema),
|
|
164
|
+
}));
|
|
165
|
+
const ConditionalSchema = z.strictObject({
|
|
166
|
+
description: z.string().optional(),
|
|
167
|
+
when: z.array(z.union([ConditionalWhenSchema, ConditionalElseSchema])).min(1),
|
|
168
|
+
});
|
|
169
|
+
const ParallelSchema = z.strictObject({
|
|
170
|
+
description: z.string().optional(),
|
|
171
|
+
strategy: z.enum(['wait_all', 'wait_any']).optional(),
|
|
172
|
+
steps: z.array(z.lazy(() => FlowStepSchema)),
|
|
173
|
+
});
|
|
174
|
+
const LoopAccumulateSchema = z.strictObject({
|
|
175
|
+
as: z.string(),
|
|
176
|
+
initial: JsonValueSchema,
|
|
177
|
+
merge: ExpressionStringSchema,
|
|
178
|
+
});
|
|
179
|
+
const ForEachLoopSchema = z.strictObject({
|
|
180
|
+
type: z.literal('for_each'),
|
|
181
|
+
name: z.string().optional(),
|
|
182
|
+
description: z.string().optional(),
|
|
183
|
+
for_each: z.strictObject({
|
|
184
|
+
item: z.string(),
|
|
185
|
+
in: ExpressionStringSchema,
|
|
186
|
+
}),
|
|
187
|
+
max_concurrency: z.int().positive().optional(),
|
|
188
|
+
until: ExpressionStringSchema.optional(),
|
|
189
|
+
accumulate: LoopAccumulateSchema.optional(),
|
|
190
|
+
steps: z.array(z.lazy(() => FlowStepSchema)),
|
|
191
|
+
});
|
|
192
|
+
const WhileLoopSchema = z.strictObject({
|
|
193
|
+
type: z.literal('while'),
|
|
194
|
+
name: z.string().optional(),
|
|
195
|
+
description: z.string().optional(),
|
|
196
|
+
while: ExpressionStringSchema,
|
|
197
|
+
accumulate: LoopAccumulateSchema.optional(),
|
|
198
|
+
steps: z.array(z.lazy(() => FlowStepSchema)),
|
|
199
|
+
});
|
|
200
|
+
const LoopSchema = z.lazy(() => z.union([ForEachLoopSchema, WhileLoopSchema]));
|
|
201
|
+
export const FlowStepSchema = z.lazy(() => z.union([
|
|
202
|
+
z.strictObject({
|
|
203
|
+
do: z.string(),
|
|
204
|
+
}),
|
|
205
|
+
z.strictObject({
|
|
206
|
+
parallel: ParallelSchema,
|
|
207
|
+
}),
|
|
208
|
+
z.strictObject({
|
|
209
|
+
conditional: ConditionalSchema,
|
|
210
|
+
}),
|
|
211
|
+
z.strictObject({
|
|
212
|
+
loop: LoopSchema,
|
|
213
|
+
}),
|
|
214
|
+
]));
|
|
215
|
+
export const WorkflowMetadataSchema = z.strictObject({
|
|
216
|
+
name: z.string().min(1),
|
|
217
|
+
version: z.string().min(1),
|
|
218
|
+
description: z.string().optional(),
|
|
219
|
+
});
|
|
220
|
+
const InputsSchema = z.strictObject({
|
|
221
|
+
schema: z.record(z.string(), InputFieldSchema).optional(),
|
|
222
|
+
});
|
|
223
|
+
const DefaultsSchema = z.strictObject({
|
|
224
|
+
settings: SettingsSchema.optional(),
|
|
225
|
+
});
|
|
226
|
+
export const WorkflowDefinitionSchema = z.strictObject({
|
|
227
|
+
inputs: InputsSchema.optional(),
|
|
228
|
+
context: z.record(z.string(), JsonValueSchema).optional(),
|
|
229
|
+
defaults: DefaultsSchema.optional(),
|
|
230
|
+
defs: z.record(z.string(), DefDefinitionSchema),
|
|
231
|
+
flow: z.array(FlowStepSchema),
|
|
232
|
+
outputs: z.record(z.string(), ExpressionStringSchema),
|
|
233
|
+
});
|
|
234
|
+
const collectTaskReferences = (steps) => {
|
|
235
|
+
const refs = [];
|
|
236
|
+
for (const step of steps) {
|
|
237
|
+
if ('do' in step) {
|
|
238
|
+
refs.push(step.do);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if ('parallel' in step) {
|
|
242
|
+
refs.push(...collectTaskReferences(step.parallel.steps));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if ('conditional' in step) {
|
|
246
|
+
for (const branch of step.conditional.when) {
|
|
247
|
+
if ('condition' in branch) {
|
|
248
|
+
refs.push(...collectTaskReferences(branch.steps));
|
|
249
|
+
}
|
|
250
|
+
else if ('else' in branch) {
|
|
251
|
+
refs.push(...collectTaskReferences(branch.steps));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if ('loop' in step) {
|
|
257
|
+
refs.push(...collectTaskReferences(step.loop.steps));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return refs;
|
|
261
|
+
};
|
|
262
|
+
export const isTaskDefinition = (definition) => definition.kind === TASK_KIND;
|
|
263
|
+
export const extractTaskDefinitions = (defs) => Object.entries(defs).reduce((tasks, [defName, def]) => {
|
|
264
|
+
if (isTaskDefinition(def)) {
|
|
265
|
+
tasks[defName] = def;
|
|
266
|
+
}
|
|
267
|
+
return tasks;
|
|
268
|
+
}, {});
|
|
269
|
+
const formatZodErrors = (issues) => issues.map((issue) => {
|
|
270
|
+
const path = issue.path.join('.') || '<root>';
|
|
271
|
+
return `${path}: ${issue.message}`;
|
|
272
|
+
});
|
|
273
|
+
export function validateWorkflow(input, options) {
|
|
274
|
+
let candidate = input;
|
|
275
|
+
if (typeof input === 'string') {
|
|
276
|
+
try {
|
|
277
|
+
candidate = parseYaml(input);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const message = error instanceof Error ? error.message : 'Unknown YAML parse error';
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
errors: ['Unable to parse workflow YAML', message],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const parsed = WorkflowDefinitionSchema.safeParse(candidate);
|
|
288
|
+
if (!parsed.success) {
|
|
289
|
+
return { success: false, errors: formatZodErrors(parsed.error.issues) };
|
|
290
|
+
}
|
|
291
|
+
const errors = [];
|
|
292
|
+
const taskDefinitions = extractTaskDefinitions(parsed.data.defs);
|
|
293
|
+
const referencedTasks = new Set(collectTaskReferences(parsed.data.flow));
|
|
294
|
+
const missingTasks = Array.from(referencedTasks).filter((task) => !Object.prototype.hasOwnProperty.call(taskDefinitions, task));
|
|
295
|
+
if (missingTasks.length > 0) {
|
|
296
|
+
errors.push(`Unknown task(s) referenced in flow: ${missingTasks.join(', ')}`);
|
|
297
|
+
}
|
|
298
|
+
const bindingValidation = validateAndResolveBindings(parsed.data, {
|
|
299
|
+
bindingKinds: options?.bindingKinds,
|
|
300
|
+
actions: options?.actions,
|
|
301
|
+
});
|
|
302
|
+
if (bindingValidation.errors.length > 0) {
|
|
303
|
+
errors.push(...bindingValidation.errors);
|
|
304
|
+
}
|
|
305
|
+
if (errors.length > 0) {
|
|
306
|
+
return { success: false, errors };
|
|
307
|
+
}
|
|
308
|
+
return { success: true, data: parsed.data };
|
|
309
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
export { AbstractAction } from './action/abstract-action';
|
|
7
|
+
export { defineAction } from './action/action';
|
|
8
|
+
export { BaseWorkflowContext, } from './context';
|
|
9
|
+
export * from './dsl.types';
|
|
10
|
+
export { compileWorkflow, Workflow as Workflow, WorkflowEventEmitter, WorkflowRunner, } from './workflow';
|
|
11
|
+
export { StepType } from './workflow-event-emitter';
|
|
12
|
+
export { NonDeterministicWorkflowError } from './runner-runtime-control';
|
|
13
|
+
export { compileValue, evaluateMapping, evaluateValue, mergeSettings, } from './workflow-values';
|
|
14
|
+
export { createDeferred } from './utils/deferred';
|
|
15
|
+
export { assertSnakeCaseName, isSnakeCaseName, toSnakeCase, } from './utils/naming';
|
|
16
|
+
export { sleep, withTimeout } from './utils/timeout';
|