@gobing-ai/ts-dual-workflow-engine 0.3.12 → 0.3.14
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 +178 -76
- package/dist/events.d.ts +30 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +5 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence.d.ts +19 -1
- package/dist/persistence.d.ts.map +1 -1
- package/dist/persistence.js +41 -0
- package/dist/run-lifecycle.d.ts +6 -0
- package/dist/run-lifecycle.d.ts.map +1 -1
- package/dist/run-lifecycle.js +68 -14
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.d.ts.map +1 -1
- package/dist/schema-sql.js +17 -2
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +9 -4
- package/dist/transition-flow.d.ts.map +1 -1
- package/dist/transition-flow.js +9 -5
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/events.ts +32 -9
- package/src/host.ts +5 -1
- package/src/index.ts +2 -0
- package/src/persistence.ts +88 -1
- package/src/run-lifecycle.ts +71 -14
- package/src/schema-sql.ts +17 -2
- package/src/state-machine.ts +26 -8
- package/src/transition-flow.ts +23 -12
- package/src/types.ts +28 -0
package/src/persistence.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DbAdapter } from '@gobing-ai/ts-db';
|
|
2
2
|
import { RunCollisionError } from './errors';
|
|
3
3
|
import { WORKFLOW_ENGINE_SCHEMA_SQL } from './schema-sql';
|
|
4
|
-
import type { WorkflowPersistenceAdapter, WorkflowRunRecord, WorkflowStatus } from './types';
|
|
4
|
+
import type { ActionRedactor, WorkflowPersistenceAdapter, WorkflowRunRecord, WorkflowStatus } from './types';
|
|
5
5
|
|
|
6
6
|
/** Apply workflow-engine-owned schema to a database adapter. */
|
|
7
7
|
export async function applyWorkflowEngineSchema(db: DbAdapter): Promise<void> {
|
|
@@ -95,6 +95,49 @@ export class DbWorkflowPersistenceAdapter implements WorkflowPersistenceAdapter
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** Insert a running action row. Returns the row id for later finalization. */
|
|
99
|
+
async saveActionStart(runId: string, node: string, kind: string): Promise<string> {
|
|
100
|
+
const id = crypto.randomUUID();
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
await this.db.run(
|
|
103
|
+
`INSERT INTO action_runs (id, run_id, node, kind, status, started_at, created_at, updated_at)
|
|
104
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
105
|
+
id,
|
|
106
|
+
runId,
|
|
107
|
+
node,
|
|
108
|
+
kind,
|
|
109
|
+
'running',
|
|
110
|
+
new Date(now).toISOString(),
|
|
111
|
+
now,
|
|
112
|
+
now,
|
|
113
|
+
);
|
|
114
|
+
return id;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Finalize an action row with duration, ok flag, and optional redacted result. */
|
|
118
|
+
async saveActionFinalize(
|
|
119
|
+
actionId: string,
|
|
120
|
+
status: WorkflowStatus,
|
|
121
|
+
durationMs: number,
|
|
122
|
+
ok: boolean,
|
|
123
|
+
result?: unknown,
|
|
124
|
+
_redactor?: ActionRedactor,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
await this.db.run(
|
|
128
|
+
`UPDATE action_runs
|
|
129
|
+
SET status = ?, duration_ms = ?, ok = ?, result_json = ?, completed_at = ?, updated_at = ?
|
|
130
|
+
WHERE id = ?`,
|
|
131
|
+
status,
|
|
132
|
+
durationMs,
|
|
133
|
+
ok ? 1 : 0,
|
|
134
|
+
result !== undefined ? JSON.stringify(result) : null,
|
|
135
|
+
new Date(now).toISOString(),
|
|
136
|
+
now,
|
|
137
|
+
actionId,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
98
141
|
/** Load a single run by id. */
|
|
99
142
|
async loadRun(runId: string): Promise<WorkflowRunRecord | undefined> {
|
|
100
143
|
await applyWorkflowEngineSchema(this.db);
|
|
@@ -128,6 +171,50 @@ export class MemoryWorkflowPersistenceAdapter implements WorkflowPersistenceAdap
|
|
|
128
171
|
if (run !== undefined) this.runs.set(runId, { ...run, status, completed_at: completedAt });
|
|
129
172
|
}
|
|
130
173
|
|
|
174
|
+
readonly actionRuns: Array<{
|
|
175
|
+
id: string;
|
|
176
|
+
runId: string;
|
|
177
|
+
node: string;
|
|
178
|
+
kind: string;
|
|
179
|
+
status: WorkflowStatus;
|
|
180
|
+
durationMs: number | null;
|
|
181
|
+
ok: number | null;
|
|
182
|
+
resultJson: string | null;
|
|
183
|
+
}> = [];
|
|
184
|
+
|
|
185
|
+
/** Insert a running action row. */
|
|
186
|
+
async saveActionStart(runId: string, node: string, kind: string): Promise<string> {
|
|
187
|
+
const id = crypto.randomUUID();
|
|
188
|
+
this.actionRuns.push({
|
|
189
|
+
id,
|
|
190
|
+
runId,
|
|
191
|
+
node,
|
|
192
|
+
kind,
|
|
193
|
+
status: 'running',
|
|
194
|
+
durationMs: null,
|
|
195
|
+
ok: null,
|
|
196
|
+
resultJson: null,
|
|
197
|
+
});
|
|
198
|
+
return id;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Finalize an action row. */
|
|
202
|
+
async saveActionFinalize(
|
|
203
|
+
actionId: string,
|
|
204
|
+
status: WorkflowStatus,
|
|
205
|
+
durationMs: number,
|
|
206
|
+
ok: boolean,
|
|
207
|
+
result?: unknown,
|
|
208
|
+
_redactor?: ActionRedactor,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
const row = this.actionRuns.find((a) => a.id === actionId);
|
|
211
|
+
if (row === undefined) return;
|
|
212
|
+
row.status = status;
|
|
213
|
+
row.durationMs = durationMs;
|
|
214
|
+
row.ok = ok ? 1 : 0;
|
|
215
|
+
row.resultJson = result !== undefined ? JSON.stringify(result) : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
131
218
|
/** Save one phase/state execution record. */
|
|
132
219
|
async savePhase(runId: string, phase: string, status: WorkflowStatus): Promise<void> {
|
|
133
220
|
this.phases.push({ runId, phase, status });
|
package/src/run-lifecycle.ts
CHANGED
|
@@ -108,8 +108,18 @@ export class RunLifecycle {
|
|
|
108
108
|
async () => {
|
|
109
109
|
await lifecycle.persistence.createRun(lifecycle.runRecord(options.metadata));
|
|
110
110
|
lifecycle.logger.info('workflow run started');
|
|
111
|
-
addSpanEvent('workflow.run.started', {
|
|
112
|
-
|
|
111
|
+
addSpanEvent('workflow.run.started', {
|
|
112
|
+
workflowName,
|
|
113
|
+
mode,
|
|
114
|
+
runId,
|
|
115
|
+
dryRun: options.dryRun ?? false,
|
|
116
|
+
});
|
|
117
|
+
void lifecycle.events?.emit('workflow.run.started', {
|
|
118
|
+
workflowName,
|
|
119
|
+
mode,
|
|
120
|
+
runId,
|
|
121
|
+
dryRun: options.dryRun ?? false,
|
|
122
|
+
});
|
|
113
123
|
return await loop(lifecycle);
|
|
114
124
|
},
|
|
115
125
|
{ attributes: { 'workflow.name': workflowName, 'workflow.mode': mode, 'workflow.run_id': runId } },
|
|
@@ -120,15 +130,33 @@ export class RunLifecycle {
|
|
|
120
130
|
async enter(stateOrNodeId: string, transitionsTaken: number): Promise<void> {
|
|
121
131
|
await this.persistence.saveWorkflowState(this.runId, stateOrNodeId, { transitionsTaken });
|
|
122
132
|
await this.persistence.savePhase(this.runId, stateOrNodeId, 'running');
|
|
123
|
-
addSpanEvent('workflow.node.enter', {
|
|
124
|
-
|
|
133
|
+
addSpanEvent('workflow.node.enter', {
|
|
134
|
+
runId: this.runId,
|
|
135
|
+
node: stateOrNodeId,
|
|
136
|
+
transitionsTaken,
|
|
137
|
+
});
|
|
138
|
+
void this.events?.emit('workflow.node.enter', {
|
|
139
|
+
runId: this.runId,
|
|
140
|
+
node: stateOrNodeId,
|
|
141
|
+
transitionsTaken,
|
|
142
|
+
});
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
/** Persist a transition and emit its observability event. */
|
|
128
146
|
async recordTransition(from: string, to: string, trigger: string | null): Promise<void> {
|
|
129
147
|
await this.persistence.saveTransition(this.runId, from, to, trigger);
|
|
130
|
-
addSpanEvent('workflow.node.transition', {
|
|
131
|
-
|
|
148
|
+
addSpanEvent('workflow.node.transition', {
|
|
149
|
+
runId: this.runId,
|
|
150
|
+
from,
|
|
151
|
+
to,
|
|
152
|
+
...(trigger === null ? {} : { trigger }),
|
|
153
|
+
});
|
|
154
|
+
void this.events?.emit('workflow.node.transition', {
|
|
155
|
+
runId: this.runId,
|
|
156
|
+
from,
|
|
157
|
+
to,
|
|
158
|
+
trigger,
|
|
159
|
+
});
|
|
132
160
|
}
|
|
133
161
|
|
|
134
162
|
/** Finalize the run as succeeded and return its result. */
|
|
@@ -136,8 +164,8 @@ export class RunLifecycle {
|
|
|
136
164
|
await this.persistence.savePhase(this.runId, finalState, 'done');
|
|
137
165
|
await this.persistence.finalizeRun(this.runId, 'done', new Date().toISOString());
|
|
138
166
|
this.logger.info('workflow run done', { finalState, transitionsTaken });
|
|
139
|
-
addSpanEvent('workflow.run.done', { finalState, transitionsTaken });
|
|
140
|
-
void this.events?.emit('workflow.run.done', { finalState, transitionsTaken });
|
|
167
|
+
addSpanEvent('workflow.run.done', { runId: this.runId, finalState, transitionsTaken });
|
|
168
|
+
void this.events?.emit('workflow.run.done', { runId: this.runId, finalState, transitionsTaken });
|
|
141
169
|
return this.result('done', finalState, transitionsTaken);
|
|
142
170
|
}
|
|
143
171
|
|
|
@@ -145,32 +173,40 @@ export class RunLifecycle {
|
|
|
145
173
|
async fail(finalState: string, transitionsTaken: number, reason = 'failed'): Promise<WorkflowRunResult> {
|
|
146
174
|
await this.persistence.savePhase(this.runId, finalState, 'failed');
|
|
147
175
|
await this.persistence.finalizeRun(this.runId, 'failed', new Date().toISOString());
|
|
148
|
-
addSpanEvent('workflow.run.failed', { finalState, reason });
|
|
149
|
-
void this.events?.emit('workflow.run.failed', { finalState, reason });
|
|
176
|
+
addSpanEvent('workflow.run.failed', { runId: this.runId, finalState, reason });
|
|
177
|
+
void this.events?.emit('workflow.run.failed', { runId: this.runId, finalState, reason });
|
|
150
178
|
this.logger.warn('workflow run failed', { finalState, transitionsTaken, reason });
|
|
151
179
|
return this.result('failed', finalState, transitionsTaken, reason);
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
/** Emit action-level observability before a host action is invoked. */
|
|
155
183
|
actionStart(stateOrNodeId: string, kind: string): void {
|
|
156
|
-
addSpanEvent('workflow.action.start', { node: stateOrNodeId, kind });
|
|
157
|
-
void this.events?.emit('workflow.action.start', { node: stateOrNodeId, kind });
|
|
184
|
+
addSpanEvent('workflow.action.start', { runId: this.runId, node: stateOrNodeId, kind });
|
|
185
|
+
void this.events?.emit('workflow.action.start', { runId: this.runId, node: stateOrNodeId, kind });
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
/** Emit action-level observability after a host action settles. */
|
|
161
189
|
actionDone(stateOrNodeId: string, kind: string, durationMs: number, ok: boolean): void {
|
|
162
|
-
addSpanEvent('workflow.action.done', { node: stateOrNodeId, kind, durationMs, ok });
|
|
163
|
-
void this.events?.emit('workflow.action.done', {
|
|
190
|
+
addSpanEvent('workflow.action.done', { runId: this.runId, node: stateOrNodeId, kind, durationMs, ok });
|
|
191
|
+
void this.events?.emit('workflow.action.done', {
|
|
192
|
+
runId: this.runId,
|
|
193
|
+
node: stateOrNodeId,
|
|
194
|
+
kind,
|
|
195
|
+
durationMs,
|
|
196
|
+
ok,
|
|
197
|
+
});
|
|
164
198
|
}
|
|
165
199
|
|
|
166
200
|
/** Log and trace a non-fatal action failure for the 'continue' error policy (ADR-013 observability seam). */
|
|
167
201
|
warnActionFailed(stateOrNodeId: string, transitionsTaken: number, error?: string): void {
|
|
168
202
|
addSpanEvent('workflow.action.failed_continue', {
|
|
203
|
+
runId: this.runId,
|
|
169
204
|
node: stateOrNodeId,
|
|
170
205
|
transitionsTaken,
|
|
171
206
|
...(error === undefined ? {} : { error }),
|
|
172
207
|
});
|
|
173
208
|
void this.events?.emit('workflow.action.failed_continue', {
|
|
209
|
+
runId: this.runId,
|
|
174
210
|
node: stateOrNodeId,
|
|
175
211
|
transitionsTaken,
|
|
176
212
|
...(error === undefined ? {} : { error }),
|
|
@@ -178,6 +214,27 @@ export class RunLifecycle {
|
|
|
178
214
|
this.logger.warn('action failed (continuing)', { node: stateOrNodeId, transitionsTaken, error });
|
|
179
215
|
}
|
|
180
216
|
|
|
217
|
+
/** Emit a guard evaluation result (fired for every guard, including rejected). */
|
|
218
|
+
guardEvaluated(from: string, to: string, kind: string, passed: boolean): void {
|
|
219
|
+
void this.events?.emit('workflow.guard.evaluated', {
|
|
220
|
+
runId: this.runId,
|
|
221
|
+
from,
|
|
222
|
+
to,
|
|
223
|
+
kind,
|
|
224
|
+
passed,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Emit when an interactive HITL prompt is presented. */
|
|
229
|
+
hitlAsk(node: string, kind: string, message: string): void {
|
|
230
|
+
void this.events?.emit('workflow.hitl.ask', { runId: this.runId, node, kind, message });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Emit when an interactive HITL prompt resolves. */
|
|
234
|
+
hitlResponse(node: string, ok: boolean): void {
|
|
235
|
+
void this.events?.emit('workflow.hitl.response', { runId: this.runId, node, ok });
|
|
236
|
+
}
|
|
237
|
+
|
|
181
238
|
private result(
|
|
182
239
|
status: WorkflowStatus,
|
|
183
240
|
finalState: string,
|
package/src/schema-sql.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** SQL DDL for the dual-workflow engine's persistent schema — runs, phase_runs, transition_runs, and
|
|
1
|
+
/** SQL DDL for the dual-workflow engine's persistent schema — runs, phase_runs, transition_runs, workflow_states, and action_runs tables. */
|
|
2
2
|
export const WORKFLOW_ENGINE_SCHEMA_SQL = `
|
|
3
3
|
CREATE TABLE IF NOT EXISTS runs (
|
|
4
4
|
id TEXT PRIMARY KEY,
|
|
@@ -36,7 +36,6 @@ CREATE TABLE IF NOT EXISTS transition_runs (
|
|
|
36
36
|
updated_at INTEGER NOT NULL DEFAULT 0,
|
|
37
37
|
FOREIGN KEY (run_id) REFERENCES runs(id)
|
|
38
38
|
);
|
|
39
|
-
|
|
40
39
|
CREATE TABLE IF NOT EXISTS workflow_states (
|
|
41
40
|
id TEXT PRIMARY KEY,
|
|
42
41
|
run_id TEXT NOT NULL,
|
|
@@ -46,4 +45,20 @@ CREATE TABLE IF NOT EXISTS workflow_states (
|
|
|
46
45
|
updated_at INTEGER NOT NULL DEFAULT 0,
|
|
47
46
|
FOREIGN KEY (run_id) REFERENCES runs(id)
|
|
48
47
|
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS action_runs (
|
|
50
|
+
id TEXT PRIMARY KEY,
|
|
51
|
+
run_id TEXT NOT NULL,
|
|
52
|
+
node TEXT NOT NULL,
|
|
53
|
+
kind TEXT NOT NULL,
|
|
54
|
+
status TEXT NOT NULL,
|
|
55
|
+
duration_ms INTEGER,
|
|
56
|
+
ok INTEGER,
|
|
57
|
+
result_json TEXT,
|
|
58
|
+
started_at TEXT,
|
|
59
|
+
completed_at TEXT,
|
|
60
|
+
created_at INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
updated_at INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
FOREIGN KEY (run_id) REFERENCES runs(id)
|
|
63
|
+
);
|
|
49
64
|
`.trim();
|
package/src/state-machine.ts
CHANGED
|
@@ -88,12 +88,18 @@ export class StateMachineDriver {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// 5. Evaluate transition guards in declaration order and pick the first passing transition.
|
|
91
|
-
const nextTransition = await firstPassingTransition(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
const nextTransition = await firstPassingTransition(
|
|
92
|
+
outbound,
|
|
93
|
+
this.options.host,
|
|
94
|
+
{
|
|
95
|
+
runId,
|
|
96
|
+
current: current.id,
|
|
97
|
+
vars,
|
|
98
|
+
lastActionResult,
|
|
99
|
+
},
|
|
100
|
+
lifecycle,
|
|
101
|
+
);
|
|
102
|
+
|
|
97
103
|
if (nextTransition === undefined) {
|
|
98
104
|
return await lifecycle.fail(current.id, transitionsTaken, 'no-passing-transition');
|
|
99
105
|
}
|
|
@@ -156,6 +162,7 @@ export class StateMachineDriver {
|
|
|
156
162
|
env,
|
|
157
163
|
builtins: runtimeBuiltins(workflowName, stateId, runId, transitionsTaken, 'state-machine'),
|
|
158
164
|
});
|
|
165
|
+
const actionId = await this.options.persistence.saveActionStart(runId, stateId, action.kind);
|
|
159
166
|
const actionStartMs = Date.now();
|
|
160
167
|
lifecycle.actionStart(stateId, action.kind);
|
|
161
168
|
try {
|
|
@@ -169,7 +176,15 @@ export class StateMachineDriver {
|
|
|
169
176
|
events: options.events,
|
|
170
177
|
});
|
|
171
178
|
} finally {
|
|
172
|
-
|
|
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
|
+
);
|
|
173
188
|
}
|
|
174
189
|
if (last.terminal === true) return { outcome: 'terminal', result: last };
|
|
175
190
|
if (!last.ok) {
|
|
@@ -192,10 +207,13 @@ async function firstPassingTransition(
|
|
|
192
207
|
transitions: StateMachineWorkflowDef['transitions'],
|
|
193
208
|
host: WorkflowEngineHost,
|
|
194
209
|
context: Parameters<WorkflowEngineHost['evaluateGuard']>[2],
|
|
210
|
+
lifecycle: RunLifecycle,
|
|
195
211
|
): Promise<StateMachineWorkflowDef['transitions'][number] | undefined> {
|
|
196
212
|
for (const transition of transitions) {
|
|
197
213
|
if (transition.guard === undefined) return transition;
|
|
198
|
-
|
|
214
|
+
const passed = await host.evaluateGuard(transition.guard.kind, transition.guard.options ?? {}, context);
|
|
215
|
+
lifecycle.guardEvaluated(context.current, transition.to, transition.guard.kind, passed);
|
|
216
|
+
if (passed) return transition;
|
|
199
217
|
}
|
|
200
218
|
return undefined;
|
|
201
219
|
}
|
package/src/transition-flow.ts
CHANGED
|
@@ -57,7 +57,6 @@ export class TransitionFlowDriver {
|
|
|
57
57
|
|
|
58
58
|
// 2. Execute the node action when one is configured (skipped in dry-run).
|
|
59
59
|
if (options.dryRun) {
|
|
60
|
-
// dry-run: skip action execution, continue to next node
|
|
61
60
|
if (current.action !== undefined) {
|
|
62
61
|
lastActionResult = undefined;
|
|
63
62
|
}
|
|
@@ -67,6 +66,7 @@ export class TransitionFlowDriver {
|
|
|
67
66
|
env,
|
|
68
67
|
builtins: runtimeBuiltins(workflow.name, current.id, runId, transitionsTaken, 'transition-flow'),
|
|
69
68
|
});
|
|
69
|
+
const actionId = await this.options.persistence.saveActionStart(runId, current.id, current.action.kind);
|
|
70
70
|
const actionStartMs = Date.now();
|
|
71
71
|
lifecycle.actionStart(current.id, current.action.kind);
|
|
72
72
|
try {
|
|
@@ -80,11 +80,14 @@ export class TransitionFlowDriver {
|
|
|
80
80
|
events: options.events,
|
|
81
81
|
});
|
|
82
82
|
} finally {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
|
87
89
|
lastActionResult?.ok ?? false,
|
|
90
|
+
lastActionResult,
|
|
88
91
|
);
|
|
89
92
|
}
|
|
90
93
|
if (lastActionResult.setVars) vars = mergeSetVars(vars, lastActionResult.setVars);
|
|
@@ -107,12 +110,17 @@ export class TransitionFlowDriver {
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
// 4. Evaluate edge conditions in declaration order and pick the first passing edge.
|
|
110
|
-
const edge = await firstPassingEdge(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
const edge = await firstPassingEdge(
|
|
114
|
+
outbound,
|
|
115
|
+
this.options.host,
|
|
116
|
+
{
|
|
117
|
+
runId,
|
|
118
|
+
current: current.id,
|
|
119
|
+
vars,
|
|
120
|
+
lastActionResult,
|
|
121
|
+
},
|
|
122
|
+
lifecycle,
|
|
123
|
+
);
|
|
116
124
|
if (edge === undefined) {
|
|
117
125
|
return await lifecycle.fail(current.id, transitionsTaken, 'no-passing-edge');
|
|
118
126
|
}
|
|
@@ -138,10 +146,13 @@ async function firstPassingEdge(
|
|
|
138
146
|
edges: TransitionFlowWorkflowDef['edges'],
|
|
139
147
|
host: WorkflowEngineHost,
|
|
140
148
|
context: Parameters<WorkflowEngineHost['evaluateGuard']>[2],
|
|
149
|
+
lifecycle: RunLifecycle,
|
|
141
150
|
): Promise<TransitionFlowWorkflowDef['edges'][number] | undefined> {
|
|
142
151
|
for (const edge of edges) {
|
|
143
152
|
if (edge.condition === undefined) return edge;
|
|
144
|
-
|
|
153
|
+
const passed = await host.evaluateGuard(edge.condition.kind, edge.condition.options ?? {}, context);
|
|
154
|
+
lifecycle.guardEvaluated(context.current, edge.to, edge.condition.kind, passed);
|
|
155
|
+
if (passed) return edge;
|
|
145
156
|
}
|
|
146
157
|
return undefined;
|
|
147
158
|
}
|
package/src/types.ts
CHANGED
|
@@ -186,6 +186,23 @@ export interface WorkflowRunRecord {
|
|
|
186
186
|
readonly metadata_json: string;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/** Persisted action run record — one row per action executed in a workflow run. */
|
|
190
|
+
export interface ActionRunRecord {
|
|
191
|
+
readonly id: string;
|
|
192
|
+
readonly run_id: string;
|
|
193
|
+
readonly node: string;
|
|
194
|
+
readonly kind: string;
|
|
195
|
+
readonly status: WorkflowStatus;
|
|
196
|
+
readonly duration_ms: number | null;
|
|
197
|
+
readonly ok: number | null;
|
|
198
|
+
readonly result_json: string | null;
|
|
199
|
+
readonly started_at: string | null;
|
|
200
|
+
readonly completed_at: string | null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Optional redaction hook: given action options, return sanitized options for persistence. */
|
|
204
|
+
export type ActionRedactor = (kind: string, options: Record<string, unknown>) => Record<string, unknown>;
|
|
205
|
+
|
|
189
206
|
/** Persistence adapter implemented by DB-backed and test stores. */
|
|
190
207
|
export interface WorkflowPersistenceAdapter {
|
|
191
208
|
createRun(record: WorkflowRunRecord): Promise<void>;
|
|
@@ -193,6 +210,17 @@ export interface WorkflowPersistenceAdapter {
|
|
|
193
210
|
savePhase(runId: string, phase: string, status: WorkflowStatus): Promise<void>;
|
|
194
211
|
saveTransition(runId: string, from: string, to: string, trigger: string | null): Promise<void>;
|
|
195
212
|
saveWorkflowState(runId: string, state: string, data: Record<string, unknown>): Promise<void>;
|
|
213
|
+
/** Two-phase action persistence: insert a running row at action start. Returns the action row id. */
|
|
214
|
+
saveActionStart(runId: string, node: string, kind: string): Promise<string>;
|
|
215
|
+
/** Finalize an action row with duration, ok, result. */
|
|
216
|
+
saveActionFinalize(
|
|
217
|
+
actionId: string,
|
|
218
|
+
status: WorkflowStatus,
|
|
219
|
+
durationMs: number,
|
|
220
|
+
ok: boolean,
|
|
221
|
+
result?: unknown,
|
|
222
|
+
redactor?: ActionRedactor,
|
|
223
|
+
): Promise<void>;
|
|
196
224
|
loadRun(runId: string): Promise<WorkflowRunRecord | undefined>;
|
|
197
225
|
listRuns(): Promise<readonly WorkflowRunRecord[]>;
|
|
198
226
|
}
|