@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,234 @@
1
+ /**
2
+ * Tests for signal checking utilities
3
+ * These utilities provide cancellation support across behavior tree nodes
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import {
8
+ checkSignal,
9
+ createAbortPromise,
10
+ OperationCancelledError,
11
+ } from "./signal-check.js";
12
+
13
+ describe("signal-check utilities", () => {
14
+ describe("checkSignal", () => {
15
+ it("should fail with OperationCancelledError when signal is aborted", () => {
16
+ const controller = new AbortController();
17
+ controller.abort();
18
+
19
+ try {
20
+ checkSignal(controller.signal);
21
+ expect.fail("Should have thrown an error");
22
+ } catch (error) {
23
+ expect(error).toBeInstanceOf(OperationCancelledError);
24
+ expect((error as OperationCancelledError).message).toBe("Operation was cancelled");
25
+ }
26
+ });
27
+
28
+ it("should succeed when signal is undefined", () => {
29
+ expect(() => checkSignal(undefined)).not.toThrow();
30
+ });
31
+
32
+ it("should succeed when signal is provided but not aborted", () => {
33
+ const controller = new AbortController();
34
+ expect(() => checkSignal(controller.signal)).not.toThrow();
35
+ });
36
+
37
+ it("should include custom message in error when provided", () => {
38
+ const controller = new AbortController();
39
+ controller.abort();
40
+
41
+ try {
42
+ checkSignal(controller.signal, "Custom operation");
43
+ expect.fail("Should have thrown an error");
44
+ } catch (error) {
45
+ expect((error as OperationCancelledError).message).toBe("Custom operation");
46
+ }
47
+ });
48
+ });
49
+
50
+ describe("createAbortPromise", () => {
51
+ it("should reject with OperationCancelledError when signal is aborted", async () => {
52
+ const controller = new AbortController();
53
+ const promise = createAbortPromise(controller.signal);
54
+
55
+ // Abort after a short delay
56
+ setTimeout(() => controller.abort(), 10);
57
+
58
+ await expect(promise).rejects.toThrow(OperationCancelledError);
59
+ await expect(promise).rejects.toThrow("Operation was cancelled");
60
+ });
61
+
62
+ it("should reject immediately if signal is already aborted", async () => {
63
+ const controller = new AbortController();
64
+ controller.abort();
65
+
66
+ const promise = createAbortPromise(controller.signal);
67
+
68
+ await expect(promise).rejects.toThrow(OperationCancelledError);
69
+ });
70
+
71
+ it("should never resolve if signal is never aborted (race with timeout)", async () => {
72
+ const controller = new AbortController();
73
+ const abortPromise = createAbortPromise(controller.signal);
74
+
75
+ // Race with a timeout - abort promise should not resolve
76
+ const timeoutPromise = new Promise((resolve) =>
77
+ setTimeout(() => resolve("timeout"), 50),
78
+ );
79
+
80
+ const result = await Promise.race([abortPromise, timeoutPromise]);
81
+ expect(result).toBe("timeout");
82
+ });
83
+
84
+ it("should not reject when signal is undefined (race with timeout)", async () => {
85
+ const abortPromise = createAbortPromise(undefined);
86
+ const timeoutPromise = new Promise((resolve) =>
87
+ setTimeout(() => resolve("timeout"), 50),
88
+ );
89
+
90
+ const result = await Promise.race([abortPromise, timeoutPromise]);
91
+ expect(result).toBe("timeout");
92
+ });
93
+
94
+ it("should include custom message in error when provided", async () => {
95
+ const controller = new AbortController();
96
+ controller.abort();
97
+
98
+ const promise = createAbortPromise(controller.signal, "Async operation");
99
+
100
+ await expect(promise).rejects.toThrow("Async operation");
101
+ });
102
+
103
+ it("should clean up event listener when signal is aborted", async () => {
104
+ const controller = new AbortController();
105
+ const promise = createAbortPromise(controller.signal);
106
+
107
+ // Abort the signal
108
+ controller.abort();
109
+
110
+ // Wait for rejection
111
+ await expect(promise).rejects.toThrow(OperationCancelledError);
112
+
113
+ // Verify event listener was removed (no way to directly test, but we can check it doesn't throw)
114
+ expect(() => controller.abort()).not.toThrow();
115
+ });
116
+ });
117
+
118
+ describe("OperationCancelledError", () => {
119
+ it("should be an instance of Error", () => {
120
+ const error = new OperationCancelledError();
121
+ expect(error).toBeInstanceOf(Error);
122
+ });
123
+
124
+ it("should have correct name", () => {
125
+ const error = new OperationCancelledError();
126
+ expect(error.name).toBe("OperationCancelledError");
127
+ });
128
+
129
+ it("should support custom message", () => {
130
+ const error = new OperationCancelledError("Custom message");
131
+ expect(error.message).toBe("Custom message");
132
+ });
133
+
134
+ it("should have default message", () => {
135
+ const error = new OperationCancelledError();
136
+ expect(error.message).toBe("Operation was cancelled");
137
+ });
138
+ });
139
+
140
+ describe("integration scenarios", () => {
141
+ it("should support using checkSignal in loops", async () => {
142
+ const controller = new AbortController();
143
+ let iterations = 0;
144
+
145
+ const performWork = async () => {
146
+ for (let i = 0; i < 1000; i++) {
147
+ await checkSignal(controller.signal);
148
+ iterations++;
149
+
150
+ if (i === 5) {
151
+ controller.abort();
152
+ }
153
+ }
154
+ };
155
+
156
+ try {
157
+ await performWork();
158
+ expect.fail("Should have thrown an error");
159
+ } catch (error) {
160
+ expect(error).toBeInstanceOf(OperationCancelledError);
161
+ expect(iterations).toBe(6); // 0, 1, 2, 3, 4, 5, then abort
162
+ }
163
+ });
164
+
165
+ it("should support racing abort promise with actual work", async () => {
166
+ const controller = new AbortController();
167
+
168
+ const work = new Promise<string>((resolve) => {
169
+ setTimeout(() => resolve("work completed"), 100);
170
+ });
171
+
172
+ const abort = createAbortPromise(controller.signal);
173
+
174
+ // Abort after 20ms
175
+ setTimeout(() => controller.abort(), 20);
176
+
177
+ // Abort should win the race
178
+ await expect(Promise.race([work, abort])).rejects.toThrow(
179
+ OperationCancelledError,
180
+ );
181
+ });
182
+
183
+ it("should support checking signal before async operations", async () => {
184
+ const controller = new AbortController();
185
+ controller.abort();
186
+
187
+ const performAsyncWork = async () => {
188
+ // Check signal before starting work
189
+ await checkSignal(controller.signal);
190
+
191
+ // This should never execute
192
+ await new Promise((resolve) => setTimeout(resolve, 100));
193
+ return "completed";
194
+ };
195
+
196
+ try {
197
+ await performAsyncWork();
198
+ expect.fail("Should have thrown an error");
199
+ } catch (error) {
200
+ expect(error).toBeInstanceOf(OperationCancelledError);
201
+ }
202
+ });
203
+
204
+ it("should support checking signal multiple times during execution", async () => {
205
+ const controller = new AbortController();
206
+ let checkpointReached = 0;
207
+
208
+ const performWork = async () => {
209
+ await checkSignal(controller.signal);
210
+ checkpointReached = 1;
211
+ await new Promise((resolve) => setTimeout(resolve, 10));
212
+
213
+ await checkSignal(controller.signal);
214
+ checkpointReached = 2;
215
+ await new Promise((resolve) => setTimeout(resolve, 10));
216
+
217
+ await checkSignal(controller.signal);
218
+ checkpointReached = 3;
219
+ };
220
+
221
+ // Start work and abort after checkpoint 1 but before checkpoint 2
222
+ const workPromise = performWork();
223
+ setTimeout(() => controller.abort(), 5);
224
+
225
+ try {
226
+ await workPromise;
227
+ expect.fail("Should have thrown an error");
228
+ } catch (error) {
229
+ expect(error).toBeInstanceOf(OperationCancelledError);
230
+ expect(checkpointReached).toBe(1);
231
+ }
232
+ });
233
+ });
234
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Signal checking utilities for cancellation support
3
+ *
4
+ * These utilities provide a consistent way to check for cancellation signals
5
+ * across all behavior tree nodes. They work with AbortController/AbortSignal
6
+ * to enable interruption of long-running operations.
7
+ *
8
+ * Usage:
9
+ * 1. checkSignal() - Use in loops and before operations to check if cancelled
10
+ * 2. createAbortPromise() - Use with Promise.race() to cancel async operations
11
+ *
12
+ * @module signal-check
13
+ */
14
+
15
+ /**
16
+ * Error thrown when an operation is cancelled via AbortSignal
17
+ */
18
+ export class OperationCancelledError extends Error {
19
+ constructor(message: string = "Operation was cancelled") {
20
+ super(message);
21
+ this.name = "OperationCancelledError";
22
+
23
+ // Maintains proper stack trace in V8 environments (Chrome, Node.js)
24
+ if (Error.captureStackTrace) {
25
+ Error.captureStackTrace(this, OperationCancelledError);
26
+ }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Synchronously check if an abort signal has been triggered
32
+ *
33
+ * This is the primary mechanism for cooperative cancellation in behavior tree nodes.
34
+ * Call this function:
35
+ * - At the start of node execution
36
+ * - Before ticking each child in a composite
37
+ * - Inside loops during long-running operations
38
+ * - Before starting expensive operations
39
+ *
40
+ * @param signal - Optional AbortSignal from TickContext
41
+ * @param message - Optional custom error message (defaults to "Operation was cancelled")
42
+ * @throws OperationCancelledError if signal is aborted
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // In a composite node
47
+ * async executeTick(context: TemporalContext): Promise<NodeStatus> {
48
+ * checkSignal(context.signal); // Throws if cancelled
49
+ * for (const child of this._children) {
50
+ * const status = await child.tick(context);
51
+ * // ...
52
+ * }
53
+ * }
54
+ * ```
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // In a decorator with loops
59
+ * async executeTick(context: TemporalContext): Promise<NodeStatus> {
60
+ * for (let i = 0; i < maxAttempts; i++) {
61
+ * checkSignal(context.signal, 'Retry operation');
62
+ * // ...
63
+ * }
64
+ * }
65
+ * ```
66
+ */
67
+ export function checkSignal(signal?: AbortSignal, message?: string): void {
68
+ if (signal?.aborted) {
69
+ throw new OperationCancelledError(message);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Create a promise that rejects when an abort signal is triggered
75
+ *
76
+ * This enables true async cancellation by racing with actual work.
77
+ * Use with Promise.race() to cancel Promises that don't natively support signals.
78
+ *
79
+ * The promise:
80
+ * - Rejects immediately if signal is already aborted
81
+ * - Rejects when signal fires 'abort' event
82
+ * - Never resolves (only rejects or remains pending)
83
+ * - Cleans up event listener when aborted
84
+ *
85
+ * @param signal - Optional AbortSignal from TickContext
86
+ * @param message - Optional custom error message (defaults to "Operation was cancelled")
87
+ * @returns Promise that rejects with OperationCancelledError when signal aborts
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * // Racing with a Promise that doesn't support signals
92
+ * protected async executeWithPlaywright(adapter: PlaywrightAdapter, context: TickContext) {
93
+ * const work = someAsyncOperation();
94
+ * const abort = createAbortPromise(context.signal);
95
+ *
96
+ * const result = await Promise.race([work, abort]);
97
+ * return result;
98
+ * }
99
+ * ```
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // In a node that performs multiple async steps
104
+ * protected async executeWithPlaywright(adapter: PlaywrightAdapter, context: TickContext) {
105
+ * const abort = createAbortPromise(context.signal);
106
+ *
107
+ * const step1 = adapter.page.waitForSelector('.loading', { state: 'hidden' });
108
+ * await Promise.race([step1, abort]);
109
+ *
110
+ * const step2 = adapter.page.click('.button');
111
+ * await Promise.race([step2, abort]);
112
+ *
113
+ * return NodeStatus.SUCCESS;
114
+ * }
115
+ * ```
116
+ */
117
+ export function createAbortPromise(
118
+ signal?: AbortSignal,
119
+ message?: string,
120
+ ): Promise<never> {
121
+ return new Promise((_, reject) => {
122
+ // If no signal provided, never reject (infinite pending)
123
+ if (!signal) {
124
+ return;
125
+ }
126
+
127
+ // If already aborted, reject immediately
128
+ if (signal.aborted) {
129
+ reject(new OperationCancelledError(message));
130
+ return;
131
+ }
132
+
133
+ // Listen for abort event
134
+ const onAbort = () => {
135
+ reject(new OperationCancelledError(message));
136
+ };
137
+
138
+ signal.addEventListener("abort", onAbort, { once: true });
139
+ });
140
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * YAML parser and validation error types
3
+ */
4
+
5
+ /**
6
+ * Base class for all YAML validation errors
7
+ */
8
+ export class ValidationError extends Error {
9
+ constructor(
10
+ message: string,
11
+ public path?: string,
12
+ public suggestion?: string,
13
+ ) {
14
+ super(message);
15
+ this.name = "ValidationError";
16
+ }
17
+
18
+ /**
19
+ * Format error message with path and suggestion
20
+ */
21
+ format(): string {
22
+ let formatted = this.message;
23
+
24
+ if (this.path) {
25
+ formatted = `${this.path}: ${formatted}`;
26
+ }
27
+
28
+ if (this.suggestion) {
29
+ formatted += `\nSuggestion: ${this.suggestion}`;
30
+ }
31
+
32
+ return formatted;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * YAML syntax error (Stage 1)
38
+ * Thrown when YAML is malformed
39
+ */
40
+ export class YamlSyntaxError extends ValidationError {
41
+ constructor(
42
+ message: string,
43
+ public line?: number,
44
+ public column?: number,
45
+ suggestion?: string,
46
+ ) {
47
+ super(message, undefined, suggestion);
48
+ this.name = "YamlSyntaxError";
49
+ }
50
+
51
+ format(): string {
52
+ let formatted = this.message;
53
+
54
+ if (this.line !== undefined) {
55
+ formatted = `Line ${this.line}${this.column !== undefined ? `, Column ${this.column}` : ""}: ${formatted}`;
56
+ }
57
+
58
+ if (this.suggestion) {
59
+ formatted += `\nSuggestion: ${this.suggestion}`;
60
+ }
61
+
62
+ return formatted;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Tree structure validation error (Stage 2)
68
+ * Thrown when tree definition structure is invalid
69
+ */
70
+ export class StructureValidationError extends ValidationError {
71
+ constructor(message: string, path?: string, suggestion?: string) {
72
+ super(message, path, suggestion);
73
+ this.name = "StructureValidationError";
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Node configuration validation error (Stage 3)
79
+ * Thrown when node-specific configuration is invalid
80
+ */
81
+ export class ConfigValidationError extends ValidationError {
82
+ constructor(
83
+ message: string,
84
+ public nodeType: string,
85
+ path?: string,
86
+ suggestion?: string,
87
+ ) {
88
+ super(message, path, suggestion);
89
+ this.name = "ConfigValidationError";
90
+ }
91
+
92
+ format(): string {
93
+ let formatted = `Invalid configuration for node type '${this.nodeType}'`;
94
+
95
+ if (this.path) {
96
+ formatted += ` at ${this.path}`;
97
+ }
98
+
99
+ formatted += `:\n${this.message}`;
100
+
101
+ if (this.suggestion) {
102
+ formatted += `\nSuggestion: ${this.suggestion}`;
103
+ }
104
+
105
+ return formatted;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Semantic validation error (Stage 4)
111
+ * Thrown when semantic rules are violated (duplicate IDs, circular refs, etc.)
112
+ */
113
+ export class SemanticValidationError extends ValidationError {
114
+ constructor(message: string, path?: string, suggestion?: string) {
115
+ super(message, path, suggestion);
116
+ this.name = "SemanticValidationError";
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Collect multiple validation errors
122
+ */
123
+ export class ValidationErrors extends Error {
124
+ constructor(public errors: ValidationError[]) {
125
+ super(`Validation failed with ${errors.length} error(s)`);
126
+ this.name = "ValidationErrors";
127
+ }
128
+
129
+ /**
130
+ * Format all errors as a single message
131
+ */
132
+ format(): string {
133
+ const header = `YAML validation failed\n\nIssues found:`;
134
+ const issues = this.errors
135
+ .map((error, index) => {
136
+ const formatted = error.format();
137
+ return ` ${index + 1}. ${formatted.split("\n").join("\n ")}`;
138
+ })
139
+ .join("\n\n");
140
+
141
+ return `${header}\n${issues}`;
142
+ }
143
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * YAML parser and loader for behavior trees
3
+ * Provides 4-stage validation pipeline for loading workflows from YAML
4
+ */
5
+
6
+ // Core functions
7
+ export {
8
+ parseYaml,
9
+ loadTreeFromYaml,
10
+ validateYaml,
11
+ toYaml,
12
+ type LoadOptions,
13
+ type ValidationOptions,
14
+ type ValidationResult,
15
+ } from "./parser.js";
16
+
17
+ export { loadTreeFromFile } from "./loader.js";
18
+
19
+ // Error types
20
+ export {
21
+ ValidationError,
22
+ YamlSyntaxError,
23
+ StructureValidationError,
24
+ ConfigValidationError,
25
+ SemanticValidationError,
26
+ ValidationErrors,
27
+ } from "./errors.js";
28
+
29
+ // Validators
30
+ export { semanticValidator } from "./validation/semantic-validator.js";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * File system integration for YAML loading
3
+ */
4
+
5
+ import { readFile } from "fs/promises";
6
+ import type { Registry } from "../registry.js";
7
+ import type { TreeNode } from "../types.js";
8
+ import { loadTreeFromYaml, type LoadOptions } from "./parser.js";
9
+
10
+ /**
11
+ * Load and create tree from YAML file
12
+ *
13
+ * @param filePath - Path to YAML file
14
+ * @param registry - Registry with registered node types
15
+ * @param options - Loading options
16
+ * @returns Created tree node
17
+ * @throws ValidationError if validation fails
18
+ * @throws Error if file cannot be read
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const tree = await loadTreeFromFile('./workflows/checkout.yaml', registry);
23
+ * ```
24
+ */
25
+ export async function loadTreeFromFile(
26
+ filePath: string,
27
+ registry: Registry,
28
+ options: LoadOptions = {},
29
+ ): Promise<TreeNode> {
30
+ try {
31
+ const yamlContent = await readFile(filePath, "utf-8");
32
+ return loadTreeFromYaml(yamlContent, registry, options);
33
+ } catch (error) {
34
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
35
+ throw new Error(`File not found: ${filePath}`);
36
+ }
37
+ throw error;
38
+ }
39
+ }