@gobing-ai/ts-dual-workflow-engine 0.2.7 → 0.2.8
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 +271 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -9
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -0
- package/package.json +4 -3
- package/schemas/state-machine-workflow.schema.json +71 -0
- package/schemas/transition-flow-workflow.schema.json +70 -0
- package/src/config.ts +23 -12
- package/src/schema.ts +2 -0
package/README.md
CHANGED
|
@@ -1,4 +1,274 @@
|
|
|
1
1
|
# @gobing-ai/ts-dual-workflow-engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
State-machine and transition-flow workflow runtime with pluggable action runners, guard runners, and memory or database persistence.
|
|
4
4
|
|
|
5
|
+
## What It Provides
|
|
6
|
+
|
|
7
|
+
`ts-dual-workflow-engine` runs declarative workflows in two execution modes:
|
|
8
|
+
|
|
9
|
+
| Mode | Use When |
|
|
10
|
+
|------|----------|
|
|
11
|
+
| `state-machine` | A run owns one current state and chooses the next state by evaluating ordered transition guards |
|
|
12
|
+
| `transition-flow` | A run moves through nodes and edges in a DAG-like flow, executing node actions as it advances |
|
|
13
|
+
|
|
14
|
+
The package exposes:
|
|
15
|
+
|
|
16
|
+
| Export | Purpose |
|
|
17
|
+
|--------|---------|
|
|
18
|
+
| `WorkflowService` | High-level loader and runner for both workflow kinds |
|
|
19
|
+
| `StateMachineDriver` | Direct state-machine execution |
|
|
20
|
+
| `TransitionFlowDriver` | Direct transition-flow execution |
|
|
21
|
+
| `WorkflowEngineHost` | Registry for action runners and guard runners |
|
|
22
|
+
| `MemoryWorkflowPersistenceAdapter` | In-memory persistence for tests and short-lived runs |
|
|
23
|
+
| `DbWorkflowPersistenceAdapter` | DB-backed persistence over `@gobing-ai/ts-db` |
|
|
24
|
+
| `loadWorkflowDef()` / `loadWorkflowDefFromText()` | YAML workflow loading and validation |
|
|
25
|
+
| `applyWorkflowEngineSchema()` | Installs the package-owned DB schema |
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun add @gobing-ai/ts-dual-workflow-engine @gobing-ai/ts-db
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use `@gobing-ai/ts-db` only when you need durable workflow history. Memory persistence has no database requirement.
|
|
34
|
+
|
|
35
|
+
## State Machine Example
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import {
|
|
39
|
+
MemoryWorkflowPersistenceAdapter,
|
|
40
|
+
StateMachineDriver,
|
|
41
|
+
WorkflowEngineHost,
|
|
42
|
+
type ActionRunner,
|
|
43
|
+
} from '@gobing-ai/ts-dual-workflow-engine';
|
|
44
|
+
|
|
45
|
+
const captureAction: ActionRunner = {
|
|
46
|
+
kind: 'capture',
|
|
47
|
+
async execute(options) {
|
|
48
|
+
console.log(options.message);
|
|
49
|
+
return { ok: true };
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const host = new WorkflowEngineHost()
|
|
54
|
+
.registerAction(captureAction)
|
|
55
|
+
.registerGuard({ kind: 'always', evaluate: async () => true });
|
|
56
|
+
|
|
57
|
+
const driver = new StateMachineDriver({
|
|
58
|
+
host,
|
|
59
|
+
persistence: new MemoryWorkflowPersistenceAdapter(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = await driver.run(
|
|
63
|
+
{
|
|
64
|
+
name: 'approval',
|
|
65
|
+
initialState: 'draft',
|
|
66
|
+
terminalStates: ['done'],
|
|
67
|
+
vars: { message: 'approved' },
|
|
68
|
+
states: [
|
|
69
|
+
{ id: 'draft', onEnter: [{ kind: 'capture', options: { message: '${vars.message}' } }] },
|
|
70
|
+
{ id: 'done' },
|
|
71
|
+
],
|
|
72
|
+
transitions: [{ from: 'draft', to: 'done', guard: { kind: 'always' } }],
|
|
73
|
+
},
|
|
74
|
+
{ runId: 'approval-1' },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
console.log(result.status, result.finalState);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The driver persists each state snapshot, phase update, transition, and final run status through the configured persistence adapter.
|
|
81
|
+
|
|
82
|
+
## Transition Flow Example
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import {
|
|
86
|
+
createDefaultWorkflowEngineHost,
|
|
87
|
+
MemoryWorkflowPersistenceAdapter,
|
|
88
|
+
WorkflowService,
|
|
89
|
+
} from '@gobing-ai/ts-dual-workflow-engine';
|
|
90
|
+
|
|
91
|
+
const service = new WorkflowService(
|
|
92
|
+
createDefaultWorkflowEngineHost(),
|
|
93
|
+
new MemoryWorkflowPersistenceAdapter(),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const result = await service.run({
|
|
97
|
+
kind: 'transition-flow',
|
|
98
|
+
name: 'linear-flow',
|
|
99
|
+
initialNode: 'start',
|
|
100
|
+
terminalNodes: ['done'],
|
|
101
|
+
nodes: [
|
|
102
|
+
{ id: 'start', action: { kind: 'note', options: { message: 'started' } } },
|
|
103
|
+
{ id: 'done' },
|
|
104
|
+
],
|
|
105
|
+
edges: [{ from: 'start', to: 'done' }],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log(result);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The default host includes built-in `note` and `shell` action runners plus an `always` guard. For production systems, register domain-specific runners and keep shell execution explicit.
|
|
112
|
+
|
|
113
|
+
## Load Workflows from YAML
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { loadWorkflowDef, WorkflowService } from '@gobing-ai/ts-dual-workflow-engine';
|
|
117
|
+
|
|
118
|
+
const workflow = await loadWorkflowDef('./workflows/approval.yaml');
|
|
119
|
+
await service.run(workflow, { runId: 'approval-1' });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`loadWorkflowDef(path)` reads YAML or JSON from disk. File loads honor a top-level `$schema` ref by default, then validate the internal structural schema and semantic references before returning a `WorkflowDef`. The `$schema` value resolves from the bundled package schema (shipped under `node_modules/@gobing-ai/ts-dual-workflow-engine/schemas/`) — no network access; quote the value, since YAML treats a leading `@` as reserved. Relative paths and (opt-in) remote URLs also work; see `@gobing-ai/ts-runtime` → *Structured config*. `loadWorkflowDefFromText(text, source)` handles inline definitions with internal validation only.
|
|
123
|
+
|
|
124
|
+
### State-machine YAML
|
|
125
|
+
|
|
126
|
+
`kind: state-machine` is optional because state-machine is the default shape, but including it makes the file easier to scan.
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
# workflows/approval.yaml
|
|
130
|
+
$schema: "@gobing-ai/ts-dual-workflow-engine/schemas/state-machine-workflow.schema.json"
|
|
131
|
+
kind: state-machine
|
|
132
|
+
name: approval
|
|
133
|
+
initialState: draft
|
|
134
|
+
terminalStates: [done]
|
|
135
|
+
vars:
|
|
136
|
+
reviewer: robin
|
|
137
|
+
env:
|
|
138
|
+
allow: [APP_ENV]
|
|
139
|
+
states:
|
|
140
|
+
- id: draft
|
|
141
|
+
onEnter:
|
|
142
|
+
- kind: note
|
|
143
|
+
options:
|
|
144
|
+
message: "review requested by ${vars.reviewer} in ${env.APP_ENV}"
|
|
145
|
+
- id: approved
|
|
146
|
+
onEnter:
|
|
147
|
+
- kind: note
|
|
148
|
+
options:
|
|
149
|
+
message: approved
|
|
150
|
+
- id: done
|
|
151
|
+
transitions:
|
|
152
|
+
- from: draft
|
|
153
|
+
to: approved
|
|
154
|
+
guard:
|
|
155
|
+
kind: always
|
|
156
|
+
- from: approved
|
|
157
|
+
to: done
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import {
|
|
162
|
+
createDefaultWorkflowEngineHost,
|
|
163
|
+
loadWorkflowDef,
|
|
164
|
+
MemoryWorkflowPersistenceAdapter,
|
|
165
|
+
WorkflowService,
|
|
166
|
+
} from '@gobing-ai/ts-dual-workflow-engine';
|
|
167
|
+
|
|
168
|
+
const service = new WorkflowService(
|
|
169
|
+
createDefaultWorkflowEngineHost(),
|
|
170
|
+
new MemoryWorkflowPersistenceAdapter(),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const workflow = await loadWorkflowDef('./workflows/approval.yaml');
|
|
174
|
+
const result = await service.run(workflow, {
|
|
175
|
+
runId: 'approval-1',
|
|
176
|
+
env: { APP_ENV: 'development' },
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Transition-flow YAML
|
|
181
|
+
|
|
182
|
+
Transition-flow definitions must declare `kind: transition-flow`.
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
# workflows/import-file.yaml
|
|
186
|
+
$schema: "@gobing-ai/ts-dual-workflow-engine/schemas/transition-flow-workflow.schema.json"
|
|
187
|
+
kind: transition-flow
|
|
188
|
+
name: import-file
|
|
189
|
+
initialNode: read
|
|
190
|
+
terminalNodes: [done]
|
|
191
|
+
vars:
|
|
192
|
+
file: events.jsonl
|
|
193
|
+
nodes:
|
|
194
|
+
- id: read
|
|
195
|
+
type: action
|
|
196
|
+
action:
|
|
197
|
+
kind: note
|
|
198
|
+
options:
|
|
199
|
+
message: "reading ${vars.file}"
|
|
200
|
+
- id: validate
|
|
201
|
+
type: gate
|
|
202
|
+
- id: done
|
|
203
|
+
edges:
|
|
204
|
+
- from: read
|
|
205
|
+
to: validate
|
|
206
|
+
- from: validate
|
|
207
|
+
to: done
|
|
208
|
+
condition:
|
|
209
|
+
kind: always
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const workflow = await loadWorkflowDef('./workflows/import-file.yaml');
|
|
214
|
+
const result = await service.run(workflow, {
|
|
215
|
+
runId: 'import-1',
|
|
216
|
+
vars: { file: 'override.jsonl' },
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`validateWorkflowDef()` is available when the caller already has an object and only needs validation.
|
|
221
|
+
|
|
222
|
+
## Variables and Environment
|
|
223
|
+
|
|
224
|
+
Actions receive resolved template values. The engine supports:
|
|
225
|
+
|
|
226
|
+
| Template | Source |
|
|
227
|
+
|----------|--------|
|
|
228
|
+
| `${vars.name}` | Workflow vars merged with run vars |
|
|
229
|
+
| `${env.NAME}` | Environment values explicitly allowed by workflow config |
|
|
230
|
+
| `${runId}` | Current run ID |
|
|
231
|
+
| `${workflow}` | Workflow name |
|
|
232
|
+
| `${state}` | Current state or node ID |
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
await service.run(workflow, {
|
|
236
|
+
vars: { file: 'events.jsonl' },
|
|
237
|
+
env: { API_TOKEN: process.env.API_TOKEN },
|
|
238
|
+
metadata: { requestedBy: 'scheduler' },
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The workflow definition controls which environment names are visible through `env.allow`.
|
|
243
|
+
|
|
244
|
+
## DB Persistence
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { createDbAdapter } from '@gobing-ai/ts-db';
|
|
248
|
+
import {
|
|
249
|
+
applyWorkflowEngineSchema,
|
|
250
|
+
createDefaultWorkflowEngineHost,
|
|
251
|
+
DbWorkflowPersistenceAdapter,
|
|
252
|
+
WorkflowService,
|
|
253
|
+
} from '@gobing-ai/ts-dual-workflow-engine';
|
|
254
|
+
|
|
255
|
+
const db = await createDbAdapter({ driver: 'bun-sqlite', url: './workflow.db' });
|
|
256
|
+
await applyWorkflowEngineSchema(db);
|
|
257
|
+
|
|
258
|
+
const service = new WorkflowService(
|
|
259
|
+
createDefaultWorkflowEngineHost(),
|
|
260
|
+
new DbWorkflowPersistenceAdapter(db),
|
|
261
|
+
);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Use `service.listRuns()` to read persisted run records. The adapter stores run status, phase snapshots, state snapshots, and transitions.
|
|
265
|
+
|
|
266
|
+
## Error Handling
|
|
267
|
+
|
|
268
|
+
Validation failures throw `WorkflowValidationError`. Runtime finite-state-machine errors throw `FSMError`. Run failures caused by actions or guards are returned as `WorkflowRunResult` with `status: 'failed'`, preserving the run record.
|
|
269
|
+
|
|
270
|
+
## Boundary Notes
|
|
271
|
+
|
|
272
|
+
- The engine executes workflows; it does not provide a scheduler. Use `@gobing-ai/ts-infra` scheduler or an external cron trigger to start runs.
|
|
273
|
+
- Persistence is adapter-based. Downstream apps own DB lifecycle and migration ordering.
|
|
274
|
+
- Action and guard runners are the extension points. Keep domain behavior there, not in workflow parsing.
|
package/dist/config.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { WorkflowDef } from './types';
|
|
2
|
+
export interface WorkflowLoadOptions {
|
|
3
|
+
/** When true, honor a top-level `$schema` ref. Defaults to true for file loads. */
|
|
4
|
+
validateSchema?: boolean;
|
|
5
|
+
/** Optional fetch implementation for remote HTTP(S) schema refs. */
|
|
6
|
+
fetch?: (input: string) => Promise<Response>;
|
|
7
|
+
}
|
|
2
8
|
/** Load a workflow definition from YAML or JSON text. */
|
|
3
9
|
export declare function loadWorkflowDefFromText(text: string, source?: string): WorkflowDef;
|
|
4
10
|
/** Load a workflow definition from a filesystem path. */
|
|
5
|
-
export declare function loadWorkflowDef(path: string): Promise<WorkflowDef>;
|
|
11
|
+
export declare function loadWorkflowDef(path: string, options?: WorkflowLoadOptions): Promise<WorkflowDef>;
|
|
6
12
|
/** Validate semantic workflow invariants beyond structural Zod checks. */
|
|
7
13
|
export declare function validateWorkflowDef(workflow: WorkflowDef): void;
|
|
8
14
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,yDAAyD;AACzD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAa,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAChC,mFAAmF;IACnF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oEAAoE;IACpE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChD;AAED,yDAAyD;AACzD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAa,GAAG,WAAW,CAGtF;AAED,yDAAyD;AACzD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3G;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAM/D"}
|
package/dist/config.js
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadStructuredConfig } from '@gobing-ai/ts-runtime';
|
|
2
2
|
import { parse } from 'yaml';
|
|
3
3
|
import { WorkflowValidationError } from './errors.js';
|
|
4
4
|
import { WorkflowDefSchema } from './schema.js';
|
|
5
5
|
/** Load a workflow definition from YAML or JSON text. */
|
|
6
6
|
export function loadWorkflowDefFromText(text, source = '<inline>') {
|
|
7
7
|
const parsed = source.endsWith('.json') ? JSON.parse(text) : parse(text);
|
|
8
|
-
|
|
9
|
-
if (!result.success) {
|
|
10
|
-
throw new WorkflowValidationError(`Workflow definition failed schema validation for ${source}: ${result.error.issues.map((issue) => issue.message).join('; ')}`, result.error.issues);
|
|
11
|
-
}
|
|
12
|
-
validateWorkflowDef(result.data);
|
|
13
|
-
return result.data;
|
|
8
|
+
return parseWorkflowDef(parsed, source);
|
|
14
9
|
}
|
|
15
10
|
/** Load a workflow definition from a filesystem path. */
|
|
16
|
-
export async function loadWorkflowDef(path) {
|
|
17
|
-
return
|
|
11
|
+
export async function loadWorkflowDef(path, options = {}) {
|
|
12
|
+
return parseWorkflowDef(await loadStructuredConfig(path, options), path);
|
|
18
13
|
}
|
|
19
14
|
/** Validate semantic workflow invariants beyond structural Zod checks. */
|
|
20
15
|
export function validateWorkflowDef(workflow) {
|
|
@@ -37,6 +32,14 @@ function validateStateMachine(workflow) {
|
|
|
37
32
|
throw new WorkflowValidationError(`Transition target "${transition.to}" is not declared`);
|
|
38
33
|
}
|
|
39
34
|
}
|
|
35
|
+
function parseWorkflowDef(parsed, source) {
|
|
36
|
+
const result = WorkflowDefSchema.safeParse(parsed);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
throw new WorkflowValidationError(`Workflow definition failed schema validation for ${source}: ${result.error.issues.map((issue) => issue.message).join('; ')}`, result.error.issues);
|
|
39
|
+
}
|
|
40
|
+
validateWorkflowDef(result.data);
|
|
41
|
+
return result.data;
|
|
42
|
+
}
|
|
40
43
|
function validateTransitionFlow(workflow) {
|
|
41
44
|
const nodes = new Set(workflow.nodes.map((node) => node.id));
|
|
42
45
|
if (!nodes.has(workflow.initialNode)) {
|
package/dist/schema.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare const GuardDefSchema: z.ZodObject<{
|
|
|
11
11
|
}, z.core.$strip>;
|
|
12
12
|
/** Zod schema for state-machine workflow definitions. */
|
|
13
13
|
export declare const StateMachineWorkflowDefSchema: z.ZodObject<{
|
|
14
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
14
15
|
kind: z.ZodOptional<z.ZodLiteral<"state-machine">>;
|
|
15
16
|
name: z.ZodString;
|
|
16
17
|
initialState: z.ZodString;
|
|
@@ -43,6 +44,7 @@ export declare const StateMachineWorkflowDefSchema: z.ZodObject<{
|
|
|
43
44
|
}, z.core.$strip>;
|
|
44
45
|
/** Zod schema for transition-flow workflow definitions. */
|
|
45
46
|
export declare const TransitionFlowWorkflowDefSchema: z.ZodObject<{
|
|
47
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
46
48
|
kind: z.ZodLiteral<"transition-flow">;
|
|
47
49
|
name: z.ZodString;
|
|
48
50
|
initialNode: z.ZodString;
|
|
@@ -76,6 +78,7 @@ export declare const TransitionFlowWorkflowDefSchema: z.ZodObject<{
|
|
|
76
78
|
}, z.core.$strip>;
|
|
77
79
|
/** Zod schema for either supported workflow definition shape. */
|
|
78
80
|
export declare const WorkflowDefSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
81
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
79
82
|
kind: z.ZodOptional<z.ZodLiteral<"state-machine">>;
|
|
80
83
|
name: z.ZodString;
|
|
81
84
|
initialState: z.ZodString;
|
|
@@ -106,6 +109,7 @@ export declare const WorkflowDefSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
106
109
|
}, z.core.$strip>>;
|
|
107
110
|
}, z.core.$strip>>;
|
|
108
111
|
}, z.core.$strip>, z.ZodObject<{
|
|
112
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
109
113
|
kind: z.ZodLiteral<"transition-flow">;
|
|
110
114
|
name: z.ZodString;
|
|
111
115
|
initialNode: z.ZodString;
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,kDAAkD;AAClD,eAAO,MAAM,eAAe;;;iBAG1B,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,cAAc;;;iBAGzB,CAAC;AAEH,yDAAyD;AACzD,eAAO,MAAM,6BAA6B
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,kDAAkD;AAClD,eAAO,MAAM,eAAe;;;iBAG1B,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,cAAc;;;iBAGzB,CAAC;AAEH,yDAAyD;AACzD,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwBxC,CAAC;AAEH,2DAA2D;AAC3D,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuB1C,CAAC;AAEH,iEAAiE;AACjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAA4E,CAAC"}
|
package/dist/schema.js
CHANGED
|
@@ -11,6 +11,7 @@ export const GuardDefSchema = z.object({
|
|
|
11
11
|
});
|
|
12
12
|
/** Zod schema for state-machine workflow definitions. */
|
|
13
13
|
export const StateMachineWorkflowDefSchema = z.object({
|
|
14
|
+
$schema: z.string().optional(),
|
|
14
15
|
kind: z.literal('state-machine').optional(),
|
|
15
16
|
name: z.string().min(1),
|
|
16
17
|
initialState: z.string().min(1),
|
|
@@ -32,6 +33,7 @@ export const StateMachineWorkflowDefSchema = z.object({
|
|
|
32
33
|
});
|
|
33
34
|
/** Zod schema for transition-flow workflow definitions. */
|
|
34
35
|
export const TransitionFlowWorkflowDefSchema = z.object({
|
|
36
|
+
$schema: z.string().optional(),
|
|
35
37
|
kind: z.literal('transition-flow'),
|
|
36
38
|
name: z.string().min(1),
|
|
37
39
|
initialNode: z.string().min(1),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-dual-workflow-engine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "@gobing-ai/ts-dual-workflow-engine — State-machine and transition-flow workflow runtime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
|
35
|
+
"schemas",
|
|
35
36
|
"src",
|
|
36
37
|
"README.md"
|
|
37
38
|
],
|
|
@@ -47,8 +48,8 @@
|
|
|
47
48
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-dual-workflow-engine-v<version> && git push --tags' && exit 1"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
|
-
"@gobing-ai/ts-db": "^0.2.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.2.
|
|
51
|
+
"@gobing-ai/ts-db": "^0.2.8",
|
|
52
|
+
"@gobing-ai/ts-runtime": "^0.2.8",
|
|
52
53
|
"yaml": "^2.7.0",
|
|
53
54
|
"zod": "^4.1.0"
|
|
54
55
|
},
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/gobing-ai/ts-libs/main/packages/dual-workflow-engine/schemas/state-machine-workflow.schema.json",
|
|
4
|
+
"title": "@gobing-ai/ts-dual-workflow-engine State-machine Workflow",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["name", "initialState", "states", "transitions"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"kind": { "const": "state-machine" },
|
|
11
|
+
"name": { "type": "string" },
|
|
12
|
+
"initialState": { "type": "string" },
|
|
13
|
+
"terminalStates": { "type": "array", "items": { "type": "string" } },
|
|
14
|
+
"iterationBound": { "type": "integer" },
|
|
15
|
+
"vars": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
16
|
+
"env": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"allow": { "type": "array", "items": { "type": "string" } }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"states": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["id"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"id": { "type": "string" },
|
|
31
|
+
"onEnter": { "type": "array", "items": { "$ref": "#/$defs/action" } },
|
|
32
|
+
"onExit": { "type": "array", "items": { "$ref": "#/$defs/action" } }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"transitions": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"required": ["from", "to"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"from": { "type": "string" },
|
|
44
|
+
"to": { "type": "string" },
|
|
45
|
+
"trigger": { "type": "string" },
|
|
46
|
+
"guard": { "$ref": "#/$defs/guard" }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"$defs": {
|
|
52
|
+
"action": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"required": ["kind"],
|
|
56
|
+
"properties": {
|
|
57
|
+
"kind": { "type": "string" },
|
|
58
|
+
"options": { "type": "object", "additionalProperties": true }
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"guard": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"required": ["kind"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"kind": { "type": "string" },
|
|
67
|
+
"options": { "type": "object", "additionalProperties": true }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/gobing-ai/ts-libs/main/packages/dual-workflow-engine/schemas/transition-flow-workflow.schema.json",
|
|
4
|
+
"title": "@gobing-ai/ts-dual-workflow-engine Transition-flow Workflow",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["kind", "name", "initialNode", "nodes", "edges"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"kind": { "const": "transition-flow" },
|
|
11
|
+
"name": { "type": "string" },
|
|
12
|
+
"initialNode": { "type": "string" },
|
|
13
|
+
"terminalNodes": { "type": "array", "items": { "type": "string" } },
|
|
14
|
+
"iterationBound": { "type": "integer" },
|
|
15
|
+
"vars": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
16
|
+
"env": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"allow": { "type": "array", "items": { "type": "string" } }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"nodes": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["id"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"id": { "type": "string" },
|
|
31
|
+
"type": { "enum": ["action", "gate", "parallel", "decision"] },
|
|
32
|
+
"action": { "$ref": "#/$defs/action" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"edges": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"required": ["from", "to"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"from": { "type": "string" },
|
|
44
|
+
"to": { "type": "string" },
|
|
45
|
+
"condition": { "$ref": "#/$defs/guard" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"$defs": {
|
|
51
|
+
"action": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"required": ["kind"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"kind": { "type": "string" },
|
|
57
|
+
"options": { "type": "object", "additionalProperties": true }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"guard": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"additionalProperties": false,
|
|
63
|
+
"required": ["kind"],
|
|
64
|
+
"properties": {
|
|
65
|
+
"kind": { "type": "string" },
|
|
66
|
+
"options": { "type": "object", "additionalProperties": true }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadStructuredConfig } from '@gobing-ai/ts-runtime';
|
|
2
2
|
import { parse } from 'yaml';
|
|
3
3
|
import { WorkflowValidationError } from './errors';
|
|
4
4
|
import { WorkflowDefSchema } from './schema';
|
|
5
5
|
import type { WorkflowDef } from './types';
|
|
6
6
|
|
|
7
|
+
export interface WorkflowLoadOptions {
|
|
8
|
+
/** When true, honor a top-level `$schema` ref. Defaults to true for file loads. */
|
|
9
|
+
validateSchema?: boolean;
|
|
10
|
+
/** Optional fetch implementation for remote HTTP(S) schema refs. */
|
|
11
|
+
fetch?: (input: string) => Promise<Response>;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/** Load a workflow definition from YAML or JSON text. */
|
|
8
15
|
export function loadWorkflowDefFromText(text: string, source = '<inline>'): WorkflowDef {
|
|
9
16
|
const parsed = source.endsWith('.json') ? JSON.parse(text) : parse(text);
|
|
10
|
-
|
|
11
|
-
if (!result.success) {
|
|
12
|
-
throw new WorkflowValidationError(
|
|
13
|
-
`Workflow definition failed schema validation for ${source}: ${result.error.issues.map((issue) => issue.message).join('; ')}`,
|
|
14
|
-
result.error.issues,
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
validateWorkflowDef(result.data as WorkflowDef);
|
|
18
|
-
return result.data as WorkflowDef;
|
|
17
|
+
return parseWorkflowDef(parsed, source);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
/** Load a workflow definition from a filesystem path. */
|
|
22
|
-
export async function loadWorkflowDef(path: string): Promise<WorkflowDef> {
|
|
23
|
-
return
|
|
21
|
+
export async function loadWorkflowDef(path: string, options: WorkflowLoadOptions = {}): Promise<WorkflowDef> {
|
|
22
|
+
return parseWorkflowDef(await loadStructuredConfig(path, options), path);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
/** Validate semantic workflow invariants beyond structural Zod checks. */
|
|
@@ -45,6 +44,18 @@ function validateStateMachine(workflow: Extract<WorkflowDef, { kind?: 'state-mac
|
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
function parseWorkflowDef(parsed: unknown, source: string): WorkflowDef {
|
|
48
|
+
const result = WorkflowDefSchema.safeParse(parsed);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
throw new WorkflowValidationError(
|
|
51
|
+
`Workflow definition failed schema validation for ${source}: ${result.error.issues.map((issue) => issue.message).join('; ')}`,
|
|
52
|
+
result.error.issues,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
validateWorkflowDef(result.data as WorkflowDef);
|
|
56
|
+
return result.data as WorkflowDef;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
function validateTransitionFlow(workflow: Extract<WorkflowDef, { kind: 'transition-flow' }>): void {
|
|
49
60
|
const nodes = new Set(workflow.nodes.map((node) => node.id));
|
|
50
61
|
if (!nodes.has(workflow.initialNode)) {
|
package/src/schema.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const GuardDefSchema = z.object({
|
|
|
14
14
|
|
|
15
15
|
/** Zod schema for state-machine workflow definitions. */
|
|
16
16
|
export const StateMachineWorkflowDefSchema = z.object({
|
|
17
|
+
$schema: z.string().optional(),
|
|
17
18
|
kind: z.literal('state-machine').optional(),
|
|
18
19
|
name: z.string().min(1),
|
|
19
20
|
initialState: z.string().min(1),
|
|
@@ -40,6 +41,7 @@ export const StateMachineWorkflowDefSchema = z.object({
|
|
|
40
41
|
|
|
41
42
|
/** Zod schema for transition-flow workflow definitions. */
|
|
42
43
|
export const TransitionFlowWorkflowDefSchema = z.object({
|
|
44
|
+
$schema: z.string().optional(),
|
|
43
45
|
kind: z.literal('transition-flow'),
|
|
44
46
|
name: z.string().min(1),
|
|
45
47
|
initialNode: z.string().min(1),
|