@db-lyon/flowkit 0.13.0 → 0.15.0
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 +387 -386
- package/dist/.tsbuildinfo +1 -1
- package/dist/flow/runner.d.ts +12 -4
- package/dist/flow/runner.d.ts.map +1 -1
- package/dist/flow/runner.js +27 -12
- package/dist/flow/runner.js.map +1 -1
- package/dist/guard/index.d.ts +7 -0
- package/dist/guard/index.d.ts.map +1 -0
- package/dist/guard/index.js +5 -0
- package/dist/guard/index.js.map +1 -0
- package/dist/guard/pipeline.d.ts +22 -0
- package/dist/guard/pipeline.d.ts.map +1 -0
- package/dist/guard/pipeline.js +45 -0
- package/dist/guard/pipeline.js.map +1 -0
- package/dist/guard/registry.d.ts +20 -0
- package/dist/guard/registry.d.ts.map +1 -0
- package/dist/guard/registry.js +33 -0
- package/dist/guard/registry.js.map +1 -0
- package/dist/guard/task-guards.d.ts +89 -0
- package/dist/guard/task-guards.d.ts.map +1 -0
- package/dist/guard/task-guards.js +97 -0
- package/dist/guard/task-guards.js.map +1 -0
- package/dist/guard/types.d.ts +49 -0
- package/dist/guard/types.d.ts.map +1 -0
- package/dist/guard/types.js +38 -0
- package/dist/guard/types.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/task/base-task.d.ts +65 -3
- package/dist/task/base-task.d.ts.map +1 -1
- package/dist/task/base-task.js +51 -5
- package/dist/task/base-task.js.map +1 -1
- package/dist/task/index.d.ts +2 -2
- package/dist/task/index.d.ts.map +1 -1
- package/dist/task/index.js +1 -1
- package/dist/task/index.js.map +1 -1
- package/dist/task/registry.d.ts +11 -3
- package/dist/task/registry.d.ts.map +1 -1
- package/dist/task/registry.js +11 -3
- package/dist/task/registry.js.map +1 -1
- package/docs/ai-agents.md +368 -368
- package/docs/api-reference.md +606 -458
- package/docs/configuration.md +347 -347
- package/docs/custom-tasks.md +271 -258
- package/docs/guards.md +133 -0
- package/docs/releases.md +59 -0
- package/package.json +59 -53
- package/dist/flow/references.d.ts +0 -39
- package/dist/flow/references.d.ts.map +0 -1
- package/dist/flow/references.js +0 -102
- package/dist/flow/references.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,386 +1,387 @@
|
|
|
1
|
-
# @db-lyon/flowkit
|
|
2
|
-
|
|
3
|
-
YAML-configured task and flow orchestration engine for Node.js.
|
|
4
|
-
|
|
5
|
-
Define reusable **tasks** and compose them into **flows** using declarative YAML. Flowkit handles config layering, task resolution, sequential execution, nested flows, lifecycle hooks, and more.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install @db-lyon/flowkit
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Requires Node.js >= 20.
|
|
14
|
-
|
|
15
|
-
## Quick start
|
|
16
|
-
|
|
17
|
-
**1. Define your config** (`pipeline.yml`):
|
|
18
|
-
|
|
19
|
-
```yaml
|
|
20
|
-
tasks:
|
|
21
|
-
build:
|
|
22
|
-
class_path: tasks.Build
|
|
23
|
-
description: Compile the project
|
|
24
|
-
options:
|
|
25
|
-
target: production
|
|
26
|
-
|
|
27
|
-
test:
|
|
28
|
-
class_path: tasks.Test
|
|
29
|
-
description: Run the test suite
|
|
30
|
-
|
|
31
|
-
deploy:
|
|
32
|
-
class_path: tasks.Deploy
|
|
33
|
-
description: Deploy artifacts
|
|
34
|
-
|
|
35
|
-
flows:
|
|
36
|
-
ci:
|
|
37
|
-
description: Build, test, deploy
|
|
38
|
-
steps:
|
|
39
|
-
1:
|
|
40
|
-
task: build
|
|
41
|
-
2:
|
|
42
|
-
task: test
|
|
43
|
-
3:
|
|
44
|
-
task: deploy
|
|
45
|
-
options:
|
|
46
|
-
environment: staging
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**2. Create a task** (`tasks/Build.ts`):
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
import { BaseTask, type TaskResult } from '@db-lyon/flowkit';
|
|
53
|
-
|
|
54
|
-
interface BuildOptions {
|
|
55
|
-
target: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export default class Build extends BaseTask<BuildOptions> {
|
|
59
|
-
get taskName() { return 'build'; }
|
|
60
|
-
|
|
61
|
-
protected validate() {
|
|
62
|
-
if (!this.options.target) throw new Error('target is required');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async execute(): Promise<TaskResult> {
|
|
66
|
-
this.logger.info(`Building for ${this.options.target}`);
|
|
67
|
-
// ... do work ...
|
|
68
|
-
return { success: true, data: { target: this.options.target } };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
**3. Run it**:
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
import {
|
|
77
|
-
loadConfig,
|
|
78
|
-
EngineConfigSchema,
|
|
79
|
-
TaskRegistry,
|
|
80
|
-
FlowRunner,
|
|
81
|
-
} from '@db-lyon/flowkit';
|
|
82
|
-
|
|
83
|
-
const { config } = loadConfig({
|
|
84
|
-
filename: 'pipeline.yml',
|
|
85
|
-
schema: EngineConfigSchema,
|
|
86
|
-
configDir: './config',
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const registry = new TaskRegistry();
|
|
90
|
-
// Tasks with class_path like "tasks.Build" are resolved dynamically
|
|
91
|
-
// from the filesystem (tasks/Build.ts), or register them explicitly:
|
|
92
|
-
// registry.register('build', Build);
|
|
93
|
-
|
|
94
|
-
const runner = new FlowRunner({
|
|
95
|
-
tasks: config.tasks,
|
|
96
|
-
flows: config.flows,
|
|
97
|
-
registry,
|
|
98
|
-
context: { logger: console },
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const result = await runner.run({ flowName: 'ci' });
|
|
102
|
-
console.log(result.success); // true
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Features
|
|
106
|
-
|
|
107
|
-
### YAML-driven configuration
|
|
108
|
-
|
|
109
|
-
Define tasks and flows in YAML. Each task references a `class_path` (resolved to a file on disk or a registered constructor) and can carry default `options`. Flows are ordered sequences of steps that reference tasks or other flows.
|
|
110
|
-
|
|
111
|
-
### Config layering
|
|
112
|
-
|
|
113
|
-
The config loader merges multiple YAML files in order:
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
defaults (code) → pipeline.yml → pipeline.staging.yml → pipeline.local.yml
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
const { config } = loadConfig({
|
|
121
|
-
filename: 'pipeline.yml',
|
|
122
|
-
schema: EngineConfigSchema,
|
|
123
|
-
env: 'staging', // loads pipeline.staging.yml overlay
|
|
124
|
-
configDir: './config',
|
|
125
|
-
});
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Environment overlays and `.local.yml` files let you customize per-environment or per-developer without touching the base config. See [docs/configuration.md](docs/configuration.md).
|
|
129
|
-
|
|
130
|
-
### Custom tasks
|
|
131
|
-
|
|
132
|
-
Extend `BaseTask` to create your own tasks. The lifecycle is: `validate()` → `execute()` → result with timing. Exceptions are caught and returned as `{ success: false }` automatically.
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
class MyTask extends BaseTask<MyOptions> {
|
|
136
|
-
get taskName() { return 'my_task'; }
|
|
137
|
-
async execute(): Promise<TaskResult> {
|
|
138
|
-
return { success: true };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
See [docs/custom-tasks.md](docs/custom-tasks.md).
|
|
144
|
-
|
|
145
|
-
### Built-in ShellTask
|
|
146
|
-
|
|
147
|
-
Run shell commands without writing a custom task class:
|
|
148
|
-
|
|
149
|
-
```yaml
|
|
150
|
-
tasks:
|
|
151
|
-
lint:
|
|
152
|
-
class_path: shell
|
|
153
|
-
options:
|
|
154
|
-
command: npm run lint
|
|
155
|
-
cwd: /path/to/project
|
|
156
|
-
timeout: 60000
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Register it in your registry:
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
import { ShellTask } from '@db-lyon/flowkit';
|
|
163
|
-
registry.register('shell', ShellTask as any);
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Existing YAML consumers need no change. To cancel one programmatic invocation,
|
|
167
|
-
pass an invocation-specific `AbortSignal`:
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
const controller = new AbortController();
|
|
171
|
-
const result = await new ShellTask({}, {
|
|
172
|
-
command: 'npm run build',
|
|
173
|
-
signal: controller.signal,
|
|
174
|
-
}).run();
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### AI agents
|
|
178
|
-
|
|
179
|
-
Run LLM calls as steps. Flowkit has **no SDK dependencies** — you wire a
|
|
180
|
-
model-agnostic `LLMProvider` onto the task context as `ctx.llm`, and two tasks
|
|
181
|
-
consume it:
|
|
182
|
-
|
|
183
|
-
- `agent_prompt` (`AgentPromptTask`) — single-shot prompt, with optional
|
|
184
|
-
JSON-Schema structured output (validated, with a repair re-prompt).
|
|
185
|
-
- `agent` (`AgentTask`) — a tool-calling loop. Tools reference flowkit tasks,
|
|
186
|
-
flows, or other agents (or context handlers), gated by an allowlist and
|
|
187
|
-
per-tool argument validation. Multiple tool calls in a turn (including
|
|
188
|
-
parallel sub-agents) run concurrently under a cap.
|
|
189
|
-
|
|
190
|
-
Reusable agents live under an `agents:` root key and run as flow steps or as
|
|
191
|
-
other agents' tools, with mandatory budgets (`maxIterations`, `tokenBudget`,
|
|
192
|
-
`maxAgentDepth`). Iteration and concurrency live in the agent runtime, so a flow
|
|
193
|
-
stays a sequential spine with no `loop:` or parallel-step primitive.
|
|
194
|
-
|
|
195
|
-
```yaml
|
|
196
|
-
tasks:
|
|
197
|
-
extract:
|
|
198
|
-
class_path: agent_prompt
|
|
199
|
-
options:
|
|
200
|
-
prompt: "Pull the ticket fields from:\n${steps.1.text}"
|
|
201
|
-
schema:
|
|
202
|
-
type: object
|
|
203
|
-
required: [title, priority]
|
|
204
|
-
properties:
|
|
205
|
-
title: { type: string }
|
|
206
|
-
priority: { type: string, enum: [low, medium, high] }
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Every call is hardened by a shared core: per-call `timeout` (with provider
|
|
210
|
-
abort), `retries` with exponential backoff, structured-output validation +
|
|
211
|
-
repair, and output-size caps. See [docs/ai-agents.md](docs/ai-agents.md).
|
|
212
|
-
|
|
213
|
-
### Nested flows
|
|
214
|
-
|
|
215
|
-
A step can reference another flow instead of a task:
|
|
216
|
-
|
|
217
|
-
```yaml
|
|
218
|
-
flows:
|
|
219
|
-
ci:
|
|
220
|
-
description: CI pipeline
|
|
221
|
-
steps:
|
|
222
|
-
1: { task: build }
|
|
223
|
-
2: { task: test }
|
|
224
|
-
|
|
225
|
-
release:
|
|
226
|
-
description: Full release
|
|
227
|
-
steps:
|
|
228
|
-
1: { flow: ci }
|
|
229
|
-
2: { task: deploy }
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
A flow step's `options` override the options of tasks **inside** the nested
|
|
233
|
-
flow, keyed by task name. Precedence, low → high: task default → enclosing-flow
|
|
234
|
-
override → step's own inline options → runtime `params`.
|
|
235
|
-
|
|
236
|
-
```yaml
|
|
237
|
-
release:
|
|
238
|
-
steps:
|
|
239
|
-
1:
|
|
240
|
-
flow: ci
|
|
241
|
-
options:
|
|
242
|
-
test: { coverage: 90 } # overrides the `test` task's options inside `ci`
|
|
243
|
-
2: { task: deploy }
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Conditional steps (`when`)
|
|
247
|
-
|
|
248
|
-
A step runs only when its `when` is truthy. It accepts a boolean, or a string
|
|
249
|
-
expression evaluated at run time. With no `conditionEvaluator` configured, a
|
|
250
|
-
string is resolved for `${...}` references and tested for truthiness; supply a
|
|
251
|
-
`conditionEvaluator` to plug in a real expression language. A falsy result skips
|
|
252
|
-
the step (`skipReason: 'when'`) without failing the flow.
|
|
253
|
-
|
|
254
|
-
```yaml
|
|
255
|
-
steps:
|
|
256
|
-
1: { task: build }
|
|
257
|
-
2: { task: deploy, when: '${steps.1.shouldDeploy}' }
|
|
258
|
-
3: { task: notify, when: false }
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
262
|
-
new FlowRunner({
|
|
263
|
-
// ...
|
|
264
|
-
conditionEvaluator: (expr, ctx) => evalMyDsl(expr, ctx), // ctx: { steps, params, context, error }
|
|
265
|
-
});
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### Continue on failure (`ignore_failure`)
|
|
269
|
-
|
|
270
|
-
By default any failed step aborts the flow. Mark a step `ignore_failure: true`
|
|
271
|
-
to record the failure but keep going (the step result has `ignoredFailure: true`).
|
|
272
|
-
|
|
273
|
-
```yaml
|
|
274
|
-
steps:
|
|
275
|
-
1: { task: deploy }
|
|
276
|
-
2: { task: publish_release_notes, ignore_failure: true }
|
|
277
|
-
3: { task: announce }
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### Skip steps
|
|
281
|
-
|
|
282
|
-
Skip by task name or step number:
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
await runner.run({ flowName: 'release', skip: ['deploy'] });
|
|
286
|
-
await runner.run({ flowName: 'release', skip: ['2'] });
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
Or mark a step as permanently skipped in YAML:
|
|
290
|
-
|
|
291
|
-
```yaml
|
|
292
|
-
steps:
|
|
293
|
-
3:
|
|
294
|
-
task: None
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Plan mode
|
|
298
|
-
|
|
299
|
-
Preview the execution plan without running anything:
|
|
300
|
-
|
|
301
|
-
```typescript
|
|
302
|
-
const result = await runner.run({ flowName: 'ci', plan: true });
|
|
303
|
-
result.steps.forEach(s =>
|
|
304
|
-
console.log(`${s.stepNumber}: [${s.type}] ${s.name}${s.skipped ? ' (skip)' : ''}`)
|
|
305
|
-
);
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Pass `expandNestedFlows: true` to recursively expand nested-flow steps into
|
|
309
|
-
their child steps, each annotated with a hierarchical `path` (e.g. `2/1`):
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
await runner.run({ flowName: 'release', plan: true, expandNestedFlows: true });
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### Lifecycle hooks
|
|
316
|
-
|
|
317
|
-
Attach hooks to observe or react to flow execution:
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
const runner = new FlowRunner({
|
|
321
|
-
// ...
|
|
322
|
-
hooks: {
|
|
323
|
-
beforeRun: async (flowName, plan) => { /* ... */ },
|
|
324
|
-
beforeStep: async (step) => { /* ... */ },
|
|
325
|
-
afterStep: async (step, result) => { /* ... */ },
|
|
326
|
-
onStepError: async (step, error, completed) => { /* ... */ },
|
|
327
|
-
afterRun: async (result) => { /* ... */ },
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
`beforeRun`/`afterRun` fire once for the top-level flow. `beforeStep`/`afterStep` fire for every step including those inside nested flows.
|
|
333
|
-
|
|
334
|
-
### DAG utilities
|
|
335
|
-
|
|
336
|
-
Topological sort with cycle and missing-dependency detection:
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
import { topologicalSort } from '@db-lyon/flowkit';
|
|
340
|
-
|
|
341
|
-
const sorted = topologicalSort([
|
|
342
|
-
{ id: 'a', dependencies: [], data: null },
|
|
343
|
-
{ id: 'b', dependencies: ['a'], data: null },
|
|
344
|
-
{ id: 'c', dependencies: ['a', 'b'], data: null },
|
|
345
|
-
]);
|
|
346
|
-
// sorted: [a, b, c]
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
Throws `CircularDependencyError` or `MissingDependencyError` on invalid graphs.
|
|
350
|
-
|
|
351
|
-
### Logger interface
|
|
352
|
-
|
|
353
|
-
Flowkit accepts any logger that implements the `Logger` interface (compatible with pino, winston, etc.):
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
interface Logger {
|
|
357
|
-
debug(...args: unknown[]): void;
|
|
358
|
-
info(...args: unknown[]): void;
|
|
359
|
-
warn(...args: unknown[]): void;
|
|
360
|
-
error(...args: unknown[]): void;
|
|
361
|
-
child(bindings: Record<string, unknown>): Logger;
|
|
362
|
-
}
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
Pass it via the task context or flow runner config. A `noopLogger` is used by default.
|
|
366
|
-
|
|
367
|
-
## Sub-path exports
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
import { loadConfig } from '@db-lyon/flowkit/config';
|
|
371
|
-
import { BaseTask, TaskRegistry } from '@db-lyon/flowkit/task';
|
|
372
|
-
import { FlowRunner } from '@db-lyon/flowkit/flow';
|
|
373
|
-
import { topologicalSort } from '@db-lyon/flowkit/dag';
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
## Docs
|
|
377
|
-
|
|
378
|
-
- [Getting started](docs/getting-started.md) — step-by-step setup guide
|
|
379
|
-
- [Custom tasks](docs/custom-tasks.md) — writing and registering tasks
|
|
380
|
-
- [AI agents](docs/ai-agents.md) — LLM prompts, structured output, tool-calling agents
|
|
381
|
-
- [Configuration](docs/configuration.md) — YAML schema, layering, deep merge
|
|
382
|
-
- [
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
1
|
+
# @db-lyon/flowkit
|
|
2
|
+
|
|
3
|
+
YAML-configured task and flow orchestration engine for Node.js.
|
|
4
|
+
|
|
5
|
+
Define reusable **tasks** and compose them into **flows** using declarative YAML. Flowkit handles config layering, task resolution, sequential execution, nested flows, lifecycle hooks, and more.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @db-lyon/flowkit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js >= 20.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
**1. Define your config** (`pipeline.yml`):
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
tasks:
|
|
21
|
+
build:
|
|
22
|
+
class_path: tasks.Build
|
|
23
|
+
description: Compile the project
|
|
24
|
+
options:
|
|
25
|
+
target: production
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
class_path: tasks.Test
|
|
29
|
+
description: Run the test suite
|
|
30
|
+
|
|
31
|
+
deploy:
|
|
32
|
+
class_path: tasks.Deploy
|
|
33
|
+
description: Deploy artifacts
|
|
34
|
+
|
|
35
|
+
flows:
|
|
36
|
+
ci:
|
|
37
|
+
description: Build, test, deploy
|
|
38
|
+
steps:
|
|
39
|
+
1:
|
|
40
|
+
task: build
|
|
41
|
+
2:
|
|
42
|
+
task: test
|
|
43
|
+
3:
|
|
44
|
+
task: deploy
|
|
45
|
+
options:
|
|
46
|
+
environment: staging
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**2. Create a task** (`tasks/Build.ts`):
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { BaseTask, type TaskResult } from '@db-lyon/flowkit';
|
|
53
|
+
|
|
54
|
+
interface BuildOptions {
|
|
55
|
+
target: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default class Build extends BaseTask<BuildOptions> {
|
|
59
|
+
get taskName() { return 'build'; }
|
|
60
|
+
|
|
61
|
+
protected validate() {
|
|
62
|
+
if (!this.options.target) throw new Error('target is required');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async execute(): Promise<TaskResult> {
|
|
66
|
+
this.logger.info(`Building for ${this.options.target}`);
|
|
67
|
+
// ... do work ...
|
|
68
|
+
return { success: true, data: { target: this.options.target } };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**3. Run it**:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import {
|
|
77
|
+
loadConfig,
|
|
78
|
+
EngineConfigSchema,
|
|
79
|
+
TaskRegistry,
|
|
80
|
+
FlowRunner,
|
|
81
|
+
} from '@db-lyon/flowkit';
|
|
82
|
+
|
|
83
|
+
const { config } = loadConfig({
|
|
84
|
+
filename: 'pipeline.yml',
|
|
85
|
+
schema: EngineConfigSchema,
|
|
86
|
+
configDir: './config',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const registry = new TaskRegistry();
|
|
90
|
+
// Tasks with class_path like "tasks.Build" are resolved dynamically
|
|
91
|
+
// from the filesystem (tasks/Build.ts), or register them explicitly:
|
|
92
|
+
// registry.register('build', Build);
|
|
93
|
+
|
|
94
|
+
const runner = new FlowRunner({
|
|
95
|
+
tasks: config.tasks,
|
|
96
|
+
flows: config.flows,
|
|
97
|
+
registry,
|
|
98
|
+
context: { logger: console },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await runner.run({ flowName: 'ci' });
|
|
102
|
+
console.log(result.success); // true
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Features
|
|
106
|
+
|
|
107
|
+
### YAML-driven configuration
|
|
108
|
+
|
|
109
|
+
Define tasks and flows in YAML. Each task references a `class_path` (resolved to a file on disk or a registered constructor) and can carry default `options`. Flows are ordered sequences of steps that reference tasks or other flows.
|
|
110
|
+
|
|
111
|
+
### Config layering
|
|
112
|
+
|
|
113
|
+
The config loader merges multiple YAML files in order:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
defaults (code) → pipeline.yml → pipeline.staging.yml → pipeline.local.yml
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const { config } = loadConfig({
|
|
121
|
+
filename: 'pipeline.yml',
|
|
122
|
+
schema: EngineConfigSchema,
|
|
123
|
+
env: 'staging', // loads pipeline.staging.yml overlay
|
|
124
|
+
configDir: './config',
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Environment overlays and `.local.yml` files let you customize per-environment or per-developer without touching the base config. See [docs/configuration.md](docs/configuration.md).
|
|
129
|
+
|
|
130
|
+
### Custom tasks
|
|
131
|
+
|
|
132
|
+
Extend `BaseTask` to create your own tasks. The lifecycle is: `validate()` → `execute()` → result with timing. Exceptions are caught and returned as `{ success: false }` automatically.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
class MyTask extends BaseTask<MyOptions> {
|
|
136
|
+
get taskName() { return 'my_task'; }
|
|
137
|
+
async execute(): Promise<TaskResult> {
|
|
138
|
+
return { success: true };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
See [docs/custom-tasks.md](docs/custom-tasks.md).
|
|
144
|
+
|
|
145
|
+
### Built-in ShellTask
|
|
146
|
+
|
|
147
|
+
Run shell commands without writing a custom task class:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
tasks:
|
|
151
|
+
lint:
|
|
152
|
+
class_path: shell
|
|
153
|
+
options:
|
|
154
|
+
command: npm run lint
|
|
155
|
+
cwd: /path/to/project
|
|
156
|
+
timeout: 60000
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Register it in your registry:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { ShellTask } from '@db-lyon/flowkit';
|
|
163
|
+
registry.register('shell', ShellTask as any);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Existing YAML consumers need no change. To cancel one programmatic invocation,
|
|
167
|
+
pass an invocation-specific `AbortSignal`:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
const result = await new ShellTask({}, {
|
|
172
|
+
command: 'npm run build',
|
|
173
|
+
signal: controller.signal,
|
|
174
|
+
}).run();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### AI agents
|
|
178
|
+
|
|
179
|
+
Run LLM calls as steps. Flowkit has **no SDK dependencies** — you wire a
|
|
180
|
+
model-agnostic `LLMProvider` onto the task context as `ctx.llm`, and two tasks
|
|
181
|
+
consume it:
|
|
182
|
+
|
|
183
|
+
- `agent_prompt` (`AgentPromptTask`) — single-shot prompt, with optional
|
|
184
|
+
JSON-Schema structured output (validated, with a repair re-prompt).
|
|
185
|
+
- `agent` (`AgentTask`) — a tool-calling loop. Tools reference flowkit tasks,
|
|
186
|
+
flows, or other agents (or context handlers), gated by an allowlist and
|
|
187
|
+
per-tool argument validation. Multiple tool calls in a turn (including
|
|
188
|
+
parallel sub-agents) run concurrently under a cap.
|
|
189
|
+
|
|
190
|
+
Reusable agents live under an `agents:` root key and run as flow steps or as
|
|
191
|
+
other agents' tools, with mandatory budgets (`maxIterations`, `tokenBudget`,
|
|
192
|
+
`maxAgentDepth`). Iteration and concurrency live in the agent runtime, so a flow
|
|
193
|
+
stays a sequential spine with no `loop:` or parallel-step primitive.
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
tasks:
|
|
197
|
+
extract:
|
|
198
|
+
class_path: agent_prompt
|
|
199
|
+
options:
|
|
200
|
+
prompt: "Pull the ticket fields from:\n${steps.1.text}"
|
|
201
|
+
schema:
|
|
202
|
+
type: object
|
|
203
|
+
required: [title, priority]
|
|
204
|
+
properties:
|
|
205
|
+
title: { type: string }
|
|
206
|
+
priority: { type: string, enum: [low, medium, high] }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Every call is hardened by a shared core: per-call `timeout` (with provider
|
|
210
|
+
abort), `retries` with exponential backoff, structured-output validation +
|
|
211
|
+
repair, and output-size caps. See [docs/ai-agents.md](docs/ai-agents.md).
|
|
212
|
+
|
|
213
|
+
### Nested flows
|
|
214
|
+
|
|
215
|
+
A step can reference another flow instead of a task:
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
flows:
|
|
219
|
+
ci:
|
|
220
|
+
description: CI pipeline
|
|
221
|
+
steps:
|
|
222
|
+
1: { task: build }
|
|
223
|
+
2: { task: test }
|
|
224
|
+
|
|
225
|
+
release:
|
|
226
|
+
description: Full release
|
|
227
|
+
steps:
|
|
228
|
+
1: { flow: ci }
|
|
229
|
+
2: { task: deploy }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
A flow step's `options` override the options of tasks **inside** the nested
|
|
233
|
+
flow, keyed by task name. Precedence, low → high: task default → enclosing-flow
|
|
234
|
+
override → step's own inline options → runtime `params`.
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
release:
|
|
238
|
+
steps:
|
|
239
|
+
1:
|
|
240
|
+
flow: ci
|
|
241
|
+
options:
|
|
242
|
+
test: { coverage: 90 } # overrides the `test` task's options inside `ci`
|
|
243
|
+
2: { task: deploy }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Conditional steps (`when`)
|
|
247
|
+
|
|
248
|
+
A step runs only when its `when` is truthy. It accepts a boolean, or a string
|
|
249
|
+
expression evaluated at run time. With no `conditionEvaluator` configured, a
|
|
250
|
+
string is resolved for `${...}` references and tested for truthiness; supply a
|
|
251
|
+
`conditionEvaluator` to plug in a real expression language. A falsy result skips
|
|
252
|
+
the step (`skipReason: 'when'`) without failing the flow.
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
steps:
|
|
256
|
+
1: { task: build }
|
|
257
|
+
2: { task: deploy, when: '${steps.1.shouldDeploy}' }
|
|
258
|
+
3: { task: notify, when: false }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
new FlowRunner({
|
|
263
|
+
// ...
|
|
264
|
+
conditionEvaluator: (expr, ctx) => evalMyDsl(expr, ctx), // ctx: { steps, params, context, error }
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Continue on failure (`ignore_failure`)
|
|
269
|
+
|
|
270
|
+
By default any failed step aborts the flow. Mark a step `ignore_failure: true`
|
|
271
|
+
to record the failure but keep going (the step result has `ignoredFailure: true`).
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
steps:
|
|
275
|
+
1: { task: deploy }
|
|
276
|
+
2: { task: publish_release_notes, ignore_failure: true }
|
|
277
|
+
3: { task: announce }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Skip steps
|
|
281
|
+
|
|
282
|
+
Skip by task name or step number:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
await runner.run({ flowName: 'release', skip: ['deploy'] });
|
|
286
|
+
await runner.run({ flowName: 'release', skip: ['2'] });
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Or mark a step as permanently skipped in YAML:
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
steps:
|
|
293
|
+
3:
|
|
294
|
+
task: None
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Plan mode
|
|
298
|
+
|
|
299
|
+
Preview the execution plan without running anything:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const result = await runner.run({ flowName: 'ci', plan: true });
|
|
303
|
+
result.steps.forEach(s =>
|
|
304
|
+
console.log(`${s.stepNumber}: [${s.type}] ${s.name}${s.skipped ? ' (skip)' : ''}`)
|
|
305
|
+
);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Pass `expandNestedFlows: true` to recursively expand nested-flow steps into
|
|
309
|
+
their child steps, each annotated with a hierarchical `path` (e.g. `2/1`):
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
await runner.run({ flowName: 'release', plan: true, expandNestedFlows: true });
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Lifecycle hooks
|
|
316
|
+
|
|
317
|
+
Attach hooks to observe or react to flow execution:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
const runner = new FlowRunner({
|
|
321
|
+
// ...
|
|
322
|
+
hooks: {
|
|
323
|
+
beforeRun: async (flowName, plan) => { /* ... */ },
|
|
324
|
+
beforeStep: async (step) => { /* ... */ },
|
|
325
|
+
afterStep: async (step, result) => { /* ... */ },
|
|
326
|
+
onStepError: async (step, error, completed) => { /* ... */ },
|
|
327
|
+
afterRun: async (result) => { /* ... */ },
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`beforeRun`/`afterRun` fire once for the top-level flow. `beforeStep`/`afterStep` fire for every step including those inside nested flows.
|
|
333
|
+
|
|
334
|
+
### DAG utilities
|
|
335
|
+
|
|
336
|
+
Topological sort with cycle and missing-dependency detection:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { topologicalSort } from '@db-lyon/flowkit';
|
|
340
|
+
|
|
341
|
+
const sorted = topologicalSort([
|
|
342
|
+
{ id: 'a', dependencies: [], data: null },
|
|
343
|
+
{ id: 'b', dependencies: ['a'], data: null },
|
|
344
|
+
{ id: 'c', dependencies: ['a', 'b'], data: null },
|
|
345
|
+
]);
|
|
346
|
+
// sorted: [a, b, c]
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Throws `CircularDependencyError` or `MissingDependencyError` on invalid graphs.
|
|
350
|
+
|
|
351
|
+
### Logger interface
|
|
352
|
+
|
|
353
|
+
Flowkit accepts any logger that implements the `Logger` interface (compatible with pino, winston, etc.):
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
interface Logger {
|
|
357
|
+
debug(...args: unknown[]): void;
|
|
358
|
+
info(...args: unknown[]): void;
|
|
359
|
+
warn(...args: unknown[]): void;
|
|
360
|
+
error(...args: unknown[]): void;
|
|
361
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Pass it via the task context or flow runner config. A `noopLogger` is used by default.
|
|
366
|
+
|
|
367
|
+
## Sub-path exports
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import { loadConfig } from '@db-lyon/flowkit/config';
|
|
371
|
+
import { BaseTask, TaskRegistry } from '@db-lyon/flowkit/task';
|
|
372
|
+
import { FlowRunner } from '@db-lyon/flowkit/flow';
|
|
373
|
+
import { topologicalSort } from '@db-lyon/flowkit/dag';
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Docs
|
|
377
|
+
|
|
378
|
+
- [Getting started](docs/getting-started.md) — step-by-step setup guide
|
|
379
|
+
- [Custom tasks](docs/custom-tasks.md) — writing and registering tasks
|
|
380
|
+
- [AI agents](docs/ai-agents.md) — LLM prompts, structured output, tool-calling agents
|
|
381
|
+
- [Configuration](docs/configuration.md) — YAML schema, layering, deep merge
|
|
382
|
+
- [Guards](docs/guards.md) — before/after pipeline around a host operation
|
|
383
|
+
- [API reference](docs/api-reference.md) — full type and function reference
|
|
384
|
+
|
|
385
|
+
## License
|
|
386
|
+
|
|
387
|
+
MIT — see [LICENSE](LICENSE).
|