@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,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Precondition decorator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
7
|
+
import { ConfigurationError } from "../errors.js";
|
|
8
|
+
import { FailureNode, SuccessNode } from "../test-nodes.js";
|
|
9
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
10
|
+
import { Precondition } from "./precondition.js";
|
|
11
|
+
|
|
12
|
+
describe("Precondition", () => {
|
|
13
|
+
let blackboard: ScopedBlackboard;
|
|
14
|
+
let context: TemporalContext;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
blackboard = new ScopedBlackboard("root");
|
|
18
|
+
context = {
|
|
19
|
+
blackboard,
|
|
20
|
+
timestamp: Date.now(),
|
|
21
|
+
deltaTime: 0,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should execute child when precondition succeeds", async () => {
|
|
26
|
+
const precond = new Precondition({ id: "precond1" });
|
|
27
|
+
|
|
28
|
+
let childExecuted = false;
|
|
29
|
+
class TrackedChild extends SuccessNode {
|
|
30
|
+
tick(context: TemporalContext) {
|
|
31
|
+
childExecuted = true;
|
|
32
|
+
return super.tick(context);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
precond.setChild(new TrackedChild({ id: "child" }));
|
|
37
|
+
precond.addPrecondition(new SuccessNode({ id: "condition" }));
|
|
38
|
+
|
|
39
|
+
const result = await precond.tick(context);
|
|
40
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
41
|
+
expect(childExecuted).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should fail if required precondition not met", async () => {
|
|
45
|
+
const precond = new Precondition({ id: "precond1" });
|
|
46
|
+
|
|
47
|
+
precond.setChild(new SuccessNode({ id: "child" }));
|
|
48
|
+
precond.addPrecondition(
|
|
49
|
+
new FailureNode({ id: "condition" }),
|
|
50
|
+
undefined,
|
|
51
|
+
true,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const result = await precond.tick(context);
|
|
55
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should run resolver on precondition failure", async () => {
|
|
59
|
+
const precond = new Precondition({ id: "precond1" });
|
|
60
|
+
|
|
61
|
+
let resolverExecuted = false;
|
|
62
|
+
class TrackedResolver extends SuccessNode {
|
|
63
|
+
tick(context: TemporalContext) {
|
|
64
|
+
const superTick = super.tick.bind(this);
|
|
65
|
+
return (async () => {
|
|
66
|
+
resolverExecuted = true;
|
|
67
|
+
// Fix the condition
|
|
68
|
+
context.blackboard.set("conditionMet", true);
|
|
69
|
+
return await superTick(context);
|
|
70
|
+
})();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Condition checks blackboard
|
|
75
|
+
class BlackboardCondition extends SuccessNode {
|
|
76
|
+
tick(context: TemporalContext) {
|
|
77
|
+
const self = this;
|
|
78
|
+
return (async () => {
|
|
79
|
+
const met = context.blackboard.get("conditionMet");
|
|
80
|
+
self._status = met ? NodeStatus.SUCCESS : NodeStatus.FAILURE;
|
|
81
|
+
return self._status;
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
precond.setChild(new SuccessNode({ id: "child" }));
|
|
87
|
+
precond.addPrecondition(
|
|
88
|
+
new BlackboardCondition({ id: "condition" }),
|
|
89
|
+
new TrackedResolver({ id: "resolver" }),
|
|
90
|
+
true,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const result = await precond.tick(context);
|
|
94
|
+
expect(resolverExecuted).toBe(true);
|
|
95
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should skip optional preconditions", async () => {
|
|
99
|
+
const precond = new Precondition({ id: "precond1" });
|
|
100
|
+
|
|
101
|
+
let childExecuted = false;
|
|
102
|
+
class TrackedChild extends SuccessNode {
|
|
103
|
+
tick(context: TemporalContext) {
|
|
104
|
+
childExecuted = true;
|
|
105
|
+
return super.tick(context);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
precond.setChild(new TrackedChild({ id: "child" }));
|
|
110
|
+
precond.addPrecondition(
|
|
111
|
+
new FailureNode({ id: "condition" }),
|
|
112
|
+
undefined,
|
|
113
|
+
false,
|
|
114
|
+
); // Optional
|
|
115
|
+
|
|
116
|
+
const result = await precond.tick(context);
|
|
117
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
118
|
+
expect(childExecuted).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should check multiple preconditions", async () => {
|
|
122
|
+
const precond = new Precondition({ id: "precond1" });
|
|
123
|
+
|
|
124
|
+
precond.setChild(new SuccessNode({ id: "child" }));
|
|
125
|
+
precond.addPrecondition(new SuccessNode({ id: "cond1" }));
|
|
126
|
+
precond.addPrecondition(new SuccessNode({ id: "cond2" }));
|
|
127
|
+
precond.addPrecondition(new SuccessNode({ id: "cond3" }));
|
|
128
|
+
|
|
129
|
+
const result = await precond.tick(context);
|
|
130
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should fail on first failed required precondition", async () => {
|
|
134
|
+
const precond = new Precondition({ id: "precond1" });
|
|
135
|
+
|
|
136
|
+
let cond3Checked = false;
|
|
137
|
+
class Cond3 extends SuccessNode {
|
|
138
|
+
tick(context: TemporalContext) {
|
|
139
|
+
const superTick = super.tick.bind(this);
|
|
140
|
+
return (async () => {
|
|
141
|
+
cond3Checked = true;
|
|
142
|
+
return await superTick(context);
|
|
143
|
+
})();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
precond.setChild(new SuccessNode({ id: "child" }));
|
|
148
|
+
precond.addPrecondition(new SuccessNode({ id: "cond1" }));
|
|
149
|
+
precond.addPrecondition(new FailureNode({ id: "cond2" })); // Fails here
|
|
150
|
+
precond.addPrecondition(new Cond3({ id: "cond3" }));
|
|
151
|
+
|
|
152
|
+
const result = await precond.tick(context);
|
|
153
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
154
|
+
expect(cond3Checked).toBe(false); // Should not reach cond3
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should propagate RUNNING from precondition", async () => {
|
|
158
|
+
const precond = new Precondition({ id: "precond1" });
|
|
159
|
+
|
|
160
|
+
let tickCount = 0;
|
|
161
|
+
class RunningCondition extends SuccessNode {
|
|
162
|
+
tick(_context: TemporalContext) {
|
|
163
|
+
const self = this;
|
|
164
|
+
return (async () => {
|
|
165
|
+
tickCount++;
|
|
166
|
+
self._status =
|
|
167
|
+
tickCount < 2 ? NodeStatus.RUNNING : NodeStatus.SUCCESS;
|
|
168
|
+
return self._status;
|
|
169
|
+
})();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
precond.setChild(new SuccessNode({ id: "child" }));
|
|
174
|
+
precond.addPrecondition(new RunningCondition({ id: "condition" }));
|
|
175
|
+
|
|
176
|
+
let result = await precond.tick(context);
|
|
177
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
178
|
+
|
|
179
|
+
result = await precond.tick(context);
|
|
180
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should propagate ConfigurationError if no child", async () => {
|
|
184
|
+
const precond = new Precondition({ id: "precond1" });
|
|
185
|
+
precond.addPrecondition(new SuccessNode({ id: "condition" }));
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await precond.tick(context);
|
|
189
|
+
throw new Error("Expected tick to throw");
|
|
190
|
+
} catch (error) {
|
|
191
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
192
|
+
expect((error as ConfigurationError).message).toContain(
|
|
193
|
+
"Precondition requires a child",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("Multi-tick child execution - FIXED BEHAVIOR", () => {
|
|
199
|
+
it(
|
|
200
|
+
"should check precondition ONCE and not re-check while child is RUNNING",
|
|
201
|
+
async () => {
|
|
202
|
+
let conditionTickCount = 0;
|
|
203
|
+
let childTickCount = 0;
|
|
204
|
+
|
|
205
|
+
// Condition that counts how many times it's checked
|
|
206
|
+
class CountingCondition extends SuccessNode {
|
|
207
|
+
tick(_context: TemporalContext) {
|
|
208
|
+
const self = this;
|
|
209
|
+
return (async () => {
|
|
210
|
+
conditionTickCount++;
|
|
211
|
+
console.log(`[CountingCondition] Tick #${conditionTickCount}`);
|
|
212
|
+
self._status = NodeStatus.SUCCESS;
|
|
213
|
+
return NodeStatus.SUCCESS;
|
|
214
|
+
})();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Child that returns RUNNING for first 2 ticks, then SUCCESS
|
|
219
|
+
class MultiTickChild extends SuccessNode {
|
|
220
|
+
tick(context: TemporalContext) {
|
|
221
|
+
const self = this;
|
|
222
|
+
const superTick = super.tick.bind(this);
|
|
223
|
+
return (async () => {
|
|
224
|
+
childTickCount++;
|
|
225
|
+
console.log(`[MultiTickChild] Tick #${childTickCount}`);
|
|
226
|
+
|
|
227
|
+
if (childTickCount < 3) {
|
|
228
|
+
self._status = NodeStatus.RUNNING;
|
|
229
|
+
return NodeStatus.RUNNING;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return await superTick(context); // SUCCESS on tick 3
|
|
233
|
+
})();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const condition = new CountingCondition({ id: "counting-condition" });
|
|
238
|
+
const child = new MultiTickChild({ id: "multi-tick-child" });
|
|
239
|
+
const precondition = new Precondition({
|
|
240
|
+
id: "test-precondition",
|
|
241
|
+
name: "test-precondition",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
precondition.setChild(child);
|
|
245
|
+
precondition.addPrecondition(condition);
|
|
246
|
+
|
|
247
|
+
// Tick 1: Should check condition, then execute child (returns RUNNING)
|
|
248
|
+
const status1 = await precondition.tick(context);
|
|
249
|
+
expect(status1).toBe(NodeStatus.RUNNING);
|
|
250
|
+
expect(conditionTickCount).toBe(1); // Condition checked once
|
|
251
|
+
expect(childTickCount).toBe(1); // Child executed once
|
|
252
|
+
|
|
253
|
+
// Tick 2: ✅ FIXED: Should NOT re-check condition, just execute child (returns RUNNING)
|
|
254
|
+
const status2 = await precondition.tick(context);
|
|
255
|
+
expect(status2).toBe(NodeStatus.RUNNING);
|
|
256
|
+
expect(conditionTickCount).toBe(1); // ✅ Still 1! Not re-checked
|
|
257
|
+
expect(childTickCount).toBe(2); // Child executed again
|
|
258
|
+
|
|
259
|
+
// Tick 3: ✅ FIXED: Should NOT re-check condition, just execute child (returns SUCCESS)
|
|
260
|
+
const status3 = await precondition.tick(context);
|
|
261
|
+
expect(status3).toBe(NodeStatus.SUCCESS);
|
|
262
|
+
expect(conditionTickCount).toBe(1); // ✅ Still 1! Not re-checked
|
|
263
|
+
expect(childTickCount).toBe(3); // Child completes
|
|
264
|
+
|
|
265
|
+
// Summary: Condition was checked only 1 time (on first tick)
|
|
266
|
+
// This confirms the precondition is NOT re-evaluated on subsequent ticks
|
|
267
|
+
console.log(
|
|
268
|
+
"\n✅ FIXED: Precondition checked only 1 time, not re-checked during child execution",
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
it(
|
|
274
|
+
"should NOT be affected if precondition changes while child is RUNNING (safe behavior)",
|
|
275
|
+
async () => {
|
|
276
|
+
let childTickCount = 0;
|
|
277
|
+
let conditionShouldSucceed = true;
|
|
278
|
+
|
|
279
|
+
// Condition that succeeds initially, then fails on subsequent ticks
|
|
280
|
+
class ChangeableCondition extends SuccessNode {
|
|
281
|
+
tick(_context: TemporalContext) {
|
|
282
|
+
const self = this;
|
|
283
|
+
return (async () => {
|
|
284
|
+
const result = conditionShouldSucceed
|
|
285
|
+
? NodeStatus.SUCCESS
|
|
286
|
+
: NodeStatus.FAILURE;
|
|
287
|
+
console.log(`[ChangeableCondition] Returning: ${result}`);
|
|
288
|
+
self._status = result;
|
|
289
|
+
return result;
|
|
290
|
+
})();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Child that takes 3 ticks to complete
|
|
295
|
+
class SlowChild extends SuccessNode {
|
|
296
|
+
tick(context: TemporalContext) {
|
|
297
|
+
const self = this;
|
|
298
|
+
const superTick = super.tick.bind(this);
|
|
299
|
+
return (async () => {
|
|
300
|
+
childTickCount++;
|
|
301
|
+
console.log(`[SlowChild] Tick #${childTickCount}`);
|
|
302
|
+
|
|
303
|
+
if (childTickCount < 3) {
|
|
304
|
+
self._status = NodeStatus.RUNNING;
|
|
305
|
+
return NodeStatus.RUNNING;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return await superTick(context); // SUCCESS on tick 3
|
|
309
|
+
})();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const condition = new ChangeableCondition({
|
|
314
|
+
id: "changeable-condition",
|
|
315
|
+
});
|
|
316
|
+
const child = new SlowChild({ id: "slow-child" });
|
|
317
|
+
const precondition = new Precondition({
|
|
318
|
+
id: "test-precondition",
|
|
319
|
+
name: "test-precondition",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
precondition.setChild(child);
|
|
323
|
+
precondition.addPrecondition(condition);
|
|
324
|
+
|
|
325
|
+
// Tick 1: Precondition passes, child returns RUNNING
|
|
326
|
+
const status1 = await precondition.tick(context);
|
|
327
|
+
expect(status1).toBe(NodeStatus.RUNNING);
|
|
328
|
+
expect(childTickCount).toBe(1);
|
|
329
|
+
|
|
330
|
+
// Change condition to fail
|
|
331
|
+
conditionShouldSucceed = false;
|
|
332
|
+
|
|
333
|
+
// Tick 2: ✅ FIXED: Precondition is NOT re-checked, child continues
|
|
334
|
+
const status2 = await precondition.tick(context);
|
|
335
|
+
expect(status2).toBe(NodeStatus.RUNNING); // ✅ Still RUNNING!
|
|
336
|
+
expect(childTickCount).toBe(2); // ✅ Child continues executing
|
|
337
|
+
|
|
338
|
+
// Tick 3: ✅ FIXED: Child completes successfully despite precondition now failing
|
|
339
|
+
const status3 = await precondition.tick(context);
|
|
340
|
+
expect(status3).toBe(NodeStatus.SUCCESS); // ✅ Child completes!
|
|
341
|
+
expect(childTickCount).toBe(3); // Child executed all 3 ticks
|
|
342
|
+
|
|
343
|
+
// This demonstrates the fix: child execution is NOT interrupted
|
|
344
|
+
// even though the precondition would now fail if re-checked
|
|
345
|
+
console.log(
|
|
346
|
+
"\n✅ SAFE: Child execution continues uninterrupted despite precondition change",
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precondition decorator - Check/resolve preconditions before executing child
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DecoratorNode } from "../base-node.js";
|
|
6
|
+
import { ConfigurationError } from "../errors.js";
|
|
7
|
+
import { type TemporalContext, NodeStatus, type TreeNode } from "../types.js";
|
|
8
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
9
|
+
|
|
10
|
+
export interface PreconditionEntry {
|
|
11
|
+
condition: TreeNode;
|
|
12
|
+
resolver?: TreeNode;
|
|
13
|
+
required: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Precondition checks preconditions before executing the main child.
|
|
18
|
+
* If preconditions fail, attempts to resolve them using resolvers.
|
|
19
|
+
* Useful for ensuring prerequisites are met before executing actions.
|
|
20
|
+
*/
|
|
21
|
+
export class Precondition extends DecoratorNode {
|
|
22
|
+
private preconditions: PreconditionEntry[] = [];
|
|
23
|
+
private preconditionsChecked: boolean = false;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Add a precondition to check before main execution
|
|
27
|
+
*/
|
|
28
|
+
addPrecondition(
|
|
29
|
+
condition: TreeNode,
|
|
30
|
+
resolver?: TreeNode,
|
|
31
|
+
required: boolean = true,
|
|
32
|
+
): void {
|
|
33
|
+
this.preconditions.push({ condition, resolver, required });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
37
|
+
checkSignal(context.signal);
|
|
38
|
+
|
|
39
|
+
if (!this.child) {
|
|
40
|
+
throw new ConfigurationError("Precondition requires a child");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Only check preconditions if not already verified
|
|
44
|
+
if (!this.preconditionsChecked) {
|
|
45
|
+
// Check all preconditions
|
|
46
|
+
for (let i = 0; i < this.preconditions.length; i++) {
|
|
47
|
+
checkSignal(context.signal);
|
|
48
|
+
const precond = this.preconditions[i];
|
|
49
|
+
if (!precond) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.log(
|
|
54
|
+
`Checking precondition ${i + 1}/${this.preconditions.length}`,
|
|
55
|
+
);
|
|
56
|
+
const conditionResult = await precond.condition.tick(context);
|
|
57
|
+
|
|
58
|
+
if (conditionResult === NodeStatus.RUNNING) {
|
|
59
|
+
this.log(`Precondition ${i + 1} is running`);
|
|
60
|
+
this._status = NodeStatus.RUNNING;
|
|
61
|
+
return NodeStatus.RUNNING;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (conditionResult === NodeStatus.FAILURE) {
|
|
65
|
+
this.log(`Precondition ${i + 1} failed`);
|
|
66
|
+
|
|
67
|
+
// Try resolver if available
|
|
68
|
+
if (precond.resolver) {
|
|
69
|
+
this.log(`Attempting to resolve precondition ${i + 1}`);
|
|
70
|
+
const resolverResult = await precond.resolver.tick(context);
|
|
71
|
+
|
|
72
|
+
if (resolverResult === NodeStatus.RUNNING) {
|
|
73
|
+
this.log(`Resolver ${i + 1} is running`);
|
|
74
|
+
this._status = NodeStatus.RUNNING;
|
|
75
|
+
return NodeStatus.RUNNING;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (resolverResult === NodeStatus.SUCCESS) {
|
|
79
|
+
this.log(`Precondition ${i + 1} resolved successfully`);
|
|
80
|
+
// Re-check condition after resolution
|
|
81
|
+
const recheckResult = await precond.condition.tick(context);
|
|
82
|
+
if (recheckResult !== NodeStatus.SUCCESS) {
|
|
83
|
+
if (precond.required) {
|
|
84
|
+
this.log(
|
|
85
|
+
`Precondition ${i + 1} still not met after resolution`,
|
|
86
|
+
);
|
|
87
|
+
this._status = NodeStatus.FAILURE;
|
|
88
|
+
return NodeStatus.FAILURE;
|
|
89
|
+
} else {
|
|
90
|
+
this.log(`Optional precondition ${i + 1} skipped`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} else if (precond.required) {
|
|
94
|
+
this.log(`Failed to resolve required precondition ${i + 1}`);
|
|
95
|
+
this._status = NodeStatus.FAILURE;
|
|
96
|
+
return NodeStatus.FAILURE;
|
|
97
|
+
}
|
|
98
|
+
} else if (precond.required) {
|
|
99
|
+
this.log(`Required precondition ${i + 1} not met (no resolver)`);
|
|
100
|
+
this._status = NodeStatus.FAILURE;
|
|
101
|
+
return NodeStatus.FAILURE;
|
|
102
|
+
} else {
|
|
103
|
+
this.log(`Optional precondition ${i + 1} skipped`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Mark preconditions as checked once all pass
|
|
109
|
+
this.preconditionsChecked = true;
|
|
110
|
+
this.log("All preconditions met - executing main child");
|
|
111
|
+
} else {
|
|
112
|
+
this.log("Preconditions already verified - continuing child execution");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Execute child
|
|
116
|
+
checkSignal(context.signal);
|
|
117
|
+
const result = await this.child.tick(context);
|
|
118
|
+
this._status = result;
|
|
119
|
+
|
|
120
|
+
// Reset flag when child completes
|
|
121
|
+
if (result !== NodeStatus.RUNNING) {
|
|
122
|
+
this.log("Child completed - resetting precondition check flag");
|
|
123
|
+
this.preconditionsChecked = false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
protected onHalt(): void {
|
|
130
|
+
this.log("Halting - resetting precondition check flag");
|
|
131
|
+
this.preconditionsChecked = false;
|
|
132
|
+
super.onHalt();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
protected onReset(): void {
|
|
136
|
+
this.log("Resetting - clearing precondition check flag");
|
|
137
|
+
this.preconditionsChecked = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repeat decorator configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createNodeSchema, validations } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for Repeat decorator configuration
|
|
10
|
+
* Validates that numCycles is a positive integer
|
|
11
|
+
*/
|
|
12
|
+
export const repeatConfigurationSchema = createNodeSchema("Repeat", {
|
|
13
|
+
numCycles: validations.positiveInteger("numCycles"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validated Repeat configuration type
|
|
18
|
+
*/
|
|
19
|
+
export type ValidatedRepeatConfiguration = z.infer<
|
|
20
|
+
typeof repeatConfigurationSchema
|
|
21
|
+
>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Repeat decorator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
7
|
+
import { ConfigurationError } from "../errors.js";
|
|
8
|
+
import { SuccessNode } from "../test-nodes.js";
|
|
9
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
10
|
+
import { Repeat } from "./repeat.js";
|
|
11
|
+
|
|
12
|
+
describe("Repeat", () => {
|
|
13
|
+
let blackboard: ScopedBlackboard;
|
|
14
|
+
let context: TemporalContext;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
blackboard = new ScopedBlackboard("root");
|
|
18
|
+
context = {
|
|
19
|
+
blackboard,
|
|
20
|
+
timestamp: Date.now(),
|
|
21
|
+
deltaTime: 0,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should execute child exactly N times", async () => {
|
|
26
|
+
const repeat = new Repeat({ id: "repeat1", numCycles: 3 });
|
|
27
|
+
|
|
28
|
+
let tickCount = 0;
|
|
29
|
+
class CountingNode extends SuccessNode {
|
|
30
|
+
tick(context: TemporalContext) {
|
|
31
|
+
tickCount++;
|
|
32
|
+
return super.tick(context);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
repeat.setChild(new CountingNode({ id: "child" }));
|
|
37
|
+
|
|
38
|
+
// First 2 ticks return RUNNING
|
|
39
|
+
let result = await repeat.tick(context);
|
|
40
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
41
|
+
expect(tickCount).toBe(1);
|
|
42
|
+
|
|
43
|
+
result = await repeat.tick(context);
|
|
44
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
45
|
+
expect(tickCount).toBe(2);
|
|
46
|
+
|
|
47
|
+
// Third tick returns SUCCESS
|
|
48
|
+
result = await repeat.tick(context);
|
|
49
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
50
|
+
expect(tickCount).toBe(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should fail on child failure", async () => {
|
|
54
|
+
const repeat = new Repeat({ id: "repeat1", numCycles: 5 });
|
|
55
|
+
|
|
56
|
+
let tickCount = 0;
|
|
57
|
+
class FailOnThird extends SuccessNode {
|
|
58
|
+
tick(context: TemporalContext) {
|
|
59
|
+
const self = this;
|
|
60
|
+
const superTick = super.tick.bind(this);
|
|
61
|
+
return (async () => {
|
|
62
|
+
tickCount++;
|
|
63
|
+
if (tickCount === 3) {
|
|
64
|
+
self._status = NodeStatus.FAILURE;
|
|
65
|
+
return NodeStatus.FAILURE;
|
|
66
|
+
}
|
|
67
|
+
return await superTick(context);
|
|
68
|
+
})();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
repeat.setChild(new FailOnThird({ id: "child" }));
|
|
73
|
+
|
|
74
|
+
await repeat.tick(context); // Cycle 1
|
|
75
|
+
await repeat.tick(context); // Cycle 2
|
|
76
|
+
const result = await repeat.tick(context); // Cycle 3 fails
|
|
77
|
+
|
|
78
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
79
|
+
expect(tickCount).toBe(3);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should reset child between cycles", async () => {
|
|
83
|
+
const repeat = new Repeat({ id: "repeat1", numCycles: 3 });
|
|
84
|
+
|
|
85
|
+
let resetCount = 0;
|
|
86
|
+
class ResetTracker extends SuccessNode {
|
|
87
|
+
reset(): void {
|
|
88
|
+
resetCount++;
|
|
89
|
+
super.reset();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
repeat.setChild(new ResetTracker({ id: "child" }));
|
|
94
|
+
|
|
95
|
+
await repeat.tick(context); // Cycle 1, reset after
|
|
96
|
+
await repeat.tick(context); // Cycle 2, reset after
|
|
97
|
+
await repeat.tick(context); // Cycle 3, completes
|
|
98
|
+
|
|
99
|
+
expect(resetCount).toBe(2); // Reset after cycles 1 and 2
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should handle RUNNING state", async () => {
|
|
103
|
+
const repeat = new Repeat({ id: "repeat1", numCycles: 2 });
|
|
104
|
+
|
|
105
|
+
let tickCount = 0;
|
|
106
|
+
class TwoTickNode extends SuccessNode {
|
|
107
|
+
tick(context: TemporalContext) {
|
|
108
|
+
const self = this;
|
|
109
|
+
const superTick = super.tick.bind(this);
|
|
110
|
+
return (async () => {
|
|
111
|
+
tickCount++;
|
|
112
|
+
if (tickCount % 2 === 1) {
|
|
113
|
+
// Odd ticks return RUNNING
|
|
114
|
+
self._status = NodeStatus.RUNNING;
|
|
115
|
+
return NodeStatus.RUNNING;
|
|
116
|
+
}
|
|
117
|
+
return await superTick(context);
|
|
118
|
+
})();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
repeat.setChild(new TwoTickNode({ id: "child" }));
|
|
123
|
+
|
|
124
|
+
// Cycle 1: tick 1 (RUNNING), tick 2 (SUCCESS)
|
|
125
|
+
let result = await repeat.tick(context);
|
|
126
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
127
|
+
result = await repeat.tick(context);
|
|
128
|
+
expect(result).toBe(NodeStatus.RUNNING); // Still more cycles
|
|
129
|
+
|
|
130
|
+
// Cycle 2: tick 3 (RUNNING), tick 4 (SUCCESS)
|
|
131
|
+
result = await repeat.tick(context);
|
|
132
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
133
|
+
result = await repeat.tick(context);
|
|
134
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
135
|
+
|
|
136
|
+
expect(tickCount).toBe(4);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should reset cycle count on completion", async () => {
|
|
140
|
+
const repeat = new Repeat({ id: "repeat1", numCycles: 2 });
|
|
141
|
+
|
|
142
|
+
let firstRunTicks = 0;
|
|
143
|
+
let secondRunTicks = 0;
|
|
144
|
+
let inSecondRun = false;
|
|
145
|
+
|
|
146
|
+
class CountingNode extends SuccessNode {
|
|
147
|
+
tick(context: TemporalContext) {
|
|
148
|
+
const superTick = super.tick.bind(this);
|
|
149
|
+
return (async () => {
|
|
150
|
+
if (inSecondRun) {
|
|
151
|
+
secondRunTicks++;
|
|
152
|
+
} else {
|
|
153
|
+
firstRunTicks++;
|
|
154
|
+
}
|
|
155
|
+
return await superTick(context);
|
|
156
|
+
})();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
repeat.setChild(new CountingNode({ id: "child" }));
|
|
161
|
+
|
|
162
|
+
// First run
|
|
163
|
+
await repeat.tick(context);
|
|
164
|
+
await repeat.tick(context);
|
|
165
|
+
expect(firstRunTicks).toBe(2);
|
|
166
|
+
|
|
167
|
+
// Second run
|
|
168
|
+
inSecondRun = true;
|
|
169
|
+
await repeat.tick(context);
|
|
170
|
+
await repeat.tick(context);
|
|
171
|
+
expect(secondRunTicks).toBe(2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should propagate ConfigurationError if no child", async () => {
|
|
175
|
+
const repeat = new Repeat({ id: "repeat1", numCycles: 1 });
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await repeat.tick(context);
|
|
179
|
+
throw new Error("Expected tick to throw");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
182
|
+
expect((error as ConfigurationError).message).toContain(
|
|
183
|
+
"Repeat requires a child",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|