@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,893 @@
1
+ /**
2
+ * Tests for SubTree node
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it } from "vitest";
6
+ import { BehaviorTree } from "../behavior-tree.js";
7
+ import { ScopedBlackboard } from "../blackboard.js";
8
+ import { Registry } from "../registry.js";
9
+ import { FailureNode, RunningNode, SuccessNode } from "../test-nodes.js";
10
+ import type { TreeNode } from "../types.js";
11
+ import { type TemporalContext, NodeStatus } from "../types.js";
12
+ import { Sequence } from "./sequence.js";
13
+ import { SubTree } from "./sub-tree.js";
14
+
15
+ describe("SubTree", () => {
16
+ let blackboard: ScopedBlackboard;
17
+ let treeRegistry: Registry;
18
+ let context: TemporalContext;
19
+
20
+ // Helper to register a tree with BehaviorTree wrapper (uses test-scoped registry)
21
+ const registerTree = (id: string, rootNode: TreeNode): void => {
22
+ const tree = new BehaviorTree(rootNode);
23
+ treeRegistry.registerTree(id, tree, "test-source");
24
+ };
25
+
26
+ beforeEach(() => {
27
+ blackboard = new ScopedBlackboard("root");
28
+ treeRegistry = new Registry();
29
+ context = {
30
+ blackboard,
31
+ treeRegistry,
32
+ timestamp: Date.now(),
33
+ deltaTime: 0,
34
+ };
35
+ });
36
+
37
+ describe("Basic Functionality", () => {
38
+ it("should reference and execute a registered behavior tree", async () => {
39
+ // Register a simple tree
40
+ const reusableTree = new Sequence({
41
+ id: "reusable",
42
+ name: "Reusable Steps",
43
+ });
44
+ reusableTree.addChildren([
45
+ new SuccessNode({ id: "child1" }),
46
+ new SuccessNode({ id: "child2" }),
47
+ ]);
48
+ registerTree("login-steps", reusableTree);
49
+
50
+ // Create SubTree that references the tree
51
+ const subTree = new SubTree({
52
+ id: "sg1",
53
+ name: "Login",
54
+ treeId: "login-steps",
55
+ });
56
+
57
+ const result = await subTree.tick(context);
58
+ expect(result).toBe(NodeStatus.SUCCESS);
59
+ });
60
+
61
+ it("should fail when tree completes with failure", async () => {
62
+ // Register a tree that fails
63
+ const failingTree = new Sequence({
64
+ id: "failing",
65
+ name: "Failing Steps",
66
+ });
67
+ failingTree.addChildren([
68
+ new SuccessNode({ id: "child1" }),
69
+ new FailureNode({ id: "child2" }),
70
+ ]);
71
+ registerTree("failing-steps", failingTree);
72
+
73
+ const subTree = new SubTree({
74
+ id: "sg1",
75
+ name: "Failing Group",
76
+ treeId: "failing-steps",
77
+ });
78
+
79
+ const result = await subTree.tick(context);
80
+ expect(result).toBe(NodeStatus.FAILURE);
81
+ });
82
+
83
+ it("should return running when tree is running", async () => {
84
+ // Register a tree that stays running
85
+ const runningTree = new Sequence({
86
+ id: "running",
87
+ name: "Running Steps",
88
+ });
89
+ runningTree.addChildren([
90
+ new SuccessNode({ id: "child1" }),
91
+ new RunningNode({ id: "child2" }),
92
+ ]);
93
+ registerTree("running-steps", runningTree);
94
+
95
+ const subTree = new SubTree({
96
+ id: "sg1",
97
+ name: "Running Group",
98
+ treeId: "running-steps",
99
+ });
100
+
101
+ const result = await subTree.tick(context);
102
+ expect(result).toBe(NodeStatus.RUNNING);
103
+ });
104
+
105
+ it("should throw error when tree ID is not found", async () => {
106
+ const subTree = new SubTree({
107
+ id: "sg1",
108
+ name: "Invalid Group",
109
+ treeId: "nonexistent-tree",
110
+ });
111
+
112
+ const status = await subTree.tick(context);
113
+ expect(status).toBe(NodeStatus.FAILURE);
114
+ });
115
+
116
+ it("should include available trees in error message", async () => {
117
+ // Register some trees
118
+ registerTree("tree1", new SuccessNode({ id: "t1" }));
119
+ registerTree("tree2", new SuccessNode({ id: "t2" }));
120
+
121
+ const subTree = new SubTree({
122
+ id: "sg1",
123
+ name: "Invalid Group",
124
+ treeId: "nonexistent-tree",
125
+ });
126
+
127
+ const status = await subTree.tick(context);
128
+ expect(status).toBe(NodeStatus.FAILURE);
129
+ });
130
+ });
131
+
132
+ describe("Scoped Blackboard", () => {
133
+ it("should create scoped blackboard for subTree", async () => {
134
+ // Create a custom node that checks its blackboard scope
135
+ let capturedScopePath: string = "";
136
+ class CheckScopeNode extends SuccessNode {
137
+ async tick(context: TemporalContext) {
138
+ capturedScopePath = context.blackboard.getFullScopePath();
139
+ return await super.tick(context);
140
+ }
141
+ }
142
+
143
+ const tree = new Sequence({ id: "scoped", name: "Scoped Tree" });
144
+ tree.addChild(new CheckScopeNode({ id: "child1" }));
145
+ registerTree("scoped-steps", tree);
146
+
147
+ const subTree = new SubTree({
148
+ id: "sg1",
149
+ name: "Scoped Group",
150
+ treeId: "scoped-steps",
151
+ });
152
+
153
+ await subTree.tick(context);
154
+
155
+ // Should have created a scope
156
+ expect(capturedScopePath).toContain("subtree_sg1");
157
+ });
158
+
159
+ it("should isolate variables between subTrees", async () => {
160
+ // First tree sets a value
161
+ class SetValueNode extends SuccessNode {
162
+ async tick(context: TemporalContext) {
163
+ context.blackboard.set("localValue", "from-sg1");
164
+ return await super.tick(context);
165
+ }
166
+ }
167
+
168
+ // Second tree tries to read the value
169
+ let capturedValue: unknown = "not-set";
170
+ class ReadValueNode extends SuccessNode {
171
+ async tick(context: TemporalContext) {
172
+ capturedValue = context.blackboard.get("localValue");
173
+ return await super.tick(context);
174
+ }
175
+ }
176
+
177
+ const tree1 = new Sequence({ id: "tree1", name: "Tree 1" });
178
+ tree1.addChild(new SetValueNode({ id: "child1" }));
179
+ registerTree("steps1", tree1);
180
+
181
+ const tree2 = new Sequence({ id: "tree2", name: "Tree 2" });
182
+ tree2.addChild(new ReadValueNode({ id: "child2" }));
183
+ registerTree("steps2", tree2);
184
+
185
+ const sg1 = new SubTree({
186
+ id: "sg1",
187
+ name: "Group 1",
188
+ treeId: "steps1",
189
+ });
190
+ const sg2 = new SubTree({
191
+ id: "sg2",
192
+ name: "Group 2",
193
+ treeId: "steps2",
194
+ });
195
+
196
+ // Execute sg1 - sets localValue in its scope
197
+ await sg1.tick(context);
198
+
199
+ // Execute sg2 - should NOT see sg1's value
200
+ await sg2.tick(context);
201
+
202
+ // sg2 should not have access to sg1's scoped value
203
+ expect(capturedValue).toBeUndefined();
204
+ });
205
+
206
+ it("should inherit parent blackboard values", async () => {
207
+ // Create a node that reads from parent scope
208
+ let parentValue: unknown = "not-set";
209
+ class ReadParentNode extends SuccessNode {
210
+ async tick(context: TemporalContext) {
211
+ parentValue = context.blackboard.get("inheritedValue");
212
+ return await super.tick(context);
213
+ }
214
+ }
215
+
216
+ const tree = new Sequence({ id: "tree", name: "Tree" });
217
+ tree.addChild(new ReadParentNode({ id: "child1" }));
218
+ registerTree("read-parent", tree);
219
+
220
+ // Set value in parent blackboard
221
+ blackboard.set("inheritedValue", "from-parent");
222
+
223
+ const subTree = new SubTree({
224
+ id: "sg1",
225
+ name: "Reading Group",
226
+ treeId: "read-parent",
227
+ });
228
+
229
+ await subTree.tick(context);
230
+
231
+ // Should be able to read parent value
232
+ expect(parentValue).toBe("from-parent");
233
+ });
234
+
235
+ it("should not leak subTree-scoped values to parent", async () => {
236
+ // Create a node that sets a value in its context
237
+ class SetValueNode extends SuccessNode {
238
+ async tick(context: TemporalContext) {
239
+ context.blackboard.set("groupLocalValue", "group-value");
240
+ return await super.tick(context);
241
+ }
242
+ }
243
+
244
+ const tree = new Sequence({ id: "tree", name: "Tree" });
245
+ tree.addChild(new SetValueNode({ id: "child1" }));
246
+ registerTree("set-local", tree);
247
+
248
+ const subTree = new SubTree({
249
+ id: "sg1",
250
+ name: "Setting Group",
251
+ treeId: "set-local",
252
+ });
253
+
254
+ await subTree.tick(context);
255
+
256
+ // Group-local value should NOT exist in parent blackboard
257
+ expect(blackboard.has("groupLocalValue")).toBe(false);
258
+ });
259
+ });
260
+
261
+ describe("Lazy Tree Cloning", () => {
262
+ it("should clone tree only on first tick", async () => {
263
+ const tree = new SuccessNode({ id: "tree" });
264
+ registerTree("lazy-tree", tree);
265
+
266
+ const subTree = new SubTree({
267
+ id: "sg1",
268
+ name: "Lazy Group",
269
+ treeId: "lazy-tree",
270
+ });
271
+
272
+ // First tick should clone the tree
273
+ await subTree.tick(context);
274
+ expect(subTree.clonedTree).toBeDefined();
275
+
276
+ // Store reference to cloned tree
277
+ const clonedTree = subTree.clonedTree;
278
+
279
+ // Second tick should reuse the same cloned tree
280
+ await subTree.tick(context);
281
+ expect(subTree.clonedTree).toBe(clonedTree);
282
+ });
283
+
284
+ it("should clone separate instances for different subTrees", async () => {
285
+ const tree = new SuccessNode({ id: "tree" });
286
+ registerTree("shared-tree", tree);
287
+
288
+ const sg1 = new SubTree({
289
+ id: "sg1",
290
+ name: "Group 1",
291
+ treeId: "shared-tree",
292
+ });
293
+ const sg2 = new SubTree({
294
+ id: "sg2",
295
+ name: "Group 2",
296
+ treeId: "shared-tree",
297
+ });
298
+
299
+ await sg1.tick(context);
300
+ await sg2.tick(context);
301
+
302
+ // Each should have its own cloned instance
303
+ expect(sg1.clonedTree).toBeDefined();
304
+ expect(sg2.clonedTree).toBeDefined();
305
+ expect(sg1.clonedTree).not.toBe(sg2.clonedTree);
306
+ });
307
+ });
308
+
309
+ describe("Reset and Halt", () => {
310
+ it("should reset the referenced tree", async () => {
311
+ const tree = new RunningNode({ id: "tree" });
312
+ registerTree("reset-tree", tree);
313
+
314
+ const subTree = new SubTree({
315
+ id: "sg1",
316
+ name: "Reset Group",
317
+ treeId: "reset-tree",
318
+ });
319
+
320
+ await subTree.tick(context);
321
+ expect(subTree.status()).toBe(NodeStatus.RUNNING);
322
+
323
+ subTree.reset();
324
+ expect(subTree.status()).toBe(NodeStatus.IDLE);
325
+ expect(subTree.clonedTree?.status()).toBe(NodeStatus.IDLE);
326
+ });
327
+
328
+ it("should halt the referenced tree", async () => {
329
+ const tree = new RunningNode({ id: "tree" });
330
+ registerTree("halt-tree", tree);
331
+
332
+ const subTree = new SubTree({
333
+ id: "sg1",
334
+ name: "Halt Group",
335
+ treeId: "halt-tree",
336
+ });
337
+
338
+ await subTree.tick(context);
339
+ expect(subTree.status()).toBe(NodeStatus.RUNNING);
340
+ expect(subTree.clonedTree?.status()).toBe(NodeStatus.RUNNING);
341
+
342
+ subTree.halt();
343
+ expect(subTree.status()).toBe(NodeStatus.IDLE);
344
+ expect(subTree.clonedTree?.status()).toBe(NodeStatus.IDLE);
345
+ });
346
+ });
347
+
348
+ describe("Clone", () => {
349
+ it("should clone the subTree without cloning the cached tree", () => {
350
+ const subTree = new SubTree({
351
+ id: "sg1",
352
+ name: "Original SubTree",
353
+ treeId: "some-tree",
354
+ });
355
+
356
+ const cloned = subTree.clone() as SubTree;
357
+
358
+ expect(cloned.id).toBe("sg1");
359
+ expect(cloned.name).toBe("Original SubTree");
360
+ expect(cloned.treeId).toBe("some-tree");
361
+ expect(cloned.clonedTree).toBeUndefined();
362
+ });
363
+
364
+ it("should allow cloned subTree to lazy-load its own tree", async () => {
365
+ const tree = new SuccessNode({ id: "tree" });
366
+ registerTree("clone-tree", tree);
367
+
368
+ const original = new SubTree({
369
+ id: "sg1",
370
+ name: "Original",
371
+ treeId: "clone-tree",
372
+ });
373
+
374
+ // Tick original to trigger lazy loading
375
+ await original.tick(context);
376
+ expect(original.clonedTree).toBeDefined();
377
+
378
+ // Clone should not have a cached tree yet
379
+ const cloned = original.clone() as SubTree;
380
+ expect(cloned.clonedTree).toBeUndefined();
381
+
382
+ // Tick clone to trigger its own lazy loading
383
+ await cloned.tick(context);
384
+ expect(cloned.clonedTree).toBeDefined();
385
+
386
+ // Should be different instances
387
+ expect(cloned.clonedTree).not.toBe(original.clonedTree);
388
+ });
389
+ });
390
+
391
+ describe("Parameter Passing (params)", () => {
392
+ it("should pass static params to subtree blackboard", async () => {
393
+ // Create a node that reads params from its blackboard
394
+ let capturedOrderId: unknown = "not-set";
395
+ let capturedQuantity: unknown = "not-set";
396
+ class ReadParamsNode extends SuccessNode {
397
+ async tick(ctx: TemporalContext) {
398
+ capturedOrderId = ctx.blackboard.get("orderId");
399
+ capturedQuantity = ctx.blackboard.get("quantity");
400
+ return await super.tick(ctx);
401
+ }
402
+ }
403
+
404
+ const tree = new Sequence({ id: "tree", name: "Tree" });
405
+ tree.addChild(new ReadParamsNode({ id: "reader" }));
406
+ registerTree("params-tree", tree);
407
+
408
+ const subTree = new SubTree({
409
+ id: "sg1",
410
+ name: "With Params",
411
+ treeId: "params-tree",
412
+ params: {
413
+ orderId: "ORD-123",
414
+ quantity: 5,
415
+ },
416
+ });
417
+
418
+ await subTree.tick(context);
419
+
420
+ expect(capturedOrderId).toBe("ORD-123");
421
+ expect(capturedQuantity).toBe(5);
422
+ });
423
+
424
+ it("should resolve variable references in params from parent blackboard", async () => {
425
+ // Set values in parent blackboard
426
+ blackboard.set("currentCustomer", "CUST-456");
427
+ blackboard.set("selectedProduct", { id: "PROD-789", name: "Widget" });
428
+
429
+ let capturedCustomer: unknown = "not-set";
430
+ let capturedProduct: unknown = "not-set";
431
+ class ReadParamsNode extends SuccessNode {
432
+ async tick(ctx: TemporalContext) {
433
+ capturedCustomer = ctx.blackboard.get("customer");
434
+ capturedProduct = ctx.blackboard.get("product");
435
+ return await super.tick(ctx);
436
+ }
437
+ }
438
+
439
+ const tree = new Sequence({ id: "tree", name: "Tree" });
440
+ tree.addChild(new ReadParamsNode({ id: "reader" }));
441
+ registerTree("resolve-params-tree", tree);
442
+
443
+ const subTree = new SubTree({
444
+ id: "sg1",
445
+ name: "With Variable Params",
446
+ treeId: "resolve-params-tree",
447
+ params: {
448
+ customer: "${bb.currentCustomer}",
449
+ product: "${bb.selectedProduct}",
450
+ },
451
+ });
452
+
453
+ await subTree.tick(context);
454
+
455
+ expect(capturedCustomer).toBe("CUST-456");
456
+ expect(capturedProduct).toEqual({ id: "PROD-789", name: "Widget" });
457
+ });
458
+
459
+ it("should resolve params from workflow input", async () => {
460
+ // Set workflow input
461
+ context.input = Object.freeze({
462
+ orderId: "INPUT-ORD-999",
463
+ priority: "high",
464
+ });
465
+
466
+ let capturedOrderId: unknown = "not-set";
467
+ let capturedPriority: unknown = "not-set";
468
+ class ReadParamsNode extends SuccessNode {
469
+ async tick(ctx: TemporalContext) {
470
+ capturedOrderId = ctx.blackboard.get("orderId");
471
+ capturedPriority = ctx.blackboard.get("priority");
472
+ return await super.tick(ctx);
473
+ }
474
+ }
475
+
476
+ const tree = new Sequence({ id: "tree", name: "Tree" });
477
+ tree.addChild(new ReadParamsNode({ id: "reader" }));
478
+ registerTree("input-params-tree", tree);
479
+
480
+ const subTree = new SubTree({
481
+ id: "sg1",
482
+ name: "With Input Params",
483
+ treeId: "input-params-tree",
484
+ params: {
485
+ orderId: "${input.orderId}",
486
+ priority: "${input.priority}",
487
+ },
488
+ });
489
+
490
+ await subTree.tick(context);
491
+
492
+ expect(capturedOrderId).toBe("INPUT-ORD-999");
493
+ expect(capturedPriority).toBe("high");
494
+ });
495
+
496
+ it("should resolve nested property access in params", async () => {
497
+ blackboard.set("user", {
498
+ profile: {
499
+ name: "Alice",
500
+ email: "alice@example.com",
501
+ },
502
+ });
503
+
504
+ let capturedName: unknown = "not-set";
505
+ let capturedEmail: unknown = "not-set";
506
+ class ReadParamsNode extends SuccessNode {
507
+ async tick(ctx: TemporalContext) {
508
+ capturedName = ctx.blackboard.get("userName");
509
+ capturedEmail = ctx.blackboard.get("userEmail");
510
+ return await super.tick(ctx);
511
+ }
512
+ }
513
+
514
+ const tree = new Sequence({ id: "tree", name: "Tree" });
515
+ tree.addChild(new ReadParamsNode({ id: "reader" }));
516
+ registerTree("nested-params-tree", tree);
517
+
518
+ const subTree = new SubTree({
519
+ id: "sg1",
520
+ name: "With Nested Params",
521
+ treeId: "nested-params-tree",
522
+ params: {
523
+ userName: "${bb.user.profile.name}",
524
+ userEmail: "${bb.user.profile.email}",
525
+ },
526
+ });
527
+
528
+ await subTree.tick(context);
529
+
530
+ expect(capturedName).toBe("Alice");
531
+ expect(capturedEmail).toBe("alice@example.com");
532
+ });
533
+
534
+ it("should handle complex nested params structure", async () => {
535
+ blackboard.set("basePrice", 100);
536
+ context.input = Object.freeze({ taxRate: 0.08 });
537
+
538
+ let capturedConfig: unknown = "not-set";
539
+ class ReadParamsNode extends SuccessNode {
540
+ async tick(ctx: TemporalContext) {
541
+ capturedConfig = ctx.blackboard.get("config");
542
+ return await super.tick(ctx);
543
+ }
544
+ }
545
+
546
+ const tree = new Sequence({ id: "tree", name: "Tree" });
547
+ tree.addChild(new ReadParamsNode({ id: "reader" }));
548
+ registerTree("complex-params-tree", tree);
549
+
550
+ const subTree = new SubTree({
551
+ id: "sg1",
552
+ name: "With Complex Params",
553
+ treeId: "complex-params-tree",
554
+ params: {
555
+ config: {
556
+ price: "${bb.basePrice}",
557
+ tax: "${input.taxRate}",
558
+ metadata: {
559
+ source: "parent",
560
+ timestamp: 12345,
561
+ },
562
+ },
563
+ },
564
+ });
565
+
566
+ await subTree.tick(context);
567
+
568
+ expect(capturedConfig).toEqual({
569
+ price: 100,
570
+ tax: 0.08,
571
+ metadata: {
572
+ source: "parent",
573
+ timestamp: 12345,
574
+ },
575
+ });
576
+ });
577
+
578
+ it("should not leak params to parent scope", async () => {
579
+ const tree = new SuccessNode({ id: "tree" });
580
+ registerTree("leak-test-tree", tree);
581
+
582
+ const subTree = new SubTree({
583
+ id: "sg1",
584
+ name: "Leak Test",
585
+ treeId: "leak-test-tree",
586
+ params: {
587
+ secretParam: "should-not-leak",
588
+ },
589
+ });
590
+
591
+ await subTree.tick(context);
592
+
593
+ // Parent blackboard should NOT have the param
594
+ expect(blackboard.has("secretParam")).toBe(false);
595
+ });
596
+ });
597
+
598
+ describe("Output Export (outputs)", () => {
599
+ it("should export specified outputs to parent blackboard", async () => {
600
+ // Create a node that sets values in its blackboard
601
+ class SetOutputsNode extends SuccessNode {
602
+ async tick(ctx: TemporalContext) {
603
+ ctx.blackboard.set("result", "computation-result");
604
+ ctx.blackboard.set("processingTime", 150);
605
+ ctx.blackboard.set("internalState", "should-not-export");
606
+ return await super.tick(ctx);
607
+ }
608
+ }
609
+
610
+ const tree = new Sequence({ id: "tree", name: "Tree" });
611
+ tree.addChild(new SetOutputsNode({ id: "setter" }));
612
+ registerTree("outputs-tree", tree);
613
+
614
+ const subTree = new SubTree({
615
+ id: "sg1",
616
+ name: "With Outputs",
617
+ treeId: "outputs-tree",
618
+ outputs: ["result", "processingTime"],
619
+ });
620
+
621
+ await subTree.tick(context);
622
+
623
+ // Exported values should be in parent
624
+ expect(blackboard.get("result")).toBe("computation-result");
625
+ expect(blackboard.get("processingTime")).toBe(150);
626
+ // Non-exported values should NOT be in parent
627
+ expect(blackboard.has("internalState")).toBe(false);
628
+ });
629
+
630
+ it("should skip missing output keys without error", async () => {
631
+ class SetPartialOutputsNode extends SuccessNode {
632
+ async tick(ctx: TemporalContext) {
633
+ ctx.blackboard.set("existingOutput", "value");
634
+ // Note: "missingOutput" is not set
635
+ return await super.tick(ctx);
636
+ }
637
+ }
638
+
639
+ const tree = new Sequence({ id: "tree", name: "Tree" });
640
+ tree.addChild(new SetPartialOutputsNode({ id: "setter" }));
641
+ registerTree("partial-outputs-tree", tree);
642
+
643
+ const subTree = new SubTree({
644
+ id: "sg1",
645
+ name: "With Partial Outputs",
646
+ treeId: "partial-outputs-tree",
647
+ outputs: ["existingOutput", "missingOutput"],
648
+ });
649
+
650
+ const status = await subTree.tick(context);
651
+
652
+ expect(status).toBe(NodeStatus.SUCCESS);
653
+ expect(blackboard.get("existingOutput")).toBe("value");
654
+ expect(blackboard.has("missingOutput")).toBe(false);
655
+ });
656
+
657
+ it("should export complex objects", async () => {
658
+ class SetComplexOutputNode extends SuccessNode {
659
+ async tick(ctx: TemporalContext) {
660
+ ctx.blackboard.set("userResult", {
661
+ id: "USER-123",
662
+ profile: { name: "Bob", score: 95 },
663
+ tags: ["active", "premium"],
664
+ });
665
+ return await super.tick(ctx);
666
+ }
667
+ }
668
+
669
+ const tree = new Sequence({ id: "tree", name: "Tree" });
670
+ tree.addChild(new SetComplexOutputNode({ id: "setter" }));
671
+ registerTree("complex-output-tree", tree);
672
+
673
+ const subTree = new SubTree({
674
+ id: "sg1",
675
+ name: "With Complex Output",
676
+ treeId: "complex-output-tree",
677
+ outputs: ["userResult"],
678
+ });
679
+
680
+ await subTree.tick(context);
681
+
682
+ expect(blackboard.get("userResult")).toEqual({
683
+ id: "USER-123",
684
+ profile: { name: "Bob", score: 95 },
685
+ tags: ["active", "premium"],
686
+ });
687
+ });
688
+
689
+ it("should not export outputs on failure", async () => {
690
+ class SetThenFailNode extends FailureNode {
691
+ async tick(ctx: TemporalContext) {
692
+ ctx.blackboard.set("shouldNotExport", "value");
693
+ return await super.tick(ctx);
694
+ }
695
+ }
696
+
697
+ const tree = new Sequence({ id: "tree", name: "Tree" });
698
+ tree.addChild(new SetThenFailNode({ id: "failer" }));
699
+ registerTree("fail-output-tree", tree);
700
+
701
+ const subTree = new SubTree({
702
+ id: "sg1",
703
+ name: "Failing SubTree",
704
+ treeId: "fail-output-tree",
705
+ outputs: ["shouldNotExport"],
706
+ });
707
+
708
+ const status = await subTree.tick(context);
709
+
710
+ expect(status).toBe(NodeStatus.FAILURE);
711
+ // Output should NOT be exported on failure
712
+ expect(blackboard.has("shouldNotExport")).toBe(false);
713
+ });
714
+
715
+ it("should export outputs on running status", async () => {
716
+ class SetThenRunningNode extends RunningNode {
717
+ async tick(ctx: TemporalContext) {
718
+ ctx.blackboard.set("partialResult", "in-progress");
719
+ return await super.tick(ctx);
720
+ }
721
+ }
722
+
723
+ const tree = new Sequence({ id: "tree", name: "Tree" });
724
+ tree.addChild(new SetThenRunningNode({ id: "runner" }));
725
+ registerTree("running-output-tree", tree);
726
+
727
+ const subTree = new SubTree({
728
+ id: "sg1",
729
+ name: "Running SubTree",
730
+ treeId: "running-output-tree",
731
+ outputs: ["partialResult"],
732
+ });
733
+
734
+ const status = await subTree.tick(context);
735
+
736
+ expect(status).toBe(NodeStatus.RUNNING);
737
+ // Output should be exported even when running (useful for streaming results)
738
+ expect(blackboard.get("partialResult")).toBe("in-progress");
739
+ });
740
+ });
741
+
742
+ describe("Combined Params and Outputs", () => {
743
+ it("should pass params and export outputs in same subtree", async () => {
744
+ // Create a node that reads params, computes, and sets outputs
745
+ class ComputeNode extends SuccessNode {
746
+ async tick(ctx: TemporalContext) {
747
+ const price = ctx.blackboard.get("price") as number;
748
+ const quantity = ctx.blackboard.get("quantity") as number;
749
+ const taxRate = ctx.blackboard.get("taxRate") as number;
750
+
751
+ const subtotal = price * quantity;
752
+ const tax = subtotal * taxRate;
753
+ const total = subtotal + tax;
754
+
755
+ ctx.blackboard.set("subtotal", subtotal);
756
+ ctx.blackboard.set("tax", tax);
757
+ ctx.blackboard.set("total", total);
758
+ return await super.tick(ctx);
759
+ }
760
+ }
761
+
762
+ const tree = new Sequence({ id: "tree", name: "Tree" });
763
+ tree.addChild(new ComputeNode({ id: "compute" }));
764
+ registerTree("compute-tree", tree);
765
+
766
+ // Set parent values
767
+ blackboard.set("productPrice", 100);
768
+ blackboard.set("orderQuantity", 3);
769
+
770
+ const subTree = new SubTree({
771
+ id: "calculate-order",
772
+ name: "Calculate Order",
773
+ treeId: "compute-tree",
774
+ params: {
775
+ price: "${bb.productPrice}",
776
+ quantity: "${bb.orderQuantity}",
777
+ taxRate: 0.08,
778
+ },
779
+ outputs: ["subtotal", "tax", "total"],
780
+ });
781
+
782
+ await subTree.tick(context);
783
+
784
+ // Outputs should be in parent blackboard
785
+ expect(blackboard.get("subtotal")).toBe(300);
786
+ expect(blackboard.get("tax")).toBe(24);
787
+ expect(blackboard.get("total")).toBe(324);
788
+
789
+ // Params should NOT be in parent blackboard
790
+ expect(blackboard.has("price")).toBe(false);
791
+ expect(blackboard.has("quantity")).toBe(false);
792
+ expect(blackboard.has("taxRate")).toBe(false);
793
+ });
794
+
795
+ it("should handle real-world order processing scenario", async () => {
796
+ // Simulate order processing subtree
797
+ class ProcessOrderNode extends SuccessNode {
798
+ async tick(ctx: TemporalContext) {
799
+ const orderId = ctx.blackboard.get("orderId") as string;
800
+ const items = ctx.blackboard.get("items") as Array<{ price: number; qty: number }>;
801
+
802
+ // Simulate processing
803
+ const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
804
+
805
+ ctx.blackboard.set("orderResult", {
806
+ orderId,
807
+ total,
808
+ status: "processed",
809
+ timestamp: Date.now(),
810
+ });
811
+ ctx.blackboard.set("orderStatus", "SUCCESS");
812
+
813
+ return await super.tick(ctx);
814
+ }
815
+ }
816
+
817
+ const tree = new Sequence({ id: "tree", name: "Tree" });
818
+ tree.addChild(new ProcessOrderNode({ id: "process" }));
819
+ registerTree("order-processor", tree);
820
+
821
+ // Set up workflow input and parent state
822
+ context.input = Object.freeze({
823
+ orderId: "ORD-2024-001",
824
+ });
825
+ blackboard.set("cartItems", [
826
+ { price: 25, qty: 2 },
827
+ { price: 50, qty: 1 },
828
+ { price: 10, qty: 3 },
829
+ ]);
830
+
831
+ const subTree = new SubTree({
832
+ id: "process-order",
833
+ name: "Process Order",
834
+ treeId: "order-processor",
835
+ params: {
836
+ orderId: "${input.orderId}",
837
+ items: "${bb.cartItems}",
838
+ },
839
+ outputs: ["orderResult", "orderStatus"],
840
+ });
841
+
842
+ await subTree.tick(context);
843
+
844
+ // Check exported outputs
845
+ expect(blackboard.get("orderStatus")).toBe("SUCCESS");
846
+ const orderResult = blackboard.get("orderResult") as Record<string, unknown>;
847
+ expect(orderResult.orderId).toBe("ORD-2024-001");
848
+ expect(orderResult.total).toBe(130); // 25*2 + 50*1 + 10*3
849
+ expect(orderResult.status).toBe("processed");
850
+ });
851
+
852
+ it("should work with multiple subtree calls sharing outputs", async () => {
853
+ class IncrementNode extends SuccessNode {
854
+ async tick(ctx: TemporalContext) {
855
+ const current = ctx.blackboard.get("counter") as number;
856
+ ctx.blackboard.set("counter", current + 1);
857
+ return await super.tick(ctx);
858
+ }
859
+ }
860
+
861
+ const tree = new Sequence({ id: "tree", name: "Tree" });
862
+ tree.addChild(new IncrementNode({ id: "increment" }));
863
+ registerTree("incrementer", tree);
864
+
865
+ blackboard.set("counter", 0);
866
+
867
+ const subTree1 = new SubTree({
868
+ id: "inc1",
869
+ name: "Increment 1",
870
+ treeId: "incrementer",
871
+ params: { counter: "${bb.counter}" },
872
+ outputs: ["counter"],
873
+ });
874
+
875
+ const subTree2 = new SubTree({
876
+ id: "inc2",
877
+ name: "Increment 2",
878
+ treeId: "incrementer",
879
+ params: { counter: "${bb.counter}" },
880
+ outputs: ["counter"],
881
+ });
882
+
883
+ await subTree1.tick(context);
884
+ expect(blackboard.get("counter")).toBe(1);
885
+
886
+ await subTree2.tick(context);
887
+ expect(blackboard.get("counter")).toBe(2);
888
+
889
+ await subTree1.tick(context);
890
+ expect(blackboard.get("counter")).toBe(3);
891
+ });
892
+ });
893
+ });