@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,283 @@
1
+ /**
2
+ * BehaviorTree - Wrapper for TreeNode with path-based indexing
3
+ * Enables partial tree updates without full reload
4
+ */
5
+
6
+ import type {
7
+ TreeNode,
8
+ TemporalContext,
9
+ WorkflowArgs,
10
+ WorkflowResult,
11
+ } from "./types.js";
12
+ import { ScopedBlackboard } from "./blackboard.js";
13
+
14
+ /**
15
+ * BehaviorTree class that wraps a TreeNode root with path-based indexing
16
+ * Paths use child indices: /0/1/2 represents root → child[0] → child[1] → child[2]
17
+ */
18
+ /**
19
+ * Result of parsing a path with tree ID prefix
20
+ */
21
+ export interface ParsedPath {
22
+ treeId: string;
23
+ nodePath: string;
24
+ }
25
+
26
+ export class BehaviorTree {
27
+ private root: TreeNode;
28
+ private pathIndex: Map<string, TreeNode> = new Map();
29
+ private idIndex: Map<string, TreeNode> = new Map();
30
+
31
+ constructor(root: TreeNode) {
32
+ this.root = root;
33
+ this.buildNodeIndex();
34
+ }
35
+
36
+ /**
37
+ * Parse a path with tree ID prefix.
38
+ * Format: #TreeID/node/path
39
+ *
40
+ * @param fullPath Path string starting with # followed by tree ID
41
+ * @returns Object with treeId and nodePath
42
+ * @throws Error if path format is invalid
43
+ *
44
+ * Valid examples:
45
+ * - "#SimpleTest/0/1" -> { treeId: "SimpleTest", nodePath: "/0/1" }
46
+ * - "#MyTree/" -> { treeId: "MyTree", nodePath: "/" }
47
+ * - "#OnlyTree" -> { treeId: "OnlyTree", nodePath: "/" }
48
+ *
49
+ * Invalid examples:
50
+ * - "/0/1" - missing #TreeID prefix
51
+ * - "#/0/1" - empty tree ID
52
+ * - "#" - empty tree ID
53
+ */
54
+ static parsePathWithTreeId(fullPath: string): ParsedPath {
55
+ if (!fullPath.startsWith("#")) {
56
+ throw new Error(
57
+ `Invalid path format: '${fullPath}'. Path must start with #TreeID (e.g., #SimpleTest/0/1)`,
58
+ );
59
+ }
60
+
61
+ const slashIndex = fullPath.indexOf("/");
62
+ let treeId: string;
63
+ let nodePath: string;
64
+
65
+ if (slashIndex === -1) {
66
+ // No slash after tree ID, e.g., "#TreeId" means root of that tree
67
+ treeId = fullPath.slice(1);
68
+ nodePath = "/";
69
+ } else {
70
+ treeId = fullPath.slice(1, slashIndex);
71
+ nodePath = fullPath.slice(slashIndex);
72
+ }
73
+
74
+ // Validate tree ID is not empty
75
+ if (!treeId || treeId.trim() === "") {
76
+ throw new Error(
77
+ `Invalid path: tree ID cannot be empty in '${fullPath}'. Expected format: #TreeID/path`,
78
+ );
79
+ }
80
+
81
+ // Validate node path starts with /
82
+ if (!nodePath.startsWith("/")) {
83
+ throw new Error(
84
+ `Invalid path: node path must start with '/' in '${fullPath}'`,
85
+ );
86
+ }
87
+
88
+ return { treeId, nodePath };
89
+ }
90
+
91
+ /**
92
+ * Get the root node of the tree
93
+ */
94
+ getRoot(): TreeNode {
95
+ return this.root;
96
+ }
97
+
98
+ /**
99
+ * Find a node by its path
100
+ * Path format: / for root, /0 for first child, /0/1 for second child of first child
101
+ */
102
+ findNodeByPath(path: string): TreeNode | null {
103
+ return this.pathIndex.get(path) || null;
104
+ }
105
+
106
+ /**
107
+ * Find a node by its ID (if it has one)
108
+ * Convenience method for nodes with explicit IDs
109
+ */
110
+ findNodeById(nodeId: string): TreeNode | null {
111
+ return this.idIndex.get(nodeId) || null;
112
+ }
113
+
114
+ /**
115
+ * Get the path for a given node
116
+ * Returns null if the node is not in the tree
117
+ */
118
+ getNodePath(targetNode: TreeNode): string | null {
119
+ for (const [path, node] of this.pathIndex.entries()) {
120
+ if (node === targetNode) {
121
+ return path;
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * Get the path for a node by its ID
129
+ * More reliable than instance-based lookup
130
+ * Returns null if the node is not in the tree
131
+ */
132
+ getNodePathById(nodeId: string): string | null {
133
+ const node = this.findNodeById(nodeId);
134
+ if (!node) return null;
135
+
136
+ // Search pathIndex for this node
137
+ for (const [path, indexedNode] of this.pathIndex.entries()) {
138
+ if (indexedNode.id === nodeId) {
139
+ return path;
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Clone this BehaviorTree (deep clones the underlying TreeNode)
147
+ */
148
+ clone(): BehaviorTree {
149
+ const clonedRoot = this.root.clone();
150
+ return new BehaviorTree(clonedRoot);
151
+ }
152
+
153
+ /**
154
+ * Convert this BehaviorTree to a Temporal workflow function
155
+ * Returns a workflow function that can be registered with Temporal
156
+ *
157
+ * @returns A Temporal workflow function that executes this behavior tree
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * import { BehaviorTree } from '@wayfarer-ai/btree';
162
+ * import { Sequence } from '@wayfarer-ai/btree';
163
+ * import { PrintAction } from '@wayfarer-ai/btree';
164
+ *
165
+ * const root = new Sequence({ id: 'root' });
166
+ * root.addChild(new PrintAction({ id: 'step1', message: 'Hello' }));
167
+ * root.addChild(new PrintAction({ id: 'step2', message: 'World' }));
168
+ *
169
+ * const tree = new BehaviorTree(root);
170
+ * const workflow = tree.toWorkflow();
171
+ *
172
+ * // Register with Temporal worker
173
+ * // Worker.create({ workflows: { myWorkflow: workflow }, ... })
174
+ * ```
175
+ */
176
+ toWorkflow(): (args: WorkflowArgs) => Promise<WorkflowResult> {
177
+ const root = this.root;
178
+
179
+ return async function behaviorTreeWorkflow(
180
+ args: WorkflowArgs,
181
+ ): Promise<WorkflowResult> {
182
+ // Create TemporalContext
183
+ const context: TemporalContext = {
184
+ blackboard: new ScopedBlackboard(),
185
+ treeRegistry: args.treeRegistry,
186
+ timestamp: Date.now(),
187
+ sessionId: args.sessionId || `session-${Date.now()}`,
188
+ // Store input immutably for ${input.key} resolution
189
+ input: args.input ? Object.freeze({ ...args.input }) : undefined,
190
+ // Pass activities for I/O operations (deterministic Temporal execution)
191
+ activities: args.activities,
192
+ // Pass tokenProvider for IntegrationAction authentication
193
+ tokenProvider: args.tokenProvider,
194
+ };
195
+
196
+ // Also copy input to blackboard for backward compatibility
197
+ // This allows ${key} or ${bb.key} to access input values
198
+ if (args.input) {
199
+ for (const [key, value] of Object.entries(args.input)) {
200
+ context.blackboard.set(key, value);
201
+ }
202
+ }
203
+
204
+ // Execute tree
205
+ const status = await root.tick(context);
206
+
207
+ // Return result with output
208
+ return {
209
+ status,
210
+ output: context.blackboard.toJSON(),
211
+ };
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Replace a node at the given path with a new node
217
+ * Updates parent-child relationships and rebuilds the index
218
+ */
219
+ replaceNodeAtPath(path: string, newNode: TreeNode): void {
220
+ const oldNode = this.findNodeByPath(path);
221
+ if (!oldNode) {
222
+ throw new Error(`Node not found at path: ${path}`);
223
+ }
224
+
225
+ if (path === "/") {
226
+ // Special case: replacing root
227
+ this.root = newNode;
228
+ newNode.parent = undefined;
229
+ } else {
230
+ // Find parent and child index from path
231
+ const pathParts = path.split("/").filter((p) => p);
232
+ const lastPart = pathParts[pathParts.length - 1];
233
+
234
+ if (!lastPart) {
235
+ throw new Error(`Invalid path format: ${path}`);
236
+ }
237
+
238
+ const childIndex = parseInt(lastPart, 10);
239
+
240
+ const parent = oldNode.parent;
241
+ if (!parent || !parent.children) {
242
+ throw new Error(`Cannot replace node: invalid parent at path ${path}`);
243
+ }
244
+
245
+ // Update parent's children array
246
+ parent.children[childIndex] = newNode;
247
+ newNode.parent = parent;
248
+ }
249
+
250
+ // Rebuild index (paths may have changed if newNode has different children)
251
+ this.buildNodeIndex();
252
+ }
253
+
254
+ /**
255
+ * Build the node index with path-based and ID-based lookups
256
+ */
257
+ private buildNodeIndex(): void {
258
+ this.pathIndex.clear();
259
+ this.idIndex.clear();
260
+
261
+ const indexNode = (node: TreeNode, path: string) => {
262
+ // Index by path (always works)
263
+ this.pathIndex.set(path, node);
264
+
265
+ // Also index by ID if present
266
+ if (node.id) {
267
+ this.idIndex.set(node.id, node);
268
+ }
269
+
270
+ // Recursively index children
271
+ if (node.children) {
272
+ node.children.forEach((child, index) => {
273
+ // Build child path: / becomes /0, /0 becomes /0/1, etc.
274
+ const childPath = path === "/" ? `/${index}` : `${path}/${index}`;
275
+ indexNode(child, childPath);
276
+ });
277
+ }
278
+ };
279
+
280
+ // Start indexing from root with path '/'
281
+ indexNode(this.root, "/");
282
+ }
283
+ }
@@ -0,0 +1,222 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { ScopedBlackboard } from "./blackboard.js";
3
+
4
+ describe("ScopedBlackboard", () => {
5
+ let blackboard: ScopedBlackboard;
6
+
7
+ beforeEach(() => {
8
+ blackboard = new ScopedBlackboard();
9
+ });
10
+
11
+ describe("Basic operations", () => {
12
+ it("should store and retrieve values", () => {
13
+ blackboard.set("key1", "value1");
14
+ blackboard.set("key2", 42);
15
+
16
+ expect(blackboard.get("key1")).toBe("value1");
17
+ expect(blackboard.get("key2")).toBe(42);
18
+ });
19
+
20
+ it("should return undefined for non-existent keys", () => {
21
+ expect(blackboard.get("nonexistent")).toBeUndefined();
22
+ });
23
+
24
+ it("should check if key exists", () => {
25
+ blackboard.set("exists", true);
26
+
27
+ expect(blackboard.has("exists")).toBe(true);
28
+ expect(blackboard.has("notexists")).toBe(false);
29
+ });
30
+
31
+ it("should delete keys", () => {
32
+ blackboard.set("toDelete", "value");
33
+ expect(blackboard.has("toDelete")).toBe(true);
34
+
35
+ blackboard.delete("toDelete");
36
+ expect(blackboard.has("toDelete")).toBe(false);
37
+ });
38
+
39
+ it("should clear all entries", () => {
40
+ blackboard.set("key1", "value1");
41
+ blackboard.set("key2", "value2");
42
+
43
+ blackboard.clear();
44
+
45
+ expect(blackboard.has("key1")).toBe(false);
46
+ expect(blackboard.has("key2")).toBe(false);
47
+ expect(blackboard.keys()).toHaveLength(0);
48
+ });
49
+ });
50
+
51
+ describe("Scoped inheritance", () => {
52
+ it("should create child scopes", () => {
53
+ const child = blackboard.createScope("child");
54
+
55
+ expect(child.getScopeName()).toBe("child");
56
+ expect(child.getParentScope()).toBe(blackboard);
57
+ });
58
+
59
+ it("should inherit values from parent scope", () => {
60
+ blackboard.set("parentValue", "inherited");
61
+
62
+ const child = blackboard.createScope("child");
63
+ expect(child.get("parentValue")).toBe("inherited");
64
+ expect(child.has("parentValue")).toBe(true);
65
+ });
66
+
67
+ it("should override parent values in child scope", () => {
68
+ blackboard.set("value", "parent");
69
+
70
+ const child = blackboard.createScope("child");
71
+ child.set("value", "child");
72
+
73
+ expect(blackboard.get("value")).toBe("parent");
74
+ expect(child.get("value")).toBe("child");
75
+ });
76
+
77
+ it("should not affect parent when deleting in child", () => {
78
+ blackboard.set("value", "parent");
79
+
80
+ const child = blackboard.createScope("child");
81
+ child.delete("value");
82
+
83
+ expect(blackboard.get("value")).toBe("parent");
84
+ expect(child.get("value")).toBe("parent"); // Still inherited
85
+ });
86
+
87
+ it("should support multi-level inheritance", () => {
88
+ blackboard.set("level0", "root");
89
+
90
+ const child1 = blackboard.createScope("child1");
91
+ child1.set("level1", "child1");
92
+
93
+ const child2 = child1.createScope("child2");
94
+ child2.set("level2", "child2");
95
+
96
+ expect(child2.get("level0")).toBe("root");
97
+ expect(child2.get("level1")).toBe("child1");
98
+ expect(child2.get("level2")).toBe("child2");
99
+
100
+ expect(child2.getFullScopePath()).toBe("root.child1.child2");
101
+ });
102
+
103
+ it("should reuse existing child scopes", () => {
104
+ const child1 = blackboard.createScope("child");
105
+ child1.set("value", "test");
106
+
107
+ const child2 = blackboard.createScope("child");
108
+ expect(child1).toBe(child2);
109
+ expect(child2.get("value")).toBe("test");
110
+ });
111
+ });
112
+
113
+ describe("Port operations", () => {
114
+ it("should get port with default value", () => {
115
+ expect(blackboard.getPort("missing", "default")).toBe("default");
116
+
117
+ blackboard.set("exists", "value");
118
+ expect(blackboard.getPort("exists", "default")).toBe("value");
119
+ });
120
+
121
+ it("should set port value", () => {
122
+ blackboard.setPort("port", 123);
123
+ expect(blackboard.get("port")).toBe(123);
124
+ });
125
+
126
+ it("should handle typed port operations", () => {
127
+ interface Config {
128
+ timeout: number;
129
+ retries: number;
130
+ }
131
+
132
+ const config: Config = { timeout: 1000, retries: 3 };
133
+ blackboard.setPort<Config>("config", config);
134
+
135
+ const retrieved = blackboard.getPort<Config>("config");
136
+ expect(retrieved).toEqual(config);
137
+ });
138
+ });
139
+
140
+ describe("Utility methods", () => {
141
+ it("should return all keys including inherited ones", () => {
142
+ blackboard.set("parent1", "value1");
143
+ blackboard.set("parent2", "value2");
144
+
145
+ const child = blackboard.createScope("child");
146
+ child.set("child1", "value3");
147
+ child.set("parent1", "overridden"); // Override parent key
148
+
149
+ const keys = child.keys();
150
+ expect(keys).toContain("parent1");
151
+ expect(keys).toContain("parent2");
152
+ expect(keys).toContain("child1");
153
+ expect(keys).toHaveLength(3);
154
+ });
155
+
156
+ it("should return entries with local values overriding parent", () => {
157
+ blackboard.set("shared", "parent");
158
+ blackboard.set("parentOnly", "value");
159
+
160
+ const child = blackboard.createScope("child");
161
+ child.set("shared", "child");
162
+ child.set("childOnly", "value");
163
+
164
+ const entries = child.entries();
165
+ const entriesMap = new Map(entries);
166
+
167
+ expect(entriesMap.get("shared")).toBe("child");
168
+ expect(entriesMap.get("parentOnly")).toBe("value");
169
+ expect(entriesMap.get("childOnly")).toBe("value");
170
+ expect(entries).toHaveLength(3);
171
+ });
172
+
173
+ it("should convert to JSON with only local entries", () => {
174
+ blackboard.set("parent", "value");
175
+
176
+ const child = blackboard.createScope("child");
177
+ child.set("child1", "value1");
178
+ child.set("child2", "value2");
179
+
180
+ const json = child.toJSON();
181
+ expect(json).toEqual({
182
+ child1: "value1",
183
+ child2: "value2",
184
+ });
185
+ expect(json).not.toHaveProperty("parent");
186
+ });
187
+ });
188
+
189
+ describe("Debug utilities", () => {
190
+ it("should not throw when calling debug", () => {
191
+ blackboard.set("key", "value");
192
+ const child = blackboard.createScope("child");
193
+ child.set("childKey", "childValue");
194
+
195
+ // Should not throw
196
+ expect(() => child.debug()).not.toThrow();
197
+ });
198
+ });
199
+
200
+ describe("Snapshots", () => {
201
+ it("should create independent snapshots with clone", () => {
202
+ blackboard.set("x", 1);
203
+ const snapshot = blackboard.clone();
204
+ blackboard.set("x", 2);
205
+
206
+ expect(snapshot.get("x")).toBe(1);
207
+ expect(blackboard.get("x")).toBe(2);
208
+ });
209
+
210
+ it("should deep clone child scopes", () => {
211
+ blackboard.set("parent", "value");
212
+ const child = blackboard.createScope("child");
213
+ child.set("child", "value");
214
+
215
+ const snapshot = blackboard.clone();
216
+ blackboard.set("parent", "changed");
217
+
218
+ expect(snapshot.get("parent")).toBe("value");
219
+ expect(blackboard.get("parent")).toBe("changed");
220
+ });
221
+ });
222
+ });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Scoped Blackboard implementation with inheritance
3
+ * Inspired by BehaviorTree.CPP's blackboard
4
+ *
5
+ * Simple mutable API with native deep cloning for snapshots
6
+ */
7
+
8
+ import type { IScopedBlackboard } from "./types.js";
9
+
10
+ /**
11
+ * Implementation of a hierarchical blackboard with scoped inheritance
12
+ * Uses simple mutable operations with snapshot support via structuredClone
13
+ */
14
+ export class ScopedBlackboard implements IScopedBlackboard {
15
+ private data: Record<string, unknown> = {};
16
+ private parent: ScopedBlackboard | null = null;
17
+ private scopeName: string;
18
+ private childScopes: Map<string, ScopedBlackboard> = new Map();
19
+
20
+ constructor(
21
+ scopeName: string = "root",
22
+ parent: ScopedBlackboard | null = null,
23
+ ) {
24
+ this.scopeName = scopeName;
25
+ this.parent = parent;
26
+ }
27
+
28
+ get(key: string): unknown {
29
+ // First check local scope
30
+ if (key in this.data) {
31
+ return this.data[key];
32
+ }
33
+
34
+ // Then check parent scopes
35
+ if (this.parent) {
36
+ return this.parent.get(key);
37
+ }
38
+
39
+ return undefined;
40
+ }
41
+
42
+ set(key: string, value: unknown): void {
43
+ this.data[key] = value;
44
+ }
45
+
46
+ has(key: string): boolean {
47
+ // Check local scope
48
+ if (key in this.data) {
49
+ return true;
50
+ }
51
+
52
+ // Check parent scopes
53
+ if (this.parent) {
54
+ return this.parent.has(key);
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ delete(key: string): void {
61
+ // Only delete from local scope
62
+ delete this.data[key];
63
+ }
64
+
65
+ clear(): void {
66
+ this.data = {};
67
+ this.childScopes.clear();
68
+ }
69
+
70
+ createScope(name: string): IScopedBlackboard {
71
+ // Check if scope already exists
72
+ if (this.childScopes.has(name)) {
73
+ const scope = this.childScopes.get(name);
74
+ if (!scope) {
75
+ throw new Error(`Scope ${name} not found`);
76
+ }
77
+ return scope;
78
+ }
79
+
80
+ // Create new child scope
81
+ const childScope = new ScopedBlackboard(name, this);
82
+ this.childScopes.set(name, childScope);
83
+ return childScope;
84
+ }
85
+
86
+ getParentScope(): IScopedBlackboard | null {
87
+ return this.parent;
88
+ }
89
+
90
+ getScopeName(): string {
91
+ return this.scopeName;
92
+ }
93
+
94
+ getPort<T>(key: string, defaultValue?: T): T {
95
+ const value = this.get(key);
96
+ if (value === undefined && defaultValue !== undefined) {
97
+ return defaultValue;
98
+ }
99
+ return value as T;
100
+ }
101
+
102
+ setPort<T>(key: string, value: T): void {
103
+ this.set(key, value);
104
+ }
105
+
106
+ keys(): string[] {
107
+ const localKeys = Object.keys(this.data);
108
+ const parentKeys = this.parent ? this.parent.keys() : [];
109
+
110
+ // Combine keys, removing duplicates (local keys override parent keys)
111
+ const allKeys = new Set([...localKeys, ...parentKeys]);
112
+ return Array.from(allKeys);
113
+ }
114
+
115
+ entries(): [string, unknown][] {
116
+ const result: [string, unknown][] = [];
117
+ const processedKeys = new Set<string>();
118
+
119
+ // Add local entries first
120
+ for (const [key, value] of Object.entries(this.data)) {
121
+ result.push([key, value]);
122
+ processedKeys.add(key);
123
+ }
124
+
125
+ // Add parent entries that aren't overridden
126
+ if (this.parent) {
127
+ for (const [key, value] of this.parent.entries()) {
128
+ if (!processedKeys.has(key)) {
129
+ result.push([key, value]);
130
+ }
131
+ }
132
+ }
133
+
134
+ return result;
135
+ }
136
+
137
+ toJSON(): Record<string, unknown> {
138
+ // Only return local entries, not inherited ones
139
+ return { ...this.data };
140
+ }
141
+
142
+ /**
143
+ * Create a deep clone of this blackboard for snapshots
144
+ * Uses structured cloning for deep copy
145
+ */
146
+ clone(): IScopedBlackboard {
147
+ // Create a new blackboard instance with the same scope configuration
148
+ const cloned = new ScopedBlackboard(this.scopeName, this.parent);
149
+
150
+ // Deep clone the data object using structuredClone (Node 17+)
151
+ // This creates a true deep copy without freezing the original
152
+ cloned.data = structuredClone(this.data);
153
+
154
+ // Recursively clone child scopes
155
+ this.childScopes.forEach((childScope, name) => {
156
+ cloned.childScopes.set(name, childScope.clone() as ScopedBlackboard);
157
+ });
158
+
159
+ return cloned;
160
+ }
161
+
162
+ /**
163
+ * Get the full scope path (e.g., "root.child.grandchild")
164
+ */
165
+ getFullScopePath(): string {
166
+ const path: string[] = [this.scopeName];
167
+ let current = this.parent;
168
+
169
+ while (current) {
170
+ path.unshift(current.scopeName);
171
+ current = current.parent;
172
+ }
173
+
174
+ return path.join(".");
175
+ }
176
+
177
+ /**
178
+ * Debug utility to print the blackboard hierarchy
179
+ */
180
+ debug(indent: number = 0): void {
181
+ const prefix = " ".repeat(indent);
182
+ console.log(`${prefix}Scope: ${this.scopeName}`);
183
+
184
+ for (const [key, value] of Object.entries(this.data)) {
185
+ console.log(`${prefix} ${key}: ${JSON.stringify(value)}`);
186
+ }
187
+
188
+ for (const [_name, childScope] of this.childScopes) {
189
+ childScope.debug(indent + 1);
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Conditional composite configuration schema
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { nodeConfigurationSchema } from "../schemas/base.schema.js";
7
+
8
+ /**
9
+ * Schema for Conditional composite configuration
10
+ * Uses base schema only - condition logic is in child nodes
11
+ */
12
+ export const conditionalConfigurationSchema = nodeConfigurationSchema;
13
+
14
+ /**
15
+ * Validated Conditional configuration type
16
+ */
17
+ export type ValidatedConditionalConfiguration = z.infer<
18
+ typeof conditionalConfigurationSchema
19
+ >;