@q1k-oss/btree-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.
Files changed (203) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/CLAUDE.md +181 -0
  3. package/LICENSE +21 -0
  4. package/README.md +920 -0
  5. package/behaviour-tree-workflows-landing/index.html +16 -0
  6. package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
  7. package/behaviour-tree-workflows-landing/package.json +31 -0
  8. package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
  9. package/behaviour-tree-workflows-landing/src/App.css +103 -0
  10. package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
  11. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
  12. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
  13. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
  14. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
  15. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
  16. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
  17. package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
  18. package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
  19. package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
  20. package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
  21. package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
  22. package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
  23. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
  24. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
  25. package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
  26. package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
  27. package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
  28. package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
  29. package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
  30. package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
  31. package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
  32. package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
  33. package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
  34. package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
  35. package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
  36. package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
  37. package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
  38. package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
  39. package/demo-google-sheets.ts +181 -0
  40. package/demo-runtime-variables.ts +174 -0
  41. package/demo-template.ts +208 -0
  42. package/docs/ARCHITECTURE_SUMMARY.md +613 -0
  43. package/docs/NODE_REFERENCE.md +504 -0
  44. package/docs/README.md +53 -0
  45. package/docs/custom-nodes-architecture.md +826 -0
  46. package/docs/observability.md +175 -0
  47. package/docs/yaml-specification.md +990 -0
  48. package/examples/temporal/README.md +117 -0
  49. package/examples/temporal/activities.ts +373 -0
  50. package/examples/temporal/client.ts +115 -0
  51. package/examples/temporal/python-worker/activities.py +339 -0
  52. package/examples/temporal/python-worker/requirements.txt +12 -0
  53. package/examples/temporal/python-worker/worker.py +106 -0
  54. package/examples/temporal/worker.ts +66 -0
  55. package/examples/temporal/workflows.ts +6 -0
  56. package/examples/temporal/yaml-workflow-loader.ts +105 -0
  57. package/examples/yaml-test.ts +97 -0
  58. package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
  59. package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
  60. package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
  61. package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
  62. package/examples/yaml-workflows/05-order-processing.yaml +146 -0
  63. package/examples/yaml-workflows/06-activity-test.yaml +71 -0
  64. package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
  65. package/examples/yaml-workflows/08-file-processing.yaml +141 -0
  66. package/examples/yaml-workflows/09-http-request.yaml +137 -0
  67. package/examples/yaml-workflows/README.md +211 -0
  68. package/package.json +38 -0
  69. package/src/actions/code-execution.schema.ts +27 -0
  70. package/src/actions/code-execution.ts +218 -0
  71. package/src/actions/generate-file.test.ts +516 -0
  72. package/src/actions/generate-file.ts +166 -0
  73. package/src/actions/http-request.test.ts +784 -0
  74. package/src/actions/http-request.ts +228 -0
  75. package/src/actions/index.ts +20 -0
  76. package/src/actions/parse-file.test.ts +448 -0
  77. package/src/actions/parse-file.ts +139 -0
  78. package/src/actions/python-script.test.ts +439 -0
  79. package/src/actions/python-script.ts +154 -0
  80. package/src/base-node.test.ts +511 -0
  81. package/src/base-node.ts +605 -0
  82. package/src/behavior-tree.test.ts +431 -0
  83. package/src/behavior-tree.ts +283 -0
  84. package/src/blackboard.test.ts +222 -0
  85. package/src/blackboard.ts +192 -0
  86. package/src/composites/conditional.schema.ts +19 -0
  87. package/src/composites/conditional.test.ts +309 -0
  88. package/src/composites/conditional.ts +129 -0
  89. package/src/composites/for-each.schema.ts +23 -0
  90. package/src/composites/for-each.test.ts +254 -0
  91. package/src/composites/for-each.ts +132 -0
  92. package/src/composites/index.ts +15 -0
  93. package/src/composites/memory-sequence.schema.ts +19 -0
  94. package/src/composites/memory-sequence.test.ts +223 -0
  95. package/src/composites/memory-sequence.ts +98 -0
  96. package/src/composites/parallel.schema.ts +28 -0
  97. package/src/composites/parallel.test.ts +502 -0
  98. package/src/composites/parallel.ts +157 -0
  99. package/src/composites/reactive-sequence.schema.ts +19 -0
  100. package/src/composites/reactive-sequence.test.ts +170 -0
  101. package/src/composites/reactive-sequence.ts +85 -0
  102. package/src/composites/recovery.schema.ts +19 -0
  103. package/src/composites/recovery.test.ts +366 -0
  104. package/src/composites/recovery.ts +90 -0
  105. package/src/composites/selector.schema.ts +19 -0
  106. package/src/composites/selector.test.ts +387 -0
  107. package/src/composites/selector.ts +85 -0
  108. package/src/composites/sequence.schema.ts +19 -0
  109. package/src/composites/sequence.test.ts +337 -0
  110. package/src/composites/sequence.ts +72 -0
  111. package/src/composites/sub-tree.schema.ts +21 -0
  112. package/src/composites/sub-tree.test.ts +893 -0
  113. package/src/composites/sub-tree.ts +177 -0
  114. package/src/composites/while.schema.ts +24 -0
  115. package/src/composites/while.test.ts +381 -0
  116. package/src/composites/while.ts +149 -0
  117. package/src/data-store/index.ts +10 -0
  118. package/src/data-store/memory-store.ts +161 -0
  119. package/src/data-store/types.ts +94 -0
  120. package/src/debug/breakpoint.test.ts +47 -0
  121. package/src/debug/breakpoint.ts +30 -0
  122. package/src/debug/index.ts +17 -0
  123. package/src/debug/resume-point.test.ts +49 -0
  124. package/src/debug/resume-point.ts +29 -0
  125. package/src/decorators/delay.schema.ts +21 -0
  126. package/src/decorators/delay.test.ts +261 -0
  127. package/src/decorators/delay.ts +140 -0
  128. package/src/decorators/force-result.schema.ts +32 -0
  129. package/src/decorators/force-result.test.ts +133 -0
  130. package/src/decorators/force-result.ts +63 -0
  131. package/src/decorators/index.ts +13 -0
  132. package/src/decorators/invert.schema.ts +19 -0
  133. package/src/decorators/invert.test.ts +135 -0
  134. package/src/decorators/invert.ts +42 -0
  135. package/src/decorators/keep-running.schema.ts +20 -0
  136. package/src/decorators/keep-running.test.ts +105 -0
  137. package/src/decorators/keep-running.ts +49 -0
  138. package/src/decorators/precondition.schema.ts +19 -0
  139. package/src/decorators/precondition.test.ts +351 -0
  140. package/src/decorators/precondition.ts +139 -0
  141. package/src/decorators/repeat.schema.ts +21 -0
  142. package/src/decorators/repeat.test.ts +187 -0
  143. package/src/decorators/repeat.ts +94 -0
  144. package/src/decorators/run-once.schema.ts +19 -0
  145. package/src/decorators/run-once.test.ts +140 -0
  146. package/src/decorators/run-once.ts +61 -0
  147. package/src/decorators/soft-assert.schema.ts +19 -0
  148. package/src/decorators/soft-assert.test.ts +107 -0
  149. package/src/decorators/soft-assert.ts +68 -0
  150. package/src/decorators/timeout.schema.ts +21 -0
  151. package/src/decorators/timeout.test.ts +274 -0
  152. package/src/decorators/timeout.ts +159 -0
  153. package/src/errors.test.ts +63 -0
  154. package/src/errors.ts +34 -0
  155. package/src/events.test.ts +347 -0
  156. package/src/events.ts +183 -0
  157. package/src/index.ts +80 -0
  158. package/src/integrations/index.ts +30 -0
  159. package/src/integrations/integration-action.test.ts +571 -0
  160. package/src/integrations/integration-action.ts +233 -0
  161. package/src/integrations/piece-executor.ts +320 -0
  162. package/src/observability/execution-tracker.ts +320 -0
  163. package/src/observability/index.ts +23 -0
  164. package/src/observability/sinks.ts +138 -0
  165. package/src/observability/types.ts +130 -0
  166. package/src/registry-utils.ts +147 -0
  167. package/src/registry.test.ts +466 -0
  168. package/src/registry.ts +334 -0
  169. package/src/schemas/base.schema.ts +104 -0
  170. package/src/schemas/index.ts +223 -0
  171. package/src/schemas/integration.test.ts +238 -0
  172. package/src/schemas/tree-definition.schema.ts +170 -0
  173. package/src/schemas/validation.test.ts +146 -0
  174. package/src/schemas/validation.ts +122 -0
  175. package/src/scripting/index.ts +22 -0
  176. package/src/templates/template-loader.test.ts +281 -0
  177. package/src/templates/template-loader.ts +152 -0
  178. package/src/temporal-integration.test.ts +213 -0
  179. package/src/test-nodes.ts +259 -0
  180. package/src/types.ts +503 -0
  181. package/src/utilities/index.ts +17 -0
  182. package/src/utilities/log-message.test.ts +275 -0
  183. package/src/utilities/log-message.ts +134 -0
  184. package/src/utilities/regex-extract.test.ts +138 -0
  185. package/src/utilities/regex-extract.ts +108 -0
  186. package/src/utilities/variable-resolver.test.ts +416 -0
  187. package/src/utilities/variable-resolver.ts +318 -0
  188. package/src/utils/error-handler.test.ts +117 -0
  189. package/src/utils/error-handler.ts +48 -0
  190. package/src/utils/signal-check.test.ts +234 -0
  191. package/src/utils/signal-check.ts +140 -0
  192. package/src/yaml/errors.ts +143 -0
  193. package/src/yaml/index.ts +30 -0
  194. package/src/yaml/loader.ts +39 -0
  195. package/src/yaml/parser.ts +286 -0
  196. package/src/yaml/validation/semantic-validator.ts +196 -0
  197. package/templates/google-sheets/insert-row.yaml +76 -0
  198. package/templates/notification-sender.yaml +33 -0
  199. package/templates/order-validation.yaml +44 -0
  200. package/tsconfig.json +24 -0
  201. package/vitest.config.ts +25 -0
  202. package/workflows/order-processor.yaml +59 -0
  203. package/workflows/process-order-workflow.yaml +142 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * MemorySequence node - Remembers which children succeeded and skips them on retry
3
+ * Useful for long test sequences where early steps shouldn't re-run
4
+ */
5
+
6
+ import { ConfigurationError } from "../errors.js";
7
+ import {
8
+ type TemporalContext,
9
+ type NodeConfiguration,
10
+ NodeStatus,
11
+ } from "../types.js";
12
+ import { checkSignal } from "../utils/signal-check.js";
13
+ import { Sequence } from "./sequence.js";
14
+
15
+ /**
16
+ * MemorySequence extends Sequence with memory of completed children.
17
+ * When a child fails, subsequent retries will skip already-successful children.
18
+ * This is particularly useful for expensive setup steps that shouldn't be re-executed.
19
+ */
20
+ export class MemorySequence extends Sequence {
21
+ private completedChildren: Set<string> = new Set();
22
+
23
+ async executeTick(context: TemporalContext): Promise<NodeStatus> {
24
+ this.log(
25
+ `Ticking with ${this._children.length} children (${this.completedChildren.size} completed)`,
26
+ );
27
+
28
+ if (this._children.length === 0) {
29
+ return NodeStatus.SUCCESS;
30
+ }
31
+
32
+ // Start from first non-completed child
33
+ for (let i = 0; i < this._children.length; i++) {
34
+ // Check for cancellation before ticking each child
35
+ checkSignal(context.signal);
36
+
37
+ const child = this._children[i];
38
+ if (!child) {
39
+ throw new ConfigurationError(`Child at index ${i} is undefined`);
40
+ }
41
+
42
+ // Skip if already completed
43
+ if (this.completedChildren.has(child.id)) {
44
+ this.log(`Skipping completed child: ${child.name}`);
45
+ continue;
46
+ }
47
+
48
+ this.log(`Ticking child ${i}: ${child.name}`);
49
+ const childStatus = await child.tick(context);
50
+
51
+ switch (childStatus) {
52
+ case NodeStatus.SUCCESS:
53
+ this.log(`Child ${child.name} succeeded - remembering`);
54
+ this.completedChildren.add(child.id);
55
+ break;
56
+
57
+ case NodeStatus.FAILURE:
58
+ this.log(`Child ${child.name} failed - sequence fails`);
59
+ this._status = NodeStatus.FAILURE;
60
+ return NodeStatus.FAILURE;
61
+
62
+ case NodeStatus.RUNNING:
63
+ this.log(`Child ${child.name} is running`);
64
+ this._status = NodeStatus.RUNNING;
65
+ return NodeStatus.RUNNING;
66
+
67
+ default:
68
+ throw new Error(`Unexpected status from child: ${childStatus}`);
69
+ }
70
+ }
71
+
72
+ // All children succeeded
73
+ this.log("All children succeeded");
74
+ this._status = NodeStatus.SUCCESS;
75
+ return NodeStatus.SUCCESS;
76
+ }
77
+
78
+ protected onReset(): void {
79
+ super.onReset();
80
+ this.log("Clearing completed children memory");
81
+ this.completedChildren.clear();
82
+ }
83
+
84
+ protected onHalt(): void {
85
+ super.onHalt();
86
+ // Note: we don't clear memory on halt, only on reset
87
+ // This allows resuming after interruption
88
+ }
89
+ }
90
+
91
+ /**
92
+ * SequenceWithMemory is an alias for MemorySequence (BehaviorTree.CPP compatibility)
93
+ */
94
+ export class SequenceWithMemory extends MemorySequence {
95
+ constructor(config: NodeConfiguration) {
96
+ super({ ...config, type: "SequenceWithMemory" });
97
+ }
98
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Parallel composite configuration schema
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { createNodeSchema, validations } from "../schemas/base.schema.js";
7
+
8
+ /**
9
+ * Parallel execution strategy enum
10
+ */
11
+ export const parallelStrategySchema = z.enum(["strict", "any"]);
12
+
13
+ /**
14
+ * Schema for Parallel composite configuration
15
+ * Validates strategy and optional thresholds
16
+ */
17
+ export const parallelConfigurationSchema = createNodeSchema("Parallel", {
18
+ strategy: parallelStrategySchema.optional().default("strict"),
19
+ successThreshold: validations.positiveInteger("successThreshold").optional(),
20
+ failureThreshold: validations.positiveInteger("failureThreshold").optional(),
21
+ });
22
+
23
+ /**
24
+ * Validated Parallel configuration type
25
+ */
26
+ export type ValidatedParallelConfiguration = z.infer<
27
+ typeof parallelConfigurationSchema
28
+ >;
@@ -0,0 +1,502 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { ActionNode } from "../base-node.js";
3
+ import { ScopedBlackboard } from "../blackboard.js";
4
+ import { MockAction } from "../test-nodes.js";
5
+ import { type TemporalContext, NodeStatus } from "../types.js";
6
+ import { Parallel } from "./parallel.js";
7
+
8
+ /**
9
+ * Helper: Creates an action that completes after N ticks
10
+ */
11
+ class DelayedAction extends ActionNode {
12
+ private ticksRemaining: number;
13
+ private readonly totalTicks: number;
14
+ private readonly finalStatus: NodeStatus;
15
+
16
+ constructor(config: { id: string; ticks: number; finalStatus?: NodeStatus }) {
17
+ super(config);
18
+ this.totalTicks = config.ticks;
19
+ this.ticksRemaining = config.ticks;
20
+ this.finalStatus = config.finalStatus ?? NodeStatus.SUCCESS;
21
+ }
22
+
23
+ async executeTick(_context: TemporalContext): Promise<NodeStatus> {
24
+ this.log(
25
+ `Tick ${this.totalTicks - this.ticksRemaining + 1}/${this.totalTicks}`,
26
+ );
27
+
28
+ if (this.ticksRemaining > 0) {
29
+ this.ticksRemaining--;
30
+
31
+ if (this.ticksRemaining === 0) {
32
+ this.log(`Completing with ${this.finalStatus}`);
33
+ return this.finalStatus;
34
+ }
35
+
36
+ this.log("Still running");
37
+ return NodeStatus.RUNNING;
38
+ }
39
+
40
+ return this.finalStatus;
41
+ }
42
+
43
+ reset(): void {
44
+ super.reset();
45
+ this.ticksRemaining = this.totalTicks;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Helper: Creates an action that tracks execution timestamps
51
+ */
52
+ class TimestampedAction extends ActionNode {
53
+ public startTime: number = 0;
54
+ public completionTime: number = 0;
55
+ private delay: number;
56
+ private finalStatus: NodeStatus;
57
+
58
+ constructor(config: { id: string; delay: number; finalStatus?: NodeStatus }) {
59
+ super(config);
60
+ this.delay = config.delay;
61
+ this.finalStatus = config.finalStatus ?? NodeStatus.SUCCESS;
62
+ }
63
+
64
+ async executeTick(_context: TemporalContext): Promise<NodeStatus> {
65
+ this.startTime = Date.now();
66
+
67
+ // Simulate async work
68
+ await new Promise((resolve) => setTimeout(resolve, this.delay));
69
+
70
+ this.completionTime = Date.now();
71
+ return this.finalStatus;
72
+ }
73
+
74
+ reset(): void {
75
+ super.reset();
76
+ this.startTime = 0;
77
+ this.completionTime = 0;
78
+ }
79
+ }
80
+
81
+ describe("Parallel - Concurrent Execution", () => {
82
+ let context: TemporalContext;
83
+
84
+ beforeEach(() => {
85
+ context = {
86
+ blackboard: new ScopedBlackboard(),
87
+ timestamp: Date.now(),
88
+ deltaTime: 0,
89
+ };
90
+ });
91
+
92
+ describe("Basic Behavior", () => {
93
+ it("should return SUCCESS when empty", async () => {
94
+ const parallel = new Parallel({ id: "test-parallel" });
95
+ const status = await parallel.tick(context);
96
+ expect(status).toBe(NodeStatus.SUCCESS);
97
+ });
98
+
99
+ it("should execute all children concurrently (not sequentially)", async () => {
100
+ const parallel = new Parallel({ id: "test-parallel" });
101
+
102
+ // Create 3 children with 50ms delay each
103
+ const child1 = new TimestampedAction({ id: "child1", delay: 50 });
104
+ const child2 = new TimestampedAction({ id: "child2", delay: 50 });
105
+ const child3 = new TimestampedAction({ id: "child3", delay: 50 });
106
+
107
+ parallel.addChildren([child1, child2, child3]);
108
+
109
+ const overallStart = Date.now();
110
+ const status = await parallel.tick(context);
111
+ const overallEnd = Date.now();
112
+ const totalTime = overallEnd - overallStart;
113
+
114
+ // If sequential: would take ~150ms (50 * 3)
115
+ // If concurrent: should take ~50ms (max of all)
116
+ expect(totalTime).toBeLessThan(100); // Generous margin for CI
117
+ expect(status).toBe(NodeStatus.SUCCESS);
118
+
119
+ // All should start at roughly the same time
120
+ const startTimes = [
121
+ child1.startTime,
122
+ child2.startTime,
123
+ child3.startTime,
124
+ ];
125
+ const maxStartDiff = Math.max(...startTimes) - Math.min(...startTimes);
126
+ expect(maxStartDiff).toBeLessThan(20); // All start within 20ms
127
+ });
128
+ });
129
+
130
+ describe("Multi-Tick Support (RUNNING status)", () => {
131
+ it(
132
+ "should return RUNNING on first tick if children are not done",
133
+ async () => {
134
+ const parallel = new Parallel({ id: "test-parallel" });
135
+
136
+ // Child needs 3 ticks to complete
137
+ const child1 = new DelayedAction({ id: "child1", ticks: 3 });
138
+ const child2 = new MockAction({
139
+ id: "child2",
140
+ returnStatus: NodeStatus.SUCCESS,
141
+ });
142
+
143
+ parallel.addChildren([child1, child2]);
144
+
145
+ // Tick 1: child1 returns RUNNING, child2 returns SUCCESS
146
+ const status1 = await parallel.tick(context);
147
+ expect(status1).toBe(NodeStatus.RUNNING);
148
+ },
149
+ );
150
+
151
+ it("should continue tracking state across multiple ticks", async () => {
152
+ const parallel = new Parallel({ id: "test-parallel" });
153
+
154
+ const child1 = new DelayedAction({ id: "child1", ticks: 3 });
155
+ const child2 = new DelayedAction({ id: "child2", ticks: 2 });
156
+
157
+ parallel.addChildren([child1, child2]);
158
+
159
+ // Tick 1: Both running
160
+ const status1 = await parallel.tick(context);
161
+ expect(status1).toBe(NodeStatus.RUNNING);
162
+
163
+ // Tick 2: child2 done, child1 still running
164
+ const status2 = await parallel.tick(context);
165
+ expect(status2).toBe(NodeStatus.RUNNING);
166
+
167
+ // Tick 3: Both done
168
+ const status3 = await parallel.tick(context);
169
+ expect(status3).toBe(NodeStatus.SUCCESS);
170
+ });
171
+
172
+ it("should retick children that return RUNNING", async () => {
173
+ const parallel = new Parallel({ id: "test-parallel" });
174
+
175
+ let child1ExecutionCount = 0;
176
+ const child1 = new DelayedAction({ id: "child1", ticks: 2 });
177
+ const originalTick = child1.tick.bind(child1);
178
+ child1.tick = (ctx: TemporalContext) => {
179
+ child1ExecutionCount++;
180
+ return originalTick(ctx);
181
+ };
182
+
183
+ parallel.addChildren([child1]);
184
+
185
+ await parallel.tick(context); // Tick 1: Launch child (returns RUNNING)
186
+ expect(child1ExecutionCount).toBe(1);
187
+
188
+ await parallel.tick(context); // Tick 2: Retick child (completes)
189
+ expect(child1ExecutionCount).toBe(2);
190
+ });
191
+ });
192
+
193
+ describe("Strategy: strict (all must succeed)", () => {
194
+ it("should succeed when all children succeed", async () => {
195
+ const parallel = new Parallel({
196
+ id: "test-parallel",
197
+ strategy: "strict",
198
+ });
199
+
200
+ const child1 = new MockAction({
201
+ id: "child1",
202
+ returnStatus: NodeStatus.SUCCESS,
203
+ });
204
+ const child2 = new MockAction({
205
+ id: "child2",
206
+ returnStatus: NodeStatus.SUCCESS,
207
+ });
208
+ const child3 = new MockAction({
209
+ id: "child3",
210
+ returnStatus: NodeStatus.SUCCESS,
211
+ });
212
+
213
+ parallel.addChildren([child1, child2, child3]);
214
+
215
+ const status = await parallel.tick(context);
216
+ expect(status).toBe(NodeStatus.SUCCESS);
217
+ });
218
+
219
+ it("should fail if any child fails", async () => {
220
+ const parallel = new Parallel({
221
+ id: "test-parallel",
222
+ strategy: "strict",
223
+ });
224
+
225
+ const child1 = new MockAction({
226
+ id: "child1",
227
+ returnStatus: NodeStatus.SUCCESS,
228
+ });
229
+ const child2 = new MockAction({
230
+ id: "child2",
231
+ returnStatus: NodeStatus.FAILURE,
232
+ });
233
+ const child3 = new MockAction({
234
+ id: "child3",
235
+ returnStatus: NodeStatus.SUCCESS,
236
+ });
237
+
238
+ parallel.addChildren([child1, child2, child3]);
239
+
240
+ const status = await parallel.tick(context);
241
+ expect(status).toBe(NodeStatus.FAILURE);
242
+ });
243
+
244
+ it("should use strict strategy by default", async () => {
245
+ const parallel = new Parallel({ id: "test-parallel" }); // No strategy specified
246
+
247
+ const child1 = new MockAction({
248
+ id: "child1",
249
+ returnStatus: NodeStatus.SUCCESS,
250
+ });
251
+ const child2 = new MockAction({
252
+ id: "child2",
253
+ returnStatus: NodeStatus.FAILURE,
254
+ });
255
+
256
+ parallel.addChildren([child1, child2]);
257
+
258
+ const status = await parallel.tick(context);
259
+ expect(status).toBe(NodeStatus.FAILURE); // Should fail because default is strict
260
+ });
261
+
262
+ it("should wait for all children to complete before returning", async () => {
263
+ const parallel = new Parallel({
264
+ id: "test-parallel",
265
+ strategy: "strict",
266
+ });
267
+
268
+ const child1 = new DelayedAction({ id: "child1", ticks: 1 });
269
+ const child2 = new DelayedAction({ id: "child2", ticks: 3 });
270
+
271
+ parallel.addChildren([child1, child2]);
272
+
273
+ // Tick 1: child1 done, child2 still running
274
+ const status1 = await parallel.tick(context);
275
+ expect(status1).toBe(NodeStatus.RUNNING);
276
+
277
+ // Tick 2: child2 still running
278
+ const status2 = await parallel.tick(context);
279
+ expect(status2).toBe(NodeStatus.RUNNING);
280
+
281
+ // Tick 3: Both done
282
+ const status3 = await parallel.tick(context);
283
+ expect(status3).toBe(NodeStatus.SUCCESS);
284
+ });
285
+ });
286
+
287
+ describe("Strategy: unknown (at least one must succeed)", () => {
288
+ it("should succeed if at least one child succeeds", async () => {
289
+ const parallel = new Parallel({
290
+ id: "test-parallel",
291
+ strategy: "any",
292
+ });
293
+
294
+ const child1 = new MockAction({
295
+ id: "child1",
296
+ returnStatus: NodeStatus.FAILURE,
297
+ });
298
+ const child2 = new MockAction({
299
+ id: "child2",
300
+ returnStatus: NodeStatus.SUCCESS,
301
+ });
302
+ const child3 = new MockAction({
303
+ id: "child3",
304
+ returnStatus: NodeStatus.FAILURE,
305
+ });
306
+
307
+ parallel.addChildren([child1, child2, child3]);
308
+
309
+ const status = await parallel.tick(context);
310
+ expect(status).toBe(NodeStatus.SUCCESS);
311
+ });
312
+
313
+ it("should fail if all children fail", async () => {
314
+ const parallel = new Parallel({
315
+ id: "test-parallel",
316
+ strategy: "any",
317
+ });
318
+
319
+ const child1 = new MockAction({
320
+ id: "child1",
321
+ returnStatus: NodeStatus.FAILURE,
322
+ });
323
+ const child2 = new MockAction({
324
+ id: "child2",
325
+ returnStatus: NodeStatus.FAILURE,
326
+ });
327
+ const child3 = new MockAction({
328
+ id: "child3",
329
+ returnStatus: NodeStatus.FAILURE,
330
+ });
331
+
332
+ parallel.addChildren([child1, child2, child3]);
333
+
334
+ const status = await parallel.tick(context);
335
+ expect(status).toBe(NodeStatus.FAILURE);
336
+ });
337
+
338
+ it("should still wait for all children to complete", async () => {
339
+ const parallel = new Parallel({
340
+ id: "test-parallel",
341
+ strategy: "any",
342
+ });
343
+
344
+ const child1 = new DelayedAction({
345
+ id: "child1",
346
+ ticks: 1,
347
+ finalStatus: NodeStatus.SUCCESS,
348
+ });
349
+ const child2 = new DelayedAction({
350
+ id: "child2",
351
+ ticks: 3,
352
+ finalStatus: NodeStatus.FAILURE,
353
+ });
354
+
355
+ parallel.addChildren([child1, child2]);
356
+
357
+ // Even though child1 succeeded on tick 1, we wait for child2
358
+ const status1 = await parallel.tick(context);
359
+ expect(status1).toBe(NodeStatus.RUNNING);
360
+
361
+ const status2 = await parallel.tick(context);
362
+ expect(status2).toBe(NodeStatus.RUNNING);
363
+
364
+ const status3 = await parallel.tick(context);
365
+ expect(status3).toBe(NodeStatus.SUCCESS); // child1 succeeded
366
+ });
367
+ });
368
+
369
+ describe("Error Handling", () => {
370
+ it("should treat thrown errors as FAILURE", async () => {
371
+ const parallel = new Parallel({
372
+ id: "test-parallel",
373
+ strategy: "strict",
374
+ });
375
+
376
+ class ErrorAction extends ActionNode {
377
+ executeTick(_context: TemporalContext) {
378
+ throw new Error("Test error");
379
+ }
380
+ }
381
+
382
+ const child1 = new MockAction({
383
+ id: "child1",
384
+ returnStatus: NodeStatus.SUCCESS,
385
+ });
386
+ const child2 = new ErrorAction({ id: "child2" });
387
+
388
+ parallel.addChildren([child1, child2]);
389
+
390
+ const status = await parallel.tick(context);
391
+ expect(status).toBe(NodeStatus.FAILURE);
392
+ });
393
+
394
+ it("should continue executing other children when one throws", async () => {
395
+ const parallel = new Parallel({
396
+ id: "test-parallel",
397
+ strategy: "any",
398
+ });
399
+
400
+ class ErrorAction extends ActionNode {
401
+ executeTick(_context: TemporalContext) {
402
+ throw new Error("Test error");
403
+ }
404
+ }
405
+
406
+ let _child1Executed = false;
407
+ class TrackingAction extends ActionNode {
408
+ async executeTick(_context: TemporalContext): Promise<NodeStatus> {
409
+ _child1Executed = true;
410
+ return NodeStatus.SUCCESS;
411
+ }
412
+ }
413
+
414
+ const child1 = new TrackingAction({ id: "child1" });
415
+ const child2 = new ErrorAction({ id: "child2" });
416
+
417
+ parallel.addChildren([child1, child2]);
418
+
419
+ const status = await parallel.tick(context);
420
+
421
+ // With "any" strategy: succeeds if ANY child succeeds, fails only if ALL fail
422
+ // Since child1 succeeds, parallel should return SUCCESS
423
+ expect(status).toBe(NodeStatus.SUCCESS);
424
+ // Both children should have executed (errors are caught, not propagated)
425
+ expect(_child1Executed).toBe(true);
426
+ });
427
+ });
428
+
429
+ describe("State Management", () => {
430
+ it("should reset state on halt", async () => {
431
+ const parallel = new Parallel({ id: "test-parallel" });
432
+
433
+ const child1 = new DelayedAction({ id: "child1", ticks: 3 });
434
+ parallel.addChildren([child1]);
435
+
436
+ // Start execution
437
+ const status1 = await parallel.tick(context);
438
+ expect(status1).toBe(NodeStatus.RUNNING);
439
+
440
+ // Halt - this clears internal state
441
+ parallel.halt();
442
+
443
+ // Child needs to be reset too for fresh start
444
+ child1.reset();
445
+
446
+ // Should be able to start fresh
447
+ const status2 = await parallel.tick(context);
448
+ expect(status2).toBe(NodeStatus.RUNNING);
449
+ });
450
+
451
+ it("should reset state on reset", async () => {
452
+ const parallel = new Parallel({ id: "test-parallel" });
453
+
454
+ const child1 = new DelayedAction({ id: "child1", ticks: 2 });
455
+ parallel.addChildren([child1]);
456
+
457
+ // Complete execution
458
+ await parallel.tick(context);
459
+ await parallel.tick(context);
460
+
461
+ // Reset
462
+ parallel.reset();
463
+ child1.reset();
464
+
465
+ // Should be able to run again
466
+ const status1 = await parallel.tick(context);
467
+ expect(status1).toBe(NodeStatus.RUNNING);
468
+ });
469
+ });
470
+
471
+ describe("Real-World Scenario: WaitForNewPage + Click", () => {
472
+ it("should handle Playwright pattern correctly", async () => {
473
+ const parallel = new Parallel({
474
+ id: "wait-and-click",
475
+ strategy: "strict",
476
+ });
477
+
478
+ // Simulate WaitForNewPage: takes 2 ticks (waits for event)
479
+ const waitForPage = new DelayedAction({
480
+ id: "wait-for-page",
481
+ ticks: 2,
482
+ finalStatus: NodeStatus.SUCCESS,
483
+ });
484
+
485
+ // Simulate Click: completes immediately
486
+ const click = new MockAction({
487
+ id: "click",
488
+ returnStatus: NodeStatus.SUCCESS,
489
+ });
490
+
491
+ parallel.addChildren([waitForPage, click]);
492
+
493
+ // Tick 1: Click completes, WaitForPage still waiting
494
+ const status1 = await parallel.tick(context);
495
+ expect(status1).toBe(NodeStatus.RUNNING);
496
+
497
+ // Tick 2: WaitForPage completes
498
+ const status2 = await parallel.tick(context);
499
+ expect(status2).toBe(NodeStatus.SUCCESS);
500
+ });
501
+ });
502
+ });