@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,254 @@
1
+ /**
2
+ * Tests for ForEach node
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 { ForEach } from "./for-each.js";
11
+
12
+ describe("ForEach", () => {
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
+ describe("Basic Functionality", () => {
26
+ it("should iterate over collection", async () => {
27
+ const forEach = new ForEach({
28
+ id: "forEach1",
29
+ collectionKey: "items",
30
+ itemKey: "currentItem",
31
+ });
32
+
33
+ const items = ["a", "b", "c"];
34
+ blackboard.set("items", items);
35
+
36
+ const processedItems: string[] = [];
37
+ class RecordingNode extends SuccessNode {
38
+ async tick(context: TemporalContext): Promise<NodeStatus> {
39
+ const superTick = super.tick.bind(this);
40
+ const item = context.blackboard.get("currentItem");
41
+ processedItems.push(item);
42
+ return await superTick(context);
43
+ }
44
+ }
45
+
46
+ forEach.addChild(new RecordingNode({ id: "body" }));
47
+
48
+ const result = await forEach.tick(context);
49
+ expect(result).toBe(NodeStatus.SUCCESS);
50
+ expect(processedItems).toEqual(["a", "b", "c"]);
51
+ });
52
+
53
+ it("should set item and index in blackboard", async () => {
54
+ const forEach = new ForEach({
55
+ id: "forEach1",
56
+ collectionKey: "numbers",
57
+ itemKey: "num",
58
+ indexKey: "i",
59
+ });
60
+
61
+ blackboard.set("numbers", [10, 20, 30]);
62
+
63
+ const recorded: Array<{ item: number; index: number }> = [];
64
+ class RecordingNode extends SuccessNode {
65
+ async tick(context: TemporalContext): Promise<NodeStatus> {
66
+ const superTick = super.tick.bind(this);
67
+ recorded.push({
68
+ item: context.blackboard.get("num"),
69
+ index: context.blackboard.get("i"),
70
+ });
71
+ return await superTick(context);
72
+ }
73
+ }
74
+
75
+ forEach.addChild(new RecordingNode({ id: "body" }));
76
+
77
+ await forEach.tick(context);
78
+
79
+ expect(recorded).toEqual([
80
+ { item: 10, index: 0 },
81
+ { item: 20, index: 1 },
82
+ { item: 30, index: 2 },
83
+ ]);
84
+ });
85
+ });
86
+
87
+ describe("Failure Handling", () => {
88
+ it("should fail on first failure", async () => {
89
+ const forEach = new ForEach({
90
+ id: "forEach1",
91
+ collectionKey: "items",
92
+ itemKey: "item",
93
+ });
94
+
95
+ blackboard.set("items", ["a", "b", "c"]);
96
+
97
+ let tickCount = 0;
98
+ class FailOnSecond extends SuccessNode {
99
+ async tick(context: TemporalContext): Promise<NodeStatus> {
100
+ const superTick = super.tick.bind(this);
101
+ tickCount++;
102
+ if (tickCount === 2) {
103
+ this._status = NodeStatus.FAILURE;
104
+ return NodeStatus.FAILURE;
105
+ }
106
+ return await superTick(context);
107
+ }
108
+ }
109
+
110
+ forEach.addChild(new FailOnSecond({ id: "body" }));
111
+
112
+ const result = await forEach.tick(context);
113
+ expect(result).toBe(NodeStatus.FAILURE);
114
+ expect(tickCount).toBe(2); // Only processed 2 items
115
+ });
116
+
117
+ it("should fail if collection not found", async () => {
118
+ const forEach = new ForEach({
119
+ id: "forEach1",
120
+ collectionKey: "missing",
121
+ itemKey: "item",
122
+ });
123
+
124
+ forEach.addChild(new SuccessNode({ id: "body" }));
125
+
126
+ const result = await forEach.tick(context);
127
+ expect(result).toBe(NodeStatus.FAILURE);
128
+ });
129
+
130
+ it("should return FAILURE if collection is not an array", async () => {
131
+ const forEach = new ForEach({
132
+ id: "forEach1",
133
+ collectionKey: "notArray",
134
+ itemKey: "item",
135
+ });
136
+
137
+ blackboard.set("notArray", "not an array");
138
+ forEach.addChild(new SuccessNode({ id: "body" }));
139
+
140
+ const status = await forEach.tick(context);
141
+ expect(status).toBe(NodeStatus.FAILURE);
142
+ });
143
+ });
144
+
145
+ describe("RUNNING State", () => {
146
+ it("should resume from saved index on RUNNING", async () => {
147
+ const forEach = new ForEach({
148
+ id: "forEach1",
149
+ collectionKey: "items",
150
+ itemKey: "item",
151
+ });
152
+
153
+ blackboard.set("items", ["a", "b", "c"]);
154
+
155
+ let tickCount = 0;
156
+ class RunningOnSecond extends SuccessNode {
157
+ async tick(context: TemporalContext): Promise<NodeStatus> {
158
+ const superTick = super.tick.bind(this);
159
+ tickCount++;
160
+ if (tickCount === 2) {
161
+ this._status = NodeStatus.RUNNING;
162
+ return NodeStatus.RUNNING;
163
+ }
164
+ return await superTick(context);
165
+ }
166
+ }
167
+
168
+ const body = new RunningOnSecond({ id: "body" });
169
+ forEach.addChild(body);
170
+
171
+ // First tick: processes item 0, returns RUNNING on item 1
172
+ let result = await forEach.tick(context);
173
+ expect(result).toBe(NodeStatus.RUNNING);
174
+ expect(tickCount).toBe(2);
175
+
176
+ // Second tick: should resume from item 1
177
+ result = await forEach.tick(context);
178
+ expect(result).toBe(NodeStatus.SUCCESS);
179
+ expect(tickCount).toBe(4); // item 1 (again), item 2
180
+ });
181
+ });
182
+
183
+ describe("Edge Cases", () => {
184
+ it("should return SUCCESS for empty collection", async () => {
185
+ const forEach = new ForEach({
186
+ id: "forEach1",
187
+ collectionKey: "empty",
188
+ itemKey: "item",
189
+ });
190
+
191
+ blackboard.set("empty", []);
192
+ forEach.addChild(new SuccessNode({ id: "body" }));
193
+
194
+ const result = await forEach.tick(context);
195
+ expect(result).toBe(NodeStatus.SUCCESS);
196
+ });
197
+
198
+ it("should reset index on success", async () => {
199
+ const forEach = new ForEach({
200
+ id: "forEach1",
201
+ collectionKey: "items",
202
+ itemKey: "item",
203
+ });
204
+
205
+ blackboard.set("items", ["a", "b"]);
206
+
207
+ let firstRunCount = 0;
208
+ let secondRunCount = 0;
209
+
210
+ class CountingNode extends SuccessNode {
211
+ async tick(context: TemporalContext): Promise<NodeStatus> {
212
+ const superTick = super.tick.bind(this);
213
+ if (secondRunCount > 0) {
214
+ secondRunCount++;
215
+ } else {
216
+ firstRunCount++;
217
+ }
218
+ return await superTick(context);
219
+ }
220
+ }
221
+
222
+ forEach.addChild(new CountingNode({ id: "body" }));
223
+
224
+ // First execution
225
+ await forEach.tick(context);
226
+ expect(firstRunCount).toBe(2);
227
+
228
+ // Second execution should start from beginning
229
+ secondRunCount = 1;
230
+ await forEach.tick(context);
231
+ expect(secondRunCount).toBe(3); // Initial 1 + 2 items
232
+ });
233
+
234
+ it("should propagate ConfigurationError with no child", async () => {
235
+ const forEach = new ForEach({
236
+ id: "forEach1",
237
+ collectionKey: "items",
238
+ itemKey: "item",
239
+ });
240
+
241
+ blackboard.set("items", ["a"]);
242
+
243
+ try {
244
+ await forEach.tick(context);
245
+ expect.fail("Should have thrown ConfigurationError");
246
+ } catch (error) {
247
+ expect(error).toBeInstanceOf(ConfigurationError);
248
+ expect((error as ConfigurationError).message).toContain(
249
+ "ForEach requires at least one child",
250
+ );
251
+ }
252
+ });
253
+ });
254
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * ForEach node - Iterate over collection
3
+ */
4
+
5
+ import { CompositeNode } from "../base-node.js";
6
+ import { ConfigurationError } from "../errors.js";
7
+ import {
8
+ type TemporalContext,
9
+ type NodeConfiguration,
10
+ NodeStatus,
11
+ } from "../types.js";
12
+ import { checkSignal } from "../utils/signal-check.js";
13
+
14
+ export interface ForEachConfiguration extends NodeConfiguration {
15
+ collectionKey: string; // Blackboard key for array
16
+ itemKey: string; // Blackboard key for current item
17
+ indexKey?: string; // Blackboard key for current index
18
+ }
19
+
20
+ /**
21
+ * ForEach iterates over a collection from the blackboard.
22
+ * For each item, it sets the item (and optionally index) in the blackboard
23
+ * and executes the body (first child).
24
+ */
25
+ export class ForEach extends CompositeNode {
26
+ private collectionKey: string;
27
+ private itemKey: string;
28
+ private indexKey?: string;
29
+ private currentIndex: number = 0;
30
+
31
+ constructor(config: ForEachConfiguration) {
32
+ super(config);
33
+ this.collectionKey = config.collectionKey;
34
+ this.itemKey = config.itemKey;
35
+ this.indexKey = config.indexKey;
36
+ }
37
+
38
+ async executeTick(context: TemporalContext): Promise<NodeStatus> {
39
+ if (this._children.length === 0) {
40
+ throw new ConfigurationError(
41
+ "ForEach requires at least one child (body)",
42
+ );
43
+ }
44
+
45
+ const body = this._children[0];
46
+ if (!body) {
47
+ throw new ConfigurationError(
48
+ "ForEach requires at least one child (body)",
49
+ );
50
+ }
51
+
52
+ const collection = context.blackboard.get(this.collectionKey);
53
+
54
+ if (!collection) {
55
+ this.log(`Collection '${this.collectionKey}' not found in blackboard`);
56
+ this._status = NodeStatus.FAILURE;
57
+ return NodeStatus.FAILURE;
58
+ }
59
+
60
+ if (!Array.isArray(collection)) {
61
+ throw new Error(`Collection '${this.collectionKey}' is not an array`);
62
+ }
63
+
64
+ // Empty collection is success
65
+ if (collection.length === 0) {
66
+ this.log("Collection is empty - returning SUCCESS");
67
+ this._status = NodeStatus.SUCCESS;
68
+ return NodeStatus.SUCCESS;
69
+ }
70
+
71
+ this.log(
72
+ `Iterating over collection (${collection.length} items), starting at index ${this.currentIndex}`,
73
+ );
74
+
75
+ // Continue from where we left off
76
+ while (this.currentIndex < collection.length) {
77
+ // Check for cancellation before processing each item
78
+ checkSignal(context.signal);
79
+
80
+ const item = collection[this.currentIndex];
81
+
82
+ // Set current item and index in blackboard
83
+ context.blackboard.set(this.itemKey, item);
84
+ if (this.indexKey) {
85
+ context.blackboard.set(this.indexKey, this.currentIndex);
86
+ }
87
+
88
+ this.log(
89
+ `Processing item ${this.currentIndex}: ${JSON.stringify(item)}`,
90
+ );
91
+ const bodyStatus = await body.tick(context);
92
+
93
+ switch (bodyStatus) {
94
+ case NodeStatus.SUCCESS:
95
+ this.log(`Item ${this.currentIndex} succeeded`);
96
+ this.currentIndex++;
97
+ body.reset(); // Reset for next iteration
98
+ break;
99
+
100
+ case NodeStatus.FAILURE:
101
+ this.log(`Item ${this.currentIndex} failed - ForEach fails`);
102
+ this._status = NodeStatus.FAILURE;
103
+ this.currentIndex = 0; // Reset for next tick
104
+ return NodeStatus.FAILURE;
105
+
106
+ case NodeStatus.RUNNING:
107
+ this.log(`Item ${this.currentIndex} is running`);
108
+ this._status = NodeStatus.RUNNING;
109
+ return NodeStatus.RUNNING; // Will resume from this index next tick
110
+
111
+ default:
112
+ throw new Error(`Unexpected status from body: ${bodyStatus}`);
113
+ }
114
+ }
115
+
116
+ // All items processed successfully
117
+ this.log("All items processed successfully");
118
+ this._status = NodeStatus.SUCCESS;
119
+ this.currentIndex = 0; // Reset for next tick
120
+ return NodeStatus.SUCCESS;
121
+ }
122
+
123
+ protected onReset(): void {
124
+ super.onReset();
125
+ this.currentIndex = 0;
126
+ }
127
+
128
+ protected onHalt(): void {
129
+ super.onHalt();
130
+ this.currentIndex = 0;
131
+ }
132
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Export all composite nodes
3
+ */
4
+
5
+ export { Conditional } from "./conditional.js";
6
+ export { ForEach } from "./for-each.js";
7
+ export { MemorySequence, SequenceWithMemory } from "./memory-sequence.js";
8
+ export { Parallel } from "./parallel.js";
9
+ export type { ParallelStrategy } from "./parallel.js";
10
+ export { ReactiveSequence } from "./reactive-sequence.js";
11
+ export { Recovery } from "./recovery.js";
12
+ export { Selector, Fallback } from "./selector.js";
13
+ export { Sequence } from "./sequence.js";
14
+ export { SubTree } from "./sub-tree.js";
15
+ export { While } from "./while.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * MemorySequence composite configuration schema
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { nodeConfigurationSchema } from "../schemas/base.schema.js";
7
+
8
+ /**
9
+ * Schema for MemorySequence composite configuration
10
+ * Uses base schema only (no additional properties)
11
+ */
12
+ export const memorySequenceConfigurationSchema = nodeConfigurationSchema;
13
+
14
+ /**
15
+ * Validated MemorySequence configuration type
16
+ */
17
+ export type ValidatedMemorySequenceConfiguration = z.infer<
18
+ typeof memorySequenceConfigurationSchema
19
+ >;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Tests for MemorySequence 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 { MemorySequence, SequenceWithMemory } from "./memory-sequence.js";
10
+
11
+ describe("MemorySequence", () => {
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("Basic Functionality", () => {
25
+ it("should execute children in order", async () => {
26
+ const seq = new MemorySequence({ id: "seq1" });
27
+ const child1 = new SuccessNode({ id: "child1" });
28
+ const child2 = new SuccessNode({ id: "child2" });
29
+ const child3 = new SuccessNode({ id: "child3" });
30
+
31
+ seq.addChildren([child1, child2, child3]);
32
+
33
+ const result = await seq.tick(context);
34
+ expect(result).toBe(NodeStatus.SUCCESS);
35
+ });
36
+
37
+ it("should fail fast on first failure", async () => {
38
+ const seq = new MemorySequence({ id: "seq1" });
39
+ const child1 = new SuccessNode({ id: "child1" });
40
+ const child2 = new FailureNode({ id: "child2" });
41
+ const child3 = new SuccessNode({ id: "child3" });
42
+
43
+ seq.addChildren([child1, child2, child3]);
44
+
45
+ const result = await seq.tick(context);
46
+ expect(result).toBe(NodeStatus.FAILURE);
47
+ expect(child3.status()).toBe(NodeStatus.IDLE); // Never executed
48
+ });
49
+ });
50
+
51
+ describe("Memory Behavior", () => {
52
+ it("should skip completed children on retry after failure", async () => {
53
+ const seq = new MemorySequence({ id: "seq1" });
54
+
55
+ // Track execution count for each child
56
+ let child1Ticks = 0;
57
+ let child2Ticks = 0;
58
+ let child3Ticks = 0;
59
+
60
+ class CountingSuccess extends SuccessNode {
61
+ async tick(context: TemporalContext): Promise<NodeStatus> {
62
+ const superTick = super.tick.bind(this);
63
+ if (this.id === "child1") child1Ticks++;
64
+ if (this.id === "child2") child2Ticks++;
65
+ if (this.id === "child3") child3Ticks++;
66
+ return await superTick(context);
67
+ }
68
+ }
69
+
70
+ const child1 = new CountingSuccess({ id: "child1" });
71
+ const child2 = new CountingSuccess({ id: "child2" });
72
+
73
+ // Child3 fails first time, succeeds second
74
+ let child3TickCount = 0;
75
+ class FlipFlopNode extends SuccessNode {
76
+ async tick(context: TemporalContext): Promise<NodeStatus> {
77
+ const superTick = super.tick.bind(this);
78
+ child3TickCount++;
79
+ child3Ticks++;
80
+ // Fail first time, succeed second
81
+ if (child3TickCount === 1) {
82
+ this._status = NodeStatus.FAILURE;
83
+ return NodeStatus.FAILURE;
84
+ }
85
+ return await superTick(context);
86
+ }
87
+ }
88
+
89
+ const child3 = new FlipFlopNode({ id: "child3" });
90
+
91
+ seq.addChildren([child1, child2, child3]);
92
+
93
+ // First tick: child1 and child2 succeed, child3 fails
94
+ let result = await seq.tick(context);
95
+ expect(result).toBe(NodeStatus.FAILURE);
96
+ expect(child1Ticks).toBe(1);
97
+ expect(child2Ticks).toBe(1);
98
+ expect(child3Ticks).toBe(1);
99
+
100
+ // Second tick: should skip child1 and child2, go straight to child3
101
+ result = await seq.tick(context);
102
+ expect(result).toBe(NodeStatus.SUCCESS);
103
+ expect(child1Ticks).toBe(1); // Not re-executed
104
+ expect(child2Ticks).toBe(1); // Not re-executed
105
+ expect(child3Ticks).toBe(2); // Re-executed (not completed before)
106
+ });
107
+
108
+ it("should remember successful children across ticks", async () => {
109
+ const seq = new MemorySequence({ id: "seq1" });
110
+
111
+ const child1 = new SuccessNode({ id: "child1" });
112
+
113
+ // Child2 returns RUNNING first time, SUCCESS second time
114
+ let child2TickCount = 0;
115
+ class TwoTickNode extends SuccessNode {
116
+ async tick(context: TemporalContext): Promise<NodeStatus> {
117
+ const superTick = super.tick.bind(this);
118
+ child2TickCount++;
119
+ if (child2TickCount === 1) {
120
+ this._status = NodeStatus.RUNNING;
121
+ return NodeStatus.RUNNING;
122
+ }
123
+ return await superTick(context);
124
+ }
125
+ }
126
+
127
+ const child2 = new TwoTickNode({ id: "child2" });
128
+ const child3 = new SuccessNode({ id: "child3" });
129
+
130
+ seq.addChildren([child1, child2, child3]);
131
+
132
+ // First tick: child1 succeeds, child2 returns RUNNING
133
+ let result = await seq.tick(context);
134
+ expect(result).toBe(NodeStatus.RUNNING);
135
+ expect(child1.status()).toBe(NodeStatus.SUCCESS);
136
+ expect(child2.status()).toBe(NodeStatus.RUNNING);
137
+ expect(child3.status()).toBe(NodeStatus.IDLE);
138
+
139
+ // Second tick: should skip child1, continue from child2 which now succeeds
140
+ result = await seq.tick(context);
141
+ expect(result).toBe(NodeStatus.SUCCESS);
142
+ expect(child2TickCount).toBe(2);
143
+ expect(child3.status()).toBe(NodeStatus.SUCCESS);
144
+ });
145
+
146
+ it("should not skip RUNNING children", async () => {
147
+ const seq = new MemorySequence({ id: "seq1" });
148
+
149
+ const child1 = new SuccessNode({ id: "child1" });
150
+ const child2 = new RunningNode({ id: "child2" });
151
+
152
+ seq.addChildren([child1, child2]);
153
+
154
+ // First tick: child1 succeeds, child2 running
155
+ let result = await seq.tick(context);
156
+ expect(result).toBe(NodeStatus.RUNNING);
157
+
158
+ // Second tick: child2 still running, should be ticked again
159
+ result = await seq.tick(context);
160
+ expect(result).toBe(NodeStatus.RUNNING);
161
+ expect(child2.status()).toBe(NodeStatus.RUNNING);
162
+ });
163
+
164
+ it("should clear memory on reset", async () => {
165
+ const seq = new MemorySequence({ id: "seq1" });
166
+
167
+ let child1Ticks = 0;
168
+ class CountingSuccess extends SuccessNode {
169
+ async tick(context: TemporalContext): Promise<NodeStatus> {
170
+ const superTick = super.tick.bind(this);
171
+ child1Ticks++;
172
+ return await superTick(context);
173
+ }
174
+ }
175
+
176
+ const child1 = new CountingSuccess({ id: "child1" });
177
+ const child2 = new FailureNode({ id: "child2" });
178
+
179
+ seq.addChildren([child1, child2]);
180
+
181
+ // First execution: child1 succeeds, child2 fails
182
+ await seq.tick(context);
183
+ expect(child1Ticks).toBe(1);
184
+
185
+ // Reset should clear memory
186
+ seq.reset();
187
+
188
+ // Second execution: child1 should be re-executed
189
+ await seq.tick(context);
190
+ expect(child1Ticks).toBe(2);
191
+ });
192
+ });
193
+
194
+ describe("Edge Cases", () => {
195
+ it("should handle empty children array", async () => {
196
+ const seq = new MemorySequence({ id: "seq1" });
197
+ const result = await seq.tick(context);
198
+ expect(result).toBe(NodeStatus.SUCCESS);
199
+ });
200
+
201
+ it("should handle single child", async () => {
202
+ const seq = new MemorySequence({ id: "seq1" });
203
+ const child = new SuccessNode({ id: "child1" });
204
+ seq.addChild(child);
205
+
206
+ const result = await seq.tick(context);
207
+ expect(result).toBe(NodeStatus.SUCCESS);
208
+ });
209
+ });
210
+
211
+ describe("Alias", () => {
212
+ it("should work with SequenceWithMemory alias", async () => {
213
+ const seq = new SequenceWithMemory({ id: "seq1" });
214
+ const child1 = new SuccessNode({ id: "child1" });
215
+ const child2 = new SuccessNode({ id: "child2" });
216
+
217
+ seq.addChildren([child1, child2]);
218
+
219
+ const result = await seq.tick(context);
220
+ expect(result).toBe(NodeStatus.SUCCESS);
221
+ });
222
+ });
223
+ });