@nodetool-ai/dsl 0.8.2 → 0.8.4-a24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +6 -0
  2. package/dist/.nodetool-build-stamp +1 -1
  3. package/dist/core.d.ts.map +1 -1
  4. package/dist/core.js +7 -0
  5. package/dist/core.js.map +1 -1
  6. package/dist/export.d.ts.map +1 -1
  7. package/dist/export.js +7 -5
  8. package/dist/export.js.map +1 -1
  9. package/dist/flow/core.d.ts +3 -0
  10. package/dist/flow/core.d.ts.map +1 -1
  11. package/dist/flow/core.js +12 -2
  12. package/dist/flow/core.js.map +1 -1
  13. package/dist/flow/generated/nodetool.image.d.ts +1 -1
  14. package/dist/flow/generated/nodetool.image.d.ts.map +1 -1
  15. package/dist/flow/generated/nodetool.manimgx.d.ts +52 -0
  16. package/dist/flow/generated/nodetool.manimgx.d.ts.map +1 -0
  17. package/dist/flow/generated/nodetool.manimgx.js +14 -0
  18. package/dist/flow/generated/nodetool.manimgx.js.map +1 -0
  19. package/dist/flow/generated/nodetool.storyboard.d.ts +17 -0
  20. package/dist/flow/generated/nodetool.storyboard.d.ts.map +1 -1
  21. package/dist/flow/generated/nodetool.storyboard.js +3 -0
  22. package/dist/flow/generated/nodetool.storyboard.js.map +1 -1
  23. package/dist/flow/generated/nodetool.video.d.ts +1 -0
  24. package/dist/flow/generated/nodetool.video.d.ts.map +1 -1
  25. package/dist/flow/generated/nodetool.video.js.map +1 -1
  26. package/dist/flow/generated/nodetool.workflows.app_node.d.ts +11 -0
  27. package/dist/flow/generated/nodetool.workflows.app_node.d.ts.map +1 -0
  28. package/dist/flow/generated/nodetool.workflows.app_node.js +11 -0
  29. package/dist/flow/generated/nodetool.workflows.app_node.js.map +1 -0
  30. package/dist/flow/index.d.ts +1 -1
  31. package/dist/flow/index.d.ts.map +1 -1
  32. package/dist/flow/index.js +1 -1
  33. package/dist/flow/index.js.map +1 -1
  34. package/dist/generated/index.d.ts +2 -0
  35. package/dist/generated/index.d.ts.map +1 -1
  36. package/dist/generated/index.js +2 -0
  37. package/dist/generated/index.js.map +1 -1
  38. package/dist/generated/nodetool.image.d.ts +1 -1
  39. package/dist/generated/nodetool.image.d.ts.map +1 -1
  40. package/dist/generated/nodetool.manimgx.d.ts +56 -0
  41. package/dist/generated/nodetool.manimgx.d.ts.map +1 -0
  42. package/dist/generated/nodetool.manimgx.js +12 -0
  43. package/dist/generated/nodetool.manimgx.js.map +1 -0
  44. package/dist/generated/nodetool.storyboard.d.ts +17 -0
  45. package/dist/generated/nodetool.storyboard.d.ts.map +1 -1
  46. package/dist/generated/nodetool.storyboard.js +3 -0
  47. package/dist/generated/nodetool.storyboard.js.map +1 -1
  48. package/dist/generated/nodetool.video.d.ts +1 -0
  49. package/dist/generated/nodetool.video.d.ts.map +1 -1
  50. package/dist/generated/nodetool.video.js.map +1 -1
  51. package/dist/generated/nodetool.workflows.app_node.d.ts +10 -0
  52. package/dist/generated/nodetool.workflows.app_node.d.ts.map +1 -0
  53. package/dist/generated/nodetool.workflows.app_node.js +6 -0
  54. package/dist/generated/nodetool.workflows.app_node.js.map +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/registry.d.ts +5 -15
  60. package/dist/registry.d.ts.map +1 -1
  61. package/dist/registry.js +32 -58
  62. package/dist/registry.js.map +1 -1
  63. package/dist/script-export.d.ts +57 -0
  64. package/dist/script-export.d.ts.map +1 -0
  65. package/dist/script-export.js +632 -0
  66. package/dist/script-export.js.map +1 -0
  67. package/package.json +11 -15
@@ -0,0 +1,632 @@
1
+ /**
2
+ * Workflow → JavaScript script.
3
+ *
4
+ * Turns a saved workflow graph into a script that calls each node as an async
5
+ * function, in dependency order: `await` is the edge and a variable is the
6
+ * wire. Two targets share one body and differ only in how a node is called:
7
+ *
8
+ * sandbox — a body for a JS script document or a Code node. Each call
9
+ * goes through the `@nodetool-ai/sandbox-flow` pack's untyped
10
+ * bridge, which crosses to the host and runs on the invoking
11
+ * run's context.
12
+ * node — a standalone `.mjs` for plain Node.js. Each call runs in process
13
+ * through `@nodetool-ai/dsl/flow`, with secrets read from the
14
+ * environment.
15
+ *
16
+ * Both reach the same native-flow backend (docs/dsl-native-flow-design.md), so
17
+ * a node behaves the same in either. What a call cannot express is refused in
18
+ * the generated script itself: a script with an unsupported node throws before
19
+ * it runs anything, naming every node and the reason.
20
+ */
21
+ import { readCatalogNodes } from "@nodetool-ai/node-catalog";
22
+ import { rewriteBypassedNodes } from "@nodetool-ai/kernel";
23
+ const COMMENT_NODE_TYPE = "nodetool.workflows.base_node.Comment";
24
+ const GROUP_NODE_TYPE = "nodetool.workflows.base_node.Group";
25
+ const PREVIEW_NODE_TYPE = "nodetool.workflows.base_node.Preview";
26
+ const JS_RESERVED = new Set([
27
+ "arguments", "async", "await", "break", "case", "catch", "class", "const",
28
+ "continue", "debugger", "default", "delete", "do", "else", "enum", "eval",
29
+ "export", "extends", "false", "finally", "for", "function", "if",
30
+ "implements", "import", "in", "instanceof", "interface", "let", "new",
31
+ "null", "package", "private", "protected", "public", "return", "static",
32
+ "super", "switch", "this", "throw", "true", "try", "typeof", "undefined",
33
+ "var", "void", "while", "with", "yield"
34
+ ]);
35
+ /** Names the generated helpers and the sandbox globals already use. */
36
+ const RESERVED_NAMES = [
37
+ "SKIPPED", "pick", "ready", "aggregate", "call", "callStreaming", "runNode",
38
+ "streamNode", "isEmission", "inputs", "output", "emit", "results", "main",
39
+ "flow", "stream", "state", "progress", "console", "media", "workspace",
40
+ "fetch", "sleep", "crypto", "format", "image", "audio", "video", "canvas",
41
+ "nodetool", "getSecret", "process", "invoke", "invokeStream", "startFlow",
42
+ "invoke_node", "open_node_stream", "take_node_stream", "close_node_stream",
43
+ "callNode", "bytesAsLength", "Symbol", "Object", "Array", "JSON", "Error", "Math",
44
+ "Number", "String", "Promise"
45
+ ];
46
+ function asRecord(value) {
47
+ return value && typeof value === "object" && !Array.isArray(value)
48
+ ? value
49
+ : null;
50
+ }
51
+ function normalizeGraph(graph, getMetadata) {
52
+ if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
53
+ throw new Error("Workflow graph must contain node and edge arrays");
54
+ }
55
+ const descriptors = [];
56
+ const extras = new Map();
57
+ graph.nodes.forEach((raw, index) => {
58
+ const id = typeof raw.id === "string" ? raw.id : "";
59
+ const type = typeof raw.type === "string" ? raw.type : "";
60
+ if (!id)
61
+ throw new Error(`Workflow node at index ${index} is missing an id`);
62
+ if (!type)
63
+ throw new Error(`Workflow node '${id}' is missing a type`);
64
+ if (type === COMMENT_NODE_TYPE || type === GROUP_NODE_TYPE)
65
+ return;
66
+ const properties = {
67
+ ...(asRecord(raw.properties) ?? asRecord(raw.data) ?? {}),
68
+ ...(asRecord(raw.dynamic_properties) ?? {})
69
+ };
70
+ const metadata = getMetadata(type);
71
+ descriptors.push({
72
+ id,
73
+ type,
74
+ properties,
75
+ outputs: { ...Object.fromEntries((metadata?.outputs ?? []).map((slot) => [slot.name, typeToString(slot.type)])), ...(asRecord(raw.outputs) ?? {}) },
76
+ propertyTypes: { ...Object.fromEntries((metadata?.properties ?? []).map((slot) => [slot.name, typeToString(slot.type)])), ...(asRecord(raw.propertyTypes) ?? {}) },
77
+ dynamic_outputs: asRecord(raw.dynamic_outputs) ?? undefined,
78
+ dynamic_properties: asRecord(raw.dynamic_properties) ?? undefined,
79
+ ui_properties: asRecord(raw.ui_properties) ?? undefined
80
+ });
81
+ extras.set(id, {
82
+ name: typeof raw.name === "string" ? raw.name : undefined,
83
+ outputCorrelation: asRecord(raw.output_correlation) ??
84
+ asRecord(raw.dynamic_output_correlation) ??
85
+ undefined,
86
+ isStreamingOutput: typeof raw.is_streaming_output === "boolean"
87
+ ? raw.is_streaming_output
88
+ : undefined
89
+ });
90
+ });
91
+ const nodeIds = new Set(descriptors.map((node) => node.id));
92
+ const rawEdges = [];
93
+ graph.edges.forEach((raw, index) => {
94
+ const source = typeof raw.source === "string" ? raw.source : "";
95
+ const target = typeof raw.target === "string" ? raw.target : "";
96
+ const sourceHandle = typeof raw.sourceHandle === "string" ? raw.sourceHandle : "";
97
+ const targetHandle = typeof raw.targetHandle === "string" ? raw.targetHandle : "";
98
+ if (!source || !target || !sourceHandle || !targetHandle) {
99
+ throw new Error(`Workflow edge at index ${index} is missing required fields`);
100
+ }
101
+ // The runner drops edges whose ends do not exist; so does the script.
102
+ if (!nodeIds.has(source) || !nodeIds.has(target))
103
+ return;
104
+ rawEdges.push({
105
+ id: typeof raw.id === "string" ? raw.id : undefined,
106
+ source,
107
+ sourceHandle,
108
+ target,
109
+ targetHandle,
110
+ edge_type: raw.edge_type === "control" ? "control" : "data"
111
+ });
112
+ });
113
+ // Bypassed nodes are routed around exactly as the runner does it.
114
+ const rewritten = rewriteBypassedNodes({ nodes: descriptors, edges: rawEdges });
115
+ const nodes = rewritten.nodes.map((node) => ({
116
+ id: node.id,
117
+ type: node.type,
118
+ properties: (node.properties ?? {}),
119
+ ...extras.get(node.id)
120
+ }));
121
+ return {
122
+ nodes,
123
+ edges: rewritten.edges.filter((edge) => edge.edge_type !== "control"),
124
+ controlEdges: rewritten.edges.filter((edge) => edge.edge_type === "control")
125
+ };
126
+ }
127
+ function topologicalSort(nodes, edges) {
128
+ const index = new Map(nodes.map((node, i) => [node.id, i]));
129
+ const inDegree = new Map(nodes.map((node) => [node.id, 0]));
130
+ const outgoing = new Map(nodes.map((node) => [node.id, []]));
131
+ for (const edge of edges) {
132
+ outgoing.get(edge.source).push(edge.target);
133
+ inDegree.set(edge.target, inDegree.get(edge.target) + 1);
134
+ }
135
+ const byIndex = (a, b) => index.get(a.id) - index.get(b.id);
136
+ const queue = nodes.filter((node) => inDegree.get(node.id) === 0).sort(byIndex);
137
+ const ordered = [];
138
+ const byId = new Map(nodes.map((node) => [node.id, node]));
139
+ while (queue.length > 0) {
140
+ const current = queue.shift();
141
+ ordered.push(current);
142
+ for (const target of outgoing.get(current.id)) {
143
+ const left = inDegree.get(target) - 1;
144
+ inDegree.set(target, left);
145
+ if (left === 0) {
146
+ queue.push(byId.get(target));
147
+ queue.sort(byIndex);
148
+ }
149
+ }
150
+ }
151
+ if (ordered.length !== nodes.length) {
152
+ throw new Error("Workflow contains a cycle (such as a Loop), which a script cannot express");
153
+ }
154
+ return ordered;
155
+ }
156
+ // ── Metadata ────────────────────────────────────────────────────────────────
157
+ let builtinMetadata = null;
158
+ function builtinMetadataFor(nodeType) {
159
+ if (!builtinMetadata) {
160
+ builtinMetadata = new Map();
161
+ for (const { metadata } of readCatalogNodes()) {
162
+ builtinMetadata.set(metadata.node_type, metadata);
163
+ }
164
+ }
165
+ return builtinMetadata.get(nodeType);
166
+ }
167
+ function typeToString(type) {
168
+ if (!type?.type)
169
+ return "any";
170
+ const args = (type.type_args ?? []).map(typeToString);
171
+ return args.length > 0 ? `${type.type}[${args.join(", ")}]` : type.type;
172
+ }
173
+ function correlationKind(value) {
174
+ const record = asRecord(value);
175
+ return typeof record?.kind === "string" ? record.kind : undefined;
176
+ }
177
+ function correlationOf(node, metadata, slot) {
178
+ return (asRecord(node.outputCorrelation?.[slot]) ??
179
+ asRecord(metadata?.output_correlation?.[slot]));
180
+ }
181
+ function consumesStreams(metadata) {
182
+ return metadata?.is_streaming_input === true || metadata?.input_mode === "stream";
183
+ }
184
+ // ── Code formatting ─────────────────────────────────────────────────────────
185
+ function isIdentifier(name) {
186
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
187
+ }
188
+ function formatKey(name) {
189
+ return isIdentifier(name) && !JS_RESERVED.has(name) ? name : JSON.stringify(name);
190
+ }
191
+ function formatLiteral(value, indent) {
192
+ if (value === undefined)
193
+ return "undefined";
194
+ const json = JSON.stringify(value, null, 2);
195
+ if (json === undefined)
196
+ return "undefined";
197
+ return json.split("\n").join(`\n${indent}`);
198
+ }
199
+ function toCamelWords(raw) {
200
+ return raw
201
+ .split(/[^A-Za-z0-9_$]+/)
202
+ .filter(Boolean)
203
+ .map((part, index) => {
204
+ const lower = /^[A-Z0-9_]+$/.test(part) ? part.toLowerCase() : part;
205
+ return index === 0
206
+ ? lower[0].toLowerCase() + lower.slice(1)
207
+ : lower[0].toUpperCase() + lower.slice(1);
208
+ })
209
+ .join("");
210
+ }
211
+ function makeIdentifier(raw, fallback, used) {
212
+ let base = toCamelWords(raw) || fallback;
213
+ if (!/^[A-Za-z_$]/.test(base))
214
+ base = `n${base}`;
215
+ if (JS_RESERVED.has(base))
216
+ base = `${base}Node`;
217
+ let name = base;
218
+ for (let suffix = 2; used.has(name); suffix++)
219
+ name = `${base}${suffix}`;
220
+ used.add(name);
221
+ return name;
222
+ }
223
+ function className(nodeType) {
224
+ return nodeType.split(".").pop() ?? nodeType;
225
+ }
226
+ function isInputNode(node) {
227
+ return node.type.startsWith("nodetool.input.");
228
+ }
229
+ function isSinkNode(node) {
230
+ return node.type.startsWith("nodetool.output.") || node.type === PREVIEW_NODE_TYPE;
231
+ }
232
+ function nameProperty(node) {
233
+ const name = node.properties.name;
234
+ return typeof name === "string" ? name.trim() : "";
235
+ }
236
+ // ── Generated runtime ───────────────────────────────────────────────────────
237
+ const SHARED_HELPERS = `const SKIPPED = Symbol("skipped");
238
+
239
+ // The value on one output slot, or SKIPPED when the node did not run or left
240
+ // the slot empty, such as the branch an If node did not take.
241
+ function pick(result, slot) {
242
+ return result === SKIPPED || !(slot in result) ? SKIPPED : result[slot];
243
+ }
244
+
245
+ function aggregate(values, flatten) {
246
+ const received = values.filter((value) => value !== SKIPPED);
247
+ return received.length === 0 ? SKIPPED : flatten ? received.flat(1) : received;
248
+ }
249
+
250
+ // An untaken branch skips the node. Partial input keeps saved defaults.
251
+ function ready(props, defaults = {}) {
252
+ const out = {};
253
+ let received = false;
254
+ let absent = false;
255
+ for (const [key, value] of Object.entries(props)) {
256
+ if (value === SKIPPED) { absent = true; continue; }
257
+ received = true;
258
+ out[key] = value;
259
+ }
260
+ return absent && !received ? null : { ...defaults, ...out };
261
+ }
262
+
263
+ // Run a node and return its outputs, the last value per slot.
264
+ async function call(type, props, defaults) {
265
+ const args = ready(props, defaults);
266
+ return args === null ? SKIPPED : await runNode(type, args);
267
+ }
268
+
269
+ function isEmission(item) {
270
+ return typeof item.slot === "string" && "value" in item && Object.keys(item).length === 2;
271
+ }
272
+
273
+ // Run a node item by item: onItem sees each item, the result is the last value
274
+ // per slot.
275
+ async function callStreaming(type, props, onItem, defaults) {
276
+ const args = ready(props, defaults);
277
+ if (args === null) return SKIPPED;
278
+ const last = {};
279
+ for await (const item of streamNode(type, args)) {
280
+ const record = isEmission(item) ? { [item.slot]: item.value } : item;
281
+ Object.assign(last, record);
282
+ await onItem(record);
283
+ }
284
+ return last;
285
+ }`;
286
+ const NODE_RUNTIME = `const inputs = JSON.parse(process.argv[2] ?? "{}");
287
+ const results = {};
288
+
289
+ async function output(name, value) {
290
+ results[name] = value;
291
+ }
292
+
293
+ async function emit(name, value) {
294
+ (results[name] ??= []).push(value);
295
+ }
296
+
297
+ function runNode(type, args) {
298
+ return invoke(type, args);
299
+ }
300
+
301
+ function streamNode(type, args) {
302
+ return invokeStream(type, args);
303
+ }
304
+
305
+ // Print raw media bytes as their length instead of one key per byte.
306
+ function bytesAsLength(key, value) {
307
+ return value instanceof Uint8Array ? \`<\${value.length} bytes>\` : value;
308
+ }`;
309
+ export function workflowToScript(graph, options) {
310
+ const getMetadata = options.getMetadata ?? builtinMetadataFor;
311
+ const { nodes, edges, controlEdges } = normalizeGraph(graph, getMetadata);
312
+ const ordered = topologicalSort(nodes, edges);
313
+ const byId = new Map(nodes.map((node) => [node.id, node]));
314
+ const metadataOf = (node) => getMetadata(node.type);
315
+ const incoming = new Map();
316
+ const outgoing = new Map();
317
+ for (const edge of edges) {
318
+ (incoming.get(edge.target) ?? incoming.set(edge.target, []).get(edge.target)).push(edge);
319
+ (outgoing.get(edge.source) ?? outgoing.set(edge.source, []).get(edge.source)).push(edge);
320
+ }
321
+ const unsupported = [];
322
+ const refuse = (node, message) => {
323
+ if (!unsupported.some((issue) => issue.node_id === node.id && issue.message === message)) {
324
+ unsupported.push({ node_id: node.id, node_type: node.type, message });
325
+ }
326
+ };
327
+ for (const node of nodes) {
328
+ if (node.type === "nodetool.control.Zip")
329
+ refuse(node, "requires iteration lineage that native script calls cannot represent");
330
+ }
331
+ for (const edge of controlEdges) {
332
+ refuse(byId.get(edge.target), "is driven by a control edge from an agent, which a script cannot express");
333
+ }
334
+ const used = new Set([...JS_RESERVED, ...RESERVED_NAMES]);
335
+ const variables = new Map();
336
+ for (const node of ordered) {
337
+ const label = node.name ?? (nameProperty(node) || className(node.type));
338
+ variables.set(node.id, makeIdentifier(label, "node", used));
339
+ }
340
+ // Ports. Input names become `inputs.<name>`, so they must be identifiers.
341
+ const inputPorts = [];
342
+ const inputPortOf = new Map();
343
+ const usedInputs = new Set();
344
+ const outputPorts = [];
345
+ const outputPortOf = new Map();
346
+ const usedOutputs = new Set();
347
+ const sourceSlotType = (edge) => {
348
+ const metadata = metadataOf(byId.get(edge.source));
349
+ return typeToString(metadata?.outputs.find((o) => o.name === edge.sourceHandle)?.type);
350
+ };
351
+ const isExternalInput = (node) => isInputNode(node) && (incoming.get(node.id)?.length ?? 0) === 0;
352
+ for (const node of ordered) {
353
+ if (isExternalInput(node)) {
354
+ const metadata = metadataOf(node);
355
+ if (node.isStreamingOutput ?? metadata?.is_streaming_output) {
356
+ refuse(node, "is a live input stream, which a script cannot receive");
357
+ }
358
+ const raw = nameProperty(node) || node.name || node.id;
359
+ const port = makeIdentifier(raw, "input", usedInputs);
360
+ inputPortOf.set(node.id, port);
361
+ inputPorts.push({
362
+ name: port,
363
+ type: typeToString(metadata?.outputs.find((o) => o.name === "output")?.type)
364
+ });
365
+ }
366
+ else if (isSinkNode(node)) {
367
+ const raw = nameProperty(node) ||
368
+ node.name ||
369
+ (node.type === PREVIEW_NODE_TYPE ? "preview" : "output");
370
+ const port = makeIdentifier(raw, "output", usedOutputs);
371
+ outputPortOf.set(node.id, port);
372
+ const valueEdge = (incoming.get(node.id) ?? []).find((e) => e.targetHandle === "value");
373
+ outputPorts.push({ name: port, type: valueEdge ? sourceSlotType(valueEdge) : "any" });
374
+ }
375
+ }
376
+ // Per-item edges. A stream may feed a workflow output (emitted item by
377
+ // item) or a node that consumes streams (handed the collected items).
378
+ // Anything else would run once per item in the graph and is refused.
379
+ const streamConsumers = new Map();
380
+ const collectedInput = new Map();
381
+ const perItemSinkEdges = new Set();
382
+ // A slot delivers one value per item rather than one per run when it
383
+ // iterates or streams chunks, or forwards an input that arrived per item.
384
+ const perItemMemo = new Map();
385
+ const isPerItemSlot = (node, slot) => {
386
+ const key = `${node.id}\u0000${slot}`;
387
+ const known = perItemMemo.get(key);
388
+ if (known !== undefined)
389
+ return known;
390
+ const metadata = metadataOf(node);
391
+ const correlation = correlationOf(node, metadata, slot);
392
+ const kind = correlationKind(correlation);
393
+ let perItem;
394
+ if (kind === "iteration" || kind === "chunk") {
395
+ perItem = true;
396
+ }
397
+ else if (kind === "forward") {
398
+ perItem = (incoming.get(node.id) ?? []).some((edge) => edge.targetHandle === correlation?.source &&
399
+ isPerItemSlot(byId.get(edge.source), edge.sourceHandle));
400
+ }
401
+ else if (kind) {
402
+ perItem = false;
403
+ }
404
+ else {
405
+ perItem =
406
+ metadata?.outputs.find((output) => output.name === slot)?.stream ===
407
+ true ||
408
+ (node.isStreamingOutput ?? metadata?.is_streaming_output === true);
409
+ }
410
+ perItemMemo.set(key, perItem);
411
+ return perItem;
412
+ };
413
+ for (const node of ordered) {
414
+ for (const edge of outgoing.get(node.id) ?? []) {
415
+ if (!isPerItemSlot(node, edge.sourceHandle))
416
+ continue;
417
+ const target = byId.get(edge.target);
418
+ const consumers = streamConsumers.get(node.id) ?? [];
419
+ if (isSinkNode(target) && edge.targetHandle === "value") {
420
+ consumers.push({ slot: edge.sourceHandle, emitPort: outputPortOf.get(target.id) });
421
+ perItemSinkEdges.add(edge);
422
+ }
423
+ else if (consumesStreams(metadataOf(target))) {
424
+ const collectVar = makeIdentifier(`${variables.get(node.id)} ${edge.sourceHandle} items`, "items", used);
425
+ consumers.push({ slot: edge.sourceHandle, collectVar });
426
+ collectedInput.set(edge, collectVar);
427
+ }
428
+ else {
429
+ refuse(target, `receives "${edge.sourceHandle}" from ${className(node.type)} one item at a time; ` +
430
+ "in the graph it runs once per item, which one script call cannot reproduce");
431
+ }
432
+ streamConsumers.set(node.id, consumers);
433
+ }
434
+ }
435
+ for (const node of [...ordered].reverse()) {
436
+ if (!isSinkNode(node))
437
+ continue;
438
+ const valueEdges = (incoming.get(node.id) ?? []).filter((edge) => edge.targetHandle === "value");
439
+ const consumers = streamConsumers.get(node.id) ?? [];
440
+ if (consumers.length === 0 || !valueEdges.every((edge) => perItemSinkEdges.has(edge)))
441
+ continue;
442
+ for (const edge of valueEdges) {
443
+ const upstream = streamConsumers.get(edge.source) ?? [];
444
+ upstream.push(...consumers.map((consumer) => ({ ...consumer, slot: edge.sourceHandle })));
445
+ streamConsumers.set(edge.source, upstream);
446
+ }
447
+ streamConsumers.delete(node.id);
448
+ }
449
+ // Several edges into one list input arrive as one list. Into any other
450
+ // input each value is its own run of the node, which one call is not.
451
+ for (const node of ordered) {
452
+ const counts = new Map();
453
+ for (const edge of incoming.get(node.id) ?? []) {
454
+ counts.set(edge.targetHandle, (counts.get(edge.targetHandle) ?? 0) + 1);
455
+ }
456
+ for (const [handle, count] of counts) {
457
+ if (count < 2)
458
+ continue;
459
+ const type = metadataOf(node)?.properties.find((p) => p.name === handle)?.type;
460
+ if (type && type.type !== "list" && type.type !== "any") {
461
+ refuse(node, `receives ${count} values on "${handle}", and in the graph it runs once for each`);
462
+ }
463
+ }
464
+ }
465
+ const expressionFor = (edge) => {
466
+ const collected = collectedInput.get(edge);
467
+ if (collected)
468
+ return collected;
469
+ const source = byId.get(edge.source);
470
+ const variable = variables.get(source.id);
471
+ return `pick(${variable}, ${JSON.stringify(edge.sourceHandle)})`;
472
+ };
473
+ const propsLiteral = (node, indent) => {
474
+ const props = [];
475
+ const edgesIn = (incoming.get(node.id) ?? []).filter((e) => !perItemSinkEdges.has(e));
476
+ const byHandle = new Map();
477
+ for (const edge of edgesIn) {
478
+ (byHandle.get(edge.targetHandle) ?? byHandle.set(edge.targetHandle, []).get(edge.targetHandle)).push(edge);
479
+ }
480
+ const inner = `${indent} `;
481
+ for (const [handle, handleEdges] of byHandle) {
482
+ const expressions = handleEdges.map(expressionFor);
483
+ const type = metadataOf(node)?.properties.find((property) => property.name === handle)?.type;
484
+ const combined = `aggregate([${expressions.join(", ")}], ${type?.type === "list" && type.type_args?.[0]?.type !== "list"})`;
485
+ props.push([
486
+ handle,
487
+ expressions.length === 1 ? expressions[0] : combined
488
+ ]);
489
+ }
490
+ if (props.length === 0)
491
+ return "{}";
492
+ return `{\n${props.map(([k, v]) => `${inner}${formatKey(k)}: ${v}`).join(",\n")}\n${indent}}`;
493
+ };
494
+ const body = [];
495
+ const buffers = new Set([...streamConsumers.values()].flatMap((consumers) => consumers.flatMap((consumer) => consumer.collectVar ? [consumer.collectVar] : [])));
496
+ for (const buffer of buffers)
497
+ body.push(`const ${buffer} = [];`);
498
+ if (buffers.size > 0)
499
+ body.push("");
500
+ for (const node of ordered) {
501
+ const variable = variables.get(node.id);
502
+ const title = node.name ?? nameProperty(node);
503
+ body.push(commentLine(`${title ? `${title} — ` : ""}${node.type} (${node.id})`));
504
+ if (unsupported.some((issue) => issue.node_id === node.id)) {
505
+ body.push(`const ${variable} = SKIPPED; // not supported, see the check above`);
506
+ body.push("");
507
+ continue;
508
+ }
509
+ if (isExternalInput(node)) {
510
+ const port = inputPortOf.get(node.id);
511
+ body.push(`const ${variable} = await call(${JSON.stringify(node.type)}, { ...${formatLiteral(node.properties, "")}, value: inputs.${port} ?? ${formatLiteral(node.properties.value, "")} });`);
512
+ body.push("");
513
+ continue;
514
+ }
515
+ if (isSinkNode(node)) {
516
+ const port = outputPortOf.get(node.id);
517
+ const valueEdges = (incoming.get(node.id) ?? []).filter((e) => e.targetHandle === "value");
518
+ if (valueEdges.length > 0 && valueEdges.every((e) => perItemSinkEdges.has(e))) {
519
+ body.push(`// "${port}" is emitted item by item where its source runs.`);
520
+ body.push("");
521
+ continue;
522
+ }
523
+ body.push(`const ${variable} = await call(${JSON.stringify(node.type)}, ${propsLiteral(node, "")}, ${formatLiteral(node.properties, "")});`);
524
+ body.push(`if (${variable} !== SKIPPED) await output(${JSON.stringify(port)}, ${variable}.output);`);
525
+ body.push("");
526
+ continue;
527
+ }
528
+ const consumers = streamConsumers.get(node.id) ?? [];
529
+ if (consumers.length === 0) {
530
+ body.push(`const ${variable} = await call(${JSON.stringify(node.type)}, ${propsLiteral(node, "")}, ${formatLiteral(node.properties, "")});`);
531
+ body.push("");
532
+ continue;
533
+ }
534
+ body.push(`const ${variable} = await callStreaming(${JSON.stringify(node.type)}, ${propsLiteral(node, "")}, async (item) => {`);
535
+ const bySlot = new Map();
536
+ for (const consumer of consumers) {
537
+ (bySlot.get(consumer.slot) ?? bySlot.set(consumer.slot, []).get(consumer.slot)).push(consumer);
538
+ }
539
+ for (const [slot, slotConsumers] of bySlot) {
540
+ const key = JSON.stringify(slot);
541
+ body.push(` if (${key} in item) {`);
542
+ for (const consumer of slotConsumers) {
543
+ if (consumer.emitPort) {
544
+ body.push(` await emit(${JSON.stringify(consumer.emitPort)}, item[${key}]);`);
545
+ }
546
+ else if (consumer.collectVar) {
547
+ body.push(` ${consumer.collectVar}.push(item[${key}]);`);
548
+ }
549
+ }
550
+ body.push(" }");
551
+ }
552
+ body.push(`}, ${formatLiteral(node.properties, "")});`);
553
+ body.push("");
554
+ }
555
+ const guard = [];
556
+ if (unsupported.length > 0) {
557
+ const lines = unsupported.map((issue) => `- ${issue.node_type} (${issue.node_id}) ${issue.message}`);
558
+ guard.push("// This workflow has nodes a script cannot run. It stops here, before any", "// node runs, so nothing is billed for a run that cannot finish.", `throwUnsupported(${JSON.stringify(lines, null, 2)});`, "", "function throwUnsupported(problems) {", ' throw new Error("This workflow cannot run as a script:\\n" + problems.join("\\n"));', "}", "");
559
+ }
560
+ const name = options.workflowName && options.workflowName.trim()
561
+ ? options.workflowName.trim()
562
+ : "Untitled workflow";
563
+ const code = options.target === "sandbox"
564
+ ? sandboxScript(name, inputPorts, outputPorts, guard, body)
565
+ : nodeScript(name, inputPorts, outputPorts, guard, body);
566
+ return { code, inputs: inputPorts, outputs: outputPorts, unsupported };
567
+ }
568
+ function portList(ports) {
569
+ return ports.length > 0
570
+ ? ports.map((port) => `${port.name} (${port.type})`).join(", ")
571
+ : "none";
572
+ }
573
+ function commentLine(text) {
574
+ return `// ${text.replace(/\s+/g, " ")}`;
575
+ }
576
+ function sandboxScript(name, inputs, outputs, guard, body) {
577
+ return [
578
+ commentLine(`"${name}", exported from NodeTool as a sandbox script.`),
579
+ "// Run it as a JS script document or paste it into a Code node. Each node",
580
+ "// runs on the host and is billed to the run that calls it.",
581
+ commentLine(`Inputs: ${portList(inputs)}`),
582
+ commentLine(`Outputs: ${portList(outputs)}`),
583
+ 'import "@nodetool-ai/sandbox-nodetool/flow";',
584
+ 'import { callNode as runNode, streamNode } from "@nodetool-ai/sandbox-flow";',
585
+ "",
586
+ ...guard,
587
+ SHARED_HELPERS,
588
+ "",
589
+ ...body
590
+ ].join("\n").trimEnd() + "\n";
591
+ }
592
+ function nodeScript(name, inputs, outputs, guard, body) {
593
+ const example = Object.fromEntries(inputs.map((port) => [port.name, "…"]));
594
+ const indented = body
595
+ .flatMap((entry) => entry.split("\n"))
596
+ .map((line) => (line ? ` ${line}` : line));
597
+ while (indented.length > 0 && indented[indented.length - 1] === "")
598
+ indented.pop();
599
+ return [
600
+ "#!/usr/bin/env node",
601
+ commentLine(`"${name}", exported from NodeTool as a Node.js script.`),
602
+ "//",
603
+ "// Needs Node.js 22 or newer and the NodeTool node runtime:",
604
+ "// npm install @nodetool-ai/dsl",
605
+ "// Run it with the workflow inputs as a JSON object:",
606
+ `// node script.mjs '${JSON.stringify(example)}'`,
607
+ "// Provider keys are read from environment variables such as OPENAI_API_KEY.",
608
+ "// The outputs are printed as JSON when the run finishes.",
609
+ commentLine(`Inputs: ${portList(inputs)}`),
610
+ commentLine(`Outputs: ${portList(outputs)}`),
611
+ 'import { startFlow, invoke, invokeStream } from "@nodetool-ai/dsl/flow";',
612
+ "",
613
+ ...guard,
614
+ NODE_RUNTIME,
615
+ "",
616
+ SHARED_HELPERS,
617
+ "",
618
+ "async function main() {",
619
+ ...indented,
620
+ "}",
621
+ "",
622
+ "const flow = await startFlow();",
623
+ "try {",
624
+ " await flow.run(main);",
625
+ "} finally {",
626
+ " await flow.close();",
627
+ "}",
628
+ "console.log(JSON.stringify(results, bytesAsLength, 2));",
629
+ ""
630
+ ].join("\n");
631
+ }
632
+ //# sourceMappingURL=script-export.js.map