@cogitator-ai/workflows 0.5.16 → 0.6.1
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 +339 -772
- package/dist/builder.d.ts +5 -4
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +61 -77
- package/dist/builder.js.map +1 -1
- package/dist/checkpoint.d.ts.map +1 -1
- package/dist/checkpoint.js +23 -16
- package/dist/checkpoint.js.map +1 -1
- package/dist/executor.d.ts +34 -2
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +222 -37
- package/dist/executor.js.map +1 -1
- package/dist/human/approval-store.d.ts +0 -4
- package/dist/human/approval-store.d.ts.map +1 -1
- package/dist/human/approval-store.js +50 -38
- package/dist/human/approval-store.js.map +1 -1
- package/dist/human/human-node.d.ts.map +1 -1
- package/dist/human/human-node.js +6 -2
- package/dist/human/human-node.js.map +1 -1
- package/dist/human/notifiers.d.ts +0 -3
- package/dist/human/notifiers.d.ts.map +1 -1
- package/dist/human/notifiers.js +67 -25
- package/dist/human/notifiers.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/manager/scheduler.d.ts.map +1 -1
- package/dist/manager/scheduler.js +6 -2
- package/dist/manager/scheduler.js.map +1 -1
- package/dist/manager/workflow-manager.d.ts +11 -1
- package/dist/manager/workflow-manager.d.ts.map +1 -1
- package/dist/manager/workflow-manager.js +53 -8
- package/dist/manager/workflow-manager.js.map +1 -1
- package/dist/nodes/adapters.d.ts +60 -0
- package/dist/nodes/adapters.d.ts.map +1 -0
- package/dist/nodes/adapters.js +138 -0
- package/dist/nodes/adapters.js.map +1 -0
- package/dist/nodes/agent.d.ts +0 -15
- package/dist/nodes/agent.d.ts.map +1 -1
- package/dist/nodes/agent.js +17 -16
- package/dist/nodes/agent.js.map +1 -1
- package/dist/nodes/base.d.ts +11 -8
- package/dist/nodes/base.d.ts.map +1 -1
- package/dist/nodes/base.js +0 -3
- package/dist/nodes/base.js.map +1 -1
- package/dist/nodes/function.d.ts +0 -18
- package/dist/nodes/function.d.ts.map +1 -1
- package/dist/nodes/function.js +0 -9
- package/dist/nodes/function.js.map +1 -1
- package/dist/nodes/index.d.ts +1 -3
- package/dist/nodes/index.d.ts.map +1 -1
- package/dist/nodes/index.js +1 -3
- package/dist/nodes/index.js.map +1 -1
- package/dist/nodes/tool.d.ts +1 -12
- package/dist/nodes/tool.d.ts.map +1 -1
- package/dist/nodes/tool.js +4 -9
- package/dist/nodes/tool.js.map +1 -1
- package/dist/observability/exporters.d.ts +4 -4
- package/dist/observability/exporters.d.ts.map +1 -1
- package/dist/observability/exporters.js +25 -9
- package/dist/observability/exporters.js.map +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/index.js.map +1 -1
- package/dist/observability/metrics.d.ts +0 -21
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/metrics.js +22 -26
- package/dist/observability/metrics.js.map +1 -1
- package/dist/observability/tracer.d.ts +9 -69
- package/dist/observability/tracer.d.ts.map +1 -1
- package/dist/observability/tracer.js +82 -125
- package/dist/observability/tracer.js.map +1 -1
- package/dist/patterns/index.d.ts +0 -13
- package/dist/patterns/index.d.ts.map +1 -1
- package/dist/patterns/index.js +0 -13
- package/dist/patterns/index.js.map +1 -1
- package/dist/patterns/map-reduce.d.ts +0 -129
- package/dist/patterns/map-reduce.d.ts.map +1 -1
- package/dist/patterns/map-reduce.js +63 -92
- package/dist/patterns/map-reduce.js.map +1 -1
- package/dist/saga/circuit-breaker.d.ts.map +1 -1
- package/dist/saga/circuit-breaker.js +5 -2
- package/dist/saga/circuit-breaker.js.map +1 -1
- package/dist/saga/compensation.d.ts +5 -2
- package/dist/saga/compensation.d.ts.map +1 -1
- package/dist/saga/compensation.js +49 -44
- package/dist/saga/compensation.js.map +1 -1
- package/dist/saga/dead-letter.d.ts +3 -0
- package/dist/saga/dead-letter.d.ts.map +1 -1
- package/dist/saga/dead-letter.js +11 -5
- package/dist/saga/dead-letter.js.map +1 -1
- package/dist/saga/idempotency.d.ts +1 -1
- package/dist/saga/idempotency.d.ts.map +1 -1
- package/dist/saga/idempotency.js +46 -13
- package/dist/saga/idempotency.js.map +1 -1
- package/dist/saga/retry.d.ts +5 -0
- package/dist/saga/retry.d.ts.map +1 -1
- package/dist/saga/retry.js +48 -29
- package/dist/saga/retry.js.map +1 -1
- package/dist/scheduler.d.ts +16 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +100 -9
- package/dist/scheduler.js.map +1 -1
- package/dist/subworkflows/index.d.ts +0 -13
- package/dist/subworkflows/index.d.ts.map +1 -1
- package/dist/subworkflows/index.js +0 -13
- package/dist/subworkflows/index.js.map +1 -1
- package/dist/subworkflows/parallel-subworkflows.d.ts +1 -77
- package/dist/subworkflows/parallel-subworkflows.d.ts.map +1 -1
- package/dist/subworkflows/parallel-subworkflows.js +12 -40
- package/dist/subworkflows/parallel-subworkflows.js.map +1 -1
- package/dist/subworkflows/subworkflow-node.d.ts +1 -82
- package/dist/subworkflows/subworkflow-node.d.ts.map +1 -1
- package/dist/subworkflows/subworkflow-node.js +42 -41
- package/dist/subworkflows/subworkflow-node.js.map +1 -1
- package/dist/timers/cron-parser.d.ts.map +1 -1
- package/dist/timers/cron-parser.js +32 -10
- package/dist/timers/cron-parser.js.map +1 -1
- package/dist/timers/timer-manager.d.ts +10 -0
- package/dist/timers/timer-manager.d.ts.map +1 -1
- package/dist/timers/timer-manager.js +13 -0
- package/dist/timers/timer-manager.js.map +1 -1
- package/dist/timers/timer-node.d.ts.map +1 -1
- package/dist/timers/timer-node.js +1 -0
- package/dist/timers/timer-node.js.map +1 -1
- package/dist/timers/timer-store.d.ts.map +1 -1
- package/dist/timers/timer-store.js +16 -10
- package/dist/timers/timer-store.js.map +1 -1
- package/dist/triggers/cron-trigger.d.ts +6 -5
- package/dist/triggers/cron-trigger.d.ts.map +1 -1
- package/dist/triggers/cron-trigger.js +52 -18
- package/dist/triggers/cron-trigger.js.map +1 -1
- package/dist/triggers/rate-limiter.d.ts +4 -0
- package/dist/triggers/rate-limiter.d.ts.map +1 -1
- package/dist/triggers/rate-limiter.js +10 -1
- package/dist/triggers/rate-limiter.js.map +1 -1
- package/dist/triggers/trigger-manager.d.ts.map +1 -1
- package/dist/triggers/trigger-manager.js +67 -40
- package/dist/triggers/trigger-manager.js.map +1 -1
- package/dist/triggers/webhook-trigger.d.ts +1 -0
- package/dist/triggers/webhook-trigger.d.ts.map +1 -1
- package/dist/triggers/webhook-trigger.js +8 -8
- package/dist/triggers/webhook-trigger.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -3,999 +3,566 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@cogitator-ai/workflows)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
DAG-based workflow engine for Cogitator agents. Build
|
|
6
|
+
DAG-based workflow engine for Cogitator agents. Build multi-step workflows with branching, loops, joins, checkpoints, human-in-the-loop steps, timers, sagas, subworkflows, map-reduce, triggers and observability.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
pnpm add @cogitator-ai/workflows
|
|
11
|
+
pnpm add @cogitator-ai/workflows @cogitator-ai/core
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- **DAG Builder** —
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **Observability** — Tracing and metrics with multiple exporters
|
|
16
|
+
- **DAG Builder** — nodes, conditionals, loops and parallel fan-out with validation and entry-point detection
|
|
17
|
+
- **Correct joins** — a node with several upstream branches runs once, after all of them finished
|
|
18
|
+
- **Node policies** — per-node `timeout`, `retries` and `retryDelay`
|
|
19
|
+
- **Real-time Streaming** — async generator of execution events
|
|
20
|
+
- **Checkpoints** — save and resume workflow state
|
|
21
|
+
- **Pre-built Nodes** — agent, tool, function and custom nodes, plus adapters for timers, human approvals, map-reduce and subworkflows
|
|
22
|
+
- **Saga Patterns** — retries, circuit breakers, compensation, dead-letter queue, idempotency
|
|
23
|
+
- **Triggers** — cron, webhook and event triggers
|
|
24
|
+
- **Observability** — tracing (console, OTLP, Zipkin) and metrics (Prometheus format)
|
|
25
|
+
- **Workflow Management** — run store, scheduling, cancellation, retries, replay
|
|
27
26
|
|
|
28
27
|
## Quick Start
|
|
29
28
|
|
|
30
29
|
```typescript
|
|
31
|
-
import { WorkflowBuilder, WorkflowExecutor, agentNode } from '@cogitator-ai/workflows';
|
|
32
30
|
import { Cogitator, Agent } from '@cogitator-ai/core';
|
|
31
|
+
import { WorkflowBuilder, WorkflowExecutor, agentNode } from '@cogitator-ai/workflows';
|
|
32
|
+
|
|
33
|
+
type ReportState = {
|
|
34
|
+
topic: string;
|
|
35
|
+
analysis?: string;
|
|
36
|
+
};
|
|
33
37
|
|
|
34
|
-
const cogitator = new Cogitator({
|
|
35
|
-
|
|
38
|
+
const cogitator = new Cogitator({ llm: { defaultModel: 'ollama/llama3.2' } });
|
|
39
|
+
const analyst = new Agent({
|
|
40
|
+
name: 'analyst',
|
|
41
|
+
model: 'ollama/llama3.2',
|
|
42
|
+
instructions: 'Analyze topics concisely.',
|
|
36
43
|
});
|
|
37
|
-
const analyst = new Agent({ name: 'analyst', model: 'openai/gpt-4o', instructions: '...' });
|
|
38
44
|
|
|
39
|
-
const workflow = new WorkflowBuilder('
|
|
40
|
-
.
|
|
41
|
-
.addNode(
|
|
45
|
+
const workflow = new WorkflowBuilder<ReportState>('report')
|
|
46
|
+
.initialState({ topic: '' })
|
|
47
|
+
.addNode(
|
|
48
|
+
'analyze',
|
|
49
|
+
agentNode<ReportState>(analyst, {
|
|
50
|
+
inputMapper: (state) => `Analyze: ${state.topic}`,
|
|
51
|
+
stateMapper: (result) => ({ analysis: result.output }),
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
.addNode('report', async (ctx) => ({ output: `Report: ${ctx.state.analysis}` }), {
|
|
55
|
+
after: ['analyze'],
|
|
56
|
+
})
|
|
42
57
|
.build();
|
|
43
58
|
|
|
44
|
-
const
|
|
45
|
-
|
|
59
|
+
const result = await new WorkflowExecutor(cogitator).execute(workflow, { topic: 'Edge AI' });
|
|
60
|
+
console.log(result.state.analysis, result.error);
|
|
46
61
|
```
|
|
47
62
|
|
|
48
63
|
---
|
|
49
64
|
|
|
50
|
-
## Table of Contents
|
|
51
|
-
|
|
52
|
-
- [Core Concepts](#core-concepts)
|
|
53
|
-
- [Real-time Streaming](#real-time-streaming)
|
|
54
|
-
- [Pre-built Nodes](#pre-built-nodes)
|
|
55
|
-
- [Conditional Branching](#conditional-branching)
|
|
56
|
-
- [Loops](#loops)
|
|
57
|
-
- [Checkpoints](#checkpoints)
|
|
58
|
-
- [Timer System](#timer-system)
|
|
59
|
-
- [Saga Patterns](#saga-patterns)
|
|
60
|
-
- [Subworkflows](#subworkflows)
|
|
61
|
-
- [Human-in-the-Loop](#human-in-the-loop)
|
|
62
|
-
- [Map-Reduce Patterns](#map-reduce-patterns)
|
|
63
|
-
- [Triggers](#triggers)
|
|
64
|
-
- [Observability](#observability)
|
|
65
|
-
- [Workflow Management](#workflow-management)
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
65
|
## Core Concepts
|
|
70
66
|
|
|
71
67
|
### WorkflowBuilder
|
|
72
68
|
|
|
73
69
|
```typescript
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const workflow = new WorkflowBuilder<MyState>('my-workflow')
|
|
70
|
+
const workflow = new WorkflowBuilder<{ count: number }>('counter')
|
|
77
71
|
.initialState({ count: 0 })
|
|
78
|
-
.addNode('
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
async (ctx) => ({
|
|
84
|
-
output: `Count: ${ctx.state.count}`,
|
|
85
|
-
}),
|
|
86
|
-
{ after: ['step1'] }
|
|
87
|
-
)
|
|
72
|
+
.addNode('increment', async (ctx) => ({ state: { count: ctx.state.count + 1 } }))
|
|
73
|
+
.addNode('print', async (ctx) => ({ output: `Count: ${ctx.state.count}` }), {
|
|
74
|
+
after: ['increment'],
|
|
75
|
+
config: { timeout: 5_000, retries: 2, retryDelay: 500 },
|
|
76
|
+
})
|
|
88
77
|
.build();
|
|
89
78
|
```
|
|
90
79
|
|
|
80
|
+
- State types must be assignable to `Record<string, unknown>` — declare them with `type` rather than `interface`.
|
|
81
|
+
- `addNode(name, fnOrNode, { after, config })` accepts a node function or a node created by a factory.
|
|
82
|
+
- The entry point is the single node (or conditional/parallel/loop construct) without `after`. Independent roots are rejected — add a parallel fan-out or call `.entryPoint(name)`.
|
|
83
|
+
- Node functions receive `ctx.state` (a copy), `ctx.input` (outputs of upstream nodes), `ctx.nodeId`, `ctx.workflowId`, `ctx.step`, `ctx.reportProgress()` and — for built-in nodes — `cogitator`, `signal` (run cancellation) and `depth` (subworkflow nesting).
|
|
84
|
+
- Return `{ state }` to merge into the workflow state, `{ output }` for downstream nodes and `{ next }` to route dynamically.
|
|
85
|
+
|
|
91
86
|
### WorkflowExecutor
|
|
92
87
|
|
|
93
88
|
```typescript
|
|
94
|
-
import { WorkflowExecutor } from '@cogitator-ai/workflows';
|
|
95
|
-
|
|
96
89
|
const executor = new WorkflowExecutor(cogitator);
|
|
97
|
-
const result = await executor.execute(workflow, {
|
|
98
|
-
input: 'Start the workflow',
|
|
99
|
-
context: { userId: '123' },
|
|
100
|
-
timeout: 60000,
|
|
101
|
-
});
|
|
102
90
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
const result = await executor.execute(
|
|
92
|
+
workflow,
|
|
93
|
+
{ count: 5 },
|
|
94
|
+
{
|
|
95
|
+
maxConcurrency: 4, // parallel nodes per step
|
|
96
|
+
maxIterations: 100, // guards loops
|
|
97
|
+
signal: abortController.signal,
|
|
98
|
+
onNodeStart: (node) => console.log('start', node),
|
|
99
|
+
onNodeComplete: (node, output, duration) => console.log('done', node, duration),
|
|
100
|
+
onNodeError: (node, error) => console.error(node, error.message),
|
|
101
|
+
}
|
|
102
|
+
);
|
|
107
103
|
|
|
108
|
-
|
|
104
|
+
console.log(result.state, result.nodeResults, result.duration, result.error);
|
|
105
|
+
```
|
|
109
106
|
|
|
110
|
-
|
|
107
|
+
`execute()` never throws for node failures: the failure is returned in `result.error` (`NodeTimeoutError` for node timeouts).
|
|
111
108
|
|
|
112
|
-
|
|
109
|
+
Run-level policies apply to every node:
|
|
113
110
|
|
|
114
111
|
```typescript
|
|
115
|
-
|
|
112
|
+
await executor.execute(workflow, input, {
|
|
113
|
+
defaultRetry: { maxRetries: 3, backoff: 'exponential', initialDelay: 500 }, // nodes without config.retries
|
|
114
|
+
defaultCircuitBreaker: breakerConfig, // per-node breaker, shared across runs of this executor
|
|
115
|
+
deadLetterQueue: createInMemoryDLQ(), // entry for every node that finally failed
|
|
116
|
+
idempotencyStore, // reuse results of nodes that already completed for this workflowId
|
|
117
|
+
approvalStore, // default store for humanWorkflowNode
|
|
118
|
+
timerStore, // default store for persisted timers
|
|
119
|
+
tracer,
|
|
120
|
+
metricsCollector,
|
|
121
|
+
});
|
|
122
|
+
```
|
|
116
123
|
|
|
117
|
-
|
|
124
|
+
---
|
|
118
125
|
|
|
119
|
-
|
|
126
|
+
## Real-time Streaming
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
for await (const event of executor.stream(workflow, { count: 0 })) {
|
|
120
130
|
switch (event.type) {
|
|
121
131
|
case 'workflow_started':
|
|
122
|
-
console.log('Workflow started:', event.workflowId);
|
|
123
|
-
break;
|
|
124
|
-
|
|
125
132
|
case 'node_started':
|
|
126
|
-
console.log(`Node ${event.nodeId} started`);
|
|
127
|
-
break;
|
|
128
|
-
|
|
129
133
|
case 'node_progress':
|
|
130
|
-
console.log(`Node ${event.nodeId}: ${event.progress}%`);
|
|
131
|
-
break;
|
|
132
|
-
|
|
133
134
|
case 'node_completed':
|
|
134
|
-
console.log(`Node ${event.nodeId} completed:`, event.result);
|
|
135
|
-
break;
|
|
136
|
-
|
|
137
135
|
case 'node_error':
|
|
138
|
-
console.
|
|
136
|
+
console.log(event.type);
|
|
139
137
|
break;
|
|
140
|
-
|
|
141
138
|
case 'workflow_completed':
|
|
142
|
-
console.log('
|
|
139
|
+
console.log('final state', event.result.state);
|
|
143
140
|
break;
|
|
144
141
|
}
|
|
145
142
|
}
|
|
146
143
|
```
|
|
147
144
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
| Event | Description | Properties |
|
|
151
|
-
| -------------------- | -------------------------- | ------------------------------ |
|
|
152
|
-
| `workflow_started` | Workflow execution begins | `workflowId`, `timestamp` |
|
|
153
|
-
| `node_started` | Node execution begins | `nodeId`, `timestamp` |
|
|
154
|
-
| `node_progress` | Progress update from node | `nodeId`, `progress` (0-100) |
|
|
155
|
-
| `node_completed` | Node finished successfully | `nodeId`, `result`, `duration` |
|
|
156
|
-
| `node_error` | Node execution failed | `nodeId`, `error` |
|
|
157
|
-
| `workflow_completed` | Workflow finished | `result`, `duration` |
|
|
158
|
-
|
|
159
|
-
### Reporting Progress from Nodes
|
|
160
|
-
|
|
161
|
-
Use `ctx.reportProgress()` inside nodes to emit progress events:
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
const workflow = new WorkflowBuilder('processing')
|
|
165
|
-
.addNode('process', async (ctx) => {
|
|
166
|
-
const items = ctx.state.items;
|
|
167
|
-
|
|
168
|
-
for (let i = 0; i < items.length; i++) {
|
|
169
|
-
await processItem(items[i]);
|
|
170
|
-
ctx.reportProgress?.(Math.round(((i + 1) / items.length) * 100));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return { output: 'Done' };
|
|
174
|
-
})
|
|
175
|
-
.build();
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Progress Callback
|
|
179
|
-
|
|
180
|
-
For non-streaming execution, use the `onNodeProgress` callback:
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
const result = await executor.execute(workflow, {
|
|
184
|
-
input: 'Start',
|
|
185
|
-
onNodeProgress: (nodeId, progress) => {
|
|
186
|
-
console.log(`${nodeId}: ${progress}%`);
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
```
|
|
145
|
+
Nodes report progress with `ctx.reportProgress(0..100)`; `execute()` exposes it through `onNodeProgress`.
|
|
190
146
|
|
|
191
147
|
---
|
|
192
148
|
|
|
193
149
|
## Pre-built Nodes
|
|
194
150
|
|
|
195
|
-
### agentNode
|
|
196
|
-
|
|
197
|
-
Run an agent as a workflow node:
|
|
198
|
-
|
|
199
151
|
```typescript
|
|
200
|
-
import { agentNode } from '@cogitator-ai/workflows';
|
|
152
|
+
import { agentNode, toolNode, functionNode, customNode } from '@cogitator-ai/workflows';
|
|
201
153
|
|
|
202
|
-
|
|
154
|
+
builder
|
|
203
155
|
.addNode(
|
|
204
156
|
'research',
|
|
205
|
-
agentNode(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
timeout:
|
|
209
|
-
onToolCall: (call) => console.log('Tool:', call.name),
|
|
157
|
+
agentNode<MyState>(researcher, {
|
|
158
|
+
inputMapper: (state) => state.question,
|
|
159
|
+
stateMapper: (result) => ({ findings: result.output }),
|
|
160
|
+
runOptions: { timeout: 60_000 },
|
|
210
161
|
})
|
|
211
162
|
)
|
|
212
|
-
.build();
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### toolNode
|
|
216
|
-
|
|
217
|
-
Execute a tool directly:
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
import { toolNode } from '@cogitator-ai/workflows';
|
|
221
|
-
|
|
222
|
-
const workflow = new WorkflowBuilder('tool-flow')
|
|
223
|
-
.addNode('calculate', toolNode('calculator', { expression: '2 + 2' }))
|
|
224
|
-
.build();
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### functionNode
|
|
228
|
-
|
|
229
|
-
Custom function as a node:
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
import { functionNode } from '@cogitator-ai/workflows';
|
|
233
|
-
|
|
234
|
-
const workflow = new WorkflowBuilder('func-flow')
|
|
235
163
|
.addNode(
|
|
236
|
-
'
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
})
|
|
164
|
+
'calculate',
|
|
165
|
+
toolNode<MyState, { expression: string }>(calculator, {
|
|
166
|
+
argsMapper: (state) => ({ expression: state.formula }),
|
|
167
|
+
stateMapper: (result) => ({ value: result }),
|
|
168
|
+
}),
|
|
169
|
+
{ after: ['research'] }
|
|
241
170
|
)
|
|
242
|
-
.
|
|
171
|
+
.addNode(
|
|
172
|
+
'normalize',
|
|
173
|
+
functionNode<MyState, string>('normalize', async (state) => state.findings!.trim(), {
|
|
174
|
+
stateMapper: (output) => ({ findings: output as string }),
|
|
175
|
+
}),
|
|
176
|
+
{ after: ['calculate'] }
|
|
177
|
+
);
|
|
243
178
|
```
|
|
244
179
|
|
|
180
|
+
Agent and tool nodes use the run's abort signal, so cancelling the workflow cancels in-flight LLM and tool calls.
|
|
181
|
+
|
|
245
182
|
---
|
|
246
183
|
|
|
247
|
-
##
|
|
184
|
+
## Branching, Loops and Joins
|
|
248
185
|
|
|
249
186
|
```typescript
|
|
250
|
-
const workflow = new WorkflowBuilder('
|
|
251
|
-
.
|
|
252
|
-
.
|
|
187
|
+
const workflow = new WorkflowBuilder<{ approved: boolean }>('review-flow')
|
|
188
|
+
.initialState({ approved: false })
|
|
189
|
+
.addNode('review', reviewFn)
|
|
190
|
+
.addConditional('check', (state) => (state.approved ? 'publish' : 'revise'), {
|
|
253
191
|
after: ['review'],
|
|
254
192
|
})
|
|
255
|
-
.addNode('
|
|
256
|
-
.addNode('
|
|
257
|
-
.addNode('notify',
|
|
193
|
+
.addNode('publish', publishFn, { after: ['check'] })
|
|
194
|
+
.addNode('revise', reviseFn, { after: ['check'] })
|
|
195
|
+
.addNode('notify', notifyFn, { after: ['publish', 'revise'] })
|
|
258
196
|
.build();
|
|
259
197
|
```
|
|
260
198
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
## Loops
|
|
199
|
+
A conditional returns the name(s) of the branch(es) to take; every node (or construct) with `after: ['check']` is a candidate branch.
|
|
264
200
|
|
|
265
201
|
```typescript
|
|
266
|
-
const workflow = new WorkflowBuilder('retry-flow')
|
|
267
|
-
.
|
|
268
|
-
.
|
|
269
|
-
|
|
202
|
+
const workflow = new WorkflowBuilder<{ attempts: number; done: boolean }>('retry-flow')
|
|
203
|
+
.initialState({ attempts: 0, done: false })
|
|
204
|
+
.addNode('attempt', async (ctx) => ({
|
|
205
|
+
state: { attempts: ctx.state.attempts + 1, done: await tryIt() },
|
|
206
|
+
}))
|
|
207
|
+
.addLoop('again', {
|
|
208
|
+
condition: (state) =>
|
|
209
|
+
!(state as { done: boolean }).done && (state as { attempts: number }).attempts < 3,
|
|
270
210
|
back: 'attempt',
|
|
271
|
-
exit: '
|
|
211
|
+
exit: 'finish',
|
|
272
212
|
after: ['attempt'],
|
|
273
213
|
})
|
|
274
|
-
.addNode('
|
|
214
|
+
.addNode('finish', finishFn)
|
|
275
215
|
.build();
|
|
276
216
|
```
|
|
277
217
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
## Checkpoints
|
|
281
|
-
|
|
282
|
-
Save and resume workflow execution:
|
|
218
|
+
Only the loop's `back` and `exit` nodes may follow it; any other node with the loop in `after` makes `build()` throw. A root `addParallel` is the workflow's entry point:
|
|
283
219
|
|
|
284
220
|
```typescript
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
checkpointStore: store,
|
|
293
|
-
checkpointInterval: 5000, // Save every 5 seconds
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// Resume from checkpoint
|
|
297
|
-
const result = await executor.resume(checkpointId, store);
|
|
221
|
+
const workflow = new WorkflowBuilder('fan-out')
|
|
222
|
+
.addParallel('fan', ['fetch-a', 'fetch-b'])
|
|
223
|
+
.addNode('fetch-a', fetchA)
|
|
224
|
+
.addNode('fetch-b', fetchB)
|
|
225
|
+
.addNode('normalize-b', normalizeB, { after: ['fetch-b'] })
|
|
226
|
+
.addNode('merge', async (ctx) => ({ output: ctx.input }), { after: ['fetch-a', 'normalize-b'] })
|
|
227
|
+
.build();
|
|
298
228
|
```
|
|
299
229
|
|
|
300
|
-
|
|
230
|
+
`merge` runs once, after both branches finished, and receives both outputs as `ctx.input`.
|
|
301
231
|
|
|
302
|
-
|
|
232
|
+
---
|
|
303
233
|
|
|
304
|
-
|
|
234
|
+
## Checkpoints
|
|
305
235
|
|
|
306
236
|
```typescript
|
|
307
|
-
import {
|
|
237
|
+
import { FileCheckpointStore } from '@cogitator-ai/workflows';
|
|
308
238
|
|
|
309
|
-
const
|
|
310
|
-
// Fixed delay
|
|
311
|
-
.addNode('wait', delayNode(5000)) // 5 seconds
|
|
239
|
+
const executor = new WorkflowExecutor(cogitator, new FileCheckpointStore('./checkpoints'));
|
|
312
240
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
// Wait for cron schedule
|
|
320
|
-
.addNode('cron-wait', cronWaitNode('0 9 * * *')) // Wait until 9 AM
|
|
241
|
+
const first = await executor.execute(workflow, input, {
|
|
242
|
+
checkpoint: true,
|
|
243
|
+
checkpointStrategy: 'per-node', // or 'per-iteration' (default)
|
|
244
|
+
});
|
|
321
245
|
|
|
322
|
-
|
|
323
|
-
.
|
|
324
|
-
|
|
325
|
-
untilNode((state) => state.scheduledTime)
|
|
326
|
-
)
|
|
327
|
-
.build();
|
|
246
|
+
if (first.error && first.checkpointId) {
|
|
247
|
+
const resumed = await executor.resume(workflow, first.checkpointId);
|
|
248
|
+
}
|
|
328
249
|
```
|
|
329
250
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
import { parseDuration, formatDuration } from '@cogitator-ai/workflows';
|
|
334
|
-
|
|
335
|
-
const ms = parseDuration('1h30m'); // 5400000
|
|
336
|
-
const str = formatDuration(5400000); // '1h 30m'
|
|
337
|
-
```
|
|
251
|
+
---
|
|
338
252
|
|
|
339
|
-
|
|
253
|
+
## Timers
|
|
340
254
|
|
|
341
255
|
```typescript
|
|
342
256
|
import {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
257
|
+
delayNode,
|
|
258
|
+
dynamicDelayNode,
|
|
259
|
+
cronWaitNode,
|
|
260
|
+
untilNode,
|
|
261
|
+
timerWorkflowNode,
|
|
262
|
+
parseDuration,
|
|
263
|
+
formatDuration,
|
|
348
264
|
} from '@cogitator-ai/workflows';
|
|
349
265
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
266
|
+
builder
|
|
267
|
+
.addNode('cool-down', timerWorkflowNode(delayNode('cool-down', parseDuration('5m'))))
|
|
268
|
+
.addNode(
|
|
269
|
+
'backoff',
|
|
270
|
+
timerWorkflowNode(dynamicDelayNode<MyState>('backoff', (state) => state.retries * 1000)),
|
|
271
|
+
{ after: ['cool-down'] }
|
|
272
|
+
)
|
|
273
|
+
.addNode('business-hours', timerWorkflowNode(cronWaitNode('business-hours', '0 9 * * 1-5')), {
|
|
274
|
+
after: ['backoff'],
|
|
275
|
+
})
|
|
276
|
+
.addNode('deadline', timerWorkflowNode(untilNode<MyState>('deadline', (state) => state.dueAt)), {
|
|
277
|
+
after: ['business-hours'],
|
|
278
|
+
});
|
|
361
279
|
|
|
362
|
-
//
|
|
363
|
-
CRON_PRESETS.EVERY_MINUTE; // '* * * * *'
|
|
364
|
-
CRON_PRESETS.HOURLY; // '0 * * * *'
|
|
365
|
-
CRON_PRESETS.DAILY; // '0 0 * * *'
|
|
366
|
-
CRON_PRESETS.WEEKLY; // '0 0 * * 0'
|
|
367
|
-
CRON_PRESETS.MONTHLY; // '0 0 1 * *'
|
|
280
|
+
formatDuration(90_000); // '1.5m'
|
|
368
281
|
```
|
|
369
282
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
Manage recurring timers:
|
|
373
|
-
|
|
374
|
-
```typescript
|
|
375
|
-
import { createTimerManager, createRecurringScheduler } from '@cogitator-ai/workflows';
|
|
376
|
-
|
|
377
|
-
const manager = createTimerManager({
|
|
378
|
-
maxConcurrent: 10,
|
|
379
|
-
defaultTimeout: 60000,
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// One-shot timer
|
|
383
|
-
manager.schedule('task-1', 5000, async () => {
|
|
384
|
-
console.log('Executed after 5 seconds');
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// Recurring timer
|
|
388
|
-
const scheduler = createRecurringScheduler();
|
|
389
|
-
scheduler.schedule('daily-report', '0 9 * * *', async () => {
|
|
390
|
-
await generateDailyReport();
|
|
391
|
-
});
|
|
392
|
-
```
|
|
283
|
+
Waits are cancelled when the run is aborted. Pass `{ timerStore }` to `timerWorkflowNode` together with `persist: true` configs (or use `createTimerNodeHelpers(store)`) to persist timers; `createTimerManager(store)` and `createRecurringScheduler(manager)` process persisted and recurring timers. Cron helpers: `validateCronExpression`, `getNextCronOccurrence(s)`, `describeCronExpression`, `CRON_PRESETS`.
|
|
393
284
|
|
|
394
285
|
---
|
|
395
286
|
|
|
396
287
|
## Saga Patterns
|
|
397
288
|
|
|
398
|
-
### Retry
|
|
289
|
+
### Retry
|
|
399
290
|
|
|
400
291
|
```typescript
|
|
401
|
-
import { executeWithRetry, withRetry
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
initialDelay:
|
|
407
|
-
maxDelay:
|
|
408
|
-
backoffMultiplier: 2,
|
|
292
|
+
import { executeWithRetry, withRetry } from '@cogitator-ai/workflows';
|
|
293
|
+
|
|
294
|
+
const outcome = await executeWithRetry((attempt) => callService(attempt), {
|
|
295
|
+
maxRetries: 4,
|
|
296
|
+
backoff: 'exponential', // 'constant' | 'linear' | 'exponential'
|
|
297
|
+
initialDelay: 500,
|
|
298
|
+
maxDelay: 10_000,
|
|
409
299
|
jitter: 0.1,
|
|
410
|
-
|
|
411
|
-
onRetry: (attempt, error, delay) => console.log(`Retry ${attempt} in ${delay}ms`),
|
|
300
|
+
isRetryable: (error) => !error.message.includes('invalid'),
|
|
412
301
|
});
|
|
302
|
+
if (!outcome.success) console.error(outcome.error);
|
|
413
303
|
|
|
414
|
-
|
|
415
|
-
const retryableFetch = withRetry({ maxAttempts: 3 })(async (url: string) => await fetch(url));
|
|
416
|
-
|
|
417
|
-
// Class decorator
|
|
418
|
-
class ApiClient {
|
|
419
|
-
@Retryable({ maxAttempts: 3, initialDelay: 500 })
|
|
420
|
-
async request(endpoint: string) {
|
|
421
|
-
return fetch(endpoint);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
304
|
+
const fetchWithRetry = withRetry((url: string) => fetch(url), { maxRetries: 3 });
|
|
424
305
|
```
|
|
425
306
|
|
|
426
307
|
### Circuit Breaker
|
|
427
308
|
|
|
428
309
|
```typescript
|
|
429
|
-
import {
|
|
430
|
-
|
|
431
|
-
const breaker = createCircuitBreaker({
|
|
432
|
-
failureThreshold: 5,
|
|
433
|
-
successThreshold: 2,
|
|
434
|
-
timeout: 30000,
|
|
435
|
-
halfOpenMaxAttempts: 3,
|
|
436
|
-
onStateChange: (from, to) => console.log(`Circuit: ${from} -> ${to}`),
|
|
437
|
-
});
|
|
310
|
+
import { createCircuitBreaker, CircuitBreakerOpenError } from '@cogitator-ai/workflows';
|
|
311
|
+
|
|
312
|
+
const breaker = createCircuitBreaker({ threshold: 5, resetTimeout: 30_000, successThreshold: 2 });
|
|
438
313
|
|
|
439
|
-
// Use the breaker
|
|
440
314
|
try {
|
|
441
|
-
|
|
442
|
-
return await externalService.call();
|
|
443
|
-
});
|
|
315
|
+
await breaker.execute('payments', () => payments.charge(order));
|
|
444
316
|
} catch (error) {
|
|
445
317
|
if (error instanceof CircuitBreakerOpenError) {
|
|
446
|
-
|
|
318
|
+
// use a fallback
|
|
447
319
|
}
|
|
448
320
|
}
|
|
449
321
|
|
|
450
|
-
|
|
451
|
-
const stats = breaker.getStats();
|
|
452
|
-
console.log(stats.failures, stats.successes, stats.state);
|
|
453
|
-
|
|
454
|
-
// Decorator-style
|
|
455
|
-
class ServiceClient {
|
|
456
|
-
@WithCircuitBreaker({ failureThreshold: 3 })
|
|
457
|
-
async call() {
|
|
458
|
-
return fetch('/api');
|
|
459
|
-
}
|
|
460
|
-
}
|
|
322
|
+
console.log(breaker.getStats('payments'));
|
|
461
323
|
```
|
|
462
324
|
|
|
463
|
-
### Compensation
|
|
325
|
+
### Compensation
|
|
464
326
|
|
|
465
327
|
```typescript
|
|
466
|
-
import {
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
.
|
|
470
|
-
|
|
471
|
-
execute: async (ctx) => {
|
|
472
|
-
ctx.state.inventoryReserved = await inventory.reserve(ctx.data.orderId);
|
|
473
|
-
},
|
|
474
|
-
compensate: async (ctx) => {
|
|
475
|
-
await inventory.release(ctx.data.orderId);
|
|
476
|
-
},
|
|
477
|
-
})
|
|
478
|
-
.step({
|
|
479
|
-
name: 'charge-payment',
|
|
480
|
-
execute: async (ctx) => {
|
|
481
|
-
ctx.state.paymentId = await payments.charge(ctx.data.orderId);
|
|
482
|
-
},
|
|
483
|
-
compensate: async (ctx) => {
|
|
484
|
-
await payments.refund(ctx.state.paymentId);
|
|
485
|
-
},
|
|
486
|
-
})
|
|
487
|
-
.step({
|
|
488
|
-
name: 'ship-order',
|
|
489
|
-
execute: async (ctx) => {
|
|
490
|
-
await shipping.ship(ctx.data.orderId);
|
|
491
|
-
},
|
|
492
|
-
compensate: async (ctx) => {
|
|
493
|
-
await shipping.cancel(ctx.data.orderId);
|
|
494
|
-
},
|
|
495
|
-
})
|
|
328
|
+
import { compensationBuilder } from '@cogitator-ai/workflows';
|
|
329
|
+
|
|
330
|
+
const compensation = compensationBuilder<OrderState>()
|
|
331
|
+
.addStep('reserve', async (state) => inventory.release(state.orderId))
|
|
332
|
+
.addStep('charge', async (state) => payments.refund(state.paymentId!))
|
|
496
333
|
.build();
|
|
497
334
|
|
|
498
|
-
|
|
499
|
-
|
|
335
|
+
compensation.markCompleted('reserve', reservation);
|
|
336
|
+
compensation.markCompleted('charge', payment);
|
|
500
337
|
|
|
501
|
-
|
|
502
|
-
console.log('Saga failed at:', result.failedStep);
|
|
503
|
-
console.log('Compensated steps:', result.compensatedSteps);
|
|
504
|
-
}
|
|
338
|
+
const report = await compensation.compensate(state, 'ship', new Error('carrier down'));
|
|
505
339
|
```
|
|
506
340
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
```typescript
|
|
510
|
-
import { createFileDLQ, createInMemoryDLQ } from '@cogitator-ai/workflows';
|
|
511
|
-
|
|
512
|
-
const dlq = createFileDLQ('./dlq');
|
|
513
|
-
|
|
514
|
-
// Add failed item
|
|
515
|
-
await dlq.add({
|
|
516
|
-
id: 'job-123',
|
|
517
|
-
payload: { orderId: 'order-456' },
|
|
518
|
-
error: 'Payment failed',
|
|
519
|
-
source: 'checkout-workflow',
|
|
520
|
-
attemptCount: 3,
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
// Process DLQ
|
|
524
|
-
const items = await dlq.list({ source: 'checkout-workflow' });
|
|
525
|
-
for (const item of items) {
|
|
526
|
-
try {
|
|
527
|
-
await retryJob(item.payload);
|
|
528
|
-
await dlq.remove(item.id);
|
|
529
|
-
} catch {
|
|
530
|
-
await dlq.update(item.id, { attemptCount: item.attemptCount + 1 });
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
```
|
|
341
|
+
Compensations run in reverse completion order for the steps marked completed.
|
|
534
342
|
|
|
535
|
-
### Idempotency
|
|
343
|
+
### Dead Letter Queue and Idempotency
|
|
536
344
|
|
|
537
345
|
```typescript
|
|
538
|
-
import {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
keyGenerator: (orderId: string) => `order:${orderId}`,
|
|
545
|
-
ttl: 24 * 60 * 60 * 1000, // 24 hours
|
|
546
|
-
})(async (orderId: string) => {
|
|
547
|
-
return await processOrderInternal(orderId);
|
|
548
|
-
});
|
|
346
|
+
import {
|
|
347
|
+
createFileDLQ,
|
|
348
|
+
createDLQEntry,
|
|
349
|
+
createInMemoryIdempotencyStore,
|
|
350
|
+
idempotent,
|
|
351
|
+
} from '@cogitator-ai/workflows';
|
|
549
352
|
|
|
550
|
-
|
|
551
|
-
await
|
|
552
|
-
await
|
|
353
|
+
const dlq = createFileDLQ('./dlq');
|
|
354
|
+
await dlq.add(createDLQEntry('charge', workflowId, 'checkout', state, error, { attempts: 3 }));
|
|
355
|
+
const failed = await dlq.list({ workflowName: 'checkout' });
|
|
553
356
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
@Idempotent({ keyGenerator: (id) => `order:${id}`, ttl: 86400000 })
|
|
557
|
-
async process(orderId: string) {
|
|
558
|
-
return processOrderInternal(orderId);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
357
|
+
const store = createInMemoryIdempotencyStore();
|
|
358
|
+
const receipt = await idempotent(store, `charge:${orderId}`, () => payments.charge(orderId));
|
|
561
359
|
```
|
|
562
360
|
|
|
563
361
|
---
|
|
564
362
|
|
|
565
363
|
## Subworkflows
|
|
566
364
|
|
|
567
|
-
### Nested Subworkflows
|
|
568
|
-
|
|
569
365
|
```typescript
|
|
570
|
-
import {
|
|
366
|
+
import {
|
|
367
|
+
subworkflowNode,
|
|
368
|
+
subworkflowWorkflowNode,
|
|
369
|
+
fanOutFanIn,
|
|
370
|
+
parallelSubworkflowsNode,
|
|
371
|
+
} from '@cogitator-ai/workflows';
|
|
571
372
|
|
|
572
|
-
|
|
573
|
-
.addNode('prepare', prepareNode)
|
|
373
|
+
builder
|
|
574
374
|
.addNode(
|
|
575
|
-
'
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
375
|
+
'enrich',
|
|
376
|
+
subworkflowWorkflowNode(
|
|
377
|
+
subworkflowNode<ParentState, ChildState>('enrich', {
|
|
378
|
+
workflow: enrichmentWorkflow,
|
|
379
|
+
inputMapper: (state) => ({ record: state.record }),
|
|
380
|
+
outputMapper: (result, state) => ({ ...state, enriched: result.state.record }),
|
|
381
|
+
timeout: 60_000,
|
|
382
|
+
onError: 'retry', // 'propagate' | 'ignore' | 'catch' | 'retry'
|
|
383
|
+
maxDepth: 5,
|
|
384
|
+
})
|
|
385
|
+
)
|
|
582
386
|
)
|
|
583
|
-
.addNode('finalize', finalizeNode, { after: ['process'] })
|
|
584
|
-
.build();
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
### Parallel Subworkflows
|
|
588
|
-
|
|
589
|
-
```typescript
|
|
590
|
-
import { parallelSubworkflows, fanOutFanIn, scatterGather } from '@cogitator-ai/workflows';
|
|
591
|
-
|
|
592
|
-
// Fan-out/Fan-in pattern
|
|
593
|
-
const workflow = new WorkflowBuilder('parallel')
|
|
594
387
|
.addNode(
|
|
595
|
-
'
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
{
|
|
600
|
-
|
|
601
|
-
],
|
|
602
|
-
{
|
|
388
|
+
'per-region',
|
|
389
|
+
parallelSubworkflowsNode(
|
|
390
|
+
fanOutFanIn<ParentState, RegionState>('per-region', {
|
|
391
|
+
workflow: regionWorkflow,
|
|
392
|
+
getInputs: (state) => state.regions.map((region) => ({ id: region, input: { region } })),
|
|
393
|
+
aggregator: (results, state) => ({ ...state, regionCount: results.size }),
|
|
603
394
|
concurrency: 3,
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
)
|
|
608
|
-
.build();
|
|
609
|
-
|
|
610
|
-
// Scatter-Gather (collect all results)
|
|
611
|
-
const results = await scatterGather(executor, workflows, inputs);
|
|
612
|
-
|
|
613
|
-
// Race (first to complete wins)
|
|
614
|
-
const winner = await raceSubworkflows(executor, [workflow1, workflow2]);
|
|
615
|
-
|
|
616
|
-
// Fallback (try until one succeeds)
|
|
617
|
-
const result = await fallbackSubworkflows(executor, [primary, secondary, tertiary]);
|
|
395
|
+
})
|
|
396
|
+
),
|
|
397
|
+
{ after: ['enrich'] }
|
|
398
|
+
);
|
|
618
399
|
```
|
|
619
400
|
|
|
401
|
+
Child failures propagate to the parent (or follow `onError`); timeouts cancel the child run. `scatterGather`, `raceSubworkflows` and `fallbackSubworkflows` cover the other common patterns.
|
|
402
|
+
|
|
620
403
|
---
|
|
621
404
|
|
|
622
405
|
## Human-in-the-Loop
|
|
623
406
|
|
|
624
|
-
### Approval Node
|
|
625
|
-
|
|
626
407
|
```typescript
|
|
627
|
-
import {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
.addNode(
|
|
634
|
-
'request',
|
|
635
|
-
approvalNode({
|
|
636
|
-
message: (state) => `Approve expense: $${state.amount}`,
|
|
637
|
-
approvers: ['manager@company.com'],
|
|
638
|
-
timeout: 24 * 60 * 60 * 1000, // 24 hours
|
|
639
|
-
store,
|
|
640
|
-
notifier,
|
|
641
|
-
})
|
|
642
|
-
)
|
|
643
|
-
.addConditional('check', (state) => state.approved, { after: ['request'] })
|
|
644
|
-
.addNode('process', processNode, { after: ['check:true'] })
|
|
645
|
-
.addNode('reject', rejectNode, { after: ['check:false'] })
|
|
646
|
-
.build();
|
|
647
|
-
```
|
|
408
|
+
import {
|
|
409
|
+
approvalNode,
|
|
410
|
+
humanWorkflowNode,
|
|
411
|
+
InMemoryApprovalStore,
|
|
412
|
+
WebhookNotifier,
|
|
413
|
+
} from '@cogitator-ai/workflows';
|
|
648
414
|
|
|
649
|
-
|
|
415
|
+
const approvalStore = new InMemoryApprovalStore();
|
|
416
|
+
const approvalNotifier = new WebhookNotifier({ url: 'https://hooks.example.com/approvals' });
|
|
650
417
|
|
|
651
|
-
|
|
652
|
-
import { choiceNode } from '@cogitator-ai/workflows';
|
|
653
|
-
|
|
654
|
-
const workflow = new WorkflowBuilder('choice-flow')
|
|
418
|
+
builder
|
|
655
419
|
.addNode(
|
|
656
|
-
'
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
420
|
+
'approve-expense',
|
|
421
|
+
humanWorkflowNode(
|
|
422
|
+
approvalNode<ExpenseState>('approve-expense', {
|
|
423
|
+
title: 'Approve expense',
|
|
424
|
+
description: (state) => `Amount: $${state.amount}`,
|
|
425
|
+
assignee: 'manager@company.com',
|
|
426
|
+
timeout: 24 * 60 * 60 * 1000,
|
|
427
|
+
timeoutAction: 'reject',
|
|
428
|
+
}),
|
|
429
|
+
{ approvalStore, approvalNotifier, stateMapper: (result) => ({ approved: result.approved }) }
|
|
430
|
+
)
|
|
666
431
|
)
|
|
667
|
-
.
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
### Input Node
|
|
671
|
-
|
|
672
|
-
```typescript
|
|
673
|
-
import { inputNode } from '@cogitator-ai/workflows';
|
|
432
|
+
.addConditional('route', (state) => (state.approved ? 'pay' : 'decline'), {
|
|
433
|
+
after: ['approve-expense'],
|
|
434
|
+
});
|
|
674
435
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
{ name: 'priority', type: 'select', options: ['low', 'medium', 'high'] },
|
|
683
|
-
],
|
|
684
|
-
store,
|
|
685
|
-
notifier,
|
|
686
|
-
})
|
|
687
|
-
)
|
|
688
|
-
.build();
|
|
436
|
+
// Elsewhere (API handler, UI, Slack action):
|
|
437
|
+
await approvalStore.submitResponse({
|
|
438
|
+
requestId,
|
|
439
|
+
decision: true,
|
|
440
|
+
respondedBy: 'manager@company.com',
|
|
441
|
+
respondedAt: Date.now(),
|
|
442
|
+
});
|
|
689
443
|
```
|
|
690
444
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
```typescript
|
|
694
|
-
import { managementChain, chainNode } from '@cogitator-ai/workflows';
|
|
695
|
-
|
|
696
|
-
const workflow = new WorkflowBuilder('chain-approval')
|
|
697
|
-
.addNode(
|
|
698
|
-
'approval',
|
|
699
|
-
managementChain({
|
|
700
|
-
steps: [
|
|
701
|
-
{ approver: 'team-lead@co.com', requiredFor: (state) => state.amount > 100 },
|
|
702
|
-
{ approver: 'manager@co.com', requiredFor: (state) => state.amount > 1000 },
|
|
703
|
-
{ approver: 'director@co.com', requiredFor: (state) => state.amount > 10000 },
|
|
704
|
-
],
|
|
705
|
-
store,
|
|
706
|
-
notifier,
|
|
707
|
-
})
|
|
708
|
-
)
|
|
709
|
-
.build();
|
|
710
|
-
```
|
|
445
|
+
Other configs: `choiceNode`, `inputNode`, `ratingNode`, `chainNode`, `managementChain`. Notifiers: `ConsoleNotifier`, `WebhookNotifier`, `slackNotifier`, `CompositeNotifier`, `filteredNotifier`, `priorityRouter`. `FileApprovalStore` persists requests.
|
|
711
446
|
|
|
712
447
|
---
|
|
713
448
|
|
|
714
|
-
## Map-Reduce
|
|
715
|
-
|
|
716
|
-
### Map (Parallel Processing)
|
|
717
|
-
|
|
718
|
-
```typescript
|
|
719
|
-
import { mapNode, parallelMap, batchedMap } from '@cogitator-ai/workflows';
|
|
720
|
-
|
|
721
|
-
const workflow = new WorkflowBuilder('map-flow')
|
|
722
|
-
.addNode(
|
|
723
|
-
'process-items',
|
|
724
|
-
mapNode({
|
|
725
|
-
items: (state) => state.items,
|
|
726
|
-
mapper: async (item, index, ctx) => {
|
|
727
|
-
return await processItem(item);
|
|
728
|
-
},
|
|
729
|
-
concurrency: 5,
|
|
730
|
-
onProgress: ({ completed, total }) => console.log(`${completed}/${total}`),
|
|
731
|
-
})
|
|
732
|
-
)
|
|
733
|
-
.build();
|
|
734
|
-
|
|
735
|
-
// Batched processing
|
|
736
|
-
const results = await batchedMap(items, processItem, { batchSize: 10, concurrency: 3 });
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
### Reduce (Aggregation)
|
|
449
|
+
## Map-Reduce
|
|
740
450
|
|
|
741
451
|
```typescript
|
|
742
|
-
import {
|
|
452
|
+
import { mapReduceNode, mapReduceWorkflowNode } from '@cogitator-ai/workflows';
|
|
743
453
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
.build();
|
|
754
|
-
|
|
755
|
-
// Built-in aggregators
|
|
756
|
-
const collected = collect(items); // Collect all
|
|
757
|
-
const total = sum(items, (i) => i.value); // Sum values
|
|
758
|
-
const grouped = groupBy(items, (i) => i.category); // Group by key
|
|
759
|
-
const statistics = stats(items, (i) => i.score); // { min, max, avg, sum, count }
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
### Map-Reduce
|
|
763
|
-
|
|
764
|
-
```typescript
|
|
765
|
-
import { mapReduceNode, executeMapReduce } from '@cogitator-ai/workflows';
|
|
766
|
-
|
|
767
|
-
const workflow = new WorkflowBuilder('mapreduce-flow')
|
|
768
|
-
.addNode(
|
|
769
|
-
'word-count',
|
|
770
|
-
mapReduceNode({
|
|
771
|
-
items: (state) => state.documents,
|
|
772
|
-
mapper: async (doc) => {
|
|
773
|
-
const words = doc.text.split(/\s+/);
|
|
774
|
-
return words.map((w) => ({ word: w, count: 1 }));
|
|
454
|
+
builder.addNode(
|
|
455
|
+
'score-documents',
|
|
456
|
+
mapReduceWorkflowNode(
|
|
457
|
+
mapReduceNode<DocsState, number, number>('score-documents', {
|
|
458
|
+
map: {
|
|
459
|
+
items: (state) => state.documents,
|
|
460
|
+
mapper: async (doc) => scoreDocument(doc as string),
|
|
461
|
+
concurrency: 5,
|
|
462
|
+
continueOnError: true,
|
|
775
463
|
},
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}, {});
|
|
464
|
+
reduce: {
|
|
465
|
+
initial: 0,
|
|
466
|
+
reducer: (sum, item) => sum + item.result,
|
|
467
|
+
finalize: (sum, state) => sum / state.documents.length,
|
|
781
468
|
},
|
|
782
|
-
|
|
783
|
-
})
|
|
469
|
+
}),
|
|
470
|
+
{ stateMapper: (result) => ({ averageScore: result.reduced }) }
|
|
784
471
|
)
|
|
785
|
-
|
|
472
|
+
);
|
|
786
473
|
```
|
|
787
474
|
|
|
475
|
+
`mapNode` + `mapWorkflowNode`, `parallelMap`, `batchedMap` and the reducer presets `collect`, `sum`, `count`, `groupBy`, `partition`, `flatMap` and `stats` cover other shapes.
|
|
476
|
+
|
|
788
477
|
---
|
|
789
478
|
|
|
790
479
|
## Triggers
|
|
791
480
|
|
|
792
|
-
### Cron Trigger
|
|
793
|
-
|
|
794
481
|
```typescript
|
|
795
|
-
import {
|
|
796
|
-
|
|
797
|
-
const trigger = createCronTrigger({
|
|
798
|
-
expression: '0 9 * * 1-5', // 9 AM on weekdays
|
|
799
|
-
timezone: 'America/New_York',
|
|
800
|
-
workflow: dailyReportWorkflow,
|
|
801
|
-
executor,
|
|
802
|
-
onTrigger: (time) => console.log('Triggered at:', time),
|
|
803
|
-
});
|
|
482
|
+
import { createTriggerManager, cronTrigger, webhookTrigger } from '@cogitator-ai/workflows';
|
|
804
483
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
### Webhook Trigger
|
|
810
|
-
|
|
811
|
-
```typescript
|
|
812
|
-
import { createWebhookTrigger, WebhookTriggerExecutor } from '@cogitator-ai/workflows';
|
|
813
|
-
|
|
814
|
-
const webhook = createWebhookTrigger({
|
|
815
|
-
path: '/webhooks/github',
|
|
816
|
-
workflow: githubEventWorkflow,
|
|
817
|
-
executor,
|
|
818
|
-
auth: {
|
|
819
|
-
type: 'hmac',
|
|
820
|
-
secret: process.env.WEBHOOK_SECRET!,
|
|
821
|
-
header: 'X-Hub-Signature-256',
|
|
484
|
+
const triggers = createTriggerManager({
|
|
485
|
+
onTriggerFire: async (trigger, context) => {
|
|
486
|
+
const run = await manager.schedule(workflows[trigger.workflowName], { input: context.payload });
|
|
487
|
+
return run;
|
|
822
488
|
},
|
|
823
|
-
rateLimit: {
|
|
824
|
-
maxRequests: 100,
|
|
825
|
-
windowMs: 60000,
|
|
826
|
-
},
|
|
827
|
-
inputMapper: (req) => ({ event: req.body.action, payload: req.body }),
|
|
828
489
|
});
|
|
490
|
+
triggers.start();
|
|
829
491
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
```typescript
|
|
837
|
-
import {
|
|
838
|
-
createTriggerManager,
|
|
839
|
-
cronTrigger,
|
|
840
|
-
webhookTrigger,
|
|
841
|
-
eventTrigger,
|
|
842
|
-
} from '@cogitator-ai/workflows';
|
|
843
|
-
|
|
844
|
-
const manager = createTriggerManager({ executor });
|
|
845
|
-
|
|
846
|
-
manager.register(
|
|
847
|
-
'daily-report',
|
|
848
|
-
cronTrigger({
|
|
849
|
-
expression: '0 9 * * *',
|
|
850
|
-
workflow: reportWorkflow,
|
|
851
|
-
})
|
|
852
|
-
);
|
|
853
|
-
|
|
854
|
-
manager.register(
|
|
855
|
-
'github-webhook',
|
|
856
|
-
webhookTrigger({
|
|
857
|
-
path: '/hooks/github',
|
|
858
|
-
workflow: githubWorkflow,
|
|
859
|
-
})
|
|
860
|
-
);
|
|
492
|
+
await triggers.register({
|
|
493
|
+
workflowName: 'daily-report',
|
|
494
|
+
type: 'cron',
|
|
495
|
+
config: cronTrigger('0 9 * * *', { timezone: 'Europe/Berlin' }),
|
|
496
|
+
enabled: true,
|
|
497
|
+
});
|
|
861
498
|
|
|
862
|
-
|
|
863
|
-
'
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
})
|
|
868
|
-
|
|
499
|
+
await triggers.register({
|
|
500
|
+
workflowName: 'github-sync',
|
|
501
|
+
type: 'webhook',
|
|
502
|
+
config: webhookTrigger('/hooks/github', 'POST', {
|
|
503
|
+
auth: { type: 'hmac', secret: process.env.GH_SECRET! },
|
|
504
|
+
}),
|
|
505
|
+
enabled: true,
|
|
506
|
+
});
|
|
869
507
|
|
|
870
|
-
|
|
508
|
+
// In your HTTP handler:
|
|
509
|
+
const response = await triggers.handleWebhook({
|
|
510
|
+
path: req.path,
|
|
511
|
+
method: req.method,
|
|
512
|
+
headers,
|
|
513
|
+
body,
|
|
514
|
+
});
|
|
871
515
|
```
|
|
872
516
|
|
|
873
517
|
---
|
|
874
518
|
|
|
875
519
|
## Observability
|
|
876
520
|
|
|
877
|
-
### Tracing
|
|
878
|
-
|
|
879
521
|
```typescript
|
|
880
|
-
import {
|
|
881
|
-
createTracer,
|
|
882
|
-
OTLPSpanExporter,
|
|
883
|
-
ZipkinSpanExporter,
|
|
884
|
-
CompositeSpanExporter,
|
|
885
|
-
} from '@cogitator-ai/workflows';
|
|
886
|
-
|
|
887
|
-
// OTLP exporter (Jaeger, Tempo, etc.)
|
|
888
|
-
const otlpExporter = new OTLPSpanExporter({
|
|
889
|
-
endpoint: 'http://localhost:4318/v1/traces',
|
|
890
|
-
headers: { 'X-Api-Key': 'secret' },
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
// Zipkin exporter
|
|
894
|
-
const zipkinExporter = new ZipkinSpanExporter({
|
|
895
|
-
endpoint: 'http://localhost:9411/api/v2/spans',
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
// Composite (multiple exporters)
|
|
899
|
-
const exporter = new CompositeSpanExporter([otlpExporter, zipkinExporter]);
|
|
522
|
+
import { createTracer, createMetricsCollector } from '@cogitator-ai/workflows';
|
|
900
523
|
|
|
901
524
|
const tracer = createTracer({
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
// Execute with tracing
|
|
907
|
-
await executor.execute(workflow, { tracer });
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
### Metrics
|
|
911
|
-
|
|
912
|
-
```typescript
|
|
913
|
-
import { createMetricsCollector, WorkflowMetricsCollector } from '@cogitator-ai/workflows';
|
|
914
|
-
|
|
915
|
-
const metrics = createMetricsCollector({
|
|
916
|
-
prefix: 'cogitator_workflow',
|
|
917
|
-
labels: { environment: 'production' },
|
|
525
|
+
enabled: true,
|
|
526
|
+
serviceName: 'billing-workflows',
|
|
527
|
+
exporter: 'otlp', // 'console' | 'otlp' | 'jaeger' | 'zipkin'
|
|
528
|
+
exporterEndpoint: 'http://localhost:4318/v1/traces',
|
|
918
529
|
});
|
|
530
|
+
const metricsCollector = createMetricsCollector({ prefix: 'cogitator_workflow' });
|
|
919
531
|
|
|
920
|
-
|
|
921
|
-
await
|
|
532
|
+
await executor.execute(workflow, input, { tracer, metricsCollector });
|
|
533
|
+
await tracer.flush();
|
|
922
534
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
console.log(nodeMetrics.executionCount);
|
|
926
|
-
console.log(nodeMetrics.averageDuration);
|
|
927
|
-
console.log(nodeMetrics.errorRate);
|
|
928
|
-
|
|
929
|
-
const workflowMetrics = metrics.getWorkflowMetrics('my-workflow');
|
|
930
|
-
console.log(workflowMetrics.completionRate);
|
|
931
|
-
console.log(workflowMetrics.averageCompletionTime);
|
|
535
|
+
console.log(metricsCollector.getWorkflowMetrics('report'));
|
|
536
|
+
console.log(metricsCollector.toPrometheusFormat());
|
|
932
537
|
```
|
|
933
538
|
|
|
539
|
+
The executor creates one workflow span and a child span per node execution (parallel nodes keep correct parents) and records workflow/node counts, latencies and retries.
|
|
540
|
+
|
|
934
541
|
---
|
|
935
542
|
|
|
936
543
|
## Workflow Management
|
|
937
544
|
|
|
938
|
-
### WorkflowManager
|
|
939
|
-
|
|
940
545
|
```typescript
|
|
941
546
|
import { createWorkflowManager, createFileRunStore } from '@cogitator-ai/workflows';
|
|
942
547
|
|
|
943
|
-
const runStore = createFileRunStore('./runs');
|
|
944
|
-
|
|
945
548
|
const manager = createWorkflowManager({
|
|
946
|
-
|
|
947
|
-
runStore,
|
|
948
|
-
|
|
949
|
-
defaultTimeout:
|
|
549
|
+
cogitator,
|
|
550
|
+
runStore: createFileRunStore({ directory: './runs' }),
|
|
551
|
+
maxConcurrency: 10,
|
|
552
|
+
defaultTimeout: 300_000, // runs exceeding it are cancelled and marked failed
|
|
553
|
+
tracer,
|
|
554
|
+
metrics: metricsCollector,
|
|
950
555
|
});
|
|
556
|
+
manager.start();
|
|
951
557
|
|
|
952
|
-
|
|
953
|
-
const runId = await manager.schedule(workflow, {
|
|
954
|
-
input: 'Process this',
|
|
955
|
-
priority: 1,
|
|
956
|
-
scheduledAt: new Date(Date.now() + 60000), // 1 minute from now
|
|
957
|
-
tags: ['daily', 'report'],
|
|
958
|
-
});
|
|
558
|
+
const result = await manager.execute(workflow, input, { tags: ['nightly'] });
|
|
959
559
|
|
|
960
|
-
|
|
961
|
-
const run = await manager.getRun(runId);
|
|
962
|
-
console.log(run.status); // 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
963
|
-
|
|
964
|
-
// List runs
|
|
965
|
-
const runs = await manager.listRuns({
|
|
966
|
-
status: 'running',
|
|
967
|
-
workflowId: 'daily-report',
|
|
968
|
-
fromDate: new Date('2024-01-01'),
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
// Cancel a run
|
|
560
|
+
const runId = await manager.schedule(workflow, { at: Date.now() + 60_000, input, priority: 1 });
|
|
972
561
|
await manager.cancel(runId);
|
|
973
562
|
|
|
974
|
-
|
|
975
|
-
const stats = await manager.getStats();
|
|
976
|
-
|
|
977
|
-
```
|
|
978
|
-
|
|
979
|
-
### JobScheduler
|
|
980
|
-
|
|
981
|
-
```typescript
|
|
982
|
-
import { createJobScheduler, PriorityQueue } from '@cogitator-ai/workflows';
|
|
983
|
-
|
|
984
|
-
const scheduler = createJobScheduler({
|
|
985
|
-
concurrency: 5,
|
|
986
|
-
maxQueueSize: 1000,
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
// Add jobs with priority
|
|
990
|
-
scheduler.enqueue({ id: 'job-1', payload: data1, priority: 1 });
|
|
991
|
-
scheduler.enqueue({ id: 'job-2', payload: data2, priority: 10 }); // Higher priority
|
|
992
|
-
|
|
993
|
-
// Process jobs
|
|
994
|
-
scheduler.process(async (job) => {
|
|
995
|
-
await processJob(job.payload);
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
scheduler.start();
|
|
563
|
+
const runs = await manager.listRuns({ status: 'failed', workflowName: 'report', limit: 20 });
|
|
564
|
+
const stats = await manager.getStats('report');
|
|
565
|
+
await manager.retry(runs[0].id);
|
|
999
566
|
```
|
|
1000
567
|
|
|
1001
568
|
---
|