@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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel composite node
|
|
3
|
+
* Executes all children concurrently (truly concurrent, not sequential)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CompositeNode } from "../base-node.js";
|
|
7
|
+
import {
|
|
8
|
+
type TemporalContext,
|
|
9
|
+
type NodeConfiguration,
|
|
10
|
+
NodeStatus,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execution strategy for parallel node
|
|
16
|
+
*/
|
|
17
|
+
export type ParallelStrategy =
|
|
18
|
+
| "strict" // All children must succeed
|
|
19
|
+
| "any"; // At least one child must succeed
|
|
20
|
+
|
|
21
|
+
export interface ParallelConfiguration extends NodeConfiguration {
|
|
22
|
+
/**
|
|
23
|
+
* Execution strategy
|
|
24
|
+
* - 'strict': All children must succeed (default)
|
|
25
|
+
* - 'any': At least one child must succeed
|
|
26
|
+
*/
|
|
27
|
+
strategy?: ParallelStrategy;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optional: Number of children that must succeed (overrides strategy)
|
|
31
|
+
*/
|
|
32
|
+
successThreshold?: number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Optional: Number of children that must fail before parallel fails
|
|
36
|
+
*/
|
|
37
|
+
failureThreshold?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class Parallel extends CompositeNode {
|
|
41
|
+
private strategy: ParallelStrategy;
|
|
42
|
+
private successThreshold?: number;
|
|
43
|
+
private failureThreshold?: number;
|
|
44
|
+
|
|
45
|
+
constructor(config: ParallelConfiguration) {
|
|
46
|
+
super(config);
|
|
47
|
+
this.strategy = config.strategy ?? "strict";
|
|
48
|
+
this.successThreshold = config.successThreshold;
|
|
49
|
+
this.failureThreshold = config.failureThreshold;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
53
|
+
this.log(
|
|
54
|
+
`Ticking with ${this._children.length} children (strategy: ${this.strategy})`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (this._children.length === 0) {
|
|
58
|
+
return NodeStatus.SUCCESS;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Only tick children that haven't completed yet (IDLE or RUNNING)
|
|
62
|
+
// Children that are SUCCESS or FAILURE should keep their status
|
|
63
|
+
const childrenToTick = this._children.filter((child) => {
|
|
64
|
+
const status = child.status();
|
|
65
|
+
return status === NodeStatus.IDLE || status === NodeStatus.RUNNING;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.log(
|
|
69
|
+
`Ticking ${childrenToTick.length}/${this._children.length} children (others completed)`,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Check for cancellation before concurrent execution
|
|
73
|
+
checkSignal(context.signal);
|
|
74
|
+
|
|
75
|
+
// Tick active children concurrently using Promise.all
|
|
76
|
+
if (childrenToTick.length > 0) {
|
|
77
|
+
await Promise.all(childrenToTick.map((child) => child.tick(context)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Collect all statuses (from both ticked and already-completed children)
|
|
81
|
+
const allStatuses = this._children.map((child) => child.status());
|
|
82
|
+
|
|
83
|
+
// Check if any child is still running
|
|
84
|
+
const hasRunning = allStatuses.some(
|
|
85
|
+
(status) => status === NodeStatus.RUNNING,
|
|
86
|
+
);
|
|
87
|
+
if (hasRunning) {
|
|
88
|
+
this.log("At least one child returned RUNNING");
|
|
89
|
+
return NodeStatus.RUNNING;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// All children completed - count successes and failures
|
|
93
|
+
const successes = allStatuses.filter(
|
|
94
|
+
(status) => status === NodeStatus.SUCCESS,
|
|
95
|
+
).length;
|
|
96
|
+
const failures = allStatuses.filter(
|
|
97
|
+
(status) => status === NodeStatus.FAILURE,
|
|
98
|
+
).length;
|
|
99
|
+
|
|
100
|
+
this.log(`Results - Successes: ${successes}, Failures: ${failures}`);
|
|
101
|
+
|
|
102
|
+
// Check threshold-based completion first (if configured)
|
|
103
|
+
if (
|
|
104
|
+
this.successThreshold !== undefined &&
|
|
105
|
+
successes >= this.successThreshold
|
|
106
|
+
) {
|
|
107
|
+
this.log(
|
|
108
|
+
`Success threshold met: ${successes}/${this.successThreshold} -> SUCCESS`,
|
|
109
|
+
);
|
|
110
|
+
return NodeStatus.SUCCESS;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
this.failureThreshold !== undefined &&
|
|
115
|
+
failures >= this.failureThreshold
|
|
116
|
+
) {
|
|
117
|
+
this.log(
|
|
118
|
+
`Failure threshold met: ${failures}/${this.failureThreshold} -> FAILURE`,
|
|
119
|
+
);
|
|
120
|
+
return NodeStatus.FAILURE;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Apply strategy
|
|
124
|
+
if (this.strategy === "strict") {
|
|
125
|
+
// All must succeed
|
|
126
|
+
const finalStatus =
|
|
127
|
+
successes === this._children.length
|
|
128
|
+
? NodeStatus.SUCCESS
|
|
129
|
+
: NodeStatus.FAILURE;
|
|
130
|
+
this.log(
|
|
131
|
+
`Strategy 'strict': ${successes}/${this._children.length} succeeded -> ${finalStatus}`,
|
|
132
|
+
);
|
|
133
|
+
return finalStatus;
|
|
134
|
+
} else {
|
|
135
|
+
// Any (at least one must succeed)
|
|
136
|
+
const finalStatus =
|
|
137
|
+
successes > 0 ? NodeStatus.SUCCESS : NodeStatus.FAILURE;
|
|
138
|
+
this.log(`Strategy 'any': ${successes} succeeded -> ${finalStatus}`);
|
|
139
|
+
return finalStatus;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected onHalt(): void {
|
|
144
|
+
this.log("Halting parallel execution");
|
|
145
|
+
// Halt all running children
|
|
146
|
+
for (const child of this._children) {
|
|
147
|
+
if (child.status() === NodeStatus.RUNNING) {
|
|
148
|
+
child.halt();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected onReset(): void {
|
|
154
|
+
this.log("Resetting parallel state");
|
|
155
|
+
// Reset handled by parent class
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactiveSequence composite configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { nodeConfigurationSchema } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for ReactiveSequence composite configuration
|
|
10
|
+
* Uses base schema only (no additional properties)
|
|
11
|
+
*/
|
|
12
|
+
export const reactiveSequenceConfigurationSchema = nodeConfigurationSchema;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validated ReactiveSequence configuration type
|
|
16
|
+
*/
|
|
17
|
+
export type ValidatedReactiveSequenceConfiguration = z.infer<
|
|
18
|
+
typeof reactiveSequenceConfigurationSchema
|
|
19
|
+
>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ReactiveSequence node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
7
|
+
import { FailureNode, SuccessNode } from "../test-nodes.js";
|
|
8
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
9
|
+
import { ReactiveSequence } from "./reactive-sequence.js";
|
|
10
|
+
|
|
11
|
+
describe("ReactiveSequence", () => {
|
|
12
|
+
let blackboard: ScopedBlackboard;
|
|
13
|
+
let context: TemporalContext;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
blackboard = new ScopedBlackboard("root");
|
|
17
|
+
context = {
|
|
18
|
+
blackboard,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
deltaTime: 0,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("Basic Functionality", () => {
|
|
25
|
+
it("should execute children in order", async () => {
|
|
26
|
+
const seq = new ReactiveSequence({ id: "seq1" });
|
|
27
|
+
const child1 = new SuccessNode({ id: "child1" });
|
|
28
|
+
const child2 = new SuccessNode({ id: "child2" });
|
|
29
|
+
const child3 = new SuccessNode({ id: "child3" });
|
|
30
|
+
|
|
31
|
+
seq.addChildren([child1, child2, child3]);
|
|
32
|
+
|
|
33
|
+
const result = await seq.tick(context);
|
|
34
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should fail fast on first failure", async () => {
|
|
38
|
+
const seq = new ReactiveSequence({ id: "seq1" });
|
|
39
|
+
const child1 = new SuccessNode({ id: "child1" });
|
|
40
|
+
const child2 = new FailureNode({ id: "child2" });
|
|
41
|
+
const child3 = new SuccessNode({ id: "child3" });
|
|
42
|
+
|
|
43
|
+
seq.addChildren([child1, child2, child3]);
|
|
44
|
+
|
|
45
|
+
const result = await seq.tick(context);
|
|
46
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
47
|
+
expect(child3.status()).toBe(NodeStatus.IDLE); // Never executed
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("Reactive Behavior", () => {
|
|
52
|
+
it("should restart from beginning each tick", async () => {
|
|
53
|
+
const seq = new ReactiveSequence({ id: "seq1" });
|
|
54
|
+
|
|
55
|
+
// Track ticks for each child
|
|
56
|
+
let child1Ticks = 0;
|
|
57
|
+
let child2Ticks = 0;
|
|
58
|
+
|
|
59
|
+
class CountingSuccess extends SuccessNode {
|
|
60
|
+
async tick(context: TemporalContext): Promise<NodeStatus> {
|
|
61
|
+
const superTick = super.tick.bind(this);
|
|
62
|
+
if (this.id === "child1") child1Ticks++;
|
|
63
|
+
if (this.id === "child2") child2Ticks++;
|
|
64
|
+
return await superTick(context);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const child1 = new CountingSuccess({ id: "child1" });
|
|
69
|
+
const child2 = new CountingSuccess({ id: "child2" });
|
|
70
|
+
|
|
71
|
+
// Child3 runs first time, succeeds second time
|
|
72
|
+
let child3TickCount = 0;
|
|
73
|
+
class TwoTickNode extends SuccessNode {
|
|
74
|
+
async tick(context: TemporalContext): Promise<NodeStatus> {
|
|
75
|
+
const superTick = super.tick.bind(this);
|
|
76
|
+
child3TickCount++;
|
|
77
|
+
if (child3TickCount === 1) {
|
|
78
|
+
this._status = NodeStatus.RUNNING;
|
|
79
|
+
return NodeStatus.RUNNING;
|
|
80
|
+
}
|
|
81
|
+
return await superTick(context);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const child3 = new TwoTickNode({ id: "child3" });
|
|
85
|
+
|
|
86
|
+
seq.addChildren([child1, child2, child3]);
|
|
87
|
+
|
|
88
|
+
// First tick: child1, child2 succeed, child3 returns RUNNING
|
|
89
|
+
let result = await seq.tick(context);
|
|
90
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
91
|
+
expect(child1Ticks).toBe(1);
|
|
92
|
+
expect(child2Ticks).toBe(1);
|
|
93
|
+
expect(child3TickCount).toBe(1);
|
|
94
|
+
|
|
95
|
+
// Second tick: should restart from child1 (reactive behavior)
|
|
96
|
+
result = await seq.tick(context);
|
|
97
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
98
|
+
expect(child1Ticks).toBe(2); // Re-executed
|
|
99
|
+
expect(child2Ticks).toBe(2); // Re-executed
|
|
100
|
+
expect(child3TickCount).toBe(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should re-evaluate conditions that might change", async () => {
|
|
104
|
+
const seq = new ReactiveSequence({ id: "seq1" });
|
|
105
|
+
|
|
106
|
+
// Condition that checks blackboard value
|
|
107
|
+
let conditionTicks = 0;
|
|
108
|
+
class CheckValueCondition extends SuccessNode {
|
|
109
|
+
async tick(context: TemporalContext): Promise<NodeStatus> {
|
|
110
|
+
conditionTicks++;
|
|
111
|
+
const value = context.blackboard.get("shouldContinue");
|
|
112
|
+
this._status = value ? NodeStatus.SUCCESS : NodeStatus.FAILURE;
|
|
113
|
+
return this._status;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const condition = new CheckValueCondition({ id: "condition" });
|
|
118
|
+
|
|
119
|
+
// Action that stays running
|
|
120
|
+
let actionTicks = 0;
|
|
121
|
+
class RunningAction extends SuccessNode {
|
|
122
|
+
async tick(_context: TemporalContext): Promise<NodeStatus> {
|
|
123
|
+
actionTicks++;
|
|
124
|
+
this._status = NodeStatus.RUNNING;
|
|
125
|
+
return NodeStatus.RUNNING;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const action = new RunningAction({ id: "action" });
|
|
129
|
+
|
|
130
|
+
seq.addChildren([condition, action]);
|
|
131
|
+
|
|
132
|
+
// First tick: condition true, action runs
|
|
133
|
+
blackboard.set("shouldContinue", true);
|
|
134
|
+
let result = await seq.tick(context);
|
|
135
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
136
|
+
expect(conditionTicks).toBe(1);
|
|
137
|
+
expect(actionTicks).toBe(1);
|
|
138
|
+
|
|
139
|
+
// Second tick: condition still true, both re-evaluated
|
|
140
|
+
result = await seq.tick(context);
|
|
141
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
142
|
+
expect(conditionTicks).toBe(2); // Condition re-checked
|
|
143
|
+
expect(actionTicks).toBe(2);
|
|
144
|
+
|
|
145
|
+
// Third tick: condition becomes false, sequence fails
|
|
146
|
+
blackboard.set("shouldContinue", false);
|
|
147
|
+
result = await seq.tick(context);
|
|
148
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
149
|
+
expect(conditionTicks).toBe(3); // Condition re-checked again
|
|
150
|
+
expect(actionTicks).toBe(2); // Action not ticked (condition failed)
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("Edge Cases", () => {
|
|
155
|
+
it("should handle empty children array", async () => {
|
|
156
|
+
const seq = new ReactiveSequence({ id: "seq1" });
|
|
157
|
+
const result = await seq.tick(context);
|
|
158
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should handle single child", async () => {
|
|
162
|
+
const seq = new ReactiveSequence({ id: "seq1" });
|
|
163
|
+
const child = new SuccessNode({ id: "child1" });
|
|
164
|
+
seq.addChild(child);
|
|
165
|
+
|
|
166
|
+
const result = await seq.tick(context);
|
|
167
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactiveSequence node - Restarts from beginning each tick
|
|
3
|
+
* Responds to condition changes during execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ConfigurationError } from "../errors.js";
|
|
7
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
8
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
9
|
+
import { Sequence } from "./sequence.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ReactiveSequence restarts from the beginning on each tick.
|
|
13
|
+
* Unlike regular Sequence which remembers its position, ReactiveSequence
|
|
14
|
+
* re-evaluates all children from the start, making it responsive to
|
|
15
|
+
* conditions that might change between ticks.
|
|
16
|
+
*
|
|
17
|
+
* Use cases:
|
|
18
|
+
* - Real-time monitoring where conditions might change
|
|
19
|
+
* - Safety-critical checks that must be re-evaluated
|
|
20
|
+
* - Guard conditions that need constant verification
|
|
21
|
+
*/
|
|
22
|
+
export class ReactiveSequence extends Sequence {
|
|
23
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
24
|
+
this.log("Ticking (reactive - always starts from beginning)");
|
|
25
|
+
|
|
26
|
+
if (this._children.length === 0) {
|
|
27
|
+
return NodeStatus.SUCCESS;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Always start from child 0 (reactive behavior)
|
|
31
|
+
// Don't use currentChildIndex from parent Sequence
|
|
32
|
+
for (let i = 0; i < this._children.length; i++) {
|
|
33
|
+
// Check for cancellation before ticking each child
|
|
34
|
+
checkSignal(context.signal);
|
|
35
|
+
|
|
36
|
+
const child = this._children[i];
|
|
37
|
+
if (!child) {
|
|
38
|
+
throw new ConfigurationError(`Child at index ${i} is undefined`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.log(`Ticking child ${i}: ${child.name}`);
|
|
42
|
+
const childStatus = await child.tick(context);
|
|
43
|
+
|
|
44
|
+
switch (childStatus) {
|
|
45
|
+
case NodeStatus.SUCCESS:
|
|
46
|
+
this.log(`Child ${child.name} succeeded`);
|
|
47
|
+
// Continue to next child
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
case NodeStatus.FAILURE:
|
|
51
|
+
this.log(`Child ${child.name} failed - sequence fails`);
|
|
52
|
+
this._status = NodeStatus.FAILURE;
|
|
53
|
+
return NodeStatus.FAILURE;
|
|
54
|
+
|
|
55
|
+
case NodeStatus.RUNNING:
|
|
56
|
+
this.log(`Child ${child.name} is running`);
|
|
57
|
+
this._status = NodeStatus.RUNNING;
|
|
58
|
+
// Return RUNNING but don't save position - will restart next tick
|
|
59
|
+
return NodeStatus.RUNNING;
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
throw new Error(`Unexpected status from child: ${childStatus}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// All children succeeded
|
|
67
|
+
this.log("All children succeeded");
|
|
68
|
+
this._status = NodeStatus.SUCCESS;
|
|
69
|
+
return NodeStatus.SUCCESS;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Override to prevent parent Sequence from resetting currentChildIndex
|
|
74
|
+
* (ReactiveSequence doesn't use currentChildIndex)
|
|
75
|
+
*/
|
|
76
|
+
protected onReset(): void {
|
|
77
|
+
// Call BaseNode reset (skip Sequence reset)
|
|
78
|
+
this._status = NodeStatus.IDLE;
|
|
79
|
+
|
|
80
|
+
// Reset all children
|
|
81
|
+
for (const child of this._children) {
|
|
82
|
+
child.reset();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery composite configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { nodeConfigurationSchema } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for Recovery composite configuration
|
|
10
|
+
* Uses base schema only - try/catch/finally logic is in child structure
|
|
11
|
+
*/
|
|
12
|
+
export const recoveryConfigurationSchema = nodeConfigurationSchema;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validated Recovery configuration type
|
|
16
|
+
*/
|
|
17
|
+
export type ValidatedRecoveryConfiguration = z.infer<
|
|
18
|
+
typeof recoveryConfigurationSchema
|
|
19
|
+
>;
|