@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,605 @@
1
+ /**
2
+ * Base implementation for all behavior tree nodes
3
+ */
4
+
5
+ import { ConfigurationError } from "./errors.js";
6
+ import { NodeEventType } from "./events.js";
7
+ import {
8
+ type TemporalContext,
9
+ type NodeConfiguration,
10
+ NodeStatus,
11
+ type PortDefinition,
12
+ type TreeNode,
13
+ } from "./types.js";
14
+ import { OperationCancelledError } from "./utils/signal-check.js";
15
+ import { handleNodeError } from "./utils/error-handler.js";
16
+
17
+ /**
18
+ * Abstract base class for all tree nodes
19
+ */
20
+ export abstract class BaseNode implements TreeNode {
21
+ readonly id: string;
22
+ readonly name: string;
23
+ readonly type: string;
24
+
25
+ protected _status: NodeStatus = NodeStatus.IDLE;
26
+ protected _lastError?: string;
27
+ protected config: NodeConfiguration;
28
+ protected _eventEmitter?: import("./events.js").NodeEventEmitter;
29
+
30
+ parent?: TreeNode;
31
+ children?: TreeNode[];
32
+
33
+ constructor(config: NodeConfiguration) {
34
+ this.id = config.id;
35
+ this.name = config.name || config.id;
36
+ this.type = this.constructor.name;
37
+ this.config = config;
38
+ }
39
+
40
+ /**
41
+ * Main tick method - subclasses override to implement execution logic
42
+ * Returns Promise for async/RUNNING semantics
43
+ * All errors are caught and converted to NodeStatus.FAILURE
44
+ */
45
+ abstract tick(context: TemporalContext): Promise<NodeStatus>;
46
+
47
+ /**
48
+ * Clone this node (deep copy including children)
49
+ * Must be implemented by subclasses
50
+ */
51
+ abstract clone(): TreeNode;
52
+
53
+ halt(): void {
54
+ console.log(`[${this.type}:${this.name}] Halting...`);
55
+
56
+ // Emit HALT event
57
+ this._eventEmitter?.emit({
58
+ type: NodeEventType.HALT,
59
+ nodeId: this.id,
60
+ nodeName: this.name,
61
+ nodeType: this.type,
62
+ timestamp: Date.now(),
63
+ });
64
+
65
+ if (this._status === NodeStatus.RUNNING) {
66
+ this.onHalt();
67
+ this._status = NodeStatus.IDLE;
68
+ }
69
+ }
70
+
71
+ reset(): void {
72
+ console.log(`[${this.type}:${this.name}] Resetting...`);
73
+
74
+ // Emit RESET event
75
+ this._eventEmitter?.emit({
76
+ type: NodeEventType.RESET,
77
+ nodeId: this.id,
78
+ nodeName: this.name,
79
+ nodeType: this.type,
80
+ timestamp: Date.now(),
81
+ });
82
+
83
+ this._status = NodeStatus.IDLE;
84
+ this._lastError = undefined;
85
+ this.onReset();
86
+ }
87
+
88
+ status(): NodeStatus {
89
+ return this._status;
90
+ }
91
+
92
+ get lastError(): string | undefined {
93
+ return this._lastError;
94
+ }
95
+
96
+ providedPorts(): PortDefinition[] {
97
+ return [];
98
+ }
99
+
100
+ /**
101
+ * Hook for derived classes to implement custom halt logic
102
+ */
103
+ protected onHalt(): void {
104
+ // Override in derived classes if needed
105
+ }
106
+
107
+ /**
108
+ * Hook for derived classes to implement custom reset logic
109
+ */
110
+ protected onReset(): void {
111
+ // Override in derived classes if needed
112
+ }
113
+
114
+ /**
115
+ * Helper to get input value from blackboard
116
+ */
117
+ protected getInput<T>(
118
+ context: TemporalContext,
119
+ key: string,
120
+ defaultValue?: T,
121
+ ): T {
122
+ const fullKey = (this.config[key] as string) || key;
123
+ return context.blackboard.getPort<T>(fullKey, defaultValue);
124
+ }
125
+
126
+ /**
127
+ * Helper to set output value to blackboard
128
+ */
129
+ protected setOutput<T>(
130
+ context: TemporalContext,
131
+ key: string,
132
+ value: T,
133
+ ): void {
134
+ const fullKey = (this.config[key] as string) || key;
135
+ context.blackboard.setPort(fullKey, value);
136
+ }
137
+
138
+ /**
139
+ * Log helper for debugging
140
+ */
141
+ protected log(message: string, ...args: unknown[]): void {
142
+ console.log(`[${this.type}:${this.name}] ${message}`, ...args);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Base class for action nodes
148
+ * Includes resumable execution support and Effect-based async/RUNNING semantics
149
+ */
150
+ export abstract class ActionNode extends BaseNode {
151
+ /**
152
+ * Clone this action node
153
+ * Leaf nodes have no children to clone
154
+ */
155
+ clone(): TreeNode {
156
+ const ClonedClass = this.constructor as new (
157
+ config: NodeConfiguration,
158
+ ) => this;
159
+ return new ClonedClass({ ...this.config });
160
+ }
161
+
162
+ /**
163
+ * Tick with resumable execution support for leaf nodes
164
+ * Uses async/await for Promise-based async/RUNNING semantics
165
+ * All errors are caught and converted to NodeStatus.FAILURE
166
+ */
167
+ async tick(context: TemporalContext): Promise<NodeStatus> {
168
+ try {
169
+ // Store eventEmitter reference for halt/reset
170
+ this._eventEmitter = context.eventEmitter;
171
+
172
+ // Emit TICK_START event
173
+ context.eventEmitter?.emit({
174
+ type: NodeEventType.TICK_START,
175
+ nodeId: this.id,
176
+ nodeName: this.name,
177
+ nodeType: this.type,
178
+ timestamp: Date.now(),
179
+ });
180
+
181
+ // Execute the actual node logic
182
+ const status = await this.executeTick(context);
183
+ this._status = status;
184
+
185
+ // Emit TICK_END event with status
186
+ context.eventEmitter?.emit({
187
+ type: NodeEventType.TICK_END,
188
+ nodeId: this.id,
189
+ nodeName: this.name,
190
+ nodeType: this.type,
191
+ timestamp: Date.now(),
192
+ data: { status },
193
+ });
194
+
195
+ return status;
196
+ } catch (error: unknown) {
197
+ const errorMessage =
198
+ error instanceof Error ? error.message : String(error);
199
+ const errorStack = error instanceof Error ? error.stack : undefined;
200
+
201
+ // Store the error message
202
+ this._lastError = errorMessage;
203
+ this._status = NodeStatus.FAILURE;
204
+
205
+ // Emit ERROR event with proper format for ExecutionTracker
206
+ context.eventEmitter?.emit({
207
+ type: NodeEventType.ERROR,
208
+ nodeId: this.id,
209
+ nodeName: this.name,
210
+ nodeType: this.type,
211
+ timestamp: Date.now(),
212
+ data: {
213
+ error: {
214
+ message: errorMessage,
215
+ stack: errorStack,
216
+ },
217
+ blackboard: context.blackboard?.toJSON?.() ?? {},
218
+ },
219
+ });
220
+
221
+ // Emit TICK_END with FAILURE status
222
+ context.eventEmitter?.emit({
223
+ type: NodeEventType.TICK_END,
224
+ nodeId: this.id,
225
+ nodeName: this.name,
226
+ nodeType: this.type,
227
+ timestamp: Date.now(),
228
+ data: { status: NodeStatus.FAILURE },
229
+ });
230
+
231
+ // Use centralized error handler for consistent behavior
232
+ return handleNodeError(error);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Abstract method for subclasses to implement their execution logic
238
+ * Returns Promise for async operations
239
+ */
240
+ protected abstract executeTick(
241
+ context: TemporalContext,
242
+ ): Promise<NodeStatus>;
243
+ }
244
+
245
+ /**
246
+ * Base class for condition nodes
247
+ * Includes resumable execution support and async/RUNNING semantics
248
+ */
249
+ export abstract class ConditionNode extends BaseNode {
250
+ /**
251
+ * Clone this condition node
252
+ * Leaf nodes have no children to clone
253
+ */
254
+ clone(): TreeNode {
255
+ const ClonedClass = this.constructor as new (
256
+ config: NodeConfiguration,
257
+ ) => this;
258
+ return new ClonedClass({ ...this.config });
259
+ }
260
+
261
+ /**
262
+ * Tick with resumable execution support for leaf nodes
263
+ * Uses async/await for Promise-based async/RUNNING semantics
264
+ * All errors are caught and converted to NodeStatus.FAILURE
265
+ */
266
+ async tick(context: TemporalContext): Promise<NodeStatus> {
267
+ try {
268
+ // Store eventEmitter reference for halt/reset
269
+ this._eventEmitter = context.eventEmitter;
270
+
271
+ // Emit TICK_START event
272
+ context.eventEmitter?.emit({
273
+ type: NodeEventType.TICK_START,
274
+ nodeId: this.id,
275
+ nodeName: this.name,
276
+ nodeType: this.type,
277
+ timestamp: Date.now(),
278
+ });
279
+
280
+ // Execute the actual node logic
281
+ const status = await this.executeTick(context);
282
+ this._status = status;
283
+
284
+ // Emit TICK_END event with status
285
+ context.eventEmitter?.emit({
286
+ type: NodeEventType.TICK_END,
287
+ nodeId: this.id,
288
+ nodeName: this.name,
289
+ nodeType: this.type,
290
+ timestamp: Date.now(),
291
+ data: { status },
292
+ });
293
+
294
+ return status;
295
+ } catch (error: unknown) {
296
+ const errorMessage =
297
+ error instanceof Error ? error.message : String(error);
298
+ const errorStack = error instanceof Error ? error.stack : undefined;
299
+
300
+ // Store the error message
301
+ this._lastError = errorMessage;
302
+ this._status = NodeStatus.FAILURE;
303
+
304
+ // Emit ERROR event with proper format for ExecutionTracker
305
+ context.eventEmitter?.emit({
306
+ type: NodeEventType.ERROR,
307
+ nodeId: this.id,
308
+ nodeName: this.name,
309
+ nodeType: this.type,
310
+ timestamp: Date.now(),
311
+ data: {
312
+ error: {
313
+ message: errorMessage,
314
+ stack: errorStack,
315
+ },
316
+ blackboard: context.blackboard?.toJSON?.() ?? {},
317
+ },
318
+ });
319
+
320
+ // Emit TICK_END with FAILURE status
321
+ context.eventEmitter?.emit({
322
+ type: NodeEventType.TICK_END,
323
+ nodeId: this.id,
324
+ nodeName: this.name,
325
+ nodeType: this.type,
326
+ timestamp: Date.now(),
327
+ data: { status: NodeStatus.FAILURE },
328
+ });
329
+
330
+ // Use centralized error handler for consistent behavior
331
+ return handleNodeError(error);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Abstract method for subclasses to implement their execution logic
337
+ * Returns Promise for async operations
338
+ */
339
+ protected abstract executeTick(
340
+ context: TemporalContext,
341
+ ): Promise<NodeStatus>;
342
+ }
343
+
344
+ /**
345
+ * Base class for decorator nodes (single child)
346
+ */
347
+ export abstract class DecoratorNode extends BaseNode {
348
+ protected child?: TreeNode;
349
+
350
+ /**
351
+ * Clone this decorator node including its child
352
+ */
353
+ clone(): TreeNode {
354
+ const ClonedClass = this.constructor as new (
355
+ config: NodeConfiguration,
356
+ ) => this;
357
+ const cloned = new ClonedClass({ ...this.config });
358
+ if (this.child) {
359
+ cloned.setChild(this.child.clone());
360
+ }
361
+ return cloned;
362
+ }
363
+
364
+ /**
365
+ * Tick with resumable execution support - decorators can be resume points
366
+ * Uses async/await for Promise-based async/RUNNING semantics
367
+ * All errors are caught and converted to NodeStatus.FAILURE
368
+ */
369
+ async tick(context: TemporalContext): Promise<NodeStatus> {
370
+ try {
371
+ // Emit TICK_START event
372
+ context.eventEmitter?.emit({
373
+ type: NodeEventType.TICK_START,
374
+ nodeId: this.id,
375
+ nodeName: this.name,
376
+ nodeType: this.type,
377
+ timestamp: Date.now(),
378
+ });
379
+
380
+ // Execute decorator's tick logic
381
+ const status = await this.executeTick(context);
382
+
383
+ // Emit TICK_END event
384
+ context.eventEmitter?.emit({
385
+ type: NodeEventType.TICK_END,
386
+ nodeId: this.id,
387
+ nodeName: this.name,
388
+ nodeType: this.type,
389
+ timestamp: Date.now(),
390
+ data: { status },
391
+ });
392
+
393
+ return status;
394
+ } catch (error: unknown) {
395
+ const errorMessage =
396
+ error instanceof Error ? error.message : String(error);
397
+ const errorStack = error instanceof Error ? error.stack : undefined;
398
+
399
+ // Store the error message
400
+ this._lastError = errorMessage;
401
+ this._status = NodeStatus.FAILURE;
402
+
403
+ // Emit ERROR event with proper format for ExecutionTracker
404
+ context.eventEmitter?.emit({
405
+ type: NodeEventType.ERROR,
406
+ nodeId: this.id,
407
+ nodeName: this.name,
408
+ nodeType: this.type,
409
+ timestamp: Date.now(),
410
+ data: {
411
+ error: {
412
+ message: errorMessage,
413
+ stack: errorStack,
414
+ },
415
+ blackboard: context.blackboard?.toJSON?.() ?? {},
416
+ },
417
+ });
418
+
419
+ // Emit TICK_END with FAILURE status
420
+ context.eventEmitter?.emit({
421
+ type: NodeEventType.TICK_END,
422
+ nodeId: this.id,
423
+ nodeName: this.name,
424
+ nodeType: this.type,
425
+ timestamp: Date.now(),
426
+ data: { status: NodeStatus.FAILURE },
427
+ });
428
+
429
+ // Use centralized error handler for consistent behavior
430
+ return handleNodeError(error);
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Decorator nodes must implement their wrapping logic
436
+ * Returns Promise for async operations
437
+ */
438
+ protected abstract executeTick(
439
+ context: TemporalContext,
440
+ ): Promise<NodeStatus>;
441
+
442
+ setChild(child: TreeNode): void {
443
+ if (!child) {
444
+ throw new Error("Cannot set undefined child on decorator node");
445
+ }
446
+ this.child = child;
447
+ this.children = [child];
448
+ child.parent = this;
449
+ }
450
+
451
+ halt(): void {
452
+ super.halt();
453
+ if (this.child && this.child.status() === NodeStatus.RUNNING) {
454
+ this.child.halt();
455
+ }
456
+ }
457
+
458
+ reset(): void {
459
+ super.reset();
460
+ if (this.child) {
461
+ this.child.reset();
462
+ }
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Base class for composite nodes (multiple children)
468
+ */
469
+ export abstract class CompositeNode extends BaseNode {
470
+ protected _children: TreeNode[] = [];
471
+
472
+ /**
473
+ * Clone this composite node including all children
474
+ */
475
+ clone(): TreeNode {
476
+ const ClonedClass = this.constructor as new (
477
+ config: NodeConfiguration,
478
+ ) => this;
479
+ const cloned = new ClonedClass({ ...this.config });
480
+ // Clone all children
481
+ const clonedChildren = this._children.map((child) => child.clone());
482
+ cloned.addChildren(clonedChildren);
483
+ return cloned;
484
+ }
485
+
486
+ /**
487
+ * Tick with resumable execution support - composites can be resume points
488
+ * Uses async/await for Promise-based async/RUNNING semantics
489
+ * All errors are caught and converted to NodeStatus.FAILURE
490
+ */
491
+ async tick(context: TemporalContext): Promise<NodeStatus> {
492
+ try {
493
+ // Emit TICK_START event
494
+ context.eventEmitter?.emit({
495
+ type: NodeEventType.TICK_START,
496
+ nodeId: this.id,
497
+ nodeName: this.name,
498
+ nodeType: this.type,
499
+ timestamp: Date.now(),
500
+ });
501
+
502
+ // Execute composite's tick logic
503
+ const status = await this.executeTick(context);
504
+
505
+ // Emit TICK_END event
506
+ context.eventEmitter?.emit({
507
+ type: NodeEventType.TICK_END,
508
+ nodeId: this.id,
509
+ nodeName: this.name,
510
+ nodeType: this.type,
511
+ timestamp: Date.now(),
512
+ data: { status },
513
+ });
514
+
515
+ return status;
516
+ } catch (error: unknown) {
517
+ const errorMessage =
518
+ error instanceof Error ? error.message : String(error);
519
+ const errorStack = error instanceof Error ? error.stack : undefined;
520
+
521
+ // Store the error message
522
+ this._lastError = errorMessage;
523
+ this._status = NodeStatus.FAILURE;
524
+
525
+ // Emit ERROR event with proper format for ExecutionTracker
526
+ context.eventEmitter?.emit({
527
+ type: NodeEventType.ERROR,
528
+ nodeId: this.id,
529
+ nodeName: this.name,
530
+ nodeType: this.type,
531
+ timestamp: Date.now(),
532
+ data: {
533
+ error: {
534
+ message: errorMessage,
535
+ stack: errorStack,
536
+ },
537
+ blackboard: context.blackboard?.toJSON?.() ?? {},
538
+ },
539
+ });
540
+
541
+ // Emit TICK_END with FAILURE status
542
+ context.eventEmitter?.emit({
543
+ type: NodeEventType.TICK_END,
544
+ nodeId: this.id,
545
+ nodeName: this.name,
546
+ nodeType: this.type,
547
+ timestamp: Date.now(),
548
+ data: { status: NodeStatus.FAILURE },
549
+ });
550
+
551
+ // Use centralized error handler for consistent behavior
552
+ return handleNodeError(error);
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Composite nodes must implement their traversal logic
558
+ * Returns Promise for async operations
559
+ */
560
+ protected abstract executeTick(
561
+ context: TemporalContext,
562
+ ): Promise<NodeStatus>;
563
+
564
+ addChild(child: TreeNode): void {
565
+ if (!child) {
566
+ throw new Error("Cannot add undefined child to composite node");
567
+ }
568
+ this._children.push(child);
569
+ this.children = this._children;
570
+ child.parent = this;
571
+ }
572
+
573
+ addChildren(children: TreeNode[]): void {
574
+ children.forEach((child) => {
575
+ this.addChild(child);
576
+ });
577
+ }
578
+
579
+ halt(): void {
580
+ super.halt();
581
+ // Halt all running children
582
+ for (const child of this._children) {
583
+ if (child.status() === NodeStatus.RUNNING) {
584
+ child.halt();
585
+ }
586
+ }
587
+ }
588
+
589
+ reset(): void {
590
+ super.reset();
591
+ // Reset all children
592
+ for (const child of this._children) {
593
+ child.reset();
594
+ }
595
+ }
596
+
597
+ protected haltChildren(startIndex: number = 0): void {
598
+ for (let i = startIndex; i < this._children.length; i++) {
599
+ const child = this._children[i];
600
+ if (child && child.status() === NodeStatus.RUNNING) {
601
+ child.halt();
602
+ }
603
+ }
604
+ }
605
+ }