@gobing-ai/ts-dual-workflow-engine 0.2.6 → 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 CHANGED
@@ -1,4 +1,274 @@
1
1
  # @gobing-ai/ts-dual-workflow-engine
2
2
 
3
- Standalone workflow runtime for state-machine and transition-flow workflows. The package owns workflow definitions, validation schemas, variable resolution, action execution, persistence schema, and driver loops.
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
@@ -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,CAWtF;AAED,yDAAyD;AACzD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAExE;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAM/D"}
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 { getFs } from '@gobing-ai/ts-runtime';
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
- const result = WorkflowDefSchema.safeParse(parsed);
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 loadWorkflowDefFromText(await getFs().readFile(path), path);
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;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuBxC,CAAC;AAEH,2DAA2D;AAC3D,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsB1C,CAAC;AAEH,iEAAiE;AACjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAA4E,CAAC"}
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.6",
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.6",
51
- "@gobing-ai/ts-runtime": "^0.2.6",
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 { getFs } from '@gobing-ai/ts-runtime';
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
- const result = WorkflowDefSchema.safeParse(parsed);
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 loadWorkflowDefFromText(await getFs().readFile(path), path);
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),