@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
package/src/node/methods.ts
CHANGED
|
@@ -1,278 +1,278 @@
|
|
|
1
|
-
import { NodeData, NodeOutput } from "./Node";
|
|
2
|
-
import type { OutputBinding, OutputDeclaration } from "../parameter";
|
|
3
|
-
import { PortDefinitions } from "./NodeDefinition";
|
|
4
|
-
import { resolveStaticOutputDataType } from "../parameter";
|
|
5
|
-
import { NodeRegistry } from "./NodeRegistry";
|
|
6
|
-
import { Edge } from "../edge";
|
|
7
|
-
|
|
8
|
-
/** Read a static output's binding from a node's arguments bag, keyed by the output id. */
|
|
9
|
-
function getStaticBinding(args: Record<string, unknown>, outputId: string): OutputBinding | undefined {
|
|
10
|
-
return args[outputId] as OutputBinding | undefined;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Read-only helper: look up the current binding for a given output key on any node.
|
|
15
|
-
* All static-style bindings (including FunctionCall returns) live at `arguments[outputId]`.
|
|
16
|
-
* List output entries are OutputDeclarations, projected to an OutputBinding shape — only
|
|
17
|
-
* emit-mode entries are addressable by uid here; callers that need to inspect assign-mode
|
|
18
|
-
* entries should walk the declaration list directly via `arguments[out.id]`.
|
|
19
|
-
*/
|
|
20
|
-
export function getOutputBinding(node: NodeData, outputId: string): OutputBinding | undefined {
|
|
21
|
-
const args = node.arguments as Record<string, unknown>;
|
|
22
|
-
|
|
23
|
-
if (node.type === "FunctionCall") {
|
|
24
|
-
return getStaticBinding(args, outputId);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const def = NodeRegistry.getByType(node.type);
|
|
28
|
-
if (!def?.outputs) return undefined;
|
|
29
|
-
|
|
30
|
-
for (const out of def.outputs) {
|
|
31
|
-
if (out.type === "static") {
|
|
32
|
-
if (out.id === outputId) return getStaticBinding(args, out.id);
|
|
33
|
-
} else {
|
|
34
|
-
const entries = args[out.id] as OutputDeclaration[] | undefined;
|
|
35
|
-
const entry = entries?.find((e) => e.mode === "emit" && e.uid === outputId);
|
|
36
|
-
if (entry && entry.mode === "emit") return { active: true, mode: "emit", name: entry.name };
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// External input requirements for debug mode
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
/** Describes a hardware input a node needs before it can execute in debug mode. */
|
|
47
|
-
export type ExternalInput = { kind: "gpio"; pinReference: string | undefined; dataType: "bool" | "int" } | { kind: "serial" };
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get ports for a node instance.
|
|
51
|
-
*/
|
|
52
|
-
export function getPorts(node: NodeData): PortDefinitions {
|
|
53
|
-
switch (node.type) {
|
|
54
|
-
case "ReadPin":
|
|
55
|
-
return {
|
|
56
|
-
input: [
|
|
57
|
-
{ id: "ctrl", type: "control" },
|
|
58
|
-
{ id: "tool", type: "tool", label: "As Tool" },
|
|
59
|
-
],
|
|
60
|
-
output: [{ id: "ctrl", type: "control" }],
|
|
61
|
-
};
|
|
62
|
-
case "SerialRead":
|
|
63
|
-
case "WritePin":
|
|
64
|
-
case "SerialWrite":
|
|
65
|
-
case "MqttPublish":
|
|
66
|
-
return {
|
|
67
|
-
input: [{ id: "ctrl", type: "control" }],
|
|
68
|
-
output: [{ id: "ctrl", type: "control" }],
|
|
69
|
-
};
|
|
70
|
-
case "FunctionCall":
|
|
71
|
-
return {
|
|
72
|
-
input: [
|
|
73
|
-
{ id: "ctrl", type: "control" },
|
|
74
|
-
{ id: "tool", type: "tool", label: "As Tool" },
|
|
75
|
-
],
|
|
76
|
-
output: [{ id: "ctrl", type: "control" }],
|
|
77
|
-
};
|
|
78
|
-
case "Agent":
|
|
79
|
-
return {
|
|
80
|
-
input: [
|
|
81
|
-
{ id: "ctrl", type: "control" },
|
|
82
|
-
{ id: "tool", type: "tool", label: "As Tool" },
|
|
83
|
-
],
|
|
84
|
-
output: [
|
|
85
|
-
{ id: "ctrl", type: "control" },
|
|
86
|
-
{ id: "tools", type: "tool" },
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
case "SetVariable":
|
|
90
|
-
return {
|
|
91
|
-
input: [{ id: "ctrl", type: "control" }],
|
|
92
|
-
output: [{ id: "ctrl", type: "control" }],
|
|
93
|
-
};
|
|
94
|
-
case "If":
|
|
95
|
-
return {
|
|
96
|
-
input: [{ id: "ctrl", type: "control" }],
|
|
97
|
-
output: [
|
|
98
|
-
{ id: "true", type: "control", label: "True" },
|
|
99
|
-
{ id: "false", type: "control", label: "False" },
|
|
100
|
-
],
|
|
101
|
-
};
|
|
102
|
-
case "Delay":
|
|
103
|
-
return {
|
|
104
|
-
input: [{ id: "ctrl", type: "control" }],
|
|
105
|
-
output: [{ id: "ctrl", type: "control" }],
|
|
106
|
-
};
|
|
107
|
-
case "OnFunctionCall":
|
|
108
|
-
case "Ticker":
|
|
109
|
-
case "Alarm":
|
|
110
|
-
case "OnStartup":
|
|
111
|
-
case "OnPinEdge":
|
|
112
|
-
case "OnSerialReceive":
|
|
113
|
-
case "OnThreshold":
|
|
114
|
-
case "OnMqttMessage":
|
|
115
|
-
return {
|
|
116
|
-
input: [],
|
|
117
|
-
output: [{ id: "ctrl", type: "control" }],
|
|
118
|
-
};
|
|
119
|
-
case "Retriever":
|
|
120
|
-
return {
|
|
121
|
-
input: [
|
|
122
|
-
{ id: "ctrl", type: "control" },
|
|
123
|
-
{ id: "tool", type: "tool", label: "As Tool" },
|
|
124
|
-
],
|
|
125
|
-
output: [{ id: "ctrl", type: "control" }],
|
|
126
|
-
};
|
|
127
|
-
case "WebFetch":
|
|
128
|
-
return {
|
|
129
|
-
input: [{ id: "ctrl", type: "control" }],
|
|
130
|
-
output: [{ id: "ctrl", type: "control" }],
|
|
131
|
-
};
|
|
132
|
-
case "WebSearchTool":
|
|
133
|
-
return { input: [{ id: "tool", type: "tool", label: "As Tool" }], output: [] };
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Get a flat arguments record for any node instance. Used to feed parameter
|
|
139
|
-
* resolution (FromArgs<T> lambdas, activation rules). Output bindings live in
|
|
140
|
-
* the same record under their own (non-colliding) output ids — lambdas address
|
|
141
|
-
* parameters by parameter id and never hit them accidentally.
|
|
142
|
-
*/
|
|
143
|
-
export function getArguments(node: NodeData): Record<string, unknown> {
|
|
144
|
-
return node.arguments as Record<string, unknown>;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Compute the outputs the node currently can produce (before binding decisions).
|
|
149
|
-
* For FunctionCall: derived from the per-instance functionInfo snapshot (decoupled from live function defs).
|
|
150
|
-
* For all others: derived from the registered NodeDefinition's outputs[] + the node's current arguments.
|
|
151
|
-
*
|
|
152
|
-
* List outputs: only emit-mode entries are surfaced. Assign-mode entries route to
|
|
153
|
-
* existing variables and don't create new output slots in scope — they're validated
|
|
154
|
-
* as bindings, not reported as available outputs.
|
|
155
|
-
*/
|
|
156
|
-
export function getNodeAvailableOutput(node: NodeData): Record<string, NodeOutput> {
|
|
157
|
-
const result: Record<string, NodeOutput> = {};
|
|
158
|
-
|
|
159
|
-
if (node.type === "FunctionCall") {
|
|
160
|
-
for (const ret of node.functionInfo.returns) {
|
|
161
|
-
result[ret.uid] = { name: ret.name, dataType: ret.dataType };
|
|
162
|
-
}
|
|
163
|
-
return result;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const def = NodeRegistry.getByType(node.type);
|
|
167
|
-
if (!def?.outputs) return result;
|
|
168
|
-
|
|
169
|
-
const args = node.arguments as Record<string, unknown>;
|
|
170
|
-
for (const out of def.outputs) {
|
|
171
|
-
if (out.type === "static") {
|
|
172
|
-
result[out.id] = {
|
|
173
|
-
name: out.id,
|
|
174
|
-
dataType: resolveStaticOutputDataType(out, node.arguments),
|
|
175
|
-
};
|
|
176
|
-
} else {
|
|
177
|
-
const entries = (args[out.id] as OutputDeclaration[] | undefined) ?? [];
|
|
178
|
-
for (const entry of entries) {
|
|
179
|
-
if (entry.mode === "emit") {
|
|
180
|
-
result[entry.uid] = { name: entry.name, dataType: entry.dataType };
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return result;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Compute the effective outputs the node emits to variable scope: available outputs
|
|
190
|
-
* filtered to active emit bindings only (inactive bindings and active assign bindings
|
|
191
|
-
* contribute nothing — assign routes to an existing variable, inactive is discarded).
|
|
192
|
-
* Emit bindings carry a user-chosen name that overrides the output's default name (the id).
|
|
193
|
-
*
|
|
194
|
-
* Binding lookup:
|
|
195
|
-
* - Static outputs (incl. FunctionCall returns): `node.arguments[out.id]`
|
|
196
|
-
* - List outputs: each entry is already a declaration; emit entries contribute
|
|
197
|
-
* directly (no separate binding), assign entries contribute nothing
|
|
198
|
-
*/
|
|
199
|
-
export function getNodeOutput(node: NodeData): Record<string, NodeOutput> {
|
|
200
|
-
const result: Record<string, NodeOutput> = {};
|
|
201
|
-
const args = node.arguments as Record<string, unknown>;
|
|
202
|
-
|
|
203
|
-
const applyStaticBinding = (key: string, defaultName: string, dataType: NodeOutput["dataType"], binding: OutputBinding | undefined) => {
|
|
204
|
-
// No binding = treat as default emit (the seeded shape). Otherwise honor active+mode.
|
|
205
|
-
if (!binding) {
|
|
206
|
-
result[key] = { name: defaultName, dataType };
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (!binding.active) return;
|
|
210
|
-
if (binding.mode !== "emit") return;
|
|
211
|
-
result[key] = { name: binding.name, dataType };
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
if (node.type === "FunctionCall") {
|
|
215
|
-
for (const ret of node.functionInfo.returns) {
|
|
216
|
-
applyStaticBinding(ret.uid, ret.name, ret.dataType, getStaticBinding(args, ret.uid));
|
|
217
|
-
}
|
|
218
|
-
return result;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const def = NodeRegistry.getByType(node.type);
|
|
222
|
-
if (!def?.outputs) return result;
|
|
223
|
-
|
|
224
|
-
for (const out of def.outputs) {
|
|
225
|
-
if (out.type === "static") {
|
|
226
|
-
applyStaticBinding(out.id, out.id, resolveStaticOutputDataType(out, node.arguments), getStaticBinding(args, out.id));
|
|
227
|
-
} else {
|
|
228
|
-
const entries = (args[out.id] as OutputDeclaration[] | undefined) ?? [];
|
|
229
|
-
for (const entry of entries) {
|
|
230
|
-
if (entry.mode === "emit") {
|
|
231
|
-
result[entry.uid] = { name: entry.name, dataType: entry.dataType };
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Compute external input requirements for a node instance (debug mode).
|
|
241
|
-
* Returns the hardware I/O values the node will read during execution.
|
|
242
|
-
*/
|
|
243
|
-
export function getInput(node: NodeData): ExternalInput[] {
|
|
244
|
-
switch (node.type) {
|
|
245
|
-
case "ReadPin":
|
|
246
|
-
return [
|
|
247
|
-
{ kind: "gpio", pinReference: node.arguments.pinReference, dataType: node.arguments.signalType === "digital" ? "bool" : "int" },
|
|
248
|
-
];
|
|
249
|
-
case "SerialRead":
|
|
250
|
-
return [{ kind: "serial" }];
|
|
251
|
-
case "OnPinEdge":
|
|
252
|
-
return [{ kind: "gpio", pinReference: node.arguments.pinReference, dataType: "bool" }];
|
|
253
|
-
case "OnSerialReceive":
|
|
254
|
-
return [{ kind: "serial" }];
|
|
255
|
-
default:
|
|
256
|
-
return [];
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Determine whether a node is currently used as a tool input
|
|
262
|
-
* (i.e. its tool-input port has an incoming edge).
|
|
263
|
-
*
|
|
264
|
-
* Reads only the connectivity fields off each edge (`target`, `targetHandle`),
|
|
265
|
-
* so it takes the structural {@link Edge} rather than React Flow's `Edge` —
|
|
266
|
-
* keeping this (and its callers in serialization/diagnostics) headless. The
|
|
267
|
-
* editor's React Flow `Edge[]` is structurally assignable without an adapter.
|
|
268
|
-
*
|
|
269
|
-
* Editor-only connection rules (canPortAcceptEdge, getCompatibleNodeDefs,
|
|
270
|
-
* isValidConnection) live in workflow-builder's connectionRules — they operate
|
|
271
|
-
* on React Flow `Node`/`Edge` and have no place in the headless core.
|
|
272
|
-
*/
|
|
273
|
-
export function isNodeUsedAsTool(nodeId: string, nodeData: NodeData, edges: readonly Edge[]): boolean {
|
|
274
|
-
const ports = getPorts(nodeData);
|
|
275
|
-
const toolInputs = ports.input.filter((p) => p.type === "tool");
|
|
276
|
-
if (toolInputs.length === 0) return false;
|
|
277
|
-
return edges.some((e) => e.target === nodeId && toolInputs.some((p) => p.id === e.targetHandle));
|
|
278
|
-
}
|
|
1
|
+
import { NodeData, NodeOutput } from "./Node";
|
|
2
|
+
import type { OutputBinding, OutputDeclaration } from "../parameter";
|
|
3
|
+
import { PortDefinitions } from "./NodeDefinition";
|
|
4
|
+
import { resolveStaticOutputDataType } from "../parameter";
|
|
5
|
+
import { NodeRegistry } from "./NodeRegistry";
|
|
6
|
+
import { Edge } from "../edge";
|
|
7
|
+
|
|
8
|
+
/** Read a static output's binding from a node's arguments bag, keyed by the output id. */
|
|
9
|
+
function getStaticBinding(args: Record<string, unknown>, outputId: string): OutputBinding | undefined {
|
|
10
|
+
return args[outputId] as OutputBinding | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read-only helper: look up the current binding for a given output key on any node.
|
|
15
|
+
* All static-style bindings (including FunctionCall returns) live at `arguments[outputId]`.
|
|
16
|
+
* List output entries are OutputDeclarations, projected to an OutputBinding shape — only
|
|
17
|
+
* emit-mode entries are addressable by uid here; callers that need to inspect assign-mode
|
|
18
|
+
* entries should walk the declaration list directly via `arguments[out.id]`.
|
|
19
|
+
*/
|
|
20
|
+
export function getOutputBinding(node: NodeData, outputId: string): OutputBinding | undefined {
|
|
21
|
+
const args = node.arguments as Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
if (node.type === "FunctionCall") {
|
|
24
|
+
return getStaticBinding(args, outputId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const def = NodeRegistry.getByType(node.type);
|
|
28
|
+
if (!def?.outputs) return undefined;
|
|
29
|
+
|
|
30
|
+
for (const out of def.outputs) {
|
|
31
|
+
if (out.type === "static") {
|
|
32
|
+
if (out.id === outputId) return getStaticBinding(args, out.id);
|
|
33
|
+
} else {
|
|
34
|
+
const entries = args[out.id] as OutputDeclaration[] | undefined;
|
|
35
|
+
const entry = entries?.find((e) => e.mode === "emit" && e.uid === outputId);
|
|
36
|
+
if (entry && entry.mode === "emit") return { active: true, mode: "emit", name: entry.name };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// External input requirements for debug mode
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** Describes a hardware input a node needs before it can execute in debug mode. */
|
|
47
|
+
export type ExternalInput = { kind: "gpio"; pinReference: string | undefined; dataType: "bool" | "int" } | { kind: "serial" };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get ports for a node instance.
|
|
51
|
+
*/
|
|
52
|
+
export function getPorts(node: NodeData): PortDefinitions {
|
|
53
|
+
switch (node.type) {
|
|
54
|
+
case "ReadPin":
|
|
55
|
+
return {
|
|
56
|
+
input: [
|
|
57
|
+
{ id: "ctrl", type: "control" },
|
|
58
|
+
{ id: "tool", type: "tool", label: "As Tool" },
|
|
59
|
+
],
|
|
60
|
+
output: [{ id: "ctrl", type: "control" }],
|
|
61
|
+
};
|
|
62
|
+
case "SerialRead":
|
|
63
|
+
case "WritePin":
|
|
64
|
+
case "SerialWrite":
|
|
65
|
+
case "MqttPublish":
|
|
66
|
+
return {
|
|
67
|
+
input: [{ id: "ctrl", type: "control" }],
|
|
68
|
+
output: [{ id: "ctrl", type: "control" }],
|
|
69
|
+
};
|
|
70
|
+
case "FunctionCall":
|
|
71
|
+
return {
|
|
72
|
+
input: [
|
|
73
|
+
{ id: "ctrl", type: "control" },
|
|
74
|
+
{ id: "tool", type: "tool", label: "As Tool" },
|
|
75
|
+
],
|
|
76
|
+
output: [{ id: "ctrl", type: "control" }],
|
|
77
|
+
};
|
|
78
|
+
case "Agent":
|
|
79
|
+
return {
|
|
80
|
+
input: [
|
|
81
|
+
{ id: "ctrl", type: "control" },
|
|
82
|
+
{ id: "tool", type: "tool", label: "As Tool" },
|
|
83
|
+
],
|
|
84
|
+
output: [
|
|
85
|
+
{ id: "ctrl", type: "control" },
|
|
86
|
+
{ id: "tools", type: "tool" },
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
case "SetVariable":
|
|
90
|
+
return {
|
|
91
|
+
input: [{ id: "ctrl", type: "control" }],
|
|
92
|
+
output: [{ id: "ctrl", type: "control" }],
|
|
93
|
+
};
|
|
94
|
+
case "If":
|
|
95
|
+
return {
|
|
96
|
+
input: [{ id: "ctrl", type: "control" }],
|
|
97
|
+
output: [
|
|
98
|
+
{ id: "true", type: "control", label: "True" },
|
|
99
|
+
{ id: "false", type: "control", label: "False" },
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
case "Delay":
|
|
103
|
+
return {
|
|
104
|
+
input: [{ id: "ctrl", type: "control" }],
|
|
105
|
+
output: [{ id: "ctrl", type: "control" }],
|
|
106
|
+
};
|
|
107
|
+
case "OnFunctionCall":
|
|
108
|
+
case "Ticker":
|
|
109
|
+
case "Alarm":
|
|
110
|
+
case "OnStartup":
|
|
111
|
+
case "OnPinEdge":
|
|
112
|
+
case "OnSerialReceive":
|
|
113
|
+
case "OnThreshold":
|
|
114
|
+
case "OnMqttMessage":
|
|
115
|
+
return {
|
|
116
|
+
input: [],
|
|
117
|
+
output: [{ id: "ctrl", type: "control" }],
|
|
118
|
+
};
|
|
119
|
+
case "Retriever":
|
|
120
|
+
return {
|
|
121
|
+
input: [
|
|
122
|
+
{ id: "ctrl", type: "control" },
|
|
123
|
+
{ id: "tool", type: "tool", label: "As Tool" },
|
|
124
|
+
],
|
|
125
|
+
output: [{ id: "ctrl", type: "control" }],
|
|
126
|
+
};
|
|
127
|
+
case "WebFetch":
|
|
128
|
+
return {
|
|
129
|
+
input: [{ id: "ctrl", type: "control" }],
|
|
130
|
+
output: [{ id: "ctrl", type: "control" }],
|
|
131
|
+
};
|
|
132
|
+
case "WebSearchTool":
|
|
133
|
+
return { input: [{ id: "tool", type: "tool", label: "As Tool" }], output: [] };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get a flat arguments record for any node instance. Used to feed parameter
|
|
139
|
+
* resolution (FromArgs<T> lambdas, activation rules). Output bindings live in
|
|
140
|
+
* the same record under their own (non-colliding) output ids — lambdas address
|
|
141
|
+
* parameters by parameter id and never hit them accidentally.
|
|
142
|
+
*/
|
|
143
|
+
export function getArguments(node: NodeData): Record<string, unknown> {
|
|
144
|
+
return node.arguments as Record<string, unknown>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Compute the outputs the node currently can produce (before binding decisions).
|
|
149
|
+
* For FunctionCall: derived from the per-instance functionInfo snapshot (decoupled from live function defs).
|
|
150
|
+
* For all others: derived from the registered NodeDefinition's outputs[] + the node's current arguments.
|
|
151
|
+
*
|
|
152
|
+
* List outputs: only emit-mode entries are surfaced. Assign-mode entries route to
|
|
153
|
+
* existing variables and don't create new output slots in scope — they're validated
|
|
154
|
+
* as bindings, not reported as available outputs.
|
|
155
|
+
*/
|
|
156
|
+
export function getNodeAvailableOutput(node: NodeData): Record<string, NodeOutput> {
|
|
157
|
+
const result: Record<string, NodeOutput> = {};
|
|
158
|
+
|
|
159
|
+
if (node.type === "FunctionCall") {
|
|
160
|
+
for (const ret of node.functionInfo.returns) {
|
|
161
|
+
result[ret.uid] = { name: ret.name, dataType: ret.dataType };
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const def = NodeRegistry.getByType(node.type);
|
|
167
|
+
if (!def?.outputs) return result;
|
|
168
|
+
|
|
169
|
+
const args = node.arguments as Record<string, unknown>;
|
|
170
|
+
for (const out of def.outputs) {
|
|
171
|
+
if (out.type === "static") {
|
|
172
|
+
result[out.id] = {
|
|
173
|
+
name: out.id,
|
|
174
|
+
dataType: resolveStaticOutputDataType(out, node.arguments),
|
|
175
|
+
};
|
|
176
|
+
} else {
|
|
177
|
+
const entries = (args[out.id] as OutputDeclaration[] | undefined) ?? [];
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (entry.mode === "emit") {
|
|
180
|
+
result[entry.uid] = { name: entry.name, dataType: entry.dataType };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Compute the effective outputs the node emits to variable scope: available outputs
|
|
190
|
+
* filtered to active emit bindings only (inactive bindings and active assign bindings
|
|
191
|
+
* contribute nothing — assign routes to an existing variable, inactive is discarded).
|
|
192
|
+
* Emit bindings carry a user-chosen name that overrides the output's default name (the id).
|
|
193
|
+
*
|
|
194
|
+
* Binding lookup:
|
|
195
|
+
* - Static outputs (incl. FunctionCall returns): `node.arguments[out.id]`
|
|
196
|
+
* - List outputs: each entry is already a declaration; emit entries contribute
|
|
197
|
+
* directly (no separate binding), assign entries contribute nothing
|
|
198
|
+
*/
|
|
199
|
+
export function getNodeOutput(node: NodeData): Record<string, NodeOutput> {
|
|
200
|
+
const result: Record<string, NodeOutput> = {};
|
|
201
|
+
const args = node.arguments as Record<string, unknown>;
|
|
202
|
+
|
|
203
|
+
const applyStaticBinding = (key: string, defaultName: string, dataType: NodeOutput["dataType"], binding: OutputBinding | undefined) => {
|
|
204
|
+
// No binding = treat as default emit (the seeded shape). Otherwise honor active+mode.
|
|
205
|
+
if (!binding) {
|
|
206
|
+
result[key] = { name: defaultName, dataType };
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!binding.active) return;
|
|
210
|
+
if (binding.mode !== "emit") return;
|
|
211
|
+
result[key] = { name: binding.name, dataType };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (node.type === "FunctionCall") {
|
|
215
|
+
for (const ret of node.functionInfo.returns) {
|
|
216
|
+
applyStaticBinding(ret.uid, ret.name, ret.dataType, getStaticBinding(args, ret.uid));
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const def = NodeRegistry.getByType(node.type);
|
|
222
|
+
if (!def?.outputs) return result;
|
|
223
|
+
|
|
224
|
+
for (const out of def.outputs) {
|
|
225
|
+
if (out.type === "static") {
|
|
226
|
+
applyStaticBinding(out.id, out.id, resolveStaticOutputDataType(out, node.arguments), getStaticBinding(args, out.id));
|
|
227
|
+
} else {
|
|
228
|
+
const entries = (args[out.id] as OutputDeclaration[] | undefined) ?? [];
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
if (entry.mode === "emit") {
|
|
231
|
+
result[entry.uid] = { name: entry.name, dataType: entry.dataType };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Compute external input requirements for a node instance (debug mode).
|
|
241
|
+
* Returns the hardware I/O values the node will read during execution.
|
|
242
|
+
*/
|
|
243
|
+
export function getInput(node: NodeData): ExternalInput[] {
|
|
244
|
+
switch (node.type) {
|
|
245
|
+
case "ReadPin":
|
|
246
|
+
return [
|
|
247
|
+
{ kind: "gpio", pinReference: node.arguments.pinReference, dataType: node.arguments.signalType === "digital" ? "bool" : "int" },
|
|
248
|
+
];
|
|
249
|
+
case "SerialRead":
|
|
250
|
+
return [{ kind: "serial" }];
|
|
251
|
+
case "OnPinEdge":
|
|
252
|
+
return [{ kind: "gpio", pinReference: node.arguments.pinReference, dataType: "bool" }];
|
|
253
|
+
case "OnSerialReceive":
|
|
254
|
+
return [{ kind: "serial" }];
|
|
255
|
+
default:
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Determine whether a node is currently used as a tool input
|
|
262
|
+
* (i.e. its tool-input port has an incoming edge).
|
|
263
|
+
*
|
|
264
|
+
* Reads only the connectivity fields off each edge (`target`, `targetHandle`),
|
|
265
|
+
* so it takes the structural {@link Edge} rather than React Flow's `Edge` —
|
|
266
|
+
* keeping this (and its callers in serialization/diagnostics) headless. The
|
|
267
|
+
* editor's React Flow `Edge[]` is structurally assignable without an adapter.
|
|
268
|
+
*
|
|
269
|
+
* Editor-only connection rules (canPortAcceptEdge, getCompatibleNodeDefs,
|
|
270
|
+
* isValidConnection) live in workflow-builder's connectionRules — they operate
|
|
271
|
+
* on React Flow `Node`/`Edge` and have no place in the headless core.
|
|
272
|
+
*/
|
|
273
|
+
export function isNodeUsedAsTool(nodeId: string, nodeData: NodeData, edges: readonly Edge[]): boolean {
|
|
274
|
+
const ports = getPorts(nodeData);
|
|
275
|
+
const toolInputs = ports.input.filter((p) => p.type === "tool");
|
|
276
|
+
if (toolInputs.length === 0) return false;
|
|
277
|
+
return edges.some((e) => e.target === nodeId && toolInputs.some((p) => p.id === e.targetHandle));
|
|
278
|
+
}
|