@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.
Files changed (203) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/CLAUDE.md +181 -0
  3. package/LICENSE +21 -0
  4. package/README.md +920 -0
  5. package/behaviour-tree-workflows-landing/index.html +16 -0
  6. package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
  7. package/behaviour-tree-workflows-landing/package.json +31 -0
  8. package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
  9. package/behaviour-tree-workflows-landing/src/App.css +103 -0
  10. package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
  11. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
  12. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
  13. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
  14. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
  15. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
  16. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
  17. package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
  18. package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
  19. package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
  20. package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
  21. package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
  22. package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
  23. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
  24. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
  25. package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
  26. package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
  27. package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
  28. package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
  29. package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
  30. package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
  31. package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
  32. package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
  33. package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
  34. package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
  35. package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
  36. package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
  37. package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
  38. package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
  39. package/demo-google-sheets.ts +181 -0
  40. package/demo-runtime-variables.ts +174 -0
  41. package/demo-template.ts +208 -0
  42. package/docs/ARCHITECTURE_SUMMARY.md +613 -0
  43. package/docs/NODE_REFERENCE.md +504 -0
  44. package/docs/README.md +53 -0
  45. package/docs/custom-nodes-architecture.md +826 -0
  46. package/docs/observability.md +175 -0
  47. package/docs/yaml-specification.md +990 -0
  48. package/examples/temporal/README.md +117 -0
  49. package/examples/temporal/activities.ts +373 -0
  50. package/examples/temporal/client.ts +115 -0
  51. package/examples/temporal/python-worker/activities.py +339 -0
  52. package/examples/temporal/python-worker/requirements.txt +12 -0
  53. package/examples/temporal/python-worker/worker.py +106 -0
  54. package/examples/temporal/worker.ts +66 -0
  55. package/examples/temporal/workflows.ts +6 -0
  56. package/examples/temporal/yaml-workflow-loader.ts +105 -0
  57. package/examples/yaml-test.ts +97 -0
  58. package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
  59. package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
  60. package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
  61. package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
  62. package/examples/yaml-workflows/05-order-processing.yaml +146 -0
  63. package/examples/yaml-workflows/06-activity-test.yaml +71 -0
  64. package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
  65. package/examples/yaml-workflows/08-file-processing.yaml +141 -0
  66. package/examples/yaml-workflows/09-http-request.yaml +137 -0
  67. package/examples/yaml-workflows/README.md +211 -0
  68. package/package.json +38 -0
  69. package/src/actions/code-execution.schema.ts +27 -0
  70. package/src/actions/code-execution.ts +218 -0
  71. package/src/actions/generate-file.test.ts +516 -0
  72. package/src/actions/generate-file.ts +166 -0
  73. package/src/actions/http-request.test.ts +784 -0
  74. package/src/actions/http-request.ts +228 -0
  75. package/src/actions/index.ts +20 -0
  76. package/src/actions/parse-file.test.ts +448 -0
  77. package/src/actions/parse-file.ts +139 -0
  78. package/src/actions/python-script.test.ts +439 -0
  79. package/src/actions/python-script.ts +154 -0
  80. package/src/base-node.test.ts +511 -0
  81. package/src/base-node.ts +605 -0
  82. package/src/behavior-tree.test.ts +431 -0
  83. package/src/behavior-tree.ts +283 -0
  84. package/src/blackboard.test.ts +222 -0
  85. package/src/blackboard.ts +192 -0
  86. package/src/composites/conditional.schema.ts +19 -0
  87. package/src/composites/conditional.test.ts +309 -0
  88. package/src/composites/conditional.ts +129 -0
  89. package/src/composites/for-each.schema.ts +23 -0
  90. package/src/composites/for-each.test.ts +254 -0
  91. package/src/composites/for-each.ts +132 -0
  92. package/src/composites/index.ts +15 -0
  93. package/src/composites/memory-sequence.schema.ts +19 -0
  94. package/src/composites/memory-sequence.test.ts +223 -0
  95. package/src/composites/memory-sequence.ts +98 -0
  96. package/src/composites/parallel.schema.ts +28 -0
  97. package/src/composites/parallel.test.ts +502 -0
  98. package/src/composites/parallel.ts +157 -0
  99. package/src/composites/reactive-sequence.schema.ts +19 -0
  100. package/src/composites/reactive-sequence.test.ts +170 -0
  101. package/src/composites/reactive-sequence.ts +85 -0
  102. package/src/composites/recovery.schema.ts +19 -0
  103. package/src/composites/recovery.test.ts +366 -0
  104. package/src/composites/recovery.ts +90 -0
  105. package/src/composites/selector.schema.ts +19 -0
  106. package/src/composites/selector.test.ts +387 -0
  107. package/src/composites/selector.ts +85 -0
  108. package/src/composites/sequence.schema.ts +19 -0
  109. package/src/composites/sequence.test.ts +337 -0
  110. package/src/composites/sequence.ts +72 -0
  111. package/src/composites/sub-tree.schema.ts +21 -0
  112. package/src/composites/sub-tree.test.ts +893 -0
  113. package/src/composites/sub-tree.ts +177 -0
  114. package/src/composites/while.schema.ts +24 -0
  115. package/src/composites/while.test.ts +381 -0
  116. package/src/composites/while.ts +149 -0
  117. package/src/data-store/index.ts +10 -0
  118. package/src/data-store/memory-store.ts +161 -0
  119. package/src/data-store/types.ts +94 -0
  120. package/src/debug/breakpoint.test.ts +47 -0
  121. package/src/debug/breakpoint.ts +30 -0
  122. package/src/debug/index.ts +17 -0
  123. package/src/debug/resume-point.test.ts +49 -0
  124. package/src/debug/resume-point.ts +29 -0
  125. package/src/decorators/delay.schema.ts +21 -0
  126. package/src/decorators/delay.test.ts +261 -0
  127. package/src/decorators/delay.ts +140 -0
  128. package/src/decorators/force-result.schema.ts +32 -0
  129. package/src/decorators/force-result.test.ts +133 -0
  130. package/src/decorators/force-result.ts +63 -0
  131. package/src/decorators/index.ts +13 -0
  132. package/src/decorators/invert.schema.ts +19 -0
  133. package/src/decorators/invert.test.ts +135 -0
  134. package/src/decorators/invert.ts +42 -0
  135. package/src/decorators/keep-running.schema.ts +20 -0
  136. package/src/decorators/keep-running.test.ts +105 -0
  137. package/src/decorators/keep-running.ts +49 -0
  138. package/src/decorators/precondition.schema.ts +19 -0
  139. package/src/decorators/precondition.test.ts +351 -0
  140. package/src/decorators/precondition.ts +139 -0
  141. package/src/decorators/repeat.schema.ts +21 -0
  142. package/src/decorators/repeat.test.ts +187 -0
  143. package/src/decorators/repeat.ts +94 -0
  144. package/src/decorators/run-once.schema.ts +19 -0
  145. package/src/decorators/run-once.test.ts +140 -0
  146. package/src/decorators/run-once.ts +61 -0
  147. package/src/decorators/soft-assert.schema.ts +19 -0
  148. package/src/decorators/soft-assert.test.ts +107 -0
  149. package/src/decorators/soft-assert.ts +68 -0
  150. package/src/decorators/timeout.schema.ts +21 -0
  151. package/src/decorators/timeout.test.ts +274 -0
  152. package/src/decorators/timeout.ts +159 -0
  153. package/src/errors.test.ts +63 -0
  154. package/src/errors.ts +34 -0
  155. package/src/events.test.ts +347 -0
  156. package/src/events.ts +183 -0
  157. package/src/index.ts +80 -0
  158. package/src/integrations/index.ts +30 -0
  159. package/src/integrations/integration-action.test.ts +571 -0
  160. package/src/integrations/integration-action.ts +233 -0
  161. package/src/integrations/piece-executor.ts +320 -0
  162. package/src/observability/execution-tracker.ts +320 -0
  163. package/src/observability/index.ts +23 -0
  164. package/src/observability/sinks.ts +138 -0
  165. package/src/observability/types.ts +130 -0
  166. package/src/registry-utils.ts +147 -0
  167. package/src/registry.test.ts +466 -0
  168. package/src/registry.ts +334 -0
  169. package/src/schemas/base.schema.ts +104 -0
  170. package/src/schemas/index.ts +223 -0
  171. package/src/schemas/integration.test.ts +238 -0
  172. package/src/schemas/tree-definition.schema.ts +170 -0
  173. package/src/schemas/validation.test.ts +146 -0
  174. package/src/schemas/validation.ts +122 -0
  175. package/src/scripting/index.ts +22 -0
  176. package/src/templates/template-loader.test.ts +281 -0
  177. package/src/templates/template-loader.ts +152 -0
  178. package/src/temporal-integration.test.ts +213 -0
  179. package/src/test-nodes.ts +259 -0
  180. package/src/types.ts +503 -0
  181. package/src/utilities/index.ts +17 -0
  182. package/src/utilities/log-message.test.ts +275 -0
  183. package/src/utilities/log-message.ts +134 -0
  184. package/src/utilities/regex-extract.test.ts +138 -0
  185. package/src/utilities/regex-extract.ts +108 -0
  186. package/src/utilities/variable-resolver.test.ts +416 -0
  187. package/src/utilities/variable-resolver.ts +318 -0
  188. package/src/utils/error-handler.test.ts +117 -0
  189. package/src/utils/error-handler.ts +48 -0
  190. package/src/utils/signal-check.test.ts +234 -0
  191. package/src/utils/signal-check.ts +140 -0
  192. package/src/yaml/errors.ts +143 -0
  193. package/src/yaml/index.ts +30 -0
  194. package/src/yaml/loader.ts +39 -0
  195. package/src/yaml/parser.ts +286 -0
  196. package/src/yaml/validation/semantic-validator.ts +196 -0
  197. package/templates/google-sheets/insert-row.yaml +76 -0
  198. package/templates/notification-sender.yaml +33 -0
  199. package/templates/order-validation.yaml +44 -0
  200. package/tsconfig.json +24 -0
  201. package/vitest.config.ts +25 -0
  202. package/workflows/order-processor.yaml +59 -0
  203. package/workflows/process-order-workflow.yaml +142 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * While node - Loop while condition is true
3
+ */
4
+
5
+ import { CompositeNode } from "../base-node.js";
6
+ import { ConfigurationError } from "../errors.js";
7
+ import {
8
+ type TemporalContext,
9
+ type NodeConfiguration,
10
+ NodeStatus,
11
+ type TreeNode,
12
+ } from "../types.js";
13
+ import { checkSignal } from "../utils/signal-check.js";
14
+
15
+ export interface WhileConfiguration extends NodeConfiguration {
16
+ maxIterations?: number; // Safety limit
17
+ }
18
+
19
+ /**
20
+ * While loops while the condition returns SUCCESS.
21
+ * Structure:
22
+ * - First child = condition
23
+ * - Second child = body
24
+ */
25
+ export class While extends CompositeNode {
26
+ private maxIterations: number;
27
+ private currentIteration: number = 0;
28
+ private condition?: TreeNode;
29
+ private body?: TreeNode;
30
+ private bodyStarted: boolean = false;
31
+
32
+ constructor(config: WhileConfiguration) {
33
+ super(config);
34
+ this.maxIterations = config.maxIterations ?? 1000;
35
+ }
36
+
37
+ addChild(child: TreeNode): void {
38
+ if (!this.condition) {
39
+ this.condition = child;
40
+ this._children.push(child);
41
+ child.parent = this;
42
+ } else if (!this.body) {
43
+ this.body = child;
44
+ this._children.push(child);
45
+ child.parent = this;
46
+ } else {
47
+ throw new ConfigurationError(
48
+ "While can have maximum 2 children (condition, body)",
49
+ );
50
+ }
51
+ }
52
+
53
+ async executeTick(context: TemporalContext): Promise<NodeStatus> {
54
+ if (!this.condition) {
55
+ throw new ConfigurationError("While requires a condition child");
56
+ }
57
+ if (!this.body) {
58
+ throw new ConfigurationError("While requires a body child");
59
+ }
60
+
61
+ this.log(
62
+ `Starting while loop (iteration ${this.currentIteration}/${this.maxIterations})`,
63
+ );
64
+
65
+ // Loop while condition is SUCCESS
66
+ while (this.currentIteration < this.maxIterations) {
67
+ // Check for cancellation before each iteration
68
+ checkSignal(context.signal);
69
+
70
+ // Only check condition if body hasn't started for this iteration
71
+ if (!this.bodyStarted) {
72
+ // Evaluate condition
73
+ this.log(`Evaluating condition (iteration ${this.currentIteration})`);
74
+ const conditionStatus = await this.condition.tick(context);
75
+
76
+ if (conditionStatus === NodeStatus.RUNNING) {
77
+ this.log("Condition is running");
78
+ this._status = NodeStatus.RUNNING;
79
+ return NodeStatus.RUNNING;
80
+ }
81
+
82
+ if (conditionStatus === NodeStatus.FAILURE) {
83
+ this.log("Condition failed - exiting loop");
84
+ this._status = NodeStatus.SUCCESS;
85
+ this.currentIteration = 0;
86
+ this.bodyStarted = false;
87
+ return NodeStatus.SUCCESS;
88
+ }
89
+
90
+ // Condition succeeded, mark body as started
91
+ this.bodyStarted = true;
92
+ } else {
93
+ this.log(
94
+ `Body already started for iteration ${this.currentIteration} - continuing execution`,
95
+ );
96
+ }
97
+
98
+ // Execute body
99
+ this.log(`Executing body (iteration ${this.currentIteration})`);
100
+ const bodyStatus = await this.body.tick(context);
101
+
102
+ switch (bodyStatus) {
103
+ case NodeStatus.SUCCESS:
104
+ this.log("Body succeeded - continuing loop");
105
+ this.currentIteration++;
106
+ this.bodyStarted = false; // Reset for next iteration
107
+ this.condition.reset(); // Reset for next iteration
108
+ this.body.reset();
109
+ break;
110
+
111
+ case NodeStatus.FAILURE:
112
+ this.log("Body failed - While fails");
113
+ this._status = NodeStatus.FAILURE;
114
+ this.currentIteration = 0;
115
+ this.bodyStarted = false;
116
+ return NodeStatus.FAILURE;
117
+
118
+ case NodeStatus.RUNNING:
119
+ this.log("Body is running");
120
+ this._status = NodeStatus.RUNNING;
121
+ return NodeStatus.RUNNING;
122
+
123
+ default:
124
+ throw new Error(`Unexpected status from body: ${bodyStatus}`);
125
+ }
126
+ }
127
+
128
+ // Max iterations reached
129
+ this.log(`Max iterations (${this.maxIterations}) reached`);
130
+ this._status = NodeStatus.FAILURE;
131
+ this.currentIteration = 0;
132
+ this.bodyStarted = false;
133
+ return NodeStatus.FAILURE;
134
+ }
135
+
136
+ protected onReset(): void {
137
+ super.onReset();
138
+ this.log("Resetting - clearing body started flag");
139
+ this.currentIteration = 0;
140
+ this.bodyStarted = false;
141
+ }
142
+
143
+ protected onHalt(): void {
144
+ super.onHalt();
145
+ this.log("Halting - clearing body started flag");
146
+ this.currentIteration = 0;
147
+ this.bodyStarted = false;
148
+ }
149
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * DataStore module exports
3
+ *
4
+ * Provides interfaces and implementations for large data storage
5
+ * outside workflow execution context.
6
+ */
7
+
8
+ export type { DataStore, DataRef, PutOptions } from "./types.js";
9
+ export { isDataRef } from "./types.js";
10
+ export { MemoryDataStore } from "./memory-store.js";
@@ -0,0 +1,161 @@
1
+ /**
2
+ * In-Memory DataStore Implementation
3
+ *
4
+ * Simple in-memory storage for testing and development.
5
+ * Data is lost when the process terminates.
6
+ *
7
+ * Features:
8
+ * - TTL support with automatic cleanup
9
+ * - Size tracking
10
+ * - Thread-safe for single Node.js process
11
+ */
12
+
13
+ import type { DataStore, DataRef, PutOptions } from "./types.js";
14
+
15
+ interface StoredEntry {
16
+ data: unknown;
17
+ sizeBytes: number;
18
+ expiresAt?: number;
19
+ }
20
+
21
+ /**
22
+ * In-memory DataStore implementation
23
+ * Suitable for tests and local development
24
+ */
25
+ export class MemoryDataStore implements DataStore {
26
+ private storage = new Map<string, StoredEntry>();
27
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
28
+
29
+ constructor(options?: { cleanupIntervalMs?: number }) {
30
+ // Start periodic cleanup for expired entries
31
+ const cleanupMs = options?.cleanupIntervalMs ?? 60000; // 1 minute default
32
+ if (cleanupMs > 0) {
33
+ this.cleanupInterval = setInterval(() => this.cleanup(), cleanupMs);
34
+ // Don't prevent process exit
35
+ if (this.cleanupInterval.unref) {
36
+ this.cleanupInterval.unref();
37
+ }
38
+ }
39
+ }
40
+
41
+ async put(key: string, data: unknown, options?: PutOptions): Promise<DataRef> {
42
+ const serialized = JSON.stringify(data);
43
+ const sizeBytes = Buffer.byteLength(serialized, "utf8");
44
+
45
+ const entry: StoredEntry = {
46
+ data,
47
+ sizeBytes,
48
+ };
49
+
50
+ if (options?.ttlSeconds) {
51
+ entry.expiresAt = Date.now() + options.ttlSeconds * 1000;
52
+ }
53
+
54
+ this.storage.set(key, entry);
55
+
56
+ return {
57
+ store: "memory",
58
+ key,
59
+ sizeBytes,
60
+ expiresAt: entry.expiresAt,
61
+ };
62
+ }
63
+
64
+ async get(ref: DataRef): Promise<unknown> {
65
+ if (ref.store !== "memory") {
66
+ throw new Error(`MemoryDataStore cannot retrieve from store: ${ref.store}`);
67
+ }
68
+
69
+ const entry = this.storage.get(ref.key);
70
+
71
+ if (!entry) {
72
+ throw new Error(`Data not found for key: ${ref.key}`);
73
+ }
74
+
75
+ // Check expiration
76
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
77
+ this.storage.delete(ref.key);
78
+ throw new Error(`Data expired for key: ${ref.key}`);
79
+ }
80
+
81
+ // Return a deep clone to prevent accidental mutation
82
+ return structuredClone(entry.data);
83
+ }
84
+
85
+ async delete(ref: DataRef): Promise<void> {
86
+ if (ref.store !== "memory") {
87
+ throw new Error(`MemoryDataStore cannot delete from store: ${ref.store}`);
88
+ }
89
+
90
+ this.storage.delete(ref.key);
91
+ }
92
+
93
+ async exists(ref: DataRef): Promise<boolean> {
94
+ if (ref.store !== "memory") {
95
+ return false;
96
+ }
97
+
98
+ const entry = this.storage.get(ref.key);
99
+
100
+ if (!entry) {
101
+ return false;
102
+ }
103
+
104
+ // Check expiration
105
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
106
+ this.storage.delete(ref.key);
107
+ return false;
108
+ }
109
+
110
+ return true;
111
+ }
112
+
113
+ /**
114
+ * Clear all stored data
115
+ * Useful for test cleanup
116
+ */
117
+ clear(): void {
118
+ this.storage.clear();
119
+ }
120
+
121
+ /**
122
+ * Get the number of stored entries
123
+ */
124
+ size(): number {
125
+ return this.storage.size;
126
+ }
127
+
128
+ /**
129
+ * Get total bytes stored
130
+ */
131
+ totalBytes(): number {
132
+ let total = 0;
133
+ for (const entry of this.storage.values()) {
134
+ total += entry.sizeBytes;
135
+ }
136
+ return total;
137
+ }
138
+
139
+ /**
140
+ * Stop the cleanup interval
141
+ * Call this when done with the store to prevent memory leaks in tests
142
+ */
143
+ dispose(): void {
144
+ if (this.cleanupInterval) {
145
+ clearInterval(this.cleanupInterval);
146
+ this.cleanupInterval = null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Remove expired entries
152
+ */
153
+ private cleanup(): void {
154
+ const now = Date.now();
155
+ for (const [key, entry] of this.storage.entries()) {
156
+ if (entry.expiresAt && now > entry.expiresAt) {
157
+ this.storage.delete(key);
158
+ }
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * DataStore Interface
3
+ *
4
+ * Provides an abstraction for storing and retrieving large data payloads
5
+ * outside the workflow execution context. This keeps the Temporal workflow
6
+ * history lean while allowing workflows to process large datasets.
7
+ *
8
+ * Implementations:
9
+ * - MemoryDataStore: In-memory storage for tests
10
+ * - GCSDataStore: Google Cloud Storage for production (in controlplane)
11
+ * - RedisDataStore: Redis storage for caching (future)
12
+ */
13
+
14
+ /**
15
+ * Reference to data stored in a DataStore
16
+ * This lightweight reference can be passed through workflows/activities
17
+ */
18
+ export interface DataRef {
19
+ /** Storage backend identifier */
20
+ store: "gcs" | "s3" | "redis" | "memory";
21
+ /** Unique key for retrieving the data */
22
+ key: string;
23
+ /** Size of stored data in bytes (for monitoring/optimization) */
24
+ sizeBytes?: number;
25
+ /** Unix timestamp when data expires (optional TTL) */
26
+ expiresAt?: number;
27
+ }
28
+
29
+ /**
30
+ * Options for storing data
31
+ */
32
+ export interface PutOptions {
33
+ /** Time-to-live in seconds (data auto-deleted after expiry) */
34
+ ttlSeconds?: number;
35
+ /** Content type hint for serialization */
36
+ contentType?: "json" | "csv" | "binary";
37
+ /** Associated workflow ID for grouping/cleanup */
38
+ workflowId?: string;
39
+ }
40
+
41
+ /**
42
+ * DataStore interface for storing and retrieving large payloads
43
+ */
44
+ export interface DataStore {
45
+ /**
46
+ * Store data and return a reference
47
+ * @param key - Unique identifier for the data
48
+ * @param data - Data to store (will be JSON serialized)
49
+ * @param options - Storage options (TTL, content type, etc.)
50
+ * @returns Reference to the stored data
51
+ */
52
+ put(key: string, data: unknown, options?: PutOptions): Promise<DataRef>;
53
+
54
+ /**
55
+ * Retrieve data by reference
56
+ * @param ref - Reference returned from put()
57
+ * @returns The stored data (JSON deserialized)
58
+ * @throws Error if data not found or expired
59
+ */
60
+ get(ref: DataRef): Promise<unknown>;
61
+
62
+ /**
63
+ * Delete data by reference
64
+ * @param ref - Reference to delete
65
+ */
66
+ delete(ref: DataRef): Promise<void>;
67
+
68
+ /**
69
+ * Check if data exists
70
+ * @param ref - Reference to check
71
+ * @returns true if data exists and hasn't expired
72
+ */
73
+ exists(ref: DataRef): Promise<boolean>;
74
+ }
75
+
76
+ /**
77
+ * Type guard to check if a value is a DataRef
78
+ * @param value - Value to check
79
+ * @returns true if value is a DataRef
80
+ */
81
+ export function isDataRef(value: unknown): value is DataRef {
82
+ if (typeof value !== "object" || value === null) {
83
+ return false;
84
+ }
85
+
86
+ const obj = value as Record<string, unknown>;
87
+
88
+ return (
89
+ typeof obj.store === "string" &&
90
+ ["gcs", "s3", "redis", "memory"].includes(obj.store) &&
91
+ typeof obj.key === "string" &&
92
+ obj.key.length > 0
93
+ );
94
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Tests for Breakpoint node
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+ import { ScopedBlackboard } from "../blackboard.js";
7
+ import { Registry } from "../registry.js";
8
+ import { NodeStatus, type TemporalContext } from "../types.js";
9
+ import { Breakpoint } from "./breakpoint.js";
10
+
11
+ describe("Breakpoint", () => {
12
+ const createContext = (): TemporalContext => ({
13
+ blackboard: new ScopedBlackboard(),
14
+ treeRegistry: new Registry(),
15
+ signal: new AbortController().signal,
16
+ timestamp: Date.now(),
17
+ });
18
+
19
+ it("should return RUNNING to signal pause", async () => {
20
+ const node = new Breakpoint({ id: "test-break" });
21
+ const context = createContext();
22
+
23
+ const result = await node.tick(context);
24
+
25
+ expect(result).toBe(NodeStatus.RUNNING);
26
+ expect(node.status()).toBe(NodeStatus.RUNNING);
27
+ });
28
+
29
+ it("should store the breakpointId", () => {
30
+ const node = new Breakpoint({ id: "my-breakpoint" });
31
+
32
+ expect(node.breakpointId).toBe("my-breakpoint");
33
+ expect(node.id).toBe("breakpoint-my-breakpoint");
34
+ });
35
+
36
+ it("should consistently return RUNNING", async () => {
37
+ const node = new Breakpoint({ id: "pause-here" });
38
+ const context = createContext();
39
+
40
+ // Execute multiple times - should always return RUNNING
41
+ const result1 = await node.tick(context);
42
+ const result2 = await node.tick(context);
43
+
44
+ expect(result1).toBe(NodeStatus.RUNNING);
45
+ expect(result2).toBe(NodeStatus.RUNNING);
46
+ });
47
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Breakpoint - Pauses execution without failure
3
+ * Returns RUNNING to signal pause (not FAILURE)
4
+ */
5
+
6
+ import { ActionNode } from "../base-node.js";
7
+ import { type TemporalContext, NodeStatus } from "../types.js";
8
+
9
+ export interface BreakpointConfig {
10
+ id: string;
11
+ }
12
+
13
+ /**
14
+ * Breakpoint pauses execution without marking it as a failure.
15
+ * Returns RUNNING to signal that execution should pause here.
16
+ * The session can then be resumed from this point.
17
+ */
18
+ export class Breakpoint extends ActionNode {
19
+ readonly breakpointId: string;
20
+
21
+ constructor(config: BreakpointConfig) {
22
+ super({ id: `breakpoint-${config.id}` });
23
+ this.breakpointId = config.id;
24
+ }
25
+
26
+ async executeTick(_context: TemporalContext): Promise<NodeStatus> {
27
+ this._status = NodeStatus.RUNNING;
28
+ return NodeStatus.RUNNING;
29
+ }
30
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Debug nodes for behavior tree debugging and resume functionality
3
+ */
4
+
5
+ // Breakpoint is not exported - it's still in development
6
+ // The current implementation causes infinite loops with tickWhileRunning
7
+ // See docs/backlog/DEBUG_MODE.md for the full implementation plan
8
+ // export {
9
+ // Breakpoint,
10
+ // BreakpointSchema,
11
+ // type BreakpointConfig,
12
+ // } from "./breakpoint.js";
13
+
14
+ export {
15
+ ResumePoint,
16
+ type ResumePointConfig,
17
+ } from "./resume-point.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tests for ResumePoint node
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+ import { ScopedBlackboard } from "../blackboard.js";
7
+ import { Registry } from "../registry.js";
8
+ import { NodeStatus, type TemporalContext } from "../types.js";
9
+ import { ResumePoint } from "./resume-point.js";
10
+
11
+ describe("ResumePoint", () => {
12
+ const createContext = (): TemporalContext => ({
13
+ blackboard: new ScopedBlackboard(),
14
+ treeRegistry: new Registry(),
15
+ signal: new AbortController().signal,
16
+ timestamp: Date.now(),
17
+ });
18
+
19
+ it("should always return SUCCESS", async () => {
20
+ const node = new ResumePoint({ id: "test-resume" });
21
+ const context = createContext();
22
+
23
+ const result = await node.tick(context);
24
+
25
+ expect(result).toBe(NodeStatus.SUCCESS);
26
+ expect(node.status()).toBe(NodeStatus.SUCCESS);
27
+ });
28
+
29
+ it("should store the resumePointId", () => {
30
+ const node = new ResumePoint({ id: "my-custom-id" });
31
+
32
+ expect(node.resumePointId).toBe("my-custom-id");
33
+ expect(node.id).toBe("resume-point-my-custom-id");
34
+ });
35
+
36
+ it("should not affect tree execution flow", async () => {
37
+ const node = new ResumePoint({ id: "marker" });
38
+ const context = createContext();
39
+
40
+ // Execute multiple times - should always succeed
41
+ const result1 = await node.tick(context);
42
+ const result2 = await node.tick(context);
43
+ const result3 = await node.tick(context);
44
+
45
+ expect(result1).toBe(NodeStatus.SUCCESS);
46
+ expect(result2).toBe(NodeStatus.SUCCESS);
47
+ expect(result3).toBe(NodeStatus.SUCCESS);
48
+ });
49
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ResumePoint - Marker node for resume location
3
+ * Always returns SUCCESS, used purely as a findable marker for LLM-based resume
4
+ */
5
+
6
+ import { ActionNode } from "../base-node.js";
7
+ import { type TemporalContext, NodeStatus } from "../types.js";
8
+
9
+ export interface ResumePointConfig {
10
+ id: string;
11
+ }
12
+
13
+ /**
14
+ * ResumePoint is a marker node that LLM agents can insert to indicate
15
+ * where execution should resume from. It always returns SUCCESS.
16
+ */
17
+ export class ResumePoint extends ActionNode {
18
+ readonly resumePointId: string;
19
+
20
+ constructor(config: ResumePointConfig) {
21
+ super({ id: `resume-point-${config.id}` });
22
+ this.resumePointId = config.id;
23
+ }
24
+
25
+ async executeTick(_context: TemporalContext): Promise<NodeStatus> {
26
+ this._status = NodeStatus.SUCCESS;
27
+ return NodeStatus.SUCCESS;
28
+ }
29
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Delay decorator configuration schema
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { createNodeSchema, validations } from "../schemas/base.schema.js";
7
+
8
+ /**
9
+ * Schema for Delay decorator configuration
10
+ * Validates that delayMs is non-negative (can be 0)
11
+ */
12
+ export const delayConfigurationSchema = createNodeSchema("Delay", {
13
+ delayMs: validations.nonNegativeNumber("delayMs"),
14
+ });
15
+
16
+ /**
17
+ * Validated Delay configuration type
18
+ */
19
+ export type ValidatedDelayConfiguration = z.infer<
20
+ typeof delayConfigurationSchema
21
+ >;