@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,257 @@
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 { Action } from '../action/action.types';
8
+ import { BaseWorkflowContext } from '../context';
9
+ import type { RuntimeSuspensionRequest } from '../runner-runtime-control';
10
+ import { createDeferred } from '../utils/deferred';
11
+ import {
12
+ type EventEmitterLike,
13
+ type StepInfo,
14
+ StepType,
15
+ } from '../workflow-event-emitter';
16
+ import type {
17
+ CompiledTask,
18
+ CompiledWorkflow,
19
+ ExecutionState,
20
+ TaskStep,
21
+ } from '../workflow-types';
22
+
23
+ import { executeTaskStep } from './task-executor';
24
+ import type { StepExecutorEnv } from './types';
25
+
26
+ class TestContext extends BaseWorkflowContext {
27
+ public eventEmitter: EventEmitterLike = { emit: jest.fn(), on: jest.fn() };
28
+
29
+ constructor() {
30
+ super({});
31
+ }
32
+ }
33
+
34
+ const createState = (): ExecutionState => ({
35
+ input: {},
36
+ output: {},
37
+ iterationStack: [],
38
+ });
39
+ const createAction = (
40
+ impl: jest.Mock,
41
+ ): Action<unknown, unknown, BaseWorkflowContext, any> =>
42
+ ({
43
+ name: 'mock_action',
44
+ description: 'runs a task',
45
+ inputSchema: {} as any,
46
+ outputSchema: {} as any,
47
+ execute: jest.fn(),
48
+ parseInput: jest.fn(),
49
+ parseOutput: jest.fn(),
50
+ parseSettings: jest.fn(),
51
+ run: impl,
52
+ }) as Action<unknown, unknown, BaseWorkflowContext, any>;
53
+ const createTask = (runImpl: jest.Mock): CompiledTask => ({
54
+ name: 'test_task',
55
+ definition: {} as any,
56
+ actionName: 'mock_action',
57
+ action: createAction(runImpl),
58
+ inputs: { payload: { kind: 'literal', value: 123 } },
59
+ settings: {} as any,
60
+ bindings: {
61
+ tools: { calculate: { action: 'calculate_score', settings: {} } },
62
+ },
63
+ });
64
+ const createCompiled = (task: CompiledTask): CompiledWorkflow =>
65
+ ({
66
+ definition: {} as any,
67
+ tasks: { [task.name]: task },
68
+ flow: [],
69
+ outputMapping: {},
70
+ inputParser: { parse: (value: unknown) => value } as any,
71
+ }) as CompiledWorkflow;
72
+ const createEnv = (
73
+ compiled: CompiledWorkflow,
74
+ stepInfo: StepInfo,
75
+ ): StepExecutorEnv => ({
76
+ compiled,
77
+ context: new TestContext(),
78
+ runId: 'run-123',
79
+ buildInstanceStepInfo: jest.fn().mockReturnValue(stepInfo),
80
+ markSnapshot: jest.fn(),
81
+ recordStepExecution: jest.fn(),
82
+ emit: jest.fn(),
83
+ setCurrentStep: jest.fn(),
84
+ waitForStepSuspension: jest
85
+ .fn()
86
+ .mockImplementation(
87
+ () => new Promise<RuntimeSuspensionRequest>(() => undefined),
88
+ ),
89
+ clearStepSuspensions: jest.fn(),
90
+ primeStepResumeData: jest.fn(),
91
+ captureTaskOutput: jest.fn().mockResolvedValue(undefined),
92
+ executeFlow: jest.fn(),
93
+ executeStep: jest.fn(),
94
+ });
95
+ const step: TaskStep = {
96
+ id: '0:test_task',
97
+ type: StepType.Task,
98
+ label: 'test_task',
99
+ taskName: 'test_task',
100
+ };
101
+ const stepInfo: StepInfo = {
102
+ id: step.id,
103
+ name: step.label,
104
+ type: StepType.Task,
105
+ };
106
+
107
+ describe('executeTaskStep', () => {
108
+ it('runs the task, records snapshots, and emits success events', async () => {
109
+ const task = createTask(jest.fn().mockResolvedValue({ result: 'ok' }));
110
+ const compiled = createCompiled(task);
111
+ const env = createEnv(compiled, stepInfo);
112
+ const state = createState();
113
+ const result = await executeTaskStep(env, step, state, []);
114
+
115
+ expect(result).toBeUndefined();
116
+ expect(env.buildInstanceStepInfo).toHaveBeenCalledWith(
117
+ step,
118
+ state.iterationStack,
119
+ );
120
+ expect(task.action.run).toHaveBeenCalledWith(
121
+ { payload: 123 },
122
+ env.context,
123
+ task.settings,
124
+ task.bindings,
125
+ );
126
+ expect(env.setCurrentStep).toHaveBeenNthCalledWith(1, stepInfo);
127
+ expect(env.markSnapshot).toHaveBeenNthCalledWith(1, stepInfo, 'running');
128
+ expect(env.markSnapshot).toHaveBeenNthCalledWith(2, stepInfo, 'completed');
129
+ expect(env.recordStepExecution).toHaveBeenNthCalledWith(
130
+ 1,
131
+ stepInfo,
132
+ expect.objectContaining({
133
+ action: 'mock_action',
134
+ status: 'running',
135
+ startedAt: expect.any(Number),
136
+ input: { payload: 123 },
137
+ context: { before: {} },
138
+ }),
139
+ );
140
+ expect(env.recordStepExecution).toHaveBeenNthCalledWith(
141
+ 2,
142
+ stepInfo,
143
+ expect.objectContaining({
144
+ status: 'completed',
145
+ endedAt: expect.any(Number),
146
+ output: { result: 'ok' },
147
+ context: { after: {} },
148
+ }),
149
+ );
150
+ expect(env.captureTaskOutput).toHaveBeenCalledWith(task, state, {
151
+ result: 'ok',
152
+ });
153
+ expect(env.emit).toHaveBeenCalledWith('hook:step:start', {
154
+ runId: 'run-123',
155
+ step: stepInfo,
156
+ });
157
+ expect(env.emit).toHaveBeenCalledWith('hook:step:success', {
158
+ runId: 'run-123',
159
+ step: stepInfo,
160
+ });
161
+ expect(env.setCurrentStep).toHaveBeenLastCalledWith(undefined);
162
+ });
163
+
164
+ it('throws when the task is missing', async () => {
165
+ const compiled = createCompiled(createTask(jest.fn()));
166
+ compiled.tasks = {};
167
+ const env = createEnv(compiled, stepInfo);
168
+ const state = createState();
169
+
170
+ await expect(executeTaskStep(env, step, state, [])).rejects.toThrow(
171
+ 'Task "test_task" is not defined.',
172
+ );
173
+ expect(env.buildInstanceStepInfo).not.toHaveBeenCalled();
174
+ });
175
+
176
+ it('resumes an in-flight action and captures post-suspend output', async () => {
177
+ const request: RuntimeSuspensionRequest = {
178
+ stepId: step.id,
179
+ stepExecId: `${step.id}#1`,
180
+ suspendIndex: 1,
181
+ suspendKey: 'index:1',
182
+ reason: 'awaiting_user',
183
+ data: { channel: 'sms' },
184
+ awaitResults: {},
185
+ resume: createDeferred<unknown>(),
186
+ };
187
+ const task = createTask(
188
+ jest.fn().mockImplementation(async () => {
189
+ const resumed = (await request.resume.promise) as { reply: string };
190
+
191
+ return { reply: resumed.reply.toUpperCase() };
192
+ }),
193
+ );
194
+ const compiled = createCompiled(task);
195
+ const env = createEnv(compiled, stepInfo);
196
+ const state = createState();
197
+
198
+ (env.waitForStepSuspension as jest.Mock)
199
+ .mockResolvedValueOnce(request)
200
+ .mockImplementationOnce(
201
+ () => new Promise<RuntimeSuspensionRequest>(() => undefined),
202
+ );
203
+
204
+ const suspension = await executeTaskStep(env, step, state, []);
205
+
206
+ expect(suspension).toEqual(
207
+ expect.objectContaining({
208
+ step: stepInfo,
209
+ reason: 'awaiting_user',
210
+ data: { channel: 'sms' },
211
+ }),
212
+ );
213
+ expect(env.markSnapshot).toHaveBeenCalledWith(
214
+ stepInfo,
215
+ 'suspended',
216
+ 'awaiting_user',
217
+ );
218
+
219
+ await suspension?.continue({ reply: 'Sure' });
220
+
221
+ expect(env.captureTaskOutput).toHaveBeenCalledWith(task, state, {
222
+ reply: 'SURE',
223
+ });
224
+ expect(env.recordStepExecution).toHaveBeenCalledWith(
225
+ stepInfo,
226
+ expect.objectContaining({
227
+ status: 'completed',
228
+ output: { reply: 'SURE' },
229
+ }),
230
+ );
231
+ });
232
+
233
+ it('marks failure and rethrows errors from the task', async () => {
234
+ const task = createTask(jest.fn().mockRejectedValue(new Error('boom')));
235
+ const compiled = createCompiled(task);
236
+ const env = createEnv(compiled, stepInfo);
237
+ const state = createState();
238
+
239
+ await expect(executeTaskStep(env, step, state, [])).rejects.toThrow('boom');
240
+ expect(env.recordStepExecution).toHaveBeenCalledWith(
241
+ stepInfo,
242
+ expect.objectContaining({
243
+ status: 'failed',
244
+ endedAt: expect.any(Number),
245
+ error: { message: 'boom', stack: expect.any(String) },
246
+ context: { after: {} },
247
+ }),
248
+ );
249
+ expect(env.markSnapshot).toHaveBeenCalledWith(stepInfo, 'failed', 'boom');
250
+ expect(env.emit).toHaveBeenCalledWith('hook:step:error', {
251
+ runId: 'run-123',
252
+ step: stepInfo,
253
+ error: expect.any(Error),
254
+ });
255
+ expect(env.setCurrentStep).toHaveBeenLastCalledWith(undefined);
256
+ });
257
+ });
@@ -0,0 +1,248 @@
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 { RuntimeSuspensionRequest } from '../runner-runtime-control';
8
+ import type {
9
+ CompiledTask,
10
+ EvaluationScope,
11
+ ExecutionState,
12
+ Suspension,
13
+ TaskStep,
14
+ } from '../workflow-types';
15
+ import { evaluateMapping } from '../workflow-values';
16
+
17
+ import type { StepExecutorEnv } from './types';
18
+
19
+ type TaskProgressOutcome =
20
+ | { type: 'completed'; value: unknown }
21
+ | { type: 'failed'; error: unknown }
22
+ | { type: 'suspended'; request: RuntimeSuspensionRequest };
23
+
24
+ /**
25
+ * Execute a task step by running its task and handling suspension or output mapping.
26
+ *
27
+ * @param env Executor environment with compiled workflow and helpers.
28
+ * @param step The compiled task step to execute.
29
+ * @param state Mutable workflow execution state.
30
+ * @param _path Location of the step within the workflow (unused in this executor).
31
+ * @returns A suspension if the task pauses execution, otherwise void.
32
+ */
33
+ export async function executeTaskStep(
34
+ env: StepExecutorEnv,
35
+ step: TaskStep,
36
+ state: ExecutionState,
37
+ _path: Array<number | string>,
38
+ ): Promise<Suspension | void> {
39
+ void _path;
40
+ const task = env.compiled.tasks[step.taskName];
41
+ if (!task) {
42
+ throw new Error(`Task "${step.taskName}" is not defined.`);
43
+ }
44
+
45
+ const stepInfo = env.buildInstanceStepInfo(step, state.iterationStack);
46
+ const scope: EvaluationScope = {
47
+ input: state.input,
48
+ context: env.context.state,
49
+ output: state.output,
50
+ iteration: state.iteration,
51
+ accumulator: state.accumulator,
52
+ };
53
+ const inputs = await evaluateMapping(task.inputs, scope);
54
+ env.recordStepExecution(stepInfo, {
55
+ action: task.actionName,
56
+ status: 'running',
57
+ startedAt: Date.now(),
58
+ input: inputs,
59
+ context: { before: env.context.snapshot() },
60
+ });
61
+ env.beginStepExecution?.(stepInfo.id);
62
+ env.setCurrentStep(stepInfo);
63
+ env.markSnapshot(stepInfo, 'running');
64
+ env.emit('hook:step:start', { runId: env.runId, step: stepInfo });
65
+
66
+ try {
67
+ const actionPromise = Promise.resolve().then(() =>
68
+ task.action.run(inputs, env.context, task.settings, task.bindings),
69
+ );
70
+ const outcome = await waitForTaskProgress(env, stepInfo.id, actionPromise);
71
+
72
+ if (outcome.type === 'completed') {
73
+ await completeTask(
74
+ env,
75
+ stepInfo.id,
76
+ stepInfo,
77
+ task,
78
+ state,
79
+ outcome.value,
80
+ );
81
+
82
+ return undefined;
83
+ }
84
+
85
+ if (outcome.type === 'failed') {
86
+ env.clearStepSuspensions(stepInfo.id, outcome.error);
87
+ recordTaskFailure(env, stepInfo, outcome.error);
88
+ throw outcome.error;
89
+ }
90
+
91
+ return buildSuspensionContinuation(
92
+ env,
93
+ stepInfo.id,
94
+ stepInfo,
95
+ task,
96
+ state,
97
+ actionPromise,
98
+ outcome.request,
99
+ );
100
+ } finally {
101
+ env.setCurrentStep(undefined);
102
+ }
103
+ }
104
+
105
+ const waitForTaskProgress = async (
106
+ env: StepExecutorEnv,
107
+ stepId: string,
108
+ actionPromise: Promise<unknown>,
109
+ ): Promise<TaskProgressOutcome> => {
110
+ const completion = actionPromise.then(
111
+ (value): TaskProgressOutcome => ({ type: 'completed', value }),
112
+ (error): TaskProgressOutcome => ({ type: 'failed', error }),
113
+ );
114
+ const suspension = env
115
+ .waitForStepSuspension(stepId)
116
+ .then<TaskProgressOutcome>((request) => ({ type: 'suspended', request }));
117
+
118
+ return Promise.race([completion, suspension]);
119
+ };
120
+ const completeTask = async (
121
+ env: StepExecutorEnv,
122
+ stepId: string,
123
+ stepInfo: Suspension['step'],
124
+ task: CompiledTask,
125
+ state: ExecutionState,
126
+ result: unknown,
127
+ ) => {
128
+ await env.captureTaskOutput(task, state, result);
129
+ env.recordStepExecution(stepInfo, {
130
+ status: 'completed',
131
+ endedAt: Date.now(),
132
+ output: result,
133
+ context: { after: env.context.snapshot() },
134
+ });
135
+ env.markSnapshot(stepInfo, 'completed');
136
+ env.emit('hook:step:success', { runId: env.runId, step: stepInfo });
137
+ env.clearStepSuspensions(stepId);
138
+ };
139
+ const recordSuspension = (
140
+ env: StepExecutorEnv,
141
+ stepInfo: Suspension['step'],
142
+ reason?: string,
143
+ data?: unknown,
144
+ ) => {
145
+ env.recordStepExecution(stepInfo, {
146
+ status: 'suspended',
147
+ endedAt: Date.now(),
148
+ reason,
149
+ context: { after: env.context.snapshot() },
150
+ });
151
+ env.markSnapshot(stepInfo, 'suspended', reason);
152
+ env.emit('hook:step:suspended', {
153
+ runId: env.runId,
154
+ step: stepInfo,
155
+ reason,
156
+ data,
157
+ });
158
+ };
159
+ const buildSuspensionContinuation = (
160
+ env: StepExecutorEnv,
161
+ stepId: string,
162
+ stepInfo: Suspension['step'],
163
+ task: CompiledTask,
164
+ state: ExecutionState,
165
+ actionPromise: Promise<unknown>,
166
+ request: RuntimeSuspensionRequest,
167
+ ): Suspension => {
168
+ recordSuspension(env, stepInfo, request.reason, request.data);
169
+
170
+ let resumed = false;
171
+
172
+ return {
173
+ step: stepInfo,
174
+ reason: request.reason,
175
+ data: request.data,
176
+ stepExecId: request.stepExecId,
177
+ suspendIndex: request.suspendIndex,
178
+ suspendKey: request.suspendKey,
179
+ awaitResults: request.awaitResults,
180
+ continue: async (resumeData: unknown) => {
181
+ if (resumed) {
182
+ throw new Error(
183
+ `Suspension for step "${stepInfo.id}" has already been resumed.`,
184
+ );
185
+ }
186
+
187
+ resumed = true;
188
+ env.setCurrentStep(stepInfo);
189
+ env.recordStepSuspendResult?.({
190
+ stepId,
191
+ stepExecId: request.stepExecId,
192
+ suspendIndex: request.suspendIndex,
193
+ suspendKey: request.suspendKey,
194
+ resumeData,
195
+ });
196
+ request.resume.resolve(resumeData);
197
+
198
+ let outcome: TaskProgressOutcome;
199
+ try {
200
+ outcome = await waitForTaskProgress(env, stepId, actionPromise);
201
+ } finally {
202
+ env.setCurrentStep(undefined);
203
+ }
204
+
205
+ if (outcome.type === 'suspended') {
206
+ return buildSuspensionContinuation(
207
+ env,
208
+ stepId,
209
+ stepInfo,
210
+ task,
211
+ state,
212
+ actionPromise,
213
+ outcome.request,
214
+ );
215
+ }
216
+
217
+ if (outcome.type === 'completed') {
218
+ await completeTask(env, stepId, stepInfo, task, state, outcome.value);
219
+
220
+ return undefined;
221
+ }
222
+
223
+ env.clearStepSuspensions(stepId, outcome.error);
224
+ recordTaskFailure(env, stepInfo, outcome.error);
225
+ throw outcome.error;
226
+ },
227
+ };
228
+ };
229
+ const recordTaskFailure = (
230
+ env: StepExecutorEnv,
231
+ stepInfo: Suspension['step'],
232
+ error: unknown,
233
+ ) => {
234
+ env.recordStepExecution(stepInfo, {
235
+ status: 'failed',
236
+ endedAt: Date.now(),
237
+ error: normalizeError(error),
238
+ context: { after: env.context.snapshot() },
239
+ });
240
+ env.markSnapshot(stepInfo, 'failed', normalizeErrorMessage(error));
241
+ env.emit('hook:step:error', { runId: env.runId, step: stepInfo, error });
242
+ };
243
+ const normalizeError = (error: unknown): { message: string; stack?: string } =>
244
+ error instanceof Error
245
+ ? { message: error.message, stack: error.stack }
246
+ : { message: String(error) };
247
+ const normalizeErrorMessage = (error: unknown): string =>
248
+ error instanceof Error ? error.message : String(error);
@@ -0,0 +1,70 @@
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
+ ActionSnapshot,
9
+ BaseWorkflowContext,
10
+ StepExecutionRecord,
11
+ } from '../context';
12
+ import type {
13
+ RuntimeResolvedSuspension,
14
+ RuntimeStepReplaySeed,
15
+ RuntimeSuspensionRequest,
16
+ } from '../runner-runtime-control';
17
+ import type { StepInfo, WorkflowEventMap } from '../workflow-event-emitter';
18
+ import type {
19
+ CompiledStep,
20
+ CompiledTask,
21
+ CompiledWorkflow,
22
+ ExecutionState,
23
+ Suspension,
24
+ } from '../workflow-types';
25
+
26
+ export type StepExecutorEnv = {
27
+ compiled: CompiledWorkflow;
28
+ context: BaseWorkflowContext;
29
+ runId?: string;
30
+ buildInstanceStepInfo: (
31
+ step: CompiledStep,
32
+ iterationStack: number[],
33
+ ) => StepInfo;
34
+ markSnapshot: (
35
+ step: StepInfo,
36
+ status: ActionSnapshot['status'],
37
+ reason?: string,
38
+ ) => void;
39
+ recordStepExecution: (
40
+ step: StepInfo,
41
+ update: Partial<StepExecutionRecord>,
42
+ ) => void;
43
+ emit: <K extends keyof WorkflowEventMap>(
44
+ event: K,
45
+ payload: WorkflowEventMap[K],
46
+ ) => void;
47
+ setCurrentStep: (step?: StepInfo) => void;
48
+ beginStepExecution?: (stepId: string) => string;
49
+ waitForStepSuspension: (stepId: string) => Promise<RuntimeSuspensionRequest>;
50
+ clearStepSuspensions: (stepId: string, error?: unknown) => void;
51
+ primeStepResumeData: (stepId: string, resumeData: unknown) => void;
52
+ prepareStepReplay?: (seed: RuntimeStepReplaySeed) => void;
53
+ recordStepSuspendResult?: (params: RuntimeResolvedSuspension) => void;
54
+ captureTaskOutput: (
55
+ task: CompiledTask,
56
+ state: ExecutionState,
57
+ result: unknown,
58
+ ) => Promise<void>;
59
+ executeFlow: (
60
+ steps: CompiledStep[],
61
+ state: ExecutionState,
62
+ path: Array<number | string>,
63
+ startIndex?: number,
64
+ ) => Promise<Suspension | void>;
65
+ executeStep: (
66
+ step: CompiledStep,
67
+ state: ExecutionState,
68
+ path: Array<number | string>,
69
+ ) => Promise<Suspension | void>;
70
+ };