@q1k-oss/behaviour-tree-workflows 0.0.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/LICENSE +21 -0
- package/README.md +920 -0
- package/dist/index.cjs +5011 -0
- package/dist/index.d.cts +3320 -0
- package/dist/index.d.ts +3320 -0
- package/dist/index.js +4879 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
# @q1k-oss/behaviour-tree-workflows
|
|
2
|
+
|
|
3
|
+
Core behavior tree implementation for TypeScript, designed for AI-native workflows.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **22 Production-Ready Nodes**: 11 composites + 10 decorators + 1 scripting node for comprehensive control flow
|
|
8
|
+
- ✅ **YAML Workflows**: Declarative workflow definitions with 4-stage validation pipeline and Zod schemas
|
|
9
|
+
- ✅ **Temporal Workflows**: Native integration with Temporal for durable, resumable workflow execution
|
|
10
|
+
- ✅ **Hierarchical Blackboard**: Scoped data storage with inheritance and deep cloning
|
|
11
|
+
- ✅ **Event System**: Observable node lifecycle events for real-time monitoring
|
|
12
|
+
- ✅ **Smart Execution Snapshots**: Capture-on-change with diffs & execution traces for efficient debugging
|
|
13
|
+
- ✅ **Type-Safe**: Strongly typed TypeScript with **534 tests passing** (89%+ coverage)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @q1k-oss/behaviour-tree
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Programmatic API
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import {
|
|
27
|
+
Sequence,
|
|
28
|
+
PrintAction,
|
|
29
|
+
ScopedBlackboard,
|
|
30
|
+
TickEngine
|
|
31
|
+
} from '@q1k-oss/behaviour-tree-workflows';
|
|
32
|
+
|
|
33
|
+
// Create a behavior tree
|
|
34
|
+
const sequence = new Sequence({ id: 'main' });
|
|
35
|
+
sequence.addChildren([
|
|
36
|
+
new PrintAction({ id: 'hello', message: 'Hello' }),
|
|
37
|
+
new PrintAction({ id: 'world', message: 'World!' })
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// Execute it
|
|
41
|
+
const blackboard = new ScopedBlackboard();
|
|
42
|
+
const engine = new TickEngine(sequence);
|
|
43
|
+
await engine.tick(blackboard);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### YAML Workflows (Recommended)
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { Registry, registerStandardNodes, loadTreeFromYaml } from '@q1k-oss/behaviour-tree-workflows';
|
|
50
|
+
|
|
51
|
+
// Setup registry with all built-in nodes
|
|
52
|
+
const registry = new Registry();
|
|
53
|
+
registerStandardNodes(registry); // Registers all 32 built-in nodes!
|
|
54
|
+
|
|
55
|
+
// Add your custom nodes
|
|
56
|
+
registry.register('MyCustomAction', MyCustomAction, { category: 'action' });
|
|
57
|
+
|
|
58
|
+
// Load from YAML
|
|
59
|
+
const tree = loadTreeFromYaml(`
|
|
60
|
+
type: Sequence
|
|
61
|
+
id: my-workflow
|
|
62
|
+
children:
|
|
63
|
+
- type: PrintAction
|
|
64
|
+
id: hello
|
|
65
|
+
props:
|
|
66
|
+
message: "Hello from YAML!"
|
|
67
|
+
- type: MyCustomAction
|
|
68
|
+
id: custom
|
|
69
|
+
`, registry);
|
|
70
|
+
|
|
71
|
+
// Execute
|
|
72
|
+
await tree.execute();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## YAML Workflows
|
|
76
|
+
|
|
77
|
+
Define behavior trees declaratively using YAML with comprehensive validation:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
type: Sequence
|
|
81
|
+
id: user-onboarding
|
|
82
|
+
name: User Onboarding Flow
|
|
83
|
+
|
|
84
|
+
children:
|
|
85
|
+
- type: PrintAction
|
|
86
|
+
id: welcome
|
|
87
|
+
props:
|
|
88
|
+
message: "Welcome to our platform!"
|
|
89
|
+
|
|
90
|
+
- type: Timeout
|
|
91
|
+
id: profile-timeout
|
|
92
|
+
props:
|
|
93
|
+
timeoutMs: 30000
|
|
94
|
+
children:
|
|
95
|
+
- type: Sequence
|
|
96
|
+
id: profile-setup
|
|
97
|
+
children:
|
|
98
|
+
- type: PrintAction
|
|
99
|
+
id: request-info
|
|
100
|
+
props:
|
|
101
|
+
message: "Please complete your profile..."
|
|
102
|
+
|
|
103
|
+
- type: Delay
|
|
104
|
+
id: wait
|
|
105
|
+
props:
|
|
106
|
+
delayMs: 1000
|
|
107
|
+
children:
|
|
108
|
+
- type: PrintAction
|
|
109
|
+
id: processing
|
|
110
|
+
props:
|
|
111
|
+
message: "Processing..."
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Loading YAML Workflows
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import {
|
|
118
|
+
Registry,
|
|
119
|
+
registerStandardNodes,
|
|
120
|
+
loadTreeFromYaml,
|
|
121
|
+
loadTreeFromFile
|
|
122
|
+
} from '@q1k-oss/behaviour-tree-workflows';
|
|
123
|
+
|
|
124
|
+
// Setup registry with all 32 built-in nodes
|
|
125
|
+
const registry = new Registry();
|
|
126
|
+
registerStandardNodes(registry); // One line instead of 32!
|
|
127
|
+
|
|
128
|
+
// Optionally register your custom nodes
|
|
129
|
+
registry.register('MyCustomAction', MyCustomAction, { category: 'action' });
|
|
130
|
+
|
|
131
|
+
// Load from string
|
|
132
|
+
const yamlString = `
|
|
133
|
+
type: Sequence
|
|
134
|
+
id: my-workflow
|
|
135
|
+
children:
|
|
136
|
+
- type: PrintAction
|
|
137
|
+
id: hello
|
|
138
|
+
props:
|
|
139
|
+
message: "Hello from YAML!"
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
const tree = loadTreeFromYaml(yamlString, registry);
|
|
143
|
+
|
|
144
|
+
// Load from file
|
|
145
|
+
const tree = await loadTreeFromFile('./workflows/onboarding.yaml', registry);
|
|
146
|
+
|
|
147
|
+
// Execute like any other tree
|
|
148
|
+
const result = await tree.execute();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Built-in nodes** (automatically registered by `registerStandardNodes()`):
|
|
152
|
+
- **10 Composites**: Sequence, Selector, Parallel, ForEach, While, Conditional, ReactiveSequence, MemorySequence, Recovery, SubTree
|
|
153
|
+
- **10 Decorators**: Timeout, Delay, Repeat, Invert, ForceSuccess, ForceFailure, RunOnce, KeepRunningUntilFailure, Precondition, SoftAssert
|
|
154
|
+
- **9 Actions/Conditions**: PrintAction, MockAction, CounterAction, CheckCondition, AlwaysCondition, WaitAction, Script, LogMessage, RegexExtract
|
|
155
|
+
- **3 Test Nodes**: SuccessNode, FailureNode, RunningNode
|
|
156
|
+
|
|
157
|
+
### 4-Stage Validation Pipeline
|
|
158
|
+
|
|
159
|
+
YAML workflows undergo comprehensive validation before execution:
|
|
160
|
+
|
|
161
|
+
1. **YAML Syntax** - Validates well-formed YAML (indentation, structure)
|
|
162
|
+
2. **Tree Structure** - Ensures required fields (`type`, `id`) and correct data types
|
|
163
|
+
3. **Node Configuration** - Validates node-specific properties using Zod schemas
|
|
164
|
+
4. **Semantic Rules** - Checks ID uniqueness, child counts, circular references
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { validateYaml } from '@q1k-oss/behaviour-tree-workflows';
|
|
168
|
+
|
|
169
|
+
// Validate without executing
|
|
170
|
+
const result = validateYaml(yamlString, registry);
|
|
171
|
+
|
|
172
|
+
if (!result.valid) {
|
|
173
|
+
result.errors.forEach(error => {
|
|
174
|
+
console.error(error.format());
|
|
175
|
+
// Example output:
|
|
176
|
+
// "root.children[2].props.timeoutMs: Number must be greater than 0
|
|
177
|
+
// Suggestion: Use a positive timeout value in milliseconds"
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Benefits:**
|
|
183
|
+
- **Declarative**: Define workflows in YAML instead of TypeScript
|
|
184
|
+
- **Validated**: Comprehensive validation with helpful error messages
|
|
185
|
+
- **Type-Safe**: Runtime validation using Zod schemas
|
|
186
|
+
- **AI-Friendly**: Easy for LLMs to generate and modify workflows
|
|
187
|
+
|
|
188
|
+
See [YAML Specification](./docs/yaml-specification.md) for complete reference with examples for all 22 node types.
|
|
189
|
+
|
|
190
|
+
## Node Types
|
|
191
|
+
|
|
192
|
+
### Core Composites (11)
|
|
193
|
+
| Node | Purpose | Use Case |
|
|
194
|
+
|------|---------|----------|
|
|
195
|
+
| `Sequence` | Execute in order | Happy path flows |
|
|
196
|
+
| `Selector` | Try until success | Fallback strategies |
|
|
197
|
+
| `Parallel` | Execute concurrently | Parallel operations |
|
|
198
|
+
| `SubTree` | Reference reusable workflow | DRY workflows |
|
|
199
|
+
| `MemorySequence` | Skip completed | Expensive retries |
|
|
200
|
+
| `ReactiveSequence` | Always restart | Reactive monitoring |
|
|
201
|
+
| `Conditional` | If-then-else | Branching logic |
|
|
202
|
+
| `ForEach` | Iterate collection | Data-driven tests |
|
|
203
|
+
| `While` | Loop until false | Polling & waiting |
|
|
204
|
+
| `Recovery` | Try-catch-finally | Error handling |
|
|
205
|
+
|
|
206
|
+
### Advanced Decorators (6)
|
|
207
|
+
| Node | Purpose | Use Case |
|
|
208
|
+
|------|---------|----------|
|
|
209
|
+
| `Invert` | Flip result | Negate conditions |
|
|
210
|
+
| `Timeout` | Time limit | Prevent hangs |
|
|
211
|
+
| `Delay` | Add delay | Rate limiting |
|
|
212
|
+
| `Repeat` | Execute N times | Loops |
|
|
213
|
+
| `RunOnce` | Execute once | Expensive init |
|
|
214
|
+
| `ForceSuccess/Failure` | Override result | Graceful degradation |
|
|
215
|
+
| `KeepRunningUntilFailure` | Loop while success | Pagination |
|
|
216
|
+
| `Precondition` | Check prerequisites | Validation |
|
|
217
|
+
| `SoftAssert` | Non-critical checks | Continue on failure |
|
|
218
|
+
|
|
219
|
+
> **Note**: For retry functionality in Temporal workflows, use [Temporal's native RetryPolicy](https://docs.temporal.io/develop/typescript/failure-detection#retry-policy) instead of a decorator.
|
|
220
|
+
|
|
221
|
+
### Scripting Node (1)
|
|
222
|
+
| Node | Purpose | Use Case |
|
|
223
|
+
|------|---------|----------|
|
|
224
|
+
| `Script` | Execute scripts | Blackboard manipulation, calculations, validations |
|
|
225
|
+
|
|
226
|
+
The **Script** node enables blackboard manipulation through a simple scripting DSL:
|
|
227
|
+
|
|
228
|
+
**Supported Operations:**
|
|
229
|
+
- ✅ Variable assignments (`x = 10`)
|
|
230
|
+
- ✅ Arithmetic (`+`, `-`, `*`, `/`, `%`)
|
|
231
|
+
- ✅ Comparisons (`==`, `!=`, `>`, `<`, `>=`, `<=`)
|
|
232
|
+
- ✅ Logical operators (`&&`, `||`, `!`)
|
|
233
|
+
- ✅ String concatenation
|
|
234
|
+
- ✅ Property access (`user.profile.name`)
|
|
235
|
+
|
|
236
|
+
**Example: Store and Verify Pattern**
|
|
237
|
+
```typescript
|
|
238
|
+
// Store values
|
|
239
|
+
const storeScript = new Script({
|
|
240
|
+
id: 'store-data',
|
|
241
|
+
textContent: `
|
|
242
|
+
pageTitle = "Shopping Cart"
|
|
243
|
+
elementCount = 5
|
|
244
|
+
total = price * quantity
|
|
245
|
+
`
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Verify stored values
|
|
249
|
+
const verifyScript = new Script({
|
|
250
|
+
id: 'verify-data',
|
|
251
|
+
textContent: `
|
|
252
|
+
titleMatches = pageTitle == "Shopping Cart"
|
|
253
|
+
hasItems = elementCount > 0
|
|
254
|
+
isValid = titleMatches && hasItems
|
|
255
|
+
`
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**More Examples:**
|
|
260
|
+
```typescript
|
|
261
|
+
// Calculate order total with discount
|
|
262
|
+
new Script({
|
|
263
|
+
id: 'calculate',
|
|
264
|
+
textContent: `
|
|
265
|
+
subtotal = price * quantity
|
|
266
|
+
discount = subtotal * 0.1
|
|
267
|
+
total = subtotal - discount
|
|
268
|
+
`
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Validate form data
|
|
272
|
+
new Script({
|
|
273
|
+
id: 'validate',
|
|
274
|
+
textContent: `
|
|
275
|
+
hasUsername = username != null
|
|
276
|
+
isAdult = age >= 18
|
|
277
|
+
isValid = hasUsername && isAdult
|
|
278
|
+
`
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Format display strings
|
|
282
|
+
new Script({
|
|
283
|
+
id: 'format',
|
|
284
|
+
textContent: `
|
|
285
|
+
fullName = firstName + " " + lastName
|
|
286
|
+
greeting = "Hello, " + fullName + "!"
|
|
287
|
+
`
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Key Concepts
|
|
292
|
+
|
|
293
|
+
### Node Status
|
|
294
|
+
```typescript
|
|
295
|
+
enum NodeStatus {
|
|
296
|
+
SUCCESS, // Completed successfully
|
|
297
|
+
FAILURE, // Failed
|
|
298
|
+
RUNNING, // Still executing (async)
|
|
299
|
+
IDLE // Not started
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Blackboard (Scoped State)
|
|
304
|
+
```typescript
|
|
305
|
+
const blackboard = new ScopedBlackboard('root');
|
|
306
|
+
blackboard.set('userId', 123);
|
|
307
|
+
|
|
308
|
+
// Create child scope with inheritance
|
|
309
|
+
const stepScope = blackboard.createScope('step1');
|
|
310
|
+
stepScope.get('userId'); // Returns 123 (inherited)
|
|
311
|
+
stepScope.set('token', 'abc'); // Local to step1
|
|
312
|
+
|
|
313
|
+
// Parent doesn't see child values
|
|
314
|
+
blackboard.get('token'); // undefined
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Step Nodes (Scoped Blackboard)
|
|
318
|
+
```typescript
|
|
319
|
+
const loginStep = new Step({
|
|
320
|
+
id: 'login',
|
|
321
|
+
name: 'Login',
|
|
322
|
+
nlDescription: 'Login with valid credentials',
|
|
323
|
+
generated: false
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Variables set in loginStep are isolated from other steps
|
|
327
|
+
loginStep.addChild(new SetVariable({ key: 'sessionToken', value: 'xyz' }));
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Async Execution
|
|
331
|
+
```typescript
|
|
332
|
+
const engine = new TickEngine(tree);
|
|
333
|
+
|
|
334
|
+
// Single tick
|
|
335
|
+
await engine.tick(blackboard);
|
|
336
|
+
|
|
337
|
+
// Tick until non-RUNNING (for async operations)
|
|
338
|
+
await engine.tickWhileRunning(blackboard, maxTicks);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Tick Loop Optimization
|
|
342
|
+
|
|
343
|
+
By default, the TickEngine uses auto exponential backoff for optimal performance:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// Default: Auto mode (exponential backoff)
|
|
347
|
+
const engine = new TickEngine(tree);
|
|
348
|
+
// Tick delays: 0→0→0→0→0→1→2→4→8→16ms (capped)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The delay strategy automatically resets when:
|
|
352
|
+
- **Node completes**: Status changes from RUNNING → SUCCESS/FAILURE
|
|
353
|
+
- **New operation starts**: Status changes from SUCCESS/FAILURE → RUNNING
|
|
354
|
+
|
|
355
|
+
This ensures each operation gets optimal performance regardless of previous operation timing.
|
|
356
|
+
|
|
357
|
+
For debugging or specific requirements, use fixed delays:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// Fixed delay mode
|
|
361
|
+
const engine = new TickEngine(tree, { tickDelayMs: 10 });
|
|
362
|
+
|
|
363
|
+
// Immediate mode (legacy behavior)
|
|
364
|
+
const engine = new TickEngine(tree, { tickDelayMs: 0 });
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Benefits of Auto Mode:**
|
|
368
|
+
- Fast operations (< 200ms): Complete quickly with minimal overhead
|
|
369
|
+
- Slow operations (> 1s): Reduce CPU usage by ~80%
|
|
370
|
+
- Polling scenarios: Automatically adapt to operation timing
|
|
371
|
+
|
|
372
|
+
## Loading External Data with Script Node
|
|
373
|
+
|
|
374
|
+
The **Script node** provides built-in functions to load test data and environment variables into the blackboard. This enables clean separation: Script handles external data, atoms consume from blackboard.
|
|
375
|
+
|
|
376
|
+
### Built-in Functions
|
|
377
|
+
|
|
378
|
+
#### `param(key)` - Load Test Data
|
|
379
|
+
Access test parameters from CSV files, data tables, or test runs:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
import { Script } from '@q1k-oss/behaviour-tree-workflows';
|
|
383
|
+
|
|
384
|
+
// Setup test data
|
|
385
|
+
const context = {
|
|
386
|
+
blackboard: new ScopedBlackboard(),
|
|
387
|
+
timestamp: Date.now(),
|
|
388
|
+
testData: new Map([
|
|
389
|
+
['username', 'john.doe'],
|
|
390
|
+
['password', 'secret123'],
|
|
391
|
+
['age', 25]
|
|
392
|
+
])
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// Load test data into blackboard
|
|
396
|
+
const script = new Script({
|
|
397
|
+
id: 'load-data',
|
|
398
|
+
textContent: `
|
|
399
|
+
username = param("username")
|
|
400
|
+
password = param("password")
|
|
401
|
+
age = param("age")
|
|
402
|
+
`
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await script.tick(context);
|
|
406
|
+
|
|
407
|
+
// Now atoms can access from blackboard
|
|
408
|
+
console.log(context.blackboard.get('username')); // 'john.doe'
|
|
409
|
+
console.log(context.blackboard.get('age')); // 25
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### `env(key)` - Load Environment Variables
|
|
413
|
+
Access environment configuration at runtime:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
process.env.BASE_URL = 'https://staging.example.com';
|
|
417
|
+
process.env.API_KEY = 'test-key-123';
|
|
418
|
+
|
|
419
|
+
const script = new Script({
|
|
420
|
+
id: 'load-env',
|
|
421
|
+
textContent: `
|
|
422
|
+
baseUrl = env("BASE_URL")
|
|
423
|
+
apiKey = env("API_KEY")
|
|
424
|
+
`
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await script.tick(context);
|
|
428
|
+
|
|
429
|
+
console.log(context.blackboard.get('baseUrl')); // 'https://staging.example.com'
|
|
430
|
+
console.log(context.blackboard.get('apiKey')); // 'test-key-123'
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Computed Values
|
|
434
|
+
|
|
435
|
+
Scripts can build derived values from test data and environment:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
const script = new Script({
|
|
439
|
+
id: 'build-url',
|
|
440
|
+
textContent: `
|
|
441
|
+
// Load external data
|
|
442
|
+
baseUrl = env("BASE_URL")
|
|
443
|
+
userId = param("userId")
|
|
444
|
+
postId = param("postId")
|
|
445
|
+
|
|
446
|
+
// Build computed URL
|
|
447
|
+
apiUrl = baseUrl + "/users/" + userId + "/posts/" + postId
|
|
448
|
+
|
|
449
|
+
// Conditional logic
|
|
450
|
+
timeout = userId > 1000 ? 30000 : 5000
|
|
451
|
+
`
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
await script.tick(context);
|
|
455
|
+
|
|
456
|
+
// Atoms read computed values from blackboard
|
|
457
|
+
console.log(context.blackboard.get('apiUrl'));
|
|
458
|
+
// Result: 'https://staging.example.com/users/123/posts/456'
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Benefits
|
|
462
|
+
|
|
463
|
+
**✅ Separation of Concerns**
|
|
464
|
+
- Script: External data access (`param()`, `env()`)
|
|
465
|
+
- Atoms: Browser automation (click, fill, navigate)
|
|
466
|
+
- Blackboard: Data exchange layer
|
|
467
|
+
|
|
468
|
+
**✅ Explicit Data Flow**
|
|
469
|
+
- Easy to debug: inspect blackboard after Script execution
|
|
470
|
+
- No hidden resolution in atoms
|
|
471
|
+
|
|
472
|
+
**✅ Powerful Transformations**
|
|
473
|
+
- Build URLs from multiple sources
|
|
474
|
+
- Perform calculations with test data
|
|
475
|
+
- Apply conditional logic
|
|
476
|
+
- String concatenation and formatting
|
|
477
|
+
|
|
478
|
+
**✅ Extensible**
|
|
479
|
+
- Easy to add more built-in functions: `localStorage()`, `fetch()`
|
|
480
|
+
- Future: async functions for API calls
|
|
481
|
+
|
|
482
|
+
## Advanced Features
|
|
483
|
+
|
|
484
|
+
### 🌊 Temporal Workflows
|
|
485
|
+
|
|
486
|
+
Behavior trees can run as **Temporal workflows** for durable, fault-tolerant execution with native resumability.
|
|
487
|
+
|
|
488
|
+
#### YAML Workflows in Temporal (Recommended)
|
|
489
|
+
|
|
490
|
+
Define workflows in YAML and execute them in Temporal:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// yaml-workflow-loader.ts
|
|
494
|
+
import {
|
|
495
|
+
BehaviorTree,
|
|
496
|
+
Registry,
|
|
497
|
+
registerStandardNodes,
|
|
498
|
+
loadTreeFromYaml,
|
|
499
|
+
type WorkflowArgs,
|
|
500
|
+
type WorkflowResult,
|
|
501
|
+
} from '@q1k-oss/behaviour-tree-workflows';
|
|
502
|
+
|
|
503
|
+
export interface YamlWorkflowArgs extends WorkflowArgs {
|
|
504
|
+
yamlContent: string;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export async function yamlWorkflow(args: YamlWorkflowArgs): Promise<WorkflowResult> {
|
|
508
|
+
// Setup registry with all built-in nodes
|
|
509
|
+
const registry = new Registry();
|
|
510
|
+
registerStandardNodes(registry);
|
|
511
|
+
|
|
512
|
+
// Register custom nodes here
|
|
513
|
+
// registry.register('MyCustomNode', MyCustomNode, { category: 'action' });
|
|
514
|
+
|
|
515
|
+
// Parse YAML and execute
|
|
516
|
+
const root = loadTreeFromYaml(args.yamlContent, registry);
|
|
517
|
+
const tree = new BehaviorTree(root);
|
|
518
|
+
return tree.toWorkflow()(args);
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Client usage:**
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
import { readFileSync } from 'fs';
|
|
526
|
+
|
|
527
|
+
// Load YAML file (client-side, not in workflow sandbox)
|
|
528
|
+
const yamlContent = readFileSync('./workflows/order-processing.yaml', 'utf-8');
|
|
529
|
+
|
|
530
|
+
// Execute as Temporal workflow
|
|
531
|
+
const result = await client.workflow.execute('yamlWorkflow', {
|
|
532
|
+
taskQueue: 'behaviour-tree-workflows',
|
|
533
|
+
workflowId: `order-${Date.now()}`,
|
|
534
|
+
args: [{
|
|
535
|
+
input: {},
|
|
536
|
+
treeRegistry: new Registry(),
|
|
537
|
+
yamlContent // Pass YAML content to workflow
|
|
538
|
+
}]
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Example YAML workflow:**
|
|
543
|
+
|
|
544
|
+
```yaml
|
|
545
|
+
type: Sequence
|
|
546
|
+
id: order-processing
|
|
547
|
+
name: Order Processing Workflow
|
|
548
|
+
|
|
549
|
+
children:
|
|
550
|
+
- type: Timeout
|
|
551
|
+
id: validation-timeout
|
|
552
|
+
props:
|
|
553
|
+
timeoutMs: 5000
|
|
554
|
+
children:
|
|
555
|
+
- type: Parallel
|
|
556
|
+
id: validation-checks
|
|
557
|
+
props:
|
|
558
|
+
strategy: "strict"
|
|
559
|
+
children:
|
|
560
|
+
- type: PrintAction
|
|
561
|
+
id: validate-inventory
|
|
562
|
+
props:
|
|
563
|
+
message: "✓ Validating inventory..."
|
|
564
|
+
- type: PrintAction
|
|
565
|
+
id: validate-payment
|
|
566
|
+
props:
|
|
567
|
+
message: "✓ Validating payment..."
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
#### Programmatic Workflows
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
import { BehaviorTree, Sequence, PrintAction } from '@q1k-oss/behaviour-tree-workflows';
|
|
574
|
+
import type { WorkflowArgs, WorkflowResult } from '@q1k-oss/behaviour-tree-workflows';
|
|
575
|
+
|
|
576
|
+
export async function myWorkflow(args: WorkflowArgs): Promise<WorkflowResult> {
|
|
577
|
+
const root = new Sequence({ id: 'root' });
|
|
578
|
+
root.addChild(new PrintAction({ id: 'step1', message: 'Hello' }));
|
|
579
|
+
root.addChild(new PrintAction({ id: 'step2', message: 'World' }));
|
|
580
|
+
|
|
581
|
+
const tree = new BehaviorTree(root);
|
|
582
|
+
return tree.toWorkflow()(args);
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**Temporal Benefits:**
|
|
587
|
+
- **Automatic Resumability**: Workflows resume automatically after failures through event sourcing and deterministic replay
|
|
588
|
+
- **Durable Execution**: Workflow state persists across process crashes and restarts
|
|
589
|
+
- **Long-Running Workflows**: Run for days, weeks, or months without state loss
|
|
590
|
+
- **Built-in Retries**: Use Temporal's RetryPolicy for activities (no custom retry decorators needed)
|
|
591
|
+
- **Observability**: Full execution history and timeline in Temporal UI
|
|
592
|
+
|
|
593
|
+
**No Manual Resume Needed**: Unlike standalone execution, Temporal handles all resumability automatically. If a workflow crashes or times out, Temporal replays the event history and resumes from the exact point of failure.
|
|
594
|
+
|
|
595
|
+
See [`examples/temporal/`](./examples/temporal/) and [`examples/yaml-workflows/`](./examples/yaml-workflows/) for complete examples.
|
|
596
|
+
|
|
597
|
+
### 📡 Event System
|
|
598
|
+
|
|
599
|
+
Subscribe to node lifecycle events for real-time monitoring and observability:
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
import { NodeEventEmitter } from '@q1k-oss/behaviour-tree-workflows';
|
|
603
|
+
|
|
604
|
+
const eventEmitter = new NodeEventEmitter();
|
|
605
|
+
|
|
606
|
+
// Subscribe to events
|
|
607
|
+
eventEmitter.on('TICK_START', (event) => {
|
|
608
|
+
console.log(`Node ${event.nodeId} starting...`);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
eventEmitter.on('TICK_END', (event) => {
|
|
612
|
+
console.log(`Node ${event.nodeId} completed with ${event.status}`);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
eventEmitter.on('ERROR', (event) => {
|
|
616
|
+
console.error(`Node ${event.nodeId} errored:`, event.error);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Create engine with event emitter
|
|
620
|
+
const engine = new TickEngine(tree, { eventEmitter });
|
|
621
|
+
await engine.tick(blackboard);
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Available Events:**
|
|
625
|
+
- `TICK_START` - Node begins execution
|
|
626
|
+
- `TICK_END` - Node completes (SUCCESS/FAILURE/RUNNING)
|
|
627
|
+
- `ERROR` - Node throws an error
|
|
628
|
+
- `HALT` - Node is halted/cancelled
|
|
629
|
+
- `RESET` - Node is reset
|
|
630
|
+
- `STATUS_CHANGE` - Node status changes
|
|
631
|
+
|
|
632
|
+
**Use Cases:**
|
|
633
|
+
- Real-time test execution monitoring
|
|
634
|
+
- Performance profiling
|
|
635
|
+
- Custom logging and analytics
|
|
636
|
+
- Integration with external monitoring tools
|
|
637
|
+
|
|
638
|
+
See [`examples/event-monitoring.ts`](./examples/event-monitoring.ts) for complete examples.
|
|
639
|
+
|
|
640
|
+
### 📸 Smart Execution Snapshots
|
|
641
|
+
|
|
642
|
+
**⚡ Efficient**: Snapshots captured ONLY when blackboard state changes (not every tick!)
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
const engine = new TickEngine(tree, {
|
|
646
|
+
captureSnapshots: true // Auto-creates event emitter if needed
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
await engine.tick(blackboard);
|
|
650
|
+
|
|
651
|
+
// Get captured snapshots (only when state changed)
|
|
652
|
+
const snapshots = engine.getSnapshots();
|
|
653
|
+
|
|
654
|
+
snapshots.forEach(snap => {
|
|
655
|
+
console.log(`Tick #${snap.tickNumber}:`);
|
|
656
|
+
|
|
657
|
+
// See exactly what changed
|
|
658
|
+
console.log('Added:', snap.blackboardDiff.added);
|
|
659
|
+
console.log('Modified:', snap.blackboardDiff.modified);
|
|
660
|
+
console.log('Deleted:', snap.blackboardDiff.deleted);
|
|
661
|
+
|
|
662
|
+
// See which nodes executed
|
|
663
|
+
snap.executionTrace.forEach(node => {
|
|
664
|
+
console.log(` ${node.nodeName}: ${node.status} (${node.duration}ms)`);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Access full state
|
|
668
|
+
console.log('Total state:', snap.blackboard.toJSON());
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Always clear when done
|
|
672
|
+
engine.clearSnapshots();
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**📊 Rich Snapshot Data:**
|
|
676
|
+
```typescript
|
|
677
|
+
interface ExecutionSnapshot {
|
|
678
|
+
timestamp: number; // When captured
|
|
679
|
+
tickNumber: number; // Which tick
|
|
680
|
+
blackboard: IScopedBlackboard; // Deep clone of full state
|
|
681
|
+
blackboardDiff: { // What changed
|
|
682
|
+
added: Record<string, any>;
|
|
683
|
+
modified: Record<string, { from: any; to: any }>;
|
|
684
|
+
deleted: string[];
|
|
685
|
+
};
|
|
686
|
+
executionTrace: Array<{ // Which nodes ran
|
|
687
|
+
nodeId: string;
|
|
688
|
+
nodeName: string;
|
|
689
|
+
nodeType: string;
|
|
690
|
+
status: NodeStatus;
|
|
691
|
+
startTime: number;
|
|
692
|
+
duration: number;
|
|
693
|
+
}>;
|
|
694
|
+
rootNodeId: string;
|
|
695
|
+
rootStatus: NodeStatus;
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**🎯 Key Benefits:**
|
|
700
|
+
- **Efficient**: Only capture when state changes (not on every tick)
|
|
701
|
+
- **Precise Diffs**: See exactly what was added/modified/deleted
|
|
702
|
+
- **Execution Context**: Know which nodes executed in each snapshot
|
|
703
|
+
- **Time-Travel**: Jump to any point and inspect full state
|
|
704
|
+
- **AI-Ready**: Perfect for feeding to LLMs for root cause analysis
|
|
705
|
+
- **Zero Overhead When Disabled**: No performance impact when `captureSnapshots: false`
|
|
706
|
+
|
|
707
|
+
**💡 Use Cases:**
|
|
708
|
+
```typescript
|
|
709
|
+
// 1. Find when a value was set
|
|
710
|
+
const snapshot = snapshots.find(s =>
|
|
711
|
+
s.blackboardDiff.added.hasOwnProperty('username')
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
// 2. Track value evolution
|
|
715
|
+
snapshots.forEach(s => {
|
|
716
|
+
if (s.blackboard.has('counter')) {
|
|
717
|
+
console.log(`Tick #${s.tickNumber}: counter = ${s.blackboard.get('counter')}`);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// 3. Identify which action caused the bug
|
|
722
|
+
const bugSnapshot = snapshots[snapshots.length - 1];
|
|
723
|
+
console.log('Last executed nodes:', bugSnapshot.executionTrace);
|
|
724
|
+
|
|
725
|
+
// 4. Compare expected vs actual
|
|
726
|
+
if (testFailed) {
|
|
727
|
+
const finalSnapshot = snapshots[snapshots.length - 1];
|
|
728
|
+
console.log('Expected total:', expectedTotal);
|
|
729
|
+
console.log('Actual total:', finalSnapshot.blackboard.get('total'));
|
|
730
|
+
console.log('Diff:', finalSnapshot.blackboardDiff);
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
**⚠️ Important:**
|
|
735
|
+
- Snapshots accumulate across ticks - clear regularly for long sessions
|
|
736
|
+
- Each snapshot is a deep clone - memory grows with blackboard size
|
|
737
|
+
- Disable in production, enable only for debugging/test analysis
|
|
738
|
+
|
|
739
|
+
See [`examples/snapshot-debugging.ts`](./examples/snapshot-debugging.ts) for complete debugging workflow.
|
|
740
|
+
|
|
741
|
+
## Development
|
|
742
|
+
|
|
743
|
+
### Running Tests
|
|
744
|
+
```bash
|
|
745
|
+
npm test # Run all tests with coverage
|
|
746
|
+
npm run test:watch # Watch mode
|
|
747
|
+
npm run test:ui # UI mode
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Current status: **534 tests passing** across 36 test files with **89%+ coverage**
|
|
751
|
+
|
|
752
|
+
### Building
|
|
753
|
+
```bash
|
|
754
|
+
npm run build # Production build
|
|
755
|
+
npm run dev # Watch mode
|
|
756
|
+
npm run typecheck # Type checking
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Modifying the Script Grammar
|
|
760
|
+
|
|
761
|
+
The Script node uses ANTLR4 to parse scripts. Generated parser files are committed to avoid Java dependency for users.
|
|
762
|
+
|
|
763
|
+
**To modify the grammar (`src/scripting/ScriptLang.g4`):**
|
|
764
|
+
|
|
765
|
+
1. **Install Java** (required for ANTLR)
|
|
766
|
+
```bash
|
|
767
|
+
# macOS
|
|
768
|
+
brew install openjdk
|
|
769
|
+
|
|
770
|
+
# Ubuntu
|
|
771
|
+
apt install default-jre
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
2. **Regenerate parser**
|
|
775
|
+
```bash
|
|
776
|
+
npm run scripting:generate
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
3. **Commit generated files**
|
|
780
|
+
```bash
|
|
781
|
+
git add src/scripting/generated/
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**Note**: Regular users don't need Java - only developers modifying the grammar.
|
|
785
|
+
|
|
786
|
+
### Project Structure
|
|
787
|
+
```
|
|
788
|
+
src/
|
|
789
|
+
├── base-node.ts # BaseNode abstract class
|
|
790
|
+
├── types.ts # Core types & enums
|
|
791
|
+
├── blackboard.ts # ScopedBlackboard
|
|
792
|
+
├── tick-engine.ts # TickEngine
|
|
793
|
+
├── registry.ts # Node registry
|
|
794
|
+
├── composites/ # Composite nodes
|
|
795
|
+
│ ├── sequence.ts
|
|
796
|
+
│ ├── selector.ts
|
|
797
|
+
│ ├── parallel.ts
|
|
798
|
+
│ ├── step.ts # ✨ NEW
|
|
799
|
+
│ ├── memory-sequence.ts # ✨ NEW
|
|
800
|
+
│ ├── reactive-sequence.ts # ✨ NEW
|
|
801
|
+
│ ├── conditional.ts # ✨ NEW
|
|
802
|
+
│ ├── for-each.ts # ✨ NEW
|
|
803
|
+
│ ├── while.ts # ✨ NEW
|
|
804
|
+
│ └── recovery.ts # ✨ NEW
|
|
805
|
+
├── decorators/ # Decorator nodes
|
|
806
|
+
│ ├── invert.ts
|
|
807
|
+
│ ├── timeout.ts
|
|
808
|
+
│ ├── delay.ts
|
|
809
|
+
│ ├── force-result.ts # ✨ NEW
|
|
810
|
+
│ ├── repeat.ts # ✨ NEW
|
|
811
|
+
│ ├── keep-running.ts # ✨ NEW
|
|
812
|
+
│ ├── run-once.ts # ✨ NEW
|
|
813
|
+
│ ├── precondition.ts # ✨ NEW
|
|
814
|
+
│ └── soft-assert.ts # ✨ NEW
|
|
815
|
+
└── test-nodes.ts # Helper nodes for testing
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
## Contributing
|
|
819
|
+
|
|
820
|
+
### Adding New Nodes
|
|
821
|
+
|
|
822
|
+
1. **Create node file** in `src/composites/` or `src/decorators/`
|
|
823
|
+
2. **Extend base class**:
|
|
824
|
+
```typescript
|
|
825
|
+
import { CompositeNode } from '@q1k-oss/behaviour-tree-workflows';
|
|
826
|
+
import { TemporalContext, NodeStatus } from '@q1k-oss/behaviour-tree-workflows';
|
|
827
|
+
|
|
828
|
+
export class MyNode extends CompositeNode {
|
|
829
|
+
protected async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
830
|
+
// Implementation using async/await
|
|
831
|
+
const status = await this.child.tick(context);
|
|
832
|
+
return status;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
protected onHalt(): void { /* cleanup */ }
|
|
836
|
+
protected onReset(): void { /* reset state */ }
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
3. **Write tests** in `*.test.ts`:
|
|
841
|
+
- Basic success/failure cases
|
|
842
|
+
- RUNNING state handling
|
|
843
|
+
- Edge cases
|
|
844
|
+
- Reset/halt behavior
|
|
845
|
+
- Blackboard integration
|
|
846
|
+
|
|
847
|
+
4. **Export** in `src/index.ts` and update indexes
|
|
848
|
+
|
|
849
|
+
5. **Document** in examples/README.md
|
|
850
|
+
|
|
851
|
+
### Testing Guidelines
|
|
852
|
+
|
|
853
|
+
- Use `describe/it` structure with clear test names
|
|
854
|
+
- Test all status transitions (SUCCESS, FAILURE, RUNNING)
|
|
855
|
+
- Test edge cases (empty children, null values)
|
|
856
|
+
- Test state cleanup (halt, reset)
|
|
857
|
+
- Use helper nodes from `src/test-nodes.ts`
|
|
858
|
+
- Aim for >90% coverage
|
|
859
|
+
|
|
860
|
+
### Code Style
|
|
861
|
+
|
|
862
|
+
- Follow existing patterns in the codebase
|
|
863
|
+
- Use async/await for async operations
|
|
864
|
+
- Implement lifecycle methods (onHalt, onReset)
|
|
865
|
+
- Add logging with `this.log()`
|
|
866
|
+
- Document complex logic with comments
|
|
867
|
+
- Keep nodes focused (single responsibility)
|
|
868
|
+
|
|
869
|
+
### Error Handling with `_lastError`
|
|
870
|
+
|
|
871
|
+
Nodes that fail should provide meaningful error context via the `_lastError` property when the default error isn't descriptive enough:
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
catch (error) {
|
|
875
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
876
|
+
this._lastError = `Verification failed: expected "${expected}" within ${timeout}ms: ${errorMessage}`;
|
|
877
|
+
this.log(this._lastError);
|
|
878
|
+
return NodeStatus.FAILURE;
|
|
879
|
+
}
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**When to use:**
|
|
883
|
+
- Verification/assertion nodes (ExpectText, ExpectVisible, etc.)
|
|
884
|
+
- Nodes where users need to understand expected vs actual
|
|
885
|
+
- Any node where debugging requires clearer context
|
|
886
|
+
|
|
887
|
+
**When NOT needed:**
|
|
888
|
+
- Action nodes (Click, Fill) - underlying errors are usually descriptive
|
|
889
|
+
- Control flow nodes - children fail, not the composite itself
|
|
890
|
+
|
|
891
|
+
The `_lastError` is automatically surfaced via `tickWhileRunning()` result and the execution feedback system. See `.cursor/rules/node-error-handling.mdc` for detailed guidelines.
|
|
892
|
+
|
|
893
|
+
## Architecture Overview
|
|
894
|
+
|
|
895
|
+
**Core Principles:**
|
|
896
|
+
- **Nodes**: All nodes inherit from `BaseNode` and implement `tick(context)`
|
|
897
|
+
- **Status**: Every tick returns `SUCCESS | FAILURE | RUNNING | IDLE`
|
|
898
|
+
- **State**: `ScopedBlackboard` provides hierarchical data with inheritance
|
|
899
|
+
- **Execution**: Workflows execute via Temporal for production use, or standalone for testing/development
|
|
900
|
+
- **Async**: Async/await powered operations with proper RUNNING status propagation (parents observe RUNNING across ticks)
|
|
901
|
+
- **Temporal Integration**: Native workflow conversion via `tree.toWorkflow()` for durable execution
|
|
902
|
+
|
|
903
|
+
**Design Patterns:**
|
|
904
|
+
- **Composite Pattern**: Nodes contain child nodes
|
|
905
|
+
- **Visitor Pattern**: TickEngine visits tree during execution
|
|
906
|
+
- **Strategy Pattern**: Different node types implement different behaviors
|
|
907
|
+
- **Factory Pattern**: Registry creates nodes from definitions
|
|
908
|
+
- **Observer Pattern**: TickEngine callbacks (onTick, onError)
|
|
909
|
+
|
|
910
|
+
**Integration:**
|
|
911
|
+
- Registry pattern enables dynamic tree creation from JSON
|
|
912
|
+
- Scoped blackboard enables step isolation for test authoring
|
|
913
|
+
|
|
914
|
+
## License
|
|
915
|
+
|
|
916
|
+
MIT
|
|
917
|
+
|
|
918
|
+
## Credits
|
|
919
|
+
|
|
920
|
+
Inspired by [BehaviorTree.CPP](https://github.com/BehaviorTree/BehaviorTree.CPP) with adaptations for TypeScript.
|