@rk0429/agentic-relay 19.14.4 → 20.0.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/application/workflow-execution-service.d.ts +8 -1
- package/dist/application/workflow-execution-service.js +331 -166
- package/dist/application/workflow-execution-service.js.map +1 -1
- package/dist/bin/relay.js +19 -16
- package/dist/bin/relay.js.map +1 -1
- package/dist/domain/loop-execution.d.ts +7 -5
- package/dist/domain/loop-execution.js +61 -24
- package/dist/domain/loop-execution.js.map +1 -1
- package/dist/domain/workflow-loader.js +6 -8
- package/dist/domain/workflow-loader.js.map +1 -1
- package/dist/domain/workflow-schema.d.ts +21 -9
- package/dist/domain/workflow-schema.js +42 -24
- package/dist/domain/workflow-schema.js.map +1 -1
- package/dist/infrastructure/store/loop-state-repository.js +10 -10
- package/dist/infrastructure/store/loop-state-repository.js.map +1 -1
- package/dist/interfaces/mcp/relay-mcp-server.js +1 -1
- package/dist/interfaces/mcp/relay-mcp-server.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ export interface WorkflowExecutionDeps {
|
|
|
12
12
|
executeAgentStep: AgentStepExecutor;
|
|
13
13
|
executeCommand: CommandExecutor;
|
|
14
14
|
workflowRepo: WorkflowFileRepository;
|
|
15
|
+
readFile: (path: string, encoding: BufferEncoding) => Promise<string>;
|
|
15
16
|
}
|
|
16
17
|
export interface AgentStepExecutor {
|
|
17
18
|
execute(opts: {
|
|
@@ -116,11 +117,15 @@ export declare class WorkflowExecutionService {
|
|
|
116
117
|
private finalizeRootResult;
|
|
117
118
|
private persistInterruptState;
|
|
118
119
|
private runWorkflowFrame;
|
|
120
|
+
private handlePendingExitCondition;
|
|
121
|
+
private beginNewIterationIfNeeded;
|
|
122
|
+
private executeIterationSteps;
|
|
119
123
|
private resumeFrames;
|
|
120
124
|
private resumeParentFrames;
|
|
121
125
|
private executeStep;
|
|
122
126
|
private runSubWorkflow;
|
|
123
127
|
private evaluateExitCondition;
|
|
128
|
+
private evaluateExitConditionsAt;
|
|
124
129
|
private handleStepFailure;
|
|
125
130
|
private logNewlySkippedSteps;
|
|
126
131
|
private reportProgress;
|
|
@@ -134,9 +139,11 @@ export declare class WorkflowExecutionService {
|
|
|
134
139
|
private buildExitConditionCheckpoint;
|
|
135
140
|
private rebaseInterruptState;
|
|
136
141
|
private getNextStepIdAfter;
|
|
142
|
+
private getContinuationStepId;
|
|
137
143
|
private getStepIndex;
|
|
138
144
|
private getStepDefinition;
|
|
139
|
-
private
|
|
145
|
+
private resolveExitConditionTargetStepId;
|
|
146
|
+
private findCompletedStepResult;
|
|
140
147
|
private findTerminalCompletedResult;
|
|
141
148
|
private toPublicResult;
|
|
142
149
|
private assertFrameMatchesWorkflow;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { access
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
2
|
import { dirname, basename, resolve } from "node:path";
|
|
3
3
|
import { ValidationError } from "../core/errors.js";
|
|
4
4
|
import { ExitConditionEvaluator, } from "../domain/exit-condition-evaluator.js";
|
|
5
5
|
import { LoopExecution, NestDepth, } from "../domain/loop-execution.js";
|
|
6
6
|
import { SessionResolver } from "../domain/session-resolver.js";
|
|
7
|
+
import { isWorkflowStep } from "../domain/workflow-schema.js";
|
|
7
8
|
import { computeWorkflowDigest, loadWorkflow, } from "../domain/workflow-loader.js";
|
|
8
9
|
const DEFAULT_INTERRUPT_GRACE_MS = 30_000;
|
|
9
10
|
const EXIT_VERDICT_INSTRUCTION = "Respond with STOP or CONTINUE on the first non-empty line. You may explain your reasoning after that first line.";
|
|
@@ -129,159 +130,258 @@ export class WorkflowExecutionService {
|
|
|
129
130
|
let continuationStepId;
|
|
130
131
|
while (execution.status === "running") {
|
|
131
132
|
if (execution.pendingExitCondition) {
|
|
132
|
-
const
|
|
133
|
-
const outcome = await this.evaluateExitCondition({
|
|
133
|
+
const pendingResult = await this.handlePendingExitCondition({
|
|
134
134
|
execution,
|
|
135
135
|
workflow,
|
|
136
|
+
workflowPath,
|
|
137
|
+
workflowDigest,
|
|
136
138
|
cwd,
|
|
137
139
|
signal,
|
|
138
140
|
interruptTracker,
|
|
139
|
-
checkpoint,
|
|
140
141
|
});
|
|
141
|
-
if (
|
|
142
|
-
return
|
|
142
|
+
if (pendingResult.kind === "interrupted") {
|
|
143
|
+
return pendingResult.result;
|
|
144
|
+
}
|
|
145
|
+
if (pendingResult.kind === "stop") {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
continuationStepId = pendingResult.continuationStepId;
|
|
149
|
+
}
|
|
150
|
+
const iteration = await this.beginNewIterationIfNeeded({
|
|
151
|
+
execution,
|
|
152
|
+
workflow,
|
|
153
|
+
continuationStepId,
|
|
154
|
+
});
|
|
155
|
+
continuationStepId = undefined;
|
|
156
|
+
if (iteration.action === "completed") {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
if (!iteration.nextStepId) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const stepExecution = await this.executeIterationSteps({
|
|
163
|
+
execution,
|
|
164
|
+
workflow,
|
|
165
|
+
workflowPath,
|
|
166
|
+
workflowDigest,
|
|
167
|
+
cwd,
|
|
168
|
+
signal,
|
|
169
|
+
interruptTracker,
|
|
170
|
+
startIndex: this.getStepIndex(workflow, iteration.nextStepId),
|
|
171
|
+
vars: opts.vars,
|
|
172
|
+
});
|
|
173
|
+
if (stepExecution.kind === "interrupted") {
|
|
174
|
+
return stepExecution.result;
|
|
175
|
+
}
|
|
176
|
+
if (stepExecution.exitConditionMet) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (execution.nextStepId === null &&
|
|
180
|
+
execution.currentIteration >= workflow.loop.maxIterations) {
|
|
181
|
+
execution.complete("max_iterations_reached", this.now().toISOString());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (execution.status === "completed") {
|
|
185
|
+
await this.deps.eventLog.logLoopCompleted(execution.executionId, execution.completionReason, execution.currentIteration, this.computeDuration(execution.startedAt, execution.completedAt));
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
status: "completed",
|
|
189
|
+
execution,
|
|
190
|
+
workflow,
|
|
191
|
+
workflowPath,
|
|
192
|
+
workflowDigest,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async handlePendingExitCondition(opts) {
|
|
196
|
+
const { execution, workflow, workflowPath, workflowDigest, cwd, signal, interruptTracker, } = opts;
|
|
197
|
+
const checkpoint = execution.pendingExitCondition;
|
|
198
|
+
const evaluation = await this.evaluateExitConditionsAt({
|
|
199
|
+
execution,
|
|
200
|
+
workflow,
|
|
201
|
+
cwd,
|
|
202
|
+
signal,
|
|
203
|
+
interruptTracker,
|
|
204
|
+
timing: checkpoint.timing,
|
|
205
|
+
stepId: checkpoint.stepId,
|
|
206
|
+
startIndex: checkpoint.conditionIndex,
|
|
207
|
+
});
|
|
208
|
+
if (evaluation.kind === "interrupted") {
|
|
209
|
+
return {
|
|
210
|
+
kind: "interrupted",
|
|
211
|
+
result: {
|
|
212
|
+
status: "interrupted",
|
|
213
|
+
execution,
|
|
214
|
+
workflow,
|
|
215
|
+
workflowPath,
|
|
216
|
+
workflowDigest,
|
|
217
|
+
interruptState: evaluation.interruptState,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (evaluation.kind === "stop") {
|
|
222
|
+
return { kind: "stop" };
|
|
223
|
+
}
|
|
224
|
+
const continuationStepId = this.getContinuationStepId(workflow, checkpoint.timing, checkpoint.stepId);
|
|
225
|
+
if (interruptTracker.isInterruptRequested()) {
|
|
226
|
+
return {
|
|
227
|
+
kind: "interrupted",
|
|
228
|
+
result: {
|
|
229
|
+
status: "interrupted",
|
|
230
|
+
execution,
|
|
231
|
+
workflow,
|
|
232
|
+
workflowPath,
|
|
233
|
+
workflowDigest,
|
|
234
|
+
interruptState: execution.interrupt({
|
|
235
|
+
nextStepId: continuationStepId ?? undefined,
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
kind: "continue",
|
|
242
|
+
continuationStepId,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async beginNewIterationIfNeeded(opts) {
|
|
246
|
+
const { execution, workflow } = opts;
|
|
247
|
+
const shouldBeginIteration = execution.currentIteration === 0 ||
|
|
248
|
+
opts.continuationStepId === null ||
|
|
249
|
+
(opts.continuationStepId === undefined && execution.nextStepId === null);
|
|
250
|
+
let continuationStepId = opts.continuationStepId;
|
|
251
|
+
if (shouldBeginIteration) {
|
|
252
|
+
if (execution.currentIteration >= workflow.loop.maxIterations) {
|
|
253
|
+
execution.complete("max_iterations_reached", this.now().toISOString());
|
|
254
|
+
return { action: "completed" };
|
|
255
|
+
}
|
|
256
|
+
execution.beginIteration();
|
|
257
|
+
continuationStepId = undefined;
|
|
258
|
+
await this.deps.eventLog.logIterationStarted(execution.executionId, execution.currentIteration);
|
|
259
|
+
await this.reportProgress({
|
|
260
|
+
snapshot: execution.toProgressSnapshot(),
|
|
261
|
+
event: { type: "iteration_started" },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const nextStepId = continuationStepId ?? execution.nextStepId ?? workflow.steps[0]?.id ?? null;
|
|
265
|
+
if (!nextStepId && execution.currentIteration >= workflow.loop.maxIterations) {
|
|
266
|
+
execution.complete("max_iterations_reached", this.now().toISOString());
|
|
267
|
+
return { action: "completed" };
|
|
268
|
+
}
|
|
269
|
+
return { action: "continue", nextStepId };
|
|
270
|
+
}
|
|
271
|
+
async executeIterationSteps(opts) {
|
|
272
|
+
const { execution, workflow, workflowPath, workflowDigest, cwd, signal, interruptTracker, startIndex, vars, } = opts;
|
|
273
|
+
let exitConditionMet = false;
|
|
274
|
+
for (const step of workflow.steps.slice(startIndex)) {
|
|
275
|
+
const beforeEvaluation = await this.evaluateExitConditionsAt({
|
|
276
|
+
execution,
|
|
277
|
+
workflow,
|
|
278
|
+
cwd,
|
|
279
|
+
signal,
|
|
280
|
+
interruptTracker,
|
|
281
|
+
timing: "before",
|
|
282
|
+
stepId: step.id,
|
|
283
|
+
});
|
|
284
|
+
if (beforeEvaluation.kind === "interrupted") {
|
|
285
|
+
return {
|
|
286
|
+
kind: "interrupted",
|
|
287
|
+
result: {
|
|
143
288
|
status: "interrupted",
|
|
144
289
|
execution,
|
|
145
290
|
workflow,
|
|
146
291
|
workflowPath,
|
|
147
292
|
workflowDigest,
|
|
148
|
-
interruptState:
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
checkpoint.kind === "after_step"
|
|
161
|
-
? this.getNextStepIdAfter(workflow, checkpoint.evaluateAfterStepId)
|
|
162
|
-
: null;
|
|
163
|
-
if (interruptTracker.isInterruptRequested()) {
|
|
164
|
-
return {
|
|
293
|
+
interruptState: beforeEvaluation.interruptState,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (beforeEvaluation.kind === "stop") {
|
|
298
|
+
exitConditionMet = true;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
if (interruptTracker.isInterruptRequested()) {
|
|
302
|
+
return {
|
|
303
|
+
kind: "interrupted",
|
|
304
|
+
result: {
|
|
165
305
|
status: "interrupted",
|
|
166
306
|
execution,
|
|
167
307
|
workflow,
|
|
168
308
|
workflowPath,
|
|
169
309
|
workflowDigest,
|
|
170
310
|
interruptState: execution.interrupt({
|
|
171
|
-
nextStepId:
|
|
311
|
+
nextStepId: step.id,
|
|
172
312
|
}),
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const shouldBeginIteration = execution.currentIteration === 0 ||
|
|
177
|
-
continuationStepId === null ||
|
|
178
|
-
(continuationStepId === undefined && execution.nextStepId === null);
|
|
179
|
-
if (shouldBeginIteration) {
|
|
180
|
-
if (execution.currentIteration >= workflow.loop.maxIterations) {
|
|
181
|
-
execution.complete("max_iterations_reached", this.now().toISOString());
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
execution.beginIteration();
|
|
185
|
-
continuationStepId = undefined;
|
|
186
|
-
await this.deps.eventLog.logIterationStarted(execution.executionId, execution.currentIteration);
|
|
187
|
-
await this.reportProgress({
|
|
188
|
-
snapshot: execution.toProgressSnapshot(),
|
|
189
|
-
event: { type: "iteration_started" },
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
const nextStepId = continuationStepId ?? execution.nextStepId ?? workflow.steps[0]?.id ?? null;
|
|
193
|
-
continuationStepId = undefined;
|
|
194
|
-
if (!nextStepId) {
|
|
195
|
-
if (execution.currentIteration >= workflow.loop.maxIterations) {
|
|
196
|
-
execution.complete("max_iterations_reached", this.now().toISOString());
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
continue;
|
|
313
|
+
},
|
|
314
|
+
};
|
|
200
315
|
}
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return {
|
|
316
|
+
const outcome = await this.executeStep({
|
|
317
|
+
step,
|
|
318
|
+
execution,
|
|
319
|
+
workflow,
|
|
320
|
+
workflowPath,
|
|
321
|
+
cwd,
|
|
322
|
+
signal,
|
|
323
|
+
vars,
|
|
324
|
+
interruptTracker,
|
|
325
|
+
});
|
|
326
|
+
if (outcome.kind === "interrupted") {
|
|
327
|
+
return {
|
|
328
|
+
kind: "interrupted",
|
|
329
|
+
result: {
|
|
216
330
|
status: "interrupted",
|
|
217
331
|
execution,
|
|
218
332
|
workflow,
|
|
219
333
|
workflowPath,
|
|
220
334
|
workflowDigest,
|
|
221
335
|
interruptState: outcome.interruptState,
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
workflow,
|
|
242
|
-
workflowPath,
|
|
243
|
-
workflowDigest,
|
|
244
|
-
interruptState: evaluation.interruptState,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
const before = execution.stepResults;
|
|
248
|
-
execution.applyExitVerdict(evaluation.verdict, {
|
|
249
|
-
evaluatedAfter: checkpoint.evaluateAfterStepId,
|
|
250
|
-
});
|
|
251
|
-
await this.logNewlySkippedSteps(execution, before);
|
|
252
|
-
if (evaluation.verdict === "stop") {
|
|
253
|
-
completedViaExitCondition = true;
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (interruptTracker.isInterruptRequested()) {
|
|
258
|
-
return {
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (execution.status !== "running") {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
const afterEvaluation = await this.evaluateExitConditionsAt({
|
|
343
|
+
execution,
|
|
344
|
+
workflow,
|
|
345
|
+
cwd,
|
|
346
|
+
signal,
|
|
347
|
+
interruptTracker,
|
|
348
|
+
timing: "after",
|
|
349
|
+
stepId: step.id,
|
|
350
|
+
});
|
|
351
|
+
if (afterEvaluation.kind === "interrupted") {
|
|
352
|
+
return {
|
|
353
|
+
kind: "interrupted",
|
|
354
|
+
result: {
|
|
259
355
|
status: "interrupted",
|
|
260
356
|
execution,
|
|
261
357
|
workflow,
|
|
262
358
|
workflowPath,
|
|
263
359
|
workflowDigest,
|
|
264
|
-
interruptState:
|
|
265
|
-
}
|
|
266
|
-
}
|
|
360
|
+
interruptState: afterEvaluation.interruptState,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
267
363
|
}
|
|
268
|
-
if (
|
|
364
|
+
if (afterEvaluation.kind === "stop") {
|
|
365
|
+
exitConditionMet = true;
|
|
269
366
|
break;
|
|
270
367
|
}
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
368
|
+
if (interruptTracker.isInterruptRequested()) {
|
|
369
|
+
return {
|
|
370
|
+
kind: "interrupted",
|
|
371
|
+
result: {
|
|
372
|
+
status: "interrupted",
|
|
373
|
+
execution,
|
|
374
|
+
workflow,
|
|
375
|
+
workflowPath,
|
|
376
|
+
workflowDigest,
|
|
377
|
+
interruptState: execution.interrupt(),
|
|
378
|
+
},
|
|
379
|
+
};
|
|
274
380
|
}
|
|
275
381
|
}
|
|
276
|
-
if (execution.status === "completed") {
|
|
277
|
-
await this.deps.eventLog.logLoopCompleted(execution.executionId, execution.completionReason, execution.currentIteration, this.computeDuration(execution.startedAt, execution.completedAt));
|
|
278
|
-
}
|
|
279
382
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
workflow,
|
|
283
|
-
workflowPath,
|
|
284
|
-
workflowDigest,
|
|
383
|
+
kind: "completed",
|
|
384
|
+
exitConditionMet,
|
|
285
385
|
};
|
|
286
386
|
}
|
|
287
387
|
async resumeFrames(opts) {
|
|
@@ -533,30 +633,27 @@ export class WorkflowExecutionService {
|
|
|
533
633
|
};
|
|
534
634
|
}
|
|
535
635
|
async evaluateExitCondition(opts) {
|
|
536
|
-
const { execution, workflow, cwd, signal, interruptTracker, checkpoint } = opts;
|
|
537
|
-
const
|
|
538
|
-
if (!exitCondition) {
|
|
539
|
-
return {
|
|
540
|
-
kind: "continue",
|
|
541
|
-
verdict: "continue",
|
|
542
|
-
checkpoint,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
const evaluationSignal = interruptTracker.beginOperation(`exit:${execution.currentIteration}`, signal);
|
|
636
|
+
const { execution, workflow, condition, cwd, signal, interruptTracker, checkpoint } = opts;
|
|
637
|
+
const evaluationSignal = interruptTracker.beginOperation(`exit:${execution.currentIteration}:${checkpoint.timing}:${checkpoint.stepId}:${checkpoint.conditionIndex}`, signal);
|
|
546
638
|
try {
|
|
547
|
-
if (
|
|
548
|
-
const targetStepId =
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
639
|
+
if (condition.type === "agent") {
|
|
640
|
+
const targetStepId = this.resolveExitConditionTargetStepId(workflow, checkpoint);
|
|
641
|
+
const targetResult = this.findCompletedStepResult(execution, targetStepId);
|
|
642
|
+
if (!targetResult) {
|
|
643
|
+
return {
|
|
644
|
+
kind: "continue",
|
|
645
|
+
verdict: "continue",
|
|
646
|
+
checkpoint,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
552
649
|
if (!targetResult.fullResponsePath) {
|
|
553
|
-
throw new ValidationError(`Step "${
|
|
650
|
+
throw new ValidationError(`Step "${targetResult.stepId}" is missing fullResponsePath required for exit condition evaluation.`);
|
|
554
651
|
}
|
|
555
|
-
const responseContent = await readFile(targetResult.fullResponsePath, "utf8");
|
|
556
|
-
const prompt = this.buildExitConditionPrompt(
|
|
652
|
+
const responseContent = await this.deps.readFile(targetResult.fullResponsePath, "utf8");
|
|
653
|
+
const prompt = this.buildExitConditionPrompt(condition.agent.prompt, targetResult.fullResponsePath, responseContent);
|
|
557
654
|
const result = await this.deps.executeAgentStep.execute({
|
|
558
655
|
prompt,
|
|
559
|
-
backend:
|
|
656
|
+
backend: condition.agent.backend,
|
|
560
657
|
cwd,
|
|
561
658
|
signal: evaluationSignal,
|
|
562
659
|
});
|
|
@@ -573,11 +670,11 @@ export class WorkflowExecutionService {
|
|
|
573
670
|
parsedVerdict,
|
|
574
671
|
};
|
|
575
672
|
}
|
|
576
|
-
const commandResult = await this.deps.executeCommand.execute(
|
|
673
|
+
const commandResult = await this.deps.executeCommand.execute(condition.command.run, {
|
|
577
674
|
cwd,
|
|
578
675
|
signal: evaluationSignal,
|
|
579
676
|
});
|
|
580
|
-
const verdict = ExitConditionEvaluator.evaluateExitCode(commandResult.exitCode,
|
|
677
|
+
const verdict = ExitConditionEvaluator.evaluateExitCode(commandResult.exitCode, condition.command.successMeans);
|
|
581
678
|
await this.deps.eventLog.logExitConditionEvaluated(execution.executionId, verdict, execution.currentIteration);
|
|
582
679
|
return {
|
|
583
680
|
kind: "continue",
|
|
@@ -592,7 +689,8 @@ export class WorkflowExecutionService {
|
|
|
592
689
|
checkpoint,
|
|
593
690
|
interruptState: execution.interrupt({
|
|
594
691
|
pendingExitCondition: checkpoint,
|
|
595
|
-
nextStepId:
|
|
692
|
+
nextStepId: this.getContinuationStepId(workflow, checkpoint.timing, checkpoint.stepId) ??
|
|
693
|
+
undefined,
|
|
596
694
|
}),
|
|
597
695
|
};
|
|
598
696
|
}
|
|
@@ -610,7 +708,8 @@ export class WorkflowExecutionService {
|
|
|
610
708
|
checkpoint,
|
|
611
709
|
interruptState: execution.interrupt({
|
|
612
710
|
pendingExitCondition: checkpoint,
|
|
613
|
-
nextStepId:
|
|
711
|
+
nextStepId: this.getContinuationStepId(workflow, checkpoint.timing, checkpoint.stepId) ??
|
|
712
|
+
undefined,
|
|
614
713
|
}),
|
|
615
714
|
};
|
|
616
715
|
}
|
|
@@ -618,6 +717,59 @@ export class WorkflowExecutionService {
|
|
|
618
717
|
interruptTracker.endOperation();
|
|
619
718
|
}
|
|
620
719
|
}
|
|
720
|
+
async evaluateExitConditionsAt(opts) {
|
|
721
|
+
const { execution, workflow, cwd, signal, interruptTracker, timing, stepId, startIndex = 0, } = opts;
|
|
722
|
+
const conditions = execution.getConditionsAt(timing, stepId);
|
|
723
|
+
if (conditions.length === 0) {
|
|
724
|
+
if (execution.pendingExitCondition) {
|
|
725
|
+
execution.applyExitVerdict("continue");
|
|
726
|
+
}
|
|
727
|
+
return { kind: "continue" };
|
|
728
|
+
}
|
|
729
|
+
for (let index = startIndex; index < conditions.length; index += 1) {
|
|
730
|
+
const checkpoint = this.buildExitConditionCheckpoint(timing, stepId, index);
|
|
731
|
+
const outcome = await this.evaluateExitCondition({
|
|
732
|
+
execution,
|
|
733
|
+
workflow,
|
|
734
|
+
condition: conditions[index],
|
|
735
|
+
cwd,
|
|
736
|
+
signal,
|
|
737
|
+
interruptTracker,
|
|
738
|
+
checkpoint,
|
|
739
|
+
});
|
|
740
|
+
if (outcome.kind === "interrupted") {
|
|
741
|
+
return {
|
|
742
|
+
kind: "interrupted",
|
|
743
|
+
checkpoint,
|
|
744
|
+
interruptState: outcome.interruptState,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
if (outcome.verdict === "stop") {
|
|
748
|
+
const before = execution.stepResults;
|
|
749
|
+
execution.applyExitVerdict("stop", { checkpoint });
|
|
750
|
+
await this.logNewlySkippedSteps(execution, before);
|
|
751
|
+
return {
|
|
752
|
+
kind: "stop",
|
|
753
|
+
checkpoint,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (interruptTracker.isInterruptRequested()) {
|
|
757
|
+
const nextIndex = index + 1;
|
|
758
|
+
if (nextIndex < conditions.length) {
|
|
759
|
+
return {
|
|
760
|
+
kind: "interrupted",
|
|
761
|
+
checkpoint,
|
|
762
|
+
interruptState: execution.interrupt({
|
|
763
|
+
pendingExitCondition: this.buildExitConditionCheckpoint(timing, stepId, nextIndex),
|
|
764
|
+
nextStepId: this.getContinuationStepId(workflow, timing, stepId) ?? undefined,
|
|
765
|
+
}),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
execution.applyExitVerdict("continue");
|
|
771
|
+
return { kind: "continue" };
|
|
772
|
+
}
|
|
621
773
|
async handleStepFailure(opts) {
|
|
622
774
|
const { execution, workflow, step, message, duration } = opts;
|
|
623
775
|
await this.deps.eventLog.logStepFailed(execution.executionId, step.id, execution.currentIteration, message, duration);
|
|
@@ -763,21 +915,18 @@ ${responseContent}`;
|
|
|
763
915
|
}
|
|
764
916
|
async readVerdictOutput(fullResponsePath, summary) {
|
|
765
917
|
try {
|
|
766
|
-
return await readFile(fullResponsePath, "utf8");
|
|
918
|
+
return await this.deps.readFile(fullResponsePath, "utf8");
|
|
767
919
|
}
|
|
768
920
|
catch {
|
|
769
921
|
return summary;
|
|
770
922
|
}
|
|
771
923
|
}
|
|
772
|
-
buildExitConditionCheckpoint(
|
|
773
|
-
return
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
: {
|
|
779
|
-
kind: "iteration_end",
|
|
780
|
-
};
|
|
924
|
+
buildExitConditionCheckpoint(timing, stepId, conditionIndex) {
|
|
925
|
+
return {
|
|
926
|
+
timing,
|
|
927
|
+
stepId,
|
|
928
|
+
conditionIndex,
|
|
929
|
+
};
|
|
781
930
|
}
|
|
782
931
|
rebaseInterruptState(state, executionId, startedAt, prefixFrames) {
|
|
783
932
|
return {
|
|
@@ -791,6 +940,9 @@ ${responseContent}`;
|
|
|
791
940
|
const index = this.getStepIndex(workflow, stepId);
|
|
792
941
|
return workflow.steps[index + 1]?.id ?? null;
|
|
793
942
|
}
|
|
943
|
+
getContinuationStepId(workflow, timing, stepId) {
|
|
944
|
+
return timing === "before" ? stepId : this.getNextStepIdAfter(workflow, stepId);
|
|
945
|
+
}
|
|
794
946
|
getStepIndex(workflow, stepId) {
|
|
795
947
|
const index = workflow.steps.findIndex((step) => step.id === stepId);
|
|
796
948
|
if (index === -1) {
|
|
@@ -801,18 +953,22 @@ ${responseContent}`;
|
|
|
801
953
|
getStepDefinition(workflow, stepId) {
|
|
802
954
|
return workflow.steps[this.getStepIndex(workflow, stepId)];
|
|
803
955
|
}
|
|
804
|
-
|
|
956
|
+
resolveExitConditionTargetStepId(workflow, checkpoint) {
|
|
957
|
+
if (checkpoint.timing === "after") {
|
|
958
|
+
return checkpoint.stepId;
|
|
959
|
+
}
|
|
960
|
+
const stepIndex = this.getStepIndex(workflow, checkpoint.stepId);
|
|
961
|
+
return workflow.steps[stepIndex - 1]?.id;
|
|
962
|
+
}
|
|
963
|
+
findCompletedStepResult(execution, stepId) {
|
|
805
964
|
if (!stepId) {
|
|
806
|
-
|
|
965
|
+
return undefined;
|
|
807
966
|
}
|
|
808
967
|
const result = execution.stepResults
|
|
809
968
|
.filter((candidate) => candidate.stepId === stepId &&
|
|
810
969
|
candidate.iteration === execution.currentIteration)
|
|
811
970
|
.at(-1);
|
|
812
|
-
|
|
813
|
-
throw new ValidationError(`Step "${stepId}" is not completed and cannot be used for ${context}.`);
|
|
814
|
-
}
|
|
815
|
-
return result;
|
|
971
|
+
return result?.status === "completed" ? result : undefined;
|
|
816
972
|
}
|
|
817
973
|
findTerminalCompletedResult(execution) {
|
|
818
974
|
const stepResults = execution.stepResults;
|
|
@@ -849,12 +1005,24 @@ ${responseContent}`;
|
|
|
849
1005
|
recoveryHint: "Retry with a checkpoint created from the same workflow definition, or discard the tampered state file.",
|
|
850
1006
|
});
|
|
851
1007
|
}
|
|
852
|
-
if (frame.pendingExitCondition
|
|
853
|
-
(!frame.pendingExitCondition.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
1008
|
+
if (frame.pendingExitCondition) {
|
|
1009
|
+
if (!stepIds.has(frame.pendingExitCondition.stepId)) {
|
|
1010
|
+
throw new ValidationError(`Resume preflight failed: frames[${frameIndex}].pendingExitCondition.stepId "${frame.pendingExitCondition.stepId}" does not exist in workflow "${frame.workflowPath}".`, {
|
|
1011
|
+
recoveryHint: "Use a checkpoint whose pendingExitCondition still matches the workflow definition.",
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
const matchingConditions = workflow.loop.exitConditions?.filter((condition) => {
|
|
1015
|
+
const defaultStepId = condition.timing === "before"
|
|
1016
|
+
? workflow.steps[0]?.id
|
|
1017
|
+
: workflow.steps.at(-1)?.id;
|
|
1018
|
+
return (condition.timing === frame.pendingExitCondition.timing &&
|
|
1019
|
+
(condition.step ?? defaultStepId) === frame.pendingExitCondition.stepId);
|
|
1020
|
+
}) ?? [];
|
|
1021
|
+
if (frame.pendingExitCondition.conditionIndex >= matchingConditions.length) {
|
|
1022
|
+
throw new ValidationError(`Resume preflight failed: frames[${frameIndex}].pendingExitCondition.conditionIndex ${frame.pendingExitCondition.conditionIndex} is out of range for workflow "${frame.workflowPath}".`, {
|
|
1023
|
+
recoveryHint: "Use a checkpoint whose pendingExitCondition still matches the workflow definition.",
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
858
1026
|
}
|
|
859
1027
|
for (const result of frame.stepResults) {
|
|
860
1028
|
if (!stepIds.has(result.stepId)) {
|
|
@@ -990,7 +1158,4 @@ class InterruptTracker {
|
|
|
990
1158
|
}
|
|
991
1159
|
}
|
|
992
1160
|
}
|
|
993
|
-
function isWorkflowStep(step) {
|
|
994
|
-
return typeof step.workflow === "string";
|
|
995
|
-
}
|
|
996
1161
|
//# sourceMappingURL=workflow-execution-service.js.map
|