@gobing-ai/ts-dual-workflow-engine 0.3.1 → 0.3.2

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 (45) hide show
  1. package/README.md +506 -5
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +7 -11
  5. package/dist/events.d.ts +49 -0
  6. package/dist/events.d.ts.map +1 -0
  7. package/dist/events.js +0 -0
  8. package/dist/extensions.d.ts +60 -0
  9. package/dist/extensions.d.ts.map +1 -0
  10. package/dist/extensions.js +85 -0
  11. package/dist/index.d.ts +5 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -1
  14. package/dist/run-lifecycle.d.ts +58 -0
  15. package/dist/run-lifecycle.d.ts.map +1 -0
  16. package/dist/run-lifecycle.js +149 -0
  17. package/dist/schema-sql.d.ts +1 -0
  18. package/dist/schema-sql.d.ts.map +1 -1
  19. package/dist/schema-sql.js +1 -0
  20. package/dist/schema.d.ts +44 -0
  21. package/dist/schema.d.ts.map +1 -1
  22. package/dist/schema.js +3 -0
  23. package/dist/state-machine.d.ts +7 -2
  24. package/dist/state-machine.d.ts.map +1 -1
  25. package/dist/state-machine.js +63 -72
  26. package/dist/transition-flow.d.ts +1 -2
  27. package/dist/transition-flow.d.ts.map +1 -1
  28. package/dist/transition-flow.js +35 -61
  29. package/dist/types.d.ts +14 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/variables.d.ts +6 -1
  32. package/dist/variables.d.ts.map +1 -1
  33. package/dist/variables.js +7 -0
  34. package/package.json +4 -4
  35. package/src/config.ts +8 -11
  36. package/src/events.ts +19 -0
  37. package/src/extensions.ts +163 -0
  38. package/src/index.ts +24 -1
  39. package/src/run-lifecycle.ts +211 -0
  40. package/src/schema-sql.ts +1 -0
  41. package/src/schema.ts +3 -0
  42. package/src/state-machine.ts +78 -128
  43. package/src/transition-flow.ts +47 -105
  44. package/src/types.ts +16 -0
  45. package/src/variables.ts +13 -1
@@ -1,16 +1,16 @@
1
- import { getProcessEnv } from '@gobing-ai/ts-runtime';
2
1
  import { FSMError } from './errors';
3
2
  import type { WorkflowEngineHost } from './host';
3
+ import { allowedEnv, RunLifecycle, runtimeBuiltins } from './run-lifecycle';
4
4
  import type {
5
5
  ActionDef,
6
6
  ActionResult,
7
+ OnErrorPolicy,
7
8
  StateMachineWorkflowDef,
8
9
  WorkflowPersistenceAdapter,
9
10
  WorkflowRunOptions,
10
- WorkflowRunRecord,
11
11
  WorkflowRunResult,
12
12
  } from './types';
13
- import { mergeVars, resolveTemplates } from './variables';
13
+ import { mergeVars, resolveOnErrorPolicy, resolveTemplates } from './variables';
14
14
 
15
15
  /** Dependencies required by the state-machine driver. */
16
16
  export interface StateMachineDriverOptions {
@@ -24,29 +24,39 @@ export class StateMachineDriver {
24
24
 
25
25
  /** Run a state-machine workflow to completion or failure. */
26
26
  async run(workflow: StateMachineWorkflowDef, options: WorkflowRunOptions = {}): Promise<WorkflowRunResult> {
27
- const runId = options.runId ?? crypto.randomUUID();
28
- const startedAt = new Date().toISOString();
29
- const mode = 'state-machine';
30
- await this.options.persistence.createRun(runRecord(runId, workflow.name, mode, startedAt, options.metadata));
27
+ return await RunLifecycle.run(
28
+ workflow.name,
29
+ 'state-machine',
30
+ { persistence: this.options.persistence, events: options.events },
31
+ options,
32
+ (lifecycle) => this.loop(workflow, options, lifecycle),
33
+ );
34
+ }
31
35
 
36
+ private async loop(
37
+ workflow: StateMachineWorkflowDef,
38
+ options: WorkflowRunOptions,
39
+ lifecycle: RunLifecycle,
40
+ ): Promise<WorkflowRunResult> {
41
+ const runId = lifecycle.runId;
32
42
  const states = new Map(workflow.states.map((state) => [state.id, state]));
33
43
  const terminal = new Set(workflow.terminalStates ?? []);
34
44
  const vars = mergeVars(workflow.vars, options.vars);
35
- const env = allowedEnv(workflow.env?.allow ?? [], options.env ?? getProcessEnv());
45
+ const env = allowedEnv(workflow.env?.allow ?? [], options.env);
36
46
  let current = states.get(workflow.initialState);
37
47
  let transitionsTaken = 0;
38
48
  let lastActionResult: ActionResult | undefined;
39
49
  const iterationBound = workflow.iterationBound ?? 50;
50
+ const defaultOnError = workflow.defaultOnError;
40
51
 
41
52
  if (current === undefined) throw new FSMError(`Initial state "${workflow.initialState}" is not declared`);
42
53
 
43
54
  while (true) {
44
55
  // 1. Persist current state snapshot before work starts.
45
- await this.options.persistence.saveWorkflowState(runId, current.id, { transitionsTaken });
46
- await this.options.persistence.savePhase(runId, current.id, 'running');
56
+ await lifecycle.enter(current.id, transitionsTaken);
47
57
 
48
58
  // 2. Execute this state's on-enter actions in declaration order.
49
- lastActionResult = await this.runActions(
59
+ const enter = await this.runActions(
50
60
  current.onEnter ?? [],
51
61
  workflow.name,
52
62
  current.id,
@@ -55,26 +65,26 @@ export class StateMachineDriver {
55
65
  env,
56
66
  options,
57
67
  transitionsTaken,
68
+ lifecycle,
69
+ defaultOnError,
58
70
  );
59
- if (lastActionResult?.ok === false)
60
- return await this.fail(
61
- runId,
62
- workflow.name,
63
- mode,
64
- current.id,
65
- transitionsTaken,
66
- lastActionResult.error,
67
- );
68
-
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;
69
76
  // 3. Stop immediately when an action explicitly declares terminal success.
70
- if (lastActionResult?.terminal === true) {
71
- return await this.done(runId, workflow.name, mode, current.id, transitionsTaken);
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);
72
83
  }
73
84
 
74
- // 4. Stop when the current state is terminal or has no outbound transitions.
75
85
  const outbound = workflow.transitions.filter((transition) => transition.from === current?.id);
76
86
  if (terminal.has(current.id) || outbound.length === 0) {
77
- return await this.done(runId, workflow.name, mode, current.id, transitionsTaken);
87
+ return await lifecycle.done(current.id, transitionsTaken);
78
88
  }
79
89
 
80
90
  // 5. Evaluate transition guards in declaration order and pick the first passing transition.
@@ -85,18 +95,11 @@ export class StateMachineDriver {
85
95
  lastActionResult,
86
96
  });
87
97
  if (nextTransition === undefined) {
88
- return await this.fail(
89
- runId,
90
- workflow.name,
91
- mode,
92
- current.id,
93
- transitionsTaken,
94
- 'no-passing-transition',
95
- );
98
+ return await lifecycle.fail(current.id, transitionsTaken, 'no-passing-transition');
96
99
  }
97
100
 
98
101
  // 6. Execute this state's on-exit actions before changing state.
99
- const exitResult = await this.runActions(
102
+ const exit = await this.runActions(
100
103
  current.onExit ?? [],
101
104
  workflow.name,
102
105
  current.id,
@@ -105,27 +108,17 @@ export class StateMachineDriver {
105
108
  env,
106
109
  options,
107
110
  transitionsTaken,
111
+ lifecycle,
112
+ defaultOnError,
108
113
  );
109
- if (exitResult?.ok === false)
110
- return await this.fail(runId, workflow.name, mode, current.id, transitionsTaken, exitResult.error);
114
+ if (exit.result !== undefined) lastActionResult = exit.result;
115
+ if (exit.outcome === 'fail') return await lifecycle.fail(current.id, transitionsTaken, exit.result?.error);
111
116
 
112
117
  // 7. Persist transition and move to the target state.
113
118
  transitionsTaken += 1;
114
- await this.options.persistence.saveTransition(
115
- runId,
116
- current.id,
117
- nextTransition.to,
118
- nextTransition.trigger ?? null,
119
- );
119
+ await lifecycle.recordTransition(current.id, nextTransition.to, nextTransition.trigger ?? null);
120
120
  if (transitionsTaken > iterationBound) {
121
- return await this.fail(
122
- runId,
123
- workflow.name,
124
- mode,
125
- current.id,
126
- transitionsTaken,
127
- 'iteration-bound-exceeded',
128
- );
121
+ return await lifecycle.fail(current.id, transitionsTaken, 'iteration-bound-exceeded');
129
122
  }
130
123
  const nextState = states.get(nextTransition.to);
131
124
  if (nextState === undefined) throw new FSMError(`Transition target "${nextTransition.to}" is not declared`);
@@ -133,6 +126,12 @@ export class StateMachineDriver {
133
126
  }
134
127
  }
135
128
 
129
+ /**
130
+ * Run a state's actions in order. Returns the last action result (retained even
131
+ * when a failure was continued past, so downstream guards can inspect it) plus an
132
+ * `outcome` discriminator: `terminal` (an action declared terminal success),
133
+ * `fail` (a failure under a 'fail' policy — caller must halt), or `completed`.
134
+ */
136
135
  private async runActions(
137
136
  actions: readonly ActionDef[],
138
137
  workflowName: string,
@@ -142,70 +141,45 @@ export class StateMachineDriver {
142
141
  env: Record<string, string>,
143
142
  options: WorkflowRunOptions,
144
143
  transitionsTaken: number,
145
- ): Promise<ActionResult | undefined> {
144
+ lifecycle: RunLifecycle,
145
+ defaultOnError: OnErrorPolicy | undefined,
146
+ ): Promise<RunActionsOutcome> {
146
147
  let last: ActionResult | undefined;
147
148
  for (const action of actions) {
148
149
  const resolved = resolveTemplates(action.options ?? {}, {
149
150
  vars,
150
151
  env,
151
- builtins: runtimeBuiltins(workflowName, stateId, runId, transitionsTaken),
152
- });
153
- last = await this.options.host.runAction(action.kind, resolved, {
154
- runId,
155
- workdir: options.workdir,
156
- stateOrNodeId: stateId,
157
- vars,
158
- env,
159
- metadata: options.metadata,
152
+ builtins: runtimeBuiltins(workflowName, stateId, runId, transitionsTaken, 'state-machine'),
160
153
  });
161
- if (!last.ok || last.terminal === true) return last;
154
+ const actionStartMs = Date.now();
155
+ lifecycle.actionStart(stateId, action.kind);
156
+ try {
157
+ last = await this.options.host.runAction(action.kind, resolved, {
158
+ runId,
159
+ workdir: options.workdir,
160
+ stateOrNodeId: stateId,
161
+ vars,
162
+ env,
163
+ metadata: options.metadata,
164
+ });
165
+ } finally {
166
+ lifecycle.actionDone(stateId, action.kind, Date.now() - actionStartMs, last?.ok ?? false);
167
+ }
168
+ if (last.terminal === true) return { outcome: 'terminal', result: last };
169
+ if (!last.ok) {
170
+ const policy = resolveOnErrorPolicy(action.onError, defaultOnError, options.onError);
171
+ if (policy === 'fail') return { outcome: 'fail', result: last };
172
+ lifecycle.warnActionFailed(stateId, transitionsTaken, last.error);
173
+ }
162
174
  }
163
- return last;
164
- }
165
-
166
- private async done(
167
- runId: string,
168
- workflowName: string,
169
- mode: 'state-machine',
170
- finalState: string,
171
- transitionsTaken: number,
172
- ): Promise<WorkflowRunResult> {
173
- await this.options.persistence.savePhase(runId, finalState, 'done');
174
- await this.options.persistence.finalizeRun(runId, 'done', new Date().toISOString());
175
- return { runId, workflowName, mode, status: 'done', finalState, transitionsTaken };
176
- }
177
-
178
- private async fail(
179
- runId: string,
180
- workflowName: string,
181
- mode: 'state-machine',
182
- finalState: string,
183
- transitionsTaken: number,
184
- reason = 'failed',
185
- ): Promise<WorkflowRunResult> {
186
- await this.options.persistence.savePhase(runId, finalState, 'failed');
187
- await this.options.persistence.finalizeRun(runId, 'failed', new Date().toISOString());
188
- return { runId, workflowName, mode, status: 'failed', finalState, transitionsTaken, reason };
175
+ return { outcome: 'completed', result: last };
189
176
  }
190
177
  }
191
178
 
192
- /** Built-in bare template values available to state-machine action options. */
193
- function runtimeBuiltins(
194
- workflowName: string,
195
- stateId: string,
196
- runId: string,
197
- transitionsTaken: number,
198
- ): Record<string, string | number> {
199
- return {
200
- workflow: workflowName,
201
- runId,
202
- task: workflowName,
203
- state: stateId,
204
- node: stateId,
205
- iteration: transitionsTaken,
206
- run: runId,
207
- runtime: 'state-machine',
208
- };
179
+ /** Result of running a state's actions: the last action result plus a control-flow discriminator. */
180
+ interface RunActionsOutcome {
181
+ readonly outcome: 'completed' | 'terminal' | 'fail';
182
+ readonly result: ActionResult | undefined;
209
183
  }
210
184
 
211
185
  async function firstPassingTransition(
@@ -219,27 +193,3 @@ async function firstPassingTransition(
219
193
  }
220
194
  return undefined;
221
195
  }
222
-
223
- function allowedEnv(names: readonly string[], source: Record<string, string | undefined>): Record<string, string> {
224
- return Object.fromEntries(
225
- names.flatMap((name) => (source[name] === undefined ? [] : [[name, source[name] as string]])),
226
- );
227
- }
228
-
229
- function runRecord(
230
- runId: string,
231
- workflowName: string,
232
- mode: string,
233
- startedAt: string,
234
- metadata: unknown,
235
- ): WorkflowRunRecord {
236
- return {
237
- id: runId,
238
- workflow_name: workflowName,
239
- mode,
240
- status: 'running',
241
- started_at: startedAt,
242
- completed_at: null,
243
- metadata_json: JSON.stringify(metadata ?? {}),
244
- };
245
- }
@@ -1,15 +1,14 @@
1
- import { getProcessEnv } from '@gobing-ai/ts-runtime';
2
1
  import { FSMError } from './errors';
3
2
  import type { WorkflowEngineHost } from './host';
3
+ import { allowedEnv, RunLifecycle, runtimeBuiltins } from './run-lifecycle';
4
4
  import type {
5
5
  ActionResult,
6
6
  TransitionFlowWorkflowDef,
7
7
  WorkflowPersistenceAdapter,
8
8
  WorkflowRunOptions,
9
- WorkflowRunRecord,
10
9
  WorkflowRunResult,
11
10
  } from './types';
12
- import { mergeVars, resolveTemplates } from './variables';
11
+ import { mergeVars, resolveOnErrorPolicy, resolveTemplates } from './variables';
13
12
 
14
13
  /** Dependencies required by the transition-flow driver. */
15
14
  export interface TransitionFlowDriverOptions {
@@ -23,19 +22,30 @@ export class TransitionFlowDriver {
23
22
 
24
23
  /** Run a transition-flow workflow to completion or failure. */
25
24
  async run(workflow: TransitionFlowWorkflowDef, options: WorkflowRunOptions = {}): Promise<WorkflowRunResult> {
26
- const runId = options.runId ?? crypto.randomUUID();
27
- const startedAt = new Date().toISOString();
28
- const mode = 'transition-flow';
29
- await this.options.persistence.createRun(runRecord(runId, workflow.name, mode, startedAt, options.metadata));
25
+ return await RunLifecycle.run(
26
+ workflow.name,
27
+ 'transition-flow',
28
+ { persistence: this.options.persistence, events: options.events },
29
+ options,
30
+ (lifecycle) => this.loop(workflow, options, lifecycle),
31
+ );
32
+ }
30
33
 
34
+ private async loop(
35
+ workflow: TransitionFlowWorkflowDef,
36
+ options: WorkflowRunOptions,
37
+ lifecycle: RunLifecycle,
38
+ ): Promise<WorkflowRunResult> {
39
+ const runId = lifecycle.runId;
31
40
  const nodes = new Map(workflow.nodes.map((node) => [node.id, node]));
32
41
  const terminal = new Set(workflow.terminalNodes ?? []);
33
42
  const vars = mergeVars(workflow.vars, options.vars);
34
- const env = allowedEnv(workflow.env?.allow ?? [], options.env ?? getProcessEnv());
43
+ const env = allowedEnv(workflow.env?.allow ?? [], options.env);
35
44
  let current = nodes.get(workflow.initialNode);
36
45
  let transitionsTaken = 0;
37
46
  let lastActionResult: ActionResult | undefined;
38
47
  const iterationBound = workflow.iterationBound ?? 50;
48
+ const defaultOnError = workflow.defaultOnError;
39
49
 
40
50
  if (current === undefined) {
41
51
  throw new FSMError(`Initial node "${workflow.initialNode}" is not declared`);
@@ -43,43 +53,50 @@ export class TransitionFlowDriver {
43
53
 
44
54
  while (true) {
45
55
  // 1. Persist current node snapshot before action execution.
46
- await this.options.persistence.saveWorkflowState(runId, current.id, { transitionsTaken });
47
- await this.options.persistence.savePhase(runId, current.id, 'running');
56
+ await lifecycle.enter(current.id, transitionsTaken);
48
57
 
49
58
  // 2. Execute the node action when one is configured.
50
59
  if (current.action !== undefined) {
51
60
  const resolved = resolveTemplates(current.action.options ?? {}, {
52
61
  vars,
53
62
  env,
54
- builtins: runtimeBuiltins(workflow.name, current.id, runId, transitionsTaken),
55
- });
56
- lastActionResult = await this.options.host.runAction(current.action.kind, resolved, {
57
- runId,
58
- workdir: options.workdir,
59
- stateOrNodeId: current.id,
60
- vars,
61
- env,
62
- metadata: options.metadata,
63
+ builtins: runtimeBuiltins(workflow.name, current.id, runId, transitionsTaken, 'transition-flow'),
63
64
  });
64
- if (!lastActionResult.ok) {
65
- return await this.fail(
65
+ const actionStartMs = Date.now();
66
+ lifecycle.actionStart(current.id, current.action.kind);
67
+ try {
68
+ lastActionResult = await this.options.host.runAction(current.action.kind, resolved, {
66
69
  runId,
67
- workflow.name,
68
- mode,
70
+ workdir: options.workdir,
71
+ stateOrNodeId: current.id,
72
+ vars,
73
+ env,
74
+ metadata: options.metadata,
75
+ });
76
+ } finally {
77
+ lifecycle.actionDone(
69
78
  current.id,
70
- transitionsTaken,
71
- lastActionResult.error,
79
+ current.action.kind,
80
+ Date.now() - actionStartMs,
81
+ lastActionResult?.ok ?? false,
72
82
  );
73
83
  }
84
+ if (!lastActionResult.ok) {
85
+ const policy = resolveOnErrorPolicy(current.action.onError, defaultOnError, options.onError);
86
+ if (policy === 'fail') {
87
+ return await lifecycle.fail(current.id, transitionsTaken, lastActionResult.error);
88
+ }
89
+ lifecycle.warnActionFailed(current.id, transitionsTaken, lastActionResult.error);
90
+ }
74
91
  if (lastActionResult.terminal === true) {
75
- return await this.done(runId, workflow.name, mode, current.id, transitionsTaken);
92
+ return await lifecycle.done(current.id, transitionsTaken);
76
93
  }
77
94
  }
78
95
 
79
96
  // 3. Stop when the node is terminal or no outgoing edge exists.
80
97
  const outbound = workflow.edges.filter((edge) => edge.from === current?.id);
81
98
  if (terminal.has(current.id) || outbound.length === 0) {
82
- return await this.done(runId, workflow.name, mode, current.id, transitionsTaken);
99
+ return await lifecycle.done(current.id, transitionsTaken);
83
100
  }
84
101
 
85
102
  // 4. Evaluate edge conditions in declaration order and pick the first passing edge.
@@ -90,23 +107,16 @@ export class TransitionFlowDriver {
90
107
  lastActionResult,
91
108
  });
92
109
  if (edge === undefined) {
93
- return await this.fail(runId, workflow.name, mode, current.id, transitionsTaken, 'no-passing-edge');
110
+ return await lifecycle.fail(current.id, transitionsTaken, 'no-passing-edge');
94
111
  }
95
112
 
96
113
  // 5. Persist the edge transition.
97
114
  transitionsTaken += 1;
98
- await this.options.persistence.saveTransition(runId, current.id, edge.to, edge.condition?.kind ?? null);
115
+ await lifecycle.recordTransition(current.id, edge.to, edge.condition?.kind ?? null);
99
116
 
100
117
  // 6. Enforce the iteration bound after taking the transition.
101
118
  if (transitionsTaken > iterationBound) {
102
- return await this.fail(
103
- runId,
104
- workflow.name,
105
- mode,
106
- current.id,
107
- transitionsTaken,
108
- 'iteration-bound-exceeded',
109
- );
119
+ return await lifecycle.fail(current.id, transitionsTaken, 'iteration-bound-exceeded');
110
120
  }
111
121
 
112
122
  // 7. Move to the target node and repeat.
@@ -115,50 +125,6 @@ export class TransitionFlowDriver {
115
125
  current = nextNode;
116
126
  }
117
127
  }
118
-
119
- private async done(
120
- runId: string,
121
- workflowName: string,
122
- mode: 'transition-flow',
123
- finalState: string,
124
- transitionsTaken: number,
125
- ): Promise<WorkflowRunResult> {
126
- await this.options.persistence.savePhase(runId, finalState, 'done');
127
- await this.options.persistence.finalizeRun(runId, 'done', new Date().toISOString());
128
- return { runId, workflowName, mode, status: 'done', finalState, transitionsTaken };
129
- }
130
-
131
- private async fail(
132
- runId: string,
133
- workflowName: string,
134
- mode: 'transition-flow',
135
- finalState: string,
136
- transitionsTaken: number,
137
- reason = 'failed',
138
- ): Promise<WorkflowRunResult> {
139
- await this.options.persistence.savePhase(runId, finalState, 'failed');
140
- await this.options.persistence.finalizeRun(runId, 'failed', new Date().toISOString());
141
- return { runId, workflowName, mode, status: 'failed', finalState, transitionsTaken, reason };
142
- }
143
- }
144
-
145
- /** Built-in bare template values available to transition-flow action options. */
146
- function runtimeBuiltins(
147
- workflowName: string,
148
- nodeId: string,
149
- runId: string,
150
- transitionsTaken: number,
151
- ): Record<string, string | number> {
152
- return {
153
- workflow: workflowName,
154
- runId,
155
- task: workflowName,
156
- state: nodeId,
157
- node: nodeId,
158
- iteration: transitionsTaken,
159
- run: runId,
160
- runtime: 'transition-flow',
161
- };
162
128
  }
163
129
 
164
130
  async function firstPassingEdge(
@@ -172,27 +138,3 @@ async function firstPassingEdge(
172
138
  }
173
139
  return undefined;
174
140
  }
175
-
176
- function allowedEnv(names: readonly string[], source: Record<string, string | undefined>): Record<string, string> {
177
- return Object.fromEntries(
178
- names.flatMap((name) => (source[name] === undefined ? [] : [[name, source[name] as string]])),
179
- );
180
- }
181
-
182
- function runRecord(
183
- runId: string,
184
- workflowName: string,
185
- mode: string,
186
- startedAt: string,
187
- metadata: unknown,
188
- ): WorkflowRunRecord {
189
- return {
190
- id: runId,
191
- workflow_name: workflowName,
192
- mode,
193
- status: 'running',
194
- started_at: startedAt,
195
- completed_at: null,
196
- metadata_json: JSON.stringify(metadata ?? {}),
197
- };
198
- }
package/src/types.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /** Action error handling policy: fail-fast or log-and-continue. */
2
+ export type OnErrorPolicy = 'fail' | 'continue';
3
+
4
+ import type { EventBus } from '@gobing-ai/ts-infra';
5
+ import type { WorkflowEngineEvents } from './events';
6
+
1
7
  /** Workflow execution status persisted for runs and phases. */
2
8
  export type WorkflowStatus = 'running' | 'done' | 'failed';
3
9
 
@@ -13,6 +19,8 @@ export interface Env {
13
19
  export interface ActionDef {
14
20
  readonly kind: string;
15
21
  readonly options?: Record<string, unknown>;
22
+ /** Per-action error policy override. Falls back to workflow default then run-option then 'fail'. */
23
+ readonly onError?: OnErrorPolicy;
16
24
  }
17
25
 
18
26
  /** Guard predicate definition used by state-machine transitions and transition-flow edges. */
@@ -51,6 +59,8 @@ export interface StateMachineWorkflowDef {
51
59
  readonly initialState: string;
52
60
  readonly terminalStates?: readonly string[];
53
61
  readonly iterationBound?: number;
62
+ /** Default error policy applied to actions that don't specify their own. Defaults to 'fail'. */
63
+ readonly defaultOnError?: OnErrorPolicy;
54
64
  readonly vars?: Vars;
55
65
  readonly env?: Env;
56
66
  readonly states: readonly StateDef[];
@@ -86,6 +96,8 @@ export interface TransitionFlowWorkflowDef {
86
96
  readonly initialNode: string;
87
97
  readonly terminalNodes?: readonly string[];
88
98
  readonly iterationBound?: number;
99
+ /** Default error policy applied to actions that don't specify their own. Defaults to 'fail'. */
100
+ readonly defaultOnError?: OnErrorPolicy;
89
101
  readonly vars?: Vars;
90
102
  readonly env?: Env;
91
103
  readonly nodes: readonly FlowNodeDef[];
@@ -140,6 +152,10 @@ export interface WorkflowRunOptions {
140
152
  readonly vars?: Vars;
141
153
  readonly env?: Record<string, string | undefined>;
142
154
  readonly metadata?: Record<string, unknown>;
155
+ /** Optional event bus for structured run observability. */
156
+ readonly events?: EventBus<WorkflowEngineEvents>;
157
+ /** Run-level error policy override. Lowest precedence; action-level wins. */
158
+ readonly onError?: OnErrorPolicy;
143
159
  }
144
160
 
145
161
  /** Result returned by both driver loops. */
package/src/variables.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { WorkflowValidationError } from './errors';
2
- import type { Vars } from './types';
2
+ import type { OnErrorPolicy, Vars } from './types';
3
3
 
4
4
  const TEMPLATE_REF = /\$\{([^}]+)\}/g;
5
5
 
@@ -55,3 +55,15 @@ export function resolveTemplateString(value: string, context: VariableContext):
55
55
  return String(resolved);
56
56
  });
57
57
  }
58
+
59
+ /**
60
+ * Resolve the effective error policy via fixed precedence:
61
+ * `action.onError ?? workflow.defaultOnError ?? runOptions.onError ?? 'fail'`.
62
+ */
63
+ export function resolveOnErrorPolicy(
64
+ actionOnError: OnErrorPolicy | undefined,
65
+ workflowDefault: OnErrorPolicy | undefined,
66
+ runOptionOverride: OnErrorPolicy | undefined,
67
+ ): OnErrorPolicy {
68
+ return actionOnError ?? workflowDefault ?? runOptionOverride ?? 'fail';
69
+ }