@foresthubai/workflow-core 0.3.0 → 0.4.0
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/LICENSE +202 -202
- package/NOTICE +14 -14
- package/README.md +63 -63
- package/dist/api/workflow.d.ts +2 -2
- package/dist/api/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/index.ts +11 -11
- package/src/api/workflow.ts +607 -607
- package/src/channel/Channel.ts +11 -11
- package/src/channel/ChannelDefinition.ts +76 -76
- package/src/channel/index.ts +6 -6
- package/src/channel/serialization.ts +68 -68
- package/src/deploy/index.ts +1 -1
- package/src/deploy/requirements.test.ts +61 -61
- package/src/deploy/requirements.ts +41 -41
- package/src/diagnostics/__fixtures__/diagnosticFixtures.ts +158 -158
- package/src/diagnostics/diagnostics.test.ts +878 -878
- package/src/diagnostics/diagnostics.ts +936 -936
- package/src/diagnostics/index.ts +11 -11
- package/src/edge/Edge.ts +23 -23
- package/src/edge/EdgeDefinition.ts +45 -45
- package/src/edge/EdgeType.ts +19 -19
- package/src/edge/index.ts +8 -8
- package/src/edge/serialization.ts +83 -83
- package/src/expression/index.ts +4 -4
- package/src/expression/parser.ts +362 -362
- package/src/expression/types.ts +30 -30
- package/src/function/FunctionDeclaration.ts +54 -54
- package/src/function/index.ts +3 -3
- package/src/function/serialization.ts +40 -40
- package/src/globals.d.ts +9 -9
- package/src/id/index.ts +8 -8
- package/src/index.ts +22 -22
- package/src/memory/Memory.ts +15 -15
- package/src/memory/MemoryDefinition.ts +16 -16
- package/src/memory/MemoryFileDefinition.ts +37 -37
- package/src/memory/MemoryRegistry.ts +35 -35
- package/src/memory/VectorDatabaseDefinition.ts +21 -21
- package/src/memory/index.ts +8 -8
- package/src/memory/serialization.ts +47 -47
- package/src/migration/index.ts +4 -4
- package/src/migration/migrate.test.ts +44 -44
- package/src/migration/migrate.ts +58 -58
- package/src/migration/migrations.ts +24 -24
- package/src/migration/version.ts +9 -9
- package/src/model/LLMModelDefinition.ts +12 -12
- package/src/model/Model.ts +39 -39
- package/src/model/ModelDefinition.ts +15 -15
- package/src/model/ModelRegistry.ts +33 -33
- package/src/model/index.ts +7 -7
- package/src/model/serialization.ts +30 -30
- package/src/node/AgentNode.ts +82 -82
- package/src/node/DataNode.ts +41 -41
- package/src/node/FunctionNode.ts +76 -76
- package/src/node/InputNode.ts +185 -185
- package/src/node/LogicNode.ts +33 -33
- package/src/node/MqttNode.ts +127 -127
- package/src/node/Node.ts +61 -61
- package/src/node/NodeDefinition.ts +37 -37
- package/src/node/NodeRegistry.ts +85 -85
- package/src/node/OutputNode.ts +87 -87
- package/src/node/ToolNode.ts +32 -32
- package/src/node/TriggerNode.ts +272 -272
- package/src/node/constants.ts +16 -16
- package/src/node/index.ts +26 -26
- package/src/node/methods.ts +278 -278
- package/src/node/serialization.ts +544 -544
- package/src/parameter/OutputParameter.ts +68 -68
- package/src/parameter/Parameter.ts +243 -243
- package/src/parameter/index.ts +33 -33
- package/src/variable/Variable.ts +10 -10
- package/src/variable/index.ts +16 -16
- package/src/variable/operations.ts +106 -106
- package/src/workflow/Workflow.ts +41 -41
- package/src/workflow/index.ts +3 -3
- package/src/workflow/serialization.test.ts +240 -240
- package/src/workflow/serialization.ts +242 -242
|
@@ -1,544 +1,544 @@
|
|
|
1
|
-
import type { Schemas } from "../api";
|
|
2
|
-
import type { NodeData, Node } from "./Node";
|
|
3
|
-
import type { Expression } from "../api";
|
|
4
|
-
import type { FunctionInfo } from "../function";
|
|
5
|
-
import type { OutputBinding, OutputDeclaration } from "../parameter";
|
|
6
|
-
import { pruneArguments } from "../parameter";
|
|
7
|
-
import { NodeRegistry } from "./NodeRegistry";
|
|
8
|
-
|
|
9
|
-
export type ApiNode = Schemas["Node"];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Resolve a function's signature snapshot by id. A `FunctionCall` on the wire stores
|
|
13
|
-
* only `functionId`; deserialize rebuilds the in-memory `functionInfo` snapshot
|
|
14
|
-
* (which core's variable helpers + staleness read) from the workflow's function
|
|
15
|
-
* table via this. Unknown id (e.g. a call to a since-deleted function) → a minimal
|
|
16
|
-
* stub so the node still round-trips and surfaces as deleted.
|
|
17
|
-
*/
|
|
18
|
-
export type ResolveFunctionInfo = (functionId: string) => FunctionInfo | undefined;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Serialize a domain Node to the strict API format (Schemas["Node"]).
|
|
22
|
-
* Strips hidden parameters (those whose activationRules are not met). The
|
|
23
|
-
* `isToolInput` flag is threaded into activation evaluation so rules like
|
|
24
|
-
* `isControlFlow` / `isToolInput` resolve correctly per-instance.
|
|
25
|
-
*/
|
|
26
|
-
export function serialize(node: Node, isToolInput: boolean): ApiNode {
|
|
27
|
-
const result = serializeNodeData(node, node.position, isToolInput);
|
|
28
|
-
if (node.label) {
|
|
29
|
-
result.label = node.label;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// serializeNode emits gated/optional params uniformly; this pass prunes the
|
|
33
|
-
// ones that must not reach the api — inactive for this instance, or empty
|
|
34
|
-
// optionals (so the consumer's presence check sees absent, not `""`/`null`).
|
|
35
|
-
// FunctionCall gates its own params inline (its bindings have a different api
|
|
36
|
-
// shape), so it's excluded here.
|
|
37
|
-
if ("arguments" in result && result.arguments) {
|
|
38
|
-
const def = node.type !== "FunctionCall" ? NodeRegistry.getByType(node.type) : undefined;
|
|
39
|
-
if (def) {
|
|
40
|
-
pruneArguments(result.arguments, def.parameters, isToolInput);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function serializeNodeData(data: NodeData, position: { x: number; y: number }, isToolInput: boolean): Schemas["Node"] {
|
|
48
|
-
switch (data.type) {
|
|
49
|
-
case "ReadPin":
|
|
50
|
-
return {
|
|
51
|
-
id: data.id,
|
|
52
|
-
type: data.type,
|
|
53
|
-
position: position,
|
|
54
|
-
arguments: {
|
|
55
|
-
pinReference: data.arguments.pinReference!,
|
|
56
|
-
signalType: data.arguments.signalType,
|
|
57
|
-
output: data.arguments.output,
|
|
58
|
-
toolDescription: data.arguments.toolDescription,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
case "SerialRead":
|
|
62
|
-
return {
|
|
63
|
-
id: data.id,
|
|
64
|
-
type: data.type,
|
|
65
|
-
position: position,
|
|
66
|
-
arguments: {
|
|
67
|
-
portReference: data.arguments.portReference!,
|
|
68
|
-
...(data.arguments.prompt !== undefined ? { prompt: data.arguments.prompt } : {}),
|
|
69
|
-
output: data.arguments.output,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
case "WritePin":
|
|
73
|
-
return {
|
|
74
|
-
id: data.id,
|
|
75
|
-
type: data.type,
|
|
76
|
-
position: position,
|
|
77
|
-
arguments: {
|
|
78
|
-
pinReference: data.arguments.pinReference!,
|
|
79
|
-
signalType: data.arguments.signalType,
|
|
80
|
-
value: data.arguments.value,
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
case "SerialWrite":
|
|
84
|
-
return {
|
|
85
|
-
id: data.id,
|
|
86
|
-
type: data.type,
|
|
87
|
-
position: position,
|
|
88
|
-
arguments: {
|
|
89
|
-
portReference: data.arguments.portReference!,
|
|
90
|
-
value: data.arguments.value,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
case "Agent": {
|
|
94
|
-
// outputDeclarations is a list both in domain and API. Each entry's `name`
|
|
95
|
-
// is the JSON property the LLM is asked to produce; uniqueness is enforced
|
|
96
|
-
// by diagnostics, not the schema. memoryRefs is also a 1:1 list — domain
|
|
97
|
-
// and API share the same MemoryRef shape.
|
|
98
|
-
return {
|
|
99
|
-
id: data.id,
|
|
100
|
-
type: data.type,
|
|
101
|
-
position: position,
|
|
102
|
-
arguments: {
|
|
103
|
-
name: data.arguments.name,
|
|
104
|
-
model: data.arguments.model,
|
|
105
|
-
instructions: data.arguments.instructions,
|
|
106
|
-
maxTurns: data.arguments.maxTurns,
|
|
107
|
-
outputDeclarations: data.arguments.outputDeclarations,
|
|
108
|
-
memoryRefs: data.arguments.memoryRefs ?? [],
|
|
109
|
-
answer: data.arguments.answer,
|
|
110
|
-
toolDescription: data.arguments.toolDescription,
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
case "If":
|
|
115
|
-
return {
|
|
116
|
-
id: data.id,
|
|
117
|
-
type: data.type,
|
|
118
|
-
position: position,
|
|
119
|
-
arguments: {
|
|
120
|
-
condition: data.arguments.condition,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
case "OnFunctionCall":
|
|
124
|
-
return {
|
|
125
|
-
id: data.id,
|
|
126
|
-
type: data.type,
|
|
127
|
-
position: position,
|
|
128
|
-
};
|
|
129
|
-
case "OnStartup":
|
|
130
|
-
return {
|
|
131
|
-
id: data.id,
|
|
132
|
-
type: data.type,
|
|
133
|
-
position: position,
|
|
134
|
-
};
|
|
135
|
-
case "OnPinEdge":
|
|
136
|
-
return {
|
|
137
|
-
id: data.id,
|
|
138
|
-
type: data.type,
|
|
139
|
-
position: position,
|
|
140
|
-
arguments: {
|
|
141
|
-
pinReference: data.arguments.pinReference!,
|
|
142
|
-
edge: data.arguments.edge,
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
case "OnSerialReceive":
|
|
146
|
-
return {
|
|
147
|
-
id: data.id,
|
|
148
|
-
type: data.type,
|
|
149
|
-
position: position,
|
|
150
|
-
arguments: {
|
|
151
|
-
portReference: data.arguments.portReference!,
|
|
152
|
-
output: data.arguments.output,
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
case "OnThreshold":
|
|
156
|
-
return {
|
|
157
|
-
id: data.id,
|
|
158
|
-
type: data.type,
|
|
159
|
-
position: position,
|
|
160
|
-
arguments: {
|
|
161
|
-
variable: data.arguments.variable!,
|
|
162
|
-
threshold: data.arguments.threshold!,
|
|
163
|
-
direction: data.arguments.direction,
|
|
164
|
-
deadband: data.arguments.deadband,
|
|
165
|
-
output: data.arguments.output,
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
case "Delay":
|
|
169
|
-
return {
|
|
170
|
-
id: data.id,
|
|
171
|
-
type: data.type,
|
|
172
|
-
position: position,
|
|
173
|
-
arguments: {
|
|
174
|
-
delayMs: data.arguments.delayMs!,
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
case "Ticker":
|
|
178
|
-
return {
|
|
179
|
-
id: data.id,
|
|
180
|
-
type: data.type,
|
|
181
|
-
position: position,
|
|
182
|
-
arguments: {
|
|
183
|
-
intervalValue: data.arguments.intervalValue!,
|
|
184
|
-
intervalUnit: data.arguments.intervalUnit,
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
case "Alarm":
|
|
188
|
-
return {
|
|
189
|
-
id: data.id,
|
|
190
|
-
type: data.type,
|
|
191
|
-
position: position,
|
|
192
|
-
arguments: {
|
|
193
|
-
time: data.arguments.time,
|
|
194
|
-
days: data.arguments.days,
|
|
195
|
-
},
|
|
196
|
-
};
|
|
197
|
-
case "WebSearchTool":
|
|
198
|
-
return {
|
|
199
|
-
id: data.id,
|
|
200
|
-
type: data.type,
|
|
201
|
-
position: position,
|
|
202
|
-
arguments: {
|
|
203
|
-
maxResults: data.arguments.maxResults,
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
case "Retriever":
|
|
207
|
-
return {
|
|
208
|
-
id: data.id,
|
|
209
|
-
type: data.type,
|
|
210
|
-
position: position,
|
|
211
|
-
arguments: {
|
|
212
|
-
memoryReference: data.arguments.memoryReference,
|
|
213
|
-
topK: data.arguments.topK!,
|
|
214
|
-
query: data.arguments.query,
|
|
215
|
-
output: data.arguments.output,
|
|
216
|
-
toolDescription: data.arguments.toolDescription,
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
case "WebFetch":
|
|
220
|
-
return {
|
|
221
|
-
id: data.id,
|
|
222
|
-
type: data.type,
|
|
223
|
-
position: position,
|
|
224
|
-
arguments: {
|
|
225
|
-
url: data.arguments.url,
|
|
226
|
-
maxChars: data.arguments.maxChars,
|
|
227
|
-
output: data.arguments.output,
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
case "FunctionCall": {
|
|
231
|
-
// Frontend stores FunctionCall args flat (unified with every other node), but
|
|
232
|
-
// the API schema keeps the nested { inputBindings, outputBindings } shape.
|
|
233
|
-
// Translate here so the api format stays stable. `toolDescription` sits
|
|
234
|
-
// alongside the bindings at the api level and is only emitted when the
|
|
235
|
-
// node is currently wired as a tool (exec-mode calls don't need it).
|
|
236
|
-
const inputBindings: Record<string, Expression> = {};
|
|
237
|
-
const outputBindings: Record<string, OutputBinding> = {};
|
|
238
|
-
const args = data.arguments as Record<string, unknown>;
|
|
239
|
-
for (const arg of data.functionInfo.arguments) {
|
|
240
|
-
const key = arg.uid ?? arg.name;
|
|
241
|
-
const v = args[key];
|
|
242
|
-
if (v !== undefined) inputBindings[key] = v as Expression;
|
|
243
|
-
}
|
|
244
|
-
for (const ret of data.functionInfo.returns) {
|
|
245
|
-
const key = ret.uid ?? ret.name;
|
|
246
|
-
const v = args[key];
|
|
247
|
-
if (v !== undefined) outputBindings[key] = v as OutputBinding;
|
|
248
|
-
}
|
|
249
|
-
const toolDescription = args.toolDescription as string | undefined;
|
|
250
|
-
return {
|
|
251
|
-
id: data.id,
|
|
252
|
-
type: data.type,
|
|
253
|
-
functionId: data.functionInfo.id,
|
|
254
|
-
position: position,
|
|
255
|
-
arguments: {
|
|
256
|
-
inputBindings,
|
|
257
|
-
outputBindings,
|
|
258
|
-
...(isToolInput && toolDescription !== undefined ? { toolDescription } : {}),
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
case "SetVariable":
|
|
263
|
-
return {
|
|
264
|
-
id: data.id,
|
|
265
|
-
type: data.type,
|
|
266
|
-
position: position,
|
|
267
|
-
arguments: {
|
|
268
|
-
variable: data.arguments.variable!,
|
|
269
|
-
value: data.arguments.value,
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
case "MqttPublish":
|
|
273
|
-
return {
|
|
274
|
-
id: data.id,
|
|
275
|
-
type: data.type,
|
|
276
|
-
position: position,
|
|
277
|
-
arguments: {
|
|
278
|
-
channelReference: data.arguments.channelReference ?? "",
|
|
279
|
-
dataType: data.arguments.dataType,
|
|
280
|
-
value: data.arguments.value,
|
|
281
|
-
qos: Number(data.arguments.qos) as 0 | 1 | 2,
|
|
282
|
-
retain: data.arguments.retain,
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
case "OnMqttMessage":
|
|
286
|
-
return {
|
|
287
|
-
id: data.id,
|
|
288
|
-
type: data.type,
|
|
289
|
-
position: position,
|
|
290
|
-
arguments: {
|
|
291
|
-
channelReference: data.arguments.channelReference ?? "",
|
|
292
|
-
dataType: data.arguments.dataType,
|
|
293
|
-
output: data.arguments.output,
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Convert a strict API Node to a domain Node (NodeData + position). `resolveFunctionInfo`
|
|
301
|
-
* is required only for `FunctionCall` nodes — see {@link ResolveFunctionInfo}.
|
|
302
|
-
*/
|
|
303
|
-
export function deserialize(apiNode: ApiNode, resolveFunctionInfo?: ResolveFunctionInfo): Node {
|
|
304
|
-
return { ...deserializeNodeData(apiNode, resolveFunctionInfo), position: apiNode.position };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Build the NodeData payload from an API Node (no position). */
|
|
308
|
-
function deserializeNodeData(apiNode: Schemas["Node"], resolveFunctionInfo?: ResolveFunctionInfo): NodeData {
|
|
309
|
-
switch (apiNode.type) {
|
|
310
|
-
case "ReadPin":
|
|
311
|
-
return {
|
|
312
|
-
id: apiNode.id,
|
|
313
|
-
type: apiNode.type,
|
|
314
|
-
label: apiNode.label,
|
|
315
|
-
arguments: {
|
|
316
|
-
pinReference: apiNode.arguments.pinReference ?? "",
|
|
317
|
-
signalType: apiNode.arguments.signalType,
|
|
318
|
-
output: apiNode.arguments.output as OutputBinding,
|
|
319
|
-
toolDescription: apiNode.arguments.toolDescription,
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
|
-
case "SerialRead":
|
|
323
|
-
return {
|
|
324
|
-
id: apiNode.id,
|
|
325
|
-
type: apiNode.type,
|
|
326
|
-
label: apiNode.label,
|
|
327
|
-
arguments: {
|
|
328
|
-
portReference: apiNode.arguments.portReference ?? "",
|
|
329
|
-
prompt: apiNode.arguments.prompt ?? "",
|
|
330
|
-
output: apiNode.arguments.output as OutputBinding,
|
|
331
|
-
},
|
|
332
|
-
};
|
|
333
|
-
case "Retriever":
|
|
334
|
-
return {
|
|
335
|
-
id: apiNode.id,
|
|
336
|
-
type: apiNode.type,
|
|
337
|
-
label: apiNode.label,
|
|
338
|
-
arguments: {
|
|
339
|
-
memoryReference: apiNode.arguments.memoryReference ?? "",
|
|
340
|
-
topK: apiNode.arguments.topK ?? 0,
|
|
341
|
-
query: apiNode.arguments.query,
|
|
342
|
-
output: apiNode.arguments.output as OutputBinding,
|
|
343
|
-
toolDescription: apiNode.arguments.toolDescription,
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
case "WritePin":
|
|
347
|
-
return {
|
|
348
|
-
id: apiNode.id,
|
|
349
|
-
type: apiNode.type,
|
|
350
|
-
label: apiNode.label,
|
|
351
|
-
arguments: {
|
|
352
|
-
pinReference: apiNode.arguments.pinReference ?? "",
|
|
353
|
-
signalType: apiNode.arguments.signalType,
|
|
354
|
-
value: apiNode.arguments.value,
|
|
355
|
-
},
|
|
356
|
-
};
|
|
357
|
-
case "SerialWrite":
|
|
358
|
-
return {
|
|
359
|
-
id: apiNode.id,
|
|
360
|
-
type: apiNode.type,
|
|
361
|
-
label: apiNode.label,
|
|
362
|
-
arguments: {
|
|
363
|
-
portReference: apiNode.arguments.portReference ?? "",
|
|
364
|
-
value: apiNode.arguments.value,
|
|
365
|
-
},
|
|
366
|
-
};
|
|
367
|
-
case "Agent":
|
|
368
|
-
return {
|
|
369
|
-
id: apiNode.id,
|
|
370
|
-
type: apiNode.type,
|
|
371
|
-
label: apiNode.label,
|
|
372
|
-
arguments: {
|
|
373
|
-
name: apiNode.arguments.name ?? "",
|
|
374
|
-
model: apiNode.arguments.model ?? "",
|
|
375
|
-
instructions: apiNode.arguments.instructions ?? "",
|
|
376
|
-
maxTurns: apiNode.arguments.maxTurns,
|
|
377
|
-
outputDeclarations: apiNode.arguments.outputDeclarations as OutputDeclaration[],
|
|
378
|
-
memoryRefs: apiNode.arguments.memoryRefs ?? [],
|
|
379
|
-
answer: apiNode.arguments.answer as OutputBinding,
|
|
380
|
-
toolDescription: apiNode.arguments.toolDescription,
|
|
381
|
-
},
|
|
382
|
-
};
|
|
383
|
-
case "If":
|
|
384
|
-
return {
|
|
385
|
-
id: apiNode.id,
|
|
386
|
-
type: apiNode.type,
|
|
387
|
-
label: apiNode.label,
|
|
388
|
-
arguments: {
|
|
389
|
-
condition: apiNode.arguments.condition,
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
case "OnFunctionCall":
|
|
393
|
-
return { id: apiNode.id, type: apiNode.type, label: apiNode.label, arguments: {} };
|
|
394
|
-
case "OnStartup":
|
|
395
|
-
return { id: apiNode.id, type: apiNode.type, label: apiNode.label, arguments: {} };
|
|
396
|
-
case "OnPinEdge":
|
|
397
|
-
return {
|
|
398
|
-
id: apiNode.id,
|
|
399
|
-
type: apiNode.type,
|
|
400
|
-
label: apiNode.label,
|
|
401
|
-
arguments: {
|
|
402
|
-
pinReference: apiNode.arguments.pinReference ?? "",
|
|
403
|
-
edge: apiNode.arguments.edge,
|
|
404
|
-
},
|
|
405
|
-
};
|
|
406
|
-
case "OnSerialReceive":
|
|
407
|
-
return {
|
|
408
|
-
id: apiNode.id,
|
|
409
|
-
type: apiNode.type,
|
|
410
|
-
label: apiNode.label,
|
|
411
|
-
arguments: {
|
|
412
|
-
portReference: apiNode.arguments.portReference ?? "",
|
|
413
|
-
output: apiNode.arguments.output as OutputBinding,
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
case "OnThreshold":
|
|
417
|
-
return {
|
|
418
|
-
id: apiNode.id,
|
|
419
|
-
type: apiNode.type,
|
|
420
|
-
label: apiNode.label,
|
|
421
|
-
arguments: {
|
|
422
|
-
variable: apiNode.arguments.variable,
|
|
423
|
-
threshold: apiNode.arguments.threshold,
|
|
424
|
-
direction: apiNode.arguments.direction,
|
|
425
|
-
deadband: apiNode.arguments.deadband,
|
|
426
|
-
output: apiNode.arguments.output as OutputBinding,
|
|
427
|
-
},
|
|
428
|
-
};
|
|
429
|
-
case "Delay":
|
|
430
|
-
return {
|
|
431
|
-
id: apiNode.id,
|
|
432
|
-
type: apiNode.type,
|
|
433
|
-
label: apiNode.label,
|
|
434
|
-
arguments: {
|
|
435
|
-
delayMs: apiNode.arguments.delayMs ?? 0,
|
|
436
|
-
},
|
|
437
|
-
};
|
|
438
|
-
case "Ticker":
|
|
439
|
-
return {
|
|
440
|
-
id: apiNode.id,
|
|
441
|
-
type: apiNode.type,
|
|
442
|
-
label: apiNode.label,
|
|
443
|
-
arguments: {
|
|
444
|
-
intervalValue: apiNode.arguments.intervalValue ?? 0,
|
|
445
|
-
intervalUnit: apiNode.arguments.intervalUnit,
|
|
446
|
-
},
|
|
447
|
-
};
|
|
448
|
-
case "Alarm":
|
|
449
|
-
return {
|
|
450
|
-
id: apiNode.id,
|
|
451
|
-
type: apiNode.type,
|
|
452
|
-
label: apiNode.label,
|
|
453
|
-
arguments: {
|
|
454
|
-
time: apiNode.arguments.time ?? "",
|
|
455
|
-
days: apiNode.arguments.days,
|
|
456
|
-
},
|
|
457
|
-
};
|
|
458
|
-
case "WebSearchTool":
|
|
459
|
-
return {
|
|
460
|
-
id: apiNode.id,
|
|
461
|
-
type: apiNode.type,
|
|
462
|
-
label: apiNode.label,
|
|
463
|
-
arguments: {
|
|
464
|
-
maxResults: apiNode.arguments.maxResults,
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
case "WebFetch":
|
|
468
|
-
return {
|
|
469
|
-
id: apiNode.id,
|
|
470
|
-
type: apiNode.type,
|
|
471
|
-
label: apiNode.label,
|
|
472
|
-
arguments: {
|
|
473
|
-
url: apiNode.arguments.url,
|
|
474
|
-
maxChars: apiNode.arguments.maxChars,
|
|
475
|
-
output: apiNode.arguments.output as OutputBinding,
|
|
476
|
-
},
|
|
477
|
-
};
|
|
478
|
-
case "SetVariable":
|
|
479
|
-
return {
|
|
480
|
-
id: apiNode.id,
|
|
481
|
-
type: apiNode.type,
|
|
482
|
-
label: apiNode.label,
|
|
483
|
-
arguments: {
|
|
484
|
-
variable: apiNode.arguments.variable,
|
|
485
|
-
value: apiNode.arguments.value,
|
|
486
|
-
},
|
|
487
|
-
};
|
|
488
|
-
case "FunctionCall": {
|
|
489
|
-
// Lift the api's nested { inputBindings, outputBindings } into the flat
|
|
490
|
-
// domain arguments record. Uid collisions are impossible within a single
|
|
491
|
-
// function (one namespace across args + returns). `toolDescription`
|
|
492
|
-
// sits at the same level in the api and is folded into the flat bag
|
|
493
|
-
// under the reserved `toolDescription` key.
|
|
494
|
-
const flat: Record<string, Expression | OutputBinding | string> = {
|
|
495
|
-
...((apiNode.arguments.inputBindings ?? {}) as Record<string, Expression>),
|
|
496
|
-
...((apiNode.arguments.outputBindings ?? {}) as Record<string, OutputBinding>),
|
|
497
|
-
};
|
|
498
|
-
if (apiNode.arguments.toolDescription !== undefined) {
|
|
499
|
-
flat.toolDescription = apiNode.arguments.toolDescription;
|
|
500
|
-
}
|
|
501
|
-
// The wire carries only `functionId`; rebuild the in-memory signature snapshot
|
|
502
|
-
// from the workflow's function table. A missing function (deleted/hand-edited)
|
|
503
|
-
// gets a minimal stub so the node still loads and surfaces as deleted.
|
|
504
|
-
const functionInfo: FunctionInfo = resolveFunctionInfo?.(apiNode.functionId) ?? {
|
|
505
|
-
id: apiNode.functionId,
|
|
506
|
-
version: 0,
|
|
507
|
-
name: "",
|
|
508
|
-
arguments: [],
|
|
509
|
-
returns: [],
|
|
510
|
-
};
|
|
511
|
-
return {
|
|
512
|
-
id: apiNode.id,
|
|
513
|
-
type: apiNode.type,
|
|
514
|
-
label: apiNode.label,
|
|
515
|
-
functionInfo,
|
|
516
|
-
arguments: flat,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
case "MqttPublish":
|
|
520
|
-
return {
|
|
521
|
-
id: apiNode.id,
|
|
522
|
-
type: apiNode.type,
|
|
523
|
-
label: apiNode.label,
|
|
524
|
-
arguments: {
|
|
525
|
-
channelReference: apiNode.arguments.channelReference ?? "",
|
|
526
|
-
dataType: apiNode.arguments.dataType,
|
|
527
|
-
value: apiNode.arguments.value,
|
|
528
|
-
qos: String(apiNode.arguments.qos) as "0" | "1" | "2",
|
|
529
|
-
retain: apiNode.arguments.retain,
|
|
530
|
-
},
|
|
531
|
-
};
|
|
532
|
-
case "OnMqttMessage":
|
|
533
|
-
return {
|
|
534
|
-
id: apiNode.id,
|
|
535
|
-
type: apiNode.type,
|
|
536
|
-
label: apiNode.label,
|
|
537
|
-
arguments: {
|
|
538
|
-
channelReference: apiNode.arguments.channelReference ?? "",
|
|
539
|
-
dataType: apiNode.arguments.dataType,
|
|
540
|
-
output: apiNode.arguments.output as OutputBinding,
|
|
541
|
-
},
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
}
|
|
1
|
+
import type { Schemas } from "../api";
|
|
2
|
+
import type { NodeData, Node } from "./Node";
|
|
3
|
+
import type { Expression } from "../api";
|
|
4
|
+
import type { FunctionInfo } from "../function";
|
|
5
|
+
import type { OutputBinding, OutputDeclaration } from "../parameter";
|
|
6
|
+
import { pruneArguments } from "../parameter";
|
|
7
|
+
import { NodeRegistry } from "./NodeRegistry";
|
|
8
|
+
|
|
9
|
+
export type ApiNode = Schemas["Node"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a function's signature snapshot by id. A `FunctionCall` on the wire stores
|
|
13
|
+
* only `functionId`; deserialize rebuilds the in-memory `functionInfo` snapshot
|
|
14
|
+
* (which core's variable helpers + staleness read) from the workflow's function
|
|
15
|
+
* table via this. Unknown id (e.g. a call to a since-deleted function) → a minimal
|
|
16
|
+
* stub so the node still round-trips and surfaces as deleted.
|
|
17
|
+
*/
|
|
18
|
+
export type ResolveFunctionInfo = (functionId: string) => FunctionInfo | undefined;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Serialize a domain Node to the strict API format (Schemas["Node"]).
|
|
22
|
+
* Strips hidden parameters (those whose activationRules are not met). The
|
|
23
|
+
* `isToolInput` flag is threaded into activation evaluation so rules like
|
|
24
|
+
* `isControlFlow` / `isToolInput` resolve correctly per-instance.
|
|
25
|
+
*/
|
|
26
|
+
export function serialize(node: Node, isToolInput: boolean): ApiNode {
|
|
27
|
+
const result = serializeNodeData(node, node.position, isToolInput);
|
|
28
|
+
if (node.label) {
|
|
29
|
+
result.label = node.label;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// serializeNode emits gated/optional params uniformly; this pass prunes the
|
|
33
|
+
// ones that must not reach the api — inactive for this instance, or empty
|
|
34
|
+
// optionals (so the consumer's presence check sees absent, not `""`/`null`).
|
|
35
|
+
// FunctionCall gates its own params inline (its bindings have a different api
|
|
36
|
+
// shape), so it's excluded here.
|
|
37
|
+
if ("arguments" in result && result.arguments) {
|
|
38
|
+
const def = node.type !== "FunctionCall" ? NodeRegistry.getByType(node.type) : undefined;
|
|
39
|
+
if (def) {
|
|
40
|
+
pruneArguments(result.arguments, def.parameters, isToolInput);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function serializeNodeData(data: NodeData, position: { x: number; y: number }, isToolInput: boolean): Schemas["Node"] {
|
|
48
|
+
switch (data.type) {
|
|
49
|
+
case "ReadPin":
|
|
50
|
+
return {
|
|
51
|
+
id: data.id,
|
|
52
|
+
type: data.type,
|
|
53
|
+
position: position,
|
|
54
|
+
arguments: {
|
|
55
|
+
pinReference: data.arguments.pinReference!,
|
|
56
|
+
signalType: data.arguments.signalType,
|
|
57
|
+
output: data.arguments.output,
|
|
58
|
+
toolDescription: data.arguments.toolDescription,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
case "SerialRead":
|
|
62
|
+
return {
|
|
63
|
+
id: data.id,
|
|
64
|
+
type: data.type,
|
|
65
|
+
position: position,
|
|
66
|
+
arguments: {
|
|
67
|
+
portReference: data.arguments.portReference!,
|
|
68
|
+
...(data.arguments.prompt !== undefined ? { prompt: data.arguments.prompt } : {}),
|
|
69
|
+
output: data.arguments.output,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
case "WritePin":
|
|
73
|
+
return {
|
|
74
|
+
id: data.id,
|
|
75
|
+
type: data.type,
|
|
76
|
+
position: position,
|
|
77
|
+
arguments: {
|
|
78
|
+
pinReference: data.arguments.pinReference!,
|
|
79
|
+
signalType: data.arguments.signalType,
|
|
80
|
+
value: data.arguments.value,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
case "SerialWrite":
|
|
84
|
+
return {
|
|
85
|
+
id: data.id,
|
|
86
|
+
type: data.type,
|
|
87
|
+
position: position,
|
|
88
|
+
arguments: {
|
|
89
|
+
portReference: data.arguments.portReference!,
|
|
90
|
+
value: data.arguments.value,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
case "Agent": {
|
|
94
|
+
// outputDeclarations is a list both in domain and API. Each entry's `name`
|
|
95
|
+
// is the JSON property the LLM is asked to produce; uniqueness is enforced
|
|
96
|
+
// by diagnostics, not the schema. memoryRefs is also a 1:1 list — domain
|
|
97
|
+
// and API share the same MemoryRef shape.
|
|
98
|
+
return {
|
|
99
|
+
id: data.id,
|
|
100
|
+
type: data.type,
|
|
101
|
+
position: position,
|
|
102
|
+
arguments: {
|
|
103
|
+
name: data.arguments.name,
|
|
104
|
+
model: data.arguments.model,
|
|
105
|
+
instructions: data.arguments.instructions,
|
|
106
|
+
maxTurns: data.arguments.maxTurns,
|
|
107
|
+
outputDeclarations: data.arguments.outputDeclarations,
|
|
108
|
+
memoryRefs: data.arguments.memoryRefs ?? [],
|
|
109
|
+
answer: data.arguments.answer,
|
|
110
|
+
toolDescription: data.arguments.toolDescription,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case "If":
|
|
115
|
+
return {
|
|
116
|
+
id: data.id,
|
|
117
|
+
type: data.type,
|
|
118
|
+
position: position,
|
|
119
|
+
arguments: {
|
|
120
|
+
condition: data.arguments.condition,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
case "OnFunctionCall":
|
|
124
|
+
return {
|
|
125
|
+
id: data.id,
|
|
126
|
+
type: data.type,
|
|
127
|
+
position: position,
|
|
128
|
+
};
|
|
129
|
+
case "OnStartup":
|
|
130
|
+
return {
|
|
131
|
+
id: data.id,
|
|
132
|
+
type: data.type,
|
|
133
|
+
position: position,
|
|
134
|
+
};
|
|
135
|
+
case "OnPinEdge":
|
|
136
|
+
return {
|
|
137
|
+
id: data.id,
|
|
138
|
+
type: data.type,
|
|
139
|
+
position: position,
|
|
140
|
+
arguments: {
|
|
141
|
+
pinReference: data.arguments.pinReference!,
|
|
142
|
+
edge: data.arguments.edge,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
case "OnSerialReceive":
|
|
146
|
+
return {
|
|
147
|
+
id: data.id,
|
|
148
|
+
type: data.type,
|
|
149
|
+
position: position,
|
|
150
|
+
arguments: {
|
|
151
|
+
portReference: data.arguments.portReference!,
|
|
152
|
+
output: data.arguments.output,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
case "OnThreshold":
|
|
156
|
+
return {
|
|
157
|
+
id: data.id,
|
|
158
|
+
type: data.type,
|
|
159
|
+
position: position,
|
|
160
|
+
arguments: {
|
|
161
|
+
variable: data.arguments.variable!,
|
|
162
|
+
threshold: data.arguments.threshold!,
|
|
163
|
+
direction: data.arguments.direction,
|
|
164
|
+
deadband: data.arguments.deadband,
|
|
165
|
+
output: data.arguments.output,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
case "Delay":
|
|
169
|
+
return {
|
|
170
|
+
id: data.id,
|
|
171
|
+
type: data.type,
|
|
172
|
+
position: position,
|
|
173
|
+
arguments: {
|
|
174
|
+
delayMs: data.arguments.delayMs!,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
case "Ticker":
|
|
178
|
+
return {
|
|
179
|
+
id: data.id,
|
|
180
|
+
type: data.type,
|
|
181
|
+
position: position,
|
|
182
|
+
arguments: {
|
|
183
|
+
intervalValue: data.arguments.intervalValue!,
|
|
184
|
+
intervalUnit: data.arguments.intervalUnit,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
case "Alarm":
|
|
188
|
+
return {
|
|
189
|
+
id: data.id,
|
|
190
|
+
type: data.type,
|
|
191
|
+
position: position,
|
|
192
|
+
arguments: {
|
|
193
|
+
time: data.arguments.time,
|
|
194
|
+
days: data.arguments.days,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
case "WebSearchTool":
|
|
198
|
+
return {
|
|
199
|
+
id: data.id,
|
|
200
|
+
type: data.type,
|
|
201
|
+
position: position,
|
|
202
|
+
arguments: {
|
|
203
|
+
maxResults: data.arguments.maxResults,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
case "Retriever":
|
|
207
|
+
return {
|
|
208
|
+
id: data.id,
|
|
209
|
+
type: data.type,
|
|
210
|
+
position: position,
|
|
211
|
+
arguments: {
|
|
212
|
+
memoryReference: data.arguments.memoryReference,
|
|
213
|
+
topK: data.arguments.topK!,
|
|
214
|
+
query: data.arguments.query,
|
|
215
|
+
output: data.arguments.output,
|
|
216
|
+
toolDescription: data.arguments.toolDescription,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
case "WebFetch":
|
|
220
|
+
return {
|
|
221
|
+
id: data.id,
|
|
222
|
+
type: data.type,
|
|
223
|
+
position: position,
|
|
224
|
+
arguments: {
|
|
225
|
+
url: data.arguments.url,
|
|
226
|
+
maxChars: data.arguments.maxChars,
|
|
227
|
+
output: data.arguments.output,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
case "FunctionCall": {
|
|
231
|
+
// Frontend stores FunctionCall args flat (unified with every other node), but
|
|
232
|
+
// the API schema keeps the nested { inputBindings, outputBindings } shape.
|
|
233
|
+
// Translate here so the api format stays stable. `toolDescription` sits
|
|
234
|
+
// alongside the bindings at the api level and is only emitted when the
|
|
235
|
+
// node is currently wired as a tool (exec-mode calls don't need it).
|
|
236
|
+
const inputBindings: Record<string, Expression> = {};
|
|
237
|
+
const outputBindings: Record<string, OutputBinding> = {};
|
|
238
|
+
const args = data.arguments as Record<string, unknown>;
|
|
239
|
+
for (const arg of data.functionInfo.arguments) {
|
|
240
|
+
const key = arg.uid ?? arg.name;
|
|
241
|
+
const v = args[key];
|
|
242
|
+
if (v !== undefined) inputBindings[key] = v as Expression;
|
|
243
|
+
}
|
|
244
|
+
for (const ret of data.functionInfo.returns) {
|
|
245
|
+
const key = ret.uid ?? ret.name;
|
|
246
|
+
const v = args[key];
|
|
247
|
+
if (v !== undefined) outputBindings[key] = v as OutputBinding;
|
|
248
|
+
}
|
|
249
|
+
const toolDescription = args.toolDescription as string | undefined;
|
|
250
|
+
return {
|
|
251
|
+
id: data.id,
|
|
252
|
+
type: data.type,
|
|
253
|
+
functionId: data.functionInfo.id,
|
|
254
|
+
position: position,
|
|
255
|
+
arguments: {
|
|
256
|
+
inputBindings,
|
|
257
|
+
outputBindings,
|
|
258
|
+
...(isToolInput && toolDescription !== undefined ? { toolDescription } : {}),
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
case "SetVariable":
|
|
263
|
+
return {
|
|
264
|
+
id: data.id,
|
|
265
|
+
type: data.type,
|
|
266
|
+
position: position,
|
|
267
|
+
arguments: {
|
|
268
|
+
variable: data.arguments.variable!,
|
|
269
|
+
value: data.arguments.value,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
case "MqttPublish":
|
|
273
|
+
return {
|
|
274
|
+
id: data.id,
|
|
275
|
+
type: data.type,
|
|
276
|
+
position: position,
|
|
277
|
+
arguments: {
|
|
278
|
+
channelReference: data.arguments.channelReference ?? "",
|
|
279
|
+
dataType: data.arguments.dataType,
|
|
280
|
+
value: data.arguments.value,
|
|
281
|
+
qos: Number(data.arguments.qos) as 0 | 1 | 2,
|
|
282
|
+
retain: data.arguments.retain,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
case "OnMqttMessage":
|
|
286
|
+
return {
|
|
287
|
+
id: data.id,
|
|
288
|
+
type: data.type,
|
|
289
|
+
position: position,
|
|
290
|
+
arguments: {
|
|
291
|
+
channelReference: data.arguments.channelReference ?? "",
|
|
292
|
+
dataType: data.arguments.dataType,
|
|
293
|
+
output: data.arguments.output,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Convert a strict API Node to a domain Node (NodeData + position). `resolveFunctionInfo`
|
|
301
|
+
* is required only for `FunctionCall` nodes — see {@link ResolveFunctionInfo}.
|
|
302
|
+
*/
|
|
303
|
+
export function deserialize(apiNode: ApiNode, resolveFunctionInfo?: ResolveFunctionInfo): Node {
|
|
304
|
+
return { ...deserializeNodeData(apiNode, resolveFunctionInfo), position: apiNode.position };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Build the NodeData payload from an API Node (no position). */
|
|
308
|
+
function deserializeNodeData(apiNode: Schemas["Node"], resolveFunctionInfo?: ResolveFunctionInfo): NodeData {
|
|
309
|
+
switch (apiNode.type) {
|
|
310
|
+
case "ReadPin":
|
|
311
|
+
return {
|
|
312
|
+
id: apiNode.id,
|
|
313
|
+
type: apiNode.type,
|
|
314
|
+
label: apiNode.label,
|
|
315
|
+
arguments: {
|
|
316
|
+
pinReference: apiNode.arguments.pinReference ?? "",
|
|
317
|
+
signalType: apiNode.arguments.signalType,
|
|
318
|
+
output: apiNode.arguments.output as OutputBinding,
|
|
319
|
+
toolDescription: apiNode.arguments.toolDescription,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
case "SerialRead":
|
|
323
|
+
return {
|
|
324
|
+
id: apiNode.id,
|
|
325
|
+
type: apiNode.type,
|
|
326
|
+
label: apiNode.label,
|
|
327
|
+
arguments: {
|
|
328
|
+
portReference: apiNode.arguments.portReference ?? "",
|
|
329
|
+
prompt: apiNode.arguments.prompt ?? "",
|
|
330
|
+
output: apiNode.arguments.output as OutputBinding,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
case "Retriever":
|
|
334
|
+
return {
|
|
335
|
+
id: apiNode.id,
|
|
336
|
+
type: apiNode.type,
|
|
337
|
+
label: apiNode.label,
|
|
338
|
+
arguments: {
|
|
339
|
+
memoryReference: apiNode.arguments.memoryReference ?? "",
|
|
340
|
+
topK: apiNode.arguments.topK ?? 0,
|
|
341
|
+
query: apiNode.arguments.query,
|
|
342
|
+
output: apiNode.arguments.output as OutputBinding,
|
|
343
|
+
toolDescription: apiNode.arguments.toolDescription,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
case "WritePin":
|
|
347
|
+
return {
|
|
348
|
+
id: apiNode.id,
|
|
349
|
+
type: apiNode.type,
|
|
350
|
+
label: apiNode.label,
|
|
351
|
+
arguments: {
|
|
352
|
+
pinReference: apiNode.arguments.pinReference ?? "",
|
|
353
|
+
signalType: apiNode.arguments.signalType,
|
|
354
|
+
value: apiNode.arguments.value,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
case "SerialWrite":
|
|
358
|
+
return {
|
|
359
|
+
id: apiNode.id,
|
|
360
|
+
type: apiNode.type,
|
|
361
|
+
label: apiNode.label,
|
|
362
|
+
arguments: {
|
|
363
|
+
portReference: apiNode.arguments.portReference ?? "",
|
|
364
|
+
value: apiNode.arguments.value,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
case "Agent":
|
|
368
|
+
return {
|
|
369
|
+
id: apiNode.id,
|
|
370
|
+
type: apiNode.type,
|
|
371
|
+
label: apiNode.label,
|
|
372
|
+
arguments: {
|
|
373
|
+
name: apiNode.arguments.name ?? "",
|
|
374
|
+
model: apiNode.arguments.model ?? "",
|
|
375
|
+
instructions: apiNode.arguments.instructions ?? "",
|
|
376
|
+
maxTurns: apiNode.arguments.maxTurns,
|
|
377
|
+
outputDeclarations: apiNode.arguments.outputDeclarations as OutputDeclaration[],
|
|
378
|
+
memoryRefs: apiNode.arguments.memoryRefs ?? [],
|
|
379
|
+
answer: apiNode.arguments.answer as OutputBinding,
|
|
380
|
+
toolDescription: apiNode.arguments.toolDescription,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
case "If":
|
|
384
|
+
return {
|
|
385
|
+
id: apiNode.id,
|
|
386
|
+
type: apiNode.type,
|
|
387
|
+
label: apiNode.label,
|
|
388
|
+
arguments: {
|
|
389
|
+
condition: apiNode.arguments.condition,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
case "OnFunctionCall":
|
|
393
|
+
return { id: apiNode.id, type: apiNode.type, label: apiNode.label, arguments: {} };
|
|
394
|
+
case "OnStartup":
|
|
395
|
+
return { id: apiNode.id, type: apiNode.type, label: apiNode.label, arguments: {} };
|
|
396
|
+
case "OnPinEdge":
|
|
397
|
+
return {
|
|
398
|
+
id: apiNode.id,
|
|
399
|
+
type: apiNode.type,
|
|
400
|
+
label: apiNode.label,
|
|
401
|
+
arguments: {
|
|
402
|
+
pinReference: apiNode.arguments.pinReference ?? "",
|
|
403
|
+
edge: apiNode.arguments.edge,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
case "OnSerialReceive":
|
|
407
|
+
return {
|
|
408
|
+
id: apiNode.id,
|
|
409
|
+
type: apiNode.type,
|
|
410
|
+
label: apiNode.label,
|
|
411
|
+
arguments: {
|
|
412
|
+
portReference: apiNode.arguments.portReference ?? "",
|
|
413
|
+
output: apiNode.arguments.output as OutputBinding,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
case "OnThreshold":
|
|
417
|
+
return {
|
|
418
|
+
id: apiNode.id,
|
|
419
|
+
type: apiNode.type,
|
|
420
|
+
label: apiNode.label,
|
|
421
|
+
arguments: {
|
|
422
|
+
variable: apiNode.arguments.variable,
|
|
423
|
+
threshold: apiNode.arguments.threshold,
|
|
424
|
+
direction: apiNode.arguments.direction,
|
|
425
|
+
deadband: apiNode.arguments.deadband,
|
|
426
|
+
output: apiNode.arguments.output as OutputBinding,
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
case "Delay":
|
|
430
|
+
return {
|
|
431
|
+
id: apiNode.id,
|
|
432
|
+
type: apiNode.type,
|
|
433
|
+
label: apiNode.label,
|
|
434
|
+
arguments: {
|
|
435
|
+
delayMs: apiNode.arguments.delayMs ?? 0,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
case "Ticker":
|
|
439
|
+
return {
|
|
440
|
+
id: apiNode.id,
|
|
441
|
+
type: apiNode.type,
|
|
442
|
+
label: apiNode.label,
|
|
443
|
+
arguments: {
|
|
444
|
+
intervalValue: apiNode.arguments.intervalValue ?? 0,
|
|
445
|
+
intervalUnit: apiNode.arguments.intervalUnit,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
case "Alarm":
|
|
449
|
+
return {
|
|
450
|
+
id: apiNode.id,
|
|
451
|
+
type: apiNode.type,
|
|
452
|
+
label: apiNode.label,
|
|
453
|
+
arguments: {
|
|
454
|
+
time: apiNode.arguments.time ?? "",
|
|
455
|
+
days: apiNode.arguments.days,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
case "WebSearchTool":
|
|
459
|
+
return {
|
|
460
|
+
id: apiNode.id,
|
|
461
|
+
type: apiNode.type,
|
|
462
|
+
label: apiNode.label,
|
|
463
|
+
arguments: {
|
|
464
|
+
maxResults: apiNode.arguments.maxResults,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
case "WebFetch":
|
|
468
|
+
return {
|
|
469
|
+
id: apiNode.id,
|
|
470
|
+
type: apiNode.type,
|
|
471
|
+
label: apiNode.label,
|
|
472
|
+
arguments: {
|
|
473
|
+
url: apiNode.arguments.url,
|
|
474
|
+
maxChars: apiNode.arguments.maxChars,
|
|
475
|
+
output: apiNode.arguments.output as OutputBinding,
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
case "SetVariable":
|
|
479
|
+
return {
|
|
480
|
+
id: apiNode.id,
|
|
481
|
+
type: apiNode.type,
|
|
482
|
+
label: apiNode.label,
|
|
483
|
+
arguments: {
|
|
484
|
+
variable: apiNode.arguments.variable,
|
|
485
|
+
value: apiNode.arguments.value,
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
case "FunctionCall": {
|
|
489
|
+
// Lift the api's nested { inputBindings, outputBindings } into the flat
|
|
490
|
+
// domain arguments record. Uid collisions are impossible within a single
|
|
491
|
+
// function (one namespace across args + returns). `toolDescription`
|
|
492
|
+
// sits at the same level in the api and is folded into the flat bag
|
|
493
|
+
// under the reserved `toolDescription` key.
|
|
494
|
+
const flat: Record<string, Expression | OutputBinding | string> = {
|
|
495
|
+
...((apiNode.arguments.inputBindings ?? {}) as Record<string, Expression>),
|
|
496
|
+
...((apiNode.arguments.outputBindings ?? {}) as Record<string, OutputBinding>),
|
|
497
|
+
};
|
|
498
|
+
if (apiNode.arguments.toolDescription !== undefined) {
|
|
499
|
+
flat.toolDescription = apiNode.arguments.toolDescription;
|
|
500
|
+
}
|
|
501
|
+
// The wire carries only `functionId`; rebuild the in-memory signature snapshot
|
|
502
|
+
// from the workflow's function table. A missing function (deleted/hand-edited)
|
|
503
|
+
// gets a minimal stub so the node still loads and surfaces as deleted.
|
|
504
|
+
const functionInfo: FunctionInfo = resolveFunctionInfo?.(apiNode.functionId) ?? {
|
|
505
|
+
id: apiNode.functionId,
|
|
506
|
+
version: 0,
|
|
507
|
+
name: "",
|
|
508
|
+
arguments: [],
|
|
509
|
+
returns: [],
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
id: apiNode.id,
|
|
513
|
+
type: apiNode.type,
|
|
514
|
+
label: apiNode.label,
|
|
515
|
+
functionInfo,
|
|
516
|
+
arguments: flat,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
case "MqttPublish":
|
|
520
|
+
return {
|
|
521
|
+
id: apiNode.id,
|
|
522
|
+
type: apiNode.type,
|
|
523
|
+
label: apiNode.label,
|
|
524
|
+
arguments: {
|
|
525
|
+
channelReference: apiNode.arguments.channelReference ?? "",
|
|
526
|
+
dataType: apiNode.arguments.dataType,
|
|
527
|
+
value: apiNode.arguments.value,
|
|
528
|
+
qos: String(apiNode.arguments.qos) as "0" | "1" | "2",
|
|
529
|
+
retain: apiNode.arguments.retain,
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
case "OnMqttMessage":
|
|
533
|
+
return {
|
|
534
|
+
id: apiNode.id,
|
|
535
|
+
type: apiNode.type,
|
|
536
|
+
label: apiNode.label,
|
|
537
|
+
arguments: {
|
|
538
|
+
channelReference: apiNode.arguments.channelReference ?? "",
|
|
539
|
+
dataType: apiNode.arguments.dataType,
|
|
540
|
+
output: apiNode.arguments.output as OutputBinding,
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|