@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,309 @@
1
+ /**
2
+ * Tests for Conditional node
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it } from "vitest";
6
+ import { ScopedBlackboard } from "../blackboard.js";
7
+ import { FailureNode, RunningNode, SuccessNode } from "../test-nodes.js";
8
+ import { type TemporalContext, NodeStatus } from "../types.js";
9
+ import { Conditional } from "./conditional.js";
10
+
11
+ describe("Conditional", () => {
12
+ let blackboard: ScopedBlackboard;
13
+ let context: TemporalContext;
14
+
15
+ beforeEach(() => {
16
+ blackboard = new ScopedBlackboard("root");
17
+ context = {
18
+ blackboard,
19
+ timestamp: Date.now(),
20
+ deltaTime: 0,
21
+ };
22
+ });
23
+
24
+ describe("If-Then Logic", () => {
25
+ it("should execute then branch when condition succeeds", async () => {
26
+ const conditional = new Conditional({ id: "cond1" });
27
+ const condition = new SuccessNode({ id: "condition" });
28
+ const thenBranch = new SuccessNode({ id: "then" });
29
+
30
+ conditional.addChild(condition);
31
+ conditional.addChild(thenBranch);
32
+
33
+ const result = await conditional.tick(context);
34
+ expect(result).toBe(NodeStatus.SUCCESS);
35
+ expect(thenBranch.status()).toBe(NodeStatus.SUCCESS);
36
+ });
37
+
38
+ it("should return then branch status", async () => {
39
+ const conditional = new Conditional({ id: "cond1" });
40
+ const condition = new SuccessNode({ id: "condition" });
41
+ const thenBranch = new FailureNode({ id: "then" });
42
+
43
+ conditional.addChild(condition);
44
+ conditional.addChild(thenBranch);
45
+
46
+ const result = await conditional.tick(context);
47
+ expect(result).toBe(NodeStatus.FAILURE);
48
+ });
49
+ });
50
+
51
+ describe("If-Else Logic", () => {
52
+ it("should execute else branch when condition fails", async () => {
53
+ const conditional = new Conditional({ id: "cond1" });
54
+ const condition = new FailureNode({ id: "condition" });
55
+ const thenBranch = new SuccessNode({ id: "then" });
56
+ const elseBranch = new SuccessNode({ id: "else" });
57
+
58
+ conditional.addChild(condition);
59
+ conditional.addChild(thenBranch);
60
+ conditional.addChild(elseBranch);
61
+
62
+ const result = await conditional.tick(context);
63
+ expect(result).toBe(NodeStatus.SUCCESS);
64
+ expect(thenBranch.status()).toBe(NodeStatus.IDLE); // Not executed
65
+ expect(elseBranch.status()).toBe(NodeStatus.SUCCESS);
66
+ });
67
+
68
+ it("should return else branch status", async () => {
69
+ const conditional = new Conditional({ id: "cond1" });
70
+ const condition = new FailureNode({ id: "condition" });
71
+ const thenBranch = new SuccessNode({ id: "then" });
72
+ const elseBranch = new FailureNode({ id: "else" });
73
+
74
+ conditional.addChild(condition);
75
+ conditional.addChild(thenBranch);
76
+ conditional.addChild(elseBranch);
77
+
78
+ const result = await conditional.tick(context);
79
+ expect(result).toBe(NodeStatus.FAILURE);
80
+ });
81
+
82
+ it(
83
+ "should return FAILURE when condition fails and no else branch",
84
+ async () => {
85
+ const conditional = new Conditional({ id: "cond1" });
86
+ const condition = new FailureNode({ id: "condition" });
87
+ const thenBranch = new SuccessNode({ id: "then" });
88
+
89
+ conditional.addChild(condition);
90
+ conditional.addChild(thenBranch);
91
+
92
+ const result = await conditional.tick(context);
93
+ expect(result).toBe(NodeStatus.FAILURE);
94
+ expect(thenBranch.status()).toBe(NodeStatus.IDLE);
95
+ },
96
+ );
97
+ });
98
+
99
+ describe("RUNNING State", () => {
100
+ it("should return RUNNING when condition is running", async () => {
101
+ const conditional = new Conditional({ id: "cond1" });
102
+ const condition = new RunningNode({ id: "condition" });
103
+ const thenBranch = new SuccessNode({ id: "then" });
104
+
105
+ conditional.addChild(condition);
106
+ conditional.addChild(thenBranch);
107
+
108
+ const result = await conditional.tick(context);
109
+ expect(result).toBe(NodeStatus.RUNNING);
110
+ expect(thenBranch.status()).toBe(NodeStatus.IDLE); // Not executed
111
+ });
112
+
113
+ it("should return RUNNING when then branch is running", async () => {
114
+ const conditional = new Conditional({ id: "cond1" });
115
+ const condition = new SuccessNode({ id: "condition" });
116
+ const thenBranch = new RunningNode({ id: "then" });
117
+
118
+ conditional.addChild(condition);
119
+ conditional.addChild(thenBranch);
120
+
121
+ const result = await conditional.tick(context);
122
+ expect(result).toBe(NodeStatus.RUNNING);
123
+ });
124
+
125
+ it("should return RUNNING when else branch is running", async () => {
126
+ const conditional = new Conditional({ id: "cond1" });
127
+ const condition = new FailureNode({ id: "condition" });
128
+ const thenBranch = new SuccessNode({ id: "then" });
129
+ const elseBranch = new RunningNode({ id: "else" });
130
+
131
+ conditional.addChild(condition);
132
+ conditional.addChild(thenBranch);
133
+ conditional.addChild(elseBranch);
134
+
135
+ const result = await conditional.tick(context);
136
+ expect(result).toBe(NodeStatus.RUNNING);
137
+ });
138
+ });
139
+
140
+ describe("Edge Cases", () => {
141
+ it("should enforce maximum 3 children", () => {
142
+ const conditional = new Conditional({ id: "cond1" });
143
+ conditional.addChild(new SuccessNode({ id: "child1" }));
144
+ conditional.addChild(new SuccessNode({ id: "child2" }));
145
+ conditional.addChild(new SuccessNode({ id: "child3" }));
146
+
147
+ expect(() => {
148
+ conditional.addChild(new SuccessNode({ id: "child4" }));
149
+ }).toThrow("Conditional can have maximum 3 children");
150
+ });
151
+
152
+ it("should return FAILURE without condition", async () => {
153
+ const conditional = new Conditional({ id: "cond1" });
154
+
155
+ const status = await conditional.tick(context);
156
+ expect(status).toBe(NodeStatus.FAILURE);
157
+ });
158
+
159
+ it("should return FAILURE without then branch", async () => {
160
+ const conditional = new Conditional({ id: "cond1" });
161
+ conditional.addChild(new SuccessNode({ id: "condition" }));
162
+
163
+ const status = await conditional.tick(context);
164
+ expect(status).toBe(NodeStatus.FAILURE);
165
+ });
166
+ });
167
+
168
+ describe("Multi-tick branch execution - FIXED BEHAVIOR", () => {
169
+ it(
170
+ "should check condition ONCE and not re-check while branch is RUNNING",
171
+ async () => {
172
+ let conditionTickCount = 0;
173
+ let branchTickCount = 0;
174
+
175
+ // Condition that counts how many times it's checked
176
+ class CountingCondition extends SuccessNode {
177
+ async tick(_context: TemporalContext): Promise<NodeStatus> {
178
+ conditionTickCount++;
179
+ console.log(`[CountingCondition] Tick #${conditionTickCount}`);
180
+ this._status = NodeStatus.SUCCESS;
181
+ return NodeStatus.SUCCESS;
182
+ }
183
+ }
184
+
185
+ // Branch that returns RUNNING for first 2 ticks, then SUCCESS
186
+ class MultiTickBranch extends SuccessNode {
187
+ async tick(context: TemporalContext): Promise<NodeStatus> {
188
+ const superTick = super.tick.bind(this);
189
+ branchTickCount++;
190
+ console.log(`[MultiTickBranch] Tick #${branchTickCount}`);
191
+
192
+ if (branchTickCount < 3) {
193
+ this._status = NodeStatus.RUNNING;
194
+ return NodeStatus.RUNNING;
195
+ }
196
+
197
+ return await superTick(context); // SUCCESS on tick 3
198
+ }
199
+ }
200
+
201
+ const condition = new CountingCondition({ id: "counting-condition" });
202
+ const thenBranch = new MultiTickBranch({ id: "multi-tick-branch" });
203
+ const conditional = new Conditional({
204
+ id: "test-conditional",
205
+ name: "test-conditional",
206
+ });
207
+
208
+ conditional.addChild(condition);
209
+ conditional.addChild(thenBranch);
210
+
211
+ // Tick 1: Should check condition, then execute branch (returns RUNNING)
212
+ const status1 = await conditional.tick(context);
213
+ expect(status1).toBe(NodeStatus.RUNNING);
214
+ expect(conditionTickCount).toBe(1); // Condition checked once
215
+ expect(branchTickCount).toBe(1); // Branch executed once
216
+
217
+ // Tick 2: FIXED: Should NOT re-check condition, just execute branch (returns RUNNING)
218
+ const status2 = await conditional.tick(context);
219
+ expect(status2).toBe(NodeStatus.RUNNING);
220
+ expect(conditionTickCount).toBe(1); // Still 1! Not re-checked
221
+ expect(branchTickCount).toBe(2); // Branch executed again
222
+
223
+ // Tick 3: FIXED: Should NOT re-check condition, just execute branch (returns SUCCESS)
224
+ const status3 = await conditional.tick(context);
225
+ expect(status3).toBe(NodeStatus.SUCCESS);
226
+ expect(conditionTickCount).toBe(1); // Still 1! Not re-checked
227
+ expect(branchTickCount).toBe(3); // Branch completes
228
+
229
+ // Summary: Condition was checked only 1 time (on first tick)
230
+ // This confirms the condition is NOT re-evaluated on subsequent ticks
231
+ console.log(
232
+ "\n✅ FIXED: Condition checked only 1 time, not re-checked during branch execution",
233
+ );
234
+ },
235
+ );
236
+
237
+ it(
238
+ "should NOT be affected if condition changes while branch is RUNNING",
239
+ async () => {
240
+ let branchTickCount = 0;
241
+ let conditionShouldSucceed = true;
242
+
243
+ // Condition that succeeds initially, then fails on subsequent ticks
244
+ class ChangeableCondition extends SuccessNode {
245
+ async tick(_context: TemporalContext): Promise<NodeStatus> {
246
+ const result = conditionShouldSucceed
247
+ ? NodeStatus.SUCCESS
248
+ : NodeStatus.FAILURE;
249
+ console.log(`[ChangeableCondition] Returning: ${result}`);
250
+ this._status = result;
251
+ return result;
252
+ }
253
+ }
254
+
255
+ // Branch that takes 3 ticks to complete
256
+ class SlowBranch extends SuccessNode {
257
+ async tick(context: TemporalContext): Promise<NodeStatus> {
258
+ const superTick = super.tick.bind(this);
259
+ branchTickCount++;
260
+ console.log(`[SlowBranch] Tick #${branchTickCount}`);
261
+
262
+ if (branchTickCount < 3) {
263
+ this._status = NodeStatus.RUNNING;
264
+ return NodeStatus.RUNNING;
265
+ }
266
+
267
+ return await superTick(context); // SUCCESS on tick 3
268
+ }
269
+ }
270
+
271
+ const condition = new ChangeableCondition({
272
+ id: "changeable-condition",
273
+ });
274
+ const thenBranch = new SlowBranch({ id: "slow-branch" });
275
+ const conditional = new Conditional({
276
+ id: "test-conditional",
277
+ name: "test-conditional",
278
+ });
279
+
280
+ conditional.addChild(condition);
281
+ conditional.addChild(thenBranch);
282
+
283
+ // Tick 1: Condition passes, branch returns RUNNING
284
+ const status1 = await conditional.tick(context);
285
+ expect(status1).toBe(NodeStatus.RUNNING);
286
+ expect(branchTickCount).toBe(1);
287
+
288
+ // Change condition to fail
289
+ conditionShouldSucceed = false;
290
+
291
+ // Tick 2: FIXED: Condition is NOT re-checked, branch continues
292
+ const status2 = await conditional.tick(context);
293
+ expect(status2).toBe(NodeStatus.RUNNING); // Still RUNNING!
294
+ expect(branchTickCount).toBe(2); // Branch continues executing
295
+
296
+ // Tick 3: FIXED: Branch completes successfully despite condition now failing
297
+ const status3 = await conditional.tick(context);
298
+ expect(status3).toBe(NodeStatus.SUCCESS); // Branch completes!
299
+ expect(branchTickCount).toBe(3); // Branch executed all 3 ticks
300
+
301
+ // This demonstrates the fix: branch execution is NOT interrupted
302
+ // even though the condition would now fail if re-checked
303
+ console.log(
304
+ "\n✅ SAFE: Branch execution continues uninterrupted despite condition change",
305
+ );
306
+ },
307
+ );
308
+ });
309
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Conditional node - If-then-else logic for behavior trees
3
+ */
4
+
5
+ import { CompositeNode } from "../base-node.js";
6
+ import { ConfigurationError } from "../errors.js";
7
+ import { type TemporalContext, NodeStatus, type TreeNode } from "../types.js";
8
+ import { checkSignal } from "../utils/signal-check.js";
9
+
10
+ /**
11
+ * Conditional implements if-then-else logic.
12
+ * Structure:
13
+ * - First child = condition
14
+ * - Second child = then branch
15
+ * - Third child (optional) = else branch
16
+ */
17
+ export class Conditional extends CompositeNode {
18
+ private condition?: TreeNode;
19
+ private thenBranch?: TreeNode;
20
+ private elseBranch?: TreeNode;
21
+ private conditionEvaluated: boolean = false;
22
+ private selectedBranch?: TreeNode;
23
+
24
+ /**
25
+ * Override addChild to enforce conditional structure
26
+ */
27
+ addChild(child: TreeNode): void {
28
+ if (!this.condition) {
29
+ this.condition = child;
30
+ this._children.push(child);
31
+ child.parent = this;
32
+ } else if (!this.thenBranch) {
33
+ this.thenBranch = child;
34
+ this._children.push(child);
35
+ child.parent = this;
36
+ } else if (!this.elseBranch) {
37
+ this.elseBranch = child;
38
+ this._children.push(child);
39
+ child.parent = this;
40
+ } else {
41
+ throw new ConfigurationError(
42
+ "Conditional can have maximum 3 children (condition, then, else)",
43
+ );
44
+ }
45
+ }
46
+
47
+ async executeTick(context: TemporalContext): Promise<NodeStatus> {
48
+ // Check for cancellation before processing conditional
49
+ checkSignal(context.signal);
50
+
51
+ if (!this.condition) {
52
+ throw new Error("Conditional requires at least a condition child");
53
+ }
54
+
55
+ if (!this.thenBranch) {
56
+ throw new Error(
57
+ "Conditional requires at least condition and then branch",
58
+ );
59
+ }
60
+
61
+ // Only evaluate condition if not already evaluated
62
+ if (!this.conditionEvaluated) {
63
+ this.log("Evaluating condition");
64
+ const conditionStatus = await this.condition.tick(context);
65
+
66
+ switch (conditionStatus) {
67
+ case NodeStatus.SUCCESS:
68
+ this.log("Condition succeeded - will execute then branch");
69
+ this.selectedBranch = this.thenBranch;
70
+ this.conditionEvaluated = true;
71
+ break;
72
+
73
+ case NodeStatus.FAILURE:
74
+ if (this.elseBranch) {
75
+ this.log("Condition failed - will execute else branch");
76
+ this.selectedBranch = this.elseBranch;
77
+ this.conditionEvaluated = true;
78
+ } else {
79
+ this.log("Condition failed - no else branch, returning FAILURE");
80
+ this._status = NodeStatus.FAILURE;
81
+ return NodeStatus.FAILURE;
82
+ }
83
+ break;
84
+
85
+ case NodeStatus.RUNNING:
86
+ this.log("Condition is running");
87
+ this._status = NodeStatus.RUNNING;
88
+ return NodeStatus.RUNNING;
89
+
90
+ default:
91
+ throw new Error(
92
+ `Unexpected status from condition: ${conditionStatus}`,
93
+ );
94
+ }
95
+ } else {
96
+ this.log("Condition already evaluated - continuing branch execution");
97
+ }
98
+
99
+ // Execute selected branch
100
+ if (!this.selectedBranch) {
101
+ throw new Error("No branch selected for execution");
102
+ }
103
+
104
+ const branchStatus = await this.selectedBranch.tick(context);
105
+ this._status = branchStatus;
106
+
107
+ // Reset flag when branch completes
108
+ if (branchStatus !== NodeStatus.RUNNING) {
109
+ this.log("Branch completed - resetting condition check flag");
110
+ this.conditionEvaluated = false;
111
+ this.selectedBranch = undefined;
112
+ }
113
+
114
+ return branchStatus;
115
+ }
116
+
117
+ protected onHalt(): void {
118
+ this.log("Halting - resetting condition check flag");
119
+ this.conditionEvaluated = false;
120
+ this.selectedBranch = undefined;
121
+ super.onHalt();
122
+ }
123
+
124
+ protected onReset(): void {
125
+ this.log("Resetting - clearing condition check flag");
126
+ this.conditionEvaluated = false;
127
+ this.selectedBranch = undefined;
128
+ }
129
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ForEach composite configuration schema
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { createNodeSchema, validations } from "../schemas/base.schema.js";
7
+
8
+ /**
9
+ * Schema for ForEach composite configuration
10
+ * Validates collection and item keys
11
+ */
12
+ export const forEachConfigurationSchema = createNodeSchema("ForEach", {
13
+ collectionKey: validations.blackboardKey,
14
+ itemKey: validations.blackboardKey,
15
+ indexKey: validations.blackboardKey.optional(),
16
+ });
17
+
18
+ /**
19
+ * Validated ForEach configuration type
20
+ */
21
+ export type ValidatedForEachConfiguration = z.infer<
22
+ typeof forEachConfigurationSchema
23
+ >;