@gobing-ai/ts-dual-workflow-engine 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- package/dist/action-step.d.ts +61 -0
- package/dist/action-step.d.ts.map +1 -0
- package/dist/action-step.js +81 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -0
- package/dist/events.d.ts +41 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/host.d.ts +10 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +41 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/persistence.d.ts +35 -1
- package/dist/persistence.d.ts.map +1 -1
- package/dist/persistence.js +112 -5
- package/dist/run-lifecycle.d.ts +21 -2
- package/dist/run-lifecycle.d.ts.map +1 -1
- package/dist/run-lifecycle.js +80 -16
- package/dist/schema-sql.d.ts.map +1 -1
- package/dist/schema-sql.js +5 -0
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +4 -0
- package/dist/service.d.ts +36 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +184 -0
- package/dist/state-machine.d.ts +2 -7
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +72 -71
- package/dist/transition-flow.d.ts +2 -0
- package/dist/transition-flow.d.ts.map +1 -1
- package/dist/transition-flow.js +46 -43
- package/dist/types.d.ts +59 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/schemas/state-machine-workflow.schema.json +5 -1
- package/schemas/transition-flow-workflow.schema.json +5 -1
- package/src/action-step.ts +130 -0
- package/src/errors.ts +11 -0
- package/src/events.ts +54 -4
- package/src/host.ts +53 -3
- package/src/index.ts +7 -1
- package/src/persistence.ts +137 -6
- package/src/run-lifecycle.ts +113 -15
- package/src/schema-sql.ts +5 -0
- package/src/schema.ts +4 -0
- package/src/service.ts +255 -1
- package/src/state-machine.ts +86 -114
- package/src/transition-flow.ts +59 -46
- package/src/types.ts +61 -2
package/src/state-machine.ts
CHANGED
|
@@ -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
|
|
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
|
|
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)
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
current.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
/**
|
|
201
|
-
|
|
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'],
|
package/src/transition-flow.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
72
|
+
const label = resumeFromNode ?? workflow.initialNode;
|
|
73
|
+
throw new FSMError(`Node "${label}" is not declared`);
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
while (true) {
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
96
|
+
runId,
|
|
97
|
+
mode: 'transition-flow',
|
|
98
|
+
transitionsTaken,
|
|
78
99
|
env,
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
options,
|
|
101
|
+
defaultOnError,
|
|
81
102
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
}
|