@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.
- package/.claude/settings.local.json +31 -0
- package/CLAUDE.md +181 -0
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/behaviour-tree-workflows-landing/index.html +16 -0
- package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
- package/behaviour-tree-workflows-landing/package.json +31 -0
- package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
- package/behaviour-tree-workflows-landing/src/App.css +103 -0
- package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
- package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
- package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
- package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
- package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
- package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
- package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
- package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
- package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
- package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
- package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
- package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
- package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
- package/demo-google-sheets.ts +181 -0
- package/demo-runtime-variables.ts +174 -0
- package/demo-template.ts +208 -0
- package/docs/ARCHITECTURE_SUMMARY.md +613 -0
- package/docs/NODE_REFERENCE.md +504 -0
- package/docs/README.md +53 -0
- package/docs/custom-nodes-architecture.md +826 -0
- package/docs/observability.md +175 -0
- package/docs/yaml-specification.md +990 -0
- package/examples/temporal/README.md +117 -0
- package/examples/temporal/activities.ts +373 -0
- package/examples/temporal/client.ts +115 -0
- package/examples/temporal/python-worker/activities.py +339 -0
- package/examples/temporal/python-worker/requirements.txt +12 -0
- package/examples/temporal/python-worker/worker.py +106 -0
- package/examples/temporal/worker.ts +66 -0
- package/examples/temporal/workflows.ts +6 -0
- package/examples/temporal/yaml-workflow-loader.ts +105 -0
- package/examples/yaml-test.ts +97 -0
- package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
- package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
- package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
- package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
- package/examples/yaml-workflows/05-order-processing.yaml +146 -0
- package/examples/yaml-workflows/06-activity-test.yaml +71 -0
- package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
- package/examples/yaml-workflows/08-file-processing.yaml +141 -0
- package/examples/yaml-workflows/09-http-request.yaml +137 -0
- package/examples/yaml-workflows/README.md +211 -0
- package/package.json +38 -0
- package/src/actions/code-execution.schema.ts +27 -0
- package/src/actions/code-execution.ts +218 -0
- package/src/actions/generate-file.test.ts +516 -0
- package/src/actions/generate-file.ts +166 -0
- package/src/actions/http-request.test.ts +784 -0
- package/src/actions/http-request.ts +228 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/parse-file.test.ts +448 -0
- package/src/actions/parse-file.ts +139 -0
- package/src/actions/python-script.test.ts +439 -0
- package/src/actions/python-script.ts +154 -0
- package/src/base-node.test.ts +511 -0
- package/src/base-node.ts +605 -0
- package/src/behavior-tree.test.ts +431 -0
- package/src/behavior-tree.ts +283 -0
- package/src/blackboard.test.ts +222 -0
- package/src/blackboard.ts +192 -0
- package/src/composites/conditional.schema.ts +19 -0
- package/src/composites/conditional.test.ts +309 -0
- package/src/composites/conditional.ts +129 -0
- package/src/composites/for-each.schema.ts +23 -0
- package/src/composites/for-each.test.ts +254 -0
- package/src/composites/for-each.ts +132 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/memory-sequence.schema.ts +19 -0
- package/src/composites/memory-sequence.test.ts +223 -0
- package/src/composites/memory-sequence.ts +98 -0
- package/src/composites/parallel.schema.ts +28 -0
- package/src/composites/parallel.test.ts +502 -0
- package/src/composites/parallel.ts +157 -0
- package/src/composites/reactive-sequence.schema.ts +19 -0
- package/src/composites/reactive-sequence.test.ts +170 -0
- package/src/composites/reactive-sequence.ts +85 -0
- package/src/composites/recovery.schema.ts +19 -0
- package/src/composites/recovery.test.ts +366 -0
- package/src/composites/recovery.ts +90 -0
- package/src/composites/selector.schema.ts +19 -0
- package/src/composites/selector.test.ts +387 -0
- package/src/composites/selector.ts +85 -0
- package/src/composites/sequence.schema.ts +19 -0
- package/src/composites/sequence.test.ts +337 -0
- package/src/composites/sequence.ts +72 -0
- package/src/composites/sub-tree.schema.ts +21 -0
- package/src/composites/sub-tree.test.ts +893 -0
- package/src/composites/sub-tree.ts +177 -0
- package/src/composites/while.schema.ts +24 -0
- package/src/composites/while.test.ts +381 -0
- package/src/composites/while.ts +149 -0
- package/src/data-store/index.ts +10 -0
- package/src/data-store/memory-store.ts +161 -0
- package/src/data-store/types.ts +94 -0
- package/src/debug/breakpoint.test.ts +47 -0
- package/src/debug/breakpoint.ts +30 -0
- package/src/debug/index.ts +17 -0
- package/src/debug/resume-point.test.ts +49 -0
- package/src/debug/resume-point.ts +29 -0
- package/src/decorators/delay.schema.ts +21 -0
- package/src/decorators/delay.test.ts +261 -0
- package/src/decorators/delay.ts +140 -0
- package/src/decorators/force-result.schema.ts +32 -0
- package/src/decorators/force-result.test.ts +133 -0
- package/src/decorators/force-result.ts +63 -0
- package/src/decorators/index.ts +13 -0
- package/src/decorators/invert.schema.ts +19 -0
- package/src/decorators/invert.test.ts +135 -0
- package/src/decorators/invert.ts +42 -0
- package/src/decorators/keep-running.schema.ts +20 -0
- package/src/decorators/keep-running.test.ts +105 -0
- package/src/decorators/keep-running.ts +49 -0
- package/src/decorators/precondition.schema.ts +19 -0
- package/src/decorators/precondition.test.ts +351 -0
- package/src/decorators/precondition.ts +139 -0
- package/src/decorators/repeat.schema.ts +21 -0
- package/src/decorators/repeat.test.ts +187 -0
- package/src/decorators/repeat.ts +94 -0
- package/src/decorators/run-once.schema.ts +19 -0
- package/src/decorators/run-once.test.ts +140 -0
- package/src/decorators/run-once.ts +61 -0
- package/src/decorators/soft-assert.schema.ts +19 -0
- package/src/decorators/soft-assert.test.ts +107 -0
- package/src/decorators/soft-assert.ts +68 -0
- package/src/decorators/timeout.schema.ts +21 -0
- package/src/decorators/timeout.test.ts +274 -0
- package/src/decorators/timeout.ts +159 -0
- package/src/errors.test.ts +63 -0
- package/src/errors.ts +34 -0
- package/src/events.test.ts +347 -0
- package/src/events.ts +183 -0
- package/src/index.ts +80 -0
- package/src/integrations/index.ts +30 -0
- package/src/integrations/integration-action.test.ts +571 -0
- package/src/integrations/integration-action.ts +233 -0
- package/src/integrations/piece-executor.ts +320 -0
- package/src/observability/execution-tracker.ts +320 -0
- package/src/observability/index.ts +23 -0
- package/src/observability/sinks.ts +138 -0
- package/src/observability/types.ts +130 -0
- package/src/registry-utils.ts +147 -0
- package/src/registry.test.ts +466 -0
- package/src/registry.ts +334 -0
- package/src/schemas/base.schema.ts +104 -0
- package/src/schemas/index.ts +223 -0
- package/src/schemas/integration.test.ts +238 -0
- package/src/schemas/tree-definition.schema.ts +170 -0
- package/src/schemas/validation.test.ts +146 -0
- package/src/schemas/validation.ts +122 -0
- package/src/scripting/index.ts +22 -0
- package/src/templates/template-loader.test.ts +281 -0
- package/src/templates/template-loader.ts +152 -0
- package/src/temporal-integration.test.ts +213 -0
- package/src/test-nodes.ts +259 -0
- package/src/types.ts +503 -0
- package/src/utilities/index.ts +17 -0
- package/src/utilities/log-message.test.ts +275 -0
- package/src/utilities/log-message.ts +134 -0
- package/src/utilities/regex-extract.test.ts +138 -0
- package/src/utilities/regex-extract.ts +108 -0
- package/src/utilities/variable-resolver.test.ts +416 -0
- package/src/utilities/variable-resolver.ts +318 -0
- package/src/utils/error-handler.test.ts +117 -0
- package/src/utils/error-handler.ts +48 -0
- package/src/utils/signal-check.test.ts +234 -0
- package/src/utils/signal-check.ts +140 -0
- package/src/yaml/errors.ts +143 -0
- package/src/yaml/index.ts +30 -0
- package/src/yaml/loader.ts +39 -0
- package/src/yaml/parser.ts +286 -0
- package/src/yaml/validation/semantic-validator.ts +196 -0
- package/templates/google-sheets/insert-row.yaml +76 -0
- package/templates/notification-sender.yaml +33 -0
- package/templates/order-validation.yaml +44 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +25 -0
- package/workflows/order-processor.yaml +59 -0
- 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
|
+
}
|