@foresthubai/workflow-core 0.4.3 → 0.4.5

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 (298) hide show
  1. package/LICENSE +202 -202
  2. package/NOTICE +14 -14
  3. package/README.md +63 -63
  4. package/dist/api/deployment.d.ts +74 -0
  5. package/dist/api/deployment.d.ts.map +1 -0
  6. package/dist/api/deployment.js +6 -0
  7. package/dist/api/deployment.js.map +1 -0
  8. package/dist/api/engine.d.ts +744 -0
  9. package/dist/api/engine.d.ts.map +1 -0
  10. package/dist/api/engine.js +6 -0
  11. package/dist/api/engine.js.map +1 -0
  12. package/dist/api/index.d.ts +4 -0
  13. package/dist/api/index.d.ts.map +1 -1
  14. package/dist/api/index.js +2 -0
  15. package/dist/api/index.js.map +1 -1
  16. package/dist/api/workflow.d.ts +25 -9
  17. package/dist/api/workflow.d.ts.map +1 -1
  18. package/dist/channel/Channel.d.ts +1 -1
  19. package/dist/channel/Channel.d.ts.map +1 -1
  20. package/dist/channel/Channel.js +3 -1
  21. package/dist/channel/Channel.js.map +1 -1
  22. package/dist/channel/ChannelDefinition.d.ts.map +1 -1
  23. package/dist/channel/ChannelDefinition.js +24 -0
  24. package/dist/channel/ChannelDefinition.js.map +1 -1
  25. package/dist/channel/index.d.ts.map +1 -1
  26. package/dist/channel/index.js +2 -0
  27. package/dist/channel/index.js.map +1 -1
  28. package/dist/channel/serialization.d.ts.map +1 -1
  29. package/dist/channel/serialization.js +8 -0
  30. package/dist/channel/serialization.js.map +1 -1
  31. package/dist/deploy/index.d.ts +5 -1
  32. package/dist/deploy/index.d.ts.map +1 -1
  33. package/dist/deploy/index.js +4 -1
  34. package/dist/deploy/index.js.map +1 -1
  35. package/dist/deploy/inputs.d.ts +26 -0
  36. package/dist/deploy/inputs.d.ts.map +1 -0
  37. package/dist/deploy/inputs.js +4 -0
  38. package/dist/deploy/inputs.js.map +1 -0
  39. package/dist/deploy/requirements.d.ts +24 -0
  40. package/dist/deploy/requirements.d.ts.map +1 -1
  41. package/dist/deploy/requirements.js +63 -0
  42. package/dist/deploy/requirements.js.map +1 -1
  43. package/dist/deploy/spec.d.ts +27 -0
  44. package/dist/deploy/spec.d.ts.map +1 -0
  45. package/dist/deploy/spec.js +352 -0
  46. package/dist/deploy/spec.js.map +1 -0
  47. package/dist/diagnostics/__fixtures__/diagnosticFixtures.d.ts.map +1 -1
  48. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js +2 -0
  49. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js.map +1 -1
  50. package/dist/diagnostics/diagnostics.d.ts.map +1 -1
  51. package/dist/diagnostics/diagnostics.js +2 -0
  52. package/dist/diagnostics/diagnostics.js.map +1 -1
  53. package/dist/diagnostics/index.d.ts.map +1 -1
  54. package/dist/diagnostics/index.js +2 -0
  55. package/dist/diagnostics/index.js.map +1 -1
  56. package/dist/edge/Edge.d.ts.map +1 -1
  57. package/dist/edge/Edge.js +2 -0
  58. package/dist/edge/Edge.js.map +1 -1
  59. package/dist/edge/EdgeDefinition.d.ts.map +1 -1
  60. package/dist/edge/EdgeDefinition.js +2 -0
  61. package/dist/edge/EdgeDefinition.js.map +1 -1
  62. package/dist/edge/EdgeType.d.ts.map +1 -1
  63. package/dist/edge/EdgeType.js +2 -0
  64. package/dist/edge/EdgeType.js.map +1 -1
  65. package/dist/edge/index.d.ts.map +1 -1
  66. package/dist/edge/index.js +2 -2
  67. package/dist/edge/index.js.map +1 -1
  68. package/dist/edge/serialization.d.ts.map +1 -1
  69. package/dist/edge/serialization.js +2 -0
  70. package/dist/edge/serialization.js.map +1 -1
  71. package/dist/expression/index.d.ts.map +1 -1
  72. package/dist/expression/index.js +2 -0
  73. package/dist/expression/index.js.map +1 -1
  74. package/dist/expression/parser.d.ts.map +1 -1
  75. package/dist/expression/parser.js +2 -0
  76. package/dist/expression/parser.js.map +1 -1
  77. package/dist/expression/types.d.ts.map +1 -1
  78. package/dist/expression/types.js +2 -0
  79. package/dist/expression/types.js.map +1 -1
  80. package/dist/function/FunctionDeclaration.d.ts.map +1 -1
  81. package/dist/function/FunctionDeclaration.js +2 -0
  82. package/dist/function/FunctionDeclaration.js.map +1 -1
  83. package/dist/function/index.d.ts.map +1 -1
  84. package/dist/function/index.js +2 -0
  85. package/dist/function/index.js.map +1 -1
  86. package/dist/function/serialization.d.ts.map +1 -1
  87. package/dist/function/serialization.js +2 -0
  88. package/dist/function/serialization.js.map +1 -1
  89. package/dist/id/index.d.ts.map +1 -1
  90. package/dist/id/index.js +2 -0
  91. package/dist/id/index.js.map +1 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +2 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/memory/Memory.d.ts.map +1 -1
  96. package/dist/memory/Memory.js +2 -0
  97. package/dist/memory/Memory.js.map +1 -1
  98. package/dist/memory/MemoryDefinition.d.ts.map +1 -1
  99. package/dist/memory/MemoryDefinition.js +2 -0
  100. package/dist/memory/MemoryDefinition.js.map +1 -1
  101. package/dist/memory/MemoryFileDefinition.d.ts.map +1 -1
  102. package/dist/memory/MemoryFileDefinition.js +2 -0
  103. package/dist/memory/MemoryFileDefinition.js.map +1 -1
  104. package/dist/memory/MemoryRegistry.d.ts.map +1 -1
  105. package/dist/memory/MemoryRegistry.js +2 -0
  106. package/dist/memory/MemoryRegistry.js.map +1 -1
  107. package/dist/memory/VectorDatabaseDefinition.d.ts.map +1 -1
  108. package/dist/memory/VectorDatabaseDefinition.js +2 -0
  109. package/dist/memory/VectorDatabaseDefinition.js.map +1 -1
  110. package/dist/memory/index.d.ts.map +1 -1
  111. package/dist/memory/index.js +2 -0
  112. package/dist/memory/index.js.map +1 -1
  113. package/dist/memory/serialization.d.ts.map +1 -1
  114. package/dist/memory/serialization.js +2 -0
  115. package/dist/memory/serialization.js.map +1 -1
  116. package/dist/migration/index.d.ts.map +1 -1
  117. package/dist/migration/index.js +2 -0
  118. package/dist/migration/index.js.map +1 -1
  119. package/dist/migration/migrate.d.ts.map +1 -1
  120. package/dist/migration/migrate.js +2 -0
  121. package/dist/migration/migrate.js.map +1 -1
  122. package/dist/migration/migrations.d.ts.map +1 -1
  123. package/dist/migration/migrations.js +2 -0
  124. package/dist/migration/migrations.js.map +1 -1
  125. package/dist/migration/version.d.ts.map +1 -1
  126. package/dist/migration/version.js +2 -0
  127. package/dist/migration/version.js.map +1 -1
  128. package/dist/model/LLMModelDefinition.d.ts.map +1 -1
  129. package/dist/model/LLMModelDefinition.js +2 -0
  130. package/dist/model/LLMModelDefinition.js.map +1 -1
  131. package/dist/model/Model.d.ts.map +1 -1
  132. package/dist/model/Model.js +2 -13
  133. package/dist/model/Model.js.map +1 -1
  134. package/dist/model/ModelDefinition.d.ts.map +1 -1
  135. package/dist/model/ModelDefinition.js +2 -0
  136. package/dist/model/ModelDefinition.js.map +1 -1
  137. package/dist/model/ModelRegistry.d.ts.map +1 -1
  138. package/dist/model/ModelRegistry.js +2 -0
  139. package/dist/model/ModelRegistry.js.map +1 -1
  140. package/dist/model/index.d.ts.map +1 -1
  141. package/dist/model/index.js +2 -0
  142. package/dist/model/index.js.map +1 -1
  143. package/dist/model/serialization.d.ts.map +1 -1
  144. package/dist/model/serialization.js +2 -0
  145. package/dist/model/serialization.js.map +1 -1
  146. package/dist/node/AgentNode.d.ts.map +1 -1
  147. package/dist/node/AgentNode.js +2 -0
  148. package/dist/node/AgentNode.js.map +1 -1
  149. package/dist/node/DataNode.d.ts.map +1 -1
  150. package/dist/node/DataNode.js +2 -0
  151. package/dist/node/DataNode.js.map +1 -1
  152. package/dist/node/FunctionNode.d.ts.map +1 -1
  153. package/dist/node/FunctionNode.js +2 -0
  154. package/dist/node/FunctionNode.js.map +1 -1
  155. package/dist/node/InputNode.d.ts.map +1 -1
  156. package/dist/node/InputNode.js +2 -0
  157. package/dist/node/InputNode.js.map +1 -1
  158. package/dist/node/LogicNode.d.ts.map +1 -1
  159. package/dist/node/LogicNode.js +2 -0
  160. package/dist/node/LogicNode.js.map +1 -1
  161. package/dist/node/MqttNode.d.ts.map +1 -1
  162. package/dist/node/MqttNode.js +2 -0
  163. package/dist/node/MqttNode.js.map +1 -1
  164. package/dist/node/Node.d.ts.map +1 -1
  165. package/dist/node/Node.js +2 -7
  166. package/dist/node/Node.js.map +1 -1
  167. package/dist/node/NodeDefinition.d.ts.map +1 -1
  168. package/dist/node/NodeDefinition.js +2 -0
  169. package/dist/node/NodeDefinition.js.map +1 -1
  170. package/dist/node/NodeRegistry.d.ts.map +1 -1
  171. package/dist/node/NodeRegistry.js +2 -0
  172. package/dist/node/NodeRegistry.js.map +1 -1
  173. package/dist/node/OutputNode.d.ts.map +1 -1
  174. package/dist/node/OutputNode.js +4 -2
  175. package/dist/node/OutputNode.js.map +1 -1
  176. package/dist/node/ToolNode.d.ts.map +1 -1
  177. package/dist/node/ToolNode.js +2 -0
  178. package/dist/node/ToolNode.js.map +1 -1
  179. package/dist/node/TriggerNode.d.ts.map +1 -1
  180. package/dist/node/TriggerNode.js +2 -0
  181. package/dist/node/TriggerNode.js.map +1 -1
  182. package/dist/node/constants.d.ts.map +1 -1
  183. package/dist/node/constants.js +2 -0
  184. package/dist/node/constants.js.map +1 -1
  185. package/dist/node/index.d.ts.map +1 -1
  186. package/dist/node/index.js +2 -0
  187. package/dist/node/index.js.map +1 -1
  188. package/dist/node/methods.d.ts.map +1 -1
  189. package/dist/node/methods.js +2 -0
  190. package/dist/node/methods.js.map +1 -1
  191. package/dist/node/serialization.d.ts.map +1 -1
  192. package/dist/node/serialization.js +33 -13
  193. package/dist/node/serialization.js.map +1 -1
  194. package/dist/parameter/OutputParameter.d.ts.map +1 -1
  195. package/dist/parameter/OutputParameter.js +2 -0
  196. package/dist/parameter/OutputParameter.js.map +1 -1
  197. package/dist/parameter/Parameter.d.ts.map +1 -1
  198. package/dist/parameter/Parameter.js +2 -0
  199. package/dist/parameter/Parameter.js.map +1 -1
  200. package/dist/parameter/index.d.ts.map +1 -1
  201. package/dist/parameter/index.js +2 -0
  202. package/dist/parameter/index.js.map +1 -1
  203. package/dist/variable/Variable.d.ts.map +1 -1
  204. package/dist/variable/Variable.js +2 -0
  205. package/dist/variable/Variable.js.map +1 -1
  206. package/dist/variable/index.d.ts.map +1 -1
  207. package/dist/variable/index.js +2 -3
  208. package/dist/variable/index.js.map +1 -1
  209. package/dist/variable/operations.d.ts.map +1 -1
  210. package/dist/variable/operations.js +2 -0
  211. package/dist/variable/operations.js.map +1 -1
  212. package/dist/workflow/Workflow.d.ts.map +1 -1
  213. package/dist/workflow/Workflow.js +2 -0
  214. package/dist/workflow/Workflow.js.map +1 -1
  215. package/dist/workflow/index.d.ts.map +1 -1
  216. package/dist/workflow/index.js +2 -0
  217. package/dist/workflow/index.js.map +1 -1
  218. package/dist/workflow/serialization.d.ts.map +1 -1
  219. package/dist/workflow/serialization.js +2 -4
  220. package/dist/workflow/serialization.js.map +1 -1
  221. package/package.json +2 -2
  222. package/src/api/deployment.ts +74 -0
  223. package/src/api/engine.ts +744 -0
  224. package/src/api/index.ts +29 -11
  225. package/src/api/workflow.ts +25 -9
  226. package/src/channel/Channel.ts +14 -11
  227. package/src/channel/ChannelDefinition.ts +101 -76
  228. package/src/channel/index.ts +9 -6
  229. package/src/channel/serialization.ts +77 -68
  230. package/src/deploy/index.ts +17 -1
  231. package/src/deploy/inputs.ts +44 -0
  232. package/src/deploy/requirements.test.ts +64 -61
  233. package/src/deploy/requirements.ts +159 -41
  234. package/src/deploy/spec.test.ts +227 -0
  235. package/src/deploy/spec.ts +397 -0
  236. package/src/diagnostics/__fixtures__/diagnosticFixtures.ts +161 -158
  237. package/src/diagnostics/diagnostics.test.ts +881 -878
  238. package/src/diagnostics/diagnostics.ts +939 -936
  239. package/src/diagnostics/index.ts +14 -11
  240. package/src/edge/Edge.ts +26 -23
  241. package/src/edge/EdgeDefinition.ts +48 -45
  242. package/src/edge/EdgeType.ts +22 -19
  243. package/src/edge/index.ts +11 -8
  244. package/src/edge/serialization.ts +86 -83
  245. package/src/expression/index.ts +7 -4
  246. package/src/expression/parser.ts +365 -362
  247. package/src/expression/types.ts +33 -30
  248. package/src/function/FunctionDeclaration.ts +57 -54
  249. package/src/function/index.ts +6 -3
  250. package/src/function/serialization.ts +43 -40
  251. package/src/globals.d.ts +12 -9
  252. package/src/id/index.ts +11 -8
  253. package/src/index.ts +25 -22
  254. package/src/memory/Memory.ts +18 -15
  255. package/src/memory/MemoryDefinition.ts +19 -16
  256. package/src/memory/MemoryFileDefinition.ts +40 -37
  257. package/src/memory/MemoryRegistry.ts +38 -35
  258. package/src/memory/VectorDatabaseDefinition.ts +24 -21
  259. package/src/memory/index.ts +11 -8
  260. package/src/memory/serialization.ts +50 -47
  261. package/src/migration/index.ts +7 -4
  262. package/src/migration/migrate.test.ts +47 -44
  263. package/src/migration/migrate.ts +61 -58
  264. package/src/migration/migrations.ts +27 -24
  265. package/src/migration/version.ts +12 -9
  266. package/src/model/LLMModelDefinition.ts +15 -12
  267. package/src/model/Model.ts +42 -39
  268. package/src/model/ModelDefinition.ts +18 -15
  269. package/src/model/ModelRegistry.ts +36 -33
  270. package/src/model/index.ts +10 -7
  271. package/src/model/serialization.ts +33 -30
  272. package/src/node/AgentNode.ts +85 -82
  273. package/src/node/DataNode.ts +44 -41
  274. package/src/node/FunctionNode.ts +79 -76
  275. package/src/node/InputNode.ts +188 -185
  276. package/src/node/LogicNode.ts +36 -33
  277. package/src/node/MqttNode.ts +130 -127
  278. package/src/node/Node.ts +64 -61
  279. package/src/node/NodeDefinition.ts +40 -37
  280. package/src/node/NodeRegistry.ts +88 -85
  281. package/src/node/OutputNode.ts +90 -87
  282. package/src/node/ToolNode.ts +35 -32
  283. package/src/node/TriggerNode.ts +275 -272
  284. package/src/node/constants.ts +19 -16
  285. package/src/node/index.ts +29 -26
  286. package/src/node/methods.ts +281 -278
  287. package/src/node/serialization.test.ts +51 -0
  288. package/src/node/serialization.ts +565 -544
  289. package/src/parameter/OutputParameter.ts +71 -68
  290. package/src/parameter/Parameter.ts +246 -243
  291. package/src/parameter/index.ts +36 -33
  292. package/src/variable/Variable.ts +13 -10
  293. package/src/variable/index.ts +19 -16
  294. package/src/variable/operations.ts +109 -106
  295. package/src/workflow/Workflow.ts +44 -41
  296. package/src/workflow/index.ts +6 -3
  297. package/src/workflow/serialization.test.ts +243 -240
  298. package/src/workflow/serialization.ts +245 -242
@@ -1,278 +1,281 @@
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
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 ForestHub.
3
+
4
+ import { NodeData, NodeOutput } from "./Node";
5
+ import type { OutputBinding, OutputDeclaration } from "../parameter";
6
+ import { PortDefinitions } from "./NodeDefinition";
7
+ import { resolveStaticOutputDataType } from "../parameter";
8
+ import { NodeRegistry } from "./NodeRegistry";
9
+ import { Edge } from "../edge";
10
+
11
+ /** Read a static output's binding from a node's arguments bag, keyed by the output id. */
12
+ function getStaticBinding(args: Record<string, unknown>, outputId: string): OutputBinding | undefined {
13
+ return args[outputId] as OutputBinding | undefined;
14
+ }
15
+
16
+ /**
17
+ * Read-only helper: look up the current binding for a given output key on any node.
18
+ * All static-style bindings (including FunctionCall returns) live at `arguments[outputId]`.
19
+ * List output entries are OutputDeclarations, projected to an OutputBinding shape — only
20
+ * emit-mode entries are addressable by uid here; callers that need to inspect assign-mode
21
+ * entries should walk the declaration list directly via `arguments[out.id]`.
22
+ */
23
+ export function getOutputBinding(node: NodeData, outputId: string): OutputBinding | undefined {
24
+ const args = node.arguments as Record<string, unknown>;
25
+
26
+ if (node.type === "FunctionCall") {
27
+ return getStaticBinding(args, outputId);
28
+ }
29
+
30
+ const def = NodeRegistry.getByType(node.type);
31
+ if (!def?.outputs) return undefined;
32
+
33
+ for (const out of def.outputs) {
34
+ if (out.type === "static") {
35
+ if (out.id === outputId) return getStaticBinding(args, out.id);
36
+ } else {
37
+ const entries = args[out.id] as OutputDeclaration[] | undefined;
38
+ const entry = entries?.find((e) => e.mode === "emit" && e.uid === outputId);
39
+ if (entry && entry.mode === "emit") return { active: true, mode: "emit", name: entry.name };
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // External input requirements for debug mode
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /** Describes a hardware input a node needs before it can execute in debug mode. */
50
+ export type ExternalInput = { kind: "gpio"; pinReference: string | undefined; dataType: "bool" | "int" } | { kind: "serial" };
51
+
52
+ /**
53
+ * Get ports for a node instance.
54
+ */
55
+ export function getPorts(node: NodeData): PortDefinitions {
56
+ switch (node.type) {
57
+ case "ReadPin":
58
+ return {
59
+ input: [
60
+ { id: "ctrl", type: "control" },
61
+ { id: "tool", type: "tool", label: "As Tool" },
62
+ ],
63
+ output: [{ id: "ctrl", type: "control" }],
64
+ };
65
+ case "SerialRead":
66
+ case "WritePin":
67
+ case "SerialWrite":
68
+ case "MqttPublish":
69
+ return {
70
+ input: [{ id: "ctrl", type: "control" }],
71
+ output: [{ id: "ctrl", type: "control" }],
72
+ };
73
+ case "FunctionCall":
74
+ return {
75
+ input: [
76
+ { id: "ctrl", type: "control" },
77
+ { id: "tool", type: "tool", label: "As Tool" },
78
+ ],
79
+ output: [{ id: "ctrl", type: "control" }],
80
+ };
81
+ case "Agent":
82
+ return {
83
+ input: [
84
+ { id: "ctrl", type: "control" },
85
+ { id: "tool", type: "tool", label: "As Tool" },
86
+ ],
87
+ output: [
88
+ { id: "ctrl", type: "control" },
89
+ { id: "tools", type: "tool" },
90
+ ],
91
+ };
92
+ case "SetVariable":
93
+ return {
94
+ input: [{ id: "ctrl", type: "control" }],
95
+ output: [{ id: "ctrl", type: "control" }],
96
+ };
97
+ case "If":
98
+ return {
99
+ input: [{ id: "ctrl", type: "control" }],
100
+ output: [
101
+ { id: "true", type: "control", label: "True" },
102
+ { id: "false", type: "control", label: "False" },
103
+ ],
104
+ };
105
+ case "Delay":
106
+ return {
107
+ input: [{ id: "ctrl", type: "control" }],
108
+ output: [{ id: "ctrl", type: "control" }],
109
+ };
110
+ case "OnFunctionCall":
111
+ case "Ticker":
112
+ case "Alarm":
113
+ case "OnStartup":
114
+ case "OnPinEdge":
115
+ case "OnSerialReceive":
116
+ case "OnThreshold":
117
+ case "OnMqttMessage":
118
+ return {
119
+ input: [],
120
+ output: [{ id: "ctrl", type: "control" }],
121
+ };
122
+ case "Retriever":
123
+ return {
124
+ input: [
125
+ { id: "ctrl", type: "control" },
126
+ { id: "tool", type: "tool", label: "As Tool" },
127
+ ],
128
+ output: [{ id: "ctrl", type: "control" }],
129
+ };
130
+ case "WebFetch":
131
+ return {
132
+ input: [{ id: "ctrl", type: "control" }],
133
+ output: [{ id: "ctrl", type: "control" }],
134
+ };
135
+ case "WebSearchTool":
136
+ return { input: [{ id: "tool", type: "tool", label: "As Tool" }], output: [] };
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get a flat arguments record for any node instance. Used to feed parameter
142
+ * resolution (FromArgs<T> lambdas, activation rules). Output bindings live in
143
+ * the same record under their own (non-colliding) output ids — lambdas address
144
+ * parameters by parameter id and never hit them accidentally.
145
+ */
146
+ export function getArguments(node: NodeData): Record<string, unknown> {
147
+ return node.arguments as Record<string, unknown>;
148
+ }
149
+
150
+ /**
151
+ * Compute the outputs the node currently can produce (before binding decisions).
152
+ * For FunctionCall: derived from the per-instance functionInfo snapshot (decoupled from live function defs).
153
+ * For all others: derived from the registered NodeDefinition's outputs[] + the node's current arguments.
154
+ *
155
+ * List outputs: only emit-mode entries are surfaced. Assign-mode entries route to
156
+ * existing variables and don't create new output slots in scope — they're validated
157
+ * as bindings, not reported as available outputs.
158
+ */
159
+ export function getNodeAvailableOutput(node: NodeData): Record<string, NodeOutput> {
160
+ const result: Record<string, NodeOutput> = {};
161
+
162
+ if (node.type === "FunctionCall") {
163
+ for (const ret of node.functionInfo.returns) {
164
+ result[ret.uid] = { name: ret.name, dataType: ret.dataType };
165
+ }
166
+ return result;
167
+ }
168
+
169
+ const def = NodeRegistry.getByType(node.type);
170
+ if (!def?.outputs) return result;
171
+
172
+ const args = node.arguments as Record<string, unknown>;
173
+ for (const out of def.outputs) {
174
+ if (out.type === "static") {
175
+ result[out.id] = {
176
+ name: out.id,
177
+ dataType: resolveStaticOutputDataType(out, node.arguments),
178
+ };
179
+ } else {
180
+ const entries = (args[out.id] as OutputDeclaration[] | undefined) ?? [];
181
+ for (const entry of entries) {
182
+ if (entry.mode === "emit") {
183
+ result[entry.uid] = { name: entry.name, dataType: entry.dataType };
184
+ }
185
+ }
186
+ }
187
+ }
188
+ return result;
189
+ }
190
+
191
+ /**
192
+ * Compute the effective outputs the node emits to variable scope: available outputs
193
+ * filtered to active emit bindings only (inactive bindings and active assign bindings
194
+ * contribute nothing — assign routes to an existing variable, inactive is discarded).
195
+ * Emit bindings carry a user-chosen name that overrides the output's default name (the id).
196
+ *
197
+ * Binding lookup:
198
+ * - Static outputs (incl. FunctionCall returns): `node.arguments[out.id]`
199
+ * - List outputs: each entry is already a declaration; emit entries contribute
200
+ * directly (no separate binding), assign entries contribute nothing
201
+ */
202
+ export function getNodeOutput(node: NodeData): Record<string, NodeOutput> {
203
+ const result: Record<string, NodeOutput> = {};
204
+ const args = node.arguments as Record<string, unknown>;
205
+
206
+ const applyStaticBinding = (key: string, defaultName: string, dataType: NodeOutput["dataType"], binding: OutputBinding | undefined) => {
207
+ // No binding = treat as default emit (the seeded shape). Otherwise honor active+mode.
208
+ if (!binding) {
209
+ result[key] = { name: defaultName, dataType };
210
+ return;
211
+ }
212
+ if (!binding.active) return;
213
+ if (binding.mode !== "emit") return;
214
+ result[key] = { name: binding.name, dataType };
215
+ };
216
+
217
+ if (node.type === "FunctionCall") {
218
+ for (const ret of node.functionInfo.returns) {
219
+ applyStaticBinding(ret.uid, ret.name, ret.dataType, getStaticBinding(args, ret.uid));
220
+ }
221
+ return result;
222
+ }
223
+
224
+ const def = NodeRegistry.getByType(node.type);
225
+ if (!def?.outputs) return result;
226
+
227
+ for (const out of def.outputs) {
228
+ if (out.type === "static") {
229
+ applyStaticBinding(out.id, out.id, resolveStaticOutputDataType(out, node.arguments), getStaticBinding(args, out.id));
230
+ } else {
231
+ const entries = (args[out.id] as OutputDeclaration[] | undefined) ?? [];
232
+ for (const entry of entries) {
233
+ if (entry.mode === "emit") {
234
+ result[entry.uid] = { name: entry.name, dataType: entry.dataType };
235
+ }
236
+ }
237
+ }
238
+ }
239
+ return result;
240
+ }
241
+
242
+ /**
243
+ * Compute external input requirements for a node instance (debug mode).
244
+ * Returns the hardware I/O values the node will read during execution.
245
+ */
246
+ export function getInput(node: NodeData): ExternalInput[] {
247
+ switch (node.type) {
248
+ case "ReadPin":
249
+ return [
250
+ { kind: "gpio", pinReference: node.arguments.pinReference, dataType: node.arguments.signalType === "digital" ? "bool" : "int" },
251
+ ];
252
+ case "SerialRead":
253
+ return [{ kind: "serial" }];
254
+ case "OnPinEdge":
255
+ return [{ kind: "gpio", pinReference: node.arguments.pinReference, dataType: "bool" }];
256
+ case "OnSerialReceive":
257
+ return [{ kind: "serial" }];
258
+ default:
259
+ return [];
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Determine whether a node is currently used as a tool input
265
+ * (i.e. its tool-input port has an incoming edge).
266
+ *
267
+ * Reads only the connectivity fields off each edge (`target`, `targetHandle`),
268
+ * so it takes the structural {@link Edge} rather than React Flow's `Edge` —
269
+ * keeping this (and its callers in serialization/diagnostics) headless. The
270
+ * editor's React Flow `Edge[]` is structurally assignable without an adapter.
271
+ *
272
+ * Editor-only connection rules (canPortAcceptEdge, getCompatibleNodeDefs,
273
+ * isValidConnection) live in workflow-builder's connectionRules they operate
274
+ * on React Flow `Node`/`Edge` and have no place in the headless core.
275
+ */
276
+ export function isNodeUsedAsTool(nodeId: string, nodeData: NodeData, edges: readonly Edge[]): boolean {
277
+ const ports = getPorts(nodeData);
278
+ const toolInputs = ports.input.filter((p) => p.type === "tool");
279
+ if (toolInputs.length === 0) return false;
280
+ return edges.some((e) => e.target === nodeId && toolInputs.some((p) => p.id === e.targetHandle));
281
+ }
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 ForestHub.
3
+
4
+ import { describe, it, expect } from "vitest";
5
+ import { serialize, deserialize } from "./serialization";
6
+ import type { Node } from "./Node";
7
+
8
+ // Drafts save on purpose: a required-but-unset argument must serialize as
9
+ // ABSENT — never as `undefined`, `""`, or (worst) `null` in the JSON file.
10
+
11
+ function makeNode(type: string, args: Record<string, unknown>): Node {
12
+ return { id: "n1", type, position: { x: 0, y: 0 }, arguments: args } as unknown as Node;
13
+ }
14
+
15
+ describe("serialize: draft (incomplete) nodes", () => {
16
+ it("omits a required-but-unset argument entirely", () => {
17
+ const api = serialize(makeNode("ReadPin", {}), false);
18
+ expect("arguments" in api && api.arguments && "pinReference" in api.arguments).toBe(false);
19
+ });
20
+
21
+ it("emits no null/undefined values in the JSON form", () => {
22
+ const api = serialize(makeNode("OnThreshold", {}), false);
23
+ const json = JSON.parse(JSON.stringify(api)) as { arguments?: Record<string, unknown> };
24
+ expect(Object.values(json.arguments ?? {})).not.toContain(null);
25
+ expect(json).toEqual({ id: "n1", type: "OnThreshold", position: { x: 0, y: 0 }, arguments: {} });
26
+ });
27
+
28
+ it("unset MqttPublish qos stays absent instead of NaN→null", () => {
29
+ const api = serialize(makeNode("MqttPublish", { channelReference: "ch1" }), false);
30
+ const json = JSON.parse(JSON.stringify(api)) as { arguments: Record<string, unknown> };
31
+ expect("qos" in json.arguments).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe("serialize: complete nodes", () => {
36
+ it("keeps configured required arguments", () => {
37
+ const api = serialize(makeNode("Delay", { delayMs: 250 }), false);
38
+ expect(api).toMatchObject({ arguments: { delayMs: 250 } });
39
+ });
40
+
41
+ it("maps MqttPublish qos from domain string to wire number", () => {
42
+ const api = serialize(makeNode("MqttPublish", { channelReference: "ch1", qos: "2" }), false);
43
+ expect(api).toMatchObject({ arguments: { qos: 2 } });
44
+ });
45
+
46
+ it("round-trips a configured node through deserialize", () => {
47
+ const node = makeNode("Ticker", { intervalValue: 5, intervalUnit: "seconds" });
48
+ const back = deserialize(serialize(node, false));
49
+ expect(back.arguments).toMatchObject({ intervalValue: 5, intervalUnit: "seconds" });
50
+ });
51
+ });