@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.
@@ -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 requireCompletedStepResult;
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, readFile } from "node:fs/promises";
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 checkpoint = execution.pendingExitCondition;
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 (outcome.kind === "interrupted") {
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: outcome.interruptState,
149
- };
150
- }
151
- const before = execution.stepResults;
152
- execution.applyExitVerdict(outcome.verdict, {
153
- evaluatedAfter: checkpoint.evaluateAfterStepId,
154
- });
155
- await this.logNewlySkippedSteps(execution, before);
156
- if (outcome.verdict === "stop") {
157
- break;
158
- }
159
- continuationStepId =
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: continuationStepId ?? undefined,
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 startIndex = this.getStepIndex(workflow, nextStepId);
202
- let completedViaExitCondition = false;
203
- for (const step of workflow.steps.slice(startIndex)) {
204
- const outcome = await this.executeStep({
205
- step,
206
- execution,
207
- workflow,
208
- workflowPath,
209
- cwd,
210
- signal,
211
- vars: opts.vars,
212
- interruptTracker,
213
- });
214
- if (outcome.kind === "interrupted") {
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
- if (execution.status !== "running") {
225
- break;
226
- }
227
- if (execution.shouldEvaluateAfter(step.id)) {
228
- const checkpoint = this.buildExitConditionCheckpoint(workflow, step.id);
229
- const evaluation = await this.evaluateExitCondition({
230
- execution,
231
- workflow,
232
- cwd,
233
- signal,
234
- interruptTracker,
235
- checkpoint,
236
- });
237
- if (evaluation.kind === "interrupted") {
238
- return {
239
- status: "interrupted",
240
- execution,
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: execution.interrupt(),
265
- };
266
- }
360
+ interruptState: afterEvaluation.interruptState,
361
+ },
362
+ };
267
363
  }
268
- if (completedViaExitCondition) {
364
+ if (afterEvaluation.kind === "stop") {
365
+ exitConditionMet = true;
269
366
  break;
270
367
  }
271
- if (execution.nextStepId === null &&
272
- execution.currentIteration >= workflow.loop.maxIterations) {
273
- execution.complete("max_iterations_reached", this.now().toISOString());
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
- status: "completed",
281
- execution,
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 exitCondition = workflow.loop.exitCondition;
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 (exitCondition.type === "agent") {
548
- const targetStepId = checkpoint.kind === "after_step"
549
- ? checkpoint.evaluateAfterStepId
550
- : workflow.steps.at(-1)?.id;
551
- const targetResult = this.requireCompletedStepResult(execution, targetStepId, `exit condition evaluation in iteration ${execution.currentIteration}`);
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 "${targetStepId}" is missing fullResponsePath required for exit condition evaluation.`);
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(exitCondition.agent.prompt, targetResult.fullResponsePath, responseContent);
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: exitCondition.agent.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(exitCondition.command.run, {
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, exitCondition.command.successMeans);
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: null,
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: null,
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(workflow, stepId) {
773
- return workflow.loop.exitCondition?.evaluateAfter
774
- ? {
775
- kind: "after_step",
776
- evaluateAfterStepId: stepId,
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
- requireCompletedStepResult(execution, stepId, context) {
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
- throw new ValidationError(`Cannot resolve step result for ${context}: target step is missing.`);
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
- if (!result || result.status !== "completed") {
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?.kind === "after_step" &&
853
- (!frame.pendingExitCondition.evaluateAfterStepId ||
854
- !stepIds.has(frame.pendingExitCondition.evaluateAfterStepId))) {
855
- throw new ValidationError(`Resume preflight failed: frames[${frameIndex}].pendingExitCondition references an unknown step in workflow "${frame.workflowPath}".`, {
856
- recoveryHint: "Use a checkpoint whose pendingExitCondition still matches the workflow definition.",
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