@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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repeat decorator - Execute child N times
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DecoratorNode } from "../base-node.js";
|
|
6
|
+
import { ConfigurationError } from "../errors.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
|
+
export interface RepeatConfiguration extends NodeConfiguration {
|
|
15
|
+
numCycles: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Repeat executes its child exactly N times.
|
|
20
|
+
* Returns SUCCESS when all cycles complete successfully.
|
|
21
|
+
* Returns FAILURE if any cycle fails.
|
|
22
|
+
*/
|
|
23
|
+
export class Repeat extends DecoratorNode {
|
|
24
|
+
private numCycles: number;
|
|
25
|
+
private currentCycle: number = 0;
|
|
26
|
+
|
|
27
|
+
constructor(config: RepeatConfiguration) {
|
|
28
|
+
super(config);
|
|
29
|
+
this.numCycles = config.numCycles;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
33
|
+
checkSignal(context.signal);
|
|
34
|
+
|
|
35
|
+
if (!this.child) {
|
|
36
|
+
throw new ConfigurationError("Repeat requires a child");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.log(`Repeat cycle ${this.currentCycle}/${this.numCycles}`);
|
|
40
|
+
|
|
41
|
+
// Check if we've completed all cycles
|
|
42
|
+
if (this.currentCycle >= this.numCycles) {
|
|
43
|
+
this.log("All cycles completed");
|
|
44
|
+
this._status = NodeStatus.SUCCESS;
|
|
45
|
+
this.currentCycle = 0; // Reset for next run
|
|
46
|
+
return NodeStatus.SUCCESS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Tick child
|
|
50
|
+
const result = await this.child.tick(context);
|
|
51
|
+
|
|
52
|
+
switch (result) {
|
|
53
|
+
case NodeStatus.SUCCESS:
|
|
54
|
+
this.log(`Cycle ${this.currentCycle} succeeded`);
|
|
55
|
+
this.currentCycle++;
|
|
56
|
+
|
|
57
|
+
// Check if more cycles remain
|
|
58
|
+
if (this.currentCycle < this.numCycles) {
|
|
59
|
+
this.child.reset(); // Reset for next cycle
|
|
60
|
+
this._status = NodeStatus.RUNNING;
|
|
61
|
+
return NodeStatus.RUNNING;
|
|
62
|
+
} else {
|
|
63
|
+
// All cycles complete - don't reset after final cycle
|
|
64
|
+
this._status = NodeStatus.SUCCESS;
|
|
65
|
+
this.currentCycle = 0;
|
|
66
|
+
return NodeStatus.SUCCESS;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case NodeStatus.FAILURE:
|
|
70
|
+
this.log(`Cycle ${this.currentCycle} failed`);
|
|
71
|
+
this._status = NodeStatus.FAILURE;
|
|
72
|
+
this.currentCycle = 0;
|
|
73
|
+
return NodeStatus.FAILURE;
|
|
74
|
+
|
|
75
|
+
case NodeStatus.RUNNING:
|
|
76
|
+
this.log(`Cycle ${this.currentCycle} is running`);
|
|
77
|
+
this._status = NodeStatus.RUNNING;
|
|
78
|
+
return NodeStatus.RUNNING;
|
|
79
|
+
|
|
80
|
+
default:
|
|
81
|
+
throw new Error(`Unexpected status from child: ${result}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected onReset(): void {
|
|
86
|
+
super.onReset();
|
|
87
|
+
this.currentCycle = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected onHalt(): void {
|
|
91
|
+
super.onHalt();
|
|
92
|
+
this.currentCycle = 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunOnce decorator configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { nodeConfigurationSchema } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for RunOnce decorator configuration
|
|
10
|
+
* Uses base schema only (no additional properties)
|
|
11
|
+
*/
|
|
12
|
+
export const runOnceConfigurationSchema = nodeConfigurationSchema;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validated RunOnce configuration type
|
|
16
|
+
*/
|
|
17
|
+
export type ValidatedRunOnceConfiguration = z.infer<
|
|
18
|
+
typeof runOnceConfigurationSchema
|
|
19
|
+
>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for RunOnce 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 { RunOnce } from "./run-once.js";
|
|
11
|
+
|
|
12
|
+
describe("RunOnce", () => {
|
|
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 once", async () => {
|
|
26
|
+
const runOnce = new RunOnce({ id: "once1" });
|
|
27
|
+
|
|
28
|
+
let tickCount = 0;
|
|
29
|
+
class CountingNode extends SuccessNode {
|
|
30
|
+
async tick(context: TemporalContext) {
|
|
31
|
+
tickCount++;
|
|
32
|
+
return await super.tick(context);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
runOnce.setChild(new CountingNode({ id: "child" }));
|
|
37
|
+
|
|
38
|
+
await runOnce.tick(context);
|
|
39
|
+
await runOnce.tick(context);
|
|
40
|
+
await runOnce.tick(context);
|
|
41
|
+
|
|
42
|
+
expect(tickCount).toBe(1); // Only executed once
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should cache SUCCESS result", async () => {
|
|
46
|
+
const runOnce = new RunOnce({ id: "once1" });
|
|
47
|
+
runOnce.setChild(new SuccessNode({ id: "child" }));
|
|
48
|
+
|
|
49
|
+
const result1 = await runOnce.tick(context);
|
|
50
|
+
const result2 = await runOnce.tick(context);
|
|
51
|
+
const result3 = await runOnce.tick(context);
|
|
52
|
+
|
|
53
|
+
expect(result1).toBe(NodeStatus.SUCCESS);
|
|
54
|
+
expect(result2).toBe(NodeStatus.SUCCESS);
|
|
55
|
+
expect(result3).toBe(NodeStatus.SUCCESS);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should cache FAILURE result", async () => {
|
|
59
|
+
const runOnce = new RunOnce({ id: "once1" });
|
|
60
|
+
runOnce.setChild(new FailureNode({ id: "child" }));
|
|
61
|
+
|
|
62
|
+
const result1 = await runOnce.tick(context);
|
|
63
|
+
const result2 = await runOnce.tick(context);
|
|
64
|
+
|
|
65
|
+
expect(result1).toBe(NodeStatus.FAILURE);
|
|
66
|
+
expect(result2).toBe(NodeStatus.FAILURE);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should not cache RUNNING result", async () => {
|
|
70
|
+
const runOnce = new RunOnce({ id: "once1" });
|
|
71
|
+
|
|
72
|
+
let tickCount = 0;
|
|
73
|
+
class RunningThenSuccess extends SuccessNode {
|
|
74
|
+
async tick(context: TemporalContext) {
|
|
75
|
+
tickCount++;
|
|
76
|
+
if (tickCount < 3) {
|
|
77
|
+
this._status = NodeStatus.RUNNING;
|
|
78
|
+
return NodeStatus.RUNNING;
|
|
79
|
+
}
|
|
80
|
+
return await super.tick(context);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
runOnce.setChild(new RunningThenSuccess({ id: "child" }));
|
|
85
|
+
|
|
86
|
+
const result1 = await runOnce.tick(context); // RUNNING
|
|
87
|
+
const result2 = await runOnce.tick(context); // RUNNING
|
|
88
|
+
const result3 = await runOnce.tick(context); // SUCCESS (cached)
|
|
89
|
+
const result4 = await runOnce.tick(context); // SUCCESS (from cache)
|
|
90
|
+
|
|
91
|
+
expect(result1).toBe(NodeStatus.RUNNING);
|
|
92
|
+
expect(result2).toBe(NodeStatus.RUNNING);
|
|
93
|
+
expect(result3).toBe(NodeStatus.SUCCESS);
|
|
94
|
+
expect(result4).toBe(NodeStatus.SUCCESS);
|
|
95
|
+
expect(tickCount).toBe(3); // Ticked 3 times (not cached while RUNNING)
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should reset cache on reset", async () => {
|
|
99
|
+
const runOnce = new RunOnce({ id: "once1" });
|
|
100
|
+
|
|
101
|
+
let tickCount = 0;
|
|
102
|
+
class CountingNode extends SuccessNode {
|
|
103
|
+
async tick(context: TemporalContext) {
|
|
104
|
+
tickCount++;
|
|
105
|
+
return await super.tick(context);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
runOnce.setChild(new CountingNode({ id: "child" }));
|
|
110
|
+
|
|
111
|
+
// First execution
|
|
112
|
+
await runOnce.tick(context);
|
|
113
|
+
expect(tickCount).toBe(1);
|
|
114
|
+
|
|
115
|
+
// Second tick - should use cache
|
|
116
|
+
await runOnce.tick(context);
|
|
117
|
+
expect(tickCount).toBe(1);
|
|
118
|
+
|
|
119
|
+
// Reset
|
|
120
|
+
runOnce.reset();
|
|
121
|
+
|
|
122
|
+
// Third tick - should execute again
|
|
123
|
+
await runOnce.tick(context);
|
|
124
|
+
expect(tickCount).toBe(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should propagate ConfigurationError if no child", async () => {
|
|
128
|
+
const runOnce = new RunOnce({ id: "once1" });
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await runOnce.tick(context);
|
|
132
|
+
expect.fail("Should have thrown an error");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
135
|
+
expect((error as ConfigurationError).message).toContain(
|
|
136
|
+
"RunOnce requires a child",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunOnce decorator - Execute child only once per session
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DecoratorNode } from "../base-node.js";
|
|
6
|
+
import { ConfigurationError } from "../errors.js";
|
|
7
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
8
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RunOnce executes its child only once and caches the result.
|
|
12
|
+
* Subsequent ticks return the cached result without re-executing the child.
|
|
13
|
+
* Useful for initialization or one-time setup operations.
|
|
14
|
+
*/
|
|
15
|
+
export class RunOnce extends DecoratorNode {
|
|
16
|
+
private hasRun: boolean = false;
|
|
17
|
+
private cachedResult?: NodeStatus;
|
|
18
|
+
|
|
19
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
20
|
+
checkSignal(context.signal);
|
|
21
|
+
|
|
22
|
+
if (!this.child) {
|
|
23
|
+
throw new ConfigurationError("RunOnce requires a child");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Return cached result if already executed
|
|
27
|
+
if (this.hasRun) {
|
|
28
|
+
this.log(
|
|
29
|
+
`Already executed, returning cached result: ${this.cachedResult}`,
|
|
30
|
+
);
|
|
31
|
+
if (this.cachedResult === undefined) {
|
|
32
|
+
this._status = NodeStatus.RUNNING;
|
|
33
|
+
return NodeStatus.RUNNING;
|
|
34
|
+
}
|
|
35
|
+
this._status = this.cachedResult;
|
|
36
|
+
return this.cachedResult;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Execute child for the first time
|
|
40
|
+
this.log("First execution - ticking child");
|
|
41
|
+
const result = await this.child.tick(context);
|
|
42
|
+
|
|
43
|
+
// Cache result only if not RUNNING
|
|
44
|
+
if (result !== NodeStatus.RUNNING) {
|
|
45
|
+
this.hasRun = true;
|
|
46
|
+
this.cachedResult = result;
|
|
47
|
+
this.log(`Caching result: ${result}`);
|
|
48
|
+
} else {
|
|
49
|
+
this.log("Child is running - will retry on next tick");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this._status = result;
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected onReset(): void {
|
|
57
|
+
super.onReset();
|
|
58
|
+
this.hasRun = false;
|
|
59
|
+
this.cachedResult = undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SoftAssert decorator configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { nodeConfigurationSchema } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for SoftAssert decorator configuration
|
|
10
|
+
* Uses base schema only (no additional properties)
|
|
11
|
+
*/
|
|
12
|
+
export const softAssertConfigurationSchema = nodeConfigurationSchema;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validated SoftAssert configuration type
|
|
16
|
+
*/
|
|
17
|
+
export type ValidatedSoftAssertConfiguration = z.infer<
|
|
18
|
+
typeof softAssertConfigurationSchema
|
|
19
|
+
>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SoftAssert 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, RunningNode, SuccessNode } from "../test-nodes.js";
|
|
9
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
10
|
+
import { SoftAssert } from "./soft-assert.js";
|
|
11
|
+
|
|
12
|
+
describe("SoftAssert", () => {
|
|
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 convert FAILURE to SUCCESS", async () => {
|
|
26
|
+
const soft = new SoftAssert({ id: "soft1" });
|
|
27
|
+
soft.setChild(new FailureNode({ id: "child" }));
|
|
28
|
+
|
|
29
|
+
const result = await soft.tick(context);
|
|
30
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should propagate SUCCESS", async () => {
|
|
34
|
+
const soft = new SoftAssert({ id: "soft1" });
|
|
35
|
+
soft.setChild(new SuccessNode({ id: "child" }));
|
|
36
|
+
|
|
37
|
+
const result = await soft.tick(context);
|
|
38
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should propagate RUNNING", async () => {
|
|
42
|
+
const soft = new SoftAssert({ id: "soft1" });
|
|
43
|
+
soft.setChild(new RunningNode({ id: "child" }));
|
|
44
|
+
|
|
45
|
+
const result = await soft.tick(context);
|
|
46
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should log failures", async () => {
|
|
50
|
+
const soft = new SoftAssert({ id: "soft1" });
|
|
51
|
+
soft.setChild(new FailureNode({ id: "child" }));
|
|
52
|
+
|
|
53
|
+
await soft.tick(context);
|
|
54
|
+
|
|
55
|
+
const failures = soft.getFailures();
|
|
56
|
+
expect(failures).toHaveLength(1);
|
|
57
|
+
expect(failures[0]?.message).toContain("Soft assertion failed");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should track multiple failures", async () => {
|
|
61
|
+
const soft = new SoftAssert({ id: "soft1" });
|
|
62
|
+
soft.setChild(new FailureNode({ id: "child" }));
|
|
63
|
+
|
|
64
|
+
await soft.tick(context);
|
|
65
|
+
await soft.tick(context);
|
|
66
|
+
await soft.tick(context);
|
|
67
|
+
|
|
68
|
+
const failures = soft.getFailures();
|
|
69
|
+
expect(failures).toHaveLength(3);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should reset failure history on reset", async () => {
|
|
73
|
+
const soft = new SoftAssert({ id: "soft1" });
|
|
74
|
+
soft.setChild(new FailureNode({ id: "child" }));
|
|
75
|
+
|
|
76
|
+
await soft.tick(context);
|
|
77
|
+
await soft.tick(context);
|
|
78
|
+
expect(soft.hasFailures()).toBe(true);
|
|
79
|
+
|
|
80
|
+
soft.reset();
|
|
81
|
+
expect(soft.hasFailures()).toBe(false);
|
|
82
|
+
expect(soft.getFailures()).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should provide hasFailures helper", async () => {
|
|
86
|
+
const soft = new SoftAssert({ id: "soft1" });
|
|
87
|
+
soft.setChild(new FailureNode({ id: "child" }));
|
|
88
|
+
|
|
89
|
+
expect(soft.hasFailures()).toBe(false);
|
|
90
|
+
await soft.tick(context);
|
|
91
|
+
expect(soft.hasFailures()).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should propagate ConfigurationError if no child", async () => {
|
|
95
|
+
const soft = new SoftAssert({ id: "soft1" });
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await soft.tick(context);
|
|
99
|
+
expect.fail("Should have thrown an error");
|
|
100
|
+
} catch (error) {
|
|
101
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
102
|
+
expect((error as ConfigurationError).message).toContain(
|
|
103
|
+
"SoftAssert requires a child",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SoftAssert decorator - Continue even if child fails
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DecoratorNode } from "../base-node.js";
|
|
6
|
+
import { ConfigurationError } from "../errors.js";
|
|
7
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
8
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SoftAssert converts child FAILURE to SUCCESS, allowing execution to continue.
|
|
12
|
+
* Logs all failures for later review but doesn't halt execution.
|
|
13
|
+
* Useful for non-critical checks that shouldn't block the test.
|
|
14
|
+
*/
|
|
15
|
+
export class SoftAssert extends DecoratorNode {
|
|
16
|
+
private failures: Array<{
|
|
17
|
+
timestamp: number;
|
|
18
|
+
message: string;
|
|
19
|
+
}> = [];
|
|
20
|
+
|
|
21
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
22
|
+
checkSignal(context.signal);
|
|
23
|
+
|
|
24
|
+
if (!this.child) {
|
|
25
|
+
throw new ConfigurationError("SoftAssert requires a child");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await this.child.tick(context);
|
|
29
|
+
|
|
30
|
+
if (result === NodeStatus.FAILURE) {
|
|
31
|
+
// Log failure
|
|
32
|
+
const failure = {
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
message: `Soft assertion failed: ${this.child.name}`,
|
|
35
|
+
};
|
|
36
|
+
this.failures.push(failure);
|
|
37
|
+
|
|
38
|
+
this.log(`Soft assertion failed (continuing): ${this.child.name}`);
|
|
39
|
+
|
|
40
|
+
// Convert FAILURE to SUCCESS
|
|
41
|
+
this._status = NodeStatus.SUCCESS;
|
|
42
|
+
return NodeStatus.SUCCESS;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Propagate SUCCESS or RUNNING as-is
|
|
46
|
+
this._status = result;
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all recorded failures
|
|
52
|
+
*/
|
|
53
|
+
getFailures(): Array<{ timestamp: number; message: string }> {
|
|
54
|
+
return [...this.failures];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if any assertions have failed
|
|
59
|
+
*/
|
|
60
|
+
hasFailures(): boolean {
|
|
61
|
+
return this.failures.length > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected onReset(): void {
|
|
65
|
+
super.onReset();
|
|
66
|
+
this.failures = [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeout decorator configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createNodeSchema, validations } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for Timeout decorator configuration
|
|
10
|
+
* Validates that timeoutMs is a positive number
|
|
11
|
+
*/
|
|
12
|
+
export const timeoutConfigurationSchema = createNodeSchema("Timeout", {
|
|
13
|
+
timeoutMs: validations.positiveNumber("timeoutMs"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validated Timeout configuration type
|
|
18
|
+
*/
|
|
19
|
+
export type ValidatedTimeoutConfiguration = z.infer<
|
|
20
|
+
typeof timeoutConfigurationSchema
|
|
21
|
+
>;
|