@nodetool-ai/runtime 0.7.0-rc.11

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 (198) hide show
  1. package/dist/bridge-protocol.d.ts +39 -0
  2. package/dist/bridge-protocol.d.ts.map +1 -0
  3. package/dist/bridge-protocol.js +39 -0
  4. package/dist/bridge-protocol.js.map +1 -0
  5. package/dist/comfy-executor.d.ts +41 -0
  6. package/dist/comfy-executor.d.ts.map +1 -0
  7. package/dist/comfy-executor.js +305 -0
  8. package/dist/comfy-executor.js.map +1 -0
  9. package/dist/context-packer.d.ts +22 -0
  10. package/dist/context-packer.d.ts.map +1 -0
  11. package/dist/context-packer.js +66 -0
  12. package/dist/context-packer.js.map +1 -0
  13. package/dist/context.d.ts +405 -0
  14. package/dist/context.d.ts.map +1 -0
  15. package/dist/context.js +1455 -0
  16. package/dist/context.js.map +1 -0
  17. package/dist/index.d.ts +18 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +15 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/node-executor.d.ts +53 -0
  22. package/dist/node-executor.d.ts.map +1 -0
  23. package/dist/node-executor.js +9 -0
  24. package/dist/node-executor.js.map +1 -0
  25. package/dist/providers/aki-manifest.json +34 -0
  26. package/dist/providers/aki-provider.d.ts +65 -0
  27. package/dist/providers/aki-provider.d.ts.map +1 -0
  28. package/dist/providers/aki-provider.js +517 -0
  29. package/dist/providers/aki-provider.js.map +1 -0
  30. package/dist/providers/anthropic-provider.d.ts +55 -0
  31. package/dist/providers/anthropic-provider.d.ts.map +1 -0
  32. package/dist/providers/anthropic-provider.js +563 -0
  33. package/dist/providers/anthropic-provider.js.map +1 -0
  34. package/dist/providers/base-provider.d.ts +194 -0
  35. package/dist/providers/base-provider.d.ts.map +1 -0
  36. package/dist/providers/base-provider.js +515 -0
  37. package/dist/providers/base-provider.js.map +1 -0
  38. package/dist/providers/cerebras-provider.d.ts +20 -0
  39. package/dist/providers/cerebras-provider.d.ts.map +1 -0
  40. package/dist/providers/cerebras-provider.js +52 -0
  41. package/dist/providers/cerebras-provider.js.map +1 -0
  42. package/dist/providers/claude-agent-provider.d.ts +107 -0
  43. package/dist/providers/claude-agent-provider.d.ts.map +1 -0
  44. package/dist/providers/claude-agent-provider.js +586 -0
  45. package/dist/providers/claude-agent-provider.js.map +1 -0
  46. package/dist/providers/cohere-provider.d.ts +36 -0
  47. package/dist/providers/cohere-provider.d.ts.map +1 -0
  48. package/dist/providers/cohere-provider.js +116 -0
  49. package/dist/providers/cohere-provider.js.map +1 -0
  50. package/dist/providers/cost-calculator.d.ts +109 -0
  51. package/dist/providers/cost-calculator.d.ts.map +1 -0
  52. package/dist/providers/cost-calculator.js +475 -0
  53. package/dist/providers/cost-calculator.js.map +1 -0
  54. package/dist/providers/deepseek-provider.d.ts +25 -0
  55. package/dist/providers/deepseek-provider.d.ts.map +1 -0
  56. package/dist/providers/deepseek-provider.js +58 -0
  57. package/dist/providers/deepseek-provider.js.map +1 -0
  58. package/dist/providers/fake-provider.d.ts +52 -0
  59. package/dist/providers/fake-provider.d.ts.map +1 -0
  60. package/dist/providers/fake-provider.js +107 -0
  61. package/dist/providers/fake-provider.js.map +1 -0
  62. package/dist/providers/fal-provider.d.ts +32 -0
  63. package/dist/providers/fal-provider.d.ts.map +1 -0
  64. package/dist/providers/fal-provider.js +445 -0
  65. package/dist/providers/fal-provider.js.map +1 -0
  66. package/dist/providers/gemini-provider.d.ts +109 -0
  67. package/dist/providers/gemini-provider.d.ts.map +1 -0
  68. package/dist/providers/gemini-provider.js +943 -0
  69. package/dist/providers/gemini-provider.js.map +1 -0
  70. package/dist/providers/groq-provider.d.ts +20 -0
  71. package/dist/providers/groq-provider.d.ts.map +1 -0
  72. package/dist/providers/groq-provider.js +52 -0
  73. package/dist/providers/groq-provider.js.map +1 -0
  74. package/dist/providers/huggingface-provider.d.ts +59 -0
  75. package/dist/providers/huggingface-provider.d.ts.map +1 -0
  76. package/dist/providers/huggingface-provider.js +305 -0
  77. package/dist/providers/huggingface-provider.js.map +1 -0
  78. package/dist/providers/index.d.ts +69 -0
  79. package/dist/providers/index.d.ts.map +1 -0
  80. package/dist/providers/index.js +137 -0
  81. package/dist/providers/index.js.map +1 -0
  82. package/dist/providers/jina-provider.d.ts +37 -0
  83. package/dist/providers/jina-provider.d.ts.map +1 -0
  84. package/dist/providers/jina-provider.js +118 -0
  85. package/dist/providers/jina-provider.js.map +1 -0
  86. package/dist/providers/kie-provider.d.ts +19 -0
  87. package/dist/providers/kie-provider.d.ts.map +1 -0
  88. package/dist/providers/kie-provider.js +118 -0
  89. package/dist/providers/kie-provider.js.map +1 -0
  90. package/dist/providers/llama-provider.d.ts +49 -0
  91. package/dist/providers/llama-provider.d.ts.map +1 -0
  92. package/dist/providers/llama-provider.js +381 -0
  93. package/dist/providers/llama-provider.js.map +1 -0
  94. package/dist/providers/lmstudio-provider.d.ts +22 -0
  95. package/dist/providers/lmstudio-provider.d.ts.map +1 -0
  96. package/dist/providers/lmstudio-provider.js +63 -0
  97. package/dist/providers/lmstudio-provider.js.map +1 -0
  98. package/dist/providers/manifest-models.d.ts +12 -0
  99. package/dist/providers/manifest-models.d.ts.map +1 -0
  100. package/dist/providers/manifest-models.js +96 -0
  101. package/dist/providers/manifest-models.js.map +1 -0
  102. package/dist/providers/meshy-provider.d.ts +53 -0
  103. package/dist/providers/meshy-provider.d.ts.map +1 -0
  104. package/dist/providers/meshy-provider.js +230 -0
  105. package/dist/providers/meshy-provider.js.map +1 -0
  106. package/dist/providers/minimax-provider.d.ts +66 -0
  107. package/dist/providers/minimax-provider.d.ts.map +1 -0
  108. package/dist/providers/minimax-provider.js +531 -0
  109. package/dist/providers/minimax-provider.js.map +1 -0
  110. package/dist/providers/mistral-provider.d.ts +26 -0
  111. package/dist/providers/mistral-provider.d.ts.map +1 -0
  112. package/dist/providers/mistral-provider.js +74 -0
  113. package/dist/providers/mistral-provider.js.map +1 -0
  114. package/dist/providers/moonshot-provider.d.ts +24 -0
  115. package/dist/providers/moonshot-provider.d.ts.map +1 -0
  116. package/dist/providers/moonshot-provider.js +43 -0
  117. package/dist/providers/moonshot-provider.js.map +1 -0
  118. package/dist/providers/ollama-provider.d.ts +71 -0
  119. package/dist/providers/ollama-provider.d.ts.map +1 -0
  120. package/dist/providers/ollama-provider.js +480 -0
  121. package/dist/providers/ollama-provider.js.map +1 -0
  122. package/dist/providers/openai-provider.d.ts +103 -0
  123. package/dist/providers/openai-provider.d.ts.map +1 -0
  124. package/dist/providers/openai-provider.js +917 -0
  125. package/dist/providers/openai-provider.js.map +1 -0
  126. package/dist/providers/openrouter-provider.d.ts +49 -0
  127. package/dist/providers/openrouter-provider.d.ts.map +1 -0
  128. package/dist/providers/openrouter-provider.js +147 -0
  129. package/dist/providers/openrouter-provider.js.map +1 -0
  130. package/dist/providers/provider-registry.d.ts +24 -0
  131. package/dist/providers/provider-registry.d.ts.map +1 -0
  132. package/dist/providers/provider-registry.js +124 -0
  133. package/dist/providers/provider-registry.js.map +1 -0
  134. package/dist/providers/python-provider.d.ts +75 -0
  135. package/dist/providers/python-provider.d.ts.map +1 -0
  136. package/dist/providers/python-provider.js +157 -0
  137. package/dist/providers/python-provider.js.map +1 -0
  138. package/dist/providers/replicate-provider.d.ts +73 -0
  139. package/dist/providers/replicate-provider.d.ts.map +1 -0
  140. package/dist/providers/replicate-provider.js +484 -0
  141. package/dist/providers/replicate-provider.js.map +1 -0
  142. package/dist/providers/rodin-provider.d.ts +41 -0
  143. package/dist/providers/rodin-provider.d.ts.map +1 -0
  144. package/dist/providers/rodin-provider.js +236 -0
  145. package/dist/providers/rodin-provider.js.map +1 -0
  146. package/dist/providers/scripted-provider.d.ts +141 -0
  147. package/dist/providers/scripted-provider.d.ts.map +1 -0
  148. package/dist/providers/scripted-provider.js +246 -0
  149. package/dist/providers/scripted-provider.js.map +1 -0
  150. package/dist/providers/together-provider.d.ts +20 -0
  151. package/dist/providers/together-provider.d.ts.map +1 -0
  152. package/dist/providers/together-provider.js +56 -0
  153. package/dist/providers/together-provider.js.map +1 -0
  154. package/dist/providers/types.d.ts +202 -0
  155. package/dist/providers/types.d.ts.map +1 -0
  156. package/dist/providers/types.js +2 -0
  157. package/dist/providers/types.js.map +1 -0
  158. package/dist/providers/vllm-provider.d.ts +22 -0
  159. package/dist/providers/vllm-provider.d.ts.map +1 -0
  160. package/dist/providers/vllm-provider.js +66 -0
  161. package/dist/providers/vllm-provider.js.map +1 -0
  162. package/dist/providers/voyage-provider.d.ts +37 -0
  163. package/dist/providers/voyage-provider.d.ts.map +1 -0
  164. package/dist/providers/voyage-provider.js +124 -0
  165. package/dist/providers/voyage-provider.js.map +1 -0
  166. package/dist/providers/xai-provider.d.ts +24 -0
  167. package/dist/providers/xai-provider.d.ts.map +1 -0
  168. package/dist/providers/xai-provider.js +57 -0
  169. package/dist/providers/xai-provider.js.map +1 -0
  170. package/dist/python-bridge-types.d.ts +70 -0
  171. package/dist/python-bridge-types.d.ts.map +1 -0
  172. package/dist/python-bridge-types.js +2 -0
  173. package/dist/python-bridge-types.js.map +1 -0
  174. package/dist/python-node-executor.d.ts +20 -0
  175. package/dist/python-node-executor.d.ts.map +1 -0
  176. package/dist/python-node-executor.js +219 -0
  177. package/dist/python-node-executor.js.map +1 -0
  178. package/dist/python-stdio-bridge.d.ts +67 -0
  179. package/dist/python-stdio-bridge.d.ts.map +1 -0
  180. package/dist/python-stdio-bridge.js +704 -0
  181. package/dist/python-stdio-bridge.js.map +1 -0
  182. package/dist/recommended-models.d.ts +17 -0
  183. package/dist/recommended-models.d.ts.map +1 -0
  184. package/dist/recommended-models.js +139 -0
  185. package/dist/recommended-models.js.map +1 -0
  186. package/dist/telemetry.d.ts +61 -0
  187. package/dist/telemetry.d.ts.map +1 -0
  188. package/dist/telemetry.js +128 -0
  189. package/dist/telemetry.js.map +1 -0
  190. package/dist/trace-exporters.d.ts +72 -0
  191. package/dist/trace-exporters.d.ts.map +1 -0
  192. package/dist/trace-exporters.js +216 -0
  193. package/dist/trace-exporters.js.map +1 -0
  194. package/dist/tracing-helpers.d.ts +99 -0
  195. package/dist/tracing-helpers.d.ts.map +1 -0
  196. package/dist/tracing-helpers.js +217 -0
  197. package/dist/tracing-helpers.js.map +1 -0
  198. package/package.json +64 -0
@@ -0,0 +1,1455 @@
1
+ /**
2
+ * ProcessingContext – runtime context for node execution.
3
+ *
4
+ * Port of src/nodetool/workflows/processing_context.py.
5
+ *
6
+ * Provides:
7
+ * - Message queue for emitting ProcessingMessages.
8
+ * - Cache get/set interface.
9
+ * - Output normalization (sanitize memory URIs, etc.).
10
+ * - Asset handling with pluggable storage adapters.
11
+ */
12
+ import { randomUUID } from "node:crypto";
13
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
+ /**
17
+ * In-memory cache adapter (default for tests and single-process execution).
18
+ */
19
+ export class MemoryCache {
20
+ _store = new Map();
21
+ async get(key) {
22
+ const entry = this._store.get(key);
23
+ if (!entry)
24
+ return undefined;
25
+ if (entry.expires !== null && Date.now() > entry.expires) {
26
+ this._store.delete(key);
27
+ return undefined;
28
+ }
29
+ return entry.value;
30
+ }
31
+ async set(key, value, ttlSeconds) {
32
+ const expires = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null;
33
+ this._store.set(key, { value, expires });
34
+ }
35
+ async has(key) {
36
+ return (await this.get(key)) !== undefined;
37
+ }
38
+ async delete(key) {
39
+ this._store.delete(key);
40
+ }
41
+ }
42
+ function isWithinRoot(root, target) {
43
+ const rel = relative(root, target);
44
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
45
+ }
46
+ function normalizeStorageKey(key) {
47
+ const cleaned = normalize(key.replaceAll("\\", "/")).replace(/^\/+/, "");
48
+ if (!cleaned ||
49
+ cleaned === "." ||
50
+ cleaned.startsWith("..") ||
51
+ cleaned.includes(`..${sep}`)) {
52
+ throw new Error(`Invalid storage key: ${key}`);
53
+ }
54
+ return cleaned;
55
+ }
56
+ function joinStorageKey(prefix, key) {
57
+ const normalizedKey = normalizeStorageKey(key);
58
+ if (!prefix)
59
+ return normalizedKey;
60
+ const normalizedPrefix = normalizeStorageKey(prefix);
61
+ return `${normalizedPrefix}/${normalizedKey}`;
62
+ }
63
+ /**
64
+ * In-memory storage adapter useful for tests and single-process ephemeral runs.
65
+ */
66
+ export class InMemoryStorageAdapter {
67
+ _store = new Map();
68
+ async store(key, data, _contentType) {
69
+ const normalized = normalizeStorageKey(key);
70
+ this._store.set(normalized, new Uint8Array(data));
71
+ return `memory://${normalized}`;
72
+ }
73
+ async retrieve(uri) {
74
+ if (!uri.startsWith("memory://"))
75
+ return null;
76
+ const key = uri.slice("memory://".length);
77
+ const value = this._store.get(key);
78
+ return value ? new Uint8Array(value) : null;
79
+ }
80
+ async exists(uri) {
81
+ if (!uri.startsWith("memory://"))
82
+ return false;
83
+ const key = uri.slice("memory://".length);
84
+ return this._store.has(key);
85
+ }
86
+ }
87
+ /**
88
+ * File-system storage adapter rooted to a single base directory.
89
+ */
90
+ export class FileStorageAdapter {
91
+ rootDir;
92
+ constructor(rootDir) {
93
+ this.rootDir = resolve(rootDir);
94
+ }
95
+ resolvePathFromKey(key) {
96
+ const normalized = normalizeStorageKey(key);
97
+ const absolute = resolve(join(this.rootDir, normalized));
98
+ if (!isWithinRoot(this.rootDir, absolute)) {
99
+ throw new Error(`Storage key escapes root: ${key}`);
100
+ }
101
+ return absolute;
102
+ }
103
+ resolvePathFromUri(uri) {
104
+ let absolute = null;
105
+ if (uri.startsWith("file://")) {
106
+ try {
107
+ absolute = resolve(fileURLToPath(uri));
108
+ }
109
+ catch {
110
+ // Invalid file:// URI — treat as unresolvable.
111
+ return null;
112
+ }
113
+ }
114
+ else if (uri.startsWith("/api/storage/") ||
115
+ uri.startsWith("api/storage/")) {
116
+ const key = uri.replace(/^\/?api\/storage\//, "");
117
+ absolute = this.resolvePathFromKey(key);
118
+ }
119
+ else if (/^https?:\/\//.test(uri)) {
120
+ try {
121
+ const parsed = new URL(uri);
122
+ if (parsed.pathname.startsWith("/api/storage/")) {
123
+ const key = parsed.pathname.replace(/^\/api\/storage\//, "");
124
+ absolute = this.resolvePathFromKey(key);
125
+ }
126
+ }
127
+ catch {
128
+ // Malformed URL — treat as unresolvable.
129
+ return null;
130
+ }
131
+ }
132
+ else {
133
+ return null;
134
+ }
135
+ if (absolute === null || !isWithinRoot(this.rootDir, absolute)) {
136
+ return null;
137
+ }
138
+ return absolute;
139
+ }
140
+ async store(key, data, _contentType) {
141
+ const absolutePath = this.resolvePathFromKey(key);
142
+ await mkdir(dirname(absolutePath), { recursive: true });
143
+ await writeFile(absolutePath, data);
144
+ return pathToFileURL(absolutePath).toString();
145
+ }
146
+ async retrieve(uri) {
147
+ const absolutePath = this.resolvePathFromUri(uri);
148
+ if (!absolutePath)
149
+ return null;
150
+ try {
151
+ return await readFile(absolutePath);
152
+ }
153
+ catch {
154
+ // File not found or unreadable — caller handles null.
155
+ return null;
156
+ }
157
+ }
158
+ async exists(uri) {
159
+ const absolutePath = this.resolvePathFromUri(uri);
160
+ if (!absolutePath)
161
+ return false;
162
+ try {
163
+ await access(absolutePath);
164
+ return true;
165
+ }
166
+ catch {
167
+ // File does not exist or is inaccessible.
168
+ return false;
169
+ }
170
+ }
171
+ }
172
+ /**
173
+ * S3-backed storage adapter with injected client operations.
174
+ *
175
+ * This avoids hard-coupling runtime to any specific SDK while providing
176
+ * predictable URI behavior (`s3://bucket/key`).
177
+ */
178
+ export class S3StorageAdapter {
179
+ bucket;
180
+ prefix;
181
+ client;
182
+ constructor(opts) {
183
+ if (!opts.bucket) {
184
+ throw new Error("S3 bucket is required");
185
+ }
186
+ this.bucket = opts.bucket;
187
+ this.client = opts.client;
188
+ this.prefix = opts.prefix ? normalizeStorageKey(opts.prefix) : null;
189
+ }
190
+ keyForStore(key) {
191
+ return joinStorageKey(this.prefix ?? undefined, key);
192
+ }
193
+ parseUri(uri) {
194
+ if (!uri.startsWith("s3://"))
195
+ return null;
196
+ const withoutScheme = uri.slice("s3://".length);
197
+ const slashIndex = withoutScheme.indexOf("/");
198
+ if (slashIndex <= 0 || slashIndex === withoutScheme.length - 1) {
199
+ return null;
200
+ }
201
+ const bucket = withoutScheme.slice(0, slashIndex);
202
+ const key = withoutScheme.slice(slashIndex + 1);
203
+ return { bucket, key };
204
+ }
205
+ async store(key, data, contentType) {
206
+ const objectKey = this.keyForStore(key);
207
+ await this.client.putObject({
208
+ bucket: this.bucket,
209
+ key: objectKey,
210
+ body: data,
211
+ contentType
212
+ });
213
+ return `s3://${this.bucket}/${objectKey}`;
214
+ }
215
+ async retrieve(uri) {
216
+ const parsed = this.parseUri(uri);
217
+ if (!parsed)
218
+ return null;
219
+ if (parsed.bucket !== this.bucket)
220
+ return null;
221
+ return this.client.getObject(parsed);
222
+ }
223
+ async exists(uri) {
224
+ const parsed = this.parseUri(uri);
225
+ if (!parsed)
226
+ return false;
227
+ if (parsed.bucket !== this.bucket)
228
+ return false;
229
+ return this.client.headObject(parsed);
230
+ }
231
+ }
232
+ /**
233
+ * Resolve paths relative to a configured workspace root.
234
+ *
235
+ * Supported path forms:
236
+ * - /workspace/foo/bar.txt
237
+ * - workspace/foo/bar.txt
238
+ * - absolute paths (treated as workspace-relative)
239
+ * - relative paths
240
+ */
241
+ export function resolveWorkspacePath(workspaceDir, path) {
242
+ if (workspaceDir == null) {
243
+ throw new Error("No workspace is assigned. File operations require a user-defined workspace. Please configure a workspace before performing disk I/O operations.");
244
+ }
245
+ if (workspaceDir === "") {
246
+ throw new Error("Workspace directory is required");
247
+ }
248
+ const workspaceRoot = resolve(workspaceDir);
249
+ const normalizedPath = path.replaceAll("\\", "/");
250
+ let relativePath;
251
+ if (normalizedPath.startsWith("/workspace/")) {
252
+ relativePath = normalizedPath.slice("/workspace/".length);
253
+ }
254
+ else if (normalizedPath.startsWith("workspace/")) {
255
+ relativePath = normalizedPath.slice("workspace/".length);
256
+ }
257
+ else if (isAbsolute(normalizedPath) ||
258
+ /^[A-Za-z]:\//.test(normalizedPath)) {
259
+ if (normalizedPath.startsWith("/")) {
260
+ relativePath = normalizedPath.slice(1);
261
+ }
262
+ else {
263
+ relativePath = normalizedPath.replace(/^[A-Za-z]:\//, "");
264
+ }
265
+ }
266
+ else {
267
+ relativePath = normalizedPath;
268
+ }
269
+ const absPath = resolve(join(workspaceRoot, relativePath));
270
+ if (!isWithinRoot(workspaceRoot, absPath)) {
271
+ throw new Error(`Resolved path '${absPath}' is outside the workspace directory.`);
272
+ }
273
+ return absPath;
274
+ }
275
+ // ---------------------------------------------------------------------------
276
+ // ProcessingContext
277
+ // ---------------------------------------------------------------------------
278
+ export class ProcessingContext {
279
+ jobId;
280
+ workflowId;
281
+ userId;
282
+ workspaceDir;
283
+ assetOutputMode;
284
+ environment;
285
+ /** Message queue: all emitted processing messages. */
286
+ _messages = [];
287
+ /** Latest node status by node id. */
288
+ _nodeStatuses = new Map();
289
+ /** Latest edge status by edge id. */
290
+ _edgeStatuses = new Map();
291
+ /** Optional message listener (for real-time streaming). */
292
+ _onMessage = null;
293
+ /** Cache adapter. */
294
+ cache;
295
+ /** Storage adapter (optional, for asset handling). */
296
+ storage;
297
+ /** Variables shared across node execution. */
298
+ _variables;
299
+ /** Optional async secret resolver. */
300
+ _secretResolver = null;
301
+ /** Fetch function used by HTTP helpers. */
302
+ _fetch;
303
+ /** Optional temporary URL resolver for stored assets. */
304
+ _tempUrlResolver = null;
305
+ /** Optional async provider resolver by provider id. */
306
+ _providerResolver = null;
307
+ /** Optional model-layer adapters used to mirror Python ProcessingContext APIs. */
308
+ _modelInterfaces = null;
309
+ /** Provider cache keyed by provider id. */
310
+ _providers = new Map();
311
+ /** In-context memory URI cache (memory:// key-value objects). */
312
+ _memory = new Map();
313
+ /** Optional control event dispatcher (set by workflow runner). */
314
+ _sendControlEvent = null;
315
+ /** Optional executor resolver for sub-workflow execution. */
316
+ _resolveExecutor = null;
317
+ /** Optional node type resolver for sub-workflow graph hydration. */
318
+ _resolveNodeType = null;
319
+ /** Total cost tracked for operations executed via this context. */
320
+ _totalCost = 0;
321
+ /** Per-operation cost entries. */
322
+ _operationCosts = [];
323
+ constructor(opts) {
324
+ this.jobId = opts.jobId;
325
+ this.workflowId = opts.workflowId ?? null;
326
+ this.userId = opts.userId ?? "default";
327
+ this.workspaceDir = opts.workspaceDir ?? null;
328
+ this.assetOutputMode = opts.assetOutputMode ?? "python";
329
+ this.cache = opts.cache ?? new MemoryCache();
330
+ this.storage = opts.storage ?? null;
331
+ this._onMessage = opts.onMessage ?? null;
332
+ this._variables = { ...(opts.variables ?? {}) };
333
+ const env = {};
334
+ for (const [k, v] of Object.entries(process.env)) {
335
+ if (typeof v === "string")
336
+ env[k] = v;
337
+ }
338
+ this.environment = { ...env, ...(opts.environment ?? {}) };
339
+ this._secretResolver = opts.secretResolver ?? null;
340
+ this._fetch =
341
+ opts.fetchFn ??
342
+ ((input, init) => fetch(input, init));
343
+ this._tempUrlResolver = opts.tempUrlResolver ?? null;
344
+ this._modelInterfaces = opts.modelInterfaces ?? null;
345
+ }
346
+ copy() {
347
+ const next = new ProcessingContext({
348
+ jobId: this.jobId,
349
+ workflowId: this.workflowId,
350
+ userId: this.userId,
351
+ workspaceDir: this.workspaceDir,
352
+ assetOutputMode: this.assetOutputMode,
353
+ cache: this.cache,
354
+ storage: this.storage,
355
+ onMessage: this._onMessage ?? undefined,
356
+ variables: { ...this._variables },
357
+ environment: { ...this.environment },
358
+ fetchFn: this._fetch,
359
+ secretResolver: this._secretResolver ?? undefined,
360
+ tempUrlResolver: this._tempUrlResolver ?? undefined
361
+ });
362
+ next._providerResolver = this._providerResolver;
363
+ next._modelInterfaces = this._modelInterfaces;
364
+ next._sendControlEvent = this._sendControlEvent;
365
+ next._providers = new Map(this._providers);
366
+ next._totalCost = this._totalCost;
367
+ next._operationCosts = this._operationCosts.map((c) => ({ ...c }));
368
+ return next;
369
+ }
370
+ // -----------------------------------------------------------------------
371
+ // Provider resolution
372
+ // -----------------------------------------------------------------------
373
+ setProviderResolver(resolver) {
374
+ this._providerResolver = resolver;
375
+ }
376
+ setModelInterfaces(modelInterfaces) {
377
+ this._modelInterfaces = modelInterfaces;
378
+ }
379
+ // -----------------------------------------------------------------------
380
+ // Control event dispatch
381
+ // -----------------------------------------------------------------------
382
+ /**
383
+ * Set the control event dispatcher (called by the workflow runner).
384
+ * This allows agent nodes to dispatch control events to controlled nodes
385
+ * and await their results.
386
+ */
387
+ setSendControlEvent(fn) {
388
+ this._sendControlEvent = fn;
389
+ }
390
+ /**
391
+ * Dispatch a control event to a target node and await its output.
392
+ * Returns null if no control event dispatcher is configured.
393
+ */
394
+ async sendControlEvent(targetNodeId, properties) {
395
+ if (!this._sendControlEvent)
396
+ return null;
397
+ return this._sendControlEvent(targetNodeId, properties);
398
+ }
399
+ // -----------------------------------------------------------------------
400
+ // Sub-workflow executor resolution
401
+ // -----------------------------------------------------------------------
402
+ setResolveExecutor(fn) {
403
+ this._resolveExecutor = fn;
404
+ }
405
+ get resolveExecutor() {
406
+ return this._resolveExecutor;
407
+ }
408
+ setResolveNodeType(fn) {
409
+ this._resolveNodeType = fn;
410
+ }
411
+ get resolveNodeType() {
412
+ return this._resolveNodeType;
413
+ }
414
+ /**
415
+ * Check if control event dispatch is available.
416
+ */
417
+ get hasControlEventSupport() {
418
+ return this._sendControlEvent !== null;
419
+ }
420
+ registerProvider(providerId, provider) {
421
+ this._providers.set(providerId, provider);
422
+ }
423
+ async getProvider(providerId) {
424
+ if (!providerId || providerId.trim() === "") {
425
+ throw new Error("providerId is required");
426
+ }
427
+ const cached = this._providers.get(providerId);
428
+ if (cached)
429
+ return cached;
430
+ if (!this._providerResolver) {
431
+ const { getRegisteredProvider } = await import("./providers/index.js");
432
+ const reg = getRegisteredProvider(providerId);
433
+ if (!reg) {
434
+ throw new Error(`No provider registered for "${providerId}"`);
435
+ }
436
+ // Resolve any missing secret kwargs from the context's own secret resolver
437
+ const kwargs = { ...reg.kwargs };
438
+ for (const [key, value] of Object.entries(kwargs)) {
439
+ if (!value) {
440
+ const envVal = process.env[key];
441
+ if (envVal) {
442
+ kwargs[key] = envVal;
443
+ }
444
+ else {
445
+ const secret = await this.getSecret(key);
446
+ if (secret) {
447
+ kwargs[key] = secret;
448
+ }
449
+ }
450
+ }
451
+ }
452
+ const resolved = new reg.cls(kwargs);
453
+ this._providers.set(providerId, resolved);
454
+ resolved.setMessageEmitter((msg) => this.postMessage(msg));
455
+ return resolved;
456
+ }
457
+ const resolved = await this._providerResolver(providerId);
458
+ this._providers.set(providerId, resolved);
459
+ resolved.setMessageEmitter((msg) => this.postMessage(msg));
460
+ return resolved;
461
+ }
462
+ async get_provider(providerId) {
463
+ return this.getProvider(providerId);
464
+ }
465
+ // -----------------------------------------------------------------------
466
+ // Secrets / Variables API
467
+ // -----------------------------------------------------------------------
468
+ setSecretResolver(resolver) {
469
+ this._secretResolver = resolver;
470
+ }
471
+ setTempUrlResolver(resolver) {
472
+ this._tempUrlResolver = resolver;
473
+ }
474
+ async getSecret(key) {
475
+ if (!this._secretResolver)
476
+ return null;
477
+ const value = await this._secretResolver(key, this.userId);
478
+ return value ?? null;
479
+ }
480
+ async getSecretRequired(key) {
481
+ const value = await this.getSecret(key);
482
+ if (value === null || value === "") {
483
+ throw new Error(`Missing required secret: ${key}`);
484
+ }
485
+ return value;
486
+ }
487
+ async get_secret(key) {
488
+ return this.getSecret(key);
489
+ }
490
+ async get_secret_required(key) {
491
+ return this.getSecretRequired(key);
492
+ }
493
+ get(key, defaultValue) {
494
+ if (Object.prototype.hasOwnProperty.call(this._variables, key)) {
495
+ return this._variables[key];
496
+ }
497
+ return defaultValue;
498
+ }
499
+ set(key, value) {
500
+ this._variables[key] = value;
501
+ void this.persistVariableIfNeeded(key, value);
502
+ if (key.startsWith("memory://")) {
503
+ this._memory.set(key, value);
504
+ }
505
+ }
506
+ async storeStepResult(key, value) {
507
+ const path = this.workspacePathFor(`${key}.json`);
508
+ await mkdir(dirname(path), { recursive: true });
509
+ await writeFile(path, JSON.stringify(value, null, 2), "utf8");
510
+ this._variables[key] = { __workspace_result__: `${key}.json` };
511
+ return path;
512
+ }
513
+ async loadStepResult(key, defaultValue) {
514
+ const marker = this._variables[key];
515
+ if (marker &&
516
+ typeof marker === "object" &&
517
+ "__workspace_result__" in marker) {
518
+ const rel = marker
519
+ .__workspace_result__;
520
+ const path = this.workspacePathFor(String(rel));
521
+ try {
522
+ const raw = await readFile(path, "utf8");
523
+ return JSON.parse(raw);
524
+ }
525
+ catch {
526
+ // File missing or corrupt — fall back to default.
527
+ return defaultValue;
528
+ }
529
+ }
530
+ if (Object.prototype.hasOwnProperty.call(this._variables, key)) {
531
+ return this._variables[key];
532
+ }
533
+ return defaultValue;
534
+ }
535
+ workspacePathFor(fileName) {
536
+ if (!this.workspaceDir) {
537
+ throw new Error("workspace_dir is required to persist workflow variables");
538
+ }
539
+ return resolveWorkspacePath(this.workspaceDir, fileName);
540
+ }
541
+ async persistVariableIfNeeded(key, value) {
542
+ if (!this.workspaceDir)
543
+ return;
544
+ if (value === undefined || value === null)
545
+ return;
546
+ if (typeof value === "function" ||
547
+ typeof value === "symbol" ||
548
+ typeof value === "bigint")
549
+ return;
550
+ const filePath = this.workspacePathFor(`var_${key}.json`);
551
+ try {
552
+ await mkdir(dirname(filePath), { recursive: true });
553
+ await writeFile(filePath, JSON.stringify(value, null, 2), "utf8");
554
+ }
555
+ catch {
556
+ // Best-effort persistence.
557
+ }
558
+ }
559
+ // -----------------------------------------------------------------------
560
+ // Node result cache helpers
561
+ // -----------------------------------------------------------------------
562
+ generateNodeCacheKey(nodeType, nodeProps) {
563
+ const normalizedProps = nodeProps && typeof nodeProps === "object"
564
+ ? JSON.stringify(nodeProps, Object.keys(nodeProps).sort())
565
+ : JSON.stringify(nodeProps ?? null);
566
+ return `${this.userId}:${nodeType}:${normalizedProps}`;
567
+ }
568
+ async getCachedResult(nodeType, nodeProps) {
569
+ const key = this.generateNodeCacheKey(nodeType, nodeProps);
570
+ return this.cache.get(key);
571
+ }
572
+ async cacheResult(nodeType, nodeProps, result, ttlSeconds = 3600) {
573
+ const key = this.generateNodeCacheKey(nodeType, nodeProps);
574
+ await this.cache.set(key, result, ttlSeconds);
575
+ }
576
+ // -----------------------------------------------------------------------
577
+ // Message queue API
578
+ // -----------------------------------------------------------------------
579
+ /**
580
+ * Emit a processing message.
581
+ * Appended to the internal queue and forwarded to listener if set.
582
+ */
583
+ emit(msg) {
584
+ this._messages.push(msg);
585
+ this._notifyMessage();
586
+ if (msg.type === "node_update" && msg.node_id) {
587
+ this._nodeStatuses.set(msg.node_id, msg);
588
+ }
589
+ if (msg.type === "edge_update" && msg.edge_id) {
590
+ this._edgeStatuses.set(msg.edge_id, msg);
591
+ }
592
+ if (this._onMessage) {
593
+ this._onMessage(msg);
594
+ }
595
+ }
596
+ postMessage(msg) {
597
+ this.emit(msg);
598
+ }
599
+ post_message(msg) {
600
+ this.emit(msg);
601
+ }
602
+ /** Get all emitted messages. */
603
+ getMessages() {
604
+ return this._messages;
605
+ }
606
+ hasMessages() {
607
+ return this._messages.length > 0;
608
+ }
609
+ popMessage() {
610
+ return this._messages.shift();
611
+ }
612
+ _messageResolve = null;
613
+ /** Notify that a new message has been pushed. */
614
+ _notifyMessage() {
615
+ if (this._messageResolve) {
616
+ const resolve = this._messageResolve;
617
+ this._messageResolve = null;
618
+ resolve();
619
+ }
620
+ }
621
+ async popMessageAsync() {
622
+ while (this._messages.length === 0) {
623
+ await new Promise((r) => {
624
+ this._messageResolve = r;
625
+ });
626
+ }
627
+ return this._messages.shift();
628
+ }
629
+ async pop_message_async() {
630
+ return this.popMessageAsync();
631
+ }
632
+ getNodeStatuses() {
633
+ return Object.fromEntries(this._nodeStatuses);
634
+ }
635
+ getEdgeStatuses() {
636
+ return Object.fromEntries(this._edgeStatuses);
637
+ }
638
+ /** Clear the message queue. */
639
+ clearMessages() {
640
+ this._messages = [];
641
+ }
642
+ clear_messages() {
643
+ this.clearMessages();
644
+ }
645
+ trackOperationCost(operation, cost, metadata) {
646
+ const safeCost = Number.isFinite(cost) ? cost : 0;
647
+ this._totalCost += safeCost;
648
+ this._operationCosts.push({
649
+ operation,
650
+ cost: safeCost,
651
+ timestamp: new Date().toISOString(),
652
+ ...(metadata ?? {})
653
+ });
654
+ }
655
+ addToTotalCost(cost) {
656
+ const safeCost = Number.isFinite(cost) ? cost : 0;
657
+ this._totalCost += safeCost;
658
+ }
659
+ resetTotalCost() {
660
+ this._totalCost = 0;
661
+ this._operationCosts = [];
662
+ }
663
+ getTotalCost() {
664
+ return this._totalCost;
665
+ }
666
+ getOperationCosts() {
667
+ return this._operationCosts;
668
+ }
669
+ // -----------------------------------------------------------------------
670
+ // Memory cache helpers
671
+ // -----------------------------------------------------------------------
672
+ clearMemory(pattern) {
673
+ if (!pattern) {
674
+ this._memory.clear();
675
+ return;
676
+ }
677
+ for (const key of this._memory.keys()) {
678
+ if (key.includes(pattern)) {
679
+ this._memory.delete(key);
680
+ }
681
+ }
682
+ }
683
+ getMemoryStats() {
684
+ const byPrefix = {};
685
+ for (const key of this._memory.keys()) {
686
+ const withoutScheme = key.replace(/^memory:\/\//, "");
687
+ const prefix = withoutScheme.includes("/")
688
+ ? withoutScheme.split("/")[0]
689
+ : withoutScheme;
690
+ byPrefix[prefix] = (byPrefix[prefix] ?? 0) + 1;
691
+ }
692
+ return { total: this._memory.size, byPrefix };
693
+ }
694
+ // -----------------------------------------------------------------------
695
+ // HTTP helpers
696
+ // -----------------------------------------------------------------------
697
+ async httpRequestWithRetries(method, url, opts = {}) {
698
+ const retry = opts.retry ?? {};
699
+ const maxRetries = retry.maxRetries ?? 3;
700
+ const backoffMs = retry.backoffMs ?? 500;
701
+ const retryStatuses = retry.retryStatuses ?? [
702
+ 408, 425, 429, 500, 502, 503, 504
703
+ ];
704
+ const headers = {
705
+ "User-Agent": "nodetool-ts-runtime/0.1",
706
+ Accept: "*/*",
707
+ ...(opts.headers ?? {})
708
+ };
709
+ let lastError;
710
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
711
+ try {
712
+ const response = await this._fetch(url, {
713
+ ...opts,
714
+ method,
715
+ headers
716
+ });
717
+ if (retryStatuses.includes(response.status) &&
718
+ attempt < maxRetries - 1) {
719
+ const retryAfter = response.headers.get("Retry-After");
720
+ const retryDelay = retryAfter ? Number(retryAfter) * 1000 : NaN;
721
+ const delayMs = Number.isFinite(retryDelay)
722
+ ? retryDelay
723
+ : backoffMs * 2 ** attempt;
724
+ await new Promise((r) => setTimeout(r, Math.max(0, delayMs)));
725
+ continue;
726
+ }
727
+ if (!response.ok) {
728
+ throw new Error(`HTTP ${response.status} ${response.statusText} for ${method} ${url}`);
729
+ }
730
+ return response;
731
+ }
732
+ catch (error) {
733
+ lastError = error;
734
+ if (attempt >= maxRetries - 1)
735
+ break;
736
+ const delayMs = backoffMs * 2 ** attempt;
737
+ await new Promise((r) => setTimeout(r, delayMs));
738
+ }
739
+ }
740
+ throw lastError instanceof Error
741
+ ? lastError
742
+ : new Error(`HTTP request failed: ${method} ${url}`);
743
+ }
744
+ async httpGet(url, opts = {}) {
745
+ return this.httpRequestWithRetries("GET", url, opts);
746
+ }
747
+ async httpPost(url, opts = {}) {
748
+ return this.httpRequestWithRetries("POST", url, opts);
749
+ }
750
+ async httpPatch(url, opts = {}) {
751
+ return this.httpRequestWithRetries("PATCH", url, opts);
752
+ }
753
+ async httpPut(url, opts = {}) {
754
+ return this.httpRequestWithRetries("PUT", url, opts);
755
+ }
756
+ async httpDelete(url, opts = {}) {
757
+ return this.httpRequestWithRetries("DELETE", url, opts);
758
+ }
759
+ async httpHead(url, opts = {}) {
760
+ return this.httpRequestWithRetries("HEAD", url, opts);
761
+ }
762
+ async downloadFile(url, opts = {}) {
763
+ const response = await this.httpGet(url, opts);
764
+ const arr = await response.arrayBuffer();
765
+ return new Uint8Array(arr);
766
+ }
767
+ async downloadText(url, opts = {}) {
768
+ const response = await this.httpGet(url, opts);
769
+ return response.text();
770
+ }
771
+ async http_get(url, opts = {}) {
772
+ return this.httpGet(url, opts);
773
+ }
774
+ async http_post(url, opts = {}) {
775
+ return this.httpPost(url, opts);
776
+ }
777
+ async http_patch(url, opts = {}) {
778
+ return this.httpPatch(url, opts);
779
+ }
780
+ async http_put(url, opts = {}) {
781
+ return this.httpPut(url, opts);
782
+ }
783
+ async http_delete(url, opts = {}) {
784
+ return this.httpDelete(url, opts);
785
+ }
786
+ async http_head(url, opts = {}) {
787
+ return this.httpHead(url, opts);
788
+ }
789
+ requireModelInterface(name) {
790
+ const fn = this._modelInterfaces?.[name];
791
+ if (!fn) {
792
+ throw new Error(`ProcessingContext model interface '${String(name)}' is not configured`);
793
+ }
794
+ return fn;
795
+ }
796
+ async getJob(jobId) {
797
+ const fn = this.requireModelInterface("getJob");
798
+ return fn({ userId: this.userId, jobId });
799
+ }
800
+ async get_job(jobId) {
801
+ return this.getJob(jobId);
802
+ }
803
+ async createAsset(args) {
804
+ const fn = this.requireModelInterface("createAsset");
805
+ const content = args.content ?? args.instructions;
806
+ if (!content) {
807
+ throw new Error("Asset content is required");
808
+ }
809
+ return fn({
810
+ userId: this.userId,
811
+ workflowId: this.workflowId,
812
+ jobId: this.jobId,
813
+ name: args.name,
814
+ contentType: args.contentType,
815
+ content,
816
+ parentId: args.parentId ?? null,
817
+ nodeId: args.nodeId ?? null
818
+ });
819
+ }
820
+ async create_asset(args) {
821
+ return this.createAsset(args);
822
+ }
823
+ resolveSandboxFilePath(path) {
824
+ if (this.workspaceDir == null || this.workspaceDir === "") {
825
+ throw new Error("workspaceDir is required for sandbox file operations");
826
+ }
827
+ const workspaceRoot = resolve(this.workspaceDir);
828
+ const normalizedPath = path.replaceAll("\\", "/");
829
+ if ((isAbsolute(normalizedPath) || /^[A-Za-z]:\//.test(normalizedPath)) &&
830
+ isWithinRoot(workspaceRoot, resolve(normalizedPath))) {
831
+ return resolve(normalizedPath);
832
+ }
833
+ return resolveWorkspacePath(this.workspaceDir, path);
834
+ }
835
+ static guessMimeFromPath(path) {
836
+ const ext = extname(path).toLowerCase();
837
+ const byExt = {
838
+ ".png": "image/png",
839
+ ".jpg": "image/jpeg",
840
+ ".jpeg": "image/jpeg",
841
+ ".webp": "image/webp",
842
+ ".gif": "image/gif",
843
+ ".bmp": "image/bmp",
844
+ ".svg": "image/svg+xml",
845
+ ".wav": "audio/wav",
846
+ ".mp3": "audio/mpeg",
847
+ ".ogg": "audio/ogg",
848
+ ".flac": "audio/flac",
849
+ ".m4a": "audio/mp4",
850
+ ".mp4": "video/mp4",
851
+ ".webm": "video/webm",
852
+ ".mov": "video/quicktime",
853
+ ".txt": "text/plain",
854
+ ".md": "text/markdown",
855
+ ".json": "application/json",
856
+ ".pdf": "application/pdf",
857
+ ".csv": "text/csv"
858
+ };
859
+ return byExt[ext] ?? "application/octet-stream";
860
+ }
861
+ static assetTypeForMime(mime) {
862
+ if (mime.startsWith("image/"))
863
+ return "image";
864
+ if (mime.startsWith("audio/"))
865
+ return "audio";
866
+ if (mime.startsWith("video/"))
867
+ return "video";
868
+ if (mime.startsWith("text/"))
869
+ return "text";
870
+ if (mime === "application/pdf")
871
+ return "document";
872
+ return "asset";
873
+ }
874
+ parseAssetIdCandidates(assetId) {
875
+ const trimmed = assetId.trim();
876
+ if (!trimmed) {
877
+ return [];
878
+ }
879
+ const raw = trimmed.startsWith("asset://") ? trimmed.slice("asset://".length) : trimmed;
880
+ const primary = raw.split(/[/?#]/)[0];
881
+ if (!primary) {
882
+ return [];
883
+ }
884
+ const withoutExt = primary.replace(/\.[^.]+$/, "");
885
+ return Array.from(new Set([primary, withoutExt].filter(Boolean)));
886
+ }
887
+ async assetToSandbox(assetId, path) {
888
+ const outputPath = this.resolveSandboxFilePath(path);
889
+ const uriCandidates = new Set();
890
+ const idCandidates = this.parseAssetIdCandidates(assetId);
891
+ const trimmed = assetId.trim();
892
+ const resolutionAttempts = [];
893
+ if (trimmed.includes("://") && !trimmed.startsWith("asset://")) {
894
+ uriCandidates.add(trimmed);
895
+ }
896
+ for (const candidate of idCandidates) {
897
+ for (const prefix of ["memory://", "file://", "s3://"]) {
898
+ uriCandidates.add(`${prefix}${candidate}`);
899
+ uriCandidates.add(`${prefix}assets/${candidate}`);
900
+ }
901
+ }
902
+ let bytes = null;
903
+ if (this.storage) {
904
+ for (const uri of uriCandidates) {
905
+ try {
906
+ const retrieved = await this.storage.retrieve(uri);
907
+ if (retrieved) {
908
+ bytes = retrieved;
909
+ break;
910
+ }
911
+ resolutionAttempts.push(`storage miss: ${uri}`);
912
+ }
913
+ catch (error) {
914
+ resolutionAttempts.push(`storage error: ${uri} (${error instanceof Error ? error.message : String(error)})`);
915
+ }
916
+ }
917
+ }
918
+ if (!bytes) {
919
+ let baseUrl = (this.environment.NODETOOL_API_URL ??
920
+ process.env.NODETOOL_API_URL ??
921
+ "http://localhost:7777");
922
+ while (baseUrl.endsWith("/")) {
923
+ baseUrl = baseUrl.slice(0, -1);
924
+ }
925
+ for (const candidate of idCandidates) {
926
+ try {
927
+ const metaResponse = await this.httpGet(`${baseUrl}/api/assets/${encodeURIComponent(candidate)}`);
928
+ const metadata = (await metaResponse.json());
929
+ const getUrl = metadata.get_url;
930
+ const uri = metadata.uri;
931
+ if (typeof getUrl === "string" && getUrl) {
932
+ const downloadUrl = getUrl.startsWith("/")
933
+ ? `${baseUrl}${getUrl}`
934
+ : getUrl;
935
+ bytes = await this.downloadFile(downloadUrl, {
936
+ retry: { maxRetries: 1, backoffMs: 200 }
937
+ });
938
+ resolutionAttempts.push(`downloaded: ${downloadUrl}`);
939
+ break;
940
+ }
941
+ if (typeof uri === "string" && this.storage) {
942
+ const retrieved = await this.storage.retrieve(uri);
943
+ if (retrieved) {
944
+ bytes = retrieved;
945
+ resolutionAttempts.push(`storage hit via metadata: ${uri}`);
946
+ break;
947
+ }
948
+ resolutionAttempts.push(`storage miss via metadata: ${uri}`);
949
+ }
950
+ }
951
+ catch (error) {
952
+ resolutionAttempts.push(`api lookup error for ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
953
+ }
954
+ }
955
+ }
956
+ if (!bytes) {
957
+ const details = resolutionAttempts.length > 0
958
+ ? ` Attempts: ${resolutionAttempts.join("; ")}`
959
+ : "";
960
+ throw new Error(`Unable to resolve asset '${assetId}' to sandbox bytes.${details}`);
961
+ }
962
+ await mkdir(dirname(outputPath), { recursive: true });
963
+ await writeFile(outputPath, bytes);
964
+ return outputPath;
965
+ }
966
+ async asset_to_sandbox(asset_id, path) {
967
+ return this.assetToSandbox(asset_id, path);
968
+ }
969
+ async sandboxToAsset(path) {
970
+ const filePath = this.resolveSandboxFilePath(path);
971
+ const bytes = await readFile(filePath);
972
+ const contentType = ProcessingContext.guessMimeFromPath(filePath);
973
+ const created = (await this.createAsset({
974
+ name: basename(filePath),
975
+ contentType,
976
+ content: bytes
977
+ }));
978
+ const type = ProcessingContext.assetTypeForMime(contentType);
979
+ const assetId = typeof created.id === "string" ? created.id : null;
980
+ const uri = assetId !== null
981
+ ? `asset://${assetId}`
982
+ : typeof created.uri === "string"
983
+ ? created.uri
984
+ : pathToFileURL(filePath).toString();
985
+ return {
986
+ type,
987
+ uri,
988
+ asset_id: assetId
989
+ };
990
+ }
991
+ async sandbox_to_asset(path) {
992
+ return this.sandboxToAsset(path);
993
+ }
994
+ async createMessage(req) {
995
+ if (!req.thread_id) {
996
+ throw new Error("Thread ID is required");
997
+ }
998
+ const fn = this.requireModelInterface("createMessage");
999
+ return fn({ userId: this.userId, req });
1000
+ }
1001
+ async create_message(req) {
1002
+ return this.createMessage(req);
1003
+ }
1004
+ async getThreadMessages(threadId, limit = 10, startKey = null, reverse = false) {
1005
+ const fn = this.requireModelInterface("getMessages");
1006
+ return fn({
1007
+ userId: this.userId,
1008
+ threadId,
1009
+ limit,
1010
+ startKey,
1011
+ reverse
1012
+ });
1013
+ }
1014
+ async get_messages(thread_id, limit = 10, start_key = null, reverse = false) {
1015
+ return this.getThreadMessages(thread_id, limit, start_key, reverse);
1016
+ }
1017
+ async assetsToDataUri(value) {
1018
+ return this.normalizeOutputValue(value, "data_uri");
1019
+ }
1020
+ async assetsToStorageUrl(value) {
1021
+ return this.normalizeOutputValue(value, "storage_url");
1022
+ }
1023
+ async assetsToWorkspaceFiles(value) {
1024
+ return this.normalizeOutputValue(value, "workspace");
1025
+ }
1026
+ async uploadAssetsToTemp(value) {
1027
+ return this.normalizeOutputValue(value, "temp_url");
1028
+ }
1029
+ // -----------------------------------------------------------------------
1030
+ // Provider prediction pipeline
1031
+ // -----------------------------------------------------------------------
1032
+ emitPrediction(status, req, id, data, error, startedAt) {
1033
+ this.emit({
1034
+ type: "prediction",
1035
+ id,
1036
+ user_id: this.userId,
1037
+ node_id: req.nodeId ?? "",
1038
+ workflow_id: req.workflowId ?? this.workflowId ?? null,
1039
+ provider: req.provider,
1040
+ model: req.model,
1041
+ status,
1042
+ params: req.params ?? {},
1043
+ data: data ?? null,
1044
+ error: error ?? null,
1045
+ duration: startedAt ? Date.now() - startedAt : null
1046
+ });
1047
+ }
1048
+ /**
1049
+ * Resolve /api/storage/ URIs in message content to data URIs so providers
1050
+ * can fetch them without needing a base URL.
1051
+ */
1052
+ async resolveMessageMediaUris(messages) {
1053
+ if (!this.storage)
1054
+ return messages;
1055
+ const resolved = [];
1056
+ for (const msg of messages) {
1057
+ if (!Array.isArray(msg.content)) {
1058
+ resolved.push(msg);
1059
+ continue;
1060
+ }
1061
+ const parts = [];
1062
+ for (const part of msg.content) {
1063
+ if (part.type === "image_url" &&
1064
+ part.image.uri &&
1065
+ !part.image.uri.startsWith("data:") &&
1066
+ !part.image.uri.startsWith("http")) {
1067
+ const bytes = await this.storage.retrieve(part.image.uri);
1068
+ if (bytes) {
1069
+ const ext = part.image.uri.split(".").pop()?.toLowerCase() ?? "png";
1070
+ const mime = {
1071
+ png: "image/png",
1072
+ jpg: "image/jpeg",
1073
+ jpeg: "image/jpeg",
1074
+ gif: "image/gif",
1075
+ webp: "image/webp"
1076
+ };
1077
+ const mimeType = mime[ext] ?? part.image.mimeType ?? "image/png";
1078
+ const b64 = Buffer.from(bytes).toString("base64");
1079
+ parts.push({
1080
+ type: "image_url",
1081
+ image: { uri: `data:${mimeType};base64,${b64}`, mimeType }
1082
+ });
1083
+ continue;
1084
+ }
1085
+ }
1086
+ if (part.type === "audio" &&
1087
+ part.audio.uri &&
1088
+ !part.audio.uri.startsWith("data:") &&
1089
+ !part.audio.uri.startsWith("http")) {
1090
+ const bytes = await this.storage.retrieve(part.audio.uri);
1091
+ if (bytes) {
1092
+ const ext = part.audio.uri.split(".").pop()?.toLowerCase() ?? "mp3";
1093
+ const mime = {
1094
+ mp3: "audio/mpeg",
1095
+ wav: "audio/wav",
1096
+ ogg: "audio/ogg",
1097
+ m4a: "audio/mp4",
1098
+ flac: "audio/flac"
1099
+ };
1100
+ const mimeType = mime[ext] ?? part.audio.mimeType ?? "audio/mpeg";
1101
+ const b64 = Buffer.from(bytes).toString("base64");
1102
+ parts.push({
1103
+ type: "audio",
1104
+ audio: { uri: `data:${mimeType};base64,${b64}`, mimeType }
1105
+ });
1106
+ continue;
1107
+ }
1108
+ }
1109
+ parts.push(part);
1110
+ }
1111
+ resolved.push({ ...msg, content: parts });
1112
+ }
1113
+ return resolved;
1114
+ }
1115
+ async dispatchCapability(provider, req) {
1116
+ const params = req.params ?? {};
1117
+ switch (req.capability) {
1118
+ case "generate_message":
1119
+ return provider.generateMessageTraced({
1120
+ messages: await this.resolveMessageMediaUris(params.messages ?? []),
1121
+ model: req.model,
1122
+ tools: params.tools,
1123
+ toolChoice: params.tool_choice,
1124
+ maxTokens: params.max_tokens,
1125
+ temperature: params.temperature,
1126
+ topP: params.top_p,
1127
+ presencePenalty: params.presence_penalty,
1128
+ frequencyPenalty: params.frequency_penalty
1129
+ });
1130
+ case "text_to_image":
1131
+ return provider.textToImage({
1132
+ prompt: String(params.prompt ?? ""),
1133
+ model: { id: req.model, name: req.model, provider: req.provider },
1134
+ width: params.width,
1135
+ height: params.height,
1136
+ aspectRatio: params.aspect_ratio,
1137
+ resolution: params.resolution,
1138
+ negativePrompt: params.negative_prompt,
1139
+ quality: params.quality
1140
+ });
1141
+ case "image_to_image":
1142
+ return provider.imageToImage(params.image, {
1143
+ prompt: String(params.prompt ?? ""),
1144
+ model: { id: req.model, name: req.model, provider: req.provider },
1145
+ negativePrompt: params.negative_prompt,
1146
+ targetWidth: params.target_width,
1147
+ targetHeight: params.target_height,
1148
+ aspectRatio: params.aspect_ratio,
1149
+ resolution: params.resolution,
1150
+ strength: params.strength,
1151
+ quality: params.quality
1152
+ });
1153
+ case "text_to_video":
1154
+ return provider.textToVideo({
1155
+ prompt: String(params.prompt ?? ""),
1156
+ model: { id: req.model, name: req.model, provider: req.provider },
1157
+ negativePrompt: params.negative_prompt,
1158
+ numFrames: params.num_frames,
1159
+ durationSeconds: params.duration_seconds,
1160
+ aspectRatio: params.aspect_ratio,
1161
+ resolution: params.resolution
1162
+ });
1163
+ case "image_to_video":
1164
+ return provider.imageToVideo(params.image, {
1165
+ prompt: params.prompt,
1166
+ model: { id: req.model, name: req.model, provider: req.provider },
1167
+ negativePrompt: params.negative_prompt,
1168
+ numFrames: params.num_frames,
1169
+ durationSeconds: params.duration_seconds,
1170
+ aspectRatio: params.aspect_ratio,
1171
+ resolution: params.resolution
1172
+ });
1173
+ case "automatic_speech_recognition":
1174
+ return provider.automaticSpeechRecognition({
1175
+ audio: params.audio,
1176
+ model: req.model,
1177
+ language: params.language,
1178
+ prompt: params.prompt,
1179
+ temperature: params.temperature
1180
+ });
1181
+ case "generate_embedding":
1182
+ return provider.generateEmbedding({
1183
+ text: params.text ?? "",
1184
+ model: req.model,
1185
+ dimensions: params.dimensions
1186
+ });
1187
+ default:
1188
+ throw new Error(`Capability '${req.capability}' requires streaming API`);
1189
+ }
1190
+ }
1191
+ async runProviderPrediction(req) {
1192
+ const id = randomUUID();
1193
+ const startedAt = Date.now();
1194
+ this.emitPrediction("running", req, id, null, undefined, startedAt);
1195
+ try {
1196
+ const provider = await this.getProvider(req.provider);
1197
+ const output = await this.dispatchCapability(provider, req);
1198
+ this.emitPrediction("completed", req, id, output, undefined, startedAt);
1199
+ return output;
1200
+ }
1201
+ catch (error) {
1202
+ const message = error instanceof Error ? error.message : String(error);
1203
+ this.emitPrediction("failed", req, id, null, message, startedAt);
1204
+ throw error;
1205
+ }
1206
+ }
1207
+ async *streamProviderPrediction(req) {
1208
+ const id = randomUUID();
1209
+ const startedAt = Date.now();
1210
+ this.emitPrediction("running", req, id, null, undefined, startedAt);
1211
+ try {
1212
+ const provider = await this.getProvider(req.provider);
1213
+ const params = req.params ?? {};
1214
+ if (req.capability === "generate_messages") {
1215
+ for await (const item of provider.generateMessagesTraced({
1216
+ messages: await this.resolveMessageMediaUris(params.messages ?? []),
1217
+ model: req.model,
1218
+ tools: params.tools,
1219
+ maxTokens: params.max_tokens,
1220
+ temperature: params.temperature,
1221
+ topP: params.top_p,
1222
+ presencePenalty: params.presence_penalty,
1223
+ frequencyPenalty: params.frequency_penalty,
1224
+ audio: params.audio,
1225
+ threadId: params.thread_id
1226
+ })) {
1227
+ yield item;
1228
+ }
1229
+ }
1230
+ else if (req.capability === "text_to_speech") {
1231
+ for await (const item of provider.textToSpeech({
1232
+ text: String(params.text ?? ""),
1233
+ model: req.model,
1234
+ voice: params.voice,
1235
+ speed: params.speed
1236
+ })) {
1237
+ yield item;
1238
+ }
1239
+ }
1240
+ else {
1241
+ throw new Error(`Capability '${req.capability}' is not streamable`);
1242
+ }
1243
+ this.emitPrediction("completed", req, id, null, undefined, startedAt);
1244
+ }
1245
+ catch (error) {
1246
+ const message = error instanceof Error ? error.message : String(error);
1247
+ this.emitPrediction("failed", req, id, null, message, startedAt);
1248
+ throw error;
1249
+ }
1250
+ }
1251
+ // -----------------------------------------------------------------------
1252
+ // Output normalization
1253
+ // -----------------------------------------------------------------------
1254
+ /**
1255
+ * Sanitize memory URIs from output values before sending to client.
1256
+ * Replaces internal memory:// URIs with safe placeholders.
1257
+ *
1258
+ * Port of sanitize_memory_uris_for_client() from types.py.
1259
+ */
1260
+ static sanitizeForClient(value) {
1261
+ if (value === null || value === undefined)
1262
+ return value;
1263
+ const binary = ProcessingContext.toClientBytes(value);
1264
+ if (binary)
1265
+ return binary;
1266
+ if (Array.isArray(value)) {
1267
+ return value.map((v) => ProcessingContext.sanitizeForClient(v));
1268
+ }
1269
+ if (typeof value !== "object")
1270
+ return value;
1271
+ const obj = value;
1272
+ const uri = obj.uri;
1273
+ const isAssetLike = "type" in obj && typeof uri === "string";
1274
+ if (isAssetLike && uri.startsWith("memory://")) {
1275
+ const sanitized = { ...obj };
1276
+ if (sanitized.data !== undefined && sanitized.data !== null) {
1277
+ sanitized.uri = "";
1278
+ }
1279
+ else if (sanitized.asset_id) {
1280
+ sanitized.uri = `asset://${String(sanitized.asset_id)}`;
1281
+ }
1282
+ else {
1283
+ sanitized.uri = "";
1284
+ }
1285
+ const result = {};
1286
+ for (const [k, v] of Object.entries(sanitized)) {
1287
+ if (k === "uri" || k === "data" || k === "asset_id") {
1288
+ result[k] = v;
1289
+ }
1290
+ else {
1291
+ result[k] = ProcessingContext.sanitizeForClient(v);
1292
+ }
1293
+ }
1294
+ return result;
1295
+ }
1296
+ const result = {};
1297
+ for (const [k, v] of Object.entries(obj)) {
1298
+ result[k] = ProcessingContext.sanitizeForClient(v);
1299
+ }
1300
+ return result;
1301
+ }
1302
+ static toClientBytes(value) {
1303
+ if (value instanceof Uint8Array) {
1304
+ return { type: "bytes", length: value.length };
1305
+ }
1306
+ if (value instanceof ArrayBuffer) {
1307
+ return { type: "bytes", length: value.byteLength };
1308
+ }
1309
+ return null;
1310
+ }
1311
+ /**
1312
+ * Resolve a file path against the configured workspace root.
1313
+ */
1314
+ resolveWorkspacePath(path) {
1315
+ return resolveWorkspacePath(this.workspaceDir, path);
1316
+ }
1317
+ static isAssetLike(value) {
1318
+ if (!value || typeof value !== "object" || Array.isArray(value))
1319
+ return false;
1320
+ const v = value;
1321
+ return "type" in v && ("uri" in v || "data" in v || "asset_id" in v);
1322
+ }
1323
+ static guessAssetMime(asset) {
1324
+ const explicit = asset.mime_type ?? asset.content_type;
1325
+ if (typeof explicit === "string" && explicit)
1326
+ return explicit;
1327
+ const type = String(asset.type ?? "").toLowerCase();
1328
+ if (type.includes("image"))
1329
+ return "image/png";
1330
+ if (type.includes("audio"))
1331
+ return "audio/wav";
1332
+ if (type.includes("video"))
1333
+ return "video/mp4";
1334
+ if (type.includes("text"))
1335
+ return "text/plain";
1336
+ if (type.includes("model3d"))
1337
+ return "model/gltf-binary";
1338
+ return "application/octet-stream";
1339
+ }
1340
+ static extForMime(mime) {
1341
+ const map = {
1342
+ "image/png": "png",
1343
+ "image/jpeg": "jpg",
1344
+ "image/webp": "webp",
1345
+ "audio/wav": "wav",
1346
+ "audio/mpeg": "mp3",
1347
+ "video/mp4": "mp4",
1348
+ "text/plain": "txt",
1349
+ "application/json": "json",
1350
+ "model/gltf-binary": "glb"
1351
+ };
1352
+ return map[mime] ?? "bin";
1353
+ }
1354
+ static decodeAssetData(data) {
1355
+ if (data === null || data === undefined)
1356
+ return null;
1357
+ if (data instanceof Uint8Array)
1358
+ return data;
1359
+ if (Array.isArray(data) && data.every((v) => Number.isInteger(v))) {
1360
+ return new Uint8Array(data);
1361
+ }
1362
+ if (typeof data === "string") {
1363
+ return Uint8Array.from(Buffer.from(data, "base64"));
1364
+ }
1365
+ return null;
1366
+ }
1367
+ async getAssetBytes(asset) {
1368
+ const decoded = ProcessingContext.decodeAssetData(asset.data);
1369
+ if (decoded)
1370
+ return decoded;
1371
+ const uri = asset.uri;
1372
+ if (typeof uri !== "string" || !this.storage)
1373
+ return null;
1374
+ return this.storage.retrieve(uri);
1375
+ }
1376
+ async materializeAsset(asset, mode) {
1377
+ if (mode === "python" || mode === "raw") {
1378
+ return asset;
1379
+ }
1380
+ const bytes = await this.getAssetBytes(asset);
1381
+ if (!bytes)
1382
+ return asset;
1383
+ const mime = ProcessingContext.guessAssetMime(asset);
1384
+ if (mode === "data_uri") {
1385
+ const encoded = Buffer.from(bytes).toString("base64");
1386
+ return {
1387
+ ...asset,
1388
+ uri: `data:${mime};base64,${encoded}`
1389
+ };
1390
+ }
1391
+ if (mode === "storage_url") {
1392
+ if (!this.storage)
1393
+ return asset;
1394
+ const key = `assets/${randomUUID()}.${ProcessingContext.extForMime(mime)}`;
1395
+ const uri = await this.storage.store(key, bytes, mime);
1396
+ return {
1397
+ ...asset,
1398
+ uri,
1399
+ data: undefined
1400
+ };
1401
+ }
1402
+ if (mode === "temp_url") {
1403
+ if (!this.storage)
1404
+ return asset;
1405
+ const key = `temp/${randomUUID()}.${ProcessingContext.extForMime(mime)}`;
1406
+ const storedUri = await this.storage.store(key, bytes, mime);
1407
+ const resolvedUri = this._tempUrlResolver
1408
+ ? await this._tempUrlResolver(storedUri)
1409
+ : storedUri;
1410
+ return {
1411
+ ...asset,
1412
+ uri: resolvedUri,
1413
+ data: undefined
1414
+ };
1415
+ }
1416
+ if (mode === "workspace") {
1417
+ if (!this.workspaceDir) {
1418
+ throw new Error("workspace_dir is required for workspace asset output");
1419
+ }
1420
+ const workspaceAssets = new FileStorageAdapter(resolveWorkspacePath(this.workspaceDir, "assets"));
1421
+ const key = `${randomUUID()}.${ProcessingContext.extForMime(mime)}`;
1422
+ const uri = await workspaceAssets.store(key, bytes, mime);
1423
+ return {
1424
+ ...asset,
1425
+ uri,
1426
+ data: undefined
1427
+ };
1428
+ }
1429
+ return asset;
1430
+ }
1431
+ /**
1432
+ * Recursively normalize workflow outputs, materializing asset-like values
1433
+ * according to the selected output mode.
1434
+ */
1435
+ async normalizeOutputValue(value, mode = this.assetOutputMode) {
1436
+ if (value === null || value === undefined)
1437
+ return value;
1438
+ const binary = ProcessingContext.toClientBytes(value);
1439
+ if (binary)
1440
+ return binary;
1441
+ if (Array.isArray(value)) {
1442
+ return Promise.all(value.map((item) => this.normalizeOutputValue(item, mode)));
1443
+ }
1444
+ if (ProcessingContext.isAssetLike(value)) {
1445
+ return this.materializeAsset(value, mode);
1446
+ }
1447
+ if (typeof value === "object") {
1448
+ const entries = Object.entries(value);
1449
+ const normalized = await Promise.all(entries.map(async ([k, v]) => [k, await this.normalizeOutputValue(v, mode)]));
1450
+ return Object.fromEntries(normalized);
1451
+ }
1452
+ return value;
1453
+ }
1454
+ }
1455
+ //# sourceMappingURL=context.js.map