@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.
- package/dist/bridge-protocol.d.ts +39 -0
- package/dist/bridge-protocol.d.ts.map +1 -0
- package/dist/bridge-protocol.js +39 -0
- package/dist/bridge-protocol.js.map +1 -0
- package/dist/comfy-executor.d.ts +41 -0
- package/dist/comfy-executor.d.ts.map +1 -0
- package/dist/comfy-executor.js +305 -0
- package/dist/comfy-executor.js.map +1 -0
- package/dist/context-packer.d.ts +22 -0
- package/dist/context-packer.d.ts.map +1 -0
- package/dist/context-packer.js +66 -0
- package/dist/context-packer.js.map +1 -0
- package/dist/context.d.ts +405 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +1455 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node-executor.d.ts +53 -0
- package/dist/node-executor.d.ts.map +1 -0
- package/dist/node-executor.js +9 -0
- package/dist/node-executor.js.map +1 -0
- package/dist/providers/aki-manifest.json +34 -0
- package/dist/providers/aki-provider.d.ts +65 -0
- package/dist/providers/aki-provider.d.ts.map +1 -0
- package/dist/providers/aki-provider.js +517 -0
- package/dist/providers/aki-provider.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +55 -0
- package/dist/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/providers/anthropic-provider.js +563 -0
- package/dist/providers/anthropic-provider.js.map +1 -0
- package/dist/providers/base-provider.d.ts +194 -0
- package/dist/providers/base-provider.d.ts.map +1 -0
- package/dist/providers/base-provider.js +515 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/cerebras-provider.d.ts +20 -0
- package/dist/providers/cerebras-provider.d.ts.map +1 -0
- package/dist/providers/cerebras-provider.js +52 -0
- package/dist/providers/cerebras-provider.js.map +1 -0
- package/dist/providers/claude-agent-provider.d.ts +107 -0
- package/dist/providers/claude-agent-provider.d.ts.map +1 -0
- package/dist/providers/claude-agent-provider.js +586 -0
- package/dist/providers/claude-agent-provider.js.map +1 -0
- package/dist/providers/cohere-provider.d.ts +36 -0
- package/dist/providers/cohere-provider.d.ts.map +1 -0
- package/dist/providers/cohere-provider.js +116 -0
- package/dist/providers/cohere-provider.js.map +1 -0
- package/dist/providers/cost-calculator.d.ts +109 -0
- package/dist/providers/cost-calculator.d.ts.map +1 -0
- package/dist/providers/cost-calculator.js +475 -0
- package/dist/providers/cost-calculator.js.map +1 -0
- package/dist/providers/deepseek-provider.d.ts +25 -0
- package/dist/providers/deepseek-provider.d.ts.map +1 -0
- package/dist/providers/deepseek-provider.js +58 -0
- package/dist/providers/deepseek-provider.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +52 -0
- package/dist/providers/fake-provider.d.ts.map +1 -0
- package/dist/providers/fake-provider.js +107 -0
- package/dist/providers/fake-provider.js.map +1 -0
- package/dist/providers/fal-provider.d.ts +32 -0
- package/dist/providers/fal-provider.d.ts.map +1 -0
- package/dist/providers/fal-provider.js +445 -0
- package/dist/providers/fal-provider.js.map +1 -0
- package/dist/providers/gemini-provider.d.ts +109 -0
- package/dist/providers/gemini-provider.d.ts.map +1 -0
- package/dist/providers/gemini-provider.js +943 -0
- package/dist/providers/gemini-provider.js.map +1 -0
- package/dist/providers/groq-provider.d.ts +20 -0
- package/dist/providers/groq-provider.d.ts.map +1 -0
- package/dist/providers/groq-provider.js +52 -0
- package/dist/providers/groq-provider.js.map +1 -0
- package/dist/providers/huggingface-provider.d.ts +59 -0
- package/dist/providers/huggingface-provider.d.ts.map +1 -0
- package/dist/providers/huggingface-provider.js +305 -0
- package/dist/providers/huggingface-provider.js.map +1 -0
- package/dist/providers/index.d.ts +69 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +137 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/jina-provider.d.ts +37 -0
- package/dist/providers/jina-provider.d.ts.map +1 -0
- package/dist/providers/jina-provider.js +118 -0
- package/dist/providers/jina-provider.js.map +1 -0
- package/dist/providers/kie-provider.d.ts +19 -0
- package/dist/providers/kie-provider.d.ts.map +1 -0
- package/dist/providers/kie-provider.js +118 -0
- package/dist/providers/kie-provider.js.map +1 -0
- package/dist/providers/llama-provider.d.ts +49 -0
- package/dist/providers/llama-provider.d.ts.map +1 -0
- package/dist/providers/llama-provider.js +381 -0
- package/dist/providers/llama-provider.js.map +1 -0
- package/dist/providers/lmstudio-provider.d.ts +22 -0
- package/dist/providers/lmstudio-provider.d.ts.map +1 -0
- package/dist/providers/lmstudio-provider.js +63 -0
- package/dist/providers/lmstudio-provider.js.map +1 -0
- package/dist/providers/manifest-models.d.ts +12 -0
- package/dist/providers/manifest-models.d.ts.map +1 -0
- package/dist/providers/manifest-models.js +96 -0
- package/dist/providers/manifest-models.js.map +1 -0
- package/dist/providers/meshy-provider.d.ts +53 -0
- package/dist/providers/meshy-provider.d.ts.map +1 -0
- package/dist/providers/meshy-provider.js +230 -0
- package/dist/providers/meshy-provider.js.map +1 -0
- package/dist/providers/minimax-provider.d.ts +66 -0
- package/dist/providers/minimax-provider.d.ts.map +1 -0
- package/dist/providers/minimax-provider.js +531 -0
- package/dist/providers/minimax-provider.js.map +1 -0
- package/dist/providers/mistral-provider.d.ts +26 -0
- package/dist/providers/mistral-provider.d.ts.map +1 -0
- package/dist/providers/mistral-provider.js +74 -0
- package/dist/providers/mistral-provider.js.map +1 -0
- package/dist/providers/moonshot-provider.d.ts +24 -0
- package/dist/providers/moonshot-provider.d.ts.map +1 -0
- package/dist/providers/moonshot-provider.js +43 -0
- package/dist/providers/moonshot-provider.js.map +1 -0
- package/dist/providers/ollama-provider.d.ts +71 -0
- package/dist/providers/ollama-provider.d.ts.map +1 -0
- package/dist/providers/ollama-provider.js +480 -0
- package/dist/providers/ollama-provider.js.map +1 -0
- package/dist/providers/openai-provider.d.ts +103 -0
- package/dist/providers/openai-provider.d.ts.map +1 -0
- package/dist/providers/openai-provider.js +917 -0
- package/dist/providers/openai-provider.js.map +1 -0
- package/dist/providers/openrouter-provider.d.ts +49 -0
- package/dist/providers/openrouter-provider.d.ts.map +1 -0
- package/dist/providers/openrouter-provider.js +147 -0
- package/dist/providers/openrouter-provider.js.map +1 -0
- package/dist/providers/provider-registry.d.ts +24 -0
- package/dist/providers/provider-registry.d.ts.map +1 -0
- package/dist/providers/provider-registry.js +124 -0
- package/dist/providers/provider-registry.js.map +1 -0
- package/dist/providers/python-provider.d.ts +75 -0
- package/dist/providers/python-provider.d.ts.map +1 -0
- package/dist/providers/python-provider.js +157 -0
- package/dist/providers/python-provider.js.map +1 -0
- package/dist/providers/replicate-provider.d.ts +73 -0
- package/dist/providers/replicate-provider.d.ts.map +1 -0
- package/dist/providers/replicate-provider.js +484 -0
- package/dist/providers/replicate-provider.js.map +1 -0
- package/dist/providers/rodin-provider.d.ts +41 -0
- package/dist/providers/rodin-provider.d.ts.map +1 -0
- package/dist/providers/rodin-provider.js +236 -0
- package/dist/providers/rodin-provider.js.map +1 -0
- package/dist/providers/scripted-provider.d.ts +141 -0
- package/dist/providers/scripted-provider.d.ts.map +1 -0
- package/dist/providers/scripted-provider.js +246 -0
- package/dist/providers/scripted-provider.js.map +1 -0
- package/dist/providers/together-provider.d.ts +20 -0
- package/dist/providers/together-provider.d.ts.map +1 -0
- package/dist/providers/together-provider.js +56 -0
- package/dist/providers/together-provider.js.map +1 -0
- package/dist/providers/types.d.ts +202 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/vllm-provider.d.ts +22 -0
- package/dist/providers/vllm-provider.d.ts.map +1 -0
- package/dist/providers/vllm-provider.js +66 -0
- package/dist/providers/vllm-provider.js.map +1 -0
- package/dist/providers/voyage-provider.d.ts +37 -0
- package/dist/providers/voyage-provider.d.ts.map +1 -0
- package/dist/providers/voyage-provider.js +124 -0
- package/dist/providers/voyage-provider.js.map +1 -0
- package/dist/providers/xai-provider.d.ts +24 -0
- package/dist/providers/xai-provider.d.ts.map +1 -0
- package/dist/providers/xai-provider.js +57 -0
- package/dist/providers/xai-provider.js.map +1 -0
- package/dist/python-bridge-types.d.ts +70 -0
- package/dist/python-bridge-types.d.ts.map +1 -0
- package/dist/python-bridge-types.js +2 -0
- package/dist/python-bridge-types.js.map +1 -0
- package/dist/python-node-executor.d.ts +20 -0
- package/dist/python-node-executor.d.ts.map +1 -0
- package/dist/python-node-executor.js +219 -0
- package/dist/python-node-executor.js.map +1 -0
- package/dist/python-stdio-bridge.d.ts +67 -0
- package/dist/python-stdio-bridge.d.ts.map +1 -0
- package/dist/python-stdio-bridge.js +704 -0
- package/dist/python-stdio-bridge.js.map +1 -0
- package/dist/recommended-models.d.ts +17 -0
- package/dist/recommended-models.d.ts.map +1 -0
- package/dist/recommended-models.js +139 -0
- package/dist/recommended-models.js.map +1 -0
- package/dist/telemetry.d.ts +61 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +128 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/trace-exporters.d.ts +72 -0
- package/dist/trace-exporters.d.ts.map +1 -0
- package/dist/trace-exporters.js +216 -0
- package/dist/trace-exporters.js.map +1 -0
- package/dist/tracing-helpers.d.ts +99 -0
- package/dist/tracing-helpers.d.ts.map +1 -0
- package/dist/tracing-helpers.js +217 -0
- package/dist/tracing-helpers.js.map +1 -0
- package/package.json +64 -0
package/dist/context.js
ADDED
|
@@ -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
|