@gobing-ai/ts-dual-workflow-engine 0.3.16 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +22 -6
  2. package/dist/action-step.d.ts +61 -0
  3. package/dist/action-step.d.ts.map +1 -0
  4. package/dist/action-step.js +81 -0
  5. package/dist/errors.d.ts +5 -0
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/errors.js +9 -0
  8. package/dist/events.d.ts +41 -0
  9. package/dist/events.d.ts.map +1 -1
  10. package/dist/host.d.ts +10 -1
  11. package/dist/host.d.ts.map +1 -1
  12. package/dist/host.js +41 -2
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/persistence.d.ts +35 -1
  17. package/dist/persistence.d.ts.map +1 -1
  18. package/dist/persistence.js +112 -5
  19. package/dist/run-lifecycle.d.ts +21 -2
  20. package/dist/run-lifecycle.d.ts.map +1 -1
  21. package/dist/run-lifecycle.js +80 -16
  22. package/dist/schema-sql.d.ts.map +1 -1
  23. package/dist/schema-sql.js +5 -0
  24. package/dist/schema.d.ts +4 -0
  25. package/dist/schema.d.ts.map +1 -1
  26. package/dist/schema.js +4 -0
  27. package/dist/service.d.ts +36 -2
  28. package/dist/service.d.ts.map +1 -1
  29. package/dist/service.js +184 -0
  30. package/dist/state-machine.d.ts +2 -7
  31. package/dist/state-machine.d.ts.map +1 -1
  32. package/dist/state-machine.js +72 -71
  33. package/dist/transition-flow.d.ts +2 -0
  34. package/dist/transition-flow.d.ts.map +1 -1
  35. package/dist/transition-flow.js +46 -43
  36. package/dist/types.d.ts +59 -3
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +4 -4
  39. package/schemas/state-machine-workflow.schema.json +5 -1
  40. package/schemas/transition-flow-workflow.schema.json +5 -1
  41. package/src/action-step.ts +130 -0
  42. package/src/errors.ts +11 -0
  43. package/src/events.ts +54 -4
  44. package/src/host.ts +53 -3
  45. package/src/index.ts +7 -1
  46. package/src/persistence.ts +137 -6
  47. package/src/run-lifecycle.ts +113 -15
  48. package/src/schema-sql.ts +5 -0
  49. package/src/schema.ts +4 -0
  50. package/src/service.ts +255 -1
  51. package/src/state-machine.ts +86 -114
  52. package/src/transition-flow.ts +59 -46
  53. package/src/types.ts +61 -2
@@ -1,16 +1,15 @@
1
+ import { runActionSequence } from './action-step';
1
2
  import { FSMError } from './errors';
2
3
  import type { WorkflowEngineHost } from './host';
3
- import { allowedEnv, RunLifecycle, runtimeBuiltins } from './run-lifecycle';
4
+ import { allowedEnv, RunLifecycle } from './run-lifecycle';
4
5
  import type {
5
- ActionDef,
6
6
  ActionResult,
7
- OnErrorPolicy,
8
7
  StateMachineWorkflowDef,
9
8
  WorkflowPersistenceAdapter,
10
9
  WorkflowRunOptions,
11
10
  WorkflowRunResult,
12
11
  } from './types';
13
- import { mergeSetVars, mergeVars, resolveOnErrorPolicy, resolveTemplates } from './variables';
12
+ import { mergeSetVars, mergeVars } from './variables';
14
13
 
15
14
  /** Dependencies required by the state-machine driver. */
16
15
  export interface StateMachineDriverOptions {
@@ -33,53 +32,89 @@ export class StateMachineDriver {
33
32
  );
34
33
  }
35
34
 
35
+ /** Resume a paused state-machine run from the given state, skipping on-enter. */
36
+ async resume(
37
+ workflow: StateMachineWorkflowDef,
38
+ runId: string,
39
+ resumeFromState: string,
40
+ externalKey: string | undefined,
41
+ options: WorkflowRunOptions = {},
42
+ ): Promise<WorkflowRunResult> {
43
+ return await RunLifecycle.resume(
44
+ workflow.name,
45
+ 'state-machine',
46
+ { persistence: this.options.persistence, events: options.events },
47
+ runId,
48
+ externalKey,
49
+ (lifecycle) => this.loop(workflow, options, lifecycle, resumeFromState),
50
+ );
51
+ }
52
+
36
53
  private async loop(
37
54
  workflow: StateMachineWorkflowDef,
38
55
  options: WorkflowRunOptions,
39
56
  lifecycle: RunLifecycle,
57
+ resumeFromState?: string,
40
58
  ): Promise<WorkflowRunResult> {
41
59
  const runId = lifecycle.runId;
42
60
  const states = new Map(workflow.states.map((state) => [state.id, state]));
43
61
  const terminal = new Set(workflow.terminalStates ?? []);
44
62
  let vars = mergeVars(workflow.vars, options.vars);
45
63
  const env = allowedEnv(workflow.env?.allow ?? [], options.env);
46
- let current = states.get(workflow.initialState);
64
+ let current = resumeFromState !== undefined ? states.get(resumeFromState) : states.get(workflow.initialState);
47
65
  let transitionsTaken = 0;
48
66
  let lastActionResult: ActionResult | undefined;
49
67
  const iterationBound = workflow.iterationBound ?? 50;
50
68
  const defaultOnError = workflow.defaultOnError;
69
+ let isResume = resumeFromState !== undefined;
51
70
 
52
- if (current === undefined) throw new FSMError(`Initial state "${workflow.initialState}" is not declared`);
71
+ if (current === undefined) {
72
+ const label = resumeFromState ?? workflow.initialState;
73
+ throw new FSMError(`State "${label}" is not declared`);
74
+ }
53
75
 
54
76
  while (true) {
55
- // 1. Persist current state snapshot before work starts.
56
- await lifecycle.enter(current.id, transitionsTaken);
57
-
58
- // 2. Execute this state's on-enter actions in declaration order.
59
- const enter = await this.runActions(
60
- current.onEnter ?? [],
61
- workflow.name,
62
- current.id,
63
- runId,
64
- vars,
65
- env,
66
- options,
67
- transitionsTaken,
68
- lifecycle,
69
- defaultOnError,
70
- );
71
- // Retain the last action result (including failures the policy continued
72
- // past) so downstream guards can inspect it — matching the transition-flow
73
- // driver's `continue` semantics. A state with no enter actions must not
74
- // erase the previous result.
75
- if (enter.result !== undefined) lastActionResult = enter.result;
76
- if (enter.result?.setVars) vars = mergeSetVars(vars, enter.result.setVars);
77
- if (enter.outcome === 'terminal') {
78
- return await lifecycle.done(current.id, transitionsTaken);
79
- }
80
- // 4. Halt only when an action failed under a 'fail' policy.
81
- if (enter.outcome === 'fail') {
82
- return await lifecycle.fail(current.id, transitionsTaken, lastActionResult?.error);
77
+ if (isResume) {
78
+ // Resume: skip enter actions on the first iteration (already ran before pause).
79
+ isResume = false;
80
+ } else {
81
+ // 1. Persist current state snapshot before work starts.
82
+ await lifecycle.enter(current.id, transitionsTaken);
83
+
84
+ // 2. Execute this state's on-enter actions in declaration order.
85
+ const enter = options.dryRun
86
+ ? EMPTY_OUTCOME
87
+ : await runActionSequence(current.onEnter ?? [], vars, {
88
+ host: this.options.host,
89
+ persistence: this.options.persistence,
90
+ lifecycle,
91
+ workflowName: workflow.name,
92
+ stateOrNodeId: current.id,
93
+ runId,
94
+ mode: 'state-machine',
95
+ transitionsTaken,
96
+ env,
97
+ options,
98
+ defaultOnError,
99
+ });
100
+ // Retain the last action result (including failures the policy continued
101
+ // past) so downstream guards can inspect it — matching the transition-flow
102
+ // driver's `continue` semantics. A state with no enter actions must not
103
+ // erase the previous result.
104
+ if (enter.result !== undefined) lastActionResult = enter.result;
105
+ if (enter.result?.setVars) vars = mergeSetVars(vars, enter.result.setVars);
106
+ if (enter.outcome === 'terminal') {
107
+ return await lifecycle.done(current.id, transitionsTaken);
108
+ }
109
+ // 4. Halt only when an action failed under a 'fail' policy.
110
+ if (enter.outcome === 'fail') {
111
+ return await lifecycle.fail(current.id, transitionsTaken, lastActionResult?.error);
112
+ }
113
+
114
+ // Pause: if the state declares pause, stop advancing and persist the paused position.
115
+ if (current.pause === true) {
116
+ return await lifecycle.pause(current.id, transitionsTaken);
117
+ }
83
118
  }
84
119
 
85
120
  const outbound = workflow.transitions.filter((transition) => transition.from === current?.id);
@@ -105,18 +140,21 @@ export class StateMachineDriver {
105
140
  }
106
141
 
107
142
  // 6. Execute this state's on-exit actions before changing state.
108
- const exit = await this.runActions(
109
- current.onExit ?? [],
110
- workflow.name,
111
- current.id,
112
- runId,
113
- vars,
114
- env,
115
- options,
116
- transitionsTaken,
117
- lifecycle,
118
- defaultOnError,
119
- );
143
+ const exit = options.dryRun
144
+ ? EMPTY_OUTCOME
145
+ : await runActionSequence(current.onExit ?? [], vars, {
146
+ host: this.options.host,
147
+ persistence: this.options.persistence,
148
+ lifecycle,
149
+ workflowName: workflow.name,
150
+ stateOrNodeId: current.id,
151
+ runId,
152
+ mode: 'state-machine',
153
+ transitionsTaken,
154
+ env,
155
+ options,
156
+ defaultOnError,
157
+ });
120
158
  if (exit.result !== undefined) lastActionResult = exit.result;
121
159
  if (exit.result?.setVars) vars = mergeSetVars(vars, exit.result.setVars);
122
160
  if (exit.outcome === 'fail') return await lifecycle.fail(current.id, transitionsTaken, exit.result?.error);
@@ -132,76 +170,10 @@ export class StateMachineDriver {
132
170
  current = nextState;
133
171
  }
134
172
  }
135
-
136
- /**
137
- * Run a state's actions in order. Returns the last action result (retained even
138
- * when a failure was continued past, so downstream guards can inspect it) plus an
139
- * `outcome` discriminator: `terminal` (an action declared terminal success),
140
- * `fail` (a failure under a 'fail' policy — caller must halt), or `completed`.
141
- */
142
- private async runActions(
143
- actions: readonly ActionDef[],
144
- workflowName: string,
145
- stateId: string,
146
- runId: string,
147
- vars: Record<string, string>,
148
- env: Record<string, string>,
149
- options: WorkflowRunOptions,
150
- transitionsTaken: number,
151
- lifecycle: RunLifecycle,
152
- defaultOnError: OnErrorPolicy | undefined,
153
- ): Promise<RunActionsOutcome> {
154
- if (options.dryRun) {
155
- return { outcome: 'completed', result: undefined };
156
- }
157
-
158
- let last: ActionResult | undefined;
159
- for (const action of actions) {
160
- const resolved = resolveTemplates(action.options ?? {}, {
161
- vars,
162
- env,
163
- builtins: runtimeBuiltins(workflowName, stateId, runId, transitionsTaken, 'state-machine'),
164
- });
165
- const actionId = await this.options.persistence.saveActionStart(runId, stateId, action.kind);
166
- const actionStartMs = Date.now();
167
- lifecycle.actionStart(stateId, action.kind);
168
- try {
169
- last = await this.options.host.runAction(action.kind, resolved, {
170
- runId,
171
- workdir: options.workdir,
172
- stateOrNodeId: stateId,
173
- vars,
174
- env,
175
- metadata: options.metadata,
176
- events: options.events,
177
- });
178
- } finally {
179
- const durationMs = Date.now() - actionStartMs;
180
- lifecycle.actionDone(stateId, action.kind, durationMs, last?.ok ?? false);
181
- void this.options.persistence.saveActionFinalize(
182
- actionId,
183
- last?.ok !== false ? 'done' : 'failed',
184
- durationMs,
185
- last?.ok ?? false,
186
- last,
187
- );
188
- }
189
- if (last.terminal === true) return { outcome: 'terminal', result: last };
190
- if (!last.ok) {
191
- const policy = resolveOnErrorPolicy(action.onError, defaultOnError, options.onError);
192
- if (policy === 'fail') return { outcome: 'fail', result: last };
193
- lifecycle.warnActionFailed(stateId, transitionsTaken, last.error);
194
- }
195
- }
196
- return { outcome: 'completed', result: last };
197
- }
198
173
  }
199
174
 
200
- /** Result of running a state's actions: the last action result plus a control-flow discriminator. */
201
- interface RunActionsOutcome {
202
- readonly outcome: 'completed' | 'terminal' | 'fail';
203
- readonly result: ActionResult | undefined;
204
- }
175
+ /** Dry-run sentinel: no action ran, so there is nothing to retain and nothing to halt on. */
176
+ const EMPTY_OUTCOME = { outcome: 'completed', result: undefined } as const;
205
177
 
206
178
  async function firstPassingTransition(
207
179
  transitions: StateMachineWorkflowDef['transitions'],
@@ -1,6 +1,7 @@
1
+ import { runActionStep } from './action-step';
1
2
  import { FSMError } from './errors';
2
3
  import type { WorkflowEngineHost } from './host';
3
- import { allowedEnv, RunLifecycle, runtimeBuiltins } from './run-lifecycle';
4
+ import { allowedEnv, RunLifecycle } from './run-lifecycle';
4
5
  import type {
5
6
  ActionResult,
6
7
  TransitionFlowWorkflowDef,
@@ -8,7 +9,7 @@ import type {
8
9
  WorkflowRunOptions,
9
10
  WorkflowRunResult,
10
11
  } from './types';
11
- import { mergeSetVars, mergeVars, resolveOnErrorPolicy, resolveTemplates } from './variables';
12
+ import { mergeSetVars, mergeVars } from './variables';
12
13
 
13
14
  /** Dependencies required by the transition-flow driver. */
14
15
  export interface TransitionFlowDriverOptions {
@@ -31,75 +32,87 @@ export class TransitionFlowDriver {
31
32
  );
32
33
  }
33
34
 
35
+ /** Resume a paused transition-flow run from the given node, skipping node action. */
36
+ async resume(
37
+ workflow: TransitionFlowWorkflowDef,
38
+ runId: string,
39
+ resumeFromNode: string,
40
+ externalKey: string | undefined,
41
+ options: WorkflowRunOptions = {},
42
+ ): Promise<WorkflowRunResult> {
43
+ return await RunLifecycle.resume(
44
+ workflow.name,
45
+ 'transition-flow',
46
+ { persistence: this.options.persistence, events: options.events },
47
+ runId,
48
+ externalKey,
49
+ (lifecycle) => this.loop(workflow, options, lifecycle, resumeFromNode),
50
+ );
51
+ }
52
+
34
53
  private async loop(
35
54
  workflow: TransitionFlowWorkflowDef,
36
55
  options: WorkflowRunOptions,
37
56
  lifecycle: RunLifecycle,
57
+ resumeFromNode?: string,
38
58
  ): Promise<WorkflowRunResult> {
39
59
  const runId = lifecycle.runId;
40
60
  const nodes = new Map(workflow.nodes.map((node) => [node.id, node]));
41
61
  const terminal = new Set(workflow.terminalNodes ?? []);
42
62
  let vars = mergeVars(workflow.vars, options.vars);
43
63
  const env = allowedEnv(workflow.env?.allow ?? [], options.env);
44
- let current = nodes.get(workflow.initialNode);
64
+ let current = resumeFromNode !== undefined ? nodes.get(resumeFromNode) : nodes.get(workflow.initialNode);
45
65
  let transitionsTaken = 0;
46
66
  let lastActionResult: ActionResult | undefined;
47
67
  const iterationBound = workflow.iterationBound ?? 50;
48
68
  const defaultOnError = workflow.defaultOnError;
69
+ let isResume = resumeFromNode !== undefined;
49
70
 
50
71
  if (current === undefined) {
51
- throw new FSMError(`Initial node "${workflow.initialNode}" is not declared`);
72
+ const label = resumeFromNode ?? workflow.initialNode;
73
+ throw new FSMError(`Node "${label}" is not declared`);
52
74
  }
53
75
 
54
76
  while (true) {
55
- // 1. Persist current node snapshot before action execution.
56
- await lifecycle.enter(current.id, transitionsTaken);
77
+ if (isResume) {
78
+ // Resume: skip enter + node action on the first iteration (already ran before pause).
79
+ isResume = false;
80
+ } else {
81
+ // 1. Persist current node snapshot before action execution.
82
+ await lifecycle.enter(current.id, transitionsTaken);
57
83
 
58
- // 2. Execute the node action when one is configured (skipped in dry-run).
59
- if (options.dryRun) {
60
- if (current.action !== undefined) {
61
- lastActionResult = undefined;
62
- }
63
- } else if (current.action !== undefined) {
64
- const resolved = resolveTemplates(current.action.options ?? {}, {
65
- vars,
66
- env,
67
- builtins: runtimeBuiltins(workflow.name, current.id, runId, transitionsTaken, 'transition-flow'),
68
- });
69
- const actionId = await this.options.persistence.saveActionStart(runId, current.id, current.action.kind);
70
- const actionStartMs = Date.now();
71
- lifecycle.actionStart(current.id, current.action.kind);
72
- try {
73
- lastActionResult = await this.options.host.runAction(current.action.kind, resolved, {
74
- runId,
75
- workdir: options.workdir,
84
+ // 2. Execute the node action when one is configured (skipped in dry-run).
85
+ if (options.dryRun) {
86
+ if (current.action !== undefined) {
87
+ lastActionResult = undefined;
88
+ }
89
+ } else if (current.action !== undefined) {
90
+ const step = await runActionStep(current.action, vars, {
91
+ host: this.options.host,
92
+ persistence: this.options.persistence,
93
+ lifecycle,
94
+ workflowName: workflow.name,
76
95
  stateOrNodeId: current.id,
77
- vars,
96
+ runId,
97
+ mode: 'transition-flow',
98
+ transitionsTaken,
78
99
  env,
79
- metadata: options.metadata,
80
- events: options.events,
100
+ options,
101
+ defaultOnError,
81
102
  });
82
- } finally {
83
- const durationMs = Date.now() - actionStartMs;
84
- lifecycle.actionDone(current.id, current.action.kind, durationMs, lastActionResult?.ok ?? false);
85
- void this.options.persistence.saveActionFinalize(
86
- actionId,
87
- lastActionResult?.ok !== false ? 'done' : 'failed',
88
- durationMs,
89
- lastActionResult?.ok ?? false,
90
- lastActionResult,
91
- );
92
- }
93
- if (lastActionResult.setVars) vars = mergeSetVars(vars, lastActionResult.setVars);
94
- if (!lastActionResult.ok) {
95
- const policy = resolveOnErrorPolicy(current.action.onError, defaultOnError, options.onError);
96
- if (policy === 'fail') {
97
- return await lifecycle.fail(current.id, transitionsTaken, lastActionResult.error);
103
+ lastActionResult = step.result;
104
+ if (step.result?.setVars) vars = mergeSetVars(vars, step.result.setVars);
105
+ if (step.outcome === 'terminal') {
106
+ return await lifecycle.done(current.id, transitionsTaken);
107
+ }
108
+ if (step.outcome === 'fail') {
109
+ return await lifecycle.fail(current.id, transitionsTaken, step.result?.error);
98
110
  }
99
- lifecycle.warnActionFailed(current.id, transitionsTaken, lastActionResult.error);
100
111
  }
101
- if (lastActionResult.terminal === true) {
102
- return await lifecycle.done(current.id, transitionsTaken);
112
+
113
+ // Pause: if the node declares pause, stop advancing and persist the paused position.
114
+ if (current.pause === true) {
115
+ return await lifecycle.pause(current.id, transitionsTaken);
103
116
  }
104
117
  }
105
118
 
package/src/types.ts CHANGED
@@ -5,7 +5,7 @@ import type { EventBus } from '@gobing-ai/ts-infra';
5
5
  import type { WorkflowEngineEvents } from './events';
6
6
 
7
7
  /** Workflow execution status persisted for runs and phases. */
8
- export type WorkflowStatus = 'running' | 'done' | 'failed';
8
+ export type WorkflowStatus = 'running' | 'done' | 'failed' | 'paused';
9
9
 
10
10
  /** Runtime variables and user variables available to workflow definitions. */
11
11
  export type Vars = Record<string, string>;
@@ -36,6 +36,8 @@ export interface StateDef {
36
36
  readonly description?: string;
37
37
  readonly onEnter?: readonly ActionDef[];
38
38
  readonly onExit?: readonly ActionDef[];
39
+ /** When true, the engine pauses the run at this state instead of auto-advancing. */
40
+ readonly pause?: boolean;
39
41
  }
40
42
 
41
43
  /** One transition in a state-machine workflow. */
@@ -74,6 +76,8 @@ export interface FlowNodeDef {
74
76
  readonly description?: string;
75
77
  readonly type?: 'action' | 'gate' | 'parallel' | 'decision';
76
78
  readonly action?: ActionDef;
79
+ /** When true, the engine pauses the run at this node instead of auto-advancing. */
80
+ readonly pause?: boolean;
77
81
  }
78
82
 
79
83
  /** Transition-flow edge definition. */
@@ -140,13 +144,20 @@ export interface GuardContext {
140
144
  readonly runId: string;
141
145
  readonly current: string;
142
146
  readonly vars: Vars;
147
+ readonly workdir?: string;
143
148
  readonly lastActionResult?: ActionResult;
144
149
  }
145
150
 
151
+ /** Rich guard evaluation result. Boolean guard runners remain supported for compatibility. */
152
+ export interface GuardEvaluationResult {
153
+ readonly passed: boolean;
154
+ readonly report?: unknown;
155
+ }
156
+
146
157
  /** Guard runner implementation registered in the workflow host. */
147
158
  export interface GuardRunner {
148
159
  readonly kind: string;
149
- evaluate(options: Record<string, unknown>, context: GuardContext): Promise<boolean>;
160
+ evaluate(options: Record<string, unknown>, context: GuardContext): Promise<boolean | GuardEvaluationResult>;
150
161
  }
151
162
 
152
163
  /** Input for running a workflow. */
@@ -162,6 +173,8 @@ export interface WorkflowRunOptions {
162
173
  readonly onError?: OnErrorPolicy;
163
174
  /** Validate the definition and walk the transition graph without executing actions. */
164
175
  readonly dryRun?: boolean;
176
+ /** Optional caller-supplied external key, unique per workflow definition. */
177
+ readonly externalKey?: string;
165
178
  }
166
179
 
167
180
  /** Result returned by both driver loops. */
@@ -184,8 +197,44 @@ export interface WorkflowRunRecord {
184
197
  readonly started_at: string;
185
198
  readonly completed_at: string | null;
186
199
  readonly metadata_json: string;
200
+ /** Optional caller-supplied external key, unique per workflow definition. */
201
+ readonly external_key?: string | null;
187
202
  }
188
203
 
204
+ /** Result of force-setting the current state of a run. */
205
+ export interface WorkflowReseedResult {
206
+ readonly fromState: string | null;
207
+ readonly toState: string;
208
+ }
209
+
210
+ /** Reason categories when an external transition request is denied. */
211
+ export type TransitionDeniedReason = 'no-such-transition' | 'guard-failed';
212
+
213
+ /** Result when an external transition request is allowed. */
214
+ export interface TransitionAllowed {
215
+ readonly allowed: true;
216
+ /** The state the run has moved to. */
217
+ readonly toState: string;
218
+ /** The state the run moved from. */
219
+ readonly fromState: string;
220
+ }
221
+
222
+ /** Result when an external transition request is denied. */
223
+ export interface TransitionDenied {
224
+ readonly allowed: false;
225
+ /** Machine-readable reason category. */
226
+ readonly reason: TransitionDeniedReason;
227
+ /** Human-readable detail from the guard (when guard-failed) or transition lookup. */
228
+ readonly detail: string;
229
+ /** The guard kind that was evaluated (when guard-failed). */
230
+ readonly guardKind?: string;
231
+ /** Machine-readable guard report/output when a guard was evaluated. */
232
+ readonly guardReport?: unknown;
233
+ }
234
+
235
+ /** Discriminated union result for external transition requests. */
236
+ export type TransitionRequestResult = TransitionAllowed | TransitionDenied;
237
+
189
238
  /** Persisted action run record — one row per action executed in a workflow run. */
190
239
  export interface ActionRunRecord {
191
240
  readonly id: string;
@@ -223,4 +272,14 @@ export interface WorkflowPersistenceAdapter {
223
272
  ): Promise<void>;
224
273
  loadRun(runId: string): Promise<WorkflowRunRecord | undefined>;
225
274
  listRuns(): Promise<readonly WorkflowRunRecord[]>;
275
+ /** Look up a run by its external key within a workflow definition. Returns undefined if not found. */
276
+ findRunByKey(workflowName: string, externalKey: string): Promise<WorkflowRunRecord | undefined>;
277
+ /** Create a run or attach to an existing one by external key. Atomic create-or-attach semantics. */
278
+ createOrAttachRun(record: WorkflowRunRecord): Promise<WorkflowRunRecord>;
279
+ /** Force-set the current state of a run (consumer-side authority reconciliation). */
280
+ reseedRun(runId: string, newState: string): Promise<WorkflowReseedResult>;
281
+ /** Load the current state name for a run (latest state snapshot). Returns undefined if no state recorded. */
282
+ loadCurrentState(runId: string): Promise<string | undefined>;
283
+ /** List runs with status 'paused'. Optional filters: workflow name, limit. Ordered most-recent-first. */
284
+ listPausedRuns(options?: { workflowName?: string; limit?: number }): Promise<readonly WorkflowRunRecord[]>;
226
285
  }