@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/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.