@qaick/executor 0.1.0-alpha.1

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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # QAick Executor
2
+
3
+ `@qaick/executor` is the host-neutral execution domain for QAick Flow and Project artifacts. It owns orchestration, bindings, control blocks, Action Executor dispatch, lifecycle events, cancellation, retries, timing, and live execution results.
4
+
5
+ Host integration and transport implementations remain outside this package. Register host-provided Action Executors through the public API:
6
+
7
+ ```ts
8
+ import { createMigratedExecutor } from '@qaick/executor';
9
+
10
+ const executor = createMigratedExecutor({ actionExecutors: [actionExecutor] });
11
+ const run = await executor.execute(request);
12
+ const result = await run.result;
13
+ ```
14
+
15
+ The package requires Node 20 or newer and depends on the published `@qaick/contract` package. Build with `npm run build`, run tests with `npm test`, and inspect publication contents with `npm pack --dry-run`.
@@ -0,0 +1,30 @@
1
+ import type { ActionExecutor, ControlCommand, ExecutionControlState, Executor, ExecutorConfiguration, ExecutorEvent, ExecutorRequest, ExecutorResult, ExecutorRun } from './types.js';
2
+ export declare function createExecutor(configuration?: ExecutorConfiguration): Executor;
3
+ export declare class DefaultExecutor implements Executor {
4
+ private readonly runs;
5
+ private readonly actionExecutors;
6
+ private readonly maxTimeoutMs;
7
+ constructor(configuration?: ExecutorConfiguration);
8
+ registerActionExecutor(executor: ActionExecutor): void;
9
+ execute(request: ExecutorRequest): Promise<ExecutorRun>;
10
+ cancel(requestId: string): Promise<void>;
11
+ control(requestId: string, command: ControlCommand): Promise<void>;
12
+ controlState(requestId: string): ExecutionControlState | undefined;
13
+ events(requestId: string): AsyncIterable<ExecutorEvent>;
14
+ result(requestId: string): Promise<ExecutorResult>;
15
+ retryAction(requestId: string, stepId: string, invocationPath?: string[]): Promise<void>;
16
+ retryOperation(requestId: string, stepId: string, invocationPath?: string[]): Promise<void>;
17
+ runFromHere(requestId: string, stepId: string, invocationPath?: string[]): Promise<ExecutorRun | undefined>;
18
+ private run;
19
+ private executeProject;
20
+ private executeFlow;
21
+ private executeStep;
22
+ private executeControl;
23
+ private executeAction;
24
+ private boundary;
25
+ private pauseAtBoundary;
26
+ private emit;
27
+ private finish;
28
+ private baseResult;
29
+ private failure;
30
+ }
@@ -0,0 +1,556 @@
1
+ const MAX_TIMEOUT_MS = 300_000;
2
+ export function createExecutor(configuration = {}) {
3
+ return new DefaultExecutor(configuration);
4
+ }
5
+ export class DefaultExecutor {
6
+ runs = new Map();
7
+ actionExecutors;
8
+ maxTimeoutMs;
9
+ constructor(configuration = {}) {
10
+ this.actionExecutors = [...(configuration.actionExecutors ?? [])];
11
+ this.maxTimeoutMs = configuration.maxTimeoutMs ?? MAX_TIMEOUT_MS;
12
+ }
13
+ registerActionExecutor(executor) { this.actionExecutors.push(executor); }
14
+ async execute(request) {
15
+ let resolve;
16
+ const result = new Promise(promiseResolve => { resolve = promiseResolve; });
17
+ const run = {
18
+ request, resolve, events: [], listeners: [], abort: new AbortController(), cancelled: false,
19
+ timedOut: false, controlState: 'idle', pauseRequested: false, stepRequested: false,
20
+ controlWaiters: [], hitBreakpoints: new Set(), stopAfter: request.debugSession?.stopAfterStepId,
21
+ startedMs: Date.now(), pausedDurationMs: 0,
22
+ };
23
+ this.runs.set(request.requestId, run);
24
+ void this.run(run);
25
+ return { requestId: request.requestId, result };
26
+ }
27
+ async cancel(requestId) {
28
+ const run = this.runs.get(requestId);
29
+ if (!run || run.result)
30
+ return;
31
+ run.cancelled = true;
32
+ run.controlState = 'stopped';
33
+ run.abort.abort();
34
+ run.controlWaiters.splice(0).forEach(resolve => resolve());
35
+ run.listeners.splice(0).forEach(resolve => resolve());
36
+ }
37
+ async control(requestId, command) {
38
+ const run = this.runs.get(requestId);
39
+ if (!run || run.result)
40
+ return;
41
+ if (command.type === 'stop')
42
+ return this.cancel(requestId);
43
+ if (command.type === 'stop-after') {
44
+ run.stopAfter = command.stepId;
45
+ return;
46
+ }
47
+ if (command.type === 'pause') {
48
+ if (run.controlState !== 'paused' && run.controlState !== 'pause-requested') {
49
+ run.pauseRequested = true;
50
+ run.controlState = 'pause-requested';
51
+ this.emit(run, 'waiting', 'waiting', 'Pause requested.', undefined, { type: 'pause', });
52
+ }
53
+ return;
54
+ }
55
+ if (command.type === 'resume') {
56
+ run.pauseRequested = false;
57
+ run.stepRequested = false;
58
+ run.controlState = 'resuming';
59
+ run.controlWaiters.splice(0).forEach(resolve => resolve());
60
+ return;
61
+ }
62
+ if (command.type === 'step' && run.controlState === 'paused') {
63
+ run.pauseRequested = false;
64
+ run.stepRequested = true;
65
+ run.controlState = 'resuming';
66
+ run.controlWaiters.splice(0).forEach(resolve => resolve());
67
+ }
68
+ }
69
+ controlState(requestId) { return this.runs.get(requestId)?.controlState; }
70
+ async *events(requestId) {
71
+ const run = this.runs.get(requestId);
72
+ if (!run)
73
+ return;
74
+ let index = 0;
75
+ while (true) {
76
+ while (index < run.events.length)
77
+ yield run.events[index++];
78
+ if (run.result)
79
+ return;
80
+ await new Promise(resolve => run.listeners.push(resolve));
81
+ }
82
+ }
83
+ async result(requestId) {
84
+ const run = this.runs.get(requestId);
85
+ if (!run)
86
+ throw new Error(`Unknown execution ${requestId}.`);
87
+ if (run.result)
88
+ return run.result;
89
+ return new Promise(resolve => {
90
+ const previous = run.resolve;
91
+ run.resolve = value => { previous(value); resolve(value); };
92
+ });
93
+ }
94
+ async retryAction(requestId, stepId, invocationPath) {
95
+ const run = this.runs.get(requestId);
96
+ if (!run?.result || run.cancelled || run.abort.signal.aborted)
97
+ return;
98
+ const located = findStep(run.result.steps, stepId, invocationPath);
99
+ const definition = findDefinition(run.request.executionPackage.steps, stepId, invocationPath);
100
+ if (!located || !definition || definition.kind === 'operation' || definition.control || located.state === 'skipped')
101
+ return;
102
+ this.emit(run, 'waiting', 'waiting', `Interactive retry requested for ${located.name}.`, definition, { type: 'step', stepId });
103
+ try {
104
+ const action = await this.executeAction(run, definition, located.inputs);
105
+ const previous = located.attempts ?? [];
106
+ located.attempts = [...previous, ...action.attempts.map(attempt => ({ ...attempt, kind: 'interactive' }))];
107
+ located.outputs = action.outputs;
108
+ located.evidence = action.evidence;
109
+ located.state = 'finished';
110
+ located.terminalReason = 'completed';
111
+ delete located.error;
112
+ if (run.result.state === 'failed') {
113
+ run.result.state = 'completed';
114
+ run.result.terminalReason = 'completed';
115
+ delete run.result.error;
116
+ }
117
+ this.emit(run, 'step_finished', 'finished', `${located.name} interactive retry completed.`, definition);
118
+ }
119
+ catch (cause) {
120
+ located.state = 'failed';
121
+ located.terminalReason = 'failed';
122
+ located.error = this.failure(cause, run);
123
+ this.emit(run, 'step_finished', 'failed', `${located.name} interactive retry failed.`, definition);
124
+ }
125
+ }
126
+ async retryOperation(requestId, stepId, invocationPath) {
127
+ const run = this.runs.get(requestId);
128
+ if (!run?.result || run.cancelled || run.abort.signal.aborted)
129
+ return;
130
+ const located = findStep(run.result.steps, stepId, invocationPath);
131
+ const definition = findDefinition(run.request.executionPackage.steps, stepId, invocationPath);
132
+ if (!located || !definition?.children?.length || definition.control || definition.kind !== 'operation')
133
+ return;
134
+ const nested = await this.executeFlow(run, definition.children, located.inputs, [...(located.invocationPath ?? []).slice(0, -1), definition.id], new Date().toISOString());
135
+ const previous = located.attempts ?? [];
136
+ located.attempts = [...previous, { attempt: previous.length + 1, kind: 'interactive', state: nested.state === 'completed' ? 'finished' : 'failed', startedAt: nested.startedAt, finishedAt: nested.finishedAt, inputs: located.inputs, outputs: nested.steps.at(-1)?.outputs ?? {}, durationMs: nested.steps.reduce((sum, child) => sum + child.durationMs, 0), children: nested.steps, ...(nested.error ? { error: nested.error } : {}) }];
137
+ located.children = nested.steps;
138
+ located.outputs = nested.steps.at(-1)?.outputs ?? {};
139
+ located.state = nested.state === 'completed' ? 'finished' : nested.state === 'cancelled' ? 'cancelled' : 'failed';
140
+ located.terminalReason = nested.terminalReason ?? (located.state === 'finished' ? 'completed' : 'failed');
141
+ located.error = nested.error;
142
+ if (located.state === 'finished' && run.result.state === 'failed') {
143
+ run.result.state = 'completed';
144
+ run.result.terminalReason = 'completed';
145
+ delete run.result.error;
146
+ }
147
+ }
148
+ async runFromHere(requestId, stepId, invocationPath) {
149
+ const run = this.runs.get(requestId);
150
+ if (!run)
151
+ return undefined;
152
+ if (invocationPath?.length)
153
+ return undefined;
154
+ const index = run.request.executionPackage.steps.findIndex(step => step.id === stepId);
155
+ if (index < 0)
156
+ return undefined;
157
+ const selected = findStep(run.result?.steps ?? [], stepId, invocationPath);
158
+ if (!selected)
159
+ return undefined;
160
+ const requestIdForRun = `${requestId}:from:${stepId}`;
161
+ const remainingSteps = run.request.executionPackage.steps.slice(index).map((step, offset) => offset === 0
162
+ ? { ...step, bindings: undefined, staticInputs: { ...(step.staticInputs ?? {}), ...selected.inputs } }
163
+ : step);
164
+ return this.execute({
165
+ ...run.request,
166
+ requestId: requestIdForRun,
167
+ executionPackage: { ...run.request.executionPackage, steps: remainingSteps, target: { kind: 'flow', id: run.request.executionPackage.target.id, name: run.request.executionPackage.target.name } },
168
+ inputs: selected.inputs,
169
+ });
170
+ }
171
+ async run(run) {
172
+ const startedAt = new Date().toISOString();
173
+ const timeoutMs = run.request.options?.timeoutMs;
174
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > this.maxTimeoutMs)) {
175
+ this.finish(run, this.baseResult(run, startedAt, 'failed', { code: 'PREFLIGHT_ERROR', message: `Timeout must be between 0 and ${this.maxTimeoutMs} ms.` }));
176
+ return;
177
+ }
178
+ let timeout;
179
+ if (timeoutMs !== undefined)
180
+ timeout = setTimeout(() => { run.timedOut = true; run.abort.abort(); }, timeoutMs);
181
+ this.emit(run, 'run_started', 'created', 'Execution accepted.');
182
+ this.emit(run, 'preflight_started', 'preflighting', 'Validating execution package.');
183
+ const issues = preflight(run.request.executionPackage, run.request.inputs, run.request.runtimeValues ?? {});
184
+ if (issues.length > 0 || run.request.options?.dryRun) {
185
+ const result = this.baseResult(run, startedAt, issues.length > 0 ? 'failed' : 'completed', undefined, issues);
186
+ if (timeout)
187
+ clearTimeout(timeout);
188
+ this.finish(run, result);
189
+ return;
190
+ }
191
+ run.controlState = 'running';
192
+ try {
193
+ const result = run.request.executionPackage.target.kind === 'project'
194
+ ? await this.executeProject(run, startedAt)
195
+ : await this.executeFlow(run, run.request.executionPackage.steps, run.request.inputs, [], startedAt);
196
+ if (timeout)
197
+ clearTimeout(timeout);
198
+ this.finish(run, result);
199
+ }
200
+ catch (cause) {
201
+ if (timeout)
202
+ clearTimeout(timeout);
203
+ const error = this.failure(cause, run);
204
+ this.finish(run, this.baseResult(run, startedAt, run.timedOut ? 'failed' : 'cancelled', error));
205
+ }
206
+ }
207
+ async executeProject(run, startedAt) {
208
+ const pkg = run.request.executionPackage;
209
+ const children = [];
210
+ let inputs = { ...run.request.inputs };
211
+ for (const flow of pkg.flows ?? []) {
212
+ if (run.cancelled || run.abort.signal.aborted)
213
+ throw new Error('Execution cancelled.');
214
+ const child = await this.executeFlow(run, flow.steps, inputs, [flow.id], startedAt, { kind: 'flow', id: flow.id, name: flow.name });
215
+ children.push(child);
216
+ if (child.state !== 'completed')
217
+ return this.baseResult(run, startedAt, child.state, child.error, undefined, children);
218
+ const last = child.steps.at(-1)?.outputs ?? {};
219
+ const mapped = flow.outputBindings
220
+ ? Object.fromEntries(Object.entries(flow.outputBindings).flatMap(([name, ref]) => {
221
+ const match = /^\$outputs\.(.+)$/.exec(ref);
222
+ return match && last[match[1]] !== undefined ? [[name, last[match[1]]]] : [];
223
+ }))
224
+ : last;
225
+ inputs = { ...inputs, ...mapped };
226
+ }
227
+ return this.baseResult(run, startedAt, 'completed', undefined, undefined, children);
228
+ }
229
+ async executeFlow(run, steps, initialInputs, path, startedAt, target = run.request.executionPackage.target, signal = run.abort.signal) {
230
+ const outputsById = new Map();
231
+ const results = [];
232
+ let state = 'completed';
233
+ let error;
234
+ for (const step of orderSteps(steps)) {
235
+ if (run.cancelled || signal.aborted)
236
+ throw new Error('Execution cancelled.');
237
+ await this.boundary(run, step, [...path, step.id], signal);
238
+ const result = await this.executeStep(run, step, initialInputs, outputsById, path, signal);
239
+ results.push(result);
240
+ outputsById.set(step.id, result.outputs);
241
+ if (result.state === 'failed' || result.state === 'cancelled') {
242
+ state = result.state === 'cancelled' ? 'cancelled' : 'failed';
243
+ error = result.error;
244
+ if (step.control?.onError !== 'continue')
245
+ break;
246
+ }
247
+ }
248
+ return {
249
+ ...this.baseResult(run, startedAt, state, error),
250
+ target,
251
+ steps: results,
252
+ };
253
+ }
254
+ async executeStep(run, step, scope, outputsById, path, signal = run.abort.signal) {
255
+ const startedMs = Date.now();
256
+ const invocationPath = [...path, step.id];
257
+ const base = { id: step.id, name: step.name, kind: step.kind ?? 'action', inputs: {}, outputs: {}, invocationId: invocationPath.join('/'), invocationPath };
258
+ if (step.dependsOn?.some(id => !outputsById.has(id)))
259
+ return { ...base, state: 'skipped', durationMs: 0, terminalReason: 'dependency-skipped' };
260
+ const inputs = { ...(step.staticInputs ?? {}), ...(step.bindings ? resolveBindings(step.bindings, scope, outputsById) : {}) };
261
+ this.emit(run, 'step_started', 'running', `${step.name} started.`, step);
262
+ try {
263
+ if (step.delayMs) {
264
+ this.emit(run, 'waiting', 'waiting', `${step.name} is waiting for ${step.delayMs} ms.`, step);
265
+ await delay(step.delayMs, signal);
266
+ }
267
+ if (step.control) {
268
+ const controlResult = await this.executeControl(run, step, inputs, scope, outputsById, path, signal);
269
+ return { ...base, ...controlResult, inputs, durationMs: Date.now() - startedMs };
270
+ }
271
+ if (step.kind === 'operation' && step.children) {
272
+ const nested = await this.executeFlow(run, step.children, { ...scope, ...inputs }, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal);
273
+ const outputs = nested.steps.at(-1)?.outputs ?? {};
274
+ const result = { ...base, state: nested.state === 'completed' ? 'finished' : nested.state, inputs, outputs, durationMs: Date.now() - startedMs, children: nested.steps, terminalReason: nested.terminalReason ?? (nested.state === 'completed' ? 'completed' : 'failed'), error: nested.error };
275
+ this.emit(run, 'step_finished', result.state, `${step.name} ${result.state}.`, step);
276
+ return result;
277
+ }
278
+ const actionResult = await this.executeAction(run, step, inputs, signal);
279
+ const result = { ...base, state: 'finished', inputs, outputs: actionResult.outputs, durationMs: Date.now() - startedMs, terminalReason: 'completed', attempts: actionResult.attempts, evidence: actionResult.evidence };
280
+ this.emit(run, 'step_finished', 'finished', `${step.name} finished.`, step);
281
+ if (run.stopAfter === step.id) {
282
+ run.stopAfter = undefined;
283
+ await this.pauseAtBoundary(run, step);
284
+ }
285
+ return result;
286
+ }
287
+ catch (cause) {
288
+ const failure = this.failure(cause, run, signal);
289
+ const state = run.timedOut ? 'failed' : run.cancelled || signal.aborted ? 'cancelled' : 'failed';
290
+ const result = { ...base, state, inputs, durationMs: Date.now() - startedMs, terminalReason: run.timedOut ? 'timeout' : state === 'cancelled' ? 'cancelled' : 'failed', error: failure };
291
+ this.emit(run, 'step_finished', state, `${step.name} ${state}.`, step);
292
+ return result;
293
+ }
294
+ }
295
+ async executeControl(run, step, inputs, scope, outputs, path, signal = run.abort.signal) {
296
+ const control = step.control;
297
+ if (control.condition !== undefined && !evaluateCondition(control.condition, { ...scope, ...inputs }, outputs))
298
+ return { state: 'skipped', outputs: {}, terminalReason: 'condition-skipped' };
299
+ if (control.type === 'repeat') {
300
+ const total = control.iterations ?? 1;
301
+ const children = [];
302
+ for (let current = 1; current <= total; current++) {
303
+ const child = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id, String(current)], new Date().toISOString(), run.request.executionPackage.target, signal);
304
+ children.push(...child.steps.map(item => ({ ...item, iteration: { loopId: step.id, current, total } })));
305
+ if (child.state !== 'completed' && control.onError !== 'continue')
306
+ return { state: child.state, outputs: child.steps.at(-1)?.outputs ?? {}, children, terminalReason: child.terminalReason, error: child.error };
307
+ if (control.iterationDelayMs)
308
+ await delay(control.iterationDelayMs, signal);
309
+ }
310
+ return { state: 'finished', outputs: children.at(-1)?.outputs ?? {}, children, terminalReason: 'completed' };
311
+ }
312
+ if (control.type === 'branch') {
313
+ const branch = control.branches?.find(candidate => evaluateCondition(candidate.condition, { ...scope, ...inputs }, outputs));
314
+ if (!branch)
315
+ return { state: 'skipped', outputs: {}, terminalReason: 'branch-skipped' };
316
+ const nested = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, signal);
317
+ return { state: nested.state === 'completed' ? 'finished' : nested.state, outputs: nested.steps.at(-1)?.outputs ?? {}, children: nested.steps, terminalReason: nested.terminalReason, error: nested.error };
318
+ }
319
+ if (control.type === 'parallel') {
320
+ const branches = control.branches ?? [];
321
+ const selected = branches.filter(branch => evaluateCondition(branch.condition, { ...scope, ...inputs }, outputs));
322
+ const controllers = selected.map(() => new AbortController());
323
+ let winnerIndex;
324
+ const branchRuns = await Promise.all(selected.map(async (branch, index) => {
325
+ try {
326
+ const result = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, controllers[index].signal);
327
+ if (control.join === 'first' && result.state === 'completed' && winnerIndex === undefined) {
328
+ winnerIndex = index;
329
+ controllers.forEach((controller, candidate) => { if (candidate !== index)
330
+ controller.abort(); });
331
+ }
332
+ return { branch, result };
333
+ }
334
+ catch (cause) {
335
+ return { branch, result: this.baseResult(run, new Date().toISOString(), controllers[index].signal.aborted ? 'cancelled' : 'failed', this.failure(cause, run, controllers[index].signal)) };
336
+ }
337
+ }));
338
+ const nested = branchRuns.map(item => item.result);
339
+ const children = nested.flatMap(item => item.steps);
340
+ const considered = control.join === 'first' && winnerIndex !== undefined ? [branchRuns[winnerIndex]] : branchRuns;
341
+ const failed = considered.find(item => item.result.state !== 'completed');
342
+ const branchOutputs = considered.map(({ branch, result }) => ({ id: branch.id, outputs: result.steps.at(-1)?.outputs ?? {} }));
343
+ const mergedOutputs = mergeBranchOutputs(branchOutputs, control.mergeBindings, control.merge ?? (control.join === 'first' ? 'first' : 'collect'));
344
+ return { state: failed ? invocationState(failed.result.state) : 'finished', outputs: mergedOutputs, children, terminalReason: failed?.result.terminalReason ?? 'parallel-joined', error: failed?.result.error };
345
+ }
346
+ const nested = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal);
347
+ return { state: nested.state === 'completed' ? 'finished' : nested.state, outputs: nested.steps.at(-1)?.outputs ?? {}, children: nested.steps, terminalReason: nested.terminalReason, error: nested.error };
348
+ }
349
+ async executeAction(run, step, inputs, signal = run.abort.signal) {
350
+ if (run.request.options?.dryRun)
351
+ return { outputs: {}, attempts: [] };
352
+ const executor = this.actionExecutors.find(candidate => candidate.supports(step.action));
353
+ if (!executor)
354
+ throw failure('UNSUPPORTED_ACTION', `No Action Executor is registered for ${step.action}.`);
355
+ const maxAttempts = Math.max(1, step.retry?.maxAttempts ?? 1);
356
+ const attempts = [];
357
+ let lastFailure;
358
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
359
+ const startedAt = new Date().toISOString();
360
+ const startedMs = Date.now();
361
+ try {
362
+ const context = { action: step.action, inputs, environment: run.request.environment, runtimeValues: run.request.runtimeValues ?? {}, step, signal };
363
+ const raw = await executor.execute(context);
364
+ const normalized = isActionResult(raw) ? raw : { outputs: raw };
365
+ const finishedAt = new Date().toISOString();
366
+ attempts.push({ attempt, state: 'finished', startedAt, finishedAt, inputs, outputs: normalized.outputs, durationMs: Date.now() - startedMs, evidence: normalized.evidence });
367
+ return { outputs: normalized.outputs, evidence: normalized.evidence, attempts };
368
+ }
369
+ catch (cause) {
370
+ lastFailure = this.failure(cause, run, signal);
371
+ attempts.push({ attempt, state: run.timedOut ? 'failed' : run.cancelled || signal.aborted ? 'cancelled' : 'failed', startedAt, finishedAt: new Date().toISOString(), inputs, outputs: {}, durationMs: Date.now() - startedMs, error: lastFailure });
372
+ if (attempt < maxAttempts && step.retry?.delayMs)
373
+ await delay(step.retry.delayMs, signal);
374
+ }
375
+ }
376
+ throw lastFailure ?? failure('ACTION_FAILED', `Action ${step.action} failed.`);
377
+ }
378
+ async boundary(run, step, path, signal = run.abort.signal) {
379
+ const breakpointKey = stepPathForBoundary(step, run, path);
380
+ const breakpoints = run.request.debugSession?.breakpoints ?? [];
381
+ if (breakpoints.includes(step.id) && !run.hitBreakpoints.has(breakpointKey)) {
382
+ run.hitBreakpoints.add(breakpointKey);
383
+ run.pauseRequested = true;
384
+ this.emit(run, 'waiting', 'waiting', `Breakpoint reached before ${step.name}.`, step, { type: 'pause', stepId: step.id });
385
+ }
386
+ if (run.pauseRequested || run.controlState === 'pause-requested')
387
+ await this.pauseAtBoundary(run, step);
388
+ if (run.cancelled || signal.aborted)
389
+ throw new Error('Execution cancelled.');
390
+ }
391
+ async pauseAtBoundary(run, step) {
392
+ run.controlState = 'paused';
393
+ run.pausedStartedMs = Date.now();
394
+ this.emit(run, 'waiting', 'waiting', `Paused before ${step.name}.`, step, { type: 'pause', stepId: step.id });
395
+ await new Promise(resolve => run.controlWaiters.push(resolve));
396
+ run.pausedDurationMs += Date.now() - (run.pausedStartedMs ?? Date.now());
397
+ run.pausedStartedMs = undefined;
398
+ run.controlState = 'running';
399
+ if (run.stepRequested) {
400
+ run.stepRequested = false;
401
+ run.pauseRequested = true;
402
+ }
403
+ }
404
+ emit(run, type, state, message, step, control) {
405
+ run.events.push({ requestId: run.request.requestId, sequence: run.events.length + 1, type, state, message, timestamp: new Date().toISOString(), ...(step ? { stepId: step.id, stepName: step.name } : {}), ...(control ? { control } : {}) });
406
+ run.listeners.splice(0).forEach(resolve => resolve());
407
+ }
408
+ finish(run, result) {
409
+ const finishedAt = new Date().toISOString();
410
+ const finalResult = { ...result, finishedAt, executionTiming: { wallDurationMs: Date.now() - run.startedMs, activeDurationMs: Date.now() - run.startedMs - run.pausedDurationMs, pausedDurationMs: run.pausedDurationMs } };
411
+ run.result = finalResult;
412
+ run.controlState = finalResult.state === 'completed' ? 'completed' : finalResult.state === 'cancelled' ? 'stopped' : 'failed';
413
+ this.emit(run, 'run_finished', finalResult.state, finalResult.error?.message ?? `Execution ${finalResult.state}.`);
414
+ run.resolve(finalResult);
415
+ run.listeners.splice(0).forEach(resolve => resolve());
416
+ }
417
+ baseResult(run, startedAt, state, error, issues, children) {
418
+ return { requestId: run.request.requestId, state, target: run.request.executionPackage.target, environment: run.request.environment, startedAt, finishedAt: new Date().toISOString(), steps: [], ...(children ? { children } : {}), ...(error ? { error } : {}), ...(issues ? { issues } : {}) };
419
+ }
420
+ failure(cause, run, signal = run.abort.signal) {
421
+ if (isFailure(cause))
422
+ return cause;
423
+ if (run.timedOut)
424
+ return failure('ACTION_TIMEOUT', 'Execution exceeded its timeout.');
425
+ if (run.cancelled || signal.aborted)
426
+ return failure('ACTION_CANCELLED', 'Execution was cancelled.');
427
+ return failure('ACTION_FAILED', cause instanceof Error ? cause.message : 'Action execution failed.');
428
+ }
429
+ }
430
+ function stepPathForBoundary(step, run, path) {
431
+ return `${run.request.requestId}:${path.join('.')}:${step.id}`;
432
+ }
433
+ function preflight(pkg, inputs, runtimeValues) {
434
+ const issues = [];
435
+ if (!pkg.packageId || !pkg.target?.id)
436
+ issues.push({ code: 'INVALID_PACKAGE', message: 'Execution package identity is incomplete.' });
437
+ for (const name of pkg.requiredInputs ?? [])
438
+ if (inputs[name] === undefined)
439
+ issues.push({ code: 'MISSING_INPUT', message: `Required input '${name}' is missing.` });
440
+ for (const name of pkg.runtimeRequirements ?? [])
441
+ if (runtimeValues[name] === undefined)
442
+ issues.push({ code: 'MISSING_RUNTIME_VALUE', message: `Runtime value '${name}' is missing.` });
443
+ return issues;
444
+ }
445
+ function orderSteps(steps) {
446
+ const byId = new Map();
447
+ for (const step of steps) {
448
+ if (byId.has(step.id))
449
+ throw failure('PREFLIGHT_ERROR', `Graph contains duplicate step id ${step.id}.`);
450
+ byId.set(step.id, step);
451
+ }
452
+ const remaining = [...steps];
453
+ const ordered = [];
454
+ while (remaining.length) {
455
+ const index = remaining.findIndex(step => (step.dependsOn ?? []).every(dependency => byId.has(dependency) && ordered.some(done => done.id === dependency)));
456
+ if (index < 0) {
457
+ const missing = remaining.flatMap(step => step.dependsOn ?? []).find(dependency => !byId.has(dependency));
458
+ throw failure('PREFLIGHT_ERROR', missing ? `Graph dependency ${missing} is missing.` : 'Graph contains a dependency cycle.');
459
+ }
460
+ ordered.push(remaining.splice(index, 1)[0]);
461
+ }
462
+ return ordered;
463
+ }
464
+ function findStep(results, stepId, invocationPath) {
465
+ for (const result of results) {
466
+ if (result.id === stepId && (!invocationPath || result.invocationPath?.join('.') === invocationPath.join('.')))
467
+ return result;
468
+ const nested = result.children && findStep(result.children, stepId, invocationPath);
469
+ if (nested)
470
+ return nested;
471
+ }
472
+ return undefined;
473
+ }
474
+ function findDefinition(steps, stepId, invocationPath) {
475
+ for (const step of steps) {
476
+ if (step.id === stepId && (!invocationPath || invocationPath.at(-1) === stepId))
477
+ return step;
478
+ const nested = step.children && findDefinition(step.children, stepId, invocationPath);
479
+ if (nested)
480
+ return nested;
481
+ for (const branch of step.control?.branches ?? []) {
482
+ const branchNested = findDefinition(branch.children, stepId, invocationPath);
483
+ if (branchNested)
484
+ return branchNested;
485
+ }
486
+ }
487
+ return undefined;
488
+ }
489
+ function invocationState(state) {
490
+ return state === 'cancelled' ? 'cancelled' : state === 'failed' ? 'failed' : 'skipped';
491
+ }
492
+ function mergeBranchOutputs(branches, bindings, policy) {
493
+ if (bindings)
494
+ return Object.fromEntries(Object.entries(bindings).flatMap(([name, source]) => {
495
+ const match = /^([^.]+)\.(.+)$/.exec(source);
496
+ const branch = match ? branches.find(item => item.id === match[1]) : undefined;
497
+ return branch && match && Object.prototype.hasOwnProperty.call(branch.outputs, match[2]) ? [[name, branch.outputs[match[2]]]] : [];
498
+ }));
499
+ if (policy === 'first')
500
+ return { ...(branches[0]?.outputs ?? {}) };
501
+ if (policy === 'last')
502
+ return { ...(branches.at(-1)?.outputs ?? {}) };
503
+ return Object.fromEntries(branches.flatMap(branch => Object.entries(branch.outputs).map(([name, value]) => [`${branch.id}.${name}`, value])));
504
+ }
505
+ function resolveBindings(bindings, scope, outputs) {
506
+ return Object.fromEntries(Object.entries(bindings).map(([name, reference]) => [name, resolveReference(reference, scope, outputs)]));
507
+ }
508
+ function resolveReference(reference, scope, outputs) {
509
+ const input = /^\$inputs\.(.+)$/.exec(reference);
510
+ if (input)
511
+ return readPath(scope, input[1]);
512
+ const step = /^\$steps\.([^.]+)\.(.+)$/.exec(reference);
513
+ if (step)
514
+ return readPath(outputs.get(step[1]) ?? {}, step[2]);
515
+ if (reference.startsWith('$'))
516
+ throw failure('BINDING_RESOLUTION_ERROR', `Binding '${reference}' could not be resolved.`);
517
+ return reference;
518
+ }
519
+ function readPath(value, path) {
520
+ const result = path.split('.').reduce((current, segment) => isRecord(current) ? current[segment] : undefined, value);
521
+ if (result === undefined)
522
+ throw failure('BINDING_RESOLUTION_ERROR', `Binding path '${path}' could not be resolved.`);
523
+ return result;
524
+ }
525
+ function evaluateCondition(condition, scope, outputs) {
526
+ if (typeof condition === 'boolean')
527
+ return condition;
528
+ let value;
529
+ try {
530
+ value = resolveReference(condition.binding, scope, outputs);
531
+ }
532
+ catch {
533
+ value = undefined;
534
+ }
535
+ if (condition.exists !== undefined)
536
+ return condition.exists === (value !== undefined);
537
+ if ('equals' in condition)
538
+ return value === condition.equals;
539
+ if ('notEquals' in condition)
540
+ return value !== condition.notEquals;
541
+ return Boolean(value);
542
+ }
543
+ function delay(durationMs, signal) {
544
+ return new Promise((resolve, reject) => {
545
+ if (signal.aborted) {
546
+ reject(new Error('Execution cancelled.'));
547
+ return;
548
+ }
549
+ const timer = setTimeout(resolve, durationMs);
550
+ signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('Execution cancelled.')); }, { once: true });
551
+ });
552
+ }
553
+ function failure(code, message) { return { code, message, category: code }; }
554
+ function isFailure(value) { return isRecord(value) && typeof value.code === 'string' && typeof value.message === 'string'; }
555
+ function isActionResult(value) { return isRecord(value) && isRecord(value.outputs); }
556
+ function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
@@ -0,0 +1,5 @@
1
+ import type { ActionExecutionContext, ActionExecutionResult, ActionExecutor } from './types.js';
2
+ export declare class InMemoryActionExecutor implements ActionExecutor {
3
+ supports(action: string): boolean;
4
+ execute(context: ActionExecutionContext): Promise<ActionExecutionResult>;
5
+ }
@@ -0,0 +1,6 @@
1
+ export class InMemoryActionExecutor {
2
+ supports(action) { return action.startsWith('in-memory.') || action.startsWith('studio:'); }
3
+ async execute(context) {
4
+ return { outputs: context.step.output };
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './executor.js';
3
+ export * from './in-memory-action-executor.js';
4
+ export * from './migrated-executor.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './executor.js';
3
+ export * from './in-memory-action-executor.js';
4
+ export * from './migrated-executor.js';
@@ -0,0 +1,5 @@
1
+ import type { ActionAdapter, ActionContext } from './migrated-types.js';
2
+ export declare class InMemoryActionAdapter implements ActionAdapter {
3
+ supports(action: string): boolean;
4
+ execute(context: ActionContext): Promise<Record<string, unknown>>;
5
+ }
@@ -0,0 +1,4 @@
1
+ export class InMemoryActionAdapter {
2
+ supports(action) { return action.startsWith('in-memory.') || action.startsWith('studio:'); }
3
+ async execute(context) { return context.step.output; }
4
+ }
@@ -0,0 +1,3 @@
1
+ import type { ExecutionPackage } from './types';
2
+ /** Stable, dependency-free content identity for the normalized execution package. */
3
+ export declare function hashExecutionPackage(pack: ExecutionPackage): string;