@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,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
|