@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,351 @@
1
+ /**
2
+ * Tests for Precondition decorator
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it } from "vitest";
6
+ import { ScopedBlackboard } from "../blackboard.js";
7
+ import { ConfigurationError } from "../errors.js";
8
+ import { FailureNode, SuccessNode } from "../test-nodes.js";
9
+ import { type TemporalContext, NodeStatus } from "../types.js";
10
+ import { Precondition } from "./precondition.js";
11
+
12
+ describe("Precondition", () => {
13
+ let blackboard: ScopedBlackboard;
14
+ let context: TemporalContext;
15
+
16
+ beforeEach(() => {
17
+ blackboard = new ScopedBlackboard("root");
18
+ context = {
19
+ blackboard,
20
+ timestamp: Date.now(),
21
+ deltaTime: 0,
22
+ };
23
+ });
24
+
25
+ it("should execute child when precondition succeeds", async () => {
26
+ const precond = new Precondition({ id: "precond1" });
27
+
28
+ let childExecuted = false;
29
+ class TrackedChild extends SuccessNode {
30
+ tick(context: TemporalContext) {
31
+ childExecuted = true;
32
+ return super.tick(context);
33
+ }
34
+ }
35
+
36
+ precond.setChild(new TrackedChild({ id: "child" }));
37
+ precond.addPrecondition(new SuccessNode({ id: "condition" }));
38
+
39
+ const result = await precond.tick(context);
40
+ expect(result).toBe(NodeStatus.SUCCESS);
41
+ expect(childExecuted).toBe(true);
42
+ });
43
+
44
+ it("should fail if required precondition not met", async () => {
45
+ const precond = new Precondition({ id: "precond1" });
46
+
47
+ precond.setChild(new SuccessNode({ id: "child" }));
48
+ precond.addPrecondition(
49
+ new FailureNode({ id: "condition" }),
50
+ undefined,
51
+ true,
52
+ );
53
+
54
+ const result = await precond.tick(context);
55
+ expect(result).toBe(NodeStatus.FAILURE);
56
+ });
57
+
58
+ it("should run resolver on precondition failure", async () => {
59
+ const precond = new Precondition({ id: "precond1" });
60
+
61
+ let resolverExecuted = false;
62
+ class TrackedResolver extends SuccessNode {
63
+ tick(context: TemporalContext) {
64
+ const superTick = super.tick.bind(this);
65
+ return (async () => {
66
+ resolverExecuted = true;
67
+ // Fix the condition
68
+ context.blackboard.set("conditionMet", true);
69
+ return await superTick(context);
70
+ })();
71
+ }
72
+ }
73
+
74
+ // Condition checks blackboard
75
+ class BlackboardCondition extends SuccessNode {
76
+ tick(context: TemporalContext) {
77
+ const self = this;
78
+ return (async () => {
79
+ const met = context.blackboard.get("conditionMet");
80
+ self._status = met ? NodeStatus.SUCCESS : NodeStatus.FAILURE;
81
+ return self._status;
82
+ })();
83
+ }
84
+ }
85
+
86
+ precond.setChild(new SuccessNode({ id: "child" }));
87
+ precond.addPrecondition(
88
+ new BlackboardCondition({ id: "condition" }),
89
+ new TrackedResolver({ id: "resolver" }),
90
+ true,
91
+ );
92
+
93
+ const result = await precond.tick(context);
94
+ expect(resolverExecuted).toBe(true);
95
+ expect(result).toBe(NodeStatus.SUCCESS);
96
+ });
97
+
98
+ it("should skip optional preconditions", async () => {
99
+ const precond = new Precondition({ id: "precond1" });
100
+
101
+ let childExecuted = false;
102
+ class TrackedChild extends SuccessNode {
103
+ tick(context: TemporalContext) {
104
+ childExecuted = true;
105
+ return super.tick(context);
106
+ }
107
+ }
108
+
109
+ precond.setChild(new TrackedChild({ id: "child" }));
110
+ precond.addPrecondition(
111
+ new FailureNode({ id: "condition" }),
112
+ undefined,
113
+ false,
114
+ ); // Optional
115
+
116
+ const result = await precond.tick(context);
117
+ expect(result).toBe(NodeStatus.SUCCESS);
118
+ expect(childExecuted).toBe(true);
119
+ });
120
+
121
+ it("should check multiple preconditions", async () => {
122
+ const precond = new Precondition({ id: "precond1" });
123
+
124
+ precond.setChild(new SuccessNode({ id: "child" }));
125
+ precond.addPrecondition(new SuccessNode({ id: "cond1" }));
126
+ precond.addPrecondition(new SuccessNode({ id: "cond2" }));
127
+ precond.addPrecondition(new SuccessNode({ id: "cond3" }));
128
+
129
+ const result = await precond.tick(context);
130
+ expect(result).toBe(NodeStatus.SUCCESS);
131
+ });
132
+
133
+ it("should fail on first failed required precondition", async () => {
134
+ const precond = new Precondition({ id: "precond1" });
135
+
136
+ let cond3Checked = false;
137
+ class Cond3 extends SuccessNode {
138
+ tick(context: TemporalContext) {
139
+ const superTick = super.tick.bind(this);
140
+ return (async () => {
141
+ cond3Checked = true;
142
+ return await superTick(context);
143
+ })();
144
+ }
145
+ }
146
+
147
+ precond.setChild(new SuccessNode({ id: "child" }));
148
+ precond.addPrecondition(new SuccessNode({ id: "cond1" }));
149
+ precond.addPrecondition(new FailureNode({ id: "cond2" })); // Fails here
150
+ precond.addPrecondition(new Cond3({ id: "cond3" }));
151
+
152
+ const result = await precond.tick(context);
153
+ expect(result).toBe(NodeStatus.FAILURE);
154
+ expect(cond3Checked).toBe(false); // Should not reach cond3
155
+ });
156
+
157
+ it("should propagate RUNNING from precondition", async () => {
158
+ const precond = new Precondition({ id: "precond1" });
159
+
160
+ let tickCount = 0;
161
+ class RunningCondition extends SuccessNode {
162
+ tick(_context: TemporalContext) {
163
+ const self = this;
164
+ return (async () => {
165
+ tickCount++;
166
+ self._status =
167
+ tickCount < 2 ? NodeStatus.RUNNING : NodeStatus.SUCCESS;
168
+ return self._status;
169
+ })();
170
+ }
171
+ }
172
+
173
+ precond.setChild(new SuccessNode({ id: "child" }));
174
+ precond.addPrecondition(new RunningCondition({ id: "condition" }));
175
+
176
+ let result = await precond.tick(context);
177
+ expect(result).toBe(NodeStatus.RUNNING);
178
+
179
+ result = await precond.tick(context);
180
+ expect(result).toBe(NodeStatus.SUCCESS);
181
+ });
182
+
183
+ it("should propagate ConfigurationError if no child", async () => {
184
+ const precond = new Precondition({ id: "precond1" });
185
+ precond.addPrecondition(new SuccessNode({ id: "condition" }));
186
+
187
+ try {
188
+ await precond.tick(context);
189
+ throw new Error("Expected tick to throw");
190
+ } catch (error) {
191
+ expect(error).toBeInstanceOf(ConfigurationError);
192
+ expect((error as ConfigurationError).message).toContain(
193
+ "Precondition requires a child",
194
+ );
195
+ }
196
+ });
197
+
198
+ describe("Multi-tick child execution - FIXED BEHAVIOR", () => {
199
+ it(
200
+ "should check precondition ONCE and not re-check while child is RUNNING",
201
+ async () => {
202
+ let conditionTickCount = 0;
203
+ let childTickCount = 0;
204
+
205
+ // Condition that counts how many times it's checked
206
+ class CountingCondition extends SuccessNode {
207
+ tick(_context: TemporalContext) {
208
+ const self = this;
209
+ return (async () => {
210
+ conditionTickCount++;
211
+ console.log(`[CountingCondition] Tick #${conditionTickCount}`);
212
+ self._status = NodeStatus.SUCCESS;
213
+ return NodeStatus.SUCCESS;
214
+ })();
215
+ }
216
+ }
217
+
218
+ // Child that returns RUNNING for first 2 ticks, then SUCCESS
219
+ class MultiTickChild extends SuccessNode {
220
+ tick(context: TemporalContext) {
221
+ const self = this;
222
+ const superTick = super.tick.bind(this);
223
+ return (async () => {
224
+ childTickCount++;
225
+ console.log(`[MultiTickChild] Tick #${childTickCount}`);
226
+
227
+ if (childTickCount < 3) {
228
+ self._status = NodeStatus.RUNNING;
229
+ return NodeStatus.RUNNING;
230
+ }
231
+
232
+ return await superTick(context); // SUCCESS on tick 3
233
+ })();
234
+ }
235
+ }
236
+
237
+ const condition = new CountingCondition({ id: "counting-condition" });
238
+ const child = new MultiTickChild({ id: "multi-tick-child" });
239
+ const precondition = new Precondition({
240
+ id: "test-precondition",
241
+ name: "test-precondition",
242
+ });
243
+
244
+ precondition.setChild(child);
245
+ precondition.addPrecondition(condition);
246
+
247
+ // Tick 1: Should check condition, then execute child (returns RUNNING)
248
+ const status1 = await precondition.tick(context);
249
+ expect(status1).toBe(NodeStatus.RUNNING);
250
+ expect(conditionTickCount).toBe(1); // Condition checked once
251
+ expect(childTickCount).toBe(1); // Child executed once
252
+
253
+ // Tick 2: ✅ FIXED: Should NOT re-check condition, just execute child (returns RUNNING)
254
+ const status2 = await precondition.tick(context);
255
+ expect(status2).toBe(NodeStatus.RUNNING);
256
+ expect(conditionTickCount).toBe(1); // ✅ Still 1! Not re-checked
257
+ expect(childTickCount).toBe(2); // Child executed again
258
+
259
+ // Tick 3: ✅ FIXED: Should NOT re-check condition, just execute child (returns SUCCESS)
260
+ const status3 = await precondition.tick(context);
261
+ expect(status3).toBe(NodeStatus.SUCCESS);
262
+ expect(conditionTickCount).toBe(1); // ✅ Still 1! Not re-checked
263
+ expect(childTickCount).toBe(3); // Child completes
264
+
265
+ // Summary: Condition was checked only 1 time (on first tick)
266
+ // This confirms the precondition is NOT re-evaluated on subsequent ticks
267
+ console.log(
268
+ "\n✅ FIXED: Precondition checked only 1 time, not re-checked during child execution",
269
+ );
270
+ },
271
+ );
272
+
273
+ it(
274
+ "should NOT be affected if precondition changes while child is RUNNING (safe behavior)",
275
+ async () => {
276
+ let childTickCount = 0;
277
+ let conditionShouldSucceed = true;
278
+
279
+ // Condition that succeeds initially, then fails on subsequent ticks
280
+ class ChangeableCondition extends SuccessNode {
281
+ tick(_context: TemporalContext) {
282
+ const self = this;
283
+ return (async () => {
284
+ const result = conditionShouldSucceed
285
+ ? NodeStatus.SUCCESS
286
+ : NodeStatus.FAILURE;
287
+ console.log(`[ChangeableCondition] Returning: ${result}`);
288
+ self._status = result;
289
+ return result;
290
+ })();
291
+ }
292
+ }
293
+
294
+ // Child that takes 3 ticks to complete
295
+ class SlowChild extends SuccessNode {
296
+ tick(context: TemporalContext) {
297
+ const self = this;
298
+ const superTick = super.tick.bind(this);
299
+ return (async () => {
300
+ childTickCount++;
301
+ console.log(`[SlowChild] Tick #${childTickCount}`);
302
+
303
+ if (childTickCount < 3) {
304
+ self._status = NodeStatus.RUNNING;
305
+ return NodeStatus.RUNNING;
306
+ }
307
+
308
+ return await superTick(context); // SUCCESS on tick 3
309
+ })();
310
+ }
311
+ }
312
+
313
+ const condition = new ChangeableCondition({
314
+ id: "changeable-condition",
315
+ });
316
+ const child = new SlowChild({ id: "slow-child" });
317
+ const precondition = new Precondition({
318
+ id: "test-precondition",
319
+ name: "test-precondition",
320
+ });
321
+
322
+ precondition.setChild(child);
323
+ precondition.addPrecondition(condition);
324
+
325
+ // Tick 1: Precondition passes, child returns RUNNING
326
+ const status1 = await precondition.tick(context);
327
+ expect(status1).toBe(NodeStatus.RUNNING);
328
+ expect(childTickCount).toBe(1);
329
+
330
+ // Change condition to fail
331
+ conditionShouldSucceed = false;
332
+
333
+ // Tick 2: ✅ FIXED: Precondition is NOT re-checked, child continues
334
+ const status2 = await precondition.tick(context);
335
+ expect(status2).toBe(NodeStatus.RUNNING); // ✅ Still RUNNING!
336
+ expect(childTickCount).toBe(2); // ✅ Child continues executing
337
+
338
+ // Tick 3: ✅ FIXED: Child completes successfully despite precondition now failing
339
+ const status3 = await precondition.tick(context);
340
+ expect(status3).toBe(NodeStatus.SUCCESS); // ✅ Child completes!
341
+ expect(childTickCount).toBe(3); // Child executed all 3 ticks
342
+
343
+ // This demonstrates the fix: child execution is NOT interrupted
344
+ // even though the precondition would now fail if re-checked
345
+ console.log(
346
+ "\n✅ SAFE: Child execution continues uninterrupted despite precondition change",
347
+ );
348
+ },
349
+ );
350
+ });
351
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Precondition decorator - Check/resolve preconditions before executing child
3
+ */
4
+
5
+ import { DecoratorNode } 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
+ export interface PreconditionEntry {
11
+ condition: TreeNode;
12
+ resolver?: TreeNode;
13
+ required: boolean;
14
+ }
15
+
16
+ /**
17
+ * Precondition checks preconditions before executing the main child.
18
+ * If preconditions fail, attempts to resolve them using resolvers.
19
+ * Useful for ensuring prerequisites are met before executing actions.
20
+ */
21
+ export class Precondition extends DecoratorNode {
22
+ private preconditions: PreconditionEntry[] = [];
23
+ private preconditionsChecked: boolean = false;
24
+
25
+ /**
26
+ * Add a precondition to check before main execution
27
+ */
28
+ addPrecondition(
29
+ condition: TreeNode,
30
+ resolver?: TreeNode,
31
+ required: boolean = true,
32
+ ): void {
33
+ this.preconditions.push({ condition, resolver, required });
34
+ }
35
+
36
+ async executeTick(context: TemporalContext): Promise<NodeStatus> {
37
+ checkSignal(context.signal);
38
+
39
+ if (!this.child) {
40
+ throw new ConfigurationError("Precondition requires a child");
41
+ }
42
+
43
+ // Only check preconditions if not already verified
44
+ if (!this.preconditionsChecked) {
45
+ // Check all preconditions
46
+ for (let i = 0; i < this.preconditions.length; i++) {
47
+ checkSignal(context.signal);
48
+ const precond = this.preconditions[i];
49
+ if (!precond) {
50
+ continue;
51
+ }
52
+
53
+ this.log(
54
+ `Checking precondition ${i + 1}/${this.preconditions.length}`,
55
+ );
56
+ const conditionResult = await precond.condition.tick(context);
57
+
58
+ if (conditionResult === NodeStatus.RUNNING) {
59
+ this.log(`Precondition ${i + 1} is running`);
60
+ this._status = NodeStatus.RUNNING;
61
+ return NodeStatus.RUNNING;
62
+ }
63
+
64
+ if (conditionResult === NodeStatus.FAILURE) {
65
+ this.log(`Precondition ${i + 1} failed`);
66
+
67
+ // Try resolver if available
68
+ if (precond.resolver) {
69
+ this.log(`Attempting to resolve precondition ${i + 1}`);
70
+ const resolverResult = await precond.resolver.tick(context);
71
+
72
+ if (resolverResult === NodeStatus.RUNNING) {
73
+ this.log(`Resolver ${i + 1} is running`);
74
+ this._status = NodeStatus.RUNNING;
75
+ return NodeStatus.RUNNING;
76
+ }
77
+
78
+ if (resolverResult === NodeStatus.SUCCESS) {
79
+ this.log(`Precondition ${i + 1} resolved successfully`);
80
+ // Re-check condition after resolution
81
+ const recheckResult = await precond.condition.tick(context);
82
+ if (recheckResult !== NodeStatus.SUCCESS) {
83
+ if (precond.required) {
84
+ this.log(
85
+ `Precondition ${i + 1} still not met after resolution`,
86
+ );
87
+ this._status = NodeStatus.FAILURE;
88
+ return NodeStatus.FAILURE;
89
+ } else {
90
+ this.log(`Optional precondition ${i + 1} skipped`);
91
+ }
92
+ }
93
+ } else if (precond.required) {
94
+ this.log(`Failed to resolve required precondition ${i + 1}`);
95
+ this._status = NodeStatus.FAILURE;
96
+ return NodeStatus.FAILURE;
97
+ }
98
+ } else if (precond.required) {
99
+ this.log(`Required precondition ${i + 1} not met (no resolver)`);
100
+ this._status = NodeStatus.FAILURE;
101
+ return NodeStatus.FAILURE;
102
+ } else {
103
+ this.log(`Optional precondition ${i + 1} skipped`);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Mark preconditions as checked once all pass
109
+ this.preconditionsChecked = true;
110
+ this.log("All preconditions met - executing main child");
111
+ } else {
112
+ this.log("Preconditions already verified - continuing child execution");
113
+ }
114
+
115
+ // Execute child
116
+ checkSignal(context.signal);
117
+ const result = await this.child.tick(context);
118
+ this._status = result;
119
+
120
+ // Reset flag when child completes
121
+ if (result !== NodeStatus.RUNNING) {
122
+ this.log("Child completed - resetting precondition check flag");
123
+ this.preconditionsChecked = false;
124
+ }
125
+
126
+ return result;
127
+ }
128
+
129
+ protected onHalt(): void {
130
+ this.log("Halting - resetting precondition check flag");
131
+ this.preconditionsChecked = false;
132
+ super.onHalt();
133
+ }
134
+
135
+ protected onReset(): void {
136
+ this.log("Resetting - clearing precondition check flag");
137
+ this.preconditionsChecked = false;
138
+ }
139
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Repeat decorator configuration schema
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { createNodeSchema, validations } from "../schemas/base.schema.js";
7
+
8
+ /**
9
+ * Schema for Repeat decorator configuration
10
+ * Validates that numCycles is a positive integer
11
+ */
12
+ export const repeatConfigurationSchema = createNodeSchema("Repeat", {
13
+ numCycles: validations.positiveInteger("numCycles"),
14
+ });
15
+
16
+ /**
17
+ * Validated Repeat configuration type
18
+ */
19
+ export type ValidatedRepeatConfiguration = z.infer<
20
+ typeof repeatConfigurationSchema
21
+ >;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Tests for Repeat decorator
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it } from "vitest";
6
+ import { ScopedBlackboard } from "../blackboard.js";
7
+ import { ConfigurationError } from "../errors.js";
8
+ import { SuccessNode } from "../test-nodes.js";
9
+ import { type TemporalContext, NodeStatus } from "../types.js";
10
+ import { Repeat } from "./repeat.js";
11
+
12
+ describe("Repeat", () => {
13
+ let blackboard: ScopedBlackboard;
14
+ let context: TemporalContext;
15
+
16
+ beforeEach(() => {
17
+ blackboard = new ScopedBlackboard("root");
18
+ context = {
19
+ blackboard,
20
+ timestamp: Date.now(),
21
+ deltaTime: 0,
22
+ };
23
+ });
24
+
25
+ it("should execute child exactly N times", async () => {
26
+ const repeat = new Repeat({ id: "repeat1", numCycles: 3 });
27
+
28
+ let tickCount = 0;
29
+ class CountingNode extends SuccessNode {
30
+ tick(context: TemporalContext) {
31
+ tickCount++;
32
+ return super.tick(context);
33
+ }
34
+ }
35
+
36
+ repeat.setChild(new CountingNode({ id: "child" }));
37
+
38
+ // First 2 ticks return RUNNING
39
+ let result = await repeat.tick(context);
40
+ expect(result).toBe(NodeStatus.RUNNING);
41
+ expect(tickCount).toBe(1);
42
+
43
+ result = await repeat.tick(context);
44
+ expect(result).toBe(NodeStatus.RUNNING);
45
+ expect(tickCount).toBe(2);
46
+
47
+ // Third tick returns SUCCESS
48
+ result = await repeat.tick(context);
49
+ expect(result).toBe(NodeStatus.SUCCESS);
50
+ expect(tickCount).toBe(3);
51
+ });
52
+
53
+ it("should fail on child failure", async () => {
54
+ const repeat = new Repeat({ id: "repeat1", numCycles: 5 });
55
+
56
+ let tickCount = 0;
57
+ class FailOnThird extends SuccessNode {
58
+ tick(context: TemporalContext) {
59
+ const self = this;
60
+ const superTick = super.tick.bind(this);
61
+ return (async () => {
62
+ tickCount++;
63
+ if (tickCount === 3) {
64
+ self._status = NodeStatus.FAILURE;
65
+ return NodeStatus.FAILURE;
66
+ }
67
+ return await superTick(context);
68
+ })();
69
+ }
70
+ }
71
+
72
+ repeat.setChild(new FailOnThird({ id: "child" }));
73
+
74
+ await repeat.tick(context); // Cycle 1
75
+ await repeat.tick(context); // Cycle 2
76
+ const result = await repeat.tick(context); // Cycle 3 fails
77
+
78
+ expect(result).toBe(NodeStatus.FAILURE);
79
+ expect(tickCount).toBe(3);
80
+ });
81
+
82
+ it("should reset child between cycles", async () => {
83
+ const repeat = new Repeat({ id: "repeat1", numCycles: 3 });
84
+
85
+ let resetCount = 0;
86
+ class ResetTracker extends SuccessNode {
87
+ reset(): void {
88
+ resetCount++;
89
+ super.reset();
90
+ }
91
+ }
92
+
93
+ repeat.setChild(new ResetTracker({ id: "child" }));
94
+
95
+ await repeat.tick(context); // Cycle 1, reset after
96
+ await repeat.tick(context); // Cycle 2, reset after
97
+ await repeat.tick(context); // Cycle 3, completes
98
+
99
+ expect(resetCount).toBe(2); // Reset after cycles 1 and 2
100
+ });
101
+
102
+ it("should handle RUNNING state", async () => {
103
+ const repeat = new Repeat({ id: "repeat1", numCycles: 2 });
104
+
105
+ let tickCount = 0;
106
+ class TwoTickNode extends SuccessNode {
107
+ tick(context: TemporalContext) {
108
+ const self = this;
109
+ const superTick = super.tick.bind(this);
110
+ return (async () => {
111
+ tickCount++;
112
+ if (tickCount % 2 === 1) {
113
+ // Odd ticks return RUNNING
114
+ self._status = NodeStatus.RUNNING;
115
+ return NodeStatus.RUNNING;
116
+ }
117
+ return await superTick(context);
118
+ })();
119
+ }
120
+ }
121
+
122
+ repeat.setChild(new TwoTickNode({ id: "child" }));
123
+
124
+ // Cycle 1: tick 1 (RUNNING), tick 2 (SUCCESS)
125
+ let result = await repeat.tick(context);
126
+ expect(result).toBe(NodeStatus.RUNNING);
127
+ result = await repeat.tick(context);
128
+ expect(result).toBe(NodeStatus.RUNNING); // Still more cycles
129
+
130
+ // Cycle 2: tick 3 (RUNNING), tick 4 (SUCCESS)
131
+ result = await repeat.tick(context);
132
+ expect(result).toBe(NodeStatus.RUNNING);
133
+ result = await repeat.tick(context);
134
+ expect(result).toBe(NodeStatus.SUCCESS);
135
+
136
+ expect(tickCount).toBe(4);
137
+ });
138
+
139
+ it("should reset cycle count on completion", async () => {
140
+ const repeat = new Repeat({ id: "repeat1", numCycles: 2 });
141
+
142
+ let firstRunTicks = 0;
143
+ let secondRunTicks = 0;
144
+ let inSecondRun = false;
145
+
146
+ class CountingNode extends SuccessNode {
147
+ tick(context: TemporalContext) {
148
+ const superTick = super.tick.bind(this);
149
+ return (async () => {
150
+ if (inSecondRun) {
151
+ secondRunTicks++;
152
+ } else {
153
+ firstRunTicks++;
154
+ }
155
+ return await superTick(context);
156
+ })();
157
+ }
158
+ }
159
+
160
+ repeat.setChild(new CountingNode({ id: "child" }));
161
+
162
+ // First run
163
+ await repeat.tick(context);
164
+ await repeat.tick(context);
165
+ expect(firstRunTicks).toBe(2);
166
+
167
+ // Second run
168
+ inSecondRun = true;
169
+ await repeat.tick(context);
170
+ await repeat.tick(context);
171
+ expect(secondRunTicks).toBe(2);
172
+ });
173
+
174
+ it("should propagate ConfigurationError if no child", async () => {
175
+ const repeat = new Repeat({ id: "repeat1", numCycles: 1 });
176
+
177
+ try {
178
+ await repeat.tick(context);
179
+ throw new Error("Expected tick to throw");
180
+ } catch (error) {
181
+ expect(error).toBeInstanceOf(ConfigurationError);
182
+ expect((error as ConfigurationError).message).toContain(
183
+ "Repeat requires a child",
184
+ );
185
+ }
186
+ });
187
+ });