@qaick/executor 0.1.0 → 0.2.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/dist/executor.d.ts +17 -4
- package/dist/executor.js +513 -262
- package/dist/migrated-artifact-hash.d.ts +1 -1
- package/dist/migrated-executor.d.ts +4 -4
- package/dist/migrated-executor.js +16 -11
- package/dist/types.d.ts +82 -6
- package/package.json +2 -2
package/dist/executor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionExecutor, ControlCommand, ExecutionControlState, Executor, ExecutorConfiguration, ExecutorEvent, ExecutorRequest, ExecutorResult, ExecutorRun } from './types.js';
|
|
1
|
+
import type { ActReplayTarget, ActReplayHandle, OperationReplayHandle, ActionExecutor, ControlCommand, ExecutionControlState, Executor, ExecutorConfiguration, ExecutorEvent, ExecutorRequest, ExecutorResult, ExecutorRun } from './types.js';
|
|
2
2
|
export declare function createExecutor(configuration?: ExecutorConfiguration): Executor;
|
|
3
3
|
export declare class DefaultExecutor implements Executor {
|
|
4
4
|
private readonly runs;
|
|
@@ -7,23 +7,36 @@ export declare class DefaultExecutor implements Executor {
|
|
|
7
7
|
constructor(configuration?: ExecutorConfiguration);
|
|
8
8
|
registerActionExecutor(executor: ActionExecutor): void;
|
|
9
9
|
execute(request: ExecutorRequest): Promise<ExecutorRun>;
|
|
10
|
+
private startRun;
|
|
10
11
|
cancel(requestId: string): Promise<void>;
|
|
11
12
|
control(requestId: string, command: ControlCommand): Promise<void>;
|
|
12
13
|
controlState(requestId: string): ExecutionControlState | undefined;
|
|
13
14
|
events(requestId: string): AsyncIterable<ExecutorEvent>;
|
|
14
15
|
result(requestId: string): Promise<ExecutorResult>;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
private replayTarget;
|
|
17
|
+
retryAction(requestId: string, target: ActReplayTarget): Promise<ActReplayHandle>;
|
|
18
|
+
private replayEvents;
|
|
19
|
+
private emitReplay;
|
|
20
|
+
private replayAct;
|
|
21
|
+
retryOperation(requestId: string, target: ActReplayTarget): Promise<OperationReplayHandle>;
|
|
22
|
+
private replayOperation;
|
|
23
|
+
runFromHere(sourceRequestId: string, target: ActReplayTarget): Promise<ExecutorRun>;
|
|
18
24
|
private run;
|
|
19
25
|
private executeProject;
|
|
20
26
|
private executeFlow;
|
|
21
27
|
private executeStep;
|
|
22
28
|
private executeControl;
|
|
29
|
+
private resolveBindings;
|
|
30
|
+
private resolveOperand;
|
|
31
|
+
private evaluateCondition;
|
|
32
|
+
private capture;
|
|
33
|
+
private captureExternalEvidence;
|
|
34
|
+
private assertActive;
|
|
23
35
|
private executeAction;
|
|
24
36
|
private boundary;
|
|
25
37
|
private pauseAtBoundary;
|
|
26
38
|
private emit;
|
|
39
|
+
private emitReplayOccurrence;
|
|
27
40
|
private finish;
|
|
28
41
|
private baseResult;
|
|
29
42
|
private failure;
|
package/dist/executor.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { resolveDeclaredInputs } from './declared-inputs.js';
|
|
2
|
+
import { executionOccurrences, executionOperandDependencyIssues, hierarchyAddressIssues } from '@qaick/contract';
|
|
3
|
+
const addressKey = (address) => JSON.stringify([address.scope, address.segments.map(s => s.kind === 'body' ? ['body'] : [s.kind, s.position])]);
|
|
4
|
+
const invocationKey = (address, iterations) => JSON.stringify([addressKey(address), iterations.map(i => [addressKey(i.loopAddress), i.iteration])]);
|
|
5
|
+
function freezePlan(value) {
|
|
6
|
+
if (value && typeof value === 'object') {
|
|
7
|
+
Object.values(value).forEach(freezePlan);
|
|
8
|
+
Object.freeze(value);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
2
11
|
const MAX_TIMEOUT_MS = 300_000;
|
|
3
12
|
export function createExecutor(configuration = {}) {
|
|
4
13
|
return new DefaultExecutor(configuration);
|
|
@@ -13,21 +22,37 @@ export class DefaultExecutor {
|
|
|
13
22
|
}
|
|
14
23
|
registerActionExecutor(executor) { this.actionExecutors.push(executor); }
|
|
15
24
|
async execute(request) {
|
|
25
|
+
return this.startRun(request);
|
|
26
|
+
}
|
|
27
|
+
async startRun(request, debugSeed, debugProvenance) {
|
|
28
|
+
if (this.runs.has(request.requestId))
|
|
29
|
+
throw failure('PREFLIGHT_ERROR', 'Execution request ID already belongs to a Run.');
|
|
30
|
+
const requestSnapshot = structuredClone(request);
|
|
31
|
+
const validation = [];
|
|
32
|
+
let plan = new Map();
|
|
33
|
+
try {
|
|
34
|
+
// Shared traversal validates the projection before constructing the immutable plan.
|
|
35
|
+
plan = new Map(executionOccurrences(requestSnapshot.executionPackage).map(o => [addressKey(o.address), o]));
|
|
36
|
+
freezePlan(requestSnapshot.executionPackage);
|
|
37
|
+
}
|
|
38
|
+
catch (cause) {
|
|
39
|
+
validation.push({ code: 'invalid-execution-package', message: cause instanceof Error ? cause.message : 'Invalid Execution v7 projection.' });
|
|
40
|
+
}
|
|
16
41
|
let resolve;
|
|
17
42
|
const result = new Promise(promiseResolve => { resolve = promiseResolve; });
|
|
18
43
|
const run = {
|
|
19
|
-
request, resolve, events: [], listeners: [], abort: new AbortController(), cancelled: false,
|
|
44
|
+
debugSeed, debugProvenance, request: requestSnapshot, plan, journal: new Map(), replayCaptures: new Map(), resolve, events: [], listeners: [], abort: new AbortController(), cancelled: false,
|
|
20
45
|
timedOut: false, controlState: 'idle', pauseRequested: false, stepRequested: false,
|
|
21
46
|
controlWaiters: [], hitBreakpoints: new Set(), stopAfter: request.debugSession?.stopAfterStepId,
|
|
22
47
|
startedMs: Date.now(), pausedDurationMs: 0,
|
|
23
48
|
};
|
|
24
49
|
this.runs.set(request.requestId, run);
|
|
25
|
-
void this.run(run);
|
|
26
|
-
return { requestId: request.requestId, result };
|
|
50
|
+
void this.run(run, validation);
|
|
51
|
+
return { requestId: request.requestId, result, ...(debugProvenance ? { debugProvenance } : {}) };
|
|
27
52
|
}
|
|
28
53
|
async cancel(requestId) {
|
|
29
54
|
const run = this.runs.get(requestId);
|
|
30
|
-
if (!run || run.result)
|
|
55
|
+
if (!run || run.result || run.abort.signal.aborted)
|
|
31
56
|
return;
|
|
32
57
|
run.cancelled = true;
|
|
33
58
|
run.controlState = 'stopped';
|
|
@@ -92,92 +117,287 @@ export class DefaultExecutor {
|
|
|
92
117
|
run.resolve = value => { previous(value); resolve(value); };
|
|
93
118
|
});
|
|
94
119
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
// Terminal Act replay targets only private original structural captures.
|
|
121
|
+
replayTarget(requestId, target, kind) {
|
|
122
|
+
const parent = this.runs.get(requestId);
|
|
123
|
+
const reject = (message) => { throw failure('unsupported-execution-context', message); };
|
|
124
|
+
if (!parent?.result)
|
|
125
|
+
reject('Act replay requires a terminal original Run.');
|
|
126
|
+
const run = parent;
|
|
127
|
+
if (run.cancelled || run.timedOut)
|
|
128
|
+
reject('Interrupted original Runs cannot be replayed.');
|
|
129
|
+
if (run.activeReplay)
|
|
130
|
+
reject('An interactive replay session is already active for this Run.');
|
|
131
|
+
if (!isRecord(target) || Object.keys(target).some(key => !['structuralAddress', 'iterations'].includes(key)) || hierarchyAddressIssues(target.structuralAddress).length || !Array.isArray(target.iterations))
|
|
132
|
+
reject('Malformed structural replay target.');
|
|
133
|
+
const occurrence = run.plan.get(addressKey(target.structuralAddress));
|
|
134
|
+
if (!occurrence)
|
|
135
|
+
reject('Replay structural target does not exist in the original plan.');
|
|
136
|
+
const selections = target.iterations;
|
|
137
|
+
if (selections.length !== occurrence.loops.length || selections.some(selection => !isRecord(selection) || Object.keys(selection).some(key => !['loopAddress', 'iteration'].includes(key))
|
|
138
|
+
|| hierarchyAddressIssues(selection.loopAddress).length || !Number.isSafeInteger(selection.iteration) || selection.iteration < 1)
|
|
139
|
+
|| new Set(selections.map(selection => addressKey(selection.loopAddress))).size !== selections.length
|
|
140
|
+
|| occurrence.loops.some(loop => !selections.some(selection => addressKey(selection.loopAddress) === addressKey(loop))))
|
|
141
|
+
reject('Replay requires the complete explicit actual loop vector.');
|
|
142
|
+
const iterations = occurrence.loops.map(loop => ({ loopAddress: loop, iteration: selections.find(selection => addressKey(selection.loopAddress) === addressKey(loop)).iteration }));
|
|
143
|
+
const capture = run.replayCaptures.get(invocationKey(target.structuralAddress, iterations));
|
|
144
|
+
if (!capture || capture.kind !== kind)
|
|
145
|
+
reject('No original matching input capture exists for that structural invocation.');
|
|
146
|
+
return { run, occurrence: occurrence, capture: capture };
|
|
147
|
+
}
|
|
148
|
+
async retryAction(requestId, target) {
|
|
149
|
+
const { run, occurrence, capture } = this.replayTarget(requestId, target, 'action');
|
|
150
|
+
const snapshot = structuredClone({ structuralAddress: capture.structuralAddress, iterations: capture.iterations });
|
|
151
|
+
freezePlan(snapshot);
|
|
152
|
+
const session = {
|
|
153
|
+
parentRequestId: requestId, sessionId: crypto.randomUUID(), target: snapshot,
|
|
154
|
+
request: structuredClone(run.request), abort: new AbortController(), cancelled: false, timedOut: false,
|
|
155
|
+
kind: 'action', events: [], listeners: [],
|
|
156
|
+
};
|
|
157
|
+
run.activeReplay = session;
|
|
158
|
+
const result = this.replayAct(run, session, occurrence.step, capture.inputs);
|
|
159
|
+
const handle = {
|
|
160
|
+
parentRequestId: requestId, sessionId: session.sessionId, target: snapshot, result,
|
|
161
|
+
events: () => this.replayEvents(session),
|
|
162
|
+
cancel: async () => {
|
|
163
|
+
if (session.result || session.abort.signal.aborted)
|
|
164
|
+
return;
|
|
165
|
+
session.cancelled = true;
|
|
166
|
+
session.abort.abort();
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
return Object.freeze(handle);
|
|
170
|
+
}
|
|
171
|
+
async *replayEvents(session) {
|
|
172
|
+
let index = 0;
|
|
173
|
+
while (true) {
|
|
174
|
+
while (index < session.events.length)
|
|
175
|
+
yield session.events[index++];
|
|
176
|
+
if (session.result)
|
|
177
|
+
return;
|
|
178
|
+
await new Promise(resolve => session.listeners.push(resolve));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
emitReplay(session, type, state, attempt, terminalReason) {
|
|
182
|
+
const event = {
|
|
183
|
+
parentRequestId: session.parentRequestId, sessionId: session.sessionId,
|
|
184
|
+
sequence: session.events.length + 1, timestamp: new Date().toISOString(),
|
|
185
|
+
...structuredClone(session.target), kind: session.kind, type, state,
|
|
186
|
+
...(attempt === undefined ? {} : { attempt }), ...(terminalReason ? { terminalReason } : {}),
|
|
187
|
+
};
|
|
188
|
+
freezePlan(event);
|
|
189
|
+
session.events.push(event);
|
|
190
|
+
session.listeners.splice(0).forEach(resolve => resolve());
|
|
191
|
+
}
|
|
192
|
+
async replayAct(parent, session, step, inputs) {
|
|
193
|
+
const startedMs = Date.now(), startedAt = new Date().toISOString();
|
|
194
|
+
const attempts = [];
|
|
195
|
+
let timer;
|
|
196
|
+
let outputs = {}, evidence, error;
|
|
197
|
+
let state = 'completed';
|
|
104
198
|
try {
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
199
|
+
// The approved session deadline is fresh and covers all attempts and retry delays.
|
|
200
|
+
const timeoutMs = session.request.options?.timeoutMs;
|
|
201
|
+
if (timeoutMs !== undefined)
|
|
202
|
+
timer = setTimeout(() => {
|
|
203
|
+
if (session.abort.signal.aborted)
|
|
204
|
+
return;
|
|
205
|
+
session.timedOut = true;
|
|
206
|
+
session.abort.abort();
|
|
207
|
+
}, timeoutMs);
|
|
208
|
+
this.emitReplay(session, 'replay_started', 'running');
|
|
209
|
+
const result = await this.executeAction(session, step, inputs, attempts, session.abort.signal, (type, attempt, attemptState) => this.emitReplay(session, type, attemptState, attempt));
|
|
210
|
+
this.assertActive(session, session.abort.signal);
|
|
211
|
+
outputs = structuredClone(result.outputs);
|
|
212
|
+
evidence = structuredClone(result.evidence);
|
|
119
213
|
}
|
|
120
214
|
catch (cause) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
located.error = this.failure(cause, run);
|
|
124
|
-
this.emit(run, 'step_finished', 'failed', `${located.name} interactive retry failed.`, definition);
|
|
215
|
+
error = structuredClone(this.failure(cause, session));
|
|
216
|
+
state = session.timedOut ? 'failed' : session.cancelled ? 'cancelled' : 'failed';
|
|
125
217
|
}
|
|
218
|
+
finally {
|
|
219
|
+
if (timer !== undefined)
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
// One admitted session owns this lock and releases it on every terminal path.
|
|
222
|
+
parent.activeReplay = undefined;
|
|
223
|
+
}
|
|
224
|
+
const terminalReason = session.timedOut ? 'timeout' : state === 'cancelled' ? 'cancelled' : state === 'failed' ? 'failed' : 'completed';
|
|
225
|
+
const result = {
|
|
226
|
+
parentRequestId: session.parentRequestId, sessionId: session.sessionId, target: structuredClone(session.target), kind: 'action',
|
|
227
|
+
state, terminalReason, startedAt, finishedAt: new Date().toISOString(), durationMs: Date.now() - startedMs,
|
|
228
|
+
inputs: structuredClone(inputs), outputs, ...(evidence ? { evidence } : {}), attempts,
|
|
229
|
+
...(error ? { error } : {}),
|
|
230
|
+
};
|
|
231
|
+
freezePlan(result);
|
|
232
|
+
session.result = result;
|
|
233
|
+
this.emitReplay(session, 'replay_finished', state, undefined, terminalReason);
|
|
234
|
+
return result;
|
|
126
235
|
}
|
|
127
|
-
async retryOperation(requestId,
|
|
128
|
-
const run = this.
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
located.error = nested.error;
|
|
150
|
-
if (located.state === 'finished' && run.result.state === 'failed') {
|
|
151
|
-
run.result.state = 'completed';
|
|
152
|
-
run.result.terminalReason = 'completed';
|
|
153
|
-
delete run.result.error;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
async runFromHere(requestId, stepId, invocationPath) {
|
|
157
|
-
const run = this.runs.get(requestId);
|
|
158
|
-
if (!run)
|
|
159
|
-
return undefined;
|
|
160
|
-
if (invocationPath?.length)
|
|
161
|
-
return undefined;
|
|
162
|
-
const index = run.request.executionPackage.steps.findIndex(step => step.id === stepId);
|
|
163
|
-
if (index < 0)
|
|
164
|
-
return undefined;
|
|
165
|
-
const selected = findStep(run.result?.steps ?? [], stepId, invocationPath);
|
|
166
|
-
if (!selected)
|
|
167
|
-
return undefined;
|
|
168
|
-
const requestIdForRun = `${requestId}:from:${stepId}`;
|
|
169
|
-
const remainingSteps = run.request.executionPackage.steps.slice(index).map((step, offset) => offset === 0
|
|
170
|
-
? { ...step, bindings: undefined, staticInputs: { ...(step.staticInputs ?? {}), ...selected.inputs } }
|
|
171
|
-
: step);
|
|
172
|
-
return this.execute({
|
|
173
|
-
...run.request,
|
|
174
|
-
requestId: requestIdForRun,
|
|
175
|
-
executionPackage: { ...run.request.executionPackage, steps: remainingSteps, target: { kind: 'flow', id: run.request.executionPackage.target.id, name: run.request.executionPackage.target.name } },
|
|
176
|
-
inputs: selected.inputs,
|
|
236
|
+
async retryOperation(requestId, target) {
|
|
237
|
+
const { run, occurrence, capture } = this.replayTarget(requestId, target, 'operation');
|
|
238
|
+
if (capture.kind !== 'operation')
|
|
239
|
+
throw failure('unsupported-execution-context', 'Operation capture required.');
|
|
240
|
+
const snapshot = structuredClone({ structuralAddress: capture.structuralAddress, iterations: capture.iterations });
|
|
241
|
+
freezePlan(snapshot);
|
|
242
|
+
const session = {
|
|
243
|
+
parentRequestId: requestId, sessionId: crypto.randomUUID(), target: snapshot, kind: 'operation',
|
|
244
|
+
request: structuredClone(run.request), abort: new AbortController(), cancelled: false, timedOut: false,
|
|
245
|
+
events: [], listeners: [],
|
|
246
|
+
};
|
|
247
|
+
run.activeReplay = session;
|
|
248
|
+
const result = this.replayOperation(run, session, occurrence.step, capture);
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
parentRequestId: requestId, sessionId: session.sessionId, target: snapshot, result,
|
|
251
|
+
events: () => this.replayEvents(session),
|
|
252
|
+
cancel: async () => {
|
|
253
|
+
if (session.result || session.abort.signal.aborted)
|
|
254
|
+
return;
|
|
255
|
+
session.cancelled = true;
|
|
256
|
+
session.abort.abort();
|
|
257
|
+
},
|
|
177
258
|
});
|
|
178
259
|
}
|
|
179
|
-
async
|
|
260
|
+
async replayOperation(parent, session, step, capture) {
|
|
261
|
+
const startedMs = Date.now(), startedAt = new Date().toISOString();
|
|
262
|
+
// Separate structural state, not a new Run: no registration, root result, resolver,
|
|
263
|
+
// historical journal, controls, captures or original event stream are reused.
|
|
264
|
+
const context = {
|
|
265
|
+
request: session.request, plan: parent.plan, journal: new Map(), replayCaptures: new Map(),
|
|
266
|
+
replay: { session, capture, root: capture.structuralAddress },
|
|
267
|
+
events: [], listeners: [], abort: session.abort,
|
|
268
|
+
get cancelled() { return session.cancelled; }, get timedOut() { return session.timedOut; },
|
|
269
|
+
controlState: 'running', pauseRequested: false, stepRequested: false,
|
|
270
|
+
controlWaiters: [], hitBreakpoints: new Set(), startedMs, pausedDurationMs: 0,
|
|
271
|
+
};
|
|
272
|
+
let timer;
|
|
273
|
+
let operation, error;
|
|
274
|
+
let state = 'completed';
|
|
275
|
+
try {
|
|
276
|
+
const timeoutMs = session.request.options?.timeoutMs;
|
|
277
|
+
if (timeoutMs !== undefined)
|
|
278
|
+
timer = setTimeout(() => {
|
|
279
|
+
if (session.abort.signal.aborted)
|
|
280
|
+
return;
|
|
281
|
+
session.timedOut = true;
|
|
282
|
+
session.abort.abort();
|
|
283
|
+
}, timeoutMs);
|
|
284
|
+
this.emitReplay(session, 'replay_started', 'running');
|
|
285
|
+
operation = await this.executeStep(context, step, capture.lexicalScope, [], session.abort.signal, capture.iterations);
|
|
286
|
+
this.assertActive(session, session.abort.signal);
|
|
287
|
+
if (operation.state !== 'finished') {
|
|
288
|
+
state = 'failed';
|
|
289
|
+
error = operation.error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (cause) {
|
|
293
|
+
error = structuredClone(this.failure(cause, session));
|
|
294
|
+
state = session.timedOut ? 'failed' : session.cancelled ? 'cancelled' : 'failed';
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
if (timer !== undefined)
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
parent.activeReplay = undefined;
|
|
300
|
+
}
|
|
301
|
+
const terminalReason = session.timedOut ? 'timeout' : state === 'cancelled' ? 'cancelled' : state === 'failed' ? 'failed' : 'completed';
|
|
302
|
+
const result = {
|
|
303
|
+
parentRequestId: session.parentRequestId, sessionId: session.sessionId,
|
|
304
|
+
target: structuredClone(session.target), kind: 'operation', state, terminalReason,
|
|
305
|
+
startedAt, finishedAt: new Date().toISOString(), durationMs: Date.now() - startedMs,
|
|
306
|
+
inputs: structuredClone(capture.inputs), lexicalScope: structuredClone(capture.lexicalScope),
|
|
307
|
+
children: structuredClone(operation?.children ?? []), outputs: state === 'completed' ? structuredClone(operation?.outputs ?? {}) : {},
|
|
308
|
+
...(error ? { error: structuredClone(error) } : {}),
|
|
309
|
+
};
|
|
310
|
+
freezePlan(result);
|
|
311
|
+
session.result = result;
|
|
312
|
+
this.emitReplay(session, 'replay_finished', state, undefined, terminalReason);
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
async runFromHere(sourceRequestId, target) {
|
|
316
|
+
const source = this.runs.get(sourceRequestId);
|
|
317
|
+
const reject = (message) => { throw failure('unsupported-execution-context', message); };
|
|
318
|
+
if (!source?.result || source.cancelled || source.timedOut)
|
|
319
|
+
reject('Run From Here requires a terminal uninterrupted source Run.');
|
|
320
|
+
if (!isRecord(target) || Object.keys(target).some(k => !['structuralAddress', 'iterations'].includes(k))
|
|
321
|
+
|| hierarchyAddressIssues(target.structuralAddress).length || !Array.isArray(target.iterations) || target.iterations.length)
|
|
322
|
+
reject('Run From Here requires a top-level target with an empty iteration vector.');
|
|
323
|
+
const address = target.structuralAddress, segments = address.segments;
|
|
324
|
+
const topLevel = address.scope === 'routine'
|
|
325
|
+
? segments.length === 2 && segments[0].kind === 'flow' && segments[1].kind === 'step'
|
|
326
|
+
: segments.length === 1 && segments[0].kind === 'step';
|
|
327
|
+
if (!topLevel)
|
|
328
|
+
reject('Nested/control continuation is not supported by Run From Here.');
|
|
329
|
+
const parent = source;
|
|
330
|
+
const capture = parent.replayCaptures.get(invocationKey(address, []));
|
|
331
|
+
if (!capture || (capture.kind !== 'action' && capture.kind !== 'operation'))
|
|
332
|
+
reject('No eligible private original input capture exists.');
|
|
333
|
+
const package_ = structuredClone(parent.request.executionPackage);
|
|
334
|
+
const position = segments.at(-1);
|
|
335
|
+
if (position.kind !== 'step')
|
|
336
|
+
throw failure('unsupported-execution-context', 'Expected a top-level step.');
|
|
337
|
+
const stepPosition = position.position;
|
|
338
|
+
const omitted = [];
|
|
339
|
+
if (package_.executionContext.mode === 'routine') {
|
|
340
|
+
const first = segments[0];
|
|
341
|
+
if (first.kind !== 'flow')
|
|
342
|
+
throw failure('unsupported-execution-context', 'Routine selection requires an original Flow ordinal.');
|
|
343
|
+
const flowPosition = first.position;
|
|
344
|
+
const omittedFlowInputs = new Set((package_.flows ?? []).filter(f => f.structuralPosition < flowPosition).flatMap(f => (f.inputDeclarations ?? []).map(d => d.name)));
|
|
345
|
+
package_.flows = (package_.flows ?? []).filter(flow => {
|
|
346
|
+
if (flow.structuralPosition < flowPosition) {
|
|
347
|
+
omitted.push(...flow.steps.map(s => s.structuralAddress));
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
if (flow.structuralPosition === flowPosition)
|
|
351
|
+
flow.steps = flow.steps.filter(step => {
|
|
352
|
+
const last = step.structuralAddress.segments.at(-1);
|
|
353
|
+
if (last.kind === 'step' && last.position < stepPosition) {
|
|
354
|
+
omitted.push(step.structuralAddress);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
});
|
|
359
|
+
return true;
|
|
360
|
+
});
|
|
361
|
+
const retainedRequirements = new Set([...(package_.inputDeclarations ?? []).map(d => d.name), ...(package_.flows ?? []).flatMap(f => [...(f.requiredInputs ?? []), ...(f.inputDeclarations ?? []).map(d => d.name)])]);
|
|
362
|
+
if (package_.requiredInputs)
|
|
363
|
+
package_.requiredInputs = package_.requiredInputs.filter(name => !omittedFlowInputs.has(name) || retainedRequirements.has(name));
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
package_.steps = package_.steps.filter(step => {
|
|
367
|
+
const last = step.structuralAddress.segments.at(-1);
|
|
368
|
+
if (last.kind === 'step' && last.position < stepPosition) {
|
|
369
|
+
omitted.push(step.structuralAddress);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// Keep existing omissions and original parent/Flow ordinals; never lift or renumber.
|
|
376
|
+
const omissions = new Map((package_.omittedOccurrences ?? []).map(o => [addressKey(o.address), o]));
|
|
377
|
+
for (const omittedAddress of omitted)
|
|
378
|
+
omissions.set(addressKey(omittedAddress), { address: omittedAddress, reason: 'Run From Here predecessor omitted.' });
|
|
379
|
+
package_.omittedOccurrences = [...omissions.values()];
|
|
380
|
+
const seed = {
|
|
381
|
+
structuralAddress: structuredClone(capture.structuralAddress), inputs: structuredClone(capture.inputs),
|
|
382
|
+
...(capture.kind === 'operation' ? { kind: 'operation', lexicalScope: structuredClone(capture.lexicalScope) } : { kind: 'action' }),
|
|
383
|
+
};
|
|
384
|
+
const provenance = {
|
|
385
|
+
kind: 'run-from-here', sourceRequestId, selection: structuredClone(target),
|
|
386
|
+
inputSeed: { kind: 'captured-inputs', inputs: structuredClone(capture.inputs),
|
|
387
|
+
...(capture.kind === 'operation' ? { lexicalScope: structuredClone(capture.lexicalScope) } : {}) },
|
|
388
|
+
omittedOccurrences: package_.omittedOccurrences.map(o => structuredClone(o.address)),
|
|
389
|
+
};
|
|
390
|
+
freezePlan(seed);
|
|
391
|
+
freezePlan(provenance);
|
|
392
|
+
// Normal Run creation is independent of replay admission and has no external evidence view.
|
|
393
|
+
return this.startRun({ ...structuredClone(parent.request), requestId: crypto.randomUUID(), executionPackage: package_ }, seed, provenance);
|
|
394
|
+
}
|
|
395
|
+
async run(run, validation) {
|
|
180
396
|
const startedAt = new Date().toISOString();
|
|
397
|
+
if (validation.length) {
|
|
398
|
+
this.finish(run, this.baseResult(run, startedAt, 'failed', undefined, validation));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
181
401
|
const timeoutMs = run.request.options?.timeoutMs;
|
|
182
402
|
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > this.maxTimeoutMs)) {
|
|
183
403
|
this.finish(run, this.baseResult(run, startedAt, 'failed', { code: 'PREFLIGHT_ERROR', message: `Timeout must be between 0 and ${this.maxTimeoutMs} ms.` }));
|
|
@@ -185,7 +405,8 @@ export class DefaultExecutor {
|
|
|
185
405
|
}
|
|
186
406
|
let timeout;
|
|
187
407
|
if (timeoutMs !== undefined)
|
|
188
|
-
timeout = setTimeout(() => {
|
|
408
|
+
timeout = setTimeout(() => { if (run.abort.signal.aborted)
|
|
409
|
+
return; run.timedOut = true; run.abort.abort(); }, timeoutMs);
|
|
189
410
|
this.emit(run, 'run_started', 'created', 'Execution accepted.');
|
|
190
411
|
this.emit(run, 'preflight_started', 'preflighting', 'Validating execution package.');
|
|
191
412
|
const arguments_ = resolveDeclaredInputs(run.request.executionPackage.inputDeclarations, run.request.inputs);
|
|
@@ -211,7 +432,7 @@ export class DefaultExecutor {
|
|
|
211
432
|
if (timeout)
|
|
212
433
|
clearTimeout(timeout);
|
|
213
434
|
const error = this.failure(cause, run);
|
|
214
|
-
this.finish(run, this.baseResult(run, startedAt, run.timedOut ? '
|
|
435
|
+
this.finish(run, this.baseResult(run, startedAt, run.cancelled || run.abort.signal.aborted && !run.timedOut ? 'cancelled' : 'failed', error));
|
|
215
436
|
}
|
|
216
437
|
}
|
|
217
438
|
async executeProject(run, startedAt) {
|
|
@@ -229,33 +450,34 @@ export class DefaultExecutor {
|
|
|
229
450
|
children.push(child);
|
|
230
451
|
if (child.state !== 'completed')
|
|
231
452
|
return this.baseResult(run, startedAt, child.state, child.error, undefined, children);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
? Object.fromEntries(Object.entries(flow.outputBindings).flatMap(([name, ref]) => {
|
|
235
|
-
const match = /^\$outputs\.(.+)$/.exec(ref);
|
|
236
|
-
return match && last[match[1]] !== undefined ? [[name, last[match[1]]]] : [];
|
|
237
|
-
}))
|
|
238
|
-
: last;
|
|
239
|
-
inputs = { ...inputs, ...mapped };
|
|
453
|
+
if (flow.outputBindings && Object.keys(flow.outputBindings).length)
|
|
454
|
+
throw failure('unsupported-context', 'Flow completion exports are outside this foundation slice.');
|
|
240
455
|
}
|
|
241
456
|
return this.baseResult(run, startedAt, 'completed', undefined, undefined, children);
|
|
242
457
|
}
|
|
243
|
-
async executeFlow(run, steps, initialInputs, path, startedAt, target = run.request.executionPackage.target, signal = run.abort.signal) {
|
|
244
|
-
const outputsById = new Map();
|
|
458
|
+
async executeFlow(run, steps, initialInputs, path, startedAt, target = run.request.executionPackage.target, signal = run.abort.signal, iterations = [], bodyFailurePolicy = 'stop') {
|
|
245
459
|
const results = [];
|
|
246
460
|
let state = 'completed';
|
|
247
461
|
let error;
|
|
248
|
-
for (const step of
|
|
462
|
+
for (const step of steps) {
|
|
249
463
|
if (run.cancelled || signal.aborted)
|
|
250
464
|
throw new Error('Execution cancelled.');
|
|
251
|
-
await this.boundary(run, step, [...path, step.id], signal);
|
|
252
|
-
|
|
465
|
+
await this.boundary(run, step, [...path, step.id], signal, iterations);
|
|
466
|
+
if (run.cancelled || signal.aborted)
|
|
467
|
+
throw new Error('Execution cancelled.');
|
|
468
|
+
const result = await this.executeStep(run, step, initialInputs, path, signal, iterations);
|
|
253
469
|
results.push(result);
|
|
254
|
-
outputsById.set(step.id, result.outputs);
|
|
255
470
|
if (result.state === 'failed' || result.state === 'cancelled') {
|
|
471
|
+
const interrupted = result.state === 'cancelled' || run.cancelled || run.timedOut || signal.aborted
|
|
472
|
+
|| result.terminalReason === 'timeout' || result.error?.category === 'ACTION_TIMEOUT'
|
|
473
|
+
|| result.error?.code === 'ACTION_TIMEOUT' || result.error?.code === 'HTTP_TIMEOUT';
|
|
474
|
+
// Only this explicitly configured body's ordinary failures are tolerated.
|
|
475
|
+
// Its child failures remain recorded; policies never leak into nested bodies.
|
|
476
|
+
if (bodyFailurePolicy === 'continue' && !interrupted)
|
|
477
|
+
continue;
|
|
256
478
|
state = result.state === 'cancelled' ? 'cancelled' : 'failed';
|
|
257
479
|
error = result.error;
|
|
258
|
-
if (step.control?.onError !== 'continue')
|
|
480
|
+
if (interrupted || step.control?.onError !== 'continue')
|
|
259
481
|
break;
|
|
260
482
|
}
|
|
261
483
|
}
|
|
@@ -265,68 +487,82 @@ export class DefaultExecutor {
|
|
|
265
487
|
steps: results,
|
|
266
488
|
};
|
|
267
489
|
}
|
|
268
|
-
async executeStep(run, step, scope,
|
|
490
|
+
async executeStep(run, step, scope, path, signal = run.abort.signal, iterations = []) {
|
|
269
491
|
const startedMs = Date.now();
|
|
270
492
|
const invocationPath = [...path, step.id];
|
|
271
|
-
const base = { id: step.id, name: step.name, kind: step.kind ?? 'action', inputs: {}, outputs: {}, invocationId: invocationPath.join('/'), invocationPath };
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
this.emit(run, 'step_started', 'running', `${step.name} started.`, step);
|
|
493
|
+
const base = { id: step.id, name: step.name, kind: step.kind ?? 'action', inputs: {}, outputs: {}, structuralAddress: structuredClone(step.structuralAddress), iterations: structuredClone(iterations), invocationId: invocationPath.join('/'), invocationPath };
|
|
494
|
+
let inputs = {};
|
|
495
|
+
const attempts = [];
|
|
496
|
+
this.emit(run, 'step_started', 'running', `${step.name} started.`, step, undefined, iterations);
|
|
276
497
|
try {
|
|
498
|
+
const debugSeed = run.debugSeed && addressKey(run.debugSeed.structuralAddress) === addressKey(step.structuralAddress) ? run.debugSeed : undefined;
|
|
499
|
+
const rootCapture = run.replay && addressKey(run.replay.root) === addressKey(step.structuralAddress) ? run.replay.capture : debugSeed?.kind === 'operation' ? debugSeed : undefined;
|
|
500
|
+
inputs = debugSeed ? structuredClone(debugSeed.inputs) : rootCapture ? structuredClone(rootCapture.inputs) : { ...structuredClone(step.staticInputs ?? {}), ...this.resolveBindings(run, step.bindings ?? {}, scope, step.structuralAddress) };
|
|
277
501
|
if (step.delayMs) {
|
|
278
|
-
this.emit(run, 'waiting', 'waiting', `${step.name} is waiting for ${step.delayMs} ms.`, step);
|
|
502
|
+
this.emit(run, 'waiting', 'waiting', `${step.name} is waiting for ${step.delayMs} ms.`, step, undefined, iterations);
|
|
279
503
|
await delay(step.delayMs, signal);
|
|
280
504
|
}
|
|
281
505
|
if (step.control) {
|
|
282
|
-
const controlResult = await this.executeControl(run, step, inputs, scope,
|
|
506
|
+
const controlResult = await this.executeControl(run, step, inputs, scope, path, signal, iterations);
|
|
283
507
|
return { ...base, ...controlResult, inputs, durationMs: Date.now() - startedMs };
|
|
284
508
|
}
|
|
285
509
|
if (step.kind === 'operation' && step.children) {
|
|
286
|
-
const arguments_ = resolveDeclaredInputs(step.inputDeclarations, inputs);
|
|
510
|
+
const arguments_ = rootCapture ? { inputs: structuredClone(rootCapture.inputs), issues: [] } : resolveDeclaredInputs(step.inputDeclarations, inputs);
|
|
287
511
|
if (arguments_.issues.length)
|
|
288
512
|
throw failure('BINDING_RESOLUTION_ERROR', arguments_.issues.map(issue => issue.message).join(' '));
|
|
289
|
-
const childScope = { ...scope, ...arguments_.inputs };
|
|
513
|
+
const childScope = rootCapture ? structuredClone(rootCapture.lexicalScope) : { ...scope, ...arguments_.inputs };
|
|
290
514
|
for (const declaration of step.inputDeclarations ?? [])
|
|
291
515
|
if (!Object.hasOwn(arguments_.inputs, declaration.name))
|
|
292
516
|
delete childScope[declaration.name];
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
517
|
+
if (!run.replay)
|
|
518
|
+
this.capture(run, step, iterations, arguments_.inputs, {
|
|
519
|
+
kind: 'operation', lexicalScope: structuredClone(childScope),
|
|
520
|
+
externalEvidence: this.captureExternalEvidence(run, step, iterations),
|
|
521
|
+
});
|
|
522
|
+
const nested = await this.executeFlow(run, step.children, childScope, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal, iterations);
|
|
523
|
+
const outputs = nested.state === 'completed'
|
|
524
|
+
? { ...structuredClone(step.staticOutputs ?? {}), ...this.resolveBindings(run, step.outputBindings ?? {}, childScope, step.structuralAddress, { phase: 'completion', address: step.structuralAddress, iterations }) }
|
|
525
|
+
: {};
|
|
297
526
|
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 };
|
|
298
|
-
this.emit(run, 'step_finished', result.state, `${step.name} ${result.state}.`, step);
|
|
527
|
+
this.emit(run, 'step_finished', result.state, `${step.name} ${result.state}.`, step, undefined, iterations);
|
|
299
528
|
return result;
|
|
300
529
|
}
|
|
301
|
-
|
|
530
|
+
if (!run.replay)
|
|
531
|
+
this.capture(run, step, iterations, inputs, { kind: 'action' });
|
|
532
|
+
const actionResult = await this.executeAction(run, step, inputs, attempts, signal, run.replay ? (type, attempt, state) => this.emitReplayOccurrence(run.replay.session, type, state, step, iterations, attempt) : undefined);
|
|
533
|
+
if (signal.aborted || run.cancelled)
|
|
534
|
+
throw failure(run.timedOut ? 'ACTION_TIMEOUT' : 'ACTION_CANCELLED', 'Execution ended before evidence publication.');
|
|
535
|
+
run.journal.set(invocationKey(step.structuralAddress, iterations), { outputs: structuredClone(actionResult.outputs), ...(actionResult.evidence ? { evidence: structuredClone(actionResult.evidence) } : {}) });
|
|
536
|
+
freezePlan(attempts);
|
|
302
537
|
const result = { ...base, state: 'finished', inputs, outputs: actionResult.outputs, durationMs: Date.now() - startedMs, terminalReason: 'completed', attempts: actionResult.attempts, evidence: actionResult.evidence };
|
|
303
|
-
this.emit(run, 'step_finished', 'finished', `${step.name} finished.`, step);
|
|
538
|
+
this.emit(run, 'step_finished', 'finished', `${step.name} finished.`, step, undefined, iterations);
|
|
304
539
|
if (run.stopAfter === step.id) {
|
|
305
540
|
run.stopAfter = undefined;
|
|
306
|
-
await this.pauseAtBoundary(run, step);
|
|
541
|
+
await this.pauseAtBoundary(run, step, iterations);
|
|
307
542
|
}
|
|
308
543
|
return result;
|
|
309
544
|
}
|
|
310
545
|
catch (cause) {
|
|
311
546
|
const failure = this.failure(cause, run, signal);
|
|
312
547
|
const state = run.timedOut ? 'failed' : run.cancelled || signal.aborted ? 'cancelled' : 'failed';
|
|
313
|
-
|
|
314
|
-
|
|
548
|
+
freezePlan(attempts);
|
|
549
|
+
const result = { ...base, state, inputs, durationMs: Date.now() - startedMs, ...(attempts.length ? { attempts } : {}), terminalReason: run.timedOut ? 'timeout' : state === 'cancelled' ? 'cancelled' : 'failed', error: failure };
|
|
550
|
+
this.emit(run, 'step_finished', state, `${step.name} ${state}.`, step, undefined, iterations);
|
|
315
551
|
return result;
|
|
316
552
|
}
|
|
317
553
|
}
|
|
318
|
-
async executeControl(run, step, inputs, scope,
|
|
554
|
+
async executeControl(run, step, inputs, scope, path, signal = run.abort.signal, iterations = []) {
|
|
319
555
|
const control = step.control;
|
|
320
|
-
if (control.condition !== undefined && !evaluateCondition(control.condition, { ...scope, ...inputs },
|
|
556
|
+
if (control.condition !== undefined && !this.evaluateCondition(run, control.condition, { ...scope, ...inputs }, step.structuralAddress))
|
|
321
557
|
return { state: 'skipped', outputs: {}, terminalReason: 'condition-skipped' };
|
|
322
558
|
if (control.type === 'repeat') {
|
|
323
|
-
const total =
|
|
559
|
+
const total = Math.max(1, Math.min(control.iterations ?? (control.durationMs !== undefined ? 32 : 1), 32));
|
|
324
560
|
const loopStarted = Date.now();
|
|
325
561
|
const children = [];
|
|
326
562
|
for (let current = 1; current <= total; current++) {
|
|
327
|
-
if (
|
|
563
|
+
if (control.durationMs !== undefined && Date.now() - loopStarted >= control.durationMs)
|
|
328
564
|
break;
|
|
329
|
-
const child = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id, String(current)], new Date().toISOString(), run.request.executionPackage.target, signal);
|
|
565
|
+
const child = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id, String(current)], new Date().toISOString(), run.request.executionPackage.target, signal, [...iterations, { loopAddress: step.structuralAddress, iteration: current }]);
|
|
330
566
|
children.push(...child.steps.map(item => ({ ...item, iteration: { loopId: step.id, current, total } })));
|
|
331
567
|
if (child.state !== 'completed' && control.onError !== 'continue')
|
|
332
568
|
return { state: child.state, outputs: child.steps.at(-1)?.outputs ?? {}, children, terminalReason: child.terminalReason, error: child.error };
|
|
@@ -335,43 +571,29 @@ export class DefaultExecutor {
|
|
|
335
571
|
}
|
|
336
572
|
return { state: 'finished', outputs: children.at(-1)?.outputs ?? {}, children, terminalReason: 'completed' };
|
|
337
573
|
}
|
|
338
|
-
if (control.type === 'branch'
|
|
574
|
+
if (control.type === 'branch') {
|
|
339
575
|
const children = [];
|
|
340
576
|
for (const branch of control.branches ?? []) {
|
|
341
|
-
if (!evaluateCondition(branch.condition, { ...scope, ...inputs },
|
|
577
|
+
if (!this.evaluateCondition(run, branch.condition, { ...scope, ...inputs }, step.structuralAddress))
|
|
342
578
|
continue;
|
|
343
|
-
const nested = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, signal);
|
|
579
|
+
const nested = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, signal, iterations);
|
|
344
580
|
children.push(...nested.steps);
|
|
345
581
|
if (nested.state !== 'completed' && control.onError !== 'continue')
|
|
346
582
|
return { state: nested.state, outputs: nested.steps.at(-1)?.outputs ?? {}, children, terminalReason: nested.terminalReason, error: nested.error };
|
|
347
583
|
}
|
|
348
584
|
return { state: children.length ? 'finished' : 'skipped', outputs: children.at(-1)?.outputs ?? {}, children, terminalReason: children.length ? 'completed' : 'branch-skipped' };
|
|
349
585
|
}
|
|
350
|
-
if (control.type === 'branch') {
|
|
351
|
-
const branch = control.branches?.find(candidate => evaluateCondition(candidate.condition, { ...scope, ...inputs }, outputs));
|
|
352
|
-
if (!branch)
|
|
353
|
-
return { state: 'skipped', outputs: {}, terminalReason: 'branch-skipped' };
|
|
354
|
-
const nested = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, signal);
|
|
355
|
-
return { state: nested.state === 'completed' ? 'finished' : nested.state, outputs: nested.steps.at(-1)?.outputs ?? {}, children: nested.steps, terminalReason: nested.terminalReason, error: nested.error };
|
|
356
|
-
}
|
|
357
586
|
if (control.type === 'parallel') {
|
|
358
587
|
const branches = control.branches ?? [];
|
|
359
|
-
const
|
|
360
|
-
const selected = branches.filter(branch => evaluateCondition(branch.condition, { ...scope, ...inputs }, outputs));
|
|
588
|
+
const selected = branches.filter(branch => this.evaluateCondition(run, branch.condition, { ...scope, ...inputs }, step.structuralAddress));
|
|
361
589
|
const controllers = selected.map(() => new AbortController());
|
|
362
590
|
const abortBranches = () => controllers.forEach(controller => controller.abort());
|
|
363
591
|
signal.addEventListener('abort', abortBranches, { once: true });
|
|
364
592
|
if (signal.aborted)
|
|
365
593
|
abortBranches();
|
|
366
|
-
let winnerIndex;
|
|
367
594
|
const branchRuns = await Promise.all(selected.map(async (branch, index) => {
|
|
368
595
|
try {
|
|
369
|
-
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);
|
|
370
|
-
if (join === 'first' && result.state === 'completed' && winnerIndex === undefined) {
|
|
371
|
-
winnerIndex = index;
|
|
372
|
-
controllers.forEach((controller, candidate) => { if (candidate !== index)
|
|
373
|
-
controller.abort(); });
|
|
374
|
-
}
|
|
596
|
+
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, iterations);
|
|
375
597
|
return { branch, result };
|
|
376
598
|
}
|
|
377
599
|
catch (cause) {
|
|
@@ -381,61 +603,166 @@ export class DefaultExecutor {
|
|
|
381
603
|
signal.removeEventListener('abort', abortBranches);
|
|
382
604
|
const nested = branchRuns.map(item => item.result);
|
|
383
605
|
const children = nested.flatMap(item => item.steps);
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return { state: failed ? invocationState(failed.result.state) : 'finished', outputs: mergedOutputs, children, terminalReason: failed?.result.terminalReason ?? 'parallel-joined', error: failed?.result.error };
|
|
606
|
+
const failed = branchRuns.find(item => item.result.state !== 'completed');
|
|
607
|
+
if (control.mergeBindings)
|
|
608
|
+
throw failure('unsupported-context', 'Parallel output projections are outside this foundation slice.');
|
|
609
|
+
return { state: failed ? invocationState(failed.result.state) : 'finished', outputs: {}, children, terminalReason: failed?.result.terminalReason ?? 'parallel-joined', error: failed?.result.error };
|
|
389
610
|
}
|
|
390
|
-
const nested = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal);
|
|
611
|
+
const nested = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal, iterations, control.type === 'sequence' ? control.onError ?? 'stop' : 'stop');
|
|
391
612
|
return { state: nested.state === 'completed' ? 'finished' : nested.state, outputs: nested.steps.at(-1)?.outputs ?? {}, children: nested.steps, terminalReason: nested.terminalReason, error: nested.error };
|
|
392
613
|
}
|
|
393
|
-
|
|
614
|
+
resolveBindings(run, bindings, scope, consumer, context = { phase: 'entry', address: consumer }) {
|
|
615
|
+
return Object.fromEntries(Object.entries(bindings).map(([name, operand]) => [name, this.resolveOperand(run, operand, scope, consumer, context)]));
|
|
616
|
+
}
|
|
617
|
+
resolveOperand(run, operand, scope, consumer, context = { phase: 'entry', address: consumer }) {
|
|
618
|
+
const issues = executionOperandDependencyIssues(run.request.executionPackage, operand, context);
|
|
619
|
+
if (issues.length)
|
|
620
|
+
throw { code: issues[0].code, message: issues[0].reason, details: { consumer, operand } };
|
|
621
|
+
if (operand.type === 'input') {
|
|
622
|
+
let value = scope;
|
|
623
|
+
for (const part of operand.path) {
|
|
624
|
+
if (value === null || typeof value !== 'object' || !Object.hasOwn(value, part))
|
|
625
|
+
throw failure('BINDING_RESOLUTION_ERROR', `Required lexical input ${JSON.stringify(operand.path)} is absent.`);
|
|
626
|
+
value = value[part];
|
|
627
|
+
}
|
|
628
|
+
if (value === undefined)
|
|
629
|
+
throw failure('BINDING_RESOLUTION_ERROR', 'Required lexical input value is undefined.');
|
|
630
|
+
return structuredClone(value);
|
|
631
|
+
}
|
|
632
|
+
if (operand.type === 'unresolved-output')
|
|
633
|
+
throw failure(operand.code, operand.reason);
|
|
634
|
+
const source = run.plan.get(addressKey(operand.address));
|
|
635
|
+
const selectors = source.loops.map(loop => ({ loopAddress: loop, iteration: operand.iterations.find(i => addressKey(i.loopAddress) === addressKey(loop)).iteration }));
|
|
636
|
+
const key = invocationKey(source.address, selectors);
|
|
637
|
+
const invocation = run.replay && !ownsAddress(run.replay.root, source.address)
|
|
638
|
+
? run.replay.capture.externalEvidence.get(key) : run.journal.get(key);
|
|
639
|
+
if (!invocation || !Object.hasOwn(invocation.outputs, operand.outputName) || invocation.outputs[operand.outputName] === undefined)
|
|
640
|
+
throw { code: 'output-evidence-unavailable', message: run.replay ? 'Required successful Act output is unavailable in this replay evidence view.' : 'Required successful Act output is unavailable in this Run.', details: { missing: invocation ? 'output' : 'invocation', source: operand.address, iterations: selectors, outputName: operand.outputName, consumer } };
|
|
641
|
+
return structuredClone(invocation.outputs[operand.outputName]);
|
|
642
|
+
}
|
|
643
|
+
evaluateCondition(run, condition, scope, consumer) {
|
|
644
|
+
if (typeof condition === 'boolean')
|
|
645
|
+
return condition;
|
|
646
|
+
const value = this.resolveOperand(run, condition.binding, scope, consumer);
|
|
647
|
+
if (condition.exists !== undefined)
|
|
648
|
+
return condition.exists === (value !== undefined);
|
|
649
|
+
if ('equals' in condition)
|
|
650
|
+
return value === condition.equals;
|
|
651
|
+
if ('notEquals' in condition)
|
|
652
|
+
return value !== condition.notEquals;
|
|
653
|
+
return Boolean(value);
|
|
654
|
+
}
|
|
655
|
+
capture(run, step, iterations, inputs, extra) {
|
|
656
|
+
const capture = { structuralAddress: structuredClone(step.structuralAddress), iterations: structuredClone(iterations), inputs: structuredClone(inputs), ...extra };
|
|
657
|
+
freezePlan(capture);
|
|
658
|
+
run.replayCaptures.set(invocationKey(step.structuralAddress, iterations), capture);
|
|
659
|
+
}
|
|
660
|
+
captureExternalEvidence(run, operation, iterations) {
|
|
661
|
+
const evidence = new Map();
|
|
662
|
+
const owns = (address) => address.scope === operation.structuralAddress.scope
|
|
663
|
+
&& operation.structuralAddress.segments.every((segment, index) => JSON.stringify(segment) === JSON.stringify(address.segments[index]));
|
|
664
|
+
const captureOperand = (operand) => {
|
|
665
|
+
if (operand.type !== 'hierarchy-output' || owns(operand.address))
|
|
666
|
+
return;
|
|
667
|
+
// An exact external reference must already be causal at this boundary. No journal scan,
|
|
668
|
+
// legacy ID lookup, or inference of selectors from the current invocation is permitted.
|
|
669
|
+
if (executionOperandDependencyIssues(run.request.executionPackage, operand, { phase: 'completion', address: operation.structuralAddress, iterations }).length)
|
|
670
|
+
return;
|
|
671
|
+
const source = run.plan.get(addressKey(operand.address));
|
|
672
|
+
const selectors = source.loops.map(loop => ({ loopAddress: loop, iteration: operand.iterations.find(i => addressKey(i.loopAddress) === addressKey(loop)).iteration }));
|
|
673
|
+
const key = invocationKey(source.address, selectors);
|
|
674
|
+
const successful = run.journal.get(key);
|
|
675
|
+
if (!successful || !Object.hasOwn(successful.outputs, operand.outputName) || successful.outputs[operand.outputName] === undefined)
|
|
676
|
+
return;
|
|
677
|
+
const snapshot = structuredClone(successful);
|
|
678
|
+
freezePlan(snapshot);
|
|
679
|
+
evidence.set(key, snapshot);
|
|
680
|
+
};
|
|
681
|
+
const condition = (value) => { if (value && typeof value !== 'boolean')
|
|
682
|
+
captureOperand(value.binding); };
|
|
683
|
+
// Shared traversal has already assigned each occurrence exactly one structural identity,
|
|
684
|
+
// including branch children that also appear in a container's presentation children.
|
|
685
|
+
for (const occurrence of run.plan.values()) {
|
|
686
|
+
if (!owns(occurrence.address))
|
|
687
|
+
continue;
|
|
688
|
+
const step = occurrence.step;
|
|
689
|
+
Object.values(step.bindings ?? {}).forEach(captureOperand);
|
|
690
|
+
Object.values(step.outputBindings ?? {}).forEach(captureOperand);
|
|
691
|
+
condition(step.control?.condition);
|
|
692
|
+
for (const branch of step.control?.branches ?? [])
|
|
693
|
+
condition(branch.condition);
|
|
694
|
+
}
|
|
695
|
+
return evidence;
|
|
696
|
+
}
|
|
697
|
+
assertActive(run, signal) {
|
|
698
|
+
if (run.timedOut)
|
|
699
|
+
throw failure('ACTION_TIMEOUT', 'Execution exceeded its timeout.');
|
|
700
|
+
if (run.cancelled || signal.aborted)
|
|
701
|
+
throw failure('ACTION_CANCELLED', 'Execution was cancelled.');
|
|
702
|
+
}
|
|
703
|
+
async executeAction(run, step, inputs, attempts, signal = run.abort.signal, onAttempt) {
|
|
394
704
|
if (run.request.options?.dryRun)
|
|
395
|
-
return { outputs: {}, attempts
|
|
705
|
+
return { outputs: {}, attempts };
|
|
396
706
|
const executor = this.actionExecutors.find(candidate => candidate.supports(step.action));
|
|
397
707
|
if (!executor)
|
|
398
708
|
throw failure('UNSUPPORTED_ACTION', `No Action Executor is registered for ${step.action}.`);
|
|
709
|
+
const originalInputs = structuredClone(inputs);
|
|
710
|
+
freezePlan(originalInputs);
|
|
399
711
|
const maxAttempts = Math.max(1, step.retry?.maxAttempts ?? 1);
|
|
400
|
-
const attempts = [];
|
|
401
712
|
let lastFailure;
|
|
402
713
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
714
|
+
const context = { action: step.action, inputs: structuredClone(originalInputs), environment: run.request.environment, runtimeValues: run.request.runtimeValues ?? {}, step, signal };
|
|
715
|
+
this.assertActive(run, signal);
|
|
403
716
|
const startedAt = new Date().toISOString();
|
|
404
717
|
const startedMs = Date.now();
|
|
718
|
+
onAttempt?.('attempt_started', attempt, 'running');
|
|
405
719
|
try {
|
|
406
|
-
const context = { action: step.action, inputs, environment: run.request.environment, runtimeValues: run.request.runtimeValues ?? {}, step, signal };
|
|
407
720
|
const raw = await executor.execute(context);
|
|
721
|
+
this.assertActive(run, signal);
|
|
408
722
|
const normalized = isActionResult(raw) ? raw : { outputs: raw };
|
|
409
|
-
const
|
|
410
|
-
|
|
723
|
+
const history = { attempt, kind: 'automatic', state: 'finished', startedAt, finishedAt: new Date().toISOString(), inputs: structuredClone(originalInputs), outputs: structuredClone(normalized.outputs), durationMs: Date.now() - startedMs, evidence: structuredClone(normalized.evidence) };
|
|
724
|
+
freezePlan(history);
|
|
725
|
+
attempts.push(history);
|
|
726
|
+
onAttempt?.('attempt_finished', attempt, history.state);
|
|
411
727
|
return { outputs: normalized.outputs, evidence: normalized.evidence, attempts };
|
|
412
728
|
}
|
|
413
729
|
catch (cause) {
|
|
414
730
|
lastFailure = this.failure(cause, run, signal);
|
|
415
|
-
|
|
416
|
-
|
|
731
|
+
const history = { attempt, kind: 'automatic', state: run.timedOut ? 'failed' : run.cancelled || signal.aborted ? 'cancelled' : 'failed', startedAt, finishedAt: new Date().toISOString(), inputs: structuredClone(originalInputs), outputs: {}, durationMs: Date.now() - startedMs, error: structuredClone(lastFailure) };
|
|
732
|
+
freezePlan(history);
|
|
733
|
+
attempts.push(history);
|
|
734
|
+
onAttempt?.('attempt_finished', attempt, history.state);
|
|
735
|
+
this.assertActive(run, signal);
|
|
736
|
+
if (attempt < maxAttempts && step.retry?.delayMs) {
|
|
737
|
+
this.assertActive(run, signal);
|
|
417
738
|
await delay(step.retry.delayMs, signal);
|
|
739
|
+
this.assertActive(run, signal);
|
|
740
|
+
}
|
|
418
741
|
}
|
|
419
742
|
}
|
|
420
743
|
throw lastFailure ?? failure('ACTION_FAILED', `Action ${step.action} failed.`);
|
|
421
744
|
}
|
|
422
|
-
async boundary(run, step, path, signal = run.abort.signal) {
|
|
745
|
+
async boundary(run, step, path, signal = run.abort.signal, iterations = []) {
|
|
746
|
+
if (run.replay) {
|
|
747
|
+
this.assertActive(run, signal);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
423
750
|
const breakpointKey = stepPathForBoundary(step, run, path);
|
|
424
751
|
const breakpoints = run.request.debugSession?.breakpoints ?? [];
|
|
425
752
|
if (breakpoints.includes(step.id) && !run.hitBreakpoints.has(breakpointKey)) {
|
|
426
753
|
run.hitBreakpoints.add(breakpointKey);
|
|
427
754
|
run.pauseRequested = true;
|
|
428
|
-
this.emit(run, 'waiting', 'waiting', `Breakpoint reached before ${step.name}.`, step, { type: 'pause', stepId: step.id });
|
|
755
|
+
this.emit(run, 'waiting', 'waiting', `Breakpoint reached before ${step.name}.`, step, { type: 'pause', stepId: step.id }, iterations);
|
|
429
756
|
}
|
|
430
757
|
if (run.pauseRequested || run.controlState === 'pause-requested')
|
|
431
|
-
await this.pauseAtBoundary(run, step);
|
|
758
|
+
await this.pauseAtBoundary(run, step, iterations);
|
|
432
759
|
if (run.cancelled || signal.aborted)
|
|
433
760
|
throw new Error('Execution cancelled.');
|
|
434
761
|
}
|
|
435
|
-
async pauseAtBoundary(run, step) {
|
|
762
|
+
async pauseAtBoundary(run, step, iterations) {
|
|
436
763
|
run.controlState = 'paused';
|
|
437
764
|
run.pausedStartedMs = Date.now();
|
|
438
|
-
this.emit(run, 'waiting', 'waiting', `Paused before ${step.name}.`, step, { type: 'pause', stepId: step.id });
|
|
765
|
+
this.emit(run, 'waiting', 'waiting', `Paused before ${step.name}.`, step, { type: 'pause', stepId: step.id }, iterations);
|
|
439
766
|
await new Promise(resolve => run.controlWaiters.push(resolve));
|
|
440
767
|
run.pausedDurationMs += Date.now() - (run.pausedStartedMs ?? Date.now());
|
|
441
768
|
run.pausedStartedMs = undefined;
|
|
@@ -445,10 +772,25 @@ export class DefaultExecutor {
|
|
|
445
772
|
run.pauseRequested = true;
|
|
446
773
|
}
|
|
447
774
|
}
|
|
448
|
-
emit(run, type, state, message, step, control) {
|
|
449
|
-
|
|
775
|
+
emit(run, type, state, message, step, control, iterations = []) {
|
|
776
|
+
if (run.replay && step) {
|
|
777
|
+
this.emitReplayOccurrence(run.replay.session, type, state, step, iterations);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
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, structuralAddress: structuredClone(step.structuralAddress), iterations: structuredClone(iterations), kind: step.kind ?? 'action' } : {}), ...(control ? { control } : {}) });
|
|
450
781
|
run.listeners.splice(0).forEach(resolve => resolve());
|
|
451
782
|
}
|
|
783
|
+
emitReplayOccurrence(session, type, state, step, iterations, attempt) {
|
|
784
|
+
const event = {
|
|
785
|
+
parentRequestId: session.parentRequestId, sessionId: session.sessionId,
|
|
786
|
+
sequence: session.events.length + 1, timestamp: new Date().toISOString(),
|
|
787
|
+
structuralAddress: structuredClone(step.structuralAddress), iterations: structuredClone(iterations),
|
|
788
|
+
kind: step.kind ?? 'action', type, state, ...(attempt === undefined ? {} : { attempt }),
|
|
789
|
+
};
|
|
790
|
+
freezePlan(event);
|
|
791
|
+
session.events.push(event);
|
|
792
|
+
session.listeners.splice(0).forEach(resolve => resolve());
|
|
793
|
+
}
|
|
452
794
|
finish(run, result) {
|
|
453
795
|
const finishedAt = new Date().toISOString();
|
|
454
796
|
const finalResult = { ...result, finishedAt, executionTiming: { wallDurationMs: Date.now() - run.startedMs, activeDurationMs: Date.now() - run.startedMs - run.pausedDurationMs, pausedDurationMs: run.pausedDurationMs } };
|
|
@@ -459,15 +801,15 @@ export class DefaultExecutor {
|
|
|
459
801
|
run.listeners.splice(0).forEach(resolve => resolve());
|
|
460
802
|
}
|
|
461
803
|
baseResult(run, startedAt, state, error, issues, children) {
|
|
462
|
-
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 } : {}) };
|
|
804
|
+
return { ...(run.debugProvenance ? { debugProvenance: run.debugProvenance } : {}), 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 } : {}) };
|
|
463
805
|
}
|
|
464
806
|
failure(cause, run, signal = run.abort.signal) {
|
|
465
|
-
if (isFailure(cause))
|
|
466
|
-
return cause;
|
|
467
807
|
if (run.timedOut)
|
|
468
808
|
return failure('ACTION_TIMEOUT', 'Execution exceeded its timeout.');
|
|
469
809
|
if (run.cancelled || signal.aborted)
|
|
470
810
|
return failure('ACTION_CANCELLED', 'Execution was cancelled.');
|
|
811
|
+
if (isFailure(cause))
|
|
812
|
+
return cause;
|
|
471
813
|
return failure('ACTION_FAILED', cause instanceof Error ? cause.message : 'Action execution failed.');
|
|
472
814
|
}
|
|
473
815
|
}
|
|
@@ -487,104 +829,9 @@ function preflight(pkg, inputs, runtimeValues) {
|
|
|
487
829
|
issues.push({ code: 'MISSING_RUNTIME_VALUE', message: `Runtime value '${name}' is missing.` });
|
|
488
830
|
return issues;
|
|
489
831
|
}
|
|
490
|
-
function orderSteps(steps) {
|
|
491
|
-
const byId = new Map();
|
|
492
|
-
for (const step of steps) {
|
|
493
|
-
if (byId.has(step.id))
|
|
494
|
-
throw failure('PREFLIGHT_ERROR', `Graph contains duplicate step id ${step.id}.`);
|
|
495
|
-
byId.set(step.id, step);
|
|
496
|
-
}
|
|
497
|
-
const remaining = [...steps];
|
|
498
|
-
const ordered = [];
|
|
499
|
-
while (remaining.length) {
|
|
500
|
-
const index = remaining.findIndex(step => (step.dependsOn ?? []).every(dependency => byId.has(dependency) && ordered.some(done => done.id === dependency)));
|
|
501
|
-
if (index < 0) {
|
|
502
|
-
const missing = remaining.flatMap(step => step.dependsOn ?? []).find(dependency => !byId.has(dependency));
|
|
503
|
-
throw failure('PREFLIGHT_ERROR', missing ? `Graph dependency ${missing} is missing.` : 'Graph contains a dependency cycle.');
|
|
504
|
-
}
|
|
505
|
-
ordered.push(remaining.splice(index, 1)[0]);
|
|
506
|
-
}
|
|
507
|
-
return ordered;
|
|
508
|
-
}
|
|
509
|
-
function findStep(results, stepId, invocationPath) {
|
|
510
|
-
for (const result of results) {
|
|
511
|
-
if (result.id === stepId && (!invocationPath || result.invocationPath?.join('.') === invocationPath.join('.')))
|
|
512
|
-
return result;
|
|
513
|
-
const nested = result.children && findStep(result.children, stepId, invocationPath);
|
|
514
|
-
if (nested)
|
|
515
|
-
return nested;
|
|
516
|
-
}
|
|
517
|
-
return undefined;
|
|
518
|
-
}
|
|
519
|
-
function findDefinition(steps, stepId, invocationPath) {
|
|
520
|
-
for (const step of steps) {
|
|
521
|
-
if (step.id === stepId && (!invocationPath || invocationPath.at(-1) === stepId))
|
|
522
|
-
return step;
|
|
523
|
-
const nested = step.children && findDefinition(step.children, stepId, invocationPath);
|
|
524
|
-
if (nested)
|
|
525
|
-
return nested;
|
|
526
|
-
for (const branch of step.control?.branches ?? []) {
|
|
527
|
-
const branchNested = findDefinition(branch.children, stepId, invocationPath);
|
|
528
|
-
if (branchNested)
|
|
529
|
-
return branchNested;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
return undefined;
|
|
533
|
-
}
|
|
534
832
|
function invocationState(state) {
|
|
535
833
|
return state === 'cancelled' ? 'cancelled' : state === 'failed' ? 'failed' : 'skipped';
|
|
536
834
|
}
|
|
537
|
-
function mergeBranchOutputs(branches, bindings, policy) {
|
|
538
|
-
if (bindings)
|
|
539
|
-
return Object.fromEntries(Object.entries(bindings).flatMap(([name, source]) => {
|
|
540
|
-
const match = /^([^.]+)\.(.+)$/.exec(source);
|
|
541
|
-
const branch = match ? branches.find(item => item.id === match[1]) : undefined;
|
|
542
|
-
return branch && match && Object.prototype.hasOwnProperty.call(branch.outputs, match[2]) ? [[name, branch.outputs[match[2]]]] : [];
|
|
543
|
-
}));
|
|
544
|
-
if (policy === 'first')
|
|
545
|
-
return { ...(branches[0]?.outputs ?? {}) };
|
|
546
|
-
if (policy === 'last')
|
|
547
|
-
return { ...(branches.at(-1)?.outputs ?? {}) };
|
|
548
|
-
return Object.fromEntries(branches.flatMap(branch => Object.entries(branch.outputs).map(([name, value]) => [`${branch.id}.${name}`, value])));
|
|
549
|
-
}
|
|
550
|
-
function resolveBindings(bindings, scope, outputs) {
|
|
551
|
-
return Object.fromEntries(Object.entries(bindings).map(([name, reference]) => [name, resolveReference(reference, scope, outputs)]));
|
|
552
|
-
}
|
|
553
|
-
function resolveReference(reference, scope, outputs) {
|
|
554
|
-
const input = /^\$inputs\.(.+)$/.exec(reference);
|
|
555
|
-
if (input)
|
|
556
|
-
return readPath(scope, input[1]);
|
|
557
|
-
const step = /^\$steps\.([^.]+)\.(.+)$/.exec(reference);
|
|
558
|
-
if (step)
|
|
559
|
-
return readPath(outputs.get(step[1]) ?? {}, step[2]);
|
|
560
|
-
if (reference.startsWith('$'))
|
|
561
|
-
throw failure('BINDING_RESOLUTION_ERROR', `Binding '${reference}' could not be resolved.`);
|
|
562
|
-
return reference;
|
|
563
|
-
}
|
|
564
|
-
function readPath(value, path) {
|
|
565
|
-
const result = path.split('.').reduce((current, segment) => isRecord(current) ? current[segment] : undefined, value);
|
|
566
|
-
if (result === undefined)
|
|
567
|
-
throw failure('BINDING_RESOLUTION_ERROR', `Binding path '${path}' could not be resolved.`);
|
|
568
|
-
return result;
|
|
569
|
-
}
|
|
570
|
-
function evaluateCondition(condition, scope, outputs) {
|
|
571
|
-
if (typeof condition === 'boolean')
|
|
572
|
-
return condition;
|
|
573
|
-
let value;
|
|
574
|
-
try {
|
|
575
|
-
value = resolveReference(condition.binding, scope, outputs);
|
|
576
|
-
}
|
|
577
|
-
catch {
|
|
578
|
-
value = undefined;
|
|
579
|
-
}
|
|
580
|
-
if (condition.exists !== undefined)
|
|
581
|
-
return condition.exists === (value !== undefined);
|
|
582
|
-
if ('equals' in condition)
|
|
583
|
-
return value === condition.equals;
|
|
584
|
-
if ('notEquals' in condition)
|
|
585
|
-
return value !== condition.notEquals;
|
|
586
|
-
return Boolean(value);
|
|
587
|
-
}
|
|
588
835
|
function delay(durationMs, signal) {
|
|
589
836
|
return new Promise((resolve, reject) => {
|
|
590
837
|
if (signal.aborted) {
|
|
@@ -599,3 +846,7 @@ function failure(code, message) { return { code, message, category: code }; }
|
|
|
599
846
|
function isFailure(value) { return isRecord(value) && typeof value.code === 'string' && typeof value.message === 'string'; }
|
|
600
847
|
function isActionResult(value) { return isRecord(value) && isRecord(value.outputs); }
|
|
601
848
|
function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
|
849
|
+
function ownsAddress(root, address) {
|
|
850
|
+
return root.segments.length <= address.segments.length
|
|
851
|
+
&& addressKey(root) === addressKey({ scope: address.scope, segments: address.segments.slice(0, root.segments.length) });
|
|
852
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ExecutionPackage } from './types';
|
|
1
|
+
import type { ExecutionPackage } from './migrated-types';
|
|
2
2
|
/** Stable, dependency-free content identity for the normalized execution package. */
|
|
3
3
|
export declare function hashExecutionPackage(pack: ExecutionPackage): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionExecutor, Executor, ExecutorConfiguration, ExecutorEvent, ExecutorRequest, ExecutorResult, ExecutorRun, ControlCommand, ExecutionControlState } from './types.js';
|
|
1
|
+
import type { ActReplayTarget, ActReplayHandle, OperationReplayHandle, ActionExecutor, Executor, ExecutorConfiguration, ExecutorEvent, ExecutorRequest, ExecutorResult, ExecutorRun, ControlCommand, ExecutionControlState } from './types.js';
|
|
2
2
|
export declare function createMigratedExecutor(configuration?: ExecutorConfiguration): Executor;
|
|
3
3
|
/**
|
|
4
4
|
* Compatibility facade over the extracted Runner engine. The facade owns the
|
|
@@ -18,7 +18,7 @@ export declare class MigratedExecutor implements Executor {
|
|
|
18
18
|
controlState(requestId: string): ExecutionControlState | undefined;
|
|
19
19
|
events(requestId: string): AsyncIterable<ExecutorEvent>;
|
|
20
20
|
result(requestId: string): Promise<ExecutorResult>;
|
|
21
|
-
retryAction(requestId: string,
|
|
22
|
-
retryOperation(requestId: string,
|
|
23
|
-
runFromHere(requestId: string,
|
|
21
|
+
retryAction(requestId: string, target: ActReplayTarget): Promise<ActReplayHandle>;
|
|
22
|
+
retryOperation(requestId: string, target: ActReplayTarget): Promise<OperationReplayHandle>;
|
|
23
|
+
runFromHere(requestId: string, target: ActReplayTarget): Promise<ExecutorRun>;
|
|
24
24
|
}
|
|
@@ -48,17 +48,22 @@ export class MigratedExecutor {
|
|
|
48
48
|
controlState(requestId) { return this.routineRuns.has(requestId) ? this.routine.controlState(requestId) : this.engine.controlState(requestId); }
|
|
49
49
|
async *events(requestId) { yield* (this.routineRuns.has(requestId) ? this.routine.events(requestId) : this.engine.events(requestId)); }
|
|
50
50
|
async result(requestId) { return (this.routineRuns.has(requestId) ? this.routine.result(requestId) : this.engine.result(requestId)); }
|
|
51
|
-
async retryAction(requestId,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
async retryAction(requestId, target) {
|
|
52
|
+
if (this.routineRuns.has(requestId))
|
|
53
|
+
return this.routine.retryAction(requestId, target);
|
|
54
|
+
throw { code: 'unsupported-execution-context', message: 'Structural Act replay requires the canonical execution path.' };
|
|
55
|
+
}
|
|
56
|
+
async retryOperation(requestId, target) {
|
|
57
|
+
if (this.routineRuns.has(requestId))
|
|
58
|
+
return this.routine.retryOperation(requestId, target);
|
|
59
|
+
throw { code: 'unsupported-execution-context', message: 'Structural Operation replay requires the canonical execution path.' };
|
|
60
|
+
}
|
|
61
|
+
async runFromHere(requestId, target) {
|
|
62
|
+
if (!this.routineRuns.has(requestId))
|
|
63
|
+
throw { code: 'unsupported-execution-context', message: 'Structural Run From Here requires the canonical execution path.' };
|
|
64
|
+
const started = await this.routine.runFromHere(requestId, target);
|
|
65
|
+
this.routineRuns.add(started.requestId);
|
|
66
|
+
return started;
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
function toActionAdapter(executor) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExecutionPackage, ExecutionStep, ExecutionState, InvocationState, TerminalReason } from '@qaick/contract';
|
|
2
|
-
export type { ExecutionPackage, ExecutionStep, ExecutionState, InvocationState, TerminalReason } from '@qaick/contract';
|
|
1
|
+
import type { ExecutionPackageV7 as ExecutionPackage, ExecutionStepV7 as ExecutionStep, ExecutionState, InvocationState, TerminalReason } from '@qaick/contract';
|
|
2
|
+
export type { ExecutionPackageV7 as ExecutionPackage, ExecutionStepV7 as ExecutionStep, ExecutionState, InvocationState, TerminalReason } from '@qaick/contract';
|
|
3
3
|
export type ActionFailureCode = 'ACTION_FAILED' | 'ACTION_TIMEOUT' | 'ACTION_CANCELLED' | 'UNSUPPORTED_ACTION' | 'BINDING_RESOLUTION_ERROR' | 'CONTROL_BLOCK_ERROR' | 'PREFLIGHT_ERROR' | 'INTERNAL_EXECUTION_ERROR';
|
|
4
4
|
export interface ActionFailure {
|
|
5
5
|
code: string;
|
|
@@ -12,7 +12,7 @@ export interface ActionExecutionContext {
|
|
|
12
12
|
inputs: Record<string, unknown>;
|
|
13
13
|
environment: string;
|
|
14
14
|
runtimeValues: Record<string, unknown>;
|
|
15
|
-
step: ExecutionStep
|
|
15
|
+
step: Pick<ExecutionStep, 'id' | 'name' | 'action' | 'output' | 'adapterConfig'>;
|
|
16
16
|
signal: AbortSignal;
|
|
17
17
|
}
|
|
18
18
|
export interface ActionExecutionResult {
|
|
@@ -63,6 +63,8 @@ export interface ExecutorStepResult {
|
|
|
63
63
|
inputs: Record<string, unknown>;
|
|
64
64
|
outputs: Record<string, unknown>;
|
|
65
65
|
durationMs: number;
|
|
66
|
+
structuralAddress?: import('@qaick/contract').HierarchyAddress;
|
|
67
|
+
iterations?: import('@qaick/contract').IterationSelection[];
|
|
66
68
|
invocationId?: string;
|
|
67
69
|
invocationPath?: string[];
|
|
68
70
|
kind?: 'action' | 'operation' | 'control-block';
|
|
@@ -78,6 +80,7 @@ export interface ExecutorStepResult {
|
|
|
78
80
|
error?: ActionFailure;
|
|
79
81
|
}
|
|
80
82
|
export interface ExecutorResult {
|
|
83
|
+
readonly debugProvenance?: RunFromHereProvenance;
|
|
81
84
|
requestId: string;
|
|
82
85
|
state: Exclude<ExecutionState, 'created' | 'preflighting' | 'running'>;
|
|
83
86
|
target: ExecutionPackage['target'];
|
|
@@ -100,6 +103,9 @@ export interface ExecutorResult {
|
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
export interface ExecutorEvent {
|
|
106
|
+
structuralAddress?: import('@qaick/contract').HierarchyAddress;
|
|
107
|
+
iterations?: import('@qaick/contract').IterationSelection[];
|
|
108
|
+
kind?: 'action' | 'operation' | 'control-block';
|
|
103
109
|
requestId: string;
|
|
104
110
|
sequence: number;
|
|
105
111
|
type: 'run_started' | 'preflight_started' | 'step_started' | 'waiting' | 'step_finished' | 'run_finished';
|
|
@@ -138,9 +144,79 @@ export type ControlCommand = {
|
|
|
138
144
|
};
|
|
139
145
|
export type ExecutionControlState = 'idle' | 'running' | 'pause-requested' | 'paused' | 'resuming' | 'stopped' | 'failed' | 'completed';
|
|
140
146
|
export interface ExecutorRun {
|
|
147
|
+
readonly debugProvenance?: RunFromHereProvenance;
|
|
141
148
|
requestId: string;
|
|
142
149
|
result: Promise<ExecutorResult>;
|
|
143
150
|
}
|
|
151
|
+
/** Runtime diagnostic target; never serialized into an Execution v7 package. */
|
|
152
|
+
export interface ActReplayTarget {
|
|
153
|
+
structuralAddress: import('@qaick/contract').HierarchyAddress;
|
|
154
|
+
iterations: import('@qaick/contract').IterationSelection[];
|
|
155
|
+
}
|
|
156
|
+
/** Descriptive runtime provenance, not an execution operand or persisted package field. */
|
|
157
|
+
export interface RunFromHereProvenance {
|
|
158
|
+
readonly kind: 'run-from-here';
|
|
159
|
+
readonly sourceRequestId: string;
|
|
160
|
+
readonly selection: Readonly<ActReplayTarget>;
|
|
161
|
+
readonly inputSeed: {
|
|
162
|
+
readonly kind: 'captured-inputs';
|
|
163
|
+
readonly inputs: Readonly<Record<string, unknown>>;
|
|
164
|
+
readonly lexicalScope?: Readonly<Record<string, unknown>>;
|
|
165
|
+
};
|
|
166
|
+
readonly omittedOccurrences: readonly import('@qaick/contract').HierarchyAddress[];
|
|
167
|
+
}
|
|
168
|
+
export interface ActReplayResult {
|
|
169
|
+
readonly parentRequestId: string;
|
|
170
|
+
readonly sessionId: string;
|
|
171
|
+
readonly target: Readonly<ActReplayTarget>;
|
|
172
|
+
readonly kind: 'action';
|
|
173
|
+
readonly state: 'completed' | 'failed' | 'cancelled';
|
|
174
|
+
readonly terminalReason: 'completed' | 'failed' | 'cancelled' | 'timeout';
|
|
175
|
+
readonly startedAt: string;
|
|
176
|
+
readonly finishedAt: string;
|
|
177
|
+
readonly durationMs: number;
|
|
178
|
+
readonly inputs: Readonly<Record<string, unknown>>;
|
|
179
|
+
readonly outputs: Readonly<Record<string, unknown>>;
|
|
180
|
+
readonly evidence?: Readonly<Record<string, unknown>>;
|
|
181
|
+
readonly attempts: readonly ExecutionAttempt[];
|
|
182
|
+
readonly error?: Readonly<ActionFailure>;
|
|
183
|
+
}
|
|
184
|
+
export interface ActReplayEvent {
|
|
185
|
+
readonly parentRequestId: string;
|
|
186
|
+
readonly sessionId: string;
|
|
187
|
+
readonly sequence: number;
|
|
188
|
+
readonly timestamp: string;
|
|
189
|
+
readonly structuralAddress: import('@qaick/contract').HierarchyAddress;
|
|
190
|
+
readonly iterations: import('@qaick/contract').IterationSelection[];
|
|
191
|
+
readonly kind: 'action';
|
|
192
|
+
readonly type: 'replay_started' | 'attempt_started' | 'attempt_finished' | 'replay_finished';
|
|
193
|
+
readonly state: 'running' | 'finished' | 'completed' | 'failed' | 'cancelled';
|
|
194
|
+
readonly attempt?: number;
|
|
195
|
+
readonly terminalReason?: ActReplayResult['terminalReason'];
|
|
196
|
+
}
|
|
197
|
+
/** A replay is associated with a historical Run; this is not a new ExecutorRun. */
|
|
198
|
+
export interface ActReplayHandle {
|
|
199
|
+
readonly parentRequestId: string;
|
|
200
|
+
readonly sessionId: string;
|
|
201
|
+
readonly target: Readonly<ActReplayTarget>;
|
|
202
|
+
readonly result: Promise<ActReplayResult>;
|
|
203
|
+
events(): AsyncIterable<ActReplayEvent>;
|
|
204
|
+
cancel(): Promise<void>;
|
|
205
|
+
}
|
|
206
|
+
export interface OperationReplayResult extends Omit<ActReplayResult, 'kind' | 'attempts' | 'evidence'> {
|
|
207
|
+
readonly kind: 'operation';
|
|
208
|
+
readonly lexicalScope: Readonly<Record<string, unknown>>;
|
|
209
|
+
readonly children: readonly ExecutorStepResult[];
|
|
210
|
+
}
|
|
211
|
+
export interface OperationReplayEvent extends Omit<ActReplayEvent, 'kind' | 'type' | 'state'> {
|
|
212
|
+
readonly kind: 'action' | 'operation' | 'control-block';
|
|
213
|
+
readonly type: ActReplayEvent['type'] | 'step_started' | 'step_finished' | 'waiting';
|
|
214
|
+
readonly state: ActReplayEvent['state'] | 'waiting' | 'skipped';
|
|
215
|
+
}
|
|
216
|
+
export interface OperationReplayHandle extends Omit<ActReplayHandle, 'result' | 'events'> {
|
|
217
|
+
readonly result: Promise<OperationReplayResult>;
|
|
218
|
+
events(): AsyncIterable<OperationReplayEvent>;
|
|
219
|
+
}
|
|
144
220
|
export interface ExecutorConfiguration {
|
|
145
221
|
actionExecutors?: ActionExecutor[];
|
|
146
222
|
maxTimeoutMs?: number;
|
|
@@ -152,8 +228,8 @@ export interface Executor {
|
|
|
152
228
|
controlState(requestId: string): ExecutionControlState | undefined;
|
|
153
229
|
events(requestId: string): AsyncIterable<ExecutorEvent>;
|
|
154
230
|
result(requestId: string): Promise<ExecutorResult>;
|
|
155
|
-
retryAction(requestId: string,
|
|
156
|
-
retryOperation(requestId: string,
|
|
157
|
-
runFromHere(requestId: string,
|
|
231
|
+
retryAction(requestId: string, target: ActReplayTarget): Promise<ActReplayHandle>;
|
|
232
|
+
retryOperation(requestId: string, target: ActReplayTarget): Promise<OperationReplayHandle>;
|
|
233
|
+
runFromHere(requestId: string, target: ActReplayTarget): Promise<ExecutorRun>;
|
|
158
234
|
registerActionExecutor(executor: ActionExecutor): void;
|
|
159
235
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qaick/executor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Host-neutral QAick action execution primitives.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@qaick/contract": "0.
|
|
37
|
+
"@qaick/contract": "0.3.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^24.13.3",
|