@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,613 @@
1
+ # btree Architecture Summary
2
+
3
+ ## Overview
4
+
5
+ btree is a behavior tree library for TypeScript, designed for AI-native workflows with native Temporal integration.
6
+
7
+ ---
8
+
9
+ ## Node Categories
10
+
11
+ ### 1. Pure Control Flow Nodes - Run in Temporal Sandbox
12
+
13
+ | Category | Nodes | Purpose |
14
+ |----------|-------|---------|
15
+ | **Composites** (11) | Sequence, Selector, Parallel, ForEach, While, Conditional, Recovery, SubTree, MemorySequence, ReactiveSequence | Orchestration, control flow |
16
+ | **Decorators** (10) | Timeout, Delay, Repeat, Invert, ForceSuccess, ForceFailure, RunOnce, KeepRunningUntilFailure, Precondition, SoftAssert | Modify child behavior |
17
+ | **Conditions** (3) | CheckCondition, AlwaysCondition, LogMessage | Simple checks, logging |
18
+
19
+ These run entirely in Temporal's workflow sandbox - no external I/O.
20
+
21
+ ### 2. Activity-Based I/O Nodes - Run via Activities
22
+
23
+ | Node | Activity | Purpose |
24
+ |------|----------|---------|
25
+ | **HttpRequest** | `fetchUrl` | HTTP requests (GET, POST, etc.) |
26
+ | **GenerateFile** | `generateFile` | Create CSV/Excel/JSON files |
27
+ | **ParseFile** | `parseFile` | Parse CSV/Excel files |
28
+ | **PythonScript** | `executePythonScript` | Run Python code |
29
+ | **JavaScriptNode** | `executeJavaScript` | Run JavaScript code (ES5) |
30
+ | **IntegrationAction** | `executePieceAction` | Active Pieces integrations |
31
+
32
+ These nodes require activities for external I/O. They follow the pattern:
33
+ 1. Check if activity exists in context
34
+ 2. Call activity with request
35
+ 3. Store result reference in blackboard
36
+
37
+ ---
38
+
39
+ ## Decision: Replace Script with JavaScriptNode
40
+
41
+ ### Rationale
42
+
43
+ The current `Script` node uses `js-interpreter` inline, which fails in Temporal's sandbox due to frozen objects. Instead of maintaining dual execution paths, we'll:
44
+
45
+ 1. **Deprecate `Script` node** - Remove inline js-interpreter execution
46
+ 2. **Add `JavaScriptNode`** - Activity-based, same pattern as `PythonScript`
47
+
48
+ This provides consistency:
49
+ - `PythonScript` → runs Python via activity
50
+ - `JavaScriptNode` → runs JavaScript via activity
51
+
52
+ ### Migration
53
+
54
+ ```yaml
55
+ # OLD (Script - deprecated)
56
+ type: CodeExecution
57
+ id: transform
58
+ props:
59
+ code: |
60
+ var items = getBB('items');
61
+ setBB('total', items.length);
62
+
63
+ # NEW (JavaScriptNode)
64
+ type: JavaScriptNode
65
+ id: transform
66
+ props:
67
+ code: |
68
+ var items = getBB('items');
69
+ setBB('total', items.length);
70
+ timeout: 5000
71
+ outputKey: "scriptResult" # Optional: store full result
72
+ ```
73
+
74
+ The API remains the same (`getBB`, `setBB`, `getInput`, `getEnv`).
75
+
76
+ ---
77
+
78
+ ## Large Payload Architecture
79
+
80
+ ### The Problem
81
+
82
+ Temporal has limits:
83
+ - **2MB payload limit** per activity argument/result
84
+ - **50MB event history limit** per workflow
85
+ - Large data round-trips are inefficient
86
+
87
+ ### Solution: Shared Data Store
88
+
89
+ Activities communicate via a shared data store. The workflow only sees references and metadata.
90
+
91
+ ```
92
+ ┌──────────────────────────────────────────────────────────────┐
93
+ │ DataStore (Abstract) │
94
+ │ Implementation: GCS / Redis / PostgreSQL / Mock (tests) │
95
+ └──────────────────────────────────────────────────────────────┘
96
+ ▲ ▲ ▲
97
+ │ write │ read/write │ read
98
+ │ │ │
99
+ ┌───────┴───────┐ ┌───────┴───────┐ ┌──────┴────────┐
100
+ │ HttpRequest │ │ JavaScriptNode│ │ GenerateFile │
101
+ │ Activity │ │ Activity │ │ Activity │
102
+ │ │ │ │ │ │
103
+ │ Fetches API │ │ Transforms │ │ Creates file │
104
+ │ Stores in DS │ │ data from DS │ │ from DS data │
105
+ │ Returns: ref │ │ Returns: ref │ │ Returns: path │
106
+ └───────────────┘ └───────────────┘ └───────────────┘
107
+ │ │ │
108
+ │ { ref, count } │ { ref, summary } │ { filePath }
109
+ ▼ ▼ ▼
110
+ ┌──────────────────────────────────────────────────────────────┐
111
+ │ Workflow (Temporal Sandbox) │
112
+ │ │
113
+ │ Blackboard stores ONLY: │
114
+ │ - References: { dataRef: "gs://bucket/workflow/123/data" } │
115
+ │ - Metadata: { rowCount: 5000, status: "ready" } │
116
+ │ - Flags: { hasErrors: false, needsRetry: true } │
117
+ │ │
118
+ │ Blackboard NEVER stores large data directly │
119
+ └──────────────────────────────────────────────────────────────┘
120
+ ```
121
+
122
+ ---
123
+
124
+ ## DataStore Interface
125
+
126
+ ### Abstract Interface (Mockable)
127
+
128
+ ```typescript
129
+ /**
130
+ * Abstract data store for large payload handling.
131
+ * Activities use this to store/retrieve data without passing through workflow.
132
+ *
133
+ * Implementations:
134
+ * - GCSDataStore: Production (Google Cloud Storage)
135
+ * - RedisDataStore: Fast ephemeral data
136
+ * - MemoryDataStore: Unit tests
137
+ */
138
+ export interface DataStore {
139
+ /**
140
+ * Store data and return a reference
141
+ * @param key - Unique key (e.g., "workflow:123:httpResponse:456")
142
+ * @param data - Data to store (will be JSON serialized)
143
+ * @param options - TTL, metadata, etc.
144
+ * @returns Reference object with retrieval info
145
+ */
146
+ put(key: string, data: unknown, options?: PutOptions): Promise<DataRef>;
147
+
148
+ /**
149
+ * Retrieve data by reference
150
+ * @param ref - Reference returned from put()
151
+ * @returns The stored data (JSON deserialized)
152
+ */
153
+ get(ref: DataRef): Promise<unknown>;
154
+
155
+ /**
156
+ * Delete data by reference
157
+ * @param ref - Reference to delete
158
+ */
159
+ delete(ref: DataRef): Promise<void>;
160
+
161
+ /**
162
+ * Check if data exists
163
+ * @param ref - Reference to check
164
+ */
165
+ exists(ref: DataRef): Promise<boolean>;
166
+ }
167
+
168
+ export interface DataRef {
169
+ /** Storage backend identifier */
170
+ store: 'gcs' | 'redis' | 'memory';
171
+ /** Full key/path to data */
172
+ key: string;
173
+ /** Size in bytes (for decisions) */
174
+ sizeBytes?: number;
175
+ /** When data expires (if applicable) */
176
+ expiresAt?: number;
177
+ }
178
+
179
+ export interface PutOptions {
180
+ /** Time-to-live in seconds */
181
+ ttlSeconds?: number;
182
+ /** Content type hint */
183
+ contentType?: 'json' | 'csv' | 'binary';
184
+ /** Workflow context for key namespacing */
185
+ workflowId?: string;
186
+ }
187
+ ```
188
+
189
+ ### GCS Implementation (Production)
190
+
191
+ ```typescript
192
+ import { Storage } from '@google-cloud/storage';
193
+
194
+ export class GCSDataStore implements DataStore {
195
+ private storage: Storage;
196
+ private bucket: string;
197
+ private prefix: string;
198
+
199
+ constructor(config: { projectId?: string; bucket: string; prefix?: string }) {
200
+ this.storage = new Storage({ projectId: config.projectId });
201
+ this.bucket = config.bucket;
202
+ this.prefix = config.prefix ?? 'workflows/';
203
+ }
204
+
205
+ async put(key: string, data: unknown, options?: PutOptions): Promise<DataRef> {
206
+ const fullKey = `${this.prefix}${key}`;
207
+ const body = JSON.stringify(data);
208
+ const sizeBytes = Buffer.byteLength(body, 'utf8');
209
+
210
+ const file = this.storage.bucket(this.bucket).file(fullKey);
211
+
212
+ const metadata: Record<string, string> = {};
213
+ if (options?.workflowId) metadata['workflow-id'] = options.workflowId;
214
+
215
+ let expiresAt: number | undefined;
216
+ if (options?.ttlSeconds) {
217
+ expiresAt = Date.now() + options.ttlSeconds * 1000;
218
+ metadata['expires-at'] = String(expiresAt);
219
+ }
220
+
221
+ await file.save(body, {
222
+ contentType: 'application/json',
223
+ metadata: { metadata },
224
+ });
225
+
226
+ return {
227
+ store: 'gcs',
228
+ key: fullKey,
229
+ sizeBytes,
230
+ expiresAt,
231
+ };
232
+ }
233
+
234
+ async get(ref: DataRef): Promise<unknown> {
235
+ const file = this.storage.bucket(this.bucket).file(ref.key);
236
+ const [contents] = await file.download();
237
+ return JSON.parse(contents.toString('utf8'));
238
+ }
239
+
240
+ async delete(ref: DataRef): Promise<void> {
241
+ const file = this.storage.bucket(this.bucket).file(ref.key);
242
+ await file.delete({ ignoreNotFound: true });
243
+ }
244
+
245
+ async exists(ref: DataRef): Promise<boolean> {
246
+ const file = this.storage.bucket(this.bucket).file(ref.key);
247
+ const [exists] = await file.exists();
248
+ return exists;
249
+ }
250
+ }
251
+ ```
252
+
253
+ ### Memory Implementation (Tests)
254
+
255
+ ```typescript
256
+ export class MemoryDataStore implements DataStore {
257
+ private store = new Map<string, { data: unknown; expiresAt?: number }>();
258
+
259
+ async put(key: string, data: unknown, options?: PutOptions): Promise<DataRef> {
260
+ const expiresAt = options?.ttlSeconds
261
+ ? Date.now() + options.ttlSeconds * 1000
262
+ : undefined;
263
+
264
+ this.store.set(key, { data, expiresAt });
265
+
266
+ return {
267
+ store: 'memory',
268
+ key,
269
+ sizeBytes: JSON.stringify(data).length,
270
+ expiresAt
271
+ };
272
+ }
273
+
274
+ async get(ref: DataRef): Promise<unknown> {
275
+ const entry = this.store.get(ref.key);
276
+ if (!entry) return null;
277
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
278
+ this.store.delete(ref.key);
279
+ return null;
280
+ }
281
+ return entry.data;
282
+ }
283
+
284
+ async delete(ref: DataRef): Promise<void> {
285
+ this.store.delete(ref.key);
286
+ }
287
+
288
+ async exists(ref: DataRef): Promise<boolean> {
289
+ const entry = this.store.get(ref.key);
290
+ if (!entry) return false;
291
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
292
+ this.store.delete(ref.key);
293
+ return false;
294
+ }
295
+ return true;
296
+ }
297
+
298
+ /** Test helper: clear all data */
299
+ clear(): void {
300
+ this.store.clear();
301
+ }
302
+ }
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Updated BtreeActivities Interface
308
+
309
+ ```typescript
310
+ export interface BtreeActivities {
311
+ /** Execute an Active Pieces action */
312
+ executePieceAction: (request: PieceActivityRequest) => Promise<unknown>;
313
+
314
+ /** Execute Python code */
315
+ executePythonScript?: (request: PythonScriptRequest) => Promise<ScriptResult>;
316
+
317
+ /** Execute JavaScript code (replaces inline Script node) */
318
+ executeJavaScript?: (request: JavaScriptRequest) => Promise<ScriptResult>;
319
+
320
+ /** Parse CSV/Excel file */
321
+ parseFile?: (request: ParseFileRequest) => Promise<ParseFileResult>;
322
+
323
+ /** Generate CSV/Excel/JSON file */
324
+ generateFile?: (request: GenerateFileRequest) => Promise<GenerateFileResult>;
325
+
326
+ /** Make HTTP request */
327
+ fetchUrl?: (request: HttpRequestActivity) => Promise<HttpResponseActivity>;
328
+ }
329
+
330
+ /** Request for JavaScript execution */
331
+ export interface JavaScriptRequest {
332
+ /** JavaScript code (ES5 syntax) */
333
+ code: string;
334
+ /** DataRefs to resolve before execution */
335
+ dataRefs?: Record<string, DataRef>;
336
+ /** Small values to pass directly */
337
+ context?: Record<string, unknown>;
338
+ /** Workflow input (read-only in script) */
339
+ input?: Record<string, unknown>;
340
+ /** Allowed environment variables */
341
+ allowedEnvVars?: string[];
342
+ /** Execution timeout in ms (default: 5000) */
343
+ timeout?: number;
344
+ /** Workflow ID for data store namespacing */
345
+ workflowId?: string;
346
+ }
347
+
348
+ /** Result from script execution */
349
+ export interface ScriptResult {
350
+ /** Small values returned directly */
351
+ values: Record<string, unknown>;
352
+ /** Large values stored in data store */
353
+ dataRefs: Record<string, DataRef>;
354
+ /** Console.log output */
355
+ logs: string[];
356
+ /** Execution time in ms */
357
+ executionTimeMs: number;
358
+ }
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Activity Data Flow Pattern
364
+
365
+ ### HttpRequest Activity
366
+
367
+ ```typescript
368
+ async function fetchUrlActivity(
369
+ request: HttpRequestActivity,
370
+ dataStore: DataStore,
371
+ workflowId: string
372
+ ): Promise<HttpResponseActivity> {
373
+ const response = await fetch(request.url, {
374
+ method: request.method,
375
+ headers: request.headers,
376
+ body: request.body ? JSON.stringify(request.body) : undefined,
377
+ });
378
+
379
+ const data = await response.json();
380
+ const dataSize = JSON.stringify(data).length;
381
+
382
+ // Threshold: 100KB - below this, return directly
383
+ const INLINE_THRESHOLD = 100 * 1024;
384
+
385
+ if (dataSize < INLINE_THRESHOLD) {
386
+ // Small response: return directly
387
+ return {
388
+ status: response.status,
389
+ headers: Object.fromEntries(response.headers),
390
+ data, // Inline
391
+ };
392
+ } else {
393
+ // Large response: store in data store
394
+ const ref = await dataStore.put(
395
+ `${workflowId}/http/${Date.now()}`,
396
+ data,
397
+ { ttlSeconds: 3600 } // 1 hour TTL
398
+ );
399
+
400
+ return {
401
+ status: response.status,
402
+ headers: Object.fromEntries(response.headers),
403
+ dataRef: ref, // Reference only
404
+ rowCount: Array.isArray(data) ? data.length : undefined,
405
+ };
406
+ }
407
+ }
408
+ ```
409
+
410
+ ### JavaScriptNode Activity
411
+
412
+ ```typescript
413
+ async function executeJavaScriptActivity(
414
+ request: JavaScriptRequest,
415
+ dataStore: DataStore
416
+ ): Promise<ScriptResult> {
417
+ const startTime = Date.now();
418
+
419
+ // 1. Resolve all data refs to actual data
420
+ const resolvedContext: Record<string, unknown> = { ...request.context };
421
+
422
+ for (const [key, ref] of Object.entries(request.dataRefs || {})) {
423
+ resolvedContext[key] = await dataStore.get(ref);
424
+ }
425
+
426
+ // 2. Execute script with js-interpreter
427
+ const Interpreter = require('js-interpreter');
428
+ const modifiedValues: Record<string, unknown> = {};
429
+ const logs: string[] = [];
430
+
431
+ const initFunc = (interpreter: any, globalObject: any) => {
432
+ // getBB - reads from resolved context
433
+ interpreter.setProperty(globalObject, 'getBB',
434
+ interpreter.createNativeFunction((key: string) => {
435
+ return interpreter.nativeToPseudo(resolvedContext[key]);
436
+ })
437
+ );
438
+
439
+ // setBB - writes to modified values
440
+ interpreter.setProperty(globalObject, 'setBB',
441
+ interpreter.createNativeFunction((key: string, value: any) => {
442
+ modifiedValues[key] = interpreter.pseudoToNative(value);
443
+ })
444
+ );
445
+
446
+ // getInput - read-only workflow input
447
+ interpreter.setProperty(globalObject, 'getInput',
448
+ interpreter.createNativeFunction((key: string) => {
449
+ return interpreter.nativeToPseudo(request.input?.[key]);
450
+ })
451
+ );
452
+
453
+ // getEnv - allowed env vars only
454
+ interpreter.setProperty(globalObject, 'getEnv',
455
+ interpreter.createNativeFunction((key: string) => {
456
+ if (request.allowedEnvVars?.includes(key)) {
457
+ return interpreter.nativeToPseudo(process.env[key]);
458
+ }
459
+ return interpreter.nativeToPseudo(undefined);
460
+ })
461
+ );
462
+
463
+ // console.log
464
+ interpreter.setProperty(globalObject, 'console',
465
+ interpreter.nativeToPseudo({
466
+ log: (...args: any[]) => logs.push(args.map(String).join(' '))
467
+ })
468
+ );
469
+ };
470
+
471
+ const interp = new Interpreter(request.code, initFunc);
472
+
473
+ // Execute with timeout
474
+ const timeout = request.timeout || 5000;
475
+ while (interp.step()) {
476
+ if (Date.now() - startTime > timeout) {
477
+ throw new Error(`Script execution timeout after ${timeout}ms`);
478
+ }
479
+ }
480
+
481
+ // 3. Store large results in data store, keep small ones inline
482
+ const INLINE_THRESHOLD = 10 * 1024; // 10KB
483
+ const resultValues: Record<string, unknown> = {};
484
+ const resultRefs: Record<string, DataRef> = {};
485
+
486
+ for (const [key, value] of Object.entries(modifiedValues)) {
487
+ const size = JSON.stringify(value).length;
488
+
489
+ if (size < INLINE_THRESHOLD) {
490
+ resultValues[key] = value;
491
+ } else {
492
+ resultRefs[key] = await dataStore.put(
493
+ `${request.workflowId}/js/${key}/${Date.now()}`,
494
+ value,
495
+ { ttlSeconds: 3600 }
496
+ );
497
+ }
498
+ }
499
+
500
+ return {
501
+ values: resultValues,
502
+ dataRefs: resultRefs,
503
+ logs,
504
+ executionTimeMs: Date.now() - startTime,
505
+ };
506
+ }
507
+ ```
508
+
509
+ ---
510
+
511
+ ## Blackboard: Reference Resolution
512
+
513
+ The blackboard needs to understand data references. When a node needs actual data, it resolves the ref via the data store.
514
+
515
+ ```typescript
516
+ // In node execution
517
+ const httpResult = context.blackboard.get('apiResponse');
518
+
519
+ if (isDataRef(httpResult)) {
520
+ // This is a reference - actual data is in data store
521
+ // Node can either:
522
+ // 1. Pass the ref to an activity (activity resolves it)
523
+ // 2. Use a utility to resolve inline (if small data expected)
524
+ } else {
525
+ // This is actual data (small payload returned inline)
526
+ }
527
+ ```
528
+
529
+ ### Helper Type Guard
530
+
531
+ ```typescript
532
+ export function isDataRef(value: unknown): value is DataRef {
533
+ return (
534
+ typeof value === 'object' &&
535
+ value !== null &&
536
+ 'store' in value &&
537
+ 'key' in value &&
538
+ ['gcs', 'redis', 'memory'].includes((value as DataRef).store)
539
+ );
540
+ }
541
+ ```
542
+
543
+ ---
544
+
545
+ ## Worker Configuration
546
+
547
+ ```typescript
548
+ // worker.ts
549
+ import { GCSDataStore, MemoryDataStore } from './data-store';
550
+
551
+ // Production: Use GCS
552
+ const dataStore = new GCSDataStore({
553
+ projectId: process.env.GCP_PROJECT_ID,
554
+ bucket: process.env.GCS_BUCKET!,
555
+ prefix: 'workflows/',
556
+ });
557
+
558
+ // Tests: Use memory
559
+ // const dataStore = new MemoryDataStore();
560
+
561
+ // Create activities with data store injected
562
+ const activities = {
563
+ fetchUrlActivity: (req) => fetchUrlActivity(req, dataStore, workflowId),
564
+ executeJavaScriptActivity: (req) => executeJavaScriptActivity(req, dataStore),
565
+ generateFileActivity: (req) => generateFileActivity(req, dataStore),
566
+ // ... etc
567
+ };
568
+ ```
569
+
570
+ ---
571
+
572
+ ## Summary
573
+
574
+ | Component | Responsibility |
575
+ |-----------|---------------|
576
+ | **Workflow Sandbox** | Orchestration only. Stores refs + metadata in blackboard. |
577
+ | **Activities** | All I/O. Read/write data store. Return refs for large data. |
578
+ | **DataStore** | Shared storage between activities. GCS for prod, Memory for tests. |
579
+ | **Blackboard** | Holds small values inline, DataRefs for large values. |
580
+ | **JavaScriptNode** | Activity-based JS execution. Replaces inline Script node. |
581
+
582
+ ### Key Principles
583
+
584
+ 1. **Blackboard is lightweight** - Never store large data directly
585
+ 2. **Activities share via DataStore** - Not through workflow state
586
+ 3. **DataStore is abstract** - Can swap GCS/Redis/Memory without code changes
587
+ 4. **References carry metadata** - Size, expiry, type for smart decisions
588
+ 5. **Threshold-based inline** - Small data (<100KB) can skip data store
589
+
590
+ ---
591
+
592
+ ## Storage Alternatives
593
+
594
+ ### Google Cloud Filestore (NFS) - Future Option
595
+
596
+ If local filesystem semantics are needed (similar to AWS EFS), consider **Google Cloud Filestore**:
597
+
598
+ | Feature | GCS (Current) | Filestore |
599
+ |---------|---------------|-----------|
600
+ | **Interface** | Object storage API | NFS mount (local filesystem) |
601
+ | **Use Case** | Data refs, long-term storage | Scratch space, temp files |
602
+ | **Min Size** | Pay per use | 1 TiB (Basic tier) |
603
+ | **Performance** | High throughput | Up to 26 GiB/s, 900K IOPS |
604
+ | **GKE Integration** | SDK calls | CSI driver, PersistentVolumes |
605
+
606
+ **When to consider Filestore:**
607
+ - Worker pods need shared scratch filesystem
608
+ - Code execution needs local file I/O patterns
609
+ - AI/ML workloads with large datasets
610
+
611
+ **Hybrid approach (if needed):**
612
+ - Filestore → Worker scratch space, temp files during execution
613
+ - GCS → Long-term storage, workflow data refs, archived outputs