@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,337 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
3
|
+
import { MockAction } from "../test-nodes.js";
|
|
4
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
5
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
6
|
+
import { Sequence } from "./sequence.js";
|
|
7
|
+
|
|
8
|
+
describe("Sequence", () => {
|
|
9
|
+
let context: TemporalContext;
|
|
10
|
+
let sequence: Sequence;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
context = {
|
|
14
|
+
blackboard: new ScopedBlackboard(),
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
deltaTime: 0,
|
|
17
|
+
};
|
|
18
|
+
sequence = new Sequence({ id: "test-sequence" });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should return SUCCESS when empty", async () => {
|
|
22
|
+
const status = await sequence.tick(context);
|
|
23
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should execute all children in order when all succeed", async () => {
|
|
27
|
+
const executionOrder: string[] = [];
|
|
28
|
+
|
|
29
|
+
const child1 = new MockAction({
|
|
30
|
+
id: "child1",
|
|
31
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
32
|
+
});
|
|
33
|
+
const child2 = new MockAction({
|
|
34
|
+
id: "child2",
|
|
35
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
36
|
+
});
|
|
37
|
+
const child3 = new MockAction({
|
|
38
|
+
id: "child3",
|
|
39
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Spy on tick to track execution order
|
|
43
|
+
const originalTick1 = child1.tick.bind(child1);
|
|
44
|
+
const originalTick2 = child2.tick.bind(child2);
|
|
45
|
+
const originalTick3 = child3.tick.bind(child3);
|
|
46
|
+
|
|
47
|
+
child1.tick = (ctx) => {
|
|
48
|
+
executionOrder.push("child1");
|
|
49
|
+
return originalTick1(ctx);
|
|
50
|
+
};
|
|
51
|
+
child2.tick = (ctx) => {
|
|
52
|
+
executionOrder.push("child2");
|
|
53
|
+
return originalTick2(ctx);
|
|
54
|
+
};
|
|
55
|
+
child3.tick = (ctx) => {
|
|
56
|
+
executionOrder.push("child3");
|
|
57
|
+
return originalTick3(ctx);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
sequence.addChildren([child1, child2, child3]);
|
|
61
|
+
|
|
62
|
+
const status = await sequence.tick(context);
|
|
63
|
+
|
|
64
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
65
|
+
expect(executionOrder).toEqual(["child1", "child2", "child3"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should stop and return FAILURE when a child fails", async () => {
|
|
69
|
+
const executionOrder: string[] = [];
|
|
70
|
+
|
|
71
|
+
const child1 = new MockAction({
|
|
72
|
+
id: "child1",
|
|
73
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
74
|
+
});
|
|
75
|
+
const child2 = new MockAction({
|
|
76
|
+
id: "child2",
|
|
77
|
+
returnStatus: NodeStatus.FAILURE,
|
|
78
|
+
});
|
|
79
|
+
const child3 = new MockAction({
|
|
80
|
+
id: "child3",
|
|
81
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Track execution
|
|
85
|
+
const originalTick1 = child1.tick.bind(child1);
|
|
86
|
+
const originalTick2 = child2.tick.bind(child2);
|
|
87
|
+
const originalTick3 = child3.tick.bind(child3);
|
|
88
|
+
|
|
89
|
+
child1.tick = (ctx) => {
|
|
90
|
+
executionOrder.push("child1");
|
|
91
|
+
return originalTick1(ctx);
|
|
92
|
+
};
|
|
93
|
+
child2.tick = (ctx) => {
|
|
94
|
+
executionOrder.push("child2");
|
|
95
|
+
return originalTick2(ctx);
|
|
96
|
+
};
|
|
97
|
+
child3.tick = (ctx) => {
|
|
98
|
+
executionOrder.push("child3");
|
|
99
|
+
return originalTick3(ctx);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
sequence.addChildren([child1, child2, child3]);
|
|
103
|
+
|
|
104
|
+
const status = await sequence.tick(context);
|
|
105
|
+
|
|
106
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
107
|
+
expect(executionOrder).toEqual(["child1", "child2"]); // child3 should not execute
|
|
108
|
+
expect(sequence.status()).toBe(NodeStatus.FAILURE);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should handle RUNNING status correctly", async () => {
|
|
112
|
+
const child1 = new MockAction({
|
|
113
|
+
id: "child1",
|
|
114
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
115
|
+
});
|
|
116
|
+
let child2 = new MockAction({
|
|
117
|
+
id: "child2",
|
|
118
|
+
returnStatus: NodeStatus.RUNNING,
|
|
119
|
+
ticksBeforeComplete: 3,
|
|
120
|
+
});
|
|
121
|
+
const child3 = new MockAction({
|
|
122
|
+
id: "child3",
|
|
123
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
sequence.addChildren([child1, child2, child3]);
|
|
127
|
+
|
|
128
|
+
// First tick - child2 returns RUNNING
|
|
129
|
+
let status = await sequence.tick(context);
|
|
130
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
131
|
+
expect(child1.status()).toBe(NodeStatus.SUCCESS);
|
|
132
|
+
expect(child2.status()).toBe(NodeStatus.RUNNING);
|
|
133
|
+
|
|
134
|
+
// Second tick - child2 still RUNNING
|
|
135
|
+
status = await sequence.tick(context);
|
|
136
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
137
|
+
|
|
138
|
+
// Third tick - child2 completes
|
|
139
|
+
child2 = new MockAction({
|
|
140
|
+
id: "child2",
|
|
141
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
142
|
+
});
|
|
143
|
+
sequence._children[1] = child2; // Replace the child
|
|
144
|
+
|
|
145
|
+
status = await sequence.tick(context);
|
|
146
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
147
|
+
expect(child3.status()).toBe(NodeStatus.SUCCESS);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should reset child index on completion", async () => {
|
|
151
|
+
const child1 = new MockAction({
|
|
152
|
+
id: "child1",
|
|
153
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
154
|
+
});
|
|
155
|
+
const child2 = new MockAction({
|
|
156
|
+
id: "child2",
|
|
157
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
sequence.addChildren([child1, child2]);
|
|
161
|
+
|
|
162
|
+
// First execution
|
|
163
|
+
await sequence.tick(context);
|
|
164
|
+
expect((sequence as unknown).currentChildIndex).toBe(0);
|
|
165
|
+
|
|
166
|
+
// Second execution should start from beginning
|
|
167
|
+
await sequence.tick(context);
|
|
168
|
+
expect(child1.status()).toBe(NodeStatus.SUCCESS);
|
|
169
|
+
expect(child2.status()).toBe(NodeStatus.SUCCESS);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should halt running children when halted", async () => {
|
|
173
|
+
const child1 = new MockAction({
|
|
174
|
+
id: "child1",
|
|
175
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
176
|
+
});
|
|
177
|
+
const child2 = new MockAction({
|
|
178
|
+
id: "child2",
|
|
179
|
+
returnStatus: NodeStatus.RUNNING,
|
|
180
|
+
});
|
|
181
|
+
const child3 = new MockAction({
|
|
182
|
+
id: "child3",
|
|
183
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
sequence.addChildren([child1, child2, child3]);
|
|
187
|
+
|
|
188
|
+
// Start execution
|
|
189
|
+
await sequence.tick(context);
|
|
190
|
+
expect(sequence.status()).toBe(NodeStatus.RUNNING);
|
|
191
|
+
|
|
192
|
+
// Halt the sequence
|
|
193
|
+
sequence.halt();
|
|
194
|
+
|
|
195
|
+
expect(sequence.status()).toBe(NodeStatus.IDLE);
|
|
196
|
+
expect((sequence as unknown).currentChildIndex).toBe(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should throw error if child is undefined", () => {
|
|
200
|
+
expect(() => sequence.addChild(undefined as unknown)).toThrow(
|
|
201
|
+
"Cannot add undefined child to composite node",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should handle mixed sync and async children", async () => {
|
|
206
|
+
const syncChild = {
|
|
207
|
+
id: "sync",
|
|
208
|
+
name: "sync",
|
|
209
|
+
type: "SyncNode",
|
|
210
|
+
status: () => NodeStatus.SUCCESS,
|
|
211
|
+
tick: async (_ctx: TemporalContext) => NodeStatus.SUCCESS, // Sync tick
|
|
212
|
+
halt: () => {},
|
|
213
|
+
reset: () => {},
|
|
214
|
+
parent: undefined,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const asyncChild = new MockAction({
|
|
218
|
+
id: "async",
|
|
219
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
sequence.addChildren([syncChild as unknown, asyncChild]);
|
|
223
|
+
|
|
224
|
+
const status = await sequence.tick(context);
|
|
225
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("Signal-based cancellation", () => {
|
|
229
|
+
it("should stop executing children when signal is aborted", async () => {
|
|
230
|
+
const executionOrder: string[] = [];
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
|
|
233
|
+
// Add signal to context
|
|
234
|
+
context.signal = controller.signal;
|
|
235
|
+
|
|
236
|
+
// Create children
|
|
237
|
+
const child1 = new MockAction({
|
|
238
|
+
id: "child1",
|
|
239
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
240
|
+
});
|
|
241
|
+
const child2 = new MockAction({
|
|
242
|
+
id: "child2",
|
|
243
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
244
|
+
});
|
|
245
|
+
const child3 = new MockAction({
|
|
246
|
+
id: "child3",
|
|
247
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Track execution order
|
|
251
|
+
const originalTick1 = child1.tick.bind(child1);
|
|
252
|
+
const originalTick2 = child2.tick.bind(child2);
|
|
253
|
+
const originalTick3 = child3.tick.bind(child3);
|
|
254
|
+
|
|
255
|
+
child1.tick = (ctx: TemporalContext) => {
|
|
256
|
+
executionOrder.push("child1");
|
|
257
|
+
return originalTick1(ctx);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
child2.tick = (ctx: TemporalContext) => {
|
|
261
|
+
executionOrder.push("child2");
|
|
262
|
+
return originalTick2(ctx);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
child3.tick = (ctx: TemporalContext) => {
|
|
266
|
+
executionOrder.push("child3");
|
|
267
|
+
return originalTick3(ctx);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
sequence.addChildren([child1, child2, child3]);
|
|
271
|
+
|
|
272
|
+
// Abort signal BEFORE ticking - Sequence should check signal immediately
|
|
273
|
+
controller.abort();
|
|
274
|
+
|
|
275
|
+
// Tick should fail with OperationCancelledError before any children execute
|
|
276
|
+
try {
|
|
277
|
+
await sequence.tick(context);
|
|
278
|
+
expect.fail("Should have thrown an error");
|
|
279
|
+
} catch (error) {
|
|
280
|
+
expect(error).toBeInstanceOf(Error);
|
|
281
|
+
expect((error as Error).name).toBe("OperationCancelledError");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// No children should execute because signal was already aborted
|
|
285
|
+
expect(executionOrder.length).toBe(0);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should respect abort signal in child iteration loop", async () => {
|
|
289
|
+
const controller = new AbortController();
|
|
290
|
+
const childrenExecuted: string[] = [];
|
|
291
|
+
|
|
292
|
+
// Add signal to context
|
|
293
|
+
context.signal = controller.signal;
|
|
294
|
+
|
|
295
|
+
// Create 5 children
|
|
296
|
+
const children = Array.from({ length: 5 }, (_, i) => {
|
|
297
|
+
const child = new MockAction({
|
|
298
|
+
id: `child${i}`,
|
|
299
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
300
|
+
});
|
|
301
|
+
child.tick = async (ctx: TemporalContext) => {
|
|
302
|
+
await checkSignal(ctx.signal);
|
|
303
|
+
childrenExecuted.push(`child${i}`);
|
|
304
|
+
return NodeStatus.SUCCESS;
|
|
305
|
+
};
|
|
306
|
+
return child;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
sequence.addChildren(children);
|
|
310
|
+
|
|
311
|
+
// Abort after 2 children have executed
|
|
312
|
+
let execCount = 0;
|
|
313
|
+
const _originalTick = children[0].tick;
|
|
314
|
+
children.forEach((child, _idx) => {
|
|
315
|
+
const orig = child.tick;
|
|
316
|
+
child.tick = async (ctx: TemporalContext) => {
|
|
317
|
+
execCount++;
|
|
318
|
+
if (execCount === 2) {
|
|
319
|
+
controller.abort();
|
|
320
|
+
}
|
|
321
|
+
return await orig(ctx);
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await sequence.tick(context);
|
|
327
|
+
expect.fail("Should have thrown an error");
|
|
328
|
+
} catch (error) {
|
|
329
|
+
expect(error).toBeInstanceOf(Error);
|
|
330
|
+
expect((error as Error).name).toBe("OperationCancelledError");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Should stop after detecting abort
|
|
334
|
+
expect(childrenExecuted.length).toBeLessThanOrEqual(2);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequence composite node
|
|
3
|
+
* Executes children in order until one fails or all succeed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CompositeNode } from "../base-node.js";
|
|
7
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
8
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
9
|
+
|
|
10
|
+
export class Sequence extends CompositeNode {
|
|
11
|
+
private currentChildIndex: number = 0;
|
|
12
|
+
|
|
13
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
14
|
+
this.log("Ticking with", this._children.length, "children");
|
|
15
|
+
|
|
16
|
+
if (this._children.length === 0) {
|
|
17
|
+
return NodeStatus.SUCCESS;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Continue from where we left off if RUNNING
|
|
21
|
+
while (this.currentChildIndex < this._children.length) {
|
|
22
|
+
// Check for cancellation before ticking each child
|
|
23
|
+
checkSignal(context.signal);
|
|
24
|
+
|
|
25
|
+
const child = this._children[this.currentChildIndex];
|
|
26
|
+
if (!child) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Child at index ${this.currentChildIndex} is undefined`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.log(`Ticking child ${this.currentChildIndex}: ${child.name}`);
|
|
33
|
+
const childStatus = await child.tick(context);
|
|
34
|
+
|
|
35
|
+
switch (childStatus) {
|
|
36
|
+
case NodeStatus.SUCCESS:
|
|
37
|
+
this.log(`Child ${child.name} succeeded`);
|
|
38
|
+
this.currentChildIndex++;
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
case NodeStatus.FAILURE:
|
|
42
|
+
this.log(`Child ${child.name} failed - sequence fails`);
|
|
43
|
+
this._status = NodeStatus.FAILURE;
|
|
44
|
+
this.currentChildIndex = 0;
|
|
45
|
+
return NodeStatus.FAILURE;
|
|
46
|
+
|
|
47
|
+
case NodeStatus.RUNNING:
|
|
48
|
+
this.log(`Child ${child.name} is running`);
|
|
49
|
+
this._status = NodeStatus.RUNNING;
|
|
50
|
+
return NodeStatus.RUNNING;
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unexpected status from child: ${childStatus}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// All children succeeded
|
|
58
|
+
this.log("All children succeeded");
|
|
59
|
+
this._status = NodeStatus.SUCCESS;
|
|
60
|
+
this.currentChildIndex = 0;
|
|
61
|
+
return NodeStatus.SUCCESS;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected onHalt(): void {
|
|
65
|
+
this.haltChildren(this.currentChildIndex);
|
|
66
|
+
this.currentChildIndex = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected onReset(): void {
|
|
70
|
+
this.currentChildIndex = 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubTree composite configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createNodeSchema, validations } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for SubTree composite configuration
|
|
10
|
+
* Validates treeId reference
|
|
11
|
+
*/
|
|
12
|
+
export const subTreeConfigurationSchema = createNodeSchema("SubTree", {
|
|
13
|
+
treeId: validations.treeId,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validated SubTree configuration type
|
|
18
|
+
*/
|
|
19
|
+
export type ValidatedSubTreeConfiguration = z.infer<
|
|
20
|
+
typeof subTreeConfigurationSchema
|
|
21
|
+
>;
|